四、对话设计
虽然这项技术允许我们开发一个可以以任何方式运行的机器人,但这并不意味着我们应该这样做。用户对他们的消息通信有一定的期望,例如确认收到消息、快速响应以及稍后继续对话的能力。虽然与机器人交谈不同于与人交谈,但给朋友发消息是最相似的体验。由于用户仍在习惯机器人,将这些交互作为机器人行为的样本是合理的。
成功的机器人可以表现出许多类型的行为,但有一些共同的模式和风格。这并不是说创新停滞了;一点也不!在给定技术和预算限制的情况下,这些使用案例基于空间中常见的观察模式。创新的空间已经成熟,唯一的问题是,我们集体想象力的极限是什么?
这些常见的用例在与用户交流时也遵循一定的规则。在我的职业生涯中,对我来说,最重要的是要认识到大多数技术用户并不像我一样使用技术。我喜欢命令行及其精确性。不是以英语为母语的人,自然语言的模糊性一直令人困扰。但是机器人给了用户使用这种模糊自然语言的能力。因此,bot 开发者需要进行一定程度的自我约束。开发人员可以很容易地创建一个更像使用命令行的机器人体验。
考虑到自然语言处理(NLP)的局限性和用户的期望,小心机器人在不理解事物和要求用户反馈时的行为变得比以往任何时候都重要。通过谨慎的方法和有意识地选择我们发送给用户的回复类型,创造愉快的体验是可以实现的。
常见使用案例
开发人员正在创造各种各样的对话体验。我们可以体验专门从事销售物品、回答产品问题、发送订单状态、回答订单查询、提供云基础设施、搜索多个数据源、共享 cat GIFs 以及做数百万其他事情的机器人。
在高层次上,我们将把机器人分成两个更大的类别:消费者和企业。当然,子类别之间有重叠,但也有一些明显的分界线。
常见的消费者案例
消费者机器人通常可以通过 Facebook Messenger、Slack 和其他公共消息应用等通道获得;网络聊天;语音接口;或者甚至是定制的移动应用。在质量等级的低端,它们只不过是玩具。在高端,它们可能是令人印象深刻的设计和工程壮举。由于我们在第一章中讨论的人工智能和机器人热,许多公司都在他们的产品中部署机器人。例如,Atlassian 为其 JIRA 产品设计了一款 Slackbot。甚至亚马逊也在其移动购物应用中集成了聊天机器人。你还会发现一些品牌通过 Facebook Messenger 涉足机器人。脸书页面使公司很容易拥有一个面向外部的公共通道,通过公共帖子或 Messenger 与客户交谈。如果是 Messenger,人工代理需要登录到页面收件箱并回复每条消息。许多公司的第一步是部署一个信使机器人,它可以回复一些类型的用户查询,剩下的就交给人类来回复。就效用而言,我们仍在试图回答这个问题,什么对用户最有意义?该领域的各种机器人无疑表明了这一点。以下是一些有效方法的大类。
常见问题解答
FAQ bot 通常是团队首次进入 bot 和 NLP 领域进行技术测试。这是一个简单的用例:让我们将现有的 FAQ 作为一个机器人放在 Facebook Messenger 或企业消息中。这样,最常见的问题可以在员工花时间回答之前被机器人捕捉到。从用户的角度来看,一个简单的基于文本的 FAQ 机器人可以变成非常有趣和美观的东西。一个常见问题的答案不仅仅是一段无聊的文字。答案可以包括更多的内容,例如图像、视频和附加内容的链接。
例如,考虑一个金融服务机器人,它可以回答关于金融话题的不同类型的问题。在其响应中,它可以嵌入额外的感兴趣的建议主题作为按钮。此时,用户可以查看相关术语及其定义。如果有网站直观地表现了一个概念,例如,铁秃鹰期权投资策略,这些链接可以包含在用户点击以获得更多信息的响应中。当然,我们的对话设计需要平衡所有的内容和可能的用户超载。介于两者之间的最佳点可以有效地为用户提供愉快的机器人体验。图 4-1 是国际儿童基金会嵌入网页的 FAQ bot 的一个例子。
图 4-1
一个基本的 FAQ 机器人
面向任务的机器人
面向任务的机器人是一个虚拟代理,可以帮助用户完成特定领域的各种任务。这些类型的机器人有时被称为礼宾机器人。例如,JIRA 的 Slackbot(图 4-2 )是面向任务的。它可以根据团队正在进行的对话创建任务和分配任务。
图 4-2
JIRA Slackbot 公司
我曾经做过一个糖尿病教练聊天机器人,它可以帮助患有二型糖尿病的用户根据之前的对话和其他用户数据,个性化地询问饮食和锻炼建议。还有一些金融服务机器人连接到交易账户,更新用户的账户余额和头寸,甚至进行交易,如 TD Ameritrade 机器人(图 4-3 )。我们在第三章中开发的日历路易斯应用是日历任务机器人的基础。
图 4-3
使用 TD Ameritrade bot 进行股票交易
广播机器人
广播机器人是一个有趣的概念,非常普遍。我们可以认为这是一个无需提示就向用户伸出手的机器人,而不是用户先联系机器人。在一些机器人中,让机器人参与进来更像是一种模式。例如,不同的新闻机器人,如 Facebook Messenger 上的 CNN 机器人,将每天发布当天的重大新闻。
在一些名人机器人实现中可以看到这种情况的子集和更细微的版本。通常,这些类型的机器人是为了好玩而存在的。他们采用名人的个性,可以与用户谈论感兴趣的话题、产品以及与名人分支机构互动的其他方式。这个机器人可以引导你浏览主题脚本,给你发送视频和图像,也许还可以谈论名人代言的产品。对话几乎完全由机器人驱动,而不是用户。这是一个有趣的讲故事的设备,但它的成功归结于一致的新鲜内容。图 4-4 展示了一个项目 Cali 的例子,这是一个为了好玩而创建的 Snoop Dogg 机器人。
图 4-4
卡利项目:Snoop Dogg bot
电子商务 Bot
尽管机器人在北美还不算大,但它们正慢慢开始向消费者出售产品。从技术角度来看,这并不是一个非常具有挑战性的任务;更大的挑战是让用户使用短信,而不是应用或网站。这些类型的机器人中电子商务集成的数量各不相同。例如,一些机器人提供完整的端到端购物体验。通过机器人看衣服(图 4-5 )或鲜花(图 4-6 )与网购体验是不同的。一些机器人倾向于这一点,并提供古怪或创新的方式来计算出什么产品显示给用户,以获得冲动购买!
图 4-5
路易威登机器人
我们还遇到过这样的情况,机器人只负责广播购买收据和订单状态更新,具有有限的机器人功能。其他所有事情都会自动发送给人工客户支持代表。虽然这种体验并没有完全融入电子商务,但它是进入这一旅程并让客户熟悉机器人的第一步。简而言之,公司正在拥抱所谓的数字化驱动的消费者之旅,机器人是这一战略的一部分。 1
图 4-6
1-800-Flowers.com 助手
不同的消息平台提供不同级别的支付支持。我们当然可以通过机器人创建电子商务,提供一个自定义的结帐页面,用户可以在那里输入他们的支付信息。对话在此暂停。一旦付款被处理,一条消息被发送到机器人继续对话。另一方面,Facebook Messenger 提供了与 Stripe 和 PayPal 等系统的更深层次的集成。在这个版本中,支付体验完全保留在 Facebook Messenger 应用中。从用户的角度来看,摩擦越少越好。随着用户开始更加信任消息应用来存储他们的支付信息,我们将会看到越来越多这样的支付集成。苹果发布了其商务聊天 2 产品,你可以打赌 Apple Pay 支付已经完全集成。 3
常见企业案例
企业机器人可能更专门针对某个领域或主题。它们通常使用网络聊天组件部署,或者集成到企业消息系统中,甚至集成到企业呼叫中心和交互式语音应答(IVR)系统中,如思科的统一通信中心。它们也可以部署在电子邮件端点上。机器人可以与单点登录解决方案、强大的现有企业后端和知识管理数据库集成。根据企业的实践,这些将从简单的试点机器人到机器学习驱动的大规模部署。
自助服务机器人
企业场景中最常见的用例之一是事件自助服务。企业拥有庞大的知识库,内部问讯台代理使用这些知识库向用户传达可能的解决方案,并指导他们解决问题。机器人可以向用户传达这些一步一步的故障诊断指导。例如,对内部帮助台最常见的查询之一是密码重置。如果公司能够自动处理这样的请求,它们可以减少大量的交易量,坦率地说,还可以节省资金。你可以想象一个电器制造商发布了一个聊天机器人来帮助诊断和修复问题,然后才让服务工程师介入。
这些自助服务机器人背后的想法是,它们可以为用户提供各种自助服务内容,尤其是最常见的查询,甚至可以自动化客户支持团队正在进行的一些常见工作。这些机器人通常与实时聊天系统集成在一起,这样用户最初可能会与机器人聊天,但在机器人的指示不能解决问题的情况下,可以迅速重新路由到与人类客户服务代理的实时聊天或电话对话。
过程自动化机器人
机器人过程自动化(RPA)是当今的一个大话题。像 IPsoft 这样的公司专注于构建能够自动化业务和 IT 任务的机器人和技术。在这种情况下,机器人不一定是聊天机器人,而是执行自动化的计算机代理。这些任务可以包括从账户供应、网站自动化和业务流程自动化的一切。有些公司专注于创建自动化平台,如 Automation Anywhere 和 UiPath。随着机器学习这些天被用于从合同分析到皮肤癌诊断的一切,聊天机器人可以作为这些过程的优秀前端。在 RPA 场景中,聊天机器人更像是一个指挥者,而不是自动化者。此外,这些机器人可能会集成到 Remedy 和 ServiceNow 等票务系统中,以跟踪其工作。
在其他情况下,聊天机器人对用户不太可见。例如,Slack 是一个很好的机器人平台,可以监听团队对话,并在合适的自然语言出现时显示数据。简单监听一些自然语言输入并提供答案的机器人是一种自动化机器人。举例来说,一组医学专家浏览程序的文本描述,并负责将它们翻译成保险代码。这个过程可以由机器学习算法自动完成,该算法可以观察团队的行为和结果,然后接管数据。
同样,逻辑背后的实际大脑可能不在机器人本身内部。可能有一个单独的系统实现保险代码的机器学习模型。或者,自动化代码可以是 Python、PowerShell 或任何其他脚本。机器人充当接收自然语言和编排自动化的前端(图 4-7 )。
图 4-7
自动化机器人流程示例
知识管理机器人
另一种类型的企业机器人可以解决跨各种数据源的自然语言搜索问题。许多公司在不同的系统上有巨大的知识库。能够使用自然语言集成所有这些资源是非常重要的。在这些机器人中有一些有趣的选择,比如向用户显示哪些内容,以什么格式显示,以及如何收集反馈,以确定哪些内容对查询最有用。
这些项目试图解决的自然语言搜索的更大问题非常有趣,超出了本书的范围。这种类型的机器人在群组对话环境中非常有趣,当群组就感兴趣的主题进行对话时,机器人会查询文章、报告、白皮书和案例研究。在搜索过程中,小组对机器人的反馈可以进一步提供监督学习数据,以进一步改善搜索体验。
代表对话
我们如何开始开发一个对话式聊天机器人?一个好的地方是试图用图形表示对话流。聊天机器人可以处理什么样的任务?它需要寻找什么意图和实体来实现这些目标?它如何帮助填充缺失的数据?
我们将这个对话称为一个图,它是由边连接的 Node 的集合。图 4-8 展示了一个无向图。图中的每个 Node 都至少与一个其他 Node 相连。每个 Node 代表对话的一个状态,边代表状态之间的转换。
图 4-8
无向图
我们将在边缘使用箭头来显示流向。这被称为有向图。我们从根 Node 开始。根 Node 是会话开始时的状态。使用我们的日历机器人作为示例,我们知道我们的机器人应该支持添加新条目、编辑现有条目、删除条目、检查可用性以及提供日历或事件的摘要。我们可以用图 4-9 来表示这个机器人。
图 4-9
日历礼宾机器人对话的表示
注意,会话基于用户的话语在状态之间移动,这决定了 LUIS 意图。会话中的每个 Node 都有内置的逻辑来解析实体并为状态执行正确的逻辑。在一个状态执行完它的逻辑后,会话转移回根 Node。
状态之间的转换可以通过编程或用户输入来调用。例如,假设我们的机器人支持创建日历约会。回想一下在第三章中,我们创建了一个 LUIS 应用,它允许我们传递几个实体或者不传递实体作为话语的一部分来添加一个日历条目。如果我们的添加新条目对话框没有收到关于主题和被邀请者的信息,例如在话语“明天下午 2 点见面”中,我们可以在另一个状态中引出该信息。另一方面,如果用户使用包含这些实体的话语,例如“明天下午 2 点和 kim 见面喝咖啡”,我们不需要引出这些额外的信息。这种有条件的状态转换如图 4-10 所示。
图 4-10
基于用户输入有条件地转移到状态
创建对话图的过程通常被称为意图和实体映射;我们将意图和实体组合建模为状态 Node 之间的转换。
机器人响应
机器人对用户查询的响应可以采取多种形式。理解不同的选项以及如何最好地利用它们是任何 bot 设计的关键。在接下来的章节中,我们将深入探讨各种通道中的一些概念。
积木
我们现在理解了如何获取用户输入并将其映射到机器人状态和功能。我们也知道如何将我们的机器人代码组织成各种对话状态。我们设计的下一步是弄清楚机器人给用户发送什么作为回报。机器人可以以多种方式做出反应。默认情况下,我们认为文本或语音输出。最典型的情况是,我们简单地发送回纯文本。一些消息通道支持更复杂的东西,比如 Markdown 或 HTML。Markdown 是一种纯文本格式语法。 5 以下降价输入转换成图 4-11 中的格式化内容:
图 4-11
格式化的减价文件
# H1
# H2
Hello, my _name_ is **Szymon Rozga**
I like:
1\. Bots
1\. Dogs
1\. Music
机器人平台也可以支持语音响应。许多平台也支持语音合成标记语言(SSML)作为语音输出格式。SSML 是一种标记语言,它提供了关于应该如何使用诸如停顿、中断、速率和音高的变化等元素来构造语音的元数据。这里有一个来自 WC3 推荐 6 的不言自明的例子:
<?xml version="1.0"?>
<speak version="1.0" xmlns:="http://www.w3.org/2001/10/synthesis"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.w3.org/2001/10/synthesis
http://www.w3.org/TR/speech-synthesis/synthesis.xsd"
xml:lang="en-US">
That is a <emphasis> big </emphasis> car!
That is a <emphasis level="strong"> huge </emphasis>
bank account!
</speak>
对用户的输出不一定总是文本。我们可以使用图像和视频向用户传达许多想法。作为发送回用户的任何消息的一部分,我们可能会附加各种内容,如视频、音频文件和图像。具体支持的格式将取决于底层操作系统和通道。一些系统还允许其他文件附件,例如 XML 文件或某种本机格式。
向用户展示内容的另一种机制是卡片。卡片通常是图像、文本和作为行动号召的可选按钮的组合。我们的 YouTube 搜索机器人从第章 1 (图 4-12 )清晰地显示了视频名称、描述,以及一组卡片中的观看按钮。
图 4-12
卡片横向列表;也称为旋转木马
这种布局被称为传送带。它并排展示了几个卡片,并让用户能够滑动或滚动各个卡片。
按钮通常作为卡片的一部分发送,但是它们也可以作为没有关联图像的独立元素发送。按钮有很多种。最受欢迎的三个按钮用于打开网页、向机器人发回消息(IM back)或向机器人发回消息(post back)。IM back 和 post back 的区别在于,post back 消息不会出现在消息历史中,而 IM back 消息会出现在消息历史中。不是所有的通道都支持这两种方法,但是通过点击按钮向机器人发送消息的总体精神得到了广泛的支持。
另一种按钮是登录按钮。登录按钮通过 web 视图中的登录启动身份验证或授权流程。登录完成后,机器人会收到任何必要的访问令牌,并可以继续进行认证会话,如图 4-13 所示。
图 4-13
具有建议操作/快速回复的认证机器人
前面描述的所有内容都保存在用户的聊天记录中。传送带、卡片、按钮,当然还有所有的文本都可供用户滚动浏览。有一种形式的元素只在包含它的消息的上下文中显示。该功能是建议操作,也称为快速回复。这些按钮显示在用户界面的底部,直到用户做出响应。这些按钮是清晰的行动号召,也是愉悦对话体验不可或缺的工具。图 4-14 显示了一个使用建议动作引导用户到 TD Ameritrade bot 中可用的视频类别的示例。
图 4-14
TD Ameritrade bot 中的视频类别建议操作
机器人中的认证和授权
老实说,没有人会向机器人聊天窗口发送用户名和密码。这是一个安全风险。我们不希望脸书或 Slack 或任何其他通道在他们的消息历史中有我们用户的登录凭证。归根结底,bot 只是一个 web 服务,因此使用标准的 OAuth 或 OpenID 连接流是一种自然的选择。
正确的方法是利用登录卡,这是一种包含按钮的卡,该按钮可以打开登录网页,供用户输入他们的凭据(图 4-15 )。
图 4-15
标准的签到卡
通常,这个登录页面将是一个 OAuth 页面(图 4-16 )。
图 4-16
OAuth 授权代码流
OAuth 2.0 7 是互联网上基于令牌授权的标准。OAuth 2.0 支持几种不同类型的授权流。三足 OAuth 流允许资源所有者(用户)向 API(服务提供者)授予对应用(消费者)的访问权。在 bot 的上下文中,它看起来如下:
-
用户点击按钮以在第三方的 web 视图中打开服务的登录页面,并输入他们的用户名/密码组合。这个登录页面的 URI 通常包括一个客户端 ID 和一个重定向 URI。重定向 URI 是我们的 bot web 服务的一个端点。
-
一旦用户成功登录,该服务会将用户重定向回机器人重定向 URI。机器人重定向 URI 端点接收授权码。这是用户对应用使用服务的授权。机器人用授权码交换来自令牌端点的访问令牌(和可选的刷新令牌)。
-
当代表 bot 用户向服务发出请求时,bot 使用访问令牌。
-
通常,访问令牌是短期的,而刷新令牌是长期的。在任何时候,机器人都可以通过发布刷新令牌从令牌端点请求新的访问令牌。
关于这个和其他 OAuth 流程的细节有大量的文档。RFC 是一个很好的起点。 8 关键是 bot 是一个 web 服务,完整的 OAuth 流程可以以集成的方式发生。从 UX 的角度来看,唯一棘手的部分是确保浏览器窗口在登录完成后自动关闭。各种通道处理这一问题的方式略有不同。虽然我们可以手动实现整个流程,我们在第八章中展示了这一点,但是 Bot 框架确实提供了额外的工具来促进这一过程。9
专业卡
在支持卡片的平台上,卡片是用户体验的一个重要组成部分。我们讨论了通用卡的概念。一些频道提供几种专用卡。例如,可以发送收据卡(图 4-17 )来传达购物收据,其中包含总额、税款、付款确认等信息。
图 4-17
信使收据模板
此外,Messenger 使开发人员能够使用四种航空旅行卡,如行程、登机牌(图 4-18 )、值机和航班更新。
图 4-18
Messenger 登机牌模板
轻按登机牌会显示一个带有二维码的全屏版本,可以在机场使用(图 4-19 )。根据我们的目标平台,可能会有其他模板供我们使用。如果它们存在并且符合你的用例,就使用它们。它们提供了良好的本地用户体验。
图 4-19
Messenger 登机牌模板详细信息
另一种形式的专用卡是使用自定义图形的卡。一种常见的方法是在机器人处理用户输入时在 web 服务上生成自定义图形。在第十一章中,我们将使用 Headless Chrome 构建一个简单的自定义图形渲染器,以展示使用 HTML 和 JavaScript 构建自定义图形是多么容易。
最后,微软推出了一种新的卡片格式,叫做适配卡。 10 自适应卡,我们将在第十一章展开,是一种平台无关的方式,使用简单的基于容器的布局引擎来描述文本、图像和输入字段的布局。然后,Microsoft Bot 通道连接器能够将这些卡渲染成特定于平台的渲染。自适应卡是定制图形方法的专用版本,集成了逻辑以在卡中生成按钮和行为。有多少频道最终会支持这种格式还有待观察,但许多微软拥有的频道已经这样做了。
图 4-20 显示了一个适配卡的 HTML 渲染示例。
图 4-20
自适应卡样本
图 4-21 显示了微软团队应用上输入表单卡的渲染。
图 4-21
输入表单卡示例
其他功能
机器人可能包括几个其他有趣的功能,可以真正使机器人体验发光。以下是其中的一些功能:
-
主动消息传递:机器人可以异步联系用户,由事件而不是传入的消息触发。如果 bot 存储了用户的地址(服务 URL、对话和用户 id 的组合),它可以利用它与用户进行通信。
-
人工移交:在客户服务场景和高度可见的面向公众的机器人部署中,拥有一种将对话从机器人无缝转移到人工代理的机制是一个成功机器人的必要条件。
-
支付:越来越多的平台开放他们的支付系统,以方便对话整合。Facebook Messenger 的支付程序集成了 easy Stripe/PayPal。微软为整个 Windows 生态系统和 Bot 框架的支付提供了简单的条纹集成。
对话体验指南
在开发 bot 体验时,我们应该遵循一些重要的指导原则。其中一些可能并不适用于每种类型的机器人,或者可能与消费者机器人和企业机器人更相关,但是在设计机器人时应该至少记住这个列表。
焦点
正如在第一章和第二章中所讨论的,机器人的技术和智能程度是有限的。我们的机器人不应该试图变得太聪明;人类总是能以这样或那样的方式破解机器人。例如,处理来自用户的问候,如“嗨”或“你好”,这是完全可以的。我们不想陷入处理各种不同类型问候的困境。不要开始为“怎么了?"对"嗨。“如果你正在读这本书,你很可能没有微软或谷歌的预算(图 4-22 )。我们是来帮忙做任务的,不是一般的 AI。诚实地承认我们机器人的局限性是可以的。
图 4-22
构建机器人的好建议
不要假装机器人是人类
我们不希望我们的机器人最终出现在恐怖谷。 11 也就是说,与大多数(如果不是全部的话)类人物体一样,真实的人类会觉得有些事情不太对劲,从而产生奇怪和诡异的感觉(图 4-23 )。我们不希望我们的用户有这种感觉。这与用人类的形象来表现你的机器人是相辅相成的。如果你通过一个虚拟形象来表现你的机器人,使用一个明显暗示非人类实体的图标。Siri 和 Cortana 在这方面做得非常好。
图 4-23
我们绝对是在恐怖谷
不要区分机器人的性别
围绕这个话题有很多文章。 12 值得注意的是,即使 Siri、Cortana 和 Alexa 以及一些较老的虚拟助手都有女性名字,但谷歌和脸书却选择了谷歌助手和 m。这种非性别化机器人的趋势在业内一直持续着。采用女性角色会很快变得怪异,就像电影中 AI 的性感化一样。
总是呈现下一个最佳行动
我们的机器人不应该在用户不知道下一步该做什么的情况下让用户无所事事。bot 应该有一个欢迎消息,向用户介绍它自己、它的功能以及它能做什么。当用户感到困惑并寻求帮助,或者机器人无法识别用户的输入时,机器人也应该建议一些选项。关键是,如果在对话的任何一点,用户看到一个没有建议后续步骤的空白消息框,这将成为令人困惑的对话体验。Facebook Messenger、Skype 和其他频道具有上下文快速回复功能,在聊天界面底部显示按钮选项(图 4-24 )。提出这样的建议是一个很好的方式来交流我们的机器人的能力和局限性。
图 4-24
下一步最佳行动
语气一致
机器人通常最终会获得一个名字和个性。虽然我不认为性别化的名字有意义,但你的机器人应该有个性和一致的语气。记住,这些是品牌在向你的顾客说话。有些机器人很健谈。其他人就不那么想了。有些是正式的。其他的更轻松。为你的机器人选择一个并保持一致。此外,尽管这是一项有趣的技术,但如果我们想保持以品牌为中心的声音,我们应该避免使用自然语言生成模型(自动生成响应的机器学习算法)。
利用丰富的内容
机器人为我们提供了不仅仅利用文本的机会。我们可以格式化文本并包含图像、视频和音频文件。我们可以渲染卡片(图 4-25 ),甚至在您的卡片中创建一些自定义图形。我们需要最大限度地利用这些功能
图 4-25
丰富的机器人内容是一个好主意
宽容一点
自然语言很复杂。预计用户输入是模糊的。我们的机器人应该有对话路径来确认信息或引出丢失的数据。如果希望用户输入一个数字,我们应该解析任何可能的输入,但也要清楚机器人的期望。如果可能的话,通过使用快速回复功能向用户提供一些可能值的建议。用户会对这些建议感到满意。没有什么比不知道和不被教导如何与机器人交流更令人沮丧的了。
避免陷入困境
在我们的机器人的任何一点上,用户应该能够改变对话主题。除非绝对必要,否则我们的机器人应该尝试卡在对话上下文中。例如,让我们假设一个日历机器人正在向用户询问日期。我们的机器人需要一个解析为日期的字符串。如果用户输入“删除明天上午 9 点的约会”,我们的机器人应该优雅地处理查询,而不是说“对不起,这不是一个日期”。请以年/月/日的格式输入日期。
不要滥用主动信息
机器人给了我们随时联系用户的能力,即使机器人看不到来自用户的消息。不要滥用这种特权。在消息应用中,用户在收到消息时会收到通知。没有比不断发送提醒或试图重新参与更简单的方法了。一些频道对此也有具体的政策。在信息传递通道中做一个好公民。
为人类提供一条清晰的道路
如果现在有一件事应该是清楚的,那就是机器人不能理解一切。即使功能范围有限,也会有机器人无法处理的问题。如果与用例相关,我们的机器人应该有能力以某种方式将用户连接到人类代理。无论是显示带有案例号的电话号码,还是无缝集成到实时聊天系统中,我们的机器人都应该清楚我们的用户如何与人类交流,以帮助他们解决问题(图 4-26 )。比如我曾经遇到过一个可以回答常见问题的 bot。我看了一篇关于这个机器人的新闻稿,所以我决定尝试一下。我开始对话,收到一条关于点击按钮的消息。没有按钮。我问:“我能怎么办?”我被转到了一个人类特工那里。在这一点上,我不能做任何事情,直到一个人类处理我的情况。我也不知道需要多长时间。他们的呼叫中心还开着吗?一旦代理人来了,我就和他们说话,然后被送回机器人那里。我完全沉默,没有按钮。我说的是“测试”我得到的下一条消息是我又要被调走了。在这一点上,我只是退出。不要让你的用户沮丧地退出。
图 4-26
与人类交谈的清晰路径
向你的用户学习
使用对话式体验从用户那里收集数据很简单。使用用户输入来解决 LUIS 的冲突意图并利用该数据来训练 LUIS 也很容易。当然,给予用户输入的重要性应该与给予培训师提供的话语的权重有很大不同。但是如果我们有数据,我们应该利用它。图 4-27 显示了我们如何实现这种方法的例子。在图中,我们将用户反馈存储到主动学习数据存储中,并且我们的主动学习过程确定在使用数据点训练 LUIS 之前应该观察多少相同的反馈。小心基于用户输入的自动化训练。你不想走泰的路。 13
图 4-27
实施主动学习
随着你在不同的消息通道中积累经验,你可能会学到更多的规则,但这个列表是一个很好的起点,我建议我们在每个聊天机器人项目中遵循。
结论
对话设计是一个丰富的领域。对于如何与用户互动,以及如何以文本之外的形式交流想法,我们有许多选择。当开发机器人时,我们的方法应该总是“由用户做正确的事情”用户的会话体验可能对语气、品牌、冗长和过度使用非常敏感(你不需要事事都用卡)。尽管在早期阶段有一些关键的抽象,比如卡片,但这个领域已经发展到可以最好地处理机器人与用户的交互。随着机器人变得越来越普遍,这些机制将会改进,数量也会增加。例如,微软的 adaptive cards 是一个试图拓展机器人在与用户对话中所能提供的功能的项目。我希望随着 bot 变得越来越普遍,消息通道将支持越来越多类型的 bot 卡行为。
我们现在对机器人执行的常见操作以及它们是如何执行的有了很好的基本理解。唯一剩下的问题是,我们如何将所有这些放在代码中?在下一章中,我们将会这样做,并将这些想法付诸实践。
品牌必须把握数字化驱动的消费者之旅,否则有成为猎物的风险: www.adweek.com/digital/brands-must-grasp-the-digitally-driven-consumer-journey-or-risk-becoming-prey/
2
苹果商务聊天: https://developer.apple.com/business-chat/
3
4
ipsoften Amelia:https://www.ipsoft.com/amelia/
5
Markdown:语法: https://daringfireball.net/projects/markdown/syntax
6
语音合成标记语言 1.1 版 WC3 推荐: https://www.w3.org/TR/speech-synthesis11/
7
OAuth 2.0 文档: https://oauth.net/2/
8
oath 2.0 RFC:
9
10
11
恐怖谷:为什么我们会觉得类似人类的机器人和玩偶如此令人毛骨悚然: https://www.theguardian.com/commentisfree/2015/nov/13/robots-human-uncanny-valley
12
机器人仅仅花了六年时间就开始抛弃过时的性别刻板印象: https://qz.com/1033587/it-took-only-six-years-for-bots-to-start-ditching-outdated-gender-stereotypes/
13
微软让新的人工智能机器人 Tay 沉默: https://techcrunch.com/2016/03/24/microsoft-silences-its-new-a-i-bot-tay-after-twitter-users-teach-it-racism/
五、Microsoft Bot 框架简介
微软的 Bot Builder SDK 有两种版本:C#和 Node.js。正如在第一章中提到的,出于本书的目的,我们将使用 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 包,并安装 botbuilder 、 dotenv-extended 和 restify 包。为此,您可以创建一个新目录,并键入以下命令:
npm init
npm install botbuilder dotenv-extended restify --save
图 5-1 显示了本地机器上典型的高级 bot 架构。这个想法是,node 应用主要依赖于两个组件。首先,bot Builder SDK 是我们用来构建 Bot 的 Bot 引擎。第二,来自任何通道的所有消息,无论是来自机器外部还是来自开发人员机器的 bot 框架仿真器,都通过 HTTP 端点发送到 Bot。我们使用 restify 来监听 HTTP 消息,并将它们发送给 SDK。
图 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": "³.9.0",
"dotenv-extended": "¹.0.4",
"restify": "⁴.3.0"
},
"devDependencies": {
"eslint": "⁴.10.0",
"eslint-config-google": "⁰.9.1",
"eslint-config-standard": "¹⁰.2.1",
"eslint-plugin-import": "².8.0",
"eslint-plugin-node": "⁵.2.1",
"eslint-plugin-promise": "³.6.0",
"eslint-plugin-standard": "³.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=
我们需要 botbuilder 和 restify 库。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 所示。
图 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、连接器的名称如 slack 或 facebook ,以及一个我们的机器人发送消息的响应 URL。URL 通常类似于 https://facebook.botframework.com
。 Session.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 )。
图 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* 文件。我们将在控制台中收到安全警告;现在可以忽略这些。此时,我们真的要单击连接按钮了。
图 5-4
模拟器连接用户界面
我们将在模拟器日志中看到两条消息。两者都是类型对话更新(图 5-5 )。
图 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 存储。我们现在可以用下面的代码来替换它:
图 5-6
它还活着!
const inMemoryStorage = new builder.MemoryBotStorage();
bot.set('storage', inMemoryStorage);
啊哈!我们的机器人还活着。模拟器现在包含了更多的东西。一个消息类型的传入帖子,文本为“echo!”以及带有文本“echo: echo!”的 message 类型的传出帖子和带有调试事件数据的 POST。单击 POST 链接将再次显示在这个请求中接收或发送的 JSON。请注意,这两种有效载荷是不同的,尽管在它们的下面使用了相同的接口 IMessage。我们将在第六章对此进行更深入的探讨。以下是传入或传出消息中的部分数据列表:
-
发送者信息(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 没有针对通道实现特定类型的消息时使用。我们将在第八章和第九章中探究一些例子。
-
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 install 和 npm start 在本地运行它。下载模拟器并将其连接到机器人。
-
仔细检查请求/响应消息。
-
观察模拟器和机器人之间的行为。
-
探索模拟器。使用“设置”菜单创建新对话或向机器人发送系统活动消息。它有什么反应?你能写一些代码来处理这些消息吗?
在本练习结束时,您应该熟悉如何运行一个未经身份验证的本地 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 服务的免费层来实现。
图 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 美元的试用账户,当综合费用达到支出限额时,服务会自动关闭。如果需要,免费帐户可以转换为付费帐户,从您的信用卡中收取额外费用(或其他支付方式)。
图 5-8
空 Azure 门户
步骤 2:创建 Bot 注册
在 Azure 门户中,单击左上角窗格中的“创建资源”按钮。在搜索市场文本字段中,输入 azure bot 。你会得到很多结果,但我们感兴趣的是前三名(图 5-9 )。
图 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 通道注册位置的位置。准备就绪后,单击创建。
图 5-10
创建新的 bot 通道注册
门户顶部有一个进度指示器,当注册准备就绪时,我们会收到通知。我们还可以通过使用左侧窗格上的资源组按钮导航到资源组(图 5-11 )。
图 5-11
我们资源组中的资源
导航到 bot 通道注册,然后导航到设置刀片(图 5-12 )。请注意,Azure 自动填充了应用洞察标识符和密钥。这些将用于跟踪我们的机器人分析数据。我们将在第十三章中看到其中一个分析仪表板。
图 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 会话来解决这个问题。
图 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 )!
图 5-14
有效!我们的机器人连接到我们的第一个频道!
步骤 5:连接到 Facebook Messenger
很酷,对吧?bot 框架几乎完全与我们的 Bot 集成在一起。我们现在将继续整合我们的机器人与 Facebook Messenger。Bot 通道注册上的通道刀片使我们能够连接到微软支持的通道(图 5-15 )。
图 5-15
频道仪表板
点击 Facebook Messenger 按钮,进入 Messenger 配置界面(图 5-16 )。我们将需要从脸书获得四条数据:页面 ID、应用 ID、应用秘密和页面访问令牌。最后,我们应该注意回调 URL 和验证令牌。我们将需要这些建立脸书和 Bot 框架之间的连接。
图 5-16
Facebook Messenger Bot 框架连接器设置
现在让我们设置必要的脸书资产。我们必须有一个脸书帐户来完成以下任务。导航到 Facebook.com,使用右上角的下拉菜单创建一个新页面(图 5-17 )。脸书将询问页面的类型。出于本例的目的,我们可以选择品牌/产品类型和应用页面子类别。
图 5-17
创建新的脸书页面
我创建了一个名为 Szymon 测试页面的页面。我们可以通过点击左侧导航窗格上的“关于”链接来找到页面 ID(图 5-18 )。在最底部,我们会找到页面 ID。我们需要将该值复制到 Bot 框架 Facebook Messenger 频道配置表单中(图 5-16 )。
图 5-18
脸书页面关于页面,包括页面 ID
接下来,在新的浏览器标签或窗口中,导航至 https://developers.facebook.com
。如果您还没有注册,请注册一个开发者帐户。创建一个新的应用(图 5-19 )。给它起任何你喜欢的名字。
图 5-19
创建新的脸书应用
完成后,通过左侧边栏菜单导航到设置➤基本页面,并将 Facebook 应用 ID 和应用秘密复制到 Bot 框架表单中(图 5-20 )。
图 5-20
应用 ID 和应用密码
接下来,导航到仪表板(从左侧栏的链接)并设置 Messenger 产品。向下滚动页面,直到到达令牌生成部分。在令牌生成部分选择页面,生成页面访问令牌(图 5-21 )。将令牌复制到 Azure 门户内的 Bot 框架表单中。
图 5-21
生成页面访问令牌
接下来,滚动到 Webhooks 部分(就在脸书应用仪表板的令牌生成部分下方),然后单击设置 Webhooks。您将看到一个弹出窗口,要求您输入回拨 URL 和验证令牌。从 Azure 门户的配置 Facebook Messenger 表单中复制并粘贴这两个内容。
在订阅字段部分,选择以下字段:
-
信息
-
消息 _ 交付
-
消息 _ 阅读
-
消息传递 _ 回发
-
消息传递 _ 选项
-
消息 _ 回应
单击验证并保存。最后,从下拉列表中选择您希望 bot 订阅的页面,然后单击 subscribe。您的设置页面应该如图 5-22 所示。
图 5-22
订阅我们测试页面上的消息
确保保存 Bot 框架配置。就这样!您可以在 Messenger 联系人中找到该页面。你可以给它发送一条信息,你应该得到它的回应(图 5-23 )。
图 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 中,我们创建了一个新的应用服务计划,并选择了免费定价等级。免费很好。
图 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 )来设置部署用户名和密码组合。
图 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 中的文件列表上。
图 5-26
Kudu ZipDeploy 用户界面
还有一步。进入 Bot Channels 注册条目中的 Settings blade,将 messages endpoint 设置为您的新应用服务(图 5-27 )。确保点击保存按钮。
图 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 的起点。
图 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 连接器,我们现在可以让机器人响应这些查询。我们将在第十三章的中讨论机器人将对话移交给代理人的想法,称为人工移交。
脸书上的一个机器人是一个脸书应用,它通过网络钩子订阅进入脸书页面的消息。我们注册了 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 显示了结果信息。
图 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 显示了结果交换。
图 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 支持的一种卡。以下是其他受支持的卡:
-
适配卡:柔性卡,包含容器、按钮、输入域、语音、文本、图像等项目的组合;并非所有频道都支持。我们将在第十一章中深入探讨适应卡。
-
动画卡:支持动画 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 ):
图 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 :用户在一个频道上所有对话的数据
默认情况下,这些对象都存储在内存中,但是我们可以很容易地提供一个替代的存储服务实现。我们将在第六章看到一个例子。
瀑布和提示
一个瀑布是在一个机器人上处理输入消息的一系列函数。通用 Bot 构造函数将一组函数作为参数。这是瀑布。Bot Builder SDK 连续调用每个函数,将上一步的结果传递给当前步骤。这种方法最常见的用途是使用提示符向用户询问更多信息。在下面的代码中,我们使用文本提示,但是 Bot Builder SDK 支持数字、日期或多选等输入(图 5-32 )。
图 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 )。这在第一步可能有条件地要求额外输入的情况下很有用。我们将在日历机器人代码中使用它。
图 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 显示了结果对话。
图 5-34
数据收集瀑布
对话
让我们用对话式设计来实现这个完整的循环。在第四章中,我们讨论了如何使用我们称之为对话的 Node 图来建立对话模型。到目前为止,在这一章中,我们已经学习了瀑布,以及如何用代码建立对话模型。
我们还学会了如何利用提示从用户那里收集数据。回想一下,提示是从用户那里收集数据的简单机制。
builder.Prompts.text(session, "where?");
提示很有意思。我们称函数为( builder。Prompts.text ),将对话转换为提示。一旦用户发送了有效的响应,瀑布中的下一步就可以访问提示的结果。图 5-35 显示了整个过程。从我们的瀑布的角度来看,我们并不真正知道 Prompts.choice 调用在做什么,我们也不关心。它监听用户输入,进行一些验证,重新提示错误的输入,并且只返回一个有效的结果,除非用户取消。所有这些逻辑对我们来说都是隐藏的。
图 5-35
对话之间概念上的控制转移
这种交互与编程函数调用的模型相同。通常实现函数调用的方式是使用堆栈。检查图 5-36 和以下代码:
function f(a,b) { return a + b; }
当函数 f 被调用时,函数的参数被压入栈顶。然后,该函数的代码处理堆栈。在此示例中,函数添加参数。最后,留在堆栈顶部的唯一值是函数的返回值。然后,调用函数可以对返回值做任何它想做的事情。
图 5-36
堆栈上的函数调用
这是对话中提示的工作方式。Bot Builder SDK 中的一般概念是一个对话框。提示是一种对话框。对话只不过是对对话逻辑的封装,类似于函数调用。用一些参数初始化一个对话框。它接收来自用户的输入,执行自己的代码或调用其他对话框,并可以向用户发送响应。一旦对话框的目的实现了,它就向调用对话框返回值。简而言之,调用对话框将子对话框推到堆栈的顶部。当子对话框完成时,它会从堆栈中弹出自己。
让我们回到我们选择提示的例子。在对话框堆栈模型中,根对话框放置提示。选择堆栈顶部的对话框。在对话框完成执行后,产生的用户输入对象被向下传递回根对话框。然后根对话框对结果对象做任何它需要做的事情。图 5-37 记录了随时间变化的行为。
图 5-37
一段时间内对话框堆栈上的对话框
我们可以进一步发展这个概念。我们可以想象日历机器人中的一个流程,其中添加一个新的日历条目会调用一个新的对话框。姑且称之为 AddCalendarEntry 。然后它会调用一个提示。Time 对话框收集事件的日期和时间,并调用一个提示。Text 对话框收集事件的主题。AddCalendarEntry 打包收集的数据,并通过调用一些日历 API 创建一个新的日历条目。控制权然后返回到根对话框。我们在图 5-32 中对此进行了说明。我们甚至可以让 AddCalendarEntry 调用另一个对话框,该对话框封装了调用 API 的逻辑,如果该过程足够复杂,并且我们想要重用来自其他对话框的逻辑(图 5-38 )。
图 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 所示。
图 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 中看到的,看起来好像当我们寻求帮助时,我们的机器人忘记了我们在添加约会对话框中。事实上,全局动作匹配的默认行为是替换堆栈顶部的对话框。换句话说,添加约会对话框被移除,并被帮助对话框所取代。
图 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 所示。
图 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(可能是我们在第三章中创建的那个)并将其用作 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 的意图和实体对象被传递到对话框中。
图 5-42
最终由我们的 LUIS 车型提供动力!
练习 5-2
将你的机器人连接到路易斯
在本任务中,您将把一个机器人连接到您在第三章中创建的 LUIS 应用。
-
创建一个空机器人,并创建一个对话框来处理在第三章中创建的每种类型的意图。对于每个对话,只需发送一条带有对话名称的消息。
-
向您的机器人注册 LUIS 识别器,并确认其工作正常。
-
每个对话瀑布的第一个方法是传递会话对象和一个 args 对象。使用调试器浏览对象。 5 来自路易斯的数据是什么结构?或者,将代表 args 对象的 JSON 字符串发送给用户。
识别器是 Bot Builder SDK 中的一个强大功能,它允许我们基于传入的消息为我们的 Bot 配备各种行为。
构建一个简单的日历机器人
理想情况下,我们构建对话的模式变得越来越清晰。本书提供的 git repos 包括一个日历礼宾机器人,我们在本书的剩余章节中构建了这个机器人。对 bot 进行更改的每个章节在 repo 中都有自己的文件夹。Chapter 5 文件夹包括与 LUIS 集成的框架代码,并发回一条消息,说明机器人理解了什么。Auth 和 API 集成将在第七章中介绍。我们在第十章中添加了基本的多语言支持,在第十二章中添加了人工交接,在第十三章中添加了分析集成。
这些是我们打算在第五章回答的一些问题:
-
在 Node 的上下文中,我们如何构造一个 bot 和它的组件对话框?
-
解释传入对话框的数据的代码的一般模式是什么?
-
尽管可以使用 Bot Builder SDK 创建端到端测试,但就其当前形式而言,单元测试对话逻辑并不是最直接的任务。我们如何构建我们的代码,以便尽可能好地进行单元测试?
当我们深入代码并检查不同的组件时,请记住以下几点:
-
随着代码的构建和测试,我们会发现 LUIS 应用中存在一些漏洞。在这段代码的构建过程中,我的模型与第三章中产生的模型有了一点变化。这些不是突破性的变化,而是新的话语和实体。代码示例包括这个版本的模型。
-
我们需要定义每个对话的范围。例如,“编辑日历条目”对话框旨在关注移动约会。
-
我们创建了一些助手类,包含一些最复杂的逻辑,即从 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 的细节的情况下,我们开发了一个聊天机器人,它可以解释各种各样的自然语言输入,执行我们旨在支持第三章中的用例。剩下唯一要做的事情就是引入一个日历 API,并将 LUIS 意图和实体组合转换成正确的 API 调用。
在我们开始之前,我们将更深入地研究 Bot Builder SDK,以确保我们在最终实现中选择了正确的方法。
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
六、深入研究 Bot Builder SDK
在前一章中,我们构建了一个简单的 bot,它可以利用现有的 LUIS 应用和 Bot Builder SDK 来实现日历 Bot 的对话流。就目前情况来看,这个机器人毫无用处。它用描述它从用户输入中理解的内容的文本来响应,但是它没有完成任何实质性的事情。我们正在将我们的 bot 连接到 Google Calendar API,但与此同时,我们需要弄清楚 Bot Builder SDK 为我们提供了哪些工具来创建有意义的对话体验。
在本章中,我们将详细阐述我们在第五章代码中使用的一些技术,并更彻底地探索 Bot Builder SDK 的一些特性。我们将弄清楚 SDK 如何存储状态、构建具有丰富内容的消息、构建动作和卡片,并允许框架定制通道行为、对话行为和用户动作处理。最后,我们将看看如何最好地将机器人功能组合成可重用的组件。
对话状态
正如前面几章所提到的,一个好的对话引擎会存储每个用户和对话的状态,这样每当用户与机器人通信时,都会检索到对话流的正确状态,从而为用户提供一致的体验。在 Bot Builder SDK 中,默认情况下,这种状态通过恰当命名的 MemoryBotStorage 存储在内存中。历史上,状态存储在云端点中;然而,这已被否决。有时,我们可能会在一些旧文档中遇到对 state service 的引用,所以请注意它已经不存在了。
每个对话的状态由 bot 开发人员可以访问的三个桶组成。我们在前一章中介绍了所有这些方法,但要重申的是,它们如下:
-
userData :一个用户在一个频道的所有对话中的数据
-
privateconversiondata:会话范围内的私有用户数据
-
conversationData :会话的数据,由参与会话的任何用户共享
此外,当一个对话框正在执行时,我们可以访问它的状态对象,称为 dialogData 。每当收到来自用户的消息时,Bot Builder SDK 将从状态存储中检索用户的状态,在会话对象上填充三个数据对象和 dialogData,并执行对话中当前步骤的逻辑。一旦所有的响应都被发送出去,框架将把状态保存回状态存储器。
let entry = new et.EntityTranslator(session.dialogData.addEntry);
if (!entry.hasDateTime) {
entry.setEntity(results.response);
}
session.dialogData.addEntry = entry;
在前一章的一些代码中,有些情况下我们必须从 dialogData 重新创建一个自定义对象,然后将该对象存储到 dialogData 中。这样做的原因是,将对象保存到 dialogData(或任何其他状态容器)中会将对象转换为普通的 JavaScript 对象,就像使用 JSON.stringify 一样。在重置为新对象之前,尝试调用前面代码中 session.dialogData.addEntry 上的任何方法都会导致错误。
存储机制是由一个名为 IBotStorage 的接口实现的。
export interface IBotStorage {
getData(context: IBotStorageContext, callback: (err: Error, data: IBotStorageData) => void): void;
saveData(context: IBotStorageContext, data: IBotStorageData, callback?: (err: Error) => void): void;
}
我们在构建新的 bot 实例时实例化的 ChatConnector 类会安装默认的 MemoryBotStorage 实例,这对于开发来说是一个很好的选择。SDK 允许我们提供自己的实现来替换默认功能,这是您在生产部署中最想做的事情,因为这可以确保在实例重新启动时存储状态,而不是删除状态。例如,微软提供了两个额外的接口实现,一个是 Azure Cosmos DB 1 的 NoSQL 实现,另一个是 Azure Table Storage 的实现。 2 两者都是可以通过 Azure 门户获得的 Azure 服务。你可以在 botbuilder-azure Node 包中找到这两个存储实现,记录在 https://github.com/Microsoft/BotBuilder-Azure
。您还可以编写自己的 IBotStorage 实现,并将其注册到 SDK。编写自己的实现就是遵循简单的 IBotStorage 接口。
const bot = new builder.UniversalBot(connector, (session) => {
// ... Bot code ...
})
.set('storage', storageImplementation);
信息
在前一章中,我们的机器人通过使用 session.send 或 session.endDialog 方法发送文本消息来与用户通信。这很好,但它限制了我们的机器人相当数量。机器人和用户之间的消息由我们在前一章的“机器人构建器 SDK 基础”一节中遇到的各种数据组成。
Bot Builder IMessage 接口定义了消息的真正组成。
interface IEvent {
type: string;
address: IAddress;
agent?: string;
source?: string;
sourceEvent?: any;
user?: IIdentity;
}
interface IMessage extends IEvent {
timestamp?: string; // UTC Time when message was sent (set by service)
localTimestamp?: string; // Local time when message was sent (set by client or bot, Ex: 2016-09-23T13:07:49.4714686-07:00)
summary?: string; // Text to be displayed by as fall-back and as short description of the message content in e.g. list of recent conversations
text?: string; // Message text
speak?: string; // Spoken message as Speech Synthesis Markup Language (SSML)
textLocale?: string; // Identified language of the message text.
attachments?: IAttachment[]; // This is placeholder for structured objects attached to this message
suggestedActions: ISuggestedActions; // Quick reply actions that can be suggested as part of the message
entities?: any[]; // This property is intended to keep structured data objects intended for Client application e.g.: Contacts, Reservation, Booking, Tickets. Structure of these object objects should be known to Client application.
textFormat?: string; // Format of text fields [plain|markdown|xml] default:markdown
attachmentLayout?: string; // AttachmentLayout - hint for how to deal with multiple attachments Values: [list|carousel] default:list
inputHint?: string; // Hint for clients to indicate if the bot is waiting for input or not.
value?: any; // Open-ended value.
name?: string; // Name of the operation to invoke or the name of the event.
relatesTo?: IAddress; // Reference to another conversation or message.
code?: string; // Code indicating why the conversation has ended.
}
对于这一章,我们将对文本、附件、建议动作和附件布局最感兴趣,因为它们构成了一个好的对话式 UX 的基础。
为了用代码创建一个消息对象,我们创建一个生成器。消息对象。此时,您可以按照下面的示例分配属性。然后可以将消息传递到 session.send 方法中。
const reply = new builder.Message(session)
.text('Here are some results for you')
.attachmentLayout(builder.AttachmentLayout.carousel)
.attachments(cards);
session.send(reply);
同样,当消息进入您的 bot 时,会话对象包含一个消息对象。同样的界面。相同类型的数据。但是,这一次,它来自通道,而不是来自机器人。
const bot = new builder.UniversalBot(connector, [
(session) => {
const input = session.message.text;
}]);
请注意,IMessage 继承自 IEvent,这意味着它有一个类型字段。该字段被设置为 IMessage 的消息,但也有其他事件可能来自框架或自定义应用。
基于通道支持,bot 框架支持的其他一些事件类型如下:
-
conversationUpdate :在对话中添加或删除用户,或者对话的某些元数据发生变化时引发;用于群聊管理。
-
contactRelationUpdate :在用户的联系人列表中添加或删除机器人时引发。
-
输入:用户输入消息时引发;并非所有频道都支持。
-
ping :判断 bot 端点是否可用。
-
deleteUserData :当用户请求删除他们的用户数据时引发。
-
endOfConversation :当对话结束时引发。
-
invoke :当请求机器人执行一些自定义逻辑时引发。例如,一些通道可能需要调用机器人上的一个函数并期待响应。Bot 框架将把这个请求作为 invoke 请求发送,期待一个同步的 HTTP 回复。这种情况并不常见。
我们可以通过使用 UniversalBot 上的 on 方法为每种事件类型注册一个处理程序。与处理事件的机器人的对话可以为您的用户提供更身临其境的对话体验(图 6-1 )。
图 6-1
响应输入和对话更新事件的机器人
const bot = new builder.UniversalBot(connector, [
(session) => {
}
]);
bot.on('conversationUpdate', (data) => {
if (data.membersAdded && data.membersAdded.length > 0) {
if (data.address.bot.id === data.membersAdded[0].id) return;
const name = data.membersAdded[0].name;
const msg = new builder.Message().address(data.address);
msg.text('Welcome to the conversation ' + name + '!');
msg.textLocale('en-US');
bot.send(msg);
}
});
bot.on('typing', (data) => {
const msg = new builder.Message().address(data.address);
msg.text('I see you typing... You\'ve got me hooked! Reel me in!');
msg.textLocale('en-US');
bot.send(msg);
});
地址和主动信息
在消息接口中,address 属性唯一地表示对话中的用户。看起来是这样的:
interface IAddress {
channelId: string; // Unique identifier for channel
user: IIdentity; // User that sent or should receive the message
bot?: IIdentity; // Bot that either received or is sending the message
conversation?: IIdentity; // Represents the current conversation and tracks where replies should be routed to.
}
地址背后的重要性在于,我们可以使用它在对话范围之外主动发送消息。例如,我们可以创建一个进程,每五秒钟向一个随机地址发送一条消息。这个消息对用户的对话框堆栈没有任何影响。
const addresses = {};
const bot = new builder.UniversalBot(connector, [
(session) => {
const userid = session.message.address.user.id;
addresses[userid] = session.message.address;
session.send('Give me a couple of seconds');
}
]);
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
setInterval(() => {
const keys = Object.keys(addresses);
if (keys.length == 0) return;
const r = getRandomInt(0, keys.length-1);
const addr = addresses[keys[r]];
const msg = new builder.Message().address(addr).text('hello from outside dialog stack!');
bot.send(msg);
}, 5000);
如果我们确实想修改对话框堆栈,也许通过调用一个复杂的对话框操作,我们可以在 UniversalBot 对象上使用 beginDialog 方法。
setInterval(() => {
var keys = Object.keys(addresses);
if (keys.length == 0) return;
var r = getRandomInt(0, keys.length-1);
var addr = addresses[keys[r]];
bot.beginDialog(addr, "dialogname", { arg: true});
}, 5000);
这些概念的意义在于,我们可以让不同系统中的外部事件开始影响用户在机器人内部的对话状态。在下一章中,我们将看到 OAuth web 钩子的应用。
丰富的内容
可以使用 BotBuilder IMessage 界面中的附件功能向用户发送丰富的内容。在 Bot Builder SDK 中,附件只是一个名称、内容 URL 和 MIME 类型。3Bot Builder SDK 中的一条消息接受零个或多个附件。由 bot 连接器将消息翻译成通道能够理解的内容。并非每个频道都支持所有类型的邮件和附件。创建各种 MIME 类型的附件时要小心。
例如,要共享一个图像,我们可以使用下面的代码:
const bot = new builder.UniversalBot(connector, [
(session) => {
session.send({
text: "Here, have an apple.",
attachments: [
{
contentType: 'image/jpeg',
contentUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/1/15/Red_Apple.jpg/1200px-Red_Apple.jpg',
name: 'Apple'
}
]
})
}
]);
图 6-2 显示了模拟器中的用户界面,图 6-3 显示了 Facebook Messenger 中的用户界面。我们可以想象在其他平台上类似的渲染。
图 6-3
Facebook Messenger 图片附件
图 6-2
模拟器图像附件
这段代码将发送音频文件附件,可以在消息通道中直接播放。
const bot = new builder.UniversalBot(connector, [
(session) => {
session.send({
text: "Here, have some sound!",
attachments: [
{
contentType: 'audio/ogg',
contentUrl: 'https://upload.wikimedia.org/wikipedia/en/f/f4/Free_as_a_Bird_%28Beatles_song_-_sample%29.ogg',
name: 'Free as a bird'
}
]
})
}
]);
图 6-4 显示的是模拟器,图 6-5 显示的是 Facebook Messenger()。
图 6-5
Facebook Messenger 中的 OGG 声音文件附件
图 6-4
模拟器中的 OGG 声音文件附件
呜呜!好像不支持 OGG 4 文件。这是一个很好的例子,说明了当我们的机器人向脸书或任何其他通道发送无效消息时,机器人框架的行为。我们将在本章后面的“通道错误”部分对此进行进一步的研究。我的控制台错误日志包含以下消息:
Error: Request to 'https://facebook.botframework.com/v3/conversations/1912213132125901-1946375382318514/activities/mid.%24cAAbqN9VFI95k_ueUOVezaJiLWZXe' failed: [400] Bad Request
如果我们查看 Bot Framework Messenger Channels 页面中的错误列表,我们应该会发现如图 6-6 所示的另一条线索。
图 6-6
Messenger 上 OGG 声音文件的 Bot 框架错误
好了,它们让诊断问题变得有些容易。我们知道我们必须提供不同的文件格式。让我们试试 MP3。
const bot = new builder.UniversalBot(connector, [
(session) => {
session.send({
text: "Ok have a vulture instead!",
attachments: [
{
contentType: 'audio/mp3',
contentUrl: 'http://static1.grsites.com/archive/sounds/birds/birds004.mp3',
name: 'Vulture'
}
]
})
}
]);
你可以在图 6-7 和图 6-8 中看到模拟器和 Facebook Messenger 的渲染结果。
图 6-8
Facebook Messenger MP3 文件附件
图 6-7
模拟器 MP3 文件附件
模拟器仍然产生一个链接,但 Messenger 有一个内置的音频播放器,你可以利用!上传视频的体验也是类似的。Messenger 将在对话中提供内置的视频播放器。
练习 6-1
试验附件
这个练习的目标是编写一个简单的机器人,它可以向用户发送不同类型的附件,并观察模拟器和另一个通道(如 Facebook Messenger)的行为。
-
使用 echo bot 作为起点创建一个基本 bot。
-
通过 bot 功能,在消息中发送不同类型的附件,如 JSON、XML 或 file。尝试一些类型的富媒体,如视频。模拟器如何呈现这些类型的附件?Messenger 怎么样?
-
尝试从模拟器向机器人发送图像。传入的消息包含什么数据?这与您通过 Messenger 发送图像有什么不同吗?
附件是与用户共享各种丰富内容的一种简单方式。明智地使用它们来创造丰富多彩、引人入胜的对话体验。
小跟班
机器人还可以向用户发送按钮。按钮是用户执行任务的一个独特的行动号召。每个按钮都有一个与之关联的标签,以及一个值。按钮也有一个动作类型,它将决定当按钮被点击时按钮如何处理这个值。三种最常见的操作类型是打开 URL、回发和 IM back。Open URL 通常在消息应用中打开一个 web 视图,或者在桌面设置中打开一个新的浏览器窗口。post back 和 IM back 都将按钮的值作为消息发送给机器人。两者的区别在于,点击回发不应该在聊天记录中显示来自用户的消息,而 IM 回发应该显示。并非所有通道都实现这两种类型的按钮。
const bot = new builder.UniversalBot(connector, [
(session) => {
const cardActions = [
builder.CardAction.openUrl(session, 'http://google.com', "Open Google"),
builder.CardAction.imBack(session, "Hello!", "Im Back"),
builder.CardAction.postBack(session, "Hello!", "Post Back")
];
const card = new builder.HeroCard(session).buttons(cardActions);
const msg = new builder.Message(session).text("sample actions").addAttachment(card);
session.send(msg);
}
]);
注意,在前面的代码中,我们使用了一个 CardAction 对象。CardAction 是我们前面讨论过的数据的封装:动作类型、标题和值。通道连接器通常会将一个动作呈现到单个平台上的一个按钮中。
图 6-9 显示了在模拟器中运行这段代码的样子,图 6-10 显示了它在 Facebook Messenger 中的样子。如果我们点击模拟器中的 Open Google 按钮,它会在默认浏览器中打开网页。我们首先点击 Im Back,然后一旦收到回复卡,我们就点击 Post Back。请注意,Im Back 发送了一条消息,该消息出现在聊天历史中,而 Post Back 按钮发送了一条机器人会响应的消息,但该消息不会出现在聊天历史中。
图 6-9
模拟器中 Bot Builder 按钮行为的示例
Messenger 的工作方式略有不同。 5 我们来看看手机 app 行为。如果我们点击打开谷歌,一个网页视图将会出现,覆盖了大约 90%的屏幕。如果我们点击 Im Back 和 Post Back,应用会显示相同的行为。Messenger 仅支持回发;此外,消息值永远不会显示给用户。聊天记录只包含被点击按钮的标题。
图 6-10
Facebook Messenger 中的按钮行为示例
Bot Builder SDK 支持以下操作类型:
-
openUrl :在浏览器中打开一个 Url
-
imBack :从用户向机器人发送一条消息,所有对话参与者都可以看到这条消息
-
回发:从用户向机器人发送一条消息,这条消息可能对所有对话参与者都不可见
-
通话:拨打电话
-
playAudio :在 bot 界面中播放音频文件
-
playVideo :在 bot 界面中播放视频文件
-
showImage :显示机器人界面内的图像
-
下载文件:下载文件到设备
-
登录:启动 OAuth 流程
当然,并不是所有通道都支持所有类型。此外,通道本身可能支持 Bot Builder SDK 不支持的其他功能。例如,图 6-11 显示了截至本文撰写时 Messenger 通过其按钮模板支持的操作的文档。在本章的后面,我们将研究如何利用本机通道功能。
图 6-11
Messenger 按钮模板类型
在 Bot Builder SDK 中,可以通过使用 card action 类中的静态工厂方法来创建每个卡片动作。以下是来自 Bot Builder 源代码的相关代码:
CardAction.call = function (session, number, title) {
return new CardAction(session).type('call').value(number).title(title || "Click to call");
};
CardAction.openUrl = function (session, url, title) {
return new CardAction(session).type('openUrl').value(url).title(title || "Click to open website in your browser");
};
CardAction.openApp = function (session, url, title) {
return new CardAction(session).type('openApp').value(url).title(title || "Click to open website in a webview");
};
CardAction.imBack = function (session, msg, title) {
return new CardAction(session).type('imBack').value(msg).title(title || "Click to send response to bot");
};
CardAction.postBack = function (session, msg, title) {
return new CardAction(session).type('postBack').value(msg).title(title || "Click to send response to bot");
};
CardAction.playAudio = function (session, url, title) {
return new CardAction(session).type('playAudio').value(url).title(title || "Click to play audio file");
};
CardAction.playVideo = function (session, url, title) {
return new CardAction(session).type('playVideo').value(url).title(title || "Click to play video");
};
CardAction.showImage = function (session, url, title) {
return new CardAction(session).type('showImage').value(url).title(title || "Click to view image");
};
CardAction.downloadFile = function (session, url, title) {
return new CardAction(session).type('downloadFile').value(url).title(title || "Click to download file");
};
信用卡
另一种类型的机器人建设者附件是英雄卡。在我们之前的按钮动作的例子中,我们忽略了按钮动作需要成为英雄卡对象的一部分,但是那是什么呢?
英雄卡一词源于赛车界。卡片本身通常比棒球卡要大,旨在宣传比赛团队,特别是车手和赞助商。它包括照片、关于司机和赞助商的信息、联系信息等等。但实际上这个概念让人想起典型的棒球或神奇宝贝卡片。
在 UX 设计中,卡片是展示图像、文本和动作的一种有组织的方式。当谷歌在 Android 和网络上向世界介绍其材料设计 6 时,它给大众带来了卡片。图 6-12 显示了来自谷歌材料设计文档的两个卡片设计示例。注意图像、标题、副标题和行动号召的不同用法。
图 6-12
谷歌的材料设计卡样本
在机器人的上下文中,术语英雄卡指的是一组带有文本、动作按钮和可选默认点击行为的图像。不同的通道会叫卡不同的东西。脸书称它们为模板。其他平台只是将这种想法称为将内容附加到消息中。归根结底,UX 的概念是一样的。
在 Bot Builder SDK 中,我们可以使用以下代码创建一个卡。我们还展示了这张卡片如何在模拟器中呈现(图 6-13 )和在 Facebook Messenger 上呈现(图 6-14 )。
图 6-14
Facebook Messenger 中的相同英雄卡
图 6-13
由模拟器渲染的英雄卡
const bot = new builder.UniversalBot(connector, [
(session) => {
const cardActions = [
builder.CardAction.openUrl(session, 'http://google.com', "Open Google"),
builder.CardAction.imBack(session, "Hello!", "Im Back"),
builder.CardAction.postBack(session, "Hello!", "Post Back")
];
const card = new builder.HeroCard(session)
.buttons(cardActions)
.text('this is some text')
.title('card title')
.subtitle('card subtitle')
.images([new builder.CardImage(session).url("https://bot-framework.azureedge.net/bot-icons-v1/bot-framework-default-7.png").toImage()])
.tap(builder.CardAction.openUrl(session, "http://dev.botframework.com"));
const msg = new builder.Message(session).text("sample actions").addAttachment(card);
session.send(msg);
}
]);
卡片是传达用户调用的机器人操作结果的一种很好的方式。如果你想用图像和后续行动显示一些数据,没有比使用卡片更好的方法了。事实上,您得到的只是几个不同的文本字段,具有有限的格式化能力,这意味着这种方法产生的 UX 可能有点有限。这是有意的。对于更复杂的可视化和场景,您可以利用自适应卡或渲染自定义图形。我们将在第十一章探讨这两个主题。
下一个问题是,我们能以旋转木马的方式并排展示卡片吗?当然可以。Bot Builder SDK 中的消息有一个名为 attachmentLayout 的属性。我们将此设置为 carousel,添加更多的卡,我们就完成了!模拟器(图 6-15 )和 Facebook Messenger(图 6-16 )负责以一种友好的转盘格式将卡片展开。默认的附件布局是一个列表。使用这种布局,卡片会一张一张地出现。这不是最用户友好的方法。
图 6-16
信使上同样的英雄卡旋转木马
图 6-15
模拟器中的英雄卡转盘
const bot = new builder.UniversalBot(connector, [
(session) => {
const cardActions = [
builder.CardAction.openUrl(session, 'http://google.com', "Open Google"),
builder.CardAction.imBack(session, "Hello!", "Im Back"),
builder.CardAction.postBack(session, "Hello!", "Post Back")
];
const msg = new builder.Message(session).text("sample actions");
for(let i=0;i<3;i++) {
const card = new builder.HeroCard(session)
.buttons(cardActions)
.text('this is some text')
.title('card title')
.subtitle('card subtitle')
.images([new builder.CardImage(session).url("https://bot-framework.azureedge.net/bot-icons-v1/bot-framework-default-7.png").toImage()])
.tap(builder.CardAction.openUrl(session, "http://dev.botframework.com"));
msg.addAttachment(card);
}
msg.attachmentLayout(builder.AttachmentLayout.carousel);
session.send(msg);
}
]);
卡片可能有点棘手,因为按钮和图像有很多种布局方式。每个平台都有稍微不同的规则。在某些平台上,openUrl 按钮(但不是其他的)必须指向一个 HTTPS 地址。还可能存在限制每张卡的按钮数量、转盘中的卡数量和图像纵横比的规则。微软的机器人框架将尽可能以最好的方式处理这一切,但意识到这些限制将有助于我们调试我们的机器人。
建议的行动
我们已经在对话式设计的背景下讨论了建议的动作;它们是特定于消息上下文的操作,可以在收到消息后立即执行。如果有另一条消息进来,上下文就会丢失,建议的操作也会消失。这与卡片操作相反,卡片操作几乎永远留在聊天记录中。典型的建议动作 UX,也称为快速回复,是沿着屏幕底部水平排列的按钮列表。
构建建议动作的代码类似于英雄卡片,除了我们需要的唯一数据是卡片动作的集合。“建议操作”区域中允许的操作类型将取决于频道。图 6-17 和图 6-18 分别显示了模拟器和 Facebook Messenger 上的效果图。
图 6-18
在 Messenger 中建议相同的操作
图 6-17
模拟器中呈现的建议操作
msg.suggestedActions(new builder.SuggestedActions(session).actions([
builder.CardAction.postBack(session, "Option 1", "Option 1"),
builder.CardAction.postBack(session, "Option 2", "Option 2"),
builder.CardAction.postBack(session, "Option 3", "Option 3")
]));
建议的动作按钮很好地保持了与用户的对话,而不要求用户猜测他们可以在文本消息字段中键入什么。
练习 6-2
卡片和 建议动作
字典和辞典是好的机器人导航体验的好灵感。用户可以输入一个单词。得到的卡片可以显示单词的图像和定义。下面的一个按钮可以让我们打开一个参考页面,比如在 https://www.merriam-webster.com/
上。建议的动作可以是一组当前单词的同义词按钮。让我们把这种互动落实到位。
-
使用
https://dictionaryapi.com
创建帐户并建立连接。这个 API 将允许您使用字典和同义词库 API。 -
创建一个机器人,它可以使用 Dictionary API 根据用户输入查找单词,并以包含单词和定义文本的 hero card 作为响应。包括一个打开词典网站上该单词页面的按钮。
-
连接到同义词库 API,返回前十个同义词作为建议操作。
-
作为奖励,使用 Bing 图像搜索 API 来填充卡片中的图像。您可以在 Azure 中获得一个访问密钥,并使用以下示例作为指南:
https://docs.microsoft.com/en-us/azure/cognitive-services/bing-image-search/image-search-sdk-node-quickstart
。
现在,您已经有了将您的机器人连接到不同 API 并将这些 API 响应转换成英雄卡、按钮和建议动作的经验。干得好!
通道误差
在“丰富内容”部分,我们注意到当我们的机器人向 Facebook Messenger 连接器发送错误请求时,我们的机器人将收到 HTTP 错误。这个错误也被打印在机器人的控制台输出中。似乎脸书机器人连接器从脸书 API 向我们的机器人报告了一个错误。太酷了。我们看到的额外功能是 Azure 中的频道详情页面也包含所有这些错误。虽然很小,但这是一个强大的功能。它允许我们快速查看有多少消息被 API 拒绝以及错误代码。我们遇到的情况是,不支持特定的文件类型格式,这只是许多可能的错误之一。如果消息格式不正确,如果存在身份验证问题,或者如果脸书出于任何其他原因拒绝连接器消息,我们都会看到错误。类似的想法也适用于另一组连接器。一般来说,连接器善于将 Bot 框架活动转换成不会被通道拒绝的东西,但它确实发生了。
一般来说,如果我们的 bot 向 Bot 框架连接器发送消息,而消息没有出现在接口上,那么连接器和通道之间的交互很可能有问题,这个在线错误日志将包含有关失败的信息。
频道数据
我们已经多次提到,不同的通道可能会以不同的方式呈现消息,或者对某些项目有不同的规则,例如旋转木马中英雄卡的数量或英雄卡中按钮的数量。我们已经展示了 Messenger 和模拟器渲染的例子,因为这些通道通常工作良好。Skype 是另一个支持大量 Bot Builder 功能的软件(这很有意义,因为两者都属于微软)。Slack 对这些特性没有丰富的支持,但是它的可编辑消息是一个巧妙的特性,我们将在第八章中介绍。
为了便于说明,图 6-19 是之前讨论过的具有建议动作的转盘在松弛状态下的样子。
图 6-19
Slack 中呈现的相同 Bot 生成器对象
那不是旋转木马。Slack 里没有这个概念!也没有什么牌可言;它只是带有附件的消息。图像也不可点击;默认链接显示在图像上方。Im Back 和 Post Back 按钮都显示为回发。没有建议行动/快速回复的概念。您可以在网上找到有关松弛消息格式的更多信息。 7
然而,Bot Builder SDK 背后的团队已经考虑到了这样一个问题,即您可能希望指定确切的本机通道消息,而不是该通道的默认 Bot 框架连接器呈现。解决方案是在消息对象上提供一个包含传入消息的本机通道 JSON 数据的字段,以及一个可能包含本机通道 JSON 响应的字段。
Node SDK 中使用的术语是 source event(Bot Builder 的 C#版本将这个概念称为 channelData)。Node SDK 中的 sourceEvent 存在于 IEvent 接口上。记住,这也是 IMessage 实现的接口。这意味着来自 bot 连接器的任何事件都可能包含原始通道 JSON。
让我们看看 Facebook Messenger 中的一个特性,它并不容易被 Bot 框架支持。默认情况下,Messenger 中的卡片要求图像的宽高比为 1.91:1。 8 连接者默认的英雄卡转换利用了这个模板。然而,有能力利用 1:1 的图像比例。文档中还有其他被 Bot 框架隐藏的选项。例如,脸书有一个特殊的标志,将卡片设置为可共享。此外,您可以控制由 Messenger 中的 openURL 按钮调用的 WebView 的大小。现在,我们将坚持修改图像的纵横比。
首先,让我们看看发送相同卡片的代码,我们已经使用 hero card 对象发送了该卡片,但是使用了脸书的本地格式:
const bot = new builder.UniversalBot(connector, [
(session) => {
if (session.message.address.channelId == 'facebook') {
const msg = new builder.Message(session);
msg.sourceEvent({
facebook: {
attachment: {
type: 'template',
payload: {
template_type: 'generic',
elements: [
{
title: 'card title',
subtitle: 'card subtitle',
image_url: 'https://bot-framework.azureedge.net/bot-icons-v1/bot-framework-default-7.png',
default_action: {
type: 'web_url',
url: 'http://dev.botframework.com',
webview_height_ratio: 'tall',
},
buttons: [
{
type: "web_url",
url: "http://google.com",
title: "Open Google"
},
{
type: 'postback',
title: 'Im Back',
payload: 'Hello!'
},
{
type: 'postback',
title: 'Post Back',
payload: 'Hello!'
}
]
}
],
}
}
}
});
session.send(msg);
} else {
session.send('this bot is unsupported outside of facebook!');
}
}
]);
渲染图(图 6-20 )看起来与使用英雄卡的渲染图一样。
图 6-20
在 Messenger 中呈现通用模板
我们设置 image_aspect_ratio 为正方形,现在脸书渲染为正方形(图 6-21 )!
图 6-21
在 Messenger 上呈现带有方形图像的通用模板
const msg = new builder.Message(session);
msg.sourceEvent({
facebook: {
attachment: {
type: 'template',
payload: {
template_type: 'generic',
image_aspect_ratio: 'square',
// more...
}
}
}
});
session.send(msg);
就这么简单!这只是一个味道。在第八章中,我们将探索使用 Bot 框架来集成本机 Slack 特性。
群聊
有些类型的机器人是为了在群体环境中使用。在 Messenger、Twitter direct messages 或类似平台的环境中,用户和机器人之间的交互通常是一对一的。然而,一些频道,尤其是 Slack,专注于协作。在这种情况下,同时与多个用户对话的能力变得非常重要。让你的机器人能够有效地参与群体对话以及正确处理提及标签是非常重要的。
一些通道将允许机器人查看在通道中用户之间发送的每一条消息。其他频道只会在提到机器人时向其发送消息(例如,“嘿@szymonbot,写一本关于机器人的书好吗?”).
如果我们在一个允许我们的机器人在一个群组设置中查看所有消息的通道中,我们的机器人可以监控对话并根据讨论悄悄执行代码(因为回复群组对话中的每条消息有点烦人),或者它可以忽略没有提到机器人的所有内容。它还可以实现这两种行为的组合,通过使用某个命令来激活机器人,并使其变得健谈。
在“消息”部分,我们展示了消息的界面。我们忽略了实体列表,但它在这里变得相关。我们可能从连接器接收到的一种类型的实体是提及。该对象包括上述用户的姓名和 id,如下所示:
{
mentioned: {
id: '',
name: ''
},
text: ''
};
脸书不支持这种类型的实体,但 Slack 支持。我们将在第八章中连接一个机器人到 Slack,但与此同时,这里的代码可以在直接消息场景中总是回复,但只有在群聊中被提到时才会回复:
const bot = new builder.UniversalBot(connector, [
(session) => {
const botMention = _.find(session.message.entities, function (e) { return e.type == 'mention' && e.mentioned.id == session.message.address.bot.id; });
if (session.message.address.conversation.isGroup && botMention) {
session.send('hello ' + session.message.user.name + '!');
}
else if (!session.message.address.conversation.isGroup) {
// 1 on 1 session
session.send('hello ' + session.message.user.name + '!');
} else {
// silently looking at non-mention messages
// session.send('bein creepy...');
}
session.send(msg);
}
]);
图 6-22 是在直接对话的松弛状态下的体验。
图 6-22
Slack 中支持群聊的机器人直接发送消息
图 6-23 显示了群聊中的行为(原谅过于原始的用户名 srozga2)。
图 6-23
支持群聊的机器人忽略没有提及的消息
自定义对话框
我们已经使用 bot.dialog(…)方法构建了我们的对话框。我们还讨论了瀑布的概念。在我们在前一章开始的日历机器人中,我们的每个对话都是通过瀑布实现的:一组按顺序执行的步骤。我们可以跳过一些步骤或在所有步骤完成之前结束对话,但预定义顺序的想法是关键。这个逻辑是由 Bot Builder SDK 中的一个名为 WaterfallDialog 的类实现的。如果我们看看对话框(…)调用背后的代码,我们会发现这一点:
if (Array.isArray(dialog) || typeof dialog === 'function') {
d = new WaterfallDialog(dialog);
} else {
d = <any>dialog;
}
如果我们想要编码的对话片段不容易用瀑布式抽象来表示呢?我们有什么选择?我们可以创建一个对话框的自定义实现!
在 Bot Builder SDK 中,对话框是一个表示用户和 Bot 之间某种交互的类。对话框可以调用其他对话框,并接受这些子对话框的返回值。它们存在于对话堆栈中,与函数调用堆栈没有什么不同。使用默认的瀑布帮助器隐藏了其中的一些细节;实现一个定制的对话框让我们更接近对话框堆栈的现实。Bot Builder 中的抽象对话框类如下所示:
export abstract class Dialog extends ActionSet {
public begin<T>(session: Session, args?: T): void {
this.replyReceived(session);
}
abstract replyReceived(session: Session, recognizeResult?: IRecognizeResult): void;
public dialogResumed<T>(session: Session, result: IDialogResult<T>): void {
if (result.error) {
session.error(result.error);
}
}
public recognize(context: IRecognizeDialogContext, cb: (err: Error, result: IRecognizeResult) => void): void {
cb(null, { score: 0.1 });
}
}
Dialog 只是一个我们可以继承的类,它有四个重要的方法。
-
Begin :当对话框第一次放入堆栈时调用。
-
ReplyReceived :每当用户的消息到达时调用。
-
DialogResumed :当子对话框结束,当前对话框再次激活时调用。dialogResumed 方法接收的参数之一是子对话框的结果对象。
-
识别:允许我们添加自定义对话框识别逻辑。默认情况下,BotBuilder 提供声明性方法来设置自定义全局或对话框范围的识别。但是,如果我们想添加进一步的识别逻辑,我们可以使用这种方法。我们将在“操作”部分对此进行更深入的讨论。
为了说明这些概念,我们创建了一个 BasicCustomDialog。由于 Bot Builder 是用 TypeScript 编写的, 9 是 JavaScript 的一个类型化超集,我们继续用 TypeScript 编写子类,用 TypeScript 编译器(tsc)编译成 JavaScript,然后在 app.js 中使用它
让我们看看自定义对话框的代码。这恰好是 TypeScript,因为它在使用继承时有一个更干净的接口;编译后的 JavaScript 将在后面显示。当对话开始时,它发送“开始”文本。当它收到一条消息时,它用“已收到回复”文本进行响应。如果用户发送了“提示”文本,对话框将要求用户输入一些文本。然后,它将在 dialogResumed 方法中接收文本输入,并打印结果。如果用户输入了“done”,则对话框结束并返回到根对话框。
import { Dialog, ResumeReason, IDialogResult, Session, Prompts } from 'botbuilder'
export class BasicCustomDialog extends Dialog {
constructor() {
super();
}
// called when the dialog is invoked
public begin<T>(session: Session, args?: T): void {
session.send('begin');
}
// called any time a message is received
public replyReceived(session: Session): void {
session.send('reply received');
if(session.message.text === 'prompt') {
Prompts.text(session, 'please enter any text!');
} else if(session.message.text == 'done') {
session.endDialog('dialog ending');
} else {
// no-op
}
}
public dialogResumed(session: Session, result: any): void {
session.send('dialog resumed with value: ' + result);
}
}
我们在 app.js 中直接使用对话框的实例。在默认的瀑布中,我们回显任何消息,除了开始自定义对话框的“自定义”输入。
const bot = new builder.UniversalBot(connector, [
(session) => {
if(session.message.text === 'custom') {
session.beginDialog('custom');
} else {
session.send('echo ' + session.message.text);
}
}
]);
const customDialogs = require('./customdialogs');
bot.dialog('custom', new customDialogs.BasicCustomDialog());
图 6-24 显示了一个示例交互的样子。
图 6-24
与自定义对话框交互
顺便提一下,Promps.text、Prompts.number 和其他提示对话框都是作为自定义对话框实现的。
接下来显示了为定制对话框编译的 JavaScript。推理起来有点困难,但归根结底,这是标准的 ES5 JavaScript 原型继承。10
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
exports.__esModule = true;
var botbuilder_1 = require("botbuilder");
var BasicCustomDialog = /** @class */ (function (_super) {
__extends(BasicCustomDialog, _super);
function BasicCustomDialog() {
return _super.call(this) || this;
}
// called when the dialog is invoked
BasicCustomDialog.prototype.begin = function (session, args) {
session.send('begin');
};
// called any time a message is received
BasicCustomDialog.prototype.replyReceived = function (session) {
session.send('reply received');
if (session.message.text === 'prompt') {
botbuilder_1.Prompts.text(session, 'please enter any text!');
}
else if (session.message.text == 'done') {
session.endDialog('dialog ending');
}
else {
// no-op
}
};
BasicCustomDialog.prototype.dialogResumed = function (session, result) {
session.send('dialog resumed with value: ' + result);
};
return BasicCustomDialog;
}(botbuilder_1.Dialog));
exports.BasicCustomDialog = BasicCustomDialog;
练习 6-3
实现自定义提示号
作为自定义对话框概念的练习,您现在将创建一个自定义 Prompts.number 对话框。这个练习纯粹是学术性的;了解框架级的行为是如何实现的是很有趣的。
-
创建一个具有两步瀑布的 bot,它使用标准的 Prompts.number 收集一个数值,并在第二个瀑布步骤中将该数值发送回用户。请注意,您将在瀑布函数的 args 参数上使用响应字段。
-
创建一个自定义对话框,收集用户输入,直到它收到一个数字。出于练习的目的,您可以使用 parseFloat。当收到有效的号码时,使用与 Prompts.number 返回的结构相同的对象调用 session.endDialogWithResult。如果用户的输入无效,则返回一条错误消息并再次请求号码。
-
在瀑布中,不要调用 Prompts.number,而是调用新的自定义对话框。你的瀑布应该还能用!
-
另外,在你的自定义对话框中添加逻辑,允许最多五次尝试。之后,返回一个取消的结果到你的瀑布。
您现在已经了解了 Bot Builder SDK 中所有对话框的构建块!我们可以利用这些知识来建立任何形式的互动。
行动
我们现在对抽象对话框有多强大以及 Bot Builder SDK 如何管理对话框堆栈有了很好的了解。该框架的一个关键部分是如何将用户动作与对话堆栈的转换联系起来,我们对此没有很好的理解。在最基本的层面上,我们可以编写简单地调用 beginDialog 的代码。但是我们如何根据用户输入做出决定呢?我们如何将它与我们在前一章中学习的识别器挂钩,特别是 LUIS?这就是行动允许我们做的事情。
Bot Builder SDK 包含六种类型的操作,其中两种是全局的,四种是对话框范围的。这两个全局操作是 triggerAction 和 customAction。我们以前遇到过触发作用。它允许机器人在对话期间的任何时候当意图匹配时调用对话,假设该意图事先不匹配对话范围内的动作。每当接收到用户输入时,都会对这些进行评估。默认行为是在调用对话框之前清除整个对话框堆栈。
lib.dialog(constants.dialogNames.AddCalendarEntry, [
function (session, args, next) {
...
]).triggerAction({
matches: constants.intentNames.AddCalendarEntry
});
上一章日历机器人代码中的每个主要对话框都使用默认的 triggerAction 行为,除了帮助。帮助对话框在对话框栈的顶部被调用*,所以当它完成时,我们回到用户开始所在的对话框。为了达到这个效果,我们重写 onSelectAction 方法并指定我们想要的行为。*
lib.dialog(constants.dialogNames.Help, (session, args, next) => {
...
}).triggerAction({
matches: constants.intentNames.Help,
onSelectAction: (session, args, next) => {
session.beginDialog(args.action, args);
}
});
customAction 直接绑定到 bot 对象,而不是对话框。它允许我们绑定一个函数来响应用户输入。我们没有机会像对话框实现那样向用户查询更多信息。这对于简单地返回消息或根据用户输入执行一些 HTTP 调用的功能来说是很好的。事实上,我们可以像这样重写帮助对话框。代码看起来很简单,但是我们失去了对话模型的封装性和可扩展性。换句话说,我们在自己的对话框中不再有逻辑,不再有执行几个步骤、收集用户输入或向调用对象提供结果的能力。
lib.customAction({
matches: constants.intentNames.Help,
onSelectAction: (session, args, next) => {
session.send("Hi, I am a calendar concierge bot. I can help you create, delete and move appointments. I can also tell you about your calendar and check your availability!");
}
});
四种类型的上下文操作是 beginDialogAction、reloadAction、cancelAction 和 endConversationAction。让我们逐一检查。
BeginDialogAction 创建一个操作,只要该操作匹配,就会在堆栈上推一个新对话框。日历机器人中的上下文帮助对话框使用了这种方法。我们创建了两个对话框:一个是 AddCalendarEntry 对话框的帮助,另一个是 RemoveCalendarEntry 对话框的帮助。
// help message when help requested during the add calendar entry dialog
lib.dialog(constants.dialogNames.AddCalendarEntryHelp, (session, args, next) => {
const msg = "To add an appointment, we gather the following information: time, subject and location. You can also simply say 'add appointment with Bob tomorrow at 2pm for an hour for coffee' and we'll take it from there!";
session.endDialog(msg);
});
// help message when help requested during the remove calendar entry dialog
lib.dialog(constants.dialogNames.RemoveCalendarEntryHelp, (session, args, next) => {
const msg = "You can remove any calendar either by subject or by time!";
session.endDialog(msg);
});
然后,我们的 AddCalendarEntry 对话框可以将 beginDialogAction 绑定到相应的帮助对话框。
lib.dialog(constants.dialogNames.AddCalendarEntry, [
// code
]).beginDialogAction(constants.dialogNames.AddCalendarEntryHelp, constants.dialogNames.AddCalendarEntryHelp, { matches: constants.intentNames.Help })
.triggerAction({ matches: constants.intentNames.AddCalendarEntry });
请注意,此操作的行为与手动调用 beginDialog 相同。新对话框放在对话框堆栈的顶部,当前对话框完成后继续。
reloadAction 调用执行 replaceDialog。replaceDialog 是 session 对象上的一个方法,该方法结束当前对话框并用另一个对话框的实例替换它。在新对话框完成之前,父对话框不会得到结果。在实践中,我们可以利用它来重新开始一个交互,或者在流程中间切换到一个更合适的对话。
以下是对话的代码(见图 6-25 ):
图 6-25
触发 reloadAction 的示例对话
lib.dialog(constants.dialogNames.AddCalendarEntry, [
// code
])
.beginDialogAction(constants.dialogNames.AddCalendarEntryHelp, constants.dialogNames.AddCalendarEntryHelp, { matches: constants.intentNames.Help })
.reloadAction('startOver', "Ok, let's start over...", { matches: /^restart$/i })
.triggerAction({ matches: constants.intentNames.AddCalendarEntry });
CancelAction 允许我们取消当前对话框。父对话框将在其恢复处理程序中收到一个设置为 true 的取消标志。这允许对话框正确地对取消进行操作。代码如下(对话可视化如图 6-26 所示):
图 6-26
触发取消的示例对话
lib.dialog(constants.dialogNames.AddCalendarEntry, [
// code
])
.beginDialogAction(constants.dialogNames.AddCalendarEntryHelp, constants.dialogNames.AddCalendarEntryHelp, { matches: constants.intentNames.Help })
.reloadAction('startOver', "Ok, let's start over...", { matches: /^restart$/i })
.cancelAction('cancel', 'Cancelled.', { matches: /^cancel$/i})
.triggerAction({ matches: constants.intentNames.AddCalendarEntry });
最后,endConversationAction 允许我们绑定到 session.endConversation 调用。结束对话意味着清除整个对话堆栈,并从状态存储中删除所有用户和对话数据。如果用户再次开始向机器人发送消息,就会创建一个新的对话,而不知道之前的交互。代码如下(图 6-27 显示对话可视化):
图 6-27
触发 endConversationAction 的示例对话
lib.dialog(constants.dialogNames.AddCalendarEntry, [
// code
])
.beginDialogAction(constants.dialogNames.AddCalendarEntryHelp, constants.dialogNames.AddCalendarEntryHelp, { matches: constants.intentNames.Help })
.reloadAction('startOver', "Ok, let's start over...", { matches: /^restart$/i })
.cancelAction('cancel', 'Cancelled.', { matches: /^cancel$/i})
.endConversationAction('end', "conversation over!", { matches: /^end!$/i })
.triggerAction({ matches: constants.intentNames.AddCalendarEntry });
关于行动的额外注释
回想一下上一章,每个识别器接受一个用户输入,并返回一个带有意图文本值和分数的对象。我们提到了这样一个事实,即我们可以使用识别器来确定 LUIS 的意图,使用正则表达式,或者实现任何定制逻辑。我们创建的每个动作中的匹配对象是我们指定一个动作对哪个识别器意图感兴趣的一种方式。matches 对象实现以下接口:
export interface IDialogActionOptions {
matches?: RegExp|RegExp[]|string|string[];
intentThreshold?: number;
onFindAction?: (context: IFindActionRouteContext, callback: (err: Error | null, score: number, routeData?: IActionRouteData) => void) => void;
onSelectAction?: (session: Session, args?: any, next?: Function) => void;
}
以下是该对象包含的内容:
-
Matches 是操作要查找的目的名称或正则表达式。
-
intentThreshold 是识别器为使此操作被调用而必须分配给意图的最低分数。
-
onFindAction 允许我们在检查一个动作是否应该被触发时调用定制逻辑。
-
onSelectAction 允许您自定义操作的行为。例如,如果您不想清除对话框堆栈,而是想将对话框放在堆栈顶部,请使用它。在之前的动作示例中,我们已经看到了这一点。
除了这种级别的定制之外,Bot Builder SDK 对操作及其优先级有非常具体的规则。回想一下,在关于自定义对话框的讨论中,我们已经看到了全局操作、对话框范围的操作以及每个对话框上可能的识别实现。消息到达时的动作解析顺序如下。首先,系统试图定位当前对话框的识别功能的实现。之后,SDK 查看对话框堆栈,从当前对话框一直到根对话框。如果该路径上没有匹配的动作,则查询全局动作。这个顺序确保最接近当前用户体验的动作被首先处理。当你设计你的机器人交互时,请记住这一点。
图书馆
库是打包和分发相关机器人对话框、识别器和其他功能的一种方式。库可以引用其他库,从而产生功能高度组合的机器人。从开发人员的角度来看,库只是一个包装精美的对话框、识别器和其他 Bot Builder 对象的集合,带有一个名称,通常还有一组帮助调用对话框和其他特定于库的特性的 helper 方法。在我们第五章的日历礼宾机器人中,每个对话框都是与高级机器人功能相关的库的一部分。app.js 代码加载所有模块,然后通过 bot.library 调用将它们安装到主 bot 中。
const helpModule = require('./dialogs/help');
const addEntryModule = require('./dialogs/addEntry');
const removeEntryModule = require('./dialogs/removeEntry');
const editEntryModule = require('./dialogs/editEntry');
const checkAvailabilityModule = require('./dialogs/checkAvailability');
const summarizeModule = require('./dialogs/summarize');
const bot = new builder.UniversalBot(connector, [
(session) => {
// code
}
]);
bot.library(addEntryModule.create());
bot.library(helpModule.create());
bot.library(removeEntryModule.create());
bot.library(editEntryModule.create());
bot.library(checkAvailabilityModule.create());
bot.library(summarizeModule.create());
这是库组合在起作用:UniversalBot 本身就是库的一个子类。我们的主 UniversalBot 库导入了其他六个库。从任何其他上下文中对对话框的引用必须使用库名作为前缀来命名空间。从根库或 UniversalBot 对象中的对话框的角度来看,调用任何其他库的对话框都必须使用格式为 libName:dialogName 的限定名。这种完全限定的对话框名称引用过程只有在跨越库边界时才是必要的。在同一库的上下文中,库前缀不是必需的。
一种常见的模式是在调用库对话框的模块中公开一个助手方法。把它想象成库封装;一个库不应该知道另一个库的内部情况。例如,我们的帮助库公开了一个方法来实现这一点。
const lib = new builder.Library('help');
exports.help = (session) => {
session.beginDialog('help:' + constants.dialogNames.Help);
};
结论
微软的 bot Builder SDK 是一个强大的 Bot 构造库和对话引擎,可以帮助我们开发各种类型的异步对话体验,从简单的来回到具有多种行为的复杂 Bot。对话抽象是一种强大的对话建模方式。识别器定义了我们的机器人用来将用户输入转换成机器可读意图的机制。动作将那些识别器结果映射到对话堆栈上的操作。一个对话框主要关心三件事:当它开始时会发生什么,当收到用户消息时会发生什么,当子对话框返回结果时会发生什么。每个对话框都利用 bot 上下文,称为会话,来检索用户消息并创建响应。响应可以由文本、视频、音频或图像组成。此外,卡片可以产生更丰富和上下文敏感的体验。建议的动作负责防止用户猜测下一步该做什么。
在下一章中,我们将应用这些概念将我们的机器人与谷歌日历 API 集成,我们将采取措施创建一个引人注目的第一版日历机器人体验。
蓝色宇宙 DB: https://azure.microsoft.com/en-us/services/cosmos-db/
2
天蓝色桌面存储: https://azure.microsoft.com/en-us/services/storage/tables/
3
MIME 类型:
4
OGG 格式,一个自由、开放的容器格式: https://en.wikipedia.org/wiki/Ogg
5
Facebook Messenger SendAPI 按钮文档: https://developers.facebook.com/docs/messenger-platform/send-messages/buttons
6
谷歌材质设计: https://material.io/guidelines/
7
时差消息: https://api.slack.com/docs/messages
8
脸书通用模板参考: https://developers.facebook.com/docs/messenger-platform/send-messages/template/generic
9
打字稿: http://www.typescriptlang.org/
10
JavaScript ES5 中的经典继承: https://eli.thegreenplace.net/2013/10/22/classical-inheritance-in-javascript-es5