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:
Michael Hansen 2024-11-13 15:50:08 -06:00 committed by GitHub
parent 4002bc3c25
commit 51c6ee97b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 53 additions and 102 deletions

View file

@ -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,

View file

@ -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

View file

@ -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"]
} }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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>"

View file

@ -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',
}), }),
}), }),
}), }),

View file

@ -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',

View file

@ -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

View file

@ -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",
}, },
} }