Skip to content

Commit

Permalink
Migrate to new intent error response keys (#109269)
Browse files Browse the repository at this point in the history
  • Loading branch information
synesthesiam authored and frenck committed Feb 1, 2024
1 parent c31dfd6 commit 77b2555
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 86 deletions.
128 changes: 66 additions & 62 deletions homeassistant/components/conversation/default_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,15 @@
from typing import IO, Any

from hassil.expression import Expression, ListReference, Sequence
from hassil.intents import (
Intents,
ResponseType,
SlotList,
TextSlotList,
WildcardSlotList,
)
from hassil.intents import Intents, SlotList, TextSlotList, WildcardSlotList
from hassil.recognize import (
MISSING_ENTITY,
RecognizeResult,
UnmatchedEntity,
UnmatchedTextEntity,
recognize_all,
)
from hassil.util import merge_dict
from home_assistant_intents import get_intents, get_languages
from home_assistant_intents import ErrorKey, get_intents, get_languages
import yaml

from homeassistant import core, setup
Expand Down Expand Up @@ -259,7 +252,7 @@ async def async_process(self, user_input: ConversationInput) -> ConversationResu
return _make_error_result(
language,
intent.IntentResponseErrorCode.NO_INTENT_MATCH,
self._get_error_text(ResponseType.NO_INTENT, lang_intents),
self._get_error_text(ErrorKey.NO_INTENT, lang_intents),
conversation_id,
)

Expand All @@ -273,9 +266,7 @@ async def async_process(self, user_input: ConversationInput) -> ConversationResu
else "",
result.unmatched_entities_list,
)
error_response_type, error_response_args = _get_unmatched_response(
result.unmatched_entities
)
error_response_type, error_response_args = _get_unmatched_response(result)
return _make_error_result(
language,
intent.IntentResponseErrorCode.NO_VALID_TARGETS,
Expand Down Expand Up @@ -325,15 +316,15 @@ async def async_process(self, user_input: ConversationInput) -> ConversationResu
return _make_error_result(
language,
intent.IntentResponseErrorCode.FAILED_TO_HANDLE,
self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents),
self._get_error_text(ErrorKey.HANDLE_ERROR, lang_intents),
conversation_id,
)
except intent.IntentUnexpectedError:
_LOGGER.exception("Unexpected intent error")
return _make_error_result(
language,
intent.IntentResponseErrorCode.UNKNOWN,
self._get_error_text(ResponseType.HANDLE_ERROR, lang_intents),
self._get_error_text(ErrorKey.HANDLE_ERROR, lang_intents),
conversation_id,
)

Expand Down Expand Up @@ -795,15 +786,15 @@ def _make_intent_context(

def _get_error_text(
self,
response_type: ResponseType,
error_key: ErrorKey,
lang_intents: LanguageIntents | None,
**response_args,
) -> str:
"""Get response error text by type."""
if lang_intents is None:
return _DEFAULT_ERROR_TEXT

response_key = response_type.value
response_key = error_key.value
response_str = (
lang_intents.error_responses.get(response_key) or _DEFAULT_ERROR_TEXT
)
Expand Down Expand Up @@ -916,59 +907,72 @@ def _make_error_result(
return ConversationResult(response, conversation_id)


def _get_unmatched_response(
unmatched_entities: dict[str, UnmatchedEntity],
) -> tuple[ResponseType, dict[str, Any]]:
error_response_type = ResponseType.NO_INTENT
error_response_args: dict[str, Any] = {}
def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str, Any]]:
"""Get key and template arguments for error when there are unmatched intent entities/slots."""

if unmatched_name := unmatched_entities.get("name"):
# Unmatched device or entity
assert isinstance(unmatched_name, UnmatchedTextEntity)
error_response_type = ResponseType.NO_ENTITY
error_response_args["entity"] = unmatched_name.text
# Filter out non-text and missing context entities
unmatched_text: dict[str, str] = {
key: entity.text.strip()
for key, entity in result.unmatched_entities.items()
if isinstance(entity, UnmatchedTextEntity) and entity.text != MISSING_ENTITY
}

elif unmatched_area := unmatched_entities.get("area"):
# Unmatched area
assert isinstance(unmatched_area, UnmatchedTextEntity)
error_response_type = ResponseType.NO_AREA
error_response_args["area"] = unmatched_area.text
if unmatched_area := unmatched_text.get("area"):
# area only
return ErrorKey.NO_AREA, {"area": unmatched_area}

return error_response_type, error_response_args
# Area may still have matched
matched_area: str | None = None
if matched_area_entity := result.entities.get("area"):
matched_area = matched_area_entity.text.strip()

if unmatched_name := unmatched_text.get("name"):
if matched_area:
# device in area
return ErrorKey.NO_ENTITY_IN_AREA, {
"entity": unmatched_name,
"area": matched_area,
}

# device only
return ErrorKey.NO_ENTITY, {"entity": unmatched_name}

