Remove corpus counts from chat chrome
This commit is contained in:
@@ -133,6 +133,17 @@ class QueryFrame:
|
||||
age_text: str | None = None
|
||||
indication: str | None = None # symptom/disease, for symptom_to_drug
|
||||
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
|
||||
# asking for one regimen to apply to a particular patient. This is a
|
||||
# materially different sufficiency contract: a section survey must label
|
||||
# every branch, while a patient-specific dose may need clarification.
|
||||
section_overview: bool = False
|
||||
# Self-contained meaning used downstream. It rewrites references/omitted
|
||||
# subjects only; retrieval still trusts the separately validated drug ids,
|
||||
# facet and constraints below rather than parsing authority back from text.
|
||||
standalone_query: str | None = None
|
||||
depends_on_previous_turn: bool = False
|
||||
needs_clarify: bool = False
|
||||
clarify_reason: str | None = None
|
||||
# Short suggested replies for `clarify_reason` (e.g. ("Người lớn", "Trẻ
|
||||
@@ -174,6 +185,19 @@ FRAME_SCHEMA = {
|
||||
"'uống' or 'tiêm' to your own prior clarify question about route IS "
|
||||
"this field — read it here, do not leave it null and re-ask."
|
||||
),
|
||||
"section_overview": (
|
||||
"true when the user asks for a general survey of the requested section "
|
||||
"(all ADRs/interactions/precautions/dose regimens, 'theo Dược thư', "
|
||||
"'nếu có'), not one dose/decision for a specific patient"
|
||||
),
|
||||
"standalone_query": (
|
||||
"self-contained Vietnamese question after resolving pronouns/omitted subject "
|
||||
"from history; do not answer it. For a self-contained current turn, copy its "
|
||||
"meaning without adding facts"
|
||||
),
|
||||
"depends_on_previous_turn": (
|
||||
"true only when standalone_query needed entity/facet/constraints from history"
|
||||
),
|
||||
"needs_clarify": "true only if the turn cannot be acted on without more info",
|
||||
"clarify_reason": "short Vietnamese question to ask, or null",
|
||||
"quick_replies": (
|
||||
@@ -187,6 +211,17 @@ FRAME_SCHEMA = {
|
||||
),
|
||||
}
|
||||
|
||||
_ALLOWED_POPULATIONS = {
|
||||
"tre_em", "tre_so_sinh", "nguoi_lon", "nguoi_cao_tuoi",
|
||||
"phu_nu_co_thai", "phu_nu_cho_con_bu", "suy_than", "suy_gan",
|
||||
}
|
||||
_ALLOWED_ROUTES = {
|
||||
"uong", "tiem_tinh_mach", "tiem_bap", "tiem_duoi_da",
|
||||
"dat_truc_trang", "boi_ngoai_da", "nho_mat", "nho_mui", "khac",
|
||||
}
|
||||
_QUICK_REPLY_MAX_ITEMS = 4
|
||||
_QUICK_REPLY_MAX_CHARS = 40
|
||||
|
||||
_SYSTEM = """Bạn là bộ HIỂU CÂU HỎI cho một chatbot tra cứu Dược thư Quốc gia Việt Nam.
|
||||
Nhiệm vụ: đọc câu hỏi (tiếng Việt, có thể sai chính tả, viết tắt, nhiều lượt) và
|
||||
XUẤT RA một JSON mô tả ý định. TUYỆT ĐỐI KHÔNG trả lời câu hỏi y khoa, không nêu liều.
|
||||
@@ -203,8 +238,24 @@ Quy tắc bắt buộc:
|
||||
- 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.
|
||||
- Với "dosing_calc", bộ hiểu câu hỏi chỉ bắt buộc làm rõ population và tuổi/cân
|
||||
nặng nếu là trẻ em/trẻ sơ sinh. KHÔNG tự kết luận rằng route luôn bắt buộc: nếu
|
||||
người dùng chưa nêu đường dùng thì giữ route=null; tầng trả lời sẽ nhìn chính
|
||||
BẰNG CHỨNG truy xuất được để quyết định câu hỏi đã đủ rõ hay chưa. Nhờ vậy nếu
|
||||
nguồn chỉ có một đường dùng phù hợp thì trả lời thẳng, còn nếu có nhiều nhánh
|
||||
khác nhau mới hỏi lại và sinh quick_replies.
|
||||
- Phân biệt tra cứu TOÀN MỤC với áp dụng cho MỘT NGƯỜI BỆNH. Nếu người dùng hỏi
|
||||
tổng quan kiểu "Dược thư hướng dẫn dùng thế nào", "các ADR/tương tác/thận trọng
|
||||
nào", "theo nhóm/tần suất", "nếu có" thì đặt section_overview=true và KHÔNG
|
||||
hỏi người lớn/trẻ em/đường dùng chỉ để thu hẹp. Hãy trả về các nhánh trong sách
|
||||
với nhãn rõ ràng. Chỉ đặt section_overview=false và hỏi thêm khi người dùng muốn
|
||||
chọn một liều/quyết định áp dụng cho ca bệnh cụ thể.
|
||||
- Lượt nối tiếp ("còn liều thì sao", "nó dùng cho trẻ em?") -> dùng LỊCH SỬ để biết
|
||||
thuốc đang nói tới và điền vào "drugs".
|
||||
thuốc đang nói tới và điền vào "drugs". Đồng thời viết `standalone_query` thành
|
||||
câu có nghĩa độc lập (ví dụ "Còn chống chỉ định của levetiracetam?" ->
|
||||
"Chống chỉ định của levetiracetam") và đặt `depends_on_previous_turn=true`.
|
||||
- Nếu lượt hiện tại đã tự đủ nghĩa, `standalone_query` chỉ chuẩn hóa chính câu đó,
|
||||
không chèn thuốc/đối tượng cũ và đặt `depends_on_previous_turn=false`.
|
||||
- QUAN TRỌNG — lượt hiện tại trả lời câu hỏi bạn VỪA hỏi: nếu dòng "Trợ lý:" cuối
|
||||
cùng trong LỊCH SỬ là một câu hỏi làm rõ (vd "Người lớn hay trẻ em?", "Uống hay
|
||||
tiêm?", "Cân nặng bao nhiêu kg?"), và CÂU HỎI HIỆN TẠI là một câu trả lời ngắn
|
||||
@@ -308,8 +359,16 @@ class LlmQueryUnderstander:
|
||||
ids.add(resolution.drug_id)
|
||||
elif resolution.status == "ambiguous":
|
||||
ids.update(resolution.candidate_drug_ids)
|
||||
for drug_id, _score in self._resolver.suggest(line, k=5, min_score=0.55):
|
||||
ids.add(drug_id)
|
||||
else:
|
||||
# Exact/ambiguous resolution has already exhaustively scanned
|
||||
# the aliases present in this line. Running the O(catalog)
|
||||
# fuzzy pass as well only adds unrelated candidates and cost
|
||||
# (~1s on the real catalog); fuzzy suggestions are useful only
|
||||
# when no exact candidate was found (the typo path).
|
||||
for drug_id, _score in self._resolver.suggest(
|
||||
line, k=5, min_score=0.55
|
||||
):
|
||||
ids.add(drug_id)
|
||||
return ids
|
||||
|
||||
def understand(
|
||||
@@ -424,19 +483,29 @@ class LlmQueryUnderstander:
|
||||
turn_type = data.get("turn_type")
|
||||
if turn_type not in TURN_TYPES:
|
||||
turn_type = "drug_attribute" if drugs else "out_of_scope"
|
||||
needs_clarify = data.get("needs_clarify") is True
|
||||
clarify_reason = _clean_str(data.get("clarify_reason"))
|
||||
quick_replies = (
|
||||
_clean_quick_replies(data.get("quick_replies"))
|
||||
if needs_clarify and clarify_reason
|
||||
else ()
|
||||
)
|
||||
return QueryFrame(
|
||||
turn_type=turn_type,
|
||||
drugs=drugs,
|
||||
unknown_drugs=tuple(dict.fromkeys(unknown)),
|
||||
attribute=attribute,
|
||||
population=_clean_str(data.get("population")),
|
||||
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")),
|
||||
route=_clean_str(data.get("route")),
|
||||
needs_clarify=bool(data.get("needs_clarify")),
|
||||
clarify_reason=_clean_str(data.get("clarify_reason")),
|
||||
quick_replies=tuple(_as_list(data.get("quick_replies"))),
|
||||
route=_clean_enum(data.get("route"), _ALLOWED_ROUTES),
|
||||
section_overview=data.get("section_overview") is True,
|
||||
standalone_query=_clean_str(data.get("standalone_query")),
|
||||
depends_on_previous_turn=data.get("depends_on_previous_turn") is True,
|
||||
needs_clarify=needs_clarify,
|
||||
clarify_reason=clarify_reason,
|
||||
quick_replies=quick_replies,
|
||||
raw=data if isinstance(data, dict) else {},
|
||||
)
|
||||
|
||||
@@ -527,12 +596,39 @@ def _clean_str(value) -> str | None:
|
||||
return None
|
||||
|
||||
|
||||
def _clean_enum(value, allowed: set[str]) -> str | None:
|
||||
cleaned = _clean_str(value)
|
||||
return cleaned if cleaned in allowed else None
|
||||
|
||||
|
||||
def _clean_quick_replies(value) -> tuple[str, ...]:
|
||||
"""Keep the LLM's dynamic suggestions, but enforce the UI contract."""
|
||||
if not isinstance(value, list):
|
||||
return ()
|
||||
replies: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for item in value:
|
||||
if not isinstance(item, str):
|
||||
continue
|
||||
reply = " ".join(item.split())
|
||||
key = reply.casefold()
|
||||
if not reply or len(reply) > _QUICK_REPLY_MAX_CHARS or key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
replies.append(reply)
|
||||
if len(replies) >= _QUICK_REPLY_MAX_ITEMS:
|
||||
break
|
||||
return tuple(replies)
|
||||
|
||||
|
||||
def _clean_float(value) -> float | None:
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
parsed = float(value)
|
||||
return parsed if 0 < parsed <= 500 else None
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return float(value.replace(",", ".").split()[0])
|
||||
parsed = float(value.replace(",", ".").split()[0])
|
||||
return parsed if 0 < parsed <= 500 else None
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user