Report missing entities/areas instead of failing to match in Assist (#107151)

* Report missing entities/areas instead of failing

* Fix test

* Update assist pipeline test snapshots

* Test complete match failure

* Fix conflict
This commit is contained in:
Michael Hansen 2024-01-04 17:09:20 -06:00 committed by GitHub
parent 1a7b06f66a
commit 269500cb29
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 1200 additions and 608 deletions

View file

@ -19,7 +19,12 @@ from hassil.intents import (
TextSlotList,
WildcardSlotList,
)
from hassil.recognize import RecognizeResult, recognize_all
from hassil.recognize import (
RecognizeResult,
UnmatchedEntity,
UnmatchedTextEntity,
recognize_all,
)
from hassil.util import merge_dict
from home_assistant_intents import get_domains_and_languages, get_intents
import yaml
@ -213,6 +218,7 @@ class DefaultAgent(AbstractConversationAgent):
lang_intents = self._lang_intents.get(language)
if result is None:
# Intent was not recognized
_LOGGER.debug("No intent was matched for '%s'", user_input.text)
return _make_error_result(
language,
@ -221,6 +227,28 @@ class DefaultAgent(AbstractConversationAgent):
conversation_id,
)
if result.unmatched_entities:
# Intent was recognized, but not entity/area names, etc.
_LOGGER.debug(
"Recognized intent '%s' for template '%s' but had unmatched: %s",
result.intent.name,
result.intent_sentence.text
if result.intent_sentence is not None
else "",
result.unmatched_entities_list,
)
error_response_type, error_response_args = _get_unmatched_response(
result.unmatched_entities
)
return _make_error_result(
language,
intent.IntentResponseErrorCode.NO_VALID_TARGETS,
self._get_error_text(
error_response_type, lang_intents, **error_response_args
),
conversation_id,
)
# Will never happen because result will be None when no intents are
# loaded in async_recognize.
assert lang_intents is not None
@ -302,8 +330,36 @@ class DefaultAgent(AbstractConversationAgent):
# Keep looking in case an entity has the same name
maybe_result = result
if maybe_result is not None:
# Successful strict match
return maybe_result
# Try again with missing entities enabled
for result in recognize_all(
user_input.text,
lang_intents.intents,
slot_lists=slot_lists,
intent_context=intent_context,
allow_unmatched_entities=True,
):
if maybe_result is None:
# First result
maybe_result = result
elif len(result.unmatched_entities) < len(maybe_result.unmatched_entities):
# 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
maybe_result = result
if (maybe_result is not None) and maybe_result.unmatched_entities:
# Failed to match, but we have more information about why in unmatched_entities
return maybe_result
# Complete match failure
return None
async def _build_speech(
self,
language: str,
@ -655,15 +711,22 @@ class DefaultAgent(AbstractConversationAgent):
return {"area": device_area.id}
def _get_error_text(
self, response_type: ResponseType, lang_intents: LanguageIntents | None
self,
response_type: ResponseType,
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_str = lang_intents.error_responses.get(response_key)
return response_str or _DEFAULT_ERROR_TEXT
response_str = (
lang_intents.error_responses.get(response_key) or _DEFAULT_ERROR_TEXT
)
response_template = template.Template(response_str, self.hass)
return response_template.async_render(response_args)
def register_trigger(
self,
@ -783,6 +846,27 @@ 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] = {}
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
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
return error_response_type, error_response_args
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
"""Collect list reference names recursively."""
if isinstance(expression, Sequence):

View file

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

View file

@ -26,7 +26,7 @@ ha-av==10.1.1
ha-ffmpeg==3.1.0
habluetooth==2.0.2
hass-nabucasa==0.75.1
hassil==1.5.1
hassil==1.5.2
home-assistant-bluetooth==1.11.0
home-assistant-frontend==20240104.0
home-assistant-intents==2024.1.2

View file

@ -1007,7 +1007,7 @@ hass-nabucasa==0.75.1
hass-splunk==0.1.1
# homeassistant.components.conversation
hassil==1.5.1
hassil==1.5.2
# homeassistant.components.jewish_calendar
hdate==0.10.4

View file

@ -809,7 +809,7 @@ habluetooth==2.0.2
hass-nabucasa==0.75.1
# homeassistant.components.conversation
hassil==1.5.1
hassil==1.5.2
# homeassistant.components.jewish_calendar
hdate==0.10.4

View file

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

View file

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

View file

@ -1,4 +1,104 @@
# serializer version: 1
# name: test_custom_agent
dict({
'conversation_id': 'test-conv-id',
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
]),
'targets': list([
]),
}),
'language': 'test-language',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Test response',
}),
}),
}),
})
# ---
# name: test_custom_sentences
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
]),
'targets': list([
]),
}),
'language': 'en-us',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'You ordered a stout',
}),
}),
}),
})
# ---
# name: test_custom_sentences.1
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
]),
'targets': list([
]),
}),
'language': 'en-us',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'You ordered a lager',
}),
}),
}),
})
# ---
# name: test_custom_sentences_config
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Stealth mode engaged',
}),
}),
}),
})
# ---
# name: test_get_agent_info
dict({
'id': 'homeassistant',
@ -225,6 +325,686 @@
]),
})
# ---
# name: test_http_api_handle_failure
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'failed_to_handle',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'An unexpected error occurred while handling the intent',
}),
}),
}),
})
# ---
# name: test_http_api_no_match
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named do something',
}),
}),
}),
})
# ---
# name: test_http_api_unexpected_failure
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'unknown',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'An unexpected error occurred while handling the intent',
}),
}),
}),
})
# ---
# name: test_http_processing_intent[None]
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'kitchen',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent[homeassistant]
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'kitchen',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_alias_added_removed
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'kitchen light',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_alias_added_removed.1
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'kitchen light',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_alias_added_removed.2
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named late added alias',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_conversion_not_expose_new
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No area named kitchen',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_conversion_not_expose_new.1
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'kitchen light',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_added_removed
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'kitchen',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_added_removed.1
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.late',
'name': 'friendly light',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_added_removed.2
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.late',
'name': 'friendly light',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_added_removed.3
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No area named late added',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_exposed
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'kitchen light',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_exposed.1
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'kitchen light',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_exposed.2
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No area named kitchen',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_exposed.3
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No area named my cool',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_exposed.4
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'kitchen light',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_exposed.5
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'kitchen light',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_renamed
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'kitchen light',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_renamed.1
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'renamed light',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_renamed.2
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No area named kitchen',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_renamed.3
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'kitchen light',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_entity_renamed.4
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No area named renamed',
}),
}),
}),
})
# ---
# name: test_http_processing_intent_target_ha_agent
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'failed': list([
]),
'success': list([
dict({
'id': 'light.kitchen',
'name': 'kitchen',
'type': 'entity',
}),
]),
'targets': list([
]),
}),
'language': 'en',
'response_type': 'action_done',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on the light',
}),
}),
}),
})
# ---
# name: test_turn_on_intent[None-turn kitchen on-None]
dict({
'conversation_id': None,
@ -339,7 +1119,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on light',
'speech': 'Turned on the light',
}),
}),
}),
@ -369,7 +1149,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on light',
'speech': 'Turned on the light',
}),
}),
}),
@ -399,7 +1179,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on light',
'speech': 'Turned on the light',
}),
}),
}),
@ -429,7 +1209,7 @@
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'Turned on light',
'speech': 'Turned on the light',
}),
}),
}),
@ -465,6 +1245,126 @@
}),
})
# ---
# name: test_ws_api[payload0]
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test text',
}),
}),
}),
})
# ---
# name: test_ws_api[payload1]
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_intent_match',
}),
'language': 'test-language',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
})
# ---
# name: test_ws_api[payload2]
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test text',
}),
}),
}),
})
# ---
# name: test_ws_api[payload3]
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test text',
}),
}),
}),
})
# ---
# name: test_ws_api[payload4]
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_intent_match',
}),
'language': 'test-language',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': "Sorry, I couldn't understand that",
}),
}),
}),
})
# ---
# name: test_ws_api[payload5]
dict({
'conversation_id': None,
'response': dict({
'card': dict({
}),
'data': dict({
'code': 'no_valid_targets',
}),
'language': 'en',
'response_type': 'error',
'speech': dict({
'plain': dict({
'extra_data': None,
'speech': 'No device or entity named test text',
}),
}),
}),
})
# ---
# name: test_ws_get_agent_info
dict({
'attribution': None,
@ -590,7 +1490,23 @@
}),
}),
}),
None,
dict({
'details': dict({
'domain': dict({
'name': 'domain',
'text': '',
'value': 'script',
}),
}),
'intent': dict({
'name': 'HassTurnOn',
}),
'slots': dict({
'domain': 'script',
}),
'targets': dict({
}),
}),
]),
})
# ---

View file

@ -57,7 +57,7 @@ async def test_hidden_entities_skipped(
assert len(calls) == 0
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
async def test_exposed_domains(hass: HomeAssistant, init_components) -> None:
@ -70,10 +70,10 @@ async def test_exposed_domains(hass: HomeAssistant, init_components) -> None:
hass, "turn on test media player", None, Context(), None
)
# This is an intent match failure instead of a handle failure because the
# media player domain is not exposed.
# This is a match failure instead of a handle failure because the media
# player domain is not exposed.
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
async def test_exposed_areas(
@ -127,9 +127,9 @@ async def test_exposed_areas(
hass, "turn on lights in the bedroom", None, Context(), None
)
# This should be an intent match failure because the area isn't in the slot list
# This should be a match failure because the area isn't in the slot list
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
async def test_conversation_agent(
@ -417,6 +417,48 @@ async def test_device_area_context(
result = await conversation.async_converse(
hass, f"turn {command} all lights", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert (
result.response.error_code
== intent.IntentResponseErrorCode.NO_VALID_TARGETS
)
async def test_error_missing_entity(hass: HomeAssistant, init_components) -> None:
"""Test error message when entity is missing."""
result = await conversation.async_converse(
hass, "turn on missing entity", None, Context(), 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 device or entity named missing entity"
)
async def test_error_missing_area(hass: HomeAssistant, init_components) -> None:
"""Test error message when area is missing."""
result = await conversation.async_converse(
hass, "turn on the lights in missing area", None, Context(), 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"
async def test_error_match_failure(hass: HomeAssistant, init_components) -> None:
"""Test response with complete match failure."""
with patch(
"homeassistant.components.conversation.default_agent.recognize_all",
return_value=[],
):
result = await conversation.async_converse(
hass, "do something", None, Context(), None
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert (
result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH

View file

@ -58,6 +58,7 @@ async def test_http_processing_intent(
hass_admin_user: MockUser,
agent_id,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent via HTTP API."""
# Add an alias
@ -78,27 +79,7 @@ async def test_http_processing_intent(
assert len(calls) == 1
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
async def test_http_processing_intent_target_ha_agent(
@ -108,6 +89,7 @@ async def test_http_processing_intent_target_ha_agent(
hass_admin_user: MockUser,
mock_agent,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent can be processed via HTTP API with picking agent."""
# Add an alias
@ -127,28 +109,8 @@ async def test_http_processing_intent_target_ha_agent(
assert resp.status == HTTPStatus.OK
assert len(calls) == 1
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
async def test_http_processing_intent_entity_added_removed(
@ -157,6 +119,7 @@ async def test_http_processing_intent_entity_added_removed(
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent via HTTP API with entities added later.
@ -179,27 +142,8 @@ async def test_http_processing_intent_entity_added_removed(
assert len(calls) == 1
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Add an entity
entity_registry.async_get_or_create(
@ -215,27 +159,8 @@ async def test_http_processing_intent_entity_added_removed(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.late", "name": "friendly light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Now add an alias
entity_registry.async_update_entity("light.late", aliases={"late added light"})
@ -248,27 +173,8 @@ async def test_http_processing_intent_entity_added_removed(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.late", "name": "friendly light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Now delete the entity
hass.states.async_remove("light.late")
@ -280,21 +186,8 @@ async def test_http_processing_intent_entity_added_removed(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"conversation_id": None,
"response": {
"card": {},
"data": {"code": "no_intent_match"},
"language": hass.config.language,
"response_type": "error",
"speech": {
"plain": {
"extra_data": None,
"speech": "Sorry, I couldn't understand that",
}
},
},
}
assert data == snapshot
assert data["response"]["response_type"] == "error"
async def test_http_processing_intent_alias_added_removed(
@ -303,6 +196,7 @@ async def test_http_processing_intent_alias_added_removed(
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent via HTTP API with aliases added later.
@ -324,27 +218,8 @@ async def test_http_processing_intent_alias_added_removed(
assert len(calls) == 1
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Add an alias
entity_registry.async_update_entity("light.kitchen", aliases={"late added alias"})
@ -357,27 +232,8 @@ async def test_http_processing_intent_alias_added_removed(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Now remove the alieas
entity_registry.async_update_entity("light.kitchen", aliases={})
@ -389,21 +245,8 @@ async def test_http_processing_intent_alias_added_removed(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"conversation_id": None,
"response": {
"card": {},
"data": {"code": "no_intent_match"},
"language": hass.config.language,
"response_type": "error",
"speech": {
"plain": {
"extra_data": None,
"speech": "Sorry, I couldn't understand that",
}
},
},
}
assert data == snapshot
assert data["response"]["response_type"] == "error"
async def test_http_processing_intent_entity_renamed(
@ -413,6 +256,7 @@ async def test_http_processing_intent_entity_renamed(
hass_admin_user: MockUser,
entity_registry: er.EntityRegistry,
enable_custom_integrations: None,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent via HTTP API with entities renamed later.
@ -442,27 +286,8 @@ async def test_http_processing_intent_entity_renamed(
assert len(calls) == 1
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Rename the entity
entity_registry.async_update_entity("light.kitchen", name="renamed light")
@ -476,27 +301,8 @@ async def test_http_processing_intent_entity_renamed(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "renamed light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
client = await hass_client()
resp = await client.post(
@ -505,21 +311,8 @@ async def test_http_processing_intent_entity_renamed(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"conversation_id": None,
"response": {
"card": {},
"data": {"code": "no_intent_match"},
"language": hass.config.language,
"response_type": "error",
"speech": {
"plain": {
"extra_data": None,
"speech": "Sorry, I couldn't understand that",
}
},
},
}
assert data == snapshot
assert data["response"]["response_type"] == "error"
# Now clear the custom name
entity_registry.async_update_entity("light.kitchen", name=None)
@ -533,27 +326,8 @@ async def test_http_processing_intent_entity_renamed(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
client = await hass_client()
resp = await client.post(
@ -562,21 +336,8 @@ async def test_http_processing_intent_entity_renamed(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"conversation_id": None,
"response": {
"card": {},
"data": {"code": "no_intent_match"},
"language": hass.config.language,
"response_type": "error",
"speech": {
"plain": {
"extra_data": None,
"speech": "Sorry, I couldn't understand that",
}
},
},
}
assert data == snapshot
assert data["response"]["response_type"] == "error"
async def test_http_processing_intent_entity_exposed(
@ -586,6 +347,7 @@ async def test_http_processing_intent_entity_exposed(
hass_admin_user: MockUser,
entity_registry: er.EntityRegistry,
enable_custom_integrations: None,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent via HTTP API with manual expose.
@ -617,27 +379,8 @@ async def test_http_processing_intent_entity_exposed(
assert len(calls) == 1
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
client = await hass_client()
@ -649,27 +392,8 @@ async def test_http_processing_intent_entity_exposed(
assert len(calls) == 1
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
# Unexpose the entity
expose_entity(hass, "light.kitchen", False)
@ -682,21 +406,8 @@ async def test_http_processing_intent_entity_exposed(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"conversation_id": None,
"response": {
"card": {},
"data": {"code": "no_intent_match"},
"language": hass.config.language,
"response_type": "error",
"speech": {
"plain": {
"extra_data": None,
"speech": "Sorry, I couldn't understand that",
}
},
},
}
assert data == snapshot
assert data["response"]["response_type"] == "error"
client = await hass_client()
resp = await client.post(
@ -705,21 +416,8 @@ async def test_http_processing_intent_entity_exposed(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"conversation_id": None,
"response": {
"card": {},
"data": {"code": "no_intent_match"},
"language": hass.config.language,
"response_type": "error",
"speech": {
"plain": {
"extra_data": None,
"speech": "Sorry, I couldn't understand that",
}
},
},
}
assert data == snapshot
assert data["response"]["response_type"] == "error"
# Now expose the entity
expose_entity(hass, "light.kitchen", True)
@ -733,27 +431,8 @@ async def test_http_processing_intent_entity_exposed(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
client = await hass_client()
resp = await client.post(
@ -762,27 +441,8 @@ async def test_http_processing_intent_entity_exposed(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
async def test_http_processing_intent_conversion_not_expose_new(
@ -792,6 +452,7 @@ async def test_http_processing_intent_conversion_not_expose_new(
hass_admin_user: MockUser,
entity_registry: er.EntityRegistry,
enable_custom_integrations: None,
snapshot: SnapshotAssertion,
) -> None:
"""Test processing intent via HTTP API when not exposing new entities."""
# Disable exposing new entities to the default agent
@ -820,21 +481,8 @@ async def test_http_processing_intent_conversion_not_expose_new(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"conversation_id": None,
"response": {
"card": {},
"data": {"code": "no_intent_match"},
"language": hass.config.language,
"response_type": "error",
"speech": {
"plain": {
"extra_data": None,
"speech": "Sorry, I couldn't understand that",
}
},
},
}
assert data == snapshot
assert data["response"]["response_type"] == "error"
# Expose the entity
expose_entity(hass, "light.kitchen", True)
@ -848,27 +496,8 @@ async def test_http_processing_intent_conversion_not_expose_new(
assert len(calls) == 1
data = await resp.json()
assert data == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Turned on the light",
}
},
"language": hass.config.language,
"data": {
"targets": [],
"success": [
{"id": "light.kitchen", "name": "kitchen light", "type": "entity"}
],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
@pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS)
@ -936,7 +565,10 @@ async def test_turn_off_intent(hass: HomeAssistant, init_components, sentence) -
async def test_http_api_no_match(
hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test the HTTP conversation API with an intent match failure."""
client = await hass_client()
@ -947,25 +579,15 @@ async def test_http_api_no_match(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"response_type": "error",
"card": {},
"speech": {
"plain": {
"speech": "Sorry, I couldn't understand that",
"extra_data": None,
},
},
"language": hass.config.language,
"data": {"code": "no_intent_match"},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "error"
async def test_http_api_handle_failure(
hass: HomeAssistant, init_components, hass_client: ClientSessionGenerator
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test the HTTP conversation API with an error during handling."""
client = await hass_client()
@ -984,29 +606,16 @@ async def test_http_api_handle_failure(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"response_type": "error",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "An unexpected error occurred while handling the intent",
}
},
"language": hass.config.language,
"data": {
"code": "failed_to_handle",
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "error"
assert data["response"]["data"]["code"] == "failed_to_handle"
async def test_http_api_unexpected_failure(
hass: HomeAssistant,
init_components,
hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test the HTTP conversation API with an unexpected error during handling."""
client = await hass_client()
@ -1025,23 +634,9 @@ async def test_http_api_unexpected_failure(
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"response_type": "error",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "An unexpected error occurred while handling the intent",
}
},
"language": hass.config.language,
"data": {
"code": "unknown",
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "error"
assert data["response"]["data"]["code"] == "unknown"
async def test_http_api_wrong_data(
@ -1062,6 +657,7 @@ async def test_custom_agent(
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
mock_agent,
snapshot: SnapshotAssertion,
) -> None:
"""Test a custom conversation agent."""
assert await async_setup_component(hass, "homeassistant", {})
@ -1079,21 +675,11 @@ async def test_custom_agent(
resp = await client.post("/api/conversation/process", json=data)
assert resp.status == HTTPStatus.OK
assert await resp.json() == {
"response": {
"response_type": "action_done",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Test response",
}
},
"language": "test-language",
"data": {"targets": [], "success": [], "failed": []},
},
"conversation_id": "test-conv-id",
}
data = await resp.json()
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
assert data["response"]["speech"]["plain"]["speech"] == "Test response"
assert data["conversation_id"] == "test-conv-id"
assert len(mock_agent.calls) == 1
assert mock_agent.calls[0].text == "Test Text"
@ -1136,7 +722,10 @@ async def test_custom_agent(
],
)
async def test_ws_api(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator, payload
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
payload,
snapshot: SnapshotAssertion,
) -> None:
"""Test the Websocket conversation API."""
assert await async_setup_component(hass, "homeassistant", {})
@ -1148,21 +737,7 @@ async def test_ws_api(
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == {
"response": {
"response_type": "error",
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Sorry, I couldn't understand that",
}
},
"language": payload.get("language", hass.config.language),
"data": {"code": "no_intent_match"},
},
"conversation_id": None,
}
assert msg["result"] == snapshot
@pytest.mark.parametrize("agent_id", AGENT_ID_OPTIONS)
@ -1198,7 +773,10 @@ async def test_ws_prepare(
async def test_custom_sentences(
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
snapshot: SnapshotAssertion,
) -> None:
"""Test custom sentences with a custom intent."""
assert await async_setup_component(hass, "homeassistant", {})
@ -1223,30 +801,19 @@ async def test_custom_sentences(
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": f"You ordered a {beer_style}",
}
},
"language": language,
"response_type": "action_done",
"data": {
"targets": [],
"success": [],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
assert (
data["response"]["speech"]["plain"]["speech"]
== f"You ordered a {beer_style}"
)
async def test_custom_sentences_config(
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
snapshot: SnapshotAssertion,
) -> None:
"""Test custom sentences with a custom intent in config."""
assert await async_setup_component(hass, "homeassistant", {})
@ -1274,26 +841,9 @@ async def test_custom_sentences_config(
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data == {
"response": {
"card": {},
"speech": {
"plain": {
"extra_data": None,
"speech": "Stealth mode engaged",
}
},
"language": hass.config.language,
"response_type": "action_done",
"data": {
"targets": [],
"success": [],
"failed": [],
},
},
"conversation_id": None,
}
assert data == snapshot
assert data["response"]["response_type"] == "action_done"
assert data["response"]["speech"]["plain"]["speech"] == "Stealth mode engaged"
async def test_prepare_reload(hass: HomeAssistant) -> None:

View file

@ -192,7 +192,7 @@ async def test_cant_turn_on_lock(hass: HomeAssistant) -> None:
)
assert result.response.response_type == intent.IntentResponseType.ERROR
assert result.response.error_code == intent.IntentResponseErrorCode.NO_INTENT_MATCH
assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS
def test_async_register(hass: HomeAssistant) -> None: