Found by
test_stream_dispatch_final_fallback_uses_raw_answer_not_the_evidence_pack in #171 (services/ai/tests/unit/test_streaming_dispatch.py), while auditing test coverage for #154.
What happens
Both study_service.dispatch() and study_service.stream_dispatch() share the same final-fallback branch, used when generation fails (or, for streaming, when the stream yields nothing and the buffered retry also fails):
answer = raw_answer or "No relevant content found."
raw_answer is the bare answer string QVAC's own /query endpoint returns alongside the retrieved chunks — not pack.context_block(), the assembled, deduplicated evidence the rest of the pipeline treats as the actual retrieved content.
The gap
If raw_answer happens to be empty while pack.chunks holds real retrieved passages — which QVAC can return, e.g. when its own answer field is blank but sources are populated — the student sees "No relevant content found." even though relevant content was retrieved and is sitting right there in the pack.
This contradicts the graceful-degradation principle stated in docs/overview.md:
With generation disabled, every study action still returns source passages, so the system remains usable on memory-constrained machines.
Here generation isn't even disabled — it just failed — and the evidence still gets dropped.
Suggested fix
Prefer the pack when it has content:
answer = pack.context_block() or raw_answer or "No relevant content found."
This is already the pattern used one branch up, in the not meta.generation_required or rag_only path of dispatch() — so the fix is bringing the failure-fallback branch in line with the retrieval-only branch's existing behaviour, not inventing a new pattern.
Scope
Two call sites, both in services/ai/app/services/study_service.py:
_route()'s else branch (non-streaming dispatch())
stream_dispatch()'s final else branch
Tasks
Acceptance criteria
- A retrieval that returns real chunks but an empty
raw_answer, combined with a generation failure, surfaces the retrieved passages rather than "No relevant content found."
- Behaviour is identical between
dispatch() and stream_dispatch() for this branch, as it already is everywhere else in the file
Found by
test_stream_dispatch_final_fallback_uses_raw_answer_not_the_evidence_packin #171 (services/ai/tests/unit/test_streaming_dispatch.py), while auditing test coverage for #154.What happens
Both
study_service.dispatch()andstudy_service.stream_dispatch()share the same final-fallback branch, used when generation fails (or, for streaming, when the stream yields nothing and the buffered retry also fails):raw_answeris the bareanswerstring QVAC's own/queryendpoint returns alongside the retrieved chunks — notpack.context_block(), the assembled, deduplicated evidence the rest of the pipeline treats as the actual retrieved content.The gap
If
raw_answerhappens to be empty whilepack.chunksholds real retrieved passages — which QVAC can return, e.g. when its own answer field is blank but sources are populated — the student sees "No relevant content found." even though relevant content was retrieved and is sitting right there in the pack.This contradicts the graceful-degradation principle stated in
docs/overview.md:Here generation isn't even disabled — it just failed — and the evidence still gets dropped.
Suggested fix
Prefer the pack when it has content:
This is already the pattern used one branch up, in the
not meta.generation_required or rag_onlypath ofdispatch()— so the fix is bringing the failure-fallback branch in line with the retrieval-only branch's existing behaviour, not inventing a new pattern.Scope
Two call sites, both in
services/ai/app/services/study_service.py:_route()'selsebranch (non-streamingdispatch())stream_dispatch()'s finalelsebranchTasks
pack.context_block()test_stream_dispatch_final_fallback_uses_raw_answer_not_the_evidence_pack— it currently documents the old behaviour on purpose; once fixed it should assert the passage text is presentdispatch()path and add one if missingpack.chunksis genuinely empty (should still fall through toraw_answer, then the generic message)Acceptance criteria
raw_answer, combined with a generation failure, surfaces the retrieved passages rather than "No relevant content found."dispatch()andstream_dispatch()for this branch, as it already is everywhere else in the file