Help Your Skill Remember with Attributes Manager

Step 1: Use session attributes

In Step 1, you'll learn a few ways that session attributes help you manage memory throughout your skill.

Step 1 has two sub-steps:

  • First, remember which question was asked
  • Then, check the answer and keep score.

First, remember which question was asked

A question takes two exchanges, the "yes" to get the question and the "{month} {year}" to answer it. However, if your skill doesn't know what question it asked in the first exchange, it can't check the answer in the second exchange.

  1. On the Code tab, in the index.js or lambda_function.py file, find the PlayGameHandler code.

  2. In the handler's handle() function, replace this code.

//Import the celebrity functions and get a random celebrity.
const cfunctions = require('./celebrityFunctions.js');
const celeb = cfunctions.getRandomCeleb();
var title = celeb.name;
//Ask the question
const speakOutput = `In what month and year was ${celeb.name} born?`;
from celebrityFunctions import get_random_celeb
celeb = get_random_celeb()
title = celeb["name"]
speak_output =  f'In what month and year was {celeb["name"]} born?'

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

// get the current session attributes, creating an object you can read/update
const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();

var speakOutput = '';

//check if there's a current celebrity. If so, repeat the question and exit.
if (
    sessionAttributes.hasOwnProperty('current_celeb') &&
    sessionAttributes.current_celeb !== null
) {
    speakOutput = `In what month and year was ${sessionAttributes.current_celeb.name} born?`;
    return handlerInput.responseBuilder
        .speak(speakOutput)
        .reprompt(speakOutput)
        .getResponse();
}

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

//Import the celebrity functions and get a random celebrity.
const cfunctions = require('./celebrityFunctions.js');
const celeb = cfunctions.getRandomCeleb(sessionAttributes.past_celebs);
var title = celeb.name;
var subtitle = 'What month and year were they born?';

// Check to see if there are any celebrities left.
if (celeb.id === 0) {
    speakOutput = `You have run out of celebrities. Thanks for playing!`;
    title = 'Game Over';
    subtitle = '';
} else {
    //set the "current_celeb" attribute
    sessionAttributes.current_celeb = celeb;

    //save the session attributes
    handlerInput.attributesManager.setSessionAttributes(sessionAttributes);

    //Ask the question
    speakOutput = `In what month and year was ${celeb.name} born?`;
}
# get the current session attributes, creating an object you can read/update
session_attributes = handler_input.attributes_manager.session_attributes

speak_output = ''

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

    speak_output = f'In what month and year was {session_attributes["current_celeb"]["name"]} born?'
    return (
        handler_input.response_builder
            .speak(speak_output)
            .ask(speak_output)
            .response
    )

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

# Import the celebrity functions and get a random celebrity.
from celebrityFunctions import get_random_celeb
celeb = get_random_celeb(session_attributes["past_celebs"])
title = celeb["name"]
subtitle = 'What month and year were they born?'

# Check to see if there are any celebrities left.
if celeb["id"] == 0:
    speak_output = 'You have run out of celebrities. Thanks for playing!'
    title = 'Game Over'
    subtitle = ''
else:
    # set the "current_celeb" attribute
    session_attributes["current_celeb"] = celeb

    # save the session attributes
    handler_input.attributes_manager.session_attributes = session_attributes

    # Ask the question
    speak_output = f'In what month and year was {celeb["name"]} born?'

To understand what you just added, you need to examine the code elements and what their capabilities are.

First, meet the Attributes Manager, also known as handlerInput.attributesManager (Node.js) or handler_input.attributes_manager (Python). It has methods to get and set all the different types of attributes. On the second line, you'll use it to get an object with all the session attributes.

Next, you'll check if there's a property currently in the session attributes object called current_celeb, and if so, does it have a real value? This is because when a user asks a question, you'll set that value to the celebrity's name. When the user answers this question, you'll clear the value so that you know it's ready to receive a new question.

Why do you need to do this? Like in a normal conversation, you have to be prepared for the other person to go off-script. If the user says "Yes" again, for whatever reason, you don't want them to have another celebrity chosen.

If the user has a current celebrity, everything else is short-circuited, and the skill asks the question again.

Next, you determine whether there's a session attribute named past_celebs. This is an array of previous celebrities used in the game. When the array isn't there, it's created. Then, the array is passed as an argument to get a random celebrity, which will filter out celebrities listed in the array before picking a celebrity.

If there are no more celebrities to choose from, the getRandomCeleb (Node.js) or get_random_celeb (Python) function returns the id "0". The code handles that with a "Game Over" message.

Also, the subtitle for our simple APL document gets turned into a variable, so the skill doesn't ask when "Game Over" was born. You'll be reminded to change that in a moment.

If you haven't run out of celebrities, before you ask the question, you set the current_celeb property of the session attributes object to the celebrity's name. Then, you update the session attributes in the attributes manager. This action makes sure that the celebrity's name and any other session values are set to memory.

Finally, you ask the same question, except, down in the APL data.

  1. In the APL section of the index.js or lambda_function.py file, replace this code.
Subtitle: 'What month and year were they born?',
"Subtitle": 'What month and year were they born?',

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

Subtitle: subtitle,
"Subtitle": subtitle,

When you complete this sub-step, you not only saved the current celebrity to session memory so the GetBirthdayIntentHandler can check the answer. You also added functionality to prevent repeats and cheating, and to check whether you ran out of celebrities.

Then, check the answer and keep score

This update to PlayGameHandler function will add the following things:

  • Get the session variables
  • Check for a score and initialize it if there isn't one.
  • Check the answer and handle right, wrong, and error conditions.

"How can there be error conditions?" you ask. "Doesn't Alexa deliver a month and year as values?" Yes and no.

Alexa accepts and delivers "Novemberish" for a month. Alexa also doesn't force a four-digit number. That slot is more general-purpose for multi-digit numbers.

This brings up the question, "How far should you go to accommodate natural speaking styles?" If someone says, "Novemberish seventy-three," do you request a correct month name and full year or do your own translation to "November 1973?" To keep this from getting overwhelmingly complex (it is a beginner workshop after all), the workshop chose to apply the first option.

You should also understand that conversation is unstructured. What does this mean? Generally, Alexa doesn't require people to say specific things in a specific order. If a user blurts out "Alexa, December 1940," before Alexa names a celebrity, that utterance will still cause a request to go to the GetBirthdayIntentHandler. You'll start the updated handler by checking to see if there's actually a celebrity associated with this particular game instance.

Because this update replaces most everything, you'll replace the whole handle() part of the handler with the next four code blocks. However, the workshop breaks the blocks up to make it easier for you to understand what's going on.

  1. Find the entire handle() part of the GetBirthdayIntentHandler code in the index.js or lambda_function.py file, and replace it with the following code, by copying and pasting it. We will add back in the visual response and response builder in a moment.
handle(handlerInput) {
    var speakOutput = '';

    // get the current session attributes, creating an object you can read/update
    const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();

    // if there's a current_celeb attribute but it's null, 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')
    ) {
        speakOutput =
            "I'm sorry, there's no active question right now. Would you like a question?";
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }

    // Replace me in the next step
}
def handle(self, handler_input):
    # type: (HandlerInput) -> Response
    speak_output = ''

    # get the current session attributes, creating an object you can read/update
    session_attributes = handler_input.attributes_manager.session_attributes

    # if there's a current_celeb attribute but it's None, or there isn't one
    # error, cue them to say "yes" and end

    if (('current_celeb' in session_attributes.keys() and
        session_attributes["current_celeb"] == None) or
        'current_celeb' not in session_attributes.keys()):

        speak_output = "I'm sorry, there's no active question right now. Would you like a question?"

        return (
            handler_input.response_builder
                .speak(speak_output)
                .ask(speak_output)
                .response
        )

    # Replace me in the next step

This code block loads the session attributes from the attributes manager. If there is no current_celeb property or the property's value does not have a value, the code returns an error message, and the game stops right there. However, the error message also asks a question to help the user answer "Yes" and activate a question. Whenever you can, don't just deliver an error message, but try to move the user to say something that will make the situation better.

  1. Replace the comment Replace me in the next step comment with the following code, putting it at the bottom of the handle() method. This code gets and checks the user's answer.
//Get the slot values
var year = handlerInput.requestEnvelope.request.intent.slots.year.value;
var month = handlerInput.requestEnvelope.request.intent.slots.month.value;

//Okay, check the answer
const cfunctions = require('./celebrityFunctions.js');
const winner = cfunctions.checkAnswer(
    sessionAttributes.current_celeb,
    month,
    year
);
# Get the slot values
year = ask_utils.request_util.get_slot(handler_input, "year").value
month = ask_utils.request_util.get_slot(handler_input, "month").value

# Okay, check the answer
from celebrityFunctions import check_answer
winner = check_answer(
    session_attributes["current_celeb"],
    month,
    year
)

With this code, you get the slot values as before, then send them along with the current celebrity from the session values to the check answer function that the workshop provided. If the month name isn't recognized or the year is under 100, the code returns the error condition. As noted previously, the skill asks the user to provide a month name and full year name. Then, the skill sends the user's response and processing is terminated. In addition, in both of the previous cases, rather than send a new APL document about the error, whatever APL document is current remains on screen.

  1. Directly below the code you just pasted, copy and paste the following code still within the handle() method. This code performs actions when the user gives a valid answer.
// Add the celebrity to the list of past celebs.
// Store the value for the rest of the function,
// and set the current celebrity to null
sessionAttributes.past_celebs.push(sessionAttributes.current_celeb);
const cname = sessionAttributes.current_celeb.name;
sessionAttributes.current_celeb = null;

//Let's now check if there's a current score. If not, initialize it.
if (!sessionAttributes.hasOwnProperty('score'))
  sessionAttributes.score = 0;

//We'll need variables for our visual. Let's initialize them.
var title,
    subtitle = '';

//Did they get it?
if (winner) {
    sessionAttributes.score += 1;
    title = 'Congratulations!';
    subtitle = 'Wanna go again?';
    speakOutput = `Yay! You got ${cname}'s birthday right! Your score is now
        ${sessionAttributes.score}. Want to try another?`;
} else {
    title = 'Awww shucks';
    subtitle = 'Another?';
    speakOutput = `Sorry. You didn't get the right month and year for
        ${cname}. Maybe you'll get the next one. Want to try another?`;
}

//store all the updated session data
handlerInput.attributesManager.setSessionAttributes(sessionAttributes);

# Add the celebrity to the list of past celebs.
# Store the value for the rest of the function,
# and set the current celebrity to None
session_attributes["past_celebs"].append(session_attributes["current_celeb"])
cname = session_attributes["current_celeb"]["name"]
session_attributes["current_celeb"] = None

# 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

# We'll need variables for our visual. Let's initialize them.
title = ''
subtitle = ''

# Did they get it?
if winner:
    session_attributes["score"] = session_attributes["score"] + 1
    title = 'Congratulations!'
    subtitle = 'Wanna go again?'
    speak_output = f"Yay! You got {cname}'s birthday right! Your score is now " \
        f"{session_attributes['score']}. Want to try another?"
else:
    title = 'Awww shucks'
    subtitle = 'Another?'
    speak_output = f"Sorry. You didn't get the right month and year for " \
        f"{cname}. Maybe you'll get the next one. Want to try another?"

# store all the updated session data
handler_input.attributes_manager.session_attributes = session_attributes

Because the skill has used the celebrity name, whether the user's answer was right or wrong, you add the name to the past_celebs array, copy the name for use through the rest of the code, and clear the current_celeb value.

Next, you initialize a score attribute if there isn't one. You check whether the answer is right (true) or wrong (false) and process the answer accordingly. Previously the code used hard-coded values for the title and subtitle in the APL code. Now you use different text for the different cases, so these values are replaced with variables that you set here. With all that done, you save the session attributes.

  1. Directly below the code you just pasted, copy and paste the following code. This code responds to the user.
//====================================================================
// Add a visual with Alexa Layouts
//====================================================================

// Check to make sure the device supports APL
if (
    Alexa.getSupportedInterfaces(handlerInput.requestEnvelope)[
        'Alexa.Presentation.APL'
    ]
) {
    // Import an Alexa Presentation Language (APL) template
    var APL_simple = require('./documents/APL_simple.json');

    // add a directive to render the simple template
    handlerInput.responseBuilder.addDirective({
        type: 'Alexa.Presentation.APL.RenderDocument',
        document: APL_simple,
        datasources: {
            myData: {
                //====================================================================
                // Set a headline and subhead to display on the screen if there is one
                //====================================================================
                Title: title,
                Subtitle: subtitle,
            },
        },
    });
}

//====================================================================
// Send the response back to Alexa
//====================================================================
return handlerInput.responseBuilder
    .speak(speakOutput)
    .reprompt(speakOutput)
    .getResponse();

#====================================================================
# Add a visual with Alexa Layouts
#====================================================================

# Import an Alexa Presentation Language (APL) template
with open("./documents/APL_simple.json") as apl_doc:
    apl_simple = json.load(apl_doc)

    if ask_utils.get_supported_interfaces(
            handler_input).alexa_presentation_apl is not None:
        handler_input.response_builder.add_directive(
            RenderDocumentDirective(
                document=apl_simple,
                datasources={
                    "myData": {
                        #====================================================================
                        # Set a headline and subhead to display on the screen if there is one
                        #====================================================================
                        "Title": title,
                        "Subtitle": subtitle,
                    }
                }
            )
        )

return (
    handler_input.response_builder
        .speak(speak_output)
        # .ask(speak_output)
        .response
)

When you are finished, your entire GetBirthdayIntentHandler should look like this.

