Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions homeassistant/components/conversation/chat_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,17 +293,23 @@ class ToolResultContent:
agent_id: str
tool_call_id: str
tool_name: str
tool_result: JsonObjectType
result: llm.ToolResult
created: datetime = field(init=False, default_factory=utcnow)

@property
def tool_result(self) -> JsonObjectType:
"""Return the data of the result."""
return self.result.data

def as_dict(self) -> dict[str, Any]:
"""Return a dictionary representation of the content."""
return {
"role": self.role,
"agent_id": self.agent_id,
"tool_call_id": self.tool_call_id,
"tool_name": self.tool_name,
"tool_result": self.tool_result,
"result": asdict(self.result),
"tool_result": self.result.data,
"created": self.created,
}

Expand All @@ -327,6 +333,7 @@ class ToolResultContentDeltaDict(TypedDict, total=False):
role: Literal["tool_result"]
tool_call_id: str
tool_name: str
result: llm.ToolResult
tool_result: JsonObjectType


Expand Down Expand Up @@ -460,16 +467,17 @@ async def async_add_assistant_content(
try:
tool_result = await tool_call_tasks[tool_input.id]
except (HomeAssistantError, probatio.Invalid) as e:
tool_result = {"error": type(e).__name__}
error_data: JsonObjectType = {"error": type(e).__name__}
if str(e):
tool_result["error_text"] = str(e)
error_data["error_text"] = str(e)
tool_result = llm.ToolResult(data=error_data, error=True)
LOGGER.debug("Tool response: %s", tool_result)

response_content = ToolResultContent(
agent_id=content.agent_id,
tool_call_id=tool_input.id,
tool_name=tool_input.tool_name,
tool_result=tool_result,
result=tool_result,
)
self.content.append(response_content)
_async_notify_subscribers(
Expand Down Expand Up @@ -567,7 +575,7 @@ async def async_add_delta_content_stream(
):
yield tool_result
if self.delta_listener:
self.delta_listener(self, asdict(tool_result))
self.delta_listener(self, tool_result.as_dict())
current_content = ""
current_thinking_content = ""
current_native = None
Expand All @@ -585,15 +593,17 @@ async def async_add_delta_content_stream(
}:
self.delta_listener(self, filtered_delta)
elif delta["role"] == "tool_result":
if (result := delta.get("result")) is None:
result = llm.ToolResult(data=delta["tool_result"])
content = ToolResultContent(
agent_id=agent_id,
tool_call_id=delta["tool_call_id"],
tool_name=delta["tool_name"],
tool_result=delta["tool_result"],
result=result,
)
yield content
if self.delta_listener:
self.delta_listener(self, asdict(content))
self.delta_listener(self, content.as_dict())
self.async_add_assistant_content_without_tools(content)
else:
raise ValueError(
Expand All @@ -620,7 +630,7 @@ async def async_add_delta_content_stream(
):
yield tool_result
if self.delta_listener:
self.delta_listener(self, asdict(tool_result))
self.delta_listener(self, tool_result.as_dict())

async def _async_expand_prompt_template(
self,
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/conversation/default_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,7 @@ async def _async_execute_intent(
agent_id=user_input.agent_id,
tool_call_id=tool_input.id,
tool_name=tool_input.tool_name,
tool_result=tool_result,
result=llm.ToolResult(data=tool_result),
)
)

Expand Down Expand Up @@ -1583,7 +1583,7 @@ async def _handle_trigger_result(
agent_id=user_input.agent_id,
tool_call_id=tool_input.id,
tool_name=tool_input.tool_name,
tool_result=tool_result,
result=llm.ToolResult(data=tool_result),
)
)

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/conversation/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"requirements": [
"gazetteer-matcher==1.1.0",
"hassil==3.12.1",
"home-assistant-intents==2026.8.28"
"home-assistant-intents==2026.9.17"
]
}
4 changes: 2 additions & 2 deletions homeassistant/components/conversation/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ def async_get_result_from_chat_log(
) -> ConversationResult:
"""Get the result from the chat log."""
tool_results = [
content.tool_result
content.result.data
for content in chat_log.content[chat_log.llm_input_provided_index :]
if isinstance(content, ToolResultContent)
and isinstance(content.tool_result, llm.IntentResponseDict)
and isinstance(content.result.data, llm.IntentResponseDict)
]

if tool_results:
Expand Down
23 changes: 13 additions & 10 deletions homeassistant/components/mcp_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,12 @@ async def handle_read_resource(uri: AnyUrl) -> Sequence[ReadResourceContents]:
tool_response = await llm_api.async_call_tool(
llm.ToolInput(tool_name=LIVE_CONTEXT_TOOL_NAME, tool_args={})
)
if not tool_response.get("success"):
raise HomeAssistantError(cast(str, tool_response["error"]))
if not tool_response.data.get("success"):
raise HomeAssistantError(cast(str, tool_response.data["error"]))

return [
ReadResourceContents(
content=cast(str, tool_response["result"]),
content=cast(str, tool_response.data["result"]),
mime_type=SNAPSHOT_RESOURCE_MIME_TYPE,
)
]
Expand All @@ -164,7 +164,7 @@ async def list_tools() -> list[types.Tool]:
return [_format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools]

@server.call_tool() # type: ignore[untyped-decorator]
async def call_tool(name: str, arguments: dict) -> Sequence[types.TextContent]:
async def call_tool(name: str, arguments: dict) -> types.CallToolResult:
"""Handle calling tools."""
llm_api = await get_api_instance()
tool_input = llm.ToolInput(tool_name=name, tool_args=arguments)
Expand All @@ -174,11 +174,14 @@ async def call_tool(name: str, arguments: dict) -> Sequence[types.TextContent]:
tool_response = await llm_api.async_call_tool(tool_input)
except (HomeAssistantError, probatio.Invalid) as e:
raise HomeAssistantError(f"Error calling tool: {e}") from e
return [
types.TextContent(
type="text",
text=json.dumps(tool_response, ensure_ascii=False),
)
]
return types.CallToolResult(
content=[
types.TextContent(
type="text",
text=json.dumps(tool_response.data, ensure_ascii=False),
)
],
isError=tool_response.error,
)

return server
18 changes: 15 additions & 3 deletions homeassistant/components/onkyo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: OnkyoConfigEntry) -> boo
try:
info = await async_interview(host)
except TimeoutError as exc:
raise ConfigEntryNotReady(f"Timed out interviewing: {host}") from exc
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="interview_timeout",
translation_placeholders={"host": host},
) from exc
except OSError as exc:
raise ConfigEntryNotReady(f"Unexpected exception interviewing: {host}") from exc
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="interview_error",
translation_placeholders={"host": host},
) from exc

