From f1452f311958c7e750706ac25b24768ae1cc284f Mon Sep 17 00:00:00 2001 From: Melvin Strobl Date: Tue, 20 Oct 2026 08:34:07 +0200 Subject: [PATCH] wren sees every area and reads the command plain Shown only the wired areas, the model took "turn on the bathroom light" for the living room's light and "the office light" for the kitchen's, both above 0.93: 5 of the 28 recorded 7B commands switched a wrong light. Every area is shown now, with a light each, so such a call names an unwired device and goes on to `unhandled`. With every area shown, a transcript's "Turn on the kitchen light." fell to 0.80. The command is passed lower case without sentence marks, as the model was trained, which brings it to 0.99. --- backend/fluksio/flow/nodes/wren.py | 17 +++++++++++++---- backend/tests/flow/test_wren.py | 9 ++++++++- docs/reference/node-types.md | 6 ++++-- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/backend/fluksio/flow/nodes/wren.py b/backend/fluksio/flow/nodes/wren.py index 7b4a60a..f63e8d8 100644 --- a/backend/fluksio/flow/nodes/wren.py +++ b/backend/fluksio/flow/nodes/wren.py @@ -9,6 +9,7 @@ allowlist: the model can reach nothing that is not wired to the node. from __future__ import annotations +import re import time from collections.abc import Iterable from typing import Any @@ -120,7 +121,8 @@ _SAID = { #: What every area held in training: a light, and in the living room, which #: always existed, media and heating besides. A house missing them reads as #: unfamiliar and the confidence drops with it, so they are filled in; a call -#: on one that nothing is wired to is reported and not applied. +#: on one that nothing is wired to is reported and not applied. Every area is +#: shown, wired or not, or "the bathroom light" lands on a light that is. _TRAINED = {"living room": ("light", "media", "heat")} @@ -186,6 +188,15 @@ def _said(call: Any, area: str) -> str: return f"{area} {said}" +def _plain(command: str) -> str: + """The command as the model was trained on it: lower case, no sentence marks. + + A transcript's "Turn on the kitchen light." reads less sure than the same + words plain, 0.80 against 0.99 with every area shown. + """ + return re.sub(r"[.,!?;:]+(?=\s|$)", "", command).strip().lower() + + def _text(call: Any) -> str: """The call as the chat shows it, less the device it names.""" args = ", ".join(f"{k}={v}" for k, v in call.args.items() if k != "id") @@ -311,8 +322,6 @@ class WrenNode(Node): areas: dict[str, list[Any]] = {} for area in wren.AREAS: wired = {kind for (a, kind) in self.devices if a == area} - if not wired and area not in _TRAINED: - continue kinds = wired | set(_TRAINED.get(area, ("light",))) areas[area] = [] for kind in (k for k in wren.KINDS if k in kinds): @@ -354,7 +363,7 @@ class WrenNode(Node): # small. Scoped to the call, so numpy elsewhere in the process keeps # its threads. with threadpool_limits(1): - calls, confidence = wren.act(prompt, house) + calls, confidence = wren.act(_plain(prompt), house) confidence = round(float(confidence), 3) write = self.cfg.apply and confidence >= self.cfg.min_confidence writes: dict[str, Any] = {} diff --git a/backend/tests/flow/test_wren.py b/backend/tests/flow/test_wren.py index 87a98e0..a96fde7 100644 --- a/backend/tests/flow/test_wren.py +++ b/backend/tests/flow/test_wren.py @@ -11,7 +11,7 @@ import pytest from fluksio.flow.messages import DType, MessageSpec from fluksio.flow.nodes.base import Node -from fluksio.flow.nodes.wren import CANT, WrenNode, _value +from fluksio.flow.nodes.wren import CANT, WrenNode, _plain, _value from fluksio.flow.pipeline import Pipeline @@ -72,6 +72,11 @@ def test_only_the_prompt_wakes_it(fake_wren): } +def test_the_model_reads_the_command_plain(): + assert _plain("Turn on the kitchen light.") == "turn on the kitchen light" + assert _plain("Heat it to 22.5, please!") == "heat it to 22.5 please" + + @pytest.mark.parametrize( ("tool", "args", "dtype", "value"), [ @@ -114,6 +119,8 @@ def test_applied_calls_reach_their_targets(fake_wren): ("house.living_room_setpoint", True), ("", False), ] + # Areas nothing is wired to are shown too, so a call can name them. + assert set(fake_wren.houses[0].areas) == set(fake_wren.AREAS) living = fake_wren.houses[0].areas["living room"] assert [(d.kind, d.state) for d in living] == [ ("light", "off"), diff --git a/docs/reference/node-types.md b/docs/reference/node-types.md index a48c3bc..ed870e6 100644 --- a/docs/reference/node-types.md +++ b/docs/reference/node-types.md @@ -406,8 +406,10 @@ 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 Redis, and not otherwise. -The node adds default devices to the model's view for missing expected devices. -They are reported but cannot receive writes unless wired. +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`. The command is read +in lower case without sentence marks, as the model was trained. !!! note "What the current model cannot do"