Files as Data Source
"Chat with your spreadsheet" is the #1 demo in GenAI — and a minefield of silent wrong answers. Four patterns, from naive to production-grade.
▶ Watch this reelWhat you'll learn
- Chat with CSV/Excel
- DuckDB + LLM
- Code-interpreter pattern
- Tabular RAG
Remember this
- Never let the model compute or read raw tabular data at scale — engines compute, models interpret
- DuckDB turns files into queryable tables with zero import; code-interpreter covers plots/transforms; route per question
- Tabular RAG keeps whole tables (with captions) as chunks — split rows are unretrievable meaning
Why naive chat-with-CSV fails
- Silent sampling (context limits) · arithmetic drift · structure flattening (merged cells, totals rows).
- State coverage explicitly: 'analyzed 48,211 rows'.
DuckDB + LLM
CREATE VIEW ... SELECT * FROM 'file.parquet'— zero-copy, in-process.- DESCRIBE + sampled values into the prompt; model writes SQL, engine executes (GA-18 fences).
Code-interpreter pattern
- For plots/transforms/multi-file logic SQL can't express.
- Same sandbox rules as AG-03; charts as linked artifacts.
- Router per question: 'how many/sum' → SQL · 'show/plot/combine' → code.
Tabular RAG
- Whole table + caption + section per chunk; caption-first indexing; key columns optionally mirrored to DuckDB.
Principle: models interpret, engines compute.
Code: The tri-router: one file, three engines
async def answer_over_file(question: str, file_path: str) -> Answer:
# 0 · one-time setup: file becomes all three things
con = duckdb.connect()
con.execute(f"CREATE VIEW data AS SELECT * FROM '{file_path}'")
workspace = sandbox.put(file_path) # code-interpreter
chunks = index_tables_with_captions(file_path) # tabular RAG
# 1 · cheap router classifies the question
route = classify(question, classes=["aggregate", "transform", "semantic"])
if route == "aggregate": # DuckDB
sql = llm(SQL_PROMPT, schema=describe(con), q=question)
rows = con.execute(f"SELECT * FROM ({sql}) _q LIMIT 500").fetchdf()
return llm(ANSWER_PROMPT, q=question, exact_results=rows)
if route == "transform": # code interpreter
code = llm(CODE_PROMPT, q=question, files=[workspace.name])
out = await sandbox.run(code, timeout=60)
return Answer(text=out.stdout, artifacts=out.files)
return rag_answer(question, store=chunks) # semantic → tabular RAG
# Rule of the whole reel: models interpret, engines compute.