diff --git a/autogpt/config/ai_config.py b/autogpt/config/ai_config.py index d18c75ba..f335d619 100644 --- a/autogpt/config/ai_config.py +++ b/autogpt/config/ai_config.py @@ -6,7 +6,7 @@ from __future__ import annotations import os from pathlib import Path -from typing import Type +from typing import Type, Optional import yaml from autogpt.prompts.generator import PromptGenerator diff --git a/autogpt/models/base_open_ai_plugin.py b/autogpt/models/base_open_ai_plugin.py index 37a64660..3aafff84 100644 --- a/autogpt/models/base_open_ai_plugin.py +++ b/autogpt/models/base_open_ai_plugin.py @@ -1,5 +1,4 @@ """Handles loading of plugins.""" -import zipfile from typing import Any, Dict, List, Optional, Tuple, TypedDict from typing import TypeVar @@ -11,7 +10,7 @@ class Message(TypedDict): content: str -class BaseOpenAIPluginClient(): +class BaseOpenAIPlugin: """ This is a template for Auto-GPT plugins. """ diff --git a/autogpt/plugins.py b/autogpt/plugins.py index bc822717..2455a89e 100644 --- a/autogpt/plugins.py +++ b/autogpt/plugins.py @@ -14,7 +14,7 @@ from abstract_singleton import AbstractSingleton from openapi_python_client.cli import Config as OpenAPIConfig from autogpt.config import Config -from autogpt.models.base_open_ai_plugin import BaseOpenAIPluginClient +from autogpt.models.base_open_ai_plugin import BaseOpenAIPlugin def inspect_zip_for_module(zip_path: str, debug: bool = False) -> Optional[str]: @@ -160,18 +160,18 @@ def initialize_openai_plugins(manifests_specs: dict, cfg: Config, debug: bool = def instantiate_openai_plugin_clients(manifests_specs_clients: dict, cfg: Config, debug: bool = False) -> dict: """ - Instantiates BaseOpenAIPluginClient instances for each OpenAI plugin. + Instantiates BaseOpenAIPlugin instances for each OpenAI plugin. Args: manifests_specs_clients (dict): per url dictionary of manifest, spec and client. cfg (Config): Config instance including plugins config debug (bool, optional): Enable debug logging. Defaults to False. Returns: - plugins (dict): per url dictionary of BaseOpenAIPluginClient instances. + plugins (dict): per url dictionary of BaseOpenAIPlugin instances. """ plugins = {} for url, manifest_spec_client in manifests_specs_clients.items(): - plugins[url] = BaseOpenAIPluginClient(manifest_spec_client) + plugins[url] = BaseOpenAIPlugin(manifest_spec_client) return plugins @@ -197,7 +197,7 @@ def scan_plugins(cfg: Config, debug: bool = False) -> List[Tuple[str, Path]]: if manifests_specs.keys(): manifests_specs_clients = initialize_openai_plugins(manifests_specs, cfg, debug) for url, openai_plugin_meta in manifests_specs_clients.items(): - plugin = BaseOpenAIPluginClient(openai_plugin_meta) + plugin = BaseOpenAIPlugin(openai_plugin_meta) plugins.append((plugin, url)) return plugins diff --git a/tests/unit/models/test_base_open_api_plugin.py b/tests/unit/models/test_base_open_api_plugin.py new file mode 100644 index 00000000..3dc58d51 --- /dev/null +++ b/tests/unit/models/test_base_open_api_plugin.py @@ -0,0 +1,61 @@ +import pytest +from typing import Any, Dict, List, Optional, Tuple +from autogpt.models.base_open_ai_plugin import BaseOpenAIPlugin, Message, PromptGenerator + + +class DummyPlugin(BaseOpenAIPlugin): + pass + + +@pytest.fixture +def dummy_plugin(): + manifests_specs_clients = { + "manifest": { + "name_for_model": "Dummy", + "schema_version": "1.0", + "description_for_model": "A dummy plugin for testing purposes" + }, + "client": None, + "openapi_spec": None + } + return DummyPlugin(manifests_specs_clients) + + +def test_dummy_plugin_inheritance(dummy_plugin): + assert isinstance(dummy_plugin, BaseOpenAIPlugin) + + +def test_dummy_plugin_name(dummy_plugin): + assert dummy_plugin._name == "Dummy" + + +def test_dummy_plugin_version(dummy_plugin): + assert dummy_plugin._version == "1.0" + + +def test_dummy_plugin_description(dummy_plugin): + assert dummy_plugin._description == "A dummy plugin for testing purposes" + + +def test_dummy_plugin_default_methods(dummy_plugin): + assert not dummy_plugin.can_handle_on_response() + assert not dummy_plugin.can_handle_post_prompt() + assert not dummy_plugin.can_handle_on_planning() + assert not dummy_plugin.can_handle_post_planning() + assert not dummy_plugin.can_handle_pre_instruction() + assert not dummy_plugin.can_handle_on_instruction() + assert not dummy_plugin.can_handle_post_instruction() + assert not dummy_plugin.can_handle_pre_command() + assert not dummy_plugin.can_handle_post_command() + assert not dummy_plugin.can_handle_chat_completion(None, None, None, None) + + assert dummy_plugin.on_response(None) is None + assert dummy_plugin.post_prompt(None) is None + assert dummy_plugin.on_planning(None, None) is None + assert dummy_plugin.post_planning(None) is None + assert dummy_plugin.pre_instruction(None) is None + assert dummy_plugin.on_instruction(None) is None + assert dummy_plugin.post_instruction(None) is None + assert dummy_plugin.pre_command(None, None) is None + assert dummy_plugin.post_command(None, None) is None + assert dummy_plugin.handle_chat_completion(None, None, None, None) is None diff --git a/tests/unit/test_plugins_gpt_generated.py b/tests/unit/test_plugins_gpt_generated.py new file mode 100644 index 00000000..f0cc2c44 --- /dev/null +++ b/tests/unit/test_plugins_gpt_generated.py @@ -0,0 +1,222 @@ +import json +import os +import tempfile +import zipfile +from pathlib import Path +from urllib.parse import urlparse +from unittest.mock import MagicMock, patch + +import pytest + +from autogpt.plugins import create_directory_if_not_exists, inspect_zip_for_module, load_plugins, \ + blacklist_whitelist_check, instantiate_openai_plugin_clients, scan_plugins, fetch_openai_plugins_manifest_and_spec, \ + initialize_openai_plugins + + +PLUGINS_TEST_DIR = "tests/unit/data/test_plugins" +PLUGIN_TEST_ZIP_FILE = "Auto-GPT-Plugin-Test-master.zip" +PLUGIN_TEST_INIT_PY = "Auto-GPT-Plugin-Test-master/src/auto_gpt_plugin_template/__init__.py" + + +def test_inspect_zip_for_module(): + with tempfile.TemporaryDirectory() as temp_dir: + zip_path = os.path.join(temp_dir, "sample.zip") + with zipfile.ZipFile(zip_path, "w") as zf: + zf.writestr("test_module/__init__.py", "") + + result = plugins.inspect_zip_for_module(zip_path) + assert result == "test_module/__init__.py" + + result = plugins.inspect_zip_for_module(zip_path, debug=True) + assert result == "test_module/__init__.py" + + with zipfile.ZipFile(zip_path, "w") as zf: + zf.writestr("not_a_module.py", "") + + result = plugins.inspect_zip_for_module(zip_path) + assert result is None + + +def test_write_dict_to_json_file(): + with tempfile.NamedTemporaryFile(mode="w+", delete=False) as file: + test_data = {"test_key": "test_value"} + plugins.write_dict_to_json_file(test_data, file.name) + + file.seek(0) + loaded_data = json.load(file) + assert loaded_data == test_data + + +def test_create_directory_if_not_exists(): + with tempfile.TemporaryDirectory() as temp_dir: + new_dir = os.path.join(temp_dir, "test_dir") + assert not os.path.exists(new_dir) + + result = create_directory_if_not_exists(new_dir) + assert result is True + assert os.path.exists(new_dir) + + result = create_directory_if_not_exists(new_dir) + assert result is True + + +@pytest.fixture +def config_mock(): + config = MagicMock() + config.plugins_dir = "/plugins" + config.plugins_openai = [] + return config + + +@patch("autogpt.plugins.write_dict_to_json_file") +@patch("requests.get") +@patch("autogpt.plugins.create_directory_if_not_exists") +def test_fetch_openai_plugins_manifest_and_spec(create_directory_mock, write_dict_mock, requests_get_mock, config_mock): + requests_get_mock.side_effect = [ + MagicMock(status_code=200, json=lambda: { + "schema_version": "v1", + "api": {"type": "openapi", "url": "http://example.com/openapi.json"} + }), + MagicMock(status_code=404), + ] + + config_mock.plugins_openai = ["http://example.com"] + + result = fetch_openai_plugins_manifest_and_spec(config_mock) + assert len(result) == 1 + assert "http://example.com" in result + assert "manifest" in result["http://example.com"] + assert "openapi_spec" in result["http://example.com"] + + create_directory_mock.assert_called_once() + write_dict_mock.assert_called_once() + requests_get_mock.assert_has_calls([ + patch("requests.get", args=("http://example.com/.well-known/ai-plugin.json",)), + patch("requests.get", args=("http://example.com/openapi.json",)), + ]) + + +# @patch("BaseOpenAIPlugin") +# def test_instantiate_openai_plugin_clients(base_openai_plugin_client_mock, config_mock): +# manifests_specs_clients = { +# "http://example.com": { +# "manifest": {}, +# "openapi_spec": {}, +# "client": MagicMock(), +# } +# } +# +# result = instantiate_openai_plugin_clients(manifests_specs_clients, config_mock) +# assert len(result) == 1 + + +def test_scan_plugins(config_mock): + with patch("inspect_zip_for_module", return_value="test_module/__init__.py"): + plugins = scan_plugins(config_mock) + assert len(plugins) == 0 + + +# @patch("BaseOpenAIPlugin") +# def test_initialize_openai_plugins(base_openai_plugin_client_mock, config_mock): +# manifests_specs = { +# "http://example.com": { +# "manifest": {}, +# "openapi_spec": {}, +# } +# } +# +# with patch("Path.cwd") as cwd_mock: +# cwd_mock.return_value = Path("/fake_cwd") +# result = initialize_openai_plugins(manifests_specs, config_mock) +# assert len(result) == 1 +# assert "http://example.com" in result +# assert "client" in result["http://example.com"] + + +def test_blacklist_whitelist_check(config_mock): + class Plugin1(MagicMock): + __name__ = "Plugin1" + + class Plugin2(MagicMock): + __name__ = "Plugin2" + + config_mock.plugins_blacklist = ["Plugin1"] + config_mock.plugins_whitelist = ["Plugin2"] + + plugins = [Plugin1, Plugin2] + result = blacklist_whitelist_check(plugins, config_mock) + assert len(result) == 1 + assert isinstance(result[0], Plugin2) + + config_mock.plugins_blacklist = [] + config_mock.plugins_whitelist = [] + + with patch("builtins.input", side_effect=["y", "n"]): + result = blacklist_whitelist_check(plugins, config_mock) + assert len(result) == 1 + assert isinstance(result[0], Plugin1) + + +@patch("autogpt.plugins.scan_plugins") +@patch("autogpt.plugins.blacklist_whitelist_check") +def test_load_plugins(blacklist_whitelist_check_mock, scan_plugins_mock, config_mock): + load_plugins(cfg=config_mock, debug=True) + + scan_plugins_mock.assert_called_once_with(config_mock) + blacklist_whitelist_check_mock.assert_called_once_with(scan_plugins_mock.return_value, config_mock) + +def test_inspect_zip_for_module_no_init_py(): + with patch("zipfile.ZipFile") as zip_mock: + zip_mock.return_value.__enter__.return_value.namelist.return_value = ["test_module/file1.py"] + + result = inspect_zip_for_module("test_module.zip") + assert result is None + + +def test_create_directory_if_not_exists_error(): + with patch("os.makedirs") as makedirs_mock: + makedirs_mock.side_effect = OSError("Error creating directory") + + result = create_directory_if_not_exists("non_existent_dir") + assert result is False + + +def test_fetch_openai_plugins_manifest_and_spec_invalid_manifest(): + with patch("requests.get") as get_mock: + get_mock.return_value.status_code = 200 + get_mock.return_value.json.return_value = { + "schema_version": "v2", + "api": {"type": "openapi"}, + } + + config = MagicMock() + config.plugins_openai = ["http://example.com"] + + result = fetch_openai_plugins_manifest_and_spec(config) + assert result == {} + + +# @patch("BaseOpenAIPlugin") +# def test_instantiate_openai_plugin_clients_invalid_input(base_openai_plugin_client_mock): +# with pytest.raises(TypeError): +# instantiate_openai_plugin_clients("invalid_input", MagicMock()) + + +def test_scan_plugins_invalid_config(): + with pytest.raises(AttributeError): + scan_plugins("invalid_config") + + +def test_blacklist_whitelist_check_invalid_plugins_input(): + with pytest.raises(TypeError): + blacklist_whitelist_check("invalid_plugins_input", MagicMock()) + + +def test_blacklist_whitelist_check_invalid_config_input(): + with pytest.raises(TypeError): + blacklist_whitelist_check([], "invalid_config_input") + + +def test_load_plugins_invalid_config_input(): + with pytest.raises(TypeError): + load_plugins("invalid_config_input")