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:56:38 -07:00
committed by Copybara-Service
parent 4df79dd5c9
commit b05fef9ba7
9 changed files with 170 additions and 99 deletions
@@ -42,8 +42,10 @@ from ..experimental import a2a_experimental
from .part_converter import A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY
from .part_converter import A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
from .part_converter import A2A_DATA_PART_METADATA_TYPE_KEY
from .part_converter import A2APartToGenAIPartConverter
from .part_converter import convert_a2a_part_to_genai_part
from .part_converter import convert_genai_part_to_a2a_part
from .part_converter import GenAIPartToA2APartConverter
from .utils import _get_adk_metadata_key
# Constants
@@ -169,6 +171,7 @@ def convert_a2a_task_to_event(
a2a_task: Task,
author: Optional[str] = None,
invocation_context: Optional[InvocationContext] = None,
part_converter: A2APartToGenAIPartConverter = convert_a2a_part_to_genai_part,
) -> Event:
"""Converts an A2A task to an ADK event.
@@ -177,6 +180,7 @@ def convert_a2a_task_to_event(
author: The author of the event. Defaults to "a2a agent" if not provided.
invocation_context: The invocation context containing session information.
If provided, the branch will be set from the context.
part_converter: The function to convert A2A part to GenAI part.
Returns:
An ADK Event object representing the converted task.
@@ -203,7 +207,9 @@ def convert_a2a_task_to_event(
# Convert message if available
if message:
try:
return convert_a2a_message_to_event(message, author, invocation_context)
return convert_a2a_message_to_event(
message, author, invocation_context, part_converter=part_converter
)
except Exception as e:
logger.error("Failed to convert A2A task message to event: %s", e)
raise RuntimeError(f"Failed to convert task message: {e}") from e
@@ -229,6 +235,7 @@ def convert_a2a_message_to_event(
a2a_message: Message,
author: Optional[str] = None,
invocation_context: Optional[InvocationContext] = None,
part_converter: A2APartToGenAIPartConverter = convert_a2a_part_to_genai_part,
) -> Event:
"""Converts an A2A message to an ADK event.
@@ -237,6 +244,7 @@ def convert_a2a_message_to_event(
author: The author of the event. Defaults to "a2a agent" if not provided.
invocation_context: The invocation context containing session information.
If provided, the branch will be set from the context.
part_converter: The function to convert A2A part to GenAI part.
Returns:
An ADK Event object with converted content and long-running tool metadata.
@@ -269,7 +277,7 @@ def convert_a2a_message_to_event(
for a2a_part in a2a_message.parts:
try:
part = convert_a2a_part_to_genai_part(a2a_part)
part = part_converter(a2a_part)
if part is None:
logger.warning("Failed to convert A2A part, skipping: %s", a2a_part)
continue
@@ -322,13 +330,18 @@ def convert_a2a_message_to_event(
@a2a_experimental
def convert_event_to_a2a_message(
event: Event, invocation_context: InvocationContext, role: Role = Role.agent
event: Event,
invocation_context: InvocationContext,
role: Role = Role.agent,
part_converter: GenAIPartToA2APartConverter = convert_genai_part_to_a2a_part,
) -> Optional[Message]:
"""Converts an ADK event to an A2A message.
Args:
event: The ADK event to convert.
invocation_context: The invocation context.
role: The role of the message.
part_converter: The function to convert GenAI part to A2A part.
Returns:
An A2A Message if the event has content, None otherwise.
@@ -347,7 +360,7 @@ def convert_event_to_a2a_message(
try:
a2a_parts = []
for part in event.content.parts:
a2a_part = convert_genai_part_to_a2a_part(part)
a2a_part = part_converter(part)
if a2a_part:
a2a_parts.append(a2a_part)
_process_long_running_tool(a2a_part, event)
@@ -477,6 +490,7 @@ def convert_event_to_a2a_events(
invocation_context: InvocationContext,
task_id: Optional[str] = None,
context_id: Optional[str] = None,
part_converter: GenAIPartToA2APartConverter = convert_genai_part_to_a2a_part,
) -> List[A2AEvent]:
"""Converts a GenAI event to a list of A2A events.
@@ -485,6 +499,7 @@ def convert_event_to_a2a_events(
invocation_context: The invocation context.
task_id: Optional task ID to use for generated events.
context_id: Optional Context ID to use for generated events.
part_converter: The function to convert GenAI part to A2A part.
Returns:
A list of A2A events representing the converted ADK event.
@@ -509,7 +524,9 @@ def convert_event_to_a2a_events(
a2a_events.append(error_event)
# Handle regular message content
message = convert_event_to_a2a_message(event, invocation_context)
message = convert_event_to_a2a_message(
event, invocation_context, part_converter=part_converter
)
if message:
running_event = _create_status_update_event(
message, invocation_context, event, task_id, context_id
@@ -19,6 +19,7 @@ module containing utilities for conversion betwen A2A Part and Google GenAI Part
from __future__ import annotations
import base64
from collections.abc import Callable
import json
import logging
from typing import Optional
@@ -51,6 +52,14 @@ A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT = 'code_execution_result'
A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE = 'executable_code'
A2APartToGenAIPartConverter = Callable[
[a2a_types.Part], Optional[genai_types.Part]
]
GenAIPartToA2APartConverter = Callable[
[genai_types.Part], Optional[a2a_types.Part]
]
@a2a_experimental
def convert_a2a_part_to_genai_part(
a2a_part: a2a_types.Part,
@@ -31,6 +31,7 @@ from google.genai import types as genai_types
from ...runners import RunConfig
from ..experimental import a2a_experimental
from .part_converter import A2APartToGenAIPartConverter
from .part_converter import convert_a2a_part_to_genai_part
@@ -50,6 +51,7 @@ def _get_user_id(request: RequestContext) -> str:
@a2a_experimental
def convert_a2a_request_to_adk_run_args(
request: RequestContext,
part_converter: A2APartToGenAIPartConverter = convert_a2a_part_to_genai_part,
) -> dict[str, Any]:
if not request.message:
@@ -60,10 +62,7 @@ def convert_a2a_request_to_adk_run_args(
'session_id': request.context_id,
'new_message': genai_types.Content(
role='user',
parts=[
convert_a2a_part_to_genai_part(part)
for part in request.message.parts
],
parts=[part_converter(part) for part in request.message.parts],
),
'run_config': RunConfig(),
}
@@ -53,6 +53,10 @@ from pydantic import BaseModel
from typing_extensions import override
from ..converters.event_converter import convert_event_to_a2a_events
from ..converters.part_converter import A2APartToGenAIPartConverter
from ..converters.part_converter import convert_a2a_part_to_genai_part
from ..converters.part_converter import convert_genai_part_to_a2a_part
from ..converters.part_converter import GenAIPartToA2APartConverter
from ..converters.request_converter import convert_a2a_request_to_adk_run_args
from ..converters.utils import _get_adk_metadata_key
from ..experimental import a2a_experimental
@@ -65,12 +69,18 @@ logger = logging.getLogger('google_adk.' + __name__)
class A2aAgentExecutorConfig(BaseModel):
"""Configuration for the A2aAgentExecutor."""
pass
a2a_part_converter: A2APartToGenAIPartConverter = (
convert_a2a_part_to_genai_part
)
gen_ai_part_converter: GenAIPartToA2APartConverter = (
convert_genai_part_to_a2a_part
)
@a2a_experimental
class A2aAgentExecutor(AgentExecutor):
"""An AgentExecutor that runs an ADK Agent against an A2A request and
publishes updates to an event queue.
"""
@@ -82,7 +92,7 @@ class A2aAgentExecutor(AgentExecutor):
):
super().__init__()
self._runner = runner
self._config = config
self._config = config or A2aAgentExecutorConfig()
async def _resolve_runner(self) -> Runner:
"""Resolve the runner, handling cases where it's a callable that returns a Runner."""
@@ -183,7 +193,9 @@ class A2aAgentExecutor(AgentExecutor):
runner = await self._resolve_runner()
# Convert the a2a request to ADK run args
run_args = convert_a2a_request_to_adk_run_args(context)
run_args = convert_a2a_request_to_adk_run_args(
context, self._config.a2a_part_converter
)
# ensure the session exists
session = await self._prepare_session(context, run_args, runner)
@@ -217,7 +229,11 @@ class A2aAgentExecutor(AgentExecutor):
async with Aclosing(runner.run_async(**run_args)) as agen:
async for adk_event in agen:
for a2a_event in convert_event_to_a2a_events(
adk_event, invocation_context, context.task_id, context.context_id
adk_event,
invocation_context,
context.task_id,
context.context_id,
self._config.gen_ai_part_converter,
):
task_result_aggregator.process_event(a2a_event)
await event_queue.enqueue_event(a2a_event)
+17 -4
View File
@@ -57,7 +57,10 @@ import httpx
from ..a2a.converters.event_converter import convert_a2a_message_to_event
from ..a2a.converters.event_converter import convert_a2a_task_to_event
from ..a2a.converters.event_converter import convert_event_to_a2a_message
from ..a2a.converters.part_converter import A2APartToGenAIPartConverter
from ..a2a.converters.part_converter import convert_a2a_part_to_genai_part
from ..a2a.converters.part_converter import convert_genai_part_to_a2a_part
from ..a2a.converters.part_converter import GenAIPartToA2APartConverter
from ..a2a.experimental import a2a_experimental
from ..a2a.logs.log_utils import build_a2a_request_log
from ..a2a.logs.log_utils import build_a2a_response_log
@@ -120,6 +123,8 @@ class RemoteA2aAgent(BaseAgent):
description: str = "",
httpx_client: Optional[httpx.AsyncClient] = None,
timeout: float = DEFAULT_TIMEOUT,
genai_part_converter: GenAIPartToA2APartConverter = convert_genai_part_to_a2a_part,
a2a_part_converter: A2APartToGenAIPartConverter = convert_a2a_part_to_genai_part,
**kwargs: Any,
) -> None:
"""Initialize RemoteA2aAgent.
@@ -149,6 +154,8 @@ class RemoteA2aAgent(BaseAgent):
self._httpx_client_needs_cleanup = httpx_client is None
self._timeout = timeout
self._is_resolved = False
self._genai_part_converter = genai_part_converter
self._a2a_part_converter = a2a_part_converter
# Validate and store agent card reference
if isinstance(agent_card, AgentCard):
@@ -298,7 +305,7 @@ class RemoteA2aAgent(BaseAgent):
return None
a2a_message = convert_event_to_a2a_message(
ctx.session.events[-1], ctx, Role.user
ctx.session.events[-1], ctx, Role.user, self._genai_part_converter
)
if function_call_event.custom_metadata:
a2a_message.task_id = (
@@ -355,7 +362,7 @@ class RemoteA2aAgent(BaseAgent):
for part in event.content.parts:
converted_part = convert_genai_part_to_a2a_part(part)
converted_part = self._genai_part_converter(part)
if converted_part:
message_parts.append(converted_part)
else:
@@ -380,7 +387,10 @@ class RemoteA2aAgent(BaseAgent):
if a2a_response.root.result:
if isinstance(a2a_response.root.result, A2ATask):
event = convert_a2a_task_to_event(
a2a_response.root.result, self.name, ctx
a2a_response.root.result,
self.name,
ctx,
self._a2a_part_converter,
)
event.custom_metadata = event.custom_metadata or {}
event.custom_metadata[A2A_METADATA_PREFIX + "task_id"] = (
@@ -389,7 +399,10 @@ class RemoteA2aAgent(BaseAgent):
else:
event = convert_a2a_message_to_event(
a2a_response.root.result, self.name, ctx
a2a_response.root.result,
self.name,
ctx,
self._a2a_part_converter,
)
event.custom_metadata = event.custom_metadata or {}
if a2a_response.root.result.task_id:
@@ -652,6 +652,8 @@ class TestA2AToEventConverters:
with patch(
"google.adk.a2a.converters.event_converter.convert_a2a_message_to_event"
) as mock_convert_message:
from google.adk.a2a.converters.part_converter import convert_a2a_part_to_genai_part
mock_event = Mock(spec=Event)
mock_convert_message.return_value = mock_event
@@ -662,7 +664,10 @@ class TestA2AToEventConverters:
assert result == mock_event
# Should call convert_a2a_message_to_event with the status message
mock_convert_message.assert_called_once_with(
mock_status.message, "test-author", self.mock_invocation_context
mock_status.message,
"test-author",
self.mock_invocation_context,
part_converter=convert_a2a_part_to_genai_part,
)
def test_convert_a2a_task_to_event_with_history_message(self):
@@ -680,6 +685,8 @@ class TestA2AToEventConverters:
with patch(
"google.adk.a2a.converters.event_converter.convert_a2a_message_to_event"
) as mock_convert_message:
from google.adk.a2a.converters.part_converter import convert_a2a_part_to_genai_part
mock_event = Mock(spec=Event)
mock_event.invocation_id = "test-invocation-id"
mock_convert_message.return_value = mock_event
@@ -688,7 +695,10 @@ class TestA2AToEventConverters:
# Verify the message converter was called with correct parameters
mock_convert_message.assert_called_once_with(
mock_message, "test-author", None
mock_message,
"test-author",
None,
part_converter=convert_a2a_part_to_genai_part,
)
assert result == mock_event
@@ -761,10 +771,7 @@ class TestA2AToEventConverters:
with pytest.raises(RuntimeError, match="Failed to convert task message"):
convert_a2a_task_to_event(mock_task, "test-author")
@patch(
"google.adk.a2a.converters.event_converter.convert_a2a_part_to_genai_part"
)
def test_convert_a2a_message_to_event_success(self, mock_convert_part):
def test_convert_a2a_message_to_event_success(self):
"""Test successful conversion of A2A message to event."""
from google.adk.a2a.converters.event_converter import convert_a2a_message_to_event
from google.genai import types as genai_types
@@ -772,13 +779,17 @@ class TestA2AToEventConverters:
# Create mock parts and message with valid genai Part
mock_a2a_part = Mock()
mock_genai_part = genai_types.Part(text="test content")
mock_convert_part = Mock()
mock_convert_part.return_value = mock_genai_part
mock_message = Mock(spec=Message)
mock_message.parts = [mock_a2a_part]
result = convert_a2a_message_to_event(
mock_message, "test-author", self.mock_invocation_context
mock_message,
"test-author",
self.mock_invocation_context,
mock_convert_part,
)
# Verify conversion was successful
@@ -790,12 +801,7 @@ class TestA2AToEventConverters:
assert result.content.parts[0].text == "test content"
mock_convert_part.assert_called_once_with(mock_a2a_part)
@patch(
"google.adk.a2a.converters.event_converter.convert_a2a_part_to_genai_part"
)
def test_convert_a2a_message_to_event_with_long_running_tools(
self, mock_convert_part
):
def test_convert_a2a_message_to_event_with_long_running_tools(self):
"""Test conversion with long-running tools by mocking the entire flow."""
from google.adk.a2a.converters.event_converter import convert_a2a_message_to_event
@@ -805,6 +811,7 @@ class TestA2AToEventConverters:
mock_message.parts = [mock_a2a_part]
# Mock the part conversion to return None to simulate long-running tool detection logic
mock_convert_part = Mock()
mock_convert_part.return_value = None
# Patch the long-running tool detection since the main logic is in the actual conversion
@@ -812,7 +819,10 @@ class TestA2AToEventConverters:
"google.adk.a2a.converters.event_converter.logger"
) as mock_logger:
result = convert_a2a_message_to_event(
mock_message, "test-author", self.mock_invocation_context
mock_message,
"test-author",
self.mock_invocation_context,
mock_convert_part,
)
# Verify basic conversion worked
@@ -845,24 +855,23 @@ class TestA2AToEventConverters:
with pytest.raises(ValueError, match="A2A message cannot be None"):
convert_a2a_message_to_event(None)
@patch(
"google.adk.a2a.converters.event_converter.convert_a2a_part_to_genai_part"
)
def test_convert_a2a_message_to_event_part_conversion_fails(
self, mock_convert_part
):
def test_convert_a2a_message_to_event_part_conversion_fails(self):
"""Test handling when part conversion returns None."""
from google.adk.a2a.converters.event_converter import convert_a2a_message_to_event
# Setup mock to return None (conversion failure)
mock_a2a_part = Mock()
mock_convert_part = Mock()
mock_convert_part.return_value = None
mock_message = Mock(spec=Message)
mock_message.parts = [mock_a2a_part]
result = convert_a2a_message_to_event(
mock_message, "test-author", self.mock_invocation_context
mock_message,
"test-author",
self.mock_invocation_context,
mock_convert_part,
)
# Verify event was created but with no parts
@@ -871,12 +880,7 @@ class TestA2AToEventConverters:
assert result.content.role == "model"
assert len(result.content.parts) == 0
@patch(
"google.adk.a2a.converters.event_converter.convert_a2a_part_to_genai_part"
)
def test_convert_a2a_message_to_event_part_conversion_exception(
self, mock_convert_part
):
def test_convert_a2a_message_to_event_part_conversion_exception(self):
"""Test handling when part conversion raises exception."""
from google.adk.a2a.converters.event_converter import convert_a2a_message_to_event
from google.genai import types as genai_types
@@ -886,6 +890,7 @@ class TestA2AToEventConverters:
mock_a2a_part2 = Mock()
mock_genai_part = genai_types.Part(text="successful conversion")
mock_convert_part = Mock()
mock_convert_part.side_effect = [
Exception("Conversion failed"), # First part fails
mock_genai_part, # Second part succeeds
@@ -895,7 +900,10 @@ class TestA2AToEventConverters:
mock_message.parts = [mock_a2a_part1, mock_a2a_part2]
result = convert_a2a_message_to_event(
mock_message, "test-author", self.mock_invocation_context
mock_message,
"test-author",
self.mock_invocation_context,
mock_convert_part,
)
# Verify event was created with only the successfully converted part
@@ -905,12 +913,7 @@ class TestA2AToEventConverters:
assert len(result.content.parts) == 1
assert result.content.parts[0].text == "successful conversion"
@patch(
"google.adk.a2a.converters.event_converter.convert_a2a_part_to_genai_part"
)
def test_convert_a2a_message_to_event_missing_tool_id(
self, mock_convert_part
):
def test_convert_a2a_message_to_event_missing_tool_id(self):
"""Test handling of message conversion when part conversion fails."""
from google.adk.a2a.converters.event_converter import convert_a2a_message_to_event
@@ -920,10 +923,14 @@ class TestA2AToEventConverters:
mock_message.parts = [mock_a2a_part]
# Mock the part conversion to return None
mock_convert_part = Mock()
mock_convert_part.return_value = None
result = convert_a2a_message_to_event(
mock_message, "test-author", self.mock_invocation_context
mock_message,
"test-author",
self.mock_invocation_context,
mock_convert_part,
)
# Verify basic conversion worked
@@ -146,10 +146,7 @@ class TestGetUserId:
class TestConvertA2aRequestToAdkRunArgs:
"""Test cases for convert_a2a_request_to_adk_run_args function."""
@patch(
"google.adk.a2a.converters.request_converter.convert_a2a_part_to_genai_part"
)
def test_convert_a2a_request_basic(self, mock_convert_part):
def test_convert_a2a_request_basic(self):
"""Test basic conversion of A2A request to ADK run args."""
# Arrange
mock_part1 = Mock()
@@ -172,10 +169,11 @@ class TestConvertA2aRequestToAdkRunArgs:
# Create proper genai_types.Part objects instead of mocks
mock_genai_part1 = genai_types.Part(text="test part 1")
mock_genai_part2 = genai_types.Part(text="test part 2")
mock_convert_part = Mock()
mock_convert_part.side_effect = [mock_genai_part1, mock_genai_part2]
# Act
result = convert_a2a_request_to_adk_run_args(request)
result = convert_a2a_request_to_adk_run_args(request, mock_convert_part)
# Assert
assert result is not None
@@ -201,14 +199,12 @@ class TestConvertA2aRequestToAdkRunArgs:
with pytest.raises(ValueError, match="Request message cannot be None"):
convert_a2a_request_to_adk_run_args(request)
@patch(
"google.adk.a2a.converters.request_converter.convert_a2a_part_to_genai_part"
)
def test_convert_a2a_request_empty_parts(self, mock_convert_part):
def test_convert_a2a_request_empty_parts(self):
"""Test conversion with empty parts list."""
# Arrange
mock_message = Mock()
mock_message.parts = []
mock_convert_part = Mock()
request = Mock(spec=RequestContext)
request.message = mock_message
@@ -216,7 +212,7 @@ class TestConvertA2aRequestToAdkRunArgs:
request.call_context = None
# Act
result = convert_a2a_request_to_adk_run_args(request)
result = convert_a2a_request_to_adk_run_args(request, mock_convert_part)
# Assert
assert result is not None
@@ -230,10 +226,7 @@ class TestConvertA2aRequestToAdkRunArgs:
# Verify convert_part wasn't called
mock_convert_part.assert_not_called()
@patch(
"google.adk.a2a.converters.request_converter.convert_a2a_part_to_genai_part"
)
def test_convert_a2a_request_none_context_id(self, mock_convert_part):
def test_convert_a2a_request_none_context_id(self):
"""Test conversion when context_id is None."""
# Arrange
mock_part = Mock()
@@ -247,10 +240,11 @@ class TestConvertA2aRequestToAdkRunArgs:
# Create proper genai_types.Part object instead of mock
mock_genai_part = genai_types.Part(text="test part")
mock_convert_part = Mock()
mock_convert_part.return_value = mock_genai_part
# Act
result = convert_a2a_request_to_adk_run_args(request)
result = convert_a2a_request_to_adk_run_args(request, mock_convert_part)
# Assert
assert result is not None
@@ -261,10 +255,7 @@ class TestConvertA2aRequestToAdkRunArgs:
assert result["new_message"].parts == [mock_genai_part]
assert isinstance(result["run_config"], RunConfig)
@patch(
"google.adk.a2a.converters.request_converter.convert_a2a_part_to_genai_part"
)
def test_convert_a2a_request_no_auth(self, mock_convert_part):
def test_convert_a2a_request_no_auth(self):
"""Test conversion when no authentication is available."""
# Arrange
mock_part = Mock()
@@ -278,10 +269,11 @@ class TestConvertA2aRequestToAdkRunArgs:
# Create proper genai_types.Part object instead of mock
mock_genai_part = genai_types.Part(text="test part")
mock_convert_part = Mock()
mock_convert_part.return_value = mock_genai_part
# Act
result = convert_a2a_request_to_adk_run_args(request)
result = convert_a2a_request_to_adk_run_args(request, mock_convert_part)
# Assert
assert result is not None
@@ -296,10 +288,7 @@ class TestConvertA2aRequestToAdkRunArgs:
class TestIntegration:
"""Integration test cases combining both functions."""
@patch(
"google.adk.a2a.converters.request_converter.convert_a2a_part_to_genai_part"
)
def test_end_to_end_conversion_with_auth_user(self, mock_convert_part):
def test_end_to_end_conversion_with_auth_user(self):
"""Test end-to-end conversion with authenticated user."""
# Arrange
mock_user = Mock()
@@ -319,10 +308,11 @@ class TestIntegration:
# Create proper genai_types.Part object instead of mock
mock_genai_part = genai_types.Part(text="test part")
mock_convert_part = Mock()
mock_convert_part.return_value = mock_genai_part
# Act
result = convert_a2a_request_to_adk_run_args(request)
result = convert_a2a_request_to_adk_run_args(request, mock_convert_part)
# Assert
assert result is not None
@@ -333,10 +323,7 @@ class TestIntegration:
assert result["new_message"].parts == [mock_genai_part]
assert isinstance(result["run_config"], RunConfig)
@patch(
"google.adk.a2a.converters.request_converter.convert_a2a_part_to_genai_part"
)
def test_end_to_end_conversion_with_fallback_user(self, mock_convert_part):
def test_end_to_end_conversion_with_fallback_user(self):
"""Test end-to-end conversion with fallback user ID."""
# Arrange
mock_part = Mock()
@@ -350,10 +337,11 @@ class TestIntegration:
# Create proper genai_types.Part object instead of mock
mock_genai_part = genai_types.Part(text="test part")
mock_convert_part = Mock()
mock_convert_part.return_value = mock_genai_part
# Act
result = convert_a2a_request_to_adk_run_args(request)
result = convert_a2a_request_to_adk_run_args(request, mock_convert_part)
# Assert
assert result is not None
@@ -56,7 +56,12 @@ class TestA2aAgentExecutor:
self.mock_runner._new_invocation_context = Mock()
self.mock_runner.run_async = AsyncMock()
self.mock_config = Mock(spec=A2aAgentExecutorConfig)
self.mock_a2a_part_converter = Mock()
self.mock_gen_ai_part_converter = Mock()
self.mock_config = A2aAgentExecutorConfig(
a2a_part_converter=self.mock_a2a_part_converter,
gen_ai_part_converter=self.mock_gen_ai_part_converter,
)
self.executor = A2aAgentExecutor(
runner=self.mock_runner, config=self.mock_config
)
+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)