Example: Function calling
Example: Function calling
For the complete documentation index, see llms.txt.
This example answers an inbound Voximplant call, connects it to OpenAI Realtime, and handles function calls in VoxEngine. Select a model version to keep the session configuration and complete scenario synchronized. gpt-realtime-2.1 is the default for new applications.
It includes three tools:
get_weatherhang_upwarm_transfer
Jump to the Full VoxEngine scenarios.
Prerequisites
- Set up an inbound entrypoint for the caller:
- Create a routing rule that points the destination (phone number / WhatsApp / SIP username / app user alias) to this scenario.
- Store your OpenAI API key in Voximplant Secrets under
OPENAI_API_KEY.
Session setup
GPT Realtime 2.1
GPT Realtime 1.5
The scenario uses sessionUpdate to define:
- Realtime model instructions
- low reasoning effort
- nested
semantic_vadturn detection - tool schemas (
tools) tool_choice: "auto"
Tool definitions are declared in SESSION_CONFIG.session.tools and sent right after SessionCreated.
1 const SESSION_CONFIG = { 2 session: { 3 type: "realtime", 4 model: "gpt-realtime-2.1", 5 instructions: SYSTEM_PROMPT, 6 reasoning: { effort: "low" }, 7 output_modalities: ["audio"], 8 audio: { 9 input: { 10 turn_detection: { type: "semantic_vad", interrupt_response: true }, 11 }, 12 output: { 13 voice: "alloy", 14 }, 15 }, 16 tools: [ 17 { 18 type: "function", 19 name: WEATHER_TOOL, 20 description: "Get the weather for a given location", 21 parameters: { 22 type: "object", 23 properties: { 24 location: { type: "string" }, 25 }, 26 required: ["location"], 27 }, 28 }, 29 { 30 type: "function", 31 name: HANGUP_TOOL, 32 description: "Hang up the call", 33 parameters: { 34 type: "object", 35 properties: {}, 36 required: [], 37 }, 38 }, 39 { 40 type: "function", 41 name: WARM_TRANSFER_TOOL, 42 description: "Warm transfer the caller to a phone number", 43 parameters: { 44 type: "object", 45 properties: { 46 destination_number: { type: "string" }, 47 message: { type: "string" }, 48 delay_ms: { type: "integer" }, 49 }, 50 required: [], 51 }, 52 }, 53 ], 54 tool_choice: "auto", 55 }, 56 }; 57 58 voiceAIClient.sessionUpdate(SESSION_CONFIG);
Connect call audio
After SessionUpdated, the example bridges call audio to OpenAI and prompts the agent to greet the caller. See the full scenarios below for the version-specific greeting.
1 VoxEngine.sendMediaBetween(call, voiceAIClient);
Function calling flow
The example listens for OpenAI.RealtimeAPIEvents.ResponseFunctionCallArgumentsDone, parses arguments, and returns tool output with conversationItemCreate (type: "function_call_output"), then calls responseCreate so the assistant continues.
get_weather
Returns a stub weather payload for the requested location.
hang_up
Sets pendingHangup = true, returns hangup_scheduled, and hangs up after assistant audio completes (ResponseOutputAudioDone or WebSocketMediaEnded).
warm_transfer
Places a PSTN leg, plays a brief intro to the callee, then bridges the original caller to the transfer leg after a delay.
Barge-in
The scenario clears buffered model audio whenever OpenAI detects caller speech:
1 voiceAIClient.addEventListener(OpenAI.RealtimeAPIEvents.InputAudioBufferSpeechStarted, () => { 2 voiceAIClient.clearMediaBuffer(); 3 });
Full VoxEngine scenarios
Notes
- Tool implementations are demo stubs. Replace them with production APIs and transfer logic.
- The warm transfer demo uses a default destination if the model does not provide one.
GPT Realtime 2.1
GPT Realtime 1.5
1 /** 2 * Voximplant + OpenAI Realtime API connector demo 3 * Scenario: answer an incoming call and handle OpenAI Realtime 2.1 function calls. 4 */ 5 6 require(Modules.OpenAI); 7 8 const SYSTEM_PROMPT = ` 9 You are Voxi, a helpful phone assistant. 10 Keep responses short and telephony-friendly. 11 If the caller asks for weather, call get_weather. 12 If the caller wants to end the call, say a brief goodbye and call hang_up. 13 If the caller asks for a warm transfer, call warm_transfer with destination_number. 14 `; 15 16 const WEATHER_TOOL = "get_weather"; 17 const HANGUP_TOOL = "hang_up"; 18 const WARM_TRANSFER_TOOL = "warm_transfer"; 19 20 const DEFAULT_TRANSFER_NUMBER = "+18889277255"; 21 const DEFAULT_TRANSFER_DELAY_MS = 3000; 22 const DEFAULT_TRANSFER_GREETING = 23 "Hi, this is Voxi. I'm warm transferring a caller. Please hold for a brief note, then I'll connect you."; 24 25 const SESSION_CONFIG = { 26 session: { 27 type: "realtime", 28 model: "gpt-realtime-2.1", 29 instructions: SYSTEM_PROMPT, 30 reasoning: {effort: "low"}, 31 output_modalities: ["audio"], 32 audio: { 33 input: { 34 turn_detection: {type: "semantic_vad", interrupt_response: true}, 35 }, 36 output: { 37 voice: "alloy", 38 }, 39 }, 40 tools: [ 41 { 42 type: "function", 43 name: WEATHER_TOOL, 44 description: "Get the weather for a given location", 45 parameters: { 46 type: "object", 47 properties: { 48 location: {type: "string"}, 49 }, 50 required: ["location"], 51 }, 52 }, 53 { 54 type: "function", 55 name: HANGUP_TOOL, 56 description: "Hang up the call", 57 parameters: { 58 type: "object", 59 properties: {}, 60 required: [], 61 }, 62 }, 63 { 64 type: "function", 65 name: WARM_TRANSFER_TOOL, 66 description: "Warm transfer the caller to a phone number", 67 parameters: { 68 type: "object", 69 properties: { 70 destination_number: {type: "string"}, 71 message: {type: "string"}, 72 delay_ms: {type: "integer"}, 73 }, 74 required: [], 75 }, 76 }, 77 ], 78 tool_choice: "auto", 79 }, 80 }; 81 82 VoxEngine.addEventListener(AppEvents.CallAlerting, async ({call}) => { 83 let voiceAIClient; 84 let transferCall; 85 let transferInProgress = false; 86 let pendingHangup = false; 87 88 call.addEventListener(CallEvents.Disconnected, () => VoxEngine.terminate()); 89 call.addEventListener(CallEvents.Failed, () => VoxEngine.terminate()); 90 91 try { 92 call.answer(); 93 // call.record({hd_audio: true, stereo: true}); // Optional: record the call 94 95 voiceAIClient = await OpenAI.createRealtimeAPIClient({ 96 apiKey: VoxEngine.getSecretValue("OPENAI_API_KEY"), 97 model: "gpt-realtime-2.1", 98 onWebSocketClose: (event) => { 99 Logger.write("===OpenAI.WebSocket.Close==="); 100 if (event) Logger.write(JSON.stringify(event)); 101 VoxEngine.terminate(); 102 }, 103 }); 104 105 voiceAIClient.addEventListener(OpenAI.RealtimeAPIEvents.SessionCreated, () => { 106 voiceAIClient.sessionUpdate(SESSION_CONFIG); 107 }); 108 109 voiceAIClient.addEventListener(OpenAI.RealtimeAPIEvents.SessionUpdated, () => { 110 VoxEngine.sendMediaBetween(call, voiceAIClient); // media connected 111 voiceAIClient.conversationItemCreate({ // request a greeting 112 item: { 113 type: "message", 114 role: "user", 115 content: [ 116 { 117 type: "input_text", 118 text: "The phone call just connected. " 119 + 'Say exactly: "Hello! How can I help today?"', 120 }, 121 ], 122 }, 123 }); 124 voiceAIClient.responseCreate({}); // ask for the response now 125 }); 126 127 voiceAIClient.addEventListener( 128 OpenAI.RealtimeAPIEvents.InputAudioBufferSpeechStarted, 129 () => { 130 Logger.write("===BARGE-IN: OpenAI.InputAudioBufferSpeechStarted==="); 131 voiceAIClient.clearMediaBuffer(); 132 }, 133 ); 134 135 // Handle function calls 136 voiceAIClient.addEventListener( 137 OpenAI.RealtimeAPIEvents.ResponseFunctionCallArgumentsDone, 138 async (event) => { 139 const payload = event?.data?.payload || event?.data || {}; 140 const toolName = payload.name || payload.tool_name; 141 const toolCallId = payload.call_id || payload.callId; 142 const rawArgs = payload.arguments; 143 144 if (!toolName || !toolCallId) { 145 Logger.write("===TOOL_CALL_MISSING_FIELDS==="); 146 Logger.write(JSON.stringify(payload)); 147 return; 148 } 149 150 let args = {}; 151 if (typeof rawArgs === "string") { 152 try { 153 args = JSON.parse(rawArgs); 154 } catch (error) { 155 Logger.write("===TOOL_ARGS_PARSE_ERROR==="); 156 Logger.write(rawArgs); 157 Logger.write(error); 158 } 159 } else if (rawArgs && typeof rawArgs === "object") { 160 args = rawArgs; 161 } 162 163 Logger.write("===TOOL_CALL_RECEIVED==="); 164 Logger.write(JSON.stringify({toolName, args})); 165 166 if (toolName === WEATHER_TOOL) { 167 const location = args.location || "Unknown"; 168 const result = { 169 location, 170 temperature_f: 72, 171 condition: "sunny", 172 }; 173 voiceAIClient.conversationItemCreate({ 174 item: { 175 type: "function_call_output", 176 call_id: toolCallId, 177 output: JSON.stringify(result), 178 }, 179 }); 180 voiceAIClient.responseCreate({}); 181 return; 182 } 183 184 if (toolName === HANGUP_TOOL) { 185 pendingHangup = true; 186 voiceAIClient.conversationItemCreate({ 187 item: { 188 type: "function_call_output", 189 call_id: toolCallId, 190 output: JSON.stringify({status: "hangup_scheduled"}), 191 }, 192 }); 193 voiceAIClient.responseCreate({}); 194 return; 195 } 196 197 if (toolName === WARM_TRANSFER_TOOL) { 198 if (transferInProgress) { 199 voiceAIClient.conversationItemCreate({ 200 item: { 201 type: "function_call_output", 202 call_id: toolCallId, 203 output: JSON.stringify({status: "transfer_already_in_progress"}), 204 }, 205 }); 206 voiceAIClient.responseCreate({}); 207 return; 208 } 209 210 transferInProgress = true; 211 212 const destination = 213 args.destination_number || 214 args.destination || 215 args.phone_number || 216 args.number || 217 DEFAULT_TRANSFER_NUMBER; 218 const delayMs = Number.isFinite(args.delay_ms) 219 ? args.delay_ms 220 : DEFAULT_TRANSFER_DELAY_MS; 221 const message = args.message || DEFAULT_TRANSFER_GREETING; 222 223 try { 224 transferCall = VoxEngine.callPSTN(destination, call.callerid()); 225 // transferCall = VoxEngine.callUser({username: destination, callerid: call.callerid()}); 226 // transferCall = VoxEngine.callSIP(`sip:${destination}@your-sip-domain`, call.callerid()); 227 // transferCall = VoxEngine.callWhatsappUser({number: destination, callerid: call.callerid()}); 228 229 transferCall.addEventListener(CallEvents.Connected, () => { 230 Logger.write(`===WARM_TRANSFER_CONNECTED=== ${destination}`); 231 transferCall.say(message); 232 233 setTimeout(() => { 234 try { 235 voiceAIClient.clearMediaBuffer(); 236 call.stopMediaTo(voiceAIClient); 237 voiceAIClient.stopMediaTo(call); 238 VoxEngine.sendMediaBetween(call, transferCall); 239 voiceAIClient.close(); 240 Logger.write("===WARM_TRANSFER_BRIDGED==="); 241 } catch (bridgeError) { 242 Logger.write("===WARM_TRANSFER_BRIDGE_ERROR==="); 243 Logger.write(bridgeError); 244 } 245 }, delayMs); 246 }); 247 248 transferCall.addEventListener(CallEvents.Failed, (event) => { 249 Logger.write("===WARM_TRANSFER_FAILED==="); 250 Logger.write(JSON.stringify(event)); 251 transferInProgress = false; 252 }); 253 254 voiceAIClient.conversationItemCreate({ 255 item: { 256 type: "function_call_output", 257 call_id: toolCallId, 258 output: JSON.stringify({ 259 status: "transfer_started", 260 destination, 261 delay_ms: delayMs, 262 }), 263 }, 264 }); 265 voiceAIClient.responseCreate({}); 266 } catch (transferError) { 267 Logger.write("===WARM_TRANSFER_ERROR==="); 268 Logger.write(transferError); 269 transferInProgress = false; 270 voiceAIClient.conversationItemCreate({ 271 item: { 272 type: "function_call_output", 273 call_id: toolCallId, 274 output: JSON.stringify({error: "warm_transfer_failed"}), 275 }, 276 }); 277 voiceAIClient.responseCreate({}); 278 } 279 return; 280 } 281 282 voiceAIClient.conversationItemCreate({ 283 item: { 284 type: "function_call_output", 285 call_id: toolCallId, 286 output: JSON.stringify({error: `Unhandled tool: ${toolName}`}), 287 }, 288 }); 289 voiceAIClient.responseCreate({}); 290 }, 291 ); 292 293 voiceAIClient.addEventListener(OpenAI.RealtimeAPIEvents.ResponseOutputAudioDone, () => { 294 if (!pendingHangup) return; 295 Logger.write("===HANGUP_AFTER_AGENT_AUDIO==="); 296 pendingHangup = false; 297 call.hangup(); 298 }); 299 300 voiceAIClient.addEventListener(OpenAI.Events.WebSocketMediaEnded, () => { 301 if (!pendingHangup) return; 302 Logger.write("===HANGUP_AFTER_MEDIA_ENDED==="); 303 pendingHangup = false; 304 call.hangup(); 305 }); 306 307 // Consolidated "log-only" handlers 308 [ 309 OpenAI.RealtimeAPIEvents.ResponseCreated, 310 OpenAI.RealtimeAPIEvents.ResponseDone, 311 OpenAI.RealtimeAPIEvents.ResponseOutputAudioTranscriptDone, 312 OpenAI.RealtimeAPIEvents.ResponseFunctionCallArgumentsDelta, 313 OpenAI.RealtimeAPIEvents.ConnectorInformation, 314 OpenAI.RealtimeAPIEvents.HTTPResponse, 315 OpenAI.RealtimeAPIEvents.WebSocketError, 316 OpenAI.RealtimeAPIEvents.Unknown, 317 OpenAI.Events.WebSocketMediaStarted, 318 OpenAI.Events.WebSocketMediaEnded, 319 ].forEach((eventName) => { 320 voiceAIClient.addEventListener(eventName, (event) => { 321 Logger.write(`===${event.name}===`); 322 if (event?.data) Logger.write(JSON.stringify(event.data)); 323 }); 324 }); 325 } catch (error) { 326 Logger.write("===UNHANDLED_ERROR==="); 327 Logger.write(error); 328 voiceAIClient?.close(); 329 VoxEngine.terminate(); 330 } 331 });