Sending Reminders

Similar to proactive events, reminders are activated out-of-session when the user isn’t actively using the skill. Unlike proactive events, reminders don’t involve a backend system. Instead, reminders are kind of like alarm clocks, triggered at a specified date and time.

In the Star Port 75 skill, we can set a reminder to remind the user that their trip is about to happen. After the user has scheduled a trip, the skill can ask them if they want to set a reminder for one day before their trip begins. We’ll start by creating a new module responsible for scheduling reminders.

Creating a Reminder

When scheduling a trip, our skill walks the user through a dialog to make sure that it has captured the destination, departure date, and return date from the user. It then confirms that the information gathered meets the user’s expectations. And, if so, it finishes by scheduling the trip and sending the trip details to the backend booking system (which is Google Calendar for our purposes).

It’s at the end of that flow, after the trip has been scheduled, that the skill could offer to set a reminder. We could write the code that sets reminders in the handle() function of ScheduleTripIntentHandler, which is at the tail end of the trip planning flow. But that function is already rather lengthy, so to keep it from getting any longer and to afford us some flexibility in how reminders are set, let’s create a separate module that is solely responsible for the creation of reminders:

 const​ Alexa = require(​'ask-sdk-core'​);
 
 module.exports = {
 async​ setReminder(handlerInput, destination, departureDate) {
 const​ { serviceClientFactory, responseBuilder } = handlerInput;
 
 const​ reminderRequest = require(​'./reminderRequest.json'​);
  reminderRequest.requestTime = ​new​ Date().toISOString();
 const​ reminderText =
  handlerInput.t(​'REMINDER_MSG'​, { destination: destination });
  reminderRequest.alertInfo.spokenInfo.content.push({
 'locale'​: Alexa.getLocale(handlerInput.requestEnvelope),
 'text'​: reminderText,
 'ssml'​: ​`<speak>​${reminderText}​</speak>`
  });
 
 try​ {
 const​ reminderClient =
  serviceClientFactory.getReminderManagementServiceClient();
 await​ reminderClient.createReminder(reminderRequest);
 
  } ​catch​ (error) {
 if​ (error.name === ​'ServiceError'​ &&
  (error.statusCode == 403 || error.statusCode == 401)) {
  responseBuilder.withAskForPermissionsConsentCard(
  [​'alexa::alerts:reminders:skill:readwrite'​]);
 throw​ ​new​ Error(handlerInput.t(​'REMINDER_PERMISSION'​));
  } ​else​ {
 throw​ ​new​ Error(handlerInput.t(​'REMINDER_ERROR'​));
  }
  }
 
  }
 };

As you can see, this module has a single function called setReminder(). The setReminder() function logically has three main sections. The first thing it does is load a reminder request object from a JSON-based template file named reminderRequest.json and populate a few of its properties. The template looks like this:

 {
 "requestTime"​ : ​"TBD"​,
 "trigger"​: {
 "type"​ : ​"SCHEDULED_RELATIVE"​,
 "offsetInSeconds"​ : ​"30"
  },
 "alertInfo"​: {
 "spokenInfo"​: {
 "content"​: []
  }
  },
 "pushNotification"​ : {
 "status"​ : ​"ENABLED"
  }
 }

The first thing to notice about this template is the trigger property. Its type is set to “SCHEDULED_RELATIVE” with offsetInSeconds set to “30” to indicate that the reminder should be triggered exactly 30 seconds after it is created. Ultimately, we’ll want to set a “SCHEDULED_ABSOLUTE” trigger so that the reminder can be set to a time one day before the trip begins. But absolute triggers can be difficult to test because we’ll need to potentially wait several days to see if they work. For now, we’ll use a relative trigger so we can test our skill and get quick feedback.

As you can see, the requestTime property is set to “TBD” as a placeholder value. The setReminder() function overwrites this value with the current time. Also, notice that the alertInfo.spokenInfo.content property is an empty list. setReminder() populates this by pushing a locale-specific object with the text that is to be spoken when the reminder triggers.

The second thing that the setReminder() function does is create the reminder. It does this by passing the reminder request object to the createReminder() function on the reminder client (which was obtained by calling getReminderManagementServiceClient() on the service client factory). If the reminder is created successfully, then the setReminder() function is done. But if anything goes wrong, then that brings us to the third part of the function: the catch block.

