Skip to content

Commit d1cb209

Browse files
committed
replace inquirer with a better lib (mostly just to understand the cli/wiz flows, but also for searchable models)
use_shortcuts is a bummer with search, so bye make the filters behave use a real list of the providers we dont need preferred models honestly, it all lives in the lite llm package anyway cleanup linting/comment changes, move all models list to the other option cleanup cli/wizard implementation and move a util to utils get tests passing and do a little more pr noise cleanup formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise format manually something is weird. missing double-quote get tests passing and do a little more pr noise cleanup formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise formatting noise format manually something is weird.
1 parent 692b85b commit d1cb209

21 files changed

Lines changed: 7676 additions & 875 deletions

agentstack/auth.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import socket
88
from pathlib import Path
99

10-
import inquirer
10+
import questionary
1111
from appdirs import user_data_dir
1212
from agentstack import log
1313

@@ -73,6 +73,7 @@ def do_GET(self):
7373
self.end_headers()
7474
self.wfile.write(f'Error: {str(e)}'.encode())
7575

76+
7677
def find_free_port():
7778
"""Find a free port on localhost"""
7879
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
@@ -81,6 +82,7 @@ def find_free_port():
8182
port = s.getsockname()[1]
8283
return port
8384

85+
8486
def start_auth_server():
8587
"""Start the local authentication server"""
8688
port = find_free_port()
@@ -96,7 +98,7 @@ def login():
9698
token = get_stored_token()
9799
if token:
98100
log.success("You are already authenticated!")
99-
if not inquirer.confirm('Would you like to log in with a different account?'):
101+
if not questionary.confirm('Would you like to log in with a different account?').ask():
100102
return
101103

102104
# Start the local server
@@ -139,4 +141,4 @@ def get_stored_token():
139141
config = json.load(f)
140142
return config.get('bearer_token')
141143
except Exception:
142-
return None
144+
return None

agentstack/cli/cli.py

Lines changed: 86 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,63 @@
11
from typing import Optional
22
from art import text2art
3-
import inquirer
3+
import questionary
44
from agentstack import conf, log
55
from agentstack.conf import ConfigFile
6-
from agentstack.exceptions import ValidationError
7-
from agentstack.utils import validator_not_empty, is_snake_case
86
from agentstack.generation import InsertionPoint
97
from agentstack import repo
10-
from agentstack.providers import get_available_models, get_all_available_models
8+
from agentstack.providers import get_available_models
9+
from agentstack.utils import is_snake_case
10+
11+
PREFERRED_MODELS = [
12+
'groq/deepseek-r1-distill-llama-70b',
13+
'deepseek/deepseek-chat',
14+
'deepseek/deepseek-coder',
15+
'deepseek/deepseek-reasoner',
16+
'openai/gpt-4o',
17+
'anthropic/claude-3-5-sonnet',
18+
'openai/o1-preview',
19+
'openai/gpt-4-turbo',
20+
'anthropic/claude-3-opus',
21+
]
22+
23+
24+
def get_validated_input(
25+
message: str,
26+
validate_func=None,
27+
min_length: int = 0,
28+
snake_case: bool = False,
29+
) -> str:
30+
"""Helper function to get validated input from user.
31+
32+
Args:
33+
message: The prompt message to display
34+
validate_func: Optional custom validation function that returns (bool, str)
35+
min_length: Minimum length requirement (0 for no requirement)
36+
snake_case: Whether to enforce snake_case naming
37+
"""
38+
while True:
39+
40+
def validate(text: str) -> str | bool:
41+
if min_length and len(text) < min_length:
42+
return f"Input must be at least {min_length} characters long"
43+
44+
if snake_case and not is_snake_case(text):
45+
return "Input must be in snake_case format (lowercase with underscores)"
46+
47+
if validate_func:
48+
is_valid, error_msg = validate_func(text)
49+
if not is_valid:
50+
return error_msg
51+
52+
return True
53+
54+
value = questionary.text(
55+
message,
56+
validate=validate if validate_func or min_length or snake_case else None,
57+
).ask()
58+
59+
if value:
60+
return value
1161

1262

