Intent Layers¶
In a nutshell
Normally a skill listens for all of its commands at once. Intent Layers let a skill turn commands on and off as a conversation progresses, so only the choices that make sense are available at that point, much like a "choose your own adventure" book where each page unlocks the next set of options. This is handy for step-by-step flows, games, or anything that should react differently depending on what the user just did. For the broader picture, see the Glossary.
๐ Formal specification
IntentLayer gating is implemented through the session's intent context. Each layer is a session context token its intents require. The mechanism is specified by OVOS-CONTEXT-1: Intent Context (see Context and the spec index).
IntentLayers are per-session. IntentLayer state lives in the session, so layered skills are concurrency-safe across HiveMind satellites. Two satellites can be in different layers at the same time. (The lower-level enable_intent / disable_intent calls in the next section still change the global intent set, so prefer layers for per-session flows.)
Not the same as skill permissions. Intent layers switch groups of intents on and off inside one already-active skill. Whether a skill is even allowed to participate in converse at all (whitelists, blacklists, ConverseMode) is a separate, coarser gate. See Permissions & Activation Control.
Managing Intents¶
Sometimes you might want to manually enable or disable an intent. In OVOSSkills you can do this explicitly to create stateful interactions:
class RotatingIntentsSkill(OVOSSkill):
def initialize(self):
# NOTE: this must be done in initialize, not in __init__
self.disable_intent("B.intent")
self.disable_intent("C.intent")
@intent_handler("A.intent")
def handle_A_intent(self, message):
# do stuff
self.enable_intent("B.intent")
self.disable_intent("A.intent")
@intent_handler("B.intent")
def handle_B_intent(self, message):
# do stuff
self.enable_intent("C.intent")
self.disable_intent("B.intent")
@intent_handler("C.intent")
def handle_C_intent(self, message):
# do stuff
self.enable_intent("A.intent")
self.disable_intent("C.intent")
NOTE:
enable_intent/disable_intentchange the global intent set. These states are shared across Sessions. For per-session gating, use Intent Layers (which gate via intent context) instead.
Two mechanisms, similar names, different scope
The skill methods self.enable_intent() and self.disable_intent() are device-wide. They work by deregistering the intent and registering it again, so the effect lands on the shared pipeline registry no matter which session the calling skill was serving, and no matter whether it was serving one at all.
The bus messages ovos.intent.enable and ovos.intent.disable (OVOS-INTENT-4 ยง8.5) are a different mechanism with a different scope. Each binds to the session its message carries: under the default session the effect is device-wide, because every session inherits that registration, while under a satellite's own session_id it suppresses the intent for that satellite alone and leaves every other session matching. The adapt and padatious pipeline plugins each keep their own session-scoped record and consult it when they choose match candidates.
A skill method is therefore not the in-process shortcut for the bus message of nearly the same name. Reach for the bus message when one session must differ from the others, and for intent layers when a skill is walking one session through a sequence of states.
State Machines¶
ovos-workshop also provides IntentLayers, to manage groups of intents together.
IntentLayers work well for implementing state machines.
The Manual way¶
In this example we implement the Konami Code, doing everything the manual way instead of using decorators.
class KonamiCodeSkill(OVOSSkill):
def initialize(self):
self.counter = 0
self.top_fails = 3
up_intent = IntentBuilder('KonamiUpIntent').require("KonamiUpKeyword").build()
down_intent = IntentBuilder('KonamiDownIntent').require("KonamiDownKeyword").build()
left_intent = IntentBuilder('KonamiLeftIntent').require("KonamiLeftKeyword").build()
right_intent = IntentBuilder('KonamiRightIntent').require("KonamiRightKeyword").build()
b_intent = IntentBuilder('KonamiBIntent').require("KonamiBKeyword").build()
a_intent = IntentBuilder('KonamiAIntent').require("KonamiAKeyword").build()
self.register_intent(up_intent, self.handle_up_intent)
self.register_intent(down_intent, self.handle_down_intent)
self.register_intent(left_intent, self.handle_left_intent)
self.register_intent(right_intent, self.handle_right_intent)
self.register_intent(b_intent, self.handle_b_intent)
self.register_intent(a_intent, self.handle_a_intent)
def build_intent_layers(self):
self.intent_layers.update_layer("up1", ["KonamiUpIntent"])
self.intent_layers.update_layer("up2", ["KonamiUpIntent"])
self.intent_layers.update_layer("down1", ["KonamiDownIntent"])
self.intent_layers.update_layer("down2", ["KonamiDownIntent"])
self.intent_layers.update_layer("left1", ["KonamiLeftIntent"])
self.intent_layers.update_layer("right1",["KonamiRightIntent"])
self.intent_layers.update_layer("left2", ["KonamiLeftIntent"])
self.intent_layers.update_layer("right2",["KonamiRightIntent"])
self.intent_layers.update_layer("B",["KonamiBIntent"])
self.intent_layers.update_layer("A",["KonamiAIntent"])
self.intent_layers.activate_layer("up1")
def reset(self):
self.active = False
self.counter = 0
self.intent_layers.disable()
self.intent_layers.activate_layer("up1")
def handle_up_intent(self, message):
if self.intent_layers.is_active("up1"):
self.intent_layers.deactivate_layer("up1")
self.intent_layers.activate_layer("up2")
else:
self.intent_layers.activate_layer("down1")
self.intent_layers.deactivate_layer("up2")
self.acknowledge()
def handle_down_intent(self, message):
if self.intent_layers.is_active("down1"):
self.intent_layers.deactivate_layer("down1")
self.intent_layers.activate_layer("down2")
else:
self.intent_layers.activate_layer("left1")
self.intent_layers.deactivate_layer("down2")
self.acknowledge()
# handle_left_intent, handle_right_intent, and handle_b_intent follow the
# same pattern: check which layer of the pair is active, deactivate it,
# and activate the next layer in the sequence (left1/left2, then
# right1/right2, then "B", then "A")
def handle_a_intent(self, message):
self.play_audio(self.find_resource("power_up.mp3", "snd"))
self.reset()
def stop(self):
if self.active:
self.reset()
def converse(self, message):
if self.active:
if not any(self.voc_match(utt, kw) for kw in ["KonamiUpKeyword",
"KonamiDownKeyword",
"KonamiLeftKeyword",
"KonamiRightKeyword",
"KonamiBKeyword",
"KonamiAKeyword"]):
self.counter += 1
if self.counter > self.top_fails:
self.speak("Wrong cheat code")
self.reset()
else:
self.speak("Wrong! Try again")
return True
return False
Decorators¶
When you have many complex chained intents, IntentLayers often makes your work easier. A
layer is a named group of intents that you can manage at once.
Slightly more complex than the previous example, we may want to offer several "forks" on the intent execution, enabling different intent groups depending on previous interactions.
skill-moon-game (a VoiceGamez title, not public) is an example full voice
game implemented this way.
Here is an excerpt from the game to illustrate usage of IntentLayer decorators:
NOTE:
IntentLayersare per-session, gated via intent context (OVOS-CONTEXT-1), so each voice satellite keeps its own layer state instead of all joining the same game.
from ovos_workshop.decorators.layers import layer_intent, enables_layer, \
disables_layer, resets_layers
class Apollo11GameSkill(OVOSSkill):
def initialize(self):
# start with all game states disabled
self.intent_layers.disable()
@intent_handler(IntentBuilder("StartApollo11Intent"). \
optionally("startKeyword"). \
require("MoonGameKeyword"))
def handle_start_intent(self, message=None):
if not self.playing:
self.playing = True
self.speak_dialog("start.game")
self.handle_intro()
else:
self.speak_dialog("already.started")
@layer_intent(IntentBuilder("StopApollo11Intent"). \
require("stopKeyword"). \
optionally("MoonGameKeyword"),
layer_name="stop_game")
@resets_layers()
def handle_game_over(self, message=None):
if self.playing:
self.speak_dialog("stop.game")
@enables_layer(layer_name="guard")
@enables_layer(layer_name="stop_game")
def handle_intro(self):
self.speak_dialog("reach_gate")
self.speak_dialog("guard")
self.speak_dialog("present_id", expect_response=True)
@layer_intent(IntentBuilder("Yes1Apollo11Intent").require("yesKeyword"),
layer_name="guard")
def handle_yes1(self, message=None):
self.speak_dialog("guard_yes")
self.briefing_question1()
@layer_intent(IntentBuilder("No1Apollo11Intent").require("noKeyword"),
layer_name="guard")
@enables_layer(layer_name="guard2")
@disables_layer(layer_name="guard")
def handle_no1(self, message=None):
self.speak_dialog("guard_no")
self.speak_dialog("present_id", expect_response=True)
# (...) more intent layers
def converse(self, message):
if not self.playing:
return False
# (...)
# take corrective action when no intent matched
if self.intent_layers.is_active("guard") or \
self.intent_layers.is_active("guard2"):
self.speak_dialog("guard_dead")
self.handle_game_over()
# (...)
else:
self.speak_dialog("invalid.command", expect_response=True)
return True
Under the hood¶
self.intent_layers is an instance of IntentLayers
(ovos_workshop.decorators.layers.IntentLayers), created in OVOSSkill.__init__ and bound
to the skill during bind() โ so it is always available by the time your skill code runs,
decorator or not (which is why the "Manual way" above can call it directly from
initialize()).
A layer is not a separate matching mechanism. It's a named group of intents mapped to a
single synthetic intent context token (layer_<name>, prefixed internally with
the skill id). Every intent registered under that layer is set to require the token, so:
activate_layer("guard")callsself.set_context(...)for the layer's token. The layer's intents become matchable.deactivate_layer("guard")callsself.remove_context(...). They stop matching again.- The intents themselves stay registered with the intent service for the skill's whole lifetime. Only whether their required context is present changes. There's no detach/re-attach churn.
Because layer state rides on intent context, and intent context is per-session, each voice satellite talking to a shared skill keeps its own independent set of active layers.
Read next: Permissions & Activation Control Related: Context ยท Intent Design ยท Converse ยท Decorators