mirror of
https://github.com/encounter/adk-python.git
synced 2026-07-09 18:19:28 -07:00
chore: Enhance a2a part converters
a. fix binary data conversion b. support thoughts, code execution result, executable codes conversion PiperOrigin-RevId: 775827259
This commit is contained in:
committed by
Copybara-Service
parent
738d1a8b84
commit
832a633351
@@ -18,6 +18,7 @@ module containing utilities for conversion betwen A2A Part and Google GenAI Part
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
@@ -43,8 +44,11 @@ from ...utils.feature_decorator import working_in_progress
|
|||||||
logger = logging.getLogger('google_adk.' + __name__)
|
logger = logging.getLogger('google_adk.' + __name__)
|
||||||
|
|
||||||
A2A_DATA_PART_METADATA_TYPE_KEY = 'type'
|
A2A_DATA_PART_METADATA_TYPE_KEY = 'type'
|
||||||
|
A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY = 'is_long_running'
|
||||||
A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL = 'function_call'
|
A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL = 'function_call'
|
||||||
A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE = 'function_response'
|
A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE = 'function_response'
|
||||||
|
A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT = 'code_execution_result'
|
||||||
|
A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE = 'executable_code'
|
||||||
|
|
||||||
|
|
||||||
@working_in_progress
|
@working_in_progress
|
||||||
@@ -67,7 +71,8 @@ def convert_a2a_part_to_genai_part(
|
|||||||
elif isinstance(part.file, a2a_types.FileWithBytes):
|
elif isinstance(part.file, a2a_types.FileWithBytes):
|
||||||
return genai_types.Part(
|
return genai_types.Part(
|
||||||
inline_data=genai_types.Blob(
|
inline_data=genai_types.Blob(
|
||||||
data=part.file.bytes.encode('utf-8'), mime_type=part.file.mimeType
|
data=base64.b64decode(part.file.bytes),
|
||||||
|
mime_type=part.file.mimeType,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -84,7 +89,11 @@ def convert_a2a_part_to_genai_part(
|
|||||||
# response.
|
# response.
|
||||||
# TODO once A2A defined how to suervice such information, migrate below
|
# TODO once A2A defined how to suervice such information, migrate below
|
||||||
# logic accordinlgy
|
# logic accordinlgy
|
||||||
if part.metadata and A2A_DATA_PART_METADATA_TYPE_KEY in part.metadata:
|
if (
|
||||||
|
part.metadata
|
||||||
|
and _get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)
|
||||||
|
in part.metadata
|
||||||
|
):
|
||||||
if (
|
if (
|
||||||
part.metadata[_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
|
part.metadata[_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
|
||||||
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
||||||
@@ -103,6 +112,24 @@ def convert_a2a_part_to_genai_part(
|
|||||||
part.data, by_alias=True
|
part.data, by_alias=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
part.metadata[_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
|
||||||
|
== A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT
|
||||||
|
):
|
||||||
|
return genai_types.Part(
|
||||||
|
code_execution_result=genai_types.CodeExecutionResult.model_validate(
|
||||||
|
part.data, by_alias=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
part.metadata[_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
|
||||||
|
== A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE
|
||||||
|
):
|
||||||
|
return genai_types.Part(
|
||||||
|
executable_code=genai_types.ExecutableCode.model_validate(
|
||||||
|
part.data, by_alias=True
|
||||||
|
)
|
||||||
|
)
|
||||||
return genai_types.Part(text=json.dumps(part.data))
|
return genai_types.Part(text=json.dumps(part.data))
|
||||||
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -118,27 +145,40 @@ def convert_genai_part_to_a2a_part(
|
|||||||
part: genai_types.Part,
|
part: genai_types.Part,
|
||||||
) -> Optional[a2a_types.Part]:
|
) -> Optional[a2a_types.Part]:
|
||||||
"""Convert a Google GenAI Part to an A2A Part."""
|
"""Convert a Google GenAI Part to an A2A Part."""
|
||||||
|
|
||||||
if part.text:
|
if part.text:
|
||||||
return a2a_types.TextPart(text=part.text)
|
a2a_part = a2a_types.TextPart(text=part.text)
|
||||||
|
if part.thought is not None:
|
||||||
|
a2a_part.metadata = {_get_adk_metadata_key('thought'): part.thought}
|
||||||
|
return a2a_types.Part(root=a2a_part)
|
||||||
|
|
||||||
if part.file_data:
|
if part.file_data:
|
||||||
return a2a_types.FilePart(
|
return a2a_types.Part(
|
||||||
file=a2a_types.FileWithUri(
|
root=a2a_types.FilePart(
|
||||||
uri=part.file_data.file_uri,
|
file=a2a_types.FileWithUri(
|
||||||
mimeType=part.file_data.mime_type,
|
uri=part.file_data.file_uri,
|
||||||
|
mimeType=part.file_data.mime_type,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if part.inline_data:
|
if part.inline_data:
|
||||||
return a2a_types.Part(
|
a2a_part = a2a_types.FilePart(
|
||||||
root=a2a_types.FilePart(
|
file=a2a_types.FileWithBytes(
|
||||||
file=a2a_types.FileWithBytes(
|
bytes=base64.b64encode(part.inline_data.data).decode('utf-8'),
|
||||||
bytes=part.inline_data.data,
|
mimeType=part.inline_data.mime_type,
|
||||||
mimeType=part.inline_data.mime_type,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if part.video_metadata:
|
||||||
|
a2a_part.metadata = {
|
||||||
|
_get_adk_metadata_key(
|
||||||
|
'video_metadata'
|
||||||
|
): part.video_metadata.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
}
|
||||||
|
|
||||||
|
return a2a_types.Part(root=a2a_part)
|
||||||
|
|
||||||
# Conver the funcall and function reponse to A2A DataPart.
|
# Conver the funcall and function reponse to A2A DataPart.
|
||||||
# This is mainly for converting human in the loop and auth request and
|
# This is mainly for converting human in the loop and auth request and
|
||||||
# response.
|
# response.
|
||||||
@@ -151,9 +191,9 @@ def convert_genai_part_to_a2a_part(
|
|||||||
by_alias=True, exclude_none=True
|
by_alias=True, exclude_none=True
|
||||||
),
|
),
|
||||||
metadata={
|
metadata={
|
||||||
A2A_DATA_PART_METADATA_TYPE_KEY: (
|
_get_adk_metadata_key(
|
||||||
A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
A2A_DATA_PART_METADATA_TYPE_KEY
|
||||||
)
|
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -165,9 +205,37 @@ def convert_genai_part_to_a2a_part(
|
|||||||
by_alias=True, exclude_none=True
|
by_alias=True, exclude_none=True
|
||||||
),
|
),
|
||||||
metadata={
|
metadata={
|
||||||
A2A_DATA_PART_METADATA_TYPE_KEY: (
|
_get_adk_metadata_key(
|
||||||
A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
|
A2A_DATA_PART_METADATA_TYPE_KEY
|
||||||
)
|
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if part.code_execution_result:
|
||||||
|
return a2a_types.Part(
|
||||||
|
root=a2a_types.DataPart(
|
||||||
|
data=part.code_execution_result.model_dump(
|
||||||
|
by_alias=True, exclude_none=True
|
||||||
|
),
|
||||||
|
metadata={
|
||||||
|
_get_adk_metadata_key(
|
||||||
|
A2A_DATA_PART_METADATA_TYPE_KEY
|
||||||
|
): A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if part.executable_code:
|
||||||
|
return a2a_types.Part(
|
||||||
|
root=a2a_types.DataPart(
|
||||||
|
data=part.executable_code.model_dump(
|
||||||
|
by_alias=True, exclude_none=True
|
||||||
|
),
|
||||||
|
metadata={
|
||||||
|
_get_adk_metadata_key(
|
||||||
|
A2A_DATA_PART_METADATA_TYPE_KEY
|
||||||
|
): A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,17 +21,20 @@ import pytest
|
|||||||
|
|
||||||
# Skip all tests in this module if Python version is less than 3.10
|
# Skip all tests in this module if Python version is less than 3.10
|
||||||
pytestmark = pytest.mark.skipif(
|
pytestmark = pytest.mark.skipif(
|
||||||
sys.version_info < (3, 10), reason="A2A tool requires Python 3.10+"
|
sys.version_info < (3, 10), reason="A2A requires Python 3.10+"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Import dependencies with version checking
|
# Import dependencies with version checking
|
||||||
try:
|
try:
|
||||||
from a2a import types as a2a_types
|
from a2a import types as a2a_types
|
||||||
|
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT
|
||||||
|
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE
|
||||||
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
||||||
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
|
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
|
||||||
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_KEY
|
from google.adk.a2a.converters.part_converter import A2A_DATA_PART_METADATA_TYPE_KEY
|
||||||
from google.adk.a2a.converters.part_converter import convert_a2a_part_to_genai_part
|
from google.adk.a2a.converters.part_converter import convert_a2a_part_to_genai_part
|
||||||
from google.adk.a2a.converters.part_converter import convert_genai_part_to_a2a_part
|
from google.adk.a2a.converters.part_converter import convert_genai_part_to_a2a_part
|
||||||
|
from google.adk.a2a.converters.utils import _get_adk_metadata_key
|
||||||
from google.genai import types as genai_types
|
from google.genai import types as genai_types
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
if sys.version_info < (3, 10):
|
if sys.version_info < (3, 10):
|
||||||
@@ -44,9 +47,12 @@ except ImportError as e:
|
|||||||
genai_types = DummyTypes()
|
genai_types = DummyTypes()
|
||||||
A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL = "function_call"
|
A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL = "function_call"
|
||||||
A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE = "function_response"
|
A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE = "function_response"
|
||||||
|
A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT = "code_execution_result"
|
||||||
|
A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE = "executable_code"
|
||||||
A2A_DATA_PART_METADATA_TYPE_KEY = "type"
|
A2A_DATA_PART_METADATA_TYPE_KEY = "type"
|
||||||
convert_a2a_part_to_genai_part = lambda x: None
|
convert_a2a_part_to_genai_part = lambda x: None
|
||||||
convert_genai_part_to_a2a_part = lambda x: None
|
convert_genai_part_to_a2a_part = lambda x: None
|
||||||
|
_get_adk_metadata_key = lambda x: f"adk_{x}"
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
@@ -92,11 +98,14 @@ class TestConvertA2aPartToGenaiPart:
|
|||||||
"""Test conversion of A2A FilePart with bytes to GenAI Part."""
|
"""Test conversion of A2A FilePart with bytes to GenAI Part."""
|
||||||
# Arrange
|
# Arrange
|
||||||
test_bytes = b"test file content"
|
test_bytes = b"test file content"
|
||||||
# Note: A2A FileWithBytes converts bytes to string automatically
|
# A2A FileWithBytes expects base64-encoded string
|
||||||
|
import base64
|
||||||
|
|
||||||
|
base64_encoded = base64.b64encode(test_bytes).decode("utf-8")
|
||||||
a2a_part = a2a_types.Part(
|
a2a_part = a2a_types.Part(
|
||||||
root=a2a_types.FilePart(
|
root=a2a_types.FilePart(
|
||||||
file=a2a_types.FileWithBytes(
|
file=a2a_types.FileWithBytes(
|
||||||
bytes=test_bytes, mimeType="text/plain"
|
bytes=base64_encoded, mimeType="text/plain"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -108,7 +117,7 @@ class TestConvertA2aPartToGenaiPart:
|
|||||||
assert result is not None
|
assert result is not None
|
||||||
assert isinstance(result, genai_types.Part)
|
assert isinstance(result, genai_types.Part)
|
||||||
assert result.inline_data is not None
|
assert result.inline_data is not None
|
||||||
# Source code now properly converts A2A string back to bytes for GenAI Blob
|
# The converter decodes base64 back to original bytes
|
||||||
assert result.inline_data.data == test_bytes
|
assert result.inline_data.data == test_bytes
|
||||||
assert result.inline_data.mime_type == "text/plain"
|
assert result.inline_data.mime_type == "text/plain"
|
||||||
|
|
||||||
@@ -123,9 +132,9 @@ class TestConvertA2aPartToGenaiPart:
|
|||||||
root=a2a_types.DataPart(
|
root=a2a_types.DataPart(
|
||||||
data=function_call_data,
|
data=function_call_data,
|
||||||
metadata={
|
metadata={
|
||||||
A2A_DATA_PART_METADATA_TYPE_KEY: (
|
_get_adk_metadata_key(
|
||||||
A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
A2A_DATA_PART_METADATA_TYPE_KEY
|
||||||
),
|
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
|
||||||
"adk_type": A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
|
"adk_type": A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -152,9 +161,9 @@ class TestConvertA2aPartToGenaiPart:
|
|||||||
root=a2a_types.DataPart(
|
root=a2a_types.DataPart(
|
||||||
data=function_response_data,
|
data=function_response_data,
|
||||||
metadata={
|
metadata={
|
||||||
A2A_DATA_PART_METADATA_TYPE_KEY: (
|
_get_adk_metadata_key(
|
||||||
A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
|
A2A_DATA_PART_METADATA_TYPE_KEY
|
||||||
),
|
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE,
|
||||||
"adk_type": A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE,
|
"adk_type": A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -260,8 +269,25 @@ class TestConvertGenaiPartToA2aPart:
|
|||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert isinstance(result, a2a_types.TextPart)
|
assert isinstance(result, a2a_types.Part)
|
||||||
assert result.text == "Hello, world!"
|
assert isinstance(result.root, a2a_types.TextPart)
|
||||||
|
assert result.root.text == "Hello, world!"
|
||||||
|
|
||||||
|
def test_convert_text_part_with_thought(self):
|
||||||
|
"""Test conversion of GenAI text Part with thought to A2A Part."""
|
||||||
|
# Arrange - thought is a boolean field in genai_types.Part
|
||||||
|
genai_part = genai_types.Part(text="Hello, world!", thought=True)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = convert_genai_part_to_a2a_part(genai_part)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, a2a_types.Part)
|
||||||
|
assert isinstance(result.root, a2a_types.TextPart)
|
||||||
|
assert result.root.text == "Hello, world!"
|
||||||
|
assert result.root.metadata is not None
|
||||||
|
assert result.root.metadata[_get_adk_metadata_key("thought")] == True
|
||||||
|
|
||||||
def test_convert_file_data_part(self):
|
def test_convert_file_data_part(self):
|
||||||
"""Test conversion of GenAI file_data Part to A2A Part."""
|
"""Test conversion of GenAI file_data Part to A2A Part."""
|
||||||
@@ -277,10 +303,11 @@ class TestConvertGenaiPartToA2aPart:
|
|||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert isinstance(result, a2a_types.FilePart)
|
assert isinstance(result, a2a_types.Part)
|
||||||
assert isinstance(result.file, a2a_types.FileWithUri)
|
assert isinstance(result.root, a2a_types.FilePart)
|
||||||
assert result.file.uri == "gs://bucket/file.txt"
|
assert isinstance(result.root.file, a2a_types.FileWithUri)
|
||||||
assert result.file.mimeType == "text/plain"
|
assert result.root.file.uri == "gs://bucket/file.txt"
|
||||||
|
assert result.root.file.mimeType == "text/plain"
|
||||||
|
|
||||||
def test_convert_inline_data_part(self):
|
def test_convert_inline_data_part(self):
|
||||||
"""Test conversion of GenAI inline_data Part to A2A Part."""
|
"""Test conversion of GenAI inline_data Part to A2A Part."""
|
||||||
@@ -298,10 +325,34 @@ class TestConvertGenaiPartToA2aPart:
|
|||||||
assert isinstance(result, a2a_types.Part)
|
assert isinstance(result, a2a_types.Part)
|
||||||
assert isinstance(result.root, a2a_types.FilePart)
|
assert isinstance(result.root, a2a_types.FilePart)
|
||||||
assert isinstance(result.root.file, a2a_types.FileWithBytes)
|
assert isinstance(result.root.file, a2a_types.FileWithBytes)
|
||||||
# A2A FileWithBytes stores bytes as strings
|
# A2A FileWithBytes now stores base64-encoded bytes to ensure round-trip compatibility
|
||||||
assert result.root.file.bytes == test_bytes.decode("utf-8")
|
import base64
|
||||||
|
|
||||||
|
expected_base64 = base64.b64encode(test_bytes).decode("utf-8")
|
||||||
|
assert result.root.file.bytes == expected_base64
|
||||||
assert result.root.file.mimeType == "text/plain"
|
assert result.root.file.mimeType == "text/plain"
|
||||||
|
|
||||||
|
def test_convert_inline_data_part_with_video_metadata(self):
|
||||||
|
"""Test conversion of GenAI inline_data Part with video metadata to A2A Part."""
|
||||||
|
# Arrange
|
||||||
|
test_bytes = b"test video content"
|
||||||
|
video_metadata = genai_types.VideoMetadata(fps=30.0)
|
||||||
|
genai_part = genai_types.Part(
|
||||||
|
inline_data=genai_types.Blob(data=test_bytes, mime_type="video/mp4"),
|
||||||
|
video_metadata=video_metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = convert_genai_part_to_a2a_part(genai_part)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, a2a_types.Part)
|
||||||
|
assert isinstance(result.root, a2a_types.FilePart)
|
||||||
|
assert isinstance(result.root.file, a2a_types.FileWithBytes)
|
||||||
|
assert result.root.metadata is not None
|
||||||
|
assert _get_adk_metadata_key("video_metadata") in result.root.metadata
|
||||||
|
|
||||||
def test_convert_function_call_part(self):
|
def test_convert_function_call_part(self):
|
||||||
"""Test conversion of GenAI function_call Part to A2A Part."""
|
"""Test conversion of GenAI function_call Part to A2A Part."""
|
||||||
# Arrange
|
# Arrange
|
||||||
@@ -320,7 +371,9 @@ class TestConvertGenaiPartToA2aPart:
|
|||||||
expected_data = function_call.model_dump(by_alias=True, exclude_none=True)
|
expected_data = function_call.model_dump(by_alias=True, exclude_none=True)
|
||||||
assert result.root.data == expected_data
|
assert result.root.data == expected_data
|
||||||
assert (
|
assert (
|
||||||
result.root.metadata[A2A_DATA_PART_METADATA_TYPE_KEY]
|
result.root.metadata[
|
||||||
|
_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)
|
||||||
|
]
|
||||||
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -344,10 +397,62 @@ class TestConvertGenaiPartToA2aPart:
|
|||||||
)
|
)
|
||||||
assert result.root.data == expected_data
|
assert result.root.data == expected_data
|
||||||
assert (
|
assert (
|
||||||
result.root.metadata[A2A_DATA_PART_METADATA_TYPE_KEY]
|
result.root.metadata[
|
||||||
|
_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)
|
||||||
|
]
|
||||||
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
|
== A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_convert_code_execution_result_part(self):
|
||||||
|
"""Test conversion of GenAI code_execution_result Part to A2A Part."""
|
||||||
|
# Arrange
|
||||||
|
code_execution_result = genai_types.CodeExecutionResult(
|
||||||
|
outcome=genai_types.Outcome.OUTCOME_OK, output="Hello, World!"
|
||||||
|
)
|
||||||
|
genai_part = genai_types.Part(code_execution_result=code_execution_result)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = convert_genai_part_to_a2a_part(genai_part)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, a2a_types.Part)
|
||||||
|
assert isinstance(result.root, a2a_types.DataPart)
|
||||||
|
expected_data = code_execution_result.model_dump(
|
||||||
|
by_alias=True, exclude_none=True
|
||||||
|
)
|
||||||
|
assert result.root.data == expected_data
|
||||||
|
assert (
|
||||||
|
result.root.metadata[
|
||||||
|
_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)
|
||||||
|
]
|
||||||
|
== A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_convert_executable_code_part(self):
|
||||||
|
"""Test conversion of GenAI executable_code Part to A2A Part."""
|
||||||
|
# Arrange
|
||||||
|
executable_code = genai_types.ExecutableCode(
|
||||||
|
language=genai_types.Language.PYTHON, code="print('Hello, World!')"
|
||||||
|
)
|
||||||
|
genai_part = genai_types.Part(executable_code=executable_code)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = convert_genai_part_to_a2a_part(genai_part)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, a2a_types.Part)
|
||||||
|
assert isinstance(result.root, a2a_types.DataPart)
|
||||||
|
expected_data = executable_code.model_dump(by_alias=True, exclude_none=True)
|
||||||
|
assert result.root.data == expected_data
|
||||||
|
assert (
|
||||||
|
result.root.metadata[
|
||||||
|
_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)
|
||||||
|
]
|
||||||
|
== A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE
|
||||||
|
)
|
||||||
|
|
||||||
def test_convert_unsupported_part(self):
|
def test_convert_unsupported_part(self):
|
||||||
"""Test handling of unsupported GenAI Part types."""
|
"""Test handling of unsupported GenAI Part types."""
|
||||||
# Arrange - Create a GenAI Part with no recognized fields
|
# Arrange - Create a GenAI Part with no recognized fields
|
||||||
@@ -379,8 +484,9 @@ class TestRoundTripConversions:
|
|||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert result_a2a_part is not None
|
assert result_a2a_part is not None
|
||||||
assert isinstance(result_a2a_part, a2a_types.TextPart)
|
assert isinstance(result_a2a_part, a2a_types.Part)
|
||||||
assert result_a2a_part.text == original_text
|
assert isinstance(result_a2a_part.root, a2a_types.TextPart)
|
||||||
|
assert result_a2a_part.root.text == original_text
|
||||||
|
|
||||||
def test_file_uri_round_trip(self):
|
def test_file_uri_round_trip(self):
|
||||||
"""Test round-trip conversion for file parts with URI."""
|
"""Test round-trip conversion for file parts with URI."""
|
||||||
@@ -401,10 +507,122 @@ class TestRoundTripConversions:
|
|||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
assert result_a2a_part is not None
|
assert result_a2a_part is not None
|
||||||
assert isinstance(result_a2a_part, a2a_types.FilePart)
|
assert isinstance(result_a2a_part, a2a_types.Part)
|
||||||
assert isinstance(result_a2a_part.file, a2a_types.FileWithUri)
|
assert isinstance(result_a2a_part.root, a2a_types.FilePart)
|
||||||
assert result_a2a_part.file.uri == original_uri
|
assert isinstance(result_a2a_part.root.file, a2a_types.FileWithUri)
|
||||||
assert result_a2a_part.file.mimeType == original_mime_type
|
assert result_a2a_part.root.file.uri == original_uri
|
||||||
|
assert result_a2a_part.root.file.mimeType == original_mime_type
|
||||||
|
|
||||||
|
def test_file_bytes_round_trip(self):
|
||||||
|
"""Test round-trip conversion for file parts with bytes."""
|
||||||
|
# Arrange
|
||||||
|
original_bytes = b"test file content for round trip"
|
||||||
|
original_mime_type = "application/octet-stream"
|
||||||
|
|
||||||
|
# Start with GenAI part (the more common starting point)
|
||||||
|
genai_part = genai_types.Part(
|
||||||
|
inline_data=genai_types.Blob(
|
||||||
|
data=original_bytes, mime_type=original_mime_type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Act - Round trip: GenAI -> A2A -> GenAI
|
||||||
|
a2a_part = convert_genai_part_to_a2a_part(genai_part)
|
||||||
|
result_genai_part = convert_a2a_part_to_genai_part(a2a_part)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result_genai_part is not None
|
||||||
|
assert isinstance(result_genai_part, genai_types.Part)
|
||||||
|
assert result_genai_part.inline_data is not None
|
||||||
|
assert result_genai_part.inline_data.data == original_bytes
|
||||||
|
assert result_genai_part.inline_data.mime_type == original_mime_type
|
||||||
|
|
||||||
|
def test_function_call_round_trip(self):
|
||||||
|
"""Test round-trip conversion for function call parts."""
|
||||||
|
# Arrange
|
||||||
|
function_call = genai_types.FunctionCall(
|
||||||
|
name="test_function", args={"param1": "value1", "param2": 42}
|
||||||
|
)
|
||||||
|
genai_part = genai_types.Part(function_call=function_call)
|
||||||
|
|
||||||
|
# Act - Round trip: GenAI -> A2A -> GenAI
|
||||||
|
a2a_part = convert_genai_part_to_a2a_part(genai_part)
|
||||||
|
result_genai_part = convert_a2a_part_to_genai_part(a2a_part)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result_genai_part is not None
|
||||||
|
assert isinstance(result_genai_part, genai_types.Part)
|
||||||
|
assert result_genai_part.function_call is not None
|
||||||
|
assert result_genai_part.function_call.name == function_call.name
|
||||||
|
assert result_genai_part.function_call.args == function_call.args
|
||||||
|
|
||||||
|
def test_function_response_round_trip(self):
|
||||||
|
"""Test round-trip conversion for function response parts."""
|
||||||
|
# Arrange
|
||||||
|
function_response = genai_types.FunctionResponse(
|
||||||
|
name="test_function", response={"result": "success", "data": [1, 2, 3]}
|
||||||
|
)
|
||||||
|
genai_part = genai_types.Part(function_response=function_response)
|
||||||
|
|
||||||
|
# Act - Round trip: GenAI -> A2A -> GenAI
|
||||||
|
a2a_part = convert_genai_part_to_a2a_part(genai_part)
|
||||||
|
result_genai_part = convert_a2a_part_to_genai_part(a2a_part)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result_genai_part is not None
|
||||||
|
assert isinstance(result_genai_part, genai_types.Part)
|
||||||
|
assert result_genai_part.function_response is not None
|
||||||
|
assert result_genai_part.function_response.name == function_response.name
|
||||||
|
assert (
|
||||||
|
result_genai_part.function_response.response
|
||||||
|
== function_response.response
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_code_execution_result_round_trip(self):
|
||||||
|
"""Test round-trip conversion for code execution result parts."""
|
||||||
|
# Arrange
|
||||||
|
code_execution_result = genai_types.CodeExecutionResult(
|
||||||
|
outcome=genai_types.Outcome.OUTCOME_OK, output="Hello, World!"
|
||||||
|
)
|
||||||
|
genai_part = genai_types.Part(code_execution_result=code_execution_result)
|
||||||
|
|
||||||
|
# Act - Round trip: GenAI -> A2A -> GenAI
|
||||||
|
a2a_part = convert_genai_part_to_a2a_part(genai_part)
|
||||||
|
result_genai_part = convert_a2a_part_to_genai_part(a2a_part)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result_genai_part is not None
|
||||||
|
assert isinstance(result_genai_part, genai_types.Part)
|
||||||
|
assert result_genai_part.code_execution_result is not None
|
||||||
|
assert (
|
||||||
|
result_genai_part.code_execution_result.outcome
|
||||||
|
== code_execution_result.outcome
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
result_genai_part.code_execution_result.output
|
||||||
|
== code_execution_result.output
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_executable_code_round_trip(self):
|
||||||
|
"""Test round-trip conversion for executable code parts."""
|
||||||
|
# Arrange
|
||||||
|
executable_code = genai_types.ExecutableCode(
|
||||||
|
language=genai_types.Language.PYTHON, code="print('Hello, World!')"
|
||||||
|
)
|
||||||
|
genai_part = genai_types.Part(executable_code=executable_code)
|
||||||
|
|
||||||
|
# Act - Round trip: GenAI -> A2A -> GenAI
|
||||||
|
a2a_part = convert_genai_part_to_a2a_part(genai_part)
|
||||||
|
result_genai_part = convert_a2a_part_to_genai_part(a2a_part)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result_genai_part is not None
|
||||||
|
assert isinstance(result_genai_part, genai_types.Part)
|
||||||
|
assert result_genai_part.executable_code is not None
|
||||||
|
assert (
|
||||||
|
result_genai_part.executable_code.language == executable_code.language
|
||||||
|
)
|
||||||
|
assert result_genai_part.executable_code.code == executable_code.code
|
||||||
|
|
||||||
|
|
||||||
class TestEdgeCases:
|
class TestEdgeCases:
|
||||||
@@ -468,3 +686,75 @@ class TestEdgeCases:
|
|||||||
# Assert
|
# Assert
|
||||||
assert result is not None
|
assert result is not None
|
||||||
assert result.text == json.dumps(data)
|
assert result.text == json.dumps(data)
|
||||||
|
|
||||||
|
|
||||||
|
class TestNewConstants:
|
||||||
|
"""Test cases for new constants and functionality."""
|
||||||
|
|
||||||
|
def test_new_constants_exist(self):
|
||||||
|
"""Test that new constants are defined."""
|
||||||
|
assert (
|
||||||
|
A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT
|
||||||
|
== "code_execution_result"
|
||||||
|
)
|
||||||
|
assert A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE == "executable_code"
|
||||||
|
|
||||||
|
def test_convert_a2a_data_part_with_code_execution_result_metadata(self):
|
||||||
|
"""Test conversion of A2A DataPart with code execution result metadata."""
|
||||||
|
# Arrange
|
||||||
|
code_execution_result_data = {
|
||||||
|
"outcome": "OUTCOME_OK",
|
||||||
|
"output": "Hello, World!",
|
||||||
|
}
|
||||||
|
a2a_part = a2a_types.Part(
|
||||||
|
root=a2a_types.DataPart(
|
||||||
|
data=code_execution_result_data,
|
||||||
|
metadata={
|
||||||
|
_get_adk_metadata_key(
|
||||||
|
A2A_DATA_PART_METADATA_TYPE_KEY
|
||||||
|
): A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = convert_a2a_part_to_genai_part(a2a_part)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, genai_types.Part)
|
||||||
|
# Now it should convert back to a proper CodeExecutionResult
|
||||||
|
assert result.code_execution_result is not None
|
||||||
|
assert (
|
||||||
|
result.code_execution_result.outcome == genai_types.Outcome.OUTCOME_OK
|
||||||
|
)
|
||||||
|
assert result.code_execution_result.output == "Hello, World!"
|
||||||
|
|
||||||
|
def test_convert_a2a_data_part_with_executable_code_metadata(self):
|
||||||
|
"""Test conversion of A2A DataPart with executable code metadata."""
|
||||||
|
# Arrange
|
||||||
|
executable_code_data = {
|
||||||
|
"language": "PYTHON",
|
||||||
|
"code": "print('Hello, World!')",
|
||||||
|
}
|
||||||
|
a2a_part = a2a_types.Part(
|
||||||
|
root=a2a_types.DataPart(
|
||||||
|
data=executable_code_data,
|
||||||
|
metadata={
|
||||||
|
_get_adk_metadata_key(
|
||||||
|
A2A_DATA_PART_METADATA_TYPE_KEY
|
||||||
|
): A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
result = convert_a2a_part_to_genai_part(a2a_part)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert result is not None
|
||||||
|
assert isinstance(result, genai_types.Part)
|
||||||
|
# Now it should convert back to a proper ExecutableCode
|
||||||
|
assert result.executable_code is not None
|
||||||
|
assert result.executable_code.language == genai_types.Language.PYTHON
|
||||||
|
assert result.executable_code.code == "print('Hello, World!')"
|
||||||
|
|||||||
Reference in New Issue
Block a user