Skip to content

Commit b05fef9

Browse files
google-genai-botcopybara-github
authored andcommitted
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
1 parent 4df79dd commit b05fef9

File tree

9 files changed

+170
-99
lines changed

9 files changed

+170
-99
lines changed

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

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@
4242
from .part_converter import A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY
4343
from .part_converter import A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
4444
from .part_converter import A2A_DATA_PART_METADATA_TYPE_KEY
45+
from .part_converter import A2APartToGenAIPartConverter
4546
from .part_converter import convert_a2a_part_to_genai_part
4647
from .part_converter import convert_genai_part_to_a2a_part
48+
from .part_converter import GenAIPartToA2APartConverter
4749
from .utils import _get_adk_metadata_key
4850

4951
# Constants
@@ -169,6 +171,7 @@ def convert_a2a_task_to_event(
169171
a2a_task: Task,
170172
author: Optional[str] = None,
171173
invocation_context: Optional[InvocationContext] = None,
174+
part_converter: A2APartToGenAIPartConverter = convert_a2a_part_to_genai_part,
172175
) -> Event:
173176
"""Converts an A2A task to an ADK event.
174177
@@ -177,6 +180,7 @@ def convert_a2a_task_to_event(
177180
author: The author of the event. Defaults to "a2a agent" if not provided.
178181
invocation_context: The invocation context containing session information.
179182
If provided, the branch will be set from the context.
183+
part_converter: The function to convert A2A part to GenAI part.
180184
181185
Returns:
182186
An ADK Event object representing the converted task.
@@ -203,7 +207,9 @@ def convert_a2a_task_to_event(
203207
# Convert message if available
204208
if message:
205209
try:
206-
return convert_a2a_message_to_event(message, author, invocation_context)
210+
return convert_a2a_message_to_event(
211+
message, author, invocation_context, part_converter=part_converter
212+
)
207213
except Exception as e:
208214
logger.error("Failed to convert A2A task message to event: %s", e)
209215
raise RuntimeError(f"Failed to convert task message: {e}") from e
@@ -229,6 +235,7 @@ def convert_a2a_message_to_event(
229235
a2a_message: Message,
230236
author: Optional[str] = None,
231237
invocation_context: Optional[InvocationContext] = None,
238+
part_converter: A2APartToGenAIPartConverter = convert_a2a_part_to_genai_part,
232239
) -> Event:
233240
"""Converts an A2A message to an ADK event.
234241
@@ -237,6 +244,7 @@ def convert_a2a_message_to_event(
237244
author: The author of the event. Defaults to "a2a agent" if not provided.
238245
invocation_context: The invocation context containing session information.
239246
If provided, the branch will be set from the context.
247+
part_converter: The function to convert A2A part to GenAI part.
240248
241249
Returns:
242250
An ADK Event object with converted content and long-running tool metadata.
@@ -269,7 +277,7 @@ def convert_a2a_message_to_event(
269277

270278
for a2a_part in a2a_message.parts:
271279
try:
272-
part = convert_a2a_part_to_genai_part(a2a_part)
280+
part = part_converter(a2a_part)
273281
if part is None:
274282
logger.warning("Failed to convert A2A part, skipping: %s", a2a_part)
275283
continue
@@ -322,13 +330,18 @@ def convert_a2a_message_to_event(
322330

323331
@a2a_experimental
324332
def convert_event_to_a2a_message(
325-
event: Event, invocation_context: InvocationContext, role: Role = Role.agent
333+
event: Event,
334+
invocation_context: InvocationContext,
335+
role: Role = Role.agent,
336+
part_converter: GenAIPartToA2APartConverter = convert_genai_part_to_a2a_part,
326337
) -> Optional[Message]:
327338
"""Converts an ADK event to an A2A message.
328339
329340
Args:
330341
event: The ADK event to convert.
331342
invocation_context: The invocation context.
343+
role: The role of the message.
344+
part_converter: The function to convert GenAI part to A2A part.
332345
333346
Returns:
334347
An A2A Message if the event has content, None otherwise.
@@ -347,7 +360,7 @@ def convert_event_to_a2a_message(
347360
try:
348361
a2a_parts = []
349362
for part in event.content.parts:
350-
a2a_part = convert_genai_part_to_a2a_part(part)
363+
a2a_part = part_converter(part)
351364
if a2a_part:
352365
a2a_parts.append(a2a_part)
353366
_process_long_running_tool(a2a_part, event)
@@ -477,6 +490,7 @@ def convert_event_to_a2a_events(
477490
invocation_context: InvocationContext,
478491
task_id: Optional[str] = None,
479492
context_id: Optional[str] = None,
493+
part_converter: GenAIPartToA2APartConverter = convert_genai_part_to_a2a_part,
480494
) -> List[A2AEvent]:
481495
"""Converts a GenAI event to a list of A2A events.
482496
@@ -485,6 +499,7 @@ def convert_event_to_a2a_events(
485499
invocation_context: The invocation context.
486500
task_id: Optional task ID to use for generated events.
487501
context_id: Optional Context ID to use for generated events.
502+
part_converter: The function to convert GenAI part to A2A part.
488503
489504
Returns:
490505
A list of A2A events representing the converted ADK event.
@@ -509,7 +524,9 @@ def convert_event_to_a2a_events(
509524
a2a_events.append(error_event)
510525

511526
# Handle regular message content
512-
message = convert_event_to_a2a_message(event, invocation_context)
527+
message = convert_event_to_a2a_message(
528+
event, invocation_context, part_converter=part_converter
529+
)
513530
if message:
514531
running_event = _create_status_update_event(
515532
message, invocation_context, event, task_id, context_id

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from __future__ import annotations
2020

2121
import base64
22+
from collections.abc import Callable
2223
import json
2324
import logging
2425
from typing import Optional
@@ -51,6 +52,14 @@
5152
A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE = 'executable_code'
5253

5354

55+
A2APartToGenAIPartConverter = Callable[
56+
[a2a_types.Part], Optional[genai_types.Part]
57+
]
58+
GenAIPartToA2APartConverter = Callable[
59+
[genai_types.Part], Optional[a2a_types.Part]
60+
]
61+
62+
5463
@a2a_experimental
5564
def convert_a2a_part_to_genai_part(
5665
a2a_part: a2a_types.Part,

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
from ...runners import RunConfig
3333
from ..experimental import a2a_experimental
34+
from .part_converter import A2APartToGenAIPartConverter
3435
from .part_converter import convert_a2a_part_to_genai_part
3536

3637

@@ -50,6 +51,7 @@ def _get_user_id(request: RequestContext) -> str:
5051
@a2a_experimental
5152
def convert_a2a_request_to_adk_run_args(
5253
request: RequestContext,
54+
part_converter: A2APartToGenAIPartConverter = convert_a2a_part_to_genai_part,
5355
) -> dict[str, Any]:
5456

5557
if not request.message:
@@ -60,10 +62,7 @@ def convert_a2a_request_to_adk_run_args(
6062
'session_id': request.context_id,
6163
'new_message': genai_types.Content(
6264
role='user',
63-
parts=[
64-
convert_a2a_part_to_genai_part(part)
65-
for part in request.message.parts
66-
],
65+
parts=[part_converter(part) for part in request.message.parts],
6766
),
6867
'run_config': RunConfig(),
6968
}

src/google/adk/a2a/executor/a2a_agent_executor.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@
5353
from typing_extensions import override
5454

5555
from ..converters.event_converter import convert_event_to_a2a_events
56+
from ..converters.part_converter import A2APartToGenAIPartConverter
57+
from ..converters.part_converter import convert_a2a_part_to_genai_part
58+
from ..converters.part_converter import convert_genai_part_to_a2a_part
59+
from ..converters.part_converter import GenAIPartToA2APartConverter
5660
from ..converters.request_converter import convert_a2a_request_to_adk_run_args
5761
from ..converters.utils import _get_adk_metadata_key
5862
from ..experimental import a2a_experimental
@@ -65,12 +69,18 @@
6569
class A2aAgentExecutorConfig(BaseModel):
6670
"""Configuration for the A2aAgentExecutor."""
6771

68-
pass
72+
a2a_part_converter: A2APartToGenAIPartConverter = (
73+
convert_a2a_part_to_genai_part
74+
)
75+
gen_ai_part_converter: GenAIPartToA2APartConverter = (
76+
convert_genai_part_to_a2a_part
77+
)
6978

7079

7180
@a2a_experimental
7281
class A2aAgentExecutor(AgentExecutor):
7382
"""An AgentExecutor that runs an ADK Agent against an A2A request and
83+
7484
publishes updates to an event queue.
7585
"""
7686

@@ -82,7 +92,7 @@ def __init__(
8292
):
8393
super().__init__()
8494
self._runner = runner
85-
self._config = config
95+
self._config = config or A2aAgentExecutorConfig()
8696

8797
async def _resolve_runner(self) -> Runner:
8898
"""Resolve the runner, handling cases where it's a callable that returns a Runner."""
@@ -183,7 +193,9 @@ async def _handle_request(
183193
runner = await self._resolve_runner()
184194

185195
# Convert the a2a request to ADK run args
186-
run_args = convert_a2a_request_to_adk_run_args(context)
196+
run_args = convert_a2a_request_to_adk_run_args(
197+
context, self._config.a2a_part_converter
198+
)
187199

188200
# ensure the session exists
189201
session = await self._prepare_session(context, run_args, runner)
@@ -217,7 +229,11 @@ async def _handle_request(
217229
async with Aclosing(runner.run_async(**run_args)) as agen:
218230
async for adk_event in agen:
219231
for a2a_event in convert_event_to_a2a_events(
220-
adk_event, invocation_context, context.task_id, context.context_id
232+
adk_event,
233+
invocation_context,
234+
context.task_id,
235+
context.context_id,
236+
self._config.gen_ai_part_converter,
221237
):
222238
task_result_aggregator.process_event(a2a_event)
223239
await event_queue.enqueue_event(a2a_event)

src/google/adk/agents/remote_a2a_agent.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@
5757
from ..a2a.converters.event_converter import convert_a2a_message_to_event
5858
from ..a2a.converters.event_converter import convert_a2a_task_to_event
5959
from ..a2a.converters.event_converter import convert_event_to_a2a_message
60+
from ..a2a.converters.part_converter import A2APartToGenAIPartConverter
61+
from ..a2a.converters.part_converter import convert_a2a_part_to_genai_part
6062
from ..a2a.converters.part_converter import convert_genai_part_to_a2a_part
63+
from ..a2a.converters.part_converter import GenAIPartToA2APartConverter
6164
from ..a2a.experimental import a2a_experimental
6265
from ..a2a.logs.log_utils import build_a2a_request_log
6366
from ..a2a.logs.log_utils import build_a2a_response_log
@@ -120,6 +123,8 @@ def __init__(
120123
description: str = "",
121124
httpx_client: Optional[httpx.AsyncClient] = None,
122125
timeout: float = DEFAULT_TIMEOUT,
126+
genai_part_converter: GenAIPartToA2APartConverter = convert_genai_part_to_a2a_part,
127+
a2a_part_converter: A2APartToGenAIPartConverter = convert_a2a_part_to_genai_part,
123128
**kwargs: Any,
124129
) -> None:
125130
"""Initialize RemoteA2aAgent.
@@ -149,6 +154,8 @@ def __init__(
149154
self._httpx_client_needs_cleanup = httpx_client is None
150155
self._timeout = timeout
151156
self._is_resolved = False
157+
self._genai_part_converter = genai_part_converter
158+
self._a2a_part_converter = a2a_part_converter
152159

153160
# Validate and store agent card reference
154161
if isinstance(agent_card, AgentCard):
@@ -298,7 +305,7 @@ def _create_a2a_request_for_user_function_response(
298305
return None
299306

300307
a2a_message = convert_event_to_a2a_message(
301-
ctx.session.events[-1], ctx, Role.user
308+
ctx.session.events[-1], ctx, Role.user, self._genai_part_converter
302309
)
303310
if function_call_event.custom_metadata:
304311
a2a_message.task_id = (
@@ -355,7 +362,7 @@ def _construct_message_parts_from_session(
355362

356363
for part in event.content.parts:
357364

358-
converted_part = convert_genai_part_to_a2a_part(part)
365+
converted_part = self._genai_part_converter(part)
359366
if converted_part:
360367
message_parts.append(converted_part)
361368
else:
@@ -380,7 +387,10 @@ async def _handle_a2a_response(
380387
if a2a_response.root.result:
381388
if isinstance(a2a_response.root.result, A2ATask):
382389
event = convert_a2a_task_to_event(
383-
a2a_response.root.result, self.name, ctx
390+
a2a_response.root.result,
391+
self.name,
392+
ctx,
393+
self._a2a_part_converter,
384394
)
385395
event.custom_metadata = event.custom_metadata or {}
386396
event.custom_metadata[A2A_METADATA_PREFIX + "task_id"] = (
@@ -389,7 +399,10 @@ async def _handle_a2a_response(
389399

390400
else:
391401
event = convert_a2a_message_to_event(
392-
a2a_response.root.result, self.name, ctx
402+
a2a_response.root.result,
403+
self.name,
404+
ctx,
405+
self._a2a_part_converter,
393406
)
394407
event.custom_metadata = event.custom_metadata or {}
395408
if a2a_response.root.result.task_id:

0 commit comments

Comments
 (0)