mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-08 07:38:29 +00:00
Fix issue #2383: Add invoke method to BaseTool for models without function calling support
Co-Authored-By: Joe Moura <joao@crewai.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import warnings
|
||||
from abc import ABC, abstractmethod
|
||||
from inspect import signature
|
||||
from typing import Any, Callable, Type, get_args, get_origin
|
||||
from typing import Any, Callable, Dict, Optional, Type, Union, get_args, get_origin
|
||||
|
||||
from pydantic import (
|
||||
BaseModel,
|
||||
@@ -75,6 +75,46 @@ class BaseTool(BaseModel, ABC):
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""Here goes the actual implementation of the tool."""
|
||||
|
||||
def invoke(
|
||||
self, input: Union[str, dict], config: Optional[dict] = None, **kwargs: Any
|
||||
) -> Any:
|
||||
"""Main method for tool execution.
|
||||
|
||||
This method provides a fallback implementation for models that don't support
|
||||
function calling natively (like QwQ-32B-Preview and deepseek-chat).
|
||||
It parses the input and calls the _run method with the appropriate arguments.
|
||||
"""
|
||||
if isinstance(input, str):
|
||||
# Try to parse as JSON if it's a string
|
||||
try:
|
||||
import json
|
||||
input = json.loads(input)
|
||||
except json.JSONDecodeError:
|
||||
# If not valid JSON, pass as a single argument
|
||||
return self._run(input)
|
||||
|
||||
if not isinstance(input, dict):
|
||||
# If input is not a dict after parsing, pass it directly
|
||||
return self._run(input)
|
||||
|
||||
# Get the expected arguments from the schema
|
||||
if hasattr(self, 'args_schema') and self.args_schema is not None:
|
||||
try:
|
||||
# Extract argument names from the schema
|
||||
arg_names = list(self.args_schema.model_json_schema()["properties"].keys())
|
||||
|
||||
# Filter the input to only include valid arguments
|
||||
filtered_args = {k: v for k, v in input.items() if k in arg_names}
|
||||
|
||||
# Call _run with the filtered arguments
|
||||
return self._run(**filtered_args)
|
||||
except Exception:
|
||||
# Fallback to passing the entire input dict if schema parsing fails
|
||||
pass
|
||||
|
||||
# If we couldn't parse the schema or there was an error, just pass the input dict
|
||||
return self._run(**input)
|
||||
|
||||
def to_structured_tool(self) -> CrewStructuredTool:
|
||||
"""Convert this tool to a CrewStructuredTool instance."""
|
||||
|
||||
55
tests/tools/test_invoke_method.py
Normal file
55
tests/tools/test_invoke_method.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from typing import Type
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from crewai.tools import BaseTool
|
||||
|
||||
|
||||
class TestToolInput(BaseModel):
|
||||
param: str = Field(description="A test parameter")
|
||||
|
||||
|
||||
class TestTool(BaseTool):
|
||||
name: str = "Test Tool"
|
||||
description: str = "A tool for testing the invoke method"
|
||||
args_schema: Type[BaseModel] = TestToolInput
|
||||
|
||||
def _run(self, param: str) -> str:
|
||||
return f"Tool executed with: {param}"
|
||||
|
||||
|
||||
def test_invoke_with_dict():
|
||||
"""Test that invoke works with a dictionary input."""
|
||||
tool = TestTool()
|
||||
result = tool.invoke(input={"param": "test value"})
|
||||
assert result == "Tool executed with: test value"
|
||||
|
||||
|
||||
def test_invoke_with_json_string():
|
||||
"""Test that invoke works with a JSON string input."""
|
||||
tool = TestTool()
|
||||
result = tool.invoke(input='{"param": "test value"}')
|
||||
assert result == "Tool executed with: test value"
|
||||
|
||||
|
||||
def test_invoke_with_raw_string():
|
||||
"""Test that invoke works with a raw string input."""
|
||||
tool = TestTool()
|
||||
result = tool.invoke(input="test value")
|
||||
assert result == "Tool executed with: test value"
|
||||
|
||||
|
||||
def test_invoke_with_empty_dict():
|
||||
"""Test that invoke handles empty dict input appropriately."""
|
||||
tool = TestTool()
|
||||
with pytest.raises(Exception):
|
||||
# Should raise an exception since param is required
|
||||
tool.invoke(input={})
|
||||
|
||||
|
||||
def test_invoke_with_extra_args():
|
||||
"""Test that invoke filters out extra arguments not in the schema."""
|
||||
tool = TestTool()
|
||||
result = tool.invoke(input={"param": "test value", "extra": "ignored"})
|
||||
assert result == "Tool executed with: test value"
|
||||
Reference in New Issue
Block a user