定义流程#

介绍#

到目前为止,您只看到一个流程,即主流程。但在 Colang 中,我们可以定义许多不同的流程,就像其他编程语言中的函数一样。流程定义了由一系列语句组成的特定交互模式。流程的名称由小写字母、数字、下划线和空格字符组成。此外,流程定义可以包括带有可选默认值的输入和输出参数(或简称:输入和输出参数)。

重要提示

流程语法定义

flow <name of flow>[ $<in_param_name>[=<default_value>]...] [-> <out_param_name>[=<default_value>][, <out_param_name>[=<default_value>]]...]
    ["""<flow summary>"""]
    <interaction pattern>

示例

flow bot say $text $intensity=1.0
    """Bot says given text."""
    # ...

flow user said $text
    """User said given text."""
    # ...

flow user said something -> $transcript
    """User said something."""
    # ...

在流程名称中允许空格字符的选择带来了一些限制

  • 关键字 andoras 不能在流程名称中使用,需要用前导下划线字符进行转义(例如,this _and that)。但通常,与其使用例如“and”这个词,不如使用“then”这个词来组合动作,例如 bot greet then smile 来描述顺序依赖关系。或者,如果它是并发发生的,则可以将其写成 bot greet smiling

  • 使用变量和表达式 章节所示,变量始终以 $ 字符开头。

与动作一样,可以使用关键字 startawaitmatch 启动流程并等待其完成

flows/call_a_flow/main.co#
flow main
    # Start and wait for a flow in two steps using a flow reference
    start bot express greeting as $flow_ref
    match $flow_ref.Finished()

    # Start and wait for a flow to finish
    await bot express greeting

    # Or without the optional await keyword
    bot express greeting

    match RestartEvent()

flow bot express greeting
    await UtteranceBotAction(script="Hi")

请注意,启动流程将立即处理并触发流程的所有初始语句,直到第一个等待事件的语句

flows/start_flow/main.co#
flow main
    start bot handle user welcoming
    match RestartEvent() # <- This statement is only processed once the previous flow has started

flow bot handle user welcoming
    start UtteranceBotAction(script="Hi")
    start GestureBotAction(gesture="Wave") as $action_ref
    match $action_ref.Finished() # <- At this point the flow is considered to have started
    match UtteranceUserAction().Finished()
    start UtteranceBotAction(script="How are you?")

重要提示

启动流程将立即处理并触发流程的所有初始语句,直到第一个等待事件的语句。

流程事件#

