Open source · self-hosted
An AI analyst you can audit.
Give it a business's spreadsheets, PDFs and emails. It reports what changed, why, and what to do next, and every number is recomputed and every claim cited before you see it.
- 18/20 real-data checks : checks passed on real public datasets, evals/results/2026-10-23-baseline-real.json
- 0 wrong numbers : numbers the verifier found wrong in the latest real-model run (3/3 jobs), evals/results/2026-10-16-configured-small-smoke.json
- V1–V8 verifier : 8 checks on every report before it ships, the verifier's check table in the recorded run
- runs on free local models : the default install: planner ollama:qwen3.5:9b, extractor ollama:qwen3.5:9b, judge ollama:qwen3.5:9b, embedder transformers:Xenova/bge-m3
git clone https://github.com/gfirik/agentic-analytics
cd agentic-analytics && bun install Exhibit · a real report from the example run, verified
Report · coffee-roaster · April
Revenue went from $44,100N12 in 2026-03 to $36,200N11 in 2026-04 (a change of −$7,900N29, −17.9%N30). It stood at $37,000N17 in 2026-06.
By channel, “wholesale” fell $8,500N149, more than the whole drop; “retail” rose $600N151.
The hotel customer paused its standing order because of the new price list. D1
Recommended: offer wholesale accounts a volume-tier discount to win back paused standing orders, $3,995N350 to $7,191N351 over the next quarter.
verified: 101 numbers recomputed, 0 mismatched · 6 citations checked
-
1/4 Revenue fell −17.9% how it was computed
N30 I5 trend_break.v1 revenue 2026-04:change_pct
−17.9% recomputed
Function, in code
pct_change(N11, N12)Inputs Input Fact Value N11 revenue_2026-04 $36,200 N12 revenue_2026-03 $44,100 -
2/4 April revenue, $36,200 the SQL and the sheet rows it read
N11 revenue_2026-04
$36,200 recomputed
SQL, run in DuckDB
WITH m1 AS ( SELECT strftime("date", '%Y-%m') AS period, sum("amount") AS value FROM "t_t1" WHERE NOT _is_total AND "date" IS NOT NULL AND (("status" IS NULL) OR ("status" NOT IN ($1))) AND strftime("date", '%Y-%m') BETWEEN $2 AND $3 GROUP BY ALL ) SELECT period, value FROM m1 ORDER BY period$1 = 'refunded' · $2 = '2026-04' · $3 = '2026-04'
Cited rows · sales-2026.xlsx · 50 cited
Rows of sales-2026.xlsx around the first cited row Row date amount status 3094 2026-03-31 17.89 refunded 3095 (cited) 2026-04-01 32.9 paid 3096 (cited) 2026-04-01 20.98 paid -
3/4 Wholesale: $8,500 the change split by channel
N149 contribution.v1 revenue 2026-04 by channel:magnitude:wholesale
$8,500 recomputed
Function, in code
abs(N144)Inputs Input Fact Value N144 contribution.v1 revenue 2026-04 by channel:delta:wholesale −$8,500 -
4/4 The cause quoted from email-0412.eml, checked against the file
D1 email · email-0412.eml
Quoted from email-0412.eml · Standing order
After the new price list we are pausing our standing order while we compare suppliers.
quote checked against the source
Fact ledger · every fact behind the exhibit
| ID | Value | Fact | Computed by | Verified |
|---|---|---|---|---|
| N11 | $36,200 | revenue_2026-04 | SQL in DuckDB | recomputed |
| N12 | $44,100 | revenue_2026-03 | SQL in DuckDB | recomputed |
| N17 | $37,000 | revenue_2026-06 | SQL in DuckDB | recomputed |
| N29 | −$7,900 | I5 trend_break.v1 revenue 2026-04:change | delta(N11, N12) | recomputed |
| N30 | −17.9% | I5 trend_break.v1 revenue 2026-04:change_pct | pct_change(N11, N12) | recomputed |
| N110 | $28,200 | revenue[channel=retail]_2026-03 | SQL in DuckDB | recomputed |
| N111 | $28,800 | revenue[channel=retail]_2026-04 | SQL in DuckDB | recomputed |
| N112 | $15,900 | revenue[channel=wholesale]_2026-03 | SQL in DuckDB | recomputed |
| N113 | $7,400 | revenue[channel=wholesale]_2026-04 | SQL in DuckDB | recomputed |
| N115 | $10,188.15 | revenue[product=Filter blend 250g]_2026-03 | SQL in DuckDB | recomputed |
| N116 | $9,551.53 | revenue[product=Filter blend 250g]_2026-04 | SQL in DuckDB | recomputed |
| N117 | $8,797.74 | revenue[product=Drip bags x10]_2026-03 | SQL in DuckDB | recomputed |
| N118 | $8,973.81 | revenue[product=Drip bags x10]_2026-04 | SQL in DuckDB | recomputed |
| N119 | $9,214.11 | revenue[product=Espresso blend 250g]_2026-03 | SQL in DuckDB | recomputed |
| N120 | $10,274.66 | revenue[product=Espresso blend 250g]_2026-04 | SQL in DuckDB | recomputed |
| N121 | $5,565.48 | revenue[product=Filter blend 1kg]_2026-03 | SQL in DuckDB | recomputed |
| N122 | $3,773.01 | revenue[product=Filter blend 1kg]_2026-04 | SQL in DuckDB | recomputed |
| N123 | $10,334.52 | revenue[product=Espresso blend 1kg]_2026-03 | SQL in DuckDB | recomputed |
| N124 | $3,626.99 | revenue[product=Espresso blend 1kg]_2026-04 | SQL in DuckDB | recomputed |
| N144 | −$8,500 | contribution.v1 revenue 2026-04 by channel:delta:wholesale | delta(N113, N112) | recomputed |
| N145 | $600 | contribution.v1 revenue 2026-04 by channel:delta:retail | delta(N111, N110) | recomputed |
| N146 | −$7,900 | contribution.v1 revenue 2026-04 by channel:delta:total | sum(N145, N144) | recomputed |
| N149 | $8,500 | contribution.v1 revenue 2026-04 by channel:magnitude:wholesale | abs(N144) | recomputed |
| N150 | 1 | contribution.v1 revenue 2026-04 by channel:exceeds:wholesale | abs_exceeds(N144, N146) | recomputed |
| N151 | $600 | contribution.v1 revenue 2026-04 by channel:magnitude:retail | abs(N145) | recomputed |
| N163 | 725 | revenue__qty[product=Filter blend 250g]_2026-03 | SQL in DuckDB | recomputed |
| N164 | 657 | revenue__qty[product=Filter blend 250g]_2026-04 | SQL in DuckDB | recomputed |
| N165 | 636 | revenue__qty[product=Drip bags x10]_2026-03 | SQL in DuckDB | recomputed |
| N166 | 629 | revenue__qty[product=Drip bags x10]_2026-04 | SQL in DuckDB | recomputed |
| N167 | 691 | revenue__qty[product=Espresso blend 250g]_2026-03 | SQL in DuckDB | recomputed |
| N168 | 711 | revenue__qty[product=Espresso blend 250g]_2026-04 | SQL in DuckDB | recomputed |
| N169 | 25 | revenue__qty[product=Filter blend 1kg]_2026-03 | SQL in DuckDB | recomputed |
| N170 | 17 | revenue__qty[product=Filter blend 1kg]_2026-04 | SQL in DuckDB | recomputed |
| N171 | 46 | revenue__qty[product=Espresso blend 1kg]_2026-03 | SQL in DuckDB | recomputed |
| N172 | 17 | revenue__qty[product=Espresso blend 1kg]_2026-04 | SQL in DuckDB | recomputed |
| N173 | −$1,911.07 | pvm.v1 revenue 2026-04 by product:pvm:volume | pvm.v1(N115, N163, N116, N164, N117, N165, N118, N166, N119, N167, N120, N168, N121, N169, N122, N170, N123, N171, N124, N172) | recomputed |
| N174 | −$7,170.84 | pvm.v1 revenue 2026-04 by product:pvm:mix | pvm.v1(N115, N163, N116, N164, N117, N165, N118, N166, N119, N167, N120, N168, N121, N169, N122, N170, N123, N171, N124, N172) | recomputed |
| N175 | $1,181.91 | pvm.v1 revenue 2026-04 by product:pvm:price | pvm.v1(N115, N163, N116, N164, N117, N165, N118, N166, N119, N167, N120, N168, N121, N169, N122, N170, N123, N171, N124, N172) | recomputed |
| N347 | 50.0% | scenario impact.v1:recover_share:param_low | scenario_param() | recomputed |
| N348 | 90.0% | scenario impact.v1:recover_share:param_high | scenario_param() | recomputed |
| N349 | 6.0% | scenario impact.v1:recover_share:param_cost | scenario_param() | recomputed |
| N350 | $3,995 | scenario impact.v1:recover_share:impact_low | impact.v1:recover_share(N144, N349, N347) | recomputed |
| N351 | $7,191 | scenario impact.v1:recover_share:impact_high | impact.v1:recover_share(N144, N349, N348) | recomputed |
| D1 | quote | email · email-0412.eml | After the new price list we are pausing our standing order while we compare suppliers. | checked |
The engine, opened up
A small factory for business questions.
11 stations, from your files to a checked report. The models work behind glass; every number is made by the database.
How to read the machine
Files in on the left, a checked report out on the right
Metrics, Explain, Decide and Verify sit behind glass: they call a model, and no model is allowed to write a number. Every number is computed by the database, then computed again by the verifier.
The long tank in front is the job's database file, where every station saves its work; the room at the back holds the models.
Scroll to move down the line, or explore the machine yourself.
Stations 01–02
Read and understand the files
- Read the files Opens every file and finds the real tables in it.
- Understand the columns Works out what each column means.
Learn: Ingestion & profiling · Security
Stations 03–04
Agree what to measure, then calculate it
- Agree what to measure Decides what to measure, and lets you approve it.
- Calculate the numbers Every number is made here, by the database, not by a model.
Learn: Metric layer · Analysis
Stations 05–08
Find out why, and suggest what to do
- Make documents searchable Makes your documents and emails searchable.
- Look things up online Optional: outside context, such as an industry benchmark.
- Find out why A model looks for why things changed.
- Suggest what to do A model suggests what to do next.
- The AI models Where the models work, each with one narrow job, behind glass.
Learn: Retrieval · Security · Web lane · The planner (agent) · Model router & free providers
Explore
Turn it, zoom in, pick a station
Drag to turn the machine, scroll or pinch to zoom, and right-drag or use two fingers to move it. Choose any station to see what it does.
Back to tour (or Esc) returns to the stop you scrolled to.
Every station, as a list
The stations, in the order a job runs them
-
Station 00 · Doors
Your files go in
Where you hand over your files. A terminal command, an HTTP API, an MCP server for AI assistants and a Telegram bot: whichever you use, the same job starts.
No model: plain code.
- Input folder
- examples/coffee-roaster
- Files handed over
- 4
Learn: Ingestion & profiling
-
Station 01 · Ingest
Read the files
Opens every file and finds the real tables in it. Titles, totals and several tables on one sheet are told apart; documents and emails are cut into sections. Every row remembers its file and row number.
No model: plain code.
- Files read
- 4
- Tables found
- 3
- Document sections
- 9
Learn: Ingestion & profiling
-
Station 02 · Profile
Understand the columns
Works out what each column means. Dates, money and its currency, customers; it flags problems such as duplicates or refunds, and screens cell text for instructions aimed at a model.
No model: plain code.
- Data-quality issues
- 3
Learn: Ingestion & profiling · Security
-
Station 03 · Metrics · METRICS_DRAFT
Agree what to measure
Decides what to measure, and lets you approve it. A model proposes each definition; code checks every column it names and compiles it to SQL, and you confirm or edit it before anything is measured.
Calls a model: the planner.
- revenue
- sum of amount, per month
- Status
- assumed
Learn: Metric layer
-
Station 04 · Analysis · MEASURE · DETECT · DECOMPOSE
Calculate the numbers
Every number is made here, by the database, not by a model. SQL computes each figure, tests a change against its own noise and splits it by who caused it. Each number is stored as a fact with an id, its query and the rows it read.
No model: plain code.
Learn: Analysis
-
Station 05 · Index docs · INDEX_DOCS
Make documents searchable
-
Station 06 · Web lane · WEB_CONTEXT
Look things up online
Optional: outside context, such as an industry benchmark. Before any search leaves your computer, customer names and amounts are removed from it; fetched pages are read only by the quarantined extractor.
No model: plain code.
- W1
- coffee-benchmarks.example
- Gap to the benchmark
- 1.4 pp N270
- Source
- offline web fixture (coffeeWeb)
-
Station 07 · Explain
Find out why
A model looks for why things changed. It sees the list of facts with their ids, never the raw data, and works through a few fixed tools. It writes {{N…}} placeholders, never a number.
Calls a model: the planner and extractor.
- Cause found in
- D1 · email-0412.eml
- Graded
- document supported
Learn: The planner (agent) · Security
-
Station 08 · Decide
Suggest what to do
A model suggests what to do next. What each action could be worth is a range computed by code, with the metric and the date that will show whether it worked.
Calls a model: the planner.
- Top action
- Offer wholesale accounts a volume-tier discount to win back paused standing orders
- Checked later by
- revenue >= $44,100 by 2026-09-30
Learn: The planner (agent)
-
Station 09 · Verify
Check everything
Nothing reaches you until it passes. Its checks recompute every number from its SQL, find every quote in its file and drop or reword any claim they can't prove. A small judge model only confirms that a quote supports its claim.
Calls a model: the judge.
- Numbers recomputed
- 101 · 0 mismatched
- Citations checked
- 6
- Claims dropped · reworded
- 2 · 2
Learn: The verifier (V1–V8)
-
Station 10 · Render
Write the report
Builds the final report. Each placeholder is filled with its checked fact; in the report, any number opens how it was computed.
No model: plain code.
- Headline
- Revenue fell after March, driven by wholesale (a change of −$7,900).
Learn: Report & fact IDs
Beside the line
-
Beside the line · Workspace · job.duckdb
The job's database file
One DuckDB file per job, where every station saves its work. Tables, facts, document passages, claims and checks live in it, and each stage is checkpointed there, so a job that stops resumes where it was.
No model: plain code.
- Events recorded
- 31
Learn: Report & fact IDs
-
Beside the line · Model room · planner · extractor · judge
The AI models
Where the models work, each with one narrow job, behind glass. The planner drafts definitions, causes and actions; the extractor alone reads untrusted text; the judge checks quotes. They run on free endpoints or your own machine.
The models themselves.
- In this run
- scripted planner (coffeePlanner), no LLM
Learn: Model router & free providers · Security
§1 How it works
What one run produced, stop by stop.
The files it read, the SQL it ran, the figures and the checks, from the same run.
One real run · examples/coffee-roaster · scripted planner, no LLM
01 Files INGEST
| File | Kind | Read as |
|---|---|---|
| email-0412.eml | headers and body | |
| ops-notes.docx | document | text, by section and page |
| price-list.pdf | document | text, by section and page |
| sales-2026.xlsx | table | sheets, split into table regions |
02 Profile PROFILE
- Tables
- 3
- Document sections
- 9
- Data-quality issues
- 3
Every column gets a role and a type before anything is measured; the issues go into the report.
03 Metric METRICS_DRAFT
revenue = sum of amount, per month · USD · assumed
Drafted in plain words and YAML, confirmed or assumed, then compiled to SQL:
SELECT strftime("date", '%Y-%m') AS period, sum("amount") AS value
FROM "t_t1" 04 SQL MEASURE · DETECT · DECOMPOSE
| Revenue, 2026-03 | $44,100 N12 |
|---|---|
| Revenue, 2026-04 | $36,200 N11 |
| Change | −$7,900 N29 |
| Change, percent | −17.9% N30 |
| By channel: wholesale | −$8,500 N144 |
| Wholesale's share of the change | 108% N147 |
05 Evidence INDEX_DOCS · WEB_CONTEXT · EXPLAIN · DECIDE
-
After the new price list we are pausing our standing order while we compare suppliers.
D1 email-0412.eml · Purchasing, Hotel Group · Standing order · document supported -
Wholesale prices increase 12% from 15 March 2026.
D7 price-list.pdf · p.2 · correlated -
Specialty coffee roasters typically see refunds of 1–2% of sales.
W1 external · coffee-benchmarks.example · a benchmark page from this run's offline web fixture, compared with the run's own numbers, never in place of them
06 Verify VERIFY
- V1 placeholders ran 19 times · pass
- V2 recompute ran 101 times · pass
- V3 re-derive ran 3 times · pass
- V4 definitions ran 18 times · pass
- V5 citations ran 73 times · pass
- V6 causal ran 18 times · 2 caught
- V7 grounding ran 2 times · pass
- V8 external ran 18 times · pass
101 numbers recomputed, 0 mismatched · 6 citations checked · 2 claims dropped, 2 downgraded
07 Report RENDER
Revenue fell after March, driven by wholesale (a change of −$7,900).
#1 action of 2 · medium confidence Offer wholesale accounts a volume-tier discount to win back paused standing orders: +$3,995 N350 to +$7,191 N351 , next quarter.
Checked later against: revenue >= $44,100 by 2026-09-30
§2 Why it's different
Built so you don't have to take its word for it.
Three rules the engine can't break.
-
01 · Arithmetic
The model never does arithmetic
The model writes placeholders; every figure is a fact computed by SQL or a deterministic function, then recomputed.
The pipelineThe model writes
Revenue went from {{N12}} in 2026-03 to {{N11}} in 2026-04 (a change of {{N29}}, {{N30}}).You read
Revenue went from $44,100N12 in 2026-03 to $36,200N11 in 2026-04 (a change of −$7,900N29, −17.9%N30).
-
02 · Definitions
Definitions you confirm
Each metric is drafted in plain words and YAML before anything is measured. You accept, edit or drop it.
The metric layerrevenue: sum of amount, per month · assumed · compiles toSELECT strftime("date", '%Y-%m') AS period, sum("amount") AS value FROM "t_t1" -
03 · Citations
Every claim is cited
Causes come from your documents, quoted exactly and checked against the source, and graded by how strong they are.
The verifierAfter the new price list we are pausing our standing order while we compare suppliers.
D1 email · email-0412.eml
§3 Results, as they are
Measured, with the caveats next to the numbers.
Real public data first; a run with jobs missing is labelled, not hidden.
| Metric · bar | Real public data 4 datasets, checked against SQL written separately | Free-tier model, smoke qwen/qwen3.8-27b on Groq, small-model mode | Nightly regression 30 generated businesses, same roles, over up to two nights | Offline baseline scripted heuristic, no LLM: checks the harness |
|---|---|---|---|---|
| Planted recall@5 bar ≥ 80% | 18/20 checks against SQL written separately Scored by its own checks instead: real files have no planted events. | 80.0% meets the bar | 68.0% below bar | 96.0% meets the bar |
| Distractor FP rate bar ≤ 10% | 0.0% meets the bar | 0.0% meets the bar | 0.0% meets the bar | |
| Driver accuracy bar ≥ 90% | 100.0% meets the bar | 100.0% meets the bar | 100.0% meets the bar | |
| Cause grade accuracy bar ≥ 80% | 75.0% below bar | 67.6% below bar | 100.0% meets the bar | |
| Citation precision bar ≥ 95% | 100.0% meets the bar | 100.0% meets the bar | 100.0% meets the bar | |
| Recommendation relevance bar ≥ 70% | 66.7% below bar | 30.0% below bar | 100.0% meets the bar | |
| Number mismatches bar 0 | 0 meets the bar | 0 meets the bar | 0 meets the bar |
Real public data is checked against SQL written separately; a free-tier model then runs the smoke suite. The generated businesses with planted events are the nightly regression check, run over up to two nights within the free tier's quota; the latest published run is shown, labelled partial only if jobs are still unfinished after its second night. The offline baseline is a scripted heuristic tuned on the same datasets, so its scores are optimistic: it tests the engine and the harness, not model quality. All results (RESULTS.md) · How quality is measured
Sources: 4 result files
evals/results/2026-10-23-baseline-real.jsonevals/results/2026-10-16-configured-small-smoke.jsonevals/results/2026-10-21-configured-small-full.jsonevals/results/2026-10-21-baseline-full.json
§4 Quickstart
Run it on the example, then on your files.
Install, pick a free model, analyze the example.
-
Step 1 · Install
git clone \ https://github.com/gfirik/agentic-analytics cd agentic-analytics bun install bun run aa doctorNode 22 and Bun.
doctorsays what this machine has. -
Step 2 · Model
# Ollama, or a free endpoint in .env cp .env.example .envFree and local only: Ollama or a free OpenAI-compatible endpoint. Running free
-
Step 3 · Analyze
bun run aa analyze \ examples/coffee-roaster --open # no model yet? replay it offline AA_DEMO_MODEL=scripted \ bun run aa analyze \ examples/coffee-roaster \ --yes --embedder hashThe report opens with every number linked to its working.
Standalone binaries · Docker
No Node or Bun needed: attached to each tagged release, from v0.1.0 on, with a checksum file. Releases
aa-linux-x64aa-linux-arm64aa-macos-arm64aa-windows-x64.exeSHA256SUMS
docker compose up · API, viewer and MCP on http://127.0.0.1:8080
Four doors, one engine
- CLI Analyze files, ask follow-ups, open the working behind any number.
aa analyze ./exports --open - HTTP API Upload files, stream progress over SSE, fetch the report as JSON or HTML.
aa serve - MCP Claude Desktop, Cursor or any MCP client analyzes files and quotes verified numbers.
aa mcp - Telegram A demo bot: send files, get the summary and the report back.
bun run --filter @agentic-analytics/telegram start