Add read-only production runtime audit

This commit is contained in:
2026-08-17 11:17:40 +07:00
parent 057d4ed9dc
commit a1de4715a4
106 changed files with 6869 additions and 1782 deletions
+91 -3
View File
@@ -593,6 +593,7 @@ class LlmQueryUnderstander:
frame = _apply_broad_condition_cue(
frame, turn, self._condition_normalizer
)
frame = _apply_general_condition_scope(frame, turn)
frame = _apply_reverse_relation_cues(frame, turn)
section_match = _SECTION_RESOLVER.resolve(turn)
frame = _apply_named_drug_cues(
@@ -603,7 +604,8 @@ class LlmQueryUnderstander:
resolved_section_phrase=(section_match.phrase if section_match else None),
)
frame = _apply_multi_section_clarify(frame, turn)
return _merge_with_prior_frame(frame, prior_frame)
frame = _merge_with_prior_frame(frame, prior_frame)
return _apply_contextual_candidate_safety(frame, turn, prior_frame)
@staticmethod
def _resolve_id(value: str, shown: dict[str, str]) -> str | None:
@@ -754,7 +756,7 @@ def _apply_condition_candidate_cue(
"""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)
condition = frame.condition or normalizer.detect_known_alias(turn)
if condition is None:
return frame
text = f" {normalize_name(turn)} "
@@ -767,6 +769,8 @@ def _apply_condition_candidate_cue(
" option dieu tri ",
" ung vien nao ",
" cac ung vien nao ",
" co chi dinh lien quan ",
" co chi dinh cho ",
)
if not any(cue in text for cue in candidate_cues):
return frame
@@ -782,6 +786,80 @@ def _apply_condition_candidate_cue(
)
def _apply_general_condition_scope(frame: QueryFrame, turn: str) -> QueryFrame:
"""Do not turn a disease name into an unstated patient impairment.
A general reverse lookup such as ``Viêm gan B mạn dùng thuốc gì?`` names
the condition being treated; it does not say that a particular patient has
hepatic impairment. The understanding model can otherwise duplicate the
same phrase into ``patient_context.hepatic`` and trigger a stage-2 safety
review, mixing contraindication/precaution citations into a general
indication list. Explicit patient cues keep the full context untouched.
"""
if frame.turn_type not in {"condition_to_drug", "symptom_to_drug"}:
return frame
text = f" {normalize_name(turn)} "
patient_cues = (
" bn ", " benh nhan ", " nguoi benh ", " kem ", " di ung ",
" dang dung ", " mang thai ", " cho con bu ", " tuoi ", " kg ",
" ckd ", " suy than ", " suy gan ", " child pugh ", " egfr ",
" creatinin ", " ast ", " alt ",
)
if any(cue in text for cue in patient_cues):
return frame
primary = (
frame.condition.normalized_condition
if frame.condition is not None
else frame.indication
)
return replace(frame, patient_context=PatientContext(primary_condition=primary))
def _apply_contextual_candidate_safety(
frame: QueryFrame,
turn: str,
prior_frame: QueryFrame | None,
) -> QueryFrame:
"""Keep ``các thuốc trên`` on the prior condition-to-drug candidate lane.
This follow-up asks to compare the already retrieved candidates against a
new patient constraint. It is not a reverse disease->contraindication
lookup, even if the current turn contains words such as ``bệnh thận``.
"""
if prior_frame is None or prior_frame.turn_type not in {
"condition_to_drug", "symptom_to_drug"
}:
return frame
text = f" {normalize_name(turn)} "
refers_to_candidates = any(
cue in text for cue in (" cac thuoc tren ", " trong cac thuoc tren ")
)
safety_cue = any(
cue in text
for cue in (
" luu y ", " than trong ", " benh than ", " suy than ",
" benh gan ", " suy gan ", " di ung ", " tuong tac ",
)
)
if not (refers_to_candidates and safety_cue):
return frame
condition = frame.condition or prior_frame.condition
return replace(
frame,
turn_type="condition_to_drug",
indication=(
condition.normalized_condition
if condition is not None
else frame.indication or prior_frame.indication
),
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:
@@ -1037,7 +1115,17 @@ def _merge_with_prior_frame(frame: QueryFrame, prior_frame: QueryFrame | None) -
indication=indication or prior_frame.indication,
condition=condition,
patient_context=patient_context,
attribute=frame.attribute or prior_frame.attribute,
# A current drug-attribute clarify with no attribute is an explicit
# ambiguity signal (for example, the user named both "chỉ định" and
# "chống chỉ định"). Re-inheriting the previous turn's attribute here
# silently picks one of those sections and poisons the frame remembered
# for the next quick reply. Other continuation shapes still inherit the
# prior slot as before (notably pediatric dosing clarifications).
attribute=(
frame.attribute
if frame.turn_type == "drug_attribute" and frame.needs_clarify
else frame.attribute or prior_frame.attribute
),
)