定义流程#
介绍#
到目前为止,您只看到一个流程,即主流程。但在 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."""
# ...
在流程名称中允许空格字符的选择带来了一些限制
关键字
and
、or
和as
不能在流程名称中使用,需要用前导下划线字符进行转义(例如,this _and that
)。但通常,与其使用例如“and”这个词,不如使用“then”这个词来组合动作,例如bot greet then smile
来描述顺序依赖关系。或者,如果它是并发发生的,则可以将其写成bot greet smiling
。如 使用变量和表达式 章节所示,变量始终以
$
字符开头。
与动作一样,可以使用关键字 start
、await
和 match
启动流程并等待其完成
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 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()
主要区别在于,使用引用匹配流程事件将特定于实际引用的流程实例,而通过流程名称匹配将对该流程的任何流程实例都成功。
这是一个带有参数的流程示例
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 有点难以模拟,因为 UtteranceBotAction 和 GestureBotAction 都没有持续时间,并且会立即完成。在交互式系统中,机器人实际说话并使用例如动画来执行手势动作,这将需要一些时间才能完成。但是我们也可以通过使用 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 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
关键字,因为它是默认值
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()
现在,让我们看看如果两个匹配的流程在动作上不一致,从而导致最后两个语句不同,会发生什么
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
)。
在以下情况之一中,交互模式被视为已成功完成
模式的所有语句都已成功处理,并且流程已到达结尾。
到达
return
语句作为模式的一部分,表明流程定义的模式已成功匹配交互(请参阅 流程控制 部分)流程定义的模式被认为已基于来自另一个流程的内部事件成功匹配(请参阅 内部事件 部分)。
注意
记住:流程的 Finished
事件在 await
语句中隐式匹配,该语句组合了流程的启动,然后等待其完成。
如果流程中的交互模式失败,则流程本身被视为失败,从而生成 Failed
事件。交互模式可能因以下原因之一而失败
模式中的动作触发语句(例如
UtteranceBotAction(script="Yes")
)与另一个并发模式的动作触发语句(例如UtteranceBotAction(script="No")
)冲突,并且特异性低于另一个。模式的当前匹配语句正在等待不可能的事件(例如,等待已失败的流程完成)。
到达
abort
语句作为模式的一部分,表明无法针对交互匹配(因此失败)该模式(请参阅 流程控制 部分)。该模式由于另一个流程生成的内部事件而失败(请参阅 内部事件 部分)。
在流程层次结构上下文中,案例 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”将导致流程 ‘pattern a’ 失败
> Hello
Hello
Pattern a failed
失败的原因在于流程失败的方式
用户话语事件“Hello”同时匹配并推进 ‘pattern c’ 和 ‘pattern b’
流程模式 ‘pattern c’ 和 ‘pattern b’ 由于其不同的动作而冲突,并且 ‘pattern b’ 由于特异性较低而失败
‘pattern b’ 的失败使得流程 ‘pattern a’ 永远无法完成,因为它正在等待流程 ‘pattern b’ 成功完成,因此 ‘pattern a’ 也失败了(请参阅案例 B)
失败的流程并不总是需要导致父流程也失败,可以通过使用关键字 start
异步启动流程,或者使用 when/or when
流程控制结构(请参阅 流程控制 部分)
这些是由于不可能的事件而导致模式可能失败的所有情况
事件匹配语句等待特定流程的
FlowFinished
事件,但流程失败。事件匹配语句等待特定流程的
FlowFailed
事件,但流程成功完成。事件匹配语句等待特定流程的
FlowStarted
事件,但流程完成或失败。
流程分组#
与动作一样,我们可以在使用分组运算符 and
和 or
构建的流程组上使用 start
和 await
。让我们根据以下四种情况,使用两个占位符流程 ‘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"
虽然这在如何设计交互模式方面提供了很大的灵活性,但将所有动作和事件包装到流程中,然后在主交互模式设计中使用它们被认为是“良好的设计”。
流程命名约定#
您现在可能已经发现了流程命名中刻意使用时态。虽然关于如何命名流程没有约束性规则,但我们建议遵循这些约定
如果流程与表示机器人或用户动作/意图的系统事件/动作相关,则以诸如
bot
或user
之类的主语开头流程名称。使用动词的祈使形式来描述应执行的机器人动作,例如
bot say $text
。使用动词的过去式来描述已发生的动作,例如
user said something
或bot said something
使用形式
<subject> started <verb continuous form> ...
来描述已开始的动作,例如bot started saying something
或user started saying something
对于应激活并等待特定交互模式做出反应的流程,以名词或动名词形式的活动开头,例如
reaction to user greeting
、handling user leaving
或tracking 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"
请注意,类机器人动作流程将随机地将三个话语之一与问候手势组合在一起,而只有在收到指定的用户话语或用户手势之一时,类用户动作流程才会完成。借助更多示例或正则表达式,可以使这些机器人和用户意图流程更灵活。但是它们永远无法涵盖所有情况,在关于 利用大型语言模型 的部分中,我们将看到如何解决这个问题。
重要提示
机器人或用户意图的所有示例都必须使用 and
或 or
在流程的单个语句中定义,以组合它们。包含多个语句(注释除外)的流程将不会被解释为类意图流程。
内部事件#
除了所有读取和写入到系统事件通道的事件之外,还有一组特殊的内部事件,这些事件优先于系统事件,并且不会显示在事件通道上
# 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)
内部事件可以像系统事件一样进行匹配和生成,但是将优先于任何下一个系统事件进行处理。这使我们能够创建更高级的流程,例如,当调用未定义的流程时触发的模式
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
事件,并警告用户尝试启动未定义的流程。
接下来,我们将更多地了解如何 使用变量和表达式。