diff --git a/changelog/3863.added.2.md b/changelog/3863.added.2.md
new file mode 100644
index 000000000..9c0ab90ba
--- /dev/null
+++ b/changelog/3863.added.2.md
@@ -0,0 +1 @@
+- Added `LLMContextSummaryConfig` (summary generation params: `target_context_tokens`, `min_messages_after_summary`, `summarization_prompt`) and `LLMAutoContextSummarizationConfig` (auto-trigger thresholds: `max_context_tokens`, `max_unsummarized_messages`, plus a nested `summary_config`). These replace the monolithic `LLMContextSummarizationConfig`.
diff --git a/changelog/3863.added.md b/changelog/3863.added.md
new file mode 100644
index 000000000..d6214aed0
--- /dev/null
+++ b/changelog/3863.added.md
@@ -0,0 +1 @@
+- Added `LLMSummarizeContextFrame` to trigger on-demand context summarization from anywhere in the pipeline (e.g. a function call tool). Accepts an optional `config: LLMContextSummaryConfig` to override summary generation settings per request.
diff --git a/changelog/3863.changed.md b/changelog/3863.changed.md
new file mode 100644
index 000000000..faf5712d8
--- /dev/null
+++ b/changelog/3863.changed.md
@@ -0,0 +1 @@
+- ⚠️ Renamed `LLMAssistantAggregatorParams` fields: `enable_context_summarization` → `enable_auto_context_summarization` and `context_summarization_config` → `auto_context_summarization_config` (now accepts `LLMAutoContextSummarizationConfig`). The old names still work with a `DeprecationWarning` for one release cycle.
diff --git a/changelog/3863.deprecated.md b/changelog/3863.deprecated.md
new file mode 100644
index 000000000..ba2311fbd
--- /dev/null
+++ b/changelog/3863.deprecated.md
@@ -0,0 +1 @@
+- Deprecated `LLMContextSummarizationConfig`. Use `LLMAutoContextSummarizationConfig` with a nested `LLMContextSummaryConfig` instead. The old class emits a `DeprecationWarning`.
diff --git a/examples/foundational/54-context-summarization-openai.py b/examples/foundational/54-context-summarization-openai.py
index 45f27854f..ff6701bec 100644
--- a/examples/foundational/54-context-summarization-openai.py
+++ b/examples/foundational/54-context-summarization-openai.py
@@ -41,7 +41,10 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams
-from pipecat.utils.context.llm_context_summarization import LLMContextSummarizationConfig
+from pipecat.utils.context.llm_context_summarization import (
+ LLMAutoContextSummarizationConfig,
+ LLMContextSummaryConfig,
+)
load_dotenv(override=True)
@@ -120,14 +123,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
vad_analyzer=SileroVADAnalyzer(),
),
assistant_params=LLMAssistantAggregatorParams(
- enable_context_summarization=True,
+ enable_auto_context_summarization=True,
# Optional: customize context summarization behavior
# Using low limits to demonstrate the feature quickly
- context_summarization_config=LLMContextSummarizationConfig(
+ auto_context_summarization_config=LLMAutoContextSummarizationConfig(
max_context_tokens=1000, # Trigger summarization at 1000 tokens
- target_context_tokens=800, # Target context size for the summarization
max_unsummarized_messages=10, # Or when 10 new messages accumulate
- min_messages_after_summary=2, # Keep last 2 messages uncompressed
+ summary_config=LLMContextSummaryConfig(
+ target_context_tokens=800, # Target context size for the summarization
+ min_messages_after_summary=2, # Keep last 2 messages uncompressed
+ ),
),
),
)
diff --git a/examples/foundational/54a-context-summarization-google.py b/examples/foundational/54a-context-summarization-google.py
index 2ce29e959..7d2a91310 100644
--- a/examples/foundational/54a-context-summarization-google.py
+++ b/examples/foundational/54a-context-summarization-google.py
@@ -41,7 +41,10 @@ from pipecat.services.llm_service import FunctionCallParams
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams
-from pipecat.utils.context.llm_context_summarization import LLMContextSummarizationConfig
+from pipecat.utils.context.llm_context_summarization import (
+ LLMAutoContextSummarizationConfig,
+ LLMContextSummaryConfig,
+)
load_dotenv(override=True)
@@ -120,14 +123,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
vad_analyzer=SileroVADAnalyzer(),
),
assistant_params=LLMAssistantAggregatorParams(
- enable_context_summarization=True,
+ enable_auto_context_summarization=True,
# Optional: customize context summarization behavior
# Using low limits to demonstrate the feature quickly
- context_summarization_config=LLMContextSummarizationConfig(
+ auto_context_summarization_config=LLMAutoContextSummarizationConfig(
max_context_tokens=1000, # Trigger summarization at 1000 tokens
- target_context_tokens=800, # Target context size for the summarization
max_unsummarized_messages=10, # Or when 10 new messages accumulate
- min_messages_after_summary=2, # Keep last 2 messages uncompressed
+ summary_config=LLMContextSummaryConfig(
+ target_context_tokens=800, # Target context size for the summarization
+ min_messages_after_summary=2, # Keep last 2 messages uncompressed
+ ),
),
),
)
diff --git a/examples/foundational/54b-context-summarization-manual-openai.py b/examples/foundational/54b-context-summarization-manual-openai.py
new file mode 100644
index 000000000..e8acf4bf1
--- /dev/null
+++ b/examples/foundational/54b-context-summarization-manual-openai.py
@@ -0,0 +1,179 @@
+#
+# Copyright (c) 2024-2026, Daily
+#
+# SPDX-License-Identifier: BSD 2-Clause License
+#
+
+"""Example demonstrating manual context summarization via a function call.
+
+This example shows how to trigger context summarization on demand rather than
+automatically. The user can ask the bot to "summarize the conversation" and the
+bot will call a function that pushes an LLMSummarizeContextFrame into the
+pipeline, causing the LLM service to compress the conversation history.
+
+Unlike example 54, automatic summarization is NOT enabled here. Summarization
+only happens when the user explicitly requests it through the function call.
+"""
+
+import os
+
+from dotenv import load_dotenv
+from loguru import logger
+
+from pipecat.adapters.schemas.function_schema import FunctionSchema
+from pipecat.adapters.schemas.tools_schema import ToolsSchema
+from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnalyzerV3
+from pipecat.audio.vad.silero import SileroVADAnalyzer
+from pipecat.audio.vad.vad_analyzer import VADParams
+from pipecat.frames.frames import LLMRunFrame, LLMSummarizeContextFrame
+from pipecat.pipeline.pipeline import Pipeline
+from pipecat.pipeline.runner import PipelineRunner
+from pipecat.pipeline.task import PipelineParams, PipelineTask
+from pipecat.processors.aggregators.llm_context import LLMContext
+from pipecat.processors.aggregators.llm_response_universal import (
+ LLMContextAggregatorPair,
+ LLMUserAggregatorParams,
+)
+from pipecat.runner.types import RunnerArguments
+from pipecat.runner.utils import create_transport
+from pipecat.services.cartesia.tts import CartesiaTTSService
+from pipecat.services.deepgram.stt import DeepgramSTTService
+from pipecat.services.llm_service import FunctionCallParams
+from pipecat.services.openai.llm import OpenAILLMService
+from pipecat.transports.base_transport import BaseTransport, TransportParams
+from pipecat.transports.daily.transport import DailyParams
+from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams
+from pipecat.turns.user_stop import TurnAnalyzerUserTurnStopStrategy
+from pipecat.turns.user_turn_strategies import UserTurnStrategies
+
+load_dotenv(override=True)
+
+# We use lambdas to defer transport parameter creation until the transport
+# type is selected at runtime.
+transport_params = {
+ "daily": lambda: DailyParams(
+ audio_in_enabled=True,
+ audio_out_enabled=True,
+ ),
+ "twilio": lambda: FastAPIWebsocketParams(
+ audio_in_enabled=True,
+ audio_out_enabled=True,
+ ),
+ "webrtc": lambda: TransportParams(
+ audio_in_enabled=True,
+ audio_out_enabled=True,
+ ),
+}
+
+
+async def summarize_conversation(params: FunctionCallParams):
+ """Trigger manual context summarization via a pipeline frame."""
+ logger.info("Tool called: summarize_conversation")
+ await params.result_callback({"status": "summarization_requested"})
+ await params.llm.queue_frame(LLMSummarizeContextFrame())
+
+
+async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
+ logger.info("Starting bot")
+
+ stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY"))
+
+ tts = CartesiaTTSService(
+ api_key=os.getenv("CARTESIA_API_KEY"),
+ voice_id="71a7ad14-091c-4e8e-a314-022ece01c121", # British Reading Lady
+ )
+
+ llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"))
+
+ llm.register_function("summarize_conversation", summarize_conversation)
+
+ summarize_function = FunctionSchema(
+ name="summarize_conversation",
+ description=(
+ "Summarize and compress the conversation history. "
+ "Call this when the user asks you to summarize the conversation "
+ "or when you want to free up context space."
+ ),
+ properties={},
+ required=[],
+ )
+ tools = ToolsSchema(standard_tools=[summarize_function])
+
+ messages = [
+ {
+ "role": "system",
+ "content": (
+ "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your "
+ "capabilities in a succinct way. Your output will be spoken aloud, so avoid "
+ "special characters that can't easily be spoken, such as emojis or bullet points. "
+ "Respond to what the user said in a creative and helpful way. "
+ "If the user asks you to summarize the conversation, call the "
+ "summarize_conversation function. After summarization, briefly acknowledge "
+ "that the conversation history has been compressed."
+ ),
+ },
+ ]
+
+ context = LLMContext(messages, tools=tools)
+
+ # Automatic summarization is NOT enabled here (enable_auto_context_summarization
+ # defaults to False). The summarizer is still created internally so that
+ # LLMSummarizeContextFrame frames pushed via the function call are handled.
+ user_aggregator, assistant_aggregator = LLMContextAggregatorPair(
+ context,
+ user_params=LLMUserAggregatorParams(
+ user_turn_strategies=UserTurnStrategies(
+ stop=[TurnAnalyzerUserTurnStopStrategy(turn_analyzer=LocalSmartTurnAnalyzerV3())]
+ ),
+ vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)),
+ ),
+ )
+
+ pipeline = Pipeline(
+ [
+ transport.input(), # Transport user input
+ stt,
+ user_aggregator, # User responses
+ llm, # LLM
+ tts, # TTS
+ transport.output(), # Transport bot output
+ assistant_aggregator, # Assistant spoken responses
+ ]
+ )
+
+ task = PipelineTask(
+ pipeline,
+ params=PipelineParams(
+ enable_metrics=True,
+ enable_usage_metrics=True,
+ ),
+ idle_timeout_secs=runner_args.pipeline_idle_timeout_secs,
+ )
+
+ @transport.event_handler("on_client_connected")
+ async def on_client_connected(transport, client):
+ logger.info("Client connected")
+ # Kick off the conversation.
+ messages.append({"role": "system", "content": "Please introduce yourself to the user."})
+ await task.queue_frames([LLMRunFrame()])
+
+ @transport.event_handler("on_client_disconnected")
+ async def on_client_disconnected(transport, client):
+ logger.info("Client disconnected")
+ await task.cancel()
+
+ runner = PipelineRunner(handle_sigint=runner_args.handle_sigint)
+
+ await runner.run(task)
+
+
+async def bot(runner_args: RunnerArguments):
+ """Main bot entry point compatible with Pipecat Cloud."""
+ transport = await create_transport(runner_args, transport_params)
+ await run_bot(transport, runner_args)
+
+
+if __name__ == "__main__":
+ from pipecat.runner.run import main
+
+ main()
diff --git a/examples/foundational/54c-context-summarization-dedicated-llm.py b/examples/foundational/54c-context-summarization-dedicated-llm.py
index 3b2195e80..1dce3890f 100644
--- a/examples/foundational/54c-context-summarization-dedicated-llm.py
+++ b/examples/foundational/54c-context-summarization-dedicated-llm.py
@@ -44,7 +44,10 @@ from pipecat.services.openai.llm import OpenAILLMService
from pipecat.transports.base_transport import BaseTransport, TransportParams
from pipecat.transports.daily.transport import DailyParams
from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams
-from pipecat.utils.context.llm_context_summarization import LLMContextSummarizationConfig
+from pipecat.utils.context.llm_context_summarization import (
+ LLMAutoContextSummarizationConfig,
+ LLMContextSummaryConfig,
+)
load_dotenv(override=True)
@@ -147,23 +150,25 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
vad_analyzer=SileroVADAnalyzer(),
),
assistant_params=LLMAssistantAggregatorParams(
- enable_context_summarization=True,
- context_summarization_config=LLMContextSummarizationConfig(
+ enable_auto_context_summarization=True,
+ auto_context_summarization_config=LLMAutoContextSummarizationConfig(
# Trigger thresholds (low values to demonstrate quickly)
max_context_tokens=1000,
max_unsummarized_messages=10,
- # Summary generation
- target_context_tokens=800,
- min_messages_after_summary=2,
- summarization_prompt=CUSTOM_SUMMARIZATION_PROMPT,
- # Custom summary format - wrap in XML tags so the system
- # prompt can identify summaries vs. live conversation
- summary_message_template="\n{summary}\n",
- # Use a dedicated cheap LLM for summarization instead of
- # the primary conversation model
- llm=summarization_llm,
- # Cancel summarization if it takes longer than 60 seconds
- summarization_timeout=60.0,
+ summary_config=LLMContextSummaryConfig(
+ # Summary generation
+ target_context_tokens=800,
+ min_messages_after_summary=2,
+ summarization_prompt=CUSTOM_SUMMARIZATION_PROMPT,
+ # Custom summary format - wrap in XML tags so the system
+ # prompt can identify summaries vs. live conversation
+ summary_message_template="\n{summary}\n",
+ # Use a dedicated cheap LLM for summarization instead of
+ # the primary conversation model
+ llm=summarization_llm,
+ # Cancel summarization if it takes longer than 60 seconds
+ summarization_timeout=60.0,
+ ),
),
),
)
diff --git a/src/pipecat/frames/frames.py b/src/pipecat/frames/frames.py
index e1d2c37ff..126f3c001 100644
--- a/src/pipecat/frames/frames.py
+++ b/src/pipecat/frames/frames.py
@@ -43,6 +43,7 @@ if TYPE_CHECKING:
from pipecat.processors.aggregators.llm_context import LLMContext, NotGiven
from pipecat.processors.frame_processor import FrameProcessor
from pipecat.services.settings import ServiceSettings
+ from pipecat.utils.context.llm_context_summarization import LLMContextSummaryConfig
from pipecat.utils.tracing.tracing_context import TracingContext
@@ -2000,6 +2001,22 @@ class LLMAssistantPushAggregationFrame(ControlFrame):
"""
+@dataclass
+class LLMSummarizeContextFrame(ControlFrame):
+ """Frame requesting on-demand context summarization.
+
+ Push this frame into the pipeline to trigger a manual context summarization.
+
+ Parameters:
+ config: Optional per-request override for summary generation settings
+ (prompt, token budget, messages to keep). If ``None``, the
+ summarizer's default :class:`~pipecat.utils.context.llm_context_summarization.LLMContextSummaryConfig`
+ is used.
+ """
+
+ config: Optional["LLMContextSummaryConfig"] = None
+
+
@dataclass
class LLMContextSummaryRequestFrame(ControlFrame):
"""Frame requesting context summarization from an LLM service.
diff --git a/src/pipecat/processors/aggregators/llm_context_summarizer.py b/src/pipecat/processors/aggregators/llm_context_summarizer.py
index bfdbbceb0..54879a8bb 100644
--- a/src/pipecat/processors/aggregators/llm_context_summarizer.py
+++ b/src/pipecat/processors/aggregators/llm_context_summarizer.py
@@ -19,14 +19,16 @@ from pipecat.frames.frames import (
LLMContextSummaryRequestFrame,
LLMContextSummaryResultFrame,
LLMFullResponseStartFrame,
+ LLMSummarizeContextFrame,
)
from pipecat.processors.aggregators.llm_context import LLMContext, LLMSpecificMessage
from pipecat.utils.asyncio.task_manager import BaseTaskManager
from pipecat.utils.base_object import BaseObject
from pipecat.utils.context.llm_context_summarization import (
DEFAULT_SUMMARIZATION_TIMEOUT,
- LLMContextSummarizationConfig,
+ LLMAutoContextSummarizationConfig,
LLMContextSummarizationUtil,
+ LLMContextSummaryConfig,
)
if TYPE_CHECKING:
@@ -55,9 +57,20 @@ class SummaryAppliedEvent:
class LLMContextSummarizer(BaseObject):
"""Summarizer for managing LLM context summarization.
- This class manages automatic context summarization when token or message
- limits are reached. It monitors the LLM context size, triggers
- summarization requests, and applies the results to compress conversation history.
+ This class manages context summarization, either automatically when token or
+ message limits are reached, or on-demand when an ``LLMSummarizeContextFrame``
+ is received. It monitors the LLM context size, triggers summarization requests,
+ and applies the results to compress conversation history.
+
+ When ``auto_trigger=True`` (the default), summarization is triggered
+ automatically based on the configured thresholds in
+ ``LLMAutoContextSummarizationConfig``. When ``auto_trigger=False``,
+ threshold checks are skipped and summarization only happens when an
+ ``LLMSummarizeContextFrame`` is explicitly pushed into the pipeline.
+
+ Both modes can coexist: set ``auto_trigger=True`` and also push
+ ``LLMSummarizeContextFrame`` at any time to force an immediate summarization
+ (subject to the ``_summarization_in_progress`` guard).
Event handlers available:
@@ -88,18 +101,26 @@ class LLMContextSummarizer(BaseObject):
self,
*,
context: LLMContext,
- config: Optional[LLMContextSummarizationConfig] = None,
+ config: Optional[LLMAutoContextSummarizationConfig] = None,
+ auto_trigger: bool = True,
):
"""Initialize the context summarizer.
Args:
context: The LLM context to monitor and summarize.
- config: Configuration for summarization behavior. If None, uses default config.
+ config: Auto-summarization configuration controlling both trigger
+ thresholds and default summary generation parameters. If None,
+ uses default ``LLMAutoContextSummarizationConfig`` values.
+ auto_trigger: Whether to automatically trigger summarization when
+ thresholds are reached. When False, summarization only happens
+ when an ``LLMSummarizeContextFrame`` is pushed into the pipeline.
+ Defaults to True.
"""
super().__init__()
self._context = context
- self._config = config or LLMContextSummarizationConfig()
+ self._auto_config = config or LLMAutoContextSummarizationConfig()
+ self._auto_trigger = auto_trigger
self._task_manager: Optional[BaseTaskManager] = None
@@ -137,6 +158,8 @@ class LLMContextSummarizer(BaseObject):
"""
if isinstance(frame, LLMFullResponseStartFrame):
await self._handle_llm_response_start(frame)
+ elif isinstance(frame, LLMSummarizeContextFrame):
+ await self._handle_manual_summarization_request(frame)
elif isinstance(frame, LLMContextSummaryResultFrame):
await self._handle_summary_result(frame)
elif isinstance(frame, InterruptionFrame):
@@ -151,12 +174,24 @@ class LLMContextSummarizer(BaseObject):
if self._should_summarize():
await self._request_summarization()
- async def _handle_interruption(self):
- """Handle interruption by canceling summarization in progress.
+ async def _handle_manual_summarization_request(self, frame: LLMSummarizeContextFrame):
+ """Handle an explicit on-demand summarization request.
+
+ Reuses the same ``_request_summarization()`` code path as auto mode,
+ so bookkeeping (``_summarization_in_progress``,
+ ``_pending_summary_request_id``) is always updated correctly.
Args:
- frame: The interruption frame.
+ frame: The manual summarization request frame, optionally carrying
+ a per-request :class:`~pipecat.utils.context.llm_context_summarization.LLMContextSummaryConfig`.
"""
+ if self._summarization_in_progress:
+ logger.debug(f"{self}: Summarization already in progress, ignoring manual request")
+ return
+ await self._request_summarization(config_override=frame.config)
+
+ async def _handle_interruption(self):
+ """Handle interruption by canceling summarization in progress."""
# Reset summarization state to allow new requests. This is necessary because
# the request frame (LLMContextSummaryRequestFrame) may have been cancelled
# during interruption. We preserve _pending_summary_request_id to handle the
@@ -179,13 +214,17 @@ class LLMContextSummarizer(BaseObject):
Returns:
True if all conditions are met:
+ - ``auto_trigger`` is enabled
- No summarization currently in progress
- AND either:
- - Token count exceeds max_context_tokens
- - OR message count exceeds max_unsummarized_messages since last summary
+ - Token count exceeds ``max_context_tokens``
+ - OR message count exceeds ``max_unsummarized_messages`` since last summary
"""
logger.trace(f"{self}: Checking if context summarization is needed")
+ if not self._auto_trigger:
+ return False
+
if self._summarization_in_progress:
logger.debug(f"{self}: Summarization already in progress")
return False
@@ -195,20 +234,20 @@ class LLMContextSummarizer(BaseObject):
num_messages = len(self._context.messages)
# Check if we've reached the token limit
- token_limit = self._config.max_context_tokens
+ token_limit = self._auto_config.max_context_tokens
token_limit_exceeded = total_tokens >= token_limit
# Check if we've exceeded max unsummarized messages
messages_since_summary = len(self._context.messages) - 1
message_threshold_exceeded = (
- messages_since_summary >= self._config.max_unsummarized_messages
+ messages_since_summary >= self._auto_config.max_unsummarized_messages
)
logger.trace(
f"{self}: Context has {num_messages} messages, "
f"~{total_tokens} tokens (limit: {token_limit}), "
f"{messages_since_summary} messages since last summary "
- f"(message threshold: {self._config.max_unsummarized_messages})"
+ f"(message threshold: {self._auto_config.max_unsummarized_messages})"
)
# Trigger if either limit is exceeded
@@ -223,23 +262,30 @@ class LLMContextSummarizer(BaseObject):
reason.append(f"~{total_tokens} tokens (>={token_limit} limit)")
if message_threshold_exceeded:
reason.append(
- f"{messages_since_summary} messages (>={self._config.max_unsummarized_messages} threshold)"
+ f"{messages_since_summary} messages (>={self._auto_config.max_unsummarized_messages} threshold)"
)
logger.debug(f"{self}: ✓ Summarization needed - {', '.join(reason)}")
return True
- async def _request_summarization(self):
+ async def _request_summarization(
+ self, config_override: Optional[LLMContextSummaryConfig] = None
+ ):
"""Request context summarization from LLM service.
Creates a summarization request frame and either handles it directly
using a dedicated LLM (if configured) or emits it via event handler
- for the pipeline's primary LLM. Tracks the request ID to match async
- responses and prevent race conditions.
+ for the pipeline's primary LLM.
+ Tracks the request ID to match async responses and prevent race conditions.
+
+ Args:
+ config_override: Optional per-request summary configuration. If provided,
+ overrides the default summary generation settings from
+ ``self._auto_config.summary_config``.
"""
# Generate unique request ID
request_id = str(uuid.uuid4())
- min_keep = self._config.min_messages_after_summary
+ summary_config = config_override or self._auto_config.summary_config
# Mark summarization in progress
self._summarization_in_progress = True
@@ -251,16 +297,16 @@ class LLMContextSummarizer(BaseObject):
request_frame = LLMContextSummaryRequestFrame(
request_id=request_id,
context=self._context,
- min_messages_to_keep=min_keep,
- target_context_tokens=self._config.target_context_tokens,
- summarization_prompt=self._config.summary_prompt,
- summarization_timeout=self._config.summarization_timeout,
+ min_messages_to_keep=summary_config.min_messages_after_summary,
+ target_context_tokens=summary_config.target_context_tokens,
+ summarization_prompt=summary_config.summary_prompt,
+ summarization_timeout=summary_config.summarization_timeout,
)
- if self._config.llm:
+ if summary_config.llm:
# Use dedicated LLM directly — no need to involve the pipeline
self.task_manager.create_task(
- self._generate_summary_with_dedicated_llm(self._config.llm, request_frame),
+ self._generate_summary_with_dedicated_llm(summary_config.llm, request_frame),
f"{self}-dedicated-llm-summary",
)
else:
@@ -323,7 +369,9 @@ class LLMContextSummarizer(BaseObject):
"""
logger.debug(f"{self}: Received summary result (request_id={frame.request_id})")
- # Check if this is the result we're waiting for
+ # Check if this is the result we're waiting for. Both auto and manual
+ # summarization set _pending_summary_request_id via _request_summarization(),
+ # so this check always applies.
if frame.request_id != self._pending_summary_request_id:
logger.debug(f"{self}: Ignoring stale summary result (request_id={frame.request_id})")
return
@@ -360,7 +408,7 @@ class LLMContextSummarizer(BaseObject):
if last_summarized_index >= len(self._context.messages):
return False
- min_keep = self._config.min_messages_after_summary
+ min_keep = self._auto_config.summary_config.min_messages_after_summary
remaining = len(self._context.messages) - 1 - last_summarized_index
if remaining < min_keep:
return False
@@ -377,6 +425,7 @@ class LLMContextSummarizer(BaseObject):
summary: The generated summary text.
last_summarized_index: Index of the last message that was summarized.
"""
+ config = self._auto_config.summary_config
messages = self._context.messages
# Find the first system message to preserve. LLMSpecificMessage instances are excluded
@@ -397,7 +446,7 @@ class LLMContextSummarizer(BaseObject):
# Create summary message as a user message (the summary is context
# provided *to* the assistant, not something the assistant said)
- summary_content = self._config.summary_message_template.format(summary=summary)
+ summary_content = config.summary_message_template.format(summary=summary)
summary_message = {"role": "user", "content": summary_content}
# Reconstruct context
diff --git a/src/pipecat/processors/aggregators/llm_response_universal.py b/src/pipecat/processors/aggregators/llm_response_universal.py
index b255748e0..c43cc279d 100644
--- a/src/pipecat/processors/aggregators/llm_response_universal.py
+++ b/src/pipecat/processors/aggregators/llm_response_universal.py
@@ -79,7 +79,10 @@ from pipecat.turns.user_stop import BaseUserTurnStopStrategy, UserTurnStoppedPar
from pipecat.turns.user_turn_completion_mixin import UserTurnCompletionConfig
from pipecat.turns.user_turn_controller import UserTurnController
from pipecat.turns.user_turn_strategies import ExternalUserTurnStrategies, UserTurnStrategies
-from pipecat.utils.context.llm_context_summarization import LLMContextSummarizationConfig
+from pipecat.utils.context.llm_context_summarization import (
+ LLMAutoContextSummarizationConfig,
+ LLMContextSummarizationConfig,
+)
from pipecat.utils.string import TextPartForConcatenation, concatenate_aggregated_text
from pipecat.utils.time import time_now_iso8601
@@ -125,18 +128,54 @@ class LLMAssistantAggregatorParams:
in text frames by adding spaces between tokens. This parameter is
ignored when used with the newer LLMAssistantAggregator, which
handles word spacing automatically.
- enable_context_summarization: Enable automatic context summarization when token
- limits are reached (disabled by default). When enabled, older conversation
- messages are automatically compressed into summaries to manage context size.
- context_summarization_config: Configuration for context summarization behavior.
- Controls thresholds, message preservation, and summarization prompts. If None
- and summarization is enabled, uses default configuration values.
+ enable_auto_context_summarization: Enable automatic context summarization when token
+ or message-count limits are reached (disabled by default). When enabled,
+ older conversation messages are automatically compressed into summaries to
+ manage context size.
+ auto_context_summarization_config: Configuration for automatic context
+ summarization. Controls trigger thresholds, message preservation, and
+ summarization prompts. If None, uses default
+ ``LLMAutoContextSummarizationConfig`` values.
"""
expect_stripped_words: bool = True
- enable_context_summarization: bool = False
+ enable_auto_context_summarization: bool = False
+ auto_context_summarization_config: Optional[LLMAutoContextSummarizationConfig] = None
+
+ # ---------------------------------------------------------------------------
+ # Deprecated field names — kept for backward compatibility.
+ # Use enable_auto_context_summarization and auto_context_summarization_config instead.
+ # ---------------------------------------------------------------------------
+ enable_context_summarization: Optional[bool] = None
context_summarization_config: Optional[LLMContextSummarizationConfig] = None
+ def __post_init__(self):
+ if self.enable_context_summarization is not None:
+ warnings.warn(
+ "LLMAssistantAggregatorParams.enable_context_summarization is deprecated. "
+ "Use enable_auto_context_summarization instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ self.enable_auto_context_summarization = self.enable_context_summarization
+ self.enable_context_summarization = None
+
+ if self.context_summarization_config is not None:
+ warnings.warn(
+ "LLMAssistantAggregatorParams.context_summarization_config is deprecated. "
+ "Use auto_context_summarization_config (LLMAutoContextSummarizationConfig) instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ if isinstance(self.context_summarization_config, LLMContextSummarizationConfig):
+ self.auto_context_summarization_config = (
+ self.context_summarization_config.to_auto_config()
+ )
+ else:
+ # Accept LLMAutoContextSummarizationConfig passed to the deprecated field
+ self.auto_context_summarization_config = self.context_summarization_config # type: ignore[assignment]
+ self.context_summarization_config = None
+
@dataclass
class UserTurnStoppedMessage:
@@ -825,16 +864,18 @@ class LLMAssistantAggregator(LLMContextAggregator):
self._thought_aggregation: List[TextPartForConcatenation] = []
self._thought_start_time: str = ""
- # Context summarization
- self._summarizer: Optional[LLMContextSummarizer] = None
- if self._params.enable_context_summarization:
- self._summarizer = LLMContextSummarizer(
- context=self._context,
- config=self._params.context_summarization_config,
- )
- self._summarizer.add_event_handler(
- "on_request_summarization", self._on_request_summarization
- )
+ # Context summarization — always create the summarizer so that manually
+ # pushed LLMSummarizeContextFrame frames are always handled.
+ # Auto-triggering based on thresholds is only enabled when
+ # enable_auto_context_summarization is True.
+ self._summarizer: Optional[LLMContextSummarizer] = LLMContextSummarizer(
+ context=self._context,
+ config=self._params.auto_context_summarization_config,
+ auto_trigger=self._params.enable_auto_context_summarization,
+ )
+ self._summarizer.add_event_handler(
+ "on_request_summarization", self._on_request_summarization
+ )
self._register_event_handler("on_assistant_turn_started")
self._register_event_handler("on_assistant_turn_stopped")
diff --git a/src/pipecat/utils/context/llm_context_summarization.py b/src/pipecat/utils/context/llm_context_summarization.py
index 0bdebb3a2..e68311942 100644
--- a/src/pipecat/utils/context/llm_context_summarization.py
+++ b/src/pipecat/utils/context/llm_context_summarization.py
@@ -10,7 +10,8 @@ This module provides reusable functionality for automatically compressing conver
context when token limits are reached, enabling efficient long-running conversations.
"""
-from dataclasses import dataclass
+import warnings
+from dataclasses import dataclass, field
from typing import TYPE_CHECKING, List, Optional
if TYPE_CHECKING:
@@ -54,26 +55,18 @@ The conversation transcript follows. Generate only the summary, no other text.""
@dataclass
-class LLMContextSummarizationConfig:
- """Configuration for context summarization behavior.
+class LLMContextSummaryConfig:
+ """Configuration for summary generation parameters.
- Controls when and how conversation context is automatically compressed
- to manage token limits in long-running conversations.
+ Contains settings that control how a summary is generated. Used by both
+ automatic and manual summarization modes.
Parameters:
- max_context_tokens: Maximum allowed context size in tokens. When this
- limit is reached, summarization is triggered to compress the context.
- The tokens are calculated using the industry-standard approximation
- of 1 token ≈ 4 characters.
target_context_tokens: Maximum token size for the generated summary.
This value is passed directly to the LLM as the max_tokens parameter
when generating the summary. Should be sized appropriately to allow
the summary plus recent preserved messages to fit within reasonable
context limits.
- max_unsummarized_messages: Maximum number of new messages that can
- accumulate since the last summary before triggering a new
- summarization. This ensures regular compression even if token
- limits are not reached.
min_messages_after_summary: Number of recent messages to preserve
uncompressed after each summarization. These messages maintain
immediate conversational context.
@@ -94,6 +87,94 @@ class LLMContextSummarizationConfig:
is aborted with an error and future summarizations are unblocked.
"""
+ target_context_tokens: int = 6000
+ min_messages_after_summary: int = 4
+ summarization_prompt: Optional[str] = None
+ summary_message_template: str = "Conversation summary: {summary}"
+ llm: Optional["LLMService"] = None
+ summarization_timeout: float = DEFAULT_SUMMARIZATION_TIMEOUT
+
+ def __post_init__(self):
+ """Validate configuration parameters."""
+ if self.target_context_tokens <= 0:
+ raise ValueError("target_context_tokens must be positive")
+ if self.min_messages_after_summary < 0:
+ raise ValueError("min_messages_after_summary must be non-negative")
+
+ @property
+ def summary_prompt(self) -> str:
+ """Get the summarization prompt to use.
+
+ Returns:
+ The custom prompt if set, otherwise the default summarization prompt.
+ """
+ return self.summarization_prompt or DEFAULT_SUMMARIZATION_PROMPT
+
+
+@dataclass
+class LLMAutoContextSummarizationConfig:
+ """Configuration for automatic context summarization.
+
+ Controls when conversation context is automatically compressed and how
+ that summary is generated. Summarization is triggered when either the
+ token limit or the unsummarized message count threshold is exceeded.
+
+ Parameters:
+ max_context_tokens: Maximum allowed context size in tokens. When this
+ limit is reached, summarization is triggered to compress the context.
+ The tokens are calculated using the industry-standard approximation
+ of 1 token ≈ 4 characters.
+ max_unsummarized_messages: Maximum number of new messages that can
+ accumulate since the last summary before triggering a new
+ summarization. This ensures regular compression even if token
+ limits are not reached.
+ summary_config: Configuration for summary generation parameters
+ (prompt, token budget, messages to keep). If not provided, uses
+ default ``LLMContextSummaryConfig`` values.
+ """
+
+ max_context_tokens: int = 8000
+ max_unsummarized_messages: int = 20
+ summary_config: LLMContextSummaryConfig = field(default_factory=LLMContextSummaryConfig)
+
+ def __post_init__(self):
+ """Validate configuration parameters."""
+ if self.max_context_tokens <= 0:
+ raise ValueError("max_context_tokens must be positive")
+ if self.max_unsummarized_messages < 1:
+ raise ValueError("max_unsummarized_messages must be at least 1")
+
+ # Auto-adjust target_context_tokens if it exceeds max_context_tokens
+ if self.summary_config.target_context_tokens > self.max_context_tokens:
+ # Use 80% of max_context_tokens as a reasonable default
+ self.summary_config.target_context_tokens = int(self.max_context_tokens * 0.8)
+
+
+@dataclass
+class LLMContextSummarizationConfig:
+ """Configuration for context summarization behavior.
+
+ .. deprecated::
+ Use :class:`LLMAutoContextSummarizationConfig` with a nested
+ :class:`LLMContextSummaryConfig` instead::
+
+ LLMAutoContextSummarizationConfig(
+ max_context_tokens=8000,
+ max_unsummarized_messages=20,
+ summary_config=LLMContextSummaryConfig(
+ target_context_tokens=6000,
+ min_messages_after_summary=4,
+ ),
+ )
+
+ Parameters:
+ max_context_tokens: Maximum allowed context size in tokens.
+ target_context_tokens: Maximum token size for the generated summary.
+ max_unsummarized_messages: Maximum new messages before triggering summarization.
+ min_messages_after_summary: Number of recent messages to preserve.
+ summarization_prompt: Custom prompt for summary generation.
+ """
+
max_context_tokens: int = 8000
target_context_tokens: int = 6000
max_unsummarized_messages: int = 20
@@ -105,6 +186,12 @@ class LLMContextSummarizationConfig:
def __post_init__(self):
"""Validate configuration parameters."""
+ warnings.warn(
+ "LLMContextSummarizationConfig is deprecated. "
+ "Use LLMAutoContextSummarizationConfig with a nested LLMContextSummaryConfig instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
if self.max_context_tokens <= 0:
raise ValueError("max_context_tokens must be positive")
if self.target_context_tokens <= 0:
@@ -129,6 +216,25 @@ class LLMContextSummarizationConfig:
"""
return self.summarization_prompt or DEFAULT_SUMMARIZATION_PROMPT
+ def to_auto_config(self) -> LLMAutoContextSummarizationConfig:
+ """Convert to the new :class:`LLMAutoContextSummarizationConfig`.
+
+ Returns:
+ An equivalent ``LLMAutoContextSummarizationConfig`` instance.
+ """
+ return LLMAutoContextSummarizationConfig(
+ max_context_tokens=self.max_context_tokens,
+ max_unsummarized_messages=self.max_unsummarized_messages,
+ summary_config=LLMContextSummaryConfig(
+ target_context_tokens=self.target_context_tokens,
+ min_messages_after_summary=self.min_messages_after_summary,
+ summarization_prompt=self.summarization_prompt,
+ summary_message_template=self.summary_message_template,
+ llm=self.llm,
+ summarization_timeout=self.summarization_timeout,
+ ),
+ )
+
@dataclass
class LLMMessagesToSummarize:
diff --git a/tests/test_context_summarization.py b/tests/test_context_summarization.py
index ca56e7a32..10223a606 100644
--- a/tests/test_context_summarization.py
+++ b/tests/test_context_summarization.py
@@ -14,8 +14,10 @@ from pipecat.frames.frames import LLMContextSummaryRequestFrame, LLMContextSumma
from pipecat.processors.aggregators.llm_context import LLMContext, LLMSpecificMessage
from pipecat.services.llm_service import LLMService
from pipecat.utils.context.llm_context_summarization import (
+ LLMAutoContextSummarizationConfig,
LLMContextSummarizationConfig,
LLMContextSummarizationUtil,
+ LLMContextSummaryConfig,
)
@@ -167,43 +169,109 @@ class TestContextSummarizationMixin(unittest.TestCase):
self.assertIn("USER: First part Second part", transcript)
-class TestLLMContextSummarizationConfig(unittest.TestCase):
- """Tests for LLMContextSummarizationConfig."""
+class TestLLMContextSummaryConfig(unittest.TestCase):
+ """Tests for LLMContextSummaryConfig."""
def test_default_config(self):
"""Test default configuration values."""
- config = LLMContextSummarizationConfig()
+ config = LLMContextSummaryConfig()
- self.assertEqual(config.max_context_tokens, 8000)
- self.assertEqual(config.max_unsummarized_messages, 20)
+ self.assertEqual(config.target_context_tokens, 6000)
self.assertEqual(config.min_messages_after_summary, 4)
self.assertIsNone(config.summarization_prompt)
def test_custom_config(self):
"""Test custom configuration."""
- config = LLMContextSummarizationConfig(
- max_context_tokens=2500,
+ config = LLMContextSummaryConfig(
target_context_tokens=2000,
- max_unsummarized_messages=15,
min_messages_after_summary=4,
summarization_prompt="Custom prompt",
)
- self.assertEqual(config.max_context_tokens, 2500)
self.assertEqual(config.target_context_tokens, 2000)
- self.assertEqual(config.max_unsummarized_messages, 15)
self.assertEqual(config.min_messages_after_summary, 4)
self.assertEqual(config.summary_prompt, "Custom prompt")
def test_summary_prompt_property(self):
"""Test summary_prompt property uses default when None."""
- config = LLMContextSummarizationConfig()
+ config = LLMContextSummaryConfig()
self.assertIn("summarizing a conversation", config.summary_prompt.lower())
- config_with_custom = LLMContextSummarizationConfig(summarization_prompt="Custom")
+ config_with_custom = LLMContextSummaryConfig(summarization_prompt="Custom")
self.assertEqual(config_with_custom.summary_prompt, "Custom")
+class TestLLMAutoContextSummarizationConfig(unittest.TestCase):
+ """Tests for LLMAutoContextSummarizationConfig."""
+
+ def test_default_config(self):
+ """Test default configuration values."""
+ config = LLMAutoContextSummarizationConfig()
+
+ self.assertEqual(config.max_context_tokens, 8000)
+ self.assertEqual(config.max_unsummarized_messages, 20)
+ self.assertEqual(config.summary_config.target_context_tokens, 6000)
+ self.assertEqual(config.summary_config.min_messages_after_summary, 4)
+
+ def test_custom_config(self):
+ """Test custom configuration."""
+ config = LLMAutoContextSummarizationConfig(
+ max_context_tokens=2500,
+ max_unsummarized_messages=15,
+ summary_config=LLMContextSummaryConfig(
+ target_context_tokens=2000,
+ min_messages_after_summary=4,
+ summarization_prompt="Custom prompt",
+ ),
+ )
+
+ self.assertEqual(config.max_context_tokens, 2500)
+ self.assertEqual(config.max_unsummarized_messages, 15)
+ self.assertEqual(config.summary_config.target_context_tokens, 2000)
+ self.assertEqual(config.summary_config.min_messages_after_summary, 4)
+ self.assertEqual(config.summary_config.summary_prompt, "Custom prompt")
+
+ def test_target_tokens_auto_adjusted(self):
+ """Test that target_context_tokens is auto-adjusted when it exceeds max."""
+ config = LLMAutoContextSummarizationConfig(
+ max_context_tokens=1000,
+ summary_config=LLMContextSummaryConfig(target_context_tokens=9000),
+ )
+ self.assertLessEqual(config.summary_config.target_context_tokens, config.max_context_tokens)
+
+
+class TestLLMContextSummarizationConfigDeprecated(unittest.TestCase):
+ """Tests for deprecated LLMContextSummarizationConfig."""
+
+ def test_emits_deprecation_warning(self):
+ """Test that instantiating the deprecated config emits a DeprecationWarning."""
+ with self.assertWarns(DeprecationWarning):
+ LLMContextSummarizationConfig()
+
+ def test_to_auto_config(self):
+ """Test conversion to the new LLMAutoContextSummarizationConfig."""
+ import warnings
+
+ with warnings.catch_warnings():
+ warnings.simplefilter("ignore", DeprecationWarning)
+ old_config = LLMContextSummarizationConfig(
+ max_context_tokens=2500,
+ target_context_tokens=2000,
+ max_unsummarized_messages=15,
+ min_messages_after_summary=4,
+ summarization_prompt="Custom",
+ )
+
+ new_config = old_config.to_auto_config()
+
+ self.assertIsInstance(new_config, LLMAutoContextSummarizationConfig)
+ self.assertEqual(new_config.max_context_tokens, 2500)
+ self.assertEqual(new_config.max_unsummarized_messages, 15)
+ self.assertEqual(new_config.summary_config.target_context_tokens, 2000)
+ self.assertEqual(new_config.summary_config.min_messages_after_summary, 4)
+ self.assertEqual(new_config.summary_config.summarization_prompt, "Custom")
+
+
class TestFunctionCallHandling(unittest.TestCase):
"""Tests for function call handling in summarization."""
@@ -670,10 +738,12 @@ class TestDedicatedLLMSummarization(unittest.IsolatedAsyncioTestCase):
{"role": "user", "content": f"Test message {i} that adds tokens to context."}
)
- config = LLMContextSummarizationConfig(
+ config = LLMAutoContextSummarizationConfig(
max_context_tokens=50, # Very low to trigger easily
- llm=dedicated_llm,
- summarization_timeout=5.0,
+ summary_config=LLMContextSummaryConfig(
+ llm=dedicated_llm,
+ summarization_timeout=5.0,
+ ),
)
return context, config
@@ -736,7 +806,7 @@ class TestDedicatedLLMSummarization(unittest.IsolatedAsyncioTestCase):
dedicated_llm._generate_summary = slow_summary
context, config = self._create_context_and_config(dedicated_llm)
- config.summarization_timeout = 0.1 # Very short timeout
+ config.summary_config.summarization_timeout = 0.1 # Very short timeout
summarizer = LLMContextSummarizer(context=context, config=config)
await summarizer.setup(self.task_manager)
@@ -826,7 +896,7 @@ class TestDedicatedLLMSummarization(unittest.IsolatedAsyncioTestCase):
{"role": "user", "content": f"Test message {i} that adds tokens to context."}
)
- config = LLMContextSummarizationConfig(max_context_tokens=50)
+ config = LLMAutoContextSummarizationConfig(max_context_tokens=50)
summarizer = LLMContextSummarizer(context=context, config=config)
await summarizer.setup(self.task_manager)
diff --git a/tests/test_llm_context_summarizer.py b/tests/test_llm_context_summarizer.py
index 0439d403d..7e8b326f9 100644
--- a/tests/test_llm_context_summarizer.py
+++ b/tests/test_llm_context_summarizer.py
@@ -12,6 +12,7 @@ from pipecat.frames.frames import (
LLMContextSummaryRequestFrame,
LLMContextSummaryResultFrame,
LLMFullResponseStartFrame,
+ LLMSummarizeContextFrame,
)
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_context_summarizer import (
@@ -19,7 +20,10 @@ from pipecat.processors.aggregators.llm_context_summarizer import (
SummaryAppliedEvent,
)
from pipecat.utils.asyncio.task_manager import TaskManager, TaskManagerParams
-from pipecat.utils.context.llm_context_summarization import LLMContextSummarizationConfig
+from pipecat.utils.context.llm_context_summarization import (
+ LLMAutoContextSummarizationConfig,
+ LLMContextSummaryConfig,
+)
class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
@@ -35,7 +39,7 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
async def test_summarization_triggered_by_token_limit(self):
"""Test that summarization is triggered when token limit is reached."""
- config = LLMContextSummarizationConfig(
+ config = LLMAutoContextSummarizationConfig(
max_context_tokens=100, # Very low to trigger easily
max_unsummarized_messages=100, # High so it doesn't trigger by message count
)
@@ -71,7 +75,7 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
async def test_summarization_triggered_by_message_count(self):
"""Test that summarization is triggered when message count threshold is reached."""
- config = LLMContextSummarizationConfig(
+ config = LLMAutoContextSummarizationConfig(
max_context_tokens=100000, # Very high so it doesn't trigger by tokens
max_unsummarized_messages=5, # Low to trigger easily
)
@@ -101,7 +105,7 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
async def test_summarization_not_triggered_below_thresholds(self):
"""Test that summarization is not triggered when below thresholds."""
- config = LLMContextSummarizationConfig(
+ config = LLMAutoContextSummarizationConfig(
max_context_tokens=10000,
max_unsummarized_messages=20,
)
@@ -130,7 +134,7 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
async def test_summarization_in_progress_prevents_duplicate(self):
"""Test that a summarization in progress prevents triggering another."""
- config = LLMContextSummarizationConfig(
+ config = LLMAutoContextSummarizationConfig(
max_context_tokens=50, # Very low
max_unsummarized_messages=100,
)
@@ -161,7 +165,10 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
async def test_summary_result_handling(self):
"""Test that summary results are processed and applied correctly."""
- config = LLMContextSummarizationConfig(max_context_tokens=50, min_messages_after_summary=2)
+ config = LLMAutoContextSummarizationConfig(
+ max_context_tokens=50,
+ summary_config=LLMContextSummaryConfig(min_messages_after_summary=2),
+ )
summarizer = LLMContextSummarizer(context=self.context, config=config)
await summarizer.setup(self.task_manager)
@@ -208,7 +215,7 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
async def test_interruption_cancels_summarization(self):
"""Test that an interruption cancels pending summarization."""
- config = LLMContextSummarizationConfig(max_context_tokens=50)
+ config = LLMAutoContextSummarizationConfig(max_context_tokens=50)
summarizer = LLMContextSummarizer(context=self.context, config=config)
await summarizer.setup(self.task_manager)
@@ -238,7 +245,10 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
async def test_stale_summary_result_ignored(self):
"""Test that stale summary results are ignored."""
- config = LLMContextSummarizationConfig(max_context_tokens=50, min_messages_after_summary=2)
+ config = LLMAutoContextSummarizationConfig(
+ max_context_tokens=50,
+ summary_config=LLMContextSummaryConfig(min_messages_after_summary=2),
+ )
summarizer = LLMContextSummarizer(context=self.context, config=config)
await summarizer.setup(self.task_manager)
@@ -294,9 +304,116 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
await summarizer.cleanup()
+ async def test_manual_summarization_via_frame(self):
+ """Test that LLMSummarizeContextFrame triggers summarization on demand."""
+ config = LLMAutoContextSummarizationConfig(
+ max_context_tokens=100000, # High — auto trigger would never fire
+ max_unsummarized_messages=100,
+ )
+
+ summarizer = LLMContextSummarizer(
+ context=self.context,
+ config=config,
+ auto_trigger=False, # Disable auto; only manual requests should work
+ )
+ await summarizer.setup(self.task_manager)
+
+ request_frame = None
+
+ @summarizer.event_handler("on_request_summarization")
+ async def on_request_summarization(summarizer, frame):
+ nonlocal request_frame
+ request_frame = frame
+
+ # Add messages
+ for i in range(5):
+ self.context.add_message({"role": "user", "content": f"Message {i}"})
+
+ # Auto-trigger should NOT fire even on LLMFullResponseStartFrame
+ await summarizer.process_frame(LLMFullResponseStartFrame())
+ self.assertIsNone(request_frame)
+
+ # Manual trigger via LLMSummarizeContextFrame should fire
+ await summarizer.process_frame(LLMSummarizeContextFrame())
+ self.assertIsNotNone(request_frame)
+ self.assertIsInstance(request_frame, LLMContextSummaryRequestFrame)
+
+ # The request must have a valid request_id and carry the current context
+ self.assertTrue(request_frame.request_id)
+ self.assertEqual(request_frame.context, self.context)
+
+ await summarizer.cleanup()
+
+ async def test_manual_summarization_with_config_override(self):
+ """Test that LLMSummarizeContextFrame can override default summary config."""
+ config = LLMAutoContextSummarizationConfig(
+ max_context_tokens=100000,
+ summary_config=LLMContextSummaryConfig(
+ target_context_tokens=6000,
+ min_messages_after_summary=4,
+ ),
+ )
+
+ summarizer = LLMContextSummarizer(context=self.context, config=config)
+ await summarizer.setup(self.task_manager)
+
+ request_frame = None
+
+ @summarizer.event_handler("on_request_summarization")
+ async def on_request_summarization(summarizer, frame):
+ nonlocal request_frame
+ request_frame = frame
+
+ for i in range(5):
+ self.context.add_message({"role": "user", "content": f"Message {i}"})
+
+ # Push a manual frame with custom config overrides
+ custom_config = LLMContextSummaryConfig(
+ target_context_tokens=500,
+ min_messages_after_summary=1,
+ )
+ await summarizer.process_frame(LLMSummarizeContextFrame(config=custom_config))
+
+ self.assertIsNotNone(request_frame)
+ # The request should use the overridden values
+ self.assertEqual(request_frame.target_context_tokens, 500)
+ self.assertEqual(request_frame.min_messages_to_keep, 1)
+
+ await summarizer.cleanup()
+
+ async def test_manual_summarization_blocked_when_in_progress(self):
+ """Test that a second LLMSummarizeContextFrame is ignored while one is in progress."""
+ config = LLMAutoContextSummarizationConfig(max_context_tokens=100000)
+
+ summarizer = LLMContextSummarizer(context=self.context, config=config)
+ await summarizer.setup(self.task_manager)
+
+ request_count = 0
+
+ @summarizer.event_handler("on_request_summarization")
+ async def on_request_summarization(summarizer, frame):
+ nonlocal request_count
+ request_count += 1
+
+ for i in range(5):
+ self.context.add_message({"role": "user", "content": f"Message {i}"})
+
+ # First manual request
+ await summarizer.process_frame(LLMSummarizeContextFrame())
+ self.assertEqual(request_count, 1)
+
+ # Second manual request while first is in progress — should be ignored
+ await summarizer.process_frame(LLMSummarizeContextFrame())
+ self.assertEqual(request_count, 1)
+
+ await summarizer.cleanup()
+
async def test_summary_message_role_is_user(self):
"""Test that the summary message uses the user role."""
- config = LLMContextSummarizationConfig(max_context_tokens=50, min_messages_after_summary=2)
+ config = LLMAutoContextSummarizationConfig(
+ max_context_tokens=50,
+ summary_config=LLMContextSummaryConfig(min_messages_after_summary=2),
+ )
summarizer = LLMContextSummarizer(context=self.context, config=config)
await summarizer.setup(self.task_manager)
@@ -335,7 +452,10 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
async def test_summary_message_default_template(self):
"""Test that the default summary_message_template is used."""
- config = LLMContextSummarizationConfig(max_context_tokens=50, min_messages_after_summary=2)
+ config = LLMAutoContextSummarizationConfig(
+ max_context_tokens=50,
+ summary_config=LLMContextSummaryConfig(min_messages_after_summary=2),
+ )
summarizer = LLMContextSummarizer(context=self.context, config=config)
await summarizer.setup(self.task_manager)
@@ -377,10 +497,12 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
async def test_summary_message_custom_template(self):
"""Test that a custom summary_message_template is applied."""
- config = LLMContextSummarizationConfig(
+ config = LLMAutoContextSummarizationConfig(
max_context_tokens=50,
- min_messages_after_summary=2,
- summary_message_template="\n{summary}\n",
+ summary_config=LLMContextSummaryConfig(
+ min_messages_after_summary=2,
+ summary_message_template="\n{summary}\n",
+ ),
)
summarizer = LLMContextSummarizer(context=self.context, config=config)
@@ -420,7 +542,10 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
async def test_on_summary_applied_event(self):
"""Test that on_summary_applied event fires with correct data."""
- config = LLMContextSummarizationConfig(max_context_tokens=50, min_messages_after_summary=2)
+ config = LLMAutoContextSummarizationConfig(
+ max_context_tokens=50,
+ summary_config=LLMContextSummaryConfig(min_messages_after_summary=2),
+ )
summarizer = LLMContextSummarizer(context=self.context, config=config)
await summarizer.setup(self.task_manager)
@@ -474,7 +599,10 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
async def test_on_summary_applied_not_fired_on_error(self):
"""Test that on_summary_applied event is NOT fired when summarization fails."""
- config = LLMContextSummarizationConfig(max_context_tokens=50, min_messages_after_summary=2)
+ config = LLMAutoContextSummarizationConfig(
+ max_context_tokens=50,
+ summary_config=LLMContextSummaryConfig(min_messages_after_summary=2),
+ )
summarizer = LLMContextSummarizer(context=self.context, config=config)
await summarizer.setup(self.task_manager)
@@ -515,9 +643,9 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
async def test_request_frame_includes_timeout(self):
"""Test that the request frame includes the configured summarization_timeout."""
- config = LLMContextSummarizationConfig(
+ config = LLMAutoContextSummarizationConfig(
max_context_tokens=50,
- summarization_timeout=60.0,
+ summary_config=LLMContextSummaryConfig(summarization_timeout=60.0),
)
summarizer = LLMContextSummarizer(context=self.context, config=config)