In a previous blog post we covered the basics of how Amazon Connect can use Lex bots to collect details from the caller and fulfill a simple task such as sending an e-mail or a text message. By using a Lambda function within the fulfillment code hook of a bot we can trigger pretty much any automated task that Lambda can handle. Once this task is complete and Lex receives the appropriate response it will return to the contact flow and the caller can be routed into a queue or another flow.
This is incredibly powerful if we want our bot to perform a task after it identifies the caller’s intent and collects all the required slots. However, there are times when, depending on a collected value, we might need to collect more details or, after checking a database, we may want to override a slot collected earlier. In this post we will look at ways to build these types of intelligent interactions using more complex Lambda responses.
Let’s start by reviewing the basics of how the two AWS services interact.
When invoking Lambda, Lex will send a JSON payload that will contain the current intent, details about slots as well as information about the bot and some system variables (including where within the Lex bot Lambda has been invoked). For a full description of the entire input please review the official AWS documentation, but for our purposes the most relevant portion is within the current intent section, shown below.
"currentIntent": { "name": "BookHotel", "slots": { "city": null, "room": null }
By referencing this section of the input Lambda can access the intent and slots collected by Lex and do any necessary operations, then return an appropriate response. This response will be different depending on the action you want Lex to take next. Currently there are 5 actions or instructions Lambda can send back to Lex.
“dialogAction”: {“type”: “ElicitIntent, ElicitSlot, ConfirmIntent, Delegate, or Close”}
The body of each action will look a bit different. Here is, for example, how a “close” action response will look like:
const response = { "dialogAction": {"type": "Close", "fulfillmentState": "Fulfilled", "message": { "contentType": "PlainText", "content": "Your hotel has been booked." } } };
This is an example of the typical response a fulfillment Lambda function would send back to Lex after it completed whatever action is necessary. It will let the bot know that everything went fine, it can now play the content of the message to the caller and return to the contact flow.
However, in case something is not fine, and we need to have the Lex bot collect additional details we can use the elicit slot response. As the name might imply, this dialog action will let the bot know it must go back and collect a new slot value. For the example below let’s say our Lambda code checked a database and discovered that king rooms are not available in Chicago. Our function can send back the following response which will inform the caller they need to choose a queen or single room.
const response = { "dialogAction": {"type": "ElicitSlot", "message": { "contentType": "PlainText", "content": `We're sorry but King is not supported for ${event.currentIntent.slots.city}. Please choose Queen or Single` }, "intentName": "BookHotel", "slots": event.currentIntent.slots, "slotToElicit": "room" } }
Something to keep in mind is that if you are building this kind of response, in your fulfillment Lambda function you should make sure to have a “close” path that actually fulfills the intent of the caller if all values are valid. Potentially infinite loops that keep requesting the same slots are very likely to get an error like: “Invalid Lambda Response: Reached second execution of fulfillment lambda on the same utterance”.
Something else to consider is that it’s also possible to use Lex simply to collect data and validate everything within Amazon Connect. This could look something like the screenshot below. We use Lex to collect the city and room and pass both parameters into a Lambda function that checks a database and lets us know if the entries are invalid. In this example we ask the caller to enter their details again, but we could also set up a separate intent (or a separate bot) for collecting city or room and route the caller to this new get input node.
This approach can make a lot of sense if we’re only collecting one or two entries from the caller and the Lambda function doesn’t need to do anything except validate these. It might also be easier to manage for call center supervisors as they can review the logic in the contact flows instead of having to read code inside of AWS Lambda. Finally, this approach also allows you to more easily customize the behavior if one of the entries is not valid. By handling validation within Connect we can build a more detailed menu to handle exceptions instead of relying on the prompt text from an elicit slot response. That said, if you need to do more complex validation of entries the dialog code hook is probably the best option.
So far, we only covered invoking Lambda as a final step of the Lex bot, inside the fulfillment code hook however, we can also invoke functions inside the validation or dialog code hook area. The main difference is that for validation Amazon Lex invokes the specified Lambda function on each user input (utterance) while the fulfillment Lambda function will only be invoked once all slots are filled out.
Since the dialog code hook will be invoked after each utterance, even before all slots have customer entries we will need to use a different type of response. If the room slot value is null because we haven’t asked the caller to make a room selection yet, we don’t want to use an elicit slot response and bypass the regular slot collection mechanism. Instead we can make use of the delegate dialog action. Delegate essentially passes all details back to Lex and lets it decide what is the next appropriate action, which may be to collect the next required slot or fulfill the intent.
A delegate dialog action response could look like this:
const response = { "dialogAction": { "type":"Delegate", "slots": { "city":”Seattle”, "room": null } } }
This response will let Lex know the city input of Seattle is a valid entry and room is currently null, so it should be collected next. Note that you should be careful not to hard-code null as an option that could be repeated as Lex will try to collect that slot only to have it overwritten by the null Lambda response again and again. For a step by step breakdown on how delegate works with Lex please review this example.
Note that using delegate, you can also overwrite the customer input. Maybe instead of Seattle your system needs to use the metropolitan area of “Seattle-Tacoma-Bellevue”. After performing the validation check the delegate response can simply return the new entry to Lex and the bot will pass it on to Amazon Connect once the data collection is complete.
Now that we covered the different options for invoking Lambda and the different responses Lex can receive, let’s look at some pseudo-code for a function that can handle both validation and fulfillment for a hotel booking Lex bot.
// --------------- Helpers to build responses which match the structure of the necessary dialog actions ----------------------- function elicitSlot(sessionAttributes, intentName, slots, slotToElicit, message) { return { sessionAttributes, dialogAction: { type: 'ElicitSlot', intentName, slots, slotToElicit, message, }, }; } function close(sessionAttributes, fulfillmentState, message) { return { sessionAttributes, dialogAction: { type: 'Close', fulfillmentState, message, }, }; } function delegate(sessionAttributes, slots) { return { sessionAttributes, dialogAction: { type: 'Delegate', slots, }, }; } // --------------- Main handler ----------------------- exports.handler = (event, context, callback) => { console.log("incoming event details: " + JSON.stringify(event)); try { console.log(`event.bot.name=${event.bot.name}`); console.log("incoming event details: " + JSON.stringify(event)); //Save details from the Lex inpout const city = event.currentIntent.slots.city; const room = event.currentIntent.slots.room; const source = event.invocationSource; //Check if Lambda is invoked to validate or fulfill the request if (source === "DialogCodeHook") { const outputSessionAttributes = event.sessionAttributes || {}; let slots = intentRequest.currentIntent.slots; // Check if any slots have been collected yet and validate the ones that have been colected. if (city === null && room === null){ // nothing has been collected yet so we can just pass the null values back to Lex for standard collection callback(delegate(outputSessionAttributes,slots)); } else if(city === null && room !== null){ // This is where you can validate the room type if (valid entry) //The room choice is a valid entry so we can just pass the null slots back to Lex for collection slots = { "city": null, "room": room }; callback(delegate(outputSessionAttributes,slots)); } else if (not a valid entry){ //The room choice is not a valid entry so we will elicit the room slot again. callback(elicitSlot(outputSessionAttributes, intentRequest.currentIntent.name,intentRequest.currentIntent.slots, "room", { contentType: 'PlainText', content: 'We apologize but that is not a valid entry for room type. Please make a new selection.' })) } } else if(city !== null && room === null){ // This is where you can validate the city and either delegate the slots back to Lex so we can collect room type or elicit the city slot again. } else{ // This is where you can validate both entries if necessary and delegate the valid slots back to Lex so we can fulfil the intent or elicit the necessary slots again. } } else if (source === "FulfillmentCodeHook"){ //This is where you will enter the code that will fulfil this intent and finall let Lex know that this request has been closed succesfuly callback(close(outputSessionAttributes, 'Fulfilled', { contentType: 'PlainText', content: "Your hotel has been booked." }); } } catch (err) { callback(err); } };
While several chunks are missing on purpose and the main handler could certainly be simplified hopefully this function gave you a good idea how you should structure your own Lambda functions. For more ideas on how to improve your contact flows and assistance deploying your Lex bots please email Craig Reishus.
hey what did you use to make the flow chart ? You placed a screenshot on the blog post and it looks like MS. Visio.
Hi Eva, the image I used is just a screenshot from the Amazon Connect contact flow editor.
Thank you,