Skip to content

Latest commit

 

History

History
1463 lines (975 loc) · 77.4 KB

File metadata and controls

1463 lines (975 loc) · 77.4 KB

五、Microsoft Bot 框架简介

微软的 Bot Builder SDK 有两种版本:C#和 Node.js。正如在第 1 章中提到的,出于本书的目的,我们将使用 Node.js 版本。Node.js 是跨平台的 JavaScript 运行时;事实上,它是跨平台的,并且基于像 JavaScript 这样的入门语言,这意味着我们可以更容易地展示使用该技术构建机器人是多么容易。我们保持在 EcmaScript6 的范围内;然而,机器人框架机器人可以使用任何风格的 JavaScript 来构建。Bot Builder 框架本身是用 TypeScript 编写的,TypeScript 是 JavaScript 的超集,包含可选的静态类型,可以编译成 JavaScript。

对于本章,我们应该对 Node.js 和 npm(Node 包管理器)有一个初步的了解。整本书提供的代码将包括 npm 包定义,所以我们只需要运行两个命令。

npm install
npm start

我们本章的目的是编写一个基本的 echo bot,并使用微软的 channel connectors 将其部署到 Facebook Messenger。一旦我们设置了基本的机器人,我们将深入到机器人构建器 SDK 中的不同概念,这些概念真正允许我们编写杀手级机器人:瀑布、对话框、识别器、会话、卡等等。走吧!

Microsoft Bot Builder SDK 基础知识

我们将用来编写机器人的核心库称为机器人构建器 SDK ( https://github.com/Microsoft/BotBuilder )。首先,您需要创建一个新的 Node 包,并安装 botbuilderdotenv-extendedrestify 包。为此,您可以创建一个新目录,并键入以下命令:

npm init
npm install botbuilder dotenv-extended restify --save

5-1 显示了本地机器上典型的高级 bot 架构。这个想法是,node 应用主要依赖于两个组件。首先,bot Builder SDK 是我们用来构建 Bot 的 Bot 引擎。第二,来自任何通道的所有消息,无论是来自机器外部还是来自开发人员机器的 bot 框架仿真器,都通过 HTTP 端点发送到 Bot。我们使用 restify 来监听 HTTP 消息,并将它们发送给 SDK。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig1_HTML.jpg

图 5-1

典型的高级 bot 架构

作为手动创建 package.json 文件的替代方法,我们可以使用随书提供的 echo-bot 代码来引导这个练习。回声机器人的 package.json 如下所示。注意, eslint 依赖项纯粹是针对我们的开发环境的,所以我们可以运行 JavaScript linter1来检查风格和潜在的编程错误。

{
  "name": "practical-bot-development-echo-bot",
  "version": "1.0.0",
  "description": "Echo Bot from Chapter 1, Practical Bot Development",
  "scripts": {
    "start": "node app.js"
  },
  "author": "Szymon Rozga",
  "license": "MIT",
  "dependencies": {
    "botbuilder": "^3.9.0",
    "dotenv-extended": "^1.0.4",
    "restify": "^4.3.0"
  },
  "devDependencies": {
    "eslint": "^4.10.0",
    "eslint-config-google": "^0.9.1",
    "eslint-config-standard": "^10.2.1",
    "eslint-plugin-import": "^2.8.0",
    "eslint-plugin-node": "^5.2.1",
    "eslint-plugin-promise": "^3.6.0",
    "eslint-plugin-standard": "^3.0.1"
  }
}

bot 本身在 app.js 文件中定义。注意,包定义中的启动脚本将 app.js 指定为我们的 bot 的入口点。

// load env variables
require('dotenv-extended').load();

const builder = require('botbuilder');
const restify = require('restify');

// setup our web server
const server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, () => {
    console.log('%s listening to %s', server.name, server.url);
});

// initialize the chat bot
const connector = new builder.ChatConnector({
    appId: process.env.MICROSOFT_APP_ID,
    appPassword: process.env.MICROSOFT_APP_PASSWORD
});
server.post('/api/messages', connector.listen());

const bot = new builder.UniversalBot(connector, [
    (session) => {
        // for every message, send back the text prepended by echo:
        session.send('echo: ' + session.message.text);
    }
]);

让我们看看这段代码。我们使用一个名为 dotenv 的库来加载环境变量。

require('dotenv-extended').load();

环境变量从名为的文件中加载。env 到 process.env JavaScript 对象中。 .env.defaults 文件包含默认的环境变量,可以用来指定 Node.js 所需的值。在这种情况下,文件如下所示:

MICROSOFT_APP_ID=
MICROSOFT_APP_PASSWORD=

我们需要 botbuilderrestify 库。Botbuilder 不言自明。Restify 用于为我们运行一个 web 服务器端点。

const builder = require('botbuilder');
const restify = require('restify');

现在我们设置我们的 web 服务器来监听端口 3978 上的消息。

const server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, () => {
    console.log('%s listening to %s', server.name, server.url);
});

接下来,我们创建一个所谓的聊天连接器。在 Bot 框架的上下文中,通道连接器是由 Microsoft 创建和维护的端点,有助于将消息从本机平台格式转换为 Bot Builder SDK 格式。的建造者。ChatConnector 对象知道如何从这些连接器接收 HTTP 消息,将它们传递给 bot 会话引擎,并将任何传出的消息发送回连接器,如图 5-2 所示。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig2_HTML.jpg

图 5-2

Microsoft Bot 框架连接器

环境变量 MICROSOFT_APP_ID 和 MICROSOFT_APP_PASSWORD 是我们的 bot 的凭证。我们将在稍后使用 Azure 创建 Azure Bot 服务注册时,在 Bot 框架中设置它们。目前,我们可以将这些值留空,因为我们还不太关心如何保护我们的 bot。

const connector = new builder.ChatConnector({
    appId: process.env.MICROSOFT_APP_ID,
    appPassword: process.env.MICROSOFT_APP_PASSWORD
});

接下来我们告诉 restify,任何对/api/messages端点的请求,或者更确切地说是对http://localhost:3978/api/messages的请求,都应该由 connector.listen()返回的函数来处理。也就是说,我们允许 Bot 框架处理所有进入该端点的消息。

server.post('/api/messages', connector.listen());

最后,我们创建了通用机器人。它被称为通用机器人,因为它不依赖于任何特定的平台。它使用连接器来接收和发送数据。任何进入机器人的消息都将被发送到函数数组。目前,我们只有一个功能。该函数接受一个会话对象。该对象包含诸如消息之类的数据,但也包含关于用户和对话的数据。机器人通过调用 session.send 函数来响应用户。

const bot = new builder.UniversalBot(connector, [
    (session) => {
        // for every message, send back the text prepended by echo:
        session.send('echo: ' + session.message.text);
    }
]);

注意,Bot Builder SDK 负责为传入的 HTTP 请求提供正确的 HTTP 响应。实际上,如果 Bot Builder 处理代码没有问题,内部将返回 HTTP Accepted (202),否则将返回 HTTP Internal Server Error (500)。

我们的响应的内容是异步的,这意味着我们的 bot 收到的对原始请求的响应不包含任何内容。正如我们将在下一章中看到的,一个传入的请求包括一个通道 ID、连接器的名称如 slackfacebook ,以及一个我们的机器人发送消息的响应 URL。URL 通常类似于 https://facebook.botframework.comSession.send 将向响应 URL 发送一个 HTTP POST 请求。

我们可以通过简单地执行以下命令来运行这个 bot:

npm install
npm start

我们将在控制台中看到一些 Node.js 输出。应该有一个服务器运行在端口 3978 上,路径为/api/messages。根据我们的本地 Node.js 设置和我们机器上预先存在的软件,我们可能需要更新到最新版本的 node-gyp 包,一个用于编译本地插件工具的工具。

我们如何与机器人交流?我们可以尝试使用像 curl 这样的命令行 HTTP 工具来发送消息,但是我们必须拥有一个响应 url 来查看任何响应。此外,我们需要添加逻辑来获取访问令牌,以通过任何安全检查。简单地测试这个机器人似乎工作量太大了。

当然,我们不一定要这样做。微软为我们提供了一个模拟器来测试我们的机器人。可在 https://emulator.botframework.com/ 下载。仿真器支持 Linux、Windows 和 OS X(图 5-3 )。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig3_HTML.jpg

图 5-3

Bot 框架仿真器

准备好,因为我们会经常使用模拟器。以下是我们应该注意的几点:

  • 我们可以在地址栏中输入我们的机器人 URL ( /api/messages)。模拟器还允许我们处理 bot 安全性并指定应用 ID/密码。我们稍后会谈到这一点。

  • 日志部分向我们展示了 bot 和模拟器之间发送的所有消息。我们可以看到模拟器打开了一个端口来托管响应 URL。在本例中,它是端口 58462。

  • 模拟器日志指示何时有更新,所以我们总是运行最新和最好的版本。

  • 有一些关于 ngrok 的措辞。Ngrok 是一个反向代理,允许我们将来自公共 HTTPS 端点的请求隧道传输到本地 web 服务器。当从远程计算机测试机器人连接性时,这非常有用,例如,如果我们想在 Facebook Messenger 上运行本地机器人。我们还可以使用模拟器向远程机器人发送消息。

  • 细节部分显示了在 bot 和模拟器之间发送的每条消息的 JSON。

让我们继续连接到我们的机器人。我们在地址栏中输入http://localhost:3978/api/messages,暂时将微软应用 ID 和微软应用密码字段留空(图 5-4 ),因为我们还没有设置*。env* 文件。我们将在控制台中收到安全警告;现在可以忽略这些。此时,我们真的要单击连接按钮了。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig4_HTML.jpg

图 5-4

模拟器连接用户界面

我们将在模拟器日志中看到两条消息。两者都是类型对话更新(图 5-5 )。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig5_HTML.jpg

图 5-5

从模拟器到我们的 bot 建立连接时更新消息

这是什么意思?bot 和消费连接器(本例中是模拟器)之间的每条消息都被称为一个活动,每个活动都有一个类型。有像留言打字这样的类型。如果活动属于类型消息,那么它实际上就是机器人和用户之间的消息。一个打字活动告诉连接器显示一个打字指示器。之前,我们看到了 conversationUpdate 类型。此类型表示对话中有更改;最常见的情况是,用户加入或离开对话。在用户和机器人之间的一对一对话中,用户和机器人将是对话的两个成员。在群聊场景中,机器人加上所有用户将成为对话的一部分。消息元数据将包括关于哪些用户加入或离开对话的信息。事实上,如果我们点击两个 conversationUpdate 活动的 POST 链接,我们会在 Details 部分找到 JSON。以下是两条消息的内容:

{
    "type": "conversationUpdate",
    "membersAdded": [
        {
            "id": "default-user",
            "name": "User"
        }
    ],
    "id": "hg71ma8cfj27",
    "channelId": "emulator",
    "timestamp": "2018-02-22T22:02:10.507Z",
    "localTimestamp": "2018-02-22T17:02:10-05:00",
    "recipient": {
        "id": "8k53ghlggkl2jl0a3",
        "name": "Bot"
    },
    "conversation": {
        "id": "mf24ln43lde3"
    },
    "serviceUrl": "http://localhost:58462"
}

{
    "type": "conversationUpdate",
    "membersAdded": [
        {
            "id": "8k53ghlggkl2jl0a3",
            "name": "Bot"
        }
    ],
    "id": "jfcdbhek0m4m",
    "channelId": "emulator",
    "timestamp": "2018-02-22T22:02:10.502Z",
    "localTimestamp": "2018-02-22T17:02:10-05:00",
    "recipient": {
        "id": "8k53ghlggkl2jl0a3",
        "name": "Bot"
    },
    "conversation": {
        "id": "mf24ln43lde3"
    },
    "from": {
        "id": "default-user",
        "name": "User"
    },
    "serviceUrl": "http://localhost:58462"
}

现在,让我们向机器人发送一条消息,文本为“echo!”并查看仿真器日志(图 5-6 )。请注意,如果我们不设置显式的 bot 存储实现,我们可能会收到类似这样的警告:“警告:Bot 框架状态 API 不推荐用于生产环境,在未来的版本中可能会被弃用。”我们将在下一章深入探讨这个问题。简单地说,强烈建议我们不要使用默认的 bot 存储。我们现在可以用下面的代码来替换它:

img/455925_1_En_5_Chapter/455925_1_En_5_Fig6_HTML.jpg

图 5-6

它还活着!

const inMemoryStorage = new builder.MemoryBotStorage();
bot.set('storage', inMemoryStorage);

啊哈!我们的机器人还活着。模拟器现在包含了更多的东西。一个消息类型的传入帖子,文本为“echo!”以及带有文本“echo: echo!”的 message 类型的传出帖子和带有调试事件数据的 POST。单击 POST 链接将再次显示在这个请求中接收或发送的 JSON。请注意,这两种有效载荷是不同的,尽管在它们的下面使用了相同的接口 IMessage。我们将在第 6 章对此进行更深入的探讨。以下是传入或传出消息中的部分数据列表:

  • 发送者信息(id/name) :发送者的特定于频道的标识符和用户名。如果消息是从用户到机器人的,这就是用户。在相反的方向,发送者是机器人。Bot Builder SDK 负责填充这些数据。在我们的 JSON 中,这是来自字段的*。*

  • 收件人信息(id/name) :发件人信息的逆。这是接收方字段。

  • 时间戳:消息发送的日期和时间。通常,的时间戳将采用 UTC 时间,而的 localTimestamp 将采用当地时区,尽管很容易混淆,但 bot 响应的 localTimestamp 是一个 UTC 时间戳。

  • ID :唯一活动标识符。这通常映射到特定于通道的消息 ID。id 由通道分配。在模拟器中,传入的消息将被分配一个 ID。传出的消息不会。

  • ReplyToId :当前消息响应的活动的标识符。这用于在消息客户端中线程化对话。

  • 会话:平台上的会话标识。

  • 类型:活动的类型。可能的值包括 message、conversationUpdate、contactRelationUpdate、typing、ping、deleteUserData、endOfConversation、event 和 invoke。

  • 文本:消息的文本。

  • TextFormat :文本字段格式。可能的值有 plain、markdown 和 xml。

  • 附件(Attachments):这是 Bot 框架发送媒体附件(如视频、图像、音频或其他类型,如英雄卡)的结构。我们也可以将该字段用于任何类型的自定义附件。

  • 文本本地:用户的语言。

  • ChannelData :特定于频道的数据。对于传入的消息,这可以包括来自通道的原始本地消息,例如本地 Facebook Messenger SendAPI。对于传出消息,这将是我们希望传递给通道的原始本机消息。这通常在 Microsoft channel connectors 没有针对通道实现特定类型的消息时使用。我们将在第 8 章和第 9 章中探究一些例子。

  • ChannelId :消息平台通道标识。

  • ServiceUrl :机器人向其发送消息的端点。

  • 实体:用户和机器人之间传递的数据对象集合。

让我们更详细地检查交换的消息。来自模拟器的传入消息如下所示:

{
    "type": "message",
    "text": "echo!",
    "from": {
        "id": "default-user",
        "name": "User"
    },
    "locale": "en-US",
    "textFormat": "plain",
    "timestamp": "2018-02-22T22:03:40.871Z",
    "channelData": {
        "clientActivityId": "1519336929414.7950057585459784.0"
    },
    "entities": [
        {
            "type": "ClientCapabilities",
            "requiresBotState": true,
            "supportsTts": true,
            "supportsListening": true
        }
    ],
    "id": "50769feaaj9j",
    "channelId": "emulator",
    "localTimestamp": "2018-02-22T17:03:40-05:00",
    "recipient": {
        "id": "8k53ghlggkl2jl0a3",
        "name": "Bot"
    },
    "conversation": {
        "id": "mf24ln43lde3"
    },
    "serviceUrl": "http://localhost:58462"
}

这里应该没有惊喜。响应看起来很相似,但不那么冗长。这是典型的。通道连接器将用尽可能多的支持数据填充传入的消息。响应不需要包含所有这些内容。值得注意的一点是 ID 没有被填充;通道连接器通常会为我们处理这些问题。

{
    "type": "message",
    "text": "echo: echo!",
    "locale": "en-US",
    "localTimestamp": "2018-02-22T22:03:41.136Z",
    "from": {
        "id": "8k53ghlggkl2jl0a3",
        "name": "Bot"
    },
    "recipient": {
        "id": "default-user",
        "name": "User"
    },
    "inputHint": "acceptingInput",
    "id": null,
    "replyToId": "50769feaaj9j"
}

我们还注意到input int字段的存在,它主要与语音助手系统相关,并且向消息平台指示麦克风的建议状态。例如,接受输入将指示用户可能对机器人消息做出响应,而期望输入将指示用户响应正在等待中。

最后,调试事件提供关于机器人如何执行请求的数据。

{
    "type": "event",
    "name": "debug",
    "value": [
        {
            "type": "log",
            "timestamp": 1519337020880,
            "level": "info",
            "msg": "UniversalBot(\"*\") routing \"echo!\" from \"emulator\"",
            "args": []
        },
        {
            "type": "log",
            "timestamp": 1519337020881,
            "level": "info",
            "msg": "Session.beginDialog(/)",
            "args": []
        },
        {
            "type": "log",
            "timestamp": 1519337020882,
            "level": "info",
            "msg": "waterfall() step 1 of 1",
            "args": []
        },
        {
            "type": "log",
            "timestamp": 1519337020882,
            "level": "info",
            "msg": "Session.send()",
            "args": []
        },
        {
            "type": "log",
            "timestamp": 1519337021136,
            "level": "info",
            "msg": "Session.sendBatch() sending 1 message(s)",
            "args": []
        }
    ],
    "relatesTo": {
        "id": "50769feaaj9j",
        "channelId": "emulator",
        "user": {
            "id": "default-user",
            "name": "User"
        },
        "conversation": {
            "id": "mf24ln43lde3"
        },
        "bot": {
            "id": "8k53ghlggkl2jl0a3",
            "name": "Bot"
        },
        "serviceUrl": "http://localhost:58462"
    },
    "text": "Debug Event",
    "localTimestamp": "2018-02-22T22:03:41.157Z",
    "from": {
        "id": "8k53ghlggkl2jl0a3",
        "name": "Bot"
    },
    "recipient": {
        "id": "default-user",
        "name": "User"
    },
    "id": null,
    "replyToId": "50769feaaj9j"
}

注意,这些值与 bot 控制台输出中显示的值相同。同样,如果我们不覆盖默认的 bot 状态,我们将会看到更多与废弃代码相关的数据。控制台输出如下所示:

UniversalBot("*") routing "echo!" from "emulator"
Session.beginDialog(/)
/ - waterfall() step 1 of 1
/ - Session.send()
/ - Session.sendBatch() sending 1 message(s)

该输出跟踪用户请求是如何执行的,以及它是如何遍历对话的。我们将在本章中进一步讨论这个问题。

如果我们使用模拟器发送更多的消息,我们会看到相同类型的输出,因为这个机器人非常简单。随着我们在卡片等特性上获得更多的经验,我们将从使用模拟器和进一步检查 JSON 消息中受益。该协议是 Bot 框架强大功能的重要组成部分:我们应该尽可能地熟悉它。

练习 5-1

连接到仿真器

检索 echo bot 代码,并使用 npm installnpm start 在本地运行它。下载模拟器并将其连接到机器人。

  1. 仔细检查请求/响应消息。

  2. 观察模拟器和机器人之间的行为。

  3. 探索模拟器。使用“设置”菜单创建新对话或向机器人发送系统活动消息。它有什么反应?你能写一些代码来处理这些消息吗?

在本练习结束时,您应该熟悉如何运行一个未经身份验证的本地 bot 并通过仿真器连接到它。

Bot 框架端到端设置

我们现在有了一个机器人。我们如何将它连接到所有这些不同的通道?Bot 框架使这变得简单。我们的目标是通过 Azure 门户向 bot 框架注册我们的 bot 及其端点,并向 Facebook Messenger 频道订阅 Bot。

有几件事我们必须做。首先,我们必须在 Azure 门户上创建一个 Azure Bot 服务注册。我们可能需要创建我们的第一个 Azure 订阅。这个设置的一部分是使用 ngrok 来允许从互联网访问这个机器人,所以我们应该确保我们已经从这里安装了 ngrok:https://ngrok.com/。最后,我们将把机器人部署到 Facebook Messenger。这意味着我们需要创建一个脸书页面,一个脸书应用,以及 Messenger 和 Webhook 集成,并将所有这些连接回机器人框架。有很多步骤,但是一旦我们熟悉了 Azure 和脸书的术语,就没那么麻烦了。我们将首先快速浏览说明,然后回头解释每一步都做了什么。

步骤 1:连接到 Azure

我们的第一步是登录 Azure 门户。如果你有 Azure 账户,那太好了。如果您已经订阅了 Azure,请直接跳到步骤 2。如果您没有,您可以前往 https://azure.microsoft.com/en-us/free/ 创建一个免费的开发者账户,拥有 200 美元的 30 天信用额度。

点击“免费开始”您需要使用 Microsoft 或工作帐户登录。如果你两者都没有,你可以在 https://account.microsoft.com/account 轻松创建一个微软账户。一旦你通过认证,你会看到如图 5-7 所示的页面。该页面将收集您的个人信息,并通过短信和有效信用卡验证您的身份。不要惊慌。信用卡是验证你的身份所必需的。机会是,你甚至不会接近使用 200 美元的信用,如果你这样做,你不会被收费;您将无法继续使用这些服务。我们在本书中使用 Azure 的大部分内容可以通过各种 Azure 服务的免费层来实现。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig7_HTML.jpg

图 5-7

Azure 注册页面

一旦这个过程完成,你就可以在 https://portal.azure.com 进入 Azure 门户。它看起来有点像图 5-8 。在右上角,您将看到您注册时使用的电子邮件地址和您的目录名。例如,如果我的电子邮件是szymon.rozga@aol.com(它不是),那么我的目录名将是 SZYMONROZGAAOL。如果您被添加到其他目录,该菜单将是一个下拉菜单,供您选择要导航到哪个目录。

Azure 帐户包含订阅。订阅是一个计费实体。如果我们导航到 https://portal.azure.com/#blade/Microsoft_Azure_Billing/SubscriptionsBlade ,或者门户中的订阅服务,并且我们刚刚创建了 200 美元的试用帐户,我们应该会看到一个名为“免费试用”的订阅。每个 Azure 订阅还可以包含一个或多个资源组。资源组是资源的逻辑容器,资源是单独的 Azure 服务。与每个资源组中的资源相关联的所有成本都根据与包含订阅相关联的付款方法来收取。有了 200 美元的试用账户,当综合费用达到支出限额时,服务会自动关闭。如果需要,免费帐户可以转换为付费帐户,从您的信用卡中收取额外费用(或其他支付方式)。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig8_HTML.jpg

图 5-8

空 Azure 门户

步骤 2:创建 Bot 注册

在 Azure 门户中,单击左上角窗格中的“创建资源”按钮。在搜索市场文本字段中,输入 azure bot 。你会得到很多结果,但我们感兴趣的是前三名(图 5-9 )。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig9_HTML.jpg

图 5-9

Azure bot 资源

这是三个选项:

  • web 应用 Bot :指向部署在 Azure 上的 Web 应用的 Bot 注册

  • Functions bot :指向作为 Azure 函数运行的 Bot 的 Bot 注册,Azure 的无服务器计算选项之一

  • Bot 通道注册:无云端后端的 Bot 注册

出于我们的目的,我们将创建一个 bot 通道注册 bot,因为我们将继续在我们的笔记本电脑上本地运行 Bot。单击 Bot 频道注册,然后单击创建。根据图 5-10 ,输入一个 bot 名称、将包含该注册的资源组的名称以及资源位置,即,将托管该注册的 Azure 区域。对于定价层,选择 F0;这是自由选择,足以满足我们的需求。暂时将消息传递端点保留为空,并将 Application Insights 选择为 on。Application Insights 是微软的云遥测和日志记录服务之一。bot 框架使用它来存储关于您的 Bot 注册使用的数据和分析。默认情况下,这将创建应用洞察的基本和免费层。选择尽可能靠近 Bot 通道注册位置的位置。准备就绪后,单击创建。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig10_HTML.jpg

图 5-10

创建新的 bot 通道注册

门户顶部有一个进度指示器,当注册准备就绪时,我们会收到通知。我们还可以通过使用左侧窗格上的资源组按钮导航到资源组(图 5-11 )。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig11_HTML.jpg

图 5-11

我们资源组中的资源

导航到 bot 通道注册,然后导航到设置刀片(图 5-12 )。请注意,Azure 自动填充了应用洞察标识符和密钥。这些将用于跟踪我们的机器人分析数据。我们将在第 13 章中看到其中一个分析仪表板。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig12_HTML.jpg

图 5-12

Bot 通道注册设置刀片

我们还会看到微软应用 ID。记下这个值。单击其正上方的管理链接,导航至 Microsoft 应用门户。这可能会再次要求我们的登录信息,因为它是一个独立于 Azure 的网站。一旦我们在应用列表中找到新创建的 bot,单击 Generate New Password(在 Application Secrets 部分)并保存值;你只能看一次!还记得我们在机器人控制台输出中看到了我们的机器人不安全的警告吗?我们现在将解决这个问题。

步骤 3:保护我们的机器人

在包含 echo bot 代码的目录中,创建一个名为。env 并提供 Microsoft 应用 ID 和密码:

# Bot Framework Credentials
MICROSOFT_APP_ID={ID HERE}
MICROSOFT_APP_PASSWORD={PASSWORD HERE}

关闭并重新启动 bot (npm start)。

如果我们现在尝试从模拟器连接,模拟器将显示以下日志消息:

[08:00:16] -> POST 401 [conversationUpdate]
[08:00:16] Error: The bot's MSA appId or password is incorrect.
[08:00:16] Edit your bot's MSA info

bot 控制台输出将包含以下消息:

ERROR: ChatConnector: receive - no security token sent.

现在看起来安全多了,对吧?我们必须在模拟器端输入相同的应用 ID 和密码。单击“编辑我们的机器人的 MSA(微软帐户)信息”链接,并将数据输入模拟器。如果我们现在尝试使用模拟器连接,它会工作得很好。在继续之前向机器人发送消息进行确认。

步骤 4:设置远程访问

我们可以将机器人部署到 Azure,将脸书连接器连接到该端点,然后就到此为止。但是我们如何开发或调试脸书特有的功能呢?bot 框架方式是运行 bot 的本地实例,并将测试脸书页面连接到本地 Bot 进行开发。

为此,请从命令行运行 ngrok。

ngrok http 3978

我们将看到图 5-13 中的数据。默认情况下,ngrok 会分配一个随机的子域(付费的 ngrok 版本允许您指定一个域名)。在这种情况下,我的网址是 https://cc6c5d5f.ngrok.io 。注意,ngrok 的免费版本在我们每次运行它时都会提供一个随机的子域。我们可以通过升级到付费版本或者简单地尽可能长时间保留 ngrok 会话来解决这个问题。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig13_HTML.jpg

图 5-13

Ngrok 将 HTTP/HTTPS 请求转发到我们的本地 bot

让我们看看这是否有效。在模拟器中,输入 ngrok URL,后跟 /api/messages 。例如,对于前面的 URL,正确的消息端点是 https://cc6c5d5f.ngrok.io/api/messages 。将应用 ID 和应用密码信息添加到模拟器中。单击 Connect 后,模拟器应该会成功连接到 bot 并与之聊天。

现在,在 Bot 通道注册设置刀片中分配相同的消息传递端点 URL,图 5-12 。接下来,使用 Web Chat blade 导航到测试,并尝试向机器人发送消息。应该能行。您已经将第一个频道连接到您的机器人(图 5-14 )!

img/455925_1_En_5_Chapter/455925_1_En_5_Fig14_HTML.jpg

图 5-14

有效!我们的机器人连接到我们的第一个频道!

步骤 5:连接到 Facebook Messenger

很酷,对吧?bot 框架几乎完全与我们的 Bot 集成在一起。我们现在将继续整合我们的机器人与 Facebook Messenger。Bot 通道注册上的通道刀片使我们能够连接到微软支持的通道(图 5-15 )。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig15_HTML.jpg

图 5-15

频道仪表板

点击 Facebook Messenger 按钮,进入 Messenger 配置界面(图 5-16 )。我们将需要从脸书获得四条数据:页面 ID、应用 ID、应用秘密和页面访问令牌。最后,我们应该注意回调 URL 和验证令牌。我们将需要这些建立脸书和 Bot 框架之间的连接。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig16_HTML.jpg

图 5-16

Facebook Messenger Bot 框架连接器设置

现在让我们设置必要的脸书资产。我们必须有一个脸书帐户来完成以下任务。导航到 Facebook.com,使用右上角的下拉菜单创建一个新页面(图 5-17 )。脸书将询问页面的类型。出于本例的目的,我们可以选择品牌/产品类型和应用页面子类别。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig17_HTML.jpg

图 5-17

创建新的脸书页面

我创建了一个名为 Szymon 测试页面的页面。我们可以通过点击左侧导航窗格上的“关于”链接来找到页面 ID(图 5-18 )。在最底部,我们会找到页面 ID。我们需要将该值复制到 Bot 框架 Facebook Messenger 频道配置表单中(图 5-16 )。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig18_HTML.jpg

图 5-18

脸书页面关于页面,包括页面 ID

接下来,在新的浏览器标签或窗口中,导航至 https://developers.facebook.com 。如果您还没有注册,请注册一个开发者帐户。创建一个新的应用(图 5-19 )。给它起任何你喜欢的名字。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig19_HTML.jpg

图 5-19

创建新的脸书应用

完成后,通过左侧边栏菜单导航到设置➤基本页面,并将 Facebook 应用 ID 和应用秘密复制到 Bot 框架表单中(图 5-20 )。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig20_HTML.jpg

图 5-20

应用 ID 和应用密码

接下来,导航到仪表板(从左侧栏的链接)并设置 Messenger 产品。向下滚动页面,直到到达令牌生成部分。在令牌生成部分选择页面,生成页面访问令牌(图 5-21 )。将令牌复制到 Azure 门户内的 Bot 框架表单中。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig21_HTML.jpg

图 5-21

生成页面访问令牌

接下来,滚动到 Webhooks 部分(就在脸书应用仪表板的令牌生成部分下方),然后单击设置 Webhooks。您将看到一个弹出窗口,要求您输入回拨 URL 和验证令牌。从 Azure 门户的配置 Facebook Messenger 表单中复制并粘贴这两个内容。

在订阅字段部分,选择以下字段:

  • 信息

  • 消息 _ 交付

  • 消息 _ 阅读

  • 消息传递 _ 回发

  • 消息传递 _ 选项

  • 消息 _ 回应

单击验证并保存。最后,从下拉列表中选择您希望 bot 订阅的页面,然后单击 subscribe。您的设置页面应该如图 5-22 所示。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig22_HTML.jpg

图 5-22

订阅我们测试页面上的消息

确保保存 Bot 框架配置。就这样!您可以在 Messenger 联系人中找到该页面。你可以给它发送一条信息,你应该得到它的回应(图 5-23 )。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig23_HTML.jpg

图 5-23

在 Messenger 中工作的回声机器人

步骤 6:部署到 Azure

如果我们不将代码部署到云中,这就不是一个完整的教程。我们将创建一个 web 应用,并使用 Kudu ZipDeploy 部署 Node.js 应用。最后,我们将把 bot 通道注册指向 web 应用。

进入我们在步骤 2 中创建的 Azure 资源组,并创建一个新资源。搜索 web app 。选择 Web App 而不是 Web App Bot。Web 应用 bot 是 Bot 通道注册和应用服务的组合。我们不需要这个组合,因为我们已经创建了一个 bot 通道注册。

创建 web 应用时,我们需要给它一个名称。还要确保选择了正确的资源组(图 5-24 )。Azure 会将其添加到我们现有的资源组中,并为我们创建一个新的应用服务计划。应用服务计划是 web 应用和类似计算资源的容器;它定义了我们的应用运行的硬件以及成本。在图 5-24 中,我们创建了一个新的应用服务计划,并选择了免费定价等级。免费很好。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig24_HTML.jpg

图 5-24

创建新的应用服务和应用服务计划

在部署我们的 echo bot 之前,我们需要添加两件事情。首先,我们向基本 URL 端点添加一个响应,以验证我们的 bot 是否已部署。将此代码添加到 app.js 文件的末尾:

server.get('/', (req, res, next) => {
    res.send(200, { "success": true });
    next();
});

其次,对于基于 Windows 的 Azure 设置,我们还需要包含一个自定义的 web.config 文件来告诉互联网信息服务(IIS) 2 如何运行 Node 应用。 3

<?xml version="1.0" encoding="utf-8"?
<!--
     This configuration file is required if iisnode is used to run node processes behind
     IIS or IIS Express.  For more information, visit:

     https://github.com/tjanczuk/iisnode/blob/master/src/samples/configuration/web.config
-->

<configuration>
  <system.webServer>
    <!-- Visit http://blogs.msdn.com/b/windowsazure/archive/2013/11/14/introduction-to-websockets-on-windows-azure-web-sites.aspx for more information on WebSocket support -->
    <webSocket enabled="false" />
    <handlers>
      <!-- Indicates that the server.js file is a node.js site to be handled by the iisnode module -->
      <add name="iisnode" path="app.js" verb="*" modules="iisnode"/>
    </handlers>
    <rewrite>
      <rules>
        <!-- Do not interfere with requests for node-inspector debugging -->
        <rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true">
          <match url="^app.js\/debug[\/]?" />
        </rule>

        <!-- First we consider whether the incoming URL matches a physical file in the /public folder -->
        <rule name="StaticContent">
          <action type="Rewrite" url="public{REQUEST_URI}"/>
        </rule>

        <!-- All other URLs are mapped to the node.js site entry point -->
        <rule name="DynamicContent">
          <conditions>
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True"/>
          </conditions>
          <action type="Rewrite" url="app.js"/>
        </rule>
      </rules>
    </rewrite>

    <!-- 'bin' directory has no special meaning in node.js and apps can be placed in it -->
    <security>
      <requestFiltering>
        <hiddenSegments>
          <remove segment="bin"/>
        </hiddenSegments>
      </requestFiltering>
    </security>

    <!-- Make sure error responses are left untouched -->
    <httpErrors existingResponse="PassThrough" />

    <!--
      You can control how Node is hosted within IIS using the following options:
        * watchedFiles: semi-colon separated list of files that will be watched for changes to restart the server
        * node_env: will be propagated to node as NODE_ENV environment variable
        * debuggingEnabled - controls whether the built-in debugger is enabled

      See https://github.com/tjanczuk/iisnode/blob/master/src/samples/configuration/web.config for a full list of options
    -->
    <!--<iisnode watchedFiles="web.config;*.js"/>-->
  </system.webServer>
