From 1e1160906efe998845b43b02c5553a0986b1efed Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Sat, 17 Jan 2026 06:56:08 -0500 Subject: [PATCH] Update on_user_idle to on_user_turn_idle --- changelog/3482.added.md | 2 +- examples/foundational/17-detect-user-idle.py | 7 ++--- .../aggregators/llm_response_universal.py | 18 ++++++------ src/pipecat/turns/user_idle_controller.py | 12 ++++---- src/pipecat/turns/user_turn_processor.py | 18 ++++++------ tests/test_user_idle_controller.py | 28 +++++++++---------- 6 files changed, 44 insertions(+), 41 deletions(-) diff --git a/changelog/3482.added.md b/changelog/3482.added.md index 35522f36c..465d409f4 100644 --- a/changelog/3482.added.md +++ b/changelog/3482.added.md @@ -1 +1 @@ -- Added `UserIdleController` for detecting user idle state, integrated into `LLMUserAggregator` and `UserTurnProcessor` via optional `user_idle_timeout` parameter. Emits `on_user_idle` event for application-level handling. Deprecated `UserIdleProcessor` in favor of the new compositional approach. +- Added `UserIdleController` for detecting user idle state, integrated into `LLMUserAggregator` and `UserTurnProcessor` via optional `user_idle_timeout` parameter. Emits `on_user_turn_idle` event for application-level handling. Deprecated `UserIdleProcessor` in favor of the new compositional approach. diff --git a/examples/foundational/17-detect-user-idle.py b/examples/foundational/17-detect-user-idle.py index b4042c1d1..52277a584 100644 --- a/examples/foundational/17-detect-user-idle.py +++ b/examples/foundational/17-detect-user-idle.py @@ -14,7 +14,6 @@ from pipecat.audio.turn.smart_turn.local_smart_turn_v3 import LocalSmartTurnAnal from pipecat.audio.vad.silero import SileroVADAnalyzer from pipecat.audio.vad.vad_analyzer import VADParams from pipecat.frames.frames import ( - EndFrame, EndTaskFrame, LLMMessagesAppendFrame, LLMRunFrame, @@ -155,12 +154,12 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments): # Set up idle handling with retry logic idle_handler = IdleHandler() - @user_aggregator.event_handler("on_user_idle") - async def handle_user_idle(aggregator): + @user_aggregator.event_handler("on_user_turn_idle") + async def on_user_turn_idle(aggregator): await idle_handler.handle_idle(aggregator) @user_aggregator.event_handler("on_user_turn_started") - async def handle_user_turn_started(aggregator, strategy): + async def on_user_turn_started(aggregator, strategy): idle_handler.reset() @transport.event_handler("on_client_connected") diff --git a/src/pipecat/processors/aggregators/llm_response_universal.py b/src/pipecat/processors/aggregators/llm_response_universal.py index 4b0e89008..eeccd18af 100644 --- a/src/pipecat/processors/aggregators/llm_response_universal.py +++ b/src/pipecat/processors/aggregators/llm_response_universal.py @@ -82,7 +82,7 @@ class LLMUserAggregatorParams: user_turn_stop_timeout: Time in seconds to wait before considering the user's turn finished. user_idle_timeout: Optional timeout in seconds for detecting user idle state. - If set, the aggregator will emit an `on_user_idle` event when the user + If set, the aggregator will emit an `on_user_turn_idle` event when the user has been idle (not speaking) for this duration. Set to None to disable idle detection. """ @@ -297,7 +297,7 @@ class LLMUserAggregator(LLMContextAggregator): - on_user_turn_started: Called when the user turn starts - on_user_turn_stopped: Called when the user turn ends - on_user_turn_stop_timeout: Called when no user turn stop strategy triggers - - on_user_idle: Called when the user has been idle for the configured timeout + - on_user_turn_idle: Called when the user has been idle for the configured timeout Example:: @@ -313,8 +313,8 @@ class LLMUserAggregator(LLMContextAggregator): async def on_user_turn_stop_timeout(aggregator): ... - @aggregator.event_handler("on_user_idle") - async def on_user_idle(aggregator): + @aggregator.event_handler("on_user_turn_idle") + async def on_user_turn_idle(aggregator): ... """ @@ -339,7 +339,7 @@ class LLMUserAggregator(LLMContextAggregator): self._register_event_handler("on_user_turn_started") self._register_event_handler("on_user_turn_stopped") self._register_event_handler("on_user_turn_stop_timeout") - self._register_event_handler("on_user_idle") + self._register_event_handler("on_user_turn_idle") user_turn_strategies = self._params.user_turn_strategies or UserTurnStrategies() @@ -368,7 +368,9 @@ class LLMUserAggregator(LLMContextAggregator): self._user_idle_controller = UserIdleController( user_idle_timeout=self._params.user_idle_timeout ) - self._user_idle_controller.add_event_handler("on_user_idle", self._on_user_idle) + self._user_idle_controller.add_event_handler( + "on_user_turn_idle", self._on_user_turn_idle + ) async def cleanup(self): """Clean up processor resources.""" @@ -594,8 +596,8 @@ class LLMUserAggregator(LLMContextAggregator): async def _on_user_turn_stop_timeout(self, controller): await self._call_event_handler("on_user_turn_stop_timeout") - async def _on_user_idle(self, controller): - await self._call_event_handler("on_user_idle") + async def _on_user_turn_idle(self, controller): + await self._call_event_handler("on_user_turn_idle") class LLMAssistantAggregator(LLMContextAggregator): diff --git a/src/pipecat/turns/user_idle_controller.py b/src/pipecat/turns/user_idle_controller.py index 4157d6d30..cce35b9cb 100644 --- a/src/pipecat/turns/user_idle_controller.py +++ b/src/pipecat/turns/user_idle_controller.py @@ -36,12 +36,12 @@ class UserIdleController(BaseObject): Event handlers available: - - on_user_idle: Emitted when the user has been idle for the timeout period. + - on_user_turn_idle: Emitted when the user has been idle for the timeout period. Example:: - @controller.event_handler("on_user_idle") - async def on_user_idle(controller): + @controller.event_handler("on_user_turn_idle") + async def on_user_turn_idle(controller): # Handle user idle - send reminder, prompt, etc. ... """ @@ -68,7 +68,7 @@ class UserIdleController(BaseObject): self.user_idle_event = asyncio.Event() self.user_idle_task: Optional[asyncio.Task] = None - self._register_event_handler("on_user_idle", sync=True) + self._register_event_handler("on_user_turn_idle", sync=True) @property def task_manager(self) -> BaseTaskManager: @@ -161,7 +161,7 @@ class UserIdleController(BaseObject): frames are received (UserSpeakingFrame or BotSpeakingFrame). Function calls are tracked via state since they only send start/end frames. If no activity is detected for the configured timeout period and no function call is in - progress, the on_user_idle event is triggered. + progress, the on_user_turn_idle event is triggered. """ while True: try: @@ -170,4 +170,4 @@ class UserIdleController(BaseObject): except asyncio.TimeoutError: # Only trigger if conversation has started and no function call is in progress if self._conversation_started and not self._function_call_in_progress: - await self._call_event_handler("on_user_idle") + await self._call_event_handler("on_user_turn_idle") diff --git a/src/pipecat/turns/user_turn_processor.py b/src/pipecat/turns/user_turn_processor.py index 62693c07d..6c771811e 100644 --- a/src/pipecat/turns/user_turn_processor.py +++ b/src/pipecat/turns/user_turn_processor.py @@ -39,7 +39,7 @@ class UserTurnProcessor(FrameProcessor): - on_user_turn_started: Emitted when a user turn starts. - on_user_turn_stopped: Emitted when a user turn stops. - on_user_turn_stop_timeout: Emitted if no stop strategy triggers before timeout. - - on_user_idle: Emitted when the user has been idle for the configured timeout. + - on_user_turn_idle: Emitted when the user has been idle for the configured timeout. Example:: @@ -55,8 +55,8 @@ class UserTurnProcessor(FrameProcessor): async def on_user_turn_stop_timeout(processor): ... - @processor.event_handler("on_user_idle") - async def on_user_idle(processor): + @processor.event_handler("on_user_turn_idle") + async def on_user_turn_idle(processor): ... """ @@ -76,7 +76,7 @@ class UserTurnProcessor(FrameProcessor): user_turn_stop_timeout: Timeout in seconds to automatically stop a user turn if no activity is detected. user_idle_timeout: Optional timeout in seconds for detecting user idle state. - If set, the processor will emit an `on_user_idle` event when the user + If set, the processor will emit an `on_user_turn_idle` event when the user has been idle (not speaking) for this duration. Set to None to disable idle detection. **kwargs: Additional keyword arguments. @@ -86,7 +86,7 @@ class UserTurnProcessor(FrameProcessor): self._register_event_handler("on_user_turn_started") self._register_event_handler("on_user_turn_stopped") self._register_event_handler("on_user_turn_stop_timeout") - self._register_event_handler("on_user_idle") + self._register_event_handler("on_user_turn_idle") self._user_turn_controller = UserTurnController( user_turn_strategies=user_turn_strategies or UserTurnStrategies(), @@ -108,7 +108,9 @@ class UserTurnProcessor(FrameProcessor): self._user_idle_controller: Optional[UserIdleController] = None if user_idle_timeout: self._user_idle_controller = UserIdleController(user_idle_timeout=user_idle_timeout) - self._user_idle_controller.add_event_handler("on_user_idle", self._on_user_idle) + self._user_idle_controller.add_event_handler( + "on_user_turn_idle", self._on_user_turn_idle + ) async def cleanup(self): """Clean up processor resources.""" @@ -208,5 +210,5 @@ class UserTurnProcessor(FrameProcessor): async def _on_user_turn_stop_timeout(self, controller): await self._call_event_handler("on_user_turn_stop_timeout") - async def _on_user_idle(self, controller): - await self._call_event_handler("on_user_idle") + async def _on_user_turn_idle(self, controller): + await self._call_event_handler("on_user_turn_idle") diff --git a/tests/test_user_idle_controller.py b/tests/test_user_idle_controller.py index 568819eb8..5de665353 100644 --- a/tests/test_user_idle_controller.py +++ b/tests/test_user_idle_controller.py @@ -32,8 +32,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase): idle_triggered = False - @controller.event_handler("on_user_idle") - async def on_user_idle(controller): + @controller.event_handler("on_user_turn_idle") + async def on_user_turn_idle(controller): nonlocal idle_triggered idle_triggered = True @@ -54,8 +54,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase): idle_triggered = False - @controller.event_handler("on_user_idle") - async def on_user_idle(controller): + @controller.event_handler("on_user_turn_idle") + async def on_user_turn_idle(controller): nonlocal idle_triggered idle_triggered = True @@ -78,8 +78,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase): idle_triggered = False - @controller.event_handler("on_user_idle") - async def on_user_idle(controller): + @controller.event_handler("on_user_turn_idle") + async def on_user_turn_idle(controller): nonlocal idle_triggered idle_triggered = True @@ -102,8 +102,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase): idle_triggered = False - @controller.event_handler("on_user_idle") - async def on_user_idle(controller): + @controller.event_handler("on_user_turn_idle") + async def on_user_turn_idle(controller): nonlocal idle_triggered idle_triggered = True @@ -143,8 +143,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase): idle_triggered = False - @controller.event_handler("on_user_idle") - async def on_user_idle(controller): + @controller.event_handler("on_user_turn_idle") + async def on_user_turn_idle(controller): nonlocal idle_triggered idle_triggered = True @@ -162,8 +162,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase): idle_triggered = False - @controller.event_handler("on_user_idle") - async def on_user_idle(controller): + @controller.event_handler("on_user_turn_idle") + async def on_user_turn_idle(controller): nonlocal idle_triggered idle_triggered = True @@ -184,8 +184,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase): idle_count = 0 - @controller.event_handler("on_user_idle") - async def on_user_idle(controller): + @controller.event_handler("on_user_turn_idle") + async def on_user_turn_idle(controller): nonlocal idle_count idle_count += 1