Zum Hauptinhalt springen

Voice Agents JS Example (OpenAI Attended Transfer)

Voice Agents JS Example - OpenAI Attended Transfer

This example demonstrates how to use OpenAI as an intelligent screening assistant for attended transfers. Instead of manually calling colleagues to check availability, any employee can use the AI Voice Agents to automatically screen and connect calls.

The Problem

When someone answers a call and the caller asks for a specific person, they typically must:

  1. Put the caller on hold
  2. Call the destination person to check if they're available
  3. Relay information back and forth
  4. Manually connect the call or take a message

This is time-consuming and inefficient for everyone involved.

The Solution: One-Button Screening

This script enables attended transfer with AI screening. With a programmable button on your desk phone:

  1. Caller asks for someone: "I need to speak with James"
  2. Press the programmed button: Automatically puts caller on hold and dials the Voice Agents
  3. Tell the AI: "Find James" or "Check if James is available"
  4. AI handles screening: Calls James, asks if he wants to accept, gets response
  5. Automatic result: Call connects to James, or you get notified he's busy

Desk Phone Button Setup

Most VoIP desk phones (Yealink, Polycom, Snom, etc.) support programmable buttons that can:

  • Put the active call on hold
  • Dial the Voice Agents extension (e.g., 5000)

Example button configuration:

Button Label: "Screen Call"
Action: Transfer
Destination: 5000

When pressed, your current call goes on hold and the Voice Agents answers immediately, ready for your instruction.

How It Works:

Step 1: Incoming Call

  • Employee answers a call
  • Caller: "I'd like to speak with James"
  • Employee presses the "Screen Call" button (call automatically goes on hold)

Step 2: AI Voice Agents Activation

  • Button press dials the Voice Agents extension (e.g., 5000)
  • AI: "What would you like to do?"
  • Employee: "Find James" or "Transfer to James"
  • AI recognizes "James" and looks up extension 4001

Step 3: AI Screens the Destination

  • AI automatically calls James at extension 4001
  • James answers the AI call
  • AI: "There is a call for you from [Caller Name]. Would you like to accept it?"
  • James responds naturally: "Yes, put them through" OR "No, I'm busy right now"

Step 4: Automatic Connection

  • If James accepts: The original caller is automatically connected to James
  • If James rejects: Employee hears "Transfer failed" and can inform caller: "I'm sorry, James is unavailable. Can I take a message?"

Benefits:

  • One-Button Operation: Single button press handles everything
  • Time Savings: No manual dialing or waiting for colleagues to answer
  • Professional Screening: AI provides context (caller name) to help with decisions
  • Better Experience: Employees can decline calls when busy or in meetings
  • Reduced Hold Time: Faster resolution for callers
  • Universal: Works for receptionists, assistants, team members, or anyone with a desk phone

Alternative Use Case: Direct Caller Interaction

The same script also works when callers interact directly with the AI Voice Agents (without any employee answering first):

Direct Caller Workflow:

  1. Caller dials the company and reaches the AI Voice Agents directly
  2. AI: "How may I help you today?"
  3. Caller: "I need to speak with James"
  4. AI calls James with attended transfer (same screening process)
  5. If accepted → caller connected; if rejected → AI informs caller

This mode is useful for:

  • After-hours auto-attendant
  • Department direct lines
  • Self-service call routing
  • Reducing front-desk call volume

OpenAI Setup

Please refer to the documentation here for OpenAI setup, including configuring the project, webhook, API key, and then setting up the trunk and dialplan on Vodia.


Technical Overview

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

Mode 1: Reception/Concierge Mode (initial call)

  • Receptionist or caller speaks to AI
  • AI recognizes names/extensions from the directory map
  • When instructed to "find" or "transfer to" someone, AI initiates screening call

Mode 2: Attendant Mode (destination call)

  • The employee receives a call with ivraction: 'attendant'
  • AI announces the caller and asks if they want to accept
  • Employee responds naturally: "Yes", "Sure", "No", "I'm busy", etc.
  • AI calls call_accept() or call_reject() based on response
  • Result is sent back to the reception/concierge call

