feat(agent): add agent folder management routes and functionality

This commit is contained in:
Davidson Gomes 2025-05-07 11:58:59 -03:00
parent 0e3c331a72
commit 72f666aca1
4 changed files with 475 additions and 25 deletions

View File

@ -1,7 +1,7 @@
from fastapi import APIRouter, Depends, HTTPException, status, Header
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
from typing import List, Dict, Any, Optional
import uuid
from src.core.jwt_middleware import (
get_jwt_token,
@ -10,6 +10,9 @@ from src.core.jwt_middleware import (
from src.schemas.schemas import (
Agent,
AgentCreate,
AgentFolder,
AgentFolderCreate,
AgentFolderUpdate,
)
from src.services import (
agent_service,
@ -65,6 +68,243 @@ router = APIRouter(
)
# Rotas para pastas de agentes
@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),
):
"""Cria uma nova pasta para organizar agentes"""
# Verifica se o usuário tem acesso ao cliente da pasta
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),
):
"""Lista as pastas de agentes de um cliente"""
# Verifica se o usuário tem acesso aos dados deste cliente
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),
):
"""Obtém os detalhes de uma pasta específica"""
# Verifica se o usuário tem acesso aos dados deste cliente
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="Pasta não encontrada"
)
# Verifica se a pasta pertence ao cliente informado
if folder.client_id != x_client_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Pasta não pertence ao cliente informado",
)
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),
):
"""Atualiza uma pasta de agentes"""
# Verifica se o usuário tem acesso aos dados deste cliente
await verify_user_client(payload, db, x_client_id)
# Verifica se a pasta existe
folder = agent_service.get_agent_folder(db, folder_id)
if not folder:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Pasta não encontrada"
)
# Verifica se a pasta pertence ao cliente informado
if folder.client_id != x_client_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Pasta não pertence ao cliente informado",
)
# Atualiza a pasta
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 uma pasta de agentes"""
# Verifica se o usuário tem acesso aos dados deste cliente
await verify_user_client(payload, db, x_client_id)
# Verifica se a pasta existe
folder = agent_service.get_agent_folder(db, folder_id)
if not folder:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Pasta não encontrada"
)
# Verifica se a pasta pertence ao cliente informado
if folder.client_id != x_client_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Pasta não pertence ao cliente informado",
)
# Deleta a pasta
if not agent_service.delete_agent_folder(db, folder_id):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Pasta não encontrada"
)
@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),
):
"""Lista os agentes em uma pasta específica"""
# Verifica se o usuário tem acesso aos dados deste cliente
await verify_user_client(payload, db, x_client_id)
# Verifica se a pasta existe
folder = agent_service.get_agent_folder(db, folder_id)
if not folder:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Pasta não encontrada"
)
# Verifica se a pasta pertence ao cliente informado
if folder.client_id != x_client_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Pasta não pertence ao cliente informado",
)
# Lista os agentes da pasta
agents = agent_service.get_agents_by_folder(db, folder_id, skip, limit)
# Adiciona URL do agent card quando necessário
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),
):
"""Atribui um agente a uma pasta ou remove da pasta atual (se folder_id=None)"""
# Verifica se o usuário tem acesso aos dados deste cliente
await verify_user_client(payload, db, x_client_id)
# Verifica se o agente existe
agent = agent_service.get_agent(db, agent_id)
if not agent:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Agente não encontrado"
)
# Verifica se o agente pertence ao cliente informado
if agent.client_id != x_client_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Agente não pertence ao cliente informado",
)
# Se folder_id for fornecido, verifica se a pasta existe e pertence ao mesmo cliente
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="Pasta não encontrada"
)
if folder.client_id != x_client_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Pasta não pertence ao cliente informado",
)
# Atribui o agente à pasta ou remove da pasta atual
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
# Modificação nas rotas existentes para suportar filtro por pasta
@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="Filtrar por pasta"),
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
agents = agent_service.get_agents_by_client(
db, x_client_id, skip, limit, True, folder_id
)
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,
@ -82,26 +322,6 @@ async def create_agent(
return db_agent
@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,
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)
agents = agent_service.get_agents_by_client(db, x_client_id, skip, limit)
for agent in agents:
if not agent.agent_card_url:
agent.agent_card_url = agent.agent_card_url_property
return agents
@router.get("/{agent_id}", response_model=Agent)
async def read_agent(
agent_id: uuid.UUID,

View File

@ -51,6 +51,23 @@ class User(Base):
)
class AgentFolder(Base):
__tablename__ = "agent_folders"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
client_id = Column(UUID(as_uuid=True), ForeignKey("clients.id", ondelete="CASCADE"))
name = Column(String, nullable=False)
description = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
# Relação com o cliente
client = relationship("Client", backref="agent_folders")
# Relação com os agentes
agents = relationship("Agent", back_populates="folder")
class Agent(Base):
__tablename__ = "agents"
@ -63,6 +80,12 @@ class Agent(Base):
api_key = Column(String, nullable=True, default="")
instruction = Column(Text)
agent_card_url = Column(String, nullable=True)
# Nova coluna para a pasta - opcional (nullable=True)
folder_id = Column(
UUID(as_uuid=True),
ForeignKey("agent_folders.id", ondelete="SET NULL"),
nullable=True,
)
config = Column(JSON, default={})
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
@ -74,6 +97,9 @@ class Agent(Base):
),
)
# Relação com a pasta
folder = relationship("AgentFolder", back_populates="agents")
@property
def agent_card_url_property(self) -> str:
"""Virtual URL for the agent card"""

View File

@ -1,4 +1,4 @@
from pydantic import BaseModel, Field, validator
from pydantic import BaseModel, Field, validator, EmailStr, UUID4, ConfigDict
from typing import Optional, Dict, Any, Union, List
from datetime import datetime
from uuid import UUID
@ -213,3 +213,26 @@ class Tool(ToolBase):
class Config:
from_attributes = True
# Schema para pasta de agentes
class AgentFolderBase(BaseModel):
name: str
description: Optional[str] = None
class AgentFolderCreate(AgentFolderBase):
client_id: UUID4
class AgentFolderUpdate(AgentFolderBase):
pass
class AgentFolder(AgentFolderBase):
id: UUID4
client_id: UUID4
created_at: datetime
updated_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)

View File

@ -1,7 +1,7 @@
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
from fastapi import HTTPException, status
from src.models.models import Agent
from src.models.models import Agent, AgentFolder
from src.schemas.schemas import AgentCreate
from typing import List, Optional, Dict, Any, Union
from src.services.mcp_server_service import get_mcp_server
@ -89,11 +89,16 @@ def get_agents_by_client(
skip: int = 0,
limit: int = 100,
active_only: bool = True,
folder_id: Optional[uuid.UUID] = None,
) -> List[Agent]:
"""Search for agents by client with pagination"""
"""Search for agents by client with pagination and optional folder filter"""
try:
query = db.query(Agent).filter(Agent.client_id == client_id)
# Filtra por pasta se especificado
if folder_id is not None:
query = query.filter(Agent.folder_id == folder_id)
agents = query.offset(skip).limit(limit).all()
return agents
@ -604,3 +609,179 @@ def activate_agent(db: Session, agent_id: uuid.UUID) -> bool:
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error reactivating agent",
)
# Funções para pastas de agentes
def create_agent_folder(
db: Session, client_id: uuid.UUID, name: str, description: Optional[str] = None
) -> AgentFolder:
"""Cria uma nova pasta para organizar agentes"""
try:
folder = AgentFolder(client_id=client_id, name=name, description=description)
db.add(folder)
db.commit()
db.refresh(folder)
logger.info(f"Pasta de agentes criada com sucesso: {folder.id}")
return folder
except SQLAlchemyError as e:
db.rollback()
logger.error(f"Erro ao criar pasta de agentes: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Erro ao criar pasta de agentes: {str(e)}",
)
def get_agent_folder(db: Session, folder_id: uuid.UUID) -> Optional[AgentFolder]:
"""Busca uma pasta de agentes pelo ID"""
try:
return db.query(AgentFolder).filter(AgentFolder.id == folder_id).first()
except SQLAlchemyError as e:
logger.error(f"Erro ao buscar pasta de agentes {folder_id}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao buscar pasta de agentes",
)
def get_agent_folders_by_client(
db: Session, client_id: uuid.UUID, skip: int = 0, limit: int = 100
) -> List[AgentFolder]:
"""Lista as pastas de agentes de um cliente"""
try:
return (
db.query(AgentFolder)
.filter(AgentFolder.client_id == client_id)
.offset(skip)
.limit(limit)
.all()
)
except SQLAlchemyError as e:
logger.error(f"Erro ao listar pastas de agentes: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao listar pastas de agentes",
)
def update_agent_folder(
db: Session,
folder_id: uuid.UUID,
name: Optional[str] = None,
description: Optional[str] = None,
) -> Optional[AgentFolder]:
"""Atualiza uma pasta de agentes"""
try:
folder = get_agent_folder(db, folder_id)
if not folder:
return None
if name is not None:
folder.name = name
if description is not None:
folder.description = description
db.commit()
db.refresh(folder)
logger.info(f"Pasta de agentes atualizada: {folder_id}")
return folder
except SQLAlchemyError as e:
db.rollback()
logger.error(f"Erro ao atualizar pasta de agentes {folder_id}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao atualizar pasta de agentes",
)
def delete_agent_folder(db: Session, folder_id: uuid.UUID) -> bool:
"""Remove uma pasta de agentes e desvincula os agentes"""
try:
folder = get_agent_folder(db, folder_id)
if not folder:
return False
# Desvincula os agentes da pasta (não deleta os agentes)
agents = db.query(Agent).filter(Agent.folder_id == folder_id).all()
for agent in agents:
agent.folder_id = None
# Deleta a pasta
db.delete(folder)
db.commit()
logger.info(f"Pasta de agentes removida: {folder_id}")
return True
except SQLAlchemyError as e:
db.rollback()
logger.error(f"Erro ao remover pasta de agentes {folder_id}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao remover pasta de agentes",
)
def assign_agent_to_folder(
db: Session, agent_id: uuid.UUID, folder_id: Optional[uuid.UUID]
) -> Optional[Agent]:
"""Atribui um agente a uma pasta (ou remove da pasta se folder_id for None)"""
try:
agent = get_agent(db, agent_id)
if not agent:
return None
# Se folder_id for None, remove o agente da pasta atual
if folder_id is None:
agent.folder_id = None
db.commit()
db.refresh(agent)
logger.info(f"Agente removido da pasta: {agent_id}")
return agent
# Verifica se a pasta existe
folder = get_agent_folder(db, folder_id)
if not folder:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Pasta não encontrada",
)
# Verifica se a pasta pertence ao mesmo cliente do agente
if folder.client_id != agent.client_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="A pasta deve pertencer ao mesmo cliente do agente",
)
# Atribui o agente à pasta
agent.folder_id = folder_id
db.commit()
db.refresh(agent)
logger.info(f"Agente atribuído à pasta {folder_id}: {agent_id}")
return agent
except SQLAlchemyError as e:
db.rollback()
logger.error(f"Erro ao atribuir agente à pasta: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao atribuir agente à pasta",
)
def get_agents_by_folder(
db: Session, folder_id: uuid.UUID, skip: int = 0, limit: int = 100
) -> List[Agent]:
"""Lista os agentes de uma pasta específica"""
try:
return (
db.query(Agent)
.filter(Agent.folder_id == folder_id)
.offset(skip)
.limit(limit)
.all()
)
except SQLAlchemyError as e:
logger.error(f"Erro ao listar agentes da pasta {folder_id}: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Erro ao listar agentes da pasta",
)