Skip to content

Commit a4b6d00

Browse files
praisonai-triage-agent[bot]MervinPraisonCopilot
authored
feat: add frictionless onboarding with praisonai setup wizard and post-install hooks (#1453)
* feat: implement frictionless onboarding with praisonai setup wizard - Add interactive setup command with provider selection (OpenAI, Anthropic, Google, Ollama) - Support both interactive and non-interactive modes - Secure .env file creation with 600 permissions - Idempotent operation (safe to re-run) - Create ~/.praisonai directory structure (logs, sessions) - Modify install.sh and install.ps1 for post-install onboarding - Add --no-onboard flag to skip wizard - Add PraisonAIConfigError for missing API keys - Update doctor command to suggest 'praisonai setup' - Include comprehensive unit and integration tests Fixes #1451 Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com> * fix: harden windows installer command execution and stabilize setup test Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/8c683e28-66c2-42f7-abc2-08efd37b22c7 Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com> * fix: improve powershell python command parsing diagnostics Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/8c683e28-66c2-42f7-abc2-08efd37b22c7 Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com> * fix: comprehensive setup wizard security and robustness improvements - Add remediation hints to PraisonAIConfigError for better guidance - Fix setup reset to remove both .env and config.yaml files - Create directories with secure 700 permissions - Enhance non-interactive validation (prefer env vars, require model) - Use atomic .env file creation with secure permissions - Fix starter YAML to use selected model instead of hardcoded gpt-4o-mini - Add TTY/interactive checks to PowerShell installer - Improve shell script python command precedence - Remove unnecessary f-string prefixes (ruff F541) - Make tests platform-independent and Windows-compatible - Export PraisonAIConfigError in errors.__all__ Addresses CodeRabbit security and architecture findings. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Mervin Praison <MervinPraison@users.noreply.github.com> --------- Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com> Co-authored-by: MervinPraison <MervinPraison@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
1 parent d8e52bb commit a4b6d00

13 files changed

Lines changed: 1193 additions & 15 deletions

File tree

src/praisonai-agents/praisonaiagents/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ def _get_lazy_cache():
212212
'LLMError': ('praisonaiagents.errors', 'LLMError'),
213213
'ValidationError': ('praisonaiagents.errors', 'ValidationError'),
214214
'NetworkError': ('praisonaiagents.errors', 'NetworkError'),
215+
'PraisonAIConfigError': ('praisonaiagents.errors', 'PraisonAIConfigError'),
215216
'ErrorContextProtocol': ('praisonaiagents.errors', 'ErrorContextProtocol'),
216217
'Heartbeat': ('praisonaiagents.agent.heartbeat', 'Heartbeat'),
217218
'HeartbeatConfig': ('praisonaiagents.agent.heartbeat', 'HeartbeatConfig'),

src/praisonai-agents/praisonaiagents/errors.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,44 @@ def __init__(
292292
self.target_agent = target_agent
293293

294294

295+
class PraisonAIConfigError(PraisonAIError):
296+
"""
297+
Configuration error (missing API keys, invalid settings, etc).
298+
299+
Usually indicates setup issues that require user action.
300+
"""
301+
302+
def __init__(
303+
self,
304+
message: str,
305+
config_key: Optional[str] = None,
306+
agent_id: str = "unknown",
307+
run_id: Optional[str] = None,
308+
is_retryable: bool = False, # Config errors need user intervention
309+
remediation_hint: Optional[str] = None,
310+
context: Optional[Dict[str, Any]] = None
311+
):
312+
context = context or {}
313+
if config_key:
314+
context.update({"config_key": config_key})
315+
if remediation_hint is None:
316+
remediation_hint = f"Set {config_key} or run the setup wizard before retrying."
317+
if remediation_hint:
318+
context["remediation_hint"] = remediation_hint
319+
message = f"{message} Remediation: {remediation_hint}"
320+
321+
super().__init__(
322+
message,
323+
agent_id=agent_id,
324+
run_id=run_id,
325+
error_category="validation", # Configuration is a type of validation error
326+
is_retryable=is_retryable,
327+
context=context
328+
)
329+
self.config_key = config_key
330+
self.remediation_hint = remediation_hint
331+
332+
295333
# Specialized handoff errors (maintain backward compatibility)
296334
class HandoffCycleError(HandoffError):
297335
"""Circular handoff dependency detected."""
@@ -341,5 +379,6 @@ def __init__(self, message: str, timeout_seconds: Optional[float] = None, **kwar
341379
"HandoffError",
342380
"HandoffCycleError",
343381
"HandoffDepthError",
344-
"HandoffTimeoutError"
382+
"HandoffTimeoutError",
383+
"PraisonAIConfigError"
345384
]

src/praisonai/praisonai/cli/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ def register_commands():
280280
from .commands.lsp import app as lsp_app
281281
from .commands.diag import app as diag_app
282282
from .commands.doctor import app as doctor_app
283+
from .commands.setup import app as setup_app
283284
from .commands.obs import app as obs_app
284285
from .commands.acp import app as acp_app
285286
from .commands.mcp import app as mcp_app
@@ -359,6 +360,7 @@ def register_commands():
359360
app.add_typer(lsp_app, name="lsp", help="LSP service lifecycle")
360361
app.add_typer(diag_app, name="diag", help="Diagnostics export")
361362
app.add_typer(doctor_app, name="doctor", help="Health checks and diagnostics")
363+
app.add_typer(setup_app, name="setup", help="Interactive onboarding / configuration wizard")
362364
app.add_typer(obs_app, name="obs", help="Observability diagnostics and management")
363365
app.add_typer(acp_app, name="acp", help="Agent Client Protocol server")
364366
app.add_typer(mcp_app, name="mcp", help="MCP server management")

src/praisonai/praisonai/cli/commands/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
'lsp_app',
2121
'diag_app',
2222
'doctor_app',
23+
'setup_app',
2324
'acp_app',
2425
'mcp_app',
2526
'rag_app',
@@ -72,6 +73,9 @@ def __getattr__(name: str):
7273
elif name == 'doctor_app':
7374
from .doctor import app as doctor_app
7475
return doctor_app
76+
elif name == 'setup_app':
77+
from .setup import app as setup_app
78+
return setup_app
7579
elif name == 'acp_app':
7680
from .acp import app as acp_app
7781
return acp_app
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
"""
2+
Setup command group for PraisonAI CLI.
3+
4+
Provides interactive onboarding and configuration wizard.
5+
"""
6+
7+
import os
8+
from pathlib import Path
9+
from typing import Optional
10+
11+
import typer
12+
13+
from ..output.console import get_output_controller
14+
15+
app = typer.Typer(help="Interactive onboarding / configuration wizard")
16+
17+
# Default PRAISON_HOME directory
18+
def get_praison_home() -> Path:
19+
"""Get the PraisonAI home directory."""
20+
home = os.getenv("PRAISONAI_HOME")
21+
if home:
22+
return Path(home)
23+
return Path.home() / ".praisonai"
24+
25+
PRAISON_HOME = get_praison_home()
26+
ENV_FILE = PRAISON_HOME / ".env"
27+
28+
# Provider configurations
29+
PROVIDERS = {
30+
"1": ("openai", "OPENAI_API_KEY", "gpt-4o-mini"),
31+
"2": ("anthropic", "ANTHROPIC_API_KEY", "claude-3-5-sonnet-latest"),
32+
"3": ("google", "GEMINI_API_KEY", "gemini-2.0-flash"),
33+
"4": ("ollama", None, "llama3.2"),
34+
"5": ("custom", None, None),
35+
}
36+
37+
PROVIDER_NAMES = {
38+
"openai": ("OpenAI", "OPENAI_API_KEY", "gpt-4o-mini"),
39+
"anthropic": ("Anthropic", "ANTHROPIC_API_KEY", "claude-3-5-sonnet-latest"),
40+
"google": ("Google", "GEMINI_API_KEY", "gemini-2.0-flash"),
41+
"ollama": ("Ollama", None, "llama3.2"),
42+
"custom": ("Custom", None, None),
43+
}
44+
45+
46+
def _run_setup(
47+
non_interactive: bool = False,
48+
provider: Optional[str] = None,
49+
api_key: Optional[str] = None,
50+
model: Optional[str] = None,
51+
) -> int:
52+
"""Run the setup wizard."""
53+
try:
54+
from ..features.setup.handler import SetupHandler
55+
handler = SetupHandler()
56+
return handler.execute(
57+
non_interactive=non_interactive,
58+
provider=provider,
59+
api_key=api_key,
60+
model=model
61+
)
62+
except ImportError as e:
63+
output = get_output_controller()
64+
output.print_error(f"Setup module not available: {e}")
65+
return 4
66+
except Exception as e:
67+
output = get_output_controller()
68+
output.print_error(f"Setup error: {e}")
69+
return 1
70+
71+
72+
@app.callback(invoke_without_command=True)
73+
def setup_callback(
74+
ctx: typer.Context,
75+
non_interactive: bool = typer.Option(False, "--non-interactive", help="Run in non-interactive mode"),
76+
provider: Optional[str] = typer.Option(None, "--provider", help="LLM provider (openai, anthropic, google, ollama, custom)"),
77+
api_key: Optional[str] = typer.Option(None, "--api-key", help="API key for the provider"),
78+
model: Optional[str] = typer.Option(None, "--model", help="Default model to use"),
79+
):
80+
"""Run the onboarding wizard (idempotent — safe to re-run)."""
81+
if ctx.invoked_subcommand:
82+
return
83+
84+
exit_code = _run_setup(
85+
non_interactive=non_interactive,
86+
provider=provider,
87+
api_key=api_key,
88+
model=model
89+
)
90+
raise typer.Exit(exit_code)
91+
92+
93+
@app.command("wizard")
94+
def setup_wizard(
95+
provider: Optional[str] = typer.Option(None, "--provider", help="LLM provider"),
96+
api_key: Optional[str] = typer.Option(None, "--api-key", help="API key"),
97+
model: Optional[str] = typer.Option(None, "--model", help="Default model"),
98+
):
99+
"""Run the interactive setup wizard."""
100+
exit_code = _run_setup(
101+
non_interactive=False,
102+
provider=provider,
103+
api_key=api_key,
104+
model=model
105+
)
106+
raise typer.Exit(exit_code)
107+
108+
109+
@app.command("config")
110+
def setup_config(
111+
show: bool = typer.Option(False, "--show", help="Show current configuration"),
112+
edit: bool = typer.Option(False, "--edit", help="Edit configuration file"),
113+
):
114+
"""Manage setup configuration."""
115+
output = get_output_controller()
116+
117+
if show:
118+
if ENV_FILE.exists():
119+
output.console.print(f"[bold]Configuration at {ENV_FILE}:[/bold]")
120+
content = ENV_FILE.read_text()
121+
# Don't show actual API keys for security
122+
lines = []
123+
for line in content.split('\n'):
124+
if '=' in line and any(key in line for key in ['API_KEY', 'TOKEN', 'SECRET']):
125+
key, _ = line.split('=', 1)
126+
lines.append(f"{key}=***")
127+
else:
128+
lines.append(line)
129+
output.console.print('\n'.join(lines))
130+
else:
131+
output.print_warning(f"No configuration found at {ENV_FILE}")
132+
output.console.print("Run [cyan]praisonai setup[/cyan] to create one.")
133+
134+
if edit:
135+
import subprocess
136+
editor = os.getenv("EDITOR", "nano")
137+
try:
138+
subprocess.run([editor, str(ENV_FILE)], check=True)
139+
except subprocess.CalledProcessError:
140+
output.print_error(f"Failed to open editor: {editor}")
141+
except FileNotFoundError:
142+
output.print_error(f"Editor not found: {editor}")
143+
144+
145+
@app.command("reset")
146+
def setup_reset(
147+
force: bool = typer.Option(False, "--force", help="Skip confirmation"),
148+
):
149+
"""Reset setup configuration."""
150+
output = get_output_controller()
151+
152+
praison_home = get_praison_home()
153+
env_file = praison_home / ".env"
154+
config_file = praison_home / "config.yaml"
155+
files_to_remove = [path for path in (env_file, config_file) if path.exists()]
156+
157+
if not files_to_remove:
158+
output.print_info("No setup configuration to reset.")
159+
return
160+
161+
if not force:
162+
confirm = typer.confirm(f"Reset configuration at {praison_home}?")
163+
if not confirm:
164+
output.print_info("Reset cancelled.")
165+
return
166+
167+
try:
168+
for path in files_to_remove:
169+
path.unlink()
170+
output.print_success("Configuration reset successfully.")
171+
output.console.print("Run [cyan]praisonai setup[/cyan] to configure again.")
172+
except Exception as e:
173+
output.print_error(f"Failed to reset configuration: {e}")
174+
raise typer.Exit(1)

src/praisonai/praisonai/cli/features/doctor/checks/env_checks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def check_openai_api_key(config: DoctorConfig) -> CheckResult:
181181
category=CheckCategory.ENVIRONMENT,
182182
status=CheckStatus.FAIL,
183183
message="OPENAI_API_KEY not configured and no alternative providers found",
184-
remediation="Set OPENAI_API_KEY environment variable or configure an alternative provider",
184+
remediation="Run 'praisonai setup' to configure API keys, or set OPENAI_API_KEY environment variable",
185185
severity=CheckSeverity.HIGH,
186186
)
187187

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""
2+
Setup feature module for PraisonAI CLI.
3+
4+
Provides interactive onboarding and configuration management.
5+
"""
6+
7+
from .handler import SetupHandler
8+
9+
__all__ = ["SetupHandler"]

0 commit comments

Comments
 (0)