From bbb5776763e4cc05a3395a94042bcd01ca3bb86f Mon Sep 17 00:00:00 2001 From: teknium1 Date: Mon, 2 Feb 2026 19:28:27 -0800 Subject: [PATCH] Enhance tool availability checks and user feedback in CLI - Updated the CLI to include a new method for displaying warnings about disabled tools due to missing API keys. - Integrated tool availability checks into the setup wizard and doctor commands, providing users with clear information on which tools are available and what is required for full functionality. - Improved user prompts and feedback regarding API key configuration, emphasizing the importance of setting up keys for certain tools. - Added detailed summaries of tool availability during setup and diagnostics, enhancing the overall user experience. --- cli.py | 27 +++++++- hermes_cli/doctor.py | 31 +++++++++ hermes_cli/setup.py | 146 ++++++++++++++++++++++++++++++++++++------- model_tools.py | 128 ++++++++++++++++++++++++++++++++++++- 4 files changed, 307 insertions(+), 25 deletions(-) diff --git a/cli.py b/cli.py index bd06439f..301c45be 100755 --- a/cli.py +++ b/cli.py @@ -596,7 +596,7 @@ class HermesCLI: self._show_status() else: # Get tools for display - tools = get_tool_definitions(enabled_toolsets=self.enabled_toolsets) + tools = get_tool_definitions(enabled_toolsets=self.enabled_toolsets, quiet_mode=True) # Get terminal working directory (where commands will execute) cwd = os.getenv("TERMINAL_CWD", os.getcwd()) @@ -611,8 +611,33 @@ class HermesCLI: session_id=self.session_id, ) + # Show tool availability warnings if any tools are disabled + self._show_tool_availability_warnings() + self.console.print() + def _show_tool_availability_warnings(self): + """Show warnings about disabled tools due to missing API keys.""" + try: + from model_tools import check_tool_availability, TOOLSET_REQUIREMENTS + + available, unavailable = check_tool_availability() + + # Filter to only those missing API keys (not system deps) + api_key_missing = [u for u in unavailable if u["missing_vars"]] + + if api_key_missing: + self.console.print() + self.console.print("[yellow]⚠️ Some tools disabled (missing API keys):[/]") + for item in api_key_missing: + tools_str = ", ".join(item["tools"][:2]) # Show first 2 tools + if len(item["tools"]) > 2: + tools_str += f", +{len(item['tools'])-2} more" + self.console.print(f" [dim]• {item['name']}[/] [dim italic]({', '.join(item['missing_vars'])})[/]") + self.console.print("[dim] Run 'hermes setup' to configure[/]") + except Exception: + pass # Don't crash on import errors + def _show_status(self): """Show current status bar.""" # Get tool count diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 2b69317b..82b7e541 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -256,6 +256,37 @@ def run_doctor(args): except Exception as e: check_warn("Anthropic API", f"({e})") + # ========================================================================= + # Check: Tool Availability + # ========================================================================= + print() + print(color("◆ Tool Availability", Colors.CYAN, Colors.BOLD)) + + try: + # Add project root to path for imports + sys.path.insert(0, str(PROJECT_ROOT)) + from model_tools import check_tool_availability, TOOLSET_REQUIREMENTS + + available, unavailable = check_tool_availability() + + for tid in available: + info = TOOLSET_REQUIREMENTS.get(tid, {}) + check_ok(info.get("name", tid)) + + for item in unavailable: + if item["missing_vars"]: + vars_str = ", ".join(item["missing_vars"]) + check_warn(item["name"], f"(missing {vars_str})") + else: + check_warn(item["name"], "(system dependency not met)") + + # Count disabled tools with API key requirements + api_disabled = [u for u in unavailable if u["missing_vars"]] + if api_disabled: + issues.append("Run 'hermes setup' to configure missing API keys for full tool access") + except Exception as e: + check_warn("Could not check tool availability", f"({e})") + # ========================================================================= # Summary # ========================================================================= diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 8afaac4f..a766e3af 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -510,48 +510,148 @@ def run_setup_wizard(args): # Step 7: Additional Tools (Optional) # ========================================================================= print_header("Additional Tools (Optional)") + print_info("These tools extend the agent's capabilities.") + print_info("Without their API keys, the corresponding features will be disabled.") + print() - # Firecrawl - if not get_env_value('FIRECRAWL_API_KEY'): - if prompt_yes_no("Set up web scraping (Firecrawl)?", False): - print_info("Get your API key at: https://firecrawl.dev/") - api_key = prompt("Firecrawl API key", password=True) + # Firecrawl - Web scraping + print_info("─" * 50) + print(color(" Web Search & Scraping (Firecrawl)", Colors.CYAN)) + print_info(" Enables: web_search, web_extract tools") + print_info(" Use case: Search the web, read webpage content") + if get_env_value('FIRECRAWL_API_KEY'): + print_success(" Status: Configured ✓") + if prompt_yes_no(" Update Firecrawl API key?", False): + api_key = prompt(" API key", password=True) if api_key: save_env_value("FIRECRAWL_API_KEY", api_key) - print_success("Firecrawl API key saved") + print_success(" Updated") else: - print_info("Firecrawl: already configured") + print_warning(" Status: Not configured (tools will be disabled)") + if prompt_yes_no(" Set up Firecrawl?", False): + print_info(" Get your API key at: https://firecrawl.dev/") + api_key = prompt(" API key", password=True) + if api_key: + save_env_value("FIRECRAWL_API_KEY", api_key) + print_success(" Configured ✓") + print() - # Browserbase - if not get_env_value('BROWSERBASE_API_KEY'): - if prompt_yes_no("Set up browser automation (Browserbase)?", False): - print_info("Get your API key at: https://browserbase.com/") - api_key = prompt("Browserbase API key", password=True) - project_id = prompt("Browserbase project ID") + # Browserbase - Browser automation + print_info("─" * 50) + print(color(" Browser Automation (Browserbase)", Colors.CYAN)) + print_info(" Enables: browser_navigate, browser_click, etc.") + print_info(" Use case: Interact with web pages, fill forms, screenshots") + if get_env_value('BROWSERBASE_API_KEY'): + print_success(" Status: Configured ✓") + if prompt_yes_no(" Update Browserbase credentials?", False): + api_key = prompt(" API key", password=True) + project_id = prompt(" Project ID") if api_key: save_env_value("BROWSERBASE_API_KEY", api_key) if project_id: save_env_value("BROWSERBASE_PROJECT_ID", project_id) - print_success("Browserbase configured") + print_success(" Updated") else: - print_info("Browserbase: already configured") + print_warning(" Status: Not configured (tools will be disabled)") + if prompt_yes_no(" Set up Browserbase?", False): + print_info(" Get credentials at: https://browserbase.com/") + api_key = prompt(" API key", password=True) + project_id = prompt(" Project ID") + if api_key: + save_env_value("BROWSERBASE_API_KEY", api_key) + if project_id: + save_env_value("BROWSERBASE_PROJECT_ID", project_id) + print_success(" Configured ✓") + print() - # FAL - if not get_env_value('FAL_KEY'): - if prompt_yes_no("Set up image generation (FAL)?", False): - print_info("Get your API key at: https://fal.ai/") - api_key = prompt("FAL API key", password=True) + # FAL - Image generation + print_info("─" * 50) + print(color(" Image Generation (FAL)", Colors.CYAN)) + print_info(" Enables: image_generate tool") + print_info(" Use case: Generate images from text prompts (FLUX)") + if get_env_value('FAL_KEY'): + print_success(" Status: Configured ✓") + if prompt_yes_no(" Update FAL API key?", False): + api_key = prompt(" API key", password=True) if api_key: save_env_value("FAL_KEY", api_key) - print_success("FAL API key saved") + print_success(" Updated") else: - print_info("FAL: already configured") + print_warning(" Status: Not configured (tool will be disabled)") + if prompt_yes_no(" Set up FAL?", False): + print_info(" Get your API key at: https://fal.ai/") + api_key = prompt(" API key", password=True) + if api_key: + save_env_value("FAL_KEY", api_key) + print_success(" Configured ✓") # ========================================================================= # Save config # ========================================================================= save_config(config) + # ========================================================================= + # Tool Availability Summary + # ========================================================================= + print() + print_header("Tool Availability Summary") + + # Check which tools are available + tool_status = [] + + # OpenRouter (required for vision, moa) + if get_env_value('OPENROUTER_API_KEY'): + tool_status.append(("Vision (image analysis)", True, None)) + tool_status.append(("Mixture of Agents", True, None)) + else: + tool_status.append(("Vision (image analysis)", False, "OPENROUTER_API_KEY")) + tool_status.append(("Mixture of Agents", False, "OPENROUTER_API_KEY")) + + # Firecrawl (web tools) + if get_env_value('FIRECRAWL_API_KEY'): + tool_status.append(("Web Search & Extract", True, None)) + else: + tool_status.append(("Web Search & Extract", False, "FIRECRAWL_API_KEY")) + + # Browserbase (browser tools) + if get_env_value('BROWSERBASE_API_KEY'): + tool_status.append(("Browser Automation", True, None)) + else: + tool_status.append(("Browser Automation", False, "BROWSERBASE_API_KEY")) + + # FAL (image generation) + if get_env_value('FAL_KEY'): + tool_status.append(("Image Generation", True, None)) + else: + tool_status.append(("Image Generation", False, "FAL_KEY")) + + # Terminal (always available if system deps met) + tool_status.append(("Terminal/Commands", True, None)) + + # Skills (always available if skills dir exists) + tool_status.append(("Skills Knowledge Base", True, None)) + + # Print status + available_count = sum(1 for _, avail, _ in tool_status if avail) + total_count = len(tool_status) + + print_info(f"{available_count}/{total_count} tool categories available:") + print() + + for name, available, missing_var in tool_status: + if available: + print(f" {color('✓', Colors.GREEN)} {name}") + else: + print(f" {color('✗', Colors.RED)} {name} {color(f'(missing {missing_var})', Colors.DIM)}") + + print() + + disabled_tools = [(name, var) for name, avail, var in tool_status if not avail] + if disabled_tools: + print_warning("Some tools are disabled. Run 'hermes setup' again to configure them,") + print_warning("or edit ~/.hermes/.env directly to add the missing API keys.") + print() + # ========================================================================= # Done! # ========================================================================= @@ -568,7 +668,7 @@ def run_setup_wizard(args): print(f" Model, terminal backend, compression, etc.") print() print(f" {color('API Keys:', Colors.YELLOW)} {get_env_path()}") - print(f" OpenRouter, Anthropic, Firecrawl, etc.") + print(f" OpenRouter, Custom Endpoint, tool API keys") print() print(f" {color('Data:', Colors.YELLOW)} {hermes_home}/") print(f" Cron jobs, sessions, logs") diff --git a/model_tools.py b/model_tools.py index 9878951d..13886019 100644 --- a/model_tools.py +++ b/model_tools.py @@ -28,7 +28,8 @@ Usage: import json import asyncio -from typing import Dict, Any, List, Optional +import os +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.terminal_tool import terminal_tool, check_terminal_requirements, TERMINAL_TOOL_DESCRIPTION, cleanup_vm @@ -71,6 +72,131 @@ from toolsets import ( get_toolset_info, print_toolset_tree ) + +# ============================================================================= +# Tool Availability Checking +# ============================================================================= + +# Maps toolsets to their required API keys/environment variables +TOOLSET_REQUIREMENTS = { + "web": { + "name": "Web Search & Extract", + "env_vars": ["FIRECRAWL_API_KEY"], + "check_fn": check_firecrawl_api_key, + "setup_url": "https://firecrawl.dev/", + "tools": ["web_search", "web_extract"], + }, + "vision": { + "name": "Vision (Image Analysis)", + "env_vars": ["OPENROUTER_API_KEY"], + "check_fn": check_vision_requirements, + "setup_url": "https://openrouter.ai/keys", + "tools": ["vision_analyze"], + }, + "moa": { + "name": "Mixture of Agents", + "env_vars": ["OPENROUTER_API_KEY"], + "check_fn": check_moa_requirements, + "setup_url": "https://openrouter.ai/keys", + "tools": ["mixture_of_agents"], + }, + "image_gen": { + "name": "Image Generation", + "env_vars": ["FAL_KEY"], + "check_fn": check_image_generation_requirements, + "setup_url": "https://fal.ai/", + "tools": ["image_generate"], + }, + "browser": { + "name": "Browser Automation", + "env_vars": ["BROWSERBASE_API_KEY", "BROWSERBASE_PROJECT_ID"], + "check_fn": check_browser_requirements, + "setup_url": "https://browserbase.com/", + "tools": ["browser_navigate", "browser_snapshot", "browser_click", "browser_type"], + }, + "terminal": { + "name": "Terminal/Command Execution", + "env_vars": [], # No API key required, just system dependencies + "check_fn": check_terminal_requirements, + "setup_url": None, + "tools": ["terminal"], + }, + "skills": { + "name": "Skills Knowledge Base", + "env_vars": [], # Just needs skills directory + "check_fn": check_skills_requirements, + "setup_url": None, + "tools": ["skills_categories", "skills_list", "skill_view"], + }, +} + + +def check_tool_availability(quiet: bool = False) -> Tuple[List[str], List[Dict[str, Any]]]: + """ + Check which tool categories are available based on API keys and requirements. + + Returns: + Tuple containing: + - List of available toolset names + - List of dicts with info about unavailable toolsets and what's missing + """ + available = [] + unavailable = [] + + for toolset_id, info in TOOLSET_REQUIREMENTS.items(): + if info["check_fn"](): + available.append(toolset_id) + else: + # Figure out what's missing + missing_vars = [var for var in info["env_vars"] if not os.getenv(var)] + unavailable.append({ + "id": toolset_id, + "name": info["name"], + "missing_vars": missing_vars, + "setup_url": info["setup_url"], + "tools": info["tools"], + }) + + 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]]: """ Get tool definitions for web tools in OpenAI's expected format.