diff --git a/docs/api/openapi.json b/docs/api/openapi.json index eef12c7..362f1e0 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -4330,7 +4330,7 @@ }, "responses": { "200": { - "description": "`text/event-stream` — emits `status`, `llm_call`, `tool_call`, `usage`,\n`progress`, `result` (and `error` on failure). See SSE Events table below.\n\n**ManifestResult shape** (used by `progress` and `result`):\n\n```json\n{\n \"basedata\": {\n \"date\": \"2026-01-15\",\n \"kLieferant\": 42,\n \"lieferantName\": \"Muster GmbH\",\n \"manifestNr\": \"LS-2026-001\",\n \"manifestType\": \"lieferschein\"\n },\n \"notes\": [\"Pos. 3: VPE-Angabe fehlte, Standardwert 1 angenommen.\"],\n \"items\": {\n \"pagesFrom\": 1,\n \"pagesTo\": 2,\n \"items\": [\n {\n \"position\": 1,\n \"count\": 10,\n \"vpe\": 1,\n \"unit\": \"Stk.\",\n \"articleName\": \"Einhängesieb Stahl\",\n \"liefArtNr\": \"ES-100\",\n \"mwst\": \"19\",\n \"eknetto\": 450,\n \"page\": 1\n }\n ]\n }\n}\n```\n", + "description": "`text/event-stream` — emits `status`, `llm_call`, `tool_call`, `usage`,\n`progress`, `result` (and `error` on failure). See SSE Events table below.\n\n**ManifestResult shape** (used by `progress` and `result`):\n\n```json\n{\n \"basedata\": {\n \"date\": \"2026-01-15\",\n \"lieferant_id\": 42,\n \"lieferantName\": \"Muster GmbH\",\n \"manifestNr\": \"LS-2026-001\",\n \"manifestType\": \"lieferschein\"\n },\n \"notes\": [\"Pos. 3: VPE-Angabe fehlte, Standardwert 1 angenommen.\"],\n \"items\": {\n \"pagesFrom\": 1,\n \"pagesTo\": 2,\n \"items\": [\n {\n \"position\": 1,\n \"count\": 10,\n \"vpe\": 1,\n \"unit\": \"Stk.\",\n \"articleName\": \"Einhängesieb Stahl\",\n \"liefArtNr\": \"ES-100\",\n \"mwst\": \"19\",\n \"eknetto\": 450,\n \"page\": 1\n }\n ]\n }\n}\n```\n", "content": { "text/event-stream": { "schema": { diff --git a/docs/api/routes/delivery/parse.md b/docs/api/routes/delivery/parse.md index 5708112..c8cf3f5 100644 --- a/docs/api/routes/delivery/parse.md +++ b/docs/api/routes/delivery/parse.md @@ -51,7 +51,7 @@ The response is a `text/event-stream`. Events are emitted in order until the str { "basedata": { "date": "2026-01-15", - "kLieferant": 42, + "lieferant_id": 42, "lieferantName": "Muster GmbH", "manifestNr": "LS-2026-001", "manifestType": "lieferschein" diff --git a/src/api/routes/deliveryParse.js b/src/api/routes/deliveryParse.js index 047603e..b136ceb 100644 --- a/src/api/routes/deliveryParse.js +++ b/src/api/routes/deliveryParse.js @@ -4,6 +4,7 @@ import { execFileSync } from 'child_process'; import multer from 'multer'; import { LLMOrbiter, OpenRouterProvider, defineTool, DirectReturnError } from 'libllmorbiter'; import * as dbCompanySetting from '../../db/companysetting.js'; +import * as dbSupplier from '../../db/supplier.js'; // ─── Multer — memory storage (no temp files); accepts PDF or text ─────────────── const upload = multer({ @@ -67,7 +68,7 @@ Rules: - manifestType: "rechnung" or "lieferschein". For "lieferschein", prices and taxes must be null. - Exclude non-product lines (shipping, handling, etc.). - Line items: Extract count, vpe (default: 1), liefArtNr, mwst (7 or 19), and eknetto per piece(euro cents). -- Match supplier against the provided list for kLieferant and lieferantName. For unknown suppliers, extract lieferantName and set kLieferant to null. +- Match supplier against the provided list for lieferant_id and lieferantName. For unknown suppliers, extract lieferantName and set lieferant_id to null. - Write notes strictly in German. - The output_manifest tool has three strictly optional components: basedata, notes, items. Only include a component when it carries genuinely new data for this batch; omit the rest. - Include basedata ONLY when at least one of its fields is currently null/empty and you can fill it in, or when a value actually changes. If all basedata fields are already set and unchanged, omit basedata. @@ -89,6 +90,17 @@ Unit (ME) rules: - A packaging size like "VE100" means the bulk pack contains 100 individual items: set vpe = 100 and unit = the individual item (e.g. Stk.). - Example: "Einhängesiebe Stahl, VE100" -> vpe = 100, unit = "Stk.", count = 1 (one bulk pack of 100 sieves).`; +function buildSystemPrompt(suppliers = []) { + const supplierList = suppliers.length > 0 + ? suppliers.map((s) => `${s.id} | ${s.name}`).join('\n') + : '(no suppliers registered)'; + + return `${BASE_PROMPT} + +Suppliers (lieferant_id | lieferant_name): +${supplierList}`; +} + // ─── Shared SSE manifest extraction (POST /delivery/parse) ───────────────────── /** * Fetches the OpenRouter API key, opens the SSE stream, and runs the LLM @@ -113,6 +125,14 @@ async function parseDocumentManifest({ req, res, pages, sourceLabel }) { return res.status(400).json({ error: 'OpenRouter API key not configured (companysetting: keys.openrouter)' }); } + // ── Fetch suppliers from Lieferanten group ──────────────────────── + let suppliers = []; + try { + suppliers = await dbSupplier.list(req.user.id); + } catch (err) { + console.warn('delivery parse: failed to list suppliers:', err?.message || err); + } + // ── Open SSE stream ────────────────────────────────────────────── res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); @@ -124,7 +144,7 @@ async function parseDocumentManifest({ req, res, pages, sourceLabel }) { // ── Accumulated state ──────────────────────────────────────────── const manifestBasedata = { date: null, - kLieferant: null, + lieferant_id: null, lieferantName: null, manifestNr: null, manifestType: null, @@ -151,7 +171,7 @@ async function parseDocumentManifest({ req, res, pages, sourceLabel }) { additionalProperties: false, properties: { date: { type: ['string', 'null'], description: 'Document date (ISO 8601)' }, - kLieferant: { type: ['number', 'null'], description: 'Lieferant ID' }, + lieferant_id: { type: ['number', 'null'], description: 'Lieferant ID' }, lieferantName: { type: 'string', description: 'Lieferant name' }, manifestNr: { type: ['string', 'null'], description: 'Lieferscheinnummer or Rechnungsnummer' }, manifestType: { @@ -219,9 +239,15 @@ async function parseDocumentManifest({ req, res, pages, sourceLabel }) { // Merge basedata if (args.basedata && typeof args.basedata === 'object') { - for (const key of ['date', 'kLieferant', 'lieferantName', 'manifestNr', 'manifestType']) { + for (const key of ['date', 'lieferant_id', 'lieferantName', 'manifestNr', 'manifestType']) { if (args.basedata[key] != null) manifestBasedata[key] = args.basedata[key]; } + if (args.basedata.kLieferant != null && manifestBasedata.lieferant_id == null) { + manifestBasedata.lieferant_id = args.basedata.kLieferant; + } + if (args.basedata.lieferant_name != null && manifestBasedata.lieferantName == null) { + manifestBasedata.lieferantName = args.basedata.lieferant_name; + } } // Merge notes if (Array.isArray(args.notes)) { @@ -343,9 +369,9 @@ async function parseDocumentManifest({ req, res, pages, sourceLabel }) { .map((p, idx) => `--- Page ${idx + 1} of ${pages.length} ---\n${p}`) .join('\n\n'); - const prompt = `${BASE_PROMPT} + const systemPrompt = buildSystemPrompt(suppliers); -You are processing all ${pages.length} page(s) of this document. + const prompt = `You are processing all ${pages.length} page(s) of this document. Current manifest_basedata (fill in what is still missing): ${JSON.stringify(manifestBasedata, null, 2)} @@ -359,9 +385,15 @@ ${JSON.stringify(manifestItems, null, 2)} Page content: ${pageContent}`; - console.log(`[llmorbiter] Starting run: model=${model}, pages=${pages.length} (${sourceLabel})`); + console.log(`[llmorbiter] Starting run: model=${model}, pages=${pages.length} (${sourceLabel}), suppliers=${suppliers.length}`); currentPage = 1; - await orbiter.run({ prompt, model, tools: [outputManifestTool], signal: abortController.signal }); + await orbiter.run({ + prompt, + systemPrompt, + model, + tools: [outputManifestTool], + signal: abortController.signal, + }); console.log(`[llmorbiter] Run completed successfully. Extracted ${manifestItems.items.length} item(s), ${manifestNotes.length} note(s)`); } catch (err) { if (err.name === 'AbortError' || abortController.signal.aborted) { @@ -468,7 +500,7 @@ export function register(app, basePath = '') { * { * "basedata": { * "date": "2026-01-15", - * "kLieferant": 42, + * "lieferant_id": 42, * "lieferantName": "Muster GmbH", * "manifestNr": "LS-2026-001", * "manifestType": "lieferschein"