Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce more fine control over delegation #2362

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/crewai/agent.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import re
import shutil
import subprocess
from typing import Any, Dict, List, Literal, Optional, Sequence, Union
from typing import Any, Dict, List, Literal, Optional, Sequence, Union, cast

from pydantic import Field, InstanceOf, PrivateAttr, model_validator

Expand Down Expand Up @@ -50,6 +50,7 @@ class Agent(BaseAgent):
max_rpm: Maximum number of requests per minute for the agent execution to be respected.
verbose: Whether the agent execution should be in verbose mode.
allow_delegation: Whether the agent is allowed to delegate tasks to other agents.
delegate_to: List of agents this agent can delegate to. If None and allow_delegation is True, can delegate to all agents.
tools: Tools at agents disposal
step_callback: Callback to be executed after each step of the agent execution.
knowledge_sources: Knowledge sources for the agent.
Expand Down Expand Up @@ -342,10 +343,17 @@ def create_agent_executor(
callbacks=[TokenCalcHandler(self._token_process)],
)

def get_delegation_tools(self, agents: List[BaseAgent]):
agent_tools = AgentTools(agents=agents)
tools = agent_tools.tools()
return tools
def get_delegation_tools(self, agents: Sequence[BaseAgent]) -> Sequence[BaseTool]:
# If delegate_to is specified, use those agents instead of all agents
agents_to_use: List[BaseAgent]
if self.delegate_to is not None:
agents_to_use = cast(List[BaseAgent], list(self.delegate_to))
else:
agents_to_use = list(agents) # Convert to list to match expected type

agent_tools = AgentTools(agents=agents_to_use)
delegation_tools = agent_tools.tools()
return delegation_tools

def get_multimodal_tools(self) -> Sequence[BaseTool]:
from crewai.tools.agent_tools.add_image_tool import AddImageTool
Expand Down
17 changes: 14 additions & 3 deletions src/crewai/agents/agent_builder/base_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from abc import ABC, abstractmethod
from copy import copy as shallow_copy
from hashlib import md5
from typing import Any, Dict, List, Optional, TypeVar
from typing import Any, Dict, List, Optional, Sequence, TypeVar

from pydantic import (
UUID4,
Expand Down Expand Up @@ -42,6 +42,7 @@ class BaseAgent(ABC, BaseModel):
verbose (bool): Verbose mode for the Agent Execution.
max_rpm (Optional[int]): Maximum number of requests per minute for the agent execution.
allow_delegation (bool): Allow delegation of tasks to agents.
delegate_to (Optional[List["BaseAgent"]]): List of agents this agent can delegate to. If None and allow_delegation is True, can delegate to all agents.
tools (Optional[List[Any]]): Tools at the agent's disposal.
max_iter (int): Maximum iterations for an agent to execute a task.
agent_executor (InstanceOf): An instance of the CrewAgentExecutor class.
Expand All @@ -63,7 +64,7 @@ class BaseAgent(ABC, BaseModel):
Abstract method to create an agent executor.
_parse_tools(tools: List[BaseTool]) -> List[Any]:
Abstract method to parse tools.
get_delegation_tools(agents: List["BaseAgent"]):
get_delegation_tools(agents: Sequence["BaseAgent"]) -> Sequence[BaseTool]:
Abstract method to set the agents task tools for handling delegation and question asking to other agents in crew.
get_output_converter(llm, model, instructions):
Abstract method to get the converter class for the agent to create json/pydantic outputs.
Expand Down Expand Up @@ -113,6 +114,10 @@ class BaseAgent(ABC, BaseModel):
default=False,
description="Enable agent to delegate and ask questions among each other.",
)
delegate_to: Optional[List["BaseAgent"]] = Field(
default=None,
description="List of agents this agent can delegate to. If None and allow_delegation is True, can delegate to all agents.",
)
tools: Optional[List[BaseTool]] = Field(
default_factory=list, description="Tools at agents' disposal"
)
Expand Down Expand Up @@ -258,7 +263,7 @@ def _parse_tools(self, tools: List[BaseTool]) -> List[BaseTool]:
pass

@abstractmethod
def get_delegation_tools(self, agents: List["BaseAgent"]) -> List[BaseTool]:
def get_delegation_tools(self, agents: Sequence["BaseAgent"]) -> Sequence[BaseTool]:
"""Set the task tools that init BaseAgenTools class."""
pass

