Creating an OpenWhisk Alexa skill
In a previous post, I looked at the mechanics of how to create an Alexa skill to tell me which colour 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 end point 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 convient, as detailed in the last post. However, as I can only register one end point with Alexa, but will (eventually) have many intents, I decided to create two actions:
- BinDay: A action to check that the request came from Alexa & 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 re-usable when I create new skills.
BinDay: The router action
BinDay is my router action. It has two tasks:
- Check the provided application id is correct
- Invoke the correct intent action
It’s a standard OpenWhik action, so our function is called main and it takes a dictionary of args and 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 lets look at invoking the correct intent action. Part of the payload from Alexa is the request object that looks something liek 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 it’s 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 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 the in the list.
That’s it for routing. We’re calling out 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 colour 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 colour = "green"
if weekOfYear % 2 == 0 {
// even week - so black bin
colour = "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 colour 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 (colour == "black") {
colour = "green"
} else {
colour = "black"
}
}
Finally we want to say something nice to Alexa. I’ve picked the phrase: “The {colour} bin is next Thursday” for this, but then I realised that as I know which day of the week it is, I could say “The {colour} bin is tomorrow” if it’s Wednesday and “The {colour} bin is today” for Thursday and “The {colour} 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 \(colour) 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 colour bin I need to put out this week.
Fin
The alexa-binday GitHub repository has all the code. It also shows how I organise 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!