Skip to content

Commit bca4e6e

Browse files
feat(summarizing_conversation_manager): implement summarization of older messages
1 parent b04c4be commit bca4e6e

File tree

6 files changed

+692
-5
lines changed

6 files changed

+692
-5
lines changed

src/strands/agent/conversation_manager/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
- NullConversationManager: A no-op implementation that does not modify conversation history
77
- SlidingWindowConversationManager: An implementation that maintains a sliding window of messages to control context
88
size while preserving conversation coherence
9+
- SummarizingConversationManager: An extension of sliding window that can optionally summarize older context instead
10+
of simply trimming it
911
1012
Conversation managers help control memory usage and context length while maintaining relevant conversation state, which
1113
is critical for effective agent interactions.
@@ -14,5 +16,11 @@
1416
from .conversation_manager import ConversationManager
1517
from .null_conversation_manager import NullConversationManager
1618
from .sliding_window_conversation_manager import SlidingWindowConversationManager
19+
from .summarizing_conversation_manager import SummarizingConversationManager
1720

18-
__all__ = ["ConversationManager", "NullConversationManager", "SlidingWindowConversationManager"]
21+
__all__ = [
22+
"ConversationManager",
23+
"NullConversationManager",
24+
"SlidingWindowConversationManager",
25+
"SummarizingConversationManager",
26+
]

src/strands/agent/conversation_manager/conversation_manager.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""Abstract interface for conversation history management."""
22

33
from abc import ABC, abstractmethod
4-
from typing import Optional
4+
from typing import TYPE_CHECKING, Optional
55

66
from ...types.content import Messages
77

8+
if TYPE_CHECKING:
9+
from ..agent import Agent
10+
811

912
class ConversationManager(ABC):
1013
"""Abstract base class for managing conversation history.
@@ -34,7 +37,9 @@ def apply_management(self, messages: Messages) -> None:
3437

3538
@abstractmethod
3639
# pragma: no cover
37-
def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> None:
40+
def reduce_context(
41+
self, messages: Messages, e: Optional[Exception] = None, agent: Optional["Agent"] = None
42+
) -> None:
3843
"""Called when the model's context window is exceeded.
3944
4045
This method should implement the specific strategy for reducing the window size when a context overflow occurs.
@@ -51,5 +56,6 @@ def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> N
5156
messages: The conversation history to reduce.
5257
This list is modified in-place.
5358
e: The exception that triggered the context reduction, if any.
59+
agent: The agent instance, if available for advanced reduction strategies.
5460
"""
5561
pass

src/strands/agent/conversation_manager/null_conversation_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ def apply_management(self, messages: Messages) -> None:
2525
"""
2626
pass
2727

28-
def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> None:
28+
def reduce_context(self, messages: Messages, e: Optional[Exception] = None, **kwargs) -> None:
2929
"""Does not reduce context and raises an exception.
3030
3131
Args:
3232
messages: The conversation history that will remain unmodified.
3333
e: The exception that triggered the context reduction, if any.
34+
**kwargs: Additional keyword arguments (ignored).
3435
3536
Raises:
3637
e: If provided.

src/strands/agent/conversation_manager/sliding_window_conversation_manager.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def _remove_dangling_messages(self, messages: Messages) -> None:
105105
if not any("toolResult" in content for content in messages[-1]["content"]):
106106
messages.pop()
107107

108-
def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> None:
108+
def reduce_context(self, messages: Messages, e: Optional[Exception] = None, **kwargs) -> None:
109109
"""Trim the oldest messages to reduce the conversation context size.
110110
111111
The method handles special cases where trimming the messages leads to:
@@ -116,6 +116,7 @@ def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> N
116116
messages: The messages to reduce.
117117
This list is modified in-place.
118118
e: The exception that triggered the context reduction, if any.
119+
**kwargs: Additional keyword arguments (ignored).
119120
120121
Raises:
121122
ContextWindowOverflowException: If the context cannot be reduced further.
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
"""Summarizing conversation history management with configurable options."""
2+
3+
import json
4+
import logging
5+
from typing import TYPE_CHECKING, List, Optional, cast
6+
7+
from ...types.content import ContentBlock, Message, Messages
8+
from ...types.exceptions import ContextWindowOverflowException
9+
from ...types.tools import ToolResult
10+
from .conversation_manager import ConversationManager
11+
12+
if TYPE_CHECKING:
13+
from ..agent import Agent
14+
15+
16+
logger = logging.getLogger(__name__)
17+
18+
19+
class SummarizingConversationManager(ConversationManager):
20+
"""Extends sliding window manager with optional summarization of older messages.
21+
22+
This manager provides a configurable option to summarize older context instead of
23+
simply trimming it, helping preserve important information while staying within
24+
context limits.
25+
"""
26+
27+
def __init__(
28+
self,
29+
window_size: int = 40,
30+
summary_ratio: float = 0.3,
31+
preserve_recent_messages: int = 10,
32+
summarization_agent: Optional["Agent"] = None,
33+
summarization_system_prompt: Optional[str] = None,
34+
):
35+
"""Initialize the summarizing conversation manager.
36+
37+
Args:
38+
window_size: Maximum number of messages to keep in history.
39+
Defaults to 40 messages.
40+
summary_ratio: Ratio of messages to summarize vs keep when window is exceeded.
41+
Value between 0.1 and 0.8. Defaults to 0.3 (summarize 30% of oldest messages).
42+
preserve_recent_messages: Minimum number of recent messages to always keep.
43+
Defaults to 10 messages.
44+
summarization_agent: Optional agent to use for summarization instead of the parent agent.
45+
If provided, this agent can use tools as part of the summarization process.
46+
summarization_system_prompt: Optional system prompt override for summarization.
47+
If None, uses the default summarization prompt.
48+
"""
49+
self.window_size = window_size
50+
self.summary_ratio = max(0.1, min(0.8, summary_ratio))
51+
self.preserve_recent_messages = preserve_recent_messages
52+
self.summarization_agent = summarization_agent
53+
self.summarization_system_prompt = summarization_system_prompt
54+
55+
def apply_management(self, messages: Messages) -> None:
56+
"""Apply management strategy when message count exceeds window size.
57+
58+
Args:
59+
messages: The conversation history to manage.
60+
This list is modified in-place.
61+
"""
62+
if len(messages) > self.window_size:
63+
# Call reduce_context (agent will be provided by the caller when needed)
64+
self.reduce_context(messages)
65+
66+
def reduce_context(
67+
self, messages: Messages, e: Optional[Exception] = None, agent: Optional["Agent"] = None
68+
) -> None:
69+
"""Reduce context using summarization.
70+
71+
Args:
72+
messages: The messages to reduce.
73+
This list is modified in-place.
74+
e: The exception that triggered the context reduction, if any.
75+
agent: The agent instance to use for summarization.
76+
If None, context overflow will raise an exception.
77+
78+
Raises:
79+
ContextWindowOverflowException: If the context cannot be summarized.
80+
"""
81+
if agent is None:
82+
raise ContextWindowOverflowException("No agent provided for context reduction") from e
83+
84+
try:
85+
# Calculate how many messages to summarize
86+
messages_to_summarize_count = max(1, int(len(messages) * self.summary_ratio))
87+
88+
# Ensure we don't summarize recent messages
89+
messages_to_summarize_count = min(
90+
messages_to_summarize_count, len(messages) - self.preserve_recent_messages
91+
)
92+
93+
if messages_to_summarize_count <= 0:
94+
raise ContextWindowOverflowException("Cannot summarize: insufficient messages for summarization") from e
95+
96+
# Extract messages to summarize
97+
messages_to_summarize = messages[:messages_to_summarize_count]
98+
remaining_messages = messages[messages_to_summarize_count:]
99+
100+
# Generate summary
101+
summary_content = self._generate_summary(messages_to_summarize, agent)
102+
103+
# Create a summary message
104+
summary_message: Message = {"role": "assistant", "content": [ContentBlock(text=summary_content)]}
105+
106+
# Replace the summarized messages with the summary
107+
messages[:] = [summary_message] + remaining_messages
108+
109+
except Exception as summarization_error:
110+
logger.error("Summarization failed: %s", summarization_error)
111+
raise
112+
113+
def _generate_summary(self, messages: List[Message], agent: "Agent") -> str:
114+
"""Generate a summary of the provided messages.
115+
116+
Args:
117+
messages: The messages to summarize.
118+
agent: The agent instance to use for summarization.
119+
120+
Returns:
121+
A text summary of the conversation history.
122+
123+
Raises:
124+
Exception: If summary generation fails.
125+
"""
126+
# Choose which agent to use for summarization
127+
summarization_agent = self.summarization_agent if self.summarization_agent is not None else agent
128+
129+
# Format messages for summarization, preserving rich content
130+
formatted_messages = self._format_messages_for_summarization(messages)
131+
132+
# Use custom system prompt if provided, otherwise use default
133+
system_prompt = (
134+
self.summarization_system_prompt
135+
if self.summarization_system_prompt is not None
136+
else (
137+
"You are a conversation summarizer. Provide a concise summary of the conversation history.\n\n"
138+
"Format Requirements:\n"
139+
"- You MUST create a structured and concise summary in bullet-point format.\n"
140+
"- You MUST NOT respond conversationally.\n"
141+
"- You MUST NOT address the user directly.\n\n"
142+
"Task:\n"
143+
"Your task is to create a structured summary document:\n"
144+
"- It MUST contain bullet points with key topics and questions covered\n"
145+
"- It MUST contain bullet points for all significant tools executed and their results\n"
146+
"- It MUST contain bullet points for any code or technical information shared\n"
147+
"- It MUST contain a section of key insights gained\n"
148+
"- It MUST format the summary in the third person\n\n"
149+
"Example format:\n\n"
150+
"## Conversation Summary\n"
151+
"* Topic 1: Key information\n"
152+
"* Topic 2: Key information\n"
153+
"*\n"
154+
"## Tools Executed\n"
155+
"* Tool X: Result Y"
156+
)
157+
)
158+
159+
# Save original system prompt and messages to restore later
160+
original_system_prompt = summarization_agent.system_prompt
161+
original_messages = summarization_agent.messages.copy()
162+
163+
try:
164+
# Temporarily set the system prompt and set formatted messages for summarization
165+
summarization_agent.system_prompt = system_prompt
166+
summarization_agent.messages = formatted_messages
167+
168+
# Use the agent to generate summary with rich content (can use tools if needed)
169+
result = summarization_agent("Please summarize this conversation.")
170+
summary_response = ""
171+
172+
# Extract text content from the result
173+
if hasattr(result, "message") and result.message and "content" in result.message:
174+
for content_block in result.message["content"]:
175+
if "text" in content_block:
176+
summary_response += content_block["text"]
177+
178+
return summary_response.strip()
179+
180+
finally:
181+
# Restore original agent state
182+
summarization_agent.system_prompt = original_system_prompt
183+
summarization_agent.messages = original_messages
184+
185+
def _format_messages_for_summarization(self, messages: List[Message]) -> List[Message]:
186+
"""Format messages for summarization, preserving rich content.
187+
188+
Args:
189+
messages: The messages to format.
190+
191+
Returns:
192+
A list of formatted messages with rich content preserved for agent analysis.
193+
"""
194+
formatted_messages = []
195+
196+
for _, message in enumerate(messages):
197+
role = message["role"]
198+
formatted_content = []
199+
200+
for content in message["content"]:
201+
if "text" in content:
202+
formatted_content.append(ContentBlock(text=content["text"]))
203+
elif "toolUse" in content:
204+
tool_use = content["toolUse"]
205+
formatted_content.append(ContentBlock(text=f"[Used tool: {tool_use.get('name', 'unknown')}]"))
206+
elif "toolResult" in content:
207+
tool_result = cast(ToolResult, content["toolResult"])
208+
status = tool_result.get("status", "")
209+
if tool_result.get("content"):
210+
for tr_content in tool_result["content"]:
211+
if "text" in tr_content:
212+
text = tr_content["text"]
213+
formatted_content.append(ContentBlock(text=f"[Tool result text: {text}]"))
214+
elif "json" in tr_content:
215+
json_str = json.dumps(tr_content["json"])
216+
formatted_content.append(ContentBlock(text=f"[Tool result JSON: {json_str}]"))
217+
elif "image" in tr_content:
218+
formatted_content.append(ContentBlock(image=tr_content["image"]))
219+
elif "document" in tr_content:
220+
formatted_content.append(ContentBlock(document=tr_content["document"]))
221+
else:
222+
formatted_content.append(ContentBlock(text=f"[Tool result: {status}]"))
223+
elif "image" in content:
224+
formatted_content.append(ContentBlock(image=content["image"]))
225+
elif "document" in content:
226+
formatted_content.append(ContentBlock(document=content["document"]))
227+
228+
if formatted_content:
229+
formatted_message = {"role": role, "content": formatted_content}
230+
formatted_messages.append(formatted_message)
231+
232+
return formatted_messages

0 commit comments

Comments
 (0)