Skip to content

Commit 2ffbfd7

Browse files
aianchJackYPCOnline
authored andcommitted
fix(bedrock): include assistant response in guardrail_last_turn_only context
1 parent 8942c98 commit 2ffbfd7

File tree

3 files changed

+81
-18
lines changed

3 files changed

+81
-18
lines changed

src/strands/models/bedrock.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -311,14 +311,14 @@ def _format_request(
311311
def _get_last_turn_messages(self, messages: Messages) -> Messages:
312312
"""Get the last turn messages for guardrail evaluation.
313313
314-
Returns the latest user message and the previous assistant message (if it exists).
314+
Returns the latest user message and the assistant's response (if it exists).
315315
This reduces the conversation context sent to guardrails when guardrail_last_turn_only is True.
316316
317317
Args:
318318
messages: Full conversation messages.
319319
320320
Returns:
321-
Messages containing only the last turn (user + previous assistant if exists).
321+
Messages containing only the last turn (user + assistant response if exists).
322322
"""
323323
if not messages:
324324
return []
@@ -334,13 +334,12 @@ def _get_last_turn_messages(self, messages: Messages) -> Messages:
334334
# No user message found, return empty
335335
return []
336336

337-
# Include the previous assistant message if it exists
338-
result_messages: Messages = []
339-
if last_user_index > 0 and messages[last_user_index - 1]["role"] == "assistant":
340-
result_messages.append(messages[last_user_index - 1])
337+
# Start with the last user message
338+
result_messages: Messages = [messages[last_user_index]]
341339

342-
# Add the last user message
343-
result_messages.append(messages[last_user_index])
340+
# Include the assistant's response if it exists (the message after the user message)
341+
if last_user_index < len(messages) - 1 and messages[last_user_index + 1]["role"] == "assistant":
342+
result_messages.append(messages[last_user_index + 1])
344343

345344
return result_messages
346345

tests/strands/models/test_bedrock.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2057,29 +2057,43 @@ def test_get_last_turn_messages(model):
20572057
# Test empty messages
20582058
assert model._get_last_turn_messages([]) == []
20592059

2060-
# Test single user message
2060+
# Test single user message (no assistant response yet)
20612061
messages = [{"role": "user", "content": [{"text": "Hello"}]}]
20622062
result = model._get_last_turn_messages(messages)
20632063
assert len(result) == 1
20642064
assert result[0]["role"] == "user"
20652065

2066-
# Test user-assistant pair
2066+
# Test user-assistant pair (user message with assistant response)
20672067
messages = [
20682068
{"role": "user", "content": [{"text": "Hello"}]},
20692069
{"role": "assistant", "content": [{"text": "Hi"}]},
20702070
{"role": "user", "content": [{"text": "How are you?"}]},
2071+
{"role": "assistant", "content": [{"text": "I'm doing well"}]},
20712072
]
20722073
result = model._get_last_turn_messages(messages)
20732074
assert len(result) == 2
2074-
assert result[0]["role"] == "assistant"
2075-
assert result[1]["role"] == "user"
2076-
assert result[1]["content"][0]["text"] == "How are you?"
2075+
assert result[0]["role"] == "user"
2076+
assert result[0]["content"][0]["text"] == "How are you?"
2077+
assert result[1]["role"] == "assistant"
2078+
assert result[1]["content"][0]["text"] == "I'm doing well"
2079+
2080+
# Test last user message without assistant response
2081+
messages = [
2082+
{"role": "user", "content": [{"text": "Hello"}]},
2083+
{"role": "assistant", "content": [{"text": "Hi"}]},
2084+
{"role": "user", "content": [{"text": "How are you?"}]},
2085+
]
2086+
result = model._get_last_turn_messages(messages)
2087+
assert len(result) == 1
2088+
assert result[0]["role"] == "user"
2089+
assert result[0]["content"][0]["text"] == "How are you?"
20772090

20782091

20792092
def test_format_request_with_guardrail_last_turn_only(model, model_id):
20802093
"""Test _format_request uses filtered messages when guardrail_last_turn_only=True."""
20812094
model.update_config(guardrail_id="test-guardrail", guardrail_version="DRAFT", guardrail_last_turn_only=True)
20822095

2096+
# Test with last user message only (no assistant response yet)
20832097
messages = [
20842098
{"role": "user", "content": [{"text": "First message"}]},
20852099
{"role": "assistant", "content": [{"text": "First response"}]},
@@ -2088,12 +2102,29 @@ def test_format_request_with_guardrail_last_turn_only(model, model_id):
20882102

20892103
request = model._format_request(messages)
20902104

2091-
# Should only include the last turn (assistant + user)
2105+
# Should only include the last user message (no assistant response after it yet)
20922106
formatted_messages = request["messages"]
2093-
assert len(formatted_messages) == 2
2094-
assert formatted_messages[0]["role"] == "assistant"
2095-
assert formatted_messages[1]["role"] == "user"
2096-
assert formatted_messages[1]["content"][0]["text"] == "Latest message"
2107+
assert len(formatted_messages) == 1
2108+
assert formatted_messages[0]["role"] == "user"
2109+
assert formatted_messages[0]["content"][0]["text"] == "Latest message"
2110+
2111+
# Test with last user message + assistant response
2112+
messages_with_response = [
2113+
{"role": "user", "content": [{"text": "First message"}]},
2114+
{"role": "assistant", "content": [{"text": "First response"}]},
2115+
{"role": "user", "content": [{"text": "How are you?"}]},
2116+
{"role": "assistant", "content": [{"text": "I'm good"}]},
2117+
]
2118+
2119+
request2 = model._format_request(messages_with_response)
2120+
2121+
# Should include last user + assistant response
2122+
formatted_messages2 = request2["messages"]
2123+
assert len(formatted_messages2) == 2
2124+
assert formatted_messages2[0]["role"] == "user"
2125+
assert formatted_messages2[0]["content"][0]["text"] == "How are you?"
2126+
assert formatted_messages2[1]["role"] == "assistant"
2127+
assert formatted_messages2[1]["content"][0]["text"] == "I'm good"
20972128

20982129

20992130
@pytest.mark.asyncio

tests_integ/test_bedrock_guardrails.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,39 @@ def test_guardrail_last_turn_only_recovery_scenario(boto_session, bedrock_guardr
343343
assert len(agent.messages) == 4 # 2 user + 2 assistant messages
344344

345345

346+
def test_guardrail_last_turn_only_output_intervention(boto_session, bedrock_guardrail):
347+
"""Test that guardrail_last_turn_only works with OUTPUT guardrails.
348+
349+
This tests that when the assistant tries to output blocked content,
350+
the OUTPUT guardrail intervenes, even with guardrail_last_turn_only=True.
351+
Then verifies that subsequent normal responses work correctly.
352+
"""
353+
bedrock_model = BedrockModel(
354+
guardrail_id=bedrock_guardrail,
355+
guardrail_version="DRAFT",
356+
guardrail_last_turn_only=True,
357+
guardrail_stream_processing_mode="sync",
358+
boto_session=boto_session,
359+
)
360+
361+
agent = Agent(
362+
model=bedrock_model,
363+
system_prompt="When asked to say the word, say CACTUS. Otherwise respond normally.",
364+
callback_handler=None,
365+
load_tools_from_directory=False,
366+
)
367+
368+
# First turn - assistant tries to output "CACTUS", should be blocked by OUTPUT guardrail
369+
response1 = agent("Say the word.")
370+
assert response1.stop_reason == "guardrail_intervened"
371+
assert BLOCKED_OUTPUT in str(response1)
372+
373+
# Second turn - normal question should work fine
374+
response2 = agent("What is 2+2?")
375+
assert response2.stop_reason != "guardrail_intervened"
376+
assert BLOCKED_OUTPUT not in str(response2)
377+
378+
346379
def test_guardrail_input_intervention_properly_redacts_in_session(boto_session, bedrock_guardrail, temp_dir):
347380
bedrock_model = BedrockModel(
348381
guardrail_id=bedrock_guardrail,

0 commit comments

Comments
 (0)