""" ┌──────────────────────────────────────────────────────────────────────────────┐ │ @author: Davidson Gomes │ │ @file: agent_routes.py │ │ Developed by: Davidson Gomes │ │ Creation date: May 13, 2025 │ │ Contact: contato@evolution-api.com │ ├──────────────────────────────────────────────────────────────────────────────┤ │ @copyright © Evolution API 2025. All rights reserved. │ │ Licensed under the Apache License, Version 2.0 │ │ │ │ You may not use this file except in compliance with the License. │ │ You may obtain a copy of the License at │ │ │ │ http://www.apache.org/licenses/LICENSE-2.0 │ │ │ │ Unless required by applicable law or agreed to in writing, software │ │ distributed under the License is distributed on an "AS IS" BASIS, │ │ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │ │ See the License for the specific language governing permissions and │ │ limitations under the License. │ ├──────────────────────────────────────────────────────────────────────────────┤ │ @important │ │ For any future changes to the code in this file, it is recommended to │ │ include, together with the modification, the information of the developer │ │ who changed it and the date of modification. │ └──────────────────────────────────────────────────────────────────────────────┘ """ from fastapi import APIRouter, Depends, HTTPException, status, Header, Query from sqlalchemy.orm import Session from src.config.database import get_db from typing import List, Dict, Any, Optional import uuid from src.core.jwt_middleware import ( get_jwt_token, verify_user_client, ) from src.schemas.schemas import ( Agent, AgentCreate, AgentFolder, AgentFolderCreate, AgentFolderUpdate, ApiKey, ApiKeyCreate, ApiKeyUpdate, ) from src.services import agent_service, mcp_server_service, apikey_service import logging logger = logging.getLogger(__name__) async def format_agent_tools( mcp_servers: List[Dict[str, Any]], db: Session ) -> List[Dict[str, Any]]: """Format MCP server tools for agent card skills""" formatted_tools = [] for server in mcp_servers: try: # Get the MCP server by ID server_id = uuid.UUID(server["id"]) mcp_server = mcp_server_service.get_mcp_server(db, server_id) if not mcp_server: logger.warning(f"MCP server not found: {server_id}") continue # Format each tool for tool in mcp_server.tools: formatted_tool = { "id": tool["id"], "name": tool["name"], "description": tool["description"], "tags": tool["tags"], "examples": tool["examples"], "inputModes": tool["inputModes"], "outputModes": tool["outputModes"], } formatted_tools.append(formatted_tool) except Exception as e: logger.error( f"Error formatting tools for MCP server {server.get('id')}: {str(e)}" ) continue return formatted_tools router = APIRouter( prefix="/agents", tags=["agents"], responses={404: {"description": "Not found"}}, ) @router.post("/apikeys", response_model=ApiKey, status_code=status.HTTP_201_CREATED) async def create_api_key( key: ApiKeyCreate, db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): """Create a new API key""" await verify_user_client(payload, db, key.client_id) db_key = apikey_service.create_api_key( db, key.client_id, key.name, key.provider, key.key_value ) return db_key @router.get("/apikeys", response_model=List[ApiKey]) async def read_api_keys( x_client_id: uuid.UUID = Header(..., alias="x-client-id"), skip: int = 0, limit: int = 100, sort_by: str = Query( "name", description="Field to sort: name, provider, created_at" ), sort_direction: str = Query("asc", description="Sort direction: asc, desc"), db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): """List API keys for a client""" # Verify if the user has access to this client's data await verify_user_client(payload, db, x_client_id) keys = apikey_service.get_api_keys_by_client( db, x_client_id, skip, limit, sort_by, sort_direction ) return keys @router.get("/apikeys/{key_id}", response_model=ApiKey) async def read_api_key( key_id: uuid.UUID, x_client_id: uuid.UUID = Header(..., alias="x-client-id"), db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): """Get details of a specific API key""" # Verify if the user has access to this client's data await verify_user_client(payload, db, x_client_id) key = apikey_service.get_api_key(db, key_id) if not key: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="API Key not found" ) # Verify if the key belongs to the specified client if key.client_id != x_client_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="API Key does not belong to the specified client", ) return key @router.put("/apikeys/{key_id}", response_model=ApiKey) async def update_api_key( key_id: uuid.UUID, key_data: ApiKeyUpdate, x_client_id: uuid.UUID = Header(..., alias="x-client-id"), db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): """Update an API key""" # Verify if the user has access to this client's data await verify_user_client(payload, db, x_client_id) # Verify if the key exists key = apikey_service.get_api_key(db, key_id) if not key: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="API Key not found" ) # Verify if the key belongs to the specified client if key.client_id != x_client_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="API Key does not belong to the specified client", ) # Update the key updated_key = apikey_service.update_api_key( db, key_id, key_data.name, key_data.provider, key_data.key_value, key_data.is_active, ) return updated_key @router.delete("/apikeys/{key_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_api_key( key_id: uuid.UUID, x_client_id: uuid.UUID = Header(..., alias="x-client-id"), db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): """Deactivate an API key (soft delete)""" # Verify if the user has access to this client's data await verify_user_client(payload, db, x_client_id) # Verify if the key exists key = apikey_service.get_api_key(db, key_id) if not key: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="API Key not found" ) # Verify if the key belongs to the specified client if key.client_id != x_client_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="API Key does not belong to the specified client", ) # Deactivate the key if not apikey_service.delete_api_key(db, key_id): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="API Key not found" ) # Agent folder routes @router.post( "/folders", response_model=AgentFolder, status_code=status.HTTP_201_CREATED ) async def create_folder( folder: AgentFolderCreate, db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): """Create a new folder to organize agents""" # Verify if the user has access to the folder's client await verify_user_client(payload, db, folder.client_id) return agent_service.create_agent_folder( db, folder.client_id, folder.name, folder.description ) @router.get("/folders", response_model=List[AgentFolder]) async def read_folders( x_client_id: uuid.UUID = Header(..., alias="x-client-id"), skip: int = 0, limit: int = 100, db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): """List agent folders for a client""" # Verify if the user has access to this client's data await verify_user_client(payload, db, x_client_id) return agent_service.get_agent_folders_by_client(db, x_client_id, skip, limit) @router.get("/folders/{folder_id}", response_model=AgentFolder) async def read_folder( folder_id: uuid.UUID, x_client_id: uuid.UUID = Header(..., alias="x-client-id"), db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): """Get details of a specific folder""" # Verify if the user has access to this client's data await verify_user_client(payload, db, x_client_id) folder = agent_service.get_agent_folder(db, folder_id) if not folder: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found" ) # Verify if the folder belongs to the specified client if folder.client_id != x_client_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Folder does not belong to the specified client", ) return folder @router.put("/folders/{folder_id}", response_model=AgentFolder) async def update_folder( folder_id: uuid.UUID, folder_data: AgentFolderUpdate, x_client_id: uuid.UUID = Header(..., alias="x-client-id"), db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): """Update an agent folder""" # Verify if the user has access to this client's data await verify_user_client(payload, db, x_client_id) # Verify if the folder exists folder = agent_service.get_agent_folder(db, folder_id) if not folder: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found" ) # Verify if the folder belongs to the specified client if folder.client_id != x_client_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Folder does not belong to the specified client", ) # Update the folder updated_folder = agent_service.update_agent_folder( db, folder_id, folder_data.name, folder_data.description ) return updated_folder @router.delete("/folders/{folder_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_folder( folder_id: uuid.UUID, x_client_id: uuid.UUID = Header(..., alias="x-client-id"), db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): """Remove an agent folder""" # Verify if the user has access to this client's data await verify_user_client(payload, db, x_client_id) # Verify if the folder exists folder = agent_service.get_agent_folder(db, folder_id) if not folder: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found" ) # Verify if the folder belongs to the specified client if folder.client_id != x_client_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Folder does not belong to the specified client", ) # Delete the folder if not agent_service.delete_agent_folder(db, folder_id): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found" ) @router.get("/folders/{folder_id}/agents", response_model=List[Agent]) async def read_folder_agents( folder_id: uuid.UUID, x_client_id: uuid.UUID = Header(..., alias="x-client-id"), skip: int = 0, limit: int = 100, db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): """List agents in a specific folder""" # Verify if the user has access to this client's data await verify_user_client(payload, db, x_client_id) # Verify if the folder exists folder = agent_service.get_agent_folder(db, folder_id) if not folder: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found" ) # Verify if the folder belongs to the specified client if folder.client_id != x_client_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Folder does not belong to the specified client", ) # List the agents in the folder agents = agent_service.get_agents_by_folder(db, folder_id, skip, limit) # Add agent card URL when needed for agent in agents: if not agent.agent_card_url: agent.agent_card_url = agent.agent_card_url_property return agents @router.put("/{agent_id}/folder", response_model=Agent) async def assign_agent_to_folder( agent_id: uuid.UUID, folder_id: Optional[uuid.UUID] = None, x_client_id: uuid.UUID = Header(..., alias="x-client-id"), db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): """Assign an agent to a folder or remove from the current folder (if folder_id=None)""" # Verify if the user has access to this client's data await verify_user_client(payload, db, x_client_id) # Verify if the agent exists agent = agent_service.get_agent(db, agent_id) if not agent: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found" ) # Verify if the agent belongs to the specified client if agent.client_id != x_client_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Agent does not belong to the specified client", ) # If folder_id is provided, verify if the folder exists and belongs to the same client if folder_id: folder = agent_service.get_agent_folder(db, folder_id) if not folder: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Folder not found" ) if folder.client_id != x_client_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Folder does not belong to the specified client", ) # Assign the agent to the folder or remove from the current folder updated_agent = agent_service.assign_agent_to_folder(db, agent_id, folder_id) if not updated_agent.agent_card_url: updated_agent.agent_card_url = updated_agent.agent_card_url_property return updated_agent # Agent routes (after specific routes) @router.get("/", response_model=List[Agent]) async def read_agents( x_client_id: uuid.UUID = Header(..., alias="x-client-id"), skip: int = 0, limit: int = 100, folder_id: Optional[uuid.UUID] = Query(None, description="Filter by folder"), sort_by: str = Query("name", description="Field to sort: name, created_at"), sort_direction: str = Query("asc", description="Sort direction: asc, desc"), db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): # Verify if the user has access to this client's data await verify_user_client(payload, db, x_client_id) # Get agents with optional folder filter and sorting agents = agent_service.get_agents_by_client( db, x_client_id, skip, limit, True, folder_id, sort_by, sort_direction ) for agent in agents: if not agent.agent_card_url: agent.agent_card_url = agent.agent_card_url_property return agents @router.post("/", response_model=Agent, status_code=status.HTTP_201_CREATED) async def create_agent( agent: AgentCreate, db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): # Verify if the user has access to the agent's client await verify_user_client(payload, db, agent.client_id) db_agent = await agent_service.create_agent(db, agent) if not db_agent.agent_card_url: db_agent.agent_card_url = db_agent.agent_card_url_property return db_agent @router.get("/{agent_id}", response_model=Agent) async def read_agent( agent_id: uuid.UUID, x_client_id: uuid.UUID = Header(..., alias="x-client-id"), db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): db_agent = agent_service.get_agent(db, agent_id) if db_agent is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found" ) # Verify if the user has access to the agent's client await verify_user_client(payload, db, x_client_id) if not db_agent.agent_card_url: db_agent.agent_card_url = db_agent.agent_card_url_property return db_agent @router.put("/{agent_id}", response_model=Agent) async def update_agent( agent_id: uuid.UUID, agent_data: Dict[str, Any], db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): # Get the current agent db_agent = agent_service.get_agent(db, agent_id) if db_agent is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found" ) # Verify if the user has access to the agent's client await verify_user_client(payload, db, db_agent.client_id) # If trying to change the client_id, verify permission for the new client as well if "client_id" in agent_data and agent_data["client_id"] != str(db_agent.client_id): new_client_id = uuid.UUID(agent_data["client_id"]) await verify_user_client(payload, db, new_client_id) updated_agent = await agent_service.update_agent(db, agent_id, agent_data) if not updated_agent.agent_card_url: updated_agent.agent_card_url = updated_agent.agent_card_url_property return updated_agent @router.delete("/{agent_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_agent( agent_id: uuid.UUID, db: Session = Depends(get_db), payload: dict = Depends(get_jwt_token), ): # Get the agent db_agent = agent_service.get_agent(db, agent_id) if db_agent is None: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found" ) # Verify if the user has access to the agent's client await verify_user_client(payload, db, db_agent.client_id) if not agent_service.delete_agent(db, agent_id): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found" )