Introduction
As a resident of Chicago, I utilize the local public transportation nearly every day. Particularly, the buses that run immediately by my apartment. My residence is at the intersection of three major bus lines; bi-directionally, totaling six bus stops. A common problem I’ve had is quickly needing to know exactly what the upcoming bus arrival time is. Rather than texting the bus stop numbers, or viewing Google Maps for the upcoming bus arrival times, I decided to implement an Amazon Alexa Skill. This allows me to hands-free prompt the Amazon Echo in either my kitchen or room for the next bus arrival time.
Many topics can be discussed with regards to building Amazon Alexa Skills, but to build a simple one-intent skill is quite straightforward. In this blog, I am going to run through some of the basics for building Amazon Alexa Skills, while building out the Next Bus app described above.
Getting Started
Before starting with skill development, we’ll be using the Chicago Transit Authority’s (CTA) Bus Tracker API. The API is available cost-free, but it is required to sign up for an API key. Access and information for the API can be found here.
Next, we will need to sign up/sign into an Amazon Developer account, then proceed to the Alexa Developer Console. Once in this console, we can click “Create Skill” to initialize our Amazon Alexa Skill.
Write in your desired “Skill Name“, select “Custom” as the skill model, and “Alexa-Hosted (Node.js)” under host method. Selecting “Alexa-Hosted” is convenient when implementing a basic skill since most of the initial setup (lambda creation, logs, etc.) is automated. Additionally, Python is another language that is often used to develop skills with, but for this tutorial we’ll utilize Node.js. Additionally, choose the default template selected at the next setup step.
Interaction Model
The first step in developing an Amazon Alexa Skill is defining the interaction model. Here, we’ll define the invocation name of our skill, along with the intent to get the next bus time.
Invocation Name
The invocation name is essentially two or more words that invoke/launch the skill. Under the “Invocation” tab, we can keep it easy and set our “Skill Invocation Name” to “next bus”. Thus, when stating “Alexa, next bus” the skill is launched.
Note: if you have other transportation-related skills connected to your Alexa account, they may conflict in the namespace for “next bus”.
Intent
Next, we’ll define our skill’s intent. An intent serves an action that reflects a user’s verbal demand. In a way, it can be thought of as a verbal function call. We can construct what this verbal command should look like by defining our intent and the proper parameters.
When requesting the arrival time for the next bus, we need two parameters: 1) the bus number, and 2) the bus direction. When defining an intent, I also find it easier to use a launch phrase, such as “tell“. Additionally, to minimize the word count in our intent to increase verbal speed/efficiency we’ll keep the intent as simple as possible.
Thus, let’s define our intent as: tell next bus {busNumber} {busDirection}
Slot Types
Before we start constructing the intent, let’s look at our slot types. A slot type is essentially a defined type for given verbal parameters. In our case, we need to associate a slot type with busNumber and busDirection, disjoint. Amazon has built-in default slot types for categories including numbers, dates, durations, etc. For busNumber, we can simply use the Amazon default slot type for numbers: AMAZON.NUMBER. We still need to import our AMAZON.NUMBER slot type however. Click “Add” next to “Slot Types“. Then, click the radio button for “Use an existing slot type from Alexa’s built-in library“, search for AMAZON.NUMBER, then click “+ Add Slot Type“.
For busDirection, we need to define a custom slot type however. Let’s define our custom slot type DIRECTION as the following set: {“north”, “east”, “south”, “west”}. To define our custom slot type, click “Add” next to “Slot Types“. In “Create custom slot type” enter “DIRECTION” and create the slot type. Under “Slot Values“, enter “north“, “east“, “south“, and “west” respectfully, clicking on the “+” for each.
Intent Construction
Now that our slot types are ready, let’s construct the actual intent. Underneath “Intents” remove any default intents not under “Built-In Intents“. Next, go ahead and click “Add” next to “Intents“. For “Create custom intent“, let’s name our intent “GetNextBusIntent“. Next we’ll define our intent slots for busNumber and busDirection. Under “Intent Slots“, in the “name” column create a slot called busNumber and associate with the slot type “AMAZON.NUMBER“. Likewise for busDirection, but associate with the slot type “DIRECTION“. Now, under “Sample Utterances“, type “tell next bus {busNumber} {busDirection}“, then click the “+“.
We have our primary intent structure defined, but let’s make a simpler intent structure to also cover the case that the user states only “next bus” before stating a number and direction. Thus, let’s add another sample utterance for “{busNumber} {busDirection}“.
Our GetNextBusIntent should look similar to the following:
Our intent GetNextBusIntent to get the next bus time is now ready to go. At the top of the page, click “Save Model” then “Build Model“.
Code Implementation
With our skill now configured correctly, let’s spin up some code and test!
In the top navigation, click on the “Code” tab. Here, we’ll see a client-side IDE being provided for simple usage. I recommend clicking “Download Skill“, then opening an IDE to then import the unzipped files. Likewise, I recommend checking the default files into version control to help track progress through the tutorial.
Among the unzipped files, index.js should contain a simple Alexa Skill implementation by Amazon. The JavaScript in this file is primarily a collection of intent handlers. While we removed unnecessary intents earlier, I do recommend removing any intent handlers that aren’t being used for any of the built-in intents. For example, HelloWorldIntentHandler can be removed.
Launch Request Handler
Although we want our skill to be invocated and activated with “tell next bus {busNumber} {busDirection}“, our invocation name is “next bus“, and thus would just launch the default LaunchRequest if the user only stated “next bus“. Let’s implement a simple response in the handler LaunchRequestHandler if by chance the user does simply state “next bus“.
We can replace our LaunchRequestHandler in index.js as the following:
const LaunchRequestHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'LaunchRequest'; }, handle(handlerInput) { const speakOutput = 'Welcome, please state the bus number and direction:'; return handlerInput.responseBuilder .speak(speakOutput) .reprompt(speakOutput) .getResponse(); } };
In our handler above, canHandle first checks to ensure the handler should be run by validating LaunchRequest is the active request type. It then builds and returns a response, which includes our prompt [stored in speakOutput] for a bus number and direction.
After adding our above LaunchRequestHandler, we can click “Save“, then “Deploy” to deploy the changes. Next, we can test our intent handler by clicking on the “Test” tab in the top navigation.
On the “Test” tab, we’ll see an Alexa emulator available to us. If we type “next bus“, we should see the following result where Alexa is prompting “Welcome: please state the bus number and direction:“:
At this point, if the user were to state a number and direction, our GetNextBusIntent would be triggered. But, we still need to implement our GetNextBusIntent handler.
Get Next Bus Intent: Basics
Now that we’ve implemented a simple handler for LaunchRequest, we can begin the implementation for the handler of GetNextBusIntent. Let’s implement the initial handler as a function that simply reads us back our bus number and direction to ensure the intent is working as expected.
Let’s add the following initial handler named NextBusIntentHandler:
const NextBusIntentHandler = { canHandle(handlerInput) { return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest' && Alexa.getIntentName(handlerInput.requestEnvelope) === 'GetNextBusIntent'; }, async handle(handlerInput) { // Extract the busNumber and busDirection from the intent const { requestEnvelope } = handlerInput; const busNumber = Alexa.getSlotValue(requestEnvelope, 'busNumber'); const busDirection = Alexa.getSlotValue(requestEnvelope, 'busDirection'); // Respond with the user provided number and direction to confirm the intent functionality const speakOutput = `You requested the next arrival time for bus #${busNumber} ${busDirection}-bound.`; return handlerInput.responseBuilder .speak(speakOutput) .withShouldEndSession(true) // Force the skill to close once intent handled .getResponse(); } };
In the above code, we’re extracting our busNumber and busDirection parameters. Then responding with a string that includes our parameters, allowing us to ensure the foundation of the intent is working.
Before we build and test, we also need to add our NextBusIntentHandler to addRequestHandlers at the bottom of the file as such:
exports.handler = Alexa.SkillBuilders.custom() .addRequestHandlers( LaunchRequestHandler, NextBusIntentHandler, HelpIntentHandler, CancelAndStopIntentHandler, SessionEndedRequestHandler, IntentReflectorHandler ) .addErrorHandlers( ErrorHandler, ) .lambda();
We should now be ready to “Save” and “Deploy“. Now, upon stating “tell next bus forty nine south“, we should receive the response “You requested the next arrival time for bus #49 south-bound.“:
Alternatively, if the user were to say “next bus” first (invoking LaunchRequest), then “forty nine south” (invoking GetNextBusIntent), our skill should be able to handle this workflow as well:
We now have a functioning skill structure and can prompt the skill with a bus number and direction. Thus, we’re ready to implement our async call to the API to receive live bus information.
Get Next Bus Intent: Network Call
Before we implement the call for the data needed, we’ll need to obtain an API key for the Chicago Transit Authority’s (CTA) Bus Tracker API.
Configuration File
Once we obtain an API key, let’s make a separate file called cta.config.js in the same directory as our index.js. In cta.config.js we’ll store our API key along with other configurable options for the Next Bus skill. It is also recommended to add our cta.config.js file to .gitignore, avoiding checking the API key into version control.
For this tutorial, we’ll utilize the bus stops for CTA Bus #49 at Western & Milwaukee. But if you would like to use/test with other bus stops, the information can be found easily via this bus tracker.
We can structure cta.config.js as follows, including the Bus Tracker API key, the root URL for the API, and our desired bus stops:
module.exports = { config: { BUS_API_KEY: 'your_bus_api_key', BUS_ROOT_URL: 'http://www.ctabustracker.com/', BUS_STOPS: { "49": { "north": 8400, "south": 8212 } } } };
Additionally, in index.js, let’s import this config as CTA_CONFIG at the top of the file:
const Alexa = require('ask-sdk-core'); const CTA_CONFIG = require('./cta.config.js');
Now, our configuration file is in place allowing us to easily edit the supported bus stops, and keep our API key abstracted from version control.
Call to Endpoint
For this tutorial, we’ll be using axios to make the call to the API to gather our bus information. Let’s add axios as a dependency in the package.json file:
"dependencies": { "ask-sdk-core": "^2.6.0", "ask-sdk-model": "^1.18.0", "aws-sdk": "^2.326.0", "axios": "^0.19.2" }
Additionally, in index.js, let’s import axios for use at the top of the file:
const Alexa = require('ask-sdk-core'); const CTA_CONFIG = require('./cta.config.js'); const axios = require('axios');
With axios, we can now easily make a call to the CTA Bus Tracker endpoint to gather the data we need. In index.js, we can insert the following code after gathering the busDirection:
// Extract the busNumber and busDirection from the intent const { requestEnvelope } = handlerInput; const busNumber = Alexa.getSlotValue(requestEnvelope, 'busNumber'); const busDirection = Alexa.getSlotValue(requestEnvelope, 'busDirection'); // Given the bus number and direction, get the corresponding bus stop number from the CTA config file const busStop = CTA_CONFIG.config.BUS_STOPS[busNumber][busDirection.toLowerCase()]; // Construct the params needed for the API call const params = { key: CTA_CONFIG.config.BUS_API_KEY, format: 'json', rt: busNumber, stpid: busStop }; // Execute the API call to get the real-time next bus predictions let response = await axios.get(`${CTA_CONFIG.config.BUS_ROOT_URL}bustime/api/v2/getpredictions`, {params: params});
In the above code, we’re first extracting the corresponding bus stop, given our bus number and direction. Next, we’re constructing an object with the required params for the API call. This includes our API key, response format (JSON), bus/route number, and bus stop number. We’re then using axios to make the GET request, and storing the response. Note, we are using async/await to avoid the intent handler from continuing before the response is received.
Additionally, feel free to reference the CTA Bus Tracker API documentation when reviewing the code with regards to the API.
Next, let’s implement the logic to parse the response and construct our speech output string. In index.js, we can remove the previously implemented speakOutput constant and replace with the following:
// Execute the API call to get the real-time next bus predictions let response = await axios.get(`${CTA_CONFIG.config.BUS_ROOT_URL}bustime/api/v2/getpredictions`, {params: params}); // Define the speakOutput string variable, then populate accordingly let speakOutput; // Check to ensure there is a 'bustime-response' object if(response && response.data && response.data['bustime-response']){ // Check to ensure there are available prediction times if(response.data['bustime-response'].prd && 0 < response.data['bustime-response'].prd.length){ // Extract the next prediction time let nextTime = response.data['bustime-response'].prd[0].prdctdn; // Construct the next bus arrival speech output with the given time retrieved speakOutput = `${nextTime} minutes until the next ${busDirection}-bound ${busNumber} bus.`; }else if (response.data['bustime-response'].error && 0 < response.data['bustime-response'].error.length){ // If in this block, there are no available next arrival times for the given bus stop speakOutput = `No available arrival times for bus #${busNumber} ${busDirection}-bound.`; }else{ speakOutput = `An error has occurred while retrieving next time for bus #${busNumber} ${busDirection}-bound.`; } }else{ speakOutput = `An error has occurred while retrieving next time for bus #${busNumber} ${busDirection}-bound.`; }
We’re checking to ensure response has the objects we’re expecting and stating there is an error otherwise. We’re then checking to ensure there are available next bus prediction times; otherwise, stating that there are none. When checking bus times in the off-hours, this is essential to add to the initial implementation.
Given we’ve changed index.js accordingly, we can now “Save” and “Deploy” our changes. Now, if we prompt Alexa with “tell next bus forty nine south” we should see something similar to: “9 minutes until the next north-bound 49 bus.”
Whoo! 🎉
We now have a working Alexa skill that responds with the next bus prediction time actively!
Conclusion
We have successfully implemented a basic Amazon Alexa Skill used for obtaining the next bus time. While implementing a basic skill is relatively straightforward, there are a wide spectrum of topics with regards to Amazon Alexa Skills. Moving forward with skill development, topics such as automated code deployment, display template support, skill localization, and others could be looked into.
With regards to this skill, we can extend our functionality by gathering multiple next arrival times, supporting display templates (for Echo Show), and/or adding support for the subway/train.
For more information on the development of Alexa Skills, Amazon’s documentation provides a high degree of active information.
————————–
Note: this Alexa Skill is not a published [public] skill; only for tutorial purposes.
I have had fun playing with this. Any plans to update the coding now that Display Interface is being replaced by APL?