mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-16 04:18:35 +00:00
fix: add async method validation to crew decorators
This commit adds validation to detect and reject async methods in crew decorators (@agent, @task, @crew, @llm, @tool, @callback, @cache_handler, @before_kickoff, @after_kickoff). Previously, decorating async methods would silently fail at runtime with confusing errors like "'coroutine' object has no attribute 'name'". Now, a clear TypeError is raised at decoration time with: - The specific decorator name that doesn't support async - The method name that was incorrectly defined as async - Helpful suggestions for workarounds Fixes #3988 Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
@@ -2,16 +2,12 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, overload
|
from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, overload
|
||||||
|
|
||||||
from crewai.project.utils import memoize
|
from crewai.project.utils import memoize
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from crewai import Agent, Crew, Task
|
|
||||||
|
|
||||||
from crewai.project.wrappers import (
|
from crewai.project.wrappers import (
|
||||||
AfterKickoffMethod,
|
AfterKickoffMethod,
|
||||||
AgentMethod,
|
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")
|
P = ParamSpec("P")
|
||||||
P2 = ParamSpec("P2")
|
P2 = ParamSpec("P2")
|
||||||
R = TypeVar("R")
|
R = TypeVar("R")
|
||||||
@@ -44,7 +65,11 @@ def before_kickoff(meth: Callable[P, R]) -> BeforeKickoffMethod[P, R]:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A wrapped method marked for before kickoff execution.
|
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)
|
return BeforeKickoffMethod(meth)
|
||||||
|
|
||||||
|
|
||||||
@@ -56,7 +81,11 @@ def after_kickoff(meth: Callable[P, R]) -> AfterKickoffMethod[P, R]:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A wrapped method marked for after kickoff execution.
|
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)
|
return AfterKickoffMethod(meth)
|
||||||
|
|
||||||
|
|
||||||
@@ -68,7 +97,11 @@ def task(meth: Callable[P, TaskResultT]) -> TaskMethod[P, TaskResultT]:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A wrapped method marked as a task with memoization.
|
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))
|
return TaskMethod(memoize(meth))
|
||||||
|
|
||||||
|
|
||||||
@@ -80,7 +113,11 @@ def agent(meth: Callable[P, R]) -> AgentMethod[P, R]:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A wrapped method marked as an agent with memoization.
|
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))
|
return AgentMethod(memoize(meth))
|
||||||
|
|
||||||
|
|
||||||
@@ -92,7 +129,11 @@ def llm(meth: Callable[P, R]) -> LLMMethod[P, R]:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A wrapped method marked as an LLM provider with memoization.
|
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))
|
return LLMMethod(memoize(meth))
|
||||||
|
|
||||||
|
|
||||||
@@ -128,7 +169,11 @@ def tool(meth: Callable[P, R]) -> ToolMethod[P, R]:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A wrapped method marked as a tool with memoization.
|
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))
|
return ToolMethod(memoize(meth))
|
||||||
|
|
||||||
|
|
||||||
@@ -140,7 +185,11 @@ def callback(meth: Callable[P, R]) -> CallbackMethod[P, R]:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A wrapped method marked as a callback with memoization.
|
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))
|
return CallbackMethod(memoize(meth))
|
||||||
|
|
||||||
|
|
||||||
@@ -152,7 +201,11 @@ def cache_handler(meth: Callable[P, R]) -> CacheHandlerMethod[P, R]:
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A wrapped method marked as a cache handler with memoization.
|
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))
|
return CacheHandlerMethod(memoize(meth))
|
||||||
|
|
||||||
|
|
||||||
@@ -174,7 +227,11 @@ def crew(
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A wrapped method that instantiates tasks and agents before execution.
|
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)
|
@wraps(meth)
|
||||||
def wrapper(self: CrewInstance, *args: Any, **kwargs: Any) -> Crew:
|
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