feat(agent): add support for CrewAI agents and update related configurations
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
):
|
||||
|
||||
0
src/services/custom_agents/__init__.py
Normal file
0
src/services/custom_agents/__init__.py
Normal file
266
src/services/custom_agents/crew_ai_agent.py
Normal file
266
src/services/custom_agents/crew_ai_agent.py
Normal 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)}")],
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user