Skip to content

[BUG]Type Error in Hierarchical Process Delegation #2606

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

Open
HaohanTsao opened this issue Apr 15, 2025 · 17 comments
Open

[BUG]Type Error in Hierarchical Process Delegation #2606

HaohanTsao opened this issue Apr 15, 2025 · 17 comments
Labels
bug Something isn't working

Comments

@HaohanTsao
Copy link

HaohanTsao commented Apr 15, 2025

Description

When using CrewAI's hierarchical process with delegation, the manager agent fails to delegate tasks due to type validation errors in the DelegateWorkToolSchema. The manager attempts to pass dictionary objects for task and context parameters, but the schema expects string values.

Steps to Reproduce

  1. Create a basic hierarchical crew with a manager agent and worker agents
  2. Enable delegation for the manager (allow_delegation=True)
  3. Set up the process as hierarchical (process=Process.hierarchical)
  4. Run the crew
import os
from dotenv import load_dotenv
from crewai import Agent, Task, Crew, Process

# Load environment variables
load_dotenv()

def main():
    # Define the researcher agent
    researcher = Agent(
        role="Research Specialist",
        goal="Find accurate and relevant information on a given topic",
        backstory="You are an expert at gathering information with a keen eye for detail. Your specialty is conducting thorough research on any topic.",
        allow_delegation=False,
        verbose=True
    )
    
    # Define the writer agent
    writer = Agent(
        role="Content Writer",
        goal="Create well-structured, engaging content based on research",
        backstory="You are an experienced writer with a talent for turning complex information into clear, concise, and engaging content.",
        allow_delegation=False,
        verbose=True
    )
    
    # Define the manager agent
    manager = Agent(
        role="Project Manager",
        goal="Coordinate the research and writing process to ensure high-quality output",
        backstory="You are a skilled project manager with years of experience coordinating teams. You know how to allocate tasks and ensure all work meets high standards.",
        allow_delegation=True,  # Manager needs delegation ability
        verbose=True
    )
    
    # Define the tasks
    research_task = Task(
        description="Research the history and impact of artificial intelligence in healthcare. Find key milestones, current applications, and future trends. Focus on factual information from reliable sources.",
        expected_output="A comprehensive report with factual information about AI in healthcare, including historical development, current applications, and future trends.",
        agent=researcher
    )
    
    writing_task = Task(
        description="Using the research provided, create a well-structured article about AI in healthcare. The article should be informative, engaging, and accessible to a non-technical audience.",
        expected_output="A well-written article about AI in healthcare that effectively communicates the information from the research in an engaging way.",
        agent=writer
    )
    
    # Create the crew with hierarchical process
    crew = Crew(
        agents=[researcher, writer],
        tasks=[research_task, writing_task],
        manager_agent=manager,  # Specify the manager agent
        process=Process.hierarchical,  # Use hierarchical process
        verbose=True
    )

    # same problem while using llm manager 
    # crew = Crew(
    #      agents=[researcher, writer],
    #      tasks=[research_task, writing_task],
    #      manager_llm="gpt-4o-mini",  # Specify the manager agent
    #      process=Process.hierarchical,  # Use hierarchical process
    #       verbose=True
    # )
    
    # Start the crew
    result = crew.kickoff()
    
    print("\n\n=== Final Result ===")
    print(result)

if __name__ == "__main__":
    main()

Expected behavior

The manager agent should be able to successfully delegate tasks to appropriate worker agents.

Screenshots/Code snippets

Image Image

Operating System

macOS Sonoma

Python Version

3.12

crewAI Version

0.114.0

crewAI Tools Version

0.40.1

Virtual Environment

Conda

Evidence

The tool execution fails with validation errors. The manager is attempting to pass dictionary objects when the schema expects strings:

Tool Usage Failed
Name: Delegate work to coworker
Error: Arguments validation failed: 2 validation errors for DelegateWorkToolSchema
task
  Input should be a valid string [type=string_type, input_value={'description': 'Research...thcare.', 'type': 'str'}, input_type=dict]
context
  Input should be a valid string [type=string_type, input_value={'description': 'This tas...ations.', 'type': 'str'}, input_type=dict]

Possible Solution

Analysis of the CrewAI Delegation Error and Proposed Solution

After analyzing the CrewAI codebase thoroughly, I can identify the exact cause of the delegation error and suggest an appropriate fix that the repository owners would likely appreciate.

Root Cause Analysis

The issue occurs in the hierarchical process when the manager agent attempts to delegate tasks. Here's what's happening:

  1. The DelegateWorkToolSchema (in delegate_work_tool.py) is defined with string fields:

    class DelegateWorkToolSchema(BaseModel):
        task: str = Field(..., description="The task to delegate")
        context: str = Field(..., description="The context for the task")
        coworker: str = Field(..., description="The role/name of the coworker to delegate to")
  2. However, when the manager agent tries to delegate, it's passing Task objects or dictionaries with a format like:

    {'description': 'Research...thcare.', 'type': 'str'}
  3. The validation fails because the schema expects strings, not dictionaries.

Most Likely Solution

The best solution would be to modify the DelegateWorkToolSchema class to accept both string and dictionary inputs, ensuring backward compatibility while fixing the issue:

from typing import Optional, Union, Dict, Any
from pydantic import BaseModel, Field

class DelegateWorkToolSchema(BaseModel):
    task: Union[str, Dict[str, Any]] = Field(..., description="The task to delegate")
    context: Union[str, Dict[str, Any]] = Field(..., description="The context for the task")
    coworker: str = Field(..., description="The role/name of the coworker to delegate to")

Then, modify the DelegateWorkTool._run method to handle both formats:

def _run(
    self,
    task: Union[str, Dict[str, Any]],
    context: Union[str, Dict[str, Any]],
    coworker: Optional[str] = None,
    **kwargs,
) -> str:
    # Convert task to string if it's a dictionary
    if isinstance(task, dict) and "description" in task:
        task = task["description"]
    
    # Convert context to string if it's a dictionary
    if isinstance(context, dict) and "description" in context:
        context = context["description"]
        
    coworker = self._get_coworker(coworker, **kwargs)
    return self._execute(coworker, task, context)

Why This Solution Works

  1. Backward Compatibility: This solution maintains compatibility with existing code that passes strings.

  2. Enhanced Flexibility: It allows for both string and dictionary inputs, making the API more flexible.

  3. Minimal Changes: The fix is localized to just the delegation tool schema and its implementation.

  4. Matches Workflow Intent: It maintains the original intent of the delegation workflow while fixing the type mismatch.

This approach is also aligned with how CrewAI handles other tool validations in the codebase, as seen in the tool_usage.py file where there are several input validation and conversion mechanisms.

Alternative Solution (If Type Changes Are Undesirable)

If changing the schema types is problematic, an alternative would be to add pre-processing in the Agent.get_delegation_tools method to ensure tasks are converted to strings before being passed to the delegation tool:

def get_delegation_tools(self, agents: List[BaseAgent]):
    # Create a custom wrapper that pre-processes arguments
    agent_tools = AgentTools(agents=agents, preprocess_task_to_string=True)
    tools = agent_tools.tools()
    return tools

Additional context

None

@Vidit-Ostwal
Copy link
Contributor

Hi @HaohanTsao, which large language model are you using because of which the validation issue is coming up?

@HaohanTsao
Copy link
Author

gpt-4o-mini

@Vidit-Ostwal
Copy link
Contributor

Can you try a small experiment for me?

can you replace this function

    def _generate_description(self):
        print("Generating description...")

        calling_class = self.__class__.__name__
        print(f"Method called by: {calling_class}")

        args_schema = {}
        for name, field in self.args_schema.model_fields.items():
            args_schema[name] = {
                "description": field.description,
                "type": BaseTool._get_arg_annotations(field.annotation),
                "required": True  # Assuming all parameters are required
            }
        
        print(args_schema)

        usage_instructions = f"""Usage Instructions: To use this tool ({self.name}), you must provide the following parameters with their corresponding data types:\n"""

        for name, info in args_schema.items():
            usage_instructions += f"- {name}: {info['description']} (Type: {info['type']})\n"

        example_args = {}
        for name, info in args_schema.items():
            if info["type"] == "str":
                example_args[name] = f'"{name}_example"'
            elif info["type"] == "int":
                example_args[name] = "42"
            elif info["type"] == "float":
                example_args[name] = "3.14"
            elif info["type"] == "bool":
                example_args[name] = "True"
            else:
                example_args[name] = f'"{name}_example"'

        correct_usage = f"\nExample Usage:\n{self.name}({', '.join([f'{name}={value}' for name, value in example_args.items()])})\n"

        self.description = f"Tool Name: {self.name}\nTool Arguments: {args_schema}\nTool Description: {self.description}\n{usage_instructions}{correct_usage}"
        print(self.description)

here,

def _generate_description(self):
args_schema = {
name: {
"description": field.description,
"type": BaseTool._get_arg_annotations(field.annotation),
}
for name, field in self.args_schema.model_fields.items()
}
self.description = f"Tool Name: {self.name}\nTool Arguments: {args_schema}\nTool Description: {self.description}"

and check whether this validation error is still there.

@HaohanTsao
Copy link
Author

Ok, I just tried with gpt-3.5-turbo. It runs without error.

@Vidit-Ostwal
Copy link
Contributor

Ok, I just tried with gpt-3.5-turbo. It runs without error.

With the above mentioned change or independently, it works?

@HaohanTsao
Copy link
Author

HaohanTsao commented Apr 15, 2025

Independently.

And I also tried your method on both 4o-mini and 3.5-turbo. Both work.

Here is a part of printed output by 3.5-turbo.

Generating description...
Method called by: DelegateWorkTool
{'task': {'description': 'The task to delegate', 'type': 'str', 'required': True}, 'context': {'description': 'The context for the task', 'type': 'str', 'required': True}, 'coworker': {'description': 'The role/name of the coworker to delegate to', 'type': 'str', 'required': True}}
Tool Name: Delegate work to coworker
Tool Arguments: {'task': {'description': 'The task to delegate', 'type': 'str', 'required': True}, 'context': {'description': 'The context for the task', 'type': 'str', 'required': True}, 'coworker': {'description': 'The role/name of the coworker to delegate to', 'type': 'str', 'required': True}}
Tool Description: Delegate a specific task to one of the following coworkers: Research Specialist, Content Writer
The 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 absolutely everything you know, don't reference things but instead explain them.
Usage Instructions: To use this tool (Delegate work to coworker), you must provide the following parameters with their corresponding data types:
- task: The task to delegate (Type: str)
- context: The context for the task (Type: str)
- coworker: The role/name of the coworker to delegate to (Type: str)

Example Usage:
Delegate work to coworker(task="task_example", context="context_example", coworker="coworker_example")

Generating description...
Method called by: AskQuestionTool
{'question': {'description': 'The question to ask', 'type': 'str', 'required': True}, 'context': {'description': 'The context for the question', 'type': 'str', 'required': True}, 'coworker': {'description': 'The role/name of the coworker to ask', 'type': 'str', 'required': True}}
Tool Name: Ask question to coworker
Tool Arguments: {'question': {'description': 'The question to ask', 'type': 'str', 'required': True}, 'context': {'description': 'The context for the question', 'type': 'str', 'required': True}, 'coworker': {'description': 'The role/name of the coworker to ask', 'type': 'str', 'required': True}}
Tool Description: Ask a specific question to one of the following coworkers: Research Specialist, Content Writer
The input to this tool should be the coworker, the question you have for them, and ALL necessary context to ask the question properly, they know nothing about the question, so share absolutely everything you know, don't reference things but instead explain them.
Usage Instructions: To use this tool (Ask question to coworker), you must provide the following parameters with their corresponding data types:
- question: The question to ask (Type: str)
- context: The context for the question (Type: str)
- coworker: The role/name of the coworker to ask (Type: str)

Example Usage:
Ask question to coworker(question="question_example", context="context_example", coworker="coworker_example")

Generating description...
Method called by: DelegateWorkTool
{'task': {'description': 'The task to delegate', 'type': 'str', 'required': True}, 'context': {'description': 'The context for the task', 'type': 'str', 'required': True}, 'coworker': {'description': 'The role/name of the coworker to delegate to', 'type': 'str', 'required': True}}
Tool Name: Delegate work to coworker
Tool Arguments: {'task': {'description': 'The task to delegate', 'type': 'str', 'required': True}, 'context': {'description': 'The context for the task', 'type': 'str', 'required': True}, 'coworker': {'description': 'The role/name of the coworker to delegate to', 'type': 'str', 'required': True}}
Tool Description: Delegate a specific task to one of the following coworkers: Research Specialist
The 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 absolutely everything you know, don't reference things but instead explain them.
Usage Instructions: To use this tool (Delegate work to coworker), you must provide the following parameters with their corresponding data types:
- task: The task to delegate (Type: str)
- context: The context for the task (Type: str)
- coworker: The role/name of the coworker to delegate to (Type: str)

Example Usage:
Delegate work to coworker(task="task_example", context="context_example", coworker="coworker_example")

Generating description...
Method called by: AskQuestionTool
{'question': {'description': 'The question to ask', 'type': 'str', 'required': True}, 'context': {'description': 'The context for the question', 'type': 'str', 'required': True}, 'coworker': {'description': 'The role/name of the coworker to ask', 'type': 'str', 'required': True}}
Tool Name: Ask question to coworker
Tool Arguments: {'question': {'description': 'The question to ask', 'type': 'str', 'required': True}, 'context': {'description': 'The context for the question', 'type': 'str', 'required': True}, 'coworker': {'description': 'The role/name of the coworker to ask', 'type': 'str', 'required': True}}
Tool Description: Ask a specific question to one of the following coworkers: Research Specialist
The input to this tool should be the coworker, the question you have for them, and ALL necessary context to ask the question properly, they know nothing about the question, so share absolutely everything you know, don't reference things but instead explain them.
Usage Instructions: To use this tool (Ask question to coworker), you must provide the following parameters with their corresponding data types:
- question: The question to ask (Type: str)
- context: The context for the question (Type: str)
- coworker: The role/name of the coworker to ask (Type: str)

Example Usage:
Ask question to coworker(question="question_example", context="context_example", coworker="coworker_example")

@Vidit-Ostwal
Copy link
Contributor

@lucasgomide, what do you think on this?

This validation issue is a problem which was faced before as well.
Check this issue. #2508, I think the issue is with the way we are generating the tool description.
Now I think smarter models are able to follow the instruction we are setting, while somewhat dumb models are getting stuck at this.

The issue I see, is

Tool Name: Delegate work to coworker
Tool Arguments: {'task': {'description': 'The task to delegate', 'type': 'str', 'required': True}, 'context': {'description': 'The context for the task', 'type': 'str', 'required': True}, 'coworker': {'description': 'The role/name of the coworker to delegate to', 'type': 'str', 'required': True}}
Tool Description: Delegate a specific task to one of the following coworkers: Research Specialist, Content Writer
The 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 absolutely everything you know, don't reference things but instead explain them.

In the tool arguments, we give a description kind of field, because of which I believe llm are generating a nested dictionary.
Check the above image,
Instead I propose, to add a example usage of how to use the tool.

Tool Name: Delegate work to coworker
Tool Arguments: {'task': {'description': 'The task to delegate', 'type': 'str', 'required': True}, 'context': {'description': 'The context for the task', 'type': 'str', 'required': True}, 'coworker': {'description': 'The role/name of the coworker to delegate to', 'type': 'str', 'required': True}}
Tool Description: Delegate a specific task to one of the following coworkers: Research Specialist, Content Writer
The 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 absolutely everything you know, don't reference things but instead explain them.
Usage Instructions: To use this tool (Delegate work to coworker), you must provide the following parameters with their corresponding data types:
- task: The task to delegate (Type: str)
- context: The context for the task (Type: str)
- coworker: The role/name of the coworker to delegate to (Type: str)

Example Usage:
Delegate work to coworker(task="task_example", context="context_example", coworker="coworker_example")

There is no way to tell, whether this prompting would work or not?. Just to add and check whether users are facing these validation issue or not.

@HaohanTsao
Copy link
Author

Thanks for digging into this. I see your point about it being a prompt engineering issue.

What about keeping both solutions? We could improve the tool descriptions as you suggested AND keep the flexible type handling as a safety net.

This way we're covered regardless of which LLM is being used or how it interprets the instructions.

What do you think?

@Vidit-Ostwal
Copy link
Contributor

What do you think?

so the validation fix can not be applied globally,
For example this is how all the tools are defined currently, even the delegate is another tool which is being used by the agent.

If we relax the validation which is happening in this particular case, then it might resolve the issue currently, but this issue will persists, if any other user uses any other tool.

Also, another consideration is if tomorrow, llm by itself, decides to make the nested dictionary, with some other parameter, then also this tool will fail.

That's why I am trying to create a global solution, which will apply on all the tools.

@lucasgomide
Copy link
Contributor

lucasgomide commented Apr 15, 2025

@lucasgomide, what do you think on this?

This topic is relatively new to me, so I’ll need some time to digest it, though. At the first view, i totally agreed with @HaohanTsao (including its PR) but as i said, I need a bit more time to fully understand what’s happening

I really appreciate your though @Vidit-Ostwal .. very clear. I'm going to dive deeper into it tomorrow

@Vidit-Ostwal
Copy link
Contributor

Vidit-Ostwal commented Apr 15, 2025

Let me know if you need any help on this one!?
Happy to jump over a call, if required.
@lucasgomide

@HaohanTsao
Copy link
Author

I'm wondering what if we just embrace what LLMs do best - generating plain text?

For parameters like "task" and "context" that are essentially just descriptions, what if we explicitly instruct LLMs to provide simple text respectively without any JSON or nested structures?

This would be a bigger change to the tool description system than my PR fix and cost more, but might solve a whole class of similar issues across different models.

Do you think this approach would work with CrewAI's design philosophy, or would it require too much refactoring?

@Vidit-Ostwal
Copy link
Contributor

I'm wondering what if we just embrace what LLMs do best - generating plain text?

For parameters like "task" and "context" that are essentially just descriptions, what if we explicitly instruct LLMs to provide simple text respectively without any JSON or nested structures?

This would be a bigger change to the tool description system than my PR fix and cost more, but might solve a whole class of similar issues across different models.

Do you think this approach would work with CrewAI's design philosophy, or would it require too much refactoring?

I am a bit biased to use pydantic classes as a type check to ensure only relevant fields are passed!
Lucas, can surely add more context on this one.

@lucasgomide
Copy link
Contributor

here's my 🪙

  1. Similar issues: I believe we could do better with prompts. Frequently I saw several similar erros happing - even when better models, they are auto-resolved after a few retries. We have to improve our output schema - by using better their description, for instance. That's why I strongly believe we have improve LLM instructions on this issue as well.
  2. Impact > Effort: we can solve this issue with minimal effort and deliver a huge impact.
  3. Test: this change could be very impactful, whether in a good or bad way.. so we need to test it thoroughly under stress with several of models.

@HaohanTsao
Copy link
Author

Thank you for your explanation and insights! I appreciate you taking the time to share your perspective on addressing this issue.

I understand your approach now - focusing on improving the prompts/instructions rather than adapting the code to handle various output formats. That makes sense from a design philosophy standpoint.

Should I close my PR ? Since it doesn't align with the preferred direction. I'm looking forward to seeing how the prompt engineering solution develops, and would be happy to help test or contribute to that approach if needed.

Thanks again for the thoughtful discussion around this issue!

@Vidit-Ostwal
Copy link
Contributor

here's my 🪙

  1. Similar issues: I believe we could do better with prompts. Frequently I saw several similar erros happing - even when better models, they are auto-resolved after a few retries. We have to improve our output schema - by using better their description, for instance. That's why I strongly believe we have improve LLM instructions on this issue as well.
  2. Impact > Effort: we can solve this issue with minimal effort and deliver a huge impact.
  3. Test: this change could be very impactful, whether in a good or bad way.. so we need to test it thoroughly under stress with several of models.

Agreed in this, the entire change if we need to do will happen in the def _generate_description(self): function defined. Let me know any different instruction schema or prompt you have in mind, will try to raise a PR soon.

@lucasgomide
Copy link
Contributor

Should I close my PR ? Since it doesn't align with the preferred direction. I'm looking forward to seeing how the prompt engineering solution develops, and would be happy to help test or contribute to that approach if needed.

Yeah you can close (: Thank your for bringing this theme up and your patience 😆

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
3 participants