Create fulfillment using webhook

In Dialogflow, fulfillment is a service, app, feed, conversation, or other logic that can resolve a user request. In our case, we need fulfillment that can schedule an appointment for the bike shop given the time and date information provided by the intent Make Appointment.

For this setup, we provide a webhook as a backend service that can receive the time and date parameters from the intent and create an event on Google Calendar via API. To accomplish this, we need to perform two tasks:

  • Obtain credentials for Google Calendar API.
  • Create a new calendar and configure the code in the webhook.

Create a webhook with the inline editor

Dialogflow has an inline editor in the console that allows you to directly write NodeJS code, which then can be deployed to run as a webhook on Firebase.

To create a webhook using Dialogflow's inline editor, follow these steps:

  1. Click on the Make Appointment intent.
  2. In the Fulfillment section, toggle on the Enable Webhook call for this intent button.
  3. Click SAVE.
  4. Click the Fulfillment tab on the navigation bar to go to the fulfillment page.
  5. Toggle the inline editor's button to ENABLED.
  6. Delete the existing content in the package.json tab of the inline editor.
  7. Copy and paste the JSON 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.5.0"
      }
    }
    
  8. Delete the existing code in the index.js tab of the inline editor.

  9. 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.
    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
    const timeZoneOffset = '-07:00';         // Change it to your time zone offset
    
    exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
      const agent = new WebhookClient({ request, response });
    
      function makeAppointment (agent) {
        // Use the Dialogflow's date and time parameters to create Javascript Date instances, 'dateTimeStart' and 'dateTimeEnd',
        // which are used to specify the appointment's time.
        const appointmentDuration = 1;// Define the length of the appointment to be one hour.
        const dateTimeStart = convertParametersDate(agent.parameters.date, agent.parameters.time);
        const dateTimeEnd = addHours(dateTimeStart, appointmentDuration);
        const appointmentTimeString = getLocaleTimeString(dateTimeStart);
        const appointmentDateString = getLocaleDateString(dateTimeStart);
        // Check the availability of the time slot and set up an appointment if the time slot is available on the calendar
        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, we're booked on ${appointmentDateString} at ${appointmentTimeString}. Is there anything else I can do for you?`);
        });
      }
      let intentMap = new Map();
      intentMap.set('Make Appointment', makeAppointment);  // It maps the intent 'Make Appointment' to the function 'makeAppointment()'
      agent.handleRequest(intentMap);
    });
    
    function createCalendarEvent (dateTimeStart, dateTimeEnd) {
      return new Promise((resolve, reject) => {
        calendar.events.list({  // List all events in the specified time period
          auth: serviceAccountAuth,
          calendarId: calendarId,
          timeMin: dateTimeStart.toISOString(),
          timeMax: dateTimeEnd.toISOString()
        }, (err, calendarResponse) => {
          // Check if there exists any event on the calendar given the specified the time period
          if (err || calendarResponse.data.items.length > 0) {
            reject(err || new Error('Requested time conflicts with another appointment'));
          } else {
            // Create an 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 convertParametersDate(date, time){
      return new Date(Date.parse(date.split('T')[0] + 'T' + time.split('T')[1].split('-')[0] + timeZoneOffset));
    }
    
    // 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 });
    }
    
  10. Click DEPLOY.

    Figure 7. Flowchart showing the connection to the webhook function makeAppointment().

We now have the intent Make Appointment connected to the function makeAppointment() in the webhook. Let's examine the following piece of code in the webhook:

exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
  const agent = new WebhookClient({ request, response });

  function makeAppointment (agent) {
    // Calculate appointment start and end datetimes (end = +1hr from start)
    const dateTimeStart = convertParametersDate(agent.parameters.date, agent.parameters.time);
    const dateTimeEnd = addHours(dateTimeStart, 1);
    const appointmentTimeString = getLocaleTimeString(dateTimeStart);
    const appointmentDateString = getLocaleDateString(dateTimeStart);
    // Check the availability of the time, and make an appointment if there is time on the calendar
    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, we're booked on ${appointmentDateString} at ${appointmentTimeString}. Is there anything else I can do for you?`);
    });
  }
  let intentMap = new Map();
  intentMap.set('Make Appointment', makeAppointment);
  agent.handleRequest(intentMap);
});

In the code above, notice the line,

intentMap.set('Make Appointment', makeAppointment);

The function set()is called on a Map object, intentMap. This function links an intent to a specific function in the code. In this case, the call establishes the mapping between the intent Make Appointment and the function makeAppointment().

