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);
7require(Modules.ApplicationStorage);
8
9const SYSTEM_PROMPT = `You are a helpful phone assistant for Voximplant callers.
10If you need external data, call the get_weather tool.
11If the caller wants to end the call, call the hangup_call tool.`;
12
13// -------------------- Ultravox WebSocket API settings --------------------
14const AGENT_CONFIG = {
15 systemPrompt: SYSTEM_PROMPT,
16 model: "ultravox-v0.7",
17 voice: "Mark",
18};
19const WEATHER_TOOL_NAME = "get_weather"; // Needs to be defined in the Ultravox portal
20const HANGUP_TOOL_NAME = "hangUp"; // Built-in Ultravox tool
21
22VoxEngine.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});