openclaw-voice-assistant/openclaw_client.py
Claw - AI Now Inc 1662bc141a Initial commit: Bilingual Voice Assistant for Google AIY Voice Kit V1
Features:
- Bilingual support (English/Mandarin Chinese)
- Hotword detection: 'Hey Osiris' / '你好 Osiris'
- Music playback control (MP3, WAV, OGG, FLAC)
- OpenClaw integration for AI responses
- Google AIY Voice Kit V1 compatible
- Text-to-speech in both languages
- Voice command recognition
- Raspberry Pi ready with installation script

AI Now Inc - Del Mar Demo Unit 🏭
2026-03-01 00:02:49 -08:00

238 lines
6.9 KiB
Python
Executable File

#!/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()