1363
def welcome_message():
@@ -31,10 +81,10 @@ def undo() -> None:
3181
log.warning("There are uncommitted changes that may be overwritten.")
3282
for changed in changed_files:
3383
log.info(f" - {changed}")
34-
should_continue = inquirer.confirm(
35-
message="Do you want to continue?",
84+
should_continue = questionary.confirm(
85+
"Do you want to continue?",
3686
default=False,
37-
)
87+
).ask()
3888
if not should_continue:
3989
return
4090

@@ -50,67 +100,38 @@ def configure_default_model():
50100

51101
log.info("Project does not have a default model configured.")
52102

53-
while True:
54-
# Get models from litellm
55-
preferred_models = get_available_models()
56-
all_models = get_all_available_models()
57-
58-
other_msg = "Other (enter a model name)"
59-
advanced_msg = f"Select from {len(all_models)} models for advanced use cases"
60-
61-
model = inquirer.list_input(
62-
message="Which model would you like to use?",
63-
choices=preferred_models + [advanced_msg, other_msg],
64-
)
65-
66-
if model == other_msg:
67-
log.info('A list of available models is available at: "https://docs.litellm.ai/docs/providers"')
68-
model = inquirer.text(message="Enter the model name")
69-
break
70-
71-
elif model == advanced_msg:
72-
return_msg = "↩ Return to preferred models"
73-
advanced_model = inquirer.list_input(
74-
message="Select from all available models",
75-
choices=[return_msg] + all_models,
76-
)
77-
78-
if advanced_model == return_msg:
79-
continue # Go back to preferred models list
80-
81-
model = advanced_model
82-
break
83-
84-
else:
85-
break # Selected from preferred models
103+
# First question - show preferred models + "Other" option
104+
other_msg = "Other (see all available models)"
105+
model_choice = questionary.select(
106+
"Which model would you like to use?",
107+
choices=PREFERRED_MODELS + [other_msg],
108+
use_indicator=True,
109+
use_shortcuts=False,
110+
use_jk_keys=False,
111+
use_emacs_keys=False,
112+
use_arrow_keys=True,
113+
use_search_filter=True,
114+
).ask()
115+
116+
# If they choose "Other", show searchable all available models
117+
if model_choice == other_msg:
118+
log.info('\nA complete list of models is available at: "https://docs.litellm.ai/docs/providers"')
119+
available_models = get_available_models()
120+
121+
model_choice = questionary.select(
122+
"Select from all available models:",
123+
choices=available_models,
124+
use_indicator=True,
125+
use_shortcuts=False,
126+
use_jk_keys=False,
127+
use_emacs_keys=False,
128+
use_arrow_keys=True,
129+
use_search_filter=True,
130+
).ask()
86131

87132
log.debug("Writing default model to project config.")
88133
with ConfigFile() as agentstack_config:
89-
agentstack_config.default_model = model
90-
91-
92-
def get_validated_input(
93-
message: str,
94-
validate_func=None,
95-
min_length: int = 0,
96-
snake_case: bool = False,
97-
) -> str:
98-
"""Helper function to get validated input from user.
99-
100-
Args:
101-
message: The prompt message to display
102-
validate_func: Optional custom validation function
103-
min_length: Minimum length requirement (0 for no requirement)
104-
snake_case: Whether to enforce snake_case naming
105-
"""
106-
while True:
107-
value = inquirer.text(
108-
message=message,
109-
validate=validate_func or validator_not_empty(min_length) if min_length else None,
110-
)
111-
if snake_case and not is_snake_case(value):
112-
raise ValidationError("Input must be in snake_case")
113-
return value
134+
agentstack_config.default_model = model_choice
114135

115136

116137
def parse_insertion_point(position: Optional[str] = None) -> Optional[InsertionPoint]:

agentstack/cli/init.py

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import os, sys
1+
import os
2+
import sys
23
from typing import Optional
3-
from pathlib import Path
4-
import inquirer
4+
import questionary
55
from textwrap import shorten
66

77
from agentstack import conf, log
@@ -38,12 +38,12 @@ def require_uv():
3838

3939
def prompt_slug_name() -> str:
4040
"""Prompt the user for a project name."""
41-
41+
4242
def _validate(slug_name: Optional[str]) -> bool:
4343
if not slug_name:
4444
log.error("Project name cannot be empty")
4545
return False
46-
46+
4747
if not is_snake_case(slug_name):
4848
log.error("Project name must be snake_case")
4949
return False
@@ -53,22 +53,12 @@ def _validate(slug_name: Optional[str]) -> bool:
5353
return False
5454

