From 7a55d2d7db1fd2a2a9090ef6ab5b2870a0753df0 Mon Sep 17 00:00:00 2001 From: Mark Backman Date: Fri, 15 Nov 2024 16:11:35 -0500 Subject: [PATCH] Add end session handler and update example --- examples/foundational/25-conversation-flow.py | 85 +++++++++++++++++-- src/pipecat/flows/manager.py | 29 ++++++- 2 files changed, 103 insertions(+), 11 deletions(-) diff --git a/examples/foundational/25-conversation-flow.py b/examples/foundational/25-conversation-flow.py index 84434d0a2..e9e22233e 100644 --- a/examples/foundational/25-conversation-flow.py +++ b/examples/foundational/25-conversation-flow.py @@ -28,7 +28,35 @@ load_dotenv(override=True) logger.remove(0) logger.add(sys.stderr, level="DEBUG") -# Define our conversation flow +# Flow Configuration +# +# This configuration defines a simple food ordering system with the following states: +# +# 1. start +# - Initial state where user chooses between pizza or sushi +# - Functions: choose_pizza, choose_sushi +# - Transitions to: choose_pizza or choose_sushi +# +# 2. choose_pizza +# - Handles pizza size selection and order confirmation +# - Functions: +# * select_pizza_size (terminal function, can be called multiple times) +# * end (transitions to end node after order confirmation) +# - Pre-action: Immediate TTS acknowledgment +# +# 3. choose_sushi +# - Handles sushi roll count selection and order confirmation +# - Functions: +# * select_roll_count (terminal function, can be called multiple times) +# * end (transitions to end node after order confirmation) +# - Pre-action: Immediate TTS acknowledgment +# +# 4. end +# - Final state that closes the conversation +# - No functions available +# - Pre-action: Farewell message +# - Post-action: Ends conversation + flow_config = { "initial_node": "start", "nodes": { @@ -59,14 +87,19 @@ flow_config = { "choose_pizza": { "message": { "role": "system", - "content": "The user has chosen pizza. You must now ask them to select a size using the select_pizza_size function. Do not proceed until they use this function. Do not assume any selections have been made.", + "content": """You are handling a pizza order. Use the available functions: + - Use select_pizza_size when the user specifies a size (can be used multiple times if they change their mind) + - Use the end function ONLY when the user confirms they are done with their order + + After each size selection, confirm the selection and ask if they want to change it or complete their order. + Only use the end function after the user confirms they are satisfied with their order.""", }, "functions": [ { "type": "function", "function": { "name": "select_pizza_size", - "description": "Select pizza size", + "description": "Record the selected pizza size", "parameters": { "type": "object", "properties": { @@ -79,21 +112,36 @@ flow_config = { "required": ["size"], }, }, - } + }, + { + "type": "function", + "function": { + "name": "end", + "description": "Complete the order (use only after user confirms)", + "parameters": {"type": "object", "properties": {}}, + }, + }, + ], + "pre_actions": [ + {"type": "tts_say", "text": "Ok, let me help you with your pizza order..."} ], - "pre_actions": [{"type": "tts_say", "text": "Ok, let me pull up our pizza menu..."}], }, "choose_sushi": { "message": { "role": "system", - "content": "The user has chosen sushi. Immediately say: 'How many sushi rolls would you like to order?' If they answer provide to the question of how many rolls, use the select_roll_count function.", + "content": """You are handling a sushi order. Use the available functions: + - Use select_roll_count when the user specifies how many rolls (can be used multiple times if they change their mind) + - Use the end function ONLY when the user confirms they are done with their order + + After each roll count selection, confirm the count and ask if they want to change it or complete their order. + Only use the end function after the user confirms they are satisfied with their order.""", }, "functions": [ { "type": "function", "function": { "name": "select_roll_count", - "description": "Select number of sushi rolls", + "description": "Record the number of sushi rolls", "parameters": { "type": "object", "properties": { @@ -107,9 +155,28 @@ flow_config = { "required": ["count"], }, }, - } + }, + { + "type": "function", + "function": { + "name": "end", + "description": "Complete the order (use only after user confirms)", + "parameters": {"type": "object", "properties": {}}, + }, + }, ], - "pre_actions": [{"type": "tts_say", "text": "Ok, one moment..."}], + "pre_actions": [ + {"type": "tts_say", "text": "Ok, let me help you with your sushi order..."} + ], + }, + "end": { + "message": { + "role": "system", + "content": "The order is complete. Thank the user and end the conversation.", + }, + "functions": [], + "pre_actions": [{"type": "tts_say", "text": "Thank you for your order! Goodbye!"}], + "post_actions": [{"type": "end_conversation"}], }, }, } diff --git a/src/pipecat/flows/manager.py b/src/pipecat/flows/manager.py index 83e1f858e..a923628ac 100644 --- a/src/pipecat/flows/manager.py +++ b/src/pipecat/flows/manager.py @@ -10,6 +10,7 @@ from typing import Callable, Dict, List, Optional from loguru import logger from pipecat.frames.frames import ( + EndFrame, LLMMessagesAppendFrame, LLMMessagesUpdateFrame, LLMSetToolsFrame, @@ -51,6 +52,7 @@ class FlowManager: # Register built-in actions self.register_action("tts_say", self._handle_tts_action) + self.register_action("end_conversation", self._handle_end_action) async def initialize(self, initial_messages: List[dict]): """Initialize the flow with starting messages and functions. @@ -137,14 +139,37 @@ class FlowManager: logger.warning(f"No handler registered for action type: {action_type}") async def _handle_tts_action(self, action: dict): - """Built-in handler for TTS actions""" + """Built-in handler for TTS actions that speak immediately. + + This handler attempts to use the TTS service directly to speak the text + immediately, bypassing the pipeline queue. If no TTS service is available, + it falls back to queueing the text through the pipeline. + + Args: + action: Dictionary containing the action configuration. + Must include a 'text' key with the text to speak. + """ if self.tts: - # Direct call to TTS service to speak text + # Direct call to TTS service to speak text immediately await self.tts.say(action["text"]) else: # Fall back to queued TTS if no direct service available await self.task.queue_frame(TTSSpeakFrame(text=action["text"])) + async def _handle_end_action(self, action: dict): + """Built-in handler for ending the conversation. + + This handler queues an EndFrame to terminate the conversation. If the action + includes a 'text' key, it will queue that text to be spoken before ending. + + Args: + action: Dictionary containing the action configuration. + Optional 'text' key for a goodbye message. + """ + if action.get("text"): # Optional goodbye message + await self.task.queue_frame(TTSSpeakFrame(text=action["text"])) + await self.task.queue_frame(EndFrame()) + async def handle_transition(self, function_name: str): """Handle node transition triggered by a function call.