diff --git a/CHANGELOG.md b/CHANGELOG.md index b4d7d1f09..a16af9013 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added foundational example `31-user-idle.py` demonstrating how to use - `UserIdleProcessor` for handling user inactivity. - - Introduced pipeline frame observers. Observers can view all the frames that go through the pipeline without the need to inject processors in the pipeline. This can be useful, for example, to implement frame loggers or @@ -59,6 +56,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Updated the `17-detect-user-idle.py` to show how to use the `retry_count`. + - Enhanced `UserIdleProcessor` with retry counting functionality. Callbacks now support an optional `retry_count` parameter to implement escalating responses to user inactivity. diff --git a/examples/foundational/17-detect-user-idle.py b/examples/foundational/17-detect-user-idle.py index aa94d2b60..21d9d5941 100644 --- a/examples/foundational/17-detect-user-idle.py +++ b/examples/foundational/17-detect-user-idle.py @@ -14,7 +14,7 @@ from loguru import logger from runner import configure from pipecat.audio.vad.silero import SileroVADAnalyzer -from pipecat.frames.frames import LLMMessagesFrame +from pipecat.frames.frames import EndFrame, LLMMessagesFrame, TTSSpeakFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.runner import PipelineRunner from pipecat.pipeline.task import PipelineParams, PipelineTask @@ -63,16 +63,36 @@ async def main(): context = OpenAILLMContext(messages) context_aggregator = llm.create_context_aggregator(context) - async def user_idle_callback(user_idle: UserIdleProcessor): - messages.append( - { - "role": "system", - "content": "Ask the user if they are still there and try to prompt for some input, but be short.", - } - ) - await user_idle.push_frame(LLMMessagesFrame(messages)) + async def handle_user_idle(processor: UserIdleProcessor, retry_count: int) -> bool: + if retry_count == 1: + # First attempt: Add a gentle prompt to the conversation + messages.append( + { + "role": "system", + "content": "The user has been quiet for a while. Politely and concisely ask if they're still there.", + } + ) + await user_idle.push_frame(LLMMessagesFrame(messages)) + return True + elif retry_count == 2: + # Second attempt: More direct prompt + messages.append( + { + "role": "system", + "content": "The user is still inactive. Concisely ask if they would like to continue the conversation.", + } + ) + await user_idle.push_frame(LLMMessagesFrame(messages)) + return True + else: + # Third attempt: End the conversation + await user_idle.push_frame( + TTSSpeakFrame("It seems like you're busy right now. Have a nice day!") + ) + await task.queue_frame(EndFrame()) + return False - user_idle = UserIdleProcessor(callback=user_idle_callback, timeout=5.0) + user_idle = UserIdleProcessor(callback=handle_user_idle, timeout=5.0) pipeline = Pipeline( [ @@ -102,6 +122,10 @@ async def main(): messages.append({"role": "system", "content": "Please introduce yourself to the user."}) await task.queue_frames([context_aggregator.user().get_context_frame()]) + @transport.event_handler("on_participant_left") + async def on_participant_left(transport, participant, reason): + await task.queue_frame(EndFrame()) + runner = PipelineRunner() await runner.run(task) diff --git a/examples/foundational/31-user-idle.py b/examples/foundational/31-user-idle.py deleted file mode 100644 index c8beb4b71..000000000 --- a/examples/foundational/31-user-idle.py +++ /dev/null @@ -1,145 +0,0 @@ -# -# Copyright (c) 2024–2025, Daily -# -# SPDX-License-Identifier: BSD 2-Clause License -# - -import asyncio -import os -import sys - -import aiohttp -from dotenv import load_dotenv -from loguru import logger -from runner import configure - -from pipecat.audio.vad.silero import SileroVADAnalyzer -from pipecat.frames.frames import EndFrame, TTSSpeakFrame -from pipecat.pipeline.pipeline import Pipeline -from pipecat.pipeline.runner import PipelineRunner -from pipecat.pipeline.task import PipelineParams, PipelineTask -from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext -from pipecat.processors.user_idle_processor import UserIdleProcessor -from pipecat.services.cartesia import CartesiaTTSService -from pipecat.services.openai import OpenAILLMService -from pipecat.transports.services.daily import DailyParams, DailyTransport - -load_dotenv(override=True) - -logger.remove(0) -logger.add(sys.stderr, level="DEBUG") - - -# Example callback using the new style with retry control -async def handle_user_idle(processor: UserIdleProcessor, retry_count: int) -> bool: - if retry_count == 1: - # First attempt: Add a gentle prompt to the conversation - messages.append( - { - "role": "system", - "content": "The user has been quiet for a while. Politely and concisely ask if they're still there.", - } - ) - await task.queue_frames([context_aggregator.user().get_context_frame()]) - return True - elif retry_count == 2: - # Second attempt: More direct prompt - messages.append( - { - "role": "system", - "content": "The user is still inactive. Concisely ask if they would like to continue the conversation.", - } - ) - await task.queue_frames([context_aggregator.user().get_context_frame()]) - return True - else: - # Third attempt: End the conversation - await task.queue_frames( - [TTSSpeakFrame("It seems like you're busy right now. Have a nice day!")] - ) - await asyncio.sleep(3) - await task.queue_frame(EndFrame()) - return False - - -async def main(): - global task, messages, context_aggregator # Make these accessible to the idle handler - - async with aiohttp.ClientSession() as session: - (room_url, token) = await configure(session) - - transport = DailyTransport( - room_url, - token, - "Respond bot", - DailyParams( - audio_out_enabled=True, - transcription_enabled=True, - vad_enabled=True, - vad_analyzer=SileroVADAnalyzer(), - ), - ) - - tts = CartesiaTTSService( - api_key=os.getenv("CARTESIA_API_KEY"), - voice_id="79a125e8-cd45-4c13-8a67-188112f4dd22", # British Lady - ) - - llm = OpenAILLMService(api_key=os.getenv("OPENAI_API_KEY"), model="gpt-4") - - messages = [ - { - "role": "system", - "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way.", - }, - ] - - context = OpenAILLMContext(messages) - context_aggregator = llm.create_context_aggregator(context) - - # Create the idle processor - idle_processor = UserIdleProcessor( - callback=handle_user_idle, - timeout=5.0, # 5 seconds of inactivity before triggering - ) - - pipeline = Pipeline( - [ - transport.input(), # Transport user input - idle_processor, # Add the idle processor - context_aggregator.user(), # User responses - llm, # LLM - tts, # TTS - transport.output(), # Transport bot output - context_aggregator.assistant(), # Assistant spoken responses - ] - ) - - task = PipelineTask( - pipeline, - PipelineParams( - allow_interruptions=True, - enable_metrics=True, - enable_usage_metrics=True, - report_only_initial_ttfb=True, - ), - ) - - @transport.event_handler("on_first_participant_joined") - async def on_first_participant_joined(transport, participant): - await transport.capture_participant_transcription(participant["id"]) - # Kick off the conversation. - messages.append({"role": "system", "content": "Please introduce yourself to the user."}) - await task.queue_frames([context_aggregator.user().get_context_frame()]) - - @transport.event_handler("on_participant_left") - async def on_participant_left(transport, participant, reason): - await task.queue_frame(EndFrame()) - - runner = PipelineRunner() - - await runner.run(task) - - -if __name__ == "__main__": - asyncio.run(main())