/ tools / tool_backend_helpers.py
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())