Fix target matching in Assist sentence parser debugger (#178202)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Michael Hansen
2026-08-05 04:27:24 -04:00
committed by GitHub
co-authored by Claude Opus 5
parent a107789a5b
commit 935764fb6c
2 changed files with 177 additions and 15 deletions
@@ -409,10 +409,13 @@ class DefaultAgent(ConversationEntity):
}
if successful_match:
satellite_area, _ = self._get_satellite_area_and_device(
user_input.satellite_id, user_input.device_id
)
result_dict["targets"] = {
state.entity_id: {"matched": is_matched}
for state, is_matched in _get_debug_targets(
self.hass, intent_result
self.hass, intent_result, satellite_area
)
}
@@ -1676,12 +1679,14 @@ def _collect_list_references(expression: Expression, list_names: set[str]) -> No
def _get_debug_targets(
hass: HomeAssistant,
result: RecognizeResult,
satellite_area: ar.AreaEntry | None = None,
) -> Iterable[tuple[State, bool]]:
"""Yield state/is_matched pairs for a hassil recognition."""
entities = result.entities
name: str | None = None
area_name: str | None = None
floor_name: str | None = None
domains: set[str] | None = None
device_classes: set[str] | None = None
state_names: set[str] | None = None
@@ -1692,6 +1697,9 @@ def _get_debug_targets(
if "area" in entities:
area_name = str(entities["area"].value)
if "floor" in entities:
floor_name = str(entities["floor"].value)
if "domain" in entities:
domains = set(cv.ensure_list(entities["domain"].value))
@@ -1702,24 +1710,27 @@ def _get_debug_targets(
# HassGetState only
state_names = set(cv.ensure_list(entities["state"].value))
if (
(name is None)
and (area_name is None)
and (not domains)
and (not device_classes)
and (not state_names)
):
constraints = intent.MatchTargetsConstraints(
name=name,
area_name=area_name,
floor_name=floor_name,
domains=domains,
device_classes=device_classes,
assistant=DOMAIN,
)
if not (constraints.has_constraints or state_names):
# Avoid "matching" all entities when there is no filter
return
states = intent.async_match_states(
hass,
name=name,
area_name=area_name,
domains=domains,
device_classes=device_classes,
# Mirror the preferences used when the intent is actually handled so that
# duplicate names are deduplicated the same way.
preferences = intent.MatchTargetsPreferences(
area_id=satellite_area.id if satellite_area is not None else None
)
states = intent.async_match_targets(hass, constraints, preferences).states
for state in states:
# For queries, a target is "matched" based on its state
is_matched = (state_names is None) or (state.state in state_names)
+152 -1
View File
@@ -9,6 +9,7 @@ from freezegun import freeze_time
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components import conversation
from homeassistant.components.conversation import (
AssistantContent,
ConversationInput,
@@ -17,13 +18,16 @@ from homeassistant.components.conversation import (
)
from homeassistant.components.conversation.const import HOME_ASSISTANT_AGENT
from homeassistant.components.conversation.models import ConversationResult
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
from homeassistant.const import ATTR_FRIENDLY_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import (
area_registry as ar,
chat_session,
device_registry as dr,
entity_registry as er,
floor_registry as fr,
intent,
)
from homeassistant.setup import async_setup_component
@@ -31,7 +35,12 @@ from homeassistant.util.dt import utcnow
from . import MockAgent
from tests.common import MockUser, async_fire_time_changed, async_mock_service
from tests.common import (
MockConfigEntry,
MockUser,
async_fire_time_changed,
async_mock_service,
)
from tests.typing import ClientSessionGenerator, WebSocketGenerator
AGENT_ID_OPTIONS = [
@@ -454,6 +463,148 @@ async def test_ws_hass_agent_debug_null_result(
assert msg["result"]["results"] == [None]
async def test_ws_hass_agent_debug_floor(
hass: HomeAssistant,
init_components,
hass_ws_client: WebSocketGenerator,
area_registry: ar.AreaRegistry,
floor_registry: fr.FloorRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that debug targets are restricted to the matched floor."""
first_floor = floor_registry.async_create("first floor")
floor_registry.async_create("ground floor")
bedroom_area = area_registry.async_create("bedroom", floor_id=first_floor.floor_id)
bedroom_light = entity_registry.async_get_or_create(
"light", "demo", "bedroom", original_name="bedroom light"
)
entity_registry.async_update_entity(
bedroom_light.entity_id, area_id=bedroom_area.id
)
hass.states.async_set(bedroom_light.entity_id, "on")
# Not assigned to a floor
garage_area = area_registry.async_create("garage")
garage_light = entity_registry.async_get_or_create(
"light", "demo", "garage", original_name="garage light"
)
entity_registry.async_update_entity(garage_light.entity_id, area_id=garage_area.id)
hass.states.async_set(garage_light.entity_id, "on")
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
"type": "conversation/agent/homeassistant/debug",
"sentences": [
"turn off the lights on the first floor",
"turn off the lights on the ground floor",
],
}
)
msg = await client.receive_json()
assert msg["success"]
results = msg["result"]["results"]
assert results[0]["match"]
assert results[0]["targets"] == {bedroom_light.entity_id: {"matched": True}}
# No areas are assigned to the ground floor
assert results[1]["match"]
assert results[1]["targets"] == {}
async def test_ws_hass_agent_debug_unexposed_entity(
hass: HomeAssistant,
init_components,
hass_ws_client: WebSocketGenerator,
area_registry: ar.AreaRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that debug targets only include entities exposed to Assist."""
kitchen_area = area_registry.async_create("kitchen")
exposed_light = entity_registry.async_get_or_create(
"light", "demo", "exposed", original_name="exposed light"
)
hidden_light = entity_registry.async_get_or_create(
"light", "demo", "hidden", original_name="hidden light"
)
for entity_entry in (exposed_light, hidden_light):
entity_registry.async_update_entity(
entity_entry.entity_id, area_id=kitchen_area.id
)
hass.states.async_set(entity_entry.entity_id, "on")
async_expose_entity(hass, conversation.DOMAIN, hidden_light.entity_id, False)
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
"type": "conversation/agent/homeassistant/debug",
"sentences": ["turn off the lights in the kitchen"],
}
)
msg = await client.receive_json()
assert msg["success"]
results = msg["result"]["results"]
assert results[0]["match"]
assert results[0]["targets"] == {exposed_light.entity_id: {"matched": True}}
async def test_ws_hass_agent_debug_preferred_area(
hass: HomeAssistant,
init_components,
hass_ws_client: WebSocketGenerator,
area_registry: ar.AreaRegistry,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that debug targets use the requesting device's area to disambiguate."""
config_entry = MockConfigEntry()
config_entry.add_to_hass(hass)
bedroom_area = area_registry.async_create("bedroom")
office_area = area_registry.async_create("office")
# Duplicate names in two areas
bedroom_light = entity_registry.async_get_or_create(
"light", "demo", "bedroom", original_name="overhead light"
)
entity_registry.async_update_entity(
bedroom_light.entity_id, area_id=bedroom_area.id
)
hass.states.async_set(bedroom_light.entity_id, "on")
office_light = entity_registry.async_get_or_create(
"light", "demo", "office", original_name="overhead light"
)
entity_registry.async_update_entity(office_light.entity_id, area_id=office_area.id)
hass.states.async_set(office_light.entity_id, "on")
voice_device = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={("demo", "voice-satellite")},
)
device_registry.async_update_device(voice_device.id, area_id=office_area.id)
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
"type": "conversation/agent/homeassistant/debug",
"sentences": ["turn off the overhead light"],
"device_id": voice_device.id,
}
)
msg = await client.receive_json()
assert msg["success"]
results = msg["result"]["results"]
assert results[0]["match"]
assert results[0]["targets"] == {office_light.entity_id: {"matched": True}}
async def test_ws_hass_agent_debug_out_of_range(
hass: HomeAssistant,
init_components,