更多关于 Flows#
在 定义 Flows 章节中,我们学习了 flows 的核心机制。在本节中,我们将着眼于与 flows 相关的更高级主题。
激活 Flow#
我们已经见过使用 start
和 await
关键字来触发 flow。现在我们介绍第三个关键字 activate
,它可以启动一个 flow。activate
与 start
的区别在于 flow 完成或失败时的行为。如果一个 flow 被激活,它总会在结束后自动重启一个新的 flow 实例。此外,特定的 flow 配置(具有相同的 flow 参数)只能激活一次,即使多次激活也不会启动新的实例。
重要提示
Flow 激活语句语法定义
activate <Flow> [and <Flow>]…
不支持激活的 flow 的引用赋值,因为实例会在重启后更改
仅支持 and-groups,不支持 or-groups
示例
# Activate a single flow
activate handling user presents
# Activate two different instances of the same flow with parameters
activate handling user said "Hi"
activate handling user said "Bye"
# Activate a group of flows
activate handling user presents and handling question repetition 5.0
import core
flow main
activate managing user greeting
bot say "Welcome"
user said "Bye"
bot say "Goodbye"
match RestartEvent()
flow managing user greeting
user said "Hi"
bot say "Hello again"
通过运行此示例,您将看到机器人回复“Hello again”,只要您持续用“Hi”问候
Welcome
> Hi
Hello again
> Hi
Hello again
> Bye
Goodbye
> Hi
Hello again
> Bye
>
相比之下,您只能说一次“Bye”,之后需要重启故事。
激活 flow 使您能够持续将交互事件序列与 flow 中定义的模式进行匹配,即使该模式先前已成功匹配交互事件序列(已完成)或失败。由于相同的 flow 配置只能激活一次,您可以直接在需要 flow 功能的任何地方使用 flow 激活。这种按需模式比在实际知道是否需要之前在开始时激活一次要好。
重要提示
激活 flow 将启动一个 flow,并在 flow 结束(完成或失败)时自动重启它,以匹配重复出现的交互模式。
或者,您可以使用 @active
装饰器符号在启动时将 flow 作为主 flow 的子 flow 激活
import core
flow main
bot say "Welcome"
user said "Bye"
bot say "Goodbye"
match RestartEvent()
@active
flow managing user greeting
user said "Hi"
bot say "Hello again"
如果您对在单独的 Colang 库模块中定义的 flow 使用 @active
装饰器,它们将在库导入时自动激活。但我们建议您尽可能使用 activate
语句,因为它更明确,并且可以提高可读性。
重要提示
主 flow 的行为也类似于激活的 flow。一旦它到达末尾,它将自动重启。
但有一个例外!如果一个 flow 不包含任何等待事件并立即完成的语句,它将只在激活时运行一次,并且会保持激活状态,否则您将得到一个无限循环。
import core
flow main
activate managing user greeting
# No additional match statement need to keep this flow activated without repeating
flow managing user greeting
user said "Hi"
bot say "Hello again"
> Hi
Hello again
> Hi
Hello again
请注意,主 flow 在末尾不需要任何匹配语句,并且将继续保持激活状态而不会重复,即使它已到达末尾。
重要提示
立即完成(不等待任何事件)的激活 flow 将只运行一次,并保持激活状态。
启动新的 Flow 实例#
在某些情况下,仅在 flow 完成后重启 flow 是不够的,因为这可能会错过某些模式重复
import core
flow main
activate managing user presence
bot say "Welcome"
match RestartEvent()
flow managing user presence
user said "Hi"
bot say "Hello again"
user said "Bye"
bot say "Goodbye"
在下面的交互中,我们看到用户的第二个“Hi”没有触发任何操作,因为 flow 已经前进到下一个语句 user said "Bye"
,但由于其激活,尚未启动新实例
Welcome
> Hi
Hello again
> Hi
> Bye
Goodbye
> Hi
Hello again
>
如果我们想在当前实例结束之前启动新实例,我们可以通过在交互序列中的相应位置添加一个名为 start_new_flow_instance
的标签来实现
# ...
flow managing user presence
user said "Hi"
start_new_flow_instance: # Start a new instance of the flow and continue with this one
bot say "Hello again"
user said "Bye"
bot say "Goodbye"
我们现在看到正确的行为
Welcome
> Hi
Hello again
> Hi
Hello again
> Bye
Goodbye
> Bye
>
请注意,一旦第二个实例前进到下一个匹配语句,就会启动第三个实例,等待下一个用户输入“Hi”。其他两个实例将并行前进。由于第一个实例已经启动了一个新实例(第二个),它将不再启动另一个实例,这样我们就不会在进行时获得越来越多的实例。请注意,第二个“Bye”不会触发任何操作,因为第一个和第二个实例已经完成,而第三个实例仍处于第一个语句,等待“Hi”。
注意
您可以将 start_new_flow_instance
标签视为位于每个激活 flow 的末尾。在不同位置定义它会将它从默认位置(末尾)向上移动。
停用 Flow#
激活的 flow 通常会保持活动状态,因为它总是在完成或失败时重启。要停用激活的 flow,您可以使用 deactivate 关键字
重要提示
Flow 停用语句语法定义
deactivate <Flow>
示例
# Deactivate a single flow
deactivate handling user presents
# Deactivate two different instances of the same flow with different parameters
deactivate handling user said "Hi"
deactivate handling user said "Bye"
在底层,deactivate 关键字将中止 flow 并禁用重启。它是以下语句的快捷方式
send StopFlow(flow_id="flow name", deactivate=True)
覆盖 Flows#
Flow 可以被另一个同名的 flow 通过使用 override 装饰器来覆盖
flow bot greet
bot say "Hi"
@override
flow bot greet
bot say "Hello"
在此示例中,第二个 ‘bot greet’ flow 将覆盖第一个。当使用从库导入的 Colang 模块来覆盖时,这尤其有用,例如,覆盖标准库核心模块中的 ‘bot say’ flow 以包含额外的日志语句
import core
flow main
bot say "Hi"
@override
flow bot say $text
log "bot say {$text}"
await UtteranceBotAction(script=$text) as $action
目前,flow 的定义顺序没有区别,因此只能定义两个同名的 flow,其中一个必须具有 override 装饰器。
注意
如果两个 flow 具有相同的名称,则必须通过 override 装饰器来确定优先级。
交互循环#
到目前为止,任何并发进行的 flows 导致不同事件生成都会产生冲突,需要解决。虽然这在许多情况下是有意义的,但有时人们希望允许多个不同的操作同时发生。特别是当这些操作在不同的模态上时。我们可以通过使用 flow 上的装饰器样式语法定义不同的交互循环来实现这一点
重要提示
交互循环语法定义
@loop([id=]"<loop_name>"[,[priority=]<integer_number>])
flow <name of flow> ...
提示:要为每个 flow 调用生成新的循环名称,请使用循环名称“NEW”
默认情况下,任何没有显式交互循环的 flow 都继承其父 flow 的交互循环,并且优先级为 0。现在让我们看一个第二个交互循环的示例,以设计增强主交互而不是与之竞争的 flows
import core
import avatars
flow main
activate handling bot gesture reaction
while True # Keep reacting to user inputs
when user said "Hi"
bot say "Hi"
or when user said something
bot say "Thanks for sharing"
or when user said "Bye"
bot say "Goodbye"
@loop("bot gesture reaction")
flow handling bot gesture reaction # Just a grouping flow for different bot reactions
activate reaction of bot to user greeting
activate reaction of bot to user leaving
flow reaction of bot to user greeting
user said "Hi"
bot gesture "smile"
flow reaction of bot to user leaving
user said "Bye"
bot gesture "frown"
该示例实现了两个机器人反应 flow,它们监听用户说“Hi”或“Bye”。每当两个事件之一发生时,机器人将分别显示相应的姿势“smile”或“frown”。请注意,这些 flow 如何从父 flow ‘handling bot gesture reaction’ 继承它们的交互循环 ID,这与主 flow 不同。因此,机器人姿势操作永远不会与来自主交互 flow 的机器人说话操作竞争,并且将并行触发
> Hi
Gesture: smile
Hi
> I am feeling great today
Thanks for sharing
> I am looking forward to my birthday
Thanks for sharing
> Bye
Gesture: frown
Goodbye
默认情况下,不同交互循环中的并行 flows 按照其启动或激活顺序前进。如果例如在一个 flow 中设置了全局变量并在另一个 flow 中读取,这可能是一个重要的细节。如果顺序错误,则全局变量在另一个 flow 读取时尚未设置。为了强制执行处理顺序,使其独立于启动或激活顺序,您可以使用整数定义交互循环优先级。默认情况下,任何交互循环的优先级都为 0。数字越高表示优先级越高,而较低(负数)数字表示处理优先级较低。
Flow 冲突解决优先级#
在 定义 Flows 章节中,我们已经了解了一些关于解决 flows 之间操作冲突的机制。现在我们将更详细地了解这一点。
对于每个成功匹配的语句,都会计算一个匹配分数,该分数大于 \(0.0\)(不匹配)且小于或等于 \(1.0\)(完美匹配)。当预期事件的所有参数与实际事件的所有参数匹配时,即为完美匹配。如果实际事件的参数多于预期事件,则匹配分数将降低,方法是将匹配分数乘以 \(0.9\) 的因子,对于每个缺失的参数。因此,假设我们有一个包含五个参数的匹配事件,但我们只指定了其中两个,则分数将为 \(0.9^{5-2} = 0.729\)。由于系统事件可以触发一系列内部事件,我们需要考虑该序列中生成的所有匹配分数。让我们使用以下示例来更好地说明这一点
flow main
activate pattern a and pattern b
flow pattern a
user said "Hi"
bot say "Hello"
flow pattern b
user said something
bot say "Sure"
flow user said $text
match UtteranceUserActionFinished(final_transcript=$text)
flow user said something
match UtteranceUserActionFinished()
flow bot say $text
await UtteranceBotAction(script=$text)
启动主 flow 后,两个 flow ‘pattern a’ 和 ‘pattern b’ 将处于活动状态并等待用户说些什么。让我们看看由事件 UtteranceUserActionFinished(final_transcript="Hi")
触发的两个事件生成链
1) UtteranceUserActionFinished(final_transcript="Hi") -> send FlowFinished(flow_id="user said", text="Hi") -> send StartFlow(flow_id="bot say", text="Hello") -> send StartUtteranceBotAction(text="Hello")
2) UtteranceUserActionFinished(final_transcript="Hi") -> send FlowFinished(flow_id="user said something") -> send StartFlow(flow_id="bot say", text="Sure") -> send StartUtteranceBotAction(text="Sure")
由于这些链末尾的结果操作事件不同,因此会存在冲突,需要解决。让我们看看这些链中相应的匹配语句
1) match UtteranceUserActionFinished(final_transcript="Hi") -> match FlowFinished(flow_id="user said", text="Hi") -> match StartFlow(flow_id="bot say", text="Hello")
2) match UtteranceUserActionFinished() -> match FlowFinished(flow_id="user said something") -> match StartFlow(flow_id="bot say", text="Sure")
将这些匹配语句与事件进行比较将产生以下匹配分数
1) 1.0 -> 1.0 -> 1.0
2) 0.9 -> 1.0 -> 1.0
为了找到最佳事件匹配序列,我们将从左到右比较来自不同链的每个匹配分数,并在一个分数高于另一个分数时立即确定获胜者。您会看到第二个链中的第一个匹配不是完美的,导致值为 \(0.9\)。因此,第一个链是获胜者,第二个链将失败,从而产生以下输出
> Hi
Hello
在某些情况下,您可能希望影响某些匹配的匹配分数,以更改冲突解决结果。您可以通过使用语句 priority <float_value>
指定 flow 优先级来执行此操作,其中值介于 \(0.0\) 和 \(1.0\) 之间。flow 中的每个匹配都将乘以当前的 flow 优先级。由于此方法目前只能降低匹配分数,因此您不能使用它来提高匹配的优先级。有时可以采用的一种解决方法是通过添加缺失的参数来提高非完美匹配的匹配分数,使用正则表达式匹配任何值,例如 regex(".*")
# ...
flow user said something
match UtteranceUserActionFinished(final_transcript=regex(".*"))
# ...
在此示例中,操作之间的冲突解决将随机发生,因为所有匹配分数都相等。