Compare commits

...

13 Commits

Author SHA1 Message Date
João Moura
70b04ab782 Merge branch 'main' into devin/1735586761-fix-hierarchical-delegation 2024-12-30 20:54:57 -03:00
Devin AI
1302c40607 Fix: Sort imports in test_hierarchical_delegation.py
Co-Authored-By: Joe Moura <joao@crewai.com>
2024-12-30 20:29:33 +00:00
Devin AI
f9d11e5cd8 Fix: Update delegation tool handling in hierarchical mode for proper agent recognition
Co-Authored-By: Joe Moura <joao@crewai.com>
2024-12-30 20:27:08 +00:00
Devin AI
c59c4afc16 Fix: Pass task context through delegation chain for proper coworker recognition
Co-Authored-By: Joe Moura <joao@crewai.com>
2024-12-30 20:26:43 +00:00
devin-ai-integration[bot]
ba0965ef87 fix: add tiktoken as explicit dependency and document Rust requirement (#1826)
* feat: add tiktoken as explicit dependency and document Rust requirement

- Add tiktoken>=0.8.0 as explicit dependency to ensure pre-built wheels are used
- Document Rust compiler requirement as fallback in README.md
- Addresses issue #1824 tiktoken build failure

Co-Authored-By: Joe Moura <joao@crewai.com>

* fix: adjust tiktoken version to ~=0.7.0 for dependency compatibility

- Update tiktoken dependency to ~=0.7.0 to resolve conflict with embedchain
- Maintain compatibility with crewai-tools dependency chain
- Addresses CI build failures

Co-Authored-By: Joe Moura <joao@crewai.com>

* docs: add troubleshooting section and make tiktoken optional

Co-Authored-By: Joe Moura <joao@crewai.com>

* Update README.md

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Joe Moura <joao@crewai.com>
Co-authored-by: João Moura <joaomdmoura@gmail.com>
2024-12-30 17:10:56 -03:00
devin-ai-integration[bot]
d85898cf29 fix(manager_llm): handle coworker role name case/whitespace properly (#1820)
* fix(manager_llm): handle coworker role name case/whitespace properly

- Add .strip() to agent name and role comparisons in base_agent_tools.py
- Add test case for varied role name cases and whitespace
- Fix issue #1503 with manager LLM delegation

Co-Authored-By: Joe Moura <joao@crewai.com>

* fix(manager_llm): improve error handling and add debug logging

- Add debug logging for better observability
- Add sanitize_agent_name helper method
- Enhance error messages with more context
- Add parameterized tests for edge cases:
  - Embedded quotes
  - Trailing newlines
  - Multiple whitespace
  - Case variations
  - None values
- Improve error handling with specific exceptions

Co-Authored-By: Joe Moura <joao@crewai.com>

* style: fix import sorting in base_agent_tools and test_manager_llm_delegation

Co-Authored-By: Joe Moura <joao@crewai.com>

* fix(manager_llm): improve whitespace normalization in role name matching

Co-Authored-By: Joe Moura <joao@crewai.com>

* style: fix import sorting in base_agent_tools and test_manager_llm_delegation

Co-Authored-By: Joe Moura <joao@crewai.com>

* fix(manager_llm): add error message template for agent tool execution errors

Co-Authored-By: Joe Moura <joao@crewai.com>

* style: fix import sorting in test_manager_llm_delegation.py

Co-Authored-By: Joe Moura <joao@crewai.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Joe Moura <joao@crewai.com>
2024-12-30 16:58:18 -03:00
Devin AI
c08b4e5d9c Fix: Apply Ruff auto-fix for import formatting
Co-Authored-By: Joe Moura <joao@crewai.com>
2024-12-30 19:39:18 +00:00
Devin AI
a9341b31f5 Fix: Add blank line between stdlib and third-party imports
Co-Authored-By: Joe Moura <joao@crewai.com>
2024-12-30 19:35:05 +00:00
Devin AI
1153e8e498 Fix: Remove extra blank line in imports for test_hierarchical_delegation.py
Co-Authored-By: Joe Moura <joao@crewai.com>
2024-12-30 19:33:19 +00:00
Devin AI
4764e114ad Fix: Alphabetize imports in test_hierarchical_delegation.py
Co-Authored-By: Joe Moura <joao@crewai.com>
2024-12-30 19:31:02 +00:00
Devin AI
a59c13cb58 Fix: Sort imports in test_hierarchical_delegation.py
Co-Authored-By: Joe Moura <joao@crewai.com>
2024-12-30 19:28:36 +00:00
Devin AI
0a137ca9dd Fix delegate coworker bug in hierarchical mode and add test
- Update _prepare_tools and _update_manager_tools to handle async execution in hierarchical mode
- Add test cases to verify delegation tool behavior
- Ensure proper tool updates during async task execution

Co-Authored-By: Joe Moura <joao@crewai.com>
2024-12-30 19:26:01 +00:00
devin-ai-integration[bot]
73f328860b Fix interpolation for output_file in Task (#1803) (#1814)
* fix: interpolate output_file attribute from YAML

Co-Authored-By: Joe Moura <joao@crewai.com>

* fix: add security validation for output_file paths

Co-Authored-By: Joe Moura <joao@crewai.com>

* fix: add _original_output_file private attribute to fix type-checker error

Co-Authored-By: Joe Moura <joao@crewai.com>

* fix: update interpolate_only to handle None inputs and remove duplicate attribute

Co-Authored-By: Joe Moura <joao@crewai.com>

* fix: improve output_file validation and error messages

Co-Authored-By: Joe Moura <joao@crewai.com>

* test: add end-to-end tests for output_file functionality

Co-Authored-By: Joe Moura <joao@crewai.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Joe Moura <joao@crewai.com>
2024-12-29 01:57:59 -03:00
14 changed files with 949 additions and 57 deletions

View File

@@ -85,7 +85,6 @@ First, install CrewAI:
```shell
pip install crewai
```
If you want to install the 'crewai' package along with its optional features that include additional tools for agents, you can do so by using the following command:
```shell
@@ -93,6 +92,22 @@ pip install 'crewai[tools]'
```
The command above installs the basic package and also adds extra components which require more dependencies to function.
### Troubleshooting Dependencies
If you encounter issues during installation or usage, here are some common solutions:
#### Common Issues
1. **ModuleNotFoundError: No module named 'tiktoken'**
- Install tiktoken explicitly: `pip install 'crewai[embeddings]'`
- If using embedchain or other tools: `pip install 'crewai[tools]'`
2. **Failed building wheel for tiktoken**
- Ensure Rust compiler is installed (see installation steps above)
- For Windows: Verify Visual C++ Build Tools are installed
- Try upgrading pip: `pip install --upgrade pip`
- If issues persist, use a pre-built wheel: `pip install tiktoken --prefer-binary`
### 2. Setting Up Your Crew with the YAML Configuration
To create a new CrewAI project, run the following CLI (Command Line Interface) command:

View File

@@ -8,27 +8,38 @@ authors = [
{ name = "Joao Moura", email = "joao@crewai.com" }
]
dependencies = [
# Core Dependencies
"pydantic>=2.4.2",
"openai>=1.13.3",
"litellm>=1.44.22",
"instructor>=1.3.3",
# Text Processing
"pdfplumber>=0.11.4",
"regex>=2024.9.11",
# Telemetry and Monitoring
"opentelemetry-api>=1.22.0",
"opentelemetry-sdk>=1.22.0",
"opentelemetry-exporter-otlp-proto-http>=1.22.0",
"instructor>=1.3.3",
"regex>=2024.9.11",
"click>=8.1.7",
# Data Handling
"chromadb>=0.5.23",
"openpyxl>=3.1.5",
"pyvis>=0.3.2",
# Authentication and Security
"auth0-python>=4.7.1",
"python-dotenv>=1.0.0",
# Configuration and Utils
"click>=8.1.7",
"appdirs>=1.4.4",
"jsonref>=1.1.0",
"json-repair>=0.25.2",
"auth0-python>=4.7.1",
"litellm>=1.44.22",
"pyvis>=0.3.2",
"uv>=0.4.25",
"tomli-w>=1.1.0",
"tomli>=2.0.2",
"chromadb>=0.5.23",
"pdfplumber>=0.11.4",
"openpyxl>=3.1.5",
"blinker>=1.9.0",
]
@@ -39,6 +50,9 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = ["crewai-tools>=0.17.0"]
embeddings = [
"tiktoken~=0.7.0"
]
agentops = ["agentops>=0.3.0"]
fastembed = ["fastembed>=0.4.1"]
pdfplumber = [

View File

@@ -406,8 +406,17 @@ class Agent(BaseAgent):
callbacks=[TokenCalcHandler(self._token_process)],
)
def get_delegation_tools(self, agents: List[BaseAgent]):
agent_tools = AgentTools(agents=agents)
def get_delegation_tools(self, agents: List[BaseAgent], task: Optional[Task] = None) -> List[BaseTool]:
"""Get the delegation tools for this agent.
Args:
agents: List of agents that can be delegated to
task: Optional task context for delegation
Returns:
List of delegation tools
"""
agent_tools = AgentTools(agents=agents, task=task, i18n=self.i18n)
tools = agent_tools.tools()
return tools

View File

@@ -232,7 +232,7 @@ class BaseAgent(ABC, BaseModel):
pass
@abstractmethod
def get_delegation_tools(self, agents: List["BaseAgent"]) -> List[BaseTool]:
def get_delegation_tools(self, agents: List["BaseAgent"], task=None) -> List[BaseTool]:
"""Set the task tools that init BaseAgenTools class."""
pass

View File

@@ -798,15 +798,34 @@ class Crew(BaseModel):
return None
def _prepare_tools(self, agent: BaseAgent, task: Task, tools: List[Tool]) -> List[Tool]:
"""Prepare tools for an agent, including delegation tools if allowed.
Args:
agent: Agent that will receive the tools
task: Task being executed
tools: List of existing tools
Returns:
List of tools with delegation and other tools added
"""
# Add delegation tools if agent allows delegation
if agent.allow_delegation:
if self.process == Process.hierarchical:
if self.manager_agent:
tools = self._update_manager_tools(task, tools)
# For hierarchical process, handle both manager and regular agent tools
if agent == self.manager_agent:
# Manager can delegate to all regular agents
tools = self._inject_delegation_tools(tools, agent, [a for a in self.agents if a != agent], task=task)
elif task and task.async_execution and task.agent:
# For async tasks in hierarchical mode, only allow delegation to the task's assigned agent
tools = self._inject_delegation_tools(tools, agent, [task.agent], task=task)
else:
# Regular agents can delegate to manager and other agents
delegation_agents = [self.manager_agent] + [a for a in self.agents if a != agent]
tools = self._inject_delegation_tools(tools, agent, delegation_agents, task=task)
else:
raise ValueError("Manager agent is required for hierarchical process.")
elif agent and agent.allow_delegation:
else:
tools = self._add_delegation_tools(task, tools)
# Add code execution tools if agent allows code execution
@@ -839,8 +858,19 @@ class Crew(BaseModel):
return tools
def _inject_delegation_tools(self, tools: List[Tool], task_agent: BaseAgent, agents: List[BaseAgent]):
delegation_tools = task_agent.get_delegation_tools(agents)
def _inject_delegation_tools(self, tools: List[Tool], task_agent: BaseAgent, agents: List[BaseAgent], task=None):
"""Inject delegation tools for the given agent.
Args:
tools: List of existing tools
task_agent: Agent that will receive the delegation tools
agents: List of agents that can be delegated to
task: Optional task context for delegation
Returns:
List of tools with delegation tools added
"""
delegation_tools = task_agent.get_delegation_tools(agents, task=task)
return self._merge_tools(tools, delegation_tools)
def _add_multimodal_tools(self, agent: BaseAgent, tools: List[Tool]):
@@ -852,8 +882,17 @@ class Crew(BaseModel):
return self._merge_tools(tools, code_tools)
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:
agents_for_delegation = []
if self.process == Process.hierarchical and self.manager_agent:
# In hierarchical mode, allow delegation to manager if the task agent isn't the manager
if task.agent != self.manager_agent:
agents_for_delegation = [self.manager_agent]
# Also include regular agents for delegation
agents_for_delegation.extend([agent for agent in self.agents if agent != task.agent])
else:
agents_for_delegation = [agent for agent in self.agents if agent != task.agent]
if len(agents_for_delegation) > 0 and task.agent:
if not tools:
tools = []
tools = self._inject_delegation_tools(tools, task.agent, agents_for_delegation)
@@ -868,9 +907,23 @@ class Crew(BaseModel):
def _update_manager_tools(self, task: Task, tools: List[Tool]):
if self.manager_agent:
if task.agent:
tools = self._inject_delegation_tools(tools, task.agent, [task.agent])
# In hierarchical mode with async execution, only allow delegation to the task's assigned agent
if task.async_execution and self.process == Process.hierarchical:
tools = self._inject_delegation_tools(tools, task.agent, [task.agent] if task.agent else [])
else:
# In hierarchical mode, allow bidirectional delegation
if self.process == Process.hierarchical:
# For non-manager agents, allow delegation to manager and other agents
if task.agent != self.manager_agent:
delegation_agents = [self.manager_agent] + self.agents
tools = self._inject_delegation_tools(tools, task.agent, [a for a in delegation_agents if a != task.agent])
else:
# For manager, allow delegation to all agents
tools = self._inject_delegation_tools(tools, task.agent, [a for a in self.agents if a != task.agent])
else:
tools = self._inject_delegation_tools(tools, task.agent, self.agents, task=task)
else:
tools = self._inject_delegation_tools(tools, self.manager_agent, self.agents)
tools = self._inject_delegation_tools(tools, self.manager_agent, self.agents, task=task)
return tools
def _get_context(self, task: Task, task_outputs: List[TaskOutput]):

View File

@@ -179,6 +179,7 @@ class Task(BaseModel):
_execution_span: Optional[Span] = PrivateAttr(default=None)
_original_description: Optional[str] = PrivateAttr(default=None)
_original_expected_output: Optional[str] = PrivateAttr(default=None)
_original_output_file: Optional[str] = PrivateAttr(default=None)
_thread: Optional[threading.Thread] = PrivateAttr(default=None)
_execution_time: Optional[float] = PrivateAttr(default=None)
@@ -213,8 +214,46 @@ class Task(BaseModel):
@field_validator("output_file")
@classmethod
def output_file_validation(cls, value: str) -> str:
"""Validate the output file path by removing the / from the beginning of the path."""
def output_file_validation(cls, value: Optional[str]) -> Optional[str]:
"""Validate the output file path.
Args:
value: The output file path to validate. Can be None or a string.
If the path contains template variables (e.g. {var}), leading slashes are preserved.
For regular paths, leading slashes are stripped.
Returns:
The validated and potentially modified path, or None if no path was provided.
Raises:
ValueError: If the path contains invalid characters, path traversal attempts,
or other security concerns.
"""
if value is None:
return None
# Basic security checks
if ".." in value:
raise ValueError("Path traversal attempts are not allowed in output_file paths")
# Check for shell expansion first
if value.startswith('~') or value.startswith('$'):
raise ValueError("Shell expansion characters are not allowed in output_file paths")
# Then check other shell special characters
if any(char in value for char in ['|', '>', '<', '&', ';']):
raise ValueError("Shell special characters are not allowed in output_file paths")
# Don't strip leading slash if it's a template path with variables
if "{" in value or "}" in value:
# Validate template variable format
template_vars = [part.split("}")[0] for part in value.split("{")[1:]]
for var in template_vars:
if not var.isidentifier():
raise ValueError(f"Invalid template variable name: {var}")
return value
# Strip leading slash for regular paths
if value.startswith("/"):
return value[1:]
return value
@@ -393,27 +432,89 @@ class Task(BaseModel):
tasks_slices = [self.description, output]
return "\n".join(tasks_slices)
def interpolate_inputs(self, inputs: Dict[str, Any]) -> None:
"""Interpolate inputs into the task description and expected output."""
def interpolate_inputs(self, inputs: Dict[str, Union[str, int, float]]) -> None:
"""Interpolate inputs into the task description, expected output, and output file path.
Args:
inputs: Dictionary mapping template variables to their values.
Supported value types are strings, integers, and floats.
Raises:
ValueError: If a required template variable is missing from inputs.
"""
if self._original_description is None:
self._original_description = self.description
if self._original_expected_output is None:
self._original_expected_output = self.expected_output
if self.output_file is not None and self._original_output_file is None:
self._original_output_file = self.output_file
if inputs:
if not inputs:
return
try:
self.description = self._original_description.format(**inputs)
except KeyError as e:
raise ValueError(f"Missing required template variable '{e.args[0]}' in description") from e
except ValueError as e:
raise ValueError(f"Error interpolating description: {str(e)}") from e
try:
self.expected_output = self.interpolate_only(
input_string=self._original_expected_output, inputs=inputs
)
except (KeyError, ValueError) as e:
raise ValueError(f"Error interpolating expected_output: {str(e)}") from e
def interpolate_only(self, input_string: str, inputs: Dict[str, Any]) -> str:
"""Interpolate placeholders (e.g., {key}) in a string while leaving JSON untouched."""
escaped_string = input_string.replace("{", "{{").replace("}", "}}")
if self.output_file is not None:
try:
self.output_file = self.interpolate_only(
input_string=self._original_output_file, inputs=inputs
)
except (KeyError, ValueError) as e:
raise ValueError(f"Error interpolating output_file path: {str(e)}") from e
for key in inputs.keys():
escaped_string = escaped_string.replace(f"{{{{{key}}}}}", f"{{{key}}}")
def interpolate_only(self, input_string: Optional[str], inputs: Dict[str, Union[str, int, float]]) -> str:
"""Interpolate placeholders (e.g., {key}) in a string while leaving JSON untouched.
Args:
input_string: The string containing template variables to interpolate.
Can be None or empty, in which case an empty string is returned.
inputs: Dictionary mapping template variables to their values.
Supported value types are strings, integers, and floats.
If input_string is empty or has no placeholders, inputs can be empty.
Returns:
The interpolated string with all template variables replaced with their values.
Empty string if input_string is None or empty.
Raises:
ValueError: If a required template variable is missing from inputs.
KeyError: If a template variable is not found in the inputs dictionary.
"""
if input_string is None or not input_string:
return ""
if "{" not in input_string and "}" not in input_string:
return input_string
if not inputs:
raise ValueError("Inputs dictionary cannot be empty when interpolating variables")
return escaped_string.format(**inputs)
try:
# Validate input types
for key, value in inputs.items():
if not isinstance(value, (str, int, float)):
raise ValueError(f"Value for key '{key}' must be a string, integer, or float, got {type(value).__name__}")
escaped_string = input_string.replace("{", "{{").replace("}", "}}")
for key in inputs.keys():
escaped_string = escaped_string.replace(f"{{{{{key}}}}}", f"{{{key}}}")
return escaped_string.format(**inputs)
except KeyError as e:
raise KeyError(f"Template variable '{e.args[0]}' not found in inputs dictionary") from e
except ValueError as e:
raise ValueError(f"Error during string interpolation: {str(e)}") from e
def increment_tools_errors(self) -> None:
"""Increment the tools errors counter."""

View File

@@ -9,24 +9,36 @@ from .delegate_work_tool import DelegateWorkTool
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(), task=None):
self.agents = agents
self.i18n = i18n
self.task = task
def tools(self) -> list[BaseTool]:
"""Get all available agent tools"""
coworkers = ", ".join([f"{agent.role}" for agent in self.agents])
# Format coworkers list based on agents and task context
if len(self.agents) == 1:
coworkers = self.agents[0].role
elif self.task and hasattr(self.task, 'async_execution') and self.task.async_execution and hasattr(self.task, 'agent') and self.task.agent:
# For async tasks with a specific agent, only show that agent
coworkers = self.task.agent.role
else:
# Show all agents for non-async tasks or when no specific agent is assigned
coworkers = ", ".join([agent.role for agent in self.agents])
# Ensure coworkers list doesn't have extra spaces or newlines
coworkers = coworkers.strip()
delegate_tool = DelegateWorkTool(
agents=self.agents,
i18n=self.i18n,
description=self.i18n.tools("delegate_work").format(coworkers=coworkers), # type: ignore
description=f"Delegate a specific task to one of the following coworkers: {coworkers}\n",
)
ask_tool = AskQuestionTool(
agents=self.agents,
i18n=self.i18n,
description=self.i18n.tools("ask_question").format(coworkers=coworkers), # type: ignore
description=f"Ask a specific question to one of the following coworkers: {coworkers}\n",
)
return [delegate_tool, ask_tool]

View File

@@ -1,3 +1,4 @@
import logging
from typing import Optional, Union
from pydantic import Field
@@ -7,6 +8,8 @@ from crewai.task import Task
from crewai.tools.base_tool import BaseTool
from crewai.utilities import I18N
logger = logging.getLogger(__name__)
class BaseAgentTool(BaseTool):
"""Base class for agent-related tools"""
@@ -16,6 +19,25 @@ class BaseAgentTool(BaseTool):
default_factory=I18N, description="Internationalization settings"
)
def sanitize_agent_name(self, name: str) -> str:
"""
Sanitize agent role name by normalizing whitespace and setting to lowercase.
Converts all whitespace (including newlines) to single spaces and removes quotes.
Args:
name (str): The agent role name to sanitize
Returns:
str: The sanitized agent role name, with whitespace normalized,
converted to lowercase, and quotes removed
"""
if not name:
return ""
# Normalize all whitespace (including newlines) to single spaces
normalized = " ".join(name.split())
# Remove quotes and convert to lowercase
return normalized.replace('"', "").casefold()
def _get_coworker(self, coworker: Optional[str], **kwargs) -> Optional[str]:
coworker = coworker or kwargs.get("co_worker") or kwargs.get("coworker")
if coworker:
@@ -25,11 +47,27 @@ class BaseAgentTool(BaseTool):
return coworker
def _execute(
self, agent_name: Union[str, None], task: str, context: Union[str, None]
self,
agent_name: Optional[str],
task: str,
context: Optional[str] = None
) -> str:
"""
Execute delegation to an agent with case-insensitive and whitespace-tolerant matching.
Args:
agent_name: Name/role of the agent to delegate to (case-insensitive)
task: The specific question or task to delegate
context: Optional additional context for the task execution
Returns:
str: The execution result from the delegated agent or an error message
if the agent cannot be found
"""
try:
if agent_name is None:
agent_name = ""
logger.debug("No agent name provided, using empty string")
# It is important to remove the quotes from the agent name.
# The reason we have to do this is because less-powerful LLM's
@@ -38,31 +76,49 @@ class BaseAgentTool(BaseTool):
# {"task": "....", "coworker": "....
# when it should look like this:
# {"task": "....", "coworker": "...."}
agent_name = agent_name.casefold().replace('"', "").replace("\n", "")
sanitized_name = self.sanitize_agent_name(agent_name)
logger.debug(f"Sanitized agent name from '{agent_name}' to '{sanitized_name}'")
available_agents = [agent.role for agent in self.agents]
logger.debug(f"Available agents: {available_agents}")
agent = [ # type: ignore # Incompatible types in assignment (expression has type "list[BaseAgent]", variable has type "str | None")
available_agent
for available_agent in self.agents
if available_agent.role.casefold().replace("\n", "") == agent_name
if self.sanitize_agent_name(available_agent.role) == sanitized_name
]
except Exception as _:
logger.debug(f"Found {len(agent)} matching agents for role '{sanitized_name}'")
except (AttributeError, ValueError) as e:
# Handle specific exceptions that might occur during role name processing
return self.i18n.errors("agent_tool_unexisting_coworker").format(
coworkers="\n".join(
[f"- {agent.role.casefold()}" for agent in self.agents]
)
[f"- {self.sanitize_agent_name(agent.role)}" for agent in self.agents]
),
error=str(e)
)
if not agent:
# No matching agent found after sanitization
return self.i18n.errors("agent_tool_unexisting_coworker").format(
coworkers="\n".join(
[f"- {agent.role.casefold()}" for agent in self.agents]
)
[f"- {self.sanitize_agent_name(agent.role)}" for agent in self.agents]
),
error=f"No agent found with role '{sanitized_name}'"
)
agent = agent[0]
task_with_assigned_agent = Task( # type: ignore # Incompatible types in assignment (expression has type "Task", variable has type "str")
description=task,
agent=agent,
expected_output=agent.i18n.slice("manager_request"),
i18n=agent.i18n,
)
return agent.execute_task(task_with_assigned_agent, context)
try:
task_with_assigned_agent = Task(
description=task,
agent=agent,
expected_output=agent.i18n.slice("manager_request"),
i18n=agent.i18n,
)
logger.debug(f"Created task for agent '{self.sanitize_agent_name(agent.role)}': {task}")
return agent.execute_task(task_with_assigned_agent, context)
except Exception as e:
# Handle task creation or execution errors
return self.i18n.errors("agent_tool_execution_error").format(
agent_role=self.sanitize_agent_name(agent.role),
error=str(e)
)

