How to Build an Alexa Skill from Scratch with lazysusan

Learn to build an Alexa skill that’s simple enough you won’t need a ton of code, but complex enough to fully acquaint you with the lazysusan framework.

Written by
Sam McDavid
on
July 11, 2017
Filed under:

Before you dive into this post, you’ll want to check out our previous blog post, which outlines how lazysusan works, what dependencies you'll need to get started, and how to deploy one of the example skills. (Go ahead — we’ll wait.)

Got it? Great. Now, you’re ready to get your hands dirty and build an Alexa skill from scratch.

The Alexa skill we’re building in this post is a fortune cookie skill. It’s a good example to walk through together: simple enough that we don’t need a ton of code, but complex enough to fully acquaint you with the lazysusan framework.

Assumptions

We know what they say about making assumptions — but in this case, we need them so that we can hit the ground running. So, we’re going to assume that you::

  1. Set up all of the dependencies from the previous blog post.
  2. Know how to program well enough that you can read some simple Python code.
  3. Understand YAML syntax.
  4. Have a decent understanding of state machines.

Directory Structure

We’ll start by laying out the directory structure and creating each file and folder — and don’t worry: we’ll do it one a time, explaining each as we go along.

You should create a folder called fortune_cookie to house all of your project files.

.
├── Fortune
│   ├── callbacks
│   │   ├── __init__.py
│   │   └── random_fortune.py
│   ├── lib
│   ├── handler.py
│   ├── serverless.yml
│   └── states.yml
├── envs
│   └── development
├── Makefile
├── intent_schema.json
├── sample_utterances.txt
└── skill_information.txt

Amazon Developer Portal Meta Data

The skill_information.txt, intent_schema.json, and skill_information.txt files are purely for documentation purposes on how you should create in the Amazon Developer Portal. If you work with others to build out an Alexa skill, it’s a good idea to version control this information. The files are included here, so you can just copy and paste the values into the Amazon Developer Portal.

skill_information.txt

Name: Fortune Cookie
Invocation Name: fortune cookie  

intent_schema.json

{  
"intents": [    
   {
     "intent": "StartFortuneCookieIntent"
   },
   {
     "intent": "AMAZON.YesIntent"
   },
   {
     "intent": "AMAZON.NoIntent"
   }
 ]
}

sample_utterances.txt

StartFortuneCookieIntent for my fortune  
StartFortuneCookieIntent for a fortune cookie  
StartFortuneCookieIntent open  
AMAZON.YesIntent yes i want a fortune  
AMAZON.YesIntent yes i want a fortune cookie  
AMAZON.YesIntent yes tell me  
AMAZON.YesIntent i want a fortune  
AMAZON.YesIntent i want a fortune cookie  
AMAZON.YesIntent tell me  
AMAZON.NoIntent no i don't want a fortune  
AMAZON.NoIntent no i don't want a fortune cookie  
AMAZON.NoIntent no don't tell me  
AMAZON.NoIntent i don't want a fortune  
AMAZON.NoIntent i don't want a fortune cookie  
AMAZON.NoIntent don't tell me  
AMAZON.NoIntent no i do not want a fortune  
AMAZON.NoIntent no i do not want a fortune cookie  
AMAZON.NoIntent no do not tell me  
AMAZON.NoIntent i do not want a fortune  
AMAZON.NoIntent i do not want a fortune cookie  
AMAZON.NoIntent do not tell me  

Environments

In lazysusan applications, you’ll probably have multiple deployment environments. Typically:

  • a development environment where you can test changes that you make;
  • a staging environment where changes the team makes can be tested by stakeholders; and
  • a production environment, which is available to anyone who enables your Alexa skill.

To handle this gracefully, you need an envs folder as well as an environment file for each development stage you wish to deploy. Here are the contents of envs/development for this project:

envs/development

AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXXX AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX  
AWS_REGION=us-east-1
DEV_NAME=development  
LAZYSUSAN_SESSION_STORAGE_BACKEND=cookie  
LAZYSUSAN_LOG_LEVEL=logging.INFO  

The environment variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY_ID, AWS_REGION, and DEV_NAME are solely used for deployment. The remaining environment variables adjust the functionality of lazysusan to fit the needs of your skill. Such environment variables are thoroughly covered in the lazysusan documentation.

Makefile

The Makefile helps us script certain actions that we need to take when working with our Docker container. Simply copy and paste this file.

Makefile

