wren node: targets from the wiring, on wren 2

Every output is a target and every input besides prompt a reading. A
targets setting gives descriptions, values, ranges and per-target
guards, model takes a trained pack's artifact digest, min_confidence
defaults to the model's threshold. A command is applied whole or not at
all, and targets the model was not trained on read degraded. The home
path (area/kind ports, groups, brightness, temperature answers) is gone.
This commit is contained in:
2026-10-24 21:28:42 +02:00
parent 6892651e76
commit 07fa5a2dc2
5 changed files with 745 additions and 863 deletions
+72 -88
View File
@@ -340,113 +340,97 @@ use a [batch flow](../concepts/runs.md) and a Function node.
### Wren
**`wren`** — turn a typed command into values for the devices wired to it.
**`wren`** — turn a typed command into values for the targets wired to it.
[Wren](https://github.com/stroblme/wren) is an optional local language model
for device commands. Install it in the engine's environment with
`pip install git+https://github.com/stroblme/wren@eae4d6ff9ded68e36624972fd3f4a8f8d210b0c2`.
The `wren` package on PyPI is an unrelated one. Without the model the node
reports a missing dependency.
that turns short commands into calls on a fixed list of targets. Install it in
the engine's environment with
`pip install git+https://github.com/stroblme/wren`. The `wren` package on PyPI
is an unrelated one. Without the model the node reports a missing dependency.
[Wren's own docs](https://stroblme.github.io/wren/) explain how to describe
targets, train a model for them, and what a model can and cannot do.
| Input | Type | Notes |
|---|---|---|
| `prompt` | `str` | the command, and the only input that runs the node |
| `presence` | `str` | the area the user is in; anything else reads as away |
| `outdoor` | `float` or `int` | °C; 20 without it, clamped to −5…30 |
| `dark` | `bool` | without it, dark before 07:00 and from 18:00 |
| `<area>_temperature` | `float` or `int` | °C, read when the command asks how warm the area is |
| any other | `bool`, `int`, `float` or `str` | a **reading**: a target a command may ask about but not set |
The outputs are `actions`, a list of records, `reply`, `unhandled`,
`brightness`, and any number of **targets**. A
target is an output named `<area>_<kind>` and bound to the message its device
listens on: `living_room_setpoint` bound to `home.setpoint` is the living
room's heating. The wiring is the allowlist, so the model can write nothing
that is not wired to the node.
| Output | Type | Notes |
|---|---|---|
| `actions` | `list` | one record per call |
| `reply` | `str` | what was set or read, as "Kitchen light on." |
| `unhandled` | `str` | the prompt, when the node did nothing with it |
| any other | `bool`, `int`, `float` or `str` | a **target** a command may set |
- **area**: `living_room`, `kitchen`, `bathroom`, `bedroom`, `office`,
`garden`, `patio`, `garage`, `driveway`, `balcony`
- **kind**: `light`, `lamp`, `brightness` for a light; `setpoint`, `heating`,
`heat`, `boost` for heating; `media`, `music`, `tv`; `shutter`, `blind`,
`cover`; `water`, `watering`, `sprinkler`; `switch`, `plug`, `socket`,
`appliances` for a switch, which only wren's s10 model knows
A port naming anything else, or a kind the installed model lacks, is refused
when the flow is built. A port is
named after its message unless it is given a name of its own, in the node
panel's port name field or as `{"name": "setpoint", "port":
"living_room_setpoint"}` through the flow API.
Targets of one area and kind are one device to the model, and each takes what
a call means for its type. A target's current value is the state the model
reads.
| Call | `bool` | number | `str` |
|---|---|---|---|
| `lights.on` / `off` | `true` / `false` | `100` / `0` | `on` / `off` |
| `media.play` / `stop` | `true` / `false` | — | `play` / `stop` |
| `heating.set` | — | the temperature | — |
| `heating.boost` / `off` | `true` / `false` | — | `boost` / `off` |
| `shutters.up` / `down` | `true` / `false` | `100` / `0` | `up` / `down` |
| `shutters.stop` | — | — | `stop` |
| `watering.start` / `stop` | `true` / `false` | — | `on` / `off` |
| `switches.on` / `off` | `true` / `false` | `1` / `0` | `on` / `off` |
| `lights.set` / `dim` / `brighten` | — | the level, on `brightness` only | — |
A target is named by its port and bound to the message it stands for:
`living_room_heating` bound to `home.setpoint` is the living room's heating.
Its name, type and current value are what the model is shown, so the wiring
is the allowlist and the model can reach nothing that is not wired to the
node. A target nothing has set yet is shown as off, as 0 within its range, or
as its first value. A port is named after its message unless it is given a
name of its own, in the node panel's port name field or as
`{"name": "setpoint", "port": "living_room_heating"}` through the flow API.
| Setting | Default | Notes |
|---|---|---|
| `apply` | `false` | write to the targets; off, the node only reports |
| `min_confidence` | `0.93` | below this nothing is written. wren's s10 model is less sure of itself, use `0.8` with it |
| `aliases` | empty | the house's own words, or words the recogniser hears wrong, in the model's: `{"traverse": "living room light", "page too": "patio"}` |
| `model` | empty | the trained model, an artifact digest; empty runs wren's demo model |
| `min_confidence` | empty | the confidence a call needs; empty takes the model's own threshold |
| `targets` | empty | by target port, what the model is told of it and how far it is trusted (below) |
| `aliases` | empty | your own words, or words the recogniser hears wrong, in the model's: `{"traverse": "living room light", "page too": "patio"}` |
Each record in `actions` contains `message`, `value`, `applied`, `delay_s`,
`call`, and `confidence`. Unwired calls have an empty `message` and are not
applied. Keep `apply=false` while checking how the model interprets commands.
Every field of a `targets` entry is optional:
`reply` is one sentence saying what was applied, such as "Kitchen light on."
or "Garage light on in 5 minutes.". When nothing was applied, `unhandled`
carries the prompt instead, for something else to answer, such as an
[Assistant](#assistant). When no node reads `unhandled`, `reply` says "Sorry,
I can't do that yet." instead, so a spoken command never goes unanswered.
| Field | Notes |
|---|---|
| `description` | more words for the target, which help pick it: `"awning over the patio"` |
| `values` | the only values a `str` target takes: `["UP", "DOWN", "STOP"]` |
| `range` | the lowest and highest value of a number: `[5, 30]` |
| `unit` | said after a number in the reply, such as `"°C"`; the model never sees it |
| `min_confidence` | this target's own bar, used over the node's, such as a higher one for a lock |
| `pass_on` | wren never sets it, and a command on it goes on to `unhandled` |
`brightness` is the house's, a `float` or `int` from 0 to 100. No wren model
makes brightness calls yet. For one that does, the node writes them to
`brightness` whatever area they name: "set the brightness in the kitchen to 30%" sets the
house to 30. "Dim" and "brighter" move it 30 from its current value (70
when nothing has set it), and a dim stops at 10. The reply is "Brightness at
40 percent.". Without the output these calls go on to `unhandled`.
A target that is not a `bool`, `int`, `float` or `str`, a `targets` entry
naming no target, `values` on a target that is not a `str` and a `range` on
one that is not a number are refused when the flow is built.
A call with a delay ("in 10m") is held by the engine's work queue, like the
Debounce node's second value: it survives a restart where the queue is in
A command is applied whole or not at all. Its calls are written only when
`apply` is on, every call reaches its target's bar, and none is on a
`pass_on` target or on one the model was not trained on. Applying part of a
command would leave half of a duration ("off for 7 minutes" is off now and on
again later), or let the assistant do again what was already done. Otherwise
nothing is written and `unhandled` carries the prompt, for something else to
answer, such as an [Assistant](#assistant). When no node reads `unhandled`,
`reply` says "Sorry, I can't do that yet." instead, so a spoken command never
goes unanswered.
`reply` says what was set, as "Kitchen light on, living room heating to 21 °C
in 5 minutes.", and answers a read from the target's current value, as
"Outdoor temperature is 6.2 °C.", with `apply` off too, since a read writes
nothing. A read of a target nothing has written yet goes on to `unhandled`. Each record in `actions` contains `message`, `value`,
`applied`, `delay_s`, `call` as wren writes it, such as
`set(kitchen_light, true)`, and that call's `confidence`. Keep `apply=false`
while checking how the model reads commands.
A call with a delay ("in 10 minutes") is held by the engine's work queue, like
the Debounce node's second value: it survives a restart where the queue is in
Redis, and not otherwise.
The model sees every area, wired or not, each with at least a light, so "the
bathroom light" is not taken for a light that is wired elsewhere. Such a call
is reported but not applied, and goes on to `unhandled`. So is a call on an
area the command does not name, unless it is the user's area or, with no area
named, the living room: "switch on the appliances" is a word the pinned model
never learned, and it lands on whichever light it was shown. The command is read
in lower case without sentence marks and with number words as digits ("in
five minutes" as "in 5 minutes"), as the model was trained. `aliases` are
applied before, whole words in any case, and `unhandled` carries the prompt
with them.
A model knows the targets it was trained on: their names, types, `values`,
`range`, descriptions and which are readings. A target wired since, or
described otherwise since, is one it never learnt. The node then reads
degraded, naming such targets and asking for the model to be retrained, and a
call on one is never applied. wren's demo model knows the 20 targets of its
[demo home](https://github.com/stroblme/wren/blob/main/wren/home.json), so
it runs without that warning only on ports named and described as there. A
`model` the artifact store does not have or cannot read also reads degraded,
and every command goes on to `unhandled`. A model trained with wren goes into
the store with `PUT /artifacts` (see the [API](../code/api.md#artifacts)).
A command for all of a kind, such as "turn off all lights", "switch every
light on in 5 minutes" or "everything off in the office", is not the
model's. The node applies it to every wired target of that kind, within the
areas it names, and replies "All lights off." or "Lights off in the
office.". A question ("are all lights off") or an exception ("all but the
kitchen") goes to the model as any other command.
A question for an area's temperature ("how warm is it in the kitchen") is
answered from its `<area>_temperature` input, or from `outdoor` for an area
outside, as "It's 8 degrees outside.". Without a reading it goes on to
`unhandled`.
!!! note "What the current model cannot do"
Commands are English, such as "turn on the lights in the kitchen".
The model uses the areas above, one device per kind and area, on/off
actions, and whole-minute delays. It does not dim lights or set colors.
The command is read in lower case without sentence marks and with number words
as digits ("in five minutes" as "in 5 minutes"), as the model was trained.
`aliases` are applied before, whole words in any case, and `unhandled` carries
the prompt with them.
The chat's [Control tab](../code/agents.md#control) sends commands by hand.