Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 56 additions & 5 deletions crates/genie-core/src/tools/quick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2474,15 +2474,26 @@ fn home_status_target(text: &str) -> Option<String> {
return Some("speed limit".into());
}

if text.contains("water pressure") {
// The bare-appliance branches below key on an appliance noun phrase alone
// ("water pressure", "sump pump", ...). Unlike the multi-word household-
// scenario phrases above, a *command* can plausibly contain the noun —
// "remind me to check the water pressure", "remind me to buy a baby
// monitor" — and keying on the noun alone hijacked those writes into a
// status *read*: the router answered with a device report instead of
// abstaining so the LLM can set the reminder. Require a status-question
// shape, exactly as the iron block below already does: the standard gate,
// plus the "did ..." form the gate prefixes do not cover.
let status_shaped = looks_like_status_query(text) || text.starts_with("did ");

if status_shaped && text.contains("water pressure") {
return Some("water pressure".into());
}

if text.contains("sump pump") {
if status_shaped && text.contains("sump pump") {
return Some("sump pump".into());
}

if text.contains("sous vide") {
if status_shaped && text.contains("sous vide") {
Comment on lines +2486 to +2496

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Tighten status_shaped to require a live device-state question.

looks_like_status_query matches any " status" substring, and starts_with("did ") accepts history/ownership questions. For example, "Remind me to check the water pressure status" and "Did I buy a baby monitor?" can still return home_status targets instead of reaching reminder or memory handling. Reject reminder/history prefixes or use a stricter live-status predicate.

Also applies to: 2518-2519

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/genie-core/src/tools/quick.rs` around lines 2486 - 2496, The
status_shaped predicate in the target-selection logic should only accept live
device-state questions, not reminder or historical/ownership queries. Tighten
the logic around looks_like_status_query and starts_with("did ") to reject
reminder/history prefixes while preserving valid live-status handling for water
pressure, sump pump, and sous vide targets.

return Some("sous vide".into());
}

Expand All @@ -2504,7 +2515,7 @@ fn home_status_target(text: &str) -> Option<String> {
return Some("printer ink".into());
}

if text.contains("baby monitor") {
if status_shaped && text.contains("baby monitor") {
return Some("baby monitor".into());
}

Expand Down Expand Up @@ -2543,7 +2554,12 @@ fn home_status_target(text: &str) -> Option<String> {
return Some("iron".into());
}

if text.contains("water") && text.contains("hot") {
// "how hot is the water" is a genuine water-heater reading whose "how "
// prefix none of the gate shapes cover, so admit it alongside them here.
if (status_shaped || text.starts_with("how hot "))
&& text.contains("water")
&& text.contains("hot")
{
Comment on lines +2557 to +2562

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Narrow the how hot exception to water-temperature questions.

text.starts_with("how hot ") also matches advice requests such as "How hot should I make the water for tea?", routing them to water heater. Require the intended interrogative form (for example, "how hot is …") rather than the broad prefix, and add a negative regression.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/genie-core/src/tools/quick.rs` around lines 2557 - 2562, In the
water-heater routing condition, narrow the text-based exception in the
status-shaped check from the broad “how hot ” prefix to the intended
interrogative form such as “how hot is ”, while preserving the existing water
and hot checks. Add a negative regression covering advice questions like “How
hot should I make the water for tea?” to ensure they are not routed to water
heater.

return Some("water heater".into());
}

Expand Down Expand Up @@ -5550,6 +5566,41 @@ mod tests {
assert_eq!(call.arguments["entity"], "water pressure");
}

#[test]
fn bare_appliance_status_needs_a_status_shaped_question() {
// Commands that merely *contain* the appliance noun must abstain so the
// LLM can ground the write; on `main` each emitted a home_status read
// ("remind me to test the sump pump" answered with the sump-pump
// report instead of setting a reminder).
for utterance in [
"Remind me to check the water pressure",
"Remind me to test the sump pump",
"Remind me to buy a baby monitor",
"Remind me to buy a sous vide",
"How do I cook sous vide?",
"Remind me to buy hot water bottles",
] {
assert!(
route(utterance).is_none(),
"{utterance:?} is a command, not a status question — it must abstain"
);
}

// Genuine status-shaped questions still resolve to the same entities.
for (utterance, entity) in [
("Check the water pressure", "water pressure"),
("Is the sump pump running?", "sump pump"),
("Is the sous vide on?", "sous vide"),
("Is the baby monitor on?", "baby monitor"),
("Is the water hot?", "water heater"),
("How hot is the water?", "water heater"),
] {
let call = route(utterance).unwrap_or_else(|| panic!("no route for {utterance:?}"));
assert_eq!(call.name, "home_status", "{utterance:?}");
assert_eq!(call.arguments["entity"], entity, "{utterance:?}");
}
}

#[test]
fn ice_status_matches_whole_words_not_substrings() {
// "ice"/"icy" were matched as substrings, so "pr[ice]" and "sp[icy]"
Expand Down
Loading