From 5dd48e2ca96bfc534dc6dc0135d7ae24e59e8c82 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Sep 2026 20:26:51 +0100 Subject: [PATCH] Set tool metadata in vacuum (#182706) Co-authored-by: Claude --- homeassistant/components/vacuum/llm.py | 30 ++++++++++++++++++++------ tests/components/vacuum/test_llm.py | 20 ++++++++++++++++- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/vacuum/llm.py b/homeassistant/components/vacuum/llm.py index 37c366a32b2e..0ff9887ecc33 100644 --- a/homeassistant/components/vacuum/llm.py +++ b/homeassistant/components/vacuum/llm.py @@ -4,7 +4,13 @@ from homeassistant.components.homeassistant import async_should_expose from homeassistant.components.llm import LLMTools from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import intent -from homeassistant.helpers.llm import LLM_API_ASSIST, IntentTool, LLMContext, Tool +from homeassistant.helpers.llm import ( + LLM_API_ASSIST, + IntentTool, + LLMContext, + Tool, + ToolAnnotations, +) from .const import DOMAIN from .intent import ( @@ -13,12 +19,16 @@ from .intent import ( INTENT_VACUUM_START, ) +# Each intent dispatches a command to the user's own vacuum, and the command +# runs again on every call. +LLM_ANNOTATIONS = ToolAnnotations(open_world=False) + # Intents owned by this integration that are exposed as LLM tools. -LLM_INTENTS = ( - INTENT_VACUUM_CLEAN_AREA, - INTENT_VACUUM_RETURN_TO_BASE, - INTENT_VACUUM_START, -) +LLM_INTENTS = { + INTENT_VACUUM_CLEAN_AREA: "Clean area", + INTENT_VACUUM_RETURN_TO_BASE: "Return vacuum to base", + INTENT_VACUUM_START: "Start vacuum", +} @callback @@ -39,7 +49,13 @@ def async_get_tools( return None tools: list[Tool] = [ - IntentTool(f"{DOMAIN}__{handler.intent_type}", handler) + IntentTool( + f"{DOMAIN}__{handler.intent_type}", + handler, + title=LLM_INTENTS[handler.intent_type], + integration=DOMAIN, + annotations=LLM_ANNOTATIONS, + ) for handler in intent.async_get(hass) if handler.intent_type in LLM_INTENTS ] diff --git a/tests/components/vacuum/test_llm.py b/tests/components/vacuum/test_llm.py index e24bf00f3c94..05049b1c3454 100644 --- a/tests/components/vacuum/test_llm.py +++ b/tests/components/vacuum/test_llm.py @@ -48,7 +48,25 @@ async def _tool_names(hass: HomeAssistant) -> set[str]: async def test_intent_tool_exposed(hass: HomeAssistant) -> None: """Test the intent tool is offered for an exposed vacuum entity.""" - assert await _tool_names(hass) >= TOOL_NAMES + result = await llm_component.async_get_tools(hass, _llm_context(), "assist") + tools = {tool.name: tool for tool in result.tools} + assert tools.keys() >= TOOL_NAMES + + # A vacuum command runs again on every call, so none of these is idempotent. + annotations = llm.ToolAnnotations(open_world=False) + assert { + name: (tool.title, tool.integration, tool.annotations) + for name, tool in tools.items() + if name in TOOL_NAMES + } == { + "vacuum__HassVacuumCleanArea": ("Clean area", "vacuum", annotations), + "vacuum__HassVacuumReturnToBase": ( + "Return vacuum to base", + "vacuum", + annotations, + ), + "vacuum__HassVacuumStart": ("Start vacuum", "vacuum", annotations), + } async def test_intent_tool_not_exposed(hass: HomeAssistant) -> None: