diff --git a/homeassistant/components/almond/__init__.py b/homeassistant/components/almond/__init__.py index 09ff85491ba..a391795df15 100644 --- a/homeassistant/components/almond/__init__.py +++ b/homeassistant/components/almond/__init__.py @@ -286,7 +286,11 @@ class AlmondAgent(conversation.AbstractConversationAgent): return True async def async_process( - self, text: str, context: Context, conversation_id: str | None = None + self, + text: str, + context: Context, + conversation_id: str | None = None, + language: str | None = None, ) -> intent.IntentResponse: """Process a sentence.""" response = await self.api.async_converse_text(text, conversation_id) @@ -310,6 +314,6 @@ class AlmondAgent(conversation.AbstractConversationAgent): buffer += "," buffer += f" {message['title']}" - intent_result = intent.IntentResponse() + intent_result = intent.IntentResponse(language=language) intent_result.async_set_speech(buffer.strip()) return intent_result diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index deab740909e..6bfd619f4d1 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -22,6 +22,7 @@ from .default_agent import DefaultAgent, async_register _LOGGER = logging.getLogger(__name__) ATTR_TEXT = "text" +ATTR_LANGUAGE = "language" DOMAIN = "conversation" @@ -31,7 +32,9 @@ DATA_CONFIG = "conversation_config" SERVICE_PROCESS = "process" -SERVICE_PROCESS_SCHEMA = vol.Schema({vol.Required(ATTR_TEXT): cv.string}) +SERVICE_PROCESS_SCHEMA = vol.Schema( + {vol.Required(ATTR_TEXT): cv.string, vol.Optional(ATTR_LANGUAGE): cv.string} +) CONFIG_SCHEMA = vol.Schema( { @@ -66,7 +69,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.debug("Processing: <%s>", text) agent = await _get_agent(hass) try: - await agent.async_process(text, service.context) + await agent.async_process( + text, service.context, language=service.data.get(ATTR_LANGUAGE) + ) except intent.IntentHandleError as err: _LOGGER.error("Error processing %s: %s", text, err) @@ -82,7 +87,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @websocket_api.websocket_command( - {"type": "conversation/process", "text": str, vol.Optional("conversation_id"): str} + { + "type": "conversation/process", + "text": str, + vol.Optional("conversation_id"): str, + vol.Optional("language"): str, + } ) @websocket_api.async_response async def websocket_process( @@ -94,7 +104,11 @@ async def websocket_process( connection.send_result( msg["id"], await _async_converse( - hass, msg["text"], msg.get("conversation_id"), connection.context(msg) + hass, + msg["text"], + msg.get("conversation_id"), + connection.context(msg), + msg.get("language"), ), ) @@ -143,7 +157,13 @@ class ConversationProcessView(http.HomeAssistantView): name = "api:conversation:process" @RequestDataValidator( - vol.Schema({vol.Required("text"): str, vol.Optional("conversation_id"): str}) + vol.Schema( + { + vol.Required("text"): str, + vol.Optional("conversation_id"): str, + vol.Optional("language"): str, + } + ) ) async def post(self, request, data): """Send a request for processing.""" @@ -151,7 +171,11 @@ class ConversationProcessView(http.HomeAssistantView): try: intent_result = await _async_converse( - hass, data["text"], data.get("conversation_id"), self.context(request) + hass, + text=data["text"], + conversation_id=data.get("conversation_id"), + context=self.context(request), + language=data.get("language"), ) except intent.IntentError as err: _LOGGER.error("Error handling intent: %s", err) @@ -182,17 +206,20 @@ async def _async_converse( text: str, conversation_id: str | None, context: core.Context, + language: str | None = None, ) -> intent.IntentResponse: """Process text and get intent.""" agent = await _get_agent(hass) try: - intent_result = await agent.async_process(text, context, conversation_id) + intent_result = await agent.async_process( + text, context, conversation_id, language + ) except intent.IntentHandleError as err: - intent_result = intent.IntentResponse() + intent_result = intent.IntentResponse(language=language) intent_result.async_set_speech(str(err)) if intent_result is None: - intent_result = intent.IntentResponse() + intent_result = intent.IntentResponse(language=language) intent_result.async_set_speech("Sorry, I didn't understand that") return intent_result diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py index a19ae6d697b..d38c6acb936 100644 --- a/homeassistant/components/conversation/agent.py +++ b/homeassistant/components/conversation/agent.py @@ -25,6 +25,10 @@ class AbstractConversationAgent(ABC): @abstractmethod async def async_process( - self, text: str, context: Context, conversation_id: str | None = None + self, + text: str, + context: Context, + conversation_id: str | None = None, + language: str | None = None, ) -> intent.IntentResponse | None: """Process a sentence.""" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 9079f7893ec..2c92d5245b6 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -111,7 +111,11 @@ class DefaultAgent(AbstractConversationAgent): async_register(self.hass, intent_type, sentences) async def async_process( - self, text: str, context: core.Context, conversation_id: str | None = None + self, + text: str, + context: core.Context, + conversation_id: str | None = None, + language: str | None = None, ) -> intent.IntentResponse | None: """Process a sentence.""" intents = self.hass.data[DOMAIN] @@ -128,6 +132,7 @@ class DefaultAgent(AbstractConversationAgent): {key: {"value": value} for key, value in match.groupdict().items()}, text, context, + language, ) return None diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 7b0b1033016..37d336cda2f 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -56,6 +56,7 @@ async def async_handle( slots: _SlotsType | None = None, text_input: str | None = None, context: Context | None = None, + language: str | None = None, ) -> IntentResponse: """Handle an intent.""" handler: IntentHandler = hass.data.get(DATA_KEY, {}).get(intent_type) @@ -66,7 +67,12 @@ async def async_handle( if context is None: context = Context() - intent = Intent(hass, platform, intent_type, slots or {}, text_input, context) + if language is None: + language = hass.config.language + + intent = Intent( + hass, platform, intent_type, slots or {}, text_input, context, language + ) try: _LOGGER.info("Triggering intent handler %s", handler) @@ -218,7 +224,15 @@ class ServiceIntentHandler(IntentHandler): class Intent: """Hold the intent.""" - __slots__ = ["hass", "platform", "intent_type", "slots", "text_input", "context"] + __slots__ = [ + "hass", + "platform", + "intent_type", + "slots", + "text_input", + "context", + "language", + ] def __init__( self, @@ -228,6 +242,7 @@ class Intent: slots: _SlotsType, text_input: str | None, context: Context, + language: str, ) -> None: """Initialize an intent.""" self.hass = hass @@ -236,36 +251,52 @@ class Intent: self.slots = slots self.text_input = text_input self.context = context + self.language = language @callback def create_response(self) -> IntentResponse: """Create a response.""" - return IntentResponse(self) + return IntentResponse(self, language=self.language) class IntentResponse: """Response to an intent.""" - def __init__(self, intent: Intent | None = None) -> None: + def __init__( + self, intent: Intent | None = None, language: str | None = None + ) -> None: """Initialize an IntentResponse.""" self.intent = intent self.speech: dict[str, dict[str, Any]] = {} self.reprompt: dict[str, dict[str, Any]] = {} self.card: dict[str, dict[str, str]] = {} + self.language = language @callback def async_set_speech( - self, speech: str, speech_type: str = "plain", extra_data: Any | None = None + self, + speech: str, + speech_type: str = "plain", + extra_data: Any | None = None, ) -> None: """Set speech response.""" - self.speech[speech_type] = {"speech": speech, "extra_data": extra_data} + self.speech[speech_type] = { + "speech": speech, + "extra_data": extra_data, + } @callback def async_set_reprompt( - self, speech: str, speech_type: str = "plain", extra_data: Any | None = None + self, + speech: str, + speech_type: str = "plain", + extra_data: Any | None = None, ) -> None: """Set reprompt response.""" - self.reprompt[speech_type] = {"reprompt": speech, "extra_data": extra_data} + self.reprompt[speech_type] = { + "reprompt": speech, + "extra_data": extra_data, + } @callback def async_set_card( @@ -275,10 +306,15 @@ class IntentResponse: self.card[card_type] = {"title": title, "content": content} @callback - def as_dict(self) -> dict[str, dict[str, dict[str, Any]]]: + def as_dict(self) -> dict[str, Any]: """Return a dictionary representation of an intent response.""" - return ( - {"speech": self.speech, "reprompt": self.reprompt, "card": self.card} - if self.reprompt - else {"speech": self.speech, "card": self.card} - ) + response_dict: dict[str, Any] = { + "speech": self.speech, + "card": self.card, + "language": self.language, + } + + if self.reprompt: + response_dict["reprompt"] = self.reprompt + + return response_dict diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index cbaa3278e9c..9815f42029d 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -125,7 +125,13 @@ async def test_http_processing_intent(hass, hass_client, hass_admin_user): "card": { "simple": {"content": "You chose a Grolsch.", "title": "Beer ordered"} }, - "speech": {"plain": {"extra_data": None, "speech": "I've ordered a Grolsch!"}}, + "speech": { + "plain": { + "extra_data": None, + "speech": "I've ordered a Grolsch!", + } + }, + "language": hass.config.language, } @@ -248,10 +254,10 @@ async def test_custom_agent(hass, hass_client, hass_admin_user): class MyAgent(conversation.AbstractConversationAgent): """Test Agent.""" - async def async_process(self, text, context, conversation_id): + async def async_process(self, text, context, conversation_id, language): """Process some text.""" - calls.append((text, context, conversation_id)) - response = intent.IntentResponse() + calls.append((text, context, conversation_id, language)) + response = intent.IntentResponse(language=language) response.async_set_speech("Test response") return response @@ -263,15 +269,26 @@ async def test_custom_agent(hass, hass_client, hass_admin_user): resp = await client.post( "/api/conversation/process", - json={"text": "Test Text", "conversation_id": "test-conv-id"}, + json={ + "text": "Test Text", + "conversation_id": "test-conv-id", + "language": "test-language", + }, ) assert resp.status == HTTPStatus.OK assert await resp.json() == { "card": {}, - "speech": {"plain": {"extra_data": None, "speech": "Test response"}}, + "speech": { + "plain": { + "extra_data": None, + "speech": "Test response", + } + }, + "language": "test-language", } assert len(calls) == 1 assert calls[0][0] == "Test Text" assert calls[0][1].user_id == hass_admin_user.id assert calls[0][2] == "test-conv-id" + assert calls[0][3] == "test-language" diff --git a/tests/components/intent/test_init.py b/tests/components/intent/test_init.py index 6ea4d045c4d..1c5e53cd2fd 100644 --- a/tests/components/intent/test_init.py +++ b/tests/components/intent/test_init.py @@ -46,7 +46,13 @@ async def test_http_handle_intent(hass, hass_client, hass_admin_user): "card": { "simple": {"content": "You chose a Belgian.", "title": "Beer ordered"} }, - "speech": {"plain": {"extra_data": None, "speech": "I've ordered a Belgian!"}}, + "speech": { + "plain": { + "extra_data": None, + "speech": "I've ordered a Belgian!", + } + }, + "language": hass.config.language, }