仅作个人用途,微软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来当复读机
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(标识属性, 标识值)

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 多租户可无视