I've spent a lot of time deep diving into dialog management. It is a great tool for building complex multi-turn conversational skills. As I continue to share my learnings with the community, I'm often asked about eliciting slot confirmations.
Dialog management simplifies creating a multi-turn conversational skill. When you set at least one of your slots required to fulfull the intent, the Alexa voice service will keep track of the dialog state and the required slots that have yet to be collected and send this information to your back end. At each turn, your skill can return a dialog directive to determine what Alexa will do. Returning a Dialog.Delegate directive will have the Alexa voice service automatically prompt for the next slot. Returning the Dialog.ElicitSlot directive will allow you to determine what slot Alexa will prompt next. Returning the Dialog.ConfirmSlot directive will allow you ask your customer to confirm a slot value.
In this technical post, I build upon the coffee shop skill that I introduced in the blog about how to dynamically elicit slots based on a previous answer using dialog management.
Our coffee shop skill allows the customer to order coffee or tea. Based on their drink of choice, the skill either asks what kind of coffee roast (coffeeRoast) or type of tea (teaType) they want. In my previous post, I walked through the steps to dynamically elicit the coffeeRoast and teaType slots based upon the drink using the Dialog.ElicitSlot directive.
Let's say we want to add the ability to add some flavor to our coffee. Our coffee shop charges an extra $0.50 to add a flavor, so we'll use the Dialog.ConfirmSlot directive to ask if the customer is willing to except the additional charges. First, we only want to upsell the flavor when drink is coffee, so we will use Dialog.ElicitSlot to dynamically elicit our new flavor. Second, we will confirm the slot with Dialog.ConfirmSlot, but what if our customer doesn't want to add any flavor from the start? We'll add "no thanks" as one of our flavor slot values and we will only return the Dialog.ConfirmSlot directive when the customer chooses a flavors.
The two steps will be:
Last, we will need to handle what to do next based on if the customer confirms or denies the extra cost for adding a flavor.
Follow along as I walk you through the process.
We'll start by updating our voice model. At this point, our coffee shop skill has three custom slots, drink, coffeeRoast, and teaType. The values are:
drink |
coffeRoast |
teaType |
coffe |
light |
black |
tea |
medium |
green |
|
medium dark |
white |
|
dark |
oolong |
When interacting with our skill, a customer may say one of the the following utterances to tell us their order.
Start my order
I'll have {drink}
I want to drink {drink}
I want {coffeeRoast} {drink}
I want {drink} to drink
I want {teaType} {drink}
{teaType} {drink} sounds great
{drink} please
To enable our customer to be able specify their desired flavor we'll need to update our utterances and create a new custom slot. For example, they may say something like:
I want a medium roast coffee with a shot of vanilla
I want to drink coffee with hazelnut
I want a shot of caramel in my coffee
Coffee with vanilla
These utterances will use our new slot which we'll call flavor. Using our drink, coffeeRoast and flavor slots we will replace the values we want to capture with our slots.
I want a {coffeeRoast} {drink} with a shot of {flavor}
I want to drink {drink} with {flavor}
I want a shot of {flavor} in my {drink}
{drink} with {flavor}
The table below shows our flavor slot's custom values.
flavors |
no thanks |
vanilla |
hazelnut |
caramel |
The no thanks value will allow our customer to decline a flavor add-on which we will prompt for using the Dialog.ElicitSlot directive if their drink is coffee. If any other value is given, we will ask if it's ok to charge $0.50 extra to add a flavor.
Now that we understand how our voice user interaction model will change. Let's take a look at our back-end code.
Similar to our coffeeRoast and teaType slots, our flavor slot is dynamic. We will only ask our customers if they want to add flavors if their drink is coffee. So we will be using Dialog.ElicitSlot to elicit the flavor slot and Dialog.SlotConfirmation to elicit a confirmation. To acheive this we'll define two more handlers. These handlers will represent the various situations of our skill and we will use them to elicit and confirm the flavor slot. Our handlers are:
This handler will use the Dialog.ElicitSlot directive to dynamically elicit the flavor slot. The canHandle function will return true if:
For example, this will occur if the user says, "I want dark coffee."
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.intent.slots.drink.value
&& handlerInput.requestEnvelope.request.intent.slots.drink.value === "coffee"
&& handlerInput.requestEnvelope.request.intent.slots.coffeeRoast.value
&& handlerInput.requestEnvelope.request.intent.slots.flavor.value
&& handlerInput.requestEnvelope.request.intent.slots.flavor.value !== "no thanks"
&& handlerInput.requestEnvelope.request.intent.slots.flavor.confirmationStatus === "NONE";
},
The handler will then use the Dialog.ElicitSlot directive to elicit the flavor slot.
handle(handlerInput) {
return handlerInput.responseBuilder
.speak("I can add some flavor to your coffee. Which would you like, caramel, hazelnut, or vanilla? You can also say no thanks.")
.reprompt("What flavor would you like added to your coffee, caramel, hazelnut, or vanilla? You can also say no thanks.")
.addElicitSlotDirective("flavor")
.getResponse();
}
Below is the the whole handler at a glance:
const CoffeeRoastGivenPromptFlavorOrderIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.intent.slots.drink.value
&& handlerInput.requestEnvelope.request.intent.slots.drink.value === "coffee"
&& handlerInput.requestEnvelope.request.intent.slots.coffeeRoast.value
&& !handlerInput.requestEnvelope.request.intent.slots.flavor.value;
},
handle(handlerInput) {
return handlerInput.responseBuilder
.speak("I can add some flavor to your coffee. Which would you like, caramel, hazelnut, or vanilla? You can also say no thanks.")
.reprompt("What flavor would you like added to your coffee, caramel, hazelnut, or vanilla? You can also say no thanks.")
.addElicitSlotDirective("flavor")
.getResponse();
}
};
Now that we're able to dynamically elicit the flavor slot if the order coffee, we will move to the next handler.
Our FlavorGivenConfirmSlotOrderIntentHandler will use the Dialog.ConfirmSlot directive. Its canHandle function will return true if:
The last condition, request.intent.slots.flavor.confirmationStatus equals NONE, is super important. Without it the skill will continuously pester our customer to confirm the flavor slot. Before a slot has been confirmed the confirmationStatus is NONE. Once the customer provides an answer it will be either DENIED or CONFIRMED, so before we confirm the flavor slot, we should check the confirmationStatus.
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.intent.slots.drink.value
&& handlerInput.requestEnvelope.request.intent.slots.drink.value === "coffee"
&& handlerInput.requestEnvelope.request.intent.slots.coffeeRoast.value
&& handlerInput.requestEnvelope.request.intent.slots.flavor.value
&& handlerInput.requestEnvelope.request.intent.slots.flavor.value !== "no thanks"
&& handlerInput.requestEnvelope.request.intent.slots.flavor.confirmationStatus === "NONE";
},
Our handle function builds the confirmation prompt and returns the Dialog.ConfirmSlot directive.
handle(handlerInput) {
const flavor = handlerInput.requestEnvelope.request.intent.slots.flavor.value;
const speechText = `Adding ${flavor} will cost $0.50 extra. Would you like me to add it?`;
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(speechText)
.addConfirmSlotDirective("flavor")
.getResponse();
}
Zooming out you can see how the handler's canHandle and handle functions fit together:
const FlavorGivenConfirmSlotOrderIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.intent.slots.drink.value
&& handlerInput.requestEnvelope.request.intent.slots.drink.value === "coffee"
&& handlerInput.requestEnvelope.request.intent.slots.coffeeRoast.value
&& handlerInput.requestEnvelope.request.intent.slots.flavor.value
&& handlerInput.requestEnvelope.request.intent.slots.flavor.value !== "no thanks"
&& handlerInput.requestEnvelope.request.intent.slots.flavor.confirmationStatus === "NONE";
},
handle(handlerInput) {
const flavor = handlerInput.requestEnvelope.request.intent.slots.flavor.value;
const speechText = `Adding ${flavor} will cost $0.50 extra. Would you like me to add it?`;
return handlerInput.responseBuilder
.speak(speechText)
.reprompt(speechText)
.addConfirmSlotDirective("flavor")
.getResponse();
}
};
Once the slot has been either confirmed or denied it's up to you to determine what to do next. Since we don't need to collect anymore slots and our FlavorGivenConfirmSlotOrderIntentHandler only happens after we've collected all of our coffee related slots, we'll handle the aftermath of the confirmation in our CompletedOrderIntentHandler.
This handler runs after the dialog has completed. The canHandle will return true if:
The canHandle translates to three simple checks.
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.dialogState === "COMPLETED";
},
The handle function includes our logic to handle the confirmationStatus. If drink is coffee, our logic checks the confirmationStatus. If it's DENIED we let our customer know that we've removed the flavor from their order. If it's CONFIRMED then we let them know that they made a great choice. At this point our sample skill ends, but if we were building a real coffee shop skill, we would plug our logic to add the item to their shopping cart and ask if they want to, "check out or add another item to their shopping cart?"
handle(handlerInput) {
const slots = handlerInput.requestEnvelope.request.intent.slots;
const drink = slots.drink.value;
let type = '';
let speechText = "Awesome! ";
if (drink === 'coffee') {
type = slots.coffeeRoast.value;
if(slots.flavor.confirmationStatus === "DENIED") {
speechText = `Ok, I've removed ${slots.flavor.value} from your order. `;
}
else if (slots.flavor.confirmationStatus === "CONFIRMED") {
speechText = `Yummy. ${slots.flavor.value} is an excellent choice. `;
}
} else if (drink === 'tea') {
type = handlerInput.requestEnvelope.request.intent.slots.teaType.value;
}
speechText += `I've added ${type} ${drink} to your shopping cart.`;
return handlerInput.responseBuilder
.speak(speechText)
.getResponse();
}
You can see what the handler looks like put together below.
const CompletedOrderIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.dialogState === "COMPLETED";
},
handle(handlerInput) {
const slots = handlerInput.requestEnvelope.request.intent.slots;
const drink = slots.drink.value;
let type;
let speechText = "Awesome! ";
if (drink === 'coffee') {
type = slots.coffeeRoast.value;
if(slots.flavor.confirmationStatus === "DENIED") {
speechText = `Ok, I've removed ${slots.flavor.value} from your order. `;
}
else if (slots.flavor.confirmationStatus === "CONFIRMED") {
speechText = `Yummy. ${slots.flavor.value} is an excellent choice. `;
}
} else if (drink === 'tea') {
type = handlerInput.requestEnvelope.request.intent.slots.teaType.value;
} else {
type = 'water';
}
speechText += `I've added ${type} ${drink} to your shopping cart.`;
return handlerInput.responseBuilder
.speak(speechText)
.getResponse();
}
}
After all that, you may be asking, what should I do if the customer doesn't provide a valid option for drink. What if they say water or baseball? In my next post I'll demonstrate how you can validate user input. Until then, think about how you would do this. What handlers would you need? How would your re-elicit a slot? What dialog directives would you need?
Now that you've read through this post, try to think about how you can put these techniques to use in your own skills. Let's continue the discussion online! You can find me on Twitter. Reach out to me at @SleepyDeveloper.