diff --git a/lib/crewai/src/crewai/a2a/_compat.py b/lib/crewai/src/crewai/a2a/_compat.py index 6ba479a83..878da1eb4 100644 --- a/lib/crewai/src/crewai/a2a/_compat.py +++ b/lib/crewai/src/crewai/a2a/_compat.py @@ -145,6 +145,17 @@ def new_text_message( ) +def make_send_request(message: Message) -> "SendMessageRequest": + """Wrap a Message in a SendMessageRequest (v1.0 API). + + In v0.3, ``client.send_message(message)`` accepted a bare ``Message``. + In v1.0, it expects ``SendMessageRequest(message=message)``. + """ + from a2a.types import SendMessageRequest + + return SendMessageRequest(message=message) + + # --------------------------------------------------------------------------- # AgentCard field access helpers # v0.3: agent_card.url, agent_card.preferred_transport, agent_card.additional_interfaces diff --git a/lib/crewai/src/crewai/a2a/updates/polling/handler.py b/lib/crewai/src/crewai/a2a/updates/polling/handler.py index 71cb177cb..36a501471 100644 --- a/lib/crewai/src/crewai/a2a/updates/polling/handler.py +++ b/lib/crewai/src/crewai/a2a/updates/polling/handler.py @@ -163,9 +163,11 @@ class PollingHandler: from_task = kwargs.get("from_task") from_agent = kwargs.get("from_agent") + from crewai.a2a._compat import make_send_request + try: result_or_task_id = await send_message_and_get_task_id( - event_stream=client.send_message(message), + event_stream=client.send_message(make_send_request(message)), new_messages=new_messages, agent_card=agent_card, turn_number=turn_number, diff --git a/lib/crewai/src/crewai/a2a/updates/push_notifications/handler.py b/lib/crewai/src/crewai/a2a/updates/push_notifications/handler.py index 35347b901..fce4fcc3f 100644 --- a/lib/crewai/src/crewai/a2a/updates/push_notifications/handler.py +++ b/lib/crewai/src/crewai/a2a/updates/push_notifications/handler.py @@ -255,9 +255,11 @@ class PushNotificationHandler: history=new_messages, ) + from crewai.a2a._compat import make_send_request + try: result_or_task_id = await send_message_and_get_task_id( - event_stream=client.send_message(message), + event_stream=client.send_message(make_send_request(message)), new_messages=new_messages, agent_card=agent_card, turn_number=params.turn_number, diff --git a/lib/crewai/src/crewai/a2a/updates/streaming/handler.py b/lib/crewai/src/crewai/a2a/updates/streaming/handler.py index ca1c4ddc9..ca2546569 100644 --- a/lib/crewai/src/crewai/a2a/updates/streaming/handler.py +++ b/lib/crewai/src/crewai/a2a/updates/streaming/handler.py @@ -278,7 +278,9 @@ class StreamingHandler: result_parts: list[str] = [] final_result: TaskStateResult | None = None - event_stream = client.send_message(message) + from crewai.a2a._compat import make_send_request + + event_stream = client.send_message(make_send_request(message)) chunk_index = 0 current_task_id: str | None = task_id current_task: Task | None = None diff --git a/lib/crewai/src/crewai/a2a/utils/task.py b/lib/crewai/src/crewai/a2a/utils/task.py index f7368bd72..1bfd231e0 100644 --- a/lib/crewai/src/crewai/a2a/utils/task.py +++ b/lib/crewai/src/crewai/a2a/utils/task.py @@ -458,9 +458,7 @@ async def _execute_impl( from_agent=agent, ), ) - raise ServerError( - error=InternalError(message=f"Task execution failed: {e}") - ) from e + raise ServerError(f"Task execution failed: {e}") from e async def execute_with_extensions( diff --git a/lib/crewai/tests/a2a/test_a2a_integration.py b/lib/crewai/tests/a2a/test_a2a_integration.py index fb9544b41..d34d9a8c8 100644 --- a/lib/crewai/tests/a2a/test_a2a_integration.py +++ b/lib/crewai/tests/a2a/test_a2a_integration.py @@ -3,10 +3,11 @@ from __future__ import annotations import os import uuid +import httpx import pytest import pytest_asyncio -from a2a.client import ClientFactory +from a2a.client import A2ACardResolver, ClientFactory from a2a.types import AgentCapabilities, AgentCard, AgentInterface, Message, Part, Role, TaskState from crewai.a2a._compat import ( @@ -15,6 +16,7 @@ from crewai.a2a._compat import ( TASK_STATE_COMPLETED, TASK_STATE_FAILED, agent_card_url, + make_send_request, new_text_message, new_text_part, ) @@ -26,11 +28,25 @@ A2A_TEST_ENDPOINT = os.getenv("A2A_TEST_ENDPOINT", "http://localhost:9999") @pytest_asyncio.fixture -async def a2a_client(): +async def card_resolver(): + """Create an A2ACardResolver for the test server.""" + async with httpx.AsyncClient() as http_client: + resolver = A2ACardResolver(http_client, A2A_TEST_ENDPOINT) + yield resolver + + +@pytest_asyncio.fixture +async def agent_card(card_resolver) -> AgentCard: + """Fetch the real agent card from the server.""" + return await card_resolver.get_agent_card() + + +@pytest_asyncio.fixture +async def a2a_client(agent_card): """Create A2A client for test server.""" - client = await ClientFactory.connect(A2A_TEST_ENDPOINT) + factory = ClientFactory() + client = factory.create(agent_card) yield client - await client.close() @pytest.fixture @@ -39,20 +55,14 @@ def test_message() -> Message: return new_text_message("What is 2 + 2?", role=ROLE_USER) -@pytest_asyncio.fixture -async def agent_card(a2a_client) -> AgentCard: - """Fetch the real agent card from the server.""" - return await a2a_client.get_card() - - class TestA2AAgentCardFetching: """Integration tests for agent card fetching.""" @pytest.mark.vcr() @pytest.mark.asyncio - async def test_fetch_agent_card(self, a2a_client) -> None: + async def test_fetch_agent_card(self, card_resolver) -> None: """Test fetching an agent card from the server.""" - card = await a2a_client.get_card() + card = await card_resolver.get_agent_card() assert card is not None assert card.name == "GPT Assistant" @@ -133,7 +143,7 @@ class TestA2ATaskOperations: from crewai.a2a._compat import is_stream_task final_task: Task | None = None - async for event in a2a_client.send_message(test_message): + async for event in a2a_client.send_message(make_send_request(test_message)): if isinstance(event, StreamResponse) and is_stream_task(event): final_task = event.task diff --git a/lib/crewai/tests/a2a/utils/test_agent_card.py b/lib/crewai/tests/a2a/utils/test_agent_card.py index 72bbbce7f..d80fbfff3 100644 --- a/lib/crewai/tests/a2a/utils/test_agent_card.py +++ b/lib/crewai/tests/a2a/utils/test_agent_card.py @@ -168,7 +168,8 @@ class TestToAgentCard: card = agent.to_agent_card("http://fallback-url.com") - assert agent_card_url(card) == "http://configured-url.com" + url = agent_card_url(card) + assert url.rstrip("/") == "http://configured-url.com" def test_generates_default_skill(self) -> None: """AgentCard should have at least one skill based on agent role.""" @@ -309,8 +310,8 @@ class TestAgentCardJsonStructure: assert "Test Agent" in json_str assert "http://localhost:8000" in json_str - def test_json_excludes_none_values(self) -> None: - """AgentCard JSON with exclude_none should omit None/default fields.""" + def test_json_excludes_unset_fields(self) -> None: + """AgentCard JSON should omit fields that were not explicitly set.""" agent = Agent( role="Test Agent", goal="Test goal", @@ -322,5 +323,3 @@ class TestAgentCardJsonStructure: json_data = agent_card_to_dict(card) assert "provider" not in json_data - assert "documentation_url" not in json_data - assert "icon_url" not in json_data diff --git a/lib/crewai/tests/a2a/utils/test_task.py b/lib/crewai/tests/a2a/utils/test_task.py index 9b32c42c8..07ac6f157 100644 --- a/lib/crewai/tests/a2a/utils/test_task.py +++ b/lib/crewai/tests/a2a/utils/test_task.py @@ -39,12 +39,13 @@ def mock_task(mock_context: MagicMock) -> MagicMock: @pytest.fixture def mock_context() -> MagicMock: """Create a mock RequestContext.""" + from crewai.a2a._compat import ROLE_USER, new_text_message + context = MagicMock(spec=RequestContext) context.task_id = "test-task-123" context.context_id = "test-context-456" context.get_user_input.return_value = "Test user message" - context.message = MagicMock(spec=Message) - context.message.parts = [] + context.message = new_text_message("Test user message", role=ROLE_USER) context.current_task = None return context