Example: Function calling

View as Markdown

For the complete documentation index, see llms.txt.

This example answers an inbound call, connects it to Ultravox, and handles client tool invocations via Ultravox data messages.

⬇️ Jump to the Full VoxEngine scenario.

Prerequisites

Tool invocation flow

Ultravox emits a ClientToolInvocation data message when the agent calls a client tool. The event payload includes:

  • toolName
  • invocationId
  • parameters (tool input)

The example listens for these events and returns a client_tool_result response using voiceAIClient.clientToolResult(...).

Tool response (from the example)
1voiceAIClient.clientToolResult({
2 type: "client_tool_result",
3 invocationId,
4 responseType: "tool-response",
5 agentReaction: "speaks",
6 result: JSON.stringify({ location, temperature_f: 72, condition: "sunny" }),
7});

Full VoxEngine scenario

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