Skip to content

Commit cd507c1

Browse files
wyf7107copybara-github
authored andcommitted
feat: include thoughts and tool calls in compaction summaries
Port of GitHub PR: bdb5582 The compaction summarizer fed only message text to the LLM, dropping agent thoughts and tool calls/responses. Include them, skip a prior compaction's own thought, and reiterate the user request in the default prompt to reduce drift. Co-authored-by: Yifan Wang <wanyif@google.com> PiperOrigin-RevId: 927437249
1 parent 5b06baf commit cd507c1

2 files changed

Lines changed: 120 additions & 14 deletions

File tree

src/google/adk/apps/llm_event_summarizer.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,19 @@ class LlmEventSummarizer(BaseEventsSummarizer):
4747
"""
4848

4949
_DEFAULT_PROMPT_TEMPLATE = (
50-
'The following is a conversation history between a user and an AI'
51-
' agent. Please summarize the conversation, focusing on key'
52-
' information and decisions made, as well as any unresolved'
50+
'The following is a conversation history between a user and an AI agent.'
51+
' It may or may not start from a compacted history. Please identify and'
52+
' reiterate the user request, summarize the context so far, focusing on'
53+
' key decisions made and information obtained, as well as any unresolved'
5354
' questions or tasks. The summary should be concise and capture the'
54-
' essence of the interaction.\\n\\n{conversation_history}'
55+
' essence of the interaction.\n\n{conversation_history}'
5556
)
5657

58+
# Tool call args and responses can be large (e.g. search results). Cap how
59+
# much of each is rendered so compaction does not inflate the very context
60+
# it exists to shrink.
61+
_MAX_TOOL_CONTENT_CHARS = 2000
62+
5763
def __init__(
5864
self,
5965
llm: BaseLlm,
@@ -71,14 +77,42 @@ def __init__(
7177
self._prompt_template = prompt_template or self._DEFAULT_PROMPT_TEMPLATE
7278

7379
def _format_events_for_prompt(self, events: list[Event]) -> str:
74-
"""Formats a list of events into a string for the LLM prompt."""
80+
"""Formats events into prompt text, including thoughts and tool calls.
81+
82+
Thoughts carry the agent's analysis of tool responses, and tool calls and
83+
responses carry the evidence retrieved so far, so all three are included.
84+
Thoughts emitted by a compaction event are skipped so a prior summary's
85+
reasoning does not leak into the next summary.
86+
"""
7587
formatted_history = []
7688
for event in events:
77-
if event.content and event.content.parts:
78-
for part in event.content.parts:
79-
if part.text:
80-
formatted_history.append(f'{event.author}: {part.text}')
81-
return '\\n'.join(formatted_history)
89+
if not (event.content and event.content.parts):
90+
continue
91+
is_compaction = bool(event.actions and event.actions.compaction)
92+
for part in event.content.parts:
93+
if part.thought and part.text:
94+
if not is_compaction:
95+
formatted_history.append(f'{event.author} (thought): {part.text}')
96+
elif part.text:
97+
formatted_history.append(f'{event.author}: {part.text}')
98+
if part.function_call:
99+
args = self._truncate(str(part.function_call.args))
100+
formatted_history.append(
101+
f'{event.author} called tool: {part.function_call.name}({args})'
102+
)
103+
if part.function_response:
104+
response = self._truncate(str(part.function_response.response))
105+
formatted_history.append(
106+
f'Tool response from {part.function_response.name}: {response}'
107+
)
108+
return '\n'.join(formatted_history)
109+
110+
def _truncate(self, text: str) -> str:
111+
"""Caps `text` at the tool-content limit, marking dropped characters."""
112+
limit = self._MAX_TOOL_CONTENT_CHARS
113+
if len(text) <= limit:
114+
return text
115+
return f'{text[:limit]}... [truncated {len(text) - limit} chars]'
82116

83117
async def maybe_summarize_events(
84118
self, *, events: list[Event]

tests/unittests/apps/test_llm_event_summarizer.py

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ async def test_maybe_compact_events_success(self):
5454
self._create_event(1.0, 'Hello', 'user'),
5555
self._create_event(2.0, 'Hi there!', 'model'),
5656
]
57-
expected_conversation_history = 'user: Hello\\nmodel: Hi there!'
57+
expected_conversation_history = 'user: Hello\nmodel: Hi there!'
5858
expected_prompt = self.compactor._DEFAULT_PROMPT_TEMPLATE.format(
5959
conversation_history=expected_conversation_history
6060
)
@@ -162,7 +162,7 @@ def test_format_events_for_prompt(self):
162162
parts=[
163163
Part(
164164
function_call=FunctionCall(
165-
id='call_1', name='tool', args={}
165+
id='call_1', name='tool', args={'q': 'x'}
166166
)
167167
)
168168
]
@@ -186,8 +186,80 @@ def test_format_events_for_prompt(self):
186186
),
187187
]
188188
expected_formatted_history = (
189-
'user: User says...\\nmodel: Model replies...\\nuser: Another user'
190-
' input\\nmodel: More model text'
189+
'user: User says...\nmodel: Model replies...\nuser: Another user'
190+
' input\nmodel: More model text\nmodel called tool:'
191+
" tool({'q': 'x'})\nTool response from tool: {'result': 'done'}"
191192
)
192193
formatted_history = self.compactor._format_events_for_prompt(events)
193194
self.assertEqual(formatted_history, expected_formatted_history)
195+
196+
def test_format_events_for_prompt_includes_thoughts(self):
197+
events = [
198+
self._create_event(1.0, 'What is the weather?', 'user'),
199+
Event(
200+
timestamp=2.0,
201+
author='model',
202+
content=Content(
203+
parts=[
204+
Part(text='Let me check the tool output.', thought=True),
205+
Part(text='It is sunny.'),
206+
]
207+
),
208+
),
209+
]
210+
expected_formatted_history = (
211+
'user: What is the weather?\nmodel (thought): Let me check the tool'
212+
' output.\nmodel: It is sunny.'
213+
)
214+
formatted_history = self.compactor._format_events_for_prompt(events)
215+
self.assertEqual(formatted_history, expected_formatted_history)
216+
217+
def test_format_events_for_prompt_skips_compaction_event_thought(self):
218+
events = [
219+
Event(
220+
timestamp=1.0,
221+
author='model',
222+
content=Content(
223+
parts=[
224+
Part(text='Stale summarizer reasoning.', thought=True),
225+
Part(text='Prior summary.'),
226+
]
227+
),
228+
actions=EventActions(
229+
compaction=EventCompaction(
230+
start_timestamp=0.0,
231+
end_timestamp=1.0,
232+
compacted_content=Content(parts=[Part(text='Prior')]),
233+
)
234+
),
235+
),
236+
self._create_event(2.0, 'New user input', 'user'),
237+
]
238+
expected_formatted_history = 'model: Prior summary.\nuser: New user input'
239+
formatted_history = self.compactor._format_events_for_prompt(events)
240+
self.assertEqual(formatted_history, expected_formatted_history)
241+
242+
def test_format_events_for_prompt_truncates_large_tool_response(self):
243+
limit = self.compactor._MAX_TOOL_CONTENT_CHARS
244+
large_value = 'x' * (limit + 500)
245+
events = [
246+
Event(
247+
timestamp=1.0,
248+
author='model',
249+
content=Content(
250+
parts=[
251+
Part(
252+
function_response=FunctionResponse(
253+
id='call_1',
254+
name='search',
255+
response={'data': large_value},
256+
)
257+
)
258+
]
259+
),
260+
),
261+
]
262+
formatted_history = self.compactor._format_events_for_prompt(events)
263+
self.assertIn('Tool response from search:', formatted_history)
264+
self.assertIn('... [truncated', formatted_history)
265+
self.assertLess(len(formatted_history), len(large_value))

0 commit comments

Comments
 (0)