manager = ReceiverManager(hass, entry, info)

Expand All @@ -75,7 +83,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: OnkyoConfigEntry) -> boo
try:
await error
except OSError as exc:
raise ConfigEntryNotReady(f"Unable to connect to: {host}") from exc
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
translation_placeholders={"host": host},
) from exc

return True

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/onkyo/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ rules:
entity-device-class: todo
entity-disabled-by-default: done
entity-translations: todo
exception-translations: todo
exception-translations: done
icon-translations: todo
reconfiguration-flow: done
repair-issues: done
Expand Down
9 changes: 9 additions & 0 deletions homeassistant/components/onkyo/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@
}
},
"exceptions": {
"cannot_connect": {
"message": "Could not connect to the receiver at {host}."
},
"interview_error": {
"message": "Unexpected error while interviewing the receiver at {host}."
},
"interview_timeout": {
"message": "Timed out while interviewing the receiver at {host}."
},
"invalid_sound_mode": {
"message": "Cannot select sound mode \"{invalid_sound_mode}\" for entity: {entity_id}."
},
Expand Down
19 changes: 15 additions & 4 deletions homeassistant/helpers/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ class ToolInput:
external: bool = False


@dataclass(slots=True)
class ToolResult:
"""Result of a tool call."""

data: JsonObjectType
error: bool = False


class Tool:
"""LLM Tool base class."""

Expand All @@ -164,7 +172,7 @@ class Tool:
@abstractmethod
async def async_call(
self, hass: HomeAssistant, tool_input: ToolInput, llm_context: LLMContext
) -> JsonObjectType:
) -> ToolResult | JsonObjectType:
"""Call the tool."""
raise NotImplementedError

Expand All @@ -184,7 +192,7 @@ class APIInstance:
tools: list[Tool]
custom_serializer: Callable[[Any], Any] | None = None

async def async_call_tool(self, tool_input: ToolInput) -> JsonObjectType:
async def async_call_tool(self, tool_input: ToolInput) -> ToolResult:
"""Call a LLM tool, validate args and return the response."""
from homeassistant.components.conversation import ( # noqa: PLC0415
ConversationTraceEventType,
Expand All @@ -202,7 +210,10 @@ async def async_call_tool(self, tool_input: ToolInput) -> JsonObjectType:
else:
raise HomeAssistantError(f'Tool "{tool_input.tool_name}" not found')

return await tool.async_call(self.api.hass, tool_input, self.llm_context)
result = await tool.async_call(self.api.hass, tool_input, self.llm_context)
if isinstance(result, ToolResult):
return result
return ToolResult(data=result)


@dataclass(slots=True, kw_only=True)
Expand Down Expand Up @@ -331,7 +342,7 @@ def __init__(self, namespace: str, tool: Tool) -> None:
@override
async def async_call(
self, hass: HomeAssistant, tool_input: ToolInput, llm_context: LLMContext
) -> JsonObjectType:
) -> ToolResult | JsonObjectType:
"""Handle the intent."""
return await self.tool.async_call(
hass,
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/package_constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ hass-nabucasa==2.7.0
hassil==3.12.1
home-assistant-bluetooth==2.0.0
home-assistant-frontend==20260826.7
home-assistant-intents==2026.8.28
home-assistant-intents==2026.9.17
httpx==0.28.1
ifaddr==0.2.0
Jinja2==3.1.6
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading