chore: finalize backend feature scope

This commit is contained in:
刘金宝
2026-06-11 16:19:07 +08:00
parent d855ecab82
commit ec515d5453
43 changed files with 680 additions and 712 deletions
+42 -32
View File
@@ -103,7 +103,6 @@ def run_api_contract_tests() -> None:
auth_me_operation = openapi_payload["paths"]["/api/v1/auth/me"]["get"]
assert any("HTTPBearer" in item for item in auth_me_operation.get("security", []))
assert "HTTPBearer" in openapi_payload["components"]["securitySchemes"]
assert "delete" not in openapi_payload["paths"]["/api/v1/cases/{case_id}"]
assert "/api/v1/training-config/recommended" in openapi_payload["paths"]
assert "/api/v1/training-config/options" in openapi_payload["paths"]
assert "/api/v1/sessions/{session_id}/hints/stream" in openapi_payload["paths"]
@@ -116,16 +115,33 @@ def run_api_contract_tests() -> None:
assert "/api/v1/teaching/evaluation" in openapi_payload["paths"]
assert "/api/v1/knowledge-admin/documents/upload" in openapi_payload["paths"]
assert "/api/v1/learning-assistant/chat" not in openapi_payload["paths"]
assert "/api/v1/learning-assistant/chat/stream" in openapi_payload["paths"]
assert "/api/v1/learning-assistant/sessions" in openapi_payload["paths"]
assert "/api/v1/learning-assistant/sessions/{assistant_session_id}/chat/stream" in openapi_payload["paths"]
assistant_session = client.post("/api/v1/learning-assistant/sessions", headers=headers, json={"title": "医学知识学习"})
assert assistant_session.status_code == 200
assistant_session_id = assistant_session.json()["data"]["assistant_session_id"]
assert assistant_session_id.startswith("las_")
assert assistant_session.json()["data"]["user_id"] == "api_user_001"
assert assistant_session.json()["data"]["institution_id"] == 1
cross_user_assistant = client.post(
f"/api/v1/learning-assistant/sessions/{assistant_session_id}/chat/stream",
headers={"Authorization": "Bearer api_user_002_token", "X-Entry-Scene": "api_test"},
json={"question": "这个会话是否属于我?", "top_k": 1},
)
assert cross_user_assistant.status_code == 404
assert cross_user_assistant.json()["code"] == "LEARNING_ASSISTANT_SESSION_NOT_FOUND"
with client.stream(
"POST",
"/api/v1/learning-assistant/chat/stream",
f"/api/v1/learning-assistant/sessions/{assistant_session_id}/chat/stream",
headers=headers,
json={"question": "支气管肺炎有哪些常见表现?", "top_k": 1},
) as no_kb_stream:
assert no_kb_stream.status_code == 200
no_kb_stream_text = "".join(no_kb_stream.iter_text())
assert "event: session_ready" in no_kb_stream_text
assert assistant_session_id in no_kb_stream_text
assert "event: retrieval_done" in no_kb_stream_text
assert '"retrieval_hit": false' in no_kb_stream_text
assert "event: answer_delta" in no_kb_stream_text
@@ -162,7 +178,7 @@ def run_api_contract_tests() -> None:
with client.stream(
"POST",
"/api/v1/learning-assistant/chat/stream",
f"/api/v1/learning-assistant/sessions/{assistant_session_id}/chat/stream",
headers=headers,
json={"question": "血氧下降说明什么?", "top_k": 1},
) as rag_stream:
@@ -174,9 +190,21 @@ def run_api_contract_tests() -> None:
assert "event: answer_delta" in rag_stream_text
assert "event: answer_done" in rag_stream_text
cases = client.get("/api/v1/cases", headers=headers)
assert cases.status_code == 200
case_id = cases.json()["data"]["items"][0]["id"]
with client.stream(
"POST",
f"/api/v1/learning-assistant/sessions/{assistant_session_id}/chat/stream",
headers=headers,
json={"question": "结合刚才的问题再解释一下严重程度判断。", "top_k": 1},
) as session_rag_stream:
assert session_rag_stream.status_code == 200
session_rag_stream_text = "".join(session_rag_stream.iter_text())
assert "event: session_ready" in session_rag_stream_text
assert "event: retrieval_done" in session_rag_stream_text
assert "event: answer_delta" in session_rag_stream_text
assert "event: answer_done" in session_rag_stream_text
assert assistant_session_id in session_rag_stream_text
case_id = 1
teaching_items = client.get(f"/api/v1/teaching/cases/{case_id}/items", headers=headers)
assert teaching_items.status_code == 200
@@ -284,6 +312,13 @@ def run_api_contract_tests() -> None:
)
assert invalid_config.status_code == 422
invalid_mode = client.post(
"/api/v1/sessions",
headers=headers,
json={"case_id": case_id, "training_type": "diagnosis_treatment", "mode": "invalid", "score_type": "percentage"},
)
assert invalid_mode.status_code == 422
cross_user = client.get(
f"/api/v1/sessions/{session_id}/order-items",
headers={"Authorization": "Bearer api_user_002_token", "X-Entry-Scene": "api_test"},
@@ -388,23 +423,12 @@ def run_api_contract_tests() -> None:
assert cross_user_detail.status_code == 404
assert cross_user_detail.json()["code"] == "EVALUATION_NOT_FOUND"
pdf = client.post(f"/api/v1/evaluations/{evaluation_id}/export-pdf", headers=headers)
assert pdf.status_code == 200
assert pdf.json()["data"]["file_path"]
pdf_download = client.get(f"/api/v1/evaluations/{evaluation_id}/download-pdf", headers=headers)
assert pdf_download.status_code == 200
assert pdf_download.headers["content-type"].startswith("application/pdf")
assert "attachment" in pdf_download.headers.get("content-disposition", "")
assert pdf_download.content.startswith(b"%PDF")
cross_user_pdf = client.post(
f"/api/v1/evaluations/{evaluation_id}/export-pdf",
headers={"Authorization": "Bearer api_user_002_token", "X-Entry-Scene": "api_test"},
)
assert cross_user_pdf.status_code == 404
assert cross_user_pdf.json()["code"] == "EVALUATION_NOT_FOUND"
cross_user_pdf_download = client.get(
f"/api/v1/evaluations/{evaluation_id}/download-pdf",
headers={"Authorization": "Bearer api_user_002_token", "X-Entry-Scene": "api_test"},
@@ -439,20 +463,6 @@ def run_api_contract_tests() -> None:
assert "event: hint_delta" in hint_stream_text
assert "event: hint_done" in hint_stream_text
teaching = client.post(
"/api/v1/sessions",
headers=headers,
json={"case_id": case_id, "training_type": "diagnosis_treatment", "mode": "teaching", "score_type": "percentage"},
)
assert teaching.status_code == 200
teaching_hint = client.post(
f"/api/v1/sessions/{teaching.json()['data']['session_id']}/hints",
headers=headers,
json={"scope": "current_conversation"},
)
assert teaching_hint.status_code == 400
assert teaching_hint.json()["code"] == "SESSION_STATUS_INVALID"
if __name__ == "__main__":
run_api_contract_tests()
print("api contract tests passed")
+1 -1
View File
@@ -64,7 +64,7 @@ def test_reasoning_effort_disabled_when_thinking_off() -> None:
def test_hint_agent_invalid_json_fallback() -> None:
"""新手提示:验证模型输出结构不匹配时使用稳定 fallback。"""
"""练习提示:验证模型输出结构不匹配时使用稳定 fallback。"""
agent = HintAgent()
payload = {
"case": {
+19 -3
View File
@@ -1,4 +1,5 @@
import asyncio
import json
import os
import sys
import tempfile
@@ -37,6 +38,21 @@ from app.services.session_service import SessionService
from scripts.init_demo_db import init_database
async def collect_stream_reply(session_service: SessionService, ctx: UserContext, session_id: int, message: str) -> str:
"""测试辅助:消费流式问诊 SSE,提取 AI 病人增量文本。"""
stream = await session_service.stream_chat(ctx, session_id, message)
reply = ""
done = False
async for event in stream:
if event.startswith("event: message_delta"):
payload_text = event.split("data:", 1)[1].strip()
reply += json.loads(payload_text)["delta"]
if event.startswith("event: message_done"):
done = True
assert done is True
return reply
async def run_demo_flow() -> None:
"""完整闭环:验证第一版 Demo 的核心训练链路可跑通。"""
init_database()
@@ -69,9 +85,9 @@ async def run_demo_flow() -> None:
assert created.status == "inquiry"
assert created.patient_config["labels"]["visit_environment"] == "门诊"
chat = await session_service.chat(ctx, created.session_id, ChatRequest(message="孩子最高体温多少?").message)
chat_reply = await collect_stream_reply(session_service, ctx, created.session_id, ChatRequest(message="孩子最高体温多少?").message)
db.commit()
assert chat.reply
assert chat_reply
order = order_service.create_order(created.session_id, ctx.user_id, CreateOrderRequest(item_code="chest_xray").item_code)
db.commit()
@@ -136,7 +152,7 @@ async def run_demo_flow() -> None:
assert treatment.status == "evaluating"
try:
await session_service.chat(ctx, created.session_id, "治疗后还能问诊吗?")
await session_service.stream_chat(ctx, created.session_id, "治疗后还能问诊吗?")
except AppError as exc:
assert exc.code == "SESSION_STATUS_INVALID"
else: