Skip to main content
Glama
NirmalKumar31

agentic-analytics-engine

Agentic Analytics Engine

An analytics engine where language models plan investigations and interpret results, while deterministic tools own computation, statistics, security and provenance.

Point it at a built-in commerce warehouse or upload your own CSV or Parquet file. Ask a question in plain English. The engine decomposes it into analytical tasks, runs them in parallel through an MCP tool layer over DuckDB, verifies every claim against the rows that produced it, and publishes only what survived.

Every number in the report is clickable. Clicking it shows the task, the SQL, the result rows with the cited cells highlighted, the arithmetic the engine recomputed, the MCP calls involved, and the dataset fingerprint.

It runs with no API credentials.


The architecture that matters

        PROBABILISTIC CONTROL PLANE          decides what to investigate
   ┌──────────────────────────────────────┐
   │  Question Analyst   Planner          │
   │  Worker             Critic           │
   │  Visualiser         Reporter         │
   └──────────────────┬───────────────────┘
                      │  typed requests only
                      ▼
        DETERMINISTIC ANALYTICS PLANE        owns every number
   ┌──────────────────────────────────────┐
   │  MCP server (17 tools, 4 resources)  │
   │  Semantic metric layer               │
   │  DuckDB, locked read-only            │
   │  SciPy statistics                    │
   │  Change decomposition                │
   │  Result registry                     │
   │  Numeric verification                │
   │  Chart builder                       │
   │  Session isolation                   │
   └──────────────────────────────────────┘

The model chooses what to compute. It never computes. It does not write the metric formulas, run the database, calculate a statistic, build a chart specification, decide what a session may access, or validate its own numbers.

What survives if you delete every model provider: dataset ingestion, profiling, semantic schema inference, the metric layer, DuckDB execution, period comparison, segmentation, trend analysis, statistical testing, change decomposition, the MCP server and client, the SQL guard, numeric verification, provenance, chart construction, session isolation, the evaluation harness, and the API. What is lost is natural-language interpretation, dynamic planning, hypothesis generation and narrative writing.

That split is the point of the project. See docs/ARCHITECTURE.md.


Related MCP server: mcp-data-lens

The problem it addresses

"Chat with your CSV" fails in a specific way: the model writes SQL, reads the output, then writes prose, and nothing checks that the prose matches the output. A number off by a factor of ten, a direction that is backwards, and a causal claim built on a correlation all look exactly like a correct answer.

This separates three things those tools conflate:

Produced by

Checked by

Calculated fact

DuckDB

Arithmetic recomputed from the cited cells

Statistical result

SciPy

The test is run, never asserted

Interpretation

The model

Deterministic claim rules, then a critic

A claim failing any check does not reach the report. In the recorded shipping-repeat demo the worker proposes "Being in the late group causes the observed difference"; the verifier withholds it and the report says so.


Run it

make bootstrap    # virtualenv, Python and npm dependencies
make data         # generate the demo warehouse (deterministic, ~2 seconds)
make dev          # build the frontend and serve on http://127.0.0.1:8000

No .env required. make verify runs everything CI runs.

make test         # 683 Python tests
make evaluate     # score the engine against the injected patterns
make record       # re-record the three demo runs

Deterministic analytics, not generated SQL

The MCP server exposes analytical capabilities, not a SQL endpoint. In the benchmark, 35 of 35 tool calls used a governed tool and none used model-written SQL.

17 tools — list_tables, describe_table, profile_table, profile_dataset, sample_rows, list_metrics, compute_metric, compare_segments, compare_periods, analyze_timeseries, decompose_change, rank_contributors, correlation_matrix, statistical_test, aggregate_for_question, get_result, and run_readonly_sql as a guarded fallback.

4 resources — dataset://catalog, dataset://schema/{session_id}/{table}, metrics://definitions, result://{session_id}/{result_id}.

The tool that answers "why" is decompose_change. For an additive metric it splits a change into per-segment contributions. For a rate it runs a shift-share decomposition separating movement within segments from volume moving between them, and both reconcile exactly to the observed change or report that they did not. On the demo warehouse the Q3 margin drop resolves to −3.76pp of rate effect and −3.67pp of mix effect, summing to the observed −7.63pp — which is the pattern the generator injected.

Built on the official MCP Python SDK v2 (mcp 2.2.0, protocol 2026-07-28). Agents always hold a real mcp.Client. On the deployed site that client uses the SDK's in-process transport to the server object in the same container; /mcp is a separate Streamable HTTP transport for external callers, exercised in CI and against the built image. On the public deployment /mcp is withdrawn on purpose — see Security.


Three execution modes, labelled

A scripted-provider run executes the same graph, the same MCP calls, the same SQL and the same verification as a model-driven one. It is still not a language model, and the UI never implies otherwise.

Badge

What it means

Recorded

Replaying a committed run

Deterministic live

Executing now; agent decisions from a scripted deterministic provider

AI live

Executing now; agent decisions from a language model

/api/config reports execution_mode, model_inference_remote and mcp_remote_enabled, and every recording carries provider_kind and run_kind. /api/health answers liveness and carries an instance_id that changes if the process is replaced; /api/ready is what the platform's health check uses, and returns 503 unless the demo warehouse and the recordings are both present — a container that came up without them is alive and cannot serve anyone.

In deterministic mode the Ask panel says so in as many words: question interpretation is rule-based, while the SQL, the statistics, the MCP tool execution, the verification and the provenance are real.


Upload your own data

The public deployment accepts a CSV or Parquet file with no account.

Each upload gets a capability-based anonymous session: a cryptographically random handle plus a separate bearer capability delivered as an HttpOnly cookie. The handle appears in MCP resource URIs and authorises nothing on its own. This is isolation, not authentication — anyone holding the token is the session, and the docs say so rather than implying more.

  • One file, 15 MB, 200 columns, CSV or Parquet only on the public deployment

  • Its own DuckDB database and its own scratch directory, both erased when the session ends or expires after 30 minutes

  • Parquet validated through its metadata — columns, row groups, nesting, declared uncompressed size — before any data is read

  • CSV screened as bounded delimited text. CSV has no magic bytes, and the implementation does not claim otherwise

  • Per-IP and global rate limits, and a cap on concurrent analyses

An uploaded file has no governed metric layer, so the engine infers one from column types and cardinality and marks everything inferred. It will not invent business meaning: two columns that could both be revenue produce a clarifying question, not a guess.

How a question about your file is answered

aggregate_for_question maps the question onto that inferred schema by rule, inside the engine. A column counts only when the question names it; the operation comes from a fixed vocabulary — count, total, average, ranking, trend; and a column the operation needs is filled in only when the schema offers exactly one candidate, so the choice is forced rather than guessed. The engine then composes the SQL itself and it still passes the guard.

When the question cannot be resolved that way, it is refused with the reason and the table's profile is offered instead:

this question could not be mapped to the table without guessing (the question does not name which numeric column to use, and the table has 2 to choose from)

That refusal is the feature. Aggregating whichever column happened to look groupable would produce a number that reads like an answer without being one.

What leaves the server

Nothing, on the public deployment: the provider is the scripted one and no inference call is made. On a deployment configured with a cloud model, the schema, the profile and aggregated results are sent — and individual cells of your file are not. sample_rows is refused for uploaded data, and a profile's per-column minimum and maximum are withheld from the model, because those are cells rather than summaries. An aggregate over a group of one row can still equal a cell; that is inherent to aggregation and is stated in docs/LIMITATIONS.md rather than glossed over.


Provenance

Every tool execution produces a ResultSnapshot: the SQL, columns, rows, row count, truncation flag, duration, parameters, warnings, and the dataset fingerprint. A finding cites result_ids and specific (row, column) cells.

Before the critic model is consulted, two deterministic gates run:

  1. Claim shape — causal language on observational data, "significant" with no test behind it, or a claim citing nothing.

  2. Arithmetic — every number in the text must appear in a cited result or be derivable from two cited cells by subtraction, ratio or percentage change. A stated change is recomputed and compared, including its sign.

A model is the wrong tool for checking arithmetic, so it is not asked to.

And the report cannot undo that. The reporter is an organiser, not an author: it returns which verified findings belong in the summary and how to group them, and the engine writes every factual sentence from those findings' own text. Section headings are engine-chosen too, because a heading short enough to look like a label is still long enough to assert something. There is no free-text field in the schema the reporter fills, so a sentence like "New customers are the primary cause of poor Electronics performance" has nowhere to enter — which is a stronger guarantee than scanning the prose afterwards for the claims someone thought to look for.


The demo dataset

Generated locally from seed 20260924. No download.

Table

Rows

customers

30,000

products

800

orders

104,915

order_items

239,794

returns

19,471

shipping_events

104,915

marketing_daily

2,924

Two years, 5.3 MB of Parquet, fingerprint sha256:8e9ad9348f7dc18660d78ed3cd4d4b32. CI generates it twice and fails if the fingerprints differ.

Six phenomena are injected: a Q3 margin compression from discounting and a mix shift; a category with elevated returns concentrated in new customers; a carrier running late in one region; late first delivery associated with lower repeat purchase; an acquisition channel leading on revenue and trailing on contribution; and Q4 seasonality.

The answer key lives in data/ground_truth.py. Two tests keep it away from the agents: one walks the import graph, and one captures every prompt, context object and schema any provider sees during a full benchmark and asserts the answer key appears in none of them.


Benchmark

This is a deterministic end-to-end engine benchmark. It runs with the scripted provider, so it measures graph execution, MCP execution, SQL and statistical correctness, provenance, deterministic verification and publication behaviour. It does not measure language-model question understanding, planning quality or tool-selection reliability.

cases passed                      8 / 8
injected patterns recovered       6 / 6

candidate findings                37
  supported                       35
  withheld                         2
candidate support rate            35/37 = 0.946

published findings                35
  unsupported published            0
publication-gate integrity        35/35 = 1.000

findings numerically verified     35/35 = 1.000
SQL statements SQLGuard accepts   33/33 = 1.000
tool calls succeeded              35/35 = 1.000
provenance complete               35/35 = 1.000
chart fields valid              205/205 = 1.000

tool calls using a governed tool  35/35 = 1.000
tool calls using generated SQL     0

The two rates answer different questions. Candidate support is expected below 1.0 — a run that withholds nothing is not verifying anything. Publication-gate integrity must be exactly 1.0, and is a consistency check on the gate rather than an accuracy score: it asks whether the gate emitted anything its own pipeline rejected. Independent evidence that the findings are right comes from the injected patterns, which the agents cannot see.

The eight cases are a controlled synthetic regression suite over one generated warehouse. They are not evidence of external validity across arbitrary business datasets, and nothing here should be read as such.

Engine runtime is 0.094 s per question on a local machine and about 0.15 s on a GitHub-hosted runner, both with the scripted provider. That is a deterministic engine figure, it varies with the hardware, and it excludes model inference entirely — it is not a latency claim about an AI system.

Reproduce with make evaluate. Details in docs/EVALUATION.md.


Security

The model never reaches DuckDB directly.

SQLGuard parses every statement with sqlglot and works on the AST. 230 adversarial tests cover writes, DDL, COPY, ATTACH, extension loading, remote URLs, multi-statement payloads, file-as-table syntax, and DuckDB specifics like SUMMARIZE and FROM x SELECT. A further set covers read-only denial of service: unbounded generators, cross-join explosion, AST size and depth, join and CTE ceilings, and recursive CTEs, which are refused outright. Deep nesting previously crashed sqlglot's recursive-descent parser before any check could run.

The engine runs with enable_external_access=false and lock_configuration=true, applied after loading and irreversible for the connection's life. Tests bypass the guard on purpose to confirm DuckDB refuses on its own. Query cancellation uses con.interrupt(), verified to stop CPU work rather than just the waiting coroutine.

The MCP endpoint fails closed. A loopback binding gets a localhost allow-list automatically. A network binding must declare its hostnames; one that declares none has the remote transport withdrawn with a 503 rather than served with Host validation disabled. The public deployment leaves the allow-list empty, so the endpoint is withdrawn there by choice — the site's agents reach the same MCP server in-process, so nothing is lost but the exposure.

Uploaded cells do not go to a remote model. sample_rows is refused for an uploaded dataset whenever inference is remote, and a profile's per-column minimum and maximum are withheld from the one representation handed to an agent. A test inspects the actual prompt payloads rather than the code path.

Charts are built by the engine from an encoding the model chose, then validated on the server and again in the browser.

Dataset values are data. A cell containing IGNORE PREVIOUS INSTRUCTIONS or <script> round-trips as a string.


Providers

Mode

Credential

Used for

fake

no

tests, CI, recordings, the public demo

local

no

an Ollama-compatible server

cloud

yes

a hosted API, configured only through the environment

The scripted provider is a rule-based stand-in, not a language model. It reads every figure it writes out of a real ResultSnapshot, and on correlation tasks it proposes a causal claim — the mistake real models make most often — so the rejection path is exercised by a genuine error.

Reaching a paid endpoint requires both AAE_PROVIDER_MODE=cloud and a key in the environment. No default path spends money.


Layout

src/agentic_analytics/
  data/         deterministic generator + the injected answer key
  warehouse/    DuckDB sessions, capability model, metric layer, SQL guard, uploads
  analytics/    result contract, execution, metrics, statistics, decomposition, semantics
  mcp_layer/    MCP server (tools + resources) and the client agents use
  llm/          provider interface, scripted / Ollama / cloud adapters
  agents/       analyst, planner, worker, critic, visualiser, reporter
  verification/ arithmetic, claim shape, chart safety, multiple comparisons
  graph/        LangGraph workflow, state reducers, run orchestration
  api/          FastAPI, SSE, uploads, rate limits, mounted MCP endpoint
  recordings/   capture and publication acceptance
  evaluation/   benchmark cases and scoring
web/            React frontend

Limitations

The benchmark numbers come from the scripted provider and measure the engine, not model planning. Statistical scope is five test families with Holm correction within a task and no causal identification. Correlations above 50,000 rows use a seeded sample. There is no authentication — session capability is not an account. Rate limits are in-process, not durable quotas.

The full list is in docs/LIMITATIONS.md.


Verified

683 Python tests, 35 frontend tests, and 12 browser tests run on Chromium in CI and on Chromium, Firefox and WebKit before a release. 88% branch coverage. ruff, ruff format, mypy (with disallow_untyped_defs), pip-audit and npm audit clean. Frontend production bundle about 380 kB gzipped, of which about 296 kB is the Vega chart engine in a lazily-loaded chunk.

MIT licensed.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables users to ask questions about their spreadsheets in plain English and receive instant answers using DuckDB-powered analytics, supporting multiple file formats like Excel, CSV, and Parquet.
    2
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Enables AI agents to discover and execute analytical queries on a DuckDB dataset through typed MCP tools, with guarded read-only SQL support for complex calculations without direct database access.
    6
    MIT