Bot Framework SDK学习日志
仅作个人用途,微软Azure的机器人框架SDK Python分支的学习日志。
目录
机器人初识
机器人交互
机器人交互涉及到活动的交换,而这些活动在轮次中进行处理。
-
活动(activities):活动是用户或者 通道(channel) 与机器人之间的交互。
-
轮次(turns):一轮次的对话包含了用户传给机器人的活动,也包含了机器人发给用户的即时响应(也是活动)。
类似于回合制战斗,速度快的我方先行采取一个行动后,速度慢的对面再采取一个行动,双方行动过后该轮次(或称回合)结束。
机器人应用程序结构
- *机器人(bot)*类,用于处理机器人应用的聊天推理
- 识别&解释用户的输入
- 对输入进行推理&执行相关任务
- 生成响应(如:机器人正在干什么)
- *适配器(adapter)*类,用于处理与通道的连接
- 提供用于处理来自用户通道的请求的方法
- 提供用于对用户通道生成请求的方法
- 包含一个中间件通道,包括机器人轮次处理程序外部的轮次处理
- 调用机器人的轮次处理程序
- 捕获不在轮次处理程序中处理的错误
机器人每个轮次还需要检索和存储 状态(state)。状态通过 存储(storage)、机器人状态(bot state) 和 属性访问器(property accessor) 类进行处理。
机器人逻辑
- 活动处理程序(activity handler),提供事件驱动模型,其中传入的活动类型&子类型是 事件(event)。
- 对话库(dialog library),提供基于状态的模型用于管理与用户进行的长时间聊天。
机器人适配器
适配器提供用于启动轮次的 过程活动(process activity) 方法。
- 将请求正文和请求头用于参数
- 检查身份验证头是否有效
- 为轮次创建一个 上下文(context) 对象
- 上下文对象包含有关活动的信息
- 通过中间件管道发送上下文对象
- 将上下文对象发送到机器人对象的 轮次处理程序(turn handler)
适配器还可以:
- 格式化&发送响应活动
- 公开机器人连接器(Bot Connector) REST API提供的其他方法
- 捕获在轮次中不会被捕获到的错误&异常
轮次上下文
轮次上下文(turn context) 对象提供有关活动的信息。
- 例如发送方和接收方、通道、处理该活动所需的其他数据
轮次上下文不仅将 入站活动(inbound activity) 传递到所有的中间件组件和应用程序逻辑,还提供了所需要的机制让中间件组件和机器人逻辑发送 出站活动(outbound activity)。
中间件
SDK的中间件由一组线性组件构成,其中每一个组件都会按照顺序执行并有一个操作活动的机会。
中间件管道的最后一个阶段:回调机器人类中的轮次处理程序(已经被适配器的过程活动方法注册)。中间件执行被适配器调用的on turn
方法。
轮次处理程序采用轮次上下文作为参数。在轮次处理程序函数内运行的应用程序逻辑会处理入站活动的内容,并生成活动作为响应,在轮次上下文中调用send activity
方法来发送出站活动。调用send activity
方法会导致中间件组件在出站活动上被调用。
中间件组件于轮次处理程序函数之前和之后执行。这些执行在本质上是套娃。
活动处理堆栈
- 通道终结点向Azure机器人服务发送HTTP POST信息
- Azure机器人服务处理活动,发送给适配器和轮次上下文
- 适配器和轮次上下文调用
on turn
方法,发送给机器人 - 机器人调用
send activity
方法,一个个返回给通道终结点 - 通道终结点发送回状态码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),用于实际读取&写入某个状态属性,提供了
get
、set
和delete
方法用于从轮次内部访问状态属性。访问器创建需要用到属性名称。之后便可以使用访问器来获取和处理机器人状态的该属性。
访问器允许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:处理来自第一个提示的结果,并开始第二次提示
- 瀑布#3:处理来自第二个提示的结果,结束对话(堆栈入口消失)
瀑布式对话框的上下文被存储在瀑布式步骤上下文中。这个步骤上下文与对话上下文类似,提供对当前轮次上下文和状态的访问。使用瀑布式步骤上下文对象来与瀑布式步骤中的对话框集进行交互。
对话框的返回值可以在瀑布式步骤中处理,也可以从机器人的轮次处理程序中处理(一般只需要在机器人的轮次论及中检查对话框轮次结果的状态)。
瀑布步骤上下文包含以下属性:
Options
:包含对话框的输入信息Values
:包含可以添加到上下文中的信息,并被带入后续步骤中Result
:包含前一个步骤的结果
Python的
next
方法可以在同一轮次内继续进行瀑布式对话框的下一步,也就是在需要时跳过某个步骤。
提示(Prompt) 提供了一种简单的方法来询问用户的信息并评估他们的反应。
-
提示本质上就是一个两步的对话框。首先提示会要求输入,然后返回有效值,或者从头开始重新提示。
-
调用提示时,可以在 提示选项(prompt options) 中指定要提示的文本、如果验证失败了的重新提示,以及回答提示的选择。
- 一般而言,提示和重新提示属性都属于活动。
-
当提示被创建时,也可以选择为提示添加自定义验证(如:小于18岁就不行的年龄验证)。提示先行检查它是否接收到了一个有效的数字,然后运行自定义验证。假如验证失败了,就重新提示。
-
当一个提示完成时,就会明确地返回所要求的结果值。
提示 | 说明 | 返回值 |
---|---|---|
附件提示(Attachment prompt) | 要求一个或多个附件,例如文档或者图片。 | 一个 附件(attachment) 对象的集合 |
选项提示(Choice prompt) | 从一系列的选项中要求选择一个。 | 一个找到的选项对象 |
确认提示(Confirm prompt) | 请求提供 Yes 或 No | 一个布尔值 |
日期时间提示(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) | 识别器的返回值。如果必要,验证码可以修改此值 |
使用对话框
位于堆栈最顶层的被视为活动中的对话框,对话框上下文会将所有的输入引向这个活动中的对话框。
- 对话框开始
- 对话框被推入堆栈,成为活动中的对话框
- 对话框结束(被
replace dialog
方法移除)或者另一个对话框被推入堆栈并成为活动中的对话框
配置机器人
在 Python 中,机器人的配置文件为 config.py
。
配置格式:XXX = os.environ.get(标识属性, 标识值)
。
1 | import os |
标识属性 | 标识值 |
---|---|
MicrosoftAppType |
MultiTenant (多租户) |
MicrosoftAppId |
机器人的应用ID |
MicrosoftAppPassword |
机器人的应用密码 |
MicrosoftAppTenantId |
多租户可无视 |