Search code examples
node.jsalexa-skills-kit

Alexa input validation for type AMAZON.NUMBER using dialog model


I am using the ASK SDK 2.0 for Node.js. I have a skill that uses a dialog model to prompt the user for a series of inputs, all of which are required slots with the type AMAZON.NUMBER. When a user gives a numerical response, everything works fine. However, if the user gives a non-numeric response, such as "yellow", the slot value is filled in with:

"value": "?"

and moves on to propmpt the user for the input for the next slot. How can I get it to reprompt the user for that slot again if they provide an invalid response? I've poured over the documentation and can't find anything. Ideally I would want the skill to reprompt the user until a valid input is given (i.e., the value isn't "?")

(Strangely, if i set the type to AMAZON.DATE, it will automatically reprompt once, and then if an invalid type is provided a second time the skill will just quit out.)

My response looks like this:

"response": {
    "directives": [{
        "type": "Dialog.Delegate",
        "updatedIntent": {
            "name": "MRRIntent",
            "confirmationStatus": "NONE",
            "slots": {
                "growthValue": {
                    "name": "growthValue",
                    "value": "?",
                    "confirmationStatus": "NONE"
                },
                "churnValue": {
                    "name": "churnValue",
                    "value": "?",
                    "confirmationStatus": "NONE"
                },
                "startingValue": {
                    "name": "startingValue",
                    "value": "10",
                    "confirmationStatus": "NONE"
                }
            }
        }
    }]
}

Where in this example startingValue was given a numerical response, but the other two (growthValue and churnValue) were given non-number responses.

Where in the intent handler would I be able to check for this value and reprompt on the particular slots that are failing? I am using the Dialog.Directive which the docs say not to use reprompt with ( https://developer.amazon.com/docs/custom-skills/dialog-interface-reference.html#details ), unless I am misunderstanding it.

My handlers look like this:

const InProgressPlanMyTripHandler = {
    canHandle(handlerInput) {
        const request = handlerInput.requestEnvelope.request;
        return request.type === 'IntentRequest' &&
            request.intent.name === 'MRRIntent' &&
            request.dialogState !== 'COMPLETED';
    },
    handle(handlerInput) {
        const currentIntent = handlerInput.requestEnvelope.request.intent;
        return handlerInput.responseBuilder
            .addDelegateDirective(currentIntent)
            .getResponse();
    },
};

const CompletedPlanMyTripHandler = {
    canHandle(handlerInput) {
        const request = handlerInput.requestEnvelope.request;
        return request.type === 'IntentRequest' && request.intent.name === 'MRRIntent';
    },
    handle(handlerInput) {

        const responseBuilder = handlerInput.responseBuilder;
        const filledSlots = handlerInput.requestEnvelope.request.intent.slots;
        const slotValues = getSlotValues(filledSlots);

        let speechOutput = `Your values are: startingValue: ${slotValues.startingValue.synonym} growthValue: ${slotValues.growthValue.synonym}, and churnValue: ${slotValues.churnValue.synonym}`

        return responseBuilder
            .speak(speechOutput)
            .getResponse();
    },
};

I used the Plan My Trip example as my starting point, so the vast majority of my code is going to be identical to it: https://github.com/alexa/alexa-cookbook/tree/master/feature-demos/skill-demo-plan-my-trip

What am I missing / not understanding? Thanks


Solution

  • I believe I've found a solution -- I'm not sure it's the best solution, but it appears to work.

    In the CompletedPlanMyTripHandler, I added the following check to the handle method:

    handle(handlerInput) {
        const currentIntent = handlerInput.requestEnvelope.request.intent;
    
        let slots = currentIntent.slots;
        let badInputSlot;
        for (let x in slots) {
            if (slots.hasOwnProperty(x) && slots[x].hasOwnProperty('value')) {
                if (isNaN(slots[x].value)) {
                    badInputSlot = x;
                    break;
                }
            }
        }
        if (badInputSlot) {
            return handlerInput.responseBuilder.speak('I do not understand. Please respond with a number.').reprompt('Please respond with a number').addElicitSlotDirective(badInputSlot, currentIntent).getResponse();
        } else {
            return handlerInput.responseBuilder
                .addDelegateDirective(currentIntent)
                .getResponse();
        }
    },
    

    What it's doing is in the for loop its checking the the slots on the intent, seeing if each one has a value property (which is only added once a response has been supplied), and then does an isNaN check on it to see if the value is in fact valid. If it is not, we need to ask the user again for that value, so we store the slot's name in badInputSlot and break out of the loop.

    Now, we move on to the if statement, and if there is a value assigned to badInputSlot we return an elicit slot directive for the specific slot that had a bad value.

    Then, after the user supplies a new value, we repeat the process until every slot in handlerInput.requestEnvelope.request.intent.slots has a value property that passes our isNaN validation check. Once that happens, badInputSlot will be undefined, and our if statement will go to the else block and return a delegate directive to finish off the intent.