cadrumo.application.ledger.llm_classification module¶
LLM-assisted ledger classification: suggest / apply / provider availability.
Wires the existing LLMClassifier engine into
the operator suggest -> review -> confirm / override / reject loop without
rebuilding the classifier. The contract is deliberately thin:
suggest_llm_classification()loads one transaction, runs the caller-composed classifier port with the category-enabled prompt spec, and returns a typedLLMClassificationSuggestionwithout persisting anything. Rejecting a suggestion is simply not applying it.apply_llm_classification()persists an accepted suggestion through the established classification write (set_classification()), stampingclassified_bywith the classifier’sdecided_by(llm:<model>provenance, distinct from manual /rule:) and recording the model’sconfidenceandreason. The accepted decision is appended to the profile audit trail through the caller-injected bucket-event repository as aledger.transaction.classifiedevent.
Hallucination containment stays inside the engine: the classifier’s
classify runs the allow-list-guarded
parse_response(), so an out-of-allow-list
value is rejected before it ever reaches this module.
Stage-1 constraint. suggest_llm_classification() /
apply_llm_classification() persist only the non-regulated
business_classification and optional expense category; they never set a
regulated tax value.
Stage-2 saturation. saturate_llm_classification() /
apply_saturated_llm_classification() additionally persist the
model-selected iva_category and the system-DERIVED taxable_base /
iva_rate / iva_amount. The model still never emits a number — the rate
is looked up from the registry and the base and amount are derived with
round_to_cents. irpf_category remains operator-only.
Core types:
TransactionCatalogue.
- class ResolvedEvidence(reference, text, images)[source]¶
Bases:
objectA transaction’s linked evidence resolved for an on-host read.
Exactly one read mode is populated:
textfor a text-layer PDF (inlined into the prompt and fed to the cloud subprocess classifier, consent-gated) orimagesfor a scan-only PDF / image (read in memory by the LOCAL vision model, on-host, gestor-allowed). Theimagesare transient FINANCIAL-derived bytes and MUST never be persisted or logged (sensitive-financial-data-secure-storage-only).- Parameters:
reference (str)
text (str | None)
images (tuple[EvidenceImage, ...])
-
images:
tuple[EvidenceImage,...]¶
- resolve_llm_evidence(transaction, *, bucket_id, settings, ports)[source]¶
Resolve a transaction’s linked evidence to an on-host read, or
None.Returns
Nonewhen the transaction has no linked evidence. A text-layer PDF yieldstextand routes to the cloud subprocess classifier, so it is gated by the cloud-upload consent posture (default-off, gestor-barred, per-invocation). A scan-only PDF or an image yields base64imagesand is read in memory by the LOCAL Ollama vision model – fully on-host, needing no cloud consent and permitted for gestor deployments. Bytes are read from secure storage into memory only; nothing is written to disk (sensitive-financial-data-secure-storage-only).- Raises:
PurchaseInvoiceEvidenceInputError – When a text-layer read would transmit to a cloud model but the per-invocation consent gate is not satisfied.
- Return type:
- Parameters:
transaction (Transaction)
bucket_id (str)
settings (Settings)
ports (LLMClassificationPorts)
- classify_with_evidence(transaction, evidence, *, text_classifier, spec, vision_classifier, vision_model, settings, ports)[source]¶
Classify, routing scan/image evidence to the on-host vision classifier.
Returns
(response, provenance). Image evidence is read by the local vision model (llm:local-vision:<model>provenance) and needs notext_classifier; text or no evidence runs the cloud subprocesstext_classifier(llm:<provider>:<model>provenance), which must be present.vision_modeloverrides the settings default vision model for this read.- Raises:
TransactionValidationError – When the text path is taken but no
text_classifierwas resolved (no--llmprovider supplied).PurchaseInvoiceEvidenceInputError – When the on-host reader is unavailable; the error carries the provisioning probe’s exact precondition verdict.
LLMClassifierError – When the reader call fails after its preconditions are confirmed satisfied.
- Return type:
- Parameters:
transaction (Transaction)
evidence (ResolvedEvidence | None)
text_classifier (LLMClassifier | None)
spec (PromptSpec)
vision_classifier (VisionClassifier | None)
vision_model (str | None)
settings (Settings)
ports (LLMClassificationPorts)
- suggest_llm_classification(*, bucket_id, transaction_id, operation, classifier=None, vision_classifier=None, vision_model=None, transaction_repository, read_evidence=False, settings, ports)[source]¶
Run the LLM classifier for one transaction and return a suggestion.
Loads the transaction, runs the injected classifier (default-resolved with the category-enabled prompt spec), and returns the typed suggestion. Persists nothing — this is the suggest step of the suggest / review / confirm / reject loop.
- Parameters:
bucket_id (
str) – Active profile bucket id.transaction_id (
str) – Stable id of the transaction to classify.operation (
PinnedAuthorityOperation) – Generation-pinned authority operation for the prompt’s dated category vocabulary and profile hints.classifier (
LLMClassifier|None) – Injected classifier (dependency injection for tests). With the cloud transports deleted the on-host reader is the default, so an injected classifier is the only non-default case.vision_classifier (
VisionClassifier|None) – Injected on-host vision classifier used when the evidence is a scan-only PDF or image; default-resolved otherwise.vision_model (
str|None) – Overrides the settings default local vision model (e.g.qwen2.5vl:7b) for an image/scan read;Noneuses the default.transaction_repository (
TransactionCatalogueCoCommitWriterProtocol) – Injected catalogue repository.read_evidence (
bool) – When True, resolve the transaction’s linked evidence and read it on-host — a text-layer PDF is inlined and sent to the cloud classifier (consent-gated), a scan-only PDF or image is read by the local vision model (no consent needed). Off by default.settings (
Settings) – Injected settings; defaults toload_settings().ports (
LLMClassificationPorts) – Injected evidence-reading and classifier execution ports.
- Return type:
- Returns:
- Raises:
TransactionNotFoundError – When the transaction id is unknown.
LLMClassifierError – When the classifier fails (e.g. provider CLI unavailable, hallucinated out-of-allow-list value).
- apply_llm_classification(suggestion, *, bucket_id, business_pct=None, actor='operator', source_command, transaction_repository, bucket_event_repository, occurred_at=None)[source]¶
Persist an accepted LLM suggestion with
llm:provenance.Writes the decision through
set_classification(), stampingclassified_bywith the suggestion’sprovenance(the classifier’sdecided_by, e.g.llm:<model>) and recording the model’sconfidenceandreason. Persists the catalogue and emits aLEDGER_TRANSACTION_CLASSIFIEDevent atomically.The MVP persists only the non-regulated
business_classificationand optional expensecategory. It never sets a regulated tax value.A
MIXEDsuggestion requires an explicitbusiness_pct(the LLM does not produce one); apply refuses instructively when it is absent. The expensecategoryis recorded only forBUSINESS/MIXEDclassifications.- Parameters:
suggestion (
LLMClassificationSuggestion) – The acceptedLLMClassificationSuggestion.bucket_id (
str) – Active profile bucket id.business_pct (
Decimal|None) – Required whensuggestion.classificationisMIXED.actor (
str) – Operator identity for the audit event.source_command (
str) – Source-command label for the audit event.transaction_repository (
TransactionCatalogueCoCommitWriterProtocol) – Injected catalogue repository.bucket_event_repository (
BucketEventHistoryCoCommitWriterProtocol) – Injected audit-event repository.occurred_at (
datetime|None) – Override clock for deterministic tests.
- Return type:
- Returns:
A
ManualLedgerTransactionResultreflecting the persisted decision.- Raises:
TransactionNotFoundError – When the transaction id is unknown.
TransactionValidationError – When the transaction is not ACTIVE or a
MIXEDsuggestion is applied without abusiness_pct.
- saturate_llm_classification(*, bucket_id, transaction_id, operation, classifier=None, vision_classifier=None, vision_model=None, transaction_repository, on_date=None, read_evidence=False, settings, ports)[source]¶
Run the saturating LLM classifier for one transaction and return a suggestion.
Loads the transaction, runs the injected classifier (default-resolved with the saturation prompt spec), then DERIVES the regulated tax substrate from the model’s selected
IvaCategoryusing the registry rate and a deterministic inverse split. Persists nothing — this is the suggest step; rejecting a suggestion is simply not applying it.- Parameters:
bucket_id (
str) – Active profile bucket id.transaction_id (
str) – Stable id of the transaction to classify.operation (
PinnedAuthorityOperation) – Generation-pinned authority operation for the prompt’s dated category and IVA projections.classifier (
LLMClassifier|None) – Injected classifier (dependency injection for tests). With the cloud transports deleted the on-host reader is the default; an injected classifier overrides it, with the saturation prompt spec.vision_classifier (
VisionClassifier|None) – Injected on-host vision classifier used when the evidence is a scan-only PDF or image; default-resolved otherwise.vision_model (
str|None) – Overrides the settings default local vision model (e.g.qwen2.5vl:7b) for an image/scan read;Noneuses the default.transaction_repository (
TransactionCatalogueCoCommitWriterProtocol) – Injected catalogue repository.on_date (
date|None) – Effective date used to resolve the registry rate; defaults to the transaction’s value date (or booked date).read_evidence (
bool) – When True, resolve the transaction’s linked evidence, extract its text on-host, and inject it into the prompt. Off by default.settings (
Settings) – Injected settings; defaults toload_settings().ports (
LLMClassificationPorts) – Injected evidence-reading and classifier execution ports.
- Return type:
- Returns:
A
LLMSaturatedSuggestioncarrying the model’s selections and the system-derived euro substrate.- Raises:
TransactionNotFoundError – When the transaction id is unknown.
LLMClassifierError – When the classifier fails (provider CLI unavailable, hallucinated out-of-allow-list value).
- apply_saturated_llm_classification(suggestion, *, bucket_id, business_pct=None, actor='operator', source_command, ports, occurred_at=None)[source]¶
Persist an accepted saturated suggestion through the manual write path.
Composes the established single-writer manual-command write (
update_manual_transaction_fields()) rather than re-implementing it, so the regulated fields land with their existing validators plus thegross == taxable_base + iva_amountinvariant, and stampsclassified_bywith the suggestion’sllm:<model>provenance viaclassified_by_override.The non-regulated business decision (classification, expense category) and the model-selected
iva_categoryare persisted; the regulated euro figures are persisted only when the category was derivable (a non-derivable category leaves the operator to complete the numbers). AMIXEDsuggestion requires a business percentage — the model’s proposedbusiness_pctis used unless the caller overrides it; apply refuses instructively when neither is present.- Parameters:
suggestion (
LLMSaturatedSuggestion) – The acceptedLLMSaturatedSuggestion.bucket_id (
str) – Active profile bucket id.business_pct (
Decimal|None) – Operator override for the MIXED business percentage; falls back to the model’s proposedbusiness_pct.actor (
str) – Operator identity for the audit event.source_command (
str) – Source-command label recording the operator’s verb.ports (
LedgerActionPorts) – Injected ledger action ports for the catalogue and audit-event repositories.occurred_at (
datetime|None) – Override clock for deterministic tests.
- Return type:
- Returns:
A
ManualLedgerTransactionResultreflecting the persisted state.- Raises:
TransactionValidationError – When a
MIXEDsuggestion is applied with no business percentage available.
- derive_operator_iva_substrate(*, bucket_id, transaction_id, iva_category, on_date=None, actor='operator', source_command, ports, occurred_at=None)[source]¶
Derive and persist the IVA substrate for an OPERATOR-chosen category.
The same grounded derivation the saturating LLM path uses (
resolve_category_rate()+split_gross_at_rate()), but initiated by the operator rather than the model — the fallback for when the model declines (returnsunknown) or the operator simply knows the category. Given a transaction already classified BUSINESS or MIXED and the selectedIvaCategory, it resolves the registry rate, splits the gross into taxable base and IVA amount, and persists them through the manual write withderived:provenance. Only the IVA substrate is touched; the business classification stays as-is. A non-derivable category persists nothing and returns an explanatory note.- Return type:
- Returns:
The
OperatorIvaDerivationResultrecording the persisted IVA substrate, or an explanatory note when the category is non-derivable.- Raises:
TransactionNotFoundError – When the transaction id is unknown.
TransactionValidationError – When the transaction is not classified BUSINESS or MIXED (IVA applies only to business activity).
- Parameters:
bucket_id (str)
transaction_id (str)
iva_category (IvaCategory)
on_date (date | None)
actor (str)
source_command (str)
ports (LedgerActionPorts)
occurred_at (datetime | None)
- suggest_evidence_split(*, bucket_id, transaction_id, operation, proposer=None, vision_classifier=None, vision_model=None, transaction_repository, on_date=None, read_evidence=True, settings, ports)[source]¶
Propose an evidence-driven N-way split for one transaction.
Loads the transaction, runs the injected proposer (default-resolved with the saturation prompt spec) over the optional on-host evidence text, DERIVES each child’s euro amount from the parent gross and the model’s proportion (summing exactly to the parent), and DERIVES each child’s regulated tax substrate from the registry rate for the model-selected IVA category. Persists nothing — this is the suggest step.
- Parameters:
bucket_id (
str) – Active profile bucket id.transaction_id (
str) – Stable id of the transaction to split.operation (
PinnedAuthorityOperation) – Generation-pinned authority operation for the prompt’s dated category and IVA projections.proposer (
LLMSplitProposer|None) – Injected split proposer (dependency injection for tests). With the cloud transports deleted the on-host reader is the default, so an injected proposer is the only non-default case.vision_classifier (
VisionClassifier|None) – Injected on-host vision classifier used when the evidence is a scan-only PDF or image; default-resolved otherwise.vision_model (
str|None) – Overrides the settings default local vision model (e.g.qwen2.5vl:7b) for an image/scan read;Noneuses the default.transaction_repository (
TransactionCatalogueCoCommitWriterProtocol) – Injected catalogue repository.on_date (
date|None) – Effective date used to resolve each child’s registry rate; defaults to the transaction’s value date (or booked date).read_evidence (
bool) – When True (default for splitting), resolve the transaction’s linked evidence, extract its text on-host, and inject it into the prompt.settings (
Settings) – Injected settings; defaults toload_settings().ports (
LLMClassificationPorts) – Injected evidence-reading and classifier execution ports.
- Return type:
- Returns:
A
LLMSplitSuggestionwhose child amounts sum exactly to the parent.- Raises:
TransactionNotFoundError – When the transaction id is unknown.
LLMClassifierError – When the proposer fails (provider CLI unavailable, hallucinated out-of-allow-list value, or a malformed split response).
- apply_evidence_split(suggestion, *, bucket_id, actor='operator', source_command, ports, occurred_at=None)[source]¶
Apply a reviewed evidence-driven split through the single-writer split path.
Delegates the whole mutation to the established single writer rather than re-implementing it:
split_transaction_with_classified_children()redistributes the parent into children whose magnitudes sum exactly to the parent and stamps each child’s model-selected expense and IVA category, the registry-DERIVED regulated numbers, the parent’s inherited evidence link, and thellm:<model>provenance. This function only translates the reviewed suggestion into that writer’s arguments; it performs no write of its own and no follow-up per-child field patch.Because the writer persists the parent transition, every classified child, and all bucket events in one transaction, a child can never come to rest split but unclassified or missing its evidence link. The writer enforces children-sum-to-parent, the non-negative-magnitude invariant, and
gross == taxable_base + iva_amountper child. The LLM never supplies a persisted euro amount or regulated number.- Parameters:
suggestion (
LLMSplitSuggestion) – The acceptedLLMSplitSuggestion.bucket_id (
str) – Active profile bucket id.actor (
str) – Operator identity for the audit events.source_command (
str) – Source-command label recording the operator’s verb.ports (
LedgerActionPorts) – Injected ledger action ports for the catalogue and audit-event repositories.occurred_at (
datetime|None) – Override clock for deterministic tests.
- Return type:
- Returns:
An
LLMSplitApplyResultnaming the split group and its children.- Raises:
TransactionNotFoundError – When the parent transaction id is unknown.
TransactionValidationError – When the split invariants are violated.
- apply_evidence_classification(suggestion, *, bucket_id, actor='operator', source_command, ports, occurred_at=None)[source]¶
Apply a no-split (single-child) evidence suggestion in place on the parent.
The auto-split router uses one model call — the split proposer — to decide whether to split. When the proposer returns a single child (the “no split warranted” verdict), that child already carries the model-selected expense and IVA categories and the registry-DERIVED
taxable_base/iva_rate/iva_amountfor the whole gross. This stamps them on the parent through the single-writerupdate_manual_transaction_fields()with thellm:<model>provenance — the same valuesapply_evidence_split()hands its children, but applied in place to one existing row instead of redistributed across new ones. The parent’s own evidence link is preserved verbatim by that write and is never re-set here. The model emits no euro amount or regulated number (llm-selects-system-derives-tax-numbers).- Parameters:
suggestion (
LLMSplitSuggestion) – A no-splitLLMSplitSuggestion(exactly one child).bucket_id (
str) – Active profile bucket id.actor (
str) – Operator identity for the audit event.source_command (
str) – Source-command label recording the operator’s verb.ports (
LedgerActionPorts) – Injected ledger action ports for the catalogue and audit-event repositories.occurred_at (
datetime|None) – Override clock for deterministic tests.
- Return type:
- Returns:
The
ManualLedgerTransactionResultfor the in-place classification.- Raises:
TransactionValidationError – When the suggestion recommends a split (use
apply_evidence_split()).TransactionNotFoundError – When the transaction id is unknown.
- reject_llm_suggestion(suggestion, *, bucket_id, reason='', actor='operator', source_command, transaction_repository, bucket_event_repository, occurred_at=None)[source]¶
Record an explicit, audit-trailed rejection of an LLM suggestion.
This is the fourth decision terminal of the suggest -> review -> decide loop (after approve = apply and update = manual override). It captures what the model proposed and the operator’s reason in a
LEDGER_TRANSACTION_LLM_SUGGESTION_REJECTEDbucket event, and mutates nothing — the transaction’s classification, numbers, and lifecycle are untouched, so its review status stayspending(it is still unclassified). No regulated number is written; the model emitted none and reject writes none.- Parameters:
suggestion (
LLMClassificationSuggestion|LLMSaturatedSuggestion|LLMSplitSuggestion) – The captured proposal being rejected — a stage-1 classification, a saturated suggestion, or an evidence-driven split.bucket_id (
str) – Active profile bucket id.reason (
str) – The operator’s free-text reason for rejecting (optional).actor (
str) – Operator identity for the audit event.source_command (
str) – Source-command label recording the operator’s verb.transaction_repository (
TransactionCatalogueCoCommitWriterProtocol) – Injected catalogue repository.bucket_event_repository (
BucketEventHistoryCoCommitWriterProtocol) – Injected audit-event repository.occurred_at (
datetime|None) – Override clock for deterministic tests.
- Return type:
- Returns:
An
LLMSuggestionRejectionResultnaming the recorded event.- Raises:
TransactionNotFoundError – When the transaction id is unknown.
TransactionValidationError – When the transaction is not active.