Business Productivity
How to build a smart interactive voice response (IVR) call routing system
Many businesses operate Interactive Voice Response (IVR) systems that allow callers to use their telephones to interact with various business systems. IVRs work by providing prompts to callers and allowing them to respond using their own voice or via dual-tone multi frequency (DTMF) input on their phones. IVRs are commonly used in automated, self-service use cases to reduce the dependency on human interaction to obtain information or take specific actions. There is a high probability that you have interacted with an IVR if you have ever called a number to check your bank account balance, pay a bill, or get the status of a recent order.
Another common IVR use case is to allow callers to be routed to destinations such as departments or external vendors that are on different telephone systems or contact centers (e.g. “speak to an agent”). The Amazon Chime SDK PSTN Audio service integrates with Amazon Lex, and enables builders to create serverless applications that provide conversational interfaces for calls to or from the public switched telephone network (PSTN). The service supports bridging calls to PSTN phone numbers and Session Initiation Protocol (SIP) trunks using Amazon Chime Voice Connectors. In this blog post, we will show you how you can use these capabilities to create a simple IVR with Amazon Lex that can intelligently route calls between multiple telephone systems using the Amazon Chime SDK PSTN Audio service.
Call Routing with Amazon Chime SDK PSTN Audio
Overview of solution
Amazon Chime SDK PSTN Audio service works with an AWS Lambda function to provide programmable telephony. In this demo we will be using Amazon Lex’s to natural language processing (NLP) to interpret requests from a caller and use it to make a routing decision. Other conditions could be used such as the time of day, calling number, called number, or DTMF input. External data sources or services like Amazon DynamoDB can be accessed by the AWS Lambda function to inform the routing decision.
Intelligent IVRs use artificial intelligence to route calls, rather than utilizing a customer service representative. In the architecture below, Amazon Lex uses natural language processing to understand spoken commands, then routes a caller to the right destination. The Amazon Chime SDK PSTN Audio service integrates with Amazon Lex, and enables builders to create serverless applications that provide conversational interfaces for calls to or from the public switched telephone network (PSTN).
Prerequisites
Before getting started, you must have the following prerequisites:
- Node JS v12+ and npm installed
- yarn installed
- AWS Command Line Interface (AWS CLI) installed
- AWS Account with appropriate permissions
- AWS Service Quota allowance for Amazon Chime SDK Phone Numbers
This demo also assumes intermediate knowledge of the Amazon Chime SDK PSTN Audio service and Amazon Lex. As a primer, we recommend completing the AWS workshop: Building Telephony-Powered Applications with the Amazon Chime SDK PSTN Audio Service. For background information on Amazon Lex and the terminology and features used in this demo, please see Amazon Lex: How It Works.
Walkthrough
The sample code referenced in this blog post will deploy a functional IVR demo that you can configure to route calls to either a Public Switched Telephone Network (PSTN) number or directly to an Amazon Chime SDK Voice Connector. This routing decision will be made based on the information passed from the Amazon Lex bot to the Amazon Chime SDK SIP media application. Calls routed to an Amazon Chime Voice Connector will contain additional Session Initiation Protocol (SIP) headers to pass information to the SIP user agent.
At a high level, the demo consists of two components:
- Web-based SIP client that is deployed locally and can be used to answer the incoming call.
- Back-end resources that include:
- AWS Lambda functions
- Amazon Chime Voice Connectors and SIP media applications
- Amazon DynamoDB Table
- Amazon Simple Storage Service (Amazon S3) Bucket
- Amazon Lex bot
- Amazon Elastic Cloud Compute (EC2)Instance and AWS Networking components used for the Asterisk Private Branch Exchange (PBX) phone system.
Deploy Back-end Resources
Clone the demo repository and run yarn launch to deploy:
git clone https://github.com/aws-samples/amazon-chime-pstn-audio-with-amazon-lex-ivr
cd amazon-chime-pstn-audio-with-amazon-lex-ivr
yarn launch
After deploying the AWS Cloud Development Kit (CDK), a United States telephone number will be provided to you as one of the outputs:
Outputs:
ChimeLexIVR.pstnPhoneNumber = +1NPANXXXXXX
Deploy Web-based SIP Client
Change to the site directory and launch the web-based SIP client locally:
cd site
yarn
yarn run start
This will start a local server that can be accessed at http://localhost:8080
Note: Deploying this demo and receiving traffic from the demo created in this post can incur AWS charges. Be sure to deprovision unused components when complete to avoid excess charges.
How It Works
Pre-loading session information to the Amazon Lex bot using Amazon Chime SDK PSTN Audio
When the phone number (ChimeLexIVR.pstnPhoneNumber
) is called, it will be delivered to an Amazon Chime SDK PSTN Audio SIP media application. When the SIP media application answers, we will preload information into the Amazon Lex session, then the startBotConversation action will connect the call to an Amazon Lex bot.
async function startSessions(event) {
const putSessionCommandParams = {
botAliasId: lexBotAliasId,
botId: lexBotId,
localeId: 'en_US',
sessionId: event.CallDetails.Participants[0].CallId,
sessionState: {
SessionAttributes: {
phoneNumber: event.CallDetails.Participants[0].From,
},
intent: {
name: 'RouteCall',
},
dialogAction: { type: 'ElicitSlot', slotToElicit: 'Department' },
},
};
try {
console.log(JSON.stringify(putSessionCommandParams, null, 3));
await lexClient.send(new PutSessionCommand(putSessionCommandParams));
return true;
} catch (err) {
console.log(`Error: ${err}`);
}
}
When the Amazon Chime SDK PSTN Audio SIP media application starts the Lex bot conversation, it will use the CallId
of the caller as the sessionId
within the Lex bot. We will use this to prepopulate the slot to be used when the conversation actually starts. In this case, we will use putSession
to tell the Lex bot to elicit a specific slot
on a specific intent
when the caller joins the conversation. This will allow us to immediately capture the department name from the caller.
exports.handler = async (event, context, callback) =>{
console.log('Lambda is invoked with calldetails:' + JSON.stringify(event));
let actions;
switch (event.InvocationEventType) {
case 'NEW_INBOUND_CALL':
console.log('NEW INBOUND CALL');
await startSessions(event);
speakAction.Parameters.CallId = event.CallDetails.Participants[0].CallId;
startBotConversationAction.Parameters.Configuration.SessionState.SessionAttributes.phoneNumber = event.CallDetails.Participants[0].From;
actions = [speakAction, startBotConversationAction];
break;
Amazon Lex Bot Processing
The Amazon Lex bot built in the demo contains two intents:
Because we used putSession to elicit the Department
slot on the RouteCall
intent, we will be using that slot and looking for values within that slot.
Once the Amazon Lex Bot captures the slot, we will need to verify that the Department spoken is a valid department. In order to do this, an optional code hook is used to perform validation. This code hook is an associated AWS Lambda function that will be executed at every turn of the conversation.
Slot Validation
Once the Slot has been captured, validation will occur using a AWS Lambda function associated to the Amazon Lex bot.
def RouteCall(intent_request):
session_attributes = get_session_attributes(intent_request)
slots = get_slots(intent_request)
department = get_slot(intent_request, "Department")
query_department = get_department(department)
if query_department:
print('querying department')
text = "Connecting you to " + department + " department."
message = {"contentType": "PlainText", "content": text}
fulfillment_state = "Fulfilled"
return close(session_attributes, "RouteCall", fulfillment_state, message)
else:
if 'failure_count' in session_attributes:
print(f"Failure Count: {session_attributes['failure_count']}")
if int(session_attributes['failure_count']) >= 2:
text = "Sorry, I couldn't find that department. Let me connect you to an operator."
message = {"contentType": "PlainText", "content": text}
fulfillment_state = "Fulfilled"
return close(session_attributes, "RouteCall", fulfillment_state, message)
else:
session_attributes['failure_count'] = int(session_attributes['failure_count']) + 1
try_ex(lambda: slots.pop("Department"))
text = "Sorry, I couldn't find that department. Can you try again?"
message = {"contentType": "PlainText", "content": text}
return elicit_slot(session_attributes, intent_request["sessionState"]["intent"]["name"],slots,"Department",message)
else:
session_attributes['failure_count'] = 1
try_ex(lambda: slots.pop("Department"))
text = "Sorry, I couldn't find that department. Can you try again?"
message = {"contentType": "PlainText", "content": text}
return elicit_slot(session_attributes, intent_request["sessionState"]["intent"]["name"],slots,"Department",message)
This AWS Lambda function will get the department spoken from the slot and compare that to a list of department names stored in an Amazon DynamoDB table pre-populated with sample department names (deployed as part of the CDK). The Amazon Lex CodeHook function is invoked with a JSON event that will be parsed. Below is a truncated example of the event. If a department
is not found in slots, a failure will be recorded and tracked using the session attributes of the Amazon Lex bot. A failure will result in a retry of the slot. When the failure_count
exceeds 2, the call will be completed as closed with a default route.
{
"sessionState": {
"sessionAttributes": {},
"activeContexts": [],
"intent": {
"slots": {
"Department": {
"shape": "Scalar",
"value": {
"originalValue": "history",
"resolvedValues": ["history"],
"interpretedValue": "history"
}
}
},
"confirmationState": "None",
"name": "RouteCall",
"state": "InProgress"
},
"originatingRequestId": "7be9976e-50df-4e07-9d99-670445c55102"
}
}
department
will be set to the interpretatedValue
here. In this case, department
will be history
. This value is then queried against the Amazon DynamoDB table to see if it is a valid department.
def get_department(department_name):
try:
response = dynamodb_client.get_item(
Key={
"department_name": {
"S": str(department_name),
},
},
TableName=department_table,
)
if "Item" in response:
return True
else:
return False
except Exception as err:
logger.error("DynamoDB Query error: failed to fetch data from table. Error: ", exc_info=err)
return None
When history
is queried in the Amazon DynamoDB table, it returns a True
value which will cause the AWS Lambda function to return the following to the Lex bot:
{
"messages": [
{
"contentType": "PlainText",
"content": "Connecting you to history department."
}
],
"sessionState": {
"dialogAction": { "type": "Close" },
"sessionAttributes": {},
"intent": { "name": "RouteCall", "state": "Fulfilled" }
}
}
This JSON will tell the Amazon Lex bot the intent has been fulfilled and should be closed and play the Connecting you to history department
message.
Return to SIP media application
Once the RouteCall intent has been closed in the Amazon Lex bot, the information will be returned to the Amazon Chime SDK SIP media application and the associated AWS Lambda function will be invoked with an InvocationEventType: ACTION_SUCCESSFUL with Type: StartBotConversation indicating that the Amazon Lex processing was completed successfully.
The AWS Lambda function associated with the Amazon Chime SDK SIP media application will then use the information it received to determine how to route the call:
case 'ACTION_SUCCESSFUL':
console.log('ACTION SUCCESSFUL');
switch (event.ActionData.Type) {
case 'StartBotConversation':
console.log('StartBotConversation Success');
const callerIdNumber = event.CallDetails.Participants[0].From;
let lexDepartment;
let route = {};
if ('Department' in event.ActionData.IntentResult.SessionState.Intent.Slots ) {
lexDepartment = event.ActionData.IntentResult.SessionState.Intent.Slots.Department.Value.InterpretedValue;
route = await getRoute(lexDepartment);
console.log(`Route from DynamoDB: ${route}`);
console.log(`lexDepartment from event: ${lexDepartment}`);
} else {
lexDepartment = 'Unknown';
route.service = 'voiceConnector';
route.number = '000000';
}
switch (route.service) {
case 'voiceConnector':
console.log('Using Amazon Chime Voice Connector Routing');
vcCallAndBridgeAction.Parameters.CallerIdNumber = callerIdNumber;
vcCallAndBridgeAction.Parameters.SipHeaders['X-LexInfo'] = lexDepartment;
vcCallAndBridgeAction.Parameters.Endpoints[0].Uri = route.number;
actions = [vcCallAndBridgeAction];
break;
case 'pstnNumber':
console.log('Using PSTN Routing');
pstnCallAndBridgeAction.Parameters.CallerIdNumber = callerIdNumber;
pstnCallAndBridgeAction.Parameters.Endpoints[0].Uri = route.number;
actions = [pstnCallAndBridgeAction];
break;
default:
break;
}
break;
When the SIP media application determines that the ACTION_SUCCESSFUL
is for a StartBotConversation
ActionData Type, the AWS Lambda function will extract the lexDepartment
from the json in the event if it exists.
"SessionState": {
"SessionAttributes": {},
"Intent": {
"Name": "RouteCall",
"Slots": {
"Department": {
"Value": {
"OriginalValue": "history",
"InterpretedValue": "history",
"ResolvedValues": ["history"]
},
"Values": []
}
},
"State": "Fulfilled",
"ConfirmationState": "None"
}
},
It will then query the associated Department Amazon DynamoDB table to determine which service to use to route the call and the number to use as the Uri when making this call. In the above Table example, all of the calls will be routed to the Amazon Chime Voice Connector.
Using CallAndBridgeAction to a Voice Connector
The vcCallAndBridgeAction
and pstnCallAndBridgeAction
will both bridge calls, however, the vcCallAndBridgeAction
will allow you to send calls to an Amazon Chime Voice Connector instead of a PSTN number. This will allow you to send additional SIP headers in the INVITE.
{
"SchemaVersion": "1.0",
"Actions": [
{
"Type": "CallAndBridge",
"Parameters": {
"CallTimeoutSeconds": 30,
"CallerIdNumber": "+1NPANXXXXXX",
"Endpoints": [
{
"Uri": "600300",
"BridgeEndpointType": "AWS",
"Arn": "arn:aws:chime:us-east-1:104621577074:vc/ehwryzdm9hy4u6rrud7jym"
}
],
"SipHeaders": {
"X-LexInfo": "history",
}
}
}
]
}
In this example, the call is placed with a URI containing 600300
and delivered to an Amazon Chime Voice Connector which will deliver the call to the associated host.
As shown in this example, the department captured in the Amazon Lex bot will be delivered as a SIP header to the Asterisk server that is built as part of this deployment. The Request URI will contain the URI defined in the CallAndBridge
action combined with the Inbound route of the Amazon Chime Voice Connector.
INVITE sip:600300@192.0.2.31:5060;transport=UDP SIP/2.0
From: <sip:+1NPANXXXXXX@10.0.174.226:5060>;tag=56Htvc9UXX7Ug
To: <sip:600300@192.0.2.31:5060>;transport=UDP
X-Lexinfo: history
X-SMA-Max-Forwards: 4
X-VoiceConnector-ID: ehwryzdm9hy4u6rrud7jym
X-Amzn-TargetArn: arn:aws:chime:us-east-1:104621577074:vc/ehwryzdm9hy4u6rrud7jym
SIP Client
Also included in this demo is a web-based SIP client that can optionally be used to answer the incoming call. This SIP client can be used to answer the call and establish two-way audio.
Audio Playback
If the included SIP client is not used, the Asterisk server will answer the call and play back a wav file to the caller based on the department captured in the Amazon Lex bot. During the EC2 instance deployment, a Python script is executed as part of the user-data passed to the instance. This Python script will create wav files by calling Polly using Boto3.
def createPolly(pollyText, fileName): response = polly.synthesize_speech( OutputFormat='pcm', Text=pollyText, SampleRate='8000', VoiceId='Joanna' )
if 'AudioStream' in response: outputWav = '/var/lib/asterisk/sounds/en/' + fileName + '.wav' with wave.open(outputWav, 'wb') as wav_file: wav_file.setparams((1, 2, 8000, 0, 'NONE', 'NONE')) wav_file.writeframes(response['AudioStream'].read()) return outputWav
Using CallAndBridgeAction to a Phone Number
To route a call to a PSTN number, in the Amazon DynamoDB Table, change the service to pstnNumber and change the number to an E.164 number. For example:
Cleaning up
To avoid incurring future charges, please clean up resources by running the following command from your terminal window:
yarn destroy
Conclusion
This blog post has demonstrated the benefits of integrating the programmatic telephony routing capabilities of Amazon Chime SDK PSTN Audio service with conversational interfaces powered by Amazon Lex. We’ve shown how these services can be used to build an IVR that can help intelligently route callers to multiple telephone systems, like a contact center, over Amazon Chime Voice Connector SIP trunking or the PSTN.
The Amazon Chime SDK PSTN audio is available in US-East-1 and US-West-2 AWS regions. To learn more about the PSTN Audio conversational voice experience and call bridging, refer to the Amazon Chime SDK PSTN Audio Developer Guide and visit the GitHub Repository for the sample code.