Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
59 changes: 48 additions & 11 deletions colony_agent/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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(
Expand All @@ -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", "")
Expand All @@ -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": {
Expand Down Expand Up @@ -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:
Expand Down
125 changes: 121 additions & 4 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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"]
Loading