feat(agent): add support for CrewAI agents and update related configurations

This commit is contained in:
Davidson Gomes
2025-05-14 08:23:59 -03:00
parent 98c559e1ce
commit 0dbf6d1c13
15 changed files with 638 additions and 11 deletions

View File

@@ -32,13 +32,15 @@ from google.adk.agents.llm_agent import LlmAgent
from google.adk.agents import SequentialAgent, ParallelAgent, LoopAgent, BaseAgent
from google.adk.models.lite_llm import LiteLlm
from google.adk.tools.agent_tool import AgentTool
from src.schemas.schemas import Agent
from src.utils.logger import setup_logger
from src.core.exceptions import AgentNotFoundError
from src.services.agent_service import get_agent
from src.services.custom_tools import CustomToolBuilder
from src.services.mcp_service import MCPService
from src.services.a2a_agent import A2ACustomAgent
from src.services.workflow_agent import WorkflowAgent
from src.services.custom_agents.a2a_agent import A2ACustomAgent
from src.services.custom_agents.workflow_agent import WorkflowAgent
from src.services.custom_agents.crew_ai_agent import CrewAIAgent
from src.services.apikey_service import get_decrypted_api_key
from sqlalchemy.orm import Session
from contextlib import AsyncExitStack
@@ -47,6 +49,8 @@ from google.adk.tools import load_memory
from datetime import datetime
import uuid
from src.schemas.agent_config import CrewAITask
logger = setup_logger(__name__)
@@ -104,6 +108,18 @@ class AgentBuilder:
current_time=current_time,
)
# add role on beginning of the prompt
if agent.role:
formatted_prompt = (
f"<agent_role>{agent.role}</agent_role>\n\n{formatted_prompt}"
)
# add goal on beginning of the prompt
if agent.goal:
formatted_prompt = (
f"<agent_goal>{agent.goal}</agent_goal>\n\n{formatted_prompt}"
)
# Check if load_memory is enabled
if agent.config.get("load_memory"):
all_tools.append(load_memory)
@@ -298,6 +314,56 @@ class AgentBuilder:
logger.error(f"Error building Workflow agent: {str(e)}")
raise ValueError(f"Error building Workflow agent: {str(e)}")
async def build_crew_ai_agent(
self, root_agent: Agent
) -> Tuple[CrewAIAgent, Optional[AsyncExitStack]]:
"""Build a CrewAI agent with its sub-agents."""
logger.info(f"Creating CrewAI agent: {root_agent.name}")
agent_config = root_agent.config or {}
# Verify if we have tasks configured
if not agent_config.get("tasks"):
raise ValueError("tasks are required for CrewAI agents")
try:
# Get sub-agents if there are any
sub_agents = []
if root_agent.config.get("sub_agents"):
sub_agents_with_stacks = await self._get_sub_agents(
root_agent.config.get("sub_agents")
)
sub_agents = [agent for agent, _ in sub_agents_with_stacks]
# Additional configurations
config = root_agent.config or {}
# Convert tasks to the expected format by CrewAIAgent
tasks = []
for task_config in config.get("tasks", []):
task = CrewAITask(
agent_id=task_config.get("agent_id"),
description=task_config.get("description", ""),
expected_output=task_config.get("expected_output", ""),
)
tasks.append(task)
# Create the CrewAI agent
crew_ai_agent = CrewAIAgent(
name=root_agent.name,
tasks=tasks,
db=self.db,
sub_agents=sub_agents,
)
logger.info(f"CrewAI agent created successfully: {root_agent.name}")
return crew_ai_agent, None
except Exception as e:
logger.error(f"Error building CrewAI agent: {str(e)}")
raise ValueError(f"Error building CrewAI agent: {str(e)}")
async def build_composite_agent(
self, root_agent
) -> Tuple[SequentialAgent | ParallelAgent | LoopAgent, Optional[AsyncExitStack]]:
@@ -367,7 +433,8 @@ class AgentBuilder:
| ParallelAgent
| LoopAgent
| A2ACustomAgent
| WorkflowAgent,
| WorkflowAgent
| CrewAIAgent,
Optional[AsyncExitStack],
]:
"""Build the appropriate agent based on the type of the root agent."""
@@ -377,5 +444,7 @@ class AgentBuilder:
return await self.build_a2a_agent(root_agent)
elif root_agent.type == "workflow":
return await self.build_workflow_agent(root_agent)
elif root_agent.type == "crew_ai":
return await self.build_crew_ai_agent(root_agent)
else:
return await self.build_composite_agent(root_agent)

View File

@@ -202,6 +202,53 @@ async def create_agent(db: Session, agent: AgentCreate) -> Agent:
if "api_key" not in agent.config or not agent.config["api_key"]:
agent.config["api_key"] = generate_api_key()
elif agent.type == "crew_ai":
if not isinstance(agent.config, dict):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid configuration: must be an object with tasks",
)
if "tasks" not in agent.config:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid configuration: tasks is required for crew_ai agents",
)
if not agent.config["tasks"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid configuration: tasks cannot be empty",
)
# Validar se todos os agent_id nas tasks existem
for task in agent.config["tasks"]:
if "agent_id" not in task:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Each task must have an agent_id",
)
agent_id = task["agent_id"]
task_agent = get_agent(db, agent_id)
if not task_agent:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Agent not found for task: {agent_id}",
)
# Validar sub_agents se existir
if "sub_agents" in agent.config and agent.config["sub_agents"]:
if not validate_sub_agents(db, agent.config["sub_agents"]):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="One or more sub-agents do not exist",
)
# Gerar API key se não existir
if "api_key" not in agent.config or not agent.config["api_key"]:
agent.config["api_key"] = generate_api_key()
# Additional sub-agent validation (for non-llm and non-a2a types)
elif agent.type != "llm":
if not isinstance(agent.config, dict):
@@ -637,6 +684,47 @@ async def update_agent(
if "config" not in agent_data:
agent_data["config"] = agent_config
# Validar configuração de crew_ai, se aplicável
if ("type" in agent_data and agent_data["type"] == "crew_ai") or (
agent.type == "crew_ai" and "config" in agent_data
):
config = agent_data.get("config", {})
if "tasks" not in config:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid configuration: tasks is required for crew_ai agents",
)
if not config["tasks"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid configuration: tasks cannot be empty",
)
# Validar se todos os agent_id nas tasks existem
for task in config["tasks"]:
if "agent_id" not in task:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Each task must have an agent_id",
)
agent_id = task["agent_id"]
task_agent = get_agent(db, agent_id)
if not task_agent:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Agent not found for task: {agent_id}",
)
# Validar sub_agents se existir
if "sub_agents" in config and config["sub_agents"]:
if not validate_sub_agents(db, config["sub_agents"]):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="One or more sub-agents do not exist",
)
if not agent_config.get("api_key") and (
"config" not in agent_data or not agent_data["config"].get("api_key")
):

View File

View File

