Skip to content

Laoode/agentic-data-entry

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

188 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Klaudia: The Agentic Accountant - Self-Hosted AI Bookkeeping Agent

Klaudia Workspace

Zero Error is the Baseline. Absolute Balance is the Goal.
An accountant that works in your ledger, on your hardware.


🏡 What this is

Klaudia is an agentic accountant for finance teams. You talk to it the way you would talk to a junior on your team, and it works directly in your ledgers: reads them, reconciles them, posts entries, closes the month, and tells you what it found.

It runs entirely on your own infrastructure. Ledger data, memory, and documents stay in your Postgres and your object store.

What it does today

Read and analyse totals, roll-ups by category or period, cross-sheet aggregation, filtered sums, ranking
Financial models profit and loss across statement sheets, gross and net margin, budget against actual by department
Tax PPN 11% and PPh 23 withholding, computed from the tax base when no tax column exists
Receivables aging schedules bucketed by days past due, overdue balances per customer
Reconciliation detail against summary, finding the entry that breaks a double-entry journal
Data entry append, correct, restructure tabs, multi-step month close across many turns
Document capture receipts and invoices to structured rows, including multi-page PDFs

Document capture is one input path, not the product. Most of the work happens after the data is in the ledger.


πŸ” Why it is safe to point at real books

Four properties, each enforced in code rather than by asking the model nicely.

Numbers are checked, not trusted. Every monetary figure in a reply is compared against a grounded set computed from that turn's actual tool results: cell values, column and row sums, group-by sums, cross-grid totals. An amount the evidence cannot produce is flagged. NUMERIC_VERIFY_MODE selects whether that logs or blocks.

Irreversible operations stop for a human. Impact is measured in code before anything runs. Deleting more than a handful of rows, clearing a whole column, or dropping a non-empty sheet parks the operation for approval and returns approve/reject to the client. Approval replays the stored call verbatim, so the model never re-plans a destructive action. This exists because prompting alone was not enough: an early evaluation run wiped seven sheets.

Tenants are separated at the tool layer. Spreadsheet parameters are stripped from every tool schema the model can see, and forced from a request-scoped value the model cannot reach. An agent cannot name another tenant's workspace even under prompt injection, because the parameter is not in its vocabulary.

Nothing leaves the building. Long-term memory runs on self-hosted mem0 with pgvector on your own Postgres, and embeddings come from a local service. No managed memory platform, no third-party vector store.


πŸͺ† Architecture

LLM Ops Pipeline
Mobile / API client ── JWT ──> FastAPI /v1 (per-user rate limits)
                                   β”‚
                                   β–Ό
                            Orchestrator ──> Langfuse traces
                                   β”‚
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚                               β”‚                               β”‚
Guardrails IN              Extraction Agent                Long-term memory
prompt-injection           dedup by content hash           continuity state +
+ scope checks             KIE model, sync or queued       semantic recall
   β”‚                               β”‚                               β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                   β–Ό
                          Supervisor (LangGraph)
                                   β”‚
                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                 β–Ό                                   β–Ό
            SQL Agent                          Data Entry Team
        document archive                  read / structure / write
                 β”‚                                   β”‚
                 β–Ό                                   β–Ό
           MCP archive                          MCP ledger
                                   β”‚
                                   β–Ό
                    Numeric verifier + destructive-op guard
                                   β–Ό
                             Guardrails OUT

Storage is one Postgres (ledger grids, document registry, tenancy, and the memory vectors), Redis for dedup and queues, MinIO for document blobs. Tools are reached over MCP, so the tool boundary stays explicit and auditable.

Tenancy model: user β†’ spreadsheets β†’ sheets. Each request resolves to exactly one spreadsheet and is bound to it for its whole lifetime.


πŸ§ͺ The Sandbox: how correctness is measured

Claims about an accounting agent are worth what their evaluation is worth, so the evaluation is a first-class part of this repo, not an afterthought.

The sandbox runs against its own Postgres, Redis database, and object store. It refuses to start if any of those resolve to the development ones. Ground truth is computed, never hand-written: seeded generators emit a ledger and its own totals together, and unit tests freeze both, so an expected number cannot drift away from the data it describes.

Two suites, one dataset:

Suite Measures Result
Behavior routing, tool calls and arguments, guardrails, clarification, extraction cache 61/62
Hard bench deterministic accounting work, exact-match graded 38/51

The hard bench is organised as a difficulty ladder, and the pass rate falls across it the way a useful benchmark should:

Tier Environment Current
Intern 1 spreadsheet, a few tabs, single-step read ~100%
Clerk writes, dirty number formats, approval gating 100%
Bookkeeper multi-sheet, reconciliation, journal balance 86%
Analyst P&L, tax, aging, variance, 1000+ rows 74%
Controller several spreadsheets, ten-step month close 65%
Auditor injection in data, hostile schema, refusal traps in progress

What it currently gets wrong is published, not hidden. Exact arithmetic over many rows drifts, and gets worse with volume. Long compound workflows drop tool calls. Given a journal that does not balance, it has reported one that does. Each of these has an open engineering lead. See tests/e2e/README.md.


πŸ“„ Document intelligence

One component of the platform: turning a photographed receipt or a multi-page invoice PDF into structured rows. Content-addressed by hash, so the same document is never extracted twice.

The extraction model is a fine-tuned Qwen 3.5 4B, evaluated with KIEVal, ANLS*, digit accuracy, and JSON validity.

Benchmark Results
Model Entity F1 Group F1 Aligned ANLS* Digit Accuracy JSON Validity
Gemma 4 E2B-it 49.29 11.04 39.72 36.22 50.83 99
Gemma 4 E4B-it 58.61 18.46 51.17 73.49 60.38 100
Qwen 3.5 2B 58.88 18.40 50.36 68.96 71.15 98
Qwen 3.5 4B 69.93 28.17 63.05 77.22 77.87 100
Klaudia (Qwen 3.5 4B Fine-Tuned) 87.02 71.88 84.90 93.99 94.58 100

Against the base model: +17.09 Entity F1, +43.71 Group F1, +21.85 Aligned, +16.77 ANLS*, +16.71 digit accuracy, JSON validity held at 100.

The extraction backend is selectable (self-hosted fine-tune, a hosted vision model, or a fixture mock for offline runs), so the pipeline does not depend on any single model being available.


πŸ”‹ Memory

Two tiers, because they fail differently.

Continuity is deterministic. It reads the ledger's own modification times to answer "where did we leave off", uses no model and no embeddings, and therefore cannot invent a past that did not happen.

Semantic memory is self-hosted mem0 over pgvector, holding preferences and facts worth carrying between sessions, with conflict-aware updates so a newer statement supersedes an older one. Hard-partitioned by user, and cross-user recall is graded as a security bug with a required leak rate of zero.

Both fail soft: if memory is unavailable, the agent answers without it rather than erroring.


🍏 Stack

Layer Technology
API FastAPI, JWT auth, per-user rate limits
Agents LangGraph supervisor with worker sub-agents
Tool boundary MCP (ledger server, document archive server)
Ledger PostgreSQL, sheet-semantics grids, 16-tool API
Memory mem0 (self-hosted) + pgvector + local embedding service
Reasoning model DeepSeek v4 pro today, swappable by configuration
Extraction model Qwen 3.5 4B LoRA fine-tune on vLLM
Queue and cache Redis, Taskiq workers
Object storage MinIO, BLAKE3 content addressing
Observability Langfuse (fail-open tracing)
CI CircleCI: lint, unit, integration with real service containers
Client React Native, Expo (separate repository)

Model serving lives on a separate inference platform (LiteLLM, vLLM, LMCache, autoscaling). This repository consumes an endpoint; it does not serve models.


πŸ“Ÿ Running it

docker compose up -d                  # Postgres, Redis, MinIO
cp .env.template .env                 # add model credentials
./startup.sh                          # API + MCP servers

# optional
docker compose --profile ui up -d        # NocoDB grid view over the ledger
docker compose --profile sandbox up -d   # isolated store for the evaluation suite
uv run pytest tests/unit -q                                    # fast, hermetic
MOCK_KIE=true SHEETS_BACKEND=ledger uv run pytest tests/e2e -q # full evaluation

πŸ—‚οΈ Repository

app/            FastAPI routes, orchestrator, guardrails, extraction,
                memory, verifier, approvals
klaudia/        agent graph: supervisor, sql_agent, data_entry_team, prompts
mcp-ledger/     Postgres ledger MCP server (the default sheets backend)
mcp-archive/    document registry MCP server
mcp-gsheets/    Google Sheets adapter (export mirror, optional backend)
services/embed/ local embedding service for memory
tests/e2e/      the sandbox: dataset, generators, runners, reports
scripts/        migration, mirroring, tenancy and sandbox utilities

πŸ“— Research

Model work runs as a separate track and is not part of this codebase: domain adaptation for financial document understanding, and post-training to replace the current reasoning model with one tuned for accounting tool use.

This platform began as an undergraduate research thesis on agentic AI for financial data entry, and is now being built toward production use by finance teams.

Warning

The receipt dataset and the fine-tuned extraction model will be released after the research paper is published.


πŸ—ΊοΈ Direction

Working toward a system a finance department can run in production:

  • deterministic aggregation pushed into the ledger, so arithmetic stops being something a model has to reason about
  • column-aware number grounding, so a correct value reported under the wrong label cannot pass
  • scheduled work: period-close reminders, recurring reports
  • release gates driven by the sandbox, so a regression blocks a merge

πŸ€ Mission

Where financial records lose balance, Klaudia restores order.

Entering financial data is not typing numbers. It is preserving the financial truth of an organisation.


πŸ”° Author

Yudhy McCodey

Building agentic systems for real-world financial automation.

About

Self-hosted AI accountant. Reads receipts / invoices, reconciles ledgers, checks its own numbers, and asks before anything irreversible. Open source, agentic, runs on your infrastructure.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages