Editor’s Note: We recently launched a new training course called Cake Time: Build an Engaging Alexa Skill. The course is a self-paced, skill-building course that offers guidance on how to build a high-quality skill from start to finish. In this 4-part blog series, we’re going to dive deep into the coding modules within the course. You can read part 1 on designing the voice user interface for Cake Time here. Part 2 covers how to use slot delegation to collect slots turn by turn.
Editor’s Note: We have changed the name of the Alexa skill in our beginner tutorial from Cake Walk to Cake Time given the term’s racially insensitive history.
In my previous post on designing the Cake Time skill, I did a deep dive on situational design. Instead of a flow chart, my colleague and I focused on the conversation and used situational design. This helped us focus the experience and enabled us to build a useful, simple, and sticky skill. We determined that we would need a way to remember our customer’s birthday. Shared context and memory are paramount to natural, conversational interactions. The shared context changes over time based on past interactions. Once two people get to know one another, they don’t need to keep reintroducing themselves as if they never met before. When designing your skill, you should make sure to give your skill some memory. Your customers will appreciate that the skill doesn’t ask for the same information over and over. The reduced friction may lead to a better customer experience and customer retention. In part 3 of our Cake Time series, I cover how we added memory to Cake Time to create a more engaging voice experience.
Let’s quickly review our design. We have two situations based on the customer’s birthday: unknown and known. If the birthday is unknown, we must ask for it. If the birthday is known, we use it to calculate the number of days until their next one, unless it’s their birthday. In that case, we wish them a happy birthday.
Alexa skills are stateless and don’t automatically remember information between requests. We have to file the birthday away somewhere. If we don’t, sadly we’ll immediate forget it after we send back a response. The next time our customer uses our skill, the birthday is yet again unknown. To our customer’s chagrin, we’ll have ask for it again. This is less than ideal. We could use session attributes, which are passed back and forth between our skill and the Alexa service via the request and the response. Doing so will allow us to remember their birthday a little longer. Sadly, however, this solution is not perfect. Session attributes are forgotten as soon as the session ends as the skill quits. This means we need something a little more permanent. Two great places where you can store information are Amazon S3 and DynamoDB. If you’re using your own AWS account to host your skill, you’ll most likely want to use DynamoDB, which is a key-value and document database. If you’re using Alexa-hosted skills, which is what I used to build the Cake Time skill, you only have Amazon S3 at your disposal. Wherever you choose to save their birthday, the code to read, write, and delete it stays the same. The only difference is how you set things up.
Let’s take a look at how to set up Amazon S3 and DynamoDB with the Alexa Skills Kit (ASK) Software Development Kit (SDK) for Node.js
Before we are able to save, we need to configure a persistence adapter. A persistence adapter is an object that is configured to the storage service we want to connect to and operate on its data using persistent attributes. We can use the persistence adapter to access our storage service, which wires it into the SDK so we can use the same code to read, write and delete data from whatever service we are connected to. Once we configure our persistence adapter, we will register it with the SDK and our skillBuilder using the withPersistenceAdapter function.
When using Alexa-hosted skills, the service sets up your skill’s AWS Lambda function. It does not include the S3 persistence adapter, so you’ll have to update your package.json file. You’ll need to add “ask-sdk-s3-persistence-adapter”: “^2.0.0” the dependencies. Upon doing so, your package.json file will look like:
package.json
{
"name": "cake-time",
"version": "0.9.0",
"description": "alexa utility for quickly building skills",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Amazon Alexa",
"license": "ISC",
"dependencies": {
"ask-sdk-core": "^2.0.7",
"ask-sdk-model": "^1.4.1",
"aws-sdk": "^2.326.0",
"ask-sdk-s3-persistence-adapter": "^2.0.0"
}
}
Once you’ve updated the package.json, you’ll need to click on the deploy button to have the Alexa service install the ask-sdk-s3-persistence-adapter. To use the adapter in your code, you’ll need to load the module into your index.js file. To do so you, use the require keyword:
const persistenceAdapter = require('ask-sdk-s3-persistence-adapter');
You can put this anywhere in your file as long as it appears before you use it. It’s a standard practice to load your modules at the top of your files, so I recommend that you put it there.
Next, we need to use the module to create an S3 persistence adapter. The constructor takes in an object. bucketName is the name of the bucket that we want our s3PersistenceAdapter to connect to. Since we used Alexa-hosted, a bucket was created for us. We can access the name of that bucket with the S3_PERSISTENCE_BUCKET environment variable. You can access environment variables from process.env. To access the name of our bucket you would use, process.env.S3_PERSISTENCE_BUCKET. Below we are using the S3 persistence adapter and passing it the bucket name we want to connect to.
const s3PersistenceAdapter = new persistenceAdapter.S3PersistenceAdapter({
bucketName: process.env.S3_PERSISTENCE_BUCKET
});
Lastly, we register our adapter using withPersistenceAdapter:
.withPersistenceAdapter(
s3PersistenceAdapter
)
Let’s zoom out and take a look at our SkillBuilder:
index.js
// top of the file
const persistenceAdapter = require('ask-sdk-s3-persistence-adapter');
// ...
// continued at the bottom of the file
const s3PersistenceAdapter = new persistenceAdapter.S3PersistenceAdapter({
bucketName:process.env.S3_PERSISTENCE_BUCKET
});
exports.handler = Alexa.SkillBuilders.custom()
.withPersistenceAdapter(s3PersistenceAdapter)
.addRequestHandlers(
HasBirthdayLaunchRequestHandler,
LaunchRequestHandler,
CaptureBirthdayIntentHandler,
HelpIntentHandler,
CancelAndStopIntentHandler,
SessionEndedRequestHandler,
IntentReflectorHandler
) // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
.addErrorHandlers(ErrorHandler)
.lambda();
Now that we understand how to set up the S3 persistence adapter with Alexa-hosted skills, let’s take a look at how we’d set up the DynamoDB adapter.
The configuration process for DynamoDB is similar and takes 4 steps.
1. Update the package.json with a new module dependency
"ask-sdk-s3-persistence-adapter": "^2.0.0"
2. Import the module into your index.js file
const ddbAdapter = require('ask-sdk-dynamodb-persistence-adapter');
3. Create a DynamoDB persistence adapter
const ddbTableName = 'cake-time';
const ddbPersistenceAdapter = new dbdAdapter.DynamoDbPersistenceAdapter({
tableName: tableName,
createTable: true,
});
4. Register it with your SkillBuilder
.withPersistenceAdapter(ddbPersistenceAdapter)
Let’s zoom in to see what the package.json and index.js file will look like:
package.json
{
"name": "cake-time",
"version": "0.9.0",
"description": "alexa utility for quickly building skills",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Amazon Alexa",
"license": "ISC",
"dependencies": {
"ask-sdk-core": "^2.0.7",
"ask-sdk-model": "^1.4.1",
"aws-sdk": "^2.326.0",
"ask-sdk-dynamodb-persistence-adapter": "^2.0.0"
}
}
index.js
// top of the file
const ddbAdapter = require('ask-sdk-dynamodb-persistence-adapter');
const ddbTableName = 'cake-time';
// ...
// continued at the bottom of the file
const ddbPersistenceAdapter = new ddbAdapter.DynamoDbPersistenceAdapter({
tableName: tableName,
createTable: true,
});
exports.handler = Alexa.SkillBuilders.custom()
.withPersistenceAdapter(ddbPersistenceAdapter)
.addRequestHandlers(
HasBirthdayLaunchRequestHandler,
LaunchRequestHandler,
CaptureBirthdayIntentHandler,
HelpIntentHandler,
CancelAndStopIntentHandler,
SessionEndedRequestHandler,
IntentReflectorHandler
) // make sure IntentReflectorHandler is last so it doesn't override your custom intent handlers
.addErrorHandlers(ErrorHandler)
.lambda();
Now that we understand how to configure the S3 and DynamoDB persistence adapters, we can move onto using Persistent Attributes to read and write the birthday.
Let’s start with writing data. We’ll use the AttributesManager to interact with our persistent attributes.
const attributesManager = handlerInput.attributesManager;
We’ll then use the setPersistentAttributes to set the attributes that we want to save. In this case, we’ll be saving the month, day, and year slot values.
Let’s create the object to store our attributes. We’ll call it birthdayAttributes. We’ll map our slot values that we collected into a dictionary:
const birthdayAttributes = {
"year": year,
"month": month,
"day": day
};
If the customer said their birthday was “November seventh nineteen eighty three,” then the data would look like:
{
"year": "1983",
"month": "11",
"day": "7"
}
To save the data to our storage service we pass birthdayAttributes to setPersistentAttributes and then call savePersistentAttributes.
attributesManager.setPersistentAttributes(birthdayAttributes);
attributesManager.savePersistentAttributes();
Now that we understand how to save the birthday, let’s take a look at how we recall their birthday.
Reading information is even easier. We’ll use the AttributesManager, but this time we only need to call one function: getPersistentAttributes. The function is asynchronous, so you’ll want to use the async/await keywords.
async handle(handlerInput) {
const attributesManager = handlerInput.attributesManager;
const persistentAttributes = await attributesManager.getPersistentAttributes() || {};
console.log("year", persistentAttributes.year);
console.log("month", persistentAttributes.month);
console.log("day", persistentAttributes.day);
//...
}
If it’s the first time the customer has opened the skill, there won’t be a record of it in the storage service. This will cause our code to throw an error if we try to access an attribute of a null value so we set persistentAttributes to an “OR” expression:
const persistentAttributes = await attributesManager.getPersistentAttributes() || {};
This means that persistentAttributes will never be null. It will either be a birthday record or and empty object. The empty object will indicate that the birthday is unknown and our skill won’t crash if we call persistentAttributes.year. Instead it will return undefined.
Now that we’re able to read and write the birthday to our storage service, our customers will be thankful that we don’t have to ask for it every time they start the skill.
To this point, we’ve been very focused on the design of our voice user interface, but now we need take a moment and think about our back end. Our skill’s behavior changes based upon the situation. The SDK uses handlers to service the requests that come to our skill. When someone launches the skill we get a LaunchRequest. If the birthday is unknown, we need to ask for it. If the birthday is known we have to calculate the number of days until their next birthday. This means we’re going to need two handlers that can handle a launch request: one for unknown, one for known.
We already have a LaunchRequest handler, so we’ll need to create one more that handles the case when the birthday is known. We’ll call it HasBirthdayLaunchRequestHandler. Our handler’s canHandle function needs to read the birthday from our storage service and return true if it exists. The handle function needs to read from our storage service and return true if it exists. Both functions need access to the same data. While we can have both functions read the data, there’s a slight delay in doing so which will affect performance. It can also potentially cost us a lot of money to read information over and over.
What we need is a way to load the data once per session and make that data available to all of the handlers in our skill. We can use the AttributesManager to accomplish that. So how do we load the data only once? We could try to lazy load it. That would require us to:
This would be great, but then we have duplicate a bunch of code whenever we want to access the birthday. If only there was something that ran before our handlers that could handle fetching the birthday only once per session. Turns out there is! We can use a request interceptor.
There are two types of interceptors.
Like handlers you can define more than one. Let’s take a look at how we would define an interceptor to read our birthday from our storage service once per session.
First we need to create an interceptor and then register it. There are two registration functions: One to register request interceptors and another to register response interceptors. Interceptors must define a function called process. The code in this function will be executed.
const someInterceptor = {
process(handlerInput) {
// code
...
}
}
To register our request interceptor, we need to pass it to addRequestInterceptors. Since our interceptor loads our customer’s birthday we named it, LoadBirthdayInterceptor.
If we have a birthday record in the session store, we’ll get it and use the AttributesManager to store them into the session attributes.
const sessionAttributes = await attributesManager.getPersistentAttributes() || {};
We want to make sure that we have properly received the month, day, and year values from our session store. We use the ternary operator, ‘?’ to define an in-line if-else statement. For each attribute, we check to see if we have a non-null value if not we default to 0.
const year = sessionAttributes.hasOwnProperty('year') ? sessionAttributes.year : 0;
const month = sessionAttributes.hasOwnProperty('month') ? sessionAttributes.month : 0;
const day = sessionAttributes.hasOwnProperty('day') ? sessionAttributes.day : 0;
We only want to set the sessionAttributes if we have all non-zero values for month, day, and year.
if (year && month && day) {
attributesManager.setSessionAttributes(sessionAttributes);
}
Zooming out our handler looks like:
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);
}
}
}
To register the handler we would use addRequestInterceptors. Let’s take a look at our SkillBuilders:
exports.handler = Alexa.SkillBuilders.custom()
.withPersistenceAdapter(ddbPersistenceAdapter)
.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();
At this point, the Cake Time skill can remember and recall our customer’s birthday. This goes a long way to making the experience feel more natural and personal. We’ve also fulfilled a major portion of our design. The last step is to check if it’s their birthday. If so, we need to wish them a happy birthday. If not, we need to count down the days.
When you design your own skills and you determine that there’s some data that your skill needs to be able to recall, use persistent attributes to save it. Your skill will no longer need to repeatedly ask your customer for the same information over and over. Let’s keep the conversation going. If you have any questions or comments about memory and Cake Time reach out to me on Twitter @SleepyDeveloper.
Stay tuned for the next post in this series to learn how to use the Alexa Settings API to look up the customer’s time zone. We’ll need it to accurately compute the number of days until their next birthday.