Skip to content

Commit 29f246e

Browse files
committed
feat: add prompts to conversational history when using structured output
- Modified structured_output_async to add prompts to conversation history - Modified structured_output_async to add structured output results to conversation history - Updated docstrings to reflect new behavior - Updated all related tests to verify conversation history is updated - Updated hook tests to expect MessageAddedEvent for prompts and outputs - Maintains backward compatibility Resolves strands-agents#810
1 parent 7d51d73 commit 29f246e

3 files changed

Lines changed: 137 additions & 28 deletions

File tree

src/strands/agent/agent.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ async def invoke_async(self, prompt: AgentInput = None, **kwargs: Any) -> AgentR
441441
def structured_output(self, output_model: Type[T], prompt: AgentInput = None) -> T:
442442
"""This method allows you to get structured output from the agent.
443443
444-
If you pass in a prompt, it will be used temporarily without adding it to the conversation history.
444+
If you pass in a prompt, it will be added to the conversation history along with the structured output result.
445445
If you don't pass in a prompt, it will use only the existing conversation history to respond.
446446
447447
For smaller models, you may want to use the optional prompt to add additional instructions to explicitly
@@ -470,7 +470,7 @@ def execute() -> T:
470470
async def structured_output_async(self, output_model: Type[T], prompt: AgentInput = None) -> T:
471471
"""This method allows you to get structured output from the agent.
472472
473-
If you pass in a prompt, it will be used temporarily without adding it to the conversation history.
473+
If you pass in a prompt, it will be added to the conversation history along with the structured output result.
474474
If you don't pass in a prompt, it will use only the existing conversation history to respond.
475475
476476
For smaller models, you may want to use the optional prompt to add additional instructions to explicitly
@@ -479,7 +479,7 @@ async def structured_output_async(self, output_model: Type[T], prompt: AgentInpu
479479
Args:
480480
output_model: The output model (a JSON schema written as a Pydantic BaseModel)
481481
that the agent will use when responding.
482-
prompt: The prompt to use for the agent (will not be added to conversation history).
482+
prompt: The prompt to use for the agent (will be added to conversation history).
483483
484484
Raises:
485485
ValueError: If no conversation history or prompt is provided.
@@ -492,7 +492,13 @@ async def structured_output_async(self, output_model: Type[T], prompt: AgentInpu
492492
if not self.messages and not prompt:
493493
raise ValueError("No conversation history or prompt provided")
494494

495-
temp_messages: Messages = self.messages + self._convert_prompt_to_messages(prompt)
495+
# Add prompt to conversation history if provided
496+
if prompt:
497+
prompt_messages = self._convert_prompt_to_messages(prompt)
498+
for message in prompt_messages:
499+
self._append_message(message)
500+
501+
temp_messages: Messages = self.messages
496502

497503
structured_output_span.set_attributes(
498504
{
@@ -519,7 +525,16 @@ async def structured_output_async(self, output_model: Type[T], prompt: AgentInpu
519525
structured_output_span.add_event(
520526
"gen_ai.choice", attributes={"message": serialize(event["output"].model_dump())}
521527
)
522-
return event["output"]
528+
529+
# Add structured output result to conversation history
530+
result = event["output"]
531+
assistant_message = {
532+
"role": "assistant",
533+
"content": [{"text": f"Structured output ({output_model.__name__}): {result.model_dump_json()}"}]
534+
}
535+
self._append_message(assistant_message)
536+
537+
return result
523538

524539
finally:
525540
self.hooks.invoke_callbacks(AfterInvocationEvent(agent=self))

tests/strands/agent/test_agent.py

Lines changed: 109 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -985,6 +985,9 @@ def test_agent_structured_output(agent, system_prompt, user, agenerator):
985985
agent.tracer = mock_strands_tracer
986986

987987
agent.model.structured_output = unittest.mock.Mock(return_value=agenerator([{"output": user}]))
988+
agent.hooks = unittest.mock.MagicMock()
989+
agent.hooks.invoke_callbacks = unittest.mock.Mock()
990+
agent.callback_handler = unittest.mock.Mock()
988991

989992
prompt = "Jane Doe is 30 years old and her email is jane@doe.com"
990993

@@ -995,12 +998,31 @@ def test_agent_structured_output(agent, system_prompt, user, agenerator):
995998
exp_result = user
996999
assert tru_result == exp_result
9971000

998-
# Verify conversation history is not polluted
999-
assert len(agent.messages) == initial_message_count
1001+
# Verify conversation history is updated with prompt and structured output
1002+
assert len(agent.messages) == initial_message_count + 2
1003+
1004+
# Verify the prompt was added to conversation history
1005+
user_message_added = any(
1006+
msg['role'] == 'user' and prompt in msg['content'][0]['text']
1007+
for msg in agent.messages
1008+
)
1009+
assert user_message_added, "User prompt should be added to conversation history"
1010+
1011+
# Verify the structured output was added to conversation history
1012+
assistant_message_added = any(
1013+
msg['role'] == 'assistant' and 'Structured output (User):' in msg['content'][0]['text']
1014+
for msg in agent.messages
1015+
)
1016+
assert assistant_message_added, "Structured output should be added to conversation history"
10001017

1001-
# Verify the model was called with temporary messages array
1018+
# Verify the model was called with all messages (including the added prompt)
10021019
agent.model.structured_output.assert_called_once_with(
1003-
type(user), [{"role": "user", "content": [{"text": prompt}]}], system_prompt=system_prompt
1020+
type(user),
1021+
[
1022+
{"role": "user", "content": [{"text": prompt}]},
1023+
{"role": "assistant", "content": [{"text": f"Structured output (User): {user.model_dump_json()}"}]}
1024+
],
1025+
system_prompt=system_prompt
10041026
)
10051027

10061028
mock_span.set_attributes.assert_called_once_with(
@@ -1052,12 +1074,31 @@ def test_agent_structured_output_multi_modal_input(agent, system_prompt, user, a
10521074
exp_result = user
10531075
assert tru_result == exp_result
10541076

1055-
# Verify conversation history is not polluted
1056-
assert len(agent.messages) == initial_message_count
1077+
# Verify conversation history is updated with prompt and structured output
1078+
assert len(agent.messages) == initial_message_count + 2
1079+
1080+
# Verify the multi-modal prompt was added to conversation history
1081+
user_message_added = any(
1082+
msg['role'] == 'user' and 'Please describe the user in this image' in msg['content'][0]['text']
1083+
for msg in agent.messages
1084+
)
1085+
assert user_message_added, "Multi-modal user prompt should be added to conversation history"
1086+
1087+
# Verify the structured output was added to conversation history
1088+
assistant_message_added = any(
1089+
msg['role'] == 'assistant' and 'Structured output (User):' in msg['content'][0]['text']
1090+
for msg in agent.messages
1091+
)
1092+
assert assistant_message_added, "Structured output should be added to conversation history"
10571093

1058-
# Verify the model was called with temporary messages array
1094+
# Verify the model was called with all messages (including the added prompt)
10591095
agent.model.structured_output.assert_called_once_with(
1060-
type(user), [{"role": "user", "content": prompt}], system_prompt=system_prompt
1096+
type(user),
1097+
[
1098+
{"role": "user", "content": prompt},
1099+
{"role": "assistant", "content": [{"text": f"Structured output (User): {user.model_dump_json()}"}]}
1100+
],
1101+
system_prompt=system_prompt
10611102
)
10621103

10631104
mock_span.add_event.assert_called_with(
@@ -1069,6 +1110,9 @@ def test_agent_structured_output_multi_modal_input(agent, system_prompt, user, a
10691110
@pytest.mark.asyncio
10701111
async def test_agent_structured_output_in_async_context(agent, user, agenerator):
10711112
agent.model.structured_output = unittest.mock.Mock(return_value=agenerator([{"output": user}]))
1113+
agent.hooks = unittest.mock.MagicMock()
1114+
agent.hooks.invoke_callbacks = unittest.mock.Mock()
1115+
agent.callback_handler = unittest.mock.Mock()
10721116

10731117
prompt = "Jane Doe is 30 years old and her email is jane@doe.com"
10741118

@@ -1079,13 +1123,30 @@ async def test_agent_structured_output_in_async_context(agent, user, agenerator)
10791123
exp_result = user
10801124
assert tru_result == exp_result
10811125

1082-
# Verify conversation history is not polluted
1083-
assert len(agent.messages) == initial_message_count
1126+
# Verify conversation history is updated with prompt and structured output
1127+
assert len(agent.messages) == initial_message_count + 2
1128+
1129+
# Verify the prompt was added to conversation history
1130+
user_message_added = any(
1131+
msg['role'] == 'user' and prompt in msg['content'][0]['text']
1132+
for msg in agent.messages
1133+
)
1134+
assert user_message_added, "User prompt should be added to conversation history"
1135+
1136+
# Verify the structured output was added to conversation history
1137+
assistant_message_added = any(
1138+
msg['role'] == 'assistant' and 'Structured output (User):' in msg['content'][0]['text']
1139+
for msg in agent.messages
1140+
)
1141+
assert assistant_message_added, "Structured output should be added to conversation history"
10841142

10851143

10861144
def test_agent_structured_output_without_prompt(agent, system_prompt, user, agenerator):
10871145
"""Test that structured_output works with existing conversation history and no new prompt."""
10881146
agent.model.structured_output = unittest.mock.Mock(return_value=agenerator([{"output": user}]))
1147+
agent.hooks = unittest.mock.MagicMock()
1148+
agent.hooks.invoke_callbacks = unittest.mock.Mock()
1149+
agent.callback_handler = unittest.mock.Mock()
10891150

10901151
# Add some existing messages to the agent
10911152
existing_messages = [
@@ -1100,17 +1161,27 @@ def test_agent_structured_output_without_prompt(agent, system_prompt, user, agen
11001161
exp_result = user
11011162
assert tru_result == exp_result
11021163

1103-
# Verify conversation history is unchanged
1104-
assert len(agent.messages) == initial_message_count
1105-
assert agent.messages == existing_messages
1164+
# Verify conversation history is updated with structured output only (no prompt added)
1165+
assert len(agent.messages) == initial_message_count + 1
1166+
1167+
# Verify the structured output was added to conversation history
1168+
assistant_message_added = any(
1169+
msg['role'] == 'assistant' and 'Structured output (User):' in msg['content'][0]['text']
1170+
for msg in agent.messages
1171+
)
1172+
assert assistant_message_added, "Structured output should be added to conversation history"
11061173

1107-
# Verify the model was called with existing messages only
1108-
agent.model.structured_output.assert_called_once_with(type(user), existing_messages, system_prompt=system_prompt)
1174+
# Verify the model was called with existing messages plus the added structured output
1175+
expected_messages = existing_messages + [{"role": "assistant", "content": [{"text": f"Structured output (User): {user.model_dump_json()}"}]}]
1176+
agent.model.structured_output.assert_called_once_with(type(user), expected_messages, system_prompt=system_prompt)
11091177

11101178

11111179
@pytest.mark.asyncio
11121180
async def test_agent_structured_output_async(agent, system_prompt, user, agenerator):
11131181
agent.model.structured_output = unittest.mock.Mock(return_value=agenerator([{"output": user}]))
1182+
agent.hooks = unittest.mock.MagicMock()
1183+
agent.hooks.invoke_callbacks = unittest.mock.Mock()
1184+
agent.callback_handler = unittest.mock.Mock()
11141185

11151186
prompt = "Jane Doe is 30 years old and her email is jane@doe.com"
11161187

@@ -1121,12 +1192,31 @@ async def test_agent_structured_output_async(agent, system_prompt, user, agenera
11211192
exp_result = user
11221193
assert tru_result == exp_result
11231194

1124-
# Verify conversation history is not polluted
1125-
assert len(agent.messages) == initial_message_count
1195+
# Verify conversation history is updated with prompt and structured output
1196+
assert len(agent.messages) == initial_message_count + 2
1197+
1198+
# Verify the prompt was added to conversation history
1199+
user_message_added = any(
1200+
msg['role'] == 'user' and prompt in msg['content'][0]['text']
1201+
for msg in agent.messages
1202+
)
1203+
assert user_message_added, "User prompt should be added to conversation history"
1204+
1205+
# Verify the structured output was added to conversation history
1206+
assistant_message_added = any(
1207+
msg['role'] == 'assistant' and 'Structured output (User):' in msg['content'][0]['text']
1208+
for msg in agent.messages
1209+
)
1210+
assert assistant_message_added, "Structured output should be added to conversation history"
11261211

1127-
# Verify the model was called with temporary messages array
1212+
# Verify the model was called with all messages (including the added prompt)
11281213
agent.model.structured_output.assert_called_once_with(
1129-
type(user), [{"role": "user", "content": [{"text": prompt}]}], system_prompt=system_prompt
1214+
type(user),
1215+
[
1216+
{"role": "user", "content": [{"text": prompt}]},
1217+
{"role": "assistant", "content": [{"text": f"Structured output (User): {user.model_dump_json()}"}]}
1218+
],
1219+
system_prompt=system_prompt
11301220
)
11311221

11321222

tests/strands/agent/test_agent_hooks.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,12 +267,14 @@ def test_agent_structured_output_hooks(agent, hook_provider, user, agenerator):
267267

268268
length, events = hook_provider.get_events()
269269

270-
assert length == 2
270+
assert length == 4 # BeforeInvocationEvent, MessageAddedEvent (prompt), MessageAddedEvent (output), AfterInvocationEvent
271271

272272
assert next(events) == BeforeInvocationEvent(agent=agent)
273+
assert next(events) == MessageAddedEvent(agent=agent, message=agent.messages[0]) # Prompt added
274+
assert next(events) == MessageAddedEvent(agent=agent, message=agent.messages[1]) # Output added
273275
assert next(events) == AfterInvocationEvent(agent=agent)
274276

275-
assert len(agent.messages) == 0 # no new messages added
277+
assert len(agent.messages) == 2 # prompt and structured output added
276278

277279

278280
@pytest.mark.asyncio
@@ -284,9 +286,11 @@ async def test_agent_structured_async_output_hooks(agent, hook_provider, user, a
284286

285287
length, events = hook_provider.get_events()
286288

287-
assert length == 2
289+
assert length == 4 # BeforeInvocationEvent, MessageAddedEvent (prompt), MessageAddedEvent (output), AfterInvocationEvent
288290

289291
assert next(events) == BeforeInvocationEvent(agent=agent)
292+
assert next(events) == MessageAddedEvent(agent=agent, message=agent.messages[0]) # Prompt added
293+
assert next(events) == MessageAddedEvent(agent=agent, message=agent.messages[1]) # Output added
290294
assert next(events) == AfterInvocationEvent(agent=agent)
291295

292-
assert len(agent.messages) == 0 # no new messages added
296+
assert len(agent.messages) == 2 # prompt and structured output added

0 commit comments

Comments
 (0)