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
This commit is contained in:
Michael Hansen 2024-05-15 21:23:24 -05:00 committed by GitHub
parent f31873a846
commit daee3d8db0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 91 additions and 14 deletions

View File

@ -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,

View File

@ -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:

View File

@ -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"]
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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