Merge branch 'simp/r3-01-E' into simp/r3-01
This commit is contained in:
+271
-563
File diff suppressed because it is too large
Load Diff
+219
-372
File diff suppressed because it is too large
Load Diff
+56
-304
@@ -1,22 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Toolset Distributions Module
|
||||
"""Toolset distributions for batch data-generation runs.
|
||||
|
||||
This module defines distributions of toolsets for data generation runs.
|
||||
Each distribution specifies which toolsets should be used and their probability
|
||||
of being selected for any given prompt during the batch processing.
|
||||
|
||||
A distribution is a dictionary mapping toolset names to their selection probability (%).
|
||||
Probabilities should sum to 100, but the system will normalize if they don't.
|
||||
|
||||
Usage:
|
||||
from toolset_distributions import get_distribution, list_distributions
|
||||
|
||||
# Get a specific distribution
|
||||
dist = get_distribution("image_gen")
|
||||
|
||||
# List all available distributions
|
||||
all_dists = list_distributions()
|
||||
A distribution maps toolset names to the % chance each is enabled for a prompt
|
||||
(sampled independently, so several toolsets can be active at once).
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Optional
|
||||
@@ -24,335 +10,101 @@ import random
|
||||
from toolsets import validate_toolset
|
||||
|
||||
|
||||
# Distribution definitions
|
||||
# Each key is a distribution name, and the value is a dict of toolset_name: probability_percentage
|
||||
def _dist(description: str, **toolsets: int) -> Dict[str, object]:
|
||||
return {"description": description, "toolsets": toolsets}
|
||||
|
||||
|
||||
DISTRIBUTIONS = {
|
||||
# Default: All tools available 100% of the time
|
||||
"default": {
|
||||
"description": "All available tools, all the time",
|
||||
"toolsets": {
|
||||
"web": 100,
|
||||
"vision": 100,
|
||||
"image_gen": 100,
|
||||
"terminal": 100,
|
||||
"file": 100,
|
||||
"browser": 100
|
||||
}
|
||||
},
|
||||
|
||||
# Image generation focused distribution
|
||||
"image_gen": {
|
||||
"description": "Heavy focus on image generation with vision and web support",
|
||||
"toolsets": {
|
||||
"image_gen": 90, # 80% chance of image generation tools
|
||||
"vision": 90, # 60% chance of vision tools
|
||||
"web": 55, # 40% chance of web tools
|
||||
"terminal": 45
|
||||
}
|
||||
},
|
||||
|
||||
# Research-focused distribution
|
||||
"research": {
|
||||
"description": "Web research with vision analysis and reasoning",
|
||||
"toolsets": {
|
||||
"web": 90, # 90% chance of web tools
|
||||
"browser": 70, # 70% chance of browser tools for deep research
|
||||
"vision": 50, # 50% chance of vision tools
|
||||
"terminal": 10 # 10% chance of terminal tools
|
||||
}
|
||||
},
|
||||
|
||||
# Scientific problem solving focused distribution
|
||||
"science": {
|
||||
"description": "Scientific research with web, terminal, file, and browser capabilities",
|
||||
"toolsets": {
|
||||
"web": 94, # 94% chance of web tools
|
||||
"terminal": 94, # 94% chance of terminal tools
|
||||
"file": 94, # 94% chance of file tools
|
||||
"vision": 65, # 65% chance of vision tools
|
||||
"browser": 50, # 50% chance of browser for accessing papers/databases
|
||||
"image_gen": 15 # 15% chance of image generation tools
|
||||
}
|
||||
},
|
||||
|
||||
# Development-focused distribution
|
||||
"development": {
|
||||
"description": "Terminal, file tools, and reasoning with occasional web lookup",
|
||||
"toolsets": {
|
||||
"terminal": 80, # 80% chance of terminal tools
|
||||
"file": 80, # 80% chance of file tools (read, write, patch, search)
|
||||
"web": 30, # 30% chance of web tools
|
||||
"vision": 10 # 10% chance of vision tools
|
||||
}
|
||||
},
|
||||
|
||||
# Safe mode (no terminal)
|
||||
"safe": {
|
||||
"description": "All tools except terminal for safety",
|
||||
"toolsets": {
|
||||
"web": 80,
|
||||
"browser": 70, # Browser is safe (no local filesystem access)
|
||||
"vision": 60,
|
||||
"image_gen": 60
|
||||
}
|
||||
},
|
||||
|
||||
# Balanced distribution
|
||||
"balanced": {
|
||||
"description": "Equal probability of all toolsets",
|
||||
"toolsets": {
|
||||
"web": 50,
|
||||
"vision": 50,
|
||||
"image_gen": 50,
|
||||
"terminal": 50,
|
||||
"file": 50,
|
||||
"browser": 50
|
||||
}
|
||||
},
|
||||
|
||||
# Minimal (web only)
|
||||
"minimal": {
|
||||
"description": "Only web tools for basic research",
|
||||
"toolsets": {
|
||||
"web": 100
|
||||
}
|
||||
},
|
||||
|
||||
# Terminal only
|
||||
"terminal_only": {
|
||||
"description": "Terminal and file tools for code execution tasks",
|
||||
"toolsets": {
|
||||
"terminal": 100,
|
||||
"file": 100
|
||||
}
|
||||
},
|
||||
|
||||
# Terminal + web (common for coding tasks that need docs)
|
||||
"terminal_web": {
|
||||
"description": "Terminal and file tools with web search for documentation lookup",
|
||||
"toolsets": {
|
||||
"terminal": 100,
|
||||
"file": 100,
|
||||
"web": 100
|
||||
}
|
||||
},
|
||||
|
||||
# Creative (vision + image generation)
|
||||
"creative": {
|
||||
"description": "Image generation and vision analysis focus",
|
||||
"toolsets": {
|
||||
"image_gen": 90,
|
||||
"vision": 90,
|
||||
"web": 30
|
||||
}
|
||||
},
|
||||
|
||||
# Reasoning heavy
|
||||
"reasoning": {
|
||||
"description": "Heavy research/reasoning distribution with minimal other tools",
|
||||
"toolsets": {
|
||||
"web": 90,
|
||||
"file": 60,
|
||||
"terminal": 20
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
# Browser-based web interaction
|
||||
"browser_use": {
|
||||
"description": "Full browser-based web interaction with search, vision, and page control",
|
||||
"toolsets": {
|
||||
"browser": 100, # All browser tools always available
|
||||
"web": 80, # Web search for finding URLs and quick lookups
|
||||
"vision": 70 # Vision analysis for images found on pages
|
||||
}
|
||||
},
|
||||
|
||||
# Browser only (no other tools)
|
||||
"browser_only": {
|
||||
"description": "Only browser automation tools for pure web interaction tasks",
|
||||
"toolsets": {
|
||||
"browser": 100
|
||||
}
|
||||
},
|
||||
|
||||
# Browser-focused tasks distribution (for browser-use-tasks.jsonl)
|
||||
"browser_tasks": {
|
||||
"description": "Browser-focused distribution (browser toolset includes web_search for finding URLs since Google blocks direct browser searches)",
|
||||
"toolsets": {
|
||||
"browser": 97, # 97% - browser tools (includes web_search) almost always available
|
||||
"vision": 12, # 12% - vision analysis occasionally
|
||||
"terminal": 15 # 15% - terminal occasionally for local operations
|
||||
}
|
||||
},
|
||||
|
||||
# Terminal-focused tasks distribution (for nous-terminal-tasks.jsonl)
|
||||
"terminal_tasks": {
|
||||
"description": "Terminal-focused distribution with high terminal/file availability, occasional other tools",
|
||||
"toolsets": {
|
||||
"terminal": 97, # 97% - terminal almost always available
|
||||
"file": 97, # 97% - file tools almost always available
|
||||
"web": 97, # 15% - web search/scrape for documentation
|
||||
"browser": 75, # 10% - browser occasionally for web interaction
|
||||
"vision": 50, # 8% - vision analysis rarely
|
||||
"image_gen": 10 # 3% - image generation very rarely
|
||||
}
|
||||
},
|
||||
|
||||
# Mixed browser+terminal tasks distribution (for mixed-browser-terminal-tasks.jsonl)
|
||||
"mixed_tasks": {
|
||||
"description": "Mixed distribution with high browser, terminal, and file availability for complex tasks",
|
||||
"toolsets": {
|
||||
"browser": 92, # 92% - browser tools highly available
|
||||
"terminal": 92, # 92% - terminal highly available
|
||||
"file": 92, # 92% - file tools highly available
|
||||
"web": 35, # 35% - web search/scrape fairly common
|
||||
"vision": 15, # 15% - vision analysis occasionally
|
||||
"image_gen": 15 # 15% - image generation occasionally
|
||||
}
|
||||
}
|
||||
"default": _dist("All available tools, all the time",
|
||||
web=100, vision=100, image_gen=100, terminal=100, file=100, browser=100),
|
||||
"image_gen": _dist("Heavy focus on image generation with vision and web support",
|
||||
image_gen=90, vision=90, web=55, terminal=45),
|
||||
"research": _dist("Web research with vision analysis and reasoning",
|
||||
web=90, browser=70, vision=50, terminal=10),
|
||||
"science": _dist("Scientific research with web, terminal, file, and browser capabilities",
|
||||
web=94, terminal=94, file=94, vision=65, browser=50, image_gen=15),
|
||||
"development": _dist("Terminal, file tools, and reasoning with occasional web lookup",
|
||||
terminal=80, file=80, web=30, vision=10),
|
||||
"safe": _dist("All tools except terminal for safety",
|
||||
web=80, browser=70, vision=60, image_gen=60),
|
||||
"balanced": _dist("Equal probability of all toolsets",
|
||||
web=50, vision=50, image_gen=50, terminal=50, file=50, browser=50),
|
||||
"minimal": _dist("Only web tools for basic research", web=100),
|
||||
"terminal_only": _dist("Terminal and file tools for code execution tasks", terminal=100, file=100),
|
||||
"terminal_web": _dist("Terminal and file tools with web search for documentation lookup",
|
||||
terminal=100, file=100, web=100),
|
||||
"creative": _dist("Image generation and vision analysis focus", image_gen=90, vision=90, web=30),
|
||||
"reasoning": _dist("Heavy research/reasoning distribution with minimal other tools",
|
||||
web=90, file=60, terminal=20),
|
||||
"browser_use": _dist("Full browser-based web interaction with search, vision, and page control",
|
||||
browser=100, web=80, vision=70),
|
||||
"browser_only": _dist("Only browser automation tools for pure web interaction tasks", browser=100),
|
||||
# browser-use-tasks.jsonl: the browser toolset includes web_search since Google blocks direct browser searches
|
||||
"browser_tasks": _dist(
|
||||
"Browser-focused distribution (browser toolset includes web_search for finding URLs since Google blocks direct browser searches)",
|
||||
browser=97, vision=12, terminal=15,
|
||||
),
|
||||
# nous-terminal-tasks.jsonl
|
||||
"terminal_tasks": _dist(
|
||||
"Terminal-focused distribution with high terminal/file availability, occasional other tools",
|
||||
terminal=97, file=97, web=97, browser=75, vision=50, image_gen=10,
|
||||
),
|
||||
# mixed-browser-terminal-tasks.jsonl
|
||||
"mixed_tasks": _dist(
|
||||
"Mixed distribution with high browser, terminal, and file availability for complex tasks",
|
||||
browser=92, terminal=92, file=92, web=35, vision=15, image_gen=15,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_distribution(name: str) -> Optional[Dict[str, any]]:
|
||||
"""
|
||||
Get a toolset distribution by name.
|
||||
|
||||
Args:
|
||||
name (str): Name of the distribution
|
||||
|
||||
Returns:
|
||||
Dict: Distribution definition with description and toolsets
|
||||
None: If distribution not found
|
||||
"""
|
||||
"""Distribution definition (description + toolsets), or None if unknown."""
|
||||
return DISTRIBUTIONS.get(name)
|
||||
|
||||
|
||||
def list_distributions() -> Dict[str, Dict]:
|
||||
"""
|
||||
List all available distributions.
|
||||
|
||||
Returns:
|
||||
Dict: All distribution definitions
|
||||
"""
|
||||
return DISTRIBUTIONS.copy()
|
||||
|
||||
|
||||
def sample_toolsets_from_distribution(distribution_name: str) -> List[str]:
|
||||
"""
|
||||
Sample toolsets based on a distribution's probabilities.
|
||||
|
||||
Each toolset in the distribution has a % chance of being included.
|
||||
This allows multiple toolsets to be active simultaneously.
|
||||
|
||||
Args:
|
||||
distribution_name (str): Name of the distribution to sample from
|
||||
|
||||
Returns:
|
||||
List[str]: List of sampled toolset names
|
||||
|
||||
Raises:
|
||||
ValueError: If distribution name is not found
|
||||
"""Sample toolset names, each included independently with its % probability.
|
||||
|
||||
Falls back to the highest-probability toolset when nothing was rolled.
|
||||
Raises ValueError for an unknown distribution.
|
||||
"""
|
||||
dist = get_distribution(distribution_name)
|
||||
if not dist:
|
||||
raise ValueError(f"Unknown distribution: {distribution_name}")
|
||||
|
||||
# Sample each toolset independently based on its probability
|
||||
|
||||
selected_toolsets = []
|
||||
|
||||
for toolset_name, probability in dist["toolsets"].items():
|
||||
# Validate toolset exists
|
||||
if not validate_toolset(toolset_name):
|
||||
print(f"⚠️ Warning: Toolset '{toolset_name}' in distribution '{distribution_name}' is not valid")
|
||||
continue
|
||||
|
||||
# Roll the dice - if random value is less than probability, include this toolset
|
||||
if random.random() * 100 < probability:
|
||||
selected_toolsets.append(toolset_name)
|
||||
|
||||
# If no toolsets were selected (can happen with low probabilities),
|
||||
# ensure at least one toolset is selected by picking the highest probability one
|
||||
|
||||
if not selected_toolsets and dist["toolsets"]:
|
||||
# Find toolset with highest probability
|
||||
highest_prob_toolset = max(dist["toolsets"].items(), key=lambda x: x[1])[0]
|
||||
if validate_toolset(highest_prob_toolset):
|
||||
selected_toolsets.append(highest_prob_toolset)
|
||||
|
||||
|
||||
return selected_toolsets
|
||||
|
||||
|
||||
def validate_distribution(distribution_name: str) -> bool:
|
||||
"""
|
||||
Check if a distribution name is valid.
|
||||
|
||||
Args:
|
||||
distribution_name (str): Distribution name to validate
|
||||
|
||||
Returns:
|
||||
bool: True if valid, False otherwise
|
||||
"""
|
||||
return distribution_name in DISTRIBUTIONS
|
||||
|
||||
|
||||
def print_distribution_info(distribution_name: str) -> None:
|
||||
"""
|
||||
Print detailed information about a distribution.
|
||||
|
||||
Args:
|
||||
distribution_name (str): Distribution name
|
||||
"""
|
||||
"""Print a distribution's description and toolset probabilities (highest first)."""
|
||||
dist = get_distribution(distribution_name)
|
||||
if not dist:
|
||||
print(f"❌ Unknown distribution: {distribution_name}")
|
||||
return
|
||||
|
||||
|
||||
print(f"\n📊 Distribution: {distribution_name}")
|
||||
print(f" Description: {dist['description']}")
|
||||
print(" Toolsets:")
|
||||
for toolset, prob in sorted(dist["toolsets"].items(), key=lambda x: x[1], reverse=True):
|
||||
print(f" • {toolset:15} : {prob:3}% chance")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""
|
||||
Demo and testing of the distributions system
|
||||
"""
|
||||
print("📊 Toolset Distributions Demo")
|
||||
print("=" * 60)
|
||||
|
||||
# List all distributions
|
||||
print("\n📋 Available Distributions:")
|
||||
print("-" * 40)
|
||||
for name, dist in list_distributions().items():
|
||||
print(f"\n {name}:")
|
||||
print(f" {dist['description']}")
|
||||
toolset_list = ", ".join([f"{ts}({p}%)" for ts, p in dist["toolsets"].items()])
|
||||
print(f" Toolsets: {toolset_list}")
|
||||
|
||||
# Demo sampling
|
||||
print("\n\n🎲 Sampling Examples:")
|
||||
print("-" * 40)
|
||||
|
||||
test_distributions = ["image_gen", "research", "balanced", "default"]
|
||||
|
||||
for dist_name in test_distributions:
|
||||
print(f"\n{dist_name}:")
|
||||
# Sample 5 times to show variability
|
||||
samples = []
|
||||
for _ in range(5):
|
||||
sampled = sample_toolsets_from_distribution(dist_name)
|
||||
samples.append(sorted(sampled))
|
||||
|
||||
print(f" Sample 1: {samples[0]}")
|
||||
print(f" Sample 2: {samples[1]}")
|
||||
print(f" Sample 3: {samples[2]}")
|
||||
print(f" Sample 4: {samples[3]}")
|
||||
print(f" Sample 5: {samples[4]}")
|
||||
|
||||
# Show detailed info
|
||||
print("\n\n📊 Detailed Distribution Info:")
|
||||
print("-" * 40)
|
||||
print_distribution_info("image_gen")
|
||||
print_distribution_info("research")
|
||||
|
||||
|
||||
+65
-148
@@ -3,45 +3,29 @@
|
||||
from typing import Dict, List, Any, Set, Optional, Tuple
|
||||
|
||||
|
||||
# Shared tool list for CLI and all messaging platform toolsets.
|
||||
# Edit this once to update all platforms simultaneously.
|
||||
# Shared tool list for CLI and all messaging platform toolsets (edit once, all
|
||||
# platforms follow). Desktop GUI affordances are deliberately NOT here: they live
|
||||
# in `desktop_ui`/`project`, enabled per desktop-sourced session by the GUI gateway
|
||||
# (tui_gateway/server.py::_load_enabled_toolsets). HA, kanban and computer_use
|
||||
# entries are further gated by their tools' check_fns.
|
||||
_HERMES_CORE_TOOLS = [
|
||||
# Web
|
||||
"web_search", "web_extract",
|
||||
# Terminal + process management
|
||||
"terminal", "process_manage",
|
||||
# Desktop GUI affordances (read_terminal, open_preview, project_*) are NOT
|
||||
# here: they live in `desktop_ui`/`project`, enabled only by the GUI gateway
|
||||
# per desktop-sourced session (tui_gateway/server.py::_load_enabled_toolsets).
|
||||
# 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",
|
||||
# replaces other tools when browser.backend is "browser-use"
|
||||
"browser_exec",
|
||||
# Text-to-speech
|
||||
"browser_exec", # replaces the other browser tools when browser.backend is "browser-use"
|
||||
"text_to_speech",
|
||||
# Planning & memory
|
||||
"todo_list", "memory",
|
||||
# Session history search
|
||||
"session_search",
|
||||
# Clarifying questions
|
||||
"clarify",
|
||||
# Code execution + delegation
|
||||
"execute_code", "delegate_task",
|
||||
# Cronjob management
|
||||
"cronjob_manage",
|
||||
# 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 coordination — check_fn in tools/kanban_tools.py admits these only
|
||||
# for kanban workers (HERMES_KANBAN_TASK) or profiles enabling `kanban`.
|
||||
"kanban_show", "kanban_list",
|
||||
"kanban_complete", "kanban_block", "kanban_request_review",
|
||||
"kanban_request_changes",
|
||||
@@ -49,17 +33,17 @@ _HERMES_CORE_TOOLS = [
|
||||
"kanban_comment", "kanban_create", "kanban_link",
|
||||
"kanban_unblock",
|
||||
"kanban_attach", "kanban_attach_url", "kanban_attachments",
|
||||
# Computer use (macOS, gated on cua-driver being installed via check_fn)
|
||||
"computer_use",
|
||||
]
|
||||
|
||||
# Webhook payloads are untrusted third-party content: no file/system execution.
|
||||
_HERMES_WEBHOOK_SAFE_TOOLS = [
|
||||
"web_search",
|
||||
"web_extract",
|
||||
"vision_analyze",
|
||||
"clarify",
|
||||
_HERMES_WEBHOOK_SAFE_TOOLS = ["web_search", "web_extract", "vision_analyze", "clarify"]
|
||||
_HA_TOOLS = ["ha_list_entities", "ha_get_state", "ha_list_services", "ha_call_service"]
|
||||
_FEISHU_TOOLS = [
|
||||
"feishu_doc_read", "feishu_drive_list_comments", "feishu_drive_list_comment_replies",
|
||||
"feishu_drive_reply_comment", "feishu_drive_add_comment",
|
||||
]
|
||||
_YUANBAO_TOOLS = ["yb_query_group_info", "yb_query_group_members", "yb_send_dm", "yb_search_sticker", "yb_send_sticker"]
|
||||
|
||||
|
||||
def _ts(description, tools=(), includes=(), **extra):
|
||||
@@ -79,11 +63,7 @@ def _core_without(*excluded, kanban=True):
|
||||
|
||||
# Coding posture: everything you reach for while pairing on code; drops messaging,
|
||||
# tts, image_gen, home-assistant, cron, kanban and computer-use.
|
||||
_CODING_TOOLS = _core_without(
|
||||
"image_generate", "text_to_speech", "cronjob_manage", "computer_use",
|
||||
"ha_list_entities", "ha_get_state", "ha_list_services", "ha_call_service",
|
||||
kanban=False,
|
||||
)
|
||||
_CODING_TOOLS = _core_without("image_generate", "text_to_speech", "cronjob_manage", "computer_use", *_HA_TOOLS, kanban=False)
|
||||
|
||||
# Core toolset definitions: individual tools or references to other toolsets.
|
||||
TOOLSETS = {
|
||||
@@ -129,12 +109,7 @@ TOOLSETS = {
|
||||
"browser": _ts(
|
||||
"Browser automation for web interaction (navigate, click, type, scroll, "
|
||||
"iframes, hold-click) with web search for finding URLs",
|
||||
[
|
||||
"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",
|
||||
"browser_exec", "web_search",
|
||||
],
|
||||
[t for t in _HERMES_CORE_TOOLS if t.startswith("browser_")] + ["web_search"],
|
||||
),
|
||||
"cronjob": _ts(
|
||||
"Cronjob management tool - create, list, update, pause, resume, remove, and "
|
||||
@@ -178,10 +153,7 @@ TOOLSETS = {
|
||||
["execute_code"],
|
||||
),
|
||||
"delegation": _ts("Spawn subagents with isolated context for complex subtasks", ["delegate_task"]),
|
||||
"homeassistant": _ts(
|
||||
"Home Assistant smart home control and monitoring",
|
||||
["ha_list_entities", "ha_get_state", "ha_list_services", "ha_call_service"],
|
||||
),
|
||||
"homeassistant": _ts("Home Assistant smart home control and monitoring", _HA_TOOLS),
|
||||
"kanban": _ts(
|
||||
"Kanban multi-agent coordination — only active when the agent is spawned by "
|
||||
"the kanban dispatcher (HERMES_KANBAN_TASK env set). The dispatcher runs "
|
||||
@@ -190,12 +162,7 @@ TOOLSETS = {
|
||||
"first-class review (request_review — not a block), return review changes, "
|
||||
"block for human input, heartbeat during long ops, comment on threads, attach "
|
||||
"files, and (for orchestrators) list, unblock, and fan out tasks.",
|
||||
[
|
||||
"kanban_show", "kanban_list", "kanban_complete", "kanban_block",
|
||||
"kanban_request_review", "kanban_request_changes", "kanban_heartbeat",
|
||||
"kanban_comment", "kanban_create", "kanban_link", "kanban_unblock",
|
||||
"kanban_attach", "kanban_attach_url", "kanban_attachments",
|
||||
],
|
||||
[t for t in _HERMES_CORE_TOOLS if t.startswith("kanban_")],
|
||||
),
|
||||
"discord": _ts(
|
||||
"Discord read and participate tools (fetch messages, search members, create threads)",
|
||||
@@ -205,21 +172,9 @@ TOOLSETS = {
|
||||
"Discord server management (list channels/roles, pin messages, assign roles)",
|
||||
["discord_admin"],
|
||||
),
|
||||
"yuanbao": _ts(
|
||||
"Yuanbao platform tools - group info, member queries, DM, stickers",
|
||||
[
|
||||
"yb_query_group_info", "yb_query_group_members", "yb_send_dm", "yb_search_sticker",
|
||||
"yb_send_sticker",
|
||||
],
|
||||
),
|
||||
"yuanbao": _ts("Yuanbao platform tools - group info, member queries, DM, stickers", _YUANBAO_TOOLS),
|
||||
"feishu_doc": _ts("Read Feishu/Lark document content", ["feishu_doc_read"]),
|
||||
"feishu_drive": _ts(
|
||||
"Feishu/Lark document comment operations (list, reply, add)",
|
||||
[
|
||||
"feishu_drive_list_comments", "feishu_drive_list_comment_replies",
|
||||
"feishu_drive_reply_comment", "feishu_drive_add_comment",
|
||||
],
|
||||
),
|
||||
"feishu_drive": _ts("Feishu/Lark document comment operations (list, reply, add)", _FEISHU_TOOLS[1:]),
|
||||
"spotify": _ts(
|
||||
"Native Spotify playback, search, playlist, album, and library tools",
|
||||
[
|
||||
@@ -286,14 +241,7 @@ TOOLSETS = {
|
||||
"hermes-mattermost": _bundle("Mattermost bot toolset - self-hosted team messaging (full access)"),
|
||||
"hermes-matrix": _bundle("Matrix bot toolset - decentralized encrypted messaging (full access)"),
|
||||
"hermes-dingtalk": _bundle("DingTalk bot toolset - enterprise messaging platform (full access)"),
|
||||
"hermes-feishu": _bundle(
|
||||
"Feishu/Lark bot toolset - enterprise messaging via Feishu/Lark (full access)",
|
||||
[
|
||||
"feishu_doc_read", "feishu_drive_list_comments",
|
||||
"feishu_drive_list_comment_replies", "feishu_drive_reply_comment",
|
||||
"feishu_drive_add_comment",
|
||||
],
|
||||
),
|
||||
"hermes-feishu": _bundle("Feishu/Lark bot toolset - enterprise messaging via Feishu/Lark (full access)", _FEISHU_TOOLS),
|
||||
"hermes-weixin": _bundle("Weixin bot toolset - personal WeChat messaging via iLink (full access)"),
|
||||
"hermes-qqbot": _bundle("QQBot toolset - QQ messaging via Official Bot API v2 (full access)"),
|
||||
"hermes-wecom": _bundle("WeCom bot toolset - enterprise WeChat messaging (full access)"),
|
||||
@@ -302,10 +250,7 @@ TOOLSETS = {
|
||||
),
|
||||
"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",
|
||||
],
|
||||
"tools": _HERMES_CORE_TOOLS + _YUANBAO_TOOLS,
|
||||
"module": "tools.yuanbao_tools",
|
||||
"includes": [],
|
||||
},
|
||||
@@ -337,30 +282,33 @@ def _registry():
|
||||
return None
|
||||
|
||||
|
||||
def _registry_call(method: str, default):
|
||||
"""registry.<method>() or *default* when the registry is unavailable or the call fails."""
|
||||
registry = _registry()
|
||||
if registry is None:
|
||||
return default
|
||||
try:
|
||||
return getattr(registry, method)()
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _registry_generation() -> Tuple[int, int]:
|
||||
reg = _registry()
|
||||
return (id(reg), getattr(reg, "_generation", 0)) if reg is not None else (0, 0)
|
||||
|
||||
|
||||
def _static_copy(toolset: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {
|
||||
**toolset,
|
||||
"tools": list(toolset.get("tools", [])),
|
||||
"includes": list(toolset.get("includes", [])),
|
||||
}
|
||||
|
||||
|
||||
def get_toolset(name: str, *, include_registry: bool = True) -> Optional[Dict[str, Any]]:
|
||||
"""Return a toolset definition, or None if unknown.
|
||||
"""Toolset definition, or None if unknown.
|
||||
|
||||
include_registry=True merges tools plugins/overlays registered into this
|
||||
toolset and resolves registry-only (plugin/MCP) toolsets and aliases.
|
||||
include_registry=False returns only the static TOOLSETS entry (copied), so
|
||||
platform reverse-mapping (#49622) is unaffected by registry additions.
|
||||
include_registry=True merges plugin/overlay tools registered into this toolset
|
||||
and resolves registry-only (plugin/MCP) toolsets and aliases; False returns a
|
||||
copy of the static TOOLSETS entry only, so platform reverse-mapping is
|
||||
unaffected by registry additions.
|
||||
"""
|
||||
toolset = TOOLSETS.get(name)
|
||||
if not include_registry:
|
||||
return _static_copy(toolset) if toolset else None
|
||||
return {**toolset, "tools": list(toolset.get("tools", [])), "includes": list(toolset.get("includes", []))} if toolset else None
|
||||
|
||||
registry = _registry()
|
||||
if registry is None:
|
||||
@@ -391,24 +339,24 @@ def get_toolset(name: str, *, include_registry: bool = True) -> Optional[Dict[st
|
||||
def bundle_non_core_tools(toolset_name: str) -> Set[str]:
|
||||
"""A bundle's tools minus _HERMES_CORE_TOOLS (one level of includes).
|
||||
|
||||
Bundles are `_HERMES_CORE_TOOLS + extras`; disabling one must not strip the
|
||||
core tools every other toolset shares. One `includes` pass suffices because
|
||||
only hermes-gateway nests bundles. Unknown names: full resolution minus core.
|
||||
Disabling a `core + extras` bundle must not strip the core tools every other
|
||||
toolset shares. One `includes` pass suffices (only hermes-gateway nests
|
||||
bundles). Unknown names: full resolution minus core.
|
||||
"""
|
||||
core = set(_HERMES_CORE_TOOLS)
|
||||
ts_def = get_toolset(toolset_name)
|
||||
if not (ts_def and "tools" in ts_def):
|
||||
return set(resolve_toolset(toolset_name)) - core
|
||||
to_remove = set(ts_def["tools"]) - core
|
||||
to_remove = set(ts_def["tools"])
|
||||
for inc in ts_def.get("includes", []):
|
||||
inc_def = get_toolset(inc)
|
||||
if inc_def and "tools" in inc_def:
|
||||
to_remove.update(set(inc_def["tools"]) - core)
|
||||
return to_remove
|
||||
to_remove.update(inc_def["tools"])
|
||||
return to_remove - core
|
||||
|
||||
|
||||
# Memo keyed on (name, include_registry, id(registry), registry generation).
|
||||
# Engages only at the public entry (visited is None); recursion is untouched.
|
||||
# Memo keyed on (name, include_registry, id(registry), registry generation);
|
||||
# engages only at the public entry (visited is None).
|
||||
_resolve_toolset_memo: Dict[Tuple[str, bool, int, int], List[str]] = {}
|
||||
|
||||
|
||||
@@ -422,23 +370,19 @@ def _plugin_platform_bundle(name: str) -> List[str]:
|
||||
from gateway.platform_registry import platform_registry
|
||||
if not platform_registry.is_registered(platform_name):
|
||||
return []
|
||||
tools = set(_HERMES_CORE_TOOLS)
|
||||
registry = _registry()
|
||||
if registry is not None:
|
||||
try:
|
||||
tools.update(e.name for e in registry.get_all_entries() if e.toolset == platform_name)
|
||||
except Exception:
|
||||
pass
|
||||
return list(tools)
|
||||
except Exception:
|
||||
return []
|
||||
tools = set(_HERMES_CORE_TOOLS)
|
||||
try:
|
||||
tools.update(e.name for e in _registry_call("get_all_entries", ()) if e.toolset == platform_name)
|
||||
except Exception:
|
||||
pass
|
||||
return list(tools)
|
||||
|
||||
|
||||
def resolve_toolset(name: str, visited: Set[str] = None, *, include_registry: bool = True) -> List[str]:
|
||||
"""Recursively resolve a toolset (and its includes) to a sorted tool-name list.
|
||||
|
||||
include_registry=False resolves the static TOOLSETS view only (#49622).
|
||||
"""
|
||||
include_registry=False resolves the static TOOLSETS view only."""
|
||||
external_call = visited is None
|
||||
if external_call:
|
||||
memo_key = (name, include_registry, *_registry_generation())
|
||||
@@ -454,8 +398,7 @@ def resolve_toolset(name: str, visited: Set[str] = None, *, include_registry: bo
|
||||
all_tools.update(resolve_toolset(toolset_name, visited.copy(), include_registry=include_registry))
|
||||
return sorted(all_tools)
|
||||
|
||||
# Diamond include or cycle: return [] silently — the tools were (or will
|
||||
# be) collected via another path, so this is not an error.
|
||||
# Diamond include or cycle: [] silently — the tools are collected via another path.
|
||||
if name in visited:
|
||||
return []
|
||||
visited.add(name)
|
||||
@@ -470,32 +413,19 @@ def resolve_toolset(name: str, visited: Set[str] = None, *, include_registry: bo
|
||||
|
||||
result = sorted(tools)
|
||||
if external_call:
|
||||
# Stale-generation entries are never hit again; bound the memo.
|
||||
if len(_resolve_toolset_memo) >= 256:
|
||||
if len(_resolve_toolset_memo) >= 256: # stale-generation entries are never hit again
|
||||
_resolve_toolset_memo.clear()
|
||||
_resolve_toolset_memo[(name, include_registry, *_registry_generation())] = list(result)
|
||||
_resolve_toolset_memo[memo_key] = list(result)
|
||||
return result
|
||||
|
||||
|
||||
def _get_plugin_toolset_names() -> Set[str]:
|
||||
"""Registry toolset names absent from the static TOOLSETS dict."""
|
||||
registry = _registry()
|
||||
if registry is None:
|
||||
return set()
|
||||
try:
|
||||
return {n for n in registry.get_registered_toolset_names() if n not in TOOLSETS}
|
||||
except Exception:
|
||||
return set()
|
||||
return {n for n in _registry_call("get_registered_toolset_names", ()) if n not in TOOLSETS}
|
||||
|
||||
|
||||
def _get_registry_toolset_aliases() -> Dict[str, str]:
|
||||
registry = _registry()
|
||||
if registry is None:
|
||||
return {}
|
||||
try:
|
||||
return registry.get_registered_toolset_aliases()
|
||||
except Exception:
|
||||
return {}
|
||||
return _registry_call("get_registered_toolset_aliases", {})
|
||||
|
||||
|
||||
def _display_alias(ts_name: str, aliases: Dict[str, str]) -> Optional[str]:
|
||||
@@ -527,23 +457,13 @@ def get_toolset_names() -> List[str]:
|
||||
|
||||
|
||||
def validate_toolset(name: str) -> bool:
|
||||
if name in {"all", "*"} or name in TOOLSETS:
|
||||
return True
|
||||
return name in _get_plugin_toolset_names() or name in _get_registry_toolset_aliases()
|
||||
return (name in {"all", "*"} or name in TOOLSETS
|
||||
or name in _get_plugin_toolset_names() or name in _get_registry_toolset_aliases())
|
||||
|
||||
|
||||
def create_custom_toolset(
|
||||
name: str,
|
||||
description: str,
|
||||
tools: List[str] = None,
|
||||
includes: List[str] = None
|
||||
) -> None:
|
||||
def create_custom_toolset(name: str, description: str, tools: List[str] = None, includes: List[str] = None) -> None:
|
||||
"""Register a runtime toolset in TOOLSETS."""
|
||||
TOOLSETS[name] = {
|
||||
"description": description,
|
||||
"tools": tools or [],
|
||||
"includes": includes or []
|
||||
}
|
||||
TOOLSETS[name] = _ts(description, tools or [], includes or [])
|
||||
|
||||
|
||||
def get_toolset_info(name: str) -> Dict[str, Any]:
|
||||
@@ -553,11 +473,8 @@ def get_toolset_info(name: str) -> Dict[str, Any]:
|
||||
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"])
|
||||
"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"]),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user