仅作个人用途,微软 Azure 的机器人框架 SDK Python 分支的学习日志。

目录

-----------------------------

机器人初识

机器人交互

机器人交互涉及到活动的交换,而这些活动在轮次中进行处理。

  1. 活动(activities):活动是用户或者 通道(channel) 与机器人之间的交互。

  2. 轮次(turns):一轮次的对话包含了用户传给机器人的活动,也包含了机器人发给用户的即时响应(也是活动)。

    类似于回合制战斗,速度快的我方先行采取一个行动后,速度慢的对面再采取一个行动,双方行动过后该轮次(或称回合)结束。

机器人应用程序结构

  1. * 机器人(bot)* 类,用于处理机器人应用的聊天推理
    • 识别 & 解释用户的输入
    • 对输入进行推理 & 执行相关任务
    • 生成响应(如:机器人正在干什么)
  2. * 适配器(adapter)* 类,用于处理与通道的连接
    • 提供用于处理来自用户通道的请求的方法
    • 提供用于对用户通道生成请求的方法
    • 包含一个中间件通道,包括机器人轮次处理程序外部的轮次处理
    • 调用机器人的轮次处理程序
    • 捕获不在轮次处理程序中处理的错误

机器人每个轮次还需要检索和存储 状态(state)。状态通过 存储(storage)机器人状态(bot state)属性访问器(property accessor) 类进行处理。

机器人逻辑

  1. 活动处理程序(activity handler),提供事件驱动模型,其中传入的活动类型 & 子类型是 事件(event)
  2. 对话库(dialog library),提供基于状态的模型用于管理与用户进行的长时间聊天。

机器人适配器

适配器提供用于启动轮次的 过程活动(process activity) 方法。

  • 将请求正文和请求头用于参数
  • 检查身份验证头是否有效
  • 为轮次创建一个 上下文(context) 对象
    • 上下文对象包含有关活动的信息
  • 通过中间件管道发送上下文对象
  • 将上下文对象发送到机器人对象的 轮次处理程序(turn handler)

适配器还可以:

  • 格式化 & 发送响应活动
  • 公开机器人连接器(Bot Connector) REST API 提供的其他方法
  • 捕获在轮次中不会被捕获到的错误 & 异常

轮次上下文

轮次上下文(turn context) 对象提供有关活动的信息。

  • 例如发送方和接收方、通道、处理该活动所需的其他数据

轮次上下文不仅将 入站活动(inbound activity) 传递到所有的中间件组件和应用程序逻辑,还提供了所需要的机制让中间件组件和机器人逻辑发送 出站活动(outbound activity)

中间件

SDK 的中间件由一组线性组件构成,其中每一个组件都会按照顺序执行并有一个操作活动的机会。

中间件管道的最后一个阶段:回调机器人类中的轮次处理程序(已经被适配器的过程活动方法注册)。中间件执行被适配器调用的 on turn 方法。

轮次处理程序采用轮次上下文作为参数。在轮次处理程序函数内运行的应用程序逻辑会处理入站活动的内容,并生成活动作为响应,在轮次上下文中调用 send activity 方法来发送出站活动。调用 send activity 方法会导致中间件组件在出站活动上被调用。

中间件组件于轮次处理程序函数之前和之后执行。这些执行在本质上是套娃。

活动处理堆栈

  1. 通道终结点向 Azure 机器人服务发送 HTTP POST 信息
  2. Azure 机器人服务处理活动,发送给适配器和轮次上下文
  3. 适配器和轮次上下文调用 on turn 方法,发送给机器人
  4. 机器人调用 send activity 方法,一个个返回给通道终结点
  5. 通道终结点发送回状态码 200,机器人同理

机器人模板

  • 资源预配
  • 一个特定于语言的 HTTP 终结点实现,可以将传入的活动路由到一个适配器
  • 一个适配器对象
  • 一个机器人对象
-----------------------------

机器人加深认识

管理状态

之前说过,机器人本质上是没有状态的。状态并不是必需的,部分机器人可以不需要状态(也就是用户不提供信息)就正常运行;部分机器人则必须提供了状态才能提供有用的聊天信息,例如以前收到的有关用户的数据。

状态就像是记忆,提供给了机器人后便能让机器人记住有关用户或者本次聊天的信息。

  • 存储层(storage layer),在后端实际存储状态信息的一层。采用物理存储,如:内存、Azure 服务器、第三方服务器。

    • 内存存储:临时存储,机器人一重开就清除
    • Azure Blob 存储:连接到 Azure Blob 存储对象数据库
    • Azure Cosmos DB 分区存储:连接到分区的 Cosmos DB NoSQL 数据库
  • 状态管理(state management),自动在基础存储层中读取 & 写入机器人的状态。状态以 状态属性(state properties) 的键值对形式存储。

    状态属性被集结到有范围的「桶」(帮助组织这些属性的集合)内,SDK 的三个桶分别是:用户状态(user state)聊天状态(conversation state)私人聊天状态(private conversation state)。这些桶又是 bot state 类的子类。

    • 用户状态适合用于跟踪有关用户的信息,如:用户的姓名

    • 聊天状态适合用于跟踪聊天的上下文,如:机器人是否向用户提出了问题,这个问题又是啥

    • 私人聊天状态适合用于支持群组聊天的通道,如:课堂抢答机器人(聚合每位学生的成绩,最终用私聊方式将信息发送给相应的学生)

  • 状态属性访问器(state property accessors),用于实际读取 & 写入某个状态属性,提供了 getsetdelete 方法用于从轮次内部访问状态属性。

    访问器创建需要用到属性名称。之后便可以使用访问器来获取和处理机器人状态的该属性。

    访问器允许 SDK 从基础存储获取状态 & 更新机器人的状态缓存(机器人维护的本地缓存,用于存储状态对象和允许在不访问基础存储的情况下执行读取 & 写入操作)。

    • get 方法,从状态缓存请求属性。如果在缓存中就返回属性,否则从状态管理对象获取该属性

    • set 方法,使用新属性值更新状态缓存

    • delete 方法,从缓存和基础存储中删除属性

    • save changes 方法(状态管理对象的),检查状态缓存中属性所有的更改,并将属性写入存储

      • 要注意,set 方法记录更新的状态后,该状态属性尚未保存到持久性存储,只是保存到机器人的状态缓存内而已。

对话框(dialog) 库使用在机器人的 会话状态(conversation state) 上定义的对话框状态属性访问器来保留对话在会话中的位置。对话框状态属性还允许每个对话框在轮次之中存储临时信息。

对话框管理器(dialog manager) 使用用户和会话状态管理对象提供内存范围(这些内存范围可以用于自适应对话框)。

活动处理程序

生成机器人时,用于处理和响应消息的机器人逻辑将进入 on_message_activity 处理程序。同样,用于处理正在添加到聊天中的成员的逻辑将进入 on_members_added 处理程序。每当一个成员加入到聊天,这个处理程序就会被调用。

机器人逻辑处理来自单个或多个通道的传入活动,并在响应中发生传出活动。

在 Python 里,要使用 ActivityHandler 派生机器人类,前者为不同类型的活动定义各种各样的处理程序,例如上文的 on_message_activity 处理程序。

事件 Handler 说明
已收到任一活动类型 on_turn 根据收到的活动类型,调用其他处理程序。
已收到消息活动 on_message_activity 处理 message 活动。
已收到聊天更新活动 on_conversation_update_activity 收到 conversationUpdate 活动时,如果除了机器人以外的成员加入或者退出聊天,则调用某个处理程序。
非机器人成员加入了聊天 on_members_added_activity 处理加入聊天的成员。
非机器人成员退出了聊天 on_members_removed_activity 处理退出聊天的成员。
已收到事件活动 on_event_activity 收到 event 活动时,调用特定于事件类型的处理程序。
已收到令牌响应事件活动 on_token_response_event 处理令牌响应时间。
已收到非令牌响应事件活动 on_event_activity 处理其他类型的事件。
已收到消息回应活动 on_message_reaction_activity 收到 messageReaction 活动时,如果已经在消息中添加 & 删除一个或多个回应,则调用处理程序。
消息回应已添加到消息 on_reaction_added 处理添加到消息的回应。
从消息中删除了消息回应 on_reaction_removed 处理从消息中删除的回应。
已收到安装更新活动 on_installation_update 对于 installationUpdate 活动,根据机器人是「已安装」还是「已卸载」来调用处理程序。
安装了机器人 on_installation_update_add 添加逻辑来确定何时在组织单位中安装了机器人。
卸载了机器人 on_installation_update_remove 添加逻辑来确定何时在组织单位中卸载了机器人。
已收到其他活动类型 on_unrecognized_activity_type 处理未经处理的任何活动类型。

