This commit is contained in:
seb
2026-10-11 12:58:15 +02:00
parent 8262e35e53
commit 1149ffddcb
24 changed files with 575 additions and 261 deletions

View File

@@ -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." 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 # Naming Conventions — dashback
Apply these conventions when writing or refactoring code in this project. Apply these conventions when writing or refactoring code in this project.

View File

@@ -22,6 +22,7 @@ process.on('SIGTERM', () => {
shutdown().then(() => process.exit(0)); shutdown().then(() => process.exit(0));
}); });
// MARK: calls startApi from ./api/index.js
startApi().catch((err) => { startApi().catch((err) => {
sysLog.error('Failed to start API:', err.message); sysLog.error('Failed to start API:', err.message);
process.exit(1); process.exit(1);

View File

@@ -1,6 +1,5 @@
import { LLMOrbiter, OpenRouterProvider } from 'libllmorbiter'; import { LLMOrbiter, OpenRouterProvider } from 'libllmorbiter';
import * as db from '../db/modelconversation.js'; import * as db from '../db/modelconversation.js';
import * as dbCompanySetting from '../db/companysetting.js';
import { getStore } from '../db/store.js'; import { getStore } from '../db/store.js';
import env from '../config/env.js'; import env from '../config/env.js';
import { emit as emitCompanyEvent } from '../api/sse.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 { resolve, dirname, extname } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { randomUUID } from 'node:crypto'; import { randomUUID } from 'node:crypto';
import { buildSystemPrompt } from './prompts.js';
import { createDashbackRequestTool } from '../tools/dashback-request.js'; import { createDashbackRequestTool } from '../tools/dashback-request.js';
import { createAskUserTool } from '../tools/ask-user.js'; import { createAskUserTool } from '../tools/ask-user.js';
import { createJavascriptExecuteTool } from '../tools/javascript.js'; import { createJavascriptExecuteTool } from '../tools/javascript.js';
import { createResetVfsTool } from '../tools/reset-vfs.js'; import { createResetVfsTool } from '../tools/reset-vfs.js';
import { resolveVfsRoot, prepareVfs, getMimeType } from '../tools/vfs.js'; import { resolveVfsRoot, prepareVfs, getMimeType } from '../tools/vfs.js';
import { listSkillFiles } from '../tools/skill.js';
import { createApiSuggestionsOrIssuesTool } from '../tools/apisuggestionsorissues.js'; import { createApiSuggestionsOrIssuesTool } from '../tools/apisuggestionsorissues.js';
import { createFindFilesTool } from '../tools/find-files.js'; import { createFindFilesTool } from '../tools/find-files.js';
import { createReadFileTool } from '../tools/read-file.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 { createSetUserRouteTool } from '../tools/set-user-route.js';
import { resolveOpenRouterKey, resolveDefaultModel, resolveModelReasoning, RETIRED_MODELS, hasConfiguredOpenRouterKey } from '../tools/llm-key.js'; import { resolveOpenRouterKey, resolveDefaultModel, resolveModelReasoning, RETIRED_MODELS, hasConfiguredOpenRouterKey } from '../tools/llm-key.js';
// #region Constants
const KEEP_ALIVE_MS = 20_000; 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; // #region Message Helpers
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');
}
/**
* 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>}
*/
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/<hash>"). 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 "<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.',
'',
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-<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.',
'',
'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-<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');
const sections = [];
if (prefix) sections.push(prefix);
sections.push(basePrompt);
if (suffix) sections.push(suffix);
return sections.join('\n\n');
}
/** /**
* Per-conversation manager. Owns the SSE subscribers of one conversation and * Per-conversation manager. Owns the SSE subscribers of one conversation and
@@ -391,6 +243,10 @@ export function historyToLlmMessages(history) {
return messages; return messages;
} }
// #endregion
// #region ConversationManager
export class ConversationManager { export class ConversationManager {
/** /**
* @param {number} conversationId * @param {number} conversationId
@@ -408,6 +264,8 @@ export class ConversationManager {
this.isProcessing = false; this.isProcessing = false;
} }
// #region Subscription & Broadcast
/** /**
* Broadcast an SSE event to all subscribers of this conversation. * Broadcast an SSE event to all subscribers of this conversation.
* @param {string} event * @param {string} event
@@ -461,6 +319,10 @@ export class ConversationManager {
return unsubscribe; return unsubscribe;
} }
// #endregion
// #region Attachments
/** /**
* Saves attachments sent with a user message to the company's VFS * Saves attachments sent with a user message to the company's VFS
* under `attachments/<filename>` and registers them in `modelvfs`. * under `attachments/<filename>` and registers them in `modelvfs`.
@@ -554,6 +416,10 @@ export class ConversationManager {
return tags; return tags;
} }
// #endregion
// #region Message Handlers
/** /**
* Handle an incoming user message. * Handle an incoming user message.
* 1. Extracts text content and saves any attachments into the company VFS (`attachments/`). * 1. Extracts text content and saves any attachments into the company VFS (`attachments/`).
@@ -649,7 +515,9 @@ export class ConversationManager {
return toolMsg; return toolMsg;
} }
// #endregion
// #region LLM Run
/** /**
* Run the LLM over the stored transcript, stream the answer over SSE, * Run the LLM over the stored transcript, stream the answer over SSE,
* persist reasoning, tool calls, and assistant messages, then return to `idle`. * 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' }); this.broadcast('state', error ? { state: 'done', error } : { state: 'done' });
} }
// #endregion
// #region Teardown
destroy() { destroy() {
this.destroyed = true; this.destroyed = true;
this.orbiter?.abort('Conversation manager closed'); this.orbiter?.abort('Conversation manager closed');
@@ -958,8 +830,14 @@ export class ConversationManager {
} }
this.subscribers.clear(); this.subscribers.clear();
} }
// #endregion
} }
// #endregion
// #region Manager Registry
/** @type {Map<number, ConversationManager>} */ /** @type {Map<number, ConversationManager>} */
const activeManagers = new Map(); const activeManagers = new Map();
@@ -1011,3 +889,5 @@ export function closeAllManagers() {
activeManagers.delete(id); activeManagers.delete(id);
} }
} }
// #endregion

