# NHTSA Complaints ยท DuckDB + RAG Chatbot Build your **own dataset** from the live [NHTSA](https://www.nhtsa.gov/) vehicle-complaints API, store it in **DuckDB** (with a vector index), and chat with it in natural language. An LLM **router** decides per question whether to answer with **SQL** (exact filters, counts, VIN lookup) or a **semantic vector search** (meaning-based retrieval), then streams a natural-language answer. > **Model strategy:** **Primary = HuggingFace**, secondary fallbacks = **OpenAI** or **Google**. > This applies to both the chat LLM and the embedding model (see [Models](#models)). --- ## Features - ๐Ÿš— **Dynamic cascading dropdowns** โ€” pick a *Make* โ†’ its *Models* load automatically โ†’ *Model year* (2015โ€“2026). Data from the NHTSA vPIC API. - ๐Ÿ—„๏ธ **DuckDB storage** โ€” complaints stored in a single embedded DuckDB file (no server). Idempotent upsert by `odiNumber`. - ๐Ÿ”Ž **Vector search (RAG)** โ€” every record is embedded into an `embedding` column; semantic search uses a **HNSW ANN index** (DuckDB `vss` extension) with cosine similarity. - ๐Ÿง  **LangGraph router** โ€” an intent classifier routes each question: `SQL` vs `semantic`. - ๐Ÿ’ฌ **Conversational chatbot** โ€” persistent history, follow-up memory, streamed answers, tables for list results. - ๐Ÿ“‹ **Sample-question templates** โ€” one-click example questions (right panel). - โš™๏ธ **Config-driven** โ€” all models, paths, and tuning live in `.env` (no hardcoded values). - ๐Ÿ“Š **Observability** โ€” per-call **timing + token usage** logged to the terminal and shown under each answer. - ๐Ÿณ **Dockerized** โ€” `docker-compose` with a persistent volume for the DuckDB file. --- ## Technology Stack | Layer | Technology | |-------|-----------| | **Language** | Python 3.11+ | | **UI** | [Streamlit](https://streamlit.io/) | | **Database** | [DuckDB](https://duckdb.org/) (embedded, columnar) | | **Vector index** | DuckDB `vss` extension โ€” **HNSW** approximate nearest-neighbor, cosine metric | | **Orchestration** | [LangGraph](https://langchain-ai.github.io/langgraph/) (router โ†’ sql/semantic โ†’ answer) | | **LLM framework** | [LangChain](https://python.langchain.com/) | | **Chat LLMs** | `langchain-huggingface` (Qwen via novita) ยท `langchain-openai` (gpt-4o) ยท `langchain-google-genai` (gemini) | | **Embeddings** | `sentence-transformers` (local MiniLM) ยท `langchain-google-genai` (gemini-embedding) | | **Data** | `pandas`, `requests` | | **Config** | `python-dotenv` (`.env`) | | **Container** | Docker + docker-compose | --- ## APIs Used ### External REST APIs (data source) | API | Endpoint (`.env`) | Purpose | |-----|-------------------|---------| | NHTSA **vPIC** โ€” makes | `GET {VPIC_URL}/GetMakesForVehicleType/car?format=json` | List all car makes | | NHTSA **vPIC** โ€” models | `GET {VPIC_URL}/GetModelsForMake/{make}?format=json` | Models for a selected make | | NHTSA **Complaints** | `GET {COMPLAINTS_URL}?make=..&model=..&modelYear=..` | Vehicle complaints for make/model/year | - `VPIC_URL` = `https://vpic.nhtsa.dot.gov/api/vehicles` - `COMPLAINTS_URL` = `https://api.nhtsa.gov/complaints/complaintsByVehicle` ### Model provider APIs | Provider | API | Used for | Auth | |----------|-----|----------|------| | HuggingFace | Inference Providers (`router.huggingface.co` โ†’ novita) | Chat (primary) | `HUGGINGFACEHUB_API_TOKEN` | | OpenAI | Chat Completions (`api.openai.com`) | Chat (secondary) | `OPENAI_API_KEY` | | Google | Gemini API (`generativelanguage.googleapis.com`) | Chat + embeddings (secondary) | `GOOGLE_API_KEY` | --- ## Models The app supports **three interchangeable providers**. HuggingFace is primary; OpenAI / Google are fallbacks (useful when HF Inference credits are depleted). ### Chat LLM (`Provider` dropdown in the sidebar) | Priority | Provider | Model (`.env`) | Notes | |----------|----------|----------------|-------| | **Primary** | HuggingFace | `HF_MODEL=Qwen/Qwen2.5-72B-Instruct` via `HF_PROVIDER=novita` | Uses `HUGGINGFACEHUB_API_TOKEN`. Free monthly credits; may hit `402` when depleted. | | Secondary | OpenAI | `OPENAI_MODEL=gpt-4o` | Uses `OPENAI_API_KEY`. Billed to your key. Real token streaming. | | Secondary | Google | `GOOGLE_CHAT_MODEL=gemini-2.0-flash` | Uses `GOOGLE_API_KEY`. Generous free tier (per-day quota). | ### Embedding model (semantic search) โ€” set by `EMBED_PROVIDER` in `.env` | Priority | `EMBED_PROVIDER` | Model | Dim | Notes | |----------|------------------|-------|-----|-------| | **Primary** | `hf` | `sentence-transformers/all-MiniLM-L6-v2` | 384 | Local, **free, offline**. Best for embedding all rows. | | Secondary | `google` | `models/gemini-embedding-001` | 768 | API-based; free tier ~100 req/min. | > Switching `EMBED_PROVIDER` changes the vector dimension (384 โ†” 768). The app auto-recreates > the `embedding` column + HNSW index, then you must **re-embed** (see [Backfill](#4-backfill-embeddings)). --- ## Architecture ![Architecture diagram](architecture.png) > Editable source: [`architecture.drawio`](architecture.drawio) (open in [draw.io](https://app.diagrams.net) or import into Miro). ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Streamlit UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” Make/Model/Year โ”€โ”€โ–ถ โ”‚ Fetch & store Chat + templates โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ NHTSA APIs โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ (vPIC + complaints) โ–ผ โ”Œโ”€โ”€โ”€โ”€โ”€ LangGraph โ”€โ”€โ”€โ”€โ”€โ” DuckDB (complaints table) โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ router (LLM) โ”‚ - columns + vin โ”‚ โ”œโ”€ sql_node โ”‚โ”€โ”€โ–ถ DuckDB SQL - embedding FLOAT[dim] โ”‚ โ””โ”€ semantic_node โ”‚โ”€โ”€โ–ถ HNSW vector search - HNSW cosine index โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ–ผ stream_answer (LLM) โ”€โ”€โ–ถ NL answer + table ``` - **2 LLM calls per question:** `router` (question โ†’ SQL or semantic phrase) + `answer` (rows โ†’ NL). - **RAG:** the semantic path retrieves the top-k similar complaints and grounds the answer on them. --- ## Prerequisites - **Python 3.11+** - API keys as needed (at least one chat provider + optionally Google for embeddings): - `HUGGINGFACEHUB_API_TOKEN` (primary chat) - `OPENAI_API_KEY` (secondary chat) - `GOOGLE_API_KEY` (secondary chat + `google` embeddings) --- ## Setup ### 1. Install dependencies ```bash python -m pip install -r requirements.txt # For local HuggingFace embeddings (EMBED_PROVIDER=hf): python -m pip install sentence-transformers ``` ### 2. Create `.env` All configuration lives in `.env` (no hardcoded values in the code). Create it in the project root: ```env # ---- Secrets ---- HUGGINGFACEHUB_API_TOKEN=hf_xxx OPENAI_API_KEY=sk-xxx GOOGLE_API_KEY=AIza-xxx # ---- DuckDB ---- DUCKDB_PATH=data/complaints.duckdb TABLE=complaints # ---- Embedding backend ---- # hf -> EMBED_MODEL=sentence-transformers/all-MiniLM-L6-v2 EMBED_DIM=384 # google -> EMBED_MODEL=models/gemini-embedding-001 EMBED_DIM=768 EMBED_PROVIDER=hf EMBED_MODEL=sentence-transformers/all-MiniLM-L6-v2 EMBED_DIM=384 # ---- Backfill throttling (Gemini free tier ~100 req/min) ---- EMBED_MAX_ROWS=500 EMBED_CHUNK=50 EMBED_SLEEP=1.0 # ---- Chat LLMs ---- HF_MODEL=Qwen/Qwen2.5-72B-Instruct HF_PROVIDER=novita OPENAI_MODEL=gpt-4o GOOGLE_CHAT_MODEL=gemini-2.0-flash # ---- NHTSA data source APIs ---- VPIC_URL=https://vpic.nhtsa.dot.gov/api/vehicles COMPLAINTS_URL=https://api.nhtsa.gov/complaints/complaintsByVehicle ``` > `.env` is git-ignored. Missing keys fail loudly (`RuntimeError: Missing required config '...'`). ### 3. Run the app ```bash python -m streamlit run duckdb_app.py ``` Open http://localhost:8501. Then: 1. In the sidebar, pick a **Make**, **Model**, **Model year** โ†’ **Fetch & store**. 2. (First time / after switching embedding provider) run the **backfill** below to embed existing rows. 3. Ask questions in the chat, or click a sample-question template. 4. Pick the chat **Provider** (HuggingFace primary; switch to OpenAI/Google if HF credits are depleted). ### 4. Backfill embeddings New rows are embedded automatically on **Fetch & store**. To embed rows that predate the `embedding` column (or after switching `EMBED_PROVIDER`), run the one-off backfill **with the app and DBeaver closed** (DuckDB allows one writer): ```bash # Local HF (free) โ€” embed everything at once: EMBED_MAX_ROWS=100000 python backfill_embeddings.py # Google (free tier) โ€” process a capped subset per run, resumable: python backfill_embeddings.py # embeds next EMBED_MAX_ROWS rows; re-run to continue ``` The status caption in the app shows coverage, e.g. `โ€ฆ 7331 rows ยท embedded 7331 (100%) ยท vector index HNSW โœ“ (hf, 384d)`. --- ## Docker ```bash docker compose up --build # โ†’ http://localhost:8501 ``` - Reads keys from `.env` (`env_file`). - Persists the DuckDB file on the `duckdb-data` volume. - For local `hf` embeddings inside Docker, uncomment `sentence-transformers` in `requirements.txt` (adds torch โ€” a larger image). `google` embeddings need no extra dependency. --- ## Example questions **Exact (SQL route):** - How many complaints involved a fire? - What are the top 10 most common components? - Give me the details of VIN 5YJ3E1EB8LF - How many complaints were filed in 2020? **Meaning-based (semantic / vector route):** - Find complaints about the car suddenly accelerating on its own - Anything similar to phantom braking on the highway? - Complaints describing the battery catching fire --- ## Inspecting the DuckDB file **DBeaver:** New Connection โ†’ DuckDB โ†’ point to `data/complaints.duckdb`. Match the DuckDB JDBC driver to **1.5.x**, and either close the app first or open read-only (`duckdb.read_only = true`) โ€” only one writer is allowed. **CLI / Python:** ```python import duckdb con = duckdb.connect("data/complaints.duckdb", read_only=True) print(con.sql("SELECT make, model, COUNT(*) FROM complaints GROUP BY 1,2")) ``` --- ## Project files | File | Purpose | |------|---------| | `duckdb_app.py` | **Main app** โ€” dataset builder + DuckDB + LangGraph RAG chatbot | | `backfill_embeddings.py` | One-off / resumable embedding backfill for existing rows | | `nhtsa_app.py` | Simpler Streamlit app โ€” build a HF `Dataset` + LLM Q&A (no DuckDB) | | `nhtsa_dataset.py` | Standalone script โ€” fetch NHTSA data into a HF `Dataset` and query it | | `app.py` | Minimal LangChain demo (provider dropdown) | | `LLM_Intro.ipynb` | Bootcamp notebook (HuggingFace + OpenAI intro) | | `Dockerfile`, `docker-compose.yml` | Containerization | | `requirements.txt` | Dependencies | | `.env` | All config + secrets (git-ignored) | --- ## Troubleshooting | Error | Cause / Fix | |-------|-------------| | `402 Payment Required` (novita) | HuggingFace Inference credits depleted โ†’ switch Provider to **OpenAI**/**Google**, or go HF PRO. | | `429 RESOURCE_EXHAUSTED` (Gemini) | Google free-tier quota hit โ†’ wait for daily reset, or use another provider. | | `429 Request too large ... TPM` (OpenAI) | Handled โ€” heavy `embedding`/`embed_text` columns are stripped before the LLM. Reduce result size if it recurs. | | `IO Error ... used by another process` | DuckDB file locked by the app/DBeaver โ†’ close the other writer. | | `ModuleNotFoundError: duckdb` | Wrong interpreter โ†’ install into the one running Streamlit. | | `Missing required config '...'` | A key is absent from `.env` โ†’ add it. |