Commit graph

64 commits

Author SHA1 Message Date
Yiorgis Gozadinos
18e1127375
Report the multi-database gates and which surfaces were reached
The two hard gates are pass/fail, not rates: any scope leak or attribution
error is a bug. The report names the offending cases and prints a greppable
GATES: PASSED / FAILED line, since this dataset is an acceptance gate rather
than a number to watch drift on.

The attribution gate covers only families where exactly one database is
correct. B5 is answered by two, so citing both is right there and policing
it would report a bug that is not one.

Surface coverage comes from the model's own Python, now carried on the run
result: counting executions said how often code ran but never which surface
it reached. Coverage is measured, not required, so a surface nothing reaches
is a finding about whether it earns its place in the VFS.

answer_correct is recorded as a score rather than a boolean, since booleans
become assertions and the run reads its accuracy headline from scores. The
two gates stay boolean assertions, which is what they are.

DatasetSpec grows a report_hook so this lives with the dataset instead of
becoming a key check in the shared QA reporting.

The reference-config invariant said a dataset with a deterministic evaluator
must declare no judge. That is not true here: RefusalJudge runs on any case
carrying an answerability label, which B6 and B7 do, so its sampling has to
be pinned. The assertion now requires a pinned block wherever a judge runs
and allows its absence only as the claim that none does.
2026-08-27 09:26:52 +03:00
Yiorgis Gozadinos
3f9066f4f9
Ask the multi-database questions and score them deterministically
Two dataset keys, because an S-family surface question cannot pass under
the RAG target and scoring it there would be noise: multidb carries the
behaviour families, multidb_surfaces the sandbox surfaces.

Scoring is deterministic. The answers are known numbers, names and counts,
so a red gate means the code is broken rather than that a judge spiralled.
Numbers are extracted and compared, since "1,240 metres" and "1240 m" are
both legitimate. B1 and B3 require the gold value present AND the twin's
absent: a hedge naming both elevations passes a presence check while
demonstrating the confusion those families exist to provoke. Refusals stay
judged, via the answerability labels the existing RefusalJudge reads, since
a phrase matcher keys on wording the model may never use.

B3 and B4 carry 9 and 10 instances because their gates are pass/fail and a
handful of cases is not evidence of absence. Half the B4 instances ask about
a near-name pair member with its twin excluded, so honouring scope costs the
model the other strong match instead of being free.

B2 rotates the order it lists the databases. RRF ties resolve to insertion
order, which is the configured order, so a fixed order would measure
ordering rather than fusion.

A scope travels in the case inputs via ScopedQuestion, since the task
function receives inputs and never metadata.
2026-08-27 09:26:52 +03:00
Yiorgis Gozadinos
7ae395a107
Generate the multi-database corpus and build its databases
The acceptance dataset for lancedb.databases needs a corpus where the
expected database is known per question, which no existing dataset gives.
Three databases: northern and southern hold station reports on one schema,
equipment holds spec sheets that share the vocabulary and answer nothing.

Names are invented throughout. A real station lets the model answer from
priors, which would measure memorisation rather than retrieval.

Three near-name pairs and one shared entity carry the attribution cases.
Pair members are identical apart from the name and the numbers, since a
difference in instrument or technician would hand the model a free
discriminator. Elevations and years are unique across the corpus so a
number identifies one station, and document counts differ per database so
a count cannot be right by luck while attribution is wrong.

Questions and gold answers both derive from STATIONS and INSTRUMENTS, so
they cannot drift apart. Readings are derived with SHA-256 over database
and name: hash() is salted per process, and keying on the name alone gave
the two Station Auk reports identical tables.

evaluations run populates one database and refuses a configured set, so
the builder opens each database itself and the run is --skip-db. It
asserts no single chunk holds all twelve monthly readings, because
without that S3 collapses into a search question and the guarantee has to
survive a chunker change.
2026-08-27 09:26:52 +03:00
Yiorgis Gozadinos
ba760b76c2
Thread per-case source scope through the capability runner
The runner passed document_filter onto capability state but not sources,
so an eval case could not be scoped to named databases. Scoping is what
the multi-database acceptance dataset has to assert, both that a scoped
question honours its databases and that an empty scope refuses rather
than answering without evidence.

sources=[] covers no database while None covers every one the client
covers, so the empty list is threaded on an is-not-None check and a test
pins the two apart.
2026-08-27 09:26:52 +03:00
Yiorgis Gozadinos
503db3271c
Name the evaluations set check for what it answers
`covers_a_set` is true for a mapping of one, which is a configured database
like any other; `uses_configured_databases` says that. Its population guard
said "several" for the same reason. Document evaluating the configured set
with `--skip-db`, against population, which writes one database and needs a
path.
2026-08-26 13:04:06 +03:00
Yiorgis Gozadinos
1d09b4e31b
Find a document in whichever database holds it
`get_document_by_id`, `get_document_by_uri` and `get_chunk_by_id` read
through repositories a client covering a set does not have, so a lookup by
identifier raised AttributeError and `resolve_document` with it. An
identifier says nothing about which database holds it, so every database is
asked at once and the first that has it, in configured order, answers.

On the evaluation side, `--db` overrides the configured set as the CLI
documents, and population refuses a set rather than ingesting into a
database the run would not read. A case filter matching nothing raises
instead of reporting 0.0000 as though it were a score.
2026-08-24 10:03:47 +03:00
Yiorgis Gozadinos
2bbd949a29
Record the database each citation came from
A run over several databases could report which documents were cited but
not which database grounded the answer: `_result_from_run` walked the
citation index for `document_uri` and dropped `Citation.source`. The
distribution is not recoverable from the report afterwards, so a sharded
run would have measured everything except attribution.

`cited_sources` is one entry per cited chunk, in citation order, empty
where the database is unnamed.
2026-08-24 10:03:47 +03:00
Yiorgis Gozadinos
eed820df1b
Evaluate over a configured set of databases
The runner passed the dataset's path to every arm, which opens one database
and is what makes `--db` meaningful. A run over `lancedb.databases` has to
pass none instead, so the client resolves the set, and `DatasetSpec.covers_a_set`
is the one place that decides which of the two a run is.
2026-08-24 10:03:47 +03:00
Yiorgis Gozadinos
a57a73b6d0
Add stable ids to FRAMES question rows 2026-08-24 09:03:44 +03:00
Yiorgis Gozadinos
aa49ec6cb3
Exclude FRAMES questions whose articles were deleted from Wikipedia 2026-08-24 08:44:01 +03:00
Yiorgis Gozadinos
c00ccb7322
Harden FRAMES corpus fetching against Wikimedia rate limits 2026-08-24 08:44:01 +03:00
Yiorgis Gozadinos
152d57d6e2
Add frames evaluation dataset 2026-08-24 08:44:01 +03:00
Yiorgis Gozadinos
19d5b2e7f6
Split the evaluation benchmark by responsibility
benchmark.py was 1220 lines holding six unrelated jobs: populating a
database, running retrieval, running QA, resolving datasets, moving
databases to and from HuggingFace, and wiring the Typer CLI.

qa.py takes both QA runners with their live summary, refusal metrics and
target resolution. population.py takes populate_db and the batched ingest.
retrieval.py takes run_retrieval_benchmark. artifacts.py takes HF_REPO_ID and
the download/upload bodies. experiment.py takes DEFAULT_JUDGE_MODEL and
build_experiment_metadata, which retrieval and QA both record.

benchmark.py keeps the CLI at 258 lines: the Typer app, config and case-id
loading, dataset resolution, evaluate_dataset, and three commands whose
bodies are now a loop over specs. The module-level side effects stay with it
— load_dotenv before configure_telemetry, so credentials and LOGFIRE_TOKEN
are in the environment before telemetry and model setup read them — so no
importable module carries one.

Test patch targets follow the code. get_model, run_capability_question,
run_capability_conversation, set_eval_attribute and HaikuRAG are patched
inside moved code, so they move with it; run_qa_benchmark,
run_retrieval_benchmark and find_config_file stay patchable on
evaluations.benchmark because evaluate_dataset and _load_config still look
them up there.

One assertion got stronger: a QA test patched benchmark.HaikuRAG to prove the
QA path does not open its own client. qa.py has no HaikuRAG reference at all
now, so the test asserts that instead.
2026-08-20 14:08:09 +03:00
Yiorgis Gozadinos
bd7946178d
Pin the eval judge to qwen3.8 2026-08-18 14:28:16 +03:00
Yiorgis Gozadinos
73dfc62f33
Exclude fully unjudged conversations from the macro pass rate 2026-08-17 11:19:59 +03:00
Yiorgis Gozadinos
587ba75a62
Thread the document filter through the live QA runner 2026-08-17 11:03:52 +03:00
Yiorgis Gozadinos
bf94efc655
Simplify the eval harness and share the embed-fill path. 2026-08-17 10:56:26 +03:00
Yiorgis Gozadinos
db2b8fb883
Add compaction arms and grounding status to MTRAG live runs 2026-08-17 10:54:20 +03:00
Yiorgis Gozadinos
73d9d93db9
Add MTRAG ClapNQ multi-turn evaluation
IBM's MTRAG benchmark (ClapNQ domain, pinned repo SHA): retrieval with
Recall@k/nDCG@k against binary qrels, gold-prefix QA replaying reference
conversation prefixes as message history, and live-session replay
carrying the model's own answers and tool history across turns.

Corpus population gains a bounded, resumable batched ingest path.
ConversationInput case type with transcript rendering for the judge,
eligibility-aware citation scoring, refusal precision/recall via a
label-aware RefusalJudge, per-turn verdicts with judged-turn coverage,
and per-turn tool-traffic attributes counted from each turn's new
messages so the arrays survive prior-turn compaction.
2026-08-17 10:53:16 +03:00
Yiorgis Gozadinos
a75d89122e
Drop the unused per-dataset filter default
No `DatasetSpec` declared `search_filter`, so `resolve_search_filter`
and the `--filter ""` clearing rule reconciled the flag against a
default that never existed. The flag alone covers the case. An empty
clause reaches `ChunkRepository.search`, which already treats it as
unfiltered.

Rename to `document_filter` throughout, matching
`run_capability_question`'s parameter and the metadata key that lands
in Logfire.

`_stub_spec` merges its overrides, so a test can override a loader
instead of rebuilding the whole spec.
2026-08-17 10:29:28 +03:00
Yiorgis Gozadinos
9b2ae347d2
Use filters that match the datasets they document
The `--filter` examples used `uri LIKE '%arxiv%'`, which matches no
`orb_text` document: its URIs are bare arXiv ids such as `2407.01528v3`.
A clause that matches nothing scores MAP 0 rather than erroring, so the
example failed silently.

`await_args` is typed `_Call | None`, so subscripting it fails
`ty check`; `call_args` carries the same call for an AsyncMock.
2026-08-17 10:19:51 +03:00
cwiesen
a68cb23b6e fix: resolve incorrect dataset and column namings 2026-08-14 16:29:51 -05:00
cwiesen
93c21272d1 feat: add search_filter to evaluations 2026-08-14 16:29:51 -05:00
Yiorgis Gozadinos
e522d8cfa8
Remove the wix evaluation dataset 2026-08-14 14:46:51 +03:00
Yiorgis Gozadinos
ba963864f3
Pin the eval judge sampling and standardise on Qwen3-Reranker 2026-08-06 13:17:58 +03:00
Yiorgis Gozadinos
eb11a165b8
Stop loading page rasters on the title and update paths 2026-08-06 13:04:51 +03:00
Yiorgis Gozadinos
788a2fe731
Score retrieval evals by document URI 2026-08-06 13:04:25 +03:00
Yiorgis Gozadinos
a4839c0ad2
Score retrieval evals from search results 2026-08-06 13:04:24 +03:00
Yiorgis Gozadinos
c137305468
Scope the limit notice and spend the cite window on own turns only
The request-limit notice said only the cite tool remained available, but
chat registers rag and analysis in one agent, so exhausting analysis
claimed rag_search was gone too. Scoped to the capability's own tools.

The cite window was counted over every model request once loaded, so
turns spent on another capability expired it before the model was ever
placed where citing was the obvious move. Count only requests whose
preceding response called one of this capability's tools; engagement is
also the only thing that can loop, which is all the bound guards against.

Also: _count_tool_traffic returns a named tuple rather than four bare
ints, and counts failures only for this capability's tools, so host-tool
retries and output-validation retries no longer read as its failures.
2026-07-30 19:14:14 +03:00
Yiorgis Gozadinos
27ed0b3bb3
Send analysis to the sandbox when only its search budget is spent
The notice told the model to answer from what it had the moment
qa.max_searches ran out, while up to 15 code executions remained and
in-code search() does not count against that budget. It now names the
spent tool and points at whichever evidence tool still has budget,
falling back to answer-and-cite only when none do.

Also count RetryPromptPart in n_failed_tools: _cite rejects with
ModelRetry, so a run whose every cite attempt was refused reported zero
failures. And note that n_requests is the run's request count, which
tracks a capability's own budget only while it stays loaded.
2026-07-30 18:17:26 +03:00
Yiorgis Gozadinos
721acbcf38
Address review on PR #524
- _budget_notice no longer names the cite tool after prepare_tools has
  withdrawn it; the post-grace state gets the plain no-tools text back.
- Split search-budget rejections from any failed tool call: the code tool
  raises ToolFailed for every error in model-written Python, so
  budget_spent was true for a ZeroDivisionError.
- docs/capabilities/rag.md described the old single-turn removal.
- Drop the rationale clause from the CHANGELOG entry.
2026-07-30 15:50:06 +03:00
Yiorgis Gozadinos
a528ab912f
Record per-case retrieval diagnostics in evaluations
Count tool traffic from the message history, where refused and repeated
calls stay visible, unlike state.searches which is keyed by query.
2026-07-30 14:56:17 +03:00
Yiorgis Gozadinos
77585ef26a
fix per-question capability limits and tool isolation 2026-07-24 15:26:18 +03:00
Yiorgis Gozadinos
9deb1f2bd4
replace haiku.skills with native Pydantic AI capabilities 2026-07-24 15:26:17 +03:00
Yiorgis Gozadinos
0e8fa551f3
Restore hotpotqa evaluation dataset 2026-07-17 16:25:27 +03:00
Yiorgis Gozadinos
144900d385
Remove the mxbai reranking provider 2026-07-14 11:09:55 +03:00
Yiorgis Gozadinos
e9bd56467c
Add T²-RAGBench leaderboard submission exporter 2026-06-08 11:51:02 +03:00
Yiorgis Gozadinos
a3a73f1331
Add --filter-ids to run QA on a case-id subset 2026-06-08 09:36:01 +03:00
Yiorgis Gozadinos
1d7d540270
Match the numeric scale convention in Number-Match 2026-06-06 14:52:05 +03:00
Yiorgis Gozadinos
d5b0aafa2b
Add T²-RAGBench TAT-DQA subset; generalize subset layout 2026-06-06 14:52:05 +03:00
Yiorgis Gozadinos
de9731d5f3
Score Number-Match on the declared ANSWER line, magnitude and ×100 scale 2026-06-06 14:52:05 +03:00
Yiorgis Gozadinos
390deb4203
Normalize unicode signs 2026-06-06 14:52:04 +03:00
Yiorgis Gozadinos
80594cd38c
Handle percent/decimal convention in Number-Match 2026-06-06 14:52:04 +03:00
Yiorgis Gozadinos
c2a48b55c0
Add deterministic Number-Match QA scoring for T²-RAGBench 2026-06-06 14:52:04 +03:00
Yiorgis Gozadinos
9f1bd9940a
Add T²-RAGBench FinQA evaluation dataset 2026-06-06 14:52:04 +03:00
Yiorgis Gozadinos
e653c49fce
Remove unecessary Mean Reciprocal Rank metric 2026-06-01 10:40:52 +03:00
Yiorgis Gozadinos
00f2a40a60
Refresh benchmarks doc and remove unused eval datasets 2026-06-01 10:40:51 +03:00
Yiorgis Gozadinos
392c74039b
bump pydantic-ai-slim to 1.100 and haiku.skills to 0.17
Migrate off two APIs slated for removal in pydantic-ai 2.0:

- Agent(tool_retries=, output_retries=) -> Agent(retries={"tools": ...,
  "output": ...}) in the LLM-as-judge evaluator.
- Evaluator.evaluation_name class attribute -> overriding
  get_default_evaluation_name() on the citation MRR / MAP evaluators.

haiku.skills 0.17.0 already migrated its internal AGUIAdapter,
MCPToolset and ProcessEventStream usage; no further changes needed on
our side beyond the pin bumps.
2026-05-21 13:20:54 +03:00
Yiorgis Gozadinos
8c57a3ca99
Drop the multi-agent research workflow 2026-05-20 12:46:48 +03:00
Yiorgis Gozadinos
ceb645564c
Drop GEPA prompt optimization and --target qa from evaluations 2026-05-19 11:01:07 +03:00