Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
22b42d9
fix: metadata serialization for custom objects and large integers
youngchannelforyou Aug 21, 2025
49824b3
fix: Replace magic number 15 with constant _MAX_SAFE_INTEGER_DIGITS
youngchannelforyou Aug 21, 2025
b551530
refactor: Simplify metadata serialization by removing redundant JSON …
youngchannelforyou Aug 21, 2025
33318f5
fix: Handle JSON-unserializable objects in metadata conversion
youngchannelforyou Aug 21, 2025
6b3f04f
test: Add numeric string roundtrip test and fix mypy type error
youngchannelforyou Aug 21, 2025
e560f95
fix: remove comment
youngchannelforyou Aug 21, 2025
995d554
Merge branch 'main' into fix-proto-utils-metadata-serialization
holtskinner Aug 21, 2025
eafdd42
Merge branch 'main' into fix-proto-utils-metadata-serialization
youngchannelforyou Aug 23, 2025
52a8fe4
refactor: simplify proto_utils metadata conversion and add utility fu…
youngchannelforyou Aug 25, 2025
6df6f6f
Merge branch 'main' into fix-proto-utils-metadata-serialization
youngchannelforyou Aug 26, 2025
37eb3d0
Merge branch 'main' into fix-proto-utils-metadata-serialization
youngchannelforyou Aug 28, 2025
671aee0
Merge branch 'main' into fix-proto-utils-metadata-serialization
holtskinner Sep 3, 2025
ff153f4
Move standalone utility functions to top of file
holtskinner Sep 3, 2025
ea0d978
Ignore Deprecated lint rule UP38
holtskinner Sep 3, 2025
1c58507
Update src/a2a/utils/proto_utils.py
youngchannelforyou Sep 3, 2025
6c5a27d
Update src/a2a/utils/proto_utils.py
youngchannelforyou Sep 3, 2025
8c249a7
Update src/a2a/utils/proto_utils.py
youngchannelforyou Sep 3, 2025
e61374f
Merge branch 'fix-proto-utils-metadata-serialization' of https://gith…
holtskinner Sep 3, 2025
ef3b263
Merge branch 'main' into fix-proto-utils-metadata-serialization
youngchannelforyou Sep 4, 2025
a258007
Merge branch 'main' into fix-proto-utils-metadata-serialization
holtskinner Sep 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 67 additions & 5 deletions src/a2a/utils/proto_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,53 @@
if metadata is None:
return None
return struct_pb2.Struct(
# TODO: Add support for other types.
fields={
key: struct_pb2.Value(string_value=value)
key: cls._convert_value_to_proto(value)
for key, value in metadata.items()
if isinstance(value, str)
}
)

@classmethod
def _convert_value_to_proto(cls, value: Any) -> struct_pb2.Value:
"""Convert Python value to protobuf Value."""
if value is None:
proto_value = struct_pb2.Value()
proto_value.null_value = 0

Check failure on line 61 in src/a2a/utils/proto_utils.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Cannot assign to attribute "null_value" for class "Value"   "Literal[0]" is not assignable to "ValueType" (reportAttributeAccessIssue)
return proto_value
if isinstance(value, bool):
return struct_pb2.Value(bool_value=value)
if isinstance(value, int):
if abs(value) > (2**53 - 1):
return struct_pb2.Value(string_value=str(value))
return struct_pb2.Value(number_value=float(value))
if isinstance(value, float):
return struct_pb2.Value(number_value=value)
if isinstance(value, str):
return struct_pb2.Value(string_value=value)
if isinstance(value, dict):
serializable_dict = cls._make_json_serializable(value)
json_data = json.dumps(serializable_dict, ensure_ascii=False)
struct_value = struct_pb2.Struct()
json_format.Parse(json_data, struct_value)
return struct_pb2.Value(struct_value=struct_value)
if isinstance(value, list | tuple):
list_value = struct_pb2.ListValue()
for item in value:
converted_item = cls._convert_value_to_proto(item)
list_value.values.append(converted_item)
return struct_pb2.Value(list_value=list_value)
return struct_pb2.Value(string_value=str(value))

@classmethod
def _make_json_serializable(cls, value: Any) -> Any:
if value is None or isinstance(value, str | int | float | bool):
return value
if isinstance(value, dict):
return {k: cls._make_json_serializable(v) for k, v in value.items()}
if isinstance(value, list | tuple):
return [cls._make_json_serializable(item) for item in value]
return str(value)

@classmethod
def part(cls, part: types.Part) -> a2a_pb2.Part:
if isinstance(part.root, types.TextPart):
Expand Down Expand Up @@ -478,11 +517,34 @@
@classmethod
def metadata(cls, metadata: struct_pb2.Struct) -> dict[str, Any]:
return {
key: value.string_value
key: cls._convert_proto_to_value(value)
for key, value in metadata.fields.items()
if value.string_value
}

