AWS Contact Center

Updating your addresses with Amazon Connect and Amazon Lex

When someone moves, they spend time notifying their service account providers (electric, water, insurance, etc.) to update their address information.
This post explains how to create an Amazon Lex bot in an Amazon Connect contact flow to automate the address update process. After you create the bot, you use AWS Lambda to confirm the new address and update your backend database.

Prerequisites

To follow along, you need the following services:

For more information, see Getting Started with Amazon Connect.

Create an Amazon Lex bot

This custom Amazon Lex bot example demonstrates the Press or Say integration with Amazon Connect. The bot prompts callers to press or say a number that matches the menu option for the task to complete.

  1. Open the Amazon Lex console in US West (Oregon) Region.
  2. If you are creating your first bot, choose Get Started. Otherwise, select Bots and choose Create.
  3. On the Create your bot page, choose Custom bot, and provide the following information:
    • In the Bot name box, enter the name “AddressHelper.”
    • In the Output voice box, select the voice for your bot to use when speaking to callers.
    • In the Session timeout boxes, enter how long the bot should wait to get input from a caller before ending the session.
    • Under COPPA, select whether the bot is subject to the Children’s Online Privacy Protection Act.
  4. Choose Create.

Configure the Amazon Lex bot

Determine how the bot responds to customers by providing intents, sample utterances, slots for input, and error handling.

Create intents

An intent is an action that a customer wants to perform. You can create a bot with one or more intents. Configure this bot with two intents: one to look up account information, and another to speak with an agent.

  1. Select the plus (+) icon next to Intents, and choose Create new intent.
  2. Name your intent “UpdateAddress.” Select Add.
  3. Repeat step 1 to create another intent, and name it “TalkToAnAgent.”

Add slots and sample utterances

The slots define the data that a customer must provide to fulfill the intent. The collection of utterances and slots matches the customer’s intent (based on their input) and determines what happens next. Use the following procedures to define the intents, and add slots and sample utterances:

  1. In the Amazon Lex console, open the AddressHelper bot.
  2. Select the UpdateAddress intent and choose Slots.
  3. On the Slots screen, for Name, enter zipCode.
  4. For Slot type, select AMAZON.NUMBER.
  5. For Prompt, enter the text for the bot to speak when it answers the call. In this case, “What is the zip code?”.
  6. Make sure that the Required check box is selected, and then choose the plus (+) icon and add another slot.
  7. For Name, enter streetNumber.
  8. For Slot type, select AMAZON.NUMBER.
  9. For Prompt, add the text to be spoken, in this case, “What is the street number?”.
  10. Make sure that the Required check box is selected, and then choose the plus (+) icon to add another slot.
  11. For Name, enter streetAddress.
  12. For Slot type, select AMAZON.StreetAddress.
  13. For Prompt, add the text to be spoken, in this case, “What is the street address?”.
  14. Make sure that the Required check box is selected, and then choose the plus (+) icon.
  15. Add a sample utterance, such as, “Update address” (don’t include the quotes) and choose the plus (+) icon.
  16. Add a second utterance, such as, “I would like to update my address” (don’t include the quotes), and choose the plus (+) icon.

  1. Choose TalkToAnAgent.
  2. 18. Add a sample utterance, such as, “Speak to an agent” (without the quotes), and choose the plus (+) icon.
  3. 19. Add a second utterance, “agent” (without the quotes), and choose the plus (+) icon.

Create a Lambda function

When the customer speaks the zip code, the Amazon Lex bot invokes an AWS Lambda function to validate the input and ensure it received a valid zip code. After confirmation, a call goes to the Google Geo API to get the city and state associated with the given zip code.
If the input given is not valid, the system plays back an appropriate message to the user with SSML tagging through Amazon Polly. To create a Lambda function, use the following steps:

    1. In the AWS Lambda console, choose the US West (Oregon) Region.
    2. Choose Create Function, Author from scratch.
    3. Enter the function name as “AddressHelper.”
    4. Select the Runtime “Node.js 14.x” or latest version
    5. For Permissions, choose Create new role with Lambda permissions.
    6. Download the address helper code.
    7. Open the downloaded code in a text editor, copy the entire contents, and paste it in the index.js field, as shown below:

  1. Choose Save.

