mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-05 22:28:29 +00:00
Compare commits
2 Commits
devin/1757
...
devin/1753
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c06478afcc | ||
|
|
b3be8a6588 |
144
reproduce_issue_3226.py
Normal file
144
reproduce_issue_3226.py
Normal 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())
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from .base_tool import BaseTool, tool, EnvVar
|
||||
from .base_tool import BaseTool, tool, EnvVar, Tool
|
||||
|
||||
__all__ = [
|
||||
"BaseTool",
|
||||
"Tool",
|
||||
"tool",
|
||||
"EnvVar",
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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
221
tests/test_custom_tools.py
Normal 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"
|
||||
Reference in New Issue
Block a user