From f7de2ae0ebba31b6de6af2df2ff816a314691edd Mon Sep 17 00:00:00 2001 From: Phil Stephens Date: Fri, 22 Aug 2025 04:50:38 +0000 Subject: [PATCH 01/12] Update proto conversion utilities --- src/a2a/utils/proto_utils.py | 89 ++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index e8f9e7181..8278aaf75 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -46,14 +46,7 @@ def metadata( ) -> struct_pb2.Struct | None: if metadata is None: return None - return struct_pb2.Struct( - # TODO: Add support for other types. - fields={ - key: struct_pb2.Value(string_value=value) - for key, value in metadata.items() - if isinstance(value, str) - } - ) + return dict_to_struct(metadata) @classmethod def part(cls, part: types.Part) -> a2a_pb2.Part: @@ -81,7 +74,9 @@ def file( ) -> a2a_pb2.FilePart: if isinstance(file, types.FileWithUri): return a2a_pb2.FilePart( - file_with_uri=file.uri, mime_type=file.mime_type, name=file.name + file_with_uri=file.uri, + mime_type=file.mime_type, + name=file.name ) return a2a_pb2.FilePart( file_with_bytes=file.bytes.encode('utf-8'), @@ -324,6 +319,23 @@ def capabilities( return a2a_pb2.AgentCapabilities( streaming=bool(capabilities.streaming), push_notifications=bool(capabilities.push_notifications), + extensions=[ + cls.extension(x) for x in capabilities.extensions + ] + if capabilities.extensions + else None, + ) + + @classmethod + def extension( + cls, + extension: types.AgentExtension, + ) -> a2a_pb2.AgentExtension: + return a2a_pb2.AgentExtension( + uri=extension.uri, + description=extension.description, + params=dict_to_struct(extension.params), + required=extension.required, ) @classmethod @@ -477,11 +489,9 @@ def message(cls, message: a2a_pb2.Message) -> types.Message: @classmethod def metadata(cls, metadata: struct_pb2.Struct) -> dict[str, Any]: - return { - key: value.string_value - for key, value in metadata.fields.items() - if value.string_value - } + if not metadata: + return {} + return struct_to_dict(metadata) @classmethod def part(cls, part: a2a_pb2.Part) -> types.Part: @@ -777,6 +787,23 @@ def capabilities( return types.AgentCapabilities( streaming=capabilities.streaming, push_notifications=capabilities.push_notifications, + extensions=[ + cls.agent_extension(x) for x in capabilities.extensions + ] + if capabilities.extensions + else None, + ) + + @classmethod + def agent_extension( + cls, + extension: a2a_pb2.AgentExtension, + ) -> types.AgentExtension: + return types.AgentExtension( + uri=extension.uri, + description=extension.description, + params=struct_to_dict(extension.params), + required=extension.required, ) @classmethod @@ -916,3 +943,37 @@ def role(cls, role: a2a_pb2.Role) -> types.Role: return types.Role.agent case _: return types.Role.agent + + +def struct_to_dict(struct: struct_pb2.Struct) -> dict[str, Any]: + """Converts a Struct proto to a Python dict.""" + + def convert(value: struct_pb2.Value) -> Any: + if value.HasField('list_value'): + return [convert(v) for v in value.list_value.values] + elif value.HasField('struct_value'): + return {k: convert(v) for k, v in value.struct_value.fields.items()} + elif value.HasField('number_value'): + return value.number_value + elif value.HasField('string_value'): + return value.string_value + elif value.HasField('bool_value'): + return value.bool_value + elif value.HasField('null_value'): + return None + else: + raise ValueError(f'Unsupported type: {value}') + + return {k: convert(v) for k, v in struct.fields.items()} + + +def dict_to_struct(dictionary: dict[str, Any]) -> struct_pb2.Struct: + """Converts a Python dict to a Struct proto.""" + struct = struct_pb2.Struct() + for key, val in dictionary.items(): + if isinstance(val, dict): + struct[key] = dict_to_struct(val) + else: + struct[key] = val + return struct + From 50b61cb94bdbf83ed7aaa9a3a8fb302c0bec7fa2 Mon Sep 17 00:00:00 2001 From: Phil Stephens Date: Fri, 22 Aug 2025 04:59:14 +0000 Subject: [PATCH 02/12] Update proto conversion utilities --- src/a2a/utils/proto_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index 8278aaf75..4b53bbd59 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -334,7 +334,7 @@ def extension( return a2a_pb2.AgentExtension( uri=extension.uri, description=extension.description, - params=dict_to_struct(extension.params), + params=dict_to_struct(extension.params) if extension.params else None, required=extension.required, ) From 5c99db36a906841c9b16c55b6af0fb1b18eacb63 Mon Sep 17 00:00:00 2001 From: Phil Stephens Date: Fri, 22 Aug 2025 05:05:35 +0000 Subject: [PATCH 03/12] Update proto conversion utilities --- src/a2a/utils/proto_utils.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index 4b53bbd59..03e187d3f 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -320,10 +320,8 @@ def capabilities( streaming=bool(capabilities.streaming), push_notifications=bool(capabilities.push_notifications), extensions=[ - cls.extension(x) for x in capabilities.extensions - ] - if capabilities.extensions - else None, + cls.extension(x) for x in capabilities.extensions or [] + ], ) @classmethod @@ -489,7 +487,7 @@ def message(cls, message: a2a_pb2.Message) -> types.Message: @classmethod def metadata(cls, metadata: struct_pb2.Struct) -> dict[str, Any]: - if not metadata: + if not metadata.fields: return {} return struct_to_dict(metadata) @@ -789,9 +787,7 @@ def capabilities( push_notifications=capabilities.push_notifications, extensions=[ cls.agent_extension(x) for x in capabilities.extensions - ] - if capabilities.extensions - else None, + ], ) @classmethod From f1115a0d57a265848964c18014e3f3de2672c611 Mon Sep 17 00:00:00 2001 From: Phil Stephens Date: Fri, 22 Aug 2025 05:07:47 +0000 Subject: [PATCH 04/12] Update proto conversion utilities --- src/a2a/utils/proto_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index 03e187d3f..f64cee559 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -489,7 +489,7 @@ def message(cls, message: a2a_pb2.Message) -> types.Message: def metadata(cls, metadata: struct_pb2.Struct) -> dict[str, Any]: if not metadata.fields: return {} - return struct_to_dict(metadata) + return struct_to_dict(metadata) @classmethod def part(cls, part: a2a_pb2.Part) -> types.Part: From 8bcc14160407dd9d02638f09fa1623aff821ec20 Mon Sep 17 00:00:00 2001 From: Phil Stephens Date: Fri, 22 Aug 2025 05:10:56 +0000 Subject: [PATCH 05/12] Update proto conversion utilities --- src/a2a/utils/proto_utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index f64cee559..df4221e8f 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -947,15 +947,15 @@ def struct_to_dict(struct: struct_pb2.Struct) -> dict[str, Any]: def convert(value: struct_pb2.Value) -> Any: if value.HasField('list_value'): return [convert(v) for v in value.list_value.values] - elif value.HasField('struct_value'): + if value.HasField('struct_value'): return {k: convert(v) for k, v in value.struct_value.fields.items()} - elif value.HasField('number_value'): + if value.HasField('number_value'): return value.number_value - elif value.HasField('string_value'): + if value.HasField('string_value'): return value.string_value - elif value.HasField('bool_value'): + if value.HasField('bool_value'): return value.bool_value - elif value.HasField('null_value'): + if value.HasField('null_value'): return None else: raise ValueError(f'Unsupported type: {value}') From 7e08be1c5bf051665d6c054bfd027264412ce1b3 Mon Sep 17 00:00:00 2001 From: Phil Stephens Date: Fri, 22 Aug 2025 05:15:39 +0000 Subject: [PATCH 06/12] Update proto conversion utilities --- src/a2a/utils/proto_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index df4221e8f..bbea0f690 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -957,8 +957,7 @@ def convert(value: struct_pb2.Value) -> Any: return value.bool_value if value.HasField('null_value'): return None - else: - raise ValueError(f'Unsupported type: {value}') + raise ValueError(f'Unsupported type: {value}') return {k: convert(v) for k, v in struct.fields.items()} From e91b31ca137a8932fec1a1d21da64258f4c71956 Mon Sep 17 00:00:00 2001 From: Phil Stephens Date: Fri, 22 Aug 2025 05:23:17 +0000 Subject: [PATCH 07/12] Update proto conversion utilities --- src/a2a/utils/proto_utils.py | 65 ++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index bbea0f690..c9ae88c34 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -74,9 +74,7 @@ def file( ) -> a2a_pb2.FilePart: if isinstance(file, types.FileWithUri): return a2a_pb2.FilePart( - file_with_uri=file.uri, - mime_type=file.mime_type, - name=file.name + file_with_uri=file.uri, mime_type=file.mime_type, name=file.name ) return a2a_pb2.FilePart( file_with_bytes=file.bytes.encode('utf-8'), @@ -332,7 +330,9 @@ def extension( return a2a_pb2.AgentExtension( uri=extension.uri, description=extension.description, - params=dict_to_struct(extension.params) if extension.params else None, + params=dict_to_struct(extension.params) + if extension.params + else None, required=extension.required, ) @@ -487,9 +487,9 @@ def message(cls, message: a2a_pb2.Message) -> types.Message: @classmethod def metadata(cls, metadata: struct_pb2.Struct) -> dict[str, Any]: - if not metadata.fields: + if not metadata.fields: return {} - return struct_to_dict(metadata) + return struct_to_dict(metadata) @classmethod def part(cls, part: a2a_pb2.Part) -> types.Part: @@ -942,33 +942,32 @@ def role(cls, role: a2a_pb2.Role) -> types.Role: def struct_to_dict(struct: struct_pb2.Struct) -> dict[str, Any]: - """Converts a Struct proto to a Python dict.""" - - def convert(value: struct_pb2.Value) -> Any: - if value.HasField('list_value'): - return [convert(v) for v in value.list_value.values] - if value.HasField('struct_value'): - return {k: convert(v) for k, v in value.struct_value.fields.items()} - if value.HasField('number_value'): - return value.number_value - if value.HasField('string_value'): - return value.string_value - if value.HasField('bool_value'): - return value.bool_value - if value.HasField('null_value'): - return None - raise ValueError(f'Unsupported type: {value}') - - return {k: convert(v) for k, v in struct.fields.items()} + """Converts a Struct proto to a Python dict.""" + + def convert(value: struct_pb2.Value) -> Any: + if value.HasField('list_value'): + return [convert(v) for v in value.list_value.values] + if value.HasField('struct_value'): + return {k: convert(v) for k, v in value.struct_value.fields.items()} + if value.HasField('number_value'): + return value.number_value + if value.HasField('string_value'): + return value.string_value + if value.HasField('bool_value'): + return value.bool_value + if value.HasField('null_value'): + return None + raise ValueError(f'Unsupported type: {value}') + return {k: convert(v) for k, v in struct.fields.items()} -def dict_to_struct(dictionary: dict[str, Any]) -> struct_pb2.Struct: - """Converts a Python dict to a Struct proto.""" - struct = struct_pb2.Struct() - for key, val in dictionary.items(): - if isinstance(val, dict): - struct[key] = dict_to_struct(val) - else: - struct[key] = val - return struct +def dict_to_struct(dictionary: dict[str, Any]) -> struct_pb2.Struct: + """Converts a Python dict to a Struct proto.""" + struct = struct_pb2.Struct() + for key, val in dictionary.items(): + if isinstance(val, dict): + struct[key] = dict_to_struct(val) + else: + struct[key] = val + return struct From f8e8262c134e72038d26355da06af0db49925895 Mon Sep 17 00:00:00 2001 From: Phil Stephens Date: Fri, 22 Aug 2025 17:51:10 +0000 Subject: [PATCH 08/12] Update proto conversion utilities --- src/a2a/utils/proto_utils.py | 34 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index c9ae88c34..98eeef36e 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -489,7 +489,7 @@ def message(cls, message: a2a_pb2.Message) -> types.Message: def metadata(cls, metadata: struct_pb2.Struct) -> dict[str, Any]: if not metadata.fields: return {} - return struct_to_dict(metadata) + return json_format.MessageToDict(metadata) @classmethod def part(cls, part: a2a_pb2.Part) -> types.Part: @@ -798,7 +798,7 @@ def agent_extension( return types.AgentExtension( uri=extension.uri, description=extension.description, - params=struct_to_dict(extension.params), + params=json_format.MessageToDict(extension.params), required=extension.required, ) @@ -941,29 +941,19 @@ def role(cls, role: a2a_pb2.Role) -> types.Role: return types.Role.agent -def struct_to_dict(struct: struct_pb2.Struct) -> dict[str, Any]: - """Converts a Struct proto to a Python dict.""" - - def convert(value: struct_pb2.Value) -> Any: - if value.HasField('list_value'): - return [convert(v) for v in value.list_value.values] - if value.HasField('struct_value'): - return {k: convert(v) for k, v in value.struct_value.fields.items()} - if value.HasField('number_value'): - return value.number_value - if value.HasField('string_value'): - return value.string_value - if value.HasField('bool_value'): - return value.bool_value - if value.HasField('null_value'): - return None - raise ValueError(f'Unsupported type: {value}') +def dict_to_struct(dictionary: dict[str, Any]) -> struct_pb2.Struct: + """Converts a Python dict to a Struct proto. - return {k: convert(v) for k, v in struct.fields.items()} + Unforunately, using the json_format.ParseDict does not work because this + wants the dictionary to be an exact match of the Struct proto with fields + and keys and values, not the traditional python dict struture. + Args: + dictionary: The Python dict to convert. -def dict_to_struct(dictionary: dict[str, Any]) -> struct_pb2.Struct: - """Converts a Python dict to a Struct proto.""" + Returns: + The Struct proto. + """ struct = struct_pb2.Struct() for key, val in dictionary.items(): if isinstance(val, dict): From a4a1a8ac83210f19e664d25c91a68748b31ee832 Mon Sep 17 00:00:00 2001 From: Phil Stephens Date: Fri, 22 Aug 2025 18:00:58 +0000 Subject: [PATCH 09/12] Remove enforcement of UUID for context id --- src/a2a/utils/task.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/a2a/utils/task.py b/src/a2a/utils/task.py index 602723670..bd5ee431f 100644 --- a/src/a2a/utils/task.py +++ b/src/a2a/utils/task.py @@ -29,16 +29,10 @@ def new_task(request: Message) -> Task: raise ValueError('TextPart content cannot be empty') context_id_str = request.context_id - if context_id_str is not None: - try: - uuid.UUID(context_id_str) - context_id = context_id_str - except (ValueError, AttributeError, TypeError) as e: - raise ValueError( - f"Invalid context_id: '{context_id_str}' is not a valid UUID." - ) from e - else: + if not context_id_str: context_id = str(uuid.uuid4()) + else: + context_id = context_id_str return Task( status=TaskStatus(state=TaskState.submitted), From 409e3a697d5b3310bbb137115450e60f1e742f6e Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Fri, 22 Aug 2025 22:07:00 +0100 Subject: [PATCH 10/12] Remove now invalid test --- tests/utils/test_task.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/utils/test_task.py b/tests/utils/test_task.py index 774413163..cb3dc3868 100644 --- a/tests/utils/test_task.py +++ b/tests/utils/test_task.py @@ -188,24 +188,6 @@ def test_completed_task_invalid_artifact_type(self): history=[], ) - def test_new_task_with_invalid_context_id(self): - """Test that new_task raises a ValueError for various invalid context_id formats.""" - invalid_ids = ['not-a-uuid', ''] - for invalid_id in invalid_ids: - with self.subTest(invalid_id=invalid_id): - with pytest.raises( - ValueError, - match=f"Invalid context_id: '{invalid_id}' is not a valid UUID.", - ): - new_task( - Message( - role=Role.user, - parts=[Part(root=TextPart(text='test message'))], - message_id=str(uuid.uuid4()), - context_id=invalid_id, - ) - ) - if __name__ == '__main__': unittest.main() From 8cf9baeecc8a018fb6d125f1c975de3ae4d2f68d Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Fri, 22 Aug 2025 22:08:49 +0100 Subject: [PATCH 11/12] Simplify context_id creation --- src/a2a/utils/task.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/a2a/utils/task.py b/src/a2a/utils/task.py index bd5ee431f..06aa23ea7 100644 --- a/src/a2a/utils/task.py +++ b/src/a2a/utils/task.py @@ -28,16 +28,10 @@ def new_task(request: Message) -> Task: if isinstance(part.root, TextPart) and not part.root.text: raise ValueError('TextPart content cannot be empty') - context_id_str = request.context_id - if not context_id_str: - context_id = str(uuid.uuid4()) - else: - context_id = context_id_str - return Task( status=TaskStatus(state=TaskState.submitted), id=(request.task_id if request.task_id else str(uuid.uuid4())), - context_id=context_id, + context_id=(request.context_id if request.context_id else str(uuid.uuid4())), history=[request], ) From fc8df2df5c1873a179a48b9c2fd37a30082fd59f Mon Sep 17 00:00:00 2001 From: Holt Skinner Date: Fri, 22 Aug 2025 22:12:50 +0100 Subject: [PATCH 12/12] Formatting --- src/a2a/utils/task.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/a2a/utils/task.py b/src/a2a/utils/task.py index 06aa23ea7..22556cde3 100644 --- a/src/a2a/utils/task.py +++ b/src/a2a/utils/task.py @@ -30,8 +30,8 @@ def new_task(request: Message) -> Task: return Task( status=TaskStatus(state=TaskState.submitted), - id=(request.task_id if request.task_id else str(uuid.uuid4())), - context_id=(request.context_id if request.context_id else str(uuid.uuid4())), + id=request.task_id or str(uuid.uuid4()), + context_id=request.context_id or str(uuid.uuid4()), history=[request], )