更多关于 Flows#

定义 Flows部分,我们学习了 flows 的核心机制。在本节中,我们将了解更多与 flows 相关的进阶主题。

激活 Flow#

我们已经了解了使用 startawait 关键字来触发 flow。现在,我们将介绍第三个关键字 activate,它也可以启动 flow。activatestart 的区别在于 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
more_on_flows/activate_flow/main.co#
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”打招呼,Bot 就会回复“Hello again”。

Welcome

> Hi

Hello again

> Hi

Hello again

> Bye

Goodbye

> Hi

Hello again

> Bye
>

相反,您只能说一次“Bye”,然后才能重启故事。

激活 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 不包含任何等待事件的语句并且立即完成,它将仅在激活时运行一次,并且将保持激活状态,否则您将获得无限循环。

more_on_flows/non-repeating-flows/main.co#
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 是不够的,因为这可能会错过某些模式重复

more_on_flows/restart_flow_instance/main.co#
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 的标签来实现

more_on_flows/start_new_flow_instance/main.co#
# ...

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 将覆盖第一个 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 装饰器优先处理其中一个。

交互循环#

到目前为止,任何并发进行的 flow,如果导致不同的事件生成,都会产生冲突,需要解决。虽然这在许多情况下都是有意义的,但有时人们希望允许多个不同的动作同时发生。特别是当这些动作在不同的模态上时。我们可以通过使用 flow 上的装饰器样式语法定义不同的交互循环来实现这一点

重要提示

交互循环语法定义

@loop([id=]"<loop_name>"[,[priority=]<integer_number>])
flow <name of flow> ...

提示:要为每个 flow 调用生成新的循环名称,请使用循环名称“NEW”

默认情况下,任何没有显式交互循环的 flow 都继承其父 flow 的交互循环,并且优先级为 0。现在让我们看一个第二个交互循环的示例,以设计增强主交互而不是与之竞争的 flow

more_on_flows/interaction_loops/main.co#
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"

该示例实现了两个 Bot 反应 flow,用于监听用户说“Hi”或“Bye”。每当发生这两个事件之一时,Bot 将分别显示相应的姿势“微笑”或“皱眉”。请注意,这些 flow 如何从父 flow 'handling bot gesture reaction' 继承其交互循环 ID,该 ID 与主 flow 不同。因此,Bot 姿势动作永远不会与主交互 flow 中的 Bot say 动作竞争,并且将并行触发

> 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

默认情况下,不同交互循环中的并行 flow 按其启动或激活的顺序前进。如果例如在一个 flow 中设置了一个全局变量,而在另一个 flow 中读取该变量,这可能是一个重要的细节。如果顺序错误,则全局变量在另一个 flow 读取时尚未设置。为了强制执行处理顺序,使其独立于启动或激活顺序,您可以使用整数定义交互循环优先级。默认情况下,任何交互循环的优先级均为 0。数字越大表示优先级越高,数字越小(负数)表示处理优先级越低。

Flow 冲突解决优先级#

定义 Flows部分,我们已经了解了一些关于解决 flow 之间动作冲突的机制。现在我们将更详细地了解这一点。

对于每个成功的匹配语句,都会计算出一个匹配分数,该分数大于 \(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(".*"))

# ...

在此示例中,动作之间的冲突解决将随机发生,因为所有匹配分数都相等。