chore: update environment variables and improve agent configuration

This commit is contained in:
Davidson Gomes
2025-04-29 19:29:48 -03:00
parent 0a27670de5
commit 13a6247780
16 changed files with 520 additions and 131 deletions

View File

@@ -1,4 +1,6 @@
from fastapi import APIRouter, Depends, HTTPException, status
from datetime import datetime
import os
from fastapi import APIRouter, Depends, HTTPException, status, Header, Request
from sqlalchemy.orm import Session
from src.config.database import get_db
from typing import List, Dict, Any
@@ -13,12 +15,59 @@ from src.schemas.schemas import (
)
from src.services import (
agent_service,
mcp_server_service,
)
from src.services.agent_runner import run_agent
from src.services.service_providers import (
session_service,
artifacts_service,
memory_service,
)
import logging
from src.services.session_service import get_session_events
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"],
@@ -38,23 +87,24 @@ async def create_agent(
return agent_service.create_agent(db, agent)
@router.get("/{client_id}", response_model=List[Agent])
@router.get("/", response_model=List[Agent])
async def read_agents(
client_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),
):
# Verify if the user has access to this client's data
await verify_user_client(payload, db, client_id)
await verify_user_client(payload, db, x_client_id)
return agent_service.get_agents_by_client(db, client_id, skip, limit)
return agent_service.get_agents_by_client(db, x_client_id, skip, limit)
@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),
):
@@ -65,7 +115,7 @@ async def read_agent(
)
# Verify if the user has access to the agent's client
await verify_user_client(payload, db, db_agent.client_id)
await verify_user_client(payload, db, x_client_id)
return db_agent
@@ -115,3 +165,201 @@ async def delete_agent(
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found"
)
@router.get("/{agent_id}/.well-known/agent.json")
async def get_agent_json(
agent_id: uuid.UUID,
db: Session = Depends(get_db),
):
try:
agent = agent_service.get_agent(db, agent_id)
if agent is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found"
)
mcp_servers = agent.config.get("mcp_servers", [])
formatted_tools = await format_agent_tools(mcp_servers, db)
AGENT_CARD = {
"name": agent.name,
"description": agent.description,
"url": f"{os.getenv('API_URL', '')}/api/v1/agents/{agent.id}",
"provider": {
"organization": os.getenv("ORGANIZATION_NAME", ""),
"url": os.getenv("ORGANIZATION_URL", ""),
},
"version": os.getenv("API_VERSION", ""),
"capabilities": {
"streaming": False,
"pushNotifications": False,
"stateTransitionHistory": True,
},
"authentication": {
"schemes": ["apiKey"],
"credentials": {"in": "header", "name": "x-api-key"},
},
"defaultInputModes": ["text", "application/json"],
"defaultOutputModes": ["text", "application/json"],
"skills": formatted_tools,
}
return AGENT_CARD
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Error generating agent card",
)
@router.post("/{agent_id}/tasks/send")
async def handle_task(
agent_id: uuid.UUID,
request: Request,
x_api_key: str = Header(..., alias="x-api-key"),
db: Session = Depends(get_db),
):
"""Endpoint to clients A2A send a new task (with an initial user message)."""
try:
# Verify agent
agent = agent_service.get_agent(db, agent_id)
if agent is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Agent not found"
)
# Verify API key
agent_config = agent.config
if agent_config.get("api_key") != x_api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid API key for this agent",
)
# Process request
try:
task_request = await request.json()
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid request format"
)
# Validate required fields
task_id = task_request.get("id")
if not task_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Task ID is required"
)
# Extract user message
try:
user_message = task_request["message"]["parts"][0]["text"]
except (KeyError, IndexError):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid message format"
)
# Configure session and metadata
session_id = f"{task_id}_{agent_id}"
metadata = task_request.get("metadata", {})
history_length = metadata.get("historyLength", 50)
# Initialize response
response_task = {
"id": task_id,
"sessionId": session_id,
"status": {
"state": "running",
"timestamp": datetime.now().isoformat(),
"message": None,
"error": None,
},
"artifacts": [],
"history": [],
"metadata": metadata,
}
try:
# Execute agent
final_response_text = await run_agent(
str(agent_id),
task_id,
user_message,
session_service,
artifacts_service,
memory_service,
db,
session_id,
)
# Update status to completed
response_task["status"].update(
{
"state": "completed",
"timestamp": datetime.now().isoformat(),
"message": {
"role": "agent",
"parts": [{"type": "text", "text": final_response_text}],
},
}
)
# Add artifacts
if final_response_text:
response_task["artifacts"].append(
{
"type": "text",
"content": final_response_text,
"metadata": {
"generated_at": datetime.now().isoformat(),
"content_type": "text/plain",
},
}
)
except Exception as e:
# Update status to failed
response_task["status"].update(
{
"state": "failed",
"timestamp": datetime.now().isoformat(),
"error": {"code": "AGENT_EXECUTION_ERROR", "message": str(e)},
}
)
# Process history
try:
history_messages = get_session_events(session_service, session_id)
history_messages = history_messages[-history_length:]
formatted_history = []
for event in history_messages:
if event.content and event.content.parts:
role = (
"agent" if event.content.role == "model" else event.content.role
)
formatted_history.append(
{
"role": role,
"parts": [
{"type": "text", "text": part.text}
for part in event.content.parts
if part.text
],
}
)
response_task["history"] = formatted_history
except Exception as e:
logger.error(f"Error processing history: {str(e)}")
return response_task
except HTTPException:
raise
except Exception as e:
logger.error(f"Unexpected error in handle_task: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Internal server error",
)

