Writing a Voice Bot Application Using Avaya CPaaS

“I’m interested in the moment when two objects collide and generate a third. The third object is where the interesting work is.”

Bruce Mau

In my previous article, Building Communications Applications Using Avaya OneCloud CPaaS, I presented three ways (using two programming language) of how to use Avaya CPaaS to create an outbound SMS text campaign/appointment reminder application. The application was launched from the command line and read telephone numbers from an Excel spreadsheet. Every number was used to sent a custom text message. To accomplish all this, the software used the CPaaS Send SMS REST API.

In most cases, the CPaaS REST APIs are used in proactive situations — send a text, make a call, buy a telephone number, retrieve information about a telephone number, create a SIP domain, etc. To handle reactive, asynchronous situations like receiving and responding to a text or a telephone call, you need inboundXML. inboundXML allows an application to publish a webhook that is dynamically invoked by CPaaS. The application can then process CPaaS requests (e.g. an incoming telephone call or text message) and respond with “next step” instructions — play announcement, collect speech/digits, transfer call, send SMS, etc.

For a deeper look into inboundXML processing, please watch my video in this article — Writing Voice and Text Applications with Avaya CPaaS inboundXML.

To help programmers get started using inboundXML, I’ve written a node.js application that is triggered on an inbound telephone call, asks the caller to “Say something,” and depending on what it hears, execute a few simple commands.

Here are the words the application is listening for and the actions it will take:

“Goodbye” : This will release the telephone call.

“Redirect” : This will redirect CPaaS to new webhook and then release the call.

“Play” : This will instruct the application to play a Hayden concerto to the caller.

“Ring” : The application will play two seconds of ringing.

“Dial tone” : The application will play two seconds of dial tone.

“Transfer” : The caller will be transferred to a new destination.

If anything else is said, the application will repeat what it heard and then wait for the caller to say something.

While the application doesn’t perform anything particularly useful, it demonstrates a number of important inboundXML elements and provides a basic framework that can be used to create much more interesting solutions. Once you understand what is happening, it’s easy to see how this could become the start of a CPaaS cloud IVR.

I hosted the application on my Ubuntu Linux server. You can experience it in action by calling 606-506-8390.

Notes:

I use node.js Express for the application framework. This provides an easy way to create POST and GET entry points.

CPaaS passes the speech-to-text response to the application in SpeechResult.

Rather than building the inboundXML elements from scratch, I use the Avaya CPaaS node.js helper library.

There are only a few things you need to change in order to host it on your own server. Pay attention to the three constants near the top and change them to your own values.

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const cpaas = require('@avaya/cpaas'); //Avaya cloud
var enums = cpaas.enums;
var ix = cpaas.inboundXml;

// Replace the following with your own values
const HOST = "xx.xx.xx.xx"; // Application IP address
const URL_PORT = xxxx; // Application port
const TRANSFER_NUMBER = "+1xxxxxxxxxx"; // Ten digit transfer telephone number

const REQUEST_URL = "http://" + HOST + ":" + URL_PORT.toString() + "/cpaas-request/";
const REDIRECT_URL = "http://" + HOST + ":" + URL_PORT.toString() + "/cpaas-redirect/";

var app = express();

// Middleware to parse JSON
app.use(bodyParser.urlencoded({
	extended: true
}));
app.use(bodyParser.json());
app.use(cors());

// Tell server to listen on port
var server = app.listen(URL_PORT, function() {
	var host = server.address().address;
	var port = server.address().port;
});

// Entry point for a GET from a web browser
app.get('/', function(req, res) {
	res.send("The CPaaS virtual agent is running.");
});

//Entry point for voice
app.post('/cpaas-request/', function(req, res) {
	//CPaaS Inbound XML Parameters	
	var from = req.body.From;
	var to = req.body.To;

	var hangup = false;
	var prompt = "Say something";
	if ("SpeechResult" in req.body) {
		prompt = "I heard you say " + req.body.SpeechResult;
		if (req.body.SpeechResult == "goodbye") {
			hangup = true;
			prompt = "Goodbye";
		} else if (req.body.SpeechResult == "transfer") {
			processTransfer("You are about to be transferred.", res);
			return;
		} else if (req.body.SpeechResult == "redirect") {
			processRedirect(res);
			return;
		} else if (req.body.SpeechResult == "play") {
			processPlay(res);
			return;
		} else if (req.body.SpeechResult == "dial tone") {
			processTone("tone_stream://%(2000,0,350,440)", res);
			return;
		} else if (req.body.SpeechResult == "ring") {
			processTone("tone_stream://%(2000,4000,440,480)", res);
			return;
		}
	}
	sendResponseToCPaaS(prompt, hangup, res);
});

