From b46e43aadc42e227cb900cdcca85501757e638c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E9=87=91=E5=AE=9D?= Date: Thu, 4 Jun 2026 10:55:23 +0800 Subject: [PATCH] prepare fastapi root layout for server deployment --- .dockerignore | 8 +- .env.example | 30 +-- .env.production.example | 45 ++++ .gitignore | 5 +- Dockerfile | 23 +- README.md | 237 ++++++++++-------- {backend/app => app}/__init__.py | 0 {backend/app => app}/agents/__init__.py | 0 {backend/app => app}/agents/hint_agent.py | 0 {backend/app => app}/agents/llm_adapter.py | 0 {backend/app => app}/agents/orchestrator.py | 0 {backend/app => app}/agents/patient_agent.py | 0 {backend/app => app}/agents/report_agent.py | 0 {backend/app => app}/agents/scoring_agent.py | 0 {backend/app => app}/api/__init__.py | 0 {backend/app => app}/api/agent.py | 0 {backend/app => app}/api/auth.py | 0 {backend/app => app}/api/cases.py | 0 {backend/app => app}/api/evaluations.py | 0 app/api/health.py | 56 +++++ {backend/app => app}/api/imports.py | 0 {backend/app => app}/api/knowledge.py | 0 {backend/app => app}/api/llm_test.py | 0 {backend/app => app}/api/router.py | 0 {backend/app => app}/api/sessions.py | 0 {backend/app => app}/core/__init__.py | 0 {backend/app => app}/core/config.py | 71 +++++- {backend/app => app}/core/context.py | 0 {backend/app => app}/core/errors.py | 0 {backend/app => app}/core/exceptions.py | 0 {backend/app => app}/core/response.py | 0 {backend/app => app}/core/user_context.py | 0 {backend/app => app}/db/__init__.py | 0 {backend/app => app}/db/base.py | 0 {backend/app => app}/db/session.py | 6 +- {backend/app => app}/main.py | 12 +- {backend/app => app}/models/__init__.py | 0 {backend/app => app}/models/audit.py | 0 {backend/app => app}/models/department.py | 0 {backend/app => app}/models/knowledge.py | 0 {backend/app => app}/models/mixins.py | 0 {backend/app => app}/models/prompt.py | 0 {backend/app => app}/models/source_case.py | 0 {backend/app => app}/models/training.py | 0 .../app => app}/models/training_record.py | 0 {backend/app => app}/models/user.py | 0 .../prompts/hint/novice_case_hint.md | 0 .../app => app}/prompts/hint/novice_hint.md | 0 .../knowledge/guideline_search_query.md | 0 .../app => app}/prompts/patient/free_chat.md | 0 .../app => app}/prompts/patient/novice.md | 0 .../app => app}/prompts/patient/practice.md | 0 .../app => app}/prompts/patient/teaching.md | 0 .../prompts/polish/doctor_question_polish.md | 0 .../prompts/report/evaluation_report.md | 0 .../prompts/scoring/default_five_point.md | 0 .../prompts/scoring/default_percentage.md | 0 .../prompts/scoring/pediatrics_pneumonia.md | 0 {backend/app => app}/repositories/__init__.py | 0 .../repositories/audit_repository.py | 0 .../repositories/case_repository.py | 0 .../repositories/evaluation_repository.py | 0 .../repositories/knowledge_repository.py | 0 .../repositories/session_repository.py | 0 .../repositories/source_case_repository.py | 0 .../training_record_repository.py | 0 {backend/app => app}/schemas/__init__.py | 0 {backend/app => app}/schemas/agent.py | 0 {backend/app => app}/schemas/auth.py | 0 {backend/app => app}/schemas/case.py | 0 {backend/app => app}/schemas/evaluation.py | 0 {backend/app => app}/schemas/imports.py | 0 {backend/app => app}/schemas/knowledge.py | 0 {backend/app => app}/schemas/llm.py | 0 {backend/app => app}/schemas/session.py | 0 {backend/app => app}/services/__init__.py | 0 .../app => app}/services/audit_service.py | 0 {backend/app => app}/services/case_service.py | 0 .../services/case_sql_import_service.py | 0 .../services/evaluation_service.py | 0 .../services/external_auth_service.py | 0 .../app => app}/services/knowledge_service.py | 0 .../app => app}/services/order_service.py | 0 .../services/pdf_export_service.py | 0 .../app => app}/services/runtime_memory.py | 2 + .../app => app}/services/session_service.py | 0 backend/README.md | 45 ---- backend/pyproject.toml => pyproject.toml | 0 backend/requirements.txt => requirements.txt | 0 {backend/scripts => scripts}/__init__.py | 0 .../check_final_demo_readiness.py | 0 .../scripts => scripts}/check_final_schema.py | 0 .../clear_training_runtime_data.py | 0 .../debug_patient_stream.py | 0 .../scripts => scripts}/drop_legacy_tables.py | 0 .../import_source_case_sql.py | 0 {backend/scripts => scripts}/init_demo_db.py | 0 .../migrate_to_new_schema.py | 0 .../migrate_user_department_score_detail.py | 0 {backend/tests => tests}/test_api_contract.py | 4 + {backend/tests => tests}/test_core_logic.py | 0 {backend/tests => tests}/test_demo_flow.py | 0 .../test_import_source_case_sql.py | 0 103 files changed, 347 insertions(+), 197 deletions(-) create mode 100644 .env.production.example rename {backend/app => app}/__init__.py (100%) rename {backend/app => app}/agents/__init__.py (100%) rename {backend/app => app}/agents/hint_agent.py (100%) rename {backend/app => app}/agents/llm_adapter.py (100%) rename {backend/app => app}/agents/orchestrator.py (100%) rename {backend/app => app}/agents/patient_agent.py (100%) rename {backend/app => app}/agents/report_agent.py (100%) rename {backend/app => app}/agents/scoring_agent.py (100%) rename {backend/app => app}/api/__init__.py (100%) rename {backend/app => app}/api/agent.py (100%) rename {backend/app => app}/api/auth.py (100%) rename {backend/app => app}/api/cases.py (100%) rename {backend/app => app}/api/evaluations.py (100%) create mode 100644 app/api/health.py rename {backend/app => app}/api/imports.py (100%) rename {backend/app => app}/api/knowledge.py (100%) rename {backend/app => app}/api/llm_test.py (100%) rename {backend/app => app}/api/router.py (100%) rename {backend/app => app}/api/sessions.py (100%) rename {backend/app => app}/core/__init__.py (100%) rename {backend/app => app}/core/config.py (65%) rename {backend/app => app}/core/context.py (100%) rename {backend/app => app}/core/errors.py (100%) rename {backend/app => app}/core/exceptions.py (100%) rename {backend/app => app}/core/response.py (100%) rename {backend/app => app}/core/user_context.py (100%) rename {backend/app => app}/db/__init__.py (100%) rename {backend/app => app}/db/base.py (100%) rename {backend/app => app}/db/session.py (76%) rename {backend/app => app}/main.py (74%) rename {backend/app => app}/models/__init__.py (100%) rename {backend/app => app}/models/audit.py (100%) rename {backend/app => app}/models/department.py (100%) rename {backend/app => app}/models/knowledge.py (100%) rename {backend/app => app}/models/mixins.py (100%) rename {backend/app => app}/models/prompt.py (100%) rename {backend/app => app}/models/source_case.py (100%) rename {backend/app => app}/models/training.py (100%) rename {backend/app => app}/models/training_record.py (100%) rename {backend/app => app}/models/user.py (100%) rename {backend/app => app}/prompts/hint/novice_case_hint.md (100%) rename {backend/app => app}/prompts/hint/novice_hint.md (100%) rename {backend/app => app}/prompts/knowledge/guideline_search_query.md (100%) rename {backend/app => app}/prompts/patient/free_chat.md (100%) rename {backend/app => app}/prompts/patient/novice.md (100%) rename {backend/app => app}/prompts/patient/practice.md (100%) rename {backend/app => app}/prompts/patient/teaching.md (100%) rename {backend/app => app}/prompts/polish/doctor_question_polish.md (100%) rename {backend/app => app}/prompts/report/evaluation_report.md (100%) rename {backend/app => app}/prompts/scoring/default_five_point.md (100%) rename {backend/app => app}/prompts/scoring/default_percentage.md (100%) rename {backend/app => app}/prompts/scoring/pediatrics_pneumonia.md (100%) rename {backend/app => app}/repositories/__init__.py (100%) rename {backend/app => app}/repositories/audit_repository.py (100%) rename {backend/app => app}/repositories/case_repository.py (100%) rename {backend/app => app}/repositories/evaluation_repository.py (100%) rename {backend/app => app}/repositories/knowledge_repository.py (100%) rename {backend/app => app}/repositories/session_repository.py (100%) rename {backend/app => app}/repositories/source_case_repository.py (100%) rename {backend/app => app}/repositories/training_record_repository.py (100%) rename {backend/app => app}/schemas/__init__.py (100%) rename {backend/app => app}/schemas/agent.py (100%) rename {backend/app => app}/schemas/auth.py (100%) rename {backend/app => app}/schemas/case.py (100%) rename {backend/app => app}/schemas/evaluation.py (100%) rename {backend/app => app}/schemas/imports.py (100%) rename {backend/app => app}/schemas/knowledge.py (100%) rename {backend/app => app}/schemas/llm.py (100%) rename {backend/app => app}/schemas/session.py (100%) rename {backend/app => app}/services/__init__.py (100%) rename {backend/app => app}/services/audit_service.py (100%) rename {backend/app => app}/services/case_service.py (100%) rename {backend/app => app}/services/case_sql_import_service.py (100%) rename {backend/app => app}/services/evaluation_service.py (100%) rename {backend/app => app}/services/external_auth_service.py (100%) rename {backend/app => app}/services/knowledge_service.py (100%) rename {backend/app => app}/services/order_service.py (100%) rename {backend/app => app}/services/pdf_export_service.py (100%) rename {backend/app => app}/services/runtime_memory.py (96%) rename {backend/app => app}/services/session_service.py (100%) delete mode 100644 backend/README.md rename backend/pyproject.toml => pyproject.toml (100%) rename backend/requirements.txt => requirements.txt (100%) rename {backend/scripts => scripts}/__init__.py (100%) rename {backend/scripts => scripts}/check_final_demo_readiness.py (100%) rename {backend/scripts => scripts}/check_final_schema.py (100%) rename {backend/scripts => scripts}/clear_training_runtime_data.py (100%) rename {backend/scripts => scripts}/debug_patient_stream.py (100%) rename {backend/scripts => scripts}/drop_legacy_tables.py (100%) rename {backend/scripts => scripts}/import_source_case_sql.py (100%) rename {backend/scripts => scripts}/init_demo_db.py (100%) rename {backend/scripts => scripts}/migrate_to_new_schema.py (100%) rename {backend/scripts => scripts}/migrate_user_department_score_detail.py (100%) rename {backend/tests => tests}/test_api_contract.py (98%) rename {backend/tests => tests}/test_core_logic.py (100%) rename {backend/tests => tests}/test_demo_flow.py (100%) rename {backend/tests => tests}/test_import_source_case_sql.py (100%) diff --git a/.dockerignore b/.dockerignore index fa7144d..d8f85ee 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,7 @@ .env .env.* !.env.example +!.env.production.example __pycache__/ *.py[cod] @@ -11,8 +12,8 @@ __pycache__/ .mypy_cache/ .coverage -backend/.venv/ -backend/storage/ +.venv/ +backend/ storage/ reports/ uploads/ @@ -25,4 +26,5 @@ uploads/ frontend/ docs/ demo_frontend/ -scripts/ +tests/ +scripts/*.ps1 diff --git a/.env.example b/.env.example index f905da1..96f880e 100644 --- a/.env.example +++ b/.env.example @@ -1,32 +1,32 @@ APP_NAME=Medical Consultation Agent APP_ENV=local APP_DEBUG=true +APP_ROOT_PATH= API_V1_PREFIX=/api/v1 -APP_HOST=127.0.0.1 -APP_PORT=9000 -# MySQL -# MYSQL_URL keeps the original async style URL for future async SQLAlchemy. -# DATABASE_URL is optional; the current sync backend will normalize MYSQL_URL to mysql+pymysql automatically. -MYSQL_URL=mysql+aiomysql://root:@mysql:3306/medical_platform?charset=utf8mb4 -DATABASE_URL=mysql+pymysql://root:@mysql:3306/medical_platform?charset=utf8mb4 - -# Redis +# Local MySQL and Redis +DATABASE_URL=mysql+pymysql://root:CHANGE_ME@127.0.0.1:3306/medical?charset=utf8mb4 RUNTIME_MEMORY_BACKEND=redis -REDIS_URL=redis://redis:6379/0 +RUNTIME_MEMORY_FALLBACK_ENABLED=true RUNTIME_MEMORY_TTL_SECONDS=7200 +REDIS_URL=redis://127.0.0.1:6379/0 -# Django user center auth for frontend integration -AUTH_USER_ME_URL=http://django:8000/api/user/users/me/ +# Django user center +AUTH_VALIDATE_ENABLED=true +AUTH_USER_ME_URL=http://127.0.0.1:8000/api/user/users/me/ AUTH_TIMEOUT_SECONDS=5 AUTH_CACHE_TTL_SECONDS=300 +# Browser origins, separated by commas +CORS_ALLOW_ORIGINS=http://127.0.0.1:5173,http://localhost:5173 +CORS_ALLOW_ORIGIN_REGEX=^http://(127\.0\.0\.1|localhost|192\.168\.\d+\.\d+):\d+$ + # OpenAI-compatible LLM LLM_BASE_URL=https://api.deepseek.com/chat/completions LLM_API_KEY= -LLM_MODEL=deepseek-v4-pro -LLM_FAST_MODEL=deepseek-v4-pro -LLM_REASON_MODEL=deepseek-v4-pro +LLM_MODEL=deepseek-chat +LLM_FAST_MODEL=deepseek-chat +LLM_REASON_MODEL=deepseek-reasoner LLM_TIMEOUT_SECONDS=45 LLM_CHAT_TIMEOUT_SECONDS=20 LLM_STREAM_FIRST_TOKEN_TIMEOUT_SECONDS=15 diff --git a/.env.production.example b/.env.production.example new file mode 100644 index 0000000..cd6ab6c --- /dev/null +++ b/.env.production.example @@ -0,0 +1,45 @@ +APP_NAME=Medical Consultation Agent +APP_ENV=production +APP_DEBUG=false +APP_ROOT_PATH=/fastapi +API_V1_PREFIX=/api/v1 + +# Docker Compose service names +DATABASE_URL=mysql+pymysql://root:CHANGE_ME@mysql:3306/medical?charset=utf8mb4 +RUNTIME_MEMORY_BACKEND=redis +RUNTIME_MEMORY_FALLBACK_ENABLED=false +RUNTIME_MEMORY_TTL_SECONDS=7200 +REDIS_URL=redis://redis:6379/0 + +# Django service in the same Docker network +AUTH_VALIDATE_ENABLED=true +AUTH_USER_ME_URL=http://django:8000/api/user/users/me/ +AUTH_TIMEOUT_SECONDS=5 +AUTH_CACHE_TTL_SECONDS=300 + +# Replace with the actual frontend origin when cross-origin access is required +CORS_ALLOW_ORIGINS=http://YOUR_PUBLIC_HOST +CORS_ALLOW_ORIGIN_REGEX= + +# OpenAI-compatible LLM +LLM_BASE_URL=https://api.deepseek.com/chat/completions +LLM_API_KEY=CHANGE_ME +LLM_MODEL=deepseek-chat +LLM_FAST_MODEL=deepseek-chat +LLM_REASON_MODEL=deepseek-reasoner +LLM_TIMEOUT_SECONDS=45 +LLM_CHAT_TIMEOUT_SECONDS=20 +LLM_STREAM_FIRST_TOKEN_TIMEOUT_SECONDS=15 +LLM_STREAM_TOTAL_TIMEOUT_SECONDS=45 +LLM_STREAM_ENABLED=true +LLM_MOCK_ENABLED=false +LLM_FALLBACK_TO_MOCK=false +LLM_FAST_THINKING_ENABLED=false +LLM_REASON_THINKING_ENABLED=false +LLM_REASONING_EFFORT=low +LLM_FAST_MAX_TOKENS=512 +LLM_HINT_MAX_TOKENS=1200 +LLM_SCORING_JSON_RESPONSE=true +LLM_SCORING_MAX_TOKENS=4096 + +REPORT_STORAGE_DIR=/app/storage/reports diff --git a/.gitignore b/.gitignore index a2c1eaa..a9993bd 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ htmlcov/ .venv/ venv/ backend/.venv/ +/backend/ # Node / frontend frontend/ @@ -22,6 +23,7 @@ frontend/*.tsbuildinfo .env .env.* !.env.example +!.env.production.example # Local runtime data and generated artifacts storage/ @@ -37,7 +39,8 @@ uploads/ # Demo-only or temporary files docs/ demo_frontend/ -/scripts/ +scripts/check_mysql_demo.ps1 +scripts/init_mysql_demo.ps1 backend/test*.sql # Editor / OS diff --git a/Dockerfile b/Dockerfile index ae59838..bb820d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,27 +2,26 @@ FROM python:3.11-slim ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ - PIP_NO_CACHE_DIR=1 + PIP_NO_CACHE_DIR=1 \ + APP_ENV=production WORKDIR /app -COPY backend/requirements.txt ./requirements.txt +COPY requirements.txt . RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple \ --no-cache-dir \ -r requirements.txt -COPY backend ./backend +COPY app ./app +COPY scripts ./scripts +COPY pyproject.toml README.md ./ -WORKDIR /app/backend +RUN mkdir -p /app/logs /app/storage/reports EXPOSE 9000 -CMD [ - "uvicorn", - "app.main:app", - "--host", - "0.0.0.0", - "--port", - "9000" -] +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:9000/health/live', timeout=3)" + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "9000", "--proxy-headers", "--forwarded-allow-ips=*"] diff --git a/README.md b/README.md index d829001..eca5ce8 100644 --- a/README.md +++ b/README.md @@ -1,143 +1,174 @@ -# 医疗问诊 Agent 后端 +# 医疗问诊 Agent FastAPI 后端 -医疗问诊 Agent 是宿主医疗教学平台中的问诊训练子功能。后端基于 FastAPI 构建,负责病例读取、训练会话、多轮问诊、检查申请、诊断治疗提交、AI 评价、PDF 报告导出和历史记录查询。 +医疗问诊 Agent 是医疗教学平台中的问诊训练服务。后端负责 Django 用户身份校验、病例读取、多轮问诊、检查申请、诊断治疗提交、AI 评价、评分明细、PDF 报告和历史训练记录。 -## 技术栈 +## 项目结构 + +仓库根目录可以直接部署为服务器的 `fastapi/` 目录: + +```text +fastapi/ +├── app/ # FastAPI 应用、Agent、服务、模型与提示词 +├── scripts/ # 数据库迁移、结构检查与运维脚本 +├── tests/ # 核心逻辑与接口测试 +├── Dockerfile +├── requirements.txt +├── .env.example +└── .env.production.example +``` + +## 核心依赖 - Python 3.11 - FastAPI - SQLAlchemy 2.x -- MySQL -- Redis -- OpenAI-compatible LLM Adapter -- ReportLab PDF - -## 核心功能 - -- Django 用户中心 token 校验 -- 病例列表与病例详情 -- 病例 SQL 安全导入与删除 -- 训练会话创建 -- 多轮问诊与 SSE 流式回复 -- 练习提示 -- 检查/检验申请 -- 诊断与治疗提交 -- AI 评价报告生成 -- 评分明细落库 -- PDF 报告导出 -- 历史评价查询 -- LLM Fast/Reason 测试 +- MySQL 8 +- Redis 7 +- OpenAI-compatible LLM API +- Django 用户中心 `/api/user/users/me/` ## 本地启动 ```powershell -cd backend python -m venv .venv .\.venv\Scripts\activate pip install -r requirements.txt -cd .. copy .env.example .env -``` - -编辑 `.env`,填写 MySQL、Redis、Django 用户中心和 LLM 配置。 - -启动服务: - -```powershell -cd backend uvicorn app.main:app --host 127.0.0.1 --port 9000 ``` -访问: +本地 Swagger: ```text http://127.0.0.1:9000/docs ``` -## Docker 启动 +真实密码、LLM Key 和 access token 只写入本地 `.env` 或服务器环境变量,不提交到 Git。 -```powershell -copy .env.example .env -docker build -t medical-consultation-agent-backend . -docker run --env-file .env -p 9000:9000 medical-consultation-agent-backend +## Docker Compose 部署 + +服务器目录: + +```text +/home/code/medical-ai/ +├── django/ +├── fastapi/ # 本仓库 +├── vueapp/ +├── vuecms/ +└── docker-compose.yml ``` -## 环境变量 +首次拉取: -关键配置见 `.env.example`: - -```env -DATABASE_URL=mysql+pymysql://root:@mysql:3306/medical_platform?charset=utf8mb4 -MYSQL_URL=mysql+aiomysql://root:@mysql:3306/medical_platform?charset=utf8mb4 -REDIS_URL=redis://redis:6379/0 -AUTH_USER_ME_URL=http://django:8000/api/user/users/me/ -LLM_BASE_URL=https://api.deepseek.com/chat/completions -LLM_API_KEY= +```bash +cd /home/code/medical-ai +git clone http://82.157.235.104:3000/Liu_JB/LiuJinbao.git fastapi +cp fastapi/.env.production.example fastapi/.env +vi fastapi/.env ``` -真实数据库密码、LLM Key 和 access token 只写入本地 `.env` 或部署环境变量。 +必须在服务器 `.env` 中填写: + +- MySQL 密码和数据库名 +- `LLM_API_KEY` +- 实际前端来源 `CORS_ALLOW_ORIGINS` +- Nginx 使用 `/fastapi/` 前缀时保留 `APP_ROOT_PATH=/fastapi` + +父目录 `docker-compose.yml` 的 FastAPI 服务需要包含: + +```yaml +fastapi: + build: + context: ./fastapi + container_name: fastapi + restart: always + ports: + - "9000:9000" + env_file: + - ./fastapi/.env + volumes: + - ./logs/fastapi:/app/logs + - ./data/fastapi-reports:/app/storage/reports + depends_on: + - mysql + - redis + - django + networks: + - medical +``` + +构建并启动: + +```bash +cd /home/code/medical-ai +docker compose config +docker compose build fastapi +docker compose up -d fastapi +docker compose logs --tail=200 fastapi +``` + +后续更新: + +```bash +cd /home/code/medical-ai/fastapi +git pull origin main +cd .. +docker compose build fastapi +docker compose up -d fastapi +``` ## 数据库初始化与检查 -数据库需先创建好,表结构由后端脚本创建和校验。 - -```powershell -cd backend -.\.venv\Scripts\python.exe scripts\migrate_to_new_schema.py -.\.venv\Scripts\python.exe scripts\migrate_user_department_score_detail.py -.\.venv\Scripts\python.exe scripts\check_final_schema.py -.\.venv\Scripts\python.exe scripts\check_final_demo_readiness.py -``` - -清空训练运行数据和本地报告文件: - -```powershell -cd backend -.\.venv\Scripts\python.exe scripts\clear_training_runtime_data.py --confirm CLEAR_TRAINING_DATA --reports -``` - -该脚本只清理训练会话、检查申请、提交、评价记录、评分明细、审计日志和本地 PDF 报告,不删除病例、用户、科室、检查项、评分规则、提示词和知识库。 - -## 用户认证 - -前端请求医疗问诊 Agent 时携带宿主系统 access token: - -```http -Authorization: Bearer -X-Entry-Scene: mac_vue_dev -``` - -后端会转发 token 到 Django 用户中心: - -```text -GET /api/user/users/me/ -``` - -Django 返回 200 后,后端使用返回的 `id` 作为本系统内部用户隔离 ID。前端不需要传 `X-User-Id`。 - -验证接口: +服务启动后先进行只读结构检查: ```bash -curl -X GET "http://127.0.0.1:9000/api/v1/auth/me" \ +cd /home/code/medical-ai +docker compose exec fastapi python scripts/check_final_schema.py +docker compose exec fastapi python scripts/check_final_demo_readiness.py +``` + +仅在结构检查确认缺少 Agent 所需表时,备份数据库后执行: + +```bash +docker compose exec fastapi python scripts/migrate_to_new_schema.py +docker compose exec fastapi python scripts/migrate_user_department_score_detail.py +``` + +迁移脚本使用 `create_all` 补齐 Agent 所需表,不删除 Django 或现有业务表;`migrate_to_new_schema.py` 会在缺少对应数据时写入 Demo 病例和基础数据。 + +## 部署验证 + +容器内部端口验证: + +```bash +curl http://127.0.0.1:9000/health/live +curl http://127.0.0.1:9000/health/ready +``` + +使用 Nginx `/fastapi/` 代理后的公网验证: + +```text +http://8.160.178.88/fastapi/docs +http://8.160.178.88/fastapi/openapi.json +http://8.160.178.88/fastapi/health/ready +``` + +`/health/live` 返回 200 表示 FastAPI 进程正常。`/health/ready` 返回 200 表示 MySQL、Redis 和关键配置已经就绪;返回 503 时查看响应中的检查项和容器日志。 + +验证 Django 用户中心联调: + +```bash +curl "http://8.160.178.88/fastapi/api/v1/auth/me" \ -H "Authorization: Bearer " \ - -H "X-Entry-Scene: mac_vue_dev" + -H "X-Entry-Scene: production_vue" ``` ## 测试 ```powershell -cd backend -.\.venv\Scripts\python.exe -m compileall app scripts tests -.\.venv\Scripts\python.exe tests\test_core_logic.py -.\.venv\Scripts\python.exe tests\test_api_contract.py -.\.venv\Scripts\python.exe tests\test_demo_flow.py -``` - -## API 文档 - -启动后访问: - -```text -http://127.0.0.1:9000/docs -http://127.0.0.1:9000/openapi.json +python -m compileall app scripts tests +python tests\test_core_logic.py +python tests\test_api_contract.py +python tests\test_demo_flow.py +python tests\test_import_source_case_sql.py ``` diff --git a/backend/app/__init__.py b/app/__init__.py similarity index 100% rename from backend/app/__init__.py rename to app/__init__.py diff --git a/backend/app/agents/__init__.py b/app/agents/__init__.py similarity index 100% rename from backend/app/agents/__init__.py rename to app/agents/__init__.py diff --git a/backend/app/agents/hint_agent.py b/app/agents/hint_agent.py similarity index 100% rename from backend/app/agents/hint_agent.py rename to app/agents/hint_agent.py diff --git a/backend/app/agents/llm_adapter.py b/app/agents/llm_adapter.py similarity index 100% rename from backend/app/agents/llm_adapter.py rename to app/agents/llm_adapter.py diff --git a/backend/app/agents/orchestrator.py b/app/agents/orchestrator.py similarity index 100% rename from backend/app/agents/orchestrator.py rename to app/agents/orchestrator.py diff --git a/backend/app/agents/patient_agent.py b/app/agents/patient_agent.py similarity index 100% rename from backend/app/agents/patient_agent.py rename to app/agents/patient_agent.py diff --git a/backend/app/agents/report_agent.py b/app/agents/report_agent.py similarity index 100% rename from backend/app/agents/report_agent.py rename to app/agents/report_agent.py diff --git a/backend/app/agents/scoring_agent.py b/app/agents/scoring_agent.py similarity index 100% rename from backend/app/agents/scoring_agent.py rename to app/agents/scoring_agent.py diff --git a/backend/app/api/__init__.py b/app/api/__init__.py similarity index 100% rename from backend/app/api/__init__.py rename to app/api/__init__.py diff --git a/backend/app/api/agent.py b/app/api/agent.py similarity index 100% rename from backend/app/api/agent.py rename to app/api/agent.py diff --git a/backend/app/api/auth.py b/app/api/auth.py similarity index 100% rename from backend/app/api/auth.py rename to app/api/auth.py diff --git a/backend/app/api/cases.py b/app/api/cases.py similarity index 100% rename from backend/app/api/cases.py rename to app/api/cases.py diff --git a/backend/app/api/evaluations.py b/app/api/evaluations.py similarity index 100% rename from backend/app/api/evaluations.py rename to app/api/evaluations.py diff --git a/app/api/health.py b/app/api/health.py new file mode 100644 index 0000000..48f2a6e --- /dev/null +++ b/app/api/health.py @@ -0,0 +1,56 @@ +from fastapi import APIRouter +from fastapi.responses import JSONResponse +from sqlalchemy import text + +from app.core.config import settings +from app.core.response import ok +from app.db.session import SessionLocal + +router = APIRouter() + + +@router.get("/live") +def live(): + """存活检查:确认 FastAPI 进程已启动并可以响应请求。""" + return ok({"status": "live", "environment": settings.app_env}) + + +@router.get("/ready") +def ready(): + """就绪检查:验证生产配置、MySQL 和 Redis 是否支持核心业务运行。""" + checks: dict[str, object] = { + "configuration": {"ok": True, "errors": []}, + "mysql": {"ok": False}, + "redis": {"ok": False}, + } + + config_errors = settings.deployment_config_errors() + checks["configuration"] = {"ok": not config_errors, "errors": config_errors} + + try: + with SessionLocal() as db: + db.execute(text("SELECT 1")) + checks["mysql"] = {"ok": True} + except Exception: + checks["mysql"] = {"ok": False, "message": "database connection failed"} + + try: + import redis + + redis.Redis.from_url( + settings.redis_url, + decode_responses=True, + socket_connect_timeout=2, + socket_timeout=2, + ).ping() + checks["redis"] = {"ok": True} + except Exception: + checks["redis"] = {"ok": False, "message": "redis connection failed"} + + is_ready = all(bool(item.get("ok")) for item in checks.values() if isinstance(item, dict)) + payload = { + "code": "OK" if is_ready else "SERVICE_NOT_READY", + "message": "success" if is_ready else "service dependencies are not ready", + "data": {"status": "ready" if is_ready else "not_ready", "checks": checks}, + } + return JSONResponse(status_code=200 if is_ready else 503, content=payload) diff --git a/backend/app/api/imports.py b/app/api/imports.py similarity index 100% rename from backend/app/api/imports.py rename to app/api/imports.py diff --git a/backend/app/api/knowledge.py b/app/api/knowledge.py similarity index 100% rename from backend/app/api/knowledge.py rename to app/api/knowledge.py diff --git a/backend/app/api/llm_test.py b/app/api/llm_test.py similarity index 100% rename from backend/app/api/llm_test.py rename to app/api/llm_test.py diff --git a/backend/app/api/router.py b/app/api/router.py similarity index 100% rename from backend/app/api/router.py rename to app/api/router.py diff --git a/backend/app/api/sessions.py b/app/api/sessions.py similarity index 100% rename from backend/app/api/sessions.py rename to app/api/sessions.py diff --git a/backend/app/core/__init__.py b/app/core/__init__.py similarity index 100% rename from backend/app/core/__init__.py rename to app/core/__init__.py diff --git a/backend/app/core/config.py b/app/core/config.py similarity index 65% rename from backend/app/core/config.py rename to app/core/config.py index a5060e9..cb8d21e 100644 --- a/backend/app/core/config.py +++ b/app/core/config.py @@ -7,8 +7,11 @@ from pydantic import BaseModel, Field def _load_dotenv_file() -> None: """环境加载:轻量读取项目根目录 `.env`,避免强依赖 python-dotenv。""" - env_path = Path(__file__).resolve().parents[3] / ".env" - if not env_path.exists(): + env_path = next( + (parent / ".env" for parent in Path(__file__).resolve().parents if (parent / ".env").exists()), + None, + ) + if env_path is None: return for line in env_path.read_text(encoding="utf-8").splitlines(): if not line or line.strip().startswith("#") or "=" not in line: @@ -38,13 +41,36 @@ def _normalize_sync_database_url(url: str) -> str: return url +def _env_bool(key: str, default: bool) -> bool: + """布尔配置:统一解析环境变量中的 true/false 开关。""" + return os.getenv(key, str(default)).lower() == "true" + + +def _env_csv(key: str, default: str = "") -> list[str]: + """列表配置:把逗号分隔的环境变量转换为去空白列表。""" + return [item.strip() for item in os.getenv(key, default).split(",") if item.strip()] + + class Settings(BaseModel): """系统配置:集中管理数据库、DeepSeek、报告和短期 memory 配置。""" app_name: str = Field(default_factory=lambda: os.getenv("APP_NAME", "Medical Consultation Agent Demo")) app_env: str = Field(default_factory=lambda: os.getenv("APP_ENV", "local")) - app_debug: bool = Field(default_factory=lambda: os.getenv("APP_DEBUG", "true").lower() == "true") + app_debug: bool = Field(default_factory=lambda: _env_bool("APP_DEBUG", True)) + app_root_path: str = Field(default_factory=lambda: os.getenv("APP_ROOT_PATH", "")) api_v1_prefix: str = Field(default_factory=lambda: os.getenv("API_V1_PREFIX", "/api/v1")) + cors_allow_origins: list[str] = Field( + default_factory=lambda: _env_csv( + "CORS_ALLOW_ORIGINS", + "http://127.0.0.1:5173,http://localhost:5173,http://127.0.0.1:5174,http://localhost:5174", + ) + ) + cors_allow_origin_regex: str = Field( + default_factory=lambda: os.getenv( + "CORS_ALLOW_ORIGIN_REGEX", + r"^http://(127\.0\.0\.1|localhost|192\.168\.\d+\.\d+):\d+$", + ) + ) mysql_url: str = Field(default_factory=lambda: os.getenv("MYSQL_URL", "")) database_url: str = Field( @@ -68,26 +94,51 @@ class Settings(BaseModel): default_factory=lambda: int(os.getenv("LLM_STREAM_FIRST_TOKEN_TIMEOUT_SECONDS", "15")) ) llm_stream_total_timeout_seconds: int = Field(default_factory=lambda: int(os.getenv("LLM_STREAM_TOTAL_TIMEOUT_SECONDS", "45"))) - llm_stream_enabled: bool = Field(default_factory=lambda: os.getenv("LLM_STREAM_ENABLED", "true").lower() == "true") - llm_mock_enabled: bool = Field(default_factory=lambda: os.getenv("LLM_MOCK_ENABLED", "true").lower() == "true") - llm_fallback_to_mock: bool = Field(default_factory=lambda: os.getenv("LLM_FALLBACK_TO_MOCK", "true").lower() == "true") - llm_fast_thinking_enabled: bool = Field(default_factory=lambda: os.getenv("LLM_FAST_THINKING_ENABLED", "false").lower() == "true") - llm_reason_thinking_enabled: bool = Field(default_factory=lambda: os.getenv("LLM_REASON_THINKING_ENABLED", "false").lower() == "true") + llm_stream_enabled: bool = Field(default_factory=lambda: _env_bool("LLM_STREAM_ENABLED", True)) + llm_mock_enabled: bool = Field(default_factory=lambda: _env_bool("LLM_MOCK_ENABLED", True)) + llm_fallback_to_mock: bool = Field(default_factory=lambda: _env_bool("LLM_FALLBACK_TO_MOCK", True)) + llm_fast_thinking_enabled: bool = Field(default_factory=lambda: _env_bool("LLM_FAST_THINKING_ENABLED", False)) + llm_reason_thinking_enabled: bool = Field(default_factory=lambda: _env_bool("LLM_REASON_THINKING_ENABLED", False)) llm_reasoning_effort: str = Field(default_factory=lambda: os.getenv("LLM_REASONING_EFFORT", "low")) llm_fast_max_tokens: int = Field(default_factory=lambda: int(os.getenv("LLM_FAST_MAX_TOKENS", "512"))) llm_hint_max_tokens: int = Field(default_factory=lambda: int(os.getenv("LLM_HINT_MAX_TOKENS", "1200"))) - llm_scoring_json_response: bool = Field(default_factory=lambda: os.getenv("LLM_SCORING_JSON_RESPONSE", "true").lower() == "true") + llm_scoring_json_response: bool = Field(default_factory=lambda: _env_bool("LLM_SCORING_JSON_RESPONSE", True)) llm_scoring_max_tokens: int = Field(default_factory=lambda: int(os.getenv("LLM_SCORING_MAX_TOKENS", "4096"))) report_storage_dir: str = Field(default_factory=lambda: os.getenv("REPORT_STORAGE_DIR", "./storage/reports")) runtime_memory_ttl_seconds: int = Field(default_factory=lambda: int(os.getenv("RUNTIME_MEMORY_TTL_SECONDS", "7200"))) runtime_memory_backend: str = Field(default_factory=lambda: os.getenv("RUNTIME_MEMORY_BACKEND", "memory")) + runtime_memory_fallback_enabled: bool = Field( + default_factory=lambda: _env_bool("RUNTIME_MEMORY_FALLBACK_ENABLED", True) + ) redis_url: str = Field(default_factory=lambda: os.getenv("REDIS_URL", "redis://redis:6379/0")) - auth_validate_enabled: bool = Field(default_factory=lambda: os.getenv("AUTH_VALIDATE_ENABLED", "true").lower() == "true") + auth_validate_enabled: bool = Field(default_factory=lambda: _env_bool("AUTH_VALIDATE_ENABLED", True)) auth_user_me_url: str = Field(default_factory=lambda: os.getenv("AUTH_USER_ME_URL", "")) auth_timeout_seconds: int = Field(default_factory=lambda: int(os.getenv("AUTH_TIMEOUT_SECONDS", "5"))) auth_cache_ttl_seconds: int = Field(default_factory=lambda: int(os.getenv("AUTH_CACHE_TTL_SECONDS", "300"))) + @property + def is_production(self) -> bool: + """环境判断:标识当前是否运行在生产环境。""" + return self.app_env.lower() == "production" + + def deployment_config_errors(self) -> list[str]: + """部署检查:返回会导致生产核心链路不可用的配置问题。""" + errors: list[str] = [] + if self.database_url.startswith("sqlite"): + errors.append("DATABASE_URL must use MySQL") + if "CHANGE_ME" in self.database_url: + errors.append("DATABASE_URL still contains a placeholder") + if self.runtime_memory_backend.lower() != "redis": + errors.append("RUNTIME_MEMORY_BACKEND must be redis") + if not self.redis_url: + errors.append("REDIS_URL is required") + if not self.auth_user_me_url: + errors.append("AUTH_USER_ME_URL is required") + if self.llm_api_key in {"", "CHANGE_ME"} and not self.llm_mock_enabled: + errors.append("LLM_API_KEY is required when mock mode is disabled") + return errors + def as_public_dict(self) -> dict[str, Any]: """配置展示:返回允许暴露给 Demo 前端的功能开关。""" mock_enabled = self.llm_mock_enabled or not self.llm_api_key diff --git a/backend/app/core/context.py b/app/core/context.py similarity index 100% rename from backend/app/core/context.py rename to app/core/context.py diff --git a/backend/app/core/errors.py b/app/core/errors.py similarity index 100% rename from backend/app/core/errors.py rename to app/core/errors.py diff --git a/backend/app/core/exceptions.py b/app/core/exceptions.py similarity index 100% rename from backend/app/core/exceptions.py rename to app/core/exceptions.py diff --git a/backend/app/core/response.py b/app/core/response.py similarity index 100% rename from backend/app/core/response.py rename to app/core/response.py diff --git a/backend/app/core/user_context.py b/app/core/user_context.py similarity index 100% rename from backend/app/core/user_context.py rename to app/core/user_context.py diff --git a/backend/app/db/__init__.py b/app/db/__init__.py similarity index 100% rename from backend/app/db/__init__.py rename to app/db/__init__.py diff --git a/backend/app/db/base.py b/app/db/base.py similarity index 100% rename from backend/app/db/base.py rename to app/db/base.py diff --git a/backend/app/db/session.py b/app/db/session.py similarity index 76% rename from backend/app/db/session.py rename to app/db/session.py index db05de1..909d4f4 100644 --- a/backend/app/db/session.py +++ b/app/db/session.py @@ -7,12 +7,16 @@ from sqlalchemy.orm import Session, sessionmaker from app.core.config import settings +if settings.is_production and settings.database_url.startswith("sqlite"): + raise RuntimeError("Production deployment requires DATABASE_URL configured for MySQL") + + def _engine_kwargs() -> dict: """数据库连接:根据数据库类型设置 SQLAlchemy engine 参数。""" if settings.database_url.startswith("sqlite"): Path("storage").mkdir(exist_ok=True) return {"connect_args": {"check_same_thread": False}} - return {"pool_pre_ping": True, "pool_recycle": 3600} + return {"pool_pre_ping": True, "pool_recycle": 3600, "connect_args": {"connect_timeout": 5}} engine = create_engine(settings.database_url, **_engine_kwargs()) diff --git a/backend/app/main.py b/app/main.py similarity index 74% rename from backend/app/main.py rename to app/main.py index 43995e2..4a27bd6 100644 --- a/backend/app/main.py +++ b/app/main.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from app.api import health from app.api.router import api_router from app.core.config import settings from app.core.errors import register_exception_handlers @@ -12,24 +13,21 @@ def create_app() -> FastAPI: title=settings.app_name, debug=settings.app_debug, version="0.1.0", + root_path=settings.app_root_path, docs_url="/docs", redoc_url="/redoc", ) app.add_middleware( CORSMiddleware, - allow_origins=[ - "http://127.0.0.1:5173", - "http://localhost:5173", - "http://127.0.0.1:5174", - "http://localhost:5174", - ], - allow_origin_regex=r"^http://(127\.0\.0\.1|localhost|192\.168\.\d+\.\d+):\d+$", + allow_origins=settings.cors_allow_origins, + allow_origin_regex=settings.cors_allow_origin_regex or None, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) + app.include_router(health.router, prefix="/health", tags=["health"]) app.include_router(api_router, prefix=settings.api_v1_prefix) register_exception_handlers(app) return app diff --git a/backend/app/models/__init__.py b/app/models/__init__.py similarity index 100% rename from backend/app/models/__init__.py rename to app/models/__init__.py diff --git a/backend/app/models/audit.py b/app/models/audit.py similarity index 100% rename from backend/app/models/audit.py rename to app/models/audit.py diff --git a/backend/app/models/department.py b/app/models/department.py similarity index 100% rename from backend/app/models/department.py rename to app/models/department.py diff --git a/backend/app/models/knowledge.py b/app/models/knowledge.py similarity index 100% rename from backend/app/models/knowledge.py rename to app/models/knowledge.py diff --git a/backend/app/models/mixins.py b/app/models/mixins.py similarity index 100% rename from backend/app/models/mixins.py rename to app/models/mixins.py diff --git a/backend/app/models/prompt.py b/app/models/prompt.py similarity index 100% rename from backend/app/models/prompt.py rename to app/models/prompt.py diff --git a/backend/app/models/source_case.py b/app/models/source_case.py similarity index 100% rename from backend/app/models/source_case.py rename to app/models/source_case.py diff --git a/backend/app/models/training.py b/app/models/training.py similarity index 100% rename from backend/app/models/training.py rename to app/models/training.py diff --git a/backend/app/models/training_record.py b/app/models/training_record.py similarity index 100% rename from backend/app/models/training_record.py rename to app/models/training_record.py diff --git a/backend/app/models/user.py b/app/models/user.py similarity index 100% rename from backend/app/models/user.py rename to app/models/user.py diff --git a/backend/app/prompts/hint/novice_case_hint.md b/app/prompts/hint/novice_case_hint.md similarity index 100% rename from backend/app/prompts/hint/novice_case_hint.md rename to app/prompts/hint/novice_case_hint.md diff --git a/backend/app/prompts/hint/novice_hint.md b/app/prompts/hint/novice_hint.md similarity index 100% rename from backend/app/prompts/hint/novice_hint.md rename to app/prompts/hint/novice_hint.md diff --git a/backend/app/prompts/knowledge/guideline_search_query.md b/app/prompts/knowledge/guideline_search_query.md similarity index 100% rename from backend/app/prompts/knowledge/guideline_search_query.md rename to app/prompts/knowledge/guideline_search_query.md diff --git a/backend/app/prompts/patient/free_chat.md b/app/prompts/patient/free_chat.md similarity index 100% rename from backend/app/prompts/patient/free_chat.md rename to app/prompts/patient/free_chat.md diff --git a/backend/app/prompts/patient/novice.md b/app/prompts/patient/novice.md similarity index 100% rename from backend/app/prompts/patient/novice.md rename to app/prompts/patient/novice.md diff --git a/backend/app/prompts/patient/practice.md b/app/prompts/patient/practice.md similarity index 100% rename from backend/app/prompts/patient/practice.md rename to app/prompts/patient/practice.md diff --git a/backend/app/prompts/patient/teaching.md b/app/prompts/patient/teaching.md similarity index 100% rename from backend/app/prompts/patient/teaching.md rename to app/prompts/patient/teaching.md diff --git a/backend/app/prompts/polish/doctor_question_polish.md b/app/prompts/polish/doctor_question_polish.md similarity index 100% rename from backend/app/prompts/polish/doctor_question_polish.md rename to app/prompts/polish/doctor_question_polish.md diff --git a/backend/app/prompts/report/evaluation_report.md b/app/prompts/report/evaluation_report.md similarity index 100% rename from backend/app/prompts/report/evaluation_report.md rename to app/prompts/report/evaluation_report.md diff --git a/backend/app/prompts/scoring/default_five_point.md b/app/prompts/scoring/default_five_point.md similarity index 100% rename from backend/app/prompts/scoring/default_five_point.md rename to app/prompts/scoring/default_five_point.md diff --git a/backend/app/prompts/scoring/default_percentage.md b/app/prompts/scoring/default_percentage.md similarity index 100% rename from backend/app/prompts/scoring/default_percentage.md rename to app/prompts/scoring/default_percentage.md diff --git a/backend/app/prompts/scoring/pediatrics_pneumonia.md b/app/prompts/scoring/pediatrics_pneumonia.md similarity index 100% rename from backend/app/prompts/scoring/pediatrics_pneumonia.md rename to app/prompts/scoring/pediatrics_pneumonia.md diff --git a/backend/app/repositories/__init__.py b/app/repositories/__init__.py similarity index 100% rename from backend/app/repositories/__init__.py rename to app/repositories/__init__.py diff --git a/backend/app/repositories/audit_repository.py b/app/repositories/audit_repository.py similarity index 100% rename from backend/app/repositories/audit_repository.py rename to app/repositories/audit_repository.py diff --git a/backend/app/repositories/case_repository.py b/app/repositories/case_repository.py similarity index 100% rename from backend/app/repositories/case_repository.py rename to app/repositories/case_repository.py diff --git a/backend/app/repositories/evaluation_repository.py b/app/repositories/evaluation_repository.py similarity index 100% rename from backend/app/repositories/evaluation_repository.py rename to app/repositories/evaluation_repository.py diff --git a/backend/app/repositories/knowledge_repository.py b/app/repositories/knowledge_repository.py similarity index 100% rename from backend/app/repositories/knowledge_repository.py rename to app/repositories/knowledge_repository.py diff --git a/backend/app/repositories/session_repository.py b/app/repositories/session_repository.py similarity index 100% rename from backend/app/repositories/session_repository.py rename to app/repositories/session_repository.py diff --git a/backend/app/repositories/source_case_repository.py b/app/repositories/source_case_repository.py similarity index 100% rename from backend/app/repositories/source_case_repository.py rename to app/repositories/source_case_repository.py diff --git a/backend/app/repositories/training_record_repository.py b/app/repositories/training_record_repository.py similarity index 100% rename from backend/app/repositories/training_record_repository.py rename to app/repositories/training_record_repository.py diff --git a/backend/app/schemas/__init__.py b/app/schemas/__init__.py similarity index 100% rename from backend/app/schemas/__init__.py rename to app/schemas/__init__.py diff --git a/backend/app/schemas/agent.py b/app/schemas/agent.py similarity index 100% rename from backend/app/schemas/agent.py rename to app/schemas/agent.py diff --git a/backend/app/schemas/auth.py b/app/schemas/auth.py similarity index 100% rename from backend/app/schemas/auth.py rename to app/schemas/auth.py diff --git a/backend/app/schemas/case.py b/app/schemas/case.py similarity index 100% rename from backend/app/schemas/case.py rename to app/schemas/case.py diff --git a/backend/app/schemas/evaluation.py b/app/schemas/evaluation.py similarity index 100% rename from backend/app/schemas/evaluation.py rename to app/schemas/evaluation.py diff --git a/backend/app/schemas/imports.py b/app/schemas/imports.py similarity index 100% rename from backend/app/schemas/imports.py rename to app/schemas/imports.py diff --git a/backend/app/schemas/knowledge.py b/app/schemas/knowledge.py similarity index 100% rename from backend/app/schemas/knowledge.py rename to app/schemas/knowledge.py diff --git a/backend/app/schemas/llm.py b/app/schemas/llm.py similarity index 100% rename from backend/app/schemas/llm.py rename to app/schemas/llm.py diff --git a/backend/app/schemas/session.py b/app/schemas/session.py similarity index 100% rename from backend/app/schemas/session.py rename to app/schemas/session.py diff --git a/backend/app/services/__init__.py b/app/services/__init__.py similarity index 100% rename from backend/app/services/__init__.py rename to app/services/__init__.py diff --git a/backend/app/services/audit_service.py b/app/services/audit_service.py similarity index 100% rename from backend/app/services/audit_service.py rename to app/services/audit_service.py diff --git a/backend/app/services/case_service.py b/app/services/case_service.py similarity index 100% rename from backend/app/services/case_service.py rename to app/services/case_service.py diff --git a/backend/app/services/case_sql_import_service.py b/app/services/case_sql_import_service.py similarity index 100% rename from backend/app/services/case_sql_import_service.py rename to app/services/case_sql_import_service.py diff --git a/backend/app/services/evaluation_service.py b/app/services/evaluation_service.py similarity index 100% rename from backend/app/services/evaluation_service.py rename to app/services/evaluation_service.py diff --git a/backend/app/services/external_auth_service.py b/app/services/external_auth_service.py similarity index 100% rename from backend/app/services/external_auth_service.py rename to app/services/external_auth_service.py diff --git a/backend/app/services/knowledge_service.py b/app/services/knowledge_service.py similarity index 100% rename from backend/app/services/knowledge_service.py rename to app/services/knowledge_service.py diff --git a/backend/app/services/order_service.py b/app/services/order_service.py similarity index 100% rename from backend/app/services/order_service.py rename to app/services/order_service.py diff --git a/backend/app/services/pdf_export_service.py b/app/services/pdf_export_service.py similarity index 100% rename from backend/app/services/pdf_export_service.py rename to app/services/pdf_export_service.py diff --git a/backend/app/services/runtime_memory.py b/app/services/runtime_memory.py similarity index 96% rename from backend/app/services/runtime_memory.py rename to app/services/runtime_memory.py index 72a7d72..64aa791 100644 --- a/backend/app/services/runtime_memory.py +++ b/app/services/runtime_memory.py @@ -125,6 +125,8 @@ def create_runtime_memory_service() -> BaseRuntimeMemoryService: service.client.ping() return service except Exception: + if settings.is_production or not settings.runtime_memory_fallback_enabled: + raise RuntimeError("Redis runtime memory is required but unavailable") return InMemoryRuntimeMemoryService() return InMemoryRuntimeMemoryService() diff --git a/backend/app/services/session_service.py b/app/services/session_service.py similarity index 100% rename from backend/app/services/session_service.py rename to app/services/session_service.py diff --git a/backend/README.md b/backend/README.md deleted file mode 100644 index 3da9d12..0000000 --- a/backend/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Backend - -医疗问诊 Agent FastAPI 后端工程。 - -## 启动 - -```powershell -python -m venv .venv -.\.venv\Scripts\activate -pip install -r requirements.txt -cd .. -copy .env.example .env -cd backend -uvicorn app.main:app --host 127.0.0.1 --port 9000 -``` - -Swagger: - -```text -http://127.0.0.1:9000/docs -``` - -## 配置 - -后端读取项目根目录 `.env`。核心配置: - -```env -DATABASE_URL=mysql+pymysql://root:@mysql:3306/medical_platform?charset=utf8mb4 -MYSQL_URL=mysql+aiomysql://root:@mysql:3306/medical_platform?charset=utf8mb4 -REDIS_URL=redis://redis:6379/0 -AUTH_USER_ME_URL=http://django:8000/api/user/users/me/ -LLM_API_KEY= -``` - -真实密码、API Key 和 access token 只写入部署环境或本地 `.env`。 - -## 核心约束 - -- 用户身份只来自 `Authorization: Bearer `。 -- 后端转发 token 到 Django 用户中心 `/api/user/users/me/`。 -- Django 返回的 `id` 是本系统内部用户隔离字段。 -- 问诊消息进入短期 memory,不作为长期历史保存。 -- 检查检验结果只从数据库读取。 -- 完整训练结束后保存 `training_record` 和 `training_score_detail`。 -- LLM 调用统一经过 `app/agents/llm_adapter.py`。 diff --git a/backend/pyproject.toml b/pyproject.toml similarity index 100% rename from backend/pyproject.toml rename to pyproject.toml diff --git a/backend/requirements.txt b/requirements.txt similarity index 100% rename from backend/requirements.txt rename to requirements.txt diff --git a/backend/scripts/__init__.py b/scripts/__init__.py similarity index 100% rename from backend/scripts/__init__.py rename to scripts/__init__.py diff --git a/backend/scripts/check_final_demo_readiness.py b/scripts/check_final_demo_readiness.py similarity index 100% rename from backend/scripts/check_final_demo_readiness.py rename to scripts/check_final_demo_readiness.py diff --git a/backend/scripts/check_final_schema.py b/scripts/check_final_schema.py similarity index 100% rename from backend/scripts/check_final_schema.py rename to scripts/check_final_schema.py diff --git a/backend/scripts/clear_training_runtime_data.py b/scripts/clear_training_runtime_data.py similarity index 100% rename from backend/scripts/clear_training_runtime_data.py rename to scripts/clear_training_runtime_data.py diff --git a/backend/scripts/debug_patient_stream.py b/scripts/debug_patient_stream.py similarity index 100% rename from backend/scripts/debug_patient_stream.py rename to scripts/debug_patient_stream.py diff --git a/backend/scripts/drop_legacy_tables.py b/scripts/drop_legacy_tables.py similarity index 100% rename from backend/scripts/drop_legacy_tables.py rename to scripts/drop_legacy_tables.py diff --git a/backend/scripts/import_source_case_sql.py b/scripts/import_source_case_sql.py similarity index 100% rename from backend/scripts/import_source_case_sql.py rename to scripts/import_source_case_sql.py diff --git a/backend/scripts/init_demo_db.py b/scripts/init_demo_db.py similarity index 100% rename from backend/scripts/init_demo_db.py rename to scripts/init_demo_db.py diff --git a/backend/scripts/migrate_to_new_schema.py b/scripts/migrate_to_new_schema.py similarity index 100% rename from backend/scripts/migrate_to_new_schema.py rename to scripts/migrate_to_new_schema.py diff --git a/backend/scripts/migrate_user_department_score_detail.py b/scripts/migrate_user_department_score_detail.py similarity index 100% rename from backend/scripts/migrate_user_department_score_detail.py rename to scripts/migrate_user_department_score_detail.py diff --git a/backend/tests/test_api_contract.py b/tests/test_api_contract.py similarity index 98% rename from backend/tests/test_api_contract.py rename to tests/test_api_contract.py index ba5c443..64a62e8 100644 --- a/backend/tests/test_api_contract.py +++ b/tests/test_api_contract.py @@ -62,6 +62,10 @@ def run_api_contract_tests() -> None: client = TestClient(app) headers = {"Authorization": "Bearer api_user_001_token", "X-Entry-Scene": "api_test"} + live = client.get("/health/live") + assert live.status_code == 200 + assert live.json()["data"]["status"] == "live" + missing_user = client.get("/api/v1/agent/hello") assert missing_user.status_code == 401 assert missing_user.json()["code"] == "AUTH_CREDENTIAL_REQUIRED" diff --git a/backend/tests/test_core_logic.py b/tests/test_core_logic.py similarity index 100% rename from backend/tests/test_core_logic.py rename to tests/test_core_logic.py diff --git a/backend/tests/test_demo_flow.py b/tests/test_demo_flow.py similarity index 100% rename from backend/tests/test_demo_flow.py rename to tests/test_demo_flow.py diff --git a/backend/tests/test_import_source_case_sql.py b/tests/test_import_source_case_sql.py similarity index 100% rename from backend/tests/test_import_source_case_sql.py rename to tests/test_import_source_case_sql.py