tool_backend_helpers.py
1 """Shared helpers for tool backend selection.""" 2 3 from __future__ import annotations 4 5 import os 6 from pathlib import Path 7 from typing import Any, Dict 8 9 from utils import is_truthy_value 10 11 12 _DEFAULT_BROWSER_PROVIDER = "local" 13 _DEFAULT_MODAL_MODE = "auto" 14 _VALID_MODAL_MODES = {"auto", "direct", "managed"} 15 16 17 def managed_nous_tools_enabled() -> bool: 18 """Return True when the user has an active paid Nous subscription. 19 20 The Tool Gateway is available to any Nous subscriber who is NOT on 21 the free tier. We intentionally catch all exceptions and return 22 False — never block the agent startup path. 23 """ 24 try: 25 from hermes_cli.auth import get_nous_auth_status 26 27 status = get_nous_auth_status() 28 if not status.get("logged_in"): 29 return False 30 31 from hermes_cli.models import check_nous_free_tier 32 33 if check_nous_free_tier(): 34 return False # free-tier users don't get gateway access 35 return True 36 except Exception: 37 return False 38 39 40 def normalize_browser_cloud_provider(value: object | None) -> str: 41 """Return a normalized browser provider key.""" 42 provider = str(value or _DEFAULT_BROWSER_PROVIDER).strip().lower() 43 return provider or _DEFAULT_BROWSER_PROVIDER 44 45 46 def coerce_modal_mode(value: object | None) -> str: 47 """Return the requested modal mode when valid, else the default.""" 48 mode = str(value or _DEFAULT_MODAL_MODE).strip().lower() 49 if mode in _VALID_MODAL_MODES: 50 return mode 51 return _DEFAULT_MODAL_MODE 52 53 54 def normalize_modal_mode(value: object | None) -> str: 55 """Return a normalized modal execution mode.""" 56 return coerce_modal_mode(value) 57 58 59 def has_direct_modal_credentials() -> bool: 60 """Return True when direct Modal credentials/config are available.""" 61 return bool( 62 (os.getenv("MODAL_TOKEN_ID") and os.getenv("MODAL_TOKEN_SECRET")) 63 or (Path.home() / ".modal.toml").exists() 64 ) 65 66 67 def resolve_modal_backend_state( 68 modal_mode: object | None, 69 *, 70 has_direct: bool, 71 managed_ready: bool, 72 ) -> Dict[str, Any]: 73 """Resolve direct vs managed Modal backend selection. 74 75 Semantics: 76 - ``direct`` means direct-only 77 - ``managed`` means managed-only 78 - ``auto`` prefers managed when available, then falls back to direct 79 """ 80 requested_mode = coerce_modal_mode(modal_mode) 81 normalized_mode = normalize_modal_mode(modal_mode) 82 managed_mode_blocked = ( 83 requested_mode == "managed" and not managed_nous_tools_enabled() 84 ) 85 86 if normalized_mode == "managed": 87 selected_backend = "managed" if managed_nous_tools_enabled() and managed_ready else None 88 elif normalized_mode == "direct": 89 selected_backend = "direct" if has_direct else None 90 else: 91 selected_backend = "managed" if managed_nous_tools_enabled() and managed_ready else "direct" if has_direct else None 92 93 return { 94 "requested_mode": requested_mode, 95 "mode": normalized_mode, 96 "has_direct": has_direct, 97 "managed_ready": managed_ready, 98 "managed_mode_blocked": managed_mode_blocked, 99 "selected_backend": selected_backend, 100 } 101 102 103 def resolve_openai_audio_api_key() -> str: 104 """Prefer the voice-tools key, but fall back to the normal OpenAI key.""" 105 return ( 106 os.getenv("VOICE_TOOLS_OPENAI_KEY", "") 107 or os.getenv("OPENAI_API_KEY", "") 108 ).strip() 109 110 111 def prefers_gateway(config_section: str) -> bool: 112 """Return True when the user opted into the Tool Gateway for this tool. 113 114 Reads ``<section>.use_gateway`` from config.yaml. Never raises. 115 """ 116 try: 117 from hermes_cli.config import load_config 118 section = (load_config() or {}).get(config_section) 119 if isinstance(section, dict): 120 return is_truthy_value(section.get("use_gateway"), default=False) 121 except Exception: 122 pass 123 return False 124 125 126 def fal_key_is_configured() -> bool: 127 """Return True when FAL_KEY is set to a non-whitespace value. 128 129 Consults both ``os.environ`` and ``~/.hermes/.env`` (via 130 ``hermes_cli.config.get_env_value`` when available) so tool-side 131 checks and CLI setup-time checks agree. A whitespace-only value 132 is treated as unset everywhere. 133 """ 134 value = os.getenv("FAL_KEY") 135 if value is None: 136 # Fall back to the .env file for CLI paths that may run before 137 # dotenv is loaded into os.environ. 138 try: 139 from hermes_cli.config import get_env_value 140 141 value = get_env_value("FAL_KEY") 142 except Exception: 143 value = None 144 return bool(value and value.strip())