Prioritize custom intents over builtin (#120358)

pull/120376/head
Michael Hansen 2024-06-24 13:57:56 -05:00 committed by GitHub
parent 6689dbbcc6
commit 46dcf1dc44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 134 additions and 1 deletions

View File

@ -419,6 +419,7 @@ class DefaultAgent(ConversationEntity):
language: str, language: str,
) -> RecognizeResult | None: ) -> RecognizeResult | None:
"""Search intents for a match to user input.""" """Search intents for a match to user input."""
custom_result: RecognizeResult | None = None
name_result: RecognizeResult | None = None name_result: RecognizeResult | None = None
best_results: list[RecognizeResult] = [] best_results: list[RecognizeResult] = []
best_text_chunks_matched: int | None = None best_text_chunks_matched: int | None = None
@ -429,6 +430,20 @@ class DefaultAgent(ConversationEntity):
intent_context=intent_context, intent_context=intent_context,
language=language, language=language,
): ):
# User intents have highest priority
if (result.intent_metadata is not None) and result.intent_metadata.get(
METADATA_CUSTOM_SENTENCE
):
if (custom_result is None) or (
result.text_chunks_matched > custom_result.text_chunks_matched
):
custom_result = result
# Clear builtin results
best_results = []
name_result = None
continue
# Prioritize results with a "name" slot, but still prefer ones with # Prioritize results with a "name" slot, but still prefer ones with
# more literal text matched. # more literal text matched.
if ( if (
@ -453,6 +468,10 @@ class DefaultAgent(ConversationEntity):
# We will resolve the ambiguity below. # We will resolve the ambiguity below.
best_results.append(result) best_results.append(result)
if custom_result is not None:
# Prioritize user intents
return custom_result
if name_result is not None: if name_result is not None:
# Prioritize matches with entity names above area names # Prioritize matches with entity names above area names
return name_result return name_result
@ -718,11 +737,22 @@ class DefaultAgent(ConversationEntity):
if self._config_intents and ( if self._config_intents and (
self.hass.config.language in (language, language_variant) self.hass.config.language in (language, language_variant)
): ):
hass_config_path = self.hass.config.path()
merge_dict( merge_dict(
intents_dict, intents_dict,
{ {
"intents": { "intents": {
intent_name: {"data": [{"sentences": sentences}]} intent_name: {
"data": [
{
"sentences": sentences,
"metadata": {
METADATA_CUSTOM_SENTENCE: True,
METADATA_CUSTOM_FILE: hass_config_path,
},
}
]
}
for intent_name, sentences in self._config_intents.items() for intent_name, sentences in self._config_intents.items()
} }
}, },

View File

@ -1,12 +1,15 @@
"""The tests for the Conversation component.""" """The tests for the Conversation component."""
from http import HTTPStatus from http import HTTPStatus
import os
import tempfile
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
import voluptuous as vol import voluptuous as vol
import yaml
from homeassistant.components import conversation from homeassistant.components import conversation
from homeassistant.components.conversation import default_agent from homeassistant.components.conversation import default_agent
@ -1389,3 +1392,103 @@ async def test_ws_hass_agent_debug_sentence_trigger(
# Trigger should not have been executed # Trigger should not have been executed
assert len(calls) == 0 assert len(calls) == 0
async def test_custom_sentences_priority(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
snapshot: SnapshotAssertion,
) -> None:
"""Test that user intents from custom_sentences have priority over builtin intents/sentences."""
with tempfile.NamedTemporaryFile(
mode="w+",
encoding="utf-8",
suffix=".yaml",
dir=os.path.join(hass.config.config_dir, "custom_sentences", "en"),
) as custom_sentences_file:
# Add a custom sentence that would match a builtin sentence.
# Custom sentences have priority.
yaml.dump(
{
"language": "en",
"intents": {
"CustomIntent": {"data": [{"sentences": ["turn on the lamp"]}]}
},
},
custom_sentences_file,
)
custom_sentences_file.flush()
custom_sentences_file.seek(0)
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "conversation", {})
assert await async_setup_component(hass, "light", {})
assert await async_setup_component(hass, "intent", {})
assert await async_setup_component(
hass,
"intent_script",
{
"intent_script": {
"CustomIntent": {"speech": {"text": "custom response"}}
}
},
)
# Ensure that a "lamp" exists so that we can verify the custom intent
# overrides the builtin sentence.
hass.states.async_set("light.lamp", "off")
client = await hass_client()
resp = await client.post(
"/api/conversation/process",
json={
"text": "turn on the lamp",
"language": hass.config.language,
},
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data["response"]["response_type"] == "action_done"
assert data["response"]["speech"]["plain"]["speech"] == "custom response"
async def test_config_sentences_priority(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
hass_admin_user: MockUser,
snapshot: SnapshotAssertion,
) -> None:
"""Test that user intents from configuration.yaml have priority over builtin intents/sentences."""
# Add a custom sentence that would match a builtin sentence.
# Custom sentences have priority.
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, "intent", {})
assert await async_setup_component(
hass,
"conversation",
{"conversation": {"intents": {"CustomIntent": ["turn on the lamp"]}}},
)
assert await async_setup_component(hass, "light", {})
assert await async_setup_component(
hass,
"intent_script",
{"intent_script": {"CustomIntent": {"speech": {"text": "custom response"}}}},
)
# Ensure that a "lamp" exists so that we can verify the custom intent
# overrides the builtin sentence.
hass.states.async_set("light.lamp", "off")
client = await hass_client()
resp = await client.post(
"/api/conversation/process",
json={
"text": "turn on the lamp",
"language": hass.config.language,
},
)
assert resp.status == HTTPStatus.OK
data = await resp.json()
assert data["response"]["response_type"] == "action_done"
assert data["response"]["speech"]["plain"]["speech"] == "custom response"