diff --git a/src/services/conversationManager.js b/src/services/conversationManager.js index ccc77db..9a18dd1 100644 --- a/src/services/conversationManager.js +++ b/src/services/conversationManager.js @@ -109,9 +109,9 @@ async function buildSystemPrompt(vfsRoot, googleAuthId) { `Date: ${today}`, '', 'YOUR TOOLS:', - '1. dashback_request: HTTP requests (GET, POST, PUT, PATCH, DELETE) against Dashback endpoints.', + '1. dashback_request: HTTP requests (GET, POST, PUT, PATCH, DELETE) against QuixPOS Article Management System.', '2. ask_user: Prompt user for clarification or decisions. Pauses execution until response.', - '3. javascript_execute: Sandboxed Node environment for computation, aggregation, and binary I/O. Assign module.exports. API: fetch("/path"). VFS: fs/vfs.', + '3. javascript_execute: Sandboxed Node environment for computation, aggregation, and binary I/O. Assign module.exports. API: fetch("/path"). vfs.', '4. find_files: List VFS files. Set skills=true for SKILL files.', '5. read_file: Read VFS text file (seek/limit windowed).', '6. patch: Create, overwrite, or replace text in VFS files (only text write path; creates SKILL files).', @@ -126,13 +126,12 @@ async function buildSystemPrompt(vfsRoot, googleAuthId) { '', 'VIRTUAL FILESYSTEM:', '- Persistent per-company VFS. Paths are root-relative forward-slash (e.g. "labels/sheet1.png").', - '- Text: list with find_files, read with read_file, write with patch. Binary/buffers: javascript_execute.', - '- Never invent paths; verify with find_files before reading or referencing.', + '- Text: list with find_files, read with read_file, write with patch.', '', 'SKILL FILES:', '- Reusable instruction docs: ".SKILL" at VFS root. First line is title, followed by markdown.', '- Root-only (no "/"). Discover via find_files({ skills: true }).', - '- Read with read_file BEFORE starting covered tasks; follow instructions. Propose saving improved procedures as new SKILL files.', + '- Read with read_file BEFORE starting covered tasks. Propose saving improved procedures as new SKILL files.', '- Exempt from reset_vfs. Delete permanently via rm.', '', buildSkillIndex(vfsRoot), @@ -143,10 +142,7 @@ async function buildSystemPrompt(vfsRoot, googleAuthId) { '- `image/` is READ-ONLY database storage. Write generated images elsewhere (e.g. "images/…").', '', 'OPERATING GUIDELINES:', - '- Endpoint reference: see DASHBACK API ROUTES below.', - '- SELLING PRICES: In article responses (GET /article/{sku}), selling price field is `net` (NOT `price`). Schema in `article.prices`: { minmultiplier, net, taxRate, customergroupId }.', '- GROSS vs NET PRICES: User prices are almost always GROSS (incl. VAT); API requires NET. Formula: `net = gross / (1 + taxRate / 100)`. Compute in javascript_execute with high precision (4–6 decimals) to prevent rounding error.', - '- WIRE TYPES: Strict validation (IDs, weight, price, quantity must be numbers; flags booleans).', '- DISPLAYING GENERATED IMAGES: Inline markdown images supported. Embed written VFS image paths: `![descriptive alt](artifacts/example.png)`. Other files (PDF, JSON, CSV) do not render inline; provide download links.', '- LABEL TEMPLATES (/labeltemplate): Must use Dashback JSON schema `{ "objects": [ ... ] }`. Do NOT use ZPL.', '- Format output with markdown tables, bullet points, or bold key-value pairs.', @@ -168,6 +164,38 @@ async function buildSystemPrompt(vfsRoot, googleAuthId) { * `idle`/`busy` state. The finer SSE states (`thinking`, `working`, `done`) * and the streamed `delta` chunks are SSE-only. */ +function parseUtcDate(val) { + if (!val) return new Date(); + if (val instanceof Date) return isNaN(val.getTime()) ? new Date() : val; + if (typeof val === 'string') { + const s = val.trim(); + if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}/.test(s) && !s.endsWith('Z') && !/[+-]\d{2}(:?\d{2})?$/.test(s)) { + const parsed = new Date(s.replace(' ', 'T') + 'Z'); + if (!isNaN(parsed.getTime())) return parsed; + } + const parsed = new Date(s); + if (!isNaN(parsed.getTime())) return parsed; + } + return new Date(); +} + +export function formatTimePrefix(createdDate) { + const d = parseUtcDate(createdDate); + const parts = new Intl.DateTimeFormat('en-GB', { + timeZone: 'Europe/Berlin', + hour: '2-digit', + minute: '2-digit', + hour12: false, + timeZoneName: 'short', + }).formatToParts(d); + const hour = parts.find((p) => p.type === 'hour')?.value.padStart(2, '0') || '00'; + const minute = parts.find((p) => p.type === 'minute')?.value.padStart(2, '0') || '00'; + let tz = parts.find((p) => p.type === 'timeZoneName')?.value || 'MEST'; + if (tz === 'CEST') tz = 'MEST'; + else if (tz === 'CET') tz = 'MET'; + return `[${hour}:${minute}${tz}] `; +} + export function historyToLlmMessages(history) { const messages = []; for (const m of history) { @@ -179,7 +207,12 @@ export function historyToLlmMessages(history) { payload = { content: m.message }; } if (m.role === 'user') { - messages.push({ role: 'user', content: payload.content ?? (typeof m.message === 'string' ? m.message : '') }); + const rawContent = payload.content ?? (typeof m.message === 'string' ? m.message : ''); + const timePrefix = formatTimePrefix(m.created_at); + const content = /^\s*\[\d{2}:\d{2}[A-Za-z]+\]\s*/.test(rawContent) + ? rawContent + : `${timePrefix}${rawContent}`; + messages.push({ role: 'user', content }); } else if (m.role === 'assistant') { messages.push({ role: 'assistant',