Merge pull request #3863 from pipecat-ai/filipi/manual_summarization
Manual context summarization
This commit is contained in:
1
changelog/3863.added.2.md
Normal file
1
changelog/3863.added.2.md
Normal file
@@ -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`.
|
||||
1
changelog/3863.added.md
Normal file
1
changelog/3863.added.md
Normal file
@@ -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.
|
||||
1
changelog/3863.changed.md
Normal file
1
changelog/3863.changed.md
Normal file
@@ -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.
|
||||
1
changelog/3863.deprecated.md
Normal file
1
changelog/3863.deprecated.md
Normal file
@@ -0,0 +1 @@
|
||||
- Deprecated `LLMContextSummarizationConfig`. Use `LLMAutoContextSummarizationConfig` with a nested `LLMContextSummaryConfig` instead. The old class emits a `DeprecationWarning`.
|
||||
@@ -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
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
179
examples/foundational/54b-context-summarization-manual-openai.py
Normal file
179
examples/foundational/54b-context-summarization-manual-openai.py
Normal file
@@ -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()
|
||||
@@ -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="<context_summary>\n{summary}\n</context_summary>",
|
||||
# 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="<context_summary>\n{summary}\n</context_summary>",
|
||||
# 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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="<context_summary>\n{summary}\n</context_summary>",
|
||||
summary_config=LLMContextSummaryConfig(
|
||||
min_messages_after_summary=2,
|
||||
summary_message_template="<context_summary>\n{summary}\n</context_summary>",
|
||||
),
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user