Upgrade to hassil 2.0 (#130544)
* Working on hassil 2.0 * Bump to hassil 2.0 * Update snapshots * Remove debug logging
This commit is contained in:
parent
4002bc3c25
commit
51c6ee97b1
12 changed files with 53 additions and 102 deletions
|
@ -16,11 +16,11 @@ from hassil.expression import Expression, ListReference, Sequence
|
||||||
from hassil.intents import Intents, SlotList, TextSlotList, WildcardSlotList
|
from hassil.intents import Intents, SlotList, TextSlotList, WildcardSlotList
|
||||||
from hassil.recognize import (
|
from hassil.recognize import (
|
||||||
MISSING_ENTITY,
|
MISSING_ENTITY,
|
||||||
MatchEntity,
|
|
||||||
RecognizeResult,
|
RecognizeResult,
|
||||||
UnmatchedTextEntity,
|
|
||||||
recognize_all,
|
recognize_all,
|
||||||
|
recognize_best,
|
||||||
)
|
)
|
||||||
|
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
|
||||||
from hassil.util import merge_dict
|
from hassil.util import merge_dict
|
||||||
from home_assistant_intents import ErrorKey, get_intents, get_languages
|
from home_assistant_intents import ErrorKey, get_intents, get_languages
|
||||||
import yaml
|
import yaml
|
||||||
|
@ -499,6 +499,7 @@ class DefaultAgent(ConversationEntity):
|
||||||
maybe_result: RecognizeResult | None = None
|
maybe_result: RecognizeResult | None = None
|
||||||
best_num_matched_entities = 0
|
best_num_matched_entities = 0
|
||||||
best_num_unmatched_entities = 0
|
best_num_unmatched_entities = 0
|
||||||
|
best_num_unmatched_ranges = 0
|
||||||
for result in recognize_all(
|
for result in recognize_all(
|
||||||
user_input.text,
|
user_input.text,
|
||||||
lang_intents.intents,
|
lang_intents.intents,
|
||||||
|
@ -517,10 +518,14 @@ class DefaultAgent(ConversationEntity):
|
||||||
num_matched_entities += 1
|
num_matched_entities += 1
|
||||||
|
|
||||||
num_unmatched_entities = 0
|
num_unmatched_entities = 0
|
||||||
|
num_unmatched_ranges = 0
|
||||||
for unmatched_entity in result.unmatched_entities_list:
|
for unmatched_entity in result.unmatched_entities_list:
|
||||||
if isinstance(unmatched_entity, UnmatchedTextEntity):
|
if isinstance(unmatched_entity, UnmatchedTextEntity):
|
||||||
if unmatched_entity.text != MISSING_ENTITY:
|
if unmatched_entity.text != MISSING_ENTITY:
|
||||||
num_unmatched_entities += 1
|
num_unmatched_entities += 1
|
||||||
|
elif isinstance(unmatched_entity, UnmatchedRangeEntity):
|
||||||
|
num_unmatched_ranges += 1
|
||||||
|
num_unmatched_entities += 1
|
||||||
else:
|
else:
|
||||||
num_unmatched_entities += 1
|
num_unmatched_entities += 1
|
||||||
|
|
||||||
|
@ -532,15 +537,24 @@ class DefaultAgent(ConversationEntity):
|
||||||
(num_matched_entities == best_num_matched_entities)
|
(num_matched_entities == best_num_matched_entities)
|
||||||
and (num_unmatched_entities < best_num_unmatched_entities)
|
and (num_unmatched_entities < best_num_unmatched_entities)
|
||||||
)
|
)
|
||||||
|
or (
|
||||||
|
# Prefer unmatched ranges
|
||||||
|
(num_matched_entities == best_num_matched_entities)
|
||||||
|
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||||
|
and (num_unmatched_ranges > best_num_unmatched_ranges)
|
||||||
|
)
|
||||||
or (
|
or (
|
||||||
# More literal text matched
|
# More literal text matched
|
||||||
(num_matched_entities == best_num_matched_entities)
|
(num_matched_entities == best_num_matched_entities)
|
||||||
and (num_unmatched_entities == best_num_unmatched_entities)
|
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||||
|
and (num_unmatched_ranges == best_num_unmatched_ranges)
|
||||||
and (result.text_chunks_matched > maybe_result.text_chunks_matched)
|
and (result.text_chunks_matched > maybe_result.text_chunks_matched)
|
||||||
)
|
)
|
||||||
or (
|
or (
|
||||||
# Prefer match failures with entities
|
# Prefer match failures with entities
|
||||||
(result.text_chunks_matched == maybe_result.text_chunks_matched)
|
(result.text_chunks_matched == maybe_result.text_chunks_matched)
|
||||||
|
and (num_unmatched_entities == best_num_unmatched_entities)
|
||||||
|
and (num_unmatched_ranges == best_num_unmatched_ranges)
|
||||||
and (
|
and (
|
||||||
("name" in result.entities)
|
("name" in result.entities)
|
||||||
or ("name" in result.unmatched_entities)
|
or ("name" in result.unmatched_entities)
|
||||||
|
@ -550,6 +564,7 @@ class DefaultAgent(ConversationEntity):
|
||||||
maybe_result = result
|
maybe_result = result
|
||||||
best_num_matched_entities = num_matched_entities
|
best_num_matched_entities = num_matched_entities
|
||||||
best_num_unmatched_entities = num_unmatched_entities
|
best_num_unmatched_entities = num_unmatched_entities
|
||||||
|
best_num_unmatched_ranges = num_unmatched_ranges
|
||||||
|
|
||||||
return maybe_result
|
return maybe_result
|
||||||
|
|
||||||
|
@ -562,76 +577,15 @@ class DefaultAgent(ConversationEntity):
|
||||||
language: str,
|
language: str,
|
||||||
) -> RecognizeResult | None:
|
) -> RecognizeResult | None:
|
||||||
"""Search intents for a strict match to user input."""
|
"""Search intents for a strict match to user input."""
|
||||||
custom_found = False
|
return recognize_best(
|
||||||
name_found = False
|
|
||||||
best_results: list[RecognizeResult] = []
|
|
||||||
best_name_quality: int | None = None
|
|
||||||
best_text_chunks_matched: int | None = None
|
|
||||||
for result in recognize_all(
|
|
||||||
user_input.text,
|
user_input.text,
|
||||||
lang_intents.intents,
|
lang_intents.intents,
|
||||||
slot_lists=slot_lists,
|
slot_lists=slot_lists,
|
||||||
intent_context=intent_context,
|
intent_context=intent_context,
|
||||||
language=language,
|
language=language,
|
||||||
):
|
best_metadata_key=METADATA_CUSTOM_SENTENCE,
|
||||||
# Prioritize user intents
|
best_slot_name="name",
|
||||||
is_custom = (
|
)
|
||||||
result.intent_metadata is not None
|
|
||||||
and result.intent_metadata.get(METADATA_CUSTOM_SENTENCE)
|
|
||||||
)
|
|
||||||
|
|
||||||
if custom_found and not is_custom:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not custom_found and is_custom:
|
|
||||||
custom_found = True
|
|
||||||
# Clear builtin results
|
|
||||||
name_found = False
|
|
||||||
best_results = []
|
|
||||||
best_name_quality = None
|
|
||||||
best_text_chunks_matched = None
|
|
||||||
|
|
||||||
# Prioritize results with a "name" slot
|
|
||||||
name = result.entities.get("name")
|
|
||||||
is_name = name and not name.is_wildcard
|
|
||||||
|
|
||||||
if name_found and not is_name:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not name_found and is_name:
|
|
||||||
name_found = True
|
|
||||||
# Clear non-name results
|
|
||||||
best_results = []
|
|
||||||
best_text_chunks_matched = None
|
|
||||||
|
|
||||||
if is_name:
|
|
||||||
# Prioritize results with a better "name" slot
|
|
||||||
name_quality = len(cast(MatchEntity, name).value.split())
|
|
||||||
if (best_name_quality is None) or (name_quality > best_name_quality):
|
|
||||||
best_name_quality = name_quality
|
|
||||||
# Clear worse name results
|
|
||||||
best_results = []
|
|
||||||
best_text_chunks_matched = None
|
|
||||||
elif name_quality < best_name_quality:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Prioritize results with more literal text
|
|
||||||
# This causes wildcards to match last.
|
|
||||||
if (best_text_chunks_matched is None) or (
|
|
||||||
result.text_chunks_matched > best_text_chunks_matched
|
|
||||||
):
|
|
||||||
best_results = [result]
|
|
||||||
best_text_chunks_matched = result.text_chunks_matched
|
|
||||||
elif result.text_chunks_matched == best_text_chunks_matched:
|
|
||||||
# Accumulate results with the same number of literal text matched.
|
|
||||||
# We will resolve the ambiguity below.
|
|
||||||
best_results.append(result)
|
|
||||||
|
|
||||||
if best_results:
|
|
||||||
# Successful strict match
|
|
||||||
return best_results[0]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def _build_speech(
|
async def _build_speech(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -6,12 +6,8 @@ from collections.abc import Iterable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from hassil.recognize import (
|
from hassil.recognize import MISSING_ENTITY, RecognizeResult
|
||||||
MISSING_ENTITY,
|
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
|
||||||
RecognizeResult,
|
|
||||||
UnmatchedRangeEntity,
|
|
||||||
UnmatchedTextEntity,
|
|
||||||
)
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import http, websocket_api
|
from homeassistant.components import http, websocket_api
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.11.6"]
|
"requirements": ["hassil==2.0.1", "home-assistant-intents==2024.11.13"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from hassil.recognize import PUNCTUATION, RecognizeResult
|
from hassil.recognize import RecognizeResult
|
||||||
|
from hassil.util import PUNCTUATION_ALL
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_COMMAND, CONF_PLATFORM
|
from homeassistant.const import CONF_COMMAND, CONF_PLATFORM
|
||||||
|
@ -20,7 +21,7 @@ from .const import DATA_DEFAULT_ENTITY, DOMAIN
|
||||||
def has_no_punctuation(value: list[str]) -> list[str]:
|
def has_no_punctuation(value: list[str]) -> list[str]:
|
||||||
"""Validate result does not contain punctuation."""
|
"""Validate result does not contain punctuation."""
|
||||||
for sentence in value:
|
for sentence in value:
|
||||||
if PUNCTUATION.search(sentence):
|
if PUNCTUATION_ALL.search(sentence):
|
||||||
raise vol.Invalid("sentence should not contain punctuation")
|
raise vol.Invalid("sentence should not contain punctuation")
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
|
@ -32,10 +32,10 @@ go2rtc-client==0.1.1
|
||||||
ha-ffmpeg==3.2.2
|
ha-ffmpeg==3.2.2
|
||||||
habluetooth==3.6.0
|
habluetooth==3.6.0
|
||||||
hass-nabucasa==0.84.0
|
hass-nabucasa==0.84.0
|
||||||
hassil==1.7.4
|
hassil==2.0.1
|
||||||
home-assistant-bluetooth==1.13.0
|
home-assistant-bluetooth==1.13.0
|
||||||
home-assistant-frontend==20241106.2
|
home-assistant-frontend==20241106.2
|
||||||
home-assistant-intents==2024.11.6
|
home-assistant-intents==2024.11.13
|
||||||
httpx==0.27.2
|
httpx==0.27.2
|
||||||
ifaddr==0.2.0
|
ifaddr==0.2.0
|
||||||
Jinja2==3.1.4
|
Jinja2==3.1.4
|
||||||
|
|
|
@ -1093,7 +1093,7 @@ hass-nabucasa==0.84.0
|
||||||
hass-splunk==0.1.1
|
hass-splunk==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
hassil==1.7.4
|
hassil==2.0.1
|
||||||
|
|
||||||
# homeassistant.components.jewish_calendar
|
# homeassistant.components.jewish_calendar
|
||||||
hdate==0.10.9
|
hdate==0.10.9
|
||||||
|
@ -1130,7 +1130,7 @@ holidays==0.60
|
||||||
home-assistant-frontend==20241106.2
|
home-assistant-frontend==20241106.2
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2024.11.6
|
home-assistant-intents==2024.11.13
|
||||||
|
|
||||||
# homeassistant.components.home_connect
|
# homeassistant.components.home_connect
|
||||||
homeconnect==0.8.0
|
homeconnect==0.8.0
|
||||||
|
|
|
@ -928,7 +928,7 @@ habluetooth==3.6.0
|
||||||
hass-nabucasa==0.84.0
|
hass-nabucasa==0.84.0
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
hassil==1.7.4
|
hassil==2.0.1
|
||||||
|
|
||||||
# homeassistant.components.jewish_calendar
|
# homeassistant.components.jewish_calendar
|
||||||
hdate==0.10.9
|
hdate==0.10.9
|
||||||
|
@ -956,7 +956,7 @@ holidays==0.60
|
||||||
home-assistant-frontend==20241106.2
|
home-assistant-frontend==20241106.2
|
||||||
|
|
||||||
# homeassistant.components.conversation
|
# homeassistant.components.conversation
|
||||||
home-assistant-intents==2024.11.6
|
home-assistant-intents==2024.11.13
|
||||||
|
|
||||||
# homeassistant.components.home_connect
|
# homeassistant.components.home_connect
|
||||||
homeconnect==0.8.0
|
homeconnect==0.8.0
|
||||||
|
|
|
@ -23,7 +23,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.5.0,source=/uv,target=/bin/uv \
|
||||||
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
|
-c /usr/src/homeassistant/homeassistant/package_constraints.txt \
|
||||||
-r /usr/src/homeassistant/requirements.txt \
|
-r /usr/src/homeassistant/requirements.txt \
|
||||||
stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.7.3 \
|
stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.7.3 \
|
||||||
PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.2 hassil==1.7.4 home-assistant-intents==2024.11.6 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
|
PyTurboJPEG==1.7.5 ha-ffmpeg==3.2.2 hassil==2.0.1 home-assistant-intents==2024.11.13 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2
|
||||||
|
|
||||||
LABEL "name"="hassfest"
|
LABEL "name"="hassfest"
|
||||||
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"
|
LABEL "maintainer"="Home Assistant <hello@home-assistant.io>"
|
||||||
|
|
|
@ -697,7 +697,7 @@
|
||||||
'speech': dict({
|
'speech': dict({
|
||||||
'plain': dict({
|
'plain': dict({
|
||||||
'extra_data': None,
|
'extra_data': None,
|
||||||
'speech': 'Sorry, I am not aware of any area called are',
|
'speech': 'Sorry, I am not aware of any area called Are',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
@ -741,7 +741,7 @@
|
||||||
'speech': dict({
|
'speech': dict({
|
||||||
'plain': dict({
|
'plain': dict({
|
||||||
'extra_data': None,
|
'extra_data': None,
|
||||||
'speech': 'Sorry, I am not aware of any area called are',
|
'speech': 'Sorry, I am not aware of any area called Are',
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -639,7 +639,7 @@
|
||||||
'details': dict({
|
'details': dict({
|
||||||
'brightness': dict({
|
'brightness': dict({
|
||||||
'name': 'brightness',
|
'name': 'brightness',
|
||||||
'text': '100%',
|
'text': '100',
|
||||||
'value': 100,
|
'value': 100,
|
||||||
}),
|
}),
|
||||||
'name': dict({
|
'name': dict({
|
||||||
|
@ -654,7 +654,7 @@
|
||||||
'match': True,
|
'match': True,
|
||||||
'sentence_template': '[<numeric_value_set>] <name> brightness [to] <brightness>',
|
'sentence_template': '[<numeric_value_set>] <name> brightness [to] <brightness>',
|
||||||
'slots': dict({
|
'slots': dict({
|
||||||
'brightness': '100%',
|
'brightness': '100',
|
||||||
'name': 'test light',
|
'name': 'test light',
|
||||||
}),
|
}),
|
||||||
'source': 'builtin',
|
'source': 'builtin',
|
||||||
|
|
|
@ -770,8 +770,8 @@ async def test_error_no_device_on_floor_exposed(
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.conversation.default_agent.recognize_all",
|
"homeassistant.components.conversation.default_agent.recognize_best",
|
||||||
return_value=[recognize_result],
|
return_value=recognize_result,
|
||||||
):
|
):
|
||||||
result = await conversation.async_converse(
|
result = await conversation.async_converse(
|
||||||
hass, "turn on test light on the ground floor", None, Context(), None
|
hass, "turn on test light on the ground floor", None, Context(), None
|
||||||
|
@ -838,8 +838,8 @@ async def test_error_no_domain(hass: HomeAssistant) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.conversation.default_agent.recognize_all",
|
"homeassistant.components.conversation.default_agent.recognize_best",
|
||||||
return_value=[recognize_result],
|
return_value=recognize_result,
|
||||||
):
|
):
|
||||||
result = await conversation.async_converse(
|
result = await conversation.async_converse(
|
||||||
hass, "turn on the fans", None, Context(), None
|
hass, "turn on the fans", None, Context(), None
|
||||||
|
@ -873,8 +873,8 @@ async def test_error_no_domain_exposed(hass: HomeAssistant) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.conversation.default_agent.recognize_all",
|
"homeassistant.components.conversation.default_agent.recognize_best",
|
||||||
return_value=[recognize_result],
|
return_value=recognize_result,
|
||||||
):
|
):
|
||||||
result = await conversation.async_converse(
|
result = await conversation.async_converse(
|
||||||
hass, "turn on the fans", None, Context(), None
|
hass, "turn on the fans", None, Context(), None
|
||||||
|
@ -1047,8 +1047,8 @@ async def test_error_no_device_class(hass: HomeAssistant) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.conversation.default_agent.recognize_all",
|
"homeassistant.components.conversation.default_agent.recognize_best",
|
||||||
return_value=[recognize_result],
|
return_value=recognize_result,
|
||||||
):
|
):
|
||||||
result = await conversation.async_converse(
|
result = await conversation.async_converse(
|
||||||
hass, "open the windows", None, Context(), None
|
hass, "open the windows", None, Context(), None
|
||||||
|
@ -1096,8 +1096,8 @@ async def test_error_no_device_class_exposed(hass: HomeAssistant) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.conversation.default_agent.recognize_all",
|
"homeassistant.components.conversation.default_agent.recognize_best",
|
||||||
return_value=[recognize_result],
|
return_value=recognize_result,
|
||||||
):
|
):
|
||||||
result = await conversation.async_converse(
|
result = await conversation.async_converse(
|
||||||
hass, "open all the windows", None, Context(), None
|
hass, "open all the windows", None, Context(), None
|
||||||
|
@ -1207,8 +1207,8 @@ async def test_error_no_device_class_on_floor_exposed(
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.conversation.default_agent.recognize_all",
|
"homeassistant.components.conversation.default_agent.recognize_best",
|
||||||
return_value=[recognize_result],
|
return_value=recognize_result,
|
||||||
):
|
):
|
||||||
result = await conversation.async_converse(
|
result = await conversation.async_converse(
|
||||||
hass, "open ground floor windows", None, Context(), None
|
hass, "open ground floor windows", None, Context(), None
|
||||||
|
@ -1229,8 +1229,8 @@ async def test_error_no_device_class_on_floor_exposed(
|
||||||
async def test_error_no_intent(hass: HomeAssistant) -> None:
|
async def test_error_no_intent(hass: HomeAssistant) -> None:
|
||||||
"""Test response with an intent match failure."""
|
"""Test response with an intent match failure."""
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.conversation.default_agent.recognize_all",
|
"homeassistant.components.conversation.default_agent.recognize_best",
|
||||||
return_value=[],
|
return_value=None,
|
||||||
):
|
):
|
||||||
result = await conversation.async_converse(
|
result = await conversation.async_converse(
|
||||||
hass, "do something", None, Context(), None
|
hass, "do something", None, Context(), None
|
||||||
|
|
|
@ -56,7 +56,7 @@ async def test_converation_trace(
|
||||||
"intent_name": "HassListAddItem",
|
"intent_name": "HassListAddItem",
|
||||||
"slots": {
|
"slots": {
|
||||||
"name": "Shopping List",
|
"name": "Shopping List",
|
||||||
"item": "apples ",
|
"item": "apples",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue