Skip to content
Open
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
14 changes: 10 additions & 4 deletions src/bedrock_agentcore/runtime/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Bedrock AgentCore runtime utilities for object conversion and serialization."""

import base64
from dataclasses import asdict, is_dataclass
from typing import Any

Expand All @@ -10,13 +11,14 @@ def convert_complex_objects(obj: Any, _depth: int = 0) -> Any:
if _depth > 50:
return f"<too_deep:{type(obj).__name__}>"

# Handle Pydantic models (like AIMessage)
# Handle Pydantic models (like AIMessage). The dump can still hold bytes or
# sets, so it is converted too.
if hasattr(obj, "model_dump"):
return obj.model_dump()
return convert_complex_objects(obj.model_dump(), _depth + 1)

# Handle dataclasses (like AgentResult)
# Handle dataclasses (like AgentResult), converting the dump for the same reason
elif is_dataclass(obj):
return asdict(obj)
return convert_complex_objects(asdict(obj), _depth + 1)

# Handle dictionaries recursively
elif isinstance(obj, dict):
Expand All @@ -30,6 +32,10 @@ def convert_complex_objects(obj: Any, _depth: int = 0) -> Any:
elif isinstance(obj, set):
return [convert_complex_objects(item, _depth + 1) for item in obj]

# Handle binary data (base64, since JSON has no bytes type)
elif isinstance(obj, (bytes, bytearray)):
return base64.b64encode(obj).decode("ascii")

# Return primitives as-is
else:
return obj
Expand Down
8 changes: 8 additions & 0 deletions tests/bedrock_agentcore/runtime/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1059,6 +1059,14 @@ def test_convert_to_sse_json_serializable_data(self):
parsed_data = json.loads(json_part)
assert parsed_data == test_data

def test_convert_to_sse_streams_bytes_as_json_object(self):
"""An event holding bytes must arrive as a JSON object, not a string of its repr (#659)."""
app = BedrockAgentCoreApp()

sse_string = app._convert_to_sse({"result": {"redactedContent": b"abc"}}).decode("utf-8")

assert json.loads(sse_string[6:-2]) == {"result": {"redactedContent": "YWJj"}}

def test_convert_to_sse_non_serializable_object(self):
"""Test that non-JSON-serializable objects trigger error handling."""
app = BedrockAgentCoreApp()
Expand Down
28 changes: 28 additions & 0 deletions tests/bedrock_agentcore/runtime/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for Bedrock AgentCore runtime utilities."""

import json
from dataclasses import dataclass
from typing import List, Optional

Expand Down Expand Up @@ -178,6 +179,33 @@ def test_sets(self):
assert "b" in result
assert "c" in result

def test_bytes_become_base64_anywhere_in_the_tree(self):
"""Bytes must end up JSON-serializable, including inside dataclasses and Pydantic models."""

@dataclass
class Block:
data: bytes

class Blob(BaseModel):
data: bytes

event = {
"raw": b"abc",
"buffer": bytearray(b"\x00\xff"),
"dataclass": Block(data=b"abc"),
"pydantic": Blob(data=b"abc"),
}

result = convert_complex_objects(event)

assert result == {
"raw": "YWJj",
"buffer": "AP8=",
"dataclass": {"data": "YWJj"},
"pydantic": {"data": "YWJj"},
}
json.dumps(result)

def test_nested_sets_with_complex_objects(self):
"""Test sets containing hashable objects (complex objects can't be in sets)."""

Expand Down
Loading