Skip to main content

Overview

In conversational applications, it’s important to handle situations where users go silent or inactive. Pipecat provides built-in idle detection through LLMUserAggregator and UserTurnProcessor, allowing your bot to respond appropriately when users haven’t spoken for a defined period.

How It Works

Idle detection monitors user activity and:
  1. Starts a timer when the bot finishes speaking (BotStoppedSpeakingFrame)
  2. Cancels the timer when the user or bot starts speaking
  3. Suppresses the timer during function calls and active user turns (to avoid false triggers during interruptions)
  4. Emits an on_user_turn_idle event when the timer expires
  5. Allows you to implement escalating responses or gracefully end the conversation in your application code

Basic Implementation

Step 1: Enable Idle Detection

Enable idle detection by setting the user_idle_timeout parameter when creating your aggregator:
from pipecat.processors.aggregators.llm_response_universal import (
    LLMContextAggregatorPair,
    LLMUserAggregatorParams,
)

user_aggregator, assistant_aggregator = LLMContextAggregatorPair(
    context,
    user_params=LLMUserAggregatorParams(
        user_idle_timeout=5.0,  # Detect idle after 5 seconds
    ),
)

Step 2: Handle Idle Events

Create an event handler to respond when the user becomes idle:
@user_aggregator.event_handler("on_user_turn_idle")
async def on_user_turn_idle(aggregator):
    # Send a reminder to the user
    message = {
        "role": "system",
        "content": "The user has been quiet. Politely ask if they're still there.",
    }
    await aggregator.push_frame(LLMMessagesAppendFrame([message], run_llm=True))

Step 3: Implement Retry Logic (Optional)

For escalating responses, track retry count in your application:
class IdleHandler:
    def __init__(self):
        self._retry_count = 0

    def reset(self):
        self._retry_count = 0

    async def handle_idle(self, aggregator):
        self._retry_count += 1

        if self._retry_count == 1:
            # First attempt - gentle reminder
            message = {
                "role": "system",
                "content": "The user has been quiet. Politely ask if they're still there.",
            }
            await aggregator.push_frame(LLMMessagesAppendFrame([message], run_llm=True))
        elif self._retry_count == 2:
            # Second attempt - more direct
            message = {
                "role": "system",
                "content": "The user is still inactive. Ask if they'd like to continue.",
            }
            await aggregator.push_frame(LLMMessagesAppendFrame([message], run_llm=True))
        else:
            # Third attempt - end conversation
            await aggregator.push_frame(
                TTSSpeakFrame("It seems like you're busy. Have a nice day!")
            )
            await aggregator.push_frame(EndTaskFrame(), FrameDirection.UPSTREAM)

# Use the handler
idle_handler = IdleHandler()

@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 on_user_turn_started(aggregator, strategy):
    idle_handler.reset()  # Reset retry count when user speaks

Updating Timeout at Runtime

You can enable, disable, or change the idle timeout at runtime by pushing a UserIdleTimeoutUpdateFrame:
from pipecat.frames.frames import UserIdleTimeoutUpdateFrame

# Enable idle detection (or change timeout)
await task.queue_frame(UserIdleTimeoutUpdateFrame(timeout=10.0))

# Disable idle detection
await task.queue_frame(UserIdleTimeoutUpdateFrame(timeout=0))
This is useful when you want to enable idle detection only at certain points in the conversation, or adjust the timeout based on context.

Best Practices

  • Set appropriate timeouts: Shorter timeouts (5-10 seconds) work well for voice conversations
  • Use escalating responses: Start with gentle reminders and gradually become more direct
  • Limit retry attempts: After 2-3 unsuccessful attempts, consider ending the conversation gracefully by pushing an EndTaskFrame
  • Reset on user activity: Use the on_user_turn_started event to reset your retry counter when the user speaks
  • Let the LLM respond naturally: Use system messages to prompt the LLM rather than hardcoded TTS responses for more natural interactions

Next Steps

Implementing idle user detection improves the conversational experience by ensuring your bot can handle periods of user inactivity gracefully, either by prompting for re-engagement or politely ending the conversation when appropriate.