frames: BotInterruptionFrame is deprecated, use InterruptionTaskFrame

This commit is contained in:
Aleix Conchillo Flaqué
2025-09-08 19:26:28 -07:00
parent 9d9f10ae0e
commit 8249b014f0
16 changed files with 81 additions and 56 deletions

View File

@@ -29,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Deprecated ### Deprecated
- `BotInterruptionFrame` is now deprecated, use `InterruptionTaskFrame` instead.
- `StartInterruptionFrame` is now deprected, use `InterruptionFrame` instead. - `StartInterruptionFrame` is now deprected, use `InterruptionFrame` instead.
- Deprecate `VisionImageFrameAggregator` because `VisionImageRawFrame` has been - Deprecate `VisionImageFrameAggregator` because `VisionImageRawFrame` has been

View File

@@ -14,7 +14,7 @@ from loguru import logger
from pipecat.audio.vad.silero import SileroVADAnalyzer from pipecat.audio.vad.silero import SileroVADAnalyzer
from pipecat.frames.frames import ( from pipecat.frames.frames import (
BotInterruptionFrame, InterruptionFrame,
TextFrame, TextFrame,
TranscriptionFrame, TranscriptionFrame,
UserStartedSpeakingFrame, UserStartedSpeakingFrame,
@@ -115,7 +115,7 @@ async def main():
await task.queue_frames( await task.queue_frames(
[ [
BotInterruptionFrame(), InterruptionFrame(),
UserStartedSpeakingFrame(), UserStartedSpeakingFrame(),
TranscriptionFrame( TranscriptionFrame(
user_id=participant_id, user_id=participant_id,

View File

@@ -48,7 +48,7 @@ class CustomObserver(BaseObserver):
"""Observer to log interruptions and bot speaking events to the console. """Observer to log interruptions and bot speaking events to the console.
Logs all frame instances of: Logs all frame instances of:
- StartInterruptionFrame - InterruptionFrame
- BotStartedSpeakingFrame - BotStartedSpeakingFrame
- BotStoppedSpeakingFrame - BotStoppedSpeakingFrame

View File

@@ -21,9 +21,9 @@ from typing import List, Optional
from loguru import logger from loguru import logger
from pipecat.frames.frames import ( from pipecat.frames.frames import (
BotInterruptionFrame,
EndFrame, EndFrame,
Frame, Frame,
InterruptionTaskFrame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMTextFrame, LLMTextFrame,
@@ -360,7 +360,7 @@ class ClassificationProcessor(FrameProcessor):
await self._voicemail_notifier.notify() # Clear buffered TTS frames await self._voicemail_notifier.notify() # Clear buffered TTS frames
# Interrupt the current pipeline to stop any ongoing processing # Interrupt the current pipeline to stop any ongoing processing
await self.push_frame(BotInterruptionFrame(), FrameDirection.UPSTREAM) await self.push_frame(InterruptionTaskFrame(), FrameDirection.UPSTREAM)
# Set the voicemail event to trigger the voicemail handler # Set the voicemail event to trigger the voicemail handler
self._voicemail_event.clear() self._voicemail_event.clear()

View File

@@ -935,20 +935,6 @@ class VADUserStoppedSpeakingFrame(SystemFrame):
pass pass
@dataclass
class BotInterruptionFrame(SystemFrame):
"""Frame indicating the bot should be interrupted.
Emitted when the bot should be interrupted. This will mainly cause the
same actions as if the user interrupted except that the
UserStartedSpeakingFrame and UserStoppedSpeakingFrame won't be generated.
This frame should be pushed upstreams. It results in the BaseInputTransport
starting an interruption by pushing a StartInterruptionFrame downstream.
"""
pass
@dataclass @dataclass
class BotStartedSpeakingFrame(SystemFrame): class BotStartedSpeakingFrame(SystemFrame):
"""Frame indicating the bot started speaking. """Frame indicating the bot started speaking.
@@ -1336,6 +1322,47 @@ class StopTaskFrame(TaskFrame):
pass pass
@dataclass
class InterruptionTaskFrame(TaskFrame):
"""Frame indicating the bot should be interrupted.
Emitted when the bot should be interrupted. This will mainly cause the
same actions as if the user interrupted except that the
UserStartedSpeakingFrame and UserStoppedSpeakingFrame won't be generated.
This frame should be pushed upstream.
"""
pass
@dataclass
class BotInterruptionFrame(InterruptionTaskFrame):
"""Frame indicating the bot should be interrupted.
.. deprecated:: 0.0.85
This frame is deprecated and will be removed in a future version.
Instead, use `InterruptionTaskFrame`.
Emitted when the bot should be interrupted. This will mainly cause the
same actions as if the user interrupted except that the
UserStartedSpeakingFrame and UserStoppedSpeakingFrame won't be generated.
This frame should be pushed upstream.
"""
def __post_init__(self):
super().__post_init__()
import warnings
with warnings.catch_warnings():
warnings.simplefilter("always")
warnings.warn(
"BotInterruptionFrame is deprecated and will be removed in a future version. "
"Instead, use InterruptionTaskFrame.",
DeprecationWarning,
stacklevel=2,
)
# #
# Control frames # Control frames
# #

View File

@@ -54,7 +54,7 @@ class DebugLogObserver(BaseObserver):
Log frames with specific source/destination filters:: Log frames with specific source/destination filters::
from pipecat.frames.frames import StartInterruptionFrame, UserStartedSpeakingFrame, LLMTextFrame from pipecat.frames.frames import InterruptionFrame, UserStartedSpeakingFrame, LLMTextFrame
from pipecat.observers.loggers.debug_log_observer import DebugLogObserver, FrameEndpoint from pipecat.observers.loggers.debug_log_observer import DebugLogObserver, FrameEndpoint
from pipecat.transports.base_output import BaseOutputTransport from pipecat.transports.base_output import BaseOutputTransport
from pipecat.services.stt_service import STTService from pipecat.services.stt_service import STTService
@@ -62,8 +62,8 @@ class DebugLogObserver(BaseObserver):
observers=[ observers=[
DebugLogObserver( DebugLogObserver(
frame_types={ frame_types={
# Only log StartInterruptionFrame when source is BaseOutputTransport # Only log InterruptionFrame when source is BaseOutputTransport
StartInterruptionFrame: (BaseOutputTransport, FrameEndpoint.SOURCE), InterruptionFrame: (BaseOutputTransport, FrameEndpoint.SOURCE),
# Only log UserStartedSpeakingFrame when destination is STTService # Only log UserStartedSpeakingFrame when destination is STTService
UserStartedSpeakingFrame: (STTService, FrameEndpoint.DESTINATION), UserStartedSpeakingFrame: (STTService, FrameEndpoint.DESTINATION),
# Log LLMTextFrame regardless of source or destination type # Log LLMTextFrame regardless of source or destination type

View File

@@ -32,6 +32,8 @@ from pipecat.frames.frames import (
Frame, Frame,
HeartbeatFrame, HeartbeatFrame,
InputAudioRawFrame, InputAudioRawFrame,
InterruptionFrame,
InterruptionTaskFrame,
MetricsFrame, MetricsFrame,
StartFrame, StartFrame,
StopFrame, StopFrame,
@@ -627,13 +629,20 @@ class PipelineTask(BasePipelineTask):
if isinstance(frame, EndTaskFrame): if isinstance(frame, EndTaskFrame):
# Tell the task we should end nicely. # Tell the task we should end nicely.
logger.debug(f"{self}: received end task frame {frame}")
await self.queue_frame(EndFrame()) await self.queue_frame(EndFrame())
elif isinstance(frame, CancelTaskFrame): elif isinstance(frame, CancelTaskFrame):
# Tell the task we should end right away. # Tell the task we should end right away.
logger.debug(f"{self}: received cancel task frame {frame}")
await self.queue_frame(CancelFrame()) await self.queue_frame(CancelFrame())
elif isinstance(frame, StopTaskFrame): elif isinstance(frame, StopTaskFrame):
# Tell the task we should stop nicely. # Tell the task we should stop nicely.
logger.debug(f"{self}: received stop task frame {frame}")
await self.queue_frame(StopFrame()) await self.queue_frame(StopFrame())
elif isinstance(frame, InterruptionTaskFrame):
# Tell the task we should interrupt the pipeline.
logger.debug(f"{self}: received interruption task frame {frame}")
await self.queue_frame(InterruptionFrame())
elif isinstance(frame, ErrorFrame): elif isinstance(frame, ErrorFrame):
if frame.fatal: if frame.fatal:
logger.error(f"A fatal error occurred: {frame}") logger.error(f"A fatal error occurred: {frame}")
@@ -642,7 +651,7 @@ class PipelineTask(BasePipelineTask):
# Tell the task we should stop. # Tell the task we should stop.
await self.queue_frame(StopTaskFrame()) await self.queue_frame(StopTaskFrame())
else: else:
logger.warning(f"Something went wrong: {frame}") logger.warning(f"{self}: Something went wrong: {frame}")
async def _sink_push_frame(self, frame: Frame, direction: FrameDirection): async def _sink_push_frame(self, frame: Frame, direction: FrameDirection):
"""Process frames coming downstream from the pipeline. """Process frames coming downstream from the pipeline.

View File

@@ -16,15 +16,15 @@ from typing import Optional
from pipecat.audio.dtmf.types import KeypadEntry from pipecat.audio.dtmf.types import KeypadEntry
from pipecat.frames.frames import ( from pipecat.frames.frames import (
BotInterruptionFrame,
CancelFrame, CancelFrame,
EndFrame, EndFrame,
Frame, Frame,
InputDTMFFrame, InputDTMFFrame,
InterruptionTaskFrame,
StartFrame, StartFrame,
TranscriptionFrame, TranscriptionFrame,
) )
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor, FrameProcessorSetup from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
from pipecat.utils.time import time_now_iso8601 from pipecat.utils.time import time_now_iso8601
@@ -105,7 +105,7 @@ class DTMFAggregator(FrameProcessor):
# For first digit, schedule interruption. # For first digit, schedule interruption.
if is_first_digit: if is_first_digit:
await self.push_frame(BotInterruptionFrame(), FrameDirection.UPSTREAM) await self.push_frame(InterruptionTaskFrame(), FrameDirection.UPSTREAM)
# Check for immediate flush conditions # Check for immediate flush conditions
if frame.button == self._termination_digit: if frame.button == self._termination_digit:

View File

@@ -22,7 +22,6 @@ from pipecat.audio.interruptions.base_interruption_strategy import BaseInterrupt
from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams
from pipecat.audio.vad.vad_analyzer import VADParams from pipecat.audio.vad.vad_analyzer import VADParams
from pipecat.frames.frames import ( from pipecat.frames.frames import (
BotInterruptionFrame,
BotStartedSpeakingFrame, BotStartedSpeakingFrame,
BotStoppedSpeakingFrame, BotStoppedSpeakingFrame,
CancelFrame, CancelFrame,
@@ -36,7 +35,7 @@ from pipecat.frames.frames import (
FunctionCallsStartedFrame, FunctionCallsStartedFrame,
InputAudioRawFrame, InputAudioRawFrame,
InterimTranscriptionFrame, InterimTranscriptionFrame,
InterruptionFrame, InterruptionTaskFrame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
LLMMessagesAppendFrame, LLMMessagesAppendFrame,
@@ -532,9 +531,9 @@ class LLMUserContextAggregator(LLMContextResponseAggregator):
if should_interrupt: if should_interrupt:
logger.debug( logger.debug(
"Interruption conditions met - pushing BotInterruptionFrame and aggregation" "Interruption conditions met - pushing InterruptionTaskFrame and aggregation"
) )
await self.push_frame(BotInterruptionFrame(), FrameDirection.UPSTREAM) await self.push_frame(InterruptionTaskFrame(), FrameDirection.UPSTREAM)
await self._process_aggregation() await self._process_aggregation()
else: else:
logger.debug("Interruption conditions not met - not pushing aggregation") logger.debug("Interruption conditions not met - not pushing aggregation")

View File

@@ -13,7 +13,6 @@ LLM processing, and text-to-speech components in conversational AI pipelines.
import asyncio import asyncio
import json import json
from dataclasses import dataclass
from typing import Any, Dict, List, Literal, Optional, Set from typing import Any, Dict, List, Literal, Optional, Set
from loguru import logger from loguru import logger
@@ -23,7 +22,6 @@ from pipecat.audio.interruptions.base_interruption_strategy import BaseInterrupt
from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams from pipecat.audio.turn.smart_turn.base_smart_turn import SmartTurnParams
from pipecat.audio.vad.vad_analyzer import VADParams from pipecat.audio.vad.vad_analyzer import VADParams
from pipecat.frames.frames import ( from pipecat.frames.frames import (
BotInterruptionFrame,
BotStartedSpeakingFrame, BotStartedSpeakingFrame,
BotStoppedSpeakingFrame, BotStoppedSpeakingFrame,
CancelFrame, CancelFrame,
@@ -37,7 +35,7 @@ from pipecat.frames.frames import (
FunctionCallsStartedFrame, FunctionCallsStartedFrame,
InputAudioRawFrame, InputAudioRawFrame,
InterimTranscriptionFrame, InterimTranscriptionFrame,
InterruptionFrame, InterruptionTaskFrame,
LLMContextAssistantTimestampFrame, LLMContextAssistantTimestampFrame,
LLMContextFrame, LLMContextFrame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
@@ -311,9 +309,9 @@ class LLMUserAggregator(LLMContextAggregator):
if should_interrupt: if should_interrupt:
logger.debug( logger.debug(
"Interruption conditions met - pushing BotInterruptionFrame and aggregation" "Interruption conditions met - pushing InterruptionTaskFrame and aggregation"
) )
await self.push_frame(BotInterruptionFrame(), FrameDirection.UPSTREAM) await self.push_frame(InterruptionTaskFrame(), FrameDirection.UPSTREAM)
await self._process_aggregation() await self._process_aggregation()
else: else:
logger.debug("Interruption conditions not met - not pushing aggregation") logger.debug("Interruption conditions not met - not pushing aggregation")

View File

@@ -30,7 +30,6 @@ from loguru import logger
from pydantic import BaseModel, Field, PrivateAttr, ValidationError from pydantic import BaseModel, Field, PrivateAttr, ValidationError
from pipecat.frames.frames import ( from pipecat.frames.frames import (
BotInterruptionFrame,
BotStartedSpeakingFrame, BotStartedSpeakingFrame,
BotStoppedSpeakingFrame, BotStoppedSpeakingFrame,
CancelFrame, CancelFrame,
@@ -42,6 +41,7 @@ from pipecat.frames.frames import (
FunctionCallResultFrame, FunctionCallResultFrame,
InputAudioRawFrame, InputAudioRawFrame,
InterimTranscriptionFrame, InterimTranscriptionFrame,
InterruptionTaskFrame,
LLMContextFrame, LLMContextFrame,
LLMFullResponseEndFrame, LLMFullResponseEndFrame,
LLMFullResponseStartFrame, LLMFullResponseStartFrame,
@@ -1206,7 +1206,7 @@ class RTVIProcessor(FrameProcessor):
async def interrupt_bot(self): async def interrupt_bot(self):
"""Send a bot interruption frame upstream.""" """Send a bot interruption frame upstream."""
await self.push_frame(BotInterruptionFrame(), FrameDirection.UPSTREAM) await self.push_frame(InterruptionTaskFrame(), FrameDirection.UPSTREAM)
async def send_server_message(self, data: Any): async def send_server_message(self, data: Any):
"""Send a server message to the client.""" """Send a server message to the client."""

View File

@@ -86,7 +86,7 @@ class AssistantTranscriptProcessor(BaseTranscriptProcessor):
transcript messages. Utterances are completed when: transcript messages. Utterances are completed when:
- The bot stops speaking (BotStoppedSpeakingFrame) - The bot stops speaking (BotStoppedSpeakingFrame)
- The bot is interrupted (StartInterruptionFrame) - The bot is interrupted (InterruptionFrame)
- The pipeline ends (EndFrame) - The pipeline ends (EndFrame)
""" """
@@ -185,7 +185,7 @@ class AssistantTranscriptProcessor(BaseTranscriptProcessor):
- TTSTextFrame: Aggregates text for current utterance - TTSTextFrame: Aggregates text for current utterance
- BotStoppedSpeakingFrame: Completes current utterance - BotStoppedSpeakingFrame: Completes current utterance
- StartInterruptionFrame: Completes current utterance due to interruption - InterruptionFrame: Completes current utterance due to interruption
- EndFrame: Completes current utterance at pipeline end - EndFrame: Completes current utterance at pipeline end
- CancelFrame: Completes current utterance due to cancellation - CancelFrame: Completes current utterance due to cancellation

View File

@@ -558,7 +558,7 @@ class ElevenLabsTTSService(AudioContextWordTTSService):
logger.trace(f"Closing context {self._context_id} due to interruption") logger.trace(f"Closing context {self._context_id} due to interruption")
try: try:
# ElevenLabs requires that Pipecat manages the contexts and closes them # ElevenLabs requires that Pipecat manages the contexts and closes them
# when they're not longer in use. Since a StartInterruptionFrame is pushed # when they're not longer in use. Since an InterruptionFrame is pushed
# every time the user speaks, we'll use this as a trigger to close the context # every time the user speaks, we'll use this as a trigger to close the context
# and reset the state. # and reset the state.
# Note: We do not need to call remove_audio_context here, as the context is # Note: We do not need to call remove_audio_context here, as the context is

View File

@@ -19,12 +19,12 @@ from loguru import logger
from pydantic import BaseModel from pydantic import BaseModel
from pipecat.frames.frames import ( from pipecat.frames.frames import (
BotInterruptionFrame,
CancelFrame, CancelFrame,
EndFrame, EndFrame,
ErrorFrame, ErrorFrame,
Frame, Frame,
InterimTranscriptionFrame, InterimTranscriptionFrame,
InterruptionTaskFrame,
StartFrame, StartFrame,
TranscriptionFrame, TranscriptionFrame,
UserStartedSpeakingFrame, UserStartedSpeakingFrame,
@@ -756,7 +756,7 @@ class SpeechmaticsSTTService(STTService):
if self._params.enable_vad and not self._is_speaking: if self._params.enable_vad and not self._is_speaking:
logger.debug("User started speaking") logger.debug("User started speaking")
self._is_speaking = True self._is_speaking = True
upstream_frames += [BotInterruptionFrame()] upstream_frames += [InterruptionTaskFrame()]
downstream_frames += [UserStartedSpeakingFrame()] downstream_frames += [UserStartedSpeakingFrame()]
# If final, then re-parse into TranscriptionFrame # If final, then re-parse into TranscriptionFrame

View File

@@ -22,7 +22,6 @@ from pipecat.audio.turn.base_turn_analyzer import (
) )
from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADState from pipecat.audio.vad.vad_analyzer import VADAnalyzer, VADState
from pipecat.frames.frames import ( from pipecat.frames.frames import (
BotInterruptionFrame,
BotStartedSpeakingFrame, BotStartedSpeakingFrame,
BotStoppedSpeakingFrame, BotStoppedSpeakingFrame,
CancelFrame, CancelFrame,
@@ -34,6 +33,7 @@ from pipecat.frames.frames import (
InputAudioRawFrame, InputAudioRawFrame,
InputImageRawFrame, InputImageRawFrame,
InterruptionFrame, InterruptionFrame,
InterruptionTaskFrame,
MetricsFrame, MetricsFrame,
SpeechControlParamsFrame, SpeechControlParamsFrame,
StartFrame, StartFrame,
@@ -289,8 +289,6 @@ class BaseInputTransport(FrameProcessor):
elif isinstance(frame, CancelFrame): elif isinstance(frame, CancelFrame):
await self.cancel(frame) await self.cancel(frame)
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
elif isinstance(frame, BotInterruptionFrame):
await self._handle_bot_interruption(frame)
elif isinstance(frame, BotStartedSpeakingFrame): elif isinstance(frame, BotStartedSpeakingFrame):
await self._handle_bot_started_speaking(frame) await self._handle_bot_started_speaking(frame)
await self.push_frame(frame, direction) await self.push_frame(frame, direction)
@@ -335,13 +333,6 @@ class BaseInputTransport(FrameProcessor):
# Handle interruptions # Handle interruptions
# #
async def _handle_bot_interruption(self, frame: BotInterruptionFrame):
"""Handle bot interruption frames."""
logger.debug("Bot interruption")
if self.interruptions_allowed:
await self._start_interruption()
await self.push_frame(InterruptionFrame())
async def _handle_user_interruption(self, vad_state: VADState, emulated: bool = False): async def _handle_user_interruption(self, vad_state: VADState, emulated: bool = False):
"""Handle user interruption events based on speaking state.""" """Handle user interruption events based on speaking state."""
if vad_state == VADState.SPEAKING: if vad_state == VADState.SPEAKING:
@@ -353,7 +344,7 @@ class BaseInputTransport(FrameProcessor):
await self.push_frame(downstream_frame) await self.push_frame(downstream_frame)
await self.push_frame(upstream_frame, FrameDirection.UPSTREAM) await self.push_frame(upstream_frame, FrameDirection.UPSTREAM)
# Only push StartInterruptionFrame if: # Only push InterruptionFrame if:
# 1. No interruption config is set, OR # 1. No interruption config is set, OR
# 2. Interruption config is set but bot is not speaking # 2. Interruption config is set but bot is not speaking
should_push_immediate_interruption = ( should_push_immediate_interruption = (

View File

@@ -287,9 +287,8 @@ class BaseOutputTransport(FrameProcessor):
await super().process_frame(frame, direction) await super().process_frame(frame, direction)
# #
# System frames (like StartInterruptionFrame) are pushed # System frames (like InterruptionFrame) are pushed immediately. Other
# immediately. Other frames require order so they are put in the sink # frames require order so they are put in the sink queue.
# queue.
# #
if isinstance(frame, StartFrame): if isinstance(frame, StartFrame):
# Push StartFrame before start(), because we want StartFrame to be # Push StartFrame before start(), because we want StartFrame to be