Files
crewAI/tests/tools/test_structured_tool.py
Devin AI 75b7c579f6 Fix type-checking errors in structured_tool.py and tests
- Add comprehensive type annotations for all function parameters and return types
- Fix generic type parameters for dict, Callable, and Flow classes
- Add proper type ignore comments for complex type inference scenarios
- Resolve all 27 mypy errors across Python 3.10-3.13
- Ensure compatibility with strict type checking requirements

Co-Authored-By: João <joao@crewai.com>
2025-09-04 08:58:43 +00:00

351 lines
10 KiB
Python

from collections.abc import Callable
from typing import Any, Optional
import pytest
from pydantic import BaseModel, Field
from crewai.tools import BaseTool
from crewai.tools.structured_tool import CrewStructuredTool
# Test fixtures
@pytest.fixture
def basic_function() -> Callable[[str, int], str]:
def test_func(param1: str, param2: int = 0) -> str:
"""Test function with basic params."""
return f"{param1} {param2}"
return test_func
@pytest.fixture
def schema_class() -> type[BaseModel]:
class TestSchema(BaseModel):
param1: str
param2: int = Field(default=0)
return TestSchema
def test_initialization(
basic_function: Callable[[str], str], schema_class: type[BaseModel]
) -> None:
"""Test basic initialization of CrewStructuredTool"""
tool = CrewStructuredTool(
name="test_tool",
description="Test tool description",
func=basic_function,
args_schema=schema_class,
)
assert tool.name == "test_tool"
assert tool.description == "Test tool description"
assert tool.func == basic_function
assert tool.args_schema == schema_class
def test_from_function(basic_function: Callable[[str], str]) -> None:
"""Test creating tool from function"""
tool = CrewStructuredTool.from_function(
func=basic_function, name="test_tool", description="Test description"
)
assert tool.name == "test_tool"
assert tool.description == "Test description"
assert tool.func == basic_function
assert isinstance(tool.args_schema, type(BaseModel))
def test_validate_function_signature(
basic_function: Callable[[str, int], str], schema_class: type[BaseModel]
) -> None:
"""Test function signature validation"""
tool = CrewStructuredTool(
name="test_tool",
description="Test tool",
func=basic_function,
args_schema=schema_class,
)
# Should not raise any exceptions
tool._validate_function_signature()
@pytest.mark.asyncio
async def test_ainvoke(basic_function: Callable[[str, int], str]) -> None:
"""Test asynchronous invocation"""
tool = CrewStructuredTool.from_function(func=basic_function, name="test_tool")
result = await tool.ainvoke(input={"param1": "test"})
assert result == "test 0"
def test_parse_args_dict(basic_function: Callable[[str, int], str]) -> None:
"""Test parsing dictionary arguments"""
tool = CrewStructuredTool.from_function(func=basic_function, name="test_tool")
parsed = tool._parse_args({"param1": "test", "param2": 42})
assert parsed["param1"] == "test"
assert parsed["param2"] == 42
def test_parse_args_string(basic_function: Callable[[str, int], str]) -> None:
"""Test parsing string arguments"""
tool = CrewStructuredTool.from_function(func=basic_function, name="test_tool")
parsed = tool._parse_args('{"param1": "test", "param2": 42}')
assert parsed["param1"] == "test"
assert parsed["param2"] == 42
def test_complex_types() -> None:
"""Test handling of complex parameter types"""
def complex_func(nested: dict[str, Any], items: list[Any]) -> str:
"""Process complex types."""
return f"Processed {len(items)} items with {len(nested)} nested keys"
tool = CrewStructuredTool.from_function(
func=complex_func, name="test_tool", description="Test complex types"
)
result = tool.invoke({"nested": {"key": "value"}, "items": [1, 2, 3]})
assert result == "Processed 3 items with 1 nested keys"
def test_schema_inheritance() -> None:
"""Test tool creation with inherited schema"""
def extended_func(base_param: str, extra_param: int) -> str:
"""Test function with inherited schema."""
return f"{base_param} {extra_param}"
class BaseSchema(BaseModel):
base_param: str
class ExtendedSchema(BaseSchema):
extra_param: int
tool = CrewStructuredTool.from_function(
func=extended_func, name="test_tool", args_schema=ExtendedSchema
)
result = tool.invoke({"base_param": "test", "extra_param": 42})
assert result == "test 42"
def test_default_values_in_schema() -> None:
"""Test handling of default values in schema"""
def default_func(
required_param: str,
optional_param: str = "default",
nullable_param: Optional[int] = None,
) -> str:
"""Test function with default values."""
return f"{required_param} {optional_param} {nullable_param}"
tool = CrewStructuredTool.from_function(
func=default_func, name="test_tool", description="Test defaults"
)
# Test with minimal parameters
result = tool.invoke({"required_param": "test"})
assert result == "test default None"
# Test with all parameters
result = tool.invoke(
{"required_param": "test", "optional_param": "custom", "nullable_param": 42}
)
assert result == "test custom 42"
@pytest.fixture
def custom_tool_decorator() -> Any:
from crewai.tools import tool
@tool("custom_tool", result_as_answer=True)
async def custom_tool() -> str:
"""This is a tool that does something"""
return "Hello World from Custom Tool"
return custom_tool
@pytest.fixture
def custom_tool() -> BaseTool:
from crewai.tools import BaseTool
class CustomTool(BaseTool):
name: str = "my_tool"
description: str = "This is a tool that does something"
result_as_answer: bool = True
async def _run(self) -> str:
return "Hello World from Custom Tool"
return CustomTool()
def build_simple_crew(tool: Any) -> Any:
from crewai import Agent, Crew, Task
agent1 = Agent(
role="Simple role",
goal="Simple goal",
backstory="Simple backstory",
tools=[tool],
)
say_hi_task = Task(
description="Use the custom tool result as answer.",
agent=agent1,
expected_output="Use the tool result",
)
crew = Crew(agents=[agent1], tasks=[say_hi_task])
return crew
@pytest.mark.vcr(filter_headers=["authorization"])
def test_async_tool_using_within_isolated_crew(custom_tool: BaseTool) -> None:
crew = build_simple_crew(custom_tool)
result = crew.kickoff()
assert result.raw == "Hello World from Custom Tool"
@pytest.mark.vcr(filter_headers=["authorization"])
def test_async_tool_using_decorator_within_isolated_crew(
custom_tool_decorator: Any,
) -> None:
crew = build_simple_crew(custom_tool_decorator)
result = crew.kickoff()
assert result.raw == "Hello World from Custom Tool"
@pytest.mark.vcr(filter_headers=["authorization"])
def test_async_tool_within_flow(custom_tool: BaseTool) -> None:
from crewai.flow.flow import Flow, start
class StructuredExampleFlow(Flow): # type: ignore[type-arg]
@start()
async def start(self) -> Any:
crew = build_simple_crew(custom_tool)
result = await crew.kickoff_async()
return result
flow = StructuredExampleFlow()
result = flow.kickoff()
assert result.raw == "Hello World from Custom Tool"
@pytest.mark.vcr(filter_headers=["authorization"])
def test_async_tool_using_decorator_within_flow(custom_tool_decorator: Any) -> None:
from crewai.flow.flow import Flow, start
class StructuredExampleFlow(Flow): # type: ignore[type-arg]
@start()
async def start(self) -> Any:
crew = build_simple_crew(custom_tool_decorator)
result = await crew.kickoff_async()
return result
flow = StructuredExampleFlow()
result = flow.kickoff()
assert result.raw == "Hello World from Custom Tool"
def test_invoke_sync_function_single_execution() -> None:
"""Test that sync functions are called only once, not twice."""
call_count = 0
def counting_func(message: str) -> str:
nonlocal call_count
call_count += 1
return f"Called {call_count} times with: {message}"
tool = CrewStructuredTool.from_function(
func=counting_func, name="counting_tool", description="A tool that counts calls"
)
result = tool.invoke({"message": "test"})
assert call_count == 1, f"Function was called {call_count} times, expected 1"
assert result == "Called 1 times with: test"
def test_invoke_async_function_outside_event_loop() -> None:
"""Test that async functions work correctly when called outside event loop."""
async def async_func(message: str) -> str:
return f"Async result: {message}"
tool = CrewStructuredTool.from_function(
func=async_func, name="async_tool", description="An async tool"
)
result = tool.invoke({"message": "test"})
assert result == "Async result: test"
@pytest.mark.asyncio
async def test_invoke_async_function_in_event_loop_raises_error() -> None:
"""Test that async functions raise RuntimeError when called from within event loop."""
async def async_func(message: str) -> str:
return f"Async result: {message}"
tool = CrewStructuredTool.from_function(
func=async_func, name="async_tool", description="An async tool"
)
with pytest.raises(
RuntimeError,
match="Cannot call async tool.*from synchronous context within an event loop",
):
tool.invoke({"message": "test"})
def test_invoke_sync_function_returning_coroutine() -> None:
"""Test handling of sync functions that return coroutines."""
async def inner_async(message: str) -> str:
return f"Inner async: {message}"
def sync_func_returning_coro(message: str) -> Any:
return inner_async(message)
tool = CrewStructuredTool.from_function(
func=sync_func_returning_coro,
name="sync_coro_tool",
description="A sync tool that returns coroutine",
)
result = tool.invoke({"message": "test"})
assert result == "Inner async: test"
@pytest.mark.asyncio
async def test_invoke_sync_function_returning_coroutine_in_event_loop_raises_error() -> (
None
):
"""Test that sync functions returning coroutines raise RuntimeError in event loop."""
async def inner_async(message: str) -> str:
return f"Inner async: {message}"
def sync_func_returning_coro(message: str) -> Any:
return inner_async(message)
tool = CrewStructuredTool.from_function(
func=sync_func_returning_coro,
name="sync_coro_tool",
description="A sync tool that returns coroutine",
)
with pytest.raises(
RuntimeError,
match="Sync function.*returned a coroutine but we're in an event loop",
):
tool.invoke({"message": "test"})