Spaces:
Running on Zero
Running on Zero
feat: build retrieval index with llama cpp
Browse filesCo-authored-by: Codex <noreply@openai.com>
- DESIGN.md +30 -60
- README.md +14 -7
- app.py +4 -4
- data/project_index.json +0 -0
- data/sample_trace.jsonl +4 -4
- hackathon_advisor/data.py +115 -62
- hackathon_advisor/llama_embedding.py +108 -0
- hackathon_advisor/prize_ledger.py +9 -15
- hackathon_advisor/trace_export.py +15 -1
- pyproject.toml +6 -0
- requirements.txt +1 -0
- scripts/build_project_index.py +68 -10
- scripts/modal_build_project_index.py +92 -0
- tests/__init__.py +1 -0
- tests/conftest.py +10 -0
- tests/helpers.py +26 -0
- tests/test_agent.py +20 -18
- tests/test_app.py +5 -2
- tests/test_artifact_bundle.py +3 -1
- tests/test_chapter.py +5 -2
- tests/test_data.py +5 -3
- tests/test_demo_rehearsal.py +3 -1
- tests/test_field_notes.py +3 -1
- tests/test_lora_dataset.py +5 -3
- tests/test_lora_training_kit.py +4 -2
- tests/test_prize_ledger.py +6 -1
- tests/test_submission_packet.py +4 -2
- tests/test_trace_export.py +4 -2
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 | **`
|
| 132 |
-
| Fine-tune
|
| 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
|
| 154 |
-
|
| 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 — `
|
| 225 |
|
| 226 |
-
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 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
|
| 240 |
-
llama.cpp
|
| 241 |
|
| 242 |
| Model | llama.cpp? | Runtime | Notes |
|
| 243 |
|---|---|---|---|
|
| 244 |
-
| `openbmb/MiniCPM5-1B` | ✅ | llama.cpp / Ollama |
|
| 245 |
-
| `
|
| 246 |
| ASR (Nemotron / Parakeet) | ❌ | NeMo / transformers | FastConformer-RNNT |
|
| 247 |
| `pipecat-ai/smart-turn-v3` | ❌ | ONNX Runtime | Whisper encoder + classifier head |
|
| 248 |
|
| 249 |
-
|
| 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
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 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 |
-
|
| 378 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) |
|
| 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 |
-
-
|
| 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 |
-
|
| 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
|
|
|
|
|
|
|
| 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
|
| 140 |
-
the user-facing app stays centered on idea evaluation. The main `/api/bootstrap` payload does
|
|
|
|
| 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=
|
| 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.
|
|
|
|
|
|
|
| 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-
|
| 2 |
-
{"artifact_title": "A local-first archive cartographer for family photos", "input": "A local-first archive cartographer for family photos", "overall": 4.
|
| 3 |
-
{"artifact_title": "
|
| 4 |
-
{"artifact_title": "
|
|
|
|
| 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
|
| 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
|
|
|
|
| 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.
|
| 225 |
-
self.
|
| 226 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 265 |
-
query_norm = self._norm(query_doc)
|
| 266 |
hits: list[SearchHit] = []
|
| 267 |
-
for page_number, (project,
|
| 268 |
-
zip(self.projects, self.
|
| 269 |
start=1,
|
| 270 |
):
|
| 271 |
-
|
| 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=
|
| 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 -
|
| 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
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
}
|
| 339 |
indexed_documents = []
|
| 340 |
-
for project,
|
| 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 |
-
"
|
| 350 |
-
"
|
| 351 |
-
"
|
| 352 |
-
"weights": weights,
|
| 353 |
}
|
| 354 |
)
|
| 355 |
return {
|
| 356 |
-
"schema_version":
|
| 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 |
-
"
|
| 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") !=
|
| 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": "
|
| 17 |
-
"model": "
|
| 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": "
|
| 27 |
-
"runtime": "
|
| 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
|
| 44 |
},
|
| 45 |
{
|
| 46 |
"name": "Off-Brand",
|
|
@@ -69,8 +62,8 @@ BADGE_LEDGER = [
|
|
| 69 |
},
|
| 70 |
{
|
| 71 |
"name": "Llama Champion",
|
| 72 |
-
"status": "
|
| 73 |
-
"evidence": "
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
def main() -> None:
|
| 15 |
-
parser = argparse.ArgumentParser(
|
|
|
|
|
|
|
| 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 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
| 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['
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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"] == "
|
| 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"] == "
|
| 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 =
|
| 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 =
|
| 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 |
|
| 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 =
|
| 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 == "
|
| 16 |
|
| 17 |
|
| 18 |
def test_project_index_whitespace() -> None:
|
| 19 |
-
index =
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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"] == "
|
| 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": "
|
| 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 =
|
| 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 =
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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": "
|
| 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 =
|
| 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"] == "
|
| 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"
|