Initiate and validate the Lambda function
When the user responds to the slots, the Amazon Lex bot calls Lambda to integrate and provide appropriate responses. Use the following steps to configure the responses:

  1. Choose Lambda initialization and validation.
  2. Choose the Lambda function to integrate.
  3. Choose Latest for the version or alias.

The following code example shows where it prints the request and the details that Amazon Lex invokes to the Lambda function. Turning on logging helps you troubleshoot issues when they arise. The dispatch function responds to the request based on the input given by Amazon Lex


		let AWS = require('aws-sdk');
		let lambda = new AWS.Lambda();
		const https = require("https");

		exports.handler = (event, context, callback) => {
			console.log("incoming event details: " + JSON.stringify(event));
			try {
				// By default, treat the user request as coming from the America/New_York time zone.
				process.env.TZ = 'America/New_York';
				console.log(`event.bot.name=${event.bot.name}`);
				console.log("incoming event details: " + JSON.stringify(event));
				//Send the request to the dispatch function
				 dispatch(event, (response) => loggingCallback(response, callback));

			} catch (err) {
				callback(err);
			}
		};
	

The following code example shows the dispatch function validation to ensure that it comes from the UpdateAddress intent and sends the control to the apptStatus function to handle the request. This helps if you have multiple intents with Lambda hooks, and you want to ensure that the request goes to the right Lambda function.


	/**
	 * Called when the user specifies an intent for this skill.
	 */
	function dispatch(intentRequest, callback) {
		 //console.log(JSON.stringify(intentRequest, null, 2));
		//console.log(`dispatch userId=${intentRequest.userId}, intent=${intentRequest.currentIntent.name}`);

		var name = intentRequest.currentIntent.name;
		console.log('Intent Name : ', name);
		// Dispatch to your skill's intent handlers
		if (name === 'UpdateAddress') {
			console.log('Calling apptStatus -->');
			return apptStatus(intentRequest, callback);
		}
		throw new Error(`Intent with name ${name} not supported`); 
	}
	

The apptStatus function, described later, initiates all the variables required to send the response back based on the caller’s input. It collects zip code first to get the city and state. Then it collects street number and street address.
The code first checks to see that it has collected the zip code. If not, and if it has tried fewer than three times, then it prompts the customer to reinput the zip code. If the attempt exceeds three times, the code sends the customer to an agent.

After collecting the zip code, the code checks to see if the length of the zip code is five digits. If not, and if it has tried fewer than three times, then it prompts the customer to repeat the zip code. If the attempt exceeds three times, the code sends the customer to an agent.
After the code successfully collects a five-digit zip code, it finds the city and state, calling the function getCityAndState by passing the zip code. If the function successfully resolves to a state and city, the code moves on to elicit the next slot. If not, and if it has tried fewer than three times, then it prompts the customer to repeat the zip code. If the attempt exceeds three times, the code sends the customer to an agent.
After the code successfully finds the state and city based on the zip code, it prompts the customer to enter the street number. If the customer exceeds the time limit without responding, the code sends the customer to an agent.

So far, the program has captured the state and city based on the zip code, and the street number thanks to customer input. It next prompts the customer to enter the street address. If the customer exceeds the time limit without responding, the code sends the customer to an agent.
After it successfully captures all the slots that are required, the code confirms all its gathered information with the customer. If the customer confirms, then we update the backend or send them to an agent to further assist.
If the customer says “no,” Amazon Lex passes the denied function from the confirmation prompt, and sends the customer to an agent for further assistance.
After capturing the street number, address, and zip code, and the customer confirms, raise the fulfillment and update the address in the backend.
The apptStatus function looks like the following code example:


		function apptStatus(intentRequest, callback) {
			console.log('apptStatus->');
			var ZipCode = intentRequest.currentIntent.slots.zipCode;
			var StreetNumber = intentRequest.currentIntent.slots.streetNumber;
			var StreetAddress = intentRequest.currentIntent.slots.streetAddress;
			var source = intentRequest.invocationSource;
			console.log(' StreetNumber : ' , StreetNumber, ' Street Address : ' , StreetAddress , ' ZipCode : ', ZipCode );
			const outputSessionAttributes = intentRequest.sessionAttributes || {};
			const confirmationStatus = intentRequest.currentIntent.confirmationStatus;
			var phoneNumber = "web-user";
			if (intentRequest.sessionAttributes.PhoneNumber != undefined){
				phoneNumber = intentRequest.sessionAttributes.PhoneNumber;
			}
		   // const phoneNumber = intentRequest.sessionAttributes.phoneNumber;
			console.log("customer phone number: " + phoneNumber);
			console.log(confirmationStatus);


			if (source === 'DialogCodeHook') {
				// Perform basic validation on the supplied input slots.
				const slots = intentRequest.currentIntent.slots;
				const validationResult = validateAppttatus(StreetNumber, ZipCode, StreetAddress);
				   if (!validationResult.isValid) {
					slots[‘${validationResult.violatedSlot}‘] = null;
					callback(elicitSlot(outputSessionAttributes, intentRequest.currentIntent.name,
					slots, validationResult.violatedSlot, validationResult.message));

					return;
				}

				// add the customer utterence
				appendTranscript(outputSessionAttributes, 'Customer', intentRequest.inputTranscript, phoneNumber);


				if ( !ZipCode) {

					// we could use the default prompt, but then we'll have to add that to the transcript separately
					let prompt = 'What is the Zip Code?';
					if (outputSessionAttributes.promptedForZip) {
					   var ctr = parseInt(outputSessionAttributes.promptedForZipCounter);
					   ctr ++;
					   outputSessionAttributes.promptedForZipCounter = ctr;
					   if(ctr > 3){
						   callback(close(outputSessionAttributes, 'Failed', { contentType: 'Plaintext', content: `Hmm... let me get you to an agent who can better assist you!` }));
						   return;
					   }
					   prompt = 'Sorry; what was that zip code again?'; 
					}else{
						outputSessionAttributes.promptedForZipCounter = "1";
					}
					outputSessionAttributes.promptedForZip = 'true';
					outputSessionAttributes.ZipCodeUtterance = intentRequest.inputTranscript;
					callback(elicitSlot(outputSessionAttributes, intentRequest.currentIntent.name,
					intentRequest.currentIntent.slots, 'zipCode', buildMessage(prompt)));
					return;
				}
				if(ZipCode){
					   var ctr = parseInt(outputSessionAttributes.promptedForZipCounter);
					   ctr ++;
					   outputSessionAttributes.promptedForZipCounter = ctr;
					   if(ZipCode.length != 5){
						   if(ctr > 3){
							   callback(close(outputSessionAttributes, 'Failed', { contentType: 'Plaintext', content: `Hmm... let me get you to an agent who can better assist you!` }));
							   return;
						   }
						prompt = 'Sorry; what was that zip code again?'; 
						outputSessionAttributes.ZipCodeUtterance = intentRequest.inputTranscript;
						callback(elicitSlot(outputSessionAttributes, intentRequest.currentIntent.name,
						intentRequest.currentIntent.slots, 'zipCode', buildMessage(prompt)));
						return;
					   }
				}
				if (ZipCode && !StreetNumber) {

					// if you use the default prompt, you must add it to the transcript separately
					getCityAndState(ZipCode, function(result){
						console.log(result);
						let prompt = 'And the Street Number?';
						let city = "";
						let state = "";
						try{
							city =  result.results[0].address_components[1].long_name;
							state = result.results[0].address_components[2].long_name;
							result.results[0].address_components.forEach(function(item){
								console.log(item);
								item.types.forEach(function(it){
									console.log(it);
								   if(it==='locality'){
									city =  item.long_name;   
								   } 
								  if(it==='administrative_area_level_1'){
									state =  item.long_name;   
								   } 

								});
							});
						}catch(e){
						   console.log(JSON.stringify(e));
						   var ctr = parseInt(outputSessionAttributes.promptedForZipCounter);
						   ctr ++;
						   outputSessionAttributes.promptedForZipCounter = ctr;
						   if(ctr > 3){
							   callback(close(outputSessionAttributes, 'Failed', { contentType: 'Plaintext', content: `Hmm... let me connect you to an agent who can better assist you!` }));
							   return;
						   }
							prompt = 'Sorry; the zip code you entered is invalid, please try a valid zip code.'; 
							outputSessionAttributes.ZipCodeUtterance = intentRequest.inputTranscript;
							callback(elicitSlot(outputSessionAttributes, intentRequest.currentIntent.name,
							intentRequest.currentIntent.slots, 'zipCode', buildMessage(prompt)));
							return;
						}
						outputSessionAttributes.City = city;
						outputSessionAttributes.State = state;
						if (outputSessionAttributes.promptedForStreetNumber) {
						   var ctr = parseInt(outputSessionAttributes.promptedForStreetNumberCounter);
						   ctr ++;
						   outputSessionAttributes.promptedForStreetNumberCounter = ctr;
							if(ctr > 3){
							   callback(close(outputSessionAttributes, 'Failed', { contentType: 'Plaintext', content: `Hmm... let me get you to an agent who can better assist you!` }));
							   return;
						   }               
						   prompt = `Sorry  can you repeat the street number? `; 
						}else{
							prompt = `Great!, you live in : ${city}  ${state}  now what is the street number `;
							//prompt = 'Great!, you are in '+  result.city + '  ' + result.state + ' now what is the street number?';
							outputSessionAttributes.promptedForStreetNumberCounter = "1";
							outputSessionAttributes.promptedForStreetNumber = "true";
						}
						callback(elicitSlot(outputSessionAttributes, intentRequest.currentIntent.name,
							intentRequest.currentIntent.slots, 'streetNumber', buildSSMLMessage(prompt)));
					});
				}
				//validate close hours of operation, we assume open hours are 8am to 5pm
				if(StreetNumber && ZipCode && !StreetAddress){

					// we could use the default prompt, but then we'll have to add that to the transcript separately
					let prompt = 'And the Street Address?';
					if (outputSessionAttributes.promptedForStreetAddress) {
					   var ctr = parseInt(outputSessionAttributes.promptedForStreetAddressCounter);
					   ctr ++;
					   outputSessionAttributes.promptedForStreetAddressCounter = ctr;
						if(ctr > 3){
						   callback(close(outputSessionAttributes, 'Failed', { contentType: 'Plaintext', content: `Hmm... let me connect you to an agent who can better assist you!` }));
						   return;
					   }               
					   prompt = 'Sorry; what was that Street Address?'; 
					}else{
						outputSessionAttributes.promptedForStreetAddressCounter = "1";
					}
					outputSessionAttributes.promptedForZip = 'true';
					outputSessionAttributes.promptedForStreetNumber = 'true';
					outputSessionAttributes.StreetAddressUtterance = intentRequest.inputTranscript;
					outputSessionAttributes.ZipCode = ZipCode;
					outputSessionAttributes.StreetNumber = StreetNumber;
					callback(elicitSlot(outputSessionAttributes, intentRequest.currentIntent.name,
					intentRequest.currentIntent.slots, 'streetAddress', buildMessage(prompt)));
					return;            
				}

				if (StreetNumber && ZipCode && StreetAddress && (confirmationStatus == "None")) {
					outputSessionAttributes.StreetAddressUtterance = intentRequest.inputTranscript;
					var city = outputSessionAttributes.City;
					var state = outputSessionAttributes.State;
					var fullAddress= StreetNumber + ' ' + StreetAddress + ' ' + city + ' ' + state;
					//fetch the trip details
					console.log('StreetNumber : ' , StreetNumber , ' ZipCode --> ', ZipCode, ' StreetAddress --> ' , StreetAddress);
					callback(confirmIntent(outputSessionAttributes, intentRequest.currentIntent.name, slots,
						{ contentType: 'SSML', content: `You'd like me change the address to : ${StreetNumber}  ${StreetAddress}  : ${city} .   ${state}  Is that right` }));
					return;

					/*getCityAndState(fullAddress, function(result){
						var street = result.results[0].address_components[1].long_name;            

					});*/
				}

				if ( StreetNumber && ZipCode && (confirmationStatus == "Denied")) {

					outputSessionAttributes.confirmStatus = confirmationStatus;
					console.log("user denied prompt: " + JSON.stringify(outputSessionAttributes));
					callback(close(outputSessionAttributes, 'Fulfilled', { contentType: 'Plaintext', content: `Hmm... let me get you to an agent who can better assist you!` }));

					return;
				}

				if (StreetNumber && ZipCode && StreetAddress && (confirmationStatus == "Confirmed")) {
					outputSessionAttributes.confirmStatus = confirmationStatus;
					console.log("user confirmed prompt: " + JSON.stringify(outputSessionAttributes));
					callback(close(outputSessionAttributes, 'Fulfilled', { contentType: 'Plaintext', content: `Okay.  Your address is changed!` }));
					return;
				}

				//callback(delegate(outputSessionAttributes, slots));
				return;
			}
		}
	

The following function sends the confirmation request and is generic enough that you can call it from anywhere.


	function confirmIntent(sessionAttributes, intentName, slots, message) {
		// add to transcript 
		appendTranscript(sessionAttributes, 'Amazon Lex', message.content);

		return {
			sessionAttributes,
			dialogAction: {
				type: 'ConfirmIntent',
				intentName,
				slots,
				message,
			},
		};
	}
	

The following function sends the close request and is generic enough that you can call it from anywhere.


function close(sessionAttributes, fulfillmentState, message) {
    // add to transcript 
    appendTranscript(sessionAttributes, 'Amazon Lex', message.content);
    
    return {
        sessionAttributes,
        dialogAction: {
            type: 'Close',
            fulfillmentState,
            message,
        },
    };
}

The following function delegates to the next slot based on the input collected. It is generic enough that you can call it from anywhere.


function delegate(sessionAttributes, slots) {
    return {
        sessionAttributes,
        dialogAction: {
            type: 'Delegate',
            slots,
        },
    };
}

The following function sends the message with SSML tags. It is generic enough that you can call it from anywhere.


function buildSSMLMessage(messageContent) {
	return {
		contentType: 'SSML',
		content: messageContent
	};
}

The following function sends the message with plaintext, without SSML tags. It is generic enough that you can call it from anywhere.


