Skip to content

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

  1. tax_rules SQL table — rows scoped to the supplier and/or customer jurisdiction whose effective_from <= occurred_on < effective_to. Retrieved as Pydantic models.
  2. FalkorDB graph triples — same jurisdictions, predicates matching common VAT concepts. Retrieved as list[KnowledgeTriple].
  3. The LLM — a Pydantic AI agent. Default model = z-ai/glm-5.2 via 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_rate field; 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.