feat: 增加 Agent Excel 批量测试

This commit is contained in:
2026-08-12 17:00:50 +08:00
parent c4231f268b
commit 3faecab6ac
26 changed files with 1327 additions and 6 deletions

View File

@@ -0,0 +1,157 @@
from __future__ import annotations
import json
from datetime import UTC, datetime
from typing import Annotated
from urllib.parse import quote
from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile, status
from fastapi.responses import StreamingResponse
from sqlalchemy import func, select, update
from sqlalchemy.orm import Session
from app.api.pagination import page_result
from app.core.database import get_db
from app.core.dependencies import get_current_admin
from app.core.responses import api_success
from app.models.admin import Admin
from app.models.agent_batch import AgentBatchTest, AgentBatchTestItem
from app.services.admin_service import OperationLogService
from app.services.agent_batch_test_service import AgentBatchTestService, item_dict, job_dict
router = APIRouter()
@router.get("/agent/batch/template")
def download_batch_template(
current_admin: Admin = Depends(get_current_admin),
) -> StreamingResponse:
return StreamingResponse(
AgentBatchTestService.template_workbook(),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": "attachment; filename*=UTF-8''Agent_batch_test_template.xlsx"},
)
@router.post("/agent/batch/jobs")
def create_batch_job(
file: Annotated[UploadFile, File(...)],
configJson: Annotated[str, Form(...)],
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
try:
config = json.loads(configJson)
except json.JSONDecodeError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="批量测试配置格式不正确") from exc
if not isinstance(config, dict):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="批量测试配置格式不正确")
rows = AgentBatchTestService.parse_questions(file)
job = AgentBatchTestService.create_job(
db,
admin=current_admin,
filename=file.filename or "Agent批量测试.xlsx",
rows=rows,
config=config,
)
OperationLogService.write(db, admin_id=current_admin.id, module="agent", action="batch_create", target_id=job.id)
db.commit()
return api_success(job_dict(job))
@router.get("/agent/batch/jobs")
def list_batch_jobs(
page: int = Query(default=1, ge=1),
pageSize: int = Query(default=10, ge=10, le=100),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
total = db.scalar(select(func.count(AgentBatchTest.id))) or 0
rows = list(
db.scalars(
select(AgentBatchTest)
.order_by(AgentBatchTest.created_at.desc(), AgentBatchTest.id.desc())
.offset((page - 1) * pageSize)
.limit(pageSize)
)
)
return api_success(page_result([job_dict(row) for row in rows], total=total, page=page, page_size=pageSize))
@router.get("/agent/batch/jobs/{job_id}")
def get_batch_job(
job_id: int,
page: int = Query(default=1, ge=1),
pageSize: int = Query(default=20, ge=10, le=100),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
job = _job_or_404(db, job_id)
total = db.scalar(select(func.count(AgentBatchTestItem.id)).where(AgentBatchTestItem.batch_id == job.id)) or 0
items = list(
db.scalars(
select(AgentBatchTestItem)
.where(AgentBatchTestItem.batch_id == job.id)
.order_by(AgentBatchTestItem.row_number.asc(), AgentBatchTestItem.id.asc())
.offset((page - 1) * pageSize)
.limit(pageSize)
)
)
return api_success({**job_dict(job), "items": page_result([item_dict(item) for item in items], total=total, page=page, page_size=pageSize)})
@router.get("/agent/batch/jobs/{job_id}/export")
def export_batch_job(
job_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> StreamingResponse:
job = _job_or_404(db, job_id)
items = list(
db.scalars(
select(AgentBatchTestItem)
.where(AgentBatchTestItem.batch_id == job.id)
.order_by(AgentBatchTestItem.row_number.asc(), AgentBatchTestItem.id.asc())
)
)
filename = quote(f"Agent批量测试结果_{job.id}.xlsx")
OperationLogService.write(db, admin_id=current_admin.id, module="agent", action="batch_export", target_id=job.id)
db.commit()
return StreamingResponse(
AgentBatchTestService.export_workbook(job, items),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename*=UTF-8''{filename}"},
)
@router.post("/agent/batch/jobs/{job_id}/cancel")
def cancel_batch_job(
job_id: int,
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
job = _job_or_404(db, job_id)
if job.status not in {"pending", "running"}:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="当前任务已结束,不能取消")
now = datetime.now(UTC).replace(tzinfo=None)
job.status = "cancelled"
job.cancelled_at = now
job.finished_at = now
db.add(job)
db.execute(
update(AgentBatchTestItem)
.where(AgentBatchTestItem.batch_id == job.id, AgentBatchTestItem.status == "pending")
.values(status="cancelled", finished_at=now, next_run_at=None)
)
OperationLogService.write(db, admin_id=current_admin.id, module="agent", action="batch_cancel", target_id=job.id)
db.commit()
db.refresh(job)
return api_success(job_dict(job))
def _job_or_404(db: Session, job_id: int) -> AgentBatchTest:
job = db.get(AgentBatchTest, job_id)
if job is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="批量测试任务不存在")
return job

View File

@@ -7,6 +7,7 @@ from app.api import (
admin_auth,
admin_content_generation,
admin_agent_records,
admin_agent_batch,
admin_dashboard,
admin_entitlements,
admin_knowledge,
@@ -38,6 +39,7 @@ api_router.include_router(admin_management.router, prefix="/admin", tags=["admin
guard = [Depends(enforce_admin_access)]
api_router.include_router(admin_content_generation.router, prefix="/admin", tags=["admin-content-generation"], dependencies=guard)
api_router.include_router(admin_agent_records.router, prefix="/admin", tags=["admin-agent-records"], dependencies=guard)
api_router.include_router(admin_agent_batch.router, prefix="/admin", tags=["admin-agent-batch"], dependencies=guard)
api_router.include_router(admin_dashboard.router, prefix="/admin", tags=["admin"], dependencies=guard)
api_router.include_router(admin_entitlements.router, prefix="/admin", tags=["admin-entitlements"], dependencies=guard)
api_router.include_router(admin_users.router, prefix="/admin", tags=["admin"], dependencies=guard)