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"