"""Conversation agent for OpenClaw integration.""" from __future__ import annotations import asyncio import json import logging from typing import Any import aiohttp import voluptuous as vol from homeassistant.components.conversation import ( AbstractConversationAgent, ConversationInput, ConversationResult, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.intent import IntentResponse from .const import ( CONF_AGENT_NAME, CONF_OPENCLAW_HOST, CONF_OPENCLAW_PORT, CONF_TIMEOUT, DEFAULT_AGENT, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_TIMEOUT, DOMAIN, OPENCLAW_API_PATH, ) _LOGGER = logging.getLogger(__name__) # Schema for configuration CONFIG_SCHEMA = vol.Schema({ vol.Optional(CONF_OPENCLAW_HOST, default=DEFAULT_HOST): str, vol.Optional(CONF_OPENCLAW_PORT, default=DEFAULT_PORT): int, vol.Optional(CONF_AGENT_NAME, default=DEFAULT_AGENT): str, vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): int, }) class OpenClawAgent(AbstractConversationAgent): """OpenClaw conversation agent.""" def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None: """Initialize the agent.""" self.hass = hass self.config = config self.host = config.get(CONF_OPENCLAW_HOST, DEFAULT_HOST) self.port = config.get(CONF_OPENCLAW_PORT, DEFAULT_PORT) self.agent_name = config.get(CONF_AGENT_NAME, DEFAULT_AGENT) self.timeout = config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT) @property def supported_languages(self) -> list[str]: """Return a list of supported languages.""" return ["en"] # OpenClaw primarily supports English @property def attribution(self) -> dict[str, str] | None: """Return attribution information.""" return { "name": "OpenClaw", "url": "https://github.com/homeai/openclaw", } async def async_process( self, user_input: ConversationInput ) -> ConversationResult: """Process a sentence and return a response.""" text = user_input.text conversation_id = user_input.conversation_id _LOGGER.debug("Processing message: %s", text) try: response_text = await self._call_openclaw(text) # Create proper IntentResponse for Home Assistant intent_response = IntentResponse(language=user_input.language or "en") intent_response.async_set_speech(response_text) return ConversationResult( response=intent_response, conversation_id=conversation_id, ) except Exception as err: _LOGGER.error("Error calling OpenClaw: %s", err) intent_response = IntentResponse(language=user_input.language or "en") intent_response.async_set_speech("I'm sorry, I encountered an error processing your request.") return ConversationResult( response=intent_response, conversation_id=conversation_id, ) async def _call_openclaw(self, message: str) -> str: """Call OpenClaw API and return the response.""" url = f"http://{self.host}:{self.port}{OPENCLAW_API_PATH}" payload = { "message": message, "agent": self.agent_name, } session = async_get_clientsession(self.hass) try: async with asyncio.timeout(self.timeout): async with session.post( url, json=payload, headers={"Content-Type": "application/json"}, ) as response: if response.status != 200: _LOGGER.error( "OpenClaw returned status %s: %s", response.status, await response.text(), ) return "I'm sorry, I couldn't process that request." data = await response.json() return data.get("response", "I didn't get a response.") except asyncio.TimeoutError: _LOGGER.error("Timeout calling OpenClaw") return "I'm sorry, the request timed out." except aiohttp.ClientError as err: _LOGGER.error("Error connecting to OpenClaw: %s", err) return "I'm sorry, I couldn't connect to the OpenClaw service." class OpenClawCLIAgent(AbstractConversationAgent): """OpenClaw conversation agent using CLI (fallback if HTTP API unavailable).""" def __init__(self, hass: HomeAssistant, config: dict[str, Any]) -> None: """Initialize the agent.""" self.hass = hass self.config = config self.agent_name = config.get(CONF_AGENT_NAME, DEFAULT_AGENT) self.timeout = config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT) @property def supported_languages(self) -> list[str]: """Return a list of supported languages.""" return ["en"] @property def attribution(self) -> dict[str, str] | None: """Return attribution information.""" return { "name": "OpenClaw", "url": "https://github.com/homeai/openclaw", } async def async_process( self, user_input: ConversationInput ) -> ConversationResult: """Process a sentence using OpenClaw CLI.""" text = user_input.text conversation_id = user_input.conversation_id _LOGGER.debug("Processing message via CLI: %s", text) try: response_text = await self._call_openclaw_cli(text) # Create proper IntentResponse for Home Assistant intent_response = IntentResponse(language=user_input.language or "en") intent_response.async_set_speech(response_text) return ConversationResult( response=intent_response, conversation_id=conversation_id, ) except Exception as err: _LOGGER.error("Error calling OpenClaw CLI: %s", err) intent_response = IntentResponse(language=user_input.language or "en") intent_response.async_set_speech("I'm sorry, I encountered an error processing your request.") return ConversationResult( response=intent_response, conversation_id=conversation_id, ) async def _call_openclaw_cli(self, message: str) -> str: """Call OpenClaw CLI and return the response.""" cmd = [ "openclaw", "agent", "--message", message, "--agent", self.agent_name, ] proc = None try: proc = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await asyncio.wait_for( proc.communicate(), timeout=self.timeout, ) if proc.returncode != 0: _LOGGER.error("OpenClaw CLI failed: %s", stderr.decode().strip()) return "I'm sorry, I couldn't process that request." return stdout.decode().strip() except asyncio.TimeoutError: if proc is not None: proc.kill() await proc.wait() _LOGGER.error("Timeout calling OpenClaw CLI") return "I'm sorry, the request timed out." except FileNotFoundError: _LOGGER.error("OpenClaw CLI not found") return "I'm sorry, OpenClaw is not available."