Automation save dialog to suggest name, description and labels (#26071)

* AI Task structure

* Suggest description and labels too
This commit is contained in:
Paulus Schoutsen 2025-07-06 09:57:16 +02:00 committed by GitHub
parent 5760614b65
commit 27b36707e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 130 additions and 13 deletions

View File

@ -1,14 +1,23 @@
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import type { Selector } from "./selector";
export interface AITaskPreferences { export interface AITaskPreferences {
gen_data_entity_id: string | null; gen_data_entity_id: string | null;
} }
export interface GenDataTaskResult { export interface GenDataTaskResult<T = string> {
conversation_id: string; conversation_id: string;
data: string; data: T;
} }
export interface AITaskStructureField {
description?: string;
required?: boolean;
selector: Selector;
}
export type AITaskStructure = Record<string, AITaskStructureField>;
export const fetchAITaskPreferences = (hass: HomeAssistant) => export const fetchAITaskPreferences = (hass: HomeAssistant) =>
hass.callWS<AITaskPreferences>({ hass.callWS<AITaskPreferences>({
type: "ai_task/preferences/get", type: "ai_task/preferences/get",
@ -23,15 +32,16 @@ export const saveAITaskPreferences = (
...preferences, ...preferences,
}); });
export const generateDataAITask = async ( export const generateDataAITask = async <T = string>(
hass: HomeAssistant, hass: HomeAssistant,
task: { task: {
task_name: string; task_name: string;
entity_id?: string; entity_id?: string;
instructions: string; instructions: string;
structure?: AITaskStructure;
} }
): Promise<GenDataTaskResult> => { ): Promise<GenDataTaskResult<T>> => {
const result = await hass.callService<GenDataTaskResult>( const result = await hass.callService<GenDataTaskResult<T>>(
"ai_task", "ai_task",
"generate_data", "generate_data",
task, task,

View File

@ -30,6 +30,9 @@ import {
generateDataAITask, generateDataAITask,
} from "../../../../data/ai_task"; } from "../../../../data/ai_task";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
import { subscribeOne } from "../../../../common/util/subscribe-one";
import { subscribeLabelRegistry } from "../../../../data/label_registry";
@customElement("ha-dialog-automation-save") @customElement("ha-dialog-automation-save")
class DialogAutomationSave extends LitElement implements HassDialog { class DialogAutomationSave extends LitElement implements HassDialog {
@ -75,7 +78,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
this._entryUpdates.category ? "category" : "", this._entryUpdates.category ? "category" : "",
this._entryUpdates.labels.length > 0 ? "labels" : "", this._entryUpdates.labels.length > 0 ? "labels" : "",
this._entryUpdates.area ? "area" : "", this._entryUpdates.area ? "area" : "",
]; ].filter(Boolean);
} }
public closeDialog() { public closeDialog() {
@ -346,17 +349,121 @@ class DialogAutomationSave extends LitElement implements HassDialog {
} }
private async _suggest() { private async _suggest() {
const result = await generateDataAITask(this.hass, { const labels = await subscribeOne(
task_name: "frontend:automation:save", this.hass.connection,
instructions: `Suggest one name for the following Home Assistant automation. subscribeLabelRegistry
Your answer should only contain the name, without any additional text or formatting. ).then((labs) =>
The name should be relevant to the automation's purpose and should not exceed 50 characters. Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name]))
The name should be short, descriptive, sentence case, and written in the language ${this.hass.language}. );
const automationInspiration: string[] = [];
for (const automation of Object.values(this.hass.states)) {
const entityEntry = this.hass.entities[automation.entity_id];
if (
computeStateDomain(automation) !== "automation" ||
automation.attributes.restored ||
!automation.attributes.friendly_name ||
!entityEntry
) {
continue;
}
let inspiration = `- ${automation.attributes.friendly_name}`;
if (entityEntry.labels.length) {
inspiration += ` (labels: ${entityEntry.labels
.map((label) => labels[label])
.join(", ")})`;
}
automationInspiration.push(inspiration);
}
const result = await generateDataAITask<{
name: string;
description: string | undefined;
labels: string[] | undefined;
}>(this.hass, {
task_name: "frontend:automation:save",
instructions: `Suggest in language "${this.hass.language}" a name, description, and labels for the following Home Assistant automation.
The name should be relevant to the automation's purpose.
${
automationInspiration.length
? `The name should be in same style as existing automations.
Suggest labels if relevant to the automation's purpose.
Only suggest labels that are already used by existing automations.`
: `The name should be short, descriptive, sentence case, and written in the language ${this.hass.language}.`
}
If the automation contains 5+ steps, include a short description.
For inspiration, here are existing automations:
${automationInspiration.join("\n")}
The automation configuration is as follows:
${dump(this._params.config)} ${dump(this._params.config)}
`, `,
structure: {
name: {
description: "The name of the automation",
required: true,
selector: {
text: {},
},
},
description: {
description: "A short description of the automation",
required: false,
selector: {
text: {},
},
},
labels: {
description: "Labels for the automation",
required: false,
selector: {
text: {
multiple: true,
},
},
},
},
}); });
this._newName = result.data.trim(); this._newName = result.data.name;
if (result.data.description) {
this._newDescription = result.data.description;
if (!this._visibleOptionals.includes("description")) {
this._visibleOptionals = [...this._visibleOptionals, "description"];
}
}
if (result.data.labels?.length) {
// We get back label names, convert them to IDs
const newLabels: Record<string, undefined | string> = Object.fromEntries(
result.data.labels.map((name) => [name, undefined])
);
let toFind = result.data.labels.length;
for (const [labelId, labelName] of Object.entries(labels)) {
if (labelName in newLabels && newLabels[labelName] === undefined) {
newLabels[labelName] = labelId;
toFind--;
if (toFind === 0) {
break;
}
}
}
const foundLabels = Object.values(newLabels).filter(
(labelId) => labelId !== undefined
);
if (foundLabels.length) {
this._entryUpdates = {
...this._entryUpdates,
labels: foundLabels,
};
if (!this._visibleOptionals.includes("labels")) {
this._visibleOptionals = [...this._visibleOptionals, "labels"];
}
}
}
} }
private async _save(): Promise<void> { private async _save(): Promise<void> {