Call Transfer demo (#1348)
* Updated code to dial out to an operator, keep track of operator conversation while escalated and then return to conversation when finished * Removed unnecessary imports * Updated bot runner code, added call routing file and then updated the call transfer and voicemail detection examples * Updated the bot files * Made prompt one level higher in the body and an array * Updated call transfer examples to work correctly * Updated gemini voicemail detection example to work * Added twilio bot support back to the bot_runner * Moved some state management, participant management and other logic to the helper file. * Updated comments * Updated env and requirements file * Ran the examples and made sure code works. Still need to work on the prompts a bit * Fixed format issue * Add support to disable summary in call transfer * Added support for operator transfer mode * Updated readme file * Updated readme based on feedback, and handling of various properties in the json to be more flexible for future examples * Updated number of endpoints * Updated readme to remove fly deployment text and replaced with Pipecat Cloud * Starting to tweak function calls and prompts * Updated examples to more consistently call the functions and say what they need to say * Updated examples * Updated examples * Updated examples to work correctly * Add simple bot versions of dialin and dialout * Refactored the bot runner file to make adding future examples easier * Based on feedback, removed examples for multiple LLMs and also adjusted voicemail detection code to be simpler * Made sure to only capture the users transcription once * Updated readme with latest changes * Forgot to update the order of examples in one place * Fixed formatting issue * Adjusted based on james feedback * Changed default_mode to default_calltransfer_mode
This commit is contained in:
211
examples/phone-chatbot/bot_runner_helpers.py
Normal file
211
examples/phone-chatbot/bot_runner_helpers.py
Normal file
@@ -0,0 +1,211 @@
|
||||
# bot_runner_helpers.py
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from bot_constants import (
|
||||
DEFAULT_CALLTRANSFER_MODE,
|
||||
DEFAULT_DIALIN_EXAMPLE,
|
||||
DEFAULT_SPEAK_SUMMARY,
|
||||
DEFAULT_STORE_SUMMARY,
|
||||
DEFAULT_TEST_IN_PREBUILT,
|
||||
)
|
||||
from call_connection_manager import CallConfigManager
|
||||
|
||||
# ----------------- Configuration Helpers ----------------- #
|
||||
|
||||
|
||||
def determine_room_capabilities(config_body: Optional[Dict[str, Any]] = None) -> Dict[str, bool]:
|
||||
"""Determine room capabilities based on the configuration.
|
||||
|
||||
This function examines the configuration to determine which capabilities
|
||||
the Daily room should have enabled.
|
||||
|
||||
Args:
|
||||
config_body: Configuration dictionary that determines room capabilities
|
||||
|
||||
Returns:
|
||||
Dictionary of capability flags
|
||||
"""
|
||||
capabilities = {
|
||||
"enable_dialin": False,
|
||||
"enable_dialout": False,
|
||||
# Add more capabilities here in the future as needed
|
||||
}
|
||||
|
||||
if not config_body:
|
||||
return capabilities
|
||||
|
||||
# Check for dialin capability
|
||||
capabilities["enable_dialin"] = "dialin_settings" in config_body
|
||||
|
||||
# Check for dialout capability - needed for outbound calls or transfers
|
||||
has_dialout_settings = "dialout_settings" in config_body
|
||||
|
||||
# Check if there's a transfer to an operator configured
|
||||
has_call_transfer = "call_transfer" in config_body
|
||||
|
||||
# Enable dialout if any condition requires it
|
||||
capabilities["enable_dialout"] = has_dialout_settings or has_call_transfer
|
||||
|
||||
return capabilities
|
||||
|
||||
|
||||
def ensure_dialout_settings_array(body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Ensures dialout_settings is an array of objects.
|
||||
|
||||
Args:
|
||||
body: The configuration dictionary
|
||||
|
||||
Returns:
|
||||
Updated configuration with dialout_settings as an array
|
||||
"""
|
||||
if "dialout_settings" in body:
|
||||
# Convert to array if it's not already one
|
||||
if not isinstance(body["dialout_settings"], list):
|
||||
body["dialout_settings"] = [body["dialout_settings"]]
|
||||
|
||||
return body
|
||||
|
||||
|
||||
def ensure_prompt_config(body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Ensures the body has appropriate prompts settings, but doesn't add defaults.
|
||||
|
||||
Only makes sure the prompt section exists, allowing the bot script to handle defaults.
|
||||
|
||||
Args:
|
||||
body: The configuration dictionary
|
||||
|
||||
Returns:
|
||||
Updated configuration with prompt settings section
|
||||
"""
|
||||
if "prompts" not in body:
|
||||
body["prompts"] = []
|
||||
return body
|
||||
|
||||
|
||||
def create_call_transfer_settings(body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Create call transfer settings based on configuration and customer mapping.
|
||||
|
||||
Args:
|
||||
body: The configuration dictionary
|
||||
|
||||
Returns:
|
||||
Call transfer settings dictionary
|
||||
"""
|
||||
# Default transfer settings
|
||||
transfer_settings = {
|
||||
"mode": DEFAULT_CALLTRANSFER_MODE,
|
||||
"speakSummary": DEFAULT_SPEAK_SUMMARY,
|
||||
"storeSummary": DEFAULT_STORE_SUMMARY,
|
||||
"testInPrebuilt": DEFAULT_TEST_IN_PREBUILT,
|
||||
}
|
||||
|
||||
# If call_transfer already exists, merge the defaults with the existing settings
|
||||
# This ensures all required fields exist while preserving user-specified values
|
||||
if "call_transfer" in body:
|
||||
existing_settings = body["call_transfer"]
|
||||
# Update defaults with existing settings (existing values will override defaults)
|
||||
for key, value in existing_settings.items():
|
||||
transfer_settings[key] = value
|
||||
else:
|
||||
# No existing call_transfer - check if we have dialin settings for customer lookup
|
||||
if "dialin_settings" in body:
|
||||
# Create a temporary routing manager just for customer lookup
|
||||
call_config_manager = CallConfigManager(body)
|
||||
|
||||
# Get caller info
|
||||
caller_info = call_config_manager.get_caller_info()
|
||||
from_number = caller_info.get("caller_number")
|
||||
|
||||
if from_number:
|
||||
# Get customer name from phone number
|
||||
customer_name = call_config_manager.get_customer_name(from_number)
|
||||
|
||||
# If we know the customer name, add it to the config for the bot to use
|
||||
if customer_name:
|
||||
transfer_settings["customerName"] = customer_name
|
||||
|
||||
return transfer_settings
|
||||
|
||||
|
||||
def create_simple_dialin_settings(body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Create simple dialin settings based on configuration.
|
||||
|
||||
Args:
|
||||
body: The configuration dictionary
|
||||
|
||||
Returns:
|
||||
Simple dialin settings dictionary
|
||||
"""
|
||||
# Default simple dialin settings
|
||||
simple_dialin_settings = {
|
||||
"testInPrebuilt": DEFAULT_TEST_IN_PREBUILT,
|
||||
}
|
||||
|
||||
# If simple_dialin already exists, merge the defaults with the existing settings
|
||||
if "simple_dialin" in body:
|
||||
existing_settings = body["simple_dialin"]
|
||||
# Update defaults with existing settings (existing values will override defaults)
|
||||
for key, value in existing_settings.items():
|
||||
simple_dialin_settings[key] = value
|
||||
|
||||
return simple_dialin_settings
|
||||
|
||||
|
||||
def create_simple_dialout_settings(body: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Create simple dialout settings based on configuration.
|
||||
|
||||
Args:
|
||||
body: The configuration dictionary
|
||||
|
||||
Returns:
|
||||
Simple dialout settings dictionary
|
||||
"""
|
||||
# Default simple dialout settings
|
||||
simple_dialout_settings = {
|
||||
"testInPrebuilt": DEFAULT_TEST_IN_PREBUILT,
|
||||
}
|
||||
|
||||
# If simple_dialout already exists, merge the defaults with the existing settings
|
||||
if "simple_dialout" in body:
|
||||
existing_settings = body["simple_dialout"]
|
||||
# Update defaults with existing settings (existing values will override defaults)
|
||||
for key, value in existing_settings.items():
|
||||
simple_dialout_settings[key] = value
|
||||
|
||||
return simple_dialout_settings
|
||||
|
||||
|
||||
async def process_dialin_request(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Process incoming dial-in request data to create a properly formatted body.
|
||||
|
||||
Converts camelCase fields received from webhook to snake_case format
|
||||
for internal consistency across the codebase.
|
||||
|
||||
Args:
|
||||
data: Raw dialin data from webhook
|
||||
|
||||
Returns:
|
||||
Properly formatted configuration with snake_case keys
|
||||
"""
|
||||
# Create base body with dialin settings
|
||||
body = {
|
||||
"dialin_settings": {
|
||||
"to": data.get("To", ""),
|
||||
"from": data.get("From", ""),
|
||||
"call_id": data.get("callId", data.get("CallSid", "")), # Convert to snake_case
|
||||
"call_domain": data.get("callDomain", ""), # Convert to snake_case
|
||||
}
|
||||
}
|
||||
|
||||
# Use the global default to determine which example to run for dialin webhooks
|
||||
example = DEFAULT_DIALIN_EXAMPLE
|
||||
|
||||
# Configure the bot based on the example
|
||||
if example == "call_transfer":
|
||||
# Create call transfer settings
|
||||
body["call_transfer"] = create_call_transfer_settings(body)
|
||||
elif example == "simple_dialin":
|
||||
# Create simple dialin settings
|
||||
body["simple_dialin"] = create_simple_dialin_settings(body)
|
||||
|
||||
return body
|
||||
Reference in New Issue
Block a user