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

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.0.10] - develop
### Added
- Add CrewAI agents
## [0.0.9] - 2025-05-13
### Added

View File

@ -11,9 +11,11 @@ The Evo AI platform allows:
- Client management
- MCP server configuration
- Custom tools management
- **[Google Agent Development Kit (ADK)](https://google.github.io/adk-docs/)**: Base framework for agent development, providing support for LLM Agents, Sequential Agents, Loop Agents, Parallel Agents and Custom Agents
- JWT authentication with email verification
- **Agent 2 Agent (A2A) Protocol Support**: Interoperability between AI agents following Google's A2A specification
- **Workflow Agent with LangGraph**: Building complex agent workflows with LangGraph and ReactFlow
- **[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
@ -30,6 +32,8 @@ Agent based on language models like GPT-4, Claude, etc. Can be configured with t
"client_id": "{{client_id}}",
"name": "personal_assistant",
"description": "Specialized personal assistant",
"role": "Personal Assistant",
"goal": "Help users with daily tasks and provide relevant information",
"type": "llm",
"model": "gpt-4",
"api_key_id": "stored-api-key-uuid",
@ -150,6 +154,39 @@ 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"]
}
}
```
### Common Characteristics
- All agent types can have sub-agents
@ -355,7 +392,7 @@ Evo AI implements the Google's Agent 2 Agent (A2A) protocol, enabling seamless c
- **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
- **Task Management**: Support for task creation, execution, and status tracking
- **State Management**: Tracking of agent states and conversation history
- **Authentication**: Secure API key-based authentication for agent interactions

View File

@ -0,0 +1,34 @@
"""add_crew_ai_coluns_agents_table
Revision ID: 611d84e70bb2
Revises: bdc5d363e2e1
Create Date: 2025-05-14 07:31:08.741620
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '611d84e70bb2'
down_revision: Union[str, None] = 'bdc5d363e2e1'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('agents', sa.Column('role', sa.String(), nullable=True))
op.add_column('agents', sa.Column('goal', sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('agents', 'goal')
op.drop_column('agents', 'role')
# ### end Alembic commands ###

View File

@ -0,0 +1,43 @@
"""add_crew_ai_agent_type_agents_table
Revision ID: bdc5d363e2e1
Revises: 6db4a526335b
Create Date: 2025-05-14 06:23:14.701878
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "bdc5d363e2e1"
down_revision: Union[str, None] = "6db4a526335b"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("check_agent_type", "agents", type_="check")
op.create_check_constraint(
"check_agent_type",
"agents",
"type IN ('llm', 'sequential', 'parallel', 'loop', 'a2a', 'workflow', 'crew_ai')",
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("check_agent_type", "agents", type_="check")
op.create_check_constraint(
"check_agent_type",
"agents",
"type IN ('llm', 'sequential', 'parallel', 'loop', 'a2a', 'workflow')",
)
# ### end Alembic commands ###

View File

@ -51,6 +51,7 @@ 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,6 +24,8 @@
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

@ -100,6 +100,8 @@ class Agent(Base):
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
client_id = Column(UUID(as_uuid=True), ForeignKey("clients.id", ondelete="CASCADE"))
name = Column(String, nullable=False)
role = Column(String, nullable=True)
goal = Column(Text, nullable=True)
description = Column(Text, nullable=True)
type = Column(String, nullable=False)
model = Column(String, nullable=True, default="")
@ -121,7 +123,7 @@ class Agent(Base):
__table_args__ = (
CheckConstraint(
"type IN ('llm', 'sequential', 'parallel', 'loop', 'a2a', 'workflow')",
"type IN ('llm', 'sequential', 'parallel', 'loop', 'a2a', 'workflow', 'crew_ai')",
name="check_agent_type",
),
)

View File

@ -32,6 +32,8 @@ from pydantic import BaseModel, Field
from uuid import UUID
import secrets
import string
import uuid
from pydantic import validator
class ToolConfig(BaseModel):
@ -234,3 +236,42 @@ class WorkflowConfig(BaseModel):
class Config:
from_attributes = True
class CrewAITask(BaseModel):
"""Task configuration for Crew AI agents"""
agent_id: Union[UUID, str] = Field(
..., description="ID of the agent assigned to this task"
)
description: str = Field(..., description="Description of the task to be performed")
expected_output: str = Field(..., description="Expected output from this task")
@validator("agent_id")
def validate_agent_id(cls, v):
if isinstance(v, str):
try:
return uuid.UUID(v)
except ValueError:
raise ValueError(f"Invalid UUID format for agent_id: {v}")
return v
class Config:
from_attributes = True
class CrewAIConfig(BaseModel):
"""Configuration for Crew AI agents"""
tasks: List[CrewAITask] = Field(
..., description="List of tasks to be performed by the crew"
)
api_key: Optional[str] = Field(
default_factory=generate_api_key, description="API key for the Crew AI agent"
)
sub_agents: Optional[List[UUID]] = Field(
default_factory=list, description="List of IDs of sub-agents used in crew"
)
class Config:
from_attributes = True

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
from src.schemas.agent_config import LLMConfig, CrewAIConfig
class ClientBase(BaseModel):
@ -94,8 +94,11 @@ class AgentBase(BaseModel):
None, description="Agent name (no spaces or special characters)"
)
description: Optional[str] = Field(None, description="Agent description")
role: Optional[str] = Field(None, description="Agent role in the system")
goal: Optional[str] = Field(None, description="Agent goal or objective")
type: str = Field(
..., description="Agent type (llm, sequential, parallel, loop, a2a, workflow)"
...,
description="Agent type (llm, sequential, parallel, loop, a2a, workflow, crew_ai)",
)
model: Optional[str] = Field(
None, description="Agent model (required only for llm type)"
@ -126,9 +129,17 @@ class AgentBase(BaseModel):
@validator("type")
def validate_type(cls, v):
if v not in ["llm", "sequential", "parallel", "loop", "a2a", "workflow"]:
if v not in [
"llm",
"sequential",
"parallel",
"loop",
"a2a",
"workflow",
"crew_ai",
]:
raise ValueError(
"Invalid agent type. Must be: llm, sequential, parallel, loop, a2a or workflow"
"Invalid agent type. Must be: llm, sequential, parallel, loop, a2a, workflow or crew_ai"
)
return v
@ -188,6 +199,33 @@ class AgentBase(BaseModel):
raise ValueError(
f'Agent {values["type"]} must have at least one sub-agent'
)
elif values["type"] == "crew_ai":
if not isinstance(v, dict):
raise ValueError(f'Invalid configuration for agent {values["type"]}')
if "tasks" not in v:
raise ValueError(f'Agent {values["type"]} must have tasks')
if not isinstance(v["tasks"], list):
raise ValueError("tasks must be a list")
if not v["tasks"]:
raise ValueError(f'Agent {values["type"]} must have at least one task')
for task in v["tasks"]:
if not isinstance(task, dict):
raise ValueError("Each task must be a dictionary")
required_fields = ["agent_id", "description", "expected_output"]
for field in required_fields:
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")
try:
# Convert the dictionary to CrewAIConfig
v = CrewAIConfig(**v)
except Exception as e:
raise ValueError(f"Invalid Crew AI configuration for agent: {str(e)}")
return v

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)}")],
),
)