1
src/services/prompts.js Normal file
View File

@@ -0,0 +1 @@
export * from '../tools/prompts.js';

View File

@@ -7,6 +7,12 @@ import env from '../config/env.js';
import { getImageByHash } from '../db/pic.js'; import { getImageByHash } from '../db/pic.js';
import { prepareVfs, extractFullHash } from './vfs.js'; import { prepareVfs, extractFullHash } from './vfs.js';
import { resolveOpenRouterKey, resolveDefaultModel } from './llm-key.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. * 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 }) { export function createAnalyzeImageTool({ googleAuthId, model }) {
return defineTool({ return defineTool({
name: 'analyze_image', name: 'analyze_image',
description: description: TOOL_DESCRIPTIONS.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/<hash>"). Returns model evaluation.',
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -286,23 +290,23 @@ export function createAnalyzeImageTool({ googleAuthId, model }) {
properties: { properties: {
note: { note: {
type: ['string', 'null'], 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: { paths: {
type: 'array', type: 'array',
description: '1–9 VFS paths or DB paths ("image/<hash>").', description: TOOL_PARAMETER_DESCRIPTIONS.analyze_image.paths,
items: { type: 'string' }, items: { type: 'string' },
minItems: 1, minItems: 1,
maxItems: MAX_IMAGES, maxItems: MAX_IMAGES,
}, },
prompt: { prompt: {
type: ['string', 'null'], type: ['string', 'null'],
description: 'Inspection criteria or question, or null.', description: TOOL_PARAMETER_DESCRIPTIONS.analyze_image.prompt,
}, },
grid: { grid: {
type: ['string', 'null'], type: ['string', 'null'],
enum: [...Object.keys(GRID_CONFIGS), 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 ────────────────────────────────────── // ── Ask the vision model ──────────────────────────────────────
const question = typeof prompt === 'string' && prompt.trim() ? prompt.trim() : null; const question = typeof prompt === 'string' && prompt.trim() ? prompt.trim() : null;
const text = items.length > 1 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` + `The grid is ${shape.name}. Each image has its file name printed beneath it:\n` +
items.map((it, idx) => { items.map((it, idx) => {
const row = Math.floor(idx / GRID_CONFIGS[shape.name].cols) + 1; const row = Math.floor(idx / GRID_CONFIGS[shape.name].cols) + 1;
const col = (idx % GRID_CONFIGS[shape.name].cols) + 1; const col = (idx % GRID_CONFIGS[shape.name].cols) + 1;
return `- Row ${row}, Col ${col}: "${it.name}"`; return `- Row ${row}, Col ${col}: "${it.name}"`;
}).join('\n') }).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 apiKey = await resolveOpenRouterKey(googleAuthId);
const visionModel = (await resolveDefaultModel(googleAuthId)) || model; const visionModel = (await resolveDefaultModel(googleAuthId)) || model;

View File

@@ -1,5 +1,6 @@
import { defineTool } from 'libllmorbiter'; import { defineTool } from 'libllmorbiter';
import * as db from '../db/company.js'; 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 * 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 }) { export function createApiSuggestionsOrIssuesTool({ googleAuthId, companyId }) {
return defineTool({ return defineTool({
name: 'apisuggestionsorissues', name: 'apisuggestionsorissues',
description: 'Report missing API endpoint, feature request, bug, or issue for triage.', description: TOOL_DESCRIPTIONS.apisuggestionsorissues,
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -20,16 +21,16 @@ export function createApiSuggestionsOrIssuesTool({ googleAuthId, companyId }) {
properties: { properties: {
text: { text: {
type: 'string', type: 'string',
description: 'Issue or request details.', description: TOOL_PARAMETER_DESCRIPTIONS.apisuggestionsorissues.text,
}, },
type: { type: {
type: ['string', 'null'], type: ['string', 'null'],
enum: ['missing_api', 'bug', 'suggestion', 'issue', 'other', null], enum: ['missing_api', 'bug', 'suggestion', 'issue', 'other', null],
description: 'Report category or null.', description: TOOL_PARAMETER_DESCRIPTIONS.apisuggestionsorissues.type,
}, },
title: { title: {
type: ['string', 'null'], type: ['string', 'null'],
description: 'Short summary or null.', description: TOOL_PARAMETER_DESCRIPTIONS.apisuggestionsorissues.title,
}, },
}, },
}, },

View File

@@ -1,4 +1,5 @@
import { defineTool, DirectReturnError } from 'libllmorbiter'; import { defineTool, DirectReturnError } from 'libllmorbiter';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
/** /**
* ask_user tool: * ask_user tool:
@@ -13,8 +14,7 @@ import { defineTool, DirectReturnError } from 'libllmorbiter';
export function createAskUserTool() { export function createAskUserTool() {
return defineTool({ return defineTool({
name: 'ask_user', name: 'ask_user',
description: description: TOOL_DESCRIPTIONS.ask_user,
'Prompt user for clarification or decision with concise question and up to 3 options.',
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -23,11 +23,11 @@ export function createAskUserTool() {
properties: { properties: {
question: { question: {
type: 'string', type: 'string',
description: 'Question prompt.', description: TOOL_PARAMETER_DESCRIPTIONS.ask_user.question,
}, },
options: { options: {
type: ['array', 'null'], type: ['array', 'null'],
description: 'Up to 3 option labels, or null.', description: TOOL_PARAMETER_DESCRIPTIONS.ask_user.options,
items: { type: 'string' }, items: { type: 'string' },
maxItems: 3, maxItems: 3,
}, },

View File

@@ -1,6 +1,7 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { defineTool } from 'libllmorbiter'; import { defineTool } from 'libllmorbiter';
import env from '../config/env.js'; import env from '../config/env.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
const MAX_RESPONSE_CHARS = 20_000; const MAX_RESPONSE_CHARS = 20_000;
const REQUEST_TIMEOUT_MS = 30_000; const REQUEST_TIMEOUT_MS = 30_000;
@@ -14,7 +15,7 @@ const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
export function createDashbackRequestTool({ googleAuthId }) { export function createDashbackRequestTool({ googleAuthId }) {
return defineTool({ return defineTool({
name: 'dashback_request', name: 'dashback_request',
description: 'Call Dashback REST API. Paths are root-relative (e.g. "/article/2").', description: TOOL_DESCRIPTIONS.dashback_request,
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -23,12 +24,12 @@ export function createDashbackRequestTool({ googleAuthId }) {
properties: { properties: {
note: { note: {
type: ['string', 'null'], 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.' }, method: { type: 'string', enum: METHODS, description: TOOL_PARAMETER_DESCRIPTIONS.dashback_request.method },
path: { type: 'string', description: 'Endpoint path (e.g. "/article/2").' }, path: { type: 'string', description: TOOL_PARAMETER_DESCRIPTIONS.dashback_request.path },
query: { type: ['object', 'null'], description: 'Query parameters object or null.' }, query: { type: ['object', 'null'], description: TOOL_PARAMETER_DESCRIPTIONS.dashback_request.query },
body: { type: ['string', 'null'], description: 'JSON request payload string or null.' }, body: { type: ['string', 'null'], description: TOOL_PARAMETER_DESCRIPTIONS.dashback_request.body },
}, },
}, },
invoke: async ({ method = 'GET', path, query = null, body = null, note }) => { invoke: async ({ method = 'GET', path, query = null, body = null, note }) => {

View File

@@ -1,4 +1,5 @@
import { defineTool, ToolError } from 'libllmorbiter'; 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. * Calculates the standard GS1 check digit for a string of numeric digits.
@@ -44,9 +45,7 @@ function resolveBarcodeFormat(length) {
export function createEanChecksumTool() { export function createEanChecksumTool() {
return defineTool({ return defineTool({
name: 'ean_checksum', name: 'ean_checksum',
description: description: TOOL_DESCRIPTIONS.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.',
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -55,16 +54,16 @@ export function createEanChecksumTool() {
properties: { properties: {
note: { note: {
type: ['string', 'null'], 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: { code: {
type: 'string', 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: { action: {
type: ['string', 'null'], type: ['string', 'null'],
enum: ['auto', 'validate', 'calculate', 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,
}, },
}, },
}, },

View File

@@ -7,6 +7,7 @@ import env from '../config/env.js';
import { getImageByHash } from '../db/pic.js'; import { getImageByHash } from '../db/pic.js';
import { prepareVfs, syncVfsToDb, extractFullHash, isReadOnlyDatabasePath } from './vfs.js'; import { prepareVfs, syncVfsToDb, extractFullHash, isReadOnlyDatabasePath } from './vfs.js';
import { resolveOpenRouterKey, resolveImageModel } from './llm-key.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. * 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 }) { export function createEditImageTool({ googleAuthId, model: fallbackModel }) {
return defineTool({ return defineTool({
name: 'edit_image', name: 'edit_image',
description: description: TOOL_DESCRIPTIONS.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).',
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',

View File

@@ -3,6 +3,7 @@ import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import { isSkillPath, listSkillFiles, SKILL_EXT } from './skill.js'; import { isSkillPath, listSkillFiles, SKILL_EXT } from './skill.js';
import { prepareVfs } from './vfs.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). * 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 }) { export function createFindFilesTool({ googleAuthId }) {
return defineTool({ return defineTool({
name: 'find_files', name: 'find_files',
description: description: TOOL_DESCRIPTIONS.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).',
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -90,27 +89,27 @@ export function createFindFilesTool({ googleAuthId }) {
properties: { properties: {
dirname: { dirname: {
type: ['string', 'null'], 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: { recursive: {
type: ['boolean', 'null'], type: ['boolean', 'null'],
description: 'Walk subdirectories or null. Default false.', description: TOOL_PARAMETER_DESCRIPTIONS.find_files.recursive,
}, },
skills: { skills: {
type: ['boolean', 'null'], 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: { seek: {
type: ['number', 'null'], type: ['number', 'null'],
description: 'Pagination offset or null. Default 0.', description: TOOL_PARAMETER_DESCRIPTIONS.find_files.seek,
}, },
limit: { limit: {
type: ['number', 'null'], type: ['number', 'null'],
description: `Max entries to return or null. Default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}.`, description: TOOL_PARAMETER_DESCRIPTIONS.find_files.limit,
}, },
maxDepth: { maxDepth: {
type: ['number', 'null'], type: ['number', 'null'],
description: 'Max recursive directory depth or null. Default 3.', description: TOOL_PARAMETER_DESCRIPTIONS.find_files.maxDepth,
}, },
}, },
}, },

View File

@@ -3,6 +3,7 @@ import path from 'path';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import env from '../config/env.js'; import env from '../config/env.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
import { createVfs, prepareVfs, syncVfsToDb } from './vfs.js'; import { createVfs, prepareVfs, syncVfsToDb } from './vfs.js';
import { runSandboxJob } from '../services/javascriptSandbox.js'; import { runSandboxJob } from '../services/javascriptSandbox.js';
@@ -271,17 +272,7 @@ function formatSandboxError(code, report) {
export function createJavascriptExecuteTool({ googleAuthId }) { export function createJavascriptExecuteTool({ googleAuthId }) {
return defineTool({ return defineTool({
name: 'javascript_execute', name: 'javascript_execute',
description: description: TOOL_DESCRIPTIONS.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 <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).',
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -289,28 +280,28 @@ export function createJavascriptExecuteTool({ googleAuthId }) {
properties: { properties: {
note: { note: {
type: ['string', 'null'], 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: { code: {
type: 'string', 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: { appletvfsfile: {
type: 'string', 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: { applet_input: {
type: 'object', 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, additionalProperties: true,
}, },
timeout: { timeout: {
type: ['number', 'null'], 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: { max_output: {
type: ['number', 'null'], type: ['number', 'null'],
description: `Max stdout chars or null. Default ${DEFAULT_MAX_OUTPUT}.`, description: TOOL_PARAMETER_DESCRIPTIONS.javascript_execute.max_output,
}, },
}, },
}, },

View File

@@ -1,4 +1,5 @@
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import { TOOL_DESCRIPTIONS } from './prompts.js';
const DEFAULT_TAX_RATE = 19; const DEFAULT_TAX_RATE = 19;
const DEFAULT_PRECISION = 6; const DEFAULT_PRECISION = 6;
@@ -109,9 +110,7 @@ export function calculatePrice({ net, gross, amount, from, taxRate = DEFAULT_TAX
export function createNetGrossCalcTool() { export function createNetGrossCalcTool() {
return defineTool({ return defineTool({
name: 'net_gross_calc', name: 'net_gross_calc',
description: description: TOOL_DESCRIPTIONS.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.',
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',

View File

@@ -4,6 +4,7 @@ import { defineTool, ToolError } from 'libllmorbiter';
import { isReadOnlyDatabasePath } from './vfs.js'; import { isReadOnlyDatabasePath } from './vfs.js';
import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js'; import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js';
import { prepareVfs, syncVfsToDb } from './vfs.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). * patch — applies edit operations to files in the virtual filesystem (VFS).
@@ -66,11 +67,7 @@ function toLines(text) {
export function createPatchTool({ googleAuthId }) { export function createPatchTool({ googleAuthId }) {
return defineTool({ return defineTool({
name: 'patch', name: 'patch',
description: description: TOOL_DESCRIPTIONS.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 "<slug>.SKILL" (line 1 is title, or provide title field).',
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -79,7 +76,7 @@ export function createPatchTool({ googleAuthId }) {
properties: { properties: {
operations: { operations: {
type: 'array', type: 'array',
description: 'Ordered patch operations.', description: PATCH_PARAMETER_DESCRIPTIONS.operations,
items: { items: {
type: 'object', type: 'object',
additionalProperties: false, additionalProperties: false,
@@ -87,23 +84,23 @@ export function createPatchTool({ googleAuthId }) {
properties: { properties: {
file: { file: {
type: 'string', type: 'string',
description: 'VFS path (e.g. "notes.md", "pipeline.SKILL"). SKILL files must be root-level (no "/").', description: PATCH_PARAMETER_DESCRIPTIONS.file,
}, },
search: { search: {
type: ['string', 'null'], type: ['string', 'null'],
description: 'Exact text to replace. null/omitted = overwrite or create file.', description: PATCH_PARAMETER_DESCRIPTIONS.search,
}, },
replace: { replace: {
type: 'string', type: 'string',
description: 'Replacement text.', description: PATCH_PARAMETER_DESCRIPTIONS.replace,
}, },
title: { title: {
type: ['string', 'null'], type: ['string', 'null'],
description: 'Optional line-1 title for SKILL files or null.', description: PATCH_PARAMETER_DESCRIPTIONS.title,
}, },
skiplines: { skiplines: {
type: ['integer', 'null'], type: ['integer', 'null'],
description: 'Lines to skip before searching or null.', description: PATCH_PARAMETER_DESCRIPTIONS.skiplines,
}, },
}, },
}, },

View File

@@ -3,6 +3,7 @@ import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import { pdfToText, splitPdfPages, pdfToPng } from 'pdfconvert-wasm'; import { pdfToText, splitPdfPages, pdfToPng } from 'pdfconvert-wasm';
import { prepareVfs, syncVfsToDb } from './vfs.js'; import { prepareVfs, syncVfsToDb } from './vfs.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
const IMAGE_FOLDER = 'images'; const IMAGE_FOLDER = 'images';
const DEFAULT_DPI = 150; const DEFAULT_DPI = 150;
@@ -42,8 +43,7 @@ function slugifyBase(filename) {
export function createPdfReadTool({ googleAuthId }) { export function createPdfReadTool({ googleAuthId }) {
return defineTool({ return defineTool({
name: 'pdf_read', name: 'pdf_read',
description: description: TOOL_DESCRIPTIONS.pdf_read,
'Read and convert PDF/text documents to text or VFS PNG images (stored in "images/").',
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -52,20 +52,20 @@ export function createPdfReadTool({ googleAuthId }) {
properties: { properties: {
path: { path: {
type: 'string', type: 'string',
description: 'VFS path or HTTP(S) URL.', description: TOOL_PARAMETER_DESCRIPTIONS.pdf_read.path,
}, },
format: { format: {
type: ['string', 'null'], type: ['string', 'null'],
enum: ['txt', 'png', 'both', 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: { page: {
type: ['number', 'null'], type: ['number', 'null'],
description: '1-based page number or null for all pages.', description: TOOL_PARAMETER_DESCRIPTIONS.pdf_read.page,
}, },
dpi: { dpi: {
type: ['number', 'null'], type: ['number', 'null'],
description: 'PNG render DPI or null. Default 150.', description: TOOL_PARAMETER_DESCRIPTIONS.pdf_read.dpi,
}, },
}, },
}, },

436
src/tools/prompts.js Normal file
View File

@@ -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 <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 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 "<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. 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 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/<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 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/<hash>"). 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 "<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:',
'- 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-<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_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

View File

@@ -4,6 +4,7 @@ import { defineTool, ToolError } from 'libllmorbiter';
import { isSkillPath, parseSkill, SKILL_EXT } from './skill.js'; import { isSkillPath, parseSkill, SKILL_EXT } from './skill.js';
import { getApiRouteSkill } from './routes-skills.js'; import { getApiRouteSkill } from './routes-skills.js';
import { prepareVfs } from './vfs.js'; import { prepareVfs } from './vfs.js';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
const DEFAULT_LIMIT = 400; const DEFAULT_LIMIT = 400;
const MAX_LIMIT = 5000; const MAX_LIMIT = 5000;
@@ -61,10 +62,7 @@ function looksBinary(buf) {
export function createReadFileTool({ googleAuthId }) { export function createReadFileTool({ googleAuthId }) {
return defineTool({ return defineTool({
name: 'read_file', name: 'read_file',
description: description: TOOL_DESCRIPTIONS.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).',
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -73,15 +71,15 @@ export function createReadFileTool({ googleAuthId }) {
properties: { properties: {
filename: { filename: {
type: 'string', type: 'string',
description: 'VFS file path.', description: TOOL_PARAMETER_DESCRIPTIONS.read_file.filename,
}, },
seek: { seek: {
type: ['number', 'null'], type: ['number', 'null'],
description: 'Line offset (0-indexed) or null. Default 0.', description: TOOL_PARAMETER_DESCRIPTIONS.read_file.seek,
}, },
limit: { limit: {
type: ['number', 'null'], type: ['number', 'null'],
description: `Max lines to return or null. Default ${DEFAULT_LIMIT}, max ${MAX_LIMIT}.`, description: TOOL_PARAMETER_DESCRIPTIONS.read_file.limit,
}, },
}, },
}, },

View File

@@ -3,6 +3,7 @@ import path from 'path';
import { defineTool } from 'libllmorbiter'; import { defineTool } from 'libllmorbiter';
import { isSkillPath } from './skill.js'; import { isSkillPath } from './skill.js';
import { resolveVfsRoot } from './vfs.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. * reset_vfs — clears throwaway artifacts out of the virtual filesystem.
@@ -26,7 +27,7 @@ import { resolveVfsRoot } from './vfs.js';
export function createResetVfsTool({ googleAuthId }) { export function createResetVfsTool({ googleAuthId }) {
return defineTool({ return defineTool({
name: 'reset_vfs', name: 'reset_vfs',
description: 'Clear VFS files, images, and artifacts. Preserves SKILL files by default.', description: TOOL_DESCRIPTIONS.reset_vfs,
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -34,7 +35,7 @@ export function createResetVfsTool({ googleAuthId }) {
properties: { properties: {
include_skills: { include_skills: {
type: ['boolean', 'null'], type: ['boolean', 'null'],
description: 'Delete SKILL files too (or null). Default false.', description: TOOL_PARAMETER_DESCRIPTIONS.reset_vfs.include_skills,
}, },
}, },
}, },

View File

@@ -3,6 +3,7 @@ import path from 'path';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import { isReadOnlyDatabasePath, prepareVfs } from './vfs.js'; import { isReadOnlyDatabasePath, prepareVfs } from './vfs.js';
import { isSkillPath } from './skill.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). * rm — deletes a single file from the virtual filesystem (VFS).
@@ -64,7 +65,7 @@ export function deleteVfsFile(vfsRoot, relPath) {
export function createRmTool({ googleAuthId }) { export function createRmTool({ googleAuthId }) {
return defineTool({ return defineTool({
name: 'rm', name: 'rm',
description: 'Delete single VFS file (e.g. specific artifact or SKILL file). Irreversible.', description: TOOL_DESCRIPTIONS.rm,
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -73,7 +74,7 @@ export function createRmTool({ googleAuthId }) {
properties: { properties: {
filename: { filename: {
type: 'string', type: 'string',
description: 'VFS file path to delete.', description: TOOL_PARAMETER_DESCRIPTIONS.rm.filename,
}, },
}, },
}, },

View File

@@ -1,5 +1,6 @@
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import { getStore, getProvider } from '../db/store.js'; import { getStore, getProvider } from '../db/store.js';
import { TOOL_DESCRIPTIONS } from './prompts.js';
const DEFAULT_LIMIT = 3; const DEFAULT_LIMIT = 3;
const MAX_LIMIT = 20; const MAX_LIMIT = 20;
@@ -23,9 +24,7 @@ function parseWords(input) {
export function createSearchArticlesTool({ googleAuthId, companyId }) { export function createSearchArticlesTool({ googleAuthId, companyId }) {
return defineTool({ return defineTool({
name: 'search_articles', name: 'search_articles',
description: description: TOOL_DESCRIPTIONS.search_articles,
'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.`,
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',

View File

@@ -4,6 +4,7 @@ import sharp from 'sharp';
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import * as dbCompanySetting from '../db/companysetting.js'; import * as dbCompanySetting from '../db/companysetting.js';
import { prepareVfs, syncVfsToDb } from './vfs.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 SERPER_ENDPOINT = 'https://google.serper.dev/images';
const SEARCH_TIMEOUT_MS = 15_000; const SEARCH_TIMEOUT_MS = 15_000;
@@ -136,8 +137,7 @@ async function toAvif(img, { signal } = {}) {
export function createSearchImagesTool({ googleAuthId }) { export function createSearchImagesTool({ googleAuthId }) {
return defineTool({ return defineTool({
name: 'search_images', name: 'search_images',
description: description: TOOL_DESCRIPTIONS.search_images,
'Search Google Images and download candidates to VFS as AVIF. Returns VFS paths for inspection via analyze_image.',
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -146,23 +146,23 @@ export function createSearchImagesTool({ googleAuthId }) {
properties: { properties: {
note: { note: {
type: ['string', 'null'], 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: { query: {
type: 'string', type: 'string',
description: 'Search query.', description: TOOL_PARAMETER_DESCRIPTIONS.search_images.query,
}, },
count: { count: {
type: ['number', 'null'], type: ['number', 'null'],
description: `Candidate count or null. Default ${DEFAULT_COUNT}, max ${MAX_COUNT}.`, description: TOOL_PARAMETER_DESCRIPTIONS.search_images.count,
}, },
gl: { gl: {
type: ['string', 'null'], type: ['string', 'null'],
description: 'Country code or null. Default "de".', description: TOOL_PARAMETER_DESCRIPTIONS.search_images.gl,
}, },
hl: { hl: {
type: ['string', 'null'], type: ['string', 'null'],
description: 'Language code or null. Default "de".', description: TOOL_PARAMETER_DESCRIPTIONS.search_images.hl,
}, },
}, },
}, },

View File

@@ -1,4 +1,5 @@
import { defineTool } from 'libllmorbiter'; import { defineTool } from 'libllmorbiter';
import { TOOL_DESCRIPTIONS, TOOL_PARAMETER_DESCRIPTIONS } from './prompts.js';
/** /**
* Creates the set-user-route tool. * Creates the set-user-route tool.
@@ -10,8 +11,7 @@ import { defineTool } from 'libllmorbiter';
export function createSetUserRouteTool({ onRoute } = {}) { export function createSetUserRouteTool({ onRoute } = {}) {
return defineTool({ return defineTool({
name: 'set-user-route', name: 'set-user-route',
description: description: TOOL_DESCRIPTIONS.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.',
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -20,7 +20,7 @@ export function createSetUserRouteTool({ onRoute } = {}) {
properties: { properties: {
route: { route: {
type: 'string', 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,
}, },
}, },
}, },

View File

@@ -1,5 +1,6 @@
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import * as dbCompanySetting from '../db/companysetting.js'; 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 SERPER_SCRAPE_ENDPOINT = 'https://scrape.serper.dev';
const SCRAPE_TIMEOUT_MS = 25_000; const SCRAPE_TIMEOUT_MS = 25_000;
@@ -63,7 +64,7 @@ async function serperScrape(url, apiKey, { signal } = {}) {
export function createUrlGetTool({ googleAuthId }) { export function createUrlGetTool({ googleAuthId }) {
return defineTool({ return defineTool({
name: 'url_get', name: 'url_get',
description: 'Fetch and scrape web page content via Serper Scrape (text, markdown, metadata).', description: TOOL_DESCRIPTIONS.url_get,
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',
@@ -72,7 +73,7 @@ export function createUrlGetTool({ googleAuthId }) {
properties: { properties: {
url: { url: {
type: 'string', type: 'string',
description: 'Target URL.', description: TOOL_PARAMETER_DESCRIPTIONS.url_get.url,
}, },
}, },
}, },

View File

@@ -1,5 +1,6 @@
import { defineTool, ToolError } from 'libllmorbiter'; import { defineTool, ToolError } from 'libllmorbiter';
import * as dbCompanySetting from '../db/companysetting.js'; import * as dbCompanySetting from '../db/companysetting.js';
import { TOOL_DESCRIPTIONS } from './prompts.js';
const SERPER_SEARCH_ENDPOINT = 'https://google.serper.dev/search'; const SERPER_SEARCH_ENDPOINT = 'https://google.serper.dev/search';
const SEARCH_TIMEOUT_MS = 15_000; 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 }) { export function createWebSearchTool({ googleAuthId }) {
return defineTool({ return defineTool({
name: 'web_search', name: 'web_search',
description: description: TOOL_DESCRIPTIONS.web_search,
'Search Google via Serper.dev. Use timePeriod "day" or "week" for news/recent events. Returns 10 results per page.',
strict: false, strict: false,
parameters: { parameters: {
type: 'object', type: 'object',