cadrumo.adapters.outbound.llm.invoice_field_grounding module¶
Transport-neutral invoice-field response schema, parser and grounded re-validation.
A model asked to read an invoice returns strings. Whether it read those strings
off rasterised pixels (evidence_draft_vision) or off an already-extracted
text layer (evidence_draft_text) changes nothing about what must happen
next: the response has to be recovered from possibly-chatty completion text,
validated against a strict schema, and then re-validated field by field so a
value the model invented is dropped rather than trusted.
That second step is the whole point. The reading model is probabilistic and a
fabricated taxable base or supplier identifier would flow into a Modelo 303/390
filing looking exactly like a read one. Every field here is therefore checked
against an independent authority – the AEAT checksum algorithm
(validate_runtime_spanish_tax_id()),
the EU VIES structural format
table (nif_iva_format_for_country()), the date parser
(parse_date()), the finite European-decimal authority
(coerce_finite_european_decimal()) – and a field that fails
its check becomes None. None is safe because the confirm path treats a
missing figure as a hard refusal naming the operator override; a guessed figure
would instead be minted silently.
Nothing in this module knows about images, providers or transports, which is why it lives here rather than beside either reader.
See also
InvoiceDraftTyped draft every grounded reader returns.
validate_runtime_spanish_tax_id()Spanish NIF/NIE/CIF checksum authority.
nif_iva_format_for_country()EU intra-community NIF-IVA structural format authority.
- class ExtractedInvoiceFields(**data)[source]¶
Bases:
_ExtractedInvoiceFieldClaimsRaw string fields the reading model transcribed, before grounded re-validation.
Every field is an optional string: the model is instructed to transcribe the printed value verbatim (never compute or infer it) and this schema accepts whatever string it returns. Grounded re-validation into typed values (a checksum-valid tax id, a parsed date, a parsed Decimal) happens in
ground_extracted_fields(), never here – a malformed or hallucinated string must be rejected downstream, not coerced at the schema boundary.- Parameters:
data (Any)
- class ExtractedFieldAnchors(**data)[source]¶
Bases:
_ExtractedInvoiceFieldClaimsThe verbatim printed substring the model read each field from.
Deliberately a mirror of
ExtractedInvoiceFieldsrather than extra attributes on it. The two carry different KINDS of claim: a field holds the value in its declared form, an anchor holds the form the document actually printed, and the whole anti-fabrication argument rests on being able to compare them. Folded into one model that comparison becomes an attribute naming convention; kept apart it is a type.Every attribute is an optional string for the same reason the value side is: the model returns strings, and nothing here rejects or rewrites one. An anchor that does not occur in the document is caught by the anchor check (
evaluate_anchor()), never by this schema, because that check needs the document and this schema does not have it.- Parameters:
data (Any)
- class ExtractedRoleEvidence(**data)[source]¶
Bases:
BaseModelThe printed context assigning each identity value to a party role.
A THIRD kind of claim, kept in its own model for the reason the anchors are:
ExtractedInvoiceFieldsholds what the identifier is,ExtractedFieldAnchorsholds where on the page it was printed, and this holds what on the page says it belongs to that party. An invoice prints two identifiers of identical shape, so the printed form of one is no evidence at all about whose it is – which is exactly the question the counterparty role turns on.Only the identity fields appear here, matching the role-bearing entries in
INVOICE_FIELD_CONTRACTS. Every attribute is an optional string on the same terms as the other two halves: nothing is rejected or rewritten here, because the check that matters needs the document and this schema does not have it. A value that does not occur in the transcription is dropped byprinted_excerpt_occurs()at the grounding stage, so an invented role evidence cannot promote an identity.- Parameters:
data (Any)
- supplier_tax_id: str | None¶
- customer_tax_id: str | None¶
- class ExtractedInvoiceResponse(**data)[source]¶
Bases:
BaseModelOne reading model’s complete reply: every value beside its printed anchor.
The parser’s single return type. Splitting the reply into models but returning them as one object keeps the pairing structural – a caller cannot hold values without the anchors that justify them, which is exactly the separation that let a value travel to a filing with nothing to point at. The role-evidence half is bound in the same way and for a sharper version of the same reason: an identity carried without it is a party name nothing on the document assigns.
- Variables:
fields – The transcribed values, before grounded re-validation.
anchors – The printed form each value was read from.
role_evidence – The printed context assigning each identity value to its party role. Defaults to an all-
Nonerecord, so a reply that offers none parses cleanly and simply evidences no role – the fail-safe direction, since an unevidenced identity refuses.
- Parameters:
data (Any)
- fields: ExtractedInvoiceFields¶
- anchors: ExtractedFieldAnchors¶
- role_evidence: ExtractedRoleEvidence¶
- parse_invoice_extraction_response(text)[source]¶
Parse a reading model’s raw completion text into values and their anchors.
The prompt asks for one flat object carrying each field beside its
<field>_anchorsibling, and each IDENTITY field beside its<field>_role_evidencesibling too, so the flat reply is split here into its typed halves. Splitting on the declared suffixes rather than on a second hand-written key list means the prompt and the parser cannot ask for and read different keys.A model that returns only the value keys parses cleanly with empty anchors and no role evidence: that is a reply with nothing to point at and nothing assigning its identities to a party, which the grounding stage records honestly rather than a malformed one to reject – and an unevidenced identity refuses downstream, so the tolerant parse costs no safety. A key matching neither a declared field nor a declared anchor nor a declared role-evidence key IS rejected – an unrecognised key is a model inventing structure, and tolerating it here would let a fabricated field ride along unnoticed.
- Parameters:
text (
str) – Raw completion text from the reading model.- Returns:
The parsed (but not yet grounded) values, the printed anchors they were read from, and the printed role evidence assigning each identity to a party.
- Return type:
- Raises:
PurchaseInvoiceEvidenceReaderError – When no JSON object is present, the object is not a JSON object, or either half fails schema validation. The reader answered unusably, which is the reader’s fault and not the operator’s, so the refusal is retryable and says so.
- ground_extracted_fields(response, *, operation, raw_text_length, origin)[source]¶
Re-validate the model’s transcribed strings into a grounded
InvoiceDraft.Each field is grounded through the validator its DECLARED form selects (
INVOICE_FIELD_CONTRACTS), the same declaration the compiled prompt renders its per-field guidance from, so the two sides cannot state different expectations. A field left ungrounded here fails the parity gate’s fully-populated round.A field the model transcribed but that fails grounded validation (an invalid tax-id checksum, an unparsable date, a non-numeric amount) is dropped to
Nonerather than trusted – the same “never fabricate” discipline the text-layer heuristics apply. For an IDENTITY field the rejection is also RECORDED, as anIDENTITY_UNVERIFIEDfinding: dropping the value is right, dropping the fact that the document printed one is not, and only this stage still holds that fact.Every field that survives grounding also gets a
FieldProvenanceenvelope carrying the verbatim anchor the model reported for it, so no value reaches the operator without the printed form it claims to have come from.- Parameters:
response (
ExtractedInvoiceResponse) – The parsed, not-yet-grounded reply. Left untouched: its anchor half carries the verbatim printed forms.operation (
PinnedAuthorityOperation) – The caller-owned generation-pinned authority operation used for the printed-country vocabulary lookup.raw_text_length (
int) – How much source material the reader had to work with.origin (
FieldOrigin) – How these values were obtained. Required rather than defaulted: a reader that could omit it would silently claim whichever origin was most convenient, and the exact-versus-probabilistic distinction is the one thing the record exists to keep honest.
- Returns:
The grounded draft, one provenance envelope per field that carries a value.
- Return type: