crewAI icon indicating copy to clipboard operation
crewAI copied to clipboard

Not able to use tool with multiple input

Open vikasr111 opened this issue 1 year ago • 7 comments

I am playing around with CrewAI with the goal of using it in our production app. I have done a basic setup where I am trying to setup an agent with multi-input tool.

from crewai import Agent, Task, Crew, Process
from langchain.tools import tool

# Define a structured tool with multiple parameters
@tool
def data_analysis_tool(data_source, analysis_type, output_format):
    """
    Perform data analysis on the specified data source.
    Args:
        data_source: The source of the data to analyze.
        analysis_type: The type of analysis to perform.
        output_format: The format of the analysis output.
    Returns:
        A string describing the result of the analysis.
    """
    # Tool logic here
    return f"Analysis of {data_source} using {analysis_type} in {output_format} format"

# Create an agent and assign the tool
analyst_agent = Agent(
    role='Data Analyst',
    goal='Perform data analysis based on given parameters',
    backstory='An experienced data analyst capable of handling various types of data analysis tasks.',
    tools=[data_analysis_tool],
    llm=ChatOpenAI(model_name="gpt-4-1106-preview", temperature=0.1)
)

# Define a task with parameters for the tool
task = Task(
    description='Analyze the sales data',
    context={
        'data_source': 'sales_database',
        'analysis_type': 'trend_analysis',
        'output_format': 'PDF'
    },
    agent=analyst_agent
)

# Create a crew and assign the task
crew = Crew(agents=[analyst_agent], tasks=[task])

# Execute the crew
result = crew.kickoff()
print(result)

When I am trying to run I am always getting following error:

---------------------------------------------------------------------------
ValidationError                           Traceback (most recent call last)
[<ipython-input-31-c57ec2325d57>](https://localhost:8080/#) in <cell line: 43>()
     41 
     42 # Execute the crew
---> 43 result = crew.kickoff()
     44 print(result)

13 frames
[/usr/local/lib/python3.10/dist-packages/pydantic/v1/main.py](https://localhost:8080/#) in __init__(__pydantic_self__, **data)
    339         values, fields_set, validation_error = validate_model(__pydantic_self__.__class__, data)
    340         if validation_error:
--> 341             raise validation_error
    342         try:
    343             object_setattr(__pydantic_self__, '__dict__', values)

ValidationError: 2 validation errors for data_analysis_toolSchemaSchema
analysis_type
  field required (type=value_error.missing)
output_format
  field required (type=value_error.missing)

Does CrewAI works well with Structured tools with multiple input parameter? What am I doing wrong?

vikasr111 avatar Jan 14 '24 15:01 vikasr111

I have opened the same issue in #110 and this issue is from pydantic version that crewai is using. The issue is also ongoing in langchain.

Haripritamreddy avatar Jan 14 '24 15:01 Haripritamreddy

@Haripritamreddy you are right. Have you found any temporary solution for this?

vikasr111 avatar Jan 14 '24 16:01 vikasr111

Unlike langchain which can work with pydantic v1 and v2 crewai does not have this yet. I tried to install the oldest version of crewai 0.1.0 with pydantic 1.10.10 I get the error

The conflict is caused by:
    The user requested pydantic==1.10.10
    crewai 0.1.0 depends on pydantic<3.0.0 and >=2.4.2

So there is currently no way unless the next release of crewai adds pydantic v1 support or langchain should solve the issue.

Haripritamreddy avatar Jan 14 '24 16:01 Haripritamreddy

@tkmerkel found a solution for it in the issue i opened. It has worked for me and it will work for you .

class SearchTools():

  @tool("Search the internet")
  def search_internet(payload):
    """Useful to search the internet about a given topic and return relevant results
    :param payload: str, a string representation of dictionary containing the following keys:

    query: str, the query to search for
    image_count: int, the number of top results to return

    example payload:
    {
        "query": "cat",
        "result_count": 4
    }
    """
    url = "https://google.serper.dev/search"

    query = json.loads(payload)['query']
    top_result_to_return = json.loads(payload)['result_count']
    
    search_payload = json.dumps({"q": query})
    headers = {
        'X-API-KEY': os.environ['SERPER_API_KEY'],
        'content-type': 'application/json'
    }
    response = requests.request("POST", url, headers=headers, data=search_payload)
    # check if there is an organic key
    if 'organic' not in response.json():
      return "Sorry, I couldn't find anything about that, there could be an error with you serper api key."
    else:
      results = response.json()['organic']
      stirng = []
      for result in results[:top_result_to_return]:
        try:
          stirng.append('\n'.join([
              f"Title: {result['title']}", f"Link: {result['link']}",
              f"Snippet: {result['snippet']}", "\n-----------------"
          ]))
        except KeyError:
          next

      return '\n'.join(stirng)

The explaination they gave is

For what its worth, I've been getting around it by having all my functions have a single parameter payload and that payload being a dictionary you can pass multiple arguments to.

Haripritamreddy avatar Jan 15 '24 02:01 Haripritamreddy

@joaomdmoura what do you think of this? I can revert to pydantic.v1, but I don't think that is wise because the upgrade is imminent and the performance increase from v2 is notable. If the dict approach for multiple inputs is working and testable, I think it should be noted in the docs and to move from there

greysonlalonde avatar Jan 15 '24 05:01 greysonlalonde

i too have been futzing with a solution based on the @tkmerkel code, works but finicky at times - specially with arrays as you can see:

def convert_tool_for_crewai(tool: BaseTool):
    if tool.metadata and "crewai" in tool.metadata.keys() and tool.metadata["crewai"] == True:
        return tool
    elif tool.args_schema and len(tool.args) <= 1:
        return tool
    elif not tool.args_schema:
        return tool

    def parse_input_and_delegate(tool_input: Optional[str] = None) -> Any:
        """Parse the input and delegate to the function."""
        # parse starting with first { and last }
        tool_input = tool_input or "{}"
        tool_input_input: Dict = {}
        try:
            tool_input_input = json.loads(tool_input[tool_input.find("{"):tool_input.rfind("}")+1])
            #handle for arrays here :/
            for k,v in tool_input_input.items():
                if tool.args[k]["type"] == "array" and not isinstance(v,list):
                    tool_input_input[k] = [v]
        except:
            # trust the input, it might be positional...
            tool_input_input = tool_input # type: ignore
        #parse it!
        parsed_input = tool._parse_input(tool_input_input)
        tool_args, tool_kwargs = tool._to_args_and_kwargs(parsed_input)
        observation = (
            tool._run(*tool_args, run_manager=None, **tool_kwargs)
            if signature(tool._run).parameters.get("run_manager")
            else tool._run(*tool_args, **tool_kwargs)
        )
        return observation

    # gen a prompt describing the fields expected
    tool_args_desc = ""
    for k,v in tool.args.items():
        tool_args_desc += f"field: {k}"
        if "type" in v.keys():
            if v['type'] == "array":
                tool_args_desc += f"\ntype: {v['type']} of {v['items']['type']}"
            else:
                tool_args_desc += f"\ntype: {v['type']}"
        tool_args_desc += "\n\n"

    # create the tool
    crewai_tool = Tool.from_function(
        func=parse_input_and_delegate,
        name=f"{tool.name}_crewai",
        description=tool.description,
        args_schema=create_model(
            f"{tool.name}_crewai_input", 
            tool_input=Field(
                default="{}",
                description=f"This MUST be a string representation of a dictionary containing the following fields:\n\n{tool_args_desc.strip()}"
            )
        ),
        return_direct=tool.return_direct,
        metadata={
            "crewai": True,
            "original_tool": tool
        }
    )

    return crewai_tool

I found some tools (e.g. MS365, Gmail) would cause the large tool description to go beyond the 1000-ish char limit. Here im just splitting the tool description from the single args description.

micahparker avatar Feb 08 '24 21:02 micahparker

I too have been tormented by the multi-parameter input of tools. I used to always concatenate multiple inputs with a special character, such as '#'. But now I have found a very effective method, and I hope it can help everyone.

    @tool("Save the given code to a given path")
    def save_code(path, code_content):
        """Useful to write a file to a given path with a given content.
        :param path: string, the full path of the file(instead of the file name).
        :param code_content: string, the code content you want to save.

            example:
            {
                "path": "",
                "code_content": "using UnityEngine;..."
            }
        """

guoguoguilai avatar Mar 08 '24 07:03 guoguoguilai

I too have been tormented by the multi-parameter input of tools. I used to always concatenate multiple inputs with a special character, such as '#'. But now I have found a very effective method, and I hope it can help everyone.

    @tool("Save the given code to a given path")
    def save_code(path, code_content):
        """Useful to write a file to a given path with a given content.
        :param path: string, the full path of the file(instead of the file name).
        :param code_content: string, the code content you want to save.

            example:
            {
                "path": "",
                "code_content": "using UnityEngine;..."
            }
        """

MVP

guymorita avatar Jun 11 '24 16:06 guymorita