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.recognize import (
MISSING_ENTITY,
MatchEntity,
RecognizeResult,
UnmatchedTextEntity,
recognize_all,
recognize_best,
)
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
from hassil.util import merge_dict
from home_assistant_intents import ErrorKey, get_intents, get_languages
import yaml
@ -499,6 +499,7 @@ class DefaultAgent(ConversationEntity):
maybe_result: RecognizeResult | None = None
best_num_matched_entities = 0
best_num_unmatched_entities = 0
best_num_unmatched_ranges = 0
for result in recognize_all(
user_input.text,
lang_intents.intents,
@ -517,10 +518,14 @@ class DefaultAgent(ConversationEntity):
num_matched_entities += 1
num_unmatched_entities = 0
num_unmatched_ranges = 0
for unmatched_entity in result.unmatched_entities_list:
if isinstance(unmatched_entity, UnmatchedTextEntity):
if unmatched_entity.text != MISSING_ENTITY:
num_unmatched_entities += 1
elif isinstance(unmatched_entity, UnmatchedRangeEntity):
num_unmatched_ranges += 1
num_unmatched_entities += 1
else:
num_unmatched_entities += 1
@ -532,15 +537,24 @@ class DefaultAgent(ConversationEntity):
(num_matched_entities == best_num_matched_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 (
# More literal text matched
(num_matched_entities == best_num_matched_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)
)
or (
# Prefer match failures with entities
(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 (
("name" in result.entities)
or ("name" in result.unmatched_entities)
@ -550,6 +564,7 @@ class DefaultAgent(ConversationEntity):
maybe_result = result
best_num_matched_entities = num_matched_entities
best_num_unmatched_entities = num_unmatched_entities
best_num_unmatched_ranges = num_unmatched_ranges
return maybe_result
@ -562,76 +577,15 @@ class DefaultAgent(ConversationEntity):
language: str,
) -> RecognizeResult | None:
"""Search intents for a strict match to user input."""
custom_found = False
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(
return recognize_best(
user_input.text,
lang_intents.intents,
slot_lists=slot_lists,
intent_context=intent_context,
language=language,
):
# Prioritize user intents
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
best_metadata_key=METADATA_CUSTOM_SENTENCE,
best_slot_name="name",
)
async def _build_speech(
self,

View file

@ -6,12 +6,8 @@ from collections.abc import Iterable
from typing import Any
from aiohttp import web
from hassil.recognize import (
MISSING_ENTITY,
RecognizeResult,
UnmatchedRangeEntity,
UnmatchedTextEntity,
)
from hassil.recognize import MISSING_ENTITY, RecognizeResult
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
import voluptuous as vol
from homeassistant.components import http, websocket_api

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"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 hassil.recognize import PUNCTUATION, RecognizeResult
from hassil.recognize import RecognizeResult
from hassil.util import PUNCTUATION_ALL
import voluptuous as vol
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]:
"""Validate result does not contain punctuation."""
for sentence in value:
if PUNCTUATION.search(sentence):
if PUNCTUATION_ALL.search(sentence):
raise vol.Invalid("sentence should not contain punctuation")
return value

View file

@ -32,10 +32,10 @@ go2rtc-client==0.1.1
ha-ffmpeg==3.2.2
habluetooth==3.6.0
hass-nabucasa==0.84.0
hassil==1.7.4
hassil==2.0.1
home-assistant-bluetooth==1.13.0
home-assistant-frontend==20241106.2
home-assistant-intents==2024.11.6
home-assistant-intents==2024.11.13
httpx==0.27.2
ifaddr==0.2.0
Jinja2==3.1.4

View file

@ -1093,7 +1093,7 @@ hass-nabucasa==0.84.0
hass-splunk==0.1.1
# homeassistant.components.conversation
hassil==1.7.4
hassil==2.0.1
# homeassistant.components.jewish_calendar
hdate==0.10.9
@ -1130,7 +1130,7 @@ holidays==0.60
home-assistant-frontend==20241106.2
# homeassistant.components.conversation
home-assistant-intents==2024.11.6
home-assistant-intents==2024.11.13
# homeassistant.components.home_connect
homeconnect==0.8.0

View file

@ -928,7 +928,7 @@ habluetooth==3.6.0
hass-nabucasa==0.84.0
# homeassistant.components.conversation
hassil==1.7.4
hassil==2.0.1
# homeassistant.components.jewish_calendar
hdate==0.10.9
@ -956,7 +956,7 @@ holidays==0.60
home-assistant-frontend==20241106.2
# homeassistant.components.conversation
home-assistant-intents==2024.11.6
home-assistant-intents==2024.11.13
# homeassistant.components.home_connect
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 \
-r /usr/src/homeassistant/requirements.txt \
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 "maintainer"="Home Assistant <hello@home-assistant.io>"

View file

@ -697,7 +697,7 @@
'speech': dict({
'plain': dict({
'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({
'plain': dict({
'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({
'brightness': dict({
'name': 'brightness',
'text': '100%',
'text': '100',
'value': 100,
}),
'name': dict({
@ -654,7 +654,7 @@
'match': True,
'sentence_template': '[<numeric_value_set>] <name> brightness [to] <brightness>',
'slots': dict({
'brightness': '100%',
'brightness': '100',
'name': 'test light',
}),
'source': 'builtin',

View file

@ -770,8 +770,8 @@ async def test_error_no_device_on_floor_exposed(
)
with patch(
"homeassistant.components.conversation.default_agent.recognize_all",
return_value=[recognize_result],
"homeassistant.components.conversation.default_agent.recognize_best",
return_value=recognize_result,
):
result = await conversation.async_converse(
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(
"homeassistant.components.conversation.default_agent.recognize_all",
return_value=[recognize_result],
"homeassistant.components.conversation.default_agent.recognize_best",
return_value=recognize_result,
):
result = await conversation.async_converse(
hass, "turn on the fans", None, Context(), None
@ -873,8 +873,8 @@ async def test_error_no_domain_exposed(hass: HomeAssistant) -> None:
)
with patch(
"homeassistant.components.conversation.default_agent.recognize_all",
return_value=[recognize_result],
"homeassistant.components.conversation.default_agent.recognize_best",
return_value=recognize_result,
):
result = await conversation.async_converse(
hass, "turn on the fans", None, Context(), None
@ -1047,8 +1047,8 @@ async def test_error_no_device_class(hass: HomeAssistant) -> None:
)
with patch(
"homeassistant.components.conversation.default_agent.recognize_all",
return_value=[recognize_result],
"homeassistant.components.conversation.default_agent.recognize_best",
return_value=recognize_result,
):
result = await conversation.async_converse(
hass, "open the windows", None, Context(), None
@ -1096,8 +1096,8 @@ async def test_error_no_device_class_exposed(hass: HomeAssistant) -> None:
)
with patch(
"homeassistant.components.conversation.default_agent.recognize_all",
return_value=[recognize_result],
"homeassistant.components.conversation.default_agent.recognize_best",
return_value=recognize_result,
):
result = await conversation.async_converse(
hass, "open all the windows", None, Context(), None
@ -1207,8 +1207,8 @@ async def test_error_no_device_class_on_floor_exposed(
)
with patch(
"homeassistant.components.conversation.default_agent.recognize_all",
return_value=[recognize_result],
"homeassistant.components.conversation.default_agent.recognize_best",
return_value=recognize_result,
):
result = await conversation.async_converse(
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:
"""Test response with an intent match failure."""
with patch(
"homeassistant.components.conversation.default_agent.recognize_all",
return_value=[],
"homeassistant.components.conversation.default_agent.recognize_best",
return_value=None,
):
result = await conversation.async_converse(
hass, "do something", None, Context(), None

View file

@ -56,7 +56,7 @@ async def test_converation_trace(
"intent_name": "HassListAddItem",
"slots": {
"name": "Shopping List",
"item": "apples ",
"item": "apples",
},
}