From daee3d8db05430261652df352be1b0a06cea5a7f Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 15 May 2024 21:23:24 -0500 Subject: [PATCH] Don't prioritize "name" slot if it's a wildcard in default conversation agent (#117518) * Don't prioritize "name" slot if it's a wildcard * Fix typing error --- .../components/conversation/default_agent.py | 33 +++++++++--- homeassistant/components/conversation/http.py | 4 +- .../components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../conversation/test_default_agent.py | 54 +++++++++++++++++++ .../custom_sentences/en/beer.yaml | 6 +++ 8 files changed, 91 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 98e8d07bd58..da77fc1ccb6 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -418,7 +418,9 @@ class DefaultAgent(ConversationEntity): language: str, ) -> RecognizeResult | None: """Search intents for a match to user input.""" - maybe_result: RecognizeResult | None = None + name_result: RecognizeResult | None = None + best_results: list[RecognizeResult] = [] + best_text_chunks_matched: int | None = None for result in recognize_all( user_input.text, lang_intents.intents, @@ -426,18 +428,33 @@ class DefaultAgent(ConversationEntity): intent_context=intent_context, language=language, ): - if "name" in result.entities: - return result + if ("name" in result.entities) and ( + not result.entities["name"].is_wildcard + ): + name_result = result - # Keep looking in case an entity has the same name - maybe_result = result + if (best_text_chunks_matched is None) or ( + result.text_chunks_matched > best_text_chunks_matched + ): + # Only overwrite if more literal text was matched. + # This causes wildcards to match last. + best_results = [result] + best_text_chunks_matched = result.text_chunks_matched + elif result.text_chunks_matched == best_text_chunks_matched: + # Accumulate results with the same number of literal text matched. + # We will resolve the ambiguity below. + best_results.append(result) - if maybe_result is not None: + if name_result is not None: + # Prioritize matches with entity names above area names + return name_result + + if best_results: # Successful strict match - return maybe_result + return best_results[0] # Try again with missing entities enabled - best_num_unmatched_entities = 0 + maybe_result: RecognizeResult | None = None for result in recognize_all( user_input.text, lang_intents.intents, diff --git a/homeassistant/components/conversation/http.py b/homeassistant/components/conversation/http.py index e582dacf284..209887fed0b 100644 --- a/homeassistant/components/conversation/http.py +++ b/homeassistant/components/conversation/http.py @@ -311,9 +311,9 @@ def _get_debug_targets( def _get_unmatched_slots( result: RecognizeResult, -) -> dict[str, str | int]: +) -> dict[str, str | int | float]: """Return a dict of unmatched text/range slot entities.""" - unmatched_slots: dict[str, str | int] = {} + unmatched_slots: dict[str, str | int | float] = {} for entity in result.unmatched_entities_list: if isinstance(entity, UnmatchedTextEntity): if entity.text == MISSING_ENTITY: diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 82e2adca680..b42a4c5004f 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.4.24"] + "requirements": ["hassil==1.7.1", "home-assistant-intents==2024.4.24"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1d5d645c693..ccf2c75e5b9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ ha-av==10.1.1 ha-ffmpeg==3.2.0 habluetooth==3.0.1 hass-nabucasa==0.78.0 -hassil==1.6.1 +hassil==1.7.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240501.1 home-assistant-intents==2024.4.24 diff --git a/requirements_all.txt b/requirements_all.txt index 31e17a0c6fd..e9ef18adacb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1047,7 +1047,7 @@ hass-nabucasa==0.78.0 hass-splunk==0.1.1 # homeassistant.components.conversation -hassil==1.6.1 +hassil==1.7.1 # homeassistant.components.jewish_calendar hdate==0.10.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 692342fd05c..07b8abed6a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -858,7 +858,7 @@ habluetooth==3.0.1 hass-nabucasa==0.78.0 # homeassistant.components.conversation -hassil==1.6.1 +hassil==1.7.1 # homeassistant.components.jewish_calendar hdate==0.10.8 diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 1ff3dd406c4..648a7d572ef 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -1122,3 +1122,57 @@ async def test_device_id_in_handler(hass: HomeAssistant, init_components) -> Non ) assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert handler.device_id == device_id + + +async def test_name_wildcard_lower_priority( + hass: HomeAssistant, init_components +) -> None: + """Test that the default agent does not prioritize a {name} slot when it's a wildcard.""" + + class OrderBeerIntentHandler(intent.IntentHandler): + intent_type = "OrderBeer" + + def __init__(self) -> None: + super().__init__() + self.triggered = False + + async def async_handle( + self, intent_obj: intent.Intent + ) -> intent.IntentResponse: + self.triggered = True + return intent_obj.create_response() + + class OrderFoodIntentHandler(intent.IntentHandler): + intent_type = "OrderFood" + + def __init__(self) -> None: + super().__init__() + self.triggered = False + + async def async_handle( + self, intent_obj: intent.Intent + ) -> intent.IntentResponse: + self.triggered = True + return intent_obj.create_response() + + beer_handler = OrderBeerIntentHandler() + food_handler = OrderFoodIntentHandler() + intent.async_register(hass, beer_handler) + intent.async_register(hass, food_handler) + + # Matches OrderBeer because more literal text is matched ("a") + result = await conversation.async_converse( + hass, "I'd like to order a stout please", None, Context(), None + ) + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert beer_handler.triggered + assert not food_handler.triggered + + # Matches OrderFood because "cookie" is not in the beer styles list + beer_handler.triggered = False + result = await conversation.async_converse( + hass, "I'd like to order a cookie please", None, Context(), None + ) + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert not beer_handler.triggered + assert food_handler.triggered diff --git a/tests/testing_config/custom_sentences/en/beer.yaml b/tests/testing_config/custom_sentences/en/beer.yaml index cedaae42ed1..f318e0221b2 100644 --- a/tests/testing_config/custom_sentences/en/beer.yaml +++ b/tests/testing_config/custom_sentences/en/beer.yaml @@ -4,8 +4,14 @@ intents: data: - sentences: - "I'd like to order a {beer_style} [please]" + OrderFood: + data: + - sentences: + - "I'd like to order {food_name:name} [please]" lists: beer_style: values: - "stout" - "lager" + food_name: + wildcard: true