会话验证调度器

Lego Party is my side hustle — Lego-themed entertainment like parties, classes, boozy Lego building, therapy, you name it! My last article took this business into the 21st Century (and let me stop taking calls during my day job) when we created an online class calendar with Acuity Scheduling.

乐高派对是我忙碌的一面-以乐高为主题的娱乐活动,如聚会,课堂,豪华的乐高积木,疗法,随便你说! 我的上一篇文章将这项业务带入了21世纪(并让我在日常工作中不再接听电话),当时我们使用Acuity Scheduling创建了在线课堂日历。

Folks find businesses online. That's hardly news, but more and more it isn't enough to just be online. Businesses need to meet the customer on their own turf. Today Lego Party is making that next step, and bringing online scheduling directly to people on Facebook using Acuity Scheduling's appointment scheduling API. We'll also need a conversational AI platform such as Init.ai, Wit.ai and api.ai to name a few. For this project, I've selected Init.ai's conversational API because of their nice SDK and straight forward Facebook Messenger connection.

人们在网上找到商家。 这不是什么新闻,但是越来越多的信息不足以仅仅在线。 企业需要在自己的地盘上与客户见面。 今天,乐高派对正在迈出下一步,并使用Acuity Scheduling的约会计划API将在线计划直接带给Facebook上的人们。 我们还需要一个对话式AI平台,例如Init.ai , Wit.ai和api.ai。 对于这个项目,我选择Init.ai的会话API是因为它们具有不错的SDK和直接的Facebook Messenger连接。

总览 ( Overview )

We'll be building a conversational chat bot for Lego Party's Facebook page to handle the two most common scheduling tasks for clients:

我们将为Lego Party的Facebook页面构建一个会话聊天机器人,以处理针对客户的两个最常见的调度任务:

  1. Booking new classes预订新班
  2. Checking their booked classes检查他们预定的课程

We'll need both an Acuity Scheduling account and an Init.ai account (no credit cards required!). Acuity Scheduling is an online appointment scheduler with all the bells and whistles Lego Party needs, as well as a developer friendly scheduling API for checking upcoming classes and creating new bookings. Init.ai is a developer platform for building conversational apps powered by Natural Language Processing, with direct integration to Facebook Messenger.

我们需要一个Acuity Scheduling帐户和一个Init.ai帐户(不需要信用卡!)。 敏锐调度是一个在线预约调度与所有的花里胡哨的乐高派对的需要,以及开发者友好的调度API ,用于检查即将到来的类和创造新的预订。 Init.ai是一个开发人员平台,用于构建由Natural Language Processing驱动的对话应用程序,并直接集成到Facebook Messenger。

The application itself will consist of a single server component written in Node.js using Acuity's SDK, Init.ai's SDK, Moment.js and a sample app from Init.ai. And we'll be building it to do this:

该应用程序本身将包含一个使用Acuity的SDK , Init.ai的SDK , Moment.js和Init.ai 的示例应用程序以Node.js编写的服务器组件。 我们将构建它来执行此操作:

Let's get started!

让我们开始吧!

视力调度 ( Acuity Scheduling )

Get started with Acuity Scheduling by signing up for a free trial here. Feel free to poke around! But for this project we'll need some classes, so don't leave without creating a couple "Class" appointment types, and be sure to offer a few sessions for each class. Here's the upcoming class schedule folks see when they visit my client scheduling page:

在此处注册免费试用,开始使用Acuity Scheduling。 随意戳一下! 但是对于该项目,我们将需要一些类,因此请不要离开而创建几个“类”约会类型 ,并确保为每个类提供一些会话。 这是即将到来的课程时间表,人们在访问我的客户时间表页面时会看到:

@media (max-width: 1280px) { .go-go-gadget-react img:first-child { display: none; } }@media (max-width: 780px) {.go-go-gadget-react { flex-direction: column; }.go-go-gadget-react img { margin-left: 0 !important; margin-bottom: 12px !important; }.header-thingy { margin-top: 20px; }.button-thingy { margin-left: 0 !important; margin-top: 12px !important; }} @media (max-width: 1280px) { .go-go-gadget-react img:first-child { display: none; } }@media (max-width: 780px) {.go-go-gadget-react { flex-direction: column; }.go-go-gadget-react img { margin-left: 0 !important; margin-bottom: 12px !important; }.header-thingy { margin-top: 20px; }.button-thingy { margin-left: 0 !important; margin-top: 12px !important; }}

Once you have your classes set up, have a quick look at the scheduling APIs we'll be integrating:

设置好类之后,快速浏览一下我们将要集成的调度API:

  • get appointment-types to provide a list of classes获取约会类型以提供课程列表
  • get availability/classes to fetch the dates and times of a class's sessions获取班级/班级以获取班级的日期和时间
  • post appointments to book new classes发布约会以预定新课程
  • get appointments to pull someone's upcoming appointments得到约会拉某人的即将到来的约会

