Skip to content

Commit 3127f36

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: add metadata extraction symmetry to A2A converters
Propagates citations, grounding metadata, usage metadata, custom metadata, and error codes in all A2A-to-ADK converters (Task, Message, StatusUpdate) to match the existing support in ArtifactUpdate converter and ADK-to-A2A serialization. PiperOrigin-RevId: 954615406
1 parent 096ecfc commit 3127f36

2 files changed

Lines changed: 130 additions & 17 deletions

File tree

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

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,31 @@ def _create_mock_function_call_for_required_user_input(
408408
return output_parts, long_running_function_ids
409409

410410

411+
def _extract_all_metadata_fields(metadata: Any) -> dict[str, Any]:
412+
"""Extracts all GenAI metadata fields from A2A metadata."""
413+
metadata_dict = _compat.meta_to_dict(metadata)
414+
if not metadata_dict:
415+
return {}
416+
fields = {
417+
"grounding_metadata": _extract_genai_metadata(
418+
metadata_dict, "grounding_metadata", genai_types.GroundingMetadata
419+
),
420+
"custom_metadata": _extract_genai_metadata(
421+
metadata_dict, "custom_metadata", None
422+
),
423+
"usage_metadata": _extract_genai_metadata(
424+
metadata_dict,
425+
"usage_metadata",
426+
genai_types.GenerateContentResponseUsageMetadata,
427+
),
428+
"error_code": _extract_genai_metadata(metadata_dict, "error_code", None),
429+
"citation_metadata": _extract_genai_metadata(
430+
metadata_dict, "citation_metadata", genai_types.CitationMetadata
431+
),
432+
}
433+
return {k: v for k, v in fields.items() if v is not None}
434+
435+
411436
@a2a_experimental
412437
def convert_a2a_task_to_event(
413438
a2a_task: Task,
@@ -438,6 +463,8 @@ def convert_a2a_task_to_event(
438463
event_actions = EventActions()
439464
output_parts = []
440465
long_running_function_ids = set()
466+
metadata_fields: dict[str, Any] = {}
467+
status_message = _compat.normalize_message(a2a_task.status.message)
441468
if a2a_task.artifacts:
442469
artifact_parts = [
443470
part for artifact in a2a_task.artifacts for part in artifact.parts
@@ -446,10 +473,11 @@ def convert_a2a_task_to_event(
446473
event_actions = _merge_event_actions(
447474
event_actions, _extract_event_actions(artifact.metadata)
448475
)
476+
if not metadata_fields:
477+
metadata_fields = _extract_all_metadata_fields(artifact.metadata)
449478
output_parts, _ = _convert_a2a_parts_to_adk_parts(
450479
artifact_parts, part_converter
451480
)
452-
status_message = _compat.normalize_message(a2a_task.status.message)
453481
if status_message and (
454482
a2a_task.status.state == _compat.TS_INPUT_REQUIRED
455483
or a2a_task.status.state == _compat.TS_AUTH_REQUIRED
@@ -458,11 +486,15 @@ def convert_a2a_task_to_event(
458486
event_actions,
459487
_extract_event_actions(status_message.metadata),
460488
)
489+
if not metadata_fields:
490+
metadata_fields = _extract_all_metadata_fields(status_message.metadata)
461491
parts, ids = _convert_a2a_parts_to_adk_parts(
462492
status_message.parts, part_converter
463493
)
464494
output_parts.extend(parts)
465495
long_running_function_ids.update(ids)
496+
elif status_message and not metadata_fields:
497+
metadata_fields = _extract_all_metadata_fields(status_message.metadata)
466498

467499
output_parts, long_running_function_ids = (
468500
_create_mock_function_call_for_required_user_input(
@@ -476,6 +508,7 @@ def convert_a2a_task_to_event(
476508
author,
477509
event_actions,
478510
long_running_function_ids,
511+
**metadata_fields,
479512
)
480513

481514
except Exception as e:
@@ -515,12 +548,14 @@ def convert_a2a_message_to_event(
515548
a2a_message.parts, part_converter
516549
)
517550
content_role = _a2a_role_to_content_role(getattr(a2a_message, "role", None))
551+
metadata_fields = _extract_all_metadata_fields(a2a_message.metadata)
518552
return _create_event(
519553
output_parts,
520554
invocation_context,
521555
author,
522556
_extract_event_actions(a2a_message.metadata),
523557
content_role=content_role,
558+
**metadata_fields,
524559
)
525560

526561
except Exception as e:
@@ -553,9 +588,11 @@ def convert_a2a_status_update_to_event(
553588
output_parts = []
554589
long_running_function_ids = set()
555590
event_actions = EventActions()
591+
metadata_fields = {}
556592
status_message = _compat.normalize_message(a2a_status_update.status.message)
557593
if status_message:
558594
event_actions = _extract_event_actions(status_message.metadata)
595+
metadata_fields = _extract_all_metadata_fields(status_message.metadata)
559596
parts, ids = _convert_a2a_parts_to_adk_parts(
560597
status_message.parts, part_converter
561598
)
@@ -576,6 +613,7 @@ def convert_a2a_status_update_to_event(
576613
author,
577614
event_actions,
578615
long_running_function_ids,
616+
**metadata_fields,
579617
)
580618
except Exception as e:
581619
logger.error("Failed to convert A2A status update to event: %s", e)
@@ -608,28 +646,16 @@ def convert_a2a_artifact_update_to_event(
608646
output_parts, _ = _convert_a2a_parts_to_adk_parts(
609647
a2a_artifact_update.artifact.parts, part_converter
610648
)
611-
metadata_dict = _compat.meta_to_dict(a2a_artifact_update.artifact.metadata)
649+
metadata_fields = _extract_all_metadata_fields(
650+
a2a_artifact_update.artifact.metadata
651+
)
612652
return _create_event(
613653
output_parts,
614654
invocation_context,
615655
author,
616656
_extract_event_actions(a2a_artifact_update.artifact.metadata),
617657
partial=not a2a_artifact_update.last_chunk,
618-
grounding_metadata=_extract_genai_metadata(
619-
metadata_dict, "grounding_metadata", genai_types.GroundingMetadata
620-
),
621-
custom_metadata=_extract_genai_metadata(
622-
metadata_dict, "custom_metadata", None
623-
),
624-
usage_metadata=_extract_genai_metadata(
625-
metadata_dict,
626-
"usage_metadata",
627-
genai_types.GenerateContentResponseUsageMetadata,
628-
),
629-
error_code=_extract_genai_metadata(metadata_dict, "error_code", None),
630-
citation_metadata=_extract_genai_metadata(
631-
metadata_dict, "citation_metadata", genai_types.CitationMetadata
632-
),
658+
**metadata_fields,
633659
)
634660
except Exception as e:
635661
logger.error("Failed to convert A2A artifact update to event: %s", e)

tests/unittests/a2a/converters/test_to_adk.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from a2a.types import Part as A2APart
2222
from a2a.types import Task
2323
from a2a.types import TaskArtifactUpdateEvent
24+
from a2a.types import TaskStatusUpdateEvent
2425
from google.adk.a2a import _compat
2526
from google.adk.a2a.converters.from_adk_event import convert_event_to_a2a_events
2627
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_END_TAG
@@ -37,6 +38,7 @@
3738
from google.adk.a2a.converters.utils import _get_adk_metadata_key
3839
from google.adk.agents.invocation_context import InvocationContext
3940
from google.adk.events import Event
41+
from google.adk.events.event_actions import EventActions
4042
from google.genai import types as genai_types
4143
import pytest
4244

@@ -754,3 +756,88 @@ def test_extract_genai_metadata_not_dict_but_class_provided(self) -> None:
754756
genai_types.GenerateContentResponseUsageMetadata,
755757
)
756758
assert result is None
759+
760+
def test_grounding_metadata_round_trip_task(self) -> None:
761+
"""Tests that grounding metadata can be successfully extracted from a Task."""
762+
event = Event(
763+
author="agent",
764+
grounding_metadata=genai_types.GroundingMetadata(
765+
search_entry_point=genai_types.SearchEntryPoint(
766+
rendered_content="test-task"
767+
)
768+
),
769+
content=genai_types.Content(
770+
role="model", parts=[genai_types.Part(text="hi")]
771+
),
772+
)
773+
a2a_events = convert_event_to_a2a_events(
774+
event, {}, task_id="t", context_id="c"
775+
)
776+
artifact_update = next(
777+
e for e in a2a_events if isinstance(e, TaskArtifactUpdateEvent)
778+
)
779+
# Construct a Task from the artifact update
780+
task = Task(
781+
id="t",
782+
context_id="c",
783+
artifacts=[artifact_update.artifact],
784+
status=_compat.make_task_status(_compat.TS_COMPLETED),
785+
)
786+
back = convert_a2a_task_to_event(task, "agent")
787+
assert back is not None
788+
assert back.grounding_metadata is not None
789+
assert (
790+
back.grounding_metadata.search_entry_point.rendered_content
791+
== "test-task"
792+
)
793+
794+
def test_grounding_metadata_round_trip_status_update(self) -> None:
795+
"""Tests that grounding metadata can be successfully extracted from a status update."""
796+
event = Event(
797+
author="agent",
798+
actions=EventActions(state_delta={"key": "val"}),
799+
grounding_metadata=genai_types.GroundingMetadata(
800+
search_entry_point=genai_types.SearchEntryPoint(
801+
rendered_content="test-status"
802+
)
803+
),
804+
)
805+
a2a_events = convert_event_to_a2a_events(
806+
event, {}, task_id="t", context_id="c"
807+
)
808+
status_update = next(
809+
e for e in a2a_events if isinstance(e, TaskStatusUpdateEvent)
810+
)
811+
back = convert_a2a_status_update_to_event(status_update, "agent")
812+
assert back is not None
813+
assert back.grounding_metadata is not None
814+
assert (
815+
back.grounding_metadata.search_entry_point.rendered_content
816+
== "test-status"
817+
)
818+
819+
def test_grounding_metadata_round_trip_message(self) -> None:
820+
"""Tests that grounding metadata can be successfully extracted from a Message."""
821+
event = Event(
822+
author="agent",
823+
actions=EventActions(state_delta={"key": "val"}),
824+
grounding_metadata=genai_types.GroundingMetadata(
825+
search_entry_point=genai_types.SearchEntryPoint(
826+
rendered_content="test-message"
827+
)
828+
),
829+
)
830+
a2a_events = convert_event_to_a2a_events(
831+
event, {}, task_id="t", context_id="c"
832+
)
833+
status_update = next(
834+
e for e in a2a_events if isinstance(e, TaskStatusUpdateEvent)
835+
)
836+
message = status_update.status.message
837+
back = convert_a2a_message_to_event(message, "agent")
838+
assert back is not None
839+
assert back.grounding_metadata is not None
840+
assert (
841+
back.grounding_metadata.search_entry_point.rendered_content
842+
== "test-message"
843+
)

0 commit comments

Comments
 (0)