VAT decisioning
The vat_decision reasoner is what makes this an agent and not
a rule lookup. Every purchase booking calls it; the rate that
lands on the e-Fin entry is the LLM-reasoned rate, with the rule
ids it relied on stamped on the booking for audit.
The three layers
flowchart TB
ctx["TransactionContext<br/>supplier_jurisdiction<br/>customer_jurisdiction<br/>customer_type<br/>service_type<br/>amount, date"]
rules[("SQL tax_rules<br/>structured rows<br/>with effective dates")]
kg[("FalkorDB<br/>knowledge graph<br/>triples and relationships")]
llm["Pydantic AI<br/>reasoner LLM"]
out["VatDecision<br/>rate, confidence, rule_ids, reasoning"]
ctx --> llm
rules --> llm
kg --> llm
llm --> out
1. The rules table
A structured Turso table with one row per rule: jurisdiction,
tax_type, category, rate, effective_from, effective_to,
summary, conditions, source_document_id. Operator can list
them with /rules, add via /addrule, delete via /delrule.
When a new tax PDF is uploaded (kind=tax-rule), the ingest
pipeline extracts rules into this table automatically — see the
Estonia page for the worked example.
2. The knowledge graph
Same source PDFs also produce knowledge triples (subject,
predicate, object) mirrored into FalkorDB. Predicates are
dynamic — the extractor invents them as needed
(HAS_THRESHOLD, APPLIES_TO, REQUIRES_REGISTRATION, etc.).
The graph captures relationships the SQL table can't: "small_business_scheme applies to enterprises with turnover < 40000 EUR" or "OSS handles distance sales to consumers in other EU member states."
It's a reasoning layer, not a SQL mirror. Free-form. Queryable
via /ruleq with Cypher.
3. The reasoner
A separate Pydantic AI agent. Given a TransactionContext it:
- Retrieves applicable rules from the SQL table (jurisdiction + effective dates)
- Retrieves matching triples from the graph
- Builds a prompt: context + rules + triples
- Returns
VatDecision(rate, confidence, applicable_rule_ids, reasoning)
Confidence is one of high / medium / low. The booker uses:
- high / medium → apply the LLM rate silently, stamp the rule ids on the booking
- low → fall back to OCR's
vat_ratefield if present; otherwise the booker returnsSKIPPED_VAT_UNCERTAINand the operator is alerted
decide_vat never raises. On any error path it returns
VatDecision(rate='unknown', confidence='low').
Why this design
Most accounting tools ship with hardcoded VAT rates per country. When tax law changes (a new EU directive, an Estonian VAT update), they wait for a software release. Stale rates → wrong books.
This design has no hardcoded rates. The agent's "knowledge" lives in two data layers the operator can extend by uploading a PDF. Tax law changes? Upload the new guidance, the agent re-extracts, the next booking reasons against the updated rules. The codebase doesn't move.
What's stamped on each booking
Every successful booking carries:
vat_rate_dropdown— the rate that went on the e-Fin line- A log entry:
booker: recon #N vat_decision rate='X' confidence=Y rules=[ids]
/vat-decision runs the same reasoner manually with any context
you supply; useful for sanity-checking before bulk-booking.