每个处理程序都有一个 turn_context,用于提供有关对应于入站 HTTP 请求的传入活动的信息。

示例:

  • 处理 on_members_added 来发送欢迎信息,并处理 on_message 来当复读机
python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class EchoBot(ActivityHandler):
async def on_members_added_activity(
self,
members_added: [ChannelAccount],
turn_context: TurnContext
):
"""
每当一个成员加入聊天,就发送`Hello and Welcome`。
"""
for member in members_added:
if member.id != turn_context.activity.recipient.id:
await turn_context.send_activity('Hello and Welcome!')

async def on_message_activity(
self,
turn_context: TurnContext
):
"""
每收到一个消息,就发送`Echo: {消息}`
"""
return await turn_context.send_activity(
MessageFactory.text(f'Echo: {turn_context.activity.text}')
)

对话库

对话框提供管理与用户长期对话的方法。

  • 每一个对话框都代表了一个会话任务(运行完成后可以返回收集到的信息)
  • 每一个对话框都代表了一个基本的控制流单元:可以开始、继续和停止;暂停和恢复;或被取消
  • 对话框类似于编程语言中的方法或者函数。启动对话框时可以传入参数,且该对话框之后可以在结束时生成一个返回值

对话框可以实现 多轮会话(multi-turn conversation),所以对话框依赖于跨多个轮次的 持久性状态(persisted state)。如果对话框中没有状态,机器人就会不知道它在会话中所处的位置,也不知道它已经收集好的信息。

因此,想要在会话中保留对话框的位置,就要在每个轮次中检索对话框状态并保存到内存。这一操作由(机器人的会话状态定义的)对话框状态属性访问器处理。

说明
对话框集(Dialog set) 定义一组对话框,这些对话框可以相互引用 & 协同工作。
对话框上下文(Dialog context) 包含有关所有正在活动中的对话框的信息。
对话框实例(Dialog instance) 包含有关单个正在活动中的对话框的信息。
对话框轮次结果(Dialog turn result) 包含活动的或最近的活动对话框中的状态信息。如果活动对话框已经结束,则包含其返回值。

为了简化管理机器人聊天,对话框库提供了一些对话框类型:

类型 说明
对话框(Dialog) 所有对话框的基类。
容器对话框(Container dialog) 所有容器对话框的基类。
组件对话框(Component dialog) 一种通用类型的容器对话框。它封装了一组对话框作为一个整体重复使用集。
组件对话框启动后,将以其集合中的指定对话框开头。内部进程完成后,组件对话框便结束。
瀑布对话框(Waterfall dialog) 定义一系列步骤,使机器人能够引导用户完成线性流程。
提示对话框(Prompt dialogs) 要求用户输入并返回结果。

三大对话框

  • 组件对话框是一种容器对话框,允许集合中的对话框调用集合中的其他对话框,如:瀑布式对话框调用提示对话框。

    组件对话框还提供了一种创建独立对话框以及处理特定场景的策略,将一个大的对话框集分解成更易于管理的片段。每个片段又都有着自己的对话框集,并避免与包含它的对话框集发生任何名称冲突。

  • 提示对话框是一个旨在向用户询问特定类型信息的对话框,如:一个日期。

  • 瀑布式对话框是对话的具体实现,通常用于收集用户的信息,或者引导用户完成一系列的任务。对话的每一步都被实现为一个需要 瀑布式步骤上下文(waterfall step context) 作为参数的异步函数。

    每一步,机器人提示用户输入,或者可以开启一个子对话框,等待回应,然后将结果传递给下一步。第一个函数的结果被作为参数传给下一个函数,以此类推。

    1. 对话框上下文开始瀑布
    2. 瀑布 #1:第一次提示
    3. 瀑布 #2:处理来自第一个提示的结果,并开始第二次提示
    4. 瀑布 #3:处理来自第二个提示的结果,结束对话(堆栈入口消失)

    瀑布式对话框的上下文被存储在瀑布式步骤上下文中。这个步骤上下文与对话上下文类似,提供对当前轮次上下文和状态的访问。使用瀑布式步骤上下文对象来与瀑布式步骤中的对话框集进行交互。

    对话框的返回值可以在瀑布式步骤中处理,也可以从机器人的轮次处理程序中处理(一般只需要在机器人的轮次论及中检查对话框轮次结果的状态)。


瀑布步骤上下文包含以下属性:

  • Options:包含对话框的输入信息
  • Values:包含可以添加到上下文中的信息,并被带入后续步骤中
  • Result:包含前一个步骤的结果

Python 的 next 方法可以在同一轮次内继续进行瀑布式对话框的下一步,也就是在需要时跳过某个步骤。

提示(Prompt) 提供了一种简单的方法来询问用户的信息并评估他们的反应。

  • 提示本质上就是一个两步的对话框。首先提示会要求输入,然后返回有效值,或者从头开始重新提示。

  • 调用提示时,可以在 提示选项(prompt options) 中指定要提示的文本、如果验证失败了的重新提示,以及回答提示的选择。

    • 一般而言,提示和重新提示属性都属于活动。
  • 当提示被创建时,也可以选择为提示添加自定义验证(如:小于 18 岁就不行的年龄验证)。提示先行检查它是否接收到了一个有效的数字,然后运行自定义验证。假如验证失败了,就重新提示。

  • 当一个提示完成时,就会明确地返回所要求的结果值。

提示 说明 返回值
附件提示(Attachment prompt) 要求一个或多个附件,例如文档或者图片。 一个 附件(attachment) 对象的集合
选项提示(Choice prompt) 从一系列的选项中要求选择一个。 一个找到的选项对象
确认提示(Confirm prompt) 请求提供 YesNo 一个布尔值
日期时间提示(Date-time prompt) 请求提供一个日期时间 一个日期时间解析对象的集合
数字提示(Number prompt) 请求提供一个数字 一个数字值
文本提示(Text prompt) 请求提供一个常规的文字输入 一个字符串

步骤上下文的 prompt 方法的第二个参数要求提供一个 prompt options 对象,该对象包含了这些属性:

属性 描述
初始提示(Prompt / Initial prompt) 发送给用户的初始活动,用来征求用户的输入
重试提示(Retry prompt) 如果用户的第一个输入没有得到验证,就发送该活动
选项(Choices) 一个供用户选择的选项列表,和选项提示配合使用
验证(Validations) 用于自定义验证器的额外参数
样式(Style) 定义选项提示或确认提示的选项将如何呈现给用户

始终应当指定好初始提示和重试提示。假设用户的输入无效,重试提示就会发送给用户;假设重试提示没有被指定,则会发送初始提示。

但是假设发回给用户的活动来自于验证器,就不会发送重试提示。


一个验证器函数带有一个 提示验证器上下文(prompt validator context) 参数,并返回一个布尔值,代表了输入是否通过了验证。

提示验证器上下文包含了这些属性:

属性 描述
上下文(Context) 机器人当前的轮次上下文
识别(Recognized) 一个带有被识别器处理过的用户输入信息的 提示识别器结果(prompt recognizer result)
选项(Options) 包含了在调用中提供的提示选项,以启动提示

而提示识别器结果有这些属性:

属性 描述
成功(Succeeded) 表示识别器是否能够解析输入的内容
值(Value) 识别器的返回值。如果必要,验证码可以修改此值

使用对话框

位于堆栈最顶层的被视为活动中的对话框,对话框上下文会将所有的输入引向这个活动中的对话框。

  1. 对话框开始
  2. 对话框被推入堆栈,成为活动中的对话框
  3. 对话框结束(被 replace dialog 方法移除)或者另一个对话框被推入堆栈并成为活动中的对话框
-----------------------------
按照微软官方文档,我的 Python 版本为 3.8.3。

配置机器人

在 Python 中,机器人的配置文件为 config.py

配置格式:XXX = os.environ.get(标识属性, 标识值)

python
1
2
3
4
5
import os
class DefaultConfig:
PORT = 3978
APP_ID = os.environ.get('MicrosoftAppId', '')
APP_PASSWORD = os.environ.get('MicrosoftAppPassword', '')
标识属性 标识值
MicrosoftAppType MultiTenant(多租户)
MicrosoftAppId 机器人的应用 ID
MicrosoftAppPassword 机器人的应用密码
MicrosoftAppTenantId 多租户可无视