@@ -0,0 +1,266 @@
"""
┌──────────────────────────────────────────────────────────────────────────────┐
│ @author: Davidson Gomes │
│ @file: a2a_agent.py │
│ Developed by: Davidson Gomes │
│ Creation date: May 13, 2025 │
│ Contact: contato@evolution-api.com │
├──────────────────────────────────────────────────────────────────────────────┤
│ @copyright © Evolution API 2025. All rights reserved. │
│ Licensed under the Apache License, Version 2.0 │
│ │
│ You may not use this file except in compliance with the License. │
│ You may obtain a copy of the License at │
│ │
│ http://www.apache.org/licenses/LICENSE-2.0 │
│ │
│ Unless required by applicable law or agreed to in writing, software │
│ distributed under the License is distributed on an "AS IS" BASIS, │
│ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. │
│ See the License for the specific language governing permissions and │
│ limitations under the License. │
├──────────────────────────────────────────────────────────────────────────────┤
│ @important │
│ For any future changes to the code in this file, it is recommended to │
│ include, together with the modification, the information of the developer │
│ who changed it and the date of modification. │
└──────────────────────────────────────────────────────────────────────────────┘
"""
from attr import Factory
from google.adk.agents import BaseAgent
from google.adk.agents.invocation_context import InvocationContext
from google.adk.events import Event
from google.genai.types import Content, Part
from src.services.agent_service import get_agent
from src.services.apikey_service import get_decrypted_api_key
from sqlalchemy.orm import Session
from typing import AsyncGenerator, List
from src.schemas.agent_config import CrewAITask
from crewai import Agent, Task, Crew, LLM
class CrewAIAgent(BaseAgent):
"""
Custom agent that implements the CrewAI protocol directly.
This agent implements the interaction with an external CrewAI service.
"""
# Field declarations for Pydantic
tasks: List[CrewAITask]
db: Session
def __init__(
self,
name: str,
tasks: List[CrewAITask],
db: Session,
sub_agents: List[BaseAgent] = [],
**kwargs,
):
"""
Initialize the CrewAI agent.
Args:
name: Agent name
tasks: List of tasks to be executed
db: Database session
sub_agents: List of sub-agents to be executed after the CrewAI agent
"""
# Initialize base class
super().__init__(
name=name,
tasks=tasks,
db=db,
sub_agents=sub_agents,
**kwargs,
)
def _generate_llm(self, model: str, api_key: str):
"""
Generate the LLM for the CrewAI agent.
"""
return LLM(model=model, api_key=api_key)
def _agent_builder(self, agent_id: str):
"""
Build the CrewAI agent.
"""
agent = get_agent(self.db, agent_id)
if not agent:
raise ValueError(f"Agent with id {agent_id} not found")
api_key = None
decrypted_key = get_decrypted_api_key(self.db, agent.api_key_id)
if decrypted_key:
api_key = decrypted_key
else:
raise ValueError(
f"API key with ID {agent.api_key_id} not found or inactive"
)
if not api_key:
raise ValueError(f"API key for agent {agent.name} not found")
return Agent(
role=agent.role,
goal=agent.goal,
backstory=agent.instruction,
llm=self._generate_llm(agent.model, api_key),
verbose=True,
)
def _tasks_and_agents_builder(self):
"""
Build the CrewAI tasks.
"""
tasks = []
agents = []
for task in self.tasks:
agent = self._agent_builder(task.agent_id)
agents.append(agent)
tasks.append(
Task(
description=task.description,
expected_output=task.expected_output,
agent=agent,
)
)
return tasks, agents
def _crew_builder(self):
"""
Build the CrewAI crew.
"""
tasks, agents = self._tasks_and_agents_builder()
return Crew(
agents=agents,
tasks=tasks,
verbose=True,
)
async def _run_async_impl(
self, ctx: InvocationContext
) -> AsyncGenerator[Event, None]:
"""
Implementation of the CrewAI.
This method follows the pattern of implementing custom agents,
sending the user's message to the CrewAI service and monitoring the response.
"""
try:
# Extract the user's message from the context
user_message = None
# Search for the user's message in the session events
if ctx.session and hasattr(ctx.session, "events") and ctx.session.events:
for event in reversed(ctx.session.events):
if event.author == "user" and event.content and event.content.parts:
user_message = event.content.parts[0].text
print("Message found in session events")
break
# Check in the session state if the message was not found in the events
if not user_message and ctx.session and ctx.session.state:
if "user_message" in ctx.session.state:
user_message = ctx.session.state["user_message"]
elif "message" in ctx.session.state:
user_message = ctx.session.state["message"]
if not user_message:
yield Event(
author=self.name,
content=Content(
role="agent",
parts=[Part(text="User message not found")],
),
)
return
try:
# Replace any {content} in the task descriptions with the user's input
for task in self.tasks:
task.description = task.description.replace(
"{content}", user_message
)
# Build the Crew
crew = self._crew_builder()
# Start the agent status
yield Event(
author=self.name,
content=Content(
role="agent",
parts=[Part(text=f"Starting CrewAI processing...")],
),
)
# Prepare inputs (if there are placeholders to replace)
inputs = {"user_message": user_message}
# Notify the user that the processing is in progress
yield Event(
author=self.name,
content=Content(
role="agent",
parts=[Part(text=f"Processing your request...")],
),
)
# Try first with kickoff() normally
try:
# If it fails, try with kickoff_async
result = await crew.kickoff_async(inputs=inputs)
print(f"Result of crew.kickoff_async(): {result}")
except Exception as e:
print(f"Error executing crew.kickoff_async(): {str(e)}")
print("Trying alternative with crew.kickoff()")
result = crew.kickoff(inputs=inputs)
print(f"Result of crew.kickoff(): {result}")
# Create an event for the final result
final_event = Event(
author=self.name,
content=Content(role="agent", parts=[Part(text=str(result))]),
)
# Transmit the event to the client
yield final_event
# Execute sub-agents
for sub_agent in self.sub_agents:
async for event in sub_agent.run_async(ctx):
yield event
except Exception as e:
error_msg = f"Error sending request: {str(e)}"
print(error_msg)
print(f"Error type: {type(e).__name__}")
print(f"Error details: {str(e)}")
yield Event(
author=self.name,
content=Content(role="agent", parts=[Part(text=error_msg)]),
)
return
except Exception as e:
# Handle any uncaught error
print(f"Error executing CrewAI agent: {str(e)}")
yield Event(
author=self.name,
content=Content(
role="agent",
parts=[Part(text=f"Error interacting with CrewAI agent: {str(e)}")],
),
)