docs: add comprehensive PROJECT.md knowledge base for AI agent context
Browse files- PROJECT.md +464 -0
PROJECT.md
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Kasualdad LFED — Project Knowledge Base
|
| 2 |
+
|
| 3 |
+
> **Last updated:** 2026-06-06 (Session 3)
|
| 4 |
+
> **Target:** HF Build Small Hackathon — "Backyard AI" chapter
|
| 5 |
+
> **Space:** https://huggingface.co/spaces/build-small-hackathon/Kasualdad_LFED
|
| 6 |
+
> **Remote:** `git@hf.co:spaces/build-small-hackathon/Kasualdad_LFED` (remote name: `space`)
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## What It Does
|
| 11 |
+
|
| 12 |
+
A Gradio app that lets school district admins ask plain-English questions about student data. A local LLM (llama.cpp + Qwen2.5-Coder-7B GGUF) generates DuckDB SQL, which is validated and executed on in-memory seed data. **Nothing leaves the machine — no API calls, no cloud.**
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
## Architecture & Data Flow
|
| 17 |
+
|
| 18 |
+
```
|
| 19 |
+
User types question
|
| 20 |
+
↓
|
| 21 |
+
app.py → handle_query() [@spaces.GPU on HF Zero GPU]
|
| 22 |
+
↓
|
| 23 |
+
model_inference.py → build_prompt() + llama.cpp inference
|
| 24 |
+
↓ raw LLM output (JSON or ```sql```)
|
| 25 |
+
data_engine.py → extract_sql() → validate_sql() → execute_safe()
|
| 26 |
+
↓ pandas DataFrame
|
| 27 |
+
app.py → renders SQL + table in Gradio UI
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
**Key design rule:** `app.py` is a thin controller. All logic lives in the three engine modules.
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## File Map
|
| 35 |
+
|
| 36 |
+
| File | Purpose | Lines |
|
| 37 |
+
|---|---|---|
|
| 38 |
+
| `app.py` | Gradio UI, CSS, `@spaces.GPU` wiring, Parquet bootstrap | ~380 |
|
| 39 |
+
| `model_inference.py` | llama.cpp wrapper, CUDA preload, model cache, SQL generation + streaming | ~290 |
|
| 40 |
+
| `data_engine.py` | DuckDB lifecycle, Parquet loading, SQL extraction/validation/execution | ~280 |
|
| 41 |
+
| `prompts.py` | System prompt, schema docs, few-shot examples, prompt assembler | ~130 |
|
| 42 |
+
| `data/generate_seed.py` | Generates 2,900 students, enrollment, attendance | ~200 |
|
| 43 |
+
| `data/export_parquet.py` | One-shot script: seed → Parquet files | ~60 |
|
| 44 |
+
| `data/enrollment.parquet` | Pre-generated enrollment table (1,526 bytes) | — |
|
| 45 |
+
| `data/attendance.parquet` | Pre-generated attendance table (31,482 bytes) | — |
|
| 46 |
+
| `requirements.txt` | Pinned Python deps (see below) | 9 lines |
|
| 47 |
+
| `README.md` | Public Space README | ~280 |
|
| 48 |
+
| `docs/HANDOFF.md` | Developer session handoff doc | ~190 |
|
| 49 |
+
| `docs/PLAN.md` | Original build plan | ~170 |
|
| 50 |
+
| `tests/` | pytest suite (81 tests: execution guard, data engine, model inference) | 4 files |
|
| 51 |
+
| `modal_train/` | Modal fine-tuning pipeline (synthetic data, Unsloth QLoRA, GGUF export) | 5 files |
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
## Model & Inference
|
| 56 |
+
|
| 57 |
+
### Active Model
|
| 58 |
+
|
| 59 |
+
- **Repo:** `mradermacher/Qwen2.5-Coder-7B-Instruct-GGUF`
|
| 60 |
+
- **File:** `Qwen2.5-Coder-7B-Instruct.Q4_K_M.gguf` (~4.68 GB)
|
| 61 |
+
- **Quantization:** Q4_K_M
|
| 62 |
+
- **Context:** 4096 tokens (`n_ctx`)
|
| 63 |
+
- **GPU layers:** `-1` (all layers; Zero GPU handles context switching)
|
| 64 |
+
|
| 65 |
+
### Model Resolution
|
| 66 |
+
|
| 67 |
+
`model_inference.py:_resolve_model_path()` [model_inference.py:124]
|
| 68 |
+
1. Check local cache: `/tmp/lfed-models/qwen/...` (fast if pre-downloaded)
|
| 69 |
+
2. Fallback: download from HF Hub via `huggingface_hub.hf_hub_download()`
|
| 70 |
+
|
| 71 |
+
### Inference Config
|
| 72 |
+
|
| 73 |
+
| Parameter | Local | HF Zero GPU | Note |
|
| 74 |
+
|---|---|---|---|
|
| 75 |
+
| `n_ctx` | 4096 | 4096 | |
|
| 76 |
+
| `n_threads` | 2 | 4 | Detected via `SPACES_ZERO_GPU` env |
|
| 77 |
+
| `n_gpu_layers` | -1 | -1 | Always GPU; Zero GPU emulation at module level |
|
| 78 |
+
| `max_tokens` | 256 | 256 | |
|
| 79 |
+
| `temperature` | 0.0 | 0.0 | Deterministic |
|
| 80 |
+
| `stop` | `\n\n`, `Question:`, `User:` | same | |
|
| 81 |
+
|
| 82 |
+
### Thread Safety
|
| 83 |
+
|
| 84 |
+
`model_inference.py` caches the model in a module-level `_llm` global with a `threading.Lock` (double-check pattern). `generate_sql()` auto-loads if `llm=None`.
|
| 85 |
+
|
| 86 |
+
---
|
| 87 |
+
|
| 88 |
+
## Zero GPU / HF Space Configuration
|
| 89 |
+
|
| 90 |
+
### Space Identity
|
| 91 |
+
- **URL:** `https://huggingface.co/spaces/build-small-hackathon/Kasualdad_LFED`
|
| 92 |
+
- **SDK:** Gradio 6.16.0
|
| 93 |
+
- **Python:** 3.12
|
| 94 |
+
- **Hardware:** Zero GPU (NVIDIA RTX Pro 6000 Blackwell, half — 48 GB VRAM)
|
| 95 |
+
- **Git remote:** `space` → `https://huggingface.co/spaces/build-small-hackathon/Kasualdad_LFED`
|
| 96 |
+
|
| 97 |
+
### Zero GPU Architecture
|
| 98 |
+
|
| 99 |
+
Zero GPU uses **CUDA emulation** at module level and **real GPU** inside `@spaces.GPU` functions. This means:
|
| 100 |
+
- Model loads with `n_gpu_layers=-1` at startup (module level, emulated CUDA)
|
| 101 |
+
- Inference runs inside `@spaces.GPU` decorated `handle_query()` (real GPU)
|
| 102 |
+
- Zero GPU transparently switches between emulated and real CUDA contexts
|
| 103 |
+
|
| 104 |
+
### `@spaces.GPU` Decorator
|
| 105 |
+
|
| 106 |
+
`app.py:37` — `handle_query()` is the only decorated function. Required for Zero GPU to recognize the Space as GPU-enabled.
|
| 107 |
+
|
| 108 |
+
### Startup Sequence (app.py:16-30)
|
| 109 |
+
|
| 110 |
+
1. Print banner
|
| 111 |
+
2. **Ensure Parquet seed files exist** — if `/data/enrollment.parquet` and `/data/attendance.parquet` aren't found, generate them via `export_parquet.py` into `/data/` (persistent Space storage) or `data/` (local dev)
|
| 112 |
+
3. Load model (`load_model()`) — downloads from HF Hub if not cached, loads into GPU/RAM
|
| 113 |
+
4. Launch Gradio UI
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
## CUDA Dependency Story (Full Debugging History)
|
| 118 |
+
|
| 119 |
+
### Problem
|
| 120 |
+
`llama-cpp-python` installed with CUDA 12.1 wheel (`--extra-index-url .../cu121`). At import time, `libllama.so` needs multiple CUDA shared libraries (`libcudart.so.12`, `libcublas.so.12`, `libcublasLt.so.12`, etc.), but they aren't on the system linker path in the HF Space container.
|
| 121 |
+
|
| 122 |
+
### Failed Attempt #1 — `LD_LIBRARY_PATH` via `os.environ`
|
| 123 |
+
**Why it failed:** The C dynamic linker (`ld.so`) reads `LD_LIBRARY_PATH` once at process start. Setting `os.environ["LD_LIBRARY_PATH"]` in Python at runtime has zero effect on `dlopen()`.
|
| 124 |
+
|
| 125 |
+
### Failed Attempt #2 — Load only `libcudart.so.12`
|
| 126 |
+
**Why it failed:** `libllama.so` also needs `libcublas.so.12`, `libcublasLt.so.12`, and other CUDA libs. Loading just `libcudart` got us past the first error but hit the next missing library.
|
| 127 |
+
|
| 128 |
+
### Working Solution — Bulk ctypes preload
|
| 129 |
+
`model_inference.py:20-78`
|
| 130 |
+
|
| 131 |
+
```python
|
| 132 |
+
import ctypes
|
| 133 |
+
|
| 134 |
+
def _preload_dir(lib_dir):
|
| 135 |
+
"""Preload every lib*.so* in a directory with RTLD_GLOBAL."""
|
| 136 |
+
for f in sorted(os.listdir(lib_dir)):
|
| 137 |
+
if f.startswith("lib") and ".so" in f:
|
| 138 |
+
ctypes.CDLL(os.path.join(lib_dir, f), mode=ctypes.RTLD_GLOBAL)
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
**Why it works:** `ctypes.CDLL(full_path, mode=RTLD_GLOBAL)` calls `dlopen()` with the full path + `RTLD_GLOBAL` flag. This loads the library into the process and registers it under its SONAME. When `libllama.so` is subsequently `dlopen()`'d, the linker finds all CUDA dependencies already resident.
|
| 142 |
+
|
| 143 |
+
**Search order:**
|
| 144 |
+
1. System CUDA: `/usr/local/cuda/lib64`, `/usr/local/cuda-12.1/lib64`, `/usr/local/cuda-12.4/lib64`
|
| 145 |
+
2. Pip packages: iterate `sys.path`, find `nvidia/*/lib/` directories, preload all `.so` files
|
| 146 |
+
|
| 147 |
+
### Required CUDA Pip Packages
|
| 148 |
+
|
| 149 |
+
| Package | Version | Provides |
|
| 150 |
+
|---|---|---|
|
| 151 |
+
| `nvidia-cuda-runtime-cu12` | 12.1.105 | `libcudart.so.12` |
|
| 152 |
+
| `nvidia-cublas-cu12` | 12.1.3.1 | `libcublas.so.12`, `libcublasLt.so.12` |
|
| 153 |
+
| `nvidia-cusparse-cu12` | 12.1.0.106 | `libcusparse.so.12` |
|
| 154 |
+
|
| 155 |
+
These are in `requirements.txt`. On the HF Space, the bulk preload picked up **39 total CUDA shared libraries** from the transitive dependencies of these packages.
|
| 156 |
+
|
| 157 |
+
### Import Guard
|
| 158 |
+
|
| 159 |
+
`model_inference.py:81-91` — If the `from llama_cpp import Llama` still fails (truly missing CUDA), a `RuntimeError` with a clear diagnostic is raised:
|
| 160 |
+
|
| 161 |
+
```
|
| 162 |
+
Failed to load llama-cpp-python — CUDA runtime not found.
|
| 163 |
+
If this is a CPU-only machine, install the CPU wheel:
|
| 164 |
+
pip uninstall llama-cpp-python -y && pip install llama-cpp-python
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
---
|
| 168 |
+
|
| 169 |
+
## UI Design System
|
| 170 |
+
|
| 171 |
+
### Current Palette (Cool Professional — slate + indigo)
|
| 172 |
+
|
| 173 |
+
| Token | Value | Usage |
|
| 174 |
+
|---|---|---|
|
| 175 |
+
| `--bg` | `#f1f5f9` (slate-100) | Page background |
|
| 176 |
+
| `--surface` | `#ffffff` | Cards, inputs, code blocks, tables |
|
| 177 |
+
| `--surface-alt` | `#f8fafc` (slate-50) | Table headers |
|
| 178 |
+
| `--border` | `#e2e8f0` (slate-200) | All borders |
|
| 179 |
+
| `--text` | `#1e293b` (slate-800) | Primary body text |
|
| 180 |
+
| `--text-muted` | `#64748b` (slate-500) | Secondary text, labels |
|
| 181 |
+
| `--action` | `#4f46e5` (indigo-600) | Primary CTA buttons |
|
| 182 |
+
| `--action-hover` | `#4338ca` (indigo-700) | Button hover state |
|
| 183 |
+
| `--error` | `#b91c1c` (red-700) | Error messages |
|
| 184 |
+
| `--success` | `#059669` (emerald-600) | Success messages |
|
| 185 |
+
| `--radius` | `12px` | All containers, inputs |
|
| 186 |
+
| `--radius-lg` | `20px` | (defined but not heavily used) |
|
| 187 |
+
| `--transition` | `120ms ease-out` | Hover/focus transitions |
|
| 188 |
+
|
| 189 |
+
### Fonts
|
| 190 |
+
- **Headings:** Cormorant Garamond (serif) — `--font-display`
|
| 191 |
+
- **Body/UI:** Atkinson Hyperlegible (sans) — `--font-ui`
|
| 192 |
+
- **Code:** JetBrains Mono — `--font-mono`
|
| 193 |
+
|
| 194 |
+
All loaded via Google Fonts in `HEAD_HTML` (`app.py:345`).
|
| 195 |
+
|
| 196 |
+
### Gradio Dark-Theme Overrides
|
| 197 |
+
`app.py:77-82` — A "nuke block" strips Gradio's dark-theme defaults before our rules apply:
|
| 198 |
+
```css
|
| 199 |
+
.gr-textbox, .gr-code, .gr-dataframe, .gr-accordion {
|
| 200 |
+
background: transparent !important;
|
| 201 |
+
border-color: transparent !important;
|
| 202 |
+
}
|
| 203 |
+
```
|
| 204 |
+
|
| 205 |
+
The CSS uses aggressive descendant selectors (`> div > div`, `[data-testid]`) because Gradio nests components in multiple wrapper divs.
|
| 206 |
+
|
| 207 |
+
### Design Evolution
|
| 208 |
+
1. **Original:** Linear/Vercel inspired — teal accent (`#14b8a6`), Inter font, white backgrounds
|
| 209 |
+
2. **WellFull Collective:** Warm terracotta + sage palette — rejected (looked "bad" for a data tool)
|
| 210 |
+
3. **Current:** Cool slate + indigo professional palette with the WellFull structure (fonts, radii, spacing)
|
| 211 |
+
|
| 212 |
+
---
|
| 213 |
+
|
| 214 |
+
## Data Engine & Parquet Optimization
|
| 215 |
+
|
| 216 |
+
### Per-Request Lifecycle
|
| 217 |
+
`data_engine.py:create_session()` → `get_connection()` + `seed_database()`
|
| 218 |
+
|
| 219 |
+
Each query creates a **fresh in-memory DuckDB** connection. This ensures:
|
| 220 |
+
- Thread safety (no shared state between requests)
|
| 221 |
+
- Query isolation (can't affect other requests)
|
| 222 |
+
- Clean state (no stale data)
|
| 223 |
+
|
| 224 |
+
### Seeding Priority
|
| 225 |
+
`data_engine.py:seed_database()` [data_engine.py:62-96]
|
| 226 |
+
|
| 227 |
+
1. **Parquet files** (fastest — 33 KB, single-digit ms):
|
| 228 |
+
- `/data/enrollment.parquet` + `/data/attendance.parquet` (Space persistent storage)
|
| 229 |
+
- `data/enrollment.parquet` + `data/attendance.parquet` (local dev)
|
| 230 |
+
- Loaded via DuckDB `read_parquet()` → `CREATE TABLE ... AS SELECT *`
|
| 231 |
+
2. **`data/seed.sql`** (custom overrides)
|
| 232 |
+
3. **Python generator** `data/generate_seed.py` (slow fallback — ~2,900 students, ~11,600 attendance rows)
|
| 233 |
+
|
| 234 |
+
### Parquet Bootstrap
|
| 235 |
+
`app.py:22-29` — On startup, if no Parquet files are found in any of the search dirs, `export_parquet.py` is called to generate them. On the Space, they go to `/data/` which persists across restarts.
|
| 236 |
+
|
| 237 |
+
### SQL Safety Pipeline
|
| 238 |
+
`data_engine.py:execute_safe()` [data_engine.py:235-260]
|
| 239 |
+
|
| 240 |
+
1. `extract_sql()` — Parse JSON envelope → ```sql``` block → raw fallback
|
| 241 |
+
2. `validate_sql()` — Forbidden token check (DROP, DELETE, INSERT, UPDATE, etc.) + schema-aware `EXPLAIN` validation
|
| 242 |
+
3. Wrap: `SELECT * FROM (<user_query>) AS _safe LIMIT 1000`
|
| 243 |
+
4. Execute directly on DuckDB
|
| 244 |
+
5. Return `(cleaned_sql, DataFrame)`
|
| 245 |
+
|
| 246 |
+
### Forbidden Tokens
|
| 247 |
+
`data_engine.py:22-25` — `DROP`, `DELETE`, `INSERT`, `UPDATE`, `ALTER`, `TRUNCATE`, `CREATE`, `ATTACH`, `DETACH`, `PRAGMA`
|
| 248 |
+
|
| 249 |
+
---
|
| 250 |
+
|
| 251 |
+
## Database Schema
|
| 252 |
+
|
| 253 |
+
### `enrollment`
|
| 254 |
+
|
| 255 |
+
| Column | Type | Description |
|
| 256 |
+
|---|---|---|
|
| 257 |
+
| `school_year` | VARCHAR | School year, format `'YYYY-YYYY'` |
|
| 258 |
+
| `school_name` | VARCHAR | One of 5 schools |
|
| 259 |
+
| `grade_level` | INTEGER | Grade level (K=0 through 12) |
|
| 260 |
+
| `student_count` | INTEGER | Students enrolled in that grade/year/school |
|
| 261 |
+
|
| 262 |
+
### `attendance`
|
| 263 |
+
|
| 264 |
+
| Column | Type | Description |
|
| 265 |
+
|---|---|---|
|
| 266 |
+
| `student_id` | INTEGER | Unique student identifier |
|
| 267 |
+
| `school_name` | VARCHAR | School the student attends |
|
| 268 |
+
| `school_year` | VARCHAR | School year, format `'YYYY-YYYY'` |
|
| 269 |
+
| `absence_count` | INTEGER | Total absences for that year |
|
| 270 |
+
| `is_chronically_absent` | BOOLEAN | TRUE if missed ≥10% of school days |
|
| 271 |
+
|
| 272 |
+
### Schools
|
| 273 |
+
|
| 274 |
+
| School | Grades | Base Enrollment |
|
| 275 |
+
|---|---|---|
|
| 276 |
+
| Lincoln Elementary | K–5 | 520 |
|
| 277 |
+
| Washington Middle | 6–8 | 480 |
|
| 278 |
+
| Jefferson High | 9–12 | 900 |
|
| 279 |
+
| Roosevelt Academy | K–8 | 380 |
|
| 280 |
+
| Kennedy Prep | 6–12 | 620 |
|
| 281 |
+
|
| 282 |
+
### Seed Data Stats
|
| 283 |
+
- **Students:** 2,900
|
| 284 |
+
- **Chronic absenteeism rate:** 15% (435 students)
|
| 285 |
+
- **Enrollment rows:** 116 (5 schools × 4 years × variable grades)
|
| 286 |
+
- **Attendance rows:** 11,600 (2,900 students × 4 years)
|
| 287 |
+
- **School years:** 2021-2022, 2022-2023, 2023-2024, 2024-2025
|
| 288 |
+
- **YOY growth:** 3% per year with ±15% random grade-size variance
|
| 289 |
+
- **Reproducible:** `random.seed(42)` in `generate_seed.py`
|
| 290 |
+
|
| 291 |
+
---
|
| 292 |
+
|
| 293 |
+
## Prompt Engineering
|
| 294 |
+
|
| 295 |
+
`prompts.py` assembles the full LLM prompt:
|
| 296 |
+
|
| 297 |
+
### Structure
|
| 298 |
+
```
|
| 299 |
+
SYSTEM_PROMPT (rules, constraints, output format)
|
| 300 |
+
↓
|
| 301 |
+
Schema documentation (table + column list)
|
| 302 |
+
↓
|
| 303 |
+
Few-shot examples (4 question→SQL pairs)
|
| 304 |
+
↓
|
| 305 |
+
User question
|
| 306 |
+
↓
|
| 307 |
+
"SQL:"
|
| 308 |
+
```
|
| 309 |
+
|
| 310 |
+
### System Prompt Rules
|
| 311 |
+
`prompts.py:SYSTEM_PROMPT` [prompts.py:9-18]
|
| 312 |
+
1. Only SELECT statements
|
| 313 |
+
2. Exact table/column names from schema
|
| 314 |
+
3. Proper DuckDB syntax (VARCHAR → single quotes, BOOLEAN → TRUE/FALSE)
|
| 315 |
+
4. Use column aliases for aggregations
|
| 316 |
+
5. Join on logical columns
|
| 317 |
+
6. Make reasonable assumptions if ambiguous
|
| 318 |
+
7. Output ONLY the ```sql``` block, no explanation
|
| 319 |
+
|
| 320 |
+
### Few-Shot Examples
|
| 321 |
+
`prompts.py:FEW_SHOT_EXAMPLES` [prompts.py:56-76]
|
| 322 |
+
|
| 323 |
+
| Question | SQL pattern |
|
| 324 |
+
|---|---|
|
| 325 |
+
| "How many chronically absent in 2023-2024?" | `COUNT(*)` with `WHERE ... AND is_chronically_absent = TRUE` |
|
| 326 |
+
| "Show enrollment per school sorted" | `SUM(student_count) GROUP BY school_name ORDER BY ... DESC` |
|
| 327 |
+
| "What percentage at Lincoln Elementary?" | `COUNT(CASE WHEN ...) * 100.0 / COUNT(*)` |
|
| 328 |
+
| "Enrollment trend since 2021" | `GROUP BY school_year ORDER BY school_year` |
|
| 329 |
+
|
| 330 |
+
---
|
| 331 |
+
|
| 332 |
+
## Fine-Tuning Pipeline (Modal)
|
| 333 |
+
|
| 334 |
+
### Location: `modal_train/`
|
| 335 |
+
|
| 336 |
+
| Script | Purpose |
|
| 337 |
+
|---|---|
|
| 338 |
+
| `generate_synthetic.py` | Creates 1,289 NL→SQL pairs from 32 query templates |
|
| 339 |
+
| `train.py` | Unsloth QLoRA on Qwen2.5-Coder-7B (r=16, 4-bit, 3 epochs, A10G) |
|
| 340 |
+
| `export_gguf.py` | Merges LoRA → converts to GGUF Q4_K_M → pushes to HF Hub |
|
| 341 |
+
| `modal_app.py` | Modal orchestration (`modal.App("kasualdad-lfed-train")`) |
|
| 342 |
+
| `train.jsonl` | Training data (1,289 pairs) |
|
| 343 |
+
|
| 344 |
+
### How to Run
|
| 345 |
+
```bash
|
| 346 |
+
modal secret create huggingface HF_TOKEN=<token>
|
| 347 |
+
modal run modal_train/modal_app.py
|
| 348 |
+
```
|
| 349 |
+
|
| 350 |
+
### Post-Training
|
| 351 |
+
After the fine-tuned model is pushed to HF Hub, swap `HF_REPO_ID` and `HF_MODEL_FILE` in `model_inference.py:103-107`. The exact repo ID is documented in `docs/HANDOFF.md`.
|
| 352 |
+
|
| 353 |
+
**Status:** ⏳ Not yet run (needs HF_TOKEN Modal secret)
|
| 354 |
+
|
| 355 |
+
---
|
| 356 |
+
|
| 357 |
+
## Dependencies
|
| 358 |
+
|
| 359 |
+
### Python (`requirements.txt`)
|
| 360 |
+
|
| 361 |
+
| Package | Version | Why |
|
| 362 |
+
|---|---|---|
|
| 363 |
+
| `gradio` | 6.16.0 | Web UI |
|
| 364 |
+
| `duckdb` | 1.5.3 | In-memory SQL engine |
|
| 365 |
+
| `llama-cpp-python` | 0.3.26 | Local LLM inference (CUDA 12.1 wheel) |
|
| 366 |
+
| `huggingface_hub` | 1.18.0 | Model download, HF Hub API |
|
| 367 |
+
| `spaces` | latest | `@spaces.GPU` decorator for Zero GPU |
|
| 368 |
+
| `nvidia-cuda-runtime-cu12` | 12.1.105 | `libcudart.so.12` |
|
| 369 |
+
| `nvidia-cublas-cu12` | 12.1.3.1 | `libcublas.so.12`, `libcublasLt.so.12` |
|
| 370 |
+
| `nvidia-cusparse-cu12` | 12.1.0.106 | `libcusparse.so.12` |
|
| 371 |
+
|
| 372 |
+
**Wheel index:** `--extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu121` — required to get the pre-built CUDA 12.1 wheel (avoids 15-minute source compile).
|
| 373 |
+
|
| 374 |
+
---
|
| 375 |
+
|
| 376 |
+
## Hackathon Badges
|
| 377 |
+
|
| 378 |
+
| Badge | Status | Implementation |
|
| 379 |
+
|---|---|---|
|
| 380 |
+
| **Off the Grid** | ✅ | llama.cpp + local GGUF, no API calls |
|
| 381 |
+
| **Well-Tuned** | ✅ | Fine-tuning pipeline ready (Modal, 1,289 pairs) |
|
| 382 |
+
| **Llama Champion** | ✅ | llama.cpp sole inference backend, Q4_K_M, streaming |
|
| 383 |
+
| **Off-Brand** | ✅ | Custom design system (Atkinson Hyperlegible + Cormorant Garamond, slate + indigo, WCAG AA) |
|
| 384 |
+
|
| 385 |
+
---
|
| 386 |
+
|
| 387 |
+
## Gotchas & Pitfalls
|
| 388 |
+
|
| 389 |
+
### 1. `LD_LIBRARY_PATH` doesn't work at Python runtime
|
| 390 |
+
The dynamic linker caches it at process start. Use `ctypes.CDLL(path, mode=RTLD_GLOBAL)` to preload libraries instead.
|
| 391 |
+
|
| 392 |
+
### 2. Zero GPU has CUDA emulation at module level
|
| 393 |
+
Load models with GPU layers at startup (module level), not lazily inside `@spaces.GPU` functions. The docs explicitly say lazy loading is "significantly less efficient."
|
| 394 |
+
|
| 395 |
+
### 3. Gradio dark theme bleeds through CSS
|
| 396 |
+
Gradio 6 uses a dark theme by default. You need a "nuke block" (`background: transparent !important`) at the top of your CSS before applying light-theme colors, and you need to target nested wrapper divs (`> div > div`) because Gradio wraps everything.
|
| 397 |
+
|
| 398 |
+
### 4. HF Spaces auto-link models detected in repo text
|
| 399 |
+
Any `username/repo-name` string in README, code, or docs gets linked on the Space page. Keep commented-out future model references out of the repo, or use a non-detectable format (e.g., point to HANDOFF.md instead of spelling out the repo ID).
|
| 400 |
+
|
| 401 |
+
### 5. The fine-tuned model doesn't exist yet
|
| 402 |
+
`kasualdad/lfed-qwen2.5-coder-7b-sql-gguf` is a placeholder. All references to it were scrubbed from the repo to prevent double-linking on the HF Space. The exact repo ID lives only in `docs/HANDOFF.md` for when training completes.
|
| 403 |
+
|
| 404 |
+
### 6. Parquet files are gitignored but force-committed
|
| 405 |
+
The `.gitignore` has `*.parquet`, but the seed Parquet files (33 KB total) were force-added (`git add -f`) because they're needed for local dev and Space builds. Update `export_parquet.py` and re-export if the seed data changes.
|
| 406 |
+
|
| 407 |
+
### 7. macOS vs Linux CUDA divergence
|
| 408 |
+
Locally (macOS), `llama-cpp-python` uses Metal or CPU — the CUDA preload code is harmless (it searches for `.so` files that don't exist and skips silently). On the HF Space (Linux), it preloads 39 CUDA libraries.
|
| 409 |
+
|
| 410 |
+
---
|
| 411 |
+
|
| 412 |
+
## Quick Start
|
| 413 |
+
|
| 414 |
+
```bash
|
| 415 |
+
# Clone
|
| 416 |
+
cd Kasualdad_LFED
|
| 417 |
+
|
| 418 |
+
# Virtual env
|
| 419 |
+
python3.12 -m venv .venv && source .venv/bin/activate
|
| 420 |
+
|
| 421 |
+
# Install
|
| 422 |
+
pip install -r requirements.txt
|
| 423 |
+
|
| 424 |
+
# Generate Parquet seed files (first time only)
|
| 425 |
+
python data/export_parquet.py
|
| 426 |
+
|
| 427 |
+
# Run
|
| 428 |
+
python app.py
|
| 429 |
+
# → http://localhost:7860
|
| 430 |
+
|
| 431 |
+
# Tests
|
| 432 |
+
pytest tests/ -v
|
| 433 |
+
|
| 434 |
+
# Deploy
|
| 435 |
+
git push space main
|
| 436 |
+
```
|
| 437 |
+
|
| 438 |
+
---
|
| 439 |
+
|
| 440 |
+
## Git History (this session)
|
| 441 |
+
|
| 442 |
+
```
|
| 443 |
+
0e9c140 perf: load seed data from Parquet instead of generating on every query
|
| 444 |
+
9bae1e9 fix: remove remaining fine-tuned model repo ID from HANDOFF.md
|
| 445 |
+
14ee1d7 fix: remove nonexistent fine-tuned model reference to prevent double-linking on HF Space
|
| 446 |
+
9618e4b design: hybrid cool palette (slate+indigo), aggressive overrides for Gradio dark-theme boxes
|
| 447 |
+
47f650c fix: restore GPU offload on Zero GPU (emulation mode handles module-level CUDA)
|
| 448 |
+
827143b design: apply WellFull Collective palette + Cormorant Garamond + Atkinson Hyperlegible
|
| 449 |
+
7a51b3e fix: CPU-only on Zero GPU (model persists in RAM), 8 threads, restore eager load
|
| 450 |
+
9ba7b17 fix: add @spaces.GPU decorator, defer model load to first query (Zero GPU)
|
| 451 |
+
a963ee6 fix: preload ALL CUDA .so files, add nvidia-cublas-cu12 + nvidia-cusparse-cu12
|
| 452 |
+
adf4793 fix: preload libcudart.so.12 via ctypes (LD_LIBRARY_PATH has no effect at runtime)
|
| 453 |
+
a6f6cbd fix: resolve CUDA runtime from pip package, add GPU→CPU fallback
|
| 454 |
+
```
|
| 455 |
+
|
| 456 |
+
---
|
| 457 |
+
|
| 458 |
+
## Future Work
|
| 459 |
+
|
| 460 |
+
- [ ] **Run Modal fine-tuning** — needs `modal secret create huggingface HF_TOKEN=<token>`, then `modal run modal_train/modal_app.py`
|
| 461 |
+
- [ ] **Swap to fine-tuned model** — after GGUF is pushed to HF Hub, update `HF_REPO_ID`/`HF_MODEL_FILE` in `model_inference.py:103-107`
|
| 462 |
+
- [ ] **Add streaming back** — `generate_sql_streaming()` exists but is unused (switched to synchronous for HF Space reliability). Could re-enable with proper Zero GPU timeout handling.
|
| 463 |
+
- [ ] **HF Space smoke test** — verify all 6 example queries return correct results after each deploy
|
| 464 |
+
- [ ] **PRO subscription** — if 5 min/day GPU quota is too tight, upgrade to PRO (40 min/day)
|