Files
fastapi/scripts/init_demo_db.py
T
2026-06-04 17:50:22 +08:00

291 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import sys
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。"""
if db.scalar(select(TeachingCase).where(TeachingCase.case_id == case_id)):
return
db.add(
TeachingCase(
case_id=case_id,
teaching_goal="围绕儿科肺炎问诊、检查选择、诊断依据、治疗决策和医患沟通完成互动训练。",
discussion_questions="如何判断病情严重程度?哪些检查是关键检查?治疗方案如何兼顾抗感染、平喘和氧合监测?",
teacher_guide="观察学生是否完整追问发热、咳嗽、喘息、既往史、接触史,并能解释胸片、炎症指标和血氧。",
scoring_focus="问诊完整性、检查合理性、诊断准确性、治疗计划、风险预案、人文沟通。",
)
)
def _seed_exam_items(db, case_id: int) -> None:
"""检查项目种子:写入病例可申请检查和固定返回结果。"""
if db.scalar(select(CaseExamItem).where(CaseExamItem.case_id == case_id)):
return
items = [
("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"),
("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.")