定义流程#
引言#
到目前为止,您只见过一个流程,即主流程。但在 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
)。但通常情况下,您可以使用 ‘then’ 来组合动作,而不是使用 ‘and’,例如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 都没有持续时间,会立即完成。在实际交互系统中,如果 bot 说话并使用动画进行手势动作,这将需要一些时间才能完成。但我们也可以使用 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”启动,等待第一个用户语音动作。在用户交互后,您会看到这两个流程都完成了,因为它们匹配了交互模式。请注意,最后一个 bot 动作“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 中,默认情况下并发生成两个不同的事件会发生冲突,需要解决。只能生成一个,但哪个呢?冲突事件的解决是基于当前模式匹配的特异性(specificity)进行的。特异性计算为一个匹配分数,该分数取决于与相应事件中所有可用参数相比匹配的参数数量。如果匹配所有可用事件参数,匹配分数将最高。由于在第一次运行时用户问了“你好吗?”,并且流程“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 或用户动作/意图的系统事件/动作相关,则以
bot
或user
等主语开头命名流程。使用动词的祈使形式描述应执行的 bot 动作,例如
bot say $text
。使用动词的过去形式描述已发生的动作,例如
user said something
或bot said something
使用
<主语> started <动词进行时> ...
形式描述已开始的动作,例如bot started saying something
或user started saying something
对于应该被激活并等待特定交互模式以作出反应的流程,以活动的名词或动名词形式开头,例如
reaction to user greeting
(对用户问候的反应)、handling user leaving
(处理用户离开)或tracking bot talking state
(跟踪 bot 说话状态)。
类似动作和类似意图的流程#
我们已经看到了一些用户和 bot 类似动作流程的示例
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)
借助这些流程,我们可以构建另一种抽象,即代表 bot 或用户意图的流程
# 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 类似动作流程会随机将三种语音中的一种与问候手势结合,而用户类似动作流程只有在收到指定的某个用户语音或用户手势后才会完成。借助更多示例或正则表达式,这些 bot 和用户意图流程可以变得更加灵活。但它们永远不会涵盖所有情况,在使用大型语言模型一节中,我们将看到如何解决这个问题。
重要
bot 或用户意图的所有示例必须在流程中使用 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
事件,并会警告用户尝试启动未定义的流程。
接下来,我们将看到更多关于如何使用变量与表达式的内容。