When you have a natural conversation with another person, you might find the conversation can take different directions. Therefore, the context of the conversation can change quite rapidly. With dialog management, you can ensure your skill is able to handle context switching.
Previously, the dialog model required the user to complete the dialog in order to switch context to a different intent, but recent updates to dialog management now make it possible to switch context between intents.
This is great news because you can now build a truly frame-based voice user interface. If the user wants to interact with another intent part way through the dialog, they can. When they want to resume the dialog, they can speak an utterance that will trigger the dialog and it will be active again.
One thing to note is that you are responsible for keeping track of the filled slots when switching contexts. If you don't, the dialog will start over at the beginning instead of where the user left off. Requiring the user to repeat the same information all over again is not a great experience, so you'll want to be sure to save this.
Below I'm going to show you a simple way to keep the context intact while switching between intents.
For this example, I'm going to refer to a sample skill called Pet Match. It uses three required slots: size, temperament, and energy to match a user with a dog. The four values defined in the custom sizeType slot are tiny, small, medium, and large.
When the skill elicits the slot it asks, "There are dogs that are tiny, small, medium, and large, which would you like?" For many people choosing one of the four options is pretty straightforward, but it would be good to provide some help if the user wanted to know what large means.
With the new context switching support for dialog management, we can provide a new intent called ExplainSizeIntent that allows the user to ask, How many pounds is a large dog?
, which will context switch out of dialog management and provide an answer. At this point, the user could say, I want a large dog
which would re-activate the PetMatchIntent and continue prompting the user for the required slots.
Being able to jump back and forth between intents is a great voice-first experience because the user isn't stuck in a menu. We refer to this approach as frames, where each intent is a frame. While you're in the middle of a dialog, you can speak an utterance that allows you to access another intent and then go back to the one you were on before. Interactive voice response (IVR), which is the backbone of many automated call centers, ropes the caller into rigid path often referred to as a tree. There's no way move across branches without going all the way back to the top-level menu, which is unnatural and should be avoided when designing a voice experience.
Following voice-first best practices, let's think about the conversation the user will have with Alexa and then walk back to the code:
User: Alexa, tell pet match I want a dog.
Alexa: What are two things you're looking for in a dog?
User: I want a low-energy guard dog.
Alexa: There are dogs that are tiny, small, medium, and large. Which would you like?
User: How many pounds is a large dog?
Alexa: A large dog is 55 to 77 pounds. What size dog would you like?
User: I want a large dog.
Alexa: So a large, low-energy guard dog sounds good for you. You should consider a doberman pincher.
To allow the user to ask for clarification on size, we will need to:
First, we are going to add a new intent called ExplainSizeIntent and define the following utterances:
How many {unitOfMeasurement} is a {size} dog
What is a {size} dog
Upon doing so, the skill will be able to identify the size the user has enquired about we can respond with an explanation. The unitOfMeasurement slot is assigned a custom slot type called unitOfMeasurementType that allows us to identify what unit of measurement the user asked for. The values are, pounds, kilograms. In our backend we can look up the size based on the unitOfMeasurement using the following data structure:
let sizeChart = {
"tiny": {
"pounds": "4 to 6",
"kilograms": "1.8 to 2.7",
},
"small": {
"pounds": "7 to 20",
"kilograms": "3.18 to 9",
},
"medium": {
"pounds": "21 to 54",
"kilograms": "9.53 to 24.49",
},
"large": {
"pounds": "55 to 80",
"kilograms": "24.94 to 38.28",
}
}
We'll plug size and unitOfMeasurementType into the sizeChart, sizeChart[size][unitOfMeasurement]
, to look up the size. We will also want to determine a default unitOfMeasurement because the What is a {size} dog
utterance allows the user to omit it.
Dialog management supports context switching so the user may interact with another intent during the slot collection, but you must manage the context, otherwise when the user switches back the previously collected slots will disappear and the user will have to provide all the slots over again.
To save the dialog management context, we will leverage session attributes. Session attributes provide a way to keep track of information throughout the life of you skill. To store something into the session, simply define a key and set the desired value, this.attributes['myKey'] = value.
To support a context switch at any point, we will need to save the collected slots into the session attributes. To do this we will hook into the dialog management state machine and update the delegateSlotCollection
function to save the state with the following code. (Remember that we are using Pet Match as a basis.)
if (this.event.request.dialogState !== "COMPLETED") {
this.attributes['temp_' + this.event.request.intent.name] = this.event.request.intent;
} else {
delete this.attributes['temp_' + this.event.request.intent.name];
}
When we are in any state other than COMPLETED, we save the data into the session attributes and we name the session attribute after the intent name while appending temp_ to the beginning. Saving the state information based on the intent name will allow us to build a skill that has multiple intents that delegate slot collection that can context switch and not lose any previously collected slots.
When the user asks, How many pounds is a large dog?
the ExplainSizeIntent will explain to the user that, A large dog is 55 to 80 pounds. What size dog would you like?
When the user responds I would like a large dog, the skill will context switch back to the PetMatchIntent, but the dialogState will be STARTED and none of the intents that we have previously captured will be present in the request, so we will need to restore them from the session.
To do so, we will modify the save context code to rehydrate the slots from the session and the new code is wrapped in // new code and // end new code comments.
if (this.event.request.dialogState !== "COMPLETED") {
// new code
if (this.attributes['temp_' + this.event.request.intent.name]) {
let tempSlots = this.attributes['temp_' + this.event.request.intent.name].slots;
Object.keys(tempSlots).forEach(currentSlot => {
if (tempSlots[currentSlot].value) {
this.event.request.intent.slots[currentSlot] = tempSlots[currentSlot]
}
}, this);
} else {
this.attributes['temp_' + this.event.request.intent.name] = this.event.request.intent;
}
// end new code
} else {
delete this.attributes['temp_' + this.event.request.intent.name];
}
To determine if we need to rehydrate, we check to see if the session attributes contain an attribute named 'temp_' + this.event.request.intent.name
. If we were returning to the PetMatchIntent, the session attributes would contain, temp_PetMatchIntent. Next we loop through the set of slots that we saved in the session attributes, and for each one that has a value, restore it into the request. Doing so enables the skill to continue on through dialog management where it left off before the user invoked the ExplainSizeIntent
Below is the delegateSlotCollection
function updated with the context managment code integrated into it.
function delegateSlotCollection() {
let updatedIntent = this.event.request.intent;
// We only need to restore state if we aren't COMPLETED.
if (this.event.request.dialogState !== "COMPLETED") {
if (this.attributes['temp_' + this.event.request.intent.name]) {
let tempSlots = this.attributes['temp_' + this.event.request.intent.name].slots;
Object.keys(tempSlots).forEach(currentSlot => {
if (tempSlots[currentSlot].value) {
this.event.request.intent.slots[currentSlot] = tempSlots[currentSlot]
}
}, this);
} else {
this.attributes['temp_' + this.event.request.intent.name] = this.event.request.intent;
}
} else {
delete this.attributes['temp_' + this.event.request.intent.name];
}
if (this.event.request.dialogState === "STARTED") {
// optionally pre-fill slots: update the intent object with slot values
// for which you have defaults, then return Dialog.Delegate with this
// updated intent in the updatedIntent property
disambiguateSlot.call(this);
console.log("disambiguated: " + JSON.stringify(this.event));
this.emit(":delegate", updatedIntent);
} else if (this.event.request.dialogState !== "COMPLETED") {
console.log("in not completed");
//console.log(JSON.stringify(this.event));
disambiguateSlot.call(this);
this.emit(":delegate", updatedIntent);
} else {
console.log("in completed");
// Dialog is now complete and all required slots should be filled,
// so call your normal intent handler.
return this.event.request.intent.slots;
}
return null;
}
As you add context switching to your Alexa skills that use dialog management, I want to hear about your experiences, the challenges you faced, and how you overcame them. Please reach out to me on Twitter @sleepydeveloper to let me know!