feat: 支持 Agent 并发批测与洞察导出
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user