mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-04 08:28:36 +00:00
fix: streamline quick start onboarding flow
Maintainer edit: continue the wizard simplification pass by making Quick Start save after the API key path, hiding save/summary actions until they are needed, removing failed-key side effects, and aligning beginner docs with the local WebUI path.
This commit is contained in:
parent
fa5f7f5b88
commit
0e9b136315
@ -234,7 +234,7 @@ Windows PowerShell:
|
|||||||
irm https://raw.githubusercontent.com/HKUDS/nanobot/main/scripts/install.ps1 | iex
|
irm https://raw.githubusercontent.com/HKUDS/nanobot/main/scripts/install.ps1 | iex
|
||||||
```
|
```
|
||||||
|
|
||||||
The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If you finish the wizard and save the config, skip the manual initialize/configure steps below and go straight to **Test one message**.
|
The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes, skip the manual initialize/configure steps below and go straight to **Test one message**.
|
||||||
|
|
||||||
To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install.
|
To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install.
|
||||||
|
|
||||||
@ -290,7 +290,7 @@ nanobot --version
|
|||||||
|
|
||||||
**1. Initialize**
|
**1. Initialize**
|
||||||
|
|
||||||
Skip this step if the one-command setup already started the wizard and you saved the config there.
|
Skip this step if the one-command setup already started the wizard and Quick Start finished there.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nanobot onboard
|
nanobot onboard
|
||||||
|
|||||||
@ -32,7 +32,7 @@ On Windows PowerShell:
|
|||||||
irm https://raw.githubusercontent.com/HKUDS/nanobot/main/scripts/install.ps1 | iex
|
irm https://raw.githubusercontent.com/HKUDS/nanobot/main/scripts/install.ps1 | iex
|
||||||
```
|
```
|
||||||
|
|
||||||
The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If you finish the wizard and save the config, skip the manual initialize/configure steps and go straight to [Check the Setup](#4-check-the-setup).
|
The default command installs or upgrades `nanobot-ai` from PyPI, then starts `nanobot onboard`. It avoids system-wide pip installs by using an active virtual environment, `uv`, `pipx`, or a managed venv under `~/.nanobot/venv`. If Quick Start finishes, skip the manual initialize/configure steps and go straight to [Check the Setup](#4-check-the-setup).
|
||||||
|
|
||||||
To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install.
|
To preview the plan without changing your environment, pass `--dry-run`; combine it with `--dev` when you want to preview the main-branch install.
|
||||||
|
|
||||||
@ -96,7 +96,7 @@ The docs use `python` in commands. If your system exposes Python 3.11+ as `pytho
|
|||||||
|
|
||||||
## 2. Initialize
|
## 2. Initialize
|
||||||
|
|
||||||
Skip this section if the one-command setup already started the wizard and you saved the config there.
|
Skip this section if the one-command setup already started the wizard and Quick Start finished there.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nanobot onboard
|
nanobot onboard
|
||||||
|
|||||||
@ -2,23 +2,20 @@
|
|||||||
|
|
||||||
This page is for you if you have never used a terminal, edited a JSON file, or configured an AI model before.
|
This page is for you if you have never used a terminal, edited a JSON file, or configured an AI model before.
|
||||||
|
|
||||||
The goal is small: get one local nanobot reply. Do not connect Telegram, Discord, WebUI, Docker, local models, or deployment yet. Those are easier after the first reply works.
|
The goal is small: get one local nanobot reply in your browser. Do not connect Telegram, Discord, Docker, local models, or deployment yet. Those are easier after the first reply works.
|
||||||
|
|
||||||
## What You Are Setting Up
|
## What You Are Setting Up
|
||||||
|
|
||||||
You will see these words during setup:
|
You only need these words for Quick Start:
|
||||||
|
|
||||||
| Word | Plain meaning |
|
| Word | Plain meaning |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Terminal | A text window where you paste commands and press Enter. |
|
| Terminal | A text window where you paste commands and press Enter. |
|
||||||
| Command | One line of text you run in the terminal. |
|
| Command | One line of text you run in the terminal. |
|
||||||
| API key | A password-like token from an AI provider. Do not share it publicly. |
|
| API key | A password-like token from an AI provider. Do not share it publicly. |
|
||||||
| Provider | The service that owns the API key or local model endpoint. |
|
|
||||||
| Model | The AI model ID that the provider can run. |
|
|
||||||
| Config file | The settings file nanobot reads when it starts. |
|
| Config file | The settings file nanobot reads when it starts. |
|
||||||
| Wizard | An interactive terminal menu that edits the config file for you. |
|
| Wizard | An interactive terminal menu that edits the config file for you. |
|
||||||
| Model preset | A named model choice in the config file. |
|
| Browser UI | The local web page where you chat with nanobot. |
|
||||||
| `apiBase` | The HTTP address of a provider endpoint. Leave it blank unless your provider, proxy, or local server tells you to set one. |
|
|
||||||
|
|
||||||
## 1. Open a Terminal
|
## 1. Open a Terminal
|
||||||
|
|
||||||
@ -64,8 +61,6 @@ If `python3` works but `python` does not, replace `python` with `python3` in the
|
|||||||
|
|
||||||
nanobot does not create AI accounts or API keys for you. Use an AI provider account, company endpoint, subscription endpoint, or local model server that you already control. The steps below use OpenRouter because it is the recommended beginner path in the wizard; it is not a ranking or endorsement.
|
nanobot does not create AI accounts or API keys for you. Use an AI provider account, company endpoint, subscription endpoint, or local model server that you already control. The steps below use OpenRouter because it is the recommended beginner path in the wizard; it is not a ranking or endorsement.
|
||||||
|
|
||||||
If you use another provider, keep the same shape but replace the provider name, API key, and model ID with values from that provider. [`provider-cookbook.md`](./provider-cookbook.md) has copyable snippets for several common patterns.
|
|
||||||
|
|
||||||
For the example path:
|
For the example path:
|
||||||
|
|
||||||
1. Open [openrouter.ai/keys](https://openrouter.ai/keys).
|
1. Open [openrouter.ai/keys](https://openrouter.ai/keys).
|
||||||
@ -161,12 +156,10 @@ The wizard is a terminal menu. It is not a graphical app, but it lets you choose
|
|||||||
You will see a menu like this:
|
You will see a menu like this:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
> What would you like to configure?
|
> What would you like to do?
|
||||||
[Q] Quick Start (API key only)
|
[Q] Quick Start (API key only)
|
||||||
[A] Advanced Settings
|
[A] Advanced Settings
|
||||||
[V] View Configuration Summary
|
[X] Exit
|
||||||
[S] Save and Exit
|
|
||||||
[X] Exit Without Saving
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Move through the wizard like this:
|
Move through the wizard like this:
|
||||||
@ -174,33 +167,20 @@ Move through the wizard like this:
|
|||||||
| When you see | Do this |
|
| When you see | Do this |
|
||||||
|---|---|
|
|---|---|
|
||||||
| A menu | Use the arrow keys to highlight an option, then press `Enter`. |
|
| A menu | Use the arrow keys to highlight an option, then press `Enter`. |
|
||||||
| A text field | Type or paste the value, then press `Enter`. |
|
| The OpenRouter API key field | Paste the key, then press `Enter`. |
|
||||||
| A field you do not need | Keep the shown default or leave it blank, then press `Enter`. |
|
| A back option in Advanced Settings | Choose it to return to the previous menu. |
|
||||||
| A back option | Choose it to return to the previous menu. |
|
|
||||||
|
|
||||||
For the first setup, choose `[Q] Quick Start (API key only)`. It configures the recommended local browser UI and model choice for you. Use `Advanced Settings` later only if you need a different provider, model, channel, gateway, or tool setup.
|
For the first setup, choose `[Q] Quick Start (API key only)`. It configures the recommended local browser UI and default AI settings for you. Use `Advanced Settings` later only if you need a different provider, chat app, or tool setup.
|
||||||
|
|
||||||
If you are following the OpenRouter example:
|
If you are following the OpenRouter example:
|
||||||
|
|
||||||
1. Choose `[Q] Quick Start (API key only)`.
|
1. Choose `[Q] Quick Start (API key only)`.
|
||||||
2. Paste your OpenRouter API key, or press `Enter` and add it to the config file later.
|
2. Paste your OpenRouter API key.
|
||||||
3. Review the Quick Start summary.
|
3. Review the Quick Start summary. The wizard saves and exits when Quick Start finishes.
|
||||||
4. Choose `[S] Save and Exit`.
|
|
||||||
|
|
||||||
The recommended path enables the local WebUI with default WebSocket settings and uses a built-in model choice. You do not need to choose a model ID for the first run.
|
The recommended path enables the local WebUI and default AI settings. You do not need to choose a model, endpoint, or chat channel for the first run.
|
||||||
|
|
||||||
If OpenRouter later says your account cannot use the built-in model choice, return to the wizard, choose `Advanced Settings`, then edit `Model Presets` and enter another OpenRouter model ID that your account can access.
|
If you already know that you need another provider, choose `Advanced Settings` instead of Quick Start. [`provider-cookbook.md`](./provider-cookbook.md) has copyable examples for several common provider setups. After you change advanced settings, a save option appears in the main menu. Choose `[S] Save and Exit`.
|
||||||
|
|
||||||
If you already know that you need another provider, choose `Advanced Settings` instead of Quick Start and enter that provider's values:
|
|
||||||
|
|
||||||
| Wizard field | What to enter |
|
|
||||||
|---|---|
|
|
||||||
| Provider menu | The provider that owns your API key or endpoint. |
|
|
||||||
| API key | The key from that provider, or leave it blank only if the provider does not use one. |
|
|
||||||
| `apiBase` | Leave blank unless the provider docs, proxy docs, or local server docs give you a URL. |
|
|
||||||
| Model ID | A model ID that provider can actually serve. |
|
|
||||||
|
|
||||||
Then choose `[S] Save and Exit`.
|
|
||||||
|
|
||||||
The wizard creates or updates:
|
The wizard creates or updates:
|
||||||
|
|
||||||
@ -209,7 +189,9 @@ The wizard creates or updates:
|
|||||||
| `~/.nanobot/config.json` | Settings file. |
|
| `~/.nanobot/config.json` | Settings file. |
|
||||||
| `~/.nanobot/workspace/` | Working folder for memory, sessions, and generated files. |
|
| `~/.nanobot/workspace/` | Working folder for memory, sessions, and generated files. |
|
||||||
|
|
||||||
## How to Merge JSON Snippets
|
If Quick Start finished successfully, skip to [Send the First Message](#7-send-the-first-message). The next two sections are only for manual setup.
|
||||||
|
|
||||||
|
## Manual Setup: How to Merge JSON Snippets
|
||||||
|
|
||||||
Most docs examples are snippets, not whole files. Your `config.json` has one outer `{ ... }`. Add new top-level sections such as `providers`, `modelPresets`, `agents`, or `channels` inside that same outer object.
|
Most docs examples are snippets, not whole files. Your `config.json` has one outer `{ ... }`. Add new top-level sections such as `providers`, `modelPresets`, `agents`, or `channels` inside that same outer object.
|
||||||
|
|
||||||
@ -243,7 +225,7 @@ Merge them into one object:
|
|||||||
|
|
||||||
Notice the comma after the `providers` block. JSON needs commas between sibling sections, but not after the last section. If this feels hard, use `nanobot onboard` whenever possible.
|
Notice the comma after the `providers` block. JSON needs commas between sibling sections, but not after the last section. If this feels hard, use `nanobot onboard` whenever possible.
|
||||||
|
|
||||||
## 6. Manual Config Fallback
|
## 6. Manual Setup: Config Fallback
|
||||||
|
|
||||||
Use this only if the wizard is unavailable or you prefer opening the file yourself.
|
Use this only if the wizard is unavailable or you prefer opening the file yourself.
|
||||||
|
|
||||||
@ -292,6 +274,11 @@ If this is a brand-new install and you have not configured anything else yet, re
|
|||||||
"defaults": {
|
"defaults": {
|
||||||
"modelPreset": "primary"
|
"modelPreset": "primary"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"websocket": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -314,15 +301,19 @@ This should show the config file path, workspace path, and the active model or p
|
|||||||
|
|
||||||
It is normal for most providers to say `not set`. Only the provider you selected for the active preset needs to look configured.
|
It is normal for most providers to say `not set`. Only the provider you selected for the active preset needs to look configured.
|
||||||
|
|
||||||
Run:
|
Start the local browser UI:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nanobot agent -m "Hello!"
|
nanobot gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
If that works, nanobot is installed and can call the model.
|
Leave that terminal open, then open `http://127.0.0.1:8765` in your browser and send:
|
||||||
|
|
||||||
You should see a normal assistant reply in the terminal. The exact words will differ, but it should look like this shape:
|
```text
|
||||||
|
Hello!
|
||||||
|
```
|
||||||
|
|
||||||
|
If that works, nanobot is installed and can call the model. You should see a normal assistant reply in the browser. The exact words will differ, but it should look like this shape:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Hello! How can I help you today?
|
Hello! How can I help you today?
|
||||||
@ -331,12 +322,12 @@ Hello! How can I help you today?
|
|||||||
If `nanobot` is not found, run:
|
If `nanobot` is not found, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m nanobot agent -m "Hello!"
|
python -m nanobot gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
Use `python3 -m nanobot agent -m "Hello!"` or `py -m nanobot agent -m "Hello!"` if that is the Python command that worked in step 2.
|
Use `python3 -m nanobot gateway` or `py -m nanobot gateway` if that is the Python command that worked in step 2.
|
||||||
|
|
||||||
Once this works, nanobot can help with its own next setup step. Run `nanobot agent`, ask it to read these docs and update your current config for one specific goal, then run `/restart` when nanobot tells you the config is ready. For example, ask it to enable the browser UI, add one provider preset, or configure one chat app.
|
Once this works, nanobot can help with its own next setup step. In the browser UI, ask it to read these docs and update your current config for one specific goal, then run `/restart` when nanobot tells you the config is ready. For example, ask it to add one provider preset or configure one chat app.
|
||||||
|
|
||||||
## 8. If Something Fails
|
## 8. If Something Fails
|
||||||
|
|
||||||
@ -346,7 +337,7 @@ Do not change many things at once. Check the exact error:
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `JSON parse error` | The config file has a missing comma, extra comma, or mismatched brace. Copy the example again. |
|
| `JSON parse error` | The config file has a missing comma, extra comma, or mismatched brace. Copy the example again. |
|
||||||
| `401`, `unauthorized`, or `invalid API key` | The API key is wrong, expired, has extra spaces, or was pasted under the wrong provider. |
|
| `401`, `unauthorized`, or `invalid API key` | The API key is wrong, expired, has extra spaces, or was pasted under the wrong provider. |
|
||||||
| `model not found` | The model ID is not available through the selected provider or your account cannot use it. |
|
| `model not found` | Your account cannot use the default model. Return to `nanobot onboard`, choose `Advanced Settings`, then edit `Model Presets`. |
|
||||||
| `nanobot: command not found` | The install worked in Python, but your shell cannot find the script. Use `python -m nanobot ...`, `python3 -m nanobot ...`, or `py -m nanobot ...`, matching the Python command that worked earlier. |
|
| `nanobot: command not found` | The install worked in Python, but your shell cannot find the script. Use `python -m nanobot ...`, `python3 -m nanobot ...`, or `py -m nanobot ...`, matching the Python command that worked earlier. |
|
||||||
| No response after editing config | Restart the command. Long-running processes read config when they start. |
|
| No response after editing config | Restart the command. Long-running processes read config when they start. |
|
||||||
|
|
||||||
@ -357,7 +348,7 @@ For a fuller diagnosis path, see [`troubleshooting.md`](./troubleshooting.md).
|
|||||||
Skip these until the first local message works:
|
Skip these until the first local message works:
|
||||||
|
|
||||||
- `apiBase`: hosted built-in providers often already have default endpoints. You only need `apiBase` for local models, proxies, custom OpenAI-compatible providers, or special regional/subscription endpoints.
|
- `apiBase`: hosted built-in providers often already have default endpoints. You only need `apiBase` for local models, proxies, custom OpenAI-compatible providers, or special regional/subscription endpoints.
|
||||||
- WebUI and chat apps: first prove `nanobot agent -m "Hello!"`.
|
- chat apps: first prove the local browser UI can answer.
|
||||||
- fallback models: useful later, but not needed for the first reply.
|
- fallback models: useful later, but not needed for the first reply.
|
||||||
- Langfuse: useful for observability, but not needed for first setup.
|
- Langfuse: useful for observability, but not needed for first setup.
|
||||||
|
|
||||||
@ -365,22 +356,15 @@ Skip these until the first local message works:
|
|||||||
|
|
||||||
After the first reply works, choose only one next goal. Keep the terminal that runs `nanobot gateway` open whenever you use the WebUI or a chat app.
|
After the first reply works, choose only one next goal. Keep the terminal that runs `nanobot gateway` open whenever you use the WebUI or a chat app.
|
||||||
|
|
||||||
### Open the Browser UI
|
### Open the Browser UI Again
|
||||||
|
|
||||||
1. Add this snippet to `~/.nanobot/config.json`. Merge it into the existing file instead of replacing the whole file:
|
Run:
|
||||||
|
|
||||||
```json
|
|
||||||
{ "channels": { "websocket": { "enabled": true } } }
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Run:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nanobot gateway
|
nanobot gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Leave that terminal open.
|
Leave that terminal open, then open `http://127.0.0.1:8765` in your browser.
|
||||||
4. Open `http://127.0.0.1:8765` in your browser.
|
|
||||||
|
|
||||||
To stop the WebUI later, return to the gateway terminal and press `Ctrl+C`.
|
To stop the WebUI later, return to the gateway terminal and press `Ctrl+C`.
|
||||||
|
|
||||||
@ -413,7 +397,7 @@ When you ask for help, include:
|
|||||||
- the command you ran;
|
- the command you ran;
|
||||||
- `nanobot --version`;
|
- `nanobot --version`;
|
||||||
- `nanobot status`;
|
- `nanobot status`;
|
||||||
- whether `nanobot agent -m "Hello!"` works;
|
- whether the browser UI can answer `Hello!`;
|
||||||
- the exact error text;
|
- the exact error text;
|
||||||
- a config snippet with API keys and tokens removed.
|
- a config snippet with API keys and tokens removed.
|
||||||
|
|
||||||
|
|||||||
@ -388,9 +388,9 @@ def _show_main_menu_header() -> None:
|
|||||||
body = Table.grid(expand=True)
|
body = Table.grid(expand=True)
|
||||||
body.add_column(ratio=1)
|
body.add_column(ratio=1)
|
||||||
body.add_row(f"{__logo__} [bold {_UI_TEXT}]nanobot[/] [{_UI_MUTED}]v{__version__}[/]")
|
body.add_row(f"{__logo__} [bold {_UI_TEXT}]nanobot[/] [{_UI_MUTED}]v{__version__}[/]")
|
||||||
body.add_row(f"[{_UI_ACCENT}]Quick Start only needs an API key.[/]")
|
body.add_row(f"[{_UI_ACCENT}]Quick Start only needs an OpenRouter API key.[/]")
|
||||||
body.add_row(
|
body.add_row(
|
||||||
f"[{_UI_MUTED}]Provider, channel, model, and gateway settings stay in Advanced.[/]"
|
f"[{_UI_MUTED}]Use Advanced later for other providers or chat apps.[/]"
|
||||||
)
|
)
|
||||||
console.print(
|
console.print(
|
||||||
Panel(
|
Panel(
|
||||||
@ -1377,9 +1377,9 @@ def _show_summary(config: Config) -> None:
|
|||||||
_pause()
|
_pause()
|
||||||
|
|
||||||
|
|
||||||
def _pause() -> None:
|
def _pause(message: str = "Press Enter to continue...") -> None:
|
||||||
"""Pause for user acknowledgement before clearing the screen."""
|
"""Pause for user acknowledgement before clearing the screen."""
|
||||||
_get_questionary().text("Press Enter to continue...", default="").ask()
|
_get_questionary().text(message, default="").ask()
|
||||||
|
|
||||||
|
|
||||||
# --- Quick Start ---
|
# --- Quick Start ---
|
||||||
@ -1422,16 +1422,19 @@ def _configure_recommended_provider(config: Config) -> bool:
|
|||||||
_display, _is_gateway, _is_local, default_api_base = _get_provider_info().get(
|
_display, _is_gateway, _is_local, default_api_base = _get_provider_info().get(
|
||||||
provider_name, (provider_name, False, False, "")
|
provider_name, (provider_name, False, False, "")
|
||||||
)
|
)
|
||||||
if default_api_base and not provider_config.api_base:
|
|
||||||
provider_config.api_base = default_api_base
|
|
||||||
|
|
||||||
api_key = _input_with_existing(
|
api_key = _input_with_existing(
|
||||||
"OpenRouter API key (get one at https://openrouter.ai/keys; Enter to add later)",
|
"OpenRouter API key (get one at https://openrouter.ai/keys)",
|
||||||
provider_config.api_key,
|
provider_config.api_key,
|
||||||
"str",
|
"str",
|
||||||
)
|
)
|
||||||
if api_key is not None:
|
if api_key is not None:
|
||||||
provider_config.api_key = api_key or None
|
provider_config.api_key = api_key.strip() or None
|
||||||
|
if not provider_config.api_key:
|
||||||
|
console.print("[yellow]! OpenRouter API key is required for Quick Start[/yellow]")
|
||||||
|
return False
|
||||||
|
if default_api_base and not provider_config.api_base:
|
||||||
|
provider_config.api_base = default_api_base
|
||||||
|
|
||||||
_set_primary_quick_start_preset(
|
_set_primary_quick_start_preset(
|
||||||
config,
|
config,
|
||||||
@ -1457,49 +1460,35 @@ def _enable_quick_start_websocket_defaults(config: Config) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def _show_quick_start_summary(config: Config, channel_name: str | None) -> None:
|
def _show_quick_start_summary(config: Config) -> None:
|
||||||
"""Show the small summary users need before returning to the menu."""
|
"""Show the small summary users need before returning to the menu."""
|
||||||
_show_quick_start_progress(3)
|
_show_quick_start_progress(3)
|
||||||
preset = config.model_presets.get("primary")
|
preset = config.model_presets.get("primary")
|
||||||
api_key_status = None
|
provider_label = "AI provider"
|
||||||
|
has_api_key = True
|
||||||
if preset:
|
if preset:
|
||||||
provider_config = getattr(config.providers, preset.provider, None)
|
provider_config = getattr(config.providers, preset.provider, None)
|
||||||
_display, _is_gateway, is_local, _api_base = _get_provider_info().get(
|
provider_label, _is_gateway, is_local, _api_base = _get_provider_info().get(
|
||||||
preset.provider, (preset.provider, False, False, "")
|
preset.provider, (preset.provider, False, False, "")
|
||||||
)
|
)
|
||||||
if not is_local:
|
has_api_key = is_local or bool(provider_config and provider_config.api_key)
|
||||||
api_key_status = (
|
|
||||||
"configured"
|
|
||||||
if provider_config and provider_config.api_key
|
|
||||||
else "add later"
|
|
||||||
)
|
|
||||||
|
|
||||||
next_step = (
|
start_command = "`nanobot gateway`"
|
||||||
"Save, then run `nanobot gateway`"
|
next_step = f"Run {start_command}"
|
||||||
if channel_name
|
status = "Ready"
|
||||||
else "Save, then run `nanobot agent -m \"Hello!\"`"
|
if not has_api_key:
|
||||||
)
|
status = f"{provider_label} API key missing"
|
||||||
if api_key_status == "add later":
|
next_step = f"Add your {provider_label} API key, then run {start_command}"
|
||||||
next_step = (
|
|
||||||
"Save, add your API key to config, then run `nanobot gateway`"
|
|
||||||
if channel_name
|
|
||||||
else "Save, add your API key to config, then run `nanobot agent -m \"Hello!\"`"
|
|
||||||
)
|
|
||||||
|
|
||||||
rows = [
|
rows = [
|
||||||
("Provider", preset.provider if preset else "[not set]"),
|
("Status", status),
|
||||||
("Model", preset.model if preset else "[not set]"),
|
|
||||||
("Entry point", "Not enabled yet" if channel_name is None else channel_name),
|
|
||||||
("Next", next_step),
|
("Next", next_step),
|
||||||
|
("Open", "http://127.0.0.1:8765"),
|
||||||
]
|
]
|
||||||
if api_key_status:
|
|
||||||
rows.insert(2, ("API key", api_key_status))
|
|
||||||
if channel_name == "websocket":
|
|
||||||
rows.append(("WebUI", "Open http://127.0.0.1:8765 after the gateway starts"))
|
|
||||||
_print_summary_panel(rows, "Quick Start")
|
_print_summary_panel(rows, "Quick Start")
|
||||||
|
|
||||||
|
|
||||||
def _configure_quick_start(config: Config) -> None:
|
def _configure_quick_start(config: Config) -> bool:
|
||||||
"""First-run path: API key + local WebUI, with advanced settings hidden."""
|
"""First-run path: API key + local WebUI, with advanced settings hidden."""
|
||||||
console.clear()
|
console.clear()
|
||||||
_show_section_header(
|
_show_section_header(
|
||||||
@ -1508,12 +1497,13 @@ def _configure_quick_start(config: Config) -> None:
|
|||||||
)
|
)
|
||||||
if not _configure_recommended_provider(config):
|
if not _configure_recommended_provider(config):
|
||||||
_pause()
|
_pause()
|
||||||
return
|
return False
|
||||||
if not _enable_quick_start_websocket_defaults(config):
|
if not _enable_quick_start_websocket_defaults(config):
|
||||||
_pause()
|
_pause()
|
||||||
return
|
return False
|
||||||
_show_quick_start_summary(config, "websocket")
|
_show_quick_start_summary(config)
|
||||||
_pause()
|
_pause("Press Enter to save and exit...")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# --- Main Entry Point ---
|
# --- Main Entry Point ---
|
||||||
@ -1547,6 +1537,19 @@ def _prompt_main_menu_exit(has_unsaved_changes: bool) -> str:
|
|||||||
return "resume"
|
return "resume"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_main_menu_choices(has_unsaved_changes: bool) -> list[str]:
|
||||||
|
"""Return the top-level choices, keeping save actions hidden until needed."""
|
||||||
|
choices = [
|
||||||
|
"[Q] Quick Start (API key only)",
|
||||||
|
"[A] Advanced Settings",
|
||||||
|
]
|
||||||
|
if has_unsaved_changes:
|
||||||
|
choices.extend(["[S] Save and Exit", "[X] Exit Without Saving"])
|
||||||
|
else:
|
||||||
|
choices.append("[X] Exit")
|
||||||
|
return choices
|
||||||
|
|
||||||
|
|
||||||
def _configure_advanced_settings(config: Config) -> None:
|
def _configure_advanced_settings(config: Config) -> None:
|
||||||
"""Show lower-frequency setup options behind one advanced menu."""
|
"""Show lower-frequency setup options behind one advanced menu."""
|
||||||
last_choice: str | None = None
|
last_choice: str | None = None
|
||||||
@ -1568,6 +1571,7 @@ def _configure_advanced_settings(config: Config) -> None:
|
|||||||
"[I] API Server",
|
"[I] API Server",
|
||||||
"[G] Gateway",
|
"[G] Gateway",
|
||||||
"[T] Tools",
|
"[T] Tools",
|
||||||
|
"[V] View Configuration Summary",
|
||||||
"<- Back",
|
"<- Back",
|
||||||
],
|
],
|
||||||
default=last_choice,
|
default=last_choice,
|
||||||
@ -1588,6 +1592,7 @@ def _configure_advanced_settings(config: Config) -> None:
|
|||||||
"[I] API Server": lambda: _configure_general_settings(config, "API Server"),
|
"[I] API Server": lambda: _configure_general_settings(config, "API Server"),
|
||||||
"[G] Gateway": lambda: _configure_general_settings(config, "Gateway"),
|
"[G] Gateway": lambda: _configure_general_settings(config, "Gateway"),
|
||||||
"[T] Tools": lambda: _configure_general_settings(config, "Tools"),
|
"[T] Tools": lambda: _configure_general_settings(config, "Tools"),
|
||||||
|
"[V] View Configuration Summary": lambda: _show_summary(config),
|
||||||
}
|
}
|
||||||
action_fn = _advanced_dispatch.get(answer)
|
action_fn = _advanced_dispatch.get(answer)
|
||||||
if action_fn:
|
if action_fn:
|
||||||
@ -1624,14 +1629,8 @@ def run_onboard(initial_config: Config | None = None) -> OnboardResult:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
answer = _get_questionary().select(
|
answer = _get_questionary().select(
|
||||||
"What would you like to configure?",
|
"What would you like to do?",
|
||||||
choices=[
|
choices=_get_main_menu_choices(_has_unsaved_changes(original_config, config)),
|
||||||
"[Q] Quick Start (API key only)",
|
|
||||||
"[A] Advanced Settings",
|
|
||||||
"[V] View Configuration Summary",
|
|
||||||
"[S] Save and Exit",
|
|
||||||
"[X] Exit Without Saving",
|
|
||||||
],
|
|
||||||
default=last_main_choice,
|
default=last_main_choice,
|
||||||
qmark=">",
|
qmark=">",
|
||||||
).ask()
|
).ask()
|
||||||
@ -1646,15 +1645,18 @@ def run_onboard(initial_config: Config | None = None) -> OnboardResult:
|
|||||||
return OnboardResult(config=original_config, should_save=False)
|
return OnboardResult(config=original_config, should_save=False)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if answer == "[Q] Quick Start (API key only)":
|
||||||
|
if _configure_quick_start(config):
|
||||||
|
return OnboardResult(config=config, should_save=True)
|
||||||
|
continue
|
||||||
|
|
||||||
_menu_dispatch = {
|
_menu_dispatch = {
|
||||||
"[Q] Quick Start (API key only)": lambda: _configure_quick_start(config),
|
|
||||||
"[A] Advanced Settings": lambda: _configure_advanced_settings(config),
|
"[A] Advanced Settings": lambda: _configure_advanced_settings(config),
|
||||||
"[V] View Configuration Summary": lambda: _show_summary(config),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if answer == "[S] Save and Exit":
|
if answer == "[S] Save and Exit":
|
||||||
return OnboardResult(config=config, should_save=True)
|
return OnboardResult(config=config, should_save=True)
|
||||||
if answer == "[X] Exit Without Saving":
|
if answer in {"[X] Exit", "[X] Exit Without Saving"}:
|
||||||
return OnboardResult(config=original_config, should_save=False)
|
return OnboardResult(config=original_config, should_save=False)
|
||||||
|
|
||||||
action_fn = _menu_dispatch.get(answer)
|
action_fn = _menu_dispatch.get(answer)
|
||||||
|
|||||||
@ -858,13 +858,29 @@ class TestApiServerRegistration:
|
|||||||
class TestMainMenuUpdate:
|
class TestMainMenuUpdate:
|
||||||
"""Tests for main menu including new Channel Common and API Server items."""
|
"""Tests for main menu including new Channel Common and API Server items."""
|
||||||
|
|
||||||
|
def test_main_menu_hides_save_actions_until_needed(self):
|
||||||
|
"""The first screen should not show save or summary actions before edits."""
|
||||||
|
from nanobot.cli.onboard import _get_main_menu_choices
|
||||||
|
|
||||||
|
clean_choices = _get_main_menu_choices(False)
|
||||||
|
dirty_choices = _get_main_menu_choices(True)
|
||||||
|
|
||||||
|
assert clean_choices == [
|
||||||
|
"[Q] Quick Start (API key only)",
|
||||||
|
"[A] Advanced Settings",
|
||||||
|
"[X] Exit",
|
||||||
|
]
|
||||||
|
assert "[S] Save and Exit" not in clean_choices
|
||||||
|
assert "[V] View Configuration Summary" not in clean_choices
|
||||||
|
assert "[S] Save and Exit" in dirty_choices
|
||||||
|
assert "[X] Exit Without Saving" in dirty_choices
|
||||||
|
|
||||||
def test_run_onboard_quick_start_edit(self, monkeypatch):
|
def test_run_onboard_quick_start_edit(self, monkeypatch):
|
||||||
"""run_onboard should route [Q] to Quick Start."""
|
"""run_onboard should route [Q] to Quick Start."""
|
||||||
initial_config = Config()
|
initial_config = Config()
|
||||||
|
|
||||||
responses = iter([
|
responses = iter([
|
||||||
"[Q] Quick Start (API key only)",
|
"[Q] Quick Start (API key only)",
|
||||||
"[S] Save and Exit",
|
|
||||||
])
|
])
|
||||||
|
|
||||||
class FakePrompt:
|
class FakePrompt:
|
||||||
@ -879,6 +895,7 @@ class TestMainMenuUpdate:
|
|||||||
|
|
||||||
def fake_quick_start(config):
|
def fake_quick_start(config):
|
||||||
config.agents.defaults.bot_name = "quickbot"
|
config.agents.defaults.bot_name = "quickbot"
|
||||||
|
return True
|
||||||
|
|
||||||
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
monkeypatch.setattr(onboard_wizard, "_show_main_menu_header", lambda: None)
|
||||||
monkeypatch.setattr(onboard_wizard, "questionary", SimpleNamespace(select=fake_select))
|
monkeypatch.setattr(onboard_wizard, "questionary", SimpleNamespace(select=fake_select))
|
||||||
@ -899,16 +916,19 @@ class TestMainMenuUpdate:
|
|||||||
def fail_websocket_config(*_args, **_kwargs):
|
def fail_websocket_config(*_args, **_kwargs):
|
||||||
raise AssertionError("recommended Quick Start should not open WebSocket settings")
|
raise AssertionError("recommended Quick Start should not open WebSocket settings")
|
||||||
|
|
||||||
|
pause_messages: list[str] = []
|
||||||
|
|
||||||
monkeypatch.setattr(onboard_wizard.console, "clear", lambda: None)
|
monkeypatch.setattr(onboard_wizard.console, "clear", lambda: None)
|
||||||
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
|
monkeypatch.setattr(onboard_wizard, "_show_section_header", lambda *a, **kw: None)
|
||||||
monkeypatch.setattr(onboard_wizard, "_input_with_existing", lambda *a, **kw: "sk-or-test")
|
monkeypatch.setattr(onboard_wizard, "_input_with_existing", lambda *a, **kw: "sk-or-test")
|
||||||
monkeypatch.setattr(onboard_wizard, "_input_model_with_autocomplete", fail_model_input)
|
monkeypatch.setattr(onboard_wizard, "_input_model_with_autocomplete", fail_model_input)
|
||||||
monkeypatch.setattr(onboard_wizard, "_configure_pydantic_model", fail_websocket_config)
|
monkeypatch.setattr(onboard_wizard, "_configure_pydantic_model", fail_websocket_config)
|
||||||
monkeypatch.setattr(onboard_wizard, "_print_summary_panel", lambda *a, **kw: None)
|
monkeypatch.setattr(onboard_wizard, "_print_summary_panel", lambda *a, **kw: None)
|
||||||
monkeypatch.setattr(onboard_wizard, "_pause", lambda: None)
|
monkeypatch.setattr(onboard_wizard, "_pause", lambda message="": pause_messages.append(message))
|
||||||
|
|
||||||
onboard_wizard._configure_quick_start(config)
|
assert onboard_wizard._configure_quick_start(config) is True
|
||||||
|
|
||||||
|
assert pause_messages == ["Press Enter to save and exit..."]
|
||||||
assert config.providers.openrouter.api_key == "sk-or-test"
|
assert config.providers.openrouter.api_key == "sk-or-test"
|
||||||
assert config.providers.openrouter.api_base == "https://openrouter.ai/api/v1"
|
assert config.providers.openrouter.api_base == "https://openrouter.ai/api/v1"
|
||||||
assert config.agents.defaults.model_preset == "primary"
|
assert config.agents.defaults.model_preset == "primary"
|
||||||
@ -918,6 +938,19 @@ class TestMainMenuUpdate:
|
|||||||
assert websocket["enabled"] is True
|
assert websocket["enabled"] is True
|
||||||
assert websocket["websocketRequiresToken"] is True
|
assert websocket["websocketRequiresToken"] is True
|
||||||
|
|
||||||
|
def test_quick_start_requires_api_key_before_setting_defaults(self, monkeypatch):
|
||||||
|
"""Quick Start should not create a ready-looking config without an API key."""
|
||||||
|
config = Config()
|
||||||
|
|
||||||
|
monkeypatch.setattr(onboard_wizard, "_show_quick_start_progress", lambda *_args: None)
|
||||||
|
monkeypatch.setattr(onboard_wizard, "_input_with_existing", lambda *a, **kw: "")
|
||||||
|
|
||||||
|
assert onboard_wizard._configure_recommended_provider(config) is False
|
||||||
|
|
||||||
|
assert config.providers.openrouter.api_key is None
|
||||||
|
assert config.providers.openrouter.api_base is None
|
||||||
|
assert "primary" not in config.model_presets
|
||||||
|
|
||||||
def test_quick_start_summary_calls_out_missing_api_key(self, monkeypatch):
|
def test_quick_start_summary_calls_out_missing_api_key(self, monkeypatch):
|
||||||
"""Quick Start summary should not tell users to run gateway before adding a key."""
|
"""Quick Start summary should not tell users to run gateway before adding a key."""
|
||||||
config = Config()
|
config = Config()
|
||||||
@ -935,11 +968,18 @@ class TestMainMenuUpdate:
|
|||||||
lambda rows, _title: captured.setdefault("rows", rows),
|
lambda rows, _title: captured.setdefault("rows", rows),
|
||||||
)
|
)
|
||||||
|
|
||||||
onboard_wizard._show_quick_start_summary(config, "websocket")
|
onboard_wizard._show_quick_start_summary(config)
|
||||||
|
|
||||||
|
labels = [label for label, _value in captured["rows"]]
|
||||||
rows = dict(captured["rows"])
|
rows = dict(captured["rows"])
|
||||||
assert rows["API key"] == "add later"
|
assert rows["Status"] == "OpenRouter API key missing"
|
||||||
assert "add your API key" in rows["Next"]
|
assert "API key" in rows["Next"]
|
||||||
|
assert "nanobot gateway" in rows["Next"]
|
||||||
|
assert labels.index("Next") < labels.index("Open")
|
||||||
|
assert "Model" not in rows
|
||||||
|
assert "Entry point" not in rows
|
||||||
|
assert "API key" not in rows
|
||||||
|
assert "Defaults" not in rows
|
||||||
|
|
||||||
def test_configure_login_channel_defaults_to_login(self, monkeypatch):
|
def test_configure_login_channel_defaults_to_login(self, monkeypatch):
|
||||||
"""The channel wizard should start login before exposing advanced fields."""
|
"""The channel wizard should start login before exposing advanced fields."""
|
||||||
@ -1088,13 +1128,15 @@ class TestMainMenuUpdate:
|
|||||||
assert result.config.api.port == 9999
|
assert result.config.api.port == 9999
|
||||||
|
|
||||||
def test_view_summary_calls_pause(self, monkeypatch):
|
def test_view_summary_calls_pause(self, monkeypatch):
|
||||||
"""[V] View Summary should pause before returning to main menu."""
|
"""Advanced [V] View Summary should pause before returning to the menu."""
|
||||||
initial_config = Config()
|
initial_config = Config()
|
||||||
pause_called = {"n": 0}
|
pause_called = {"n": 0}
|
||||||
|
|
||||||
responses = iter([
|
responses = iter([
|
||||||
|
"[A] Advanced Settings",
|
||||||
"[V] View Configuration Summary",
|
"[V] View Configuration Summary",
|
||||||
"[S] Save and Exit",
|
KeyboardInterrupt(),
|
||||||
|
"[X] Exit",
|
||||||
])
|
])
|
||||||
|
|
||||||
class FakePrompt:
|
class FakePrompt:
|
||||||
@ -1123,7 +1165,7 @@ class TestMainMenuUpdate:
|
|||||||
|
|
||||||
result = run_onboard(initial_config=initial_config)
|
result = run_onboard(initial_config=initial_config)
|
||||||
|
|
||||||
assert result.should_save is True
|
assert result.should_save is False
|
||||||
assert pause_called["n"] == 1
|
assert pause_called["n"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user