mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-08 23:58:34 +00:00
- 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>
351 lines
10 KiB
Python
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"})
|