Fix intent loading and incorporate unmatched entities more (#108423)

* Incorporate unmatched entities more

* Don't list targets when match is incomplete

* Add test for out of range
This commit is contained in:
Michael Hansen 2024-01-23 19:31:57 -06:00 committed by GitHub
parent c725238c20
commit d8a1c58b12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 382 additions and 141 deletions

View file

@ -6,6 +6,7 @@ from collections import defaultdict
from collections.abc import Awaitable, Callable, Iterable
from dataclasses import dataclass
import functools
import itertools
import logging
from pathlib import Path
import re
@ -20,6 +21,7 @@ from hassil.intents import (
WildcardSlotList,
)
from hassil.recognize import (
MISSING_ENTITY,
RecognizeResult,
UnmatchedEntity,
UnmatchedTextEntity,
@ -75,7 +77,7 @@ class LanguageIntents:
intents_dict: dict[str, Any]
intent_responses: dict[str, Any]
error_responses: dict[str, Any]
loaded_components: set[str]
language_variant: str | None
@dataclass(slots=True)
@ -181,9 +183,7 @@ class DefaultAgent(AbstractConversationAgent):
lang_intents = self._lang_intents.get(language)
# Reload intents if missing or new components
if lang_intents is None or (
lang_intents.loaded_components - self.hass.config.components
):
if lang_intents is None:
# Load intents in executor
lang_intents = await self.async_get_or_load_intents(language)
@ -357,6 +357,13 @@ class DefaultAgent(AbstractConversationAgent):
intent_context=intent_context,
allow_unmatched_entities=True,
):
# Remove missing entities that couldn't be filled from context
for entity_key, entity in list(result.unmatched_entities.items()):
if isinstance(entity, UnmatchedTextEntity) and (
entity.text == MISSING_ENTITY
):
result.unmatched_entities.pop(entity_key)
if maybe_result is None:
# First result
maybe_result = result
@ -364,8 +371,11 @@ class DefaultAgent(AbstractConversationAgent):
# Fewer unmatched entities
maybe_result = result
elif len(result.unmatched_entities) == len(maybe_result.unmatched_entities):
if result.text_chunks_matched > maybe_result.text_chunks_matched:
# More literal text chunks matched
if (result.text_chunks_matched > maybe_result.text_chunks_matched) or (
(result.text_chunks_matched == maybe_result.text_chunks_matched)
and ("name" in result.unmatched_entities) # prefer entities
):
# More literal text chunks matched, but prefer entities to areas, etc.
maybe_result = result
if (maybe_result is not None) and maybe_result.unmatched_entities:
@ -484,84 +494,93 @@ class DefaultAgent(AbstractConversationAgent):
if lang_intents is None:
intents_dict: dict[str, Any] = {}
loaded_components: set[str] = set()
language_variant: str | None = None
else:
intents_dict = lang_intents.intents_dict
loaded_components = lang_intents.loaded_components
language_variant = lang_intents.language_variant
# en-US, en_US, en, ...
language_variations = list(_get_language_variations(language))
domains_langs = get_domains_and_languages()
# Check if any new components have been loaded
intents_changed = False
for component in hass_components:
if component in loaded_components:
continue
if not language_variant:
# Choose a language variant upfront and commit to it for custom
# sentences, etc.
all_language_variants = {
lang.lower(): lang for lang in itertools.chain(*domains_langs.values())
}
# Don't check component again
loaded_components.add(component)
# Check for intents for this component with the target language.
# Try en-US, en, etc.
for language_variation in language_variations:
component_intents = get_intents(
component, language_variation, json_load=json_load
)
if component_intents:
# Merge sentences into existing dictionary
merge_dict(intents_dict, component_intents)
# Will need to recreate graph
intents_changed = True
_LOGGER.debug(
"Loaded intents component=%s, language=%s (%s)",
component,
language,
language_variation,
)
# en-US, en_US, en, ...
for maybe_variant in _get_language_variations(language):
matching_variant = all_language_variants.get(maybe_variant.lower())
if matching_variant:
language_variant = matching_variant
break
if not language_variant:
_LOGGER.warning(
"Unable to find supported language variant for %s", language
)
return None
# Load intents for all domains supported by this language variant
for domain in domains_langs:
domain_intents = get_intents(
domain, language_variant, json_load=json_load
)
if not domain_intents:
continue
# Merge sentences into existing dictionary
merge_dict(intents_dict, domain_intents)
# Will need to recreate graph
intents_changed = True
_LOGGER.debug(
"Loaded intents domain=%s, language=%s (%s)",
domain,
language,
language_variant,
)
# Check for custom sentences in <config>/custom_sentences/<language>/
if lang_intents is None:
# Only load custom sentences once, otherwise they will be re-loaded
# when components change.
for language_variation in language_variations:
custom_sentences_dir = Path(
self.hass.config.path("custom_sentences", language_variation)
)
if custom_sentences_dir.is_dir():
for custom_sentences_path in custom_sentences_dir.rglob("*.yaml"):
with custom_sentences_path.open(
encoding="utf-8"
) as custom_sentences_file:
# Merge custom sentences
if isinstance(
custom_sentences_yaml := yaml.safe_load(
custom_sentences_file
),
dict,
):
merge_dict(intents_dict, custom_sentences_yaml)
else:
_LOGGER.warning(
"Custom sentences file does not match expected format path=%s",
custom_sentences_file.name,
)
custom_sentences_dir = Path(
self.hass.config.path("custom_sentences", language_variant)
)
if custom_sentences_dir.is_dir():
for custom_sentences_path in custom_sentences_dir.rglob("*.yaml"):
with custom_sentences_path.open(
encoding="utf-8"
) as custom_sentences_file:
# Merge custom sentences
if isinstance(
custom_sentences_yaml := yaml.safe_load(
custom_sentences_file
),
dict,
):
merge_dict(intents_dict, custom_sentences_yaml)
else:
_LOGGER.warning(
"Custom sentences file does not match expected format path=%s",
custom_sentences_file.name,
)
# Will need to recreate graph
intents_changed = True
_LOGGER.debug(
"Loaded custom sentences language=%s (%s), path=%s",
language,
language_variation,
custom_sentences_path,
)
# Stop after first matched language variation
break
# Will need to recreate graph
intents_changed = True
_LOGGER.debug(
"Loaded custom sentences language=%s (%s), path=%s",
language,
language_variant,
custom_sentences_path,
)
# Load sentences from HA config for default language only
if self._config_intents and (language == self.hass.config.language):
if self._config_intents and (
self.hass.config.language in (language, language_variant)
):
merge_dict(
intents_dict,
{
@ -598,7 +617,7 @@ class DefaultAgent(AbstractConversationAgent):
intents_dict,
intent_responses,
error_responses,
loaded_components,
language_variant,
)
self._lang_intents[language] = lang_intents
else: