Zum Hauptinhalt springen

IVR JS Example (OpenAI Attended Transfer)

IVR JS Example - OpenAI Attended Transfer

This advanced example demonstrates how OpenAI can act as an intelligent attendant for attended transfers. The AI first answers as a concierge/receptionist, and when initiating a transfer, it acts as an intermediary that asks the destination party if they want to accept the call before connecting.

Setup:

  • Create a new project in OpenAI platform for your SIP connection to OpenAI.
  • Get the project id, and the related API key, to be used below.
  • Setup a webhook in this project - a URL that can reach your Vodia PBX - with the endpoint e.g.: https://YOUR-VODIA-TENANT.com/openai. The "/openai" is the default, but you can change it here and the PBX.
  • Create a new "OpenAI" trunk in Vodia PBX for connection to OpenAI. Make sure to enter the project id (taken above) as the username when creating the trunk.
  • Create a dialplan entry for use of that trunk with the Pattern as openai and replacement as empty.

How Attended Transfer Works:

This example uses a dual-mode approach where the same IVR script behaves differently based on the ivraction parameter:

  1. Concierge Mode (initial call):

    • Caller speaks to AI and requests to be transferred
    • AI recognizes the transfer request and extracts the destination
    • Instead of blind transfer, AI initiates a new call to the destination
  2. Attendant Mode (destination call):

    • The destination receives a call with ivraction: 'attendant'
    • AI announces the caller and asks if they want to accept
    • Based on the response, AI calls call_accept() or call_reject()
    • Result is sent back to the concierge IVR

Key Components:

  • call.dial(): Used to initiate the outbound call to the destination with custom parameters
  • ivraction: 'attendant': Signals that this call should use attendant mode instructions
  • cobj: Pass the original call object to be connected after acceptance
  • call.find(): Retrieve active calls to get the original caller's information
  • call.transfer({ action: 'accept/reject' }): Complete or cancel the attended transfer
  • Webhook with body.type == 'att_transfer': Receive the result back in the concierge IVR

Real-World Use Cases:

  • Executive screening: "There's a call from John Smith. Would you like to take it?"
  • Busy checking: AI can ask if the person is available before connecting
  • Privacy protection: Destination can decline calls before they're connected
  • Call context: AI can provide caller information to help with decision
  • Professional experience: More sophisticated than blind transfers
note

The model specified in the script is gpt-realtime. You must verify its current availability under your OpenAI plan. Additionally, check for potential rate limit/cost impacts and identify a suitable alternative model should the version be inaccessible

// OpenAI integration through SIP
// Auto-generated from AI Assistant configuration
'use strict'

var secret = "sk-proj-KEY"

var texts = {
initial: {
en: "Hi, I am your Vodia AI assistant. How may I help you today?"
}
}

function text(name) {
var prompt = texts[name]
if (call.lang in prompt) return prompt[call.lang]
return prompt["en"]
}

var timer = setTimeout(function() {
call.transfer('700')
}, 30000)

call.http(onhttp)
function onhttp (args) {
console.log('Open AI Ringing ... ')
console.log(JSON.stringify(args))
const body = JSON.parse(args.body)
console.log('Body: ')
console.log(JSON.stringify(body))
if (body.type == 'realtime.call.incoming') {
const callid = body.data.call_id
console.log('Call id: ')
console.log(callid)
system.http({
method: 'POST',
url: 'https://api.openai.com/v1/realtime/calls/' + callid + '/accept',
header: [
{ name: 'Authorization', value: 'Bearer ' + secret, secret: true },
{ name: 'Content-Type', value: 'application/json' }
],
body: JSON.stringify({
type: "realtime",
model: "gpt-realtime",
instructions: "You are a helpful assistant."
}),
callback: function(code, response, headers) {
connected(code, response, headers, callid)
}
})
}
else if (body.type == 'att_transfer') {
if (body.result == 'true') {
console.log('Att transfer succeeded')
call.say({ text: 'Attendant transfer succeeded', callback: function() { call.hangup() }})
}
else {
console.log('Att transfer failed')
call.say({ text: 'Attendant transfer failed', callback: function() { call.hangup() }})
}
}
}