@classmethod
def _convert_proto_to_value(cls, value: struct_pb2.Value) -> Any:
if value.HasField('null_value'):
return None
if value.HasField('bool_value'):
return value.bool_value
if value.HasField('number_value'):
return value.number_value
if value.HasField('string_value'):
string_val = value.string_value
if string_val.lstrip('-').isdigit():
return int(string_val)
return string_val
if value.HasField('struct_value'):
return {
k: cls._convert_proto_to_value(v)
for k, v in value.struct_value.fields.items()
}
if value.HasField('list_value'):
return [
cls._convert_proto_to_value(v) for v in value.list_value.values
]
return None

@classmethod
def part(cls, part: a2a_pb2.Part) -> types.Part:
if part.HasField('text'):
Expand Down
103 changes: 103 additions & 0 deletions tests/utils/test_proto_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,106 @@ def test_none_handling(self):
assert proto_utils.ToProto.provider(None) is None
assert proto_utils.ToProto.security(None) is None
assert proto_utils.ToProto.security_schemes(None) is None

def test_metadata_conversion(self):
"""Test metadata conversion with various data types."""
metadata = {
'null_value': None,
'bool_value': True,
'int_value': 42,
'float_value': 3.14,
'string_value': 'hello',
'dict_value': {'nested': 'dict', 'count': 10},
'list_value': [1, 'two', 3.0, True, None],
'tuple_value': (1, 2, 3),
'complex_list': [
{'name': 'item1', 'values': [1, 2, 3]},
{'name': 'item2', 'values': [4, 5, 6]},
],
}

# Convert to proto
proto_metadata = proto_utils.ToProto.metadata(metadata)
assert proto_metadata is not None

# Convert back to Python
roundtrip_metadata = proto_utils.FromProto.metadata(proto_metadata)

# Verify all values are preserved correctly
assert roundtrip_metadata['null_value'] is None
assert roundtrip_metadata['bool_value'] is True
assert roundtrip_metadata['int_value'] == 42
assert roundtrip_metadata['float_value'] == 3.14
assert roundtrip_metadata['string_value'] == 'hello'
assert roundtrip_metadata['dict_value']['nested'] == 'dict'
assert roundtrip_metadata['dict_value']['count'] == 10
assert roundtrip_metadata['list_value'] == [1, 'two', 3.0, True, None]
assert roundtrip_metadata['tuple_value'] == [
1,
2,
3,
] # tuples become lists
assert len(roundtrip_metadata['complex_list']) == 2
assert roundtrip_metadata['complex_list'][0]['name'] == 'item1'

def test_metadata_with_custom_objects(self):
"""Test metadata conversion with custom objects that need str() fallback."""

class CustomObject:
def __str__(self):
return 'custom_object_str'

def __repr__(self):
return 'CustomObject()'

metadata = {
'custom_obj': CustomObject(),
'list_with_custom': [1, CustomObject(), 'text'],
'nested_custom': {'obj': CustomObject(), 'normal': 'value'},
}

# Convert to proto
proto_metadata = proto_utils.ToProto.metadata(metadata)
assert proto_metadata is not None

# Convert back to Python
roundtrip_metadata = proto_utils.FromProto.metadata(proto_metadata)

# Custom objects should be converted to strings
assert roundtrip_metadata['custom_obj'] == 'custom_object_str'
assert roundtrip_metadata['list_with_custom'] == [
1,
'custom_object_str',
'text',
]
assert roundtrip_metadata['nested_custom']['obj'] == 'custom_object_str'
assert roundtrip_metadata['nested_custom']['normal'] == 'value'

def test_metadata_edge_cases(self):
"""Test metadata conversion with edge cases."""
metadata = {
'empty_dict': {},
'empty_list': [],
'zero': 0,
'false': False,
'empty_string': '',
'unicode_string': 'string test',
'large_number': 9999999999999999,
'negative_number': -42,
'float_precision': 0.123456789,
}

# Convert to proto and back
proto_metadata = proto_utils.ToProto.metadata(metadata)
roundtrip_metadata = proto_utils.FromProto.metadata(proto_metadata)

# Verify edge cases are handled correctly
assert roundtrip_metadata['empty_dict'] == {}
assert roundtrip_metadata['empty_list'] == []
assert roundtrip_metadata['zero'] == 0
assert roundtrip_metadata['false'] is False
assert roundtrip_metadata['empty_string'] == ''
assert roundtrip_metadata['unicode_string'] == 'string test'
assert roundtrip_metadata['large_number'] == 9999999999999999
assert roundtrip_metadata['negative_number'] == -42
assert abs(roundtrip_metadata['float_precision'] - 0.123456789) < 1e-10
Loading