# Default error
return ErrorKey.NO_INTENT, {}


def _get_no_states_matched_response(
no_states_error: intent.NoStatesMatchedError,
) -> tuple[ResponseType, dict[str, Any]]:
"""Return error response type and template arguments for error."""
if not (
no_states_error.area
and (no_states_error.device_classes or no_states_error.domains)
):
# Device class and domain must be paired with an area for the error
# message.
return ResponseType.NO_INTENT, {}

error_response_args: dict[str, Any] = {"area": no_states_error.area}

# Check device classes first, since it's more specific than domain
) -> tuple[ErrorKey, dict[str, Any]]:
"""Return key and template arguments for error when intent returns no matching states."""

# Device classes should be checked before domains
if no_states_error.device_classes:
# No exposed entities of a particular class in an area.
# Example: "close the bedroom windows"
#
# Only use the first device class for the error message
error_response_args["device_class"] = next(iter(no_states_error.device_classes))

return ResponseType.NO_DEVICE_CLASS, error_response_args

# No exposed entities of a domain in an area.
# Example: "turn on lights in kitchen"
assert no_states_error.domains
#
# Only use the first domain for the error message
error_response_args["domain"] = next(iter(no_states_error.domains))

return ResponseType.NO_DOMAIN, error_response_args
device_class = next(iter(no_states_error.device_classes)) # first device class
if no_states_error.area:
# device_class in area
return ErrorKey.NO_DEVICE_CLASS_IN_AREA, {
"device_class": device_class,
"area": no_states_error.area,
}

# device_class only
return ErrorKey.NO_DEVICE_CLASS, {"device_class": device_class}

if no_states_error.domains:
domain = next(iter(no_states_error.domains)) # first domain
if no_states_error.area:
# domain in area
return ErrorKey.NO_DOMAIN_IN_AREA, {
"domain": domain,
"area": no_states_error.area,
}

# domain only
return ErrorKey.NO_DOMAIN, {"domain": domain}

# Default error
return ErrorKey.NO_INTENT, {}


def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/conversation/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["hassil==1.6.0", "home-assistant-intents==2024.1.29"]
"requirements": ["hassil==1.6.0", "home-assistant-intents==2024.2.1"]
}
2 changes: 1 addition & 1 deletion homeassistant/package_constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ hass-nabucasa==0.76.0
hassil==1.6.0
home-assistant-bluetooth==1.12.0
home-assistant-frontend==20240131.0
home-assistant-intents==2024.1.29
home-assistant-intents==2024.2.1
httpx==0.26.0
ifaddr==0.2.0
janus==1.0.0
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1062,7 +1062,7 @@ holidays==0.41
home-assistant-frontend==20240131.0

# homeassistant.components.conversation
home-assistant-intents==2024.1.29
home-assistant-intents==2024.2.1

# homeassistant.components.home_connect
homeconnect==0.7.2
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@ holidays==0.41
home-assistant-frontend==20240131.0

# homeassistant.components.conversation
home-assistant-intents==2024.1.29
home-assistant-intents==2024.2.1

# homeassistant.components.home_connect
homeconnect==0.7.2
Expand Down
18 changes: 9 additions & 9 deletions tests/components/conversation/snapshots/test_init.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'An unexpected error occurred while handling the intent',
'speech': 'An unexpected error occurred',
}),
}),
}),
Expand Down Expand Up @@ -379,7 +379,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'An unexpected error occurred while handling the intent',
'speech': 'An unexpected error occurred',
}),
}),
}),
Expand Down Expand Up @@ -519,7 +519,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Sorry, I am not aware of any device or entity called late added alias',
'speech': 'Sorry, I am not aware of any device called late added alias',
}),
}),
}),
Expand All @@ -539,7 +539,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Sorry, I am not aware of any device or entity called kitchen light',
'speech': 'Sorry, I am not aware of any device called kitchen light',
}),
}),
}),
Expand Down Expand Up @@ -679,7 +679,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Sorry, I am not aware of any device or entity called late added light',
'speech': 'Sorry, I am not aware of any device called late added light',
}),
}),
}),
Expand Down Expand Up @@ -759,7 +759,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Sorry, I am not aware of any device or entity called kitchen light',
'speech': 'Sorry, I am not aware of any device called kitchen light',
}),
}),
}),
Expand All @@ -779,7 +779,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Sorry, I am not aware of any device or entity called my cool light',
'speech': 'Sorry, I am not aware of any device called my cool light',
}),
}),
}),
Expand Down Expand Up @@ -919,7 +919,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Sorry, I am not aware of any device or entity called kitchen light',
'speech': 'Sorry, I am not aware of any device called kitchen light',
}),
}),
}),
Expand Down Expand Up @@ -969,7 +969,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Sorry, I am not aware of any device or entity called renamed light',
'speech': 'Sorry, I am not aware of any device called renamed light',
}),
}),
}),
Expand Down
Loading

0 comments on commit 77b2555

Please sign in to comment.