Update to use toolsets and make them easy to create and configure

This commit is contained in:
Teknium 2025-09-10 00:43:55 -07:00
parent c7fa4447b8
commit 17608c1142
5 changed files with 1210 additions and 742 deletions

121
README.md
View file

@ -1,13 +1,65 @@
# Hermes Agent
An AI agent with advanced tool-calling capabilities, featuring a flexible toolsets system for organizing and managing tools.
## Features
- **Web Tools**: Search, extract content, and crawl websites
- **Terminal Tools**: Execute commands with interactive session support
- **Vision Tools**: Analyze images from URLs
- **Reasoning Tools**: Advanced multi-model reasoning (Mixture of Agents)
- **Creative Tools**: Generate images from text prompts
- **Toolsets System**: Organize tools into logical groups for different scenarios
## Setup ## Setup
``` ```bash
pip install -r requirements.txt pip install -r requirements.txt
git clone git@github.com:NousResearch/hecate.git git clone git@github.com:NousResearch/hecate.git
cd hecate cd hecate
pip install -e . pip install -e .
``` ```
## Run ## Toolsets System
The agent uses a toolsets system for organizing and managing tools. All tools must be part of a toolset to be accessible - individual tool selection is not supported. This ensures consistent and logical grouping of capabilities.
### Key Concepts
- **Toolsets**: Logical groups of tools for specific use cases (e.g., "research", "development", "debugging")
- **Composition**: Toolsets can include other toolsets for powerful combinations
- **Custom Toolsets**: Create your own toolsets at runtime or by editing `toolsets.py`
- **Toolset-Only Access**: Tools are only accessible through toolsets, not individually
### Available Toolsets
See `toolsets.py` for the complete list of predefined toolsets including:
- Basic toolsets (web, terminal, vision, creative, reasoning)
- Composite toolsets (research, development, analysis, etc.)
- Scenario-specific toolsets (debugging, documentation, API testing, etc.)
- Special toolsets (safe mode without terminal, minimal, offline)
### Using Toolsets
```bash
# Use a predefined toolset
python run_agent.py --enabled_toolsets=research --query "Find latest AI papers"
# Combine multiple toolsets
python run_agent.py --enabled_toolsets=web,vision --query "Analyze this website"
# Safe mode (no terminal access)
python run_agent.py --enabled_toolsets=safe --query "Help without running commands"
# List all available toolsets and tools
python run_agent.py --list_tools
``` ```
For detailed documentation on toolsets, see `TOOLSETS_README.md`.
## Basic Usage
### Default (all tools enabled)
```bash
python run_agent.py \ python run_agent.py \
--query "search up the latest docs on jit in python 3.13 and write me basic example that's not in their docs. profile its perf" \ --query "search up the latest docs on jit in python 3.13 and write me basic example that's not in their docs. profile its perf" \
--max_turns 20 \ --max_turns 20 \
@ -15,3 +67,68 @@ python run_agent.py \
--base_url https://api.anthropic.com/v1/ \ --base_url https://api.anthropic.com/v1/ \
--api_key $ANTHROPIC_API_KEY --api_key $ANTHROPIC_API_KEY
``` ```
### With specific toolset
```bash
python run_agent.py \
--query "Debug this Python error" \
--enabled_toolsets=debugging \
--model claude-sonnet-4-20250514 \
--api_key $ANTHROPIC_API_KEY
```
### Python API
```python
from run_agent import AIAgent
# Use a specific toolset
agent = AIAgent(
model="claude-opus-4-20250514",
enabled_toolsets=["research"]
)
response = agent.chat("Find information about quantum computing")
# Create custom toolset at runtime
from toolsets import create_custom_toolset
create_custom_toolset(
name="my_tools",
description="My custom toolkit",
tools=["web_search"],
includes=["terminal", "vision"]
)
agent = AIAgent(enabled_toolsets=["my_tools"])
```
## Command Line Arguments
- `--query`: The question or task for the agent
- `--model`: Model to use (default: claude-opus-4-20250514)
- `--api_key`: API key for authentication
- `--base_url`: API endpoint URL
- `--max_turns`: Maximum number of tool-calling iterations
- `--enabled_toolsets`: Comma-separated list of toolsets to enable
- `--disabled_toolsets`: Comma-separated list of toolsets to disable
- `--list_tools`: List all available toolsets and tools
- `--save_trajectories`: Save conversation trajectories to JSONL files
## Environment Variables
Set these environment variables to enable different tools:
- `FIRECRAWL_API_KEY`: For web tools (search, extract, crawl)
- `MORPH_API_KEY`: For terminal tools
- `NOUS_API_KEY`: For vision and reasoning tools
- `FAL_KEY`: For image generation tools
- `ANTHROPIC_API_KEY`: For the main agent model
## Documentation
- `TOOLSETS_README.md`: Comprehensive guide to the toolsets system
- `toolsets.py`: View and modify available toolsets
- `model_tools.py`: Core tool definitions and handlers
## Examples
See `TOOLSETS_README.md` for extensive examples of using different toolsets for various scenarios.

View file

@ -23,7 +23,7 @@ Usage:
web_tools = get_tool_definitions(enabled_toolsets=['web_tools']) web_tools = get_tool_definitions(enabled_toolsets=['web_tools'])
# Handle function calls from model # Handle function calls from model
result = handle_function_call("web_search", {"query": "Python", "limit": 3}) result = handle_function_call("web_search", {"query": "Python"})
""" """
import json import json
@ -35,6 +35,11 @@ from terminal_tool import terminal_tool, check_hecate_requirements, TERMINAL_TOO
from vision_tools import vision_analyze_tool, check_vision_requirements from vision_tools import vision_analyze_tool, check_vision_requirements
from mixture_of_agents_tool import mixture_of_agents_tool, check_moa_requirements from mixture_of_agents_tool import mixture_of_agents_tool, check_moa_requirements
from image_generation_tool import image_generate_tool, check_image_generation_requirements from image_generation_tool import image_generate_tool, check_image_generation_requirements
from toolsets import (
get_toolset, resolve_toolset, resolve_multiple_toolsets,
get_all_toolsets, get_toolset_names, validate_toolset,
get_toolset_info, print_toolset_tree
)
def get_web_tool_definitions() -> List[Dict[str, Any]]: def get_web_tool_definitions() -> List[Dict[str, Any]]:
""" """
@ -48,20 +53,13 @@ def get_web_tool_definitions() -> List[Dict[str, Any]]:
"type": "function", "type": "function",
"function": { "function": {
"name": "web_search", "name": "web_search",
"description": "Search the web for information on any topic. Returns relevant results with titles and URLs. Uses advanced search depth for comprehensive results.", "description": "Search the web for information on any topic. Returns up to 5 relevant results with titles and URLs. Uses advanced search depth for comprehensive results.",
"parameters": { "parameters": {
"type": "object", "type": "object",
"properties": { "properties": {
"query": { "query": {
"type": "string", "type": "string",
"description": "The search query to look up on the web" "description": "The search query to look up on the web"
},
"limit": {
"type": "integer",
"description": "Maximum number of results to return (default: 5, max: 10)",
"default": 5,
"minimum": 1,
"maximum": 10
} }
}, },
"required": ["query"] "required": ["query"]
@ -308,145 +306,146 @@ def get_toolset_for_tool(tool_name: str) -> str:
def get_tool_definitions( def get_tool_definitions(
enabled_tools: List[str] = None,
disabled_tools: List[str] = None,
enabled_toolsets: List[str] = None, enabled_toolsets: List[str] = None,
disabled_toolsets: List[str] = None disabled_toolsets: List[str] = None
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
""" """
Get tool definitions for model API calls with optional filtering. Get tool definitions for model API calls with toolset-based filtering.
This function aggregates tool definitions from all available toolsets This function aggregates tool definitions from available toolsets.
and applies filtering based on the provided parameters. All tools must be part of a toolset to be accessible. Individual tool
selection is not supported - use toolsets to organize and select tools.
Filter Priority (higher priority overrides lower):
1. enabled_tools (highest priority - only these tools, overrides everything)
2. disabled_tools (applied after toolset filtering)
3. enabled_toolsets (only tools from these toolsets)
4. disabled_toolsets (exclude tools from these toolsets)
Args: Args:
enabled_tools (List[str]): Only include these specific tools. If provided, enabled_toolsets (List[str]): Only include tools from these toolsets.
ONLY these tools will be included (overrides all other filters) If None, all available tools are included.
disabled_tools (List[str]): Exclude these specific tools (applied after toolset filtering) disabled_toolsets (List[str]): Exclude tools from these toolsets.
enabled_toolsets (List[str]): Only include tools from these toolsets Applied only if enabled_toolsets is None.
disabled_toolsets (List[str]): Exclude tools from these toolsets
Returns: Returns:
List[Dict]: Filtered list of tool definitions List[Dict]: Filtered list of tool definitions
Examples: Examples:
# Only web tools # Use predefined toolsets
tools = get_tool_definitions(enabled_toolsets=["web_tools"]) tools = get_tool_definitions(enabled_toolsets=["research"])
tools = get_tool_definitions(enabled_toolsets=["development"])
# All tools except terminal # Combine multiple toolsets
tools = get_tool_definitions(disabled_tools=["terminal"]) tools = get_tool_definitions(enabled_toolsets=["web", "vision"])
# Only specific tools (overrides toolset filters) # All tools except those in terminal toolset
tools = get_tool_definitions(enabled_tools=["web_search", "web_extract"]) tools = get_tool_definitions(disabled_toolsets=["terminal"])
# Conflicting filters (enabled_tools wins) # Default - all available tools
tools = get_tool_definitions(enabled_toolsets=["web_tools"], enabled_tools=["terminal"]) tools = get_tool_definitions()
# Result: Only terminal tool (enabled_tools overrides enabled_toolsets)
""" """
# Detect and warn about potential conflicts # Collect all available tool definitions
conflicts_detected = False all_available_tools_map = {}
if enabled_tools and (enabled_toolsets or disabled_toolsets or disabled_tools): # Map tool names to their definitions
print("⚠️ enabled_tools overrides all other filters") if check_firecrawl_api_key():
conflicts_detected = True for tool in get_web_tool_definitions():
all_available_tools_map[tool["function"]["name"]] = tool
if enabled_toolsets and disabled_toolsets: if check_hecate_requirements():
# Check for overlap for tool in get_terminal_tool_definitions():
enabled_set = set(enabled_toolsets) all_available_tools_map[tool["function"]["name"]] = tool
disabled_set = set(disabled_toolsets)
overlap = enabled_set & disabled_set
if overlap:
print(f"⚠️ Conflicting toolsets: {overlap} in both enabled and disabled")
print(f" → enabled_toolsets takes priority")
conflicts_detected = True
if enabled_tools and disabled_tools: if check_vision_requirements():
# Check for overlap for tool in get_vision_tool_definitions():
enabled_set = set(enabled_tools) all_available_tools_map[tool["function"]["name"]] = tool
disabled_set = set(disabled_tools)
overlap = enabled_set & disabled_set
if overlap:
print(f"⚠️ Conflicting tools: {overlap} in both enabled and disabled")
print(f" → enabled_tools takes priority")
conflicts_detected = True
all_tools = [] if check_moa_requirements():
for tool in get_moa_tool_definitions():
all_available_tools_map[tool["function"]["name"]] = tool
# Collect all available tools from each toolset if check_image_generation_requirements():
toolset_tools = { for tool in get_image_tool_definitions():
"web_tools": get_web_tool_definitions() if check_firecrawl_api_key() else [], all_available_tools_map[tool["function"]["name"]] = tool
"terminal_tools": get_terminal_tool_definitions() if check_hecate_requirements() else [],
"vision_tools": get_vision_tool_definitions() if check_vision_requirements() else [],
"moa_tools": get_moa_tool_definitions() if check_moa_requirements() else [],
"image_tools": get_image_tool_definitions() if check_image_generation_requirements() else []
}
# HIGHEST PRIORITY: enabled_tools (overrides everything) # Determine which tools to include based on toolsets
if enabled_tools: tools_to_include = set()
if conflicts_detected:
print(f"🎯 Using only enabled_tools: {enabled_tools}")
# Collect all available tools first
all_available_tools = []
for tools in toolset_tools.values():
all_available_tools.extend(tools)
# Only include specifically enabled tools
tool_names_to_include = set(enabled_tools)
filtered_tools = [
tool for tool in all_available_tools
if tool["function"]["name"] in tool_names_to_include
]
# Warn about requested tools that aren't available
found_tools = {tool["function"]["name"] for tool in filtered_tools}
missing_tools = tool_names_to_include - found_tools
if missing_tools:
print(f"⚠️ Requested tools not available: {missing_tools}")
return filtered_tools
# Apply toolset-level filtering first
if enabled_toolsets: if enabled_toolsets:
# Only include tools from enabled toolsets # Only include tools from enabled toolsets
for toolset_name in enabled_toolsets: for toolset_name in enabled_toolsets:
if toolset_name in toolset_tools: if validate_toolset(toolset_name):
all_tools.extend(toolset_tools[toolset_name]) resolved_tools = resolve_toolset(toolset_name)
tools_to_include.update(resolved_tools)
print(f"✅ Enabled toolset '{toolset_name}': {', '.join(resolved_tools) if resolved_tools else 'no tools'}")
else: else:
print(f"⚠️ Unknown toolset: {toolset_name}") # Try legacy compatibility
if toolset_name in ["web_tools", "terminal_tools", "vision_tools", "moa_tools", "image_tools"]:
# Map legacy names to new system
legacy_map = {
"web_tools": ["web_search", "web_extract", "web_crawl"],
"terminal_tools": ["terminal"],
"vision_tools": ["vision_analyze"],
"moa_tools": ["mixture_of_agents"],
"image_tools": ["image_generate"]
}
legacy_tools = legacy_map.get(toolset_name, [])
tools_to_include.update(legacy_tools)
print(f"✅ Enabled legacy toolset '{toolset_name}': {', '.join(legacy_tools)}")
else:
print(f"⚠️ Unknown toolset: {toolset_name}")
elif disabled_toolsets: elif disabled_toolsets:
# Include all tools except from disabled toolsets # Start with all tools from all toolsets, then remove disabled ones
for toolset_name, tools in toolset_tools.items(): # Note: Only tools that are part of toolsets are accessible
if toolset_name not in disabled_toolsets: # We need to get all tools from all defined toolsets
all_tools.extend(tools) from toolsets import get_all_toolsets
all_toolset_tools = set()
for toolset_name in get_all_toolsets():
resolved_tools = resolve_toolset(toolset_name)
all_toolset_tools.update(resolved_tools)
# Start with all tools from toolsets
tools_to_include = all_toolset_tools
# Remove tools from disabled toolsets
for toolset_name in disabled_toolsets:
if validate_toolset(toolset_name):
resolved_tools = resolve_toolset(toolset_name)
tools_to_include.difference_update(resolved_tools)
print(f"🚫 Disabled toolset '{toolset_name}': {', '.join(resolved_tools) if resolved_tools else 'no tools'}")
else:
# Try legacy compatibility
if toolset_name in ["web_tools", "terminal_tools", "vision_tools", "moa_tools", "image_tools"]:
legacy_map = {
"web_tools": ["web_search", "web_extract", "web_crawl"],
"terminal_tools": ["terminal"],
"vision_tools": ["vision_analyze"],
"moa_tools": ["mixture_of_agents"],
"image_tools": ["image_generate"]
}
legacy_tools = legacy_map.get(toolset_name, [])
tools_to_include.difference_update(legacy_tools)
print(f"🚫 Disabled legacy toolset '{toolset_name}': {', '.join(legacy_tools)}")
else:
print(f"⚠️ Unknown toolset: {toolset_name}")
else: else:
# Include all available tools # No filtering - include all tools from all defined toolsets
for tools in toolset_tools.values(): from toolsets import get_all_toolsets
all_tools.extend(tools) for toolset_name in get_all_toolsets():
resolved_tools = resolve_toolset(toolset_name)
tools_to_include.update(resolved_tools)
# Apply tool-level filtering (disabled_tools) # Build final tool list (only include tools that are available)
if disabled_tools: filtered_tools = []
tool_names_to_exclude = set(disabled_tools) for tool_name in tools_to_include:
original_tools = [tool["function"]["name"] for tool in all_tools] if tool_name in all_available_tools_map:
filtered_tools.append(all_available_tools_map[tool_name])
all_tools = [ # Sort tools for consistent ordering
tool for tool in all_tools filtered_tools.sort(key=lambda t: t["function"]["name"])
if tool["function"]["name"] not in tool_names_to_exclude
]
# Show what was actually filtered out if filtered_tools:
remaining_tools = {tool["function"]["name"] for tool in all_tools} tool_names = [t["function"]["name"] for t in filtered_tools]
actually_excluded = set(original_tools) & tool_names_to_exclude print(f"🛠️ Final tool selection ({len(filtered_tools)} tools): {', '.join(tool_names)}")
if actually_excluded: else:
print(f"🚫 Excluded tools: {actually_excluded}") print("🛠️ No tools selected (all filtered out or unavailable)")
return all_tools return filtered_tools
def handle_web_function_call(function_name: str, function_args: Dict[str, Any]) -> str: def handle_web_function_call(function_name: str, function_args: Dict[str, Any]) -> str:
""" """
@ -461,9 +460,8 @@ def handle_web_function_call(function_name: str, function_args: Dict[str, Any])
""" """
if function_name == "web_search": if function_name == "web_search":
query = function_args.get("query", "") query = function_args.get("query", "")
limit = function_args.get("limit", 5) # Always use fixed limit of 5
# Ensure limit is within bounds limit = 5
limit = max(1, min(10, limit))
return web_search_tool(query, limit) return web_search_tool(query, limit)
elif function_name == "web_extract": elif function_name == "web_extract":

View file

@ -21,6 +21,7 @@ Usage:
""" """
import json import json
import os import os
import time import time
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
@ -47,8 +48,6 @@ class AIAgent:
model: str = "gpt-4", model: str = "gpt-4",
max_iterations: int = 10, max_iterations: int = 10,
tool_delay: float = 1.0, tool_delay: float = 1.0,
enabled_tools: List[str] = None,
disabled_tools: List[str] = None,
enabled_toolsets: List[str] = None, enabled_toolsets: List[str] = None,
disabled_toolsets: List[str] = None, disabled_toolsets: List[str] = None,
save_trajectories: bool = False save_trajectories: bool = False
@ -62,8 +61,6 @@ class AIAgent:
model (str): Model name to use (default: "gpt-4") model (str): Model name to use (default: "gpt-4")
max_iterations (int): Maximum number of tool calling iterations (default: 10) max_iterations (int): Maximum number of tool calling iterations (default: 10)
tool_delay (float): Delay between tool calls in seconds (default: 1.0) tool_delay (float): Delay between tool calls in seconds (default: 1.0)
enabled_tools (List[str]): Only enable these specific tools (optional)
disabled_tools (List[str]): Disable these specific tools (optional)
enabled_toolsets (List[str]): Only enable tools from these toolsets (optional) enabled_toolsets (List[str]): Only enable tools from these toolsets (optional)
disabled_toolsets (List[str]): Disable tools from these toolsets (optional) disabled_toolsets (List[str]): Disable tools from these toolsets (optional)
save_trajectories (bool): Whether to save conversation trajectories to JSONL files (default: False) save_trajectories (bool): Whether to save conversation trajectories to JSONL files (default: False)
@ -73,9 +70,7 @@ class AIAgent:
self.tool_delay = tool_delay self.tool_delay = tool_delay
self.save_trajectories = save_trajectories self.save_trajectories = save_trajectories
# Store tool filtering options # Store toolset filtering options
self.enabled_tools = enabled_tools
self.disabled_tools = disabled_tools
self.enabled_toolsets = enabled_toolsets self.enabled_toolsets = enabled_toolsets
self.disabled_toolsets = disabled_toolsets self.disabled_toolsets = disabled_toolsets
@ -98,8 +93,6 @@ class AIAgent:
# Get available tools with filtering # Get available tools with filtering
self.tools = get_tool_definitions( self.tools = get_tool_definitions(
enabled_tools=enabled_tools,
disabled_tools=disabled_tools,
enabled_toolsets=enabled_toolsets, enabled_toolsets=enabled_toolsets,
disabled_toolsets=disabled_toolsets disabled_toolsets=disabled_toolsets
) )
@ -110,10 +103,6 @@ class AIAgent:
print(f"🛠️ Loaded {len(self.tools)} tools: {', '.join(tool_names)}") print(f"🛠️ Loaded {len(self.tools)} tools: {', '.join(tool_names)}")
# Show filtering info if applied # Show filtering info if applied
if enabled_tools:
print(f" ✅ Enabled tools: {', '.join(enabled_tools)}")
if disabled_tools:
print(f" ❌ Disabled tools: {', '.join(disabled_tools)}")
if enabled_toolsets: if enabled_toolsets:
print(f" ✅ Enabled toolsets: {', '.join(enabled_toolsets)}") print(f" ✅ Enabled toolsets: {', '.join(enabled_toolsets)}")
if disabled_toolsets: if disabled_toolsets:
@ -475,8 +464,6 @@ def main(
api_key: str = None, api_key: str = None,
base_url: str = "https://api.anthropic.com/v1/", base_url: str = "https://api.anthropic.com/v1/",
max_turns: int = 10, max_turns: int = 10,
enabled_tools: str = None,
disabled_tools: str = None,
enabled_toolsets: str = None, enabled_toolsets: str = None,
disabled_toolsets: str = None, disabled_toolsets: str = None,
list_tools: bool = False, list_tools: bool = False,
@ -491,12 +478,15 @@ def main(
api_key (str): API key for authentication. Uses ANTHROPIC_API_KEY env var if not provided. api_key (str): API key for authentication. Uses ANTHROPIC_API_KEY env var if not provided.
base_url (str): Base URL for the model API. Defaults to https://api.anthropic.com/v1/ base_url (str): Base URL for the model API. Defaults to https://api.anthropic.com/v1/
max_turns (int): Maximum number of API call iterations. Defaults to 10. max_turns (int): Maximum number of API call iterations. Defaults to 10.
enabled_tools (str): Comma-separated list of tools to enable (e.g., "web_search,terminal") enabled_toolsets (str): Comma-separated list of toolsets to enable. Supports predefined
disabled_tools (str): Comma-separated list of tools to disable (e.g., "terminal") toolsets (e.g., "research", "development", "safe").
enabled_toolsets (str): Comma-separated list of toolsets to enable (e.g., "web_tools") Multiple toolsets can be combined: "web,vision"
disabled_toolsets (str): Comma-separated list of toolsets to disable (e.g., "terminal_tools") disabled_toolsets (str): Comma-separated list of toolsets to disable (e.g., "terminal")
list_tools (bool): Just list available tools and exit list_tools (bool): Just list available tools and exit
save_trajectories (bool): Save conversation trajectories to JSONL files. Defaults to False. save_trajectories (bool): Save conversation trajectories to JSONL files. Defaults to False.
Toolset Examples:
- "research": Web search, extract, crawl + vision tools
""" """
print("🤖 AI Agent with Tool Calling") print("🤖 AI Agent with Tool Calling")
print("=" * 50) print("=" * 50)
@ -504,14 +494,58 @@ def main(
# Handle tool listing # Handle tool listing
if list_tools: if list_tools:
from model_tools import get_all_tool_names, get_toolset_for_tool, get_available_toolsets from model_tools import get_all_tool_names, get_toolset_for_tool, get_available_toolsets
from toolsets import get_all_toolsets, get_toolset_info
print("📋 Available Tools & Toolsets:") print("📋 Available Tools & Toolsets:")
print("-" * 30) print("-" * 50)
# Show toolsets # Show new toolsets system
toolsets = get_available_toolsets() print("\n🎯 Predefined Toolsets (New System):")
print("📦 Toolsets:") print("-" * 40)
for name, info in toolsets.items(): all_toolsets = get_all_toolsets()
# Group by category
basic_toolsets = []
composite_toolsets = []
scenario_toolsets = []
for name, toolset in all_toolsets.items():
info = get_toolset_info(name)
if info:
entry = (name, info)
if name in ["web", "terminal", "vision", "creative", "reasoning"]:
basic_toolsets.append(entry)
elif name in ["research", "development", "analysis", "content_creation", "full_stack"]:
composite_toolsets.append(entry)
else:
scenario_toolsets.append(entry)
# Print basic toolsets
print("\n📌 Basic Toolsets:")
for name, info in basic_toolsets:
tools_str = ', '.join(info['resolved_tools']) if info['resolved_tools'] else 'none'
print(f"{name:15} - {info['description']}")
print(f" Tools: {tools_str}")
# Print composite toolsets
print("\n📂 Composite Toolsets (built from other toolsets):")
for name, info in composite_toolsets:
includes_str = ', '.join(info['includes']) if info['includes'] else 'none'
print(f"{name:15} - {info['description']}")
print(f" Includes: {includes_str}")
print(f" Total tools: {info['tool_count']}")
# Print scenario-specific toolsets
print("\n🎭 Scenario-Specific Toolsets:")
for name, info in scenario_toolsets:
print(f"{name:20} - {info['description']}")
print(f" Total tools: {info['tool_count']}")
# Show legacy toolset compatibility
print("\n📦 Legacy Toolsets (for backward compatibility):")
legacy_toolsets = get_available_toolsets()
for name, info in legacy_toolsets.items():
status = "" if info["available"] else "" status = "" if info["available"] else ""
print(f" {status} {name}: {info['description']}") print(f" {status} {name}: {info['description']}")
if not info["available"]: if not info["available"]:
@ -520,35 +554,30 @@ def main(
# Show individual tools # Show individual tools
all_tools = get_all_tool_names() all_tools = get_all_tool_names()
print(f"\n🔧 Individual Tools ({len(all_tools)} available):") print(f"\n🔧 Individual Tools ({len(all_tools)} available):")
for tool_name in all_tools: for tool_name in sorted(all_tools):
toolset = get_toolset_for_tool(tool_name) toolset = get_toolset_for_tool(tool_name)
print(f" 📌 {tool_name} (from {toolset})") print(f" 📌 {tool_name} (from {toolset})")
print(f"\n💡 Usage Examples:") print(f"\n💡 Usage Examples:")
print(f" # Run with only web tools") print(f" # Use predefined toolsets")
print(f" python run_agent.py --enabled_toolsets=web_tools --query='search for Python news'") print(f" python run_agent.py --enabled_toolsets=research --query='search for Python news'")
print(f" # Run with specific tools only") print(f" python run_agent.py --enabled_toolsets=development --query='debug this code'")
print(f" python run_agent.py --enabled_tools=web_search,web_extract --query='research topic'") print(f" python run_agent.py --enabled_toolsets=safe --query='analyze without terminal'")
print(f" # Run without terminal tools") print(f" ")
print(f" python run_agent.py --disabled_tools=terminal --query='web research only'") print(f" # Combine multiple toolsets")
print(f" python run_agent.py --enabled_toolsets=web,vision --query='analyze website'")
print(f" ")
print(f" # Disable toolsets")
print(f" python run_agent.py --disabled_toolsets=terminal --query='no command execution'")
print(f" ")
print(f" # Run with trajectory saving enabled") print(f" # Run with trajectory saving enabled")
print(f" python run_agent.py --save_trajectories --query='your question here'") print(f" python run_agent.py --save_trajectories --query='your question here'")
return return
# Parse tool selection arguments # Parse toolset selection arguments
enabled_tools_list = None
disabled_tools_list = None
enabled_toolsets_list = None enabled_toolsets_list = None
disabled_toolsets_list = None disabled_toolsets_list = None
if enabled_tools:
enabled_tools_list = [t.strip() for t in enabled_tools.split(",")]
print(f"🎯 Enabled tools: {enabled_tools_list}")
if disabled_tools:
disabled_tools_list = [t.strip() for t in disabled_tools.split(",")]
print(f"🚫 Disabled tools: {disabled_tools_list}")
if enabled_toolsets: if enabled_toolsets:
enabled_toolsets_list = [t.strip() for t in enabled_toolsets.split(",")] enabled_toolsets_list = [t.strip() for t in enabled_toolsets.split(",")]
print(f"🎯 Enabled toolsets: {enabled_toolsets_list}") print(f"🎯 Enabled toolsets: {enabled_toolsets_list}")
@ -569,8 +598,6 @@ def main(
model=model, model=model,
api_key=api_key, api_key=api_key,
max_iterations=max_turns, max_iterations=max_turns,
enabled_tools=enabled_tools_list,
disabled_tools=disabled_tools_list,
enabled_toolsets=enabled_toolsets_list, enabled_toolsets=enabled_toolsets_list,
disabled_toolsets=disabled_toolsets_list, disabled_toolsets=disabled_toolsets_list,
save_trajectories=save_trajectories save_trajectories=save_trajectories

View file

@ -17,14 +17,14 @@ export WEB_TOOLS_DEBUG=true
python run_agent.py \ python run_agent.py \
--query "$PROMPT" \ --query "$PROMPT" \
--max_turns 30 \ --max_turns 30 \
# --model claude-sonnet-4-20250514 \ --model claude-sonnet-4-20250514 \
# --base_url https://api.anthropic.com/v1/ \ --base_url https://api.anthropic.com/v1/ \
--model hermes-4-70B \
--base_url http://bore.pub:8292/v1 \
--api_key $ANTHROPIC_API_KEY \ --api_key $ANTHROPIC_API_KEY \
--save_trajectories --save_trajectories \
#--enabled_toolsets=vision_tools --enabled_toolsets=web
# --model claude-sonnet-4-20250514 \
#
#Possible Toolsets: #Possible Toolsets:
#web_tools #web_tools
#vision_tools #vision_tools

326
toolsets.py Normal file
View file

@ -0,0 +1,326 @@
#!/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
import json
# 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", "web_crawl"],
"includes": [] # No other toolsets included
},
"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 tools",
"tools": ["terminal"],
"includes": []
},
"moa": {
"description": "Advanced reasoning and problem-solving tools",
"tools": ["mixture_of_agents"],
"includes": []
},
# Scenario-specific toolsets
"debugging": {
"description": "Debugging and troubleshooting toolkit",
"tools": ["terminal"],
"includes": ["web"] # For searching error messages and solutions
},
"safe": {
"description": "Safe toolkit without terminal access",
"tools": ["mixture_of_agents"],
"includes": ["web", "vision", "creative"]
}
}
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
"""
# Return toolset definition
return TOOLSETS.get(name)
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()
# Check for cycles
if name in visited:
print(f"⚠️ Circular dependency detected in toolset '{name}'")
return []
visited.add(name)
# Get toolset definition
toolset = TOOLSETS.get(name)
if not toolset:
return []
# Collect direct tools
tools = set(toolset.get("tools", []))
# Recursively resolve included toolsets
for included_name in toolset.get("includes", []):
included_tools = resolve_toolset(included_name, visited.copy())
tools.update(included_tools)
return list(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 list(all_tools)
def get_all_toolsets() -> Dict[str, Dict[str, Any]]:
"""
Get all available toolsets with their definitions.
Returns:
Dict: All toolset definitions
"""
return TOOLSETS.copy()
def get_toolset_names() -> List[str]:
"""
Get names of all available toolsets (excluding aliases).
Returns:
List[str]: List of toolset names
"""
return list(TOOLSETS.keys())
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
"""
return name in TOOLSETS
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": len(toolset["includes"]) > 0
}
def print_toolset_tree(name: str, indent: int = 0) -> None:
"""
Print a tree view of a toolset and its composition.
Args:
name (str): Toolset name
indent (int): Current indentation level
"""
prefix = " " * indent
toolset = get_toolset(name)
if not toolset:
print(f"{prefix}❌ Unknown toolset: {name}")
return
# Print toolset name and description
print(f"{prefix}📦 {name}: {toolset['description']}")
# Print direct tools
if toolset["tools"]:
print(f"{prefix} 🔧 Tools: {', '.join(toolset['tools'])}")
# Print included toolsets
if toolset["includes"]:
print(f"{prefix} 📂 Includes:")
for included in toolset["includes"]:
print_toolset_tree(included, indent + 2)
if __name__ == "__main__":
"""
Demo and testing of the toolsets system
"""
print("🎯 Toolsets System Demo")
print("=" * 60)
# Show all available toolsets
print("\n📦 Available Toolsets:")
print("-" * 40)
for name, toolset in get_all_toolsets().items():
info = get_toolset_info(name)
composite = "📂" if info["is_composite"] else "🔧"
print(f"{composite} {name:20} - {toolset['description']}")
print(f" Tools: {len(info['resolved_tools'])} total")
# Demo toolset resolution
print("\n🔍 Toolset Resolution Examples:")
print("-" * 40)
examples = ["research", "development", "full_stack", "minimal", "safe"]
for name in examples:
tools = resolve_toolset(name)
print(f"\n{name}:")
print(f" Resolved to {len(tools)} tools: {', '.join(sorted(tools))}")
# Show toolset composition tree
print("\n🌳 Toolset Composition Tree:")
print("-" * 40)
print("\nExample: 'content_creation' toolset:")
print_toolset_tree("content_creation")
print("\nExample: 'full_stack' toolset:")
print_toolset_tree("full_stack")
# Demo multiple toolset resolution
print("\n🔗 Multiple Toolset Resolution:")
print("-" * 40)
combined = resolve_multiple_toolsets(["minimal", "vision", "reasoning"])
print(f"Combining ['minimal', 'vision', 'reasoning']:")
print(f" Result: {', '.join(sorted(combined))}")
# Demo custom toolset creation
print("\n Custom 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(f"Created 'my_custom' toolset:")
print(f" Description: {custom_info['description']}")
print(f" Resolved tools: {', '.join(custom_info['resolved_tools'])}")