关于流的更多信息
在 定义流 部分,我们学习了流的核心机制。在本节中,我们将探讨与流相关的更高级主题。
激活一个流
我们已经了解了使用 start
和 await
关键字来触发流。现在,我们将介绍第三个关键字 activate
,它也可以启动一个流。activate
与 start
的区别在于流在完成或失败后的行为。如果一个流被激活,它总会在结束后自动重启一个新的流实例。此外,特定的流配置(具有相同的流参数)只能被激活一次,即使多次激活也不会启动新的实例。
重要提示
流激活语句语法定义
activate <Flow> [and <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"
运行此示例,你会看到只要你一直用 “Hi” 打招呼,机器人就会回复 “Hello again”
Welcome
> Hi
Hello again
> Hi
Hello again
> Bye
Goodbye
> Hi
Hello again
> Bye
>
相比之下,在重新开始故事之前,你只能说一次 “Bye” 。
激活一个流使你能够持续将交互事件序列与流中定义的模式进行匹配,即使该模式之前已成功匹配交互事件序列(已完成)或失败。由于相同的流配置只能激活一次,因此你可以在任何需要流功能的地方直接使用流激活。这种按需模式比在开始时激活一次(在你真正知道是否需要它之前)更好。
重要提示
激活一个流将启动一个流,并在它结束(完成或失败)后自动重启,以匹配重复出现的交互模式。
重要提示
主流的行为也像激活的流。一旦它到达末尾,它将自动重启。
但是,这条规则有一个例外!如果一个流不包含任何等待事件并立即结束的语句,那么它在激活后只会运行一次,并且会保持激活状态,否则你会得到一个无限循环。
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
请注意,即使主流程到达末尾,它也不需要在末尾有任何匹配语句,并且将继续被激活而不会重复运行。
重要提示
立即完成(不等待任何事件)的激活流只会运行一次,并保持激活状态。
启动一个新的流实例
在某些情况下,仅在流完成后重启一次是不够的,因为这可能会错过某些模式重复
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” 没有触发任何操作,因为流已经前进到下一个语句 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
标签视为位于每个激活流的末尾。在不同的位置定义它会将其从默认位置(末尾)向上移动。
停用一个流
激活的流通常会保持活动状态,因为它总是在完成或失败时重启。要停用激活的流,你可以使用 deactivate 关键字
重要提示
流停用语句语法定义
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 关键字将中止流并禁用重启。它是此语句的快捷方式
send StopFlow(flow_id="flow name", deactivate=True)
覆盖流
可以使用 override 装饰器,用另一个同名的流来覆盖一个流
flow bot greet
bot say "Hi"
@override
flow bot greet
bot say "Hello"
在本示例中,第二个 ‘bot greet’ 流将覆盖第一个。当使用从库导入的 Colang 模块来覆盖时,这尤其有用,例如,覆盖标准库核心模块中的 ‘bot say’ 流以包含额外的日志语句
import core
flow main
bot say "Hi"
@override
flow bot say $text
log "bot say {$text}"
await UtteranceBotAction(script=$text) as $action
目前,流的定义顺序没有区别,因此只能定义两个同名的流,其中一个必须具有 override 装饰器。
注意
如果两个流具有相同的名称,则必须使用 override 装饰器来优先处理其中一个。
交互循环
到目前为止,任何并发进行的流(导致不同的事件生成)都会产生冲突,需要解决。虽然这在许多情况下是有意义的,但有时人们希望允许不同的动作同时发生。特别是当这些动作在不同的模态上时。我们可以通过在流上使用装饰器样式语法来定义不同的交互循环来实现这一点
重要提示
交互循环语法定义
@loop([id=]"<loop_name>"[,[priority=]<integer_number>])
flow <name of flow> ...
提示:要为每个流调用生成新的循环名称,请使用循环名称 “NEW”
默认情况下,任何没有显式交互循环的流都会继承其父流的交互循环,并且优先级为 0。现在让我们看一个第二个交互循环的示例,以设计增强主交互而不是与之竞争的流
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"
该示例实现了两个机器人反应流,它们监听用户说 “Hi” 或 “Bye”。每当这两个事件之一发生时,机器人将分别显示相应的姿势 “微笑” 或 “皱眉”。请注意,这些流如何从父流 ‘handling bot gesture reaction’ 继承其交互循环 ID,这与主流不同。因此,机器人姿势动作永远不会与来自主交互流的机器人说话动作竞争,并且将并行触发
> 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
默认情况下,不同交互循环中的并行流按照其启动或激活的顺序前进。如果例如在一个流中设置全局变量并在另一个流中读取,这可能是一个重要的细节。如果顺序错误,则当另一个流读取时,全局变量尚未设置。为了强制执行处理顺序,使其独立于启动或激活顺序,你可以使用整数定义交互循环优先级。默认情况下,任何交互循环的优先级为 0。数字越大,优先级越高;数字越小(负数),处理优先级越低。
流冲突解决优先级
在 定义流 部分,我们已经了解了一些关于解决流之间动作冲突的机制。现在我们将更详细地研究这一点。
对于每个成功的匹配语句,都会计算一个匹配分数,该分数大于 \(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)
启动主流后,两个流 ‘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>
指定流优先级来做到这一点,其中值介于 \(0.0\) 和 \(1.0\) 之间。流中的每个匹配都将乘以当前的流优先级。由于此方法目前只能降低匹配分数,因此你不能使用它来提高匹配的优先级。有时可以采用的一种解决方法是通过使用正则表达式(如 regex(".*")
)添加缺失的参数来提高非完美匹配的匹配分数
# ...
flow user said something
match UtteranceUserActionFinished(final_transcript=regex(".*"))
# ...
在本示例中,动作之间的冲突解决将随机发生,因为所有匹配分数都相等。