Each async score job:
- Runs InlineScorer.score() in thread pool
- Writes standard run artifacts (metadata.json, scores.csv, summary.md)
- Runs optimization_advisor => optimization_advice.md
- Result appears in 运行列表 and 报告详情 with full report
New endpoints:
- POST /api/score/async (202, job_id immediate)
- GET /api/score/jobs (list all jobs)
- GET /api/score/jobs/{id} (single job status)
Frontend:
- 评分记录 nav page with card list
- 5s auto-polling for queued/running jobs
- 查看报告 button navigates to existing 报告详情 page
Dify: change /api/score -> /api/score/async, no response parsing needed
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
90 lines
3.2 KiB
Python
90 lines
3.2 KiB
Python
"""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
|