JacobLinCool Codex commited on
Commit
e12a049
·
verified ·
1 Parent(s): 3ee3ed0

feat: build retrieval index with llama cpp

Browse files

Co-authored-by: Codex <noreply@openai.com>

DESIGN.md CHANGED
@@ -128,8 +128,8 @@ investigate → ideate → score loop — the experience collapses without the m
128
  | ↳ fallback | `nvidia/parakeet-tdt-0.6b-v3` | 0.6B | **transformers** (no NeMo) | CC-BY-4.0 | 🟩 (Quest brand — verify, §5.1) |
129
  | LLM brain | **`openbmb/MiniCPM5-1B`** ("OpenCPM5") | 1.08B | **transformers** (self-parse XML) / llama.cpp | **Apache-2.0** | 🏮 OpenBMB |
130
  | Turn detection (voice-later) | **`pipecat-ai/smart-turn-v3`** | ~8M | ONNX Runtime (browser) | BSD-2 | (natural voice UX) |
131
- | Embedder | **`google/embeddinggemma-300m`** | ~300M | sentence-transformers / llama.cpp | Gemma (gated) | 🔌 Off the Grid · 🦙 Llama Champion |
132
- | Fine-tune (to add) | LoRA on MiniCPM5 → published to Hub | — | PEFT / Modal | — | 🎯 Well-Tuned |
133
 
134
  **Total ≈ 1.9B params → ≤4B → 🐜 Tiny Titan eligible.** All open-weight, all runnable locally → 🔌 Off the Grid.
135
 
@@ -150,8 +150,8 @@ With **text-first + batch ASR**, the old "streaming ASR vs ZeroGPU" Config A/B t
150
  remains as the Gradio-client contract for external checks.
151
  - **Voice (later bonus):** push-to-talk records an utterance → POST blob → the same `@spaces.GPU` call also runs
152
  Nemotron/Parakeet ASR (batch) before the brain. No persistent stream, no WebRTC, **no TURN server**.
153
- - **Modal (build-time only):** crawl the org + build the EmbeddingGemma index offline; the Space ships with the index
154
- artifact. Runtime never calls Modal → 🔌 Off the Grid holds (see §10).
155
 
156
  > Off the Grid = no proprietary cloud inference APIs. Open weights on an HF GPU Space / local box / Modal all qualify.
157
 
@@ -221,33 +221,28 @@ Silero VAD turn detection, FastRTC. Documented but not on the text-first critica
221
  required before relying on it; fallbacks: port pipecat's numpy-only mel extractor to JS, or do feature-extraction +
222
  onnx **server-side** per posted blob. Pair with `@ricky0123/vad-web` (Silero) for the speech start/stop gate.
223
 
224
- ### 5.4 EmbeddingGemma — `google/embeddinggemma-300m`
225
 
226
- - **Gated** accept Gemma terms + `HF_TOKEN`. 2048-token ctx, 100+ langs, mean pooling, **fp32/bf16 only (no fp16)**.
227
- ```python
228
- from sentence_transformers import SentenceTransformer
229
- m = SentenceTransformer("google/embeddinggemma-300m", truncate_dim=256) # Matryoshka 768→512→256→128
230
- q = m.encode_query("voice game for kids") # prefix: "task: search result | query: "
231
- d = m.encode_document(project_descriptions) # prefix: "title: none | text: "
232
- ```
233
- - **Exact prefixes matter:** query → `task: search result | query: `; document → `title: {title} | text: `; whitespace
234
- clustering → prompt `Clustering` (`task: clustering | query: `). 256-dim is a good speed/quality tradeoff.
235
- - Footprint ~1.2 GB fp32 / ~0.6 GB bf16; QAT Q4_0/Q8_0 + ONNX (`onnx-community/embeddinggemma-300m-ONNX`).
236
 
237
  ### 5.5 llama.cpp support (🦙 Llama Champion)
238
 
239
- The two **language** models run on llama.cpp; the two **audio** models use their own runtimes. Running the core LLM on
240
- llama.cpp earns the badge.
241
 
242
  | Model | llama.cpp? | Runtime | Notes |
243
  |---|---|---|---|
244
- | `openbmb/MiniCPM5-1B` | ✅ | llama.cpp / Ollama | `openbmb/MiniCPM5-1B-GGUF` (Q4_K_M 688 MB); standard Llama arch |
245
- | `google/embeddinggemma-300m` | ✅ | `llama-embedding` | `gemma-embedding` arch (build b6384); `ggml-org/embeddinggemma-300M-GGUF` |
246
  | ASR (Nemotron / Parakeet) | ❌ | NeMo / transformers | FastConformer-RNNT |
247
  | `pipecat-ai/smart-turn-v3` | ❌ | ONNX Runtime | Whisper encoder + classifier head |
248
 
