379 lines
15 KiB
JavaScript
379 lines
15 KiB
JavaScript
import * as dbCompanySetting from '../db/companysetting.js';
|
|
import { listSkillFiles } from './skill.js';
|
|
|
|
// #region System Prompt
|
|
|
|
export const SYSTEM_PROMPT_RULES = [
|
|
'RULES & VFS:',
|
|
'- VFS: Persistent root-relative paths ("labels/sheet1.png"). Uploads land in "attachments/..." ([ATTACHMENT name= path= type=]); inspect via read_file, pdf_read, analyze_image, or javascript_execute + ExcelJS. Can host static sites.',
|
|
'- Links: Web links use "https://..." or domains; relative paths format as [label](relative/path). Inline images: . Other files require download links.',
|
|
'- SKILLS: Flat root "<slug>.SKILL" markdown files. ALWAYS read_file matching skill BEFORE executing covered tasks. Exempt from reset_vfs.',
|
|
'- API: Routes are not inlined. BEFORE dashback_request, read_file the api-<section>.SKILL for parameters and response structures. api-binary-streaming-routes.SKILL covers fetch()-only upload/stream endpoints.',
|
|
'- Types: T<n> refers to globally shared object schemas across skills.',
|
|
'- Images: Pipeline: search_images -> analyze_image -> upload via fetch() in javascript_execute. "image/" is read-only; write generated assets to "images/...".',
|
|
'- Labels: Use Dashback JSON `{ "objects": [ ... ] }` for /labeltemplate. Never use ZPL.',
|
|
'- Navigation: Call set_user_route when the user asks to open or view any page/entity.',
|
|
].join('\n');
|
|
|
|
export const SYSTEM_PROMPT_ROUTE_MATRIX = [
|
|
'ROUTE MATRIX (client German, API skills = api-<section>):',
|
|
'- Search: /artikel?target={query}&filter={filter}&offset={n} -> article, category, brand',
|
|
'- Article Detail: /artikel/${sku} -> article, category, brand, attributename, propertyname, unit, supplier, printer, labeltemplate, ledger',
|
|
'- Warehouse / Stock: /artikel/lager -> warehouse, stock, ledger',
|
|
'- Brands: /artikel/marken -> brand',
|
|
'- Label Templates: /artikel/vorlagen -> labeltemplate, printer',
|
|
'- Attributes: /artikel/merkmale -> attributename, attributevalue, propertyname',
|
|
'- Categories: /kategorien | /kategorien/${slug1}/${slug2} -> category, article',
|
|
'- Deliveries: /lieferung | /lieferung/neu | /lieferung/${deliveryId} | /lieferung/lieferanten -> delivery, supplier, article, warehouse, stock, ledger',
|
|
'- Quotes: /auftrag | /auftrag/neu | /auftrag/${quoteId} -> quote, entity, article',
|
|
'- Orders: /auftrag/bestellung | /auftrag/bestellung/neu | /auftrag/bestellung/${orderId} -> order, entity, account, payment, shipping, package',
|
|
'- Customers: /kunde | /kunde/neu | /kunde/${entityId} | /kunde/gruppen -> entity, customergroup, number',
|
|
'- History & Stats: /infos/articlehistory | /infos/globalhistory | /infos/statistics -> article, company, order, delivery, ledger',
|
|
].join('\n');
|
|
|
|
// #endregion
|
|
|
|
// #region Tool Prompts
|
|
|
|
/**
|
|
* Descriptions for all tools registered in the agent's toolbelt.
|
|
* Used in defineTool({ description }) across src/tools/*.
|
|
*/
|
|
export const TOOL_DESCRIPTIONS = {
|
|
dashback_request:
|
|
'Call Dashback (QuixPOS) REST API with GET/POST/PUT/PATCH/DELETE. Root-relative paths (e.g. "/article/2").',
|
|
|
|
ask_user:
|
|
'Prompt user for missing parameters with a concise question and up to 3 options. Pauses run until answered.',
|
|
|
|
javascript_execute:
|
|
'Execute JS in an isolated sandbox via inline `code` (assign to `module.exports`) or `appletvfsfile` ' +
|
|
'(.applet.js returning typed CSV per @applet-input-schema, @applet-output-schema, and @applet-description; details in applet.SKILL). ' +
|
|
'Pre-injected globals: fs/vfs, sharp, ExcelJS, PDFDocument, crypto, and fetch("/path"). No require/import.',
|
|
|
|
find_files:
|
|
'List VFS files (root-relative paths). Set skills=true to list root-level "<slug>.SKILL" files.',
|
|
|
|
read_file:
|
|
'Read text file (text, CSV, JSON, markdown, SKILL) from VFS with seek/limit pagination. Binary not supported.',
|
|
|
|
patch:
|
|
'Create, overwrite, or search-and-replace text in VFS files. Operations apply in order. Omit search to create/overwrite.',
|
|
|
|
search_images:
|
|
'Search Google Images and download candidates to VFS as AVIF. Inspect candidates via analyze_image.',
|
|
|
|
analyze_image:
|
|
'Inspect 1-9 images via vision model. Paths: VFS-relative ("images/foo.avif") or DB ("image/<hash>").',
|
|
|
|
web_search:
|
|
'Search Google via Serper.dev. Use timePeriod "day" or "week" for news/recent events. 10 results/page.',
|
|
|
|
url_get:
|
|
'Fetch and scrape web page content (text, markdown, metadata) via Serper Scrape.',
|
|
|
|
pdf_read:
|
|
'Read PDF/text documents as layout text ("txt") or render pages to VFS PNG images ("png").',
|
|
|
|
reset_vfs:
|
|
'Clear VFS files, images, and artifacts. Preserves SKILL files unless include_skills=true.',
|
|
|
|
rm:
|
|
'Permanently delete a single VFS file or SKILL file.',
|
|
|
|
apisuggestionsorissues:
|
|
'Report missing API endpoint, feature request, bug, or issue for triage.',
|
|
|
|
search_articles:
|
|
'Search articles by name, brand, EAN, SKU, or supplier SKU (AND logic, ordered by name).',
|
|
|
|
edit_image:
|
|
'Generate or edit images via a vision model. Supports reference image attachments, size tier, ratio, and VFS AVIF output.',
|
|
|
|
ean_checksum:
|
|
'Validate or calculate check digits for EAN-13, EAN-8, UPC-A, and GTIN-14 barcodes.',
|
|
|
|
net_gross_calc:
|
|
'Convert net/gross prices with VAT rate (default 19%). Returns rounded display values and 6-decimal net API values.',
|
|
|
|
set_user_route:
|
|
'Navigate user to a frontend route when asked to view/open an entity or page (see ROUTE MATRIX in prompt).',
|
|
|
|
subagent:
|
|
'Delegate a task to an isolated subagent. Pre-loads requested SKILL files into its ' +
|
|
'system prompt and returns a compact summary of tool executions alongside the final output. ' +
|
|
'Subagents can call all tools you can call except subagent, ask_user, and set_user_route. ' +
|
|
'To efficiently communicate with subagents, store the input and output data in VFS files ' +
|
|
'and provide these paths in the subagent instructions.',
|
|
|
|
draft_delivery_manifest:
|
|
'Stores extracted invoice/delivery-note data: header metadata (basedata), and product line items. Each component is strictly optional.',
|
|
};
|
|
|
|
// #endregion
|
|
|
|
// #region Tool Parameters
|
|
|
|
const NOTE_DESC = 'Status message shown to the user during execution.';
|
|
|
|
/**
|
|
* Descriptions for tool input parameters across src/tools/*.
|
|
*/
|
|
export const TOOL_PARAMETER_DESCRIPTIONS = {
|
|
dashback_request: {
|
|
note: NOTE_DESC,
|
|
method: 'HTTP method.',
|
|
path: 'Endpoint path (e.g. "/article/2").',
|
|
query: 'Query parameters object or null.',
|
|
body: 'JSON payload string or null.',
|
|
},
|
|
|
|
ask_user: {
|
|
question: 'Question prompt.',
|
|
options: 'Up to 3 option labels, or null.',
|
|
},
|
|
|
|
javascript_execute: {
|
|
note: NOTE_DESC,
|
|
code: 'Inline JS code. Assign result to `module.exports`. Wrap async in IIFE. Mutually exclusive with appletvfsfile.',
|
|
appletvfsfile: 'VFS path to .applet.js file. Mutually exclusive with code.',
|
|
applet_input: 'Input values passed to applet export function. Returns typed CSV rows.',
|
|
timeout: 'Timeout in ms (default 20000, max 60000).',
|
|
max_output: 'Max stdout chars (default 20000).',
|
|
},
|
|
|
|
find_files: {
|
|
dirname: 'Directory to list (default "."). Must be "." if skills=true.',
|
|
recursive: 'Walk subdirectories (default false).',
|
|
skills: 'List only root SKILL files with parsed titles (default false).',
|
|
seek: 'Pagination offset (default 0).',
|
|
limit: 'Max entries (default 50, max 500).',
|
|
maxDepth: 'Max recursive depth (default 3).',
|
|
},
|
|
|
|
read_file: {
|
|
filename: 'VFS file path.',
|
|
seek: '0-indexed line offset (default 0).',
|
|
limit: 'Max lines to return (default 400, max 5000).',
|
|
},
|
|
|
|
patch: {
|
|
operations: 'Ordered patch operations.',
|
|
file: 'VFS path. SKILL files must be root-level.',
|
|
search: 'Exact text to replace. Omit to overwrite or create file.',
|
|
replace: 'Replacement text.',
|
|
title: 'Line-1 title for SKILL files.',
|
|
skiplines: 'Lines to skip before search.',
|
|
},
|
|
|
|
search_images: {
|
|
note: NOTE_DESC,
|
|
query: 'Search query.',
|
|
count: 'Candidate count (default 4, max 9).',
|
|
gl: 'Country code (default "de").',
|
|
hl: 'Language code (default "de").',
|
|
},
|
|
|
|
analyze_image: {
|
|
note: NOTE_DESC,
|
|
paths: '1-9 VFS paths or DB paths ("image/<hash>").',
|
|
prompt: 'Inspection criteria or question.',
|
|
grid: 'Grid layout or null for auto.',
|
|
},
|
|
|
|
web_search: {
|
|
note: NOTE_DESC,
|
|
query: 'Search query.',
|
|
timePeriod: 'Time filter: "day" | "week" | "month" | "year".',
|
|
gl: 'Country code (default "de").',
|
|
hl: 'Language code (default "de").',
|
|
page: 'Result page number (10/page, default 1).',
|
|
},
|
|
|
|
url_get: {
|
|
url: 'Target URL.',
|
|
},
|
|
|
|
pdf_read: {
|
|
path: 'VFS path or HTTP(S) URL.',
|
|
format: '"txt" (layout text), "png" (page images), or "both" (default "txt").',
|
|
page: '1-based page number, or null for all pages.',
|
|
dpi: 'Render DPI for PNG (default 150).',
|
|
},
|
|
|
|
reset_vfs: {
|
|
include_skills: 'Delete SKILL files too (default false).',
|
|
},
|
|
|
|
rm: {
|
|
filename: 'VFS file path to delete.',
|
|
},
|
|
|
|
apisuggestionsorissues: {
|
|
text: 'Issue or request details.',
|
|
type: 'Report category.',
|
|
title: 'Short summary.',
|
|
},
|
|
|
|
search_articles: {
|
|
searchquery: 'Keywords matching name, brand, EAN, SKU, or supplier SKU (AND logic).',
|
|
excludewords: 'Keywords to exclude (AND logic).',
|
|
offset: 'Pagination offset (default 0).',
|
|
limit: 'Window size 1-20 (default 3).',
|
|
},
|
|
|
|
edit_image: {
|
|
note: NOTE_DESC,
|
|
prompt: 'Prompt for text-to-image or edit/transformation instructions.',
|
|
attachments: 'Reference image attachments for img2img.',
|
|
attachment_vfsfilename: 'VFS path or DB path ("image/<hash>").',
|
|
attachment_high: 'High resolution input if true (default false).',
|
|
size: 'Resolution tier: "1K" (default), "2K", "4K".',
|
|
ratio: 'Aspect ratio: "1:1" (default), "4:3", "3:4".',
|
|
vfsoutputfilename: 'Target VFS path (always saved as AVIF). Defaults to "images/generated-<timestamp>.avif".',
|
|
},
|
|
|
|
ean_checksum: {
|
|
note: NOTE_DESC,
|
|
code: 'Barcode number to validate or complete.',
|
|
action: '"auto" (default), "validate", or "calculate".',
|
|
},
|
|
|
|
net_gross_calc: {
|
|
note: NOTE_DESC,
|
|
gross: 'Gross price (incl. VAT).',
|
|
net: 'Net price (excl. VAT).',
|
|
taxRate: 'VAT percentage (default 19).',
|
|
amount: 'Price amount when paired with "from".',
|
|
from: '"gross" (default) or "net".',
|
|
precision: 'Net price API decimal precision 2-8 (default 6).',
|
|
items: 'Array of prices or calculation objects for batch processing.',
|
|
},
|
|
|
|
set_user_route: {
|
|
route: 'Frontend route from Route Matrix (e.g. "/artikel/ART-10042", "/auftrag/1001").',
|
|
},
|
|
|
|
subagent: {
|
|
note: NOTE_DESC,
|
|
task: 'Specific instructions and goal for the subagent.',
|
|
skills: 'Array of SKILLs (e.g. ["api-article.SKILL", "label.SKILL"]) to inject directly into the subagent system prompt.',
|
|
conversation_id: 'Existing subagent conversation ID to follow up on, or null to create a new one.',
|
|
max_iterations: 'Safety bound on tool loops (min 10, default 20, max 50).',
|
|
},
|
|
|
|
draft_delivery_manifest: {
|
|
basedata: 'Header/metadata. Only include fields that are currently missing or changed.',
|
|
date: 'Document date (ISO 8601).',
|
|
lieferant_id: 'Lieferant ID.',
|
|
lieferantName: 'Lieferant name.',
|
|
manifestNr: 'Lieferscheinnummer or Rechnungsnummer.',
|
|
manifestType: 'Document type.',
|
|
items: 'New product line items.',
|
|
numberOfItemsToAdd: 'Number of NEW line items being added in this call.',
|
|
items_list: 'Invoice/delivery product line items.',
|
|
position: 'Position.',
|
|
count: 'Quantity (Menge).',
|
|
vpe: 'Packaging unit size (VPE). Default: 1.',
|
|
articleName: 'Article name.',
|
|
liefArtNr: 'Supplier article number.',
|
|
mwst: 'VAT rate; null if missing.',
|
|
eknetto: 'Net price per vpe in euro cents; null if missing.',
|
|
},
|
|
};
|
|
|
|
export const PATCH_PARAMETER_DESCRIPTIONS = TOOL_PARAMETER_DESCRIPTIONS.patch;
|
|
export const ANALYZE_IMAGE_VISION_COMPARE_PROMPT =
|
|
'Compare these images, evaluate visual quality, flag watermarks/overlays/defects, and state the best candidate with rationale.';
|
|
export const ANALYZE_IMAGE_VISION_DESCRIBE_PROMPT =
|
|
'Describe this image, evaluate visual quality, flag watermarks/overlays/defects, and verify suitability as a product/category image.';
|
|
|
|
// #endregion
|
|
|
|
// #region Skill Index
|
|
|
|
/**
|
|
* Builds the SKILL index block for the system prompt.
|
|
*
|
|
* @param {string} vfsRoot
|
|
* @returns {string}
|
|
*/
|
|
export function buildSkillIndex(vfsRoot) {
|
|
if (!vfsRoot) {
|
|
return 'AVAILABLE SKILLS: unknown. Call find_files({ skills: true }) to verify.';
|
|
}
|
|
|
|
let skills;
|
|
try {
|
|
skills = listSkillFiles(vfsRoot);
|
|
} catch (err) {
|
|
console.warn('conversation: failed to list skill files:', err.message);
|
|
return 'AVAILABLE SKILLS: unreadable this turn. Call find_files({ skills: true }) to verify.';
|
|
}
|
|
|
|
if (skills.length === 0) {
|
|
return 'AVAILABLE SKILLS: none.';
|
|
}
|
|
|
|
return [
|
|
`AVAILABLE SKILLS (${skills.length}): stored in VFS root. Read with read_file before executing covered tasks.`,
|
|
...skills.map((s) => `- ${s.path} (${s.lines} lines): ${s.title}`),
|
|
].join('\n');
|
|
}
|
|
|
|
// #endregion
|
|
|
|
// #region Factory
|
|
|
|
/**
|
|
* System prompt: base instructions and contextual index.
|
|
*
|
|
* @param {string} vfsRoot Resolved VFS root for this user's company, used to list SKILL files.
|
|
* @param {number} [googleAuthId]
|
|
* @returns {Promise<string>}
|
|
*/
|
|
export async function buildSystemPrompt(vfsRoot, googleAuthId) {
|
|
let prefix = '';
|
|
let suffix = '';
|
|
|
|
if (googleAuthId) {
|
|
const [prefixRes, suffixRes] = await Promise.allSettled([
|
|
dbCompanySetting.getByKey(googleAuthId, 'systemprompt-prefix', { maskWriteonly: false }),
|
|
dbCompanySetting.getByKey(googleAuthId, 'systemprompt-suffix', { maskWriteonly: false }),
|
|
]);
|
|
|
|
if (prefixRes.status === 'fulfilled') {
|
|
prefix = prefixRes.value?.value?.trim() || '';
|
|
} else {
|
|
console.error('conversation: failed to read systemprompt-prefix:', prefixRes.reason?.message);
|
|
}
|
|
|
|
if (suffixRes.status === 'fulfilled') {
|
|
suffix = suffixRes.value?.value?.trim() || '';
|
|
} else {
|
|
console.error('conversation: failed to read systemprompt-suffix:', suffixRes.reason?.message);
|
|
}
|
|
}
|
|
|
|
const today = new Date().toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' });
|
|
const basePrompt = [
|
|
'QuixPOS Article Management System API Assistant',
|
|
'[TESTSYSTEM: destructive commands allowed; primary goal is finding bugs]',
|
|
'---',
|
|
SYSTEM_PROMPT_RULES,
|
|
'---',
|
|
SYSTEM_PROMPT_ROUTE_MATRIX,
|
|
'---',
|
|
`Date: ${today}`,
|
|
'---',
|
|
buildSkillIndex(vfsRoot),
|
|
].join('\n');
|
|
|
|
const sections = [];
|
|
if (prefix) sections.push(prefix);
|
|
sections.push(basePrompt);
|
|
if (suffix) sections.push(suffix);
|
|
return sections.join('\n\n');
|
|
}
|
|
|
|
// #endregion
|