Writing an Alexa Skill with CRUD functionality using Dynamo DB, Serverless and AWS Lambdas
The user can simply ask Alexa to add an expense with an amount and category.
Hey there 👋🏽. In this tutorial, we will be going over how to write an Alexa skill with the ability to create an object, read an object, delete an object and update an object by using Dynamo DB, Serverless and AWS Lambdas.
Within this skill, we are going to create a simple expense management voice application. The user can simply ask Alexa to add an expense with an amount and category. From there, the user can update the amount for that expense, check the amount for that expense and also delete that expense. When the user performs these voice commands, it manipulates the Dynamo DB storage located in their AWS Developer console.
Below is a quick demo showing how the Alexa skill would work using the Alexa Skills Kit Developer console:
Before we get cracking, there are a couple of concepts you should understand regarding the Alexa Skills Kit.
What is an Alexa Skills Kit?
The Alexa Skill kit is simply a kit or collection of APIs, tools and documentation which assists you in extending the capabilities of Alexa by writing custom skills. If you would like to understand this better, you can start by taking the beginner course which you can find here.
AWS Lambda function
This is the code uploaded to AWS Lambda. Lambda supports coding in Node.js, Java, Python, or C#. A smart home skill must be implemented as a Lambda function. You can also choose to use a Lambda function for the service of a custom skill.
Intents
A representation of the action that fulfils a customer’s spoken request. Intents can have further arguments called slots that represent variable information. For example, “Alexa, ask History Buff what happened on June third”. In this statement, …what happened on June third maps to a specific intent that can be handled by a particular Alexa ability.
Interaction Model
A defined set of logic parameters for your skill that will determine the voice interface through which customers will interact with your skill as well as map customers’ spoken input to the intents your cloud-based service can handle.
Invocation
The act of beginning an interaction with a particular Alexa ability.
Skill
A set of actions or tasks that are accomplished by Alexa. Skills are like apps for Alexa, helping customers perform everyday tasks or engage with your content naturally with voice.
Slot
An argument to an intent that gives Alexa more information about that request. For example, Alexa, ask History Buff what happened on June third. In this statement, …June third is the value of a date slot that refines the request.
Utterance
The words the user says to Alexa to convey what they want to do, or to provide a response to a question Alexa asks.
Okay, so now that you understand the concepts a bit better, we can start coding the Alexa Skill!
Creating a skill on the Alexa Developer Console
- To get started, head over to https://developer.amazon.com/alexa/console/ask
- You will be prompted to log in, so if you do not have an Alexa/Amazon Developer account, create one and sign in. It’s free!
- Click on Create Skill and name the skill ‘My Expenses’ in the Skill name field. Keep the language as ‘English UK’.
- In the model selection for your skill, select ‘Custom’ and for your method to host your skill’s backend, select ‘Provision your own’.
- Now, you can click Create Skill on the top right to go to the next stage.
- You will be taken to a ‘Choose a template’ page. Select ‘Start from scratch’ and then click on Choose.
- You will then be taken to the skills kit builder where your intents, slots and utterances are located.
- To save time, I will provide the interaction model for this skill which will configure the relevant intents, slots and utterances to get this skill to work. On the side bar on the left, click on JSON Editor. Now insert this JSON code which has the interaction model for this skill:
{
"interactionModel": {
"languageModel": {
"invocationName": "my expenses",
"intents": [
{
"name": "AddExpense",
"slots": [
{
"name": "Amount",
"type": "AMAZON.NUMBER"
},
{
"name": "Category",
"type": "CATEGORIES"
}
],
"samples": [
"I used {Amount} for {Category}",
"I paid {Amount} for {Category}",
"Add {Amount} for {Category}",
"{Amount} for {Category}",
"paid {Amount} for {Category}",
"for {Category} paid {Amount}",
"{Amount} paid for {Category}"
]
},
{
"name": "AMAZON.StartOverIntent",
"samples": []
},
{
"name": "AMAZON.RepeatIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": [
"help",
"help me"
]
},
{
"name": "AMAZON.StopIntent",
"samples": [
"stop"
]
},
{
"name": "AMAZON.CancelIntent",
"samples": [
"cancel"
]
},
{
"name": "AMAZON.YesIntent",
"samples": []
},
{
"name": "AMAZON.NoIntent",
"samples": []
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
},
{
"name": "GetExpense",
"slots": [
{
"name": "Category",
"type": "CATEGORIES"
}
],
"samples": [
"Check the amount put down for {Category}",
"how much did I put down for {Category}",
"Check amount for {Category}"
]
},
{
"name": "DeleteExpense",
"slots": [
{
"name": "Category",
"type": "CATEGORIES"
}
],
"samples": [
"Delete expense for {Category}",
"I would like to delete the expense for {Category}"
]
},
{
"name": "UpdateExpense",
"slots": [
{
"name": "Amount",
"type": "AMAZON.NUMBER"
},
{
"name": "Category",
"type": "CATEGORIES"
}
],
"samples": [
"Additional {Amount} for {Category}",
"I spent an extra {Amount} on {Category}"
]
}
],
"types": [
{
"name": "CATEGORIES",
"values": [
{
"name": {
"value": "Dining out"
}
},
{
"name": {
"value": "Groceries"
}
},
{
"name": {
"value": "Fuel"
}
},
{
"name": {
"value": "Car"
}
},
{
"name": {
"value": "Traveling"
}
},
{
"name": {
"value": "Loan"
}
},
{
"name": {
"value": "Internet"
}
},
{
"name": {
"value": "Telephone"
}
},
{
"name": {
"value": "Credit Card "
}
},
{
"name": {
"value": "Other"
}
}
]
}
]
}
}
}
9. Now click on Build Model. This will now build the relevant models for the Alexa skill and will add the intents, slots and utterances. This may take a couple of minutes.
10. Once the build is finished, click on Save Model.
Configuration and Creating accounts
Now it’s time to get your hands dirty! The backend.
As stated previously, the lambda function is the code uploaded to AWS Lambda. Lambda supports coding in Node.js, Java, Python, or C#. There are multiple ways to deploy your Alexa skill. You can use the ASK CLI or the Alexa Skills Kit Developer console to invoke your Alexa handlers.
However, we configured our Alexa Skill to ‘Provision our own’ so we will be using a framework called Serverless. Serverless is the native architecture of the cloud that enables you to shift more of your operational responsibilities to AWS, increasing your agility and innovation. Serverless allows you to build and run applications and services without thinking about servers.
Creating an AWS account
Before we begin, if you don’t already, you need to create a few accounts. Firstly, you will need to create an AWS account. Bear in mind that this is different to the Alexa Developer account you created before. This is where our Dynamo DB and Lambda function will be stored. To create an AWS account, go to https://aws.amazon.com/console/. Once you create the account, select the ‘Free Plan’.
Creating a Serverless account and installing package
You will also need to create a Serverless account. Go to https://serverless.com/ and click on Sign-up free. Fill in the details and sign up for a free account. Once you sign up, you will be prompted to choose a username and an app name. You can call the app ‘myexpenses’.
Once your sign up is finished, you will need to install the Serverless package on your system.
To do this, follow the steps shown in the image below:
When prompted to log in, use the account you created for Serverless.
After you’ve logged in through the terminal, you can start creating a Serverless application. However, before we do that, we need to get hold of a few secret keys to get our Serverless application to integrate with our AWS account.
Getting AWS access key id/secret access key and storing them in your local AWS credentials file
1. Log in to your AWS account and on the top right corner in your user account, go to ‘My Security Credentials’.
2. We’re going to start by creating an IAM user. Go to ‘Users’ on the left-hand navigation side-bar and click on Add user. Enter a username and select ‘Programmatic Access’
3. Next, you will be taken to Permissions. Select ‘Attach existing policies directly’ and then choose the ‘Administrator Access’ policy. Now click on Next:Tags. You don’t need to fill in anything here. Then click on Next:Review and then Create User.
4. You will now be given an Access Key ID and a Secret Access Key. Make sure you save those details somewhere because that is the last time you will see them in your AWS account. Once you do this, move onto the next stage.
Setting environment variables, installing the ASK CLI and associating an AWS profile
Go to your terminal and type in:
npm install -g ask-cli
Now type in:
ask init
You will be prompted to ‘Create new profile’. Click that and enter a profile name. You will be taken to the Alexa/Amazon developer login and now login with your Alexa/Amazon Developer account. Not your AWS account!
You will then be prompted to go back to your terminal where you will be asked to insert your AWS Access Key ID and Secret Access Key. Use the ones that you generated previously from your AWS account. Once you do this, it will create a file named /.aws/credentials.
If you type in:
cat ~/.aws/credentials
you will see it stores your Access Key ID and your Secret Key. For it to work with your serverless application, you need to modify this file slightly.
Type in:
vim ~/.aws/credentials
on your terminal. Within the file, where it says [ask_cli_default] or the name of your associated AWS profile, just below it, type in [serverless]. Now save the file and exit.
Build better voice apps. Get more articles & interviews from voice technology experts at voicetechpodcast.com
Developing the Serverless Alexa Skill
Now that we’ve sorted out the configuration part, we can start creating the serverless application. On your terminal, create a new folder by typing in:
mkdir myexpenses
Go into that folder and run this command:
serverless create --template aws-nodejs
Now in your favourite IDE (I usually use Visual Studio), open the myexpenses project you just created. In the ‘handler.js’ file, insert this code:
'use strict';const AWS = require('aws-sdk');const Alexa = require("alexa-sdk");const lambda = new AWS.Lambda();const dynamoDb = new AWS.DynamoDB.DocumentClient();const uuid = require('uuid');exports.handler = function(event, context, callback) {const alexa = Alexa.handler(event, context);alexa.appId = "XXXX";alexa.registerHandlers(handlers);alexa.execute();};const handlers = {'LaunchRequest': function() {this.emit('Prompt');},'Unhandled': function() {this.emit('AMAZON.HelpIntent');},'AddExpense': function() {var amount = this.event.request.intent.slots.Amount.value;var category = this.event.request.intent.slots.Category.value;var timestamp = new Date().getTime();var userId = this.event.context.System.user.userId;if ((typeof(amount) != "undefined") || (typeof(category) != "undefined")) {console.log("\n\nLoading handler\n\n");const dynamodbParams = {TableName: process.env.DYNAMODB_TABLE_EXPENSES,Item: {id: uuid.v4(),userId: userId,amount: amount,category: category,createdAt: timestamp,updatedAt: timestamp,},};const params = {TableName: process.env.DYNAMODB_TABLE_EXPENSES,FilterExpression: 'category = :this_category',ExpressionAttributeValues: {':this_category': category}};console.log('Attempting to get expense', params);dynamoDb.scan(params).promise().then(data => {console.log('Got expense: ' + JSON.stringify(data), params);const self = this;const item = data.Items[0];if (!item) {dynamoDb.put(dynamodbParams).promise().then(data => {console.log('Expense added: ', dynamodbParams);this.emit(':ask', 'Added £' + amount + ' for ' + category + '. You can check an expense, delete an expense or update one. You choose.');}).catch(err => {console.error(err);this.emit(':tell', 'Hey, hey, hey, we have a problem.');});} else {this.emit(':ask', 'An expense already exists for ' + category + ' .Perhaps you would like to update the expense?')}})}},'GetExpense': function() {var category = this.event.request.intent.slots.Category.value;if ((typeof(category) != "undefined")) {console.log("\n\nLoading handler\n\n");const params = {TableName: process.env.DYNAMODB_TABLE_EXPENSES,FilterExpression: 'category = :this_category',ExpressionAttributeValues: {':this_category': category}};console.log('Attempting to get expense', params);const self = this;dynamoDb.scan(params, function(err, data) {const item = data.Items[0];if (!item) {self.emit(':ask', 'Sorry, We cant find that expense. Try again with another expense or add a new one.');}if (item) {console.log("DEBUG: Getitem worked. ");self.emit(':ask', 'You put down £' + data.Items[0].amount + ' for ' + data.Items[0].category + '. Is there anything else I can help with?');}});} else {this.emit('NoMatch')}},'DeleteExpense': function() {var category = this.event.request.intent.slots.Category.value;const {userId} = this.event.session.user;console.log(userId)console.log(category)if ((typeof(category) != "undefined")) {console.log("\n\nLoading handler\n\n");const params = {TableName: process.env.DYNAMODB_TABLE_EXPENSES,FilterExpression: 'category = :this_category',ExpressionAttributeValues: {':this_category': category}};console.log('Attempting to get expense', params);dynamoDb.scan(params).promise().then(data => {console.log('Got expense: ' + JSON.stringify(data), params);const self = this;const item = data.Items[0];if (!item) {self.emit(':ask', 'Sorry, We cant delete that expense because it does not exist. Try again with another expense or add a new one.');}if (item) {console.log('Attempting to delete data', data);const newparams = {TableName: process.env.DYNAMODB_TABLE_EXPENSES,Key: {id: data.Items[0].id,createdAt: data.Items[0].createdAt}};console.log(newparams)dynamoDb.delete(newparams, function(err, data) {if (err) {console.error("Unable to read item. Error JSON:", JSON.stringify(err, null, 2));self.emit(':tell', 'Oopsy daisy, something went wrong.');} else {console.log("DEBUG: deleteItem worked. ");self.emit(':ask', 'So, i have deleted the expense with category ' + category + ' . Wanna do anything else?');}});}})}},'UpdateExpense': function() {var category = this.event.request.intent.slots.Category.value;var amount = this.event.request.intent.slots.Amount.value;console.log(category)console.log(amount)if ((typeof(category) != "undefined") || (typeof(amount) != "undefined")) {console.log("\n\nLoading handler\n\n");const params = {TableName: process.env.DYNAMODB_TABLE_EXPENSES,FilterExpression: 'category = :this_category',ExpressionAttributeValues: {':this_category': category}};console.log('Attempting to get expense', params);dynamoDb.scan(params).promise().then(data => {console.log('Got expense: ' + JSON.stringify(data), params);const self = this;let newamount;const item = data.Items[0];if (!item) {self.emit(':ask', 'Sorry, we cant update that expense because it does not exist. Try again with another expense or add a new one.');}if (item) {console.log('Attempting to update data', data);newamount = parseInt(amount, 10) + parseInt(data.Items[0].amount, 10)console.log(newamount)const newparams = {TableName: process.env.DYNAMODB_TABLE_EXPENSES,Key: {id: data.Items[0].id,createdAt: data.Items[0].createdAt},UpdateExpression: "set amount = :newamount",ExpressionAttributeValues: {":newamount": newamount,},ReturnValues: "UPDATED_NEW"};console.log(newparams)dynamoDb.update(newparams, function(err, data) {if (err) {console.error("Unable to read item. Error JSON:", JSON.stringify(err, null, 2));self.emit(':tell', 'Oopsy daisy, something went wrong.');} else {console.log("DEBUG: updateItem worked. ");self.emit(':ask', 'Expense for category ' + category + ' has been updated to £' + newamount + ' . Wanna do anything else?');}});}})}},'AMAZON.YesIntent': function() {this.emit('Prompt');},'AMAZON.NoIntent': function() {this.emit('AMAZON.StopIntent');},'Prompt': function() {this.emit(':ask', 'Hey there and Welcome to Expense management. I can do a couple of things: Add an expense, delete an expense, get an expense and update an expense. Let me know how I can help', 'Please say that again?');},'PromptGet': function() {this.emit(':ask', 'Please tell me what expense category you would like to check', 'Please say that again?');},'NoMatch': function() {this.emit(':ask', 'Sorry, I couldn\'t understand.', 'Please say that again?');},'AMAZON.HelpIntent': function() {const speechOutput = 'You need to mention expense amount and a category';const reprompt = 'Say hello, to hear me speak.';this.response.speak(speechOutput).listen(reprompt);this.emit(':responseReady');},'AMAZON.CancelIntent': function() {this.response.speak('Goodbye!');this.emit(':responseReady');},'AMAZON.StopIntent': function() {this.response.speak('See you later!');this.emit(':responseReady');}};
Don’t forget to update the alexa.appId = “XXXX” value in your lambda function with your Skill ID. (line 10, handler.js). Your skill ID can be found by going to your Skills kit dashboard by logging into https://developer.amazon.com/alexa/console/ask. Under the skill ‘My Expenses’ it will say ‘View Skill ID’. Click this and copy the skill ID. You can also find it in the Endpoint link within your skill. Save the handler.js file now.
Serverless.yml (Update the Serverless.yml file with the following code)
service: alexa-homeprovider:name: awsruntime: nodejs10.xstage: ${opt:stage, self:custom.defaultStage}profile: ${self:custom.profiles.${self:provider.stage}}region: us-east-1environment:DYNAMODB_TABLE_EXPENSES: ${self:service}-expenses-${opt:stage, self:provider.stage}iamRoleStatements:- Effect: AllowAction:- dynamodb:DescribeTable- dynamodb:Query- dynamodb:Scan- dynamodb:GetItem- dynamodb:PutItem- dynamodb:UpdateItem- dynamodb:DeleteItemResource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE_EXPENSES}"custom:defaultStage: devprofiles:dev: serverlessprod: serverlessdynamodb:start:migrate: trueresources:Resources:DynamoDbTableExpenses:Type: 'AWS::DynamoDB::Table'DeletionPolicy: RetainProperties:AttributeDefinitions:-AttributeName: idAttributeType: S-AttributeName: createdAtAttributeType: NKeySchema:-AttributeName: idKeyType: HASH-AttributeName: createdAtKeyType: RANGEProvisionedThroughput:ReadCapacityUnits: 1WriteCapacityUnits: 1TableName: ${self:provider.environment.DYNAMODB_TABLE_EXPENSES}functions:addExpense:handler: handler.handlerevents:- alexaSkill
This will bundle up your application so it is ready for deployment. Also, this file will create your Dynamo DB table with the required permissions and store this in your AWS console. It will also create your Lambda function within your developer console too so you can check that out. You can learn more about serverless Dynamo DB templating here.
This YAML file helps you configure your lambda functions as well as your Dynamo DB table. To see your table in your Dynamo DB storage, if you go to your AWS console, go to Dynamo DB, click on ‘Tables’, then you should see your table created there. However, you can’t do this until you deploy the serverless application. Let’s move onto the next stage.
Create a package.json file and insert the following code:
{"name": "alexa-demo","version": "1.0.0","description": "Alexa Demo","main": "handler.js","dependencies": {"alexa-sdk": "^1.0.25","aws-sdk": "^2.263.1"}}
Save that file and now in your project directory, type in the terminal:
npm install
This will install the relevant packages.
Once you have saved everything, we can now deploy the Alexa skill using Serverless! In your project directory, open a terminal and type in:
serverless deploy
Before we actually start testing our skill, we need to configure an endpoint for our serverless application to work. So, in your Alexa Skills Kit Builder, click on Endpoint, then select ‘AWS Lambda ARN’. In the field ‘Default region’, insert the following:
arn:aws:lambda:us-east-1:<awsaccountid>:function:alexa-home-dev-addExpense
You need to replace <awsaccountid> with your actual 12 digit AWS account ID which you can find in your AWS account. Once you set the endpoint, click on Save Endpoints.
Well done! You just created an Alexa Skill!
Testing the Skill
This is the most exciting part! Now that you have done all the hands-on stuff, go try your skill out!
You can go to the Alexa Developer Console and click on the Test tab. Start by clicking and saying into the microphone, ‘open my expenses’. Otherwise, you can simply type into the text box to interact with Alexa.
You can say stuff like ‘add fifty for fuel’. If you now go to your AWS Dynamo DB storage and into your table, you will see that it added a new record. Amazing! That means your skill is functioning.
Here is a sample conversation you can do with this skill:
Me: open my expenses
Alexa: Hey there and Welcome to Expense management. I can do a couple of things: Add an expense, delete an expense, get an expense and update an expense. Let me know how I can help
Me: add fifty for groceries
Alexa: Added £50 for groceries. You can check an expense, delete an expense or update one. You choose.
Me: i paid an extra fifty on groceries
Alexa: Expense for category groceries has been updated to £100 . Wanna do anything else?
Me: check amount for groceries
Alexa: You put down £100 for groceries. Is there anything else I can help with?
Me: delete expense for groceries
Alexa: So, i have deleted the expense with category groceries . Wanna do anything else?
Me: no
Alexa: See you later!
That’s all for now! I hope that you have found this tutorial very useful and if you get stuck on any part of the tutorial, feel free to contact me. You can find the repository on Gitlab here.
Happy coding 😁