Files
dashback/src/tools/prompts.js
2026-10-11 13:12:47 +02:00

409 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 <columns>` 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 "<slug>.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 "<slug>.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/<hash>"). 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/<hash>").',
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/<hash>").',
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-<timestamp>.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 "<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 = [
'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-<section>.SKILL for exact params and response shapes. `api-binary-streaming-routes.SKILL` covers fetch()-only routes.',
'- 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.',
].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-<section>):',
'| 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 <slug>.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<string>}
*/
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