Merge PR #1763: align onboard with config and workspace overrides
align onboard with config and workspace overrides
This commit is contained in:
22
README.md
22
README.md
@@ -1162,10 +1162,27 @@ MCP tools are automatically discovered and registered on startup. The LLM can us
|
|||||||
|
|
||||||
## 🧩 Multiple Instances
|
## 🧩 Multiple Instances
|
||||||
|
|
||||||
Run multiple nanobot instances simultaneously with separate configs and runtime data. Use `--config` as the main entrypoint, and optionally use `--workspace` to override the workspace for a specific run.
|
Run multiple nanobot instances simultaneously with separate configs and runtime data. Use `--config` as the main entrypoint. Optionally pass `--workspace` during `onboard` when you want to initialize or update the saved workspace for a specific instance.
|
||||||
|
|
||||||
### Quick Start
|
### Quick Start
|
||||||
|
|
||||||
|
If you want each instance to have its own dedicated workspace from the start, pass both `--config` and `--workspace` during onboarding.
|
||||||
|
|
||||||
|
**Initialize instances:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create separate instance configs and workspaces
|
||||||
|
nanobot onboard --config ~/.nanobot-telegram/config.json --workspace ~/.nanobot-telegram/workspace
|
||||||
|
nanobot onboard --config ~/.nanobot-discord/config.json --workspace ~/.nanobot-discord/workspace
|
||||||
|
nanobot onboard --config ~/.nanobot-feishu/config.json --workspace ~/.nanobot-feishu/workspace
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configure each instance:**
|
||||||
|
|
||||||
|
Edit `~/.nanobot-telegram/config.json`, `~/.nanobot-discord/config.json`, etc. with different channel settings. The workspace you passed during `onboard` is saved into each config as that instance's default workspace.
|
||||||
|
|
||||||
|
**Run instances:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Instance A - Telegram bot
|
# Instance A - Telegram bot
|
||||||
nanobot gateway --config ~/.nanobot-telegram/config.json
|
nanobot gateway --config ~/.nanobot-telegram/config.json
|
||||||
@@ -1265,7 +1282,8 @@ nanobot gateway --config ~/.nanobot-telegram/config.json --workspace /tmp/nanobo
|
|||||||
|
|
||||||
| Command | Description |
|
| Command | Description |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `nanobot onboard` | Initialize config & workspace |
|
| `nanobot onboard` | Initialize config & workspace at `~/.nanobot/` |
|
||||||
|
| `nanobot onboard -c <config> -w <workspace>` | Initialize or refresh a specific instance config and workspace |
|
||||||
| `nanobot agent -m "..."` | Chat with the agent |
|
| `nanobot agent -m "..."` | Chat with the agent |
|
||||||
| `nanobot agent -w <workspace>` | Chat against a specific workspace |
|
| `nanobot agent -w <workspace>` | Chat against a specific workspace |
|
||||||
| `nanobot agent -w <workspace> -c <config>` | Chat against a specific workspace/config |
|
| `nanobot agent -w <workspace> -c <config>` | Chat against a specific workspace/config |
|
||||||
|
|||||||
@@ -262,28 +262,42 @@ def main(
|
|||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def onboard():
|
def onboard(
|
||||||
|
workspace: str | None = typer.Option(None, "--workspace", "-w", help="Workspace directory"),
|
||||||
|
config: str | None = typer.Option(None, "--config", "-c", help="Path to config file"),
|
||||||
|
):
|
||||||
"""Initialize nanobot configuration and workspace."""
|
"""Initialize nanobot configuration and workspace."""
|
||||||
from nanobot.config.loader import get_config_path, load_config, save_config
|
from nanobot.config.loader import get_config_path, load_config, save_config, set_config_path
|
||||||
from nanobot.config.schema import Config
|
from nanobot.config.schema import Config
|
||||||
|
|
||||||
config_path = get_config_path()
|
if config:
|
||||||
|
config_path = Path(config).expanduser().resolve()
|
||||||
|
set_config_path(config_path)
|
||||||
|
console.print(f"[dim]Using config: {config_path}[/dim]")
|
||||||
|
else:
|
||||||
|
config_path = get_config_path()
|
||||||
|
|
||||||
|
def _apply_workspace_override(loaded: Config) -> Config:
|
||||||
|
if workspace:
|
||||||
|
loaded.agents.defaults.workspace = workspace
|
||||||
|
return loaded
|
||||||
|
|
||||||
|
# Create or update config
|
||||||
if config_path.exists():
|
if config_path.exists():
|
||||||
console.print(f"[yellow]Config already exists at {config_path}[/yellow]")
|
console.print(f"[yellow]Config already exists at {config_path}[/yellow]")
|
||||||
console.print(" [bold]y[/bold] = overwrite with defaults (existing values will be lost)")
|
console.print(" [bold]y[/bold] = overwrite with defaults (existing values will be lost)")
|
||||||
console.print(" [bold]N[/bold] = refresh config, keeping existing values and adding new fields")
|
console.print(" [bold]N[/bold] = refresh config, keeping existing values and adding new fields")
|
||||||
if typer.confirm("Overwrite?"):
|
if typer.confirm("Overwrite?"):
|
||||||
config = Config()
|
config = _apply_workspace_override(Config())
|
||||||
save_config(config)
|
save_config(config, config_path)
|
||||||
console.print(f"[green]✓[/green] Config reset to defaults at {config_path}")
|
console.print(f"[green]✓[/green] Config reset to defaults at {config_path}")
|
||||||
else:
|
else:
|
||||||
config = load_config()
|
config = _apply_workspace_override(load_config(config_path))
|
||||||
save_config(config)
|
save_config(config, config_path)
|
||||||
console.print(f"[green]✓[/green] Config refreshed at {config_path} (existing values preserved)")
|
console.print(f"[green]✓[/green] Config refreshed at {config_path} (existing values preserved)")
|
||||||
else:
|
else:
|
||||||
config = Config()
|
config = _apply_workspace_override(Config())
|
||||||
save_config(config)
|
save_config(config, config_path)
|
||||||
console.print(f"[green]✓[/green] Created config at {config_path}")
|
console.print(f"[green]✓[/green] Created config at {config_path}")
|
||||||
console.print("[dim]Config template now uses `maxTokens` + `contextWindowTokens`; `memoryWindow` is no longer a runtime setting.[/dim]")
|
console.print("[dim]Config template now uses `maxTokens` + `contextWindowTokens`; `memoryWindow` is no longer a runtime setting.[/dim]")
|
||||||
|
|
||||||
@@ -297,11 +311,15 @@ def onboard():
|
|||||||
|
|
||||||
sync_workspace_templates(workspace)
|
sync_workspace_templates(workspace)
|
||||||
|
|
||||||
|
agent_cmd = 'nanobot agent -m "Hello!"'
|
||||||
|
if config:
|
||||||
|
agent_cmd += f" --config {config_path}"
|
||||||
|
|
||||||
console.print(f"\n{__logo__} nanobot is ready!")
|
console.print(f"\n{__logo__} nanobot is ready!")
|
||||||
console.print("\nNext steps:")
|
console.print("\nNext steps:")
|
||||||
console.print(" 1. Add your API key to [cyan]~/.nanobot/config.json[/cyan]")
|
console.print(f" 1. Add your API key to [cyan]{config_path}[/cyan]")
|
||||||
console.print(" Get one at: https://openrouter.ai/keys")
|
console.print(" Get one at: https://openrouter.ai/keys")
|
||||||
console.print(" 2. Chat: [cyan]nanobot agent -m \"Hello!\"[/cyan]")
|
console.print(f" 2. Chat: [cyan]{agent_cmd}[/cyan]")
|
||||||
console.print("\n[dim]Want Telegram/WhatsApp? See: https://github.com/HKUDS/nanobot#-chat-apps[/dim]")
|
console.print("\n[dim]Want Telegram/WhatsApp? See: https://github.com/HKUDS/nanobot#-chat-apps[/dim]")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -43,7 +44,14 @@ def mock_paths():
|
|||||||
|
|
||||||
mock_cp.return_value = config_file
|
mock_cp.return_value = config_file
|
||||||
mock_ws.return_value = workspace_dir
|
mock_ws.return_value = workspace_dir
|
||||||
mock_sc.side_effect = lambda config: config_file.write_text("{}")
|
mock_lc.side_effect = lambda _config_path=None: Config()
|
||||||
|
|
||||||
|
def _save_config(config: Config, config_path: Path | None = None):
|
||||||
|
target = config_path or config_file
|
||||||
|
target.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
target.write_text(json.dumps(config.model_dump(by_alias=True)), encoding="utf-8")
|
||||||
|
|
||||||
|
mock_sc.side_effect = _save_config
|
||||||
|
|
||||||
yield config_file, workspace_dir, mock_ws
|
yield config_file, workspace_dir, mock_ws
|
||||||
|
|
||||||
@@ -109,6 +117,40 @@ def test_onboard_existing_workspace_safe_create(mock_paths):
|
|||||||
assert (workspace_dir / "AGENTS.md").exists()
|
assert (workspace_dir / "AGENTS.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_onboard_help_shows_workspace_and_config_options():
|
||||||
|
result = runner.invoke(app, ["onboard", "--help"])
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
stripped_output = _strip_ansi(result.stdout)
|
||||||
|
assert "--workspace" in stripped_output
|
||||||
|
assert "-w" in stripped_output
|
||||||
|
assert "--config" in stripped_output
|
||||||
|
assert "-c" in stripped_output
|
||||||
|
assert "--dir" not in stripped_output
|
||||||
|
|
||||||
|
|
||||||
|
def test_onboard_uses_explicit_config_and_workspace_paths(tmp_path, monkeypatch):
|
||||||
|
config_path = tmp_path / "instance" / "config.json"
|
||||||
|
workspace_path = tmp_path / "workspace"
|
||||||
|
|
||||||
|
monkeypatch.setattr("nanobot.channels.registry.discover_all", lambda: {})
|
||||||
|
|
||||||
|
result = runner.invoke(
|
||||||
|
app,
|
||||||
|
["onboard", "--config", str(config_path), "--workspace", str(workspace_path)],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.exit_code == 0
|
||||||
|
saved = Config.model_validate(json.loads(config_path.read_text(encoding="utf-8")))
|
||||||
|
assert saved.workspace_path == workspace_path
|
||||||
|
assert (workspace_path / "AGENTS.md").exists()
|
||||||
|
stripped_output = _strip_ansi(result.stdout)
|
||||||
|
compact_output = stripped_output.replace("\n", "")
|
||||||
|
resolved_config = str(config_path.resolve())
|
||||||
|
assert resolved_config in compact_output
|
||||||
|
assert f"--config {resolved_config}" in compact_output
|
||||||
|
|
||||||
|
|
||||||
def test_config_matches_github_copilot_codex_with_hyphen_prefix():
|
def test_config_matches_github_copilot_codex_with_hyphen_prefix():
|
||||||
config = Config()
|
config = Config()
|
||||||
config.agents.defaults.model = "github-copilot/gpt-5.3-codex"
|
config.agents.defaults.model = "github-copilot/gpt-5.3-codex"
|
||||||
|
|||||||
Reference in New Issue
Block a user