Fix send_message to use SendMessageRequest wrapper, fix ServerError call

- Add make_send_request() helper in _compat.py for v1.0 API
- Update all handlers to wrap Message in SendMessageRequest
- Fix ServerError(error=...) → ServerError(message) in task.py
- Fix MessageToDict parameter name (always_print_fields_with_no_presence)
- Update integration tests for v1.0 client API (A2ACardResolver, ClientFactory)
- Fix test mocks to use real protobuf Message instead of MagicMock

Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
Devin AI
2026-04-24 16:09:29 +00:00
parent bec175ec9a
commit 54470f4932
8 changed files with 51 additions and 26 deletions

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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