Skip to content

Commit ae1175e

Browse files
committed
nested property support
1 parent fb837a4 commit ae1175e

File tree

2 files changed

+108
-3
lines changed

2 files changed

+108
-3
lines changed

src/strands/models/bedrock.py

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,37 @@
8989
}
9090
_BEDROCK_CONTENT_BLOCK_TYPES: set[_ContentBlockType] = set(_BEDROCK_CONTENT_BLOCK_FIELDS.keys())
9191

92+
# Nested schemas for deep filtering of Bedrock content blocks
93+
_BEDROCK_CONTENT_BLOCK_SCHEMAS: dict[_ContentBlockType, dict[str, Any]] = {
94+
"image": {
95+
"format": True,
96+
"source": {"bytes": True, "s3Location": {"bucket": True, "key": True, "region": True, "version": True}},
97+
},
98+
"toolResult": {"content": True, "toolUseId": True, "status": True},
99+
"toolUse": {"input": True, "name": True, "toolUseId": True},
100+
"document": {
101+
"name": True,
102+
"source": {"bytes": True, "s3Location": {"bucket": True, "key": True, "region": True, "version": True}},
103+
"format": True,
104+
"citations": True,
105+
"context": True,
106+
},
107+
"video": {
108+
"format": True,
109+
"source": {"bytes": True, "s3Location": {"bucket": True, "key": True, "region": True, "version": True}},
110+
},
111+
"reasoningContent": {"reasoningText": {"text": True, "signature": True}, "redactedContent": True},
112+
"citationsContent": {"citations": True, "content": True},
113+
"cachePoint": {"type": True},
114+
"guardContent": {
115+
"image": {
116+
"format": True,
117+
"source": {"bytes": True, "s3Location": {"bucket": True, "key": True, "region": True, "version": True}},
118+
},
119+
"text": {"qualifiers": True, "text": True},
120+
},
121+
}
122+
92123
T = TypeVar("T", bound=BaseModel)
93124

94125
DEFAULT_READ_TIMEOUT = 120
@@ -369,12 +400,12 @@ def _format_bedrock_messages(self, messages: Messages) -> Messages:
369400
# Should only be one block type per content block since it is a discriminated union
370401
block_type = cast(_ContentBlockType, next(iter(filterable_block_types)))
371402
block_data = content_block[block_type]
372-
allowed_fields = _BEDROCK_CONTENT_BLOCK_FIELDS[block_type].copy()
403+
schema = _BEDROCK_CONTENT_BLOCK_SCHEMAS[block_type].copy()
373404

374405
if block_type == "toolResult" and not self._should_include_tool_result_status():
375-
allowed_fields.discard("status")
406+
schema.pop("status", None)
376407

377-
cleaned_data = {k: v for k, v in block_data.items() if k in allowed_fields}
408+
cleaned_data = _deep_filter(block_data, schema)
378409
cleaned_content.append(cast(ContentBlock, {block_type: cleaned_data}))
379410
else:
380411
# Keep other content blocks as-is
@@ -805,3 +836,34 @@ async def structured_output(
805836
raise ValueError("No valid tool use or tool use input was found in the Bedrock response.")
806837

807838
yield {"output": output_model(**output_response)}
839+
840+
841+
def _deep_filter(data: Union[dict[str, Any], Any], schema: dict[str, Any]) -> dict[str, Any]:
842+
"""Fast recursive filtering using nested dict schemas.
843+
844+
Args:
845+
data: Input data to filter (content block or nested dict)
846+
schema: Schema defining allowed fields and nested structure
847+
848+
Returns:
849+
Filtered dictionary containing only schema-defined fields
850+
"""
851+
if not isinstance(data, dict):
852+
return {}
853+
854+
result = {}
855+
for key in data.keys() & schema.keys():
856+
value = data[key]
857+
schema_spec = schema[key]
858+
859+
if schema_spec is True:
860+
result[key] = value
861+
elif isinstance(schema_spec, dict) and isinstance(value, dict):
862+
filtered = _deep_filter(value, schema_spec)
863+
if filtered:
864+
result[key] = filtered
865+
elif isinstance(schema_spec, dict) and isinstance(value, list):
866+
result[key] = [_deep_filter(item, schema_spec) for item in value if isinstance(item, dict)]
867+
else:
868+
result[key] = value
869+
return result

tests/strands/models/test_bedrock.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1488,6 +1488,28 @@ def test_format_request_filters_image_content_blocks(model, model_id):
14881488
assert "metadata" not in image_block
14891489

14901490

1491+
def test_format_request_filters_nested_image_s3_fields(model, model_id):
1492+
"""Test deep filtering of nested s3Location fields in image blocks."""
1493+
messages = [
1494+
{
1495+
"role": "user",
1496+
"content": [
1497+
{
1498+
"image": {
1499+
"format": "png",
1500+
"source": {"s3Location": {"bucket": "my-bucket", "key": "image.png", "extraField": "filtered"}},
1501+
}
1502+
}
1503+
],
1504+
}
1505+
]
1506+
1507+
formatted_request = model.format_request(messages)
1508+
s3_location = formatted_request["messages"][0]["content"][0]["image"]["source"]["s3Location"]
1509+
1510+
assert s3_location == {"bucket": "my-bucket", "key": "image.png"}
1511+
1512+
14911513
def test_format_request_filters_document_content_blocks(model, model_id):
14921514
"""Test that format_request filters extra fields from document content blocks."""
14931515
messages = [
@@ -1516,6 +1538,27 @@ def test_format_request_filters_document_content_blocks(model, model_id):
15161538
assert "metadata" not in document_block
15171539

15181540

1541+
def test_format_request_filters_nested_reasoning_content(model, model_id):
1542+
"""Test deep filtering of nested reasoningText fields."""
1543+
messages = [
1544+
{
1545+
"role": "assistant",
1546+
"content": [
1547+
{
1548+
"reasoningContent": {
1549+
"reasoningText": {"text": "thinking...", "signature": "abc123", "extraField": "filtered"}
1550+
}
1551+
}
1552+
],
1553+
}
1554+
]
1555+
1556+
formatted_request = model.format_request(messages)
1557+
reasoning_text = formatted_request["messages"][0]["content"][0]["reasoningContent"]["reasoningText"]
1558+
1559+
assert reasoning_text == {"text": "thinking...", "signature": "abc123"}
1560+
1561+
15191562
def test_format_request_filters_video_content_blocks(model, model_id):
15201563
"""Test that format_request filters extra fields from video content blocks."""
15211564
messages = [

0 commit comments

Comments
 (0)