This commit is contained in:
seb
2026-10-11 14:23:47 +02:00
parent 1a51944e59
commit e72467da50

View File

@@ -3,88 +3,71 @@ import { listSkillFiles } from './skill.js';
// #region Tool Prompts & Descriptions // #region Tool Prompts & Descriptions
const NOTE_DESC = 'Status message shown to the user during execution.';
/** /**
* Descriptions for all tools registered in the agent's toolbelt. * Descriptions for all tools registered in the agent's toolbelt.
* Used in defineTool({ description }) across src/tools/*. * Used in defineTool({ description }) across src/tools/*.
*/ */
export const TOOL_DESCRIPTIONS = { export const TOOL_DESCRIPTIONS = {
dashback_request: dashback_request:
'Call the Dashback (QuixPOS) REST API with GET, POST, PUT, PATCH or DELETE. Paths are root-relative (e.g. "/article/2").', 'Call Dashback (QuixPOS) REST API with GET/POST/PUT/PATCH/DELETE. Root-relative paths (e.g. "/article/2").',
ask_user: 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.', 'Prompt user for missing parameters with a concise question and up to 3 options. Pauses run until answered.',
javascript_execute: 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`. ' + 'Execute JS in an isolated sandbox via inline `code` (assign to `module.exports`) or `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.js returning typed CSV per @applet-input-schema and @applet-output-schema; details in applet.SKILL). ' +
'Applet files must start with `// @applet-input-schema {JSON Schema}` on line 1 and `// @applet-output-schema <columns>` on line 2. ' + 'Pre-injected globals: fs/vfs, sharp, ExcelJS, PDFDocument, crypto, and fetch("/path"). No require/import.',
'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: find_files:
'List VFS files. Root-relative forward-slash paths. ' + 'List VFS files (root-relative paths). Set skills=true to list root-level "<slug>.SKILL" files.',
'Set skills=true to list root-level "<slug>.SKILL" files (reusable instructions exempt from reset_vfs; read via read_file).',
read_file: read_file:
'Read text file (text, CSV, JSON, markdown, …) from VFS. Root-relative forward-slash paths. ' + 'Read text file (text, CSV, JSON, markdown, SKILL) from VFS with seek/limit pagination. Binary not supported.',
'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: patch:
'Create, overwrite, or search-and-replace text in VFS files (the only write path for text files). Operations apply in order. ' + 'Create, overwrite, or search-and-replace text in VFS files. Operations apply in order. Omit search to create/overwrite.',
'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 "<slug>.SKILL" (line 1 is title, or provide title field).',
search_images: search_images:
'Search Google Images and download candidates to VFS as AVIF. Returns VFS paths for inspection via analyze_image.', 'Search Google Images and download candidates to VFS as AVIF. Inspect candidates via analyze_image.',
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. ' + 'Inspect 1-9 images via vision model. Paths: VFS-relative ("images/foo.avif") or DB ("image/<hash>").',
'Paths: VFS-relative ("images/foo.avif") or DB ("image/<hash>"). Returns model evaluation.',
web_search: web_search:
'Search Google via Serper.dev. Use timePeriod "day" or "week" for news/recent events. Returns 10 results per page.', 'Search Google via Serper.dev. Use timePeriod "day" or "week" for news/recent events. 10 results/page.',
url_get: url_get:
'Fetch and scrape web page content via Serper Scrape (text, markdown, metadata).', 'Fetch and scrape web page content (text, markdown, metadata) via Serper Scrape.',
pdf_read: pdf_read:
'Read PDF/text documents as layout text ("txt") or render pages to VFS PNG images ("png", stored in "images/").', 'Read PDF/text documents as layout text ("txt") or render pages to VFS PNG images ("png").',
reset_vfs: reset_vfs:
'Clear VFS files, images, and artifacts. Preserves SKILL files by default.', 'Clear VFS files, images, and artifacts. Preserves SKILL files unless include_skills=true.',
rm: rm:
'Delete single VFS file (e.g. specific artifact or SKILL file). Irreversible.', 'Permanently delete a single VFS file or SKILL file.',
apisuggestionsorissues: apisuggestionsorissues:
'Report missing API endpoint, feature request, bug, or issue for triage.', 'Report missing API endpoint, feature request, bug, or issue for triage.',
search_articles: search_articles:
'Text search articles by name, brand, EAN, SKU, or supplier SKU. ' + 'Search articles by name, brand, EAN, SKU, or supplier SKU (AND logic, ordered by name).',
'Returns window of full articles (default 3, max 20), offset, and total count. Ordered alphabetically by name.',
edit_image: edit_image:
'Generate or edit images using the configured company image model (modelslugimage). ' + 'Generate or edit images via modelslugimage. Supports reference image attachments, size tier, ratio, and VFS AVIF output.',
'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: ean_checksum:
'Validate EAN/GTIN/UPC barcode check digits or calculate the missing check digit for barcodes ' + 'Validate or calculate check digits for EAN-13, EAN-8, UPC-A, and GTIN-14 barcodes.',
'(EAN-13, EAN-8, UPC-A, GTIN-14). Strips spaces and hyphens automatically.',
net_gross_calc: net_gross_calc:
'Convert between net and gross prices (net to gross, gross to net) with VAT/tax rate (default 19%). ' + 'Convert net/gross prices with VAT rate (default 19%). Returns rounded display values and 6-decimal net API values.',
'Computes 2-decimal rounded display values and 6-decimal high-precision net values for API article creation.',
set_user_route: 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.', 'Navigate user to a frontend route when asked to view/open an entity or page (see ROUTE MATRIX in prompt).',
}; };
// #region Tool Parameter Descriptions // #region Tool Parameter Descriptions
@@ -94,11 +77,11 @@ export const TOOL_DESCRIPTIONS = {
*/ */
export const TOOL_PARAMETER_DESCRIPTIONS = { export const TOOL_PARAMETER_DESCRIPTIONS = {
dashback_request: { 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.', note: NOTE_DESC,
method: 'HTTP method.', method: 'HTTP method.',
path: 'Endpoint path (e.g. "/article/2").', path: 'Endpoint path (e.g. "/article/2").',
query: 'Query parameters object or null.', query: 'Query parameters object or null.',
body: 'JSON request payload string or null.', body: 'JSON payload string or null.',
}, },
ask_user: { ask_user: {
@@ -107,62 +90,60 @@ export const TOOL_PARAMETER_DESCRIPTIONS = {
}, },
javascript_execute: { 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.', note: NOTE_DESC,
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.', code: 'Inline JS code. Assign result to `module.exports`. Wrap async in IIFE. Mutually exclusive with appletvfsfile.',
appletvfsfile: appletvfsfile: 'VFS path to .applet.js file. Mutually exclusive with code.',
'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 applet export function. Returns typed CSV rows.',
applet_input: timeout: 'Timeout in ms (default 20000, max 60000).',
'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.', max_output: 'Max stdout chars (default 20000).',
timeout: 'Execution timeout in ms or null. Default 20000, max 60000.',
max_output: 'Max stdout chars or null. Default 20000.',
}, },
find_files: { find_files: {
dirname: 'VFS directory to list or null. Default "." (root). Must be "." or empty if skills=true.', dirname: 'Directory to list (default "."). Must be "." if skills=true.',
recursive: 'Walk subdirectories or null. Default false.', recursive: 'Walk subdirectories (default false).',
skills: 'List only root SKILL files with parsed titles or null. Overrides dirname/recursive. Default false.', skills: 'List only root SKILL files with parsed titles (default false).',
seek: 'Pagination offset or null. Default 0.', seek: 'Pagination offset (default 0).',
limit: 'Max entries to return or null. Default 50, max 500.', limit: 'Max entries (default 50, max 500).',
maxDepth: 'Max recursive directory depth or null. Default 3.', maxDepth: 'Max recursive depth (default 3).',
}, },
read_file: { read_file: {
filename: 'VFS file path.', filename: 'VFS file path.',
seek: 'Line offset (0-indexed) or null. Default 0.', seek: '0-indexed line offset (default 0).',
limit: 'Max lines to return or null. Default 400, max 5000.', limit: 'Max lines to return (default 400, max 5000).',
}, },
patch: { patch: {
operations: 'Ordered patch operations.', operations: 'Ordered patch operations.',
file: 'VFS path (e.g. "notes.md", "pipeline.SKILL"). SKILL files must be root-level (no "/").', file: 'VFS path. SKILL files must be root-level.',
search: 'Exact text to replace. null/omitted = overwrite or create file.', search: 'Exact text to replace. Omit to overwrite or create file.',
replace: 'Replacement text.', replace: 'Replacement text.',
title: 'Optional line-1 title for SKILL files or null.', title: 'Line-1 title for SKILL files.',
skiplines: 'Lines to skip before searching or null.', skiplines: 'Lines to skip before search.',
}, },
search_images: { search_images: {
note: 'A note explaining what this image search is doing (or null). This note will be displayed to the user while executing.', note: NOTE_DESC,
query: 'Search query.', query: 'Search query.',
count: 'Candidate count or null. Default 4, max 9.', count: 'Candidate count (default 4, max 9).',
gl: 'Country code or null. Default "de".', gl: 'Country code (default "de").',
hl: 'Language code or null. Default "de".', hl: 'Language code (default "de").',
}, },
analyze_image: { 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.', note: NOTE_DESC,
paths: '1–9 VFS paths or DB paths ("image/<hash>").', paths: '1–9 VFS paths or DB paths ("image/<hash>").',
prompt: 'Inspection criteria or question, or null.', prompt: 'Inspection criteria or question.',
grid: 'Grid layout or null for auto.', grid: 'Grid layout or null for auto.',
}, },
web_search: { 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.', note: NOTE_DESC,
query: 'Search query.', query: 'Search query.',
timePeriod: 'Time filter: "day", "week", "month", or "year", or null.', timePeriod: 'Time filter: "day" | "week" | "month" | "year".',
gl: 'Country code or null. Default "de".', gl: 'Country code (default "de").',
hl: 'Language code or null. Default "de".', hl: 'Language code (default "de").',
page: 'Result page number (10/page) or null. Default 1.', page: 'Result page number (10/page, default 1).',
}, },
url_get: { url_get: {
@@ -171,13 +152,13 @@ export const TOOL_PARAMETER_DESCRIPTIONS = {
pdf_read: { pdf_read: {
path: 'VFS path or HTTP(S) URL.', path: 'VFS path or HTTP(S) URL.',
format: 'Output format: "txt" (layout text), "png" (page images), "both", or null. Default "txt".', format: '"txt" (layout text), "png" (page images), or "both" (default "txt").',
page: '1-based page number or null for all pages.', page: '1-based page number, or null for all pages.',
dpi: 'PNG render DPI or null. Default 150.', dpi: 'Render DPI for PNG (default 150).',
}, },
reset_vfs: { reset_vfs: {
include_skills: 'Delete SKILL files too (or null). Default false.', include_skills: 'Delete SKILL files too (default false).',
}, },
rm: { rm: {
@@ -186,53 +167,47 @@ export const TOOL_PARAMETER_DESCRIPTIONS = {
apisuggestionsorissues: { apisuggestionsorissues: {
text: 'Issue or request details.', text: 'Issue or request details.',
type: 'Report category or null.', type: 'Report category.',
title: 'Short summary or null.', title: 'Short summary.',
}, },
search_articles: { search_articles: {
searchquery: searchquery: 'Keywords matching name, brand, EAN, SKU, or supplier SKU (AND logic).',
'Words that must all match across name, brand, EAN, SKU, or supplier SKU (AND logic, order-independent) or null.', excludewords: 'Keywords to exclude (AND logic).',
excludewords: offset: 'Pagination offset (default 0).',
'Words that if all match exclude the article (AND logic, order-independent) or null.', limit: 'Window size 1–20 (default 3).',
offset: 'Pagination offset or null. Default 0.',
limit: 'Result window size (1–20) or null. Default 3.',
}, },
edit_image: { 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.', note: NOTE_DESC,
prompt: 'Description of the image to generate, or edit/transformation instructions when attachments are provided.', prompt: 'Prompt for text-to-image or edit/transformation instructions.',
attachments: attachments: 'Reference image attachments for img2img.',
'Optional reference image(s) for editing/image-to-image or null. Omit for text-to-image generation.', attachment_vfsfilename: 'VFS path or DB path ("image/<hash>").',
attachment_vfsfilename: attachment_high: 'High resolution input if true (default false).',
'VFS path (e.g. "images/sample.png") or database path ("image/<hash>").', size: 'Resolution tier: "1K" (default), "2K", "4K".',
attachment_high: ratio: 'Aspect ratio: "1:1" (default), "4:3", "3:4".',
'Feed image in high quality/resolution if true; low quality/resolution if false or null (default).', vfsoutputfilename: 'Target VFS path (always saved as AVIF). Defaults to "images/generated-<timestamp>.avif".',
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-<timestamp>.avif". Output is always converted to AVIF.',
}, },
ean_checksum: { 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.', note: NOTE_DESC,
code: 'Barcode number to validate or complete (e.g. 12 digits for EAN-13 check digit calculation, or 13 digits for EAN-13 validation).', code: 'Barcode number to validate or complete.',
action: 'Optional action: "auto" (default: infer from length), "validate" (check last digit), "calculate" (compute check digit for the given payload), or null.', action: '"auto" (default), "validate", or "calculate".',
}, },
net_gross_calc: { net_gross_calc: {
note: 'A note explaining what this price calculation is doing. This note will be displayed to the user while executing.', note: NOTE_DESC,
gross: 'Gross price (incl. VAT) or null. When provided, computes net price and tax amount.', gross: 'Gross price (incl. VAT).',
net: 'Net price (excl. VAT) or null. When provided, computes gross price and tax amount.', net: 'Net price (excl. VAT).',
taxRate: 'Tax/VAT rate percentage (e.g. 19 for 19%, 7 for 7%) or null. Default 19.', taxRate: 'VAT percentage (default 19).',
amount: 'Generic price amount or null when used with "from" ("gross" or "net").', amount: 'Price amount when paired with "from".',
from: 'Direction when using "amount": "gross" (default) or "net".', from: '"gross" (default) or "net".',
precision: 'Decimal precision for net price API value or null (default 6, between 2 and 8).', precision: 'Net price API decimal precision 2–8 (default 6).',
items: 'Optional array of prices or objects for batch calculation: [19.99, 49.95] or [{ gross: 19.99, taxRate: 7 }].', items: 'Array of prices or calculation objects for batch processing.',
}, },
set_user_route: { set_user_route: {
route: 'Target frontend route path according to the Route Construction Matrix (e.g. "/artikel/ART-10042", "/kategorien/elektronik/kabel", "/auftrag/1001").', route: 'Frontend route from Route Matrix (e.g. "/artikel/ART-10042", "/auftrag/1001").',
}, },
}; };
@@ -242,70 +217,42 @@ export const PATCH_PARAM_DESCRIPTIONS = TOOL_PARAMETER_DESCRIPTIONS.patch;
// #region Tool-specific Subprompts // #region Tool-specific Subprompts
export const ANALYZE_IMAGE_VISION_COMPARE_PROMPT = 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.'; 'Compare these images, evaluate visual quality, flag watermarks/overlays/defects, and state the best candidate with rationale.';
export const ANALYZE_IMAGE_VISION_DESCRIBE_PROMPT = 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.'; 'Describe this image, evaluate visual quality, flag watermarks/overlays/defects, and verify suitability as a product/category image.';
// #endregion // #endregion
// #endregion // #endregion
// #region System Prompt Components // #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 "<slug>.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 = [ export const SYSTEM_PROMPT_RULES = [
'RULES:', 'RULES & VFS:',
'- LABELS (/labeltemplate): Dashback JSON `{ "objects": [ ... ] }`. Do NOT use ZPL.', '- 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.',
'- 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/…").', '- 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.',
'- OUTPUT: Markdown + emoji. Render VFS images inline `![alt](artifacts/example.png)`; other files (PDF, JSON, CSV) need download links.', '- SKILLS: Flat root "<slug>.SKILL" markdown files. ALWAYS read_file matching skill BEFORE executing covered tasks. Exempt from reset_vfs.',
'- 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-<section>.SKILL for parameters and response structures. api-binary-streaming-routes.SKILL covers fetch()-only upload/stream endpoints.',
'- API: routes are not inlined. BEFORE dashback_request, read_file the api-<section>.SKILL for exact params and response shapes. `api-binary-streaming-routes.SKILL` covers fetch()-only routes.', '- Types: T<n> refers to globally shared object schemas across skills.',
'- T* TYPES: `T<n>` (e.g. T2) is shorthand for a reusable object shape. Ids are GLOBAL: the same T<n> always means the same shape in every skill. Each api-<section>.SKILL only lists, in its "Reusable types" block, the T<n> it uses (plus the ones those reference), so a skill shows a subset of the registry, and a T<n> may appear in several skills with an identical definition.', '- Images: Pipeline: search_images -> analyze_image -> upload via fetch() in javascript_execute. "image/" is read-only; write generated assets to "images/...".',
].join('\n'); '- 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.',
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'); ].join('\n');
export const SYSTEM_PROMPT_ROUTE_MATRIX = [ export const SYSTEM_PROMPT_ROUTE_MATRIX = [
'ROUTE MATRIX (client routes German, API sections English; skills = api-<section>):', 'ROUTE MATRIX (client German, API skills = api-<section>):',
'| Destination | Path | Example | API sections |', '- 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',
'| Article Search | `/artikel?target={query}&filter={filter}&offset={n}` | `/artikel?target=kabel` | article, category, brand |', '- Warehouse / Stock: /artikel/lager -> warehouse, stock, ledger',
'| Article Detail | `/artikel/${encodeURIComponent(sku)}` | `/artikel/ART-10042` | article, category, brand, attributename, propertyname, unit, supplier, printer, labeltemplate, ledger |', '- Brands: /artikel/marken -> brand',
'| Warehouse / Stock | `/artikel/lager` | `/artikel/lager` | warehouse, stock, ledger |', '- Label Templates: /artikel/vorlagen -> labeltemplate, printer',
'| Brands | `/artikel/marken` | `/artikel/marken` | brand |', '- Attributes: /artikel/merkmale -> attributename, attributevalue, propertyname',
'| Label Templates | `/artikel/vorlagen` | `/artikel/vorlagen` | labeltemplate, printer |', '- Categories: /kategorien | /kategorien/${slug1}/${slug2} -> category, article',
'| Attributes | `/artikel/merkmale` | `/artikel/merkmale` | attributename, attributevalue, propertyname |', '- Deliveries: /lieferung | /lieferung/neu | /lieferung/${deliveryId} | /lieferung/lieferanten -> delivery, supplier, article, warehouse, stock, ledger',
'| Category Root | `/kategorien` | `/kategorien` | category |', '- Quotes: /auftrag | /auftrag/neu | /auftrag/${quoteId} -> quote, entity, article',
'| Category Subtree | `/kategorien/${slug1}/${slug2}` (encoded slugs) | `/kategorien/elektronik/kabel` | category, article |', '- Orders: /auftrag/bestellung | /auftrag/bestellung/neu | /auftrag/bestellung/${orderId} -> order, entity, account, payment, shipping, package',
'| Delivery List | `/lieferung` | `/lieferung` | delivery |', '- Customers: /kunde | /kunde/neu | /kunde/${entityId} | /kunde/gruppen -> entity, customergroup, number',
'| New Delivery | `/lieferung/neu` | `/lieferung/neu` | delivery, supplier, article, warehouse, stock |', '- History & Stats: /infos/articlehistory | /infos/globalhistory | /infos/statistics -> article, company, order, delivery, ledger',
'| 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'); ].join('\n');
// #endregion // #endregion
@@ -315,17 +262,12 @@ export const SYSTEM_PROMPT_ROUTE_MATRIX = [
/** /**
* Builds the SKILL index block for the system prompt. * 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 * @param {string} vfsRoot
* @returns {string} * @returns {string}
*/ */
export function buildSkillIndex(vfsRoot) { export function buildSkillIndex(vfsRoot) {
if (!vfsRoot) { if (!vfsRoot) {
return 'AVAILABLE SKILLS: unknown (the virtual filesystem could not be read this turn). Call find_files({ skills: true }) to check.'; return 'AVAILABLE SKILLS: unknown. Call find_files({ skills: true }) to verify.';
} }
let skills; let skills;
@@ -333,21 +275,16 @@ export function buildSkillIndex(vfsRoot) {
skills = listSkillFiles(vfsRoot); skills = listSkillFiles(vfsRoot);
} catch (err) { } catch (err) {
console.warn('conversation: failed to list skill files:', err.message); 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.'; return 'AVAILABLE SKILLS: unreadable this turn. Call find_files({ skills: true }) to verify.';
} }
if (skills.length === 0) { if (skills.length === 0) {
return [ return 'AVAILABLE SKILLS: none.';
'AVAILABLE SKILLS: none. This company has no SKILL files yet.',
].join('\n');
} }
const width = Math.max(...skills.map((s) => s.path.length));
return [ return [
`AVAILABLE SKILLS (${skills.length}): stored in the VFS root as flat <slug>.SKILL files.`, `AVAILABLE SKILLS (${skills.length}): stored in VFS root. Read with read_file before executing covered tasks.`,
'Before starting a task that any of these cover, read the matching skill with read_file and follow it.', ...skills.map((s) => `- ${s.path} (${s.lines} lines): ${s.title}`),
'',
...skills.map((s) => ` ${s.path.padEnd(width)} ${String(s.lines).padStart(4)} lines | ${s.title}`),
].join('\n'); ].join('\n');
} }
@@ -356,8 +293,7 @@ export function buildSkillIndex(vfsRoot) {
// #region System Prompt // #region System Prompt
/** /**
* System prompt: base instructions plus the generated API routes reference * System prompt: base instructions and contextual index.
* (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 {string} vfsRoot Resolved VFS root for this user's company, used to list SKILL files.
* @param {number} [googleAuthId] * @param {number} [googleAuthId]
@@ -385,15 +321,11 @@ export async function buildSystemPrompt(vfsRoot, googleAuthId) {
const basePrompt = [ const basePrompt = [
'QuixPOS Article Management System API Assistant', 'QuixPOS Article Management System API Assistant',
`Date: ${today}`, `Date: ${today}`,
'[TESTSYSTEM, destructive commands are allowed, main purpose of this system is to find bugs]', '[TESTSYSTEM: destructive commands allowed; primary goal is finding bugs]',
'',
SYSTEM_PROMPT_VFS_RULES,
'',
buildSkillIndex(vfsRoot),
'', '',
SYSTEM_PROMPT_RULES, SYSTEM_PROMPT_RULES,
'', '',
SYSTEM_PROMPT_BINARY_UPLOAD, buildSkillIndex(vfsRoot),
'', '',
SYSTEM_PROMPT_ROUTE_MATRIX, SYSTEM_PROMPT_ROUTE_MATRIX,
].join('\n'); ].join('\n');
@@ -405,4 +337,4 @@ export async function buildSystemPrompt(vfsRoot, googleAuthId) {
return sections.join('\n\n'); return sections.join('\n\n');
} }
// #endregion // #endregion