diff --git a/README.md b/README.md index e8621ba..12ca66d 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ Un agente configurabile basato su DeepAgents che supporta più provider LLM. - **Estensibile**: Aggiungi strumenti personalizzati, modifica i prompt e configura i sub-agenti secondo le tue esigenze - **Strumenti unificati**: Gestisci qualsiasi provider con la stessa interfaccia - **Strumenti personalizzati**: Include strumenti per gestire Proxmox e Ollama +- **Completamento shell**: Supporta bash e zsh per autocompletamento dei comandi +- **Test completi**: Suite di test per garantire la qualità del codice ## Installazione @@ -20,6 +22,21 @@ Un agente configurabile basato su DeepAgents che supporta più provider LLM. pip install -r requirements.txt ``` +### Configurazione del Completamento Shell + +#### Bash +```bash +# Aggiungi al tuo ~/.bashrc o ~/.bash_profile +source /path/to/maxclaw_agent/completions/maxclaw.bash +``` + +#### Zsh +```bash +# Aggiungi al tuo ~/.zshrc +fpath=(/path/to/maxclaw_agent/completions $fpath) +autoload -Uz compinit && compinit +``` + ## Configurazione Puoi configurare MaxClaw Agent in diversi modi: @@ -202,6 +219,21 @@ Gli strumenti Ollama specifici sono ancora disponibili per retrocompatibilità: - `ollama_status`: Controlla se Ollama è in esecuzione - `ollama_running_models`: Mostra i modelli attualmente in esecuzione +## Test + +Esegui la suite di test: +```bash +# Esegui tutti i test +pytest tests/ + +# Esegui test specifici +pytest tests/test_config_manager.py +pytest tests/test_provider_tools.py + +# Esegui con output verboso +pytest tests/ -v +``` + ## Personalizzazione Per personalizzare l'agente, modifica `main.py`: @@ -210,13 +242,6 @@ Per personalizzare l'agente, modifica `main.py`: - Aggiungi strumenti personalizzati alla chiamata `create_deep_agent` - Configura i sub-agenti o altre funzionalità di DeepAgents -## Test - -Esegui lo script di test: -```bash -python test_agent.py -``` - ## Sicurezza MaxClaw Agent implementa misure di sicurezza avanzate: diff --git a/completions/maxclaw.bash b/completions/maxclaw.bash new file mode 100644 index 0000000..59e4499 --- /dev/null +++ b/completions/maxclaw.bash @@ -0,0 +1,88 @@ +#!/bin/bash +# Bash completion for MaxClaw Agent CLI + +_maxclaw_completions() +{ + local cur prev commands + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + # Main commands + commands="list search add remove set-active active test export import help" + + # Handle main command completion + if [[ ${COMP_CWORD} -eq 1 ]]; then + COMPREPLY=( $(compgen -W "${commands}" -- ${cur}) ) + return 0 + fi + + # Handle subcommand-specific completions + case "${prev}" in + search) + # Search term - no completion + return 0 + ;; + add) + # Add command options + local add_options="--non-interactive --name --provider --model --api-key --base-url --set-active" + COMPREPLY=( $(compgen -W "${add_options}" -- ${cur}) ) + return 0 + ;; + remove|set-active|test) + # Configuration names - try to get from config list + if command -v python3 &> /dev/null; then + local configs=$(python3 -c " +import sys +sys.path.insert(0, '.') +try: + from config_manager import config_manager + configs = config_manager.list_configuration_names() + print(' '.join(configs)) +except: + pass +" 2>/dev/null) + COMPREPLY=( $(compgen -W "${configs}" -- ${cur}) ) + fi + return 0 + ;; + export) + # Export options + local export_options="--format --output" + COMPREPLY=( $(compgen -W "${export_options}" -- ${cur}) ) + return 0 + ;; + import) + # File completion + COMPREPLY=( $(compgen -f -- ${cur}) ) + return 0 + ;; + --provider) + # Provider options + local providers="openai ollama openrouter" + COMPREPLY=( $(compgen -W "${providers}" -- ${cur}) ) + return 0 + ;; + --format) + # Format options + local formats="json yaml" + COMPREPLY=( $(compgen -W "${formats}" -- ${cur}) ) + return 0 + ;; + esac + + # Handle option arguments + case "${cur}" in + -*) + # Option completion + local options="--non-interactive --name --provider --model --api-key --base-url --set-active --format --output" + COMPREPLY=( $(compgen -W "${options}" -- ${cur}) ) + return 0 + ;; + esac + + return 0 +} + +complete -F _maxclaw_completions python3 config_cli.py +complete -F _maxclaw_completions python config_cli.py \ No newline at end of file diff --git a/completions/maxclaw.zsh b/completions/maxclaw.zsh new file mode 100644 index 0000000..b56b33d --- /dev/null +++ b/completions/maxclaw.zsh @@ -0,0 +1,79 @@ +#compdef config_cli.py +# Zsh completion for MaxClaw Agent CLI + +_maxclaw() { + local -a commands + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments -C \ + '1: :->command' \ + '*::arg:->args' + + case $state in + command) + commands=( + 'list:List all configurations' + 'search:Search configurations' + 'add:Add a new configuration' + 'remove:Remove a configuration' + 'set-active:Set active configuration' + 'active:Show active configuration' + 'test:Test connection to a configuration' + 'export:Export configurations to JSON/YAML' + 'import:Import configurations from JSON/YAML' + 'help:Show help' + ) + _describe -t commands 'MaxClaw commands' commands + ;; + args) + case ${line[1]} in + search) + _message 'search term' + ;; + add) + _arguments \ + '--non-interactive[Non-interactive mode]' \ + '--name[Configuration name]:name' \ + '--provider[Provider]:provider:(openai ollama openrouter)' \ + '--model[Model name]:model' \ + '--api-key[API key]:api_key' \ + '--base-url[Base URL]:base_url' \ + '--set-active[Set as active configuration]' + ;; + remove|set-active|test) + # Try to get configuration names + local -a configs + if (( $+commands[python3] )); then + configs=(${(f)"$(python3 -c " +import sys +sys.path.insert(0, '.') +try: + from config_manager import config_manager + configs = config_manager.list_configuration_names() + for c in configs: + print(c) +except: + pass +" 2>/dev/null)"}) + fi + if (( ${#configs} )); then + _describe -t configurations 'configurations' configs + else + _message 'configuration name' + fi + ;; + export) + _arguments \ + '--format[Export format]:format:(json yaml)' \ + '--output[Output filename]:filename' + ;; + import) + _files -g '*.json *.yaml *.yml' + ;; + esac + ;; + esac +} + +_maxclaw "$@" \ No newline at end of file diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py new file mode 100644 index 0000000..a5d9313 --- /dev/null +++ b/tests/test_config_manager.py @@ -0,0 +1,186 @@ +""" +Test suite for MaxClaw Agent configuration manager +""" +import os +import json +import tempfile +import pytest +from pathlib import Path +import sys + +# Add parent directory to path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from config_manager import ConfigManager + + +class TestConfigManager: + """Test cases for ConfigManager""" + + def setup_method(self): + """Set up test environment""" + self.temp_dir = tempfile.mkdtemp() + self.config_manager = ConfigManager(self.temp_dir) + + def teardown_method(self): + """Clean up test environment""" + import shutil + shutil.rmtree(self.temp_dir, ignore_errors=True) + + def test_initialization(self): + """Test ConfigManager initialization""" + assert self.config_manager.config_dir.exists() + assert self.config_manager.config_file.exists() + assert self.config_manager.key_file.exists() + assert self.config_manager.config == {"configurations": {}, "active_configuration": None, "version": "1.0"} + + def test_add_configuration(self): + """Test adding a configuration""" + config = { + "provider": "ollama", + "model_name": "gemma2:9b", + "base_url": "http://localhost:11434" + } + + self.config_manager.add_configuration("test-config", config) + + # Verify configuration was added + assert self.config_manager.configuration_exists("test-config") + retrieved = self.config_manager.get_configuration("test-config") + assert retrieved["provider"] == "ollama" + assert retrieved["model_name"] == "gemma2:9b" + assert retrieved["base_url"] == "http://localhost:11434" + + def test_remove_configuration(self): + """Test removing a configuration""" + config = { + "provider": "openai", + "model_name": "gpt-4o", + "api_key": "sk-test123" + } + + self.config_manager.add_configuration("test-openai", config) + assert self.config_manager.configuration_exists("test-openai") + + # Remove configuration + result = self.config_manager.remove_configuration("test-openai") + assert result is True + assert not self.config_manager.configuration_exists("test-openai") + + def test_set_active_configuration(self): + """Test setting active configuration""" + config = { + "provider": "ollama", + "model_name": "gemma2:9b", + "base_url": "http://localhost:11434" + } + + self.config_manager.add_configuration("test-config", config) + + # Set as active + result = self.config_manager.set_active_configuration("test-config") + assert result is True + assert self.config_manager.get_active_configuration() == "test-config" + + def test_validation(self): + """Test configuration validation""" + # Valid configuration + valid_config = { + "provider": "ollama", + "model_name": "gemma2:9b", + "base_url": "http://localhost:11434" + } + + is_valid, errors = self.config_manager.validate_configuration(valid_config) + assert is_valid is True + assert len(errors) == 0 + + # Invalid configuration - missing provider + invalid_config = { + "model_name": "gemma2:9b", + "base_url": "http://localhost:11434" + } + + is_valid, errors = self.config_manager.validate_configuration(invalid_config) + assert is_valid is False + assert "Provider is required" in errors + + # Invalid configuration - OpenAI without API key + invalid_openai = { + "provider": "openai", + "model_name": "gpt-4o" + } + + is_valid, errors = self.config_manager.validate_configuration(invalid_openai) + assert is_valid is False + assert "API key is required for OpenAI provider" in errors + + def test_encryption(self): + """Test that API keys are encrypted""" + config = { + "provider": "openai", + "model_name": "gpt-4o", + "api_key": "sk-test123456789" + } + + self.config_manager.add_configuration("test-openai", config) + + # Read the raw config file + with open(self.config_manager.config_file, 'r') as f: + raw_config = json.load(f) + + # Check that API key is encrypted in the file + stored_config = raw_config["configurations"]["test-openai"] + assert "api_key" in stored_config + assert stored_config["api_key"] != "sk-test123456789" # Should be encrypted + assert len(stored_config["api_key"]) > 20 # Encrypted data is longer + + # But when retrieved, it should be decrypted + retrieved = self.config_manager.get_configuration("test-openai") + assert retrieved["api_key"] == "sk-test123456789" + + def test_list_configurations(self): + """Test listing configurations""" + # Add multiple configurations + configs = [ + ("config1", {"provider": "ollama", "model_name": "gemma2:9b", "base_url": "http://localhost:11434"}), + ("config2", {"provider": "openai", "model_name": "gpt-4o", "api_key": "sk-test"}), + ("config3", {"provider": "openrouter", "model_name": "claude-3.5-sonnet", "api_key": "sk-or-test", "base_url": "https://openrouter.ai/api/v1"}) + ] + + for name, config in configs: + self.config_manager.add_configuration(name, config) + + # List configurations + config_names = self.config_manager.list_configuration_names() + assert len(config_names) == 3 + assert "config1" in config_names + assert "config2" in config_names + assert "config3" in config_names + + def test_backup_creation(self): + """Test that backups are created""" + config = { + "provider": "ollama", + "model_name": "gemma2:9b", + "base_url": "http://localhost:11434" + } + + # Add first configuration + self.config_manager.add_configuration("config1", config) + + # Add second configuration (should create backup) + config2 = { + "provider": "openai", + "model_name": "gpt-4o", + "api_key": "sk-test" + } + self.config_manager.add_configuration("config2", config2) + + # Check that backup file exists + backup_file = self.config_manager.config_file.with_suffix('.json.bak') + assert backup_file.exists() + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/tests/test_provider_tools.py b/tests/test_provider_tools.py new file mode 100644 index 0000000..b7dfa65 --- /dev/null +++ b/tests/test_provider_tools.py @@ -0,0 +1,256 @@ +""" +Test suite for MaxClaw Agent provider tools +""" +import os +import sys +import pytest +from unittest.mock import Mock, patch, MagicMock + +# Add parent directory to path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from tools.provider_tools import ( + OllamaProviderTools, + OpenAIProviderTools, + OpenRouterProviderTools, + get_provider_tools +) + + +class TestOllamaProviderTools: + """Test cases for OllamaProviderTools""" + + def setup_method(self): + """Set up test environment""" + self.tools = OllamaProviderTools(base_url="http://localhost:11434") + + def test_initialization(self): + """Test OllamaProviderTools initialization""" + assert self.tools.provider_name == "ollama" + assert self.tools.base_url == "http://localhost:11434" + assert self.tools.api_url == "http://localhost:11434/api" + + @patch('requests.get') + def test_list_models_success(self, mock_get): + """Test successful model listing""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "models": [ + {"name": "gemma2:9b", "size": 5000000000}, + {"name": "llama2:7b", "size": 4000000000} + ] + } + mock_get.return_value = mock_response + + result = self.tools.list_models() + + assert "Available Ollama Models" in result + assert "gemma2:9b" in result + assert "llama2:7b" in result + + @patch('requests.get') + def test_list_models_failure(self, mock_get): + """Test failed model listing""" + mock_get.side_effect = Exception("Connection failed") + + result = self.tools.list_models() + + assert "Error listing models" in result + + @patch('requests.post') + def test_get_model_info_success(self, mock_post): + """Test successful model info retrieval""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "license": "Apache 2.0", + "parameters": "9B", + "details": { + "parameter_size": "9B", + "quantization_level": "Q4_0" + } + } + mock_post.return_value = mock_response + + result = self.tools.get_model_info("gemma2:9b") + + assert "Model Information: gemma2:9b" in result + assert "License: Apache 2.0" in result + assert "Parameter Size: 9B" in result + + @patch('requests.get') + def test_test_connection_success(self, mock_get): + """Test successful connection test""" + mock_response = Mock() + mock_response.status_code = 200 + mock_get.return_value = mock_response + + result = self.tools.test_connection() + + assert "✅ Ollama instance at http://localhost:11434 is running" in result + + @patch('requests.get') + def test_test_connection_failure(self, mock_get): + """Test failed connection test""" + mock_get.side_effect = Exception("Connection failed") + + result = self.tools.test_connection() + + assert "❌ Cannot connect to Ollama instance" in result + + def test_get_tools(self): + """Test tool generation""" + tools = self.tools.get_tools() + + assert len(tools) == 6 + tool_names = [tool.name for tool in tools] + assert "ollama_list_models" in tool_names + assert "ollama_model_info" in tool_names + assert "ollama_test_connection" in tool_names + assert "ollama_get_default_model" in tool_names + assert "ollama_set_default_model" in tool_names + assert "ollama_info" in tool_names + + +class TestOpenAIProviderTools: + """Test cases for OpenAIProviderTools""" + + def setup_method(self): + """Set up test environment""" + self.tools = OpenAIProviderTools(api_key="sk-test123") + + def test_initialization(self): + """Test OpenAIProviderTools initialization""" + assert self.tools.provider_name == "openai" + assert self.tools.api_key == "sk-test123" + assert self.tools.api_url == "https://api.openai.com/v1" + + @patch('requests.get') + def test_list_models_success(self, mock_get): + """Test successful model listing""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "data": [ + {"id": "gpt-4o"}, + {"id": "gpt-4o-mini"}, + {"id": "gpt-3.5-turbo"} + ] + } + mock_get.return_value = mock_response + + result = self.tools.list_models() + + assert "Available OpenAI Models" in result + assert "gpt-4o" in result + assert "gpt-4o-mini" in result + + @patch('requests.get') + def test_test_connection_success(self, mock_get): + """Test successful connection test""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {"data": []} + mock_get.return_value = mock_response + + result = self.tools.test_connection() + + assert "✅ OpenAI API connection successful" in result + + def test_get_tools(self): + """Test tool generation""" + tools = self.tools.get_tools() + + assert len(tools) == 6 + tool_names = [tool.name for tool in tools] + assert "openai_list_models" in tool_names + assert "openai_model_info" in tool_names + assert "openai_test_connection" in tool_names + + +class TestOpenRouterProviderTools: + """Test cases for OpenRouterProviderTools""" + + def setup_method(self): + """Set up test environment""" + self.tools = OpenRouterProviderTools( + api_key="sk-or-test123", + base_url="https://openrouter.ai/api/v1" + ) + + def test_initialization(self): + """Test OpenRouterProviderTools initialization""" + assert self.tools.provider_name == "openrouter" + assert self.tools.api_key == "sk-or-test123" + assert self.tools.base_url == "https://openrouter.ai/api/v1" + + @patch('requests.get') + def test_list_models_success(self, mock_get): + """Test successful model listing""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "data": [ + {"id": "anthropic/claude-3.5-sonnet", "name": "Claude 3.5 Sonnet"}, + {"id": "openai/gpt-4o", "name": "GPT-4o"} + ] + } + mock_get.return_value = mock_response + + result = self.tools.list_models() + + assert "Available OpenRouter Models" in result + assert "anthropic/claude-3.5-sonnet" in result + assert "openai/gpt-4o" in result + + def test_get_tools(self): + """Test tool generation""" + tools = self.tools.get_tools() + + assert len(tools) == 6 + tool_names = [tool.name for tool in tools] + assert "openrouter_list_models" in tool_names + assert "openrouter_model_info" in tool_names + assert "openrouter_test_connection" in tool_names + + +class TestGetProviderTools: + """Test cases for get_provider_tools function""" + + @patch.dict(os.environ, {"OLLAMA_BASE_URL": "http://localhost:11434"}) + def test_get_ollama_tools(self): + """Test getting Ollama tools""" + tools = get_provider_tools("ollama") + + assert len(tools) == 6 + tool_names = [tool.name for tool in tools] + assert "ollama_list_models" in tool_names + + @patch.dict(os.environ, {"OPENAI_API_KEY": "sk-test123"}) + def test_get_openai_tools(self): + """Test getting OpenAI tools""" + tools = get_provider_tools("openai") + + assert len(tools) == 6 + tool_names = [tool.name for tool in tools] + assert "openai_list_models" in tool_names + + @patch.dict(os.environ, {"OPENROUTER_API_KEY": "sk-or-test123"}) + def test_get_openrouter_tools(self): + """Test getting OpenRouter tools""" + tools = get_provider_tools("openrouter") + + assert len(tools) == 6 + tool_names = [tool.name for tool in tools] + assert "openrouter_list_models" in tool_names + + def test_get_unknown_provider_tools(self): + """Test getting tools for unknown provider""" + tools = get_provider_tools("unknown") + + assert len(tools) == 0 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file