Conversations are dynamic and do not follow a straight line. Have you ever been in a conversation with someone where you asked a question and instead of the answer they reply with a question? In this case, the person on the other end was missing some important information that they needed in order to make a decision. In conversation, it's perfectly normal for someone to answer with follow-up questions until they've obtained all the information that they need in order to provide an answer. When your conversational partner asks a follow-up question, you will need to:
When you break it down it's pretty complicated and it's amazing that we humans can do that without much effort.
In order to create a conversational skill, you'll need to mimic that behavior. First you should write a happy path script, which is the simplest conversation that your customers will have with your skill. After that you should try to anticipate how users will deviate from your happy path. Doing so will help you to anticipate areas where they might ask a follow-up question. You can then build in answers to those questions. If you’d like to dive deeper into designing a happy path script, check out the script it out section of the Designing for Conversation online course.
One important thing that your skill will need to do is keep track of the information that it has been collected. Today's blog post will share how you can enable your Alexa skill to capture important information using context switching.
The Foodie is an example skill that we created for our Designing for Conversation online course. It's a skill that recommends meals and recipes to it's customers based on a set of questions. When the customer expresses a desire to cook, The Foodie tailors its questions to figure out what kind of recipe to recommend. "There are small medium and large portions, which would you like?" is one of those questions.
Some people might be just fine answering, small or medium while others might be watching their caloric intake and would like to know how many calories are in a large portion before they decide. To do this, the Foodie uses two intents.
When the customer chooses a portion size, the skill will re-enter the RecommendationIntent and we'll need to restore the context manually otherwise the skill wont remember what has already been collected.
Failing to do so will result in a frustrating experience and our customers will not be pleased. Take a minute to watch this video to see how frustrating that would be:
Wasn’t that awkward? You shouldn't expect your customers to have keep repeating previously provided information over and over. One of the great things about conversation is that it's reactive and conversational partners can work together to communicate their thoughts and feelings.
Let's take a look at how we are remembering context with The Foodie.
The Alexa Skills Kit Software Development Kit (SDK) includes a lot of great features. One great feature is called session attributes. We can use these attributes to store important information that our skill need not forget. If you’re unfamiliar with session attributes and are interested in learning more, take a look at Alexa Skill Recipe: Using Session Attributes to Enable Repeat Responses.
The Foodie, just like its conversational human counterpart, uses five steps to context switch:
Let’s take a look at how these 5 steps translate to code.
To get the session attributes, we will make use of the SDK’s AttributesManager. It includes a function called getSessionAttriutes which reads the session attributes from the response to our skill code. We’ll need to access the attributes later so we will save them into a variable called sessionAttributes.
const session handlerInput.attributesManager.getSessionAttributes();
Now let's check them for previously collected slot values.
Using the sessionAttributes, we’ll check to see if we’ve previously saved the current intent’s slots. If they aren’t there then it’s the first time our user’s experienced this intent during this interaction (session).
If, however, we’ve already collected some of the slots, then we have some slots to restore. In this case, Alexa is either continuing where the dialog management flow previously left off, or returning from a context switch. To check the later case, our if statement would look like:
if (sessionAttributes[currentIntent.name]) {
// We have slots to restore!
}
Now that we’ve identified something to restore, let’s restore them.
To restore our slots, we need to identify slots that are filled vs. those that are empty. This will help us determine where the conversation needs to pick up. We don’t want to have our skill ask again for information previously given.
To do this we will loop through the entire set of slots for the sessionAttributes and add them to the currentIntent’s slot values. In code that would look like:
const savedSlots = sessionAttributes[currentIntent.name].slots;
foreach(let key in savedSlots) {
if (!currentIntent.slots[key].value && savedSlots[key].value) {
currentIntent.slots[key] = savedSlots[key];
}
}
The if statement ensures that we are only restoring slots values when we:
We are doing this to allow the user to override their choice. For example, if they asked for a large portion and then changed their mind asking for a medium portion, the code will allow them to do so.
We’ve restored our slots. Let’s move on to setting our session attributes slots to match with those of currentIntent.
This is an important step. We don’t want Alexa to forget the slots especially after all that hard work that we did. So to do that our code will update the sessionAttributes to be the currentIntent that we just finished updating in our loop.
sessionAttributes[currentIntent.name] = currentIntent;
attributesManager.setSessionAttributes(sessionAttributes);
Now let’s move onto the final step.
We’re not out of the woods yet. We still need to tell the Alexa Service that we’ve changed the currentIntent’s slot values. To do so, we need to send our modified currentIntent along with the Dialog.Delegate that we pass back to the Alexa Service to have dialog management automatically prompt for our missing required slots. The SDK makes it easy for us. We simply need to pass currentIntent to the addDelegateDirective() function.
return handlerInput.responseBuilder
.addDelegateDirective(currentIntent);
Now that we’ve seen the code piece by piece, let’s take a look at what it looks like all together.
const DialogManagementStateInterceptor = {
process(handlerInput) {
const currentIntent = handlerInput.requestEnvelope.request.intent;
if (handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.dialogState !== "COMPLETED") {
const attributesManager = handlerInput.attributesManager;
const sessionAttributes = attributesManager.getSessionAttributes();
// If there are no session attributes we've never entered dialog management
// for this intent before.
if (sessionAttributes[currentIntent.name]) {
let savedSlots = sessionAttributes[currentIntent.name].slots;
for (let key in savedSlots) {
// we let the current intent's values override the session attributes
// that way the user can override previously given values.
// this includes anything we have previously stored in their profile.
if (!currentIntentSlots[key].value && savedSlots[key].value) {
currentIntent.slots[key] = savedSlots[key];
}
}
}
sessionAttributes[currentIntent.name] = currentIntent;
attributesManager.setSessionAttributes(sessionAttributes);
}
}
};
This code will go a long way to ensure that your customers aren’t forced to repeating themselves when the conversation caues your skill to switch back and forth between contexts.
Did you notice that we wrapped all of code in DialogManagementStateInterceptor? Interceptors run at either every request or response. The if statement checks to see whether the request is an intent request where the dialogState isn’t completed. This allows our code to support context-switching from any intent where dialog management is activated.
If you’ve never used an interceptor you’ll need to register it. Failing to do so will leave your interceptor inoperative and thus sad and lonely. Request interceptors are processed before our handlers while response interceptors are processed after. Since our handlers need the most up to date synced slots, we need a request interceptor.
We’ll pass our DialogManagementStateInterceptor to addRequestInterceptors() to do so:
const skillBuilder = Alexa.SkillBuilders.custom();
exports.handler = skillBuilder
.addRequestHandlers(
// ... handlers
)
.addRequestInterceptors(
DialogManagementStateInterceptor
)
.addErrorHandlers(ErrorHandler)
.withTableName("theFoodie")
.lambda();
With this code, our skill can switch contexts without forgetting anything.
Conversations can take on many branching paths. For example, a question can be answered with a question. Once the answer to the second question has been provided, it's up to your skill to prompt your customer to answer the original one. To avoid frustration and to build a more conversational skill, your skill should remember the information previously collected so it doesn't ask for it over an over. Being able to support a context switch will go a long way into building a conversational skill.
Now that you've read through this post, try to think about how you can put these techniques and features to use in your own skills. Let's continue the discussion online! You can find me on Twitter @SleepyDeveloper.