const GetBirthdayIntentHandler = {
    canHandle(handlerInput) {
        return (
            Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' &&
            Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetBirthdayIntent'
        );
    },

    handle(handlerInput) {
        //====================================================================
        // Set your speech output
        //====================================================================
        var speakOutput = '';

        // get the current session attributes, creating an object you can read/update
        const sessionAttributes = handlerInput.attributesManager.getSessionAttributes();

        // if the current_celeb is null, error, cue them to say "yes" and end
        if (sessionAttributes.current_celeb === null)
        {
            speakOutput =
                "I'm sorry, there's no active question right now. Would you like a question?";
            return handlerInput.responseBuilder
                .speak(speakOutput)
                .reprompt(speakOutput)
                .getResponse();
        }


        //Get the slot values
        var year = handlerInput.requestEnvelope.request.intent.slots.year.value;
        var month = handlerInput.requestEnvelope.request.intent.slots.month.value;

        //Okay, check the answer
        const cfunctions = require('./celebrityFunctions.js');
        const winner = cfunctions.checkAnswer(
            sessionAttributes.current_celeb,
            month,
            year
        );

        // Add the celebrity to the list of past celebs.
        // Store the value for the rest of the function,
        // and set the current celebrity to null
        sessionAttributes.past_celebs.push(sessionAttributes.current_celeb);
        const cname = sessionAttributes.current_celeb.name;
        sessionAttributes.current_celeb = null;

        //We'll need variables for our visual. Let's initialize them.
        var title,
            subtitle = '';

        //Did they get it?
        if (winner) {
            sessionAttributes.score += 1;
            title = 'Congratulations!';
            subtitle = 'Wanna go again?';
            speakOutput = `Yay! You got ${cname}'s birthday right! Your score is now
            ${sessionAttributes.score}. Want to try another?`;
        } else {
            title = 'Awww shucks';
            subtitle = 'Another?';
            speakOutput = `Sorry. You didn't get the right month and year for
            ${cname}. Maybe you'll get the next one. Want to try another?`;
        }

        //store all the updated session data
        handlerInput.attributesManager.setSessionAttributes(sessionAttributes);

        //====================================================================
        // Add a visual with Alexa Layouts
        //====================================================================

        // Check to make sure the device supports APL
        if (
            Alexa.getSupportedInterfaces(handlerInput.requestEnvelope)[
                'Alexa.Presentation.APL'
            ]
        ) {
            // Import an Alexa Presentation Language (APL) template
            var APL_simple = require('./documents/APL_simple.json');

            // add a directive to render the simple template
            handlerInput.responseBuilder.addDirective({
                type: 'Alexa.Presentation.APL.RenderDocument',
                document: APL_simple,
                datasources: {
                    myData: {
                        //====================================================================
                        // Set a headline and subhead to display on the screen if there is one
                        //====================================================================
                        Title: title,
                        Subtitle: subtitle,
                    },
                },
            });
        }

        //====================================================================
        // Send the response back to Alexa
        //====================================================================
        return handlerInput.responseBuilder
            .speak(speakOutput)
            .reprompt(speakOutput)
            .getResponse();
    }
};
class GetBirthdayIntentHandler(AbstractRequestHandler):
    """Handler for Skill Launch."""
    def can_handle(self, handler_input):
        # type: (HandlerInput) -> bool

        return (
            ask_utils.is_request_type("IntentRequest")(handler_input)
                and ask_utils.is_intent_name("GetBirthdayIntent")(handler_input)
        )

    def handle(self, handler_input):
        # type: (HandlerInput) -> Response
        speak_output = ''

        # get the current session attributes, creating an object you can read/update
        session_attributes = handler_input.attributes_manager.session_attributes

        # if there's a current_celeb attribute but it's None, or there isn't one
        # error, cue them to say "yes" and end

        if session_attributes["current_celeb"] == None:

            speak_output = "I'm sorry, there's no active question right now. Would you like a question?"

            return (
                handler_input.response_builder
                    .speak(speak_output)
                    .ask(speak_output)
                    .response
            )

        # Get the slot values
        year = ask_utils.request_util.get_slot(handler_input, "year").value
        month = ask_utils.request_util.get_slot(handler_input, "month").value

        # Okay, check the answer
        from celebrityFunctions import check_answer
        winner = check_answer(
            session_attributes["current_celeb"],
            month,
            year
        )

        # Add the celebrity to the list of past celebs.
        # Store the value for the rest of the function,
        # and set the current celebrity to None
        session_attributes["past_celebs"].append(session_attributes["current_celeb"])
        cname = session_attributes["current_celeb"]["name"]
        session_attributes["current_celeb"] = None

        # We'll need variables for our visual. Let's initialize them.
        title = ''
        subtitle = ''

        # Did they get it?
        if winner:
            session_attributes["score"] = session_attributes["score"] + 1
            title = 'Congratulations!'
            subtitle = 'Wanna go again?'
            speak_output = f"Yay! You got {cname}'s birthday right! Your score is now " \
                f"{session_attributes['score']}. Want to try another?"
        else:
            title = 'Awww shucks'
            subtitle = 'Another?'
            speak_output = f"Sorry. You didn't get the right month and year for " \
                f"{cname}. Maybe you'll get the next one. Want to try another?"

        # store all the updated session data
        handler_input.attributes_manager.session_attributes = session_attributes

        #====================================================================
        # Add a visual with Alexa Layouts
        #====================================================================

        # Import an Alexa Presentation Language (APL) template
        with open("./documents/APL_simple.json") as apl_doc:
            apl_simple = json.load(apl_doc)

            if ask_utils.get_supported_interfaces(
                    handler_input).alexa_presentation_apl is not None:
                handler_input.response_builder.add_directive(
                    RenderDocumentDirective(
                        document=apl_simple,
                        datasources={
                            "myData": {
                                #====================================================================
                                # Set a headline and subhead to display on the screen if there is one
                                #====================================================================
                                "Title": title,
                                "Subtitle": subtitle,
                            }
                        }
                    )
                )

        return (
            handler_input.response_builder
                .speak(speak_output)
                # .ask(speak_output)
                .response
        )

  1. In the upper-right corner of the Code tab page, click Save, and then click Deploy to deploy your code.

At this point, you have a playable game.