更多关于 Flows#

定义 Flows 部分中,我们学习了 flows 的核心机制。在本节中,我们将研究与 flows 相关的更高级的主题。

激活 Flow#

我们已经看到了使用 startawait 关键字来触发 flow。现在,我们介绍第三个关键字 activate,它也可以启动 flow。与 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
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”问候,机器人就会回复“Hello again”。

Welcome

> Hi

Hello again

> Hi

Hello again

> Bye

Goodbye

> Hi

Hello again

> Bye
>

相反,您只能在重新启动 story 之前说一次“Bye”。

激活 flow 使您能够持续将交互事件序列与 flow 中定义的模式进行匹配,即使该模式之前已成功匹配交互事件序列(已完成)或失败。 由于相同的 flow 配置只能激活一次,因此您可以在需要 flow 功能的任何地方直接使用 flow 激活。 这种 按需模式 比在您实际知道是否需要它之前,一开始就激活它一次要好。

重要

激活 flow 将启动 flow,并在 flow 结束(完成或失败)时自动重新启动,以匹配重新出现的交互模式。

或者,您可以使用 @active 装饰器表示法在开始时激活 flow,作为 main 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 库模块中定义的 flows 使用 @active 装饰器,则它们会在导入库时自动激活。 但是,我们建议您尽可能使用 activate 语句,因为它更明确,并且可以提高可读性。

重要

main 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

请注意,main 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#

可以通过使用 override 装饰器,使用具有相同名称的另一个 flow 来覆盖 flow

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

目前,flows 的定义顺序没有区别,因此只能定义两个具有相同名称的 flows,其中一个必须具有 override 装饰器。

注意

如果两个 flows 具有相同的名称,则必须通过 override 装饰器优先考虑一个。

交互循环#

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

重要

交互循环语法定义

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

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

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

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"

这个例子实现了两个机器人反应流程,分别监听用户说“Hi”或“Bye”。每当这两个事件之一发生时,机器人将分别显示相应的姿势“微笑”或“皱眉”。请注意,这些流程如何从父流程‘处理机器人姿势反应’继承其交互循环 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)

在启动主流程后,两个流程‘模式 a’‘模式 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(".*"))

# ...

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