Skip to content

Commit 517fb20

Browse files
committed
[feat][cli-tips][add rotating tips command and module][feat][cli-models][add model discovery command
via litellm][feat][typo-correction][suggest closest command on typo][feat][error-hints][classify errors with recovery hints][improvement][next-step-tip][show contextual tip after init and setup-check][improvement][cli-banner][refactor banner to use shared tip module]
1 parent 7b3eedc commit 517fb20

4 files changed

Lines changed: 778 additions & 97 deletions

File tree

swarms/cli/main.py

Lines changed: 260 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
"""
1818

1919
import argparse
20+
import difflib
2021
import getpass
2122
import os
2223
import subprocess
24+
import sys
2325
from pathlib import Path
2426
from typing import Any, Dict, List, Optional, Union
2527

@@ -56,6 +58,36 @@
5658
load_swarms_env()
5759

5860

61+
# Single source of truth for valid CLI commands. Used by argparse for
62+
# strict validation and by the typo-correction pre-check in main() to
63+
# suggest the closest match when a user mistypes a command.
64+
COMMAND_CHOICES: List[str] = [
65+
"init",
66+
"onboarding",
67+
"get-api-key",
68+
"check-login",
69+
"run-agents",
70+
"load-markdown",
71+
"agent",
72+
"chat",
73+
"upgrade",
74+
"autoswarm",
75+
"setup-check",
76+
"llm-council",
77+
"heavy-swarm",
78+
"tips",
79+
"models",
80+
]
81+
82+
83+
def _suggest_command(typed: str) -> Optional[str]:
84+
"""Return the closest valid command, or None if nothing is close enough."""
85+
matches = difflib.get_close_matches(
86+
typed, COMMAND_CHOICES, n=1, cutoff=0.5
87+
)
88+
return matches[0] if matches else None
89+
90+
5991
def run_autoswarm(
6092
task: str,
6193
model: str,
@@ -942,26 +974,11 @@ def setup_argument_parser() -> argparse.ArgumentParser:
942974
action=CustomHelpAction,
943975
help="Show this help message and exit",
944976
)
945-
command_choices = [
946-
"init",
947-
"onboarding",
948-
"get-api-key",
949-
"check-login",
950-
"run-agents",
951-
"load-markdown",
952-
"agent",
953-
"chat",
954-
"upgrade",
955-
"autoswarm",
956-
"setup-check",
957-
"llm-council",
958-
"heavy-swarm",
959-
]
960977
parser.add_argument(
961978
"command",
962979
metavar="COMMAND",
963-
choices=command_choices,
964-
help=f"Command to execute. Available commands: {', '.join(command_choices)}",
980+
choices=COMMAND_CHOICES,
981+
help=f"Command to execute. Available commands: {', '.join(COMMAND_CHOICES)}",
965982
)
966983
parser.add_argument(
967984
"--yaml-file",
@@ -1159,6 +1176,44 @@ def setup_argument_parser() -> argparse.ArgumentParser:
11591176
default=None,
11601177
help="Directory for 'swarms init' (default: prompted interactively)",
11611178
)
1179+
# Tips command arguments
1180+
parser.add_argument(
1181+
"--count",
1182+
type=int,
1183+
default=1,
1184+
help="Number of tips to show (used with 'swarms tips')",
1185+
)
1186+
parser.add_argument(
1187+
"--category",
1188+
type=str,
1189+
default=None,
1190+
help="Restrict tips to a category: commands, agents, swarms, models, pro, trivia, env, community",
1191+
)
1192+
parser.add_argument(
1193+
"--all",
1194+
dest="all_tips",
1195+
action="store_true",
1196+
help="Show every tip in the selected category (or every category when no --category is given)",
1197+
)
1198+
# Models command arguments
1199+
parser.add_argument(
1200+
"--search",
1201+
type=str,
1202+
default=None,
1203+
help="Fuzzy-search the model list (used with 'swarms models')",
1204+
)
1205+
parser.add_argument(
1206+
"--info",
1207+
type=str,
1208+
default=None,
1209+
help="Show full metadata for a single model (used with 'swarms models')",
1210+
)
1211+
parser.add_argument(
1212+
"--provider",
1213+
type=str,
1214+
default=None,
1215+
help="Restrict 'swarms models' to one provider (e.g. openai, anthropic)",
1216+
)
11621217
return parser
11631218

11641219

@@ -1998,6 +2053,81 @@ def handle_init(args: argparse.Namespace) -> None:
19982053
)
19992054
)
20002055

2056+
# Contextual next-step tip
2057+
from swarms.cli.tips import render_tip
2058+
2059+
console.print()
2060+
console.print(render_tip(category="commands"))
2061+
console.print()
2062+
2063+
2064+
def handle_tips(args: argparse.Namespace) -> None:
2065+
"""
2066+
Display random tips & tricks for the Swarms CLI.
2067+
2068+
Flags:
2069+
--count N Show N random tips (default: 1).
2070+
--category CAT Restrict to one category (e.g. agents, swarms, pro).
2071+
--all Show every tip in the (optionally filtered) pool.
2072+
"""
2073+
from swarms.cli.tips import (
2074+
TIP_CATEGORIES,
2075+
list_categories,
2076+
render_random_tips,
2077+
render_tip,
2078+
)
2079+
2080+
category = getattr(args, "category", None)
2081+
show_all = getattr(args, "all_tips", False)
2082+
n = max(1, getattr(args, "count", 1) or 1)
2083+
2084+
try:
2085+
if show_all:
2086+
categories = [category] if category else list_categories()
2087+
for cat in categories:
2088+
console.print(f"\n[bold red]── {cat} ──[/bold red]")
2089+
for tip_body in TIP_CATEGORIES[cat]:
2090+
console.print(render_tip(body=tip_body))
2091+
return
2092+
2093+
for line in render_random_tips(n=n, category=category):
2094+
console.print(line)
2095+
except ValueError as e:
2096+
show_error(
2097+
"Unknown tip category",
2098+
f"{e}\nRun 'swarms tips' with no --category to see a random tip from any pool.",
2099+
)
2100+
2101+
2102+
def handle_models(args: argparse.Namespace) -> None:
2103+
"""
2104+
Discover and inspect LLM models available via LiteLLM.
2105+
2106+
Flags:
2107+
--search PATTERN Fuzzy-match against the model list.
2108+
--info NAME Show full metadata for one model.
2109+
--provider NAME Filter the list to one provider.
2110+
2111+
With no flag, prints every model grouped by provider.
2112+
"""
2113+
from swarms.cli.models import (
2114+
list_models,
2115+
search_models,
2116+
show_model_info,
2117+
)
2118+
2119+
info_name = getattr(args, "info", None)
2120+
search_pattern = getattr(args, "search", None)
2121+
provider = getattr(args, "provider", None)
2122+
2123+
if info_name:
2124+
show_model_info(console, info_name)
2125+
return
2126+
if search_pattern:
2127+
search_models(console, search_pattern)
2128+
return
2129+
list_models(console, provider=provider)
2130+
20012131

20022132
def route_command(args: argparse.Namespace) -> None:
20032133
"""
@@ -2035,6 +2165,8 @@ def route_command(args: argparse.Namespace) -> None:
20352165
),
20362166
"llm-council": handle_llm_council,
20372167
"heavy-swarm": handle_heavy_swarm,
2168+
"tips": handle_tips,
2169+
"models": handle_models,
20382170
}
20392171

