Example: Function calling

View as MarkdownOpen in Claude

This example answers an inbound Voximplant call, connects it to OpenAI Realtime, and handles function calls in VoxEngine.

It includes three tools:

  • get_weather
  • hang_up
  • warm_transfer

⬇️ Jump to the Full VoxEngine scenario.

Prerequisites

Session setup

The scenario uses sessionUpdate to define:

  • Realtime model instructions
  • server_vad turn detection
  • tool schemas (tools)
  • tool_choice: "auto"

Tool definitions are declared in SESSION_CONFIG.session.tools and sent right after SessionCreated.

Connect call audio

After SessionUpdated, the example bridges call audio to OpenAI:

Connect call audio
1VoxEngine.sendMediaBetween(call, voiceAIClient);
2voiceAIClient.responseCreate({ instructions: "Hello! How can I help today?" });

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 when caller speech starts:

Barge-in
1voiceAIClient.addEventListener(
2 OpenAI.RealtimeAPIEvents.InputAudioBufferSpeechStarted,
3 () => {
4 voiceAIClient.clearMediaBuffer();
5 }
6);

Notes

  • Tool implementations are demo stubs. Replace with real APIs and transfer logic for production.
  • The warm transfer demo uses a default destination if the model does not provide one.

See the VoxEngine API Reference for more details.

Full VoxEngine scenario

