feat: 支持 Agent 并发批测与洞察导出

This commit is contained in:
2026-08-13 17:25:22 +08:00
parent 3faecab6ac
commit f952d6dc58
22 changed files with 666 additions and 44 deletions

View File

@@ -16,6 +16,8 @@ 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.models.ai_config import ModelConfig
from app.models.knowledge import Knowledge
from app.services.admin_service import OperationLogService
from app.services.agent_batch_test_service import AgentBatchTestService, item_dict, job_dict
@@ -23,6 +25,58 @@ from app.services.agent_batch_test_service import AgentBatchTestService, item_di
router = APIRouter()
@router.get("/agent/model-options")
def agent_model_options(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
"""Safe Agent-only model choices without provider credentials or endpoints."""
items = list(db.scalars(select(ModelConfig).order_by(ModelConfig.is_default.desc(), ModelConfig.id.desc())))
return api_success([
{
"id": item.id,
"displayName": item.display_name,
"modelName": item.model_name,
"temperature": float(item.temperature) if item.temperature is not None else None,
"topP": float(item.top_p) if item.top_p is not None else None,
"topK": item.top_k,
"presencePenalty": float(item.presence_penalty) if item.presence_penalty is not None else None,
"frequencyPenalty": float(item.frequency_penalty) if item.frequency_penalty is not None else None,
"maxToken": item.max_token,
"streamEnabled": item.stream_enabled,
"enabled": item.enabled,
"isDefault": item.is_default,
}
for item in items
])
@router.get("/agent/knowledge-options")
def agent_knowledge_options(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
"""Safe Agent-only knowledge choices; does not grant Knowledge Management access."""
items = list(
db.scalars(
select(Knowledge)
.where(Knowledge.lifecycle_status == "active")
.order_by(Knowledge.id.desc())
)
)
return api_success([
{
"id": item.id,
"name": item.name,
"status": item.status,
"knowledgeType": item.knowledge_type,
"lifecycleStatus": item.lifecycle_status,
"sourceStatus": item.source_status,
}
for item in items
])
@router.get("/agent/batch/template")
def download_batch_template(
current_admin: Admin = Depends(get_current_admin),
@@ -67,10 +121,12 @@ def list_batch_jobs(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
total = db.scalar(select(func.count(AgentBatchTest.id))) or 0
owner_filter = _owner_filter(current_admin)
total = db.scalar(select(func.count(AgentBatchTest.id)).where(*owner_filter)) or 0
rows = list(
db.scalars(
select(AgentBatchTest)
.where(*owner_filter)
.order_by(AgentBatchTest.created_at.desc(), AgentBatchTest.id.desc())
.offset((page - 1) * pageSize)
.limit(pageSize)
@@ -87,7 +143,7 @@ def get_batch_job(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
job = _job_or_404(db, job_id)
job = _job_or_404(db, job_id, current_admin)
total = db.scalar(select(func.count(AgentBatchTestItem.id)).where(AgentBatchTestItem.batch_id == job.id)) or 0
items = list(
db.scalars(
@@ -107,7 +163,7 @@ def export_batch_job(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> StreamingResponse:
job = _job_or_404(db, job_id)
job = _job_or_404(db, job_id, current_admin)
items = list(
db.scalars(
select(AgentBatchTestItem)
@@ -131,7 +187,7 @@ def cancel_batch_job(
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> dict:
job = _job_or_404(db, job_id)
job = _job_or_404(db, job_id, current_admin)
if job.status not in {"pending", "running"}:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="当前任务已结束,不能取消")
now = datetime.now(UTC).replace(tzinfo=None)
@@ -150,8 +206,12 @@ def cancel_batch_job(
return api_success(job_dict(job))
def _job_or_404(db: Session, job_id: int) -> AgentBatchTest:
def _owner_filter(admin: Admin) -> tuple:
return () if admin.is_super_admin else (AgentBatchTest.created_by == admin.id,)
def _job_or_404(db: Session, job_id: int, admin: Admin) -> AgentBatchTest:
job = db.get(AgentBatchTest, job_id)
if job is None:
if job is None or (not admin.is_super_admin and job.created_by != admin.id):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="批量测试任务不存在")
return job

View File

@@ -7,7 +7,7 @@ from io import StringIO
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi.responses import Response
from fastapi.responses import Response, StreamingResponse
from sqlalchemy import exists, func, or_, select
from sqlalchemy.orm import Session, load_only
@@ -23,6 +23,7 @@ from app.models.user import User
from app.api.pagination import page_result
from app.services.admin_service import OperationLogService
from app.services.question_insight_service import QuestionInsightService
from app.services.question_insight_export_service import QuestionInsightExportService
from app.services.growth_profile_service import topic_dict, topic_summary_dict
from app.services.help_card_service import help_card_dict
from app.services.share_draft_service import share_draft_dict
@@ -324,6 +325,45 @@ def refresh_question_insights(
return api_success(result)
@router.get("/question-insights/export")
def export_question_insights(
dateFrom: datetime | None = Query(default=None),
dateTo: datetime | None = Query(default=None),
minCount: int = Query(default=2, ge=1, le=50),
maxMessages: int = Query(default=5000, ge=100, le=20000),
db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin),
) -> StreamingResponse:
if dateFrom is not None and dateTo is not None and dateFrom > dateTo:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="开始时间不能晚于结束时间")
result = QuestionInsightService.summarize(
db,
date_from=dateFrom,
date_to=dateTo,
min_count=minCount,
max_messages=maxMessages,
page=1,
page_size=maxMessages,
)
OperationLogService.write(
db,
admin_id=current_admin.id,
module="question_insight",
action=f"export:{result['total']}",
)
db.commit()
filename = f"question_insights_{_export_date_label(dateFrom)}_{_export_date_label(dateTo)}.xlsx"
return StreamingResponse(
QuestionInsightExportService.build(result),
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
def _export_date_label(value: datetime | None) -> str:
return value.strftime("%Y%m%d") if value is not None else "all"
def _chat_query(
*,
keyword: str,