Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/praisonai-agents/praisonaiagents/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ def _get_lazy_cache():
'LLMError': ('praisonaiagents.errors', 'LLMError'),
'ValidationError': ('praisonaiagents.errors', 'ValidationError'),
'NetworkError': ('praisonaiagents.errors', 'NetworkError'),
'PraisonAIConfigError': ('praisonaiagents.errors', 'PraisonAIConfigError'),
'ErrorContextProtocol': ('praisonaiagents.errors', 'ErrorContextProtocol'),
'Heartbeat': ('praisonaiagents.agent.heartbeat', 'Heartbeat'),
'HeartbeatConfig': ('praisonaiagents.agent.heartbeat', 'HeartbeatConfig'),
Expand Down
41 changes: 40 additions & 1 deletion src/praisonai-agents/praisonaiagents/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,44 @@ def __init__(
self.target_agent = target_agent


class PraisonAIConfigError(PraisonAIError):
"""
Configuration error (missing API keys, invalid settings, etc).

Usually indicates setup issues that require user action.
"""

def __init__(
self,
message: str,
config_key: Optional[str] = None,
agent_id: str = "unknown",
run_id: Optional[str] = None,
is_retryable: bool = False, # Config errors need user intervention
remediation_hint: Optional[str] = None,
context: Optional[Dict[str, Any]] = None
):
context = context or {}
if config_key:
context.update({"config_key": config_key})
if remediation_hint is None:
remediation_hint = f"Set {config_key} or run the setup wizard before retrying."
if remediation_hint:
context["remediation_hint"] = remediation_hint
message = f"{message} Remediation: {remediation_hint}"

super().__init__(
message,
agent_id=agent_id,
run_id=run_id,
error_category="validation", # Configuration is a type of validation error
is_retryable=is_retryable,
context=context
)
self.config_key = config_key
Comment on lines +295 to +329
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a structured remediation hint to config errors.

PraisonAIConfigError captures config_key, but the rendered exception still relies entirely on caller-supplied text for next steps. Add an optional/default remediation hint so setup failures consistently tell users how to recover. As per coding guidelines, "Error handling: Fail fast with clear error messages; include remediation hints in exceptions; propagate context (agent name, tool name, session ID); provide hook points for error interception (on_error events)".

Proposed refinement
 class PraisonAIConfigError(PraisonAIError):
@@
     def __init__(
         self,
         message: str,
         config_key: Optional[str] = None,
         agent_id: str = "unknown",
         run_id: Optional[str] = None,
         is_retryable: bool = False,  # Config errors need user intervention
+        remediation_hint: Optional[str] = None,
         context: Optional[Dict[str, Any]] = None
     ):
         context = context or {}
         if config_key:
             context.update({"config_key": config_key})
+            if remediation_hint is None:
+                remediation_hint = f"Set {config_key} or run the setup wizard before retrying."
+        if remediation_hint:
+            context["remediation_hint"] = remediation_hint
+            message = f"{message} Remediation: {remediation_hint}"
         
         super().__init__(
             message,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/praisonai-agents/praisonaiagents/errors.py` around lines 295 - 323,
PraisonAIConfigError should carry a structured remediation hint: add an optional
parameter remediation_hint: Optional[str] = None to the PraisonAIConfigError
__init__, compute a sensible default (e.g., "Verify and set the {config_key}
configuration" when config_key is present) when remediation_hint is not
provided, include this hint in the error's context (context["remediation_hint"]
= remediation_hint) and set self.remediation_hint = remediation_hint, and ensure
the call to super().__init__ still receives the augmented context so downstream
renderers and on_error hooks (error interception points) can show consistent
recovery steps; update references to PraisonAIConfigError accordingly.

self.remediation_hint = remediation_hint


# Specialized handoff errors (maintain backward compatibility)
class HandoffCycleError(HandoffError):
"""Circular handoff dependency detected."""
Expand Down Expand Up @@ -341,5 +379,6 @@ def __init__(self, message: str, timeout_seconds: Optional[float] = None, **kwar
"HandoffError",
"HandoffCycleError",
"HandoffDepthError",
"HandoffTimeoutError"
"HandoffTimeoutError",
"PraisonAIConfigError"
]
2 changes: 2 additions & 0 deletions src/praisonai/praisonai/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ def register_commands():
from .commands.lsp import app as lsp_app
from .commands.diag import app as diag_app
from .commands.doctor import app as doctor_app
from .commands.setup import app as setup_app
from .commands.obs import app as obs_app
from .commands.acp import app as acp_app
from .commands.mcp import app as mcp_app
Expand Down Expand Up @@ -359,6 +360,7 @@ def register_commands():
app.add_typer(lsp_app, name="lsp", help="LSP service lifecycle")
app.add_typer(diag_app, name="diag", help="Diagnostics export")
app.add_typer(doctor_app, name="doctor", help="Health checks and diagnostics")
app.add_typer(setup_app, name="setup", help="Interactive onboarding / configuration wizard")
app.add_typer(obs_app, name="obs", help="Observability diagnostics and management")
app.add_typer(acp_app, name="acp", help="Agent Client Protocol server")
app.add_typer(mcp_app, name="mcp", help="MCP server management")
Expand Down
4 changes: 4 additions & 0 deletions src/praisonai/praisonai/cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
'lsp_app',
'diag_app',
'doctor_app',
'setup_app',
'acp_app',
'mcp_app',
'rag_app',
Expand Down Expand Up @@ -72,6 +73,9 @@ def __getattr__(name: str):
elif name == 'doctor_app':
from .doctor import app as doctor_app
return doctor_app
elif name == 'setup_app':
from .setup import app as setup_app
return setup_app
elif name == 'acp_app':
from .acp import app as acp_app
return acp_app
Expand Down
174 changes: 174 additions & 0 deletions src/praisonai/praisonai/cli/commands/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"""
Setup command group for PraisonAI CLI.

Provides interactive onboarding and configuration wizard.
"""

import os
from pathlib import Path
from typing import Optional

import typer

from ..output.console import get_output_controller

app = typer.Typer(help="Interactive onboarding / configuration wizard")

# Default PRAISON_HOME directory
def get_praison_home() -> Path:
"""Get the PraisonAI home directory."""
home = os.getenv("PRAISONAI_HOME")
if home:
return Path(home)
return Path.home() / ".praisonai"

PRAISON_HOME = get_praison_home()
ENV_FILE = PRAISON_HOME / ".env"

# Provider configurations
PROVIDERS = {
"1": ("openai", "OPENAI_API_KEY", "gpt-4o-mini"),
"2": ("anthropic", "ANTHROPIC_API_KEY", "claude-3-5-sonnet-latest"),
"3": ("google", "GEMINI_API_KEY", "gemini-2.0-flash"),
"4": ("ollama", None, "llama3.2"),
"5": ("custom", None, None),
}

PROVIDER_NAMES = {
"openai": ("OpenAI", "OPENAI_API_KEY", "gpt-4o-mini"),
"anthropic": ("Anthropic", "ANTHROPIC_API_KEY", "claude-3-5-sonnet-latest"),
"google": ("Google", "GEMINI_API_KEY", "gemini-2.0-flash"),
"ollama": ("Ollama", None, "llama3.2"),
"custom": ("Custom", None, None),
}


def _run_setup(
non_interactive: bool = False,
provider: Optional[str] = None,
api_key: Optional[str] = None,
model: Optional[str] = None,
) -> int:
"""Run the setup wizard."""
try:
from ..features.setup.handler import SetupHandler
handler = SetupHandler()
return handler.execute(
non_interactive=non_interactive,
provider=provider,
api_key=api_key,
model=model
)
except ImportError as e:
output = get_output_controller()
output.print_error(f"Setup module not available: {e}")
return 4
except Exception as e:
output = get_output_controller()
output.print_error(f"Setup error: {e}")
return 1


@app.callback(invoke_without_command=True)
def setup_callback(
ctx: typer.Context,
non_interactive: bool = typer.Option(False, "--non-interactive", help="Run in non-interactive mode"),
provider: Optional[str] = typer.Option(None, "--provider", help="LLM provider (openai, anthropic, google, ollama, custom)"),
api_key: Optional[str] = typer.Option(None, "--api-key", help="API key for the provider"),
model: Optional[str] = typer.Option(None, "--model", help="Default model to use"),
):
"""Run the onboarding wizard (idempotent — safe to re-run)."""
if ctx.invoked_subcommand:
return

exit_code = _run_setup(
non_interactive=non_interactive,
provider=provider,
api_key=api_key,
model=model
)
raise typer.Exit(exit_code)


@app.command("wizard")
def setup_wizard(
provider: Optional[str] = typer.Option(None, "--provider", help="LLM provider"),
api_key: Optional[str] = typer.Option(None, "--api-key", help="API key"),
model: Optional[str] = typer.Option(None, "--model", help="Default model"),
):
"""Run the interactive setup wizard."""
exit_code = _run_setup(
non_interactive=False,
provider=provider,
api_key=api_key,
model=model
)
raise typer.Exit(exit_code)


@app.command("config")
def setup_config(
show: bool = typer.Option(False, "--show", help="Show current configuration"),
edit: bool = typer.Option(False, "--edit", help="Edit configuration file"),
):
"""Manage setup configuration."""
output = get_output_controller()

if show:
if ENV_FILE.exists():
output.console.print(f"[bold]Configuration at {ENV_FILE}:[/bold]")
content = ENV_FILE.read_text()
# Don't show actual API keys for security
lines = []
for line in content.split('\n'):
if '=' in line and any(key in line for key in ['API_KEY', 'TOKEN', 'SECRET']):
key, _ = line.split('=', 1)
lines.append(f"{key}=***")
else:
lines.append(line)
output.console.print('\n'.join(lines))
else:
output.print_warning(f"No configuration found at {ENV_FILE}")
output.console.print("Run [cyan]praisonai setup[/cyan] to create one.")

if edit:
import subprocess
editor = os.getenv("EDITOR", "nano")
try:
subprocess.run([editor, str(ENV_FILE)], check=True)
except subprocess.CalledProcessError:
output.print_error(f"Failed to open editor: {editor}")
except FileNotFoundError:
output.print_error(f"Editor not found: {editor}")


@app.command("reset")
def setup_reset(
force: bool = typer.Option(False, "--force", help="Skip confirmation"),
):
"""Reset setup configuration."""
output = get_output_controller()

praison_home = get_praison_home()
env_file = praison_home / ".env"
config_file = praison_home / "config.yaml"
files_to_remove = [path for path in (env_file, config_file) if path.exists()]

if not files_to_remove:
output.print_info("No setup configuration to reset.")
return

if not force:
confirm = typer.confirm(f"Reset configuration at {praison_home}?")
if not confirm:
output.print_info("Reset cancelled.")
return

try:
for path in files_to_remove:
path.unlink()
output.print_success("Configuration reset successfully.")
output.console.print("Run [cyan]praisonai setup[/cyan] to configure again.")
except Exception as e:
output.print_error(f"Failed to reset configuration: {e}")
raise typer.Exit(1)
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def check_openai_api_key(config: DoctorConfig) -> CheckResult:
category=CheckCategory.ENVIRONMENT,
status=CheckStatus.FAIL,
message="OPENAI_API_KEY not configured and no alternative providers found",
remediation="Set OPENAI_API_KEY environment variable or configure an alternative provider",
remediation="Run 'praisonai setup' to configure API keys, or set OPENAI_API_KEY environment variable",
severity=CheckSeverity.HIGH,
)

Expand Down
9 changes: 9 additions & 0 deletions src/praisonai/praisonai/cli/features/setup/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
Setup feature module for PraisonAI CLI.

Provides interactive onboarding and configuration management.
"""

from .handler import SetupHandler

__all__ = ["SetupHandler"]
Loading