feat: implement hierarchical agent delegation with allowed_agents parameter

- Add allowed_agents field to BaseAgent class with validation
- Modify AgentTools to filter delegation targets based on allowed_agents
- Update Agent.get_delegation_tools to pass delegating agent context
- Support both role strings and agent instances in allowed_agents
- Implement case-insensitive role matching for flexibility
- Add comprehensive test coverage for all scenarios
- Maintain backward compatibility (None = allow all agents)
- Handle edge cases (empty list = no delegation allowed)

Addresses issue #2068 for controlled hierarchical delegation

Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
Devin AI
2025-06-17 16:33:52 +00:00
parent db1e9e9b9a
commit 8fd2867155
4 changed files with 322 additions and 7 deletions

View File

@@ -565,7 +565,7 @@ class Agent(BaseAgent):
def get_delegation_tools(self, agents: List[BaseAgent]): def get_delegation_tools(self, agents: List[BaseAgent]):
agent_tools = AgentTools(agents=agents) agent_tools = AgentTools(agents=agents)
tools = agent_tools.tools() tools = agent_tools.tools(delegating_agent=self)
return tools return tools
def get_multimodal_tools(self) -> Sequence[BaseTool]: def get_multimodal_tools(self) -> Sequence[BaseTool]:

View File

@@ -2,7 +2,7 @@ import uuid
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from copy import copy as shallow_copy from copy import copy as shallow_copy
from hashlib import md5 from hashlib import md5
from typing import Any, Callable, Dict, List, Optional, TypeVar from typing import Any, Callable, Dict, List, Optional, TypeVar, Union
from pydantic import ( from pydantic import (
UUID4, UUID4,
@@ -108,6 +108,10 @@ class BaseAgent(ABC, BaseModel):
default=False, default=False,
description="Enable agent to delegate and ask questions among each other.", description="Enable agent to delegate and ask questions among each other.",
) )
allowed_agents: Optional[List[Union[str, "BaseAgent"]]] = Field(
default=None,
description="List of agent roles or agent instances that this agent can delegate to. If None, can delegate to all agents when allow_delegation=True.",
)
tools: Optional[List[BaseTool]] = Field( tools: Optional[List[BaseTool]] = Field(
default_factory=list, description="Tools at agents' disposal" default_factory=list, description="Tools at agents' disposal"
) )
@@ -195,6 +199,22 @@ class BaseAgent(ABC, BaseModel):
) )
return processed_tools return processed_tools
@field_validator("allowed_agents")
@classmethod
def validate_allowed_agents(cls, allowed_agents: Optional[List[Union[str, "BaseAgent"]]]) -> Optional[List[Union[str, "BaseAgent"]]]:
"""Validate the allowed_agents list."""
if allowed_agents is None:
return None
if not isinstance(allowed_agents, list):
raise ValueError("allowed_agents must be a list of agent roles (strings) or agent instances")
for agent in allowed_agents:
if not isinstance(agent, (str, BaseAgent)):
raise ValueError("Each item in allowed_agents must be either a string (agent role) or a BaseAgent instance")
return allowed_agents
@model_validator(mode="after") @model_validator(mode="after")
def validate_and_set_attributes(self): def validate_and_set_attributes(self):
# Validate required fields # Validate required fields

View File

