Add production condition retrieval smoke test
This commit is contained in:
+169
-17
@@ -24,6 +24,7 @@ from typing import Protocol
|
||||
|
||||
from .answer import AnswerBlock, AnswerPlan, Citation, GroundedAnswerService
|
||||
from .budget import RequestBudget
|
||||
from .clinical import ConditionRelation, MedicationCandidateAssessment
|
||||
from .models import EvidenceDecision, RetrievalResult
|
||||
from .policy import looks_non_human
|
||||
from .service import RetrievalService
|
||||
@@ -84,6 +85,7 @@ class AgentReply:
|
||||
blocks: tuple[AnswerBlock, ...] = ()
|
||||
answer_mode: str = "concise"
|
||||
plan: AnswerPlan | None = None
|
||||
candidate_assessments: tuple[MedicationCandidateAssessment, ...] = ()
|
||||
|
||||
|
||||
class RagAgent:
|
||||
@@ -298,6 +300,7 @@ class RagAgent:
|
||||
frame.needs_clarify
|
||||
and frame.clarify_reason
|
||||
and tt != "dosing_calc"
|
||||
and tt != "condition_to_drug"
|
||||
and not section_overview
|
||||
):
|
||||
# `system_error` set means this isn't a real clarify at all — the
|
||||
@@ -321,6 +324,54 @@ class RagAgent:
|
||||
quick_replies=frame.quick_replies,
|
||||
)
|
||||
|
||||
if tt in {"condition_to_drug", "symptom_to_drug"}:
|
||||
if frame.condition and frame.condition.ambiguous:
|
||||
return AgentReply(
|
||||
"clarify",
|
||||
"ambiguous_condition",
|
||||
clarification=(
|
||||
frame.condition.clarify_question
|
||||
or "Anh/chị muốn hỏi loại bệnh cụ thể nào?"
|
||||
),
|
||||
turn_type=tt,
|
||||
)
|
||||
if frame.condition_relation != ConditionRelation.INDICATION:
|
||||
return AgentReply(
|
||||
"abstain",
|
||||
"unsupported_reverse_relation",
|
||||
answer=(
|
||||
"Câu hỏi này đang hỏi quan hệ khác với chỉ định điều trị "
|
||||
"(ví dụ thuốc gây bệnh hoặc chống chỉ định theo bệnh). Hệ "
|
||||
"thống chưa tra ngược quan hệ đó và sẽ không biến nó thành "
|
||||
"danh sách thuốc điều trị."
|
||||
),
|
||||
turn_type=tt,
|
||||
)
|
||||
if frame.condition or frame.indication:
|
||||
return self._condition_to_drug(turn, frame, budget)
|
||||
return AgentReply(
|
||||
"clarify",
|
||||
"no_indication" if tt == "symptom_to_drug" else "no_condition",
|
||||
clarification=(
|
||||
"Anh/chị mô tả triệu chứng hoặc chỉ định cần tra giúp em với?"
|
||||
if tt == "symptom_to_drug"
|
||||
else "Anh/chị muốn tra thuốc có chỉ định cho bệnh/condition nào?"
|
||||
),
|
||||
turn_type=tt,
|
||||
)
|
||||
|
||||
if tt == "condition_relation":
|
||||
return AgentReply(
|
||||
"abstain",
|
||||
"unsupported_reverse_relation",
|
||||
answer=(
|
||||
"Hệ thống nhận ra đây không phải câu hỏi thuốc điều trị bệnh, "
|
||||
"nên không dùng mục Chỉ định để trả lời. Tra ngược thuốc gây "
|
||||
"bệnh/chống chỉ định theo bệnh chưa được hỗ trợ trong phiên bản này."
|
||||
),
|
||||
turn_type=tt,
|
||||
)
|
||||
|
||||
if tt == "drug_attribute" and frame.drugs and frame.attribute is None:
|
||||
return AgentReply(
|
||||
"clarify", "missing_attribute",
|
||||
@@ -354,14 +405,6 @@ class RagAgent:
|
||||
"abstain", "drug_not_in_formulary",
|
||||
answer=f"Không tìm thấy \"{names}\" trong Dược thư Quốc gia Việt Nam.",
|
||||
turn_type=tt)
|
||||
if tt == "symptom_to_drug":
|
||||
if frame.indication:
|
||||
return self._symptom_to_drug(turn, frame, budget)
|
||||
return AgentReply(
|
||||
"clarify", "no_indication",
|
||||
clarification="Anh/chị mô tả triệu chứng hoặc chỉ định cần tra giúp "
|
||||
"em với?",
|
||||
turn_type=tt)
|
||||
return AgentReply(
|
||||
"clarify", "no_drug",
|
||||
clarification="Anh/chị muốn tra thuốc nào?", turn_type=tt)
|
||||
@@ -381,13 +424,25 @@ class RagAgent:
|
||||
section_key = (
|
||||
"lieu_luong_va_cach_dung"
|
||||
if frame.turn_type == "dosing_calc"
|
||||
else frame.attribute
|
||||
else (
|
||||
"chi_dinh" if frame.turn_type == "drug_to_condition" else frame.attribute
|
||||
)
|
||||
)
|
||||
result = self._retrieval.retrieve_framed(
|
||||
frame.drugs[0], section_key, query,
|
||||
is_overview=frame.turn_type == "drug_overview",
|
||||
)
|
||||
return self._grounded(query, result, frame, budget=budget)
|
||||
if frame.patient_context.requires_safety_review:
|
||||
result = self._retrieval.retrieve_patient_drug_context(
|
||||
frame.drugs[0], result, frame.patient_context
|
||||
)
|
||||
return self._grounded(
|
||||
query,
|
||||
result,
|
||||
frame,
|
||||
patient_specific=frame.patient_context.requires_safety_review,
|
||||
budget=budget,
|
||||
)
|
||||
|
||||
def _interaction(self, turn: str, frame: QueryFrame, budget: RequestBudget) -> AgentReply:
|
||||
"""Gather the interaction section of each named drug and synthesise.
|
||||
@@ -424,7 +479,7 @@ class RagAgent:
|
||||
combined = self._retrieval.decide(tuple(evidences))
|
||||
return self._grounded(query, combined, frame, budget=budget)
|
||||
|
||||
def _symptom_to_drug(
|
||||
def _condition_to_drug(
|
||||
self, turn: str, frame: QueryFrame, budget: RequestBudget
|
||||
) -> AgentReply:
|
||||
"""Reverse lookup: a symptom/indication -> which drugs' `chi_dinh`
|
||||
@@ -435,12 +490,15 @@ class RagAgent:
|
||||
Absence is stated plainly, never as "no such drug exists" — the
|
||||
formulary may simply not name this indication under any monograph.
|
||||
"""
|
||||
result = self._retrieval.retrieve_by_indication(frame.indication)
|
||||
condition_text = (
|
||||
frame.condition.retrieval_text if frame.condition else frame.indication
|
||||
)
|
||||
result = self._retrieval.retrieve_by_indication(condition_text)
|
||||
if result.decision == EvidenceDecision.ABSTAIN:
|
||||
return AgentReply(
|
||||
"abstain", result.reason,
|
||||
answer=f"Không tìm thấy thuốc nào trong Dược thư Quốc gia Việt Nam ghi "
|
||||
f"nhận chỉ định cho \"{frame.indication}\". Điều này KHÔNG có "
|
||||
f"nhận chỉ định cho \"{condition_text}\". Điều này KHÔNG có "
|
||||
"nghĩa là không có thuốc điều trị — vui lòng tra theo tên thuốc "
|
||||
"cụ thể nếu đã biết.",
|
||||
turn_type=frame.turn_type)
|
||||
@@ -448,19 +506,65 @@ class RagAgent:
|
||||
# drugs actually found, not `frame.drugs` (empty by construction for
|
||||
# this turn_type; the router only reaches here with no named drug).
|
||||
matched_drugs = tuple(dict.fromkeys(
|
||||
evidence.matched_doc_id.split("__")[0] for evidence in result.evidence
|
||||
evidence.drug_id or evidence.matched_doc_id.split("__")[0]
|
||||
for evidence in result.evidence
|
||||
))
|
||||
return self._grounded(
|
||||
turn, result, frame, drugs=matched_drugs, list_mode=True, budget=budget
|
||||
assessments: tuple[MedicationCandidateAssessment, ...] = ()
|
||||
patient_specific = frame.patient_context.requires_safety_review
|
||||
if patient_specific:
|
||||
result, assessments = self._retrieval.assess_patient_candidates(
|
||||
result, frame.patient_context
|
||||
)
|
||||
if assessments:
|
||||
matched_drugs = tuple(item.drug_id for item in assessments)
|
||||
if result.decision == EvidenceDecision.ABSTAIN:
|
||||
return AgentReply(
|
||||
"abstain",
|
||||
result.reason,
|
||||
answer=(
|
||||
"Có bằng chứng chỉ định cho bệnh chính nhưng chưa tìm thấy đủ "
|
||||
"bằng chứng an toàn liên quan đến dữ kiện người bệnh trong các "
|
||||
"mục Dược thư được tra. Không suy ra thuốc là phù hợp/an toàn."
|
||||
),
|
||||
drugs=matched_drugs,
|
||||
turn_type=frame.turn_type,
|
||||
candidate_assessments=assessments,
|
||||
)
|
||||
generation_query = (
|
||||
_patient_generation_query(frame)
|
||||
if patient_specific
|
||||
else _synthesize_query(turn, frame)
|
||||
)
|
||||
return self._grounded(
|
||||
generation_query,
|
||||
result,
|
||||
frame,
|
||||
drugs=matched_drugs,
|
||||
list_mode=True,
|
||||
patient_specific=patient_specific,
|
||||
assessments=assessments,
|
||||
budget=budget,
|
||||
)
|
||||
|
||||
# Compatibility name for older tests/callers while the public taxonomy
|
||||
# moves from symptom-only wording to condition-centric wording.
|
||||
_symptom_to_drug = _condition_to_drug
|
||||
|
||||
def _grounded(
|
||||
self, turn: str, result: RetrievalResult, frame: QueryFrame,
|
||||
drugs: tuple[str, ...] | None = None, list_mode: bool = False,
|
||||
patient_specific: bool = False,
|
||||
assessments: tuple[MedicationCandidateAssessment, ...] = (),
|
||||
budget: RequestBudget | None = None,
|
||||
) -> AgentReply:
|
||||
ga = self._answers.answer_from_result(
|
||||
turn, result, list_mode=list_mode, budget=budget, prechecked=True
|
||||
turn,
|
||||
result,
|
||||
list_mode=list_mode,
|
||||
patient_specific=patient_specific,
|
||||
candidate_drug_ids=drugs or (),
|
||||
budget=budget,
|
||||
prechecked=True,
|
||||
)
|
||||
decision = ga.result.decision.value
|
||||
if ga.clarification is not None:
|
||||
@@ -479,6 +583,7 @@ class RagAgent:
|
||||
blocks=ga.blocks,
|
||||
answer_mode=ga.answer_mode,
|
||||
plan=ga.plan,
|
||||
candidate_assessments=assessments,
|
||||
)
|
||||
|
||||
def _remember(self, conversation_id: str, turn: str, reply: AgentReply) -> None:
|
||||
@@ -619,6 +724,53 @@ def _synthesize_query(turn: str, frame: QueryFrame) -> str:
|
||||
parts.append(f"Đường dùng: {_ROUTE_LABELS.get(frame.route, frame.route)}")
|
||||
if frame.indication:
|
||||
parts.append(f"Chỉ định/triệu chứng: {frame.indication}")
|
||||
patient = frame.patient_context
|
||||
if patient.primary_condition:
|
||||
parts.append(f"Bệnh chính: {patient.primary_condition}")
|
||||
if patient.comorbidities:
|
||||
parts.append(f"Bệnh nền: {', '.join(patient.comorbidities)}")
|
||||
if patient.allergies:
|
||||
parts.append(f"Dị ứng: {', '.join(patient.allergies)}")
|
||||
if patient.previous_adverse_reactions:
|
||||
parts.append(f"ADR trước đây: {', '.join(patient.previous_adverse_reactions)}")
|
||||
if patient.current_medications:
|
||||
parts.append(f"Thuốc đang dùng: {', '.join(patient.current_medications)}")
|
||||
if patient.renal.present:
|
||||
parts.append(f"Dữ kiện thận: {patient.renal}")
|
||||
if patient.hepatic.present:
|
||||
parts.append(f"Dữ kiện gan: {patient.hepatic}")
|
||||
if patient.pregnancy_status:
|
||||
parts.append(f"Thai kỳ: {patient.pregnancy_status}")
|
||||
if patient.breastfeeding is not None:
|
||||
parts.append(f"Cho con bú: {'có' if patient.breastfeeding else 'không'}")
|
||||
if patient.relevant_labs:
|
||||
parts.append(f"Xét nghiệm: {', '.join(patient.relevant_labs)}")
|
||||
if len(parts) == 1:
|
||||
return turn
|
||||
return ". ".join(parts) + "."
|
||||
|
||||
|
||||
def _patient_generation_query(frame: QueryFrame) -> str:
|
||||
"""Give generation the clinical task without restating user-only values.
|
||||
|
||||
Patient values have already done their job before generation: they select
|
||||
the targeted safety sections. They are not Dược thư evidence themselves.
|
||||
Passing the raw turn here encouraged the model to repeat age, eGFR or CKD
|
||||
grade inside a cited medical claim, which the numeric grounding guard then
|
||||
correctly rejected. The composer therefore receives only the evidenced
|
||||
condition and an instruction to describe the supplied positive evidence;
|
||||
exact patient values remain in structured state and retrieval traces.
|
||||
"""
|
||||
condition = (
|
||||
frame.condition.normalized_condition
|
||||
if frame.condition and frame.condition.normalized_condition
|
||||
else frame.patient_context.primary_condition
|
||||
or frame.indication
|
||||
or "bệnh chính đã nêu"
|
||||
)
|
||||
return (
|
||||
f"Tra cứu các thuốc có bằng chứng chỉ định cho {condition}. "
|
||||
"Đây là ca cụ thể: với từng ứng viên, chỉ trình bày bằng chứng an toàn "
|
||||
"dương tính đã truy xuất liên quan đến dữ kiện người bệnh; không lặp "
|
||||
"lại dữ kiện người bệnh nếu dữ kiện đó không nằm trong đoạn bằng chứng."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user