249
- Verify-before-ship: EmbeddingGemma GGUF quant accuracy drifts ([#19040](https://github.com/ggml-org/llama.cpp/issues/19040))
250
- → prefer Q8_0 or keep the embedder on sentence-transformers; MiniCPM5 tool-calling via llama.cpp is a pending PR.
251
 
252
  ---
253
 
@@ -336,46 +331,21 @@ score`) into one *code* "research" action the model calls once. The degradation
336
 
337
  ## 10. Modal — offline pipeline (build-time only → preserves Off the Grid)
338
 
339
- Modal = build-time; runtime never calls it. This is how we claim **both** 🟢 Modal **and** 🔌 Off the Grid. Modal also
340
- trains the 🎯 Well-Tuned LoRA. Crawl org Spaces → embed with EmbeddingGemma → build vector index → commit to a Volume;
341
- the Space ships the index artifact and searches locally.
342
-
343
- ```python
344
- import modal
345
- app = modal.App("bsh-advisor-index")
346
- CACHE = "/cache"
347
- hf_vol = modal.Volume.from_name("hf-cache", create_if_missing=True)
348
- index_vol = modal.Volume.from_name("bsh-index", create_if_missing=True)
349
- image = (modal.Image.debian_slim("3.12")
350
- .pip_install("sentence-transformers", "huggingface_hub", "requests", "numpy", "faiss-cpu")
351
- .env({"HF_HUB_ENABLE_HF_TRANSFER": "1", "HF_HOME": CACHE}))
352
-
353
- @app.function(image=image) # CPU: crawl one Space
354
- def crawl(space_id):
355
- import requests
356
- m = requests.get(f"https://huggingface.co/api/spaces/{space_id}").json()
357
- return {"id": space_id, "text": m.get("cardData", {}).get("short_description", "")}
358
-
359
- @app.cls(image=image, gpu="T4", volumes={CACHE: hf_vol}, scaledown_window=120)
360
- class Embedder:
361
- @modal.enter()
362
- def load(self):
363
- from sentence_transformers import SentenceTransformer
364
- self.m = SentenceTransformer("google/embeddinggemma-300m", cache_folder=CACHE, truncate_dim=256)
365
- @modal.method()
366
- def embed(self, docs): return self.m.encode_document(docs).tolist()
367
-
368
- @app.local_entrypoint()
369
- def main(org="build-small-hackathon"):
370
- import requests
371
- ids = [s["id"] for s in requests.get(f"https://huggingface.co/api/spaces?author={org}").json()]
372
- docs = [d for d in crawl.map(ids) if d["text"]]
373
- vecs = Embedder().embed.remote([d["text"] for d in docs])
374
- # build FAISS index → write to index_vol → index_vol.commit()
375
  ```
376
 
377
- - T4/CPU is plenty (pennies; $30/mo free credits). `gpu="T4"`/`"L4"` (note `"A10"`, not `"A10G"`). `volume.commit()`
378
- after writing. `HF_TOKEN` via `modal.Secret` for the gated EmbeddingGemma download. Crawl on CPU, embed on GPU.
 
 
 
 
379
 
380
  ---
381
 
@@ -429,7 +399,7 @@ open grimoire as the hero component.
429
  | 🎨 Off-Brand (badge + $1.5k) | `gr.Server` custom UI is the agent's output surface |
430
  | 🏮 OpenBMB ($10k) | brain = MiniCPM5-1B ("OpenBMB pick") |
431
  | 🟩 NVIDIA Quest (2× RTX 5080) | ASR = Nemotron (verify if Parakeet qualifies, §5.1) |
432
- | 🦙 Llama Champion (badge) | MiniCPM5 + EmbeddingGemma run through llama.cpp (§5.5) |
433
  | 📡 Sharing is Caring (badge) | publish the agent's tool-call trace to the Hub |
434
  | 📓 Field Notes (badge) | this DESIGN.md → a build blog post |
435
  | 🎖️ Bonus Quest Champion ($2k) | 6/6 badges (needs the Well-Tuned fine-tune) |
 
128
  | ↳ fallback | `nvidia/parakeet-tdt-0.6b-v3` | 0.6B | **transformers** (no NeMo) | CC-BY-4.0 | 🟩 (Quest brand — verify, §5.1) |
129
  | LLM brain | **`openbmb/MiniCPM5-1B`** ("OpenCPM5") | 1.08B | **transformers** (self-parse XML) / llama.cpp | **Apache-2.0** | 🏮 OpenBMB |
130
  | Turn detection (voice-later) | **`pipecat-ai/smart-turn-v3`** | ~8M | ONNX Runtime (browser) | BSD-2 | (natural voice UX) |
131
+ | Embedder | **`ggml-org/embeddinggemma-300M-qat-q4_0-GGUF`** | ~300M | llama.cpp / llama-cpp-python | Gemma | 🔌 Off the Grid · 🦙 Llama Champion · 🟢 Modal |
132
+ | Fine-tune | LoRA on MiniCPM5 → published to Hub | — | PEFT / HF Jobs | — | 🎯 Well-Tuned |
133
 
134
  **Total ≈ 1.9B params → ≤4B → 🐜 Tiny Titan eligible.** All open-weight, all runnable locally → 🔌 Off the Grid.
135
 
 
150
  remains as the Gradio-client contract for external checks.
151
  - **Voice (later bonus):** push-to-talk records an utterance → POST blob → the same `@spaces.GPU` call also runs
152
  Nemotron/Parakeet ASR (batch) before the brain. No persistent stream, no WebRTC, **no TURN server**.
153
+ - **Modal (build-time only):** crawl the org + build the llama.cpp EmbeddingGemma vector index offline; the Space ships
154
+ with checked-in project vectors. Runtime never calls Modal → 🔌 Off the Grid holds (see §10).
155
 
156
  > Off the Grid = no proprietary cloud inference APIs. Open weights on an HF GPU Space / local box / Modal all qualify.
157
 
 
221
  required before relying on it; fallbacks: port pipecat's numpy-only mel extractor to JS, or do feature-extraction +
222
  onnx **server-side** per posted blob. Pair with `@ricky0123/vad-web` (Silero) for the speech start/stop gate.
223
 
224
+ ### 5.4 EmbeddingGemma GGUF — `ggml-org/embeddinggemma-300M-qat-q4_0-GGUF`
225
 
226
+ - Active retrieval model: `embeddinggemma-300M-qat-Q4_0.gguf`, 768-dimensional normalized embeddings.
227
+ - Build-time path: Modal remote function runs `llama-cpp-python` with mean pooling and writes `data/project_index.json`.
228
+ - Runtime path: Space embeds each user query through the same GGUF model via llama.cpp, then performs local cosine search
229
+ over checked-in project vectors.
230
+ - Evidence is recorded in index metadata: model repo, GGUF filename, runtime, dimensions, build source, builder script,
231
+ llama-cpp-python version, and Modal app name.
 
 
 
 
232
 
233
  ### 5.5 llama.cpp support (🦙 Llama Champion)
234
 
235
+ The active Llama Champion path is the retrieval model: the project index is built with EmbeddingGemma GGUF through
236
+ llama.cpp on Modal, and runtime query embeddings use the same llama.cpp path.
237
 
238
  | Model | llama.cpp? | Runtime | Notes |
239
  |---|---|---|---|
240
+ | `openbmb/MiniCPM5-1B` | ✅ planned only | llama.cpp / Ollama | Not used for deployed tool-calling; Transformers + LoRA is the deployed brain. |
241
+ | `ggml-org/embeddinggemma-300M-qat-q4_0-GGUF` | ✅ active | llama.cpp / llama-cpp-python | Builds project vectors on Modal and embeds runtime queries in the Space. |
242
  | ASR (Nemotron / Parakeet) | ❌ | NeMo / transformers | FastConformer-RNNT |
243
  | `pipecat-ai/smart-turn-v3` | ❌ | ONNX Runtime | Whisper encoder + classifier head |
244
 
245
+ If retrieval quality becomes the bottleneck, compare Q4_0 against Q8_0, but do not keep two runtime retrieval paths.
 
246
 
247
  ---
248
 
 
331
 
332
  ## 10. Modal — offline pipeline (build-time only → preserves Off the Grid)
333
 
334
+ Modal = build-time; runtime never calls it. This is how the app claims **both** 🟢 Modal and 🔌 Off the Grid. The
335
+ canonical command is:
336
+
337
+ ```bash
338
+ .venv/bin/modal run scripts/modal_build_project_index.py \
339
+ --projects data/projects.json \
340
+ --out data/project_index.json
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  ```
342
 
343
+ The remote function installs `llama-cpp-python`, downloads
344
+ `ggml-org/embeddinggemma-300M-qat-q4_0-GGUF/embeddinggemma-300M-qat-Q4_0.gguf`, embeds every project card through
345
+ llama.cpp, and returns a schema-v2 JSON index. The local entrypoint writes that payload into the repo for Space runtime.
346
+
347
+ Latest successful run: `hackathon-advisor-llama-index` on Modal, producing a 100-document, 768-dimensional normalized
348
+ index at `2026-06-07T08:16:19+00:00`.
349
 
350
  ---
351
 
 
399
  | 🎨 Off-Brand (badge + $1.5k) | `gr.Server` custom UI is the agent's output surface |
400
  | 🏮 OpenBMB ($10k) | brain = MiniCPM5-1B ("OpenBMB pick") |
401
  | 🟩 NVIDIA Quest (2× RTX 5080) | ASR = Nemotron (verify if Parakeet qualifies, §5.1) |
402
+ | 🦙 Llama Champion (badge) | EmbeddingGemma GGUF retrieval index and runtime query embeddings run through llama.cpp (§5.5) |
403
  | 📡 Sharing is Caring (badge) | publish the agent's tool-call trace to the Hub |
404
  | 📓 Field Notes (badge) | this DESIGN.md → a build blog post |
405
  | 🎖️ Bonus Quest Champion ($2k) | 6/6 badges (needs the Well-Tuned fine-tune) |
README.md CHANGED
@@ -28,7 +28,7 @@ tags:
28
  The current milestone is a deployed ZeroGPU + MiniCPM5 LoRA advisor:
29
 
30
  - Local snapshot of public `build-small-hackathon` Spaces.
31
- - Offline search over project titles, tags, models, and descriptions.
32
  - Jargon correction for hackathon/model terms.
33
  - MiniCPM5 tool-call planning with a published PEFT LoRA adapter, plus deterministic local rules for tests and CPU-only
34
  development.
@@ -52,12 +52,14 @@ Then open <http://127.0.0.1:7860>.
52
 
53
  ```bash
54
  python scripts/crawl_hf_spaces.py --org build-small-hackathon --out data/projects.json
55
- python scripts/build_project_index.py --projects data/projects.json --out data/project_index.json
56
  python scripts/generate_sample_trace.py --projects data/projects.json --index data/project_index.json --out data/sample_trace.jsonl
57
  ```
58
 
59
  The app uses `data/projects.json` and `data/project_index.json` at runtime. The index validates the snapshot timestamp,
60
- source, project order, and digest before the app starts.
 
 
61
 
62
  ## Trace Artifact
63
 
@@ -136,8 +138,9 @@ depending on browser `localStorage`.
136
  ## Prize Ledger
137
 
138
  `/api/prize-ledger` exposes submission evidence: the documented model stack, total parameter budget, Tiny Titan
139
- eligibility, runtime backend, and badge readiness. It is kept as an API artifact rather than a primary in-app panel so
140
- the user-facing app stays centered on idea evaluation. The main `/api/bootstrap` payload does not include the ledger.
 
141
 
142
  ## Wood Map
143
 
@@ -170,15 +173,19 @@ The deployed Space is configured for ZeroGPU inference with:
170
 
171
  ```bash
172
  ADVISOR_ZERO_GPU=1
173
- ADVISOR_ZERO_GPU_DURATION=60
174
  ADVISOR_MODEL_BACKEND=minicpm-transformers
175
  ADVISOR_MODEL_ID=openbmb/MiniCPM5-1B
176
  ADVISOR_ADAPTER_ID=build-small-hackathon/hackathon-advisor-minicpm5-lora
177
  ADVISOR_ADAPTER_REVISION=25de69bcde397e1bcdd852923b56a42f10222650
 
 
178
  ```
179
 
180
  `agent_turn` wraps the engine call with `spaces.GPU` when `ADVISOR_ZERO_GPU=1`, so model loading and generation run on
181
- the ZeroGPU allocation. Local tests and CPU-only development still default to `ADVISOR_MODEL_BACKEND=rules`.
 
 
182
 
183
  ## Test
184
 
 
28
  The current milestone is a deployed ZeroGPU + MiniCPM5 LoRA advisor:
29
 
30
  - Local snapshot of public `build-small-hackathon` Spaces.
31
+ - Modal-built EmbeddingGemma GGUF retrieval index, with runtime query embeddings computed through llama.cpp.
32
  - Jargon correction for hackathon/model terms.
33
  - MiniCPM5 tool-call planning with a published PEFT LoRA adapter, plus deterministic local rules for tests and CPU-only
34
  development.
 
52
 
53
  ```bash
54
  python scripts/crawl_hf_spaces.py --org build-small-hackathon --out data/projects.json
55
+ .venv/bin/modal run scripts/modal_build_project_index.py --projects data/projects.json --out data/project_index.json
56
  python scripts/generate_sample_trace.py --projects data/projects.json --index data/project_index.json --out data/sample_trace.jsonl
57
  ```
58
 
59
  The app uses `data/projects.json` and `data/project_index.json` at runtime. The index validates the snapshot timestamp,
60
+ source, project order, digest, embedding dimensions, and normalized vector shape before the app starts. The canonical
61
+ index is built on Modal with `ggml-org/embeddinggemma-300M-qat-q4_0-GGUF` through llama.cpp; runtime search embeds the
62
+ user query with the same GGUF model and performs local cosine search over the checked-in vectors.
63
 
64
  ## Trace Artifact
65
 
 
138
  ## Prize Ledger
139
 
140
  `/api/prize-ledger` exposes submission evidence: the documented model stack, total parameter budget, Tiny Titan
141
+ eligibility, runtime backend, retrieval-index metadata, and badge readiness. It is kept as an API artifact rather than a
142
+ primary in-app panel so the user-facing app stays centered on idea evaluation. The main `/api/bootstrap` payload does
143
+ not include the ledger.
144
 
145
  ## Wood Map
146
 
 
173
 
174
  ```bash
175
  ADVISOR_ZERO_GPU=1
176
+ ADVISOR_ZERO_GPU_DURATION=120
177
  ADVISOR_MODEL_BACKEND=minicpm-transformers
178
  ADVISOR_MODEL_ID=openbmb/MiniCPM5-1B
179
  ADVISOR_ADAPTER_ID=build-small-hackathon/hackathon-advisor-minicpm5-lora
180
  ADVISOR_ADAPTER_REVISION=25de69bcde397e1bcdd852923b56a42f10222650
181
+ ADVISOR_EMBEDDING_MODEL_REPO=ggml-org/embeddinggemma-300M-qat-q4_0-GGUF
182
+ ADVISOR_EMBEDDING_MODEL_FILE=embeddinggemma-300M-qat-Q4_0.gguf
183
  ```
184
 
185
  `agent_turn` wraps the engine call with `spaces.GPU` when `ADVISOR_ZERO_GPU=1`, so model loading and generation run on
186
+ the ZeroGPU allocation. The retrieval query embedder downloads the GGUF model through `huggingface_hub` unless
187
+ `ADVISOR_EMBEDDING_MODEL_PATH` points to a local file. Local tests and CPU-only development still default to
188
+ `ADVISOR_MODEL_BACKEND=rules`.
189
 
190
  ## Test
191
 
app.py CHANGED
@@ -134,7 +134,7 @@ def runtime() -> dict:
134
 
135
  @app.get("/api/prize-ledger")
136
  def prize_ledger_endpoint() -> dict:
137
- return prize_ledger(engine.runtime_status())
138
 
139
 
140
  @app.get("/api/tool-contracts")
@@ -153,7 +153,7 @@ def demo_session() -> dict:
153
  @app.get("/api/demo-bundle.zip")
154
  def demo_bundle() -> Response:
155
  runtime_status = engine.runtime_status()
156
- ledger = prize_ledger(runtime_status)
157
  metadata = {
158
  **trace_metadata(index),
159
  "project_count": len(index.projects),
@@ -219,7 +219,7 @@ def chapter_api(payload: dict[str, Any] | None = Body(default=None)) -> Response
219
  @app.get("/api/lora-training-kit.zip")
220
  def lora_training_kit() -> Response:
221
  runtime_status = engine.runtime_status()
222
- ledger = prize_ledger(runtime_status)
223
  metadata = {
224
  **trace_metadata(index),
225
  "project_count": len(index.projects),
@@ -291,7 +291,7 @@ def submission_packet_artifact(session_json: str = "{}") -> str:
291
  **trace_metadata(index),
292
  "project_count": len(index.projects),
293
  },
294
- prize_ledger(runtime_status),
295
  )
296
 
297
 
 
134
 
135
  @app.get("/api/prize-ledger")
136
  def prize_ledger_endpoint() -> dict:
137
+ return prize_ledger(engine.runtime_status(), trace_metadata(index))
138
 
139
 
140
  @app.get("/api/tool-contracts")
 
153
  @app.get("/api/demo-bundle.zip")
154
  def demo_bundle() -> Response:
155
  runtime_status = engine.runtime_status()
156
+ ledger = prize_ledger(runtime_status, trace_metadata(index))
157
  metadata = {
158
  **trace_metadata(index),
159
  "project_count": len(index.projects),
 
219
  @app.get("/api/lora-training-kit.zip")
220
  def lora_training_kit() -> Response:
221
  runtime_status = engine.runtime_status()
222
+ ledger = prize_ledger(runtime_status, trace_metadata(index))
223
  metadata = {
224
  **trace_metadata(index),
225
  "project_count": len(index.projects),
 
291
  **trace_metadata(index),
292
  "project_count": len(index.projects),
293
  },
294
+ prize_ledger(runtime_status, trace_metadata(index)),
295
  )
296
 
297
 
data/project_index.json CHANGED
The diff for this file is too large to render. See raw diff
 
data/sample_trace.jsonl CHANGED
@@ -1,4 +1,4 @@
1
- {"app": "hackathon-advisor", "generated_at": "2026-06-07T03:39:50+00:00", "idea_count": 2, "index": {"algorithm": "tfidf-sparse-v1", "index_generated_at": "2026-06-06T19:44:42+00:00", "snapshot_digest": "1d59c168b446bc1268ef648897fac523e85b258fd6048a37409b9cfbc5c28329", "snapshot_generated_at": "2026-06-06T19:20:47+00:00"}, "schema_version": 1, "turn_count": 3, "type": "trace_manifest"}
2
- {"artifact_title": "A local-first archive cartographer for family photos", "input": "A local-first archive cartographer for family photos", "overall": 4.9, "plan_steps": 0, "response": "The ink bleeds around A local-first archive cartographer for family photos. Closest echoes: page 30: Family Bill Assistant; page 45: Kasualdad LFED; page 55: Mycelium. The seal reads ECHO x4 at 4.9/10. Keep the audience, but change the mechanism or artifact so the demo proves a gap instead of joining a cluster.", "schema_version": 1, "tool_resolution": {"call": {"arguments": {"pitch": "A local-first archive cartographer for family photos", "title": "A local-first archive cartographer for family photos"}, "name": "save_idea"}, "errors": [], "status": "valid"}, "tools": [{"name": "save_idea", "summary": "Wrote idea page 'A local-first archive cartographer for family photos'."}, {"name": "search_projects", "summary": "Checked 5 closest project echoes."}, {"name": "score_idea", "summary": "Pressed a five-quadrant seal: 4.9/10."}], "turn_index": 1, "type": "agent_turn", "verdict": "ECHO x4"}
3
- {"artifact_title": "Hands-on science coach", "input": "write bolder and find whitespace", "overall": 6.0, "plan_steps": 0, "response": "Gold gathers on Hands-on science coach. A lab-notebook companion that designs safe experiments from household materials. No close project echoes in the current snapshot. The seal reads UNWRITTEN at 6.0/10. The next move is to make one concrete before/after scene and cite the two weakest nearby echoes in the margin.", "schema_version": 1, "tool_resolution": {"call": {"arguments": {}, "name": "find_whitespace"}, "errors": [], "status": "valid"}, "tools": [{"name": "find_whitespace", "summary": "Ranked 4 under-explored regions."}, {"name": "save_idea", "summary": "Wrote idea page 'Hands-on science coach'."}, {"name": "score_idea", "summary": "Pressed a five-quadrant seal: 4.8/10."}], "turn_index": 2, "type": "agent_turn", "verdict": "UNWRITTEN"}
4
- {"artifact_title": "Hands-on science coach", "input": "make a build plan", "overall": 6.0, "plan_steps": 6, "response": "The wax seal for Hands-on science coach reads 6.0/10, UNWRITTEN. The build path is: 1. Lock a one-sentence promise and one test input that proves what is different. 2. Compare against the nearest echoes, then sharpen the part only this idea can own. 3. Build the smallest happy path: input, nearby project citations, score, and one shareable output. 4. Add one selected-goal feature only after the core loop is smooth enough to explain without narration. 5. Collect successful advisor examples before training a tiny LoRA. 6. Write build notes from the exact decisions, screenshots, and outputs.", "schema_version": 1, "tool_resolution": {"call": {"arguments": {}, "name": "make_plan"}, "errors": [], "status": "valid"}, "tools": [{"name": "score_idea", "summary": "Pressed a five-quadrant seal: 4.8/10."}, {"name": "make_plan", "summary": "Drafted 6 build steps."}], "turn_index": 3, "type": "agent_turn", "verdict": "UNWRITTEN"}
 
1
+ {"app": "hackathon-advisor", "generated_at": "2026-06-07T08:17:29+00:00", "idea_count": 2, "index": {"algorithm": "llama-cpp-embedding-v1", "index_generated_at": "2026-06-07T08:16:19+00:00", "snapshot_digest": "1d59c168b446bc1268ef648897fac523e85b258fd6048a37409b9cfbc5c28329", "snapshot_generated_at": "2026-06-06T19:20:47+00:00"}, "schema_version": 1, "turn_count": 3, "type": "trace_manifest"}
2
+ {"artifact_title": "A local-first archive cartographer for family photos", "input": "A local-first archive cartographer for family photos", "overall": 4.6, "plan_steps": 0, "response": "The ink bleeds around A local-first archive cartographer for family photos. Closest echoes: page 30: Family Bill Assistant; page 50: Local in 30s — Lore Lens; page 41: Investigative News Agent. The seal reads ECHO x4 at 4.6/10. Keep the audience, but change the mechanism or artifact so the demo proves a gap instead of joining a cluster.", "schema_version": 1, "tool_resolution": {"call": {"arguments": {"pitch": "A local-first archive cartographer for family photos", "title": "A local-first archive cartographer for family photos"}, "name": "save_idea"}, "errors": [], "status": "valid"}, "tools": [{"name": "save_idea", "summary": "Wrote idea page 'A local-first archive cartographer for family photos'."}, {"name": "search_projects", "summary": "Checked 5 closest project echoes."}, {"name": "score_idea", "summary": "Pressed a five-quadrant seal: 4.6/10."}], "turn_index": 1, "type": "agent_turn", "verdict": "ECHO x4"}
3
+ {"artifact_title": "Personal archive cartographer", "input": "write bolder and find whitespace", "overall": 4.8, "plan_steps": 0, "response": "Gold gathers on Personal archive cartographer. A tiny model that maps a private archive into stories without sending it to cloud APIs. Nearest echoes are weak: Dream Museum, Storybook. The seal reads ECHO x4 at 4.8/10. The next move is to make one concrete before/after scene and cite the two weakest nearby echoes in the margin.", "schema_version": 1, "tool_resolution": {"call": {"arguments": {}, "name": "find_whitespace"}, "errors": [], "status": "valid"}, "tools": [{"name": "find_whitespace", "summary": "Ranked 4 under-explored regions."}, {"name": "save_idea", "summary": "Wrote idea page 'Personal archive cartographer'."}, {"name": "score_idea", "summary": "Pressed a five-quadrant seal: 4.8/10."}], "turn_index": 2, "type": "agent_turn", "verdict": "ECHO x4"}
4
+ {"artifact_title": "Personal archive cartographer", "input": "make a build plan", "overall": 4.8, "plan_steps": 6, "response": "The wax seal for Personal archive cartographer reads 4.8/10, ECHO x4. The build path is: 1. Lock a one-sentence promise and one test input that proves what is different. 2. Compare against the nearest echoes, then sharpen the part only this idea can own. 3. Build the smallest happy path: input, nearby project citations, score, and one shareable output. 4. Add one selected-goal feature only after the core loop is smooth enough to explain without narration. 5. Collect successful advisor examples before training a tiny LoRA. 6. Write build notes from the exact decisions, screenshots, and outputs.", "schema_version": 1, "tool_resolution": {"call": {"arguments": {}, "name": "make_plan"}, "errors": [], "status": "valid"}, "tools": [{"name": "score_idea", "summary": "Pressed a five-quadrant seal: 4.8/10."}, {"name": "make_plan", "summary": "Drafted 6 build steps."}], "turn_index": 3, "type": "agent_turn", "verdict": "ECHO x4"}
hackathon_advisor/data.py CHANGED
@@ -1,6 +1,6 @@
1
  from __future__ import annotations
2
 
3
- from collections import Counter
4
  from dataclasses import dataclass
5
  from datetime import datetime, timezone
6
  from hashlib import sha256
@@ -8,6 +8,7 @@ import json
8
  import math
9
  from pathlib import Path
10
  import re
 
11
 
12
 
13
  TOKEN_RE = re.compile(r"[a-z0-9][a-z0-9.+_-]*", re.IGNORECASE)
@@ -22,6 +23,15 @@ GENERIC_PUBLIC_SUMMARY_RE = re.compile(
22
  re.IGNORECASE,
23
  )
24
 
 
 
 
 
 
 
 
 
 
25
 
26
  @dataclass(frozen=True)
27
  class Project:
@@ -199,47 +209,45 @@ WHITESPACE_SEEDS: tuple[WhitespaceSeed, ...] = (
199
  )
200
 
201
 
202
- INDEX_ALGORITHM = "tfidf-sparse-v1"
203
-
204
-
205
  class ProjectIndex:
206
  def __init__(
207
  self,
208
  projects: list[Project],
209
  generated_at: str,
210
  source: str,
211
- index_payload: dict | None = None,
 
212
  ) -> None:
213
  if not projects:
214
  raise ValueError("project index requires at least one project")
 
215
  self.projects = projects
216
  self.generated_at = generated_at
217
  self.source = source
218
- if index_payload is None:
219
- index_payload = build_index_payload(projects, generated_at, source)
220
- validate_index_payload(index_payload, projects, generated_at, source)
221
  self.index_generated_at = str(index_payload["generated_at"])
222
  self.index_algorithm = str(index_payload["algorithm"])
223
  self.snapshot_digest = str(index_payload["snapshot_digest"])
224
- self._idf = {str(term): float(value) for term, value in index_payload["idf"].items()}
225
- self._documents = [
226
- Counter({str(term): float(value) for term, value in document["weights"].items()})
 
 
227
  for document in index_payload["documents"]
228
  ]
229
- self._norms = [float(document["norm"]) for document in index_payload["documents"]]
230
 
231
  @classmethod
232
- def from_file(cls, path: Path) -> "ProjectIndex":
233
  data = json.loads(path.read_text(encoding="utf-8"))
234
  projects = [Project.from_dict(item) for item in data["projects"]]
235
- return cls(
236
- projects=projects,
237
- generated_at=str(data.get("generated_at") or ""),
238
- source=str(data.get("source") or ""),
239
- )
240
 
241
  @classmethod
242
- def from_files(cls, project_path: Path, index_path: Path) -> "ProjectIndex":
 
 
 
 
 
243
  data = json.loads(project_path.read_text(encoding="utf-8"))
244
  index_payload = json.loads(index_path.read_text(encoding="utf-8"))
245
  projects = [Project.from_dict(item) for item in data["projects"]]
@@ -248,8 +256,12 @@ class ProjectIndex:
248
  generated_at=str(data.get("generated_at") or ""),
249
  source=str(data.get("source") or ""),
250
  index_payload=index_payload,
 
251
  )
252
 
 
 
 
253
  def top_projects(self, limit: int = 8) -> list[Project]:
254
  return sorted(
255
  self.projects,
@@ -258,35 +270,21 @@ class ProjectIndex:
258
  )[:limit]
259
 
260
  def search(self, query: str, limit: int = 5) -> list[SearchHit]:
261
- query_terms = tokenize(query)
262
  if not query_terms:
263
  return []
264
- query_doc = Counter(query_terms)
265
- query_norm = self._norm(query_doc)
266
  hits: list[SearchHit] = []
267
- for page_number, (project, doc, doc_norm) in enumerate(
268
- zip(self.projects, self._documents, self._norms, strict=True),
269
  start=1,
270
  ):
271
- if doc_norm == 0.0 or query_norm == 0.0:
272
- continue
273
- raw = 0.0
274
- matched: list[str] = []
275
- for term, count in query_doc.items():
276
- if term not in doc:
277
- continue
278
- raw += (count * self._idf.get(term, 1.0)) * doc[term]
279
- matched.append(term)
280
- if not matched:
281
- continue
282
- title_bonus = sum(0.08 for term in matched if term in tokenize(project.title))
283
- tag_bonus = sum(0.05 for term in matched if term in tokenize(" ".join(project.tags)))
284
- score = raw / (query_norm * doc_norm) + title_bonus + tag_bonus
285
  hits.append(
286
  SearchHit(
287
  project=project,
288
  score=score,
289
- matched_terms=tuple(sorted(matched)),
290
  page_number=page_number,
291
  )
292
  )
@@ -304,7 +302,7 @@ class ProjectIndex:
304
  for seed in WHITESPACE_SEEDS:
305
  hits = self.search(seed.query, limit=3)
306
  saturation = sum(hit.score for hit in hits) / max(len(hits), 1)
307
- score = max(0.0, 1.0 - min(saturation, 0.95))
308
  if hits:
309
  evidence = f"Nearest echoes are weak: {', '.join(hit.project.title for hit in hits[:2])}."
310
  else:
@@ -321,47 +319,67 @@ class ProjectIndex:
321
  items.sort(key=lambda item: item.score, reverse=True)
322
  return items[:limit]
323
 
324
- def _norm(self, doc: Counter[str]) -> float:
325
- return math.sqrt(sum((count * self._idf.get(term, 1.0)) ** 2 for term, count in doc.items()))
 
 
 
 
326
 
327
 
328
  def tokenize(text: str) -> list[str]:
329
  return [token.lower().strip("._-+") for token in TOKEN_RE.findall(text) if len(token.strip("._-+")) > 1]
330
 
331
 
332
- def build_index_payload(projects: list[Project], snapshot_generated_at: str, source: str) -> dict:
333
- documents = [Counter(tokenize(project.searchable_text)) for project in projects]
334
- df = Counter(term for document in documents for term in document)
335
- idf = {
336
- term: math.log((1 + len(documents)) / (1 + freq)) + 1.0
337
- for term, freq in sorted(df.items())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  }
339
  indexed_documents = []
340
- for project, document in zip(projects, documents, strict=True):
341
- weights = {
342
- term: round(count * idf.get(term, 1.0), 8)
343
- for term, count in sorted(document.items())
344
- }
345
- norm = math.sqrt(sum(value * value for value in weights.values()))
346
  indexed_documents.append(
347
  {
348
  "project_id": project.id,
349
- "tokens": sum(document.values()),
350
- "unique_terms": len(document),
351
- "norm": round(norm, 8),
352
- "weights": weights,
353
  }
354
  )
355
  return {
356
- "schema_version": 1,
357
  "algorithm": INDEX_ALGORITHM,
358
  "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
359
  "snapshot_generated_at": snapshot_generated_at,
360
  "snapshot_source": source,
361
  "snapshot_digest": project_snapshot_digest(projects, snapshot_generated_at, source),
362
  "document_count": len(projects),
363
- "vocabulary_size": len(idf),
364
- "idf": {term: round(value, 8) for term, value in idf.items()},
365
  "documents": indexed_documents,
366
  }
367
 
@@ -372,7 +390,7 @@ def validate_index_payload(
372
  snapshot_generated_at: str,
373
  snapshot_source: str,
374
  ) -> None:
375
- if payload.get("schema_version") != 1:
376
  raise ValueError("unsupported project index schema version")
377
  if payload.get("algorithm") != INDEX_ALGORITHM:
378
  raise ValueError(f"unsupported project index algorithm: {payload.get('algorithm')}")
@@ -386,6 +404,16 @@ def validate_index_payload(
386
  snapshot_source,
387
  ):
388
  raise ValueError("project index digest does not match projects snapshot")
 
 
 
 
 
 
 
 
 
 
389
  documents = payload.get("documents")
390
  if not isinstance(documents, list) or len(documents) != len(projects):
391
  raise ValueError("project index document count does not match projects snapshot")
@@ -393,6 +421,31 @@ def validate_index_payload(
393
  indexed_ids = [document.get("project_id") for document in documents]
394
  if indexed_ids != project_ids:
395
  raise ValueError("project index project order does not match projects snapshot")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
 
397
 
398
  def project_snapshot_digest(projects: list[Project], generated_at: str, source: str) -> str:
 
1
  from __future__ import annotations
2
 
3
+ from collections.abc import Callable, Sequence
4
  from dataclasses import dataclass
5
  from datetime import datetime, timezone
6
  from hashlib import sha256
 
8
  import math
9
  from pathlib import Path
10
  import re
11
+ from typing import Any
12
 
13
 
14
  TOKEN_RE = re.compile(r"[a-z0-9][a-z0-9.+_-]*", re.IGNORECASE)
 
23
  re.IGNORECASE,
24
  )
25
 
26
+ INDEX_SCHEMA_VERSION = 2
27
+ INDEX_ALGORITHM = "llama-cpp-embedding-v1"
28
+ DEFAULT_EMBEDDING_MODEL_REPO = "ggml-org/embeddinggemma-300M-qat-q4_0-GGUF"
29
+ DEFAULT_EMBEDDING_MODEL_FILE = "embeddinggemma-300M-qat-Q4_0.gguf"
30
+ DEFAULT_EMBEDDING_RUNTIME = "llama.cpp via llama-cpp-python"
31
+
32
+
33
+ EmbeddingFunction = Callable[[str], Sequence[float]]
34
+
35
 
36
  @dataclass(frozen=True)
37
  class Project:
 
209
  )
210
 
211
 
 
 
 
212
  class ProjectIndex:
213
  def __init__(
214
  self,
215
  projects: list[Project],
216
  generated_at: str,
217
  source: str,
218
+ index_payload: dict,
219
+ query_embedder: EmbeddingFunction | None = None,
220
  ) -> None:
221
  if not projects:
222
  raise ValueError("project index requires at least one project")
223
+ validate_index_payload(index_payload, projects, generated_at, source)
224
  self.projects = projects
225
  self.generated_at = generated_at
226
  self.source = source
 
 
 
227
  self.index_generated_at = str(index_payload["generated_at"])
228
  self.index_algorithm = str(index_payload["algorithm"])
229
  self.snapshot_digest = str(index_payload["snapshot_digest"])
230
+ self.embedding_metadata = dict(index_payload["embedding"])
231
+ self.embedding_dimensions = int(self.embedding_metadata["dimensions"])
232
+ self._query_embedder = query_embedder
233
+ self._vectors = [
234
+ tuple(float(value) for value in document["vector"])
235
  for document in index_payload["documents"]
236
  ]
 
237
 
238
  @classmethod
239
+ def from_file(cls, path: Path, query_embedder: EmbeddingFunction | None = None) -> "ProjectIndex":
240
  data = json.loads(path.read_text(encoding="utf-8"))
241
  projects = [Project.from_dict(item) for item in data["projects"]]
242
+ raise ValueError("ProjectIndex.from_file requires a separate embedding index payload")
 
 
 
 
243
 
244
  @classmethod
245
+ def from_files(
246
+ cls,
247
+ project_path: Path,
248
+ index_path: Path,
249
+ query_embedder: EmbeddingFunction | None = None,
250
+ ) -> "ProjectIndex":
251
  data = json.loads(project_path.read_text(encoding="utf-8"))
252
  index_payload = json.loads(index_path.read_text(encoding="utf-8"))
253
  projects = [Project.from_dict(item) for item in data["projects"]]
 
256
  generated_at=str(data.get("generated_at") or ""),
257
  source=str(data.get("source") or ""),
258
  index_payload=index_payload,
259
+ query_embedder=query_embedder,
260
  )
261
 
262
+ def set_query_embedder(self, embedder: EmbeddingFunction) -> None:
263
+ self._query_embedder = embedder
264
+
265
  def top_projects(self, limit: int = 8) -> list[Project]:
266
  return sorted(
267
  self.projects,
 
270
  )[:limit]
271
 
272
  def search(self, query: str, limit: int = 5) -> list[SearchHit]:
273
+ query_terms = set(tokenize(query))
274
  if not query_terms:
275
  return []
276
+ query_vector = normalize_vector(self._embed_query(query))
 
277
  hits: list[SearchHit] = []
278
+ for page_number, (project, vector) in enumerate(
279
+ zip(self.projects, self._vectors, strict=True),
280
  start=1,
281
  ):
282
+ score = max(0.0, min(1.0, (dot_product(query_vector, vector) + 1.0) / 2.0))
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  hits.append(
284
  SearchHit(
285
  project=project,
286
  score=score,
287
+ matched_terms=matched_terms(query_terms, project),
288
  page_number=page_number,
289
  )
290
  )
 
302
  for seed in WHITESPACE_SEEDS:
303
  hits = self.search(seed.query, limit=3)
304
  saturation = sum(hit.score for hit in hits) / max(len(hits), 1)
305
+ score = max(0.0, min(1.0, 1.0 - max(0.0, saturation - 0.35) / 0.60))
306
  if hits:
307
  evidence = f"Nearest echoes are weak: {', '.join(hit.project.title for hit in hits[:2])}."
308
  else:
 
319
  items.sort(key=lambda item: item.score, reverse=True)
320
  return items[:limit]
321
 
322
+ def _embed_query(self, query: str) -> Sequence[float]:
323
+ if self._query_embedder is None:
324
+ from hackathon_advisor.llama_embedding import create_llama_cpp_embedder
325
+
326
+ self._query_embedder = create_llama_cpp_embedder(self.embedding_metadata)
327
+ return self._query_embedder(query)
328
 
329
 
330
  def tokenize(text: str) -> list[str]:
331
  return [token.lower().strip("._-+") for token in TOKEN_RE.findall(text) if len(token.strip("._-+")) > 1]
332
 
333
 
334
+ def matched_terms(query_terms: set[str], project: Project) -> tuple[str, ...]:
335
+ project_terms = set(tokenize(project.searchable_text))
336
+ return tuple(sorted(query_terms & project_terms)[:8])
337
+
338
+
339
+ def build_index_payload(
340
+ projects: list[Project],
341
+ snapshot_generated_at: str,
342
+ source: str,
343
+ embeddings: Sequence[Sequence[float]],
344
+ *,
345
+ embedding_metadata: dict[str, Any] | None = None,
346
+ ) -> dict:
347
+ if len(embeddings) != len(projects):
348
+ raise ValueError("embedding count must match project count")
349
+ normalized = [normalize_vector(vector) for vector in embeddings]
350
+ dimensions = len(normalized[0]) if normalized else 0
351
+ if dimensions <= 0:
352
+ raise ValueError("embedding vectors must not be empty")
353
+ if any(len(vector) != dimensions for vector in normalized):
354
+ raise ValueError("embedding vectors must have one shared dimension")
355
+
356
+ metadata = {
357
+ "model_repo": DEFAULT_EMBEDDING_MODEL_REPO,
358
+ "model_file": DEFAULT_EMBEDDING_MODEL_FILE,
359
+ "runtime": DEFAULT_EMBEDDING_RUNTIME,
360
+ "dimensions": dimensions,
361
+ "normalized": True,
362
+ **(embedding_metadata or {}),
363
  }
364
  indexed_documents = []
365
+ for project, vector in zip(projects, normalized, strict=True):
 
 
 
 
 
366
  indexed_documents.append(
367
  {
368
  "project_id": project.id,
369
+ "text_digest": sha256(project.searchable_text.encode("utf-8")).hexdigest(),
370
+ "norm": round(vector_norm(vector), 8),
371
+ "vector": [round(value, 8) for value in vector],
 
372
  }
373
  )
374
  return {
375
+ "schema_version": INDEX_SCHEMA_VERSION,
376
  "algorithm": INDEX_ALGORITHM,
377
  "generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
378
  "snapshot_generated_at": snapshot_generated_at,
379
  "snapshot_source": source,
380
  "snapshot_digest": project_snapshot_digest(projects, snapshot_generated_at, source),
381
  "document_count": len(projects),
382
+ "embedding": metadata,
 
383
  "documents": indexed_documents,
384
  }
385
 
 
390
  snapshot_generated_at: str,
391
  snapshot_source: str,
392
  ) -> None:
393
+ if payload.get("schema_version") != INDEX_SCHEMA_VERSION:
394
  raise ValueError("unsupported project index schema version")
395
  if payload.get("algorithm") != INDEX_ALGORITHM:
396
  raise ValueError(f"unsupported project index algorithm: {payload.get('algorithm')}")
 
404
  snapshot_source,
405
  ):
406
  raise ValueError("project index digest does not match projects snapshot")
407
+
408
+ embedding = payload.get("embedding")
409
+ if not isinstance(embedding, dict):
410
+ raise ValueError("project index embedding metadata is missing")
411
+ dimensions = int(embedding.get("dimensions") or 0)
412
+ if dimensions <= 0:
413
+ raise ValueError("project index embedding dimensions must be positive")
414
+ if embedding.get("runtime") != DEFAULT_EMBEDDING_RUNTIME:
415
+ raise ValueError("project index embedding runtime must be llama.cpp")
416
+
417
  documents = payload.get("documents")
418
  if not isinstance(documents, list) or len(documents) != len(projects):
419
  raise ValueError("project index document count does not match projects snapshot")
 
421
  indexed_ids = [document.get("project_id") for document in documents]
422
  if indexed_ids != project_ids:
423
  raise ValueError("project index project order does not match projects snapshot")
424
+ for document in documents:
425
+ vector = document.get("vector")
426
+ if not isinstance(vector, list) or len(vector) != dimensions:
427
+ raise ValueError("project index vector dimensions do not match embedding metadata")
428
+ norm = vector_norm(float(value) for value in vector)
429
+ if not 0.99 <= norm <= 1.01:
430
+ raise ValueError("project index vectors must be normalized")
431
+
432
+
433
+ def normalize_vector(vector: Sequence[float]) -> tuple[float, ...]:
434
+ values = tuple(float(value) for value in vector)
435
+ norm = vector_norm(values)
436
+ if norm == 0.0:
437
+ raise ValueError("embedding vector norm must be non-zero")
438
+ return tuple(value / norm for value in values)
439
+
440
+
441
+ def vector_norm(vector: Sequence[float]) -> float:
442
+ return math.sqrt(sum(float(value) * float(value) for value in vector))
443
+
444
+
445
+ def dot_product(left: Sequence[float], right: Sequence[float]) -> float:
446
+ if len(left) != len(right):
447
+ raise ValueError("embedding vectors must have equal dimensions")
448
+ return sum(float(a) * float(b) for a, b in zip(left, right, strict=True))
449
 
450
 
451
  def project_snapshot_digest(projects: list[Project], generated_at: str, source: str) -> str:
hackathon_advisor/llama_embedding.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from pathlib import Path
5
+ import os
6
+ from typing import Any
7
+
8
+ from hackathon_advisor.data import (
9
+ DEFAULT_EMBEDDING_MODEL_FILE,
10
+ DEFAULT_EMBEDDING_MODEL_REPO,
11
+ )
12
+
13
+
14
+ TRUE_VALUES = {"1", "true", "yes", "on"}
15
+ DEFAULT_N_CTX = 512
16
+
17
+
18
+ class LlamaCppEmbedder:
19
+ def __init__(
20
+ self,
21
+ *,
22
+ model_repo: str = DEFAULT_EMBEDDING_MODEL_REPO,
23
+ model_file: str = DEFAULT_EMBEDDING_MODEL_FILE,
24
+ model_path: str = "",
25
+ n_ctx: int = DEFAULT_N_CTX,
26
+ n_threads: int | None = None,
27
+ n_gpu_layers: int = 0,
28
+ verbose: bool = False,
29
+ ) -> None:
30
+ self.model_repo = model_repo.strip() or DEFAULT_EMBEDDING_MODEL_REPO
31
+ self.model_file = model_file.strip() or DEFAULT_EMBEDDING_MODEL_FILE
32
+ self.model_path = model_path.strip()
33
+ self.n_ctx = n_ctx
34
+ self.n_threads = n_threads
35
+ self.n_gpu_layers = n_gpu_layers
36
+ self.verbose = verbose
37
+ self._model = None
38
+
39
+ def __call__(self, text: str) -> Sequence[float]:
40
+ return self.embed(text)
41
+
42
+ def embed(self, text: str) -> Sequence[float]:
43
+ model = self._ensure_model()
44
+ return model.embed(text, normalize=True)
45
+
46
+ def _ensure_model(self):
47
+ if self._model is not None:
48
+ return self._model
49
+ from huggingface_hub import hf_hub_download
50
+ from llama_cpp import LLAMA_POOLING_TYPE_MEAN, Llama
51
+
52
+ model_path = self.model_path
53
+ if not model_path:
54
+ model_path = hf_hub_download(
55
+ repo_id=self.model_repo,
56
+ filename=self.model_file,
57
+ repo_type="model",
58
+ )
59
+ if not Path(model_path).is_file():
60
+ raise RuntimeError(f"llama.cpp embedding model was not found: {model_path}")
61
+ self._model = Llama(
62
+ model_path=model_path,
63
+ embedding=True,
64
+ pooling_type=LLAMA_POOLING_TYPE_MEAN,
65
+ n_ctx=self.n_ctx,
66
+ n_threads=self.n_threads,
67
+ n_gpu_layers=self.n_gpu_layers,
68
+ verbose=self.verbose,
69
+ )
70
+ return self._model
71
+
72
+
73
+ def create_llama_cpp_embedder(metadata: dict[str, Any]) -> LlamaCppEmbedder:
74
+ return LlamaCppEmbedder(
75
+ model_repo=os.environ.get(
76
+ "ADVISOR_EMBEDDING_MODEL_REPO",
77
+ str(metadata.get("model_repo") or DEFAULT_EMBEDDING_MODEL_REPO),
78
+ ),
79
+ model_file=os.environ.get(
80
+ "ADVISOR_EMBEDDING_MODEL_FILE",
81
+ str(metadata.get("model_file") or DEFAULT_EMBEDDING_MODEL_FILE),
82
+ ),
83
+ model_path=os.environ.get("ADVISOR_EMBEDDING_MODEL_PATH", ""),
84
+ n_ctx=_int_env("ADVISOR_EMBEDDING_N_CTX", DEFAULT_N_CTX),
85
+ n_threads=_optional_int_env("ADVISOR_EMBEDDING_THREADS"),
86
+ n_gpu_layers=_int_env("ADVISOR_EMBEDDING_GPU_LAYERS", 0),
87
+ verbose=os.environ.get("ADVISOR_EMBEDDING_VERBOSE", "").strip().lower() in TRUE_VALUES,
88
+ )
89
+
90
+
91
+ def _int_env(name: str, default: int) -> int:
92
+ raw = os.environ.get(name, "").strip()
93
+ if not raw:
94
+ return default
95
+ value = int(raw)
96
+ if value < 0:
97
+ raise RuntimeError(f"{name} must be a non-negative integer.")
98
+ return value
99
+
100
+
101
+ def _optional_int_env(name: str) -> int | None:
102
+ raw = os.environ.get(name, "").strip()
103
+ if not raw:
104
+ return None
105
+ value = int(raw)
106
+ if value <= 0:
107
+ raise RuntimeError(f"{name} must be a positive integer.")
108
+ return value
hackathon_advisor/prize_ledger.py CHANGED
@@ -13,18 +13,11 @@ MODEL_STACK = [
13
  "runtime": "ZeroGPU + transformers + PEFT",
14
  },
15
  {
16
- "role": "Retriever",
17
- "model": "offline TF-IDF snapshot",
18
- "params_b": 0.0,
19
- "status": "deployed",
20
- "runtime": "local sparse index",
21
- },
22
- {
23
- "role": "Planned embedder",
24
- "model": "google/embeddinggemma-300m",
25
  "params_b": 0.30,
26
- "status": "documented build path",
27
- "runtime": "sentence-transformers / llama.cpp",
28
  },
29
  {
30
  "role": "Voice bonus",
@@ -40,7 +33,7 @@ BADGE_LEDGER = [
40
  {
41
  "name": "Off the Grid",
42
  "status": "ready",
43
- "evidence": "Runtime uses a checked-in snapshot and local search; no proprietary inference API.",
44
  },
45
  {
46
  "name": "Off-Brand",
@@ -69,8 +62,8 @@ BADGE_LEDGER = [
69
  },
70
  {
71
  "name": "Llama Champion",
72
- "status": "planned",
73
- "evidence": "MiniCPM5 GGUF and EmbeddingGemma GGUF paths are documented; runtime does not depend on them yet.",
74
  },
75
  ]
76
 
@@ -94,11 +87,12 @@ TRAINING_ARTIFACTS = [
94
  ]
95
 
96
 
97
- def prize_ledger(runtime: dict[str, Any]) -> dict[str, Any]:
98
  total_params = round(sum(float(item["params_b"]) for item in MODEL_STACK), 2)
99
  largest = max(MODEL_STACK, key=lambda item: float(item["params_b"]))
100
  return {
101
  "runtime": runtime,
 
102
  "model_stack": MODEL_STACK,
103
  "total_params_b": total_params,
104
  "largest_model": {
 
13
  "runtime": "ZeroGPU + transformers + PEFT",
14
  },
15
  {
16
+ "role": "Embedding retriever",
17
+ "model": "ggml-org/embeddinggemma-300M-qat-q4_0-GGUF",
 
 
 
 
 
 
 
18
  "params_b": 0.30,
19
+ "status": "deployed",
20
+ "runtime": "Modal-built llama.cpp GGUF index + runtime llama.cpp query embeddings",
21
  },
22
  {
23
  "role": "Voice bonus",
 
33
  {
34
  "name": "Off the Grid",
35
  "status": "ready",
36
+ "evidence": "Runtime uses checked-in project vectors and local llama.cpp query embeddings; no proprietary inference API.",
37
  },
38
  {
39
  "name": "Off-Brand",
 
62
  },
63
  {
64
  "name": "Llama Champion",
65
+ "status": "ready",
66
+ "evidence": "Retrieval uses an EmbeddingGemma GGUF index built by llama.cpp on Modal and query embeddings computed through llama.cpp at runtime.",
67
  },
68
  ]
69
 
 
87
  ]
88
 
89
 
90
+ def prize_ledger(runtime: dict[str, Any], index_metadata: dict[str, Any] | None = None) -> dict[str, Any]:
91
  total_params = round(sum(float(item["params_b"]) for item in MODEL_STACK), 2)
92
  largest = max(MODEL_STACK, key=lambda item: float(item["params_b"]))
93
  return {
94
  "runtime": runtime,
95
+ "retrieval_index": index_metadata or {},
96
  "model_stack": MODEL_STACK,
97
  "total_params_b": total_params,
98
  "largest_model": {
hackathon_advisor/trace_export.py CHANGED
@@ -47,12 +47,26 @@ def build_trace_jsonl(session: dict[str, Any], metadata: dict[str, Any]) -> str:
47
 
48
 
49
  def trace_metadata(index: Any) -> dict[str, str]:
50
- return {
51
  "snapshot_generated_at": index.generated_at,
52
  "index_generated_at": index.index_generated_at,
53
  "index_algorithm": index.index_algorithm,
54
  "snapshot_digest": index.snapshot_digest,
55
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
 
58
  def _tools(event: dict[str, Any]) -> list[dict[str, str]]:
 
47
 
48
 
49
  def trace_metadata(index: Any) -> dict[str, str]:
50
+ metadata = {
51
  "snapshot_generated_at": index.generated_at,
52
  "index_generated_at": index.index_generated_at,
53
  "index_algorithm": index.index_algorithm,
54
  "snapshot_digest": index.snapshot_digest,
55
  }
56
+ embedding = getattr(index, "embedding_metadata", None)
57
+ if isinstance(embedding, dict):
58
+ metadata.update(
59
+ {
60
+ "embedding_model_repo": str(embedding.get("model_repo") or ""),
61
+ "embedding_model_file": str(embedding.get("model_file") or ""),
62
+ "embedding_runtime": str(embedding.get("runtime") or ""),
63
+ "embedding_build_source": str(embedding.get("build_source") or ""),
64
+ "embedding_dimensions": str(embedding.get("dimensions") or ""),
65
+ "embedding_builder": str(embedding.get("builder") or ""),
66
+ "embedding_modal_app": str(embedding.get("modal_app") or ""),
67
+ }
68
+ )
69
+ return metadata
70
 
71
 
72
  def _tools(event: dict[str, Any]) -> list[dict[str, str]]:
pyproject.toml CHANGED
@@ -10,6 +10,7 @@ dependencies = [
10
  "accelerate>=1.0,<2",
11
  "gradio>=6.16.0,<7",
12
  "huggingface-hub>=0.36,<1",
 
13
  "peft>=0.13,<1",
14
  "pillow>=10,<13",
15
  "spaces>=0.50,<1",
@@ -23,6 +24,7 @@ dev = [
23
  ]
24
  model = [
25
  "accelerate>=1.0,<2",
 
26
  "peft>=0.13,<1",
27
  "spaces>=0.50,<1",
28
  "torch>=2.8,<3",
@@ -35,6 +37,10 @@ train = [
35
  "torch>=2.8,<3",
36
  "transformers>=4.55,<5",
37
  ]
 
 
 
 
38
 
39
  [tool.pytest.ini_options]
40
  testpaths = ["tests"]
 
10
  "accelerate>=1.0,<2",
11
  "gradio>=6.16.0,<7",
12
  "huggingface-hub>=0.36,<1",
13
+ "llama-cpp-python>=0.3.26,<1",
14
  "peft>=0.13,<1",
15
  "pillow>=10,<13",
16
  "spaces>=0.50,<1",
 
24
  ]
25
  model = [
26
  "accelerate>=1.0,<2",
27
+ "llama-cpp-python>=0.3.26,<1",
28
  "peft>=0.13,<1",
29
  "spaces>=0.50,<1",
30
  "torch>=2.8,<3",
 
37
  "torch>=2.8,<3",
38
  "transformers>=4.55,<5",
39
  ]
40
+ index = [
41
+ "llama-cpp-python>=0.3.26,<1",
42
+ "modal>=1.4,<2",
43
+ ]
44
 
45
  [tool.pytest.ini_options]
46
  testpaths = ["tests"]
requirements.txt CHANGED
@@ -1,6 +1,7 @@
1
  accelerate>=1.0,<2
2
  gradio>=6.16.0,<7
3
  huggingface-hub>=0.36,<1
 
4
  peft>=0.13,<1
5
  pillow>=10,<13
6
  spaces>=0.50,<1
 
1
  accelerate>=1.0,<2
2
  gradio>=6.16.0,<7
3
  huggingface-hub>=0.36,<1
4
+ llama-cpp-python>=0.3.26,<1
5
  peft>=0.13,<1
6
  pillow>=10,<13
7
  spaces>=0.50,<1
scripts/build_project_index.py CHANGED
@@ -2,38 +2,96 @@
2
  from __future__ import annotations
3
 
4
  import argparse
 
5
  import json
6
  from pathlib import Path
7
  import sys
8
 
9
  ROOT = Path(__file__).resolve().parents[1]
10
  sys.path.insert(0, str(ROOT))
11
- from hackathon_advisor.data import Project, build_index_payload
 
 
 
 
 
 
 
12
 
13
 
14
  def main() -> None:
15
- parser = argparse.ArgumentParser(description="Build the offline project retrieval index.")
 
 
16
  parser.add_argument("--projects", default="data/projects.json")
17
  parser.add_argument("--out", default="data/project_index.json")
 
 
 
 
 
18
  args = parser.parse_args()
19
 
20
- project_path = Path(args.projects)
21
- data = json.loads(project_path.read_text(encoding="utf-8"))
22
- projects = [Project.from_dict(item) for item in data["projects"]]
23
- payload = build_index_payload(
24
- projects=projects,
25
- snapshot_generated_at=str(data.get("generated_at") or ""),
26
- source=str(data.get("source") or ""),
 
 
27
  )
28
  output = Path(args.out)
29
  output.parent.mkdir(parents=True, exist_ok=True)
30
  output.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
31
  print(
32
  "wrote "
33
- f"{payload['document_count']} docs, {payload['vocabulary_size']} terms "
34
  f"to {output}"
35
  )
36
 
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  if __name__ == "__main__":
39
  main()
 
2
  from __future__ import annotations
3
 
4
  import argparse
5
+ import importlib.metadata
6
  import json
7
  from pathlib import Path
8
  import sys
9
 
10
  ROOT = Path(__file__).resolve().parents[1]
11
  sys.path.insert(0, str(ROOT))
12
+
13
+ from hackathon_advisor.data import (
14
+ DEFAULT_EMBEDDING_MODEL_FILE,
15
+ DEFAULT_EMBEDDING_MODEL_REPO,
16
+ Project,
17
+ build_index_payload,
18
+ )
19
+ from hackathon_advisor.llama_embedding import LlamaCppEmbedder
20
 
21
 
22
  def main() -> None:
23
+ parser = argparse.ArgumentParser(
24
+ description="Build the offline project retrieval index with llama.cpp embeddings."
25
+ )
26
  parser.add_argument("--projects", default="data/projects.json")
27
  parser.add_argument("--out", default="data/project_index.json")
28
+ parser.add_argument("--model-repo", default=DEFAULT_EMBEDDING_MODEL_REPO)
29
+ parser.add_argument("--model-file", default=DEFAULT_EMBEDDING_MODEL_FILE)
30
+ parser.add_argument("--model-path", default="")
31
+ parser.add_argument("--n-ctx", type=int, default=512)
32
+ parser.add_argument("--n-threads", type=int, default=0)
33
  args = parser.parse_args()
34
 
35
+ payload = build_payload(
36
+ Path(args.projects),
37
+ model_repo=args.model_repo,
38
+ model_file=args.model_file,
39
+ model_path=args.model_path,
40
+ n_ctx=args.n_ctx,
41
+ n_threads=args.n_threads or None,
42
+ build_source="local",
43
+ builder="scripts/build_project_index.py",
44
  )
45
  output = Path(args.out)
46
  output.parent.mkdir(parents=True, exist_ok=True)
47
  output.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
48
  print(
49
  "wrote "
50
+ f"{payload['document_count']} docs, {payload['embedding']['dimensions']} dims "
51
  f"to {output}"
52
  )
53
 
54
 
55
+ def build_payload(
56
+ project_path: Path,
57
+ *,
58
+ model_repo: str,
59
+ model_file: str,
60
+ model_path: str = "",
61
+ n_ctx: int = 512,
62
+ n_threads: int | None = None,
63
+ build_source: str,
64
+ builder: str,
65
+ modal_app: str = "",
66
+ ) -> dict:
67
+ data = json.loads(project_path.read_text(encoding="utf-8"))
68
+ projects = [Project.from_dict(item) for item in data["projects"]]
69
+ embedder = LlamaCppEmbedder(
70
+ model_repo=model_repo,
71
+ model_file=model_file,
72
+ model_path=model_path,
73
+ n_ctx=n_ctx,
74
+ n_threads=n_threads,
75
+ verbose=False,
76
+ )
77
+ embeddings = [embedder.embed(project.searchable_text) for project in projects]
78
+ metadata = {
79
+ "model_repo": model_repo,
80
+ "model_file": model_file,
81
+ "build_source": build_source,
82
+ "builder": builder,
83
+ "llama_cpp_python_version": importlib.metadata.version("llama-cpp-python"),
84
+ }
85
+ if modal_app:
86
+ metadata["modal_app"] = modal_app
87
+ return build_index_payload(
88
+ projects=projects,
89
+ snapshot_generated_at=str(data.get("generated_at") or ""),
90
+ source=str(data.get("source") or ""),
91
+ embeddings=embeddings,
92
+ embedding_metadata=metadata,
93
+ )
94
+
95
+
96
  if __name__ == "__main__":
97
  main()
scripts/modal_build_project_index.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import modal
10
+
11
+
12
+ APP_NAME = "hackathon-advisor-llama-index"
13
+
14
+ app = modal.App(APP_NAME)
15
+ image = (
16
+ modal.Image.debian_slim(python_version="3.11")
17
+ .pip_install(
18
+ "huggingface-hub>=0.36,<1",
19
+ "llama-cpp-python>=0.3.26,<1",
20
+ )
21
+ .add_local_python_source("hackathon_advisor", copy=True)
22
+ .add_local_python_source("scripts", copy=True)
23
+ )
24
+
25
+
26
+ @app.function(image=image, cpu=4.0, memory=4096, timeout=1800)
27
+ def build_project_index_remote(
28
+ project_snapshot: dict[str, Any],
29
+ model_repo: str,
30
+ model_file: str,
31
+ ) -> dict[str, Any]:
32
+ from pathlib import Path
33
+ import tempfile
34
+
35
+ from scripts.build_project_index import build_payload
36
+
37
+ with tempfile.TemporaryDirectory() as tmpdir:
38
+ project_path = Path(tmpdir) / "projects.json"
39
+ project_path.write_text(
40
+ json.dumps(project_snapshot, ensure_ascii=False),
41
+ encoding="utf-8",
42
+ )
43
+ return build_payload(
44
+ project_path,
45
+ model_repo=model_repo,
46
+ model_file=model_file,
47
+ build_source="modal remote function",
48
+ builder="scripts/modal_build_project_index.py",
49
+ modal_app=APP_NAME,
50
+ )
51
+
52
+
53
+ @app.local_entrypoint()
54
+ def main(
55
+ projects: str = "data/projects.json",
56
+ out: str = "data/project_index.json",
57
+ model_repo: str = "ggml-org/embeddinggemma-300M-qat-q4_0-GGUF",
58
+ model_file: str = "embeddinggemma-300M-qat-Q4_0.gguf",
59
+ ) -> None:
60
+ project_snapshot = json.loads(Path(projects).read_text(encoding="utf-8"))
61
+ payload = build_project_index_remote.remote(project_snapshot, model_repo, model_file)
62
+ output = Path(out)
63
+ output.parent.mkdir(parents=True, exist_ok=True)
64
+ output.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
65
+ print(
66
+ "wrote "
67
+ f"{payload['document_count']} docs, {payload['embedding']['dimensions']} dims "
68
+ f"to {output}"
69
+ )
70
+
71
+
72
+ if __name__ == "__main__":
73
+ parser = argparse.ArgumentParser(description="Build the llama.cpp embedding index on Modal.")
74
+ parser.add_argument("--projects", default="data/projects.json")
75
+ parser.add_argument("--out", default="data/project_index.json")
76
+ parser.add_argument("--model-repo", default="ggml-org/embeddinggemma-300M-qat-q4_0-GGUF")
77
+ parser.add_argument("--model-file", default="embeddinggemma-300M-qat-Q4_0.gguf")
78
+ args = parser.parse_args()
79
+ with app.run():
80
+ payload = build_project_index_remote.remote(
81
+ json.loads(Path(args.projects).read_text(encoding="utf-8")),
82
+ args.model_repo,
83
+ args.model_file,
84
+ )
85
+ output = Path(args.out)
86
+ output.parent.mkdir(parents=True, exist_ok=True)
87
+ output.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
88
+ print(
89
+ "wrote "
90
+ f"{payload['document_count']} docs, {payload['embedding']['dimensions']} dims "
91
+ f"to {output}"
92
+ )
tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
tests/conftest.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from tests.helpers import test_query_embedder
4
+
5
+
6
+ def pytest_configure() -> None:
7
+ import app
8
+
9
+ app.index.set_query_embedder(test_query_embedder)
10
+ app.engine.index.set_query_embedder(test_query_embedder)
tests/helpers.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from hashlib import sha256
4
+ from pathlib import Path
5
+
6
+ from hackathon_advisor.data import ProjectIndex, normalize_vector, tokenize
7
+
8
+
9
+ def load_test_index() -> ProjectIndex:
10
+ return ProjectIndex.from_files(
11
+ Path("data/projects.json"),
12
+ Path("data/project_index.json"),
13
+ query_embedder=test_query_embedder,
14
+ )
15
+
16
+
17
+ def test_query_embedder(text: str) -> tuple[float, ...]:
18
+ vector = [0.0] * 768
19
+ for token in tokenize(text):
20
+ digest = sha256(token.encode("utf-8")).digest()
21
+ index = int.from_bytes(digest[:2], "big") % len(vector)
22
+ sign = 1.0 if digest[2] % 2 == 0 else -1.0
23
+ vector[index] += sign
24
+ if not any(vector):
25
+ vector[0] = 1.0
26
+ return normalize_vector(vector)
tests/test_agent.py CHANGED
@@ -1,5 +1,7 @@
1
  from pathlib import Path
2
 
 
 
3
  from hackathon_advisor.agent import AdvisorEngine
4
  from hackathon_advisor.data import ProjectIndex
5
  from hackathon_advisor.tool_contracts import ToolCall, ToolResolution
@@ -17,7 +19,7 @@ class StaticPlanner:
17
 
18
 
19
  def test_agent_scores_and_persists_idea() -> None:
20
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
21
  engine = AdvisorEngine(index)
22
 
23
  result = engine.turn("A local-first archive cartographer for family photos", {})
@@ -42,7 +44,7 @@ def test_agent_scores_and_persists_idea() -> None:
42
 
43
 
44
  def test_agent_finds_whitespace() -> None:
45
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
46
  engine = AdvisorEngine(index)
47
 
48
  result = engine.turn("write bolder and find whitespace", {})
@@ -54,7 +56,7 @@ def test_agent_finds_whitespace() -> None:
54
 
55
 
56
  def test_gap_command_explores_unused_whitespace() -> None:
57
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
58
  engine = AdvisorEngine(index)
59
 
60
  first = engine.turn("write bolder and find whitespace", {})
@@ -67,7 +69,7 @@ def test_gap_command_explores_unused_whitespace() -> None:
67
 
68
 
69
  def test_agent_preserves_canonical_jargon_case() -> None:
70
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
71
  engine = AdvisorEngine(index)
72
 
73
  result = engine.turn("use neutron and mini cpm on zero gpu", {})
@@ -77,7 +79,7 @@ def test_agent_preserves_canonical_jargon_case() -> None:
77
 
78
 
79
  def test_plan_command_uses_current_idea() -> None:
80
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
81
  engine = AdvisorEngine(index)
82
 
83
  first = engine.turn("A local-first archive cartographer for family photos", {})
@@ -91,7 +93,7 @@ def test_plan_command_uses_current_idea() -> None:
91
 
92
 
93
  def test_non_plan_turns_clear_stale_build_plan() -> None:
94
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
95
  engine = AdvisorEngine(index)
96
 
97
  first = engine.turn("A local-first archive cartographer for family photos", {})
@@ -105,7 +107,7 @@ def test_non_plan_turns_clear_stale_build_plan() -> None:
105
 
106
 
107
  def test_plan_and_rank_do_not_create_placeholder_ideas() -> None:
108
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
109
  engine = AdvisorEngine(index)
110
 
111
  planned = engine.turn("make a build plan", {})
@@ -120,7 +122,7 @@ def test_plan_and_rank_do_not_create_placeholder_ideas() -> None:
120
 
121
 
122
  def test_plan_uses_profile_context() -> None:
123
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
124
  engine = AdvisorEngine(index)
125
  state = {
126
  "profile": {
@@ -141,7 +143,7 @@ def test_plan_uses_profile_context() -> None:
141
 
142
 
143
  def test_distinct_idea_turns_append_to_board() -> None:
144
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
145
  engine = AdvisorEngine(index)
146
 
147
  first = engine.turn("A local-first archive cartographer for family photos", {})
@@ -155,7 +157,7 @@ def test_distinct_idea_turns_append_to_board() -> None:
155
 
156
 
157
  def test_compare_ideas_reranks_board_and_selects_winner() -> None:
158
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
159
  engine = AdvisorEngine(index)
160
 
161
  first = engine.turn("A local-first archive cartographer for family photos", {})
@@ -173,7 +175,7 @@ def test_compare_ideas_reranks_board_and_selects_winner() -> None:
173
 
174
 
175
  def test_plan_preserves_unwritten_whitespace_verdict() -> None:
176
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
177
  engine = AdvisorEngine(index)
178
 
179
  whitespace = engine.turn("write bolder and find whitespace", {})
@@ -185,7 +187,7 @@ def test_plan_preserves_unwritten_whitespace_verdict() -> None:
185
 
186
 
187
  def test_planner_get_project_drives_project_response() -> None:
188
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
189
  engine = AdvisorEngine(index, planner=StaticPlanner(ToolCall("get_project", {"id": "lolaby"})))
190
 
191
  result = engine.turn("read lolaby", {})
@@ -196,7 +198,7 @@ def test_planner_get_project_drives_project_response() -> None:
196
 
197
 
198
  def test_rule_project_reference_does_not_create_or_score_idea() -> None:
199
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
200
  engine = AdvisorEngine(index)
201
  first = engine.turn("A local-first archive cartographer for family photos", {})
202
 
@@ -213,7 +215,7 @@ def test_rule_project_reference_does_not_create_or_score_idea() -> None:
213
 
214
 
215
  def test_planner_profile_and_goals_update_state() -> None:
216
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
217
  planned = AdvisorEngine(index).turn("A local-first archive cartographer for family photos", {})
218
  planned = AdvisorEngine(index).turn("make a build plan", planned.state)
219
  assert planned.state["last_plan"]
@@ -236,7 +238,7 @@ def test_planner_profile_and_goals_update_state() -> None:
236
 
237
 
238
  def test_goal_update_invalidates_current_idea_artifact() -> None:
239
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
240
  first = AdvisorEngine(index).turn("A local-first archive cartographer for family photos", {})
241
  first = AdvisorEngine(index).turn("make a build plan", first.state)
242
  assert first.state["last_plan"]
@@ -255,7 +257,7 @@ def test_goal_update_invalidates_current_idea_artifact() -> None:
255
 
256
 
257
  def test_session_goals_apply_to_new_and_current_ideas() -> None:
258
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
259
  engine = AdvisorEngine(index)
260
  state = {"goals": ["Field Notes"]}
261
 
@@ -268,7 +270,7 @@ def test_session_goals_apply_to_new_and_current_ideas() -> None:
268
 
269
 
270
  def test_well_tuned_goal_adds_training_step_to_plan() -> None:
271
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
272
  engine = AdvisorEngine(index)
273
  state = {"goals": ["Well-Tuned"]}
274
 
@@ -281,7 +283,7 @@ def test_well_tuned_goal_adds_training_step_to_plan() -> None:
281
 
282
 
283
  def test_planner_score_idea_scores_current_idea() -> None:
284
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
285
  first = AdvisorEngine(index).turn("A local-first archive cartographer for family photos", {})
286
  engine = AdvisorEngine(index, planner=StaticPlanner(ToolCall("score_idea", {})))
287
 
 
1
  from pathlib import Path
2
 
3
+ from tests.helpers import load_test_index
4
+
5
  from hackathon_advisor.agent import AdvisorEngine
6
  from hackathon_advisor.data import ProjectIndex
7
  from hackathon_advisor.tool_contracts import ToolCall, ToolResolution
 
19
 
20
 
21
  def test_agent_scores_and_persists_idea() -> None:
22
+ index = load_test_index()
23
  engine = AdvisorEngine(index)
24
 
25
  result = engine.turn("A local-first archive cartographer for family photos", {})
 
44
 
45
 
46
  def test_agent_finds_whitespace() -> None:
47
+ index = load_test_index()
48
  engine = AdvisorEngine(index)
49
 
50
  result = engine.turn("write bolder and find whitespace", {})
 
56
 
57
 
58
  def test_gap_command_explores_unused_whitespace() -> None:
59
+ index = load_test_index()
60
  engine = AdvisorEngine(index)
61
 
62
  first = engine.turn("write bolder and find whitespace", {})
 
69
 
70
 
71
  def test_agent_preserves_canonical_jargon_case() -> None:
72
+ index = load_test_index()
73
  engine = AdvisorEngine(index)
74
 
75
  result = engine.turn("use neutron and mini cpm on zero gpu", {})
 
79
 
80
 
81
  def test_plan_command_uses_current_idea() -> None:
82
+ index = load_test_index()
83
  engine = AdvisorEngine(index)
84
 
85
  first = engine.turn("A local-first archive cartographer for family photos", {})
 
93
 
94
 
95
  def test_non_plan_turns_clear_stale_build_plan() -> None:
96
+ index = load_test_index()
97
  engine = AdvisorEngine(index)
98
 
99
  first = engine.turn("A local-first archive cartographer for family photos", {})
 
107
 
108
 
109
  def test_plan_and_rank_do_not_create_placeholder_ideas() -> None:
110
+ index = load_test_index()
111
  engine = AdvisorEngine(index)
112
 
113
  planned = engine.turn("make a build plan", {})
 
122
 
123
 
124
  def test_plan_uses_profile_context() -> None:
125
+ index = load_test_index()
126
  engine = AdvisorEngine(index)
127
  state = {
128
  "profile": {
 
143
 
144
 
145
  def test_distinct_idea_turns_append_to_board() -> None:
146
+ index = load_test_index()
147
  engine = AdvisorEngine(index)
148
 
149
  first = engine.turn("A local-first archive cartographer for family photos", {})
 
157
 
158
 
159
  def test_compare_ideas_reranks_board_and_selects_winner() -> None:
160
+ index = load_test_index()
161
  engine = AdvisorEngine(index)
162
 
163
  first = engine.turn("A local-first archive cartographer for family photos", {})
 
175
 
176
 
177
  def test_plan_preserves_unwritten_whitespace_verdict() -> None:
178
+ index = load_test_index()
179
  engine = AdvisorEngine(index)
180
 
181
  whitespace = engine.turn("write bolder and find whitespace", {})
 
187
 
188
 
189
  def test_planner_get_project_drives_project_response() -> None:
190
+ index = load_test_index()
191
  engine = AdvisorEngine(index, planner=StaticPlanner(ToolCall("get_project", {"id": "lolaby"})))
192
 
193
  result = engine.turn("read lolaby", {})
 
198
 
199
 
200
  def test_rule_project_reference_does_not_create_or_score_idea() -> None:
201
+ index = load_test_index()
202
  engine = AdvisorEngine(index)
203
  first = engine.turn("A local-first archive cartographer for family photos", {})
204
 
 
215
 
216
 
217
  def test_planner_profile_and_goals_update_state() -> None:
218
+ index = load_test_index()
219
  planned = AdvisorEngine(index).turn("A local-first archive cartographer for family photos", {})
220
  planned = AdvisorEngine(index).turn("make a build plan", planned.state)
221
  assert planned.state["last_plan"]
 
238
 
239
 
240
  def test_goal_update_invalidates_current_idea_artifact() -> None:
241
+ index = load_test_index()
242
  first = AdvisorEngine(index).turn("A local-first archive cartographer for family photos", {})
243
  first = AdvisorEngine(index).turn("make a build plan", first.state)
244
  assert first.state["last_plan"]
 
257
 
258
 
259
  def test_session_goals_apply_to_new_and_current_ideas() -> None:
260
+ index = load_test_index()
261
  engine = AdvisorEngine(index)
262
  state = {"goals": ["Field Notes"]}
263
 
 
270
 
271
 
272
  def test_well_tuned_goal_adds_training_step_to_plan() -> None:
273
+ index = load_test_index()
274
  engine = AdvisorEngine(index)
275
  state = {"goals": ["Well-Tuned"]}
276
 
 
283
 
284
 
285
  def test_planner_score_idea_scores_current_idea() -> None:
286
+ index = load_test_index()
287
  first = AdvisorEngine(index).turn("A local-first archive cartographer for family photos", {})
288
  engine = AdvisorEngine(index, planner=StaticPlanner(ToolCall("score_idea", {})))
289
 
tests/test_app.py CHANGED
@@ -39,7 +39,7 @@ def test_health_exposes_index_metadata() -> None:
39
 
40
  assert payload["ok"] is True
41
  assert payload["projects"] == len(index.projects)
42
- assert payload["index_algorithm"] == "tfidf-sparse-v1"
43
  assert payload["runtime"]["backend"] == "rules"
44
  assert len(payload["snapshot_digest"]) == 64
45
 
@@ -47,7 +47,7 @@ def test_health_exposes_index_metadata() -> None:
47
  def test_bootstrap_exposes_index_metadata() -> None:
48
  payload = bootstrap()
49
 
50
- assert payload["index_algorithm"] == "tfidf-sparse-v1"
51
  assert payload["index_generated_at"]
52
  assert payload["snapshot_digest"]
53
  assert payload["runtime"]["tool_count"] >= 8
@@ -247,5 +247,8 @@ def test_prize_ledger_endpoint_reports_submission_evidence() -> None:
247
  assert payload["runtime"]["backend"] == "rules"
248
  assert payload["tiny_titan_eligible"] is True
249
  assert any(badge["name"] == "Sharing is Caring" for badge in payload["badges"])
 
 
 
250
  assert payload["training_artifacts"][0]["endpoint"] == "lora_dataset"
251
  assert payload["training_artifacts"][1]["endpoint"] == "/api/lora-training-kit.zip"
 
39
 
40
  assert payload["ok"] is True
41
  assert payload["projects"] == len(index.projects)
42
+ assert payload["index_algorithm"] == "llama-cpp-embedding-v1"
43
  assert payload["runtime"]["backend"] == "rules"
44
  assert len(payload["snapshot_digest"]) == 64
45
 
 
47
  def test_bootstrap_exposes_index_metadata() -> None:
48
  payload = bootstrap()
49
 
50
+ assert payload["index_algorithm"] == "llama-cpp-embedding-v1"
51
  assert payload["index_generated_at"]
52
  assert payload["snapshot_digest"]
53
  assert payload["runtime"]["tool_count"] >= 8
 
247
  assert payload["runtime"]["backend"] == "rules"
248
  assert payload["tiny_titan_eligible"] is True
249
  assert any(badge["name"] == "Sharing is Caring" for badge in payload["badges"])
250
+ assert {badge["name"]: badge["status"] for badge in payload["badges"]}["Llama Champion"] == "ready"
251
+ assert payload["retrieval_index"]["index_algorithm"] == "llama-cpp-embedding-v1"
252
+ assert payload["retrieval_index"]["embedding_runtime"] == "llama.cpp via llama-cpp-python"
253
  assert payload["training_artifacts"][0]["endpoint"] == "lora_dataset"
254
  assert payload["training_artifacts"][1]["endpoint"] == "/api/lora-training-kit.zip"
tests/test_artifact_bundle.py CHANGED
@@ -1,6 +1,8 @@
1
  import json
2
  from io import BytesIO
3
  from pathlib import Path
 
 
4
  from zipfile import ZipFile
5
 
6
  from hackathon_advisor.agent import AdvisorEngine
@@ -12,7 +14,7 @@ from hackathon_advisor.trace_export import trace_metadata
12
 
13
 
14
  def test_demo_bundle_contains_submission_evidence_files() -> None:
15
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
16
  engine = AdvisorEngine(index)
17
  metadata = {
18
  **trace_metadata(index),
 
1
  import json
2
  from io import BytesIO
3
  from pathlib import Path
4
+
5
+ from tests.helpers import load_test_index
6
  from zipfile import ZipFile
7
 
8
  from hackathon_advisor.agent import AdvisorEngine
 
14
 
15
 
16
  def test_demo_bundle_contains_submission_evidence_files() -> None:
17
+ index = load_test_index()
18
  engine = AdvisorEngine(index)
19
  metadata = {
20
  **trace_metadata(index),
tests/test_chapter.py CHANGED
@@ -1,5 +1,8 @@
 
1
  from pathlib import Path
2
 
 
 
3
  from hackathon_advisor.agent import AdvisorEngine
4
  from hackathon_advisor.chapter import build_chapter_markdown
5
  from hackathon_advisor.data import ProjectIndex
@@ -7,7 +10,7 @@ from hackathon_advisor.trace_export import trace_metadata
7
 
8
 
9
  def test_chapter_markdown_contains_idea_pages_and_citations() -> None:
10
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
11
  engine = AdvisorEngine(index)
12
  state = engine.turn("A local-first archive cartographer for family photos", {}).state
13
  state = engine.turn("write bolder and find whitespace", state).state
@@ -26,7 +29,7 @@ def test_chapter_markdown_contains_idea_pages_and_citations() -> None:
26
  assert "Goals:" in markdown
27
  assert "Targets:" not in markdown
28
  assert "Closest cited pages:" in markdown
29
- assert "Page 30:" in markdown
30
 
31
 
32
  def test_empty_chapter_markdown_is_explicit() -> None:
 
1
+ import re
2
  from pathlib import Path
3
 
4
+ from tests.helpers import load_test_index
5
+
6
  from hackathon_advisor.agent import AdvisorEngine
7
  from hackathon_advisor.chapter import build_chapter_markdown
8
  from hackathon_advisor.data import ProjectIndex
 
10
 
11
 
12
  def test_chapter_markdown_contains_idea_pages_and_citations() -> None:
13
+ index = load_test_index()
14
  engine = AdvisorEngine(index)
15
  state = engine.turn("A local-first archive cartographer for family photos", {}).state
16
  state = engine.turn("write bolder and find whitespace", state).state
 
29
  assert "Goals:" in markdown
30
  assert "Targets:" not in markdown
31
  assert "Closest cited pages:" in markdown
32
+ assert re.search(r"Page \d+:", markdown)
33
 
34
 
35
  def test_empty_chapter_markdown_is_explicit() -> None:
tests/test_data.py CHANGED
@@ -1,22 +1,24 @@
1
  from pathlib import Path
 
 
2
  import json
3
 
4
  from hackathon_advisor.data import Project, ProjectIndex, public_project_summary, public_project_title
5
 
6
 
7
  def test_project_index_searches_snapshot() -> None:
8
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
9
 
10
  hits = index.search("lullaby children audio", limit=3)
11
 
12
  assert hits
13
  assert hits[0].project.id.startswith("build-small-hackathon/")
14
  assert hits[0].page_number >= 1
15
- assert index.index_algorithm == "tfidf-sparse-v1"
16
 
17
 
18
  def test_project_index_whitespace() -> None:
19
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
20
 
21
  items = index.find_whitespace(limit=3)
22
 
 
1
  from pathlib import Path
2
+
3
+ from tests.helpers import load_test_index
4
  import json
5
 
6
  from hackathon_advisor.data import Project, ProjectIndex, public_project_summary, public_project_title
7
 
8
 
9
  def test_project_index_searches_snapshot() -> None:
10
+ index = load_test_index()
11
 
12
  hits = index.search("lullaby children audio", limit=3)
13
 
14
  assert hits
15
  assert hits[0].project.id.startswith("build-small-hackathon/")
16
  assert hits[0].page_number >= 1
17
+ assert index.index_algorithm == "llama-cpp-embedding-v1"
18
 
19
 
20
  def test_project_index_whitespace() -> None:
21
+ index = load_test_index()
22
 
23
  items = index.find_whitespace(limit=3)
24
 
tests/test_demo_rehearsal.py CHANGED
@@ -1,12 +1,14 @@
1
  from pathlib import Path
2
 
 
 
3
  from hackathon_advisor.agent import AdvisorEngine
4
  from hackathon_advisor.data import ProjectIndex
5
  from hackathon_advisor.demo_rehearsal import DEMO_GOALS, build_demo_rehearsal
6
 
7
 
8
  def test_demo_rehearsal_builds_complete_session() -> None:
9
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
10
  engine = AdvisorEngine(index)
11
 
12
  payload = build_demo_rehearsal(engine)
 
1
  from pathlib import Path
2
 
3
+ from tests.helpers import load_test_index
4
+
5
  from hackathon_advisor.agent import AdvisorEngine
6
  from hackathon_advisor.data import ProjectIndex
7
  from hackathon_advisor.demo_rehearsal import DEMO_GOALS, build_demo_rehearsal
8
 
9
 
10
  def test_demo_rehearsal_builds_complete_session() -> None:
11
+ index = load_test_index()
12
  engine = AdvisorEngine(index)
13
 
14
  payload = build_demo_rehearsal(engine)
tests/test_field_notes.py CHANGED
@@ -1,5 +1,7 @@
1
  from pathlib import Path
2
 
 
 
3
  from hackathon_advisor.agent import AdvisorEngine
4
  from hackathon_advisor.data import ProjectIndex
5
  from hackathon_advisor.field_notes import build_field_notes_markdown
@@ -7,7 +9,7 @@ from hackathon_advisor.trace_export import trace_metadata
7
 
8
 
9
  def test_field_notes_markdown_contains_session_decisions() -> None:
10
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
11
  engine = AdvisorEngine(index)
12
  state = {
13
  "profile": {"skills": "frontend prototyping"},
 
1
  from pathlib import Path
2
 
3
+ from tests.helpers import load_test_index
4
+
5
  from hackathon_advisor.agent import AdvisorEngine
6
  from hackathon_advisor.data import ProjectIndex
7
  from hackathon_advisor.field_notes import build_field_notes_markdown
 
9
 
10
 
11
  def test_field_notes_markdown_contains_session_decisions() -> None:
12
+ index = load_test_index()
13
  engine = AdvisorEngine(index)
14
  state = {
15
  "profile": {"skills": "frontend prototyping"},
tests/test_lora_dataset.py CHANGED
@@ -1,6 +1,8 @@
1
  import json
2
  from pathlib import Path
3
 
 
 
4
  from hackathon_advisor.agent import AdvisorEngine
5
  from hackathon_advisor.data import ProjectIndex
6
  from hackathon_advisor.lora_dataset import BASE_MODEL, build_lora_dataset_jsonl
@@ -8,7 +10,7 @@ from hackathon_advisor.trace_export import trace_metadata
8
 
9
 
10
  def test_lora_dataset_exports_tool_call_and_response_examples() -> None:
11
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
12
  engine = AdvisorEngine(index)
13
  state = {"goals": ["Well-Tuned", "Field Notes"]}
14
  state = engine.turn("A local-first archive cartographer for family photos", state).state
@@ -23,7 +25,7 @@ def test_lora_dataset_exports_tool_call_and_response_examples() -> None:
23
  assert manifest["record_kinds"] == ["tool_call", "advisor_response"]
24
  assert manifest["example_count"] == len(examples)
25
  assert manifest["included_turn_count"] == 2
26
- assert manifest["index"]["algorithm"] == "tfidf-sparse-v1"
27
  assert {example["example_kind"] for example in examples} == {"tool_call", "advisor_response"}
28
  assert examples[0]["messages"][2]["content"].startswith('<function name="save_idea">')
29
  assert examples[0]["goals"] == ["Well-Tuned", "Field Notes"]
@@ -41,7 +43,7 @@ def test_empty_lora_dataset_only_exports_manifest() -> None:
41
  payload = build_lora_dataset_jsonl(
42
  {},
43
  {
44
- "index_algorithm": "tfidf-sparse-v1",
45
  "snapshot_generated_at": "2026-06-06T00:00:00+00:00",
46
  "index_generated_at": "2026-06-06T01:00:00+00:00",
47
  "snapshot_digest": "abc",
 
1
  import json
2
  from pathlib import Path
3
 
4
+ from tests.helpers import load_test_index
5
+
6
  from hackathon_advisor.agent import AdvisorEngine
7
  from hackathon_advisor.data import ProjectIndex
8
  from hackathon_advisor.lora_dataset import BASE_MODEL, build_lora_dataset_jsonl
 
10
 
11
 
12
  def test_lora_dataset_exports_tool_call_and_response_examples() -> None:
13
+ index = load_test_index()
14
  engine = AdvisorEngine(index)
15
  state = {"goals": ["Well-Tuned", "Field Notes"]}
16
  state = engine.turn("A local-first archive cartographer for family photos", state).state
 
25
  assert manifest["record_kinds"] == ["tool_call", "advisor_response"]
26
  assert manifest["example_count"] == len(examples)
27
  assert manifest["included_turn_count"] == 2
28
+ assert manifest["index"]["algorithm"] == "llama-cpp-embedding-v1"
29
  assert {example["example_kind"] for example in examples} == {"tool_call", "advisor_response"}
30
  assert examples[0]["messages"][2]["content"].startswith('<function name="save_idea">')
31
  assert examples[0]["goals"] == ["Well-Tuned", "Field Notes"]
 
43
  payload = build_lora_dataset_jsonl(
44
  {},
45
  {
46
+ "index_algorithm": "llama-cpp-embedding-v1",
47
  "snapshot_generated_at": "2026-06-06T00:00:00+00:00",
48
  "index_generated_at": "2026-06-06T01:00:00+00:00",
49
  "snapshot_digest": "abc",
tests/test_lora_training_kit.py CHANGED
@@ -3,6 +3,8 @@ import subprocess
3
  import sys
4
  from io import BytesIO
5
  from pathlib import Path
 
 
6
  from zipfile import ZipFile
7
 
8
  from hackathon_advisor.agent import AdvisorEngine
@@ -18,7 +20,7 @@ from hackathon_advisor.trace_export import trace_metadata
18
 
19
 
20
  def test_lora_training_kit_contains_recipe_and_model_card() -> None:
21
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
22
  engine = AdvisorEngine(index)
23
  metadata = {
24
  **trace_metadata(index),
@@ -67,7 +69,7 @@ def test_parse_lora_dataset_jsonl_rejects_empty_payload() -> None:
67
 
68
 
69
  def test_train_minicpm_lora_dry_run_writes_recipe(tmp_path: Path) -> None:
70
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
71
  engine = AdvisorEngine(index)
72
  metadata = {
73
  **trace_metadata(index),
 
3
  import sys
4
  from io import BytesIO
5
  from pathlib import Path
6
+
7
+ from tests.helpers import load_test_index
8
  from zipfile import ZipFile
9
 
10
  from hackathon_advisor.agent import AdvisorEngine
 
20
 
21
 
22
  def test_lora_training_kit_contains_recipe_and_model_card() -> None:
23
+ index = load_test_index()
24
  engine = AdvisorEngine(index)
25
  metadata = {
26
  **trace_metadata(index),
 
69
 
70
 
71
  def test_train_minicpm_lora_dry_run_writes_recipe(tmp_path: Path) -> None:
72
+ index = load_test_index()
73
  engine = AdvisorEngine(index)
74
  metadata = {
75
  **trace_metadata(index),
tests/test_prize_ledger.py CHANGED
@@ -2,7 +2,10 @@ from hackathon_advisor.prize_ledger import prize_ledger
2
 
3
 
4
  def test_prize_ledger_tracks_param_budget_and_badges() -> None:
5
- payload = prize_ledger({"backend": "rules", "model_id": "deterministic-tool-router"})
 
 
 
6
 
7
  assert payload["runtime"]["backend"] == "rules"
8
  assert payload["total_params_b"] <= payload["tiny_titan_limit_b"]
@@ -11,6 +14,8 @@ def test_prize_ledger_tracks_param_budget_and_badges() -> None:
11
  badges = {badge["name"]: badge["status"] for badge in payload["badges"]}
12
  assert badges["Off the Grid"] == "ready"
13
  assert badges["Well-Tuned"] == "ready"
 
 
14
  assert payload["training_artifacts"][0]["base_model"] == "openbmb/MiniCPM5-1B"
15
  assert payload["training_artifacts"][1]["format"] == "zip"
16
  assert payload["training_artifacts"][1]["adapter_repo"] == "build-small-hackathon/hackathon-advisor-minicpm5-lora"
 
2
 
3
 
4
  def test_prize_ledger_tracks_param_budget_and_badges() -> None:
5
+ payload = prize_ledger(
6
+ {"backend": "rules", "model_id": "deterministic-tool-router"},
7
+ {"index_algorithm": "llama-cpp-embedding-v1"},
8
+ )
9
 
10
  assert payload["runtime"]["backend"] == "rules"
11
  assert payload["total_params_b"] <= payload["tiny_titan_limit_b"]
 
14
  badges = {badge["name"]: badge["status"] for badge in payload["badges"]}
15
  assert badges["Off the Grid"] == "ready"
16
  assert badges["Well-Tuned"] == "ready"
17
+ assert badges["Llama Champion"] == "ready"
18
+ assert payload["retrieval_index"]["index_algorithm"] == "llama-cpp-embedding-v1"
19
  assert payload["training_artifacts"][0]["base_model"] == "openbmb/MiniCPM5-1B"
20
  assert payload["training_artifacts"][1]["format"] == "zip"
21
  assert payload["training_artifacts"][1]["adapter_repo"] == "build-small-hackathon/hackathon-advisor-minicpm5-lora"
tests/test_submission_packet.py CHANGED
@@ -1,5 +1,7 @@
1
  from pathlib import Path
2
 
 
 
3
  from hackathon_advisor.agent import AdvisorEngine
4
  from hackathon_advisor.data import ProjectIndex
5
  from hackathon_advisor.prize_ledger import prize_ledger
@@ -8,7 +10,7 @@ from hackathon_advisor.trace_export import trace_metadata
8
 
9
 
10
  def test_submission_packet_contains_demo_and_prize_evidence() -> None:
11
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
12
  engine = AdvisorEngine(index)
13
  state = {"goals": ["Well-Tuned", "Field Notes"]}
14
  state = engine.turn("A local-first archive cartographer for family photos", state).state
@@ -44,7 +46,7 @@ def test_empty_submission_packet_is_honest_about_missing_session_artifacts() ->
44
  {
45
  "snapshot_generated_at": "2026-06-06T00:00:00+00:00",
46
  "project_count": 100,
47
- "index_algorithm": "tfidf-sparse-v1",
48
  "index_generated_at": "2026-06-06T01:00:00+00:00",
49
  "snapshot_digest": "abc",
50
  },
 
1
  from pathlib import Path
2
 
3
+ from tests.helpers import load_test_index
4
+
5
  from hackathon_advisor.agent import AdvisorEngine
6
  from hackathon_advisor.data import ProjectIndex
7
  from hackathon_advisor.prize_ledger import prize_ledger
 
10
 
11
 
12
  def test_submission_packet_contains_demo_and_prize_evidence() -> None:
13
+ index = load_test_index()
14
  engine = AdvisorEngine(index)
15
  state = {"goals": ["Well-Tuned", "Field Notes"]}
16
  state = engine.turn("A local-first archive cartographer for family photos", state).state
 
46
  {
47
  "snapshot_generated_at": "2026-06-06T00:00:00+00:00",
48
  "project_count": 100,
49
+ "index_algorithm": "llama-cpp-embedding-v1",
50
  "index_generated_at": "2026-06-06T01:00:00+00:00",
51
  "snapshot_digest": "abc",
52
  },
tests/test_trace_export.py CHANGED
@@ -1,13 +1,15 @@
1
  import json
2
  from pathlib import Path
3
 
 
 
4
  from hackathon_advisor.agent import AdvisorEngine
5
  from hackathon_advisor.data import ProjectIndex
6
  from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
7
 
8
 
9
  def test_trace_jsonl_contains_manifest_and_turns() -> None:
10
- index = ProjectIndex.from_files(Path("data/projects.json"), Path("data/project_index.json"))
11
  engine = AdvisorEngine(index)
12
  state = engine.turn("A local-first archive cartographer for family photos", {}).state
13
  state = engine.turn("make a build plan", state).state
@@ -16,7 +18,7 @@ def test_trace_jsonl_contains_manifest_and_turns() -> None:
16
 
17
  assert lines[0]["type"] == "trace_manifest"
18
  assert lines[0]["turn_count"] == 2
19
- assert lines[0]["index"]["algorithm"] == "tfidf-sparse-v1"
20
  assert lines[1]["type"] == "agent_turn"
21
  assert lines[1]["tools"]
22
  assert lines[1]["tool_resolution"]["call"]["name"] == "save_idea"
 
1
  import json
2
  from pathlib import Path
3
 
4
+ from tests.helpers import load_test_index
5
+
6
  from hackathon_advisor.agent import AdvisorEngine
7
  from hackathon_advisor.data import ProjectIndex
8
  from hackathon_advisor.trace_export import build_trace_jsonl, trace_metadata
9
 
10
 
11
  def test_trace_jsonl_contains_manifest_and_turns() -> None:
12
+ index = load_test_index()
13
  engine = AdvisorEngine(index)
14
  state = engine.turn("A local-first archive cartographer for family photos", {}).state
15
  state = engine.turn("make a build plan", state).state
 
18
 
19
  assert lines[0]["type"] == "trace_manifest"
20
  assert lines[0]["turn_count"] == 2
21
+ assert lines[0]["index"]["algorithm"] == "llama-cpp-embedding-v1"
22
  assert lines[1]["type"] == "agent_turn"
23
  assert lines[1]["tools"]
24
  assert lines[1]["tool_resolution"]["call"]["name"] == "save_idea"