5555
return True
56-
57-
def _prompt() -> str:
58-
return inquirer.text(
59-
message="Project name (snake_case)",
60-
)
61-
56+
6257
log.info(
6358
"Provide a project name. This will be used to create a new directory in the "
6459
"current path and will be used as the project name. 🐍 Must be snake_case."
6560
)
66-
slug_name = None
67-
while not _validate(slug_name):
68-
slug_name = _prompt()
69-
70-
assert slug_name # appease type checker
71-
return slug_name
61+
return questionary.text("Project name (snake_case)", validate=_validate).ask()
7262

7363

7464
def select_template(slug_name: str, framework: Optional[str] = None) -> TemplateConfig:
@@ -77,16 +67,23 @@ def select_template(slug_name: str, framework: Optional[str] = None) -> Template
7767

7868
EMPTY = 'empty'
7969
choices = [
80-
(EMPTY, "🆕 Empty Project"),
70+
questionary.Choice('🆕 Empty Project', EMPTY),
8171
]
8272
for template in templates:
83-
choices.append((template.name, shorten(f"⚡️ {template.name} - {template.description}", 80)))
73+
choices.append(
74+
questionary.Choice(f"⚡️ {template.name} - {shorten(template.description, 80)}", template.name)
75+
)
8476

85-
choice = inquirer.list_input(
86-
message="Do you want to start with a template?",
87-
choices=[c[1] for c in choices],
88-
)
89-
template_name = next(c[0] for c in choices if c[1] == choice)
77+
template_name = questionary.select(
78+
"Do you want to start with a template?",
79+
choices=choices,
80+
use_indicator=True,
81+
use_shortcuts=False,
82+
use_jk_keys=False,
83+
use_emacs_keys=False,
84+
use_arrow_keys=True,
85+
use_search_filter=True,
86+
).ask()
9087

9188
if template_name == EMPTY:
9289
return TemplateConfig(
@@ -148,11 +145,11 @@ def init_project(
148145

149146
if framework is None:
150147
framework = template_data.framework
151-
148+
152149
if framework in frameworks.ALIASED_FRAMEWORKS:
153150
framework = frameworks.ALIASED_FRAMEWORKS[framework]
154-
155-
if not framework in frameworks.SUPPORTED_FRAMEWORKS:
151+
152+
if framework not in frameworks.SUPPORTED_FRAMEWORKS:
156153
raise Exception(f"Framework '{framework}' is not supported.")
157154
log.info(f"Using framework: {framework}")
158155

@@ -163,7 +160,7 @@ def init_project(
163160
packaging.create_venv()
164161
log.info("Installing dependencies...")
165162
packaging.install_project()
166-
163+
167164
if repo.find_parent_repo(conf.PATH):
168165
# if a repo already exists, we don't want to initialize a new one
169166
log.info("Found existing git repository; disabling tracking.")

agentstack/cli/run.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from agentstack.exceptions import ValidationError
1111
from agentstack import inputs
1212
from agentstack import frameworks
13-
from agentstack.utils import get_framework, verify_agentstack_project
13+
from agentstack.utils import verify_agentstack_project
1414

1515
MAIN_FILENAME: Path = Path("src/main.py")
1616
MAIN_MODULE_NAME = "main"
@@ -98,7 +98,7 @@ def run_project(command: str = 'run', cli_args: Optional[List[str]] = None):
9898
"""Validate that the project is ready to run and then run it."""
9999
conf.assert_project()
100100
verify_agentstack_project()
101-
101+
102102
if conf.get_framework() not in frameworks.SUPPORTED_FRAMEWORKS:
103103
raise ValidationError(f"Framework {conf.get_framework()} is not supported by agentstack.")
104104

@@ -124,7 +124,7 @@ def run_project(command: str = 'run', cli_args: Optional[List[str]] = None):
124124
log.notify("Running your agent...")
125125
project_main = _import_project_module(conf.PATH)
126126
main = getattr(project_main, command)
127-
127+
128128
# handle both async and sync entrypoints
129129
if asyncio.iscoroutinefunction(main):
130130
asyncio.run(main())

0 commit comments

Comments
 (0)