The catch block considers the error name and status code to determine if the error occurred because the user hasn’t granted the skill permission to create reminders. If that’s the case, it adds a permissions consent card to the response builder to ask for alexa::alerts:reminders:skill:readwrite permission. It then throws a new error with a localized message telling the user to grant permission in the Alexa application.

In the unlikely event that the createReminder() function fails for any other reason, the setReminder() function ends by throwing an error with a more generic message saying that it couldn’t create the reminder.

The three localized messages referenced in setReminder() will need to be defined in languageStrings.js for each of the supported languages. For English and Spanish, they might look like this:

 module.exports = {
  en: {
  translation: {
  ...
  REMINDER_MSG: ​'A reminder from Star Port 75 Travel: '​ +
 'Your trip to {{destination}} is tomorrow!'​,
  REMINDER_PERMISSION: ​'I was unable to set a reminder. Please grant '​ +
 'me permission to set a reminder in the Alexa app.'​,
  REMINDER_ERROR: ​'There was an error creating the reminder.'​,
  ...
  }
  },
  es: {
  translation: {
  ...
  REMINDER_MSG: ​'Un recordatorio de Star Port 75 Travel: '​ +
 '¡Tu viaje a {{destination}} es mañana!'​,
  REMINDER_PERMISSION: ​'No pude establecer un recordatorio. '​ +
 'Por favor, concédame permiso para configurar un recordatorio en '​ +
 'la aplicación Alexa.'​,
  REMINDER_ERROR: ​'Se produjo un error al crear el recordatorio.'​,
  ...
  }
  }
 }

Since setReminder() may need to ask for alexa::alerts:reminders:skill:readwrite permission, we’ll also need to declare that permission in skill.json, as we’ve done for other permissions before:

 "permissions": [ { "name": "alexa::profile:given_name:read" }, { "name": "alexa::profile:email:read" }, { "name": "alexa::devices:all:notifications:write" },
» {
» "name": "alexa::alerts:reminders:skill:readwrite"
» }
 ],

Having created setReminder() and declaring alexa::alerts:reminders:skill:readwrite permissions in the skill manifest, the only thing remaining is to find a place to call setReminder(). You might be thinking that we should add it to ScheduleTripIntentHandler to set the reminder at the same time that the trip is scheduled. Instead, let’s ask the user if they even want a reminder.

Making Reminders Optional

It would certainly be possible to set a reminder at the end of the trip planning flow. But doing so would be presumptuous without first asking if the user even wants a reminder. Our skill should ask the user if that’s what they want.

There’s a difference between the user granting permission to set reminders and allowing the skill to set a specific reminder. If the user plans several trips through Star Port 75 Travel, they may want to set reminders for some and not for others. The user would need to grant permission once for the skill to set reminders, but the skill shouldn’t assume it should set reminders for every trip and instead should ask each time whether that’s what they want.

In its current form, the handle() function of ScheduleTripIntentHandler concludes by telling the user to enjoy their trip and then closes the session. We’ll need to change it so that it asks if the user wants to set a reminder:

 return​ handlerInput.responseBuilder
  .speak(speakOutput)
» .reprompt(speakOutput)
  .withAskForPermissionsConsentCard(
  [​'alexa::devices:all:notifications:write'​])
  .getResponse();

Notice how in addition to calling speak() on the response builder, we now also call reprompt() with the same text to be spoken. By calling reprompt(), we’re telling Alexa to keep the session open and the microphone on to wait for another utterance. If the user doesn’t say anything, then Alexa will reprompt them after a few moments.

More specifically, we’re going to ask the user if they want the skill to set a reminder. To do that, we’ll need to make a small addition to the text that’s spoken. The following change in languageStrings.js asks the question at the end of the existing text:

 SCHEDULED_MSG: ​"You're all set. Enjoy your trip to {{destination}}!"​ +
 "If you want, I can send you a notification when the reservation "​ +
 "is confirmed. I've sent a card to the Alexa application for you "​ +
 "to give permission for notifications. "​ +
»"Would you like me to send you a reminder about this trip?"​,

Now our skill will ask the user whether or not they want to set a reminder and then leaves the microphone on to wait for a “Yes” or a “No” in response. The built-in AMAZON.YesIntent and AMAZON.NoIntent intents are perfect for this kind of response. To use these intents, we’ll need to declare them in the interaction model, as we’ve done for our other intents:

 {
 "name"​: ​"AMAZON.YesIntent"​,
 "samples"​: []
 },
 {
 "name"​: ​"AMAZON.NoIntent"​,
 "samples"​: []
 },