</configuration>

接下来,我们通过浏览器访问我们的 bot web 应用。在我的例子中,我导航到 https://srozga-test-bot-23.azurewebsites.net 。将有一个默认的“您的应用服务应用已启动并正在运行”页面。在我们部署之前,我们必须压缩 echo bot 以传输到 Azure。我们压缩所有的应用文件,包括 Node 模块目录。我们可以使用以下命令:

# Bash
zip -r echo-bot.zip .

# PowerShell
Compress-Archive -Path * -DestinationPath echo-bot.zip

现在我们有了一个 zip 文件,关于如何部署我们有两个选项。在选项 1 中,我们使用命令行,通过在https://{WEB_APP_NAME}.scm.azurewebsites.net使用 Kudu 4 端点来部署 bot。为此,我们必须首先访问应用服务中的部署凭证刀片(图 5-25 )来设置部署用户名和密码组合。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig25_HTML.jpg

图 5-25

设置部署凭据

一旦完成,我们就可以开始了。运行以下 curl 命令将启动部署过程:

curl -v POST -u srozga321 --data-binary @echo-bot.zip https://srozga-test-bot-23.scm.azurewebsites.net/api/zipdeploy

一旦你运行了这个,curl 会要求你输入密码,如图 5-25 所示。它将上传 zip 并在应用服务上设置应用。完成后,向您的应用的基本 URL 发出请求,您应该会看到一个 200 的响应,成功设置为 true。

$ curl -X GET https://srozga-test-bot-23.azurewebsites.net
{"success":true}

另一种部署方式是使用 SCM 网站上的 Kudu 接口: https://srozga-test-bot-23.scm.azurewebsites.net/ZipDeploy 。您可以简单地将 zip 文件拖放到图 5-26 中的文件列表上。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig26_HTML.jpg

图 5-26

Kudu ZipDeploy 用户界面

还有一步。进入 Bot Channels 注册条目中的 Settings blade,将 messages endpoint 设置为您的新应用服务(图 5-27 )。确保点击保存按钮。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig27_HTML.jpg

图 5-27

消息传递端点的最终更新

在网上聊天和 Messenger 上随心所欲地保存和测试。恭喜你!我们完成了很多!我们现在有一个运行在 Azure 上的机器人,使用 Node.js 和微软机器人框架与网络聊天和 Facebook Messenger 对话。接下来,我们将深入描述我们刚刚完成的任务。

我们刚刚做了什么?

在上一节中,我们经历了很多。在注册和创建一个 bot、建立到脸书的连接以及部署到 Azure 方面,有许多移动部分。许多这些动作只需要执行一次,但是作为一个 bot 开发者,你应该对不同的系统、它们如何相互连接以及它们如何设置有一个坚实的理解。

Microsoft Azure

微软 Azure 是微软的云平台。有许多类型的资源,从基础设施即服务到平台即服务,甚至软件即服务。我们可以像创建新的应用服务一样轻松地调配新的虚拟机。我们可以使用 Azure PowerShell、Azure CLI(或云 Shell)、Azure 门户(如我们在示例中所做的)或 Azure 资源管理器来创建、修改和编辑资源。这些细节超出了本书的范围,我们建议您参考 Microsoft 在线文档以获取更多信息。

Bot 频道注册条目

当我们创建 bot 通道注册时,我们创建了一个全局注册,所有的通道连接器都可以使用它来识别、验证和与我们的 bot 通信。每个连接器,无论是与 Messenger、Slack、Web Chat 还是 Skype 通信,都知道我们的机器人、其微软应用 ID/密码、消息端点和其他设置(图 5-28 )。bot 通道注册是 Bot 框架 Bot 的起点。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig28_HTML.jpg

图 5-28

概念 Bot 框架体系结构

我们跳过了 Azure 中另外两种类型的 bot 资源:Web App Bot 和 Functions Bot。一个 Web 应用机器人正是我们刚刚设置的;我们提供一个服务器来运行机器人应用。Azure Functions 是 Azure 实现无服务器计算的方法之一。它允许我们在云环境中托管不同的代码或功能,以便按需运行。我们只为我们使用的资源付费。Azure 基于负载动态扩展基础设施。函数是 bot 开发的一种非常有效的方法。对于更复杂的场景,我们需要小心设计向外扩展和多服务器部署的功能代码。出于本书的目的,我们不使用功能机器人。然而,我们建议您尝试这个主题,因为无服务器计算正变得越来越突出。

证明

我们如何确保只有授权的通道连接器或应用才能与我们的 bot 通信?这就是微软应用 ID 和应用密码的用武之地。当连接器向我们的 bot 发送消息时,它会在 HTTP 授权头中包含一个令牌。我们的机器人必须验证这个令牌。当我们的 bot 向连接器发送传出消息时,我们的 bot 必须从 Azure 检索有效的令牌,否则连接器将拒绝该消息。

Bot Builder SDK 提供了所有代码,因此这个过程对开发人员是透明的。Bot 框架文档详细描述了两个流程中的步骤: https://docs.microsoft.com/en-us/bot-framework/rest-api/bot-framework-rest-connector-authentication

连通性和 Ngrok

虽然 ngrok 不是 Bot 框架的一部分,但它是我们工具集不可或缺的一部分。Ngrok 是一个反向代理,通过 ngrok.io 上的外部可访问子域将所有请求隧道传输到我们计算机上的一个端口。免费版每次运行时都会创建一个新的随机子域;专业版允许我们有一个静态子域。Ngrok 还公开了一个 HTTPS 端点,这使得本地开发设置变得轻而易举。

通常,我们不会遇到 Ngrok 的任何问题。如果我们的 ngrok 配置正确,任何问题都可以缩小到外部服务或我们的机器人。

部署到 Facebook Messenger

每个平台都是不同的,但我们对脸书的机器人复杂性有所了解。首先,脸书用户使用脸书页面与品牌和公司互动。页面上的用户请求通常由对页面有足够访问权限的人来响应,以便通过页面的收件箱进行查看和响应。有许多企业实时聊天系统连接到脸书页面,并允许一组客户服务代表实时响应用户的查询。有了机器人框架的 Facebook Messenger 连接器,我们现在可以让机器人响应这些查询。我们将在第 13 章中讨论机器人将对话移交给代理人的想法,称为人工移交

脸书上的一个机器人是一个脸书应用,它通过网络钩子订阅进入脸书页面的消息。我们注册了 Bot 框架 web hook 端点,当消息进入我们的脸书页面时,脸书会调用该端点。bot 通道注册页面还提供了验证令牌,脸书使用它来确保连接到正确的 web 钩子。Azure 的 Bot 连接器需要知道脸书应用 ID 和应用秘密,以验证每个传入消息的签名。我们需要页面访问令牌在与页面聊天时向用户发回消息。我们可以在脸书的文档页面中找到更多关于脸书的 SendAPI 和 Messenger Webhooks 的细节: https://developers.facebook.com/docs/messenger-platform/reference/send-api/https://developers.facebook.com/docs/messenger-platform/webhook/

一旦所有这些都准备就绪,消息就可以很容易地在脸书和我们的机器人之间传递。虽然脸书有一些独特的概念,如页面访问令牌和 webhook 类型的特定名称,但我们所做的事情背后的总体想法与其他通道类似。通常,我们将在平台上创建一个应用,并在该应用和 Bot 框架端点之间建立联系。将消息转发给我们是 Bot 框架的角色。

部署到 Azure

有很多方法可以将代码部署到 Azure。我们使用的工具 Kudu 允许我们通过 REST API 进行部署。Kudu 也可以配置为从 git repo 或其他位置部署。还有其他工具可以简化部署。如果我们要使用微软的 Visual Studio 或 Visual Studio 代码编写一个机器人,有一些扩展允许我们轻松地将代码部署到 Azure 中。同样,这是一个超出本书范围的话题。为了在 Linux 应用服务上运行 Node.js bot,使用 ZipDeploy REST API 就足够了。

因为我们可以通过使用模拟器在本地开发我们的 bot,并通过运行 ngrok 在各种通道上测试本地 bot,所以在本书的其余部分我们不再部署到 Azure。如有必要,关闭 web 应用实例,这样就不会对订阅收费。确保删除 app 服务计划;简单地停止 web 应用是行不通的。

关键 Bot Builder SDK 概念

通过模拟器和 Facebook Messenger 完成运行机器人的细节感觉很好,但是机器人没有做任何有用的事情!在本节中,我们将深入研究 Node.js 库的 Bot Builder SDK。这是本章剩余部分和下一章的重点。现在,我们将讨论 Bot Builder SDK 的四个基本概念。之后,我们展示了一个日历机器人对话的框架代码,它是基于第三章第一节中 NLU 对路易斯的研究。这个机器人将知道如何与用户谈论许多日历任务,但还不会与任何 API 集成。这是一种常见的方法,用来演示对话流以及它是如何工作的,而不需要经历整个后端集成工作。让我们开始吧。

会话和消息

Session 是一个对象,它表示当前会话以及可以在其上调用的操作。在最基本的层面上,我们可以使用会话对象来发送消息。

const bot = new builder.UniversalBot(connector, [
    session => {
        // for every message, send back the text prepended by echo:
        session.send('echo: ' + session.message.text);
    }
]);

邮件可以包括图像、视频、文件和自定义附件类型。图 5-29 显示了结果信息。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig29_HTML.jpg

图 5-29

发送图像

session => {
    session.send({
        text: 'hello',
        attachments: [{
            contentType: 'image/png',
            contentUrl: 'https://upload.wikimedia.org/wikipedia/commons/b/ba/New_York-Style_Pizza.png',
            name: 'image'
        }]
    });
}

我们也可以送一张英雄卡。英雄卡是独立的容器,包括图像、标题、副标题、文本以及可选的按钮列表。图 5-30 显示了结果交换。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig30_HTML.jpg

图 5-30

比萨饼转盘样品

let msg = new builder.Message(session);
msg.text = 'Pizzas!';
msg.attachmentLayout(builder.AttachmentLayout.carousel);
msg.attachments([
    new builder.HeroCard(session)
        .title('New York Style Pizza')
        .subtitle('the best')
        .text("Really, the best pizza in the world.")
        .images([builder.CardImage.create(session, 'https://upload.wikimedia.org/wikipedia/commons/b/ba/New_York-Style_Pizza.png')])
        .buttons([
            builder.CardAction.imBack(session, "I love New York Style Pizza!", "LOVE THIS")
        ]),
    new builder.HeroCard(session)
        .title('Chicago Style Pizza')
        .subtitle('not bad')
        .text("some people don't believe this is pizza.")
        .images([builder.CardImage.create(session, 'https://upload.wikimedia.org/wikipedia/commons/3/33/Ginoseastdeepdish.jpg')])
        .buttons([
            builder.CardAction.imBack(session, "I love Chicago Style Pizza!", "LOVE THIS")
        ]),
]);

session.send(msg);

这个例子引入了一些新概念。英雄卡只是 Bot Builder SDK 支持的一种卡。以下是其他受支持的卡:

  • 适配卡:柔性卡,包含容器、按钮、输入域、语音、文本、图像等项目的组合;并非所有频道都支持。我们将在第 11 章中深入探讨适应卡。

  • 动画卡:支持动画 gif 或短视频的卡。

  • 声卡:播放音频的卡。

  • 缩略图卡:类似英雄卡,但图像尺寸更小。

  • 收据卡:呈现一张收据,包括描述、税、合计等常见行项目。

  • 签到卡:发起签到流程的卡片。

  • 显卡:播放视频的卡。

另一个有趣的地方是附件布局。默认情况下,附件在垂直列表中发送。我们选择使用 carousel,一个可滚动的水平列表,为用户提供更好的体验。

此代码中的按钮使用 IM Back 操作。这将发送按钮的值字段(“我喜欢纽约风格的披萨!”或者“我爱芝加哥风格的披萨!”)作为当点击“爱这个”按钮时给机器人的文本消息。其他动作类型如下所述。每个消息传递平台对这些类型都有不同级别的支持。

  • 回发:和 IM back 一样,但是用户看不到消息。

  • openUrl :在浏览器中打开一个 Url。这可以是桌面上的默认浏览器或应用内的 web 视图。

  • 通话:拨打电话号码。

  • downloadFile :下载文件到用户设备。

  • playAudio :播放音频文件。

  • 播放视频:播放视频文件。

  • showImage :在图像浏览器中显示图像。

我们还可以使用 Session 对象在支持书面和口头响应的通道中发送语音同意。我们既可以像在 carousel hero card 示例中那样构建一个消息对象,也可以在会话中使用一种方便的方法。以下代码片段中的输入提示告诉用户界面机器人是在等待响应、接受输入还是根本不接受输入。对于有语音助手技能开发背景的开发者来说,就像对于亚马逊的 Alexa 来说,这应该是一个熟悉的概念。

const bot = new builder.UniversalBot(connector, [
    session => {
        session.say('this is just text that the user will see', 'hello', { inputHint: builder.InputHint.acceptingInput});
    }
]);

会话也是帮助我们访问相关用户对话数据的对象。例如,我们可以将用户发送给机器人的最后一条消息存储在会话的privateconversiondata中,并在以后的会话中使用它,如下例所示(图 5-31 ):

img/455925_1_En_5_Chapter/455925_1_En_5_Fig31_HTML.jpg

图 5-31

存储消息之间的会话数据

session => {
    var lastMsg = session.privateConversationData.last;
    session.privateConversationData.last = session.message.text;
    if(lastMsg) {
        session.send(lastMsg);
    } else {
        session.send('i am memorizing what you are saying');
    }
}

Bot Builder SDK 使得在会话对象中存储三种类型的数据变得很容易。

  • privateconversiondata:会话范围内的私有用户数据

  • conversationData :对话的数据,在参与对话的所有用户之间共享

  • userData :用户在一个频道上所有对话的数据

默认情况下,这些对象都存储在内存中,但是我们可以很容易地提供一个替代的存储服务实现。我们将在第 6 章看到一个例子。

瀑布和提示

一个瀑布是在一个机器人上处理输入消息的一系列函数。通用 Bot 构造函数将一组函数作为参数。这是瀑布。Bot Builder SDK 连续调用每个函数,将上一步的结果传递给当前步骤。这种方法最常见的用途是使用提示符向用户询问更多信息。在下面的代码中,我们使用文本提示,但是 Bot Builder SDK 支持数字、日期或多选等输入(图 5-32 )。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig32_HTML.jpg

图 5-32

基本瀑布样本

const bot = new builder.UniversalBot(connector, [
    session => {
        session.send('echo 1: ' + session.message.text);
        builder.Prompts.text(session, 'enter for another echo!');
    },
    (session, results) => {
        session.send('echo 2: ' + results.response);
    }
]);

我们也可以使用下一个函数手动推进瀑布,在这种情况下,机器人不会等待额外的输入(图 5-33 )。这在第一步可能有条件地要求额外输入的情况下很有用。我们将在日历机器人代码中使用它。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig33_HTML.jpg

图 5-33

计划性瀑布前进

const bot = new builder.UniversalBot(connector, [
    (session, args, next) => {
        session.send('echo 1: ' + session.message.text);
        next({response: 'again!'});
    },
    (session, results, next) => {
        session.send('echo 2: ' + results.response);
    }
]);

下面是一个更复杂的数据收集瀑布:

const bot = new builder.UniversalBot(connector, [
    session => {
        builder.Prompts.choice(session, "What do you want to do?", "add appointment|delete appointment", builder.ListStyle.button);
    },
    (session, results) => {
        session.privateConversationData.action = { type: results.response.index };
        builder.Prompts.time(session, "when?");
    },
    (session, results, next) => {
        session.privateConversationData.action.datetime = results.response.resolution.start;
        if (session.privateConversationData.action.type == 0) {
            builder.Prompts.text(session, "where?");
        } else {
            next({ response: null });
        }
    },
    (session, results, next) => {
        session.privateConversationData.action.location = results.response;

        let summary = null;
        const dt = moment(session.privateConversationData.action.datetime).format('M/D/YYYY h:mm:ss a');

        if (session.privateConversationData.action.type ==  0) {
            summary = 'Add Appointment ' + dt + ' at location ' + session.privateConversationData.action.location;
        } else {
            summary  = 'Delete appointment  ' + dt;
        }

        const action = session.privateConversationData.action;
        // do something with action
        session.endConversation(summary);
    }
]);

在这个示例中,我们使用了更多类型的提示:选择和时间。选择提示要求用户选择一个选项。提示可以使用内嵌文本(例如,与 SMS 场景相关)或按钮来呈现选择。时间提示使用 chronos Node.js 库将日期时间的字符串表示解析为日期时间对象。像“明天下午 5 点”这样的输入可以解析为计算机可以使用的值。

注意,我们使用逻辑来跳过某些瀑布步骤。具体来说,如果我们在删除约会分支中,我们不需要事件位置。因此,我们甚至不要求它。我们利用privateconversiondata对象来存储动作对象,该对象表示我们想要针对 API 调用的操作。最后,我们使用 session.endConversation 方法来结束对话。这个方法将清除用户的状态,这样下次用户与机器人交互时,机器人就好像看到了一个新用户。

5-34 显示了结果对话。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig34_HTML.jpg

图 5-34

数据收集瀑布

对话

