mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 16:31:56 +08:00
91 lines
3.3 KiB
Python
91 lines
3.3 KiB
Python
"""
|
|
Hermes external tool adapter for Atropos ToolServer.
|
|
|
|
These tools reuse Hermes-Agent's existing tool runner (`model_tools.handle_function_call`)
|
|
so we don't duplicate external tool implementations.
|
|
|
|
Important:
|
|
- These are marked `external=True` and should be executed ONLY by ToolServer.
|
|
- We run `handle_function_call` in a worker thread because the Hermes implementation
|
|
uses `asyncio.run()` internally for some async tools (web_extract, vision, MoA, etc).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
import model_tools
|
|
|
|
from .base import Tool, ToolResult, ToolSchema
|
|
|
|
|
|
def _schema_from_openai_tool_dict(tool: Dict[str, Any], *, external: bool) -> ToolSchema:
|
|
fn = tool.get("function") or {}
|
|
name = str(fn.get("name") or "")
|
|
description = str(fn.get("description") or "")
|
|
params = fn.get("parameters") or {}
|
|
properties = params.get("properties") or {}
|
|
required = params.get("required") or []
|
|
if not isinstance(required, list):
|
|
required = []
|
|
return ToolSchema(
|
|
name=name,
|
|
description=description,
|
|
parameters=dict(properties),
|
|
required=[str(x) for x in required if isinstance(x, (str, int))],
|
|
external=external,
|
|
)
|
|
|
|
|
|
class HermesExternalTool(Tool):
|
|
def __init__(self, schema: ToolSchema):
|
|
self._schema = schema
|
|
|
|
@property
|
|
def schema(self) -> ToolSchema:
|
|
return self._schema
|
|
|
|
async def execute(self, task_id: Optional[str] = None, **kwargs: Any) -> ToolResult:
|
|
# `model_tools.handle_function_call` returns a JSON string (success or error).
|
|
# Run in a thread because some Hermes tool handlers call `asyncio.run()`.
|
|
raw = await asyncio.to_thread(model_tools.handle_function_call, self.name, kwargs, task_id)
|
|
|
|
try:
|
|
parsed = json.loads(raw)
|
|
except Exception:
|
|
# Keep as plain string.
|
|
return ToolResult(success=True, output=str(raw))
|
|
|
|
if isinstance(parsed, dict) and parsed.get("error"):
|
|
return ToolResult(success=False, error=str(parsed.get("error")), output="")
|
|
|
|
return ToolResult(success=True, output=json.dumps(parsed, ensure_ascii=False))
|
|
|
|
|
|
def build_external_tools(
|
|
*,
|
|
selected_tool_names: Optional[set[str]] = None,
|
|
) -> List[HermesExternalTool]:
|
|
"""
|
|
Build external tool wrappers from Hermes tool declarations.
|
|
|
|
Filters out sandbox-oriented tools (e.g. `terminal`) since those should run
|
|
inside the sandbox via ToolExecutor.
|
|
"""
|
|
# IMPORTANT: Hermes' `model_tools.get_tool_definitions()` only understands Hermes toolsets.
|
|
# Atropos envs add extra toolsets (filesystem/sandbox/stateful). To avoid noisy "Unknown toolset"
|
|
# prints and accidental filtering, we fetch ALL Hermes tool definitions here and filter by name.
|
|
tools = model_tools.get_tool_definitions(enabled_toolsets=None, disabled_toolsets=None, quiet_mode=True)
|
|
|
|
wrappers: List[HermesExternalTool] = []
|
|
for t in tools:
|
|
schema = _schema_from_openai_tool_dict(t, external=True)
|
|
if schema.name in {"terminal"}:
|
|
continue
|
|
if selected_tool_names is not None and schema.name not in selected_tool_names:
|
|
continue
|
|
wrappers.append(HermesExternalTool(schema))
|
|
return wrappers
|