在 Dialogflow 中,fulfillment 是指可以解决用户请求的服务、应用、Feed、会话或其他逻辑。在本例中,我们需要使用 fulfillment 根据意图提供的时间和日期信息为自行车店安排预约。
对于此设置,我们提供网络钩子作为后端服务,可从意图接收时间和日期参数,并使用 API 在 Google 日历上创建活动。为实现此目的,我们需要执行两项任务:
- 获取用于 Google Calendar API 的凭据。
- 创建一个新日历并在网络钩子中配置代码。
使用内嵌编辑器创建网络钩子
Dialogflow 在控制台中有一个内嵌编辑器,使您可以直接写入 NodeJS 代码,然后将其部署作为网络钩子在 Firebase 上运行。
如需使用 Dialogflow 的内嵌编辑器创建网络钩子,请按以下步骤操作:
- 点击“Make Appointment”意图。
- 在 Fulfillment 部分,将为此意图启用网络钩子调用 (Enable Webhook call for this intent) 按钮切换为开启。
- 点击保存 (SAVE)。
- 点击导航栏上的 Fulfillment 标签页,转到“Fulfillment”页面。
- 将内嵌编辑器的按钮切换为“已启用”(ENABLED)。
- 删除内嵌编辑器的
package.json
标签页中的现有内容。 将以下 JSON 内容复制并粘贴到内嵌编辑器的
package.json
标签页中:{ "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" } }
删除内嵌编辑器的
index.js
标签页中的现有代码。将以下代码复制并粘贴到内嵌编辑器的
index.js
页标签中:/** * 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 }); }
点击部署 (DEPLOY)。
图 7.显示与网络钩子函数
makeAppointment()
的连接的流程图。
现在,我们已将意图“Make Appointment”连接到网络钩子中的函数 makeAppointment()
。让我们探讨一下网络钩子中的以下代码:
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);
});
在上述代码中,请注意以下行:
intentMap.set('Make Appointment', makeAppointment);
函数 set()
在 Map
对象 intentMap
上调用。此函数将意图与代码中的特定函数相关联。在这种情况下,调用会建立 intent “Make Appointment”与函数 makeAppointment()
之间的映射关系。
函数 makeAppointment(agent){}
通过 agent.parameters.date
和 agent.parameters.time
从输入对象 agent
中读取日期和时间参数值。函数解析并格式化日期和时间值后,调用自定义函数 createCalendarEvent()
,该自定义函数向 Google 日历发出 API 调用以在日历上创建事件。
最后,函数 agent.add()
用于传送自定义字符串作为对用户的响应。与使用 Dialogflow 控制台提供响应不同,在使用网络钩子的情况下,我们可以使用代码逻辑构建高度动态的响应。例如,当代理成功安排预约时,它会回复以下响应:
Got it. I have your appointment scheduled on ${appointmentDateString} at ${appointmentTimeString}. See you soon. Good-bye.(搞定了。我为您安排了 ${appointmentDateString} 在 ${appointmentTimeString} 的预约。再见!)
但是,如果代理未能安排指定时间和日期的预约,则会返回以下响应:
Sorry, we're booked on ${appointmentDateString} at ${appointmentTimeString}. Is there anything else I can do for you?(抱歉,${appointmentDateString}的${appointmentTimeString} 时间段已经被别人预订了。还有其他事可以为您效劳吗?)
此时,我们无法适当地测试代码,因为它无法访问 Google Calendar API。请注意,代码中包含以下变量来个性化 Google Calendar API 设置:
const calendarId = '<INSERT CALENDAR ID HERE>'; // Example: 6ujc6j6rgfk02cp02vg6h38cs0@group.calendar.google.com
const serviceAccount = {}; // The JSON object looks like: { "type": "service_account", ... }
接下来,我们需要配置代码以访问 Google Calendar API。
获取用于 Google Calendar API 的凭据
如需获取用于 Google Calendar API 的凭据,请按以下步骤操作:
- 在 Dialogflow 的导航栏上,点击代理名称旁边的设置 ⚙ 按钮。
- 在 GOOGLE 项目表格中,点击项目 ID 链接以打开 Google Cloud Platform Console。
- 在 Google Cloud Platform Console 中,点击菜单按钮 ☰ 并选择 API 和服务 > 库。
点击 Google Calendar API 卡片。
点击 Google Calendar API 页面上的启用。
在导航栏中,点击凭据。
点击创建凭据,然后从下拉菜单中选择服务帐号密钥项。
点击服务帐号下拉菜单,然后选择新的服务帐号项。
在服务帐号名称输入字段中,输入
bike-shop-calendar
。对于角色字段,从下拉菜单中选择项目 > Owner。
点击创建。(包含您的服务帐号密钥的 JSON 文件即会下载。)
创建一个新日历并配置网络钩子中的代码
接下来,我们需要为自行车店创建一个新日历来跟踪预约情况。我们使用下载的 JSON 文件中的信息,将代理的网络钩子代码与新的 Google 日历集成。在继续执行下一组说明中的操作之前,请确保您可以查看和复制 JSON 文件的内容。
如需创建新日历并完成集成,请按以下步骤执行操作:
- 打开 Google 日历。
- 在 Google 日历的导航栏上,点击添加朋友的日历输入字段旁边的 + 按钮。
- 从下拉菜单中选择新建日历项。
- 在名称字段中,输入
Mary's Bike Shop
。 - 点击创建日历链接。(新日历
Mary's Bike Shop
即会在导航栏的我的日历设置窗口下创建。) - 点击 Mary's Bike Shop,然后选择与特定的人共享。
- 在与特定的人共享部分,点击添加共享对象。(屏幕上会显示与特定的人共享弹出式窗口。)
打开之前下载的 JSON 文件,并复制
client_email
字段中的电子邮件地址(不带引号):{ "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..." }
在与特定的人共享弹出窗口中,在添加电子邮件地址或姓名输入字段中粘贴来自
client_email
的电子邮件地址。在权限下拉菜单中,选择更改活动项。
点击发送。
向下滚动集成日历部分,然后复制日历 ID 的内容。
在 Dialogflow 控制台中,转到代理的 Fulfillment 页。
在内联编辑器的
index.js
标签页中,找到变量calendarId
。const calendarId = '<INSERT CALENDAR ID HERE>';
粘贴日历 ID 中的内容来替换
<INSERT CALENDAR ID HERE>
,如下面的代码所示:const calendarId = 'fh5kgikn3t4vvmc73423875rjc@group.calendar.google.com';
在内联编辑器的
index.js
标签页中,找到变量serviceAccount
。const serviceAccount = {};
再次访问之前下载的 JSON 文件并复制全部内容,包括最外层的花括号 (
{}
)。粘贴内容以替换
serviceAccount
的值,如以下代码段所示: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..." };
点击部署 (DEPLOY)。
测试代理以验证是否在日历上正确创建了新事件。
进一步改进
目前,只有当用户有效配合时,代理才能提供服务;如果用户说的内容超出了脚本对话,则代理无法良好地应对会话。在对话界面中,要做的很大一部分工作是让代理能够在遇到意外的用户话语时能够恢复。我们建议您了解 Dialogflow 功能强大的上下文功能,使代理能够有效地掌控会话流程。