function connected(code, response, headers, callid) {
var ws = new Websocket("wss://api.openai.com/v1/realtime?call_id=" + callid)
ws.header([
{ name: "Authorization", value: "Bearer " + secret, secret: true },
{ name: "User-Agent", value: "Vodia-PBX/69.5.3" }
])

ws.on('open', function() {
console.log("Websocket opened")
var instructions = ""
if (call.ivraction && call.ivraction == 'attendant') {
instructions = "A question has already been asked, whether they want to accept a call from someone. Please wait for a response: If they explicitly accept the call (like ok I will take it or yes or connect me etc.), then call the function call_accept. If they explicitly reject the call (like no, or don't connect me, or I can't take it right now etc.) then call the function call_reject. Do not call anything otherwise. Do not respond with normal text for these intents."
}
else {
instructions = "Here is a map of words to numbers: { James: 4001, Rob: 402, Park 1: 90 }. Whenever someone says call, transfer to, attendant tranfer, you MUST call the function 'transfer_call'. The function argument 'destination' must be the resolved number from the map and the function argument 'name' must be the name the person said. If the user provides a number directly, use it as-is, and with destiantion and name the same. Do not respond with normal text for these intents."
}
const update = {
"type": "session.update",
"session": {
"type": "realtime",
"instructions": instructions,
"tools": [
{
"type": "function",
"name": "transfer_call",
"description": "Transfers the active SIP call to a destination number",
"parameters": {
"type": "object",
"properties": {
"destination": {
"type": "string",
"description": "Phone number to transfer to"
},
"name": {
"type": "string",
"description": "Name to transfer to"
}
},
"required": ["destination", "name"]
}
},
{
"type": "function",
"name": "call_accept",
"description": "Accepts the call for attended transfer"
},
{
"type": "function",
"name": "call_reject",
"description": "Rejects the call for attended transfer"
}
],
"tool_choice": "auto"
}
}
ws.send(JSON.stringify(update))

var greeting = ""
if (call.ivraction && call.ivraction == 'attendant') {
console.log('Original from:')
console.log(call.orig_from)
const origname = call.orig_from.split('"')[1]
greeting = {
"type": "response.create",
"response": {
"instructions": "Greet with: " + "There is a call for you from " + origname + ". Would you like to accept it?"
}
}
}
else {
greeting = {
"type": "response.create",
"response": {
"instructions": "Greet with: " + "What would you like to do?"
}
}
}
ws.send(JSON.stringify(greeting))
})
ws.on('close', function() { console.log("Websocket closed") })

ws.on('message', function(message) {
//call.log('Websocket message:')
//call.log(message)
const evt = JSON.parse(message)
if (
evt.type === "response.output_item.done" &&
evt.item.type === "function_call"
) {
const args = JSON.parse(evt.item.arguments)
const destination = args.destination
console.log('Transfer to destination:')
if (typeof destination == 'string') {
console.log(destination)
}
if (evt.item.name === "transfer_call") {
const name = args.name
const calls = call.find('calls', call.extension)
if (calls.length > 0) {
const c = calls[0]
call.dial( { account: '88', dest: destination, from: c.from, cobj: c.cobj, ivraction: 'attendant' } )
}
}
else if (evt.item.name === "call_accept") {
console.log('Accept the attended transfer call')
call.transfer({ action: 'accept'} )
}
else if (evt.item.name === "call_reject") {
console.log('Reject the attended transfer call')
call.transfer({ action: 'reject'} )
}
}
})
ws.connect()
}

call.dial('openai')

Call Flow Diagram:

[Caller] → [AI Concierge] "I need to speak with James"

AI recognizes "James" = 4001

[AI initiates attended transfer]

[AI Attendant] → [James at 4001] "Call from John Smith. Accept?"

[James responds]

"Yes" → call_accept() → [Calls Connected]
"No" → call_reject() → [Transfer Failed notification]

This approach provides a much more professional experience than blind transfers, allowing the destination party to screen calls intelligently while maintaining natural conversation flow with both parties.

For more information on Vodia's JavaScript capabilities, refer to: Vodia Backend JavaScript Documentation