diff --git a/src/data/conversation.ts b/src/data/conversation.ts index c63e6ba430..c76f6a2ecf 100644 --- a/src/data/conversation.ts +++ b/src/data/conversation.ts @@ -1,3 +1,4 @@ +import { ensureArray } from "../common/array/ensure-array"; import { HomeAssistant } from "../types"; interface IntentTarget { @@ -58,6 +59,24 @@ export interface Agent { supported_languages: "*" | string[]; } +export interface AssitDebugResult { + intent: { + name: string; + }; + entities: Record< + string, + { + name: string; + value: string; + text: string; + } + >; +} + +export interface AssistDebugResponse { + results: (AssitDebugResult | null)[]; +} + export const processConversationInput = ( hass: HomeAssistant, text: string, @@ -91,3 +110,16 @@ export const prepareConversation = ( type: "conversation/prepare", language, }); + +export const debugAgent = ( + hass: HomeAssistant, + sentences: string[] | string, + language: string, + device_id?: string +): Promise => + hass.callWS({ + type: "conversation/agent/homeassistant/debug", + sentences: ensureArray(sentences), + language, + device_id, + }); diff --git a/src/panels/developer-tools/assist/developer-tools-assist.ts b/src/panels/developer-tools/assist/developer-tools-assist.ts new file mode 100644 index 0000000000..89e3890f5c --- /dev/null +++ b/src/panels/developer-tools/assist/developer-tools-assist.ts @@ -0,0 +1,240 @@ +import { dump } from "js-yaml"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import "../../../components/ha-button"; +import "../../../components/ha-code-editor"; +import "../../../components/ha-language-picker"; +import "../../../components/ha-textarea"; +import "../../../components/ha-absolute-time"; +import type { HaTextArea } from "../../../components/ha-textarea"; +import { + AssitDebugResult, + debugAgent, + listAgents, +} from "../../../data/conversation"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; +import { haStyle } from "../../../resources/styles"; +import { HomeAssistant } from "../../../types"; +import { formatLanguageCode } from "../../../common/language/format_language"; +import { storage } from "../../../common/decorators/storage"; + +type SentenceParsingResult = { + sentence: string; + language: string; + result: AssitDebugResult | null; + time: Date; +}; + +@customElement("developer-tools-assist") +class HaPanelDevAssist extends SubscribeMixin(LitElement) { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean }) public narrow!: boolean; + + @state() supportedLanguages?: string[]; + + @storage({ + key: "assist_debug_language", + state: true, + subscribe: false, + storage: "localStorage", + }) + _language?: string; + + @state() _results: SentenceParsingResult[] = []; + + @query("#sentences-input") _sentencesInput!: HaTextArea; + + @state() _validInput = false; + + private _languageChanged(ev) { + this._language = ev.detail.value; + } + + private _textAreaInput(ev) { + const value = ev.target.value; + const valid = Boolean(value); + if (valid !== this._validInput) { + this._validInput = valid; + } + } + + private async _parse() { + const sentences = this._sentencesInput.value + .split("\n") + .filter((a) => a !== ""); + const { results } = await debugAgent(this.hass, sentences, this._language!); + + this._sentencesInput.value = ""; + + const now = new Date(); + + const newResults: SentenceParsingResult[] = []; + sentences.forEach((sentence, index) => { + const result = results[index]; + + newResults.push({ + sentence, + language: this._language!, + result, + time: now, + }); + }); + this._results = [...newResults, ...this._results]; + } + + private async _fetchLanguages() { + const { agents } = await listAgents(this.hass); + const assistAgent = agents.find((agent) => agent.id === "homeassistant"); + this.supportedLanguages = + assistAgent?.supported_languages === "*" + ? undefined + : assistAgent?.supported_languages; + } + + protected firstUpdated(): void { + this._fetchLanguages(); + } + + protected render() { + return html` +
+ +
+

+ Enter sentences and see how they will be parsed by Home Assistant. + Each line will be processed as individual sentence. Intents will + not be executed on your instance. +

+ ${this.supportedLanguages + ? html` + + ` + : nothing} + +
+
+ + Parse sentences + +
+
+ + ${this._results.map((r) => { + const { sentence, result, language, time } = r; + const matched = result != null; + + return html` + +
+
+

${sentence}

+

${matched ? "✅" : "❌"}

+
+
+

+ Language: ${formatLanguageCode( + language, + this.hass.locale + )} (${language}) +

+

Execution time: + + +

+ +

+
+ ${ + result + ? html` + + ` + : html`No intent matched` + } +
+
+ `; + })} +
+ `; + } + + static get styles(): CSSResultGroup { + return [ + haStyle, + css` + .content { + padding: 28px 20px 16px; + padding: max(28px, calc(12px + env(safe-area-inset-top))) + max(20px, calc(4px + env(safe-area-inset-right))) + max(16px, env(safe-area-inset-bottom)) + max(20px, calc(4px + env(safe-area-inset-left))); + max-width: 1040px; + margin: 0 auto; + } + .description { + margin: 0; + margin-bottom: 16px; + } + ha-textarea { + width: 100%; + } + .card-actions { + text-align: right; + } + .form { + margin-bottom: 16px; + } + .result { + margin-bottom: 16px; + } + .sentence { + font-weight: 500; + margin-bottom: 8px; + display: flex; + flex-direction: row; + justify-content: space-between; + } + .sentence p { + margin: 0; + } + .info p { + margin: 0; + } + ha-code-editor, + ha-alert { + display: block; + margin-top: 16px; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "developer-tools-assist": HaPanelDevAssist; + } +} diff --git a/src/panels/developer-tools/developer-tools-router.ts b/src/panels/developer-tools/developer-tools-router.ts index b614f5e817..5b8baf3899 100644 --- a/src/panels/developer-tools/developer-tools-router.ts +++ b/src/panels/developer-tools/developer-tools-router.ts @@ -45,6 +45,10 @@ class DeveloperToolsRouter extends HassRouterPage { tag: "developer-yaml-config", load: () => import("./yaml_configuration/developer-yaml-config"), }, + assist: { + tag: "developer-tools-assist", + load: () => import("./assist/developer-tools-assist"), + }, }, }; diff --git a/src/panels/developer-tools/ha-panel-developer-tools.ts b/src/panels/developer-tools/ha-panel-developer-tools.ts index 54c5666e9f..813cc014e7 100644 --- a/src/panels/developer-tools/ha-panel-developer-tools.ts +++ b/src/panels/developer-tools/ha-panel-developer-tools.ts @@ -65,6 +65,7 @@ class PanelDeveloperTools extends LitElement { "ui.panel.developer-tools.tabs.statistics.title" )} + Assist