chore: initialize medical consultation agent demo

This commit is contained in:
刘金宝
2026-06-01 09:25:26 +08:00
commit a7733243b2
139 changed files with 15764 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
"""ORM 模型导出:初始化数据库时只导入当前新表体系需要的模型。"""
from app.models.audit import AuditLog
from app.models.department import Department
from app.models.knowledge import KnowledgeChunk, KnowledgeDocument, KnowledgeSource
from app.models.prompt import PromptTemplate
from app.models.source_case import CaseBase, CaseExamItem, ScoringRule, TeachingCase, TraditionalCase
from app.models.training import SessionOrder, SessionSubmission, TrainingSession
from app.models.training_record import TrainingRecord
from app.models.user import User, UserLearningProfile
__all__ = [
"AuditLog",
"Department",
"KnowledgeChunk",
"KnowledgeDocument",
"KnowledgeSource",
"PromptTemplate",
"CaseBase",
"CaseExamItem",
"TraditionalCase",
"TeachingCase",
"ScoringRule",
"SessionOrder",
"SessionSubmission",
"TrainingSession",
"TrainingRecord",
"User",
"UserLearningProfile",
]
+25
View File
@@ -0,0 +1,25 @@
from datetime import datetime
from sqlalchemy import DateTime, Integer, JSON, String
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
class AuditLog(Base):
"""审计日志模型:记录关键接口和安全相关元数据。"""
__tablename__ = "audit_logs"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[str | None] = mapped_column(String(128), index=True)
tenant_id: Mapped[str | None] = mapped_column(String(128))
session_id: Mapped[int | None] = mapped_column(Integer, index=True)
action: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
resource_type: Mapped[str] = mapped_column(String(64), nullable=False)
resource_id: Mapped[str | None] = mapped_column(String(128))
request_id: Mapped[str | None] = mapped_column(String(128))
ip_address: Mapped[str | None] = mapped_column(String(64))
user_agent: Mapped[str | None] = mapped_column(String(512))
metadata_: Mapped[dict | None] = mapped_column("metadata", JSON)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
+22
View File
@@ -0,0 +1,22 @@
from __future__ import annotations
from sqlalchemy import Boolean, ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
from app.models.mixins import TimestampMixin
class Department(TimestampMixin, Base):
"""科室模型:维护病例、知识库和评分规则的科室分类。"""
__tablename__ = "departments"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(String(100), nullable=False)
code: Mapped[str] = mapped_column(String(50), nullable=False, unique=True, index=True)
parent_id: Mapped[int | None] = mapped_column(ForeignKey("departments.id"), nullable=True)
sort_order: Mapped[int] = mapped_column(Integer, default=0)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
parent: Mapped["Department | None"] = relationship(remote_side=[id])
+56
View File
@@ -0,0 +1,56 @@
from sqlalchemy import Boolean, Enum, ForeignKey, Integer, JSON, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
from app.models.mixins import TimestampMixin
class KnowledgeSource(TimestampMixin, Base):
"""知识来源模型:保存指南、专家标准和考试要求来源。"""
__tablename__ = "knowledge_sources"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
source_code: Mapped[str] = mapped_column(String(64), nullable=False, unique=True, index=True)
source_name: Mapped[str] = mapped_column(String(255), nullable=False)
source_type: Mapped[str] = mapped_column(
Enum("national_standard", "department_expert", "exam_requirement", "clinical_guideline", "humanistic_care", "other"),
nullable=False,
index=True,
)
authority_level: Mapped[int] = mapped_column(Integer, default=1)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
class KnowledgeDocument(TimestampMixin, Base):
"""知识文档模型:保存知识来源下的具体文档元数据。"""
__tablename__ = "knowledge_documents"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
source_id: Mapped[int] = mapped_column(ForeignKey("knowledge_sources.id"), nullable=False, index=True)
department_id: Mapped[int | None] = mapped_column(ForeignKey("departments.id"), nullable=True, index=True)
title: Mapped[str] = mapped_column(String(255), nullable=False)
task_type: Mapped[str | None] = mapped_column(String(64), index=True)
summary: Mapped[str | None] = mapped_column(Text)
file_path: Mapped[str | None] = mapped_column(String(512))
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
source = relationship("KnowledgeSource")
class KnowledgeChunk(Base):
"""知识片段模型:保存评分前检索和拼接使用的指南片段。"""
__tablename__ = "knowledge_chunks"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
document_id: Mapped[int] = mapped_column(ForeignKey("knowledge_documents.id"), nullable=False, index=True)
department_id: Mapped[int | None] = mapped_column(ForeignKey("departments.id"), nullable=True, index=True)
task_type: Mapped[str | None] = mapped_column(String(64), index=True)
chunk_text: Mapped[str] = mapped_column(Text, nullable=False)
keywords: Mapped[list | None] = mapped_column(JSON)
weight: Mapped[float] = mapped_column(default=1.0)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
document = relationship("KnowledgeDocument")
+15
View File
@@ -0,0 +1,15 @@
from datetime import datetime
from sqlalchemy import DateTime
from sqlalchemy.orm import Mapped, mapped_column
class TimestampMixin:
"""时间字段:统一提供创建时间和更新时间。"""
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(
DateTime,
default=datetime.utcnow,
onupdate=datetime.utcnow,
)
+28
View File
@@ -0,0 +1,28 @@
from sqlalchemy import Boolean, Enum, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
from app.models.mixins import TimestampMixin
class PromptTemplate(TimestampMixin, Base):
"""提示词模板元数据:保存 Markdown 模板路径、场景、版本和启用状态。"""
__tablename__ = "prompt_templates"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="提示词模板ID")
template_code: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="模板编码")
agent_type: Mapped[str] = mapped_column(
Enum("patient", "scoring", "report", "polish", "hint", "knowledge"),
nullable=False,
index=True,
comment="Agent类型",
)
scene: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="使用场景")
version_no: Mapped[str] = mapped_column(String(32), nullable=False, comment="版本号")
model_type: Mapped[str] = mapped_column(Enum("fast", "reason"), nullable=False, comment="模型类型")
output_format: Mapped[str] = mapped_column(Enum("text", "json"), nullable=False, comment="输出格式")
file_path: Mapped[str] = mapped_column(String(512), nullable=False, comment="Markdown文件路径")
is_active: Mapped[bool] = mapped_column(Boolean, default=True, comment="是否启用")
__table_args__ = {"comment": "提示词模板元数据表"}
+228
View File
@@ -0,0 +1,228 @@
from __future__ import annotations
from decimal import Decimal
from sqlalchemy import BigInteger, Boolean, ForeignKey, Integer, JSON, Numeric, SmallInteger, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
from app.models.mixins import TimestampMixin
BIGINT_PK = BigInteger().with_variant(Integer, "sqlite")
class CaseBase(TimestampMixin, Base):
"""源库病例主表:保持 case_base 字段稳定,业务兼容字段通过属性派生。"""
__tablename__ = "case_base"
id: Mapped[int] = mapped_column(BIGINT_PK, primary_key=True, autoincrement=True, comment="病例ID")
title: Mapped[str] = mapped_column(String(255), nullable=False, comment="病例标题")
case_type: Mapped[str] = mapped_column(String(30), nullable=False, index=True, comment="病例类型")
difficulty: Mapped[str] = mapped_column(String(20), nullable=False, default="medium", index=True, comment="难度")
difficulty_score: Mapped[int | None] = mapped_column(Integer, comment="难度分")
chief_complaint: Mapped[str] = mapped_column(Text, nullable=False, comment="主诉")
description: Mapped[str] = mapped_column(Text, nullable=False, comment="病例描述")
patient_age: Mapped[int | None] = mapped_column(Integer, comment="患者年龄")
patient_gender: Mapped[str] = mapped_column(String(10), nullable=False, comment="患者性别")
tags: Mapped[str] = mapped_column(String(500), nullable=False, default="", comment="标签")
symptom_tags: Mapped[list] = mapped_column(JSON, nullable=False, default=list, comment="症状标签")
disease_tags: Mapped[list] = mapped_column(JSON, nullable=False, default=list, comment="疾病标签")
competency_tags: Mapped[list] = mapped_column(JSON, nullable=False, default=list, comment="能力标签")
guideline_tags: Mapped[list] = mapped_column(JSON, nullable=False, default=list, comment="指南标签")
knowledge_points: Mapped[list] = mapped_column(JSON, nullable=False, default=list, comment="知识点")
icd_codes: Mapped[str] = mapped_column(String(500), nullable=False, default="", comment="ICD编码")
estimated_minutes: Mapped[int | None] = mapped_column(Integer, comment="预计训练分钟数")
osce_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, comment="是否启用OSCE")
rag_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, comment="是否启用RAG")
ai_prompt_template: Mapped[str] = mapped_column(Text, nullable=False, default="", comment="病例AI提示词片段")
multimodal_assets: Mapped[list] = mapped_column(JSON, nullable=False, default=list, comment="多模态资源")
vector_status: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=0, comment="向量状态")
publish_status: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=1, index=True, comment="发布状态")
status: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=1, index=True, comment="启用状态")
created_by_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, index=True, comment="创建人ID")
department_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, index=True, comment="科室ID")
traditional_case = relationship("TraditionalCase", back_populates="case", uselist=False, cascade="all, delete-orphan")
teaching_case = relationship("TeachingCase", back_populates="case", uselist=False, cascade="all, delete-orphan")
scoring_rules = relationship("ScoringRule", back_populates="case", cascade="all, delete-orphan")
exam_items = relationship("CaseExamItem", back_populates="case", cascade="all, delete-orphan")
__table_args__ = {"comment": "病例主表"}
@property
def case_code(self) -> str:
"""病例编码:源表没有独立编码时用稳定派生值兼容前端。"""
return f"CASE_{self.id}"
@property
def patient_name(self) -> None:
"""患者姓名:源表不保存患者姓名。"""
return None
@property
def patient_occupation(self) -> None:
"""患者职业:源表不保存职业。"""
return None
@property
def supported_training_type(self) -> str:
"""训练类别:沿用源库 case_type,异常值按诊疗练习处理。"""
return self.case_type if self.case_type in {"case_analysis", "diagnosis_treatment", "consultation"} else "diagnosis_treatment"
@property
def supported_mode(self) -> str:
"""交互模式:存在 teaching_case 时为互动模式,否则为自由问诊。"""
return "interactive" if self.teaching_case else "free_chat"
@property
def has_teaching_video(self) -> bool:
"""教学视频标记:从多模态资源判断是否存在视频。"""
return any(isinstance(item, dict) and item.get("type") == "video" for item in (self.multimodal_assets or []))
@property
def has_knowledge_points(self) -> bool:
"""知识点标记:由源表 knowledge_points 判断。"""
return bool(self.knowledge_points)
@property
def has_quiz(self) -> bool:
"""题库标记:教学互动扩展表存在讨论题时视为有题库入口。"""
return bool(self.teaching_case and self.teaching_case.discussion_questions)
@property
def patient_opening(self) -> str:
"""AI 病人开场:根据主诉派生,不新增源表字段。"""
return f"家长:医生,孩子{self.chief_complaint},想请您看看。"
@property
def ai_patient_profile(self) -> dict:
"""AI 病人人设:由病例描述和提示词动态形成基础人设。"""
return {
"speaker": "患儿家长",
"answer_style": "简短、真实、只回答被问到的信息",
"prompt_template": self.ai_prompt_template,
}
@property
def hidden_patient_info(self) -> dict:
"""隐藏病情信息:使用病例描述和传统病例指南作为上下文。"""
return {
"case_description": self.description,
"guideline_reference": self.traditional_case.guideline_reference if self.traditional_case else "",
"teaching_goal": self.teaching_case.teaching_goal if self.teaching_case else "",
}
@property
def key_symptoms(self) -> list:
"""关键症状:使用源库 symptom_tags。"""
return self.symptom_tags or []
@property
def key_exams(self) -> list:
"""关键检查:使用源库 knowledge_points 作为演示阶段考核提示。"""
return self.knowledge_points or []
@property
def key_points(self) -> list:
"""考核要点:使用源库 competency_tags。"""
return self.competency_tags or []
@property
def diagnosis_primary(self) -> str:
"""标准诊断:练习模式来自 traditional_case.standard_diagnosis。"""
return self.traditional_case.standard_diagnosis if self.traditional_case else ""
@property
def diagnosis_basis(self) -> str:
"""诊断依据:优先使用 traditional_case.guideline_reference。"""
if self.traditional_case:
return self.traditional_case.guideline_reference
return self.description
@property
def treatment_plan(self) -> dict:
"""标准治疗:练习模式来自 traditional_case.standard_treatment。"""
return {"standard_treatment": self.traditional_case.standard_treatment} if self.traditional_case else {}
class TraditionalCase(TimestampMixin, Base):
"""源库传统病例表:练习模式读取 case_base + traditional_case。"""
__tablename__ = "traditional_case"
id: Mapped[int] = mapped_column(BIGINT_PK, primary_key=True, autoincrement=True, comment="传统病例ID")
standard_diagnosis: Mapped[str] = mapped_column(Text, nullable=False, comment="标准诊断")
standard_treatment: Mapped[str] = mapped_column(Text, nullable=False, comment="标准治疗")
guideline_reference: Mapped[str] = mapped_column(Text, nullable=False, comment="指南参考")
case_id: Mapped[int] = mapped_column(ForeignKey("case_base.id"), nullable=False, unique=True, index=True, comment="病例ID")
case = relationship("CaseBase", back_populates="traditional_case")
__table_args__ = {"comment": "传统病例扩展表"}
class TeachingCase(TimestampMixin, Base):
"""源库教学互动病例表:教学互动模式读取 case_base + teaching_case。"""
__tablename__ = "teaching_case"
id: Mapped[int] = mapped_column(BIGINT_PK, primary_key=True, autoincrement=True, comment="教学互动病例ID")
teaching_goal: Mapped[str] = mapped_column(Text, nullable=False, comment="教学目标")
discussion_questions: Mapped[str] = mapped_column(Text, nullable=False, comment="讨论问题")
teacher_guide: Mapped[str] = mapped_column(Text, nullable=False, comment="教师引导")
scoring_focus: Mapped[str] = mapped_column(Text, nullable=False, comment="评分重点")
case_id: Mapped[int] = mapped_column(ForeignKey("case_base.id"), nullable=False, unique=True, index=True, comment="病例ID")
case = relationship("CaseBase", back_populates="teaching_case")
__table_args__ = {"comment": "教学互动病例扩展表"}
class ScoringRule(TimestampMixin, Base):
"""源库评分规则表:评价时作为基础评分细则输入 Scoring Agent。"""
__tablename__ = "scoring_rule"
id: Mapped[int] = mapped_column(BIGINT_PK, primary_key=True, autoincrement=True, comment="评分规则ID")
dimension: Mapped[str] = mapped_column(String(50), nullable=False, index=True, comment="一级维度")
competency_dimension: Mapped[str] = mapped_column(String(50), nullable=False, index=True, comment="能力维度")
score_weight: Mapped[Decimal] = mapped_column(Numeric(5, 2), nullable=False, comment="分值权重")
ai_auto_score: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, comment="是否AI自动评分")
osce_dimension: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, comment="是否OSCE维度")
scoring_standard: Mapped[str] = mapped_column(Text, nullable=False, comment="评分标准")
rubric_json: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict, comment="结构化评分细则")
case_id: Mapped[int] = mapped_column(ForeignKey("case_base.id"), nullable=False, index=True, comment="病例ID")
case = relationship("CaseBase", back_populates="scoring_rules")
__table_args__ = (
UniqueConstraint("case_id", "dimension", "competency_dimension", name="uk_scoring_rule_case_dimension"),
{"comment": "评分规则表"},
)
class CaseExamItem(TimestampMixin, Base):
"""病例检查检验项目表:保存固定检查结果,避免由 LLM 编造检查/检验数据。"""
__tablename__ = "case_exam_item"
id: Mapped[int] = mapped_column(BIGINT_PK, primary_key=True, autoincrement=True, comment="检查项目ID")
case_id: Mapped[int] = mapped_column(ForeignKey("case_base.id"), nullable=False, index=True, comment="病例ID")
item_code: Mapped[str] = mapped_column(String(64), nullable=False, index=True, comment="检查项目编码")
item_name: Mapped[str] = mapped_column(String(128), nullable=False, comment="检查项目名称")
item_type: Mapped[str] = mapped_column(String(32), nullable=False, index=True, comment="项目类型")
category: Mapped[str | None] = mapped_column(String(64), comment="项目分类")
result_text: Mapped[str] = mapped_column(Text, nullable=False, comment="固定返回结果文本")
result_structured: Mapped[dict | None] = mapped_column(JSON, comment="结构化检查结果")
is_key: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否关键检查")
is_abnormal: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否异常结果")
score_weight: Mapped[Decimal] = mapped_column(Numeric(5, 2), default=0, comment="评分权重")
display_order: Mapped[int] = mapped_column(Integer, default=0, comment="展示顺序")
case = relationship("CaseBase", back_populates="exam_items")
__table_args__ = (
UniqueConstraint("case_id", "item_code", name="uk_case_exam_item_code"),
{"comment": "病例检查检验项目表"},
)
+91
View File
@@ -0,0 +1,91 @@
from datetime import datetime
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, JSON, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.db.base import Base
from app.models.mixins import TimestampMixin
BIGINT_PK = BigInteger().with_variant(Integer, "sqlite")
class TrainingSession(TimestampMixin, Base):
"""训练会话表:保存一次训练的运行状态、用户隔离信息和短期 memory key。"""
__tablename__ = "training_session"
id: Mapped[int] = mapped_column(BIGINT_PK, primary_key=True, autoincrement=True, comment="训练会话ID")
session_code: Mapped[str] = mapped_column(String(64), nullable=False, unique=True, index=True, comment="会话编码")
user_id: Mapped[str] = mapped_column("external_user_id", String(128), nullable=False, index=True, comment="宿主系统用户ID")
tenant_id: Mapped[str | None] = mapped_column(String(128), comment="租户或项目ID")
class_id: Mapped[str | None] = mapped_column(String(128), comment="班级或课程ID")
entry_scene: Mapped[str | None] = mapped_column(String(64), comment="入口场景")
case_id: Mapped[int] = mapped_column(ForeignKey("case_base.id"), nullable=False, index=True, comment="病例ID")
training_type: Mapped[str] = mapped_column("case_type", String(30), nullable=False, comment="病例/训练类型")
mode: Mapped[str] = mapped_column("training_mode", String(50), nullable=False, index=True, comment="训练模式")
score_type: Mapped[str] = mapped_column(String(20), default="percentage", comment="分数输出类型")
status: Mapped[str] = mapped_column(String(30), default="created", index=True, comment="会话状态")
started_at: Mapped[datetime | None] = mapped_column(DateTime, comment="开始时间")
inquiry_completed_at: Mapped[datetime | None] = mapped_column(DateTime, comment="问诊完成时间")
completed_at: Mapped[datetime | None] = mapped_column(DateTime, comment="完成时间")
memory_key: Mapped[str | None] = mapped_column(String(128), comment="短期memory key")
metadata_: Mapped[dict | None] = mapped_column("metadata", JSON, comment="扩展数据")
case = relationship("CaseBase")
orders = relationship("SessionOrder", back_populates="session", cascade="all, delete-orphan")
submission = relationship("SessionSubmission", back_populates="session", uselist=False, cascade="all, delete-orphan")
__table_args__ = {"comment": "训练会话表"}
class SessionOrder(Base):
"""训练检查申请表:记录用户在一次训练中申请过的检查/检验项目和固定结果。"""
__tablename__ = "training_order"
id: Mapped[int] = mapped_column(BIGINT_PK, primary_key=True, autoincrement=True, comment="检查申请ID")
session_id: Mapped[int] = mapped_column(ForeignKey("training_session.id"), nullable=False, index=True, comment="训练会话ID")
user_id: Mapped[str] = mapped_column("external_user_id", String(128), nullable=False, index=True, comment="宿主系统用户ID")
case_id: Mapped[int] = mapped_column(ForeignKey("case_base.id"), nullable=False, index=True, comment="病例ID")
case_exam_item_id: Mapped[int] = mapped_column("exam_item_id", ForeignKey("case_exam_item.id"), nullable=False, comment="检查项目ID")
item_code: Mapped[str] = mapped_column(String(64), nullable=False, comment="项目编码")
item_name: Mapped[str] = mapped_column(String(128), nullable=False, comment="项目名称")
item_type: Mapped[str] = mapped_column(String(32), nullable=False, comment="项目类型")
result_text: Mapped[str] = mapped_column(Text, nullable=False, comment="检查结果文本")
result_structured: Mapped[dict | None] = mapped_column(JSON, comment="结构化检查结果")
is_key: Mapped[bool] = mapped_column(default=False, comment="是否关键检查")
is_abnormal: Mapped[bool] = mapped_column(default=False, comment="是否异常结果")
ordered_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, comment="申请时间")
session = relationship("TrainingSession", back_populates="orders")
case = relationship("CaseBase")
exam_item = relationship("CaseExamItem")
__table_args__ = (
UniqueConstraint("session_id", "item_code", name="uk_training_order_session_item"),
{"comment": "训练检查申请表"},
)
class SessionSubmission(TimestampMixin, Base):
"""训练诊断治疗提交表:保存用户最终提交的诊断、治疗、沟通和随访内容。"""
__tablename__ = "training_submission"
id: Mapped[int] = mapped_column(BIGINT_PK, primary_key=True, autoincrement=True, comment="提交记录ID")
session_id: Mapped[int] = mapped_column(ForeignKey("training_session.id"), nullable=False, unique=True, comment="训练会话ID")
user_id: Mapped[str] = mapped_column("external_user_id", String(128), nullable=False, index=True, comment="宿主系统用户ID")
primary_diagnosis: Mapped[str | None] = mapped_column(Text, comment="主要诊断")
differential_diagnoses: Mapped[list | None] = mapped_column(JSON, comment="鉴别诊断")
diagnosis_basis: Mapped[str | None] = mapped_column(Text, comment="诊断依据")
treatment_principle: Mapped[str | None] = mapped_column(Text, comment="治疗原则")
treatment_measures: Mapped[str | None] = mapped_column(Text, comment="治疗措施")
risk_plan: Mapped[str | None] = mapped_column(Text, comment="风险预案")
communication: Mapped[str | None] = mapped_column(Text, comment="医患沟通")
follow_up: Mapped[str | None] = mapped_column(Text, comment="随访安排")
diagnosis_submitted_at: Mapped[datetime | None] = mapped_column(DateTime, comment="诊断提交时间")
treatment_submitted_at: Mapped[datetime | None] = mapped_column(DateTime, comment="治疗提交时间")
session = relationship("TrainingSession", back_populates="submission")
__table_args__ = {"comment": "训练诊断治疗提交表"}
+55
View File
@@ -0,0 +1,55 @@
from __future__ import annotations
from datetime import datetime
from decimal import Decimal
from sqlalchemy import BigInteger, DateTime, Integer, JSON, Numeric, String, Text, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
from app.models.mixins import TimestampMixin
BIGINT_PK = BigInteger().with_variant(Integer, "sqlite")
class TrainingRecord(TimestampMixin, Base):
"""训练记录表:完整完成问诊、诊断、治疗和评价后写入长期记录。"""
__tablename__ = "training_record"
id: Mapped[int] = mapped_column(BIGINT_PK, primary_key=True, autoincrement=True, comment="训练记录ID")
training_mode: Mapped[str] = mapped_column(String(50), nullable=False, index=True, comment="训练模式")
case_type: Mapped[str] = mapped_column(String(30), nullable=False, index=True, comment="病例/训练类型")
start_time: Mapped[datetime] = mapped_column(DateTime, nullable=False, comment="训练开始时间")
end_time: Mapped[datetime | None] = mapped_column(DateTime, comment="训练结束时间")
duration_seconds: Mapped[int | None] = mapped_column(Integer, comment="训练持续秒数")
total_score: Mapped[Decimal | None] = mapped_column(Numeric(5, 2), comment="总分")
ai_score: Mapped[Decimal | None] = mapped_column(Numeric(5, 2), comment="AI评分")
teacher_score: Mapped[Decimal | None] = mapped_column(Numeric(5, 2), comment="教师评分")
evaluation_level: Mapped[str] = mapped_column(String(20), nullable=False, default="", comment="评价等级")
status: Mapped[str] = mapped_column(String(30), nullable=False, index=True, comment="记录状态")
feedback: Mapped[str] = mapped_column(Text, nullable=False, default="", comment="总体反馈")
thinking_chain: Mapped[str] = mapped_column(Text, nullable=False, default="", comment="诊断循证与评分依据摘要")
diagnosis_path: Mapped[str] = mapped_column(Text, nullable=False, default="", comment="诊断路径摘要")
wrong_points: Mapped[list] = mapped_column(JSON, nullable=False, default=list, comment="错误点/扣分点")
missed_questions: Mapped[list] = mapped_column(JSON, nullable=False, default=list, comment="遗漏问题")
recommendation_result: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict, comment="改进建议和导出结果")
ai_feedback_structured: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict, comment="AI结构化评价")
osce_station_score: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict, comment="OSCE站点评分")
interruption_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="中断次数")
emotion_analysis: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict, comment="情绪分析")
prompt_version: Mapped[str] = mapped_column(String(50), nullable=False, default="v1", comment="提示词版本")
rag_context_version: Mapped[str] = mapped_column(String(50), nullable=False, default="none", comment="RAG上下文版本")
case_id: Mapped[int] = mapped_column(BigInteger, nullable=False, index=True, comment="病例ID")
teacher_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, index=True, comment="教师ID")
user_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, index=True, comment="数字用户ID")
external_user_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True, comment="宿主系统用户ID")
session_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, index=True, comment="训练会话ID")
evaluation_record_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True, index=True, comment="兼容旧评价记录ID")
score_type: Mapped[str] = mapped_column(String(20), nullable=False, default="percentage", comment="分数类型")
pdf_file_path: Mapped[str | None] = mapped_column(String(512), comment="PDF报告路径")
__table_args__ = (
UniqueConstraint("session_id", name="uk_training_record_session"),
{"comment": "训练记录表"},
)
+33
View File
@@ -0,0 +1,33 @@
from datetime import datetime
from sqlalchemy import DateTime, Integer, JSON, Numeric, String
from sqlalchemy.orm import Mapped, mapped_column
from app.db.base import Base
from app.models.mixins import TimestampMixin
class User(TimestampMixin, Base):
"""宿主用户引用:保存外部 user_id,不承担登录注册职责。"""
__tablename__ = "users"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
external_user_id: Mapped[str] = mapped_column(String(128), nullable=False, unique=True, index=True)
display_name: Mapped[str | None] = mapped_column(String(100), nullable=True)
class UserLearningProfile(TimestampMixin, Base):
"""学习档案模型:聚合完整评价记录形成用户能力画像。"""
__tablename__ = "user_learning_profiles"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[str] = mapped_column(String(128), nullable=False, index=True)
tenant_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
total_evaluations: Mapped[int] = mapped_column(Integer, default=0)
avg_score_percentage: Mapped[float | None] = mapped_column(Numeric(6, 2))
avg_score_five_point: Mapped[float | None] = mapped_column(Numeric(4, 2))
weak_dimensions: Mapped[list | None] = mapped_column(JSON)
last_evaluation_id: Mapped[int | None] = mapped_column(Integer)
last_trained_at: Mapped[datetime | None] = mapped_column(DateTime, index=True)