diff --git a/colony_agent/cli.py b/colony_agent/cli.py index dfa612d..db25267 100644 --- a/colony_agent/cli.py +++ b/colony_agent/cli.py @@ -41,6 +41,11 @@ def main() -> None: status_p = sub.add_parser("status", help="Show agent status") status_p.add_argument("--config", default=DEFAULT_CONFIG, help="Config file path") + # test-llm + test_p = sub.add_parser("test-llm", help="Test the LLM connection and configuration") + test_p.add_argument("--config", default=DEFAULT_CONFIG, help="Config file path") + test_p.add_argument("--prompt", help="Custom prompt to send (default: a simple greeting)") + args = parser.parse_args() if args.command == "init": @@ -49,6 +54,8 @@ def main() -> None: cmd_run(args) elif args.command == "status": cmd_status(args) + elif args.command == "test-llm": + cmd_test_llm(args) else: parser.print_help() sys.exit(1) @@ -255,5 +262,64 @@ def cmd_status(args: argparse.Namespace) -> None: print("Memory has been trimmed (contains summary)") +def cmd_test_llm(args: argparse.Namespace) -> None: + """Test the LLM connection by sending a simple prompt.""" + import time + + from colony_agent.config import AgentConfig + from colony_agent.llm import build_system_prompt, chat + + config = AgentConfig.from_file(args.config) + errors = config.validate() + if errors: + for e in errors: + print(f"Config error: {e}") + sys.exit(1) + + identity = config.identity + system_prompt = build_system_prompt( + identity.name, + identity.personality, + identity.interests, + identity.system_prompt, + identity.system_prompt_suffix, + ) + + user_prompt = args.prompt or ( + "Say hello and introduce yourself in one sentence. " + "This is a test to verify the LLM connection is working." + ) + + print(f"LLM: {config.llm.provider}") + print(f"Model: {config.llm.model}") + print(f"URL: {config.llm.base_url}") + print(f"Agent: {identity.name}") + print() + print("Sending test prompt...") + + start = time.time() + response = chat(config.llm, [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ]) + elapsed = time.time() - start + + if response: + print(f"Response ({elapsed:.1f}s):\n") + print(f" {response}") + print() + print("LLM connection is working.") + else: + print() + print("No response from LLM. Check your configuration:") + print(f" - Is the LLM running at {config.llm.base_url}?") + print(f" - Is the model '{config.llm.model}' available?") + if config.llm.api_key: + print(" - Is the API key correct?") + else: + print(" - Does the provider require an API key? (llm.api_key is empty)") + sys.exit(1) + + if __name__ == "__main__": main() diff --git a/tests/test_cli.py b/tests/test_cli.py index 7a28c50..df7211a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,7 +6,7 @@ import pytest from colony_sdk.client import ColonyAPIError -from colony_agent.cli import cmd_init, cmd_status +from colony_agent.cli import cmd_init, cmd_status, cmd_test_llm def make_status_args(config_path: str): @@ -236,3 +236,69 @@ def test_interactive_defaults(self, mock_client_cls, tmp_path, monkeypatch): 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"] + + +def make_test_llm_args(config_path: str, prompt: str | None = None): + """Create args for cmd_test_llm.""" + args = MagicMock() + args.config = config_path + args.prompt = prompt + return args + + +class TestCmdTestLLM: + @patch("colony_agent.llm.chat", return_value="Hello! I am TestBot, nice to meet you.") + def test_successful_connection(self, mock_chat, tmp_path, capsys): + config_path = write_config(tmp_path) + cmd_test_llm(make_test_llm_args(config_path)) + + output = capsys.readouterr().out + assert "Hello! I am TestBot" in output + assert "working" in output.lower() + + @patch("colony_agent.llm.chat", return_value="") + def test_no_response_shows_troubleshooting(self, mock_chat, tmp_path, capsys): + config_path = write_config(tmp_path) + with pytest.raises(SystemExit): + cmd_test_llm(make_test_llm_args(config_path)) + + output = capsys.readouterr().out + assert "No response" in output + assert "localhost:11434" in output + + @patch("colony_agent.llm.chat", return_value="Custom response.") + def test_custom_prompt(self, mock_chat, tmp_path, capsys): + config_path = write_config(tmp_path) + cmd_test_llm(make_test_llm_args(config_path, prompt="What is 2+2?")) + + output = capsys.readouterr().out + assert "Custom response" in output + # Verify the custom prompt was sent + call_messages = mock_chat.call_args[0][1] + assert call_messages[-1]["content"] == "What is 2+2?" + + @patch("colony_agent.llm.chat", return_value="Works!") + def test_shows_llm_config(self, mock_chat, tmp_path, capsys): + config_path = write_config(tmp_path) + cmd_test_llm(make_test_llm_args(config_path)) + + output = capsys.readouterr().out + assert "openai-compatible" in output + assert "qwen3:8b" in output or "localhost" in output + + @patch("colony_agent.llm.chat", return_value="Response!") + def test_shows_response_time(self, mock_chat, tmp_path, capsys): + config_path = write_config(tmp_path) + cmd_test_llm(make_test_llm_args(config_path)) + + output = capsys.readouterr().out + assert "s)" in output # e.g. "(0.1s)" + + @patch("colony_agent.llm.chat", return_value="") + def test_warns_about_missing_api_key(self, mock_chat, tmp_path, capsys): + config_path = write_config(tmp_path) + with pytest.raises(SystemExit): + cmd_test_llm(make_test_llm_args(config_path)) + + output = capsys.readouterr().out + assert "API key" in output or "api_key" in output