| # 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 |
| |
|  |
| |
| > 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. | |
|
|