Add read-only production runtime audit
This commit is contained in:
@@ -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ể thuộc 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ủ thuốc 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",
|
||||
|
||||
Reference in New Issue
Block a user