定义 Flows
简介
到目前为止,您只见过一个 flow,即 main flow。但在 Colang 中,我们可以定义许多不同的 flow,就像其他编程语言中的函数一样。一个 flow 定义了一个特定的交互模式,该模式由一系列语句组成。它有一个名称,可以包含空格字符,并且具有可选的输入和输出参数以及可选的默认值。
重要提示
Flow 语法定义
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."""
# ...
与 action 类似,可以使用关键字 start
、await
和 match
启动 flow 并等待其完成
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")
请注意,启动 flow 将立即处理并触发 flow 的所有初始语句,直至第一个等待事件的语句
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?")
重要提示
启动 flow 将立即处理并触发 flow 的所有初始语句,直至第一个等待事件的语句。
与 action 类似,flow 本身可以生成不同的事件,这些事件的优先级高于其他事件(请参阅 内部事件)
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
这些事件也可以像 flow 的对象方法一样访问
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
这些事件可以通过 flow 引用或 flow 名称本身进行匹配
# Match to flow event with flow reference
match $flow_ref.Finished()
# Match to flow event based on flow name
match (bot express greeting).Finished()
主要区别在于,通过引用匹配 flow 事件将特定于实际引用的 flow 实例,而通过 flow 名称匹配将对该 flow 的任何 flow 实例都成功。
这是一个带有参数的 flow 示例
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)
请注意,我们如何使用更简单的名称来抽象和简化 action 处理。这使我们能够将大多数 action 和事件包装到 flow 中,这些 flow 可以通过 Colang 标准库 (CSL) 轻松获得。
Flow 和 Action 生命周期
在一个 flow 中启动另一个 flow 将隐式地创建一个 flow 层级结构,其中 “main” flow 是所有 flow 的根 flow。与 action 类似,flow 的生命周期受其父 flow 的生命周期限制。换句话说,一旦启动 flow 的 flow 完成或自身停止,flow 就会停止
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” flow 启动并等待 flow “bot express greeting”,后者启动了两个 flow “bot say” 和 “bot gesture”。但是 flow “bot express greeting” 将仅等待 “bot say” 完成,如果 “bot gesture” 仍然处于活动状态,则会自动停止它。现在,使用我们简单的聊天 CLI 有点难以模拟,因为 UtteranceBotAction 和 GestureBotAction 都没有持续时间,并且会立即完成。在交互式系统中,机器人实际说话并使用例如动画进行手势 action,这将需要一些时间才能完成。但是我们也可以通过使用 TimerBotAction 来模拟这种效果,TimerBotAction 只会引入指定的延迟
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!
flow 的结束(完成或失败)也将停止所有剩余的活动 action。与 flow 类似,在 flow 中启动的 action 的生命周期受父 flow 的生命周期限制。这有助于限制意外的副作用,并使交互设计更健壮。
重要提示
任何启动的 flow 或 action 的生命周期都受父 flow 的生命周期限制。
并发模式匹配
Flow 不仅仅是其他编程语言中已知的函数。Flow 是可以并发匹配和进展的交互模式
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
两个 flow “pattern a” 和 “pattern b” 从 “main” 立即启动,等待第一个用户话语 action。在用户交互之后,您会看到两个 flow 如何完成,因为它们都匹配了交互模式。请注意,最后一个 bot action,说 “Goodbye”,在两个 flow 中都是相同的,因此只会触发一次。因此,$action_ref
实际上将指向同一个 action 对象。正如我们之前看到的,如果父 flow 已完成,action 将停止。对于在两个并发 flow 中共享的 action,这仍然成立,但只有当两个 flow 都完成时,才会强制停止它。
我们可以使用包装器 flow 来抽象 action 并进行相同的示例,它将完全相同地工作。请记住,我们不必编写 await
关键字,因为它是默认的
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)
当 flow ‘a’ 使用不太具体的匹配语句时,此示例将以相同的方式工作
# ...
flow pattern a
user said something
bot say "Goodbye"
# ...
flow user said something
match UtteranceUserAction.Finished()
现在,让我们看看如果两个匹配的 flow 在 action 上意见不一致,即在最后两个语句中有所不同,会发生什么情况
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!
我们可以从中看到,只要两个 flow 同意,它们都将按照它们的语句进展。在第三个语句中也是如此,其中 flow “pattern a” 正在等待特定的用户话语,而 “pattern b” 正在等待任何用户话语。有趣的是最后一个语句,它为这两个 flow 中的每一个触发了不同的 action,从而导致生成两个不同的事件。默认情况下,两个不同事件的并发生成在 Colang 中会发生冲突,需要解决。只能生成一个,但生成哪一个?冲突事件生成的解决是基于当前模式匹配的特异性完成的。特异性计算为匹配分数,该分数取决于与相应事件中所有可用参数相比匹配的参数数量。如果我们对所有可用事件参数都有匹配项,则匹配分数将最高。由于在第一次运行时用户询问 “How are you?”,并且 flow “pattern a” 中的第三个事件匹配语句是更好的匹配项,因此 flow “pattern a” 将成功触发其 action。另一方面,flow “pattern b” 将因冲突解决而失败。在第二次运行时,情况有所不同,只有 “pattern b” 会匹配,因此会进展。
重要提示
不同事件的并发生成会发生冲突,并将根据模式匹配的特异性(匹配分数)来解决。如果匹配分数完全相同,则将随机选择事件。
在解决事件生成冲突时,我们只考虑导致事件生成的当前事件匹配语句,而忽略 flow 中较早的模式匹配。
已完成/失败的 Flows
flow 的交互模式只能以两种不同的方式结束。要么成功匹配并触发模式的所有事件 (Finished
),要么更早失败 (Failed
)。
在以下情况之一中,交互模式被视为已成功完成
模式的所有语句都已成功处理,并且 flow 已到达其末尾。
在模式中到达
return
语句,表明 flow 定义的模式已成功匹配交互(请参阅 Flow 控制 部分)flow 定义的模式被认为已基于来自另一个 flow 的内部事件成功匹配(请参阅 内部事件 部分)。
注意
记住:flow 的 Finished
事件在 await
语句中隐式匹配,该语句结合了 flow 的启动,然后等待其完成。
如果 flow 中的交互模式失败,则 flow 本身被视为失败,并生成 Failed
事件。交互模式可能因以下原因之一而失败
模式中的 action 触发语句(例如
UtteranceBotAction(script="Yes")
)与另一个并发模式的 action 触发语句(例如UtteranceBotAction(script="No")
)冲突,并且比另一个 特异性更低。模式的当前匹配语句正在等待 不可能的事件 (例如,等待已失败的 flow 完成)。
在模式中到达
abort
语句,表明模式无法匹配交互(因此失败)(请参阅 Flow 控制 部分)。模式因另一个 flow 生成的内部事件而失败(请参阅 内部事件 部分)。
在 flow 层级结构上下文中,情况 B) 起着尤为重要的作用。让我们看一个示例以更好地理解这一点
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” 将导致 flow ‘pattern a’ 失败
> Hello
Hello
Pattern a failed
原因在于 flow 失败的方式
用户话语事件 “Hello” 并发匹配并推进 ‘pattern c’ 和 ‘pattern b’
Flow 模式 ‘pattern c’ 和 ‘pattern b’ 因其不同的 action 而冲突,并且 ‘pattern b’ 因特异性较低而失败
‘pattern b’ 的失败使得 flow ‘pattern a’ 永远无法完成,因为它正在等待 flow ‘pattern b’ 成功完成,因此 ‘pattern a’ 也失败了(请参阅情况 B)
失败的 flow 并不总是需要导致父 flow 也失败,可以通过使用关键字 start
异步启动 flow,或者使用 when/or when
flow 控制结构(请参阅 Flow 控制 部分)
这些是模式可能因不可能的事件而失败的所有情况
事件匹配语句等待特定 flow 的
FlowFinished
事件,但 flow 失败。事件匹配语句等待特定 flow 的
FlowFailed
事件,但 flow 成功完成。事件匹配语句等待特定 flow 的
FlowStarted
事件,但 flow 完成或失败。
Flow 分组
与 action 类似,我们可以对使用分组运算符 and
和 or
构建的 flow 组使用 start
和 await
。让我们根据以下四种情况,使用两个占位符 flow ‘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 并发启动,它们将一起进展,并可能导致 action 冲突。此类冲突的解决方式完全相同。让我们通过两个具体的 flow 示例来看一下
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 也解释了事件生成或组的底层机制(请参阅 事件生成 - 事件分组 部分)。随机选择是事件冲突解决的结果,而不是特殊情况。
第二个示例中带有用户 action 的情况 B 与情况 A 具有相同的效果。从语义的角度来看,这可能有点出乎意料,但与底层机制是一致的。
混合 Flow、Action 和事件分组
到目前为止,我们已经在分离的上下文中查看了事件、action 和 flow 分组。但实际上,它们都可以根据语句关键字在组中混合使用。
match
:仅接受事件组start
:接受 action 和 flow 组,但不接受事件await
:接受 action 和 flow 组,但不接受事件
# 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"
虽然这在如何设计交互模式方面提供了很大的灵活性,但 “良好的设计” 是在主交互模式设计中使用所有 action 和事件之前,将它们包装到 flow 中。
Flow 命名约定
您可能已经注意到,到目前为止,在 flow 的命名中刻意使用了时态。虽然如何命名 flow 没有约束性规则,但我们建议遵循以下约定
如果 flow 与表示 bot 或用户 action/意图的系统事件/action 相关,则以诸如
bot
或user
之类的主语开始 flow 名称。使用动词的祈使形式来描述应执行的 bot action,例如
bot say $text
。使用动词的过去式来描述已发生的 action,例如
user said something
或bot said something
使用
<subject> started <verb continuous form> ...
的形式来描述已启动的 action,例如bot started saying something
或user started saying something
对于应激活并等待特定交互模式以做出反应的 flow,以活动的名词或动名词形式开始,例如
reaction to user greeting
、handling user leaving
或tracking bot talking state
。
由于 flow 名称允许空格字符,并且我们有分组关键字 and
和 or
,因此 flow 名称当前不能在其名称中包含这两个关键字。通常,与其使用单词 “and”,不如使用单词 “then” 来组合两个 action,例如 bot greet then smile
来描述顺序依赖性。或者将其写为 bot greet smiling
(如果它是并发发生的)。
类 Action 和类 Intent 的 Flows
我们已经看过一些用户和 bot 类 action 的 flow 示例
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)
借助这些 flow,我们可以构建另一个抽象,即表示 bot 或用户意图的 flow
# 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"
请注意,bot 类 action 的 flow 将随机地将三个话语之一与问候手势结合起来,而用户类 action 的 flow 仅当收到指定的用户话语之一或用户手势时才会完成。借助更多示例或正则表达式,这些 bot 和用户意图 flow 可以变得更加灵活。但是它们永远不会涵盖所有情况,在关于 利用大型语言模型 的部分中,我们将看到如何解决这个问题。
重要提示
bot 或用户意图的所有示例都必须在使用 and
或 or
组合它们的 flow 中的单个语句中定义。包含多个语句(注释除外)的 Flow 将不会被解释为类意图的 flow。
内部事件
除了所有读取和写入系统事件通道的事件之外,还有一组特殊的内部事件,这些事件的优先级高于系统事件,并且不会显示在事件通道上
# 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 的名称,参数 flow_instance_uid
包含实际的实例标识符,因为同一个 flow 可以多次启动。此外,对于后半部分的内部事件(包括 **all_flow_variables**
),将返回所有 flow 参数和变量。
在底层,所有交互模式都基于这些内部事件。查看例如 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)
内部事件可以像系统事件一样进行匹配和生成,但将以高于任何下一个系统事件的优先级进行处理。这使我们能够创建更高级的 flow,例如,当调用未定义的 flow 时触发的模式
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)
在 flow ‘notification of undefined flow start’ 中,我们等待 UnhandledEvent
事件,该事件由 StartFlow
事件触发,并将警告用户尝试启动未定义的 flow。
接下来,我们将详细了解如何使用 使用变量和表达式。