初始化 ( Init.ai )

Init.ai is in a free beta — signup only requires a github account. After signing up, create a new Project for Class Bookings. The complexity of dealing with the Init.ai is abstracted away nicely by their SDK so I won't focus on that here. Instead, there are a couple key concepts to keep in mind for the implementation:

Init.ai是一个免费的Beta版- 注册只需一个github帐户 。 签署后,为课程预订创建一个新项目。 他们的SDK很好地抽象了处理Init.ai的复杂性,因此在这里我不再关注。 相反,实现时需要记住几个关键概念:

  • Training Conversations Example conversations used to define intents and entities and build a model.训练对话示例对话用于定义意图实体并建立模型
  • * Intents** Meanings of particular messages, such as a *greeting or providing a particular piece of data.*目的**特定消息的含义,例如*问候或提供特定数据。
  • Entities Important words within a message, extracted as data and a corresponding type.实体消息中的重要单词,提取为数据和相应的类型。
  • Models Programs built by Init.ai from Training Conversations mapping input language to intents and entities, and vice-versa.Init.ai从“训练对话”构建的模型程序,将输入语言映射到意图和实体,反之亦然。

The first step in a new project is to create some training language — the more the better! Init.ai has a simple markup language for the task. Here's an example:

新项目的第一步是创建某种培训语言-越多越好! Init.ai具有用于任务的简单标记语言。 这是一个例子:

user> What time is my class?
* checkservice> What is your e-mail address?
* prompt/emailuser> [legopartytime@example.com](email/email)
* provide/emailservice> You do not have any upcoming classes scheduled.
* upcoming/none

A user kicks off a conversation, and the service makes a reply. An intent, marked with an asterisk, follows each message, eg. * check. And entities, marked with brackets and followed by it's optional type and a name in parentheses, are defined inside messages eg. [legopartytime@example.com](email/email).

用户开始对话,然后服务进行回复。 每个消息后面都带有一个标有星号的意图* check 。 在消息中定义了实体 ,并用方括号标记,后跟它的可选类型和括号中的名称。 [legopartytime@example.com](email/email)

For this project, you can find example training conversations in the github project. Add those to your new Init.ai project and Init.ai will automatically populate a list of intents and entities for you. Then just click "Train your model" in the main menu and Init.ai will build the model to power the application. That'll take a few minutes, but we can carry on in the meantime!

对于该项目,您可以在github项目中找到示例培训对话 。 将它们添加到新的Init.ai项目中,Init.ai将自动为您填充意图和实体的列表。 然后只需在主菜单中单击“训练您的模型”,Init.ai将构建模型来为应用程序供电。 这将需要几分钟,但是我们可以同时进行!

Finally under Settings, connect a messanging plugin. Init.ai has a built in "test" messenger you can find in the bottom right-hand corner of your Init.ai console. For this project we'll use Facebook Messenger, which supports a couple extra features that the test messenger doesn't. First you'll need a Facebook page — you can create a new one here for testing — then enable the Facebook Messenger connection.

最后,在“设置”下,连接一个聊天插件。 Init.ai有一个内置的“测试”信使,您可以在Init.ai控制台的右下角找到它。 在此项目中,我们将使用Facebook Messenger,它支持测试Messenger所不具备的一些额外功能。 首先,您需要一个Facebook页面- 您可以在此处创建一个新页面进行测试 -然后启用Facebook Messenger连接。

For the rest of this project you'll interact with your chatbot through the Facebook Messenger. As soon as your Facebook page is connected, we'll start the server and build our application.

在本项目的其余部分,您将通过Facebook Messenger与您的聊天机器人进行交互。 连接您的Facebook页面后,我们将启动服务器并构建我们的应用程序。

应用程序 ( The Application )

Now we're ready to dive into the code. First, we'll start up the server.

现在,我们准备深入研究代码。 首先,我们将启动服务器。

启动服务器 (Starting The Server)

We'll be starting with a sample project Init.ai has put together. Clone that repository to get started:

我们将从Init.ai放在一起的示例项目开始。 克隆该存储库即可开始:

git clone git@github.com:init-ai/sample-logic-server.git

Now install base modules, and the couple additional modules we'll be using:

现在安装基本模块,以及我们将要使用的两个附加模块:

npm install && npm install --save acuityscheduling moment

We're almost ready to start the dev server. First, grab your Acuity user ID and API key from Business Settings - Integrations. We'll pass those into our server's environment now because we'll need them in a minute.

我们几乎已经准备好启动开发服务器。 首先, 从“业务设置-集成”中获取您的Acuity用户ID和API密钥 。 我们现在将它们传递到服务器环境中,因为一分钟之内将需要它们。

Then, to start the server run:

然后,要启动服务器运行:

