analyticsreceipts 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- analyticsreceipts-0.0.2/.gitignore +5 -0
- analyticsreceipts-0.0.2/FREEZE-v1.sha256 +3 -0
- analyticsreceipts-0.0.2/LICENSE +21 -0
- analyticsreceipts-0.0.2/PKG-INFO +313 -0
- analyticsreceipts-0.0.2/README.md +289 -0
- analyticsreceipts-0.0.2/SECURITY.md +22 -0
- analyticsreceipts-0.0.2/configs/live-lite-config.json +30 -0
- analyticsreceipts-0.0.2/configs/live-lite-v2-config.json +34 -0
- analyticsreceipts-0.0.2/fixtures/expected-grades-v1.json +42 -0
- analyticsreceipts-0.0.2/fixtures/transcripts-v1.json +106 -0
- analyticsreceipts-0.0.2/golden/EXPECTED-V1-DIVERGENCE.md +26 -0
- analyticsreceipts-0.0.2/golden/expected-v1.json +354 -0
- analyticsreceipts-0.0.2/golden/expected-v2.json +354 -0
- analyticsreceipts-0.0.2/golden/expected-v2.json.sha256 +1 -0
- analyticsreceipts-0.0.2/golden/golden-v1.json +243 -0
- analyticsreceipts-0.0.2/golden/golden-v1.json.sha256 +1 -0
- analyticsreceipts-0.0.2/model/online_retail.ossie.yaml +128 -0
- analyticsreceipts-0.0.2/pyproject.toml +44 -0
- analyticsreceipts-0.0.2/replays/fixtures-v1-broken.json +2281 -0
- analyticsreceipts-0.0.2/replays/fixtures-v1.json +2281 -0
- analyticsreceipts-0.0.2/replays/live-lite-v1.jsonl +23 -0
- analyticsreceipts-0.0.2/replays/live-lite-v1.jsonl.resources.log +216 -0
- analyticsreceipts-0.0.2/replays/live-lite-v2.jsonl +19 -0
- analyticsreceipts-0.0.2/replays/live-lite-v2.jsonl.resources.log +308 -0
- analyticsreceipts-0.0.2/reports/m1-er1-regrade.md +44 -0
- analyticsreceipts-0.0.2/reports/m1-fixtures.grades.json +402 -0
- analyticsreceipts-0.0.2/reports/m1-fixtures.md +71 -0
- analyticsreceipts-0.0.2/reports/m1-live-lite-v2.md +88 -0
- analyticsreceipts-0.0.2/reports/m1-live-lite.md +61 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/__init__.py +3 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/broken.py +25 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/compare.py +67 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/compiler.py +216 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/contract.py +50 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/control_server.py +26 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/execution.py +185 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/fixtures.py +181 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/grader.py +348 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/live.py +423 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/load.py +74 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/materialise.py +86 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/model.py +122 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/numbers.py +93 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/paths.py +31 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/rawtools.py +105 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/receipt.schema.json +56 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/receipts.py +17 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/report.py +162 -0
- analyticsreceipts-0.0.2/src/analyticsreceipts/server.py +165 -0
- analyticsreceipts-0.0.2/tests/conftest.py +31 -0
- analyticsreceipts-0.0.2/tests/semantic-model.sha256 +1 -0
- analyticsreceipts-0.0.2/tests/test_accuracy.py +78 -0
- analyticsreceipts-0.0.2/tests/test_compiler.py +91 -0
- analyticsreceipts-0.0.2/tests/test_er1.py +138 -0
- analyticsreceipts-0.0.2/tests/test_expected_tables.py +34 -0
- analyticsreceipts-0.0.2/tests/test_freeze.py +109 -0
- analyticsreceipts-0.0.2/tests/test_live_caps.py +257 -0
- analyticsreceipts-0.0.2/tests/test_rawtools.py +90 -0
- analyticsreceipts-0.0.2/tests/test_receipt_schema.py +83 -0
- analyticsreceipts-0.0.2/tests/test_reference.py +79 -0
- analyticsreceipts-0.0.2/tests/test_regressions.py +514 -0
- analyticsreceipts-0.0.2/tests/test_resource_limits.py +124 -0
- analyticsreceipts-0.0.2/tests/test_server.py +96 -0
- analyticsreceipts-0.0.2/tests/tool-manifest.json +264 -0
- analyticsreceipts-0.0.2/vendor/ossie/LICENSE +201 -0
- analyticsreceipts-0.0.2/vendor/ossie/NOTICE +5 -0
- analyticsreceipts-0.0.2/vendor/ossie/README.md +7 -0
- analyticsreceipts-0.0.2/vendor/ossie/ossie-schema.json +374 -0
- analyticsreceipts-0.0.2/vendor/ossie/spec.md +696 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
61e022183498fbb77ef49331277e8bdeb8b5d2cf2eaa3ffe12388bd4a34829fb fixtures/transcripts-v1.json
|
|
2
|
+
fa54230acf3d9b9b949801cbedfcfff6eaaa3a3d05e564848f6b013b6b991370 fixtures/expected-grades-v1.json
|
|
3
|
+
345dd5cd00485668f9f3bcdd2baf2515db0af452ecf07b968a75a62d1b404719 golden/expected-v1.json
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 moamiristat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: analyticsreceipts
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: A four-axis grader with per-answer receipts for analytics agents over a semantic layer.
|
|
5
|
+
Project-URL: Homepage, https://github.com/macamiri/analyticsreceipts
|
|
6
|
+
Project-URL: Source, https://github.com/macamiri/analyticsreceipts
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
License-File: vendor/ossie/LICENSE
|
|
10
|
+
License-File: vendor/ossie/NOTICE
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Requires-Dist: anyio>=4.10; python_version >= '3.14'
|
|
13
|
+
Requires-Dist: anyio>=4.9; python_version < '3.14'
|
|
14
|
+
Requires-Dist: duckdb==1.5.6
|
|
15
|
+
Requires-Dist: jsonschema>=4.20
|
|
16
|
+
Requires-Dist: mcp==2.3.0
|
|
17
|
+
Requires-Dist: pydantic>=2.12.0
|
|
18
|
+
Requires-Dist: pyyaml>=6.0
|
|
19
|
+
Provides-Extra: load
|
|
20
|
+
Requires-Dist: openpyxl>=3.1; extra == 'load'
|
|
21
|
+
Provides-Extra: test
|
|
22
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# analyticsreceipts
|
|
26
|
+
|
|
27
|
+
Per-answer receipts for AI analytics agents, and a grader that records which part of the question an answer got wrong: the metric, a filter, the period or the grain. Every query an agent runs leaves a receipt (the request, the compiled SQL and its parameters, the result and its hash) that you can re-run and check. The grader uses the receipt and the agent's stated reading of the question to separate an answer that is right from one whose query ran fine and answered a different question.
|
|
28
|
+
|
|
29
|
+
> Every eval result below comes from a committed replay or report in this repository.
|
|
30
|
+
|
|
31
|
+
## Who this is for
|
|
32
|
+
|
|
33
|
+
When people ask an AI assistant a business question, like "what was revenue in Germany last November?", the assistant writes a database query, runs it and reports a number. The query can run perfectly and still answer a slightly different question: revenue including refunds, or a month that only has nine days of data. Nothing in the answer warns you. This tool is for teams who run an AI assistant over their company's metric definitions. It asks the assistant a fixed set of questions with known answers. For every answer it records exactly which metric definition was used, which filters and dates were applied, the query that ran and the result it produced. Then it tells you, answer by answer, whether the assistant got the metric, the numbers and the meaning of the question right. It also counts the answers where the query ran fine but answered the wrong question.
|
|
34
|
+
|
|
35
|
+
## What it catches
|
|
36
|
+
|
|
37
|
+
These are the six answers in the fixture set whose query ran but answered a different question. The fixtures are recorded transcripts with planted failures, so these are test cases rather than model behaviour. Each line gives what the receipt and the grader show.
|
|
38
|
+
|
|
39
|
+
| question | what the answer did | what the grader records |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| Revenue in the United Kingdom in November 2011 | queried `units_sold` and reported it as revenue | metric differs: `units_sold`, gold `net_product_revenue`; result table does not match |
|
|
42
|
+
| UK revenue in October 2011 compared with October 2010 | compared with September 2011 instead of October 2010 | spec field `comparison` differs; result table does not match |
|
|
43
|
+
| Revenue in December 2011 compared with November 2011 | right numbers, but did not flag that December 2011 ends on the 9th | spec field `incomplete` differs; trap `incomplete-month` sprung |
|
|
44
|
+
| Total revenue in December 2010 | raw SQL summed both yearly sheets, so a nine-day window counts twice | result table equals the trap's naive reading (`sheet-overlap`) |
|
|
45
|
+
| Orders placed between January and November 2011 | raw SQL counted cancellation and bad-debt invoices as orders | trap `cancellation-invoices-as-orders` sprung |
|
|
46
|
+
| Average revenue per UK customer, January to November 2011 | raw SQL kept revenue with no customer id in the numerator | trap `unattributed-revenue-in-numerator` sprung |
|
|
47
|
+
|
|
48
|
+
In the last three the agent stated the right reading of the question, and the receipt shows the SQL that did something else.
|
|
49
|
+
|
|
50
|
+
These results come from the fixture harness: 26 recorded agent transcripts with planted failures, run through the real server against the real warehouse. No model is involved, so they test the grader and the server.
|
|
51
|
+
|
|
52
|
+
- The grader agreed with grades written down before it ever ran, on all 26 fixtures and on every axis.
|
|
53
|
+
- It found the 6 answers above whose query ran but answered the wrong question, the same count under rule ER-1.
|
|
54
|
+
- The canary, a question with an independently computed answer (18,980,128.63), passed.
|
|
55
|
+
- A deliberately broken compiler that counts the duplicated nine-day window twice failed the canary (19,382,476.01). Exactly one fixture grade changed, the one registered in advance.
|
|
56
|
+
|
|
57
|
+
Reproduce it with the commands in the next section; the output is `reports/m1-fixtures.md`.
|
|
58
|
+
|
|
59
|
+
## What is in the box
|
|
60
|
+
|
|
61
|
+
1. A semantic model of the UCI Online Retail II dataset (CC BY 4.0), written in the Apache Ossie format. It encodes three conventions a naive query gets wrong: the two yearly sheets share an identical nine-day window, some stock codes are fees or adjustments rather than products, and December 2011 ends on the 9th.
|
|
62
|
+
2. A read-only MCP server (stdio, official `mcp` SDK) with five tools: `list_metrics`, `get_metric`, `list_dimensions`, `get_dimension_values` and `query_metric`. It has no raw SQL tool. A request it cannot answer from a governed metric returns a typed refusal (`needs_clarification`, `no_governed_metric` or `invalid_request`). Every `query_metric` result carries a receipt. Concurrent tool calls are safe: each call gets its own database cursor and its own timeout. At most 4 queries run at once, and the read-only database is capped at 1 GB of memory and 2 threads, with no temporary files on disk.
|
|
63
|
+
3. A deterministic grader. No model is used to grade. It scores four axes:
|
|
64
|
+
|
|
65
|
+
| axis | question it answers |
|
|
66
|
+
|---|---|
|
|
67
|
+
| a, metric | did the receipt use the gold metric id and version? (governed arm only) |
|
|
68
|
+
| b, query | does the last successful result table equal the expected table? |
|
|
69
|
+
| c, number | does every number the answer states match an expected value within its rounding? |
|
|
70
|
+
| d, interpretation | does the agent's stated reading (metric, filters, period, grain, ranking) equal gold, field by field? |
|
|
71
|
+
|
|
72
|
+
Each answer lands in exactly one cell. The headline cell, `valid_wrong_question`, means the query ran and its result answers a different question: the table does not match, or a trap was sprung. Rule ER-1 in `grader.py` fixes the meaning of every cell and the only two spec equivalences the grader accepts.
|
|
73
|
+
|
|
74
|
+
4. Two harnesses. `fixtures` replays recorded transcripts with planted failures through the real server, with no model involved. `live` runs a local model through Ollama in two arms: the MCP tools, and plain `describe_tables` plus `run_sql`. `run_sql` takes one statement that starts with SELECT or WITH, and file paths in its error messages are replaced by `<path>`.
|
|
75
|
+
|
|
76
|
+
## Install and first result (about a minute, no model needed)
|
|
77
|
+
|
|
78
|
+
The eval data (the golden set, the fixtures and the replays) lives in the repository, so start from a clone:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
git clone https://github.com/macamiri/analyticsreceipts && cd analyticsreceipts
|
|
82
|
+
python -m venv .venv && . .venv/bin/activate
|
|
83
|
+
pip install ".[load,test]"
|
|
84
|
+
curl -LO https://archive.ics.uci.edu/static/public/502/online+retail+ii.zip
|
|
85
|
+
unzip online+retail+ii.zip
|
|
86
|
+
python -m analyticsreceipts.load online_retail_II.xlsx retail.duckdb
|
|
87
|
+
python -m analyticsreceipts.fixtures --db retail.duckdb --out fixtures-v1.json
|
|
88
|
+
python -m analyticsreceipts.report fixtures fixtures-v1.json --out m1-fixtures.md
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The last command prints `agreement 26/26`. `m1-fixtures.md` shows the 6 wrong-question answers and the canary result.
|
|
92
|
+
|
|
93
|
+
Measured on a clean venv on a 16 GB M1 laptop, on battery in low power mode, timed with `/usr/bin/time` (the clone itself not included):
|
|
94
|
+
|
|
95
|
+
| step | seconds |
|
|
96
|
+
|---|---|
|
|
97
|
+
| venv | 2.7 |
|
|
98
|
+
| pip install | 8.8 |
|
|
99
|
+
| download (45.6 MB) | 15.8 |
|
|
100
|
+
| unzip | 0.5 |
|
|
101
|
+
| load | 42.3 |
|
|
102
|
+
| fixtures | 4.0 |
|
|
103
|
+
| report | 0.1 |
|
|
104
|
+
| total | 74.1 |
|
|
105
|
+
|
|
106
|
+
To serve the model to your own MCP client:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
python -m analyticsreceipts.server --db retail.duckdb # bundled Online Retail II model
|
|
110
|
+
python -m analyticsreceipts.server --db your.duckdb --model your_model.ossie.yaml
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The server and the grader work from a plain `pip install`. The fixture and live harnesses need the eval data: run them from a clone's root, or set `ANALYTICSRECEIPTS_ROOT` to it.
|
|
114
|
+
|
|
115
|
+
## Run the tests
|
|
116
|
+
|
|
117
|
+
From the clone, after the load step above:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
export ANALYTICSRECEIPTS_DB=$PWD/retail.duckdb
|
|
121
|
+
pytest -q tests # 154 passed, 1 skipped (about 10 s)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The one skip is a test that needs a prompt snapshot kept out of the repository. Without `ANALYTICSRECEIPTS_DB`, the 42 tests that need the warehouse are also skipped (112 passed, 43 skipped), and pytest ends with a red "42 WAREHOUSE TESTS DID NOT RUN" banner instead of a clean pass.
|
|
125
|
+
|
|
126
|
+
## Receipt format
|
|
127
|
+
|
|
128
|
+
Every receipt validates against the JSON Schema in `src/analyticsreceipts/receipt.schema.json`, which ships in the wheel:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from analyticsreceipts.receipts import validate_receipt
|
|
132
|
+
validate_receipt(receipt) # raises jsonschema.ValidationError on a malformed receipt
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This is a real receipt, copied unchanged from `replays/fixtures-v1.json` (fixture F01: UK revenue in November 2011). It was recorded as `receipt_version` "1". Receipts written now are version "2": they add a required `compiler` field (`"governed"` for the governed compiler) and no longer include a `non_additive_guard` check, which only ever reported success. The committed replays keep format v1, so the planted-compiler replay labels its receipts `governed_metric` (see `golden/EXPECTED-V1-DIVERGENCE.md`).
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"receipt_version": "1",
|
|
140
|
+
"kind": "governed_metric",
|
|
141
|
+
"request": {
|
|
142
|
+
"metric": "net_product_revenue",
|
|
143
|
+
"time_range": {
|
|
144
|
+
"start": "2011-11-01",
|
|
145
|
+
"end": "2011-11-30"
|
|
146
|
+
},
|
|
147
|
+
"comparison_range": null,
|
|
148
|
+
"grain": null,
|
|
149
|
+
"dimensions": [],
|
|
150
|
+
"filters": [
|
|
151
|
+
{
|
|
152
|
+
"dim": "country",
|
|
153
|
+
"op": "eq",
|
|
154
|
+
"value": "United Kingdom"
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
"ranking": null
|
|
158
|
+
},
|
|
159
|
+
"metric_id": "net_product_revenue",
|
|
160
|
+
"metric_def_version": "1.0.0",
|
|
161
|
+
"semantic_model_sha256": "d1d2cd0a49caf648ad0af6fb68bbd7fe9da80ed67da93ab8f2b7def5ae64e82e",
|
|
162
|
+
"compiled_sql": "WITH lines AS (SELECT * FROM raw_lines WHERE NOT (source_sheet = 'Year 2010-2011' AND invoice_ts < TIMESTAMP '2010-12-10 00:00:00')) SELECT * FROM (SELECT SUM(CASE WHEN (trim(stock_code) SIMILAR TO '[0-9]{5}[A-Za-z]{0,2}') THEN (quantity * price) END) AS net_product_revenue FROM lines WHERE CAST(invoice_ts AS DATE) BETWEEN CAST(? AS DATE) AND CAST(? AS DATE) AND country = ?) AS q LIMIT 1001",
|
|
163
|
+
"params": [
|
|
164
|
+
"2011-11-01",
|
|
165
|
+
"2011-11-30",
|
|
166
|
+
"United Kingdom"
|
|
167
|
+
],
|
|
168
|
+
"compiled_sql_sha256": "22507f3f57a993105c7df573833c5ea95fc138126bad0a8167619c6cfca1cb1c",
|
|
169
|
+
"engine": {
|
|
170
|
+
"name": "duckdb",
|
|
171
|
+
"version": "1.5.6"
|
|
172
|
+
},
|
|
173
|
+
"snapshot": {
|
|
174
|
+
"source_xlsx_sha256": "bcbe73b35f5b7babf197fb0cb983a11f5d9ff929078d4aa53d171b1f2df2e980",
|
|
175
|
+
"max_invoice_ts": "2011-12-09T12:50:00",
|
|
176
|
+
"db_file_sha256": "c2f31a41753548a1ba21d48e152901cf40e1502637d204f1788bad035934310b"
|
|
177
|
+
},
|
|
178
|
+
"result": {
|
|
179
|
+
"columns": [
|
|
180
|
+
"net_product_revenue"
|
|
181
|
+
],
|
|
182
|
+
"rows": [
|
|
183
|
+
[
|
|
184
|
+
1264300.58
|
|
185
|
+
]
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
"result_sha256": "7557a08caf0bac91bd49730d54ae371a490f5a7836e58df85cb995c23b7bd423",
|
|
189
|
+
"row_count": 1,
|
|
190
|
+
"truncated": false,
|
|
191
|
+
"error": null,
|
|
192
|
+
"checks": [
|
|
193
|
+
{
|
|
194
|
+
"name": "time_range_complete",
|
|
195
|
+
"success": true,
|
|
196
|
+
"expected": "end <= last data date 2011-12-09",
|
|
197
|
+
"actual": "2011-11-30",
|
|
198
|
+
"note": null
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"name": "time_range_starts_in_data",
|
|
202
|
+
"success": true,
|
|
203
|
+
"expected": "start >= 2009-12-01",
|
|
204
|
+
"actual": "2011-11-01"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"name": "non_additive_guard",
|
|
208
|
+
"success": true,
|
|
209
|
+
"expected": "ratio/count-distinct computed from components in every group",
|
|
210
|
+
"actual": "additive metric"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
"name": "row_cap",
|
|
214
|
+
"success": true,
|
|
215
|
+
"expected": "<= 1000 rows",
|
|
216
|
+
"actual": 1
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
"uncertainty": {
|
|
220
|
+
"caveats_applied": [
|
|
221
|
+
"C1",
|
|
222
|
+
"C2",
|
|
223
|
+
"C3"
|
|
224
|
+
]
|
|
225
|
+
},
|
|
226
|
+
"run": {
|
|
227
|
+
"request_id": "63e876cddd1b40578fdf5df07e531f71",
|
|
228
|
+
"server_version": "0.0.1",
|
|
229
|
+
"started_at": "2026-10-15T09:32:19.203",
|
|
230
|
+
"elapsed_s": 0.013
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
To re-check a receipt, run its SQL through the same executor, against the warehouse whose file hash is `snapshot.db_file_sha256`. The executor normalises cell values before hashing, so a plain DuckDB query returns `Decimal('1264300.580')` here and hashes differently.
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from analyticsreceipts.execution import Executor, result_sha256
|
|
239
|
+
cols, rows, truncated, err = Executor("retail.duckdb").run(receipt["compiled_sql"], receipt["params"])
|
|
240
|
+
assert result_sha256(cols, rows) == receipt["result_sha256"]
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The frozen pre-registration, `golden/expected-v1.json`, has three SQL hashes (G02, G03, G07) that predate two compiler fixes, with identical result tables, so `golden/expected-v2.json` is canonical (see `golden/EXPECTED-V1-DIVERGENCE.md`).
|
|
244
|
+
|
|
245
|
+
## Validated examples
|
|
246
|
+
|
|
247
|
+
Dataset: UCI Online Retail II, https://archive.ics.uci.edu/dataset/502/online+retail+ii
|
|
248
|
+
Task: 26 recorded transcripts (17 MCP arm, 9 raw SQL arm) with planted failures, graded against 20 frozen golden questions.
|
|
249
|
+
Result: see "What it catches" above. 26 of 26 grades match the pre-registered ones; 6 answers ran and answered the wrong question; the canary passed; the broken compiler failed it.
|
|
250
|
+
Reproducible with: the commands above; the output is `reports/m1-fixtures.md`.
|
|
251
|
+
|
|
252
|
+
## Field note: a small local model (tiny N)
|
|
253
|
+
|
|
254
|
+
This is a field note and makes no benchmark claim. The model was `qwen3:4b` through Ollama, with one run per item and arm on 8 items, on a 16 GB M1 laptop. Each grade count is given twice: "as run" (the grader at the time of the run) and "under ER-1" (the rule adopted after the run; its prediction was not blind, see `reports/m1-er1-regrade.md`).
|
|
255
|
+
|
|
256
|
+
| run | arm | correct (as run / ER-1) | query ran, wrong question (as run / ER-1) | right table, misstated spec (as run / ER-1) |
|
|
257
|
+
|---|---|---|---|---|
|
|
258
|
+
| original prompt | MCP | 0 / 0 | 0 / 0 | 0 / 0 |
|
|
259
|
+
| original prompt | raw SQL | 0 / 0 | 1 / 1 | 0 / 0 |
|
|
260
|
+
| one added prompt sentence | MCP | 1 / 2 | 2 / 1 | 0 / 0 |
|
|
261
|
+
| one added prompt sentence | raw SQL | 0 / 0 | 1 / 0 | 0 / 1 |
|
|
262
|
+
|
|
263
|
+
- With the original prompt, the model made 0 `query_metric` calls in 8 runs.
|
|
264
|
+
- The added sentence told it to call `list_metrics` and then `query_metric`. After that, the MCP arm made 6 such calls.
|
|
265
|
+
- The one answer still counted as "wrong question" under ER-1 dropped the United Kingdom filter.
|
|
266
|
+
- The median MCP arm loop took 1,205 s.
|
|
267
|
+
- N is tiny, so these numbers are counts only; they support no comparison between arms.
|
|
268
|
+
|
|
269
|
+
Reproducible with: `reports/m1-live-lite.md`, `reports/m1-live-lite-v2.md` and `reports/m1-er1-regrade.md`, all generated offline from the committed replays.
|
|
270
|
+
|
|
271
|
+
## Related work
|
|
272
|
+
|
|
273
|
+
- Lee (2026), "Business Truth, not SQL Accuracy" (arXiv:2608.09254), introduces WarehouseReliabilityBench: 400 frozen tasks over two synthetic warehouses, where roughly half the correct responses are a clarification, an abstention or a refusal. It names the failure this tool counts: "queries that execute successfully while returning the wrong business number". Its answerable tasks are scored by "result equivalence against one or more accepted SQL programs", and the rest "against a behaviour contract". Its agent, QueryProof, checks inside the agent that the "filters applied, period applied" match the metric definition before it answers.
|
|
274
|
+
- [slayer-evals](https://github.com/MotleyAI/slayer-evals) asks "Do agents actually reach for SLayer's DSL — and does it beat raw SQL on correctness?". It includes traps "where the straightforward SQL silently returns wrong numbers", and each trial gets two verdicts, "correct" and "single query".
|
|
275
|
+
- Sourati et al. (2026), "Same Context, Different Agents" (arXiv:2610.20959), study semantic layers, orchestration and compaction across 1,514 tasks. Among other findings, they describe "premature answerers that answer without inspecting data", and investigators for whom "the semantic layer stops them from looking". The field note above shows a small local model skipping the tools in the same way.
|
|
276
|
+
- [semantic-layer-mcp-eval](https://github.com/rahulsuravarapu29/semantic-layer-mcp-eval) is a small prototype with "an MCP server that exposes it to an AI agent, and an evaluation with a golden answer set and a gap matrix". Its README notes that "the raw-SQL attempts are hand-written to represent common failure patterns".
|
|
277
|
+
|
|
278
|
+
What this project adds to that work:
|
|
279
|
+
- For each answer it records which field the answer got wrong (metric, filter, period, grain, comparison, the incomplete-period flag) by comparing the agent's stated reading with a golden reading.
|
|
280
|
+
- It keeps a receipt for every query, with the compiled SQL, its parameters and a hash of the result, that anyone can re-run against the same warehouse.
|
|
281
|
+
- It grades through a server that has no raw SQL tool, over your own Ossie semantic model.
|
|
282
|
+
|
|
283
|
+
How the search was done (2026-10-18):
|
|
284
|
+
- Sources: GitHub repository and code search; PyPI's full project-name index; the arXiv API (newest first); and Hacker News. Each source was checked with a query that must return a known result before its other results were read.
|
|
285
|
+
- Not checked: PyPI's search page, which blocked automated requests, so PyPI package descriptions were not read. GitHub code search does not index small new repositories, so code inside such repositories was not searched.
|
|
286
|
+
- Projects created after that date are not covered.
|
|
287
|
+
|
|
288
|
+
## When not to use this
|
|
289
|
+
|
|
290
|
+
- To rank models or compare two agents. The bundled golden set has 20 questions, which supports no comparison. The power of an exact McNemar test at this N is at most 0.196.
|
|
291
|
+
- As a decision rule. The tool reports measurements and receipts. It does not tell you whether to ship.
|
|
292
|
+
- If your metrics live outside an Ossie model. The compiler reads only Ossie YAML.
|
|
293
|
+
- For prose answers with unusual number formats. Axis c parses common forms (thousands separators, k/m/bn, percentages, currency signs) and ignores counts like "top 5" or "9 days". Anything else may be graded wrong.
|
|
294
|
+
|
|
295
|
+
## Where this breaks
|
|
296
|
+
|
|
297
|
+
- Your own model with free-text dimensions. `query_metric` returns result rows to the agent as they are, without truncation and without marking them as untrusted data. `get_dimension_values` does both: it marks values as untrusted and cuts each one to 80 characters. In the bundled model the longest value of any dimension is 20 characters (a country name). In a model that groups by comments or product descriptions, any text stored in the data reaches the agent verbatim, and that is a prompt-injection route. Treat the server as admitted for such models only after you add your own wrapping.
|
|
298
|
+
- Answers written in SQL vocabulary. Free-form specs from the raw SQL arm are never mapped to governed names. A right number stated as `count(DISTINCT customer_id)` lands in `valid_misstated_spec`, and does not count as correct.
|
|
299
|
+
- Small models that skip the tools. With the original prompt, `qwen3:4b` wrote its final answer on the first turn, without calling a tool, in 6 of 8 MCP arm runs. Every answer it gave used the metric name "revenue", which does not exist in the model. The grader reports this correctly, but the receipts then say nothing about the semantic layer.
|
|
300
|
+
- Number formats outside the parser. Axis c reads thousands separators, k/m/bn, percentages and currency signs. An answer in another format can be graded wrong.
|
|
301
|
+
|
|
302
|
+
## Known limitations
|
|
303
|
+
|
|
304
|
+
- The gold expected tables are computed by the compiler under test. For 15 of 20 items they are cross-checked against independent hand-written SQL in `tests/test_reference.py`.
|
|
305
|
+
- The `replays/*.resources.log` files are kept as recorded: once a minute during the live runs they logged the time, battery level, power source, free disk space and swap use of the laptop that ran them.
|
|
306
|
+
|
|
307
|
+
## How it was built
|
|
308
|
+
|
|
309
|
+
Built with help from coding agents. A person reviewed the golden set before it was frozen.
|
|
310
|
+
|
|
311
|
+
## License
|
|
312
|
+
|
|
313
|
+
MIT for the code. The dataset is CC BY 4.0 (UCI, DOI 10.24432/C5CG6D). The Ossie schema is vendored under its own licence; see `vendor/ossie/`.
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# analyticsreceipts
|
|
2
|
+
|
|
3
|
+
Per-answer receipts for AI analytics agents, and a grader that records which part of the question an answer got wrong: the metric, a filter, the period or the grain. Every query an agent runs leaves a receipt (the request, the compiled SQL and its parameters, the result and its hash) that you can re-run and check. The grader uses the receipt and the agent's stated reading of the question to separate an answer that is right from one whose query ran fine and answered a different question.
|
|
4
|
+
|
|
5
|
+
> Every eval result below comes from a committed replay or report in this repository.
|
|
6
|
+
|
|
7
|
+
## Who this is for
|
|
8
|
+
|
|
9
|
+
When people ask an AI assistant a business question, like "what was revenue in Germany last November?", the assistant writes a database query, runs it and reports a number. The query can run perfectly and still answer a slightly different question: revenue including refunds, or a month that only has nine days of data. Nothing in the answer warns you. This tool is for teams who run an AI assistant over their company's metric definitions. It asks the assistant a fixed set of questions with known answers. For every answer it records exactly which metric definition was used, which filters and dates were applied, the query that ran and the result it produced. Then it tells you, answer by answer, whether the assistant got the metric, the numbers and the meaning of the question right. It also counts the answers where the query ran fine but answered the wrong question.
|
|
10
|
+
|
|
11
|
+
## What it catches
|
|
12
|
+
|
|
13
|
+
These are the six answers in the fixture set whose query ran but answered a different question. The fixtures are recorded transcripts with planted failures, so these are test cases rather than model behaviour. Each line gives what the receipt and the grader show.
|
|
14
|
+
|
|
15
|
+
| question | what the answer did | what the grader records |
|
|
16
|
+
|---|---|---|
|
|
17
|
+
| Revenue in the United Kingdom in November 2011 | queried `units_sold` and reported it as revenue | metric differs: `units_sold`, gold `net_product_revenue`; result table does not match |
|
|
18
|
+
| UK revenue in October 2011 compared with October 2010 | compared with September 2011 instead of October 2010 | spec field `comparison` differs; result table does not match |
|
|
19
|
+
| Revenue in December 2011 compared with November 2011 | right numbers, but did not flag that December 2011 ends on the 9th | spec field `incomplete` differs; trap `incomplete-month` sprung |
|
|
20
|
+
| Total revenue in December 2010 | raw SQL summed both yearly sheets, so a nine-day window counts twice | result table equals the trap's naive reading (`sheet-overlap`) |
|
|
21
|
+
| Orders placed between January and November 2011 | raw SQL counted cancellation and bad-debt invoices as orders | trap `cancellation-invoices-as-orders` sprung |
|
|
22
|
+
| Average revenue per UK customer, January to November 2011 | raw SQL kept revenue with no customer id in the numerator | trap `unattributed-revenue-in-numerator` sprung |
|
|
23
|
+
|
|
24
|
+
In the last three the agent stated the right reading of the question, and the receipt shows the SQL that did something else.
|
|
25
|
+
|
|
26
|
+
These results come from the fixture harness: 26 recorded agent transcripts with planted failures, run through the real server against the real warehouse. No model is involved, so they test the grader and the server.
|
|
27
|
+
|
|
28
|
+
- The grader agreed with grades written down before it ever ran, on all 26 fixtures and on every axis.
|
|
29
|
+
- It found the 6 answers above whose query ran but answered the wrong question, the same count under rule ER-1.
|
|
30
|
+
- The canary, a question with an independently computed answer (18,980,128.63), passed.
|
|
31
|
+
- A deliberately broken compiler that counts the duplicated nine-day window twice failed the canary (19,382,476.01). Exactly one fixture grade changed, the one registered in advance.
|
|
32
|
+
|
|
33
|
+
Reproduce it with the commands in the next section; the output is `reports/m1-fixtures.md`.
|
|
34
|
+
|
|
35
|
+
## What is in the box
|
|
36
|
+
|
|
37
|
+
1. A semantic model of the UCI Online Retail II dataset (CC BY 4.0), written in the Apache Ossie format. It encodes three conventions a naive query gets wrong: the two yearly sheets share an identical nine-day window, some stock codes are fees or adjustments rather than products, and December 2011 ends on the 9th.
|
|
38
|
+
2. A read-only MCP server (stdio, official `mcp` SDK) with five tools: `list_metrics`, `get_metric`, `list_dimensions`, `get_dimension_values` and `query_metric`. It has no raw SQL tool. A request it cannot answer from a governed metric returns a typed refusal (`needs_clarification`, `no_governed_metric` or `invalid_request`). Every `query_metric` result carries a receipt. Concurrent tool calls are safe: each call gets its own database cursor and its own timeout. At most 4 queries run at once, and the read-only database is capped at 1 GB of memory and 2 threads, with no temporary files on disk.
|
|
39
|
+
3. A deterministic grader. No model is used to grade. It scores four axes:
|
|
40
|
+
|
|
41
|
+
| axis | question it answers |
|
|
42
|
+
|---|---|
|
|
43
|
+
| a, metric | did the receipt use the gold metric id and version? (governed arm only) |
|
|
44
|
+
| b, query | does the last successful result table equal the expected table? |
|
|
45
|
+
| c, number | does every number the answer states match an expected value within its rounding? |
|
|
46
|
+
| d, interpretation | does the agent's stated reading (metric, filters, period, grain, ranking) equal gold, field by field? |
|
|
47
|
+
|
|
48
|
+
Each answer lands in exactly one cell. The headline cell, `valid_wrong_question`, means the query ran and its result answers a different question: the table does not match, or a trap was sprung. Rule ER-1 in `grader.py` fixes the meaning of every cell and the only two spec equivalences the grader accepts.
|
|
49
|
+
|
|
50
|
+
4. Two harnesses. `fixtures` replays recorded transcripts with planted failures through the real server, with no model involved. `live` runs a local model through Ollama in two arms: the MCP tools, and plain `describe_tables` plus `run_sql`. `run_sql` takes one statement that starts with SELECT or WITH, and file paths in its error messages are replaced by `<path>`.
|
|
51
|
+
|
|
52
|
+
## Install and first result (about a minute, no model needed)
|
|
53
|
+
|
|
54
|
+
The eval data (the golden set, the fixtures and the replays) lives in the repository, so start from a clone:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/macamiri/analyticsreceipts && cd analyticsreceipts
|
|
58
|
+
python -m venv .venv && . .venv/bin/activate
|
|
59
|
+
pip install ".[load,test]"
|
|
60
|
+
curl -LO https://archive.ics.uci.edu/static/public/502/online+retail+ii.zip
|
|
61
|
+
unzip online+retail+ii.zip
|
|
62
|
+
python -m analyticsreceipts.load online_retail_II.xlsx retail.duckdb
|
|
63
|
+
python -m analyticsreceipts.fixtures --db retail.duckdb --out fixtures-v1.json
|
|
64
|
+
python -m analyticsreceipts.report fixtures fixtures-v1.json --out m1-fixtures.md
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The last command prints `agreement 26/26`. `m1-fixtures.md` shows the 6 wrong-question answers and the canary result.
|
|
68
|
+
|
|
69
|
+
Measured on a clean venv on a 16 GB M1 laptop, on battery in low power mode, timed with `/usr/bin/time` (the clone itself not included):
|
|
70
|
+
|
|
71
|
+
| step | seconds |
|
|
72
|
+
|---|---|
|
|
73
|
+
| venv | 2.7 |
|
|
74
|
+
| pip install | 8.8 |
|
|
75
|
+
| download (45.6 MB) | 15.8 |
|
|
76
|
+
| unzip | 0.5 |
|
|
77
|
+
| load | 42.3 |
|
|
78
|
+
| fixtures | 4.0 |
|
|
79
|
+
| report | 0.1 |
|
|
80
|
+
| total | 74.1 |
|
|
81
|
+
|
|
82
|
+
To serve the model to your own MCP client:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
python -m analyticsreceipts.server --db retail.duckdb # bundled Online Retail II model
|
|
86
|
+
python -m analyticsreceipts.server --db your.duckdb --model your_model.ossie.yaml
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The server and the grader work from a plain `pip install`. The fixture and live harnesses need the eval data: run them from a clone's root, or set `ANALYTICSRECEIPTS_ROOT` to it.
|
|
90
|
+
|
|
91
|
+
## Run the tests
|
|
92
|
+
|
|
93
|
+
From the clone, after the load step above:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
export ANALYTICSRECEIPTS_DB=$PWD/retail.duckdb
|
|
97
|
+
pytest -q tests # 154 passed, 1 skipped (about 10 s)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The one skip is a test that needs a prompt snapshot kept out of the repository. Without `ANALYTICSRECEIPTS_DB`, the 42 tests that need the warehouse are also skipped (112 passed, 43 skipped), and pytest ends with a red "42 WAREHOUSE TESTS DID NOT RUN" banner instead of a clean pass.
|
|
101
|
+
|
|
102
|
+
## Receipt format
|
|
103
|
+
|
|
104
|
+
Every receipt validates against the JSON Schema in `src/analyticsreceipts/receipt.schema.json`, which ships in the wheel:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from analyticsreceipts.receipts import validate_receipt
|
|
108
|
+
validate_receipt(receipt) # raises jsonschema.ValidationError on a malformed receipt
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This is a real receipt, copied unchanged from `replays/fixtures-v1.json` (fixture F01: UK revenue in November 2011). It was recorded as `receipt_version` "1". Receipts written now are version "2": they add a required `compiler` field (`"governed"` for the governed compiler) and no longer include a `non_additive_guard` check, which only ever reported success. The committed replays keep format v1, so the planted-compiler replay labels its receipts `governed_metric` (see `golden/EXPECTED-V1-DIVERGENCE.md`).
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"receipt_version": "1",
|
|
116
|
+
"kind": "governed_metric",
|
|
117
|
+
"request": {
|
|
118
|
+
"metric": "net_product_revenue",
|
|
119
|
+
"time_range": {
|
|
120
|
+
"start": "2011-11-01",
|
|
121
|
+
"end": "2011-11-30"
|
|
122
|
+
},
|
|
123
|
+
"comparison_range": null,
|
|
124
|
+
"grain": null,
|
|
125
|
+
"dimensions": [],
|
|
126
|
+
"filters": [
|
|
127
|
+
{
|
|
128
|
+
"dim": "country",
|
|
129
|
+
"op": "eq",
|
|
130
|
+
"value": "United Kingdom"
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
"ranking": null
|
|
134
|
+
},
|
|
135
|
+
"metric_id": "net_product_revenue",
|
|
136
|
+
"metric_def_version": "1.0.0",
|
|
137
|
+
"semantic_model_sha256": "d1d2cd0a49caf648ad0af6fb68bbd7fe9da80ed67da93ab8f2b7def5ae64e82e",
|
|
138
|
+
"compiled_sql": "WITH lines AS (SELECT * FROM raw_lines WHERE NOT (source_sheet = 'Year 2010-2011' AND invoice_ts < TIMESTAMP '2010-12-10 00:00:00')) SELECT * FROM (SELECT SUM(CASE WHEN (trim(stock_code) SIMILAR TO '[0-9]{5}[A-Za-z]{0,2}') THEN (quantity * price) END) AS net_product_revenue FROM lines WHERE CAST(invoice_ts AS DATE) BETWEEN CAST(? AS DATE) AND CAST(? AS DATE) AND country = ?) AS q LIMIT 1001",
|
|
139
|
+
"params": [
|
|
140
|
+
"2011-11-01",
|
|
141
|
+
"2011-11-30",
|
|
142
|
+
"United Kingdom"
|
|
143
|
+
],
|
|
144
|
+
"compiled_sql_sha256": "22507f3f57a993105c7df573833c5ea95fc138126bad0a8167619c6cfca1cb1c",
|
|
145
|
+
"engine": {
|
|
146
|
+
"name": "duckdb",
|
|
147
|
+
"version": "1.5.6"
|
|
148
|
+
},
|
|
149
|
+
"snapshot": {
|
|
150
|
+
"source_xlsx_sha256": "bcbe73b35f5b7babf197fb0cb983a11f5d9ff929078d4aa53d171b1f2df2e980",
|
|
151
|
+
"max_invoice_ts": "2011-12-09T12:50:00",
|
|
152
|
+
"db_file_sha256": "c2f31a41753548a1ba21d48e152901cf40e1502637d204f1788bad035934310b"
|
|
153
|
+
},
|
|
154
|
+
"result": {
|
|
155
|
+
"columns": [
|
|
156
|
+
"net_product_revenue"
|
|
157
|
+
],
|
|
158
|
+
"rows": [
|
|
159
|
+
[
|
|
160
|
+
1264300.58
|
|
161
|
+
]
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
"result_sha256": "7557a08caf0bac91bd49730d54ae371a490f5a7836e58df85cb995c23b7bd423",
|
|
165
|
+
"row_count": 1,
|
|
166
|
+
"truncated": false,
|
|
167
|
+
"error": null,
|
|
168
|
+
"checks": [
|
|
169
|
+
{
|
|
170
|
+
"name": "time_range_complete",
|
|
171
|
+
"success": true,
|
|
172
|
+
"expected": "end <= last data date 2011-12-09",
|
|
173
|
+
"actual": "2011-11-30",
|
|
174
|
+
"note": null
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "time_range_starts_in_data",
|
|
178
|
+
"success": true,
|
|
179
|
+
"expected": "start >= 2009-12-01",
|
|
180
|
+
"actual": "2011-11-01"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "non_additive_guard",
|
|
184
|
+
"success": true,
|
|
185
|
+
"expected": "ratio/count-distinct computed from components in every group",
|
|
186
|
+
"actual": "additive metric"
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"name": "row_cap",
|
|
190
|
+
"success": true,
|
|
191
|
+
"expected": "<= 1000 rows",
|
|
192
|
+
"actual": 1
|
|
193
|
+
}
|
|
194
|
+
],
|
|
195
|
+
"uncertainty": {
|
|
196
|
+
"caveats_applied": [
|
|
197
|
+
"C1",
|
|
198
|
+
"C2",
|
|
199
|
+
"C3"
|
|
200
|
+
]
|
|
201
|
+
},
|
|
202
|
+
"run": {
|
|
203
|
+
"request_id": "63e876cddd1b40578fdf5df07e531f71",
|
|
204
|
+
"server_version": "0.0.1",
|
|
205
|
+
"started_at": "2026-10-15T09:32:19.203",
|
|
206
|
+
"elapsed_s": 0.013
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
To re-check a receipt, run its SQL through the same executor, against the warehouse whose file hash is `snapshot.db_file_sha256`. The executor normalises cell values before hashing, so a plain DuckDB query returns `Decimal('1264300.580')` here and hashes differently.
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from analyticsreceipts.execution import Executor, result_sha256
|
|
215
|
+
cols, rows, truncated, err = Executor("retail.duckdb").run(receipt["compiled_sql"], receipt["params"])
|
|
216
|
+
assert result_sha256(cols, rows) == receipt["result_sha256"]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The frozen pre-registration, `golden/expected-v1.json`, has three SQL hashes (G02, G03, G07) that predate two compiler fixes, with identical result tables, so `golden/expected-v2.json` is canonical (see `golden/EXPECTED-V1-DIVERGENCE.md`).
|
|
220
|
+
|
|
221
|
+
## Validated examples
|
|
222
|
+
|
|
223
|
+
Dataset: UCI Online Retail II, https://archive.ics.uci.edu/dataset/502/online+retail+ii
|
|
224
|
+
Task: 26 recorded transcripts (17 MCP arm, 9 raw SQL arm) with planted failures, graded against 20 frozen golden questions.
|
|
225
|
+
Result: see "What it catches" above. 26 of 26 grades match the pre-registered ones; 6 answers ran and answered the wrong question; the canary passed; the broken compiler failed it.
|
|
226
|
+
Reproducible with: the commands above; the output is `reports/m1-fixtures.md`.
|
|
227
|
+
|
|
228
|
+
## Field note: a small local model (tiny N)
|
|
229
|
+
|
|
230
|
+
This is a field note and makes no benchmark claim. The model was `qwen3:4b` through Ollama, with one run per item and arm on 8 items, on a 16 GB M1 laptop. Each grade count is given twice: "as run" (the grader at the time of the run) and "under ER-1" (the rule adopted after the run; its prediction was not blind, see `reports/m1-er1-regrade.md`).
|
|
231
|
+
|
|
232
|
+
| run | arm | correct (as run / ER-1) | query ran, wrong question (as run / ER-1) | right table, misstated spec (as run / ER-1) |
|
|
233
|
+
|---|---|---|---|---|
|
|
234
|
+
| original prompt | MCP | 0 / 0 | 0 / 0 | 0 / 0 |
|
|
235
|
+
| original prompt | raw SQL | 0 / 0 | 1 / 1 | 0 / 0 |
|
|
236
|
+
| one added prompt sentence | MCP | 1 / 2 | 2 / 1 | 0 / 0 |
|
|
237
|
+
| one added prompt sentence | raw SQL | 0 / 0 | 1 / 0 | 0 / 1 |
|
|
238
|
+
|
|
239
|
+
- With the original prompt, the model made 0 `query_metric` calls in 8 runs.
|
|
240
|
+
- The added sentence told it to call `list_metrics` and then `query_metric`. After that, the MCP arm made 6 such calls.
|
|
241
|
+
- The one answer still counted as "wrong question" under ER-1 dropped the United Kingdom filter.
|
|
242
|
+
- The median MCP arm loop took 1,205 s.
|
|
243
|
+
- N is tiny, so these numbers are counts only; they support no comparison between arms.
|
|
244
|
+
|
|
245
|
+
Reproducible with: `reports/m1-live-lite.md`, `reports/m1-live-lite-v2.md` and `reports/m1-er1-regrade.md`, all generated offline from the committed replays.
|
|
246
|
+
|
|
247
|
+
## Related work
|
|
248
|
+
|
|
249
|
+
- Lee (2026), "Business Truth, not SQL Accuracy" (arXiv:2608.09254), introduces WarehouseReliabilityBench: 400 frozen tasks over two synthetic warehouses, where roughly half the correct responses are a clarification, an abstention or a refusal. It names the failure this tool counts: "queries that execute successfully while returning the wrong business number". Its answerable tasks are scored by "result equivalence against one or more accepted SQL programs", and the rest "against a behaviour contract". Its agent, QueryProof, checks inside the agent that the "filters applied, period applied" match the metric definition before it answers.
|
|
250
|
+
- [slayer-evals](https://github.com/MotleyAI/slayer-evals) asks "Do agents actually reach for SLayer's DSL — and does it beat raw SQL on correctness?". It includes traps "where the straightforward SQL silently returns wrong numbers", and each trial gets two verdicts, "correct" and "single query".
|
|
251
|
+
- Sourati et al. (2026), "Same Context, Different Agents" (arXiv:2610.20959), study semantic layers, orchestration and compaction across 1,514 tasks. Among other findings, they describe "premature answerers that answer without inspecting data", and investigators for whom "the semantic layer stops them from looking". The field note above shows a small local model skipping the tools in the same way.
|
|
252
|
+
- [semantic-layer-mcp-eval](https://github.com/rahulsuravarapu29/semantic-layer-mcp-eval) is a small prototype with "an MCP server that exposes it to an AI agent, and an evaluation with a golden answer set and a gap matrix". Its README notes that "the raw-SQL attempts are hand-written to represent common failure patterns".
|
|
253
|
+
|
|
254
|
+
What this project adds to that work:
|
|
255
|
+
- For each answer it records which field the answer got wrong (metric, filter, period, grain, comparison, the incomplete-period flag) by comparing the agent's stated reading with a golden reading.
|
|
256
|
+
- It keeps a receipt for every query, with the compiled SQL, its parameters and a hash of the result, that anyone can re-run against the same warehouse.
|
|
257
|
+
- It grades through a server that has no raw SQL tool, over your own Ossie semantic model.
|
|
258
|
+
|
|
259
|
+
How the search was done (2026-10-18):
|
|
260
|
+
- Sources: GitHub repository and code search; PyPI's full project-name index; the arXiv API (newest first); and Hacker News. Each source was checked with a query that must return a known result before its other results were read.
|
|
261
|
+
- Not checked: PyPI's search page, which blocked automated requests, so PyPI package descriptions were not read. GitHub code search does not index small new repositories, so code inside such repositories was not searched.
|
|
262
|
+
- Projects created after that date are not covered.
|
|
263
|
+
|
|
264
|
+
## When not to use this
|
|
265
|
+
|
|
266
|
+
- To rank models or compare two agents. The bundled golden set has 20 questions, which supports no comparison. The power of an exact McNemar test at this N is at most 0.196.
|
|
267
|
+
- As a decision rule. The tool reports measurements and receipts. It does not tell you whether to ship.
|
|
268
|
+
- If your metrics live outside an Ossie model. The compiler reads only Ossie YAML.
|
|
269
|
+
- For prose answers with unusual number formats. Axis c parses common forms (thousands separators, k/m/bn, percentages, currency signs) and ignores counts like "top 5" or "9 days". Anything else may be graded wrong.
|
|
270
|
+
|
|
271
|
+
## Where this breaks
|
|
272
|
+
|
|
273
|
+
- Your own model with free-text dimensions. `query_metric` returns result rows to the agent as they are, without truncation and without marking them as untrusted data. `get_dimension_values` does both: it marks values as untrusted and cuts each one to 80 characters. In the bundled model the longest value of any dimension is 20 characters (a country name). In a model that groups by comments or product descriptions, any text stored in the data reaches the agent verbatim, and that is a prompt-injection route. Treat the server as admitted for such models only after you add your own wrapping.
|
|
274
|
+
- Answers written in SQL vocabulary. Free-form specs from the raw SQL arm are never mapped to governed names. A right number stated as `count(DISTINCT customer_id)` lands in `valid_misstated_spec`, and does not count as correct.
|
|
275
|
+
- Small models that skip the tools. With the original prompt, `qwen3:4b` wrote its final answer on the first turn, without calling a tool, in 6 of 8 MCP arm runs. Every answer it gave used the metric name "revenue", which does not exist in the model. The grader reports this correctly, but the receipts then say nothing about the semantic layer.
|
|
276
|
+
- Number formats outside the parser. Axis c reads thousands separators, k/m/bn, percentages and currency signs. An answer in another format can be graded wrong.
|
|
277
|
+
|
|
278
|
+
## Known limitations
|
|
279
|
+
|
|
280
|
+
- The gold expected tables are computed by the compiler under test. For 15 of 20 items they are cross-checked against independent hand-written SQL in `tests/test_reference.py`.
|
|
281
|
+
- The `replays/*.resources.log` files are kept as recorded: once a minute during the live runs they logged the time, battery level, power source, free disk space and swap use of the laptop that ran them.
|
|
282
|
+
|
|
283
|
+
## How it was built
|
|
284
|
+
|
|
285
|
+
Built with help from coding agents. A person reviewed the golden set before it was frozen.
|
|
286
|
+
|
|
287
|
+
## License
|
|
288
|
+
|
|
289
|
+
MIT for the code. The dataset is CC BY 4.0 (UCI, DOI 10.24432/C5CG6D). The Ossie schema is vendored under its own licence; see `vendor/ossie/`.
|