Example: Function calling

View as MarkdownOpen in Claude

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});