fix: address PR review round 4 — remove web UI, fix audio/import/interface issues

Remove web UI gateway (web.py, tests, docs, toolset, env vars, Platform.WEB
enum) per maintainer request — Nous is building their own official chat UI.

Fix 1: Replace sd.wait() with polling pattern in play_audio_file() to prevent
indefinite hang when audio device stalls (consistent with play_beep()).

Fix 2: Use importlib.util.find_spec() for faster_whisper/openai availability
checks instead of module-level imports that trigger heavy native library
loading (CUDA/cuDNN) at import time.

Fix 3: Remove inspect.signature() hack in _send_voice_reply() — add **kwargs
to Telegram send_voice() so all adapters accept metadata uniformly.

Fix 4: Make session loading resilient to removed platform enum values — skip
entries with unknown platforms instead of crashing the entire gateway.
This commit is contained in:
0xbyt4 2026-03-14 09:06:52 +03:00
parent 1ad5e0ed15
commit 35748a2fb0
17 changed files with 55 additions and 2930 deletions

View file

@ -31,7 +31,6 @@ class Platform(Enum):
SIGNAL = "signal"
HOMEASSISTANT = "homeassistant"
EMAIL = "email"
WEB = "web"
@dataclass
@ -177,9 +176,6 @@ class GatewayConfig:
# Email uses extra dict for config (address + imap_host + smtp_host)
elif platform == Platform.EMAIL and config.extra.get("address"):
connected.append(platform)
# Web UI uses enabled flag only
elif platform == Platform.WEB:
connected.append(platform)
return connected
def get_home_channel(self, platform: Platform) -> Optional[HomeChannel]:
@ -470,18 +466,6 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
name=os.getenv("EMAIL_HOME_ADDRESS_NAME", "Home"),
)
# Web UI
web_enabled = os.getenv("WEB_UI_ENABLED", "").lower() in ("true", "1", "yes")
if web_enabled:
if Platform.WEB not in config.platforms:
config.platforms[Platform.WEB] = PlatformConfig()
config.platforms[Platform.WEB].enabled = True
config.platforms[Platform.WEB].extra.update({
"port": int(os.getenv("WEB_UI_PORT", "8765")),
"host": os.getenv("WEB_UI_HOST", "") or "127.0.0.1",
"token": os.getenv("WEB_UI_TOKEN", ""),
})
# Session settings
idle_minutes = os.getenv("SESSION_IDLE_MINUTES")
if idle_minutes:

View file

@ -311,6 +311,7 @@ class TelegramAdapter(BasePlatformAdapter):
caption: Optional[str] = None,
reply_to: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
**kwargs,
) -> SendResult:
"""Send audio as a native Telegram voice message or audio file."""
if not self._bot:

File diff suppressed because it is too large Load diff

View file

@ -829,13 +829,6 @@ class GatewayRunner:
return None
return EmailAdapter(config)
elif platform == Platform.WEB:
from gateway.platforms.web import WebAdapter, check_web_requirements
if not check_web_requirements():
logger.warning("Web: aiohttp not installed. Run: pip install aiohttp")
return None
return WebAdapter(config)
return None
def _is_user_authorized(self, source: SessionSource) -> bool:
@ -855,11 +848,6 @@ class GatewayRunner:
if source.platform == Platform.HOMEASSISTANT:
return True
# Web UI users are authenticated via token at the WebSocket level.
# No additional allowlist check needed.
if source.platform == Platform.WEB:
return True
user_id = source.user_id
if not user_id:
return False
@ -978,7 +966,7 @@ class GatewayRunner:
"personality", "retry", "undo", "sethome", "set-home",
"compress", "usage", "insights", "reload-mcp", "reload_mcp",
"update", "title", "resume", "provider", "rollback",
"background", "reasoning", "voice", "remote-control", "remote_control"}
"background", "reasoning", "voice"}
if command and command in _known_commands:
await self.hooks.emit(f"command:{command}", {
"platform": source.platform.value if source.platform else "",
@ -1053,10 +1041,6 @@ class GatewayRunner:
if command == "voice":
return await self._handle_voice_command(event)
if command in ("remote-control", "remote_control"):
return await self._handle_remote_control_command(event)
# User-defined quick commands (bypass agent loop, no LLM call)
if command:
quick_commands = self.config.get("quick_commands", {})
@ -1741,7 +1725,6 @@ class GatewayRunner:
"`/rollback [number]` — List or restore filesystem checkpoints",
"`/background <prompt>` — Run a prompt in a separate background session",
"`/voice [on|off|tts|status]` — Toggle voice reply mode",
"`/remote-control [port] [token]` — Start web UI for remote access",
"`/reload-mcp` — Reload MCP servers from config",
"`/update` — Update Hermes Agent to the latest version",
"`/help` — Show this message",
@ -2415,10 +2398,6 @@ class GatewayRunner:
}
if event.source.thread_id:
send_kwargs["metadata"] = {"thread_id": event.source.thread_id}
import inspect
sig = inspect.signature(adapter.send_voice)
if "metadata" not in sig.parameters:
send_kwargs.pop("metadata", None)
await adapter.send_voice(**send_kwargs)
except Exception as e:
logger.warning("Auto voice reply failed: %s", e, exc_info=True)
@ -2488,62 +2467,6 @@ class GatewayRunner:
)
return f"{result['error']}"
async def _handle_remote_control_command(self, event: MessageEvent) -> str:
"""Handle /remote-control — start or show the web UI for remote access."""
from gateway.config import Platform, PlatformConfig
is_dm = event.source and event.source.chat_type == "dm"
# Already running?
if Platform.WEB in self.adapters:
adapter = self.adapters[Platform.WEB]
local_ip = adapter._get_local_ip()
token_display = adapter._token if is_dm else "(hidden — use in DM to see token)"
return (
f"Web UI already running.\n"
f"URL: http://{local_ip}:{adapter._port}\n"
f"Token: {token_display}"
)
# Start web adapter on the fly
try:
from gateway.platforms.web import WebAdapter, check_web_requirements
if not check_web_requirements():
return "Web UI requires aiohttp. Run: pip install aiohttp"
args = event.get_command_args().strip()
port = 8765
token = ""
for part in args.split():
if part.isdigit():
port = int(part)
elif part and not part.startswith("-"):
token = part
web_config = PlatformConfig(
enabled=True,
extra={"port": port, "host": "127.0.0.1", "token": token},
)
adapter = WebAdapter(web_config)
adapter.set_message_handler(self._handle_message)
success = await adapter.connect()
if not success:
return f"Failed to start Web UI on port {port}. Port may be in use."
self.adapters[Platform.WEB] = adapter
local_ip = adapter._get_local_ip()
token_display = adapter._token if is_dm else "(hidden — use in DM to see token)"
return (
f"Web UI started!\n"
f"URL: http://{local_ip}:{adapter._port}\n"
f"Token: {token_display}\n"
f"Open this URL on your phone or any device on the same network."
)
except Exception as e:
logger.error("Failed to start web UI: %s", e, exc_info=True)
return f"Failed to start Web UI: {e}"
async def _handle_background_command(self, event: MessageEvent) -> str:
"""Handle /background <prompt> — run a prompt in a separate background session.
@ -2607,7 +2530,6 @@ class GatewayRunner:
Platform.SIGNAL: "hermes-signal",
Platform.HOMEASSISTANT: "hermes-homeassistant",
Platform.EMAIL: "hermes-email",
Platform.WEB: "hermes-web",
}
platform_toolsets_config = {}
try:
@ -2629,7 +2551,6 @@ class GatewayRunner:
Platform.SIGNAL: "signal",
Platform.HOMEASSISTANT: "homeassistant",
Platform.EMAIL: "email",
Platform.WEB: "web",
}.get(source.platform, "telegram")
config_toolsets = platform_toolsets_config.get(platform_config_key)
@ -3517,7 +3438,6 @@ class GatewayRunner:
Platform.SIGNAL: "hermes-signal",
Platform.HOMEASSISTANT: "hermes-homeassistant",
Platform.EMAIL: "hermes-email",
Platform.WEB: "hermes-web",
}
# Try to load platform_toolsets from config
@ -3542,7 +3462,6 @@ class GatewayRunner:
Platform.SIGNAL: "signal",
Platform.HOMEASSISTANT: "homeassistant",
Platform.EMAIL: "email",
Platform.WEB: "web",
}.get(source.platform, "telegram")
# Use config override if present (list of toolsets), otherwise hardcoded default

View file

@ -383,7 +383,11 @@ class SessionStore:
with open(sessions_file, "r", encoding="utf-8") as f:
data = json.load(f)
for key, entry_data in data.items():
self._entries[key] = SessionEntry.from_dict(entry_data)
try:
self._entries[key] = SessionEntry.from_dict(entry_data)
except (ValueError, KeyError):
# Skip entries with unknown/removed platform values
continue
except Exception as e:
print(f"[gateway] Warning: Failed to load sessions: {e}")