Remove chat rate limit
This commit is contained in:
@@ -335,7 +335,20 @@ class RagAgent:
|
||||
),
|
||||
turn_type=tt,
|
||||
)
|
||||
if frame.condition_relation != ConditionRelation.INDICATION:
|
||||
# UNKNOWN is treated as INDICATION here, not as a third rejection
|
||||
# state: turn_type is already condition_to_drug/symptom_to_drug at
|
||||
# this point, which only exists because the turn was read as
|
||||
# asking which drug treats the condition -- that already settles
|
||||
# the direction. Only an explicit reverse-relation reading
|
||||
# (ADVERSE_EFFECT/CONTRAINDICATION) should abstain here; UNKNOWN
|
||||
# is the model hedging on an ordinary question, not a genuine
|
||||
# reverse-relation query (found live 2026-08-13: bare "X thì dùng
|
||||
# thuốc gì" turns were reliably classified with the right
|
||||
# turn_type but condition_relation="unknown", incorrectly
|
||||
# aborting a plain treatment-lookup question).
|
||||
if frame.condition_relation in (
|
||||
ConditionRelation.ADVERSE_EFFECT, ConditionRelation.CONTRAINDICATION,
|
||||
):
|
||||
return AgentReply(
|
||||
"abstain",
|
||||
"unsupported_reverse_relation",
|
||||
|
||||
@@ -30,7 +30,7 @@ from .routing import QueryRoutingService
|
||||
# be different prompts (or a different judge) to be independent evidence —
|
||||
# see this function's own reasoning above.
|
||||
|
||||
_QUICK_REPLY_MAX_ITEMS = 4
|
||||
_QUICK_REPLY_MAX_ITEMS = 18 # one per monograph section (see rag/sections.py SECTION_ORDER)
|
||||
_QUICK_REPLY_MAX_CHARS = 40
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -207,6 +207,29 @@ class _VerificationOutcome:
|
||||
missing: tuple[str, ...] = ()
|
||||
|
||||
|
||||
# Display-only label override, 2026-08-13: these 3 of 684 catalog drug_ids are
|
||||
# missing the letter for "Đ"/"đ" entirely (ingestion's slug generator drops it
|
||||
# instead of mapping it to "d" like every other Vietnamese diacritic), so
|
||||
# `drug_id.replace("_", " ").upper()` can never reconstruct the accented name
|
||||
# and the fold-based dedup below always mismatches for them. Does not touch
|
||||
# drug_id or any stored data — only the label text shown in generated answers.
|
||||
_DRUG_LABEL_OVERRIDES: dict[str, str] = {
|
||||
"giai_oc_to_uon_van_hap_phu_vac_xin_uon_van_hap_phu":
|
||||
"GIẢI ĐỘC TỐ UỐN VÁN HẤP PHỤ (VẮC XIN UỐN VÁN HẤP PHỤ)",
|
||||
"khang_oc_to_bach_hau": "KHÁNG ĐỘC TỐ BẠCH HẦU",
|
||||
"thuoc_uong_bu_nuoc_va_ien_giai": "THUỐC UỐNG BÙ NƯỚC VÀ ĐIỆN GIẢI",
|
||||
}
|
||||
|
||||
|
||||
def _fold_diacritics(text: str) -> str:
|
||||
"""Accent-insensitive fold. Vietnamese "Đ"/"đ" is not a combining-mark
|
||||
decomposition under NFKD (unlike every other Vietnamese diacritic), so it
|
||||
survives the strip below unless mapped explicitly first."""
|
||||
text = text.replace("Đ", "D").replace("đ", "d")
|
||||
stripped = unicodedata.normalize("NFKD", text)
|
||||
return "".join(ch for ch in stripped if not unicodedata.combining(ch)).casefold()
|
||||
|
||||
|
||||
def _parse_claims(
|
||||
raw_claims: list, *, include_drug_label: bool = False
|
||||
) -> tuple[tuple[str, tuple[int, ...]], ...] | None:
|
||||
@@ -230,8 +253,8 @@ def _parse_claims(
|
||||
drug_id = item.get("drug_id")
|
||||
if not isinstance(drug_id, str) or not drug_id.strip():
|
||||
return None
|
||||
label = drug_id.replace("_", " ").upper()
|
||||
if label.casefold() not in cleaned.casefold():
|
||||
label = _DRUG_LABEL_OVERRIDES.get(drug_id) or drug_id.replace("_", " ").upper()
|
||||
if _fold_diacritics(label) not in _fold_diacritics(cleaned):
|
||||
cleaned = f"{label}: {cleaned}"
|
||||
claims.append((cleaned, tuple(citations)))
|
||||
return tuple(claims)
|
||||
|
||||
@@ -50,9 +50,11 @@ from .clinical import (
|
||||
RenalContext,
|
||||
)
|
||||
from .ports import AnswerGenerationUnavailable
|
||||
from .sections import SectionResolver
|
||||
from .text import normalize_name
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_SECTION_RESOLVER = SectionResolver()
|
||||
|
||||
# The 19 monograph section keys, kept here as the closed vocabulary the model may
|
||||
# use for `attribute`. Adding a new section is one entry, not a code change.
|
||||
@@ -208,8 +210,15 @@ FRAME_SCHEMA = {
|
||||
},
|
||||
"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"
|
||||
"DEFAULT is 'indication' — use it for the ordinary, most common case: "
|
||||
"any question asking which drug to use/take/treat a condition or "
|
||||
"symptom with (e.g. 'X thì dùng thuốc gì', 'bị X uống thuốc gì', "
|
||||
"'thuốc trị X', 'thuốc chữa X'). Only deviate from 'indication' when "
|
||||
"the question ITSELF contains an explicit reverse-direction phrase: "
|
||||
"'thuốc nào gây X' -> adverse_effect; 'thuốc nào chống chỉ định ở X' -> "
|
||||
"contraindication. Do not pick 'unknown' for an ordinary treatment "
|
||||
"question just to hedge — 'unknown' is only for text that is not "
|
||||
"readable as any of the other three at all."
|
||||
),
|
||||
"patient_context": {
|
||||
"age_text": "age exactly as stated, else null",
|
||||
@@ -285,7 +294,7 @@ _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_ITEMS = 18 # one per monograph section (see rag/sections.py SECTION_ORDER)
|
||||
_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.
|
||||
@@ -299,17 +308,24 @@ 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à 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.
|
||||
- MẶC ĐỊNH cho một BỆNH/TRIỆU CHỨNG đã nêu (không nhắc tên thuốc): câu hỏi
|
||||
đang hỏi THUỐC NÀO DÙNG ĐỂ ĐIỀU TRỊ nó -> "condition_to_drug" (bệnh đã chẩn
|
||||
đoán) hoặc "symptom_to_drug" (triệu chứng chưa phải chẩn đoán), điền
|
||||
`condition`, `condition_relation="indication"`. Đây là cách đọc MẶC ĐỊNH —
|
||||
mọi cách diễn đạt kiểu "bị/mắc [bệnh] thì/nên dùng/uống thuốc gì", "thuốc gì
|
||||
trị/chữa [bệnh]", "[bệnh] uống thuốc gì" đều thuộc nhánh này, kể cả khi
|
||||
không có từ "chỉ định". 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.
|
||||
- NGOẠI LỆ DUY NHẤT khỏi mặc định ở trên — turn_type="condition_relation" —
|
||||
là câu hỏi tra NGƯỢC từ một bệnh/biến cố sang danh sách thuốc: "thuốc nào
|
||||
GÂY tăng huyết áp", "thuốc nào CHỐNG CHỈ ĐỊNH ở bệnh nhân gout". Cụm
|
||||
"chống chỉ định" tự nó KHÔNG đủ để chọn nhánh này: khi đã nêu một thuốc làm
|
||||
đối tượng tra cứu, ví dụ "Chống chỉ định của Paracetamol là gì?" hoặc
|
||||
"Probenecid có chống chỉ định gì?", phải là turn_type="drug_attribute",
|
||||
attribute="chong_chi_dinh". Tương tự, "tác dụng không mong muốn của X" là
|
||||
thuộc tính của thuốc X, không phải tra ngược. Chỉ gán condition_relation khi
|
||||
chiều hỏi thực sự là bệnh/biến cố -> thuốc.
|
||||
- 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õ.
|
||||
@@ -374,7 +390,18 @@ Quy tắc bắt buộc:
|
||||
- Khi needs_clarify=true, kèm "quick_replies": 2-4 phương án NGẮN cho câu hỏi lại
|
||||
đó, CHỈ khi nó thực sự có vài lựa chọn rời rạc tự nhiên (vd đối tượng: "Người
|
||||
lớn"/"Trẻ em"). Để mảng rỗng nếu cần một giá trị cụ thể không có lựa chọn ngắn
|
||||
(vd hỏi cân nặng chính xác) — không bịa phương án dạng số."""
|
||||
(vd hỏi cân nặng chính xác) — không bịa phương án dạng số.
|
||||
- NGOẠI LỆ về số lượng: khi CÂU HỎI đã nêu đúng MỘT thuốc cụ thể (drugs có đúng
|
||||
một phần tử) nhưng KHÔNG nêu mục/thuộc tính nào (attribute=null, không hỏi
|
||||
liều/chỉ định/tương tác/... cụ thể) — tức người dùng chỉ gõ tên thuốc — thì
|
||||
"quick_replies" liệt kê TOÀN BỘ các mục chuyên luận của Dược thư, không giới
|
||||
hạn ở 2-4: "Tên chung quốc tế", "Mã ATC", "Loại thuốc", "Dạng thuốc và hàm
|
||||
lượng", "Dược lý và cơ chế tác dụng", "Chỉ định", "Chống chỉ định", "Thận
|
||||
trọng", "Thời kỳ mang thai", "Thời kỳ cho con bú", "Tác dụng không mong
|
||||
muốn", "Hướng dẫn xử trí ADR", "Liều lượng và cách dùng", "Tương tác thuốc",
|
||||
"Độ ổn định và bảo quản", "Tương kỵ", "Quá liều và xử trí", "Thông tin quy
|
||||
chế" — nguyên văn đúng 18 nhãn này, đúng thứ tự, không rút gọn, không tự
|
||||
đổi chữ."""
|
||||
|
||||
|
||||
class JsonLlm(Protocol):
|
||||
@@ -471,6 +498,14 @@ class LlmQueryUnderstander:
|
||||
budget: RequestBudget | None = None,
|
||||
prior_frame: QueryFrame | None = None,
|
||||
) -> QueryFrame:
|
||||
turn_resolution = self._resolver.resolve(turn) if turn.strip() else None
|
||||
resolved_turn_drug = (
|
||||
turn_resolution.drug_id
|
||||
if turn_resolution is not None
|
||||
and turn_resolution.status == "resolved"
|
||||
and turn_resolution.drug_id in self._catalog
|
||||
else None
|
||||
)
|
||||
shown = {
|
||||
drug_id: self._catalog[drug_id]
|
||||
for drug_id in self._candidate_ids(turn, history)
|
||||
@@ -534,7 +569,14 @@ class LlmQueryUnderstander:
|
||||
frame, turn, self._condition_normalizer
|
||||
)
|
||||
frame = _apply_reverse_relation_cues(frame, turn)
|
||||
frame = _apply_named_drug_cues(frame, turn)
|
||||
section_match = _SECTION_RESOLVER.resolve(turn)
|
||||
frame = _apply_named_drug_cues(
|
||||
frame,
|
||||
turn,
|
||||
resolved_turn_drug=resolved_turn_drug,
|
||||
resolved_section_key=(section_match.section_key if section_match else None),
|
||||
resolved_section_phrase=(section_match.phrase if section_match else None),
|
||||
)
|
||||
return _merge_with_prior_frame(frame, prior_frame)
|
||||
|
||||
@staticmethod
|
||||
@@ -735,23 +777,66 @@ def _apply_broad_condition_cue(
|
||||
)
|
||||
|
||||
|
||||
def _apply_named_drug_cues(frame: QueryFrame, turn: str) -> QueryFrame:
|
||||
def _apply_named_drug_cues(
|
||||
frame: QueryFrame,
|
||||
turn: str,
|
||||
resolved_turn_drug: str | None = None,
|
||||
resolved_section_key: str | None = None,
|
||||
resolved_section_phrase: str | None = None,
|
||||
) -> QueryFrame:
|
||||
"""A drug explicitly named as subject outranks reverse-condition wording."""
|
||||
if not frame.drugs:
|
||||
subject_drugs = frame.drugs or (
|
||||
(resolved_turn_drug,) if resolved_turn_drug else ()
|
||||
)
|
||||
if not subject_drugs:
|
||||
return frame
|
||||
text = f" {normalize_name(turn)} "
|
||||
explicit_reverse = 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 ",
|
||||
" thuoc nao gay ",
|
||||
" thuoc nao co the gay ",
|
||||
" thuoc nao lam tang ",
|
||||
" thuoc nao co adr ",
|
||||
)
|
||||
)
|
||||
owned_section = bool(
|
||||
resolved_turn_drug
|
||||
and resolved_section_key
|
||||
and resolved_section_phrase
|
||||
and f" {normalize_name(resolved_section_phrase)} cua " in text
|
||||
)
|
||||
if owned_section and not explicit_reverse:
|
||||
return replace(
|
||||
frame,
|
||||
turn_type="drug_attribute",
|
||||
drugs=(resolved_turn_drug,),
|
||||
attribute=resolved_section_key,
|
||||
needs_clarify=False,
|
||||
clarify_reason=None,
|
||||
quick_replies=(),
|
||||
)
|
||||
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
|
||||
contraindication = not explicit_reverse and (
|
||||
frame.attribute == "chong_chi_dinh"
|
||||
or " chong chi dinh cua " in text
|
||||
or " co chong chi dinh " in text
|
||||
or " chong chi dinh gi " in text
|
||||
or " chong chi dinh nao " in text
|
||||
or " co dung duoc khong " in text
|
||||
)
|
||||
if purpose:
|
||||
return replace(
|
||||
frame,
|
||||
turn_type="drug_to_condition",
|
||||
drugs=subject_drugs,
|
||||
attribute="chi_dinh",
|
||||
condition_relation=ConditionRelation.INDICATION,
|
||||
needs_clarify=False,
|
||||
@@ -762,6 +847,7 @@ def _apply_named_drug_cues(frame: QueryFrame, turn: str) -> QueryFrame:
|
||||
return replace(
|
||||
frame,
|
||||
turn_type="drug_attribute",
|
||||
drugs=subject_drugs,
|
||||
attribute="chong_chi_dinh",
|
||||
needs_clarify=False,
|
||||
clarify_reason=None,
|
||||
|
||||
Reference in New Issue
Block a user