Application Integration

Host Your Alexa Skill Service on AMPLIFY using API Builder, Part 2

In my Part 1 post, I described how easy it is to host an Alexa Skill Service on AMPLIFY using API Builder. I used a Custom API to handle the POST API request from the Alexa Skill Interface, and then created a JSON reply that contained the text that the Amazon Echo spoke back to me.

In this post, we continue working on our Custom API and add the following items:

  1. Verify the API request from the Alexa Skill Interface as required for getting certification from Amazon
  2. Implement a few more Skill capabilities to make our skill more useful
  3. Use ArrowDB to store data that will persist between sessions

Verify the API Request

According to Amazon, we need to verify the request made to our Skill Service to ensure that it is coming from Amazon. The steps to do this are:

  1. Check the SignatureCertChainUrl header for validity.
  2. Retrieve the certificate file from the SignatureCertChainUrl header URL.
  3. Check the certificate file for validity (PEM-encoded X.509).
  4. Extract the public key from certificate file.
  5. Decode the encrypted Signature header (it’s base64 encoded).
  6. Use the public key to decrypt the signature and retrieve a hash.
  7. Compare the hash in the signature to a SHA-1 hash of entire raw request body.
  8. Check the timestamp of request and reject it if older than 150 seconds.

This must be done on every request.

To make this easier, I leveraged the alexa-verifier npm in my API Builder project.

Note: Remember to add alexa-verifier to the dependency section of package.json:


.
.
"dependencies": {
    "async": "^1.5.0",
    "lodash": "^3.10.1",
    "pkginfo": "^0.3.1",
    "alexa-verifier": "^0.3.0"
},
.
.

My API code is below:


var AlexaAppHandler = Arrow.API.extend({
    group: 'alexa',
    path: '/api/alexaapphandler',
    method: 'POST',
    description: 'this is an api that shows how to handle requests from the Alexa Skill Voice server',
    parameters: {
        version: {description:'version'},
        session: {description:'session'},
        context: {description:'context', optional: true},
        request: {description:'request'}
    },
    action: function (req, resp, next) {
        console.log('AlexaAppHandler called');
        var cert_url = req.headers['signaturecertchainurl'];
        var signature = req.headers['signature'];
        var requestRawBody = JSON.stringify(req.body);
        if(cert_url && signature) {
            verifier(cert_url, signature, requestRawBody, function(error){
                if(!error) {
                    alexaskill(req, resp, next);
                } else {
                    resp.response.status(500);
                    resp.send({"error": "Error verifying source of request to AlexaAppHandler"});
                    next();
                }
            });
        } else {
            resp.response.status(500);
            resp.send({"error": "Proper headers not found"});
            next();
        }
    }
});
module.exports = AlexaAppHandler;

In the action property, I extract the value of two headers: signaturecertchainurl and signature. If they are both present and populated, then I pass them to the verifier method which performs the 8 steps outlined above. If the verification passes, then I call a function called alexaskill which I’ll cover shortly. Otherwise, I send an error reply to the Alexa Skill Interface and the Echo will say something like “There was a problem with the skill”.

Enhancing the Skill Implementation

If the request is verified, then the alexaskill function is called. This function implements the skill logic. The skill code below handles the following requests as required and documented by Amazon:

  1. LaunchRequest
  2. IntentRequest
  3. SessionEndedRequest

var alexaskill = function (req, resp, next) {
    var reqBody = JSON.stringify(req.body);

    switch (req.body.request.type) {
        case "LaunchRequest":
        sendResponse(req, resp, next, "Welcome to Hello Arrow. Ask Hello Arrow to say hi");
        break;

        case "IntentRequest":
            switch(req.body.request.intent.name) {
                case "HelloArrowIntent":
                    sendResponse(req, resp, next, "Hello Arrow");
                    break;

                case "AMAZON.HelpIntent":
                    getHelpCount(req, resp, next);
                    break;

                default:
                    console.log("Invalid intent");
            }
        break;

        case "SessionEndedRequest":
            // Session Ended Request
            break;

        default:
            console.log('INVALID REQUEST TYPE:' +req.body.request.type);
    }
};

The function above basically handles the 3 request types: LaunchRequest, IntentRequest and SessionEndedRequest. If the request is an IntentRequest, it then checks to see which Intent it is: HelloArrowIntent or the built-in AMAZON.HelpIntent.

LaunchRequest

The LaunchRequest will be sent when the user says: “Alexa, open Hello Arrow”. This is your opportunity to tell the user a bit about how to use the skill. Specifically, I tell Alexa to respond with:

“Welcome to Hello Arrow. Ask Hello Arrow to say hi”.

HelloArrowIntent

The intent request, HelloArrowIntent, will be sent when the user says: “Alexa, ask Hello Arrow to say hi” or any of the utterances defined in the Skill Interface Interaction model. I tell Alexa to respond with:

“Hello Arrow”

AMAZON.HelpIntent

The intent request, AMAZON.HelpIntent, will be sent when the user says: “Alexa, ask Hello Arrow for help”. This is your opportunity to provide help. You can see in the code above that I call the getHelpCount function.

More on this shortly as it also brings us to the 3rd main topic of this blog post: data persistence.

SessionEndedRequest

The request SessionEndedRequest will be sent when the user says: “exit”. This is your opportunity to do any cleanup you may need to do. You cannot send back a response to a SessionEndedRequest.

ArrowDB and Data Persistence

Consider a skill that lets a user create and manage a to do list. Or consider a skill that has a long back and forth interaction with the user, such as a recipe skill where the skill can time out and end the session (and have to start over) if the user does not interact within 16 seconds. For these examples and many more, it is important to be able to save data in between sessions.

Amazon promotes the use of DynamoDB for this, but since we are running on AMPLIFY, we have access to ArrowDB. With ArrowDB, we can define a model that will store a userId (passed in with each request) and any data for the user that we would like to persist.

Recall what an Alexa typical request body looks like below. Note that you can access the userId from the session.user object.


{
    "version": "1.0",
    "session": {
        "new": true,
        "sessionId": "amzn1.echo-api.session.xxxxxx",
        "application": {
            "applicationId": "amzn1.ask.skill.yyyyyyyyy"
        },
        "user": {
            "userId": "amzn1.ask.account.ABCDE"
        }
    },
    "context": {
        "AudioPlayer": {
            "playerActivity": "IDLE"
        },
        "System": {
            "application": {
                "applicationId": "amzn1.ask.skill.yyyyyyyyy"
            },
            "user": {
                "userId": "amzn1.ask.account.ABCDE"
            },
            "device": {
                "supportedInterfaces": {
                    "AudioPlayer": {}
                }
            }
        }
    },
    "request": {
        "type": "IntentRequest",
        "requestId": "amzn1.echo-api.request.c04a5519-ea62-453b-80a2-8e6d38945750",
        "timestamp": "2017-02-11T17:29:14Z",
        "locale": "en-US",
        "intent": {
            "name": "HelloArrowIntent"
        }
    }
}

In my skill, I keep track of how many times a user asked for help and tell them the count in the response. For example:

“Welcome to Hello Arrow Helper. You have asked for help 10 times. Ask Hello Arrow to say hi”

The code shown above for handling the AMAZON.HelpIntent intent will call the getHelpCount function. Let’s take a look at this function below:


var getHelpCount = function(req, resp, next) {
    var replyText;
    var model = Arrow.getModel("helpcount");
    model.query({uid: req.body.session.user.userId}, function(err, data){
        if(err) {
            replyText = "Welcome to Hello Arrow Helper. Ask Hello Arrow to say hi";
        } else {
            if(data.length == 0) {
                replyText = "Welcome to Hello Arrow Helper. This is the first time you have asked for help. Ask Hello Arrow to say hi";
                createHelpCountEntry(req);
            } else if(data.length > 1) {
                replyText = "Welcome to Hello Arrow Helper. Ask Hello Arrow to say hi";
            } else {
                replyText = "Welcome to Hello Arrow Helper. You have asked for help "+data[0].count+" times. Ask Hello Arrow to say hi";
                data[0].count = data[0].count+1;
                data[0].update();
            }
        }
        sendResponse(req, resp, next, replyText);
    });
};

Let’s break down the getHelpCount function:

  1. The variable model is a handle to the helpcount ArrowDB database
    • Refer to this link for programmatic CRUD access to Arrow models
  2. Perform a query on the model to find the entry associated with the user (user.userId)
  3. Increment the count and store it back in the database
  4. The rest of the code deals with the following edge cases:
    • first time the user asks for help (i.e. no entry in the database)
    • errors accessing the database
    • multiple entries for the user in the database

If no entry is found for the user, the createHelpCountEntry function is called to create an entry with a count of 1. The code for createHelpCountEntry is shown below:


var createHelpCountEntry = function(req) {
    var model = Arrow.getModel("helpcount");
    model.create({uid: req.body.session.user.userId, count: 1}, function(err, instance){
        if(err) {
            console.log('error creating helpcount database entry, err = '+err);
        }
    });
};

The model helpcount is an ArrowDB model defined as follows:


var Arrow = require('arrow');
var Model = Arrow.createModel('helpcount', {
    fields: {
        uid: {
            type: String
        },
        count: {
            type: Number
        }
    },
    connector: 'appc.arrowdb',
    actions: [
        'create',
        'read',
        'update',
        'delete',
        'deleteAll'
    ]
});
module.exports = Model;

A recording of the interaction can be found here.

As you can see in this post and the prior post, AMPLIFY’s API Builder provides the means for easily hosting Alexa Skill Services. Furthermore, ArrowDB makes it very simple and straightforward to store persistent data that can be used between sessions.

The Custom API can be found here.