Airbnb Data Analyst Agent
A reusable multi-agent analytics copilot that plans, writes, validates, charts, and narrates SQL analysis — every number cited back to source rows.
- Org
- Columbia · Agentic AI for Analytics
- Role
- Designed & built solo
- Period
- 2026
- Status
- live
- Stack
- FastAPI · LangChain · DuckDB / Postgres / Snowflake · OpenAI function calling · matplotlib · pytest
- Links
- Live demo ↗GitHub ↗
The problem
Analysts spend hours translating business questions into SQL, pulling data, checking it, and reformatting the answer into something a stakeholder can consume. Most LLM "text-to-SQL" demos hallucinate schema, fail silently on bad queries, or skip the last-mile steps that actually matter: a chart, a citation, a sanity check.
The live Airbnb implementation is the demonstration surface for a reusable analytics copilot: it decomposes the question, writes SQL that actually runs, validates the result before showing it, plots the data, and cites the rows it used. The same adapter contract supports DuckDB, Postgres, and Snowflake.
The constraints
- Text-to-SQL models hallucinate column names, misuse joins, and produce queries that run but return the wrong answer
- Single-prompt approaches have no way to recover from tool errors or mid-query course corrections
- Charts need semantic intent ("show trend over time"), not just "make a chart of this dataframe"
- Auditability is table stakes in any real analytics context — every number shown must trace back to source rows
- Latency and cost must stay bounded even as the agent retries and self-critiques
The loop
Instead of one prompt doing everything badly, five specialized agents each own one contract:
- Planner — decomposes the natural-language question into a plan of sub-queries and tool calls
- SQL Agent — writes SQL against the live warehouse schema, with access to
db.schema()anddb.query(sql) - Validator — dry-runs the SQL first, audits row counts, nulls, and types; self-critiques when the result doesn't match the planner's stated intent; retries on tool error (×3, exponential backoff)
- Chart Agent — calls
plot.auto(df, intent)to render the right visualization for the question, not just a visualization - Narrator — composes the final answer, citing every number back to specific source rows
All five communicate over a typed message bus, so every intermediate step is inspectable and replayable — the trace below is rendered from exactly that bus.
The build
Every capability is a typed tool with an explicit contract — the agents can only act through these:
| Tool | Signature |
|---|---|
db.schema() | → Table[] |
db.query(sql) | → DataFrame |
df.describe(df) | → Stats |
plot.auto(df, intent) | → PNG |
web.search(q) | → Link[] |
The warehouse layer is pluggable: DuckDB for the live demo (NYC Airbnb data), with Postgres and Snowflake adapters behind the same interface. FastAPI serves the API; the whole system is exercised by a pytest eval harness on every commit.
Evidence
Guardrails are only real if you can watch them fire. Below are recorded runs from the system — step through them. The first run includes a hallucinated column name, the database error it caused, and the validator catching and fixing it.
What keeps the system honest in production:
- SQL dry-run + row-count sanity check before any query result is trusted
- Null / type audit before any chart is rendered
- Self-critique when the SQL result doesn't match the planner's stated intent
- Golden Q/A regression suite — every commit runs canonical questions and checks the answers
- Per-query latency and cost breakdown, so behavior stays measurable as prompts evolve
❯ Do superhosts get better review scores than other hosts?
Compare avg review_scores_rating for superhosts vs non-superhosts; check statistical significance before claiming a difference.
SELECT superhost_flag, AVG(review_scores_rating) AS avg_rating,
COUNT(*) AS n FROM listings
WHERE review_scores_rating IS NOT NULL
GROUP BY superhost_flag;ERROR: column "superhost_flag" does not exist
Tool error caught. Re-read schema via db.schema() — column is host_is_superhost. Retrying (attempt 1/3, backoff 0.5s).
SELECT host_is_superhost, AVG(review_scores_rating) AS avg_rating,
COUNT(*) AS n FROM listings
WHERE review_scores_rating IS NOT NULL
GROUP BY host_is_superhost;returned 2 rows · n(true)=18,402 · n(false)=26,117
Null audit OK · sample sizes healthy · running Welch's t-test on the two groups.
Rendered bar chart — superhost 4.89 vs other 4.61 (0–5 scale).
Yes. Superhosts average 4.89 vs 4.61 (Δ=0.28, p<0.001, Welch's t-test). Source: listings.review_scores_rating × listings.host_is_superhost, n=44,519.
run complete · 11.4s end-to-end · $0.014 · 9 bus messages
Impact
- Handles multi-step analytics questions end-to-end with auditable traces
- Every answer is grounded — users can drill into the exact rows the agent cited
- Regression evals plus per-query latency/cost tracking keep production behavior measurable
- Designed to plug into any warehouse with a SQL-compatible adapter
Questions about this project? The chat answers from these case studies and links back to them.