Intents package combines sentences/responses per language (#109079)

pull/109123/head
Michael Hansen 2024-01-30 05:38:29 -06:00 committed by GitHub
parent a1f36c25d4
commit 9752e70675
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 107 additions and 119 deletions

View File

@ -6,7 +6,6 @@ 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
@ -28,7 +27,7 @@ from hassil.recognize import (
recognize_all,
)
from hassil.util import merge_dict
from home_assistant_intents import get_domains_and_languages, get_intents
from home_assistant_intents import get_intents, get_languages
import yaml
from homeassistant import core, setup
@ -156,7 +155,7 @@ class DefaultAgent(AbstractConversationAgent):
@property
def supported_languages(self) -> list[str]:
"""Return a list of supported languages."""
return get_domains_and_languages()["homeassistant"]
return get_languages()
async def async_initialize(self, config_intents: dict[str, Any] | None) -> None:
"""Initialize the default agent."""
@ -387,6 +386,7 @@ class DefaultAgent(AbstractConversationAgent):
return maybe_result
# Try again with missing entities enabled
best_num_unmatched_entities = 0
for result in recognize_all(
user_input.text,
lang_intents.intents,
@ -394,20 +394,28 @@ 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 result.text_chunks_matched < 1:
# Skip results that don't match any literal text
continue
# Don't count missing entities that couldn't be filled from context
num_unmatched_entities = 0
for entity in result.unmatched_entities_list:
if isinstance(entity, UnmatchedTextEntity):
if entity.text != MISSING_ENTITY:
num_unmatched_entities += 1
else:
num_unmatched_entities += 1
if maybe_result is None:
# First result
maybe_result = result
elif len(result.unmatched_entities) < len(maybe_result.unmatched_entities):
best_num_unmatched_entities = num_unmatched_entities
elif num_unmatched_entities < best_num_unmatched_entities:
# Fewer unmatched entities
maybe_result = result
elif len(result.unmatched_entities) == len(maybe_result.unmatched_entities):
best_num_unmatched_entities = num_unmatched_entities
elif num_unmatched_entities == best_num_unmatched_entities:
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
@ -536,14 +544,12 @@ class DefaultAgent(AbstractConversationAgent):
intents_dict = lang_intents.intents_dict
language_variant = lang_intents.language_variant
domains_langs = get_domains_and_languages()
supported_langs = set(get_languages())
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())
}
all_language_variants = {lang.lower(): lang for lang in supported_langs}
# en-US, en_US, en, ...
for maybe_variant in _get_language_variations(language):
@ -558,23 +564,17 @@ class DefaultAgent(AbstractConversationAgent):
)
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
# Load intents for this language variant
lang_variant_intents = get_intents(language_variant, json_load=json_load)
if lang_variant_intents:
# Merge sentences into existing dictionary
merge_dict(intents_dict, domain_intents)
merge_dict(intents_dict, lang_variant_intents)
# Will need to recreate graph
intents_changed = True
_LOGGER.debug(
"Loaded intents domain=%s, language=%s (%s)",
domain,
"Loaded intents language=%s (%s)",
language,
language_variant,
)

View File

@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["hassil==1.6.0", "home-assistant-intents==2024.1.2"]
"requirements": ["hassil==1.6.0", "home-assistant-intents==2024.1.29"]
}

View File

@ -29,7 +29,7 @@ hass-nabucasa==0.75.1
hassil==1.6.0
home-assistant-bluetooth==1.12.0
home-assistant-frontend==20240112.0
home-assistant-intents==2024.1.2
home-assistant-intents==2024.1.29
httpx==0.26.0
ifaddr==0.2.0
janus==1.0.0

View File

@ -1056,7 +1056,7 @@ holidays==0.41
home-assistant-frontend==20240112.0
# homeassistant.components.conversation
home-assistant-intents==2024.1.2
home-assistant-intents==2024.1.29
# homeassistant.components.home_connect
homeconnect==0.7.2

View File

@ -849,7 +849,7 @@ holidays==0.41
home-assistant-frontend==20240112.0
# homeassistant.components.conversation
home-assistant-intents==2024.1.2
home-assistant-intents==2024.1.29
# homeassistant.components.home_connect
homeconnect==0.7.2

View File

@ -48,14 +48,14 @@
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
'code': 'no_intent_match',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test transcript',
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
@ -67,7 +67,7 @@
'data': dict({
'engine': 'test',
'language': 'en-US',
'tts_input': 'No device or entity named test transcript',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'james_earl_jones',
}),
'type': <PipelineEventType.TTS_START: 'tts-start'>,
@ -75,9 +75,9 @@
dict({
'data': dict({
'tts_output': dict({
'media_id': 'media-source://tts/test?message=No+device+or+entity+named+test+transcript&language=en-US&voice=james_earl_jones',
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&voice=james_earl_jones",
'mime_type': 'audio/mpeg',
'url': '/api/tts_proxy/e5e8e318b536f0a5455f993243a34521e7ad4d6d_en-us_031e2ec052_test.mp3',
'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3',
}),
}),
'type': <PipelineEventType.TTS_END: 'tts-end'>,
@ -137,14 +137,14 @@
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
'code': 'no_intent_match',
}),
'language': 'en-US',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test transcript',
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
@ -156,7 +156,7 @@
'data': dict({
'engine': 'test',
'language': 'en-US',
'tts_input': 'No device or entity named test transcript',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'Arnold Schwarzenegger',
}),
'type': <PipelineEventType.TTS_START: 'tts-start'>,
@ -164,9 +164,9 @@
dict({
'data': dict({
'tts_output': dict({
'media_id': 'media-source://tts/test?message=No+device+or+entity+named+test+transcript&language=en-US&voice=Arnold+Schwarzenegger',
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&voice=Arnold+Schwarzenegger",
'mime_type': 'audio/mpeg',
'url': '/api/tts_proxy/e5e8e318b536f0a5455f993243a34521e7ad4d6d_en-us_2657c1a8ee_test.mp3',
'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_2657c1a8ee_test.mp3',
}),
}),
'type': <PipelineEventType.TTS_END: 'tts-end'>,
@ -226,14 +226,14 @@
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
'code': 'no_intent_match',
}),
'language': 'en-US',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test transcript',
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
@ -245,7 +245,7 @@
'data': dict({
'engine': 'test',
'language': 'en-US',
'tts_input': 'No device or entity named test transcript',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'Arnold Schwarzenegger',
}),
'type': <PipelineEventType.TTS_START: 'tts-start'>,
@ -253,9 +253,9 @@
dict({
'data': dict({
'tts_output': dict({
'media_id': 'media-source://tts/test?message=No+device+or+entity+named+test+transcript&language=en-US&voice=Arnold+Schwarzenegger',
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&voice=Arnold+Schwarzenegger",
'mime_type': 'audio/mpeg',
'url': '/api/tts_proxy/e5e8e318b536f0a5455f993243a34521e7ad4d6d_en-us_2657c1a8ee_test.mp3',
'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_2657c1a8ee_test.mp3',
}),
}),
'type': <PipelineEventType.TTS_END: 'tts-end'>,
@ -338,14 +338,14 @@
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
'code': 'no_intent_match',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test transcript',
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
@ -357,7 +357,7 @@
'data': dict({
'engine': 'test',
'language': 'en-US',
'tts_input': 'No device or entity named test transcript',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'james_earl_jones',
}),
'type': <PipelineEventType.TTS_START: 'tts-start'>,
@ -365,9 +365,9 @@
dict({
'data': dict({
'tts_output': dict({
'media_id': 'media-source://tts/test?message=No+device+or+entity+named+test+transcript&language=en-US&voice=james_earl_jones',
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&voice=james_earl_jones",
'mime_type': 'audio/mpeg',
'url': '/api/tts_proxy/e5e8e318b536f0a5455f993243a34521e7ad4d6d_en-us_031e2ec052_test.mp3',
'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3',
}),
}),
'type': <PipelineEventType.TTS_END: 'tts-end'>,

View File

@ -46,14 +46,14 @@
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
'code': 'no_intent_match',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test transcript',
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
@ -64,16 +64,16 @@
dict({
'engine': 'test',
'language': 'en-US',
'tts_input': 'No device or entity named test transcript',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'james_earl_jones',
})
# ---
# name: test_audio_pipeline.6
dict({
'tts_output': dict({
'media_id': 'media-source://tts/test?message=No+device+or+entity+named+test+transcript&language=en-US&voice=james_earl_jones',
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&voice=james_earl_jones",
'mime_type': 'audio/mpeg',
'url': '/api/tts_proxy/e5e8e318b536f0a5455f993243a34521e7ad4d6d_en-us_031e2ec052_test.mp3',
'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3',
}),
})
# ---
@ -127,14 +127,14 @@
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
'code': 'no_intent_match',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test transcript',
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
@ -145,16 +145,16 @@
dict({
'engine': 'test',
'language': 'en-US',
'tts_input': 'No device or entity named test transcript',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'james_earl_jones',
})
# ---
# name: test_audio_pipeline_debug.6
dict({
'tts_output': dict({
'media_id': 'media-source://tts/test?message=No+device+or+entity+named+test+transcript&language=en-US&voice=james_earl_jones',
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&voice=james_earl_jones",
'mime_type': 'audio/mpeg',
'url': '/api/tts_proxy/e5e8e318b536f0a5455f993243a34521e7ad4d6d_en-us_031e2ec052_test.mp3',
'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3',
}),
})
# ---
@ -220,14 +220,14 @@
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
'code': 'no_intent_match',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test transcript',
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
@ -238,16 +238,16 @@
dict({
'engine': 'test',
'language': 'en-US',
'tts_input': 'No device or entity named test transcript',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'james_earl_jones',
})
# ---
# name: test_audio_pipeline_with_enhancements.6
dict({
'tts_output': dict({
'media_id': 'media-source://tts/test?message=No+device+or+entity+named+test+transcript&language=en-US&voice=james_earl_jones',
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&voice=james_earl_jones",
'mime_type': 'audio/mpeg',
'url': '/api/tts_proxy/e5e8e318b536f0a5455f993243a34521e7ad4d6d_en-us_031e2ec052_test.mp3',
'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3',
}),
})
# ---
@ -421,14 +421,14 @@
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
'code': 'no_intent_match',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test transcript',
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
@ -439,16 +439,16 @@
dict({
'engine': 'test',
'language': 'en-US',
'tts_input': 'No device or entity named test transcript',
'tts_input': "Sorry, I couldn't understand that",
'voice': 'james_earl_jones',
})
# ---
# name: test_audio_pipeline_with_wake_word_no_timeout.8
dict({
'tts_output': dict({
'media_id': 'media-source://tts/test?message=No+device+or+entity+named+test+transcript&language=en-US&voice=james_earl_jones',
'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&voice=james_earl_jones",
'mime_type': 'audio/mpeg',
'url': '/api/tts_proxy/e5e8e318b536f0a5455f993243a34521e7ad4d6d_en-us_031e2ec052_test.mp3',
'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3',
}),
})
# ---
@ -778,7 +778,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No area named are',
'speech': 'Sorry, I am not aware of any area called are',
}),
}),
}),

View File

@ -352,14 +352,14 @@
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
'code': 'no_intent_match',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named do something',
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
@ -519,7 +519,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named late added alias',
'speech': 'Sorry, I am not aware of any device or entity called late added alias',
}),
}),
}),
@ -539,7 +539,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named kitchen light',
'speech': 'Sorry, I am not aware of any device or entity called kitchen light',
}),
}),
}),
@ -679,7 +679,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named late added light',
'speech': 'Sorry, I am not aware of any device or entity called late added light',
}),
}),
}),
@ -759,7 +759,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named kitchen light',
'speech': 'Sorry, I am not aware of any device or entity called kitchen light',
}),
}),
}),
@ -779,7 +779,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named my cool light',
'speech': 'Sorry, I am not aware of any device or entity called my cool light',
}),
}),
}),
@ -919,7 +919,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named kitchen light',
'speech': 'Sorry, I am not aware of any device or entity called kitchen light',
}),
}),
}),
@ -969,7 +969,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named renamed light',
'speech': 'Sorry, I am not aware of any device or entity called renamed light',
}),
}),
}),
@ -1252,14 +1252,14 @@
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
'code': 'no_intent_match',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test text',
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
@ -1292,14 +1292,14 @@
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
'code': 'no_intent_match',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test text',
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
@ -1312,14 +1312,14 @@
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
'code': 'no_intent_match',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test text',
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
@ -1352,14 +1352,14 @@
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
'code': 'no_intent_match',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test text',
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
@ -1510,29 +1510,7 @@
'unmatched_slots': dict({
}),
}),
dict({
'details': dict({
'domain': dict({
'name': 'domain',
'text': '',
'value': 'scene',
}),
}),
'intent': dict({
'name': 'HassTurnOn',
}),
'match': False,
'sentence_template': '[activate|<turn>] <name> [scene] [on]',
'slots': dict({
'domain': 'scene',
}),
'source': 'builtin',
'targets': dict({
}),
'unmatched_slots': dict({
'name': 'this will not match anything',
}),
}),
None,
]),
})
# ---

View File

@ -147,8 +147,8 @@ async def test_conversation_agent(
conversation.HOME_ASSISTANT_AGENT
)
with patch(
"homeassistant.components.conversation.default_agent.get_domains_and_languages",
return_value={"homeassistant": ["dwarvish", "elvish", "entish"]},
"homeassistant.components.conversation.default_agent.get_languages",
return_value=["dwarvish", "elvish", "entish"],
):
assert agent.supported_languages == ["dwarvish", "elvish", "entish"]
@ -440,7 +440,7 @@ async def test_error_missing_entity(hass: HomeAssistant, init_components) -> Non
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
== "No device or entity named missing entity"
== "Sorry, I am not aware of any device or entity called missing entity"
)
@ -452,7 +452,10 @@ async def test_error_missing_area(hass: HomeAssistant, init_components) -> None:
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert result.response.speech["plain"]["speech"] == "No area named missing area"
assert (
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any area called missing area"
)
async def test_error_no_exposed_for_domain(
@ -467,7 +470,8 @@ async def test_error_no_exposed_for_domain(
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"] == "kitchen does not contain a light"
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any light in the kitchen area"
)
@ -483,7 +487,8 @@ async def test_error_no_exposed_for_device_class(
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"] == "bedroom does not contain a window"
result.response.speech["plain"]["speech"]
== "Sorry, I am not aware of any window in the bedroom area"
)
@ -596,5 +601,5 @@ async def test_all_domains_loaded(
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
assert (
result.response.speech["plain"]["speech"]
== "No device or entity named test light"
== "Sorry, I am not aware of any device or entity called test light"
)

View File

@ -581,6 +581,7 @@ async def test_http_api_no_match(
assert data == snapshot
assert data["response"]["response_type"] == "error"
assert data["response"]["data"]["code"] == "no_intent_match"
async def test_http_api_handle_failure(
@ -738,6 +739,7 @@ async def test_ws_api(
assert msg["success"]
assert msg["result"] == snapshot
assert msg["result"]["response"]["data"]["code"] == "no_intent_match"
@pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS)
@ -1180,7 +1182,7 @@ async def test_ws_hass_agent_debug(
"turn my cool light off",
"turn on all lights in the kitchen",
"how many lights are on in the kitchen?",
"this will not match anything", # unmatched in results
"this will not match anything", # None in results
],
}
)
@ -1190,6 +1192,9 @@ async def test_ws_hass_agent_debug(
assert msg["success"]
assert msg["result"] == snapshot
# Last sentence should be a failed match
assert msg["result"]["results"][-1] is None
# Light state should not have been changed
assert len(on_calls) == 0
assert len(off_calls) == 0