ACUITY_USER_ID=123  ACUITY_API_KEY=abc123  npm run dev

Our dev server will start a "webhook" tunnel to receive messages from Init.ai:

我们的开发服务器将启动“ webhook”隧道以接收来自Init.ai的消息:

Just copy and paste that webhook into Settings under Webhooks in your Init.ai project. Note: Each time you restart the dev server, you'll need to update that webhook URL in your project settings!

只需将该webhook复制并粘贴到Init.ai项目中Webhooks下的Settings中即可。 注意:每次重新启动开发服务器时,都需要在项目设置中更新该Webhook URL!

申请代码 (Application Code)

This sample application contains a bit of boilerplate code that's worth checking out, but everything we'll need to edit is in a single file: src/runLogic.js. Code in this module will be executed each time we receive an event (eg. a message) from Init.ai through the webhook tunnel started by the server.

该示例应用程序包含一些值得检查的样板代码,但是我们需要编辑的所有内容都在一个文件中: src/runLogic.js 。 每当我们通过服务器启动的Webhook隧道从Init.ai接收到事件(例如消息)时,将执行此模块中的代码。

First things first, include the modules and a bit of config that we'll be working with:

首先,包括我们将要使用的模块和一些配置:

/*** Run Logic for Booking Appointments with Init.ai*/const InitClient = require('initai-node');
const AcuityScheduling = require('acuityscheduling');
const moment = require('moment');
const dateFormat = 'MMM D, h:mma';

This module itself exports a function which is run for each message we receive through Init.ai, and returns a promise:

这个模块本身导出一个函数,该函数针对我们通过Init.ai收到的每条消息运行,并返回一个Promise:

