I'm creating an Alexa skill where you can order a drink, there are two options: tea or coffee. I decided to use Dialog.Delegate directives to handle multi-turn conversation and ElicitSlot directives to control slot requests depending on the value of the drink 'tea or coffee'. I'm basically trying to replicate the 'Combine delegation and manual control to handle complex dialogs' example from the Alexa documentation: https://developer.amazon.com/en-US/docs/alexa/custom-skills/delegate-dialog-to-alexa.html#combine-delegation-and-manual-control-to-handle-complex-dialogs
Everything works fine, but the Dialog.Delegate directive is throwing an error. The directive correctly invokes the elicitation and confirmationStatus prompts but does not invoke the validations prompts, and when an invalid value is entered; for example 'coke'; it just repeats the elicitation prompt which makes the conversation unnatural and unintuitive. According to the documentation, nothing else needs to be done to add the validations, just add Dialog.Delegate, this is how it is in the documentation example:
Expected result
User: Alexa, tell My Coffee Shop to start my order
Alexa: Would you like coffee or tea?
User: shoes (Alexa sends the skill an IntentRequest with the OrderIntent. The drink slot contains the invalid value shoes.)
Because the dialog is not complete, the skill returns Dialog.Delegate.
Alexa: Shoes is not an item on the menu. Which would you like, coffee or tea? (Alexa responds with a prompt defined in the slot validation rules for the drink slot.)
Actual result
Alexa: Would you like coffee or tea?
User: shoes (Alexa sends the skill an IntentRequest with the OrderIntent. The drink slot contains the invalid value shoes.)
Because the dialog is not complete, the skill returns Dialog.Delegate.
Alexa: Alexa: Would you like coffee or tea?
Reply with the same elicitation prompts and it should respond with the validation propmt
I would appreciate it if you could help me understand why this is happening and tell me what I am doing wrong. I know that I could add a function in my code that validates the values of the drink and if this value is not correct, ask the user again with ElicitSlot Directive but my idea is to use Dialog.Delegate, and this directive is supposed to add the validation prompts. Thanks Alexa Devs!
Note: the values of the slots, sample utterances, and prompts are in Spanish; because I'm from Colombia!; but this does not affect the logic or operation of the skill.
My code:
Interaction Model
{
"interactionModel": {
"languageModel": {
"invocationName": "cafeteria prueba",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "HelloWorldIntent",
"slots": [],
"samples": [
"hola",
"como estás",
"di hola mundo",
"di hola",
"hola mundo"
]
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "OrderIntent",
"slots": [
{
"name": "drink",
"type": "drink",
"samples": [
"{drink}"
]
},
{
"name": "coffeeRoast",
"type": "coffeeRoast"
},
{
"name": "teaType",
"type": "teaType"
}
],
"samples": [
"quiero ordenar una bebida"
]
}
],
"types": [
{
"name": "drink",
"values": [
{
"name": {
"value": "te"
}
},
{
"name": {
"value": "café"
}
}
]
},
{
"name": "coffeeRoast",
"values": [
{
"name": {
"value": "doble"
}
},
{
"name": {
"value": "normal"
}
},
{
"name": {
"value": "negro"
}
}
]
},
{
"name": "teaType",
"values": [
{
"name": {
"value": "piña"
}
},
{
"name": {
"value": "verde"
}
},
{
"name": {
"value": "manzanilla"
}
}
]
}
]
},
"dialog": {
"intents": [
{
"name": "OrderIntent",
"delegationStrategy": "SKILL_RESPONSE",
"confirmationRequired": false,
"prompts": {},
"slots": [
{
"name": "drink",
"type": "drink",
"confirmationRequired": false,
"elicitationRequired": true,
"prompts": {
"elicitation": "Elicit.Slot.955195256762.342534459016"
},
"validations": [
{
"type": "hasEntityResolutionMatch",
"prompt": "Slot.Validation.955195256762.342534459016.331746291219"
}
]
},
{
"name": "coffeeRoast",
"type": "coffeeRoast",
"confirmationRequired": false,
"elicitationRequired": false,
"prompts": {}
},
{
"name": "teaType",
"type": "teaType",
"confirmationRequired": false,
"elicitationRequired": false,
"prompts": {}
}
]
}
],
"delegationStrategy": "ALWAYS"
},
"prompts": [
{
"id": "Elicit.Slot.955195256762.342534459016",
"variations": [
{
"type": "PlainText",
"value": "¿qué quieres tomar café o te?"
}
]
},
{
"id": "Slot.Validation.955195256762.342534459016.331746291219",
"variations": [
{
"type": "PlainText",
"value": "{drink} no está incluido en el menú ¿quieres café o te?"
}
]
}
]
}
}
Lambda (index.js)
I have not modified the other files, they are as they are created by Alexa Console auto-hosted node.js with the Start From Scratch template
/* *
* This sample demonstrates handling intents from an Alexa skill using the Alexa Skills Kit SDK (v2).
* Please visit https://alexa.design/cookbook for additional examples on implementing slots, dialog management,
* session persistence, api calls, and more.
* */
const Alexa = require('ask-sdk-core');
const LaunchRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest';
},
handle(handlerInput) {
const speakOutput = 'Welcome, you can say Hello or Help. Which would you like to try?';
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
const CoffeeGivenOrderIntentHandler = {
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 === 'café'
&& !handlerInput.requestEnvelope.request.intent.slots.coffeeRoast.value
},
handle(handlerInput) {
return handlerInput.responseBuilder
.speak('¿Cómo quieres el café normal, negro o doble?')
.reprompt('¿Cómo quieres el café normal, negro o doble?')
.addElicitSlotDirective('coffeeRoast')
.getResponse();
}
};
const TeaGivenOrderIntentHandler = {
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 === 'te'
&& !handlerInput.requestEnvelope.request.intent.slots.teaType.value
},
handle(handlerInput) {
return handlerInput.responseBuilder
.speak("¿Quieres el te de manzanilla, piña o verde?")
.reprompt("Would you like a black, green, oolong, or white tea?")
.addElicitSlotDirective('teaType')
.getResponse();
}
};
const StartedInProgressOrderIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.dialogState !== 'COMPLETED';
},
handle(handlerInput) {
const currentIntent = handlerInput.requestEnvelope.request.intent;
let drink = currentIntent.slots.drink.validations.type;
return handlerInput.responseBuilder
.speak(drink)
.addDelegateDirective(currentIntent)
.getResponse();
}
};
const CompletedOrderIntentHandler = {
canHandle(handlerInput) {
return handlerInput.requestEnvelope.request.type === "IntentRequest"
&& handlerInput.requestEnvelope.request.intent.name === "OrderIntent"
&& handlerInput.requestEnvelope.request.dialogState === "COMPLETED";
},
handle(handlerInput){
const drink = handlerInput.requestEnvelope.request.intent.slots.drink.value;
let type;
if (drink === 'café') {
type = handlerInput.requestEnvelope.request.intent.slots.coffeeRoast.value;
} else if (drink === 'te') {
type = handlerInput.requestEnvelope.request.intent.slots.teaType.value;
} else {
type = 'agua';
}
const speechText = `Toma tu ${type} ${drink}`;
return handlerInput.responseBuilder
.speak(speechText)
.getResponse();
}
};
const HelpIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.HelpIntent';
},
handle(handlerInput) {
const speakOutput = 'You can say hello to me! How can I help?';
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
const CancelAndStopIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& (Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.CancelIntent'
|| Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.StopIntent');
},
handle(handlerInput) {
const speakOutput = 'Goodbye!';
return handlerInput.responseBuilder
.speak(speakOutput)
.getResponse();
}
};
/* *
* FallbackIntent triggers when a customer says something that doesn’t map to any intents in your skill
* It must also be defined in the language model (if the locale supports it)
* This handler can be safely added but will be ingnored in locales that do not support it yet
* */
const FallbackIntentHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'AMAZON.FallbackIntent';
},
handle(handlerInput) {
const speakOutput = 'Sorry, I don\'t know about that. Please try again.';
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
/* *
* SessionEndedRequest notifies that a session was ended. This handler will be triggered when a currently open
* session is closed for one of the following reasons: 1) The user says "exit" or "quit". 2) The user does not
* respond or says something that does not match an intent defined in your voice model. 3) An error occurs
* */
const SessionEndedRequestHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'SessionEndedRequest';
},
handle(handlerInput) {
console.log(`~~~~ Session ended: ${JSON.stringify(handlerInput.requestEnvelope)}`);
// Any cleanup logic goes here.
return handlerInput.responseBuilder.getResponse(); // notice we send an empty response
}
};
/* *
* The intent reflector is used for interaction model testing and debugging.
* It will simply repeat the intent the user said. You can create custom handlers for your intents
* by defining them above, then also adding them to the request handler chain below
* */
const IntentReflectorHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest';
},
handle(handlerInput) {
const intentName = Alexa.getIntentName(handlerInput.requestEnvelope);
const speakOutput = `You just triggered ${intentName}`;
return handlerInput.responseBuilder
.speak(speakOutput)
//.reprompt('add a reprompt if you want to keep the session open for the user to respond')
.getResponse();
}
};
/**
* Generic error handling to capture any syntax or routing errors. If you receive an error
* stating the request handler chain is not found, you have not implemented a handler for
* the intent being invoked or included it in the skill builder below
* */
const ErrorHandler = {
canHandle() {
return true;
},
handle(handlerInput, error) {
const speakOutput = 'Sorry, I had trouble doing what you asked. Please try again.';
console.log(`~~~~ Error handled: ${JSON.stringify(error)}`);
return handlerInput.responseBuilder
.speak(speakOutput)
.reprompt(speakOutput)
.getResponse();
}
};
/**
* This handler acts as the entry point for your skill, routing all request and response
* payloads to the handlers above. Make sure any new handlers or interceptors you've
* defined are included below. The order matters - they're processed top to bottom
* */
exports.handler = Alexa.SkillBuilders.custom()
.addRequestHandlers(
LaunchRequestHandler,
CoffeeGivenOrderIntentHandler,
TeaGivenOrderIntentHandler,
StartedInProgressOrderIntentHandler,
CompletedOrderIntentHandler,
HelpIntentHandler,
CancelAndStopIntentHandler,
FallbackIntentHandler,
SessionEndedRequestHandler,
IntentReflectorHandler)
.addErrorHandlers(
ErrorHandler)
.withCustomUserAgent('sample/hello-world/v1.2')
.lambda();
I have same problem when I create "Accept only Slot Type's values and synonyms" for validation rule. I tried on Evaluate Model but the slot value is always "not filled". If I tried it on Simulator, the slot value is empty. The slot validation speech never play. So, I catch this validation in Lambda.