Ask the chatbot the questions it has got wrong before - #223
Merged
Merged
Conversation
Every regression this week was found the same way: someone asked beta a
question and the answer was wrong. The safety checker refusing "can you run
gsea for me". Ordinary retrieval taken down for a day by a shared Chroma
Settings object. A live answer that was correct and displayed nothing. Each was
caught by a person noticing, which is slow and only covers questions people
happen to ask.
reactome-mcp has a sweep that calls every tool and checks the answers contain
what they should; it has caught several real bugs, including one I had
introduced that morning. This is the same idea for the chatbot.
./bin/answer-sweep
Eleven questions, each carrying what a good answer must and must not contain,
run end to end through the compiled graph. 11/11 against the deployed container
in 151s, exit 0.
`must_not` earns its place. Most of the failures this replaces produced
confident, plausible prose: "Reactome does not provide a specific tool" is a
fluent sentence and a false one about the flagship feature. Checking only for
presence would have passed every one of them.
Each expectation records *why* it is checked, printed on failure, so whoever
sees a red line learns what broke last time rather than guessing at intent.
**It found a real degradation on its first run against production.**
reactome.org served a Cloudflare challenge to a burst of requests and the
species answer became "I could not find out ... due to a service error" --
which is the error handling working rather than a regression. So transient
upstream failures are retried once and the retry is reported: a sweep that
cries wolf gets ignored, which is how reactome-mcp's own sweep would have gone
had it stayed red from day one.
Not the evaluator. evaluator.py scores answer quality with ragas and costs real
money; this asks whether the chatbot still does what it was fixed to do, in two
and a half minutes, and is meant to run before every deploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An adversarial read of my own PR found three things wrong with a file whose
entire job is catching regressions:
The comment on `_contains` claimed "96 should not match 1996". It did:
`re.escape` escapes metacharacters and adds no boundaries. That made the
release check -- `must=("9",)` -- match any answer containing a digit 9,
which is close to asserting nothing. `_contains` now anchors at word edges
where the needle has them, so "R-HSA-" still matches a prefix, and the
release question asserts a plausible release number instead of a literal,
because 97 becomes 98 shortly and a check that fails on a correct answer
gets switched off.
`retried` was set on the result the retry then threw away, so a question
that needed a second attempt was reported as a clean pass -- the silent
retry the comment right above it promises not to do.
`max_seconds` was declared and never read, and the docstring advertised a
`--in-container` flag that argparse would reject. The deploy script runs it
through `docker exec`, so the docstring now shows that.
The tests drive `run()` against a stub graph, so they test the harness
rather than any answer the real chatbot gives, and the two that matter were
checked against the bugs they were written for: each fails when the fix is
reverted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Run from a plain checkout, the two questions that need the live service cannot pass, and the sweep called them regressions -- under a line that says "a question the chatbot used to get wrong and does again". A gate that fails for a reason the reader cannot act on is a gate people learn to skip. They are now marked `needs_live` and skipped, loudly, when no MCP server is configured. The skip is deliberately narrow: a test covers the dangerous direction, that they still run when MCP *is* configured, because silently skipping them inside the container would stop checking the questions the live service exists to answer. Checking them properly then showed two of my own checks were wrong. The release question required the literal word "release"; the live answer says "The current version of Reactome is 97", which is correct and was reported as a failure. And the species check asserted the literal "96", which goes up. Both now match a pattern rather than a literal, for the same reason: a check that fails on a correct answer is one that gets switched off. Verified against the running MCP sibling: 11/11 with the stricter checks, and 9/9 with 2 skipped from a plain local checkout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The word boundaries I added two commits ago closed the pattern at both ends, which is right for a number -- "96" must not match "1996" or "965" -- and wrong for a word, because an inflection is the same word. "consult" is a `must_not` guarding the medical-advice answer. Closed at both ends it stopped matching "consulting your physician", so the check quietly stopped catching the thing it exists for. Same for "muscle pain" against "muscle pains". The end is now closed only for a number. Checked against the reverted version: the test fails there and passes here. 11/11 still passes against the running MCP sibling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Every regression this week was found the same way: someone asked beta a question and the answer was wrong.
SettingsobjectEach was caught by a person noticing, which is slow and only covers questions people happen to ask.
reactome-mcp has a sweep that calls every tool and checks the answers contain what they should. It has caught several real bugs, including one I'd introduced that morning. This is the same idea for the chatbot.
11/11 against the deployed container in 151s, exit 0.
must_notearns its placeMost of the failures this replaces produced confident, plausible prose. "Reactome does not provide a specific tool" is a fluent sentence and a false one about the flagship feature. Checking only for presence would have passed every one of them.
Each expectation also records why it's checked, printed on failure — so whoever sees a red line learns what broke last time rather than guessing at intent:
It found a real degradation on its first production run
That failure above is genuine. reactome.org served a Cloudflare challenge to a burst of requests, and the species answer degraded — which is the error handling working rather than a regression.
So transient upstream failures are retried once, and the retry is reported. A sweep that cries wolf gets ignored; that's exactly how reactome-mcp's own sweep would have gone had it stayed red from day one. The retry is deliberately narrow — only an exception, or an answer that says the lookup failed.
Not the evaluator
evaluator.pyscores answer quality with ragas and costs real money. This asks something cheaper — is the chatbot still doing the thing it was fixed to do — in two and a half minutes, and is meant to run before every deploy.🤖 Generated with Claude Code
What an adversarial review of this PR found
I attacked this file on the grounds that a regression gate nobody has seen fail is not yet a gate. Five things, four of them mine:
The matching comment was false. It claimed
"96"would not match"1996". It did —re.escapeescapes metacharacters and adds no boundaries. That mattered because the release check wasmust=("9",), which matches any answer containing a digit 9. The earlier 11/11 was real, but two of those checks were asserting close to nothing.A retried question reported as a clean pass.
retriedwas set on theResultthe retry then threw away, so the(retried once)note could never print — the silent retry the comment right above it promises not to do.max_secondswas declared and never read, and the docstring advertised a--in-containerflag argparse would reject.Fixing the first one weakened a safety guard. Word boundaries at both ends stopped
"consult"matching"consulting your physician"— and"consult"is themust_notguarding the medical-advice answer. The end is now closed only for a number, where a longer one is a different number.Two checks failed on correct answers. Run against live MCP, the release question failed on "The current version of Reactome is 97" because I required the literal word "release"; the service says "version". And
must=("96",)for the species count goes up over time. Both now match a pattern. A check that fails on a correct answer is one that gets switched off.Skipping, rather than crying wolf
From a plain checkout there is no
REACTOME_MCP_URL, so the two live-service questions could never pass — and were reported as regressions under a line reading "a question the chatbot used to get wrong and does again". They're nowneeds_liveand skipped loudly. The skip is deliberately narrow, and a test covers the dangerous direction: that they still run when MCP is configured, so the container never silently stops checking them.Evidence
9/9 passed, 2 skippedfrom a plain local checkout11/11 passedagainst the running MCP sibling, with the stricter checksThe tests drive
run()against a stub graph, so they test the harness rather than any answer the real chatbot gives — the sweep can now be shown to fail, which is the only thing that makes a green one mean anything.