Converse Pipeline¶
Maturity — Mature ⬤⬤⬤⬤⬤
Long-lived and actively maintained. Depend on it freely. Rated by repository health, not version.
In a nutshell
Normally each thing you say to the assistant is handled on its own. The Converse pipeline lets a skill stay "in the conversation" for a few turns, so it can ask a follow-up and understand your reply in context, much like a person who remembers what you were just talking about. For example, after a skill asks "which room?", it can keep listening so your answer "the kitchen" lands in the right place. See the Glossary for terms, or Fallbacks for what happens when nothing is actively listening.
This is a flow stage: part of every standard pipeline rather than a matcher you choose between.
The rest of this page is for people deploying or customizing OVOS. If you only wanted to know what this stage does, you are done.
📐 Formal specification
The converse plugin is specified by OVOS-CONVERSE-1 — Active Handlers & Interactive Response, built on OVOS-PIPELINE-1. It is the imperative complement to OVOS-CONTEXT-1 — Intent Context (the declarative gating primitive, see Follow up questions). See the spec index.
The Converse Pipeline in OpenVoiceOS (OVOS) manages active conversational contexts between the assistant and skills. It lets skills keep handling user input across multiple turns, enabling more natural, stateful conversations.
Spec model vs. current code names
OVOS-CONVERSE-1 frames converse as an ordinary pipeline plugin. It is not a special case in the orchestrator. During match it inspects two session fields and returns a Match on a reserved intent_name (PIPELINE-1 §7.3), which the orchestrator then dispatches like any other intent: <skill_id>:converse for a converse claim, <skill_id>:response for delivery of a solicited reply.
Converse depends on session state ("is a skill still active?", "is someone awaiting a reply?"), so it must be placed before the other pipeline plugins in session.pipeline. First-match-wins then lets converse intercept before a generic matcher sees the utterance. Two name mappings exist between spec and current code:
| OVOS-CONVERSE-1 (canonical) | Current ovos-core code |
|---|---|
session.converse_handlers — the recency-ordered eligibility list the plugin polls |
Session.active_handlers (the deprecated Session.active_skills view is a legacy projection of it) |
session.response_mode {skill_id, expires_at} — the single-shot response window for get_response |
per-session UtteranceState.RESPONSE lock |
poll round-trip <skill_id>.converse.ping / .pong (non-dispatch, dotted form) |
since ovos-core 3.0.5a1, a broadcast ovos.converse.ping (OVOS-CONVERSE-1 §4.2) is emitted alongside a legacy per-skill {skill_id}.converse.ping, and pongs are heard on both ovos.converse.pong and skill.converse.pong; dispatch via converse:skill → {skill_id}.converse.request |
reserved-name dispatch <skill_id>:converse / <skill_id>:response |
converse:skill handler |
The mechanics below describe the current code. The spec names are the target. Note that the two lists are distinct in the spec: converse_handlers (converse eligibility) is drained independently of active_handlers (the stop cascade's recency record, PIPELINE-1 §7.1).
Purpose¶
The Converse pipeline enables multi-turn conversations by prioritizing which skills are given the opportunity to handle an utterance through their converse() method before normal intent parsing occurs.
Key purposes include:
-
Preserve conversational context across multiple turns.
-
Prioritize the skills used last for more natural interactions.
-
Enable stateful behavior, such as follow-up questions or corrections.
-
Prevent unnecessary intent parsing when a skill is already engaged.
-
Support skill-defined session control via manual activation/deactivation.
Implementation¶
Module: ovos_core.intent_services.converse_service.ConverseService
Pipeline plugin ID: ovos-converse-pipeline-plugin
Stage name: converse
ConverseService ships inside ovos-core. It is registered via the opm.pipeline entry point in its pyproject.toml:
[project.entry-points."opm.pipeline"]
ovos-converse-pipeline-plugin = "ovos_core.intent_services.converse_service:ConverseService"
Active Skill List¶
A skill is considered active if it has been called in the last 5 minutes (configurable via timeout).
Skills are called in order of when they were last active. For example, if a user speaks the following commands:
Hey Mycroft, set a timer for 10 minutes
Hey Mycroft, what's the weather
The utterance "what's the weather" is first sent to the Timer Skill's converse() method, then to the intent service for normal handling, where the Weather Skill is called.
Because the Weather Skill was called, it is added to the front of the Active Skills List. The next utterance received is directed to:
-
WeatherSkill.converse() -
TimerSkill.converse() -
Normal intent parsing service
When does a skill become active?¶
-
Before an intent is called, the skill is activated.
-
If a fallback returns True (to consume the utterance), the skill is activated right after the fallback.
-
If converse returns True (to consume the utterance), the skill is reactivated right after converse.
-
A skill can activate or deactivate itself at any time via
self.activate()/self.deactivate().
Active skills are tracked in Session.active_handlers (ovos_bus_client.session.Session; the deprecated Session.active_skills view logs a warning on every access). The converse service reads and updates this list via session.activate_skill() / session.deactivate_skill() (back-compat shims over add_active_handler / remove_active_handler), which also forward intent.service.skills.activated / intent.service.skills.deactivated on the bus.
Pipeline Stages¶
| Pipeline ID | Description | Recommended Use |
|---|---|---|
converse |
Continuous dialog for skills | Should always be present; do not remove unless you know what you are doing |
How It Works¶
sequenceDiagram
participant O as Orchestrator
participant C as ConverseService
participant S1 as Highest-priority active skill
participant S2 as Next active skill
O->>C: converse stage hit
C-->>S1: ovos.converse.ping (broadcast)
C-->>S2: ovos.converse.ping (broadcast)
C->>S1: <skill_id>.converse.ping (legacy, per skill)
C->>S2: <skill_id>.converse.ping (legacy, per skill)
S1-->>C: ovos.converse.pong (can_handle)
S2-->>C: ovos.converse.pong (can_handle)
C->>S1: {skill_id}.converse.request (first willing skill)
S1-->>C: converse() returns True
C-->>O: utterance consumed, skill reactivated
Diagram: the orchestrator hits the converse stage, ConverseService polls the active skills (one broadcast ping plus a legacy per-skill ping each), collects pongs, then sends a converse request to the first willing skill, ending with the utterance consumed and that skill reactivated.
-
conversestage is hit in the pipeline -
ConverseService.match()iterates active skills in priority order -
It first polls the active skills: since ovos-core 3.0.5a1 it emits one broadcast
ovos.converse.ping(OVOS-CONVERSE-1 §4.2) plus a legacy per-skill{skill_id}.converse.ping, and waits up to0.5sfor acknowledgements onovos.converse.pongor the legacyskill.converse.pong(can_handle) to learn which skills are willing to converse. Each poll round is correlated bycontext.utterance_id(a per-utterance uuid the orchestrator stamps per OVOS-PIPELINE-1 §9.1.1, propagated byMessage.reply/forward): a pong carrying a differentutterance_idbelongs to an earlier round and is discarded, so a slow skill's stale answer can never decide the wrong round -
The first willing skill (highest priority) is matched; the actual dispatch is sent as
{skill_id}.converse.request(emitted by theconverse:skillhandler) -
If the skill's
converse()returnsTrue, the utterance is consumed and the skill is reactivated -
If not, the next willing skill is tried
-
If no active skill accepts the input, the pipeline falls back to normal intent matching
ConverseService is a plain PipelinePlugin with a single match() method. It is not a ConfidenceMatcherPipeline, so there are no converse_high/medium/low stages. The one stage ID is converse.
Skill Integration¶
Skills integrate with the converse pipeline by:
-
Implementing a
converse()method that checks if the skill wants to handle an utterance. -
Returning
Trueif the utterance was handled,Falseotherwise. -
Managing internal state to determine when to exit conversation mode.
from ovos_workshop.skills.converse import ConversationalSkill
class MySkill(ConversationalSkill):
def converse(self, message):
utterance = message.data["utterances"][0]
if "help" in utterance:
self.speak("Here to help!")
return True # consumed
return False # pass to next handler
This enables modular, stateful conversations without hardcoding turn-taking logic into the core assistant.
Configuration¶
Customize the pipeline via mycroft.conf under skills.converse:
{
"skills": {
"converse": {
"timeout": 300,
"skill_timeouts": {},
"converse_mode": "accept_all",
"converse_whitelist": [],
"converse_blacklist": [],
"converse_activation": "accept_all",
"max_activations": -1,
"skill_activations": {},
"cross_activation": true,
"converse_priorities": {}
}
}
}
Key Options
| Config Key | Description |
|---|---|
timeout |
Default seconds before an idle skill is removed from converse mode (default 300) |
skill_timeouts |
Per-skill override of timeout |
converse_mode |
Global mode for allowing/disallowing skills from converse participation |
converse_blacklist |
Skills not allowed to enter converse mode |
converse_whitelist |
Skills explicitly allowed to converse |
converse_activation |
Controls when a skill can self-activate |
max_activations |
Default number of consecutive times a skill can activate itself (-1 = unlimited); the counter resets when the skill is deactivated |
skill_activations |
Per-skill override of max_activations |
cross_activation |
If true, any skill can activate or deactivate any other skill (the same key gates both) |
converse_priorities |
Per-skill priority overrides used when converse_activation is priority |
Converse Modes¶
| Mode | Description |
|---|---|
accept_all |
All skills are allowed to use converse mode (default) |
whitelist |
Only skills explicitly listed in converse_whitelist can use converse mode |
blacklist |
All skills can use converse mode except those in converse_blacklist |
Converse Activation Modes¶
| Mode | Description |
|---|---|
accept_all |
Any skill can activate itself unconditionally (default) |
priority |
Skills can only activate themselves if no skill with higher priority is active |
whitelist |
Only skills in converse_whitelist can activate themselves |
blacklist |
Only skills NOT in converse_blacklist can activate themselves |
Note:
converse_activationdoes not apply to regular skill activation, only to skill-initiated activation requests (for example,self.activate()).
Bus Events Handled¶
| Event | Handler |
|---|---|
intent.service.skills.activate |
handle_activate_skill_request |
intent.service.skills.deactivate |
handle_deactivate_skill_request |
intent.service.active_skills.get |
handle_get_active_skills |
skill.converse.get_response.enable |
handle_get_response_enable |
skill.converse.get_response.disable |
handle_get_response_disable |
converse:skill |
handle_converse |
get_response Support¶
During skill.get_response, the skill temporarily holds the converse channel. This is tracked per session in Session.response_mode, a single {skill_id, expires_at} object naming the one holder, or None when nobody is waiting on a direct answer. While a holder is set, match() routes the next utterance straight to that skill (match type {skill_id}.converse.get_response), bypassing the normal ping/pong path.
Session.utterance_states is a deprecated view of the same window, shaped as {skill_id: state} for code written before response_mode existed. It stores nothing of its own: reading it projects the current holder as UtteranceState.RESPONSE and reports every other skill as INTENT by omission, and writing to it, including mutating the dict it returns, forwards to set_response_mode and clear_response_mode. The two cannot disagree. Every access logs a deprecation warning naming the major version that removes it, so write new code against response_mode and the enable_response_mode / disable_response_mode / clear_response_mode methods.
-
skill.converse.get_response.enable→session.enable_response_mode(skill_id)(lock converse to this skill) -
skill.converse.get_response.disable→session.disable_response_mode(skill_id)(release lock)
Security & Performance¶
A malicious or badly designed skill using the converse method can potentially hijack the whole conversation loop and render the skills service unusable.
Protections include:
-
Timeouts for inactivity (
timeout) drop idle skills from the active list. -
max_activationslimits per skill. -
Blacklist/whitelist enforcement to restrict which skills can enter converse mode.
-
cross_activationcan be disabled to prevent skill-to-skill manipulation.
Notes¶
-
The plugin does not enforce a fallback behavior if no skill accepts the input.
-
If no skill handles the utterance via converse, the pipeline falls back to normal intent matching or fallback skills.
-
This mechanism is ideal for multi-turn conversations like dialogs, games, or assistant flows that require memory of previous input.
-
In
priorityactivation mode, every skill defaults to priority 50; per-skill overrides are set viaconverse_prioritiesin config. A skill may only self-activate if no higher-priority skill is already active.
Source code: OpenVoiceOS/ovos-core.
Read next: Stop Pipeline Related: Converse · Sessions (multi-user state) · Skill Classes · Fallback Pipeline