From 3f4bd67b49cd60e6a2e43ccd5192efe450a6e009 Mon Sep 17 00:00:00 2001 From: Hangfei Lin Date: Mon, 6 Oct 2025 22:20:17 -0700 Subject: [PATCH] fix: Make compactor optional in EventsCompactionConfig and add a default If `EventsCompactionConfig` is provided without a `compactor`, a `SlidingWindowCompactor` is now automatically instantiated using the `root_agent`'s LLM. This simplifies configuration by providing a sensible default. PiperOrigin-RevId: 816038579 --- src/google/adk/apps/app.py | 8 ++++-- ...compactor.py => base_events_summarizer.py} | 4 +-- src/google/adk/apps/compaction.py | 8 +++++- src/google/adk/apps/llm_event_summarizer.py | 6 ++-- tests/unittests/apps/test_compaction.py | 28 +++++++++++-------- .../apps/test_llm_event_summarizer.py | 6 ++-- 6 files changed, 36 insertions(+), 24 deletions(-) rename src/google/adk/apps/{base_events_compactor.py => base_events_summarizer.py} (94%) diff --git a/src/google/adk/apps/app.py b/src/google/adk/apps/app.py index 50faab62..0447a543 100644 --- a/src/google/adk/apps/app.py +++ b/src/google/adk/apps/app.py @@ -21,7 +21,7 @@ from pydantic import Field from ..agents.base_agent import BaseAgent from ..agents.context_cache_config import ContextCacheConfig -from ..apps.base_events_compactor import BaseEventsCompactor +from ..apps.base_events_summarizer import BaseEventsSummarizer from ..plugins.base_plugin import BasePlugin from ..utils.feature_decorator import experimental @@ -56,11 +56,13 @@ class EventsCompactionConfig(BaseModel): extra="forbid", ) - compactor: BaseEventsCompactor - """The event compactor strategy for the application.""" + summarizer: Optional[BaseEventsSummarizer] = None + """The event summarizer to use for compaction.""" + compaction_interval: int """The number of *new* user-initiated invocations that, once fully represented in the session's events, will trigger a compaction.""" + overlap_size: int """The number of preceding invocations to include from the end of the last compacted range. This creates an overlap between consecutive diff --git a/src/google/adk/apps/base_events_compactor.py b/src/google/adk/apps/base_events_summarizer.py similarity index 94% rename from src/google/adk/apps/base_events_compactor.py rename to src/google/adk/apps/base_events_summarizer.py index dcb875c0..5e72d7a1 100644 --- a/src/google/adk/apps/base_events_compactor.py +++ b/src/google/adk/apps/base_events_summarizer.py @@ -23,11 +23,11 @@ from ..utils.feature_decorator import experimental @experimental -class BaseEventsCompactor(abc.ABC): +class BaseEventsSummarizer(abc.ABC): """Base interface for compacting events.""" @abc.abstractmethod - async def maybe_compact_events( + async def maybe_summarize_events( self, *, events: list[Event] ) -> Optional[Event]: """Compact a list of events into a single event. diff --git a/src/google/adk/apps/compaction.py b/src/google/adk/apps/compaction.py index 83ddecfb..a6f55f9a 100644 --- a/src/google/adk/apps/compaction.py +++ b/src/google/adk/apps/compaction.py @@ -17,6 +17,7 @@ from __future__ import annotations import logging from google.adk.apps.app import App +from google.adk.apps.llm_event_summarizer import LlmEventSummarizer from google.adk.sessions.base_session_service import BaseSessionService from google.adk.sessions.session import Session @@ -180,8 +181,13 @@ async def _run_compaction_for_sliding_window( if not events_to_compact: return None + if not app.events_compaction_config.summarizer: + app.events_compaction_config.summarizer = LlmEventSummarizer( + llm=app.root_agent.canonical_model + ) + compaction_event = ( - await app.events_compaction_config.compactor.maybe_compact_events( + await app.events_compaction_config.summarizer.maybe_summarize_events( events=events_to_compact ) ) diff --git a/src/google/adk/apps/llm_event_summarizer.py b/src/google/adk/apps/llm_event_summarizer.py index ad86d891..76e3b73e 100644 --- a/src/google/adk/apps/llm_event_summarizer.py +++ b/src/google/adk/apps/llm_event_summarizer.py @@ -19,15 +19,15 @@ from google.genai import types from google.genai.types import Content from google.genai.types import Part +from ..apps.base_events_summarizer import BaseEventsSummarizer from ..events.event import Event from ..events.event_actions import EventActions from ..events.event_actions import EventCompaction from ..models.base_llm import BaseLlm from ..models.llm_request import LlmRequest -from .base_events_compactor import BaseEventsCompactor -class LlmEventSummarizer(BaseEventsCompactor): +class LlmEventSummarizer(BaseEventsSummarizer): """An LLM-based event summarizer for sliding window compaction. This class is responsible for summarizing a provided list of events into a @@ -81,7 +81,7 @@ class LlmEventSummarizer(BaseEventsCompactor): formatted_history.append(f'{event.author}: {part.text}') return '\\n'.join(formatted_history) - async def maybe_compact_events( + async def maybe_summarize_events( self, *, events: list[Event] ) -> Optional[Event]: """Compacts given events and returns the compacted content. diff --git a/tests/unittests/apps/test_compaction.py b/tests/unittests/apps/test_compaction.py index c0a427ab..483ea390 100644 --- a/tests/unittests/apps/test_compaction.py +++ b/tests/unittests/apps/test_compaction.py @@ -72,7 +72,7 @@ class TestCompaction(unittest.IsolatedAsyncioTestCase): await _run_compaction_for_sliding_window( app, session, self.mock_session_service ) - self.mock_compactor.maybe_compact_events.assert_not_called() + self.mock_compactor.maybe_summarize_events.assert_not_called() self.mock_session_service.append_event.assert_not_called() async def test_run_compaction_for_sliding_window_not_enough_new_invocations( @@ -82,7 +82,7 @@ class TestCompaction(unittest.IsolatedAsyncioTestCase): name='test', root_agent=Mock(spec=BaseAgent), events_compaction_config=EventsCompactionConfig( - compactor=self.mock_compactor, + summarizer=self.mock_compactor, compaction_interval=3, overlap_size=1, ), @@ -100,7 +100,7 @@ class TestCompaction(unittest.IsolatedAsyncioTestCase): await _run_compaction_for_sliding_window( app, session, self.mock_session_service ) - self.mock_compactor.maybe_compact_events.assert_not_called() + self.mock_compactor.maybe_summarize_events.assert_not_called() self.mock_session_service.append_event.assert_not_called() async def test_run_compaction_for_sliding_window_first_compaction(self): @@ -108,7 +108,7 @@ class TestCompaction(unittest.IsolatedAsyncioTestCase): name='test', root_agent=Mock(spec=BaseAgent), events_compaction_config=EventsCompactionConfig( - compactor=self.mock_compactor, + summarizer=self.mock_compactor, compaction_interval=2, overlap_size=1, ), @@ -124,14 +124,16 @@ class TestCompaction(unittest.IsolatedAsyncioTestCase): mock_compacted_event = self._create_compacted_event( 1.0, 4.0, 'Summary inv1-inv4' ) - self.mock_compactor.maybe_compact_events.return_value = mock_compacted_event + self.mock_compactor.maybe_summarize_events.return_value = ( + mock_compacted_event + ) await _run_compaction_for_sliding_window( app, session, self.mock_session_service ) # Expected events to compact: inv1, inv2, inv3, inv4 - compacted_events_arg = self.mock_compactor.maybe_compact_events.call_args[ + compacted_events_arg = self.mock_compactor.maybe_summarize_events.call_args[ 1 ]['events'] self.assertEqual( @@ -147,7 +149,7 @@ class TestCompaction(unittest.IsolatedAsyncioTestCase): name='test', root_agent=Mock(spec=BaseAgent), events_compaction_config=EventsCompactionConfig( - compactor=self.mock_compactor, + summarizer=self.mock_compactor, compaction_interval=2, overlap_size=1, ), @@ -174,7 +176,9 @@ class TestCompaction(unittest.IsolatedAsyncioTestCase): mock_compacted_event = self._create_compacted_event( 2.0, 5.0, 'Summary inv2-inv5' ) - self.mock_compactor.maybe_compact_events.return_value = mock_compacted_event + self.mock_compactor.maybe_summarize_events.return_value = ( + mock_compacted_event + ) await _run_compaction_for_sliding_window( app, session, self.mock_session_service @@ -183,7 +187,7 @@ class TestCompaction(unittest.IsolatedAsyncioTestCase): # New invocations are inv3, inv4, inv5 (3 new) > threshold (2). # Overlap size is 1, so start from 1 inv before inv3, which is inv2. # Compact range: inv2 to inv5. - compacted_events_arg = self.mock_compactor.maybe_compact_events.call_args[ + compacted_events_arg = self.mock_compactor.maybe_summarize_events.call_args[ 1 ]['events'] self.assertEqual( @@ -201,7 +205,7 @@ class TestCompaction(unittest.IsolatedAsyncioTestCase): name='test', root_agent=Mock(spec=BaseAgent), events_compaction_config=EventsCompactionConfig( - compactor=self.mock_compactor, + summarizer=self.mock_compactor, compaction_interval=1, overlap_size=0, ), @@ -209,11 +213,11 @@ class TestCompaction(unittest.IsolatedAsyncioTestCase): events = [self._create_event(1.0, 'inv1', 'e1')] session = Session(app_name='test', user_id='u1', id='s1', events=events) - self.mock_compactor.maybe_compact_events.return_value = None + self.mock_compactor.maybe_summarize_events.return_value = None await _run_compaction_for_sliding_window( app, session, self.mock_session_service ) - self.mock_compactor.maybe_compact_events.assert_called_once() + self.mock_compactor.maybe_summarize_events.assert_called_once() self.mock_session_service.append_event.assert_not_called() diff --git a/tests/unittests/apps/test_llm_event_summarizer.py b/tests/unittests/apps/test_llm_event_summarizer.py index af190128..4ced5d3f 100644 --- a/tests/unittests/apps/test_llm_event_summarizer.py +++ b/tests/unittests/apps/test_llm_event_summarizer.py @@ -64,7 +64,7 @@ class TestLlmEventSummarizer(unittest.IsolatedAsyncioTestCase): self.mock_llm.generate_content_async.return_value = async_gen() - compacted_event = await self.compactor.maybe_compact_events(events=events) + compacted_event = await self.compactor.maybe_summarize_events(events=events) self.assertIsNotNone(compacted_event) self.assertEqual( @@ -101,11 +101,11 @@ class TestLlmEventSummarizer(unittest.IsolatedAsyncioTestCase): self.mock_llm.generate_content_async.return_value = async_gen() - compacted_event = await self.compactor.maybe_compact_events(events=events) + compacted_event = await self.compactor.maybe_summarize_events(events=events) self.assertIsNone(compacted_event) async def test_maybe_compact_events_empty_input(self): - compacted_event = await self.compactor.maybe_compact_events(events=[]) + compacted_event = await self.compactor.maybe_summarize_events(events=[]) self.assertIsNone(compacted_event) self.mock_llm.generate_content_async.assert_not_called()