Skip to content

Commit 2316b83

Browse files
google-genai-botcopybara-github
authored andcommitted
fix: preserve metadata in A2A artifact updates
The A2A converter pipeline exhibited an architectural asymmetry regarding how event-level metadata is processed over the network. While the outbound converter (`from_adk_event.py`) correctly packs auxiliary metadata fields (`grounding_metadata`, `custom_metadata`, `usage_metadata`, and `error_code`) into the A2A artifact metadata layer, the inbound converter (`to_adk_event.py`) only reconstructed event actions natively, silently discarding the rest. This change resolves the asymmetry by properly extracting these remaining metadata fields during `convert_a2a_artifact_update_to_event()`. A new resilient extractor (`_extract_genai_metadata`) is introduced to parse and strictly validate these structures (such as `GroundingMetadata` and `GenerateContentResponseUsageMetadata`) natively into their corresponding `google.genai` Pydantic models. We ensure backward compatibility and protect against malformed payloads by relying on graceful fallback defaults (`None`) when encountering incompatible raw payloads. This ensures complete topological parity for A2A metadata serialization without impacting downstream ADK components unequipped to handle new fields. PiperOrigin-RevId: 952055514
1 parent 79ba5af commit 2316b83

2 files changed

Lines changed: 122 additions & 0 deletions

File tree

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ def _create_event(
191191
long_running_function_ids: Optional[set[str]] = None,
192192
partial: bool = False,
193193
content_role: str = "model",
194+
grounding_metadata: Any = None,
195+
custom_metadata: Any = None,
196+
usage_metadata: Any = None,
197+
error_code: Any = None,
198+
citation_metadata: Any = None,
194199
) -> Optional[Event]:
195200
"""Creates an ADK event from parts and metadata."""
196201
event_actions = actions or EventActions()
@@ -220,6 +225,11 @@ def _create_event(
220225
else None
221226
),
222227
partial=partial,
228+
grounding_metadata=grounding_metadata,
229+
custom_metadata=custom_metadata,
230+
usage_metadata=usage_metadata,
231+
error_code=error_code,
232+
citation_metadata=citation_metadata,
223233
)
224234

225235
return event
@@ -241,6 +251,28 @@ def _parse_adk_metadata_value(value: Any) -> Any:
241251
return value
242252

243253

254+
def _extract_genai_metadata(
255+
metadata_dict: dict[str, Any], key: str, model_class: Any
256+
) -> Any:
257+
raw = metadata_dict.get(_get_adk_metadata_key(key))
258+
if raw is None:
259+
return None
260+
parsed = _parse_adk_metadata_value(raw)
261+
if not isinstance(parsed, dict) and model_class:
262+
return None
263+
if not model_class:
264+
return parsed
265+
try:
266+
return model_class.model_validate(parsed)
267+
except ValidationError as error:
268+
logger.warning(
269+
"Ignoring invalid ADK %s metadata: %d validation errors",
270+
key,
271+
error.error_count(),
272+
)
273+
return None
274+
275+
244276
def _extract_event_actions(metadata: Any) -> EventActions:
245277
"""Extracts ADK event actions from A2A metadata.
246278
@@ -576,12 +608,28 @@ def convert_a2a_artifact_update_to_event(
576608
output_parts, _ = _convert_a2a_parts_to_adk_parts(
577609
a2a_artifact_update.artifact.parts, part_converter
578610
)
611+
metadata_dict = _compat.meta_to_dict(a2a_artifact_update.artifact.metadata)
579612
return _create_event(
580613
output_parts,
581614
invocation_context,
582615
author,
583616
_extract_event_actions(a2a_artifact_update.artifact.metadata),
584617
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+
),
585633
)
586634
except Exception as e:
587635
logger.error("Failed to convert A2A artifact update to event: %s", e)

tests/unittests/a2a/converters/test_to_adk.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
from a2a.types import Task
2323
from a2a.types import TaskArtifactUpdateEvent
2424
from google.adk.a2a import _compat
25+
from google.adk.a2a.converters.from_adk_event import convert_event_to_a2a_events
2526
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_END_TAG
2627
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY
2728
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_START_TAG
2829
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_TEXT_MIME_TYPE
30+
from google.adk.a2a.converters.to_adk_event import _extract_genai_metadata
2931
from google.adk.a2a.converters.to_adk_event import convert_a2a_artifact_update_to_event
3032
from google.adk.a2a.converters.to_adk_event import convert_a2a_message_to_event
3133
from google.adk.a2a.converters.to_adk_event import convert_a2a_status_update_to_event
@@ -34,6 +36,7 @@
3436
from google.adk.a2a.converters.to_adk_event import MOCK_FUNCTION_CALL_FOR_REQUIRED_USER_INPUT
3537
from google.adk.a2a.converters.utils import _get_adk_metadata_key
3638
from google.adk.agents.invocation_context import InvocationContext
39+
from google.adk.events import Event
3740
from google.genai import types as genai_types
3841
import pytest
3942

@@ -680,3 +683,74 @@ def test_convert_a2a_message_to_event_agent_role(self) -> None:
680683
)
681684

682685
assert event.content.role == "model"
686+
687+
688+
class TestExtractGenaiMetadata:
689+
690+
def test_grounding_metadata_round_trip(self) -> None:
691+
"""Tests that grounding metadata can be successfully extracted."""
692+
event = Event(
693+
author="agent",
694+
grounding_metadata=genai_types.GroundingMetadata(
695+
search_entry_point=genai_types.SearchEntryPoint(
696+
rendered_content="test"
697+
)
698+
),
699+
content=genai_types.Content(
700+
role="model", parts=[genai_types.Part(text="hi")]
701+
),
702+
)
703+
a2a_events = convert_event_to_a2a_events(
704+
event, {}, task_id="t", context_id="c"
705+
)
706+
artifact_update = next(
707+
e for e in a2a_events if isinstance(e, TaskArtifactUpdateEvent)
708+
)
709+
back = convert_a2a_artifact_update_to_event(artifact_update, "agent")
710+
assert back is not None
711+
assert back.grounding_metadata is not None
712+
assert back.grounding_metadata.search_entry_point.rendered_content == "test"
713+
714+
def test_extract_genai_metadata_valid(self) -> None:
715+
metadata_dict = {
716+
_get_adk_metadata_key(
717+
"grounding_metadata"
718+
): '{"search_entry_point": {"rendered_content": "test"}}'
719+
}
720+
result = _extract_genai_metadata(
721+
metadata_dict, "grounding_metadata", genai_types.GroundingMetadata
722+
)
723+
assert isinstance(result, genai_types.GroundingMetadata)
724+
assert result.search_entry_point.rendered_content == "test"
725+
726+
def test_extract_genai_metadata_invalid_validation_error(self) -> None:
727+
# A malformed dictionary that causes a ValidationError (e.g. wrong type for search_entry_point)
728+
metadata_dict = {
729+
_get_adk_metadata_key(
730+
"grounding_metadata"
731+
): '{"search_entry_point": ["not_a_dict"]}'
732+
}
733+
result = _extract_genai_metadata(
734+
metadata_dict, "grounding_metadata", genai_types.GroundingMetadata
735+
)
736+
assert result is None
737+
738+
def test_extract_genai_metadata_missing(self) -> None:
739+
result = _extract_genai_metadata(
740+
{"other_key": "val"},
741+
"grounding_metadata",
742+
genai_types.GroundingMetadata,
743+
)
744+
assert result is None
745+
746+
def test_extract_genai_metadata_not_dict_but_class_provided(self) -> None:
747+
# Should safely return None when JSON parsed to non-dict, but model validates expected dict/kwargs
748+
metadata_dict = {
749+
_get_adk_metadata_key("usage_metadata"): '["not", "a", "dict"]'
750+
}
751+
result = _extract_genai_metadata(
752+
metadata_dict,
753+
"usage_metadata",
754+
genai_types.GenerateContentResponseUsageMetadata,
755+
)
756+
assert result is None

0 commit comments

Comments
 (0)