< Home

My Alexa Skill workflow (Part 1/2)

It’s no secret; voice UI is one of the things that get my development juices flowing right now. For over six years I have been developing for the “traditional” web environment (the browser), and it’s refreshing to get your teeth into something new and exciting but still feel more than competent.

So with this excitement hot on the end of my fingertips, I got to work on creating my first Alexa “skill.” After a few attempts, I landed on a great workflow, this is what I am going to share with you over a series of blog posts.

Javascript was my language of choice. I wanted to make use of AWS Lambda. AWS Lambda only supports Node.js, Python, Java, and C#. Being most familiar with Javascript made it and easy choice for me. Unfortunately, AWS Lambda only supports Node 4.3 and 0.10.32 which means no ES6 support right now (silently disappears into the background).

One quick declaimer before we start, this series of articles does not explain the Alexa Skills Kit (ASK) API. If you are the kind of person that needs to know what every line of code is doing, I would strongly recommend checking out the API docs first. You can find them here.

So with that out of the way, let’s get started.

Start off by cloning the Alexa App Server repository:

git clone git@github.com:alexa-js/alexa-app-server.git

By cloning the repository, this will allow you to run a local server which allows you to test your skill. As described in the README of the repository:

The alexa-app-server module offers a stand-alone web server to host Alexa Apps (Skills). Think of it as a simple “container” that allows you to publish multiple Alexa Apps with one server easily.

Personally, I like to separate my skills out into their individual repositories. It is a personal choice. However, it does come with added benefits like security and deployment which is not in the scope of this article. So with that in mind, I like to remove the .git folder from the repository - so think of this more of a directory structure where your skills will sit.

Let’s move into the directory, install the dependencies and then remove the .git file to start.

cd alexa-app-server && npm install && rm -Rf .git/

Once you have done that, move into the examples/apps directory.

cd examples/apps

You should see there are (at the time of writing this) two example skills already in this directory. You can use these for reference at a later date, but for now, we are going to create our skill from scratch.

Let start building our skill by creating the directory your skill will live in:

mkdir spacefact && cd spacefact

Next, lets go ahead and add our dependencies.

npm init
npm install --save-dev alexa-app chai mocha

The above adds Mocha and Chai for testing which we will see in action in a bit. It also adds the alexa-app package, this abstracts the handling of a request from Alexa into a nice little DSL.

That is the tedious config out of the way. Let’s get started with creating our actual skill. Create a file called space_fact.js in the root directory and add the following content:

module.change_code = 1; // Reload module after change

var SpaceFact = (function() {
  var planets = {
    'mercury': 'Mercury is the closest planet to the Sun, and due to its proximity it is not easily seen except during twilight.',
    'venus':   'Named after the Roman goddess of love and beauty, Venus is the second largest terrestrial planet and is sometimes referred to as the Earth\'s sister planet due their similar size and mass.',
    'earth':   'The Earth is the only planet in our solar system not to be named after a Greek or Roman deity.'
  };

  var requestPlanetFact = function(planetName) {
    var key = planetName.toLowerCase();
    if(key in planets) {
      return planets[key]
    } else {
      return 'Sorry; I do not have a fact on the planet you are asking about';
    }
  };

  return {
    requestPlanetFact: requestPlanetFact
  };
})();

module.exports = SpaceFact;

Here, we are creating a Revealing Module which reveals access to the requestPlanetFact function. This function takes one argument - a planet name. This function checks to see if it has a fact for the given planet name and returns that fact if it does. If it doesn’t have a fact for the given planet it will swiftly return “Sorry; I do not have a fact on the planet you are asking about”.

Next, let’s add the test for this module. First, let’s create the test directory and cd into it:

mkdir test && cd test

Create a file called space_fact_test.js and add the following contents:

var chai      = require('chai');
var expect    = chai.expect;
var SpaceFact = require('../space_fact');

describe('SpaceFact', function() {
  describe('#requestPlanetFact', function() {
    context('with a valid planet name', function() {
      it('returns the fact about the planet Mercury', function() {
        expect(
          SpaceFact.requestPlanetFact('Mercury')
        ).to.eq('Mercury is the closest planet to the Sun, and due to its proximity it is not easily seen except during twilight.');
      });

      it('returns the fact about the planet Venus', function() {
        expect(
          SpaceFact.requestPlanetFact('Venus')
        ).to.eq(
          'Named after the Roman goddess of love and beauty, Venus is the second largest terrestrial planet and is sometimes referred to as the Earth\'s sister planet due their similar size and mass.'
        );
      });

      it('returns the fact about the planet Earth', function() {
        expect(
          SpaceFact.requestPlanetFact('Earth')
        ).to.eq('The Earth is the only planet in our solar system not to be named after a Greek or Roman deity.');
      });
    });

    context('with a planet name where it does not have a fact', function() {
      it('returns Sorry; I do not have a fact on the planet you are asking about', function() {
        expect(
          SpaceFact.requestPlanetFact('Uranus')
        ).to.eq('Sorry; I do not have a fact on the planet you are asking about')
      });
    });
  });
});

That’s it. But before we rub our hands together and move on to the next stage lets run Mocha to test it works as expected. From the root directory run:

mocha

You should see the following output:

  SpaceFact
    #requestPlanetFact
      with a valid planet name
        ✓ returns the fact about the planet Mercury
        ✓ returns the fact about the planet Venus
        ✓ returns the fact about the planet Earth
      with a planet name where it does not have a fact
        ✓ returns Sorry, I do not have a fact on the planet you are asking about


  4 passing (9ms)

So with that out of the way we are ready can go ahead and build the “handler.” Let’s create a file in the root directory called index.js, and add the following.

module.change_code = 1;

var Alexa     = require('alexa-app');
var app       = new Alexa.app('spacefact');
var SpaceFact = require('./space_fact');

This first includes the alexa-app module. We use this to create an instance of the alexa-app module for us to use.

The summary part of the GitHub README does a magnificent job of explaining what this module will provide to you:

The alexa-app module does the dirty work of interpreting the JSON request from the Alexa platform and building the JSON response that can be spoken on an Alexa-compatible device, such as the Echo. It provides a DSL for defining intents, convenience methods to more easily build the response, handle session objects, and add cards.

Then we want to handle a LaunchRequest sent by Alexa. Our skill receives a LaunchRequest when the user invokes the skill with the invocation name but does not provide any command mapping to an intent. Add the following to the bottom of the index.js file:

app.launch(function(req, res) {
  var prompt = 'Ask me about the planet Mercury, Venus or Earth. I will give you a fact on the one you choose.';
 res.say(prompt).reprompt(prompt).shouldEndSession(false);
});

Next, we want to handle an IntentRequest sent from the Alexa platform. Our skill receives an IntentRequest when the user speaks a command that maps to a custom intent. You can have multiple custom intent handlers, but for now, we are going to stick with one. Add the following to the bottom of the index.js file:

app.intent('planetFact', {  
 'slots': {
    'WORD': 'POSSIBLEWORDS'
  },
  'utterances': ['{tell me about | tell us about |} {-|WORD}']
}, function(req, res) {
  var word     = req.slot('WORD');
  var response = SpaceFact.requestPlanetFact(word);
  res.say(response).shouldEndSession(true).send();
});

Next, we want to handle the built-in intent AMAZON.StopIntent. This intent is called when the user says “stop”, “off”, “shut up” or something similar. This Intent will let the user stop an action but remain in the skill or exit the skill completely, in our case we are going to exit. Add the following to the bottom of the index.js file:

app.intent('AMAZON.StopIntent', function(req, res) {
  res.say("Goodbye").shouldEndSession(true);
});

Next, we want to handle a SessionEndedRequest sent by the Alexa platform. Our skill receives a SessionEndedRequest when a currently open session is closed for one of the following reasons:

  • The user says “exit.”

  • The user does not respond or says something that does not match an intent defined in your voice interface while the device is listening for the user’s response.

  • An error occurs.

(taken from the ASK docs)

Add the following to the bottom of the index.js file:

app.sessionEnded(function(req, res) {
  res.say("Goodbye").shouldEndSession(true);
});

Finally followed by:

module.exports = app;

That is it; now we are ready to go. We just need to boot up the alexa-app-server module and test it works as expected. From the terminal navigate back to the examples directory and start the node server:

node server

If everything went successfully, you should be able to visit localhost:8080/alexa/spacefact.

Up until this point, you have been thinking “What was the reason for cloning this repository first” this is where it will come into its own. You should see a screen similar to the below:

Alexa App Server Screenshot

Now we can test each of our handlers in the browser.

LaunchRequest

Under the heading Request, choose LaunchRequest from the type drop down. By doing this, the app will then display the request that will be sent to our skill via the Alexa platform (this comes in handy later, when we set up our lambda.) Proceed to click Send Request and scroll down to the “Response” heading. You will see the response that will be forwarded to Alexa from our skill. With any luck you should see something similar to the below:

{
  "version": "1.0",
  "response": {
    "directives": [],
    "shouldEndSession": false,
    "outputSpeech": {
      "type": "SSML",
      "ssml": "<speak>Ask me about me about the planet Mercury, Venus or Earth. I will give you a fact on the one you choose.</speak>"
    },
    "reprompt": {
      "outputSpeech": {
        "type": "SSML",
        "ssml": "<speak>Ask me about me about the planet Mercury, Venus or Earth. I will give you a fact on the one you choose.</speak>"
      }
    }
  },
  "sessionAttributes": {},
  "dummy": "text"
}

You can see _“Ask me about me about the planet Mercury, Venus or Earth. I will give you a fact on the one you choose.”, the response which we set up to be our response to a LaunchRequest in our index.js file:

IntentRequest

Next, under the heading Request, choose IntentRequest from the type drop down. By doing this, the app will then display another dropdown called “Intent,” from there, choose planetFact. Finally, type “Mercury” into the “WORD” input and click “Send Request.” Again this will produce the request sent by the Alexa platform to our skill. But the cool part about this is if you scroll down to the heading Response you will see the following output:

{
  "version": "1.0",
  "response": {
    "directives": [],
    "shouldEndSession": true,
    "outputSpeech": {
      "type": "SSML",
      "ssml": "<speak>Mercury is the closest planet to the Sun and due to its proximity it is not easily seen except during twilight.</speak>"
    }
  },
  "sessionAttributes": {},
  "dummy": "text"
}

Pretty neat, huh? Go ahead, type in a planet name that is neither Mercury, Venus or Earth and take a look at the response - I will wait.

AMAZON.StopIntent

Next, under the heading Request, choose AMAZON.StopIntent from the type drop down and click Send Request. Scroll down to the heading Response, and you will see the following output:

{
  "version": "1.0",
  "response": {
    "directives": [],
    "shouldEndSession": true,
    "outputSpeech": {
      "type": "SSML",
      "ssml": "<speak>Goodbye</speak>"
    }
  },
  "sessionAttributes": {},
  "dummy": "text"
}

SessionEndedRequest

Finally, under the heading Request, choose SessionEndedRequest from the type drop down and click Send Request. Scroll down to the heading Response, and you will see the following output:

{
  "version": "1.0",
  "response": {
    "directives": [],
    "shouldEndSession": true,
    "outputSpeech": {
      "type": "SSML",
      "ssml": "<speak>Goodbye</speak>"
    }
  },
  "sessionAttributes": {},
  "dummy": "text"
}

Again, exactly what we set up for our response to a SessionEndedRequest sent by the Alexa platform to our skill.

So there we have it, you have built your first Alexa skill. I know, I know. I can hear cries of “What? I at least thought I would be able to test it out on my Echo”. Yep, that part is in the pipeline and will be part two of this series of articles so hold tight.

Want to do more?

  1. Try adding another Custom Intent to the skill. Maybe, facts about Galaxies. As a heads up, this will mean a refactor for the planetFact intent and the utterances.

  2. Try adding more “utterances” to the planetFact intent. At the moment, the skill only responds to either tell me about {WORD}, tell us about {WORD} or {WORD}. Try expanding this to accompany more phrases. Visit this for a push in the right direction.

  3. This post was more about the codebase rather than an in-depth explanation of the ASK API. I would recommend investing some time in understanding what is meant by Utterances, Intents, Slots, etc. Visit this for a push in the right direction.