Fix ai-service Dockerfile: bake in drug_entities.json, override its path
This commit is contained in:
@@ -32,6 +32,9 @@ class EvidencePolicy:
|
||||
# A free-form question about a resolved drug otherwise hands the LLM the
|
||||
# entire monograph; rerank trims it to the sections that actually answer.
|
||||
rerank_top_k: int = 6
|
||||
# symptom_to_drug: a common symptom can match far more drugs than is
|
||||
# useful to show in one answer.
|
||||
indication_candidate_limit: int = 8
|
||||
|
||||
|
||||
class RetrievalService:
|
||||
@@ -157,6 +160,47 @@ class RetrievalService:
|
||||
return RetrievalResult(EvidenceDecision.ABSTAIN, "insufficient_retrieval_score")
|
||||
return self._decide(self._hydrate(self._rerank(query, hits)))
|
||||
|
||||
def retrieve_by_indication(self, indication_text: str) -> RetrievalResult:
|
||||
"""Reverse lookup: symptom/indication -> candidate drugs.
|
||||
|
||||
Keyword match first (deterministic, precise — nothing here can
|
||||
fabricate a drug that doesn't genuinely mention the indication).
|
||||
Dense-vector search over `chi_dinh` only is the fallback, tried
|
||||
only when the keyword pass finds nothing, to catch a paraphrase the
|
||||
book's own wording doesn't share. This is the one place in the live
|
||||
path dense search is actually used — see ADR 0008.
|
||||
"""
|
||||
if not indication_text.strip():
|
||||
return RetrievalResult(EvidenceDecision.ABSTAIN, "missing_indication")
|
||||
|
||||
find_by_indication = getattr(self._retriever, "find_by_indication", None)
|
||||
hits = (
|
||||
find_by_indication(indication_text, self._policy.indication_candidate_limit)
|
||||
if find_by_indication is not None
|
||||
else []
|
||||
)
|
||||
if not hits:
|
||||
search_indication = getattr(self._retriever, "search_indication", None)
|
||||
if search_indication is not None:
|
||||
try:
|
||||
hits = search_indication(
|
||||
indication_text, self._policy.indication_candidate_limit
|
||||
)
|
||||
except QueryEmbeddingUnavailable:
|
||||
hits = []
|
||||
# Dense search always returns its nearest neighbours, even for
|
||||
# an indication the corpus has nothing on — verified live: a
|
||||
# made-up phrase still got 8 unrelated "matches". A weak top
|
||||
# score means those neighbours aren't really about the
|
||||
# question, so don't spend a generation call finding that out
|
||||
# the slow way; abstain here, the same bar `retrieve()`'s own
|
||||
# dense fallback already applies.
|
||||
if hits and hits[0].score < self._policy.minimum_score:
|
||||
hits = []
|
||||
if not hits:
|
||||
return RetrievalResult(EvidenceDecision.ABSTAIN, "no_indication_match")
|
||||
return self._decide(self._hydrate(hits, limit=None))
|
||||
|
||||
@staticmethod
|
||||
def _is_question(query: str) -> bool:
|
||||
"""A bare drug name (one or two tokens) wants the whole monograph; more
|
||||
@@ -206,8 +250,29 @@ class RetrievalService:
|
||||
if match is None:
|
||||
return None
|
||||
hits = find_by_section(drug_id, match.section_key)
|
||||
if match.section_key == "than_trong":
|
||||
# A "thận trọng" question about a specific condition sometimes has
|
||||
# its real answer filed under "chống chỉ định" instead — found live
|
||||
# 2026-08-10: Aspirin's own "thận trọng" text never says "loét dạ
|
||||
# dày", the fact only exists in its "chống chỉ định" text ("loét
|
||||
# dạ dày hoặc tá tràng đang hoạt động"). The two are the closest
|
||||
# pair of "is this safe for my patient" categories the book has,
|
||||
# and chống chỉ định text is short — pooling it costs nothing on
|
||||
# a drug where than_trong already answers, and prevents a false
|
||||
# "not in this source" clarify/abstain on one where it doesn't.
|
||||
hits = hits + find_by_section(drug_id, "chong_chi_dinh")
|
||||
return hits or None
|
||||
|
||||
def decide(self, evidence: tuple[Evidence, ...]) -> RetrievalResult:
|
||||
"""Public entry point for a caller that assembles its own evidence
|
||||
pool across several `retrieve_framed` calls — e.g. `RagAgent`'s
|
||||
2-drug interaction path — and needs the same quarantine/provenance
|
||||
policy applied to the combined pool that a single call already gets.
|
||||
Bypassing this (hand-rolling `RetrievalResult(ANSWERABLE, ...)`) is
|
||||
exactly how the interaction path silently dropped a quarantined
|
||||
drug's evidence instead of surfacing VERIFY_PDF for it."""
|
||||
return self._decide(evidence)
|
||||
|
||||
def _decide(
|
||||
self, evidence: tuple[Evidence, ...], is_drug_overview: bool = False
|
||||
) -> RetrievalResult:
|
||||
|
||||
Reference in New Issue
Block a user