""" Agent管理API """ from fastapi import APIRouter, Depends, HTTPException, status, Query, Response from sqlalchemy.orm import Session from pydantic import BaseModel, Field 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 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 category: Optional[str] = None tags: Optional[List[str]] = None is_public: bool = False is_featured: bool = False 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 class PreviewChatTurnResponse(BaseModel): """设计器预览侧单轮对话(来自已完成执行)""" execution_id: str created_at: datetime user_text: str agent_text: str attachments: List[Dict[str, Any]] = Field(default_factory=list) class Config: from_attributes = True class AgentResponse(BaseModel): """Agent响应模型""" id: str name: str description: Optional[str] workflow_config: Dict[str, Any] budget_config: Optional[Dict[str, Any]] = None version: int status: str user_id: Optional[str] # 允许为None created_at: datetime updated_at: datetime class Config: from_attributes = True @router.get("", response_model=List[AgentResponse]) async def get_agents( response: Response, 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() # 排序和分页 agents = query.order_by(Agent.created_at.desc()).offset(skip).limit(limit).all() # 转换为响应格式,确保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, "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) return result @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, user_id=current_user.id, status="draft", category=agent_data.category, tags=agent_data.tags, is_public=1 if agent_data.is_public else 0, is_featured=1 if agent_data.is_featured else 0, ) 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 @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 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 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, 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 @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) ): """导出Agent(JSON格式)""" 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, "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) ): """导入Agent(JSON格式)""" # 提取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") agent = Agent( name=name, description=description, workflow_config={ "nodes": nodes, "edges": edges }, budget_config=bc if isinstance(bc, dict) else None, user_id=current_user.id, status="draft", # 导入的Agent默认为草稿状态 version=1 ) db.add(agent) db.commit() db.refresh(agent) return agent