mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
dont make it always get the actual thingy perfect
This commit is contained in:
@@ -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 = {
|
module.exports = {
|
||||||
id: 'ha_set_entity',
|
id: 'ha_set_entity',
|
||||||
signature: 'ha_set_entity(entity_id, state)',
|
signature: 'ha_set_entity(entity_id, state)',
|
||||||
@@ -23,18 +25,17 @@ module.exports = {
|
|||||||
return { available: true, reason: null };
|
return { available: true, reason: null };
|
||||||
},
|
},
|
||||||
async execute({ args = {}, homeAssistantService }) {
|
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
|
// Configured Home Assistant entities are the room-control surface the
|
||||||
// overseer is allowed to use. Some physical room lights are exposed by Home
|
// 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
|
// Assistant as switches because they are outlet-backed lamps, so this tool
|
||||||
// intentionally permits every configured entity for on/off control instead
|
// intentionally permits every configured entity for on/off control instead
|
||||||
// of limiting itself to HA's light domain.
|
// of limiting itself to HA's light domain.
|
||||||
const allowed = new Set(
|
const { entityId } = resolveConfiguredEntity(
|
||||||
(homeAssistantService.getState()?.entities || []).map((entry) => String(entry?.id || '')).filter(Boolean),
|
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();
|
const state = String(args?.state || '').toLowerCase();
|
||||||
if (state !== 'on' && state !== 'off') throw new Error('ha_set_entity requires args.state of on/off');
|
if (state !== 'on' && state !== 'off') throw new Error('ha_set_entity requires args.state of on/off');
|
||||||
await homeAssistantService.setEntityState(entityId, state, { source: 'overseerControl' });
|
await homeAssistantService.setEntityState(entityId, state, { source: 'overseerControl' });
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const { resolveConfiguredEntity } = require('./haEntityLookup');
|
||||||
|
|
||||||
function normalizeColorHex(value) {
|
function normalizeColorHex(value) {
|
||||||
const raw = String(value || '').trim();
|
const raw = String(value || '').trim();
|
||||||
const withoutHash = raw.startsWith('#') ? raw.slice(1) : raw;
|
const withoutHash = raw.startsWith('#') ? raw.slice(1) : raw;
|
||||||
@@ -24,15 +26,6 @@ function normalizeColorHex(value) {
|
|||||||
return `#${expanded.toLowerCase()}`;
|
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 = {
|
module.exports = {
|
||||||
id: 'ha_set_light_color',
|
id: 'ha_set_light_color',
|
||||||
signature: 'ha_set_light_color(entity_id, color_hex)',
|
signature: 'ha_set_light_color(entity_id, color_hex)',
|
||||||
@@ -62,15 +55,17 @@ module.exports = {
|
|||||||
return { available: true, reason: null };
|
return { available: true, reason: null };
|
||||||
},
|
},
|
||||||
async execute({ args = {}, homeAssistantService }) {
|
async execute({ args = {}, homeAssistantService }) {
|
||||||
const entityId = String(args?.entity_id || args?.entityId || '').trim();
|
const resolved = resolveConfiguredEntity(
|
||||||
if (!entityId) throw new Error('ha_set_light_color requires args.entity_id');
|
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
|
// Accepting args.color as a compatibility alias keeps manual/internal calls
|
||||||
// forgiving, while the public tool schema still teaches the model to send
|
// forgiving, while the public tool schema still teaches the model to send
|
||||||
// the clearer color_hex argument.
|
// the clearer color_hex argument.
|
||||||
const colorHex = normalizeColorHex(args?.color_hex ?? args?.colorHex ?? args?.color);
|
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
|
// setLightColor already requires a HA light, but checking the normalized
|
||||||
// entity state here gives the overseer a more specific error and blocks
|
// entity state here gives the overseer a more specific error and blocks
|
||||||
|
|||||||
Reference in New Issue
Block a user