diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php index f303a56..be82628 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -136,7 +136,8 @@ if (!$reply) { // ── Tier 0: Home Assistant Control ─────────────────────────────────────── // Uses entity_map stored by facts_collector to resolve natural language → entity $haEntityMapRow = JarvisDB::query( - 'SELECT fact_value FROM kb_facts WHERE category=? AND fact_key=? LIMIT 1', + 'SELECT fact_value FROM kb_facts WHERE category=? AND fact_key=? + AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY updated_at DESC LIMIT 1', ['ha', 'entity_map'] ); $haEntityMap = ($haEntityMapRow && !empty($haEntityMapRow[0]['fact_value'])) @@ -213,22 +214,45 @@ if (!$reply && preg_match('/(turn|switch|put|set)\s+(on|off)/i', $message, $acti } if (!$bestEid) { - // Build search terms from message (remove control words with word boundaries) - $searchMsg = preg_replace('/\b(turn|switch|put|set|the|my|all|please|jarvis|on|off|lights?|lamps?)\b/i', ' ', $msgLower); + // Detect preferred domain from message + $preferLight = (bool) preg_match('/\blight(s)?\b/i', $message); + $preferSwitch = (bool) preg_match('/\b(switch|plug|outlet|strip)\b/i', $message); + + // Strip control words — keep device/room name + $searchMsg = preg_replace('/\b(turn|switch|put|set|the|my|all|please|jarvis|on|off|lights?|lamps?|plugs?|strips?)\b/i', ' ', $msgLower); $searchMsg = trim(preg_replace('/\s+/', ' ', $searchMsg)); foreach ($haEntityMap as $eid => $info) { + if (($info['state'] ?? '') === 'unavailable') continue; $nameLower = strtolower($info['name']); - // Score: exact substring match = 10, word overlap = words matched - if ($searchMsg && strpos($nameLower, $searchMsg) !== false) { - $score = 10; + $domain = $info['domain'] ?? ''; + $score = 0; + + // Exact name match = highest priority + if ($nameLower === $searchMsg) { + $score = 100; + // Search is substring of name + } elseif ($searchMsg && strpos($nameLower, $searchMsg) !== false) { + $score = 40; + // Name is substring of search (e.g. search="living room light", name="Living Room") + } elseif ($searchMsg && strpos($searchMsg, $nameLower) !== false) { + $score = 30; } else { + // Word overlap scoring $words = array_filter(explode(' ', $searchMsg)); - $score = 0; foreach ($words as $w) { - if (strlen($w) > 2 && strpos($nameLower, $w) !== false) $score++; + if (strlen($w) > 2 && strpos($nameLower, $w) !== false) $score += 8; } } + + if ($score <= 0) continue; + + // Domain preference bonus + if ($preferLight && $domain === 'light') $score += 20; + if ($preferSwitch && $domain === 'switch') $score += 20; + // Penalty for clearly wrong domain when user specified one + if ($preferLight && in_array($domain, ['media_player','sensor','climate'])) $score -= 15; + if ($score > $bestScore) { $bestScore = $score; $bestEid = $eid;