From 13a6247780bd983af232e7f529f9cd58b2f26091 Mon Sep 17 00:00:00 2001 From: Davidson Gomes Date: Tue, 29 Apr 2025 19:29:48 -0300 Subject: [PATCH] chore: update environment variables and improve agent configuration --- .env | 4 + .env.example | 8 + .gitignore | 4 +- Makefile | 2 +- README.md | 54 ++++++ pyproject.toml | 1 + scripts/seeders/agent_seeder.py | 71 +++++--- scripts/seeders/mcp_server_seeder.py | 147 +++++++-------- src/api/agent_routes.py | 260 ++++++++++++++++++++++++++- src/config/settings.py | 14 +- src/models/models.py | 10 ++ src/schemas/agent_config.py | 13 ++ src/schemas/schemas.py | 24 ++- src/services/agent_runner.py | 14 +- src/services/agent_service.py | 11 +- src/services/mcp_server_service.py | 14 +- 16 files changed, 520 insertions(+), 131 deletions(-) diff --git a/.env b/.env index ea972309..ebe59e39 100644 --- a/.env +++ b/.env @@ -1,6 +1,10 @@ API_TITLE="Evo API" API_DESCRIPTION="API para execução de agentes de IA" API_VERSION="1.0.0" +API_URL="http://localhost:8000" + +ORGANIZATION_NAME="Evo AI" +ORGANIZATION_URL="https://evoai.evoapicloud.com" # Configurações do banco de dados POSTGRES_CONNECTION_STRING="postgresql://postgres:root@localhost:5432/evo_ai" diff --git a/.env.example b/.env.example index 6e92e9bc..7f7f26c9 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,11 @@ +API_TITLE="Evo API" +API_DESCRIPTION="API para execução de agentes de IA" +API_VERSION="1.0.0" +API_URL="http://localhost:8000" + +ORGANIZATION_NAME="Evo AI" +ORGANIZATION_URL="https://evoai.evoapicloud.com" + # Database settings POSTGRES_CONNECTION_STRING="postgresql://postgres:root@localhost:5432/evo_ai" diff --git a/.gitignore b/.gitignore index 54401fdc..63ae9339 100644 --- a/.gitignore +++ b/.gitignore @@ -126,4 +126,6 @@ celerybeat-schedule *.swo # OS -Thumbs.db \ No newline at end of file +Thumbs.db + +uv.lock diff --git a/Makefile b/Makefile index cd211cb2..88ecebc6 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ alembic-downgrade: # Command to run the server run: - uvicorn src.main:app --reload --host 0.0.0.0 --port 8000 --reload-dir src + uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload --env-file .env # Command to run the server in production mode run-prod: diff --git a/README.md b/README.md index 7456ed68..fa0158e1 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ The Evo AI platform allows: - MCP server configuration - Custom tools management - JWT authentication with email verification +- **Agent 2 Agent (A2A) Protocol Support**: Interoperability between AI agents following Google's A2A specification ## 🛠️ Technologies @@ -27,6 +28,59 @@ The Evo AI platform allows: - **Jinja2**: Template engine for email rendering - **Bcrypt**: Password hashing and security +## 🤖 Agent 2 Agent (A2A) Protocol Support + +Evo AI implements the Google's Agent 2 Agent (A2A) protocol, enabling seamless communication and interoperability between AI agents. This implementation includes: + +### Key Features + +- **Standardized Communication**: Agents can communicate using a common protocol regardless of their underlying implementation +- **Interoperability**: Support for agents built with different frameworks and technologies +- **Well-Known Endpoints**: Standardized endpoints for agent discovery and interaction +- **Task Management**: Support for task-based interactions between agents +- **State Management**: Tracking of agent states and conversation history +- **Authentication**: Secure API key-based authentication for agent interactions + +### Implementation Details + +- **Agent Card**: Each agent exposes a `.well-known/agent.json` endpoint with its capabilities and configuration +- **Task Handling**: Support for task creation, execution, and status tracking +- **Message Format**: Standardized message format for agent communication +- **History Tracking**: Maintains conversation history between agents +- **Artifact Management**: Support for handling different types of artifacts (text, files, etc.) + +### Example Usage + +```json +// Agent Card Example +{ + "name": "My Agent", + "description": "A helpful AI assistant", + "url": "https://api.example.com/agents/123", + "capabilities": { + "streaming": false, + "pushNotifications": false, + "stateTransitionHistory": true + }, + "authentication": { + "schemes": ["apiKey"], + "credentials": { + "in": "header", + "name": "x-api-key" + } + }, + "skills": [ + { + "id": "search", + "name": "Web Search", + "description": "Search the web for information" + } + ] +} +``` + +For more information about the A2A protocol, visit [Google's A2A Protocol Documentation](https://google.github.io/A2A/). + ## 📁 Project Structure ``` diff --git a/pyproject.toml b/pyproject.toml index f5a3a8e0..2df64356 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ "fastapi_utils==0.8.0", "bcrypt==4.3.0", "jinja2==3.1.6", + "pydantic[email]==2.11.3", ] [project.optional-dependencies] diff --git a/scripts/seeders/agent_seeder.py b/scripts/seeders/agent_seeder.py index 8751726a..8f2b618e 100644 --- a/scripts/seeders/agent_seeder.py +++ b/scripts/seeders/agent_seeder.py @@ -17,43 +17,52 @@ from dotenv import load_dotenv from src.models.models import Agent, Client, User # Configurar logging -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" +) logger = logging.getLogger(__name__) + def create_demo_agents(): """Create example agents for the demo client""" try: # Load environment variables load_dotenv() - + # Get database settings db_url = os.getenv("POSTGRES_CONNECTION_STRING") if not db_url: logger.error("Environment variable POSTGRES_CONNECTION_STRING not defined") return False - + # Connect to the database engine = create_engine(db_url) Session = sessionmaker(bind=engine) session = Session() - + try: # Obter o cliente demo pelo email do usuário demo_email = os.getenv("DEMO_EMAIL", "demo@exemplo.com") demo_user = session.query(User).filter(User.email == demo_email).first() - + if not demo_user or not demo_user.client_id: - logger.error(f"Demo user not found or not associated with a client: {demo_email}") + logger.error( + f"Demo user not found or not associated with a client: {demo_email}" + ) return False - + client_id = demo_user.client_id - + # Verificar se já existem agentes para este cliente - existing_agents = session.query(Agent).filter(Agent.client_id == client_id).all() + existing_agents = ( + session.query(Agent).filter(Agent.client_id == client_id).all() + ) if existing_agents: - logger.info(f"There are already {len(existing_agents)} agents for the client {client_id}") + logger.info( + f"There are already {len(existing_agents)} agents for the client {client_id}" + ) return True - + # Example agent definitions agents = [ { @@ -69,11 +78,12 @@ def create_demo_agents(): inform that you will consult a specialist and return soon. """, "config": { + "api_key": uuid.uuid4(), "tools": [], "mcp_servers": [], "custom_tools": [], - "sub_agents": [] - } + "sub_agents": [], + }, }, { "name": "Sales_Products", @@ -89,11 +99,12 @@ def create_demo_agents(): the customer's needs before recommending a product. """, "config": { + "api_key": uuid.uuid4(), "tools": [], "mcp_servers": [], "custom_tools": [], - "sub_agents": [] - } + "sub_agents": [], + }, }, { "name": "FAQ_Bot", @@ -109,14 +120,15 @@ def create_demo_agents(): appropriate support channel. """, "config": { + "api_key": uuid.uuid4(), "tools": [], "mcp_servers": [], "custom_tools": [], - "sub_agents": [] - } - } + "sub_agents": [], + }, + }, ] - + # Create the agents for agent_data in agents: # Create the agent @@ -128,27 +140,32 @@ def create_demo_agents(): model=agent_data["model"], api_key=agent_data["api_key"], instruction=agent_data["instruction"].strip(), - config=agent_data["config"] + config=agent_data["config"], ) - + session.add(agent) - logger.info(f"Agent '{agent_data['name']}' created for the client {client_id}") - + logger.info( + f"Agent '{agent_data['name']}' created for the client {client_id}" + ) + session.commit() - logger.info(f"All example agents were created successfully for the client {client_id}") + logger.info( + f"All example agents were created successfully for the client {client_id}" + ) return True - + except SQLAlchemyError as e: session.rollback() logger.error(f"Database error when creating example agents: {str(e)}") return False - + except Exception as e: logger.error(f"Error when creating example agents: {str(e)}") return False finally: session.close() + if __name__ == "__main__": success = create_demo_agents() - sys.exit(0 if success else 1) \ No newline at end of file + sys.exit(0 if success else 1) diff --git a/scripts/seeders/mcp_server_seeder.py b/scripts/seeders/mcp_server_seeder.py index c06d1006..2cc8b8f4 100644 --- a/scripts/seeders/mcp_server_seeder.py +++ b/scripts/seeders/mcp_server_seeder.py @@ -62,7 +62,20 @@ def create_mcp_servers(): ], }, "environments": {}, - "tools": ["sequentialthinking"], + "tools": [ + { + "id": "sequentialthinking", + "name": "Sequential Thinking", + "description": "Helps organize thoughts and break down complex problems through a structured workflow", + "tags": ["thinking", "analysis", "problem-solving"], + "examples": [ + "Help me analyze this problem", + "Guide me through this decision making process", + ], + "inputModes": ["text"], + "outputModes": ["text"], + } + ], "type": "community", "id": "4519dd69-9343-4792-af51-dc4d322fb0c9", "created_at": "2025-04-28T15:14:16.901236Z", @@ -76,87 +89,30 @@ def create_mcp_servers(): }, "environments": {}, "tools": [ - "worker_list", - "worker_get", - "worker_put", - "worker_delete", - "worker_get_worker", - "worker_logs_by_worker_name", - "worker_logs_by_ray_id", - "worker_logs_keys", - "get_kvs", - "kv_get", - "kv_put", - "kv_list", - "kv_delete", - "r2_list_buckets", - "r2_create_bucket", - "r2_delete_bucket", - "r2_list_objects", - "r2_get_object", - "r2_put_object", - "r2_delete_object", - "d1_list_databases", - "d1_create_database", - "d1_delete_database", - "d1_query", - "durable_objects_list", - "durable_objects_create", - "durable_objects_delete", - "durable_objects_list_instances", - "durable_objects_get_instance", - "durable_objects_delete_instance", - "queues_list", - "queues_create", - "queues_delete", - "queues_get", - "queues_send_message", - "queues_get_messages", - "queues_update_consumer", - "workers_ai_list_models", - "workers_ai_get_model", - "workers_ai_run_inference", - "workers_ai_list_tasks", - "workflows_list", - "workflows_create", - "workflows_delete", - "workflows_get", - "workflows_update", - "workflows_execute", - "templates_list", - "templates_get", - "templates_create_from_template", - "w4p_list_dispatchers", - "w4p_create_dispatcher", - "w4p_delete_dispatcher", - "w4p_get_dispatcher", - "w4p_update_dispatcher", - "bindings_list", - "bindings_create", - "bindings_update", - "bindings_delete", - "routing_list_routes", - "routing_create_route", - "routing_update_route", - "routing_delete_route", - "cron_list", - "cron_create", - "cron_update", - "cron_delete", - "zones_list", - "zones_create", - "zones_delete", - "zones_get", - "zones_check_activation", - "secrets_list", - "secrets_put", - "secrets_delete", - "versions_list", - "versions_get", - "versions_rollback", - "wrangler_get_config", - "wrangler_update_config", - "analytics_get", + { + "id": "worker_list", + "name": "List Workers", + "description": "List all Cloudflare Workers in your account", + "tags": ["workers", "cloudflare"], + "examples": [ + "List all my workers", + "Show me my Cloudflare workers", + ], + "inputModes": ["text"], + "outputModes": ["application/json"], + }, + { + "id": "worker_get", + "name": "Get Worker", + "description": "Get details of a specific Cloudflare Worker", + "tags": ["workers", "cloudflare"], + "examples": [ + "Show me details of worker X", + "Get information about worker Y", + ], + "inputModes": ["text"], + "outputModes": ["application/json"], + }, ], "type": "official", "id": "9138d1a2-24e6-4a75-87b0-bfa4932273e8", @@ -172,7 +128,32 @@ def create_mcp_servers(): "env": {"BRAVE_API_KEY": "env@@BRAVE_API_KEY"}, }, "environments": {"BRAVE_API_KEY": "env@@BRAVE_API_KEY"}, - "tools": ["brave_web_search", "brave_local_search"], + "tools": [ + { + "id": "brave_web_search", + "name": "Web Search", + "description": "Perform web searches using Brave Search", + "tags": ["search", "web"], + "examples": [ + "Search for Python documentation", + "Find information about AI", + ], + "inputModes": ["text"], + "outputModes": ["application/json"], + }, + { + "id": "brave_local_search", + "name": "Local Search", + "description": "Search for local businesses and places", + "tags": ["search", "local"], + "examples": [ + "Find restaurants near me", + "Search for hotels in New York", + ], + "inputModes": ["text"], + "outputModes": ["application/json"], + }, + ], "type": "official", "id": "416c94d7-77f5-43f4-8181-aeb87934ecbf", "created_at": "2025-04-28T15:20:07.647225Z", diff --git a/src/api/agent_routes.py b/src/api/agent_routes.py index 25845819..62cfefdf 100644 --- a/src/api/agent_routes.py +++ b/src/api/agent_routes.py @@ -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", + ) diff --git a/src/config/settings.py b/src/config/settings.py index 81632a73..9ac29d19 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -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() diff --git a/src/models/models.py b/src/models/models.py index dc6a1364..20b2e1d0 100644 --- a/src/models/models.py +++ b/src/models/models.py @@ -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): diff --git a/src/schemas/agent_config.py b/src/schemas/agent_config.py index 360e34c1..89710b02 100644 --- a/src/schemas/agent_config.py +++ b/src/schemas/agent_config.py @@ -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" ) diff --git a/src/schemas/schemas.py b/src/schemas/schemas.py index d894336b..78aa0116 100644 --- a/src/schemas/schemas.py +++ b/src/schemas/schemas.py @@ -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") diff --git a/src/services/agent_runner.py b/src/services/agent_runner.py index 8df8cbad..1abe17f0 100644 --- a/src/services/agent_runner.py +++ b/src/services/agent_runner.py @@ -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) diff --git a/src/services/agent_service.py b/src/services/agent_service.py index 31793edb..0bddfa8b 100644 --- a/src/services/agent_service.py +++ b/src/services/agent_service.py @@ -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() diff --git a/src/services/mcp_server_service.py b/src/services/mcp_server_service.py index cc9f54bf..639e47e7 100644 --- a/src/services/mcp_server_service.py +++ b/src/services/mcp_server_service.py @@ -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()