Evaluation¶
varagity.eval.datasets
¶
Golden Q&A dataset: loading, validation, and identity resolution (spec §16).
The golden set (data/eval/golden_qa.jsonl) is hand-authored over
tests/fixtures/corpus. Each line is one query with the chunks required
to answer it, identified portably by relative source path +
chunk_index — never by chunk_id directly, whose doc_id half
depends on file bytes:
{"query": "…", "relevant": [{"rel_source": "aurora_station.md", "chunk_index": 1}]}
rel_source is the document's POSIX path relative to the eval corpus
root — exactly the string the ingest loader hashes into doc_id (plan
decision #6) — so an entry resolves to concrete chunk_ids from the
corpus files alone: derive_doc_id(rel_source, content_hash(bytes))
plus the deterministic {doc_id}::{chunk_index} composition. No store
round-trip is needed, and the resolution is identical on every machine.
chunk_index values assume the chunk boundaries produced by the pinned
eval pipeline settings (recursive_character, size 400 / overlap 50 —
see varagity.eval.evaluate.PINNED_EVAL_SETTINGS); re-author the golden
set if those pins change.
The conversation fixtures (tests/fixtures/conversations/*.json,
spec_v3 §4.9) extend the same shape to multi-turn sequences: each turn is
a golden entry plus a scripted assistant reply and a kind tag on
follow-up turns (pronoun / elliptical / topic_shift). The
scripted replies are what both chat engines see as history — identical by
construction, so the chat eval compares engines, never their answers.
The graph golden set (data/eval/graph_golden_qa.jsonl,
spec_graphrag §12) anchors differently again, because a graph engine
returns prose rather than chunk ids: each entry carries
expected_facts (an AND of OR-groups matched case-insensitively against
the answer — :func:resolve_golden_by_fact's substring trick applied to
answers instead of chunks) and required_guids (the messages a
provenance-surfacing engine should cite). Its kind slices the report
into the question shapes the bake-off discriminates on and is golden
metadata only — never shown to an engine.
ConversationFixture
¶
Bases: BaseModel
One hand-authored multi-turn conversation over the eval corpus.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Unique fixture identity in reports. |
turns |
list[ConversationTurn]
|
The conversation's user turns, in order (≥ 2 — a single-turn conversation exercises no history). |
Source code in varagity/eval/datasets.py
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | |
ConversationTurn
¶
Bases: BaseModel
One user turn of a multi-turn eval conversation (spec_v3 §4.9).
Attributes:
| Name | Type | Description |
|---|---|---|
query |
str
|
The user's question for this turn, exactly as typed — follow-up turns deliberately keep their pronouns and ellipses; resolving them is what the eval measures. |
kind |
Literal['pronoun', 'elliptical', 'topic_shift'] | None
|
The follow-up case this turn exercises (one of
:data: |
assistant |
str | None
|
The scripted assistant reply appended to the history after this turn. Required on every non-final turn (the condenser resolves references against it); optional on the final turn, whose reply nothing reads. |
relevant |
list[GoldenChunkRef]
|
The chunks a perfect retriever would return for this turn (≥ 1), in the golden set's portable ref shape. |
Source code in varagity/eval/datasets.py
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | |
GoldenChunkRef
¶
Bases: BaseModel
Portable identity of one relevant chunk.
chunk_index anchors the ref to the pinned chunk boundaries (the
matrix's fixed configuration); fact anchors it to content, which
survives any boundary change — the chunker sweep re-resolves refs by
scanning the strategy-true chunks for the fact (spec_v2 §7.4), since an
index authored against one strategy's boundaries points at arbitrary
text under another's.
Attributes:
| Name | Type | Description |
|---|---|---|
rel_source |
str
|
Document path relative to the eval corpus root
(POSIX-style, e.g. |
chunk_index |
int
|
The chunk's position within its document (0-based), under the pinned eval chunk boundaries. |
fact |
str | None
|
A short verbatim snippet of the chunk's source text that uniquely identifies the golden content within its document (matched case-insensitively — OCR extraction may case-shift). Optional for the fixed matrix; required for a ref to participate in the chunker sweep. |
Source code in varagity/eval/datasets.py
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | |
GoldenEntry
¶
Bases: BaseModel
One golden query and every chunk required to answer it.
Attributes:
| Name | Type | Description |
|---|---|---|
query |
str
|
The evaluation question, as a user would ask it. |
relevant |
list[GoldenChunkRef]
|
The chunks a perfect retriever would return (≥ 1). |
Source code in varagity/eval/datasets.py
93 94 95 96 97 98 99 100 101 102 | |
GraphGoldenEntry
¶
Bases: BaseModel
One golden question over the synthetic message corpus (spec_graphrag §12).
Scoring is fact-anchored and house style — no LLM judge (plan decision
10). :attr:expected_facts is an AND of OR-groups: every group¶
must be satisfied, and a group is satisfied when any of its variants is
a case-insensitive substring of the engine's answer. Variants exist so
that a right answer phrased differently still scores ("march 14" /
"3/14"), not so that a wrong one squeaks through — keep them
specific enough that a distractor-fooled answer fails.
Attributes:
| Name | Type | Description |
|---|---|---|
query |
str
|
The question, exactly as it is put to the engine. |
kind |
Literal['aggregation', 'verification', 'relation']
|
The question shape this entry exercises (one of
:data: |
expected_facts |
list[list[str]]
|
One OR-group of acceptable variants per fact the answer must contain (≥ 1 group, each ≥ 1 non-blank variant). |
required_guids |
list[str]
|
Message guids a perfect provenance answer would
cite (≥ 1). Engines that surface no source ids score |
notes |
str | None
|
Why the entry is discriminative — which distractors it is built to defeat. Authoring documentation, never scored. |
Source code in varagity/eval/datasets.py
313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 | |
ResolvedGoldenEntry
¶
Bases: BaseModel
A golden entry with its refs resolved to concrete chunk_ids.
Attributes:
| Name | Type | Description |
|---|---|---|
query |
str
|
The evaluation question. |
relevant |
list[GoldenChunkRef]
|
The original portable refs (kept for reporting). |
chunk_ids |
list[str]
|
One resolved |
Source code in varagity/eval/datasets.py
105 106 107 108 109 110 111 112 113 114 115 116 117 | |
load_conversations(directory)
¶
Load and validate every conversation fixture in a directory.
Files are read in sorted name order (the report order); any malformed
fixture fails the load loudly, mirroring :func:load_golden — a
silently skipped conversation would quietly inflate every score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directory
|
Path
|
The fixtures directory (one |
required |
Returns:
| Type | Description |
|---|---|
list[ConversationFixture]
|
The validated fixtures, in file-name order. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If |
ValueError
|
If it holds no |
Source code in varagity/eval/datasets.py
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 | |
load_golden(path)
¶
Load and validate the golden dataset from a JSONL file.
Blank lines are ignored; any malformed line fails the load loudly — a silently skipped golden query would quietly inflate every score.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path
|
The |
required |
Returns:
| Type | Description |
|---|---|
list[GoldenEntry]
|
The validated entries, in file order. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If |
ValueError
|
If a line is not valid JSON or fails schema validation (message includes the 1-based line number). |
Source code in varagity/eval/datasets.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | |
load_graph_golden(path)
¶
Load and validate the graph golden dataset from a JSONL file.
Mirrors :func:load_golden — blank lines ignored, any malformed line
fails the load loudly with its 1-based line number — and additionally
rejects a repeated query, which would silently double-weight one
question in the per-kind averages.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
Path
|
The |
required |
Returns:
| Type | Description |
|---|---|
list[GraphGoldenEntry]
|
The validated entries, in file order. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If |
ValueError
|
If a line is not valid JSON, fails schema validation, or repeats an earlier entry's query, or if the file holds no entries. |
Source code in varagity/eval/datasets.py
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 | |
resolve_golden(entries, corpus_root)
¶
Resolve portable refs to the chunk_ids the stores will hold.
Reproduces the loader's identity derivation (plan decision #6) from
the corpus files themselves: doc_id hashes rel_source together
with the file's byte hash, so resolution needs no ingested store and
is stable across machines and OCR engines (bytes, not extracted text).
Whether a resolved chunk_id actually exists (i.e. chunk_index
is within the document's real chunk count) is checked by the harness
after ingest — it depends on the live chunking, not on this file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entries
|
list[GoldenEntry]
|
The validated golden entries. |
required |
corpus_root
|
Path
|
The eval corpus root the refs are relative to. |
required |
Returns:
| Type | Description |
|---|---|
list[ResolvedGoldenEntry]
|
One resolved entry per input entry, in order. |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If a ref names a file missing from the corpus. |
Source code in varagity/eval/datasets.py
153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | |
varagity.eval.containers
¶
Ephemeral backing stores via testcontainers (plan decision #4).
The eval harness measures against throwaway Postgres/Elasticsearch containers so the live corpus is never touched; the integration and e2e test suites spin the same containers. This module is the single home for that setup — image tags, schema application, and the single-node Elasticsearch tuning — shared by both consumers.
testcontainers lives in the eval dependency group (installed with
the dev group, or via uv run --group eval); its import is deferred to
call time so importing this module — which the CLI does on every start via
the eval wiring — never requires it.
Operational notes baked in:
- The Elasticsearch container disables the disk-watermark allocation checks: on a host whose disk is >90% full, the default percentage watermarks refuse to allocate the throwaway index's primary shard and every operation times out. Ephemeral stores must never depend on host disk pressure.
- Single-node Elasticsearch is
yellowby design; nothing here waits forgreen.
EphemeralStores
dataclass
¶
Connected store clients over the throwaway containers.
Attributes:
| Name | Type | Description |
|---|---|---|
store |
ContextualVectorDB
|
Vector store on the ephemeral Postgres. |
bm25 |
ElasticsearchBM25
|
BM25 store on the ephemeral Elasticsearch (index not yet created — the ingest loader creates it idempotently). |
pg_conninfo |
str
|
The Postgres conninfo, for raw assertions. |
es_url |
str
|
The Elasticsearch base URL, for raw assertions. |
Source code in varagity/eval/containers.py
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | |
ephemeral_elasticsearch()
¶
Run a throwaway single-node Elasticsearch (BM25 store backing).
Yields:
| Type | Description |
|---|---|
str
|
The container's base URL. |
Raises:
| Type | Description |
|---|---|
ModuleNotFoundError
|
If |
Source code in varagity/eval/containers.py
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | |
ephemeral_postgres()
¶
Run a throwaway pgvector Postgres with schema.sql applied.
Yields:
| Type | Description |
|---|---|
str
|
A libpq conninfo string for the container. |
Raises:
| Type | Description |
|---|---|
ModuleNotFoundError
|
If |
Source code in varagity/eval/containers.py
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | |
ephemeral_stores(index_name='varagity_eval_bm25')
¶
Run both throwaway containers and yield connected store clients.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
index_name
|
str
|
BM25 index name inside the ephemeral Elasticsearch. |
'varagity_eval_bm25'
|
Yields:
| Type | Description |
|---|---|
EphemeralStores
|
The connected stores (closed, with their containers stopped, on |
EphemeralStores
|
exit). |
Raises:
| Type | Description |
|---|---|
ModuleNotFoundError
|
If |
Source code in varagity/eval/containers.py
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |
varagity.eval.evaluate
¶
Retrieval evaluation: recall@k / pass@k over the 7-config matrix (spec §16).
Quantifies the retrieval-quality ladder, on the golden set over
tests/fixtures/corpus:
semantic_noncontextual— the vanilla-RAG v1 baseline,semantic_contextual— contextual embeddings (≈35% tier),bm25_contextual— contextual BM25,hybrid_contextual— rank fusion (≈49% tier, the default),hybrid_rerank_contextual— + cross-encoder re-ranking (≈67% tier, spec_v2 §5.5),hyde_contextual— HyDE over the same hybrid base (ADR-016): the dense arm searches with an LLM-written hypothetical passage,hyde_rerank_contextual— the HyDE+rerank pairing (rerankedcomposinghyde): HyDE steers the candidate pool, the cross-encoder judges the real query.
The chunker sweep (spec_v2 §7.4) follows the matrix: every
registered chunking strategy gets its own contextual ingest over the same
ephemeral stores and is measured across the contextual retrieval configs.
Because the golden set's chunk_index refs are authored against the
pinned default boundaries, the sweep re-resolves each ref by content:
a ref's fact snippet is located in the strategy-true chunks
(:func:resolve_golden_by_fact), and a ref counts as retrieved when any
chunk containing its fact is in the top-k — index-anchored scoring under
foreign boundaries would measure nothing but the boundary mismatch.
Measurement runs against ephemeral testcontainers stores (plan decision
4) so the live corpus is never touched, while embeddings, the¶
contextualizing LLM, and the reranker are the real GPU services from the
running compose stack (they are stateless). Two ingests cover all seven
configs: ingest A (CONTEXTUALIZE=false) backs config 1; ingest B
(CONTEXTUALIZE=true, reingest over the same stores) backs configs
2–7 against the same index — and doubles as the sweep's
recursive_character row. Each remaining strategy adds one reingest.
The two HyDE configs additionally pay one live-LLM passage generation per
query each (independent generations — sampling noise between configs 6 and
7 is part of what the pairing comparison measures).
Metric semantics: :func:recall_at_k is the Anthropic cookbook's
evaluation number (there called Pass@n) — the per-query fraction of
golden chunks present in the top-k, averaged over queries.
:func:pass_at_k is the stricter complement reported alongside it: the
fraction of queries whose golden chunks are all in the top-k. The
sweep's :func:recall_at_k_any / :func:pass_at_k_any are the same two
numbers with each golden ref generalized from one chunk id to an
acceptable set (fact-containing chunks).
The eval pipeline settings are pinned (:data:PINNED_EVAL_SETTINGS) rather
than inherited from .env: golden chunk_index values assume those
chunk boundaries, and pinning keeps runs comparable across machines. Pins
are applied by exporting environment variables and clearing the settings
cache — the same mechanism the test suite uses — because deep pipeline
code resolves get_settings() internally; the eval harness is a
single-threaded CLI path, where this is safe.
The chat-engine eval (spec_v3 §4.9) is the multi-turn
counterpart: every registered chat engine runs the hand-authored
conversation fixtures (tests/fixtures/conversations/), and each turn's
retrieval is scored fact-anchored exactly like the sweep — the engine's
search_query (the condensed rewrite, under condense_context)
drives retrieval, and a turn's ref is satisfied when any fact-carrying
chunk lands in the top-k. Assistant replies are scripted in the fixtures,
so both engines see byte-identical history: the comparison isolates the
engine, never its answers. Depths are :data:CHAT_K_VALUES, shallower
than the matrix's: the eval corpus ingests ~16 chunks, so recall@20 is
1.0 by definition and k=5 is the production context cut
(RERANK_TOP_N).
FactRef
¶
Bases: BaseModel
One golden ref resolved to the chunks that satisfy it (sweep form).
Attributes:
| Name | Type | Description |
|---|---|---|
label |
str
|
Human-readable identity in reports — the ref's |
chunk_ids |
list[str]
|
Every chunk id whose content contains the fact under the currently ingested boundaries (overlap can spread a fact over several); retrieval of any of them satisfies the ref. Empty when the fact matched nothing — a guaranteed miss. |
Source code in varagity/eval/evaluate.py
187 188 189 190 191 192 193 194 195 196 197 198 199 200 | |
FactResolvedEntry
¶
Bases: BaseModel
A golden entry re-resolved by content for one chunking strategy.
Attributes:
| Name | Type | Description |
|---|---|---|
query |
str
|
The evaluation question. |
refs |
list[FactRef]
|
One :class: |
Source code in varagity/eval/evaluate.py
203 204 205 206 207 208 209 210 211 212 | |
measure_chat_engine(engine, conversations, fact_entries, retrievers, *, llm, k_values=CHAT_K_VALUES)
¶
Run every conversation through one chat engine and score each turn.
Per turn: the engine prepares the search query against the scripted
history (one condense LLM call per follow-up under
condense_context; none under simple), then that one
search_query is scored under every retrieval config by swapping it
into the turn's fact-resolved entry and reusing
:func:measure_retriever_facts. History threads the fixture's
scripted replies, so every engine sees the identical conversation —
the comparison isolates the engine. Engine rendering is silenced
(verbose=0): the harness reports rewrites itself, and per-turn
console output across engines × turns would swamp it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
ChatEngine
|
The chat engine under measurement. |
required |
conversations
|
Sequence[ConversationFixture]
|
The fixtures, in report order. |
required |
fact_entries
|
Sequence[FactResolvedEntry]
|
One fact-resolved entry per turn, flattened in conversation order (the fixtures' turns, depth-first). |
required |
retrievers
|
Mapping[str, Retriever]
|
Retrieval configs to score under, keyed by name. |
required |
llm
|
LLMClient | None
|
Chat client for engines that condense (the live service);
|
required |
k_values
|
Sequence[int]
|
Depths to report. |
CHAT_K_VALUES
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The engine's results: |
dict[str, Any]
|
|
dict[str, Any]
|
follow-ups are the discriminating slice, first turns being |
dict[str, Any]
|
identity splits under every engine), |
dict[str, Any]
|
and latency stats), and |
dict[str, Any]
|
the search query used, per-config scores, and each ref's best |
dict[str, Any]
|
golden rank). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/eval/evaluate.py
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 | |
measure_retriever(retriever, entries, *, k_values=K_VALUES)
¶
Score one retriever over the golden entries at every depth.
Each query retrieves once at max(k_values); shallower depths are
prefix cuts of that single ranked list (retrieval order is
deterministic given the stores' contents).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
retriever
|
Retriever
|
The retrieval method under measurement. |
required |
entries
|
Sequence[ResolvedGoldenEntry]
|
Resolved golden entries. |
required |
k_values
|
Sequence[int]
|
Depths to report. |
K_VALUES
|
Returns:
| Type | Description |
|---|---|
dict[str, dict[str, float]]
|
A |
list[dict[str, int | None]]
|
|
tuple[dict[str, dict[str, float]], list[dict[str, int | None]]]
|
|
tuple[dict[str, dict[str, float]], list[dict[str, int | None]]]
|
|
tuple[dict[str, dict[str, float]], list[dict[str, int | None]]]
|
absent) — the raw material for debugging a bad score. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/eval/evaluate.py
599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 | |
measure_retriever_facts(retriever, entries, *, k_values=K_VALUES)
¶
Score one retriever over fact-resolved entries at every depth.
The sweep counterpart of :func:measure_retriever — identical shape,
with each ref satisfied by any of its acceptable chunk ids.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
retriever
|
Retriever
|
The retrieval method under measurement. |
required |
entries
|
Sequence[FactResolvedEntry]
|
Fact-resolved golden entries. |
required |
k_values
|
Sequence[int]
|
Depths to report. |
K_VALUES
|
Returns:
| Type | Description |
|---|---|
dict[str, dict[str, float]]
|
A |
list[dict[str, int | None]]
|
entry in order, each ref label's best 1-based rank among its |
tuple[dict[str, dict[str, float]], list[dict[str, int | None]]]
|
acceptable ids ( |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/eval/evaluate.py
357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 | |
pass_at_k(golden, retrieved, k)
¶
Whether all golden chunk ids are in the top-k (0.0 or 1.0).
The strict complement of :func:recall_at_k: a query passes only if
nothing a perfect retriever would return is missing. Callers average
over queries into a pass rate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
golden
|
Collection[str]
|
The query's golden chunk ids (non-empty). |
required |
retrieved
|
Sequence[str]
|
Retrieved chunk ids, best first. |
required |
k
|
int
|
Retrieval depth to evaluate at. |
required |
Returns:
| Type | Description |
|---|---|
float
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/eval/evaluate.py
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 | |
pass_at_k_any(acceptable, retrieved, k)
¶
Whether every golden ref is satisfied in the top-k (0.0 or 1.0).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
acceptable
|
Sequence[Collection[str]]
|
Per golden ref, the chunk ids that satisfy it. |
required |
retrieved
|
Sequence[str]
|
Retrieved chunk ids, best first. |
required |
k
|
int
|
Retrieval depth to evaluate at. |
required |
Returns:
| Type | Description |
|---|---|
float
|
|
float
|
else |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/eval/evaluate.py
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 | |
pinned_eval_settings(**overrides)
¶
Apply :data:PINNED_EVAL_SETTINGS (plus overrides) for a block.
Exports the pins as environment variables and clears the settings
cache, restoring both on exit — the eval-harness counterpart of the
test suite's settings_env fixture (see the module docstring for
why an environment override rather than parameter injection).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**overrides
|
str
|
Additional setting pins for this block (e.g.
|
{}
|
Yields:
| Type | Description |
|---|---|
None
|
Nothing; the pinned settings are active inside the block. |
Source code in varagity/eval/evaluate.py
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 | |
recall_at_k(golden, retrieved, k)
¶
Fraction of golden chunk ids present in the top-k retrieved.
The Anthropic cookbook's evaluation metric (there reported as Pass@n): one query's score; callers average over queries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
golden
|
Collection[str]
|
The query's golden chunk ids (non-empty). |
required |
retrieved
|
Sequence[str]
|
Retrieved chunk ids, best first. |
required |
k
|
int
|
Retrieval depth to evaluate at. |
required |
Returns:
| Type | Description |
|---|---|
float
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/eval/evaluate.py
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
recall_at_k_any(acceptable, retrieved, k)
¶
Fraction of golden refs with any acceptable chunk in the top-k.
:func:recall_at_k generalized for the chunker sweep: a ref is
satisfied by whichever strategy-true chunk carries its fact. An empty
acceptable set (the fact matched no chunk) can never be satisfied.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
acceptable
|
Sequence[Collection[str]]
|
Per golden ref, the chunk ids that satisfy it. |
required |
retrieved
|
Sequence[str]
|
Retrieved chunk ids, best first. |
required |
k
|
int
|
Retrieval depth to evaluate at. |
required |
Returns:
| Type | Description |
|---|---|
float
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/eval/evaluate.py
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 | |
resolve_golden_by_fact(entries, store)
¶
Re-resolve golden refs by content against the ingested boundaries.
For each ref with a fact, scans its document's stored chunks
(case-insensitively — OCR extraction may case-shift) and collects every
chunk containing the fact. A ref without a fact falls back to its
index-anchored chunk_id (only honest under the pinned default
boundaries — the golden set carries facts precisely so the sweep never
relies on this). A fact that matches no chunk is logged and left with
an empty acceptable set: a guaranteed miss, mirroring the OCR
benchmark's drift posture rather than silently inflating scores.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entries
|
Sequence[ResolvedGoldenEntry]
|
The index-resolved golden entries (their |
required |
store
|
ContextualVectorDB
|
The vector store holding the current strategy's ingest. |
required |
Returns:
| Type | Description |
|---|---|
list[FactResolvedEntry]
|
One fact-resolved entry per input entry, in order. |
Source code in varagity/eval/evaluate.py
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 | |
run_chat_eval(*, corpus_root=EVAL_CORPUS, conversations_dir=CONVERSATIONS_DIR, results_dir=RESULTS_DIR, ingest=ingest_corpus, verbose=None)
¶
Run the multi-turn chat-engine eval; persist results (spec_v3 §4.9).
One contextual ingest into ephemeral stores backs the whole run; every
registered chat engine (enumerated from the registry, like the chunker
sweep) then plays each conversation fixture, and every turn is scored
fact-anchored under :data:CHAT_EVAL_RETRIEVAL_CONFIGS. The condense
settings are pinned (:data:PINNED_CHAT_SETTINGS) so a host env with
the kill switch off can't silently measure condense_context as
simple. This harness decides the shipped CHAT_ENGINE default —
and "the default stays simple" is a valid result, not a failure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
corpus_root
|
Path
|
The eval corpus (defaults to the fixtures corpus the conversation refs are authored over). |
EVAL_CORPUS
|
conversations_dir
|
Path
|
The conversation fixtures directory. |
CONVERSATIONS_DIR
|
results_dir
|
Path
|
Directory for the timestamped results JSON. |
RESULTS_DIR
|
ingest
|
IngestCallable
|
Ingest callable ( |
ingest_corpus
|
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The results document (also written to |
dict[str, Any]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
FileNotFoundError
|
If the fixtures directory or a referenced corpus file is missing. |
RuntimeError
|
If the eval ingest reports failed files (a partial index would silently under-report every score). |
Source code in varagity/eval/evaluate.py
917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 | |
run_matrix(*, corpus_root=EVAL_CORPUS, golden_path=GOLDEN_PATH, results_dir=RESULTS_DIR, ingest=ingest_corpus, verbose=None)
¶
Run the 7-config retrieval matrix + the chunker sweep; persist results.
Two ingests into ephemeral stores cover all seven configs (see the
module docstring); the chunker sweep then reingests once per remaining
registered strategy (recursive_character reuses ingest B) and
measures each across :data:SWEEP_RETRIEVAL_CONFIGS with fact-anchored
golden resolution — deliberately without the HyDE configs: the
hypothetical passage depends only on the query, never on chunk
boundaries, so a per-strategy re-measure would re-pay every LLM
generation to measure the same transformation. Embeddings, the
contextualizing LLM (which also writes the HyDE passages), and the
reranker resolve from settings — the live GPU services (the semantic
strategy additionally embeds at ingest time through the same service).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
corpus_root
|
Path
|
The eval corpus (defaults to the fixtures corpus the golden set is authored over). |
EVAL_CORPUS
|
golden_path
|
Path
|
The golden dataset file. |
GOLDEN_PATH
|
results_dir
|
Path
|
Directory for the timestamped results JSON. |
RESULTS_DIR
|
ingest
|
IngestCallable
|
Ingest callable ( |
ingest_corpus
|
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The results document (also written to |
dict[str, Any]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
FileNotFoundError
|
If the golden set or a referenced corpus file is missing. |
RuntimeError
|
If either eval ingest reports failed files (a partial index would silently under-report every score). |
Source code in varagity/eval/evaluate.py
715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 | |
validate_golden_against_store(entries, stores, *, strict=True)
¶
Check that every golden ref exists in the freshly ingested store.
A golden chunk_index beyond a document's actual chunk count means
the golden set has drifted from the chunking pipeline (or, in the OCR
benchmark, that an engine's text produced different chunk boundaries)
— a score computed against it would silently under-report.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entries
|
Sequence[ResolvedGoldenEntry]
|
Resolved golden entries. |
required |
stores
|
EphemeralStores
|
The ingested ephemeral stores. |
required |
strict
|
bool
|
Raise on unresolvable refs (the retrieval matrix) instead of warning and returning them (the OCR benchmark, where per-engine boundary drift is a known, reported caveat). |
True
|
Returns:
| Type | Description |
|---|---|
list[str]
|
The unresolvable |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/eval/evaluate.py
649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 | |
write_results(kind, payload, *, results_dir=RESULTS_DIR)
¶
Persist one evaluation run's results as timestamped JSON (spec §16).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
str
|
Short result-file tag ( |
required |
payload
|
dict[str, Any]
|
The full results document. |
required |
results_dir
|
Path
|
Directory for result files (created if missing). |
RESULTS_DIR
|
Returns:
| Type | Description |
|---|---|
Path
|
The path written. |
Source code in varagity/eval/evaluate.py
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 | |
varagity.eval.graph_fixtures
¶
Synthetic iMessage fixture corpus for the graph eval (spec_graphrag §12).
Writes a real chat.db — the schema subset
:mod:varagity.graph.sources.imessage reads — so the bake-off and the
permanent regression harness parse the fixture through the product code path
(find_message_source → batch_for_path), never a test shortcut.
Two profiles (plan decision #11):
smoke— the hand-scripted conversations only (~226 messages, 10 threads, 5 named participants + the owner). Adapter development, harness unit tests, the incremental-reindex check.full— the same scripted messages verbatim, plus deterministic filler up to > 10,000 messages spanning a decade (the §12 Q2 bake-off corpus).
Golden QA (data/eval/graph_golden_qa.jsonl) is authored strictly against
scripted messages, which are byte-identical in both profiles, so a golden
never depends on filler. Filler is held to the opposite discipline: it must
never mention a golden entity or fact term (:data:FILLER_BLOCKLIST,
asserted at generation time) — that is what keeps the corpus discriminative
at 10⁴ scale rather than merely large.
The scripted content is synthetic: invented people, invented events, no real
data (spec_graphrag §6). The one exception is the attributedBody blob
reused from the Phase-1 decode fixture (innocuous, owner-reviewed), so that
the NULL-text decode path runs inside the eval too.
FixtureManifest
¶
Bases: BaseModel
What one :func:build_fixture_chat_db call wrote (never persisted).
Returned for assertions and for the harness's settings pins: the parser
maps handles to names through GRAPH_HANDLE_NAMES and the owner
through GRAPH_OWNER_ALIASES, so a caller that wants the scripted
display names ("Bob Nakamura", not "+12145550101") pins
:attr:handle_names and :attr:owner_label before parsing.
Attributes:
| Name | Type | Description |
|---|---|---|
profile |
str
|
The profile built ( |
seed |
int
|
The filler RNG seed (irrelevant for |
message_count |
int
|
Parsed messages written (scripted + filler); tapback rows are reactions, not messages, and are excluded. |
scripted_count |
int
|
Messages from the hand-authored script. |
filler_count |
int
|
Generated filler messages (0 for |
tapback_count |
int
|
Reaction rows written (they fold onto their targets). |
thread_counts |
dict[str, int]
|
Message count per thread guid. |
guids |
list[str]
|
Every message guid written, in write order — the set the
golden QA's |
first_timestamp |
datetime
|
Earliest message time (aware UTC). |
last_timestamp |
datetime
|
Latest message time (aware UTC). |
owner_label |
str
|
Display name the script was authored with for
|
handle_names |
dict[str, str]
|
|
Source code in varagity/eval/graph_fixtures.py
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 | |
apple_date(when)
¶
Encode a datetime as a message.date value in its era's units.
Rows before :data:_ERA_BOUNDARY are written in seconds (the pre-macOS
10.13/iOS 11 format) and later rows in nanoseconds, so a single fixture
database contains both eras exactly as a decade-spanning real export
does.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
when
|
datetime
|
The send time (aware UTC). |
required |
Returns:
| Type | Description |
|---|---|
int
|
The raw |
Source code in varagity/eval/graph_fixtures.py
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 | |
assert_filler_clean(text, *, context)
¶
Reject filler text that mentions a golden entity or fact term.
The discipline that keeps the full profile discriminative: every
designed distractor lives in the scripted conversations, so filler must
be inert. Matching is whole-word (optionally pluralised) and
case-insensitive — "warm" is not "arm".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
The generated filler string (message, name, or thread label). |
required |
context
|
str
|
Where it came from, for the error message. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/eval/graph_fixtures.py
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 | |
build_fixture_chat_db(dest, *, profile='smoke', seed=13, message_target=None, script_path=SCRIPT_PATH, blob_path=ATTRIBUTED_BODY_PATH)
¶
Build the synthetic chat.db for a profile and describe what it holds.
The scripted conversations are written verbatim in every profile (golden
QA is authored against them); full appends deterministic filler until
the message target is met. Determinism is over parsed content, not file
bytes — SQLite's page layout is not the contract.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dest
|
Path
|
Destination |
required |
profile
|
str
|
|
'smoke'
|
seed
|
int
|
RNG seed for the filler (same seed ⇒ same parsed content). |
13
|
message_target
|
int | None
|
Overrides the profile's minimum parsed-message count
— the knob unit tests use to exercise |
None
|
script_path
|
Path
|
The hand-authored conversation script. |
SCRIPT_PATH
|
blob_path
|
Path
|
The captured |
ATTRIBUTED_BODY_PATH
|
Returns:
| Name | Type | Description |
|---|---|---|
The |
FixtureManifest
|
class: |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the script or blob fixture is missing. |
ValueError
|
If |
Source code in varagity/eval/graph_fixtures.py
702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 | |
varagity.eval.graph_eval
¶
The graph harness — eval graph (spec_graphrag §12, §8.2).
The fourth eval subcommand, and the one that decided ADR-017: build the
synthetic message corpus, run every selected registered graph engine over it
sequentially, and score each engine's answers — because a graph engine
returns prose, not chunk ids. With the ADR accepted it stays on as the graph
regression guard: a retrieval-side change (query mode, the e5 prefix, a
rerank hook) is one --skip-build re-score away from a number.
Structurally this is :func:varagity.eval.evaluate.run_chat_eval again
(registry-enumerated engine loop, pinned settings, fact-anchored scoring,
:func:~varagity.eval.evaluate.write_results persistence, a thin Prefect
shell in :mod:varagity.pipeline.eval_flow) with three differences that come
from the subject matter:
- No stores, no testcontainers. Graph engines self-store in their own
working directories under
data/eval/graph/(plan decision #9), so the only external needs are the live llama.cpp and infinity services. - Scoring is over answers. Each golden entry carries
expected_facts(an AND of OR-groups, matched case-insensitively as substrings of the answer — :func:~varagity.eval.evaluate.resolve_golden_by_fact's trick applied to prose) andrequired_guids(provenance). An engine that surfaces no source ids scoresNoneprovenance rather than 0: that honesty is criterion §8.2#4 data. Questionkindslices the report and is never shown to an engine (plan decision #10). - The parser's naming settings are pinned from the fixture manifest.
Sender display names come from
GRAPH_OWNER_ALIASES/GRAPH_HANDLE_NAMES, so a parse without those pins yields raw phone handles and every golden naming "Bob" fails against a corpus that only ever says "+12145550101". :func:manifest_settings_pinsderives them from what the generator actually wrote, and :func:pinned_graph_settingsapplies them the way the rest of the harness pins settings (environment export + cache clear — see :func:varagity.eval.evaluate.pinned_eval_settings).
Three cost controls shape the run, because a full-profile index is hours per
engine on a single-slot llama.cpp: --engine runs one engine per session,
--skip-build reuses the corpus and the engines' working directories so
scoring can be iterated without re-paying an index, and --message-target
caps the corpus below its profile's size — Graphiti indexes at ~51 s/message,
which makes an uncapped 10,001-message build about six days, so its
full-profile seat is a capped slice and the cap is reported with the numbers.
A capped run is a different corpus: :func:corpus_stem gives it its own
database, manifest, and per-engine working directories (full-mt1000), so a
capped run can never clobber the uncapped corpus another engine is still
indexing against. The scripted messages are written in every corpus regardless
of the cap, so the golden set's provenance anchors always resolve.
The incremental re-index check (criterion §8.2#3) runs only on the
smoke profile and only when building: a second build over an
overlapping batch — the same messages plus
:data:INCREMENTAL_DELTA_MESSAGES new ones — measures what a re-export of a
grown chat.db costs. The delta messages are generator filler, which is
blocklist-guaranteed never to mention a golden term, so the check cannot
perturb the scores that follow it.
Engine libraries stay call-time imports (stage-1 decision #8) even though the shipped engine is a main dependency now: nothing here needs an engine installed to import, and selecting one whose library is missing raises a clear error naming the sync that would fix it.
corpus_stem(profile, message_target=None)
¶
Name the corpus (and the engines' workdir tier) for one run.
A --message-target run indexes a different corpus from its profile's
uncapped one, so it may not share either the generated database or the
engines' working directories: overwriting full.db while another
engine's multi-day full-profile build is still running against it would
corrupt that run's provenance silently, and reusing full/<engine>/
would score a capped run against an uncapped graph.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profile
|
str
|
|
required |
message_target
|
int | None
|
The |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The file stem for the corpus and its manifest, and the directory name |
str
|
holding each engine's workdir: |
str
|
capped. |
Source code in varagity/eval/graph_eval.py
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | |
engine_versions(names)
¶
Stamp the installed version of each engine's library.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
names
|
Sequence[str]
|
Engine names being run. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, str | None]
|
Engine name → installed distribution version, or |
dict[str, str | None]
|
library is not installed (which a run cannot actually reach, but a |
dict[str, str | None]
|
results file re-read later can). |
Source code in varagity/eval/graph_eval.py
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 | |
manifest_settings_pins(*manifests)
¶
Derive the parser's naming pins from what the generator wrote.
Sender display names are a settings concern
(:attr:~varagity.config.Settings.graph_handle_name_map,
:attr:~varagity.config.Settings.graph_owner_label), so parsing a fixture
without these pins yields raw handles and every golden question naming a
participant is unanswerable. GRAPH_HANDLE_NAMES_FILE is pinned empty
alongside them: a host .env pointing it at a contacts file that does
not exist on this machine would otherwise fail the parse.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*manifests
|
FixtureManifest
|
The manifests of every corpus this run will parse (the base corpus, plus the incremental delta corpus when the check runs). Their handle maps are merged, later manifests winning. |
()
|
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
The pins, ready for :func: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no manifest is given, or two manifests disagree about the owner label (a mismatch would silently split the owner into two graph entities). |
Source code in varagity/eval/graph_eval.py
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 | |
match_fact_groups(answer, expected_facts)
¶
Report which of an entry's OR-groups the answer satisfies.
expected_facts is an AND of OR-groups (spec_graphrag §12): a group is
satisfied when any of its variants appears in the answer, matched
case-insensitively as a substring — the same rule the chunk-RAG sweep uses
against chunk text, applied to prose.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
answer
|
str
|
The engine's answer text. |
required |
expected_facts
|
Sequence[Sequence[str]]
|
The entry's fact groups. |
required |
Returns:
| Type | Description |
|---|---|
list[bool]
|
One boolean per group, in group order. |
Source code in varagity/eval/graph_eval.py
311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 | |
measure_graph_engine(session, entries, *, mode=None, verbose=0)
¶
Ask one open engine session every golden question and score the answers.
Questions are put verbatim — an entry's kind is report metadata and is
never shown to an engine (plan decision #10). A query that raises is
recorded as a failed query (zero fact recall, None provenance) rather
than aborting the engine: at hours per index, losing a whole engine's
numbers to one bad question would be the expensive kind of strict, and
engine failure counts are themselves criterion §8.2#2 data — the same
posture :attr:~varagity.graph.records.BuildReport.failures takes for
indexing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
GraphSession
|
The open engine session to interrogate. |
required |
entries
|
Sequence[GraphGoldenEntry]
|
The golden questions, in report order. |
required |
mode
|
str | None
|
Engine query mode override ( |
None
|
verbose
|
int
|
Validated console verbosity (0–2). |
0
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
|
dict[str, Any]
|
one slice per :data: |
dict[str, Any]
|
has questions, and the full per-query detail (answer text included, |
dict[str, Any]
|
for the ADR autopsy). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/eval/graph_eval.py
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 | |
open_engine_session(name, workdir)
¶
Open one registered engine's session, translating a missing library.
Adapters import their engine libraries inside session() (plan decision
8), so a missing library surfaces here as a bare ModuleNotFoundError¶
from somewhere inside a third-party package. Translating it names the engine and the install command instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Registered engine name. |
required |
workdir
|
Path
|
The engine's working directory. |
required |
Yields:
| Type | Description |
|---|---|
GraphSession
|
The open session (closed on exit — teardown is not optional; Graphiti |
GraphSession
|
runs an embedded |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If the engine's library is not installed. |
Source code in varagity/eval/graph_eval.py
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 | |
pinned_graph_settings(**pins)
¶
Apply graph settings pins for a block, restoring them on exit.
The graph counterpart of
:func:varagity.eval.evaluate.pinned_eval_settings, and the same
mechanism for the same reason: deep code (here the iMessage parser)
resolves get_settings() internally, and the eval harness is a
single-threaded CLI path where an environment override is safe. There is
no standing pin set — every pin is derived from the fixture manifest the
run just built (:func:manifest_settings_pins), because the names the
corpus was written with are the only correct ones.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**pins
|
str
|
Settings to export for this block (e.g.
|
{}
|
Yields:
| Type | Description |
|---|---|
None
|
Nothing; the pinned settings are active inside the block. |
Source code in varagity/eval/graph_eval.py
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | |
prepare_corpus(corpus_dir, *, profile, seed=FIXTURE_SEED, message_target=None, name=None, reuse=False)
¶
Build (or reuse) one fixture corpus and return it with its manifest.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
corpus_dir
|
Path
|
Directory holding the generated databases (created if absent). |
required |
profile
|
str
|
|
required |
seed
|
int
|
Filler RNG seed. |
FIXTURE_SEED
|
message_target
|
int | None
|
Overrides the profile's message count (the incremental delta corpus uses it to ask for "the corpus plus a few"). |
None
|
name
|
str | None
|
File stem for the database and its manifest; defaults to
|
None
|
reuse
|
bool
|
Reuse an existing database and its manifest instead of
regenerating ( |
False
|
Returns:
| Type | Description |
|---|---|
tuple[Path, FixtureManifest]
|
The |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If the scripted-message or blob fixture is missing. |
ValueError
|
If |
Source code in varagity/eval/graph_eval.py
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 | |
provenance_recall(surfaced, required)
¶
Score how much of an entry's required provenance the engine surfaced.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
surfaced
|
Sequence[str]
|
Message guids the engine's evidence carried. |
required |
required
|
Sequence[str]
|
The entry's |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
|
float | None
|
surfaced no guids at all — reported honestly as "this engine cannot |
float | None
|
cite messages" rather than as a zero score (criterion §8.2#4). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in varagity/eval/graph_eval.py
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 | |
run_graph_eval(*, profile='smoke', engines=None, mode=None, skip_build=False, message_target=None, eval_root=GRAPH_EVAL_ROOT, golden_path=GRAPH_GOLDEN_PATH, results_dir=RESULTS_DIR, seed=FIXTURE_SEED, verbose=None)
¶
Run the graph bake-off over the fixture corpus; persist results (§12).
The sequence, once per selected engine (sequentially — stage-1 decision
15): wipe its working directory, index the corpus, run the incremental¶
re-index check, then ask every golden question and score the answers. The corpus is generated first and its manifest pins the parser's naming settings, so the messages the engines index say "Bob Nakamura" rather than a phone number.
This harness decided ADR-017 and stays on as the graph regression guard, exactly as the chat eval did for ADR-011.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profile
|
str
|
Fixture corpus profile — |
'smoke'
|
engines
|
Sequence[str] | None
|
Engine names to run; |
None
|
mode
|
str | None
|
Engine query mode override for every question ( |
None
|
skip_build
|
bool
|
Reuse the existing corpus and each engine's already-built working directory, and skip the incremental check — the scoring iteration loop, which must not re-pay a multi-hour index. |
False
|
message_target
|
int | None
|
Cap the corpus at this many messages instead of the
profile's own size — how an engine too slow for the uncapped
corpus (Graphiti, ~51 s/message) still gets a full-profile seat.
The scripted messages are always written, so the golden set holds;
filler tops the corpus up to the cap. A capped run gets its own
corpus and working directories (:func: |
None
|
eval_root
|
Path
|
Root for the generated corpus and the engines' working directories; resolved to an absolute path before use. |
GRAPH_EVAL_ROOT
|
golden_path
|
Path
|
The graph golden Q&A file. |
GRAPH_GOLDEN_PATH
|
results_dir
|
Path
|
Directory for the timestamped results JSON. |
RESULTS_DIR
|
seed
|
int
|
Filler RNG seed for the generated corpus. |
FIXTURE_SEED
|
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The results document (also written to |
dict[str, Any]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
FileNotFoundError
|
If the golden set or a fixture input is missing. |
RuntimeError
|
If a selected engine's library is not installed. |
Source code in varagity/eval/graph_eval.py
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 | |
select_engines(engines)
¶
Resolve the --engine filter against the registry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engines
|
Sequence[str] | None
|
Requested engine names, or |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
The engine names to run, sorted — the harness runs them sequentially |
list[str]
|
(stage-1 decision #15: there is one llama.cpp slot, so parallel |
list[str]
|
engines would only queue behind each other). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a requested name is not registered, or the filter selects nothing. |
Source code in varagity/eval/graph_eval.py
476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 | |
validate_golden_against_manifest(entries, manifest)
¶
Check that every golden provenance anchor exists in the built corpus.
The golden-validation precedent
(:func:varagity.eval.evaluate.validate_golden_against_store), and strict
for the same reason: a required_guid that no longer exists would score
every engine's provenance down for a drift in the fixture, silently.
Runs before any engine opens a session, so the failure costs seconds
rather than hours.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entries
|
Sequence[GraphGoldenEntry]
|
The loaded golden entries. |
required |
manifest
|
FixtureManifest
|
The built corpus's manifest. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If any |
Source code in varagity/eval/graph_eval.py
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 | |
varagity.eval.ocr_benchmark
¶
OCR engine benchmark: EasyOCR vs Tesseract (plan decision #10, spec §16).
EasyOCR shipped as a provisional fallback engine behind the
pluggable OCR_ENGINE factory; this benchmark supplies the data that
picks the shipped default (recorded in ADR-004). Two measurements per
engine:
-
Intrinsic quality — the scanned/mixed fixture PDFs are parsed through the real two-pass parser and compared against their known ground-truth text (
data/eval/ocr_truth/, transcribed from the images the fixtures were generated with): CER/WER viajiwerafter normalization (lowercase, punctuation stripped — which also drops Docling's markdown markup — whitespace collapsed), plus pages/sec wall-clock over a warmed engine (one untimed warm-up conversion loads models first). -
Retrieval impact (supplementary) — the full fixtures corpus is ingested once per engine into ephemeral testcontainers stores (real embeddings;
CONTEXTUALIZEoff so the engine is the only variable), then recall@k / pass@k are reported for the scanned-document golden queries. The non-scanned corpus documents ride along as distractors — without them every query would trivially retrieve the whole index. OCR noise hits BM25 keyword matching hardest, so all three retrieval methods are reported. Directional on a tiny corpus, per the plan; a golden ref an engine's chunk boundaries can't resolve is counted as a guaranteed miss and reported.
jiwer lives in the eval dependency group; its import is deferred
to call time like the group's other members.
character_error_rate(truth, hypothesis)
¶
CER between normalized ground truth and OCR output.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
truth
|
str
|
Ground-truth text (non-empty after normalization). |
required |
hypothesis
|
str
|
The engine's extracted text. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The character error rate (0.0 = perfect; can exceed 1.0). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the normalized ground truth is empty. |
Source code in varagity/eval/ocr_benchmark.py
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | |
normalize_ocr_text(text)
¶
Normalize text for OCR error metrics.
Lowercases, strips punctuation (which also removes Docling's markdown
markup — # headings, table pipes — so the comparison measures
recovered text, not export formatting), and collapses whitespace.
Applied identically to ground truth and hypothesis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
Raw ground-truth or extracted text. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The normalized text. |
Source code in varagity/eval/ocr_benchmark.py
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | |
run_ocr_benchmark(*, corpus_root=EVAL_CORPUS, golden_path=GOLDEN_PATH, results_dir=RESULTS_DIR, ingest=ingest_corpus, verbose=None)
¶
Benchmark the OCR engines and persist the results (spec §16).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
corpus_root
|
Path
|
The eval corpus holding the OCR fixtures. |
EVAL_CORPUS
|
golden_path
|
Path
|
The golden dataset file (its scanned-document queries drive the retrieval-impact measurement). |
GOLDEN_PATH
|
results_dir
|
Path
|
Directory for the timestamped results JSON. |
RESULTS_DIR
|
ingest
|
IngestCallable
|
Ingest callable ( |
ingest_corpus
|
verbose
|
int | None
|
Console verbosity (0–2); defaults to
|
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
The results document (also written to |
dict[str, Any]
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
FileNotFoundError
|
If a fixture, ground-truth file, or the golden set is missing. |
RuntimeError
|
If an engine's ingest reports failed files (a partial index would silently under-report its retrieval scores). |
Source code in varagity/eval/ocr_benchmark.py
222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 | |
word_error_rate(truth, hypothesis)
¶
WER between normalized ground truth and OCR output.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
truth
|
str
|
Ground-truth text (non-empty after normalization). |
required |
hypothesis
|
str
|
The engine's extracted text. |
required |
Returns:
| Type | Description |
|---|---|
float
|
The word error rate (0.0 = perfect; can exceed 1.0). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the normalized ground truth is empty. |
Source code in varagity/eval/ocr_benchmark.py
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |