Help Your Skill Remember with Attributes Manager

Step 2: Use persistent attributes

At its most basic, the difference between session attributes and persistent attributes is how long they last. Session attributes last until the end of the session, but persistent attributes persist beyond the end of the session.

What attributes would be good to remember beyond the session?

First, we'll remember whether our customer has used our skill before. Back in module 2, when we scripted an exchange, remember that it specified the customer was a new user. Why would that matter?

It would change how you welcome them. In this section you'll create two greetings to use depending on different situations.

  • New player
  • Returning player

Second, just like you don't want to repeat celebrities in a session, you may want to not repeat them ever. So you'll store that past_celebs array for future use.

Step 2 has five sub-steps:

  • First, install a persistence adapter in your skill
  • Next, create interceptors to make attributes easier to handle
  • Next, update your launch to welcome returning players
  • Next, update your answer checker to remember past_celebs long term
  • Last, check your data in DynamoDB

First, install a persistence adapter in your skill

Amazon offers a couple of readily available persistence adapters. The S3 persistence adapter will save your data as a JSON file in Amazon S3 (Simple Storage Service). As your game gets more complex and stores more complex data, it may need to share with other tools and services or you might just want the power of a database. In this workshop, you'll learn to use the Amazon DynamoDB persistence adapter.

For details about the adapter features, see Use DynamoDB for Data Persistence with Your Alexa-hosted Skill in the Alexa Skills Kit documentation.

First, you need to add your dependencies to package.json (Node.js) or requirements.txt (Python).

  1. In the developer console, on the Code tab page, in the left-hand file tree, locate a file named package.json.

  2. Click the package.json file to open it.

    Package.json screenshot

  3. In the main Code tab window, in the package.json file, at line 11 (see in the previous screenshot), locate the section named dependencies. At the end of the last line in that section (the line that loads aws-sdk), type a comma (,).

  4. Insert another line, and copy and paste in the following code to load your persistence adapter.

"ask-sdk-dynamodb-persistence-adapter": "^2.10.0"
  1. In the developer console, on the Code tab page, in the left-hand file tree, locate a file named requirements.txt.

  2. Click the requirements.txt file to open it.

    Requirements.txt screenshot

  3. In the main Code tab window, in the requirements.txt file, add a new line at the bottom of the file.

  4. Copy and paste in the following code to load your persistence adapter.

ask-sdk
ask-sdk-dynamodb-persistence-adapter==1.15.0
  1. In the upper-right corner of the Code tab page, click Save, and then click Deploy to deploy your code.

    Next, you need to go to the top of your index.js or lambda_function.py file to load the adapter into your skill code.

  2. In the index.js or lambda_function.py file, at the top of the file, find the following code.

const Alexa = require('ask-sdk-core');
import logging
import ask_sdk_core.utils as ask_utils

import json
from ask_sdk_model.interfaces.alexa.presentation.apl import (
    RenderDocumentDirective)

Immediately after this code, copy and paste the following code.

const AWS = require('aws-sdk');
const ddbAdapter = require('ask-sdk-dynamodb-persistence-adapter');

// are you tracking past celebrities between sessions
const celeb_tracking = true;
import os
import boto3
import json

from ask_sdk.standard import StandardSkillBuilder
from ask_sdk_dynamodb.adapter import DynamoDbAdapter
from ask_sdk_core.dispatch_components import AbstractRequestInterceptor
from ask_sdk_core.dispatch_components import AbstractResponseInterceptor

# are you tracking past celebrities between sessions
CELEB_TRACKING = True
  1. In the index.js or lambda_function.py file, scroll to very the bottom.
  1. In the line under the following code.
.addErrorHandlers(
        ErrorHandler)

Copy and paste the following code.

.withPersistenceAdapter(
    new ddbAdapter.DynamoDbPersistenceAdapter({
        tableName: process.env.DYNAMODB_PERSISTENCE_TABLE_NAME,
        createTable: false,
        dynamoDBClient: new AWS.DynamoDB({apiVersion: 'latest', region: process.env.DYNAMODB_PERSISTENCE_REGION})
    })
)
  1. Replace this line of code.
sb = SkillBuilder()

with this line of code

sb = StandardSkillBuilder(
    table_name=os.environ.get("DYNAMODB_PERSISTENCE_TABLE_NAME"), auto_create_table=False)

Next, create interceptors to make attributes easier to handle

Interceptors are functions that run every time your code runs in the data access layer. Request Interceptors run before the code runs through your can handle functions. Response interceptors run after the handlers have run, but before the response is sent back to Alexa.

In the handlers, you added code to check if session variables had been set and now you'll need to load both session AND persistent variables. It can clutter up your handler code and make it harder to read. But with a request interceptor, you can make sure all these variables have been initialized and the values made available before any of the handlers run.

Similarly, there are three different handlers where you might want to update a persistent attribute or two. That requires repeating two lines of code three times to set and save your persistent attributes. Instead, you can put them in a response interceptor once.

Interceptors can also be used to log the incoming requests and outgoing responses for debugging purposes.

  1. In the index.js or lambda_function.py file, scroll to the very bottom.

  2. Below the list of handlers, in the line under the following code.

  FallbackIntentHandler,
  SessionEndedRequestHandler,
  IntentReflectorHandler)
sb.add_exception_handler(CatchAllExceptionHandler())

Copy and paste the following code to register 2 request interceptors and 2 response interceptors.

.addRequestInterceptors(
    LoadDataInterceptor,
    LoggingRequestInterceptor
)
.addResponseInterceptors(
    SaveDataInterceptor,
    LoggingResponseInterceptor
)
# Interceptors
sb.add_global_request_interceptor(LoadDataInterceptor())
sb.add_global_request_interceptor(LoggingRequestInterceptor())

sb.add_global_response_interceptor(SaveDataInterceptor())
sb.add_global_response_interceptor(LoggingResponseInterceptor())

This new code performs the following tasks:

  • Before running through the request handlers, the LoadDataInterceptor and LoggingRequestInterceptor runs.
  • After the chosen handler returns its result, the SaveDataInterceptor and LoggingResponseInterceptor runs.
  1. Go above the code statements that prepare the result and after the ErrorHandler (Node.js) CatchAllExceptionHandler (Python) code (about 22 lines up, above the comments), copy and paste the following Request Interceptors code.
const LoadDataInterceptor = {
    async process(handlerInput) {
        const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();

        // get persistent attributes, using await to ensure the data has been returned before
        // continuing execution
        var persistent = await handlerInput.attributesManager.getPersistentAttributes();
        if(!persistent) persistent = {};

        // ensure important variables are initialized so they're used more easily in handlers.
        // This makes sure they're ready to go and makes the handler code a little more readable
        if(!sessionAttributes.hasOwnProperty('current_celeb')) sessionAttributes.current_celeb = null;  
        if(!sessionAttributes.hasOwnProperty('score')) sessionAttributes.score = 0;
        if(!persistent.hasOwnProperty('past_celebs')) persistent.past_celebs = [];  
        if(!sessionAttributes.hasOwnProperty('past_celebs')) sessionAttributes.past_celebs = [];  

        // if you're tracking past_celebs between sessions, use the persistent value
        // set the visits value (either 0 for new, or the persistent value)
        sessionAttributes.past_celebs = (celeb_tracking) ? persistent.past_celebs : sessionAttributes.past_celebs;
        sessionAttributes.visits = (persistent.hasOwnProperty('visits')) ? persistent.visits : 0;

        //set the session attributes so they're available to your handlers
        handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
    }
};
// This request interceptor will log all incoming requests of this lambda
const LoggingRequestInterceptor = {
    process(handlerInput) {
        console.log('----- REQUEST -----');
        console.log(JSON.stringify(handlerInput.requestEnvelope, null, 2));
    }
};
class LoadDataInterceptor(AbstractRequestInterceptor):
    """Check if user is invoking skill for first time and initialize preset."""
    def process(self, handler_input):
        # type: (HandlerInput) -> None
        persistent_attributes = handler_input.attributes_manager.persistent_attributes
        session_attributes = handler_input.attributes_manager.session_attributes

        # ensure important variables are initialized so they're used more easily in handlers.
        # This makes sure they're ready to go and makes the handler code a little more readable
        if 'current_celeb' not in session_attributes:
            session_attributes["current_celeb"] = None

        if 'score' not in session_attributes:
            session_attributes["score"] = 0

        if 'past_celebs' not in persistent_attributes:
            persistent_attributes["past_celebs"] = []

        if 'past_celebs' not in session_attributes:
            session_attributes["past_celebs"] = []

        # if you're tracking past_celebs between sessions, use the persistent value
        # set the visits value (either 0 for new, or the persistent value)
        session_attributes["past_celebs"] = persistent_attributes["past_celebs"] if CELEB_TRACKING else session_attributes["past_celebs"]
        session_attributes["visits"] = persistent_attributes["visits"] if 'visits' in persistent_attributes else 0

class LoggingRequestInterceptor(AbstractRequestInterceptor):
    """Log the alexa requests."""
    def process(self, handler_input):
        # type: (HandlerInput) -> None
        logger.debug('----- REQUEST -----')
        logger.debug("{}".format(
            handler_input.request_envelope.request))

The first request interceptor, LoadDataInterceptor, performs the following tasks:

  • In the second to last inner block, the loader checks that celeb_tracking variable.
  • If the variable is true, the persistent value for the past celebrities is put in the session variable for it.

The second request interceptor, LoggingRequestInterceptor, simply logs the incoming request which can be later found in Amazon CloudWatch.

  1. Directly below the previously added code block, copy and paste the following code.
// Response Interceptors run after all skill handlers complete, before the response is
// sent to the Alexa servers.
const SaveDataInterceptor = {
    async process(handlerInput) {
        const persistent = {};
        const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();
        // save (or not) the past_celebs & visits
        persistent.past_celebs = (celeb_tracking) ? sessionAttributes.past_celebs : [];
        persistent.visits = sessionAttributes.visits;
        // set and then save the persistent attributes
        handlerInput.attributesManager.setPersistentAttributes(persistent);
        let waiter = await handlerInput.attributesManager.savePersistentAttributes();
    }
};
// This response interceptor will log all outgoing responses of this lambda
const LoggingResponseInterceptor = {
    process(handlerInput, response) {
        console.log('----- RESPONSE -----');
        console.log(JSON.stringify(response, null, 2));
    }
};
class SaveDataInterceptor(AbstractResponseInterceptor):
    """Save persistence attributes before sending response to user."""
    def process(self, handler_input, response):
        # type: (HandlerInput, Response) -> None
        persistent_attributes = handler_input.attributes_manager.persistent_attributes
        session_attributes = handler_input.attributes_manager.session_attributes

        persistent_attributes["past_celebs"] = session_attributes["past_celebs"] if CELEB_TRACKING  else []
        persistent_attributes["visits"] = session_attributes["visits"]

        handler_input.attributes_manager.save_persistent_attributes()

class LoggingResponseInterceptor(AbstractResponseInterceptor):
    """Log the alexa responses."""
    def process(self, handler_input, response):
        # type: (HandlerInput, Response) -> None
        logger.debug('----- RESPONSE -----')
        logger.debug("{}".format(response))

This first response interceptor, SaveDataInterceptor, performs the following tasks:

  • If the persistent tracking of celebrities is happening, the session variable (which is updated if an answer is checked) is copied to the persistent variable.
  • The persistent variables are set, then saved.

The second response interceptor, LoggingResponseInterceptor, simply logs the incoming request which can be later found in Amazon CloudWatch.

Next, update how you launch your skill to welcome returning users

  1. In the index.js or lambda_function.py file, scroll up to nearly the top of the file, and then find the LaunchRequestHandler.

    Next, you want to get the attributes and do something with them. Because the LoadDataInterceptor request interceptor initialized everything, you simply need to pull in the session attributes, and then make the skill greeting conditional.

  2. Replace the following code.

const speakOutput =
  `Welcome to Cake Time. I'll tell you a celebrity name and you try
  to guess the month and year they were born. See how many you can get!
  Would you like to play?`;
speak_output =  f"Welcome to Cake Time. " \
    f"I'll tell you a celebrity name and you try " \
    f"to guess the month and year they were born. " \
    f"See how many you can get! " \
    f"Would you like to play?"

With the following code, by cutting and pasting it into the index.js or lambda_function.py file.

var speakOutput = "";
const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();

if(sessionAttributes.visits === 0){
    speakOutput = `Welcome to Cake Time. I'll tell you a celebrity name and
        you try to guess the month and year they were born. See how many you can get!
        Would you like to play?`;
} else {
    speakOutput = `Welcome back to Cake Time! Ready to guess some more celebrity
        birthdays?`
}

// increment the number of visits and save the session attributes so the
// ResponseInterceptor will save it persistently.
sessionAttributes.visits += 1;
handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
speak_output = ''
session_attributes = handler_input.attributes_manager.session_attributes

if session_attributes["visits"] == 0:
    speak_output = f"Welcome to Cake Time. " \
        f"I'll tell you a celebrity name and you try " \
        f"to guess the month and year they were born. " \
        f"See how many you can get! " \
        f"Would you like to play?"
else:
    speak_output = f"Welcome back to Cake Time! " \
        f"Ready to guess some more celebrity birthdays?"

# increment the number of visits and save the session attributes so the
# ResponseInterceptor will save it persistently.
session_attributes["visits"] = session_attributes["visits"] + 1

With this new code, you can choose which greeting to use based on the number of visits, and then increment the number and save it. You can have more greetings of different kinds. You can have the skill tell users their current score and standing on the leaderboards. You can even have a daily bonus that users can earn by coming back to the skill each day to check whether this visit earned the bonus for them.

Next, update your answer checker to remember past_celebs long term

First, you need to scroll down to the PlayGameHandler and delete some code. Why? Because the logic of whether to remember everything long term is being handled in the LoadDataInterceptor request interceptor and SaveDataInterceptor response interceptor, as is all of the attribute initialization.

  1. In the index.js or lambda_function.py file, scroll down and locate PlayGameHandler.

  2. Replace the following code.

//check if there's a current celebrity. If so, repeat the question and exit.
if (
    sessionAttributes.hasOwnProperty('current_celeb') &&
    sessionAttributes.current_celeb !== null
){
if 'current_celeb' in session_attributes.keys() and session_attributes["current_celeb"] != None:

With the following code, copying and pasting it into the index.js or lambda_function.py file.

//check if there's a current celebrity. If so, repeat the question and exit.
if (
    sessionAttributes.current_celeb !== null
){
if session_attributes["current_celeb"] != None:

You don't need to check if that attribute exists. It definitely does because you handled that in the RequestInterceptor and the block for initializing past_celebs.

  1. Delete the following code.
//Check for past celebrities array and create it if not available
if (!sessionAttributes.hasOwnProperty('past_celebs'))
  sessionAttributes.past_celebs = [];

# Check for past celebrities array and create it if not available
if 'past_celebs' not in session_attributes.keys():
    session_attributes["past_celebs"] = [];

You can now understand how useful the interceptors are.

Now, scroll down to the GetBirthdayIntentHandler.

  1. Replace the following code.
// if there's a current_celeb attribute but it's empty, or there isn't one
// error, cue them to say "yes" and end
if ((sessionAttributes.hasOwnProperty('current_celeb') &&
    sessionAttributes.current_celeb === null) ||
  !sessionAttributes.hasOwnProperty('current_celeb')
) {
if (('current_celeb' in session_attributes.keys() and
    session_attributes["current_celeb"] == None) or
    'current_celeb' not in session_attributes.keys()):

With the following code, copying and pasting it into the index.js or lambda_function.py file.

// if the current_celeb is empty, error, cue them to say "yes" and end
if (sessionAttributes.current_celeb === null)
{
if session_attributes["current_celeb"] == None:
  1. Scroll down a little farther, and delete the following code.
//Let's now check if there's a current score. If not, initialize it.
if (!sessionAttributes.hasOwnProperty('score'))
    sessionAttributes.score = 0;
# Let's now check if there's a current score. If not, initialize it.
if 'score' not in session_attributes.keys():
    session_attributes["score"] = 0
  1. In the upper-right corner of the Code tab page, click Save, and then click Deploy to deploy your code.

    Congratulations! You've completed all your updates. You added attribute initialization and the ability to save persistent attributes in your interceptors, so that you could delete redundant code and make your handler functions easier to read. Now you can test code to make sure your changes work properly.

  2. Click the Test tab, and repeat the steps to test your skill that you learned in Module 4.

    Run through two or three celebrities. You can even use the data from the birthdays.json file in the documents folder (on the Code tab) to make sure you get one or two correct.

What should happen when you test your skill?

  1. The game should start as normal the first time. You can get a question, answer it.
  2. Reload the page. When you say, "Launch cake game," the skill should welcome the user back instead of reverting back to the new-user welcome.
  3. You need to run through 33 questions to use up the whole sample set provided with the demo package. You can do that to make sure no celebrities repeat, but you might not have the time to do so.

Last, check your data in DynamoDB

The following steps walk you through the basics of how the DynamoDB service stores your skill data for you. Because you installed a persistence adapter in your skill earlier in this module, you don't have to understand DynamoDB in detail for this workshop. For details about how DynamoDB works, see Amazon DynamoDB on the AWS website.

  1. Click the Code tab, and from the top icon row, click DynamoDB Database. The DynamoDB home page has two main sections.

  2. On the left, locate your table ID from the column with a list of tables. The service has your table already selected, by default.

  3. On the right, click View Items to be taken to the Items view that displays a list of records.

    DynamoDB Items

    You should only have one record because DynamoDB keys each record to a unique ID for a customer.

  4. On the left side of the Items tab view, in the id column, observe the unique code string. DynamoDB created this code for the customer account's relationship with this skill. If you disable the skill, then enable it again, the service creates a new ID.

  5. To the right of the id column, in the attributes column, view the preview of the record. In this record, the user has answered one question about the actor Pierce Brosnan.

    Sometimes, for test purposes, you need to clear your data and start over.

  6. To clear your data and start over, on the Items tab, select the check box next to your id.

  7. From the Actions menu, select Delete items.

    Delete Item

    The record disappears, but DynamoDB creates a new record the next time you interact with your skill.


Was this page helpful?