Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
13 changes: 10 additions & 3 deletions src/strands/multiagent/a2a/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
streamed requests to the A2AServer.
"""

import base64
import json
import logging
import mimetypes
Expand Down Expand Up @@ -274,12 +275,18 @@ def _convert_a2a_parts_to_content_blocks(self, parts: list[Part]) -> list[Conten
uri_data = getattr(file_obj, "uri", None)

if bytes_data:
try:
# A2A bytes are always base64-encoded strings
decoded_bytes = base64.b64decode(bytes_data)
except Exception as e:
raise ValueError(f"Failed to decode base64 data for file '{raw_file_name}': {e}") from e

if file_type == "image":
content_blocks.append(
ContentBlock(
image=ImageContent(
format=file_format, # type: ignore
source=ImageSource(bytes=bytes_data),
source=ImageSource(bytes=decoded_bytes),
)
)
)
Expand All @@ -288,7 +295,7 @@ def _convert_a2a_parts_to_content_blocks(self, parts: list[Part]) -> list[Conten
ContentBlock(
video=VideoContent(
format=file_format, # type: ignore
source=VideoSource(bytes=bytes_data),
source=VideoSource(bytes=decoded_bytes),
)
)
)
Expand All @@ -298,7 +305,7 @@ def _convert_a2a_parts_to_content_blocks(self, parts: list[Part]) -> list[Conten
document=DocumentContent(
format=file_format, # type: ignore
name=file_name,
source=DocumentSource(bytes=bytes_data),
source=DocumentSource(bytes=decoded_bytes),
)
)
)
Expand Down
101 changes: 61 additions & 40 deletions tests/strands/multiagent/a2a/test_executor.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
"""Tests for the StrandsA2AExecutor class."""

import base64
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from a2a.types import InternalError, UnsupportedOperationError
from a2a.types import DataPart, FilePart, InternalError, TextPart, UnsupportedOperationError
from a2a.utils.errors import ServerError

from strands.agent.agent_result import AgentResult as SAAgentResult
from strands.multiagent.a2a.executor import StrandsA2AExecutor
from strands.types.content import ContentBlock

# Test data constants
VALID_PNG_BYTES = b"fake_png_data"
VALID_MP4_BYTES = b"fake_mp4_data"
VALID_DOCUMENT_BYTES = b"fake_document_data"


def test_executor_initialization(mock_strands_agent):
"""Test that StrandsA2AExecutor initializes correctly."""
Expand Down Expand Up @@ -96,18 +102,15 @@ def test_convert_a2a_parts_to_content_blocks_text_part():

def test_convert_a2a_parts_to_content_blocks_file_part_image_bytes():
"""Test conversion of FilePart with image bytes to ContentBlock."""
from a2a.types import FilePart

executor = StrandsA2AExecutor(MagicMock())

# Create test image bytes (no base64 encoding needed)
test_bytes = b"fake_image_data"
base64_bytes = base64.b64encode(VALID_PNG_BYTES).decode("utf-8")

# Mock file object
file_obj = MagicMock()
file_obj.name = "test_image.jpeg"
file_obj.mime_type = "image/jpeg"
file_obj.bytes = test_bytes
file_obj.name = "test_image.png"
file_obj.mime_type = "image/png"
file_obj.bytes = base64_bytes
file_obj.uri = None

# Mock FilePart with proper spec
Expand All @@ -123,24 +126,21 @@ def test_convert_a2a_parts_to_content_blocks_file_part_image_bytes():
assert len(result) == 1
content_block = result[0]
assert "image" in content_block
assert content_block["image"]["format"] == "jpeg"
assert content_block["image"]["source"]["bytes"] == test_bytes
assert content_block["image"]["format"] == "png"
assert content_block["image"]["source"]["bytes"] == VALID_PNG_BYTES


def test_convert_a2a_parts_to_content_blocks_file_part_video_bytes():
"""Test conversion of FilePart with video bytes to ContentBlock."""
from a2a.types import FilePart

executor = StrandsA2AExecutor(MagicMock())

# Create test video bytes (no base64 encoding needed)
test_bytes = b"fake_video_data"
base64_bytes = base64.b64encode(VALID_MP4_BYTES).decode("utf-8")

# Mock file object
file_obj = MagicMock()
file_obj.name = "test_video.mp4"
file_obj.mime_type = "video/mp4"
file_obj.bytes = test_bytes
file_obj.bytes = base64_bytes
file_obj.uri = None

# Mock FilePart with proper spec
Expand All @@ -157,23 +157,20 @@ def test_convert_a2a_parts_to_content_blocks_file_part_video_bytes():
content_block = result[0]
assert "video" in content_block
assert content_block["video"]["format"] == "mp4"
assert content_block["video"]["source"]["bytes"] == test_bytes
assert content_block["video"]["source"]["bytes"] == VALID_MP4_BYTES


def test_convert_a2a_parts_to_content_blocks_file_part_document_bytes():
"""Test conversion of FilePart with document bytes to ContentBlock."""
from a2a.types import FilePart

executor = StrandsA2AExecutor(MagicMock())

# Create test document bytes (no base64 encoding needed)
test_bytes = b"fake_document_data"
base64_bytes = base64.b64encode(VALID_DOCUMENT_BYTES).decode("utf-8")

# Mock file object
file_obj = MagicMock()
file_obj.name = "test_document.pdf"
file_obj.mime_type = "application/pdf"
file_obj.bytes = test_bytes
file_obj.bytes = base64_bytes
file_obj.uri = None

# Mock FilePart with proper spec
Expand All @@ -191,7 +188,7 @@ def test_convert_a2a_parts_to_content_blocks_file_part_document_bytes():
assert "document" in content_block
assert content_block["document"]["format"] == "pdf"
assert content_block["document"]["name"] == "test_document"
assert content_block["document"]["source"]["bytes"] == test_bytes
assert content_block["document"]["source"]["bytes"] == VALID_DOCUMENT_BYTES


def test_convert_a2a_parts_to_content_blocks_file_part_uri():
Expand Down Expand Up @@ -226,15 +223,15 @@ def test_convert_a2a_parts_to_content_blocks_file_part_uri():

def test_convert_a2a_parts_to_content_blocks_file_part_with_bytes():
"""Test conversion of FilePart with bytes data."""
from a2a.types import FilePart

executor = StrandsA2AExecutor(MagicMock())

base64_bytes = base64.b64encode(VALID_PNG_BYTES).decode("utf-8")

# Mock file object with bytes (no validation needed since no decoding)
file_obj = MagicMock()
file_obj.name = "test_image.png"
file_obj.mime_type = "image/png"
file_obj.bytes = b"some_binary_data"
file_obj.bytes = base64_bytes
file_obj.uri = None

# Mock FilePart with proper spec
Expand All @@ -250,7 +247,34 @@ def test_convert_a2a_parts_to_content_blocks_file_part_with_bytes():
assert len(result) == 1
content_block = result[0]
assert "image" in content_block
assert content_block["image"]["source"]["bytes"] == b"some_binary_data"
assert content_block["image"]["source"]["bytes"] == VALID_PNG_BYTES


def test_convert_a2a_parts_to_content_blocks_file_part_invalid_base64():
"""Test conversion of FilePart with invalid base64 data raises ValueError."""
executor = StrandsA2AExecutor(MagicMock())

# Invalid base64 string - contains invalid characters
invalid_base64 = "SGVsbG8gV29ybGQ@#$%"

# Mock file object with invalid base64 bytes
file_obj = MagicMock()
file_obj.name = "test.txt"
file_obj.mime_type = "text/plain"
file_obj.bytes = invalid_base64
file_obj.uri = None

# Mock FilePart
file_part = MagicMock(spec=FilePart)
file_part.file = file_obj
part = MagicMock()
part.root = file_part

# Should handle the base64 decode error gracefully and return empty list
result = executor._convert_a2a_parts_to_content_blocks([part])
assert isinstance(result, list)
# The part should be skipped due to base64 decode error
assert len(result) == 0


def test_convert_a2a_parts_to_content_blocks_data_part():
Expand Down Expand Up @@ -704,15 +728,15 @@ def test_convert_a2a_parts_to_content_blocks_empty_list():

def test_convert_a2a_parts_to_content_blocks_file_part_no_name():
"""Test conversion of FilePart with no file name."""
from a2a.types import FilePart

executor = StrandsA2AExecutor(MagicMock())

base64_bytes = base64.b64encode(VALID_DOCUMENT_BYTES).decode("utf-8")

# Mock file object without name
file_obj = MagicMock()
delattr(file_obj, "name") # Remove name attribute
file_obj.mime_type = "text/plain"
file_obj.bytes = b"test content"
file_obj.bytes = base64_bytes
file_obj.uri = None

# Mock FilePart with proper spec
Expand All @@ -733,15 +757,15 @@ def test_convert_a2a_parts_to_content_blocks_file_part_no_name():

def test_convert_a2a_parts_to_content_blocks_file_part_no_mime_type():
"""Test conversion of FilePart with no MIME type."""
from a2a.types import FilePart

executor = StrandsA2AExecutor(MagicMock())

base64_bytes = base64.b64encode(VALID_DOCUMENT_BYTES).decode("utf-8")

# Mock file object without MIME type
file_obj = MagicMock()
file_obj.name = "test_file"
delattr(file_obj, "mime_type")
file_obj.bytes = b"test content"
file_obj.bytes = base64_bytes
file_obj.uri = None

# Mock FilePart with proper spec
Expand Down Expand Up @@ -837,7 +861,6 @@ async def test_execute_streaming_mode_raises_error_for_empty_content_blocks(
@pytest.mark.asyncio
async def test_execute_with_mixed_part_types(mock_strands_agent, mock_request_context, mock_event_queue):
"""Test execute with a message containing mixed A2A part types."""
from a2a.types import DataPart, FilePart, TextPart

async def mock_stream(content_blocks):
"""Mock streaming function."""
Expand Down Expand Up @@ -866,7 +889,7 @@ async def mock_stream(content_blocks):
file_obj = MagicMock()
file_obj.name = "image.png"
file_obj.mime_type = "image/png"
file_obj.bytes = b"fake_image"
file_obj.bytes = base64.b64encode(VALID_PNG_BYTES).decode("utf-8")
file_obj.uri = None
file_part = MagicMock(spec=FilePart)
file_part.file = file_obj
Expand Down Expand Up @@ -907,8 +930,6 @@ def test_integration_example():

This test serves as documentation for the conversion functionality.
"""
from a2a.types import DataPart, FilePart, TextPart

executor = StrandsA2AExecutor(MagicMock())

# Example 1: Text content
Expand All @@ -918,7 +939,7 @@ def test_integration_example():
text_part_mock.root = text_part

# Example 2: Image file
image_bytes = b"fake_image_content"
image_bytes = base64.b64encode(VALID_PNG_BYTES).decode("utf-8")
image_file = MagicMock()
image_file.name = "photo.jpg"
image_file.mime_type = "image/jpeg"
Expand All @@ -931,7 +952,7 @@ def test_integration_example():
image_part_mock.root = image_part

# Example 3: Document file
doc_bytes = b"PDF document content"
doc_bytes = base64.b64encode(VALID_DOCUMENT_BYTES).decode("utf-8")
doc_file = MagicMock()
doc_file.name = "report.pdf"
doc_file.mime_type = "application/pdf"
Expand Down Expand Up @@ -962,13 +983,13 @@ def test_integration_example():
# Image part becomes image ContentBlock with proper format and bytes
assert "image" in content_blocks[1]
assert content_blocks[1]["image"]["format"] == "jpeg"
assert content_blocks[1]["image"]["source"]["bytes"] == image_bytes
assert content_blocks[1]["image"]["source"]["bytes"] == VALID_PNG_BYTES

# Document part becomes document ContentBlock
assert "document" in content_blocks[2]
assert content_blocks[2]["document"]["format"] == "pdf"
assert content_blocks[2]["document"]["name"] == "report" # Extension stripped
assert content_blocks[2]["document"]["source"]["bytes"] == doc_bytes
assert content_blocks[2]["document"]["source"]["bytes"] == VALID_DOCUMENT_BYTES

# Data part becomes text ContentBlock with JSON representation
assert "text" in content_blocks[3]
Expand Down
Loading
Loading