Files
hermes-agent/pm/extras.py
ethernet 7f1ddc70ce feat(python): move first-party runtime to 3.14; swap wake engine to pyopen-wakeword
requires-python >=3.14,<3.15 (was <3.14 ceiling): the old cap existed because
Rust-backed transitives lacked cp314 wheels; tflite-runtime (openwakeword's
hard linux dep) still caps at cp311, so the wake engine moves to
pyopen-wakeword 1.1.0 — py3 wheels + bundled tensorflowlite_c lib, whose
bundled melspectrogram/embedding models are byte-identical to the openWakeWord
v0.5.1 files (verified by sha256), and it loads the shipped hey_hermes.tflite.
Drops openwakeword/onnxruntime/ai-edge-litert/wake-tflite machinery entirely.
win32-arm64 gets a marker gate (no pyopen-wakeword wheel there; porcupine
covers wake). uv.lock regenerated for 3.14 (254 pkgs, all platforms).
Real-lib smoke verified: engine builds against the actual wheel, silence
scores 0, noise scores ~0.003 (threshold 0.6), scores flow 1:1 per frame.
2026-09-07 14:03:27 -04:00

209 lines
6.9 KiB
Python

"""Extras: the runtime features of the python venv.
Feature names ARE pyproject extra names. This table maps each extra to the
import that proves it (the anchor), so availability is one find_spec — no
package-manager machinery on the hot path. sync_venv([extra]) makes an extra
true; pm owns HOW (uv sync inside the venv package).
"""
from __future__ import annotations
import importlib.util
# extra name -> module that proves it is installed
ANCHORS: dict[str, str | tuple[str, ...]] = {
"anthropic": "anthropic",
"bedrock": "boto3",
"vertex": "google.auth",
"azure-identity": "azure.identity",
"exa": "exa_py",
"firecrawl": "firecrawl",
"parallel-web": "parallel",
"otlp": "opentelemetry.sdk",
"mistral": "mistralai",
"edge-tts": "edge_tts",
"tts-premium": "elevenlabs",
"voice": "faster_whisper",
"stt-whisper": "faster_whisper",
"audio-io": ("sounddevice", "numpy"),
"silk": "pilk",
"wake": "pyopen_wakeword",
"wake-openwakeword": "pyopen_wakeword",
"wake-sherpa": "sherpa_onnx",
"wake-porcupine": "pvporcupine",
"fal": "fal_client",
"honcho": "honcho",
"hindsight": "hindsight",
"supermemory": "supermemory",
"mem0": "mem0",
"messaging": "telegram",
"telegram": "telegram",
"discord": "discord",
"slack": "slack_bolt",
"matrix": ("mautrix", "asyncpg", "aiosqlite", "markdown", "aiohttp_socks"),
"dingtalk": "dingtalk_stream",
"feishu": "lark_oapi",
"wecom": "defusedxml",
"teams": "microsoft.teams.apps",
"modal": "modal",
"daytona": "daytona",
"vercel": "vercel",
"google": "googleapiclient",
"google-chat": "google.cloud.pubsub_v1",
"youtube": "youtube_transcript_api",
"acp": "acp",
"web": "fastapi",
"doc-extract": "anydoc",
"computer-use": "mcp",
"trace-upload": "huggingface_hub",
# Pillow resize recovery for vision tools (the `vision` extra is a no-op
# back-compat alias — Pillow is core — but ensure_import("vision") must
# still resolve an anchor so availability checks work).
"vision": "PIL",
}
def _anchors(extra: str) -> tuple[str, ...]:
got = ANCHORS.get(extra, extra.replace("-", "_"))
return got if isinstance(got, tuple) else (got,)
def _importable(anchor: str) -> bool:
"""An anchor already present in sys.modules counts even without a
findable spec — tests fake SDKs by inserting modules there, and the
caller's import right after this check resolves the same way."""
import sys
if anchor in sys.modules:
return True
try:
return importlib.util.find_spec(anchor) is not None
except (ImportError, ValueError):
return False
def available(extra: str) -> bool:
"""Fast, side-effect-free: are all of this extra's anchors importable?
A platform-gated extra whose gate excludes this machine reads as
unavailable — the anchors are absent by design, not by accident."""
if not extra_supported(extra):
return False
return all(_importable(a) for a in _anchors(extra))
def _platform_gates() -> dict[str, str]:
"""The [tool.hermes.extras-platforms] table from pyproject.toml:
extra -> PEP 508 marker string. Cached per process."""
global _PLATFORM_GATES
if _PLATFORM_GATES is not None:
return _PLATFORM_GATES
import tomllib
from pm.paths import repo_root
gates: dict[str, str] = {}
try:
with (repo_root() / "pyproject.toml").open("rb") as f:
data = tomllib.load(f)
table = data.get("tool", {}).get("hermes", {}).get("extras-platforms", {})
if isinstance(table, dict):
gates = {str(k): str(v) for k, v in table.items()}
except (OSError, ValueError):
pass
_PLATFORM_GATES = gates
return gates
_PLATFORM_GATES: dict[str, str] | None = None
def extra_supported(extra: str) -> bool:
"""Is this extra installable on THIS platform? True when the extra
carries no gate, or its marker matches the running platform. An
extra that IS present on this machine (anchors importable) is always
supported — an installed override beats the table (dev machines,
hand-synced venvs)."""
if all(_importable(a) for a in _anchors(extra)):
return True
marker = _platform_gates().get(extra)
if marker is None:
return True
import os
import platform
import sys
from packaging.markers import Marker
environment = {
"sys_platform": sys.platform,
"platform_system": platform.system(),
"platform_machine": platform.machine(),
"os_name": os.name,
}
try:
return bool(Marker(marker).evaluate(environment=environment))
except Exception:
# A malformed marker must never brick availability — treat as
# ungated and let the resolver be the authority.
return True
def ensure_import(extra: str) -> None:
"""Make an extra available: no-op when the anchor imports, otherwise
sync the venv with the extra enabled. Raises InstallError on failure
— including when a platform gate excludes this machine."""
if available(extra):
return
if not extra_supported(extra):
from pm.package import InstallError
marker = _platform_gates().get(extra, "")
raise InstallError(
"venv",
f"extra {extra!r} is not supported on this platform "
f"(gate: {marker!r}); the adapter degrades without it",
)
from pm.ensure import sync_venv
sync_venv([extra])
# Activation is a process-boot operation. Never mix a newly resolved
# dependency tree with libraries already imported by this process.
from hermes_cli.runtime_paths import selected_venv, site_packages
from pm.paths import repo_root, runtime_facts_path
import sys
from pathlib import Path
if runtime_facts_path().is_file():
selected = site_packages(selected_venv(repo_root())).resolve()
if selected not in {Path(entry).resolve() for entry in sys.path}:
from pm.package import InstallError
raise InstallError("venv", f"{extra} installed; restart Hermes to activate the new dependency environment")
def ensure_and_bind(extra, importer, target_globals) -> bool:
"""ensure_import + rebind module-level names after a mid-process install.
importer returns {name: value}; bound into target_globals on success."""
try:
ensure_import(extra)
except Exception as exc:
import logging
logging.getLogger(__name__).warning("extra %r unavailable: %s", extra, exc)
return False
try:
bindings = importer()
except ImportError as exc:
import logging
logging.getLogger(__name__).warning(
"import after installing %r failed: %s", extra, exc
)
return False
target_globals.update(bindings)
return True
def missing(extra: str) -> tuple[str, ...]:
return tuple(a for a in _anchors(extra) if not _importable(a))