Source code for aeat.application.ledger._llm_suggestions
"""LLM ledger classification suggestion/result contracts.These strict frozen DTOs carry the reviewable output of the governed ledgerLLM workflow: stage-1 business classification suggestions, saturated IVAcategory suggestions with system-derived tax substrate, evidence-driven splitproposals, provider availability rows, and explicit rejection receipts. Theyare application-layer contracts only; classifier engines and prompt parsing livein :mod:`~domain.transactions`, while persistence and audit events are handledby the application services that consume these records.The contracts preserve the review loop required by the LLM ADRs: a suggestionis not persisted until the operator applies it, a rejection records an auditevent without mutating the transaction, and regulated euro amounts / IVA ratesare derived by the system rather than emitted by the model.See Also: :mod:`~application.ledger._llm_classification` Application service that builds, applies, saturates, splits, and rejects these suggestions. :func:`~application.ledger.suggest_llm_classification` Stage-1 suggestion path that persists nothing. :func:`~application.ledger.saturate_llm_classification` Path that adds model-selected IVA category plus system-derived substrate. :func:`~application.ledger.suggest_evidence_split` Evidence-driven split/no-split proposal builder. :func:`~application.ledger.reject_llm_suggestion` Audit-trailed rejection terminal for any suggestion kind. :mod:`~entrypoints.cli._ledger_llm_payloads` CLI JSON-envelope projections for suggest, saturate, and reject paths. :class:`~domain.transactions.LLMClassificationResponse` Domain classifier response projected into :class:`LLMClassificationSuggestion`. :class:`~domain.transactions.LLMSplitResponse` Domain split response projected into :class:`LLMSplitSuggestion`."""from__future__importannotationsfromdecimalimportDecimalfromenumimportStrEnumfrompydanticimportBaseModel,Fieldfrom...coreimportSTRICT_FROZEN_CONFIGas_STRICT_FROZENfrom...domain.categoriesimportSpendingCategoryfrom...domain.ivaimportIvaCategoryfrom...domain.transactionsimportBusinessClassificationfrom._modelsimportManualLedgerTransactionResult
[docs]classLLMProvider(StrEnum):"""Subprocess LLM provider names accepted by the classify surface."""CLAUDE="claude"ANTIGRAVITY="antigravity"CODEX="codex"
[docs]classLLMClassificationSuggestion(BaseModel):"""One LLM classification suggestion for a transaction, not yet persisted."""model_config=_STRICT_FROZENtransaction_id:str=Field(min_length=1)provider:LLMProvider|None=Noneprovenance:str=Field(min_length=1)classification:BusinessClassificationcategory:SpendingCategory|None=Noneconfidence:Decimalreason:str=Field(min_length=1)evidence_id:str|None=Nonemultiple_components:bool|None=None"""True when the evidence read judged a split may be warranted."""@propertydefrecommends_split(self)->bool:"""True when the evidence read flagged the invoice as multi-component."""returnself.multiple_componentsisTrue
[docs]classLLMProviderAvailability(BaseModel):"""Whether one subprocess LLM provider has a usable CLI on ``PATH``."""model_config=_STRICT_FROZENprovider:LLMProvidercli_binary:str=Field(min_length=1)available:boolresolved_path:str|None=None
[docs]classLLMSaturatedSuggestion(BaseModel):"""A saturated LLM suggestion: business decision plus grounded tax substrate."""model_config=_STRICT_FROZENtransaction_id:str=Field(min_length=1)provider:LLMProvider|None=Noneprovenance:str=Field(min_length=1)classification:BusinessClassificationcategory:SpendingCategory|None=Noneconfidence:Decimalreason:str=Field(min_length=1)iva_category:IvaCategory|None=Nonebusiness_pct:Decimal|None=Noneiva_rate:Decimal|None=Nonetaxable_base:Decimal|None=Noneiva_amount:Decimal|None=Nonerate_derivable:bool=Falsederivation_note:str=""evidence_id:str|None=Noneevidence_advisory:str=""multiple_components:bool|None=None"""True when the evidence read judged a split may be warranted."""@propertydefrecommends_split(self)->bool:"""True when the evidence read flagged the invoice as multi-component."""returnself.multiple_componentsisTrue
[docs]classOperatorIvaDerivationResult(BaseModel):"""Result of an operator-initiated IVA derivation for one transaction."""model_config=_STRICT_FROZENtransaction_id:striva_category:IvaCategoryderivable:booliva_rate:Decimal|None=Nonetaxable_base:Decimal|None=Noneiva_amount:Decimal|None=Nonenote:str=""result:ManualLedgerTransactionResult|None=None
[docs]classLLMSplitChildSuggestion(BaseModel):"""One reviewed child of an evidence-driven split, with derived numbers."""model_config=_STRICT_FROZENproportion:Decimalamount:Decimaldescription:str=Field(min_length=1)category:SpendingCategory|None=Noneiva_category:IvaCategory|None=Noneiva_rate:Decimal|None=Nonetaxable_base:Decimal|None=Noneiva_amount:Decimal|None=Nonerate_derivable:bool=Falsederivation_note:str=""evidence_citation:str=""
[docs]classLLMSplitSuggestion(BaseModel):"""An evidence-driven N-way split proposal with derived child amounts."""model_config=_STRICT_FROZENtransaction_id:str=Field(min_length=1)provider:LLMProvider|None=Noneprovenance:str=Field(min_length=1)reason:str=Field(min_length=1)parent_amount:Decimalchildren:tuple[LLMSplitChildSuggestion,...]evidence_id:str|None=None@propertydefrecommends_split(self)->bool:"""True when the model proposed more than one child."""returnlen(self.children)>1
[docs]classLLMSplitApplyResult(BaseModel):"""Outcome of applying a reviewed evidence-driven split."""model_config=_STRICT_FROZENbucket_id:str=Field(min_length=1)parent_transaction_id:str=Field(min_length=1)split_group_id:str=Field(min_length=1)child_transaction_ids:tuple[str,...]provenance:str=Field(min_length=1)classified_child_count:int
[docs]classLLMSuggestionRejectionResult(BaseModel):"""Outcome of explicitly rejecting an LLM suggestion."""model_config=_STRICT_FROZENbucket_id:str=Field(min_length=1)transaction_id:str=Field(min_length=1)bucket_event_id:str=Field(min_length=1)suggestion_kind:str=Field(min_length=1)provenance:str=Field(min_length=1)operator_reason:str=""