2026-06-15 15:53:57 +08:00
|
|
|
|
"""FastAPI application factory for the RAGAS evaluation console.
|
|
|
|
|
|
|
|
|
|
|
|
The app mounts three JSON API routers and serves the single-page static
|
|
|
|
|
|
frontend. It imports rag_eval only lazily (inside the task manager worker), so
|
|
|
|
|
|
the server starts even when the evaluation dependencies are not yet installed.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-06-22 18:14:01 +08:00
|
|
|
|
import logging
|
feat: add detailed logging to all API routes and global access log middleware
Each API module now logs:
- evaluations: trigger (scenario path, task_id), status polls, list
- runs: list (count), detail (run_id, metrics, sample counts)
- scenarios: list (total, valid, error counts)
- pipeline: submit (docs_path, models, max_docs), status polls, list
- llm_profiles: CRUD ops (name, model, id), probe/test (model, ok, latency), apply (patched fields)
- score: already had per-request logging
Global middleware (webapp.access logger):
- Every API request: METHOD path -> status (latency_ms) at INFO
- Static file requests demoted to DEBUG to reduce noise
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 10:35:00 +08:00
|
|
|
|
import time
|
2026-06-15 15:53:57 +08:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
2026-06-22 18:14:01 +08:00
|
|
|
|
from fastapi import FastAPI, Request
|
|
|
|
|
|
from fastapi.encoders import jsonable_encoder
|
|
|
|
|
|
from fastapi.exceptions import RequestValidationError
|
|
|
|
|
|
from fastapi.responses import FileResponse, JSONResponse
|
2026-06-15 15:53:57 +08:00
|
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
|
|
|
2026-06-22 15:14:19 +08:00
|
|
|
|
from webapp.api import evaluations, llm_profiles, pipeline, runs, scenarios, score
|
2026-06-15 15:53:57 +08:00
|
|
|
|
|
|
|
|
|
|
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
2026-06-22 18:14:01 +08:00
|
|
|
|
logger = logging.getLogger("webapp.server")
|
feat: add detailed logging to all API routes and global access log middleware
Each API module now logs:
- evaluations: trigger (scenario path, task_id), status polls, list
- runs: list (count), detail (run_id, metrics, sample counts)
- scenarios: list (total, valid, error counts)
- pipeline: submit (docs_path, models, max_docs), status polls, list
- llm_profiles: CRUD ops (name, model, id), probe/test (model, ok, latency), apply (patched fields)
- score: already had per-request logging
Global middleware (webapp.access logger):
- Every API request: METHOD path -> status (latency_ms) at INFO
- Static file requests demoted to DEBUG to reduce noise
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 10:35:00 +08:00
|
|
|
|
access_logger = logging.getLogger("webapp.access")
|
2026-06-15 15:53:57 +08:00
|
|
|
|
|
2026-06-22 15:14:19 +08:00
|
|
|
|
# OpenAPI tag metadata — controls the grouping and descriptions in /docs.
|
|
|
|
|
|
OPENAPI_TAGS = [
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "pipeline",
|
|
|
|
|
|
"description": (
|
|
|
|
|
|
"**全链路评估 Pipeline API**\n\n"
|
|
|
|
|
|
"一次调用完成「解析文档 → 生成题库 → RAGAS 评估 → 输出报告」全流程。\n\n"
|
|
|
|
|
|
"**使用流程**\n"
|
|
|
|
|
|
"1. `POST /api/pipeline/jobs` 提交任务,立即拿到 `job_id`。\n"
|
|
|
|
|
|
"2. `GET /api/pipeline/jobs/{job_id}` 轮询 `status` / `phase` / `logs`。\n"
|
|
|
|
|
|
"3. 当 `status=completed` 时,`result` 字段包含所有产物路径。\n\n"
|
|
|
|
|
|
"**Pipeline 阶段**\n"
|
|
|
|
|
|
"| phase | 说明 |\n"
|
|
|
|
|
|
"|-------|------|\n"
|
|
|
|
|
|
"| `parsing_documents` | 调用阿里云 DocMind 解析每份 PDF |\n"
|
|
|
|
|
|
"| `generating_questions` | LLM 从文档片段生成草稿题库 |\n"
|
|
|
|
|
|
"| `evaluating` | RAGAS 在线评测打分 |\n"
|
|
|
|
|
|
"| `done` | 所有产物写入磁盘,任务完成 |"
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "evaluations",
|
|
|
|
|
|
"description": (
|
|
|
|
|
|
"**单场景评估 API**\n\n"
|
|
|
|
|
|
"基于已有 YAML 场景文件触发评估任务,并查询任务状态与日志。"
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "llm-profiles",
|
|
|
|
|
|
"description": (
|
|
|
|
|
|
"**LLM 配置管理 API**\n\n"
|
|
|
|
|
|
"增删改查已保存的 LLM 连接配置(模型名称、Base URL、API Key);"
|
|
|
|
|
|
"支持连通性测试;可将配置一键写入场景 YAML 文件。"
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "runs",
|
|
|
|
|
|
"description": "**评估运行列表 API**\n\n查询历史评估运行记录及详细报告数据。",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "scenarios",
|
|
|
|
|
|
"description": "**场景文件 API**\n\n扫描并列出 `scenarios/` 目录下所有可用的 YAML 场景文件。",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "score",
|
|
|
|
|
|
"description": (
|
|
|
|
|
|
"**实时评分 API(Dify 外部 Tool)**\n\n"
|
|
|
|
|
|
"接受单条问答记录 `(question, answer, contexts, ground_truth)`,\n"
|
|
|
|
|
|
"同步运行 RAGAS 指标打分,返回各指标得分和加权综合得分。\n\n"
|
|
|
|
|
|
"适用场景:Dify Agent 在回答后即时调用,用于质量监控或自我改进。\n\n"
|
|
|
|
|
|
"**鉴权**:若 `.env` 中配置了 `SCORE_API_TOKEN`,需携带 "
|
|
|
|
|
|
"`Authorization: Bearer <token>` 请求头。"
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
"name": "meta",
|
|
|
|
|
|
"description": "**系统 API**\n\n健康检查等基础接口。",
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
2026-06-15 15:53:57 +08:00
|
|
|
|
|
|
|
|
|
|
def create_app() -> FastAPI:
|
|
|
|
|
|
"""Build and configure the FastAPI application instance."""
|
|
|
|
|
|
app = FastAPI(
|
2026-06-22 15:14:19 +08:00
|
|
|
|
title="RAGAS 评估系统",
|
|
|
|
|
|
description=(
|
|
|
|
|
|
"西门子医疗影像 RAG 评估平台 API 文档。\n\n"
|
|
|
|
|
|
"提供以下能力:\n"
|
|
|
|
|
|
"- **Pipeline API** — 一键完成「解析文档 → 生成题库 → RAGAS 评估」全链路\n"
|
|
|
|
|
|
"- **实时评分 API** — 供 Dify 外部 Tool 调用的单题 RAGAS 评分接口\n"
|
|
|
|
|
|
"- **评估 API** — 基于 YAML 场景文件触发单次评估\n"
|
|
|
|
|
|
"- **LLM 配置 API** — 管理多个 LLM 连接配置,支持连通性测试\n"
|
|
|
|
|
|
"- **报告 API** — 查询历史运行记录与评估报告\n\n"
|
|
|
|
|
|
"> **快速开始**:调用 `POST /api/pipeline/jobs` 传入 PDF 文件夹路径即可启动完整评估流程。"
|
|
|
|
|
|
),
|
2026-06-22 15:52:30 +08:00
|
|
|
|
version="0.3.0",
|
2026-06-22 15:14:19 +08:00
|
|
|
|
openapi_tags=OPENAPI_TAGS,
|
2026-06-15 15:53:57 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
app.include_router(runs.router)
|
|
|
|
|
|
app.include_router(scenarios.router)
|
|
|
|
|
|
app.include_router(evaluations.router)
|
2026-06-16 16:18:40 +08:00
|
|
|
|
app.include_router(llm_profiles.router)
|
2026-06-22 15:14:19 +08:00
|
|
|
|
app.include_router(pipeline.router)
|
|
|
|
|
|
app.include_router(score.router)
|
2026-06-15 15:53:57 +08:00
|
|
|
|
|
feat: add detailed logging to all API routes and global access log middleware
Each API module now logs:
- evaluations: trigger (scenario path, task_id), status polls, list
- runs: list (count), detail (run_id, metrics, sample counts)
- scenarios: list (total, valid, error counts)
- pipeline: submit (docs_path, models, max_docs), status polls, list
- llm_profiles: CRUD ops (name, model, id), probe/test (model, ok, latency), apply (patched fields)
- score: already had per-request logging
Global middleware (webapp.access logger):
- Every API request: METHOD path -> status (latency_ms) at INFO
- Static file requests demoted to DEBUG to reduce noise
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-23 10:35:00 +08:00
|
|
|
|
@app.middleware("http")
|
|
|
|
|
|
async def access_log_middleware(request: Request, call_next):
|
|
|
|
|
|
"""Log every API request with method, path, status code and latency.
|
|
|
|
|
|
|
|
|
|
|
|
Static file requests are logged at DEBUG level to keep the console clean.
|
|
|
|
|
|
"""
|
|
|
|
|
|
t0 = time.monotonic()
|
|
|
|
|
|
response = await call_next(request)
|
|
|
|
|
|
latency_ms = int((time.monotonic() - t0) * 1000)
|
|
|
|
|
|
path = request.url.path
|
|
|
|
|
|
is_static = path.startswith("/static/") or path in ("/", "/favicon.ico")
|
|
|
|
|
|
msg = "%s %s → %d (%dms)", request.method, path, response.status_code, latency_ms
|
|
|
|
|
|
if is_static:
|
|
|
|
|
|
access_logger.debug(*msg)
|
|
|
|
|
|
else:
|
|
|
|
|
|
access_logger.info(*msg)
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
2026-06-22 18:14:01 +08:00
|
|
|
|
@app.exception_handler(RequestValidationError)
|
|
|
|
|
|
async def validation_exception_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
|
|
|
|
|
|
"""Log full validation error detail to help diagnose 422 responses."""
|
|
|
|
|
|
errors = jsonable_encoder(exc.errors())
|
|
|
|
|
|
logger.warning(
|
|
|
|
|
|
"[422] validation error url=%s content_type=%s errors=%s",
|
|
|
|
|
|
request.url.path,
|
|
|
|
|
|
request.headers.get("content-type", ""),
|
|
|
|
|
|
errors,
|
|
|
|
|
|
)
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
status_code=422,
|
|
|
|
|
|
content={"detail": errors},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-15 15:53:57 +08:00
|
|
|
|
@app.get("/api/health", tags=["meta"])
|
|
|
|
|
|
def health() -> dict[str, str]:
|
|
|
|
|
|
"""Report basic liveness so the UI can confirm the server is reachable."""
|
|
|
|
|
|
return {"status": "ok"}
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/", include_in_schema=False)
|
|
|
|
|
|
def index() -> FileResponse:
|
|
|
|
|
|
"""Serve the single-page console entry document."""
|
|
|
|
|
|
return FileResponse(STATIC_DIR / "index.html")
|
|
|
|
|
|
|
|
|
|
|
|
# Serve CSS/JS assets under /static while keeping API routes at /api.
|
|
|
|
|
|
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
|
|
|
|
|
|
|
|
|
|
|
return app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = create_app()
|