Wednesday, 15 March 2017

Amazon Alexa Session Attributes don't persist

I'm using the nodejs 'alexa-sdk' to develop a pension calculator skill for Amazon Alexa. The problem I have is that when switching from one state to another it doesn't keep the session attributes. It's like talking to someone who has alzheimer. I'm storing them like

this.attributes['ATTRIBUTE_NAME'] = 'some_value';

But when I get to the point to give an answer (Alexa.CreateStateHandler(states.ANSWER[...]), all attributes are 'undefined'. Can anyone advise what I'm doing wrong with storing or passing on the session attributes?

var Alexa = require('alexa-sdk');
var moment = require('moment'); // deals with dates and date formatting, for instance converts AMAZON.DATE to timestamp

// import pension calculator
var calculator = require('./pensionCalculator');

var GENDERCUTOFFDATE = new Date(1953, 12, 6);

// States are required for conversational skills.
// States assume a context. e.g. _DOB expects date of birth; providing a gender in this state would confuse Alexa.
// UX design principle have to be as unambiguous as possible in language (e.g. "are you male or female" vs "what gender are you?")
var states = {
    START: '_STARTMODE',  // Prompt the user to start or restart
    DOB: '_DOB',
    GENDER: '_GENDER',
    ANSWER: '_ANSWER'
};


// Outbound messages spoken back to end user.
// alexa-nodejs-sdk wraps all strings in the advanced SSML speech markup (<speak>STRING HERE</speak>) that allows phonetic overrides etc.
var snippets = {
            WELCOME: "<s>Welcome to the D W P Pension Age calculator.</s> " +
            "<s>You can ask to calculate your pension age or for the U K pension eligibility criteria?</s>",

            WELCOME_REPROMPT: "You can say, " +
            "Calculate my pension age or, say what are the eligibility criteria in the U K.",

            GENDER: "Thank you. Are you female or male?",

            GENDER_REPROMPT: "In order to calculate your pension age, please tell me: Are you male or female?",

            GENDER_INVALID: "Sorry I couldn't understand your gender, can you please tell me if you are you male or female?",

            DATEOFBIRTH: "Ok, please tell me what is your date of birth?",

            DATEOFBIRTH_REPROMPT: "In order to calculate your pension age please tell me your date of birth?",

            DATEOFBIRTH_INVALID_FUTURE: "Nice you're from the future. Did you bring a hoverboard? Seriously, can you please say your actual date of birth please?",

            DATEOFBIRTH_INVALID: "Please say your date of birth. For example you can say, my date of birth is the 23rd of April 1980",

            STOP: "Thank you for using the D W P pension calculator.",

            HELP: "You can ask things like: What is my pension age or what are the eligibility criteria.",

            HELP_REPROMPT: "Simply say: calculate pension or eligibility criteria.",

            UNHANDLED: "I'm sorry I couldn't understand what you meant. Can you please say it again?"
};


// You define a set of state handlers for every state PLUS the new session / launch event.
var newSessionHandlers = {

    // session variables stored in this.attributes
    // session state is stored in this.handler.state
    // handler.state vs Intent vs
    'LaunchRequest': function() {
        // Initialise State
        this.handler.state = states.START;

        // emitWithState should be called executeStateHandler("Start").
        // As such this will call a handler "Start" in the startStateHandlers object.
        // Maybe this line and the previous line could be more coherently wrapped into a single
        // function:
        // this.stateTransition( states.START, "Start" )
        this.emit("Start")
    },

    // It's unclear whether this can ever happen as it's triggered by Alexa itself.
    "Unhandled": function () {
        var speechText = "I wasn't launched yet";
        this.emit(":ask", speechText);
    }
};



// This is the beginning of our skill.
// This is a list of accepted intents Alexa is listening for
// when the skill has just started.
// In this specific version, a user can't provide things like date of birth
// or gender as part of the initial skill invocation because we've not included in this set of start state handlers.
// We could but haven't in this particular scenario.
var startStateHandlers = Alexa.CreateStateHandler(states.START, {
    'Start': function() {


        this.attributes['dob'] = '';
        this.attributes['gender'] = '';


        this.handler.state = states.START;
        var speechText = snippets.WELCOME;
        var repromptText = snippets.WELCOME_REPROMPT;

        // emit is the exit point to instruct Alexa how to and what to communicate with end user.
        // e.g. do we want further information? (:ask) / no further information, skill terminates (:tell)
        // do we provide a voice response with or without a card on the mobile device (:ask vs :askWithCard)
        // https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs
        // as we've said :ask we are expecting the user to provide more information.
        // maybe this function could be called this.respond()
        // this is going to speak the snippets.WELCOME which implicitly asks a question (hence :ask).
        // reprompt text is automatically spoken after a few seconds. This is a feature of the NodeJS SDK.
        // See Unhandled for the fallback / unrecognised utteranes.
        this.emit(':ask', speechText, repromptText);
    },

    // the intent text is defined in the
    // Alexa interaction model web page at developer.amazon.com/ask
    // represented as sample utterances.
    'StartCalculationIntent': function () {
        var speechText = snippets.DATEOFBIRTH;
        var repromptText = snippets.DATEOFBIRTH_REPROMPT;

        // Change State to calculation
        this.handler.state = states.DOB;
        this.emit(':ask', speechText, repromptText);
    },

    // a predefined Utterance that you don't need to define in your interaction model
    // We are choosing to provide this help function but equally you don't need to.
    "AMAZON.HelpIntent": function () {
        var speechText = snippets.HELP;
        var repromptText = snippets.HELP_REPROMPT;
        this.emit(':ask', speechText, repromptText);
    },
    "Unhandled": function () {
        var speechText = snippets.UNHANDLED;
        this.emit(":ask", speechText);
    },

    // User says stop. Stops even in the middle of a response.
    "AMAZON.StopIntent": function () {
        var speechText = snippets.STOP;
        this.emit(":tell", speechText);
    },

    // unclear really what the difference is; default working practice is
    // to do the same thing
    // in a production system we'd probably dedupe this function.
    "AMAZON.CancelIntent": function () {
        var speechText = snippets.STOP;
        this.emit(":tell", speechText);
    },
    "AMAZON.StartOverIntent": function () {
        this.emit("Start")
    },

    // TODO determine when this is requested and what initiates it
    // Implement handler to save state if state should be stored persistently e.g. to DynamoDB
    // 'SessionEndedRequest': function () {
    //     console.log('session ended!');
    //     this.emit(':saveState', true);
    // }

    // TODO add 'AMAZON.RepeatIntent' that repeats the last question.

});



var dobStateHandlers = Alexa.CreateStateHandler(states.DOB, {

    'DateOfBirthIntent': function () {

        var speechText = "",
            repromptText = "";

        var date_string = this.event.request.intent.slots.dob.value;
        var date = moment(date_string);

        if (date.isValid()) {

            if (!isFutureDate(date)) {
                // ALL GOOD – dob not in the future
                speechText = snippets.GENDER;
                repromptText = snippets.GENDER_REPROMPT;
                this.attributes['dob'] = date_string;

                if(isGenderNeeded(date)) {
                    // Transition to next state
                    this.handler.state = states.GENDER;
                    // this.emit(":saveState", false);
                    this.emit(':ask', speechText, repromptText);

                } else {
                    // gender not needed
                    // this.attributes['gender'] = "unisex";
                    this.handler.state = states.ANSWER;
                    // this.emit(":saveState", false);
                    this.emit("Answer")

                }



            } else {
                // dob in the future
                speechText = snippets.DATEOFBIRTH_INVALID_FUTURE;
                // this.emit(":saveState", false);
                repromptText = snippets.DATEOFBIRTH_INVALID_FUTURE; // could be improved by using alternative prompt text
                this.emit(':ask', speechText, repromptText);
            }

        } else {
            // not a valid Date
            speechText = snippets.DATEOFBIRTH_INVALID;
            // this.emit(':saveState', false);
            repromptText = snippets.DATEOFBIRTH_INVALID; // could be improved by using alternative prompt text
            this.emit(':ask', speechText, repromptText);
        }
    },
    "AMAZON.HelpIntent": function () {
        var speechText = snippets.HELP;
        var repromptText = snippets.HELP_REPROMPT;
        this.emit(':ask', speechText, repromptText);
    },
    "Unhandled": function () {
        var speechText = snippets.UNHANDLED;
        this.emit(":ask", speechText);
    },
    "AMAZON.StopIntent": function () {
        var speechText = snippets.STOP;
        this.emit(":tell", speechText);
    },
    "AMAZON.CancelIntent": function () {
        var speechText = snippets.STOP;
        this.emit(":tell", speechText);
    },
    "AMAZON.StartOverIntent": function () {
        this.emit("Start")
    },
    'SessionEndedRequest': function () {
        // this.emit(':saveState', false);
        // this.attributes['dob'] = date_string;
        // this.attributes['dob'] = date_string;
        console.log('session ended!');
    }
});


var genderStateHandlers = Alexa.CreateStateHandler(states.GENDER, {
    'GenderIntent': function () {
        var speechText = "",
            repromptText = "";

        var gender = this.event.request.intent.slots.gender.value;

        if (isGenderSlotValid(gender)) {
            // valid gender
            this.attributes['gender'] = gender;
            this.handler.state = states.ANSWER;

            this.emit(':saveState', false);
            this.emit("Answer");
        } else {
            // not a valid gender
            speechText = snippets.GENDER_INVALID;
            repromptText = snippets.GENDER_INVALID; // could be improved by using alternative prompt text

            this.emit(':saveState', false);
            this.emit(':ask', speechText, repromptText);
        }
    },
    "AMAZON.HelpIntent": function () {
        var speechText = snippets.HELP;
        var repromptText = snippets.HELP_REPROMPT;
        this.emit(':ask', speechText, repromptText);
    },
    "Unhandled": function () {
        var speechText = snippets.UNHANDLED;
        this.emit(":ask", speechText);
    },
    "AMAZON.StopIntent": function () {
        var speechText = snippets.STOP;
        this.emit(":tell", speechText);
    },
    "AMAZON.CancelIntent": function () {
        var speechText = snippets.STOP;
        this.emit(":tell", speechText);
    },
    "AMAZON.StartOverIntent": function () {
        this.emitWithState("Start")
    },
    'SessionEndedRequest': function () {
        this.emit(':saveState', false);
        console.log('session ended!');
    }
});



var answerStateHandlers = Alexa.CreateStateHandler(states.ANSWER, {
    'Answer': function () {
        console.log(`##### START INPUT SNIPPETS #####`);
        // var dob =
        // var gender = ;

        console.log(`this.attributes.dob: "${this.attributes.dob}"`);
        console.log(`this.attributes.gender: "${this.attributes.gender}"`);

        var pensionDate = calculator.calculatePension(this.attributes.dob, this.attributes.gender);
        console.log(`pensionDate: "${pensionDate}"`);

        var speechText = calculator.createPensionSnippet(pensionDate);

        // Change State to Start again
        this.handler.state = states.START;
        this.emit(':tell', speechText);

        console.log(`##### STOP SNIPPET #####`);
        console.log(``);

    },
    "Unhandled": function () {
        var speechText = snippets.UNHANDLED;
        this.emit(":ask", speechText);
    }
});



function isGenderSlotValid(gender) {
    if (gender == "male" || gender == "female") {
        return true
    } else {
        return false
    }
}


function isFutureDate(dob) {
    var today = moment();

    if (dob > today) {
        return true
    } else {
        return false
    }
}

function isGenderNeeded(dob) {
    return dob < GENDERCUTOFFDATE;
}




exports.handler = function(event, context, callback) {
    var alexa = Alexa.handler(event, context);
    alexa.appId = process.env.appId;
    // alexa.appId = "your skill ID"; // better store it as ENV variable at AWS Lambda
    // alexa.resources = languageStrings;

    // register intent handlers for each state + new session.
    // Each state constrains possible intents Alexa is listening for.
    // If you only have one handler you are context-free and cannot have state.
    alexa.registerHandlers(newSessionHandlers, startStateHandlers, dobStateHandlers, genderStateHandlers, answerStateHandlers);
    alexa.execute();
};



via cdudek

No comments:

Post a Comment