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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user