From c636b41cbcae9c05eed872ad953513b7f41ebc3a Mon Sep 17 00:00:00 2001 From: legop3 Date: Sat, 6 Jun 2026 17:46:27 -0400 Subject: [PATCH] dont make it always get the actual thingy perfect --- .../tools/haEntityLookup.js | 39 +++++++++++++++++++ .../tools/haSetEntity.js | 13 ++++--- .../tools/haSetLightColor.js | 21 ++++------ 3 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 server/src/services/overseerControlService/tools/haEntityLookup.js diff --git a/server/src/services/overseerControlService/tools/haEntityLookup.js b/server/src/services/overseerControlService/tools/haEntityLookup.js new file mode 100644 index 00000000..e1d860ba --- /dev/null +++ b/server/src/services/overseerControlService/tools/haEntityLookup.js @@ -0,0 +1,39 @@ +function getConfiguredEntities(homeAssistantService) { + const entities = homeAssistantService.getState()?.entities; + return Array.isArray(entities) ? entities : []; +} + +function resolveConfiguredEntity(homeAssistantService, rawEntityId, toolId) { + const entityId = String(rawEntityId || '').trim(); + if (!entityId) throw new Error(`${toolId} requires args.entity_id`); + + const entities = getConfiguredEntities(homeAssistantService); + const exact = entities.find((entry) => String(entry?.id || '') === entityId); + if (exact) return { entity: exact, entityId: String(exact.id) }; + + // LLM tool calls sometimes drop the Home Assistant domain prefix and send + // shelf_rgb_bulb instead of light.shelf_rgb_bulb. Suffix matching keeps that + // forgiving behavior inside the configured-entity allowlist, so the model can + // recover from a naming slip without gaining access to arbitrary HA entities. + const suffixMatches = entities.filter((entry) => { + const configuredId = String(entry?.id || ''); + return configuredId.endsWith(`.${entityId}`); + }); + + if (suffixMatches.length === 1) { + return { entity: suffixMatches[0], entityId: String(suffixMatches[0].id) }; + } + + // Ambiguous suffixes are rejected because picking one would be riskier than + // asking the model/user to use the full entity id. The error names the suffix + // issue directly so future logs point at the real failure mode. + if (suffixMatches.length > 1) { + throw new Error(`${toolId} entity_id ambiguous; use the full Home Assistant entity id`); + } + + throw new Error(`${toolId} entity_id not configured`); +} + +module.exports = { + resolveConfiguredEntity, +}; diff --git a/server/src/services/overseerControlService/tools/haSetEntity.js b/server/src/services/overseerControlService/tools/haSetEntity.js index e9cdf7a9..449a73e6 100644 --- a/server/src/services/overseerControlService/tools/haSetEntity.js +++ b/server/src/services/overseerControlService/tools/haSetEntity.js @@ -1,3 +1,5 @@ +const { resolveConfiguredEntity } = require('./haEntityLookup'); + module.exports = { id: 'ha_set_entity', signature: 'ha_set_entity(entity_id, state)', @@ -23,18 +25,17 @@ module.exports = { return { available: true, reason: null }; }, async execute({ args = {}, homeAssistantService }) { - const entityId = String(args?.entity_id || args?.entityId || '').trim(); - if (!entityId) throw new Error('ha_set_entity requires args.entity_id'); - // Configured Home Assistant entities are the room-control surface the // overseer is allowed to use. Some physical room lights are exposed by Home // Assistant as switches because they are outlet-backed lamps, so this tool // intentionally permits every configured entity for on/off control instead // of limiting itself to HA's light domain. - const allowed = new Set( - (homeAssistantService.getState()?.entities || []).map((entry) => String(entry?.id || '')).filter(Boolean), + const { entityId } = resolveConfiguredEntity( + homeAssistantService, + args?.entity_id || args?.entityId, + 'ha_set_entity', ); - if (!allowed.has(entityId)) throw new Error('ha_set_entity entity_id not configured'); + const state = String(args?.state || '').toLowerCase(); if (state !== 'on' && state !== 'off') throw new Error('ha_set_entity requires args.state of on/off'); await homeAssistantService.setEntityState(entityId, state, { source: 'overseerControl' }); diff --git a/server/src/services/overseerControlService/tools/haSetLightColor.js b/server/src/services/overseerControlService/tools/haSetLightColor.js index 639b2975..7d15a3db 100644 --- a/server/src/services/overseerControlService/tools/haSetLightColor.js +++ b/server/src/services/overseerControlService/tools/haSetLightColor.js @@ -1,3 +1,5 @@ +const { resolveConfiguredEntity } = require('./haEntityLookup'); + function normalizeColorHex(value) { const raw = String(value || '').trim(); const withoutHash = raw.startsWith('#') ? raw.slice(1) : raw; @@ -24,15 +26,6 @@ function normalizeColorHex(value) { return `#${expanded.toLowerCase()}`; } -function findConfiguredEntity(homeAssistantService, entityId) { - const entities = homeAssistantService.getState()?.entities || []; - - // The overseer only gets to act on entities that the local config already - // exposes. This mirrors ha_set_entity and prevents a model-generated entity id - // from becoming an arbitrary Home Assistant service call. - return entities.find((entry) => String(entry?.id || '') === entityId) || null; -} - module.exports = { id: 'ha_set_light_color', signature: 'ha_set_light_color(entity_id, color_hex)', @@ -62,15 +55,17 @@ module.exports = { return { available: true, reason: null }; }, async execute({ args = {}, homeAssistantService }) { - const entityId = String(args?.entity_id || args?.entityId || '').trim(); - if (!entityId) throw new Error('ha_set_light_color requires args.entity_id'); + const resolved = resolveConfiguredEntity( + homeAssistantService, + args?.entity_id || args?.entityId, + 'ha_set_light_color', + ); + const { entity, entityId } = resolved; // Accepting args.color as a compatibility alias keeps manual/internal calls // forgiving, while the public tool schema still teaches the model to send // the clearer color_hex argument. const colorHex = normalizeColorHex(args?.color_hex ?? args?.colorHex ?? args?.color); - const entity = findConfiguredEntity(homeAssistantService, entityId); - if (!entity) throw new Error('ha_set_light_color entity_id not configured'); // setLightColor already requires a HA light, but checking the normalized // entity state here gives the overseer a more specific error and blocks