From b60b6d0d65adf52b090bcb48ab101959c7d14126 Mon Sep 17 00:00:00 2001 From: Patrick Gray Date: Thu, 14 Aug 2025 12:43:26 -0400 Subject: [PATCH 1/4] summarization manager - add summary prompt to messages --- .../summarizing_conversation_manager.py | 12 ++--- .../test_summarizing_conversation_manager.py | 54 ++++++++++--------- .../test_repository_session_manager.py | 7 ++- ...rizing_conversation_manager_integration.py | 15 +++--- 4 files changed, 46 insertions(+), 42 deletions(-) diff --git a/src/strands/agent/conversation_manager/summarizing_conversation_manager.py b/src/strands/agent/conversation_manager/summarizing_conversation_manager.py index 60e832215..ac9f019ff 100644 --- a/src/strands/agent/conversation_manager/summarizing_conversation_manager.py +++ b/src/strands/agent/conversation_manager/summarizing_conversation_manager.py @@ -81,6 +81,7 @@ def __init__( self.summarization_agent = summarization_agent self.summarization_system_prompt = summarization_system_prompt self._summary_message: Optional[Message] = None + self._summary_prompt: Message = {"content": [{"text": "Please summarize this conversation"}], "role": "user"} @override def restore_from_session(self, state: dict[str, Any]) -> Optional[list[Message]]: @@ -94,7 +95,7 @@ def restore_from_session(self, state: dict[str, Any]) -> Optional[list[Message]] """ super().restore_from_session(state) self._summary_message = state.get("summary_message") - return [self._summary_message] if self._summary_message else None + return [self._summary_prompt, self._summary_message] if self._summary_message else None def get_state(self) -> dict[str, Any]: """Returns a dictionary representation of the state for the Summarizing Conversation Manager.""" @@ -152,15 +153,15 @@ def reduce_context(self, agent: "Agent", e: Optional[Exception] = None, **kwargs # Keep track of the number of messages that have been summarized thus far. self.removed_message_count += len(messages_to_summarize) - # If there is a summary message, don't count it in the removed_message_count. + # If there is a summary message, don't count it or the summary prompt in the removed_message_count. if self._summary_message: - self.removed_message_count -= 1 + self.removed_message_count -= 2 # Generate summary self._summary_message = self._generate_summary(messages_to_summarize, agent) # Replace the summarized messages with the summary - agent.messages[:] = [self._summary_message] + remaining_messages + agent.messages[:] = [self._summary_prompt, self._summary_message] + remaining_messages except Exception as summarization_error: logger.error("Summarization failed: %s", summarization_error) @@ -200,8 +201,7 @@ def _generate_summary(self, messages: List[Message], agent: "Agent") -> Message: summarization_agent.messages = messages # Use the agent to generate summary with rich content (can use tools if needed) - result = summarization_agent("Please summarize this conversation.") - + result = summarization_agent(self._summary_prompt["content"]) return result.message finally: diff --git a/tests/strands/agent/test_summarizing_conversation_manager.py b/tests/strands/agent/test_summarizing_conversation_manager.py index a97104412..cd2e7bfd8 100644 --- a/tests/strands/agent/test_summarizing_conversation_manager.py +++ b/tests/strands/agent/test_summarizing_conversation_manager.py @@ -95,13 +95,16 @@ def test_reduce_context_with_summarization(summarizing_manager, mock_agent): summarizing_manager.reduce_context(mock_agent) - # Should have: 1 summary message + 2 preserved recent messages + remaining from summarization - assert len(mock_agent.messages) == 4 + # Should have: 1 summary prompt + 1 summary message + 2 preserved recent messages + remaining from summarization + assert len(mock_agent.messages) == 5 - # First message should be the summary - assert mock_agent.messages[0]["role"] == "assistant" - first_content = mock_agent.messages[0]["content"][0] - assert "text" in first_content and "This is a summary of the conversation." in first_content["text"] + # First message should be the summary prompt + assert mock_agent.messages[0] == {"content": [{"text": "Please summarize this conversation"}], "role": "user"} + # Second message should be the summary + assert mock_agent.messages[1] == { + "content": [{"text": "This is a summary of the conversation."}], + "role": "assistant", + } # Recent messages should be preserved assert "Message 3" in str(mock_agent.messages[-2]["content"]) @@ -434,17 +437,18 @@ def test_reduce_context_tool_pair_adjustment_works_with_forward_search(): # messages_to_summarize_count = (3 - 1) * 0.5 = 1 # But split point adjustment will move forward from the toolUse, potentially increasing count manager.reduce_context(mock_agent) - # Should have summary + remaining messages - assert len(mock_agent.messages) == 2 - - # First message should be the summary - assert mock_agent.messages[0]["role"] == "assistant" - summary_content = mock_agent.messages[0]["content"][0] - assert "text" in summary_content and "This is a summary of the conversation." in summary_content["text"] - + # Should have summary prompt + summary + remaining messages + assert len(mock_agent.messages) == 3 + + # First message should be the summary prompt + assert mock_agent.messages[0] == {"content": [{"text": "Please summarize this conversation"}], "role": "user"} + # Second message should be the summary + assert mock_agent.messages[1] == { + "content": [{"text": "This is a summary of the conversation."}], + "role": "assistant", + } # Last message should be the preserved recent message - assert mock_agent.messages[1]["role"] == "user" - assert mock_agent.messages[1]["content"][0]["text"] == "Latest message" + assert mock_agent.messages[2] == {"content": [{"text": "Latest message"}], "role": "user"} def test_adjust_split_point_exceeds_message_length(summarizing_manager): @@ -590,21 +594,19 @@ def test_summarizing_conversation_manager_properly_records_removed_message_count assert manager.removed_message_count == 0 manager.reduce_context(agent) - # Assert the oldest message is the sumamry message assert manager._summary_message["content"][0]["text"] == "Summary" # There are 8 messages in the agent messages array, since half will be summarized, - # 4 will remain plus 1 summary message = 5 - assert (len(agent.messages)) == 5 + # 4 will remain plus 1 summary prompt and 1 summary message = 6 + assert len(agent.messages) == 6 # Half of the messages were summarized and removed: 8/2 = 4 assert manager.removed_message_count == 4 manager.reduce_context(agent) assert manager._summary_message["content"][0]["text"] == "Summary" - # After the first summary, 5 messages remain. Summarizing again will lead to: - # 5 - (int(5/2)) (messages to be sumamrized) + 1 (new summary message) = 5 - 2 + 1 = 4 - assert (len(agent.messages)) == 4 - # Half of the messages were summarized and removed: int(5/2) = 2 - # However, one of the messages that was summarized was the previous summary message, - # so we dont count this toward the total: - # 4 (Previously removed messages) + 2 (removed messages) - 1 (Previous summary message) = 5 + # After the first summary, 6 messages remain. Summarizing again will lead to: + # 6 - (6/2) (messages to be sumamrized) + 1 (summary prompt) + 1 (new summary message) = 6 - 3 + 1 + 1 = 5 + assert len(agent.messages) == 5 + # Half of the messages were summarized and removed: (6/2) = 3 + # However, the summary prompt and previous summary were also summarized but we don't count this in the total: + # 4 (Previously removed messages) + 3 (removed messages) - 1 (summary prompt) - 1 (Previous summary message) = 5 assert manager.removed_message_count == 5 diff --git a/tests/strands/session/test_repository_session_manager.py b/tests/strands/session/test_repository_session_manager.py index 2c25fcc38..18fb73612 100644 --- a/tests/strands/session/test_repository_session_manager.py +++ b/tests/strands/session/test_repository_session_manager.py @@ -151,10 +151,9 @@ def test_initialize_restores_existing_agent_with_summarizing_conversation_manage # Verify agent state restored assert agent.state.get("key") == "value" - # The session message plus the summary message - assert len(agent.messages) == 2 - assert agent.messages[1]["role"] == "user" - assert agent.messages[1]["content"][0]["text"] == "Hello" + # The session message plus the summary prompt plus the summary message + assert len(agent.messages) == 3 + assert agent.messages[2] == {"content": [{"text": "Hello"}], "role": "user"} assert agent.conversation_manager.removed_message_count == 1 diff --git a/tests_integ/test_summarizing_conversation_manager_integration.py b/tests_integ/test_summarizing_conversation_manager_integration.py index 719520b8d..d5413e99f 100644 --- a/tests_integ/test_summarizing_conversation_manager_integration.py +++ b/tests_integ/test_summarizing_conversation_manager_integration.py @@ -151,15 +151,18 @@ def test_summarization_with_context_overflow(model): # Verify summarization occurred assert len(agent.messages) < initial_message_count - # Should have: 1 summary + remaining messages + # Should have: 1 summary prompt + 1 summary + remaining messages # With 6 messages, summary_ratio=0.5, preserve_recent_messages=2: # messages_to_summarize = min(6 * 0.5, 6 - 2) = min(3, 4) = 3 - # So we summarize 3 messages, leaving 3 remaining + 1 summary = 4 total - expected_total_messages = 4 + # So we summarize 3 messages, leaving 3 remaining + 1 summary prompt + 1 summary = 5 total + expected_total_messages = 5 assert len(agent.messages) == expected_total_messages - # First message should be the summary (assistant message) - summary_message = agent.messages[0] + # First message should be the summary prompt + summary_prompt = agent.messages[0] + assert summary_prompt == {"content": [{"text": "Please summarize this conversation"}], "role": "user"} + # Second message should be the summary + summary_message = agent.messages[1] assert summary_message["role"] == "assistant" assert len(summary_message["content"]) > 0 @@ -361,7 +364,7 @@ def test_dedicated_summarization_agent(model, summarization_model): assert len(agent.messages) < original_length # Get the summary message - summary_message = agent.messages[0] + summary_message = agent.messages[1] assert summary_message["role"] == "assistant" # Extract summary text From 3c8a0b117b9d2efdeff15b5b30ce239798e861c2 Mon Sep 17 00:00:00 2001 From: Patrick Gray Date: Mon, 25 Aug 2025 16:35:58 -0400 Subject: [PATCH 2/4] summarize conversation - assistant to user role --- .../summarizing_conversation_manager.py | 15 +++--- .../test_summarizing_conversation_manager.py | 54 +++++++++---------- .../test_repository_session_manager.py | 8 +-- ...rizing_conversation_manager_integration.py | 19 +++---- 4 files changed, 45 insertions(+), 51 deletions(-) diff --git a/src/strands/agent/conversation_manager/summarizing_conversation_manager.py b/src/strands/agent/conversation_manager/summarizing_conversation_manager.py index ac9f019ff..92d6278a6 100644 --- a/src/strands/agent/conversation_manager/summarizing_conversation_manager.py +++ b/src/strands/agent/conversation_manager/summarizing_conversation_manager.py @@ -1,7 +1,7 @@ """Summarizing conversation history management with configurable options.""" import logging -from typing import TYPE_CHECKING, Any, List, Optional +from typing import TYPE_CHECKING, Any, List, Optional, cast from typing_extensions import override @@ -81,7 +81,6 @@ def __init__( self.summarization_agent = summarization_agent self.summarization_system_prompt = summarization_system_prompt self._summary_message: Optional[Message] = None - self._summary_prompt: Message = {"content": [{"text": "Please summarize this conversation"}], "role": "user"} @override def restore_from_session(self, state: dict[str, Any]) -> Optional[list[Message]]: @@ -95,7 +94,7 @@ def restore_from_session(self, state: dict[str, Any]) -> Optional[list[Message]] """ super().restore_from_session(state) self._summary_message = state.get("summary_message") - return [self._summary_prompt, self._summary_message] if self._summary_message else None + return [self._summary_message] if self._summary_message else None def get_state(self) -> dict[str, Any]: """Returns a dictionary representation of the state for the Summarizing Conversation Manager.""" @@ -153,15 +152,15 @@ def reduce_context(self, agent: "Agent", e: Optional[Exception] = None, **kwargs # Keep track of the number of messages that have been summarized thus far. self.removed_message_count += len(messages_to_summarize) - # If there is a summary message, don't count it or the summary prompt in the removed_message_count. + # If there is a summary message, don't count it in the removed_message_count. if self._summary_message: - self.removed_message_count -= 2 + self.removed_message_count -= 1 # Generate summary self._summary_message = self._generate_summary(messages_to_summarize, agent) # Replace the summarized messages with the summary - agent.messages[:] = [self._summary_prompt, self._summary_message] + remaining_messages + agent.messages[:] = [self._summary_message] + remaining_messages except Exception as summarization_error: logger.error("Summarization failed: %s", summarization_error) @@ -201,8 +200,8 @@ def _generate_summary(self, messages: List[Message], agent: "Agent") -> Message: summarization_agent.messages = messages # Use the agent to generate summary with rich content (can use tools if needed) - result = summarization_agent(self._summary_prompt["content"]) - return result.message + result = summarization_agent("Please summarize this conversation") + return cast(Message, {**result.message, "role": "user"}) finally: # Restore original agent state diff --git a/tests/strands/agent/test_summarizing_conversation_manager.py b/tests/strands/agent/test_summarizing_conversation_manager.py index cd2e7bfd8..6003a1710 100644 --- a/tests/strands/agent/test_summarizing_conversation_manager.py +++ b/tests/strands/agent/test_summarizing_conversation_manager.py @@ -95,16 +95,13 @@ def test_reduce_context_with_summarization(summarizing_manager, mock_agent): summarizing_manager.reduce_context(mock_agent) - # Should have: 1 summary prompt + 1 summary message + 2 preserved recent messages + remaining from summarization - assert len(mock_agent.messages) == 5 + # Should have: 1 summary message + 2 preserved recent messages + remaining from summarization + assert len(mock_agent.messages) == 4 - # First message should be the summary prompt - assert mock_agent.messages[0] == {"content": [{"text": "Please summarize this conversation"}], "role": "user"} - # Second message should be the summary - assert mock_agent.messages[1] == { - "content": [{"text": "This is a summary of the conversation."}], - "role": "assistant", - } + # First message should be the summary + assert mock_agent.messages[0]["role"] == "user" + first_content = mock_agent.messages[0]["content"][0] + assert "text" in first_content and "This is a summary of the conversation." in first_content["text"] # Recent messages should be preserved assert "Message 3" in str(mock_agent.messages[-2]["content"]) @@ -437,18 +434,17 @@ def test_reduce_context_tool_pair_adjustment_works_with_forward_search(): # messages_to_summarize_count = (3 - 1) * 0.5 = 1 # But split point adjustment will move forward from the toolUse, potentially increasing count manager.reduce_context(mock_agent) - # Should have summary prompt + summary + remaining messages - assert len(mock_agent.messages) == 3 - - # First message should be the summary prompt - assert mock_agent.messages[0] == {"content": [{"text": "Please summarize this conversation"}], "role": "user"} - # Second message should be the summary - assert mock_agent.messages[1] == { - "content": [{"text": "This is a summary of the conversation."}], - "role": "assistant", - } + # Should have summary + remaining messages + assert len(mock_agent.messages) == 2 + + # First message should be the summary + assert mock_agent.messages[0]["role"] == "user" + summary_content = mock_agent.messages[0]["content"][0] + assert "text" in summary_content and "This is a summary of the conversation." in summary_content["text"] + # Last message should be the preserved recent message - assert mock_agent.messages[2] == {"content": [{"text": "Latest message"}], "role": "user"} + assert mock_agent.messages[1]["role"] == "user" + assert mock_agent.messages[1]["content"][0]["text"] == "Latest message" def test_adjust_split_point_exceeds_message_length(summarizing_manager): @@ -594,19 +590,21 @@ def test_summarizing_conversation_manager_properly_records_removed_message_count assert manager.removed_message_count == 0 manager.reduce_context(agent) + # Assert the oldest message is the sumamry message assert manager._summary_message["content"][0]["text"] == "Summary" # There are 8 messages in the agent messages array, since half will be summarized, - # 4 will remain plus 1 summary prompt and 1 summary message = 6 - assert len(agent.messages) == 6 + # 4 will remain plus 1 summary message = 5 + assert (len(agent.messages)) == 5 # Half of the messages were summarized and removed: 8/2 = 4 assert manager.removed_message_count == 4 manager.reduce_context(agent) assert manager._summary_message["content"][0]["text"] == "Summary" - # After the first summary, 6 messages remain. Summarizing again will lead to: - # 6 - (6/2) (messages to be sumamrized) + 1 (summary prompt) + 1 (new summary message) = 6 - 3 + 1 + 1 = 5 - assert len(agent.messages) == 5 - # Half of the messages were summarized and removed: (6/2) = 3 - # However, the summary prompt and previous summary were also summarized but we don't count this in the total: - # 4 (Previously removed messages) + 3 (removed messages) - 1 (summary prompt) - 1 (Previous summary message) = 5 + # After the first summary, 5 messages remain. Summarizing again will lead to: + # 5 - (int(5/2)) (messages to be sumamrized) + 1 (new summary message) = 5 - 2 + 1 = 4 + assert (len(agent.messages)) == 4 + # Half of the messages were summarized and removed: int(5/2) = 2 + # However, one of the messages that was summarized was the previous summary message, + # so we dont count this toward the total: + # 4 (Previously removed messages) + 2 (removed messages) - 1 (Previous summary message) = 5 assert manager.removed_message_count == 5 diff --git a/tests/strands/session/test_repository_session_manager.py b/tests/strands/session/test_repository_session_manager.py index 18fb73612..fa9503e36 100644 --- a/tests/strands/session/test_repository_session_manager.py +++ b/tests/strands/session/test_repository_session_manager.py @@ -151,10 +151,10 @@ def test_initialize_restores_existing_agent_with_summarizing_conversation_manage # Verify agent state restored assert agent.state.get("key") == "value" - # The session message plus the summary prompt plus the summary message - assert len(agent.messages) == 3 - assert agent.messages[2] == {"content": [{"text": "Hello"}], "role": "user"} - assert agent.conversation_manager.removed_message_count == 1 + # The session message plus the summary message + assert len(agent.messages) == 2 + assert agent.messages[1]["role"] == "user" + assert agent.messages[1]["content"][0]["text"] == "Hello" def test_append_message(session_manager): diff --git a/tests_integ/test_summarizing_conversation_manager_integration.py b/tests_integ/test_summarizing_conversation_manager_integration.py index d5413e99f..b205c723f 100644 --- a/tests_integ/test_summarizing_conversation_manager_integration.py +++ b/tests_integ/test_summarizing_conversation_manager_integration.py @@ -151,19 +151,16 @@ def test_summarization_with_context_overflow(model): # Verify summarization occurred assert len(agent.messages) < initial_message_count - # Should have: 1 summary prompt + 1 summary + remaining messages + # Should have: 1 summary + remaining messages # With 6 messages, summary_ratio=0.5, preserve_recent_messages=2: # messages_to_summarize = min(6 * 0.5, 6 - 2) = min(3, 4) = 3 - # So we summarize 3 messages, leaving 3 remaining + 1 summary prompt + 1 summary = 5 total - expected_total_messages = 5 + # So we summarize 3 messages, leaving 3 remaining + 1 summary = 4 total + expected_total_messages = 4 assert len(agent.messages) == expected_total_messages - # First message should be the summary prompt - summary_prompt = agent.messages[0] - assert summary_prompt == {"content": [{"text": "Please summarize this conversation"}], "role": "user"} - # Second message should be the summary - summary_message = agent.messages[1] - assert summary_message["role"] == "assistant" + # First message should be the summary (assistant message) + summary_message = agent.messages[0] + assert summary_message["role"] == "user" assert len(summary_message["content"]) > 0 # Verify the summary contains actual text content @@ -364,8 +361,8 @@ def test_dedicated_summarization_agent(model, summarization_model): assert len(agent.messages) < original_length # Get the summary message - summary_message = agent.messages[1] - assert summary_message["role"] == "assistant" + summary_message = agent.messages[0] + assert summary_message["role"] == "user" # Extract summary text summary_text = None From e18f8c9ed3c804be0a014d96a38245b1f672098a Mon Sep 17 00:00:00 2001 From: Patrick Gray Date: Mon, 25 Aug 2025 16:51:20 -0400 Subject: [PATCH 3/4] fix test --- tests/strands/session/test_repository_session_manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/strands/session/test_repository_session_manager.py b/tests/strands/session/test_repository_session_manager.py index fa9503e36..2c25fcc38 100644 --- a/tests/strands/session/test_repository_session_manager.py +++ b/tests/strands/session/test_repository_session_manager.py @@ -155,6 +155,7 @@ def test_initialize_restores_existing_agent_with_summarizing_conversation_manage assert len(agent.messages) == 2 assert agent.messages[1]["role"] == "user" assert agent.messages[1]["content"][0]["text"] == "Hello" + assert agent.conversation_manager.removed_message_count == 1 def test_append_message(session_manager): From 75bfa106f4b72bfe8967e6c91bba8a16ebc1d4bf Mon Sep 17 00:00:00 2001 From: Patrick Gray Date: Mon, 25 Aug 2025 16:54:01 -0400 Subject: [PATCH 4/4] add period --- .../conversation_manager/summarizing_conversation_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/strands/agent/conversation_manager/summarizing_conversation_manager.py b/src/strands/agent/conversation_manager/summarizing_conversation_manager.py index 92d6278a6..b08b6853e 100644 --- a/src/strands/agent/conversation_manager/summarizing_conversation_manager.py +++ b/src/strands/agent/conversation_manager/summarizing_conversation_manager.py @@ -200,7 +200,7 @@ def _generate_summary(self, messages: List[Message], agent: "Agent") -> Message: summarization_agent.messages = messages # Use the agent to generate summary with rich content (can use tools if needed) - result = summarization_agent("Please summarize this conversation") + result = summarization_agent("Please summarize this conversation.") return cast(Message, {**result.message, "role": "user"}) finally: