From 1bf9e4fab7df6e22b24cb41700fbc8f154db59f8 Mon Sep 17 00:00:00 2001 From: galuis116 Date: Thu, 30 Jul 2026 17:11:15 -0400 Subject: [PATCH 1/2] fix(quick-router): accept a leading please on home_control commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every home_control matcher is anchored at token 0 — the curated exact-match device set, the starts_with arms, the `set `/`preheat ` setpoint parser, and simple_turn_request's "turn on "/"turn off " prefixes. A single leading politeness token shifted that anchor, so the most common polite actuation forms fell through to the LLM even though their bare counterparts already route: "please turn off the kitchen lights" -> ABSTAIN "please set the thermostat to 68" -> ABSTAIN "please preheat the oven to 400" -> ABSTAIN "please start the dishwasher" -> ABSTAIN Strip a single leading "please " at the two home-control entry points, mirroring scene_or_routine_activation_request and play_media_request, which already strip it for the same reason. A trailing "please" was already dropped by clean_control_entity. The strip happens before the guards rather than inside each arm, so the "in " schedule guard, the multi-clause/except/only/and abstention, and the known-device gate all still apply to the stripped text — "please turn on the lights in 5 minutes" keeps abstaining rather than actuating now. priority_home_control_request gets the same strip: its arms are mostly contains tests that survive a leading token, but "run movie night" and "warm up the car" are exact matches that do not. The scene test's negative assertion for "please turn on the lights" is updated: it was asserting the absence of a route, and its point — that a device command is not claimed as a scene activation — now holds by the call landing on home_control instead. Closes #919 --- crates/genie-core/src/tools/quick.rs | 84 +++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/crates/genie-core/src/tools/quick.rs b/crates/genie-core/src/tools/quick.rs index c639e45f..5edc5841 100644 --- a/crates/genie-core/src/tools/quick.rs +++ b/crates/genie-core/src/tools/quick.rs @@ -1402,6 +1402,13 @@ fn preferred_timer_request(text: &str) -> Option<(u64, String)> { } fn priority_home_control_request(text: &str) -> Option<(String, &'static str, Option)> { + // A leading "please" is politeness, not part of the command. Most arms here + // are `contains` tests and survive it, but the two exact-match arms + // ("run movie night", "warm up the car") are anchored at token 0 and do not. + // Strip it so the polite form of those two routes like the bare form, and so + // this entry point agrees with home_control_request below. + let text = text.strip_prefix("please ").unwrap_or(text); + if text.contains("after dinner cleanup") || text.contains("after-dinner cleanup") { return Some(("after-dinner cleanup".into(), "activate", None)); } @@ -1894,6 +1901,19 @@ fn priority_home_control_request(text: &str) -> Option<(String, &'static str, Op } fn home_control_request(text: &str) -> Option<(String, &'static str, Option)> { + // A leading "please" is politeness, not part of the command. Every matcher + // below — the curated exact-match device set, the `starts_with` arms, the + // `set `/`preheat ` setpoint parser, and `simple_turn_request` — is anchored + // at token 0, so one politeness token shifted the anchor and the single most + // common polite actuation form ("please turn off the kitchen lights") fell + // through to the LLM even though its bare counterpart already routes. + // Mirrors scene_or_routine_activation_request and play_media_request, which + // strip it for the same reason; a trailing "please" is already dropped by + // clean_control_entity. Stripping here (rather than inside each arm) also + // keeps the schedule/multi-clause/known-device guards applying to the + // stripped text, so "please turn on the lights in 5 minutes" still abstains. + let text = text.strip_prefix("please ").unwrap_or(text); + if matches!(text, "turn off the tv" | "turn off tv") { return Some(("tv".into(), "turn_off", None)); } @@ -4760,8 +4780,12 @@ mod tests { assert!(route("please help me").is_none()); // And a home_control-shaped polite request must not be claimed as a // scene activation either (no "activate"/"start"/"run" … "scene"/ - // "routine" shape). - assert!(route("please turn on the lights").is_none()); + // "routine" shape). It falls past this matcher to the home_control path, + // which strips the same leading "please" and actuates the device. + let call = route("please turn on the lights").expect("no route"); + assert_eq!(call.name, "home_control"); + assert_eq!(call.arguments["entity"], "lights"); + assert_eq!(call.arguments["action"], "turn_on"); } #[test] @@ -6256,6 +6280,62 @@ mod tests { assert_eq!(call.arguments["action"], "turn_on"); } + #[test] + fn home_control_accepts_a_leading_please() { + // Every home_control matcher is anchored at token 0 — the curated + // exact-match set, the `starts_with` arms, the `set `/`preheat ` setpoint + // parser, and simple_turn_request's "turn on "/"turn off " prefixes. One + // leading politeness token shifted that anchor, so the most common polite + // actuation forms fell through to the LLM even though their bare + // counterparts already route. Same fix as scene/routine and play_media. + for (utterance, entity, action) in [ + ( + "please turn off the kitchen lights", + "kitchen lights", + "turn_off", + ), + ("please turn off the lights", "lights", "turn_off"), + ("please turn on the bedroom lamp", "bedroom lamp", "turn_on"), + ("please turn on the fan", "fan", "turn_on"), + ("please turn off the tv", "tv", "turn_off"), + ( + "please start the dishwasher", + "dishwasher normal cycle", + "activate", + ), + ] { + let call = route(utterance).unwrap_or_else(|| panic!("no route for {utterance:?}")); + assert_eq!(call.name, "home_control", "{utterance:?}"); + assert_eq!(call.arguments["entity"], entity, "{utterance:?}"); + assert_eq!(call.arguments["action"], action, "{utterance:?}"); + } + + // Setpoints route through the same anchor and carry their value. + for (utterance, entity, value) in [ + ("please set the thermostat to 68", "thermostat", 68.0), + ("please preheat the oven to 400", "oven", 400.0), + ] { + let call = route(utterance).unwrap_or_else(|| panic!("no route for {utterance:?}")); + assert_eq!(call.name, "home_control", "{utterance:?}"); + assert_eq!(call.arguments["entity"], entity, "{utterance:?}"); + assert_eq!(call.arguments["action"], "set_temperature", "{utterance:?}"); + assert_eq!(call.arguments["value"], value, "{utterance:?}"); + } + + // Politeness is stripped *before* the guards, not instead of them: the + // schedule, multi-clause, and unknown-device abstentions must all still + // fire on the polite form, otherwise stripping "please" would trade one + // wrong answer for another. + for utterance in [ + "please turn on the lights in 5 minutes", + "please turn off everything downstairs except the kitchen lights", + "please turn on the porch light and the garage light", + "please turn on the infant monitor", + ] { + assert!(route(utterance).is_none(), "{utterance:?}"); + } + } + #[test] fn turn_command_matches_fan_as_a_whole_word_not_a_substring() { // The fan/fireplace gate used `entity.contains("fan")`, so a device whose From 3557368db19720e1cd2bf4aedd6f06267cc938f6 Mon Sep 17 00:00:00 2001 From: galuis116 Date: Fri, 31 Jul 2026 06:11:31 -0400 Subject: [PATCH 2/2] test(quick-router): pin the priority home-control entry point MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the review on #922. The existing tests exercise home_control_request through route(), but nothing proved priority_home_control_request handles a leading "please". That is the second entry point and it runs earlier in dispatch, so it needs its own coverage rather than inheriting confidence from the other. "please run movie night" is the case that matters: almost every arm in that helper is a `contains` test that survives a leading token, and this exact-match arm is the reason the strip had to be added there at all. The test also asserts the polite call equals the bare call by name and arguments, not merely that some route exists. "warm up the car" is the other exact-match arm and is deliberately not tested: it returns None on main as well, because its remote_start action does not survive canonicalize_household_action, so a leading "please" changes nothing there. Noted in the test comment so the omission reads as a decision rather than an oversight. Tests only — no behavior change. --- crates/genie-core/src/tools/quick.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/genie-core/src/tools/quick.rs b/crates/genie-core/src/tools/quick.rs index 5edc5841..f07c7360 100644 --- a/crates/genie-core/src/tools/quick.rs +++ b/crates/genie-core/src/tools/quick.rs @@ -6336,6 +6336,27 @@ mod tests { } } + #[test] + fn priority_home_control_accepts_a_leading_please() { + // priority_home_control_request is the second entry point, and it runs + // *before* the scene matcher, so it needs its own coverage. Most of its + // arms are `contains` tests that survive a leading token, but these two + // are exact matches that do not — they are the only reason the strip is + // needed there, so they are what has to be pinned. "warm up the car" is + // the other such arm but is unreachable on main too (its action does not + // survive canonicalize_household_action), so there is nothing for a + // leading "please" to change there. + let call = route("please run movie night").expect("no route"); + assert_eq!(call.name, "home_control"); + assert_eq!(call.arguments["entity"], "movie night"); + assert_eq!(call.arguments["action"], "activate"); + + // The polite form matches the bare form exactly, arguments included. + let bare = route("run movie night").expect("no route"); + assert_eq!(bare.name, call.name); + assert_eq!(bare.arguments, call.arguments); + } + #[test] fn turn_command_matches_fan_as_a_whole_word_not_a_substring() { // The fan/fireplace gate used `entity.contains("fan")`, so a device whose