Chat with a CSV, but pin a known-answer guardrail so a wrong query cannot pass
Ask a CSV or dataframe questions in plain English with PandasAI, but wrap it in a deterministic known-answer check so a confident-but-wrong generated query is caught instead of trusted.
Run this workflow
CI-verified, 2/2 fixtures passing.
Build this with your agent
One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.
Intended Use
Anyone who asks a CSV or dataframe questions in English and needs to know the answer is right. CI loads a fixed CSV fixture and asserts a deterministic aggregation (average revenue by region) equals the known answer, proving the check that would catch a wrong query actually runs. No install, no key, no model call. The df.chat() step, where the model writes and runs the Python, is fenced.
Not for
- Trusting the model's answer on faith, the whole point is to compare df.chat() against the known number and reject a mismatch
- Python 3.12+, PandasAI pins 3.8 through 3.11 so it will not install on newer runtimes
- Running untrusted generated Python unsandboxed, df.chat() executes model-written code, use its Docker sandbox (pip install pandasai-docker) for data you did not author
- Building a product on the `ee/` enterprise directory assuming MIT, that directory carries its own license
The Stack
Tested Against
github.com/sinaptik-ai/pandas-ai v3.0.0 (2026-07)python@3.11node@20Side effects & data flow
- Network
- none, local only
- Writes
- ./companies.csv, ./guardrail.py
- Credentials
- none required
Prerequisites
- Python 3.8-3.11 to actually run pandasai (it does not install on 3.12+)
- An LLM provider key for the fenced df.chat() step (via pandasai-litellm)
Steps
- 1
Load a fixed CSV and assert the aggregation matches the known answer
This is the guardrail: a small CSV whose average-revenue-by-region you already know (EMEA=200, AMER=400, APAC=150), computed with pure stdlib so there is no model in the loop. CI asserts the aggregation equals that known answer. In real use you run df.chat() and check its reply against these same numbers, a reply that differs is a wrong query. The df.chat() call itself is fenced.
cat > companies.csv <<'CSV' name,region,revenue Acme,EMEA,100 Globex,EMEA,300 Initech,AMER,200 Umbrella,AMER,600 Hooli,APAC,150 CSV python3 - <<'PY' import csv, sys by_region = {} with open("companies.csv", newline="") as f: for r in csv.DictReader(f): by_region.setdefault(r["region"], []).append(float(r["revenue"])) avg = {reg: sum(v) / len(v) for reg, v in by_region.items()} golden = {"AMER": 400.0, "APAC": 150.0, "EMEA": 200.0} if avg != golden: print("BAD: aggregation drifted from the known answer; got " + str(avg)) sys.exit(1) parts = ", ".join(reg + "=" + str(avg[reg]) for reg in sorted(avg)) print("guardrail OK: avg revenue by region " + parts + " (matches the known answer); a df.chat() reply that differs is a wrong query and must be rejected") PY - 2
Wire the guardrail around df.chat() (the model step, not checked by CI)
pip install pandasai pandasai-litellm, configure LiteLLM with your model and key, load the same CSV with pai.read_csv, then df.chat('What is the average revenue by region?'). Compare the reply against the golden numbers above and reject on mismatch. For data you did not author, run it inside the Docker sandbox (pip install pandasai-docker), since df.chat generates and executes Python. This step calls a model and runs generated code, so it is fenced.
Eval, 2 fixtures
Last passed: verified todayguardrail-okcontainstimeout 30s · max $0Expected:
guardrail OK: avg revenue by region AMER=400.0, APAC=150.0, EMEA=200.0 (matches the known answer); a df.chat() reply that differs is a wrong query and must be rejectedclean-exitexit_codetimeout 30s · max $0Expected:
0
Results
PandasAI lets you point at a CSV, an Excel export, or a dataframe and ask questions in English, getting back numbers or charts. The catch the whole roundup is built around: it answers by generating and running Python, and a wrong query looks exactly like a right one. The fix is not to trust the reply, it is to keep an objective check the model cannot fake, a total you already know, next to it. This recipe makes that check the CI: a fixed CSV, a deterministic aggregation, and the known number it must equal. Two sharp gotchas, PandasAI pins Python to 3.8 through 3.11 (it will not install on 3.12+), and the `ee/` enterprise directory is not MIT.
Did this work for you?
Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.
Related workflows
- Advisor pattern: cap how often the expensive model gets called, and catch drift
- Route through a gateway with a tested open-weights fallback
- ReMe pattern: define prospective memory as a schedule your agent can tick off
- Grind a huge one-time job overnight on a free tier's tiny rate limit
- Teach OpenCode Go your weekly chore once, then run it in minutes
- Let a free model triage your reading: one-line summary + reply flag
Liked this workflow?
Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).