mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-10 08:38:30 +00:00
feat: async crew support
native async crew execution. Improves tool decorator typing, ensures _run backward compatibility, updates docs and docstrings, adds tests, and removes duplicated logic.
This commit is contained in:
384
lib/crewai/tests/crew/test_async_crew.py
Normal file
384
lib/crewai/tests/crew/test_async_crew.py
Normal file
@@ -0,0 +1,384 @@
|
||||
"""Tests for async crew execution."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from crewai.agent import Agent
|
||||
from crewai.crew import Crew
|
||||
from crewai.task import Task
|
||||
from crewai.crews.crew_output import CrewOutput
|
||||
from crewai.tasks.task_output import TaskOutput
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_agent() -> Agent:
|
||||
"""Create a test agent."""
|
||||
return Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
llm="gpt-4o-mini",
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_task(test_agent: Agent) -> Task:
|
||||
"""Create a test task."""
|
||||
return Task(
|
||||
description="Test task description",
|
||||
expected_output="Test expected output",
|
||||
agent=test_agent,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_crew(test_agent: Agent, test_task: Task) -> Crew:
|
||||
"""Create a test crew."""
|
||||
return Crew(
|
||||
agents=[test_agent],
|
||||
tasks=[test_task],
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
|
||||
class TestAsyncCrewKickoff:
|
||||
"""Tests for async crew kickoff methods."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("crewai.task.Task.aexecute_sync", new_callable=AsyncMock)
|
||||
async def test_akickoff_basic(
|
||||
self, mock_execute: AsyncMock, test_crew: Crew
|
||||
) -> None:
|
||||
"""Test basic async crew kickoff."""
|
||||
mock_output = TaskOutput(
|
||||
description="Test task description",
|
||||
raw="Task result",
|
||||
agent="Test Agent",
|
||||
)
|
||||
mock_execute.return_value = mock_output
|
||||
|
||||
result = await test_crew.akickoff()
|
||||
|
||||
assert result is not None
|
||||
assert isinstance(result, CrewOutput)
|
||||
assert result.raw == "Task result"
|
||||
mock_execute.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("crewai.task.Task.aexecute_sync", new_callable=AsyncMock)
|
||||
async def test_akickoff_with_inputs(
|
||||
self, mock_execute: AsyncMock, test_agent: Agent
|
||||
) -> None:
|
||||
"""Test async crew kickoff with inputs."""
|
||||
task = Task(
|
||||
description="Test task for {topic}",
|
||||
expected_output="Expected output for {topic}",
|
||||
agent=test_agent,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[test_agent],
|
||||
tasks=[task],
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
mock_output = TaskOutput(
|
||||
description="Test task for AI",
|
||||
raw="Task result about AI",
|
||||
agent="Test Agent",
|
||||
)
|
||||
mock_execute.return_value = mock_output
|
||||
|
||||
result = await crew.akickoff(inputs={"topic": "AI"})
|
||||
|
||||
assert result is not None
|
||||
assert isinstance(result, CrewOutput)
|
||||
mock_execute.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("crewai.task.Task.aexecute_sync", new_callable=AsyncMock)
|
||||
async def test_akickoff_multiple_tasks(
|
||||
self, mock_execute: AsyncMock, test_agent: Agent
|
||||
) -> None:
|
||||
"""Test async crew kickoff with multiple tasks."""
|
||||
task1 = Task(
|
||||
description="First task",
|
||||
expected_output="First output",
|
||||
agent=test_agent,
|
||||
)
|
||||
task2 = Task(
|
||||
description="Second task",
|
||||
expected_output="Second output",
|
||||
agent=test_agent,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[test_agent],
|
||||
tasks=[task1, task2],
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
mock_output1 = TaskOutput(
|
||||
description="First task",
|
||||
raw="First result",
|
||||
agent="Test Agent",
|
||||
)
|
||||
mock_output2 = TaskOutput(
|
||||
description="Second task",
|
||||
raw="Second result",
|
||||
agent="Test Agent",
|
||||
)
|
||||
mock_execute.side_effect = [mock_output1, mock_output2]
|
||||
|
||||
result = await crew.akickoff()
|
||||
|
||||
assert result is not None
|
||||
assert isinstance(result, CrewOutput)
|
||||
assert result.raw == "Second result"
|
||||
assert mock_execute.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("crewai.task.Task.aexecute_sync", new_callable=AsyncMock)
|
||||
async def test_akickoff_handles_exception(
|
||||
self, mock_execute: AsyncMock, test_crew: Crew
|
||||
) -> None:
|
||||
"""Test that async kickoff handles exceptions properly."""
|
||||
mock_execute.side_effect = RuntimeError("Test error")
|
||||
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
await test_crew.akickoff()
|
||||
|
||||
assert "Test error" in str(exc_info.value)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("crewai.task.Task.aexecute_sync", new_callable=AsyncMock)
|
||||
async def test_akickoff_calls_before_callbacks(
|
||||
self, mock_execute: AsyncMock, test_agent: Agent
|
||||
) -> None:
|
||||
"""Test that async kickoff calls before_kickoff_callbacks."""
|
||||
callback_called = False
|
||||
|
||||
def before_callback(inputs: dict | None) -> dict:
|
||||
nonlocal callback_called
|
||||
callback_called = True
|
||||
return inputs or {}
|
||||
|
||||
task = Task(
|
||||
description="Test task",
|
||||
expected_output="Test output",
|
||||
agent=test_agent,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[test_agent],
|
||||
tasks=[task],
|
||||
verbose=False,
|
||||
before_kickoff_callbacks=[before_callback],
|
||||
)
|
||||
|
||||
mock_output = TaskOutput(
|
||||
description="Test task",
|
||||
raw="Task result",
|
||||
agent="Test Agent",
|
||||
)
|
||||
mock_execute.return_value = mock_output
|
||||
|
||||
await crew.akickoff()
|
||||
|
||||
assert callback_called
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("crewai.task.Task.aexecute_sync", new_callable=AsyncMock)
|
||||
async def test_akickoff_calls_after_callbacks(
|
||||
self, mock_execute: AsyncMock, test_agent: Agent
|
||||
) -> None:
|
||||
"""Test that async kickoff calls after_kickoff_callbacks."""
|
||||
callback_called = False
|
||||
|
||||
def after_callback(result: CrewOutput) -> CrewOutput:
|
||||
nonlocal callback_called
|
||||
callback_called = True
|
||||
return result
|
||||
|
||||
task = Task(
|
||||
description="Test task",
|
||||
expected_output="Test output",
|
||||
agent=test_agent,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[test_agent],
|
||||
tasks=[task],
|
||||
verbose=False,
|
||||
after_kickoff_callbacks=[after_callback],
|
||||
)
|
||||
|
||||
mock_output = TaskOutput(
|
||||
description="Test task",
|
||||
raw="Task result",
|
||||
agent="Test Agent",
|
||||
)
|
||||
mock_execute.return_value = mock_output
|
||||
|
||||
await crew.akickoff()
|
||||
|
||||
assert callback_called
|
||||
|
||||
|
||||
class TestAsyncCrewKickoffForEach:
|
||||
"""Tests for async crew kickoff_for_each methods."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("crewai.task.Task.aexecute_sync", new_callable=AsyncMock)
|
||||
async def test_akickoff_for_each_basic(
|
||||
self, mock_execute: AsyncMock, test_agent: Agent
|
||||
) -> None:
|
||||
"""Test basic async kickoff_for_each."""
|
||||
task = Task(
|
||||
description="Test task for {topic}",
|
||||
expected_output="Expected output",
|
||||
agent=test_agent,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[test_agent],
|
||||
tasks=[task],
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
mock_output1 = TaskOutput(
|
||||
description="Test task for AI",
|
||||
raw="Result about AI",
|
||||
agent="Test Agent",
|
||||
)
|
||||
mock_output2 = TaskOutput(
|
||||
description="Test task for ML",
|
||||
raw="Result about ML",
|
||||
agent="Test Agent",
|
||||
)
|
||||
mock_execute.side_effect = [mock_output1, mock_output2]
|
||||
|
||||
inputs = [{"topic": "AI"}, {"topic": "ML"}]
|
||||
results = await crew.akickoff_for_each(inputs)
|
||||
|
||||
assert len(results) == 2
|
||||
assert all(isinstance(r, CrewOutput) for r in results)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("crewai.task.Task.aexecute_sync", new_callable=AsyncMock)
|
||||
async def test_akickoff_for_each_concurrent(
|
||||
self, mock_execute: AsyncMock, test_agent: Agent
|
||||
) -> None:
|
||||
"""Test that async kickoff_for_each runs concurrently."""
|
||||
task = Task(
|
||||
description="Test task for {topic}",
|
||||
expected_output="Expected output",
|
||||
agent=test_agent,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[test_agent],
|
||||
tasks=[task],
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
mock_output = TaskOutput(
|
||||
description="Test task",
|
||||
raw="Result",
|
||||
agent="Test Agent",
|
||||
)
|
||||
mock_execute.return_value = mock_output
|
||||
|
||||
inputs = [{"topic": f"topic_{i}"} for i in range(3)]
|
||||
results = await crew.akickoff_for_each(inputs)
|
||||
|
||||
assert len(results) == 3
|
||||
|
||||
|
||||
class TestAsyncTaskExecution:
|
||||
"""Tests for async task execution within crew."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("crewai.task.Task.aexecute_sync", new_callable=AsyncMock)
|
||||
async def test_aexecute_tasks_sequential(
|
||||
self, mock_execute: AsyncMock, test_agent: Agent
|
||||
) -> None:
|
||||
"""Test async sequential task execution."""
|
||||
task1 = Task(
|
||||
description="First task",
|
||||
expected_output="First output",
|
||||
agent=test_agent,
|
||||
)
|
||||
task2 = Task(
|
||||
description="Second task",
|
||||
expected_output="Second output",
|
||||
agent=test_agent,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[test_agent],
|
||||
tasks=[task1, task2],
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
mock_output1 = TaskOutput(
|
||||
description="First task",
|
||||
raw="First result",
|
||||
agent="Test Agent",
|
||||
)
|
||||
mock_output2 = TaskOutput(
|
||||
description="Second task",
|
||||
raw="Second result",
|
||||
agent="Test Agent",
|
||||
)
|
||||
mock_execute.side_effect = [mock_output1, mock_output2]
|
||||
|
||||
result = await crew._aexecute_tasks(crew.tasks)
|
||||
|
||||
assert result is not None
|
||||
assert result.raw == "Second result"
|
||||
assert len(result.tasks_output) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("crewai.task.Task.aexecute_sync", new_callable=AsyncMock)
|
||||
async def test_aexecute_tasks_with_async_task(
|
||||
self, mock_execute: AsyncMock, test_agent: Agent
|
||||
) -> None:
|
||||
"""Test async execution with async_execution task flag."""
|
||||
task1 = Task(
|
||||
description="Async task",
|
||||
expected_output="Async output",
|
||||
agent=test_agent,
|
||||
async_execution=True,
|
||||
)
|
||||
task2 = Task(
|
||||
description="Sync task",
|
||||
expected_output="Sync output",
|
||||
agent=test_agent,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[test_agent],
|
||||
tasks=[task1, task2],
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
mock_output1 = TaskOutput(
|
||||
description="Async task",
|
||||
raw="Async result",
|
||||
agent="Test Agent",
|
||||
)
|
||||
mock_output2 = TaskOutput(
|
||||
description="Sync task",
|
||||
raw="Sync result",
|
||||
agent="Test Agent",
|
||||
)
|
||||
mock_execute.side_effect = [mock_output1, mock_output2]
|
||||
|
||||
result = await crew._aexecute_tasks(crew.tasks)
|
||||
|
||||
assert result is not None
|
||||
assert mock_execute.call_count == 2
|
||||
|
||||
|
||||
class TestAsyncProcessAsyncTasks:
|
||||
"""Tests for _aprocess_async_tasks method."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_aprocess_async_tasks_empty(self, test_crew: Crew) -> None:
|
||||
"""Test processing empty list of async tasks."""
|
||||
result = await test_crew._aprocess_async_tasks([])
|
||||
assert result == []
|
||||
Reference in New Issue
Block a user