Create a confirmation step

The agent currently schedules an appointment without asking users to confirm the date and time. Once the Make Appointment intent receives the date and time parameters from the user, the intent immediately creates an appointment block on the bike shop's calendar using the Google Calendar API. Now we need to add an additional step to confirm the date and time parameters before scheduling an appointment.

Figure 4. The happy path is extended with an additional step: confirmation.

Understand what to build

Check out the following sample dialog that contains the confirmation step:

  • User: I'd like to make an appointment.
  • Agent: Okay, I can help you with that. What day do you want to come in?
  • User: Today.
  • Agent: And what time works for you?
  • User: 3 PM.
  • Agent: Okay, Wednesday, August 1 at 3 PM. Did I get that right?
  • User: Yes.
  • Agent: Got it. I have your appointment scheduled on Wednesday, August 1 at 3 PM. See you soon. Good-bye.

Let's break down this sample dialog into the following sequence of events:

  • A user reveals the need for scheduling an appointment:
    • I'd like to make an appointment.
  • An intent (the first intent) prompts the user for the date and time of the appointment, via slot-filling:
    • Okay, I can help you with that. What day do you want to come in?
    • And what time works for you?
  • After receiving the date and time information, the intent asks the user to confirm the date and time:
    • Okay, Wednesday, August 1 at 3 PM. Did I get that right?
  • The user verifies the time and date:
    • Yes.
  • The next intent (the second intent) acknowledges the user's affirmative response and schedules an appointment:
    • Got it. I have your appointment scheduled on Wednesday, August 1 at 3 PM. See you soon. Good-bye.

The existing Make Appointment intent can still serve as the first intent. However, we need to modify the Make Appointment intent so it prompts the user to confirm the date and time of the appointment instead of scheduling it:

Okay, Wednesday, August 1 at 3 PM. Did I get that right?

The second intent, which is invoked after the Make Appointment intent, now handles the actual scheduling of an appointment. This means the second intent has the following dependency to the Make Appointment intent (the first intent):

  • The second intent is invoked only after the first intent is invoked.
  • The second intent receives the date and time information from the first intent.

In the Understand how contexts work section, we learned that we can use contexts to satisfy these requirements; we can create a follow-up intent to establish the parent and child relationship between an existing intent and a new intent.

Yes and no follow-up intents

Another important characteristic of the second intent is that it needs to catch a variety of user utterances that mean yes or no. Creating an intent with a custom entity that contains all possible affirmative and negative responses can be time-consuming—in all languages, people express yes or no in many different ways.

Luckily, Dialogflow offers a pair of special follow-up intents for this exact purpose: yes and no follow-up intents. (Dialogflow provides 5 types of special follow-up intents: custom, fallback, yes, no, and later.) A yes follow-up intent comes with a long list of training phrases that contain common expressions that translate to yes (see Figure 5), and a no follow-up intent with a list of training phrases that translate to no.

Figure 5. The first page of a yes follow-up intent's training phrases.

Next, let's use the yes and no follow-up intents to create the confirmation step for the agent.

How to build it

The agent currently invokes the Make Appointment intent when a user reveals the need for scheduling an appointment. The Make Appointment intent then obtains the data and time parameters from the user via slot-filling and schedules an appointment immediately.

To introduce the confirmation step, we need to make the following changes:

  • Update the Make Appointment intent's fulfillment function to split its tasks into two functions:
    • The checkAppointment() function checks if the time slot is available.
    • The makeAppointment() function schedules an appointment.
  • Create a pair of yes and no follow-up intents to do the following:
    • In case of a negative response, the no follow-up intent cancels the scheduling of the appointment.
    • In case of an affirmative response, the yes follow-up intent's fulfillment function makes an API call to Google Calendar to schedule the appointment.

Figure 6. Flowchart showing the yes and no follow-up intents in the bottom right corner.

Create a pair of yes and no follow-up intents

