Créer un processus "fulfillment" à l'aide d'un webhook

Dans Dialogflow, le fulfillment est un service, une application, un flux, une conversation ou toute autre logique permettant de résoudre une requête utilisateur. Dans ce cas, nous avons besoin d'un fulfillment capable de programmer un rendez-vous pour le magasin de vélos, en fonction de l'heure et de la date fournies par l'intent Prendre rendez-vous.

Pour cette configuration, nous proposons un webhook en tant que service de backend capable de recevoir les paramètres d'heure et de date issus de l'intent, et de créer un événement dans Google Agenda à l'aide de l'API. Pour ce faire, ces deux tâches doivent être effectuées :

  • Obtenir les identifiants de l'API Google Agenda
  • Créer un agenda et configurer le code dans le webhook

Créer un webhook avec l'éditeur intégré

Dialogflow dispose d'un éditeur intégré dans la console vous permettant d'écrire directement le code NodeJS, qui peut ensuite être déployé en tant que webhook sur Firebase.

Pour créer un webhook à l'aide de l'éditeur intégré de Dialogflow, procédez comme suit :

  1. Cliquez sur l'intent Prendre rendez-vous.
  2. Dans la section Fulfillment, activez le bouton Enable webhook call for this intent (Activer l'appel webhook pour cet intent).
  3. Cliquez sur ENREGISTRER.
  4. Cliquez sur l'onglet Fulfillment dans la barre de navigation pour accéder à la page de fulfillment.
  5. Réglez le bouton de l'éditeur intégré sur ENABLED (ACTIVÉ).
  6. Supprimez le contenu existant dans l'onglet package.json de l'éditeur intégré.
  7. Copiez et collez le contenu JSON ci-dessous dans l'onglet package.json de l'éditeur intégré :

    {
      "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. Supprimez le code existant dans l'onglet index.js de l'éditeur intégré.

  9. Copiez et collez le code ci-dessous dans l'onglet index.js de l'éditeur intégré :

    /**
     * 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. Cliquez sur DÉPLOYER.

    Figure 7. Organigramme illustrant la connexion à la fonction webhook makeAppointment().

L'intent Prendre rendez-vous est désormais associé à la fonction makeAppointment() du webhook. Examinons l'extrait de code suivant dans le 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);
});

Dans le code ci-dessus, analysez la ligne suivante :

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

La fonction set() est appelée sur un objet Map, intentMap. Cette fonction associe un intent à une fonction spécifique du code. Dans ce cas, l'appel établit le mappage entre l'intent Prendre rendez-vous et la fonction makeAppointment().

La fonction makeAppointment(agent){} lit les valeurs des paramètres de date et d'heure à partir de l'objet d'entrée agent, via agent.parameters.date et agent.parameters.time. Après l'analyse et la mise en forme des valeurs de date et d'heure, la fonction appelle une fonction personnalisée createCalendarEvent(), qui effectue un appel d'API à Google Agenda pour créer un événement dans l'agenda.

Enfin, la fonction agent.add() permet de fournir une chaîne personnalisée en réponse à l'utilisateur. Contrairement à la console Dialogflow, le webhook peut créer des réponses très dynamiques à l'aide de la logique du code. Par exemple, lorsque l'agent réussit à planifier un rendez-vous, il renvoie la réponse suivante :

D'accord. Votre rendez-vous est prévu pour le ${appointmentDateString} à ${appointmentTimeString}. À bientôt. Au revoir.

Toutefois, si l'agent ne parvient pas à fixer un rendez-vous à la date et à l'heure spécifiées, il renvoie la réponse suivante :

Désolé, nous n'avons pas de créneau disponible le ${appointmentDateString} à ${appointmentTimeString}. Y a-t-il autre chose que je puisse faire pour vous ?

À ce stade, nous ne pouvons pas tester le code correctement, car celui-ci n'a pas accès à l'API Google Agenda. Notez que les variables suivantes dans le code permettent de personnaliser la configuration de l'API Google Agenda :

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

Il faut ensuite configurer le code pour accéder à l'API Google Agenda.

Obtenir les identifiants pour l'API Google Agenda

Pour obtenir les identifiants de l'API Google Agenda, procédez comme suit :

  1. Dans la barre de navigation de Dialogflow, cliquez sur le bouton des paramètres (⚙) situé à côté du nom de l'agent.
  2. Dans la table GOOGLE PROJECT (PROJET GOOGLE), cliquez sur le lien "Project ID" (ID de projet) pour ouvrir la console Google Cloud Platform.
  3. Dans la console Google Cloud Platform, cliquez sur le bouton de menu (☰), puis sélectionnez API et services > Bibliothèque.
  4. Cliquez sur la fiche API Google Agenda.

  5. Cliquez sur ENABLE (ACTIVER) sur la page de l'API Google Agenda.

  6. Dans la barre de navigation, cliquez sur Credentials (Identifiants).

  7. Cliquez sur Create credentials (Créer des identifiants), puis sélectionnez l'élément Service account key (Clé de compte de service) dans le menu déroulant.

  8. Cliquez sur le menu déroulant Service account (Compte de service) et sélectionnez l'élément New service account (Nouveau compte de service).

  9. Dans le champ de saisie Service account name (Nom du compte de service), saisissez bike-shop-calendar.

  10. Pour le champ Role (Rôle), sélectionnez Project > Owner (Projet > Propriétaire) dans le menu déroulant.

  11. Cliquez sur Create (Créer). (Un fichier JSON contenant la clé de votre compte de service est téléchargé).

Créer un agenda et configurer le code dans le webhook

Nous devons ensuite créer un agenda pour que le magasin de vélos puisse effectuer le suivi des rendez-vous. Nous nous servons des informations contenues dans le fichier JSON précédemment téléchargé pour intégrer le code webhook de l'agent au nouvel agenda Google. Assurez-vous que vous pouvez afficher et copier le contenu du fichier JSON avant de passer aux instructions suivantes.

Pour créer un agenda et terminer l'intégration, procédez comme suit :

  1. Ouvrez Google Agenda.
  2. Dans la barre de navigation de Google Agenda, cliquez sur le bouton + à côté du champ de saisie Add a friend's calendar (Ajouter l'agenda d'un ami).
  3. Sélectionnez l'élément Nouvel agenda dans le menu déroulant.
  4. Dans le champ Nom, saisissez Mary's Bike Shop.
  5. Cliquez sur le lien CRÉER UN AGENDA. (Le nouvel agenda Mary's Bike Shop est créé dans la fenêtre Paramètres de mes agendas de la barre de navigation).
  6. Cliquez sur Mary's Bike Shop (Magasin de vélos de Mary), puis sélectionnez Partager avec des personnes en particulier.
  7. Dans la section Share with specific people (Partager avec des personnes en particulier), cliquez sur ADD PEOPLE (AJOUTER DES CONTACTS). La fenêtre pop-up Share with specific people (Partager avec des personnes en particulier) s'affiche.
  8. Ouvrez le fichier JSON précédemment téléchargé et copiez l'adresse e-mail dans le champ client_email, sans les guillemets :

    {
      "type": "service_account",
      "project_id": "marysbikeshop-...",
      "private_key_id": "...",
      "private_key": "...",
      "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. Dans la fenêtre pop-up Share with specific people (Partager avec des personnes en particulier), collez l'adresse e-mail de client_email dans le champ de saisie Add email or name (Ajouter une adresse e-mail ou un nom).

  10. Dans le menu déroulant Autorisations, sélectionnez l'élément Apporter des modifications aux événements.

  11. Cliquez sur SEND (ENVOYER).

  12. Faites défiler la page jusqu'à la section Integrate calendar (Intégrer l'agenda), puis copiez le contenu de Calendar ID (ID de l'agenda).

  13. Dans la console Dialogflow, accédez à la page Fulfillment de votre agent.

  14. Dans l'onglet index.js de l'éditeur intégré, trouvez la variable calendarId.

    const calendarId = '<INSERT CALENDAR ID HERE>';
    
  15. Remplacez <INSERT CALENDAR ID HERE> par le contenu de Calendar ID (ID de l'agenda), comme illustré dans le code suivant :

    const calendarId = 'fh5kgikn3t4vvmc73423875rjc@group.calendar.google.com';
    
  16. Dans l'onglet index.js de l'éditeur intégré, trouvez la variable serviceAccount.

    const serviceAccount = {};
    
  17. Consultez à nouveau le fichier JSON précédemment téléchargé et copiez l'intégralité de son contenu, y compris les accolades les plus externes ({}).

  18. Remplacez la valeur de serviceAccount par le contenu que vous avez copié à l'étape précédente, comme illustré dans l'extrait de code suivant :

    const serviceAccount = {
      "type": "service_account",
      "project_id": "marysbikeshop-...",
      "private_key_id": "...",
      "private_key": "...",
      "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. Cliquez sur DÉPLOYER.

  20. Testez l'agent pour vérifier que les nouveaux événements ont bien été créés dans l'agenda.

Autres améliorations

Pour le moment, votre agent ne peut fournir des services que lorsque les utilisateurs coopèrent. Il gère difficilement les conversations lorsque les utilisateurs ne suivent pas les dialogues scénarisés. Dans les interfaces de conversation, la partie la plus difficile consiste à créer un agent capable de récupérer des énoncés d'utilisateurs inattendus. Nous vous recommandons d'explorer Contextes, une fonctionnalité puissante de Dialogflow qui permet à un agent de contrôler le flux d'une conversation de manière efficace.