Update on_user_idle to on_user_turn_idle

This commit is contained in:
Mark Backman
2026-01-17 06:56:08 -05:00
parent 473d39791b
commit 1e1160906e
6 changed files with 44 additions and 41 deletions

View File

@@ -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.

View File

@@ -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.silero import SileroVADAnalyzer
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 (
EndFrame,
EndTaskFrame, EndTaskFrame,
LLMMessagesAppendFrame, LLMMessagesAppendFrame,
LLMRunFrame, LLMRunFrame,
@@ -155,12 +154,12 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
# Set up idle handling with retry logic # Set up idle handling with retry logic
idle_handler = IdleHandler() idle_handler = IdleHandler()
@user_aggregator.event_handler("on_user_idle") @user_aggregator.event_handler("on_user_turn_idle")
async def handle_user_idle(aggregator): async def on_user_turn_idle(aggregator):
await idle_handler.handle_idle(aggregator) await idle_handler.handle_idle(aggregator)
@user_aggregator.event_handler("on_user_turn_started") @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() idle_handler.reset()
@transport.event_handler("on_client_connected") @transport.event_handler("on_client_connected")

View File

@@ -82,7 +82,7 @@ class LLMUserAggregatorParams:
user_turn_stop_timeout: Time in seconds to wait before considering the user_turn_stop_timeout: Time in seconds to wait before considering the
user's turn finished. user's turn finished.
user_idle_timeout: Optional timeout in seconds for detecting user idle state. 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 has been idle (not speaking) for this duration. Set to None to disable
idle detection. idle detection.
""" """
@@ -297,7 +297,7 @@ class LLMUserAggregator(LLMContextAggregator):
- on_user_turn_started: Called when the user turn starts - on_user_turn_started: Called when the user turn starts
- on_user_turn_stopped: Called when the user turn ends - 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_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:: Example::
@@ -313,8 +313,8 @@ class LLMUserAggregator(LLMContextAggregator):
async def on_user_turn_stop_timeout(aggregator): async def on_user_turn_stop_timeout(aggregator):
... ...
@aggregator.event_handler("on_user_idle") @aggregator.event_handler("on_user_turn_idle")
async def on_user_idle(aggregator): 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_started")
self._register_event_handler("on_user_turn_stopped") self._register_event_handler("on_user_turn_stopped")
self._register_event_handler("on_user_turn_stop_timeout") 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() user_turn_strategies = self._params.user_turn_strategies or UserTurnStrategies()
@@ -368,7 +368,9 @@ class LLMUserAggregator(LLMContextAggregator):
self._user_idle_controller = UserIdleController( self._user_idle_controller = UserIdleController(
user_idle_timeout=self._params.user_idle_timeout 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): async def cleanup(self):
"""Clean up processor resources.""" """Clean up processor resources."""
@@ -594,8 +596,8 @@ class LLMUserAggregator(LLMContextAggregator):
async def _on_user_turn_stop_timeout(self, controller): async def _on_user_turn_stop_timeout(self, controller):
await self._call_event_handler("on_user_turn_stop_timeout") await self._call_event_handler("on_user_turn_stop_timeout")
async def _on_user_idle(self, controller): async def _on_user_turn_idle(self, controller):
await self._call_event_handler("on_user_idle") await self._call_event_handler("on_user_turn_idle")
class LLMAssistantAggregator(LLMContextAggregator): class LLMAssistantAggregator(LLMContextAggregator):

View File

@@ -36,12 +36,12 @@ class UserIdleController(BaseObject):
Event handlers available: 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:: Example::
@controller.event_handler("on_user_idle") @controller.event_handler("on_user_turn_idle")
async def on_user_idle(controller): async def on_user_turn_idle(controller):
# Handle user idle - send reminder, prompt, etc. # Handle user idle - send reminder, prompt, etc.
... ...
""" """
@@ -68,7 +68,7 @@ class UserIdleController(BaseObject):
self.user_idle_event = asyncio.Event() self.user_idle_event = asyncio.Event()
self.user_idle_task: Optional[asyncio.Task] = None 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 @property
def task_manager(self) -> BaseTaskManager: def task_manager(self) -> BaseTaskManager:
@@ -161,7 +161,7 @@ class UserIdleController(BaseObject):
frames are received (UserSpeakingFrame or BotSpeakingFrame). Function calls frames are received (UserSpeakingFrame or BotSpeakingFrame). Function calls
are tracked via state since they only send start/end frames. If no activity 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 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: while True:
try: try:
@@ -170,4 +170,4 @@ class UserIdleController(BaseObject):
except asyncio.TimeoutError: except asyncio.TimeoutError:
# Only trigger if conversation has started and no function call is in progress # Only trigger if conversation has started and no function call is in progress
if self._conversation_started and not self._function_call_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")

View File

@@ -39,7 +39,7 @@ class UserTurnProcessor(FrameProcessor):
- on_user_turn_started: Emitted when a user turn starts. - on_user_turn_started: Emitted when a user turn starts.
- on_user_turn_stopped: Emitted when a user turn stops. - 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_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:: Example::
@@ -55,8 +55,8 @@ class UserTurnProcessor(FrameProcessor):
async def on_user_turn_stop_timeout(processor): async def on_user_turn_stop_timeout(processor):
... ...
@processor.event_handler("on_user_idle") @processor.event_handler("on_user_turn_idle")
async def on_user_idle(processor): 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 user_turn_stop_timeout: Timeout in seconds to automatically stop a user turn
if no activity is detected. if no activity is detected.
user_idle_timeout: Optional timeout in seconds for detecting user idle state. 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 has been idle (not speaking) for this duration. Set to None to disable
idle detection. idle detection.
**kwargs: Additional keyword arguments. **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_started")
self._register_event_handler("on_user_turn_stopped") self._register_event_handler("on_user_turn_stopped")
self._register_event_handler("on_user_turn_stop_timeout") 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( self._user_turn_controller = UserTurnController(
user_turn_strategies=user_turn_strategies or UserTurnStrategies(), user_turn_strategies=user_turn_strategies or UserTurnStrategies(),
@@ -108,7 +108,9 @@ class UserTurnProcessor(FrameProcessor):
self._user_idle_controller: Optional[UserIdleController] = None self._user_idle_controller: Optional[UserIdleController] = None
if user_idle_timeout: if user_idle_timeout:
self._user_idle_controller = UserIdleController(user_idle_timeout=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): async def cleanup(self):
"""Clean up processor resources.""" """Clean up processor resources."""
@@ -208,5 +210,5 @@ class UserTurnProcessor(FrameProcessor):
async def _on_user_turn_stop_timeout(self, controller): async def _on_user_turn_stop_timeout(self, controller):
await self._call_event_handler("on_user_turn_stop_timeout") await self._call_event_handler("on_user_turn_stop_timeout")
async def _on_user_idle(self, controller): async def _on_user_turn_idle(self, controller):
await self._call_event_handler("on_user_idle") await self._call_event_handler("on_user_turn_idle")

View File

@@ -32,8 +32,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase):
idle_triggered = False idle_triggered = False
@controller.event_handler("on_user_idle") @controller.event_handler("on_user_turn_idle")
async def on_user_idle(controller): async def on_user_turn_idle(controller):
nonlocal idle_triggered nonlocal idle_triggered
idle_triggered = True idle_triggered = True
@@ -54,8 +54,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase):
idle_triggered = False idle_triggered = False
@controller.event_handler("on_user_idle") @controller.event_handler("on_user_turn_idle")
async def on_user_idle(controller): async def on_user_turn_idle(controller):
nonlocal idle_triggered nonlocal idle_triggered
idle_triggered = True idle_triggered = True
@@ -78,8 +78,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase):
idle_triggered = False idle_triggered = False
@controller.event_handler("on_user_idle") @controller.event_handler("on_user_turn_idle")
async def on_user_idle(controller): async def on_user_turn_idle(controller):
nonlocal idle_triggered nonlocal idle_triggered
idle_triggered = True idle_triggered = True
@@ -102,8 +102,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase):
idle_triggered = False idle_triggered = False
@controller.event_handler("on_user_idle") @controller.event_handler("on_user_turn_idle")
async def on_user_idle(controller): async def on_user_turn_idle(controller):
nonlocal idle_triggered nonlocal idle_triggered
idle_triggered = True idle_triggered = True
@@ -143,8 +143,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase):
idle_triggered = False idle_triggered = False
@controller.event_handler("on_user_idle") @controller.event_handler("on_user_turn_idle")
async def on_user_idle(controller): async def on_user_turn_idle(controller):
nonlocal idle_triggered nonlocal idle_triggered
idle_triggered = True idle_triggered = True
@@ -162,8 +162,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase):
idle_triggered = False idle_triggered = False
@controller.event_handler("on_user_idle") @controller.event_handler("on_user_turn_idle")
async def on_user_idle(controller): async def on_user_turn_idle(controller):
nonlocal idle_triggered nonlocal idle_triggered
idle_triggered = True idle_triggered = True
@@ -184,8 +184,8 @@ class TestUserIdleController(unittest.IsolatedAsyncioTestCase):
idle_count = 0 idle_count = 0
@controller.event_handler("on_user_idle") @controller.event_handler("on_user_turn_idle")
async def on_user_idle(controller): async def on_user_turn_idle(controller):
nonlocal idle_count nonlocal idle_count
idle_count += 1 idle_count += 1