mirror of
https://github.com/home-assistant/core.git
synced 2026-09-27 01:46:11 -04:00
Set tool metadata in the LLM helper, llm, homeassistant and intent (#182700)
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
2f51c3936c
commit
aed8090cbb
@@ -27,11 +27,13 @@ from homeassistant.helpers.llm import (
|
||||
LLM_API_ASSIST,
|
||||
LLMContext,
|
||||
Tool,
|
||||
ToolAnnotations,
|
||||
ToolInput,
|
||||
ToolResult,
|
||||
)
|
||||
from homeassistant.util import dt as dt_util, yaml as yaml_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .exposed_entities import async_should_expose
|
||||
|
||||
# Domains bucketed out of the exposed-entity overview.
|
||||
@@ -217,6 +219,7 @@ class GetLiveContextTool(Tool):
|
||||
"""
|
||||
|
||||
name = "homeassistant__GetLiveContext"
|
||||
title = "Get live context"
|
||||
description = (
|
||||
"Provides real-time information about the"
|
||||
" CURRENT state, value, or mode of devices,"
|
||||
@@ -234,6 +237,8 @@ class GetLiveContextTool(Tool):
|
||||
"Prefer filtering by domain when searching"
|
||||
" for multiple devices of the same type."
|
||||
)
|
||||
annotations = ToolAnnotations(read_only=True, open_world=False)
|
||||
integration = DOMAIN
|
||||
parameters = probatio.Schema(
|
||||
{
|
||||
probatio.Optional(
|
||||
|
||||
@@ -14,30 +14,63 @@ from homeassistant.helpers import (
|
||||
floor_registry as fr,
|
||||
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 .timers import async_device_supports_timers
|
||||
|
||||
# Generic intents exposed as LLM tools regardless of a timer-capable device.
|
||||
LLM_INTENTS = (
|
||||
intent.INTENT_TURN_ON,
|
||||
intent.INTENT_TURN_OFF,
|
||||
intent.INTENT_CANCEL_ALL_TIMERS,
|
||||
intent.INTENT_SET_POSITION,
|
||||
intent.INTENT_STOP_MOVING,
|
||||
)
|
||||
LLM_INTENTS = {
|
||||
intent.INTENT_TURN_ON: "Turn on",
|
||||
intent.INTENT_TURN_OFF: "Turn off",
|
||||
intent.INTENT_CANCEL_ALL_TIMERS: "Cancel all timers",
|
||||
intent.INTENT_SET_POSITION: "Set position",
|
||||
intent.INTENT_STOP_MOVING: "Stop moving",
|
||||
}
|
||||
|
||||
# Timer intents, only exposed for a device that supports timers.
|
||||
TIMER_INTENTS = (
|
||||
intent.INTENT_START_TIMER,
|
||||
intent.INTENT_CANCEL_TIMER,
|
||||
intent.INTENT_INCREASE_TIMER,
|
||||
intent.INTENT_DECREASE_TIMER,
|
||||
intent.INTENT_PAUSE_TIMER,
|
||||
intent.INTENT_UNPAUSE_TIMER,
|
||||
intent.INTENT_TIMER_STATUS,
|
||||
)
|
||||
TIMER_INTENTS = {
|
||||
intent.INTENT_START_TIMER: "Start timer",
|
||||
intent.INTENT_CANCEL_TIMER: "Cancel timer",
|
||||
intent.INTENT_INCREASE_TIMER: "Add time to timer",
|
||||
intent.INTENT_DECREASE_TIMER: "Remove time from timer",
|
||||
intent.INTENT_PAUSE_TIMER: "Pause timer",
|
||||
intent.INTENT_UNPAUSE_TIMER: "Resume timer",
|
||||
intent.INTENT_TIMER_STATUS: "Get timer status",
|
||||
}
|
||||
|
||||
INTENT_TITLES = LLM_INTENTS | TIMER_INTENTS
|
||||
|
||||
# Every intent here acts on Home Assistant's own entities and timers, so none
|
||||
# of them reaches an open world.
|
||||
_CONTROL = ToolAnnotations(idempotent=True, open_world=False)
|
||||
_REPEATS = ToolAnnotations(open_world=False)
|
||||
_ADDS = ToolAnnotations(destructive=False, open_world=False)
|
||||
_READ_ONLY = ToolAnnotations(read_only=True, open_world=False)
|
||||
|
||||
INTENT_ANNOTATIONS = {
|
||||
# Turning on a button entity presses it, which acts again on every call.
|
||||
intent.INTENT_TURN_ON: _REPEATS,
|
||||
intent.INTENT_TURN_OFF: _CONTROL,
|
||||
intent.INTENT_SET_POSITION: _CONTROL,
|
||||
intent.INTENT_STOP_MOVING: _CONTROL,
|
||||
intent.INTENT_CANCEL_ALL_TIMERS: _CONTROL,
|
||||
intent.INTENT_CANCEL_TIMER: _CONTROL,
|
||||
intent.INTENT_PAUSE_TIMER: _CONTROL,
|
||||
intent.INTENT_UNPAUSE_TIMER: _CONTROL,
|
||||
# A started timer can carry a command to run when it finishes, so it is
|
||||
# not only additive. Adding time to a timer is.
|
||||
intent.INTENT_START_TIMER: _REPEATS,
|
||||
intent.INTENT_INCREASE_TIMER: _ADDS,
|
||||
intent.INTENT_DECREASE_TIMER: _REPEATS,
|
||||
intent.INTENT_TIMER_STATUS: _READ_ONLY,
|
||||
}
|
||||
|
||||
DEVICE_CONTROL_TOOL_USAGE_PROMPT = (
|
||||
"When controlling Home Assistant always call the intent tools. "
|
||||
@@ -76,7 +109,14 @@ def async_get_tools(
|
||||
]
|
||||
|
||||
tools: list[Tool] = [
|
||||
IntentTool(f"{DOMAIN}__{handler.intent_type}", handler) for handler in handlers
|
||||
IntentTool(
|
||||
f"{DOMAIN}__{handler.intent_type}",
|
||||
handler,
|
||||
title=INTENT_TITLES[handler.intent_type],
|
||||
integration=DOMAIN,
|
||||
annotations=INTENT_ANNOTATIONS[handler.intent_type],
|
||||
)
|
||||
for handler in handlers
|
||||
]
|
||||
if not tools:
|
||||
return None
|
||||
|
||||
@@ -3,17 +3,27 @@
|
||||
from typing import override
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.llm import LLMContext, Tool, ToolInput, ToolResult
|
||||
from homeassistant.helpers.llm import (
|
||||
LLMContext,
|
||||
Tool,
|
||||
ToolAnnotations,
|
||||
ToolInput,
|
||||
ToolResult,
|
||||
)
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import LLMTools
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class GetDateTimeTool(Tool):
|
||||
"""Tool for getting the current date and time."""
|
||||
|
||||
name = "llm__GetDateTime"
|
||||
title = "Get date and time"
|
||||
description = "Provides the current date and time."
|
||||
annotations = ToolAnnotations(read_only=True, open_world=False)
|
||||
integration = DOMAIN
|
||||
|
||||
@override
|
||||
async def async_call(
|
||||
|
||||
@@ -111,6 +111,42 @@ async def test_timer_intents_offered_for_timer_device(hass: HomeAssistant) -> No
|
||||
assert "intent__HassTimerStatus" in names
|
||||
|
||||
|
||||
async def test_tool_annotations(hass: HomeAssistant) -> None:
|
||||
"""Test the intent tools declare how they behave."""
|
||||
|
||||
@callback
|
||||
def handle_timer(*args: object) -> None:
|
||||
pass
|
||||
|
||||
async_register_timer_handler(hass, "test_device", handle_timer)
|
||||
|
||||
result = await llm_component.async_get_tools(
|
||||
hass, _llm_context(device_id="test_device"), "assist"
|
||||
)
|
||||
tools = {tool.name: tool for tool in result.tools}
|
||||
|
||||
assert tools["intent__HassTurnOn"].title == "Turn on"
|
||||
assert tools["intent__HassTurnOn"].integration == "intent"
|
||||
# Turning on a button entity presses it, so the call is not idempotent.
|
||||
assert tools["intent__HassTurnOn"].annotations == llm.ToolAnnotations(
|
||||
open_world=False
|
||||
)
|
||||
assert tools["intent__HassTurnOff"].annotations == llm.ToolAnnotations(
|
||||
idempotent=True, open_world=False
|
||||
)
|
||||
# Adding time only adds, and has an effect on every call.
|
||||
assert tools["intent__HassIncreaseTimer"].annotations == llm.ToolAnnotations(
|
||||
destructive=False, open_world=False
|
||||
)
|
||||
# A started timer can carry a command to run when it finishes.
|
||||
assert tools["intent__HassStartTimer"].annotations == llm.ToolAnnotations(
|
||||
open_world=False
|
||||
)
|
||||
assert tools["intent__HassTimerStatus"].annotations == llm.ToolAnnotations(
|
||||
read_only=True, open_world=False
|
||||
)
|
||||
|
||||
|
||||
async def test_set_position_requires_exposed_cover(hass: HomeAssistant) -> None:
|
||||
"""Test intent__HassSetPosition is only exposed when a cover/valve is exposed."""
|
||||
assert "intent__HassSetPosition" in await _tool_names(hass)
|
||||
|
||||
@@ -631,12 +631,11 @@ async def test_mcp_tools_list(
|
||||
assert tool.inputSchema.get("type") == "object"
|
||||
properties = tool.inputSchema.get("properties")
|
||||
assert properties.get("name") == {"type": "string"}
|
||||
# A tool that declares no annotations is advertised as unsafe.
|
||||
assert tool.annotations == mcp.types.ToolAnnotations(
|
||||
readOnlyHint=False,
|
||||
destructiveHint=True,
|
||||
idempotentHint=False,
|
||||
openWorldHint=True,
|
||||
openWorldHint=False,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user