cadrumo.adapters.outbound.llm.invoice_extraction_prompt module

Render the invoice-extraction prompt from values an authority already resolved.

The prompt is an ARTEFACT, not a literal: a digit-free template plus values substituted at render time. This module owns the TEMPLATE and the rendering; it owns none of the numbers.

Why that matters more here than anywhere else. aeat-registry-authority-flow forbids inlining an AEAT rate, threshold or regulatory code as a Python literal, because those values are versioned by filing year plus revision and a literal bakes one year’s law into the call site. A prompt is the least-audited place in the codebase for such a literal to hide: nothing type-checks it, no gate reads it, and a stale 21 would keep steering a reading model long after the registry moved.

So the renderer cannot reach an authority at all. Every regulatory value arrives as data, in one frozen InvoiceExtractionAuthorityValues resolved by resolve_invoice_extraction_authority_values(). This package is an adapter over a model transport, and an adapter that looks a rate up for itself has made itself a second consumer of the calculation authorities. Removing the reach is a stronger guarantee than remembering not to write 21: the only rate this module can print is one it was handed.

What arrives, and what each part is for:

  • IVA rate percentages for the requested Period – every record whose effective window OVERLAPS it, because a period is a span and RD-ley 4/2024 stepped part of the reducido and super-reducido tiers mid-year.

  • Retención rate percentages, the RIRPF art. 95 figures.

  • The no-printed-tax IvaCategory members, rendered here into the prose tokens the model reads.

  • The regime mentions RD 1619/2012 art. 6.1 obliges an issuer to print. The prompt asks the model to COPY one if printed, never to choose one – a closed list offered as a recognition aid, not as a menu, because selecting from it would be the classification this stage does not do.

The two template scanners deliberately keep their own read of the vocabulary they assert is absent. They are checks, not substitutions: a scanner handed its needle by the same caller that supplied the template could be handed an empty needle and would then report clean over anything at all.

Shape is a design constraint. The target is the lowest-bound vision-capable model, so the prompt is enumerated bullets and per-field micro-guidance, never prose. A closed enumeration turns an inference problem into a selection problem and is cheaper than any explanation of it.

Every field is asked for twice: once as a value, once as an anchor. The value arrives in the form its contract row declares; the anchor is the substring exactly as the document printed it. Keeping both is what lets a later check verify that the anchor occurs in the document AND that the value equals the deterministic parse of the anchor – a value with no anchor can only be taken on trust. Where the two would be byte-identical the check is weaker, so the contract deliberately keeps them distinct: IVA (21%) yields the value 21 and the anchor 21%.

An identity field is asked for a third time: what assigns it to a party. Two tax identifiers on one invoice have the same shape, so an anchor – which says only where a number was printed – cannot say whose it is. The role-evidence key carries the printed heading or label instead, and it is checked against the transcription exactly as an anchor is, so a heading the document does not show is dropped and the identity stays unresolved rather than being assigned on the reader’s own say-so.

The enumeration is a hint, never a constraint. Documents in scope are international; a German invoice prints 19 %, which is not a Spanish registered rate. The rate line therefore says what Spain registers and that a foreign document may print something else, because a model told “the rate is one of these” would otherwise coerce 19 to 21 – fabricating the exact class of figure the null-over-guess rule exists to prevent.

See also

INVOICE_FIELD_CONTRACTS

The single field-form declaration whose rows become the field lines.

ground_extracted_fields()

The other derivation from that same declaration.

INVOICE_EXTRACTION_PROMPT_ID: Final[str]

Registry id of the pre-substitution extraction template.

INVOICE_EXTRACTION_PROMPT_VERSION: Final[int]

Version of the registered template.

Bumped when the template’s INSTRUCTIONS change – a new field, a changed rule, a different response shape – never when the values substituted into it move. The two are different facts and are carried separately: CompiledInvoiceExtractionPrompt.template_version says which instructions were issued, and CompiledInvoiceExtractionPrompt.fingerprint says which compiled text a model actually received. An integer cannot express the second, which is why registering the template does not replace the fingerprint.

