Build an Engaging Alexa Skill

Training Course

Module 5: Help Your Skill Remember with Persistent Attributes

Welcome to module 5 of our introductory course on building an engaging Alexa skill. In this module, we'll learn how to help your skill remember information with persistent attributes.
Time required: 15-30 minutes
What you’ll learn:

  • How to use persistent attributes to remember information
  • How to use the S3PersistenceAdapter to easily save information for later
  • How to use a RequestInterceptor to make your skill backend more efficient

At the end of this module, your Cake Walk skill will be able to save the user's birthday, and remember it to avoid asking the same question again. After all, what's the point of the skill collecting information if the skill won't remember it?

Use the Alexa developer console for this module. Log in to the console and open the Cake Walk skill.

Step 1: Use Amazon S3 to save and read data

Right now, you have the birthday, month, and year within the code. The problem is that the skill forgets these values when the code finishes running. To solve the problem, you are going to save the values to Amazon S3. This way, the skill can read them from session to session.

The SDK provides a useful mechanism for saving information across sessions: the AttributesManager. With the manager, your read/write code can remain the same, even if you change where you save your data later.

The backend code for Alexa skills can live on any HTTPS server. Most of the time, Alexa developers write and host their backend code using AWS. While building Cake Walk, you have been writing code in the developer console using an Alexa-hosted skill. That code is running on the AWS Free Tier, which has limitations. Alexa-hosted skills are great for learning to build and even to publish simple skills before you have a large audience. However, if your skill becomes popular, you may want to consider moving your backend code over to your own AWS resources.

How does this relate to adding memory to your skill? When using an Alexa-hosted skill for your backend code, it will be stored in Amazon S3. If you choose to build your code on your own AWS resources, it may make more sense to use Amazon DynamoDB. Don't worry if you don't know the difference between the two. The important thing to know is that the backend code you are writing now will work with Amazon S3, and it will only require minor changes to work with DynamoDB if you decide to migrate to your own AWS resources later.

Start by using the AttributesManager to save the user's birthday in Cake Walk.

a. In the developer console, click the Code tab.

b. Double-click the package.json file in the pane on the left. The file opens in the editor. 

c. Notice the "name" on line 2 is "hello-world". Change that to "cake-walk".

Find the "dependencies" section. You are going to add a dependency. It's easiest to put a new dependency at the bottom of the existing list.

d. Find the line that begins "aws-sdk". Create a new line below it, and copy and paste in the following code:

"ask-sdk-s3-persistence-adapter": "^2.0.0"

Note: When adding the new dependency, you must add a comma (,) to the end of the previous line (which begins "aws-sdk") as shown in the example.

The package.json file should look like the following now:

e. Click Save.

f. Switch back to the other file by clicking the index.js tab.

The new dependency allows you to use the AttributesManager to save and read user data using Amazon S3. Now, you need to import that dependency to the code. To do this, you need to let the code know the dependency exists.

g. In the index.js file find the line that begins const Alexa. Create a new line just below it, and copy and paste in the following code:

const persistenceAdapter = require('ask-sdk-s3-persistence-adapter');

The code should look like the following now:

h. Scroll down in the code until you find the line that begins exports.handler. Create a new line just below it, and copy and paste in the following code:

.withPersistenceAdapter(
new persistenceAdapter.S3PersistenceAdapter({bucketName:process.env.S3_PERSISTENCE_BUCKET})
)

Once added, this section of code should look like the following:

i. Click Save.

You are now set up to use AttributesManager to save and read data to Amazon S3. Later, if you decide to move your skill's backend code to your own AWS resources, you will reverse the changes made in this step.

Step 2: Make asynchronous code

Now you will modify the code to save the user's birthday. On the Code tab, within the index.js file, find the CaptureBirthdayIntentHandler. This is the handler you created in the last section. Before making changes to it, let's review the concept of asynchronous code. If you're familiar, feel free to skip ahead.

When code is run, it typically runs really fast, line by line. This is fantastic. A computer can do a lot of calculations and do them much faster than a human being can. However, when a portion of the code needs to do something that takes time before moving to the next line, the operation can get held up. It would be better if the part of the code that takes time could start and run without stopping the rest of the code from moving on. This is called asynchronous code. The reason it's important here is because saving and reading user data takes a little more time to run than the rest of the skill code. Thankfully, making a block of code asynchronous is fairly simple.

a. Find the CaptureBirthdayIntentHandler. There is a canHandle() function and a handle() function. In front of the handle() function, type async followed by a space.

