Prioritize custom intents over builtin (#120358)
parent
6689dbbcc6
commit
46dcf1dc44
|
@ -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()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue