mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Cleanup time!
This commit is contained in:
parent
9a19fe1f50
commit
70dd3a16dc
38 changed files with 150 additions and 351 deletions
|
|
@ -27,7 +27,7 @@ import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Dict, Any, Optional, Tuple
|
from typing import List, Dict, Any, Optional, Tuple
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from multiprocessing import Pool, Manager, Lock
|
from multiprocessing import Pool, Lock
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn, MofNCompleteColumn
|
from rich.progress import Progress, SpinnerColumn, BarColumn, TextColumn, TimeRemainingColumn, MofNCompleteColumn
|
||||||
|
|
@ -36,7 +36,6 @@ import fire
|
||||||
|
|
||||||
from run_agent import AIAgent
|
from run_agent import AIAgent
|
||||||
from toolset_distributions import (
|
from toolset_distributions import (
|
||||||
get_distribution,
|
|
||||||
list_distributions,
|
list_distributions,
|
||||||
sample_toolsets_from_distribution,
|
sample_toolsets_from_distribution,
|
||||||
validate_distribution
|
validate_distribution
|
||||||
|
|
@ -173,7 +172,7 @@ def _extract_tool_stats(messages: List[Dict[str, Any]]) -> Dict[str, Dict[str, i
|
||||||
if content_json.get("success") is False:
|
if content_json.get("success") is False:
|
||||||
is_success = False
|
is_success = False
|
||||||
|
|
||||||
except:
|
except (json.JSONDecodeError, ValueError, TypeError):
|
||||||
# If not JSON, check if content is empty or explicitly states an error
|
# If not JSON, check if content is empty or explicitly states an error
|
||||||
# Note: We avoid simple substring matching to prevent false positives
|
# Note: We avoid simple substring matching to prevent false positives
|
||||||
if not content:
|
if not content:
|
||||||
|
|
|
||||||
15
cli.py
15
cli.py
|
|
@ -39,16 +39,16 @@ from prompt_toolkit.layout.menus import CompletionsMenu
|
||||||
from prompt_toolkit.widgets import TextArea
|
from prompt_toolkit.widgets import TextArea
|
||||||
from prompt_toolkit.key_binding import KeyBindings
|
from prompt_toolkit.key_binding import KeyBindings
|
||||||
from prompt_toolkit.completion import Completer, Completion
|
from prompt_toolkit.completion import Completer, Completion
|
||||||
from prompt_toolkit.keys import Keys
|
|
||||||
from prompt_toolkit import print_formatted_text as _pt_print
|
from prompt_toolkit import print_formatted_text as _pt_print
|
||||||
from prompt_toolkit.formatted_text import ANSI as _PT_ANSI
|
from prompt_toolkit.formatted_text import ANSI as _PT_ANSI
|
||||||
import threading
|
import threading
|
||||||
import queue
|
import queue
|
||||||
import tempfile
|
|
||||||
|
|
||||||
|
|
||||||
# Load environment variables first
|
# Load environment variables first
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from hermes_constants import OPENROUTER_BASE_URL
|
||||||
|
|
||||||
env_path = Path(__file__).parent / '.env'
|
env_path = Path(__file__).parent / '.env'
|
||||||
if env_path.exists():
|
if env_path.exists():
|
||||||
load_dotenv(dotenv_path=env_path)
|
load_dotenv(dotenv_path=env_path)
|
||||||
|
|
@ -88,7 +88,7 @@ def load_cli_config() -> Dict[str, Any]:
|
||||||
defaults = {
|
defaults = {
|
||||||
"model": {
|
"model": {
|
||||||
"default": "anthropic/claude-opus-4.6",
|
"default": "anthropic/claude-opus-4.6",
|
||||||
"base_url": "https://openrouter.ai/api/v1",
|
"base_url": OPENROUTER_BASE_URL,
|
||||||
"provider": "auto",
|
"provider": "auto",
|
||||||
},
|
},
|
||||||
"terminal": {
|
"terminal": {
|
||||||
|
|
@ -262,20 +262,15 @@ def load_cli_config() -> Dict[str, Any]:
|
||||||
# Load configuration at module startup
|
# Load configuration at module startup
|
||||||
CLI_CONFIG = load_cli_config()
|
CLI_CONFIG = load_cli_config()
|
||||||
|
|
||||||
from rich.console import Console, Group
|
from rich.console import Console
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
from rich.text import Text
|
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
from rich.markdown import Markdown
|
|
||||||
from rich.columns import Columns
|
|
||||||
from rich.align import Align
|
|
||||||
from rich import box
|
|
||||||
|
|
||||||
import fire
|
import fire
|
||||||
|
|
||||||
# Import the agent and tool systems
|
# Import the agent and tool systems
|
||||||
from run_agent import AIAgent
|
from run_agent import AIAgent
|
||||||
from model_tools import get_tool_definitions, get_all_tool_names, get_toolset_for_tool, get_available_toolsets
|
from model_tools import get_tool_definitions, get_toolset_for_tool
|
||||||
from toolsets import get_all_toolsets, get_toolset_info, resolve_toolset, validate_toolset
|
from toolsets import get_all_toolsets, get_toolset_info, resolve_toolset, validate_toolset
|
||||||
|
|
||||||
# Cron job system for scheduled tasks
|
# Cron job system for scheduled tasks
|
||||||
|
|
|
||||||
|
|
@ -8,14 +8,13 @@ Routes messages to the appropriate destination based on:
|
||||||
- Local (always saved to files)
|
- Local (always saved to files)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, List, Optional, Any, Union
|
from typing import Dict, List, Optional, Any, Union
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from .config import Platform, GatewayConfig, HomeChannel
|
from .config import Platform, GatewayConfig
|
||||||
from .session import SessionSource
|
from .session import SessionSource
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1081,7 +1081,7 @@ class GatewayRunner:
|
||||||
try:
|
try:
|
||||||
msg = progress_queue.get_nowait()
|
msg = progress_queue.get_nowait()
|
||||||
await adapter.send(chat_id=source.chat_id, content=msg)
|
await adapter.send(chat_id=source.chat_id, content=msg)
|
||||||
except:
|
except Exception:
|
||||||
break
|
break
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import httpx
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from hermes_cli.config import get_hermes_home, get_config_path
|
from hermes_cli.config import get_hermes_home, get_config_path
|
||||||
|
from hermes_constants import OPENROUTER_BASE_URL
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import fcntl
|
import fcntl
|
||||||
|
|
@ -865,7 +866,7 @@ def _reset_config_provider() -> Path:
|
||||||
if isinstance(model, dict):
|
if isinstance(model, dict):
|
||||||
model["provider"] = "auto"
|
model["provider"] = "auto"
|
||||||
if "base_url" in model:
|
if "base_url" in model:
|
||||||
model["base_url"] = "https://openrouter.ai/api/v1"
|
model["base_url"] = OPENROUTER_BASE_URL
|
||||||
config_path.write_text(yaml.safe_dump(config, sort_keys=False))
|
config_path.write_text(yaml.safe_dump(config, sort_keys=False))
|
||||||
return config_path
|
return config_path
|
||||||
|
|
||||||
|
|
|
||||||
22
hermes_cli/colors.py
Normal file
22
hermes_cli/colors.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
"""Shared ANSI color utilities for Hermes CLI modules."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class Colors:
|
||||||
|
RESET = "\033[0m"
|
||||||
|
BOLD = "\033[1m"
|
||||||
|
DIM = "\033[2m"
|
||||||
|
RED = "\033[31m"
|
||||||
|
GREEN = "\033[32m"
|
||||||
|
YELLOW = "\033[33m"
|
||||||
|
BLUE = "\033[34m"
|
||||||
|
MAGENTA = "\033[35m"
|
||||||
|
CYAN = "\033[36m"
|
||||||
|
|
||||||
|
|
||||||
|
def color(text: str, *codes) -> str:
|
||||||
|
"""Apply color codes to text (only when output is a TTY)."""
|
||||||
|
if not sys.stdout.isatty():
|
||||||
|
return text
|
||||||
|
return "".join(codes) + text + Colors.RESET
|
||||||
|
|
@ -20,22 +20,7 @@ from typing import Dict, Any, Optional, List, Tuple
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
# ANSI colors
|
from hermes_cli.colors import Colors, color
|
||||||
class Colors:
|
|
||||||
RESET = "\033[0m"
|
|
||||||
BOLD = "\033[1m"
|
|
||||||
DIM = "\033[2m"
|
|
||||||
RED = "\033[31m"
|
|
||||||
GREEN = "\033[32m"
|
|
||||||
YELLOW = "\033[33m"
|
|
||||||
BLUE = "\033[34m"
|
|
||||||
MAGENTA = "\033[35m"
|
|
||||||
CYAN = "\033[36m"
|
|
||||||
|
|
||||||
def color(text: str, *codes) -> str:
|
|
||||||
if not sys.stdout.isatty():
|
|
||||||
return text
|
|
||||||
return "".join(codes) + text + Colors.RESET
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,14 @@ Cron subcommand for hermes CLI.
|
||||||
Handles: hermes cron [list|daemon|tick]
|
Handles: hermes cron [list|daemon|tick]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
||||||
sys.path.insert(0, str(PROJECT_ROOT))
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
# ANSI colors
|
from hermes_cli.colors import Colors, color
|
||||||
class Colors:
|
|
||||||
RESET = "\033[0m"
|
|
||||||
BOLD = "\033[1m"
|
|
||||||
DIM = "\033[2m"
|
|
||||||
RED = "\033[31m"
|
|
||||||
GREEN = "\033[32m"
|
|
||||||
YELLOW = "\033[33m"
|
|
||||||
CYAN = "\033[36m"
|
|
||||||
|
|
||||||
def color(text: str, *codes) -> str:
|
|
||||||
if not sys.stdout.isatty():
|
|
||||||
return text
|
|
||||||
return "".join(codes) + text + Colors.RESET
|
|
||||||
|
|
||||||
|
|
||||||
def cron_list(show_all: bool = False):
|
def cron_list(show_all: bool = False):
|
||||||
|
|
|
||||||
|
|
@ -23,20 +23,8 @@ if _env_path.exists():
|
||||||
# Also try project .env as fallback
|
# Also try project .env as fallback
|
||||||
load_dotenv(PROJECT_ROOT / ".env", override=False)
|
load_dotenv(PROJECT_ROOT / ".env", override=False)
|
||||||
|
|
||||||
# ANSI colors
|
from hermes_cli.colors import Colors, color
|
||||||
class Colors:
|
from hermes_constants import OPENROUTER_MODELS_URL
|
||||||
RESET = "\033[0m"
|
|
||||||
BOLD = "\033[1m"
|
|
||||||
DIM = "\033[2m"
|
|
||||||
RED = "\033[31m"
|
|
||||||
GREEN = "\033[32m"
|
|
||||||
YELLOW = "\033[33m"
|
|
||||||
CYAN = "\033[36m"
|
|
||||||
|
|
||||||
def color(text: str, *codes) -> str:
|
|
||||||
if not sys.stdout.isatty():
|
|
||||||
return text
|
|
||||||
return "".join(codes) + text + Colors.RESET
|
|
||||||
|
|
||||||
def check_ok(text: str, detail: str = ""):
|
def check_ok(text: str, detail: str = ""):
|
||||||
print(f" {color('✓', Colors.GREEN)} {text}" + (f" {color(detail, Colors.DIM)}" if detail else ""))
|
print(f" {color('✓', Colors.GREEN)} {text}" + (f" {color(detail, Colors.DIM)}" if detail else ""))
|
||||||
|
|
@ -314,7 +302,7 @@ def run_doctor(args):
|
||||||
try:
|
try:
|
||||||
import httpx
|
import httpx
|
||||||
response = httpx.get(
|
response = httpx.get(
|
||||||
"https://openrouter.ai/api/v1/models",
|
OPENROUTER_MODELS_URL,
|
||||||
headers={"Authorization": f"Bearer {openrouter_key}"},
|
headers={"Authorization": f"Bearer {openrouter_key}"},
|
||||||
timeout=10
|
timeout=10
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ if env_path.exists():
|
||||||
load_dotenv(dotenv_path=env_path)
|
load_dotenv(dotenv_path=env_path)
|
||||||
|
|
||||||
from hermes_cli import __version__
|
from hermes_cli import __version__
|
||||||
|
from hermes_constants import OPENROUTER_BASE_URL
|
||||||
|
|
||||||
|
|
||||||
def cmd_chat(args):
|
def cmd_chat(args):
|
||||||
|
|
@ -241,7 +242,7 @@ def _model_flow_openrouter(config, current_model=""):
|
||||||
model = cfg.get("model")
|
model = cfg.get("model")
|
||||||
if isinstance(model, dict):
|
if isinstance(model, dict):
|
||||||
model["provider"] = "openrouter"
|
model["provider"] = "openrouter"
|
||||||
model["base_url"] = "https://openrouter.ai/api/v1"
|
model["base_url"] = OPENROUTER_BASE_URL
|
||||||
save_config(cfg)
|
save_config(cfg)
|
||||||
deactivate_provider()
|
deactivate_provider()
|
||||||
print(f"Default model set to: {selected} (via OpenRouter)")
|
print(f"Default model set to: {selected} (via OpenRouter)")
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@ Usage:
|
||||||
hermes pairing clear-pending # Clear all expired/pending codes
|
hermes pairing clear-pending # Clear all expired/pending codes
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def pairing_command(args):
|
def pairing_command(args):
|
||||||
"""Handle hermes pairing subcommands."""
|
"""Handle hermes pairing subcommands."""
|
||||||
from gateway.pairing import PairingStore
|
from gateway.pairing import PairingStore
|
||||||
|
|
|
||||||
|
|
@ -26,23 +26,7 @@ from hermes_cli.config import (
|
||||||
ensure_hermes_home, DEFAULT_CONFIG
|
ensure_hermes_home, DEFAULT_CONFIG
|
||||||
)
|
)
|
||||||
|
|
||||||
# ANSI colors
|
from hermes_cli.colors import Colors, color
|
||||||
class Colors:
|
|
||||||
RESET = "\033[0m"
|
|
||||||
BOLD = "\033[1m"
|
|
||||||
DIM = "\033[2m"
|
|
||||||
RED = "\033[31m"
|
|
||||||
GREEN = "\033[32m"
|
|
||||||
YELLOW = "\033[33m"
|
|
||||||
BLUE = "\033[34m"
|
|
||||||
MAGENTA = "\033[35m"
|
|
||||||
CYAN = "\033[36m"
|
|
||||||
|
|
||||||
def color(text: str, *codes) -> str:
|
|
||||||
"""Apply color codes to text."""
|
|
||||||
if not sys.stdout.isatty():
|
|
||||||
return text
|
|
||||||
return "".join(codes) + text + Colors.RESET
|
|
||||||
|
|
||||||
def print_header(title: str):
|
def print_header(title: str):
|
||||||
"""Print a section header."""
|
"""Print a section header."""
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ handler are thin wrappers that parse args and delegate.
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,20 +11,8 @@ from pathlib import Path
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
||||||
|
|
||||||
# ANSI colors
|
from hermes_cli.colors import Colors, color
|
||||||
class Colors:
|
from hermes_constants import OPENROUTER_MODELS_URL
|
||||||
RESET = "\033[0m"
|
|
||||||
BOLD = "\033[1m"
|
|
||||||
DIM = "\033[2m"
|
|
||||||
RED = "\033[31m"
|
|
||||||
GREEN = "\033[32m"
|
|
||||||
YELLOW = "\033[33m"
|
|
||||||
CYAN = "\033[36m"
|
|
||||||
|
|
||||||
def color(text: str, *codes) -> str:
|
|
||||||
if not sys.stdout.isatty():
|
|
||||||
return text
|
|
||||||
return "".join(codes) + text + Colors.RESET
|
|
||||||
|
|
||||||
def check_mark(ok: bool) -> str:
|
def check_mark(ok: bool) -> str:
|
||||||
if ok:
|
if ok:
|
||||||
|
|
@ -232,7 +220,7 @@ def show_status(args):
|
||||||
jobs = data.get("jobs", [])
|
jobs = data.get("jobs", [])
|
||||||
enabled_jobs = [j for j in jobs if j.get("enabled", True)]
|
enabled_jobs = [j for j in jobs if j.get("enabled", True)]
|
||||||
print(f" Jobs: {len(enabled_jobs)} active, {len(jobs)} total")
|
print(f" Jobs: {len(enabled_jobs)} active, {len(jobs)} total")
|
||||||
except:
|
except Exception:
|
||||||
print(f" Jobs: (error reading jobs file)")
|
print(f" Jobs: (error reading jobs file)")
|
||||||
else:
|
else:
|
||||||
print(f" Jobs: 0")
|
print(f" Jobs: 0")
|
||||||
|
|
@ -250,7 +238,7 @@ def show_status(args):
|
||||||
with open(sessions_file) as f:
|
with open(sessions_file) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
print(f" Active: {len(data)} session(s)")
|
print(f" Active: {len(data)} session(s)")
|
||||||
except:
|
except Exception:
|
||||||
print(f" Active: (error reading sessions file)")
|
print(f" Active: (error reading sessions file)")
|
||||||
else:
|
else:
|
||||||
print(f" Active: 0")
|
print(f" Active: 0")
|
||||||
|
|
@ -268,7 +256,7 @@ def show_status(args):
|
||||||
try:
|
try:
|
||||||
import httpx
|
import httpx
|
||||||
response = httpx.get(
|
response = httpx.get(
|
||||||
"https://openrouter.ai/api/v1/models",
|
OPENROUTER_MODELS_URL,
|
||||||
headers={"Authorization": f"Bearer {openrouter_key}"},
|
headers={"Authorization": f"Bearer {openrouter_key}"},
|
||||||
timeout=10
|
timeout=10
|
||||||
)
|
)
|
||||||
|
|
@ -288,7 +276,7 @@ def show_status(args):
|
||||||
port_in_use = result == 0
|
port_in_use = result == 0
|
||||||
# This is informational, not necessarily bad
|
# This is informational, not necessarily bad
|
||||||
print(f" Port 18789: {'in use' if port_in_use else 'available'}")
|
print(f" Port 18789: {'in use' if port_in_use else 'available'}")
|
||||||
except:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
|
||||||
|
|
@ -13,23 +13,7 @@ import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
# ANSI colors
|
from hermes_cli.colors import Colors, color
|
||||||
class Colors:
|
|
||||||
RESET = "\033[0m"
|
|
||||||
BOLD = "\033[1m"
|
|
||||||
DIM = "\033[2m"
|
|
||||||
RED = "\033[31m"
|
|
||||||
GREEN = "\033[32m"
|
|
||||||
YELLOW = "\033[33m"
|
|
||||||
BLUE = "\033[34m"
|
|
||||||
MAGENTA = "\033[35m"
|
|
||||||
CYAN = "\033[36m"
|
|
||||||
|
|
||||||
def color(text: str, *codes) -> str:
|
|
||||||
"""Apply color codes to text (only in TTY)."""
|
|
||||||
if not sys.stdout.isatty():
|
|
||||||
return text
|
|
||||||
return "".join(codes) + text + Colors.RESET
|
|
||||||
|
|
||||||
def log_info(msg: str):
|
def log_info(msg: str):
|
||||||
print(f"{color('→', Colors.CYAN)} {msg}")
|
print(f"{color('→', Colors.CYAN)} {msg}")
|
||||||
|
|
|
||||||
9
hermes_constants.py
Normal file
9
hermes_constants.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
"""Shared constants for Hermes Agent.
|
||||||
|
|
||||||
|
Import-safe module with no dependencies — can be imported from anywhere
|
||||||
|
without risk of circular imports.
|
||||||
|
"""
|
||||||
|
|
||||||
|
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
||||||
|
OPENROUTER_MODELS_URL = f"{OPENROUTER_BASE_URL}/models"
|
||||||
|
OPENROUTER_CHAT_URL = f"{OPENROUTER_BASE_URL}/chat/completions"
|
||||||
|
|
@ -31,19 +31,17 @@ import asyncio
|
||||||
import os
|
import os
|
||||||
from typing import Dict, Any, List, Optional, Tuple
|
from typing import Dict, Any, List, Optional, Tuple
|
||||||
|
|
||||||
from tools.web_tools import web_search_tool, web_extract_tool, web_crawl_tool, check_firecrawl_api_key
|
from tools.web_tools import web_search_tool, web_extract_tool, check_firecrawl_api_key
|
||||||
from tools.terminal_tool import terminal_tool, check_terminal_requirements, TERMINAL_TOOL_DESCRIPTION, cleanup_vm
|
from tools.terminal_tool import terminal_tool, check_terminal_requirements, TERMINAL_TOOL_DESCRIPTION
|
||||||
# File manipulation tools (read, write, patch, search)
|
# File manipulation tools (read, write, patch, search)
|
||||||
from tools.file_tools import read_file_tool, write_file_tool, patch_tool, search_tool
|
from tools.file_tools import read_file_tool, write_file_tool, patch_tool, search_tool
|
||||||
from tools import check_file_requirements
|
from tools import check_file_requirements
|
||||||
# Hecate/MorphCloud terminal tool (cloud VMs) - available as alternative backend
|
|
||||||
from tools.terminal_hecate import terminal_hecate_tool, check_hecate_requirements, TERMINAL_HECATE_DESCRIPTION
|
|
||||||
from tools.vision_tools import vision_analyze_tool, check_vision_requirements
|
from tools.vision_tools import vision_analyze_tool, check_vision_requirements
|
||||||
from tools.mixture_of_agents_tool import mixture_of_agents_tool, check_moa_requirements
|
from tools.mixture_of_agents_tool import mixture_of_agents_tool, check_moa_requirements
|
||||||
from tools.image_generation_tool import image_generate_tool, check_image_generation_requirements
|
from tools.image_generation_tool import image_generate_tool, check_image_generation_requirements
|
||||||
from tools.skills_tool import skills_list, skill_view, check_skills_requirements, SKILLS_TOOL_DESCRIPTION
|
from tools.skills_tool import skills_list, skill_view, check_skills_requirements
|
||||||
# Agent-managed skill creation/editing
|
# Agent-managed skill creation/editing
|
||||||
from tools.skill_manager_tool import skill_manage, check_skill_manage_requirements, SKILL_MANAGE_SCHEMA
|
from tools.skill_manager_tool import skill_manage, SKILL_MANAGE_SCHEMA
|
||||||
# RL Training tools (Tinker-Atropos)
|
# RL Training tools (Tinker-Atropos)
|
||||||
from tools.rl_training_tool import (
|
from tools.rl_training_tool import (
|
||||||
rl_list_environments,
|
rl_list_environments,
|
||||||
|
|
@ -64,7 +62,6 @@ from tools.cronjob_tools import (
|
||||||
list_cronjobs,
|
list_cronjobs,
|
||||||
remove_cronjob,
|
remove_cronjob,
|
||||||
check_cronjob_requirements,
|
check_cronjob_requirements,
|
||||||
get_cronjob_tool_definitions,
|
|
||||||
SCHEDULE_CRONJOB_SCHEMA,
|
SCHEDULE_CRONJOB_SCHEMA,
|
||||||
LIST_CRONJOBS_SCHEMA,
|
LIST_CRONJOBS_SCHEMA,
|
||||||
REMOVE_CRONJOB_SCHEMA
|
REMOVE_CRONJOB_SCHEMA
|
||||||
|
|
@ -99,11 +96,7 @@ from tools.clarify_tool import clarify_tool, check_clarify_requirements, CLARIFY
|
||||||
from tools.code_execution_tool import execute_code, check_sandbox_requirements, EXECUTE_CODE_SCHEMA
|
from tools.code_execution_tool import execute_code, check_sandbox_requirements, EXECUTE_CODE_SCHEMA
|
||||||
# Subagent delegation
|
# Subagent delegation
|
||||||
from tools.delegate_tool import delegate_task, check_delegate_requirements, DELEGATE_TASK_SCHEMA
|
from tools.delegate_tool import delegate_task, check_delegate_requirements, DELEGATE_TASK_SCHEMA
|
||||||
from toolsets import (
|
from toolsets import resolve_toolset, validate_toolset
|
||||||
get_toolset, resolve_toolset, resolve_multiple_toolsets,
|
|
||||||
get_all_toolsets, get_toolset_names, validate_toolset,
|
|
||||||
get_toolset_info, print_toolset_tree
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -262,43 +255,6 @@ def check_tool_availability(quiet: bool = False) -> Tuple[List[str], List[Dict[s
|
||||||
return available, unavailable
|
return available, unavailable
|
||||||
|
|
||||||
|
|
||||||
def print_tool_availability_warnings(unavailable: List[Dict[str, Any]], prefix: str = ""):
|
|
||||||
"""Print warnings about unavailable tools."""
|
|
||||||
if not unavailable:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Filter to only those missing API keys (not system dependencies)
|
|
||||||
api_key_missing = [u for u in unavailable if u["missing_vars"]]
|
|
||||||
|
|
||||||
if api_key_missing:
|
|
||||||
print(f"{prefix}⚠️ Some tools are disabled due to missing API keys:")
|
|
||||||
for item in api_key_missing:
|
|
||||||
vars_str = ", ".join(item["missing_vars"])
|
|
||||||
print(f"{prefix} • {item['name']}: missing {vars_str}")
|
|
||||||
if item["setup_url"]:
|
|
||||||
print(f"{prefix} Get key at: {item['setup_url']}")
|
|
||||||
print(f"{prefix} Run 'hermes setup' to configure API keys")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def get_tool_availability_summary() -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Get a summary of tool availability for display in status/doctor commands.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict with 'available' and 'unavailable' lists of tool info
|
|
||||||
"""
|
|
||||||
available, unavailable = check_tool_availability()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"available": [
|
|
||||||
{"id": tid, "name": TOOLSET_REQUIREMENTS[tid]["name"], "tools": TOOLSET_REQUIREMENTS[tid]["tools"]}
|
|
||||||
for tid in available
|
|
||||||
],
|
|
||||||
"unavailable": unavailable,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_web_tool_definitions() -> List[Dict[str, Any]]:
|
def get_web_tool_definitions() -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Get tool definitions for web tools in OpenAI's expected format.
|
Get tool definitions for web tools in OpenAI's expected format.
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ hermes = "hermes_cli.main:main"
|
||||||
hermes-agent = "run_agent:main"
|
hermes-agent = "run_agent:main"
|
||||||
|
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
py-modules = ["run_agent", "model_tools", "toolsets", "batch_runner", "trajectory_compressor", "toolset_distributions", "cli"]
|
py-modules = ["run_agent", "model_tools", "toolsets", "batch_runner", "trajectory_compressor", "toolset_distributions", "cli", "hermes_constants"]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
include = ["tools", "hermes_cli", "gateway", "cron"]
|
include = ["tools", "hermes_cli", "gateway", "cron"]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
# NOTE: This file is maintained for convenience only.
|
||||||
|
# The canonical dependency list is in pyproject.toml.
|
||||||
|
# Preferred install: pip install -e ".[all]"
|
||||||
|
|
||||||
# Core dependencies
|
# Core dependencies
|
||||||
openai
|
openai
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
|
@ -10,6 +14,7 @@ pyyaml
|
||||||
requests
|
requests
|
||||||
jinja2
|
jinja2
|
||||||
pydantic>=2.0
|
pydantic>=2.0
|
||||||
|
PyJWT[crypto]
|
||||||
|
|
||||||
# Web tools
|
# Web tools
|
||||||
firecrawl-py
|
firecrawl-py
|
||||||
|
|
@ -23,27 +28,13 @@ litellm>=1.75.5
|
||||||
typer
|
typer
|
||||||
platformdirs
|
platformdirs
|
||||||
|
|
||||||
# Optional: For Docker backend (recommended)
|
|
||||||
# Requires Docker installed and user in 'docker' group
|
|
||||||
|
|
||||||
# Optional: For Modal backend (cloud execution)
|
|
||||||
# swe-rex[modal]>=1.4.0 # Includes modal + boto3 + swe-rex runtime
|
|
||||||
|
|
||||||
# Text-to-speech (Edge TTS is free, no API key needed)
|
# Text-to-speech (Edge TTS is free, no API key needed)
|
||||||
edge-tts
|
edge-tts
|
||||||
|
|
||||||
# Optional: Premium TTS providers
|
|
||||||
# elevenlabs # Uncomment if using ElevenLabs TTS (needs ELEVENLABS_API_KEY)
|
|
||||||
|
|
||||||
# Optional: For cron expression parsing (cronjob scheduling)
|
# Optional: For cron expression parsing (cronjob scheduling)
|
||||||
croniter
|
croniter
|
||||||
|
|
||||||
# Optional: For messaging platform integrations (gateway)
|
# Optional: For messaging platform integrations (gateway)
|
||||||
# Telegram
|
|
||||||
python-telegram-bot>=20.0
|
python-telegram-bot>=20.0
|
||||||
|
|
||||||
# Discord
|
|
||||||
discord.py>=2.0
|
discord.py>=2.0
|
||||||
|
|
||||||
# WhatsApp bridge communication + general async HTTP (used by gateway)
|
|
||||||
aiohttp>=3.9.0
|
aiohttp>=3.9.0
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,10 @@ from tools.rl_training_tool import check_rl_api_keys, get_missing_keys
|
||||||
# Config Loading
|
# Config Loading
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
from hermes_constants import OPENROUTER_BASE_URL
|
||||||
|
|
||||||
DEFAULT_MODEL = "anthropic/claude-opus-4.5"
|
DEFAULT_MODEL = "anthropic/claude-opus-4.5"
|
||||||
DEFAULT_BASE_URL = "https://openrouter.ai/api/v1"
|
DEFAULT_BASE_URL = OPENROUTER_BASE_URL
|
||||||
|
|
||||||
|
|
||||||
def load_hermes_config() -> dict:
|
def load_hermes_config() -> dict:
|
||||||
|
|
|
||||||
82
run_agent.py
82
run_agent.py
|
|
@ -25,6 +25,7 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import threading
|
import threading
|
||||||
|
|
@ -54,6 +55,8 @@ from tools.browser_tool import cleanup_browser
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
from hermes_constants import OPENROUTER_BASE_URL, OPENROUTER_MODELS_URL
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Default Agent Identity & Platform Hints
|
# Default Agent Identity & Platform Hints
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
@ -133,7 +136,7 @@ def fetch_model_metadata(force_refresh: bool = False) -> Dict[str, Dict[str, Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
"https://openrouter.ai/api/v1/models",
|
OPENROUTER_MODELS_URL,
|
||||||
timeout=10
|
timeout=10
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
@ -282,7 +285,7 @@ class ContextCompressor:
|
||||||
api_key = os.getenv("OPENROUTER_API_KEY", "")
|
api_key = os.getenv("OPENROUTER_API_KEY", "")
|
||||||
self.client = OpenAI(
|
self.client = OpenAI(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url="https://openrouter.ai/api/v1"
|
base_url=OPENROUTER_BASE_URL
|
||||||
) if api_key else None
|
) if api_key else None
|
||||||
|
|
||||||
def update_from_response(self, usage: Dict[str, Any]):
|
def update_from_response(self, usage: Dict[str, Any]):
|
||||||
|
|
@ -600,7 +603,6 @@ def build_skills_system_prompt() -> str:
|
||||||
str: The skills system prompt section, or empty string if no skills found.
|
str: The skills system prompt section, or empty string if no skills found.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
hermes_home = Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
|
||||||
|
|
@ -1093,8 +1095,8 @@ class AIAgent:
|
||||||
Args:
|
Args:
|
||||||
base_url (str): Base URL for the model API (optional)
|
base_url (str): Base URL for the model API (optional)
|
||||||
api_key (str): API key for authentication (optional, uses env var if not provided)
|
api_key (str): API key for authentication (optional, uses env var if not provided)
|
||||||
model (str): Model name to use (default: "gpt-4")
|
model (str): Model name to use (default: "anthropic/claude-opus-4.6")
|
||||||
max_iterations (int): Maximum number of tool calling iterations (default: 10)
|
max_iterations (int): Maximum number of tool calling iterations (default: 60)
|
||||||
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_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)
|
||||||
|
|
@ -1102,7 +1104,7 @@ class AIAgent:
|
||||||
verbose_logging (bool): Enable verbose logging for debugging (default: False)
|
verbose_logging (bool): Enable verbose logging for debugging (default: False)
|
||||||
quiet_mode (bool): Suppress progress output for clean CLI experience (default: False)
|
quiet_mode (bool): Suppress progress output for clean CLI experience (default: False)
|
||||||
ephemeral_system_prompt (str): System prompt used during agent execution but NOT saved to trajectories (optional)
|
ephemeral_system_prompt (str): System prompt used during agent execution but NOT saved to trajectories (optional)
|
||||||
log_prefix_chars (int): Number of characters to show in log previews for tool calls/responses (default: 20)
|
log_prefix_chars (int): Number of characters to show in log previews for tool calls/responses (default: 100)
|
||||||
log_prefix (str): Prefix to add to all log messages for identification in parallel processing (default: "")
|
log_prefix (str): Prefix to add to all log messages for identification in parallel processing (default: "")
|
||||||
providers_allowed (List[str]): OpenRouter providers to allow (optional)
|
providers_allowed (List[str]): OpenRouter providers to allow (optional)
|
||||||
providers_ignored (List[str]): OpenRouter providers to ignore (optional)
|
providers_ignored (List[str]): OpenRouter providers to ignore (optional)
|
||||||
|
|
@ -1137,7 +1139,7 @@ class AIAgent:
|
||||||
self.log_prefix = f"{log_prefix} " if log_prefix else ""
|
self.log_prefix = f"{log_prefix} " if log_prefix else ""
|
||||||
# Store effective base URL for feature detection (prompt caching, reasoning, etc.)
|
# Store effective base URL for feature detection (prompt caching, reasoning, etc.)
|
||||||
# When no base_url is provided, the client defaults to OpenRouter, so reflect that here.
|
# When no base_url is provided, the client defaults to OpenRouter, so reflect that here.
|
||||||
self.base_url = base_url or "https://openrouter.ai/api/v1"
|
self.base_url = base_url or OPENROUTER_BASE_URL
|
||||||
self.tool_progress_callback = tool_progress_callback
|
self.tool_progress_callback = tool_progress_callback
|
||||||
self.clarify_callback = clarify_callback
|
self.clarify_callback = clarify_callback
|
||||||
self._last_reported_tool = None # Track for "new tool" mode
|
self._last_reported_tool = None # Track for "new tool" mode
|
||||||
|
|
@ -1215,7 +1217,7 @@ class AIAgent:
|
||||||
if base_url:
|
if base_url:
|
||||||
client_kwargs["base_url"] = base_url
|
client_kwargs["base_url"] = base_url
|
||||||
else:
|
else:
|
||||||
client_kwargs["base_url"] = "https://openrouter.ai/api/v1"
|
client_kwargs["base_url"] = OPENROUTER_BASE_URL
|
||||||
|
|
||||||
# Handle API key - OpenRouter is the primary provider
|
# Handle API key - OpenRouter is the primary provider
|
||||||
if api_key:
|
if api_key:
|
||||||
|
|
@ -1636,7 +1638,6 @@ class AIAgent:
|
||||||
if not content:
|
if not content:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
import re
|
|
||||||
# Remove all <think>...</think> blocks (including nested ones, non-greedy)
|
# Remove all <think>...</think> blocks (including nested ones, non-greedy)
|
||||||
cleaned = re.sub(r'<think>.*?</think>', '', content, flags=re.DOTALL)
|
cleaned = re.sub(r'<think>.*?</think>', '', content, flags=re.DOTALL)
|
||||||
|
|
||||||
|
|
@ -1686,6 +1687,19 @@ class AIAgent:
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _cleanup_task_resources(self, task_id: str) -> None:
|
||||||
|
"""Clean up VM and browser resources for a given task."""
|
||||||
|
try:
|
||||||
|
cleanup_vm(task_id)
|
||||||
|
except Exception as e:
|
||||||
|
if self.verbose_logging:
|
||||||
|
logging.warning(f"Failed to cleanup VM for task {task_id}: {e}")
|
||||||
|
try:
|
||||||
|
cleanup_browser(task_id)
|
||||||
|
except Exception as e:
|
||||||
|
if self.verbose_logging:
|
||||||
|
logging.warning(f"Failed to cleanup browser for task {task_id}: {e}")
|
||||||
|
|
||||||
def _get_messages_up_to_last_assistant(self, messages: List[Dict]) -> List[Dict]:
|
def _get_messages_up_to_last_assistant(self, messages: List[Dict]) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Get messages up to (but not including) the last assistant turn.
|
Get messages up to (but not including) the last assistant turn.
|
||||||
|
|
@ -2331,7 +2345,6 @@ class AIAgent:
|
||||||
Dict: Complete conversation result with final response and message history
|
Dict: Complete conversation result with final response and message history
|
||||||
"""
|
"""
|
||||||
# Generate unique task_id if not provided to isolate VMs between concurrent tasks
|
# Generate unique task_id if not provided to isolate VMs between concurrent tasks
|
||||||
import uuid
|
|
||||||
effective_task_id = task_id or str(uuid.uuid4())
|
effective_task_id = task_id or str(uuid.uuid4())
|
||||||
|
|
||||||
# Reset retry counters at the start of each conversation to prevent state leakage
|
# Reset retry counters at the start of each conversation to prevent state leakage
|
||||||
|
|
@ -2628,17 +2641,7 @@ class AIAgent:
|
||||||
print(f"{self.log_prefix} ⏪ Rolling back to last complete assistant turn")
|
print(f"{self.log_prefix} ⏪ Rolling back to last complete assistant turn")
|
||||||
rolled_back_messages = self._get_messages_up_to_last_assistant(messages)
|
rolled_back_messages = self._get_messages_up_to_last_assistant(messages)
|
||||||
|
|
||||||
# Clean up VM and browser
|
self._cleanup_task_resources(effective_task_id)
|
||||||
try:
|
|
||||||
cleanup_vm(effective_task_id)
|
|
||||||
except Exception as e:
|
|
||||||
if self.verbose_logging:
|
|
||||||
logging.warning(f"Failed to cleanup VM for task {effective_task_id}: {e}")
|
|
||||||
try:
|
|
||||||
cleanup_browser(effective_task_id)
|
|
||||||
except Exception as e:
|
|
||||||
if self.verbose_logging:
|
|
||||||
logging.warning(f"Failed to cleanup browser for task {effective_task_id}: {e}")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"final_response": None,
|
"final_response": None,
|
||||||
|
|
@ -2846,15 +2849,7 @@ class AIAgent:
|
||||||
self._incomplete_scratchpad_retries = 0
|
self._incomplete_scratchpad_retries = 0
|
||||||
|
|
||||||
rolled_back_messages = self._get_messages_up_to_last_assistant(messages)
|
rolled_back_messages = self._get_messages_up_to_last_assistant(messages)
|
||||||
|
self._cleanup_task_resources(effective_task_id)
|
||||||
try:
|
|
||||||
cleanup_vm(effective_task_id)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
cleanup_browser(effective_task_id)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"final_response": None,
|
"final_response": None,
|
||||||
|
|
@ -3247,18 +3242,7 @@ class AIAgent:
|
||||||
self._empty_content_retries = 0 # Reset for next conversation
|
self._empty_content_retries = 0 # Reset for next conversation
|
||||||
|
|
||||||
rolled_back_messages = self._get_messages_up_to_last_assistant(messages)
|
rolled_back_messages = self._get_messages_up_to_last_assistant(messages)
|
||||||
|
self._cleanup_task_resources(effective_task_id)
|
||||||
# Clean up VM and browser
|
|
||||||
try:
|
|
||||||
cleanup_vm(effective_task_id)
|
|
||||||
except Exception as e:
|
|
||||||
if self.verbose_logging:
|
|
||||||
logging.warning(f"Failed to cleanup VM for task {effective_task_id}: {e}")
|
|
||||||
try:
|
|
||||||
cleanup_browser(effective_task_id)
|
|
||||||
except Exception as e:
|
|
||||||
if self.verbose_logging:
|
|
||||||
logging.warning(f"Failed to cleanup browser for task {effective_task_id}: {e}")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"final_response": None,
|
"final_response": None,
|
||||||
|
|
@ -3365,7 +3349,6 @@ class AIAgent:
|
||||||
final_response = summary_response.choices[0].message.content
|
final_response = summary_response.choices[0].message.content
|
||||||
# Strip think blocks from final response
|
# Strip think blocks from final response
|
||||||
if "<think>" in final_response:
|
if "<think>" in final_response:
|
||||||
import re
|
|
||||||
final_response = re.sub(r'<think>.*?</think>\s*', '', final_response, flags=re.DOTALL).strip()
|
final_response = re.sub(r'<think>.*?</think>\s*', '', final_response, flags=re.DOTALL).strip()
|
||||||
|
|
||||||
# Add to messages for session continuity
|
# Add to messages for session continuity
|
||||||
|
|
@ -3384,17 +3367,7 @@ class AIAgent:
|
||||||
self._save_trajectory(messages, user_message, completed)
|
self._save_trajectory(messages, user_message, completed)
|
||||||
|
|
||||||
# Clean up VM and browser for this task after conversation completes
|
# Clean up VM and browser for this task after conversation completes
|
||||||
try:
|
self._cleanup_task_resources(effective_task_id)
|
||||||
cleanup_vm(effective_task_id)
|
|
||||||
except Exception as e:
|
|
||||||
if self.verbose_logging:
|
|
||||||
logging.warning(f"Failed to cleanup VM for task {effective_task_id}: {e}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
cleanup_browser(effective_task_id)
|
|
||||||
except Exception as e:
|
|
||||||
if self.verbose_logging:
|
|
||||||
logging.warning(f"Failed to cleanup browser for task {effective_task_id}: {e}")
|
|
||||||
|
|
||||||
# Update session messages and save session log
|
# Update session messages and save session log
|
||||||
self._session_messages = messages
|
self._session_messages = messages
|
||||||
|
|
@ -3644,7 +3617,6 @@ def main(
|
||||||
|
|
||||||
# Save sample trajectory to UUID-named file if requested
|
# Save sample trajectory to UUID-named file if requested
|
||||||
if save_sample:
|
if save_sample:
|
||||||
import uuid
|
|
||||||
sample_id = str(uuid.uuid4())[:8]
|
sample_id = str(uuid.uuid4())[:8]
|
||||||
sample_filename = f"sample_{sample_id}.json"
|
sample_filename = f"sample_{sample_id}.json"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ def _count_tokens_for_entry(entry: Dict) -> Tuple[Dict, int]:
|
||||||
if value:
|
if value:
|
||||||
try:
|
try:
|
||||||
total += len(_TOKENIZER.encode(value))
|
total += len(_TOKENIZER.encode(value))
|
||||||
except:
|
except Exception:
|
||||||
# Fallback to character estimate
|
# Fallback to character estimate
|
||||||
total += len(value) // 4
|
total += len(value) // 4
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import signal
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Dict, Any
|
from typing import List, Dict, Any
|
||||||
import traceback
|
import traceback
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ Run with: python -m pytest tests/test_delegate.py -v
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ Create a markdown summary that captures all key information in a well-organized,
|
||||||
max_tokens=4000
|
max_tokens=4000
|
||||||
)
|
)
|
||||||
print(f"✅ SUCCESS")
|
print(f"✅ SUCCESS")
|
||||||
except:
|
except Exception:
|
||||||
print(f"❌ FAILED")
|
print(f"❌ FAILED")
|
||||||
|
|
||||||
await asyncio.sleep(0.5)
|
await asyncio.sleep(0.5)
|
||||||
|
|
@ -101,7 +101,7 @@ Create a markdown summary that captures all key information in a well-organized,
|
||||||
max_tokens=4000
|
max_tokens=4000
|
||||||
)
|
)
|
||||||
print(f"✅ SUCCESS")
|
print(f"✅ SUCCESS")
|
||||||
except:
|
except Exception:
|
||||||
print(f"❌ FAILED")
|
print(f"❌ FAILED")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import sys
|
||||||
import os
|
import os
|
||||||
import argparse
|
import argparse
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import List, Dict, Any
|
from typing import List
|
||||||
|
|
||||||
# Import the web tools to test (updated path after moving tools/)
|
# Import the web tools to test (updated path after moving tools/)
|
||||||
from tools.web_tools import (
|
from tools.web_tools import (
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ import time
|
||||||
import requests
|
import requests
|
||||||
from typing import Dict, Any, Optional, List
|
from typing import Dict, Any, Optional, List
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from hermes_constants import OPENROUTER_CHAT_URL
|
||||||
|
|
||||||
# Try to import httpx for async LLM calls
|
# Try to import httpx for async LLM calls
|
||||||
try:
|
try:
|
||||||
|
|
@ -821,7 +822,7 @@ Provide a concise summary focused on interactive elements and key content."""
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"https://openrouter.ai/api/v1/chat/completions",
|
OPENROUTER_CHAT_URL,
|
||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {api_key}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
|
|
@ -1324,7 +1325,7 @@ Focus on answering the user's specific question."""
|
||||||
async def analyze_screenshot():
|
async def analyze_screenshot():
|
||||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
"https://openrouter.ai/api/v1/chat/completions",
|
OPENROUTER_CHAT_URL,
|
||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {api_key}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
|
|
@ -1374,7 +1375,7 @@ Focus on answering the user's specific question."""
|
||||||
else:
|
else:
|
||||||
# Fallback: use synchronous requests
|
# Fallback: use synchronous requests
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
"https://openrouter.ai/api/v1/chat/completions",
|
OPENROUTER_CHAT_URL,
|
||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {api_key}",
|
"Authorization": f"Bearer {api_key}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
|
|
|
||||||
|
|
@ -345,7 +345,7 @@ def execute_code(
|
||||||
|
|
||||||
# --- Set up temp directory with hermes_tools.py and script.py ---
|
# --- Set up temp directory with hermes_tools.py and script.py ---
|
||||||
tmpdir = tempfile.mkdtemp(prefix="hermes_sandbox_")
|
tmpdir = tempfile.mkdtemp(prefix="hermes_sandbox_")
|
||||||
sock_path = f"/tmp/hermes_rpc_{uuid.uuid4().hex}.sock"
|
sock_path = os.path.join(tempfile.gettempdir(), f"hermes_rpc_{uuid.uuid4().hex}.sock")
|
||||||
|
|
||||||
tool_call_log: list = []
|
tool_call_log: list = []
|
||||||
tool_call_counter = [0] # mutable so the RPC thread can increment
|
tool_call_counter = [0] # mutable so the RPC thread can increment
|
||||||
|
|
|
||||||
|
|
@ -446,33 +446,3 @@ def _map_normalized_positions(original: str, normalized: str,
|
||||||
original_matches.append((orig_start, min(orig_end, len(original))))
|
original_matches.append((orig_start, min(orig_end, len(original))))
|
||||||
|
|
||||||
return original_matches
|
return original_matches
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Utility Functions
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
def find_best_match(content: str, pattern: str) -> Optional[Tuple[int, int, str]]:
|
|
||||||
"""
|
|
||||||
Find the best match for a pattern and return the strategy name.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple of (start, end, strategy_name) or None if no match
|
|
||||||
"""
|
|
||||||
strategies = [
|
|
||||||
("exact", _strategy_exact),
|
|
||||||
("line_trimmed", _strategy_line_trimmed),
|
|
||||||
("whitespace_normalized", _strategy_whitespace_normalized),
|
|
||||||
("indentation_flexible", _strategy_indentation_flexible),
|
|
||||||
("escape_normalized", _strategy_escape_normalized),
|
|
||||||
("trimmed_boundary", _strategy_trimmed_boundary),
|
|
||||||
("block_anchor", _strategy_block_anchor),
|
|
||||||
("context_aware", _strategy_context_aware),
|
|
||||||
]
|
|
||||||
|
|
||||||
for strategy_name, strategy_fn in strategies:
|
|
||||||
matches = strategy_fn(content, pattern)
|
|
||||||
if matches:
|
|
||||||
return (matches[0][0], matches[0][1], strategy_name)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,8 @@ import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
|
from hermes_constants import OPENROUTER_BASE_URL
|
||||||
|
|
||||||
# Initialize OpenRouter API client lazily (only when needed)
|
|
||||||
_openrouter_client = None
|
_openrouter_client = None
|
||||||
|
|
||||||
def _get_openrouter_client():
|
def _get_openrouter_client():
|
||||||
|
|
@ -66,7 +66,7 @@ def _get_openrouter_client():
|
||||||
raise ValueError("OPENROUTER_API_KEY environment variable not set")
|
raise ValueError("OPENROUTER_API_KEY environment variable not set")
|
||||||
_openrouter_client = AsyncOpenAI(
|
_openrouter_client = AsyncOpenAI(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url="https://openrouter.ai/api/v1"
|
base_url=OPENROUTER_BASE_URL
|
||||||
)
|
)
|
||||||
return _openrouter_client
|
return _openrouter_client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import logging
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
|
from hermes_constants import OPENROUTER_BASE_URL
|
||||||
|
|
||||||
SUMMARIZER_MODEL = "google/gemini-3-flash-preview"
|
SUMMARIZER_MODEL = "google/gemini-3-flash-preview"
|
||||||
MAX_SESSION_CHARS = 100_000
|
MAX_SESSION_CHARS = 100_000
|
||||||
|
|
@ -40,7 +41,7 @@ def _get_client() -> AsyncOpenAI:
|
||||||
raise ValueError("OPENROUTER_API_KEY not set")
|
raise ValueError("OPENROUTER_API_KEY not set")
|
||||||
_summarizer_client = AsyncOpenAI(
|
_summarizer_client = AsyncOpenAI(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url="https://openrouter.ai/api/v1",
|
base_url=OPENROUTER_BASE_URL,
|
||||||
)
|
)
|
||||||
return _summarizer_client
|
return _summarizer_client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from hermes_constants import OPENROUTER_BASE_URL
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Hardcoded trust configuration
|
# Hardcoded trust configuration
|
||||||
|
|
@ -941,7 +943,7 @@ def llm_audit_skill(skill_path: Path, static_result: ScanResult,
|
||||||
return static_result
|
return static_result
|
||||||
|
|
||||||
client = OpenAI(
|
client = OpenAI(
|
||||||
base_url="https://openrouter.ai/api/v1",
|
base_url=OPENROUTER_BASE_URL,
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
)
|
)
|
||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
|
|
@ -1037,11 +1039,6 @@ def _get_configured_model() -> str:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def check_guard_requirements() -> Tuple[bool, str]:
|
|
||||||
"""Check if the guard module can operate. Always returns True (no external deps)."""
|
|
||||||
return True, "Skills Guard ready"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Internal helpers
|
# Internal helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ def _check_disk_usage_warning():
|
||||||
if f.is_file():
|
if f.is_file():
|
||||||
try:
|
try:
|
||||||
total_bytes += f.stat().st_size
|
total_bytes += f.stat().st_size
|
||||||
except:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
total_gb = total_bytes / (1024 ** 3)
|
total_gb = total_bytes / (1024 ** 3)
|
||||||
|
|
@ -341,7 +341,7 @@ def _prompt_dangerous_approval(command: str, description: str, timeout_seconds:
|
||||||
def get_input():
|
def get_input():
|
||||||
try:
|
try:
|
||||||
result["choice"] = input(" Choice [o/s/a/D]: ").strip().lower()
|
result["choice"] = input(" Choice [o/s/a/D]: ").strip().lower()
|
||||||
except:
|
except (EOFError, OSError):
|
||||||
result["choice"] = ""
|
result["choice"] = ""
|
||||||
|
|
||||||
thread = threading.Thread(target=get_input, daemon=True)
|
thread = threading.Thread(target=get_input, daemon=True)
|
||||||
|
|
@ -894,7 +894,7 @@ class _SingularityEnvironment:
|
||||||
"""Cleanup on destruction."""
|
"""Cleanup on destruction."""
|
||||||
try:
|
try:
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1022,13 +1022,13 @@ class _SSHEnvironment:
|
||||||
cmd = ["ssh", "-o", f"ControlPath={self.control_socket}", "-O", "exit",
|
cmd = ["ssh", "-o", f"ControlPath={self.control_socket}", "-O", "exit",
|
||||||
f"{self.user}@{self.host}"]
|
f"{self.user}@{self.host}"]
|
||||||
subprocess.run(cmd, capture_output=True, timeout=5)
|
subprocess.run(cmd, capture_output=True, timeout=5)
|
||||||
except:
|
except (OSError, subprocess.SubprocessError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Remove socket file
|
# Remove socket file
|
||||||
try:
|
try:
|
||||||
self.control_socket.unlink()
|
self.control_socket.unlink()
|
||||||
except:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
|
@ -1039,7 +1039,7 @@ class _SSHEnvironment:
|
||||||
"""Cleanup on destruction."""
|
"""Cleanup on destruction."""
|
||||||
try:
|
try:
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1112,7 +1112,7 @@ class _DockerEnvironment:
|
||||||
"""Cleanup on destruction."""
|
"""Cleanup on destruction."""
|
||||||
try:
|
try:
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1189,7 +1189,7 @@ class _ModalEnvironment:
|
||||||
"""Cleanup on destruction."""
|
"""Cleanup on destruction."""
|
||||||
try:
|
try:
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
except:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1504,7 +1504,7 @@ def get_active_environments_info() -> Dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
size = sum(f.stat().st_size for f in Path(path).rglob('*') if f.is_file())
|
size = sum(f.stat().st_size for f in Path(path).rglob('*') if f.is_file())
|
||||||
total_size += size
|
total_size += size
|
||||||
except:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
info["total_disk_usage_mb"] = round(total_size / (1024 * 1024), 2)
|
info["total_disk_usage_mb"] = round(total_size / (1024 * 1024), 2)
|
||||||
|
|
@ -1532,7 +1532,7 @@ def cleanup_all_environments():
|
||||||
try:
|
try:
|
||||||
shutil.rmtree(path, ignore_errors=True)
|
shutil.rmtree(path, ignore_errors=True)
|
||||||
print(f"[Terminal Cleanup] Removed orphaned: {path}")
|
print(f"[Terminal Cleanup] Removed orphaned: {path}")
|
||||||
except:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if not os.getenv("HERMES_QUIET") and cleaned > 0:
|
if not os.getenv("HERMES_QUIET") and cleaned > 0:
|
||||||
|
|
@ -1669,7 +1669,6 @@ def terminal_tool(
|
||||||
# This prevents parallel tasks from overwriting each other's files
|
# This prevents parallel tasks from overwriting each other's files
|
||||||
# In CLI mode (HERMES_QUIET), use the cwd directly without subdirectories
|
# In CLI mode (HERMES_QUIET), use the cwd directly without subdirectories
|
||||||
if env_type == "local" and not os.getenv("HERMES_QUIET"):
|
if env_type == "local" and not os.getenv("HERMES_QUIET"):
|
||||||
import uuid
|
|
||||||
with _env_lock:
|
with _env_lock:
|
||||||
if effective_task_id not in _task_workdirs:
|
if effective_task_id not in _task_workdirs:
|
||||||
task_workdir = Path(cwd) / f"hermes-{effective_task_id}-{uuid.uuid4().hex[:8]}"
|
task_workdir = Path(cwd) / f"hermes-{effective_task_id}-{uuid.uuid4().hex[:8]}"
|
||||||
|
|
@ -1944,7 +1943,7 @@ def check_terminal_requirements() -> bool:
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
"""Simple test when run directly."""
|
# Simple test when run directly
|
||||||
print("Terminal Tool Module (mini-swe-agent backend)")
|
print("Terminal Tool Module (mini-swe-agent backend)")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -99,8 +99,3 @@ def transcribe_audio(file_path: str, model: Optional[str] = None) -> dict:
|
||||||
"transcript": "",
|
"transcript": "",
|
||||||
"error": str(e),
|
"error": str(e),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def check_stt_requirements() -> bool:
|
|
||||||
"""Check if OpenAI API key is available for speech-to-text."""
|
|
||||||
return bool(os.getenv("HERMES_OPENAI_API_KEY") or os.getenv("OPENAI_API_KEY"))
|
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,9 @@ import base64
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Any, Optional
|
from typing import Dict, Any, Optional
|
||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
import httpx # Use httpx for async HTTP requests
|
import httpx
|
||||||
|
from hermes_constants import OPENROUTER_BASE_URL
|
||||||
|
|
||||||
# Initialize OpenRouter API client lazily (only when needed)
|
|
||||||
_openrouter_client = None
|
_openrouter_client = None
|
||||||
|
|
||||||
def _get_openrouter_client():
|
def _get_openrouter_client():
|
||||||
|
|
@ -50,7 +50,7 @@ def _get_openrouter_client():
|
||||||
raise ValueError("OPENROUTER_API_KEY environment variable not set")
|
raise ValueError("OPENROUTER_API_KEY environment variable not set")
|
||||||
_openrouter_client = AsyncOpenAI(
|
_openrouter_client = AsyncOpenAI(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url="https://openrouter.ai/api/v1"
|
base_url=OPENROUTER_BASE_URL
|
||||||
)
|
)
|
||||||
return _openrouter_client
|
return _openrouter_client
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ from pathlib import Path
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
from firecrawl import Firecrawl
|
from firecrawl import Firecrawl
|
||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
|
from hermes_constants import OPENROUTER_BASE_URL
|
||||||
|
|
||||||
# Initialize Firecrawl client lazily (only when needed)
|
# Initialize Firecrawl client lazily (only when needed)
|
||||||
# This prevents import errors when FIRECRAWL_API_KEY is not set
|
# This prevents import errors when FIRECRAWL_API_KEY is not set
|
||||||
|
|
@ -77,7 +78,7 @@ def _get_summarizer_client():
|
||||||
raise ValueError("OPENROUTER_API_KEY environment variable not set")
|
raise ValueError("OPENROUTER_API_KEY environment variable not set")
|
||||||
_summarizer_client = AsyncOpenAI(
|
_summarizer_client = AsyncOpenAI(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
base_url="https://openrouter.ai/api/v1"
|
base_url=OPENROUTER_BASE_URL
|
||||||
)
|
)
|
||||||
return _summarizer_client
|
return _summarizer_client
|
||||||
|
|
||||||
|
|
|
||||||
57
toolsets.py
57
toolsets.py
|
|
@ -24,7 +24,6 @@ Usage:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List, Dict, Any, Set, Optional
|
from typing import List, Dict, Any, Set, Optional
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
# Core toolset definitions
|
# Core toolset definitions
|
||||||
|
|
@ -162,7 +161,7 @@ TOOLSETS = {
|
||||||
"safe": {
|
"safe": {
|
||||||
"description": "Safe toolkit without terminal access",
|
"description": "Safe toolkit without terminal access",
|
||||||
"tools": ["mixture_of_agents"],
|
"tools": ["mixture_of_agents"],
|
||||||
"includes": ["web", "vision", "creative"]
|
"includes": ["web", "vision", "image_gen"]
|
||||||
},
|
},
|
||||||
|
|
||||||
# ==========================================================================
|
# ==========================================================================
|
||||||
|
|
@ -587,50 +586,31 @@ def print_toolset_tree(name: str, indent: int = 0) -> None:
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
"""
|
print("Toolsets System Demo")
|
||||||
Demo and testing of the toolsets system
|
|
||||||
"""
|
|
||||||
print("🎯 Toolsets System Demo")
|
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
# Show all available toolsets
|
print("\nAvailable Toolsets:")
|
||||||
print("\n📦 Available Toolsets:")
|
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
for name, toolset in get_all_toolsets().items():
|
for name, toolset in get_all_toolsets().items():
|
||||||
info = get_toolset_info(name)
|
info = get_toolset_info(name)
|
||||||
composite = "📂" if info["is_composite"] else "🔧"
|
composite = "[composite]" if info["is_composite"] else "[leaf]"
|
||||||
print(f"{composite} {name:20} - {toolset['description']}")
|
print(f" {composite} {name:20} - {toolset['description']}")
|
||||||
print(f" Tools: {len(info['resolved_tools'])} total")
|
print(f" Tools: {len(info['resolved_tools'])} total")
|
||||||
|
|
||||||
|
print("\nToolset Resolution Examples:")
|
||||||
# Demo toolset resolution
|
|
||||||
print("\n🔍 Toolset Resolution Examples:")
|
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
|
for name in ["web", "terminal", "safe", "debugging"]:
|
||||||
examples = ["research", "development", "full_stack", "minimal", "safe"]
|
|
||||||
for name in examples:
|
|
||||||
tools = resolve_toolset(name)
|
tools = resolve_toolset(name)
|
||||||
print(f"\n{name}:")
|
print(f"\n {name}:")
|
||||||
print(f" Resolved to {len(tools)} tools: {', '.join(sorted(tools))}")
|
print(f" Resolved to {len(tools)} tools: {', '.join(sorted(tools))}")
|
||||||
|
|
||||||
# Show toolset composition tree
|
print("\nMultiple Toolset Resolution:")
|
||||||
print("\n🌳 Toolset Composition Tree:")
|
|
||||||
print("-" * 40)
|
print("-" * 40)
|
||||||
print("\nExample: 'content_creation' toolset:")
|
combined = resolve_multiple_toolsets(["web", "vision", "terminal"])
|
||||||
print_toolset_tree("content_creation")
|
print(f" Combining ['web', 'vision', 'terminal']:")
|
||||||
|
print(f" Result: {', '.join(sorted(combined))}")
|
||||||
|
|
||||||
print("\nExample: 'full_stack' toolset:")
|
print("\nCustom Toolset Creation:")
|
||||||
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)
|
print("-" * 40)
|
||||||
create_custom_toolset(
|
create_custom_toolset(
|
||||||
name="my_custom",
|
name="my_custom",
|
||||||
|
|
@ -638,8 +618,7 @@ if __name__ == "__main__":
|
||||||
tools=["web_search"],
|
tools=["web_search"],
|
||||||
includes=["terminal", "vision"]
|
includes=["terminal", "vision"]
|
||||||
)
|
)
|
||||||
|
|
||||||
custom_info = get_toolset_info("my_custom")
|
custom_info = get_toolset_info("my_custom")
|
||||||
print(f"Created 'my_custom' toolset:")
|
print(f" Created 'my_custom' toolset:")
|
||||||
print(f" Description: {custom_info['description']}")
|
print(f" Description: {custom_info['description']}")
|
||||||
print(f" Resolved tools: {', '.join(custom_info['resolved_tools'])}")
|
print(f" Resolved tools: {', '.join(custom_info['resolved_tools'])}")
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ from datetime import datetime
|
||||||
import fire
|
import fire
|
||||||
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn, TimeRemainingColumn
|
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn, TimeElapsedColumn, TimeRemainingColumn
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
from hermes_constants import OPENROUTER_BASE_URL
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
@ -70,7 +71,7 @@ class CompressionConfig:
|
||||||
|
|
||||||
# Summarization (OpenRouter)
|
# Summarization (OpenRouter)
|
||||||
summarization_model: str = "google/gemini-3-flash-preview"
|
summarization_model: str = "google/gemini-3-flash-preview"
|
||||||
base_url: str = "https://openrouter.ai/api/v1"
|
base_url: str = OPENROUTER_BASE_URL
|
||||||
api_key_env: str = "OPENROUTER_API_KEY"
|
api_key_env: str = "OPENROUTER_API_KEY"
|
||||||
temperature: float = 0.3
|
temperature: float = 0.3
|
||||||
max_retries: int = 3
|
max_retries: int = 3
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue