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