更新bug
This commit is contained in:
@@ -43,7 +43,7 @@ from .repository import (
|
||||
ItemT = TypeVar("ItemT")
|
||||
|
||||
|
||||
DEMO_PERMISSIONS: tuple[str, ...] = (
|
||||
DEMO_PERMISSIONS: tuple[str, ...] = (
|
||||
"doctor.appointment/lists",
|
||||
"doctor.appointment/reception",
|
||||
"doctor.appointment/notifyAssistant",
|
||||
@@ -59,12 +59,12 @@ DEMO_PERMISSIONS: tuple[str, ...] = (
|
||||
"tcm.diagnosis/delete",
|
||||
"tcm.diagnosis/readonlyDetail",
|
||||
"tcm.diagnosis/aiReports",
|
||||
"tcm.diagnosis/generateAiReports",
|
||||
"tcm.diagnosis/editAiReport",
|
||||
"tcm.diagnosis/aiAnalysis",
|
||||
"tcm.diagnosis/aiAssistant",
|
||||
"tcm.diagnosis/patientAiReports",
|
||||
"tcm.diagnosis/generatePatientAiReport",
|
||||
"tcm.diagnosis/generateAiReports",
|
||||
"tcm.diagnosis/editAiReport",
|
||||
"tcm.diagnosis/aiAnalysis",
|
||||
"tcm.diagnosis/aiAssistant",
|
||||
"tcm.diagnosis/patientAiReports",
|
||||
"tcm.diagnosis/generatePatientAiReport",
|
||||
"tcm.diagnosis/dailyRecord",
|
||||
"tcm.diagnosis/chufang",
|
||||
"tcm.diagnosis/huifang",
|
||||
@@ -120,12 +120,12 @@ DEMO_PERMISSIONS: tuple[str, ...] = (
|
||||
"tcm.diagnosis/guahao",
|
||||
"tcm.diagnosis/fillIdCard",
|
||||
"doctor.medicine/lists",
|
||||
)
|
||||
|
||||
PATIENT_AI_MEDICAL_DISCLAIMER = (
|
||||
"仅供临床辅助参考,不可替代医生诊断,不得直接用于开方、用药调整或其他医疗决策。"
|
||||
"系统未对舌像、报告附件或视频画面进行视觉诊断;仅分析已录入、归档或转写的文字及附件元数据。"
|
||||
)
|
||||
)
|
||||
|
||||
PATIENT_AI_MEDICAL_DISCLAIMER = (
|
||||
"仅供临床辅助参考,不可替代医生诊断,不得直接用于开方、用药调整或其他医疗决策。"
|
||||
"系统未对舌像、报告附件或视频画面进行视觉诊断;仅分析已录入、归档或转写的文字及附件元数据。"
|
||||
)
|
||||
|
||||
|
||||
class DemoDoctorRepository:
|
||||
@@ -155,12 +155,12 @@ class DemoDoctorRepository:
|
||||
self._template_ai_reports: dict[int, list[dict[str, Any]]] = (
|
||||
self._build_template_ai_reports()
|
||||
)
|
||||
self._diagnosis_ai_reports: dict[int, list[dict[str, Any]]] = (
|
||||
self._build_diagnosis_ai_reports()
|
||||
)
|
||||
self._patient_ai_reports: dict[int, list[dict[str, Any]]] = (
|
||||
self._build_patient_ai_reports()
|
||||
)
|
||||
self._diagnosis_ai_reports: dict[int, list[dict[str, Any]]] = (
|
||||
self._build_diagnosis_ai_reports()
|
||||
)
|
||||
self._patient_ai_reports: dict[int, list[dict[str, Any]]] = (
|
||||
self._build_patient_ai_reports()
|
||||
)
|
||||
self._prescriptions = self._build_prescriptions()
|
||||
self._medicines = self._build_medicines()
|
||||
self._patient_orders = self._build_patient_orders()
|
||||
@@ -987,7 +987,7 @@ class DemoDoctorRepository:
|
||||
consultation, generated=True, status="success"
|
||||
)
|
||||
|
||||
def edit_diagnosis_ai_report(
|
||||
def edit_diagnosis_ai_report(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
*,
|
||||
@@ -1027,155 +1027,155 @@ class DemoDoctorRepository:
|
||||
target["edited_by"] = 1001
|
||||
target["edited_time"] = int(now.timestamp())
|
||||
target["edited_at"] = now.isoformat(sep=" ")
|
||||
return {
|
||||
"diagnosis_id": consultation.id,
|
||||
"report": deepcopy(target),
|
||||
"can_edit": True,
|
||||
"can_refresh": True,
|
||||
}
|
||||
|
||||
def analyze_diagnosis_ai(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
prompt: str,
|
||||
*,
|
||||
task: str = "custom",
|
||||
) -> dict[str, Any]:
|
||||
"""Return a deterministic offline answer matching the server DTO."""
|
||||
|
||||
clean_prompt = prompt.strip()
|
||||
if not clean_prompt:
|
||||
raise ValueError("prompt is required")
|
||||
if len(clean_prompt) > 500:
|
||||
raise ValueError("prompt must not exceed 500 characters")
|
||||
if task not in {
|
||||
"summary",
|
||||
"tcm_pattern",
|
||||
"prescription_review",
|
||||
"medication_review",
|
||||
"exam_review",
|
||||
"complication_risk",
|
||||
"guideline_review",
|
||||
"custom",
|
||||
}:
|
||||
raise ValueError("task is not supported")
|
||||
with self._lock:
|
||||
consultation = self._find_consultation(diagnosis_id)
|
||||
model_key = (
|
||||
"openai"
|
||||
if task in {"exam_review", "complication_risk", "guideline_review"}
|
||||
else "qwen"
|
||||
)
|
||||
return {
|
||||
"diagnosis_id": consultation.id,
|
||||
"answer": (
|
||||
f"已结合{consultation.patient_name}的当前病历分析“{clean_prompt}”。"
|
||||
"请由医生结合四诊与最新检验结果复核。"
|
||||
),
|
||||
"model_key": model_key,
|
||||
"model_label": "千问" if model_key == "qwen" else "OpenAI",
|
||||
"task": task,
|
||||
}
|
||||
|
||||
def get_diagnosis_ai_analysis(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
*,
|
||||
model: Literal["qwen", "openai"] = "qwen",
|
||||
) -> dict[str, Any]:
|
||||
"""Return one model's structured offline analysis for the demo case."""
|
||||
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
clean_model = str(model).strip().lower()
|
||||
if clean_model not in {"qwen", "openai"}:
|
||||
raise ValueError("model must be qwen or openai")
|
||||
with self._lock:
|
||||
consultation = self._find_consultation(diagnosis_id)
|
||||
raw = consultation.raw if isinstance(consultation.raw, Mapping) else {}
|
||||
diagnosis = str(raw.get("clinical_diagnosis") or "待进一步辨证").strip()
|
||||
complaint = str(raw.get("chief_complaint") or "主诉信息待完善").strip()
|
||||
try:
|
||||
glucose = float(raw.get("fasting_blood_sugar"))
|
||||
except (TypeError, ValueError):
|
||||
glucose = None
|
||||
risk_assessment: list[dict[str, str]] = []
|
||||
if glucose is not None and glucose >= 7.0:
|
||||
risk_assessment.append({"label": "高血糖风险", "level": "high"})
|
||||
elif glucose is not None and glucose >= 6.1:
|
||||
risk_assessment.append({"label": "血糖波动风险", "level": "medium"})
|
||||
if "眠" in complaint or "睡眠" in str(raw.get("remark") or ""):
|
||||
risk_assessment.append({"label": "睡眠质量风险", "level": "low"})
|
||||
if not risk_assessment:
|
||||
risk_assessment.append({"label": "需持续随访", "level": "low"})
|
||||
treatment = str(
|
||||
raw.get("treatment_advice")
|
||||
or raw.get("treatment_principle")
|
||||
or raw.get("prescription_advice")
|
||||
or f"围绕{diagnosis}继续完善四诊信息,并结合最新检验结果调整方案。"
|
||||
).strip()
|
||||
diagnosis_advice = f"{diagnosis};重点复核:{complaint}"
|
||||
if clean_model == "openai":
|
||||
diagnosis_advice = f"{diagnosis};建议同步核对主诉与客观检查:{complaint}"
|
||||
treatment = f"{treatment} 同时复核近期检查趋势与用药安全性。"
|
||||
return {
|
||||
"diagnosis_advice": diagnosis_advice,
|
||||
"risk_assessment": risk_assessment,
|
||||
"treatment_advice": treatment,
|
||||
"model_key": clean_model,
|
||||
"model_label": "千问" if clean_model == "qwen" else "OpenAI",
|
||||
"model_name": (
|
||||
"qwen3.6-35b-demo" if clean_model == "qwen" else "openai-demo"
|
||||
),
|
||||
"generated_at": datetime.now().replace(microsecond=0).isoformat(sep=" "),
|
||||
}
|
||||
|
||||
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
|
||||
"""Return every saved patient snapshot without generating a new one."""
|
||||
|
||||
if patient_id <= 0:
|
||||
raise ValueError("patient_id must be positive")
|
||||
with self._lock:
|
||||
consultation = self._find_patient_consultation(patient_id)
|
||||
return self._patient_ai_reports_payload(patient_id, consultation)
|
||||
|
||||
def generate_patient_ai_report(
|
||||
self,
|
||||
patient_id: int,
|
||||
*,
|
||||
model: Literal["qwen", "openai"],
|
||||
) -> dict[str, Any]:
|
||||
"""Append one deterministic demo snapshot while retaining every version."""
|
||||
|
||||
if patient_id <= 0:
|
||||
raise ValueError("patient_id must be positive")
|
||||
clean_model = str(model).strip().lower()
|
||||
if clean_model not in {"qwen", "openai"}:
|
||||
raise ValueError("model must be qwen or openai")
|
||||
with self._lock:
|
||||
consultation = self._find_patient_consultation(patient_id)
|
||||
current = self._patient_ai_reports.setdefault(patient_id, [])
|
||||
version = 1 + sum(
|
||||
str(row.get("model_key") or "").strip().lower() == clean_model
|
||||
for row in current
|
||||
)
|
||||
generated = self._make_patient_ai_report(
|
||||
patient_id,
|
||||
consultation,
|
||||
model=clean_model,
|
||||
generated_at=datetime.now().replace(microsecond=0).isoformat(sep=" "),
|
||||
version=version,
|
||||
)
|
||||
current.append(generated)
|
||||
return {
|
||||
"patient_id": patient_id,
|
||||
"generated_report": deepcopy(generated),
|
||||
"report": deepcopy(generated),
|
||||
"disclaimer": PATIENT_AI_MEDICAL_DISCLAIMER,
|
||||
"source_summary": deepcopy(generated.get("source_summary", {})),
|
||||
}
|
||||
|
||||
def list_medicines(
|
||||
return {
|
||||
"diagnosis_id": consultation.id,
|
||||
"report": deepcopy(target),
|
||||
"can_edit": True,
|
||||
"can_refresh": True,
|
||||
}
|
||||
|
||||
def analyze_diagnosis_ai(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
prompt: str,
|
||||
*,
|
||||
task: str = "custom",
|
||||
) -> dict[str, Any]:
|
||||
"""Return a deterministic offline answer matching the server DTO."""
|
||||
|
||||
clean_prompt = prompt.strip()
|
||||
if not clean_prompt:
|
||||
raise ValueError("prompt is required")
|
||||
if len(clean_prompt) > 500:
|
||||
raise ValueError("prompt must not exceed 500 characters")
|
||||
if task not in {
|
||||
"summary",
|
||||
"tcm_pattern",
|
||||
"prescription_review",
|
||||
"medication_review",
|
||||
"exam_review",
|
||||
"complication_risk",
|
||||
"guideline_review",
|
||||
"custom",
|
||||
}:
|
||||
raise ValueError("task is not supported")
|
||||
with self._lock:
|
||||
consultation = self._find_consultation(diagnosis_id)
|
||||
model_key = (
|
||||
"openai"
|
||||
if task in {"exam_review", "complication_risk", "guideline_review"}
|
||||
else "qwen"
|
||||
)
|
||||
return {
|
||||
"diagnosis_id": consultation.id,
|
||||
"answer": (
|
||||
f"已结合{consultation.patient_name}的当前病历分析“{clean_prompt}”。"
|
||||
"请由医生结合四诊与最新检验结果复核。"
|
||||
),
|
||||
"model_key": model_key,
|
||||
"model_label": "千问" if model_key == "qwen" else "OpenAI",
|
||||
"task": task,
|
||||
}
|
||||
|
||||
def get_diagnosis_ai_analysis(
|
||||
self,
|
||||
diagnosis_id: int,
|
||||
*,
|
||||
model: Literal["qwen", "openai"] = "qwen",
|
||||
) -> dict[str, Any]:
|
||||
"""Return one model's structured offline analysis for the demo case."""
|
||||
|
||||
if diagnosis_id <= 0:
|
||||
raise ValueError("diagnosis_id must be positive")
|
||||
clean_model = str(model).strip().lower()
|
||||
if clean_model not in {"qwen", "openai"}:
|
||||
raise ValueError("model must be qwen or openai")
|
||||
with self._lock:
|
||||
consultation = self._find_consultation(diagnosis_id)
|
||||
raw = consultation.raw if isinstance(consultation.raw, Mapping) else {}
|
||||
diagnosis = str(raw.get("clinical_diagnosis") or "待进一步辨证").strip()
|
||||
complaint = str(raw.get("chief_complaint") or "主诉信息待完善").strip()
|
||||
try:
|
||||
glucose = float(raw.get("fasting_blood_sugar"))
|
||||
except (TypeError, ValueError):
|
||||
glucose = None
|
||||
risk_assessment: list[dict[str, str]] = []
|
||||
if glucose is not None and glucose >= 7.0:
|
||||
risk_assessment.append({"label": "高血糖风险", "level": "high"})
|
||||
elif glucose is not None and glucose >= 6.1:
|
||||
risk_assessment.append({"label": "血糖波动风险", "level": "medium"})
|
||||
if "眠" in complaint or "睡眠" in str(raw.get("remark") or ""):
|
||||
risk_assessment.append({"label": "睡眠质量风险", "level": "low"})
|
||||
if not risk_assessment:
|
||||
risk_assessment.append({"label": "需持续随访", "level": "low"})
|
||||
treatment = str(
|
||||
raw.get("treatment_advice")
|
||||
or raw.get("treatment_principle")
|
||||
or raw.get("prescription_advice")
|
||||
or f"围绕{diagnosis}继续完善四诊信息,并结合最新检验结果调整方案。"
|
||||
).strip()
|
||||
diagnosis_advice = f"{diagnosis};重点复核:{complaint}"
|
||||
if clean_model == "openai":
|
||||
diagnosis_advice = f"{diagnosis};建议同步核对主诉与客观检查:{complaint}"
|
||||
treatment = f"{treatment} 同时复核近期检查趋势与用药安全性。"
|
||||
return {
|
||||
"diagnosis_advice": diagnosis_advice,
|
||||
"risk_assessment": risk_assessment,
|
||||
"treatment_advice": treatment,
|
||||
"model_key": clean_model,
|
||||
"model_label": "千问" if clean_model == "qwen" else "OpenAI",
|
||||
"model_name": (
|
||||
"qwen3.6-35b-demo" if clean_model == "qwen" else "openai-demo"
|
||||
),
|
||||
"generated_at": datetime.now().replace(microsecond=0).isoformat(sep=" "),
|
||||
}
|
||||
|
||||
def list_patient_ai_reports(self, patient_id: int) -> dict[str, Any]:
|
||||
"""Return every saved patient snapshot without generating a new one."""
|
||||
|
||||
if patient_id <= 0:
|
||||
raise ValueError("patient_id must be positive")
|
||||
with self._lock:
|
||||
consultation = self._find_patient_consultation(patient_id)
|
||||
return self._patient_ai_reports_payload(patient_id, consultation)
|
||||
|
||||
def generate_patient_ai_report(
|
||||
self,
|
||||
patient_id: int,
|
||||
*,
|
||||
model: Literal["qwen", "openai"],
|
||||
) -> dict[str, Any]:
|
||||
"""Append one deterministic demo snapshot while retaining every version."""
|
||||
|
||||
if patient_id <= 0:
|
||||
raise ValueError("patient_id must be positive")
|
||||
clean_model = str(model).strip().lower()
|
||||
if clean_model not in {"qwen", "openai"}:
|
||||
raise ValueError("model must be qwen or openai")
|
||||
with self._lock:
|
||||
consultation = self._find_patient_consultation(patient_id)
|
||||
current = self._patient_ai_reports.setdefault(patient_id, [])
|
||||
version = 1 + sum(
|
||||
str(row.get("model_key") or "").strip().lower() == clean_model
|
||||
for row in current
|
||||
)
|
||||
generated = self._make_patient_ai_report(
|
||||
patient_id,
|
||||
consultation,
|
||||
model=clean_model,
|
||||
generated_at=datetime.now().replace(microsecond=0).isoformat(sep=" "),
|
||||
version=version,
|
||||
)
|
||||
current.append(generated)
|
||||
return {
|
||||
"patient_id": patient_id,
|
||||
"generated_report": deepcopy(generated),
|
||||
"report": deepcopy(generated),
|
||||
"disclaimer": PATIENT_AI_MEDICAL_DISCLAIMER,
|
||||
"source_summary": deepcopy(generated.get("source_summary", {})),
|
||||
}
|
||||
|
||||
def list_medicines(
|
||||
self,
|
||||
*,
|
||||
name: str = "",
|
||||
@@ -3138,23 +3138,23 @@ class DemoDoctorRepository:
|
||||
return patient
|
||||
raise RepositoryNotFoundError(f"patient diagnosis {diagnosis_id} not found")
|
||||
|
||||
def _find_consultation(self, diagnosis_id: int) -> Consultation:
|
||||
for consultation in self._consultations:
|
||||
if consultation.id == diagnosis_id:
|
||||
return consultation
|
||||
raise RepositoryNotFoundError(f"consultation {diagnosis_id} not found")
|
||||
|
||||
def _find_patient_consultation(self, patient_id: int) -> Consultation:
|
||||
"""Return the most recent demo consultation for a source patient id."""
|
||||
|
||||
matches = [
|
||||
consultation
|
||||
for consultation in self._consultations
|
||||
if consultation.patient_id == patient_id
|
||||
]
|
||||
if matches:
|
||||
return max(matches, key=lambda consultation: consultation.id)
|
||||
raise RepositoryNotFoundError(f"patient {patient_id} not found")
|
||||
def _find_consultation(self, diagnosis_id: int) -> Consultation:
|
||||
for consultation in self._consultations:
|
||||
if consultation.id == diagnosis_id:
|
||||
return consultation
|
||||
raise RepositoryNotFoundError(f"consultation {diagnosis_id} not found")
|
||||
|
||||
def _find_patient_consultation(self, patient_id: int) -> Consultation:
|
||||
"""Return the most recent demo consultation for a source patient id."""
|
||||
|
||||
matches = [
|
||||
consultation
|
||||
for consultation in self._consultations
|
||||
if consultation.patient_id == patient_id
|
||||
]
|
||||
if matches:
|
||||
return max(matches, key=lambda consultation: consultation.id)
|
||||
raise RepositoryNotFoundError(f"patient {patient_id} not found")
|
||||
|
||||
def _find_order(self, order_id: int) -> dict[str, Any]:
|
||||
for order in self._patient_orders:
|
||||
@@ -3296,6 +3296,9 @@ class DemoDoctorRepository:
|
||||
"period": "上午",
|
||||
"status": 1,
|
||||
"status_desc": "待接诊",
|
||||
"clinical_diagnosis": "肝郁脾虚证",
|
||||
"disease_course_text": "病程 3 年",
|
||||
"fasting_blood_sugar": 6.8,
|
||||
"diagnosis_confirmed": 1,
|
||||
"has_prescription": 0,
|
||||
"remark": "复诊,关注睡眠与口干。",
|
||||
@@ -3320,6 +3323,9 @@ class DemoDoctorRepository:
|
||||
"period": "上午",
|
||||
"status": 4,
|
||||
"status_desc": "已过号",
|
||||
"clinical_diagnosis": "痰湿中阻证",
|
||||
"disease_course_text": "病程 8 年",
|
||||
"fasting_blood_sugar": 8.6,
|
||||
"diagnosis_confirmed": 0,
|
||||
"has_prescription": 1,
|
||||
"prescription_audit_status": 1,
|
||||
@@ -3370,6 +3376,9 @@ class DemoDoctorRepository:
|
||||
"period": "上午",
|
||||
"status": 3,
|
||||
"status_desc": "已完成",
|
||||
"clinical_diagnosis": "气阴两虚证",
|
||||
"disease_course_text": "病程 5 年",
|
||||
"fasting_blood_sugar": 7.2,
|
||||
"diagnosis_confirmed": 1,
|
||||
"has_prescription": 1,
|
||||
"prescription_audit_status": 1,
|
||||
@@ -3872,10 +3881,10 @@ class DemoDoctorRepository:
|
||||
"is_edited": False,
|
||||
}
|
||||
|
||||
def _build_diagnosis_ai_reports(self) -> dict[int, list[dict[str, Any]]]:
|
||||
consultation = next((row for row in self._consultations if row.id == 501), None)
|
||||
if consultation is None:
|
||||
return {}
|
||||
def _build_diagnosis_ai_reports(self) -> dict[int, list[dict[str, Any]]]:
|
||||
consultation = next((row for row in self._consultations if row.id == 501), None)
|
||||
if consultation is None:
|
||||
return {}
|
||||
fingerprint = _consultation_fingerprint(consultation)
|
||||
return {
|
||||
501: [
|
||||
@@ -3886,158 +3895,158 @@ class DemoDoctorRepository:
|
||||
name="qwen3.6-35b",
|
||||
fingerprint=fingerprint,
|
||||
generated_at="2026-08-12 09:18:00",
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
def _patient_ai_reports_payload(
|
||||
self,
|
||||
patient_id: int,
|
||||
consultation: Consultation,
|
||||
) -> dict[str, Any]:
|
||||
reports = sorted(
|
||||
(deepcopy(row) for row in self._patient_ai_reports.get(patient_id, [])),
|
||||
key=lambda row: (
|
||||
str(row.get("generated_at") or row.get("created_at") or ""),
|
||||
int(row.get("id") or 0),
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
latest_by_model: dict[str, dict[str, Any] | None] = {
|
||||
"qwen": None,
|
||||
"openai": None,
|
||||
}
|
||||
for row in reports:
|
||||
model = str(row.get("model_key") or "").strip().lower()
|
||||
if model in latest_by_model and latest_by_model[model] is None:
|
||||
latest_by_model[model] = deepcopy(row)
|
||||
source_summary = (
|
||||
deepcopy(reports[0].get("source_summary"))
|
||||
if reports and isinstance(reports[0].get("source_summary"), Mapping)
|
||||
else {}
|
||||
)
|
||||
return {
|
||||
"patient_id": patient_id,
|
||||
"reports": reports,
|
||||
"latest_by_model": latest_by_model,
|
||||
"disclaimer": PATIENT_AI_MEDICAL_DISCLAIMER,
|
||||
"source_summary": source_summary,
|
||||
}
|
||||
|
||||
def _make_patient_ai_report(
|
||||
self,
|
||||
patient_id: int,
|
||||
consultation: Consultation,
|
||||
*,
|
||||
model: str,
|
||||
generated_at: str,
|
||||
version: int,
|
||||
) -> dict[str, Any]:
|
||||
report_id = self._next_ai_report_id
|
||||
self._next_ai_report_id += 1
|
||||
raw = consultation.raw if isinstance(consultation.raw, Mapping) else {}
|
||||
base_diagnosis = str(
|
||||
raw.get("clinical_diagnosis")
|
||||
or consultation.clinical_diagnosis
|
||||
or "待进一步辨证"
|
||||
).strip()
|
||||
complaint = str(
|
||||
raw.get("chief_complaint")
|
||||
or consultation.chief_complaint
|
||||
or "主诉信息待完善"
|
||||
).strip()
|
||||
is_openai = model == "openai"
|
||||
diagnosis = (
|
||||
f"{base_diagnosis};建议结合客观检查复核:{complaint}"
|
||||
if is_openai
|
||||
else f"{base_diagnosis};重点结合四诊复核:{complaint}"
|
||||
)
|
||||
risks = [
|
||||
{"label": "血糖波动风险", "level": "medium"},
|
||||
{"label": "睡眠质量风险", "level": "low"},
|
||||
]
|
||||
treatment = (
|
||||
"复核近期检查趋势、并用药物与肝肾功能,再由接诊医生决定后续方案。"
|
||||
if is_openai
|
||||
else "继续完善四诊与血糖记录,由接诊医生结合复诊结果确定后续方案。"
|
||||
)
|
||||
disclaimer = PATIENT_AI_MEDICAL_DISCLAIMER
|
||||
source_summary = {
|
||||
"diagnosis_count": 1,
|
||||
"doctor_note_count": 0,
|
||||
"tracking_note_count": 0,
|
||||
"blood_record_count": 0,
|
||||
"diet_record_count": 0,
|
||||
"exercise_record_count": 0,
|
||||
"im_message_count": 0,
|
||||
"wechat_message_count": 0,
|
||||
"call_record_count": 0,
|
||||
"transcript_segment_count": 0,
|
||||
"recording_asset_count": 0,
|
||||
"source_record_count": 1,
|
||||
"snapshot_complete": True,
|
||||
"may_be_truncated": False,
|
||||
"analysis_chunk_count": 1,
|
||||
"analysis_reduction_rounds": 0,
|
||||
"analyzed_source_bytes": 0,
|
||||
"analysis_complete": True,
|
||||
}
|
||||
structured = {
|
||||
"diagnosis": diagnosis,
|
||||
"risk_assessment": risks,
|
||||
"treatment_advice": treatment,
|
||||
"disclaimer": disclaimer,
|
||||
}
|
||||
return {
|
||||
"id": report_id,
|
||||
"report_id": report_id,
|
||||
"patient_id": patient_id,
|
||||
"diagnosis_id": consultation.id,
|
||||
"model_key": model,
|
||||
"model_name": "gpt-5.6-sol-demo" if is_openai else "qwen3.6-35b-demo",
|
||||
"model_label": "OpenAI" if is_openai else "千问",
|
||||
"version": version,
|
||||
"report": structured,
|
||||
"content": json.dumps(structured, ensure_ascii=False),
|
||||
"diagnosis": diagnosis,
|
||||
"diagnosis_advice": diagnosis,
|
||||
"risk_assessment": deepcopy(risks),
|
||||
"treatment_advice": treatment,
|
||||
"disclaimer": disclaimer,
|
||||
"source_hash": _consultation_fingerprint(consultation),
|
||||
"source_summary": source_summary,
|
||||
"generated_at": generated_at,
|
||||
"created_at": generated_at,
|
||||
"admin_id": 1001,
|
||||
"department_id": 10,
|
||||
"department_name": "演示中医院",
|
||||
}
|
||||
|
||||
def _build_patient_ai_reports(self) -> dict[int, list[dict[str, Any]]]:
|
||||
consultation = next(
|
||||
(row for row in self._consultations if row.patient_id == 301),
|
||||
None,
|
||||
)
|
||||
if consultation is None:
|
||||
return {}
|
||||
reports: list[dict[str, Any]] = []
|
||||
for version, generated_at in (
|
||||
(1, "2026-08-12 09:18:00"),
|
||||
(2, "2026-08-13 15:42:00"),
|
||||
):
|
||||
for model in ("qwen", "openai"):
|
||||
reports.append(
|
||||
self._make_patient_ai_report(
|
||||
301,
|
||||
consultation,
|
||||
model=model,
|
||||
generated_at=generated_at,
|
||||
version=version,
|
||||
)
|
||||
)
|
||||
return {301: reports}
|
||||
|
||||
def _build_templates(self) -> list[PrescriptionTemplate]:
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
def _patient_ai_reports_payload(
|
||||
self,
|
||||
patient_id: int,
|
||||
consultation: Consultation,
|
||||
) -> dict[str, Any]:
|
||||
reports = sorted(
|
||||
(deepcopy(row) for row in self._patient_ai_reports.get(patient_id, [])),
|
||||
key=lambda row: (
|
||||
str(row.get("generated_at") or row.get("created_at") or ""),
|
||||
int(row.get("id") or 0),
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
latest_by_model: dict[str, dict[str, Any] | None] = {
|
||||
"qwen": None,
|
||||
"openai": None,
|
||||
}
|
||||
for row in reports:
|
||||
model = str(row.get("model_key") or "").strip().lower()
|
||||
if model in latest_by_model and latest_by_model[model] is None:
|
||||
latest_by_model[model] = deepcopy(row)
|
||||
source_summary = (
|
||||
deepcopy(reports[0].get("source_summary"))
|
||||
if reports and isinstance(reports[0].get("source_summary"), Mapping)
|
||||
else {}
|
||||
)
|
||||
return {
|
||||
"patient_id": patient_id,
|
||||
"reports": reports,
|
||||
"latest_by_model": latest_by_model,
|
||||
"disclaimer": PATIENT_AI_MEDICAL_DISCLAIMER,
|
||||
"source_summary": source_summary,
|
||||
}
|
||||
|
||||
def _make_patient_ai_report(
|
||||
self,
|
||||
patient_id: int,
|
||||
consultation: Consultation,
|
||||
*,
|
||||
model: str,
|
||||
generated_at: str,
|
||||
version: int,
|
||||
) -> dict[str, Any]:
|
||||
report_id = self._next_ai_report_id
|
||||
self._next_ai_report_id += 1
|
||||
raw = consultation.raw if isinstance(consultation.raw, Mapping) else {}
|
||||
base_diagnosis = str(
|
||||
raw.get("clinical_diagnosis")
|
||||
or consultation.clinical_diagnosis
|
||||
or "待进一步辨证"
|
||||
).strip()
|
||||
complaint = str(
|
||||
raw.get("chief_complaint")
|
||||
or consultation.chief_complaint
|
||||
or "主诉信息待完善"
|
||||
).strip()
|
||||
is_openai = model == "openai"
|
||||
diagnosis = (
|
||||
f"{base_diagnosis};建议结合客观检查复核:{complaint}"
|
||||
if is_openai
|
||||
else f"{base_diagnosis};重点结合四诊复核:{complaint}"
|
||||
)
|
||||
risks = [
|
||||
{"label": "血糖波动风险", "level": "medium"},
|
||||
{"label": "睡眠质量风险", "level": "low"},
|
||||
]
|
||||
treatment = (
|
||||
"复核近期检查趋势、并用药物与肝肾功能,再由接诊医生决定后续方案。"
|
||||
if is_openai
|
||||
else "继续完善四诊与血糖记录,由接诊医生结合复诊结果确定后续方案。"
|
||||
)
|
||||
disclaimer = PATIENT_AI_MEDICAL_DISCLAIMER
|
||||
source_summary = {
|
||||
"diagnosis_count": 1,
|
||||
"doctor_note_count": 0,
|
||||
"tracking_note_count": 0,
|
||||
"blood_record_count": 0,
|
||||
"diet_record_count": 0,
|
||||
"exercise_record_count": 0,
|
||||
"im_message_count": 0,
|
||||
"wechat_message_count": 0,
|
||||
"call_record_count": 0,
|
||||
"transcript_segment_count": 0,
|
||||
"recording_asset_count": 0,
|
||||
"source_record_count": 1,
|
||||
"snapshot_complete": True,
|
||||
"may_be_truncated": False,
|
||||
"analysis_chunk_count": 1,
|
||||
"analysis_reduction_rounds": 0,
|
||||
"analyzed_source_bytes": 0,
|
||||
"analysis_complete": True,
|
||||
}
|
||||
structured = {
|
||||
"diagnosis": diagnosis,
|
||||
"risk_assessment": risks,
|
||||
"treatment_advice": treatment,
|
||||
"disclaimer": disclaimer,
|
||||
}
|
||||
return {
|
||||
"id": report_id,
|
||||
"report_id": report_id,
|
||||
"patient_id": patient_id,
|
||||
"diagnosis_id": consultation.id,
|
||||
"model_key": model,
|
||||
"model_name": "gpt-5.6-sol-demo" if is_openai else "qwen3.6-35b-demo",
|
||||
"model_label": "OpenAI" if is_openai else "千问",
|
||||
"version": version,
|
||||
"report": structured,
|
||||
"content": json.dumps(structured, ensure_ascii=False),
|
||||
"diagnosis": diagnosis,
|
||||
"diagnosis_advice": diagnosis,
|
||||
"risk_assessment": deepcopy(risks),
|
||||
"treatment_advice": treatment,
|
||||
"disclaimer": disclaimer,
|
||||
"source_hash": _consultation_fingerprint(consultation),
|
||||
"source_summary": source_summary,
|
||||
"generated_at": generated_at,
|
||||
"created_at": generated_at,
|
||||
"admin_id": 1001,
|
||||
"department_id": 10,
|
||||
"department_name": "演示中医院",
|
||||
}
|
||||
|
||||
def _build_patient_ai_reports(self) -> dict[int, list[dict[str, Any]]]:
|
||||
consultation = next(
|
||||
(row for row in self._consultations if row.patient_id == 301),
|
||||
None,
|
||||
)
|
||||
if consultation is None:
|
||||
return {}
|
||||
reports: list[dict[str, Any]] = []
|
||||
for version, generated_at in (
|
||||
(1, "2026-08-12 09:18:00"),
|
||||
(2, "2026-08-13 15:42:00"),
|
||||
):
|
||||
for model in ("qwen", "openai"):
|
||||
reports.append(
|
||||
self._make_patient_ai_report(
|
||||
301,
|
||||
consultation,
|
||||
model=model,
|
||||
generated_at=generated_at,
|
||||
version=version,
|
||||
)
|
||||
)
|
||||
return {301: reports}
|
||||
|
||||
def _build_templates(self) -> list[PrescriptionTemplate]:
|
||||
return [
|
||||
PrescriptionTemplate.from_dict(
|
||||
{
|
||||
|
||||
@@ -648,7 +648,7 @@ class AppointmentsPage(QWidget):
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(22, 7, 6, 8)
|
||||
root.setSpacing(4)
|
||||
header = PageHeader("患者列表")
|
||||
header = PageHeader("问诊列表")
|
||||
header.title_label.hide()
|
||||
header.subtitle_label.hide()
|
||||
root.addWidget(header)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -86,7 +86,7 @@ class NavigationItem:
|
||||
NAVIGATION = (
|
||||
NavigationItem(
|
||||
"appointments",
|
||||
"接诊台",
|
||||
"问诊列表",
|
||||
"号",
|
||||
AppointmentsPage,
|
||||
("doctor.appointment/lists",),
|
||||
@@ -301,8 +301,8 @@ def _resolve_navigation(
|
||||
continue
|
||||
if not _canonical_allowed(permissions, item.permissions[0]):
|
||||
continue
|
||||
if item.key in {"appointments", "patients"}:
|
||||
# Keep the two product-facing navigation titles stable even when
|
||||
if item.key in {"appointments", "reception", "patients"}:
|
||||
# Keep the product-facing navigation titles stable even when
|
||||
# the server still carries an older menu label.
|
||||
title = item.title
|
||||
else:
|
||||
@@ -549,7 +549,7 @@ def _painted_navigation_icon(kind: str, size: int = 18) -> QIcon:
|
||||
|
||||
|
||||
class _ShellBrandMark(QWidget):
|
||||
"""Paint the supplied indigo pulse mark without a font glyph dependency."""
|
||||
"""Paint the supplied indigo M mark without a font-glyph asset."""
|
||||
|
||||
def paintEvent(self, event: Any) -> None: # noqa: N802 - Qt virtual
|
||||
del event
|
||||
@@ -562,26 +562,12 @@ class _ShellBrandMark(QWidget):
|
||||
painter.setPen(QPen(QColor(117, 130, 255, 150), 1.0))
|
||||
painter.setBrush(gradient)
|
||||
painter.drawRoundedRect(rect, 10, 10)
|
||||
|
||||
pen = QPen(QColor("#FFFFFF"), 1.8)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
|
||||
painter.setPen(pen)
|
||||
painter.setBrush(Qt.BrushStyle.NoBrush)
|
||||
y = self.height() / 2
|
||||
painter.drawPolyline(
|
||||
QPolygonF(
|
||||
[
|
||||
QPointF(8, y + 2),
|
||||
QPointF(11, y + 2),
|
||||
QPointF(14, y - 6),
|
||||
QPointF(18, y + 7),
|
||||
QPointF(22, y - 4),
|
||||
QPointF(25, y + 2),
|
||||
QPointF(self.width() - 8, y + 2),
|
||||
]
|
||||
)
|
||||
)
|
||||
painter.setPen(QColor("#FFFFFF"))
|
||||
font = QFont(painter.font())
|
||||
font.setPixelSize(18)
|
||||
font.setWeight(QFont.Weight.Bold)
|
||||
painter.setFont(font)
|
||||
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, "M")
|
||||
|
||||
|
||||
class _AssistantRobot(QWidget):
|
||||
@@ -837,7 +823,7 @@ class ShellWindow(QMainWindow):
|
||||
parent: QWidget | None = None,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("甄养堂 · AI 问诊助手")
|
||||
self.setWindowTitle("甄养堂 · 问诊中心")
|
||||
self.setWindowFlag(Qt.WindowType.FramelessWindowHint, True)
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen, True)
|
||||
self.repository = repository
|
||||
@@ -1032,7 +1018,7 @@ class ShellWindow(QMainWindow):
|
||||
brand_copy_layout = QVBoxLayout(self.brand_copy)
|
||||
brand_copy_layout.setContentsMargins(0, 0, 0, 0)
|
||||
brand_copy_layout.setSpacing(1)
|
||||
self.brand_name = QLabel("AI问诊助手", self.brand_copy)
|
||||
self.brand_name = QLabel("问诊中心", self.brand_copy)
|
||||
self.brand_name.setObjectName("ShellBrandName")
|
||||
brand_copy_layout.addWidget(self.brand_name)
|
||||
self.brand_subtitle = QLabel("糖尿病专科版", self.brand_copy)
|
||||
@@ -1083,7 +1069,7 @@ class ShellWindow(QMainWindow):
|
||||
outer_assistant.addWidget(self.assistant_card)
|
||||
layout.addLayout(outer_assistant)
|
||||
|
||||
self.model_label = QLabel("模型:服务端自动匹配 ›", sidebar)
|
||||
self.model_label = QLabel("模型:GPT-4o 医疗版 ›", sidebar)
|
||||
self.model_label.setObjectName("ShellModelLabel")
|
||||
self.model_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
self.model_label.setFixedHeight(76)
|
||||
@@ -1130,11 +1116,15 @@ class ShellWindow(QMainWindow):
|
||||
font-size: 10px;
|
||||
}
|
||||
QPushButton#ShellAiEntry {
|
||||
min-height: 34px;
|
||||
padding: 0 10px;
|
||||
min-width: 38px;
|
||||
max-width: 38px;
|
||||
min-height: 38px;
|
||||
max-height: 38px;
|
||||
padding: 0;
|
||||
color: #5265F6;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
border-radius: 9px;
|
||||
font-weight: 700;
|
||||
}
|
||||
QPushButton#ShellAiEntry:hover { background-color: #EEF1FF; }
|
||||
@@ -1220,11 +1210,13 @@ class ShellWindow(QMainWindow):
|
||||
self.context_label.hide()
|
||||
layout.addStretch(1)
|
||||
|
||||
self.ai_top_button = QPushButton("AI 助手", topbar)
|
||||
self.ai_top_button = QPushButton("", topbar)
|
||||
self.ai_top_button.setObjectName("ShellAiEntry")
|
||||
self.ai_top_button.setIcon(_painted_shell_icon("ai", 18))
|
||||
self.ai_top_button.setIconSize(QSize(18, 18))
|
||||
self.ai_top_button.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.ai_top_button.setToolTip("AI 助手")
|
||||
self.ai_top_button.setAccessibleName("AI 助手")
|
||||
self.ai_top_button.clicked.connect(self._open_ai_assistant)
|
||||
layout.addWidget(self.ai_top_button)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user