patient-edu-qa / scripts /gen_router_augment.py
chenhaodev's picture
Initial upload: patient-edu-qa harness (code, data, LoRA, router GGUF)
784ea73 verified
Raw History Blame Contribute Delete
6.83 kB
#!/usr/bin/env python3
"""Augment router weak-label dataset.
The curated UpToDate titles yield only grounded single-intent examples. A robust
router must also learn:
(a) alert mode : chronicity / recurrence / acute / dangerous signs / population
(b) compound : parallel multi-sub-intent
This script synthesizes realistic patient utterances for these, and labels each by
running the actual rule engine (consistent weak supervision), asserting expected mode.
Output : data/router/router_train.jsonl (appended)
Algorithmic note: we *generate* the utterances from templates + vocab, then trust
scan() as the labeler; we also self-check mode against the template intent and print
any mismatch so the manual vocabulary stays aligned with the rules.
"""
import itertools
import json
import os
import re
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from scripts.red_flag_rules import scan
OUT = "/workspace/TASK18/data/router"
os.makedirs(OUT, exist_ok=True)
CAT_ENTITY = {
"heart-and-blood-vessel-disease": ["头痛", "头疼", "胸痛", "胸闷", "心慌", "血压高", "心跳快"],
"gastrointestinal-system": ["胃痛", "肚子痛", "腹痛", "反酸", "恶心", "拉肚子", "胃胀", "便秘"],
"diabetes": ["血糖高", "血糖低", "头晕没劲", "视力模糊", "手脚发麻"],
"cancer": ["淋巴结肿大", "持续低烧", "体重下降", "身上有肿块"],
"childrens-health": ["孩子发烧", "孩子咳嗽", "宝宝拉肚子", "孩子不愿意吃饭", "孩子一直哭"],
"lung-disease": ["咳喘", "气短", "咳嗽有痰", "喘不上气", "晚上憋醒"],
"brain-and-nerves": ["偏头痛", "头晕", "手麻", "癫痫发作", "记忆下降", "站不稳"],
"nervous-other": ["焦虑", "睡不好", "没精神", "情绪低落", "失眠", "担心"],
"senior-health": ["老人跌倒", "关节痛", "走几步就喘", "记性差", "尿频"],
"kidneys-and-urinary-system": ["尿频", "排尿痛", "尿量少", "下肢水肿"],
"women": ["月经不调", "痛经", "异常出血", "乳房胀痛", "白带异常"],
"muscles-joints": ["膝盖痛", "腰痛", "肩颈酸痛", "关节晨僵", "肌肉酸痛"],
"skin": ["长疹子", "皮肤痒", "湿疹", "红肿", "破皮渗液"],
"mental": ["心慌焦虑", "整夜失眠", "情绪崩溃", "坐立不安"],
"infections": ["嗓子疼", "发烧", "流鼻涕", "咳嗽", "头痛"],
}
# (trigger_template, expected_alert_types, needed flags) per mode
ALERT_TEMPLATES = [
# chronicity
("{e}已经{time}了", ["chronicity"], "duration2w"),
("{e}一直不好,{time}了", ["chronicity"], "duration2w"),
("{e}反复发作{rec}次", ["chronicity"], "recurrent"),
# acute
("突然{e}得很厉害", ["acuteness"], "acute"),
("一下子{e},人一下子就", ["acuteness"], "acute"),
# progression
("{e}越来越严重", ["progression"], "worsening"),
("{e}加重了,比前几天差", ["progression"], "worsening"),
]
TIME_CHRONIC = ["两周", "一个月", "三个月", "半年", "一年", "两个星期", "一个月多"]
REC = ["三", "四", "五六"]
ACUTE_COMBOS = {"胸痛": "突发的压榨样胸痛", "头痛": "此生最痛的头痛",
"喘不上气": "突然剧烈喘不上气", "肚子痛": "骤然的剧痛"}
def fmt(t, e, time=None, rec=None):
return t.format(e=e, time=time, rec=rec)
def gen_redflag():
out = []
n_syn = 0
for cat, ents in CAT_ENTITY.items():
for e in ents:
if not re.search(r"孩子|宝宝", e):
for t, exp_types, tag in ALERT_TEMPLATES[0:3]:
time = TIME_CHRONIC[n_syn % len(TIME_CHRONIC)]
rec = REC[n_syn % len(REC)]
u = fmt(t, e, time=time, rec=rec)
red, subs, mode = scan(u)
if mode != "alert":
print(f"[mismatch] expected alert: {u} -> {mode} red={red}")
continue
out.append(build(u))
n_syn += 1
return out
ACUTE_ENT = ["胸痛", "头痛", "喘不上气", "肚子痛", "昏迷", "抽搐"]
def gen_acute():
out = []
for e in ACUTE_ENT:
u = ACUTE_COMBOS.get(e, f"突然{e}")
red, subs, mode = scan(u)
if mode == "alert":
out.append(build(u))
return out
POP_UTT = [
"我怀孕三个月了,最近{rem}",
"我妈妈78岁,{rem}",
"我孩子1岁,{rem}",
"我做完化疗,现在{rem}",
"我在吃华法林,{rem}",
]
POP_REM = ["有点出血", "发烧", "头晕", "脸色很差"]
def gen_pop():
out = []
for u0 in POP_UTT:
for rem in POP_REM:
u = u0.format(rem=rem)
red, subs, mode = scan(u)
# population alone -> grounded is acceptable; we keep both
out.append(build(u))
return out
def gen_compound():
"""Parallel multi-intent: two symptom/entity asks joined."""
pairs = [
("头疼", "失眠", "有没有什么办法缓解,还有睡不好"),
("胃痛", "恶心", "是啥病,怎么办"),
("腿肿", "走不了路", "要紧吗,怎么处理"),
("脖子酸", "手麻", "正常吗,要不要看医生"),
("咳嗽", "低烧", "该吃什么药"),
("膝盖疼", "上下楼难受", "是关节炎吗"),
]
out = []
for a, b, c in pairs:
u = f"{a}和{b},{c}"
_, subs, mode = scan(u)
out.append(build(u))
return out
def build(u):
red, subs, mode = scan(u)
plan = {"mode": mode, "red_flags": red if mode == "alert" else [], "sub_intents": subs}
return {
"messages": [
{"role": "user", "content": f"请把下面这句话拆成结构化的患者意图计划。只输出 JSON。\n患者:{u}"},
{"role": "assistant", "content": json.dumps(plan, ensure_ascii=False)},
],
"utterance": u,
"augmented": True,
}
def main():
recs = []
recs += gen_redflag()
recs += gen_acute()
recs += gen_pop()
recs += gen_compound()
path = os.path.join(OUT, "router_augmented.jsonl")
with open(path, "w", encoding="utf-8") as f:
for r in recs:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
from collections import Counter
modes = Counter(json.loads(r["messages"][1]["content"])["mode"] for r in recs)
print("augmented records:", len(recs))
print("mode dist:", dict(modes))
# show a few examples
import random
random.seed(1)
for r in random.sample(recs, 6):
print("\nU:", r["utterance"])
print("P:", r["messages"][1]["content"])
print("saved ->", path)
if __name__ == "__main__":
main()