//Entry point for redirect
app.post('/cpaas-redirect/', function(req, res) {
	sendResponseToCPaaS("This is the redirect XML web hook.  Goodbye!", true, res);
});

async function processRedirect(res) {
	var xmlDefinition = generateXMLRedirect();
	var serverResponse = await buildCPaaSResponse(xmlDefinition);
	res.type('application/xml');
	res.send(serverResponse);
}

function generateXMLRedirect() {
	var xml_content = [];
	var redirect = ix.redirect({
		url: REDIRECT_URL
	});
	xml_content.push(redirect);
	var xmlDefinition = ix.response({
		content: xml_content
	});
	return xmlDefinition;
}

async function processTone(tone, res) {
	var xmlDefinition = generateTone();
	var serverResponse = await buildCPaaSResponse(xmlDefinition);
	res.type('application/xml');
	res.send(serverResponse);
}

function generateTone(tone) {
	var xml_content = [];
	var gather = ix.gather({
		action: REQUEST_URL,
		method: "POST",
		input: "speech",
		timeout: "20",
		content: [
			ix.play({
				loop: 1,
				url: tone
			}),
			ix.say({
				language: enums.Language.EN,
				text: "Say something",
				voice: enums.Voice.FEMALE
			})
		]
	});
	xml_content.push(gather);
	var xmlDefinition = ix.response({
		content: xml_content
	});
	return xmlDefinition;
}

async function processPlay(res) {
	var xmlDefinition = generateXMLPlay();
	var serverResponse = await buildCPaaSResponse(xmlDefinition);
	res.type('application/xml');
	res.send(serverResponse);
}

function generateXMLPlay() {
	var xml_content = [];
	var play = ix.play({
		loop: 0,
		url: "http://www.hochmuth.com/mp3/Haydn_Adagio.mp3"
	});
	xml_content.push(play);
	var xmlDefinition = ix.response({
		content: xml_content
	});
	return xmlDefinition;
}

async function processTransfer(prompt, res) {
	var xmlDefinition = generateXMLTransfer(prompt);
	var serverResponse = await buildCPaaSResponse(xmlDefinition);
	res.type('application/xml');
	res.send(serverResponse);
}

function generateXMLTransfer(prompt) {
	var responseXML = ix.response({
		content: [
			ix.say({
				language: enums.Language.EN,
				text: prompt,
				voice: enums.Voice.FEMALE
			}),
			ix.dial({
				content: [
					ix.number({
						number: TRANSFER_NUMBER
					})
				]
			})
		]
	});
	return responseXML;
}

async function sendResponseToCPaaS(prompt, release, res) {
	var xmlDefinition = generateXMLSpeech(REQUEST_URL, prompt, release);
	var serverResponse = await buildCPaaSResponse(xmlDefinition);
	res.type('application/xml');
	res.send(serverResponse);
}

function generateXMLSpeech(request_url, prompt, hangup) {
	var xml_content = [];
	if (hangup == false) {
		var gather = ix.gather({
			action: request_url,
			method: "POST",
			input: "speech",
			timeout: "20",
			content: [
				ix.say({
					language: enums.Language.EN,
					text: prompt,
					voice: enums.Voice.FEMALE
				})
			]
		});
		xml_content.push(gather);
	} else {
		var say = ix.say({
			language: enums.Language.EN,
			text: prompt,
			voice: enums.Voice.FEMALE
		});
		var hangup = ix.hangup();
		xml_content.push(say);
		xml_content.push(hangup);
	}
	var xmlDefinition = ix.response({
		content: xml_content
	});
	return xmlDefinition;
}

async function buildCPaaSResponse(xmlDefinition) {
	var result = await ix.build(xmlDefinition).then(function(xml) {
		return xml;
	}).catch(function(err) {
		console.log('The generated XML is not valid!', err);
	});
	return result;
}

Integrating Your Application with Avaya CPaaS

Once you’ve hosted and launched the application somewhere, you need to attach it to an Avaya CPaaS telephone number.  This is how I connected it to one of my numbers.  Notice how cpaas-request is the POST webhook exposed by the application as an app.post() function.  This will be the entry point for CPaaS Request messages.  Response messages will be returned to CPaaS in the Express res object.

inboundXML

Mischief Managed

That’s all there is to it.  As I wrote, this framework can be used for all sorts of cool applications.  I’ve used it to send the collected speech-to-text to Google DialogFlow for natural language processing.  In another solution, I invoked the CPaaS carrier lookup API to see if the caller was on a mobile device and if true, asked if he or she would like to switch to a digital experience.  The possibilities to innovate are endless.

Feel free to reach out to me if you have any questions.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: