diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index aba5aafd378..94af03a9e90 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -3,6 +3,7 @@ from __future__ import annotations from dataclasses import dataclass import logging +from pathlib import Path import re from typing import Any @@ -10,6 +11,7 @@ from hassil.intents import Intents, SlotList, TextSlotList from hassil.recognize import recognize from hassil.util import merge_dict from home_assistant_intents import get_intents +import yaml from homeassistant import core, setup from homeassistant.helpers import area_registry, entity_registry, intent @@ -147,6 +149,32 @@ class DefaultAgent(AbstractConversationAgent): # Will need to recreate graph intents_changed = True + _LOGGER.debug( + "Loaded intents component=%s, language=%s", component, language + ) + + # Check for custom sentences in /custom_sentences// + if lang_intents is None: + # Only load custom sentences once, otherwise they will be re-loaded + # when components change. + custom_sentences_dir = Path( + self.hass.config.path("custom_sentences", language) + ) + if custom_sentences_dir.is_dir(): + for custom_sentences_path in custom_sentences_dir.rglob("*.yaml"): + with custom_sentences_path.open( + encoding="utf-8" + ) as custom_sentences_file: + # Merge custom sentences + merge_dict(intents_dict, yaml.safe_load(custom_sentences_file)) + + # Will need to recreate graph + intents_changed = True + _LOGGER.debug( + "Loaded custom sentences language=%s, path=%s", + language, + custom_sentences_path, + ) if not intents_dict: return None diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index ffb7894cd00..22ea6208214 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -12,6 +12,19 @@ from homeassistant.setup import async_setup_component from tests.common import async_mock_service +class OrderBeerIntentHandler(intent.IntentHandler): + """Handle OrderBeer intent.""" + + intent_type = "OrderBeer" + + async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: + """Return speech response.""" + beer_style = intent_obj.slots["beer_style"]["value"] + response = intent_obj.create_response() + response.async_set_speech(f"You ordered a {beer_style}") + return response + + @pytest.fixture async def init_components(hass): """Initialize relevant components with empty configs.""" @@ -314,3 +327,43 @@ async def test_ws_api(hass, hass_ws_client, payload): }, "conversation_id": payload.get("conversation_id") or ANY, } + + +async def test_custom_sentences(hass, hass_client, hass_admin_user): + """Test custom sentences with a custom intent.""" + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, "conversation", {}) + assert await async_setup_component(hass, "intent", {}) + + # Expecting testing_config/custom_sentences/en/beer.yaml + intent.async_register(hass, OrderBeerIntentHandler()) + + # Invoke intent via HTTP API + client = await hass_client() + for beer_style in ("stout", "lager"): + resp = await client.post( + "/api/conversation/process", + json={"text": f"I'd like to order a {beer_style}, please"}, + ) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data == { + "response": { + "card": {}, + "speech": { + "plain": { + "extra_data": None, + "speech": f"You ordered a {beer_style}", + } + }, + "language": hass.config.language, + "response_type": "action_done", + "data": { + "targets": [], + "success": [], + "failed": [], + }, + }, + "conversation_id": None, + } diff --git a/tests/testing_config/custom_sentences/en/beer.yaml b/tests/testing_config/custom_sentences/en/beer.yaml new file mode 100644 index 00000000000..cedaae42ed1 --- /dev/null +++ b/tests/testing_config/custom_sentences/en/beer.yaml @@ -0,0 +1,11 @@ +language: "en" +intents: + OrderBeer: + data: + - sentences: + - "I'd like to order a {beer_style} [please]" +lists: + beer_style: + values: + - "stout" + - "lager"