Spaces:
Paused
Paused
| sidebar_position: 2 | |
| title: "Adding Tools" | |
| description: "How to add a new tool to Hermes Agent — schemas, handlers, registration, and toolsets" | |
| # Adding Tools | |
| Before writing a tool, ask yourself: **should this be a [skill](creating-skills.md) instead?** | |
| Make it a **Skill** when the capability can be expressed as instructions + shell commands + existing tools (arXiv search, git workflows, Docker management, PDF processing). | |
| Make it a **Tool** when it requires end-to-end integration with API keys, custom processing logic, binary data handling, or streaming (browser automation, TTS, vision analysis). | |
| ## Overview | |
| Adding a tool touches **3 files**: | |
| 1. **`tools/your_tool.py`** — handler, schema, check function, `registry.register()` call | |
| 2. **`toolsets.py`** — add tool name to `_HERMES_CORE_TOOLS` (or a specific toolset) | |
| 3. **`model_tools.py`** — add `"tools.your_tool"` to the `_discover_tools()` list | |
| ## Step 1: Create the Tool File | |
| Every tool file follows the same structure: | |
| ```python | |
| # tools/weather_tool.py | |
| """Weather Tool -- look up current weather for a location.""" | |
| import json | |
| import os | |
| import logging | |
| logger = logging.getLogger(__name__) | |
| # --- Availability check --- | |
| def check_weather_requirements() -> bool: | |
| """Return True if the tool's dependencies are available.""" | |
| return bool(os.getenv("WEATHER_API_KEY")) | |
| # --- Handler --- | |
| def weather_tool(location: str, units: str = "metric") -> str: | |
| """Fetch weather for a location. Returns JSON string.""" | |
| api_key = os.getenv("WEATHER_API_KEY") | |
| if not api_key: | |
| return json.dumps({"error": "WEATHER_API_KEY not configured"}) | |
| try: | |
| # ... call weather API ... | |
| return json.dumps({"location": location, "temp": 22, "units": units}) | |
| except Exception as e: | |
| return json.dumps({"error": str(e)}) | |
| # --- Schema --- | |
| WEATHER_SCHEMA = { | |
| "name": "weather", | |
| "description": "Get current weather for a location.", | |
| "parameters": { | |
| "type": "object", | |
| "properties": { | |
| "location": { | |
| "type": "string", | |
| "description": "City name or coordinates (e.g. 'London' or '51.5,-0.1')" | |
| }, | |
| "units": { | |
| "type": "string", | |
| "enum": ["metric", "imperial"], | |
| "description": "Temperature units (default: metric)", | |
| "default": "metric" | |
| } | |
| }, | |
| "required": ["location"] | |
| } | |
| } | |
| # --- Registration --- | |
| from tools.registry import registry | |
| registry.register( | |
| name="weather", | |
| toolset="weather", | |
| schema=WEATHER_SCHEMA, | |
| handler=lambda args, **kw: weather_tool( | |
| location=args.get("location", ""), | |
| units=args.get("units", "metric")), | |
| check_fn=check_weather_requirements, | |
| requires_env=["WEATHER_API_KEY"], | |
| ) | |
| ``` | |
| ### Key Rules | |
| :::danger Important | |
| - Handlers **MUST** return a JSON string (via `json.dumps()`), never raw dicts | |
| - Errors **MUST** be returned as `{"error": "message"}`, never raised as exceptions | |
| - The `check_fn` is called when building tool definitions — if it returns `False`, the tool is silently excluded | |
| - The `handler` receives `(args: dict, **kwargs)` where `args` is the LLM's tool call arguments | |
| ::: | |
| ## Step 2: Add to a Toolset | |
| In `toolsets.py`, add the tool name: | |
| ```python | |
| # If it should be available on all platforms (CLI + messaging): | |
| _HERMES_CORE_TOOLS = [ | |
| ... | |
| "weather", # <-- add here | |
| ] | |
| # Or create a new standalone toolset: | |
| "weather": { | |
| "description": "Weather lookup tools", | |
| "tools": ["weather"], | |
| "includes": [] | |
| }, | |
| ``` | |
| ## Step 3: Add Discovery Import | |
| In `model_tools.py`, add the module to the `_discover_tools()` list: | |
| ```python | |
| def _discover_tools(): | |
| _modules = [ | |
| ... | |
| "tools.weather_tool", # <-- add here | |
| ] | |
| ``` | |
| This import triggers the `registry.register()` call at the bottom of your tool file. | |
| ## Async Handlers | |
| If your handler needs async code, mark it with `is_async=True`: | |
| ```python | |
| async def weather_tool_async(location: str) -> str: | |
| async with aiohttp.ClientSession() as session: | |
| ... | |
| return json.dumps(result) | |
| registry.register( | |
| name="weather", | |
| toolset="weather", | |
| schema=WEATHER_SCHEMA, | |
| handler=lambda args, **kw: weather_tool_async(args.get("location", "")), | |
| check_fn=check_weather_requirements, | |
| is_async=True, # registry calls _run_async() automatically | |
| ) | |
| ``` | |
| The registry handles async bridging transparently — you never call `asyncio.run()` yourself. | |
| ## Handlers That Need task_id | |
| Tools that manage per-session state receive `task_id` via `**kwargs`: | |
| ```python | |
| def _handle_weather(args, **kw): | |
| task_id = kw.get("task_id") | |
| return weather_tool(args.get("location", ""), task_id=task_id) | |
| registry.register( | |
| name="weather", | |
| ... | |
| handler=_handle_weather, | |
| ) | |
| ``` | |
| ## Agent-Loop Intercepted Tools | |
| Some tools (`todo`, `memory`, `session_search`, `delegate_task`) need access to per-session agent state. These are intercepted by `run_agent.py` before reaching the registry. The registry still holds their schemas, but `dispatch()` returns a fallback error if the intercept is bypassed. | |
| ## Optional: Setup Wizard Integration | |
| If your tool requires an API key, add it to `hermes_cli/config.py`: | |
| ```python | |
| OPTIONAL_ENV_VARS = { | |
| ... | |
| "WEATHER_API_KEY": { | |
| "description": "Weather API key for weather lookup", | |
| "prompt": "Weather API key", | |
| "url": "https://weatherapi.com/", | |
| "tools": ["weather"], | |
| "password": True, | |
| }, | |
| } | |
| ``` | |
| ## Checklist | |
| - [ ] Tool file created with handler, schema, check function, and registration | |
| - [ ] Added to appropriate toolset in `toolsets.py` | |
| - [ ] Discovery import added to `model_tools.py` | |
| - [ ] Handler returns JSON strings, errors returned as `{"error": "..."}` | |
| - [ ] Optional: API key added to `OPTIONAL_ENV_VARS` in `hermes_cli/config.py` | |
| - [ ] Optional: Added to `toolset_distributions.py` for batch processing | |
| - [ ] Tested with `hermes chat -q "Use the weather tool for London"` | |