与动作类似,流程本身可以生成与流程状态或生命周期相关的不同事件。这些流程事件优先于其他事件(请参阅 内部事件

FlowStarted(flow_id: str, flow_instance_uid: str, source_flow_instance_uid: str) # When a flow has started
FlowFinished(flow_id: str, flow_instance_uid: str, source_flow_instance_uid: str) # When the interaction pattern of a flow has successfully finished
FlowFailed(flow_id: str, flow_instance_uid: str, source_flow_instance_uid: str) # When the interaction pattern of a flow has failed

也可以像流程的对象方法一样访问这些事件

Started(flow_id: str, flow_instance_uid: str, source_flow_instance_uid: str) # When a flow has started
Finished(flow_id: str, flow_instance_uid: str, source_flow_instance_uid: str) # When the interaction pattern of a flow has successfully finished
Failed(flow_id: str, flow_instance_uid: str, source_flow_instance_uid: str) # When the interaction pattern of a flow has failed

可以通过流程引用或流程名称本身来匹配这些事件

# Match to flow event with flow reference
match $flow_ref.Finished()

# Match to flow event based on flow name
match (bot express greeting).Finished()

主要区别在于,使用引用匹配流程事件将特定于实际引用的流程实例,而通过流程名称匹配将对该流程的任何流程实例都成功。

这是一个带有参数的流程示例

flows/flow_parameters/main.co#
flow main
    # Say 'Hi' with the default volume of 1.0
    bot say "Hi"

flow bot say $text $volume=1.0
    await UtteranceBotAction(script=$text, intensity=$volume)

请注意,我们如何使用更简单的名称来抽象和简化使用流程的动作处理。这使我们能够将大多数动作和事件包装到可以通过 Colang 标准库 (CSL) 轻松访问的流程中。另请参阅 内部事件 部分,其中更详细地解释了底层的流程事件机制。

流程和动作生命周期#

在一个流程中启动另一个流程将隐式创建流程的层次结构,其中“main”流程是所有这些流程的根流程。与动作一样,流程的生命周期受其父流程生命周期的限制。换句话说,一旦启动流程的流程完成或自身停止,流程就会停止

flow main
    match UserReadyEvent()
    bot express greeting

flow bot express greeting
    start bot say "Hi!" as $flow_ref
    start bot gesture "wave with one hand"
    match $flow_ref.Finished()

flow bot say $text
    await UtteranceBotAction(script=$text)

flow bot gesture $gesture
    await GestureBotAction(gesture=$gesture)

我们看到“main”流程启动并等待流程“bot express greeting”,后者启动了两个流程“bot say”和“bot gesture”。但是流程“bot express greeting”只会等待“bot say”完成,如果“bot gesture”仍然处于活动状态,则会自动停止它。现在,使用我们简单的聊天 CLI 有点难以模拟,因为 UtteranceBotActionGestureBotAction 都没有持续时间,并且会立即完成。在交互式系统中,机器人实际说话并使用例如动画来执行手势动作,这将需要一些时间才能完成。但是我们也可以通过使用 TimerBotAction 来模拟这种效果,它只会引入指定的延迟

flows/flow_hierarchy/main.co#
flow main
    match UserReadyEvent()
    bot express greeting

flow bot express greeting
    start bot say "Hi!" as $flow_ref
    start bot gesture "wave with one hand"
    match $flow_ref.Finished()

flow bot say $text
    await TimerBotAction(timer_name="utterance_timer", duration=2.0)
    await UtteranceBotAction(script=$text)

flow bot gesture $gesture
    await TimerBotAction(timer_name="gesture_timer", duration=5.0)
    await GestureBotAction(gesture=$gesture)

现在运行此代码会显示所需的行为

> /UserReadyEvent

Hi

如果您愿意,您也可以更改手势计时器的持续时间,使其小于话语计时器,以查看手势是否可以成功完成

/UserReadyEvent

Gesture: wave with on hand

Hi!

流程的结束(完成或失败)也将停止所有剩余的活动动作。与流程一样,在流程中启动的动作的生命周期受父流程生命周期的限制。这有助于限制意外的副作用,并使交互设计更加健壮。

重要提示

任何启动的流程或动作的生命周期都受父流程生命周期的限制。

并发模式匹配#

流程不仅仅是其他编程语言中已知的函数。流程是可以并发匹配和进展的交互模式

flows/concurrent_flows_basics/main.co#
flow main
    start pattern a as $flow_ref_a
    start pattern b as $flow_ref_b
    match $flow_ref_a.Finished() and $flow_ref_b.Finished()
    await UtteranceBotAction(script="End")
    match RestartEvent()

flow pattern a
    match UtteranceUserAction.Finished(final_transcript="Bye")
    await UtteranceBotAction(script="Goodbye") as $action_ref

flow pattern b
    match UtteranceUserAction.Finished(final_transcript="Hi")
    await UtteranceBotAction(script="Hello")
    match UtteranceUserAction.Finished(final_transcript="Bye")
    await UtteranceBotAction(script="Goodbye") as $action_ref
> Hi

Hello

> Bye

Goodbye

End

两个流程“pattern a”和“pattern b”从“main”立即启动,等待用户的第一个话语动作。在用户交互后,您会看到两个流程是如何完成的,因为它们都匹配了交互模式。请注意,最后一个机器人动作“Goodbye”在两个流程中是相同的,因此只会触发一次。因此,$action_ref 实际上将指向同一个动作对象。正如我们之前所见,如果父流程已完成,则动作将停止。对于在两个并发流程中共享的动作,这仍然成立,但只有当两个流程都完成时,它才会被强制停止。

我们可以使用包装器流程来抽象动作,并使相同的示例工作完全相同。请记住,我们不必编写 await 关键字,因为它是默认值

flows/concurrent_flows_basics_wrapper/main.co#
flow main
    start pattern a as $flow_ref_a
    start pattern b as $flow_ref_b
    match $flow_ref_a.Finished() and $flow_ref_b.Finished()
    bot say "End"
    match RestartEvent()

flow pattern a
    user said "Bye"
    bot say "Goodbye"

flow pattern b
    user said "Hi"
    bot say "Hello"
    user said "Bye"
    bot say "Goodbye"

flow user said $text
    match UtteranceUserAction.Finished(final_transcript=$text)

flow bot say $text
    await UtteranceBotAction(script=$text)

当流程 ‘a’ 使用不太具体的匹配语句时,此示例将以相同的方式工作

# ...

flow pattern a
    user said something
    bot say "Goodbye"

# ...

flow user said something
    match UtteranceUserAction.Finished()

现在,让我们看看如果两个匹配的流程在动作上不一致,从而导致最后两个语句不同,会发生什么

flows/action_conflict_resolution/main.co#
flow main
    start pattern a
    start pattern b
    match RestartEvent()

flow pattern a
    user said something
    bot say "Hi"
    user said "How are you?"
    bot say "Great!"

flow pattern b
    user said something
    bot say "Hi"
    user said something
    bot say "Bad!

# ...
> Hello

Hi

> How are you?

Great!

> /RestartEvent
> Welcome

Hi

> How are you doing?

Bad!

我们可以从中看到,只要两个流程达成一致,它们都将按照其语句进行。在第三个语句中也是如此,其中流程“pattern a”正在等待特定的用户话语,而“pattern b”正在等待任何用户话语。有趣的是最后一个语句,它为这两个流程中的每一个触发了不同的动作,从而导致生成两个不同的事件。默认情况下,两个不同事件的并发生成在 Colang 中会发生冲突,需要解决。只能生成一个,但是生成哪个呢?冲突事件生成的解决是基于当前模式匹配的特异性完成的。特异性计算为匹配分数,该分数取决于与相应事件中所有可用参数相比的匹配参数数量。如果我们对所有可用事件参数都进行了匹配,则匹配分数将最高。由于在第一次运行时,用户询问了“How are you?”,并且流程“pattern a”中的第三个事件匹配语句是更好的匹配,因此流程“pattern a”将成功触发其动作。另一方面,由于冲突解决,流程“pattern b”将失败。在第二次运行中,情况有所不同,只有“pattern b”会匹配,因此会取得进展。

重要提示

不同事件的并发生成会发生冲突,并将根据模式匹配的特异性(匹配分数)来解决。如果匹配分数完全相同,则将随机选择事件。

在解决事件生成冲突时,我们仅考虑导致事件生成的当前事件匹配语句,而忽略流程中较早的模式匹配。

已完成/失败的流程#

流程的交互模式只能以两种不同的方式结束。要么通过成功匹配并触发模式的所有事件 (Finished),要么更早失败 (Failed)。

在以下情况之一中,交互模式被视为已成功完成

  1. 模式的所有语句都已成功处理,并且流程已到达结尾。

  2. 到达 return 语句作为模式的一部分,表明流程定义的模式已成功匹配交互(请参阅 流程控制 部分)

  3. 流程定义的模式被认为已基于来自另一个流程的内部事件成功匹配(请参阅 内部事件 部分)。

注意

记住:流程的 Finished 事件在 await 语句中隐式匹配,该语句组合了流程的启动,然后等待其完成。

如果流程中的交互模式失败,则流程本身被视为失败,从而生成 Failed 事件。交互模式可能因以下原因之一而失败

  1. 模式中的动作触发语句(例如 UtteranceBotAction(script="Yes"))与另一个并发模式的动作触发语句(例如 UtteranceBotAction(script="No"))冲突,并且特异性低于另一个。

  2. 模式的当前匹配语句正在等待不可能的事件(例如,等待已失败的流程完成)。

  3. 到达 abort 语句作为模式的一部分,表明无法针对交互匹配(因此失败)该模式(请参阅 流程控制 部分)。

  4. 该模式由于另一个流程生成的内部事件而失败(请参阅 内部事件 部分)。

在流程层次结构上下文中,案例 B) 起着尤为重要的作用。让我们看一个示例以更好地理解这一点

flows/flows_failing/main.co#
flow main
    start pattern a as $ref
    start pattern c
    match $ref.Failed()
    bot say "Pattern a failed"
    match RestartEvent()

flow pattern a
    await pattern b

flow pattern b
    user said something
    bot say "Hi"

flow pattern c
    user said "Hello"
    bot say "Hello"

用户输入“Hello”将导致流程 ‘pattern a’ 失败

> Hello

Hello

Pattern a failed

失败的原因在于流程失败的方式

  1. 用户话语事件“Hello”同时匹配并推进 ‘pattern c’‘pattern b’

  2. 流程模式 ‘pattern c’‘pattern b’ 由于其不同的动作而冲突,并且 ‘pattern b’ 由于特异性较低而失败

  3. ‘pattern b’ 的失败使得流程 ‘pattern a’ 永远无法完成,因为它正在等待流程 ‘pattern b’ 成功完成,因此 ‘pattern a’ 也失败了(请参阅案例 B)

失败的流程并不总是需要导致父流程也失败,可以通过使用关键字 start 异步启动流程,或者使用 when/or when 流程控制结构(请参阅 流程控制 部分)

这些是由于不可能的事件而导致模式可能失败的所有情况

  • 事件匹配语句等待特定流程的 FlowFinished 事件,但流程失败。

  • 事件匹配语句等待特定流程的 FlowFailed 事件,但流程成功完成。

  • 事件匹配语句等待特定流程的 FlowStarted 事件,但流程完成或失败。

流程分组#

与动作一样,我们可以在使用分组运算符 andor 构建的流程组上使用 startawait。让我们根据以下四种情况,使用两个占位符流程 ‘a’‘b’,仔细看看这是如何工作的

# A) Starts both flows sequentially without waiting for them to finish
start a and b
# Equivalent representation:
start a
start b

# B) Starts both flows concurrently without waiting for them to finish
start a or b
# No other representation

# C) Starts both flows sequentially and waits for both flows to finish
await a and b
# Equivalent representation:
start a as $ref_a and b as $ref_b
match $ref_a.Finished() and $ref_b.Finished()

# D) Starts both flows concurrently and waits for the first (earlier) to finish
await a or b
# Equivalent representation:
start a as $ref_a or b as $ref_b
match $ref_a.Finished() or $ref_b.Finished()

案例 A 和 C 不需要过多解释,并且应该很容易理解。但是,案例 B 和 D 使用了我们在之前的模式匹配部分中已经看到的并发概念。如果两个流程同时启动,它们将一起进展,并可能导致冲突的动作。此类冲突的解决方式完全相同。让我们通过两个具体的流程示例来看一下

flow main
    # A) Starts both bot actions sequentially without waiting for them to finish
    start bot say "Hi" and bot gesture "Wave with one hand"

    # B) Starts only one of the bot actions at random since they conflict in the two concurrently started flows
    start bot say "Hi" or bot gesture "Wave with one hand"

    # C) Starts both bot actions sequentially and waits for both of them to finish
    await bot say "Hi" and bot gesture "Wave with one hand"

    # D) Starts only one of the bot actions at random and waits for it to finish
    await bot say "Hi" or bot gesture "Wave with one hand"

flow bot say $text
    await UtteranceBotAction(script=$text)

flow bot gesture $gesture
    await GestureBotAction(gesture=$gesture)
flow main
    # A) Starts both flows sequentially that will both wait for their user action event match
    start user said "Hi" and user gestured "Waving with one hand"

    # B) Starts both flows concurrently that will both wait for their user action event match
    start user said "Hi" or user gestured "Waving with one hand"

    # C) Wait for both user action events (order does not matter)
    await user said "Hi" and user gestured "Waving with one hand"

    # D) Waits for one of the user action events only
    await user said "Hi" or user gestured "Waving with one hand"

flow user said $text
    match UtteranceUserAction.Finished(final_transcript=$text)

flow user gestured $gesture
    match GestureUserAction.Finished(gesture=$gesture)

请注意

  • 第一个示例的案例 B 也解释了带有事件生成或组的底层机制(请参阅 事件生成 - 事件分组 部分)。随机选择是事件冲突解决的结果,而不是特殊情况。

  • 第二个示例中的案例 B 与用户动作具有与案例 A 相同的效果。从语义的角度来看,这可能有点出乎意料,但与底层机制是一致的。

混合流程、动作和事件分组#

到目前为止,我们已经在分离的上下文中查看了事件、动作和流程分组。但实际上,它们都可以根据语句关键字在组中混合。

  • match:仅接受事件组

  • start:接受动作和流程组,但不接受事件

  • await:接受动作和流程组,但不接受事件

# Wait for either a flow or action to finish
match (bot say "Hi").Finished() or UtteranceUserAction.Finished(final_transcript="Hello")

# Combining the start of a flow and an action
start bot say "Hi" and GestureBotAction(gesture="Wave with one hand")

# Same as before but with additional reference assignment
start bot say "Hi" as $bot_say_ref
    and GestureBotAction(gesture="Wave with one hand") as $gesture_action_ref

# Combining awaiting (start and wait for them to finish) two flows and a bot action
await bot say "Hi" or GestureBotAction(gesture="Wave with one hand") or user said "hi"

虽然这在如何设计交互模式方面提供了很大的灵活性,但将所有动作和事件包装到流程中,然后在主交互模式设计中使用它们被认为是“良好的设计”。

流程命名约定#

您现在可能已经发现了流程命名中刻意使用时态。虽然关于如何命名流程没有约束性规则,但我们建议遵循这些约定

  • 如果流程与表示机器人或用户动作/意图的系统事件/动作相关,则以诸如 botuser 之类的主语开头流程名称。

  • 使用动词的祈使形式来描述应执行的机器人动作,例如 bot say $text

  • 使用动词的过去式来描述已发生的动作,例如 user said somethingbot said something

  • 使用形式 <subject> started <verb continuous form> ... 来描述已开始的动作,例如 bot started saying somethinguser started saying something

  • 对于应激活并等待特定交互模式做出反应的流程,以名词或动名词形式的活动开头,例如 reaction to user greetinghandling user leavingtracking bot talking state

类动作和类意图流程#

我们已经看到了一些类用户和类机器人动作流程的示例

flow bot say $text
    await UtteranceBotAction(script=$text)

flow bot gesture $gesture
    await GestureBotAction(gesture=$gesture)

flow user said $text
    match UtteranceUserAction.Finished(final_transcript=$text)

flow user gestured $gesture
    match GestureUserAction.Finished(gesture=$gesture)

借助这些流程,我们可以构建另一个抽象,即表示机器人或用户意图的流程

# A bot intent flow
flow bot greet
    (bot say "Hi"
        or bot say "Hello"
        or bot say "Welcome")
        and bot gesture "Raise one hand in a greeting gesture"

# A user intent flow
flow user expressed confirmation
    user said "Yes"
        or user said "Ok"
        or user said "Sure"
        or user gestured "Thumbs up"

请注意,类机器人动作流程将随机地将三个话语之一与问候手势组合在一起,而只有在收到指定的用户话语或用户手势之一时,类用户动作流程才会完成。借助更多示例或正则表达式,可以使这些机器人和用户意图流程更灵活。但是它们永远无法涵盖所有情况,在关于 利用大型语言模型 的部分中,我们将看到如何解决这个问题。

重要提示

机器人或用户意图的所有示例都必须使用 andor 在流程的单个语句中定义,以组合它们。包含多个语句(注释除外)的流程将不会被解释为类意图流程。

内部事件#

除了所有读取和写入到系统事件通道的事件之外,还有一组特殊的内部事件,这些事件优先于系统事件,并且不会显示在事件通道上

# Starts a new flow instance with the name flow_id and an unique instance identifier flow_instance_uid
StartFlow(flow_id: str, flow_instance_uid: str, **more_variables)

# Flow will be finished successfully either by flow_id or flow_instance_uid
FinishFlow(flow_id: str, flow_instance_uid: str, **more_variables)

# Flows will be stopped and failed either by flow_id or flow_instance_uid
StopFlow(flow_id: str, flow_instance_uid: str, **more_variables)

# Flow has started (reached first match statement or end)
FlowStarted(flow_id: str, flow_instance_uid: str, **all_flow_variables, **more_variables)

# Flow with name flow_id has finished successfully (containing all flow instance variables)
FlowFinished(flow_id: str, flow_instance_uid: str, **all_flow_variables, **more_variables)

# Flow with name flow_id has failed (containing all flow instance variables)
FlowFailed(flow_id: str,  flow_instance_uid: str, **all_flow_variables, **more_variables)

# Any unhandled (unmatched) event will generate a 'UnhandledEvent' event,
# including all the corresponding interaction loop ids and original event parameters
UnhandledEvent(event: str, loop_ids: Set[str], **all_event_parameters)

请注意,参数 flow_id 包含流程的名称,参数 flow_instance_uid 包含实际的实例标识符,因为同一个流程可以多次启动。此外,对于后半部分的内部事件(包括 **all_flow_variables**),将返回所有流程参数和变量。

在底层,所有交互模式都基于这些内部事件。看一下例如 await 关键字的底层机制

# Start of a flow ...
await pattern a

# is equivalent to
start pattern a as $ref
match $ref.Finished()

# which is equivalent to
$uid = "{uid()}"
send StartFlow(flow_id="pattern a", flow_instance_uid=$uid)
match FlowStarted(flow_instance_uid=$uid) as $ref
match FlowFinished(flow_instance_uid=$ref.flow.uid)

内部事件可以像系统事件一样进行匹配和生成,但是将优先于任何下一个系统事件进行处理。这使我们能够创建更高级的流程,例如,当调用未定义的流程时触发的模式

flows/undefined_flow/main.co#
flow main
    activate notification of undefined flow start
    bot solve all your problems
    match RestartEvent()

flow notification of undefined flow start
    match UnhandledEvent(event="StartFlow") as $event
    bot say "Cannot start the undefined flow: '{$event.flow_id}'!"
    # We need to abort the flow that sent the FlowStart event since it might be waiting for it
    send StopFlow(flow_instance_uid=$event.source_flow_instance_uid)

在流程 ‘notification of undefined flow start’ 中,我们等待由 StartFlow 事件触发的 UnhandledEvent 事件,并警告用户尝试启动未定义的流程。

接下来,我们将更多地了解如何 使用变量和表达式