class CompiledInvoiceExtractionPrompt(**data)[source]

Bases: BaseModel

One compiled prompt together with the authority values that produced it.

The compiled values are carried alongside the text rather than only baked into it, so a provenance stamp can answer “under which rates was this read?” without re-parsing prose.

Variables:
  • text – The prompt as the model receives it.

  • period – The filing period the rates were resolved for.

  • iva_rate_pcts – Every registered Spanish IVA percentage whose effective window overlaps period, ascending.

  • retencion_rate_pcts – Every RIRPF art. 95 retención percentage, ascending.

  • fingerprint – Short content hash of text. Two prompts compiled from different registry values differ here, which is what makes the stamp discriminating.

  • template_version – Which registered template version issued the instructions. Distinct from fingerprint, which moves whenever any substituted value moves; this moves only when the instructions themselves change.

Parameters:

data (Any)

text: str
period: Period
template_version: int
iva_rate_pcts: tuple[Decimal, ...]
retencion_rate_pcts: tuple[Decimal, ...]
fingerprint: str
property rate_provenance: str

Return the compact <year>-<code>-<fingerprint> rate-provenance token.

Returns:

The token a reader folds into its decided_by stamp.

invoice_extraction_prompt_registry()[source]

Return the registry holding the pre-substitution extraction template.

The template is versioned prompt metadata, which is exactly what PromptRegistry already stores – so it is registered there rather than reachable only as a module constant. The compiler then asks the registry for its template instead of closing over one, which is what lets the id and version travel onto the compiled artefact.

Cached because the registered definition is immutable: rebuilding it per call would mint an equal object and lose the identity a caller can compare.

Returns:

A registry carrying the extraction template at INVOICE_EXTRACTION_PROMPT_VERSION.

Return type:

PromptRegistry

default_extraction_period()[source]

Return the period a reader falls back to when the caller names none.

Delegates to default_invoice_extraction_period() rather than re-deriving the fallback. Which coordinate an unbound document is read against is a decision, and a decision with two homes is a decision that will eventually be made two ways.

Returns:

The current civil year’s annual period.

Return type:

Period

selected_invoice_field_contracts(fields)[source]

Return the contracts a caller selected, in declaration order.

The one place a field selection becomes contracts, so the three prompt blocks that enumerate fields cannot disagree about what the prompt is asking for. A block emitting a different subset from its siblings would ask the model for a key the skeleton never lists, or list a key the Fields block never explains.

Declaration order is preserved rather than the caller’s argument order, because field ordering is part of what a measurement compares: two arms passing the same set differently ordered must get the same prompt, or they differ by argument order alone.

Parameters:

fields (Collection[str] | None) – The field names to emit, or None for every declared field.

Return type:

tuple[InvoiceFieldContract, ...]

Returns:

The selected contracts, in INVOICE_FIELD_CONTRACTS order.

Raises:

ValueError – When fields is empty, or names a field the contract declaration does not carry. Refusing is the point. A silently dropped name still renders – just shorter – and a measurement taken against a prompt missing a contract nobody noticed is worse than no measurement. An empty selection is refused on the same terms the period path fails closed on an empty rate set: a prompt asking for nothing is not a smaller prompt, it is a broken one.

render_invoice_extraction_prompt(*, values, fields=None)[source]

Render the extraction prompt from already-resolved authority values.

The whole of this module’s regulatory content arrives through this one argument. Nothing here reads a rate table, a parameter file or a category set, so the prompt cannot carry a number the application layer did not resolve for the period it names.

Parameters:
  • values (InvoiceExtractionAuthorityValues) – The regulatory values to substitute, resolved by resolve_invoice_extraction_authority_values() against a law-determined period.

  • fields (Collection[str] | None) – The field names the prompt should ask for, or None for every declared field. A named field the contract declaration does not carry refuses rather than being dropped, because a silently shorter prompt still renders and a measurement taken against one missing a contract nobody noticed is worse than no measurement.

Returns:

The prompt text plus the values that produced it.

Return type:

CompiledInvoiceExtractionPrompt