Migrate to new intent error response keys (#109269)
This commit is contained in:
parent
c2525d53dd
commit
a1eaa5cbf2
7 changed files with 174 additions and 82 deletions
|
@ -12,22 +12,15 @@ import re
|
|||
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
|
||||
|
@ -262,7 +255,7 @@ class DefaultAgent(AbstractConversationAgent):
|
|||
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,
|
||||
)
|
||||
|
||||
|
@ -276,9 +269,7 @@ class DefaultAgent(AbstractConversationAgent):
|
|||
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,
|
||||
|
@ -328,7 +319,7 @@ class DefaultAgent(AbstractConversationAgent):
|
|||
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:
|
||||
|
@ -336,7 +327,7 @@ class DefaultAgent(AbstractConversationAgent):
|
|||
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,
|
||||
)
|
||||
|
||||
|
@ -798,7 +789,7 @@ class DefaultAgent(AbstractConversationAgent):
|
|||
|
||||
def _get_error_text(
|
||||
self,
|
||||
response_type: ResponseType,
|
||||
error_key: ErrorKey,
|
||||
lang_intents: LanguageIntents | None,
|
||||
**response_args,
|
||||
) -> str:
|
||||
|
@ -806,7 +797,7 @@ class DefaultAgent(AbstractConversationAgent):
|
|||
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
|
||||
)
|
||||
|
@ -919,59 +910,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, {}
|
||||
) -> tuple[ErrorKey, dict[str, Any]]:
|
||||
"""Return key and template arguments for error when intent returns no matching states."""
|
||||
|
||||
error_response_args: dict[str, Any] = {"area": no_states_error.area}
|
||||
|
||||
# Check device classes first, since it's more specific than domain
|
||||
# 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))
|
||||
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,
|
||||
}
|
||||
|
||||
return ResponseType.NO_DEVICE_CLASS, error_response_args
|
||||
# device_class only
|
||||
return ErrorKey.NO_DEVICE_CLASS, {"device_class": device_class}
|
||||
|
||||
# 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))
|
||||
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,
|
||||
}
|
||||
|
||||
return ResponseType.NO_DOMAIN, error_response_args
|
||||
# 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:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue