mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-16 04:18:35 +00:00
Support async tool executions (#2983)
* test: fix structured tool tests No tests were being executed from this file * feat: support to run async tool Some Tool requires async execution. This commit allow us to collect tool result from coroutines * docs: add docs about asynchronous tool support
This commit is contained in:
@@ -32,6 +32,7 @@ The Enterprise Tools Repository includes:
|
|||||||
- **Customizability**: Provides the flexibility to develop custom tools or utilize existing ones, catering to the specific needs of agents.
|
- **Customizability**: Provides the flexibility to develop custom tools or utilize existing ones, catering to the specific needs of agents.
|
||||||
- **Error Handling**: Incorporates robust error handling mechanisms to ensure smooth operation.
|
- **Error Handling**: Incorporates robust error handling mechanisms to ensure smooth operation.
|
||||||
- **Caching Mechanism**: Features intelligent caching to optimize performance and reduce redundant operations.
|
- **Caching Mechanism**: Features intelligent caching to optimize performance and reduce redundant operations.
|
||||||
|
- **Asynchronous Support**: Handles both synchronous and asynchronous tools, enabling non-blocking operations.
|
||||||
|
|
||||||
## Using CrewAI Tools
|
## Using CrewAI Tools
|
||||||
|
|
||||||
@@ -177,6 +178,62 @@ class MyCustomTool(BaseTool):
|
|||||||
return "Tool's result"
|
return "Tool's result"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Asynchronous Tool Support
|
||||||
|
|
||||||
|
CrewAI supports asynchronous tools, allowing you to implement tools that perform non-blocking operations like network requests, file I/O, or other async operations without blocking the main execution thread.
|
||||||
|
|
||||||
|
### Creating Async Tools
|
||||||
|
|
||||||
|
You can create async tools in two ways:
|
||||||
|
|
||||||
|
#### 1. Using the `tool` Decorator with Async Functions
|
||||||
|
|
||||||
|
```python Code
|
||||||
|
from crewai.tools import tool
|
||||||
|
|
||||||
|
@tool("fetch_data_async")
|
||||||
|
async def fetch_data_async(query: str) -> str:
|
||||||
|
"""Asynchronously fetch data based on the query."""
|
||||||
|
# Simulate async operation
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
return f"Data retrieved for {query}"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Implementing Async Methods in Custom Tool Classes
|
||||||
|
|
||||||
|
```python Code
|
||||||
|
from crewai.tools import BaseTool
|
||||||
|
|
||||||
|
class AsyncCustomTool(BaseTool):
|
||||||
|
name: str = "async_custom_tool"
|
||||||
|
description: str = "An asynchronous custom tool"
|
||||||
|
|
||||||
|
async def _run(self, query: str = "") -> str:
|
||||||
|
"""Asynchronously run the tool"""
|
||||||
|
# Your async implementation here
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
return f"Processed {query} asynchronously"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Async Tools
|
||||||
|
|
||||||
|
Async tools work seamlessly in both standard Crew workflows and Flow-based workflows:
|
||||||
|
|
||||||
|
```python Code
|
||||||
|
# In standard Crew
|
||||||
|
agent = Agent(role="researcher", tools=[async_custom_tool])
|
||||||
|
|
||||||
|
# In Flow
|
||||||
|
class MyFlow(Flow):
|
||||||
|
@start()
|
||||||
|
async def begin(self):
|
||||||
|
crew = Crew(agents=[agent])
|
||||||
|
result = await crew.kickoff_async()
|
||||||
|
return result
|
||||||
|
```
|
||||||
|
|
||||||
|
The CrewAI framework automatically handles the execution of both synchronous and asynchronous tools, so you don't need to worry about how to call them differently.
|
||||||
|
|
||||||
### Utilizing the `tool` Decorator
|
### Utilizing the `tool` Decorator
|
||||||
|
|
||||||
```python Code
|
```python Code
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import textwrap
|
import textwrap
|
||||||
from typing import Any, Callable, Optional, Union, get_type_hints
|
from typing import Any, Callable, Optional, Union, get_type_hints
|
||||||
@@ -239,7 +241,17 @@ class CrewStructuredTool:
|
|||||||
) -> Any:
|
) -> Any:
|
||||||
"""Main method for tool execution."""
|
"""Main method for tool execution."""
|
||||||
parsed_args = self._parse_args(input)
|
parsed_args = self._parse_args(input)
|
||||||
return self.func(**parsed_args, **kwargs)
|
|
||||||
|
if inspect.iscoroutinefunction(self.func):
|
||||||
|
result = asyncio.run(self.func(**parsed_args, **kwargs))
|
||||||
|
return result
|
||||||
|
|
||||||
|
result = self.func(**parsed_args, **kwargs)
|
||||||
|
|
||||||
|
if asyncio.iscoroutine(result):
|
||||||
|
return asyncio.run(result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def args(self) -> dict:
|
def args(self) -> dict:
|
||||||
|
|||||||
299
tests/cassettes/test_async_tool_using_decorator_within_flow.yaml
Normal file
299
tests/cassettes/test_async_tool_using_decorator_within_flow.yaml
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
235
tests/cassettes/test_async_tool_using_within_isolated_crew.yaml
Normal file
235
tests/cassettes/test_async_tool_using_within_isolated_crew.yaml
Normal file
File diff suppressed because one or more lines are too long
299
tests/cassettes/test_async_tool_within_flow.yaml
Normal file
299
tests/cassettes/test_async_tool_within_flow.yaml
Normal file
File diff suppressed because one or more lines are too long
@@ -25,122 +25,206 @@ def schema_class():
|
|||||||
return TestSchema
|
return TestSchema
|
||||||
|
|
||||||
|
|
||||||
class InternalCrewStructuredTool:
|
def test_initialization(basic_function, schema_class):
|
||||||
def test_initialization(self, basic_function, schema_class):
|
"""Test basic initialization of CrewStructuredTool"""
|
||||||
"""Test basic initialization of CrewStructuredTool"""
|
tool = CrewStructuredTool(
|
||||||
tool = CrewStructuredTool(
|
name="test_tool",
|
||||||
name="test_tool",
|
description="Test tool description",
|
||||||
description="Test tool description",
|
func=basic_function,
|
||||||
func=basic_function,
|
args_schema=schema_class,
|
||||||
args_schema=schema_class,
|
)
|
||||||
)
|
|
||||||
|
|
||||||
assert tool.name == "test_tool"
|
assert tool.name == "test_tool"
|
||||||
assert tool.description == "Test tool description"
|
assert tool.description == "Test tool description"
|
||||||
assert tool.func == basic_function
|
assert tool.func == basic_function
|
||||||
assert tool.args_schema == schema_class
|
assert tool.args_schema == schema_class
|
||||||
|
|
||||||
def test_from_function(self, basic_function):
|
def test_from_function(basic_function):
|
||||||
"""Test creating tool from function"""
|
"""Test creating tool from function"""
|
||||||
tool = CrewStructuredTool.from_function(
|
tool = CrewStructuredTool.from_function(
|
||||||
func=basic_function, name="test_tool", description="Test description"
|
func=basic_function, name="test_tool", description="Test description"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert tool.name == "test_tool"
|
assert tool.name == "test_tool"
|
||||||
assert tool.description == "Test description"
|
assert tool.description == "Test description"
|
||||||
assert tool.func == basic_function
|
assert tool.func == basic_function
|
||||||
assert isinstance(tool.args_schema, type(BaseModel))
|
assert isinstance(tool.args_schema, type(BaseModel))
|
||||||
|
|
||||||
def test_validate_function_signature(self, basic_function, schema_class):
|
def test_validate_function_signature(basic_function, schema_class):
|
||||||
"""Test function signature validation"""
|
"""Test function signature validation"""
|
||||||
tool = CrewStructuredTool(
|
tool = CrewStructuredTool(
|
||||||
name="test_tool",
|
name="test_tool",
|
||||||
description="Test tool",
|
description="Test tool",
|
||||||
func=basic_function,
|
func=basic_function,
|
||||||
args_schema=schema_class,
|
args_schema=schema_class,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Should not raise any exceptions
|
# Should not raise any exceptions
|
||||||
tool._validate_function_signature()
|
tool._validate_function_signature()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_ainvoke(self, basic_function):
|
async def test_ainvoke(basic_function):
|
||||||
"""Test asynchronous invocation"""
|
"""Test asynchronous invocation"""
|
||||||
tool = CrewStructuredTool.from_function(func=basic_function, name="test_tool")
|
tool = CrewStructuredTool.from_function(func=basic_function, name="test_tool")
|
||||||
|
|
||||||
result = await tool.ainvoke(input={"param1": "test"})
|
result = await tool.ainvoke(input={"param1": "test"})
|
||||||
assert result == "test 0"
|
assert result == "test 0"
|
||||||
|
|
||||||
def test_parse_args_dict(self, basic_function):
|
def test_parse_args_dict(basic_function):
|
||||||
"""Test parsing dictionary arguments"""
|
"""Test parsing dictionary arguments"""
|
||||||
tool = CrewStructuredTool.from_function(func=basic_function, name="test_tool")
|
tool = CrewStructuredTool.from_function(func=basic_function, name="test_tool")
|
||||||
|
|
||||||
parsed = tool._parse_args({"param1": "test", "param2": 42})
|
parsed = tool._parse_args({"param1": "test", "param2": 42})
|
||||||
assert parsed["param1"] == "test"
|
assert parsed["param1"] == "test"
|
||||||
assert parsed["param2"] == 42
|
assert parsed["param2"] == 42
|
||||||
|
|
||||||
def test_parse_args_string(self, basic_function):
|
def test_parse_args_string(basic_function):
|
||||||
"""Test parsing string arguments"""
|
"""Test parsing string arguments"""
|
||||||
tool = CrewStructuredTool.from_function(func=basic_function, name="test_tool")
|
tool = CrewStructuredTool.from_function(func=basic_function, name="test_tool")
|
||||||
|
|
||||||
parsed = tool._parse_args('{"param1": "test", "param2": 42}')
|
parsed = tool._parse_args('{"param1": "test", "param2": 42}')
|
||||||
assert parsed["param1"] == "test"
|
assert parsed["param1"] == "test"
|
||||||
assert parsed["param2"] == 42
|
assert parsed["param2"] == 42
|
||||||
|
|
||||||
def test_complex_types(self):
|
def test_complex_types():
|
||||||
"""Test handling of complex parameter types"""
|
"""Test handling of complex parameter types"""
|
||||||
|
|
||||||
def complex_func(nested: dict, items: list) -> str:
|
def complex_func(nested: dict, items: list) -> str:
|
||||||
"""Process complex types."""
|
"""Process complex types."""
|
||||||
return f"Processed {len(items)} items with {len(nested)} nested keys"
|
return f"Processed {len(items)} items with {len(nested)} nested keys"
|
||||||
|
|
||||||
tool = CrewStructuredTool.from_function(
|
tool = CrewStructuredTool.from_function(
|
||||||
func=complex_func, name="test_tool", description="Test complex types"
|
func=complex_func, name="test_tool", description="Test complex types"
|
||||||
)
|
)
|
||||||
result = tool.invoke({"nested": {"key": "value"}, "items": [1, 2, 3]})
|
result = tool.invoke({"nested": {"key": "value"}, "items": [1, 2, 3]})
|
||||||
assert result == "Processed 3 items with 1 nested keys"
|
assert result == "Processed 3 items with 1 nested keys"
|
||||||
|
|
||||||
def test_schema_inheritance(self):
|
def test_schema_inheritance():
|
||||||
"""Test tool creation with inherited schema"""
|
"""Test tool creation with inherited schema"""
|
||||||
|
|
||||||
def extended_func(base_param: str, extra_param: int) -> str:
|
def extended_func(base_param: str, extra_param: int) -> str:
|
||||||
"""Test function with inherited schema."""
|
"""Test function with inherited schema."""
|
||||||
return f"{base_param} {extra_param}"
|
return f"{base_param} {extra_param}"
|
||||||
|
|
||||||
class BaseSchema(BaseModel):
|
class BaseSchema(BaseModel):
|
||||||
base_param: str
|
base_param: str
|
||||||
|
|
||||||
class ExtendedSchema(BaseSchema):
|
class ExtendedSchema(BaseSchema):
|
||||||
extra_param: int
|
extra_param: int
|
||||||
|
|
||||||
tool = CrewStructuredTool.from_function(
|
tool = CrewStructuredTool.from_function(
|
||||||
func=extended_func, name="test_tool", args_schema=ExtendedSchema
|
func=extended_func, name="test_tool", args_schema=ExtendedSchema
|
||||||
)
|
)
|
||||||
|
|
||||||
result = tool.invoke({"base_param": "test", "extra_param": 42})
|
result = tool.invoke({"base_param": "test", "extra_param": 42})
|
||||||
assert result == "test 42"
|
assert result == "test 42"
|
||||||
|
|
||||||
def test_default_values_in_schema(self):
|
def test_default_values_in_schema():
|
||||||
"""Test handling of default values in schema"""
|
"""Test handling of default values in schema"""
|
||||||
|
|
||||||
def default_func(
|
def default_func(
|
||||||
required_param: str,
|
required_param: str,
|
||||||
optional_param: str = "default",
|
optional_param: str = "default",
|
||||||
nullable_param: Optional[int] = None,
|
nullable_param: Optional[int] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Test function with default values."""
|
"""Test function with default values."""
|
||||||
return f"{required_param} {optional_param} {nullable_param}"
|
return f"{required_param} {optional_param} {nullable_param}"
|
||||||
|
|
||||||
tool = CrewStructuredTool.from_function(
|
tool = CrewStructuredTool.from_function(
|
||||||
func=default_func, name="test_tool", description="Test defaults"
|
func=default_func, name="test_tool", description="Test defaults"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test with minimal parameters
|
# Test with minimal parameters
|
||||||
result = tool.invoke({"required_param": "test"})
|
result = tool.invoke({"required_param": "test"})
|
||||||
assert result == "test default None"
|
assert result == "test default None"
|
||||||
|
|
||||||
# Test with all parameters
|
# Test with all parameters
|
||||||
result = tool.invoke(
|
result = tool.invoke(
|
||||||
{"required_param": "test", "optional_param": "custom", "nullable_param": 42}
|
{"required_param": "test", "optional_param": "custom", "nullable_param": 42}
|
||||||
)
|
)
|
||||||
assert result == "test custom 42"
|
assert result == "test custom 42"
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def custom_tool_decorator():
|
||||||
|
from crewai.tools import tool
|
||||||
|
|
||||||
|
@tool("custom_tool", result_as_answer=True)
|
||||||
|
async def custom_tool():
|
||||||
|
"""This is a tool that does something"""
|
||||||
|
return "Hello World from Custom Tool"
|
||||||
|
|
||||||
|
return custom_tool
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def custom_tool():
|
||||||
|
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):
|
||||||
|
return "Hello World from Custom Tool"
|
||||||
|
|
||||||
|
return CustomTool()
|
||||||
|
|
||||||
|
def build_simple_crew(tool):
|
||||||
|
from crewai import Agent, Task, Crew
|
||||||
|
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
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):
|
||||||
|
from crewai.flow.flow import Flow
|
||||||
|
|
||||||
|
class StructuredExampleFlow(Flow):
|
||||||
|
from crewai.flow.flow import start
|
||||||
|
|
||||||
|
@start()
|
||||||
|
async def start(self):
|
||||||
|
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):
|
||||||
|
from crewai.flow.flow import Flow
|
||||||
|
|
||||||
|
class StructuredExampleFlow(Flow):
|
||||||
|
from crewai.flow.flow import start
|
||||||
|
@start()
|
||||||
|
async def start(self):
|
||||||
|
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"
|
||||||
Reference in New Issue
Block a user