Fix llm tool results mutation (#167485)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Denis Shulyaka
2026-04-13 09:33:37 +02:00
committed by GitHub
co-authored by Copilot
parent a061e47bec
commit 81f8319af4
3 changed files with 81 additions and 5 deletions
+4 -4
View File
@@ -1434,16 +1434,16 @@ class IntentResponse:
def as_dict(self) -> dict[str, Any]:
"""Return a dictionary representation of an intent response."""
response_dict: dict[str, Any] = {
"speech": self.speech,
"card": self.card,
"speech": {k: dict(v) for k, v in self.speech.items()},
"card": {k: dict(v) for k, v in self.card.items()},
"language": self.language,
"response_type": self.response_type.value,
}
if self.reprompt:
response_dict["reprompt"] = self.reprompt
response_dict["reprompt"] = {k: dict(v) for k, v in self.reprompt.items()}
if self.speech_slots:
response_dict["speech_slots"] = self.speech_slots
response_dict["speech_slots"] = self.speech_slots.copy()
response_data: dict[str, Any] = {}
+3 -1
View File
@@ -11,6 +11,7 @@ async def test_async_get_result_from_chat_log(
) -> None:
"""Test getting result from chat log."""
intent_response = intent.IntentResponse(language="en")
tool_result = llm.IntentResponseDict(intent_response)
with (
chat_session.async_get_chat_session(hass) as session,
conversation.async_get_chat_log(
@@ -23,7 +24,7 @@ async def test_async_get_result_from_chat_log(
agent_id="mock-agent-id",
tool_call_id="mock-tool-call-id",
tool_name="mock-tool-name",
tool_result=llm.IntentResponseDict(intent_response),
tool_result=tool_result,
),
conversation.AssistantContent(
agent_id="mock-agent-id",
@@ -37,3 +38,4 @@ async def test_async_get_result_from_chat_log(
# Original intent response is returned with speech set
assert result.response is intent_response
assert result.response.speech["plain"]["speech"] == "This is a response."
assert tool_result["speech"] != result.response.speech
+74
View File
@@ -1,6 +1,7 @@
"""Tests for the intent helpers."""
import asyncio
from copy import deepcopy
from unittest.mock import MagicMock, patch
import pytest
@@ -983,3 +984,76 @@ async def test_get_all_entity_aliases(
state = State("light.test", "on", {"friendly_name": friendly_name})
assert intent.async_get_entity_aliases(hass, entry, state=state) == expected
async def test_intent_response_dict() -> None:
"""Test that IntentResponse.as_dict() copies mutable objects."""
response = intent.IntentResponse(
language="en",
intent=None,
)
# Prepare the intent response initial state
response.async_set_speech(
speech="Hello", speech_type="plain", extra_data={"key": "value"}
)
response.async_set_reprompt(
speech="Hi", speech_type="plain", extra_data={"key2": "value2"}
)
response.async_set_card(title="Title", content="Content", card_type="simple")
response.async_set_results(
success_results=[
intent.IntentResponseTarget(
type=intent.IntentResponseTargetType.FLOOR,
name="first floor",
id="floor-1",
)
],
failed_results=[
intent.IntentResponseTarget(
type=intent.IntentResponseTargetType.ENTITY,
name="kitchen light",
id="light.kitchen",
)
],
)
response.async_set_states(
matched_states=[State("light.kitchen", "on")],
unmatched_states=[State("light.bedroom", "off")],
)
response.async_set_speech_slots({"name": {"value": "kitchen"}})
response_dict1 = response.as_dict()
response_dict2 = deepcopy(response_dict1)
# Mutate the original object
response.async_set_speech(
speech="Changed", speech_type="plain", extra_data={"key": "changed"}
)
response.async_set_reprompt(
speech="Changed", speech_type="plain", extra_data={"key2": "changed2"}
)
response.async_set_card(title="Changed", content="Changed", card_type="simple")
response.async_set_results(
success_results=[
intent.IntentResponseTarget(
type=intent.IntentResponseTargetType.FLOOR,
name="changed floor",
id="floor-changed",
)
],
failed_results=[
intent.IntentResponseTarget(
type=intent.IntentResponseTargetType.ENTITY,
name="changed light",
id="light.changed",
)
],
)
response.async_set_states(
matched_states=[State("light.changed", "on")],
unmatched_states=[State("light.changed_bedroom", "off")],
)
response.async_set_speech_slots({"name": {"value": "changed"}})
# The original dict should not be affected by the mutations
assert response_dict1 == response_dict2