cadrumo.adapters.outbound.llm.invoice_field_contract module¶
The one declaration of what FORM each extracted invoice field must arrive in.
The reading prompt and the grounded re-validation step both need an answer to
“what shape is this field’s value?”, and until this module existed each held its
own. The prompt’s prose said copy the value exactly as printed; the grounder
required a bare Decimal. On an invoice printing IVA (21%)
those two answers disagree, and the disagreement was measured: a model that
obeyed the prompt literally returned "21%" and lost the field entirely, while
a model that quietly normalised to "21" was accepted. The reading was correct
both times; only the contract was ambiguous.
So the form is declared ONCE here, as typed data
(InvoiceFieldContract, one row per field of
ExtractedInvoiceFields), and both sides
derive from it: invoice_extraction_prompt renders each row’s
form_instruction into the compiled prompt, and
ground_extracted_fields() dispatches each
row’s InvoiceFieldForm to the matching grounding validator. Two
hand-maintained lists that happen to agree today is the defect this closes, not
the fix.
Anchor and value are deliberately distinct: anti-fabrication works by
anchoring the model’s transcription and closing over it, never by rewriting
what it said. Nothing here rewrites what the model transcribed:
ExtractedInvoiceFields keeps the printed
string verbatim, which is the anchor a later closure check points at. The form
declaration governs only the DERIVATION of the typed value from that anchor. For
IVA (21%) the anchor stays "21%" and the value becomes 21, which
serves anchoring rather than fighting it: the percent sign is a unit marker, not
a digit, so dropping it is unit normalisation and the number is still copied,
never computed.
See also
ground_extracted_fields()Grounding side, which dispatches on
InvoiceFieldForm.build_invoice_extraction_prompt()Prompt side, which renders
InvoiceFieldContract.form_instruction.
- ANCHOR_KEY_SUFFIX: Final[str]¶
Suffix naming a field’s anchor key in the model’s JSON response.
Declared here rather than at either use site because three derivations depend on it agreeing exactly: the compiled prompt renders the key it asks for, the parser reads the key it receives, and the parity gate proves the two are the same string. A suffix spelled twice is the same drift this module exists to close – and its failure mode is silent, because an anchor key the parser does not recognise looks identical to a model that returned no anchor at all.
- anchor_key_for_field(field_name)[source]¶
Return the JSON key carrying
field_name’s verbatim printed anchor.- Parameters:
field_name (
str) – Attribute name onExtractedInvoiceFields.- Return type:
- Returns:
The anchor key, e.g.
"iva_rate_anchor".
- ROLE_EVIDENCE_KEY_SUFFIX: Final[str]¶
Suffix naming an identity field’s role-evidence key in the model’s JSON response.
Declared beside
ANCHOR_KEY_SUFFIXand for the same reason: the compiled prompt renders the key it asks for, the parser reads the key it receives, and a suffix spelled twice drifts silently, because a key the parser does not recognise is indistinguishable from a model that returned nothing.This is a second anchor, not a second note. An anchor answers where on the page did this value come from; role evidence answers what on the page assigns that value to THIS party. Both are printed excerpts, both are checked against the transcription, and neither is the reader’s own account of what it did. That distinction is the whole point of the key existing: the reading stage previously manufactured
"the reader assigned this identifier to supplier_tax_id", which is a restatement of the assignment rather than evidence for it, and being always truthy it permanently satisfied the guard that exists to refuse an unevidenced identity.
- role_evidence_key_for_field(field_name)[source]¶
Return the JSON key carrying
field_name’s printed role evidence.- Parameters:
field_name (
str) – Attribute name of an identity field onExtractedInvoiceFields.- Return type:
- Returns:
The role-evidence key, e.g.
"supplier_tax_id_role_evidence".
- class InvoiceFieldForm(*values)[source]¶
Bases:
StrEnumClosed set of printed forms an extracted invoice field can arrive in.
A member names the SHAPE the emitted value must have, never the field it belongs to:
taxable_base,iva_amountandgrand_totalare three fields with one form, and adding a fourth monetary field must not need a fourth grounding branch.- TAX_IDENTIFIER¶
A tax identification number, checksum- or VIES-structure-validated.
- FREE_TEXT¶
An opaque printed token kept verbatim (an invoice reference).
- CALENDAR_DATE¶
A date, kept in whatever order the document prints it.
- MONETARY_AMOUNT¶
Digits with the printed decimal separator preserved, no currency sign.
- PERCENTAGE_RATE¶
A bare rate number; the printed percent sign is a unit, not a digit.
- CURRENCY_CODE¶
A three-letter ISO-4217 alphabetic code.
- class InvoiceFieldContract(**data)[source]¶
Bases:
BaseModelThe declared contract for one extracted invoice field.
- Variables:
field_name – Attribute name on
ExtractedInvoiceFields. The parity gate proves this set equals that model’s field set exactly, in both directions.form – The printed form the value must arrive in, dispatching the grounding validator.
concept – What the field MEANS, phrased without naming a label in any particular language, because documents in scope are international.
form_instruction – The per-field micro-guidance line rendered into the compiled prompt. Kept short: the design target is a lowest-bound vision model with a very small context budget.
role_evidence_instruction – How to answer “what on the page assigns this value to THIS party”. Present exactly on the identity fields and absent everywhere else, which the validator below enforces in both directions rather than leaving to the author.
- Parameters:
field_name (str)
form (InvoiceFieldForm)
concept (str)
form_instruction (str)
role_evidence_instruction (str | None)
- field_name: str¶
- form: InvoiceFieldForm¶
- concept: str¶
- form_instruction: str¶
- role_evidence_instruction: str | None¶
- property carries_role_evidence: bool¶
Whether this field is an identity field and so must evidence its role.
Derived from the declared FORM rather than from a hand-listed field set. A tax identifier is the only kind of value on an invoice that names a PARTY, so “which party is this” is a question only this form raises – and deriving it means a fourth identity field added to the table asks for role evidence by construction instead of by remembering.
- INVOICE_FIELD_CONTRACTS: tuple[InvoiceFieldContract, ...]¶
Every extracted field’s declared form, in the order the prompt lists them.
This tuple is the single declaration the parity gate binds both derivations to. Adding a field here without adding it to
ExtractedInvoiceFields(or the reverse) fails that gate, which is the whole reason the tuple exists.
- contract_for_field(field_name)[source]¶
Return the declared contract for
field_name.- Parameters:
field_name (
str) – Attribute name onExtractedInvoiceFields.- Returns:
The declared contract.
- Return type:
- Raises:
KeyError – When no contract is declared for
field_name. Raising is correct rather than returning a permissive default: an undeclared field has no agreed form, so grounding it would be a guess.