Repository navigation
Let the classifier choose collections, and measure what that is worth - #262
Merged
Merged
Conversation
Completes spec 009. `QueryIntent` gains a `collections` field, the classifier prompt describes each collection from `reactome_descriptions_info` rather than a literal list, and `generate_answer` sets the selection around retrieval. The selection rides the `selected_collections` ContextVar rather than `config["configurable"]` as tasks.md assumed: `create_retrieval_chain` gives the retriever no path for extra arguments. LangGraph copies the context into the tasks it spawns, so a value set around the call reaches the retriever inside them. It is reset in `finally`, because the graph reuses one asyncio task across turns and a selection left set would narrow the next question -- which nothing downstream could detect, since narrowing yields a confident answer from less. Three measurements, and the middle one is not the result this feature was pitched on. It holds the bar. The sweep is 13/13 with the classifier choosing, which is the control score, and five of six retrievals were narrowed. The routing is the mapping the earlier measurement predicted: the accession question to `ewas`, both variant questions to `disease_variants`, the summary question to `summations`, and the broad mechanistic one left empty. It does not reduce the prompt. Ten documents reach the model either way and context characters are within 2%, because the chain caps the fused list at ten however many collections fed it. Narrowing changes which documents arrive, not how many. T003's framing -- that context tokens would fall -- was the wrong thing to measure, and it is corrected rather than quietly dropped. What it does reduce is retrieval work: 1.82s to 1.43s median, three runs per question, timed inside `aretrieve_documents`. Five collections mean ten sub-retrievals; one means two. About a fifth, or 0.4s against a first token near ten seconds. So the honest case is narrower than the spec's: it did not fix a failing question, the control already passed. It buys a fifth off retrieval and puts the right documents in a fixed-size context, and only the first of those is measured. Tests cover the dangerous directions rather than the happy path: that an omitted field still means all collections, that a reactome selection never leaks into the userguide bundle, that the selection does not outlive the question, and that the sync and async retrieval paths honour the same selection -- the served path and the measured path are different code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…route right Two things the first commit did not cover. `state["collections"]` would raise. BaseState is total=False and the field is new, so a thread resumed from a checkpoint written before this landed has no key at all -- a KeyError on the served path, in a state that is invisible until someone resumes an old conversation. Now `.get(...) or []`, which means every collection, which is how the graph behaved before routing. Pinned by a test that deletes the key and fails against the subscript. And the routing evidence was six questions, all of them ones the sweep already asks. Probed with nine it has never seen, chosen to be awkward: three broad questions that must not narrow, two cross-collection ones where a single choice loses half the answer, four specific ones. Nine of nine as intended -- every broad question left open, every specific one narrowed to the collection holding the answer, and both cross-collection questions took `summations` alongside their primary, which is what the prompt asks for. Recorded with its limit: that is nine classifications, not nine answers. It shows the decision is sound, not that the answers are. Also written down rather than left implicit: `collections` is `list[str]` and not an enum of the five names on purpose. A hallucinated name widens to all with a WARNING, which is the direction this feature requires. Constraining the schema would make structured output reject the response instead, turning a harmless mistake into a failed answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sumed CI's lint failed where mine passed, because CI runs `poetry run mypy` over everything and I ran `mypy src`. Verifying a narrower scope than the gate is the mistake; the finding underneath it is real. `ReactToMeState` subclasses a total=False TypedDict but is itself total=True, so declaring `collections: list[str]` made it a *required* key. That invalidated four existing state constructions, and it claimed a guarantee the runtime does not have -- the previous commit had already established that a thread resumed from an older checkpoint has no such key, which is why `generate_answer` reads it with `.get`. `NotRequired[list[str]]` says what is true: absent and empty both mean every collection, and the type no longer disagrees with the accessor. Also replaced a `type: ignore[attr-defined]` in the new test that was hiding a second error rather than describing one. 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.
Completes spec 009. The classifier now chooses which Reactome collections to search, and the measurement says what that is actually worth — which is less than the spec claimed, in a way worth reading.
What landed
QueryIntentgainscollections; the prompt describes each collection fromreactome_descriptions_inforather than a literal list, so a collection added to the bundle cannot be missing from the prompt meant to offer it;generate_answersets the selection around retrieval.It rides the
selected_collectionsContextVar, notconfig["configurable"]as tasks.md assumed —create_retrieval_chaingives the retriever no path for extra arguments. LangGraph copies the context into spawned tasks, so a value set around the call reaches the retriever inside them. Reset infinally: the graph reuses one asyncio task across turns, and a selection left set would narrow the next question, which nothing downstream could detect because narrowing yields a confident answer from less.Three measurements
It holds the bar. Sweep is 13/13 with the classifier choosing — the control score — with 5 of 6 retrievals narrowed, routing exactly as the earlier measurement predicted: accession →
ewas, both variant questions →disease_variants, summary →summations, broad mechanistic → left open.It does not reduce the prompt. This corrects the spec.
Ten documents either way, context within 2% — the chain caps the fused list at ten however many collections fed it. T003's framing, that context tokens would fall, was measuring the wrong thing.
What it reduces is retrieval work: 1.82s → 1.43s median, three runs per question, timed inside
aretrieve_documents. Five collections mean ten sub-retrievals; one means two. About a fifth, or 0.4s against a first token near ten seconds.The honest case
It did not fix a failing question — the control already passed 13/13. It buys a fifth off retrieval and puts the right documents into a fixed-size context. Only the first of those is measured.
Adversarial review found two things
state["collections"]would raise.BaseStateistotal=Falseand the field is new, so a thread resumed from a pre-existing checkpoint has no key — aKeyErroron the served path, invisible until someone resumes an old conversation. Now.get(...) or [], pinned by a test that deletes the key and fails against the subscript.Six questions was a thin base to ship a router on. Probed with nine unseen and deliberately awkward ones — three broad that must not narrow, two cross-collection, four specific. Nine of nine as intended, with both cross-collection questions taking
summationsalongside their primary. Recorded with its limit: nine classifications, not nine answers.Also documented:
collectionsislist[str]and not an enum on purpose. A hallucinated name widens to all with a WARNING; constraining the schema would make structured output reject the response, turning a harmless mistake into a failed answer.Still open
complexeshas no guard question (T005), so a classifier that never routes to it would still score full marks.Checks
501 passed, 1 skipped. ruff, ruff format, mypy clean.
🤖 Generated with Claude Code