From 52a02093e395c4a9755901dbe4f3b2b825260361 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Jun 2025 12:07:29 -0400 Subject: [PATCH] Allow changing LLM Task preferences (#25779) * Allow changing LLM Task preferences * value-changed --- src/data/ai_task.ts | 43 +++++ src/data/icons.ts | 2 + .../config/voice-assistants/ai-task-pref.ts | 159 ++++++++++++++++++ .../ha-config-voice-assistants-assistants.ts | 7 + src/translations/en.json | 6 + src/types.ts | 8 +- 6 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 src/data/ai_task.ts create mode 100644 src/panels/config/voice-assistants/ai-task-pref.ts diff --git a/src/data/ai_task.ts b/src/data/ai_task.ts new file mode 100644 index 0000000000..c4f42d0bc8 --- /dev/null +++ b/src/data/ai_task.ts @@ -0,0 +1,43 @@ +import type { HomeAssistant } from "../types"; + +export interface AITaskPreferences { + gen_text_entity_id: string | null; +} + +export interface GenTextTaskResult { + conversation_id: string; + text: string; +} + +export const fetchAITaskPreferences = (hass: HomeAssistant) => + hass.callWS({ + type: "ai_task/preferences/get", + }); + +export const saveAITaskPreferences = ( + hass: HomeAssistant, + preferences: Partial +) => + hass.callWS({ + type: "ai_task/preferences/set", + ...preferences, + }); + +export const generateTextAITask = async ( + hass: HomeAssistant, + task: { + task_name: string; + entity_id?: string; + instructions: string; + } +): Promise => { + const result = await hass.callService( + "ai_task", + "generate_text", + task, + undefined, + true, + true + ); + return result.response!; +}; diff --git a/src/data/icons.ts b/src/data/icons.ts index 93cd21fee1..5d870aac2e 100644 --- a/src/data/icons.ts +++ b/src/data/icons.ts @@ -37,6 +37,7 @@ import { mdiRoomService, mdiScriptText, mdiSpeakerMessage, + mdiStarFourPoints, mdiThermostat, mdiTimerOutline, mdiToggleSwitch, @@ -66,6 +67,7 @@ export const DEFAULT_DOMAIN_ICON = mdiBookmark; /** Fallback icons for each domain */ export const FALLBACK_DOMAIN_ICONS = { + ai_task: mdiStarFourPoints, air_quality: mdiAirFilter, alert: mdiAlert, automation: mdiRobot, diff --git a/src/panels/config/voice-assistants/ai-task-pref.ts b/src/panels/config/voice-assistants/ai-task-pref.ts new file mode 100644 index 0000000000..9eba296781 --- /dev/null +++ b/src/panels/config/voice-assistants/ai-task-pref.ts @@ -0,0 +1,159 @@ +import "@material/mwc-button"; +import { mdiHelpCircle, mdiStarFourPoints } from "@mdi/js"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import "../../../components/ha-card"; +import "../../../components/ha-settings-row"; +import "../../../components/entity/ha-entity-picker"; +import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker"; +import type { HomeAssistant } from "../../../types"; +import { brandsUrl } from "../../../util/brands-url"; +import { + fetchAITaskPreferences, + saveAITaskPreferences, + type AITaskPreferences, +} from "../../../data/ai_task"; +import { documentationUrl } from "../../../util/documentation-url"; + +@customElement("ai-task-pref") +export class AITaskPref extends LitElement { + @property({ type: Boolean, reflect: true }) public narrow = false; + + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _prefs?: AITaskPreferences; + + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + fetchAITaskPreferences(this.hass).then((prefs) => { + this._prefs = prefs; + }); + } + + protected render() { + if (!this._prefs) { + return nothing; + } + + return html` + +

+ ${this.hass.localize( + "ui.panel.config.voice_assistants.ai_task.header" + )} +

+
+ + + +
+
+

+ ${this.hass!.localize( + "ui.panel.config.voice_assistants.ai_task.description", + { + button: html``, + } + )} +

+ + + ${this.hass!.localize( + "ui.panel.config.voice_assistants.ai_task.gen_text_header" + )} + + + ${this.hass!.localize( + "ui.panel.config.voice_assistants.ai_task.gen_text_description" + )} + + + +
+
+ `; + } + + private async _handlePrefChange( + ev: CustomEvent<{ value: string | undefined }> + ) { + const input = ev.target as HaEntityPicker; + const key = input.getAttribute("data-name") as keyof AITaskPreferences; + const entityId = ev.detail.value || null; + const oldPrefs = this._prefs; + this._prefs = { ...this._prefs!, [key]: entityId }; + try { + this._prefs = await saveAITaskPreferences(this.hass, { + [key]: entityId, + }); + } catch (_err: any) { + this._prefs = oldPrefs; + } + } + + static styles = css` + a { + color: var(--primary-color); + } + ha-settings-row { + padding: 0; + } + .header-actions { + position: absolute; + right: 0px; + inset-inline-end: 0px; + inset-inline-start: initial; + top: 24px; + display: flex; + flex-direction: row; + } + .header-actions .icon-link { + margin-top: -16px; + margin-right: 8px; + margin-inline-end: 8px; + margin-inline-start: initial; + direction: var(--direction); + color: var(--secondary-text-color); + } + ha-entity-picker { + flex: 1; + margin-left: 16px; + } + :host([narrow]) ha-entity-picker { + margin-left: 0; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ai-task-pref": AITaskPref; + } +} diff --git a/src/panels/config/voice-assistants/ha-config-voice-assistants-assistants.ts b/src/panels/config/voice-assistants/ha-config-voice-assistants-assistants.ts index ff959d7830..f9aa48319b 100644 --- a/src/panels/config/voice-assistants/ha-config-voice-assistants-assistants.ts +++ b/src/panels/config/voice-assistants/ha-config-voice-assistants-assistants.ts @@ -8,6 +8,7 @@ import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-tabs-subpage"; import type { HomeAssistant, Route } from "../../../types"; import "./assist-pref"; +import "./ai-task-pref"; import "./cloud-alexa-pref"; import "./cloud-discover"; import "./cloud-google-pref"; @@ -53,6 +54,12 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement { > ` : nothing} + ${isComponentLoaded(this.hass, "ai_task") + ? html`` + : nothing} ${this.cloudStatus?.logged_in ? html` { context: Context; - response?: any; + response?: T; } export interface ServiceCallRequest { @@ -248,14 +248,14 @@ export interface HomeAssistant { user?: CurrentUser; userData?: CoreFrontendUserData | null; hassUrl(path?): string; - callService( + callService( domain: ServiceCallRequest["domain"], service: ServiceCallRequest["service"], serviceData?: ServiceCallRequest["serviceData"], target?: ServiceCallRequest["target"], notifyOnError?: boolean, returnResponse?: boolean - ): Promise; + ): Promise>; callApi( method: "GET" | "POST" | "PUT" | "DELETE", path: string,