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.base_transport import BaseTransport, TransportParams
|
||||||
from pipecat.transports.daily.transport import DailyParams
|
from pipecat.transports.daily.transport import DailyParams
|
||||||
from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams
|
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)
|
load_dotenv(override=True)
|
||||||
|
|
||||||
@@ -120,14 +123,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
),
|
),
|
||||||
assistant_params=LLMAssistantAggregatorParams(
|
assistant_params=LLMAssistantAggregatorParams(
|
||||||
enable_context_summarization=True,
|
enable_auto_context_summarization=True,
|
||||||
# Optional: customize context summarization behavior
|
# Optional: customize context summarization behavior
|
||||||
# Using low limits to demonstrate the feature quickly
|
# 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
|
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
|
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.base_transport import BaseTransport, TransportParams
|
||||||
from pipecat.transports.daily.transport import DailyParams
|
from pipecat.transports.daily.transport import DailyParams
|
||||||
from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams
|
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)
|
load_dotenv(override=True)
|
||||||
|
|
||||||
@@ -120,14 +123,16 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
),
|
),
|
||||||
assistant_params=LLMAssistantAggregatorParams(
|
assistant_params=LLMAssistantAggregatorParams(
|
||||||
enable_context_summarization=True,
|
enable_auto_context_summarization=True,
|
||||||
# Optional: customize context summarization behavior
|
# Optional: customize context summarization behavior
|
||||||
# Using low limits to demonstrate the feature quickly
|
# 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
|
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
|
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.base_transport import BaseTransport, TransportParams
|
||||||
from pipecat.transports.daily.transport import DailyParams
|
from pipecat.transports.daily.transport import DailyParams
|
||||||
from pipecat.transports.websocket.fastapi import FastAPIWebsocketParams
|
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)
|
load_dotenv(override=True)
|
||||||
|
|
||||||
@@ -147,23 +150,25 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
|
|||||||
vad_analyzer=SileroVADAnalyzer(),
|
vad_analyzer=SileroVADAnalyzer(),
|
||||||
),
|
),
|
||||||
assistant_params=LLMAssistantAggregatorParams(
|
assistant_params=LLMAssistantAggregatorParams(
|
||||||
enable_context_summarization=True,
|
enable_auto_context_summarization=True,
|
||||||
context_summarization_config=LLMContextSummarizationConfig(
|
auto_context_summarization_config=LLMAutoContextSummarizationConfig(
|
||||||
# Trigger thresholds (low values to demonstrate quickly)
|
# Trigger thresholds (low values to demonstrate quickly)
|
||||||
max_context_tokens=1000,
|
max_context_tokens=1000,
|
||||||
max_unsummarized_messages=10,
|
max_unsummarized_messages=10,
|
||||||
# Summary generation
|
summary_config=LLMContextSummaryConfig(
|
||||||
target_context_tokens=800,
|
# Summary generation
|
||||||
min_messages_after_summary=2,
|
target_context_tokens=800,
|
||||||
summarization_prompt=CUSTOM_SUMMARIZATION_PROMPT,
|
min_messages_after_summary=2,
|
||||||
# Custom summary format - wrap in XML tags so the system
|
summarization_prompt=CUSTOM_SUMMARIZATION_PROMPT,
|
||||||
# prompt can identify summaries vs. live conversation
|
# Custom summary format - wrap in XML tags so the system
|
||||||
summary_message_template="<context_summary>\n{summary}\n</context_summary>",
|
# prompt can identify summaries vs. live conversation
|
||||||
# Use a dedicated cheap LLM for summarization instead of
|
summary_message_template="<context_summary>\n{summary}\n</context_summary>",
|
||||||
# the primary conversation model
|
# Use a dedicated cheap LLM for summarization instead of
|
||||||
llm=summarization_llm,
|
# the primary conversation model
|
||||||
# Cancel summarization if it takes longer than 60 seconds
|
llm=summarization_llm,
|
||||||
summarization_timeout=60.0,
|
# 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.aggregators.llm_context import LLMContext, NotGiven
|
||||||
from pipecat.processors.frame_processor import FrameProcessor
|
from pipecat.processors.frame_processor import FrameProcessor
|
||||||
from pipecat.services.settings import ServiceSettings
|
from pipecat.services.settings import ServiceSettings
|
||||||
|
from pipecat.utils.context.llm_context_summarization import LLMContextSummaryConfig
|
||||||
from pipecat.utils.tracing.tracing_context import TracingContext
|
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
|
@dataclass
|
||||||
class LLMContextSummaryRequestFrame(ControlFrame):
|
class LLMContextSummaryRequestFrame(ControlFrame):
|
||||||
"""Frame requesting context summarization from an LLM service.
|
"""Frame requesting context summarization from an LLM service.
|
||||||
|
|||||||
@@ -19,14 +19,16 @@ from pipecat.frames.frames import (
|
|||||||
LLMContextSummaryRequestFrame,
|
LLMContextSummaryRequestFrame,
|
||||||
LLMContextSummaryResultFrame,
|
LLMContextSummaryResultFrame,
|
||||||
LLMFullResponseStartFrame,
|
LLMFullResponseStartFrame,
|
||||||
|
LLMSummarizeContextFrame,
|
||||||
)
|
)
|
||||||
from pipecat.processors.aggregators.llm_context import LLMContext, LLMSpecificMessage
|
from pipecat.processors.aggregators.llm_context import LLMContext, LLMSpecificMessage
|
||||||
from pipecat.utils.asyncio.task_manager import BaseTaskManager
|
from pipecat.utils.asyncio.task_manager import BaseTaskManager
|
||||||
from pipecat.utils.base_object import BaseObject
|
from pipecat.utils.base_object import BaseObject
|
||||||
from pipecat.utils.context.llm_context_summarization import (
|
from pipecat.utils.context.llm_context_summarization import (
|
||||||
DEFAULT_SUMMARIZATION_TIMEOUT,
|
DEFAULT_SUMMARIZATION_TIMEOUT,
|
||||||
LLMContextSummarizationConfig,
|
LLMAutoContextSummarizationConfig,
|
||||||
LLMContextSummarizationUtil,
|
LLMContextSummarizationUtil,
|
||||||
|
LLMContextSummaryConfig,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -55,9 +57,20 @@ class SummaryAppliedEvent:
|
|||||||
class LLMContextSummarizer(BaseObject):
|
class LLMContextSummarizer(BaseObject):
|
||||||
"""Summarizer for managing LLM context summarization.
|
"""Summarizer for managing LLM context summarization.
|
||||||
|
|
||||||
This class manages automatic context summarization when token or message
|
This class manages context summarization, either automatically when token or
|
||||||
limits are reached. It monitors the LLM context size, triggers
|
message limits are reached, or on-demand when an ``LLMSummarizeContextFrame``
|
||||||
summarization requests, and applies the results to compress conversation history.
|
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:
|
Event handlers available:
|
||||||
|
|
||||||
@@ -88,18 +101,26 @@ class LLMContextSummarizer(BaseObject):
|
|||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
context: LLMContext,
|
context: LLMContext,
|
||||||
config: Optional[LLMContextSummarizationConfig] = None,
|
config: Optional[LLMAutoContextSummarizationConfig] = None,
|
||||||
|
auto_trigger: bool = True,
|
||||||
):
|
):
|
||||||
"""Initialize the context summarizer.
|
"""Initialize the context summarizer.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
context: The LLM context to monitor and summarize.
|
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__()
|
super().__init__()
|
||||||
|
|
||||||
self._context = context
|
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
|
self._task_manager: Optional[BaseTaskManager] = None
|
||||||
|
|
||||||
@@ -137,6 +158,8 @@ class LLMContextSummarizer(BaseObject):
|
|||||||
"""
|
"""
|
||||||
if isinstance(frame, LLMFullResponseStartFrame):
|
if isinstance(frame, LLMFullResponseStartFrame):
|
||||||
await self._handle_llm_response_start(frame)
|
await self._handle_llm_response_start(frame)
|
||||||
|
elif isinstance(frame, LLMSummarizeContextFrame):
|
||||||
|
await self._handle_manual_summarization_request(frame)
|
||||||
elif isinstance(frame, LLMContextSummaryResultFrame):
|
elif isinstance(frame, LLMContextSummaryResultFrame):
|
||||||
await self._handle_summary_result(frame)
|
await self._handle_summary_result(frame)
|
||||||
elif isinstance(frame, InterruptionFrame):
|
elif isinstance(frame, InterruptionFrame):
|
||||||
@@ -151,12 +174,24 @@ class LLMContextSummarizer(BaseObject):
|
|||||||
if self._should_summarize():
|
if self._should_summarize():
|
||||||
await self._request_summarization()
|
await self._request_summarization()
|
||||||
|
|
||||||
async def _handle_interruption(self):
|
async def _handle_manual_summarization_request(self, frame: LLMSummarizeContextFrame):
|
||||||
"""Handle interruption by canceling summarization in progress.
|
"""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:
|
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
|
# Reset summarization state to allow new requests. This is necessary because
|
||||||
# the request frame (LLMContextSummaryRequestFrame) may have been cancelled
|
# the request frame (LLMContextSummaryRequestFrame) may have been cancelled
|
||||||
# during interruption. We preserve _pending_summary_request_id to handle the
|
# during interruption. We preserve _pending_summary_request_id to handle the
|
||||||
@@ -179,13 +214,17 @@ class LLMContextSummarizer(BaseObject):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if all conditions are met:
|
True if all conditions are met:
|
||||||
|
- ``auto_trigger`` is enabled
|
||||||
- No summarization currently in progress
|
- No summarization currently in progress
|
||||||
- AND either:
|
- AND either:
|
||||||
- Token count exceeds max_context_tokens
|
- Token count exceeds ``max_context_tokens``
|
||||||
- OR message count exceeds max_unsummarized_messages since last summary
|
- OR message count exceeds ``max_unsummarized_messages`` since last summary
|
||||||
"""
|
"""
|
||||||
logger.trace(f"{self}: Checking if context summarization is needed")
|
logger.trace(f"{self}: Checking if context summarization is needed")
|
||||||
|
|
||||||
|
if not self._auto_trigger:
|
||||||
|
return False
|
||||||
|
|
||||||
if self._summarization_in_progress:
|
if self._summarization_in_progress:
|
||||||
logger.debug(f"{self}: Summarization already in progress")
|
logger.debug(f"{self}: Summarization already in progress")
|
||||||
return False
|
return False
|
||||||
@@ -195,20 +234,20 @@ class LLMContextSummarizer(BaseObject):
|
|||||||
num_messages = len(self._context.messages)
|
num_messages = len(self._context.messages)
|
||||||
|
|
||||||
# Check if we've reached the token limit
|
# 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
|
token_limit_exceeded = total_tokens >= token_limit
|
||||||
|
|
||||||
# Check if we've exceeded max unsummarized messages
|
# Check if we've exceeded max unsummarized messages
|
||||||
messages_since_summary = len(self._context.messages) - 1
|
messages_since_summary = len(self._context.messages) - 1
|
||||||
message_threshold_exceeded = (
|
message_threshold_exceeded = (
|
||||||
messages_since_summary >= self._config.max_unsummarized_messages
|
messages_since_summary >= self._auto_config.max_unsummarized_messages
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.trace(
|
logger.trace(
|
||||||
f"{self}: Context has {num_messages} messages, "
|
f"{self}: Context has {num_messages} messages, "
|
||||||
f"~{total_tokens} tokens (limit: {token_limit}), "
|
f"~{total_tokens} tokens (limit: {token_limit}), "
|
||||||
f"{messages_since_summary} messages since last summary "
|
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
|
# Trigger if either limit is exceeded
|
||||||
@@ -223,23 +262,30 @@ class LLMContextSummarizer(BaseObject):
|
|||||||
reason.append(f"~{total_tokens} tokens (>={token_limit} limit)")
|
reason.append(f"~{total_tokens} tokens (>={token_limit} limit)")
|
||||||
if message_threshold_exceeded:
|
if message_threshold_exceeded:
|
||||||
reason.append(
|
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)}")
|
logger.debug(f"{self}: ✓ Summarization needed - {', '.join(reason)}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def _request_summarization(self):
|
async def _request_summarization(
|
||||||
|
self, config_override: Optional[LLMContextSummaryConfig] = None
|
||||||
|
):
|
||||||
"""Request context summarization from LLM service.
|
"""Request context summarization from LLM service.
|
||||||
|
|
||||||
Creates a summarization request frame and either handles it directly
|
Creates a summarization request frame and either handles it directly
|
||||||
using a dedicated LLM (if configured) or emits it via event handler
|
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
|
for the pipeline's primary LLM.
|
||||||
responses and prevent race conditions.
|
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
|
# Generate unique request ID
|
||||||
request_id = str(uuid.uuid4())
|
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
|
# Mark summarization in progress
|
||||||
self._summarization_in_progress = True
|
self._summarization_in_progress = True
|
||||||
@@ -251,16 +297,16 @@ class LLMContextSummarizer(BaseObject):
|
|||||||
request_frame = LLMContextSummaryRequestFrame(
|
request_frame = LLMContextSummaryRequestFrame(
|
||||||
request_id=request_id,
|
request_id=request_id,
|
||||||
context=self._context,
|
context=self._context,
|
||||||
min_messages_to_keep=min_keep,
|
min_messages_to_keep=summary_config.min_messages_after_summary,
|
||||||
target_context_tokens=self._config.target_context_tokens,
|
target_context_tokens=summary_config.target_context_tokens,
|
||||||
summarization_prompt=self._config.summary_prompt,
|
summarization_prompt=summary_config.summary_prompt,
|
||||||
summarization_timeout=self._config.summarization_timeout,
|
summarization_timeout=summary_config.summarization_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._config.llm:
|
if summary_config.llm:
|
||||||
# Use dedicated LLM directly — no need to involve the pipeline
|
# Use dedicated LLM directly — no need to involve the pipeline
|
||||||
self.task_manager.create_task(
|
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",
|
f"{self}-dedicated-llm-summary",
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -323,7 +369,9 @@ class LLMContextSummarizer(BaseObject):
|
|||||||
"""
|
"""
|
||||||
logger.debug(f"{self}: Received summary result (request_id={frame.request_id})")
|
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:
|
if frame.request_id != self._pending_summary_request_id:
|
||||||
logger.debug(f"{self}: Ignoring stale summary result (request_id={frame.request_id})")
|
logger.debug(f"{self}: Ignoring stale summary result (request_id={frame.request_id})")
|
||||||
return
|
return
|
||||||
@@ -360,7 +408,7 @@ class LLMContextSummarizer(BaseObject):
|
|||||||
if last_summarized_index >= len(self._context.messages):
|
if last_summarized_index >= len(self._context.messages):
|
||||||
return False
|
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
|
remaining = len(self._context.messages) - 1 - last_summarized_index
|
||||||
if remaining < min_keep:
|
if remaining < min_keep:
|
||||||
return False
|
return False
|
||||||
@@ -377,6 +425,7 @@ class LLMContextSummarizer(BaseObject):
|
|||||||
summary: The generated summary text.
|
summary: The generated summary text.
|
||||||
last_summarized_index: Index of the last message that was summarized.
|
last_summarized_index: Index of the last message that was summarized.
|
||||||
"""
|
"""
|
||||||
|
config = self._auto_config.summary_config
|
||||||
messages = self._context.messages
|
messages = self._context.messages
|
||||||
|
|
||||||
# Find the first system message to preserve. LLMSpecificMessage instances are excluded
|
# 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
|
# Create summary message as a user message (the summary is context
|
||||||
# provided *to* the assistant, not something the assistant said)
|
# 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}
|
summary_message = {"role": "user", "content": summary_content}
|
||||||
|
|
||||||
# Reconstruct context
|
# 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_completion_mixin import UserTurnCompletionConfig
|
||||||
from pipecat.turns.user_turn_controller import UserTurnController
|
from pipecat.turns.user_turn_controller import UserTurnController
|
||||||
from pipecat.turns.user_turn_strategies import ExternalUserTurnStrategies, UserTurnStrategies
|
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.string import TextPartForConcatenation, concatenate_aggregated_text
|
||||||
from pipecat.utils.time import time_now_iso8601
|
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
|
in text frames by adding spaces between tokens. This parameter is
|
||||||
ignored when used with the newer LLMAssistantAggregator, which
|
ignored when used with the newer LLMAssistantAggregator, which
|
||||||
handles word spacing automatically.
|
handles word spacing automatically.
|
||||||
enable_context_summarization: Enable automatic context summarization when token
|
enable_auto_context_summarization: Enable automatic context summarization when token
|
||||||
limits are reached (disabled by default). When enabled, older conversation
|
or message-count limits are reached (disabled by default). When enabled,
|
||||||
messages are automatically compressed into summaries to manage context size.
|
older conversation messages are automatically compressed into summaries to
|
||||||
context_summarization_config: Configuration for context summarization behavior.
|
manage context size.
|
||||||
Controls thresholds, message preservation, and summarization prompts. If None
|
auto_context_summarization_config: Configuration for automatic context
|
||||||
and summarization is enabled, uses default configuration values.
|
summarization. Controls trigger thresholds, message preservation, and
|
||||||
|
summarization prompts. If None, uses default
|
||||||
|
``LLMAutoContextSummarizationConfig`` values.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
expect_stripped_words: bool = True
|
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
|
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
|
@dataclass
|
||||||
class UserTurnStoppedMessage:
|
class UserTurnStoppedMessage:
|
||||||
@@ -825,16 +864,18 @@ class LLMAssistantAggregator(LLMContextAggregator):
|
|||||||
self._thought_aggregation: List[TextPartForConcatenation] = []
|
self._thought_aggregation: List[TextPartForConcatenation] = []
|
||||||
self._thought_start_time: str = ""
|
self._thought_start_time: str = ""
|
||||||
|
|
||||||
# Context summarization
|
# Context summarization — always create the summarizer so that manually
|
||||||
self._summarizer: Optional[LLMContextSummarizer] = None
|
# pushed LLMSummarizeContextFrame frames are always handled.
|
||||||
if self._params.enable_context_summarization:
|
# Auto-triggering based on thresholds is only enabled when
|
||||||
self._summarizer = LLMContextSummarizer(
|
# enable_auto_context_summarization is True.
|
||||||
context=self._context,
|
self._summarizer: Optional[LLMContextSummarizer] = LLMContextSummarizer(
|
||||||
config=self._params.context_summarization_config,
|
context=self._context,
|
||||||
)
|
config=self._params.auto_context_summarization_config,
|
||||||
self._summarizer.add_event_handler(
|
auto_trigger=self._params.enable_auto_context_summarization,
|
||||||
"on_request_summarization", self._on_request_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_started")
|
||||||
self._register_event_handler("on_assistant_turn_stopped")
|
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.
|
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
|
from typing import TYPE_CHECKING, List, Optional
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -54,26 +55,18 @@ The conversation transcript follows. Generate only the summary, no other text.""
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMContextSummarizationConfig:
|
class LLMContextSummaryConfig:
|
||||||
"""Configuration for context summarization behavior.
|
"""Configuration for summary generation parameters.
|
||||||
|
|
||||||
Controls when and how conversation context is automatically compressed
|
Contains settings that control how a summary is generated. Used by both
|
||||||
to manage token limits in long-running conversations.
|
automatic and manual summarization modes.
|
||||||
|
|
||||||
Parameters:
|
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.
|
target_context_tokens: Maximum token size for the generated summary.
|
||||||
This value is passed directly to the LLM as the max_tokens parameter
|
This value is passed directly to the LLM as the max_tokens parameter
|
||||||
when generating the summary. Should be sized appropriately to allow
|
when generating the summary. Should be sized appropriately to allow
|
||||||
the summary plus recent preserved messages to fit within reasonable
|
the summary plus recent preserved messages to fit within reasonable
|
||||||
context limits.
|
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
|
min_messages_after_summary: Number of recent messages to preserve
|
||||||
uncompressed after each summarization. These messages maintain
|
uncompressed after each summarization. These messages maintain
|
||||||
immediate conversational context.
|
immediate conversational context.
|
||||||
@@ -94,6 +87,94 @@ class LLMContextSummarizationConfig:
|
|||||||
is aborted with an error and future summarizations are unblocked.
|
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
|
max_context_tokens: int = 8000
|
||||||
target_context_tokens: int = 6000
|
target_context_tokens: int = 6000
|
||||||
max_unsummarized_messages: int = 20
|
max_unsummarized_messages: int = 20
|
||||||
@@ -105,6 +186,12 @@ class LLMContextSummarizationConfig:
|
|||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
"""Validate configuration parameters."""
|
"""Validate configuration parameters."""
|
||||||
|
warnings.warn(
|
||||||
|
"LLMContextSummarizationConfig is deprecated. "
|
||||||
|
"Use LLMAutoContextSummarizationConfig with a nested LLMContextSummaryConfig instead.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
if self.max_context_tokens <= 0:
|
if self.max_context_tokens <= 0:
|
||||||
raise ValueError("max_context_tokens must be positive")
|
raise ValueError("max_context_tokens must be positive")
|
||||||
if self.target_context_tokens <= 0:
|
if self.target_context_tokens <= 0:
|
||||||
@@ -129,6 +216,25 @@ class LLMContextSummarizationConfig:
|
|||||||
"""
|
"""
|
||||||
return self.summarization_prompt or DEFAULT_SUMMARIZATION_PROMPT
|
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
|
@dataclass
|
||||||
class LLMMessagesToSummarize:
|
class LLMMessagesToSummarize:
|
||||||
|
|||||||
@@ -14,8 +14,10 @@ from pipecat.frames.frames import LLMContextSummaryRequestFrame, LLMContextSumma
|
|||||||
from pipecat.processors.aggregators.llm_context import LLMContext, LLMSpecificMessage
|
from pipecat.processors.aggregators.llm_context import LLMContext, LLMSpecificMessage
|
||||||
from pipecat.services.llm_service import LLMService
|
from pipecat.services.llm_service import LLMService
|
||||||
from pipecat.utils.context.llm_context_summarization import (
|
from pipecat.utils.context.llm_context_summarization import (
|
||||||
|
LLMAutoContextSummarizationConfig,
|
||||||
LLMContextSummarizationConfig,
|
LLMContextSummarizationConfig,
|
||||||
LLMContextSummarizationUtil,
|
LLMContextSummarizationUtil,
|
||||||
|
LLMContextSummaryConfig,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -167,43 +169,109 @@ class TestContextSummarizationMixin(unittest.TestCase):
|
|||||||
self.assertIn("USER: First part Second part", transcript)
|
self.assertIn("USER: First part Second part", transcript)
|
||||||
|
|
||||||
|
|
||||||
class TestLLMContextSummarizationConfig(unittest.TestCase):
|
class TestLLMContextSummaryConfig(unittest.TestCase):
|
||||||
"""Tests for LLMContextSummarizationConfig."""
|
"""Tests for LLMContextSummaryConfig."""
|
||||||
|
|
||||||
def test_default_config(self):
|
def test_default_config(self):
|
||||||
"""Test default configuration values."""
|
"""Test default configuration values."""
|
||||||
config = LLMContextSummarizationConfig()
|
config = LLMContextSummaryConfig()
|
||||||
|
|
||||||
self.assertEqual(config.max_context_tokens, 8000)
|
self.assertEqual(config.target_context_tokens, 6000)
|
||||||
self.assertEqual(config.max_unsummarized_messages, 20)
|
|
||||||
self.assertEqual(config.min_messages_after_summary, 4)
|
self.assertEqual(config.min_messages_after_summary, 4)
|
||||||
self.assertIsNone(config.summarization_prompt)
|
self.assertIsNone(config.summarization_prompt)
|
||||||
|
|
||||||
def test_custom_config(self):
|
def test_custom_config(self):
|
||||||
"""Test custom configuration."""
|
"""Test custom configuration."""
|
||||||
config = LLMContextSummarizationConfig(
|
config = LLMContextSummaryConfig(
|
||||||
max_context_tokens=2500,
|
|
||||||
target_context_tokens=2000,
|
target_context_tokens=2000,
|
||||||
max_unsummarized_messages=15,
|
|
||||||
min_messages_after_summary=4,
|
min_messages_after_summary=4,
|
||||||
summarization_prompt="Custom prompt",
|
summarization_prompt="Custom prompt",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(config.max_context_tokens, 2500)
|
|
||||||
self.assertEqual(config.target_context_tokens, 2000)
|
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.min_messages_after_summary, 4)
|
||||||
self.assertEqual(config.summary_prompt, "Custom prompt")
|
self.assertEqual(config.summary_prompt, "Custom prompt")
|
||||||
|
|
||||||
def test_summary_prompt_property(self):
|
def test_summary_prompt_property(self):
|
||||||
"""Test summary_prompt property uses default when None."""
|
"""Test summary_prompt property uses default when None."""
|
||||||
config = LLMContextSummarizationConfig()
|
config = LLMContextSummaryConfig()
|
||||||
self.assertIn("summarizing a conversation", config.summary_prompt.lower())
|
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")
|
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):
|
class TestFunctionCallHandling(unittest.TestCase):
|
||||||
"""Tests for function call handling in summarization."""
|
"""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."}
|
{"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
|
max_context_tokens=50, # Very low to trigger easily
|
||||||
llm=dedicated_llm,
|
summary_config=LLMContextSummaryConfig(
|
||||||
summarization_timeout=5.0,
|
llm=dedicated_llm,
|
||||||
|
summarization_timeout=5.0,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
return context, config
|
return context, config
|
||||||
|
|
||||||
@@ -736,7 +806,7 @@ class TestDedicatedLLMSummarization(unittest.IsolatedAsyncioTestCase):
|
|||||||
dedicated_llm._generate_summary = slow_summary
|
dedicated_llm._generate_summary = slow_summary
|
||||||
|
|
||||||
context, config = self._create_context_and_config(dedicated_llm)
|
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)
|
summarizer = LLMContextSummarizer(context=context, config=config)
|
||||||
await summarizer.setup(self.task_manager)
|
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."}
|
{"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)
|
summarizer = LLMContextSummarizer(context=context, config=config)
|
||||||
await summarizer.setup(self.task_manager)
|
await summarizer.setup(self.task_manager)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from pipecat.frames.frames import (
|
|||||||
LLMContextSummaryRequestFrame,
|
LLMContextSummaryRequestFrame,
|
||||||
LLMContextSummaryResultFrame,
|
LLMContextSummaryResultFrame,
|
||||||
LLMFullResponseStartFrame,
|
LLMFullResponseStartFrame,
|
||||||
|
LLMSummarizeContextFrame,
|
||||||
)
|
)
|
||||||
from pipecat.processors.aggregators.llm_context import LLMContext
|
from pipecat.processors.aggregators.llm_context import LLMContext
|
||||||
from pipecat.processors.aggregators.llm_context_summarizer import (
|
from pipecat.processors.aggregators.llm_context_summarizer import (
|
||||||
@@ -19,7 +20,10 @@ from pipecat.processors.aggregators.llm_context_summarizer import (
|
|||||||
SummaryAppliedEvent,
|
SummaryAppliedEvent,
|
||||||
)
|
)
|
||||||
from pipecat.utils.asyncio.task_manager import TaskManager, TaskManagerParams
|
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):
|
class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
|
||||||
@@ -35,7 +39,7 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def test_summarization_triggered_by_token_limit(self):
|
async def test_summarization_triggered_by_token_limit(self):
|
||||||
"""Test that summarization is triggered when token limit is reached."""
|
"""Test that summarization is triggered when token limit is reached."""
|
||||||
config = LLMContextSummarizationConfig(
|
config = LLMAutoContextSummarizationConfig(
|
||||||
max_context_tokens=100, # Very low to trigger easily
|
max_context_tokens=100, # Very low to trigger easily
|
||||||
max_unsummarized_messages=100, # High so it doesn't trigger by message count
|
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):
|
async def test_summarization_triggered_by_message_count(self):
|
||||||
"""Test that summarization is triggered when message count threshold is reached."""
|
"""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_context_tokens=100000, # Very high so it doesn't trigger by tokens
|
||||||
max_unsummarized_messages=5, # Low to trigger easily
|
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):
|
async def test_summarization_not_triggered_below_thresholds(self):
|
||||||
"""Test that summarization is not triggered when below thresholds."""
|
"""Test that summarization is not triggered when below thresholds."""
|
||||||
config = LLMContextSummarizationConfig(
|
config = LLMAutoContextSummarizationConfig(
|
||||||
max_context_tokens=10000,
|
max_context_tokens=10000,
|
||||||
max_unsummarized_messages=20,
|
max_unsummarized_messages=20,
|
||||||
)
|
)
|
||||||
@@ -130,7 +134,7 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def test_summarization_in_progress_prevents_duplicate(self):
|
async def test_summarization_in_progress_prevents_duplicate(self):
|
||||||
"""Test that a summarization in progress prevents triggering another."""
|
"""Test that a summarization in progress prevents triggering another."""
|
||||||
config = LLMContextSummarizationConfig(
|
config = LLMAutoContextSummarizationConfig(
|
||||||
max_context_tokens=50, # Very low
|
max_context_tokens=50, # Very low
|
||||||
max_unsummarized_messages=100,
|
max_unsummarized_messages=100,
|
||||||
)
|
)
|
||||||
@@ -161,7 +165,10 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def test_summary_result_handling(self):
|
async def test_summary_result_handling(self):
|
||||||
"""Test that summary results are processed and applied correctly."""
|
"""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)
|
summarizer = LLMContextSummarizer(context=self.context, config=config)
|
||||||
await summarizer.setup(self.task_manager)
|
await summarizer.setup(self.task_manager)
|
||||||
@@ -208,7 +215,7 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def test_interruption_cancels_summarization(self):
|
async def test_interruption_cancels_summarization(self):
|
||||||
"""Test that an interruption cancels pending summarization."""
|
"""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)
|
summarizer = LLMContextSummarizer(context=self.context, config=config)
|
||||||
await summarizer.setup(self.task_manager)
|
await summarizer.setup(self.task_manager)
|
||||||
@@ -238,7 +245,10 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def test_stale_summary_result_ignored(self):
|
async def test_stale_summary_result_ignored(self):
|
||||||
"""Test that stale summary results are ignored."""
|
"""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)
|
summarizer = LLMContextSummarizer(context=self.context, config=config)
|
||||||
await summarizer.setup(self.task_manager)
|
await summarizer.setup(self.task_manager)
|
||||||
@@ -294,9 +304,116 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
await summarizer.cleanup()
|
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):
|
async def test_summary_message_role_is_user(self):
|
||||||
"""Test that the summary message uses the user role."""
|
"""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)
|
summarizer = LLMContextSummarizer(context=self.context, config=config)
|
||||||
await summarizer.setup(self.task_manager)
|
await summarizer.setup(self.task_manager)
|
||||||
@@ -335,7 +452,10 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def test_summary_message_default_template(self):
|
async def test_summary_message_default_template(self):
|
||||||
"""Test that the default summary_message_template is used."""
|
"""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)
|
summarizer = LLMContextSummarizer(context=self.context, config=config)
|
||||||
await summarizer.setup(self.task_manager)
|
await summarizer.setup(self.task_manager)
|
||||||
@@ -377,10 +497,12 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def test_summary_message_custom_template(self):
|
async def test_summary_message_custom_template(self):
|
||||||
"""Test that a custom summary_message_template is applied."""
|
"""Test that a custom summary_message_template is applied."""
|
||||||
config = LLMContextSummarizationConfig(
|
config = LLMAutoContextSummarizationConfig(
|
||||||
max_context_tokens=50,
|
max_context_tokens=50,
|
||||||
min_messages_after_summary=2,
|
summary_config=LLMContextSummaryConfig(
|
||||||
summary_message_template="<context_summary>\n{summary}\n</context_summary>",
|
min_messages_after_summary=2,
|
||||||
|
summary_message_template="<context_summary>\n{summary}\n</context_summary>",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
summarizer = LLMContextSummarizer(context=self.context, config=config)
|
summarizer = LLMContextSummarizer(context=self.context, config=config)
|
||||||
@@ -420,7 +542,10 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def test_on_summary_applied_event(self):
|
async def test_on_summary_applied_event(self):
|
||||||
"""Test that on_summary_applied event fires with correct data."""
|
"""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)
|
summarizer = LLMContextSummarizer(context=self.context, config=config)
|
||||||
await summarizer.setup(self.task_manager)
|
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):
|
async def test_on_summary_applied_not_fired_on_error(self):
|
||||||
"""Test that on_summary_applied event is NOT fired when summarization fails."""
|
"""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)
|
summarizer = LLMContextSummarizer(context=self.context, config=config)
|
||||||
await summarizer.setup(self.task_manager)
|
await summarizer.setup(self.task_manager)
|
||||||
@@ -515,9 +643,9 @@ class TestLLMContextSummarizer(unittest.IsolatedAsyncioTestCase):
|
|||||||
|
|
||||||
async def test_request_frame_includes_timeout(self):
|
async def test_request_frame_includes_timeout(self):
|
||||||
"""Test that the request frame includes the configured summarization_timeout."""
|
"""Test that the request frame includes the configured summarization_timeout."""
|
||||||
config = LLMContextSummarizationConfig(
|
config = LLMAutoContextSummarizationConfig(
|
||||||
max_context_tokens=50,
|
max_context_tokens=50,
|
||||||
summarization_timeout=60.0,
|
summary_config=LLMContextSummaryConfig(summarization_timeout=60.0),
|
||||||
)
|
)
|
||||||
|
|
||||||
summarizer = LLMContextSummarizer(context=self.context, config=config)
|
summarizer = LLMContextSummarizer(context=self.context, config=config)
|
||||||
|
|||||||
Reference in New Issue
Block a user