llm
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user