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

  • 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 Ultravox API key in Voximplant Secrets under ULTRAVOX_API_KEY.
  • Configure tools in Ultravox (agent or call config) so tool invocations are emitted during the call.

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)
voiceAIClient.clientToolResult({
type: "client_tool_result",
invocationId,
responseType: "tool-response",
agentReaction: "speaks",
result: JSON.stringify({ location, temperature_f: 72, condition: "sunny" }),
});

Full VoxEngine scenario

voxeengine-ultravox-function-calling.js
/**
* Voximplant + Ultravox WebSocket API connector demo
* Scenario: answer an incoming call and handle client tool invocations.
*/
require(Modules.Ultravox);
const SYSTEM_PROMPT = `You are a helpful phone assistant for Voximplant callers.
If you need external data, call the get_weather tool.
If the caller wants to end the call, call the hangup_call tool.`;
// -------------------- Ultravox WebSocket API settings --------------------
const AGENT_CONFIG = {
systemPrompt: SYSTEM_PROMPT,
model: "ultravox-v0.7",
voice: "Mark",
};
const WEATHER_TOOL_NAME = "get_weather"; // Needs to be defined in the Ultravox portal
const HANGUP_TOOL_NAME = "hangUp"; // Built-in Ultravox tool
VoxEngine.addEventListener(AppEvents.CallAlerting, async ({call}) => {
let voiceAIClient;
let hangupAfterResponse = false;
let hangupTimer;
// Termination functions - add cleanup and logging as needed
call.addEventListener(CallEvents.Disconnected, ()=>VoxEngine.terminate());
call.addEventListener(CallEvents.Failed, ()=>VoxEngine.terminate());
try {
call.answer();
// call.record({ hd_audio: true, stereo: true }); // Optional: record the call
// Create client and wire media
voiceAIClient = await Ultravox.createWebSocketAPIClient(
{
endpoint: Ultravox.HTTPEndpoint.CREATE_CALL,
authorizations: {"X-API-Key": VoxEngine.getSecretValue('ULTRAVOX_API_KEY')},
body: AGENT_CONFIG,
onWebSocketClose: (event) => {
Logger.write("===ULTRAVOX_WEBSOCKET_CLOSED===");
if (event) Logger.write(JSON.stringify(event));
VoxEngine.terminate();
},
},
);
VoxEngine.sendMediaBetween(call, voiceAIClient);
// ---------------------- Event handlers -----------------------
// Function calling: handle tool requests and send back responses
voiceAIClient.addEventListener(
Ultravox.WebSocketAPIEvents.ClientToolInvocation,
(event) => {
const payload = event?.data?.payload || event?.data || {};
const {toolName, invocationId, parameters} = payload;
if (!toolName || !invocationId) return;
if (toolName !== WEATHER_TOOL_NAME && toolName !== HANGUP_TOOL_NAME) {
voiceAIClient.clientToolResult({
type: "client_tool_result",
invocationId,
errorType: "tool_not_found",
errorMessage: `Unhandled tool: ${toolName}`,
});
return;
}
if (toolName === WEATHER_TOOL_NAME) {
const location = parameters?.location || "Unknown";
const result = JSON.stringify({
location,
temperature_f: 72,
condition: "sunny",
});
voiceAIClient.clientToolResult({
type: "client_tool_result",
invocationId,
responseType: "tool-response",
agentReaction: "speaks",
result,
});
return;
}
if (toolName === HANGUP_TOOL_NAME) {
hangupAfterResponse = true;
voiceAIClient.clientToolResult({
type: "client_tool_result",
invocationId,
responseType: "tool-response",
agentReaction: "speaks",
result: JSON.stringify({ result: "Hanging up now. Goodbye!" }),
});
}
},
);
// Barge-in: keep conversation responsive and capture transcript
voiceAIClient.addEventListener(
Ultravox.WebSocketAPIEvents.Transcript,
(event) => {
const payload = event?.data?.payload || event?.data || {};
const role = payload.role;
const text = payload.text || payload.delta;
if (role && text) Logger.write(`===TRANSCRIPT=== ${role}: ${text}`);
if (role === "user") voiceAIClient.clearMediaBuffer();
if (hangupAfterResponse && role === "assistant" && payload.text) {
if (hangupTimer) clearTimeout(hangupTimer);
hangupTimer = setTimeout(() => call.hangup(), 1200);
}
},
);
voiceAIClient.addEventListener(
Ultravox.WebSocketAPIEvents.PlaybackClearBuffer,
() => voiceAIClient.clearMediaBuffer(),
);
// If the media stream ends after a goodbye response, hang up the call.
voiceAIClient.addEventListener(Ultravox.Events.WebSocketMediaEnded, () => {
if (hangupAfterResponse) call.hangup();
});
// Consolidated "log-only" handlers - key Ultravox/VoxEngine debugging events
[
Ultravox.WebSocketAPIEvents.ConnectorInformation,
Ultravox.WebSocketAPIEvents.HTTPResponse,
Ultravox.WebSocketAPIEvents.State,
Ultravox.WebSocketAPIEvents.Debug,
Ultravox.WebSocketAPIEvents.WebSocketError,
Ultravox.WebSocketAPIEvents.Unknown,
Ultravox.Events.WebSocketMediaStarted,
Ultravox.Events.WebSocketMediaEnded,
].forEach((eventName) => {
voiceAIClient.addEventListener(eventName, (event) => {
Logger.write(`===${event.name}===`);
Logger.write(JSON.stringify(event));
});
});
} catch (error) {
Logger.write("===SOMETHING_WENT_WRONG===");
Logger.write(error);
VoxEngine.terminate();
}
});