To create a pair of yes and no follow-up intents using the console, follow these steps:

  1. Click on Intents in the left navigation.
  2. Hover over the Make Appointment intent.
  3. Click Add follow-up intent and select yes. (The follow-up intent Make Appointment - yes is created under Make Appointment.)
  4. Hover over the Make Appointment intent again.
  5. Click Add follow-up intent and select no. (The follow-up intent Make Appointment - no is created under Make Appointment.)
  6. Click on Make Appointment - no.
  7. In the Contexts section, add MakeAppointment-followup as Add output context.
  8. Update the lifespan of the output context MakeAppointment-followup to 0.
  9. In the Responses section's Text response table, copy and paste the following response: Okay, I canceled your appointment. Is there anything else I can help you with?
  10. Click SAVE.
  11. Click on Intents in the left navigation.
  12. Click on Make Appointment - yes.
  13. Scroll down to the Fulfillment section and toggle on the Enable webhook call for this intent button.
  14. Click SAVE.

Update the fulfillment code

To update the fulfillment code, do the following:

  1. Click Fulfillment on the navigation bar to go to the fulfillment page.
  2. Copy and paste the content below to the package.json tab of the inline editor:

    {
      "name": "DialogflowFirebaseWebhook",
      "description": "Firebase Webhook dependencies for a Dialogflow agent.",
      "version": "0.0.1",
      "private": true,
      "license": "Apache Version 2.0",
      "author": "Google Inc.",
      "engines": {
        "node": "6"
      },
      "scripts": {
        "lint": "semistandard --fix \"**/*.js\"",
        "start": "firebase deploy --only functions",
        "deploy": "firebase deploy --only functions"
      },
      "dependencies": {
        "firebase-functions": "^2.0.2",
        "firebase-admin": "^5.13.1",
        "googleapis": "^27.0.0",
        "actions-on-google": "2.2.0",
        "dialogflow-fulfillment": "0.6.1"
      }
    }
    
  3. Go to the index.js tab of the inline editor.

  4. From the existing code, copy the lines that contain your Google Calendar credentials (which look like the code below) and save the lines in a text file temporarily:

    const calendarId = 'your-google-calendar-ID-here@group.calendar.google.com';
    const serviceAccount = {
      "type": "service_account",
      "project_id": "marysbikeshop-...",
      "private_key_id": "...",
      "private_key": "-----BEGIN PRIVATE KEY-----\n …",
      "client_email": "bike-shop-calendar@<project-id>.iam.gserviceaccount.com",
      "client_id": "...",
      "auth_uri": "https://accounts.google.com/o/oauth2/auth",
      "token_uri": "https://accounts.google.com/o/oauth2/token",
      "auth_provider_x509_cert_url": "https://www.googleapis.com...",
      "client_x509_cert_url": "https://www.googleapis.com/robot/v..."
    };
    

    Note: If you haven't set up Google Calendar credentials, follow the final set of instructions from the Create fulfillment using webhook section of the previous tutorial.

  5. Copy and paste the code below to the index.js tab of the inline editor:

    /**
     * Copyright 2017 Google Inc. All Rights Reserved.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    'use strict';
    
    const functions = require('firebase-functions');
    const {google} = require('googleapis');
    const {WebhookClient} = require('dialogflow-fulfillment');
    
    // Enter your calendar ID and service account JSON below.
    // See https://github.com/dialogflow/bike-shop/blob/master/README.md#calendar-setup
    const calendarId = '<INSERT CALENDAR ID HERE>'; // Example: 6ujc6j6rgfk02cp02vg6h38cs0@group.calendar.google.com
    const serviceAccount = {}; // The JSON object looks like: { "type": "service_account", ... }
    
    // Set up Google Calendar service account credentials
    const serviceAccountAuth = new google.auth.JWT({
      email: serviceAccount.client_email,
      key: serviceAccount.private_key,
      scopes: 'https://www.googleapis.com/auth/calendar'
    });
    
    const calendar = google.calendar('v3');
    process.env.DEBUG = 'dialogflow:*'; // It enables lib debugging statements
    
    const timeZone = 'America/Los_Angeles';  // Change it to your time zone
    
    exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
      const agent = new WebhookClient({ request, response });
    
      // This function receives the date and time values from the context 'MakeAppointment-followup'
      // and calls the createCalendarEvent() function to mark the specified time slot on Google Calendar.
      function makeAppointment (agent) {
        // Get the context 'MakeAppointment-followup'
        const context = agent.context.get('makeappointment-followup');
        // This variable needs to hold an instance of Date object that specifies the start time of the appointment.
        const dateTimeStart = convertTimestampToDate(context.parameters.date, context.parameters.time);
        // This variable holds the end time of the appointment, which is calculated by adding an hour to the start time.
        const dateTimeEnd = addHours(dateTimeStart, 1);
        // Convert the Date object into human-readable strings.
        const appointmentTimeString = getLocaleTimeString(dateTimeStart);
        const appointmentDateString = getLocaleDateString(dateTimeStart);
        // Delete the context 'MakeAppointment-followup'; this is the final step of the path.
        agent.context.delete('makeappointment-followup');
        // The createCalendarEvent() function checks the availability of the time slot and marks the time slot on Google Calendar if the slot is available.
        return createCalendarEvent(dateTimeStart, dateTimeEnd).then(() => {
          agent.add(`Got it. I have your appointment scheduled on ${appointmentDateString} at ${appointmentTimeString}. See you soon. Good-bye.`);
        }).catch(() => {
          agent.add(`Sorry, something went wrong. I couldn't book the ${appointmentDateString} at ${appointmentTimeString}. Is there anything else I can help you with?`);
        });
      }
    
      // This function receives the date and time values from the context 'MakeAppointment-followup'
      // and calls the checkCalendarAvailablity() function to check the availability of the time slot on Google Calendar.
      function checkAppointment (agent) {
        // This variable needs to hold an instance of Date object that specifies the start time of the appointment.
        const dateTimeStart = convertTimestampToDate(agent.parameters.date, agent.parameters.time);
        // This variable holds the end time of the appointment, which is calculated by adding an hour to the start time.
        const dateTimeEnd = addHours(dateTimeStart, 1);
        // Convert the Date object into human-readable strings.
        const appointmentTimeString = getLocaleTimeString(dateTimeStart);
        const appointmentDateString = getLocaleDateString(dateTimeStart);
        // The checkCalendarAvailablity() function checks the availability of the time slot.
        return checkCalendarAvailablity(dateTimeStart, dateTimeEnd).then(() => {
          // The time slot is available.
          // The function returns a response that asks for the confirmation of the date and time.
          agent.add(`Okay, ${appointmentDateString} at ${appointmentTimeString}. Did I get that right?`);
        }).catch(() => {
          // The time slot is not available.
          agent.add(`Sorry, we're booked on ${appointmentDateString} at ${appointmentTimeString}. Is there anything else I can do for you?`);
          // Delete the context 'MakeAppointment-followup' to return the flow of conversation to the beginning.
          agent.context.delete('makeappointment-followup');
        });
      }
      // Mapping of the functions to the agent's intents.
      let intentMap = new Map();
      intentMap.set('Make Appointment', checkAppointment);
      intentMap.set('Make Appointment - yes', makeAppointment);
      agent.handleRequest(intentMap);
    });
    
    // This function checks for the availability of the time slot, which starts at 'dateTimeStart' and ends at 'dateTimeEnd'.
    // 'dateTimeStart' and 'dateTimeEnd' are instances of a Date object.
    function checkCalendarAvailablity (dateTimeStart, dateTimeEnd) {
      return new Promise((resolve, reject) => {
        calendar.events.list({
          auth: serviceAccountAuth, // List events for time period
          calendarId: calendarId,
          timeMin: dateTimeStart.toISOString(),
          timeMax: dateTimeEnd.toISOString()
        }, (err, calendarResponse) => {
          // Check if there is an event already on the Bike Shop Calendar
          if (err || calendarResponse.data.items.length > 0) {
            reject(err || new Error('Requested time conflicts with another appointment'));
          }else {
            resolve(calendarResponse);
          }
        });
      });
    }
    
    // This function marks the time slot on Google Calendar. The time slot on the calendar starts at 'dateTimeStart' and ends at 'dateTimeEnd'.
    // 'dateTimeStart' and 'dateTimeEnd' are instances of a Date object.
    function createCalendarEvent (dateTimeStart, dateTimeEnd) {
      return new Promise((resolve, reject) => {
        calendar.events.list({
          auth: serviceAccountAuth, // List events for time period
          calendarId: calendarId,
          timeMin: dateTimeStart.toISOString(),
          timeMax: dateTimeEnd.toISOString()
        }, (err, calendarResponse) => {
          // Check if there is an event already on the Bike Shop Calendar
          if (err || calendarResponse.data.items.length > 0) {
            reject(err || new Error('Requested time conflicts with another appointment'));
          } else {
            // Create event for the requested time period
            calendar.events.insert({ auth: serviceAccountAuth,
              calendarId: calendarId,
              resource: {summary: 'Bike Appointment',
                start: {dateTime: dateTimeStart},
                end: {dateTime: dateTimeEnd}}
            }, (err, event) => {
              err ? reject(err) : resolve(event);
            }
            );
          }
        });
      });
    }
    
    // A helper function that receives Dialogflow's 'date' and 'time' parameters and creates a Date instance.
    function convertTimestampToDate(date, time){
      // Parse the date, time, and time zone offset values from the input parameters and create a new Date object
      return new Date(Date.parse(date.split('T')[0] + 'T' + time.split('T')[1].split('-')[0] + '-' + time.split('T')[1].split('-')[1]));
    }
    
    // A helper function that adds the integer value of 'hoursToAdd' to the Date instance 'dateObj' and returns a new Data instance.
    function addHours(dateObj, hoursToAdd){
      return new Date(new Date(dateObj).setHours(dateObj.getHours() + hoursToAdd));
    }
    
    // A helper function that converts the Date instance 'dateObj' into a string that represents this time in English.
    function getLocaleTimeString(dateObj){
      return dateObj.toLocaleTimeString('en-US', { hour: 'numeric', hour12: true, timeZone: timeZone });
    }
    
    // A helper function that converts the Date instance 'dateObj' into a string that represents this date in English.
    function getLocaleDateString(dateObj){
      return dateObj.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', timeZone: timeZone });
    }
    
  6. In the index.js tab of the inline editor, replace the following lines with the previously saved lines (from Step 4) that contain your Google Calendar credentials:

    // Enter your calendar ID and service account JSON below.
    // See https://github.com/dialogflow/bike-shop/blob/master/README.md#calendar-setup
    const calendarId = '<INSERT CALENDAR ID HERE>'; // Example: 6ujc6j6rgfk02cp02vg6h38cs0@group.calendar.google.com
    const serviceAccount = {}; // The JSON object looks like: { "type": "service_account", ... }
    
  7. Click DEPLOY.

  8. Test the update using the simulator in the Dialogflow console.

How the code works

In the code, notice that the Make Appointment intent is now mapped to the new function called checkAppointment() and the Make Appointment - yes intent is mapped to the makeAppointment() function:

  intentMap.set('Make Appointment', checkAppointment);
  intentMap.set('Make Appointment - yes', makeAppointment);

When a user tells the time and date of an appointment to the Make Appointment intent, the following events take place in the fulfillment code:

  • The Make Appointment intent calls the checkAppointment() function and passes the data and time parameters to this function.
  • The checkAppointment() function extracts the date and time parameters.
  • The checkAppointment() function calls the checkCalendarAvailablity() function to check if the appointment time slot is available on the calendar.
  • If the time slot is available, the function prompts the user to confirm the date and time. (In the Create a suggestion step section, we discuss the scenario where the time slot is not available.)
  • The Make Appointment - yes and Make Appointment - no intents participate in matching.
    • If the user's answer is no, the Make Appointment - no intent cancels the scheduling process.
    • If the answer is yes, the makeAppointment() function, which is mapped to the Make Appointment - yes intent, calls the createCalendarEvent() function to schedule an appointment using the Google Calendar API.

The happy path for scheduling an appointment is now augmented with an extra step that confirms the time and date information from users. This extra step makes a big difference in preventing the occurrence of mis-scheduled appointments when the agent is deployed in the real world.

Next, let's examine the scenario where a user selects a time slot that's not available.