fix(inbox): set_inbox_extracted_data should accept and persist accountSuggestion (#760)

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 <jonas@floden.nu>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jonas Flodén
2026-06-25 12:40:53 +02:00
committed by GitHub
parent 6fe4164adc
commit d5abc8dd1c
2 changed files with 21 additions and 3 deletions
@@ -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-47 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.
+5 -3
View File
@@ -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 (4xxx7xxx); 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