From 72e7c0ce3406db4832c91e5c8958175217dd8323 Mon Sep 17 00:00:00 2001 From: alt-glitch Date: Tue, 21 Apr 2026 16:20:45 +0530 Subject: [PATCH] fix: declare undeclared soft deps in extras and remove silent import guards Previously mutagen, aiohttp-socks, tiktoken, Pillow, psutil, datasets, neutts, and soundfile were used behind try/except ImportError with silent fallbacks, masking broken functionality at runtime. Declare each in its natural extra (messaging, cli, mcp, rl, new tts-local) so they get installed, and remove the guards so missing deps crash loudly. --- gateway/platforms/base.py | 28 ++++++---------------------- gateway/platforms/discord.py | 3 ++- hermes_cli/clipboard.py | 5 +---- hermes_cli/tools_config.py | 11 +++-------- pyproject.toml | 8 +++++--- tools/mcp_tool.py | 8 +++----- tools/neutts_synth.py | 14 +++----------- tools/process_registry.py | 11 ++++------- tools/vision_tools.py | 11 ++--------- tui_gateway/server.py | 4 ++-- 10 files changed, 31 insertions(+), 72 deletions(-) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 546ce2fe39..69cfbf9ce8 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -183,18 +183,10 @@ def proxy_kwargs_for_bot(proxy_url: str | None) -> dict: if not proxy_url: return {} if proxy_url.lower().startswith("socks"): - try: - from aiohttp_socks import ProxyConnector + from aiohttp_socks import ProxyConnector - connector = ProxyConnector.from_url(proxy_url, rdns=True) - return {"connector": connector} - except ImportError: - logger.warning( - "aiohttp_socks not installed — SOCKS proxy %s ignored. " - "Run: pip install aiohttp-socks", - proxy_url, - ) - return {} + connector = ProxyConnector.from_url(proxy_url, rdns=True) + return {"connector": connector} return {"proxy": proxy_url} @@ -216,18 +208,10 @@ def proxy_kwargs_for_aiohttp(proxy_url: str | None) -> tuple[dict, dict]: if not proxy_url: return {}, {} if proxy_url.lower().startswith("socks"): - try: - from aiohttp_socks import ProxyConnector + from aiohttp_socks import ProxyConnector - connector = ProxyConnector.from_url(proxy_url, rdns=True) - return {"connector": connector}, {} - except ImportError: - logger.warning( - "aiohttp_socks not installed — SOCKS proxy %s ignored. " - "Run: pip install aiohttp-socks", - proxy_url, - ) - return {}, {} + connector = ProxyConnector.from_url(proxy_url, rdns=True) + return {"connector": connector}, {} return {}, {"proxy": proxy_url} diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index fc0612888f..26956685e7 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -1194,9 +1194,10 @@ class DiscordAdapter(BasePlatformAdapter): try: import base64 + from mutagen.oggopus import OggOpus + duration_secs = 5.0 try: - from mutagen.oggopus import OggOpus info = OggOpus(audio_path) duration_secs = info.info.length except Exception: diff --git a/hermes_cli/clipboard.py b/hermes_cli/clipboard.py index f83cd76c50..7b616db5da 100644 --- a/hermes_cli/clipboard.py +++ b/hermes_cli/clipboard.py @@ -395,14 +395,11 @@ def _wayland_save(dest: Path) -> bool: def _convert_to_png(path: Path) -> bool: """Convert an image file to PNG in-place (requires Pillow or ImageMagick).""" - # Try Pillow first (likely installed in the venv) + from PIL import Image try: - from PIL import Image img = Image.open(path) img.save(path, "PNG") return True - except ImportError: - pass except Exception as e: logger.debug("Pillow BMP→PNG conversion failed: %s", e) diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 4a78ab985f..632c710121 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -710,19 +710,14 @@ def _estimate_tool_tokens() -> Dict[str, int]: OpenAI-format tool schema. Triggers tool discovery on first call, then caches the result for the rest of the process. - Returns an empty dict when tiktoken or the registry is unavailable. + Returns an empty dict when the registry is unavailable. """ global _tool_token_cache if _tool_token_cache is not None: return _tool_token_cache - try: - import tiktoken - enc = tiktoken.get_encoding("cl100k_base") - except Exception: - logger.debug("tiktoken unavailable; skipping tool token estimation") - _tool_token_cache = {} - return _tool_token_cache + import tiktoken + enc = tiktoken.get_encoding("cl100k_base") try: # Trigger full tool discovery (imports all tool modules). diff --git a/pyproject.toml b/pyproject.toml index efe8d87c0e..e06908cb06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,11 +40,11 @@ dependencies = [ modal = ["modal>=1.0.0,<2"] daytona = ["daytona>=0.148.0,<1"] dev = ["debugpy>=1.8.0,<2", "pytest>=9.0.2,<10", "pytest-asyncio>=1.3.0,<2", "pytest-xdist>=3.0,<4", "mcp>=1.2.0,<2"] -messaging = ["python-telegram-bot[webhooks]>=22.6,<23", "discord.py[voice]>=2.7.1,<3", "aiohttp>=3.13.3,<4", "slack-bolt>=1.18.0,<2", "slack-sdk>=3.27.0,<4", "qrcode>=7.0,<8"] +messaging = ["python-telegram-bot[webhooks]>=22.6,<23", "discord.py[voice]>=2.7.1,<3", "aiohttp>=3.13.3,<4", "slack-bolt>=1.18.0,<2", "slack-sdk>=3.27.0,<4", "qrcode>=7.0,<8", "mutagen>=1.45,<2", "aiohttp-socks>=0.9,<1"] cron = ["croniter>=6.0.0,<7"] slack = ["slack-bolt>=1.18.0,<2", "slack-sdk>=3.27.0,<4"] matrix = ["mautrix[encryption]>=0.20,<1", "Markdown>=3.6,<4", "aiosqlite>=0.20", "asyncpg>=0.29"] -cli = ["simple-term-menu>=1.0,<2"] +cli = ["simple-term-menu>=1.0,<2", "tiktoken>=0.7,<1", "Pillow>=10,<12"] tts-premium = ["elevenlabs>=1.0,<2"] voice = [ # Local STT pulls in wheel-only transitive deps (ctranslate2, onnxruntime), @@ -58,7 +58,7 @@ pty = [ "pywinpty>=2.0.0,<3; sys_platform == 'win32'", ] honcho = ["honcho-ai>=2.0.1,<3"] -mcp = ["mcp>=1.2.0,<2"] +mcp = ["mcp>=1.2.0,<2", "psutil>=5.9,<7"] homeassistant = ["aiohttp>=3.9.0,<4"] sms = ["aiohttp>=3.9.0,<4"] acp = ["agent-client-protocol>=0.9.0,<1.0"] @@ -85,7 +85,9 @@ rl = [ "fastapi>=0.104.0,<1", "uvicorn[standard]>=0.24.0,<1", "wandb>=0.15.0,<1", + "datasets>=2.14,<3", ] +tts-local = ["neutts[all]", "soundfile>=0.12,<1"] yc-bench = ["yc-bench @ git+https://github.com/collinear-ai/yc-bench.git@bfb0c88062450f46341bd9a5298903fc2e952a5c ; python_version >= '3.12'"] all = [ "hermes-agent[modal]", diff --git a/tools/mcp_tool.py b/tools/mcp_tool.py index a0a22773e5..cdd1035594 100644 --- a/tools/mcp_tool.py +++ b/tools/mcp_tool.py @@ -1453,13 +1453,11 @@ def _snapshot_child_pids() -> set: pass # Fallback: psutil + import psutil try: - import psutil return {c.pid for c in psutil.Process(my_pid).children()} - except Exception: - pass - - return set() + except psutil.Error: + return set() def _mcp_loop_exception_handler(loop, context): diff --git a/tools/neutts_synth.py b/tools/neutts_synth.py index ee2c84b235..7d3c21e51a 100644 --- a/tools/neutts_synth.py +++ b/tools/neutts_synth.py @@ -71,12 +71,7 @@ def main(): ref_text = ref_text_path.read_text(encoding="utf-8").strip() - # Import and run NeuTTS - try: - from neutts import NeuTTS - except ImportError: - print("Error: neutts not installed. Run: python -m pip install -U neutts[all]", file=sys.stderr) - sys.exit(1) + from neutts import NeuTTS tts = NeuTTS( backbone_repo=args.model, @@ -91,11 +86,8 @@ def main(): out_path = Path(args.out) out_path.parent.mkdir(parents=True, exist_ok=True) - try: - import soundfile as sf - sf.write(str(out_path), wav, 24000) - except ImportError: - _write_wav(str(out_path), wav, 24000) + import soundfile as sf + sf.write(str(out_path), wav, 24000) print(f"OK: {out_path}", file=sys.stderr) diff --git a/tools/process_registry.py b/tools/process_registry.py index ec510cae04..bd85b1d2cf 100644 --- a/tools/process_registry.py +++ b/tools/process_registry.py @@ -335,12 +335,11 @@ class ProcessRegistry: ) if use_pty: - # Try PTY mode for interactive CLI tools + if _IS_WINDOWS: + from winpty import PtyProcess as _PtyProcessCls + else: + from ptyprocess import PtyProcess as _PtyProcessCls try: - if _IS_WINDOWS: - from winpty import PtyProcess as _PtyProcessCls - else: - from ptyprocess import PtyProcess as _PtyProcessCls user_shell = _find_shell() pty_env = _sanitize_subprocess_env(os.environ, env_vars) pty_env["PYTHONUNBUFFERED"] = "1" @@ -371,8 +370,6 @@ class ProcessRegistry: self._write_checkpoint() return session - except ImportError: - logger.warning("ptyprocess not installed, falling back to pipe mode") except Exception as e: logger.warning("PTY spawn failed (%s), falling back to pipe mode", e) diff --git a/tools/vision_tools.py b/tools/vision_tools.py index d3019b1d0b..cf9f5633c1 100644 --- a/tools/vision_tools.py +++ b/tools/vision_tools.py @@ -318,15 +318,8 @@ def _resize_image_for_vision(image_path: Path, mime_type: Optional[str] = None, else: data_url = None # defer full encode; try Pillow resize first - # Attempt auto-resize with Pillow (soft dependency) - try: - from PIL import Image - import io as _io - except ImportError: - logger.info("Pillow not installed — cannot auto-resize oversized image") - if data_url is None: - data_url = _image_to_base64_data_url(image_path, mime_type=mime_type) - return data_url # caller will raise the size error + from PIL import Image + import io as _io logger.info("Image file is %.1f MB (estimated base64 %.1f MB, limit %.1f MB), auto-resizing...", file_size / (1024 * 1024), estimated_b64 / (1024 * 1024), diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 7798817803..d826929859 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -182,10 +182,10 @@ def _estimate_image_tokens(width: int, height: int) -> int: def _image_meta(path: Path) -> dict: + from PIL import Image + meta = {"name": path.name} try: - from PIL import Image - with Image.open(path) as img: width, height = img.size meta["width"] = int(width)