Skip to content

Commit aa4464d

Browse files
feat(summarizing_conversation_manager): implement summarization of older messages with model integration
1 parent 63cef21 commit aa4464d

File tree

3 files changed

+501
-1
lines changed

3 files changed

+501
-1
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+
]
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"""Summarizing conversation history management with configurable options."""
2+
3+
import logging
4+
from typing import List, Optional, cast
5+
6+
from ...types.content import ContentBlock, Message, Messages
7+
from ...types.exceptions import ContextWindowOverflowException
8+
from ...types.models.model import Model
9+
from ...types.tools import ToolResult
10+
from .sliding_window_conversation_manager import SlidingWindowConversationManager
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class SummarizingConversationManager(SlidingWindowConversationManager):
16+
"""Extends sliding window manager with optional summarization of older messages.
17+
18+
This manager provides a configurable option to summarize older context instead of
19+
simply trimming it, helping preserve important information while staying within
20+
context limits.
21+
"""
22+
23+
def __init__(
24+
self,
25+
window_size: int = 40,
26+
enable_summarization: bool = False,
27+
summarization_model: Optional[Model] = None,
28+
summary_ratio: float = 0.3,
29+
preserve_recent_messages: int = 10,
30+
):
31+
"""Initialize the summarizing conversation manager.
32+
33+
Args:
34+
window_size: Maximum number of messages to keep in history.
35+
Defaults to 40 messages.
36+
enable_summarization: Whether to enable summarization of older context.
37+
Defaults to False (falls back to sliding window behavior).
38+
summarization_model: Model to use for generating summaries.
39+
Required if enable_summarization is True.
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+
"""
45+
super().__init__(window_size)
46+
self.enable_summarization = enable_summarization
47+
self.summarization_model = summarization_model
48+
self.summary_ratio = max(0.1, min(0.8, summary_ratio))
49+
self.preserve_recent_messages = preserve_recent_messages
50+
51+
if enable_summarization and summarization_model is None:
52+
raise ValueError("summarization_model is required when enable_summarization is True")
53+
54+
def reduce_context(self, messages: Messages, e: Optional[Exception] = None) -> None:
55+
"""Reduce context using summarization if enabled, otherwise fall back to sliding window.
56+
57+
Args:
58+
messages: The messages to reduce.
59+
This list is modified in-place.
60+
e: The exception that triggered the context reduction, if any.
61+
62+
Raises:
63+
ContextWindowOverflowException: If the context cannot be reduced further.
64+
"""
65+
if not self.enable_summarization or self.summarization_model is None:
66+
# Fall back to standard sliding window behavior
67+
super().reduce_context(messages, e)
68+
return
69+
70+
try:
71+
self._reduce_context_with_summarization(messages, e)
72+
except Exception as summarization_error:
73+
logger.warning("Summarization failed, falling back to sliding window: %s", summarization_error)
74+
# Fall back to sliding window if summarization fails
75+
super().reduce_context(messages, e)
76+
77+
def _reduce_context_with_summarization(self, messages: Messages, e: Optional[Exception] = None) -> None:
78+
"""Reduce context by summarizing older messages.
79+
80+
Args:
81+
messages: The messages to reduce.
82+
This list is modified in-place.
83+
e: The exception that triggered the context reduction, if any.
84+
85+
Raises:
86+
ContextWindowOverflowException: If the context cannot be reduced further.
87+
"""
88+
if len(messages) <= self.preserve_recent_messages:
89+
raise ContextWindowOverflowException("Cannot summarize: too few messages to preserve context") from e
90+
91+
# Calculate how many messages to summarize
92+
messages_to_summarize_count = max(1, int(len(messages) * self.summary_ratio))
93+
94+
# Ensure we don't summarize recent messages
95+
messages_to_summarize_count = min(messages_to_summarize_count, len(messages) - self.preserve_recent_messages)
96+
97+
if messages_to_summarize_count <= 0:
98+
raise ContextWindowOverflowException("Cannot summarize: insufficient messages for summarization") from e
99+
100+
# Extract messages to summarize
101+
messages_to_summarize = messages[:messages_to_summarize_count]
102+
remaining_messages = messages[messages_to_summarize_count:]
103+
104+
# Generate summary
105+
summary_content = self._generate_summary(messages_to_summarize)
106+
107+
# Create a summary message
108+
summary_message: Message = {"role": "assistant", "content": [ContentBlock(text=summary_content)]}
109+
110+
# Replace the summarized messages with the summary
111+
messages[:] = [summary_message] + remaining_messages
112+
113+
def _generate_summary(self, messages: List[Message]) -> str:
114+
"""Generate a summary of the provided messages.
115+
116+
Args:
117+
messages: The messages to summarize.
118+
119+
Returns:
120+
A text summary of the conversation history.
121+
122+
Raises:
123+
Exception: If summary generation fails.
124+
"""
125+
if self.summarization_model is None:
126+
raise ValueError("Summarization model is required but not provided")
127+
128+
# Convert messages to a readable format for summarization
129+
conversation_text = self._format_messages_for_summarization(messages)
130+
131+
# Create system prompt for summarization task
132+
system_prompt = (
133+
"You are a conversation summarizer. Provide a concise summary of the conversation history. "
134+
"Focus on key decisions, important context, and any ongoing tasks or topics. "
135+
"Keep the summary under 500 words and be factual and objective."
136+
)
137+
138+
# Create user message with conversation to summarize
139+
user_prompt = f"Please summarize this conversation:\n\n{conversation_text}"
140+
summary_messages: Messages = [{"role": "user", "content": [ContentBlock(text=user_prompt)]}]
141+
142+
summary_response = ""
143+
for chunk in self.summarization_model.converse(summary_messages, system_prompt=system_prompt):
144+
# Extract text from streaming events
145+
if "contentBlockDelta" in chunk and "delta" in chunk["contentBlockDelta"]:
146+
delta = chunk["contentBlockDelta"]["delta"]
147+
if "text" in delta:
148+
summary_response += delta["text"]
149+
150+
return f"[CONVERSATION SUMMARY]: {summary_response.strip()}"
151+
152+
def _format_messages_for_summarization(self, messages: List[Message]) -> str:
153+
"""Format messages into readable text for summarization.
154+
155+
Args:
156+
messages: The messages to format.
157+
158+
Returns:
159+
A formatted string representation of the messages.
160+
"""
161+
formatted_parts = []
162+
163+
for _, message in enumerate(messages):
164+
role = message["role"].capitalize()
165+
content_parts = []
166+
167+
for content in message["content"]:
168+
if "text" in content:
169+
content_parts.append(content["text"])
170+
elif "toolUse" in content:
171+
tool_use = content["toolUse"]
172+
content_parts.append(f"[Used tool: {tool_use.get('name', 'unknown')}]")
173+
elif "toolResult" in content:
174+
tool_result = cast(ToolResult, content["toolResult"])
175+
status = tool_result.get("status", "")
176+
if tool_result.get("content"):
177+
for tr_content in tool_result["content"]:
178+
if "text" in tr_content:
179+
content_parts.append(f"[Tool result: {tr_content['text'][:100]}...]")
180+
elif "json" in tr_content:
181+
content_parts.append("[Tool result: JSON data]")
182+
else:
183+
content_parts.append(f"[Tool result: {status}]")
184+
elif "image" in content:
185+
content_parts.append("[Image content]")
186+
elif "document" in content:
187+
content_parts.append("[Document content]")
188+
189+
if content_parts:
190+
formatted_parts.append(f"{role}: {' '.join(content_parts)}")
191+
192+
return "\n".join(formatted_parts)

0 commit comments

Comments
 (0)