更新
This commit is contained in:
@@ -11,6 +11,7 @@ from doctor_workstation.core.errors import ApiProtocolError
|
||||
from doctor_workstation.core.models import Appointment, Consultation, PageResult, Prescription
|
||||
from doctor_workstation.services.mock_repository import DemoDoctorRepository
|
||||
from doctor_workstation.services.repository import (
|
||||
DIAGNOSIS_AI_PERMISSIONS,
|
||||
PRESCRIPTION_LIBRARY_PERMISSIONS,
|
||||
PRESCRIPTION_PERMISSIONS,
|
||||
RemoteDoctorRepository,
|
||||
@@ -64,6 +65,30 @@ class RecordingClient:
|
||||
return {"id": int((params or {}).get("id", 0)), "patient_name": "测试患者"}
|
||||
if endpoint == "tcm.prescription/getByAppointment":
|
||||
return {}
|
||||
if endpoint == "tcm.prescriptionLibrary/aiReports":
|
||||
return {
|
||||
"prescription_id": int((params or {}).get("id", 0)),
|
||||
"prescription_name": "疏肝健脾基础方",
|
||||
"formula_type": "主方",
|
||||
"reports": [],
|
||||
"missing_model_keys": ["qwen", "openai"],
|
||||
"can_view": True,
|
||||
"can_refresh": True,
|
||||
"can_edit": True,
|
||||
"capabilities": {"can_view": True, "can_refresh": True, "can_edit": True},
|
||||
}
|
||||
if endpoint == "tcm.diagnosis/aiReports":
|
||||
return {
|
||||
"diagnosis_id": int((params or {}).get("id", 0)),
|
||||
"patient_name": "林晓岚",
|
||||
"case_summary": "临床诊断:肝郁脾虚证",
|
||||
"reports": [],
|
||||
"missing_model_keys": ["qwen", "openai"],
|
||||
"can_view": True,
|
||||
"can_refresh": True,
|
||||
"can_edit": True,
|
||||
"capabilities": {"can_view": True, "can_refresh": True, "can_edit": True},
|
||||
}
|
||||
if endpoint == "doctor.appointment/availableSlots":
|
||||
return {"slots": [{"time": "09:00", "available": True}]}
|
||||
if endpoint == "tcm.prescriptionOrder/paidPayOrders":
|
||||
@@ -79,6 +104,78 @@ class RecordingClient:
|
||||
return {"id": 88}
|
||||
if endpoint == "tcm.diagnosis/startCall":
|
||||
return {"call_record_id": 901}
|
||||
if endpoint == "tcm.prescriptionLibrary/generateAiReports":
|
||||
return {
|
||||
"prescription_id": int(body.get("id", 0)),
|
||||
"reports": [],
|
||||
"can_refresh": True,
|
||||
"can_edit": True,
|
||||
"status": "success",
|
||||
}
|
||||
if endpoint == "tcm.prescriptionLibrary/editAiReport":
|
||||
return {
|
||||
"prescription_id": int(body.get("id", 0)),
|
||||
"report": {
|
||||
"report_id": int(body.get("report_id", 0)),
|
||||
"model_key": "qwen",
|
||||
"content": body.get("content", ""),
|
||||
},
|
||||
"can_edit": True,
|
||||
"can_refresh": True,
|
||||
}
|
||||
if endpoint == "tcm.diagnosis/generateAiReports":
|
||||
return {
|
||||
"diagnosis_id": int(body.get("id", 0)),
|
||||
"reports": [],
|
||||
"can_refresh": True,
|
||||
"can_edit": True,
|
||||
"status": "success",
|
||||
}
|
||||
if endpoint == "tcm.diagnosis/editAiReport":
|
||||
return {
|
||||
"diagnosis_id": int(body.get("id", 0)),
|
||||
"report": {
|
||||
"report_id": int(body.get("report_id", 0)),
|
||||
"model_key": "qwen",
|
||||
"content": body.get("content", ""),
|
||||
},
|
||||
"can_edit": True,
|
||||
"can_refresh": True,
|
||||
}
|
||||
if endpoint == "tcm.diagnosis/aiAssistant":
|
||||
return {
|
||||
"diagnosis_id": int(body.get("id", 0)),
|
||||
"answer": "服务端分析结果",
|
||||
"model_key": "qwen",
|
||||
"task": body.get("task"),
|
||||
}
|
||||
if endpoint == "tcm.diagnosis/aiAnalysis":
|
||||
model = str(body.get("model") or "")
|
||||
if model == "openai":
|
||||
return {
|
||||
"diagnosis_advice": "2 型糖尿病,需结合客观检查复核",
|
||||
"risk_assessment": [
|
||||
{"label": "用药安全风险", "level": "medium"},
|
||||
{"label": "肾功能风险", "level": "low"},
|
||||
],
|
||||
"treatment_advice": "复核近期检查趋势并评估联合用药安全性。",
|
||||
"model_key": "openai",
|
||||
"model_label": "OpenAI",
|
||||
"model_name": "gpt-5.2",
|
||||
"generated_at": "2026-08-14 10:31:00",
|
||||
}
|
||||
return {
|
||||
"diagnosis_advice": "2 型糖尿病,血糖控制不佳",
|
||||
"risk_assessment": [
|
||||
{"label": "高血糖风险", "level": "high"},
|
||||
{"label": "心血管风险", "level": "medium"},
|
||||
],
|
||||
"treatment_advice": "复核用药依从性并安排糖化血红蛋白检查。",
|
||||
"model_key": "qwen",
|
||||
"model_label": "千问",
|
||||
"model_name": "qwen3.6-35b",
|
||||
"generated_at": "2026-08-14 10:30:00",
|
||||
}
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@@ -395,9 +492,211 @@ def test_canonical_prescription_permissions_match_routed_views() -> None:
|
||||
"read": "wcf.prescription/read",
|
||||
"update": "wcf.prescription/edit",
|
||||
"delete": "wcf.prescription/delete",
|
||||
"ai_reports": "tcm.prescriptionLibrary/aiReports",
|
||||
"generate_ai_reports": "tcm.prescriptionLibrary/generateAiReports",
|
||||
"edit_ai_report": "tcm.prescriptionLibrary/editAiReport",
|
||||
}
|
||||
assert PRESCRIPTION_PERMISSIONS["delete"] == "cf.prescription/del"
|
||||
assert PRESCRIPTION_PERMISSIONS["patch_patient"] == "tcm.prescription/patchPatient"
|
||||
assert DIAGNOSIS_AI_PERMISSIONS == {
|
||||
"ai_reports": "tcm.diagnosis/aiReports",
|
||||
"generate_ai_reports": "tcm.diagnosis/generateAiReports",
|
||||
"edit_ai_report": "tcm.diagnosis/editAiReport",
|
||||
"analysis": "tcm.diagnosis/aiAnalysis",
|
||||
"assistant": "tcm.diagnosis/aiAssistant",
|
||||
}
|
||||
|
||||
|
||||
def test_remote_prescription_library_ai_report_endpoints() -> None:
|
||||
"""AI interpretation uses the same adminapi contract as the Vue library page."""
|
||||
|
||||
client = RecordingClient()
|
||||
repository = RemoteDoctorRepository(client)
|
||||
listed = repository.list_prescription_template_ai_reports(701)
|
||||
generated = repository.generate_prescription_template_ai_reports(701)
|
||||
edited = repository.edit_prescription_template_ai_report(
|
||||
701, report_id=9, content='{"summary":"演示"}'
|
||||
)
|
||||
|
||||
assert listed["prescription_id"] == 701
|
||||
assert generated["status"] == "success"
|
||||
assert edited["report"]["report_id"] == 9
|
||||
assert client.get_calls[-1] == (
|
||||
"tcm.prescriptionLibrary/aiReports",
|
||||
{"id": 701},
|
||||
)
|
||||
assert (
|
||||
"tcm.prescriptionLibrary/generateAiReports",
|
||||
{"id": 701},
|
||||
) in client.post_calls
|
||||
assert (
|
||||
"tcm.prescriptionLibrary/editAiReport",
|
||||
{"id": 701, "report_id": 9, "content": '{"summary":"演示"}'},
|
||||
) in client.post_calls
|
||||
|
||||
|
||||
def test_remote_diagnosis_ai_report_endpoints() -> None:
|
||||
"""Patient-profile AI reports use the diagnosis adminapi contract."""
|
||||
|
||||
client = RecordingClient()
|
||||
repository = RemoteDoctorRepository(client)
|
||||
listed = repository.list_diagnosis_ai_reports(501)
|
||||
generated = repository.generate_diagnosis_ai_reports(501)
|
||||
edited = repository.edit_diagnosis_ai_report(
|
||||
501, report_id=9, content='{"summary":"演示"}'
|
||||
)
|
||||
|
||||
assert listed["diagnosis_id"] == 501
|
||||
assert generated["status"] == "success"
|
||||
assert edited["report"]["report_id"] == 9
|
||||
assert client.get_calls[-1] == (
|
||||
"tcm.diagnosis/aiReports",
|
||||
{"id": 501},
|
||||
)
|
||||
assert (
|
||||
"tcm.diagnosis/generateAiReports",
|
||||
{"id": 501},
|
||||
) in client.post_calls
|
||||
assert (
|
||||
"tcm.diagnosis/editAiReport",
|
||||
{"id": 501, "report_id": 9, "content": '{"summary":"演示"}'},
|
||||
) in client.post_calls
|
||||
|
||||
|
||||
def test_remote_diagnosis_ai_assistant_uses_first_party_endpoint_only() -> None:
|
||||
class TimeoutRecordingClient(RecordingClient):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.timeouts: list[float | None] = []
|
||||
|
||||
def post(
|
||||
self,
|
||||
endpoint: str,
|
||||
payload: dict[str, Any] | None = None,
|
||||
*,
|
||||
timeout: float | None = None,
|
||||
) -> Any:
|
||||
self.timeouts.append(timeout)
|
||||
return super().post(endpoint, payload)
|
||||
|
||||
client = TimeoutRecordingClient()
|
||||
repository = RemoteDoctorRepository(client)
|
||||
|
||||
result = repository.analyze_diagnosis_ai(
|
||||
501,
|
||||
"请给出用药调整建议",
|
||||
task="medication_review",
|
||||
)
|
||||
|
||||
assert result["answer"] == "服务端分析结果"
|
||||
assert client.post_calls == [
|
||||
(
|
||||
"tcm.diagnosis/aiAssistant",
|
||||
{"id": 501, "prompt": "请给出用药调整建议", "task": "medication_review"},
|
||||
)
|
||||
]
|
||||
body = client.post_calls[0][1]
|
||||
assert not ({"key", "api_key", "base_url", "provider", "model"} & body.keys())
|
||||
assert client.timeouts == [105.0]
|
||||
|
||||
|
||||
def test_remote_diagnosis_ai_analysis_uses_exact_post_contract() -> None:
|
||||
"""The legacy default is qwen, followed by an explicit OpenAI request."""
|
||||
|
||||
class TimeoutRecordingClient(RecordingClient):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.timeouts: list[float | None] = []
|
||||
|
||||
def post(
|
||||
self,
|
||||
endpoint: str,
|
||||
payload: dict[str, Any] | None = None,
|
||||
*,
|
||||
timeout: float | None = None,
|
||||
) -> Any:
|
||||
self.timeouts.append(timeout)
|
||||
return super().post(endpoint, payload)
|
||||
|
||||
client = TimeoutRecordingClient()
|
||||
repository = RemoteDoctorRepository(client)
|
||||
|
||||
qwen_result = repository.get_diagnosis_ai_analysis(501)
|
||||
openai_result = repository.get_diagnosis_ai_analysis(501, model="openai")
|
||||
|
||||
assert client.post_calls == [
|
||||
("tcm.diagnosis/aiAnalysis", {"id": 501, "model": "qwen"}),
|
||||
("tcm.diagnosis/aiAnalysis", {"id": 501, "model": "openai"}),
|
||||
]
|
||||
assert client.timeouts == [105.0, 105.0]
|
||||
assert qwen_result == {
|
||||
"diagnosis_advice": "2 型糖尿病,血糖控制不佳",
|
||||
"risk_assessment": [
|
||||
{"label": "高血糖风险", "level": "high"},
|
||||
{"label": "心血管风险", "level": "medium"},
|
||||
],
|
||||
"treatment_advice": "复核用药依从性并安排糖化血红蛋白检查。",
|
||||
"model_key": "qwen",
|
||||
"model_label": "千问",
|
||||
"model_name": "qwen3.6-35b",
|
||||
"generated_at": "2026-08-14 10:30:00",
|
||||
}
|
||||
assert openai_result == {
|
||||
"diagnosis_advice": "2 型糖尿病,需结合客观检查复核",
|
||||
"risk_assessment": [
|
||||
{"label": "用药安全风险", "level": "medium"},
|
||||
{"label": "肾功能风险", "level": "low"},
|
||||
],
|
||||
"treatment_advice": "复核近期检查趋势并评估联合用药安全性。",
|
||||
"model_key": "openai",
|
||||
"model_label": "OpenAI",
|
||||
"model_name": "gpt-5.2",
|
||||
"generated_at": "2026-08-14 10:31:00",
|
||||
}
|
||||
|
||||
calls_before_validation = list(client.post_calls)
|
||||
timeouts_before_validation = list(client.timeouts)
|
||||
with pytest.raises(ValueError, match="qwen or openai"):
|
||||
repository.get_diagnosis_ai_analysis(501, model="invalid") # type: ignore[arg-type]
|
||||
with pytest.raises(ValueError, match="positive"):
|
||||
repository.get_diagnosis_ai_analysis(0)
|
||||
assert client.post_calls == calls_before_validation
|
||||
assert client.timeouts == timeouts_before_validation
|
||||
|
||||
|
||||
def test_demo_diagnosis_ai_analysis_matches_structured_contract() -> None:
|
||||
repository = DemoDoctorRepository(today=date(2026, 8, 14))
|
||||
|
||||
qwen_result = repository.get_diagnosis_ai_analysis(501)
|
||||
openai_result = repository.get_diagnosis_ai_analysis(501, model="openai")
|
||||
|
||||
expected_keys = {
|
||||
"diagnosis_advice",
|
||||
"risk_assessment",
|
||||
"treatment_advice",
|
||||
"model_key",
|
||||
"model_label",
|
||||
"model_name",
|
||||
"generated_at",
|
||||
}
|
||||
for result in (qwen_result, openai_result):
|
||||
assert set(result) == expected_keys
|
||||
assert "肝郁脾虚证" in result["diagnosis_advice"]
|
||||
assert result["treatment_advice"]
|
||||
assert result["risk_assessment"]
|
||||
assert all(
|
||||
set(item) == {"label", "level"}
|
||||
and item["level"] in {"high", "medium", "low"}
|
||||
for item in result["risk_assessment"]
|
||||
)
|
||||
assert (qwen_result["model_key"], qwen_result["model_label"]) == ("qwen", "千问")
|
||||
assert (openai_result["model_key"], openai_result["model_label"]) == (
|
||||
"openai",
|
||||
"OpenAI",
|
||||
)
|
||||
assert qwen_result["model_name"] != openai_result["model_name"]
|
||||
assert qwen_result["diagnosis_advice"] != openai_result["diagnosis_advice"]
|
||||
assert qwen_result["treatment_advice"] != openai_result["treatment_advice"]
|
||||
|
||||
|
||||
def test_demo_mutates_prescriptions_orders_and_patient_workspaces() -> None:
|
||||
|
||||
Reference in New Issue
Block a user