"""Routes for async RAGAS scoring jobs (Dify fire-and-forget integration). Dify calls POST /api/score/async → gets job_id immediately (202). Scoring runs in background, result written as a standard run artifact. View full report at GET /api/runs/{run_id} or in the 「运行列表」 page. """ from __future__ import annotations import logging from fastapi import APIRouter, HTTPException from webapp.models import AsyncScoreJobResponse, AsyncScoreJobStatus, ScoreRequest from webapp.services.score_job_manager import score_job_manager router = APIRouter(prefix="/api/score", tags=["score"]) logger = logging.getLogger("webapp.api.score_jobs") @router.post( "/async", status_code=202, response_model=AsyncScoreJobResponse, summary="提交异步评分任务(Dify 推荐方式)", responses={ 202: { "description": ( "任务已排队,立即返回 job_id(202 Accepted)。\n\n" "评分在后台执行,完成后自动生成完整报告(含优化建议)。\n" "通过 `GET /api/score/jobs/{job_id}` 查询状态," "完成后在「运行列表」页查看完整报告。" ), "content": { "application/json": { "example": {"job_id": "abc123def456", "status": "queued", "run_id": None} } }, }, }, ) def submit_async_score(request: ScoreRequest) -> AsyncScoreJobResponse: """提交异步 RAGAS 评分任务,立即返回 job_id。 **适合 Dify 工作流**:HTTP 节点无需等待评分完成(无超时风险), 工作流立即继续,评分结果在 RAGAS 平台「运行列表」中查看。 评分完成后自动生成: - 各指标得分(`scores.csv`) - 摘要报告(`summary.md`) - LLM 优化建议(`optimization_advice.md`) """ logger.info( "[score_async] submit metrics=%s has_ctx=%s has_gt=%s", request.metrics, bool(request.contexts), bool(request.ground_truth), ) status = score_job_manager.submit(request) logger.info("[score_async] queued job_id=%s", status.job_id) return AsyncScoreJobResponse(job_id=status.job_id, status=status.status) @router.get( "/jobs", response_model=dict, summary="列出所有异步评分记录", ) def list_score_jobs() -> dict: """返回所有异步评分记录,按创建时间倒序排列。""" jobs = score_job_manager.list_jobs() logger.info("[score_jobs] list count=%d", len(jobs)) return {"jobs": [j.model_dump() for j in jobs]} @router.get( "/jobs/{job_id}", response_model=AsyncScoreJobStatus, summary="查询单个异步评分任务状态", responses={404: {"description": "指定 job_id 的评分任务不存在。"}}, ) def get_score_job(job_id: str) -> AsyncScoreJobStatus: """查询单个异步评分任务的状态和结果。 `status` 为 `completed` 时,`run_id` 字段包含对应的运行 ID, 可通过 `GET /api/runs/{run_id}` 获取完整评分报告。 """ status = score_job_manager.get(job_id) if status is None: raise HTTPException(status_code=404, detail=f"Score job not found: {job_id}") return status