Merge branch 'main' of github.com:NousResearch/hermes-agent into feat/ink-refactor
This commit is contained in:
@@ -99,23 +99,41 @@ def _load_hermes_env_vars() -> dict[str, str]:
|
||||
|
||||
|
||||
def find_docker() -> Optional[str]:
|
||||
"""Locate the docker CLI binary.
|
||||
"""Locate the docker (or podman) CLI binary.
|
||||
|
||||
Checks ``shutil.which`` first (respects PATH), then probes well-known
|
||||
install locations on macOS where Docker Desktop may not be in PATH
|
||||
(e.g. when running as a gateway service via launchd).
|
||||
Resolution order:
|
||||
1. ``HERMES_DOCKER_BINARY`` env var — explicit override (e.g. ``/usr/bin/podman``)
|
||||
2. ``docker`` on PATH via ``shutil.which``
|
||||
3. ``podman`` on PATH via ``shutil.which``
|
||||
4. Well-known macOS Docker Desktop install locations
|
||||
|
||||
Returns the absolute path, or ``None`` if docker cannot be found.
|
||||
Returns the absolute path, or ``None`` if neither runtime can be found.
|
||||
"""
|
||||
global _docker_executable
|
||||
if _docker_executable is not None:
|
||||
return _docker_executable
|
||||
|
||||
# 1. Explicit override via env var (e.g. for Podman on immutable distros)
|
||||
override = os.getenv("HERMES_DOCKER_BINARY")
|
||||
if override and os.path.isfile(override) and os.access(override, os.X_OK):
|
||||
_docker_executable = override
|
||||
logger.info("Using HERMES_DOCKER_BINARY override: %s", override)
|
||||
return override
|
||||
|
||||
# 2. docker on PATH
|
||||
found = shutil.which("docker")
|
||||
if found:
|
||||
_docker_executable = found
|
||||
return found
|
||||
|
||||
# 3. podman on PATH (drop-in compatible for our use case)
|
||||
found = shutil.which("podman")
|
||||
if found:
|
||||
_docker_executable = found
|
||||
logger.info("Using podman as container runtime: %s", found)
|
||||
return found
|
||||
|
||||
# 4. Well-known macOS Docker Desktop locations
|
||||
for path in _DOCKER_SEARCH_PATHS:
|
||||
if os.path.isfile(path) and os.access(path, os.X_OK):
|
||||
_docker_executable = path
|
||||
|
||||
@@ -2036,7 +2036,7 @@ def register_mcp_servers(servers: Dict[str, dict]) -> List[str]:
|
||||
def discover_mcp_tools() -> List[str]:
|
||||
"""Entry point: load config, connect to MCP servers, register tools.
|
||||
|
||||
Called from ``model_tools._discover_tools()``. Safe to call even when
|
||||
Called from ``model_tools`` after ``discover_builtin_tools()``. Safe to call even when
|
||||
the ``mcp`` package is not installed (returns empty list).
|
||||
|
||||
Idempotent for already-connected servers. If some servers failed on a
|
||||
|
||||
@@ -14,14 +14,65 @@ Import chain (circular-import safe):
|
||||
run_agent.py, cli.py, batch_runner.py, etc.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import importlib
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Callable, Dict, List, Optional, Set
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_registry_register_call(node: ast.AST) -> bool:
|
||||
"""Return True when *node* is a ``registry.register(...)`` call expression."""
|
||||
if not isinstance(node, ast.Expr) or not isinstance(node.value, ast.Call):
|
||||
return False
|
||||
func = node.value.func
|
||||
return (
|
||||
isinstance(func, ast.Attribute)
|
||||
and func.attr == "register"
|
||||
and isinstance(func.value, ast.Name)
|
||||
and func.value.id == "registry"
|
||||
)
|
||||
|
||||
|
||||
def _module_registers_tools(module_path: Path) -> bool:
|
||||
"""Return True when the module contains a top-level ``registry.register(...)`` call.
|
||||
|
||||
Only inspects module-body statements so that helper modules which happen
|
||||
to call ``registry.register()`` inside a function are not picked up.
|
||||
"""
|
||||
try:
|
||||
source = module_path.read_text(encoding="utf-8")
|
||||
tree = ast.parse(source, filename=str(module_path))
|
||||
except (OSError, SyntaxError):
|
||||
return False
|
||||
|
||||
return any(_is_registry_register_call(stmt) for stmt in tree.body)
|
||||
|
||||
|
||||
def discover_builtin_tools(tools_dir: Optional[Path] = None) -> List[str]:
|
||||
"""Import built-in self-registering tool modules and return their module names."""
|
||||
tools_path = Path(tools_dir) if tools_dir is not None else Path(__file__).resolve().parent
|
||||
module_names = [
|
||||
f"tools.{path.stem}"
|
||||
for path in sorted(tools_path.glob("*.py"))
|
||||
if path.name not in {"__init__.py", "registry.py", "mcp_tool.py"}
|
||||
and _module_registers_tools(path)
|
||||
]
|
||||
|
||||
imported: List[str] = []
|
||||
for mod_name in module_names:
|
||||
try:
|
||||
importlib.import_module(mod_name)
|
||||
imported.append(mod_name)
|
||||
except Exception as e:
|
||||
logger.warning("Could not import tool module %s: %s", mod_name, e)
|
||||
return imported
|
||||
|
||||
|
||||
class ToolEntry:
|
||||
"""Metadata for a single registered tool."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user