diff --git a/.agents/skills/naming-conventions/SKILL.md b/.agents/skills/naming-conventions-and-style/SKILL.md similarity index 96% rename from .agents/skills/naming-conventions/SKILL.md rename to .agents/skills/naming-conventions-and-style/SKILL.md index eee56bd..f311aaf 100644 --- a/.agents/skills/naming-conventions/SKILL.md +++ b/.agents/skills/naming-conventions-and-style/SKILL.md @@ -1,8 +1,15 @@ --- -name: naming-conventions +name: naming-conventions-and-style description: "Use when: improving variable, function, module, or API names; refactoring code for clarity; choosing consistent naming in the dashback backend." --- +# Style + +use vs code region markers in code files +use: +// MARK: calls startApi from ./api/index.js +atop important functions + # Naming Conventions — dashback Apply these conventions when writing or refactoring code in this project. diff --git a/src/index.js b/src/index.js index 4b1d9e1..d0e9d0f 100644 --- a/src/index.js +++ b/src/index.js @@ -22,6 +22,7 @@ process.on('SIGTERM', () => { shutdown().then(() => process.exit(0)); }); +// MARK: calls startApi from ./api/index.js startApi().catch((err) => { sysLog.error('Failed to start API:', err.message); process.exit(1); diff --git a/src/services/conversationManager.js b/src/services/conversationManager.js index a14eed8..0627602 100644 --- a/src/services/conversationManager.js +++ b/src/services/conversationManager.js @@ -1,6 +1,5 @@ import { LLMOrbiter, OpenRouterProvider } from 'libllmorbiter'; import * as db from '../db/modelconversation.js'; -import * as dbCompanySetting from '../db/companysetting.js'; import { getStore } from '../db/store.js'; import env from '../config/env.js'; import { emit as emitCompanyEvent } from '../api/sse.js'; @@ -8,12 +7,12 @@ import { readFileSync, writeFileSync, mkdirSync } from 'fs'; import { resolve, dirname, extname } from 'path'; import { fileURLToPath } from 'url'; import { randomUUID } from 'node:crypto'; +import { buildSystemPrompt } from './prompts.js'; import { createDashbackRequestTool } from '../tools/dashback-request.js'; import { createAskUserTool } from '../tools/ask-user.js'; import { createJavascriptExecuteTool } from '../tools/javascript.js'; import { createResetVfsTool } from '../tools/reset-vfs.js'; import { resolveVfsRoot, prepareVfs, getMimeType } from '../tools/vfs.js'; -import { listSkillFiles } from '../tools/skill.js'; import { createApiSuggestionsOrIssuesTool } from '../tools/apisuggestionsorissues.js'; import { createFindFilesTool } from '../tools/find-files.js'; import { createReadFileTool } from '../tools/read-file.js'; @@ -31,161 +30,14 @@ import { createNetGrossCalcTool } from '../tools/net-gross-calc.js'; import { createSetUserRouteTool } from '../tools/set-user-route.js'; import { resolveOpenRouterKey, resolveDefaultModel, resolveModelReasoning, RETIRED_MODELS, hasConfiguredOpenRouterKey } from '../tools/llm-key.js'; +// #region Constants + const KEEP_ALIVE_MS = 20_000; +// #endregion -/** - * 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} - */ -function buildSkillIndex(vfsRoot) { - // A null root means the VFS could not be resolved (no company selected, or a store - // error). Say so plainly instead of pretending the company has no skills — otherwise the - // model would conclude no guidance exists and invent its own procedure. - 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'); -} - -/** - * 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} - */ -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}`, - '', - 'TOOLS:', - '1. dashback_request: HTTP requests (GET, POST, PUT, PATCH, DELETE) to QuixPOS API.', - '2. ask_user: Prompt user for missing decisions or parameters; pauses until answered.', - '3. javascript_execute: Sandboxed Node env for computation, binary I/O, aggregation. Assign module.exports. Has fetch("/path"), vfs, sharp, ExcelJS.', - '4. find_files: List VFS files (skills=true for SKILL files).', - '5. read_file: Read VFS text/CSV/JSON files (seek/limit windowed).', - '6. patch: Create/overwrite/replace text in VFS files, incl. SKILL files (only text write path).', - '7. search_images: Search Google Images; downloads AVIF candidates to VFS.', - '8. analyze_image: Vision analysis of up to 9 images (VFS paths or "image/"). Always evaluate images with this tool.', - '9. web_search: Google web search (timePeriod: "day"|"week" for current events).', - '10. url_get: Fetch/scrape web page as markdown.', - '11. pdf_read: Convert PDF to layout text ("txt") or VFS page images ("png").', - '12. reset_vfs: Delete all non-SKILL VFS files.', - '13. rm: Delete a single VFS file (including SKILL files).', - '14. apisuggestionsorissues: Report API bugs or missing endpoints.', - '15. search_articles: Search by name, brand, EAN, SKU, supplier SKU. Returns full articles window (default 3, max 20), offset, total.', - '16. edit_image: Generate/edit images with the company image model (modelslugimage). Supports attachments (low/high quality), sizes 1K/2K/4K, ratios 1:1/4:3/3:4. Output: AVIF in VFS.', - '17. ean_checksum: Validate or generate barcode check digits (EAN-13, EAN-8, UPC-A, GTIN-14).', - '18. net_gross_calc: Convert gross/net with VAT (default 19%). Returns 2-decimal display values and high-precision net for the API.', - '19. set-user-route: Navigate the UI (e.g. "show me the article", "open quote 1001") using the Route Matrix.', - '', - '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.', - '', - buildSkillIndex(vfsRoot), - '', - 'RULES:', - '- PRICES: user input is GROSS, API requires NET. Always use net_gross_calc (4-6 decimals): net = gross / (1 + taxRate / 100).', - '- 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.', - '', - '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: [...] } }', - '', - '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'); - - const sections = []; - if (prefix) sections.push(prefix); - sections.push(basePrompt); - if (suffix) sections.push(suffix); - return sections.join('\n\n'); -} +// #region Message Helpers /** * Per-conversation manager. Owns the SSE subscribers of one conversation and @@ -391,6 +243,10 @@ export function historyToLlmMessages(history) { return messages; } +// #endregion + +// #region ConversationManager + export class ConversationManager { /** * @param {number} conversationId @@ -408,6 +264,8 @@ export class ConversationManager { this.isProcessing = false; } + // #region Subscription & Broadcast + /** * Broadcast an SSE event to all subscribers of this conversation. * @param {string} event @@ -461,6 +319,10 @@ export class ConversationManager { return unsubscribe; } + // #endregion + + // #region Attachments + /** * Saves attachments sent with a user message to the company's VFS * under `attachments/` and registers them in `modelvfs`. @@ -554,6 +416,10 @@ export class ConversationManager { return tags; } + // #endregion + + // #region Message Handlers + /** * Handle an incoming user message. * 1. Extracts text content and saves any attachments into the company VFS (`attachments/`). @@ -649,7 +515,9 @@ export class ConversationManager { return toolMsg; } + // #endregion + // #region LLM Run /** * Run the LLM over the stored transcript, stream the answer over SSE, * persist reasoning, tool calls, and assistant messages, then return to `idle`. @@ -947,6 +815,10 @@ export class ConversationManager { this.broadcast('state', error ? { state: 'done', error } : { state: 'done' }); } + // #endregion + + // #region Teardown + destroy() { this.destroyed = true; this.orbiter?.abort('Conversation manager closed'); @@ -958,8 +830,14 @@ export class ConversationManager { } this.subscribers.clear(); } + + // #endregion } +// #endregion + +// #region Manager Registry + /** @type {Map} */ const activeManagers = new Map(); @@ -1011,3 +889,5 @@ export function closeAllManagers() { activeManagers.delete(id); } } + +// #endregion diff --git a/src/services/prompts.js b/src/services/prompts.js new file mode 100644 index 0000000..6b0b0b0 --- /dev/null +++ b/src/services/prompts.js @@ -0,0 +1 @@ +export * from '../tools/prompts.js'; diff --git a/src/tools/analyze-image.js b/src/tools/analyze-image.js index 25d37c6..431c98a 100644 --- a/src/tools/analyze-image.js +++ b/src/tools/analyze-image.js @@ -7,6 +7,12 @@ import env from '../config/env.js'; import { getImageByHash } from '../db/pic.js'; import { prepareVfs, extractFullHash } from './vfs.js'; import { resolveOpenRouterKey, resolveDefaultModel } from './llm-key.js'; +import { + TOOL_DESCRIPTIONS, + TOOL_PARAMETER_DESCRIPTIONS, + ANALYZE_IMAGE_VISION_COMPARE_PROMPT, + ANALYZE_IMAGE_VISION_DESCRIBE_PROMPT, +} from './prompts.js'; /** * analyze_image — lets a vision model actually LOOK at images held in the VFS. @@ -275,9 +281,7 @@ async function fetchDatabaseImage(hash, googleAuthId) { export function createAnalyzeImageTool({ googleAuthId, model }) { return defineTool({ name: 'analyze_image', - description: - 'Inspect 1-9 images via vision model. Multiple paths render as a labelled comparison grid. ' + - 'Paths: VFS-relative ("images/foo.avif") or DB ("image/"). Returns model evaluation.', + description: TOOL_DESCRIPTIONS.analyze_image, strict: false, parameters: { type: 'object', @@ -286,23 +290,23 @@ export function createAnalyzeImageTool({ googleAuthId, model }) { properties: { note: { type: ['string', 'null'], - description: '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.', + description: TOOL_PARAMETER_DESCRIPTIONS.analyze_image.note, }, paths: { type: 'array', - description: '1–9 VFS paths or DB paths ("image/").', + description: TOOL_PARAMETER_DESCRIPTIONS.analyze_image.paths, items: { type: 'string' }, minItems: 1, maxItems: MAX_IMAGES, }, prompt: { type: ['string', 'null'], - description: 'Inspection criteria or question, or null.', + description: TOOL_PARAMETER_DESCRIPTIONS.analyze_image.prompt, }, grid: { type: ['string', 'null'], enum: [...Object.keys(GRID_CONFIGS), null], - description: 'Grid layout or null for auto.', + description: TOOL_PARAMETER_DESCRIPTIONS.analyze_image.grid, }, }, }, @@ -378,14 +382,14 @@ export function createAnalyzeImageTool({ googleAuthId, model }) { // ── Ask the vision model ────────────────────────────────────── const question = typeof prompt === 'string' && prompt.trim() ? prompt.trim() : null; const text = items.length > 1 - ? `${question || 'Compare these images, judge visual quality, flag watermarks, text overlays or defects, and say which one is best and why.'}\n\n` + + ? `${question || ANALYZE_IMAGE_VISION_COMPARE_PROMPT}\n\n` + `The grid is ${shape.name}. Each image has its file name printed beneath it:\n` + items.map((it, idx) => { const row = Math.floor(idx / GRID_CONFIGS[shape.name].cols) + 1; const col = (idx % GRID_CONFIGS[shape.name].cols) + 1; return `- Row ${row}, Col ${col}: "${it.name}"`; }).join('\n') - : question || '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.'; + : question || ANALYZE_IMAGE_VISION_DESCRIBE_PROMPT; const apiKey = await resolveOpenRouterKey(googleAuthId); const visionModel = (await resolveDefaultModel(googleAuthId)) || model; diff --git a/src/tools/apisuggestionsorissues.js b/src/tools/apisuggestionsorissues.js index 05cf06f..305b3f1 100644 --- a/src/tools/apisuggestionsorissues.js +++ b/src/tools/apisuggestionsorissues.js @@ -1,5 +1,6 @@ import { defineTool } from 'libllmorbiter'; import * as db from '../db/company.js'; +import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; /** * Creates the apisuggestionsorissues tool allowing the agent to report @@ -11,7 +12,7 @@ import * as db from '../db/company.js'; export function createApiSuggestionsOrIssuesTool({ googleAuthId, companyId }) { return defineTool({ name: 'apisuggestionsorissues', - description: 'Report missing API endpoint, feature request, bug, or issue for triage.', + description: TOOL_DESCRIPTIONS.apisuggestionsorissues, strict: false, parameters: { type: 'object', @@ -20,16 +21,16 @@ export function createApiSuggestionsOrIssuesTool({ googleAuthId, companyId }) { properties: { text: { type: 'string', - description: 'Issue or request details.', + description: TOOL_PARAMETER_DESCRIPTIONS.apisuggestionsorissues.text, }, type: { type: ['string', 'null'], enum: ['missing_api', 'bug', 'suggestion', 'issue', 'other', null], - description: 'Report category or null.', + description: TOOL_PARAMETER_DESCRIPTIONS.apisuggestionsorissues.type, }, title: { type: ['string', 'null'], - description: 'Short summary or null.', + description: TOOL_PARAMETER_DESCRIPTIONS.apisuggestionsorissues.title, }, }, }, diff --git a/src/tools/ask-user.js b/src/tools/ask-user.js index 8b6ffd5..3396b04 100644 --- a/src/tools/ask-user.js +++ b/src/tools/ask-user.js @@ -1,4 +1,5 @@ import { defineTool, DirectReturnError } from 'libllmorbiter'; +import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; /** * ask_user tool: @@ -13,8 +14,7 @@ import { defineTool, DirectReturnError } from 'libllmorbiter'; export function createAskUserTool() { return defineTool({ name: 'ask_user', - description: - 'Prompt user for clarification or decision with concise question and up to 3 options.', + description: TOOL_DESCRIPTIONS.ask_user, strict: false, parameters: { type: 'object', @@ -23,11 +23,11 @@ export function createAskUserTool() { properties: { question: { type: 'string', - description: 'Question prompt.', + description: TOOL_PARAMETER_DESCRIPTIONS.ask_user.question, }, options: { type: ['array', 'null'], - description: 'Up to 3 option labels, or null.', + description: TOOL_PARAMETER_DESCRIPTIONS.ask_user.options, items: { type: 'string' }, maxItems: 3, }, diff --git a/src/tools/dashback-request.js b/src/tools/dashback-request.js index 30d4c18..5ce2546 100644 --- a/src/tools/dashback-request.js +++ b/src/tools/dashback-request.js @@ -1,6 +1,7 @@ import jwt from 'jsonwebtoken'; import { defineTool } from 'libllmorbiter'; import env from '../config/env.js'; +import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; const MAX_RESPONSE_CHARS = 20_000; const REQUEST_TIMEOUT_MS = 30_000; @@ -14,7 +15,7 @@ const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']; export function createDashbackRequestTool({ googleAuthId }) { return defineTool({ name: 'dashback_request', - description: 'Call Dashback REST API. Paths are root-relative (e.g. "/article/2").', + description: TOOL_DESCRIPTIONS.dashback_request, strict: false, parameters: { type: 'object', @@ -23,12 +24,12 @@ export function createDashbackRequestTool({ googleAuthId }) { properties: { note: { type: ['string', 'null'], - description: 'A note explaining what this request is doing (or null). This note will be displayed to the user while the request is executing.', + description: TOOL_PARAMETER_DESCRIPTIONS.dashback_request.note, }, - method: { type: 'string', enum: METHODS, description: 'HTTP method.' }, - path: { type: 'string', description: 'Endpoint path (e.g. "/article/2").' }, - query: { type: ['object', 'null'], description: 'Query parameters object or null.' }, - body: { type: ['string', 'null'], description: 'JSON request payload string or null.' }, + method: { type: 'string', enum: METHODS, description: TOOL_PARAMETER_DESCRIPTIONS.dashback_request.method }, + path: { type: 'string', description: TOOL_PARAMETER_DESCRIPTIONS.dashback_request.path }, + query: { type: ['object', 'null'], description: TOOL_PARAMETER_DESCRIPTIONS.dashback_request.query }, + body: { type: ['string', 'null'], description: TOOL_PARAMETER_DESCRIPTIONS.dashback_request.body }, }, }, invoke: async ({ method = 'GET', path, query = null, body = null, note }) => { diff --git a/src/tools/ean-checksum.js b/src/tools/ean-checksum.js index e328b85..aaf4651 100644 --- a/src/tools/ean-checksum.js +++ b/src/tools/ean-checksum.js @@ -1,4 +1,5 @@ import { defineTool, ToolError } from 'libllmorbiter'; +import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; /** * Calculates the standard GS1 check digit for a string of numeric digits. @@ -44,9 +45,7 @@ function resolveBarcodeFormat(length) { export function createEanChecksumTool() { return defineTool({ name: 'ean_checksum', - description: - '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.', + description: TOOL_DESCRIPTIONS.ean_checksum, strict: false, parameters: { type: 'object', @@ -55,16 +54,16 @@ export function createEanChecksumTool() { properties: { note: { type: ['string', 'null'], - description: 'A note explaining what this EAN checksum check is doing (or null). This note will be displayed to the user while executing.', + description: TOOL_PARAMETER_DESCRIPTIONS.ean_checksum.note, }, code: { type: 'string', - description: 'Barcode number to validate or complete (e.g. 12 digits for EAN-13 check digit calculation, or 13 digits for EAN-13 validation).', + description: TOOL_PARAMETER_DESCRIPTIONS.ean_checksum.code, }, action: { type: ['string', 'null'], enum: ['auto', 'validate', 'calculate', null], - description: 'Optional action: "auto" (default: infer from length), "validate" (check last digit), "calculate" (compute check digit for the given payload), or null.', + description: TOOL_PARAMETER_DESCRIPTIONS.ean_checksum.action, }, }, }, diff --git a/src/tools/edit-image.js b/src/tools/edit-image.js index 67c9795..aef7f76 100644 --- a/src/tools/edit-image.js +++ b/src/tools/edit-image.js @@ -7,6 +7,7 @@ import env from '../config/env.js'; import { getImageByHash } from '../db/pic.js'; import { prepareVfs, syncVfsToDb, extractFullHash, isReadOnlyDatabasePath } from './vfs.js'; import { resolveOpenRouterKey, resolveImageModel } from './llm-key.js'; +import { TOOL_DESCRIPTIONS } from './prompts.js'; /** * edit-image — generate or edit images using the modelslugimage company setting. @@ -251,10 +252,7 @@ async function processAttachmentToDataUrl(rawBuffer, high) { export function createEditImageTool({ googleAuthId, model: fallbackModel }) { return defineTool({ name: 'edit_image', - description: - '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).', + description: TOOL_DESCRIPTIONS.edit_image, strict: false, parameters: { type: 'object', diff --git a/src/tools/find-files.js b/src/tools/find-files.js index 4ebc1a5..e8b02d1 100644 --- a/src/tools/find-files.js +++ b/src/tools/find-files.js @@ -3,6 +3,7 @@ import path from 'path'; import { defineTool, ToolError } from 'libllmorbiter'; import { isSkillPath, listSkillFiles, SKILL_EXT } from './skill.js'; import { prepareVfs } from './vfs.js'; +import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; /** * find_files — lists the contents of the virtual filesystem (VFS). @@ -80,9 +81,7 @@ function walkVfs(absDir, vfsRoot, recursive, depth, maxDepth) { export function createFindFilesTool({ googleAuthId }) { return defineTool({ name: 'find_files', - description: - '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).', + description: TOOL_DESCRIPTIONS.find_files, strict: false, parameters: { type: 'object', @@ -90,27 +89,27 @@ export function createFindFilesTool({ googleAuthId }) { properties: { dirname: { type: ['string', 'null'], - description: 'VFS directory to list or null. Default "." (root). Must be "." or empty if skills=true.', + description: TOOL_PARAMETER_DESCRIPTIONS.find_files.dirname, }, recursive: { type: ['boolean', 'null'], - description: 'Walk subdirectories or null. Default false.', + description: TOOL_PARAMETER_DESCRIPTIONS.find_files.recursive, }, skills: { type: ['boolean', 'null'], - description: 'List only root SKILL files with parsed titles or null. Overrides dirname/recursive. Default false.', + description: TOOL_PARAMETER_DESCRIPTIONS.find_files.skills, }, seek: { type: ['number', 'null'], - description: 'Pagination offset or null. Default 0.', + description: TOOL_PARAMETER_DESCRIPTIONS.find_files.seek, }, limit: { type: ['number', 'null'], - description: `Max entries to return or null. Default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}.`, + description: TOOL_PARAMETER_DESCRIPTIONS.find_files.limit, }, maxDepth: { type: ['number', 'null'], - description: 'Max recursive directory depth or null. Default 3.', + description: TOOL_PARAMETER_DESCRIPTIONS.find_files.maxDepth, }, }, }, diff --git a/src/tools/javascript.js b/src/tools/javascript.js index beeae91..6f7b403 100644 --- a/src/tools/javascript.js +++ b/src/tools/javascript.js @@ -3,6 +3,7 @@ import path from 'path'; import jwt from 'jsonwebtoken'; import { defineTool, ToolError } from 'libllmorbiter'; import env from '../config/env.js'; +import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; import { createVfs, prepareVfs, syncVfsToDb } from './vfs.js'; import { runSandboxJob } from '../services/javascriptSandbox.js'; @@ -271,17 +272,7 @@ function formatSandboxError(code, report) { export function createJavascriptExecuteTool({ googleAuthId }) { return defineTool({ name: 'javascript_execute', - description: - 'Execute JS in an isolated worker sandbox using either inline `code` or a VFS applet. 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).', + description: TOOL_DESCRIPTIONS.javascript_execute, strict: false, parameters: { type: 'object', @@ -289,28 +280,28 @@ export function createJavascriptExecuteTool({ googleAuthId }) { properties: { note: { type: ['string', 'null'], - description: 'A note explaining what this code is doing (or null). This note will be displayed to the user while the code is executing.', + description: TOOL_PARAMETER_DESCRIPTIONS.javascript_execute.note, }, code: { type: 'string', - description: '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.', + description: TOOL_PARAMETER_DESCRIPTIONS.javascript_execute.code, }, appletvfsfile: { type: 'string', - description: '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`).', + description: TOOL_PARAMETER_DESCRIPTIONS.javascript_execute.appletvfsfile, }, applet_input: { type: 'object', - description: '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.', + description: TOOL_PARAMETER_DESCRIPTIONS.javascript_execute.applet_input, additionalProperties: true, }, timeout: { type: ['number', 'null'], - description: `Execution timeout in ms or null. Default ${DEFAULT_TIMEOUT_MS}, max ${MAX_TIMEOUT_MS}.`, + description: TOOL_PARAMETER_DESCRIPTIONS.javascript_execute.timeout, }, max_output: { type: ['number', 'null'], - description: `Max stdout chars or null. Default ${DEFAULT_MAX_OUTPUT}.`, + description: TOOL_PARAMETER_DESCRIPTIONS.javascript_execute.max_output, }, }, }, diff --git a/src/tools/net-gross-calc.js b/src/tools/net-gross-calc.js index c602493..3c46b3f 100644 --- a/src/tools/net-gross-calc.js +++ b/src/tools/net-gross-calc.js @@ -1,4 +1,5 @@ import { defineTool, ToolError } from 'libllmorbiter'; +import { TOOL_DESCRIPTIONS } from './prompts.js'; const DEFAULT_TAX_RATE = 19; const DEFAULT_PRECISION = 6; @@ -109,9 +110,7 @@ export function calculatePrice({ net, gross, amount, from, taxRate = DEFAULT_TAX export function createNetGrossCalcTool() { return defineTool({ name: 'net_gross_calc', - description: - '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.', + description: TOOL_DESCRIPTIONS.net_gross_calc, strict: false, parameters: { type: 'object', diff --git a/src/tools/patch.js b/src/tools/patch.js index e240d34..774a11c 100644 --- a/src/tools/patch.js +++ b/src/tools/patch.js @@ -4,6 +4,7 @@ import { defineTool, ToolError } from 'libllmorbiter'; import { isReadOnlyDatabasePath } from './vfs.js'; import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js'; import { prepareVfs, syncVfsToDb } from './vfs.js'; +import { TOOL_DESCRIPTIONS, PATCH_PARAMETER_DESCRIPTIONS } from './prompts.js'; /** * patch — applies edit operations to files in the virtual filesystem (VFS). @@ -66,11 +67,7 @@ function toLines(text) { export function createPatchTool({ googleAuthId }) { return defineTool({ name: 'patch', - description: - 'Create, overwrite, or search-and-replace text in VFS 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).', + description: TOOL_DESCRIPTIONS.patch, strict: false, parameters: { type: 'object', @@ -79,7 +76,7 @@ export function createPatchTool({ googleAuthId }) { properties: { operations: { type: 'array', - description: 'Ordered patch operations.', + description: PATCH_PARAMETER_DESCRIPTIONS.operations, items: { type: 'object', additionalProperties: false, @@ -87,23 +84,23 @@ export function createPatchTool({ googleAuthId }) { properties: { file: { type: 'string', - description: 'VFS path (e.g. "notes.md", "pipeline.SKILL"). SKILL files must be root-level (no "/").', + description: PATCH_PARAMETER_DESCRIPTIONS.file, }, search: { type: ['string', 'null'], - description: 'Exact text to replace. null/omitted = overwrite or create file.', + description: PATCH_PARAMETER_DESCRIPTIONS.search, }, replace: { type: 'string', - description: 'Replacement text.', + description: PATCH_PARAMETER_DESCRIPTIONS.replace, }, title: { type: ['string', 'null'], - description: 'Optional line-1 title for SKILL files or null.', + description: PATCH_PARAMETER_DESCRIPTIONS.title, }, skiplines: { type: ['integer', 'null'], - description: 'Lines to skip before searching or null.', + description: PATCH_PARAMETER_DESCRIPTIONS.skiplines, }, }, }, diff --git a/src/tools/pdf-read.js b/src/tools/pdf-read.js index cbfbeda..e0b801d 100644 --- a/src/tools/pdf-read.js +++ b/src/tools/pdf-read.js @@ -3,6 +3,7 @@ import path from 'path'; import { defineTool, ToolError } from 'libllmorbiter'; import { pdfToText, splitPdfPages, pdfToPng } from 'pdfconvert-wasm'; import { prepareVfs, syncVfsToDb } from './vfs.js'; +import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; const IMAGE_FOLDER = 'images'; const DEFAULT_DPI = 150; @@ -42,8 +43,7 @@ function slugifyBase(filename) { export function createPdfReadTool({ googleAuthId }) { return defineTool({ name: 'pdf_read', - description: - 'Read and convert PDF/text documents to text or VFS PNG images (stored in "images/").', + description: TOOL_DESCRIPTIONS.pdf_read, strict: false, parameters: { type: 'object', @@ -52,20 +52,20 @@ export function createPdfReadTool({ googleAuthId }) { properties: { path: { type: 'string', - description: 'VFS path or HTTP(S) URL.', + description: TOOL_PARAMETER_DESCRIPTIONS.pdf_read.path, }, format: { type: ['string', 'null'], enum: ['txt', 'png', 'both', null], - description: 'Output format: "txt" (layout text), "png" (page images), "both", or null. Default "txt".', + description: TOOL_PARAMETER_DESCRIPTIONS.pdf_read.format, }, page: { type: ['number', 'null'], - description: '1-based page number or null for all pages.', + description: TOOL_PARAMETER_DESCRIPTIONS.pdf_read.page, }, dpi: { type: ['number', 'null'], - description: 'PNG render DPI or null. Default 150.', + description: TOOL_PARAMETER_DESCRIPTIONS.pdf_read.dpi, }, }, }, diff --git a/src/tools/prompts.js b/src/tools/prompts.js new file mode 100644 index 0000000..01dcbda --- /dev/null +++ b/src/tools/prompts.js @@ -0,0 +1,436 @@ +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 Dashback REST API. Paths are root-relative (e.g. "/article/2").', + + ask_user: + 'Prompt user for clarification or decision with concise question and up to 3 options.', + + javascript_execute: + 'Execute JS in an isolated worker sandbox using either inline `code` or a VFS applet. 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 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. 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. 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 and convert PDF/text documents to text or VFS PNG images (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). Refer to the User Perspective Route Construction Matrix for valid route patterns.', +}; + +// #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 4, 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 + +/** + * Concise tool listing included directly in the system prompt. + */ +export const SYSTEM_PROMPT_TOOLS = [ + 'TOOLS:', + '1. dashback_request: HTTP requests (GET, POST, PUT, PATCH, DELETE) to QuixPOS API.', + '2. ask_user: Prompt user for missing decisions or parameters; pauses until answered.', + '3. javascript_execute: Sandboxed Node env for computation, binary I/O, aggregation. Assign module.exports. Has fetch("/path"), vfs, sharp, ExcelJS.', + '4. find_files: List VFS files (skills=true for SKILL files).', + '5. read_file: Read VFS text/CSV/JSON files (seek/limit windowed).', + '6. patch: Create/overwrite/replace text in VFS files, incl. SKILL files (only text write path).', + '7. search_images: Search Google Images; downloads AVIF candidates to VFS.', + '8. analyze_image: Vision analysis of up to 9 images (VFS paths or "image/"). Always evaluate images with this tool.', + '9. web_search: Google web search (timePeriod: "day"|"week" for current events).', + '10. url_get: Fetch/scrape web page as markdown.', + '11. pdf_read: Convert PDF to layout text ("txt") or VFS page images ("png").', + '12. reset_vfs: Delete all non-SKILL VFS files.', + '13. rm: Delete a single VFS file (including SKILL files).', + '14. apisuggestionsorissues: Report API bugs or missing endpoints.', + '15. search_articles: Search by name, brand, EAN, SKU, supplier SKU. Returns full articles window (default 3, max 20), offset, total.', + '16. edit_image: Generate/edit images with the company image model (modelslugimage). Supports attachments (low/high quality), sizes 1K/2K/4K, ratios 1:1/4:3/3:4. Output: AVIF in VFS.', + '17. ean_checksum: Validate or generate barcode check digits (EAN-13, EAN-8, UPC-A, GTIN-14).', + '18. net_gross_calc: Convert gross/net with VAT (default 19%). Returns 2-decimal display values and high-precision net for the API.', + '19. set-user-route: Navigate the UI (e.g. "show me the article", "open quote 1001") using the Route Matrix.', +].join('\n'); + +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:', + '- PRICES: user input is GROSS, API requires NET. Always use net_gross_calc (4-6 decimals): net = gross / (1 + taxRate / 100).', + '- 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_TOOLS, + '', + 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 diff --git a/src/tools/read-file.js b/src/tools/read-file.js index 3089fc5..fdb76c1 100644 --- a/src/tools/read-file.js +++ b/src/tools/read-file.js @@ -4,6 +4,7 @@ import { defineTool, ToolError } from 'libllmorbiter'; import { isSkillPath, parseSkill, SKILL_EXT } from './skill.js'; import { getApiRouteSkill } from './routes-skills.js'; import { prepareVfs } from './vfs.js'; +import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; const DEFAULT_LIMIT = 400; const MAX_LIMIT = 5000; @@ -61,10 +62,7 @@ function looksBinary(buf) { export function createReadFileTool({ googleAuthId }) { return defineTool({ name: 'read_file', - description: - 'Read text file 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).', + description: TOOL_DESCRIPTIONS.read_file, strict: false, parameters: { type: 'object', @@ -73,15 +71,15 @@ export function createReadFileTool({ googleAuthId }) { properties: { filename: { type: 'string', - description: 'VFS file path.', + description: TOOL_PARAMETER_DESCRIPTIONS.read_file.filename, }, seek: { type: ['number', 'null'], - description: 'Line offset (0-indexed) or null. Default 0.', + description: TOOL_PARAMETER_DESCRIPTIONS.read_file.seek, }, limit: { type: ['number', 'null'], - description: `Max lines to return or null. Default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}.`, + description: TOOL_PARAMETER_DESCRIPTIONS.read_file.limit, }, }, }, diff --git a/src/tools/reset-vfs.js b/src/tools/reset-vfs.js index 90b61de..1c25953 100644 --- a/src/tools/reset-vfs.js +++ b/src/tools/reset-vfs.js @@ -3,6 +3,7 @@ import path from 'path'; import { defineTool } from 'libllmorbiter'; import { isSkillPath } from './skill.js'; import { resolveVfsRoot } from './vfs.js'; +import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; /** * reset_vfs — clears throwaway artifacts out of the virtual filesystem. @@ -26,7 +27,7 @@ import { resolveVfsRoot } from './vfs.js'; export function createResetVfsTool({ googleAuthId }) { return defineTool({ name: 'reset_vfs', - description: 'Clear VFS files, images, and artifacts. Preserves SKILL files by default.', + description: TOOL_DESCRIPTIONS.reset_vfs, strict: false, parameters: { type: 'object', @@ -34,7 +35,7 @@ export function createResetVfsTool({ googleAuthId }) { properties: { include_skills: { type: ['boolean', 'null'], - description: 'Delete SKILL files too (or null). Default false.', + description: TOOL_PARAMETER_DESCRIPTIONS.reset_vfs.include_skills, }, }, }, diff --git a/src/tools/rm.js b/src/tools/rm.js index d5b37b6..4c5fa44 100644 --- a/src/tools/rm.js +++ b/src/tools/rm.js @@ -3,6 +3,7 @@ import path from 'path'; import { defineTool, ToolError } from 'libllmorbiter'; import { isReadOnlyDatabasePath, prepareVfs } from './vfs.js'; import { isSkillPath } from './skill.js'; +import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; /** * rm — deletes a single file from the virtual filesystem (VFS). @@ -64,7 +65,7 @@ export function deleteVfsFile(vfsRoot, relPath) { export function createRmTool({ googleAuthId }) { return defineTool({ name: 'rm', - description: 'Delete single VFS file (e.g. specific artifact or SKILL file). Irreversible.', + description: TOOL_DESCRIPTIONS.rm, strict: false, parameters: { type: 'object', @@ -73,7 +74,7 @@ export function createRmTool({ googleAuthId }) { properties: { filename: { type: 'string', - description: 'VFS file path to delete.', + description: TOOL_PARAMETER_DESCRIPTIONS.rm.filename, }, }, }, diff --git a/src/tools/search-articles.js b/src/tools/search-articles.js index f95fd79..402e24c 100644 --- a/src/tools/search-articles.js +++ b/src/tools/search-articles.js @@ -1,5 +1,6 @@ import { defineTool, ToolError } from 'libllmorbiter'; import { getStore, getProvider } from '../db/store.js'; +import { TOOL_DESCRIPTIONS } from './prompts.js'; const DEFAULT_LIMIT = 3; const MAX_LIMIT = 20; @@ -23,9 +24,7 @@ function parseWords(input) { export function createSearchArticlesTool({ googleAuthId, companyId }) { return defineTool({ name: 'search_articles', - description: - 'Text search articles by name, brand, EAN, SKU, or supplier SKU. ' + - `Returns window of full articles (default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}), offset, and total count. Ordered alphabetically by name.`, + description: TOOL_DESCRIPTIONS.search_articles, strict: false, parameters: { type: 'object', diff --git a/src/tools/search-images.js b/src/tools/search-images.js index 5f1c500..3d05196 100644 --- a/src/tools/search-images.js +++ b/src/tools/search-images.js @@ -4,6 +4,7 @@ import sharp from 'sharp'; import { defineTool, ToolError } from 'libllmorbiter'; import * as dbCompanySetting from '../db/companysetting.js'; import { prepareVfs, syncVfsToDb } from './vfs.js'; +import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; const SERPER_ENDPOINT = 'https://google.serper.dev/images'; const SEARCH_TIMEOUT_MS = 15_000; @@ -136,8 +137,7 @@ async function toAvif(img, { signal } = {}) { export function createSearchImagesTool({ googleAuthId }) { return defineTool({ name: 'search_images', - description: - 'Search Google Images and download candidates to VFS as AVIF. Returns VFS paths for inspection via analyze_image.', + description: TOOL_DESCRIPTIONS.search_images, strict: false, parameters: { type: 'object', @@ -146,23 +146,23 @@ export function createSearchImagesTool({ googleAuthId }) { properties: { note: { type: ['string', 'null'], - description: 'A note explaining what this image search is doing (or null). This note will be displayed to the user while executing.', + description: TOOL_PARAMETER_DESCRIPTIONS.search_images.note, }, query: { type: 'string', - description: 'Search query.', + description: TOOL_PARAMETER_DESCRIPTIONS.search_images.query, }, count: { type: ['number', 'null'], - description: `Candidate count or null. Default ${DEFAULT_COUNT}, max ${MAX_COUNT}.`, + description: TOOL_PARAMETER_DESCRIPTIONS.search_images.count, }, gl: { type: ['string', 'null'], - description: 'Country code or null. Default "de".', + description: TOOL_PARAMETER_DESCRIPTIONS.search_images.gl, }, hl: { type: ['string', 'null'], - description: 'Language code or null. Default "de".', + description: TOOL_PARAMETER_DESCRIPTIONS.search_images.hl, }, }, }, diff --git a/src/tools/set-user-route.js b/src/tools/set-user-route.js index 82f9ba9..d5d69d8 100644 --- a/src/tools/set-user-route.js +++ b/src/tools/set-user-route.js @@ -1,4 +1,5 @@ import { defineTool } from 'libllmorbiter'; +import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; /** * Creates the set-user-route tool. @@ -10,8 +11,7 @@ import { defineTool } from 'libllmorbiter'; export function createSetUserRouteTool({ onRoute } = {}) { return defineTool({ name: 'set-user-route', - description: - '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). Refer to the User Perspective Route Construction Matrix for valid route patterns.', + description: TOOL_DESCRIPTIONS.set_user_route, strict: false, parameters: { type: 'object', @@ -20,7 +20,7 @@ export function createSetUserRouteTool({ onRoute } = {}) { properties: { route: { type: 'string', - description: 'Target frontend route path according to the Route Construction Matrix (e.g. "/artikel/ART-10042", "/kategorien/elektronik/kabel", "/auftrag/1001").', + description: TOOL_PARAMETER_DESCRIPTIONS.set_user_route.route, }, }, }, diff --git a/src/tools/url-get.js b/src/tools/url-get.js index c13d431..273729a 100644 --- a/src/tools/url-get.js +++ b/src/tools/url-get.js @@ -1,5 +1,6 @@ import { defineTool, ToolError } from 'libllmorbiter'; import * as dbCompanySetting from '../db/companysetting.js'; +import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js'; const SERPER_SCRAPE_ENDPOINT = 'https://scrape.serper.dev'; const SCRAPE_TIMEOUT_MS = 25_000; @@ -63,7 +64,7 @@ async function serperScrape(url, apiKey, { signal } = {}) { export function createUrlGetTool({ googleAuthId }) { return defineTool({ name: 'url_get', - description: 'Fetch and scrape web page content via Serper Scrape (text, markdown, metadata).', + description: TOOL_DESCRIPTIONS.url_get, strict: false, parameters: { type: 'object', @@ -72,7 +73,7 @@ export function createUrlGetTool({ googleAuthId }) { properties: { url: { type: 'string', - description: 'Target URL.', + description: TOOL_PARAMETER_DESCRIPTIONS.url_get.url, }, }, }, diff --git a/src/tools/web-search.js b/src/tools/web-search.js index 3d5d53b..c23037a 100644 --- a/src/tools/web-search.js +++ b/src/tools/web-search.js @@ -1,5 +1,6 @@ import { defineTool, ToolError } from 'libllmorbiter'; import * as dbCompanySetting from '../db/companysetting.js'; +import { TOOL_DESCRIPTIONS } from './prompts.js'; const SERPER_SEARCH_ENDPOINT = 'https://google.serper.dev/search'; const SEARCH_TIMEOUT_MS = 15_000; @@ -88,8 +89,7 @@ async function serperWebSearch(query, apiKey, { gl = 'de', hl = 'de', tbs, page export function createWebSearchTool({ googleAuthId }) { return defineTool({ name: 'web_search', - description: - 'Search Google via Serper.dev. Use timePeriod "day" or "week" for news/recent events. Returns 10 results per page.', + description: TOOL_DESCRIPTIONS.web_search, strict: false, parameters: { type: 'object',