Spaces:
Runtime error
Runtime error
File size: 9,713 Bytes
24cdea4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 | """
Pipecat Flow Handlers for RTVI Integration
This module provides handlers for integrating Pipecat flows with RTVI punt system,
allowing client-side code execution while maintaining server-side flow management.
"""
from datetime import datetime
from typing import Callable, Optional
from pipecat.processors.frameworks.rtvi import RTVIProcessor, RTVIServerMessageFrame
from pipecat_flows import FlowConfig, FlowManager, FlowsFunctionSchema
from loguru import logger
# Define the set of action types that should get no-op handlers
NO_OP_ACTION_TYPES = {
"save_summary_conversation",
"node_summary_conversation",
"save_html_conversation",
}
def create_stub_handler(
node_name: str,
function_name: str,
rtvi_client: RTVIProcessor,
transition_to: Optional[str] = None,
) -> Callable:
"""
Create a stub handler that forwards function calls to client via RTVI.
This enables "RTVI stubs" - when server-side flow encounters a client function,
it sends the request to the client for execution while maintaining flow state.
Args:
node_name: Name of the node that contains the function
function_name: Name of the function to punt to client
rtvi_client: RTVI client for sending messages
transition_to: Optional node to transition to after function execution
Returns:
Async handler function for use with FlowManager
"""
async def stub_handler(params, flow_manager: FlowManager):
"""Fire-and-forget stub handler"""
try:
# Create function request message
message = {
"type": "function_request",
"function_name": function_name,
"params": [params],
"timestamp": datetime.now().isoformat(),
}
# Send to client via RTVI server message frame
frame = RTVIServerMessageFrame(data=message)
await flow_manager.task.queue_frame(frame)
logger.info(f"π Punted function '{function_name}' to client")
if transition_to:
state_message = {
"type": "flow_state_sync",
"current_node": transition_to,
"previous_node": node_name,
"timestamp": datetime.now().isoformat(),
}
frame = RTVIServerMessageFrame(data=state_message)
await flow_manager.task.queue_frame(frame)
logger.info(f"π State sync: {node_name} β {transition_to}")
return {"status": "success"}
except Exception as e:
logger.error(f"β Failed to punt function '{function_name}': {e}")
return {"status": "error", "error": str(e)}
return stub_handler
async def send_state_sync(flow_manager: FlowManager, node_name: str):
state_message = {
"type": "flow_state_sync",
"current_node": node_name,
"timestamp": datetime.now().isoformat(),
}
frame = RTVIServerMessageFrame(data=state_message)
await flow_manager.task.queue_frame(frame)
logger.info(f"π State sync: {node_name}")
def create_no_op_action_handler() -> Callable:
"""
Create a no-op handler for actions that should be ignored on server-side.
Returns:
Async handler function that does nothing
"""
async def no_op_handler(action: dict, flow_manager: FlowManager):
"""No-op handler for client-side only actions"""
logger.debug(
f"No-op handler called for action type: {action.get('type', 'unknown')}"
)
pass
return no_op_handler
async def end_conversation_handler(action: dict, flow_manager: FlowManager):
"""Handler for end_conversation action - works for both Daily and Twilio"""
try:
# Import here to avoid circular imports
from utils.twilio import terminate_call, get_call_context
# Determine call source
if not action or action == {}:
call_source = "function_call"
else:
call_source = "post_action"
logger.info(f"π End conversation called with source {call_source}")
# Create end_conversation message with source information
message = {
"type": "end_conversation",
"timestamp": datetime.now().isoformat(),
"call_source": call_source,
"immediate_disconnect": call_source == "function_call",
}
# Try to send RTVI message (works for Daily/WebRTC clients)
try:
frame = RTVIServerMessageFrame(data=message)
await flow_manager.task.queue_frame(frame)
logger.info("π Sent end_conversation message to client")
except Exception as rtvi_error:
logger.debug(f"RTVI message not sent (expected for Twilio): {rtvi_error}")
# For Twilio calls, terminate via REST API
call_context = get_call_context()
if call_context and call_context.get("call_sid"):
call_sid = call_context["call_sid"]
logger.info(f"π Terminating Twilio call via REST API: {call_sid}")
await terminate_call(call_sid)
# Cancel the task to end the call (works for both Daily and Twilio)
# This ensures the call is terminated regardless of transport type
if call_source == "function_call":
logger.info("π Cancelling task to end call")
await flow_manager.task.cancel()
except Exception as e:
logger.error(f"β Failed to end conversation: {e}")
def process_flow_config_handlers(
llm_flow: FlowConfig,
rtvi_client: RTVIProcessor,
) -> FlowConfig:
"""
Process flow configuration and replace string handlers with RTVI punt handlers.
This is the core of the RTVI punt system - it finds string handlers like
"__function__:handleTransition" and replaces them with lambda functions
that send function requests to the client.
Args:
llm_flow: LLM flow configuration
rtvi_client: RTVI client for sending messages
Returns:
Processed flow config with punt handlers
"""
processed_config = llm_flow.copy()
# Get the handlers once
no_op_handler = create_no_op_action_handler()
# Process each node
for node_name, node_config in processed_config.get("nodes", {}).items():
# Process functions
functions = node_config.get("functions", [])
# Inject end_conversation function at every node
end_conversation_function = FlowsFunctionSchema(
name="end_conversation",
description="Call this function when the user explicitly asks to stop, end, or terminate the current interview or conversation. This should be called when user clearly expresses they want to finish the interview session.",
properties={},
required=[],
handler=end_conversation_handler,
)
functions.append(end_conversation_function)
logger.info(f"π¬ Injected end_conversation function in node: {node_name}")
for i, func_outer in enumerate(functions):
# Handle both dict format and FlowsFunctionSchema
if isinstance(func_outer, FlowsFunctionSchema):
func = func_outer
func_name = func.name
else:
func = func_outer["function"]
func_name = func["name"]
# Skip processing if this is the end_conversation function we just added
if func_name == "end_conversation":
continue
# Convert to FlowsFunctionSchema if it's not already
if isinstance(func_outer, dict):
func_data = func_outer["function"]
name = func_data["name"]
description = func_data.get("description", "")
parameters = func_data.get("parameters", {}) or {}
properties = parameters.get("properties", {})
required = parameters.get("required", [])
# Extract Flows-specific fields
transition_to = func_data.get("transition_to")
# Create RTVI stub handler
handler = create_stub_handler(
node_name, func_name, rtvi_client, transition_to=transition_to
)
logger.info(f"π€ Converted {func_name} to RTVI stub")
new_func = FlowsFunctionSchema(
name=name,
description=description,
properties=properties,
required=required,
handler=handler,
transition_to=transition_to,
)
functions[i] = new_func
# Process pre_actions
pre_actions = node_config.get("pre_actions", [])
for action in pre_actions:
action_type = action.get("type")
if action_type in NO_OP_ACTION_TYPES:
action["handler"] = no_op_handler
logger.info(
f"Added no-op handler to pre_action: {action_type} in node: {node_name}"
)
# Process post_actions
post_actions = node_config.get("post_actions", [])
for action in post_actions:
action_type = action.get("type")
if action_type in NO_OP_ACTION_TYPES:
action["handler"] = no_op_handler
logger.info(
f"Added no-op handler to post_action: {action_type} in node: {node_name}"
)
logger.info("β
Flow config handlers processed for RTVI stubs and actions")
return processed_config
|