chore: update environment variables and improve agent configuration
This commit is contained in:
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user