NAME = "joinspartan/serverless:1.4"  
SERVERLESS_NAME=fortune  
LAMBDA_NAME=$(SERVERLESS_NAME)-serverless-$(ENV)
ENVDIR=envs 
LAMBDA_DIR=Fortune  
LIBS_DIR=$(LAMBDA_DIR)/lib
.PHONY:    libs shell env-dirs check-env deploy function
run = docker run --rm -it \
        -v `pwd`:/code \
      --env ENV=$(ENV) \
      --env-file envs/$2 \
      --name=$(LAMBDA_NAME) $(NAME) $1
libs :
   @test -d $(LIBS_DIR) || mkdir -p $(LIBS_DIR)
   rm -rf $(LIBS_DIR)/*
   pip install -t $(LIBS_DIR) PyYAML
   @test -f $(LIBS_DIR)/_yaml.so && rm $(LIBS_DIR)/_yaml.so
   pip install -t $(LIBS_DIR) --no-deps -U
git+https://github.com/spartansystems/lazysusan.git
shell : check-env env-dirs
     $(call run,bash,$(ENV))
env-dirs :
     @test -d $(ENVDIR)
# NOTE:
#
#    Deployments assume you are already running inside the docker container
#
#
deploy : check-env
     cd $(LAMBDA_DIR) && sls deploy -s $(ENV)
function : check-env
     cd $(LAMBDA_DIR) && sls deploy -s $(ENV) function -f $(SERVERLESS_NAME)
# Note the ifndef must be unindented
check-env:
 ifndef ENV
     $(error ENV is undefined)
endif  

Make Command Descriptions

$ make libs

This command installs all of the Lambda function dependencies for you so that you won’t have to find them yourself.

$ ENV=*** make shell

This command launches your docker container so that you can perform deployments and run specific tests.

$ make deploy

After running make shell, this command performs a full deploy to the region specified by the ENV variable.

$ make function

Similar to make deploy, this command only deploys code changes to the specified ENV. Keep in mind: if you make any changes to the environment, such as your env file or serverless.yml file, you’ll need to perform the full deploy to make those changes in the deployed environment.

Lambda Function

Up until now, every step we’ve completed has been setup and configuration to make writing the Lambda function as easy and painless as possible. The Lambda function is the actual skill logic that will be used whenever an Alexa device requests output on the user's behalf. Now that we’re ready to start writing code for the Lambda function, we need to create a few files and install the necessary dependencies with these commands.

$ mkdir Fortune
$ touch Fortune/handler.py
$ touch Fortune/states.yml
$ touch Fortune/serverless.yml
$ make libs

Handler

The handler is the entry point for the Alexa skill. Every time a user invokes your skill, the main function in handler.py will be executed. The main function is responsible for accepting a user's input, processing the input to generate the necessary output, and then returning the output to the user. Below are the contents of Fortune/handler.py.

import os  
import sys
CWD = os.path.dirname(os.path.realpath(__file__))
 sys.path.insert(0, os.path.join(CWD, "lib"))
from lazysusan import LazySusanApp
def main(event, lambda_context):
   state_path = os.path.join(CWD, "states.yml")
   app = LazySusanApp(state_path, session_key="FORTUNE_STATE")
   response = app.handle(event)
   return response

Knowing the code is one thing, but knowing how — and why — it works is another. Let’s walk through it together.

The first couple of lines import some dependencies so that the lib folder can be found within the Lambda function. This allows us to load the lazysusan framework and gain access to PyYAML for reading the states.yml file, which we’ll cover in great detail (but not yet).

Once LazySusanApp has been imported, we define our main function. It accepts two arguments: event and lambda_context. These two parameters are sent to your function with every invocation of your skill. The event is a JSON object that ultimately contains the intent and slot values that have been processed out of the user's request. The lambda_context is some meta data concerning the event. While the lambda_context can provide some valuable information, it isn’t vital for generating output for the user.

Inside of the main function, we see a couple of variables being set. First, we obtain the state_path , which is the path to the states.yml file, so that it can be loaded and processed by lazysusan. Next, the variable app instantiates the lazysusan framework with the state_path and a session_key. The state_path is used by lazysusan to generate input based on the event. The session_key is used to define a JSON key for storing the session information for the Alexa request/response cycle.

Once the app is instantiated, we need to obtain the correct response output for the user from the framework. We do this by setting the response variable to the output from app.handle(event). At this point, response is the full JSON response with the generated output to be sent to the user's Alexa device. We could simply return this output and not store it in a variable, but we suggest storing it in a variable so that you can log it out and debug more easily. The final step is to return the JSON response to the user.

States

The states.yml file is where all of the output for the Alexa skill is defined.

For now, we’ll leave it blank and take a closer look at defining states before filling out our states.yml.

Defining States

Because all requests for an Alexa skill hit the same endpoint with request information, we feel the best way to write an Alexa skill is to treat it as a state machine. If you’re unfamiliar with state machines, think of them as a flowchart, with each box in the flowchart being a state and the lines between boxes being the pathway from one state to the next.

State Diagram

Since we’ll be managing states throughout the skill, we need to define the flow for the skill.

Mealy state diagram
Mealy state diagram

The State Schema

When defining responses, lazysusan defers to the schema the Alexa platform has defined, allowing developers to utilize the full functionality of the platform.

However, in order to define states and state transitions, lazysusan has a wrapper schema around the response. While defining states is thoroughly covered in the lazysusan documentation, we'll take a minute to break down an example state definition.

Example Initial State

initialState:
   response:
   outputSpeech:
     type: PlainText
     text: >
       Have a good day!
   shouldEndSession: True
 is_state: True
 branches:
   default: initialState

State Name

On the first line, you’ll see initialState — the name of the state. When you define states, you can name them whatever you want. lazysusan only asks that you define an initialState that will handle the initial interaction with your users.

Response

Underneath the state name is the response key. All the data underneath this key will be converted to JSON and returned to the Alexa platform. This way, you can create a response simply by writing out the Alexa JSON schema as YAML with the output that you desire.

Is State

The next key in the schema is is_state, which defaults to True, so you don't have to specify it on every state. This key determines if the current_state gets updated during a request/response cycle. It’s primarily used when dealing with long form audio to prevent audio callbacks from accidentally triggering state transitions in your skill.

Branches

Finally, the branches section determines the list of next available outputs when the given state is the current state, and a default when an intent is given that is not valid for the state. Underneath branches is a list of key-value pairs: the key is the intent that is received with the Alexa request and the value is the name of the next state.

Initial State

In order for the skill to give the user some output, we need to define an initial state.

initialState:
   response:
   card:
     type: Simple
     title: Fortune Cookie
     content: >
       Would you like to know what your fortune cookie says?
   outputSpeech:
     type: PlainText
     text: >
       Would you like to know what your fortune cookie says?
   reprompt:
     outputSpeech:
       type: PlainText
       text: >
         Would you like to know what your fortune cookie says?
   shouldEndSession:
False  branches:
   AMAZON.NoIntent: goodBye
   AMAZON.YesIntent: !!python/name:callbacks.fortune
   default: initialState

From this state definition, we see that a content card is going to be created in the users companion device. They will be prompted, and reprompted, with the question: "Would you like to know what your fortune cookie says?" As a rule of thumb, if the session on the Alexa device is going to be left open, you should provide reprompt output.

For the branches, if the user responds with "No" (which translates to AMAZON.NoIntent), then the user will receive a goodbye message. If the user responds with "Yes," then we need to execute some custom code to generate a fortune for them. If the user says anything else, the default branch will be triggered, and the skill will start over.

Goodbye State

The goodBye state is the next easiest state to define because it’s also a static state.

goodBye:
   response:
   outputSpeech:
     type: PlainText
     text: >
       Thanks for trying fortune cookie, good bye.
   shouldEndSession: True

The response for the goodbye state should set shouldEndSession to True. This means the skill will halt execution once the output for this state is read to the user.

Fortune Cookie State

In this skill, we’ll dynamically generate a state rather than return a randomly selected static state (like with the dad jokes example in the previous blog post). However, defining a state requires a lot of boilerplate setup, so it makes sense to have a placeholder state and modify only the values that need to be changed.

fortune:
   response:
   outputSpeech:
     type: PlainText
     text: this is placeholder copy
   reprompt:
     outputSpeech:
       type: PlainText
       text: this is placeholder copy
   shouldEndSession: False
 branches:
   AMAZON.NoIntent: goodBye
   AMAZON.YesIntent: !!python/name:callbacks.fortune
   default: initialState
fortunes:
 - "Today it's up to you to create the peacefulness you long for."
- "A friend asks only for your time not your money."
- "If you refuse to accept anything but the best, you very often get it."
- "A smile is your passport into the hearts of others."
- "A good way to keep healthy is to eat more Chinese food."
- "Your high-minded principles spell success."
- "Hard work pays off in the future, laziness pays off now."

The fortune state shouldn’t surprise you. But we added in an extra key at the bottom that is not a state. The fortunes key is just a string array of fortunes, and we select one at random to modify the fortune state and give the output to the user. To execute a Python function for a given intent, reference the function using !!python/name:callbacks.function_name. As long as you import the function you want to call in Fortune/callbacks/__init__.py, then you can reference it in the states file.

Callbacks

If curiosity got the best of you and you read through the lazysusan source code, you know that each callback function is called with an extensive set of kwargs. In Python, kwargs is a dictionary of arguments that can be sent to any function. Within the lazysusan framework, we make all of the data in the skill execution available to you in every callback, so you can always get the data you need. But keep in mind: if you’re going to dynamically generate a response in your callback, it’s your responsibility to update the session state and properly build the response. You can see how to handle this in the fortune callback function.

Fortune/callbacks/random_fortune.py

import random
 from lazysusan.response import build_response_payload
def fortune(**kwargs):
     state_machine = kwargs["state_machine"]
   session = kwargs["session"]
  fortunes = state_machine["fortunes"]
  response = state_machine["fortune"]["response"]
  another_fortune = " Would you like to have another fortune cookie?"
   fortune = random.choice(fortunes)
   response["outputSpeech"]["text"] = fortune + another_fortune
   response["reprompt"]["outputSpeech"]["text"] = another_fortune
   session.update_state("fortune")
   return build_response_payload(response, session.get_state_params())

In the callback, you can see in the function definition that we get the state machine and the session out of kwargs so that they are easier to work with. Then, we get the list of fortunes that we want to pick from, along with the placeholder response that needs to be returned to the user. Next, a random fortune is picked from the array and inserted into the placeholder response. Once the response is generated, the session is updated to reflect the current state, and the response is serialized so that it can be sent back to the user.

Full State Definition

In case you didn't want to build out the states.yml file yourself, here is the full version for you to copy and paste.

Fortune/states.yml

initialState:
   response:
   card:
     type: Simple
     title: Fortune Cookie
     content: >
       Would you like to know what your fortune cookie says?
   outputSpeech:
     type: PlainText
     text: >
       Would you like to know what your fortune cookie says?
   reprompt:
     outputSpeech:
       type: PlainText
       text: >
         Would you like to know what your fortune cookie says?
   shouldEndSession: False
 branches:
   AMAZON.NoIntent: goodBye
   AMAZON.YesIntent: !!python/name:callbacks.fortune
   default: initialState
fortune:
   response:
   outputSpeech:
     type: PlainText
     text: this is placeholder copy
   reprompt:
     outputSpeech:
       type: PlainText
       text: this is placeholder copy
   shouldEndSession: False
 branches:
   AMAZON.NoIntent: goodBye
   AMAZON.YesIntent: !!python/name:callbacks.fortune
   default: initialStategoodBye:
   response:
   outputSpeech:
     type: PlainText
     text: >
       Thanks for trying fortune cookie, good bye.
   shouldEndSession: True
fortunes:  
- "Today it's up to you to create the peacefulness you long for."
- "A friend asks only for your time not your money."
- "If you refuse to accept anything but the best, you very often get it."
- "A smile is your passport into the hearts of others."
- "A good way to keep healthy is to eat more Chinese food."
- "Your high-minded principles spell success."
- "Hard work pays off in the future, laziness pays off now."

Ship It

Congratulations: the hard part is over, and you’re ready to configure your Lambda function deployment, point the skill listing in the Amazon Developer Portal to your Lambda function, and — finally — test it out.

Serverless

Deploying your skill logic to Lambda, we use serverless. This allows you to script your deployments so that they happen in a predictable manner.

Fortune/serverless.yml

service: Fortune
provider:
   name: aws
 runtime: python2.7
 region: ${env:AWS_REGION}
 memorySize: 128
 environment:
   LAZYSUSAN_SESSION_STORAGE_BACKEND:
${env:LAZYSUSAN_SESSION_STORAGE_BACKEND}
   LAZYSUSAN_LOG_LEVEL: ${env:LAZYSUSAN_LOG_LEVEL}
package:
   exclude:
   - "**/*.pyc"
   - "**/*.swp"
functions:
   fortune:
   handler: handler.main
   events:
     - alexaSkill

While the possibilities are endless with serverless, this config just deploys the included files to Lambda, sets some environment variables, and sets up the Alexa Skills Kit Trigger for your Lambda function. Now that you have the deployment configured, deploy away:

$ ENV=development make shell
# The following should be executed inside of the docker container
$ make deploy

Once the deploy command is completed, point your skill to the Lambda function and test it out in the Amazon Developer Portal or on an Alexa enabled device.

For more information about developing Alexa skills with lazysusan, we encourage you to read the documentation, where everything is covered in great detail.

what is blockchain white paper