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
34 changes: 32 additions & 2 deletions homeassistant/components/conversation/chat_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ class AssistantContent:
role: Literal["assistant"] = field(init=False, default="assistant")
agent_id: str
content: str | None = None
thinking_content: str | None = None
tool_calls: list[llm.ToolInput] | None = None
native: Any = None


@dataclass(frozen=True)
Expand All @@ -183,7 +185,9 @@ class AssistantContentDeltaDict(TypedDict, total=False):

role: Literal["assistant"]
content: str | None
thinking_content: str | None
tool_calls: list[llm.ToolInput] | None
native: Any


@dataclass
Expand Down Expand Up @@ -306,6 +310,8 @@ async def async_add_delta_content_stream(
The keys content and tool_calls will be concatenated if they appear multiple times.
"""
current_content = ""
current_thinking_content = ""
current_native: Any = None
current_tool_calls: list[llm.ToolInput] = []
tool_call_tasks: dict[str, asyncio.Task] = {}

Expand All @@ -316,6 +322,14 @@ async def async_add_delta_content_stream(
if "role" not in delta:
if delta_content := delta.get("content"):
current_content += delta_content
if delta_thinking_content := delta.get("thinking_content"):
current_thinking_content += delta_thinking_content
if delta_native := delta.get("native"):
if current_native is not None:
raise RuntimeError(
"Native content already set, cannot overwrite"
)
current_native = delta_native
if delta_tool_calls := delta.get("tool_calls"):
if self.llm_api is None:
raise ValueError("No LLM API configured")
Expand All @@ -337,11 +351,18 @@ async def async_add_delta_content_stream(
raise ValueError(f"Only assistant role expected. Got {delta['role']}")

# Yield the previous message if it has content
if current_content or current_tool_calls:
if (
current_content
or current_thinking_content
or current_tool_calls
or current_native
):
content = AssistantContent(
agent_id=agent_id,
content=current_content or None,
thinking_content=current_thinking_content or None,
tool_calls=current_tool_calls or None,
native=current_native,
)
yield content
async for tool_result in self.async_add_assistant_content(
Expand All @@ -352,16 +373,25 @@ async def async_add_delta_content_stream(
self.delta_listener(self, asdict(tool_result))

current_content = delta.get("content") or ""
current_thinking_content = delta.get("thinking_content") or ""
current_tool_calls = delta.get("tool_calls") or []
current_native = delta.get("native")

if self.delta_listener:
self.delta_listener(self, delta) # type: ignore[arg-type]

if current_content or current_tool_calls:
if (
current_content
or current_thinking_content
or current_tool_calls
or current_native
):
content = AssistantContent(
agent_id=agent_id,
content=current_content or None,
thinking_content=current_thinking_content or None,
tool_calls=current_tool_calls or None,
native=current_native,
)
yield content
async for tool_result in self.async_add_assistant_content(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/knx/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"requirements": [
"xknx==3.8.0",
"xknxproject==3.8.2",
"knx-frontend==2025.8.6.52906"
"knx-frontend==2025.8.9.63154"
],
"single_config_entry": true
}
19 changes: 18 additions & 1 deletion homeassistant/components/volvo/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
VolvoAuthException,
VolvoCarsApiBaseModel,
VolvoCarsValue,
VolvoCarsValueStatusField,
VolvoCarsVehicle,
)

Expand All @@ -36,6 +37,16 @@
type CoordinatorData = dict[str, VolvoCarsApiBaseModel | None]


def _is_invalid_api_field(field: VolvoCarsApiBaseModel | None) -> bool:
if not field:
return True

if isinstance(field, VolvoCarsValueStatusField) and field.status == "ERROR":
return True

return False


class VolvoBaseCoordinator(DataUpdateCoordinator[CoordinatorData]):
"""Volvo base coordinator."""

Expand Down Expand Up @@ -121,7 +132,13 @@ async def _async_update_data(self) -> CoordinatorData:
translation_key="update_failed",
) from result

data |= cast(CoordinatorData, result)
api_data = cast(CoordinatorData, result)
data |= {
key: field
for key, field in api_data.items()
if not _is_invalid_api_field(field)
}

valid = True

# Raise an error if not a single API call succeeded
Expand Down
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.

2 changes: 1 addition & 1 deletion requirements_test_all.txt

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

2 changes: 2 additions & 0 deletions tests/components/ai_task/snapshots/test_task.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
dict({
'agent_id': 'ai_task.test_task_entity',
'content': 'Mock result',
'native': None,
'role': 'assistant',
'thinking_content': None,
'tool_calls': None,
}),
])
Expand Down
4 changes: 4 additions & 0 deletions tests/components/anthropic/snapshots/test_conversation.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
dict({
'agent_id': 'conversation.claude_conversation',
'content': 'Certainly, calling it now!',
'native': None,
'role': 'assistant',
'thinking_content': None,
'tool_calls': list([
dict({
'id': 'toolu_0123456789AbCdEfGhIjKlM',
Expand All @@ -40,7 +42,9 @@
dict({
'agent_id': 'conversation.claude_conversation',
'content': 'I have successfully called the function',
'native': None,
'role': 'assistant',
'thinking_content': None,
'tool_calls': None,
}),
])
Expand Down
68 changes: 68 additions & 0 deletions tests/components/conversation/snapshots/test_chat_log.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,27 @@
list([
])
# ---
# name: test_add_delta_content_stream[deltas10]
list([
dict({
'agent_id': 'mock-agent-id',
'content': None,
'native': object(
),
'role': 'assistant',
'thinking_content': None,
'tool_calls': None,
}),
])
# ---
# name: test_add_delta_content_stream[deltas1]
list([
dict({
'agent_id': 'mock-agent-id',
'content': 'Test',
'native': None,
'role': 'assistant',
'thinking_content': None,
'tool_calls': None,
}),
])
Expand All @@ -18,13 +33,17 @@
dict({
'agent_id': 'mock-agent-id',
'content': 'Test',
'native': None,
'role': 'assistant',
'thinking_content': None,
'tool_calls': None,
}),
dict({
'agent_id': 'mock-agent-id',
'content': 'Test 2',
'native': None,
'role': 'assistant',
'thinking_content': None,
'tool_calls': None,
}),
])
Expand All @@ -34,7 +53,9 @@
dict({
'agent_id': 'mock-agent-id',
'content': None,
'native': None,
'role': 'assistant',
'thinking_content': None,
'tool_calls': list([
dict({
'id': 'mock-tool-call-id',
Expand All @@ -59,7 +80,9 @@
dict({
'agent_id': 'mock-agent-id',
'content': 'Test',
'native': None,
'role': 'assistant',
'thinking_content': None,
'tool_calls': list([
dict({
'id': 'mock-tool-call-id',
Expand All @@ -84,7 +107,9 @@
dict({
'agent_id': 'mock-agent-id',
'content': 'Test',
'native': None,
'role': 'assistant',
'thinking_content': None,
'tool_calls': list([
dict({
'id': 'mock-tool-call-id',
Expand All @@ -105,7 +130,9 @@
dict({
'agent_id': 'mock-agent-id',
'content': 'Test 2',
'native': None,
'role': 'assistant',
'thinking_content': None,
'tool_calls': None,
}),
])
Expand All @@ -115,7 +142,9 @@
dict({
'agent_id': 'mock-agent-id',
'content': None,
'native': None,
'role': 'assistant',
'thinking_content': None,
'tool_calls': list([
dict({
'id': 'mock-tool-call-id',
Expand Down Expand Up @@ -149,6 +178,45 @@
}),
])
# ---
# name: test_add_delta_content_stream[deltas7]
list([
dict({
'agent_id': 'mock-agent-id',
'content': None,
'native': None,
'role': 'assistant',
'thinking_content': 'Test Thinking',
'tool_calls': None,
}),
])
# ---
# name: test_add_delta_content_stream[deltas8]
list([
dict({
'agent_id': 'mock-agent-id',
'content': 'Test',
'native': None,
'role': 'assistant',
'thinking_content': 'Test Thinking',
'tool_calls': None,
}),
])
# ---
# name: test_add_delta_content_stream[deltas9]
list([
dict({
'agent_id': 'mock-agent-id',
'content': None,
'native': dict({
'type': 'test',
'value': 'Test Native',
}),
'role': 'assistant',
'thinking_content': None,
'tool_calls': None,
}),
])
# ---
# name: test_template_error
dict({
'continue_conversation': False,
Expand Down
35 changes: 35 additions & 0 deletions tests/components/conversation/test_chat_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,27 @@ async def test_tool_call_exception(
]
},
],
# With thinking content
[
{"role": "assistant"},
{"thinking_content": "Test Thinking"},
],
# With content and thinking content
[
{"role": "assistant"},
{"content": "Test"},
{"thinking_content": "Test Thinking"},
],
# With native content
[
{"role": "assistant"},
{"native": {"type": "test", "value": "Test Native"}},
],
# With native object content
[
{"role": "assistant"},
{"native": object()},
],
],
)
async def test_add_delta_content_stream(
Expand Down Expand Up @@ -634,6 +655,20 @@ async def stream(deltas):
):
pass

# Second native content
with pytest.raises(RuntimeError):
async for _tool_result_content in chat_log.async_add_delta_content_stream(
"mock-agent-id",
stream(
[
{"role": "assistant"},
{"native": "Test Native"},
{"native": "Test Native 2"},
]
),
):
pass


async def test_chat_log_reuse(
hass: HomeAssistant,
Expand Down
Loading
Loading