View File

@@ -33,7 +33,8 @@
"tool_usage_error": "I encountered an error: {error}",
"tool_arguments_error": "Error: the Action Input is not a valid key, value dictionary.",
"wrong_tool_name": "You tried to use the tool {tool}, but it doesn't exist. You must use one of the following tools, use one at time: {tools}.",
"tool_usage_exception": "I encountered an error while trying to use the tool. This was the error: {error}.\n Tool {tool} accepts these inputs: {tool_inputs}"
"tool_usage_exception": "I encountered an error while trying to use the tool. This was the error: {error}.\n Tool {tool} accepts these inputs: {tool_inputs}",
"agent_tool_execution_error": "Error executing task with agent '{agent_role}'. Error: {error}"
},
"tools": {
"delegate_work": "Delegate a specific task to one of the following coworkers: {coworkers}\nThe input to this tool should be the coworker, the task you want them to do, and ALL necessary context to execute the task, they know nothing about the task, so share absolute everything you know, don't reference things but instead explain them.",

View File

@@ -0,0 +1,243 @@
interactions:
- request:
body: !!binary |
CuIcCiQKIgoMc2VydmljZS5uYW1lEhIKEGNyZXdBSS10ZWxlbWV0cnkSuRwKEgoQY3Jld2FpLnRl
bGVtZXRyeRKjBwoQXK7w4+uvyEkrI9D5qyvcJxII5UmQ7hmczdIqDENyZXcgQ3JlYXRlZDABOfxQ
/hs4jBUYQUi3DBw4jBUYShoKDmNyZXdhaV92ZXJzaW9uEggKBjAuODYuMEoaCg5weXRob25fdmVy
c2lvbhIICgYzLjEyLjdKLgoIY3Jld19rZXkSIgogYzk3YjVmZWI1ZDFiNjZiYjU5MDA2YWFhMDFh
MjljZDZKMQoHY3Jld19pZBImCiRkZjY3NGMwYi1hOTc0LTQ3NTAtYjlkMS0yZWQxNjM3MzFiNTZK
HAoMY3Jld19wcm9jZXNzEgwKCnNlcXVlbnRpYWxKEQoLY3Jld19tZW1vcnkSAhAAShoKFGNyZXdf
bnVtYmVyX29mX3Rhc2tzEgIYAUobChVjcmV3X251bWJlcl9vZl9hZ2VudHMSAhgBStECCgtjcmV3
X2FnZW50cxLBAgq+Alt7ImtleSI6ICIwN2Q5OWI2MzA0MTFkMzVmZDkwNDdhNTMyZDUzZGRhNyIs
ICJpZCI6ICI5MDYwYTQ2Zi02MDY3LTQ1N2MtOGU3ZC04NjAyN2YzY2U5ZDUiLCAicm9sZSI6ICJS
ZXNlYXJjaGVyIiwgInZlcmJvc2U/IjogZmFsc2UsICJtYXhfaXRlciI6IDIwLCAibWF4X3JwbSI6
IG51bGwsICJmdW5jdGlvbl9jYWxsaW5nX2xsbSI6ICIiLCAibGxtIjogImdwdC00by1taW5pIiwg
ImRlbGVnYXRpb25fZW5hYmxlZD8iOiBmYWxzZSwgImFsbG93X2NvZGVfZXhlY3V0aW9uPyI6IGZh
bHNlLCAibWF4X3JldHJ5X2xpbWl0IjogMiwgInRvb2xzX25hbWVzIjogW119XUr/AQoKY3Jld190
YXNrcxLwAQrtAVt7ImtleSI6ICI2Mzk5NjUxN2YzZjNmMWM5NGQ2YmI2MTdhYTBiMWM0ZiIsICJp
ZCI6ICJjYTA4ZjkyOS0yMmI0LTQyZmQtYjViMC05N2M3MjM0ZDk5OTEiLCAiYXN5bmNfZXhlY3V0
aW9uPyI6IGZhbHNlLCAiaHVtYW5faW5wdXQ/IjogZmFsc2UsICJhZ2VudF9yb2xlIjogIlJlc2Vh
cmNoZXIiLCAiYWdlbnRfa2V5IjogIjA3ZDk5YjYzMDQxMWQzNWZkOTA0N2E1MzJkNTNkZGE3Iiwg
InRvb2xzX25hbWVzIjogW119XXoCGAGFAQABAAASjgIKEOTJZh9R45IwgGVg9cinZmISCJopKRMf
bpMJKgxUYXNrIENyZWF0ZWQwATlG+zQcOIwVGEHk0zUcOIwVGEouCghjcmV3X2tleRIiCiBjOTdi
NWZlYjVkMWI2NmJiNTkwMDZhYWEwMWEyOWNkNkoxCgdjcmV3X2lkEiYKJGRmNjc0YzBiLWE5NzQt
NDc1MC1iOWQxLTJlZDE2MzczMWI1NkouCgh0YXNrX2tleRIiCiA2Mzk5NjUxN2YzZjNmMWM5NGQ2
YmI2MTdhYTBiMWM0ZkoxCgd0YXNrX2lkEiYKJGNhMDhmOTI5LTIyYjQtNDJmZC1iNWIwLTk3Yzcy
MzRkOTk5MXoCGAGFAQABAAASowcKEEvwrN8+tNMIBwtnA+ip7jASCI78Hrh2wlsBKgxDcmV3IENy
ZWF0ZWQwATkcRqYeOIwVGEE8erQeOIwVGEoaCg5jcmV3YWlfdmVyc2lvbhIICgYwLjg2LjBKGgoO
cHl0aG9uX3ZlcnNpb24SCAoGMy4xMi43Si4KCGNyZXdfa2V5EiIKIDhjMjc1MmY0OWU1YjlkMmI2
OGNiMzVjYWM4ZmNjODZkSjEKB2NyZXdfaWQSJgokZmRkYzA4ZTMtNDUyNi00N2Q2LThlNWMtNjY0
YzIyMjc4ZDgyShwKDGNyZXdfcHJvY2VzcxIMCgpzZXF1ZW50aWFsShEKC2NyZXdfbWVtb3J5EgIQ
AEoaChRjcmV3X251bWJlcl9vZl90YXNrcxICGAFKGwoVY3Jld19udW1iZXJfb2ZfYWdlbnRzEgIY
AUrRAgoLY3Jld19hZ2VudHMSwQIKvgJbeyJrZXkiOiAiOGJkMjEzOWI1OTc1MTgxNTA2ZTQxZmQ5
YzQ1NjNkNzUiLCAiaWQiOiAiY2UxNjA2YjktMjdiOS00ZDc4LWEyODctNDZiMDNlZDg3ZTA1Iiwg
InJvbGUiOiAiUmVzZWFyY2hlciIsICJ2ZXJib3NlPyI6IGZhbHNlLCAibWF4X2l0ZXIiOiAyMCwg
Im1heF9ycG0iOiBudWxsLCAiZnVuY3Rpb25fY2FsbGluZ19sbG0iOiAiIiwgImxsbSI6ICJncHQt
NG8tbWluaSIsICJkZWxlZ2F0aW9uX2VuYWJsZWQ/IjogZmFsc2UsICJhbGxvd19jb2RlX2V4ZWN1
dGlvbj8iOiBmYWxzZSwgIm1heF9yZXRyeV9saW1pdCI6IDIsICJ0b29sc19uYW1lcyI6IFtdfV1K
/wEKCmNyZXdfdGFza3MS8AEK7QFbeyJrZXkiOiAiMGQ2ODVhMjE5OTRkOTQ5MDk3YmM1YTU2ZDcz
N2U2ZDEiLCAiaWQiOiAiNDdkMzRjZjktMGYxZS00Y2JkLTgzMzItNzRjZjY0YWRlOThlIiwgImFz
eW5jX2V4ZWN1dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAiYWdlbnRfcm9s
ZSI6ICJSZXNlYXJjaGVyIiwgImFnZW50X2tleSI6ICI4YmQyMTM5YjU5NzUxODE1MDZlNDFmZDlj
NDU2M2Q3NSIsICJ0b29sc19uYW1lcyI6IFtdfV16AhgBhQEAAQAAEo4CChAf4TXS782b0PBJ4NSB
JXwsEgjXnd13GkMzlyoMVGFzayBDcmVhdGVkMAE5mb/cHjiMFRhBGRTiHjiMFRhKLgoIY3Jld19r
ZXkSIgogOGMyNzUyZjQ5ZTViOWQyYjY4Y2IzNWNhYzhmY2M4NmRKMQoHY3Jld19pZBImCiRmZGRj
MDhlMy00NTI2LTQ3ZDYtOGU1Yy02NjRjMjIyNzhkODJKLgoIdGFza19rZXkSIgogMGQ2ODVhMjE5
OTRkOTQ5MDk3YmM1YTU2ZDczN2U2ZDFKMQoHdGFza19pZBImCiQ0N2QzNGNmOS0wZjFlLTRjYmQt
ODMzMi03NGNmNjRhZGU5OGV6AhgBhQEAAQAAEqMHChAyBGKhzDhROB5pmAoXrikyEgj6SCwzj1dU
LyoMQ3JldyBDcmVhdGVkMAE5vkjTHziMFRhBRDbhHziMFRhKGgoOY3Jld2FpX3ZlcnNpb24SCAoG
MC44Ni4wShoKDnB5dGhvbl92ZXJzaW9uEggKBjMuMTIuN0ouCghjcmV3X2tleRIiCiBiNjczNjg2
ZmM4MjJjMjAzYzdlODc5YzY3NTQyNDY5OUoxCgdjcmV3X2lkEiYKJGYyYWVlYTYzLTU2OWUtNDUz
NS1iZTY0LTRiZjYzZmU5NjhjN0ocCgxjcmV3X3Byb2Nlc3MSDAoKc2VxdWVudGlhbEoRCgtjcmV3
X21lbW9yeRICEABKGgoUY3Jld19udW1iZXJfb2ZfdGFza3MSAhgBShsKFWNyZXdfbnVtYmVyX29m
X2FnZW50cxICGAFK0QIKC2NyZXdfYWdlbnRzEsECCr4CW3sia2V5IjogImI1OWNmNzdiNmU3NjU4
NDg3MGViMWMzODgyM2Q3ZTI4IiwgImlkIjogImJiZjNkM2E4LWEwMjUtNGI0ZC1hY2Q0LTFmNzcz
NTI3MWJmMCIsICJyb2xlIjogIlJlc2VhcmNoZXIiLCAidmVyYm9zZT8iOiBmYWxzZSwgIm1heF9p
dGVyIjogMjAsICJtYXhfcnBtIjogbnVsbCwgImZ1bmN0aW9uX2NhbGxpbmdfbGxtIjogIiIsICJs
bG0iOiAiZ3B0LTRvLW1pbmkiLCAiZGVsZWdhdGlvbl9lbmFibGVkPyI6IGZhbHNlLCAiYWxsb3df
Y29kZV9leGVjdXRpb24/IjogZmFsc2UsICJtYXhfcmV0cnlfbGltaXQiOiAyLCAidG9vbHNfbmFt
ZXMiOiBbXX1dSv8BCgpjcmV3X3Rhc2tzEvABCu0BW3sia2V5IjogImE1ZTVjNThjZWExYjlkMDAz
MzJlNjg0NDFkMzI3YmRmIiwgImlkIjogIjBiOTRiMTY0LTM5NTktNGFmYS05Njg4LWJjNmEwZWMy
MWYzOCIsICJhc3luY19leGVjdXRpb24/IjogZmFsc2UsICJodW1hbl9pbnB1dD8iOiBmYWxzZSwg
ImFnZW50X3JvbGUiOiAiUmVzZWFyY2hlciIsICJhZ2VudF9rZXkiOiAiYjU5Y2Y3N2I2ZTc2NTg0
ODcwZWIxYzM4ODIzZDdlMjgiLCAidG9vbHNfbmFtZXMiOiBbXX1degIYAYUBAAEAABKOAgoQyYfi
Ftim717svttBZY3p5hIIUxR5bBHzWWkqDFRhc2sgQ3JlYXRlZDABOV4OBiA4jBUYQbLjBiA4jBUY
Si4KCGNyZXdfa2V5EiIKIGI2NzM2ODZmYzgyMmMyMDNjN2U4NzljNjc1NDI0Njk5SjEKB2NyZXdf
aWQSJgokZjJhZWVhNjMtNTY5ZS00NTM1LWJlNjQtNGJmNjNmZTk2OGM3Si4KCHRhc2tfa2V5EiIK
IGE1ZTVjNThjZWExYjlkMDAzMzJlNjg0NDFkMzI3YmRmSjEKB3Rhc2tfaWQSJgokMGI5NGIxNjQt
Mzk1OS00YWZhLTk2ODgtYmM2YTBlYzIxZjM4egIYAYUBAAEAAA==
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
Content-Length:
- '3685'
Content-Type:
- application/x-protobuf
User-Agent:
- OTel-OTLP-Exporter-Python/1.27.0
method: POST
uri: https://telemetry.crewai.com:4319/v1/traces
response:
body:
string: "\n\0"
headers:
Content-Length:
- '2'
Content-Type:
- application/x-protobuf
Date:
- Sun, 29 Dec 2024 04:43:27 GMT
status:
code: 200
message: OK
- request:
body: '{"messages": [{"role": "system", "content": "You are Researcher. You have
extensive AI research experience.\nYour personal goal is: Analyze AI topics\nTo
give my best complete final answer to the task use the exact following format:\n\nThought:
I now can give a great answer\nFinal Answer: Your final answer must be the great
and the most complete as possible, it must be outcome described.\n\nI MUST use
these formats, my job depends on it!"}, {"role": "user", "content": "\nCurrent
Task: Explain the advantages of AI.\n\nThis is the expect criteria for your
final answer: A summary of the main advantages, bullet points recommended.\nyou
MUST return the actual complete content as the final answer, not a summary.\n\nBegin!
This is VERY important to you, use the tools available and give your best Final
Answer, your job depends on it!\n\nThought:"}], "model": "gpt-4o-mini", "stop":
["\nObservation:"], "stream": false}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
connection:
- keep-alive
content-length:
- '922'
content-type:
- application/json
cookie:
- _cfuvid=eff7OIkJ0zWRunpA6z67LHqscmSe6XjNxXiPw1R3xCc-1733770413538-0.0.1.1-604800000
host:
- api.openai.com
user-agent:
- OpenAI/Python 1.52.1
x-stainless-arch:
- x64
x-stainless-async:
- 'false'
x-stainless-lang:
- python
x-stainless-os:
- Linux
x-stainless-package-version:
- 1.52.1
x-stainless-raw-response:
- 'true'
x-stainless-retry-count:
- '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.12.7
method: POST
uri: https://api.openai.com/v1/chat/completions
response:
content: "{\n \"id\": \"chatcmpl-AjfR6FDuTw7NGzy8w7sxjvOkUQlru\",\n \"object\":
\"chat.completion\",\n \"created\": 1735447404,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
\"assistant\",\n \"content\": \"I now can give a great answer \\nFinal
Answer: \\n**Advantages of AI** \\n\\n1. **Increased Efficiency and Productivity**
\ \\n - AI systems can process large amounts of data quickly and accurately,
leading to faster decision-making and increased productivity in various sectors.\\n\\n2.
**Cost Savings** \\n - Automation of repetitive and time-consuming tasks
reduces labor costs and increases operational efficiency, allowing businesses
to allocate resources more effectively.\\n\\n3. **Enhanced Data Analysis** \\n
\ - AI excels at analyzing big data, identifying patterns, and providing insights
that support better strategic planning and business decision-making.\\n\\n4.
**24/7 Availability** \\n - AI solutions, such as chatbots and virtual assistants,
operate continuously without breaks, offering constant support and customer
service, enhancing user experience.\\n\\n5. **Personalization** \\n - AI
enables the customization of content, products, and services based on user preferences
and behaviors, leading to improved customer satisfaction and loyalty.\\n\\n6.
**Improved Accuracy** \\n - AI technologies, such as machine learning algorithms,
reduce the likelihood of human error in various processes, leading to greater
accuracy and reliability.\\n\\n7. **Enhanced Innovation** \\n - AI fosters
innovative solutions by providing new tools and approaches to problem-solving,
enabling companies to develop cutting-edge products and services.\\n\\n8. **Scalability**
\ \\n - AI can be scaled to handle varying amounts of workloads without significant
changes to infrastructure, making it easier for organizations to expand operations.\\n\\n9.
**Predictive Capabilities** \\n - Advanced analytics powered by AI can anticipate
trends and outcomes, allowing businesses to proactively adjust strategies and
improve forecasting.\\n\\n10. **Health Benefits** \\n - In healthcare, AI
assists in diagnostics, personalized treatment plans, and predictive analytics,
leading to better patient care and improved health outcomes.\\n\\n11. **Safety
and Risk Mitigation** \\n - AI can enhance safety in various industries
by taking over dangerous tasks, monitoring for hazards, and predicting maintenance
needs for critical machinery, thereby preventing accidents.\\n\\n12. **Reduced
Environmental Impact** \\n - AI can optimize resource usage in areas such
as energy consumption and supply chain logistics, contributing to sustainability
efforts and reducing overall environmental footprints.\",\n \"refusal\":
null\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n
\ }\n ],\n \"usage\": {\n \"prompt_tokens\": 168,\n \"completion_tokens\":
440,\n \"total_tokens\": 608,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n
\ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"system_fingerprint\":
\"fp_0aa8d3e20b\"\n}\n"
headers:
CF-Cache-Status:
- DYNAMIC
CF-RAY:
- 8f9721053d1eb9f1-SEA
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Sun, 29 Dec 2024 04:43:32 GMT
Server:
- cloudflare
Set-Cookie:
- __cf_bm=5enubNIoQSGMYEgy8Q2FpzzhphA0y.0lXukRZrWFvMk-1735447412-1.0.1.1-FIK1sMkUl3YnW1gTC6ftDtb2mKsbosb4mwabdFAlWCfJ6pXeavYq.bPsfKNvzAb5WYq60yVGH5lHsJT05bhSgw;
path=/; expires=Sun, 29-Dec-24 05:13:32 GMT; domain=.api.openai.com; HttpOnly;
Secure; SameSite=None
- _cfuvid=63wmKMTuFamkLN8FBI4fP8JZWbjWiRxWm7wb3kz.z_A-1735447412038-0.0.1.1-604800000;
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
Transfer-Encoding:
- chunked
X-Content-Type-Options:
- nosniff
access-control-expose-headers:
- X-Request-ID
alt-svc:
- h3=":443"; ma=86400
openai-organization:
- crewai-iuxna1
openai-processing-ms:
- '7577'
openai-version:
- '2020-10-01'
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
x-ratelimit-limit-requests:
- '30000'
x-ratelimit-limit-tokens:
- '150000000'
x-ratelimit-remaining-requests:
- '29999'
x-ratelimit-remaining-tokens:
- '149999793'
x-ratelimit-reset-requests:
- 2ms
x-ratelimit-reset-tokens:
- 0s
x-request-id:
- req_55b8d714656e8f10f4e23cbe9034d66b
http_version: HTTP/1.1
status_code: 200
version: 1

View File

@@ -43,7 +43,7 @@ writer = Agent(
role="Senior Writer",
goal="Write the best content about AI and AI agents.",
backstory="You're a senior writer, specialized in technology, software engineering, AI and startups. You work as a freelancer and are now working on writing content for a new customer.",
allow_delegation=False,
allow_delegation=True,
)
@@ -391,6 +391,71 @@ def test_manager_agent_delegating_to_all_agents():
)
@pytest.mark.vcr(filter_headers=["authorization"])
def test_manager_agent_delegates_with_varied_role_cases():
"""
Test that the manager agent can delegate to agents regardless of case or whitespace variations in role names.
This test verifies the fix for issue #1503 where role matching was too strict.
"""
# Create agents with varied case and whitespace in roles
researcher_spaced = Agent(
role=" Researcher ", # Extra spaces
goal="Research with spaces in role",
backstory="A researcher with spaces in role name",
allow_delegation=False,
)
writer_caps = Agent(
role="SENIOR WRITER", # All caps
goal="Write with caps in role",
backstory="A writer with caps in role name",
allow_delegation=False,
)
task = Task(
description="Research and write about AI. The researcher should do the research, and the writer should write it up.",
expected_output="A well-researched article about AI.",
agent=researcher_spaced, # Assign to researcher with spaces
)
crew = Crew(
agents=[researcher_spaced, writer_caps],
process=Process.hierarchical,
manager_llm="gpt-4o",
tasks=[task],
)
mock_task_output = TaskOutput(
description="Mock description",
raw="mocked output",
agent="mocked agent"
)
task.output = mock_task_output
with patch.object(Task, 'execute_sync', return_value=mock_task_output) as mock_execute_sync:
crew.kickoff()
# Verify execute_sync was called once
mock_execute_sync.assert_called_once()
# Get the tools argument from the call
_, kwargs = mock_execute_sync.call_args
tools = kwargs['tools']
# Verify the delegation tools were passed correctly and can handle case/whitespace variations
assert len(tools) == 2
# Check delegation tool descriptions (should work despite case/whitespace differences)
delegation_tool = tools[0]
question_tool = tools[1]
assert "Delegate a specific task to one of the following coworkers:" in delegation_tool.description
assert " Researcher " in delegation_tool.description or "SENIOR WRITER" in delegation_tool.description
assert "Ask a specific question to one of the following coworkers:" in question_tool.description
assert " Researcher " in question_tool.description or "SENIOR WRITER" in question_tool.description
@pytest.mark.vcr(filter_headers=["authorization"])
def test_crew_with_delegating_agents():
tasks = [
@@ -1941,6 +2006,90 @@ def test_crew_log_file_output(tmp_path):
assert test_file.exists()
@pytest.mark.vcr(filter_headers=["authorization"])
def test_crew_output_file_end_to_end(tmp_path):
"""Test output file functionality in a full crew context."""
# Create an agent
agent = Agent(
role="Researcher",
goal="Analyze AI topics",
backstory="You have extensive AI research experience.",
allow_delegation=False,
)
# Create a task with dynamic output file path
dynamic_path = tmp_path / "output_{topic}.txt"
task = Task(
description="Explain the advantages of {topic}.",
expected_output="A summary of the main advantages, bullet points recommended.",
agent=agent,
output_file=str(dynamic_path),
)
# Create and run the crew
crew = Crew(
agents=[agent],
tasks=[task],
process=Process.sequential,
)
crew.kickoff(inputs={"topic": "AI"})
# Verify file creation and cleanup
expected_file = tmp_path / "output_AI.txt"
assert expected_file.exists(), f"Output file {expected_file} was not created"
@pytest.mark.vcr(filter_headers=["authorization"])
def test_crew_output_file_validation_failures():
"""Test output file validation failures in a crew context."""
agent = Agent(
role="Researcher",
goal="Analyze data",
backstory="You analyze data files.",
allow_delegation=False,
)
# Test path traversal
with pytest.raises(ValueError, match="Path traversal"):
task = Task(
description="Analyze data",
expected_output="Analysis results",
agent=agent,
output_file="../output.txt"
)
Crew(agents=[agent], tasks=[task]).kickoff()
# Test shell special characters
with pytest.raises(ValueError, match="Shell special characters"):
task = Task(
description="Analyze data",
expected_output="Analysis results",
agent=agent,
output_file="output.txt | rm -rf /"
)
Crew(agents=[agent], tasks=[task]).kickoff()
# Test shell expansion
with pytest.raises(ValueError, match="Shell expansion"):
task = Task(
description="Analyze data",
expected_output="Analysis results",
agent=agent,
output_file="~/output.txt"
)
Crew(agents=[agent], tasks=[task]).kickoff()
# Test invalid template variable
with pytest.raises(ValueError, match="Invalid template variable"):
task = Task(
description="Analyze data",
expected_output="Analysis results",
agent=agent,
output_file="{invalid-name}/output.txt"
)
Crew(agents=[agent], tasks=[task]).kickoff()
@pytest.mark.vcr(filter_headers=["authorization"])
def test_manager_agent():
from unittest.mock import patch
@@ -3125,4 +3274,4 @@ def test_multimodal_agent_live_image_analysis():
# Verify we got a meaningful response
assert isinstance(result.raw, str)
assert len(result.raw) > 100 # Expecting a detailed analysis
assert "error" not in result.raw.lower() # No error messages in response
assert "error" not in result.raw.lower() # No error messages in response

View File

@@ -719,21 +719,24 @@ def test_interpolate_inputs():
task = Task(
description="Give me a list of 5 interesting ideas about {topic} to explore for an article, what makes them unique and interesting.",
expected_output="Bullet point list of 5 interesting ideas about {topic}.",
output_file="/tmp/{topic}/output_{date}.txt"
)
task.interpolate_inputs(inputs={"topic": "AI"})
task.interpolate_inputs(inputs={"topic": "AI", "date": "2024"})
assert (
task.description
== "Give me a list of 5 interesting ideas about AI to explore for an article, what makes them unique and interesting."
)
assert task.expected_output == "Bullet point list of 5 interesting ideas about AI."
assert task.output_file == "/tmp/AI/output_2024.txt"
task.interpolate_inputs(inputs={"topic": "ML"})
task.interpolate_inputs(inputs={"topic": "ML", "date": "2025"})
assert (
task.description
== "Give me a list of 5 interesting ideas about ML to explore for an article, what makes them unique and interesting."
)
assert task.expected_output == "Bullet point list of 5 interesting ideas about ML."
assert task.output_file == "/tmp/ML/output_2025.txt"
def test_interpolate_only():
@@ -872,3 +875,61 @@ def test_key():
assert (
task.key == hash
), "The key should be the hash of the non-interpolated description."
def test_output_file_validation():
"""Test output file path validation."""
# Valid paths
assert Task(
description="Test task",
expected_output="Test output",
output_file="output.txt"
).output_file == "output.txt"
assert Task(
description="Test task",
expected_output="Test output",
output_file="/tmp/output.txt"
).output_file == "tmp/output.txt"
assert Task(
description="Test task",
expected_output="Test output",
output_file="{dir}/output_{date}.txt"
).output_file == "{dir}/output_{date}.txt"
# Invalid paths
with pytest.raises(ValueError, match="Path traversal"):
Task(
description="Test task",
expected_output="Test output",
output_file="../output.txt"
)
with pytest.raises(ValueError, match="Path traversal"):
Task(
description="Test task",
expected_output="Test output",
output_file="folder/../output.txt"
)
with pytest.raises(ValueError, match="Shell special characters"):
Task(
description="Test task",
expected_output="Test output",
output_file="output.txt | rm -rf /"
)
with pytest.raises(ValueError, match="Shell expansion"):
Task(
description="Test task",
expected_output="Test output",
output_file="~/output.txt"
)
with pytest.raises(ValueError, match="Shell expansion"):
Task(
description="Test task",
expected_output="Test output",
output_file="$HOME/output.txt"
)
with pytest.raises(ValueError, match="Invalid template variable"):
Task(
description="Test task",
expected_output="Test output",
output_file="{invalid-name}/output.txt"
)

View File

@@ -0,0 +1,123 @@
from unittest.mock import MagicMock
import pytest
from langchain_core.language_models.base import BaseLanguageModel
from crewai import Agent, Crew, Process, Task
def test_hierarchical_delegation_tool_availability():
"""Test that all agents are available for delegation in hierarchical mode."""
# Mock LLM to avoid actual API calls
mock_llm = MagicMock(spec=BaseLanguageModel)
# Create agents
manager = Agent(
role="Manager",
goal="Manage the team",
backstory="I am a manager",
allow_delegation=True,
llm=mock_llm
)
kb_agent = Agent(
role="kb_retriever_agent",
goal="Retrieve knowledge",
backstory="I am a knowledge retrieval specialist",
allow_delegation=True,
llm=mock_llm
)
worker = Agent(
role="Worker",
goal="Do the work",
backstory="I am a worker",
allow_delegation=True,
llm=mock_llm
)
# Create a task assigned to the manager
task = Task(
description="Complex task requiring delegation",
expected_output="Task completion status",
agent=manager
)
# Create the crew with hierarchical process
crew = Crew(
agents=[kb_agent, worker], # Manager should not be in agents list when using hierarchical process
tasks=[task],
process=Process.hierarchical,
manager_agent=manager # Explicitly set the manager agent for hierarchical process
)
# Get the manager's tools
tools_for_task = task.tools or manager.tools or []
manager_tools = crew._prepare_tools(manager, task, tools_for_task)
# Find delegation tools
delegation_tools = [tool for tool in manager_tools if tool.name == "Delegate work to coworker"]
assert len(delegation_tools) > 0, "Delegation tool should be present"
# Get the delegation tool description
delegate_tool = delegation_tools[0]
tool_description = str(delegate_tool.description)
# Verify all agents are available for delegation
assert "kb_retriever_agent" in tool_description, "kb_retriever_agent should be available for delegation"
assert "Worker" in tool_description, "Worker should be available for delegation"
def test_hierarchical_delegation_tool_updates():
"""Test that delegation tools are properly updated when task agent changes."""
mock_llm = MagicMock(spec=BaseLanguageModel)
manager = Agent(
role="Manager",
goal="Manage the team",
backstory="I am a manager",
allow_delegation=True,
llm=mock_llm
)
kb_agent = Agent(
role="kb_retriever_agent",
goal="Retrieve knowledge",
backstory="I am a knowledge retrieval specialist",
allow_delegation=True,
llm=mock_llm
)
# Create tasks for different agents
manager_task = Task(
description="Manager task",
expected_output="Manager task completion status",
agent=manager
)
kb_task = Task(
description="KB task",
expected_output="KB task completion status",
agent=kb_agent
)
# Create crew
crew = Crew(
agents=[kb_agent], # Manager should not be in agents list when using hierarchical process
tasks=[manager_task, kb_task],
process=Process.hierarchical,
manager_agent=manager # Explicitly set the manager agent for hierarchical process
)
# Test manager's tools
manager_tools_for_task = manager_task.tools or manager.tools or []
manager_tools = crew._prepare_tools(manager, manager_task, manager_tools_for_task)
manager_delegation = [t for t in manager_tools if t.name == "Delegate work to coworker"]
assert len(manager_delegation) > 0, "Manager should have delegation tool"
assert "kb_retriever_agent" in str(manager_delegation[0].description), "Manager should see kb_retriever_agent"
# Test kb_agent's tools
kb_tools_for_task = kb_task.tools or kb_agent.tools or []
kb_tools = crew._prepare_tools(kb_agent, kb_task, kb_tools_for_task)
kb_delegation = [t for t in kb_tools if t.name == "Delegate work to coworker"]
assert len(kb_delegation) > 0, "KB agent should have delegation tool"
assert "Manager" in str(kb_delegation[0].description), "KB agent should see manager"

View File

@@ -0,0 +1,55 @@
from unittest.mock import MagicMock
import pytest
from crewai import Agent, Task
from crewai.tools.agent_tools.base_agent_tools import BaseAgentTool
class TestAgentTool(BaseAgentTool):
"""Concrete implementation of BaseAgentTool for testing."""
def _run(self, *args, **kwargs):
"""Implement required _run method."""
return "Test response"
@pytest.mark.parametrize("role_name,should_match", [
('Futel Official Infopoint', True), # exact match
(' "Futel Official Infopoint" ', True), # extra quotes and spaces
('Futel Official Infopoint\n', True), # trailing newline
('"Futel Official Infopoint"', True), # embedded quotes
(' FUTEL\nOFFICIAL INFOPOINT ', True), # multiple whitespace and newline
('futel official infopoint', True), # lowercase
('FUTEL OFFICIAL INFOPOINT', True), # uppercase
('Non Existent Agent', False), # non-existent agent
(None, False), # None agent name
])
def test_agent_tool_role_matching(role_name, should_match):
"""Test that agent tools can match roles regardless of case, whitespace, and special characters."""
# Create test agent
test_agent = Agent(
role='Futel Official Infopoint',
goal='Answer questions about Futel',
backstory='Futel Football Club info',
allow_delegation=False
)
# Create test agent tool
agent_tool = TestAgentTool(
name="test_tool",
description="Test tool",
agents=[test_agent]
)
# Test role matching
result = agent_tool._execute(
agent_name=role_name,
task='Test task',
context=None
)
if should_match:
assert "coworker mentioned not found" not in result.lower(), \
f"Should find agent with role name: {role_name}"
else:
assert "coworker mentioned not found" in result.lower(), \
f"Should not find agent with role name: {role_name}"