| 1 | /** |
| 2 | * Voximplant + Ultravox WebSocket API connector demo |
| 3 | * Scenario: answer an incoming call and handle client tool invocations. |
| 4 | */ |
| 5 | |
| 6 | require(Modules.Ultravox); |
| 7 | require(Modules.ApplicationStorage); |
| 8 | |
| 9 | const SYSTEM_PROMPT = `You are a helpful phone assistant for Voximplant callers. |
| 10 | If you need external data, call the get_weather tool. |
| 11 | If the caller wants to end the call, call the hangup_call tool.`; |
| 12 | |
| 13 | // -------------------- Ultravox WebSocket API settings -------------------- |
| 14 | const AGENT_CONFIG = { |
| 15 | systemPrompt: SYSTEM_PROMPT, |
| 16 | model: "ultravox-v0.7", |
| 17 | voice: "Mark", |
| 18 | }; |
| 19 | const WEATHER_TOOL_NAME = "get_weather"; // Needs to be defined in the Ultravox portal |
| 20 | const HANGUP_TOOL_NAME = "hangUp"; // Built-in Ultravox tool |
| 21 | |
| 22 | VoxEngine.addEventListener(AppEvents.CallAlerting, async ({call}) => { |
| 23 | let voiceAIClient; |
| 24 | let hangupAfterResponse = false; |
| 25 | let hangupTimer; |
| 26 | |
| 27 | // Termination functions - add cleanup and logging as needed |
| 28 | call.addEventListener(CallEvents.Disconnected, ()=>VoxEngine.terminate()); |
| 29 | call.addEventListener(CallEvents.Failed, ()=>VoxEngine.terminate()); |
| 30 | |
| 31 | try { |
| 32 | call.answer(); |
| 33 | // call.record({ hd_audio: true, stereo: true }); // Optional: record the call |
| 34 | |
| 35 | // Create client and wire media |
| 36 | voiceAIClient = await Ultravox.createWebSocketAPIClient( |
| 37 | { |
| 38 | endpoint: Ultravox.HTTPEndpoint.CREATE_CALL, |
| 39 | authorizations: {"X-API-Key": (await ApplicationStorage.get("ULTRAVOX_API_KEY")).value}, |
| 40 | body: AGENT_CONFIG, |
| 41 | onWebSocketClose: (event) => { |
| 42 | Logger.write("===ULTRAVOX_WEBSOCKET_CLOSED==="); |
| 43 | if (event) Logger.write(JSON.stringify(event)); |
| 44 | VoxEngine.terminate(); |
| 45 | }, |
| 46 | }, |
| 47 | ); |
| 48 | |
| 49 | VoxEngine.sendMediaBetween(call, voiceAIClient); |
| 50 | |
| 51 | // ---------------------- Event handlers ----------------------- |
| 52 | // Function calling: handle tool requests and send back responses |
| 53 | voiceAIClient.addEventListener( |
| 54 | Ultravox.WebSocketAPIEvents.ClientToolInvocation, |
| 55 | (event) => { |
| 56 | const payload = event?.data?.payload || event?.data || {}; |
| 57 | const {toolName, invocationId, parameters} = payload; |
| 58 | |
| 59 | if (!toolName || !invocationId) return; |
| 60 | |
| 61 | if (toolName !== WEATHER_TOOL_NAME && toolName !== HANGUP_TOOL_NAME) { |
| 62 | voiceAIClient.clientToolResult({ |
| 63 | type: "client_tool_result", |
| 64 | invocationId, |
| 65 | errorType: "tool_not_found", |
| 66 | errorMessage: `Unhandled tool: ${toolName}`, |
| 67 | }); |
| 68 | return; |
| 69 | } |
| 70 | |
| 71 | if (toolName === WEATHER_TOOL_NAME) { |
| 72 | const location = parameters?.location || "Unknown"; |
| 73 | const result = JSON.stringify({ |
| 74 | location, |
| 75 | temperature_f: 72, |
| 76 | condition: "sunny", |
| 77 | }); |
| 78 | |
| 79 | voiceAIClient.clientToolResult({ |
| 80 | type: "client_tool_result", |
| 81 | invocationId, |
| 82 | responseType: "tool-response", |
| 83 | agentReaction: "speaks", |
| 84 | result, |
| 85 | }); |
| 86 | return; |
| 87 | } |
| 88 | |
| 89 | if (toolName === HANGUP_TOOL_NAME) { |
| 90 | hangupAfterResponse = true; |
| 91 | voiceAIClient.clientToolResult({ |
| 92 | type: "client_tool_result", |
| 93 | invocationId, |
| 94 | responseType: "tool-response", |
| 95 | agentReaction: "speaks", |
| 96 | result: JSON.stringify({ result: "Hanging up now. Goodbye!" }), |
| 97 | }); |
| 98 | } |
| 99 | }, |
| 100 | ); |
| 101 | |
| 102 | // Barge-in: keep conversation responsive and capture transcript |
| 103 | voiceAIClient.addEventListener( |
| 104 | Ultravox.WebSocketAPIEvents.Transcript, |
| 105 | (event) => { |
| 106 | const payload = event?.data?.payload || event?.data || {}; |
| 107 | const role = payload.role; |
| 108 | const text = payload.text || payload.delta; |
| 109 | |
| 110 | if (role && text) Logger.write(`===TRANSCRIPT=== ${role}: ${text}`); |
| 111 | if (role === "user") voiceAIClient.clearMediaBuffer(); |
| 112 | if (hangupAfterResponse && role === "assistant" && payload.text) { |
| 113 | if (hangupTimer) clearTimeout(hangupTimer); |
| 114 | hangupTimer = setTimeout(() => call.hangup(), 1200); |
| 115 | } |
| 116 | }, |
| 117 | ); |
| 118 | |
| 119 | voiceAIClient.addEventListener( |
| 120 | Ultravox.WebSocketAPIEvents.PlaybackClearBuffer, |
| 121 | () => voiceAIClient.clearMediaBuffer(), |
| 122 | ); |
| 123 | |
| 124 | // If the media stream ends after a goodbye response, hang up the call. |
| 125 | voiceAIClient.addEventListener(Ultravox.Events.WebSocketMediaEnded, () => { |
| 126 | if (hangupAfterResponse) call.hangup(); |
| 127 | }); |
| 128 | |
| 129 | // Consolidated "log-only" handlers - key Ultravox/VoxEngine debugging events |
| 130 | [ |
| 131 | Ultravox.WebSocketAPIEvents.ConnectorInformation, |
| 132 | Ultravox.WebSocketAPIEvents.HTTPResponse, |
| 133 | Ultravox.WebSocketAPIEvents.State, |
| 134 | Ultravox.WebSocketAPIEvents.Debug, |
| 135 | Ultravox.WebSocketAPIEvents.WebSocketError, |
| 136 | Ultravox.WebSocketAPIEvents.Unknown, |
| 137 | Ultravox.Events.WebSocketMediaStarted, |
| 138 | Ultravox.Events.WebSocketMediaEnded, |
| 139 | ].forEach((eventName) => { |
| 140 | voiceAIClient.addEventListener(eventName, (event) => { |
| 141 | Logger.write(`===${event.name}===`); |
| 142 | Logger.write(JSON.stringify(event)); |
| 143 | }); |
| 144 | }); |
| 145 | } catch (error) { |
| 146 | Logger.write("===SOMETHING_WENT_WRONG==="); |
| 147 | Logger.write(error); |
| 148 | VoxEngine.terminate(); |
| 149 | } |
| 150 | }); |