Skip to content

Commit 1aa6deb

Browse files
committed
docs(evaluation): feature guide and a sample dataset
Adds the Evaluation section to CLAUDE.md — the flow, the layering rule, why there are two promptfoo configs, and the contracts that are deliberately not configuration. `tests/evaluation/` ships a ready-made test set: 11 questions over 6 documents, every answer checked against the source rather than generated. The corpus itself is not committed (third-party PDFs, ~36 MB); `corpus.txt` lists the filenames and the README explains how to assemble and upload it. It lists 24 documents for 11 questions on purpose. Only 6 are the subject of a question; the other 18 are distractors, each topically adjacent to a question's source. A corpus where every document is on a different subject makes retrieval look better than it is — any half-working retriever scores a perfect hit rate when there is one candidate per topic. The repo ignores `*.csv`, so the committed test set is un-ignored explicitly: it is source, not scratch data.
1 parent cff68d6 commit 1aa6deb

5 files changed

Lines changed: 140 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ volumes/*
6565
!/services/.gitkeep # Keep the placeholder
6666

6767
*.csv
68+
# The committed evaluation test set is source, not scratch data.
69+
!tests/evaluation/*.csv
6870
*.pkl
6971

7072
#helm

CLAUDE.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,59 @@ Optional web search augmentation via the Staan API, allowing the LLM to combine
242242
- `openrag/services/orchestrators/query_service.py``_prepare_for_web_only()`, web search logic in `_prepare_for_chat_completion()`
243243
- `openrag/api/routers/user/chat.py``__prepare_sources()` merges document and web sources
244244

245+
### Evaluation (admin System page → Evaluation tab)
246+
247+
On-demand benchmarking of indexing speed, retrieval quality and answer quality.
248+
249+
**Flow** (`EvalRunner` Ray actor, `openrag/services/workers/eval_runner.py`): create the
250+
throwaway partition `__eval_<run_id>` → upload and time each corpus file over the real HTTP
251+
API → shell out to `promptfoo eval` twice → fold the outputs into metrics → drop the partition.
252+
253+
- **Layering**: `EvaluationService` never touches Ray. It dispatches through the
254+
`EvaluationRunner` port (`openrag/core/evaluation/runner.py`), implemented by
255+
`RayEvaluationRunner` (`openrag/services/workers/eval_dispatcher.py`) and injected by the
256+
container — the same port/adapter shape as `IndexingDispatcher`. The adapter resolves its
257+
detached actor on first use, so building the service does not spawn a worker.
258+
- **Datasets** are admin-uploaded: a corpus plus a CSV test set
259+
(`question,expected_answer,expected_file_ids`; the last column is optional and
260+
`;`-separated). Files live under `<data_dir>/eval/<dataset_id>/`. The corpus is
261+
streamed to disk rather than buffered, so its cap is a disk cost, not a RAM one.
262+
- **`file_id` sanitisation** (`sanitize_file_id`, `openrag/core/evaluation/identity.py`):
263+
the indexing API accepts only `[A-Za-z0-9._:-]` in a `file_id`, so a corpus file cannot be
264+
uploaded under a raw human filename. The runner uploads a sanitised id, and
265+
`metrics.summarize` sanitises **both** sides of the ground-truth comparison, so a test set
266+
naming `A B.pdf` still matches the stored `A_B.pdf`. Note `metadata.source` is the
267+
server's storage path, not the original name — the `file_id` is what a human sees.
268+
- **Two promptfoo configs, not one** (`openrag/core/evaluation/promptfoo_config.py`):
269+
retrieval hits `GET /search/partition/{partition}` (documents carry the chunk under
270+
`content`, plus `metadata.file_id`), answers hit `POST /v1/chat/completions`. Each config
271+
has one provider, so no assertion runs against an output shape it cannot read.
272+
`transformResponse` must be a single JavaScript **expression** — an IIFE or any statement
273+
makes promptfoo error every row before grading.
274+
- **Metrics** (`openrag/core/evaluation/metrics.py`): throughput from wall-clock, plus
275+
hit rate / MRR / recall using the definitions in
276+
`tests/load/automatic-evaluation-pipeline/README.md`. Rows without `expected_file_ids`
277+
are reported as `skipped_cases`, never as misses. Percentiles are nearest-rank
278+
(`ceil`) — `round` would break ties to even and report the wrong observation.
279+
- **Auth**: runs authenticate as the non-admin service user `__openrag_eval__`, whose token
280+
is regenerated at the start of every run, so no usable plaintext token is stored at rest.
281+
- **Concurrency**: one run at a time. Enforced by the partial unique index
282+
`ux_eval_runs_single_active`, not by a read-then-insert — the run row is created *before*
283+
the token is regenerated, so two racing starts cannot revoke each other's credentials.
284+
`POST /evaluation/runs` returns 409 on the loser, and 503 if the runner cannot be pinged.
285+
A run orphaned by an actor restart is reaped by cancelling it, which writes the terminal
286+
status directly; a failed provision releases the row the same way.
287+
- **Config** (`openrag/core/config/evaluation.py`, `evaluation:` in `conf/config.yaml`):
288+
limits and timeouts are env-overridable (`EVAL_*`, `PROMPTFOO_BIN`). The API base URL the
289+
runner calls back on is `server.internal_url` (env: `OPENRAG_INTERNAL_URL`) — it is a
290+
server property, not an eval one. The reserved partition prefix, CSV column names and the
291+
`file_id` alphabet are deliberately *not* config — they are contracts with stored datasets.
292+
- Requires **Node 22** (from NodeSource; distro packages predate promptfoo's floor) + a
293+
pinned promptfoo in **both** `infra/docker/api.Dockerfile` (compose runs Ray inside the
294+
API container) and `infra/docker/ray.Dockerfile` (separate Ray cluster). Dataset files are
295+
read from disk by the runner, so a **separate** Ray cluster needs `<data_dir>` on shared
296+
storage.
297+
245298
### File Quota System
246299

247300
Per-user file quota enforcement tracked via the `file_count` and `file_quota` columns on `users`, and `created_by` on `files`.

tests/evaluation/README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Sample evaluation dataset
2+
3+
`rag_dataset_sample.csv` is a ready-made test set for the admin **System →
4+
Evaluation** tab: 11 questions over 6 documents, every answer checked against
5+
the source PDF rather than generated.
6+
7+
The corpus itself is not committed — the documents are third-party PDFs
8+
totalling ~36 MB. `corpus.txt` lists the 24 filenames, drawn from the internal
9+
`rag_dataset` collection (French public-sector, agricultural, medical and AI
10+
documents).
11+
12+
## Why 24 documents for 11 questions
13+
14+
Only 6 documents are the subject of a question. The other 18 are deliberate
15+
distractors, each topically adjacent to a question's source — other AI-policy
16+
papers, other gut/neuro medical papers, other agricultural press releases. A
17+
corpus where every document is on a different subject makes retrieval look
18+
better than it is: any half-working retriever scores a perfect hit rate when
19+
there is only one candidate per topic.
20+
21+
## Assembling it
22+
23+
```bash
24+
mkdir -p /tmp/eval-corpus
25+
while IFS= read -r f; do cp "<path-to>/rag_dataset/$f" /tmp/eval-corpus/; done \
26+
< tests/evaluation/corpus.txt
27+
```
28+
29+
Then upload it, either through the Evaluation tab or the API:
30+
31+
```bash
32+
args=(); for f in /tmp/eval-corpus/*; do args+=(-F "corpus=@${f}"); done
33+
curl -X POST "$OPENRAG_URL/evaluation/datasets" \
34+
-H "Authorization: Bearer $AUTH_TOKEN" \
35+
-F "name=rag_dataset sample" \
36+
-F "testset=@tests/evaluation/rag_dataset_sample.csv" \
37+
"${args[@]}"
38+
```
39+
40+
## Test set format
41+
42+
`question,expected_answer,expected_file_ids``expected_file_ids` is optional
43+
and semicolon-separated. Name the files as they appear on disk; the indexer
44+
sanitises the id it stores (spaces are not valid in a `file_id`), and the
45+
ranking metrics match against the original filename in the chunk metadata.
46+
47+
Rows without `expected_file_ids` still count toward answer quality, but are
48+
reported as `skipped_cases` in hit rate / MRR / recall rather than scored as
49+
misses.

tests/evaluation/corpus.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2017-10-30-DP-sauvons-le-colza-francais.pdf
2+
2018-01-18-CP-red-2-energie-renouvelable-la-contribution-agricole-largement-reconnue.pdf
3+
2019-12-03-DP-la-semence-certifiee-de-soja.pdf
4+
2024-04-17-upcycling.pdf
5+
202407_charte-de-deontologie-AFCL.pdf
6+
20240930_Note_LINAGORA_IA_OpenSource_Universelle.pdf
7+
20241121_CP_ OSPX24_LUCIE-V2.pdf
8+
579_Urban-immunization-toolkit_final-1563547313.pdf
9+
9789240049130-eng.pdf
10+
Antiinflammatoire SCFA acetate propionate.pdf
11+
Competition_in_cloud_sector.pdf
12+
GAVI_use_case_Sample_draft.pdf
13+
IA Ethique.pdf
14+
Intelligence-artificielle-01-2024.pdf
15+
Kynurenine pathway ALS.pdf
16+
Leaky gut in systemic infammation.pdf
17+
Lignes-directrices-nouvelle-version-2024-10.pdf
18+
Make_France_AI_Powerhouse.pdf
19+
Manifeste-CannabiSante-PrincipesActifs.pdf
20+
Note Syndrome de Prader-Willi V2 Jan 22.pdf
21+
Note de positionnement Pensons Patients.pdf
22+
Note_aux_Operateurs_432_08_12_23.pdf
23+
Nouveau_Bigster_Dacia_en_grand.pdf
24+
ReST meets ReAct.pdf
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
question,expected_answer,expected_file_ids
2+
Quand la communauté OpenLLM France a-t-elle été créée et à l'initiative de quelle entreprise ?,En juin 2023 à l'impulsion de LINAGORA.,20241121_CP_ OSPX24_LUCIE-V2.pdf
3+
Quand le pré-entraînement du modèle LUCIE a-t-il démarré ?,Dès décembre 2023.,20241121_CP_ OSPX24_LUCIE-V2.pdf
4+
À quelle date se tient le Paris Open Source AI Summit ?,Le 22 janvier 2025.,20241121_CP_ OSPX24_LUCIE-V2.pdf
5+
Quelle est la fréquence à la naissance du syndrome de Prader-Willi ?,Environ une naissance sur 21 000.,Note Syndrome de Prader-Willi V2 Jan 22.pdf
6+
Sur quel chromosome se situe l'anomalie génétique à l'origine du syndrome de Prader-Willi ?,Le chromosome 15.,Note Syndrome de Prader-Willi V2 Jan 22.pdf
7+
Qui a décrit le syndrome de Prader-Willi et en quelle année ?,"En 1956, par trois médecins suisses : Andrea Prader, Alexis Labhart et Heinrich Willi.",Note Syndrome de Prader-Willi V2 Jan 22.pdf
8+
Quel pourcentage de biocarburants dans les transports le secteur agricole européen a-t-il permis d'atteindre selon le communiqué sur la directive RED II ?,"7,7 % de biocarburants dans les transports.",2018-01-18-CP-red-2-energie-renouvelable-la-contribution-agricole-largement-reconnue.pdf
9+
Quelle obligation d'énergie renouvelable dans les transports le Parlement européen a-t-il retenue dans la directive RED II ?,Une obligation de 12 % d'énergie renouvelable dans les transports.,2018-01-18-CP-red-2-energie-renouvelable-la-contribution-agricole-largement-reconnue.pdf
10+
Combien de morts par an les maladies cardioneurovasculaires causent-elles en France ?,Environ 140 000 morts par an.,Note de positionnement Pensons Patients.pdf
11+
À partir de quelle taille de population le président d'un établissement public de coopération intercommunale est-il visé par les incompatibilités de la charte de déontologie du conseil en lobbying ?,Plus de 100 000 habitants.,202407_charte-de-deontologie-AFCL.pdf
12+
Who is Tony in the GAVI use case scenario and what is his role?,"Tony is a 35-year-old community health worker in a remote rural village in Africa, managing a local health facility that is the first point of contact for village residents.",GAVI_use_case_Sample_draft.pdf

0 commit comments

Comments
 (0)