20402172
handler = command_handlers.get(args.command)
@@ -2047,6 +2179,95 @@ def route_command(args: argparse.Namespace) -> None:
20472179
)
20482180

20492181

2182+
def _classify_command_error(message: str) -> List[str]:
2183+
"""
2184+
Inspect an exception message and return a list of targeted recovery hints.
2185+
2186+
Matches are case-insensitive and additive: multiple hints can fire for a
2187+
single error (e.g. a missing-key error that also mentions a stale model).
2188+
"""
2189+
err = message.lower()
2190+
hints: List[str] = []
2191+
2192+
if (
2193+
"401" in err
2194+
or "unauthorized" in err
2195+
or "authenticationerror" in err
2196+
or "invalid api key" in err
2197+
or "missing api key" in err
2198+
):
2199+
hints.append(
2200+
"Authentication failed — set your provider key in [bold]swarms init[/bold] "
2201+
"or get one via [bold]swarms get-api-key[/bold]"
2202+
)
2203+
2204+
if (
2205+
"model not found" in err
2206+
or "model_not_found" in err
2207+
or ("model" in err and "does not exist" in err)
2208+
):
2209+
hints.append(
2210+
"Find a valid model with [bold]swarms models --search <name>[/bold] "
2211+
"or list them all with [bold]swarms models[/bold]"
2212+
)
2213+
2214+
if "workspace_dir" in err or "workspace directory" in err:
2215+
hints.append(
2216+
"Workspace not configured — run [bold]swarms init[/bold] to scaffold one"
2217+
)
2218+
2219+
if "rate limit" in err or "ratelimit" in err or "429" in err:
2220+
hints.append(
2221+
"Rate-limited by provider — slow down, use a smaller model with "
2222+
"[bold]--model-name[/bold], or retry in a minute"
2223+
)
2224+
2225+
if (
2226+
"connection" in err
2227+
or "timeout" in err
2228+
or "timed out" in err
2229+
or "network" in err
2230+
):
2231+
hints.append(
2232+
"Network issue — check connectivity, then run [bold]swarms setup-check --verbose[/bold]"
2233+
)
2234+
2235+
if "modulenotfounderror" in err or "no module named" in err:
2236+
hints.append(
2237+
"Missing dependency — try [bold]swarms upgrade[/bold] or "
2238+
"[bold]pip install -U swarms[/bold]"
2239+
)
2240+
2241+
return hints
2242+
2243+
2244+
def _show_command_error(error: Exception) -> None:
2245+
"""Render a command-execution error with classified recovery hints."""
2246+
hints = _classify_command_error(str(error))
2247+
2248+
hint_block = ""
2249+
if hints:
2250+
hint_block = (
2251+
"[bold white]Suggested next steps:[/bold white]\n"
2252+
+ "\n".join(f" • {h}" for h in hints)
2253+
)
2254+
else:
2255+
hint_block = (
2256+
"[bold white]Troubleshooting tips:[/bold white]\n"
2257+
"- Double-check your arguments and the command structure\n"
2258+
"- Try [bold]swarms --help[/bold] for command details\n"
2259+
"- Run [bold]swarms setup-check --verbose[/bold] to validate your environment\n"
2260+
"- Report bugs at https://github.com/kyegomez/swarms/issues"
2261+
)
2262+
2263+
console.print(
2264+
f"\n[{COLORS['error']}]Oops! An unexpected error occurred while running your command:[/{COLORS['error']}]\n"
2265+
f"[bold]{rich_escape(str(error))}[/bold]\n\n"
2266+
f"{hint_block}\n\n"
2267+
f"[dim]Traceback:[/dim]\n{rich_escape(traceback.format_exc())}"
2268+
)
2269+
2270+
20502271
def main() -> None:
20512272
"""
20522273
Main entry point for the Swarms CLI.
@@ -2076,21 +2297,34 @@ def main() -> None:
20762297
try:
20772298
show_ascii_art()
20782299

2300+
# Typo correction: catch mistyped command name before argparse
2301+
# rejects it with a generic "invalid choice" error.
2302+
if (
2303+
len(sys.argv) > 1
2304+
and not sys.argv[1].startswith("-")
2305+
and sys.argv[1] not in COMMAND_CHOICES
2306+
):
2307+
typed = sys.argv[1]
2308+
suggestion = _suggest_command(typed)
2309+
hint = (
2310+
f"\nDid you mean [bold white]swarms {suggestion}[/bold white]?"
2311+
if suggestion
2312+
else ""
2313+
)
2314+
show_error(
2315+
f"Unknown command '{typed}'",
2316+
f"Available commands: {', '.join(COMMAND_CHOICES)}{hint}\n"
2317+
f"Run [bold white]swarms --help[/bold white] for full usage.",
2318+
)
2319+
return
2320+
20792321
parser = setup_argument_parser()
20802322
args = parser.parse_args()
20812323

20822324
try:
20832325
route_command(args)
20842326
except Exception as e:
2085-
console.print(
2086-
f"\n[{COLORS['error']}]Oops! An unexpected error occurred while running your command:[/{COLORS['error']}]\n"
2087-
f"[bold]{rich_escape(str(e))}[/bold]\n\n"
2088-
"[bold white]Troubleshooting tips:[/bold white]\n"
2089-
"- Double-check your arguments and the command structure\n"
2090-
"- Try 'swarms help' for command details and examples\n"
2091-
"- If the issue persists, please report it at https://github.com/OpenAgentsInc/swarms/issues\n\n"
2092-
f"[dim]Traceback:[/dim]\n{rich_escape(traceback.format_exc())}"
2093-
)
2327+
_show_command_error(e)
20942328
return
20952329
except Exception as error:
20962330
formatter.print_panel(

0 commit comments

Comments
 (0)