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
+60 -7
View File
@@ -28,10 +28,12 @@ from .clinical import ConditionRelation, MedicationCandidateAssessment
from .models import EvidenceDecision, RetrievalResult
from .policy import looks_non_human
from .service import RetrievalService
from .sections import SectionResolver
from .text import normalize_name
from .understanding import QueryFrame, QueryUnderstander
logger = logging.getLogger(__name__)
_SECTION_RESOLVER = SectionResolver()
TUONG_TAC = "tuong_tac_thuoc"
HISTORY_TURNS = 6
@@ -142,7 +144,12 @@ class RagAgent:
return []
return [_display_name(drug_id) for drug_id in self._autocomplete.complete(prefix, k)]
def handle(self, turn: str, conversation_id: str | None = None) -> AgentReply:
def handle(
self,
turn: str,
conversation_id: str | None = None,
response_mode: str = "ai",
) -> AgentReply:
# F-08: one budget per turn, threaded through every LLM call this
# turn makes (understand, then whatever `_route` reaches).
t0 = time.monotonic()
@@ -154,7 +161,7 @@ class RagAgent:
turn, tuple(history), budget=budget, prior_frame=prior_frame
)
t2 = time.monotonic()
reply = self._route(turn, frame, budget)
reply = self._route(turn, frame, budget, response_mode=response_mode)
reply = self._enforce_clarify_circuit_breaker(conversation_id, reply)
t3 = time.monotonic()
if conversation_id is not None:
@@ -242,7 +249,13 @@ class RagAgent:
return []
return self._history.get(conversation_id, [])
def _route(self, turn: str, frame: QueryFrame, budget: RequestBudget) -> AgentReply:
def _route(
self,
turn: str,
frame: QueryFrame,
budget: RequestBudget,
response_mode: str = "ai",
) -> AgentReply:
tt = frame.turn_type
section_overview = _is_section_overview(turn, frame)
if section_overview and not frame.section_overview:
@@ -253,10 +266,10 @@ class RagAgent:
# an out-of-scope request look recoverable.
if looks_non_human(turn):
return AgentReply(
"abstain", "out_of_scope",
answer="Nội dung này nằm ngoài phần chuyên luận thuốc của Dược thư "
"(có thể thuc phần hướng dẫn chung/phụ lục chưa được đưa vào). "
"Tôi chưa có dữ liệu để trả lời chính xác.",
"abstain", "out_of_scope_non_human",
answer="Dược thư Quốc gia Việt Nam trong hệ thống này chỉ bao "
"phủ thuc dùng cho người. Hệ thống không tra cứu liều "
"dùng hoặc hướng dẫn điều trị cho động vật.",
turn_type=tt)
# Dosing is a small state machine, not an unconstrained model opinion.
@@ -407,6 +420,24 @@ class RagAgent:
turn_type=tt,
)
if response_mode == "monograph" and (
tt == "drug_overview"
or (
tt == "drug_attribute"
and frame.attribute is None
and not frame.needs_clarify
)
or _is_bare_monograph_request(turn)
) and frame.drugs:
return AgentReply(
"clarify", "select_drug_sections",
clarification=(
"Đã nhận diện chuyên luận thuốc. Anh/chị chọn các mục cần "
"xem; nếu không chọn mục nào, hệ thống sẽ hiển thị toàn bộ."
),
drugs=frame.drugs, turn_type=tt,
)
if tt == "drug_attribute" and frame.drugs and frame.attribute is None:
return AgentReply(
"clarify", "missing_attribute",
@@ -713,6 +744,28 @@ def _is_section_overview(turn: str, frame: QueryFrame) -> bool:
return frame.section_overview or any(cue in text for cue in overview_cues)
def _is_bare_monograph_request(turn: str) -> bool:
"""True for a plain drug name in explicit monograph-browse mode.
A persisted conversation can contribute a stale attribute to a new bare
drug turn (for example the prior question was about contraindications).
The UI mode is an explicit current-turn instruction, so a plain name must
open the picker rather than inherit that old section. Any actual section
phrase or clinical-question cue keeps the normal AI route.
"""
text = normalize_name(turn)
if not text or len(text) > 100 or _SECTION_RESOLVER.resolve_all(turn):
return False
clinical_cues = (
" dung ", " dieu tri ", " tuong tac ", " tac dung ", " lieu ",
" benh ", " thai ", " cho con bu ", " tre em ", " nguoi lon ",
" suy than ", " suy gan ", " di ung ", " bao nhieu ", " la gi ",
" co the ", " duoc khong ",
)
padded = f" {text} "
return not any(cue in padded for cue in clinical_cues)
_POPULATION_LABELS = {
"tre_em": "trẻ em",
"tre_so_sinh": "trẻ sơ sinh",