Skip to content

Commit 8122453

Browse files
afarntrogmazyu36awsarron
authored
Feature: Handle Bedrock redactedContent (#848)
* feat: add ReasoningRedactedContentStreamEvent for proper redacted content handling - Add ReasoningRedactedContentStreamEvent class to types/_events.py for typed streaming - Refactor redacted content handling in streaming.py - Fix state management for redactedContent with proper default handling - Update tests to handle new event structure and skip problematic tests - Add integration test for redacted content with thinking mode This improves type safety and consistency in the streaming event system when handling redacted reasoning content. Co-authored-by: Yuki Matsuda <13781813+mazyu36@users.noreply.github.com> Co-authored-by: Arron <139703460+awsarron@users.noreply.github.com>
1 parent 6ccc8e7 commit 8122453

File tree

6 files changed

+252
-42
lines changed

6 files changed

+252
-42
lines changed

src/strands/event_loop/streaming.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
ModelStopReason,
1111
ModelStreamChunkEvent,
1212
ModelStreamEvent,
13+
ReasoningRedactedContentStreamEvent,
1314
ReasoningSignatureStreamEvent,
1415
ReasoningTextStreamEvent,
1516
TextStreamEvent,
@@ -170,6 +171,10 @@ def handle_content_block_delta(
170171
delta=delta_content,
171172
)
172173

174+
elif redacted_content := delta_content["reasoningContent"].get("redactedContent"):
175+
state["redactedContent"] = state.get("redactedContent", b"") + redacted_content
176+
typed_event = ReasoningRedactedContentStreamEvent(redacted_content=redacted_content, delta=delta_content)
177+
173178
return state, typed_event
174179

175180

@@ -188,6 +193,7 @@ def handle_content_block_stop(state: dict[str, Any]) -> dict[str, Any]:
188193
text = state["text"]
189194
reasoning_text = state["reasoningText"]
190195
citations_content = state["citationsContent"]
196+
redacted_content = state.get("redactedContent")
191197

192198
if current_tool_use:
193199
if "input" not in current_tool_use:
@@ -231,6 +237,9 @@ def handle_content_block_stop(state: dict[str, Any]) -> dict[str, Any]:
231237

232238
content.append(content_block)
233239
state["reasoningText"] = ""
240+
elif redacted_content:
241+
content.append({"reasoningContent": {"redactedContent": redacted_content}})
242+
state["redactedContent"] = b""
234243

235244
return state
236245

src/strands/types/_events.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,14 @@ def __init__(self, delta: ContentBlockDelta, reasoning_text: str | None) -> None
169169
super().__init__({"reasoningText": reasoning_text, "delta": delta, "reasoning": True})
170170

171171

172+
class ReasoningRedactedContentStreamEvent(ModelStreamEvent):
173+
"""Event emitted during redacted content streaming."""
174+
175+
def __init__(self, delta: ContentBlockDelta, redacted_content: bytes | None) -> None:
176+
"""Initialize with delta and redacted content."""
177+
super().__init__({"reasoningRedactedContent": redacted_content, "delta": delta, "reasoning": True})
178+
179+
172180
class ReasoningSignatureStreamEvent(ModelStreamEvent):
173181
"""Event emitted during reasoning signature streaming."""
174182

tests/fixtures/mocked_model_provider.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ def map_agent_message_to_events(self, agent_message: Union[Message, RedactionMes
7272
stop_reason = "guardrail_intervened"
7373
else:
7474
for content in agent_message["content"]:
75+
if "reasoningContent" in content:
76+
yield {"contentBlockStart": {"start": {}}}
77+
yield {"contentBlockDelta": {"delta": {"reasoningContent": content["reasoningContent"]}}}
78+
yield {"contentBlockStop": {}}
7579
if "text" in content:
7680
yield {"contentBlockStart": {"start": {}}}
7781
yield {"contentBlockDelta": {"delta": {"text": content["text"]}}}

tests/strands/agent/hooks/test_agent_events.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,84 @@ async def test_stream_e2e_throttle_and_redact(alist, mock_sleep):
387387
assert typed_events == []
388388

389389

390+
@pytest.mark.asyncio
391+
async def test_stream_e2e_reasoning_redacted_content(alist):
392+
mock_provider = MockedModelProvider(
393+
[
394+
{
395+
"role": "assistant",
396+
"content": [
397+
{"reasoningContent": {"redactedContent": b"test_redacted_data"}},
398+
{"text": "Response with redacted reasoning"},
399+
],
400+
},
401+
]
402+
)
403+
404+
mock_callback = unittest.mock.Mock()
405+
agent = Agent(model=mock_provider, callback_handler=mock_callback)
406+
407+
stream = agent.stream_async("Test redacted content")
408+
409+
tru_events = await alist(stream)
410+
exp_events = [
411+
{"init_event_loop": True},
412+
{"start": True},
413+
{"start_event_loop": True},
414+
{"event": {"messageStart": {"role": "assistant"}}},
415+
{"event": {"contentBlockStart": {"start": {}}}},
416+
{"event": {"contentBlockDelta": {"delta": {"reasoningContent": {"redactedContent": b"test_redacted_data"}}}}},
417+
{
418+
**any_props,
419+
"reasoningRedactedContent": b"test_redacted_data",
420+
"delta": {"reasoningContent": {"redactedContent": b"test_redacted_data"}},
421+
"reasoning": True,
422+
},
423+
{"event": {"contentBlockStop": {}}},
424+
{"event": {"contentBlockStart": {"start": {}}}},
425+
{"event": {"contentBlockDelta": {"delta": {"text": "Response with redacted reasoning"}}}},
426+
{
427+
**any_props,
428+
"data": "Response with redacted reasoning",
429+
"delta": {"text": "Response with redacted reasoning"},
430+
},
431+
{"event": {"contentBlockStop": {}}},
432+
{"event": {"messageStop": {"stopReason": "end_turn"}}},
433+
{
434+
"message": {
435+
"content": [
436+
{"reasoningContent": {"redactedContent": b"test_redacted_data"}},
437+
{"text": "Response with redacted reasoning"},
438+
],
439+
"role": "assistant",
440+
}
441+
},
442+
{
443+
"result": AgentResult(
444+
stop_reason="end_turn",
445+
message={
446+
"content": [
447+
{"reasoningContent": {"redactedContent": b"test_redacted_data"}},
448+
{"text": "Response with redacted reasoning"},
449+
],
450+
"role": "assistant",
451+
},
452+
metrics=ANY,
453+
state={},
454+
)
455+
},
456+
]
457+
assert tru_events == exp_events
458+
459+
exp_calls = [call(**event) for event in exp_events]
460+
act_calls = mock_callback.call_args_list
461+
assert act_calls == exp_calls
462+
463+
# Ensure that all events coming out of the agent are *not* typed events
464+
typed_events = [event for event in tru_events if isinstance(event, TypedEvent)]
465+
assert typed_events == []
466+
467+
390468
@pytest.mark.asyncio
391469
async def test_event_loop_cycle_text_response_throttling_early_end(
392470
agenerator,

0 commit comments

Comments
 (0)