function buildMessage(messageContent) {
	return {
		contentType: 'Plaintext',
		content: messageContent
	};
}

The following function integrates with Google GeoAPI to convert a zip code to a city and state. Replace PUT-YOUR-GOOGLE-KEY-HERE with your assigned Google API key. If you do not have a key, you can request one from Google Maps’ Geocode API. After capturing the full address, you can call the API action again to see if it resolves. It otherwise prompts the customer to enter the right street number or address.


function getCityAndState(address, cb){
    console.log('Querying Google for address : ' + address);
    const url= 'https://maps.googleapis.com/maps/api/geocode/json?address=' + address + '&sensor=false&key=PUT-YOUR-GOOGLE-KEY-HERE';    
    
    https.get(url, res => {
        res.setEncoding("utf8");
          let body = "";
        res.on("data", data => {
            body += data;
        });
        res.on("end", () => {
            var bd = JSON.parse(body);
            
            console.log("Google Geocode returned: " + JSON.stringify(bd));
            //console.log(bd.results[0].formatted_address);
            /*var result = {
                "city": bd.results[0].address_components[1].long_name,
                "state": bd.results[0].address_components[3].long_name
            };*/
            cb(bd);
        });
    });
}

Build and test the Amazon Lex bot

After you create your bot, make sure it works as intended before you publish it.

    1. Choose Build to open the Test Bot window. This might take a minute or two.
    2. After the build finishes, choose Test Chatbot.
    3. Enter messages in the Test Chatbot pane.
    4. To test the UpdateAddress intent, enter “Update Address” (without the quotes), and choose enter.
    5. Enter a five-digit zip code.
      • You see the response in SSML tagging because we use this bot with Amazon Connect for the customer’s feedback. The bot connects to Google Geocode API and automatically retrieves the city and state before prompting for the street number.
    6. Enter a street number and choose enter.
    7. Enter the street address and choose enter.
    8. The response in SSML tags asks for the confirmation, choose yes.
    9. You should see the confirmation, “Okay, your address is changed.”

Publish the Amazon Lex bot and create an alias

Next, publish the bot so that you can add it to a contact flow in Amazon Connect.

  1. Choose Publish.
  2. Provide the alias “AddressHelper” for your bot. Use the alias to specify this version of the bot in the contact flow.
  3. Choose Publish.

Add the Amazon Lex bot to an Amazon Connect instance

