Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
38a9e61
chore(autodev): adapt plan paths for container + mark GPU steps [manual]
Jul 24, 2026
8b95a9b
chore: add vitest and better-sqlite3 dev deps for eval collector
Jul 24, 2026
08d024d
chore: add test script to package.json for vitest
Jul 24, 2026
02c3657
chore: add vitest.config.ts for eval collector test harness
Jul 24, 2026
2f66590
chore: verify vitest runner exits cleanly with no tests
Jul 24, 2026
92c665b
chore: mark Task 0 Step 5 (commit) complete in eval plan
Jul 24, 2026
8929413
test(collector): add failing scoring.ts test suite
Jul 24, 2026
eb51e6a
chore(collector): verify scoring.ts test fails before implementation
Jul 24, 2026
3f46008
feat(collector): implement scoring.ts (Task 1 Step 3)
Jul 24, 2026
9f04157
chore(collector): verify scoring.ts test passes (Task 1 Step 4)
Jul 24, 2026
e164d81
chore: mark Task 1 Step 5 (commit) complete in eval plan
Jul 24, 2026
0a9d60d
chore(eval-dist): add seed questions.json fixture (Task 2 Step 1)
Jul 24, 2026
854f0fc
test(collector): add failing test for questions.ts QuestionSet (Task …
Jul 24, 2026
ed89c2b
chore(eval-dist): verify questions.ts test fails (Task 2 Step 3)
Jul 24, 2026
034dfd1
feat(collector): implement questions.ts QuestionSet (Task 2 Step 4)
Jul 24, 2026
0ea00b9
test(collector): verify questions.ts test passes (Task 2 Step 5)
Jul 24, 2026
4e844f1
docs(plan): check off Task 2 Step 6 (already committed in Steps 1-5)
Jul 24, 2026
3c6ebe2
test(collector): add failing test for store.ts CollectorStore
Jul 24, 2026
080f507
chore(collector): verify store.ts test fails (Task 3 Step 2)
Jul 24, 2026
198ed55
feat(collector): implement store.ts CollectorStore (Task 3 Step 3)
Jul 24, 2026
21d1ebc
test(collector): verify store.ts test passes (Task 3 Step 4)
Jul 24, 2026
8bd11b0
docs(plan): check off Task 3 Step 5 (already committed in Steps 2-3)
Jul 24, 2026
621e1ee
feat(collector): serve bundled question set from /queries
Jul 24, 2026
f474c1a
test(collector): verify /api/external/queries type-checks (Task 4 Ste…
Jul 24, 2026
f5923b3
docs(plan): check off Task 4 Step 3 (already committed as 621e1ee)
Jul 24, 2026
b79e940
feat(collector): add POST /api/external/run handler (Task 5 Step 1)
Jul 24, 2026
7cf79e7
test(collector): verify /api/external/run type-checks (Task 5 Step 2)
Jul 24, 2026
9f57c2a
docs(plan): check off Task 5 Step 3 (already committed as b79e940)
Jul 24, 2026
706eeea
feat(collector): assemble report.json from collector store (Task 6 St…
Jul 24, 2026
cac34ca
test(collector): verify /api/external/report type-checks (Task 6 Step 2)
Jul 24, 2026
78268bb
docs(plan): check off Task 6 Step 3 (already committed as 706eeea/cac…
Jul 24, 2026
00ed38f
chore(collector): remove obsolete judge/evaluate endpoints and pg ext…
Jul 24, 2026
9c76f21
docs(plan): check off Task 7 Step 2 (verified only hooks.server.ts st…
Jul 24, 2026
bb258f7
docs(plan): check off Task 7 Step 3 (already committed as 00ed38f)
Jul 24, 2026
42c3d21
feat(collector): mode-gated startup (sqlite collector vs postgres)
Jul 24, 2026
8366e41
docs(plan): check off Task 8 Step 2 (type-check + full test run pass)
Jul 24, 2026
f32dc04
chore(autodev): skip sandbox-unrunnable local smoke tests (Node18/nes…
Jul 24, 2026
cb773f9
docs(plan): check off Task 8 Step 4 (already committed as 42c3d21)
Jul 24, 2026
bac9cf0
feat(collector): add export-questions script (Task 9 Step 1)
Jul 24, 2026
78aa27e
chore(collector): remove old export-queries script (Task 9 Step 2)
Jul 24, 2026
c951be6
fix(collector): export-questions uses standalone pg client (Task 9 St…
Jul 24, 2026
0ec9d1c
docs(plan): check off Task 9 Step 4 (already committed across Steps 1-3)
Jul 24, 2026
42d7f3a
feat(collector): slim compose stack to single eval-server service
Jul 24, 2026
5cec54e
chore(collector): remove obsolete ollama entrypoint and queries.sql
Jul 24, 2026
c387229
docs(plan): check off Task 10 Step 3 (already committed as 42d7f3a/5c…
Jul 24, 2026
3a0e194
feat(collector): bundle seed/questions.json into runtime image
Jul 24, 2026
0507389
feat(collector): assert better-sqlite3 loads in runtime image
Jul 24, 2026
e809f69
docs(plan): check off Task 11 Step 4 (already committed as 3a0e194/05…
Jul 24, 2026
6200f3e
feat(collector): simplify .env.example for single-container flow
Jul 24, 2026
898dbd2
docs(collector): rewrite README for single-container flow
Jul 24, 2026
b9843f0
docs(plan): check off Task 12 Step 3 (already committed as 6200f3e/89…
Jul 24, 2026
3e038a1
feat(compare): add shared report/judged types for compare feature
Jul 24, 2026
5e5df06
test(compare): add failing test for judged-row aggregation
Jul 24, 2026
710d072
docs(plan): check off Task 13 Step 3 (test fails as expected, module …
Jul 24, 2026
0407f5b
feat(compare): implement aggregateJudged in aggregate.ts
Jul 24, 2026
bfd664a
docs(plan): check off Task 13 Step 5 (aggregate.test.ts passes)
Jul 24, 2026
a845076
docs(plan): check off Task 13 Step 6 (already committed as 3e038a1/5e…
Jul 24, 2026
2d9cb5a
feat(compare): add judge-report.ts to judge a report's rows
Jul 24, 2026
38a92ef
docs(plan): check off Task 14 Step 2 (bun run check passes, 0 errors)
Jul 24, 2026
25aa618
docs(plan): check off Task 14 Step 3 (judge-report.ts already committ…
Jul 24, 2026
48d0089
feat(compare): add store.ts to persist comparisons in postgres
Jul 24, 2026
3f911c6
docs(plan): check off Task 15 Step 2 (bun run check passes, 0 errors)
Jul 24, 2026
aa5edcf
docs(plan): check off Task 15 Step 3 (store.ts already committed as 4…
Jul 24, 2026
8771431
feat(compare): add POST /api/compare endpoint
Jul 24, 2026
ac64656
feat(compare): add GET /api/compare/[id] endpoint
Jul 24, 2026
33a4c3a
docs(plan): check off Task 16 Step 3 (bun run check passes, 0 errors)
Jul 24, 2026
e695a3d
docs(plan): check off Task 16 Step 4 (endpoints already committed as …
Jul 24, 2026
eb062aa
feat(compare): add /compare page server load for prior comparisons
Jul 24, 2026
63fa01e
feat(compare): add /compare +page.svelte with upload + side-by-side UI
Jul 24, 2026
7f7c856
docs(plan): check off Task 17 Step 3 (bun run check passes, 0 errors)
Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 171 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

154 changes: 77 additions & 77 deletions docs/superpowers/plans/2026-07-23-partner-eval-comparison.md

Large diffs are not rendered by default.

10 changes: 4 additions & 6 deletions eval-dist/.env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# Bearer token the client uses to authenticate against /api/external/*.
# Pick a long random string; share it out-of-band with the eval operator
# so both sides know they are talking to the right deployment.
EVAL_API_KEY=replace-with-long-random-string
# Shared secret. Must match the Authorization: Bearer token your app sends.
EVAL_API_KEY=change-me-to-a-long-random-string

# Judge model. Default fits in ~6GB VRAM (GTX 1660 Super-class).
OLLAMA_JUDGE_MODEL=qwen2.5:7b
# Label written into the report so the two systems are distinguishable.
SYSTEM_LABEL=partner-org
4 changes: 4 additions & 0 deletions eval-dist/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,16 @@ ENV EVAL_SERVER_MODE=1
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/build ./build
COPY --from=build /app/package.json ./package.json
COPY --from=build /app/eval-dist/seed/questions.json ./seed/questions.json
RUN mkdir -p /app/data

# Drop any leftover dev artifacts and verify no .ts / .map remain
RUN find . -name '*.ts' -not -path './node_modules/*' -delete \
&& find . -name '*.map' -not -path './node_modules/*' -delete \
&& find . -name 'tsconfig*.json' -not -path './node_modules/*' -delete \
&& find . -name 'svelte.config*' -not -path './node_modules/*' -delete

RUN node -e "require('better-sqlite3'); console.log('better-sqlite3 OK')"

EXPOSE 3000
CMD ["node", "build/index.js"]
221 changes: 51 additions & 170 deletions eval-dist/README.md
Original file line number Diff line number Diff line change
@@ -1,217 +1,98 @@
# web-rag eval-server
# web-rag eval collector

A self-contained Docker bundle that scores **your** RAG system on the same
test queries and with the same LLM-as-judge methodology used by the
operator's reference system. You run it locally, point your retrieval +
generation code at it, and send back the resulting JSON report. The
operator runs the equivalent evaluation on their side and compares the two
reports.

The container only exposes four HTTP endpoints under `/api/external/*`.
There is no UI, no shell, and no access to the operator's source code,
prompts, or pipelines.

---
A single, self-contained container that scores **your** RAG system on a shared
set of website questions using the same retrieval metrics as the operator's
reference system. It needs **no GPU, no model download, and no database of its
own**. You run it locally, POST each answered question to it, then export one
`report.json` and send it back. The operator runs the LLM-as-judge over both
reports and produces the side-by-side comparison.

## 1. Prerequisites

- Docker 24+ and `docker compose`
- ~12 GB free disk (image + judge model weights)
- NVIDIA GPU recommended (CPU works but is ~10× slower for the judge step)
- A `read:packages` PAT for `ghcr.io` — see [`LICENSE`](./LICENSE)

---

## 2. Pull and start

## 2. Start
```bash
# Authenticate against GHCR using the credentials in LICENSE
echo "$GHCR_PAT" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin

# Configure
cp .env.example .env
# → edit .env and set EVAL_API_KEY to a long random string

# Boot (CPU)
cp .env.example .env # set EVAL_API_KEY and SYSTEM_LABEL
docker compose up -d
# …or with NVIDIA GPU
docker compose --profile gpu up -d

# Wait for the judge model to download (first run only, ~5 min)
docker compose logs -f ollama
```

The eval-server listens on `127.0.0.1:3000`. Verify it is up:

The collector listens on `127.0.0.1:3000`. Verify:
```bash
curl -H "Authorization: Bearer $EVAL_API_KEY" \
http://127.0.0.1:3000/api/external/queries | jq '.queries | length'
```

---

## 3. The four endpoints

All endpoints require `Authorization: Bearer $EVAL_API_KEY`.
## 3. Endpoints
All require `Authorization: Bearer $EVAL_API_KEY`.

### `GET /api/external/queries`
Returns the shared test query set. Every query carries the ground-truth
URLs the operator's documents resolve to. **You must ingest the same URLs
into your own system before evaluating** so retrieval can be compared.

Returns the shared question set + ground-truth URLs. Ingest the same source
URLs into your system so retrieval can be compared.
```json
{ "queries": [
{ "id": "…", "query": "…", "category": "…", "difficulty": "easy",
"expectedUrls": ["https://…", "https://…"] }
]}
{ "questionSetVersion": "v1",
"queries": [{ "id": "q1", "query": "…", "category": "…",
"difficulty": "easy", "expectedUrls": ["https://…"] }] }
```

### `POST /api/external/evaluate`
Computes retrieval metrics for one query from the document URLs your
retriever returned.

### `POST /api/external/run`
Call once per answered question, at answer-generation time.
```json
{
"sessionId": "client-run-2026-06-29",
"queryId": "<id from /queries>",
"query": "<text from /queries>",
"expectedUrls": ["https://…"],
"retrievedUrls": ["https://…", "https://…", "…"],
"k": 10
"sessionId": "run-2026-07-23",
"systemLabel": "partner-org",
"queryId": "q1",
"answer": "<your model's answer>",
"context": "<context your retriever passed to the LLM>",
"retrievedUrls": ["https://…", "…"],
"latency": { "retrievalMs": 120, "generationMs": 800 }
}
```
Returns Recall@5/10, MRR, nDCG, Hit@1/5/10. URLs are normalised
(lowercased, trailing slash stripped) before matching, so trivial
differences are fine.

### `POST /api/external/judge`
Runs the LLM-as-judge (3× with majority vote) over your generated answer.

```json
{
"sessionId": "client-run-2026-06-29",
"queryId": "<id from /queries>",
"query": "<question>",
"context": "<context your retriever passed to the LLM>",
"answer": "<your model's answer>",
"judgeRuns": 3,
"recallAtK": 0.8
}
```
Returns groundedness, completeness, correctness, hallucination,
answerable-from-context, judgeMean, and an overall failureType
(`normal | generation_failure | retrieval_failure | robust_generation |
hallucination_failure`).
The container looks up the ground-truth URLs by `queryId`, computes
Recall@5/10, MRR, nDCG, Hit@1/5/10, records latency, and returns the metrics.

### `GET /api/external/report?sessionId=…`
Downloads the full JSON report (per-query rows + aggregate summary).
Send this file back to the operator.

---
Downloads the full `report.json` (per-query rows + aggregate summary incl.
latency p95). Send this file back to the operator.

## 4. Minimal Python integration

Add this single module to your project. It is the only client code you
have to write.

```python
# eval_client.py
import os, requests, uuid

BASE = os.environ["EVAL_BASE", "http://127.0.0.1:3000"]
import os, requests
BASE = os.environ.get("EVAL_BASE", "http://127.0.0.1:3000")
KEY = os.environ["EVAL_API_KEY"]
HDR = {"Authorization": f"Bearer {KEY}"}
SESSION = os.environ.get("EVAL_SESSION", f"client-{uuid.uuid4().hex[:8]}")
SESSION = os.environ.get("EVAL_SESSION", "run-1")
LABEL = os.environ.get("SYSTEM_LABEL", "partner-org")

def queries():
r = requests.get(f"{BASE}/api/external/queries", headers=HDR, timeout=30)
r.raise_for_status()
return r.json()["queries"]

def submit_retrieval(q, retrieved_urls, k=10):
r = requests.post(f"{BASE}/api/external/evaluate", headers=HDR, json={
"sessionId": SESSION,
"queryId": q["id"],
"query": q["query"],
"expectedUrls": q["expectedUrls"],
"retrievedUrls": retrieved_urls,
"k": k,
}, timeout=30)
r.raise_for_status()
return r.json()["metrics"]

def submit_judge(q, context, answer, recall_at_k):
r = requests.post(f"{BASE}/api/external/judge", headers=HDR, json={
"sessionId": SESSION,
"queryId": q["id"],
"query": q["query"],
"context": context,
"answer": answer,
"recallAtK": recall_at_k,
}, timeout=600)
r.raise_for_status()
return r.json()["scores"]

def fetch_report():
r = requests.get(f"{BASE}/api/external/report",
headers=HDR, params={"sessionId": SESSION}, timeout=60)
r.raise_for_status()
return r.json()
```
r.raise_for_status(); return r.json()["queries"]

Wire it into your existing run loop — typically a 5-line patch:

```python
from eval_client import queries, submit_retrieval, submit_judge, fetch_report
def submit(q, answer, context, retrieved_urls, retrieval_ms, generation_ms):
r = requests.post(f"{BASE}/api/external/run", headers=HDR, json={
"sessionId": SESSION, "systemLabel": LABEL, "queryId": q["id"],
"answer": answer, "context": context, "retrievedUrls": retrieved_urls,
"latency": {"retrievalMs": retrieval_ms, "generationMs": generation_ms},
}, timeout=60)
r.raise_for_status(); return r.json()

for q in queries():
hits = my_retriever(q["query"], top_k=10) # <— your code
ctx = "\n\n".join(h["text"] for h in hits)
answer = my_generator(q["query"], ctx) # <— your code

metrics = submit_retrieval(q, [h["url"] for h in hits])
scores = submit_judge(q, ctx, answer, recall_at_k=metrics["recallAt10"])

report = fetch_report()
open("report.json", "w").write(__import__("json").dumps(report, indent=2))
import time
t0 = time.time(); hits = my_retriever(q["query"], top_k=10); t1 = time.time()
ctx = "\n\n".join(h["text"] for h in hits)
answer = my_generator(q["query"], ctx); t2 = time.time()
submit(q, answer, ctx, [h["url"] for h in hits],
int((t1-t0)*1000), int((t2-t1)*1000))
```

That is the full client-side surface — no other code in your project needs
to change.

---

## 5. Sending the report back

When the loop finishes:

Fetch and send the report:
```bash
curl -H "Authorization: Bearer $EVAL_API_KEY" \
"http://127.0.0.1:3000/api/external/report?sessionId=$EVAL_SESSION" \
-o report.json
```

Send `report.json` to the operator (email, SFTP, signed S3 URL — whatever
was agreed). The operator runs the identical evaluation against the same
queries on their reference system and produces a side-by-side comparison.

---

## 6. Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `401 Unauthorized` | Wrong / missing bearer token | Re-export `EVAL_API_KEY` in your shell; it must match `.env` |
| `502` from `/api/external/judge` | Judge model not yet downloaded | `docker compose logs ollama` and wait for `Ollama ready` |
| All Recall@K = 0 | URL mismatch | Confirm `expectedUrls` and your `retrievedUrls` use the same canonical form (your system must ingest the same source URLs) |
| Slow judge calls | Running CPU profile | Use `docker compose --profile gpu up` if you have NVIDIA + nvidia-container-toolkit |

---

## 7. Tear-down

## 5. Tear-down
```bash
docker compose down -v # stops containers AND deletes the eval Postgres
docker compose down -v # stops the container AND deletes the SQLite scratch data
```

This wipes all locally-stored results. Do this after sending the report.
Loading