dont make it always get the actual thingy perfect

This commit is contained in:
legop3
2026-06-06 17:46:27 -04:00
parent b21acc7f19
commit c636b41cbc
3 changed files with 54 additions and 19 deletions
@@ -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,
};
@@ -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' });
@@ -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