mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
* feat(video_gen): unified video_generate tool with pluggable provider backends One core video_generate tool, every backend a plugin. Mirrors the image_gen + memory_provider + context_engine architecture: ABC, registry, plugin-context registration hook, and per-plugin model catalogs surfaced through hermes tools. Surface (one schema, every backend): - operation: generate / edit / extend - modalities: text-to-video (prompt only), image-to-video (prompt + image_url), video edit (prompt + video_url), video extend (video_url) - reference_image_urls, duration, aspect_ratio, resolution, negative_prompt, audio, seed, model override - Providers ignore unknown kwargs and declare what they support via VideoGenProvider.capabilities() — backend-specific quirks stay in the backend, the agent learns one tool Backends shipped: - plugins/video_gen/xai/ — Grok-Imagine, full generate/edit/extend + image-to-video + reference images (salvaged from PR #10600 by @Jaaneek, reshaped into the plugin interface) - plugins/video_gen/fal/ — Veo 3.1 (t2v + i2v), Kling O3 i2v, Pixverse v6 i2v with model-aware payload building that drops keys a model doesn't declare Wiring: - agent/video_gen_provider.py — VideoGenProvider ABC, normalize_operation, success_response / error_response, save_b64_video / save_bytes_video, $HERMES_HOME/cache/videos/ - agent/video_gen_registry.py — thread-safe register/get/list + get_active_provider() reading video_gen.provider from config.yaml - hermes_cli/plugins.py — PluginContext.register_video_gen_provider() - hermes_cli/tools_config.py — Video Generation category in hermes tools, plugin-only providers list, model picker per plugin, config write to video_gen.{provider,model} - toolsets.py — new video_gen toolset - tests: 31 new tests covering ABC, registry, tool dispatch, both plugins - docs: developer-guide/video-gen-provider-plugin.md (parallel to the image-gen guide), sidebar + toolsets-reference + plugin guides updated Supersedes: #25035 (FAL), #17972 (FAL), #14543 (xAI), #13847 (HappyHorse), #10458 (provider categories), #10786 (xAI media+search bundle), #2984 (FAL duplicate), #19086 (Google Veo standalone — easy port to plugin interface). Co-authored-by: Jaaneek <Jaaneek@users.noreply.github.com> * feat(video_gen): dynamic schema reflects active backend's capabilities Address the 'capability variance' question — instead of one tool with a static schema that lies about what every backend supports, the video_generate tool now rebuilds its description at get_definitions() time based on the configured video_gen.provider and video_gen.model. The agent sees backend-specific guidance up-front: - 'fal-ai/veo3.1/image-to-video': 'image-to-video only — image_url is REQUIRED; text-only prompts will be rejected' - 'fal-ai/veo3.1' (t2v): no image_url restriction shown - xAI grok-imagine-video: 'operations: generate, edit, extend; up to 7 reference_image_urls' - Backends without edit/extend: 'not supported on this backend — surface that they need to switch backends via hermes tools' This is the same pattern PR #22694 used for delegate_task self-capping — documented in the dynamic-tool-schemas skill. Cache invalidation is free: get_tool_definitions() already memoizes on config.yaml mtime, so a mid-session backend swap rebuilds the schema automatically. Tested: - Empirical FAL OpenAPI schema check confirms image-to-video models require image_url (FAL returns HTTP 422 otherwise) — client-side rejection in FALVideoGenProvider.generate() now prevents the wasted round-trip - Live E2E: fal-ai/veo3.1/image-to-video + prompt-only → clean missing_image_url error; fal-ai/veo3.1 + prompt-only → dispatches - 6 new tests cover the builder (no config / image-only / full-surface / text-only / unknown provider / registry wiring), all passing - 37/37 in the slice, 134/134 in the broader regression set * test(video_gen/xai): full surface integration tests + cleaner schema Verified end-to-end that the xAI plugin handles every documented mode from PR #10600's surface: text-to-video, image-to-video, reference-images-to-video, video edit, video extend (with and without prompt). All five modes route to the correct xAI endpoint (/videos/generations, /videos/edits, /videos/extensions) with the right payload shape (image / reference_images / video keys), and all five client-side rejections fire before the network: edit-without-prompt, extend-without-video_url, image+refs conflict, >7 references, and duration/aspect_ratio clamping. 15 new integration tests grouped into four classes (endpoint routing, modalities, validation, clamping). httpx is stubbed via a small fake AsyncClient that records POSTs so the tests assert the actual payload the plugin would send to xAI — not just the success/error envelope. Also cleaned up a description redundancy: when a model's operations match the backend's overall set, we no longer print the duplicate 'operations supported by this model' line. xAI's description now reads: Active backend: xAI . model: grok-imagine-video - operations supported by this backend: edit, extend, generate - modalities supported by this backend: image, reference_images, text - aspect_ratio choices: 16:9, 1:1, 2:3, 3:2, 3:4, 4:3, 9:16 - resolution choices: 480p, 720p - duration range: 1-15s - reference_image_urls: up to 7 images Co-authored-by: Jaaneek <Jaaneek@users.noreply.github.com> * feat(video_gen): collapse surface to t2v + i2v, family-based auto-routing Two design changes per Teknium: 1) Drop edit/extend from the tool surface entirely. Only text-to-video and image-to-video remain. The agent sees a clean tool with two modalities; backend-specific quirks like xAI's edit/extend endpoints stay out of the unified schema. 2) FAL: pick a model FAMILY once, the plugin routes between the family's text-to-video and image-to-video endpoints based on whether image_url was passed. Users no longer pick 'fal-ai/veo3.1' AND 'fal-ai/veo3.1/image-to-video' as separate options — they pick 'veo3.1', and the plugin handles the rest. Catalog rewritten as families: veo3.1 fal-ai/veo3.1 / fal-ai/veo3.1/image-to-video pixverse-v6 fal-ai/pixverse/v6/text-to-video / fal-ai/pixverse/v6/image-to-video kling-o3-standard fal-ai/kling-video/o3/standard/text-to-video / fal-ai/kling-video/o3/standard/image-to-video xAI uses a single endpoint (/videos/generations) for both modes, routed by the presence of the 'image' field in the payload — no edit/extend exposure. Schema changes: - VIDEO_GENERATE_SCHEMA: drop operation, drop video_url. Final params: prompt (required), image_url, reference_image_urls, duration, aspect_ratio, resolution, negative_prompt, audio, seed, model. - VideoGenProvider ABC: drop normalize_operation, VALID_OPERATIONS, DEFAULT_OPERATION. capabilities() drops 'operations' key. - success_response: add 'modality' field ('text' | 'image') so the agent and logs can see which endpoint was actually hit. Dynamic schema builder simplified — no operations bullet, no 'switch backends if you need edit/extend' guidance. When the active backend supports both modalities (the common case), description reads: Active backend: FAL . model: pixverse-v6 - supports both text-to-video (omit image_url) and image-to-video (pass image_url) - routes automatically - aspect_ratio choices: 16:9, 9:16, 1:1 - resolution choices: 360p, 540p, 720p, 1080p - duration range: 1-15s - audio: pass audio=true to enable native audio (pricing tier) - negative_prompt: supported Tests: 51 in the video_gen slice, 216 across the broader image+video sweep, all passing. New FAL routing tests prove pixverse-v6 + no image hits text-to-video endpoint, pixverse-v6 + image_url hits image-to-video endpoint, same for veo3.1 and kling-o3-standard. Docs updated: developer-guide page rewrites the 'model families' pattern as a first-class section so external plugin authors know the convention. toolsets-reference and toolsets.py descriptions match the new surface. Co-authored-by: Jaaneek <Jaaneek@users.noreply.github.com> * feat(video_gen/fal): expand catalog to 6 families, cheap + premium tiers Catalog now covers everything Teknium specced from FAL: Cheap tier: ltx-2.3 fal-ai/ltx-2.3-22b/text-to-video / image-to-video pixverse-v6 fal-ai/pixverse/v6/text-to-video / image-to-video Premium tier: veo3.1 fal-ai/veo3.1 / fal-ai/veo3.1/image-to-video seedance-2.0 bytedance/seedance-2.0/text-to-video / image-to-video kling-v3-4k fal-ai/kling-video/v3/4k/text-to-video / image-to-video happy-horse fal-ai/happy-horse/text-to-video / image-to-video DEFAULT_MODEL moved from veo3.1 (premium) to pixverse-v6 (cheap, sane defaults, both modalities) — better first-run UX for users who haven't explicitly picked a model. New family-entry knob: image_param_key. Kling v3 4K's image-to-video endpoint expects start_image_url instead of image_url; declaring image_param_key='start_image_url' on the family lets _build_payload remap correctly. Other families default to plain image_url. Per-family capability flags reflect each model's docs: - LTX 2.3 + Happy Horse: minimal payloads (no duration/aspect/resolution enum exposed by FAL — let endpoint apply defaults) - Seedance: 6 aspect ratios incl 21:9, durations 4-15, audio supported, negative prompts NOT supported per docs - Kling v3 4K: 16:9/9:16/1:1, 3-15s, audio + negative - Veo 3.1: unchanged, 16:9/9:16, 4/6/8s Tests: +5 covering the new families (full catalog, Kling 4K start_image_url remap, Seedance routing, LTX payload minimality, Happy Horse minimality). 56/56 in the slice green. Note: I did NOT add the FAL-hosted xAI Grok-Imagine variant. Hermes already has a direct xAI plugin that talks to xAI's own API; routing the same model through FAL's wrapper would duplicate the surface without adding capabilities. Users on FAL who want Grok-Imagine should use the xAI plugin directly; flag if you want both routes available. * test(video_gen): tool-surface routing matrix — every model x modality End-to-end matrix test driven through _handle_video_generate() — the actual function the agent's video_generate tool call lands in. Writes config.yaml, invokes the registered handler with a raw args dict, then asserts the outbound HTTP/SDK call hit the right endpoint with the right payload shape. Parametrized over FAL_FAMILIES.keys() so the matrix auto-discovers new families as they're added (add a family to FAL_FAMILIES and you get both modalities tested for free). Coverage: - All 6 FAL families x {text-only, text+image} = 12 cases - xAI x {text-only, text+image} = 2 cases - tool-level model= arg overrides config = 2 cases For each case, verifies: - result['success'] is True - result['modality'] matches input shape ('text' if no image_url, 'image' otherwise) - outbound endpoint URL matches the family's text_endpoint or image_endpoint - text-only payloads carry no image-shaped keys - text+image payloads carry the family's image key (image_url for most, start_image_url for kling-v3-4k, wrapped 'image' object for xAI) All 16 cases passing. Confirms the tool surface routes every (provider, model, modality) combination correctly with zero leakage. * feat(video_gen): keep video_gen out of first-run setup, surface in status Two changes: 1. video_gen joins _DEFAULT_OFF_TOOLSETS, so it is NOT pre-selected in the first-run toolset checklist. Video gen is niche, paid, and slow — most users don't want it nagging them during initial setup. Anyone who wants it opts in via 'hermes tools' -> Video Generation, which already routes to the provider+model picker. 2. The 'hermes setup' status panel learns about video_gen — but only shows the row when a plugin reports available. Users without FAL_KEY/XAI_API_KEY see nothing about video gen; users with one of those keys see 'Video Generation (FAL) ✓' as confirmation it's wired. Verified live: - Fresh install (no creds): zero video_gen mentions in wizard. - With FAL_KEY: status row appears with active backend name. - 160/160 in the setup + tools_config + video_gen test slice. Rationale: image_gen is on by default because it's a featured creative tool used in casual chat (telegrams, etc). Video gen is heavier — long wait, paid per-second pricing. Default-off matches user intent better. --------- Co-authored-by: Jaaneek <Jaaneek@users.noreply.github.com>
866 lines
28 KiB
Python
866 lines
28 KiB
Python
#!/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 composed
|
|
from 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", "browser_dialog",
|
|
# 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",
|
|
# Kanban multi-agent coordination — only in schema when the agent is
|
|
# spawned as a kanban worker (HERMES_KANBAN_TASK env set) or the current
|
|
# profile explicitly enables the kanban toolset. Gated via check_fn in
|
|
# tools/kanban_tools.py.
|
|
"kanban_show", "kanban_list",
|
|
"kanban_complete", "kanban_block", "kanban_heartbeat",
|
|
"kanban_comment", "kanban_create", "kanban_link",
|
|
"kanban_unblock",
|
|
# Computer use (macOS, gated on cua-driver being installed via check_fn)
|
|
"computer_use",
|
|
]
|
|
|
|
|
|
# Core toolset definitions
|
|
# These can include individual tools or reference other toolsets
|
|
TOOLSETS = {
|
|
# 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": []
|
|
},
|
|
|
|
"video": {
|
|
"description": "Video analysis and understanding tools (opt-in, not in default toolset)",
|
|
"tools": ["video_analyze"],
|
|
"includes": []
|
|
},
|
|
|
|
"image_gen": {
|
|
"description": "Creative generation tools (images)",
|
|
"tools": ["image_generate"],
|
|
"includes": []
|
|
},
|
|
|
|
"video_gen": {
|
|
"description": (
|
|
"Video generation tools. Single ``video_generate`` tool covers "
|
|
"text-to-video (prompt only) and image-to-video (prompt + "
|
|
"image_url) — the active backend auto-routes. Configure via "
|
|
"``hermes tools`` → Video Generation."
|
|
),
|
|
"tools": ["video_generate"],
|
|
"includes": []
|
|
},
|
|
|
|
"computer_use": {
|
|
"description": (
|
|
"Background macOS desktop control via cua-driver — screenshots, "
|
|
"mouse, keyboard, scroll, drag. Does NOT steal the user's cursor "
|
|
"or keyboard focus. Works with any tool-capable model."
|
|
),
|
|
"tools": ["computer_use"],
|
|
"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",
|
|
"browser_dialog", "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": []
|
|
},
|
|
|
|
"kanban": {
|
|
"description": (
|
|
"Kanban multi-agent coordination — only active when the agent "
|
|
"is spawned by the kanban dispatcher (HERMES_KANBAN_TASK env "
|
|
"set). The dispatcher runs inside the gateway by default; see "
|
|
"`kanban.dispatch_in_gateway` in config.yaml. Lets workers mark "
|
|
"tasks done with structured handoffs, block for human input, "
|
|
"heartbeat during long ops, comment on threads, and (for "
|
|
"orchestrators) list, unblock, and fan out tasks."
|
|
),
|
|
"tools": [
|
|
"kanban_show", "kanban_list", "kanban_complete", "kanban_block",
|
|
"kanban_heartbeat", "kanban_comment",
|
|
"kanban_create", "kanban_link",
|
|
"kanban_unblock",
|
|
],
|
|
"includes": [],
|
|
},
|
|
|
|
"discord": {
|
|
"description": "Discord read and participate tools (fetch messages, search members, create threads)",
|
|
"tools": ["discord"],
|
|
"includes": [],
|
|
},
|
|
|
|
"discord_admin": {
|
|
"description": "Discord server management (list channels/roles, pin messages, assign roles)",
|
|
"tools": ["discord_admin"],
|
|
"includes": [],
|
|
},
|
|
|
|
"yuanbao": {
|
|
"description": "Yuanbao platform tools - group info, member queries, DM, stickers",
|
|
"tools": [
|
|
"yb_query_group_info",
|
|
"yb_query_group_members",
|
|
"yb_send_dm",
|
|
"yb_search_sticker",
|
|
"yb_send_sticker",
|
|
],
|
|
"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": []
|
|
},
|
|
|
|
"spotify": {
|
|
"description": "Native Spotify playback, search, playlist, album, and library tools",
|
|
"tools": [
|
|
"spotify_playback", "spotify_devices", "spotify_queue", "spotify_search",
|
|
"spotify_playlists", "spotify_albums", "spotify_library",
|
|
],
|
|
"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", "browser_dialog",
|
|
"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", "browser_dialog",
|
|
# 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-cron": {
|
|
# Mirrors hermes-cli so cron's "default" toolset is the same set of
|
|
# core tools users see interactively — then `hermes tools` filters
|
|
# them down per the platform config. _DEFAULT_OFF_TOOLSETS (moa,
|
|
# homeassistant, rl) are excluded by _get_platform_tools() unless
|
|
# the user explicitly enables them.
|
|
"description": "Default cron toolset - same core tools as hermes-cli; gated by `hermes tools`",
|
|
"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",
|
|
"discord_admin",
|
|
],
|
|
"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 + [
|
|
"feishu_doc_read",
|
|
"feishu_drive_list_comments",
|
|
"feishu_drive_list_comment_replies",
|
|
"feishu_drive_reply_comment",
|
|
"feishu_drive_add_comment",
|
|
],
|
|
"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-yuanbao": {
|
|
"description": "Yuanbao Bot 元宝消息平台工具集 - 群信息、成员查询、私聊、贴纸表情",
|
|
"tools": _HERMES_CORE_TOOLS + [
|
|
"yb_query_group_info",
|
|
"yb_query_group_members",
|
|
"yb_send_dm",
|
|
"yb_search_sticker",
|
|
"yb_send_sticker",
|
|
],
|
|
"module": "tools.yuanbao_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", "hermes-yuanbao"]
|
|
}
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
try:
|
|
from tools.registry import registry
|
|
except Exception:
|
|
return toolset if toolset else None
|
|
|
|
if toolset:
|
|
merged_tools = sorted(
|
|
set(toolset.get("tools", []))
|
|
| set(registry.get_tool_names_for_toolset(name))
|
|
)
|
|
return {**toolset, "tools": merged_tools}
|
|
|
|
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:
|
|
# Auto-generate a toolset for plugin platforms (hermes-<name>).
|
|
# Gives them _HERMES_CORE_TOOLS plus any tools the plugin registered
|
|
# into a toolset matching the platform name.
|
|
if name.startswith("hermes-"):
|
|
platform_name = name[len("hermes-"):]
|
|
try:
|
|
from gateway.platform_registry import platform_registry
|
|
if platform_registry.is_registered(platform_name):
|
|
plugin_tools = set(_HERMES_CORE_TOOLS)
|
|
try:
|
|
from tools.registry import registry
|
|
plugin_tools.update(
|
|
e.name for e in registry._tools.values()
|
|
if e.toolset == platform_name
|
|
)
|
|
except Exception:
|
|
pass
|
|
return list(plugin_tools)
|
|
except Exception:
|
|
pass
|
|
|
|
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'])}")
|