import * as dbCompanySetting from '../db/companysetting.js'; import { listSkillFiles } from './skill.js'; // #region Tool Prompts & Descriptions /** * Descriptions for all tools registered in the agent's toolbelt. * Used in defineTool({ description }) across src/tools/*. */ export const TOOL_DESCRIPTIONS = { dashback_request: 'Call the Dashback (QuixPOS) REST API with GET, POST, PUT, PATCH or DELETE. Paths are root-relative (e.g. "/article/2").', ask_user: 'Prompt user for missing decisions or parameters with a concise question and up to 3 options. Pauses the run until the user answers.', javascript_execute: 'Execute JS in an isolated worker sandbox using either inline `code` or a VFS applet. Use for computation, binary I/O and aggregation. Provide exactly one of `code` and `appletvfsfile`. ' + 'Applets are the preferred method when the first execution may throw: save a `.applet.js` file in the VFS so it can be inspected and further edited with `patch`, then call it by VFS path. ' + 'Applet files must start with `// @applet-input-schema {JSON Schema}` on line 1 and `// @applet-output-schema ` on line 2. ' + 'The input schema describes the GUI form passed as `applet_input`. The output line declares the RESULT COLUMNS as typed CSV: `name:type` pairs separated by `;` (e.g. `id:integer;name:string;active:boolean`); a bare `name` defaults to string, and the allowed types are string, integer, number, boolean and date. ' + 'Applets return typed CSV, NOT JSON: export a function that receives `applet_input` and returns a row object, or an array of row objects, whose keys are the declared columns (e.g. `return [{ id: 1, name: "Kaffee" }]`). Only declared columns are emitted, in header order; missing keys become empty cells. The tool result then carries `result` as a `;`-separated CSV string with a header row, plus `columns` (name/type) and `rows` (data row count). Returning a single object yields one data row. ' + 'Inline code: assign `module.exports` to return value (sync value or Promise; wrap async in IIFE: `module.exports = (async () => { ... })()`). ' + 'REST API: `await fetch("/path")` (authenticated). ' + 'VFS: directly use global `fs.readFileSync`/`writeFileSync` for binary artifacts/buffers/images (`image/` is read-only; paths must be relative paths, root is not allowed). VFS can host a static website (HTML, CSS, JS, images, etc.). ' + 'Embed generated images/links in markdown: `![alt](relative/path)`. Internet links use `https://...` or `hostname.com`/`hostname.com/filename`; a pure relative file with no dotted hostname is always a VFS relative path. Use find_files/read_file/patch for text. ' + 'All APIs are pre-injected globals: `fs`/`vfs` (virtual filesystem), `sharp` (image processing), `ExcelJS`/`exceljs` (spreadsheet processing with VFS support for readFile/writeFile/writeBuffer/load), `PDFDocument`/`PDFKit`/`pdfkit` (PDF generation with VFS support for toFile/toBuffer/pipe), WebCrypto `crypto` (`subtle`, `randomUUID`), and standard web globals. Do NOT use `require()` or `import` (disabled; all necessary APIs are already in global scope).', find_files: 'List VFS files. Root-relative forward-slash paths. ' + 'Set skills=true to list root-level ".SKILL" files (reusable instructions exempt from reset_vfs; read via read_file).', read_file: 'Read text file (text, CSV, JSON, markdown, …) from VFS. Root-relative forward-slash paths. ' + 'SKILL files return parsed title and markdown content. ' + 'Windowed via seek/limit (0-indexed lines). Binary files not supported (use analyze_image for images).', patch: 'Create, overwrite, or search-and-replace text in VFS files (the only write path for text files). Operations apply in order. ' + 'Mode: search null/omitted = create or overwrite (directories auto-created). ' + 'search provided = replace first verbatim occurrence (read file with read_file first). ' + 'SKILL files: root-only ".SKILL" (line 1 is title, or provide title field).', search_images: 'Search Google Images and download candidates to VFS as AVIF. Returns VFS paths for inspection via analyze_image.', analyze_image: 'Inspect 1-9 images via vision model. Always evaluate images with this tool before using them. Multiple paths render as a labelled comparison grid. ' + 'Paths: VFS-relative ("images/foo.avif") or DB ("image/"). Returns model evaluation.', web_search: 'Search Google via Serper.dev. Use timePeriod "day" or "week" for news/recent events. Returns 10 results per page.', url_get: 'Fetch and scrape web page content via Serper Scrape (text, markdown, metadata).', pdf_read: 'Read PDF/text documents as layout text ("txt") or render pages to VFS PNG images ("png", stored in "images/").', reset_vfs: 'Clear VFS files, images, and artifacts. Preserves SKILL files by default.', rm: 'Delete single VFS file (e.g. specific artifact or SKILL file). Irreversible.', apisuggestionsorissues: 'Report missing API endpoint, feature request, bug, or issue for triage.', search_articles: 'Text search articles by name, brand, EAN, SKU, or supplier SKU. ' + 'Returns window of full articles (default 3, max 20), offset, and total count. Ordered alphabetically by name.', edit_image: 'Generate or edit images using the configured company image model (modelslugimage). ' + 'Supports reference image attachments fed in low or high quality, output size tier, ' + 'aspect ratio, and saving directly to a specified VFS output path (always saved as AVIF).', ean_checksum: 'Validate EAN/GTIN/UPC barcode check digits or calculate the missing check digit for barcodes ' + '(EAN-13, EAN-8, UPC-A, GTIN-14). Strips spaces and hyphens automatically.', net_gross_calc: 'Convert between net and gross prices (net to gross, gross to net) with VAT/tax rate (default 19%). ' + 'Computes 2-decimal rounded display values and 6-decimal high-precision net values for API article creation.', set_user_route: 'Navigate the user to a frontend route. Use this when the user says "show me the article" or asks to see, open, or view a specific page or entity (e.g. an article detail page, orders, quotes, deliveries, or customer). Use a path from the ROUTE MATRIX in the system prompt.', }; // #region Tool Parameter Descriptions /** * Descriptions for tool input parameters across src/tools/*. */ export const TOOL_PARAMETER_DESCRIPTIONS = { dashback_request: { note: 'A note explaining what this request is doing (or null). This note will be displayed to the user while the request is executing.', method: 'HTTP method.', path: 'Endpoint path (e.g. "/article/2").', query: 'Query parameters object or null.', body: 'JSON request payload string or null.', }, ask_user: { question: 'Question prompt.', options: 'Up to 3 option labels, or null.', }, javascript_execute: { note: 'A note explaining what this code is doing (or null). This note will be displayed to the user while the code is executing.', code: 'Optional inline JavaScript code. Provide exactly one of this and appletvfsfile. Assign result to `module.exports`. Async code must use IIFE. Do not use require() or import.', appletvfsfile: 'Optional VFS-relative path to a .applet.js file. Provide exactly one of this and code. The file must begin with the two applet metadata lines (`@applet-input-schema` JSON Schema, then `@applet-output-schema` typed CSV columns like `id:integer;name:string`).', applet_input: 'Input values passed to the exported function when running appletvfsfile. Omit to pass an empty object. The applet returns typed CSV rows mapped onto its `@applet-output-schema` columns.', timeout: 'Execution timeout in ms or null. Default 20000, max 60000.', max_output: 'Max stdout chars or null. Default 20000.', }, find_files: { dirname: 'VFS directory to list or null. Default "." (root). Must be "." or empty if skills=true.', recursive: 'Walk subdirectories or null. Default false.', skills: 'List only root SKILL files with parsed titles or null. Overrides dirname/recursive. Default false.', seek: 'Pagination offset or null. Default 0.', limit: 'Max entries to return or null. Default 50, max 500.', maxDepth: 'Max recursive directory depth or null. Default 3.', }, read_file: { filename: 'VFS file path.', seek: 'Line offset (0-indexed) or null. Default 0.', limit: 'Max lines to return or null. Default 400, max 5000.', }, patch: { operations: 'Ordered patch operations.', file: 'VFS path (e.g. "notes.md", "pipeline.SKILL"). SKILL files must be root-level (no "/").', search: 'Exact text to replace. null/omitted = overwrite or create file.', replace: 'Replacement text.', title: 'Optional line-1 title for SKILL files or null.', skiplines: 'Lines to skip before searching or null.', }, search_images: { note: 'A note explaining what this image search is doing (or null). This note will be displayed to the user while executing.', query: 'Search query.', count: 'Candidate count or null. Default 4, max 9.', gl: 'Country code or null. Default "de".', hl: 'Language code or null. Default "de".', }, analyze_image: { note: 'A note explaining what this image inspection is doing (or null). This note will be displayed to the user while the images are being analyzed.', paths: '1–9 VFS paths or DB paths ("image/").', prompt: 'Inspection criteria or question, or null.', grid: 'Grid layout or null for auto.', }, web_search: { note: 'A note explaining what this search is doing (or null). This note will be displayed to the user while the search is executing.', query: 'Search query.', timePeriod: 'Time filter: "day", "week", "month", or "year", or null.', gl: 'Country code or null. Default "de".', hl: 'Language code or null. Default "de".', page: 'Result page number (10/page) or null. Default 1.', }, url_get: { url: 'Target URL.', }, pdf_read: { path: 'VFS path or HTTP(S) URL.', format: 'Output format: "txt" (layout text), "png" (page images), "both", or null. Default "txt".', page: '1-based page number or null for all pages.', dpi: 'PNG render DPI or null. Default 150.', }, reset_vfs: { include_skills: 'Delete SKILL files too (or null). Default false.', }, rm: { filename: 'VFS file path to delete.', }, apisuggestionsorissues: { text: 'Issue or request details.', type: 'Report category or null.', title: 'Short summary or null.', }, search_articles: { searchquery: 'Words that must all match across name, brand, EAN, SKU, or supplier SKU (AND logic, order-independent) or null.', excludewords: 'Words that if all match exclude the article (AND logic, order-independent) or null.', offset: 'Pagination offset or null. Default 0.', limit: 'Result window size (1–20) or null. Default 3.', }, edit_image: { note: 'A note explaining what this image generation or edit is doing (or null). This note will be displayed to the user while the image is being generated or edited.', prompt: 'Description of the image to generate, or edit/transformation instructions when attachments are provided.', attachments: 'Optional reference image(s) for editing/image-to-image or null. Omit for text-to-image generation.', attachment_vfsfilename: 'VFS path (e.g. "images/sample.png") or database path ("image/").', attachment_high: 'Feed image in high quality/resolution if true; low quality/resolution if false or null (default).', size: 'Resolution tier (3 choices): "1K" (standard, default), "2K" (high quality), "4K" (ultra high), or null.', ratio: 'Aspect ratio (3 choices): "1:1" (square, default), "4:3" (landscape), "3:4" (portrait), or null.', vfsoutputfilename: 'VFS path where the output image should be saved (e.g. "images/edited.avif") or null. Defaults to "images/generated-.avif". Output is always converted to AVIF.', }, ean_checksum: { note: 'A note explaining what this EAN checksum check is doing (or null). This note will be displayed to the user while executing.', code: 'Barcode number to validate or complete (e.g. 12 digits for EAN-13 check digit calculation, or 13 digits for EAN-13 validation).', action: 'Optional action: "auto" (default: infer from length), "validate" (check last digit), "calculate" (compute check digit for the given payload), or null.', }, net_gross_calc: { note: 'A note explaining what this price calculation is doing. This note will be displayed to the user while executing.', gross: 'Gross price (incl. VAT) or null. When provided, computes net price and tax amount.', net: 'Net price (excl. VAT) or null. When provided, computes gross price and tax amount.', taxRate: 'Tax/VAT rate percentage (e.g. 19 for 19%, 7 for 7%) or null. Default 19.', amount: 'Generic price amount or null when used with "from" ("gross" or "net").', from: 'Direction when using "amount": "gross" (default) or "net".', precision: 'Decimal precision for net price API value or null (default 6, between 2 and 8).', items: 'Optional array of prices or objects for batch calculation: [19.99, 49.95] or [{ gross: 19.99, taxRate: 7 }].', }, set_user_route: { route: 'Target frontend route path according to the Route Construction Matrix (e.g. "/artikel/ART-10042", "/kategorien/elektronik/kabel", "/auftrag/1001").', }, }; export const PATCH_PARAMETER_DESCRIPTIONS = TOOL_PARAMETER_DESCRIPTIONS.patch; export const PATCH_PARAM_DESCRIPTIONS = TOOL_PARAMETER_DESCRIPTIONS.patch; // #endregion // #region Tool-specific Subprompts export const ANALYZE_IMAGE_VISION_COMPARE_PROMPT = 'Compare these images, judge visual quality, flag watermarks, text overlays or defects, and say which one is best and why.'; export const ANALYZE_IMAGE_VISION_DESCRIBE_PROMPT = 'Describe this image, judge its visual quality, flag any watermark, text overlay or defect, and say whether it is usable as a product or category image.'; // #endregion // #endregion // #region System Prompt Components export const SYSTEM_PROMPT_VFS_RULES = [ 'VFS & SKILLS:', '- Persistent per-company VFS; root-relative forward-slash paths (e.g. "labels/sheet1.png"). Can host a static website.', '- User uploads go to "attachments/…", tagged in messages as `[ATTACHMENT name= path= type=]`. Inspect with read_file (text/csv/json), pdf_read (PDF), analyze_image (images), javascript_execute + ExcelJS (spreadsheets).', '- Markdown links: internet links are "https://…" or dotted hostnames ("hostname.com/file"); anything else is a VFS path (`[download](report.csv)`).', '- SKILL files: root-only ".SKILL" markdown (first line = title). Discover via find_files({ skills: true }). ALWAYS read_file the matching skill BEFORE executing a covered task. Propose saving improved procedures as new SKILL files. Exempt from reset_vfs; delete via rm.', ].join('\n'); export const SYSTEM_PROMPT_RULES = [ 'RULES:', '- LABELS (/labeltemplate): Dashback JSON `{ "objects": [ ... ] }`. Do NOT use ZPL.', '- IMAGES: flow search_images → analyze_image({ paths, prompt }) → upload via fetch in javascript_execute. `image/` is READ-ONLY; write generated images elsewhere (e.g. "images/…").', '- OUTPUT: Markdown + emoji. Render VFS images inline `![alt](artifacts/example.png)`; other files (PDF, JSON, CSV) need download links.', '- NAVIGATION: when the user asks to view/open a page or entity, call set-user-route with a path from the matrix.', '- API: routes are not inlined. BEFORE dashback_request, read_file the api-
.SKILL for exact params and response shapes. `api-binary-streaming-routes.SKILL` covers fetch()-only routes.', '- T* TYPES: `T` (e.g. T2) is shorthand for a reusable object shape. Ids are GLOBAL: the same T always means the same shape in every skill. Each api-
.SKILL only lists, in its "Reusable types" block, the T it uses (plus the ones those reference), so a skill shows a subset of the registry, and a T may appear in several skills with an identical definition.', ].join('\n'); export const SYSTEM_PROMPT_BINARY_UPLOAD = [ 'BINARY IMAGE UPLOAD (fetch() in javascript_execute, image/avif buffer body):', '- POST /article/{sku}/image -> 201 { image: { picId, sort, full: { w, h, size }, thumb: { w, h }, hash, thumbHash } }', '- POST /category/{id}/image -> 201 { category: { id, thumbW, thumbH, thumbHash } }', '- POST /brand/{id}/image -> 201 { brand: { id, fullHash, thumbW, thumbH, thumbHash, pics: [...] } }', ].join('\n'); export const SYSTEM_PROMPT_ROUTE_MATRIX = [ 'ROUTE MATRIX (client routes German, API sections English; skills = api-
):', '| Destination | Path | Example | API sections |', '|:---|:---|:---|:---|', '| Article Search | `/artikel?target={query}&filter={filter}&offset={n}` | `/artikel?target=kabel` | article, category, brand |', '| Article Detail | `/artikel/${encodeURIComponent(sku)}` | `/artikel/ART-10042` | article, category, brand, attributename, propertyname, unit, supplier, printer, labeltemplate, ledger |', '| Warehouse / Stock | `/artikel/lager` | `/artikel/lager` | warehouse, stock, ledger |', '| Brands | `/artikel/marken` | `/artikel/marken` | brand |', '| Label Templates | `/artikel/vorlagen` | `/artikel/vorlagen` | labeltemplate, printer |', '| Attributes | `/artikel/merkmale` | `/artikel/merkmale` | attributename, attributevalue, propertyname |', '| Category Root | `/kategorien` | `/kategorien` | category |', '| Category Subtree | `/kategorien/${slug1}/${slug2}` (encoded slugs) | `/kategorien/elektronik/kabel` | category, article |', '| Delivery List | `/lieferung` | `/lieferung` | delivery |', '| New Delivery | `/lieferung/neu` | `/lieferung/neu` | delivery, supplier, article, warehouse, stock |', '| Delivery Detail | `/lieferung/${deliveryId}` | `/lieferung/42` | delivery, supplier, ledger |', '| Suppliers | `/lieferung/lieferanten` | `/lieferung/lieferanten` | supplier |', '| Quote List | `/auftrag` | `/auftrag` | quote |', '| New Quote | `/auftrag/neu` | `/auftrag/neu` | quote, entity, article |', '| Quote Detail | `/auftrag/${quoteId}` | `/auftrag/1001` | quote, entity |', '| Order List | `/auftrag/bestellung` | `/auftrag/bestellung` | order |', '| New Order | `/auftrag/bestellung/neu` | `/auftrag/bestellung/neu` | order, entity, article |', '| Order Detail | `/auftrag/bestellung/${orderId}` | `/auftrag/bestellung/2005` | order, entity, account, payment, shipping, package |', '| Customer List | `/kunde` | `/kunde` | entity |', '| New Customer | `/kunde/neu` | `/kunde/neu` | entity, customergroup, number |', '| Customer Detail | `/kunde/${entityId}` | `/kunde/312` | entity, customergroup |', '| Customer Groups | `/kunde/gruppen` | `/kunde/gruppen` | customergroup |', '| Article History | `/infos/articlehistory` | `/infos/articlehistory` | article (`GET /article/{sku}/history`) |', '| Global History | `/infos/globalhistory` | `/infos/globalhistory` | company (`GET /company/history`) |', '| Statistics | `/infos/statistics` | `/infos/statistics` | none; aggregate from order, delivery, ledger |', ].join('\n'); // #endregion // #region Skill Index /** * Builds the SKILL index block for the system prompt. * * Listing the skills inline means the model knows what guidance exists BEFORE it decides what * to do, so it can reach for `read_file` on the right skill instead of guessing a procedure * from scratch. Only the filename, title and line count are included — the body is read on * demand, which keeps the prompt small no matter how many skills exist. * * @param {string} vfsRoot * @returns {string} */ export function buildSkillIndex(vfsRoot) { if (!vfsRoot) { return 'AVAILABLE SKILLS: unknown (the virtual filesystem could not be read this turn). Call find_files({ skills: true }) to check.'; } let skills; try { skills = listSkillFiles(vfsRoot); } catch (err) { console.warn('conversation: failed to list skill files:', err.message); return 'AVAILABLE SKILLS: could not be read this turn. Call find_files({ skills: true }) to check.'; } if (skills.length === 0) { return [ 'AVAILABLE SKILLS: none. This company has no SKILL files yet.', ].join('\n'); } const width = Math.max(...skills.map((s) => s.path.length)); return [ `AVAILABLE SKILLS (${skills.length}): stored in the VFS root as flat .SKILL files.`, 'Before starting a task that any of these cover, read the matching skill with read_file and follow it.', '', ...skills.map((s) => ` ${s.path.padEnd(width)} ${String(s.lines).padStart(4)} lines | ${s.title}`), ].join('\n'); } // #endregion // #region System Prompt /** * System prompt: base instructions plus the generated API routes reference * (docs/api/routes.md), read on each run so it stays current. * * @param {string} vfsRoot Resolved VFS root for this user's company, used to list SKILL files. * @param {number} [googleAuthId] * @returns {Promise} */ export async function buildSystemPrompt(vfsRoot, googleAuthId) { let prefix = ''; let suffix = ''; if (googleAuthId) { try { const prefixSetting = await dbCompanySetting.getByKey(googleAuthId, 'systemprompt-prefix', { maskWriteonly: false }); prefix = prefixSetting?.value?.trim() || ''; } catch (err) { console.error('conversation: failed to read systemprompt-prefix:', err.message); } try { const suffixSetting = await dbCompanySetting.getByKey(googleAuthId, 'systemprompt-suffix', { maskWriteonly: false }); suffix = suffixSetting?.value?.trim() || ''; } catch (err) { console.error('conversation: failed to read systemprompt-suffix:', err.message); } } const today = new Date().toISOString().split('T')[0]; const basePrompt = [ 'QuixPOS Article Management System API Assistant', `Date: ${today}`, '[TESTSYSTEM, destructive commands are allowed, main purpose of this system is to find bugs]', '', SYSTEM_PROMPT_VFS_RULES, '', buildSkillIndex(vfsRoot), '', SYSTEM_PROMPT_RULES, '', SYSTEM_PROMPT_BINARY_UPLOAD, '', SYSTEM_PROMPT_ROUTE_MATRIX, ].join('\n'); const sections = []; if (prefix) sections.push(prefix); sections.push(basePrompt); if (suffix) sections.push(suffix); return sections.join('\n\n'); } // #endregion