mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat: add fast-path setup for nous account
adds a nous account specific fast flow & autolaunches into chat if gateway isn't set up
This commit is contained in:
parent
bdc9b07c9d
commit
2f230b5ad9
4 changed files with 79 additions and 48 deletions
|
|
@ -2821,6 +2821,7 @@ def _prompt_model_selection(
|
||||||
pricing: Optional[Dict[str, Dict[str, str]]] = None,
|
pricing: Optional[Dict[str, Dict[str, str]]] = None,
|
||||||
unavailable_models: Optional[List[str]] = None,
|
unavailable_models: Optional[List[str]] = None,
|
||||||
portal_url: str = "",
|
portal_url: str = "",
|
||||||
|
allow_custom = True
|
||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""Interactive model selection. Puts current_model first with a marker. Returns chosen model ID or None.
|
"""Interactive model selection. Puts current_model first with a marker. Returns chosen model ID or None.
|
||||||
|
|
||||||
|
|
@ -2909,7 +2910,15 @@ def _prompt_model_selection(
|
||||||
from simple_term_menu import TerminalMenu
|
from simple_term_menu import TerminalMenu
|
||||||
|
|
||||||
choices = [f" {_label(mid)}" for mid in ordered]
|
choices = [f" {_label(mid)}" for mid in ordered]
|
||||||
|
|
||||||
|
custom_idx = None
|
||||||
|
if allow_custom:
|
||||||
|
custom_idx = len(choices)
|
||||||
choices.append(" Enter custom model name")
|
choices.append(" Enter custom model name")
|
||||||
|
|
||||||
|
skip_idx = None
|
||||||
|
if current_model:
|
||||||
|
skip_idx = len(choices)
|
||||||
choices.append(" Skip (keep current)")
|
choices.append(" Skip (keep current)")
|
||||||
|
|
||||||
# Print the unavailable block BEFORE the menu via regular print().
|
# Print the unavailable block BEFORE the menu via regular print().
|
||||||
|
|
@ -2947,21 +2956,29 @@ def _prompt_model_selection(
|
||||||
print()
|
print()
|
||||||
if idx < len(ordered):
|
if idx < len(ordered):
|
||||||
return ordered[idx]
|
return ordered[idx]
|
||||||
elif idx == len(ordered):
|
if idx == custom_idx:
|
||||||
custom = input("Enter model name: ").strip()
|
custom = input("Enter model name: ").strip()
|
||||||
return custom if custom else None
|
return custom if custom else None
|
||||||
|
if idx == skip_idx:
|
||||||
|
return None
|
||||||
return None
|
return None
|
||||||
except (ImportError, NotImplementedError, OSError, subprocess.SubprocessError):
|
except (ImportError, NotImplementedError, OSError, subprocess.SubprocessError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Fallback: numbered list
|
# Fallback: numbered list
|
||||||
print(menu_title)
|
print(menu_title)
|
||||||
num_width = len(str(len(ordered) + 2))
|
n = len(ordered)
|
||||||
|
extra = []
|
||||||
|
if allow_custom:
|
||||||
|
extra.append("Enter custom model name")
|
||||||
|
if current_model:
|
||||||
|
extra.append("Skip (keep current)")
|
||||||
|
total = n + len(extra)
|
||||||
|
num_width = len(str(total))
|
||||||
for i, mid in enumerate(ordered, 1):
|
for i, mid in enumerate(ordered, 1):
|
||||||
print(f" {i:>{num_width}}. {_label(mid)}")
|
print(f" {i:>{num_width}}. {_label(mid)}")
|
||||||
n = len(ordered)
|
for j, label in enumerate(extra, n + 1):
|
||||||
print(f" {n + 1:>{num_width}}. Enter custom model name")
|
print(f" {j:>{num_width}}. {label}")
|
||||||
print(f" {n + 2:>{num_width}}. Skip (keep current)")
|
|
||||||
|
|
||||||
if _unavailable:
|
if _unavailable:
|
||||||
_upgrade_url = (portal_url or DEFAULT_NOUS_PORTAL_URL).rstrip("/")
|
_upgrade_url = (portal_url or DEFAULT_NOUS_PORTAL_URL).rstrip("/")
|
||||||
|
|
@ -2973,18 +2990,19 @@ def _prompt_model_selection(
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
choice = input(f"Choice [1-{n + 2}] (default: skip): ").strip()
|
choice = input(f"Choice [1-{total}]: ").strip()
|
||||||
if not choice:
|
if not choice:
|
||||||
return None
|
return None
|
||||||
idx = int(choice)
|
val = int(choice)
|
||||||
if 1 <= idx <= n:
|
if 1 <= val <= n:
|
||||||
return ordered[idx - 1]
|
return ordered[val - 1]
|
||||||
elif idx == n + 1:
|
extra_idx = val - n - 1
|
||||||
|
if 0 <= extra_idx < len(extra):
|
||||||
|
if extra[extra_idx] == "Enter custom model name":
|
||||||
custom = input("Enter model name: ").strip()
|
custom = input("Enter model name: ").strip()
|
||||||
return custom if custom else None
|
return custom if custom else None
|
||||||
elif idx == n + 2:
|
return None # skip
|
||||||
return None
|
print(f"Please enter 1-{total}")
|
||||||
print(f"Please enter 1-{n + 2}")
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("Please enter a number")
|
print("Please enter a number")
|
||||||
except (KeyboardInterrupt, EOFError):
|
except (KeyboardInterrupt, EOFError):
|
||||||
|
|
@ -3260,7 +3278,6 @@ def _nous_device_code_login(
|
||||||
open_browser = False
|
open_browser = False
|
||||||
|
|
||||||
print(f"Starting Hermes login via {pconfig.name}...")
|
print(f"Starting Hermes login via {pconfig.name}...")
|
||||||
print(f"Portal: {portal_base_url}")
|
|
||||||
if insecure:
|
if insecure:
|
||||||
print("TLS verification: disabled (--insecure)")
|
print("TLS verification: disabled (--insecure)")
|
||||||
elif ca_bundle:
|
elif ca_bundle:
|
||||||
|
|
@ -3280,19 +3297,18 @@ def _nous_device_code_login(
|
||||||
interval = int(device_data["interval"])
|
interval = int(device_data["interval"])
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("To continue:")
|
|
||||||
print(f" 1. Open: {verification_url}")
|
|
||||||
print(f" 2. If prompted, enter code: {user_code}")
|
|
||||||
|
|
||||||
if open_browser:
|
if open_browser:
|
||||||
opened = webbrowser.open(verification_url)
|
opened = webbrowser.open(verification_url)
|
||||||
if opened:
|
if opened:
|
||||||
print(" (Opened browser for verification)")
|
print("If you don't see a browser window open, navigate to this URL:")
|
||||||
else:
|
else:
|
||||||
print(" Could not open browser automatically — use the URL above.")
|
print("Navigate to this URL to continue:")
|
||||||
|
print(verification_url)
|
||||||
|
print(f"If you're prompted for a code, use {user_code}")
|
||||||
|
print()
|
||||||
|
|
||||||
effective_interval = max(1, min(interval, DEVICE_AUTH_POLL_INTERVAL_CAP_SECONDS))
|
effective_interval = max(1, min(interval, DEVICE_AUTH_POLL_INTERVAL_CAP_SECONDS))
|
||||||
print(f"Waiting for approval (polling every {effective_interval}s)...")
|
print(f"Waiting for approval (checking every {effective_interval}s)...")
|
||||||
|
|
||||||
token_data = _poll_for_token(
|
token_data = _poll_for_token(
|
||||||
client=client,
|
client=client,
|
||||||
|
|
@ -3357,7 +3373,7 @@ def _nous_device_code_login(
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def _login_nous(args, pconfig: ProviderConfig) -> None:
|
def login_nous(args, pconfig: ProviderConfig) -> None:
|
||||||
"""Nous Portal device authorization flow."""
|
"""Nous Portal device authorization flow."""
|
||||||
timeout_seconds = getattr(args, "timeout", None) or 15.0
|
timeout_seconds = getattr(args, "timeout", None) or 15.0
|
||||||
insecure = bool(getattr(args, "insecure", False))
|
insecure = bool(getattr(args, "insecure", False))
|
||||||
|
|
@ -3419,7 +3435,10 @@ def _login_nous(args, pconfig: ProviderConfig) -> None:
|
||||||
)
|
)
|
||||||
model_ids = _PROVIDER_MODELS.get("nous", [])
|
model_ids = _PROVIDER_MODELS.get("nous", [])
|
||||||
|
|
||||||
|
_portal = auth_state.get("portal_base_url", "")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
unavailable_models: list = []
|
unavailable_models: list = []
|
||||||
if model_ids:
|
if model_ids:
|
||||||
pricing = get_pricing_for_provider("nous")
|
pricing = get_pricing_for_provider("nous")
|
||||||
|
|
@ -3428,14 +3447,17 @@ def _login_nous(args, pconfig: ProviderConfig) -> None:
|
||||||
model_ids, unavailable_models = partition_nous_models_by_tier(
|
model_ids, unavailable_models = partition_nous_models_by_tier(
|
||||||
model_ids, pricing, free_tier=True,
|
model_ids, pricing, free_tier=True,
|
||||||
)
|
)
|
||||||
_portal = auth_state.get("portal_base_url", "")
|
if not free_tier:
|
||||||
if model_ids:
|
|
||||||
print(f"Showing {len(model_ids)} curated models — use \"Enter custom model name\" for others.")
|
print(f"Showing {len(model_ids)} curated models — use \"Enter custom model name\" for others.")
|
||||||
|
if len(model_ids) > 1:
|
||||||
selected_model = _prompt_model_selection(
|
selected_model = _prompt_model_selection(
|
||||||
model_ids, pricing=pricing,
|
model_ids, pricing=pricing,
|
||||||
unavailable_models=unavailable_models,
|
unavailable_models=unavailable_models,
|
||||||
portal_url=_portal,
|
portal_url=_portal,
|
||||||
|
allow_custom=not free_tier
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
selected_model = model_ids[0]
|
||||||
elif unavailable_models:
|
elif unavailable_models:
|
||||||
_url = (_portal or DEFAULT_NOUS_PORTAL_URL).rstrip("/")
|
_url = (_portal or DEFAULT_NOUS_PORTAL_URL).rstrip("/")
|
||||||
print("No free models currently available.")
|
print("No free models currently available.")
|
||||||
|
|
|
||||||
|
|
@ -2124,7 +2124,7 @@ def _model_flow_nous(config, current_model="", args=None):
|
||||||
resolve_nous_runtime_credentials,
|
resolve_nous_runtime_credentials,
|
||||||
AuthError,
|
AuthError,
|
||||||
format_auth_error,
|
format_auth_error,
|
||||||
_login_nous,
|
login_nous,
|
||||||
PROVIDER_REGISTRY,
|
PROVIDER_REGISTRY,
|
||||||
)
|
)
|
||||||
from hermes_cli.config import (
|
from hermes_cli.config import (
|
||||||
|
|
@ -2137,8 +2137,6 @@ def _model_flow_nous(config, current_model="", args=None):
|
||||||
|
|
||||||
state = get_provider_auth_state("nous")
|
state = get_provider_auth_state("nous")
|
||||||
if not state or not state.get("access_token"):
|
if not state or not state.get("access_token"):
|
||||||
print("Not logged into Nous Portal. Starting login...")
|
|
||||||
print()
|
|
||||||
try:
|
try:
|
||||||
mock_args = argparse.Namespace(
|
mock_args = argparse.Namespace(
|
||||||
portal_url=getattr(args, "portal_url", None),
|
portal_url=getattr(args, "portal_url", None),
|
||||||
|
|
@ -2150,7 +2148,7 @@ def _model_flow_nous(config, current_model="", args=None):
|
||||||
ca_bundle=getattr(args, "ca_bundle", None),
|
ca_bundle=getattr(args, "ca_bundle", None),
|
||||||
insecure=bool(getattr(args, "insecure", False)),
|
insecure=bool(getattr(args, "insecure", False)),
|
||||||
)
|
)
|
||||||
_login_nous(mock_args, PROVIDER_REGISTRY["nous"])
|
login_nous(mock_args, PROVIDER_REGISTRY["nous"])
|
||||||
# Offer Tool Gateway enablement for paid subscribers
|
# Offer Tool Gateway enablement for paid subscribers
|
||||||
try:
|
try:
|
||||||
_refreshed = load_config() or {}
|
_refreshed = load_config() or {}
|
||||||
|
|
@ -2201,7 +2199,7 @@ def _model_flow_nous(config, current_model="", args=None):
|
||||||
ca_bundle=None,
|
ca_bundle=None,
|
||||||
insecure=False,
|
insecure=False,
|
||||||
)
|
)
|
||||||
_login_nous(mock_args, PROVIDER_REGISTRY["nous"])
|
login_nous(mock_args, PROVIDER_REGISTRY["nous"])
|
||||||
except Exception as login_exc:
|
except Exception as login_exc:
|
||||||
print(f"Re-login failed: {login_exc}")
|
print(f"Re-login failed: {login_exc}")
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,10 @@ import shutil
|
||||||
import sys
|
import sys
|
||||||
import copy
|
import copy
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Dict, Any
|
from typing import Literal, Optional, Dict, Any
|
||||||
|
|
||||||
from hermes_cli.nous_subscription import get_nous_subscription_features
|
from hermes_cli.nous_subscription import get_nous_subscription_features
|
||||||
|
from hermes_cli.main import _model_flow_nous
|
||||||
from tools.tool_backend_helpers import managed_nous_tools_enabled
|
from tools.tool_backend_helpers import managed_nous_tools_enabled
|
||||||
from utils import base_url_hostname
|
from utils import base_url_hostname
|
||||||
from hermes_constants import get_optional_skills_dir
|
from hermes_constants import get_optional_skills_dir
|
||||||
|
|
@ -655,7 +656,7 @@ def _prompt_container_resources(config: dict):
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def setup_model_provider(config: dict, *, quick: bool = False):
|
def setup_model_provider(config: dict, *, quick: bool | Literal["nous_portal"] = False):
|
||||||
"""Configure the inference provider and default model.
|
"""Configure the inference provider and default model.
|
||||||
|
|
||||||
Delegates to ``cmd_model()`` (the same flow used by ``hermes model``)
|
Delegates to ``cmd_model()`` (the same flow used by ``hermes model``)
|
||||||
|
|
@ -677,6 +678,10 @@ def setup_model_provider(config: dict, *, quick: bool = False):
|
||||||
# credential prompting, model selection, and config persistence.
|
# credential prompting, model selection, and config persistence.
|
||||||
from hermes_cli.main import select_provider_and_model
|
from hermes_cli.main import select_provider_and_model
|
||||||
try:
|
try:
|
||||||
|
if quick == "nous_portal":
|
||||||
|
config = load_config()
|
||||||
|
_model_flow_nous(config)
|
||||||
|
else:
|
||||||
select_provider_and_model()
|
select_provider_and_model()
|
||||||
except (SystemExit, KeyboardInterrupt):
|
except (SystemExit, KeyboardInterrupt):
|
||||||
print()
|
print()
|
||||||
|
|
@ -3030,11 +3035,15 @@ def run_setup_wizard(args):
|
||||||
config = load_config()
|
config = load_config()
|
||||||
|
|
||||||
setup_mode = prompt_choice("How would you like to set up Hermes?", [
|
setup_mode = prompt_choice("How would you like to set up Hermes?", [
|
||||||
"Quick setup — provider, model & messaging (recommended)",
|
"Nous Account setup — model & messaging (recommended)",
|
||||||
|
"Quick setup — provider, model & messaging",
|
||||||
"Full setup — configure everything",
|
"Full setup — configure everything",
|
||||||
], 0)
|
], 0)
|
||||||
|
|
||||||
if setup_mode == 0:
|
if setup_mode == 0:
|
||||||
|
_run_first_time_quick_setup(config, hermes_home, is_existing, nous_quick=True)
|
||||||
|
return
|
||||||
|
if setup_mode == 1:
|
||||||
_run_first_time_quick_setup(config, hermes_home, is_existing)
|
_run_first_time_quick_setup(config, hermes_home, is_existing)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -3095,7 +3104,7 @@ def _resolve_hermes_chat_argv() -> Optional[list[str]]:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _offer_launch_chat():
|
def _offer_launch_chat(auto_launch = False):
|
||||||
"""Prompt the user to jump straight into chat after setup."""
|
"""Prompt the user to jump straight into chat after setup."""
|
||||||
print()
|
print()
|
||||||
if not prompt_yes_no("Launch hermes chat now?", True):
|
if not prompt_yes_no("Launch hermes chat now?", True):
|
||||||
|
|
@ -3109,7 +3118,7 @@ def _offer_launch_chat():
|
||||||
os.execvp(chat_argv[0], chat_argv)
|
os.execvp(chat_argv[0], chat_argv)
|
||||||
|
|
||||||
|
|
||||||
def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool):
|
def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool, nous_quick=False):
|
||||||
"""Streamlined first-time setup: provider + model only.
|
"""Streamlined first-time setup: provider + model only.
|
||||||
|
|
||||||
Applies sensible defaults for TTS (Edge), terminal (local), agent
|
Applies sensible defaults for TTS (Edge), terminal (local), agent
|
||||||
|
|
@ -3117,7 +3126,7 @@ def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool):
|
||||||
``hermes setup <section>``.
|
``hermes setup <section>``.
|
||||||
"""
|
"""
|
||||||
# Step 1: Model & Provider (essential — skips rotation/vision/TTS)
|
# Step 1: Model & Provider (essential — skips rotation/vision/TTS)
|
||||||
setup_model_provider(config, quick=True)
|
setup_model_provider(config, quick="nous_portal" if nous_quick else True )
|
||||||
|
|
||||||
# Step 2: Apply defaults for everything else
|
# Step 2: Apply defaults for everything else
|
||||||
_apply_default_agent_settings(config)
|
_apply_default_agent_settings(config)
|
||||||
|
|
@ -3150,7 +3159,9 @@ def _run_first_time_quick_setup(config: dict, hermes_home, is_existing: bool):
|
||||||
|
|
||||||
_print_setup_summary(config, hermes_home)
|
_print_setup_summary(config, hermes_home)
|
||||||
|
|
||||||
_offer_launch_chat()
|
# if the user hasn't set up the gateway, assume they want to launch chat.
|
||||||
|
force_launch_chat = gateway_choice == 0
|
||||||
|
_offer_launch_chat(force_launch_chat)
|
||||||
|
|
||||||
|
|
||||||
def _run_quick_setup(config: dict, hermes_home):
|
def _run_quick_setup(config: dict, hermes_home):
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,7 @@ def test_cmd_model_forwards_nous_login_tls_options(monkeypatch):
|
||||||
captured["ca_bundle"] = login_args.ca_bundle
|
captured["ca_bundle"] = login_args.ca_bundle
|
||||||
captured["insecure"] = login_args.insecure
|
captured["insecure"] = login_args.insecure
|
||||||
|
|
||||||
monkeypatch.setattr("hermes_cli.auth._login_nous", _fake_login)
|
monkeypatch.setattr("hermes_cli.auth.login_nous", _fake_login)
|
||||||
|
|
||||||
hermes_main.cmd_model(
|
hermes_main.cmd_model(
|
||||||
SimpleNamespace(
|
SimpleNamespace(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue