From 1d7027557416fda20249a6a4716b950c7cc407c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Tue, 23 Dec 2025 21:55:13 -0800 Subject: [PATCH 01/11] initial user mute strategies --- src/pipecat/turns/mute/__init__.py | 0 .../turns/mute/always_user_mute_strategy.py | 41 +++++++++++ .../turns/mute/base_user_mute_strategy.py | 69 +++++++++++++++++++ .../mute/first_speech_user_mute_strategy.py | 64 +++++++++++++++++ .../mute/function_call_user_mute_strategy.py | 59 ++++++++++++++++ ...l_first_bot_complete_user_mute_strategy.py | 56 +++++++++++++++ 6 files changed, 289 insertions(+) create mode 100644 src/pipecat/turns/mute/__init__.py create mode 100644 src/pipecat/turns/mute/always_user_mute_strategy.py create mode 100644 src/pipecat/turns/mute/base_user_mute_strategy.py create mode 100644 src/pipecat/turns/mute/first_speech_user_mute_strategy.py create mode 100644 src/pipecat/turns/mute/function_call_user_mute_strategy.py create mode 100644 src/pipecat/turns/mute/mute_until_first_bot_complete_user_mute_strategy.py diff --git a/src/pipecat/turns/mute/__init__.py b/src/pipecat/turns/mute/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/pipecat/turns/mute/always_user_mute_strategy.py b/src/pipecat/turns/mute/always_user_mute_strategy.py new file mode 100644 index 000000000..0bbe85173 --- /dev/null +++ b/src/pipecat/turns/mute/always_user_mute_strategy.py @@ -0,0 +1,41 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""User mute strategy that always mutes the user while the bot is speaking.""" + +from pipecat.frames.frames import BotStartedSpeakingFrame, BotStoppedSpeakingFrame, Frame +from pipecat.turns.mute.base_user_mute_strategy import BaseUserMuteStrategy + + +class AlwaysUserMuteStrategy(BaseUserMuteStrategy): + """User mute strategy that always mutes the user while the bot is speaking.""" + + def __init__(self): + """Initialize the always user mute strategy.""" + super().__init__() + self._bot_speaking = False + + async def reset(self): + """Reset the strategy to its initial state.""" + self._bot_speaking = False + + async def process_frame(self, frame: Frame) -> bool: + """Process an incoming frame. + + Args: + frame: The frame to be processed. + + Returns: + Whether the strategy is muted. + """ + await super().process_frame(frame) + + if isinstance(frame, BotStartedSpeakingFrame): + self._bot_speaking = True + elif isinstance(frame, BotStoppedSpeakingFrame): + self._bot_speaking = False + + return self._bot_speaking diff --git a/src/pipecat/turns/mute/base_user_mute_strategy.py b/src/pipecat/turns/mute/base_user_mute_strategy.py new file mode 100644 index 000000000..dd3075c96 --- /dev/null +++ b/src/pipecat/turns/mute/base_user_mute_strategy.py @@ -0,0 +1,69 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""Base strategy for deciding whether user frames should be muted.""" + +from typing import Optional + +from pipecat.frames.frames import Frame +from pipecat.utils.asyncio.task_manager import BaseTaskManager +from pipecat.utils.base_object import BaseObject + + +class BaseUserMuteStrategy(BaseObject): + """Base class for strategies that decide whether user frames should be muted. + + A user mute strategy determines whether incoming user frames should be + suppressed based on the *current system state*. + + Typical heuristics include: + - The bot is currently speaking, so user should be muted + - A function call or tool execution is in progress + - The system is otherwise not ready to accept user input + + The strategy is evaluated per frame and returns a boolean indicating whether + the user should be muted. + + """ + + def __init__(self, **kwargs): + """Initialize the base user mute strategy.""" + super().__init__(**kwargs) + self._task_manager: Optional[BaseTaskManager] = None + + @property + def task_manager(self) -> BaseTaskManager: + """Returns the configured task manager.""" + if not self._task_manager: + raise RuntimeError(f"{self} user mute strategy was not properly setup") + return self._task_manager + + async def setup(self, task_manager: BaseTaskManager): + """Initialize the strategy with the given task manager. + + Args: + task_manager: The task manager to be associated with this instance. + """ + self._task_manager = task_manager + + async def cleanup(self): + """Cleanup the strategy.""" + pass + + async def reset(self): + """Reset the strategy to its initial state.""" + pass + + async def process_frame(self, frame: Frame) -> bool: + """Process an incoming frame. + + Args: + frame: The frame to be processed. + + Returns: + Whether the strategy is muted. + """ + return False diff --git a/src/pipecat/turns/mute/first_speech_user_mute_strategy.py b/src/pipecat/turns/mute/first_speech_user_mute_strategy.py new file mode 100644 index 000000000..29e59ae99 --- /dev/null +++ b/src/pipecat/turns/mute/first_speech_user_mute_strategy.py @@ -0,0 +1,64 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""User mute strategy that mutes the user only during the bot’s first speech.""" + +from pipecat.frames.frames import BotStartedSpeakingFrame, BotStoppedSpeakingFrame, Frame +from pipecat.turns.mute.base_user_mute_strategy import BaseUserMuteStrategy + + +class FirstSpeechUserMuteStrategy(BaseUserMuteStrategy): + """User mute strategy that mutes the user only during the bot’s first speech. + + This strategy allows user input before the bot starts speaking. Once the bot + begins its first speaking turn, user frames are muted until the bot finishes + that speech. After the bot completes its first speaking turn, user input is + no longer muted by this strategy. + + Use this strategy when early user input is acceptable, but interruptions + during the bot’s initial response should be prevented. + + """ + + def __init__(self): + """Initialize the first-bot-speech user mute strategy.""" + super().__init__() + self._bot_speaking = False + self._first_speech_handled = False + + async def reset(self): + """Reset the strategy to its initial state.""" + self._bot_speaking = False + self._first_speech_handled = False + + async def process_frame(self, frame: Frame) -> bool: + """Process an incoming frame. + + Args: + frame: The frame to be processed. + + Returns: + Whether the strategy is muted. + """ + await super().process_frame(frame) + + if isinstance(frame, BotStartedSpeakingFrame): + await self._handle_bot_started_speaking(frame) + elif isinstance(frame, BotStoppedSpeakingFrame): + await self._handle_bot_stopped_speaking(frame) + + if self._bot_speaking and not self._first_speech_handled: + return True + + return False + + async def _handle_bot_started_speaking(self, frame: BotStartedSpeakingFrame): + self._bot_speaking = True + + async def _handle_bot_stopped_speaking(self, frame: BotStoppedSpeakingFrame): + self._bot_speaking = False + if not self._first_speech_handled: + self._first_speech_handled = True diff --git a/src/pipecat/turns/mute/function_call_user_mute_strategy.py b/src/pipecat/turns/mute/function_call_user_mute_strategy.py new file mode 100644 index 000000000..938dc5936 --- /dev/null +++ b/src/pipecat/turns/mute/function_call_user_mute_strategy.py @@ -0,0 +1,59 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""User mute strategy that mutes the user while a function call is executing.""" + +from typing import Set + +from pipecat.frames.frames import ( + Frame, + FunctionCallCancelFrame, + FunctionCallResultFrame, + FunctionCallsStartedFrame, +) +from pipecat.turns.mute.base_user_mute_strategy import BaseUserMuteStrategy + + +class FunctionCallUserMuteStrategy(BaseUserMuteStrategy): + """User mute strategy that mutes the user while a function call is executing. + + This strategy ensures that user input does not interfere with ongoing + function execution. While a function call is active, all user frames are + muted. Once the function call completes or is canceled, user input is + allowed again. + + """ + + def __init__(self): + """Initialize the function call user mute strategy.""" + super().__init__() + self._function_call_in_progress: Set[str] = set() + + async def reset(self): + """Reset the strategy to its initial state.""" + self._function_call_in_progress = set() + + async def process_frame(self, frame: Frame) -> bool: + """Process an incoming frame. + + Args: + frame: The frame to be processed. + + Returns: + Whether the strategy is muted. + """ + await super().process_frame(frame) + + if isinstance(frame, FunctionCallsStartedFrame): + await self._handle_function_calls_started(frame) + elif isinstance(frame, (FunctionCallCancelFrame, FunctionCallResultFrame)): + self._function_call_in_progress.remove(frame.tool_call_id) + + return bool(self._function_call_in_progress) + + async def _handle_function_calls_started(self, frame: FunctionCallsStartedFrame): + for f in frame.function_calls: + self._function_call_in_progress.add(f.tool_call_id) diff --git a/src/pipecat/turns/mute/mute_until_first_bot_complete_user_mute_strategy.py b/src/pipecat/turns/mute/mute_until_first_bot_complete_user_mute_strategy.py new file mode 100644 index 000000000..093f5356f --- /dev/null +++ b/src/pipecat/turns/mute/mute_until_first_bot_complete_user_mute_strategy.py @@ -0,0 +1,56 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +"""User mute strategy that mutes the user until the bot completes its first speech.""" + +from pipecat.frames.frames import BotStoppedSpeakingFrame, Frame +from pipecat.turns.mute.base_user_mute_strategy import BaseUserMuteStrategy + + +class MuteUntilFirstBotCompleteUserMuteStrategy(BaseUserMuteStrategy): + """User mute strategy that mutes the user until the bot completes its first speech. + + This strategy mutes user frames immediately from the start of the + interaction, even if the bot has not started speaking yet. User input + remains muted until the bot finishes its first speaking turn. + + After the bot completes its initial speech, all subsequent user frames are + allowed to pass through without muting. + + Use this strategy when the bot must fully control the beginning of the + interaction and deliver its first response without any user interruption. + + """ + + def __init__(self): + """Initialize the mute-until-first-bot-complete user mute strategy.""" + super().__init__() + self._first_speech_handled = False + + async def reset(self): + """Reset the strategy to its initial state.""" + self._first_speech_handled = False + + async def process_frame(self, frame: Frame) -> bool: + """Process an incoming frame. + + Args: + frame: The frame to be processed. + + Returns: + Whether the strategy is muted. + """ + await super().process_frame(frame) + + if isinstance(frame, BotStoppedSpeakingFrame): + await self._handle_bot_stopped_speaking(frame) + + return not self._first_speech_handled + + async def _handle_bot_stopped_speaking(self, frame: BotStoppedSpeakingFrame): + self._bot_speaking = False + if not self._first_speech_handled: + self._first_speech_handled = True From 5a4236bc717beb602802c3739053df128bc16f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 24 Dec 2025 09:32:46 -0800 Subject: [PATCH 02/11] tests: add user mute strategy tests --- tests/test_user_mute_strategy.py | 139 +++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 tests/test_user_mute_strategy.py diff --git a/tests/test_user_mute_strategy.py b/tests/test_user_mute_strategy.py new file mode 100644 index 000000000..674a273b5 --- /dev/null +++ b/tests/test_user_mute_strategy.py @@ -0,0 +1,139 @@ +# +# Copyright (c) 2024-2025 Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +import unittest + +from pipecat.frames.frames import ( + BotStartedSpeakingFrame, + BotStoppedSpeakingFrame, + FunctionCallCancelFrame, + FunctionCallFromLLM, + FunctionCallResultFrame, + FunctionCallsStartedFrame, + InterruptionFrame, +) +from pipecat.turns.mute.always_user_mute_strategy import AlwaysUserMuteStrategy +from pipecat.turns.mute.first_speech_user_mute_strategy import FirstSpeechUserMuteStrategy +from pipecat.turns.mute.function_call_user_mute_strategy import FunctionCallUserMuteStrategy +from pipecat.turns.mute.mute_until_first_bot_complete_user_mute_strategy import ( + MuteUntilFirstBotCompleteUserMuteStrategy, +) + + +class TestAlwaysUserMuteStrategy(unittest.IsolatedAsyncioTestCase): + async def test_user_mute_strategy(self): + strategy = AlwaysUserMuteStrategy() + + self.assertTrue(await strategy.process_frame(BotStartedSpeakingFrame())) + self.assertTrue(await strategy.process_frame(InterruptionFrame())) + self.assertFalse(await strategy.process_frame(BotStoppedSpeakingFrame())) + self.assertFalse(await strategy.process_frame(InterruptionFrame())) + + +class TestFirstSpeechUserMuteStrategy(unittest.IsolatedAsyncioTestCase): + async def test_user_mute_strategy(self): + strategy = FirstSpeechUserMuteStrategy() + + self.assertFalse(await strategy.process_frame(InterruptionFrame())) + self.assertTrue(await strategy.process_frame(BotStartedSpeakingFrame())) + self.assertTrue(await strategy.process_frame(InterruptionFrame())) + self.assertFalse(await strategy.process_frame(BotStoppedSpeakingFrame())) + self.assertFalse(await strategy.process_frame(InterruptionFrame())) + + +class TestMuteUntilFirstBotCompleteUserMuteStrategy(unittest.IsolatedAsyncioTestCase): + async def test_user_mute_strategy(self): + strategy = MuteUntilFirstBotCompleteUserMuteStrategy() + + self.assertTrue(await strategy.process_frame(InterruptionFrame())) + self.assertTrue(await strategy.process_frame(BotStartedSpeakingFrame())) + self.assertTrue(await strategy.process_frame(InterruptionFrame())) + self.assertFalse(await strategy.process_frame(BotStoppedSpeakingFrame())) + self.assertFalse(await strategy.process_frame(InterruptionFrame())) + + +class TestFunctionCallUserMuteStrategy(unittest.IsolatedAsyncioTestCase): + async def test_user_mute_strategy(self): + strategy = FunctionCallUserMuteStrategy() + + self.assertFalse(await strategy.process_frame(InterruptionFrame())) + # First function call (cancelled) + self.assertTrue( + await strategy.process_frame( + FunctionCallsStartedFrame( + function_calls=[ + FunctionCallFromLLM( + function_name="fn_1", tool_call_id="1", arguments={}, context=None + ) + ] + ) + ) + ) + self.assertTrue(await strategy.process_frame(InterruptionFrame())) + self.assertFalse( + await strategy.process_frame( + FunctionCallCancelFrame(function_name="fn_1", tool_call_id="1") + ) + ) + self.assertFalse(await strategy.process_frame(InterruptionFrame())) + + # Second function call (finished) + self.assertTrue( + await strategy.process_frame( + FunctionCallsStartedFrame( + function_calls=[ + FunctionCallFromLLM( + function_name="fn_2", tool_call_id="2", arguments={}, context=None + ) + ] + ) + ) + ) + self.assertTrue(await strategy.process_frame(InterruptionFrame())) + self.assertFalse( + await strategy.process_frame( + FunctionCallResultFrame( + function_name="fn_2", tool_call_id="2", arguments={}, result={} + ) + ) + ) + self.assertFalse(await strategy.process_frame(InterruptionFrame())) + + # Multiple function calls + self.assertTrue( + await strategy.process_frame( + FunctionCallsStartedFrame( + function_calls=[ + FunctionCallFromLLM( + function_name="fn_3", tool_call_id="3", arguments={}, context=None + ), + FunctionCallFromLLM( + function_name="fn_4", tool_call_id="4", arguments={}, context=None + ), + ] + ) + ) + ) + self.assertTrue(await strategy.process_frame(InterruptionFrame())) + # First function call is done, we still should be muted since there's + # another one ongoing. + self.assertTrue( + await strategy.process_frame( + FunctionCallResultFrame( + function_name="fn_3", tool_call_id="3", arguments={}, result={} + ) + ) + ) + self.assertTrue(await strategy.process_frame(InterruptionFrame())) + # Last function call finishes. + self.assertFalse( + await strategy.process_frame( + FunctionCallResultFrame( + function_name="fn_4", tool_call_id="4", arguments={}, result={} + ) + ) + ) + self.assertFalse(await strategy.process_frame(InterruptionFrame())) From c33c8d21952be95f34806f2cba079de476a89ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 24 Dec 2025 10:17:30 -0800 Subject: [PATCH 03/11] LLMUserAggregator: add support for user mute strategies --- .../aggregators/llm_response_universal.py | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/pipecat/processors/aggregators/llm_response_universal.py b/src/pipecat/processors/aggregators/llm_response_universal.py index 6dd7f51b4..ef4269a07 100644 --- a/src/pipecat/processors/aggregators/llm_response_universal.py +++ b/src/pipecat/processors/aggregators/llm_response_universal.py @@ -15,7 +15,7 @@ import asyncio import json import warnings from abc import abstractmethod -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, Dict, List, Literal, Optional, Set, Type from loguru import logger @@ -31,6 +31,8 @@ from pipecat.frames.frames import ( FunctionCallInProgressFrame, FunctionCallResultFrame, FunctionCallsStartedFrame, + InputAudioRawFrame, + InterimTranscriptionFrame, InterruptionFrame, LLMContextAssistantTimestampFrame, LLMContextFrame, @@ -62,6 +64,7 @@ from pipecat.processors.aggregators.llm_context import ( ) from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.turns.bot.base_bot_turn_start_strategy import BaseBotTurnStartStrategy +from pipecat.turns.mute.base_user_mute_strategy import BaseUserMuteStrategy from pipecat.turns.user.base_user_turn_start_strategy import BaseUserTurnStartStrategy from pipecat.utils.string import TextPartForConcatenation, concatenate_aggregated_text from pipecat.utils.time import time_now_iso8601 @@ -77,11 +80,13 @@ class LLMUserAggregatorParams: interruption frames. This is enabled by default, but you may want to disable it if another component (e.g., an STT service) is already generating these frames. + user_mute_strategies: List of user mute strategies. user_turn_end_timeout: Time in seconds to wait before considering the user's turn finished and starting the bot turn. """ enable_user_speaking_frames: bool = True + user_mute_strategies: List[BaseUserMuteStrategy] = field(default_factory=list) user_turn_end_timeout: float = 5.0 @@ -269,6 +274,7 @@ class LLMUserAggregator(LLMContextAggregator): self._vad_user_speaking = False self._user_turn = False + self._user_is_muted = False self._user_turn_end_timeout_event = asyncio.Event() self._user_turn_end_timeout_task: Optional[asyncio.Task] = None @@ -302,6 +308,9 @@ class LLMUserAggregator(LLMContextAggregator): """ await super().process_frame(frame, direction) + if await self._maybe_mute_frame(frame): + return + if isinstance(frame, StartFrame): # Push StartFrame before start(), because we want StartFrame to be # processed by every processor before any other frame is processed. @@ -362,6 +371,9 @@ class LLMUserAggregator(LLMContextAggregator): self._user_turn_end_timeout_task_handler() ) + for s in self._params.user_mute_strategies: + await s.setup(self.task_manager) + if self.turn_start_strategies and self.turn_start_strategies.user: for s in self.turn_start_strategies.user: await s.setup(self.task_manager) @@ -387,6 +399,9 @@ class LLMUserAggregator(LLMContextAggregator): await self.cancel_task(self._user_turn_end_timeout_task) self._user_turn_end_timeout_task = None + for s in self._params.user_mute_strategies: + await s.cleanup() + if self.turn_start_strategies and self.turn_start_strategies.user: for s in self.turn_start_strategies.user: await s.cleanup() @@ -395,6 +410,34 @@ class LLMUserAggregator(LLMContextAggregator): for s in self.turn_start_strategies.bot: await s.cleanup() + async def _maybe_mute_frame(self, frame: Frame): + should_mute_frame = self._user_is_muted and isinstance( + frame, + ( + InterruptionFrame, + VADUserStartedSpeakingFrame, + VADUserStoppedSpeakingFrame, + UserStartedSpeakingFrame, + UserStoppedSpeakingFrame, + InputAudioRawFrame, + InterimTranscriptionFrame, + TranscriptionFrame, + ), + ) + + if should_mute_frame: + logger.trace(f"{frame.name} suppressed - user currently muted") + + should_mute_next_time = False + for s in self._params.user_mute_strategies: + should_mute_next_time |= await s.process_frame(frame) + + if should_mute_next_time != self._user_is_muted: + logger.debug(f"{self}: user is now {'muted' if should_mute_next_time else 'unmuted'}") + self._user_is_muted = should_mute_next_time + + return should_mute_frame + async def _turn_start_strategies_process_frame(self, frame: Frame): if self.turn_start_strategies and self.turn_start_strategies.user: for strategy in self.turn_start_strategies.user: From 30922d365fc4d669348a765e36524344e5c58266 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 24 Dec 2025 10:29:48 -0800 Subject: [PATCH 04/11] minor turn start strategies cleanup --- src/pipecat/frames/frames.py | 4 ++++ src/pipecat/processors/frame_processor.py | 2 +- src/pipecat/turns/bot/base_bot_turn_start_strategy.py | 9 ++++----- src/pipecat/turns/user/base_user_turn_start_strategy.py | 9 ++++----- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/pipecat/frames/frames.py b/src/pipecat/frames/frames.py index 1506f7b94..115c41ff6 100644 --- a/src/pipecat/frames/frames.py +++ b/src/pipecat/frames/frames.py @@ -956,6 +956,10 @@ class StartFrame(SystemFrame): enable_tracing: Whether to enable OpenTelemetry tracing. enable_usage_metrics: Whether to enable usage metrics collection. interruption_strategies: List of interruption handling strategies. + + .. deprecated:: 0.0.99 + Use the `turn_start_strategies` instead. + report_only_initial_ttfb: Whether to report only initial time-to-first-byte. """ diff --git a/src/pipecat/processors/frame_processor.py b/src/pipecat/processors/frame_processor.py index ddb546ef7..84db66da1 100644 --- a/src/pipecat/processors/frame_processor.py +++ b/src/pipecat/processors/frame_processor.py @@ -359,7 +359,7 @@ class FrameProcessor(BaseObject): def interruption_strategies(self) -> Sequence[BaseInterruptionStrategy]: """Get the interruption strategies for this processor. - .. deprecated:: 0.0.98 + .. deprecated:: 0.0.99 This function is deprecated, use the new user and bot turn start strategies insted. diff --git a/src/pipecat/turns/bot/base_bot_turn_start_strategy.py b/src/pipecat/turns/bot/base_bot_turn_start_strategy.py index 3d5222169..10bd2de9b 100644 --- a/src/pipecat/turns/bot/base_bot_turn_start_strategy.py +++ b/src/pipecat/turns/bot/base_bot_turn_start_strategy.py @@ -43,10 +43,6 @@ class BaseBotTurnStartStrategy(BaseObject): raise RuntimeError(f"{self} bot turn start strategy was not properly setup") return self._task_manager - async def reset(self): - """Reset the strategy to its initial state.""" - pass - async def setup(self, task_manager: BaseTaskManager): """Initialize the strategy with the given task manager. @@ -59,6 +55,10 @@ class BaseBotTurnStartStrategy(BaseObject): """Cleanup the strategy.""" pass + async def reset(self): + """Reset the strategy to its initial state.""" + pass + async def process_frame(self, frame: Frame): """Process an incoming frame to decide whether the bot should speak. @@ -67,7 +67,6 @@ class BaseBotTurnStartStrategy(BaseObject): Args: frame: The frame to be analyzed. - """ pass diff --git a/src/pipecat/turns/user/base_user_turn_start_strategy.py b/src/pipecat/turns/user/base_user_turn_start_strategy.py index cb825320a..c1d49f6e8 100644 --- a/src/pipecat/turns/user/base_user_turn_start_strategy.py +++ b/src/pipecat/turns/user/base_user_turn_start_strategy.py @@ -43,10 +43,6 @@ class BaseUserTurnStartStrategy(BaseObject): raise RuntimeError(f"{self} user turn start strategy was not properly setup") return self._task_manager - async def reset(self): - """Reset the strategy to its initial state.""" - pass - async def setup(self, task_manager: BaseTaskManager): """Initialize the strategy with the given task manager. @@ -59,6 +55,10 @@ class BaseUserTurnStartStrategy(BaseObject): """Cleanup the strategy.""" pass + async def reset(self): + """Reset the strategy to its initial state.""" + pass + async def process_frame(self, frame: Frame): """Process an incoming frame. @@ -67,7 +67,6 @@ class BaseUserTurnStartStrategy(BaseObject): Args: frame: The frame to be processed. - """ pass From 0abaae2f074f4f99c92e447ddc18bb9e3857c75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 24 Dec 2025 10:30:07 -0800 Subject: [PATCH 05/11] LLMUserAggregator: no need to reset strategies Turn start strategies are already reset when triggered, so there's no need to reset them again. --- .../processors/aggregators/llm_response_universal.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/pipecat/processors/aggregators/llm_response_universal.py b/src/pipecat/processors/aggregators/llm_response_universal.py index ef4269a07..9f11a54e4 100644 --- a/src/pipecat/processors/aggregators/llm_response_universal.py +++ b/src/pipecat/processors/aggregators/llm_response_universal.py @@ -287,18 +287,6 @@ class LLMUserAggregator(LLMContextAggregator): await super().cleanup() await self._cleanup() - async def reset(self): - """Reset the aggregation state and turn start strategies.""" - await super().reset() - - if self.turn_start_strategies and self.turn_start_strategies.user: - for s in self.turn_start_strategies.user: - await s.reset() - - if self.turn_start_strategies and self.turn_start_strategies.bot: - for s in self.turn_start_strategies.bot: - await s.reset() - async def process_frame(self, frame: Frame, direction: FrameDirection): """Process frames for user speech aggregation and context management. From 0efa36a04ea52f608776ff0f5b49379bcda9b412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 24 Dec 2025 10:35:00 -0800 Subject: [PATCH 06/11] examples(foundational): added 24-user-mute-strategy.py example --- .../foundational/24-user-mute-strategy.py | 178 ++++++++++++++++++ examples/foundational/README.md | 2 +- 2 files changed, 179 insertions(+), 1 deletion(-) create mode 100644 examples/foundational/24-user-mute-strategy.py diff --git a/examples/foundational/24-user-mute-strategy.py b/examples/foundational/24-user-mute-strategy.py new file mode 100644 index 000000000..097338dac --- /dev/null +++ b/examples/foundational/24-user-mute-strategy.py @@ -0,0 +1,178 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + + +import asyncio +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 +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.deepgram.stt import DeepgramSTTService +from pipecat.services.deepgram.tts import DeepgramTTSService +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.bot.turn_analyzer_bot_turn_start_strategy import TurnAnalyzerBotTurnStartStrategy +from pipecat.turns.mute.function_call_user_mute_strategy import FunctionCallUserMuteStrategy +from pipecat.turns.mute.mute_until_first_bot_complete_user_mute_strategy import ( + MuteUntilFirstBotCompleteUserMuteStrategy, +) +from pipecat.turns.turn_start_strategies import TurnStartStrategies + +load_dotenv(override=True) + + +async def fetch_weather_from_api(params: FunctionCallParams): + # Add a delay to test interruption during function calls + logger.info("Weather API call starting...") + await asyncio.sleep(5) # 5-second delay + logger.info("Weather API call completed") + await params.result_callback({"conditions": "nice", "temperature": "75"}) + + +# We store functions so objects (e.g. SileroVADAnalyzer) don't get +# instantiated. The function will be called when the desired transport gets +# selected. +transport_params = { + "daily": lambda: DailyParams( + audio_in_enabled=True, + audio_out_enabled=True, + vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)), + ), + "twilio": lambda: FastAPIWebsocketParams( + audio_in_enabled=True, + audio_out_enabled=True, + vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)), + ), + "webrtc": lambda: TransportParams( + audio_in_enabled=True, + audio_out_enabled=True, + vad_analyzer=SileroVADAnalyzer(params=VADParams(stop_secs=0.2)), + ), +} + + +async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): + logger.info(f"Starting bot") + + stt = DeepgramSTTService(api_key=os.getenv("DEEPGRAM_API_KEY")) + + tts = DeepgramTTSService(api_key=os.getenv("DEEPGRAM_API_KEY"), voice="aura-helios-en") + + llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY")) + llm.register_function("get_current_weather", fetch_weather_from_api) + + weather_function = FunctionSchema( + name="get_current_weather", + description="Get the current weather", + properties={ + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "format": { + "type": "string", + "enum": ["celsius", "fahrenheit"], + "description": "The temperature unit to use. Infer this from the user's location.", + }, + }, + required=["location", "format"], + ) + tools = ToolsSchema(standard_tools=[weather_function]) + + messages = [ + { + "role": "system", + "content": "You are a helpful assistant who can check the weather. Always check the weather when a location is mentioned. Respond concisely and naturally. Your output will be spoken aloud, so avoid special characters that can't easily be spoken, such as emojis or bullet points.", + }, + ] + + context = LLMContext(messages, tools) + context_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams( + user_mute_strategies=[ + MuteUntilFirstBotCompleteUserMuteStrategy(), + FunctionCallUserMuteStrategy(), + ] + ), + ) + + pipeline = Pipeline( + [ + transport.input(), # Transport user input + stt, # STT + context_aggregator.user(), # User responses + llm, # LLM + tts, # TTS + transport.output(), # Transport bot output + context_aggregator.assistant(), # Assistant spoken responses + ] + ) + + task = PipelineTask( + pipeline, + params=PipelineParams( + enable_metrics=True, + enable_usage_metrics=True, + turn_start_strategies=TurnStartStrategies( + bot=[TurnAnalyzerBotTurnStartStrategy(turn_analyzer=LocalSmartTurnAnalyzerV3())] + ), + ), + idle_timeout_secs=runner_args.pipeline_idle_timeout_secs, + ) + + @transport.event_handler("on_client_connected") + async def on_client_connected(transport, client): + logger.info(f"Client connected") + # Kick off the conversation with a weather-related prompt + messages.append( + { + "role": "system", + "content": "Ask the user what city they'd like to know the weather for.", + } + ) + await task.queue_frames([LLMRunFrame()]) + + @transport.event_handler("on_client_disconnected") + async def on_client_disconnected(transport, client): + logger.info(f"Client disconnected") + await task.cancel() + + runner = PipelineRunner(handle_sigint=runner_args.handle_sigint) + + await runner.run(task) + + +async def bot(runner_args: RunnerArguments): + """Main bot entry point compatible with Pipecat Cloud.""" + transport = await create_transport(runner_args, transport_params) + await run_bot(transport, runner_args) + + +if __name__ == "__main__": + from pipecat.runner.run import main + + main() diff --git a/examples/foundational/README.md b/examples/foundational/README.md index c1ead3ece..bc25d42ca 100644 --- a/examples/foundational/README.md +++ b/examples/foundational/README.md @@ -80,7 +80,7 @@ uv run 07-interruptible.py -t twilio -x NGROK_HOST_NAME ### Common Utilities - **[17-detect-user-idle.py](./17-detect-user-idle.py)**: Handle inactive users (UserIdleProcessor) -- **[24-stt-mute-filter.py](./24-stt-mute-filter.py)**: Selectively mute user input (STTMuteFilter) +- **[24-user-mute-strategy.py](./24-user-mute-strategy.py)**: Selectively mute user input (LLMUserAggregator user mute strategies) - **[28-transcription-processor.py](./28-transcription-processor.py)**: Record conversation text (TranscriptProcessor) - **[30-observer.py](./30-observer.py)**: Access frame data (Custom observers) - **[31-heartbeats.py](./31-heartbeats.py)**: Detect idle pipelines (Pipeline monitoring) From 53b450c1d1575411b2f18bcd41696777d46e91c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 24 Dec 2025 10:43:40 -0800 Subject: [PATCH 07/11] added changelog entry for user mute strategies --- changelog/3292.added.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 changelog/3292.added.md diff --git a/changelog/3292.added.md b/changelog/3292.added.md new file mode 100644 index 000000000..936d927d8 --- /dev/null +++ b/changelog/3292.added.md @@ -0,0 +1,29 @@ +- Introducing user mute strategies. User mute strategies indicate when user input should be muted based on the current system state. + + In conversational agents, user mute strategies are used to prevent user input from interrupting bot speech, tool execution, or other critical system operations. + + A list of strategies can be specified; all strategies are evaluated for every frame so that each strategy can maintain its internal state. A user frame is muted if any of the configured strategies indicates it should be muted. + + Available user mute strategies: + + * `FirstSpeechUserMuteStrategy` + * `MuteUntilFirstBotCompleteUserMuteStrategy` + * `AlwaysUserMuteStrategy` + * `FunctionCallUserMuteStrategy` + + User mute strategies replace the legacy `STTMuteFilter` and provide a more flexible and composable approach to muting user input. + + User mute strategies are configured when setting up the `LLMContextAggregatorPair`. For example: + + ```python + context_aggregator = LLMContextAggregatorPair( + context, + user_params=LLMUserAggregatorParams( + user_mute_strategies=[ + FirstSpeechUserMuteStrategy(), + ] + ), + ) + ``` + + In order to use user mute strategies you should update to the new universal `LLMContext` and `LLMContextAggregatorPair`. From 43fc26cf0e4a34873dde918b3745fddc5af356bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Wed, 24 Dec 2025 11:01:58 -0800 Subject: [PATCH 08/11] tests: add user mute strategies tests to user aggregator --- tests/test_context_aggregators_universal.py | 63 +++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/test_context_aggregators_universal.py b/tests/test_context_aggregators_universal.py index 20029e789..f8248c565 100644 --- a/tests/test_context_aggregators_universal.py +++ b/tests/test_context_aggregators_universal.py @@ -7,6 +7,12 @@ import unittest from pipecat.frames.frames import ( + BotStartedSpeakingFrame, + BotStoppedSpeakingFrame, + FunctionCallFromLLM, + FunctionCallInProgressFrame, + FunctionCallResultFrame, + FunctionCallsStartedFrame, InterruptionFrame, LLMContextFrame, LLMMessagesAppendFrame, @@ -29,6 +35,8 @@ from pipecat.tests.utils import SleepFrame, run_test from pipecat.turns.bot.transcription_bot_turn_start_strategy import ( TranscriptionBotTurnStartStrategy, ) +from pipecat.turns.mute.first_speech_user_mute_strategy import FirstSpeechUserMuteStrategy +from pipecat.turns.mute.function_call_user_mute_strategy import FunctionCallUserMuteStrategy from pipecat.turns.turn_start_strategies import TurnStartStrategies USER_TURN_END_TIMEOUT = 0.2 @@ -233,3 +241,58 @@ class TestUserAggregator(unittest.IsolatedAsyncioTestCase): # The transcription strategy should kick-in before the user turn end timeout. self.assertTrue(bot_turn) self.assertFalse(timeout) + + async def test_user_mute_strategies(self): + context = LLMContext() + + user_aggregator = LLMUserAggregator( + context, + params=LLMUserAggregatorParams( + user_mute_strategies=[ + FirstSpeechUserMuteStrategy(), + FunctionCallUserMuteStrategy(), + ] + ), + ) + + user_turn = False + + @user_aggregator.event_handler("on_user_turn_started") + async def on_user_turn_started(aggregator, strategy): + nonlocal user_turn + user_turn = True + + pipeline = Pipeline([user_aggregator]) + + frames_to_send = [ + # Bot is speaking, user should be muted. + BotStartedSpeakingFrame(), + VADUserStartedSpeakingFrame(), + VADUserStoppedSpeakingFrame(), + TranscriptionFrame(text="Hello!", user_id="", timestamp="now"), + SleepFrame(), + BotStoppedSpeakingFrame(), + # Function call is executing, user should be muted. + FunctionCallsStartedFrame( + function_calls=[ + FunctionCallFromLLM( + function_name="fn_1", tool_call_id="1", arguments={}, context=None + ) + ] + ), + SleepFrame(), + VADUserStartedSpeakingFrame(), + VADUserStoppedSpeakingFrame(), + TranscriptionFrame(text="Hello!", user_id="", timestamp="now"), + FunctionCallResultFrame( + function_name="fn_1", tool_call_id="1", arguments={}, result={} + ), + SleepFrame(), + ] + await run_test( + pipeline, + frames_to_send=frames_to_send, + ) + + # The user mute strategies should have muted the user. + self.assertFalse(user_turn) From a962c4eeba69bb6ba942acf65689c4c65336798b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Sat, 27 Dec 2025 13:47:58 -0800 Subject: [PATCH 09/11] STTMuteFilter: use FunctionCallsStartedFrame and support multiple function calls --- changelog/3292.fixed.md | 1 + .../processors/filters/stt_mute_filter.py | 14 +-- tests/test_stt_mute_filter.py | 97 ++++++++++--------- 3 files changed, 61 insertions(+), 51 deletions(-) create mode 100644 changelog/3292.fixed.md diff --git a/changelog/3292.fixed.md b/changelog/3292.fixed.md new file mode 100644 index 000000000..4d3df66b0 --- /dev/null +++ b/changelog/3292.fixed.md @@ -0,0 +1 @@ +- Fixed a bug in `STTMuteFilter` where the user was not always muted during function calls, especially when there were multiple simultaneous calls. diff --git a/src/pipecat/processors/filters/stt_mute_filter.py b/src/pipecat/processors/filters/stt_mute_filter.py index 0b822b54a..8a0dca17d 100644 --- a/src/pipecat/processors/filters/stt_mute_filter.py +++ b/src/pipecat/processors/filters/stt_mute_filter.py @@ -21,8 +21,9 @@ from pipecat.frames.frames import ( BotStartedSpeakingFrame, BotStoppedSpeakingFrame, Frame, - FunctionCallInProgressFrame, + FunctionCallCancelFrame, FunctionCallResultFrame, + FunctionCallsStartedFrame, InputAudioRawFrame, InterimTranscriptionFrame, InterruptionFrame, @@ -116,7 +117,7 @@ class STTMuteFilter(FrameProcessor): self._config = config self._first_speech_handled = False self._bot_is_speaking = False - self._function_call_in_progress = False + self._function_call_in_progress = set() self._is_muted = False async def _handle_mute_state(self, should_mute: bool): @@ -176,11 +177,12 @@ class STTMuteFilter(FrameProcessor): # Process frames to determine mute state if isinstance(frame, StartFrame): should_mute = await self._should_mute() - elif isinstance(frame, FunctionCallInProgressFrame): - self._function_call_in_progress = True + elif isinstance(frame, FunctionCallsStartedFrame): + for f in frame.function_calls: + self._function_call_in_progress.add(f.tool_call_id) should_mute = await self._should_mute() - elif isinstance(frame, FunctionCallResultFrame): - self._function_call_in_progress = False + elif isinstance(frame, (FunctionCallCancelFrame, FunctionCallResultFrame)): + self._function_call_in_progress.remove(frame.tool_call_id) should_mute = await self._should_mute() elif isinstance(frame, BotStartedSpeakingFrame): self._bot_is_speaking = True diff --git a/tests/test_stt_mute_filter.py b/tests/test_stt_mute_filter.py index 2bd041b65..c36693259 100644 --- a/tests/test_stt_mute_filter.py +++ b/tests/test_stt_mute_filter.py @@ -9,8 +9,10 @@ import unittest from pipecat.frames.frames import ( BotStartedSpeakingFrame, BotStoppedSpeakingFrame, + FunctionCallFromLLM, FunctionCallInProgressFrame, FunctionCallResultFrame, + FunctionCallsStartedFrame, InputAudioRawFrame, InterimTranscriptionFrame, TranscriptionFrame, @@ -148,54 +150,59 @@ class TestSTTMuteFilter(unittest.IsolatedAsyncioTestCase): expected_down_frames=expected_returned_frames, ) - # TODO: Revisit once we figure out how to test SystemFrames and DataFrames - # async def test_function_call_strategy(self): - # filter = STTMuteFilter(config=STTMuteConfig(strategies={STTMuteStrategy.FUNCTION_CALL})) + async def test_function_call_strategy(self): + filter = STTMuteFilter(config=STTMuteConfig(strategies={STTMuteStrategy.FUNCTION_CALL})) - # frames_to_send = [ - # VADUserStartedSpeakingFrame(), # Should pass through initially - # UserStartedSpeakingFrame(), # Should pass through initially - # VADUserStoppedSpeakingFrame(), - # UserStoppedSpeakingFrame(), - # FunctionCallInProgressFrame( - # function_name="get_weather", - # tool_call_id="call_123", - # arguments='{"location": "San Francisco"}', - # ), # Start function call - # VADUserStartedSpeakingFrame(), # Should be suppressed - # UserStartedSpeakingFrame(), # Should be suppressed - # VADUserStoppedSpeakingFrame(), # Should be suppressed - # UserStoppedSpeakingFrame(), # Should be suppressed - # FunctionCallResultFrame( - # function_name="get_weather", - # tool_call_id="call_123", - # arguments='{"location": "San Francisco"}', - # result={"temperature": 22}, - # ), # End function call - # VADUserStartedSpeakingFrame(), # Should pass through again - # UserStartedSpeakingFrame(), # Should pass through again - # VADUserStoppedSpeakingFrame(), - # UserStoppedSpeakingFrame(), - # ] + frames_to_send = [ + VADUserStartedSpeakingFrame(), # Should pass through initially + UserStartedSpeakingFrame(), # Should pass through initially + VADUserStoppedSpeakingFrame(), # Should pass through initially + UserStoppedSpeakingFrame(), # Should pass through initially + FunctionCallsStartedFrame( + function_calls=[ + FunctionCallFromLLM( + function_name="get_weather", + tool_call_id="call_123", + arguments='{"location": "San Francisco"}', + context=None, + ) + ] + ), # Start function call + VADUserStartedSpeakingFrame(), # Should be suppressed + UserStartedSpeakingFrame(), # Should be suppressed + VADUserStoppedSpeakingFrame(), # Should be suppressed + UserStoppedSpeakingFrame(), # Should be suppressed + FunctionCallResultFrame( + function_name="get_weather", + tool_call_id="call_123", + arguments='{"location": "San Francisco"}', + result={"temperature": 22}, + ), # End function call + SleepFrame(), + VADUserStartedSpeakingFrame(), # Should pass through again + UserStartedSpeakingFrame(), # Should pass through again + VADUserStoppedSpeakingFrame(), + UserStoppedSpeakingFrame(), + ] - # expected_returned_frames = [ - # VADUserStartedSpeakingFrame, - # UserStartedSpeakingFrame, - # VADUserStoppedSpeakingFrame, - # UserStoppedSpeakingFrame, - # FunctionCallInProgressFrame, - # FunctionCallResultFrame, - # VADUserStartedSpeakingFrame, - # UserStartedSpeakingFrame, - # VADUserStoppedSpeakingFrame, - # UserStoppedSpeakingFrame, - # ] + expected_returned_frames = [ + VADUserStartedSpeakingFrame, + UserStartedSpeakingFrame, + VADUserStoppedSpeakingFrame, + UserStoppedSpeakingFrame, + FunctionCallsStartedFrame, + FunctionCallResultFrame, + VADUserStartedSpeakingFrame, + UserStartedSpeakingFrame, + VADUserStoppedSpeakingFrame, + UserStoppedSpeakingFrame, + ] - # await run_test( - # filter, - # frames_to_send=frames_to_send, - # expected_down_frames=expected_returned_frames, - # ) + await run_test( + filter, + frames_to_send=frames_to_send, + expected_down_frames=expected_returned_frames, + ) async def test_mute_until_first_bot_complete_strategy(self): filter = STTMuteFilter( From c7589663b51cc5287c64a962e0f82ba0d20b29fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Sat, 27 Dec 2025 13:47:29 -0800 Subject: [PATCH 10/11] deprecate STTMuteFilter in favor of LLMUSerAggregator user mute strategies --- changelog/3292.deprecated.md | 1 + .../processors/filters/stt_mute_filter.py | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 changelog/3292.deprecated.md diff --git a/changelog/3292.deprecated.md b/changelog/3292.deprecated.md new file mode 100644 index 000000000..3aceea5f1 --- /dev/null +++ b/changelog/3292.deprecated.md @@ -0,0 +1 @@ +- `STTMuteFilter` is deprecated and will be removed in a future version. Use `LLMUserAggregator`'s new `user_mute_strategies` instead. diff --git a/src/pipecat/processors/filters/stt_mute_filter.py b/src/pipecat/processors/filters/stt_mute_filter.py index 8a0dca17d..e5a21f6db 100644 --- a/src/pipecat/processors/filters/stt_mute_filter.py +++ b/src/pipecat/processors/filters/stt_mute_filter.py @@ -51,6 +51,10 @@ class STTMuteStrategy(Enum): FUNCTION_CALL: Mute STT during function calls to prevent interruptions. ALWAYS: Always mute STT when the bot is speaking. CUSTOM: Use a custom callback to determine muting logic dynamically. + + .. deprecated:: 0.0.99 + `STTMuteStrategy` is deprecated and will be removed in a future version. + Use `LLMUserAggregator`'s new `user_mute_strategies` instead. """ FIRST_SPEECH = "first_speech" @@ -77,6 +81,10 @@ class STTMuteConfig: Note: MUTE_UNTIL_FIRST_BOT_COMPLETE and FIRST_SPEECH strategies should not be used together as they handle the first bot speech differently. + + .. deprecated:: 0.0.99 + `STTMuteConfig` is deprecated and will be removed in a future version. + Use `LLMUserAggregator`'s new `user_mute_strategies` instead. """ strategies: set[STTMuteStrategy] @@ -104,6 +112,10 @@ class STTMuteFilter(FrameProcessor): feature. When STT is muted, interruptions are automatically disabled by suppressing VAD-related frames. This prevents unwanted speech detection during bot speech, function calls, or other specified conditions. + + .. deprecated:: 0.0.99 + `STTMuteFilter` is deprecated and will be removed in a future version. + Use `LLMUserAggregator`'s new `user_mute_strategies` instead. """ def __init__(self, *, config: STTMuteConfig, **kwargs): @@ -120,6 +132,16 @@ class STTMuteFilter(FrameProcessor): self._function_call_in_progress = set() self._is_muted = False + import warnings + + with warnings.catch_warnings(): + warnings.simplefilter("always") + warnings.warn( + "`STTMuteFilter` is deprecated and will be removed in a future version. " + "Use `LLMUserAggregator`'s new `user_mute_strategies` instead.", + DeprecationWarning, + ) + async def _handle_mute_state(self, should_mute: bool): """Handle STT muting and interruption control state changes.""" if should_mute != self._is_muted: From 094d9fd7d764d24aa8b7fb470f6bfb171c7d108c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleix=20Conchillo=20Flaqu=C3=A9?= Date: Sat, 27 Dec 2025 13:59:59 -0800 Subject: [PATCH 11/11] turns(mute): make strategies available in __init__ --- examples/foundational/24-user-mute-strategy.py | 4 ++-- src/pipecat/turns/mute/__init__.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/examples/foundational/24-user-mute-strategy.py b/examples/foundational/24-user-mute-strategy.py index 097338dac..635f3e788 100644 --- a/examples/foundational/24-user-mute-strategy.py +++ b/examples/foundational/24-user-mute-strategy.py @@ -35,8 +35,8 @@ 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.bot.turn_analyzer_bot_turn_start_strategy import TurnAnalyzerBotTurnStartStrategy -from pipecat.turns.mute.function_call_user_mute_strategy import FunctionCallUserMuteStrategy -from pipecat.turns.mute.mute_until_first_bot_complete_user_mute_strategy import ( +from pipecat.turns.mute import ( + FunctionCallUserMuteStrategy, MuteUntilFirstBotCompleteUserMuteStrategy, ) from pipecat.turns.turn_start_strategies import TurnStartStrategies diff --git a/src/pipecat/turns/mute/__init__.py b/src/pipecat/turns/mute/__init__.py index e69de29bb..bd226815a 100644 --- a/src/pipecat/turns/mute/__init__.py +++ b/src/pipecat/turns/mute/__init__.py @@ -0,0 +1,13 @@ +# +# Copyright (c) 2024–2025, Daily +# +# SPDX-License-Identifier: BSD 2-Clause License +# + +from pipecat.turns.mute.always_user_mute_strategy import AlwaysUserMuteStrategy +from pipecat.turns.mute.base_user_mute_strategy import BaseUserMuteStrategy +from pipecat.turns.mute.first_speech_user_mute_strategy import FirstSpeechUserMuteStrategy +from pipecat.turns.mute.function_call_user_mute_strategy import FunctionCallUserMuteStrategy +from pipecat.turns.mute.mute_until_first_bot_complete_user_mute_strategy import ( + MuteUntilFirstBotCompleteUserMuteStrategy, +)