When I was in college, I took a class called, “Programming in the UNIX Environment.” Our first assignment was to install a UNIX based operating system. I had been an avid Windows user so it was all new to me. I’m dating myself here, but for those who are interested, I went with Ubuntu 5.04 Hoary Hedgehog the second release of Ubuntu. After pulling some tricks to repartition my hard drive so I could dual-boot Ubuntu and Windows XP, thanks to a Debian live install disc, I started setting up my programming environment. I was immediately blown away at how simple it was to install things and immediately start programming.
Our second assignment was to build an automation tool. Our professor had us turn in our assignments via email. We had to follow a strict format that included our student ID and the assignment name in the subject line. To grade our programming assignments, our professor created a bash script to automate the process. The script would read through his mbox file, create a directory for each student, save the attached programming files into the directory and compile them. Lastly, the script would run their code and compare our results with the expected results. Given his sanitized mbox file we were asked to recreate his script. It was an amazing assignment and it opened my eyes to the value of automation, which is one of the reasons why I love the Alexa Skills Kit (ASK) Command-Line Interface (CLI).
The ASK CLI makes it easier to manage your Alexa skill throughout its lifecycle. When you create an Alexa skill and choose to host your skill yourself with AWS, the two major parts of your skill exist in two places: the Alexa Developer Console, which includes your voice user interaction model or the VUI, and the AWS Console, which hosts your skill’s code. I love to use my favorite editor to write code on my computer and use the ASK CLI to push my code to AWS Lambda. On the other hand, I prefer to use the Alexa Developer Console to update my interaction model. The graphical user interface is a great tool for updating my intents, utterances, and slots because it translates my button clicks into the JSON format that my skill needs to build the interaction model.
When I check my code into my GitHub repo, I need to sync the changes I made in the Alexa Developer Console with my local files. I could manually copy and paste the interaction model’s JSON from the Alexa Developer Console and paste it over the outdated interaction model that I have on my computer. This, however, takes time and is prone to human error. I’ve introduced compiler errors into my interaction model before from a seemingly simple copy and paste. Thankfully, the ASK CLI has a helpful command called get-model that we can use to automate this task. As you’ll see below, using this command is not entirely hands off. You’ll need to manually provide some information that’s hard to memorize. This information is stored in our skill project, so we can easily write a script to automate looking it up and passing it to the command! Follow along as I walk you through automating the ASK CLI.
Since your skill can have many localized versions, you’ll need to tell the get-model command which locale you want it to download using the locale code. For example, to download the US English version of my skill’s interaction model, we would use en-US. We also have to specify the skill-id that we want to download the model from. We can look up your skill id in the Alexa Developer Console. Just click on the text that says, ‘View Skill ID’.
NOTE: If you want to minimize keystrokes, you can use -l, and -s instead.
Those familiar with the ASK CLI may be wondering why some commands don’t require the skill ID while others do. The sub commands available to the API command are thought of as generic operations and therefore makes no assumptions as to your intention, so you have to provide the context. In this case, we provide the skill ID.
Let’s take a look at the command:
$ ask api get-model --locale en-US --skill-id amzn1.ask.skill.e7afd0e1-843e-4e9b-a397-72090712bcf6
This will print your interaction model to standard output that will appear in the terminal. You can use > to save standard output into a file. Assuming I’m in the root of my project and I wanted to overwrite my en-US.json file which is in the models folder, I would add > models/en-US.json to the end of the command. Altogether the command will appear as:
$ ask api get-model --locale en-US --skill-id amzn1.ask.skill.e7afd0e1-843e-4e9b-a397-72090712bcf6 > models/en-US.json
This is awesome! I’m able to prevent copy and paste errors and can have the ASK CLI get my model for me. However, there’s more we can do to automate using this command.
At this point, I want to call out that the code I’ll be sharing with you below as is has only been tested to work on MacOS in Terminal.app and Linux in the bash shell, respectively. I don’t have a PC running Windows at my disposal. Windows users may have to make some modifications to get things working properly on their systems. From what I understand, if you’re using Windows 10, you can install the bash shell and use the code as is.
I don’t know about you, but my memory isn’t good enough to memorize the randomly generated skill ID. I can memorize the beginning, amzn1.ask.skill., but the randomly generated 36 character string (including dashes) is just too much. Even if I could remember it, I have multiple skills so there’s no way I’m memorizing the skill ID for all of them. There ought to be something we can do to automate fetching the skill ID. We’re in luck because once we deploy a skill using the ASK CLI, it writes the skill ID to our computer’s hard drive in a config file. This file is located a folder called .ask in your skill project’s root folder.
The config file is a JSON file that contains information about your skill project. The ASK CLI reads and writes to this file to store meta information about your skill. When you first create your skill using the ASK CLI, the project exists on your hard drive. Once you deploy it using the ask deploy command, your skill receives a skill ID, which the ASK CLI writes into the config file.
We can write our own script to read from this file and access the skill ID. You can choose your favorite language to write a script. Let’s write in Node.js. The ASK CLI is written in Node.js and requires that you have it installed on your computer, so it’s safe to assume that if you’re using the ASK CLI you can run Node.js scripts. To read the file and access the skill ID, it helps to know the structure of the config file. Let’s take a look at the file:
{
"deploy_settings": {
"default": {
"skill_id": "amzn1.ask.skill.870f08e0-c0ef-4ba7-8889-6319bf9f29c8",
"was_cloned": false,
"merge": {},
"resources": {
"manifest": {
"eTag": "..."
},
"interactionModel": {
"en-US": {
"eTag": "..."
},
"ja-JP": {
"eTag": "..."
}
},
"lambda": [{
"alexaUsage": [
"custom/default"
],
"arn": "arn:aws:lambda:us-east-1:xxxxxxxxxxx:function:ask-custom-coffee-shop-default",
"awsRegion": "us-east-1",
"codeUri": "lambda/us-east-1_ask-custom-coffee-shop-default",
"functionName": "ask-custom-coffee-shop-default",
"handler": "index.handler",
"revisionId": "...",
"runtime": "nodejs8.10"
}]
}
}
}
}
There’s a lot of stuff in this file but we need to access the skill_id. To access it, we can start at the root our JSON object (deploy_settings) and traverse down the tree until we get to skill_id. The resulting path is how we’ll access the skill_id from our script. We can then glue the path together with dots to access the skill_id from our code. Let’s take a look at our path.
deploy_settings.default.skill_id
Before we begin to write our script. We’re going to build it under the assumption that we will use it from our skill project’s root folder. Then we’ll make our script a little smarter. We’ll have it look for the config in the parent folder recursively until it either finds it or stops at the root. Let’s start with creating a script called, get-skill-id.js. Then we’ll define the node modules and constants that we’ll need.
We’re going to need the fs module to read the file, and the path module to help us create our path so we don’t have to wory about manually adding slashes, “/”’s between our folders. These modules are built into nodejs so we don’t need define a package.json file nor run npm install. We will define a constant called CONFIG_FILE and set it to .ask/config which is the relative path to our config file.
const fs = require('fs');
const path = require('path');
const CONFIG_FILE = '.ask/config';
To get the parent-working directory, PWD, which is where our script was called from, we will use an environment variable which happens to be the folder we want to look in for the .ask/config file. We can use the process object to access environment variables. Let’s save the PWD in a variable.
const pwd = process.env.PWD;
Now we’ll use the path module to glue pwd and CONFIG_FILE together. It will automatically add a slash if necessary. This makes our life easier because we don’t have to check if one already exists in order avoid building a path with two slashes in it.
Bad: /Users/justin/skills/coffee_shop//.ask/config
Good: /Users/justin/skills/coffee_shop/.ask/config
We want to make sure our path looks like the one labeled Good: and using path.join will do so.
const configFile = path.join(pwd, CONFIG_FILE);
Now that we have the path to our config file, let’s use the fs module to read it and finally print it to standard out!
First we’ll use existsSync to make sure that file exists. If it doesn’t and we pass it to the readFile function it will throw an error and our script will break.
if (fs.existsSync(configFile)) {
// open the file here
...
}
Once we know that the file exists, can use the following code to open the file.
fs.readFile(configFile, 'utf8', function(err, data) {
});
The readFile function takes three parameters. The path to file to open, the encoding of the file and a callback which will execute once the file has been opened. In our callback, we will:
Now that we understand what the code does piece by piece, take a look at the whole thing. At this point, feel free to copy and paste it into a file called get-skill-id.js and save it into the root folder of one of skills. You can test it by running node get-skill-id.js.
const fs = require('fs');
const path = require('path');
const CONFIG_FILE = '.ask/config';
const pwd = process.env.PWD;
const configFile = path.join(pwd, CONFIG_FILE);
if (fs.existsSync(configFile)) {
fs.readFile(configFile, 'utf8', function(err, data) {
if (err) {
throw err;
}
// prase the JSON
obj = JSON.parse(data);
// print the skill id
console.log(obj.deploy_settings.default.skill_id);
});
}
When you run the script, do you see your skill ID appear in the terminal? If so you’ve successfully automated looking up your skill ID! But we’re not done. We’re just getting started! We’ve automated looking up the skill, but now we need to pass the skill ID to the ASK CLI.
Let’s revisit the ask api get-model command. It requires the locale and the skill ID. Before we wrote our automation script, we pasted the skill ID after the –skill-id option:
--skill-id amzn1.ask.skill.e7afd0e1-843e-4e9b-a397-72090712bcf6
How do we pass the results the our script instead? Simple, we replace the skill ID with the command to run our skill and in backticks, `. Let’s take a look at how that would work.
--skill-id `node get-skill-id.js`
What’s going on here? In short, UNIX magic! It works kind of like algebric functions. Before running the ASK API get-model command, UNIX runs the command defined between the backticks, in this case our script, get-skill-id.js. The result is then passed to ask api get-model, which is executed next.
$ ask api get-model --locale en-US --skill-id `node get-skill-id.js`
That’s so much better, but we’re not about to call it a day. With a minor change we make it so we can use our get-skill-id.js file anywhere within the terminal! Why do we want to do this? Consider the case where you have more than one skill. You wouldn’t want to have to copy and paste get-skill-id.js into each one of your skill project folders. The code won’t change! We can use more UNIX magic to make it available with just one simple line of code.
We will be defining an alias. To do so we will be updating a file called .bash_profile. It should be located in your home directory. Your home directory’s name depends upon your username. On MacOS you can use the tilda slash, ~/, shortcut to get to your home folder.
Before we edit the file though, let’s put our get-skill-id.js file somewhere. I have a scripts folder on my computer where I keep useful scripts. It’s located at /Users/Justin/scripts/. I’ll keep my script there.
Feel free to use your favorite editor to update the file. Using pico on MacOS you can edit the .bash_profile file by running the following command:
$ pico ~/.bash_profile
Don’t be alarmed if the file isn’t empty. Just paste the following on a new line to set your alias: (Remember to change the path to get-skill-id.js to match where it is on your computer.)
alias gskill='node /Users/justin/scripts/get-skill-id.js'
This will alias the command to run your get-skill-id script to gskill. You’ll be able to type, ngskill from the terminal and have your get-skill-id.js script run! When naming your alias try to make it short and descriptive. gskill is short for get skill id.
To save the .bash_profile from terminal press ctrl + o and then press enter.
Once you’ve saved the file you’ll need to refresh your environment. You have two choices:
If you choose to run the source command all you need to do is pass it the path to your .bash_profile, like so: (On Linux the ~/ shortcut doesn’t work so make sure you include the path to it from your home folder.)
$ source ~/.bash_profile
Once you’ve refreshed your environment, you can now run gskill from any folder in your terminal and we can pass it to ask api get-model:
$ ask api get-model --locale en-US --skill-id `gskill`
Isn’t automation fun! We’re almost done, but we can do one more thing to make our script even smarter. Remember when we first wrote it, we assumed that it was always going to be called from the root of our skill project? Now that we have an alias, we can easily call it anywhere, so we should update our script to do so. Let’s say I’m in models folder and I want to update the model. As is, I would have to navigate up one level to the skill project root, to run my command. Why is that? The config file that our skill needs is located in the .ask folder, which is in the root. It’s not a big deal to change directories, but it can add up over time especially if you need to swap back and forth. Let’s turn our automation up to 11 and make our script attempt to find the config file for us!
Wouldn’t it be great if no matter how deep you went into your project folders, the script would look up the folder hierarchy until either found the config file or hit a dead end at your system root? We can do that fairly easily with a recursive function. Recursive functions are functions that call them themselves. It will search for and return the config file if it finds it, so let’s call it findAskConfigFile.
Before we write it, let’s think about how our current script will change. We’ll need to make two changes. First, we’ll need to update how we are setting the configFile. Our yet to be written, findAskConfigFile, function will return the config file so instead of using the path module to join the pwd and CONFIG_FILE, we’ll call our findAskConfigFile function.
Before:
const configFile = path.join(pwd, CONFIG_FILE);
After:
const folders = process.env.PWD.split('/').filter(Boolean);
const configFile = findAskConfigFile(folders);
Our findAskConfigFile function requires an array of folders, so we’ll convert our PWD from a string to an array of folders by splitting on the slash, ‘/’, character. Lastly, we call filter and pass it Boolean which will remove empty entries. If we don’t do this we’ll have an empty entry at the beginning of the folders array since our paths start with a slash.
Currently, our get-skill-id.js script checks to see if the config file exists using fs.existsSync(). Our new findAskConfigFile will do this work for us. It will return the path to the config file if it finds one, and it will return null if it doesn’t. So we can simply check if our configFile exists.
Let’s take a look at our code before and after our second change:
Before:
if (fs.existsSync(configFile)) {
...
}
After:
if (configFile) {
...
}
Alright, now that we’ve laid the ground work, let’s walkthrough our findAskConfigFile function. The function will do 4 things.
1. Check the length of the folders array. It will throw an error if it doesn’t find a config file. Since we are writing a recursive function, the folders array will be empty when we reach our system root.
if (folders.length <= 0) throw 'No config file found!';
2. Build the path to where we think the config file is in. We’ll join the folders array with the slash character, ‘/’ and then use the path module to append the path to the config file.
const directory = folders.join('/');
const askConfigFile = '/' + path.join(directory, CONFIG_FILE);
3. Check to see if the file exists and return it if found. Returning the config file here will stop the recursive function.
if (fs.existsSync(askConfigFile)) {
return askConfigFile;
}
4. If we make it this far into the code, we didn’t find the config file so it’s time to look in the parent. To do that, we will remove the last folder in the folders array which will represent the path the parent folder and return the result of calling findAskConfigFile to look in the parent.
folders.pop();
return findAskConfigFile(folders);
Recursion can be quite tricky to understand, but don’t be intimidated. The way how it works here is that if findAskConfigFile doesn’t find the config file in the current folder, it builds the path the to the parent folder by removing the last folder (the current folder) from the array of folders and calls itself again. The procress repeats until either the function finds the config file or the array of folders is empty.
Now that we understand how it all comes together, here’s the whole get-skill-id.js script including comments:
const fs = require('fs');
const path = require('path');
const CONFIG_FILE = '.ask/config';
// Break the path into an array of folders and filter out
// the empty entry for the leading '/'
const folders = process.env.PWD.split('/').filter(Boolean);
// findAskConfigFile is a recursive function that searches
// from the Parent Working Directory to one folder below root for
// the .ask/config file.
// For example, if PWD is /Work/amazon/skills/coffee_shop/lambda
// it will look for .ask/config in the following folders:
// /Work/amazon/skills/coffee_shop/lambda
// /Work/amazon/skills/coffee_shop
// /Work/amazon/skills
// /Work/amazon
// /Work
const findAskConfigFile = function (folders) {
// The ask cli downloads skills into a folder and
// writes the .ask/config file there. There should
// never be one at your root.
if (folders.length <= 0) throw 'No config file found!';
const directory = folders.join('/');
const askConfigFile = '/' + path.join(directory, CONFIG_FILE);
if (fs.existsSync(askConfigFile)) {
return askConfigFile;
}
// if .ask/config doesn't exist in the current directory
// look in the one above by removing the last item from
// folders and call findAskConfigFile again.
folders.pop();
return findAskConfigFile(folders);
};
const configFile = findAskConfigFile(folders);
if (configFile) {
fs.readFile(configFile, 'utf8', function(err, data) {
if (err) throw err;
// prase the JSON
obj = JSON.parse(data);
// print the skill id
console.log(obj.deploy_settings.default.skill_id);
});
}
At this point, your script should now work anywhere. Go ahead and try it out. Try to run it from the skill project root and from a subfolder like models. No matter how deep into your skill project you go, it will find the config file and print the skill ID! If you make a mistake and run it from somewhere outside of your skill project, it will simply print out a warning that it didn’t find it.
With this script and alias, we have automated looking up our skill ID. As a result, we’ve optimized our workflow to update our local version of the interaction model using the ASK CLI. This will help make us more productive since we no longer have to manually look up the skill ID. We took the time to make searching for the config file proactive so our script will work as long as we are in a folder that belongs to our skill. Lastly, we’re able to reuse this automation! We can use it to look up and feed the skill ID to any of the other ASK API commands that require it. For example, get-skill-enablement and nlu-profile.
I hope this post has inspired you to think about ways that you can automate common tasks associated with building and maintaining an Alexa skill. I would love to hear your ideas. Please share them with me on Twitter at @SleepyDeveloper.