Fix live multi-turn: pass the resolved drug, stop did-you-mean garbage

This commit is contained in:
2026-08-05 16:54:35 +07:00
parent ef08b4929e
commit 1e8cbdb586
29 changed files with 2013 additions and 83 deletions
@@ -0,0 +1,92 @@
"""A free-form question about a resolved drug must not dump the whole monograph
at the model. When a reranker is configured, the overview is reordered by
relevance and trimmed to top-k; a bare drug name still returns everything.
"""
from __future__ import annotations
from rag.models import EvidenceDecision, RetrievalDocument, SearchHit, SourceRef
from rag.ports import RerankUnavailable
from rag.service import EvidencePolicy, RetrievalService
def _hit(i: int) -> SearchHit:
doc = RetrievalDocument(
doc_id=f"d{i}",
drug_id="paracetamol",
kind="prose",
text=f"section {i} text",
section_key=f"sec_{i}",
source_refs=(
SourceRef(physical_page=100 + i, precision="exact", printed_page=i),
),
)
return SearchHit(document=doc, score=1.0)
class _Retriever:
def __init__(self, n: int) -> None:
self._hits = [_hit(i) for i in range(n)]
def find_by_drug(self, drug_id: str) -> list[SearchHit]:
return list(self._hits)
def search(self, query: str, drug_id: str, limit: int) -> list[SearchHit]: # noqa: ARG002
return []
class _ParentStore:
def get(self, parent_id: str): # noqa: ARG002
return None
class _Reranker:
def __init__(self, order: list[int], fail: bool = False) -> None:
self._order = order
self._fail = fail
self.calls: list[str] = []
def rerank(self, query: str, documents: list[str], top_n=None): # noqa: ARG002
self.calls.append(query)
if self._fail:
raise RerankUnavailable("provider down")
return self._order
def _service(reranker=None, top_k=3) -> RetrievalService:
return RetrievalService(
_Retriever(8),
_ParentStore(),
EvidencePolicy(rerank_top_k=top_k),
section_resolver=None,
reranker=reranker,
)
def test_a_question_reranks_the_overview_and_keeps_top_k():
reranker = _Reranker(order=[7, 6, 5, 4, 3, 2, 1, 0])
result = _service(reranker, top_k=3).retrieve(
"sốt cao uống được không", "paracetamol"
)
assert result.decision == EvidenceDecision.ANSWERABLE
assert reranker.calls, "reranker should run on a free-form question"
assert len(result.evidence) == 3
assert [e.matched_doc_id for e in result.evidence] == ["d7", "d6", "d5"]
def test_a_bare_drug_name_returns_the_whole_monograph_unreranked():
reranker = _Reranker(order=[0])
result = _service(reranker).retrieve("PARACETAMOL", "paracetamol")
assert reranker.calls == [], "a bare name must not be reranked/trimmed"
assert len(result.evidence) == 8
def test_rerank_outage_keeps_the_original_order():
reranker = _Reranker(order=[], fail=True)
result = _service(reranker).retrieve("sốt cao uống được không", "paracetamol")
# Fail-open: the answer survives, in book order, when rerank is unreachable.
assert result.decision == EvidenceDecision.ANSWERABLE
assert len(result.evidence) == 8
assert [e.matched_doc_id for e in result.evidence][:2] == ["d0", "d1"]