async handle(handlerInput) {

That section of code should now look like the following:

b. Next, you will use the AttributesManager to save the user's birthday. Within the CaptureBirthdayIntentHandler, in the handle() function, find the line that begins const day. Create a new line just below it, and copy and paste in the following code:

const attributesManager = handlerInput.attributesManager;

The Cake Walk skill code receives the year, month, and day. You need to tell Amazon S3 to save these values. The code tells the AttributesManager what the data is, and the manager sends it to Amazon S3.

c. Within the CaptureBirthdayIntentHandler, find the line you just added (it begins const attributesManager). Create a new line just below it, and copy and paste in the following code:

const birthdayAttributes = {
"year" : year,
"month" : month,
"day" : day
};

This piece of code is mapping the variables already declared in the code to corresponding variables that will be created in Amazon S3 when the code runs.

These variables are now declared as persistent (they are local to the function in which they are declared, yet their values are retained in memory between calls to the function). Now you can save the user's data to them. First, use the AttributesManager to set the data to save to Amazon S3.

That section of code should now look like the following:

d. Within the CaptureBirthdayIntentHandler, in the handle() function, find the line that begins const speakOutput. Create a new line just above it. Copy and paste in the following code:

attributesManager.setPersistentAttributes(birthdayAttributes);

Remember adding the async keyword at the beginning of a line earlier? Now, add a line of code that takes a little time to execute as the user's information is sent to Amazon S3. This line has the await keyword at the beginning.

e. Within the CaptureBirthdayIntentHandler, in the handle() function, find the line that begins attributesManager.setPersistentAttributes. Create a new line just below it. Copy and paste in the following code:

await attributesManager.savePersistentAttributes();

That section of code should now look like the following:

const CaptureBirthdayIntentHandler = {

    canHandle(handlerInput) {

        return handlerInput.requestEnvelope.request.type === 'IntentRequest'

            && handlerInput.requestEnvelope.request.intent.name === 'CaptureBirthdayIntent';

    },

    async handle(handlerInput) {

        const year = handlerInput.requestEnvelope.request.intent.slots.year.value;

        const month = handlerInput.requestEnvelope.request.intent.slots.month.value;

        const day = handlerInput.requestEnvelope.request.intent.slots.day.value;

        

        const attributesManager = handlerInput.attributesManager;

        

        const birthdayAttributes = {

            "year": year,

            "month": month,

            "day": day

            

        };

        attributesManager.setPersistentAttributes(birthdayAttributes);

        await attributesManager.savePersistentAttributes();    

        

        const speakOutput = `Thanks, I'll remember that you were born ${month} ${day} ${year}.`;

The "await" keyword tells the code to pause and continue after the user's information is saved to Amazon S3. If you do not include the keyword, the code would continue running whether the data was saved to Amazon S3 successfully or not. That could result in the data not being saved and Alexa prompting the user for their birthday each time they launch the skill.

f. Click Save.

Step 3: Read stored data

Great, now the user's birthday is saved to Amazon S3. However, now the skill needs to be updated so the next time the user opens Cake Walk, Alexa knows the user's birthday information is stored and she doesn't have to ask for it. To do this, you will modify the code to read the data stored in Amazon S3 before asking the user for their birthday. If the data exists, Alexa doesn't need to ask for it. If the data isn't there, Alexa will ask for the information.

An Amazon S3 bucket is a public cloud storage resource. A bucket is similar to a file folder for storing objects, which consists of data and descriptive metadata.

A new handler is needed to read the stored data. The canHandle() and handle() functions in the new handler will communicate with Amazon S3. However, the functions would be doing the same thing, which is wasteful. To consolidate, use an interceptor. The interceptor intercepts the request to read the birthday information stored in Amazon S3.

a. Find the comment in the code that begins // The SkillBuilder acts as the entry point. Create a new line just above this comment. Copy and paste in the following code:

const LoadBirthdayInterceptor = {
    async process(handlerInput) {
        const attributesManager = handlerInput.attributesManager;
        const sessionAttributes = await attributesManager.getPersistentAttributes() || {};

        const year = sessionAttributes.hasOwnProperty('year') ? sessionAttributes.year : 0;
        const month = sessionAttributes.hasOwnProperty('month') ? sessionAttributes.month : 0;
        const day = sessionAttributes.hasOwnProperty('day') ? sessionAttributes.day : 0;

        if (year && month && day) {
            attributesManager.setSessionAttributes(sessionAttributes);
        }
    }
};

That section of code should now look like the following:

Now add a piece of code that registers the interceptor to let the SDK know it exists.

b. Within the exports.handler section toward the bottom of the code, find the line that begins IntentReflectorHandler) // make sure. Create a new line just below it. Copy and paste in the following code:

.addRequestInterceptors(
    LoadBirthdayInterceptor
)

That section of code should now look like the following:

Finally, add the new handler. You will add it between the LaunchRequestHandler and the CaptureBirthdayIntentHandler.

c. Find the line that begins const CaptureBirthdayIntentHandler. Create a new line just above, and copy and paste in the following code for the new handler:

const HasBirthdayLaunchRequestHandler = {
    canHandle(handlerInput) {

        const attributesManager = handlerInput.attributesManager;
        const sessionAttributes = attributesManager.getSessionAttributes() || {};

        const year = sessionAttributes.hasOwnProperty('year') ? sessionAttributes.year : 0;
        const month = sessionAttributes.hasOwnProperty('month') ? sessionAttributes.month : 0;
        const day = sessionAttributes.hasOwnProperty('day') ? sessionAttributes.day : 0;

        return handlerInput.requestEnvelope.request.type === 'LaunchRequest' && year && month && day;

    },
    handle(handlerInput) {

        const attributesManager = handlerInput.attributesManager;
        const sessionAttributes = attributesManager.getSessionAttributes() || {};

        const year = sessionAttributes.hasOwnProperty('year') ? sessionAttributes.year : 0;
        const month = sessionAttributes.hasOwnProperty('month') ? sessionAttributes.month : 0;
        const day = sessionAttributes.hasOwnProperty('day') ? sessionAttributes.day : 0;

        // TODO:: Use the settings API to get current date and then compute how many days until user's birthday
        // TODO:: Say Happy birthday on the user's birthday

        const speakOutput = `Welcome back. It looks like there are X more days until your y-th birthday.`;

        return handlerInput.responseBuilder
            .speak(speakOutput)
            .getResponse();
    }
};

The new handler has the canHandle() and handle() functions. The canHandle() function checks if the user's birthday information is saved in Amazon S3. If it is, the handler lets the SDK know it can do the work (it has the user's birthday information and can do what comes next). The handle() function tells Alexa to say, "Welcome back. It looks like there are x more days until your y-th birthday."

When you changed the name of a handler in a previous section, you also had to change the name in the list of handlers at the bottom of the code. Because you added a new handler, you must add the new handler to this list.

d. Toward the bottom of the code, find the .addRequestHandlers() function, and the line that begins LaunchRequestHandler. Create a new line just above it. Copy and paste in the following code on the new line:

HasBirthdayLaunchRequestHandler,

That section of code should now look like the following:

exports.handler = Alexa.SkillBuilders.custom()

    .withPersistenceAdapter(

        new persistenceAdapter.S3PersistenceAdapter({bucketName:process.env.S3_PERSISTENCE_BUCKET})

    )

    .addRequestHandlers(

        HasBirthdayLaunchRequestHandler,

        LaunchRequestHandler,

        CaptureBirthdayIntentHandler,

        HelpIntentHandler,

        CancelAndStopIntentHandler,

        SessionEndedRequestHandler,

        IntentReflectorHandler) // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers

    .addRequestInterceptors(

        LoadBirthdayInterceptor

    )

    .addErrorHandlers(

        ErrorHandler)

    .lambda();

e. Click Save.

f. Click Deploy.

How to delete or reset the user’s birthday

When testing, you may need to delete or reset the user's birthday. There are two ways to do this.

Use the first method in the simulator on the Test tab of the Alexa developer console. Type or say, "Alexa, tell Cake Walk I was born on {month} {day} {year}."

The second method is to delete the saved information from Amazon S3 by using the following steps:

a. While on the Code tab, click Media storage on the bottom left-hand corner of the screen. The S3 Management Console opens.

b. At the top of the page, find the breadcrumbs. Click the breadcrumb that starts amzn-1-ask-skill.

c. Click on the check box next to the file(s) that begins with amzn1.ask.account.

d. Click Actions.

e. Click Delete.

f. Click Delete. The user's birthday is deleted.

Wrap-up

Here's a summary of what you did in this section. First, you adjusted the Cake Walk skill to use the AttributesManager to save and read user information to Amazon S3. Then, you added code to the CaptureBirthdayIntentHandler to save the user's birthday. Lastly, you created a new handler (HasBirthdayLaunchRequestHandler) so Alexa doesn't repeatedly ask the same user for their birthday.

It's time to test, so click the Test tab, then follow the steps below.

Step 1: Launch the skill

Say "Open Cake Walk".

Alexa should respond, "Hello! Welcome to Cake Walk. When is your birthday?"

Tell Alexa your birthday

Feel free to try giving Alexa partial information and ensure she asks for and collects the missing information.

Once she has your birth month, day, and year, Alexa should respond, "Thanks, I'll remember that your birthday is {month} {day} {year}."

The session ends. At this point, without the code you added in this section, the next time you invoke the skill, Alexa would ask for your birthday again. Now, Alexa stores this information.

Launch the skill a second time

Say "Open Cake Walk".

Alexa should respond, “Welcome back. It looks like there are X more days until your y-th birthday.”

You probably noticed that, with the way the code works right now, Alexa is saying "X" and "Y T H". Don't worry. In the next section, you will work on the code to calculate how many days until the user's next birthday so Alexa can respond with that information.

Code

If your skill isn't working or you're getting some kind of syntax error, download the code sample in Node.js or Python from the links below. Then, go to the Code tab in the Alexa developer console and copy and paste the code into the index.js file (Node.js) or the lambda_function.py file (Python). Be sure to save and deploy the code before testing it.