mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-15 19:18:30 +00:00
feat: Add A2A (Agent-to-Agent) protocol support for remote interoperability
- Implement CrewAgentExecutor class that wraps CrewAI crews as A2A-compatible agents - Add server utilities for starting A2A servers with crews - Include comprehensive test coverage for all A2A functionality - Add optional dependency group 'a2a' in pyproject.toml - Expose A2A classes in main CrewAI module with graceful import handling - Add documentation and examples for A2A integration - Support bidirectional agent communication via A2A protocol - Enable crews to participate in remote agent networks Fixes #2970 Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
197
tests/a2a/test_crew_agent_executor.py
Normal file
197
tests/a2a/test_crew_agent_executor.py
Normal file
@@ -0,0 +1,197 @@
|
||||
"""Tests for CrewAgentExecutor class."""
|
||||
|
||||
import asyncio
|
||||
import pytest
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
|
||||
from crewai import Agent, Crew, Task
|
||||
from crewai.crews.crew_output import CrewOutput
|
||||
|
||||
try:
|
||||
from crewai.a2a import CrewAgentExecutor
|
||||
from a2a.server.agent_execution import RequestContext
|
||||
from a2a.server.events import EventQueue
|
||||
from a2a.types import InvalidParamsError, UnsupportedOperationError
|
||||
from a2a.utils.errors import ServerError
|
||||
A2A_AVAILABLE = True
|
||||
except ImportError:
|
||||
A2A_AVAILABLE = False
|
||||
|
||||
|
||||
@pytest.mark.skipif(not A2A_AVAILABLE, reason="A2A integration not available")
|
||||
class TestCrewAgentExecutor:
|
||||
"""Test cases for CrewAgentExecutor."""
|
||||
|
||||
@pytest.fixture
|
||||
def sample_crew(self):
|
||||
"""Create a sample crew for testing."""
|
||||
from unittest.mock import Mock
|
||||
mock_crew = Mock()
|
||||
mock_crew.agents = []
|
||||
mock_crew.tasks = []
|
||||
return mock_crew
|
||||
|
||||
@pytest.fixture
|
||||
def crew_executor(self, sample_crew):
|
||||
"""Create a CrewAgentExecutor for testing."""
|
||||
return CrewAgentExecutor(sample_crew)
|
||||
|
||||
@pytest.fixture
|
||||
def mock_context(self):
|
||||
"""Create a mock RequestContext."""
|
||||
from a2a.types import Message, Part, TextPart
|
||||
context = Mock(spec=RequestContext)
|
||||
context.task_id = "test-task-123"
|
||||
context.context_id = "test-context-456"
|
||||
context.message = Message(
|
||||
messageId="msg-123",
|
||||
taskId="test-task-123",
|
||||
contextId="test-context-456",
|
||||
role="user",
|
||||
parts=[Part(root=TextPart(text="Test message"))]
|
||||
)
|
||||
context.get_user_input.return_value = "Test query"
|
||||
return context
|
||||
|
||||
@pytest.fixture
|
||||
def mock_event_queue(self):
|
||||
"""Create a mock EventQueue."""
|
||||
return Mock(spec=EventQueue)
|
||||
|
||||
def test_init(self, sample_crew):
|
||||
"""Test CrewAgentExecutor initialization."""
|
||||
executor = CrewAgentExecutor(sample_crew)
|
||||
|
||||
assert executor.crew == sample_crew
|
||||
assert executor.supported_content_types == ['text', 'text/plain']
|
||||
assert executor._running_tasks == {}
|
||||
|
||||
def test_init_with_custom_content_types(self, sample_crew):
|
||||
"""Test CrewAgentExecutor initialization with custom content types."""
|
||||
custom_types = ['text', 'application/json']
|
||||
executor = CrewAgentExecutor(sample_crew, supported_content_types=custom_types)
|
||||
|
||||
assert executor.supported_content_types == custom_types
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_success(self, crew_executor, mock_context, mock_event_queue):
|
||||
"""Test successful crew execution."""
|
||||
mock_output = CrewOutput(raw="Test response", json_dict=None)
|
||||
|
||||
with patch.object(crew_executor, '_execute_crew_async', return_value=mock_output):
|
||||
await crew_executor.execute(mock_context, mock_event_queue)
|
||||
|
||||
mock_event_queue.enqueue_event.assert_called_once()
|
||||
|
||||
assert len(crew_executor._running_tasks) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_validation_error(self, crew_executor, mock_event_queue):
|
||||
"""Test execution with validation error."""
|
||||
bad_context = Mock(spec=RequestContext)
|
||||
bad_context.get_user_input.return_value = ""
|
||||
|
||||
with pytest.raises(ServerError):
|
||||
await crew_executor.execute(bad_context, mock_event_queue)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_crew_error(self, crew_executor, mock_context, mock_event_queue):
|
||||
"""Test execution when crew raises an error."""
|
||||
with patch.object(crew_executor, '_execute_crew_async', side_effect=Exception("Crew error")):
|
||||
with pytest.raises(ServerError):
|
||||
await crew_executor.execute(mock_context, mock_event_queue)
|
||||
|
||||
mock_event_queue.enqueue_event.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_existing_task(self, crew_executor, mock_event_queue):
|
||||
"""Test cancelling an existing task."""
|
||||
cancel_context = Mock(spec=RequestContext)
|
||||
cancel_context.task_id = "test-task-123"
|
||||
|
||||
async def dummy_task():
|
||||
await asyncio.sleep(10)
|
||||
|
||||
mock_task = asyncio.create_task(dummy_task())
|
||||
crew_executor._running_tasks["test-task-123"] = mock_task
|
||||
|
||||
result = await crew_executor.cancel(cancel_context, mock_event_queue)
|
||||
|
||||
assert result is None
|
||||
assert "test-task-123" not in crew_executor._running_tasks
|
||||
assert mock_task.cancelled()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_nonexistent_task(self, crew_executor, mock_event_queue):
|
||||
"""Test cancelling a task that doesn't exist."""
|
||||
cancel_context = Mock(spec=RequestContext)
|
||||
cancel_context.task_id = "nonexistent-task"
|
||||
|
||||
with pytest.raises(ServerError):
|
||||
await crew_executor.cancel(cancel_context, mock_event_queue)
|
||||
|
||||
def test_convert_output_to_parts_with_raw(self, crew_executor):
|
||||
"""Test converting crew output with raw content to A2A parts."""
|
||||
output = Mock()
|
||||
output.raw = "Test response"
|
||||
output.json_dict = None
|
||||
parts = crew_executor._convert_output_to_parts(output)
|
||||
|
||||
assert len(parts) == 1
|
||||
assert parts[0].root.text == "Test response"
|
||||
|
||||
def test_convert_output_to_parts_with_json(self, crew_executor):
|
||||
"""Test converting crew output with JSON data to A2A parts."""
|
||||
output = Mock()
|
||||
output.raw = "Test response"
|
||||
output.json_dict = {"key": "value"}
|
||||
parts = crew_executor._convert_output_to_parts(output)
|
||||
|
||||
assert len(parts) == 2
|
||||
assert parts[0].root.text == "Test response"
|
||||
assert "Structured Output:" in parts[1].root.text
|
||||
assert '"key": "value"' in parts[1].root.text
|
||||
|
||||
def test_convert_output_to_parts_empty(self, crew_executor):
|
||||
"""Test converting empty crew output to A2A parts."""
|
||||
output = ""
|
||||
parts = crew_executor._convert_output_to_parts(output)
|
||||
|
||||
assert len(parts) == 1
|
||||
assert parts[0].root.text == "Crew execution completed successfully"
|
||||
|
||||
def test_validate_request_valid(self, crew_executor, mock_context):
|
||||
"""Test request validation with valid input."""
|
||||
error = crew_executor._validate_request(mock_context)
|
||||
assert error is None
|
||||
|
||||
def test_validate_request_empty_input(self, crew_executor):
|
||||
"""Test request validation with empty input."""
|
||||
context = Mock(spec=RequestContext)
|
||||
context.get_user_input.return_value = ""
|
||||
|
||||
error = crew_executor._validate_request(context)
|
||||
assert error == "Empty or missing user input"
|
||||
|
||||
def test_validate_request_whitespace_input(self, crew_executor):
|
||||
"""Test request validation with whitespace-only input."""
|
||||
context = Mock(spec=RequestContext)
|
||||
context.get_user_input.return_value = " \n\t "
|
||||
|
||||
error = crew_executor._validate_request(context)
|
||||
assert error == "Empty or missing user input"
|
||||
|
||||
def test_validate_request_exception(self, crew_executor):
|
||||
"""Test request validation when get_user_input raises exception."""
|
||||
context = Mock(spec=RequestContext)
|
||||
context.get_user_input.side_effect = Exception("Input error")
|
||||
|
||||
error = crew_executor._validate_request(context)
|
||||
assert "Failed to extract user input" in error
|
||||
|
||||
|
||||
@pytest.mark.skipif(A2A_AVAILABLE, reason="Testing import error handling")
|
||||
def test_import_error_handling():
|
||||
"""Test that import errors are handled gracefully when A2A is not available."""
|
||||
with pytest.raises(ImportError, match="A2A integration requires"):
|
||||
from crewai.a2a import CrewAgentExecutor
|
||||
Reference in New Issue
Block a user