Add websocket command to test intent recognition for default agent (#94674)

* Add websocket command to test intent recognition for default agent

* Return results as a list

* Only check intent name/entities in test

* Less verbose output in debug API
This commit is contained in:
Michael Hansen 2023-06-22 11:24:59 -05:00 committed by GitHub
parent 1459bf4011
commit 38614bc3f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 159 additions and 11 deletions

View File

@ -186,6 +186,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
websocket_api.async_register_command(hass, websocket_prepare)
websocket_api.async_register_command(hass, websocket_get_agent_info)
websocket_api.async_register_command(hass, websocket_list_agents)
websocket_api.async_register_command(hass, websocket_hass_agent_debug)
return True
@ -297,6 +298,60 @@ async def websocket_list_agents(
connection.send_message(websocket_api.result_message(msg["id"], {"agents": agents}))
@websocket_api.websocket_command(
{
vol.Required("type"): "conversation/agent/homeassistant/debug",
vol.Required("sentences"): [str],
vol.Optional("language"): str,
vol.Optional("device_id"): vol.Any(str, None),
}
)
@websocket_api.async_response
async def websocket_hass_agent_debug(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict
) -> None:
"""Return intents that would be matched by the default agent for a list of sentences."""
agent = await _get_agent_manager(hass).async_get_agent(HOME_ASSISTANT_AGENT)
assert isinstance(agent, DefaultAgent)
results = [
await agent.async_recognize(
ConversationInput(
text=sentence,
context=connection.context(msg),
conversation_id=None,
device_id=msg.get("device_id"),
language=msg.get("language", hass.config.language),
)
)
for sentence in msg["sentences"]
]
# Return results for each sentence in the same order as the input.
connection.send_result(
msg["id"],
{
"results": [
{
"intent": {
"name": result.intent.name,
},
"entities": {
entity_key: {
"name": entity.name,
"value": entity.value,
"text": entity.text,
}
for entity_key, entity in result.entities.items()
},
}
if result is not None
else None
for result in results
]
},
)
class ConversationProcessView(http.HomeAssistantView):
"""View to process text."""

View File

@ -143,11 +143,12 @@ class DefaultAgent(AbstractConversationAgent):
self.hass, DOMAIN, self._async_exposed_entities_updated
)
async def async_process(self, user_input: ConversationInput) -> ConversationResult:
"""Process a sentence."""
async def async_recognize(
self, user_input: ConversationInput
) -> RecognizeResult | None:
"""Recognize intent from user input."""
language = user_input.language or self.hass.config.language
lang_intents = self._lang_intents.get(language)
conversation_id = None # Not supported
# Reload intents if missing or new components
if lang_intents is None or (
@ -159,21 +160,26 @@ class DefaultAgent(AbstractConversationAgent):
if lang_intents is None:
# No intents loaded
_LOGGER.warning("No intents were loaded for language: %s", language)
return _make_error_result(
language,
intent.IntentResponseErrorCode.NO_INTENT_MATCH,
_DEFAULT_ERROR_TEXT,
conversation_id,
)
return None
slot_lists = self._make_slot_lists()
result = await self.hass.async_add_executor_job(
self._recognize,
user_input,
lang_intents,
slot_lists,
)
return result
async def async_process(self, user_input: ConversationInput) -> ConversationResult:
"""Process a sentence."""
language = user_input.language or self.hass.config.language
conversation_id = None # Not supported
result = await self.async_recognize(user_input)
lang_intents = self._lang_intents.get(language)
if result is None:
_LOGGER.debug("No intent was matched for '%s'", user_input.text)
return _make_error_result(
@ -183,6 +189,10 @@ class DefaultAgent(AbstractConversationAgent):
conversation_id,
)
# Will never happen because result will be None when no intents are
# loaded in async_recognize.
assert lang_intents is not None
try:
intent_response = await intent.async_handle(
self.hass,
@ -585,9 +595,12 @@ class DefaultAgent(AbstractConversationAgent):
return self._slot_lists
def _get_error_text(
self, response_type: ResponseType, lang_intents: LanguageIntents
self, response_type: ResponseType, lang_intents: LanguageIntents | None
) -> 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

View File

@ -249,3 +249,43 @@
'message': "invalid agent ID for dictionary value @ data['agent_id']. Got 'not_exist'",
})
# ---
# name: test_ws_hass_agent_debug
dict({
'results': list([
dict({
'entities': dict({
'name': dict({
'name': 'name',
'text': 'my cool light',
'value': 'my cool light',
}),
}),
'intent': dict({
'name': 'HassTurnOn',
}),
}),
dict({
'entities': dict({
'name': dict({
'name': 'name',
'text': 'my cool light',
'value': 'my cool light',
}),
}),
'intent': dict({
'name': 'HassTurnOff',
}),
}),
None,
]),
})
# ---
# name: test_ws_hass_agent_debug.1
dict({
'name': dict({
'name': 'name',
'text': 'my cool light',
'value': 'my cool light',
}),
})
# ---

View File

@ -1626,3 +1626,43 @@ async def test_ws_get_agent_info(
msg = await client.receive_json()
assert not msg["success"]
assert msg["error"] == snapshot
async def test_ws_hass_agent_debug(
hass: HomeAssistant,
init_components,
hass_ws_client: WebSocketGenerator,
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
) -> None:
"""Test homeassistant agent debug websocket command."""
client = await hass_ws_client(hass)
entity_registry.async_get_or_create(
"light", "demo", "1234", suggested_object_id="kitchen"
)
entity_registry.async_update_entity("light.kitchen", aliases={"my cool light"})
hass.states.async_set("light.kitchen", "off")
on_calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_on")
off_calls = async_mock_service(hass, LIGHT_DOMAIN, "turn_off")
await client.send_json_auto_id(
{
"type": "conversation/agent/homeassistant/debug",
"sentences": [
"turn on my cool light",
"turn my cool light off",
"this will not match anything", # null in results
],
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == snapshot
# Light state should not have been changed
assert len(on_calls) == 0
assert len(off_calls) == 0