fbpx

Blogs from the Ranch

< Back to Our Blog

Developing Alexa Skills Locally with Node.js: Implementing Persistence in an Alexa Skill

Avatar

Josh Skeen

Editor’s note: This is the fourth post in our series on developing Alexa skills.

By now, we’ve made a lot of progress in building our Airport Info skill. We tested the model and verified that the skill service behaves as expected. Then we tested the skill in the simulator and on an Alexa-enabled device. In this post, we’ll implement persistence in a new skill so that users will be able to access information saved from their previous interactions.

We’ll go over how to write Alexa skill data to storage, which is useful in cases where the skill would time out or when the interaction cycle is complete. You can see this at work in skills like the 7-Minute Workout skill, which allows users to keep track of and resume existing workouts, or when users want to resume a previous game in The Wayne Investigation.

Let’s Bake a Cake with CakeBaker

For this experiment, we’ll build upon an existing codebase and improve it. The skill, a cooking assistant called CakeBaker, guides users in cooking a cake, step by step. A user interacts with CakeBaker by asking Alexa to start a cake, then advances through the steps of the recipe by saying “next” after each response, like so:

CakeBaker steps

This continues until the user reaches the last step. But what if the skill closes before the user is able to finish a step? By default, Alexa skills close if a user doesn’t respond within 16 seconds. Right now, that means that a user would be forced to start over at the first step, losing the progress made.

Let’s fix that by implementing two new methods in our skill, called saveCakeIntent and loadCakeIntent, which will allow users to save and load their current progress to and from a database. We’ll also test the database functionality in our local environment using the alexa-app-server and alexa-app libraries we discussed in our post on implementing an intent in an Alexa Skill.

This experiment will use Node.js and alexa-app-server to develop and test the skill locally, so we will need to set up those dependencies first. If you haven’t yet done so, read our posts on setting up a local environment and implementing an intent—they will guide you in setting up a local development environment for this skill, which will involve more advanced requirements.

Let’s get started by downloading the source code for CakeBaker. We’ll be improving this source code so that it supports saving and loading cakes to the database.

To complete the experiment, we’ll need a working installation of alexa-app-server and Node.js. If you haven’t done so, install Node.js and then install alexa-app-server, using the instructions outlined in the linked posts.

Clone CakeBaker into the alexa-app-server/examples/apps directory by opening a new terminal window and entering the following within the alexa-app-server/examples/apps directory:

git clone https://github.com/bignerdranch/alexa-cakebaker

Change directories into alexa-app-server/examples/apps/alexa-cakebaker and run the following command:

npm install

This will fetch the dependencies the project requires in order to work correctly.

DynamoDB

The database we will use to store the state of the cake is Amazon’s DynamoDB, a NoSQL-style database that will ultimately live in the cloud on Amazon’s servers. To facilitate testing, we’ll install a local instance of DynamoDB. We will use the brew package manager to add DynamoDB to our local development environment.

Install Homebrew if you haven’t already done so:

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Once this command completes, install a local version of DynamoDB via Homebrew:

brew install dynamodb-local

On Windows? Follow these steps.

When the brew command completes, open a new tab in your terminal and run the following command:

dynamodb-local -sharedDb -port 4000

You should see something similar to the following:

Initializing DynamoDB Local with the following configuration:
Port:   4000
InMemory:   false
DbPath: null
SharedDb:   true
shouldDelayTransientStatuses:   false
CorsParams: *

Now we can begin developing our database functionality and test db behaviour in our local environment. Leave the tab open while you work.

Adding the new Intents

At this point, the CakeBaker skill is cloned locally and our test database instance is set up, so we’re ready to begin adding the save and load features. In order to implement them, we need two new intents for these actions: saveCakeIntent and loadCakeIntent. Let’s begin by adding the intent definitions to the bottom of the index.js file.

One for saving the cake:

skillService.intent('saveCakeIntent', {
    'utterances': ['{save} {|a|the|my} cake']
  },
  function(request, response) {
  //code goes here!
  }
);

And one for loading the cake:

skillService.intent('loadCakeIntent', {
    'utterances': ['{load|resume} {|a|the} {|last} cake']
  },
  function(request, response) {
  //code goes here!
  }
);

Implementing the Save Command

Here’s a diagram of how the save command will work:

CakeBaker save steps

In the diagram, the user’s utterance is resolved to the saveCakeIntent and then processed by the skill service. The skill service saves the cake data to the database, and once this operation completes the service, it responds to the skill interface, indicating that the write to the database succeeded.

The CakeBaker source code we checked out contains a helper called database_helper.js. Open this file, and you should see the following:

'use strict';
module.change_code = 1;
var _ = require('lodash');
var CAKEBAKER_DATA_TABLE_NAME = 'cakeBakerData';
var dynasty = require('dynasty')({});

function CakeBakerHelper() {}
var cakeBakerTable = function() {
  return dynasty.table(CAKEBAKER_DATA_TABLE_NAME);
};

CakeBakerHelper.prototype.createCakeBakerTable = function() {
  return dynasty.describe(CAKEBAKER_DATA_TABLE_NAME)
    .catch(function(error) {
      return dynasty.create(CAKEBAKER_DATA_TABLE_NAME, {
        key_schema: {
          hash: ['userId',
            'string'
          ]
        }
      });
    });
};

CakeBakerHelper.prototype.storeCakeBakerData = function(userId, cakeBakerData) {
  return cakeBakerTable().insert({
    userId: userId,
    data: cakeBakerData
  }).catch(function(error) {
    console.log(error);
  });
};

CakeBakerHelper.prototype.readCakeBakerData = function(userId) {
  return cakeBakerTable().find(userId)
    .then(function(result) {
      return result;
    })
    .catch(function(error) {
      console.log(error);
    });
};

module.exports = CakeBakerHelper;

This file contains the basic logic for creating a new table in DynamoDB we will name cakeBakerData to write cake data to. It also contains methods for reading and writing the cake data to the DynamoDB instance.

Our first task, saving a cake, will be aided by the storeCakeBakerData method the helper contains. Notice storeCakeBaker’s signature: it expects a userId and cakeBakerData. The userId is a unique identifier provided by the Alexa service upon a user enabling a skill. We will pull the userId from the request received by our service from the skill interface, and it will uniquely identify the Alexa account that the Skill is attached to so that the skill can keep track of data for different users. It is also the key we will use to look up a user’s cakeBakerData on the database.

The helper also makes use of Dynasty, an open-source library for interacting with the DynamoDB instance. Because we are developing locally, the first code change we will make is to the connection settings for the Dynasty object.

For testing locally, we will use our local machine’s DynamoDB instance. In order to do that we will need to edit the database_helper.js file and comment the line:

//var dynasty = require('dynasty')({});

and add:

//var dynasty = require('dynasty')({});
var localUrl = 'http://localhost:4000';
var localCredentials = {
  region: 'us-east-1',
  accessKeyId: 'fake',
  secretAccessKey: 'fake'
};
var localDynasty = require('dynasty')(localCredentials, localUrl);
var dynasty = localDynasty;

This will enable us to test against the local DynamoDB instance we started in the terminal using port 4000.

Creating the Cake Table

Before we can save or read cake data from DynamoDB, we’ll first need to ask DynamoDB to create a table to store it in. We can use a helpful feature of alexa-app called a “pre” hook, which will execute before the intent handlers in the skill are executed.

Open the index.js file in the alexa-cakebaker folder and add the following at line 9, right below var databaseHelper = new DatabaseHelper();:

skillService.pre = function(request, response, type) {
  databaseHelper.createCakeBakerTable();
};

This will execute before any intent is handled. If the table doesn’t exist, and if it’s already created, Dynasty will simply return an error, which we handle in the DatabaseHelper class.

Saving the Cake

Let’s implement a saveCake function, at the bottom of the index.js file before the module.exports = CakeBakerHelper; :

var saveCake = function(cakeBakerHelper, request) {
  var userId = request.userId;
  databaseHelper.storeCakeBakerData(userId, JSON.stringify(cakeBakerHelper))
    .then(function(result) {
      return result;
    }).catch(function(error) {
      console.log(error);
    });
};

The method pulls the userId from the request, passing it and a stringified version of the cake data to be written to the database.

Now we’ll put the saveCake method to use. Update the saveCakeIntent intent handler we defined earlier in the index.js file:

skillService.intent('saveCakeIntent', {
    'utterances': ['{save} {|a|the|my} cake']
  },
  function(request, response) {
    var cakeBakerHelper = getCakeBakerHelperFromRequest(request);
    saveCake(cakeBakerHelper, request);
    response.say('Your cake progress has been saved!');
    response.shouldEndSession(true).send();
    return false;
  }
);

Perfect! This should write the cake’s progress to the database when a user explicitly requests it from the skill.

We will also need to update the advanceStepIntent to make use of the saveCake method as well. When a user requests “next,” the cake should be saved implicitly to avoid any lost progress due to a timeout or the skill’s request cycle ending.

Update the advanceStepIntent to call saveCake, just after the cakeBakerHelper is incremented:

skillService.intent('advanceStepIntent', {
    'utterances': ['{next|advance|continue}']
  },
  function(request, response) {
    var cakeBakerHelper = getCakeBakerHelperFromRequest(request);
    cakeBakerHelper.currentStep++;
    saveCake(cakeBakerHelper, request);
    cakeBakerIntentFunction(cakeBakerHelper, request, response);
  }
);

Loading the Cake

A user should be able to load the cake after the skill has exited. Once the cake is loaded, the skill should pick back up at the step that the user left from, eliminating the pain of starting over from the beginning.

In order to enable this, we will have the skill read the cake from the database after looking it up with our userId and set up the CakeBakerHelper object from the persisted state. Then we’ll call cakeBakerIntentFunction to generate the response that should be sent to Alexa. Edit the index.js file and replace the loadCakeIntent Intent with the following:

skillService.intent('loadCakeIntent', {
    'utterances': ['{load|resume} {|a|the} {|last} cake']
  },
  function(request, response) {
    var userId = request.userId;
    databaseHelper.readCakeBakerData(userId).then(function(result) {
      return (result === undefined ? {} : JSON.parse(result['data']));
    }).then(function(loadedCakeBakerData) {
      var cakeBakerHelper = new CakeBakerHelper(loadedCakeBakerData);
      return cakeBakerIntentFunction(cakeBakerHelper, request, response);
    });
    return false;
  }
);

Testing that it Works

Now we can test that the new functionality works against the local database. First, let’s start the alexa-app-server. Change to the alexa-app-server/examples directory and run the local development server:

node server

Now, visit the test page at http://localhost:8080/alexa/cakebaker. We want to mimic a cake that has advanced several steps, so we’ll send several requests on the server. Configure the type to IntentRequest, and the Intent to cakeBakeIntent and hit “Send Request”. This should start a new cake.

CakeBaker intent request

Next, change the Intent to advanceStepIntent and hit “Send Request”—this mimics a user saying “next” in order to move the recipe along to the next step. Hit “Send Request” three more times. In the response area of the test page, you should see:

"response": {
    "shouldEndSession": false,
    "outputSpeech": {
      "type": "SSML",
      "ssml": "<speak>Beat 2 sticks butter and 1 and 1/2 cups sugar in a large bowl with a mixer on medium-high speed until light and fluffy, about 3 minutes. to hear the next step, say next</speak>"
    },
    "reprompt": {
      "outputSpeech": {
        "type": "SSML",
        "ssml": "<speak>I didn’t hear you. to hear the next step, say next</speak>"
      }
    }
  },

Great! Now we can test that saving to the database works. Switch the Intent to saveCakeIntent and click “Send Request”. You should see the following in the response area:

{
  "version": "1.0",
  "sessionAttributes": {
    "cake_baker": {
      "started": false,
      "currentStep": 4,
      "steps": [
        //removed for brevity
              ]
    }
  },
 "response": {
    "shouldEndSession": true,
    "outputSpeech": {
      "type": "SSML",
      "ssml": "<speak>Your cake progress has been saved!</speak>"
    }
  }

Our cake has now been saved to the database! To verify whether the skill service is working, reload the test page, then set the Intent to loadCakeIntent. Click “Send Request”. This mimics a user saying “Alexa, ask Cake Baker to load the cake.”

The response should pick up where the user left off with the fourth step in the cake recipe.

{
  "version": "1.0",
  "sessionAttributes": {
    "cake_baker": {
      "started": false,
      "currentStep": 4,
      "steps": [
        //removed for brevity
              ]
    }
  },
  "response": {
    "shouldEndSession": false,
    "outputSpeech": {
      "type": "SSML",
      "ssml": "<speak>Beat 2 sticks butter and 1 and 1/2 cups sugar in a large bowl with a mixer on medium-high speed until light and fluffy, about 3 minutes. to hear the next step, say next</speak>"
    },
    "reprompt": {
      "outputSpeech": {
        "type": "SSML",
        "ssml": "<speak>I didn’t hear you. to hear the next step, say next</speak>"
      }
    }
  },
  "dummy": "text"
}

Going Live

Now that we’ve tested the skill locally, let’s deploy it live! Fortunately, because DynamoDB is already wired to work easily with an AWS Lambda skill, we will have to do very little to deploy.

First, let’s change database_helper.js to production mode. Open database_helper.js. Uncomment:

var dynasty = require('dynasty')({});

Then comment the local development configuration we added. The top of our database_helper.js file should look like this:

'use strict';
module.change_code = 1;
var _ = require('lodash');
var CAKEBAKER_DATA_TABLE_NAME = 'cakeBakerData';
var dynasty = require('dynasty')({});
// var localUrl = 'http://localhost:4000';
// var localCredentials = {
//   region: 'us-east-1',
//   accessKeyId: 'fake',
//   secretAccessKey: 'fake'
// };
...

Now, before we run through the usual Alexa Skill deployment process, we need to configure DynamoDB on AWS. Visit https://console.aws.amazon.com/dynamodb/home and create a new table. For the table name, enter “cakeBakerData”, and for primary key enter “userId”. Finally, click “Create”.

Configuring DynamoDB on AWS

Next, we will follow the usual Alexa Skill deployment process—but with two big differences. First, we will run through setting up the skill service on AWS Lambda. Visit the Lambda dashboard and click “Create Lambda Function”. Click “skip” on the resulting page.

Zip the files within the cakebaker directory and click the “Upload a ZIP file” option in the Lambda configuration, keeping in mind that your index.js file should be at the parent level of your archive. Click “Upload” and select the archive you created.

  • In the “Name” field, enter “cakeBaker”.
  • In the “Runtime” field, be sure to select the Node.js option.

It’s important to note that the Lambda function handler and role selection are different here than they are in an AWS Lambda skill without a database. Rather than “Basic Execution Role”, select “Basic with DynamoDB”. This will redirect to a new screen, where you should click “Allow”. This step allows our AWS Lambda-backed skill service to use a DynamoDB datastore on our AWS Account.

Here is what your configuration should now look like:

CakeBaker configuration

Click “Next” and then “Create Function”.

Note the long “ARN” at the top right of the page. This is the Amazon Resource Name, and it will look something like arn:aws:lambda:us-east-1:333333289684:function:myFunction. You will need it when setting up the skill interface, so be sure to copy it from your AWS Lambda function.

CakeBaker Amazon Resource Name

Finally, click on the “Event sources” tab and click “Add event source”. Select “Alexa Skills Kit” in the Event Source Type dropdown and hit “Submit”.

CakeBaker Event sources tab

Setting up the Skill Interface

Next, we’ll set up the skill interface. Visit the Amazon Developer Console skills panel and click “Add a New Skill”. In the Skill Information tab, enter “Cake Baker” for the “Name” and “Invocation Name” fields. Leave “Custom Interaction Model” selected for the Skill Type.

CakeBaker skill interface

Click “Next”.

Now we need to set up the interaction model. Copy the intent schema and utterances from the alexa-app-server test page into the respective fields.

For the “Intent Schema” field, use:

{
  "intents": [
    {
      "intent": "advanceStepIntent",
      "slots": []
    },
    {
      "intent": "repeatStepIntent",
      "slots": []
    },
    {
      "intent": "cakeBakeIntent",
      "slots": []
    },
    {
      "intent": "loadCakeIntent",
      "slots": []
    },
    {
      "intent": "saveCakeIntent",
      "slots": []
    }
  ]
}

and for the “Sample Utterances” field, use:

advanceStepIntent   next
advanceStepIntent   advance
advanceStepIntent   continue
cakeBakeIntent  new cake
cakeBakeIntent  start cake
cakeBakeIntent  create cake
cakeBakeIntent  begin cake
cakeBakeIntent  build cake
cakeBakeIntent  new a cake
cakeBakeIntent  start a cake
cakeBakeIntent  create a cake
cakeBakeIntent  begin a cake
cakeBakeIntent  build a cake
cakeBakeIntent  new the cake
cakeBakeIntent  start the cake
cakeBakeIntent  create the cake
cakeBakeIntent  begin the cake
cakeBakeIntent  build the cake
loadCakeIntent  load cake
loadCakeIntent  resume cake
loadCakeIntent  load a cake
loadCakeIntent  resume a cake
loadCakeIntent  load the cake
loadCakeIntent  resume the cake
loadCakeIntent  load last cake
loadCakeIntent  resume last cake
loadCakeIntent  load a last cake
loadCakeIntent  resume a last cake
loadCakeIntent  load the last cake
loadCakeIntent  resume the last cake
saveCakeIntent  save cake
saveCakeIntent  save a cake
saveCakeIntent  save the cake
saveCakeIntent  save my cake

CakeBaker intent schema utterances

Click “Next”.

On the Configuration page, select “Lambda ARN (Amazon Resource Name)” and enter the ARN you copied when you set up the Lambda endpoint. Click “Next”. You can now test that the skill behaves as it did in local development. If you have an Alexa-enabled device registered to your developer account, you can now test the save and load functionality with the device. Amazon has more information on registering an Alexa-enabled device for testing, if you’re not familiar with the process.

Try the following commands, either in the test page or against a real device: “Alexa, ask Cake Baker to bake a cake”, “next”, “next”, and “Save Cake”.

Wait for a moment while the skill times out, and then say, “Alexa, ask Cake Baker to load the cake”. The skill should pick up where we left off, on the third step of Cake Baker.

Congratulations; you’ve implemented basic persistence in an Alexa Skill! In the next post, we’ll cover submitting your custom Alexa skills for certification so that they can be used by anybody with an Alexa-enabled device.

Avatar

Josh Skeen

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project