Something went wrong. Try again.
The agent that grows with you
Something went wrong. Try again.
hermes-agent toolsets.py
23 kB · 720 lines
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721#!/usr/bin/env python3"""Toolsets Module
This module provides a flexible system for defining and managing tool aliases/toolsets.Toolsets allow you to group tools together for specific scenarios and can be composedfrom individual tools or other toolsets.
Features:- Define custom toolsets with specific tools- Compose toolsets from other toolsets- Built-in common toolsets for typical use cases- Easy extension for new toolsets- Support for dynamic toolset resolution
Usage: from toolsets import get_toolset, resolve_toolset, get_all_toolsets # Get tools for a specific toolset tools = get_toolset("research") # Resolve a toolset to get all tool names (including from composed toolsets) all_tools = resolve_toolset("full_stack")"""
from typing import List, Dict, Any, Set, Optional
# Shared tool list for CLI and all messaging platform toolsets.# Edit this once to update all platforms simultaneously._HERMES_CORE_TOOLS = [ # Web "web_search", "web_extract", # Terminal + process management "terminal", "process", # File manipulation "read_file", "write_file", "patch", "search_files", # Vision + image generation "vision_analyze", "image_generate", # Skills "skills_list", "skill_view", "skill_manage", # Browser automation "browser_navigate", "browser_snapshot", "browser_click", "browser_type", "browser_scroll", "browser_back", "browser_press", "browser_get_images", "browser_vision", "browser_console", "browser_cdp", # Text-to-speech "text_to_speech", # Planning & memory "todo", "memory", # Session history search "session_search", # Clarifying questions "clarify", # Code execution + delegation "execute_code", "delegate_task", # Cronjob management "cronjob", # Cross-platform messaging (gated on gateway running via check_fn) "send_message", # Home Assistant smart home control (gated on HASS_TOKEN via check_fn) "ha_list_entities", "ha_get_state", "ha_list_services", "ha_call_service",]
# Core toolset definitions# These can include individual tools or reference other toolsetsTOOLSETS = { # Basic toolsets - individual tool categories "web": { "description": "Web research and content extraction tools", "tools": ["web_search", "web_extract"], "includes": [] # No other toolsets included }, "search": { "description": "Web search only (no content extraction/scraping)", "tools": ["web_search"], "includes": [] }, "vision": { "description": "Image analysis and vision tools", "tools": ["vision_analyze"], "includes": [] }, "image_gen": { "description": "Creative generation tools (images)", "tools": ["image_generate"], "includes": [] }, "terminal": { "description": "Terminal/command execution and process management tools", "tools": ["terminal", "process"], "includes": [] }, "moa": { "description": "Advanced reasoning and problem-solving tools", "tools": ["mixture_of_agents"], "includes": [] }, "skills": { "description": "Access, create, edit, and manage skill documents with specialized instructions and knowledge", "tools": ["skills_list", "skill_view", "skill_manage"], "includes": [] }, "browser": { "description": "Browser automation for web interaction (navigate, click, type, scroll, iframes, hold-click) with web search for finding URLs", "tools": [ "browser_navigate", "browser_snapshot", "browser_click", "browser_type", "browser_scroll", "browser_back", "browser_press", "browser_get_images", "browser_vision", "browser_console", "browser_cdp", "web_search" ], "includes": [] }, "cronjob": { "description": "Cronjob management tool - create, list, update, pause, resume, remove, and trigger scheduled tasks", "tools": ["cronjob"], "includes": [] }, "messaging": { "description": "Cross-platform messaging: send messages to Telegram, Discord, Slack, SMS, etc.", "tools": ["send_message"], "includes": [] }, "rl": { "description": "RL training tools for running reinforcement learning on Tinker-Atropos", "tools": [ "rl_list_environments", "rl_select_environment", "rl_get_current_config", "rl_edit_config", "rl_start_training", "rl_check_status", "rl_stop_training", "rl_get_results", "rl_list_runs", "rl_test_inference" ], "includes": [] }, "file": { "description": "File manipulation tools: read, write, patch (with fuzzy matching), and search (content + files)", "tools": ["read_file", "write_file", "patch", "search_files"], "includes": [] }, "tts": { "description": "Text-to-speech: convert text to audio with Edge TTS (free), ElevenLabs, OpenAI, or xAI", "tools": ["text_to_speech"], "includes": [] }, "todo": { "description": "Task planning and tracking for multi-step work", "tools": ["todo"], "includes": [] }, "memory": { "description": "Persistent memory across sessions (personal notes + user profile)", "tools": ["memory"], "includes": [] }, "session_search": { "description": "Search and recall past conversations with summarization", "tools": ["session_search"], "includes": [] }, "clarify": { "description": "Ask the user clarifying questions (multiple-choice or open-ended)", "tools": ["clarify"], "includes": [] }, "code_execution": { "description": "Run Python scripts that call tools programmatically (reduces LLM round trips)", "tools": ["execute_code"], "includes": [] }, "delegation": { "description": "Spawn subagents with isolated context for complex subtasks", "tools": ["delegate_task"], "includes": [] },
# "honcho" toolset removed — Honcho is now a memory provider plugin. # Tools are injected via MemoryManager, not the toolset system.
"homeassistant": { "description": "Home Assistant smart home control and monitoring", "tools": ["ha_list_entities", "ha_get_state", "ha_list_services", "ha_call_service"], "includes": [] },
"feishu_doc": { "description": "Read Feishu/Lark document content", "tools": ["feishu_doc_read"], "includes": [] },
"feishu_drive": { "description": "Feishu/Lark document comment operations (list, reply, add)", "tools": [ "feishu_drive_list_comments", "feishu_drive_list_comment_replies", "feishu_drive_reply_comment", "feishu_drive_add_comment", ], "includes": [] },
# Scenario-specific toolsets "debugging": { "description": "Debugging and troubleshooting toolkit", "tools": ["terminal", "process"], "includes": ["web", "file"] # For searching error messages and solutions, and file operations }, "safe": { "description": "Safe toolkit without terminal access", "tools": [], "includes": ["web", "vision", "image_gen"] }, # ========================================================================== # Full Hermes toolsets (CLI + messaging platforms) # # All platforms share the same core tools (including send_message, # which is gated on gateway running via its check_fn). # ==========================================================================
"hermes-acp": { "description": "Editor integration (VS Code, Zed, JetBrains) — coding-focused tools without messaging, audio, or clarify UI", "tools": [ "web_search", "web_extract", "terminal", "process", "read_file", "write_file", "patch", "search_files", "vision_analyze", "skills_list", "skill_view", "skill_manage", "browser_navigate", "browser_snapshot", "browser_click", "browser_type", "browser_scroll", "browser_back", "browser_press", "browser_get_images", "browser_vision", "browser_console", "browser_cdp", "todo", "memory", "session_search", "execute_code", "delegate_task", ], "includes": [] },
"hermes-api-server": { "description": "OpenAI-compatible API server — full agent tools accessible via HTTP (no interactive UI tools like clarify or send_message)", "tools": [ # Web "web_search", "web_extract", # Terminal + process management "terminal", "process", # File manipulation "read_file", "write_file", "patch", "search_files", # Vision + image generation "vision_analyze", "image_generate", # Skills "skills_list", "skill_view", "skill_manage", # Browser automation "browser_navigate", "browser_snapshot", "browser_click", "browser_type", "browser_scroll", "browser_back", "browser_press", "browser_get_images", "browser_vision", "browser_console", "browser_cdp", # Planning & memory "todo", "memory", # Session history search "session_search", # Code execution + delegation "execute_code", "delegate_task", # Cronjob management "cronjob", # Home Assistant smart home control (gated on HASS_TOKEN via check_fn) "ha_list_entities", "ha_get_state", "ha_list_services", "ha_call_service",
], "includes": [] }, "hermes-cli": { "description": "Full interactive CLI toolset - all default tools plus cronjob management", "tools": _HERMES_CORE_TOOLS, "includes": [] }, "hermes-telegram": { "description": "Telegram bot toolset - full access for personal use (terminal has safety checks)", "tools": _HERMES_CORE_TOOLS, "includes": [] }, "hermes-discord": { "description": "Discord bot toolset - full access (terminal has safety checks via dangerous command approval)", "tools": _HERMES_CORE_TOOLS + [ # Discord server introspection & management (gated on DISCORD_BOT_TOKEN via check_fn) "discord_server", ], "includes": [] }, "hermes-whatsapp": { "description": "WhatsApp bot toolset - similar to Telegram (personal messaging, more trusted)", "tools": _HERMES_CORE_TOOLS, "includes": [] }, "hermes-slack": { "description": "Slack bot toolset - full access for workspace use (terminal has safety checks)", "tools": _HERMES_CORE_TOOLS, "includes": [] }, "hermes-signal": { "description": "Signal bot toolset - encrypted messaging platform (full access)", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-bluebubbles": { "description": "BlueBubbles iMessage bot toolset - Apple iMessage via local BlueBubbles server", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-homeassistant": { "description": "Home Assistant bot toolset - smart home event monitoring and control", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-email": { "description": "Email bot toolset - interact with Hermes via email (IMAP/SMTP)", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-mattermost": { "description": "Mattermost bot toolset - self-hosted team messaging (full access)", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-matrix": { "description": "Matrix bot toolset - decentralized encrypted messaging (full access)", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-dingtalk": { "description": "DingTalk bot toolset - enterprise messaging platform (full access)", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-feishu": { "description": "Feishu/Lark bot toolset - enterprise messaging via Feishu/Lark (full access)", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-weixin": { "description": "Weixin bot toolset - personal WeChat messaging via iLink (full access)", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-qqbot": { "description": "QQBot toolset - QQ messaging via Official Bot API v2 (full access)", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-wecom": { "description": "WeCom bot toolset - enterprise WeChat messaging (full access)", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-wecom-callback": { "description": "WeCom callback toolset - enterprise self-built app messaging (full access)", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-sms": { "description": "SMS bot toolset - interact with Hermes via SMS (Twilio)", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-webhook": { "description": "Webhook toolset - receive and process external webhook events", "tools": _HERMES_CORE_TOOLS, "includes": [] },
"hermes-gateway": { "description": "Gateway toolset - union of all messaging platform tools", "tools": [], "includes": ["hermes-telegram", "hermes-discord", "hermes-whatsapp", "hermes-slack", "hermes-signal", "hermes-bluebubbles", "hermes-homeassistant", "hermes-email", "hermes-sms", "hermes-mattermost", "hermes-matrix", "hermes-dingtalk", "hermes-feishu", "hermes-wecom", "hermes-wecom-callback", "hermes-weixin", "hermes-qqbot", "hermes-webhook"] }}
def get_toolset(name: str) -> Optional[Dict[str, Any]]: """ Get a toolset definition by name. Args: name (str): Name of the toolset Returns: Dict: Toolset definition with description, tools, and includes None: If toolset not found """ toolset = TOOLSETS.get(name) if toolset: return toolset
try: from tools.registry import registry except Exception: return None
registry_toolset = name description = f"Plugin toolset: {name}" alias_target = registry.get_toolset_alias_target(name)
if name not in _get_plugin_toolset_names(): registry_toolset = alias_target if not registry_toolset: return None description = f"MCP server '{name}' tools" else: reverse_aliases = { canonical: alias for alias, canonical in _get_registry_toolset_aliases().items() if alias not in TOOLSETS } alias = reverse_aliases.get(name) if alias: description = f"MCP server '{alias}' tools"
return { "description": description, "tools": registry.get_tool_names_for_toolset(registry_toolset), "includes": [], }
def resolve_toolset(name: str, visited: Set[str] = None) -> List[str]: """ Recursively resolve a toolset to get all tool names. This function handles toolset composition by recursively resolving included toolsets and combining all tools. Args: name (str): Name of the toolset to resolve visited (Set[str]): Set of already visited toolsets (for cycle detection) Returns: List[str]: List of all tool names in the toolset """ if visited is None: visited = set() # Special aliases that represent all tools across every toolset # This ensures future toolsets are automatically included without changes. if name in {"all", "*"}: all_tools: Set[str] = set() for toolset_name in get_toolset_names(): # Use a fresh visited set per branch to avoid cross-branch contamination resolved = resolve_toolset(toolset_name, visited.copy()) all_tools.update(resolved) return sorted(all_tools)
# Check for cycles / already-resolved (diamond deps). # Silently return [] — either this is a diamond (not a bug, tools already # collected via another path) or a genuine cycle (safe to skip). if name in visited: return []
visited.add(name)
# Get toolset definition toolset = get_toolset(name) if not toolset: return []
# Collect direct tools tools = set(toolset.get("tools", []))
# Recursively resolve included toolsets, sharing the visited set across # sibling includes so diamond dependencies are only resolved once and # cycle warnings don't fire multiple times for the same cycle. for included_name in toolset.get("includes", []): included_tools = resolve_toolset(included_name, visited) tools.update(included_tools) return sorted(tools)
def resolve_multiple_toolsets(toolset_names: List[str]) -> List[str]: """ Resolve multiple toolsets and combine their tools. Args: toolset_names (List[str]): List of toolset names to resolve Returns: List[str]: Combined list of all tool names (deduplicated) """ all_tools = set() for name in toolset_names: tools = resolve_toolset(name) all_tools.update(tools) return sorted(all_tools)
def _get_plugin_toolset_names() -> Set[str]: """Return toolset names registered by plugins (from the tool registry).
These are toolsets that exist in the registry but not in the static ``TOOLSETS`` dict — i.e. they were added by plugins at load time. """ try: from tools.registry import registry return { toolset_name for toolset_name in registry.get_registered_toolset_names() if toolset_name not in TOOLSETS } except Exception: return set()
def _get_registry_toolset_aliases() -> Dict[str, str]: """Return explicit toolset aliases registered in the live registry.""" try: from tools.registry import registry return registry.get_registered_toolset_aliases() except Exception: return {}
def get_all_toolsets() -> Dict[str, Dict[str, Any]]: """ Get all available toolsets with their definitions.
Includes both statically-defined toolsets and plugin-registered ones. Returns: Dict: All toolset definitions """ result = dict(TOOLSETS) aliases = _get_registry_toolset_aliases() for ts_name in _get_plugin_toolset_names(): display_name = ts_name for alias, canonical in aliases.items(): if canonical == ts_name and alias not in TOOLSETS: display_name = alias break if display_name in result: continue toolset = get_toolset(display_name) if toolset: result[display_name] = toolset return result
def get_toolset_names() -> List[str]: """ Get names of all available toolsets (excluding aliases).
Includes plugin-registered toolset names. Returns: List[str]: List of toolset names """ names = set(TOOLSETS.keys()) aliases = _get_registry_toolset_aliases() for ts_name in _get_plugin_toolset_names(): for alias, canonical in aliases.items(): if canonical == ts_name and alias not in TOOLSETS: names.add(alias) break else: names.add(ts_name) return sorted(names)
def validate_toolset(name: str) -> bool: """ Check if a toolset name is valid. Args: name (str): Toolset name to validate Returns: bool: True if valid, False otherwise """ # Accept special alias names for convenience if name in {"all", "*"}: return True if name in TOOLSETS: return True if name in _get_plugin_toolset_names(): return True return name in _get_registry_toolset_aliases()
def create_custom_toolset( name: str, description: str, tools: List[str] = None, includes: List[str] = None) -> None: """ Create a custom toolset at runtime. Args: name (str): Name for the new toolset description (str): Description of the toolset tools (List[str]): Direct tools to include includes (List[str]): Other toolsets to include """ TOOLSETS[name] = { "description": description, "tools": tools or [], "includes": includes or [] }
def get_toolset_info(name: str) -> Dict[str, Any]: """ Get detailed information about a toolset including resolved tools. Args: name (str): Toolset name Returns: Dict: Detailed toolset information """ toolset = get_toolset(name) if not toolset: return None resolved_tools = resolve_toolset(name) return { "name": name, "description": toolset["description"], "direct_tools": toolset["tools"], "includes": toolset["includes"], "resolved_tools": resolved_tools, "tool_count": len(resolved_tools), "is_composite": bool(toolset["includes"]) }
if __name__ == "__main__": print("Toolsets System Demo") print("=" * 60) print("\nAvailable Toolsets:") print("-" * 40) for name, toolset in get_all_toolsets().items(): info = get_toolset_info(name) composite = "[composite]" if info["is_composite"] else "[leaf]" print(f" {composite} {name:20} - {toolset['description']}") print(f" Tools: {len(info['resolved_tools'])} total") print("\nToolset Resolution Examples:") print("-" * 40) for name in ["web", "terminal", "safe", "debugging"]: tools = resolve_toolset(name) print(f"\n {name}:") print(f" Resolved to {len(tools)} tools: {', '.join(sorted(tools))}") print("\nMultiple Toolset Resolution:") print("-" * 40) combined = resolve_multiple_toolsets(["web", "vision", "terminal"]) print(" Combining ['web', 'vision', 'terminal']:") print(f" Result: {', '.join(sorted(combined))}") print("\nCustom Toolset Creation:") print("-" * 40) create_custom_toolset( name="my_custom", description="My custom toolset for specific tasks", tools=["web_search"], includes=["terminal", "vision"] ) custom_info = get_toolset_info("my_custom") print(" Created 'my_custom' toolset:") print(f" Description: {custom_info['description']}") print(f" Resolved tools: {', '.join(custom_info['resolved_tools'])}")