mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Add intent LLM tools platform (#175517)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
9538aa5bcc
commit
e2508cffde
@@ -0,0 +1,68 @@
|
||||
"""LLM tools for the intent integration.
|
||||
|
||||
Exposes the generic, cross-domain intents owned by the intent integration
|
||||
(device on/off, position, timers) as LLM tools. Domain-specific intents are
|
||||
exposed by their own integration's ``llm.py`` platform.
|
||||
"""
|
||||
|
||||
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 .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,
|
||||
)
|
||||
|
||||
# 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,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_tools(
|
||||
hass: HomeAssistant, llm_context: LLMContext, api_id: str
|
||||
) -> LLMTools | None:
|
||||
"""Return LLM tools for the generic intents."""
|
||||
if api_id != LLM_API_ASSIST:
|
||||
return None
|
||||
|
||||
wanted = set(LLM_INTENTS)
|
||||
if llm_context.device_id and async_device_supports_timers(
|
||||
hass, llm_context.device_id
|
||||
):
|
||||
wanted.update(TIMER_INTENTS)
|
||||
|
||||
exposed_domains = {
|
||||
state.domain
|
||||
for state in hass.states.async_all()
|
||||
if async_should_expose(hass, llm_context.assistant, state.entity_id)
|
||||
}
|
||||
handlers = [
|
||||
handler
|
||||
for handler in intent.async_get(hass)
|
||||
if handler.intent_type in wanted
|
||||
and (handler.platforms is None or handler.platforms & exposed_domains)
|
||||
]
|
||||
|
||||
tools: list[Tool] = [
|
||||
IntentTool(handler.intent_type, handler) for handler in handlers
|
||||
]
|
||||
if not tools:
|
||||
return None
|
||||
return LLMTools(tools=tools)
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Tests for the intent LLM tools platform (generic intents)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import llm as llm_component
|
||||
from homeassistant.components.homeassistant.exposed_entities import async_expose_entity
|
||||
from homeassistant.components.intent import llm as intent_llm
|
||||
from homeassistant.components.intent.timers import async_register_timer_handler
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.helpers import llm
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
COVER_ENTITY_ID = "cover.test"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_integrations(hass: HomeAssistant) -> None:
|
||||
"""Set up the integrations and expose a cover."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
assert await async_setup_component(hass, "intent", {})
|
||||
assert await async_setup_component(hass, "llm", {})
|
||||
hass.states.async_set(COVER_ENTITY_ID, "open", {"friendly_name": "Test Cover"})
|
||||
async_expose_entity(hass, "conversation", COVER_ENTITY_ID, True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
def _llm_context(device_id: str | None = None) -> llm.LLMContext:
|
||||
"""Return an LLM context for the conversation assistant."""
|
||||
return llm.LLMContext(
|
||||
platform="test_platform",
|
||||
context=Context(),
|
||||
language="*",
|
||||
assistant="conversation",
|
||||
device_id=device_id,
|
||||
)
|
||||
|
||||
|
||||
async def _tool_names(hass: HomeAssistant) -> set[str]:
|
||||
"""Return the names of the tools offered by the intent platform."""
|
||||
result = await llm_component.async_get_tools(hass, _llm_context(), "assist")
|
||||
return {tool.name for tool in result.tools}
|
||||
|
||||
|
||||
async def test_generic_intents_exposed(hass: HomeAssistant) -> None:
|
||||
"""Test the always-on generic intents are exposed."""
|
||||
names = await _tool_names(hass)
|
||||
assert "HassTurnOn" in names
|
||||
assert "HassTurnOff" in names
|
||||
|
||||
|
||||
async def test_timer_intents_require_timer_device(hass: HomeAssistant) -> None:
|
||||
"""Test timer intents are not exposed without a timer-capable device."""
|
||||
assert "HassStartTimer" not in await _tool_names(hass)
|
||||
|
||||
|
||||
async def test_timer_intents_offered_for_timer_device(hass: HomeAssistant) -> None:
|
||||
"""Test timer intents are exposed for a timer-capable device."""
|
||||
|
||||
@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"
|
||||
)
|
||||
names = {tool.name for tool in result.tools}
|
||||
assert "HassStartTimer" in names
|
||||
assert "HassTimerStatus" in names
|
||||
|
||||
|
||||
async def test_set_position_requires_exposed_cover(hass: HomeAssistant) -> None:
|
||||
"""Test HassSetPosition is only exposed when a cover/valve is exposed."""
|
||||
assert "HassSetPosition" in await _tool_names(hass)
|
||||
|
||||
async_expose_entity(hass, "conversation", COVER_ENTITY_ID, False)
|
||||
assert "HassSetPosition" not in await _tool_names(hass)
|
||||
|
||||
|
||||
async def test_no_tools_for_other_api(hass: HomeAssistant) -> None:
|
||||
"""Test the platform returns None for an unsupported API."""
|
||||
assert intent_llm.async_get_tools(hass, _llm_context(), "other") is None
|
||||
Reference in New Issue
Block a user