Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
28 changes: 20 additions & 8 deletions src/google/adk/a2a/converters/part_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,30 @@ def convert_a2a_part_to_genai_part(
) -> Optional[genai_types.Part]:
"""Convert an A2A Part to a Google GenAI Part."""
part = a2a_part.root

thought = None
if part.metadata:
thought = part.metadata.get(_get_adk_metadata_key('thought'))

if isinstance(part, a2a_types.TextPart):
return genai_types.Part(text=part.text)
return genai_types.Part(text=part.text, thought=thought)

if isinstance(part, a2a_types.FilePart):
if isinstance(part.file, a2a_types.FileWithUri):
return genai_types.Part(
file_data=genai_types.FileData(
file_uri=part.file.uri, mime_type=part.file.mime_type
)
),
thought=thought,
)

elif isinstance(part.file, a2a_types.FileWithBytes):
return genai_types.Part(
inline_data=genai_types.Blob(
data=base64.b64decode(part.file.bytes),
mime_type=part.file.mime_type,
)
),
thought=thought,
)
else:
logger.warning(
Expand Down Expand Up @@ -104,7 +111,8 @@ def convert_a2a_part_to_genai_part(
return genai_types.Part(
function_call=genai_types.FunctionCall.model_validate(
part.data, by_alias=True
)
),
thought=thought,
)
if (
part.metadata[_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
Expand All @@ -113,7 +121,8 @@ def convert_a2a_part_to_genai_part(
return genai_types.Part(
function_response=genai_types.FunctionResponse.model_validate(
part.data, by_alias=True
)
),
thought=thought,
)
if (
part.metadata[_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
Expand All @@ -122,7 +131,8 @@ def convert_a2a_part_to_genai_part(
return genai_types.Part(
code_execution_result=genai_types.CodeExecutionResult.model_validate(
part.data, by_alias=True
)
),
thought=thought,
)
if (
part.metadata[_get_adk_metadata_key(A2A_DATA_PART_METADATA_TYPE_KEY)]
Expand All @@ -131,7 +141,8 @@ def convert_a2a_part_to_genai_part(
return genai_types.Part(
executable_code=genai_types.ExecutableCode.model_validate(
part.data, by_alias=True
)
),
thought=thought,
)
return genai_types.Part(
inline_data=genai_types.Blob(
Expand All @@ -141,7 +152,8 @@ def convert_a2a_part_to_genai_part(
)
+ A2A_DATA_PART_END_TAG,
mime_type=A2A_DATA_PART_TEXT_MIME_TYPE,
)
),
thought=thought,
)

logger.warning(
Expand Down
166 changes: 165 additions & 1 deletion tests/unittests/a2a/converters/test_part_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,62 @@ def test_convert_text_part(self):
assert isinstance(result, genai_types.Part)
assert result.text == "Hello, world!"

def test_convert_text_part_with_thought_true_metadata(self):
"""Test conversion of A2A TextPart with thought=True metadata to GenAI Part."""
# Arrange
a2a_part = a2a_types.Part(
root=a2a_types.TextPart(
text="I'm thinking about this...",
metadata={_get_adk_metadata_key("thought"): True},
)
)

# Act
result = convert_a2a_part_to_genai_part(a2a_part)

# Assert
assert result is not None
assert isinstance(result, genai_types.Part)
assert result.text == "I'm thinking about this..."
assert result.thought is True

def test_convert_text_part_with_thought_false_metadata(self):
"""Test conversion of A2A TextPart with thought=False metadata to GenAI Part."""
# Arrange
a2a_part = a2a_types.Part(
root=a2a_types.TextPart(
text="This is not a thought.",
metadata={_get_adk_metadata_key("thought"): False},
)
)

# Act
result = convert_a2a_part_to_genai_part(a2a_part)

# Assert
assert result is not None
assert isinstance(result, genai_types.Part)
assert result.text == "This is not a thought."
assert result.thought is False

def test_convert_text_part_without_thought_metadata(self):
"""Test that TextPart without thought metadata has thought=None."""
# Arrange
a2a_part = a2a_types.Part(
root=a2a_types.TextPart(
text="Regular text",
metadata={"some_other_key": "value"},
)
)

# Act
result = convert_a2a_part_to_genai_part(a2a_part)

# Assert
assert result is not None
assert result.text == "Regular text"
assert result.thought is None

def test_convert_file_part_with_uri(self):
"""Test conversion of A2A FilePart with URI to GenAI Part."""
# Arrange
Expand Down Expand Up @@ -96,6 +152,54 @@ def test_convert_file_part_with_bytes(self):
assert result.inline_data.data == test_bytes
assert result.inline_data.mime_type == "text/plain"

def test_convert_file_part_with_uri_and_thought_metadata(self):
"""Test conversion of A2A FilePart with URI and thought metadata to GenAI Part."""
# Arrange
a2a_part = a2a_types.Part(
root=a2a_types.FilePart(
file=a2a_types.FileWithUri(
uri="gs://bucket/file.txt", mime_type="text/plain"
),
metadata={_get_adk_metadata_key("thought"): True},
)
)

# Act
result = convert_a2a_part_to_genai_part(a2a_part)

# Assert
assert result is not None
assert isinstance(result, genai_types.Part)
assert result.file_data is not None
assert result.file_data.file_uri == "gs://bucket/file.txt"
assert result.thought is True

def test_convert_file_part_with_bytes_and_thought_metadata(self):
"""Test conversion of A2A FilePart with bytes and thought metadata to GenAI Part."""
# Arrange
import base64

test_bytes = b"test file content"
base64_encoded = base64.b64encode(test_bytes).decode("utf-8")
a2a_part = a2a_types.Part(
root=a2a_types.FilePart(
file=a2a_types.FileWithBytes(
bytes=base64_encoded, mime_type="text/plain"
),
metadata={_get_adk_metadata_key("thought"): True},
)
)

# Act
result = convert_a2a_part_to_genai_part(a2a_part)

# Assert
assert result is not None
assert isinstance(result, genai_types.Part)
assert result.inline_data is not None
assert result.inline_data.data == test_bytes
assert result.thought is True

def test_convert_data_part_function_call(self):
"""Test conversion of A2A DataPart with function call metadata."""
# Arrange
Expand Down Expand Up @@ -125,6 +229,34 @@ def test_convert_data_part_function_call(self):
assert result.function_call.name == "test_function"
assert result.function_call.args == {"param1": "value1", "param2": 42}

def test_convert_data_part_function_call_with_thought_metadata(self):
"""Test conversion of A2A DataPart with function call and thought metadata."""
# Arrange
function_call_data = {
"name": "test_function",
"args": {"param1": "value1"},
}
a2a_part = a2a_types.Part(
root=a2a_types.DataPart(
data=function_call_data,
metadata={
_get_adk_metadata_key(
A2A_DATA_PART_METADATA_TYPE_KEY
): A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL,
_get_adk_metadata_key("thought"): True,
},
)
)

# Act
result = convert_a2a_part_to_genai_part(a2a_part)

# Assert
assert result is not None
assert result.function_call is not None
assert result.function_call.name == "test_function"
assert result.thought is True
Comment on lines +232 to +258
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This is a good test for function_call with thought metadata. However, the changes in part_converter.py also add thought preservation for other DataPart types like function_response, code_execution_result, and executable_code. To ensure full test coverage for the changes, please consider adding similar tests for these other DataPart types.


def test_convert_data_part_function_response(self):
"""Test conversion of A2A DataPart with function response metadata."""
# Arrange
Expand Down Expand Up @@ -242,7 +374,7 @@ def test_convert_unsupported_part_type(self):

# Arrange - Create a mock unsupported part type
class UnsupportedPartType:
pass
metadata = None # Required since code accesses part.metadata

mock_part = Mock()
mock_part.root = UnsupportedPartType()
Expand Down Expand Up @@ -516,6 +648,38 @@ def test_text_part_round_trip(self):
assert isinstance(result_a2a_part.root, a2a_types.TextPart)
assert result_a2a_part.root.text == original_text

def test_text_part_with_thought_round_trip(self):
"""Test round-trip conversion for text parts with thought=True."""
# Arrange - Start with GenAI part with thought=True
original_text = "I'm reasoning about this problem..."
genai_part = genai_types.Part(text=original_text, thought=True)

# 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.text == original_text
assert result_genai_part.thought is True
Comment on lines +737 to +751
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

These round-trip tests for TextPart are great for ensuring thought is preserved. However, the fix for preserving thought seems incomplete for the full round-trip. The convert_genai_part_to_a2a_part function only handles thought for TextPart and will drop it for other part types like FilePart and DataPart.

I recommend adding similar round-trip tests for other part types (e.g., test_file_part_with_thought_round_trip) to expose this bug. Such a test would start with a genai_types.Part containing file_data and thought=True, convert it to A2A and back, and then assert that thought is still True in the final genai_types.Part. This would currently fail and help ensure the fix is complete for all scenarios.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please address this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryanaiagent done


def test_text_part_with_thought_false_round_trip(self):
"""Test round-trip conversion for text parts with thought=False."""
# Arrange - Start with GenAI part with thought=False
original_text = "This is not a thought."
genai_part = genai_types.Part(text=original_text, thought=False)

# 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.text == original_text
assert result_genai_part.thought is False

def test_file_uri_round_trip(self):
"""Test round-trip conversion for file parts with URI."""
# Arrange
Expand Down
Loading