Compare commits

...

2 Commits

Author SHA1 Message Date
Devin AI
e5a117c4ca Re-run CI for cancelled tests
Co-Authored-By: João <joao@crewai.com>
2025-11-11 08:02:47 +00:00
Devin AI
230af2749b Fix #3883: Add compatibility alias for tool decorator in crewai_tools
- Add tool function to crewai_tools/__init__.py that forwards to crewai.tools.tool
- Implement lazy import to avoid import-time side effects
- Add deprecation warning directing users to canonical import path
- Add comprehensive test coverage for the tool alias
- All tests passing (212 passed, 1 skipped)

The tool decorator can now be imported from crewai_tools for backward compatibility,
while users are encouraged to use 'from crewai.tools import tool' going forward.

Co-Authored-By: João <joao@crewai.com>
2025-11-11 07:58:29 +00:00
2 changed files with 199 additions and 0 deletions

View File

@@ -285,6 +285,52 @@ __all__ = [
"YoutubeVideoSearchTool",
"ZapierActionTool",
"ZapierActionTools",
"tool",
]
__version__ = "1.4.1"
def tool(*args, result_as_answer: bool = False, max_usage_count: int | None = None):
"""
Compatibility alias for the tool decorator from crewai.tools.
This function provides backward compatibility for users who import tool from crewai_tools.
The canonical import path is 'from crewai.tools import tool'.
Args:
*args: Positional arguments for the tool decorator.
result_as_answer: Flag to indicate if the tool result should be used as the final agent answer.
max_usage_count: Maximum number of times this tool can be used. None means unlimited usage.
Returns:
The tool decorator from crewai.tools.
Example:
>>> from crewai_tools import tool
>>> @tool
... def my_tool(question: str) -> str:
... '''Answer a question'''
... return f"Answer to: {question}"
Note:
This is a compatibility alias. Please use 'from crewai.tools import tool' instead.
"""
import warnings
warnings.warn(
"Importing 'tool' from 'crewai_tools' is deprecated. "
"Please use 'from crewai.tools import tool' instead.",
DeprecationWarning,
stacklevel=2,
)
from crewai.tools import tool as core_tool
kwargs = {}
if result_as_answer:
kwargs["result_as_answer"] = result_as_answer
if max_usage_count is not None:
kwargs["max_usage_count"] = max_usage_count
return core_tool(*args, **kwargs)

View File

@@ -0,0 +1,153 @@
"""Tests for the tool decorator compatibility alias in crewai_tools."""
import warnings
import pytest
from crewai.tools import BaseTool
def test_tool_import_from_crewai_tools():
"""Test that tool can be imported from crewai_tools."""
from crewai_tools import tool
assert callable(tool)
def test_tool_decorator_basic_usage():
"""Test that the tool decorator works with basic usage."""
from crewai_tools import tool
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
@tool
def my_tool(question: str) -> str:
"""Answer a question."""
return f"Answer to: {question}"
assert len(w) == 1
assert issubclass(w[0].category, DeprecationWarning)
assert "from crewai.tools import tool" in str(w[0].message)
assert isinstance(my_tool, BaseTool)
assert my_tool.name == "my_tool"
assert "Answer a question" in my_tool.description
assert my_tool.func("test") == "Answer to: test"
def test_tool_decorator_with_name():
"""Test that the tool decorator works with a custom name."""
from crewai_tools import tool
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
@tool("Custom Tool Name")
def my_tool(question: str) -> str:
"""Answer a question."""
return f"Answer to: {question}"
assert len(w) == 1
assert issubclass(w[0].category, DeprecationWarning)
assert isinstance(my_tool, BaseTool)
assert my_tool.name == "Custom Tool Name"
assert "Answer a question" in my_tool.description
assert my_tool.func("test") == "Answer to: test"
def test_tool_decorator_with_result_as_answer():
"""Test that the tool decorator works with result_as_answer parameter."""
from crewai_tools import tool
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
@tool("My Tool", result_as_answer=True)
def my_tool(question: str) -> str:
"""Answer a question."""
return f"Answer to: {question}"
assert len(w) == 1
assert isinstance(my_tool, BaseTool)
assert my_tool.name == "My Tool"
assert my_tool.result_as_answer is True
def test_tool_decorator_with_max_usage_count():
"""Test that the tool decorator works with max_usage_count parameter."""
from crewai_tools import tool
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
@tool("My Tool", max_usage_count=5)
def my_tool(question: str) -> str:
"""Answer a question."""
return f"Answer to: {question}"
assert len(w) == 1
assert isinstance(my_tool, BaseTool)
assert my_tool.name == "My Tool"
assert my_tool.max_usage_count == 5
assert my_tool.current_usage_count == 0
def test_tool_decorator_with_all_parameters():
"""Test that the tool decorator works with all parameters."""
from crewai_tools import tool
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always")
@tool("Custom Name", result_as_answer=True, max_usage_count=3)
def my_tool(question: str) -> str:
"""Answer a question."""
return f"Answer to: {question}"
assert len(w) == 1
assert isinstance(my_tool, BaseTool)
assert my_tool.name == "Custom Name"
assert my_tool.result_as_answer is True
assert my_tool.max_usage_count == 3
def test_tool_alias_matches_core_behavior():
"""Test that the alias behaves identically to the core tool decorator."""
from crewai.tools import tool as core_tool
from crewai_tools import tool as alias_tool
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
@alias_tool
def my_test_tool(question: str) -> str:
"""Answer a question."""
return f"Answer: {question}"
@core_tool
def my_test_tool_core(question: str) -> str:
"""Answer a question."""
return f"Answer: {question}"
assert type(my_test_tool) == type(my_test_tool_core)
assert my_test_tool.func("test") == my_test_tool_core.func("test")
assert my_test_tool.result_as_answer == my_test_tool_core.result_as_answer
assert my_test_tool.max_usage_count == my_test_tool_core.max_usage_count
def test_tool_requires_docstring():
"""Test that the tool decorator requires a docstring."""
from crewai_tools import tool
with warnings.catch_warnings(record=True):
warnings.simplefilter("always")
with pytest.raises(ValueError, match="Function must have a docstring"):
@tool
def my_tool(question: str) -> str:
return f"Answer to: {question}"