wren leaves unnamed areas to the assistant; assistant targets take a closed set of values

wren drops a call on an area the command does not name (other than the
user's or, with none named, the living room): "switch on the appliances"
turned on the bathroom light at 0.97. The assistant's `values` setting gives
a str target its only values, explains them in the tool and confirms in the
word set for each ("Canopy open.").
This commit is contained in:
2026-10-22 10:56:20 +02:00
parent f1452f3119
commit bf63b3d6d4
5 changed files with 118 additions and 22 deletions
+36
View File
@@ -138,3 +138,39 @@ def test_no_provider_or_one_that_does_not_answer_is_said(
_ask(node, "what is the weather tomorrow")
assert said.pop() == {"reply": UNAVAILABLE}
assert node._health == "degraded"
def test_a_target_with_values_takes_only_those_and_is_confirmed_in_words(
monkeypatch: pytest.MonkeyPatch,
):
seen: list[dict[str, Any]] = []
log: list[Any] = []
async def completion(**kwargs: Any) -> AsyncIterator[str | dict[str, Any]]:
seen.append(kwargs)
calls = [_call("canopy", "OPEN"), _call("canopy", "UP")]
yield {"role": "assistant", "content": None, "tool_calls": calls}
monkeypatch.setattr(ai_config, "load", lambda: PROVIDER)
monkeypatch.setattr(client, "stream_completion", completion)
monkeypatch.setattr(AssistantNode, "emit", lambda self, values: log.append(values))
node = AssistantNode(
requires=[MessageSpec(name="unhandled", port="prompt", dtype=DType.STR)],
provides=[
MessageSpec(name="canopy_manual", port="canopy", dtype=DType.STR),
MessageSpec(name="reply", dtype=DType.STR),
],
params={"values": {"canopy": {"UP": "open", "DOWN": "closed"}}},
)
node.assign_flow("voice", "assistant")
Pipeline([node])
_ask(node, "open the canopy")
[tool] = seen[0]["tools"]
assert tool["function"]["parameters"]["properties"]["value"]["enum"] == [
"UP",
"DOWN",
]
assert '"UP" means canopy open' in tool["function"]["description"]
assert log == [{"canopy": "UP"}, {"reply": "Canopy open."}]
+12
View File
@@ -204,3 +204,15 @@ def test_a_reply_says_what_was_applied_and_the_rest_is_unhandled(fake_wren):
unhandled = MessageSpec(name="house.unhandled", port="unhandled", dtype=DType.STR)
Pipeline([node, Node(lambda params, unhandled: None, requires=[unhandled])])
assert node.execute({"house.command": asked}) == {"house.unhandled": asked}
def test_a_call_on_an_area_the_command_does_not_name_is_not_applied(fake_wren):
"""A word the model never learned lands on a neighbour it was shown."""
node = _node(("bathroom_light", DType.BOOL), apply=True)
fake_wren.answer = ([FakeCall("lights.on", {"id": "bathroom:light"})], 0.97)
out = node.execute({"house.command": "switch on the appliances"})
assert out is not None
assert "house.bathroom_light" not in out
assert [a["applied"] for a in out["house.actions"]] == [False]