Add production condition retrieval smoke test
This commit is contained in:
@@ -40,7 +40,17 @@ from dataclasses import dataclass, field, replace
|
||||
from typing import Protocol, Sequence
|
||||
|
||||
from .budget import RequestBudget
|
||||
from .clinical import (
|
||||
CaseContextAction,
|
||||
ConditionNormalizer,
|
||||
ConditionQuery,
|
||||
ConditionRelation,
|
||||
HepaticContext,
|
||||
PatientContext,
|
||||
RenalContext,
|
||||
)
|
||||
from .ports import AnswerGenerationUnavailable
|
||||
from .text import normalize_name
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -114,6 +124,9 @@ TURN_TYPES = (
|
||||
"drug_overview", # a bare drug name, wants the monograph
|
||||
"interaction", # 2+ drugs, asks about combining them
|
||||
"symptom_to_drug", # a symptom/indication, wants candidate drugs
|
||||
"condition_to_drug", # a diagnosed disease/condition -> indicated drugs
|
||||
"drug_to_condition", # what condition(s) a named drug is indicated for
|
||||
"condition_relation", # reverse ADR/contraindication relation, not treatment
|
||||
"dosing_calc", # a dose that needs weight/age arithmetic
|
||||
"smalltalk", # greeting / meta, not a medical query
|
||||
"out_of_scope", # not answerable from the Part-2 monographs
|
||||
@@ -132,6 +145,10 @@ class QueryFrame:
|
||||
weight_kg: float | None = None
|
||||
age_text: str | None = None
|
||||
indication: str | None = None # symptom/disease, for symptom_to_drug
|
||||
condition: ConditionQuery | None = None
|
||||
condition_relation: ConditionRelation = ConditionRelation.INDICATION
|
||||
patient_context: PatientContext = field(default_factory=PatientContext)
|
||||
context_action: CaseContextAction = CaseContextAction.NONE
|
||||
route: str | None = None # e.g. "uong", "tiem_tinh_mach", "dat_truc_trang"
|
||||
# True when the user is asking to survey/summarise a whole named section
|
||||
# (for example all ADRs, precautions, or dosage regimens), rather than
|
||||
@@ -178,6 +195,55 @@ FRAME_SCHEMA = {
|
||||
),
|
||||
"age_text": "the age exactly as stated (e.g. '3 tuổi', '5 tháng'), else null",
|
||||
"indication": "the symptom or disease if turn_type is symptom_to_drug, else null",
|
||||
"condition": {
|
||||
"original_text": "condition phrase exactly as written, or null",
|
||||
"normalized_condition": (
|
||||
"conservative canonical condition name; expand only an unambiguous "
|
||||
"abbreviation (e.g. THA -> tăng huyết áp), or null"
|
||||
),
|
||||
"subtype": "explicit subtype only (e.g. B, mạn), else null",
|
||||
"qualifiers": ["only qualifiers explicitly present in the turn/history"],
|
||||
"ambiguous": "true when subtype materially changes the answer",
|
||||
"clarify_question": "short Vietnamese clarification if ambiguous, else null",
|
||||
},
|
||||
"condition_relation": (
|
||||
"indication | adverse_effect | contraindication | unknown. "
|
||||
"'thuốc nào gây X' is adverse_effect; 'thuốc nào chống chỉ định ở X' "
|
||||
"is contraindication, never indication"
|
||||
),
|
||||
"patient_context": {
|
||||
"age_text": "age exactly as stated, else null",
|
||||
"sex": "sex exactly/briefly as stated, else null",
|
||||
"weight_kg": "number only when stated, else null",
|
||||
"primary_condition": "the condition being treated, else null",
|
||||
"comorbidities": ["diagnosed comorbidities explicitly stated"],
|
||||
"allergies": ["drug/substance allergies explicitly stated"],
|
||||
"previous_adverse_reactions": ["previous ADRs explicitly stated"],
|
||||
"current_medications": ["current medicine names explicitly stated"],
|
||||
"pregnancy_status": "pregnancy information explicitly stated, else null",
|
||||
"breastfeeding": "true/false only when explicitly stated, else null",
|
||||
"renal": {
|
||||
"description": "renal condition wording, else null",
|
||||
"ckd_stage": "e.g. G4, else null",
|
||||
"egfr": "value with unit/text exactly as stated, else null",
|
||||
"crcl": "value with unit/text exactly as stated, else null",
|
||||
"creatinine": "value with unit/text exactly as stated, else null",
|
||||
},
|
||||
"hepatic": {
|
||||
"description": "hepatic condition wording, else null",
|
||||
"child_pugh": "class/score exactly as stated, else null",
|
||||
"ast": "value exactly as stated, else null",
|
||||
"alt": "value exactly as stated, else null",
|
||||
"bilirubin": "value exactly as stated, else null",
|
||||
},
|
||||
"relevant_labs": ["other clinical labs exactly as stated"],
|
||||
"treatment_history": ["treatments tried/failed exactly as stated"],
|
||||
},
|
||||
"context_action": (
|
||||
"continue when this turn belongs to the same patient/case as recent "
|
||||
"history; new when the user explicitly starts another case/patient/topic; "
|
||||
"none when no patient case continuity is involved"
|
||||
),
|
||||
"route": (
|
||||
"route of administration if stated or implied, normalized to one of: "
|
||||
"uong | tiem_tinh_mach | tiem_bap | tiem_duoi_da | dat_truc_trang | "
|
||||
@@ -233,8 +299,28 @@ Quy tắc bắt buộc:
|
||||
- Sai chính tả một thuốc CÓ trong danh sách thì sửa về đúng drug_id của nó
|
||||
(ví dụ "amoxicillin" -> "amoxicilin", "metfomin" -> "metformin").
|
||||
- Nếu câu nhắc 2 thuốc trở lên và hỏi về dùng chung/tương tác -> turn_type="interaction".
|
||||
- Nếu là triệu chứng/bệnh cần gợi ý thuốc (không nêu tên thuốc) -> "symptom_to_drug",
|
||||
điền "indication".
|
||||
- Nếu là BỆNH/CONDITION đã nêu và hỏi thuốc nào có chỉ định điều trị ->
|
||||
"condition_to_drug", điền `condition`, `condition_relation="indication"`.
|
||||
Có thể dùng "symptom_to_drug" cho triệu chứng chưa phải chẩn đoán; không đánh
|
||||
đồng triệu chứng với bệnh đã chẩn đoán.
|
||||
- Nếu hỏi một THUỐC đã nêu được chỉ định cho bệnh gì -> "drug_to_condition",
|
||||
attribute="chi_dinh". Đây là chiều ngược với condition_to_drug.
|
||||
- Phân biệt QUAN HỆ: "thuốc nào GÂY tăng huyết áp" ->
|
||||
turn_type="condition_relation", condition_relation="adverse_effect"; "thuốc
|
||||
nào CHỐNG CHỈ ĐỊNH ở bệnh nhân gout" -> "condition_relation",
|
||||
condition_relation="contraindication". TUYỆT ĐỐI không gán hai câu này thành
|
||||
condition_to_drug/indication.
|
||||
- Chuẩn hoá condition bảo thủ: "cao huyết áp"/"THA" -> "tăng huyết áp" khi
|
||||
chắc chắn; giữ nguyên viết tắt mơ hồ. "Viêm gan", "ung thư", "nhiễm trùng"
|
||||
không có subtype/vị trí là mơ hồ đáng kể -> ambiguous=true và hỏi làm rõ.
|
||||
"Tăng huyết áp dùng thuốc gì?" không mơ hồ và không cần hỏi tuổi/xét nghiệm.
|
||||
- Nếu câu hỏi có dữ liệu người bệnh, điền `patient_context` bằng ĐÚNG dữ kiện
|
||||
được nêu; không suy ra field còn thiếu. Bệnh nền, thuốc đang dùng, dị ứng/ADR,
|
||||
suy thận/gan, thai/cho bú và xét nghiệm là dữ liệu first-class, không bỏ vào
|
||||
một chuỗi ghi chú chung.
|
||||
- Chỉ đặt context_action="continue" khi lượt hiện tại thực sự tiếp tục CÙNG ca
|
||||
bệnh trong lịch sử. Nếu người dùng nói ca mới/BN khác hoặc chuyển chủ đề độc
|
||||
lập, đặt "new" và không mang dữ kiện bệnh nhân cũ sang.
|
||||
- Nếu hỏi liều cần cân nặng/tuổi -> "dosing_calc", điền weight_kg/age_text nếu có.
|
||||
Nói cân nặng kiểu thường ngày ("bé 30 cân", "nặng 30 ký", chỉ 1 số + "cân"/"ký"
|
||||
không kèm đơn vị khác) NGHĨA LÀ 30 kg -> điền weight_kg=30, không bỏ trống.
|
||||
@@ -337,10 +423,17 @@ class LlmQueryUnderstander:
|
||||
safer and cheaper.
|
||||
"""
|
||||
|
||||
def __init__(self, llm: JsonLlm, catalog: dict[str, str], resolver: CandidateSource) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
llm: JsonLlm,
|
||||
catalog: dict[str, str],
|
||||
resolver: CandidateSource,
|
||||
condition_normalizer: ConditionNormalizer | None = None,
|
||||
) -> None:
|
||||
self._llm = llm
|
||||
self._catalog = catalog
|
||||
self._resolver = resolver
|
||||
self._condition_normalizer = condition_normalizer or ConditionNormalizer()
|
||||
|
||||
def _candidate_ids(self, turn: str, history: Sequence[str]) -> set[str]:
|
||||
"""Every drug_id a deterministic pass finds plausible in the turn or
|
||||
@@ -433,7 +526,16 @@ class LlmQueryUnderstander:
|
||||
"sau ít phút.",
|
||||
system_error="understanding_provider_unavailable",
|
||||
)
|
||||
return _merge_with_prior_frame(self._parse(raw_text, shown), prior_frame)
|
||||
frame = self._parse(raw_text, shown, turn)
|
||||
frame = _apply_condition_candidate_cue(
|
||||
frame, turn, self._condition_normalizer
|
||||
)
|
||||
frame = _apply_broad_condition_cue(
|
||||
frame, turn, self._condition_normalizer
|
||||
)
|
||||
frame = _apply_reverse_relation_cues(frame, turn)
|
||||
frame = _apply_named_drug_cues(frame, turn)
|
||||
return _merge_with_prior_frame(frame, prior_frame)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_id(value: str, shown: dict[str, str]) -> str | None:
|
||||
@@ -453,7 +555,9 @@ class LlmQueryUnderstander:
|
||||
return drug_id
|
||||
return None
|
||||
|
||||
def _parse(self, raw_text: str, shown: dict[str, str]) -> QueryFrame:
|
||||
def _parse(
|
||||
self, raw_text: str, shown: dict[str, str], original_turn: str = ""
|
||||
) -> QueryFrame:
|
||||
try:
|
||||
data = json.loads(raw_text)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
@@ -490,6 +594,22 @@ class LlmQueryUnderstander:
|
||||
if needs_clarify and clarify_reason
|
||||
else ()
|
||||
)
|
||||
indication = _clean_str(data.get("indication"))
|
||||
condition = _parse_condition(
|
||||
data.get("condition"), indication, original_turn, self._condition_normalizer
|
||||
)
|
||||
relation = _clean_enum_value(
|
||||
data.get("condition_relation"), ConditionRelation, ConditionRelation.INDICATION
|
||||
)
|
||||
patient_context = _parse_patient_context(
|
||||
data.get("patient_context"),
|
||||
fallback_age=_clean_str(data.get("age_text")),
|
||||
fallback_weight=_clean_float(data.get("weight_kg")),
|
||||
fallback_population=_clean_enum(data.get("population"), _ALLOWED_POPULATIONS),
|
||||
)
|
||||
context_action = _clean_enum_value(
|
||||
data.get("context_action"), CaseContextAction, CaseContextAction.NONE
|
||||
)
|
||||
return QueryFrame(
|
||||
turn_type=turn_type,
|
||||
drugs=drugs,
|
||||
@@ -498,7 +618,11 @@ class LlmQueryUnderstander:
|
||||
population=_clean_enum(data.get("population"), _ALLOWED_POPULATIONS),
|
||||
weight_kg=_clean_float(data.get("weight_kg")),
|
||||
age_text=_clean_str(data.get("age_text")),
|
||||
indication=_clean_str(data.get("indication")),
|
||||
indication=(condition.normalized_condition if condition else indication),
|
||||
condition=condition,
|
||||
condition_relation=relation,
|
||||
patient_context=patient_context,
|
||||
context_action=context_action,
|
||||
route=_clean_enum(data.get("route"), _ALLOWED_ROUTES),
|
||||
section_overview=data.get("section_overview") is True,
|
||||
standalone_query=_clean_str(data.get("standalone_query")),
|
||||
@@ -510,6 +634,142 @@ class LlmQueryUnderstander:
|
||||
)
|
||||
|
||||
|
||||
def _apply_reverse_relation_cues(frame: QueryFrame, turn: str) -> QueryFrame:
|
||||
"""Fail closed on explicit reverse-relation wording.
|
||||
|
||||
The LLM remains responsible for open-ended language understanding. This
|
||||
narrow post-condition only covers unambiguous surface forms where routing
|
||||
to indication retrieval would reverse the requested relation. It contains
|
||||
no disease or drug knowledge and never creates a candidate.
|
||||
"""
|
||||
text = f" {normalize_name(turn)} "
|
||||
contraindication = any(
|
||||
cue in text
|
||||
for cue in (
|
||||
" thuoc nao chong chi dinh ",
|
||||
" nhung thuoc nao chong chi dinh ",
|
||||
" thuoc nao can tranh o ",
|
||||
" thuoc nao can tranh cho ",
|
||||
)
|
||||
)
|
||||
adverse = any(
|
||||
cue in text
|
||||
for cue in (
|
||||
" thuoc nao gay ",
|
||||
" thuoc nao co the gay ",
|
||||
" thuoc nao lam tang ",
|
||||
" thuoc nao co adr ",
|
||||
)
|
||||
)
|
||||
relation = (
|
||||
ConditionRelation.CONTRAINDICATION
|
||||
if contraindication
|
||||
else ConditionRelation.ADVERSE_EFFECT
|
||||
if adverse
|
||||
else None
|
||||
)
|
||||
if relation is None:
|
||||
return frame
|
||||
return replace(
|
||||
frame,
|
||||
turn_type="condition_relation",
|
||||
condition_relation=relation,
|
||||
needs_clarify=False,
|
||||
clarify_reason=None,
|
||||
quick_replies=(),
|
||||
)
|
||||
|
||||
|
||||
def _apply_condition_candidate_cue(
|
||||
frame: QueryFrame, turn: str, normalizer: ConditionNormalizer
|
||||
) -> QueryFrame:
|
||||
"""Keep current medicines subordinate in an explicit condition lookup."""
|
||||
if frame.turn_type == "condition_to_drug" and frame.condition is not None:
|
||||
return frame
|
||||
condition = normalizer.detect_known_alias(turn)
|
||||
if condition is None:
|
||||
return frame
|
||||
text = f" {normalize_name(turn)} "
|
||||
candidate_cues = (
|
||||
" dung thuoc gi ",
|
||||
" dung thuoc nao ",
|
||||
" thuoc nao can ",
|
||||
" lua chon thuoc nao ",
|
||||
" option ha ap ",
|
||||
" option dieu tri ",
|
||||
" ung vien nao ",
|
||||
" cac ung vien nao ",
|
||||
)
|
||||
if not any(cue in text for cue in candidate_cues):
|
||||
return frame
|
||||
return replace(
|
||||
frame,
|
||||
turn_type="condition_to_drug",
|
||||
indication=condition.normalized_condition,
|
||||
condition=condition,
|
||||
condition_relation=ConditionRelation.INDICATION,
|
||||
needs_clarify=False,
|
||||
clarify_reason=None,
|
||||
quick_replies=(),
|
||||
)
|
||||
|
||||
|
||||
def _apply_broad_condition_cue(
|
||||
frame: QueryFrame, turn: str, normalizer: ConditionNormalizer
|
||||
) -> QueryFrame:
|
||||
"""Make explicit broad disease→drug questions deterministically clarify."""
|
||||
if frame.drugs:
|
||||
return frame
|
||||
condition = normalizer.detect_broad_question(turn)
|
||||
if condition is None:
|
||||
return frame
|
||||
return replace(
|
||||
frame,
|
||||
turn_type="condition_to_drug",
|
||||
indication=condition.normalized_condition,
|
||||
condition=condition,
|
||||
condition_relation=ConditionRelation.INDICATION,
|
||||
needs_clarify=False,
|
||||
clarify_reason=None,
|
||||
quick_replies=(),
|
||||
)
|
||||
|
||||
|
||||
def _apply_named_drug_cues(frame: QueryFrame, turn: str) -> QueryFrame:
|
||||
"""A drug explicitly named as subject outranks reverse-condition wording."""
|
||||
if not frame.drugs:
|
||||
return frame
|
||||
text = f" {normalize_name(turn)} "
|
||||
purpose = (
|
||||
" co tac dung gi " in text
|
||||
and " tac dung khong mong muon " not in text
|
||||
) or " dung de lam gi " in text
|
||||
contraindication = (
|
||||
" co chong chi dinh " in text
|
||||
or " co dung duoc khong " in text
|
||||
)
|
||||
if purpose:
|
||||
return replace(
|
||||
frame,
|
||||
turn_type="drug_to_condition",
|
||||
attribute="chi_dinh",
|
||||
condition_relation=ConditionRelation.INDICATION,
|
||||
needs_clarify=False,
|
||||
clarify_reason=None,
|
||||
quick_replies=(),
|
||||
)
|
||||
if contraindication:
|
||||
return replace(
|
||||
frame,
|
||||
turn_type="drug_attribute",
|
||||
attribute="chong_chi_dinh",
|
||||
needs_clarify=False,
|
||||
clarify_reason=None,
|
||||
quick_replies=(),
|
||||
)
|
||||
return frame
|
||||
|
||||
|
||||
_KNOWN_FACT_LABELS: tuple[tuple[str, str], ...] = (
|
||||
("population", "Đối tượng"),
|
||||
("age_text", "Tuổi"),
|
||||
@@ -534,7 +794,7 @@ def _known_facts_block(prior_frame: QueryFrame | None) -> str:
|
||||
with_prior_frame` below is the code-level backstop for whatever the
|
||||
model still drops.
|
||||
"""
|
||||
if prior_frame is None or not prior_frame.needs_clarify:
|
||||
if prior_frame is None:
|
||||
return ""
|
||||
parts = []
|
||||
if prior_frame.drugs:
|
||||
@@ -545,12 +805,35 @@ def _known_facts_block(prior_frame: QueryFrame | None) -> str:
|
||||
value = getattr(prior_frame, field_name)
|
||||
if value:
|
||||
parts.append(f"{label}: {value}")
|
||||
patient = prior_frame.patient_context
|
||||
if patient.age_text:
|
||||
parts.append(f"Tuổi bệnh nhân: {patient.age_text}")
|
||||
if patient.sex:
|
||||
parts.append(f"Giới: {patient.sex}")
|
||||
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"Thận: {patient.renal}")
|
||||
if patient.hepatic.present:
|
||||
parts.append(f"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ú: {patient.breastfeeding}")
|
||||
if patient.relevant_labs:
|
||||
parts.append(f"Xét nghiệm: {', '.join(patient.relevant_labs)}")
|
||||
if not parts:
|
||||
return ""
|
||||
return (
|
||||
"THÔNG TIN ĐÃ XÁC ĐỊNH TỪ CÁC LƯỢT TRƯỚC (dữ liệu CÓ THẬT, đã xác nhận "
|
||||
"— KHÔNG hỏi lại các mục này; nếu câu hỏi hiện tại là một chủ đề mới "
|
||||
"không liên quan, hãy bỏ qua khối này thay vì gán nhầm vào lượt mới):\n"
|
||||
"THÔNG TIN ĐÃ XÁC ĐỊNH TỪ CÁC LƯỢT TRƯỚC (chỉ kế thừa nếu đây là CÙNG "
|
||||
"ca bệnh và đặt context_action=continue; nếu ca mới/chủ đề mới phải đặt "
|
||||
"context_action=new và bỏ qua toàn bộ khối; không hỏi lại dữ kiện đã có):\n"
|
||||
+ "\n".join(parts) + "\n\n"
|
||||
)
|
||||
|
||||
@@ -566,10 +849,27 @@ def _merge_with_prior_frame(frame: QueryFrame, prior_frame: QueryFrame | None) -
|
||||
the old one (the headache/OMEPRAZOL bleed this guards against runs the
|
||||
other way: don't let old fields survive into an unrelated new drug either).
|
||||
"""
|
||||
if prior_frame is None or not prior_frame.needs_clarify:
|
||||
if prior_frame is None:
|
||||
return frame
|
||||
if frame.drugs and frame.drugs != prior_frame.drugs:
|
||||
continuing_case = (
|
||||
frame.context_action == CaseContextAction.CONTINUE
|
||||
or frame.depends_on_previous_turn
|
||||
)
|
||||
legacy_clarify = prior_frame.needs_clarify
|
||||
if not continuing_case and not legacy_clarify:
|
||||
return frame
|
||||
if frame.context_action == CaseContextAction.NEW:
|
||||
return frame
|
||||
if frame.drugs and frame.drugs != prior_frame.drugs and not continuing_case:
|
||||
return frame
|
||||
patient_context = frame.patient_context
|
||||
if continuing_case:
|
||||
patient_context = patient_context.merged_with(prior_frame.patient_context)
|
||||
condition = frame.condition
|
||||
indication = frame.indication
|
||||
if continuing_case and condition is None:
|
||||
condition = prior_frame.condition
|
||||
indication = indication or prior_frame.indication
|
||||
return replace(
|
||||
frame,
|
||||
drugs=frame.drugs or prior_frame.drugs,
|
||||
@@ -577,11 +877,107 @@ def _merge_with_prior_frame(frame: QueryFrame, prior_frame: QueryFrame | None) -
|
||||
age_text=frame.age_text or prior_frame.age_text,
|
||||
weight_kg=frame.weight_kg if frame.weight_kg is not None else prior_frame.weight_kg,
|
||||
route=frame.route or prior_frame.route,
|
||||
indication=frame.indication or prior_frame.indication,
|
||||
indication=indication or prior_frame.indication,
|
||||
condition=condition,
|
||||
patient_context=patient_context,
|
||||
attribute=frame.attribute or prior_frame.attribute,
|
||||
)
|
||||
|
||||
|
||||
def _parse_condition(
|
||||
value,
|
||||
indication: str | None,
|
||||
original_turn: str,
|
||||
normalizer: ConditionNormalizer,
|
||||
) -> ConditionQuery | None:
|
||||
data = value if isinstance(value, dict) else {}
|
||||
raw = (
|
||||
_clean_str(data.get("normalized_condition"))
|
||||
or _clean_str(data.get("original_text"))
|
||||
or indication
|
||||
)
|
||||
if raw is None:
|
||||
return None
|
||||
return normalizer.normalize(
|
||||
original_query=original_turn,
|
||||
condition=raw,
|
||||
subtype=_clean_str(data.get("subtype")),
|
||||
qualifiers=tuple(_as_list(data.get("qualifiers"))),
|
||||
ambiguous=data.get("ambiguous") is True,
|
||||
clarify_question=_clean_str(data.get("clarify_question")),
|
||||
)
|
||||
|
||||
|
||||
def _parse_patient_context(
|
||||
value,
|
||||
*,
|
||||
fallback_age: str | None,
|
||||
fallback_weight: float | None,
|
||||
fallback_population: str | None,
|
||||
) -> PatientContext:
|
||||
data = value if isinstance(value, dict) else {}
|
||||
renal_data = data.get("renal") if isinstance(data.get("renal"), dict) else {}
|
||||
hepatic_data = (
|
||||
data.get("hepatic") if isinstance(data.get("hepatic"), dict) else {}
|
||||
)
|
||||
pregnancy = _clean_str(data.get("pregnancy_status"))
|
||||
breastfeeding = _clean_bool(data.get("breastfeeding"))
|
||||
renal_description = _clean_str(renal_data.get("description"))
|
||||
hepatic_description = _clean_str(hepatic_data.get("description"))
|
||||
if fallback_population == "phu_nu_co_thai" and pregnancy is None:
|
||||
pregnancy = "mang thai"
|
||||
if fallback_population == "phu_nu_cho_con_bu" and breastfeeding is None:
|
||||
breastfeeding = True
|
||||
if fallback_population == "suy_than" and renal_description is None:
|
||||
renal_description = "suy thận"
|
||||
if fallback_population == "suy_gan" and hepatic_description is None:
|
||||
hepatic_description = "suy gan"
|
||||
return PatientContext(
|
||||
age_text=_clean_str(data.get("age_text")) or fallback_age,
|
||||
sex=_clean_str(data.get("sex")),
|
||||
weight_kg=_clean_float(data.get("weight_kg")) or fallback_weight,
|
||||
primary_condition=_clean_str(data.get("primary_condition")),
|
||||
comorbidities=tuple(_as_list(data.get("comorbidities"))),
|
||||
allergies=tuple(_as_list(data.get("allergies"))),
|
||||
previous_adverse_reactions=tuple(
|
||||
_as_list(data.get("previous_adverse_reactions"))
|
||||
),
|
||||
current_medications=tuple(_as_list(data.get("current_medications"))),
|
||||
pregnancy_status=pregnancy,
|
||||
breastfeeding=breastfeeding,
|
||||
renal=RenalContext(
|
||||
description=renal_description,
|
||||
ckd_stage=_clean_str(renal_data.get("ckd_stage")),
|
||||
egfr=_clean_str(renal_data.get("egfr")),
|
||||
crcl=_clean_str(renal_data.get("crcl")),
|
||||
creatinine=_clean_str(renal_data.get("creatinine")),
|
||||
),
|
||||
hepatic=HepaticContext(
|
||||
description=hepatic_description,
|
||||
child_pugh=_clean_str(hepatic_data.get("child_pugh")),
|
||||
ast=_clean_str(hepatic_data.get("ast")),
|
||||
alt=_clean_str(hepatic_data.get("alt")),
|
||||
bilirubin=_clean_str(hepatic_data.get("bilirubin")),
|
||||
),
|
||||
relevant_labs=tuple(_as_list(data.get("relevant_labs"))),
|
||||
treatment_history=tuple(_as_list(data.get("treatment_history"))),
|
||||
)
|
||||
|
||||
|
||||
def _clean_enum_value(value, enum_type, default):
|
||||
cleaned = _clean_str(value)
|
||||
if cleaned is None:
|
||||
return default
|
||||
try:
|
||||
return enum_type(cleaned)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
|
||||
def _clean_bool(value) -> bool | None:
|
||||
return value if isinstance(value, bool) else None
|
||||
|
||||
|
||||
def _as_list(value) -> list[str]:
|
||||
if isinstance(value, str):
|
||||
return [value] if value.strip() else []
|
||||
|
||||
Reference in New Issue
Block a user