The function makeAppointment(agent){} reads the date and time parameter values from the input object agent via agent.parameters.date and agent.parameters.time. After parsing and formatting the date and time values, the function then calls a custom function createCalendarEvent(), which makes an API call to Google Calendar to create an event on the calendar.

Lastly, the function agent.add()is used to deliver a customized string as a response to the user. Unlike using the Dialogflow console to provide responses, with a webhook, we can use the logic of code to construct highly dynamic responses. For instance, when the agent schedules an appointment successfully, it replies with the following response:

Got it. I have your appointment scheduled on ${appointmentDateString} at ${appointmentTimeString}. See you soon. Good-bye.

However, if the agent fails to make an appointment on the specific time and date provided, it returns the following response:

Sorry, we're booked on ${appointmentDateString} at ${appointmentTimeString}. Is there anything else I can do for you?

At this point, we cannot test the code properly because it does not have access to the Google Calendar API. Notice that we have the following variables in the code for personalizing Google Calendar API setup:

const calendarId = '<INSERT CALENDAR ID HERE>'; // Example: 6ujc6j6rgfk02cp02vg6h38cs0@group.calendar.google.com
const serviceAccount = {}; // The JSON object looks like: { "type": "service_account", ... }

Next, we need to configure the code to access the Google Calendar API.

Obtain credentials for the Google Calendar API

To obtain credentials for the Google Calendar API, follow these steps:

  1. On Dialogflow's navigation bar, click the settings ⚙ button next to the name of the agent.
  2. In the GOOGLE PROJECT table, click the Project ID link to open the Google Cloud Platform console.
  3. In the Google Cloud Platform console, click the menu button ☰ and select APIs & Services > Library.
  4. Click on the Google Calendar API card.

  5. Click ENABLE on the Google Calendar API page.

  6. On the navigation bar, click Credentials.

  7. Click Create credentials and select the Service account key item from the dropdown menu.

  8. Click the Service account dropdown menu and select the New service account item.

  9. In the Service account name input field, enter bike-shop-calendar.

  10. For the Role field, select Project > Owner from the dropdown menu.

  11. Click Create. (A JSON file that contains your service account key is downloaded.)

Create a new calendar and configure the code in the webhook

Next, we need to create a new calendar for the bike shop to track appointments. We use the information from the downloaded JSON file to integrate the agent's webhook code with the new Google calendar. Make sure that you can view and copy the content of the JSON file before you continue with the next set of instructions.

To create a new calendar and complete the integration, follow these steps:

  1. Open your Google Calendar.
  2. On the Google Calendar's navigation bar, click the + button next to the Add a friend's calendar input field.
  3. Select the New calendar item from the dropdown menu.
  4. In the Name field, enter Mary's Bike Shop.
  5. Click the CREATE CALENDAR link. (The new calendar Mary's Bike Shop is created under the Settings for my calendars window of the navigation bar.)
  6. Click Mary's Bike Shop and select Share with specific people.
  7. In the Share with specific people section, click ADD PEOPLE. (The Share with specific people pop-up window appears on the screen.)
  8. Open the previously downloaded JSON file and copy the email address in the client_email field, without the quotation marks:

    {
      "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..."
    }
    
  9. In the Share with specific people pop-up window, paste the email address from client_email in the Add email or name input field.

  10. In the Permissions dropdown menu, select the Make changes to events item.

  11. Click SEND.

  12. Scroll down the Integrate calendar section and copy the content of Calendar ID.

  13. In the Dialogflow console, go to your agent's Fulfillment page.

  14. In the index.js tab of the inline editor, find the variable calendarId.

    const calendarId = '<INSERT CALENDAR ID HERE>';
    
  15. Paste the content of Calendar ID to replace <INSERT CALENDAR ID HERE>, as shown in the following code:

    const calendarId = 'fh5kgikn3t4vvmc73423875rjc@group.calendar.google.com';
    
  16. In the index.js tab of the inline editor, find the variable serviceAccount.

    const serviceAccount = {};
    
  17. Revisit the previously downloaded JSON file and copy the entire content, including the outermost curly brackets ({}).

  18. Paste the content to replace the value of serviceAccount, as shown in the following code snippet:

    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..."
    };
    
  19. Click DEPLOY.

  20. Test the agent to verify that new events are correctly created on the calendar.

What's next?

Currently, our agent can provide services only when users cooperate nicely; it doesn't handle conversations well if users say things that are outside of the scripted dialogs. In conversational interfaces, a big part of the effort goes into making an agent capable of recovering from unexpected user utterances. Hence, for the next step, we recommend you explore Dialogflow's powerful feature, Contexts, that allows an agent to effectively control the flow of conversation.