Add a GetHomeState tool to return the current state of the home (#140971)

* Add a GetHomeState tool to return the current state of the home

* Fix check for exposing entities

* Add "all" to get home state description
This commit is contained in:
Allen Porter 2025-03-20 19:37:54 -07:00 committed by GitHub
parent a388863e62
commit a83bf4f514
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 73 additions and 7 deletions

View File

@ -66,6 +66,11 @@ Answer questions about the world truthfully.
Answer in plain text. Keep it simple and to the point.
"""
NO_ENTITIES_PROMPT = (
"Only if the user wants to control a device, tell them to expose entities "
"to their voice assistant in Home Assistant."
)
@callback
def async_render_no_api_prompt(hass: HomeAssistant) -> str:
@ -329,10 +334,7 @@ class AssistAPI(API):
self, llm_context: LLMContext, exposed_entities: dict | None
) -> str:
if not exposed_entities or not exposed_entities["entities"]:
return (
"Only if the user wants to control a device, tell them to expose entities "
"to their voice assistant in Home Assistant."
)
return NO_ENTITIES_PROMPT
return "\n".join(
[
*self._async_get_preable(llm_context),
@ -454,6 +456,9 @@ class AssistAPI(API):
for script_entity_id in exposed_entities[SCRIPT_DOMAIN]
)
if exposed_domains:
tools.append(GetHomeStateTool())
return tools
@ -885,3 +890,39 @@ class CalendarGetEventsTool(Tool):
]
return {"success": True, "result": events}
class GetHomeStateTool(Tool):
"""Tool for getting the current state of exposed entities.
This returns state for all entities that have been exposed to
the assistant. This is different than the GetState intent, which
returns state for entities based on intent parameters.
"""
name = "get_home_state"
description = "Get the current state of all devices in the home. "
async def async_call(
self,
hass: HomeAssistant,
tool_input: ToolInput,
llm_context: LLMContext,
) -> JsonObjectType:
"""Get the current state of exposed entities."""
if llm_context.assistant is None:
# Note this doesn't happen in practice since this tool won't be
# exposed if no assistant is configured.
return {"success": False, "error": "No assistant configured"}
exposed_entities = _get_exposed_entities(hass, llm_context.assistant)
if not exposed_entities["entities"]:
return {"success": False, "error": NO_ENTITIES_PROMPT}
prompt = [
"An overview of the areas and the devices in this smart home:",
yaml_util.dump(list(exposed_entities["entities"].values())),
]
return {
"success": True,
"result": "\n".join(prompt),
}

View File

@ -181,19 +181,19 @@ async def test_assist_api(
assert len(llm.async_get_apis(hass)) == 1
api = await llm.async_get_api(hass, "assist", llm_context)
assert len(api.tools) == 0
assert [tool.name for tool in api.tools] == ["get_home_state"]
# Match all
intent_handler.platforms = None
api = await llm.async_get_api(hass, "assist", llm_context)
assert len(api.tools) == 1
assert [tool.name for tool in api.tools] == ["test_intent", "get_home_state"]
# Match specific domain
intent_handler.platforms = {"light"}
api = await llm.async_get_api(hass, "assist", llm_context)
assert len(api.tools) == 1
assert len(api.tools) == 2
tool = api.tools[0]
assert tool.name == "test_intent"
assert tool.description == "Execute Home Assistant test_intent intent"
@ -643,6 +643,15 @@ async def test_assist_api_prompt(
{exposed_entities_prompt}"""
)
# Verify that the get_home_state tool returns the same results as the exposed_entities_prompt
result = await api.async_call_tool(
llm.ToolInput(tool_name="get_home_state", tool_args={})
)
assert result == {
"success": True,
"result": exposed_entities_prompt,
}
# Fake that request is made from a specific device ID with an area
llm_context.device_id = device.id
area_prompt = (
@ -1267,3 +1276,19 @@ async def test_calendar_get_events_tool(hass: HomeAssistant) -> None:
"start_date_time": now,
"end_date_time": dt_util.start_of_local_day() + timedelta(days=7),
}
async def test_no_tools_exposed(hass: HomeAssistant) -> None:
"""Test that tools are not exposed when no entities are exposed."""
assert await async_setup_component(hass, "homeassistant", {})
context = Context()
llm_context = llm.LLMContext(
platform="test_platform",
context=context,
user_prompt="test_text",
language="*",
assistant="conversation",
device_id=None,
)
api = await llm.async_get_api(hass, "assist", llm_context)
assert api.tools == []