feat: Allow custom part converters in A2A classes

This change introduces type descriptions for the functions which convert between A2A and GenAI `Part`s. It then allows passing instances of those functions to the various A2A-related functions/classes, effectively allowing users to inject their own logic for how part conversion should occur.

The benefit of this pattern is that users can create decorators around the core `Part` conversion logic, which allows them to intercept the cases they care about while delegating the ones they do not to the core converter. This is a pattern we use a lot in the A2A Python SDK.

One example where this type of logic is useful is for extensions: this allows extension logic to, for example, interpret an A2A DataPart into a FunctionResponse using extension-specific logic.

PiperOrigin-RevId: 803186799
This commit is contained in:
Google Team Member
2025-09-04 14:57:15 -07:00
committed by Copybara-Service
parent 4df79dd5c9
commit b05fef9ba7
9 changed files with 170 additions and 99 deletions
+33 -16
View File
@@ -441,7 +441,14 @@ class TestRemoteA2aAgentMessageHandling:
def setup_method(self):
"""Setup test fixtures."""
self.agent_card = create_test_agent_card()
self.agent = RemoteA2aAgent(name="test_agent", agent_card=self.agent_card)
self.mock_genai_part_converter = Mock()
self.mock_a2a_part_converter = Mock()
self.agent = RemoteA2aAgent(
name="test_agent",
agent_card=self.agent_card,
genai_part_converter=self.mock_genai_part_converter,
a2a_part_converter=self.mock_a2a_part_converter,
)
# Mock session and context
self.mock_session = Mock(spec=Session)
@@ -519,20 +526,17 @@ class TestRemoteA2aAgentMessageHandling:
) as mock_convert:
mock_convert.return_value = mock_event
with patch(
"google.adk.agents.remote_a2a_agent.convert_genai_part_to_a2a_part"
) as mock_convert_part:
mock_a2a_part = Mock()
mock_convert_part.return_value = mock_a2a_part
mock_a2a_part = Mock()
self.mock_genai_part_converter.return_value = mock_a2a_part
result = self.agent._construct_message_parts_from_session(
self.mock_context
)
result = self.agent._construct_message_parts_from_session(
self.mock_context
)
assert len(result) == 2 # Returns tuple of (parts, context_id)
assert len(result[0]) == 1 # parts list
assert result[0][0] == mock_a2a_part
assert result[1] is None # context_id
assert len(result) == 2 # Returns tuple of (parts, context_id)
assert len(result[0]) == 1 # parts list
assert result[0][0] == mock_a2a_part
assert result[1] is None # context_id
def test_construct_message_parts_from_session_empty_events(self):
"""Test message parts construction with empty events."""
@@ -575,7 +579,10 @@ class TestRemoteA2aAgentMessageHandling:
assert result == mock_event
mock_convert.assert_called_once_with(
mock_a2a_message, self.agent.name, self.mock_context
mock_a2a_message,
self.agent.name,
self.mock_context,
self.mock_a2a_part_converter,
)
# Check that metadata was added
assert result.custom_metadata is not None
@@ -613,7 +620,10 @@ class TestRemoteA2aAgentMessageHandling:
assert result == mock_event
mock_convert.assert_called_once_with(
mock_a2a_task, self.agent.name, self.mock_context
mock_a2a_task,
self.agent.name,
self.mock_context,
self.mock_a2a_part_converter,
)
# Check that metadata was added
assert result.custom_metadata is not None
@@ -649,7 +659,14 @@ class TestRemoteA2aAgentExecution:
def setup_method(self):
"""Setup test fixtures."""
self.agent_card = create_test_agent_card()
self.agent = RemoteA2aAgent(name="test_agent", agent_card=self.agent_card)
self.mock_genai_part_converter = Mock()
self.mock_a2a_part_converter = Mock()
self.agent = RemoteA2aAgent(
name="test_agent",
agent_card=self.agent_card,
genai_part_converter=self.mock_genai_part_converter,
a2a_part_converter=self.mock_a2a_part_converter,
)
# Mock session and context
self.mock_session = Mock(spec=Session)