iobroker.ai-assistant
Version:
AI Assistant adapter allows you to control your ioBroker trought artifical intelligence based on LLMs
422 lines (398 loc) • 18.1 kB
JavaScript
"use strict";
/**
* The StatesTool class
* This class is used to handle the trigger tool
*/
class TriggerTool {
/**
* Constructor for the TriggerTool
*
* @param adapter - The adapter instance
*/
constructor(adapter) {
this.adapter = adapter;
this.adapter.log.debug(`Created new TriggerTool`);
this.name = "TriggerTool";
this.temperature = 0.2;
this.max_tokens = 4000;
this.response_format = `{
"reasoning": "${this.adapter.I18n.translate("trigger_chain_of_thought")}",
"noticeToAssistant": "${this.adapter.I18n.translate("trigger_notice_to_assistant")}",
"createTriggers": [{"objectId": "${this.adapter.I18n.translate("trigger_object_id")}", "condition": {"operator": "${this.adapter.I18n.translate("trigger_condition_operator")}", "value": "ConditionValue"}, "onlyOnStateValueChange": true/false, "executeOnlyOnce": true/false, "instruction": "${this.adapter.I18n.translate("trigger_condition_instruction")}"}],
"deleteTriggers": ["${this.adapter.I18n.translate("trigger_delete_id")}"]
}`;
this.system_prompt = `
${this.adapter.I18n.translate("trigger_prompt_1")}
${this.adapter.I18n.translate("trigger_prompt_2")}
${this.adapter.I18n.translate("trigger_prompt_3")} ${this.response_format}
`;
this.tool_message = `
${this.adapter.I18n.translate("trigger_current_time")} ${new Date().toLocaleString()}
${this.adapter.I18n.translate("trigger_created")} ###TRIGGERS###
${this.adapter.I18n.translate("trigger_instruction")} "###PROMPT###".
${this.adapter.I18n.translate("trigger_available_datapoints")} ###ENDPOINTS###
${this.adapter.I18n.translate("trigger_answer")}
`;
this.prompt = null;
this.triggers = [];
this.oldStates = {};
this.initTriggers();
}
/**
* Check if a string is a number and return the result
*
* @param str - The string to check
* @returns - The result of the check
*/
isNumber(str) {
const num = Number(str);
return Number.isFinite(num);
}
/**
* Check if a triggered endpoint has changed and trigger the tool
*
* @param id - The id of the endpoint
* @param stateValue - The value of the endpoint
* @returns - The result of the check
*/
async triggerCheck(id, stateValue) {
for (const trigger of this.triggers) {
if (trigger.objectId === id) {
this.adapter.log.debug(`${this.name}: Checking trigger for ${trigger.objectId}`);
this.adapter.log.debug(`${this.name}: Old state for ${trigger.objectId} is ${this.oldStates[id]}`);
this.adapter.log.debug(`${this.name}: New state for ${trigger.objectId} is ${stateValue}`);
this.adapter.log.debug(
`${this.name}: Only on state value change for ${trigger.objectId} is ${trigger.onlyOnStateValueChange}`,
);
if (trigger.onlyOnStateValueChange && this.oldStates[id] === stateValue) {
this.adapter.log.debug(`${this.name}: No change for ${trigger.objectId}`);
return false;
}
this.oldStates[id] = stateValue;
if (trigger.condition && trigger.condition.operator && trigger.condition.value) {
if (this.compare(stateValue, trigger.condition.operator, trigger.condition.value) === null) {
this.adapter.log.warn(
`${this.name}: Invalid operator ${trigger.condition.operator} or value ${trigger.condition.value} for ${trigger.objectId}`,
);
return false;
}
if (this.isNumber(trigger.condition.value)) {
this.adapter.log.debug(`${this.name}: Converting value to number for ${trigger.objectId}`);
trigger.condition.value = Number(trigger.condition.value);
}
if (trigger.condition.value == "true" || trigger.condition.value == "True") {
this.adapter.log.debug(
`${this.name}: Converting value to boolean true for ${trigger.objectId}`,
);
trigger.condition.value = true;
} else if (trigger.condition.value == "false" || trigger.condition.value == "False") {
this.adapter.log.debug(
`${this.name}: Converting value to boolean false for ${trigger.objectId}`,
);
trigger.condition.value = false;
}
if (this.compare(stateValue, trigger.condition.operator, trigger.condition.value)) {
this.adapter.log.info(
`${this.name}: Triggered for ${trigger.objectId} with condition ${trigger.condition.operator} ${trigger.condition.value}`,
);
const executionData = {
type: "triggerWakeUpOnStateChange",
tool: this.name,
prompt: trigger.instruction,
noticeToAssistant: this.adapter.I18n.translate(
"trigger_execute_by_condition",
trigger.objectId,
trigger.condition.operator,
trigger.condition.value,
),
result: this.adapter.I18n.translate("trigger_current_value") + stateValue,
};
if (trigger.executeOnlyOnce) {
this.adapter.log.debug(
`${this.name}: Deleting trigger for ${trigger.objectId} with executeOnlyOnce`,
);
await this.deleteTrigger(trigger.id);
}
this.adapter.startAssistantRequest(JSON.stringify(executionData));
} else {
this.adapter.log.debug(
`${this.name}: Not triggered for ${trigger.objectId} with condition ${trigger.condition.operator} ${trigger.condition.value}`,
);
}
} else {
this.adapter.log.info(`${this.name}: Triggered for ${trigger.objectId} without condition`);
const executionData = {
tool: this.name,
prompt: trigger.instruction,
noticeToAssistant: this.adapter.I18n.translate("trigger_execute", trigger.objectId),
result: this.adapter.I18n.translate("trigger_current_value") + stateValue,
};
if (trigger.executeOnlyOnce) {
this.adapter.log.debug(
`${this.name}: Deleting trigger for ${trigger.objectId} with executeOnlyOnce`,
);
await this.deleteTrigger(trigger.id);
}
this.adapter.startAssistantRequest(JSON.stringify(executionData));
}
}
}
}
/**
* Compare two values with an operator
*
* @param post - The first value
* @param operator - The operator
* @param value - The second value
* @returns - The result of the comparison
*/
compare(post, operator, value) {
switch (operator) {
case ">":
return post > value;
case "<":
return post < value;
case ">=":
return post >= value;
case "<=":
return post <= value;
case "==":
return post == value;
case "!=":
return post != value;
case "===":
return post === value;
case "!==":
return post !== value;
default:
return null;
}
}
/**
* Initialize the triggers for the tool
*
* @returns - The result of the initialization
*/
async initTriggers() {
this.adapter.log.debug(`${this.name}: Initializing triggers for ${this.name}`);
for (const trigger of this.triggers) {
this.adapter.unsubscribeForeignStates(trigger.objectId);
}
this.triggers = [];
const triggers = await this.getTriggers();
for (const trigger of triggers) {
try {
this.createTrigger(
trigger.id,
trigger.objectId,
trigger.condition,
trigger.instruction,
trigger.onlyOnStateValueChange,
trigger.executeOnlyOnce,
);
} catch (e) {
this.adapter.log.error(`${this.name}: Error while parsing trigger data for ${trigger.id}: ${e}`);
}
}
}
/**
* Get all triggers for the tool
*
* @returns - The triggers for the tool
*/
async getTriggers() {
this.adapter.log.debug(`${this.name}: Getting triggers for ${this.name}`);
const allObjects = await this.adapter.getAdapterObjectsAsync();
const triggers = [];
this.adapter.log.debug(`${this.name}: Found ${Object.keys(allObjects).length} objects`);
for (const id in allObjects) {
this.adapter.log.debug(`${this.name}: Checking object ${id}`);
if (id.includes("Triggers.")) {
this.adapter.log.debug(`${this.name}: Found trigger for ${id}`);
const state = await this.adapter.getStateAsync(id);
try {
const triggerData = JSON.parse(this.adapter.jsonRepair(state.val));
if (triggerData && triggerData.objectId && triggerData.instruction) {
this.adapter.log.debug(
`${this.name}: Found trigger for ${id} with objectId ${triggerData.objectId} and instruction ${triggerData.instruction}`,
);
triggers.push({
id: id,
objectId: triggerData.objectId,
condition: triggerData.condition,
instruction: triggerData.instruction,
onlyOnStateValueChange: triggerData.onlyOnStateValueChange,
executeOnlyOnce: triggerData.executeOnlyOnce,
});
}
} catch (e) {
this.adapter.log.error(`${this.name}: Error while parsing trigger data for ${id}: ${e}`);
}
}
}
return triggers;
}
/**
* Create a trigger for the tool
*
* @param triggerId - The id of the trigger
* @param objectId - The id of the object
* @param condition - The condition for the trigger
* @param instruction - The instruction for the trigger
* @param onlyOnStateValueChange - The state value change for the trigger
* @param executeOnlyOnce - If the trigger should only execute once
* @returns - The result of the creation
*/
async createTrigger(triggerId, objectId, condition, instruction, onlyOnStateValueChange, executeOnlyOnce) {
if (!triggerId || !objectId || !instruction) {
return this.adapter.log.warn(
`Cannot create trigger for ${triggerId} with objectId ${objectId} and instruction ${instruction}`,
);
}
this.adapter.log.debug(
`${this.name}: Creating trigger for ${triggerId} with objectId ${objectId} and instruction ${instruction}`,
);
this.adapter.log.debug(
`${this.name}: Trigger Data: ObjectId${objectId} Condition: ${condition} Instruction: ${
instruction
} OnlyOnStateValueChange: ${onlyOnStateValueChange} ExecuteOnlyOnce: ${executeOnlyOnce}`,
);
this.adapter.log.debug(`${this.name}: Subscribing to state changes for ${objectId}`);
this.adapter.subscribeForeignStates(objectId);
this.triggers.push({
id: triggerId,
objectId: objectId,
condition: condition,
instruction: instruction,
onlyOnStateValueChange: onlyOnStateValueChange,
executeOnlyOnce: executeOnlyOnce,
});
const oldState = await this.adapter.getForeignStateAsync(objectId);
this.adapter.log.debug(`${this.name}: Saving old state for ${objectId} to ${oldState.val}`);
this.oldStates[objectId] = oldState.val;
}
/**
* Delete a trigger for the tool
*
* @param triggerId - The id of the trigger
* @returns - The result of the deletion
*/
async deleteTrigger(triggerId) {
this.adapter.log.debug(`${this.name}: Deleting trigger ${triggerId}`);
try {
await this.adapter.delObjectAsync(triggerId);
this.initTriggers();
} catch (e) {
this.adapter.log.error(`${this.name}: Error while deleting trigger for ${triggerId}: ${e}`);
}
}
/**
* Request data for the tool
*
* @param prompt - The prompt for the request
* @returns - The result of the request
*/
async request(prompt) {
this.adapter.log.debug(`${this.name}: Request data for ${this.name} with prompt: ${prompt}`);
const modelResponse = await this.adapter.startModelRequest(
[{ role: "user", content: await this.constructToolMessage(prompt) }],
this.system_prompt.replace(/(\r\n|\n|\t|\r)/gm, ""),
this.max_tokens,
this.temperature,
);
const toolResponse = await this.handleResponse(modelResponse.text);
return toolResponse;
}
/**
* Construct the tool message
*
* @param prompt - The prompt for the tool
* @returns - The constructed message
*/
async constructToolMessage(prompt) {
let message = this.tool_message;
const triggers = await this.getTriggers();
message = message.replace("###TRIGGERS###", JSON.stringify(triggers));
message = message.replace("###ENDPOINTS###", await this.adapter.getAvailableEndpointsStructure());
message = message.replace("###PROMPT###", prompt);
return message;
}
/**
* Handle the response from the tool
*
* @param modelResponse - The response from the tool
* @returns - The response for the assistant
*/
async handleResponse(modelResponse) {
this.adapter.log.debug(`${this.name}: Handling response for ${this.name}`);
try {
const responseData = JSON.parse(this.adapter.jsonRepair(modelResponse.replace(/(\r\n|\n|\t|\r)/gm, "")));
const createdTriggers = [];
const deletedTriggers = [];
if (
responseData.createTriggers &&
Array.isArray(responseData.createTriggers) &&
responseData.createTriggers.length > 0
) {
for (const trigger of responseData.createTriggers) {
const id = `Triggers.${Date.now()}`;
await this.adapter.setObjectAsync(id, {
type: "state",
common: {
name: `Trigger for ${this.name}`,
type: "string",
role: "state",
read: true,
write: true,
},
});
const triggerString = JSON.stringify({
objectId: trigger.objectId,
condition: trigger.condition,
instruction: trigger.instruction,
onlyOnStateValueChange: trigger.onlyOnStateValueChange,
executeOnlyOnce: trigger.executeOnlyOnce,
});
await this.adapter.setStateAsync(id, triggerString);
this.createTrigger(
id,
trigger.objectId,
trigger.condition,
trigger.instruction,
trigger.onlyOnStateValueChange,
trigger.executeOnlyOnce,
);
createdTriggers.push({
objectId: trigger.objectId,
condition: trigger.condition,
instruction: trigger.instruction,
onlyOnStateValueChange: trigger.onlyOnStateValueChange,
executeOnlyOnce: trigger.executeOnlyOnce,
});
}
}
if (
responseData.deleteTriggers &&
Array.isArray(responseData.deleteTriggers) &&
responseData.deleteTriggers.length > 0
) {
for (const id of responseData.deleteTriggers) {
await this.deleteTrigger(id);
deletedTriggers.push(id);
}
}
const toolResponse = {
type: "toolResponse",
tool: this.name,
reasoning: responseData.reasoning,
noticeToAssistant: responseData.noticeToAssistant,
result: { createdTriggers: createdTriggers, deletedTriggers: deletedTriggers },
};
return toolResponse;
} catch (error) {
this.adapter.log.error(`${this.name}: Error while parsing response for ${this.name}: ${error}`);
return null;
}
}
}
module.exports = TriggerTool;