Key Technical Components

  • call.dial(): Initiates the outbound call to the employee with custom parameters
  • ivraction: 'attendant': Signals that this call should use attendant mode instructions
  • cobj: Passes the original call object to be connected after acceptance
  • call.find(): Retrieves active calls to get the original caller's information
  • call.transfer({ action: 'accept/reject' }): Completes or cancels the attended transfer
  • Webhook with body.type == 'att_transfer': Receives the result back in the concierge Voice Agents

Directory Mapping

In the script, you configure the name-to-extension mapping:

"Here is a map of words to numbers: { James: 4001, Rob: 402, Park 1: 90 }"

When the receptionist or caller says "James", the AI automatically resolves it to extension 4001.


Call Flow Diagrams

Desk Phone Button Workflow:

[Incoming Call] → [Employee Answers]

[Caller: "I need James"]

[Employee Presses "Screen Call" Button]

[Call Automatically Put on Hold + Voice Agents Dialed]

[AI Voice Agents: "What would you like to do?"]

[Employee: "Find James" or "Transfer to James"]

[AI recognizes "James" = 4001]

[AI calls James at 4001]

[AI to James: "Call from John Smith. Accept?"]

[James: "Yes" or "No"]

┌─────────────────┴─────────────────┐
↓ ↓
call_accept() call_reject()
↓ ↓
[Caller Connected to James] [Employee Notified: "Transfer failed"]

[Employee: "Sorry, James is unavailable"]

Direct Caller Workflow (Alternative):

[Caller] → [AI Voice Agents Auto-Answers: "How can I help?"]

[Caller: "I need to speak with James"]

[AI recognizes "James" = 4001]

[AI calls James at 4001]

[AI to James: "Call from John Smith. Accept?"]

[James: "Yes" or "No"]

call_accept() or call_reject()

[Connected or "James is unavailable"]

The Complete Script

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')

Configuration Steps

1. Set Up the Voice Agents Extension

  • Create an extension (e.g., 5000) with this JavaScript
  • Configure it as a "JavaScript Voice Agents" or "JavaScript Application"

2. Configure OpenAI API Key

  • Update the secret variable in the script with your OpenAI API key
  • Ensure your OpenAI account has access to the gpt-realtime model

3. Update the Directory Map

Configure the name-to-extension mapping for your organization:

"Here is a map of words to numbers: { James: 4001, Rob: 402, Sales: 100, Support: 200, IT Department: 300 }"

4. Program Desk Phone Buttons

Configure a button on your desk phones to streamline the process:

Yealink Example:

  • Button Type: Speed Dial or Transfer
  • Label: Screen Call
  • Value: 5000 (your Voice Agents extension)

Polycom Example:

  • Line Key: Speed Dial
  • Label: Screen
  • Number: 5000

Snom Example:

  • Function Key: Transfer
  • Number: 5000
  • Label: AI Screen

When this button is pressed:

  1. Current call is automatically put on hold
  2. Voice Agents extension is dialed
  3. Employee can immediately give instructions

5. Optional: Direct Routing

Set up direct routing for callers to reach the AI Voice Agents automatically:

  • Configure as after-hours destination
  • Set as department directory option
  • Use as overflow when no one answers

Real-World Benefits & Use Cases

  • Executive Screening: "There's a call from ABC Corp. Would you like to take it?"
  • Busy Checking: Employees can decline calls when in meetings or focused work
  • Privacy Protection: Decide who to speak with before being connected
  • Context Provided: AI announces caller information to help with decision-making
  • Professional Experience: More sophisticated than blind transfers
  • Office Efficiency: Anyone can screen calls with one button press
  • Reception Support: Front desk can handle multiple callers simultaneously
  • Team Coordination: Department members can screen for each other
  • Remote Workers: Cloud-based desk phones get the same functionality
  • After Hours: AI can handle screening when office is closed

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