Repository navigation
Speaker Context
Murdock recognises who is speaking — but that only helps if the value reaches your Home Assistant conversation agent, fresh, on the right turn. Settings → Speaker context to the conversation agent offers a delivery mode dropdown. Pick the one that matches how you run Assist.
Since 0.8.0 there is a third way that avoids the trade-off below entirely: the HA custom integration. It contributes the speaker as a prompt line that Home Assistant rebuilds on every turn, so local intent matching keeps working and the speaker is never stale. If you can install a custom component, that's the recommended route — the two modes below remain for setups that can't or won't.
Home Assistant's Assist runs two stages: the built-in intent matcher (hassil) sees the raw transcript first and handles simple commands ("turn on the light") locally; only a miss is passed to the LLM. Two facts follow:
- The intent matcher matches the whole transcript and has no comment/ignore syntax — so anything Murdock appends to the transcript breaks local matching.
- The LLM's system prompt is cached within a conversation — so a speaker value injected there can go stale if a different person speaks in the same conversation.
You can't have both perfectly at once, hence the two modes.
Murdock returns the transcript untouched. The recognised speaker is
published over MQTT (auto-discovered sensor.murdock_* entities) and/or
the legacy REST input_text. You read those in your conversation
agent's system prompt:
{% set speaker = states('sensor.murdock_current_speaker') %}
{% if speaker not in ['unknown', 'unavailable', 'unknown', ''] %}
The current speaker is {{ speaker }} ({{ states('sensor.murdock_speaker_role') }}),
recognised with {{ states('sensor.murdock_speaker_confidence') }}% confidence.
{% else %}
The speaker could not be identified.
{% endif %}- ✅ HA's local intent matching keeps working — fast, offline commands.
⚠️ HA caches the system prompt within a conversation. If user X speaks and user Y follows up in the same conversation, the LLM may still see X. Mitigate by keeping conversations short (so each command is a fresh conversation that re-renders the prompt) and putting the speaker block at the end of the prompt.
Use this if you rely on local intents (most "turn on/off/set" commands) and only occasionally need the speaker for LLM questions.
Murdock injects the recognition context directly into the transcript it returns, using two templates (known / unknown speaker). The agent therefore sees it on every single utterance — no MQTT, no caching staleness.
Example known-speaker template:
{{ transcript }}
------
The speaker was recognised as "{{ speaker }}" (role {{ role }}),
confidence {{ confidence }}%.
Example unknown-speaker template:
{{ transcript }}
------
Speaker not recognised. Closest match: "{{ nearest }}" at {{ confidence }}%.
Placeholders (plain substitution, not Jinja2 — the known/unknown
split is the template choice): {{ transcript }} (alias {{ tts }}),
{{ speaker }}, {{ role }}, {{ confidence }} (percent),
{{ distance }}, {{ nearest }}, {{ satellite }}. Unknown
placeholders render empty.
- ✅ The speaker is always correct and current, every turn.
⚠️ The command text is no longer clean, so HA's local intent matching no longer matches — every command goes to the LLM. Use this only when an LLM handles all commands.
MQTT is not switched off in this mode. The delivery mode only controls how the conversation agent gets the speaker. As long as MQTT is enabled, the
sensor.murdock_*entities keep publishing in both modes — your dashboards and automations work either way; transcript mode simply doesn't depend on them for the LLM hand-off.
Put the interpretation rule in your (static, cache-friendly) system
prompt, e.g. "Lines after ------ are recognition metadata from the
system, not spoken by the user."
The correction dictionary's ~> entries produce
markers like
Bad-Lightstrip [oder: Bed-Lightstrip]. Useful to an LLM, poison to the
intent matcher — so where they go is its own setting, independent of
the speaker delivery mode above:
| Mode | Transcript | Hints |
|---|---|---|
inline (default) |
carries [oder: …]
|
in the text |
sidecar |
stays clean | in the recognition event → integration prompt line |
clean |
stays clean | dropped |
auto |
depends | sidecar when MQTT or the HA REST push is wired, else inline |
sidecar is the combination that used to be impossible: the agent gets
the second reading and local intent matching keeps working, because the
command text is untouched.
Decide before marking. Before any hint is passed on it is checked against the mirrored vocabulary. If exactly one of the two readings is a real entity name, that reading simply wins — "Bad-Lightstrip [oder: Bed-Lightstrip]" becomes "Bed-Lightstrip" if that's the entity you own, with no hint emitted. Only genuine ambiguity (both readings are entities, or neither is) reaches the agent. Fewer, better hints.
| Your setup | Mode |
|---|---|
| Can install a custom component | HA integration — LLM API, per turn, transcript stays clean |
| Local intents for commands, LLM for questions | MQTT / system prompt |
| LLM handles everything, want the speaker always fresh | Transcript augmentation |
The third delivery mode the dropdown was left room for now exists: the
integration's LLM API is the intent-safe side channel. It works
because HA rebuilds an LLM API's prompt contribution per request, unlike
the per-conversation extra_system_prompt. The dropdown setting still
applies to the transcript itself, so you'd typically pair the integration
with mode 1 (none / MQTT) and leave the transcript untouched.