Skip to content

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:

  1. Retrieves applicable rules from the SQL table (jurisdiction + effective dates)
  2. Retrieves matching triples from the graph
  3. Builds a prompt: context + rules + triples
  4. 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_rate field if present; otherwise the booker returns SKIPPED_VAT_UNCERTAIN and 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.