Files
dashback/src/tools/prompts.js
2026-10-18 14:29:01 +02:00

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: ![alt](images/path.avif). 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