Expand All @@ -285,6 +290,7 @@ def copy(self: T) -> T: # type: ignore # Signature of "copy" incompatible with
"knowledge_sources",
"knowledge_storage",
"knowledge",
"delegate_to",
}

# Copy llm
Expand All @@ -310,6 +316,10 @@ def copy(self: T) -> T: # type: ignore # Signature of "copy" incompatible with
copied_source.storage = shared_storage
existing_knowledge_sources.append(copied_source)

existing_delegate_to = None
if self.delegate_to:
existing_delegate_to = list(self.delegate_to)

copied_data = self.model_dump(exclude=exclude)
copied_data = {k: v for k, v in copied_data.items() if v is not None}
copied_agent = type(self)(
Expand All @@ -319,6 +329,7 @@ def copy(self: T) -> T: # type: ignore # Signature of "copy" incompatible with
knowledge_sources=existing_knowledge_sources,
knowledge=copied_knowledge,
knowledge_storage=copied_knowledge_storage,
delegate_to=existing_delegate_to,
)

return copied_agent
Expand Down
87 changes: 58 additions & 29 deletions src/crewai/crew.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from concurrent.futures import Future
from copy import copy as shallow_copy
from hashlib import md5
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, Union

from pydantic import (
UUID4,
Expand Down Expand Up @@ -36,6 +36,7 @@
from crewai.task import Task
from crewai.tasks.conditional_task import ConditionalTask
from crewai.tasks.task_output import TaskOutput
from crewai.tools import BaseTool
from crewai.tools.agent_tools.agent_tools import AgentTools
from crewai.tools.base_tool import Tool
from crewai.types.usage_metrics import UsageMetrics
Expand Down Expand Up @@ -759,22 +760,27 @@ def _run_hierarchical_process(self) -> CrewOutput:
def _create_manager_agent(self):
i18n = I18N(prompt_file=self.prompt_file)
if self.manager_agent is not None:
# Ensure delegation is enabled for the manager agent
self.manager_agent.allow_delegation = True

# Set the delegate_to property to all agents in the crew
# If delegate_to is already set, it will be used instead of all agents
if self.manager_agent.delegate_to is None:
self.manager_agent.delegate_to = self.agents

manager = self.manager_agent
if manager.tools is not None and len(manager.tools) > 0:
self._logger.log(
"warning", "Manager agent should not have tools", color="orange"
)
manager.tools = []
raise Exception("Manager agent should not have tools")
else:
self.manager_llm = create_llm(self.manager_llm)
# Create delegation tools
delegation_tools = AgentTools(agents=self.agents).tools()

manager = Agent(
role=i18n.retrieve("hierarchical_manager_agent", "role"),
goal=i18n.retrieve("hierarchical_manager_agent", "goal"),
backstory=i18n.retrieve("hierarchical_manager_agent", "backstory"),
tools=AgentTools(agents=self.agents).tools(),
tools=delegation_tools,
allow_delegation=True,
delegate_to=self.agents,
llm=self.manager_llm,
verbose=self.verbose,
)
Expand Down Expand Up @@ -818,8 +824,8 @@ def _execute_tasks(
)

# Determine which tools to use - task tools take precedence over agent tools
tools_for_task = task.tools or agent_to_use.tools or []
tools_for_task = self._prepare_tools(agent_to_use, task, tools_for_task)
initial_tools = task.tools or agent_to_use.tools or []
prepared_tools = self._prepare_tools(agent_to_use, task, initial_tools)

self._log_task_start(task, agent_to_use.role)

Expand All @@ -838,7 +844,7 @@ def _execute_tasks(
future = task.execute_async(
agent=agent_to_use,
context=context,
tools=tools_for_task,
tools=prepared_tools,
)
futures.append((task, future, task_index))
else:
Expand All @@ -850,7 +856,7 @@ def _execute_tasks(
task_output = task.execute_sync(
agent=agent_to_use,
context=context,
tools=tools_for_task,
tools=prepared_tools,
)
task_outputs.append(task_output)
self._process_task_result(task, task_output)
Expand Down Expand Up @@ -888,8 +894,8 @@ def _handle_conditional_task(
return None

def _prepare_tools(
self, agent: BaseAgent, task: Task, tools: List[Tool]
) -> List[Tool]:
self, agent: BaseAgent, task: Task, tools: Sequence[BaseTool]
) -> list[BaseTool]:
# Add delegation tools if agent allows delegation
if agent.allow_delegation:
if self.process == Process.hierarchical:
Expand All @@ -904,22 +910,24 @@ def _prepare_tools(
tools = self._add_delegation_tools(task, tools)

# Add code execution tools if agent allows code execution
if agent.allow_code_execution:
if hasattr(agent, "allow_code_execution") and getattr(
agent, "allow_code_execution", False
):
tools = self._add_code_execution_tools(agent, tools)

if agent and agent.multimodal:
if hasattr(agent, "multimodal") and getattr(agent, "multimodal", False):
tools = self._add_multimodal_tools(agent, tools)

return tools
return list(tools)

def _get_agent_to_use(self, task: Task) -> Optional[BaseAgent]:
if self.process == Process.hierarchical:
return self.manager_agent
return task.agent

def _merge_tools(
self, existing_tools: List[Tool], new_tools: List[Tool]
) -> List[Tool]:
self, existing_tools: Sequence[BaseTool], new_tools: Sequence[BaseTool]
) -> Sequence[BaseTool]:
"""Merge new tools into existing tools list, avoiding duplicates by tool name."""
if not new_tools:
return existing_tools
Expand All @@ -936,21 +944,42 @@ def _merge_tools(
return tools

def _inject_delegation_tools(
self, tools: List[Tool], task_agent: BaseAgent, agents: List[BaseAgent]
self,
tools: Sequence[BaseTool],
task_agent: BaseAgent,
agents: Sequence[BaseAgent],
):
delegation_tools = task_agent.get_delegation_tools(agents)
return self._merge_tools(tools, delegation_tools)

def _add_multimodal_tools(self, agent: BaseAgent, tools: List[Tool]):
multimodal_tools = agent.get_multimodal_tools()
return self._merge_tools(tools, multimodal_tools)
def _add_multimodal_tools(
self, agent: BaseAgent, tools: Sequence[BaseTool]
) -> Sequence[BaseTool]:
if hasattr(agent, "get_multimodal_tools"):
multimodal_tools = getattr(agent, "get_multimodal_tools")()
return self._merge_tools(tools, multimodal_tools)
return tools

def _add_code_execution_tools(self, agent: BaseAgent, tools: List[Tool]):
code_tools = agent.get_code_execution_tools()
return self._merge_tools(tools, code_tools)
def _add_code_execution_tools(
self, agent: BaseAgent, tools: Sequence[BaseTool]
) -> Sequence[BaseTool]:
if hasattr(agent, "get_code_execution_tools"):
code_tools = getattr(agent, "get_code_execution_tools")()
return self._merge_tools(tools, code_tools)
return tools

def _add_delegation_tools(
self, task: Task, tools: Sequence[BaseTool]
) -> Sequence[BaseTool]:
# If the agent has specific agents to delegate to, use those
if task.agent and task.agent.delegate_to is not None:
agents_for_delegation = task.agent.delegate_to
else:
# Otherwise use all agents except the current one
agents_for_delegation = [
agent for agent in self.agents if agent != task.agent
]

def _add_delegation_tools(self, task: Task, tools: List[Tool]):
agents_for_delegation = [agent for agent in self.agents if agent != task.agent]
if len(self.agents) > 1 and len(agents_for_delegation) > 0 and task.agent:
if not tools:
tools = []
Expand All @@ -965,7 +994,7 @@ def _log_task_start(self, task: Task, role: str = "None"):
task_name=task.name, task=task.description, agent=role, status="started"
)

def _update_manager_tools(self, task: Task, tools: List[Tool]):
def _update_manager_tools(self, task: Task, tools: Sequence[BaseTool]):
if self.manager_agent:
if task.agent:
tools = self._inject_delegation_tools(tools, task.agent, [task.agent])
Expand Down
6 changes: 4 additions & 2 deletions src/crewai/tools/agent_tools/agent_tools.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import List

from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.tools.base_tool import BaseTool
from crewai.utilities import I18N
Expand All @@ -9,11 +11,11 @@
class AgentTools:
"""Manager class for agent-related tools"""

def __init__(self, agents: list[BaseAgent], i18n: I18N = I18N()):
def __init__(self, agents: List[BaseAgent], i18n: I18N = I18N()):
self.agents = agents
self.i18n = i18n

def tools(self) -> list[BaseTool]:
def tools(self) -> List[BaseTool]:
"""Get all available agent tools"""
coworkers = ", ".join([f"{agent.role}" for agent in self.agents])

Expand Down
Loading