mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-16 04:18:35 +00:00
Compare commits
1 Commits
1.6.1
...
devin/1764
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28147528f9 |
@@ -2,16 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, overload
|
||||
|
||||
from crewai.project.utils import memoize
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai import Agent, Crew, Task
|
||||
|
||||
from crewai.project.wrappers import (
|
||||
AfterKickoffMethod,
|
||||
AgentMethod,
|
||||
@@ -28,6 +24,31 @@ from crewai.project.wrappers import (
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from crewai import Agent, Crew, Task
|
||||
|
||||
|
||||
def _check_async_method(meth: Callable[..., Any], decorator_name: str) -> None:
|
||||
"""Check if a method is async and raise an error if so.
|
||||
|
||||
Args:
|
||||
meth: The method to check.
|
||||
decorator_name: The name of the decorator for the error message.
|
||||
|
||||
Raises:
|
||||
TypeError: If the method is an async function.
|
||||
"""
|
||||
if asyncio.iscoroutinefunction(meth):
|
||||
raise TypeError(
|
||||
f"The @{decorator_name} decorator does not support async methods. "
|
||||
f"Method '{meth.__name__}' is defined as async. "
|
||||
f"Please use a synchronous method instead. "
|
||||
f"If you need to perform async operations, consider: "
|
||||
f"1) Creating tools/resources synchronously before crew execution, or "
|
||||
f"2) Using asyncio.run() within a sync method for isolated async calls."
|
||||
)
|
||||
|
||||
|
||||
P = ParamSpec("P")
|
||||
P2 = ParamSpec("P2")
|
||||
R = TypeVar("R")
|
||||
@@ -44,7 +65,11 @@ def before_kickoff(meth: Callable[P, R]) -> BeforeKickoffMethod[P, R]:
|
||||
|
||||
Returns:
|
||||
A wrapped method marked for before kickoff execution.
|
||||
|
||||
Raises:
|
||||
TypeError: If the method is an async function.
|
||||
"""
|
||||
_check_async_method(meth, "before_kickoff")
|
||||
return BeforeKickoffMethod(meth)
|
||||
|
||||
|
||||
@@ -56,7 +81,11 @@ def after_kickoff(meth: Callable[P, R]) -> AfterKickoffMethod[P, R]:
|
||||
|
||||
Returns:
|
||||
A wrapped method marked for after kickoff execution.
|
||||
|
||||
Raises:
|
||||
TypeError: If the method is an async function.
|
||||
"""
|
||||
_check_async_method(meth, "after_kickoff")
|
||||
return AfterKickoffMethod(meth)
|
||||
|
||||
|
||||
@@ -68,7 +97,11 @@ def task(meth: Callable[P, TaskResultT]) -> TaskMethod[P, TaskResultT]:
|
||||
|
||||
Returns:
|
||||
A wrapped method marked as a task with memoization.
|
||||
|
||||
Raises:
|
||||
TypeError: If the method is an async function.
|
||||
"""
|
||||
_check_async_method(meth, "task")
|
||||
return TaskMethod(memoize(meth))
|
||||
|
||||
|
||||
@@ -80,7 +113,11 @@ def agent(meth: Callable[P, R]) -> AgentMethod[P, R]:
|
||||
|
||||
Returns:
|
||||
A wrapped method marked as an agent with memoization.
|
||||
|
||||
Raises:
|
||||
TypeError: If the method is an async function.
|
||||
"""
|
||||
_check_async_method(meth, "agent")
|
||||
return AgentMethod(memoize(meth))
|
||||
|
||||
|
||||
@@ -92,7 +129,11 @@ def llm(meth: Callable[P, R]) -> LLMMethod[P, R]:
|
||||
|
||||
Returns:
|
||||
A wrapped method marked as an LLM provider with memoization.
|
||||
|
||||
Raises:
|
||||
TypeError: If the method is an async function.
|
||||
"""
|
||||
_check_async_method(meth, "llm")
|
||||
return LLMMethod(memoize(meth))
|
||||
|
||||
|
||||
@@ -128,7 +169,11 @@ def tool(meth: Callable[P, R]) -> ToolMethod[P, R]:
|
||||
|
||||
Returns:
|
||||
A wrapped method marked as a tool with memoization.
|
||||
|
||||
Raises:
|
||||
TypeError: If the method is an async function.
|
||||
"""
|
||||
_check_async_method(meth, "tool")
|
||||
return ToolMethod(memoize(meth))
|
||||
|
||||
|
||||
@@ -140,7 +185,11 @@ def callback(meth: Callable[P, R]) -> CallbackMethod[P, R]:
|
||||
|
||||
Returns:
|
||||
A wrapped method marked as a callback with memoization.
|
||||
|
||||
Raises:
|
||||
TypeError: If the method is an async function.
|
||||
"""
|
||||
_check_async_method(meth, "callback")
|
||||
return CallbackMethod(memoize(meth))
|
||||
|
||||
|
||||
@@ -152,7 +201,11 @@ def cache_handler(meth: Callable[P, R]) -> CacheHandlerMethod[P, R]:
|
||||
|
||||
Returns:
|
||||
A wrapped method marked as a cache handler with memoization.
|
||||
|
||||
Raises:
|
||||
TypeError: If the method is an async function.
|
||||
"""
|
||||
_check_async_method(meth, "cache_handler")
|
||||
return CacheHandlerMethod(memoize(meth))
|
||||
|
||||
|
||||
@@ -174,7 +227,11 @@ def crew(
|
||||
|
||||
Returns:
|
||||
A wrapped method that instantiates tasks and agents before execution.
|
||||
|
||||
Raises:
|
||||
TypeError: If the method is an async function.
|
||||
"""
|
||||
_check_async_method(meth, "crew")
|
||||
|
||||
@wraps(meth)
|
||||
def wrapper(self: CrewInstance, *args: Any, **kwargs: Any) -> Crew:
|
||||
|
||||
169
lib/crewai/tests/project/test_async_decorator_validation.py
Normal file
169
lib/crewai/tests/project/test_async_decorator_validation.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""Tests for async method validation in crew decorators."""
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.project import (
|
||||
after_kickoff,
|
||||
agent,
|
||||
before_kickoff,
|
||||
cache_handler,
|
||||
callback,
|
||||
crew,
|
||||
llm,
|
||||
task,
|
||||
tool,
|
||||
)
|
||||
|
||||
|
||||
class TestAsyncDecoratorValidation:
|
||||
"""Test that decorators properly reject async methods with clear error messages."""
|
||||
|
||||
def test_agent_decorator_rejects_async_method(self):
|
||||
"""Test that @agent decorator raises TypeError for async methods."""
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
|
||||
@agent
|
||||
async def async_agent(self):
|
||||
return None
|
||||
|
||||
assert "@agent decorator does not support async methods" in str(exc_info.value)
|
||||
assert "async_agent" in str(exc_info.value)
|
||||
assert "synchronous method" in str(exc_info.value)
|
||||
|
||||
def test_task_decorator_rejects_async_method(self):
|
||||
"""Test that @task decorator raises TypeError for async methods."""
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
|
||||
@task
|
||||
async def async_task(self):
|
||||
return None
|
||||
|
||||
assert "@task decorator does not support async methods" in str(exc_info.value)
|
||||
assert "async_task" in str(exc_info.value)
|
||||
assert "synchronous method" in str(exc_info.value)
|
||||
|
||||
def test_crew_decorator_rejects_async_method(self):
|
||||
"""Test that @crew decorator raises TypeError for async methods."""
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
|
||||
@crew
|
||||
async def async_crew(self):
|
||||
return None
|
||||
|
||||
assert "@crew decorator does not support async methods" in str(exc_info.value)
|
||||
assert "async_crew" in str(exc_info.value)
|
||||
assert "synchronous method" in str(exc_info.value)
|
||||
|
||||
def test_llm_decorator_rejects_async_method(self):
|
||||
"""Test that @llm decorator raises TypeError for async methods."""
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
|
||||
@llm
|
||||
async def async_llm(self):
|
||||
return None
|
||||
|
||||
assert "@llm decorator does not support async methods" in str(exc_info.value)
|
||||
assert "async_llm" in str(exc_info.value)
|
||||
assert "synchronous method" in str(exc_info.value)
|
||||
|
||||
def test_tool_decorator_rejects_async_method(self):
|
||||
"""Test that @tool decorator raises TypeError for async methods."""
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
|
||||
@tool
|
||||
async def async_tool(self):
|
||||
return None
|
||||
|
||||
assert "@tool decorator does not support async methods" in str(exc_info.value)
|
||||
assert "async_tool" in str(exc_info.value)
|
||||
assert "synchronous method" in str(exc_info.value)
|
||||
|
||||
def test_callback_decorator_rejects_async_method(self):
|
||||
"""Test that @callback decorator raises TypeError for async methods."""
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
|
||||
@callback
|
||||
async def async_callback(self):
|
||||
return None
|
||||
|
||||
assert "@callback decorator does not support async methods" in str(exc_info.value)
|
||||
assert "async_callback" in str(exc_info.value)
|
||||
assert "synchronous method" in str(exc_info.value)
|
||||
|
||||
def test_cache_handler_decorator_rejects_async_method(self):
|
||||
"""Test that @cache_handler decorator raises TypeError for async methods."""
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
|
||||
@cache_handler
|
||||
async def async_cache_handler(self):
|
||||
return None
|
||||
|
||||
assert "@cache_handler decorator does not support async methods" in str(
|
||||
exc_info.value
|
||||
)
|
||||
assert "async_cache_handler" in str(exc_info.value)
|
||||
assert "synchronous method" in str(exc_info.value)
|
||||
|
||||
def test_before_kickoff_decorator_rejects_async_method(self):
|
||||
"""Test that @before_kickoff decorator raises TypeError for async methods."""
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
|
||||
@before_kickoff
|
||||
async def async_before_kickoff(self, inputs):
|
||||
return inputs
|
||||
|
||||
assert "@before_kickoff decorator does not support async methods" in str(
|
||||
exc_info.value
|
||||
)
|
||||
assert "async_before_kickoff" in str(exc_info.value)
|
||||
assert "synchronous method" in str(exc_info.value)
|
||||
|
||||
def test_after_kickoff_decorator_rejects_async_method(self):
|
||||
"""Test that @after_kickoff decorator raises TypeError for async methods."""
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
|
||||
@after_kickoff
|
||||
async def async_after_kickoff(self, outputs):
|
||||
return outputs
|
||||
|
||||
assert "@after_kickoff decorator does not support async methods" in str(
|
||||
exc_info.value
|
||||
)
|
||||
assert "async_after_kickoff" in str(exc_info.value)
|
||||
assert "synchronous method" in str(exc_info.value)
|
||||
|
||||
def test_sync_methods_still_work(self):
|
||||
"""Test that synchronous methods are still properly decorated."""
|
||||
from crewai import Agent, Task
|
||||
|
||||
@agent
|
||||
def sync_agent(self):
|
||||
return Agent(
|
||||
role="Test Agent", goal="Test Goal", backstory="Test Backstory"
|
||||
)
|
||||
|
||||
@task
|
||||
def sync_task(self):
|
||||
return Task(description="Test Description", expected_output="Test Output")
|
||||
|
||||
class TestCrew:
|
||||
pass
|
||||
|
||||
test_instance = TestCrew()
|
||||
agent_result = sync_agent(test_instance)
|
||||
task_result = sync_task(test_instance)
|
||||
|
||||
assert agent_result.role == "Test Agent"
|
||||
assert task_result.description == "Test Description"
|
||||
|
||||
def test_error_message_includes_workaround_suggestions(self):
|
||||
"""Test that error messages include helpful workaround suggestions."""
|
||||
with pytest.raises(TypeError) as exc_info:
|
||||
|
||||
@agent
|
||||
async def async_agent_with_tools(self):
|
||||
return None
|
||||
|
||||
error_message = str(exc_info.value)
|
||||
assert "Creating tools/resources synchronously" in error_message
|
||||
assert "asyncio.run()" in error_message
|
||||
Reference in New Issue
Block a user