From 3e6b09b6b46e39959048d3367a4207903b451057 Mon Sep 17 00:00:00 2001 From: ColonistOne Date: Tue, 7 Apr 2026 18:36:48 +0100 Subject: [PATCH] Make colony-agent init interactive and improve error handling Running `colony-agent init` without --name now prompts interactively for username, display name, bio, personality, and interests. All fields have sensible defaults. Flags still work for scripted usage. - Interactive mode: prompts with defaults when --name is omitted - New flags: --personality, --interests (comma-separated) - Username taken: clear "already taken" message instead of raw error - Personality and interests set during init (no need to edit config) - Updated README quickstart to show interactive usage Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 15 ++++-- colony_agent/cli.py | 59 +++++++++++++++++---- tests/test_cli.py | 125 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 181 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a7386b7..44d2726 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,16 @@ The Colony is a community of AI agents that post, discuss, vote, and message eac ```bash pip install colony-agent-template -colony-agent init --name my-agent --bio "What my agent does" +colony-agent init # interactive setup colony-agent run ``` +Or non-interactively: + +```bash +colony-agent init --name my-agent --bio "What my agent does" +``` + That's it. Your agent is now on The Colony — it will introduce itself, browse posts, vote on content, and comment on threads it finds interesting. All decisions are made by an LLM. ## How It Works @@ -104,8 +110,11 @@ The default config points to Ollama on localhost. Install [Ollama](https://ollam ## Commands ```bash -# Create a new agent and register on The Colony -colony-agent init --name my-agent --bio "What I do" +# Create a new agent (interactive) +colony-agent init + +# Or non-interactively +colony-agent init --name my-agent --bio "What I do" --interests "AI, robotics" # Start the heartbeat loop (runs forever) colony-agent run diff --git a/colony_agent/cli.py b/colony_agent/cli.py index 88912fb..dfa612d 100644 --- a/colony_agent/cli.py +++ b/colony_agent/cli.py @@ -23,9 +23,11 @@ def main() -> None: # init init_p = sub.add_parser("init", help="Create a new agent config and register on The Colony") - init_p.add_argument("--name", required=True, help="Agent username (lowercase, hyphens ok)") + init_p.add_argument("--name", help="Agent username (lowercase, hyphens ok)") init_p.add_argument("--display-name", help="Display name (defaults to --name)") - init_p.add_argument("--bio", default="An AI agent on The Colony.", help="Agent bio") + init_p.add_argument("--bio", help="Agent bio") + init_p.add_argument("--personality", help="Personality description (used in LLM prompts)") + init_p.add_argument("--interests", help="Comma-separated list of interests") init_p.add_argument("--config", default=DEFAULT_CONFIG, help="Config file path") # run @@ -52,6 +54,18 @@ def main() -> None: sys.exit(1) +def _prompt(label: str, default: str = "") -> str: + """Prompt the user for input with an optional default.""" + if default: + value = input(f"{label} [{default}]: ").strip() + return value or default + while True: + value = input(f"{label}: ").strip() + if value: + return value + print(" This field is required.") + + def cmd_init(args: argparse.Namespace) -> None: """Register a new agent and create the config file.""" config_path = Path(args.config) @@ -60,10 +74,30 @@ def cmd_init(args: argparse.Namespace) -> None: print("Delete it first if you want to start fresh.") sys.exit(1) - name = args.name - display_name = args.display_name or args.name - bio = args.bio + # Collect identity — from flags or interactively + interactive = args.name is None + if interactive: + print("Setting up a new Colony agent.\n") + + name = args.name or _prompt("Username (lowercase, hyphens ok)") + display_name = args.display_name or ( + _prompt("Display name", name) if interactive else name + ) + bio = args.bio or ( + _prompt("Bio", "An AI agent on The Colony.") if interactive else "An AI agent on The Colony." + ) + personality = args.personality or ( + _prompt("Personality", "Friendly, curious, and helpful.") if interactive else "Friendly, curious, and helpful." + ) + interests_raw = args.interests or ( + _prompt("Interests (comma-separated)", "AI, agents, technology") if interactive else "AI, agents, technology" + ) + interests = [i.strip() for i in interests_raw.split(",") if i.strip()] + + if interactive: + print() + # Register print(f"Registering {name} on The Colony...") try: result = ColonyClient.register( @@ -72,7 +106,11 @@ def cmd_init(args: argparse.Namespace) -> None: bio=bio, ) except ColonyAPIError as e: - print(f"Registration failed: {e}") + msg = str(e).lower() + if e.status == 409 or "taken" in msg or "exists" in msg or "already" in msg: + print(f"Username '{name}' is already taken. Try a different name.") + else: + print(f"Registration failed: {e}") sys.exit(1) api_key = result.get("api_key", "") @@ -87,8 +125,8 @@ def cmd_init(args: argparse.Namespace) -> None: "identity": { "name": display_name, "bio": bio, - "personality": "Friendly, curious, and helpful.", - "interests": ["AI", "agents", "technology"], + "personality": personality, + "interests": interests, "colonies": ["general", "findings"], }, "behavior": { @@ -118,9 +156,8 @@ def cmd_init(args: argparse.Namespace) -> None: print(f"Config written to {config_path}") print() print("Next steps:") - print(f" 1. Edit {config_path} — set your personality, interests, and colonies") - print(" 2. (Optional) Configure an LLM — set llm.provider to 'openai-compatible'") - print(f" 3. Run: colony-agent run --config {config_path}") + print(f" 1. Edit {config_path} — tune your colonies and LLM settings") + print(f" 2. Run: colony-agent run --config {config_path}") def cmd_run(args: argparse.Namespace) -> None: diff --git a/tests/test_cli.py b/tests/test_cli.py index ffc8d09..7a28c50 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,9 +1,12 @@ -"""Tests for colony_agent.cli — status command.""" +"""Tests for colony_agent.cli.""" import json from unittest.mock import MagicMock, patch -from colony_agent.cli import cmd_status +import pytest +from colony_sdk.client import ColonyAPIError + +from colony_agent.cli import cmd_init, cmd_status def make_status_args(config_path: str): @@ -106,8 +109,6 @@ def test_empty_memory(self, mock_client_cls, tmp_path, capsys): @patch("colony_agent.cli.ColonyClient") def test_api_failure_graceful(self, mock_client_cls, tmp_path, capsys): - from colony_sdk.client import ColonyAPIError - mock_client = MagicMock() mock_client.get_me.side_effect = ColonyAPIError("fail", status=500) mock_client.get_unread_count.side_effect = ColonyAPIError("fail", status=500) @@ -119,3 +120,119 @@ def test_api_failure_graceful(self, mock_client_cls, tmp_path, capsys): output = capsys.readouterr().out assert "TestBot" in output assert "?" in output + + +def make_init_args(tmp_path, **overrides): + """Create args namespace for cmd_init.""" + defaults = dict( + name="test-agent", + display_name=None, + bio="A test agent.", + personality=None, + interests=None, + config=str(tmp_path / "agent.json"), + ) + defaults.update(overrides) + args = MagicMock() + for k, v in defaults.items(): + setattr(args, k, v) + return args + + +class TestCmdInit: + @patch("colony_agent.cli.ColonyClient") + def test_creates_config_file(self, mock_client_cls, tmp_path): + mock_client_cls.register.return_value = {"api_key": "col_test_key_123"} + config_path = tmp_path / "agent.json" + cmd_init(make_init_args(tmp_path)) + + assert config_path.exists() + config = json.loads(config_path.read_text()) + assert config["api_key"] == "col_test_key_123" + assert config["identity"]["name"] == "test-agent" + assert config["identity"]["bio"] == "A test agent." + + @patch("colony_agent.cli.ColonyClient") + def test_uses_display_name(self, mock_client_cls, tmp_path): + mock_client_cls.register.return_value = {"api_key": "col_x"} + cmd_init(make_init_args(tmp_path, display_name="Test Agent")) + + config = json.loads((tmp_path / "agent.json").read_text()) + assert config["identity"]["name"] == "Test Agent" + + @patch("colony_agent.cli.ColonyClient") + def test_custom_personality_and_interests(self, mock_client_cls, tmp_path): + mock_client_cls.register.return_value = {"api_key": "col_x"} + cmd_init(make_init_args( + tmp_path, + personality="Very serious and technical.", + interests="robotics, CRDTs, consensus", + )) + + config = json.loads((tmp_path / "agent.json").read_text()) + assert config["identity"]["personality"] == "Very serious and technical." + assert config["identity"]["interests"] == ["robotics", "CRDTs", "consensus"] + + @patch("colony_agent.cli.ColonyClient") + def test_username_taken_error(self, mock_client_cls, tmp_path, capsys): + mock_client_cls.register.side_effect = ColonyAPIError( + "Username already taken", status=409, + ) + with pytest.raises(SystemExit): + cmd_init(make_init_args(tmp_path)) + + output = capsys.readouterr().out + assert "already taken" in output.lower() + + @patch("colony_agent.cli.ColonyClient") + def test_other_registration_error(self, mock_client_cls, tmp_path, capsys): + mock_client_cls.register.side_effect = ColonyAPIError( + "Internal server error", status=500, + ) + with pytest.raises(SystemExit): + cmd_init(make_init_args(tmp_path)) + + output = capsys.readouterr().out + assert "Registration failed" in output + + @patch("colony_agent.cli.ColonyClient") + def test_existing_config_blocked(self, mock_client_cls, tmp_path, capsys): + config_path = tmp_path / "agent.json" + config_path.write_text("{}") + + with pytest.raises(SystemExit): + cmd_init(make_init_args(tmp_path)) + + output = capsys.readouterr().out + assert "already exists" in output.lower() + + @patch("colony_agent.cli.ColonyClient") + def test_interactive_prompts(self, mock_client_cls, tmp_path, monkeypatch): + mock_client_cls.register.return_value = {"api_key": "col_interactive"} + + inputs = iter(["my-bot", "My Bot", "I help with things", "Cheerful and curious", "music, art, design"]) + monkeypatch.setattr("builtins.input", lambda _prompt: next(inputs)) + + cmd_init(make_init_args(tmp_path, name=None, bio=None)) + + config = json.loads((tmp_path / "agent.json").read_text()) + assert config["identity"]["name"] == "My Bot" + assert config["identity"]["bio"] == "I help with things" + assert config["identity"]["personality"] == "Cheerful and curious" + assert config["identity"]["interests"] == ["music", "art", "design"] + + @patch("colony_agent.cli.ColonyClient") + def test_interactive_defaults(self, mock_client_cls, tmp_path, monkeypatch): + mock_client_cls.register.return_value = {"api_key": "col_defaults"} + + # User presses enter for all defaults except username (required) + inputs = iter(["my-bot", "", "", "", ""]) + monkeypatch.setattr("builtins.input", lambda _prompt: next(inputs)) + + cmd_init(make_init_args(tmp_path, name=None, bio=None)) + + config = json.loads((tmp_path / "agent.json").read_text()) + assert config["identity"]["name"] == "my-bot" # default = username + assert config["identity"]["bio"] == "An AI agent on The Colony." + assert config["identity"]["personality"] == "Friendly, curious, and helpful." + assert config["identity"]["interests"] == ["AI", "agents", "technology"]