To use a bot in your contact flow, you must add the bot to your Amazon Connect instance. You can only add bots created under the same AWS account and in the same Region as your instance.

  1. Open the Amazon Connect console.
  2. Select the instance alias of the instance to which add the bot.
  3. Choose Contact flows.
  4. Under Amazon Lex, choose the plus (+) icon.
  5. Select the AddressHelper bot and choose Save Amazon Lex Bots. If you published the bot after you opened the settings for your instance, you must reload the page to see the changes.

Create a contact flow and add your Amazon Lex bot

Creating the contact flow allows you to configure the messages played to callers.

  1. Log in to your Amazon Connect instance with an account that has permissions for contact flows and Amazon Lex bots.
  2. Choose Routing , select Contact flows , and choose Create contact flow. Enter a name.
  3. Under Interact, drag a Get customer input block onto the designer, and connect it to the Entry point block.
  4. Open the Get customer input block and choose Text to speech.
  5. Enter a message that provides callers with information about what they can do. Use a message that matches the intents the bot uses, such as “Thank you for calling. How can I help you? You can say things like: ‘update address or ‘speak to an agent.’”

Add the Amazon Lex bot to your contact flow

Define the bot as the method of getting customer input.

  1. In the Get customer input block, select Amazon Lex.
  2. For Name, use AddressHelper. For Alias, use $LATEST (so it always picks the latest published bot).
  3. To specify the intents, choose Add a parameter under Intents.
  4. Enter AddressHelper, and choose Add another parameter.
  5. Enter TalkToAnAgent, and choose Save.

Finish the contact flow

After the caller interacts with the bot, finish the contact flow to complete the call for the customer.

  1. If the caller says, “update address,” use a prompt block to play a message and disconnect the call.
  2. If the caller says, “speak to an agent,” use a set queue block to set the queue and transfer the caller to the queue, which ends the contact flow.

To complete the UpdateAddress intent:

  1. Under Interact, drag a play prompt block to the designer, and connect the UpdateAddress node of the Get customer input block. After the customer updates the address from the Amazon Lex bot, the message in the play prompt block plays.
  2. Under Terminate/Transfer, drag a Disconnect/hang up block to the designer, and connect the Play prompt block. After the prompt message plays, the call disconnects.

To complete the SpeakToAgent intent:

  1. Add a Set queue block and connect it to the SpeakToAgent node of the Get customer input block.
  2. Add a Transfer to queue block and connect the Set queue block Success and Error nodes. You could also add a message that plays when the call can’t be tranferred because the queue is full, or when an error occurs.
  3. Choose Save & Publish.

Your finished contact flow looks like the following diagram:

contact-flow

Import contact flow

This section is required only if you downloaded the contact flow from this link.

    1. Log in to your Amazon Connect instance with an account that has permissions for contact flows and Amazon Lex bots.
    2. Choose Routing, Contact flows, Create contact flow.
    3. Choose the Import Flow (beta).
    4. Choose select, then choose Import.
    5. The import highlights the affected steps in your architecture in orange. Choose Set Working Queue and select the QueueName configured under your instance.

  1. Choose the Get Customer Input block and re-select the AddressHelper bot.
  2. Choose Save and Publish

Assign the contact flow to a phone number

When customers call into your contact center, the bot connects them to the contact flow assigned to the telephone number that they dialed. To make the new contact flow active, assign it to a phone number for your instance.

  1. Open the Amazon Connect dashboard.
  2. Choose View phone numbers.
  3. Select the phone number to which to assign the contact flow.
  4. Add a description.
  5. In the Contact flow/IVR menu, choose the contact flow that you just created.
  6. Choose Save.

Try it

You can try the bot and contact flow that I created for this post in the Amazon Connect demo environment. Dial 202-919-5457, and follow the prompts.

Summary

In this post, I demonstrated how to create an Amazon Connect contact flow that integrates with Google Geocode APIs to update your address with an Amazon Lex bot. I also showed how to provide automated assistance to your customers.

Through the power of Amazon Lex conversational interfaces, your customers can now choose how best to provide input, and you can better route their calls to the appropriate bot or agent. I look forward to seeing all the wonderful things you create with these great new features.

Related links
To learn more about the technologies or features that I used to create this solution in the US East (N. Virginia) Region, see the following: