4. Dialog
August 1, 2026 · View on GitHub
Spec coverage. This chapter is the reference for the Dialog renderer conformance role of OVOS-INTENT-1 §7 over the
.dialogformat of OVOS-INTENT-2 §4.2 (dialog.py), and for the.promptformat of OVOS-INTENT-2 §4.4 (prompt.py, at the end of the chapter).A dialog renderer MUST embed a conformant expander, verify slot-set consistency (§5.5), fill every
{name}from caller-supplied values, and MUST NOT emit an unfilled slot (§5.1).UnfilledSlotis that last MUST.
A .dialog file holds the phrases an assistant may speak for one response.
Rendering a dialog means: pick one phrase, expand its (a|b) / [x]
variety down to a single variant, and fill its {name} slots with values. This
chapter covers the two ways to do it. It implements OVOS-INTENT-2 §4.2.
A .dialog phrase is spoken output, not ASR input, so, unlike the
input-direction roles, it may contain mixed case and punctuation:
# weather.dialog
It is {temperature} degrees.
Right now it's {temperature} degrees out.
(Currently|At the moment) {temperature} degrees.
render(): the stateless function
render() takes an explicit list of phrases and returns one rendered sentence:
from ovos_spec_tools import render
phrases = res.load_dialog("weather", "en-US")
render(phrases, slots={"temperature": 21})
# 'It is 21 degrees.'
Slots are filled from the slots dict. The phrase and the (a|b) variant are
chosen at random. Pass rng= (anything with a choice method, e.g. a seeded
random.Random) for reproducible output. vocabularies= supplies any
<name> references.
Expansion runs before filling, with slots kept opaque, so a slot value can
never be mis-parsed as grammar. A value of "(a|b)" is filled in literally,
not expanded.
If the chosen phrase has a slot with no value, render() raises
UnfilledSlot, a half-filled phrase must never reach text-to-speech.
DialogRenderer: the stateful, multilingual renderer
render() is fine for one-off use. For a skill that speaks a response
repeatedly, DialogRenderer does better. It is built from a LocaleResources
and a dialog name:
from ovos_spec_tools import DialogRenderer
renderer = DialogRenderer(res, "weather")
renderer.render("en-US", {"temperature": 21})
renderer.render("pt-PT", {"temperature": 22}) # same renderer, another language
It adds three things over the bare function.
It is multilingual
The language is a parameter of render(), not of the constructor, one
renderer serves every language the dialog ships in.
It avoids repeating itself
DialogRenderer remembers the phrase it chose last time and avoids picking it
again, tracked per language, so a frequently-spoken response does not
sound mechanical. With two phrases, consecutive renders strictly alternate.
It carries default slots and an .entity fallback
A slot is resolved in a clear order of precedence:
- a value passed to this
render()call. - a default value, set once on the constructor and reused every call.
- a random value from the slot's
.entityvalue set. - otherwise,
UnfilledSlot.
renderer = DialogRenderer(res, "greeting", slots={"assistant": "OVOS"})
renderer.render("en-US")
Here {assistant} is filled from the default every time. A {weekday} slot
that the caller does not supply is filled from weekday.entity if the skill
ships one, useful for a slot whose value is a free pick from a known set
rather than a computed value.
Choosing between them
render() | DialogRenderer | |
|---|---|---|
| Input | an explicit phrase list | a LocaleResources + dialog name |
| Language | decided by the caller | per render() call |
| Repetition avoidance | no | yes, per language |
Default slots / .entity fallback | no | yes |
Use render() when you already hold the phrases and want one sentence. Use
DialogRenderer for a skill response spoken more than once.
Rendering .prompt files
A .prompt is the localized prompt a skill feeds to a language model. Unlike
a .dialog it is not a template. It is plain text, and the whole file,
verbatim, is one prompt.
The only special construct is {name} substitution, and it is
conservative: a prompt is free-form text full of code and JSON, so
rendering must never corrupt a brace the author did not write as a slot.
render_prompt() is the stateless function:
from ovos_spec_tools import render_prompt
render_prompt("You are {role}. Answer: {query}", {"role": "concise"})
# 'You are concise. Answer: {query}'
A {name} is replaced only when all three OVOS-INTENT-2 §4.4 conditions
hold: it is a well-formed name (§4.4 condition 1), the caller supplied a value
(condition 2), and it is not inside a fenced code block (marked with triple
backticks)
(condition 3). Everything else stays literal:
- an unfilled slot is left as
{name}(§4.4 "slots are optional", the deliberate opposite of.dialog, where every slot must be filled). - any other
{/}, such as{},{ }, or JSON like{"k": 1}, is untouched. - a
{name}inside a fenced code block is never substituted.
Known gap. OVOS-INTENT-2 §4.4 also requires author-only HTML comments
<!-- … -->to be stripped before a prompt reaches a model (a MUST).render_prompt()does not yet do this: a comment passes through verbatim. Do not rely on comment removal until it is implemented.
PromptRenderer is the resource-backed, multilingual form, built from a
LocaleResources, with the language given per call and optional default slots:
from ovos_spec_tools import PromptRenderer
renderer = PromptRenderer(res, "system", slots={"assistant": "OVOS"})
renderer.render("en-US", {"query": "what time is it"})
renderer.render("pt-PT", {"query": "que horas são"}) # same prompt, another language
It has no phrase selection, no repetition avoidance, and no .entity
fallback. A prompt is one whole-file body and its slots are optional.
Next
Language matching, the tag logic behind the loader's smart fallback.