Evaluator: survive a failed question, and keep what was already paid for - #211
Merged
Merged
Conversation
A run is bought question by question -- a rephrase call, a retrieval, a generation, then a judge pass over all of it. The report was written once, at the very end, after every model and every repeat. So a rate limit on question 18 of 20 raised, unwound the loop, and discarded the seventeen answers already purchased. Nothing was on disk. That is the reason the evaluation is unpleasant to run, and the reason it has not been run on the four retrieval changes that are owed one. **A failed question no longer ends the run.** It is named on stderr, listed under `failed_questions` in the report, and the rest are scored without it. **The references are re-aligned to the questions that survived.** This is the part that had to be got right: dropping a question from the middle and leaving the reference list alone would score every later answer against the wrong reference. That does not raise -- it produces numbers, and they look like a result. `_kept` filters the question list by the same indices, and a test pins the pairing. **Answers are written as they are produced.** `--transcript-log` appends one JSON object per answer and flushes, so whatever was bought survives the process that bought it. **`--concurrency` answers several questions at once.** Questions are independent, so this does not change what is measured. Results are placed by index rather than appended as they arrive -- appending would order them by completion time, which is the reference-misalignment bug by another route. Default is 1, so nothing changes unless asked. Six tests, with fakes rather than API calls. Four of them fail against an append-based implementation, which is the mutation worth guarding: order by arrival is plausible, passes casual inspection, and silently scores the wrong pairs. Verified: ruff and ruff format clean (the repo's pinned 0.7.4 -- a newer ruff reports RUF100 here for a rule this repo does not enable), mypy clean, full suite 237 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI runs `mypy` over the whole tree; I had only run it against src/evaluation/evaluator.py, so the fakes in the new tests went unchecked and the lint job went red on a file I had just added. `_FakeRephrase.invoke` returned `payload["user_input"]` -- `Any` out of a bare `dict` -- from a function annotated `-> str`. Both fakes now take `dict[str, Any]` and say what they return. Run `poetry run ruff check . && poetry run ruff format --check . && poetry run mypy` before pushing; a subset is not the gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e_chatbot into fix/evaluator-hardening
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A run is bought question by question — a rephrase call, a retrieval, a generation, then a judge pass over all of it. The report was written once, at the very end, after every model and every repeat.
So a rate limit on question 18 of 20 raised, unwound the loop, and discarded the seventeen answers already purchased. Nothing was on disk.
That is why the evaluation is unpleasant to run, and why it has not been run on the four retrieval changes that are owed one.
A failed question no longer ends the run
It is named on stderr, listed under
failed_questionsin the report, and the rest are scored without it.The references are re-aligned to the questions that survived
This is the part that had to be got right. Dropping a question from the middle and leaving the reference list alone would score every later answer against the wrong reference — and that does not raise. It produces numbers, and they look like a result.
_keptfilters the question list by the same indices, and a test pins the pairing.Answers are written as they are produced
--transcript-log run.jsonlappends one JSON object per answer and flushes, so whatever was bought survives the process that bought it.--concurrencyanswers several at onceQuestions are independent, so this does not change what is measured. Results are placed by index, not appended as they arrive — appending would order them by completion time, which is the reference-misalignment bug by another route.
Default is 1, so nothing changes unless asked for.
Tests
Six, with fakes rather than API calls. Four fail against an append-based implementation, which is the mutation worth guarding against: ordering by arrival is plausible, passes casual inspection, and silently scores the wrong pairs.
Verification
ruffandruff formatclean,mypyclean, full suite 237 passed.venv. The repo pins^0.7.1; a newer ruff reportsRUF100intests/evaluation/for a rule this repo does not enable — the same version trap that made LNG parameterize OpenAI model #75 look broken.Why this one first
It is the gate on everything else in the backlog. #116, #133, #144 and #153 all change what reaches the LLM, and the constitution requires a before-and-after measurement on real questions for exactly that. It is also the gating work for spec 002's D1–D3, and it is owed anyway for four retrieval changes that shipped unevaluated.
🤖 Generated with Claude Code