mirror of
https://github.com/home-assistant/core.git
synced 2025-11-14 21:40:16 +00:00
Compare commits
7 Commits
claude/tri
...
chat-log-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a88b082a1 | ||
|
|
3a36c060f6 | ||
|
|
55bec3ea61 | ||
|
|
2f453a09ff | ||
|
|
15f42a205a | ||
|
|
45c48f25d9 | ||
|
|
646f685a8f |
@@ -7,6 +7,7 @@ from collections.abc import AsyncGenerator, AsyncIterable, Callable, Generator
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from contextvars import ContextVar
|
from contextvars import ContextVar
|
||||||
from dataclasses import asdict, dataclass, field, replace
|
from dataclasses import asdict, dataclass, field, replace
|
||||||
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Literal, TypedDict, cast
|
from typing import Any, Literal, TypedDict, cast
|
||||||
@@ -16,14 +17,18 @@ import voluptuous as vol
|
|||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError, TemplateError
|
from homeassistant.exceptions import HomeAssistantError, TemplateError
|
||||||
from homeassistant.helpers import chat_session, frame, intent, llm, template
|
from homeassistant.helpers import chat_session, frame, intent, llm, template
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
from homeassistant.util.json import JsonObjectType
|
from homeassistant.util.json import JsonObjectType
|
||||||
|
|
||||||
from . import trace
|
from . import trace
|
||||||
|
from .const import ChatLogEventType
|
||||||
from .models import ConversationInput, ConversationResult
|
from .models import ConversationInput, ConversationResult
|
||||||
|
|
||||||
DATA_CHAT_LOGS: HassKey[dict[str, ChatLog]] = HassKey("conversation_chat_logs")
|
DATA_CHAT_LOGS: HassKey[dict[str, ChatLog]] = HassKey("conversation_chat_logs")
|
||||||
|
DATA_SUBSCRIPTIONS: HassKey[
|
||||||
|
list[Callable[[str, ChatLogEventType, dict[str, Any]], None]]
|
||||||
|
] = HassKey("conversation_chat_log_subscriptions")
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
current_chat_log: ContextVar[ChatLog | None] = ContextVar(
|
current_chat_log: ContextVar[ChatLog | None] = ContextVar(
|
||||||
@@ -31,6 +36,40 @@ current_chat_log: ContextVar[ChatLog | None] = ContextVar(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_subscribe_chat_logs(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
callback_func: Callable[[str, ChatLogEventType, dict[str, Any]], None],
|
||||||
|
) -> Callable[[], None]:
|
||||||
|
"""Subscribe to all chat logs."""
|
||||||
|
subscriptions = hass.data.get(DATA_SUBSCRIPTIONS)
|
||||||
|
if subscriptions is None:
|
||||||
|
subscriptions = []
|
||||||
|
hass.data[DATA_SUBSCRIPTIONS] = subscriptions
|
||||||
|
|
||||||
|
subscriptions.append(callback_func)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def unsubscribe() -> None:
|
||||||
|
"""Unsubscribe from chat logs."""
|
||||||
|
subscriptions.remove(callback_func)
|
||||||
|
|
||||||
|
return unsubscribe
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_notify_subscribers(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
conversation_id: str,
|
||||||
|
event_type: ChatLogEventType,
|
||||||
|
data: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Notify subscribers of a chat log event."""
|
||||||
|
if subscriptions := hass.data.get(DATA_SUBSCRIPTIONS):
|
||||||
|
for callback_func in subscriptions:
|
||||||
|
callback_func(conversation_id, event_type, data)
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def async_get_chat_log(
|
def async_get_chat_log(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -63,6 +102,8 @@ def async_get_chat_log(
|
|||||||
all_chat_logs = {}
|
all_chat_logs = {}
|
||||||
hass.data[DATA_CHAT_LOGS] = all_chat_logs
|
hass.data[DATA_CHAT_LOGS] = all_chat_logs
|
||||||
|
|
||||||
|
is_new_log = session.conversation_id not in all_chat_logs
|
||||||
|
|
||||||
if chat_log := all_chat_logs.get(session.conversation_id):
|
if chat_log := all_chat_logs.get(session.conversation_id):
|
||||||
chat_log = replace(chat_log, content=chat_log.content.copy())
|
chat_log = replace(chat_log, content=chat_log.content.copy())
|
||||||
else:
|
else:
|
||||||
@@ -71,6 +112,15 @@ def async_get_chat_log(
|
|||||||
if chat_log_delta_listener:
|
if chat_log_delta_listener:
|
||||||
chat_log.delta_listener = chat_log_delta_listener
|
chat_log.delta_listener = chat_log_delta_listener
|
||||||
|
|
||||||
|
# Fire CREATED event for new chat logs before any content is added
|
||||||
|
if is_new_log:
|
||||||
|
_async_notify_subscribers(
|
||||||
|
hass,
|
||||||
|
session.conversation_id,
|
||||||
|
ChatLogEventType.CREATED,
|
||||||
|
{"chat_log": chat_log.as_dict()},
|
||||||
|
)
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
chat_log.async_add_user_content(UserContent(content=user_input.text))
|
chat_log.async_add_user_content(UserContent(content=user_input.text))
|
||||||
|
|
||||||
@@ -84,14 +134,28 @@ def async_get_chat_log(
|
|||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Chat Log opened but no assistant message was added, ignoring update"
|
"Chat Log opened but no assistant message was added, ignoring update"
|
||||||
)
|
)
|
||||||
|
# If this was a new log but nothing was added, fire DELETED to clean up
|
||||||
|
if is_new_log:
|
||||||
|
_async_notify_subscribers(
|
||||||
|
hass,
|
||||||
|
session.conversation_id,
|
||||||
|
ChatLogEventType.DELETED,
|
||||||
|
{},
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if session.conversation_id not in all_chat_logs:
|
if is_new_log:
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def do_cleanup() -> None:
|
def do_cleanup() -> None:
|
||||||
"""Handle cleanup."""
|
"""Handle cleanup."""
|
||||||
all_chat_logs.pop(session.conversation_id)
|
all_chat_logs.pop(session.conversation_id)
|
||||||
|
_async_notify_subscribers(
|
||||||
|
hass,
|
||||||
|
session.conversation_id,
|
||||||
|
ChatLogEventType.DELETED,
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
session.async_on_cleanup(do_cleanup)
|
session.async_on_cleanup(do_cleanup)
|
||||||
|
|
||||||
@@ -100,6 +164,16 @@ def async_get_chat_log(
|
|||||||
|
|
||||||
all_chat_logs[session.conversation_id] = chat_log
|
all_chat_logs[session.conversation_id] = chat_log
|
||||||
|
|
||||||
|
# For new logs, CREATED was already fired before content was added
|
||||||
|
# For existing logs, fire UPDATED
|
||||||
|
if not is_new_log:
|
||||||
|
_async_notify_subscribers(
|
||||||
|
hass,
|
||||||
|
session.conversation_id,
|
||||||
|
ChatLogEventType.UPDATED,
|
||||||
|
{"chat_log": chat_log.as_dict()},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ConverseError(HomeAssistantError):
|
class ConverseError(HomeAssistantError):
|
||||||
"""Error during initialization of conversation.
|
"""Error during initialization of conversation.
|
||||||
@@ -129,6 +203,11 @@ class SystemContent:
|
|||||||
|
|
||||||
role: Literal["system"] = field(init=False, default="system")
|
role: Literal["system"] = field(init=False, default="system")
|
||||||
content: str
|
content: str
|
||||||
|
created: datetime = field(init=False, default_factory=utcnow)
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
"""Return a dictionary representation of the content."""
|
||||||
|
return {"role": self.role, "content": self.content}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -138,6 +217,20 @@ class UserContent:
|
|||||||
role: Literal["user"] = field(init=False, default="user")
|
role: Literal["user"] = field(init=False, default="user")
|
||||||
content: str
|
content: str
|
||||||
attachments: list[Attachment] | None = field(default=None)
|
attachments: list[Attachment] | None = field(default=None)
|
||||||
|
created: datetime = field(init=False, default_factory=utcnow)
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
"""Return a dictionary representation of the content."""
|
||||||
|
result: dict[str, Any] = {
|
||||||
|
"role": self.role,
|
||||||
|
"content": self.content,
|
||||||
|
"created": self.created,
|
||||||
|
}
|
||||||
|
if self.attachments:
|
||||||
|
result["attachments"] = [
|
||||||
|
attachment.as_dict() for attachment in self.attachments
|
||||||
|
]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -153,6 +246,14 @@ class Attachment:
|
|||||||
path: Path
|
path: Path
|
||||||
"""Path to the attachment on disk."""
|
"""Path to the attachment on disk."""
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
"""Return a dictionary representation of the attachment."""
|
||||||
|
return {
|
||||||
|
"media_content_id": self.media_content_id,
|
||||||
|
"mime_type": self.mime_type,
|
||||||
|
"path": str(self.path),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class AssistantContent:
|
class AssistantContent:
|
||||||
@@ -164,6 +265,22 @@ class AssistantContent:
|
|||||||
thinking_content: str | None = None
|
thinking_content: str | None = None
|
||||||
tool_calls: list[llm.ToolInput] | None = None
|
tool_calls: list[llm.ToolInput] | None = None
|
||||||
native: Any = None
|
native: Any = None
|
||||||
|
created: datetime = field(init=False, default_factory=utcnow)
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
"""Return a dictionary representation of the content."""
|
||||||
|
result: dict[str, Any] = {
|
||||||
|
"role": self.role,
|
||||||
|
"agent_id": self.agent_id,
|
||||||
|
"created": self.created,
|
||||||
|
}
|
||||||
|
if self.content:
|
||||||
|
result["content"] = self.content
|
||||||
|
if self.thinking_content:
|
||||||
|
result["thinking_content"] = self.thinking_content
|
||||||
|
if self.tool_calls:
|
||||||
|
result["tool_calls"] = self.tool_calls
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -175,6 +292,18 @@ class ToolResultContent:
|
|||||||
tool_call_id: str
|
tool_call_id: str
|
||||||
tool_name: str
|
tool_name: str
|
||||||
tool_result: JsonObjectType
|
tool_result: JsonObjectType
|
||||||
|
created: datetime = field(init=False, default_factory=utcnow)
|
||||||
|
|
||||||
|
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,
|
||||||
|
"created": self.created,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
type Content = SystemContent | UserContent | AssistantContent | ToolResultContent
|
type Content = SystemContent | UserContent | AssistantContent | ToolResultContent
|
||||||
@@ -210,6 +339,16 @@ class ChatLog:
|
|||||||
llm_api: llm.APIInstance | None = None
|
llm_api: llm.APIInstance | None = None
|
||||||
delta_listener: Callable[[ChatLog, dict], None] | None = None
|
delta_listener: Callable[[ChatLog, dict], None] | None = None
|
||||||
llm_input_provided_index = 0
|
llm_input_provided_index = 0
|
||||||
|
created: datetime = field(init=False, default_factory=utcnow)
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
"""Return a dictionary representation of the chat log."""
|
||||||
|
return {
|
||||||
|
"conversation_id": self.conversation_id,
|
||||||
|
"continue_conversation": self.continue_conversation,
|
||||||
|
"content": [c.as_dict() for c in self.content],
|
||||||
|
"created": self.created,
|
||||||
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def continue_conversation(self) -> bool:
|
def continue_conversation(self) -> bool:
|
||||||
@@ -241,6 +380,12 @@ class ChatLog:
|
|||||||
"""Add user content to the log."""
|
"""Add user content to the log."""
|
||||||
LOGGER.debug("Adding user content: %s", content)
|
LOGGER.debug("Adding user content: %s", content)
|
||||||
self.content.append(content)
|
self.content.append(content)
|
||||||
|
_async_notify_subscribers(
|
||||||
|
self.hass,
|
||||||
|
self.conversation_id,
|
||||||
|
ChatLogEventType.CONTENT_ADDED,
|
||||||
|
{"content": content.as_dict()},
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_add_assistant_content_without_tools(
|
def async_add_assistant_content_without_tools(
|
||||||
@@ -259,6 +404,12 @@ class ChatLog:
|
|||||||
):
|
):
|
||||||
raise ValueError("Non-external tool calls not allowed")
|
raise ValueError("Non-external tool calls not allowed")
|
||||||
self.content.append(content)
|
self.content.append(content)
|
||||||
|
_async_notify_subscribers(
|
||||||
|
self.hass,
|
||||||
|
self.conversation_id,
|
||||||
|
ChatLogEventType.CONTENT_ADDED,
|
||||||
|
{"content": content.as_dict()},
|
||||||
|
)
|
||||||
|
|
||||||
async def async_add_assistant_content(
|
async def async_add_assistant_content(
|
||||||
self,
|
self,
|
||||||
@@ -317,6 +468,14 @@ class ChatLog:
|
|||||||
tool_result=tool_result,
|
tool_result=tool_result,
|
||||||
)
|
)
|
||||||
self.content.append(response_content)
|
self.content.append(response_content)
|
||||||
|
_async_notify_subscribers(
|
||||||
|
self.hass,
|
||||||
|
self.conversation_id,
|
||||||
|
ChatLogEventType.CONTENT_ADDED,
|
||||||
|
{
|
||||||
|
"content": response_content.as_dict(),
|
||||||
|
},
|
||||||
|
)
|
||||||
yield response_content
|
yield response_content
|
||||||
|
|
||||||
async def async_add_delta_content_stream(
|
async def async_add_delta_content_stream(
|
||||||
@@ -593,6 +752,12 @@ class ChatLog:
|
|||||||
self.llm_api = llm_api
|
self.llm_api = llm_api
|
||||||
self.extra_system_prompt = extra_system_prompt
|
self.extra_system_prompt = extra_system_prompt
|
||||||
self.content[0] = SystemContent(content=prompt)
|
self.content[0] = SystemContent(content=prompt)
|
||||||
|
_async_notify_subscribers(
|
||||||
|
self.hass,
|
||||||
|
self.conversation_id,
|
||||||
|
ChatLogEventType.UPDATED,
|
||||||
|
{"chat_log": self.as_dict()},
|
||||||
|
)
|
||||||
|
|
||||||
LOGGER.debug("Prompt: %s", self.content)
|
LOGGER.debug("Prompt: %s", self.content)
|
||||||
LOGGER.debug("Tools: %s", self.llm_api.tools if self.llm_api else None)
|
LOGGER.debug("Tools: %s", self.llm_api.tools if self.llm_api else None)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import IntFlag
|
from enum import IntFlag, StrEnum
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from homeassistant.util.hass_dict import HassKey
|
from homeassistant.util.hass_dict import HassKey
|
||||||
@@ -30,3 +30,13 @@ class ConversationEntityFeature(IntFlag):
|
|||||||
"""Supported features of the conversation entity."""
|
"""Supported features of the conversation entity."""
|
||||||
|
|
||||||
CONTROL = 1
|
CONTROL = 1
|
||||||
|
|
||||||
|
|
||||||
|
class ChatLogEventType(StrEnum):
|
||||||
|
"""Chat log event type."""
|
||||||
|
|
||||||
|
INITIAL_STATE = "initial_state"
|
||||||
|
CREATED = "created"
|
||||||
|
UPDATED = "updated"
|
||||||
|
DELETED = "deleted"
|
||||||
|
CONTENT_ADDED = "content_added"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from homeassistant.components import http, websocket_api
|
|||||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||||
from homeassistant.const import MATCH_ALL
|
from homeassistant.const import MATCH_ALL
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.chat_session import async_get_chat_session
|
||||||
from homeassistant.util import language as language_util
|
from homeassistant.util import language as language_util
|
||||||
|
|
||||||
from .agent_manager import (
|
from .agent_manager import (
|
||||||
@@ -20,7 +21,8 @@ from .agent_manager import (
|
|||||||
async_get_agent,
|
async_get_agent,
|
||||||
get_agent_manager,
|
get_agent_manager,
|
||||||
)
|
)
|
||||||
from .const import DATA_COMPONENT
|
from .chat_log import DATA_CHAT_LOGS, async_get_chat_log, async_subscribe_chat_logs
|
||||||
|
from .const import DATA_COMPONENT, ChatLogEventType
|
||||||
from .entity import ConversationEntity
|
from .entity import ConversationEntity
|
||||||
from .models import ConversationInput
|
from .models import ConversationInput
|
||||||
|
|
||||||
@@ -35,6 +37,8 @@ def async_setup(hass: HomeAssistant) -> None:
|
|||||||
websocket_api.async_register_command(hass, websocket_list_sentences)
|
websocket_api.async_register_command(hass, websocket_list_sentences)
|
||||||
websocket_api.async_register_command(hass, websocket_hass_agent_debug)
|
websocket_api.async_register_command(hass, websocket_hass_agent_debug)
|
||||||
websocket_api.async_register_command(hass, websocket_hass_agent_language_scores)
|
websocket_api.async_register_command(hass, websocket_hass_agent_language_scores)
|
||||||
|
websocket_api.async_register_command(hass, websocket_subscribe_chat_log)
|
||||||
|
websocket_api.async_register_command(hass, websocket_subscribe_chat_log_index)
|
||||||
|
|
||||||
|
|
||||||
@websocket_api.websocket_command(
|
@websocket_api.websocket_command(
|
||||||
@@ -265,3 +269,114 @@ class ConversationProcessView(http.HomeAssistantView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return self.json(result.as_dict())
|
return self.json(result.as_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "conversation/chat_log/subscribe",
|
||||||
|
vol.Required("conversation_id"): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.require_admin
|
||||||
|
def websocket_subscribe_chat_log(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Subscribe to a chat log."""
|
||||||
|
msg_id = msg["id"]
|
||||||
|
subscribed_conversation = msg["conversation_id"]
|
||||||
|
|
||||||
|
chat_logs = hass.data.get(DATA_CHAT_LOGS)
|
||||||
|
|
||||||
|
if not chat_logs or subscribed_conversation not in chat_logs:
|
||||||
|
connection.send_error(
|
||||||
|
msg_id,
|
||||||
|
websocket_api.ERR_NOT_FOUND,
|
||||||
|
"Conversation chat log not found",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def forward_events(conversation_id: str, event_type: str, data: dict) -> None:
|
||||||
|
"""Forward chat log events to websocket connection."""
|
||||||
|
if conversation_id != subscribed_conversation:
|
||||||
|
return
|
||||||
|
|
||||||
|
connection.send_event(
|
||||||
|
msg_id,
|
||||||
|
{
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"event_type": event_type,
|
||||||
|
"data": data,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if event_type == ChatLogEventType.DELETED:
|
||||||
|
unsubscribe()
|
||||||
|
del connection.subscriptions[msg["id"]]
|
||||||
|
|
||||||
|
unsubscribe = async_subscribe_chat_logs(hass, forward_events)
|
||||||
|
connection.subscriptions[msg["id"]] = unsubscribe
|
||||||
|
connection.send_result(msg["id"])
|
||||||
|
|
||||||
|
with (
|
||||||
|
async_get_chat_session(hass, subscribed_conversation) as session,
|
||||||
|
async_get_chat_log(hass, session) as chat_log,
|
||||||
|
):
|
||||||
|
connection.send_event(
|
||||||
|
msg_id,
|
||||||
|
{
|
||||||
|
"event_type": ChatLogEventType.INITIAL_STATE,
|
||||||
|
"data": chat_log.as_dict(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "conversation/chat_log/subscribe_index",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.require_admin
|
||||||
|
def websocket_subscribe_chat_log_index(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Subscribe to a chat log."""
|
||||||
|
msg_id = msg["id"]
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def forward_events(
|
||||||
|
conversation_id: str, event_type: ChatLogEventType, data: dict
|
||||||
|
) -> None:
|
||||||
|
"""Forward chat log events to websocket connection."""
|
||||||
|
if event_type not in (ChatLogEventType.CREATED, ChatLogEventType.DELETED):
|
||||||
|
return
|
||||||
|
|
||||||
|
connection.send_event(
|
||||||
|
msg_id,
|
||||||
|
{
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"event_type": event_type,
|
||||||
|
"data": data,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
unsubscribe = async_subscribe_chat_logs(hass, forward_events)
|
||||||
|
connection.subscriptions[msg["id"]] = unsubscribe
|
||||||
|
connection.send_result(msg["id"])
|
||||||
|
|
||||||
|
chat_logs = hass.data.get(DATA_CHAT_LOGS)
|
||||||
|
|
||||||
|
if not chat_logs:
|
||||||
|
return
|
||||||
|
|
||||||
|
connection.send_event(
|
||||||
|
msg_id,
|
||||||
|
{
|
||||||
|
"event_type": ChatLogEventType.INITIAL_STATE,
|
||||||
|
"data": [c.as_dict() for c in chat_logs.values()],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
DOMAIN = ha.DOMAIN
|
DOMAIN = ha.DOMAIN
|
||||||
|
|
||||||
DATA_EXPOSED_ENTITIES: HassKey[ExposedEntities] = HassKey(f"{DOMAIN}.exposed_entites")
|
DATA_EXPOSED_ENTITIES: HassKey[ExposedEntities] = HassKey(f"{DOMAIN}.exposed_entities")
|
||||||
DATA_STOP_HANDLER = f"{DOMAIN}.stop_handler"
|
DATA_STOP_HANDLER = f"{DOMAIN}.stop_handler"
|
||||||
|
|
||||||
SERVICE_HOMEASSISTANT_STOP: Final = "stop"
|
SERVICE_HOMEASSISTANT_STOP: Final = "stop"
|
||||||
|
|||||||
@@ -6,16 +6,19 @@
|
|||||||
You are a Home Assistant expert and help users with their tasks.
|
You are a Home Assistant expert and help users with their tasks.
|
||||||
Current time is 15:59:00. Today's date is 2025-06-14.
|
Current time is 15:59:00. Today's date is 2025-06-14.
|
||||||
''',
|
''',
|
||||||
|
'created': HAFakeDatetime(2025, 6, 14, 22, 59, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'system',
|
'role': 'system',
|
||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'attachments': None,
|
'attachments': None,
|
||||||
'content': 'Test prompt',
|
'content': 'Test prompt',
|
||||||
|
'created': HAFakeDatetime(2025, 6, 14, 22, 59, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'user',
|
'role': 'user',
|
||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'ai_task.test_task_entity',
|
'agent_id': 'ai_task.test_task_entity',
|
||||||
'content': 'Mock result',
|
'content': 'Mock result',
|
||||||
|
'created': HAFakeDatetime(2025, 6, 14, 22, 59, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
|
|||||||
@@ -9,16 +9,19 @@
|
|||||||
Only if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant.
|
Only if the user wants to control a device, tell them to expose entities to their voice assistant in Home Assistant.
|
||||||
Current time is 16:00:00. Today's date is 2024-06-03.
|
Current time is 16:00:00. Today's date is 2024-06-03.
|
||||||
''',
|
''',
|
||||||
|
'created': HAFakeDatetime(2024, 6, 3, 23, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'system',
|
'role': 'system',
|
||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'attachments': None,
|
'attachments': None,
|
||||||
'content': 'Please call the test function',
|
'content': 'Please call the test function',
|
||||||
|
'created': HAFakeDatetime(2024, 6, 3, 23, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'user',
|
'role': 'user',
|
||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.claude_conversation',
|
'agent_id': 'conversation.claude_conversation',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2024, 6, 3, 23, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': ThinkingBlock(signature='ErUBCkYIARgCIkCYXaVNJShe3A86Hp7XUzh9YsCYBbJTbQsrklTAPtJ2sP/NoB6tSzpK/nTL6CjSo2R6n0KNBIg5MH6asM2R/kmaEgyB/X1FtZq5OQAC7jUaDEPWCdcwGQ4RaBy5wiIwmRxExIlDhoY6tILoVPnOExkC/0igZxHEwxK8RU/fmw0b+o+TwAarzUitwzbo21E5Kh3pa3I6yqVROf1t2F8rFocNUeCegsWV/ytwYV+ayA==', thinking='', type='thinking'),
|
'native': ThinkingBlock(signature='ErUBCkYIARgCIkCYXaVNJShe3A86Hp7XUzh9YsCYBbJTbQsrklTAPtJ2sP/NoB6tSzpK/nTL6CjSo2R6n0KNBIg5MH6asM2R/kmaEgyB/X1FtZq5OQAC7jUaDEPWCdcwGQ4RaBy5wiIwmRxExIlDhoY6tILoVPnOExkC/0igZxHEwxK8RU/fmw0b+o+TwAarzUitwzbo21E5Kh3pa3I6yqVROf1t2F8rFocNUeCegsWV/ytwYV+ayA==', thinking='', type='thinking'),
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': 'The user asked me to call a test function.Is it a test? What would the function do? Would it violate any privacy or security policies?',
|
'thinking_content': 'The user asked me to call a test function.Is it a test? What would the function do? Would it violate any privacy or security policies?',
|
||||||
@@ -27,6 +30,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.claude_conversation',
|
'agent_id': 'conversation.claude_conversation',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2024, 6, 3, 23, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': RedactedThinkingBlock(data='EroBCkYIARgCKkBJDytPJhw//4vy3t7aE+LfIkxvkAh51cBPrAvBCo6AjgI57Zt9KWPnUVV50OQJ0KZzUFoGZG5sxg95zx4qMwkoEgz43Su3myJKckvj03waDBZLIBSeoAeRUeVsJCIwQ5edQN0sa+HNeB/KUBkoMUwV+IT0eIhcpFxnILdvxUAKM4R1o4KG3x+yO0eo/kyOKiKfrCPFQhvBVmTZPFhgA2Ow8L9gGDVipcz6x3Uu9YETGEny', type='redacted_thinking'),
|
'native': RedactedThinkingBlock(data='EroBCkYIARgCKkBJDytPJhw//4vy3t7aE+LfIkxvkAh51cBPrAvBCo6AjgI57Zt9KWPnUVV50OQJ0KZzUFoGZG5sxg95zx4qMwkoEgz43Su3myJKckvj03waDBZLIBSeoAeRUeVsJCIwQ5edQN0sa+HNeB/KUBkoMUwV+IT0eIhcpFxnILdvxUAKM4R1o4KG3x+yO0eo/kyOKiKfrCPFQhvBVmTZPFhgA2Ow8L9gGDVipcz6x3Uu9YETGEny', type='redacted_thinking'),
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -35,6 +39,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.claude_conversation',
|
'agent_id': 'conversation.claude_conversation',
|
||||||
'content': 'Certainly, calling it now!',
|
'content': 'Certainly, calling it now!',
|
||||||
|
'created': HAFakeDatetime(2024, 6, 3, 23, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': ThinkingBlock(signature='ErUBCkYIARgCIkCYXaVNJShe3A86Hp7XUzh9YsCYBbJTbQsrklTAPtJ2sP/NoB6tSzpK/nTL6CjSo2R6n0KNBIg5MH6asM2R/kmaEgyB/X1FtZq5OQAC7jUaDEPWCdcwGQ4RaBy5wiIwmRxExIlDhoY6tILoVPnOExkC/0igZxHEwxK8RU/fmw0b+o+TwAarzUitwzbo21E5Kh3pa3I6yqVROf1t2F8rFocNUeCegsWV/ytwYV+ayA==', thinking='', type='thinking'),
|
'native': ThinkingBlock(signature='ErUBCkYIARgCIkCYXaVNJShe3A86Hp7XUzh9YsCYBbJTbQsrklTAPtJ2sP/NoB6tSzpK/nTL6CjSo2R6n0KNBIg5MH6asM2R/kmaEgyB/X1FtZq5OQAC7jUaDEPWCdcwGQ4RaBy5wiIwmRxExIlDhoY6tILoVPnOExkC/0igZxHEwxK8RU/fmw0b+o+TwAarzUitwzbo21E5Kh3pa3I6yqVROf1t2F8rFocNUeCegsWV/ytwYV+ayA==', thinking='', type='thinking'),
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': "Okay, let's give it a shot. Will I pass the test?",
|
'thinking_content': "Okay, let's give it a shot. Will I pass the test?",
|
||||||
@@ -51,6 +56,7 @@
|
|||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.claude_conversation',
|
'agent_id': 'conversation.claude_conversation',
|
||||||
|
'created': HAFakeDatetime(2024, 6, 3, 23, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'tool_result',
|
'role': 'tool_result',
|
||||||
'tool_call_id': 'toolu_0123456789AbCdEfGhIjKlM',
|
'tool_call_id': 'toolu_0123456789AbCdEfGhIjKlM',
|
||||||
'tool_name': 'test_tool',
|
'tool_name': 'test_tool',
|
||||||
@@ -59,6 +65,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.claude_conversation',
|
'agent_id': 'conversation.claude_conversation',
|
||||||
'content': 'I have successfully called the function',
|
'content': 'I have successfully called the function',
|
||||||
|
'created': HAFakeDatetime(2024, 6, 3, 23, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -460,11 +467,13 @@
|
|||||||
dict({
|
dict({
|
||||||
'attachments': None,
|
'attachments': None,
|
||||||
'content': 'ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB',
|
'content': 'ANTHROPIC_MAGIC_STRING_TRIGGER_REDACTED_THINKING_46C9A13E193C177646C7398A98432ECCCE4C1253D5E2D82641AC0E52CC2876CB',
|
||||||
|
'created': HAFakeDatetime(2024, 5, 24, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'user',
|
'role': 'user',
|
||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.claude_conversation',
|
'agent_id': 'conversation.claude_conversation',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2024, 5, 24, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': RedactedThinkingBlock(data='EroBCkYIARgCKkBJDytPJhw//4vy3t7aE+LfIkxvkAh51cBPrAvBCo6AjgI57Zt9KWPnUVV50OQJ0KZzUFoGZG5sxg95zx4qMwkoEgz43Su3myJKckvj03waDBZLIBSeoAeRUeVsJCIwQ5edQN0sa+HNeB/KUBkoMUwV+IT0eIhcpFxnILdvxUAKM4R1o4KG3x+yO0eo/kyOKiKfrCPFQhvBVmTZPFhgA2Ow8L9gGDVipcz6x3Uu9YETGEny', type='redacted_thinking'),
|
'native': RedactedThinkingBlock(data='EroBCkYIARgCKkBJDytPJhw//4vy3t7aE+LfIkxvkAh51cBPrAvBCo6AjgI57Zt9KWPnUVV50OQJ0KZzUFoGZG5sxg95zx4qMwkoEgz43Su3myJKckvj03waDBZLIBSeoAeRUeVsJCIwQ5edQN0sa+HNeB/KUBkoMUwV+IT0eIhcpFxnILdvxUAKM4R1o4KG3x+yO0eo/kyOKiKfrCPFQhvBVmTZPFhgA2Ow8L9gGDVipcz6x3Uu9YETGEny', type='redacted_thinking'),
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -473,6 +482,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.claude_conversation',
|
'agent_id': 'conversation.claude_conversation',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2024, 5, 24, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': RedactedThinkingBlock(data='EroBCkYIARgCKkBJDytPJhw//4vy3t7aE+LfIkxvkAh51cBPrAvBCo6AjgI57Zt9KWPnUVV50OQJ0KZzUFoGZG5sxg95zx4qMwkoEgz43Su3myJKckvj03waDBZLIBSeoAeRUeVsJCIwQ5edQN0sa+HNeB/KUBkoMUwV+IT0eIhcpFxnILdvxUAKM4R1o4KG3x+yO0eo/kyOKiKfrCPFQhvBVmTZPFhgA2Ow8L9gGDVipcz6x3Uu9YETGEny', type='redacted_thinking'),
|
'native': RedactedThinkingBlock(data='EroBCkYIARgCKkBJDytPJhw//4vy3t7aE+LfIkxvkAh51cBPrAvBCo6AjgI57Zt9KWPnUVV50OQJ0KZzUFoGZG5sxg95zx4qMwkoEgz43Su3myJKckvj03waDBZLIBSeoAeRUeVsJCIwQ5edQN0sa+HNeB/KUBkoMUwV+IT0eIhcpFxnILdvxUAKM4R1o4KG3x+yO0eo/kyOKiKfrCPFQhvBVmTZPFhgA2Ow8L9gGDVipcz6x3Uu9YETGEny', type='redacted_thinking'),
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -481,6 +491,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.claude_conversation',
|
'agent_id': 'conversation.claude_conversation',
|
||||||
'content': 'How can I help you today?',
|
'content': 'How can I help you today?',
|
||||||
|
'created': HAFakeDatetime(2024, 5, 24, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': RedactedThinkingBlock(data='EroBCkYIARgCKkBJDytPJhw//4vy3t7aE+LfIkxvkAh51cBPrAvBCo6AjgI57Zt9KWPnUVV50OQJ0KZzUFoGZG5sxg95zx4qMwkoEgz43Su3myJKckvj03waDBZLIBSeoAeRUeVsJCIwQ5edQN0sa+HNeB/KUBkoMUwV+IT0eIhcpFxnILdvxUAKM4R1o4KG3x+yO0eo/kyOKiKfrCPFQhvBVmTZPFhgA2Ow8L9gGDVipcz6x3Uu9YETGEny', type='redacted_thinking'),
|
'native': RedactedThinkingBlock(data='EroBCkYIARgCKkBJDytPJhw//4vy3t7aE+LfIkxvkAh51cBPrAvBCo6AjgI57Zt9KWPnUVV50OQJ0KZzUFoGZG5sxg95zx4qMwkoEgz43Su3myJKckvj03waDBZLIBSeoAeRUeVsJCIwQ5edQN0sa+HNeB/KUBkoMUwV+IT0eIhcpFxnILdvxUAKM4R1o4KG3x+yO0eo/kyOKiKfrCPFQhvBVmTZPFhgA2Ow8L9gGDVipcz6x3Uu9YETGEny', type='redacted_thinking'),
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -527,11 +538,13 @@
|
|||||||
dict({
|
dict({
|
||||||
'attachments': None,
|
'attachments': None,
|
||||||
'content': "What's on the news today?",
|
'content': "What's on the news today?",
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'user',
|
'role': 'user',
|
||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.claude_conversation',
|
'agent_id': 'conversation.claude_conversation',
|
||||||
'content': "To get today's news, I'll perform a web search",
|
'content': "To get today's news, I'll perform a web search",
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': ThinkingBlock(signature='ErUBCkYIARgCIkCYXaVNJShe3A86Hp7XUzh9YsCYBbJTbQsrklTAPtJ2sP/NoB6tSzpK/nTL6CjSo2R6n0KNBIg5MH6asM2R/kmaEgyB/X1FtZq5OQAC7jUaDEPWCdcwGQ4RaBy5wiIwmRxExIlDhoY6tILoVPnOExkC/0igZxHEwxK8RU/fmw0b+o+TwAarzUitwzbo21E5Kh3pa3I6yqVROf1t2F8rFocNUeCegsWV/ytwYV+ayA==', thinking='', type='thinking'),
|
'native': ThinkingBlock(signature='ErUBCkYIARgCIkCYXaVNJShe3A86Hp7XUzh9YsCYBbJTbQsrklTAPtJ2sP/NoB6tSzpK/nTL6CjSo2R6n0KNBIg5MH6asM2R/kmaEgyB/X1FtZq5OQAC7jUaDEPWCdcwGQ4RaBy5wiIwmRxExIlDhoY6tILoVPnOExkC/0igZxHEwxK8RU/fmw0b+o+TwAarzUitwzbo21E5Kh3pa3I6yqVROf1t2F8rFocNUeCegsWV/ytwYV+ayA==', thinking='', type='thinking'),
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': "The user is asking about today's news, which requires current, real-time information. This is clearly something that requires recent information beyond my knowledge cutoff. I should use the web_search tool to find today's news.",
|
'thinking_content': "The user is asking about today's news, which requires current, real-time information. This is clearly something that requires recent information beyond my knowledge cutoff. I should use the web_search tool to find today's news.",
|
||||||
@@ -548,6 +561,7 @@
|
|||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.claude_conversation',
|
'agent_id': 'conversation.claude_conversation',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'tool_result',
|
'role': 'tool_result',
|
||||||
'tool_call_id': 'srvtoolu_12345ABC',
|
'tool_call_id': 'srvtoolu_12345ABC',
|
||||||
'tool_name': 'web_search',
|
'tool_name': 'web_search',
|
||||||
@@ -578,6 +592,7 @@
|
|||||||
2. Something incredible happened
|
2. Something incredible happened
|
||||||
Those are the main headlines making news today.
|
Those are the main headlines making news today.
|
||||||
''',
|
''',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': dict({
|
'native': dict({
|
||||||
'citation_details': list([
|
'citation_details': list([
|
||||||
dict({
|
dict({
|
||||||
|
|||||||
@@ -785,6 +785,7 @@ async def test_extended_thinking(
|
|||||||
assert chat_log.content[2].content == "Hello, how can I help you today?"
|
assert chat_log.content[2].content == "Hello, how can I help you today?"
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2024-05-24 12:00:00")
|
||||||
async def test_redacted_thinking(
|
async def test_redacted_thinking(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_config_entry_with_extended_thinking: MockConfigEntry,
|
mock_config_entry_with_extended_thinking: MockConfigEntry,
|
||||||
@@ -911,6 +912,7 @@ async def test_extended_thinking_tool_call(
|
|||||||
assert mock_create.mock_calls[1][2]["messages"] == snapshot
|
assert mock_create.mock_calls[1][2]["messages"] == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2025-10-31 12:00:00")
|
||||||
async def test_web_search(
|
async def test_web_search(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_config_entry_with_web_search: MockConfigEntry,
|
mock_config_entry_with_web_search: MockConfigEntry,
|
||||||
|
|||||||
@@ -493,6 +493,7 @@
|
|||||||
'data': dict({
|
'data': dict({
|
||||||
'chat_log_delta': dict({
|
'chat_log_delta': dict({
|
||||||
'agent_id': 'test-agent',
|
'agent_id': 'test-agent',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'tool_result',
|
'role': 'tool_result',
|
||||||
'tool_call_id': 'test_tool_id',
|
'tool_call_id': 'test_tool_id',
|
||||||
'tool_name': 'test_tool',
|
'tool_name': 'test_tool',
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from collections.abc import AsyncGenerator, Generator
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import ANY, AsyncMock, Mock, patch
|
from unittest.mock import ANY, AsyncMock, Mock, patch
|
||||||
|
|
||||||
|
from freezegun import freeze_time
|
||||||
from hassil.recognize import Intent, IntentData, RecognizeResult
|
from hassil.recognize import Intent, IntentData, RecognizeResult
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
@@ -1637,6 +1638,7 @@ async def test_pipeline_language_used_instead_of_conversation_language(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@freeze_time("2025-10-31 12:00:00")
|
||||||
async def test_chat_log_tts_streaming(
|
async def test_chat_log_tts_streaming(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
hass_ws_client: WebSocketGenerator,
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': object(
|
'native': object(
|
||||||
),
|
),
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
'content': 'Test',
|
'content': 'Test',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'tool_result',
|
'role': 'tool_result',
|
||||||
'tool_call_id': 'mock-tool-call-id',
|
'tool_call_id': 'mock-tool-call-id',
|
||||||
'tool_name': 'test_tool',
|
'tool_name': 'test_tool',
|
||||||
@@ -49,6 +52,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
'content': 'Test',
|
'content': 'Test',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -61,6 +65,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
'content': 'Test',
|
'content': 'Test',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -69,6 +74,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
'content': 'Test 2',
|
'content': 'Test 2',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -81,6 +87,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -97,6 +104,7 @@
|
|||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'tool_result',
|
'role': 'tool_result',
|
||||||
'tool_call_id': 'mock-tool-call-id',
|
'tool_call_id': 'mock-tool-call-id',
|
||||||
'tool_name': 'test_tool',
|
'tool_name': 'test_tool',
|
||||||
@@ -109,6 +117,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
'content': 'Test',
|
'content': 'Test',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -125,6 +134,7 @@
|
|||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'tool_result',
|
'role': 'tool_result',
|
||||||
'tool_call_id': 'mock-tool-call-id',
|
'tool_call_id': 'mock-tool-call-id',
|
||||||
'tool_name': 'test_tool',
|
'tool_name': 'test_tool',
|
||||||
@@ -137,6 +147,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
'content': 'Test',
|
'content': 'Test',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -153,6 +164,7 @@
|
|||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'tool_result',
|
'role': 'tool_result',
|
||||||
'tool_call_id': 'mock-tool-call-id',
|
'tool_call_id': 'mock-tool-call-id',
|
||||||
'tool_name': 'test_tool',
|
'tool_name': 'test_tool',
|
||||||
@@ -161,6 +173,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
'content': 'Test 2',
|
'content': 'Test 2',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -173,6 +186,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -197,6 +211,7 @@
|
|||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'tool_result',
|
'role': 'tool_result',
|
||||||
'tool_call_id': 'mock-tool-call-id',
|
'tool_call_id': 'mock-tool-call-id',
|
||||||
'tool_name': 'test_tool',
|
'tool_name': 'test_tool',
|
||||||
@@ -204,6 +219,7 @@
|
|||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'tool_result',
|
'role': 'tool_result',
|
||||||
'tool_call_id': 'mock-tool-call-id-2',
|
'tool_call_id': 'mock-tool-call-id-2',
|
||||||
'tool_name': 'test_tool',
|
'tool_name': 'test_tool',
|
||||||
@@ -216,6 +232,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': 'Test Thinking',
|
'thinking_content': 'Test Thinking',
|
||||||
@@ -228,6 +245,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
'content': 'Test',
|
'content': 'Test',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': 'Test Thinking',
|
'thinking_content': 'Test Thinking',
|
||||||
@@ -240,6 +258,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'mock-agent-id',
|
'agent_id': 'mock-agent-id',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': dict({
|
'native': dict({
|
||||||
'type': 'test',
|
'type': 'test',
|
||||||
'value': 'Test Native',
|
'value': 'Test Native',
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, Mock, patch
|
from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
|
from freezegun import freeze_time
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -16,7 +19,12 @@ from homeassistant.components.conversation import (
|
|||||||
UserContent,
|
UserContent,
|
||||||
async_get_chat_log,
|
async_get_chat_log,
|
||||||
)
|
)
|
||||||
from homeassistant.components.conversation.chat_log import DATA_CHAT_LOGS
|
from homeassistant.components.conversation.chat_log import (
|
||||||
|
DATA_CHAT_LOGS,
|
||||||
|
Attachment,
|
||||||
|
ChatLogEventType,
|
||||||
|
async_subscribe_chat_logs,
|
||||||
|
)
|
||||||
from homeassistant.core import Context, HomeAssistant
|
from homeassistant.core import Context, HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import chat_session, llm
|
from homeassistant.helpers import chat_session, llm
|
||||||
@@ -398,6 +406,7 @@ async def test_extra_systen_prompt(
|
|||||||
assert chat_log.content[0].content.endswith(extra_system_prompt2)
|
assert chat_log.content[0].content.endswith(extra_system_prompt2)
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2025-10-31 18:00:00")
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"prerun_tool_tasks",
|
"prerun_tool_tasks",
|
||||||
[
|
[
|
||||||
@@ -484,6 +493,7 @@ async def test_tool_call(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2025-10-31 12:00:00")
|
||||||
async def test_tool_call_exception(
|
async def test_tool_call_exception(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_conversation_input: ConversationInput,
|
mock_conversation_input: ConversationInput,
|
||||||
@@ -536,6 +546,7 @@ async def test_tool_call_exception(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2025-10-31 12:00:00")
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"deltas",
|
"deltas",
|
||||||
[
|
[
|
||||||
@@ -841,3 +852,171 @@ async def test_chat_log_continue_conversation(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert chat_log.continue_conversation is True
|
assert chat_log.continue_conversation is True
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2025-10-31 12:00:00")
|
||||||
|
async def test_chat_log_subscription(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_conversation_input: ConversationInput,
|
||||||
|
) -> None:
|
||||||
|
"""Test comprehensive chat log subscription functionality."""
|
||||||
|
|
||||||
|
# Track all events received
|
||||||
|
received_events = []
|
||||||
|
|
||||||
|
def event_callback(
|
||||||
|
conversation_id: str, event_type: ChatLogEventType, data: dict[str, Any]
|
||||||
|
) -> None:
|
||||||
|
"""Track received events."""
|
||||||
|
received_events.append((conversation_id, event_type, data))
|
||||||
|
|
||||||
|
# Subscribe to chat log events
|
||||||
|
unsubscribe = async_subscribe_chat_logs(hass, event_callback)
|
||||||
|
|
||||||
|
with (
|
||||||
|
chat_session.async_get_chat_session(hass) as session,
|
||||||
|
async_get_chat_log(hass, session, mock_conversation_input) as chat_log,
|
||||||
|
):
|
||||||
|
conversation_id = session.conversation_id
|
||||||
|
|
||||||
|
# Test adding different types of content and verify events are sent
|
||||||
|
chat_log.async_add_user_content(
|
||||||
|
UserContent(
|
||||||
|
content="Check this image",
|
||||||
|
attachments=[
|
||||||
|
Attachment(
|
||||||
|
mime_type="image/jpeg",
|
||||||
|
media_content_id="media-source://bla",
|
||||||
|
path=Path("test_image.jpg"),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Check user content with attachments event
|
||||||
|
assert received_events[-1][1] == ChatLogEventType.CONTENT_ADDED
|
||||||
|
user_event = received_events[-1][2]["content"]
|
||||||
|
assert user_event["content"] == "Check this image"
|
||||||
|
assert len(user_event["attachments"]) == 1
|
||||||
|
assert user_event["attachments"][0]["mime_type"] == "image/jpeg"
|
||||||
|
|
||||||
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
|
AssistantContent(
|
||||||
|
agent_id="test-agent", content="Hello! How can I help you?"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Check basic assistant content event
|
||||||
|
assert received_events[-1][1] == ChatLogEventType.CONTENT_ADDED
|
||||||
|
basic_event = received_events[-1][2]["content"]
|
||||||
|
assert basic_event["content"] == "Hello! How can I help you?"
|
||||||
|
assert basic_event["agent_id"] == "test-agent"
|
||||||
|
|
||||||
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
|
AssistantContent(
|
||||||
|
agent_id="test-agent",
|
||||||
|
content="Let me think about that...",
|
||||||
|
thinking_content="I need to analyze the user's request carefully.",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Check assistant content with thinking event
|
||||||
|
assert received_events[-1][1] == ChatLogEventType.CONTENT_ADDED
|
||||||
|
thinking_event = received_events[-1][2]["content"]
|
||||||
|
assert (
|
||||||
|
thinking_event["thinking_content"]
|
||||||
|
== "I need to analyze the user's request carefully."
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
|
AssistantContent(
|
||||||
|
agent_id="test-agent",
|
||||||
|
content="Here's some data:",
|
||||||
|
native={"type": "chart", "data": [1, 2, 3, 4, 5]},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Check assistant content with native event
|
||||||
|
assert received_events[-1][1] == ChatLogEventType.CONTENT_ADDED
|
||||||
|
native_event = received_events[-1][2]["content"]
|
||||||
|
assert native_event["content"] == "Here's some data:"
|
||||||
|
assert native_event["agent_id"] == "test-agent"
|
||||||
|
|
||||||
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
|
ToolResultContent(
|
||||||
|
agent_id="test-agent",
|
||||||
|
tool_call_id="test-tool-call-123",
|
||||||
|
tool_name="test_tool",
|
||||||
|
tool_result="Tool execution completed successfully",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Check tool result content event
|
||||||
|
assert received_events[-1][1] == ChatLogEventType.CONTENT_ADDED
|
||||||
|
tool_result_event = received_events[-1][2]["content"]
|
||||||
|
assert tool_result_event["tool_name"] == "test_tool"
|
||||||
|
assert (
|
||||||
|
tool_result_event["tool_result"] == "Tool execution completed successfully"
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
|
AssistantContent(
|
||||||
|
agent_id="test-agent",
|
||||||
|
content="I'll call an external service",
|
||||||
|
tool_calls=[
|
||||||
|
llm.ToolInput(
|
||||||
|
id="external-tool-call-123",
|
||||||
|
tool_name="external_api_call",
|
||||||
|
tool_args={"endpoint": "https://api.example.com/data"},
|
||||||
|
external=True,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Check external tool call event
|
||||||
|
assert received_events[-1][1] == ChatLogEventType.CONTENT_ADDED
|
||||||
|
external_tool_event = received_events[-1][2]["content"]
|
||||||
|
assert len(external_tool_event["tool_calls"]) == 1
|
||||||
|
assert external_tool_event["tool_calls"][0].tool_name == "external_api_call"
|
||||||
|
|
||||||
|
# Verify we received the expected events
|
||||||
|
# Should have: 1 CREATED event + 7 CONTENT_ADDED events
|
||||||
|
assert len(received_events) == 8
|
||||||
|
|
||||||
|
# Check the first event is CREATED
|
||||||
|
assert received_events[0][1] == ChatLogEventType.CREATED
|
||||||
|
assert received_events[0][2]["chat_log"]["conversation_id"] == conversation_id
|
||||||
|
|
||||||
|
# Check the second event is CONTENT_ADDED (from mock_conversation_input)
|
||||||
|
assert received_events[1][1] == ChatLogEventType.CONTENT_ADDED
|
||||||
|
assert received_events[1][0] == conversation_id
|
||||||
|
|
||||||
|
# Test cleanup functionality
|
||||||
|
assert conversation_id in hass.data[chat_session.DATA_CHAT_SESSION]
|
||||||
|
|
||||||
|
# Set the last updated to be older than the timeout
|
||||||
|
hass.data[chat_session.DATA_CHAT_SESSION][conversation_id].last_updated = (
|
||||||
|
dt_util.utcnow() + chat_session.CONVERSATION_TIMEOUT
|
||||||
|
)
|
||||||
|
|
||||||
|
async_fire_time_changed(
|
||||||
|
hass,
|
||||||
|
dt_util.utcnow() + chat_session.CONVERSATION_TIMEOUT * 2 + timedelta(seconds=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that DELETED event was sent
|
||||||
|
assert received_events[-1][1] == ChatLogEventType.DELETED
|
||||||
|
assert received_events[-1][0] == conversation_id
|
||||||
|
|
||||||
|
# Test that unsubscribing stops receiving events
|
||||||
|
events_before_unsubscribe = len(received_events)
|
||||||
|
unsubscribe()
|
||||||
|
|
||||||
|
# Create a new session and add content - should not receive events
|
||||||
|
with (
|
||||||
|
chat_session.async_get_chat_session(hass) as session2,
|
||||||
|
async_get_chat_log(hass, session2, mock_conversation_input) as chat_log2,
|
||||||
|
):
|
||||||
|
chat_log2.async_add_assistant_content_without_tools(
|
||||||
|
AssistantContent(
|
||||||
|
agent_id="test-agent", content="This should not be received"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify no new events were received after unsubscribing
|
||||||
|
assert len(received_events) == events_before_unsubscribe
|
||||||
|
|||||||
@@ -1,23 +1,36 @@
|
|||||||
"""The tests for the HTTP API of the Conversation component."""
|
"""The tests for the HTTP API of the Conversation component."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from freezegun import freeze_time
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.conversation import async_get_agent
|
from homeassistant.components.conversation import (
|
||||||
|
AssistantContent,
|
||||||
|
ConversationInput,
|
||||||
|
async_get_agent,
|
||||||
|
async_get_chat_log,
|
||||||
|
)
|
||||||
from homeassistant.components.conversation.const import HOME_ASSISTANT_AGENT
|
from homeassistant.components.conversation.const import HOME_ASSISTANT_AGENT
|
||||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||||
from homeassistant.const import ATTR_FRIENDLY_NAME
|
from homeassistant.const import ATTR_FRIENDLY_NAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import area_registry as ar, entity_registry as er, intent
|
from homeassistant.helpers import (
|
||||||
|
area_registry as ar,
|
||||||
|
chat_session,
|
||||||
|
entity_registry as er,
|
||||||
|
intent,
|
||||||
|
)
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from . import MockAgent
|
from . import MockAgent
|
||||||
|
|
||||||
from tests.common import async_mock_service
|
from tests.common import MockUser, async_fire_time_changed, async_mock_service
|
||||||
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
||||||
|
|
||||||
AGENT_ID_OPTIONS = [
|
AGENT_ID_OPTIONS = [
|
||||||
@@ -590,3 +603,318 @@ async def test_ws_hass_language_scores_with_filter(
|
|||||||
# GB English should be preferred
|
# GB English should be preferred
|
||||||
result = msg["result"]
|
result = msg["result"]
|
||||||
assert result["preferred_language"] == "en-GB"
|
assert result["preferred_language"] == "en-GB"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ws_chat_log_index_subscription(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
init_components,
|
||||||
|
mock_conversation_input: ConversationInput,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test that we can subscribe to chat logs."""
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
with freeze_time():
|
||||||
|
now = utcnow().isoformat()
|
||||||
|
|
||||||
|
with (
|
||||||
|
chat_session.async_get_chat_session(hass) as session,
|
||||||
|
async_get_chat_log(hass, session, mock_conversation_input) as chat_log,
|
||||||
|
):
|
||||||
|
before_sub_conversation_id = session.conversation_id
|
||||||
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
|
AssistantContent("test-agent-id", "I hear you")
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.send_json_auto_id(
|
||||||
|
{"type": "conversation/chat_log/subscribe_index"}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
event_id = msg["id"]
|
||||||
|
|
||||||
|
# 1. The INITIAL_STATE event
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg == {
|
||||||
|
"type": "event",
|
||||||
|
"id": event_id,
|
||||||
|
"event": {
|
||||||
|
"event_type": "initial_state",
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"conversation_id": before_sub_conversation_id,
|
||||||
|
"continue_conversation": False,
|
||||||
|
"created": now,
|
||||||
|
"content": [
|
||||||
|
{"role": "system", "content": ""},
|
||||||
|
{"role": "user", "content": "Hello", "created": now},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"agent_id": "test-agent-id",
|
||||||
|
"content": "I hear you",
|
||||||
|
"created": now,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with (
|
||||||
|
chat_session.async_get_chat_session(hass) as session,
|
||||||
|
async_get_chat_log(hass, session, mock_conversation_input),
|
||||||
|
):
|
||||||
|
conversation_id = session.conversation_id
|
||||||
|
|
||||||
|
# We should receive 2 events for this newly created chat:
|
||||||
|
# 1. The CREATED event (fired before content is added)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg == {
|
||||||
|
"type": "event",
|
||||||
|
"id": event_id,
|
||||||
|
"event": {
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"event_type": "created",
|
||||||
|
"data": {
|
||||||
|
"chat_log": {
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"continue_conversation": False,
|
||||||
|
"created": now,
|
||||||
|
"content": [{"role": "system", "content": ""}],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. The DELETED event (since no assistant message was added)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg == {
|
||||||
|
"type": "event",
|
||||||
|
"id": event_id,
|
||||||
|
"event": {
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"event_type": "deleted",
|
||||||
|
"data": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trigger session cleanup
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.chat_session.CONVERSATION_TIMEOUT",
|
||||||
|
timedelta(0),
|
||||||
|
):
|
||||||
|
async_fire_time_changed(hass, fire_all=True)
|
||||||
|
|
||||||
|
# 3. The DELETED event of before sub conversation
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg == {
|
||||||
|
"type": "event",
|
||||||
|
"id": event_id,
|
||||||
|
"event": {
|
||||||
|
"conversation_id": before_sub_conversation_id,
|
||||||
|
"event_type": "deleted",
|
||||||
|
"data": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ws_chat_log_index_subscription_requires_admin(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
init_components,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
hass_admin_user: MockUser,
|
||||||
|
) -> None:
|
||||||
|
"""Test that chat log subscription requires admin access."""
|
||||||
|
# Create a non-admin user
|
||||||
|
hass_admin_user.groups = []
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
await client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
"type": "conversation/chat_log/subscribe_index",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
|
||||||
|
assert not msg["success"]
|
||||||
|
assert msg["error"]["code"] == "unauthorized"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_ws_chat_log_subscription(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
init_components,
|
||||||
|
mock_conversation_input: ConversationInput,
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test that we can subscribe to chat logs."""
|
||||||
|
client = await hass_ws_client(hass)
|
||||||
|
|
||||||
|
with freeze_time():
|
||||||
|
now = utcnow().isoformat()
|
||||||
|
|
||||||
|
with (
|
||||||
|
chat_session.async_get_chat_session(hass) as session,
|
||||||
|
async_get_chat_log(hass, session, mock_conversation_input) as chat_log,
|
||||||
|
):
|
||||||
|
conversation_id = session.conversation_id
|
||||||
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
|
AssistantContent("test-agent-id", "I hear you")
|
||||||
|
)
|
||||||
|
|
||||||
|
await client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
"type": "conversation/chat_log/subscribe",
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg["success"]
|
||||||
|
event_id = msg["id"]
|
||||||
|
|
||||||
|
# 1. The INITIAL_STATE event (fired before content is added)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg == {
|
||||||
|
"type": "event",
|
||||||
|
"id": event_id,
|
||||||
|
"event": {
|
||||||
|
"event_type": "initial_state",
|
||||||
|
"data": {
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"continue_conversation": False,
|
||||||
|
"created": now,
|
||||||
|
"content": [
|
||||||
|
{"role": "system", "content": ""},
|
||||||
|
{"role": "user", "content": "Hello", "created": now},
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"agent_id": "test-agent-id",
|
||||||
|
"content": "I hear you",
|
||||||
|
"created": now,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with (
|
||||||
|
chat_session.async_get_chat_session(hass, conversation_id) as session,
|
||||||
|
async_get_chat_log(hass, session, mock_conversation_input) as chat_log,
|
||||||
|
):
|
||||||
|
chat_log.async_add_assistant_content_without_tools(
|
||||||
|
AssistantContent("test-agent-id", "I still hear you")
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. The user input content added event
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg == {
|
||||||
|
"type": "event",
|
||||||
|
"id": event_id,
|
||||||
|
"event": {
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"event_type": "content_added",
|
||||||
|
"data": {
|
||||||
|
"content": {
|
||||||
|
"content": "Hello",
|
||||||
|
"role": "user",
|
||||||
|
"created": now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3. The assistant input content added event
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg == {
|
||||||
|
"type": "event",
|
||||||
|
"id": event_id,
|
||||||
|
"event": {
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"event_type": "content_added",
|
||||||
|
"data": {
|
||||||
|
"content": {
|
||||||
|
"agent_id": "test-agent-id",
|
||||||
|
"content": "I still hear you",
|
||||||
|
"role": "assistant",
|
||||||
|
"created": now,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Forward time to mimic auto-cleanup
|
||||||
|
|
||||||
|
# 4. The UPDATED event (since no assistant message was added)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg == {
|
||||||
|
"type": "event",
|
||||||
|
"id": event_id,
|
||||||
|
"event": {
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"event_type": "updated",
|
||||||
|
"data": {
|
||||||
|
"chat_log": {
|
||||||
|
"continue_conversation": False,
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"created": now,
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": "",
|
||||||
|
"role": "system",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "Hello",
|
||||||
|
"role": "user",
|
||||||
|
"created": now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent_id": "test-agent-id",
|
||||||
|
"content": "I hear you",
|
||||||
|
"role": "assistant",
|
||||||
|
"created": now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": "Hello",
|
||||||
|
"role": "user",
|
||||||
|
"created": now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"agent_id": "test-agent-id",
|
||||||
|
"content": "I still hear you",
|
||||||
|
"role": "assistant",
|
||||||
|
"created": now,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trigger session cleanup
|
||||||
|
with patch(
|
||||||
|
"homeassistant.helpers.chat_session.CONVERSATION_TIMEOUT",
|
||||||
|
timedelta(0),
|
||||||
|
):
|
||||||
|
async_fire_time_changed(hass, fire_all=True)
|
||||||
|
|
||||||
|
# 5. The DELETED event
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert msg == {
|
||||||
|
"type": "event",
|
||||||
|
"id": event_id,
|
||||||
|
"event": {
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
"event_type": "deleted",
|
||||||
|
"data": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Subscribing now will fail
|
||||||
|
await client.send_json_auto_id(
|
||||||
|
{
|
||||||
|
"type": "conversation/chat_log/subscribe",
|
||||||
|
"conversation_id": conversation_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
msg = await client.receive_json()
|
||||||
|
assert not msg["success"]
|
||||||
|
assert msg["error"]["code"] == "not_found"
|
||||||
|
|||||||
@@ -108,11 +108,13 @@
|
|||||||
dict({
|
dict({
|
||||||
'attachments': None,
|
'attachments': None,
|
||||||
'content': 'hello',
|
'content': 'hello',
|
||||||
|
'created': HAFakeDatetime(2024, 5, 24, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'user',
|
'role': 'user',
|
||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.gpt_3_5_turbo',
|
'agent_id': 'conversation.gpt_3_5_turbo',
|
||||||
'content': 'Hello, how can I help you?',
|
'content': 'Hello, how can I help you?',
|
||||||
|
'created': HAFakeDatetime(2024, 5, 24, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -125,11 +127,13 @@
|
|||||||
dict({
|
dict({
|
||||||
'attachments': None,
|
'attachments': None,
|
||||||
'content': 'Please call the test function',
|
'content': 'Please call the test function',
|
||||||
|
'created': HAFakeDatetime(2024, 5, 24, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'user',
|
'role': 'user',
|
||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.gpt_3_5_turbo',
|
'agent_id': 'conversation.gpt_3_5_turbo',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2024, 5, 24, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -146,6 +150,7 @@
|
|||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.gpt_3_5_turbo',
|
'agent_id': 'conversation.gpt_3_5_turbo',
|
||||||
|
'created': HAFakeDatetime(2024, 5, 24, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'tool_result',
|
'role': 'tool_result',
|
||||||
'tool_call_id': 'call_call_1',
|
'tool_call_id': 'call_call_1',
|
||||||
'tool_name': 'test_tool',
|
'tool_name': 'test_tool',
|
||||||
@@ -154,6 +159,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.gpt_3_5_turbo',
|
'agent_id': 'conversation.gpt_3_5_turbo',
|
||||||
'content': 'I have successfully called the function',
|
'content': 'I have successfully called the function',
|
||||||
|
'created': HAFakeDatetime(2024, 5, 24, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
|
|||||||
@@ -39,11 +39,13 @@
|
|||||||
dict({
|
dict({
|
||||||
'attachments': None,
|
'attachments': None,
|
||||||
'content': 'Please call the test function',
|
'content': 'Please call the test function',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'user',
|
'role': 'user',
|
||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.openai_conversation',
|
'agent_id': 'conversation.openai_conversation',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': 'Thinking',
|
'thinking_content': 'Thinking',
|
||||||
@@ -52,6 +54,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.openai_conversation',
|
'agent_id': 'conversation.openai_conversation',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': ResponseReasoningItem(id='rs_A', summary=[], type='reasoning', content=None, encrypted_content='AAABBB', status=None),
|
'native': ResponseReasoningItem(id='rs_A', summary=[], type='reasoning', content=None, encrypted_content='AAABBB', status=None),
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': 'Thinking more',
|
'thinking_content': 'Thinking more',
|
||||||
@@ -60,6 +63,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.openai_conversation',
|
'agent_id': 'conversation.openai_conversation',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -76,6 +80,7 @@
|
|||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.openai_conversation',
|
'agent_id': 'conversation.openai_conversation',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'tool_result',
|
'role': 'tool_result',
|
||||||
'tool_call_id': 'call_call_1',
|
'tool_call_id': 'call_call_1',
|
||||||
'tool_name': 'test_tool',
|
'tool_name': 'test_tool',
|
||||||
@@ -84,6 +89,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.openai_conversation',
|
'agent_id': 'conversation.openai_conversation',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -100,6 +106,7 @@
|
|||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.openai_conversation',
|
'agent_id': 'conversation.openai_conversation',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'tool_result',
|
'role': 'tool_result',
|
||||||
'tool_call_id': 'call_call_2',
|
'tool_call_id': 'call_call_2',
|
||||||
'tool_name': 'test_tool',
|
'tool_name': 'test_tool',
|
||||||
@@ -108,6 +115,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.openai_conversation',
|
'agent_id': 'conversation.openai_conversation',
|
||||||
'content': 'Cool',
|
'content': 'Cool',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 12, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -171,11 +179,13 @@
|
|||||||
dict({
|
dict({
|
||||||
'attachments': None,
|
'attachments': None,
|
||||||
'content': 'Please call the test function',
|
'content': 'Please call the test function',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 18, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'user',
|
'role': 'user',
|
||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.openai_conversation',
|
'agent_id': 'conversation.openai_conversation',
|
||||||
'content': None,
|
'content': None,
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 18, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
@@ -192,6 +202,7 @@
|
|||||||
}),
|
}),
|
||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.openai_conversation',
|
'agent_id': 'conversation.openai_conversation',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 18, 0, tzinfo=datetime.timezone.utc),
|
||||||
'role': 'tool_result',
|
'role': 'tool_result',
|
||||||
'tool_call_id': 'call_call_1',
|
'tool_call_id': 'call_call_1',
|
||||||
'tool_name': 'test_tool',
|
'tool_name': 'test_tool',
|
||||||
@@ -200,6 +211,7 @@
|
|||||||
dict({
|
dict({
|
||||||
'agent_id': 'conversation.openai_conversation',
|
'agent_id': 'conversation.openai_conversation',
|
||||||
'content': 'Cool',
|
'content': 'Cool',
|
||||||
|
'created': HAFakeDatetime(2025, 10, 31, 18, 0, tzinfo=datetime.timezone.utc),
|
||||||
'native': None,
|
'native': None,
|
||||||
'role': 'assistant',
|
'role': 'assistant',
|
||||||
'thinking_content': None,
|
'thinking_content': None,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from freezegun import freeze_time
|
||||||
import httpx
|
import httpx
|
||||||
from openai import AuthenticationError, RateLimitError
|
from openai import AuthenticationError, RateLimitError
|
||||||
from openai.types.responses import (
|
from openai.types.responses import (
|
||||||
@@ -239,6 +240,7 @@ async def test_conversation_agent(
|
|||||||
assert agent.supported_languages == "*"
|
assert agent.supported_languages == "*"
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2025-10-31 12:00:00")
|
||||||
async def test_function_call(
|
async def test_function_call(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_config_entry_with_reasoning_model: MockConfigEntry,
|
mock_config_entry_with_reasoning_model: MockConfigEntry,
|
||||||
@@ -298,6 +300,7 @@ async def test_function_call(
|
|||||||
assert mock_create_stream.call_args.kwargs["input"][1:] == snapshot
|
assert mock_create_stream.call_args.kwargs["input"][1:] == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2025-10-31 18:00:00")
|
||||||
async def test_function_call_without_reasoning(
|
async def test_function_call_without_reasoning(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_config_entry_with_assist: MockConfigEntry,
|
mock_config_entry_with_assist: MockConfigEntry,
|
||||||
|
|||||||
Reference in New Issue
Block a user