Set tool metadata in media_player (#182707)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Paulus Schoutsen
2026-09-21 18:49:33 +02:00
committed by GitHub
co-authored by Claude
parent fd3ecb1282
commit 64976f170b
2 changed files with 77 additions and 14 deletions
+44 -13
View File
@@ -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,
@@ -20,17 +26,36 @@ from .const import (
)
# Intents owned by this integration that are exposed as LLM tools.
LLM_INTENTS = (
INTENT_MEDIA_NEXT,
INTENT_MEDIA_PAUSE,
INTENT_PLAYER_MUTE,
INTENT_PLAYER_UNMUTE,
INTENT_MEDIA_PREVIOUS,
INTENT_MEDIA_SEARCH_AND_PLAY,
INTENT_MEDIA_UNPAUSE,
INTENT_SET_VOLUME,
INTENT_SET_VOLUME_RELATIVE,
)
LLM_INTENTS = {
INTENT_MEDIA_NEXT: "Next track",
INTENT_MEDIA_PAUSE: "Pause media",
INTENT_PLAYER_MUTE: "Mute player",
INTENT_PLAYER_UNMUTE: "Unmute player",
INTENT_MEDIA_PREVIOUS: "Previous track",
INTENT_MEDIA_SEARCH_AND_PLAY: "Search and play media",
INTENT_MEDIA_UNPAUSE: "Resume media",
INTENT_SET_VOLUME: "Set volume",
INTENT_SET_VOLUME_RELATIVE: "Change volume",
}
# Setting a value on the user's own player has no further effect when it is
# repeated. Stepping through tracks or volume has an effect on every call, and
# a search reaches the media the player can read.
_CONTROL = ToolAnnotations(idempotent=True, open_world=False)
_CUMULATIVE = ToolAnnotations(open_world=False)
INTENT_ANNOTATIONS = {
INTENT_MEDIA_PAUSE: _CONTROL,
# Unpausing clears the players it remembered, so a repeat can resume more.
INTENT_MEDIA_UNPAUSE: _CUMULATIVE,
INTENT_PLAYER_MUTE: _CONTROL,
INTENT_PLAYER_UNMUTE: _CONTROL,
INTENT_SET_VOLUME: _CONTROL,
INTENT_MEDIA_NEXT: _CUMULATIVE,
INTENT_MEDIA_PREVIOUS: _CUMULATIVE,
INTENT_SET_VOLUME_RELATIVE: _CUMULATIVE,
INTENT_MEDIA_SEARCH_AND_PLAY: ToolAnnotations(),
}
@callback
@@ -51,7 +76,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=INTENT_ANNOTATIONS[handler.intent_type],
)
for handler in intent.async_get(hass)
if handler.intent_type in LLM_INTENTS
]
+33 -1
View File
@@ -54,7 +54,39 @@ 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 media_player 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
control = llm.ToolAnnotations(idempotent=True, open_world=False)
repeats = llm.ToolAnnotations(open_world=False)
assert {
name: (tool.title, tool.integration, tool.annotations)
for name, tool in tools.items()
if name in TOOL_NAMES
} == {
"media_player__HassMediaNext": ("Next track", "media_player", repeats),
"media_player__HassMediaPause": ("Pause media", "media_player", control),
"media_player__HassMediaPlayerMute": ("Mute player", "media_player", control),
"media_player__HassMediaPlayerUnmute": (
"Unmute player",
"media_player",
control,
),
"media_player__HassMediaPrevious": ("Previous track", "media_player", repeats),
"media_player__HassMediaSearchAndPlay": (
"Search and play media",
"media_player",
llm.ToolAnnotations(),
),
"media_player__HassMediaUnpause": ("Resume media", "media_player", repeats),
"media_player__HassSetVolume": ("Set volume", "media_player", control),
"media_player__HassSetVolumeRelative": (
"Change volume",
"media_player",
repeats,
),
}
async def test_intent_tool_not_exposed(hass: HomeAssistant) -> None: