Data Agent Service

v0.2.0 both executors, every SQL source →

Ask your warehouse a question. Get an answer you can defend.

Natural-language questions over the warehouses, databases and APIs you already have — Fabric and Azure SQL, PostgreSQL, Databricks, and any REST service including a retrieval one — grounded in the glossary, metrics and schema held in OpenMetadata, fronted by Azure API Management. Each query is authorized twice: role rules in the service, then the source itself under the caller's own identity.

It runs locally on an emulator family, and against real Azure by changing configuration only.

Every capability below carries the command that proves it. Where something is designed but not built, this page says so.

end-to-end witnesses
28contract checks, both executors
44eval questions, 5 tiers
4source adapters shipped
+1.6msgateway cost at p95

The problem this solves

A language model that writes SQL is a demo. A language model you let near the revenue number is an engineering problem, and it fails in three specific ways.

It uses the wrong meaning

Contoso's fiscal year starts 1 April, so July trading is Q2, not Q3. A model reading column names alone gets a confident, wrong number — and nothing in the output says so.

Meaning lives in OpenMetadata, retrieved per question

It uses the wrong permissions

Most agents query with the service's credentials. Every user then sees everything the service can see, and the warehouse's own access control stops applying.

On-behalf-of: the engine sees the caller, not the app

You cannot check whether it is right

Without an accuracy suite and an audit trail, "it seems to work" is the whole quality argument — and it stays that way until someone is misled by it.

Scored evals, plus an audit line per query

How it fits together

Standard protocols end to end — MCP, OAuth 2.0 on-behalf-of, managed identity, TDS. No component knows whether it is talking to an emulator or to Azure.

MCP client Claude · Cursor any MCP SDK APIM gateway authn · rate limit token ceilings Executor SQL guard (parse tree) access rules OBO exchange audit OpenMetadata glossary · metrics · schema Entra ID identity · OBO Fabric Warehouse T-SQL over TDS PostgreSQL token as password Databricks Statement Execution REST · retrieval APIs OpenAPI-guarded
The gateway carries policy. The executor carries the guard, the catalog lookup and the identity exchange. Adding a source is configuration, not a fork.

What it does

capabilities, each with the evidence behind it and an honest status.

Generalizable by adapter 4 shipped

Four adapters behind one interface: TDS (Fabric, Azure SQL, Synapse), PostgreSQL, Databricks, HTTP. A new source is a config entry, not a fork — and the dialect-aware guard emits TOP or LIMIT without anyone choosing.

docs/09-adding-a-source.md · two datasets live in two engines

REST APIs as a source shipped

The OpenAPI document is the allow-list: unsafe operations are never offered, an undeclared parameter is refused rather than dropped, the ceiling is written into the request. No parse tree here — every property was translated, not ported.

10 witnesses · OpenMetadata's own API is the live source

Enterprise RAG as a source shipped

A knowledge base plugs in as a REST source. The usual hole in enterprise retrieval: the vector store is queried as a service, so a passage the asker may not read reaches their answer and nothing records it. Here the source declares whose permissions it applies, and denied fields are stripped as warehouse columns are.

top_k capped by the deployment, not the model · docs/15-http-sources

Any AI tool witnessed

A standards-compliant MCP server over Streamable HTTP, no vendor extensions. Proved by two independent official SDKs — Python and TypeScript — so a server only its own language can drive fails rather than passes.

28 client checks · config generated for Claude, Cursor, VS Code

Per-user authorization, on-behalf-of witnessed

The user's token is exchanged (OAuth 2.0 OBO), so the engine applies their permissions, not the service's. Table and column rules run on the SQL parse tree, and a withheld column disappears from schema discovery too.

3 personas: same question, different answers, one denial

Performance tested measured

k6 drives four scenarios: gateway, executor direct, catalog, rate limiter. Thresholds gate the run and payloads are inspected — an HTTP-only check would score a refusal as a success.

261 req/s · p95 69.5ms · gateway costs +1.6ms and ~2%

Natural-language evals harness ready

31 questions across five tiers and two datasets, scored on result sets rather than SQL text. Reference answers score 100%, which calibrates the instrument. The live model has not been scored — that run needs an API key.

make eval · L3 questions fail without the catalog

Auditable shipped

Every operation writes a structured line — who asked, which source, the verdict, the columns read, and whether it ran as the user or as the service, so a weaker authorization tier is visible rather than assumed.

op · user · source · verdict · authz_tier · ms

Dashboard promotion shipped

Recurring ad-hoc questions are the signal a dashboard is justified. Queries are canonicalised — literals and aliases normalised away — so the same question asked three ways lands on one template, and frequency becomes evidence.

promoter/ · witnesses · no natural language is stored

Three dashboard targets, one plan shipped

One Plan — tables, measures, dimensions, slots — spelled by each target in its own language: a Power BI semantic model, an Apache Superset dataset and chart, a Tableau workbook. Each is then asked the question through its own engine and checked against the SQL the template came from. A dashboard whose two answers disagree never reaches the catalog.

Each carries the guarded template, not the tables behind it, so no target can widen the surface the access rules narrowed.

+ + witnesses · identity carried where the tool can hold it, and named honestly where it cannot

Tableau: the generator, not the tenant generator shipped, hop unwitnessed

Tableau has no container, so the line is drawn where the evidence stops. The workbook, the VizQL query and the connected-app token are pure functions of the Plan, witnessed on every push. Publishing to a real site is not — the ledger carries that red rather than letting a green suite imply otherwise.

witnesses above the tenant line · publish() refuses by name

Why the meaning has to come from the catalog

The evals are built so that ignoring the glossary changes the answer, not just the number — because a wrong number that looks plausible is the failure mode that matters.

QuestionNaive readingWith the catalog
Q2 revenue Calendar Jul–Sep Fiscal Q2 — the year starts 1 April
Revenue SUM(amount_usd), cancellations included Net of cancellations — about 5% of web orders
Fastest support team Elapsed time: Billing is last at 502 min Resolution time excludes customer wait: Billing is first at 210 min
The third row is the test worth having.

The naive answer does not merely round differently — it names the wrong winner. A ranking flip cannot be explained away, so the eval fails loudly instead of drifting quietly.

Honest status

This project's discipline is that a claim is worth what checks it. The same rule applies to this page.

CapabilityStatusWhat backs it
Fabric + PostgreSQL, both executorsshipped28/28 contract against each; two datasets live in two engines
Databricks adaptershipped, unwitnessedIn executor-py with unit tests, over the Statement Execution API — but never run against a real workspace, and absent from the Go executor
Fabric LakehouseuntestedIts SQL analytics endpoint is the same TDS surface the Warehouse exposes, so the existing adapter should reach it — nothing here has tried
Snowflakenot built~150 lines against External OAuth trusting Entra; plan §15
DuckDBnot supportedNo adapter and no dialect entry — it appears nowhere in the code
MCP, client-agnosticwitnessed28 checks; two independent official SDKs
OBO per-user authorizationwitnessedThree personas, including a denial
Guardrails on the parse treeshippedUnit corpus shared by both implementations
Load testingmeasuredFour k6 scenarios with gating thresholds
Eval harness and gold baselineshipped31 questions, five tiers, result-set scoring
Live model accuracy scoremeasured, small nThrough Claude Code, not the SDK. Catalog vs none: pass 71% vs 50%, semantics 100% vs 40%, L3 +60 points. That run covered 14 support questions on one repeat — directional, not statistically strong, and predates the L3 expansion
Audit trailshippedStructured line per operation, authz tier recorded
Production parity (real Azure)gated, unwitnessedTerraform validates, runbook and preflight exist; not yet run against a tenant
Dashboard promotionshippedTemplates counted by pseudonymous user, k-threshold, catalog-derived titles; no question stored
Power BI targetshippedDirect Lake semantic model and PBIR report, published under OBO as the asking person, verified by evaluating its own DAX against the SQL; rendering prod-only
Apache Superset targetshippedVirtual dataset carrying the guarded template, chart and dashboard; verified through Superset’s own query layer. authz_tier: service — Superset has no on-behalf-of exchange, so the asker is recorded as an OpenMetadata owner and the docs say so rather than implying otherwise
Tableau targetgenerator shipped, hop unwitnessedWorkbook, VizQL Data Service query and connected-app token are pure functions of the Plan, recorded in the contract and witnessed in CI. authz_tier: user via direct trust. Publishing to a real site needs a tenant — red in the ledger until one is witnessed
A fourth dashboard targetnot builtCube, Looker, Metabase and the rest are a DashboardTarget each — see adding a dashboard target. The filters that decide whether one is worth building are written down there
Existing MCP server as a sourceshippedAPIM mcpMode: passthrough; OpenMetadata's own MCP server reaches the agent this way in every run
REST source, no adapter (APIM REST→MCP)blocked upstreamThe pinned gateway builds a fresh request and copies no inbound headers, so on-behalf-of sees an anonymous caller (upstream #8)
REST source adapter (incl. retrieval APIs)shippedPython executor; guarded against the OpenAPI document, registered in OpenMetadata as an apiService, 10 witnesses
GraphQL source adapternot builtNeeds a query parser with depth and cost budgets — closer to the SQL guard than to REST
Operation-level eval metricsnot builtResult-set comparison is meaningless for retrieved prose; a KB source needs its own scoring

Get it

Two executor images, same contract, same version. Pick either — nothing above the executor changes, and both now serve every configured source.

docker pull ghcr.io/calvinchengx/data-agent-service/executor-py:0.2.0
docker pull ghcr.io/calvinchengx/data-agent-service/executor-go:0.2.0
ImageSizePlatformsEngines it serves
executor-go19.5 MBlinux/amd64 · linux/arm64Fabric (T-SQL over TDS), PostgreSQL
executor-py290 MBlinux/amd64 · linux/arm64Fabric, PostgreSQL, Databricks

Choose with DAS_EXECUTOR=py|go. The Go image is a static binary on distroless — no ODBC driver, no Kerberos libraries — which is most of the difference in size. Why both exist, and what the comparison measured, is in ADR 0001.

What this version does not yet do.

The REST adapter landed after v0.1.0 and is Python-only — the Go executor has neither it nor a Databricks adapter, recorded in ADR 0001 rather than discovered later. GraphQL is not built. An unknown source kind is refused outright rather than guessed at, so a source no adapter handles fails loudly instead of being answered from the wrong engine.

Run the whole thing

One command brings up the stack from nothing, seeds it, and verifies it — the same command continuous integration runs, so a fresh clone is a tested path rather than a hope.

git clone https://github.com/calvinchengx/data-agent-service
cd data-agent-service
cp .env.example .env
make stack          # up, seed, apply, verify
make ask Q="What was net revenue in fiscal Q2?"

Local emulators and real Azure differ by configuration only. A gate greps the tree for endpoints written into code, target branching and development-only surfaces, so "it works the same in production" stays checkable.

make lint           # ruff, ty, golangci-lint
make test           # unit tests + end-to-end witnesses
make eval           # accuracy, per use case
make load           # k6 scenarios with gating thresholds
ENV=prod make test  # the same checks against a real tenant