@@ -1,3 +1,5 @@
from typing import Optional
from crewai.agents.agent_builder.base_agent import BaseAgent from crewai.agents.agent_builder.base_agent import BaseAgent
from crewai.tools.base_tool import BaseTool from crewai.tools.base_tool import BaseTool
from crewai.utilities import I18N from crewai.utilities import I18N
@@ -13,20 +15,50 @@ class AgentTools:
self.agents = agents self.agents = agents
self.i18n = i18n self.i18n = i18n
def tools(self) -> list[BaseTool]: def tools(self, delegating_agent: Optional[BaseAgent] = None) -> list[BaseTool]:
"""Get all available agent tools""" """Get all available agent tools, filtered by delegating agent's allowed_agents if specified"""
coworkers = ", ".join([f"{agent.role}" for agent in self.agents]) available_agents = self._filter_allowed_agents(delegating_agent)
if not available_agents:
return []
coworkers = ", ".join([f"{agent.role}" for agent in available_agents])
delegate_tool = DelegateWorkTool( delegate_tool = DelegateWorkTool(
agents=self.agents, agents=available_agents,
i18n=self.i18n, i18n=self.i18n,
description=self.i18n.tools("delegate_work").format(coworkers=coworkers), # type: ignore description=self.i18n.tools("delegate_work").format(coworkers=coworkers), # type: ignore
) )
ask_tool = AskQuestionTool( ask_tool = AskQuestionTool(
agents=self.agents, agents=available_agents,
i18n=self.i18n, i18n=self.i18n,
description=self.i18n.tools("ask_question").format(coworkers=coworkers), # type: ignore description=self.i18n.tools("ask_question").format(coworkers=coworkers), # type: ignore
) )
return [delegate_tool, ask_tool] return [delegate_tool, ask_tool]
def _filter_allowed_agents(self, delegating_agent: Optional[BaseAgent]) -> list[BaseAgent]:
"""Filter agents based on the delegating agent's allowed_agents list"""
if delegating_agent is None:
return self.agents
if not hasattr(delegating_agent, 'allowed_agents') or delegating_agent.allowed_agents is None:
return self.agents
if not delegating_agent.allowed_agents:
return []
filtered_agents = []
for agent in self.agents:
for allowed in delegating_agent.allowed_agents:
if isinstance(allowed, str):
if agent.role.lower() == allowed.lower():
filtered_agents.append(agent)
break
elif isinstance(allowed, BaseAgent):
if agent is allowed:
filtered_agents.append(agent)
break
return filtered_agents

View File

@@ -0,0 +1,263 @@
"""Test allowed_agents functionality for hierarchical delegation."""
import pytest
from crewai.agent import Agent
from crewai.crew import Crew
from crewai.task import Task
from crewai.tools.agent_tools.agent_tools import AgentTools
@pytest.fixture
def agents():
"""Create test agents for delegation testing."""
manager = Agent(
role="Manager",
goal="Manage the team",
backstory="You are a team manager",
allow_delegation=True,
)
researcher = Agent(
role="Researcher",
goal="Research topics",
backstory="You are a researcher",
allow_delegation=False,
)
writer = Agent(
role="Writer",
goal="Write content",
backstory="You are a writer",
allow_delegation=False,
)
analyst = Agent(
role="Analyst",
goal="Analyze data",
backstory="You are an analyst",
allow_delegation=False,
)
return manager, researcher, writer, analyst
def test_allowed_agents_with_role_strings(agents):
"""Test allowed_agents with role strings."""
manager, researcher, writer, analyst = agents
manager.allowed_agents = ["Researcher", "Writer"]
agent_tools = AgentTools(agents=[researcher, writer, analyst])
tools = agent_tools.tools(delegating_agent=manager)
assert len(tools) == 2
delegate_tool = tools[0]
assert len(delegate_tool.agents) == 2
agent_roles = [agent.role for agent in delegate_tool.agents]
assert "Researcher" in agent_roles
assert "Writer" in agent_roles
assert "Analyst" not in agent_roles
def test_allowed_agents_with_agent_instances(agents):
"""Test allowed_agents with agent instances."""
manager, researcher, writer, analyst = agents
manager.allowed_agents = [researcher, analyst]
agent_tools = AgentTools(agents=[researcher, writer, analyst])
tools = agent_tools.tools(delegating_agent=manager)
assert len(tools) == 2
delegate_tool = tools[0]
assert len(delegate_tool.agents) == 2
assert researcher in delegate_tool.agents
assert analyst in delegate_tool.agents
assert writer not in delegate_tool.agents
def test_allowed_agents_mixed_types(agents):
"""Test allowed_agents with mixed role strings and agent instances."""
manager, researcher, writer, analyst = agents
manager.allowed_agents = ["Researcher", writer]
agent_tools = AgentTools(agents=[researcher, writer, analyst])
tools = agent_tools.tools(delegating_agent=manager)
delegate_tool = tools[0]
assert len(delegate_tool.agents) == 2
assert researcher in delegate_tool.agents
assert writer in delegate_tool.agents
assert analyst not in delegate_tool.agents
def test_allowed_agents_empty_list(agents):
"""Test allowed_agents with empty list (no delegation allowed)."""
manager, researcher, writer, analyst = agents
manager.allowed_agents = []
agent_tools = AgentTools(agents=[researcher, writer, analyst])
tools = agent_tools.tools(delegating_agent=manager)
assert len(tools) == 0
def test_allowed_agents_none(agents):
"""Test allowed_agents with None (delegate to all agents)."""
manager, researcher, writer, analyst = agents
manager.allowed_agents = None
agent_tools = AgentTools(agents=[researcher, writer, analyst])
tools = agent_tools.tools(delegating_agent=manager)
delegate_tool = tools[0]
assert len(delegate_tool.agents) == 3
assert researcher in delegate_tool.agents
assert writer in delegate_tool.agents
assert analyst in delegate_tool.agents
def test_allowed_agents_case_insensitive_matching(agents):
"""Test that role matching is case-insensitive."""
manager, researcher, writer, analyst = agents
manager.allowed_agents = ["researcher", "WRITER"]
agent_tools = AgentTools(agents=[researcher, writer, analyst])
tools = agent_tools.tools(delegating_agent=manager)
delegate_tool = tools[0]
assert len(delegate_tool.agents) == 2
assert researcher in delegate_tool.agents
assert writer in delegate_tool.agents
def test_allowed_agents_validation():
"""Test validation of allowed_agents field."""
agent = Agent(
role="Test",
goal="Test",
backstory="Test",
allowed_agents=["Role1", "Role2"]
)
assert agent.allowed_agents == ["Role1", "Role2"]
agent = Agent(
role="Test",
goal="Test",
backstory="Test",
allowed_agents=None
)
assert agent.allowed_agents is None
with pytest.raises(ValueError, match="allowed_agents must be a list"):
Agent(
role="Test",
goal="Test",
backstory="Test",
allowed_agents="invalid"
)
with pytest.raises(ValueError, match="must be either a string"):
Agent(
role="Test",
goal="Test",
backstory="Test",
allowed_agents=[123]
)
def test_crew_integration_with_allowed_agents(agents):
"""Test integration with Crew class."""
manager, researcher, writer, analyst = agents
manager.allowed_agents = ["Researcher"]
task = Task(
description="Research AI trends",
expected_output="Research report",
agent=manager
)
crew = Crew(
agents=[manager, researcher, writer, analyst],
tasks=[task]
)
tools = crew._prepare_tools(manager, task, [])
delegation_tools = [tool for tool in tools if "Delegate work" in tool.name or "Ask question" in tool.name]
if delegation_tools:
delegate_tool = next(tool for tool in delegation_tools if "Delegate work" in tool.name)
assert len(delegate_tool.agents) == 1
assert delegate_tool.agents[0].role == "Researcher"
def test_backward_compatibility_no_allowed_agents(agents):
"""Test that agents without allowed_agents work as before."""
manager, researcher, writer, analyst = agents
assert manager.allowed_agents is None
agent_tools = AgentTools(agents=[researcher, writer, analyst])
tools = agent_tools.tools(delegating_agent=manager)
assert len(tools) == 2
delegate_tool = tools[0]
assert len(delegate_tool.agents) == 3
assert researcher in delegate_tool.agents
assert writer in delegate_tool.agents
assert analyst in delegate_tool.agents
def test_no_delegating_agent_parameter(agents):
"""Test AgentTools.tools() without delegating_agent parameter."""
manager, researcher, writer, analyst = agents
agent_tools = AgentTools(agents=[researcher, writer, analyst])
tools = agent_tools.tools() # No delegating_agent parameter
assert len(tools) == 2
delegate_tool = tools[0]
assert len(delegate_tool.agents) == 3
def test_allowed_agents_with_nonexistent_role(agents):
"""Test allowed_agents with role that doesn't exist in available agents."""
manager, researcher, writer, analyst = agents
manager.allowed_agents = ["Researcher", "NonExistentRole"]
agent_tools = AgentTools(agents=[researcher, writer, analyst])
tools = agent_tools.tools(delegating_agent=manager)
delegate_tool = tools[0]
assert len(delegate_tool.agents) == 1
assert researcher in delegate_tool.agents
def test_allowed_agents_with_nonexistent_instance(agents):
"""Test allowed_agents with agent instance that doesn't exist in available agents."""
manager, researcher, writer, analyst = agents
other_agent = Agent(
role="Other",
goal="Other goal",
backstory="Other backstory"
)
manager.allowed_agents = [researcher, other_agent]
agent_tools = AgentTools(agents=[researcher, writer, analyst])
tools = agent_tools.tools(delegating_agent=manager)
delegate_tool = tools[0]
assert len(delegate_tool.agents) == 1
assert researcher in delegate_tool.agents
assert other_agent not in delegate_tool.agents