Creazione del fulfillment mediante webhook

In Dialogflow, il fulfillment è un servizio, un'app, un feed, una conversazione o un'altra logica che può risolvere una richiesta utente. Nel nostro caso, è necessario completare un appuntamento per il negozio di biciclette, dato l'orario e la data forniti dall'intent Prendi appuntamento.

Per questa configurazione, forniamo un webhook come servizio di backend che può ricevere i parametri di data e ora dall'intent e creare un evento su Google Calendar utilizzando l'API. Per farlo, dobbiamo eseguire due operazioni:

  • Ottenere le credenziali per l'API Google Calendar.
  • Creare un nuovo calendario e configurare il codice nel webhook.

Creare un webhook con l'editor in linea

Nella console è presente un editor incorporato che Dialogflow consente di scrivere direttamente il codice NodeJS, che può essere eseguito per l'esecuzione come webhook su Firebase.

Per creare un webhook utilizzando l'editor in linea di Dialogflow, segui questi passaggi:

  1. Fai clic sull'intent Fissa un appuntamento.
  2. Nella sezione Fulfillment, attiva il pulsante Attiva la chiamata webhook per questo intent.
  3. Fai clic su SALVA.
  4. Fai clic sulla scheda Fulfillment sulla barra di navigazione per andare alla pagina di fulfillment.
  5. Imposta il pulsante dell'editor in linea su ABILITATO.
  6. Elimina i contenuti esistenti nella scheda package.json dell'editor incorporato.
  7. Copia i contenuti JSON indicati di seguito nella scheda package.json dell'editor incorporato:

    {
      "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. Elimina il codice esistente nella scheda index.js dell'editor incorporato.

  9. Copia il codice qui sotto e incollalo nella scheda index.js dell'editor incorporato:

    /**
     * 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. Fai clic su DEPLOYMENT.

    Figura 7. Diagramma di flusso che mostra la connessione alla funzione webhook makeAppointment().

In questo modo, l'intent Prendi appuntamento è connesso alla funzione makeAppointment() nel webhook. Esaminiamo la seguente porzione di codice nel 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);
});

Nel codice riportato sopra, osserva la riga:

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

La funzione set() viene chiamata su un oggetto Map, intentMap. Questa funzione collega un intent a una funzione specifica del codice. In questo caso, la chiamata stabilisce la mappatura tra l'intent Prendi appuntamento e la funzione makeAppointment().

La funzione makeAppointment(agent){} legge i valori parametro data e ora dall'oggetto di input agent tramite agent.parameters.date e agent.parameters.time. Dopo aver analizzato e formattato i valori di data e ora, la funzione chiama una funzione personalizzata createCalendarEvent() che effettua una chiamata API a Google Calendar per creare un evento nel calendario.

Infine, la funzione agent.add() viene utilizzata per fornire una stringa personalizzata come risposta all'utente. A differenza dell'utilizzo della console di Dialogflow per fornire risposte, con un webhook, possiamo utilizzare la logica del codice per creare risposte altamente dinamiche. Ad esempio, quando l'agente pianifica un appuntamento, risponde con la seguente risposta:

OK. Ho il tuo appuntamento fissato per il giorno ${appointmentDateString} alle ${appointmentTimeString}. A presto! Arrivederci.

Tuttavia, se l'agente non riesce a fissare un appuntamento nella data e nell'ora specificate, restituisce la seguente risposta:

Ci dispiace, abbiamo prenotato il giorno ${appointmentDateString} alle ore ${appointmentTimeString}. C'è qualcos'altro che posso fare per te?

A questo punto non possiamo testare correttamente il codice perché non ha accesso all'API di Google Calendar. Tieni presente che nel codice per personalizzare la configurazione dell'API Google Calendar sono presenti le seguenti variabili:

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

Il prossimo passaggio consiste nel configurare il codice per accedere all'API Google Calendar.

Ottenere le credenziali per l'API Google Calendar

Per ottenere le credenziali per l'API Google Calendar:

  1. Nella barra di navigazione di Dialogflow, fai clic sul pulsante impostazioni ⚙ accanto al nome dell'agente.
  2. Nella tabella PROGETTO GOOGLE, fai clic sul link ID progetto per aprire la console di Google Cloud Platform.
  3. Nella console di Google Cloud Platform, fai clic sul pulsante del menu ☰ e seleziona API e servizi & Libreria.
  4. Fai clic sulla scheda API Google Calendar.

  5. Fai clic su ABILITA nella pagina dell'API Google Calendar.

  6. Nella barra di navigazione, fai clic su Credenziali.

  7. Fai clic su Crea credenziali e seleziona la voce Chiave account di servizio dal menu a discesa.

  8. Fai clic sul menu a discesa Account di servizio e seleziona la voce Nuovo account di servizio.

  9. Nel campo Nome account di servizio, inserisci bike-shop-calendar.

  10. Nel campo Role (Ruolo), seleziona Project > Owner (Proprietario del progetto) dal menu a discesa.

  11. Fai clic su Crea. Verrà scaricato un file JSON contenente la chiave del tuo account di servizio.

Creare un nuovo calendario e configurare il codice nel webhook

Successivamente, dobbiamo creare un nuovo calendario in cui il negozio di biciclette può tenere traccia degli appuntamenti. Utilizziamo le informazioni del file JSON scaricato per integrare il codice webhook dell'agente con il nuovo calendario Google. Accertati di poter visualizzare e copiare i contenuti del file JSON prima di continuare con l'insieme di istruzioni successivo.

Per creare un nuovo calendario e completare l'integrazione:

  1. Apri Google Calendar.
  2. Nella barra di navigazione di Google Calendar, fai clic sul pulsante + accanto al campo di immissione Aggiungi il calendario di un amico.
  3. Seleziona la voce Nuovo calendario dal menu a discesa.
  4. Nel campo Nome, inserisci Mary's Bike Shop.
  5. Fai clic sul link CREA CALENDARIO. Il nuovo calendario Mary's Bike Shop viene creato nella finestra Impostazioni per i miei calendari della barra di navigazione.
  6. Fai clic su Mary's Bike Shop e seleziona Condividi con persone specifiche.
  7. Nella sezione Condividi con persone specifiche, fai clic su AGGIUNGI PERSONE. Sullo schermo viene visualizzata la finestra popup Condividi con persone specifiche.
  8. Apri il file JSON scaricato in precedenza e copia l'indirizzo email nel campo client_email, senza le virgolette:

    {
      "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. Nella finestra popup Condividi con persone specifiche, incolla l'indirizzo email da client_email nel campo di immissione Aggiungi email o nome.

  10. Nel menu a discesa Autorizzazioni, seleziona l'elemento Apportare modifiche agli eventi.

  11. Fai clic su INVIA.

  12. Scorri verso il basso nella sezione Integra calendario e copia i contenuti di ID calendario.

  13. Nella console di Dialogflow, vai alla pagina Fulfillment del tuo agente.

  14. Nella scheda index.js dell'editor incorporato, individua la variabile calendarId.

    const calendarId = '<INSERT CALENDAR ID HERE>';
    
  15. Incolla i contenuti di Calendar ID (ID calendario) per sostituire <INSERT CALENDAR ID HERE>, come mostrato nel seguente codice:

    const calendarId = 'fh5kgikn3t4vvmc73423875rjc@group.calendar.google.com';
    
  16. Nella scheda index.js dell'editor incorporato, individua la variabile serviceAccount.

    const serviceAccount = {};
    
  17. Rivedi il file JSON scaricato in precedenza e copia l'intero contenuto, incluse le parentesi graffe esterne ({}).

  18. Incolla i contenuti per sostituire il valore di serviceAccount, come illustrato nel seguente snippet di codice:

    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. Fai clic su DEPLOYMENT.

  20. Testa l'agente per verificare che i nuovi eventi siano stati creati correttamente nel calendario.

Ulteriori miglioramenti

Attualmente, il tuo agente può fornire servizi solo quando gli utenti collaborano bene; non gestisce bene le conversazioni se gli utenti dicono cose che non rientrano nelle finestre di dialogo basate su script. Nelle interfacce di conversazione, una parte importante dell'impegno consiste nel rendere un agente in grado di recuperare le frasi impreviste degli utenti. Ti consigliamo di esplorare la potente funzionalità di Dialogflow, Contesto, che consente a un agente di controllare in modo efficace il flusso della conversazione.