Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Creating an OpenWhisk Alexa Skill

DZone's Guide to

Creating an OpenWhisk Alexa Skill

Break out your Swift and dive into creating your own Alexa skill! Here, we use OpenWhisk to implement it, giving you a serverless backdrop to your action.

· IoT Zone
Free Resource

Build an open IoT platform with Red Hat—keep it flexible with open source software.

In a previous post, I looked at the mechanics of how to create an Alexa skill to tell me which color bin I needed to put out next. I'll now look at how I chose to implement it in OpenWhisk, using Swift.

An Alexa skill consists of a number of intents, and you register a single endpoint to handle them all. As I'm using OpenWhisk, I have direct web access to my actions without having to worry about setting up a separate API Gateway, which is convenient, as detailed in the last post. However, as I can only register one endpoint with Alexa, but will (eventually) have many intents, I decided to create two actions:

  • BinDay: An action to check that the request came from Alexa and invoke the correct intent action,
  • NextDay: An action to process the NextDay intent,

By splitting this way, I can implement more intents simply by adding new actions and not need to change my entry point BinDay action. Also, in theory, BinDay is reusable when I create new skills.

BinDay: The Router Action

BinDay is my router action. It has two tasks:

  1. Check the provided application id is correct
  2. Invoke the correct intent action

It's a standard OpenWhisk action, so our function is called main and it takes a dictionary of args. We must return a dictionary, which will be converted to JSON for us. This looks like:

func main(args: [String:Any]) -> [String:Any]
{
	// action code goes here
}


Let's look at how to check the application id:

// check application id
guard
    let session = args["session"] as? [String:Any],
    let application = session["application"] as? [String:Any],
    let applicationId = application["applicationId"] as? String
else {
    print("Error: Could not find applicationId");
    return ["problem": "Could not find applicationId"];
}


As Swift is strictly typed, we need to walk down our nested session dictionary to the application dictionary, where we'll find the applicationId string. The nice way to do this is via guard, so we can we be sure that applicationId is valid if we get past the else statement.

We can now check that the received id is the one we expect:

if (applicationId != getSetting("application_id")) {
    print("Error: Wrong applicationId.\nExpected: \(getSetting("application_id"))\nReceived: \(applicationId)");
    return ["problem": "Wrong applicationId"];
}


I have a useful helper function called getSetting, which retrieves a setting from the settings parameter dictionary. These are stored parameters.json and are bound to the package so that every action has access to them. This is a convenience, but it would arguably be wiser to bind the just the needed settings to each action. A simple comparison between the received applicationId and our setting determines if this call is legitimate. If it isn't, we return an error.

Now let's look at invoking the correct intent action. Part of the payload from Alexa is the request object that looks something like this:

"request": {
    "type": "IntentRequest",
    "requestId": "EdwRequestId.47fe6314-27b8-45c8-9cb9-2233695b7332",
    "locale": "en-GB",
    "timestamp": "2017-07-15T13:05:38Z",
    "intent": {
      "name": "NextBin",
      "slots": {}
    }
},


The key item in here is the intent object with its name and slots. I determined by experimentation that these properties may not exist, so I decided that if the intent was missing, then the user probably wanted the NextBin intent, so let's make that a default.

// route
var intentName = "NextBin"
var slots: [String:Any] = [:]
if
    let request = args["request"] as? [String:Any],
    let intent = request["intent"] as? [String:Any]
{
    // Found intent dictionary. if we didn't find it, then we use the defaults set up earlier
    print("Found intent dictionary")

    intentName = intent["name"] as? String ?? intentName
    slots = intent["slots"] as? [String:Any] ?? slots
}


Again, as Swift is strictly typed, we have to walk down the request to get to the intent, but this time, I used the if let construct so that I could define defaults for intentName and slots. If we find an intent dictionary, we'll override our defaults if the name or slots propreties exist. The nil-coalescing operator (??) is good for that.

Now that we know which intent is required, we can invoke an action of the same name:

