Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/a2a/compat/v0_3/rest_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ async def send_message_streaming(

async for event in self._send_stream_request(
'POST',
'/v1/message:stream',
'/v1/message:stream?alt=sse',
context=context,
json=MessageToDict(req_proto, preserving_proto_field_name=True),
):
Expand Down Expand Up @@ -291,7 +291,7 @@ async def subscribe(
try:
async for event in self._send_stream_request(
subscribe_method,
f'/v1/tasks/{request.id}:subscribe',
f'/v1/tasks/{request.id}:subscribe?alt=sse',
context=context,
):
yield event
Expand Down
4 changes: 2 additions & 2 deletions src/a2a/compat/v0_3/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from enum import Enum
from typing import Any, Literal

from pydantic import Field, RootModel
from pydantic import AliasChoices, Field, RootModel

from a2a._base import A2ABaseModel

Expand Down Expand Up @@ -1458,7 +1458,7 @@ class Message(A2ABaseModel):
"""
Optional metadata for extensions. The key is an extension-specific identifier.
"""
parts: list[Part]
parts: list[Part] = Field(validation_alias=AliasChoices('parts', 'content'))
"""
An array of content parts that form the message body. A message can be
composed of multiple parts of different types (e.g., text and files).
Expand Down
21 changes: 21 additions & 0 deletions tests/compat/v0_3/test_conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,27 @@ def test_message_conversion():
assert v03_restored == v03_msg


def test_message_content_alias():
"""Test that Message accepts 'content' as alias for 'parts' (v0.3 compatibility)."""
# Test with 'content' key (legacy v0.3 wire format)
legacy_json = {
'messageId': 'msg-123',
'role': 'user',
'content': [{'kind': 'text', 'text': 'hello'}],
}
msg = types_v03.Message.model_validate(legacy_json)
assert msg.parts == [types_v03.Part(root=types_v03.TextPart(text='hello'))]

# Test with 'parts' key (modern format)
modern_json = {
'messageId': 'msg-456',
'role': 'agent',
'parts': [{'kind': 'text', 'text': 'response'}],
}
msg2 = types_v03.Message.model_validate(modern_json)
assert msg2.parts == [types_v03.Part(root=types_v03.TextPart(text='response'))]


def test_message_conversion_minimal():
v03_msg = types_v03.Message(
message_id='m1',
Expand Down
29 changes: 29 additions & 0 deletions tests/compat/v0_3/test_rest_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,35 @@ async def mock_send_stream_request(*args, **kwargs):
assert events[1] == StreamResponse(message=Message(message_id='msg-123'))


@pytest.mark.asyncio
async def test_compat_rest_transport_stream_url_has_alt_sse_param(transport):
"""Test that streaming endpoints include ?alt=sse parameter."""
captured_paths = []

async def mock_send_stream_request(method, path, context=None, json=None):
captured_paths.append(path)
task = Task(id='task-123')
task.status.message.role = Role.ROLE_AGENT
yield StreamResponse(task=task)

transport._send_stream_request = mock_send_stream_request

# Test send_message_streaming
req = SendMessageRequest(
message=Message(message_id='msg-1', role=Role.ROLE_USER)
)
_ = [event async for event in transport.send_message_streaming(req)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

Using an asynchronous list comprehension solely to consume an async generator creates an unnecessary list in memory. Consider consuming the generator with a simple loop or storing the list of events to assert on them.

Suggested change
_ = [event async for event in transport.send_message_streaming(req)]
events = [event async for event in transport.send_message_streaming(req)]


assert captured_paths[-1] == '/v1/message:stream?alt=sse'

# Test subscribe
captured_paths.clear()
sub_req = SubscribeToTaskRequest(id='task-123')
_ = [event async for event in transport.subscribe(sub_req)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low

Using an asynchronous list comprehension solely to consume an async generator creates an unnecessary list in memory. Consider consuming the generator with a simple loop or storing the list of events to assert on them.

Suggested change
_ = [event async for event in transport.subscribe(sub_req)]
events = [event async for event in transport.subscribe(sub_req)]


assert captured_paths[-1] == '/v1/tasks/task-123:subscribe?alt=sse'


def create_405_error():
mock_response = MagicMock(spec=httpx.Response)
mock_response.status_code = 405
Expand Down
Loading