Be sure to declare them in all of the interaction models for all locales or else they may not work in certain locales.

We’ll also need to create a request handler for each of these intents:

 const​ Alexa = require(​'ask-sdk-core'​);
 const​ reminderSender = require (​'./reminderSender'​);
 
 const​ YesIntentHandler = {
  canHandle(handlerInput) {
 const​ requestEnvelope = handlerInput.requestEnvelope;
 const​ sessionAttributes =
  handlerInput.attributesManager.getSessionAttributes();
 
 return​ Alexa.getRequestType(requestEnvelope) === ​'IntentRequest'
  && Alexa.getIntentName(requestEnvelope) === ​'AMAZON.YesIntent'
  && sessionAttributes.questionAsked === ​'SetAReminder'​;
  },
 async​ handle(handlerInput) {
 const​ sessionAttributes =
  handlerInput.attributesManager.getSessionAttributes();
  sessionAttributes.questionAsked = ​null​;
 const​ destination = sessionAttributes.destination;
 const​ departureDate = sessionAttributes.departureDate;
  handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
 
 var​ speakOutput = handlerInput.t(​'YES_REMINDER'​);
 
 try​ {
 await​ reminderSender.setReminder(
  handlerInput, destination, departureDate);
  } ​catch​ (error) {
  speakOutput = error.message;
  }
 return​ handlerInput.responseBuilder
  .speak(speakOutput)
  .withShouldEndSession(​true​)
  .getResponse();
  }
 };
 
 const​ NoIntentHandler = {
  canHandle(handlerInput) {
 const​ requestEnvelope = handlerInput.requestEnvelope;
 const​ sessionAttributes =
  handlerInput.attributesManager.getSessionAttributes();
 
 return​ Alexa.getRequestType(requestEnvelope) === ​'IntentRequest'
  && Alexa.getIntentName(requestEnvelope) === ​'AMAZON.NoIntent'
  && sessionAttributes.questionAsked === ​'SetAReminder'​;
  },
  handle(handlerInput) {
 const​ sessionAttributes =
  handlerInput.attributesManager.getSessionAttributes();
  sessionAttributes.questionAsked = ​null​;
  handlerInput.attributesManager.setSessionAttributes(sessionAttributes);
 
 const​ speakOutput = handlerInput.t(​'NO_REMINDER'​);
 
 return​ handlerInput.responseBuilder
  .speak(speakOutput)
  .withShouldEndSession(​true​)
  .getResponse();
  }
 };
 
 module.exports = {
  YesIntentHandler: YesIntentHandler,
  NoIntentHandler: NoIntentHandler
 };

The canHandle() function of both request handlers look pretty much like any of our other request handlers, except for one thing. Like other intent request handlers, they check to see the request is an intent request and the intent name is either AMAZON.YesIntent or AMAZON.NoIntent. But they also check a session attribute to understand exactly what the user is saying “Yes” or “No” to.

The problem with AMAZON.YesIntent and AMAZON.NoIntent is that on their own there’s no obvious way to correlate the intent with a question that it is in answer to. If a skill asks multiple yes/no questions, then it is important for the skill to know which question the “Yes” and “No” utterances are for. Even in our skill, where there’s only one yes/no question, we don’t want to handle those intents if the user utters “Yes” or “No” randomly, without being asked a question.

Therefore, the canHandle() of both of these intent handlers inspects a session attribute named questionAsked to see if the “Yes” or “No” is in response to a question identified as “SetAReminder”. That session attribute is set in ScheduleTripIntentHandler, just before returning the response that asks the question:

 const​ sessionAttributes =
  handlerInput.attributesManager.getSessionAttributes();
 sessionAttributes.questionAsked = ​'SetAReminder'​;
 handlerInput.attributesManager.setSessionAttributes(sessionAttributes);

If either request handler matches, the first thing that their respective handle() functions does is clear the questionAsked attribute. Clearing the attribute ensures that the user won’t be able to trigger these intents at any time after the first response to the question.

After clearing the response, both request handlers end by speaking their respective messages to the user, as defined in languageStrings.js:

 YES_REMINDER: ​"Okay, I'll send you a reminder a day before your trip."​,
 NO_REMINDER: ​"Okay, I won't send you a reminder."

The main difference between the two request handlers is that the handler for AMAZON.YesIntent calls the setReminder() function from the module we created earlier to set the reminder. The call is wrapped in a try/catch block so that if there is an error while setting the reminder, the error’s message will be spoken to the user instead of the localized “YES_REMINDER” message.

Don’t forget to use require to load these two request handlers and register them with the skill builder:

 const​ YesNoIntentHandlers = require(​'./YesNoIntentHandlers'​);
  ...
 exports.handler = Alexa.SkillBuilders.custom()
  .addRequestHandlers(
  HelloWorldIntentHandler,
  ScheduleTripIntentHandler_Link,
  ScheduleTripIntentHandler,
  ScheduleTripIntentHandler_InProgress,
» YesNoIntentHandlers.YesIntentHandler,
» YesNoIntentHandlers.NoIntentHandler,
  StandardHandlers.LaunchRequestHandler,
  StandardHandlers.HelpIntentHandler,
  StandardHandlers.CancelAndStopIntentHandler,
  StandardHandlers.FallbackIntentHandler,
 
 
 
 
  StandardHandlers.SessionEndedRequestHandler,
  StandardHandlers.IntentReflectorHandler
  )
  .addErrorHandlers(
  StandardHandlers.ErrorHandler)
  .withApiClient(​new​ Alexa.DefaultApiClient())
  .addRequestInterceptors(
  LocalisationRequestInterceptor)
  .lambda();

Now we’re ready to deploy the skill and try it out. Unfortunately, you won’t be able to fully test reminders using the test facility in the developer console, because it is incapable of triggering a reminder. The skill will still work for the most part, but you’ll get an error in the intent handler for AMAZON.YesIntent when it tries to set a reminder.

Instead, you’ll need to use an actual Alexa device, such as an Echo, Echo Dot, or Echo Show. Or you can also use the Alexa companion application on your phone to test the skill. Launch the skill and plan a trip just as we’ve been doing for the past several chapters. But at the end, when Alexa asks if you want to set a reminder, answer “Yes”. Wait about 30 seconds and Alexa should chime and then immediately speak the reminder text. She’ll repeat the reminder a second time, but you can cancel the timer by saying, “Alexa, cancel timer.”

Setting an Absolutely Timed Reminder

Setting a reminder for 30 seconds after booking a trip gives us immediate satisfaction of hearing the results of our work. But ultimately we want Alexa to speak the reminder one day before the trip starts. To do that, we’ll need to change the reminder request template to use an absolute trigger:

 { "requestTime" : "TBD", "trigger": {
» "type" : "SCHEDULED_ABSOLUTE",
» "scheduledTime": "TBD"
  }, "alertInfo": { "spokenInfo": { "content": [] } }, "pushNotification" : { "status" : "ENABLED" } }

In addition to changing the type property to “SCHEDULED_ABSOLUTE”, we’ve replaced the offsetInSeconds property with a scheduledTime property that will be set to a time that is 24 hours in advance of the departure date of the planned trip. In the setReminder() function we’ll set that time like this:

 const​ Alexa = require(​'ask-sdk-core'​);
 
 const​ subtractDay = ​function​(orig, d) {
  orig.setTime(orig.getTime() - (d*24*60*60*1000));
 return​ orig;
 }
 
 
 module.exports = {
 
  ...
 
 if​ (reminderRequest.trigger.type === ​'SCHEDULED_ABSOLUTE'​) {
  reminderRequest.trigger.scheduledTime =
  subtractDay(​new​ Date(departureDate), 1).toISOString();
  }
 
  ...
 
  }
 };

If the template’s trigger type is “SCHEDULED_ABSOLUTE”, then the subtractDay() utility function will be used to subtract one day from the trip’s departure date and the result will be set to the template’s reminderRequest.trigger.scheduledTime property.

Now if you try the skill and agree to set a reminder, it will set the reminder to trigger 24 hours before the trip’s departure date. As you can imagine, if the trip’s departure date is several days in the future, you will have to wait a while to know if this works.

Although both proactive events and reminders offer ways to communicate with a user outside of an interactive session, they each serve different purposes. Proactive events are best used when triggered by some event. For example, proactive events could be used to alert the user when seats to a sold-out concert become available. Reminders, on the other hand, are best used to remind a user of something that is soon to occur. Using the concert scenario, a reminder could be set to remind the user about the show 24 hours in advance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset