from __future__ import annotations import sys from importlib.machinery import ModuleSpec, PathFinder from importlib.machinery import all_suffixes as module_suffixes from importlib.util import spec_from_file_location from itertools import chain from pathlib import Path MAPPING: dict[str, str] = {'acp_adapter': '/usr/local/lib/hermes-agent/acp_adapter', 'agent': '/usr/local/lib/hermes-agent/agent', 'batch_runner': '/usr/local/lib/hermes-agent/batch_runner', 'cli': '/usr/local/lib/hermes-agent/cli', 'cron': '/usr/local/lib/hermes-agent/cron', 'gateway': '/usr/local/lib/hermes-agent/gateway', 'hermes_bootstrap': '/usr/local/lib/hermes-agent/hermes_bootstrap', 'hermes_cli': '/usr/local/lib/hermes-agent/hermes_cli', 'hermes_constants': '/usr/local/lib/hermes-agent/hermes_constants', 'hermes_logging': '/usr/local/lib/hermes-agent/hermes_logging', 'hermes_state': '/usr/local/lib/hermes-agent/hermes_state', 'hermes_time': '/usr/local/lib/hermes-agent/hermes_time', 'mcp_serve': '/usr/local/lib/hermes-agent/mcp_serve', 'model_tools': '/usr/local/lib/hermes-agent/model_tools', 'plugins': '/usr/local/lib/hermes-agent/plugins', 'providers': '/usr/local/lib/hermes-agent/providers', 'run_agent': '/usr/local/lib/hermes-agent/run_agent', 'tools': '/usr/local/lib/hermes-agent/tools', 'toolset_distributions': '/usr/local/lib/hermes-agent/toolset_distributions', 'toolsets': '/usr/local/lib/hermes-agent/toolsets', 'trajectory_compressor': '/usr/local/lib/hermes-agent/trajectory_compressor', 'tui_gateway': '/usr/local/lib/hermes-agent/tui_gateway', 'utils': '/usr/local/lib/hermes-agent/utils'} NAMESPACES: dict[str, list[str]] = {'plugins.image_gen': ['/usr/local/lib/hermes-agent/plugins/image_gen'], 'plugins.model-providers': ['/usr/local/lib/hermes-agent/plugins/model-providers'], 'plugins.video_gen': ['/usr/local/lib/hermes-agent/plugins/video_gen'], 'plugins.hermes-achievements': ['/usr/local/lib/hermes-agent/plugins/hermes-achievements'], 'plugins.kanban': ['/usr/local/lib/hermes-agent/plugins/kanban'], 'plugins.browser': ['/usr/local/lib/hermes-agent/plugins/browser'], 'plugins.platforms': ['/usr/local/lib/hermes-agent/plugins/platforms'], 'plugins.observability': ['/usr/local/lib/hermes-agent/plugins/observability'], 'plugins.dashboard_auth': ['/usr/local/lib/hermes-agent/plugins/dashboard_auth'], 'plugins.hermes-achievements.dashboard': ['/usr/local/lib/hermes-agent/plugins/hermes-achievements/dashboard'], 'plugins.hermes-achievements.docs': ['/usr/local/lib/hermes-agent/plugins/hermes-achievements/docs'], 'plugins.hermes-achievements.tests': ['/usr/local/lib/hermes-agent/plugins/hermes-achievements/tests'], 'plugins.hermes-achievements.dashboard.dist': ['/usr/local/lib/hermes-agent/plugins/hermes-achievements/dashboard/dist'], 'plugins.hermes-achievements.docs.assets': ['/usr/local/lib/hermes-agent/plugins/hermes-achievements/docs/assets'], 'plugins.kanban.systemd': ['/usr/local/lib/hermes-agent/plugins/kanban/systemd'], 'plugins.kanban.dashboard': ['/usr/local/lib/hermes-agent/plugins/kanban/dashboard'], 'plugins.kanban.dashboard.dist': ['/usr/local/lib/hermes-agent/plugins/kanban/dashboard/dist'], 'plugins.platforms.photon.sidecar': ['/usr/local/lib/hermes-agent/plugins/platforms/photon/sidecar'], 'gateway.assets': ['/usr/local/lib/hermes-agent/gateway/assets'], 'tools.neutts_samples': ['/usr/local/lib/hermes-agent/tools/neutts_samples']} PATH_PLACEHOLDER = '__editable__.hermes_agent-0.18.2.finder' + ".__path_hook__" class _EditableFinder: # MetaPathFinder @classmethod def find_spec(cls, fullname: str, path=None, target=None) -> ModuleSpec | None: # type: ignore # Top-level packages and modules (we know these exist in the FS) if fullname in MAPPING: pkg_path = MAPPING[fullname] return cls._find_spec(fullname, Path(pkg_path)) # Handle immediate children modules (required for namespaces to work) # To avoid problems with case sensitivity in the file system we delegate # to the importlib.machinery implementation. parent, _, child = fullname.rpartition(".") if parent and parent in MAPPING: return PathFinder.find_spec(fullname, path=[MAPPING[parent]]) # Other levels of nesting should be handled automatically by importlib # using the parent path. return None @classmethod def _find_spec(cls, fullname: str, candidate_path: Path) -> ModuleSpec | None: init = candidate_path / "__init__.py" candidates = (candidate_path.with_suffix(x) for x in module_suffixes()) for candidate in chain([init], candidates): if candidate.exists(): return spec_from_file_location(fullname, candidate) return None class _EditableNamespaceFinder: # PathEntryFinder @classmethod def _path_hook(cls, path) -> type[_EditableNamespaceFinder]: if path == PATH_PLACEHOLDER: return cls raise ImportError @classmethod def _paths(cls, fullname: str) -> list[str]: paths = NAMESPACES[fullname] if not paths and fullname in MAPPING: paths = [MAPPING[fullname]] # Always add placeholder, for 2 reasons: # 1. __path__ cannot be empty for the spec to be considered namespace. # 2. In the case of nested namespaces, we need to force # import machinery to query _EditableNamespaceFinder again. return [*paths, PATH_PLACEHOLDER] @classmethod def find_spec(cls, fullname: str, target=None) -> ModuleSpec | None: # type: ignore if fullname in NAMESPACES: spec = ModuleSpec(fullname, None, is_package=True) spec.submodule_search_locations = cls._paths(fullname) return spec return None @classmethod def find_module(cls, _fullname) -> None: return None def install(): if not any(finder == _EditableFinder for finder in sys.meta_path): sys.meta_path.append(_EditableFinder) if not NAMESPACES: return if not any(hook == _EditableNamespaceFinder._path_hook for hook in sys.path_hooks): # PathEntryFinder is needed to create NamespaceSpec without private APIS sys.path_hooks.append(_EditableNamespaceFinder._path_hook) if PATH_PLACEHOLDER not in sys.path: sys.path.append(PATH_PLACEHOLDER) # Used just to trigger the path hook