fix: describe local intent answers to the model instead of replaying them
With prefer_local_intents, Home Assistant puts requests its local intents answered into the chat log as external calls of intents like HassGetCurrentTime. The model does not have these tools, and replayed as its own calls they make it apologise for using tools it does not have and doubt answers it never gave. They are now passed as a developer note with the intent, its arguments and its result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e54f0f0fa1
commit
6aed494e44
3 files changed
+77
-1
No files matched your search
@@ -18,6 +18,10 @@
|
||||
> lookup for web search uses the subentry's model instead of `gpt-4o-mini` and asks
|
||||
> as a user message (Anthropic needs one); a failed lookup no longer blocks saving
|
||||
> the options, and empty location fields are left out of the web search tool.
|
||||
> - Requests that Home Assistant's local intents answered (`prefer_local_intents`) are
|
||||
> described to the model in a note instead of being replayed as its own calls of
|
||||
> intents like `HassGetCurrentTime`, which are not among its tools; replayed, they
|
||||
> make it apologise for using tools it does not have. Not reported upstream yet.
|
||||
>
|
||||
> HACS only installs from GitHub, so this fork is installed by copying
|
||||
> `custom_components/openai_compatible` into the Home Assistant config.
|
||||
|
||||
@@ -180,13 +180,35 @@ def _convert_content_to_param(
|
||||
"""Convert any native chat message for this agent to the native format."""
|
||||
messages: ResponseInputParam = []
|
||||
reasoning_summary: list[str] = []
|
||||
# Requests that Home Assistant's local intents answered without the model
|
||||
# (prefer_local_intents) are in the chat log as calls of intents like
|
||||
# HassGetCurrentTime, which are not among the model's tools. Replayed as
|
||||
# the model's own calls, they make it apologise for using tools it does
|
||||
# not have, so they are described to it as a note instead.
|
||||
local_calls: dict[str, llm.ToolInput] = {}
|
||||
|
||||
for content in chat_content:
|
||||
if isinstance(content, conversation.ToolResultContent):
|
||||
if (tool_call := local_calls.pop(content.tool_call_id, None)) is not None:
|
||||
messages.append(
|
||||
EasyInputMessageParam(
|
||||
type="message",
|
||||
role="developer",
|
||||
content=(
|
||||
"Home Assistant's local intent recognition answered "
|
||||
"the last user request itself, without you: it ran "
|
||||
f"{tool_call.tool_name} with "
|
||||
f"{json_dumps(tool_call.tool_args)} and got "
|
||||
f"{json_dumps(content.tool_result)}. The assistant "
|
||||
"reply after this note is its wording, not yours. "
|
||||
"That intent is not one of your tools."
|
||||
),
|
||||
)
|
||||
)
|
||||
# Earlier web searches are not replayed: the chat log only keeps
|
||||
# their status, not their results, and a search without results
|
||||
# makes the model disown the answer it based on that search.
|
||||
if content.tool_name != "web_search_call":
|
||||
elif content.tool_name != "web_search_call":
|
||||
messages.append(
|
||||
FunctionCallOutput(
|
||||
type="function_call_output",
|
||||
@@ -211,6 +233,9 @@ def _convert_content_to_param(
|
||||
for tool_call in content.tool_calls:
|
||||
if tool_call.external and tool_call.tool_name == "web_search_call":
|
||||
continue
|
||||
if tool_call.external and tool_call.tool_name != "code_interpreter":
|
||||
local_calls[tool_call.id] = tool_call
|
||||
continue
|
||||
messages.append(
|
||||
ResponseFunctionToolCallParam(
|
||||
type="function_call",
|
||||
|
||||
@@ -135,3 +135,50 @@ async def test_web_search_location_leaves_out_empty_fields(
|
||||
"country": "DE",
|
||||
"timezone": "Europe/Berlin",
|
||||
}
|
||||
|
||||
|
||||
def test_local_intent_replayed_as_note() -> None:
|
||||
"""Test a request a local intent answered is described, not replayed as a call.
|
||||
|
||||
With prefer_local_intents, the chat log holds intents like
|
||||
HassGetCurrentTime as external tool calls. They are not the model's tools,
|
||||
and replayed as its own calls they make it apologise for using them.
|
||||
"""
|
||||
content = [
|
||||
conversation.UserContent(content="What time is it?"),
|
||||
conversation.AssistantContent(
|
||||
agent_id="conversation.test",
|
||||
tool_calls=[
|
||||
llm.ToolInput(
|
||||
id="intent_1",
|
||||
tool_name="HassGetCurrentTime",
|
||||
tool_args={},
|
||||
external=True,
|
||||
)
|
||||
],
|
||||
),
|
||||
conversation.ToolResultContent(
|
||||
agent_id="conversation.test",
|
||||
tool_call_id="intent_1",
|
||||
tool_name="HassGetCurrentTime",
|
||||
result=llm.ToolResult(data={"speech": {"plain": {"speech": "10:29"}}}),
|
||||
),
|
||||
conversation.AssistantContent(
|
||||
agent_id="conversation.test", content="It is 10:29"
|
||||
),
|
||||
conversation.UserContent(content="What is your name?"),
|
||||
]
|
||||
|
||||
messages = _convert_content_to_param(content)
|
||||
|
||||
assert [message["type"] for message in messages] == ["message"] * 4
|
||||
assert [message["role"] for message in messages] == [
|
||||
"user",
|
||||
"developer",
|
||||
"assistant",
|
||||
"user",
|
||||
]
|
||||
note = messages[1]["content"]
|
||||
assert "HassGetCurrentTime" in note
|
||||
assert "10:29" in note
|
||||
assert "not one of your tools" in note
|
||||
Reference in new issue
Block a user