module.exports = function runLogic(eventData) {return new Promise((resolve) => {// Application code goes here...});
};

A lot of our service's intents are asynchronous, and will gather data from Acuity's API — Init.ai's SDK is built to help with this type of asynchronous task. In the promise body, create instances of the Acuity and Init.ai SDKs:

我们许多服务的意图都是异步的,并且会从Acuity的API收集数据-Init.ai的SDK旨在帮助此类异步任务。 在promise主体中,创建Acuity和Init.ai SDK的实例:

// Create Init.ai clientconst client = InitClient.create(eventData, {succeed: resolve});// Create an Acuity clientconst acuity = new AcuityScheduling.basic({"userId": process.env.ACUITY_USER_ID,"apiKey": process.env.ACUITY_API_KEY});

Each edit we make to this file triggers the development server to reload. Our Acuity credentials are already in the server's environment from when we started the server, so we're good to go! Now we can write the logic for the two conversation tasks:

我们对该文件的每次编辑都会触发开发服务器重新加载。 自启动服务器以来,我们的Acuity凭据已经存在于服务器的环境中,因此我们很高兴! 现在,我们可以为两个对话任务编写逻辑:

  1. Booking new classes预订新班
  2. Checking their booked classes检查他们预定的课程

对话逻辑 (Conversation Logic)

Conversations in Init.ai are controlled by a flow. Tasks for a particular conversation, such as "book a class" or "check bookings", are called streams. Each stream can have multiple steps such as getEmailAddress. And each step usually corresponds to a pair of question-answer intents, processing the entities from a message to store data, request new data, etc.

Init.ai中的会话由控制。 特定对话的任务,例如“预定课程”或“检查预订”,称为 。 每个流可以具有多个步骤,例如getEmailAddress 。 而且每个步骤通常对应一对问题意图 ,处理消息中的实体以存储数据,请求新数据等。

All of this is set up in the client.runFlow method, the main entry point for our Init.ai conversation logic. Add this down to the bottom of src/runLogic.js and then we'll fill in the steps above it:

所有这些都在client.runFlow方法中设置,该方法是我们Init.ai对话逻辑的主要入口点。 将其添加到src/runLogic.js的底部,然后我们在其上方填写步骤:

// Set up the logic for our flow.//// We have two separate conversation streams: the default stream for// booking a new class, and a separate stream to get current bookings.// Conversations default to the bookClass stream, unless we receive a// 'check' intent.  Then we'll kick off the getBookings stream.client.runFlow({classifications: {'check': 'getBookings'},streams: {main: 'bookClass',bookClass: [getAppointmentType, getDatetime, getName, getEmail, bookAppointment],getBookings: [getEmail, getAppointments]}});

Our two conversation types, or streams, are bookClass and getBookings. Most people will book a class before they check their bookings, so we define that as the default stream using the main attribute. The classification attribute lets us map other intents to specific streams. In our case, we'll map the check intent to the getBookings stream.

我们的两种对话类型或bookClassgetBookings 。 大多数人会在检查预订之前预订课程,因此我们使用main属性将其定义为默认流。 classification属性使我们可以将其他意图映射到特定流。 在我们的例子中,我们将把check意图映射到getBookings流。

步骤1: getAppointmentType (Step 1: getAppointmentType)

Now we'll define our first step: getAppointmentType. Add this step above the call to runFlow:

现在,我们定义第一步: getAppointmentType 。 在对runFlow的调用之上添加此步骤:

//// Steps://const getAppointmentType = client.createStep({/*** Get the appointment type from the client response.*/extractInfo() { },/*** Satisfy this step once we have an appointment type:*/satisfied() { },/*** Prompt for an appointment type if we don't have one stored.*/prompt() { }});

Each step has a few different pieces. In our application, we'll be using extractInfo, satisfied and prompt. The extractInfo method takes any entities and other data extracted from a message and decides what to do with them. Irregardless of where we are in a conversation, extractInfo is run for each step when any message is received. Chatters can provide info in an unexpected order, or provide info for multiple steps all at once, but each step should only concern itself with that step's entities.

每个步骤都有一些不同的部分。 在我们的应用程序中,我们将使用extractInfosatisfiedpromptextractInfo方法采用从消息中提取的所有实体和其他数据,并决定如何处理它们。 无论我们在对话中的什么位置,当收到任何消息时, extractInfo针对每个步骤运行extractInfo 。 聊天者可以意外的方式提供信息,也可以一次提供多个步骤的信息,但是每个步骤仅应与该步骤的实体相关。

"Hello. My name is Inigo Montoya. You killed my father. Prepare to die."

“你好。我叫Inigo Montoya。你杀了我父亲。准备死。”

Execution continues and runFlow decides which stream we're in. Steps for the current stream are executed in order, and satisfied is called. If the step has everything it needs and can be considered complete, satisfied should return true. For the first step that is not satisfied, the prompt method is called and execution finishes.

执行继续, runFlow决定我们runFlow流。当前流的步骤按顺序执行,并调用satisfied 。 如果该步骤具有所需的一切并且可以视为完成,则satisfied应返回true。 对于不满足的第一步,将调用prompt方法,执行完成。

In our first step, we're extracting an appointment type from the response (hopefully!) —

在第一步中,我们从响应中提取约会类型(希望如此)—

/*** Get the appointment type from the client response.*/extractInfo() {// Store the appointment type ID in the conversation stateconst data = client.getPostbackData();if (data && data.appointmentTypeID) {client.updateConversationState({appointmentTypeID: data.appointmentTypeID});}},

client.getPostbackData() returns structured data from pre-defined replies in a prompt — we'll cover that in a minute. If we've received an appointment type ID, store it in the conversation state, akin to a session in a web application.

client.getPostbackData()会在提示中从预定义的回复中返回结构化数据-我们将在稍后介绍。 如果我们收到约会类型ID,则将其存储在对话状态下,类似于Web应用程序中的会话。

This step is considered satisfied once we have an appointmentTypeID stored.

一旦我们存储了appointmentTypeID ID,就认为此步骤已满足。

/*** Satisfy this step once we have an appointment type:*/satisfied() { return Boolean(client.getConversationState().appointmentTypeID) },

If it's not satisfied, prompt will be called. We'll fetch a list of appointment types from the Acuity API, filter it for public classes, and send a reply with those options to the chatter.

如果不满意,将prompt 。 我们将从Acuity API中获取约会类型的列表,将其过滤为公共类,然后将带有这些选项的回复发送给聊天者。

/*** Prompt for an appointment type if we don't have one stored.*/prompt() {// Fetch appointment types from Acuityacuity.request('/appointment-types', function (err, res, appointmentTypes) {// Build some buttons for folks to choose a classconst replies = appointmentTypes// Filter types for public classes.filter(appointmentType => appointmentType.type === 'class' && !appointmentType.private)// Create a button for each type.map(appointmentType => client.makeReplyButton(appointmentType.name,null,'bookClass',{appointmentTypeID: appointmentType.id}));// Set the response intent to prompt to choose a typeclient.addResponseWithReplies('prompt/type', null, replies);// End the asynchronous promptclient.done();});}

A nice feature of the Facebook Messenger is the ability to create convenient reply buttons for the user.

Facebook Messenger的一个不错的功能是可以为用户创建方便的回复按钮。

client.makeReplyButton( /* button text */, /* image */, /* stream */, /* structured data */ )

Our appointment type prompt creates a button for each class which displays the class name, and has the appointmentTypeID stored. When the user selects a button, we'll receive a reply with that data from that getPostbackData method called in extractInfo.

我们的约会类型提示会为每个类别创建一个按钮,该按钮显示类别名称,并存储appointmentTypeID ID。 当用户选择一个按钮时,我们将从在extractInfo调用的getPostbackData方法收到包含该数据的回复。

The service's reply is defined with client.addResponseWithReplies, sending the prompt/type intent and the class buttons:

服务的回复是使用client.addResponseWithReplies定义的,发送prompt/type 意图和类按钮:

client.addResponseWithReplies('prompt/type', null, replies);

Last of all client.done(); is called. We made an asynchronous request to the Acuity API. client.done() resolves the promise, and the service ships the response back to the user.

最后client.done(); 叫做。 我们向Acuity API发出了异步请求。 client.done()解析承诺,然后服务将响应发回给用户。

Here's the complete getAppointmentType step:

这是完整的getAppointmentType步骤:

const getAppointmentType = client.createStep({/*** Satisfy this step once we have an appointment type:*/satisfied() { return Boolean(client.getConversationState().appointmentTypeID) },/*** Get the appointment type from the client response.*/extractInfo() {// Store the appointment type ID in the conversation stateconst data = client.getPostbackData();if (data && data.appointmentTypeID) {client.updateConversationState({appointmentTypeID: data.appointmentTypeID});}},/*** Prompt for an appointment type if we don't have one stored.*/prompt() {// Fetch appointment types from Acuityacuity.request('/appointment-types', function (err, res, appointmentTypes) {// Build some buttons for folks to choose a classconst replies = appointmentTypes// Filter types for public classes.filter(appointmentType => appointmentType.type === 'class' && !appointmentType.private)// Create a button for each type.map(appointmentType => client.makeReplyButton(appointmentType.name,null,'bookClass',{appointmentTypeID: appointmentType.id}));// Set the response intent to prompt to choose a typeclient.addResponseWithReplies('prompt/type', null, replies);// End the asynchronous promptclient.done();});}});

步骤2: getDatetime (Step 2: getDatetime)

Our next step is to pick a particular class session. Similar to appointment types, we'll grab available class sessions from the Acuity API and provide a list of convenient buttons to choose from. We'll extract the session datetime from the postback data and satisfy the step once the datetime is saved:

我们的下一步是选择一个特定的课堂。 与约会类型类似,我们将从Acuity API中获取可用的课程会话,并提供一系列方便的按钮供您选择。 我们将从回发数据中提取会话datetime时间,并在保存日期时间后满足该步骤:

const getDatetime = client.createStep({satisfied() { return Boolean(client.getConversationState().datetime) },extractInfo() {// Store the datetime in the conversation stateconst data = client.getPostbackData();if (data && data.datetime) {client.updateConversationState({datetime: data.datetime});}},prompt() {// Fetch available class sessions from Acuity using the appointment type:const state = client.getConversationState();const options = {qs: {month: moment().format('YYYY-MM'),appointmentTypeID: state.appointmentTypeID}};acuity.request('/availability/classes', options, function (err, res, sessions) {// Build buttons for choosing a class session:const replies = sessions.map(session => client.makeReplyButton(moment(session.time).format(dateFormat),null,'bookClass',{datetime: session.time}));// Ship the response:client.addResponseWithReplies('prompt/datetime', null, replies);client.done();});}});

For fetching the session info from Acuity, we'll use the get availability/classes endpoint for the current month and the selected appointment type from the first step.

为了从Acuity获取会话信息,我们将使用当月的get availability/classes端点和从第一步中选择的约会类型。

const options = {qs: {month: moment().format('YYYY-MM'),appointmentTypeID: state.appointmentTypeID}};acuity.request('/availability/classes', options, ...

步骤3: getEmail (Step 3: getEmail)

Unlike the previous two steps which extract info from the user's button choice, getEmail grabs the email entity directly from the client's message using getFirstEntityWithRole. This step is also shared — it is used in both the bookClass stream and the getBookings stream.

与从用户的按钮选择中提取信息的前两个步骤不同, getEmail使用getFirstEntityWithRole直接从客户端消息中获取电子邮件实体 。 此步骤也是共享的-在bookClass流和getBookings流中都使用了此步骤。

const getEmail = client.createStep({satisfied() { return Boolean(client.getConversationState().email) },extractInfo() {// Get an e-mail provided by the user:const email = client.getFirstEntityWithRole(client.getMessagePart(), 'email/email');if (email) {client.updateConversationState({ email: email.value });}},prompt() {client.addResponse('prompt/email');// The getEmail step is used in multiple streams.  If we're not in the// default stream, set the expected next step.if (client.getStreamName() !== 'bookClass') {client.expect(client.getStreamName(), ['provide/email']);}client.done();}});

The prompt for this step sets an expectation if we're not in the default stream:

如果我们不在默认流中 ,则此步骤的提示会设置期望值:

client.expect(client.getStreamName(), ['provide/email']);

Since getEmail is shared between multiple streams, that serves as a hint that reply should provide an e-mail address and should be part of the current stream (eg. getBookings).

由于getEmail在多个流之间共享,因此提示答复应提供电子邮件地址,并且应成为当前流的一部分(例如getBookings )。

步骤4: getName (Step 4: getName)

Now we're ready to collect the user's name. Another benefit of using the Facebook Messenger connection is that it's aware of the user's Facebook profile, including their name. We can grab that with client.getMessagePart().sender and automatically store it to the conversation state.

现在,我们准备收集用户名。 使用Facebook Messenger连接的另一个好处是它知道用户的Facebook个人资料,包括他们的姓名。 我们可以使用client.getMessagePart().sender来获取它,并自动将其存储到对话状态。

Just in case it's not available, our training data contains an intent to prompt the user for their name.

万一它不可用,我们的培训数据将包含一个提示用户提示其名称的意图

const getName = client.createStep({satisfied() { return Boolean(client.getConversationState().firstName &&client.getConversationState().lastName)},extractInfo() {// Check for sender name set from Facebook,// or use the name provided by the user:const sender =  client.getMessagePart().sender;const firstName = client.getFirstEntityWithRole(client.getMessagePart(), 'firstName') || sender.first_name;const lastName = client.getFirstEntityWithRole(client.getMessagePart(), 'lastName') || sender.last_name;if (firstName) {client.updateConversationState({ firstName: firstName });}if (lastName) {client.updateConversationState({ lastName: lastName });}},prompt() {client.addResponse('prompt/name')client.done()}});

步骤5: bookAppointment (Step 5: bookAppointment)

Finally we've got what we need to book an appointment:

终于,我们有了需要预约的东西:

  • client name and e-mail address客户名称和电子邮件地址
  • the class to book预定课程
  • and the date and time.以及日期和时间。

This step won't contain an extractInfo() step since we're not looking for anything else from the user and the satisfied() method returns false since it's the last step. To book the class, the prompt() method will call Acuity's post appointments API with the client info we've been collecting in the conversation state:

此步骤将不包含extractInfo()步骤,因为我们不从用户那里寻找其他任何东西,并且因为它是最后一步,所以satisfied()方法返回false。 要预订该类, prompt()方法将使用我们一直在对话状态下收集的客户端信息调用Acuity的post appointments API:

prompt() {// Get the whole conversation state:const state = client.getConversationState();// Book the class appointment using the gathered infoconst options = {method: 'POST',body: {appointmentTypeID: state.appointmentTypeID,datetime:          state.datetime,firstName:         state.firstName,lastName:          state.lastName,email:             state.email}};acuity.request('/appointments', options, function (err, res, appointment) { ... });

After creating the booking, we can clear out the for this booking. This resets things for the user, allowing them to book another class or check their upcoming bookings:

创建预订后,我们可以清除此预订的。 这会为用户重置内容,使他们可以预订其他课程或查看他们的近期预订:

// Clear out conversation state.  This will reset our satisfied// conditions  and the user can schedule again.client.updateConversationState({appointmentTypeID: null,datetime: null});

Last, we'll send a response to the client letting them know the appointment is confirmed. The second argument for addResponse is a map of entities for the intent to include in the message to the client. In this case, we'll want to echo the class name and time back to the client in the confirmation message:

最后,我们将向客户发送回复,让他们知道约会已确认。 addResponse的第二个参数是要包含在发送给客户端的消息中的意图实体映射。 在这种情况下,我们将在确认消息中将类名和时间回显给客户端:

// Send the confirmation message, with entities for the bookingclient.addResponse('confirmation', {type: appointment.type,datetime: moment(appointment.datetime).format(dateFormat)});

Here's the full listing for the bookAppointment step:

这是bookAppointment步骤的完整清单:

const bookAppointment = client.createStep({// This is the final step:satisfied() { return false; },prompt() {// Get the whole conversation state:const state = client.getConversationState();// Book the class appointment using the gathered infoconst options = {method: 'POST',body: {appointmentTypeID: state.appointmentTypeID,datetime:          state.datetime,firstName:         state.firstName,lastName:          state.lastName,email:             state.email}};acuity.request('/appointments', options, function (err, res, appointment) {// Clear out conversation state.  This will reset our satisfied// conditions  and the user can schedule again.client.updateConversationState({type: null,appointmentTypeID: null,datetime: null});// Send the confirmation message, with entities for the bookingclient.addResponse('confirmation', {type: appointment.type,datetime: moment(appointment.datetime).format(dateFormat)});client.done();});}});

With these steps implemented, you'll be able to interact with the booking bot through Facebook Messenger and schedule an appointment: https://www.messenger.com/

完成这些步骤后,您将能够通过Facebook Messenger与预订机器人进行互动并安排约会: https : //www.messenger.com/

检查约会 (Checking Appointments)

Once a couple appointments are scheduled, it's time to implement the getBookings stream to check existing bookings. To keep things simple we'll look up a user's schedule by their e-mail address, reusing the getEmail step. After we have a client's email, there's only one more step: getAppointments to provide the user with their upcoming schedule.

一旦安排了几次约会,就可以实现getBookings 以检查现有预订了。 为简单getEmail ,我们将重复使用getEmail步骤,通过用户的电子邮件地址查找其时间表。 在收到客户的电子邮件之后,仅需执行一个步骤: getAppointments为用户提供即将到来的日程安排。

We'll use Acuity's get appointments API along with two parameters: email and minDate, set to now:

我们将使用Acuity的get appointments API和两个参数: emailminDate ,设置为现在:

// Get upcoming appointments matching an e-mail address:const state = client.getConversationState();const options = {qs: {email: state.email,minDate: moment().toISOString()}};acuity.request('/appointments', options, function (err, res, appointments) { ... }

In the response, we'll check if there are any upcoming appointments and decide which response intent to send to the client:

在响应中,我们将检查是否有任何即将到来的约会,并确定要发送给客户端的响应意图

// Decide which response intent to send: the upcoming schedule, or none:if (appointments.length) {// format upcoming schedule and response...} else {// If no appointments, send none intent:client.addResponse('upcoming/none');}

If there are any upcoming appointments matching the email address, we'll format them into a list for the response. First, sort them in chronological order then format them into a list with one session on each line. Similar to the confirmation step in the bookClass stream, we'll provide that data for the response entities:

如果有任何即将与电子邮件地址匹配的约会,我们会将其格式化为响应列表。 首先,按时间顺序对它们进行排序,然后将其格式化为列表,每行一个会话。 类似于bookClass流中的确认步骤,我们将为响应实体提供该数据:

// Sort upcoming classes chronologically and format responseconst classes = "\n" + appointments.sort((a, b) => b.datetime < a.datetime ).map(appointment =>moment(appointment.datetime).format(dateFormat)+': '+appointment.type).join(", \n")// Send upcoming appointments intent, with entities:client.addResponse('upcoming/appointments', {'number/count': appointments.length,'classes': classes});

Just before calling client.done() we'll clear the expected stream with client.expect(null). This resets the hint set in getEmail, allowing the client to enter a different stream such as booking another class after checking their schedule. Here's the complete listing for our final step:

在调用client.done()之前,我们将使用client.expect(null)清除期望的流。 这将重置getEmail的提示集,从而允许客户端输入其他流,例如在检查他们的日程之后预订另一个班级。 这是我们最后一步的完整清单:

const getAppointments = client.createStep({satisfied() { return false; },prompt() {// Get upcoming appointments matching an e-mail address:const state = client.getConversationState();const options = {qs: {email: state.email,minDate: moment().toISOString()}};acuity.request('/appointments', options, function (err, res, appointments) {// Decide which response intent to send: the upcoming schedule, or none:if (appointments.length) {// Sort upcoming classes chronologically and format responseconst classes = "\n" + appointments.sort((a, b) => b.datetime < a.datetime ).map(appointment =>moment(appointment.datetime).format(dateFormat)+': '+appointment.type).join(", \n")// Send upcoming appointments intent, with entities:client.addResponse('upcoming/appointments', {'number/count': appointments.length,'classes': classes});} else {// If no appointments, send none intent:client.addResponse('upcoming/none');}// Clear the expected stream after getting appointments:client.expect(null);client.done();});}});

Now that that's implemented, we can check our schedule:

现在已经实现了,我们可以检查我们的时间表:

结论 ( Conclusion )

Well, this article didn't write itself! But Conversational AI has come a long way, and today is more dev-friendly than ever. Creating a bot that performs well for the two tasks I gave it — that's not bad for an afternoon!

好吧,这篇文章不是自己写的! 但是,对话式AI已经走了很长一段路,而今天,它比以往任何时候都更加易于开发。 创建一个在我完成的两个任务中表现出色的机器人-一个下午还不错!

You can find the full source code for this project on github. From there, there are two major improvements that can be made:

您可以在github上找到该项目的完整源代码 。 从那里,可以进行两个主要的改进:

  • Additional training data helping the model handle unexpected input and additional API cases such as no availability for the month.帮助模型处理意外输入的其他培训数据和其他API案例,例如当月没有空房。
  • Expanding the bot into other common tasks, such as canceling or rescheduling a booking.将漫游器扩展到其他常见任务,例如取消或重新安排预订。

Plugging a conversational interface into existing APIs such as Acuity Scheduling is already a practical way to bring useful conversational functionality to other apps such as Facebook Messenger. From here, it'll only get better!

将对话接口插入现有的API(例如Acuity Scheduling )已经是一种实用的方法,可以将有用的对话功能带给其他应用程序(例如Facebook Messenger)。 从这里开始,只会变得更好!

This post was sponsored via SyndicateAds.

该帖子由SyndicateAds赞助。

翻译自: https://scotch.io/tutorials/building-a-conversational-booking-bot-with-acuity-scheduling

会话验证调度器

会话验证调度器_用视力调度建立会话式预订机器人相关推荐

  1. mysql 事件调度器_【MySQL】事件调度器 (Event Scheduler)

    一 event 介绍 事件调度器是定时触发执行的,在这个角度上也可以称作是"临时的触发器".触发器只是针对某个表产生的事件执行一些语句,而事件调度器则是在某一个(间隔)时间执行一些 ...

  2. 【Linux 内核】CFS 调度器 ② ( CFS 调度器 “ 权重 “ 概念 | CFS 调度器调度实例 | 计算进程 “ 实际运行时间 “ )

    文章目录 一.CFS 调度器 " 权重 " 概念 二.CFS 调度器调度实例 ( 计算进程 " 实际运行时间 " ) 一.CFS 调度器 " 权重 & ...

  3. 【Linux 内核】CFS 调度器 ① ( CFS 完全公平调度器概念 | CFS 调度器虚拟时钟 Virtual Runtime 概念 | 四种进程优先级 | 五种调度类 )

    文章目录 一.CFS 调度器概念 ( 完全公平调度器 ) 二.CFS 调度器虚拟时钟概念 ( Virtual Runtime ) 三.进程优先级 ( 调度优先级 | 静态优先级 | 正常优先级 | 实 ...

  4. Golang调度器GPM原理与调度全分析

    第一章 Golang调度器的由来 第二章 Goroutine调度器的GMP模型及设计思想 第三章 Goroutine调度场景过程全图文解析 一.Golang"调度器"的由来? (1 ...

  5. linux cfs调度器_模型实现

    调度器真实模型的主要成员变量及与抽象模型的对应关系 I.cfs_rq结构体     a) struct sched_entity *curr         指向当前正在执行的可调度实体.调度器的调度 ...

  6. python 分布式 调度 管理_分布式云调度处理系统

    分布式云调度处理系统. 项目参考xxl-job进行若干改动. 项目基于quartz并进行若干扩展而成,适用于公司内部做定时调度处理,方便,快捷,简单. 支持bean, groovy, shell, p ...

  7. 小数视力表转换成对数视力表_如何用视力表建立班级预订系统

    小数视力表转换成对数视力表 This article was sponsored by Acuity Scheduling. Thank you for supporting the partners ...

  8. mysql存储过程结构体_八、mysql视图、存储过程、函数以及时间调度器

    1.create or replace view emp_view as select * fromt4 ;给t4表创建一个名为emp_view的视图2.drop viewemp_view 删除视图= ...

  9. go语言学习笔记(四):调度器基础-爬上那座山

    目录 调度器概述 调度器初始化 第一个goroutine,main goroutine的创建 第一个goroutine,main goroutine的调度 非main goroutine的创建.退出以 ...

最新文章

  1. 蚂蚁金服×西安银行 | 西安银行手机银行App的智能升级之路
  2. HttpClient+Jericho HTML Parser 实现网页的抓取
  3. 信号与系统课程向学校教务科需要提交的资料
  4. 什么时候用抽象?什么时候用接口?
  5. c语言八个方向迷宫课程设计,【精品资料最新版】C语言课程设计-迷宫游戏.doc...
  6. (王道408考研数据结构)第二章线性表-第三节5:顺序表和链表的比较
  7. webpack--插件配置:处理HTML中的图片(七)
  8. 美图秀秀丰胸一秒变身D罩杯图片美容处理软件
  9. 使用Python下载m3u8流视频
  10. 车牌号识别 python + opencv
  11. NameNode故障处理之数据恢复
  12. FFMpeg的码率控制 - CBR or VBR
  13. 博网即时通讯软件的设计与实现(附源码+课件+数据库+资料)
  14. USB 3.0硬件设计
  15. Android ROM开发(一)——Windows下Cygwin和Android_Kitchen厨房的安装
  16. java 天数计算日期_Java 计算日期间天数与日期推算等操作
  17. Typescript(一)
  18. 关于网页制作的一些动态效果
  19. SRPG游戏开发(十六)第六章 基本框架 - 一 本章简介(Introduction)
  20. 谁交了片仔癀的智商税?

热门文章

  1. Node.js + Web Socket 打造即时聊天程序嗨聊(1)
  2. Spring定时任务实现方式
  3. mqtt 变为乱码 接受16进制字节流_转战物联网#183;基础篇07-深入理解MQTT协议之控制报文(数据包)格式...
  4. 准确性 敏感性 特异性_特异性图
  5. Burpsuite+夜神模拟器对app抓包(安卓7及其以上)
  6. Ruby之旅之字符串
  7. Code Combat学习心得(Kithgard地牢45关Mightier Than the Sword)
  8. 安逸云中小云厂商机遇
  9. 2021计算机考研改了大纲,2021考研计算机大纲变化!
  10. 12315提交显示服务器出错,发送到某些地址时出现 Smtp 问题 - 错误:服务器不接受 rcpt...