Step 2: Building a Location Finder
Add Location Finder Intent
The first feature you’re going to add to your skill is the to provide the closest branch when asked. Add a new intent with some sample phrases to your interaction model so that your skill knows when to search for a location. Similar to how you changed your skill invocation name, you can use the Alexa Skills Toolkit in VS Code to navigate to the web console and add a new intent.
Add 3-5 more phrases similar to the one you just added. Then, remove the HelloWorldIntent
. After you’re done save, build, and download the interaction model locally. Now, skill-package/interactionModels/custom/en-US.json
should look something like this:
{
"interactionModel": {
"languageModel": {
"invocationName": "second national",
"intents": [
{
"name": "AMAZON.CancelIntent",
"samples": []
},
{
"name": "AMAZON.HelpIntent",
"samples": []
},
{
"name": "AMAZON.StopIntent",
"samples": []
},
{
"name": "FindBranchIntent",
"slots": [],
"samples": [
"where is the closest branch",
"where is the closest location",
"where is the nearest branch",
"where is the nearest location",
"closest branch to me"
]
},
{
"name": "AMAZON.NavigateHomeIntent",
"samples": []
}
],
"types": []
}
},
"version": "4"
}
Handling Location Finder Intent for Mobile Devices
You will eventually use the Alexa-enabled device’s location to call the Yext Search API and to search for the closest branch. In the terminal, cd
into the lambda folder and add Search Core with npm i @yext/answers-core
.
Create a new file called api.js
where you will call Search. Initialize the Search Core at the top add the function that will be called to retrieve branch locations.
const { provideCore } = require('@yext/answers-core');
const core = provideCore({
apiKey: 'YOUR_ANSWERS_API_KEY',
experienceKey: 'YOUR_ANSWERS_EXPERIENCE_KEY',
locale: 'en',
sessionTrackingEnabled: true,
endpoints: {
universalSearch: "https://liveapi-sandbox.yext.com/v2/accounts/me/answers/query",
verticalSearch: "https://liveapi-sandbox.yext.com/v2/accounts/me/answers/vertical/query",
questionSubmission: "https://liveapi-sandbox.yext.com/v2/accounts/me/createQuestion",
universalAutocomplete: "https://liveapi-sandbox.yext.com/v2/accounts/me/answers/autocomplete",
verticalAutocomplete: "https://liveapi-sandbox.yext.com/v2/accounts/me/answers/vertical/autocomplete",
filterSearch: "https://liveapi-sandbox.yext.com/v2/accounts/me/answers/filtersearch"
}
});
const retrieveLocation = async (locationData) => {
try {
let searchResults = {};
if(locationData.lat && locationData.long){
searchResults = await core.verticalSearch({
query: '',
verticalKey: 'locations',
location: {
latitude: locationData.lat,
longitude: locationData.long
},
limit: 1
});
}
if(searchResults && searchResults.verticalResults.results.length > 0){
console.log('Branch Found');
const branchAddress = searchResults.verticalResults.results[0].rawData.address;
return {
title: 'Branch Found',
message: `We have a location near you at ${branchAddress.line1}, ${branchAddress.city}, ${branchAddress.region}.`
}
} else {
console.log('Could not find branch');
return {
message: `Sorry, I was not able to find a branch near you.`
}
}
} catch (err) {
console.log(`Answers Error: ${err}`);
return {
message: 'Uh Oh! It looks like something went wrong. Please try again.'
}
}
}
module.exports = {
retrieveLocation
}
Skills need to be configured to use device location. Navigate to the Alexa developer console , click on Models on the left side, then Permissions, and finally enable Location Services.
When using Alexa-enabled non-stationary devices like cars, smart watches, and phones, Alexa can use the geolocation of the device assuming the user has enabled the app and skill to use the device location. If the user is using a non-stationary (stationary devices explained below) device, certain fields will appear in the request envelope. Your skill will also need to ask the user’s permission to access device location if they haven’t already.
In index.js
within the lambda folder, replace the HelloWorldIntentHandler
with a FindBranchHandler
. Much of the code below is derived from this guide on
using location services with Alexa skills.
// index.js
const { retrieveLocation } = require('./api');
const FindBranchHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'FindBranchIntent';
},
async handle(handlerInput) {
console.log('------ ENTERING FindBranchHandler -----')
const { requestEnvelope, responseBuilder } = handlerInput;
const isGeoSupported = requestEnvelope.context.System.device.supportedInterfaces.Geolocation;
const geoObject = requestEnvelope.context.Geolocation;
// Geolocation field only exists for mobile devices
if (isGeoSupported) {
console.log('Request from mobile device...');
// Ask user's permission to allow the skill to use device location if it's not already permitted
if ( ! geoObject || ! geoObject.coordinate ) {
console.log('User needs to provide permission')
return responseBuilder
.speak('Second National would like to use your location. To turn on location sharing, please go to your Alexa app, and follow the instructions.')
.withAskForPermissionsConsentCard(['alexa::devices:all:geolocation:read'])
.reprompt(REPROMT_MESSAGE)
.getResponse();
} else {
console.log('User has provided permission...')
const ACCURACY_THRESHOLD = 100; // accuracy of 100 meters required
if (geoObject && geoObject.coordinate && geoObject.coordinate.accuracyInMeters < ACCURACY_THRESHOLD ) {
console.log(geoObject); // Print the geo-coordinates object if accuracy is within 100 meters
// retrieve the closest location using longitude and latitude
const locationResponse = await retrieveLocation({ lat: geoObject.coordinate.latitudeInDegrees, long: geoObject.coordinate.longitudeInDegrees })
if(locationResponse.title){
return responseBuilder
.speak(locationResponse.message)
.withSimpleCard(locationResponse.title, locationResponse.message)
.reprompt(REPROMT_MESSAGE)
.getResponse();
} else {
return responseBuilder
.speak(locationResponse.message)
.reprompt(REPROMT_MESSAGE)
.getResponse();
}
}
}
}
}
};
Be sure to add your new FindBranchHandler
to the list of Request Handlers at the bottom of index.js
.
// index.js
/**
* This handler acts as the entry point for your skill, routing all request and response
* payloads to the handlers above. Make sure any new handlers or interceptors you've
* defined are included below. The order matters - they're processed top to bottom
* */
exports.handler = Alexa.SkillBuilders.custom()
.addRequestHandlers(
LaunchRequestHandler,
FindBranchHandler,
HelpIntentHandler,
CancelAndStopIntentHandler,
FallbackIntentHandler,
SessionEndedRequestHandler,
IntentReflectorHandler)
.addErrorHandlers(
ErrorHandler)
.withCustomUserAgent('sample/hello-world/v1.2')
.lambda();
Testing Location Finder on a Mobile Device
The Alexa developer console and VS Code Alexa test simulators do not have Geolocation services enabled so you will need to test with the Alexa mobile app. Run the debugger in VS Code and navigate to the Alexa app on your phone. You should be able to open your skill and test your FindLocationIntent
. Before testing, add a location in your Knowledge Graph close to wherever you are right now.
You’ll notice that the microphone remains open after Alexa states the location of the branch. You could instead ask the user if they would like to know the hours of the location or prompt them to schedule an appointment.
Handling Stationary Alexa Devices
If the user of your skill is using a stationary device like an Echo, you’ll want to see if you can get access to the device’s address (not to be confused with geo-location). First, go back to the Permissions screen where you enabled location services and enable Device Address.
You’ll need to use the Alexa
Device Address API
to get the device address for the stationary device if the user has given access to do so. If not, can send a prompt back to their device or mobile app they can use to enable access. Because you’ll be using the Alexa Device API Client, you need to add the DefaultApiCilent
to your handler in index.js
.
// index.js
exports.handler = Alexa.SkillBuilders.custom()
.addRequestHandlers(
LaunchRequestHandler,
FindBranchHandler,
QuestionIntentHandler,
HelpIntentHandler,
NoHandler,
CancelAndStopIntentHandler,
FallbackIntentHandler,
SessionEndedRequestHandler,
IntentReflectorHandler)
.addErrorHandlers(
ErrorHandler)
.withApiClient(new Alexa.DefaultApiClient())
.withCustomUserAgent('sample/hello-world/v1.2')
.lambda();
Now you’ll append an else statement to your FindLocationHandler
. Here, you’ll either get back the device postal code to pass you can use to pass to the Search API to get a branch address or you’ll pass back a prompt for the user to enable access if they so choose. Much of this code is derived from
this guide on calling Alexa Service APIs
.
// index.js
const FindBranchHandler = {
canHandle(handlerInput) {
return Alexa.getRequestType(handlerInput.requestEnvelope) === 'IntentRequest'
&& Alexa.getIntentName(handlerInput.requestEnvelope) === 'FindBranchIntent';
},
async handle(handlerInput) {
console.log('------ ENTERING FindBranchHandler -----')
const { requestEnvelope, serviceClientFactory, responseBuilder } = handlerInput;
const isGeoSupported = requestEnvelope.context.System.device.supportedInterfaces.Geolocation;
const geoObject = requestEnvelope.context.Geolocation;
// Geolocation field only exists for mobile devices
if (isGeoSupported) {
// ..code that you added before for mobile devices
} else {
console.log('Request from stationary device...')
const consentToken = requestEnvelope.context.System.user.permissions
&& requestEnvelope.context.System.user.permissions.consentToken;
// Ask user's permission to allow the skill to use device address if it's not already permitted
if (!consentToken) {
return responseBuilder
.speak('Please enable Location permissions in the Amazon Alexa app.')
.withAskForPermissionsConsentCard(['read::alexa:device:all:address'])
.getResponse();
}
try {
const { deviceId } = requestEnvelope.context.System.device;
const deviceAddressServiceClient = serviceClientFactory.getDeviceAddressServiceClient();
// call Alexa Device API to get device address
const address = await deviceAddressServiceClient.getFullAddress(deviceId);
console.log('Address successfully retrieved, now responding to user.');
if (address.addressLine1 === null && address.stateOrRegion === null) {
// Ask user to set device address if they have not already
return responseBuilder
.speak(`It looks like you don't have an address set. You can set your address from the companion app.`)
.getResponse();
} else {
// retrieve the device address postal code
const locationResponse = await retrieveLocation({ postalCode: address.postalCode })
if(locationResponse.title){
return responseBuilder
.speak(locationResponse.message)
.withSimpleCard(locationResponse.title, locationResponse.message)
.reprompt(REPROMT_MESSAGE)
.getResponse();
} else {
return responseBuilder
.speak(locationResponse.message)
.reprompt(REPROMT_MESSAGE)
.getResponse();
}
}
} catch(error) {
console.log(error)
if (error.name !== 'ServiceError') {
const response = responseBuilder
.speak('Uh Oh. Looks like something went wrong.')
.getResponse();
return response;
}
throw error;
}
}
}
};
Now, if you test using a stationary Alexa device like an Echo, Echo Dot, or Echo Show, your skill will provide the address of the branch closest to you (if there is one), ask your permission to use the device address, or prompt you to add a device address if you haven’t already. To test with an Echo, be sure to log in to the device with the same Amazon account you have developed your skill with. When your debugger is running, you can open the skill by prompting the device and see the logs output in your VS Code debugger terminal.