mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-09 16:18:30 +00:00
native async crew execution. Improves tool decorator typing, ensures _run backward compatibility, updates docs and docstrings, adds tests, and removes duplicated logic.
384 lines
11 KiB
Python
384 lines
11 KiB
Python
"""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 == [] |