voxeengine-openai-function-calling.js
1/**
2 * Voximplant + OpenAI Realtime API connector demo
3 * Scenario: answer an incoming call and handle OpenAI function calls.
4 */
5
6require(Modules.OpenAI);
7require(Modules.ApplicationStorage);
8
9const SYSTEM_PROMPT = `
10You are Voxi, a helpful phone assistant.
11Keep responses short and telephony-friendly.
12If the caller asks for weather, call get_weather.
13If the caller wants to end the call, say a brief goodbye and call hang_up.
14If the caller asks for a warm transfer, call warm_transfer with destination_number.
15`;
16
17const WEATHER_TOOL = "get_weather";
18const HANGUP_TOOL = "hang_up";
19const WARM_TRANSFER_TOOL = "warm_transfer";
20
21const DEFAULT_TRANSFER_NUMBER = "+18889277255";
22const DEFAULT_TRANSFER_DELAY_MS = 3000;
23const DEFAULT_TRANSFER_GREETING =
24 "Hi, this is Voxi. I'm warm transferring a caller. Please hold for a brief note, then I'll connect you.";
25
26const SESSION_CONFIG = {
27 session: {
28 type: "realtime",
29 instructions: SYSTEM_PROMPT,
30 voice: "alloy",
31 output_modalities: ["audio"],
32 turn_detection: {type: "server_vad", interrupt_response: true},
33 tools: [
34 {
35 type: "function",
36 name: WEATHER_TOOL,
37 description: "Get the weather for a given location",
38 parameters: {
39 type: "object",
40 properties: {
41 location: {type: "string"},
42 },
43 required: ["location"],
44 },
45 },
46 {
47 type: "function",
48 name: HANGUP_TOOL,
49 description: "Hang up the call",
50 parameters: {
51 type: "object",
52 properties: {},
53 required: [],
54 },
55 },
56 {
57 type: "function",
58 name: WARM_TRANSFER_TOOL,
59 description: "Warm transfer the caller to a phone number",
60 parameters: {
61 type: "object",
62 properties: {
63 destination_number: {type: "string"},
64 message: {type: "string"},
65 delay_ms: {type: "integer"},
66 },
67 required: [],
68 },
69 },
70 ],
71 tool_choice: "auto",
72 },
73};
74
75VoxEngine.addEventListener(AppEvents.CallAlerting, async ({call}) => {
76 let voiceAIClient;
77 let transferCall;
78 let transferInProgress = false;
79 let pendingHangup = false;
80
81 call.addEventListener(CallEvents.Disconnected, () => VoxEngine.terminate());
82 call.addEventListener(CallEvents.Failed, () => VoxEngine.terminate());
83
84 try {
85 call.answer();
86 // call.record({hd_audio: true, stereo: true}); // Optional: record the call
87
88 voiceAIClient = await OpenAI.createRealtimeAPIClient({
89 apiKey: (await ApplicationStorage.get("OPENAI_API_KEY")).value,
90 model: "gpt-realtime",
91 onWebSocketClose: (event) => {
92 Logger.write("===OpenAI.WebSocket.Close===");
93 if (event) Logger.write(JSON.stringify(event));
94 VoxEngine.terminate();
95 },
96 });
97
98 voiceAIClient.addEventListener(OpenAI.RealtimeAPIEvents.SessionCreated, () => {
99 voiceAIClient.sessionUpdate(SESSION_CONFIG);
100 });
101
102 voiceAIClient.addEventListener(OpenAI.RealtimeAPIEvents.SessionUpdated, () => {
103 VoxEngine.sendMediaBetween(call, voiceAIClient);
104 voiceAIClient.responseCreate({instructions: "Hello! How can I help today?"});
105 });
106
107 voiceAIClient.addEventListener(
108 OpenAI.RealtimeAPIEvents.InputAudioBufferSpeechStarted,
109 () => {
110 Logger.write("===BARGE-IN: OpenAI.InputAudioBufferSpeechStarted===");
111 voiceAIClient.clearMediaBuffer();
112 }
113 );
114
115 // Handle function calls
116 voiceAIClient.addEventListener(
117 OpenAI.RealtimeAPIEvents.ResponseFunctionCallArgumentsDone,
118 async (event) => {
119 const payload = event?.data?.payload || event?.data || {};
120 const toolName = payload.name || payload.tool_name;
121 const toolCallId = payload.call_id || payload.callId;
122 const rawArgs = payload.arguments;
123
124 if (!toolName || !toolCallId) {
125 Logger.write("===TOOL_CALL_MISSING_FIELDS===");
126 Logger.write(JSON.stringify(payload));
127 return;
128 }
129
130 let args = {};
131 if (typeof rawArgs === "string") {
132 try {
133 args = JSON.parse(rawArgs);
134 } catch (error) {
135 Logger.write("===TOOL_ARGS_PARSE_ERROR===");
136 Logger.write(rawArgs);
137 Logger.write(error);
138 }
139 } else if (rawArgs && typeof rawArgs === "object") {
140 args = rawArgs;
141 }
142
143 Logger.write("===TOOL_CALL_RECEIVED===");
144 Logger.write(JSON.stringify({toolName, args}));
145
146 if (toolName === WEATHER_TOOL) {
147 const location = args.location || "Unknown";
148 const result = {
149 location,
150 temperature_f: 72,
151 condition: "sunny",
152 };
153 voiceAIClient.conversationItemCreate({
154 item: {
155 type: "function_call_output",
156 call_id: toolCallId,
157 output: JSON.stringify(result),
158 },
159 });
160 voiceAIClient.responseCreate({});
161 return;
162 }
163
164 if (toolName === HANGUP_TOOL) {
165 pendingHangup = true;
166 voiceAIClient.conversationItemCreate({
167 item: {
168 type: "function_call_output",
169 call_id: toolCallId,
170 output: JSON.stringify({status: "hangup_scheduled"}),
171 },
172 });
173 voiceAIClient.responseCreate({});
174 return;
175 }
176
177 if (toolName === WARM_TRANSFER_TOOL) {
178 if (transferInProgress) {
179 voiceAIClient.conversationItemCreate({
180 item: {
181 type: "function_call_output",
182 call_id: toolCallId,
183 output: JSON.stringify({status: "transfer_already_in_progress"}),
184 },
185 });
186 voiceAIClient.responseCreate({});
187 return;
188 }
189
190 transferInProgress = true;
191
192 const destination =
193 args.destination_number ||
194 args.destination ||
195 args.phone_number ||
196 args.number ||
197 DEFAULT_TRANSFER_NUMBER;
198 const delayMs = Number.isFinite(args.delay_ms)
199 ? args.delay_ms
200 : DEFAULT_TRANSFER_DELAY_MS;
201 const message = args.message || DEFAULT_TRANSFER_GREETING;
202
203 try {
204 transferCall = VoxEngine.callPSTN(destination, call.callerid());
205
206 transferCall.addEventListener(CallEvents.Connected, () => {
207 Logger.write(`===WARM_TRANSFER_CONNECTED=== ${destination}`);
208 transferCall.say(message);
209
210 setTimeout(() => {
211 try {
212 voiceAIClient.clearMediaBuffer();
213 call.stopMediaTo(voiceAIClient);
214 voiceAIClient.stopMediaTo(call);
215 VoxEngine.sendMediaBetween(call, transferCall);
216 voiceAIClient.close();
217 Logger.write("===WARM_TRANSFER_BRIDGED===");
218 } catch (bridgeError) {
219 Logger.write("===WARM_TRANSFER_BRIDGE_ERROR===");
220 Logger.write(bridgeError);
221 }
222 }, delayMs);
223 });
224
225 transferCall.addEventListener(CallEvents.Failed, (event) => {
226 Logger.write("===WARM_TRANSFER_FAILED===");
227 Logger.write(JSON.stringify(event));
228 transferInProgress = false;
229 });
230
231 voiceAIClient.conversationItemCreate({
232 item: {
233 type: "function_call_output",
234 call_id: toolCallId,
235 output: JSON.stringify({
236 status: "transfer_started",
237 destination,
238 delay_ms: delayMs,
239 }),
240 },
241 });
242 voiceAIClient.responseCreate({});
243 } catch (transferError) {
244 Logger.write("===WARM_TRANSFER_ERROR===");
245 Logger.write(transferError);
246 transferInProgress = false;
247 voiceAIClient.conversationItemCreate({
248 item: {
249 type: "function_call_output",
250 call_id: toolCallId,
251 output: JSON.stringify({error: "warm_transfer_failed"}),
252 },
253 });
254 voiceAIClient.responseCreate({});
255 }
256 return;
257 }
258
259 voiceAIClient.conversationItemCreate({
260 item: {
261 type: "function_call_output",
262 call_id: toolCallId,
263 output: JSON.stringify({error: `Unhandled tool: ${toolName}`}),
264 },
265 });
266 voiceAIClient.responseCreate({});
267 }
268 );
269
270 voiceAIClient.addEventListener(OpenAI.RealtimeAPIEvents.ResponseOutputAudioDone, () => {
271 if (!pendingHangup) return;
272 Logger.write("===HANGUP_AFTER_AGENT_AUDIO===");
273 pendingHangup = false;
274 call.hangup();
275 });
276
277 voiceAIClient.addEventListener(OpenAI.Events.WebSocketMediaEnded, () => {
278 if (!pendingHangup) return;
279 Logger.write("===HANGUP_AFTER_MEDIA_ENDED===");
280 pendingHangup = false;
281 call.hangup();
282 });
283
284 // Consolidated "log-only" handlers
285 [
286 OpenAI.RealtimeAPIEvents.ResponseCreated,
287 OpenAI.RealtimeAPIEvents.ResponseDone,
288 OpenAI.RealtimeAPIEvents.ResponseOutputAudioTranscriptDone,
289 OpenAI.RealtimeAPIEvents.ResponseFunctionCallArgumentsDelta,
290 OpenAI.RealtimeAPIEvents.ConnectorInformation,
291 OpenAI.RealtimeAPIEvents.HTTPResponse,
292 OpenAI.RealtimeAPIEvents.WebSocketError,
293 OpenAI.RealtimeAPIEvents.Unknown,
294 OpenAI.Events.WebSocketMediaStarted,
295 OpenAI.Events.WebSocketMediaEnded,
296 ].forEach((eventName) => {
297 voiceAIClient.addEventListener(eventName, (event) => {
298 Logger.write(`===${event.name}===`);
299 if (event?.data) Logger.write(JSON.stringify(event.data));
300 });
301 });
302 } catch (error) {
303 Logger.write("===UNHANDLED_ERROR===");
304 Logger.write(error);
305 voiceAIClient?.close();
306 VoxEngine.terminate();
307 }
308});