"""Onboarding flow — interactive first-time configuration wizard. When no agentkit.yaml exists, this wizard guides the user through: 1. Choosing an LLM provider 2. Entering API key 3. Selecting a default model 4. Generating agentkit.yaml + .env """ from __future__ import annotations from pathlib import Path import yaml from rich.panel import Panel from rich.prompt import Prompt, Confirm from rich import print as rprint from agentkit.server.config import find_config_path # ── Provider presets ────────────────────────────────────────────────── PROVIDER_PRESETS: dict[str, dict[str, object]] = { "deepseek": { "name": "DeepSeek", "env_key": "DEEPSEEK_API_KEY", "base_url": "https://api.deepseek.com/v1", "type": "openai", "models": { "deepseek-chat": {"alias": "default"}, "deepseek-reasoner": {"alias": "reasoning"}, }, "default_model": "deepseek-chat", }, "openai": { "name": "OpenAI", "env_key": "OPENAI_API_KEY", "base_url": "https://api.openai.com/v1", "type": "openai", "models": { "gpt-4o": {"alias": "default"}, "gpt-4o-mini": {"alias": "fast"}, }, "default_model": "gpt-4o", }, "bailian-coding": { "name": "百炼 Coding Plan", "env_key": "DASHSCOPE_API_KEY", "base_url": "https://coding.dashscope.aliyuncs.com/v1", "type": "openai", "models": { "qwen3.7-plus": {"alias": "default"}, "qwen3.6-plus": {}, "qwen3.5-plus": {}, "qwen3-max-2026-01-23": {}, "qwen3-coder-plus": {"alias": "coder"}, "qwen3-coder-next": {}, "kimi-k2.5": {}, "glm-5": {}, "glm-4.7": {}, "MiniMax-M2.5": {}, }, "default_model": "qwen3.7-plus", }, "qwen": { "name": "通义千问 (Qwen/DashScope)", "env_key": "DASHSCOPE_API_KEY", "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", "type": "openai", "models": { "qwen-plus": {"alias": "default"}, "qwen-turbo": {"alias": "fast"}, }, "default_model": "qwen-plus", }, "doubao": { "name": "豆包 (Doubao)", "env_key": "DOUBAO_API_KEY", "base_url": "https://ark.cn-beijing.volces.com/api/v3", "type": "openai", "models": { "doubao-pro-32k": {"alias": "default"}, }, "default_model": "doubao-pro-32k", }, "gemini": { "name": "Google Gemini", "env_key": "GEMINI_API_KEY", "base_url": "https://generativelanguage.googleapis.com", "type": "gemini", "models": { "gemini-2.0-flash": {"alias": "default"}, }, "default_model": "gemini-2.0-flash", }, "anthropic": { "name": "Anthropic Claude", "env_key": "ANTHROPIC_API_KEY", "base_url": "https://api.anthropic.com", "type": "anthropic", "models": { "claude-sonnet-4-20250514": {"alias": "default"}, }, "default_model": "claude-sonnet-4-20250514", }, } def needs_onboarding(config_arg: str | None = None) -> bool: """Check if onboarding is needed (no config file found or no valid LLM provider).""" config_path = find_config_path(config_arg) if config_path is None: return True # Config exists but has no valid LLM provider — needs onboarding to add one from agentkit.server.config import load_config_with_dotenv try: config = load_config_with_dotenv(config_path) return not config.has_llm_provider() except Exception: return True def run_onboarding( output_dir: str = ".", config_arg: str | None = None, ) -> str | None: """Run the interactive onboarding wizard. If agentkit.yaml already exists, only the LLM section is updated (preserving all other settings). If it doesn't exist, a full config is generated. Returns: Path to the generated/updated config file, or None if cancelled. """ # Determine output directory from config_arg if provided if config_arg: output_path = Path(config_arg).resolve().parent else: output_path = Path(output_dir).resolve() output_path.mkdir(parents=True, exist_ok=True) existing_config_path = find_config_path(config_arg) existing_config: dict[str, object] | None = None if existing_config_path: with open(existing_config_path, encoding="utf-8") as f: existing_config = yaml.safe_load(f) or {} if existing_config: rprint( Panel( "[bold]AgentKit Configuration Update[/bold]\n\n" "An [cyan]agentkit.yaml[/cyan] already exists but has no valid LLM provider.\n" "This wizard will [green]update[/green] the LLM section while preserving\n" "your existing settings.", title="AgentKit Setup", border_style="bright_blue", ) ) else: rprint( Panel( "[bold]Welcome to AgentKit![/bold]\n\n" "No configuration file found. Let's set up your first Agent.\n" "This will create [cyan]agentkit.yaml[/cyan] and [cyan].env[/cyan] for you.", title="AgentKit Setup", border_style="bright_blue", ) ) # ── Step 1: Choose LLM provider ────────────────────────────── rprint("\n[bold]Step 1: Choose your LLM provider[/bold]") provider_keys = list(PROVIDER_PRESETS.keys()) for i, key in enumerate(provider_keys, 1): preset = PROVIDER_PRESETS[key] rprint(f" [cyan]{i}[/cyan]. {preset['name']}") choice = Prompt.ask( "\nSelect a provider", choices=[str(i) for i in range(1, len(provider_keys) + 1)], default="1", ) selected_key = provider_keys[int(choice) - 1] preset = PROVIDER_PRESETS[selected_key] rprint(f"\n[green]Selected: {preset['name']}[/green]") # ── Step 2: Enter API key ───────────────────────────────────── rprint("\n[bold]Step 2: Enter your API key[/bold]") rprint(f"You can get one from the {preset['name']} dashboard.") api_key = Prompt.ask( f" {preset['env_key']}", password=True, ) if not api_key.strip(): rprint("[red]API key is required. Onboarding cancelled.[/red]") return None # ── Step 2b: Select default model ──────────────────────────── available_models = list(preset["models"].keys()) if len(available_models) > 1: rprint("\n[bold]Step 2b: Select your default model[/bold]") for i, model in enumerate(available_models, 1): alias = preset["models"][model].get("alias", "") alias_str = f" [dim]({alias})[/dim]" if alias else "" recommended = ( " [green]← recommended[/green]" if model == preset.get("default_model") else "" ) rprint(f" [cyan]{i}[/cyan]. {model}{alias_str}{recommended}") model_choice = Prompt.ask( "Select default model", choices=[str(i) for i in range(1, len(available_models) + 1)], default=str( available_models.index(preset.get("default_model", available_models[0])) + 1 ), ) selected_model = available_models[int(model_choice) - 1] # Rebuild models dict: selected model gets "default" alias updated_models: dict[str, object] = {} for model, conf in preset["models"].items(): if model == selected_model: updated_models[model] = {**conf, "alias": "default"} else: # Remove "default" alias from other models updated_models[model] = { k: v for k, v in conf.items() if k != "alias" or v != "default" } preset = {**preset, "models": updated_models} rprint(f"[green]Selected: {selected_model}[/green]") else: selected_model = available_models[0] # ── Step 3: Optional — add a second provider ───────────────── env_vars: dict[str, str] = {preset["env_key"]: api_key.strip()} providers_config: dict[str, object] = { selected_key: { "api_key": f"${{{preset['env_key']}}}", "base_url": preset["base_url"], "type": preset["type"], "models": preset["models"], } } model_aliases: dict[str, str] = { alias: f"{selected_key}/{model}" for model, conf in preset["models"].items() if (alias := conf.get("alias")) } if Confirm.ask("\nWould you like to add a second LLM provider (for fallback)?", default=False): remaining = [k for k in provider_keys if k != selected_key] for i, key in enumerate(remaining, 1): rprint(f" [cyan]{i}[/cyan]. {PROVIDER_PRESETS[key]['name']}") choice2 = Prompt.ask( "Select second provider (or press Enter to skip)", choices=[str(i) for i in range(1, len(remaining) + 1)] + [""], default="", ) if choice2: key2 = remaining[int(choice2) - 1] preset2 = PROVIDER_PRESETS[key2] api_key2 = Prompt.ask(f" {preset2['env_key']}", password=True) if api_key2.strip(): env_vars[preset2["env_key"]] = api_key2.strip() providers_config[key2] = { "api_key": f"${{{preset2['env_key']}}}", "base_url": preset2["base_url"], "type": preset2["type"], "models": preset2["models"], } for model, conf in preset2["models"].items(): alias = conf.get("alias") if alias and alias not in model_aliases: model_aliases[alias] = f"{key2}/{model}" # ── Step 4: Generate/update config files ───────────────────────── rprint("\n[bold]Step 3: Generating configuration...[/bold]") if existing_config: # Merge: only update LLM section, preserve everything else if "llm" not in existing_config: existing_config["llm"] = {} existing_config["llm"]["providers"] = providers_config existing_config["llm"]["model_aliases"] = model_aliases config = existing_config else: # New config — generate full template config = { "server": { "host": "0.0.0.0", "port": 8001, "workers": 1, "rate_limit": 60, }, "llm": { "providers": providers_config, "model_aliases": model_aliases, }, "session": { "backend": "memory", }, "bus": { "backend": "memory", }, "task_store": { "backend": "memory", }, "skills": { "auto_discover": True, "paths": ["./configs/skills"], }, "logging": { "level": "INFO", "format": "text", }, } # Write agentkit.yaml config_path = output_path / "agentkit.yaml" with open(config_path, "w", encoding="utf-8") as f: yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False) rprint(f" [green]Created:[/green] {config_path}") # Write .env (merge with existing) env_path = output_path / ".env" existing_env: dict[str, str] = {} existing_env_lines: list[str] = [] if env_path.exists(): with open(env_path, encoding="utf-8") as f: existing_env_lines = f.readlines() for line in existing_env_lines: stripped = line.strip() if stripped and not stripped.startswith("#") and "=" in stripped: k, _, v = stripped.partition("=") existing_env[k.strip()] = v.strip().strip("\"'") # Merge new env vars (new values take precedence) existing_env.update(env_vars) with open(env_path, "w", encoding="utf-8") as f: f.write("# AgentKit Environment Variables\n") f.write("# Generated by onboarding wizard\n\n") for k, v in existing_env.items(): f.write(f"{k}={v}\n") rprint(f" [green]Updated:[/green] {env_path}") # ── Step 4: Agent personality (optional) ────────────────────── rprint("\n[bold]Step 4: Customize your Agent (optional)[/bold]") rprint(" Press Enter to use defaults, or type your preferences.") agent_name = Prompt.ask(" Agent name", default="AgentKit") personality = Prompt.ask(" Personality", default="专业、友好、注重细节") speaking_style = Prompt.ask(" Speaking style", default="简洁清晰") # Create SOUL.md from agentkit.memory.profile import MemoryStore memory_store = MemoryStore(base_dir=Path.home() / ".agentkit") soul_content = f"""## 身份 我是{agent_name},一个专业的 AI 助手。 ## 性格 {personality} ## 说话方式 {speaking_style} ## 做事准则 - 准确回答用户问题 - 主动记住用户提到的偏好和信息 - 不确定时坦诚说明 """ memory_store.get_file("soul").write(soul_content.strip()) rprint(" [green]Created:[/green] ~/.agentkit/SOUL.md") rprint( Panel( "[bold green]Setup complete![/bold green]\n\n" "You can now run:\n" " [cyan]agentkit chat[/cyan] — Start chatting with your Agent\n" " [cyan]agentkit serve[/cyan] — Start the API server", border_style="green", ) ) return str(config_path)