From 230af2749bb6fc5b4cc4fa33205aa54cec2abd68 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 07:58:29 +0000 Subject: [PATCH] Fix #3883: Add compatibility alias for tool decorator in crewai_tools MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- lib/crewai-tools/src/crewai_tools/__init__.py | 46 ++++++ lib/crewai-tools/tests/test_tool_alias.py | 153 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 lib/crewai-tools/tests/test_tool_alias.py diff --git a/lib/crewai-tools/src/crewai_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/__init__.py index b9fc39f3e..90a89993b 100644 --- a/lib/crewai-tools/src/crewai_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/__init__.py @@ -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) diff --git a/lib/crewai-tools/tests/test_tool_alias.py b/lib/crewai-tools/tests/test_tool_alias.py new file mode 100644 index 000000000..5b4704630 --- /dev/null +++ b/lib/crewai-tools/tests/test_tool_alias.py @@ -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}"