View File

@@ -1,8 +1,11 @@
import os
from typing import Optional, List
from pydantic_settings import BaseSettings
from functools import lru_cache
import secrets
from dotenv import load_dotenv
# Carrega as variáveis do .env
load_dotenv()
class Settings(BaseSettings):
@@ -12,6 +15,13 @@ class Settings(BaseSettings):
API_TITLE: str = os.getenv("API_TITLE", "Evo AI API")
API_DESCRIPTION: str = os.getenv("API_DESCRIPTION", "API for executing AI agents")
API_VERSION: str = os.getenv("API_VERSION", "1.0.0")
API_URL: str = os.getenv("API_URL", "http://localhost:8000")
# Organization settings
ORGANIZATION_NAME: str = os.getenv("ORGANIZATION_NAME", "Evo AI")
ORGANIZATION_URL: str = os.getenv(
"ORGANIZATION_URL", "https://evoai.evoapicloud.com"
)
# Database settings
POSTGRES_CONNECTION_STRING: str = os.getenv(
@@ -75,10 +85,10 @@ class Settings(BaseSettings):
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
case_sensitive = True
@lru_cache()
def get_settings() -> Settings:
return Settings()

View File

@@ -1,3 +1,4 @@
import os
from sqlalchemy import (
Column,
String,
@@ -83,6 +84,13 @@ class Agent(Base):
),
)
@property
def agent_card_url(self) -> str:
"""URL virtual para o agent card que não é rastrada no banco de dados"""
return (
f"{os.getenv('API_URL', '')}/api/v1/agents/{self.id}/.well-known/agent.json"
)
def to_dict(self):
"""Converts the object to a dictionary, converting UUIDs to strings"""
result = {}
@@ -104,6 +112,8 @@ class Agent(Base):
]
else:
result[key] = value
# Adiciona a propriedade virtual ao dicionário
result["agent_card_url"] = self.agent_card_url
return result
def _convert_dict(self, d):

View File

@@ -1,6 +1,8 @@
from typing import List, Optional, Dict, Union
from pydantic import BaseModel, Field
from uuid import UUID
import secrets
import string
class ToolConfig(BaseModel):
@@ -90,9 +92,20 @@ class CustomTools(BaseModel):
from_attributes = True
def generate_api_key(length: int = 32) -> str:
"""Generate a secure API key."""
alphabet = string.ascii_letters + string.digits
return "".join(secrets.choice(alphabet) for _ in range(length))
class LLMConfig(BaseModel):
"""Configuration for LLM agents"""
api_key: str = Field(
default_factory=generate_api_key,
description="API key for the LLM. If not provided, a secure key will be generated automatically.",
)
tools: Optional[List[ToolConfig]] = Field(
default=None, description="List of available tools"
)

View File

@@ -9,7 +9,16 @@ from src.schemas.agent_config import LLMConfig
class ClientBase(BaseModel):
name: str
email: Optional[EmailStr] = None
email: Optional[str] = None
@validator("email")
def validate_email(cls, v):
if v is None:
return v
email_regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
if not re.match(email_regex, v):
raise ValueError("Invalid email format")
return v
class ClientCreate(ClientBase):
@@ -120,17 +129,28 @@ class Agent(AgentBase):
client_id: UUID
created_at: datetime
updated_at: Optional[datetime] = None
agent_card_url: Optional[str] = None
class Config:
from_attributes = True
class ToolConfig(BaseModel):
id: str
name: str
description: str
tags: List[str] = Field(default_factory=list)
examples: List[str] = Field(default_factory=list)
inputModes: List[str] = Field(default_factory=list)
outputModes: List[str] = Field(default_factory=list)
class MCPServerBase(BaseModel):
name: str
description: Optional[str] = None
config_json: Dict[str, Any] = Field(default_factory=dict)
environments: Dict[str, Any] = Field(default_factory=dict)
tools: List[str] = Field(default_factory=list)
tools: List[ToolConfig] = Field(default_factory=list)
type: str = Field(default="official")

View File

@@ -8,6 +8,7 @@ from src.core.exceptions import AgentNotFoundError, InternalServerError
from src.services.agent_service import get_agent
from src.services.agent_builder import AgentBuilder
from sqlalchemy.orm import Session
from typing import Optional
logger = setup_logger(__name__)
@@ -20,6 +21,7 @@ async def run_agent(
artifacts_service: InMemoryArtifactService,
memory_service: InMemoryMemoryService,
db: Session,
session_id: Optional[str] = None,
):
try:
logger.info(f"Starting execution of agent {agent_id} for contact {contact_id}")
@@ -45,13 +47,15 @@ async def run_agent(
artifact_service=artifacts_service,
memory_service=memory_service,
)
session_id = contact_id + "_" + agent_id
adk_session_id = contact_id + "_" + agent_id
if session_id is None:
session_id = adk_session_id
logger.info(f"Searching session for contact {contact_id}")
session = session_service.get_session(
app_name=agent_id,
user_id=contact_id,
session_id=session_id,
session_id=adk_session_id,
)
if session is None:
@@ -59,7 +63,7 @@ async def run_agent(
session = session_service.create_session(
app_name=agent_id,
user_id=contact_id,
session_id=session_id,
session_id=adk_session_id,
)
content = Content(role="user", parts=[Part(text=message)])
@@ -69,7 +73,7 @@ async def run_agent(
try:
for event in agent_runner.run(
user_id=contact_id,
session_id=session_id,
session_id=adk_session_id,
new_message=content,
):
if event.is_final_response() and event.content and event.content.parts:
@@ -79,7 +83,7 @@ async def run_agent(
completed_session = session_service.get_session(
app_name=agent_id,
user_id=contact_id,
session_id=session_id,
session_id=adk_session_id,
)
memory_service.add_session_to_memory(completed_session)

View File

@@ -1,3 +1,4 @@
import os
from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
from fastapi import HTTPException, status
@@ -27,6 +28,7 @@ def get_agent(db: Session, agent_id: uuid.UUID) -> Optional[Agent]:
if not agent:
logger.warning(f"Agent not found: {agent_id}")
return None
return agent
except SQLAlchemyError as e:
logger.error(f"Error searching for agent {agent_id}: {str(e)}")
@@ -47,7 +49,11 @@ def get_agents_by_client(
try:
query = db.query(Agent).filter(Agent.client_id == client_id)
return query.offset(skip).limit(limit).all()
agents = query.offset(skip).limit(limit).all()
# A propriedade virtual agent_card_url será automaticamente incluída
# quando os agentes forem serializados para JSON
return agents
except SQLAlchemyError as e:
logger.error(f"Error searching for client agents {client_id}: {str(e)}")
raise HTTPException(
@@ -139,6 +145,9 @@ def create_agent(db: Session, agent: AgentCreate) -> Agent:
db.commit()
db.refresh(db_agent)
logger.info(f"Agent created successfully: {db_agent.id}")
# A propriedade virtual agent_card_url será automaticamente incluída
# quando o agente for serializado para JSON
return db_agent
except SQLAlchemyError as e:
db.rollback()

View File

@@ -2,7 +2,7 @@ from sqlalchemy.orm import Session
from sqlalchemy.exc import SQLAlchemyError
from fastapi import HTTPException, status
from src.models.models import MCPServer
from src.schemas.schemas import MCPServerCreate
from src.schemas.schemas import MCPServerCreate, ToolConfig
from typing import List, Optional
import uuid
import logging
@@ -41,7 +41,11 @@ def get_mcp_servers(db: Session, skip: int = 0, limit: int = 100) -> List[MCPSer
def create_mcp_server(db: Session, server: MCPServerCreate) -> MCPServer:
"""Create a new MCP server"""
try:
db_server = MCPServer(**server.model_dump())
# Convert tools to JSON serializable format
server_data = server.model_dump()
server_data["tools"] = [tool.model_dump() for tool in server.tools]
db_server = MCPServer(**server_data)
db.add(db_server)
db.commit()
db.refresh(db_server)
@@ -65,7 +69,11 @@ def update_mcp_server(
if not db_server:
return None
for key, value in server.model_dump().items():
# Convert tools to JSON serializable format
server_data = server.model_dump()
server_data["tools"] = [tool.model_dump() for tool in server.tools]
for key, value in server_data.items():
setattr(db_server, key, value)
db.commit()