让我们用对话式设计来实现这个完整的循环。在第 4 章中,我们讨论了如何使用我们称之为对话的 Node 图来建立对话模型。到目前为止,在这一章中,我们已经学习了瀑布,以及如何用代码建立对话模型。

我们还学会了如何利用提示从用户那里收集数据。回想一下,提示是从用户那里收集数据的简单机制。

builder.Prompts.text(session, "where?");

提示很有意思。我们称函数为( builder。Prompts.text ),将对话转换为提示。一旦用户发送了有效的响应,瀑布中的下一步就可以访问提示的结果。图 5-35 显示了整个过程。从我们的瀑布的角度来看,我们并不真正知道 Prompts.choice 调用在做什么,我们也不关心。它监听用户输入,进行一些验证,重新提示错误的输入,并且只返回一个有效的结果,除非用户取消。所有这些逻辑对我们来说都是隐藏的。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig35_HTML.jpg

图 5-35

对话之间概念上的控制转移

这种交互与编程函数调用的模型相同。通常实现函数调用的方式是使用堆栈。检查图 5-36 和以下代码:

function f(a,b) { return a + b; }

当函数 f 被调用时,函数的参数被压入栈顶。然后,该函数的代码处理堆栈。在此示例中,函数添加参数。最后,留在堆栈顶部的唯一值是函数的返回值。然后,调用函数可以对返回值做任何它想做的事情。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig36_HTML.jpg

图 5-36

堆栈上的函数调用

这是对话中提示的工作方式。Bot Builder SDK 中的一般概念是一个对话框。提示是一种对话框。对话只不过是对对话逻辑的封装,类似于函数调用。用一些参数初始化一个对话框。它接收来自用户的输入,执行自己的代码或调用其他对话框,并可以向用户发送响应。一旦对话框的目的实现了,它就向调用对话框返回值。简而言之,调用对话框将子对话框推到堆栈的顶部。当子对话框完成时,它会从堆栈中弹出自己。

让我们回到我们选择提示的例子。在对话框堆栈模型中,根对话框放置提示。选择堆栈顶部的对话框。在对话框完成执行后,产生的用户输入对象被向下传递回根对话框。然后根对话框对结果对象做任何它需要做的事情。图 5-37 记录了随时间变化的行为。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig37_HTML.jpg

图 5-37

一段时间内对话框堆栈上的对话框

我们可以进一步发展这个概念。我们可以想象日历机器人中的一个流程,其中添加一个新的日历条目会调用一个新的对话框。姑且称之为 AddCalendarEntry 。然后它会调用一个提示。Time 对话框收集事件的日期和时间,并调用一个提示。Text 对话框收集事件的主题。AddCalendarEntry 打包收集的数据,并通过调用一些日历 API 创建一个新的日历条目。控制权然后返回到根对话框。我们在图 5-32 中对此进行了说明。我们甚至可以让 AddCalendarEntry 调用另一个对话框,该对话框封装了调用 API 的逻辑,如果该过程足够复杂,并且我们想要重用来自其他对话框的逻辑(图 5-38 )。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig38_HTML.jpg

图 5-38

一个更复杂的对话框堆栈,随着时间的推移而显示

瀑布和对话框是将对话设计转化为实际工作代码的主力。当然,还有更多的细节,我们将在下一章中讨论,但这是 Bot Builder SDK 背后的魔力。它的关键价值是一个引擎,可以使用对话抽象来驱动对话。在对话过程中的每一点,都会存储对话堆栈以及支持用户和对话数据。这意味着,根据对话的存储实现,用户可能会停止与机器人交谈几天,然后回来,机器人可以从用户停止的地方继续。

我们如何应用这些概念?重新查看添加和删除约会瀑布示例,我们可以创建一个 bot,它根据选择提示启动两个对话框中的一个:一个添加日历条目,另一个删除它。这些对话框有所有必要的逻辑来判断添加或删除哪个约会、解决冲突、提示用户确认等等。

const bot = new builder.UniversalBot(connector, [
    session => {
        builder.Prompts.choice(session, "What do you want to do?", "add appointment|delete appointment", builder.ListStyle.button);
    },
    (session, results) => {
        if (results.response.index == 0) {
            session.beginDialog('AddCalendarEntry');
        } else if (results.response.index == 1) {
            session.beginDialog('RemoveCalendarEntry');
        }
    },
    (session, results) => {
        session.send('excellent! we are done!');
    }
]);

bot.dialog('AddCalendarEntry', [
    (session, args) => {
        builder.Prompts.time(session, 'When should the appointment be added?');
    },
    (session, results) => {
        session.dialogData.time = results.response.resolution.start;
        builder.Prompts.text(session, 'What is the meeting subject?');
    },
    (session, results) => {
        session.dialogData.subject = results.response;
        builder.Prompts.text(session, 'Where should the meeting take place?');
    },
    (session, results) => {
        session.dialogData.location = results.response;

        // TODO: take the data and call an API to add the calendar entry

        session.endDialog('Your appointment has been added!');
    }]);
bot.dialog('RemoveCalendarEntry', [
    (session, args) => {
        builder.Prompts.time(session, 'Which time do you want to clear?');
    },
    (session, results) => {
        var time = results.response.resolution.start;
        // TODO: find the relevant appointment, resolve conflicts, confirm prompt, and delete
        session.endDialog('Your appointment has been removed!');
    }]);

我们通过调用 session.beginDialog 方法启动一个新的对话框,并传入对话框名称。我们还可以传递一个可选的参数对象,它可以通过被调用对话框中的 args 参数来访问。我们使用 session.dialogData 对象来存储对话状态。我们之前遇到过用户数据私有会话数据会话数据。这些都是整个对话的范围。然而,DialogData 的作用范围仅限于当前对话框实例的生存期。为了结束对话,我们调用 session.endDialog 。这将控制返回到根瀑布的下一步。有一个名为 session.endDialogWithResult 的方法允许我们将数据传递回调用对话框。

Messenger 中的对话最终看起来如图 5-39 所示。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig39_HTML.jpg

图 5-39

AddCalendarEntry 对话框实现的演示

这段代码有一些缺点。首先,如果我们想取消添加或删除约会,没有办法做到这一点。第二,如果我们正在添加一个约会,并决定要删除一个约会,我们不能轻易切换到删除约会对话框。我们必须完成当前对话,然后切换。第三,但不是必不可少的,将机器人连接到我们的 LUIS 模型会很好,这样用户就可以使用自然语言与机器人进行交互。我们接下来将解决前两点,然后连接到我们的 LUIS 模型,以真正在机器人中建立一些智能。

调用对话框

让我们继续下面的练习。假设我们希望允许用户在对话的任何时候寻求帮助;这是一个典型的场景。有时,帮助会与对话框相关联。在其他时候,帮助将是一个全局操作,一个可以从对话中的任何地方访问的机器人行为。Bot Builder SDK 允许我们在对话框中插入这两种类型的行为。

我们引入一个简单的帮助对话框。

bot.dialog('help', (session, args, next) => {
    session.endDialog("Hi, I am a calendar concierge bot. I can help you make and cancel appointments!");
})
.triggerAction({
    matches: /^help$/i
});

这段代码定义了一个新的对话框,它带有一个与“help”输入相匹配的全局操作处理程序。 TriggerAction 定义一个全局动作。我们说,只要用户的输入与正则表达式^help$.匹配,就会全局触发帮助对话框^字符表示一行的开始,$字符表示一行的结束。然而,一个问题出现了。正如我们在图 5-40 中看到的,看起来好像当我们寻求帮助时,我们的机器人忘记了我们在添加约会对话框中。事实上,全局动作匹配的默认行为是替换堆栈顶部的对话框。换句话说,添加约会对话框被移除,并被帮助对话框所取代。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig40_HTML.jpg

图 5-40

帮助取消上一个对话框。不好。

我们可以通过实现 onSelectAction 回调来覆盖这种行为。

bot.dialog('help', (session, args, next) => {
    session.endDialog("Hi, I am a calendar concierge bot. I can help you make and cancel appointments!");
})
.triggerAction({
    matches: /^help$/i,
    onSelectAction: (session, args, next) => {
        session.beginDialog(args.action, args);
    }
});

这带来了一个有趣的问题:我们如何影响对话框堆栈?当我们正在处理一个对话流,并且想要将控制转移到另一个对话时,我们可以使用 beginDialog 或者 replaceDialog 。replaceDialog 替换堆栈顶部的对话框,beginDialog 将一个对话框推到堆栈顶部。该会话还有一个名为 reset 的方法,用于重置整个对话框堆栈。默认行为是重置堆栈并将新对话框推至顶部。

如果我们想包含上下文帮助呢?让我们创建一个新的对话框来处理添加日历条目对话框的帮助。我们可以在一个对话框上使用 beginDialogAction 方法来定义在 AddCalendarEntry 对话框上启动新对话框的触发器。

bot.dialog('AddCalendarEntry', [
    ...
])
    .beginDialogAction('AddCalendarEntryHelp', 'AddCalendarEntryHelp', { matches: /^help$/ });
bot.dialog('AddCalendarEntryHelp', (session, args, next) => {
    let msg = "Add Calendar Entry Help: we need the time of the meeting, the subject and the location to create a new appointment for you.";
    session.endDialog(msg);
});

当我们运行这个时,我们得到了想要的效果,如图 5-41 所示。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig41_HTML.jpg

图 5-41

正确处理上下文动作

我们将在下一章更深入地探讨动作和它们的行为。

识别器

回想一下,我们定义了帮助对话框将通过正则表达式触发。Bot Builder SDK 如何实现这一点?这就是识别器的用武之地。识别器是一段代码,它接受传入的消息并确定用户的意图。识别器返回意图名称和分数。意图和得分可以来自像路易斯这样的 NLU 服务,但他们不一定要这样做。

默认情况下,如前面的例子所示,我们的 bot 中唯一的识别器是正则表达式或纯文本匹配器。它接受一个正则表达式或硬编码的字符串,并将其与传入消息的文本进行匹配。我们可以通过向 bot 的识别器列表中添加一个 RegExpRecognizer 来使用这个识别器的显式版本。下面的实现声明,如果用户的输入与提供的正则表达式匹配,则名为 HelpIntent 的意图以 1.0 的分数被解析。否则得分为 0.0。

bot.recognizer(new builder.RegExpRecognizer('HelpIntent', /^help$/i));

bot.dialog('help', (session, args, next) => {
    session.endDialog("Hi, I am a calendar concierge bot. I can help you make and cancel appointments!");
})
    .triggerAction({
        matches: 'HelpIntent',
        onSelectAction: (session, args, next) => {
            session.beginDialog(args.action, args);
        }
    });

识别器模型允许我们做的另一件事是创建一个定制的识别器,它执行我们想要的任何代码,并用一个分数来解析一个意图。这里有一个例子:

bot.recognizer({
    recognize: (context, done) => {
        var intent = { score: 0.0 };

        if (context.message.text) {
            if (context.message.text.toLowerCase().startsWith('help')) intent = { score: 1.0, intent: 'HelpIntent' };
        }
        done(null, intent);
    }
});

这是一个非常简单的例子,但是我们的思维应该与可能性赛跑。例如,如果用户的输入是非文本媒体,如图像或视频,我们可以编写一个自定义识别器来验证媒体并做出相应的响应。

Bot Builder SDK 允许我们向 Bot 注册多个识别器。每当有消息进入机器人时,都会调用每个识别器,得分最高的识别器被认为是获胜者。如果两个或更多识别器得到相同的分数,首先注册的识别器获胜。

最后,同样的机制可以用来将我们的 bot 连接到 LUIS,事实上 Bot Builder SDK 包含了一个识别器。为此,我们获取 LUIS 应用的端点 URL(可能是我们在第 3 章中创建的那个)并将其用作 LuisRecognizer 的参数。

bot.recognizer(new builder.LuisRecognizer('https://westus.api.cognitive.microsoft.com/luis/v2.0/apps/{APP_ID}?subscription-key={SUBSCRIPTION_KEY}}'));

一旦我们设置好了,我们就为我们想要全局处理的每个意图添加一个 triggerAction 调用,就像我们在帮助对话框中做的那样。作为“matches”成员传递的字符串必须对应于我们的 LUIS intent 名称。

bot.dialog('AddCalendarEntry', [
    ...
])
.beginDialogAction('AddCalendarEntryHelp', 'AddCalendarEntryHelp', { matches: /^help$/ })
.triggerAction({matches: 'AddCalendarEntry'});

bot.dialog('RemoveCalendarEntry', [
    ...
])
.triggerAction({matches: 'DeleteCalendarEntry'});

此时,我们的机器人对话可以通过使用 LUIS intents 在对话框之间导航(图 5-42 )。LUIS 的意图和实体对象被传递到对话框中。

img/455925_1_En_5_Chapter/455925_1_En_5_Fig42_HTML.jpg

图 5-42

最终由我们的 LUIS 车型提供动力!

练习 5-2

将你的机器人连接到路易斯

在本任务中,您将把一个机器人连接到您在第 3 章中创建的 LUIS 应用。

  1. 创建一个空机器人,并创建一个对话框来处理在第 3 章中创建的每种类型的意图。对于每个对话,只需发送一条带有对话名称的消息。

  2. 向您的机器人注册 LUIS 识别器,并确认其工作正常。

  3. 每个对话瀑布的第一个方法是传递会话对象和一个 args 对象。使用调试器浏览对象。 5 来自路易斯的数据是什么结构?或者,将代表 args 对象的 JSON 字符串发送给用户。

识别器是 Bot Builder SDK 中的一个强大功能,它允许我们基于传入的消息为我们的 Bot 配备各种行为。

构建一个简单的日历机器人

理想情况下,我们构建对话的模式变得越来越清晰。本书提供的 git repos 包括一个日历礼宾机器人,我们在本书的剩余章节中构建了这个机器人。对 bot 进行更改的每个章节在 repo 中都有自己的文件夹。Chapter 5 文件夹包括与 LUIS 集成的框架代码,并发回一条消息,说明机器人理解了什么。Auth 和 API 集成将在第 7 章中介绍。我们在第 10 章中添加了基本的多语言支持,在第 12 章中添加了人工交接,在第 13 章中添加了分析集成。

这些是我们打算在第 5 章回答的一些问题:

  • 在 Node 的上下文中,我们如何构造一个 bot 和它的组件对话框?

  • 解释传入对话框的数据的代码的一般模式是什么?

  • 尽管可以使用 Bot Builder SDK 创建端到端测试,但就其当前形式而言,单元测试对话逻辑并不是最直接的任务。我们如何构建我们的代码,以便尽可能好地进行单元测试?

当我们深入代码并检查不同的组件时,请记住以下几点:

  • 随着代码的构建和测试,我们会发现 LUIS 应用中存在一些漏洞。在这段代码的构建过程中,我的模型与第 3 章中产生的模型有了一点变化。这些不是突破性的变化,而是新的话语和实体。代码示例包括这个版本的模型。

  • 我们需要定义每个对话的范围。例如,“编辑日历条目”对话框旨在关注移动约会。

  • 我们创建了一些助手类,包含一些最复杂的逻辑,即从 LUIS 结果中读取每种类型的实体,并将它们转换成可以在对话框中使用的对象。例如,我们的许多对话框基于日期时间或范围以及主题或被邀请者在日历上执行操作。

我们利用 Bot Builder 库将对话框适当地模块化到库中。暂时不要担心这个。这只是捆绑对话框功能的一种方式。我们将在下一章讨论这个概念。开始回顾代码,我们将在下一章深入更多的 Bot Builder 细节。该代码的结构如下:

  • 常量和助手

  • 将 LUIS 意图和实体转换成应用对象的代码

  • 支持添加、移动和删除约会的对话框;检查可用性;并获得当天的日程安排

  • 最后,一个 app.js 入口点将所有这些联系在一起

结论

这是对 Bot 框架和 Bot Builder SDK 的介绍。我们现在可以构建基本的机器人体验了。创建 bot 通道注册、将我们的 Bot 连接到通道连接器、使用 Bot 框架仿真器和 ngrok 进行调试以及使用 Bot Builder SDK 构建 Bot 的核心概念是我们需要理解的关键部分,以便提高工作效率。Bot Builder SDK 是一个强大的库,可以在这个过程中帮助我们。我们介绍了 SDK 的核心概念。在不深入 SDK 的细节的情况下,我们开发了一个聊天机器人,它可以解释各种各样的自然语言输入,执行我们旨在支持第 3 章中的用例。剩下唯一要做的事情就是引入一个日历 API,并将 LUIS 意图和实体组合转换成正确的 API 调用。

在我们开始之前,我们将更深入地研究 Bot Builder SDK,以确保我们在最终实现中选择了正确的方法。

Footnotes [1](#Fn1_source)

JavaScript 有几个不同的 linter 选项,即 ESLint、JSLint 和 JSHint。ESLint 是可扩展性更强、功能更强大的选项之一。 https://eslint.org/

  2

Internet 信息服务(IIS)是微软丰富的可扩展 web 服务器。它运行所有 Azure Windows web 应用。 https://www.iis.net/

  3

为 Node 应用使用自定义 web . config:https://github.com/projectkudu/kudu/wiki/Using-a-custom-web.config-for-Node-apps

  4

Kudu 是 Azure 网站部署背后的引擎。它也可以在 Azure 之外运行。 https://github.com/projectkudu/kudu/wiki

  5

调试 Node.js 应用: https://nodejs.org/en/docs/guides/debugging-getting-started/ 。像 VS 代码这样丰富的 IDE 真的很容易: https://code.visualstudio.com/docs/nodejs/nodejs-debugging