386 lines
17 KiB
Python
386 lines
17 KiB
Python
from __future__ import annotations
|
||
|
||
import sys
|
||
import json
|
||
from pathlib import Path
|
||
|
||
from sqlalchemy import select
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||
|
||
from app.db.base import Base
|
||
from app.db.session import SessionLocal, engine
|
||
from app.models import (
|
||
CaseBase,
|
||
CaseExamItem,
|
||
Department,
|
||
KnowledgeChunk,
|
||
KnowledgeDocument,
|
||
KnowledgeSource,
|
||
PromptTemplate,
|
||
ScoringRule,
|
||
TeachingCase,
|
||
TraditionalCase,
|
||
)
|
||
|
||
|
||
def init_database() -> None:
|
||
"""数据库初始化:创建当前新表体系并写入第一版 Demo 种子数据。"""
|
||
Base.metadata.create_all(bind=engine)
|
||
with SessionLocal() as db:
|
||
seed_demo_data(db)
|
||
db.commit()
|
||
|
||
|
||
def seed_demo_data(db) -> None:
|
||
"""Demo 数据初始化:仅为本地/测试库写入儿科病例和基础训练数据。"""
|
||
department = _get_or_create_department(db)
|
||
case = _get_or_create_case_base(db, department.id)
|
||
_seed_traditional_case(db, case.id)
|
||
_seed_teaching_case(db, case.id)
|
||
_seed_exam_items(db, case.id)
|
||
_seed_scoring_rules(db, case.id)
|
||
_seed_knowledge(db, department.id)
|
||
_seed_prompts(db)
|
||
|
||
|
||
def _get_or_create_department(db) -> Department:
|
||
"""科室种子:写入儿科科室。"""
|
||
department = db.scalar(select(Department).where(Department.name == "儿科"))
|
||
if department:
|
||
return department
|
||
department = Department(name="儿科", category="clinical", institution_id=1)
|
||
db.add(department)
|
||
db.flush()
|
||
return department
|
||
|
||
|
||
def _get_or_create_case_base(db, department_id: int) -> CaseBase:
|
||
"""病例主表种子:以 case_base 作为病例唯一主表。"""
|
||
case = db.scalar(select(CaseBase).where(CaseBase.title == "支气管肺炎 - 6岁男性患儿"))
|
||
if case:
|
||
return case
|
||
case = CaseBase(
|
||
title="支气管肺炎 - 6岁男性患儿",
|
||
case_type="diagnosis_treatment",
|
||
difficulty="medium",
|
||
difficulty_score=2,
|
||
chief_complaint="发热、咳嗽4天,喘息1天",
|
||
description=(
|
||
"患儿4天前无明显诱因出现发热,最高体温39.2℃,伴阵发性咳嗽,后有少量白色黏痰。"
|
||
"1天前出现喘息,夜间明显,活动后加重。精神较差,食欲下降,小便略少。"
|
||
),
|
||
patient_age=6,
|
||
patient_gender="male",
|
||
tags="pediatrics,pneumonia,demo",
|
||
symptom_tags=["发热", "咳嗽", "喘息", "精神食纳差"],
|
||
disease_tags=["支气管肺炎"],
|
||
competency_tags=["问诊完整性", "儿科查体规范", "关键症状识别", "诊断准确性", "治疗计划合理性"],
|
||
guideline_tags=["CAP_2019", "HUMANISTIC_CARE"],
|
||
knowledge_points=["血常规", "CRP", "胸片", "血氧饱和度", "肺部湿啰音"],
|
||
icd_codes="",
|
||
estimated_minutes=20,
|
||
osce_enabled=False,
|
||
rag_enabled=True,
|
||
ai_prompt_template="app/prompts/patient/practice.md",
|
||
multimodal_assets=[],
|
||
vector_status=0,
|
||
publish_status=1,
|
||
status=1,
|
||
created_by_id=None,
|
||
department_id=department_id,
|
||
)
|
||
db.add(case)
|
||
db.flush()
|
||
return case
|
||
|
||
|
||
def _seed_traditional_case(db, case_id: int) -> None:
|
||
"""传统病例种子:练习模式读取 case_base + traditional_case。"""
|
||
if db.scalar(select(TraditionalCase).where(TraditionalCase.case_id == case_id)):
|
||
return
|
||
db.add(
|
||
TraditionalCase(
|
||
case_id=case_id,
|
||
standard_diagnosis="支气管肺炎",
|
||
standard_treatment=(
|
||
"抗感染、止咳平喘、改善氧合、严密观察病情变化;必要时雾化吸入缓解喘息,"
|
||
"监测体温、呼吸、血氧、精神反应和饮水尿量,出现低氧或呼吸困难加重时及时升级处理。"
|
||
),
|
||
guideline_reference=(
|
||
"诊断依据:发热、咳嗽、喘息,肺部湿啰音,炎症指标升高,胸片提示右下肺片状模糊影,"
|
||
"符合儿童社区获得性肺炎/支气管肺炎诊断思路。严重程度需结合呼吸频率、SpO2、意识、循环和进食饮水情况。"
|
||
),
|
||
)
|
||
)
|
||
|
||
|
||
def _seed_teaching_case(db, case_id: int) -> None:
|
||
"""教学互动病例种子:教学互动模式读取 case_base + teaching_case。"""
|
||
existing = db.scalar(select(TeachingCase).where(TeachingCase.case_id == case_id))
|
||
if existing:
|
||
try:
|
||
json.loads(existing.discussion_questions)
|
||
except (TypeError, json.JSONDecodeError):
|
||
existing.discussion_questions = _demo_teaching_questions_json()
|
||
return
|
||
questions_json = _demo_teaching_questions_json()
|
||
case = db.get(CaseBase, case_id)
|
||
if case and not case.multimodal_assets:
|
||
case.multimodal_assets = [{"type": "video", "title": "儿童肺炎教学示例视频", "url": ""}]
|
||
db.add(
|
||
TeachingCase(
|
||
case_id=case_id,
|
||
teaching_goal="围绕儿科肺炎问诊、检查选择、诊断依据、治疗决策和医患沟通完成互动训练。",
|
||
discussion_questions=questions_json,
|
||
teacher_guide="观察学生是否完整追问发热、咳嗽、喘息、既往史、接触史,并能解释胸片、炎症指标和血氧。",
|
||
scoring_focus="问诊完整性、检查合理性、诊断准确性、治疗计划、风险预案、人文沟通。",
|
||
)
|
||
)
|
||
|
||
|
||
def _demo_teaching_questions_json() -> str:
|
||
"""教学题库种子:为儿科支气管肺炎病例生成可测试的选择题、答案、解析和视频字段。"""
|
||
video = {"title": "儿童肺炎教学示例视频", "url": ""}
|
||
questions = [
|
||
{
|
||
"question_id": "q1",
|
||
"question_type": "single_choice",
|
||
"stem": "该患儿最需要优先关注的病情严重程度指标是?",
|
||
"options": [
|
||
{"key": "A", "text": "体温峰值"},
|
||
{"key": "B", "text": "血氧饱和度"},
|
||
{"key": "C", "text": "咳嗽天数"},
|
||
{"key": "D", "text": "食欲下降"},
|
||
],
|
||
"answer": "B",
|
||
"analysis": "血氧饱和度能帮助判断低氧和肺炎严重程度,是处置决策的重要依据。",
|
||
"video": video,
|
||
"knowledge_points": ["严重程度评估", "血氧判断"],
|
||
},
|
||
{
|
||
"question_id": "q2",
|
||
"question_type": "single_choice",
|
||
"stem": "结合发热、咳嗽、喘息和肺部湿啰音,最符合的诊断方向是?",
|
||
"options": [
|
||
{"key": "A", "text": "支气管肺炎"},
|
||
{"key": "B", "text": "急性胃肠炎"},
|
||
{"key": "C", "text": "泌尿系感染"},
|
||
{"key": "D", "text": "单纯过敏性鼻炎"},
|
||
],
|
||
"answer": "A",
|
||
"analysis": "呼吸道症状、肺部体征和影像/炎症指标共同支持儿童支气管肺炎。",
|
||
"video": video,
|
||
"knowledge_points": ["诊断依据", "肺部体征"],
|
||
},
|
||
{
|
||
"question_id": "q3",
|
||
"question_type": "single_choice",
|
||
"stem": "下列哪组检查最有助于完善本例肺炎诊断和严重程度评估?",
|
||
"options": [
|
||
{"key": "A", "text": "血常规、CRP、胸片、血氧饱和度"},
|
||
{"key": "B", "text": "肝功能、甲状腺功能、腹部超声"},
|
||
{"key": "C", "text": "胃镜、幽门螺杆菌、粪便常规"},
|
||
{"key": "D", "text": "骨龄片、维生素D、微量元素"},
|
||
],
|
||
"answer": "A",
|
||
"analysis": "炎症指标、胸部影像和血氧情况可共同支撑诊断和严重程度判断。",
|
||
"video": video,
|
||
"knowledge_points": ["检查选择", "辅助检查"],
|
||
},
|
||
{
|
||
"question_id": "q4",
|
||
"question_type": "single_choice",
|
||
"stem": "治疗方案中最需要覆盖的核心原则是?",
|
||
"options": [
|
||
{"key": "A", "text": "抗感染、止咳平喘、改善氧合、严密观察"},
|
||
{"key": "B", "text": "立即长期激素维持治疗"},
|
||
{"key": "C", "text": "只需补充维生素"},
|
||
{"key": "D", "text": "无需随访观察"},
|
||
],
|
||
"answer": "A",
|
||
"analysis": "儿童肺炎处置需围绕抗感染、呼吸症状缓解、氧合监测和病情变化预案展开。",
|
||
"video": video,
|
||
"knowledge_points": ["治疗原则", "风险预案"],
|
||
},
|
||
{
|
||
"question_id": "q5",
|
||
"question_type": "single_choice",
|
||
"stem": "向家属沟通时,最合适的内容是?",
|
||
"options": [
|
||
{"key": "A", "text": "说明病情、观察指标、用药注意事项和复诊/住院指征"},
|
||
{"key": "B", "text": "只告知已经开药即可"},
|
||
{"key": "C", "text": "不需要解释检查结果"},
|
||
{"key": "D", "text": "避免回答家属担心的问题"},
|
||
],
|
||
"answer": "A",
|
||
"analysis": "儿科场景需要重视家属知情、风险信号识别和家庭护理教育。",
|
||
"video": video,
|
||
"knowledge_points": ["人文沟通", "健康教育"],
|
||
},
|
||
]
|
||
return json.dumps(questions, ensure_ascii=False)
|
||
|
||
|
||
def _seed_exam_items(db, case_id: int) -> None:
|
||
"""检查项目种子:写入病例可申请检查和固定返回结果。"""
|
||
if db.scalar(select(CaseExamItem).where(CaseExamItem.case_id == case_id)):
|
||
return
|
||
items = [
|
||
("lung_auscultation", "肺部听诊", "physical_exam", "双肺呼吸音粗,可闻及散在湿啰音,右下肺较明显。", {"finding": "散在湿啰音", "location": "右下肺明显"}, True, True, 0),
|
||
("blood_routine", "血常规", "lab", "WBC 12.5×10^9/L,中性粒细胞比例72%,提示感染及炎症反应。", {"wbc": "12.5×10^9/L", "neutrophil": "72%"}, True, True, 1),
|
||
("crp", "CRP", "lab", "CRP 28 mg/L,提示炎症反应升高。", {"crp": "28 mg/L"}, True, True, 2),
|
||
("chest_xray", "胸片", "imaging", "双下肺纹理增多,右下肺片状模糊影,支持肺部感染。", {"finding": "右下肺片状模糊影"}, True, True, 3),
|
||
("spo2", "血氧饱和度", "vital_sign", "室内空气 SpO2 94%,处于临界偏低范围。", {"spo2": "94%"}, True, True, 4),
|
||
("mp_igm", "肺炎支原体IgM", "lab", "肺炎支原体IgM阴性。", {"mp_igm": "negative"}, False, False, 5),
|
||
]
|
||
for code, name, item_type, result_text, structured, is_key, abnormal, order in items:
|
||
db.add(
|
||
CaseExamItem(
|
||
case_id=case_id,
|
||
item_code=code,
|
||
item_name=name,
|
||
item_type=item_type,
|
||
category=item_type,
|
||
result_text=result_text,
|
||
result_structured=structured,
|
||
is_key=is_key,
|
||
is_abnormal=abnormal,
|
||
score_weight=5.0 if is_key else 1.0,
|
||
display_order=order,
|
||
)
|
||
)
|
||
|
||
|
||
def _seed_scoring_rules(db, case_id: int) -> None:
|
||
"""评分规则种子:写入 scoring_rule,评价时作为基础评分细则。"""
|
||
if db.scalar(select(ScoringRule).where(ScoringRule.case_id == case_id)):
|
||
return
|
||
rules = [
|
||
("信息获取", "问诊完整性", 25, "覆盖现病史、既往史、个人史、家族史、儿科特异性症状与家属担忧。"),
|
||
("分析推理", "诊断与鉴别诊断", 25, "结合症状、体征、胸片、炎症指标和血氧支持支气管肺炎诊断,并列出合理鉴别诊断。"),
|
||
("处置决策", "检查与治疗方案", 20, "检查申请合理,治疗原则覆盖抗感染、止咳平喘、改善氧合、风险预案和随访。"),
|
||
("沟通人文", "家属沟通", 15, "向家属说明病情、用药注意事项、危险信号、复诊或住院指征,并回应焦虑。"),
|
||
("临床整合", "流程与整体思维", 15, "流程连贯,把问诊、检查、诊断、治疗和沟通整合成完整临床决策。"),
|
||
]
|
||
for dimension, competency, weight, standard in rules:
|
||
db.add(
|
||
ScoringRule(
|
||
case_id=case_id,
|
||
dimension=dimension,
|
||
competency_dimension=competency,
|
||
score_weight=weight,
|
||
ai_auto_score=True,
|
||
osce_dimension=False,
|
||
scoring_standard=standard,
|
||
rubric_json={"max_score": weight, "criteria": standard},
|
||
)
|
||
)
|
||
|
||
|
||
def _seed_knowledge(db, department_id: int) -> None:
|
||
"""知识库种子:写入评分参考指南和人文沟通片段。"""
|
||
if db.scalar(select(KnowledgeSource).where(KnowledgeSource.source_code == "CAP_2019")):
|
||
return
|
||
source = KnowledgeSource(
|
||
source_code="CAP_2019",
|
||
source_name="儿童社区获得性肺炎诊疗规范(2019年版)",
|
||
source_type="clinical_guideline",
|
||
authority_level=5,
|
||
is_active=True,
|
||
)
|
||
human = KnowledgeSource(
|
||
source_code="HUMANISTIC_CARE",
|
||
source_name="问诊沟通与人文关怀要求",
|
||
source_type="humanistic_care",
|
||
authority_level=4,
|
||
is_active=True,
|
||
)
|
||
db.add_all([source, human])
|
||
db.flush()
|
||
doc = KnowledgeDocument(
|
||
source_id=source.id,
|
||
department_id=department_id,
|
||
title="儿童社区获得性肺炎诊疗规范摘要",
|
||
task_type="diagnosis_treatment",
|
||
summary="用于肺炎病例诊断、严重程度评估和治疗评分参考。",
|
||
file_path="docs/knowledge/cap_2019.md",
|
||
is_active=True,
|
||
)
|
||
human_doc = KnowledgeDocument(
|
||
source_id=human.id,
|
||
department_id=department_id,
|
||
title="儿科问诊人文关怀要点",
|
||
task_type="diagnosis_treatment",
|
||
summary="用于评价家属沟通、知情告知和健康教育。",
|
||
file_path="docs/knowledge/humanistic_care.md",
|
||
is_active=True,
|
||
)
|
||
db.add_all([doc, human_doc])
|
||
db.flush()
|
||
db.add_all(
|
||
[
|
||
KnowledgeChunk(
|
||
document_id=doc.id,
|
||
department_id=department_id,
|
||
task_type="diagnosis_treatment",
|
||
chunk_text="儿童肺炎诊断需综合发热、咳嗽、喘息、肺部湿啰音、炎症指标和胸部影像学新发浸润影。",
|
||
keywords=["发热", "咳嗽", "喘息", "胸片异常"],
|
||
weight=5.0,
|
||
is_active=True,
|
||
),
|
||
KnowledgeChunk(
|
||
document_id=doc.id,
|
||
department_id=department_id,
|
||
task_type="diagnosis_treatment",
|
||
chunk_text="严重程度评估应关注呼吸频率、血氧饱和度、意识状态、循环状态和进食饮水情况。",
|
||
keywords=["血氧饱和度下降", "呼吸", "严重程度"],
|
||
weight=4.5,
|
||
is_active=True,
|
||
),
|
||
KnowledgeChunk(
|
||
document_id=human_doc.id,
|
||
department_id=department_id,
|
||
task_type="diagnosis_treatment",
|
||
chunk_text="儿科问诊需要向家属说明病情观察指标、用药注意事项、复诊指征,并给予情绪安抚。",
|
||
keywords=["沟通", "健康教育", "家属"],
|
||
weight=4.0,
|
||
is_active=True,
|
||
),
|
||
]
|
||
)
|
||
|
||
|
||
def _seed_prompts(db) -> None:
|
||
"""提示词种子:写入 Markdown 模板元数据,正文保存在 prompts 目录。"""
|
||
templates = [
|
||
("patient_practice", "patient", "practice", "v1", "fast", "text", "app/prompts/patient/practice.md"),
|
||
("patient_teaching", "patient", "teaching", "v1", "fast", "text", "app/prompts/patient/teaching.md"),
|
||
("novice_case_hint", "hint", "novice", "v1", "fast", "json", "app/prompts/hint/novice_case_hint.md"),
|
||
("scoring_pediatrics_pneumonia", "scoring", "pediatrics_pneumonia", "v1", "fast", "json", "app/prompts/scoring/pediatrics_pneumonia.md"),
|
||
("scoring_teaching_interaction", "scoring", "teaching_interaction", "v1", "fast", "json", "app/prompts/scoring/teaching_interaction_evaluation.md"),
|
||
("report_evaluation", "report", "evaluation", "v1", "fast", "json", "app/prompts/report/evaluation_report.md"),
|
||
]
|
||
for code, agent_type, scene, version, model_type, output_format, file_path in templates:
|
||
template = db.scalar(select(PromptTemplate).where(PromptTemplate.template_code == code, PromptTemplate.version_no == version))
|
||
if not template:
|
||
template = PromptTemplate(template_code=code, version_no=version)
|
||
template.agent_type = agent_type
|
||
template.scene = scene
|
||
template.model_type = model_type
|
||
template.output_format = output_format
|
||
template.file_path = file_path
|
||
template.is_active = True
|
||
db.add(template)
|
||
|
||
|
||
def ensure_storage_dirs() -> None:
|
||
"""目录初始化:创建报告导出目录。"""
|
||
Path("storage/reports").mkdir(parents=True, exist_ok=True)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
ensure_storage_dirs()
|
||
init_database()
|
||
print("Demo database initialized.")
|