vat_decision
Source: agent/src/accounting_agent/tax_rules/vat_decision.py.
Called by the booker (per-row) and also exposed as a Discord
slash command /vat-decision for manual queries.
The input
TransactionContext(
supplier_jurisdiction: str, # e.g. "DE"
customer_jurisdiction: str, # e.g. "EE"
customer_type: str, # "B2B" / "B2C" / "unknown"
service_type: str, # "goods" / "services" / "saas" / "unknown"
amount: Decimal | None,
occurred_on: str | None,
)
The booker derives this from the document summary (vendor_country
→ supplier_jurisdiction) + the company's own country
(customer_jurisdiction="EE") + heuristics for the rest.
The output
VatDecision(
rate: str, # "24%" / "0%" / "exempt" / "reverse-charge" / "OSS-destination" / "unknown"
confidence: "high" | "medium" | "low",
applicable_rule_ids: list[int], # tax_rules row ids relied on
reasoning: str, # free text — surfaced in logs + /vat-decision output
)
The booker calls _decision_to_efin_rate(decision) to map the
vocabulary to e-Fin's vat_rate_dropdown strings ('24', '0',
etc.). Unmappable values (OSS-destination, unknown) fall back
to OCR's vat_rate.
What it consults
tax_rulesSQL table — rows scoped to the supplier and/or customer jurisdiction whoseeffective_from <= occurred_on < effective_to. Retrieved as Pydantic models.- FalkorDB graph triples — same jurisdictions, predicates
matching common VAT concepts. Retrieved as
list[KnowledgeTriple]. - The LLM — a Pydantic AI agent. Default model =
z-ai/glm-5.2via OpenRouter; overridable via env or/setmodel.
The prompt assembles all three into a structured context the LLM
reasons over. It NEVER guesses — if the rules + triples don't
support a decision, it returns confidence='low'.
Confidence behavior in the booker
- high / medium → apply the LLM rate; stamp the rule_ids on the booking log
- low → fall back to OCR's
vat_ratefield; if that's also missing, the booker bails with SKIPPED_VAT_UNCERTAIN
The booker also catches any exception from decide_vat and
degrades to "low confidence" — the reasoner failing must not
crash the booker.
Why this design
Most "AI accounting" tools either:
- Hardcode VAT rates per country (stale when law changes), or
- Ask the LLM blindly to pick a rate (no rules, no audit trail)
This one inverts: the agent reasons OVER explicit rules. The rules live in a structured table the operator can edit. New tax guidance? Upload the PDF, the ingest pipeline extracts rules + triples, the next booking reasons against the updated knowledge.
Every booked entry carries the rule_ids it relied on. If you
ever need to audit a decision — your accountant questions a rate
during quarterly cleanup, say — the trail is right there:
vat_decision: rate='exempt' confidence=high rules=[3] →
/rules → "small business scheme, EE, turnover < €40k".
See VAT decisioning for the long form.