633 lines
20 KiB
Python
633 lines
20 KiB
Python
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
from typing import List, Dict, Any
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
from src.config.database import get_db
|
|
from src.core.jwt_middleware import get_jwt_token, verify_user_client, verify_admin, get_current_user_client_id
|
|
from src.schemas.schemas import (
|
|
Client,
|
|
ClientCreate,
|
|
Contact,
|
|
ContactCreate,
|
|
Agent,
|
|
AgentCreate,
|
|
MCPServer,
|
|
MCPServerCreate,
|
|
Tool,
|
|
ToolCreate,
|
|
)
|
|
from src.services import (
|
|
client_service,
|
|
contact_service,
|
|
agent_service,
|
|
mcp_server_service,
|
|
tool_service,
|
|
)
|
|
from src.schemas.chat import ChatRequest, ChatResponse, ErrorResponse
|
|
from src.services.agent_runner import run_agent
|
|
from src.core.exceptions import AgentNotFoundError
|
|
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
|
|
from google.adk.sessions import DatabaseSessionService
|
|
from google.adk.memory import InMemoryMemoryService
|
|
from google.adk.events import Event
|
|
from google.adk.sessions import Session as Adk_Session
|
|
from src.config.settings import settings
|
|
from src.services.session_service import (
|
|
get_session_events,
|
|
get_session_by_id,
|
|
delete_session,
|
|
get_sessions_by_agent,
|
|
get_sessions_by_client,
|
|
)
|
|
|
|
router = APIRouter()
|
|
|
|
# Configuração do PostgreSQL
|
|
POSTGRES_CONNECTION_STRING = settings.POSTGRES_CONNECTION_STRING
|
|
|
|
# Inicializar os serviços globalmente
|
|
session_service = DatabaseSessionService(db_url=POSTGRES_CONNECTION_STRING)
|
|
artifacts_service = InMemoryArtifactService()
|
|
memory_service = InMemoryMemoryService()
|
|
|
|
|
|
@router.post(
|
|
"/chat",
|
|
response_model=ChatResponse,
|
|
responses={
|
|
400: {"model": ErrorResponse},
|
|
404: {"model": ErrorResponse},
|
|
500: {"model": ErrorResponse},
|
|
},
|
|
)
|
|
async def chat(
|
|
request: ChatRequest,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Verificar se o agente pertence ao cliente do usuário
|
|
agent = agent_service.get_agent(db, request.agent_id)
|
|
if not agent:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Agente não encontrado"
|
|
)
|
|
|
|
# Verificar se o usuário tem acesso ao agente (via cliente)
|
|
await verify_user_client(payload, db, agent.client_id)
|
|
|
|
try:
|
|
final_response_text = await run_agent(
|
|
request.agent_id,
|
|
request.contact_id,
|
|
request.message,
|
|
session_service,
|
|
artifacts_service,
|
|
memory_service,
|
|
db,
|
|
)
|
|
|
|
return {
|
|
"response": final_response_text,
|
|
"status": "success",
|
|
"timestamp": datetime.now().isoformat(),
|
|
}
|
|
|
|
except AgentNotFoundError as e:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e)
|
|
)
|
|
|
|
|
|
# Rotas para Sessões
|
|
@router.get("/sessions/client/{client_id}", response_model=List[Adk_Session])
|
|
async def get_client_sessions(
|
|
client_id: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Verificar se o usuário tem acesso aos dados deste cliente
|
|
await verify_user_client(payload, db, client_id)
|
|
return get_sessions_by_client(db, client_id)
|
|
|
|
|
|
@router.get("/sessions/agent/{agent_id}", response_model=List[Adk_Session])
|
|
async def get_agent_sessions(
|
|
agent_id: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
):
|
|
# Verificar se o agente pertence ao cliente do usuário
|
|
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"
|
|
)
|
|
|
|
# Verificar se o usuário tem acesso ao agente (via cliente)
|
|
await verify_user_client(payload, db, agent.client_id)
|
|
|
|
return get_sessions_by_agent(db, agent_id, skip, limit)
|
|
|
|
|
|
@router.get("/sessions/{session_id}", response_model=Adk_Session)
|
|
async def get_session(
|
|
session_id: str,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Obter a sessão
|
|
session = get_session_by_id(session_service, session_id)
|
|
if not session:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Sessão não encontrada"
|
|
)
|
|
|
|
# Verificar se o agente da sessão pertence ao cliente do usuário
|
|
agent_id = uuid.UUID(session.agent_id) if session.agent_id else None
|
|
if agent_id:
|
|
agent = agent_service.get_agent(db, agent_id)
|
|
if agent:
|
|
await verify_user_client(payload, db, agent.client_id)
|
|
|
|
return session
|
|
|
|
|
|
@router.get(
|
|
"/sessions/{session_id}/messages",
|
|
response_model=List[Event],
|
|
)
|
|
async def get_agent_messages(
|
|
session_id: str,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Obter a sessão
|
|
session = get_session_by_id(session_service, session_id)
|
|
if not session:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Sessão não encontrada"
|
|
)
|
|
|
|
# Verificar se o agente da sessão pertence ao cliente do usuário
|
|
agent_id = uuid.UUID(session.agent_id) if session.agent_id else None
|
|
if agent_id:
|
|
agent = agent_service.get_agent(db, agent_id)
|
|
if agent:
|
|
await verify_user_client(payload, db, agent.client_id)
|
|
|
|
return get_session_events(session_service, session_id)
|
|
|
|
|
|
@router.delete(
|
|
"/sessions/{session_id}",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
)
|
|
async def remove_session(
|
|
session_id: str,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Obter a sessão
|
|
session = get_session_by_id(session_service, session_id)
|
|
if not session:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Sessão não encontrada"
|
|
)
|
|
|
|
# Verificar se o agente da sessão pertence ao cliente do usuário
|
|
agent_id = uuid.UUID(session.agent_id) if session.agent_id else None
|
|
if agent_id:
|
|
agent = agent_service.get_agent(db, agent_id)
|
|
if agent:
|
|
await verify_user_client(payload, db, agent.client_id)
|
|
|
|
return delete_session(session_service, session_id)
|
|
|
|
|
|
# Rotas para Clientes
|
|
@router.post("/clients/", response_model=Client, status_code=status.HTTP_201_CREATED)
|
|
async def create_client(
|
|
client: ClientCreate,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Apenas administradores podem criar clientes
|
|
await verify_admin(payload)
|
|
return client_service.create_client(db, client)
|
|
|
|
|
|
@router.get("/clients/", response_model=List[Client])
|
|
async def read_clients(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Se for administrador, pode ver todos os clientes
|
|
# Se for usuário comum, só vê o próprio cliente
|
|
client_id = get_current_user_client_id(payload)
|
|
|
|
if client_id:
|
|
# Usuário comum - retorna apenas seu próprio cliente
|
|
client = client_service.get_client(db, client_id)
|
|
return [client] if client else []
|
|
else:
|
|
# Administrador - retorna todos os clientes
|
|
return client_service.get_clients(db, skip, limit)
|
|
|
|
|
|
@router.get("/clients/{client_id}", response_model=Client)
|
|
async def read_client(
|
|
client_id: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Verificar se o usuário tem acesso aos dados deste cliente
|
|
await verify_user_client(payload, db, client_id)
|
|
|
|
db_client = client_service.get_client(db, client_id)
|
|
if db_client is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Cliente não encontrado"
|
|
)
|
|
return db_client
|
|
|
|
|
|
@router.put("/clients/{client_id}", response_model=Client)
|
|
async def update_client(
|
|
client_id: uuid.UUID,
|
|
client: ClientCreate,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Verificar se o usuário tem acesso aos dados deste cliente
|
|
await verify_user_client(payload, db, client_id)
|
|
|
|
db_client = client_service.update_client(db, client_id, client)
|
|
if db_client is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Cliente não encontrado"
|
|
)
|
|
return db_client
|
|
|
|
|
|
@router.delete("/clients/{client_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_client(
|
|
client_id: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Apenas administradores podem excluir clientes
|
|
await verify_admin(payload)
|
|
|
|
if not client_service.delete_client(db, client_id):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Cliente não encontrado"
|
|
)
|
|
|
|
|
|
# Rotas para Contatos
|
|
@router.post("/contacts/", response_model=Contact, status_code=status.HTTP_201_CREATED)
|
|
async def create_contact(
|
|
contact: ContactCreate,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Verificar se o usuário tem acesso ao cliente do contato
|
|
await verify_user_client(payload, db, contact.client_id)
|
|
|
|
return contact_service.create_contact(db, contact)
|
|
|
|
|
|
@router.get("/contacts/{client_id}", response_model=List[Contact])
|
|
async def read_contacts(
|
|
client_id: uuid.UUID,
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Verificar se o usuário tem acesso aos dados deste cliente
|
|
await verify_user_client(payload, db, client_id)
|
|
|
|
return contact_service.get_contacts_by_client(db, client_id, skip, limit)
|
|
|
|
|
|
@router.get("/contact/{contact_id}", response_model=Contact)
|
|
async def read_contact(
|
|
contact_id: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
db_contact = contact_service.get_contact(db, contact_id)
|
|
if db_contact is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Contato não encontrado"
|
|
)
|
|
|
|
# Verificar se o usuário tem acesso ao cliente do contato
|
|
await verify_user_client(payload, db, db_contact.client_id)
|
|
|
|
return db_contact
|
|
|
|
|
|
@router.put("/contact/{contact_id}", response_model=Contact)
|
|
async def update_contact(
|
|
contact_id: uuid.UUID,
|
|
contact: ContactCreate,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Buscar o contato atual
|
|
db_current_contact = contact_service.get_contact(db, contact_id)
|
|
if db_current_contact is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Contato não encontrado"
|
|
)
|
|
|
|
# Verificar se o usuário tem acesso ao cliente do contato
|
|
await verify_user_client(payload, db, db_current_contact.client_id)
|
|
|
|
# Verificar se está tentando mudar o cliente
|
|
if contact.client_id != db_current_contact.client_id:
|
|
# Verificar se o usuário tem acesso ao novo cliente também
|
|
await verify_user_client(payload, db, contact.client_id)
|
|
|
|
db_contact = contact_service.update_contact(db, contact_id, contact)
|
|
if db_contact is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Contato não encontrado"
|
|
)
|
|
return db_contact
|
|
|
|
|
|
@router.delete("/contact/{contact_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_contact(
|
|
contact_id: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Buscar o contato
|
|
db_contact = contact_service.get_contact(db, contact_id)
|
|
if db_contact is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Contato não encontrado"
|
|
)
|
|
|
|
# Verificar se o usuário tem acesso ao cliente do contato
|
|
await verify_user_client(payload, db, db_contact.client_id)
|
|
|
|
if not contact_service.delete_contact(db, contact_id):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Contato não encontrado"
|
|
)
|
|
|
|
|
|
# Rotas para Agentes
|
|
@router.post("/agents/", 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),
|
|
):
|
|
# Verificar se o usuário tem acesso ao cliente do agente
|
|
await verify_user_client(payload, db, agent.client_id)
|
|
|
|
return agent_service.create_agent(db, agent)
|
|
|
|
|
|
@router.get("/agents/{client_id}", response_model=List[Agent])
|
|
async def read_agents(
|
|
client_id: uuid.UUID,
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Verificar se o usuário tem acesso aos dados deste cliente
|
|
await verify_user_client(payload, db, client_id)
|
|
|
|
return agent_service.get_agents_by_client(db, client_id, skip, limit)
|
|
|
|
|
|
@router.get("/agent/{agent_id}", response_model=Agent)
|
|
async def read_agent(
|
|
agent_id: uuid.UUID,
|
|
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="Agente não encontrado"
|
|
)
|
|
|
|
# Verificar se o usuário tem acesso ao cliente do agente
|
|
await verify_user_client(payload, db, db_agent.client_id)
|
|
|
|
return db_agent
|
|
|
|
|
|
@router.put("/agent/{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),
|
|
):
|
|
# Buscar o agente atual
|
|
db_agent = agent_service.get_agent(db, agent_id)
|
|
if db_agent is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Agente não encontrado"
|
|
)
|
|
|
|
# Verificar se o usuário tem acesso ao cliente do agente
|
|
await verify_user_client(payload, db, db_agent.client_id)
|
|
|
|
# Se estiver tentando mudar o client_id, verificar permissão para o novo cliente também
|
|
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)
|
|
|
|
return await agent_service.update_agent(db, agent_id, agent_data)
|
|
|
|
|
|
@router.delete("/agent/{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),
|
|
):
|
|
# Buscar o agente
|
|
db_agent = agent_service.get_agent(db, agent_id)
|
|
if db_agent is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Agente não encontrado"
|
|
)
|
|
|
|
# Verificar se o usuário tem acesso ao cliente do agente
|
|
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="Agente não encontrado"
|
|
)
|
|
|
|
|
|
# Rotas para Servidores MCP
|
|
@router.post(
|
|
"/mcp-servers/", response_model=MCPServer, status_code=status.HTTP_201_CREATED
|
|
)
|
|
async def create_mcp_server(
|
|
server: MCPServerCreate,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Apenas administradores podem criar servidores MCP
|
|
await verify_admin(payload)
|
|
|
|
return mcp_server_service.create_mcp_server(db, server)
|
|
|
|
|
|
@router.get("/mcp-servers/", response_model=List[MCPServer])
|
|
async def read_mcp_servers(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Todos os usuários autenticados podem listar servidores MCP
|
|
return mcp_server_service.get_mcp_servers(db, skip, limit)
|
|
|
|
|
|
@router.get("/mcp-servers/{server_id}", response_model=MCPServer)
|
|
async def read_mcp_server(
|
|
server_id: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Todos os usuários autenticados podem ver detalhes do servidor MCP
|
|
db_server = mcp_server_service.get_mcp_server(db, server_id)
|
|
if db_server is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Servidor MCP não encontrado"
|
|
)
|
|
return db_server
|
|
|
|
|
|
@router.put("/mcp-servers/{server_id}", response_model=MCPServer)
|
|
async def update_mcp_server(
|
|
server_id: uuid.UUID,
|
|
server: MCPServerCreate,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Apenas administradores podem atualizar servidores MCP
|
|
await verify_admin(payload)
|
|
|
|
db_server = mcp_server_service.update_mcp_server(db, server_id, server)
|
|
if db_server is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Servidor MCP não encontrado"
|
|
)
|
|
return db_server
|
|
|
|
|
|
@router.delete("/mcp-servers/{server_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_mcp_server(
|
|
server_id: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Apenas administradores podem excluir servidores MCP
|
|
await verify_admin(payload)
|
|
|
|
if not mcp_server_service.delete_mcp_server(db, server_id):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Servidor MCP não encontrado"
|
|
)
|
|
|
|
|
|
# Rotas para Ferramentas
|
|
@router.post("/tools/", response_model=Tool, status_code=status.HTTP_201_CREATED)
|
|
async def create_tool(
|
|
tool: ToolCreate,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Apenas administradores podem criar ferramentas
|
|
await verify_admin(payload)
|
|
|
|
return tool_service.create_tool(db, tool)
|
|
|
|
|
|
@router.get("/tools/", response_model=List[Tool])
|
|
async def read_tools(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Todos os usuários autenticados podem listar ferramentas
|
|
return tool_service.get_tools(db, skip, limit)
|
|
|
|
|
|
@router.get("/tools/{tool_id}", response_model=Tool)
|
|
async def read_tool(
|
|
tool_id: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Todos os usuários autenticados podem ver detalhes da ferramenta
|
|
db_tool = tool_service.get_tool(db, tool_id)
|
|
if db_tool is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Ferramenta não encontrada"
|
|
)
|
|
return db_tool
|
|
|
|
|
|
@router.put("/tools/{tool_id}", response_model=Tool)
|
|
async def update_tool(
|
|
tool_id: uuid.UUID,
|
|
tool: ToolCreate,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Apenas administradores podem atualizar ferramentas
|
|
await verify_admin(payload)
|
|
|
|
db_tool = tool_service.update_tool(db, tool_id, tool)
|
|
if db_tool is None:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Ferramenta não encontrada"
|
|
)
|
|
return db_tool
|
|
|
|
|
|
@router.delete("/tools/{tool_id}", status_code=status.HTTP_204_NO_CONTENT)
|
|
async def delete_tool(
|
|
tool_id: uuid.UUID,
|
|
db: Session = Depends(get_db),
|
|
payload: dict = Depends(get_jwt_token),
|
|
):
|
|
# Apenas administradores podem excluir ferramentas
|
|
await verify_admin(payload)
|
|
|
|
if not tool_service.delete_tool(db, tool_id):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Ferramenta não encontrada"
|
|
)
|