prepare fastapi root layout for server deployment
This commit is contained in:
+5
-3
@@ -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
|
||||
|
||||
+15
-15
@@ -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:<password>@mysql:3306/medical_platform?charset=utf8mb4
|
||||
DATABASE_URL=mysql+pymysql://root:<password>@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
|
||||
|
||||
@@ -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
|
||||
+4
-1
@@ -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
|
||||
|
||||
+11
-12
@@ -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=*"]
|
||||
|
||||
@@ -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:<password>@mysql:3306/medical_platform?charset=utf8mb4
|
||||
MYSQL_URL=mysql+aiomysql://root:<password>@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 <access_token>
|
||||
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 <access_token>" \
|
||||
-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
|
||||
```
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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:<password>@mysql:3306/medical_platform?charset=utf8mb4
|
||||
MYSQL_URL=mysql+aiomysql://root:<password>@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 <access_token>`。
|
||||
- 后端转发 token 到 Django 用户中心 `/api/user/users/me/`。
|
||||
- Django 返回的 `id` 是本系统内部用户隔离字段。
|
||||
- 问诊消息进入短期 memory,不作为长期历史保存。
|
||||
- 检查检验结果只从数据库读取。
|
||||
- 完整训练结束后保存 `training_record` 和 `training_score_detail`。
|
||||
- LLM 调用统一经过 `app/agents/llm_adapter.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"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user