Retellai: Callback Routing
This tutorial is based on using JavaScript based Container Applications. Make sure that you choose the correct runtime
for your container application.
In this tutorial, we will learn how to create a Retellai callback routing script using the Cloudonix Container Application Templates (CXML) language. Callback routing is highly useful when working in a contact center scenario, when more control over the participants of an on-going call is required. In this scenario, the inbound call from Reteall and the original inbound call are connected to a conference room.
The Source Code
Inbound call from the world handling
- The provided script will handle any inbound call
- The provided script will insert the inbound caller to a designted conference room
- The provided script will handle the generate a callback from Reteall to Cloudonix
const httpClient = require('https');
/* Cloudonix API Config */
const cloudonix_token = '{{ Your Cloudonix Domain Application Token}}';
/* ReTell API Config */
const retell_token = '{{ Your Retell API Token }}';
const retell_agent = '{{ Your Retell Agent ID }}';
async function httpsRequest(url, method = 'GET', headers = {}, body = null, requestTimeout = 5000) {
const start = Date.now();
let response;
if (method === 'POST') {
response = await httpClient.post(url, { headers, body });
} else if (method === 'PUT') {
response = await httpClient.put(url, { headers, body });
} else if (method === 'DELETE') {
response = await httpClient.delete(url, { headers });
} else if (method === 'GET') {
response = await httpClient.get(url, { headers });
} else {
throw new Error(`Unsupported method: ${method}`);
}
const elapsed = Date.now() - start;
if (elapsed > requestTimeout) {
console.log(`[httpsRequest] HTTP Request ${method} to ${url} timed out after ${elapsed} mSec`);
return false;
}
const status = response.status || response.statusCode || 0;
const ok = status >= 200 && status < 300;
const responseBody = typeof response.body === 'string'
? response.body
: JSON.stringify(response.body || {});
return {
status,
ok,
body: responseBody,
headers: response.headers || {}
};
}
// Create a new phone call from Reteall to Cloudonix
async function generateReTellCall(ev, conferenceHash) {
const url = 'https://api.retellai.com/v2/create-phone-call';
// SIP headers from Cloudonix
const inboundSipHeaders = JSON.parse(ev.body).SessionData.profile['trunk-sip-headers'];
const retellDynamicVariables = {
cloudonix_callerid: ev.parameters.From,
cloudonix_destination: ev.parameters.To,
cloudonix_token: ev.parameters.Session,
cloudonix_domain: ev.parameters.Domain,
cloudonix_conference_hash: conferenceHash,
cloudonix_conference: conferenceRoom
};
const retellCustomSipHeaders = {
"X-Cloudonix-Conference-Hash": conferenceHash,
"X-Cloudonix-Inbound-Token": cloudonix_token
}
for (const key in inboundSipHeaders) {
retellDynamicVariables[key] = inboundSipHeaders[key];
}
const payload = {
agent_id: retell_agent,
from_number: ev.parameters.From,
to_number: ev.parameters.To,
retell_llm_dynamic_variables: retellDynamicVariables,
custom_sip_headers: retellCustomSipHeaders
};
console.log(`[generateReTellCall] Payload: ${JSON.stringify(payload)}`);
const headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + retell_token
};
try {
const response = await httpsRequest(url, 'POST', headers, JSON.stringify(payload));
if (!response.ok) {
throw new Error(`ReTell call creation failed: ${response.status} - ${response.body}`);
}
const data = JSON.parse(response.body);
console.log(`[generateReTellCall] Success: ${data.call_id}`);
return data.call_id;
} catch (error) {
console.error(`[generateReTellCall] Error:`, error.message || error);
await deleteSession(ev.parameters.Domain, ev.parameters.Session);
return false;
}
}
// Delete a currently active session (hangup the call)
async function deleteSession(domain, session) {
const url = `https://api.cloudonix.io/customers/self/domains/${domain}/sessions/${session}`;
const headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + cloudonix_token
};
console.log(`[deleteSession] Sending DELETE to ${url}`);
const response = await httpsRequest(url, 'DELETE', headers);
if (!response.ok) {
console.error(`[deleteSession] Failed: ${response.status} ${response.body}`);
return false;
}
console.log(`[deleteSession] Success: ${response.status}`);
return true;
}
function buildConferenceCxml(ev, conferenceHash) {
let result = "<Play>https://s3.us-east-2.amazonaws.com/sounds.cloudonix.io/silence/750-milliseconds-of-silence.mp3</Play>";
result += `<Dial callerId="${ev.parameters.From}">\n`;
result += ` <Conference holdMusic="false" endConferenceOnExit="true" beep="false">${conferenceHash}</Conference>`;
result += `</Dial>`;
return result;
}
function response(content) {
let result = `<?xml version="1.0"?>`;
result += `<Response>${content}</Response>`;
return result;
}
exports.handler = async (ev) => {
// Create a unique confernce hash, based on the inbound session ID
let conferenceHash = ev.parameters.Session.slice(-12);
console.log(`[handler] Conference Hash: ${conferenceHash}`);
// Generate an outbound call from Retell to Cloudonix
const retellResult = await generateReTellCall(ev, conferenceHash);
if (!retellResult) {
console.log(`[handler] ReTell call generation failed with result ${retellResult}`);
return response("<Reject reason='rejected' />");
}
// Insert the inbound caller to the designated conference room
const cxmlResponse = buildConferenceCxml(ev, conferenceHash);
return response(cxmlResponse);
};
In-Depth Explanation
- The
httpsmodule is imported to enable HTTP requests functionality. - Configuration variables for the Retell API, including the API token and agent ID, are established.
- A custom function named
httpsRequestis created to facilitate HTTP requests, incorporating specific timeout settings and mechanisms for handling errors. - The function
getRetellCallIdis defined to initiate a Retell call using data from a Cloudonix session. - The
dialToRetellfunction is designed to generate an XML response that includes the Dial verb, directing the call towards the specified Retell agent. - A primary function,
handler, is outlined to process Cloudonix events, employing the previously defined functions to craft the necessary response. - Within the
handlerfunction,getRetellCallIdis invoked to obtain a unique ID for the Retell call. - Should the attempt to generate a Retell call ID fail, the process is designed to terminate the call.
- In cases of success, an XML response is formulated using the Dial verb to ensure the call is forwarded to the Retell agent.
- This XML response is then returned to Cloudonix to complete the call routing process.
Remember to replace the placeholders ({{ Your Retell API Token }}, {{ Your Retell Agent ID }}, {{ Your Cloudonix Domain Application Token }}) with your actual Retell API token, Retell agent ID and Cloudonix Domain Application Key.
Inbound call from Retell handling
- The provided script will handle any inbound call that is received from Retell
- The provided script will insert the inbound call from the AI Agent to the pre-designated conference room
function buildConferenceCxml(ev, conferenceRoom) {
let result = `<Dial callerId="${ev.parameters.From}">\n`;
result += ` <Conference holdMusic="false" endConferenceOnExit="true" beep="false">${conferenceRoom}</Conference>`;
result += `</Dial>`;
return result;
}
function response(content) {
let result = `<?xml version="1.0"?>`;
result += `<Response>${content}</Response>`;
return result;
}
exports.handler = async (ev) => {
// Extract the conference room from the session data
// This assumes that the conference room is included in the session data in the form of "Cloudonix-Conference-Hash"
const conferenceHash = JSON.parse(ev.parameters.SessionData).profile['trunk-sip-headers']['Cloudonix-Conference-Hash'];
console.log(`[handler] Received event conference hash ${conferenceHash}`);
// Build the CXML response to initiate a conference with the specified conference room
const retellRoutingCxml = buildConferenceCxml(ev, conferenceHash);
return response(retellRoutingCxml);
};
In-Depth Explanation
- The
buildConferenceCxmlfunction is designed to construct an XML response that includes the Dial verb, directing the call towards the specified conference room. - The
responsefunction is used to construct an XML response that includes the Response verb, enclosing the provided content. - The
handlerfunction is designed to process Cloudonix events, specifically handling inbound calls. - Within the
handlerfunction, the conference room is extracted from the session data and used to construct the CXML response. - This XML response is then returned to Cloudonix to complete the call routing process.