Skip to content

Commit f1525c2

Browse files
feat(summarizing_conversation_manager): implement summarization strategy
1 parent ffc7c5e commit f1525c2

File tree

5 files changed

+980
-2
lines changed

5 files changed

+980
-2
lines changed

src/strands/agent/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@
88

99
from .agent import Agent
1010
from .agent_result import AgentResult
11-
from .conversation_manager import ConversationManager, NullConversationManager, SlidingWindowConversationManager
11+
from .conversation_manager import (
12+
ConversationManager,
13+
NullConversationManager,
14+
SlidingWindowConversationManager,
15+
SummarizingConversationManager,
16+
)
1217

1318
__all__ = [
1419
"Agent",
1520
"AgentResult",
1621
"ConversationManager",
1722
"NullConversationManager",
1823
"SlidingWindowConversationManager",
24+
"SummarizingConversationManager",
1925
]

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 implementation that summarizes 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: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
"""Summarizing conversation history management with configurable options."""
2+
3+
import logging
4+
from typing import TYPE_CHECKING, List, Optional
5+
6+
from ...types.content import Message
7+
from ...types.exceptions import ContextWindowOverflowException
8+
from .conversation_manager import ConversationManager
9+
10+
if TYPE_CHECKING:
11+
from ..agent import Agent
12+
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
DEFAULT_SUMMARIZATION_PROMPT = """You are a conversation summarizer. Provide a concise summary of the conversation \
18+
history.
19+
20+
Format Requirements:
21+
- You MUST create a structured and concise summary in bullet-point format.
22+
- You MUST NOT respond conversationally.
23+
- You MUST NOT address the user directly.
24+
25+
Task:
26+
Your task is to create a structured summary document:
27+
- It MUST contain bullet points with key topics and questions covered
28+
- It MUST contain bullet points for all significant tools executed and their results
29+
- It MUST contain bullet points for any code or technical information shared
30+
- It MUST contain a section of key insights gained
31+
- It MUST format the summary in the third person
32+
33+
Example format:
34+
35+
## Conversation Summary
36+
* Topic 1: Key information
37+
* Topic 2: Key information
38+
*
39+
## Tools Executed
40+
* Tool X: Result Y"""
41+
42+
43+
class SummarizingConversationManager(ConversationManager):
44+
"""Extends sliding window manager with optional summarization of older messages.
45+
46+
This manager provides a configurable option to summarize older context instead of
47+
simply trimming it, helping preserve important information while staying within
48+
context limits.
49+
"""
50+
51+
def __init__(
52+
self,
53+
summary_ratio: float = 0.3,
54+
preserve_recent_messages: int = 10,
55+
summarization_agent: Optional["Agent"] = None,
56+
summarization_system_prompt: Optional[str] = None,
57+
):
58+
"""Initialize the summarizing conversation manager.
59+
60+
Args:
61+
summary_ratio: Ratio of messages to summarize vs keep when context overflow occurs.
62+
Value between 0.1 and 0.8. Defaults to 0.3 (summarize 30% of oldest messages).
63+
preserve_recent_messages: Minimum number of recent messages to always keep.
64+
Defaults to 10 messages.
65+
summarization_agent: Optional agent to use for summarization instead of the parent agent.
66+
If provided, this agent can use tools as part of the summarization process.
67+
summarization_system_prompt: Optional system prompt override for summarization.
68+
If None, uses the default summarization prompt.
69+
"""
70+
self.summary_ratio = max(0.1, min(0.8, summary_ratio))
71+
self.preserve_recent_messages = preserve_recent_messages
72+
self.summarization_agent = summarization_agent
73+
self.summarization_system_prompt = summarization_system_prompt
74+
75+
def apply_management(self, agent: "Agent") -> None:
76+
"""Apply management strategy to conversation history.
77+
78+
For the summarizing conversation manager, no proactive management is performed.
79+
Summarization only occurs when there's a context overflow that triggers reduce_context.
80+
81+
Args:
82+
agent: The agent whose conversation history will be managed.
83+
The agent's messages list is modified in-place.
84+
"""
85+
# No proactive management - summarization only happens on context overflow
86+
pass
87+
88+
def reduce_context(self, agent: "Agent", e: Optional[Exception] = None) -> None:
89+
"""Reduce context using summarization.
90+
91+
Args:
92+
agent: The agent whose conversation history will be reduced.
93+
The agent's messages list is modified in-place.
94+
e: The exception that triggered the context reduction, if any.
95+
96+
Raises:
97+
ContextWindowOverflowException: If the context cannot be summarized.
98+
"""
99+
try:
100+
# Calculate how many messages to summarize
101+
messages_to_summarize_count = max(1, int(len(agent.messages) * self.summary_ratio))
102+
103+
# Ensure we don't summarize recent messages
104+
messages_to_summarize_count = min(
105+
messages_to_summarize_count, len(agent.messages) - self.preserve_recent_messages
106+
)
107+
108+
if messages_to_summarize_count <= 0:
109+
raise ContextWindowOverflowException("Cannot summarize: insufficient messages for summarization") from e
110+
111+
# Adjust split point to avoid breaking ToolUse/ToolResult pairs
112+
messages_to_summarize_count = self._adjust_split_point_for_tool_pairs(
113+
agent.messages, messages_to_summarize_count
114+
)
115+
116+
if messages_to_summarize_count <= 0:
117+
raise ContextWindowOverflowException("Cannot summarize: insufficient messages for summarization") from e
118+
119+
# Extract messages to summarize
120+
messages_to_summarize = agent.messages[:messages_to_summarize_count]
121+
remaining_messages = agent.messages[messages_to_summarize_count:]
122+
123+
# Generate summary
124+
summary_message = self._generate_summary(messages_to_summarize, agent)
125+
126+
# Replace the summarized messages with the summary
127+
agent.messages[:] = [summary_message] + remaining_messages
128+
129+
except Exception as summarization_error:
130+
logger.error("Summarization failed: %s", summarization_error)
131+
raise
132+
133+
def _generate_summary(self, messages: List[Message], agent: "Agent") -> Message:
134+
"""Generate a summary of the provided messages.
135+
136+
Args:
137+
messages: The messages to summarize.
138+
agent: The agent instance to use for summarization.
139+
140+
Returns:
141+
A message containing the conversation summary.
142+
143+
Raises:
144+
Exception: If summary generation fails.
145+
"""
146+
# Choose which agent to use for summarization
147+
summarization_agent = self.summarization_agent if self.summarization_agent is not None else agent
148+
149+
# Use custom system prompt if provided, otherwise use default
150+
system_prompt = (
151+
self.summarization_system_prompt
152+
if self.summarization_system_prompt is not None
153+
else DEFAULT_SUMMARIZATION_PROMPT
154+
)
155+
156+
# Save original system prompt and messages to restore later
157+
original_system_prompt = summarization_agent.system_prompt
158+
original_messages = summarization_agent.messages.copy()
159+
160+
try:
161+
# Temporarily set the system prompt and set formatted messages for summarization
162+
summarization_agent.system_prompt = system_prompt
163+
summarization_agent.messages = messages
164+
165+
# Use the agent to generate summary with rich content (can use tools if needed)
166+
result = summarization_agent("Please summarize this conversation.")
167+
168+
return result.message
169+
170+
finally:
171+
# Restore original agent state
172+
summarization_agent.system_prompt = original_system_prompt
173+
summarization_agent.messages = original_messages
174+
175+
def _adjust_split_point_for_tool_pairs(self, messages: List[Message], split_point: int) -> int:
176+
"""Adjust the split point to avoid breaking ToolUse/ToolResult pairs.
177+
178+
Args:
179+
messages: The full list of messages.
180+
split_point: The initially calculated split point.
181+
182+
Returns:
183+
The adjusted split point that doesn't break ToolUse/ToolResult pairs.
184+
"""
185+
if split_point >= len(messages):
186+
return split_point
187+
188+
# Check if the message at split_point is a ToolResult
189+
if split_point < len(messages):
190+
message_at_split = messages[split_point]
191+
if self._message_contains_tool_result(message_at_split):
192+
# Find the corresponding ToolUse by looking backwards
193+
for i in range(split_point - 1, -1, -1):
194+
if self._message_contains_tool_use(messages[i]):
195+
# Move split point to before the ToolUse
196+
return i
197+
# If no ToolUse found, move split point back by 1
198+
return max(0, split_point - 1)
199+
200+
return split_point
201+
202+
def _message_contains_tool_use(self, message: Message) -> bool:
203+
"""Check if a message contains a ToolUse."""
204+
for content in message.get("content", []):
205+
if "toolUse" in content:
206+
return True
207+
return False
208+
209+
def _message_contains_tool_result(self, message: Message) -> bool:
210+
"""Check if a message contains a ToolResult."""
211+
for content in message.get("content", []):
212+
if "toolResult" in content:
213+
return True
214+
return False

0 commit comments

Comments
 (0)