Set tool metadata in todo (#182711)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Paulus Schoutsen
2026-09-24 21:25:15 +02:00
committed by GitHub
co-authored by Claude
parent c08496e105
commit ad076d23a0
2 changed files with 60 additions and 6 deletions
+27 -2
View File
@@ -14,6 +14,7 @@ from homeassistant.helpers.llm import (
IntentTool,
LLMContext,
Tool,
ToolAnnotations,
ToolInput,
ToolResult,
)
@@ -26,13 +27,27 @@ from .intent import (
)
# Intents owned by this integration that are exposed as LLM tools.
LLM_INTENTS = (INTENT_LIST_ADD_ITEM, INTENT_LIST_COMPLETE_ITEM, INTENT_LIST_REMOVE_ITEM)
LLM_INTENTS = {
INTENT_LIST_ADD_ITEM: "Add to-do list item",
INTENT_LIST_COMPLETE_ITEM: "Complete to-do list item",
INTENT_LIST_REMOVE_ITEM: "Remove to-do list item",
}
# Adding an item appends to the list and takes nothing away. Completing and
# removing both look for an item that is still there, so a repeated call
# raises instead of having no further effect.
INTENT_ANNOTATIONS = {
INTENT_LIST_ADD_ITEM: ToolAnnotations(destructive=False, open_world=False),
INTENT_LIST_COMPLETE_ITEM: ToolAnnotations(open_world=False),
INTENT_LIST_REMOVE_ITEM: ToolAnnotations(open_world=False),
}
class TodoGetItemsTool(Tool):
"""LLM Tool allowing querying a to-do list."""
name = "todo__get_items"
title = "Get to-do list items"
description = (
"Query a to-do list to find out what items are on it. "
"Use this to answer questions like "
@@ -40,6 +55,10 @@ class TodoGetItemsTool(Tool):
"'Read my grocery list'. "
"Filters items by status (needs_action, completed, all)."
)
annotations = ToolAnnotations(
read_only=True, destructive=False, idempotent=True, open_world=False
)
integration = DOMAIN
def __init__(self, todo_lists: list[str]) -> None:
"""Init the get items tool."""
@@ -115,7 +134,13 @@ def async_get_tools(
tools: list[Tool] = [TodoGetItemsTool(names)]
tools.extend(
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 -4
View File
@@ -57,6 +57,11 @@ async def test_todo_get_items_tool(hass: HomeAssistant) -> None:
tool = next((tool for tool in result.tools if tool.name == "todo__get_items"), None)
assert tool is not None
assert tool.parameters.schema["todo_list"].container == ["Mock Todo List Name"]
assert tool.title == "Get to-do list items"
assert tool.integration == todo.DOMAIN
assert tool.annotations == llm.ToolAnnotations(
read_only=True, destructive=False, idempotent=True, open_world=False
)
calls = async_mock_service(
hass,
@@ -124,7 +129,31 @@ async def test_todo_get_items_status_filter(
async def test_todo_list_intents_exposed(hass: HomeAssistant) -> None:
"""Test the todo list intents are exposed as tools when a list is exposed."""
result = await llm_component.async_get_tools(hass, _llm_context(), "assist")
names = {tool.name for tool in result.tools}
assert "todo__HassListAddItem" in names
assert "todo__HassListCompleteItem" in names
assert "todo__HassListRemoveItem" in names
tools = {tool.name: tool for tool in result.tools}
assert "todo__HassListAddItem" in tools
assert "todo__HassListCompleteItem" in tools
assert "todo__HassListRemoveItem" in tools
# Completing or removing an item raises on a repeat, so neither is
# idempotent. Adding an item takes nothing away.
assert {
name: (tool.title, tool.integration, tool.annotations)
for name, tool in tools.items()
if name.startswith("todo__HassList")
} == {
"todo__HassListAddItem": (
"Add to-do list item",
todo.DOMAIN,
llm.ToolAnnotations(destructive=False, open_world=False),
),
"todo__HassListCompleteItem": (
"Complete to-do list item",
todo.DOMAIN,
llm.ToolAnnotations(open_world=False),
),
"todo__HassListRemoveItem": (
"Remove to-do list item",
todo.DOMAIN,
llm.ToolAnnotations(open_world=False),
),
}