// work out actionName to invoke
let thisActionName = env["__OW_ACTION_NAME"] ?? ""
var parts = thisActionName.components(separatedBy: "/")
parts.removeLast()
parts.append(intentName)
let actionName = parts.joined(separator: "/")

// invoke actionName
let invocationResult = Whisk.invoke(actionNamed: actionName, withParameters: slots)


Firstly, we work out the name of the action we want to invoke. We need the fully qualified action name, which consists of the namespace, the package and then the action name, operated by forward slashes. Rather than hard-code anything, I take advantage of the fact that the environment variable __OW_ACTION_NAME contains the fully qualified action name for this action. For me, this is /19F_dev/AlexaBinDay/BinDay as my namespace is 19FT_dev, I picked the package name AlexaBinDay and this is the BinDay action.

We end up with an actionName of 19FT_dev/AlexaBinDay/NextBin for the NextBin intent and invoke it using Whisk.invoke, which is a package supplied in the OpenWhisk Swift runtime.

We can now return whatever the intent action returns straight to Alexa:

if
    let response = invocationResult["response"] as? [String:Any],
    let result = response["result"] as? [String:Any],
    let success = response["success"] as? Bool,
    success == true
{
    return result
}
 
return ["problem": "Failed to get a response from the intent action"];


We extract a response from the invocationResult and get the result and success flag from it. If success is true, then we can return the result to Alexa. Again, the if let construct is useful here, as it allows us to list a set of conditions and also assign constants as we go so that we can use them in the list.

That's it for routing. We're calling our intent action, which will do the real work, and returning the response to Alexa.

NextDay: The Intent Action

The NextDay action has to determine what color bin is next. At the moment, this is a simple hardcoded algorithm. For my particular case, each bin is put out every other week, so on even week numbers, it's the black bin, and on odd week numbers, it's the green one:

let today = Date()
let calendar = Calendar.current
let weekOfYear = calendar.component(.weekOfYear, from: today)

var color = "green"
if weekOfYear % 2 == 0 {
    // even week - so black bin
    color = "black"
}


However, there's one wrinkle. The bin is put out on Thursday, so if it's Friday, we need to tell the user the other color as that's the bin to be put out next week. We can do this using the weekday calendar component, which is a number where 0 is Sunday, 1 is Monday, and so on:

let weekday = calendar.component(.weekday, from: today)
if (weekday > 5) {
    // it's after Thursday so we need to swap
    if (color == "black") {
        color = "green"
    } else {
        color = "black"
    }
}


Finally, we want to say something nice to Alexa. I've picked the phrase: "The {color} bin is next Thursday" for this, but then I realized that, as I know which day of the week it is, I could say "The {color} bin is tomorrow" if it's Wednesday, "The {color} bin is today" for Thursday, and "The {color} bin is this Thursday if it's Monday or Tuesday:

var next = "this Thursday"
if weekday == 4 {
    next = "tomorrow"
} else if weekday == 5 {
    next = "today"
} else if weekday == 0 || weekday > 5 {
    next = "next Thursday"
}
 
return createAlexaResult("The \(color) bin is \(next)")


Finally, we use a helper function to create the correct Alexa formatting dictionary, as that's boilerplate:

func createAlexaResult(_ msg: String) -> [String:Any]
{
    return [
        "response" : [
            "shouldEndSession" : true,
            "outputSpeech" : [
              "type" : "text",
              "text" : msg,
            ],
        ],
        "version" : "1.0",
    ]
}


This is then sent back to Alexa, and I now know which color bin I need to put out this week.

Fin

The alexa-binday GitHub repository has all the code. It also shows how I organize my Swift OpenWhisk projects with a Makefile and a couple of shell scripts so that I can easily develop my actions. I should probably write about how this works.

Until then, just have a poke around the code!

Download Red Hat’s blueprint for building an open IoT platform—open source from cloud to gateways to devices.

Topics:
iot ,alexa skills ,openwhisk ,swift ,intents ,tutorial

Published at DZone with permission of Rob Allen, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}