chore: initialize medical consultation agent demo
This commit is contained in:
@@ -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",
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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])
|
||||
@@ -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")
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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": "提示词模板元数据表"}
|
||||
@@ -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": "病例检查检验项目表"},
|
||||
)
|
||||
@@ -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": "训练诊断治疗提交表"}
|
||||
@@ -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": "训练记录表"},
|
||||
)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user