From d5abc8dd1cd6e5ce82fde40bdcc8f74418727d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Flod=C3=A9n?= Date: Thu, 25 Jun 2026 12:40:53 +0200 Subject: [PATCH] fix(inbox): set_inbox_extracted_data should accept and persist accountSuggestion (#760) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tool was using InvoiceExtractionSchema which forces every lineItems.accountSuggestion to null via .transform() — the same guard that prevents the AI extractor from hallucinating BAS accounts. Agents supplying their own extraction should be able to pin a cost account per line. Adds AgentExtractionSchema (exported alongside ExtractionSchema) where accountSuggestion accepts a validated BAS expense account (class 4–7, /^[4-7]\d{3}$/) or null. set_inbox_extracted_data now parses through this schema so the field survives the round-trip to the DB and is available when gnubok_create_supplier_invoice_from_inbox builds line items. Signed-off-by: Jonas Flodén Co-authored-by: Claude Sonnet 4.6 --- .../invoice-inbox/lib/extract-invoice-fields.ts | 16 ++++++++++++++++ extensions/general/mcp-server/server.ts | 8 +++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/extensions/general/invoice-inbox/lib/extract-invoice-fields.ts b/extensions/general/invoice-inbox/lib/extract-invoice-fields.ts index 72bdb6cc..9cd8de0b 100644 --- a/extensions/general/invoice-inbox/lib/extract-invoice-fields.ts +++ b/extensions/general/invoice-inbox/lib/extract-invoice-fields.ts @@ -110,6 +110,22 @@ export const ExtractionSchema = z.object({ ), }) +// Agent-supplied extraction: accountSuggestion is preserved instead of forced +// to null. Agents (unlike AI extractors) can reliably assign a BAS expense +// account; the regex enforces the class-4–7 range required for cost accounts. +export const AgentExtractionSchema = ExtractionSchema.omit({ lineItems: true }).extend({ + lineItems: z.array( + z.object({ + description: z.string(), + quantity: z.number(), + unitPrice: z.number().nullable(), + lineTotal: z.number(), + vatRate: z.number().min(0).max(100).nullable(), + accountSuggestion: z.string().regex(/^[4-7]\d{3}$/).nullable(), + }) + ), +}) + const SYSTEM_PROMPT = `You extract invoice and receipt fields from a single document for a Swedish accounting system. Return ONLY a single JSON object that matches this schema exactly. No prose, no markdown fences, no commentary. diff --git a/extensions/general/mcp-server/server.ts b/extensions/general/mcp-server/server.ts index 4cb1c93d..9b6806d2 100644 --- a/extensions/general/mcp-server/server.ts +++ b/extensions/general/mcp-server/server.ts @@ -74,7 +74,7 @@ import { generateInvoiceEmailSubject, } from '@/lib/email/invoice-templates' import { uploadDocument, MAX_DOCUMENT_SIZE } from '@/lib/core/documents/document-service' -import { extractInvoiceFields, ExtractionSchema as InvoiceExtractionSchema } from '@/extensions/general/invoice-inbox/lib/extract-invoice-fields' +import { extractInvoiceFields, ExtractionSchema as InvoiceExtractionSchema, AgentExtractionSchema } from '@/extensions/general/invoice-inbox/lib/extract-invoice-fields' // Skatteverket filing tools (PR5). Cross-extension lib import, same sanctioned // pattern as invoice-inbox above — the CI guard only checks lib/, app/api/, // components/. The two submit tools stage ops whose commit dispatches back into @@ -9590,7 +9590,7 @@ export const tools: McpTool[] = [ inbox_item_id: { type: 'string', description: 'UUID of the invoice_inbox_items row' }, extracted_data: { type: 'object', - description: 'Full InvoiceExtractionResult (supplier, invoice, lineItems, totals, vatBreakdown). Validated server-side via the same Zod schema as the AI extractor.', + description: 'Full InvoiceExtractionResult (supplier, invoice, lineItems, totals, vatBreakdown). lineItems.accountSuggestion accepts a BAS expense account (4xxx–7xxx); AI extractor always emits null here.', }, }, required: ['inbox_item_id', 'extracted_data'], @@ -9610,10 +9610,12 @@ export const tools: McpTool[] = [ const inboxItemId = args.inbox_item_id as string if (!inboxItemId) throw new Error('inbox_item_id is required') - const parsed = InvoiceExtractionSchema.parse(args.extracted_data) + const parsed = AgentExtractionSchema.parse(args.extracted_data) // BYO extraction: confidence 0.95 marks the result as agent-supplied // (vs 1.0 the AI extractor uses on a perfect parse) so downstream UI // can render the provenance differently (ISO 27001 A.8.12). + // AgentExtractionSchema (unlike InvoiceExtractionSchema) preserves + // accountSuggestion so agents can pin a BAS cost account per line. const extracted = { ...parsed, confidence: 0.95 } const { data: item, error: fetchError } = await supabase