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.
This commit is contained in:
alt-glitch 2026-04-21 16:20:45 +05:30
parent f8d2365795
commit 72e7c0ce34
10 changed files with 31 additions and 72 deletions

View file

@ -183,18 +183,10 @@ def proxy_kwargs_for_bot(proxy_url: str | None) -> dict:
if not proxy_url: if not proxy_url:
return {} return {}
if proxy_url.lower().startswith("socks"): 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) connector = ProxyConnector.from_url(proxy_url, rdns=True)
return {"connector": connector} return {"connector": connector}
except ImportError:
logger.warning(
"aiohttp_socks not installed — SOCKS proxy %s ignored. "
"Run: pip install aiohttp-socks",
proxy_url,
)
return {}
return {"proxy": proxy_url} return {"proxy": proxy_url}
@ -216,18 +208,10 @@ def proxy_kwargs_for_aiohttp(proxy_url: str | None) -> tuple[dict, dict]:
if not proxy_url: if not proxy_url:
return {}, {} return {}, {}
if proxy_url.lower().startswith("socks"): 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) connector = ProxyConnector.from_url(proxy_url, rdns=True)
return {"connector": connector}, {} return {"connector": connector}, {}
except ImportError:
logger.warning(
"aiohttp_socks not installed — SOCKS proxy %s ignored. "
"Run: pip install aiohttp-socks",
proxy_url,
)
return {}, {}
return {}, {"proxy": proxy_url} return {}, {"proxy": proxy_url}

View file

@ -1194,9 +1194,10 @@ class DiscordAdapter(BasePlatformAdapter):
try: try:
import base64 import base64
from mutagen.oggopus import OggOpus
duration_secs = 5.0 duration_secs = 5.0
try: try:
from mutagen.oggopus import OggOpus
info = OggOpus(audio_path) info = OggOpus(audio_path)
duration_secs = info.info.length duration_secs = info.info.length
except Exception: except Exception:

View file

@ -395,14 +395,11 @@ def _wayland_save(dest: Path) -> bool:
def _convert_to_png(path: Path) -> bool: def _convert_to_png(path: Path) -> bool:
"""Convert an image file to PNG in-place (requires Pillow or ImageMagick).""" """Convert an image file to PNG in-place (requires Pillow or ImageMagick)."""
# Try Pillow first (likely installed in the venv)
try:
from PIL import Image from PIL import Image
try:
img = Image.open(path) img = Image.open(path)
img.save(path, "PNG") img.save(path, "PNG")
return True return True
except ImportError:
pass
except Exception as e: except Exception as e:
logger.debug("Pillow BMP→PNG conversion failed: %s", e) logger.debug("Pillow BMP→PNG conversion failed: %s", e)

View file

@ -710,19 +710,14 @@ def _estimate_tool_tokens() -> Dict[str, int]:
OpenAI-format tool schema. Triggers tool discovery on first call, OpenAI-format tool schema. Triggers tool discovery on first call,
then caches the result for the rest of the process. 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 global _tool_token_cache
if _tool_token_cache is not None: if _tool_token_cache is not None:
return _tool_token_cache return _tool_token_cache
try:
import tiktoken import tiktoken
enc = tiktoken.get_encoding("cl100k_base") enc = tiktoken.get_encoding("cl100k_base")
except Exception:
logger.debug("tiktoken unavailable; skipping tool token estimation")
_tool_token_cache = {}
return _tool_token_cache
try: try:
# Trigger full tool discovery (imports all tool modules). # Trigger full tool discovery (imports all tool modules).

View file

