Route OpenClaw chat by HA user
This commit is contained in:
@@ -47,9 +47,9 @@ from .const import (
|
||||
ATTR_ACCOUNT_ID,
|
||||
ATTR_SOURCE,
|
||||
ATTR_TIMESTAMP,
|
||||
ATTR_HA_USER_ID,
|
||||
CONF_ADDON_CONFIG_PATH,
|
||||
CONF_AGENT_ID,
|
||||
CONF_VOICE_AGENT_ID,
|
||||
CONF_GATEWAY_HOST,
|
||||
CONF_GATEWAY_PORT,
|
||||
CONF_GATEWAY_TOKEN,
|
||||
@@ -67,7 +67,6 @@ from .const import (
|
||||
CONF_THINKING_TIMEOUT,
|
||||
CONTEXT_STRATEGY_TRUNCATE,
|
||||
DEFAULT_AGENT_ID,
|
||||
DEFAULT_VOICE_AGENT_ID,
|
||||
DEFAULT_CONTEXT_MAX_CHARS,
|
||||
DEFAULT_CONTEXT_STRATEGY,
|
||||
DEFAULT_ENABLE_TOOL_CALLS,
|
||||
@@ -90,6 +89,13 @@ from .const import (
|
||||
from .coordinator import OpenClawCoordinator
|
||||
from .exposure import apply_context_policy, build_exposed_entities_context
|
||||
from .helpers import extract_text_recursive
|
||||
from .routing import (
|
||||
build_scoped_session_id,
|
||||
normalize_ha_user_id,
|
||||
normalize_optional_text,
|
||||
resolve_agent_id,
|
||||
resolve_model_override,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -107,7 +113,7 @@ _CARD_PATH = Path(__file__).parent / "www" / _CARD_FILENAME
|
||||
# URL at which the card JS is served (registered via register_static_path)
|
||||
_CARD_STATIC_URL = f"/openclaw/{_CARD_FILENAME}"
|
||||
# Versioned URL used for Lovelace resource registration to avoid stale browser cache
|
||||
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.62"
|
||||
_CARD_URL = f"{_CARD_STATIC_URL}?v=0.1.63"
|
||||
|
||||
OpenClawConfigEntry = ConfigEntry
|
||||
|
||||
@@ -125,6 +131,7 @@ SEND_MESSAGE_SCHEMA = vol.Schema(
|
||||
CLEAR_HISTORY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_SESSION_ID): cv.string,
|
||||
vol.Optional(ATTR_AGENT_ID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -399,18 +406,13 @@ async def _async_add_lovelace_resource(hass: HomeAssistant, url: str) -> bool:
|
||||
def _async_register_services(hass: HomeAssistant) -> None:
|
||||
"""Register openclaw.send_message and openclaw.clear_history services."""
|
||||
|
||||
def _normalize_optional_text(value: Any) -> str | None:
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
cleaned = value.strip()
|
||||
return cleaned or None
|
||||
|
||||
async def handle_send_message(call: ServiceCall) -> None:
|
||||
"""Handle the openclaw.send_message service call."""
|
||||
message: str = call.data[ATTR_MESSAGE]
|
||||
source: str | None = call.data.get(ATTR_SOURCE)
|
||||
session_id: str = call.data.get(ATTR_SESSION_ID) or "default"
|
||||
call_agent_id = _normalize_optional_text(call.data.get(ATTR_AGENT_ID))
|
||||
raw_session_id: str = call.data.get(ATTR_SESSION_ID) or "default"
|
||||
ha_user_id = normalize_ha_user_id(call.context.user_id)
|
||||
call_agent_id = normalize_optional_text(call.data.get(ATTR_AGENT_ID))
|
||||
extra_headers = _VOICE_REQUEST_HEADERS if source == "voice" else None
|
||||
|
||||
entry_data = _get_first_entry_data(hass)
|
||||
@@ -421,12 +423,12 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
||||
client: OpenClawApiClient = entry_data["client"]
|
||||
coordinator: OpenClawCoordinator = entry_data["coordinator"]
|
||||
options = _get_entry_options(hass, entry_data)
|
||||
voice_agent_id = _normalize_optional_text(
|
||||
options.get(CONF_VOICE_AGENT_ID, DEFAULT_VOICE_AGENT_ID)
|
||||
resolved_agent_id = resolve_agent_id(ha_user_id, call_agent_id)
|
||||
scoped_session_id = build_scoped_session_id(
|
||||
raw_session_id,
|
||||
ha_user_id,
|
||||
resolved_agent_id,
|
||||
)
|
||||
resolved_agent_id = call_agent_id
|
||||
if resolved_agent_id is None and source == "voice":
|
||||
resolved_agent_id = voice_agent_id
|
||||
|
||||
try:
|
||||
include_context = options.get(
|
||||
@@ -447,13 +449,13 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
||||
)
|
||||
system_prompt = apply_context_policy(raw_context, max_chars, strategy)
|
||||
|
||||
active_model = _normalize_optional_text(options.get("active_model"))
|
||||
active_model = resolve_model_override(options.get("active_model"))
|
||||
|
||||
_append_chat_history(hass, session_id, "user", message)
|
||||
_append_chat_history(hass, scoped_session_id, "user", message)
|
||||
|
||||
response = await client.async_send_message(
|
||||
message=message,
|
||||
session_id=session_id,
|
||||
session_id=scoped_session_id,
|
||||
system_prompt=system_prompt,
|
||||
agent_id=resolved_agent_id,
|
||||
model=active_model,
|
||||
@@ -469,7 +471,7 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
||||
+ "\n".join(f"- {line}" for line in tool_results)
|
||||
+ "\nRespond to the user based on these results."
|
||||
),
|
||||
session_id=session_id,
|
||||
session_id=scoped_session_id,
|
||||
system_prompt=system_prompt,
|
||||
agent_id=resolved_agent_id,
|
||||
model=active_model,
|
||||
@@ -486,12 +488,15 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
||||
list(response.keys()),
|
||||
)
|
||||
|
||||
_append_chat_history(hass, session_id, "assistant", assistant_message)
|
||||
_append_chat_history(hass, scoped_session_id, "assistant", assistant_message)
|
||||
hass.bus.async_fire(
|
||||
EVENT_MESSAGE_RECEIVED,
|
||||
{
|
||||
ATTR_MESSAGE: assistant_message,
|
||||
ATTR_SESSION_ID: session_id,
|
||||
ATTR_SESSION_ID: raw_session_id,
|
||||
ATTR_SESSION_KEY: scoped_session_id,
|
||||
ATTR_HA_USER_ID: ha_user_id,
|
||||
ATTR_AGENT_ID: resolved_agent_id,
|
||||
ATTR_MODEL: model_used,
|
||||
ATTR_TIMESTAMP: datetime.now(timezone.utc).isoformat(),
|
||||
},
|
||||
@@ -500,12 +505,20 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
||||
|
||||
except OpenClawApiError as err:
|
||||
_LOGGER.error("Failed to send message to OpenClaw: %s", err)
|
||||
_append_chat_history(hass, session_id, "assistant", f"OpenClaw error: {err}")
|
||||
_append_chat_history(
|
||||
hass,
|
||||
scoped_session_id,
|
||||
"assistant",
|
||||
f"OpenClaw error: {err}",
|
||||
)
|
||||
hass.bus.async_fire(
|
||||
EVENT_MESSAGE_RECEIVED,
|
||||
{
|
||||
ATTR_MESSAGE: f"OpenClaw error: {err}",
|
||||
ATTR_SESSION_ID: session_id,
|
||||
ATTR_SESSION_ID: raw_session_id,
|
||||
ATTR_SESSION_KEY: scoped_session_id,
|
||||
ATTR_HA_USER_ID: ha_user_id,
|
||||
ATTR_AGENT_ID: resolved_agent_id,
|
||||
ATTR_MODEL: "unknown",
|
||||
ATTR_TIMESTAMP: datetime.now(timezone.utc).isoformat(),
|
||||
},
|
||||
@@ -513,13 +526,32 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
||||
|
||||
async def handle_clear_history(call: ServiceCall) -> None:
|
||||
"""Handle the openclaw.clear_history service call."""
|
||||
session_id: str | None = call.data.get(ATTR_SESSION_ID)
|
||||
_LOGGER.info("Clear history requested (session=%s)", session_id or "all")
|
||||
raw_session_id: str | None = call.data.get(ATTR_SESSION_ID)
|
||||
ha_user_id = normalize_ha_user_id(call.context.user_id)
|
||||
requested_agent_id = normalize_optional_text(call.data.get(ATTR_AGENT_ID))
|
||||
agent_id = resolve_agent_id(ha_user_id, requested_agent_id)
|
||||
_LOGGER.info(
|
||||
"Clear history requested (user=%s, agent=%s, session=%s)",
|
||||
ha_user_id,
|
||||
agent_id,
|
||||
raw_session_id or "all",
|
||||
)
|
||||
store = _get_chat_history_store(hass)
|
||||
if session_id:
|
||||
store.pop(session_id, None)
|
||||
if raw_session_id:
|
||||
scoped_session_id = build_scoped_session_id(
|
||||
raw_session_id,
|
||||
ha_user_id,
|
||||
agent_id,
|
||||
)
|
||||
store.pop(scoped_session_id, None)
|
||||
else:
|
||||
store.clear()
|
||||
scoped_prefix = build_scoped_session_id("", ha_user_id, agent_id).rsplit(
|
||||
"__session_",
|
||||
1,
|
||||
)[0] + "__session_"
|
||||
for key in list(store):
|
||||
if key.startswith(scoped_prefix):
|
||||
store.pop(key, None)
|
||||
|
||||
async def handle_invoke_tool(call: ServiceCall) -> None:
|
||||
"""Handle the openclaw.invoke_tool service call."""
|
||||
@@ -793,9 +825,26 @@ def _async_register_websocket_api(hass: HomeAssistant) -> None:
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Return chat history for a session."""
|
||||
session_id = msg.get("session_id") or "default"
|
||||
history = _get_chat_history_store(hass).get(session_id, [])
|
||||
connection.send_result(msg["id"], {"session_id": session_id, "messages": history})
|
||||
raw_session_id = msg.get("session_id") or "default"
|
||||
connection_user = getattr(connection, "user", None)
|
||||
ha_user_id = normalize_ha_user_id(getattr(connection_user, "id", None))
|
||||
agent_id = resolve_agent_id(ha_user_id)
|
||||
scoped_session_id = build_scoped_session_id(
|
||||
raw_session_id,
|
||||
ha_user_id,
|
||||
agent_id,
|
||||
)
|
||||
history = _get_chat_history_store(hass).get(scoped_session_id, [])
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
"session_id": raw_session_id,
|
||||
ATTR_SESSION_KEY: scoped_session_id,
|
||||
ATTR_HA_USER_ID: ha_user_id,
|
||||
ATTR_AGENT_ID: agent_id,
|
||||
"messages": history,
|
||||
},
|
||||
)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_get_history)
|
||||
|
||||
@@ -813,6 +862,9 @@ def _async_register_websocket_api(hass: HomeAssistant) -> None:
|
||||
"""Return frontend-related integration settings."""
|
||||
entry_data = _get_first_entry_data(hass)
|
||||
options = _get_entry_options(hass, entry_data) if entry_data else {}
|
||||
connection_user = getattr(connection, "user", None)
|
||||
ha_user_id = normalize_ha_user_id(getattr(connection_user, "id", None))
|
||||
agent_id = resolve_agent_id(ha_user_id)
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
@@ -837,6 +889,8 @@ def _async_register_websocket_api(hass: HomeAssistant) -> None:
|
||||
CONF_THINKING_TIMEOUT,
|
||||
DEFAULT_THINKING_TIMEOUT,
|
||||
),
|
||||
ATTR_HA_USER_ID: ha_user_id,
|
||||
ATTR_AGENT_ID: agent_id,
|
||||
"language": hass.config.language,
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user