Skip to content

Commit 824ab07

Browse files
google-genai-botcopybara-github
authored andcommitted
fix: Let part converters also return multiple parts so they can support more usecases
PiperOrigin-RevId: 830882000
1 parent fd33610 commit 824ab07

File tree

7 files changed

+188
-19
lines changed

7 files changed

+188
-19
lines changed

src/google/adk/a2a/converters/event_converter.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -301,13 +301,15 @@ def convert_a2a_message_to_event(
301301
)
302302

303303
try:
304-
parts = []
304+
output_parts = []
305305
long_running_tool_ids = set()
306306

307307
for a2a_part in a2a_message.parts:
308308
try:
309-
part = part_converter(a2a_part)
310-
if part is None:
309+
parts = part_converter(a2a_part)
310+
if not isinstance(parts, list):
311+
parts = [parts] if parts else []
312+
if not parts:
311313
logger.warning("Failed to convert A2A part, skipping: %s", a2a_part)
312314
continue
313315

@@ -321,16 +323,18 @@ def convert_a2a_message_to_event(
321323
)
322324
is True
323325
):
324-
long_running_tool_ids.add(part.function_call.id)
326+
for part in parts:
327+
if part.function_call:
328+
long_running_tool_ids.add(part.function_call.id)
325329

326-
parts.append(part)
330+
output_parts.extend(parts)
327331

328332
except Exception as e:
329333
logger.error("Failed to convert A2A part: %s, error: %s", a2a_part, e)
330334
# Continue processing other parts instead of failing completely
331335
continue
332336

333-
if not parts:
337+
if not output_parts:
334338
logger.warning(
335339
"No parts could be converted from A2A message %s", a2a_message
336340
)
@@ -348,7 +352,7 @@ def convert_a2a_message_to_event(
348352
else None,
349353
content=genai_types.Content(
350354
role="model",
351-
parts=parts,
355+
parts=output_parts,
352356
),
353357
)
354358

@@ -387,15 +391,19 @@ def convert_event_to_a2a_message(
387391
return None
388392

389393
try:
390-
a2a_parts = []
394+
output_parts = []
391395
for part in event.content.parts:
392-
a2a_part = part_converter(part)
393-
if a2a_part:
394-
a2a_parts.append(a2a_part)
396+
a2a_parts = part_converter(part)
397+
if not isinstance(a2a_parts, list):
398+
a2a_parts = [a2a_parts] if a2a_parts else []
399+
for a2a_part in a2a_parts:
400+
output_parts.append(a2a_part)
395401
_process_long_running_tool(a2a_part, event)
396402

397-
if a2a_parts:
398-
return Message(message_id=str(uuid.uuid4()), role=role, parts=a2a_parts)
403+
if output_parts:
404+
return Message(
405+
message_id=str(uuid.uuid4()), role=role, parts=output_parts
406+
)
399407

400408
except Exception as e:
401409
logger.error("Failed to convert event to status message: %s", e)

src/google/adk/a2a/converters/part_converter.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
from collections.abc import Callable
2323
import json
2424
import logging
25+
from typing import List
2526
from typing import Optional
27+
from typing import Union
2628

2729
from .utils import _get_adk_metadata_key
2830

@@ -53,10 +55,11 @@
5355

5456

5557
A2APartToGenAIPartConverter = Callable[
56-
[a2a_types.Part], Optional[genai_types.Part]
58+
[a2a_types.Part], Union[Optional[genai_types.Part], List[genai_types.Part]]
5759
]
5860
GenAIPartToA2APartConverter = Callable[
59-
[genai_types.Part], Optional[a2a_types.Part]
61+
[genai_types.Part],
62+
Union[Optional[a2a_types.Part], List[a2a_types.Part]],
6063
]
6164

6265

src/google/adk/a2a/converters/request_converter.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,19 @@ def convert_a2a_request_to_agent_run_request(
110110
if request.metadata:
111111
custom_metadata['a2a_metadata'] = request.metadata
112112

113+
output_parts = []
114+
for a2a_part in request.message.parts:
115+
genai_parts = part_converter(a2a_part)
116+
if not isinstance(genai_parts, list):
117+
genai_parts = [genai_parts] if genai_parts else []
118+
output_parts.extend(genai_parts)
119+
113120
return AgentRunRequest(
114121
user_id=_get_user_id(request),
115122
session_id=request.context_id,
116123
new_message=genai_types.Content(
117124
role='user',
118-
parts=[part_converter(part) for part in request.message.parts],
125+
parts=output_parts,
119126
),
120127
run_config=RunConfig(custom_metadata=custom_metadata),
121128
)

src/google/adk/agents/remote_a2a_agent.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,9 +376,12 @@ def _construct_message_parts_from_session(
376376
continue
377377

378378
for part in event.content.parts:
379-
converted_part = self._genai_part_converter(part)
380-
if converted_part:
381-
message_parts.append(converted_part)
379+
converted_parts = self._genai_part_converter(part)
380+
if not isinstance(converted_parts, list):
381+
converted_parts = [converted_parts] if converted_parts else []
382+
383+
if converted_parts:
384+
message_parts.extend(converted_parts)
382385
else:
383386
logger.warning("Failed to convert part to A2A format: %s", part)
384387

tests/unittests/a2a/converters/test_event_converter.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,37 @@ def test_create_status_update_event_with_input_required_state(self):
576576
assert result.context_id == context_id
577577
assert result.status.state == TaskState.input_required
578578

579+
def test_convert_event_to_a2a_message_with_multiple_parts_returned(self):
580+
"""Test event to message conversion when part_converter returns multiple parts."""
581+
from a2a import types as a2a_types
582+
from google.adk.a2a.converters.event_converter import convert_event_to_a2a_message
583+
from google.genai import types as genai_types
584+
585+
# Arrange
586+
mock_genai_part = genai_types.Part(text="source part")
587+
mock_a2a_part1 = a2a_types.Part(root=a2a_types.TextPart(text="part 1"))
588+
mock_a2a_part2 = a2a_types.Part(root=a2a_types.TextPart(text="part 2"))
589+
mock_convert_part = Mock()
590+
mock_convert_part.return_value = [mock_a2a_part1, mock_a2a_part2]
591+
592+
self.mock_event.content = genai_types.Content(
593+
parts=[mock_genai_part], role="model"
594+
)
595+
596+
# Act
597+
result = convert_event_to_a2a_message(
598+
self.mock_event,
599+
self.mock_invocation_context,
600+
part_converter=mock_convert_part,
601+
)
602+
603+
# Assert
604+
assert result is not None
605+
assert len(result.parts) == 2
606+
assert result.parts[0].root.text == "part 1"
607+
assert result.parts[1].root.text == "part 2"
608+
mock_convert_part.assert_called_once_with(mock_genai_part)
609+
579610

580611
class TestA2AToEventConverters:
581612
"""Test suite for A2A to Event conversion functions."""
@@ -801,6 +832,36 @@ def test_convert_a2a_message_to_event_success(self):
801832
assert result.content.parts[0].text == "test content"
802833
mock_convert_part.assert_called_once_with(mock_a2a_part)
803834

835+
def test_convert_a2a_message_to_event_with_multiple_parts_returned(self):
836+
"""Test message to event conversion when part_converter returns multiple parts."""
837+
from google.adk.a2a.converters.event_converter import convert_a2a_message_to_event
838+
from google.genai import types as genai_types
839+
840+
# Arrange
841+
mock_a2a_part = Mock()
842+
mock_genai_part1 = genai_types.Part(text="part 1")
843+
mock_genai_part2 = genai_types.Part(text="part 2")
844+
mock_convert_part = Mock()
845+
mock_convert_part.return_value = [mock_genai_part1, mock_genai_part2]
846+
847+
mock_message = Mock(spec=Message)
848+
mock_message.parts = [mock_a2a_part]
849+
850+
# Act
851+
result = convert_a2a_message_to_event(
852+
mock_message,
853+
"test-author",
854+
self.mock_invocation_context,
855+
mock_convert_part,
856+
)
857+
858+
# Assert
859+
assert result.content.role == "model"
860+
assert len(result.content.parts) == 2
861+
assert result.content.parts[0].text == "part 1"
862+
assert result.content.parts[1].text == "part 2"
863+
mock_convert_part.assert_called_once_with(mock_a2a_part)
864+
804865
def test_convert_a2a_message_to_event_with_long_running_tools(self):
805866
"""Test conversion with long-running tools by mocking the entire flow."""
806867
from google.adk.a2a.converters.event_converter import convert_a2a_message_to_event

tests/unittests/a2a/converters/test_request_converter.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,58 @@ def test_convert_a2a_request_basic(self):
195195
mock_convert_part.assert_any_call(mock_part1)
196196
mock_convert_part.assert_any_call(mock_part2)
197197

198+
def test_convert_a2a_request_multiple_parts(self):
199+
"""Test basic conversion of A2A request to ADK AgentRunRequest."""
200+
# Arrange
201+
mock_part1 = Mock()
202+
mock_part2 = Mock()
203+
204+
mock_message = Mock()
205+
mock_message.parts = [mock_part1, mock_part2]
206+
207+
mock_user = Mock()
208+
mock_user.user_name = "test_user"
209+
210+
mock_call_context = Mock()
211+
mock_call_context.user = mock_user
212+
213+
request = Mock(spec=RequestContext)
214+
request.message = mock_message
215+
request.context_id = "test_context_123"
216+
request.call_context = mock_call_context
217+
request.metadata = {"test_key": "test_value"}
218+
219+
# Create proper genai_types.Part objects instead of mocks
220+
mock_genai_part1 = genai_types.Part(text="test part 1")
221+
mock_genai_part2 = genai_types.Part(text="test part 2")
222+
mock_convert_part = Mock()
223+
mock_convert_part.side_effect = [mock_genai_part1, mock_genai_part2]
224+
225+
# Act
226+
result = convert_a2a_request_to_agent_run_request(
227+
request, mock_convert_part
228+
)
229+
230+
# Assert
231+
assert result is not None
232+
assert result.user_id == "test_user"
233+
assert result.session_id == "test_context_123"
234+
assert isinstance(result.new_message, genai_types.Content)
235+
assert result.new_message.role == "user"
236+
assert result.new_message.parts == [
237+
mock_genai_part1,
238+
mock_genai_part2,
239+
]
240+
assert isinstance(result.run_config, RunConfig)
241+
assert result.run_config.custom_metadata == {
242+
"a2a_metadata": {"test_key": "test_value"}
243+
}
244+
245+
# Verify calls
246+
assert mock_convert_part.call_count == 2
247+
mock_convert_part.assert_any_call(mock_part1)
248+
mock_convert_part.assert_any_call(mock_part2)
249+
198250
def test_convert_a2a_request_no_message_raises_error(self):
199251
"""Test that conversion raises ValueError when message is None."""
200252
# Arrange

tests/unittests/agents/test_remote_a2a_agent.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,41 @@ def test_construct_message_parts_from_session_success(self):
653653
assert result[0][0] == mock_a2a_part
654654
assert result[1] is None # context_id
655655

656+
def test_construct_message_parts_from_session_success_multiple_parts(self):
657+
"""Test successful message parts construction from session."""
658+
# Mock event with text content
659+
mock_part = Mock()
660+
mock_part.text = "Hello world"
661+
662+
mock_content = Mock()
663+
mock_content.parts = [mock_part]
664+
665+
mock_event = Mock()
666+
mock_event.content = mock_content
667+
668+
self.mock_session.events = [mock_event]
669+
670+
with patch(
671+
"google.adk.agents.remote_a2a_agent._present_other_agent_message"
672+
) as mock_convert:
673+
mock_convert.return_value = mock_event
674+
675+
mock_a2a_part1 = Mock()
676+
mock_a2a_part2 = Mock()
677+
self.mock_genai_part_converter.return_value = [
678+
mock_a2a_part1,
679+
mock_a2a_part2,
680+
]
681+
682+
result = self.agent._construct_message_parts_from_session(
683+
self.mock_context
684+
)
685+
686+
assert len(result) == 2 # Returns tuple of (parts, context_id)
687+
assert len(result[0]) == 2 # parts list
688+
assert result[0] == [mock_a2a_part1, mock_a2a_part2]
689+
assert result[1] is None # context_id
690+
656691
def test_construct_message_parts_from_session_empty_events(self):
657692
"""Test message parts construction with empty events."""
658693
self.mock_session.events = []

0 commit comments

Comments
 (0)