From 85f10cf60a24d8263edce6520dcebc3bf3375cf8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 20 Jan 2025 02:06:06 -0500 Subject: [PATCH] Use LLM fallback when local matching matches intent but not targets (#136045) LLM fallback to be used when local matching matches intent but finds no targets --- .../components/conversation/default_agent.py | 15 +++++- .../conversation/test_default_agent.py | 48 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index d4773d50c4b..1d79709adf8 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -1350,14 +1350,25 @@ class DefaultAgent(ConversationEntity): """Try to match sentence against registered intents and return response. Only performs strict matching with exposed entities and exact wording. - Returns None if no match occurred. + Returns None if no match or a matching error occurred. """ result = await self.async_recognize_intent(user_input, strict_intents_only=True) if not isinstance(result, RecognizeResult): # No error message on failed match return None - return await self._async_process_intent_result(result, user_input) + response = await self._async_process_intent_result(result, user_input) + if ( + response.response_type == intent.IntentResponseType.ERROR + and response.error_code + not in ( + intent.IntentResponseErrorCode.FAILED_TO_HANDLE, + intent.IntentResponseErrorCode.UNKNOWN, + ) + ): + # We ignore no matching errors + return None + return response def _make_error_result( diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 2cca9858c93..80a056a6ea0 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -3104,3 +3104,51 @@ async def test_turn_on_off( ) assert len(off_calls) == 1 assert off_calls[0].data.get("entity_id") == [entity_id] + + +@pytest.mark.parametrize( + ("error_code", "return_response"), + [ + (intent.IntentResponseErrorCode.NO_INTENT_MATCH, False), + (intent.IntentResponseErrorCode.NO_VALID_TARGETS, False), + (intent.IntentResponseErrorCode.FAILED_TO_HANDLE, True), + (intent.IntentResponseErrorCode.UNKNOWN, True), + ], +) +@pytest.mark.usefixtures("init_components") +async def test_handle_intents_with_response_errors( + hass: HomeAssistant, + init_components: None, + area_registry: ar.AreaRegistry, + error_code: intent.IntentResponseErrorCode, + return_response: bool, +) -> None: + """Test that handle_intents does not return response errors.""" + assert await async_setup_component(hass, "climate", {}) + area_registry.async_create("living room") + + agent: default_agent.DefaultAgent = hass.data[DATA_DEFAULT_ENTITY] + + user_input = ConversationInput( + text="What is the temperature in the living room?", + context=Context(), + conversation_id=None, + device_id=None, + language=hass.config.language, + agent_id=None, + ) + + with patch( + "homeassistant.components.conversation.default_agent.DefaultAgent._async_process_intent_result", + return_value=default_agent._make_error_result( + user_input.language, error_code, "Mock error message" + ), + ) as mock_process: + response = await agent.async_handle_intents(user_input) + + assert len(mock_process.mock_calls) == 1 + + if return_response: + assert response is not None and response.error_code == error_code + else: + assert response is None