Files
aiagent/backend/app/api/agents.py

614 lines
20 KiB
Python
Raw Normal View History

2026-01-19 00:09:36 +08:00
"""
Agent管理API
"""
from fastapi import APIRouter, Depends, HTTPException, status, Query, Response
2026-01-19 00:09:36 +08:00
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
2026-01-19 00:09:36 +08:00
from typing import List, Optional, Dict, Any
from datetime import datetime
import logging
from app.core.database import get_db
from app.models.agent import Agent
from app.api.auth import get_current_user
from app.models.user import User
from app.core.exceptions import NotFoundError, ValidationError, ConflictError
from app.services.permission_service import check_agent_permission
from app.services.agent_workspace_chat_log import fetch_agent_preview_chat_turns
2026-01-19 00:09:36 +08:00
from app.services.workflow_validator import validate_workflow
import uuid
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/api/v1/agents",
tags=["agents"],
responses={
401: {"description": "未授权"},
404: {"description": "资源不存在"},
400: {"description": "请求参数错误"},
500: {"description": "服务器内部错误"}
}
)
class AgentCreate(BaseModel):
"""Agent创建模型"""
name: str
description: Optional[str] = None
workflow_config: Dict[str, Any] # 包含nodes和edges
budget_config: Optional[Dict[str, Any]] = None
2026-01-19 00:09:36 +08:00
class AgentUpdate(BaseModel):
"""Agent更新模型"""
name: Optional[str] = None
description: Optional[str] = None
workflow_config: Optional[Dict[str, Any]] = None
status: Optional[str] = None
budget_config: Optional[Dict[str, Any]] = None
class SceneTemplateItem(BaseModel):
"""场景模板列表项(元数据)"""
id: str
title: str
description: str
category: Optional[str] = None
default_temperature: Optional[float] = None
parameter_hints: List[str] = Field(default_factory=list)
class AgentFromSceneTemplateCreate(BaseModel):
"""从场景模板创建 Agent"""
template_id: str
name: str
description: Optional[str] = None
parameters: Dict[str, Any] = Field(default_factory=dict)
budget_config: Optional[Dict[str, Any]] = None
2026-01-19 00:09:36 +08:00
class PreviewChatTurnResponse(BaseModel):
"""设计器预览侧单轮对话(来自已完成执行)"""
execution_id: str
created_at: datetime
user_text: str
agent_text: str
2026-04-13 22:52:36 +08:00
attachments: List[Dict[str, Any]] = Field(default_factory=list)
class Config:
from_attributes = True
2026-01-19 00:09:36 +08:00
class AgentResponse(BaseModel):
"""Agent响应模型"""
id: str
name: str
description: Optional[str]
workflow_config: Dict[str, Any]
budget_config: Optional[Dict[str, Any]] = None
2026-01-19 00:09:36 +08:00
version: int
status: str
2026-03-06 22:31:41 +08:00
user_id: Optional[str] # 允许为None
2026-01-19 00:09:36 +08:00
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
@router.get("", response_model=List[AgentResponse])
async def get_agents(
response: Response,
2026-01-19 00:09:36 +08:00
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(100, ge=1, le=100, description="每页记录数"),
search: Optional[str] = Query(None, description="搜索关键词(按名称或描述)"),
status: Optional[str] = Query(None, description="状态筛选"),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
获取Agent列表
支持分页搜索状态筛选
"""
# 管理员可以看到所有Agent普通用户只能看到自己拥有的或有read权限的
if current_user.role == "admin":
query = db.query(Agent)
else:
# 获取用户拥有或有read权限的Agent
from sqlalchemy import or_
from app.models.permission import AgentPermission
# 用户拥有的Agent
owned_agents = db.query(Agent.id).filter(Agent.user_id == current_user.id).subquery()
# 用户有read权限的Agent通过用户ID或角色
user_permissions = db.query(AgentPermission.agent_id).filter(
AgentPermission.permission_type == "read",
or_(
AgentPermission.user_id == current_user.id,
AgentPermission.role_id.in_([r.id for r in current_user.roles])
)
).subquery()
query = db.query(Agent).filter(
or_(
Agent.id.in_(db.query(owned_agents.c.id)),
Agent.id.in_(db.query(user_permissions.c.agent_id))
)
)
# 搜索:按名称或描述搜索
if search:
search_pattern = f"%{search}%"
query = query.filter(
(Agent.name.like(search_pattern)) |
(Agent.description.like(search_pattern))
)
# 筛选:按状态筛选
if status:
query = query.filter(Agent.status == status)
# 先获取总数(不带分页)
total_count = query.count()
2026-01-19 00:09:36 +08:00
# 排序和分页
agents = query.order_by(Agent.created_at.desc()).offset(skip).limit(limit).all()
2026-03-06 22:31:41 +08:00
# 转换为响应格式确保user_id和日期时间字段正确处理
result = []
for agent in agents:
result.append({
"id": agent.id,
"name": agent.name,
"description": agent.description,
"workflow_config": agent.workflow_config,
"budget_config": agent.budget_config,
2026-03-06 22:31:41 +08:00
"version": agent.version,
"status": agent.status,
"user_id": agent.user_id if agent.user_id else None,
"created_at": agent.created_at if agent.created_at else datetime.now(),
"updated_at": agent.updated_at if agent.updated_at else datetime.now()
})
# 通过 X-Total-Count 响应头返回总数,前端借此正确分页
response.headers["X-Total-Count"] = str(total_count)
2026-03-06 22:31:41 +08:00
return result
2026-01-19 00:09:36 +08:00
@router.post("", response_model=AgentResponse, status_code=status.HTTP_201_CREATED)
async def create_agent(
agent_data: AgentCreate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
创建Agent
创建时会验证工作流配置的有效性
"""
# 验证工作流配置
if "nodes" not in agent_data.workflow_config or "edges" not in agent_data.workflow_config:
raise ValidationError("工作流配置必须包含nodes和edges")
nodes = agent_data.workflow_config.get("nodes", [])
edges = agent_data.workflow_config.get("edges", [])
# 验证工作流
validation_result = validate_workflow(nodes, edges)
if not validation_result["valid"]:
raise ValidationError(f"工作流配置验证失败: {', '.join(validation_result['errors'])}")
# 检查名称是否重复
existing_agent = db.query(Agent).filter(
Agent.name == agent_data.name,
Agent.user_id == current_user.id
).first()
if existing_agent:
raise ConflictError(f"Agent名称 '{agent_data.name}' 已存在")
# 创建Agent
agent = Agent(
name=agent_data.name,
description=agent_data.description,
workflow_config=agent_data.workflow_config,
budget_config=agent_data.budget_config,
2026-01-19 00:09:36 +08:00
user_id=current_user.id,
status="draft"
)
db.add(agent)
db.commit()
db.refresh(agent)
logger.info(f"用户 {current_user.username} 创建了Agent: {agent.name} ({agent.id})")
return agent
@router.get(
"/{agent_id}/preview-chat-history",
response_model=List[PreviewChatTurnResponse],
)
async def get_agent_preview_chat_history(
agent_id: str,
preview_user_id: Optional[str] = Query(
None,
description="预览会话 user_id与创建执行时 input_data.user_id 一致),只返回本会话记录",
),
limit: int = Query(50, ge=1, le=200),
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""
获取设计器预览区的对话历史按已完成执行还原
与前端 localStorage 中的 preview user_id 对齐避免多人混在同一 Agent 下串会话
须注册在 GET /{agent_id} 之前避免路径被误匹配
"""
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise NotFoundError(f"Agent不存在: {agent_id}")
if not check_agent_permission(db, current_user, agent, "read"):
raise HTTPException(status_code=403, detail="无权访问此Agent")
rows = fetch_agent_preview_chat_turns(
db,
agent_id,
preview_user_id=preview_user_id,
limit=limit,
)
return rows
2026-01-19 00:09:36 +08:00
@router.get("/{agent_id}", response_model=AgentResponse)
async def get_agent(
agent_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
获取Agent详情
"""
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise NotFoundError(f"Agent不存在: {agent_id}")
# 检查权限read权限
if not check_agent_permission(db, current_user, agent, "read"):
raise HTTPException(status_code=403, detail="无权访问此Agent")
return agent
@router.put("/{agent_id}", response_model=AgentResponse)
async def update_agent(
agent_id: str,
agent_data: AgentUpdate,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
更新Agent
如果更新了workflow_config会验证工作流配置的有效性
"""
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise NotFoundError(f"Agent不存在: {agent_id}")
# 检查权限write权限
if not check_agent_permission(db, current_user, agent, "write"):
raise HTTPException(status_code=403, detail="无权修改此Agent")
# 更新字段
if agent_data.name is not None:
# 检查名称是否重复排除当前Agent
existing_agent = db.query(Agent).filter(
Agent.name == agent_data.name,
Agent.user_id == current_user.id,
Agent.id != agent_id
).first()
if existing_agent:
raise ConflictError(f"Agent名称 '{agent_data.name}' 已存在")
agent.name = agent_data.name
if agent_data.description is not None:
agent.description = agent_data.description
if agent_data.workflow_config is not None:
# 验证工作流配置
if "nodes" not in agent_data.workflow_config or "edges" not in agent_data.workflow_config:
raise ValidationError("工作流配置必须包含nodes和edges")
nodes = agent_data.workflow_config.get("nodes", [])
edges = agent_data.workflow_config.get("edges", [])
validation_result = validate_workflow(nodes, edges)
if not validation_result["valid"]:
raise ValidationError(f"工作流配置验证失败: {', '.join(validation_result['errors'])}")
agent.workflow_config = agent_data.workflow_config
agent.version += 1 # 版本号自增
if agent_data.status is not None:
valid_statuses = ["draft", "published", "running", "stopped"]
if agent_data.status not in valid_statuses:
raise ValidationError(f"无效的状态: {agent_data.status}")
agent.status = agent_data.status
if agent_data.budget_config is not None:
agent.budget_config = agent_data.budget_config
2026-01-19 00:09:36 +08:00
db.commit()
db.refresh(agent)
logger.info(f"用户 {current_user.username} 更新了Agent: {agent.name} ({agent.id})")
return agent
@router.delete("/{agent_id}", status_code=status.HTTP_200_OK)
async def delete_agent(
agent_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
删除Agent只有所有者可以删除
"""
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise NotFoundError(f"Agent不存在: {agent_id}")
# 只有Agent所有者可以删除
if agent.user_id != current_user.id and current_user.role != "admin":
raise HTTPException(status_code=403, detail="无权删除此Agent")
agent_name = agent.name
db.delete(agent)
db.commit()
logger.info(f"用户 {current_user.username} 删除了Agent: {agent_name} ({agent_id})")
return {"message": "Agent已删除"}
@router.post("/{agent_id}/deploy", response_model=AgentResponse, status_code=status.HTTP_200_OK)
async def deploy_agent(
agent_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
部署Agent
将Agent状态设置为published
"""
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise NotFoundError(f"Agent不存在: {agent_id}")
# 检查权限deploy权限
if not check_agent_permission(db, current_user, agent, "deploy"):
raise HTTPException(status_code=403, detail="无权部署此Agent")
agent.status = "published"
db.commit()
db.refresh(agent)
logger.info(f"用户 {current_user.username} 部署了Agent: {agent.name} ({agent_id})")
return agent
@router.post("/{agent_id}/stop", response_model=AgentResponse, status_code=status.HTTP_200_OK)
async def stop_agent(
agent_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
停止Agent
将Agent状态设置为stopped
"""
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise NotFoundError(f"Agent不存在: {agent_id}")
# 检查权限deploy权限停止也需要deploy权限
if not check_agent_permission(db, current_user, agent, "deploy"):
raise HTTPException(status_code=403, detail="无权停止此Agent")
agent.status = "stopped"
db.commit()
db.refresh(agent)
logger.info(f"用户 {current_user.username} 停止了Agent: {agent.name} ({agent_id})")
return agent
2026-01-19 17:52:29 +08:00
class AgentDuplicateRequest(BaseModel):
"""Agent复制请求模型"""
name: Optional[str] = None # 如果提供,使用此名称;否则自动生成
@router.post("/{agent_id}/duplicate", response_model=AgentResponse, status_code=status.HTTP_201_CREATED)
async def duplicate_agent(
agent_id: str,
duplicate_data: Optional[AgentDuplicateRequest] = None,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""
复制Agent
创建一个新的Agent副本包含原Agent的所有配置工作流描述等
新Agent的状态为draft版本号为1
"""
# 获取原Agent
original_agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not original_agent:
raise NotFoundError(f"Agent不存在: {agent_id}")
# 检查权限read权限需要能读取原Agent才能复制
if not check_agent_permission(db, current_user, original_agent, "read"):
raise HTTPException(status_code=403, detail="无权复制此Agent")
# 生成新名称
if duplicate_data and duplicate_data.name:
new_name = duplicate_data.name
else:
# 自动生成名称:原名称 + " (副本)"
base_name = original_agent.name
new_name = f"{base_name} (副本)"
# 如果名称已存在,添加序号
counter = 1
while db.query(Agent).filter(
Agent.name == new_name,
Agent.user_id == current_user.id
).first():
counter += 1
new_name = f"{base_name} (副本 {counter})"
# 深拷贝工作流配置(避免引用问题)
import copy
new_workflow_config = copy.deepcopy(original_agent.workflow_config)
# 创建新Agent
new_agent = Agent(
name=new_name,
description=original_agent.description,
workflow_config=new_workflow_config,
budget_config=copy.deepcopy(original_agent.budget_config)
if original_agent.budget_config is not None
else None,
2026-01-19 17:52:29 +08:00
user_id=current_user.id,
status="draft", # 复制的Agent状态为草稿
version=1 # 版本号从1开始
)
db.add(new_agent)
db.commit()
db.refresh(new_agent)
logger.info(f"用户 {current_user.username} 复制了Agent: {original_agent.name} ({agent_id}) -> {new_agent.name} ({new_agent.id})")
return new_agent
2026-01-20 11:03:55 +08:00
@router.get("/{agent_id}/export", status_code=status.HTTP_200_OK)
async def export_agent(
agent_id: str,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""导出AgentJSON格式"""
try:
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise NotFoundError("Agent", agent_id)
# 检查权限read权限
if not check_agent_permission(db, current_user, agent, "read"):
raise HTTPException(status_code=403, detail="无权导出此Agent")
# 验证工作流配置
workflow_config = agent.workflow_config
if not workflow_config:
raise ValidationError("Agent工作流配置为空无法导出")
export_data = {
"id": str(agent.id),
"name": agent.name,
"description": agent.description,
"workflow_config": workflow_config,
"budget_config": agent.budget_config,
2026-01-20 11:03:55 +08:00
"version": agent.version,
"status": agent.status,
"exported_at": datetime.utcnow().isoformat()
}
logger.info(f"用户 {current_user.username} 导出Agent: {agent.name} ({agent_id})")
return export_data
except Exception as e:
logger.error(f"导出Agent失败: {str(e)}", exc_info=True)
raise
@router.post("/import", response_model=AgentResponse, status_code=status.HTTP_201_CREATED)
async def import_agent(
agent_data: Dict[str, Any],
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""导入AgentJSON格式"""
# 提取Agent数据
name = agent_data.get("name", "导入的Agent")
description = agent_data.get("description")
workflow_config = agent_data.get("workflow_config", {})
# 验证工作流配置
if not workflow_config:
raise ValidationError("Agent工作流配置不能为空")
nodes = workflow_config.get("nodes", [])
edges = workflow_config.get("edges", [])
if not nodes or not edges:
raise ValidationError("Agent工作流配置无效缺少节点或边")
# 验证工作流
validation_result = validate_workflow(nodes, edges)
if not validation_result["valid"]:
raise ValidationError(f"导入的Agent工作流验证失败: {', '.join(validation_result['errors'])}")
# 重新生成节点ID避免ID冲突
node_id_mapping = {}
for node in nodes:
old_id = node["id"]
new_id = f"node_{len(node_id_mapping)}_{old_id}"
node_id_mapping[old_id] = new_id
node["id"] = new_id
# 更新边的源节点和目标节点ID
for edge in edges:
if edge.get("source") in node_id_mapping:
edge["source"] = node_id_mapping[edge["source"]]
if edge.get("target") in node_id_mapping:
edge["target"] = node_id_mapping[edge["target"]]
# 检查名称是否已存在
base_name = name
counter = 1
while db.query(Agent).filter(
Agent.name == name,
Agent.user_id == current_user.id
).first():
name = f"{base_name} (导入 {counter})"
counter += 1
# 创建Agent
bc = agent_data.get("budget_config")
2026-01-20 11:03:55 +08:00
agent = Agent(
name=name,
description=description,
workflow_config={
"nodes": nodes,
"edges": edges
},
budget_config=bc if isinstance(bc, dict) else None,
2026-01-20 11:03:55 +08:00
user_id=current_user.id,
status="draft", # 导入的Agent默认为草稿状态
version=1
)
db.add(agent)
db.commit()
db.refresh(agent)
return agent