feat: add configurable agent ID support
Allow users to configure which OpenClaw agent the integration communicates with, instead of always defaulting to the implicit 'main' agent. Changes: - const.py: add CONF_AGENT_ID, DEFAULT_AGENT_ID='main', ATTR_AGENT_ID - api.py: accept agent_id in constructor; _headers() now includes the x-openclaw-agent-id header on every request; async_send_message and async_stream_message accept an optional per-call agent_id override - config_flow.py: expose agent_id as a text field in both the manual setup step and the options flow (Settings → Integrations → Configure) - __init__.py: read agent_id from options/data and pass it to the API client; add optional agent_id field to the send_message service schema so automations can address a specific agent per-call - services.yaml: document the new agent_id field on send_message - strings.json / translations/en.json: add UI labels for the new option The gateway routing header x-openclaw-agent-id is always sent; when agent_id is 'main' (the default) the gateway behaviour is unchanged. A per-call override on send_message takes precedence over the config. Implemented with assistance from an AI coding agent (BMO) with human review. Bot-assisted contribution reviewed by a human before submission.
This commit is contained in:
@@ -30,6 +30,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .api import OpenClawApiClient, OpenClawApiError
|
||||
from .const import (
|
||||
ATTR_AGENT_ID,
|
||||
ATTR_ATTACHMENTS,
|
||||
ATTR_MESSAGE,
|
||||
ATTR_MODEL,
|
||||
@@ -47,6 +48,7 @@ from .const import (
|
||||
ATTR_ACCOUNT_ID,
|
||||
ATTR_TIMESTAMP,
|
||||
CONF_ADDON_CONFIG_PATH,
|
||||
CONF_AGENT_ID,
|
||||
CONF_GATEWAY_HOST,
|
||||
CONF_GATEWAY_PORT,
|
||||
CONF_GATEWAY_TOKEN,
|
||||
@@ -63,6 +65,7 @@ from .const import (
|
||||
CONF_VOICE_PROVIDER,
|
||||
CONF_THINKING_TIMEOUT,
|
||||
CONTEXT_STRATEGY_TRUNCATE,
|
||||
DEFAULT_AGENT_ID,
|
||||
DEFAULT_CONTEXT_MAX_CHARS,
|
||||
DEFAULT_CONTEXT_STRATEGY,
|
||||
DEFAULT_ENABLE_TOOL_CALLS,
|
||||
@@ -106,6 +109,7 @@ SEND_MESSAGE_SCHEMA = vol.Schema(
|
||||
vol.Required(ATTR_MESSAGE): cv.string,
|
||||
vol.Optional(ATTR_SESSION_ID): cv.string,
|
||||
vol.Optional(ATTR_ATTACHMENTS): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(ATTR_AGENT_ID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -137,6 +141,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: OpenClawConfigEntry) ->
|
||||
verify_ssl = entry.data.get(CONF_VERIFY_SSL, True)
|
||||
session = async_get_clientsession(hass, verify_ssl=verify_ssl)
|
||||
|
||||
# agent_id can come from options (user-changeable) or from initial config data
|
||||
agent_id: str = entry.options.get(
|
||||
CONF_AGENT_ID, entry.data.get(CONF_AGENT_ID, DEFAULT_AGENT_ID)
|
||||
)
|
||||
|
||||
client = OpenClawApiClient(
|
||||
host=entry.data[CONF_GATEWAY_HOST],
|
||||
port=entry.data[CONF_GATEWAY_PORT],
|
||||
@@ -144,6 +153,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: OpenClawConfigEntry) ->
|
||||
use_ssl=use_ssl,
|
||||
verify_ssl=verify_ssl,
|
||||
session=session,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
|
||||
coordinator = OpenClawCoordinator(hass, client)
|
||||
@@ -385,6 +395,8 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
||||
"""Handle the openclaw.send_message service call."""
|
||||
message: str = call.data[ATTR_MESSAGE]
|
||||
session_id: str = call.data.get(ATTR_SESSION_ID) or "default"
|
||||
# Per-call agent_id overrides the client-level default when provided.
|
||||
call_agent_id: str | None = call.data.get(ATTR_AGENT_ID)
|
||||
|
||||
entry_data = _get_first_entry_data(hass)
|
||||
if not entry_data:
|
||||
@@ -420,6 +432,7 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
||||
message=message,
|
||||
session_id=session_id,
|
||||
system_prompt=system_prompt,
|
||||
agent_id=call_agent_id,
|
||||
)
|
||||
|
||||
if options.get(CONF_ENABLE_TOOL_CALLS, DEFAULT_ENABLE_TOOL_CALLS):
|
||||
@@ -433,6 +446,7 @@ def _async_register_services(hass: HomeAssistant) -> None:
|
||||
),
|
||||
session_id=session_id,
|
||||
system_prompt=system_prompt,
|
||||
agent_id=call_agent_id,
|
||||
)
|
||||
|
||||
assistant_message = _extract_assistant_message(response)
|
||||
|
||||
@@ -51,6 +51,7 @@ class OpenClawApiClient:
|
||||
use_ssl: bool = False,
|
||||
verify_ssl: bool = True,
|
||||
session: aiohttp.ClientSession | None = None,
|
||||
agent_id: str = "main",
|
||||
) -> None:
|
||||
"""Initialize the API client.
|
||||
|
||||
@@ -61,6 +62,7 @@ class OpenClawApiClient:
|
||||
use_ssl: Use HTTPS instead of HTTP.
|
||||
verify_ssl: Verify SSL certificates (set False for self-signed certs).
|
||||
session: Optional aiohttp session (reused from HA).
|
||||
agent_id: Target OpenClaw agent ID (default: "main").
|
||||
"""
|
||||
self._host = host
|
||||
self._port = port
|
||||
@@ -68,6 +70,7 @@ class OpenClawApiClient:
|
||||
self._use_ssl = use_ssl
|
||||
self._verify_ssl = verify_ssl
|
||||
self._session = session
|
||||
self._agent_id = agent_id
|
||||
self._base_url = f"{'https' if use_ssl else 'http'}://{host}:{port}"
|
||||
# ssl=False disables cert verification for self-signed certs;
|
||||
# ssl=None uses default verification.
|
||||
@@ -82,11 +85,18 @@ class OpenClawApiClient:
|
||||
"""Update the authentication token (e.g., after addon restart)."""
|
||||
self._token = token
|
||||
|
||||
def _headers(self) -> dict[str, str]:
|
||||
"""Build request headers with auth token."""
|
||||
def _headers(self, agent_id: str | None = None) -> dict[str, str]:
|
||||
"""Build request headers with auth token and agent ID.
|
||||
|
||||
Args:
|
||||
agent_id: Per-call agent ID override. Falls back to the
|
||||
client-level ``agent_id`` set in the constructor.
|
||||
"""
|
||||
effective_agent = agent_id or self._agent_id or "main"
|
||||
return {
|
||||
"Authorization": f"Bearer {self._token}",
|
||||
"Content-Type": "application/json",
|
||||
"x-openclaw-agent-id": effective_agent,
|
||||
}
|
||||
|
||||
async def _get_session(self) -> aiohttp.ClientSession:
|
||||
@@ -182,6 +192,7 @@ class OpenClawApiClient:
|
||||
model: str | None = None,
|
||||
system_prompt: str | None = None,
|
||||
stream: bool = False,
|
||||
agent_id: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Send a chat message (non-streaming).
|
||||
|
||||
@@ -190,6 +201,8 @@ class OpenClawApiClient:
|
||||
session_id: Optional session/conversation ID.
|
||||
model: Optional model override.
|
||||
stream: If True, raises ValueError (use async_stream_message).
|
||||
agent_id: Optional per-call agent ID override (overrides the
|
||||
client-level default set in the constructor).
|
||||
|
||||
Returns:
|
||||
Complete chat completion response.
|
||||
@@ -213,7 +226,7 @@ class OpenClawApiClient:
|
||||
payload["model"] = model
|
||||
|
||||
# Pass session_id as a custom header or param if supported by gateway
|
||||
headers = self._headers()
|
||||
headers = self._headers(agent_id=agent_id)
|
||||
if session_id:
|
||||
headers["X-Session-Id"] = session_id
|
||||
headers["x-openclaw-session-key"] = session_id
|
||||
@@ -247,6 +260,7 @@ class OpenClawApiClient:
|
||||
session_id: str | None = None,
|
||||
model: str | None = None,
|
||||
system_prompt: str | None = None,
|
||||
agent_id: str | None = None,
|
||||
) -> AsyncIterator[str]:
|
||||
"""Send a chat message and stream the response via SSE.
|
||||
|
||||
@@ -256,6 +270,8 @@ class OpenClawApiClient:
|
||||
message: The user message text.
|
||||
session_id: Optional session/conversation ID.
|
||||
model: Optional model override.
|
||||
agent_id: Optional per-call agent ID override (overrides the
|
||||
client-level default set in the constructor).
|
||||
|
||||
Yields:
|
||||
Content delta strings from the streaming response.
|
||||
@@ -275,7 +291,7 @@ class OpenClawApiClient:
|
||||
if model:
|
||||
payload["model"] = model
|
||||
|
||||
headers = self._headers()
|
||||
headers = self._headers(agent_id=agent_id)
|
||||
if session_id:
|
||||
headers["X-Session-Id"] = session_id
|
||||
headers["x-openclaw-session-key"] = session_id
|
||||
|
||||
@@ -34,6 +34,7 @@ from .const import (
|
||||
ADDON_SLUG,
|
||||
ADDON_SLUG_FRAGMENTS,
|
||||
CONF_ADDON_CONFIG_PATH,
|
||||
CONF_AGENT_ID,
|
||||
CONF_GATEWAY_HOST,
|
||||
CONF_GATEWAY_PORT,
|
||||
CONF_GATEWAY_TOKEN,
|
||||
@@ -52,6 +53,7 @@ from .const import (
|
||||
BROWSER_VOICE_LANGUAGES,
|
||||
CONTEXT_STRATEGY_CLEAR,
|
||||
CONTEXT_STRATEGY_TRUNCATE,
|
||||
DEFAULT_AGENT_ID,
|
||||
DEFAULT_GATEWAY_HOST,
|
||||
DEFAULT_GATEWAY_PORT,
|
||||
DEFAULT_CONTEXT_MAX_CHARS,
|
||||
@@ -411,6 +413,7 @@ class OpenClawConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_GATEWAY_TOKEN: token,
|
||||
CONF_USE_SSL: use_ssl,
|
||||
CONF_VERIFY_SSL: verify_ssl,
|
||||
CONF_AGENT_ID: user_input.get(CONF_AGENT_ID, DEFAULT_AGENT_ID),
|
||||
},
|
||||
)
|
||||
if "base" not in errors:
|
||||
@@ -429,6 +432,7 @@ class OpenClawConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
vol.Required(CONF_GATEWAY_TOKEN): str,
|
||||
vol.Optional(CONF_USE_SSL, default=False): bool,
|
||||
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
|
||||
vol.Optional(CONF_AGENT_ID, default=DEFAULT_AGENT_ID): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
@@ -453,6 +457,13 @@ class OpenClawOptionsFlow(OptionsFlow):
|
||||
selected_provider = options.get(CONF_VOICE_PROVIDER, DEFAULT_VOICE_PROVIDER)
|
||||
|
||||
schema: dict[Any, Any] = {
|
||||
vol.Optional(
|
||||
CONF_AGENT_ID,
|
||||
default=options.get(
|
||||
CONF_AGENT_ID,
|
||||
self._config_entry.data.get(CONF_AGENT_ID, DEFAULT_AGENT_ID),
|
||||
),
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_INCLUDE_EXPOSED_CONTEXT,
|
||||
default=options.get(
|
||||
|
||||
@@ -23,6 +23,7 @@ CONF_GATEWAY_TOKEN = "gateway_token"
|
||||
CONF_USE_SSL = "use_ssl"
|
||||
CONF_VERIFY_SSL = "verify_ssl"
|
||||
CONF_ADDON_CONFIG_PATH = "addon_config_path"
|
||||
CONF_AGENT_ID = "agent_id"
|
||||
|
||||
# Options
|
||||
CONF_INCLUDE_EXPOSED_CONTEXT = "include_exposed_context"
|
||||
@@ -36,6 +37,7 @@ CONF_VOICE_PROVIDER = "voice_provider"
|
||||
CONF_BROWSER_VOICE_LANGUAGE = "browser_voice_language"
|
||||
CONF_THINKING_TIMEOUT = "thinking_timeout"
|
||||
|
||||
DEFAULT_AGENT_ID = "main"
|
||||
DEFAULT_INCLUDE_EXPOSED_CONTEXT = True
|
||||
DEFAULT_CONTEXT_MAX_CHARS = 13000
|
||||
DEFAULT_CONTEXT_STRATEGY = "truncate"
|
||||
@@ -130,6 +132,7 @@ ATTR_SESSION_KEY = "session_key"
|
||||
ATTR_DRY_RUN = "dry_run"
|
||||
ATTR_MESSAGE_CHANNEL = "message_channel"
|
||||
ATTR_ACCOUNT_ID = "account_id"
|
||||
ATTR_AGENT_ID = "agent_id"
|
||||
ATTR_OK = "ok"
|
||||
ATTR_RESULT = "result"
|
||||
ATTR_ERROR = "error"
|
||||
|
||||
@@ -24,6 +24,15 @@ send_message:
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
agent_id:
|
||||
name: Agent ID
|
||||
description: >
|
||||
Optional OpenClaw agent ID to route this message to. Overrides the
|
||||
agent ID configured in the integration options. Defaults to "main".
|
||||
required: false
|
||||
example: "main"
|
||||
selector:
|
||||
text:
|
||||
|
||||
clear_history:
|
||||
name: Clear History
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"gateway_port": "Gateway Port",
|
||||
"gateway_token": "Gateway Token",
|
||||
"use_ssl": "Use SSL (HTTPS)",
|
||||
"verify_ssl": "Verify SSL certificate"
|
||||
"verify_ssl": "Verify SSL certificate",
|
||||
"agent_id": "Agent ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -37,6 +38,7 @@
|
||||
"title": "OpenClaw Options",
|
||||
"description": "Configure context and tool-calling behavior.",
|
||||
"data": {
|
||||
"agent_id": "Agent ID (e.g. main)",
|
||||
"include_exposed_context": "Include exposed entities context",
|
||||
"context_max_chars": "Max context characters",
|
||||
"context_strategy": "When context exceeds max",
|
||||
@@ -124,6 +126,10 @@
|
||||
"attachments": {
|
||||
"name": "Attachments",
|
||||
"description": "Optional file attachments."
|
||||
},
|
||||
"agent_id": {
|
||||
"name": "Agent ID",
|
||||
"description": "Optional OpenClaw agent ID to route this message to. Overrides the configured default. Defaults to \"main\"."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
"gateway_port": "Gateway Port",
|
||||
"gateway_token": "Gateway Token",
|
||||
"use_ssl": "Use SSL (HTTPS)",
|
||||
"verify_ssl": "Verify SSL certificate"
|
||||
"verify_ssl": "Verify SSL certificate",
|
||||
"agent_id": "Agent ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -39,6 +40,7 @@
|
||||
"title": "OpenClaw Options",
|
||||
"description": "Configure context and tool-calling behavior.",
|
||||
"data": {
|
||||
"agent_id": "Agent ID (e.g. main)",
|
||||
"include_exposed_context": "Include exposed entities context",
|
||||
"context_max_chars": "Max context characters",
|
||||
"context_strategy": "When context exceeds max",
|
||||
@@ -126,6 +128,10 @@
|
||||
"attachments": {
|
||||
"name": "Attachments",
|
||||
"description": "Optional file attachments."
|
||||
},
|
||||
"agent_id": {
|
||||
"name": "Agent ID",
|
||||
"description": "Optional OpenClaw agent ID to route this message to. Overrides the configured default. Defaults to \"main\"."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user