#!/usr/bin/env python3 """ OpenClaw Client for Voice Assistant Connects to OpenClaw gateway for AI responses and command processing. """ import os import json import logging import time import threading from typing import Optional, Callable, Dict, Any from pathlib import Path try: import websocket HAS_WEBSOCKET = True except ImportError: HAS_WEBSOCKET = False try: import requests HAS_REQUESTS = True except ImportError: HAS_REQUESTS = False logger = logging.getLogger(__name__) class OpenClawClient: """ Client for OpenClaw gateway communication. Supports WebSocket and HTTP APIs. """ def __init__(self, config_path: str = "config.json"): self.config = self._load_config(config_path) self.ws_url = self.config.get("openclaw", {}).get( "ws_url", "ws://192.168.1.100:18790" ) self.api_key = self.config.get("openclaw", {}).get("api_key", "") self.enabled = self.config.get("openclaw", {}).get("enabled", True) self.ws: Optional[websocket.WebSocketApp] = None self.is_connected = False self.message_handlers = [] self.reconnect_interval = self.config.get("openclaw", {}).get( "reconnect_interval", 5 ) if HAS_WEBSOCKET and self.enabled: self._init_websocket() logger.info(f"OpenClawClient initialized (enabled={self.enabled})") def _load_config(self, config_path: str) -> dict: """Load configuration from JSON file.""" try: with open(config_path, 'r') as f: return json.load(f) except FileNotFoundError: return {"openclaw": {"enabled": False}} def _init_websocket(self): """Initialize WebSocket connection.""" if not HAS_WEBSOCKET: logger.warning("websocket-client not installed") return def on_open(ws): logger.info("WebSocket connected") self.is_connected = True self._on_connect() def on_message(ws, message): logger.debug(f"Received: {message}") self._handle_message(message) def on_error(ws, error): logger.error(f"WebSocket error: {error}") def on_close(ws, close_status_code, close_msg): logger.info(f"WebSocket closed: {close_status_code} - {close_msg}") self.is_connected = False self._reconnect() self.ws = websocket.WebSocketApp( self.ws_url, on_open=on_open, on_message=on_message, on_error=on_error, on_close=on_close ) # Start connection thread thread = threading.Thread(target=self.ws.run_forever) thread.daemon = True thread.start() def _on_connect(self): """Called when connection is established.""" # Subscribe to relevant channels or send authentication if self.api_key: auth_message = { "type": "auth", "api_key": self.api_key } self.send(json.dumps(auth_message)) def _reconnect(self): """Attempt to reconnect after disconnection.""" logger.info(f"Reconnecting in {self.reconnect_interval}s...") time.sleep(self.reconnect_interval) if self.ws: self._init_websocket() def _handle_message(self, message: str): """Handle incoming message.""" try: data = json.loads(message) for handler in self.message_handlers: handler(data) except json.JSONDecodeError: logger.warning(f"Invalid JSON: {message}") def send(self, message: str) -> bool: """ Send message via WebSocket. Args: message: JSON string to send Returns: True if sent successfully """ if not self.is_connected or not self.ws: logger.warning("Not connected to OpenClaw") return False try: self.ws.send(message) return True except Exception as e: logger.error(f"Send error: {e}") return False def send_request(self, query: str, context: Optional[Dict] = None) -> Dict: """ Send a query to OpenClaw and get response. Args: query: User query string context: Optional context dictionary Returns: Response dictionary """ if not self.enabled: return {"error": "OpenClaw client disabled"} message = { "type": "query", "query": query, "timestamp": time.time() } if context: message["context"] = context # Send via WebSocket if self.send(json.dumps(message)): # Wait for response (simplified - real implementation needs async handling) time.sleep(0.5) return {"status": "sent"} else: # Fall back to HTTP if WebSocket unavailable return self._http_request(query, context) def _http_request(self, query: str, context: Optional[Dict] = None) -> Dict: """Fallback HTTP request.""" if not HAS_REQUESTS: return {"error": "HTTP client not available"} try: response = requests.post( f"{self.ws_url.replace('ws://', 'http://').replace('wss://', 'https://')}/api/query", json={"query": query, "context": context}, headers={"Authorization": f"Bearer {self.api_key}"}, timeout=10 ) response.raise_for_status() return response.json() except Exception as e: logger.error(f"HTTP request failed: {e}") return {"error": str(e)} def add_message_handler(self, handler: Callable[[Dict], None]): """Add a handler for incoming messages.""" self.message_handlers.append(handler) def get_status(self) -> Dict: """Get client status.""" return { "enabled": self.enabled, "connected": self.is_connected, "ws_url": self.ws_url } def main(): """Test the OpenClaw client.""" client = OpenClawClient() # Add message handler def on_message(data): print(f"Received: {data}") client.add_message_handler(on_message) # Test connection print(f"OpenClaw Client Status: {client.get_status()}") # Test query response = client.send_request("Hello, how are you?") print(f"Response: {response}") # Keep alive try: while True: time.sleep(1) except KeyboardInterrupt: print("\nShutting down...") if __name__ == "__main__": logging.basicConfig(level=logging.INFO) main()