389 lines
14 KiB
Python
389 lines
14 KiB
Python
"""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)
|