File size: 11,949 Bytes
744603c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# 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. |