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: ![alt](images/path.avif). Other files require download links.', '- SKILLS: Flat root ".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-
.SKILL for parameters and response structures. api-binary-streaming-routes.SKILL covers fetch()-only upload/stream endpoints.', '- Types: T 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-
):', '- 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 ".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/").', 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/").', 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/").', 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-.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} */ 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