feat(agent): add support for CrewAI agents and update related configurations
This commit is contained in:
parent
98c559e1ce
commit
0dbf6d1c13
@ -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
|
||||
|
43
README.md
43
README.md
@ -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
|
||||
|
||||
|
@ -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 ###
|
@ -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 ###
|
@ -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]
|
||||
|
@ -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 │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
"""
|
||||
|
||||
|
@ -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",
|
||||
),
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)}")],
|
||||
),
|
||||
)
|
Loading…
Reference in New Issue
Block a user