Compare commits

...

2 Commits

Author SHA1 Message Date
Devin AI
c06478afcc Fix lint and type-checker issues
- Add 'cast' import to fix mypy type compatibility error
- Remove unused imports to fix lint warnings
- Add assertions to reproduction script to use agent variables
- All custom tool patterns now work correctly

Co-Authored-By: João <joao@crewai.com>
2025-07-27 01:54:23 +00:00
Devin AI
b3be8a6588 Fix custom tool registration in CrewAI 0.150.0
- Update validate_tools method in BaseAgent to accept all documented tool patterns:
  * Function tools (with or without @tool decorator)
  * Dict-based tool definitions
  * BaseTool class inheritance
  * Direct function assignment
- Change tools field type annotation from List[BaseTool] to List[Any] to allow Pydantic validation
- Update parse_tools function to accept all BaseTool instances (not just CrewAITool)
- Add comprehensive tests covering all custom tool patterns from issue #3226
- Add reproduction script to verify all patterns work correctly

Fixes #3226

Co-Authored-By: João <joao@crewai.com>
2025-07-27 01:46:12 +00:00
5 changed files with 410 additions and 22 deletions

144
reproduce_issue_3226.py Normal file
View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""
Reproduction script for issue #3226: Cannot Register Custom Tools with Agents in CrewAI 0.150.0
This script tests all the failing patterns mentioned in the issue.
"""
import sys
import traceback
def test_function_tool():
"""Test 1: Function Tool with @tool decorator"""
print("=== Test 1: Function Tool with @tool decorator ===")
try:
from crewai.tools import tool
from crewai import Agent
@tool
def fetch_logs(query: str) -> str:
"""Fetch logs from New Relic based on query"""
return f"Logs for query: {query}"
agent = Agent(
role='CrashFetcher',
goal='Extract logs',
backstory='An agent that fetches logs',
tools=[fetch_logs],
allow_delegation=False
)
assert len(agent.tools) == 1, f"Expected 1 tool, got {len(agent.tools)}"
print("✅ Function tool with @tool decorator: SUCCESS")
return True
except Exception as e:
print(f"❌ Function tool with @tool decorator: FAILED - {e}")
traceback.print_exc()
return False
def test_dict_tool():
"""Test 2: Dict-based tool definition"""
print("\n=== Test 2: Dict-based tool definition ===")
try:
from crewai import Agent
def fetch_logs_func(query: str) -> str:
return f"Logs for query: {query}"
fetch_logs_dict = {
'name': 'fetch_logs',
'description': 'Fetch logs from New Relic',
'func': fetch_logs_func
}
agent = Agent(
role='CrashFetcher',
goal='Extract logs',
backstory='An agent that fetches logs',
tools=[fetch_logs_dict],
allow_delegation=False
)
assert len(agent.tools) == 1, f"Expected 1 tool, got {len(agent.tools)}"
print("✅ Dict-based tool: SUCCESS")
return True
except Exception as e:
print(f"❌ Dict-based tool: FAILED - {e}")
traceback.print_exc()
return False
def test_basetool_class():
"""Test 3: BaseTool class inheritance"""
print("\n=== Test 3: BaseTool class inheritance ===")
try:
from crewai.tools import BaseTool
from crewai import Agent
class FetchLogsTool(BaseTool):
name: str = "fetch_logs"
description: str = "Fetch logs from New Relic based on query"
def _run(self, query: str) -> str:
return f"Logs for query: {query}"
agent = Agent(
role='CrashFetcher',
goal='Extract logs',
backstory='An agent that fetches logs',
tools=[FetchLogsTool()],
allow_delegation=False
)
assert len(agent.tools) == 1, f"Expected 1 tool, got {len(agent.tools)}"
print("✅ BaseTool class inheritance: SUCCESS")
return True
except Exception as e:
print(f"❌ BaseTool class inheritance: FAILED - {e}")
traceback.print_exc()
return False
def test_direct_function():
"""Test 4: Direct function assignment"""
print("\n=== Test 4: Direct function assignment ===")
try:
from crewai import Agent
def fetch_logs(query: str) -> str:
"""Fetch logs from New Relic based on query"""
return f"Logs for query: {query}"
agent = Agent(
role='CrashFetcher',
goal='Extract logs',
backstory='An agent that fetches logs',
tools=[fetch_logs],
allow_delegation=False
)
assert len(agent.tools) == 1, f"Expected 1 tool, got {len(agent.tools)}"
print("✅ Direct function assignment: SUCCESS")
return True
except Exception as e:
print(f"❌ Direct function assignment: FAILED - {e}")
traceback.print_exc()
return False
def main():
"""Run all tests and report results"""
print("Testing custom tool registration patterns from issue #3226\n")
results = []
results.append(test_function_tool())
results.append(test_dict_tool())
results.append(test_basetool_class())
results.append(test_direct_function())
print("\n=== SUMMARY ===")
passed = sum(results)
total = len(results)
print(f"Tests passed: {passed}/{total}")
if passed == total:
print("🎉 All custom tool patterns are working!")
return 0
else:
print("💥 Some custom tool patterns are still broken")
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -2,7 +2,7 @@ import uuid
from abc import ABC, abstractmethod
from copy import copy as shallow_copy
from hashlib import md5
from typing import Any, Callable, Dict, List, Optional, TypeVar
from typing import Any, Callable, Dict, List, Optional, TypeVar, cast
from pydantic import (
UUID4,
@@ -25,7 +25,6 @@ from crewai.security.security_config import SecurityConfig
from crewai.tools.base_tool import BaseTool, Tool
from crewai.utilities import I18N, Logger, RPMController
from crewai.utilities.config import process_config
from crewai.utilities.converter import Converter
from crewai.utilities.string_utils import interpolate_only
T = TypeVar("T", bound="BaseAgent")
@@ -108,7 +107,7 @@ class BaseAgent(ABC, BaseModel):
default=False,
description="Enable agent to delegate and ask questions among each other.",
)
tools: Optional[List[BaseTool]] = Field(
tools: Optional[List[Any]] = Field(
default_factory=list, description="Tools at agents' disposal"
)
max_iter: int = Field(
@@ -171,27 +170,48 @@ class BaseAgent(ABC, BaseModel):
def validate_tools(cls, tools: List[Any]) -> List[BaseTool]:
"""Validate and process the tools provided to the agent.
This method ensures that each tool is either an instance of BaseTool
or an object with 'name', 'func', and 'description' attributes. If the
tool meets these criteria, it is processed and added to the list of
tools. Otherwise, a ValueError is raised.
This method ensures that each tool is either an instance of BaseTool,
a function (with or without @tool decorator), a dict with tool definition,
or an object with 'name', 'func', and 'description' attributes.
"""
if not tools:
return []
from crewai.tools.base_tool import tool
processed_tools = []
required_attrs = ["name", "func", "description"]
for tool in tools:
if isinstance(tool, BaseTool):
processed_tools.append(tool)
elif all(hasattr(tool, attr) for attr in required_attrs):
# Tool has the required attributes, create a Tool instance
processed_tools.append(Tool.from_langchain(tool))
for tool_item in tools:
if isinstance(tool_item, BaseTool):
processed_tools.append(tool_item)
elif callable(tool_item):
if hasattr(tool_item, '__doc__') and tool_item.__doc__:
converted_tool = cast(BaseTool, tool(tool_item))
processed_tools.append(converted_tool)
else:
raise ValueError(
f"Function tool '{tool_item.__name__}' must have a docstring"
)
elif isinstance(tool_item, dict):
required_keys = ['name', 'description', 'func']
if all(key in tool_item for key in required_keys):
processed_tools.append(Tool(
name=tool_item['name'],
description=tool_item['description'],
func=tool_item['func']
))
else:
raise ValueError(
f"Dict tool must contain keys: {required_keys}. "
f"Got: {list(tool_item.keys())}"
)
elif hasattr(tool_item, 'name') and hasattr(tool_item, 'func') and hasattr(tool_item, 'description'):
processed_tools.append(Tool.from_langchain(tool_item))
else:
raise ValueError(
f"Invalid tool type: {type(tool)}. "
"Tool must be an instance of BaseTool or "
"an object with 'name', 'func', and 'description' attributes."
f"Invalid tool type: {type(tool_item)}. "
"Tool must be a BaseTool instance, function, dict with "
"'name'/'description'/'func' keys, or object with "
"'name'/'func'/'description' attributes."
)
return processed_tools

View File

@@ -1,7 +1,8 @@
from .base_tool import BaseTool, tool, EnvVar
from .base_tool import BaseTool, tool, EnvVar, Tool
__all__ = [
"BaseTool",
"Tool",
"tool",
"EnvVar",
]
]

View File

@@ -11,7 +11,6 @@ from crewai.agents.parser import (
)
from crewai.llm import LLM
from crewai.llms.base_llm import BaseLLM
from crewai.tools import BaseTool as CrewAITool
from crewai.tools.base_tool import BaseTool
from crewai.tools.structured_tool import CrewStructuredTool
from crewai.tools.tool_types import ToolResult
@@ -30,10 +29,13 @@ def parse_tools(tools: List[BaseTool]) -> List[CrewStructuredTool]:
tools_list = []
for tool in tools:
if isinstance(tool, CrewAITool):
if isinstance(tool, BaseTool):
tools_list.append(tool.to_structured_tool())
else:
raise ValueError("Tool is not a CrewStructuredTool or BaseTool")
raise ValueError(
f"Tool must be a BaseTool instance, got {type(tool)}. "
"Ensure tools are properly validated before calling parse_tools."
)
return tools_list

221
tests/test_custom_tools.py Normal file
View File

@@ -0,0 +1,221 @@
"""
Test custom tool registration patterns to ensure all documented patterns work correctly.
This addresses issue #3226 where custom tool registration was broken in CrewAI 0.150.0.
"""
import pytest
from crewai import Agent
from crewai.tools import BaseTool, tool
class TestCustomToolPatterns:
"""Test all custom tool patterns mentioned in issue #3226."""
def test_function_tool_with_decorator(self):
"""Test function tool with @tool decorator."""
@tool
def fetch_logs(query: str) -> str:
"""Fetch logs from New Relic based on query"""
return f"Logs for query: {query}"
agent = Agent(
role='CrashFetcher',
goal='Extract logs',
backstory='An agent that fetches logs',
tools=[fetch_logs],
allow_delegation=False
)
assert len(agent.tools) == 1
assert agent.tools[0].name == "fetch_logs"
assert "Fetch logs from New Relic" in agent.tools[0].description
def test_dict_based_tool(self):
"""Test dict-based tool definition."""
def fetch_logs_func(query: str) -> str:
return f"Logs for query: {query}"
fetch_logs_dict = {
'name': 'fetch_logs',
'description': 'Fetch logs from New Relic',
'func': fetch_logs_func
}
agent = Agent(
role='CrashFetcher',
goal='Extract logs',
backstory='An agent that fetches logs',
tools=[fetch_logs_dict],
allow_delegation=False
)
assert len(agent.tools) == 1
assert agent.tools[0].name == "fetch_logs"
assert "Fetch logs from New Relic" in agent.tools[0].description
def test_basetool_class_inheritance(self):
"""Test BaseTool class inheritance."""
class FetchLogsTool(BaseTool):
name: str = "fetch_logs"
description: str = "Fetch logs from New Relic based on query"
def _run(self, query: str) -> str:
return f"Logs for query: {query}"
agent = Agent(
role='CrashFetcher',
goal='Extract logs',
backstory='An agent that fetches logs',
tools=[FetchLogsTool()],
allow_delegation=False
)
assert len(agent.tools) == 1
assert agent.tools[0].name == "fetch_logs"
assert "Fetch logs from New Relic" in agent.tools[0].description
def test_direct_function_assignment(self):
"""Test direct function assignment."""
def fetch_logs(query: str) -> str:
"""Fetch logs from New Relic based on query"""
return f"Logs for query: {query}"
agent = Agent(
role='CrashFetcher',
goal='Extract logs',
backstory='An agent that fetches logs',
tools=[fetch_logs],
allow_delegation=False
)
assert len(agent.tools) == 1
assert agent.tools[0].name == "fetch_logs"
assert "Fetch logs from New Relic" in agent.tools[0].description
def test_mixed_tool_types(self):
"""Test mixing different tool types in the same agent."""
@tool
def decorated_tool(query: str) -> str:
"""A decorated tool"""
return f"Decorated: {query}"
class ClassTool(BaseTool):
name: str = "class_tool"
description: str = "A class-based tool"
def _run(self, query: str) -> str:
return f"Class: {query}"
def function_tool(query: str) -> str:
"""A function tool"""
return f"Function: {query}"
dict_tool = {
'name': 'dict_tool',
'description': 'A dict-based tool',
'func': lambda query: f"Dict: {query}"
}
agent = Agent(
role='MultiTool',
goal='Use multiple tool types',
backstory='An agent with various tools',
tools=[decorated_tool, ClassTool(), function_tool, dict_tool],
allow_delegation=False
)
assert len(agent.tools) == 4
tool_names = [tool.name for tool in agent.tools]
assert "decorated_tool" in tool_names
assert "class_tool" in tool_names
assert "function_tool" in tool_names
assert "dict_tool" in tool_names
def test_invalid_tool_types(self):
"""Test that invalid tool types raise appropriate errors."""
with pytest.raises(ValueError, match="Invalid tool type"):
Agent(
role='Test',
goal='Test invalid tools',
backstory='Testing',
tools=["invalid_string_tool"],
allow_delegation=False
)
with pytest.raises(ValueError, match="Invalid tool type"):
Agent(
role='Test',
goal='Test invalid tools',
backstory='Testing',
tools=[123],
allow_delegation=False
)
def test_function_without_docstring_fails(self):
"""Test that functions without docstrings fail validation."""
def no_docstring_func(query: str) -> str:
return f"No docstring: {query}"
with pytest.raises(ValueError, match="must have a docstring"):
Agent(
role='Test',
goal='Test function without docstring',
backstory='Testing',
tools=[no_docstring_func],
allow_delegation=False
)
def test_incomplete_dict_tool_fails(self):
"""Test that dict tools missing required keys fail validation."""
incomplete_dict = {
'name': 'incomplete',
'description': 'Missing func key'
}
with pytest.raises(ValueError, match="Dict tool must contain keys"):
Agent(
role='Test',
goal='Test incomplete dict tool',
backstory='Testing',
tools=[incomplete_dict],
allow_delegation=False
)
def test_tool_execution(self):
"""Test that tools can actually be executed."""
@tool
def test_execution_tool(message: str) -> str:
"""A tool for testing execution"""
return f"Executed: {message}"
agent = Agent(
role='Executor',
goal='Execute tools',
backstory='An agent that executes tools',
tools=[test_execution_tool],
allow_delegation=False
)
tool_instance = agent.tools[0]
result = tool_instance.run(message="test")
assert result == "Executed: test"
def test_tool_with_multiple_parameters(self):
"""Test tools with multiple parameters work correctly."""
@tool
def multi_param_tool(param1: str, param2: int, param3: bool = True) -> str:
"""A tool with multiple parameters"""
return f"p1={param1}, p2={param2}, p3={param3}"
agent = Agent(
role='MultiParam',
goal='Use multi-parameter tools',
backstory='An agent with complex tools',
tools=[multi_param_tool],
allow_delegation=False
)
assert len(agent.tools) == 1
tool_instance = agent.tools[0]
result = tool_instance.run(param1="test", param2=42, param3=False)
assert result == "p1=test, p2=42, p3=False"