More on Flows
In section Defining Flows we learned the core mechanisms of flows. In this section will look at more advanced topics that are related to flows.
Activate a Flow
We already have seen the start
and await
keywords to trigger a flow. We are now introducing the third keyword activate
that can start a flow. The difference to start
lies in the behavior of the flow when it has finished or failed. If a flow was activated it will always automatically restart a new instance of the flow as soon as it has ended.
Important
Flow activation statement syntax definition:
activate <Flow> [and <Flow>]…
Currently, reference assignments for activated flows is not supported since the instance will change after a restart
Only flow and-groups are supported
Examples:
# Activate a single flow
activate handling user presents
# Activate a group of flows
activate handling user presents and handling question repetition 5.0
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"
Running this example you will see the bot responding with “Hello again” as long as you keep greeting with “Hi”:
Welcome
> Hi
Hello again
> Hi
Hello again
> Bye
Goodbye
> Hi
Hello again
> Bye
>
In contrast, you can only say “Bye” once before you restart the story.
Activating a flow enables you to keep matching the interaction event sequence against the pattern defined in the flow, even if the pattern previously successfully matched the interaction event sequence or failed. This is often used for passively observing a pattern to update a state or to trigger actions that don’t compete with the main interaction pattern.
Important
Activating a flow will start a flow and automatically restart it when it has ended to match to reoccurring interaction patterns.
Important
The main flow behaves also like an activated flow. As soon as it reaches the end it will restart automatically.
There is one exception though from this rule! If a flow does not contain any statement that waits for an event and immediately finishes, it will run only once when activated and it will stay activated since otherwise you would get an infinite loop.
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
See, how the main flow does not require any match statement at the end and will continue to be activated without repeating, even though it reached the end.
Important
An activated flow that immediately finished (does not wait for any event) will only be run once and will stay activated.
Start a new Flow Instance
In some cases it is not enough to restart a flow only once it has finished since this can miss certain pattern repetitions:
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"
In the following interaction we see that the second “Hi” of the user does not trigger anything since the flow already advanced to the next statement user said "Bye"
but did not yet start an new instance due to its activation:
Welcome
> Hi
Hello again
> Hi
> Bye
Goodbye
> Hi
Hello again
>
If we want to start an new instance before the end of the current instance, we can achieve that by adding a label called start_new_flow_instance
at the corresponding position in the interaction sequence:
# ...
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"
We now see the correct behavior:
Welcome
> Hi
Hello again
> Hi
Hello again
> Bye
Goodbye
> Bye
>
Note, that as soon as the second instance advances to the next match statement, a third instance is started, waiting for the next user input “Hi”. The other two instances will advance in parallel. Since the first instance already started a new instance (second one) it will not start another one such that we don’t get a growing number of instances when progressing. Note how the second “Bye” will not trigger anything since the first and second instance have already finished and the third instance is still at the first statement waiting for a “Hi”.
Note
You can think of the start_new_flow_instance
label being at the end of each activated flow. Defining it in a different position will move it up from the default position at the end.
Override Flows
A flow can be overridden by another flow with the same name by using the override decorator:
flow bot greet
bot say "Hi"
@override
flow bot greet
bot say "Hello"
In this example the second ‘bot greet’ flow will override the first one. This is particularly useful when working with imported Colang modules from a library to override, e.g. the ‘bot say’ flow from the core module of the standard library to include an additional log statement:
import core
flow main
bot say "Hi"
@override
flow bot say $text
log "bot say {$text}"
await UtteranceBotAction(script=$text) as $action
At the moment the definition order of flows does not make a difference and therefore only two flows with the same name can be defined where one must have the override decorator.
Note
If two flows have the same name, one must be prioritized by the override decorator.
Interaction Loops
So far, any concurrently progressing flows that resulted in different event generations created a conflict that needed to be resolved. While this makes sense in many cases, sometimes one would like to allow different actions to happen at the same time. In particular, when these actions are on different modalities. We can achieve this by defining different interaction loops using the a decorator style syntax on flows:
Important
Interaction loop syntax definition:
@loop("<loop_name>")
flow <name of flow> ...
Hint: To generate a new loop name for each flow call use the loop name “NEW”
By default, any flow without an explicit interaction loop inherits the interaction loop of its parent flow. Let’s see now an example of a second interaction loop to design flows that augment the main interaction rather than compete with it:
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"
The example implements two bot reaction flows that listen to the user saying “Hi” or “Bye”. Whenever one of the two events happen the bot will show the corresponding gesture “smile” or “frown”, respectively. Note how these flows inherit their interaction loop id from the parent flow ‘handling bot gesture reaction’ that is different from the main flow. Therefore, bot gesture actions will never compete with the bot say actions from the main interaction flow and will be triggered in parallel:
> 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
Flow Conflict Resolution Prioritization
In section Defining Flows we have already learned a bit about the mechanics of resolving an action conflict between flows. We will now look at this in more detail.
For every successful match statement a matching score is computed that is greater than \(0.0\) (no match) and smaller or equal to \(1.0\) (perfect match). A perfect match is when all parameters of the expected event match with all the parameters from the actual event. If the actual event has more parameters than the expected event the matching score will be decreased by multiplying it by a factor of \(0.9\) for every missing parameter. So let’s say we have a matching event containing five parameters, but we only specified two of them, the score would be \(0.9^{5-2} = 0.729\). Since a system event can trigger a chain of internal events we need to take into account all the generated matching scores in that sequence. Let’s use the following example to better illustrate that:
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)
After starting the main flow, the two flows ‘pattern a’ and ‘pattern b’ will be active and waiting for the user to say something. Let’s look at the two event generation chains triggered by the event 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")
Because the resulting action events at the end of these chains are different, there will be a conflict that needs to be resolved. Let’s look at the corresponding match statements in these chains:
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")
Comparing these match statements to the events will result in the following matching scores:
1) 1.0 -> 1.0 -> 1.0
2) 0.9 -> 1.0 -> 1.0
In order to find the best event matching sequence we will compare each matching score from the different chains from left to right and determine the winner as soon as one score is higher than the other. You see that the first match in the second chain is not perfect and resulted in a value of \(0.9\). Therefore, the first chain is the winner and the second will fail, resulting in the following output:
> Hi
Hello
In some cases you might want to influence the matching score of some matches to change the conflict resolution outcome. You can do this by specifying a flow priority with the statement priority <float_value>
where the value is between \(0.0\) and \(1.0\). Each match in the flow will then be multiplied by the current flow priority. Since this approach currently can only reduce the matching score you cannot use it to increase the priority of a match. A work around that can sometimes be employed is to improve the matching score of a non-perfect match by adding missing parameters using a regular expression that matches any value like that regex(".*")
:
# ...
flow user said something
match UtteranceUserActionFinished(final_transcript=regex(".*"))
# ...
In this example the conflict resolution between the actions will happen at random since all the matching scores are equal.