@ -40,11 +40,11 @@ dependencies = [
modal = ["modal>=1.0.0,<2"] modal = ["modal>=1.0.0,<2"]
daytona = ["daytona>=0.148.0,<1"] 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"] 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"] cron = ["croniter>=6.0.0,<7"]
slack = ["slack-bolt>=1.18.0,<2", "slack-sdk>=3.27.0,<4"] 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"] 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"] tts-premium = ["elevenlabs>=1.0,<2"]
voice = [ voice = [
# Local STT pulls in wheel-only transitive deps (ctranslate2, onnxruntime), # Local STT pulls in wheel-only transitive deps (ctranslate2, onnxruntime),
@ -58,7 +58,7 @@ pty = [
"pywinpty>=2.0.0,<3; sys_platform == 'win32'", "pywinpty>=2.0.0,<3; sys_platform == 'win32'",
] ]
honcho = ["honcho-ai>=2.0.1,<3"] 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"] homeassistant = ["aiohttp>=3.9.0,<4"]
sms = ["aiohttp>=3.9.0,<4"] sms = ["aiohttp>=3.9.0,<4"]
acp = ["agent-client-protocol>=0.9.0,<1.0"] acp = ["agent-client-protocol>=0.9.0,<1.0"]
@ -85,7 +85,9 @@ rl = [
"fastapi>=0.104.0,<1", "fastapi>=0.104.0,<1",
"uvicorn[standard]>=0.24.0,<1", "uvicorn[standard]>=0.24.0,<1",
"wandb>=0.15.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'"] yc-bench = ["yc-bench @ git+https://github.com/collinear-ai/yc-bench.git@bfb0c88062450f46341bd9a5298903fc2e952a5c ; python_version >= '3.12'"]
all = [ all = [
"hermes-agent[modal]", "hermes-agent[modal]",

View file

@ -1453,12 +1453,10 @@ def _snapshot_child_pids() -> set:
pass pass
# Fallback: psutil # Fallback: psutil
try:
import psutil import psutil
try:
return {c.pid for c in psutil.Process(my_pid).children()} return {c.pid for c in psutil.Process(my_pid).children()}
except Exception: except psutil.Error:
pass
return set() return set()

View file

@ -71,12 +71,7 @@ def main():
ref_text = ref_text_path.read_text(encoding="utf-8").strip() ref_text = ref_text_path.read_text(encoding="utf-8").strip()
# Import and run NeuTTS
try:
from neutts import NeuTTS 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)
tts = NeuTTS( tts = NeuTTS(
backbone_repo=args.model, backbone_repo=args.model,
@ -91,11 +86,8 @@ def main():
out_path = Path(args.out) out_path = Path(args.out)
out_path.parent.mkdir(parents=True, exist_ok=True) out_path.parent.mkdir(parents=True, exist_ok=True)
try:
import soundfile as sf import soundfile as sf
sf.write(str(out_path), wav, 24000) sf.write(str(out_path), wav, 24000)
except ImportError:
_write_wav(str(out_path), wav, 24000)
print(f"OK: {out_path}", file=sys.stderr) print(f"OK: {out_path}", file=sys.stderr)

View file

@ -335,12 +335,11 @@ class ProcessRegistry:
) )
if use_pty: if use_pty:
# Try PTY mode for interactive CLI tools
try:
if _IS_WINDOWS: if _IS_WINDOWS:
from winpty import PtyProcess as _PtyProcessCls from winpty import PtyProcess as _PtyProcessCls
else: else:
from ptyprocess import PtyProcess as _PtyProcessCls from ptyprocess import PtyProcess as _PtyProcessCls
try:
user_shell = _find_shell() user_shell = _find_shell()
pty_env = _sanitize_subprocess_env(os.environ, env_vars) pty_env = _sanitize_subprocess_env(os.environ, env_vars)
pty_env["PYTHONUNBUFFERED"] = "1" pty_env["PYTHONUNBUFFERED"] = "1"
@ -371,8 +370,6 @@ class ProcessRegistry:
self._write_checkpoint() self._write_checkpoint()
return session return session
except ImportError:
logger.warning("ptyprocess not installed, falling back to pipe mode")
except Exception as e: except Exception as e:
logger.warning("PTY spawn failed (%s), falling back to pipe mode", e) logger.warning("PTY spawn failed (%s), falling back to pipe mode", e)

View file

@ -318,15 +318,8 @@ def _resize_image_for_vision(image_path: Path, mime_type: Optional[str] = None,
else: else:
data_url = None # defer full encode; try Pillow resize first data_url = None # defer full encode; try Pillow resize first
# Attempt auto-resize with Pillow (soft dependency)
try:
from PIL import Image from PIL import Image
import io as _io 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
logger.info("Image file is %.1f MB (estimated base64 %.1f MB, limit %.1f MB), auto-resizing...", logger.info("Image file is %.1f MB (estimated base64 %.1f MB, limit %.1f MB), auto-resizing...",
file_size / (1024 * 1024), estimated_b64 / (1024 * 1024), file_size / (1024 * 1024), estimated_b64 / (1024 * 1024),

View file

@ -182,10 +182,10 @@ def _estimate_image_tokens(width: int, height: int) -> int:
def _image_meta(path: Path) -> dict: def _image_meta(path: Path) -> dict:
meta = {"name": path.name}
try:
from PIL import Image from PIL import Image
meta = {"name": path.name}
try:
with Image.open(path) as img: with Image.open(path) as img:
width, height = img.size width, height = img.size
meta["width"] = int(width) meta["width"] = int(width)