refactor(agent): remove CrewAI agent support and update related configurations

This commit is contained in:
Davidson Gomes 2025-05-14 13:13:27 -03:00
parent 2a80bdf7a3
commit 0ca6b4f3e9
10 changed files with 43 additions and 389 deletions

View File

@ -9,7 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add CrewAI agents
- Add Task Agent for structured single-task execution
- Improve context management in agent execution

View File

@ -15,7 +15,6 @@ The Evo AI platform allows:
- JWT authentication with email verification
- **[Agent 2 Agent (A2A) Protocol Support](https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/)**: Interoperability between AI agents following Google's A2A specification
- **[Workflow Agent with LangGraph](https://www.langchain.com/langgraph)**: Building complex agent workflows with LangGraph and ReactFlow
- **[CrewAI Agent Support](https://www.crewai.com/)**: Organizing agents into specialized crews with assigned tasks
- **Secure API Key Management**: Encrypted storage of API keys with Fernet encryption
- **Agent Organization**: Folder structure for organizing agents by categories
@ -154,40 +153,7 @@ Executes sub-agents in a custom workflow defined by a graph structure. This agen
The workflow structure is built using ReactFlow in the frontend, allowing visual creation and editing of complex agent workflows with nodes (representing agents or decision points) and edges (representing flow connections).
### 7. CrewAI Agent
Allows organizing agents into a "crew" with specific tasks assigned to each agent. Based on the CrewAI concept, where each agent has a specific responsibility to perform a more complex task collaboratively.
```json
{
"client_id": "{{client_id}}",
"name": "research_crew",
"type": "crew_ai",
"folder_id": "folder_id (optional)",
"config": {
"tasks": [
{
"agent_id": "agent-uuid-1",
"description": "Search for recent information on the topic",
"expected_output": "Search report in JSON format"
},
{
"agent_id": "agent-uuid-2",
"description": "Analyze data and create visualizations",
"expected_output": "Charts and analyses in HTML format"
},
{
"agent_id": "agent-uuid-3",
"description": "Write final report combining results",
"expected_output": "Markdown document with complete analysis"
}
],
"sub_agents": ["agent-uuid-4", "agent-uuid-5"]
}
}
```
### 8. Task Agent
### 7. Task Agent
Executes a specific task using a target agent. Task Agent provides a streamlined approach for structured task execution, where the agent_id specifies which agent will process the task, and the task description can include dynamic content placeholders.

View File

@ -51,7 +51,6 @@ dependencies = [
"langgraph==0.4.1",
"opentelemetry-sdk==1.33.0",
"opentelemetry-exporter-otlp==1.33.0",
"crewai==0.119.0",
]
[project.optional-dependencies]

View File

@ -24,8 +24,6 @@
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.
@update: May 14, 2025 - Added support for crew_ai agent type
"""

View File

@ -238,12 +238,15 @@ class WorkflowConfig(BaseModel):
from_attributes = True
class CrewAITask(BaseModel):
"""Task configuration for Crew AI agents"""
class AgentTask(BaseModel):
"""Task configuration for agents"""
agent_id: Union[UUID, str] = Field(
..., description="ID of the agent assigned to this task"
)
enabled_tools: Optional[List[str]] = Field(
default_factory=list, description="List of tool names to be used in the task"
)
description: str = Field(..., description="Description of the task to be performed")
expected_output: str = Field(..., description="Expected output from this task")
@ -260,17 +263,17 @@ class CrewAITask(BaseModel):
from_attributes = True
class CrewAIConfig(BaseModel):
"""Configuration for Crew AI agents"""
class AgentConfig(BaseModel):
"""Configuration for agents"""
tasks: List[CrewAITask] = Field(
..., description="List of tasks to be performed by the crew"
tasks: List[AgentTask] = Field(
..., description="List of tasks to be performed by the agent"
)
api_key: Optional[str] = Field(
default_factory=generate_api_key, description="API key for the Crew AI agent"
default_factory=generate_api_key, description="API key for the agent"
)
sub_agents: Optional[List[UUID]] = Field(
default_factory=list, description="List of IDs of sub-agents used in crew"
default_factory=list, description="List of IDs of sub-agents used in agent"
)
class Config:

View File

@ -33,7 +33,7 @@ from datetime import datetime
from uuid import UUID
import uuid
import re
from src.schemas.agent_config import LLMConfig, CrewAIConfig
from src.schemas.agent_config import LLMConfig, AgentConfig
class ClientBase(BaseModel):
@ -98,7 +98,7 @@ class AgentBase(BaseModel):
goal: Optional[str] = Field(None, description="Agent goal or objective")
type: str = Field(
...,
description="Agent type (llm, sequential, parallel, loop, a2a, workflow, crew_ai, task)",
description="Agent type (llm, sequential, parallel, loop, a2a, workflow, task)",
)
model: Optional[str] = Field(
None, description="Agent model (required only for llm type)"
@ -136,11 +136,10 @@ class AgentBase(BaseModel):
"loop",
"a2a",
"workflow",
"crew_ai",
"task",
]:
raise ValueError(
"Invalid agent type. Must be: llm, sequential, parallel, loop, a2a, workflow, crew_ai or task"
"Invalid agent type. Must be: llm, sequential, parallel, loop, a2a, workflow or task"
)
return v
@ -200,7 +199,7 @@ class AgentBase(BaseModel):
raise ValueError(
f'Agent {values["type"]} must have at least one sub-agent'
)
elif values["type"] == "crew_ai" or values["type"] == "task":
elif values["type"] == "task":
if not isinstance(v, dict):
raise ValueError(f'Invalid configuration for agent {values["type"]}')
if "tasks" not in v:
@ -217,7 +216,6 @@ class AgentBase(BaseModel):
if field not in task:
raise ValueError(f"Task missing required field: {field}")
# Validar sub_agents, se existir
if "sub_agents" in v and v["sub_agents"] is not None:
if not isinstance(v["sub_agents"], list):
raise ValueError("sub_agents must be a list")

View File

@ -40,7 +40,6 @@ from src.services.custom_tools import CustomToolBuilder
from src.services.mcp_service import MCPService
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.custom_agents.task_agent import TaskAgent
from src.services.apikey_service import get_decrypted_api_key
from sqlalchemy.orm import Session
@ -50,7 +49,7 @@ from google.adk.tools import load_memory
from datetime import datetime
import uuid
from src.schemas.agent_config import CrewAITask
from src.schemas.agent_config import AgentTask
logger = setup_logger(__name__)
@ -74,7 +73,7 @@ class AgentBuilder:
return agent_tools
async def _create_llm_agent(
self, agent
self, agent, enabled_tools: List[str] = []
) -> Tuple[LlmAgent, Optional[AsyncExitStack]]:
"""Create an LLM agent from the agent data."""
# Get custom tools from the configuration
@ -95,6 +94,10 @@ class AgentBuilder:
# Combine all tools
all_tools = custom_tools + mcp_tools + agent_tools
if enabled_tools:
all_tools = [tool for tool in all_tools if tool.name in enabled_tools]
logger.info(f"Enabled tools enabled. Total tools: {len(all_tools)}")
now = datetime.now()
current_datetime = now.strftime("%d/%m/%Y %H:%M")
current_day_of_week = now.strftime("%A")
@ -200,6 +203,8 @@ class AgentBuilder:
sub_agent, exit_stack = await self.build_a2a_agent(agent)
elif agent.type == "workflow":
sub_agent, exit_stack = await self.build_workflow_agent(agent)
elif agent.type == "task":
sub_agent, exit_stack = await self.build_task_agent(agent)
elif agent.type == "sequential":
sub_agent, exit_stack = await self.build_composite_agent(agent)
elif agent.type == "parallel":
@ -218,7 +223,7 @@ class AgentBuilder:
return sub_agents
async def build_llm_agent(
self, root_agent
self, root_agent, enabled_tools: List[str] = []
) -> Tuple[LlmAgent, Optional[AsyncExitStack]]:
"""Build an LLM agent with its sub-agents."""
logger.info("Creating LLM agent")
@ -230,7 +235,9 @@ class AgentBuilder:
)
sub_agents = [agent for agent, _ in sub_agents_with_stacks]
root_llm_agent, exit_stack = await self._create_llm_agent(root_agent)
root_llm_agent, exit_stack = await self._create_llm_agent(
root_agent, enabled_tools
)
if sub_agents:
root_llm_agent.sub_agents = sub_agents
@ -341,10 +348,11 @@ class AgentBuilder:
# Convert tasks to the expected format by TaskAgent
tasks = []
for task_config in config.get("tasks", []):
task = CrewAITask(
task = AgentTask(
agent_id=task_config.get("agent_id"),
description=task_config.get("description", ""),
expected_output=task_config.get("expected_output", ""),
enabled_tools=task_config.get("enabled_tools", []),
)
tasks.append(task)
@ -362,57 +370,7 @@ class AgentBuilder:
except Exception as e:
logger.error(f"Error building Task agent: {str(e)}")
raise ValueError(f"Error building CrewAI 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)}")
raise ValueError(f"Error building Task agent: {str(e)}")
async def build_composite_agent(
self, root_agent
@ -477,26 +435,23 @@ class AgentBuilder:
else:
raise ValueError(f"Invalid agent type: {root_agent.type}")
async def build_agent(self, root_agent) -> Tuple[
async def build_agent(self, root_agent, enabled_tools: List[str] = []) -> Tuple[
LlmAgent
| SequentialAgent
| ParallelAgent
| LoopAgent
| A2ACustomAgent
| WorkflowAgent
| CrewAIAgent
| TaskAgent,
Optional[AsyncExitStack],
]:
"""Build the appropriate agent based on the type of the root agent."""
if root_agent.type == "llm":
return await self.build_llm_agent(root_agent)
return await self.build_llm_agent(root_agent, enabled_tools)
elif root_agent.type == "a2a":
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)
elif root_agent.type == "task":
return await self.build_task_agent(root_agent)
else:

View File

@ -202,7 +202,7 @@ 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" or agent.type == "task":
elif agent.type == "task":
if not isinstance(agent.config, dict):
agent.config = {}
raise HTTPException(
@ -682,8 +682,8 @@ async def update_agent(
if "config" not in agent_data:
agent_data["config"] = agent_config
if ("type" in agent_data and agent_data["type"] in ["crew_ai", "task"]) or (
agent.type in ["crew_ai", "task"] and "config" in agent_data
if ("type" in agent_data and agent_data["type"] in ["task"]) or (
agent.type in ["task"] and "config" in agent_data
):
config = agent_data.get("config", {})
if "tasks" not in config:

View File

@ -1,266 +0,0 @@
"""
@author: Davidson Gomes
@file: crew_ai_agent.py
Developed by: Davidson Gomes
Creation date: May 14, 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)}")],
),
)

View File

@ -33,13 +33,12 @@ 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 src.schemas.agent_config import AgentTask
class TaskAgent(BaseAgent):
@ -50,13 +49,13 @@ class TaskAgent(BaseAgent):
"""
# Field declarations for Pydantic
tasks: List[CrewAITask]
tasks: List[AgentTask]
db: Session
def __init__(
self,
name: str,
tasks: List[CrewAITask],
tasks: List[AgentTask],
db: Session,
sub_agents: List[BaseAgent] = [],
**kwargs,
@ -132,6 +131,7 @@ class TaskAgent(BaseAgent):
# Replace any {content} in the task descriptions with the user's input
task = self.tasks[0]
task.description = task.description.replace("{content}", user_message)
task.enabled_tools = task.enabled_tools or []
agent = get_agent(self.db, task.agent_id)
@ -166,7 +166,9 @@ class TaskAgent(BaseAgent):
print(f"Building agent in Task agent: {agent.name}")
agent_builder = AgentBuilder(self.db)
root_agent, exit_stack = await agent_builder.build_agent(agent)
root_agent, exit_stack = await agent_builder.build_agent(
agent, task.enabled_tools
)
# Store task instructions in context for reference by sub-agents
ctx.session.state["task_instructions"] = task_message_instructions