mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
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:
parent
f8d2365795
commit
72e7c0ce34
10 changed files with 31 additions and 72 deletions
|
|
@ -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}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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)
|
from PIL import Image
|
||||||
try:
|
try:
|
||||||
from PIL import Image
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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]",
|
||||||
|
|
|
||||||
|
|
@ -1453,13 +1453,11 @@ def _snapshot_child_pids() -> set:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Fallback: psutil
|
# Fallback: psutil
|
||||||
|
import psutil
|
||||||
try:
|
try:
|
||||||
import psutil
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def _mcp_loop_exception_handler(loop, context):
|
def _mcp_loop_exception_handler(loop, context):
|
||||||
|
|
|
||||||
|
|
@ -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
|
from neutts import 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)
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -335,12 +335,11 @@ class ProcessRegistry:
|
||||||
)
|
)
|
||||||
|
|
||||||
if use_pty:
|
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:
|
try:
|
||||||
if _IS_WINDOWS:
|
|
||||||
from winpty import PtyProcess as _PtyProcessCls
|
|
||||||
else:
|
|
||||||
from ptyprocess import PtyProcess as _PtyProcessCls
|
|
||||||
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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
from PIL import Image
|
||||||
try:
|
import io as _io
|
||||||
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
|
|
||||||
|
|
||||||
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),
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
meta = {"name": path.name}
|
meta = {"name": path.name}
|
||||||
try:
|
try:
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue