From a24ab0e87994eb7e314fb9e2ff17a97f68408aba Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 6 Apr 2023 14:13:23 -0700 Subject: [PATCH 01/60] dynamically load commands from registry --- scripts/agent_manager.py | 4 ++ scripts/ai_functions.py | 8 ++- scripts/auto_gpt/__init__.py | 0 scripts/auto_gpt/commands.py | 114 +++++++++++++++++++++++++++++++++++ scripts/commands.py | 62 ++++--------------- scripts/execute_code.py | 2 + scripts/file_operations.py | 27 +++++---- scripts/main.py | 10 ++- 8 files changed, 160 insertions(+), 67 deletions(-) create mode 100644 scripts/auto_gpt/__init__.py create mode 100644 scripts/auto_gpt/commands.py diff --git a/scripts/agent_manager.py b/scripts/agent_manager.py index ad120c40..9bd87aa9 100644 --- a/scripts/agent_manager.py +++ b/scripts/agent_manager.py @@ -1,3 +1,4 @@ +from auto_gpt.commands import command from llm_utils import create_chat_completion next_key = 0 @@ -31,6 +32,7 @@ def create_agent(task, prompt, model): return key, agent_reply +@command("message_agent", "Message GPT Agent", '"key": "", "message": ""') def message_agent(key, message): global agents @@ -51,6 +53,7 @@ def message_agent(key, message): return agent_reply +@command("list_agents", "List GPT Agents", "") def list_agents(): global agents @@ -58,6 +61,7 @@ def list_agents(): return [(key, task) for key, (task, _, _) in agents.items()] +@command("delete_agent", "Delete GPT Agent", '"key": ""') def delete_agent(key): global agents diff --git a/scripts/ai_functions.py b/scripts/ai_functions.py index 05aa93a2..175dffa2 100644 --- a/scripts/ai_functions.py +++ b/scripts/ai_functions.py @@ -3,10 +3,12 @@ import json from config import Config from call_ai_function import call_ai_function from json_parser import fix_and_parse_json +from auto_gpt.commands import command + cfg = Config() # Evaluating code - +@command("evaluate_code", "Evaluate Code", '"code": ""') def evaluate_code(code: str) -> List[str]: function_string = "def analyze_code(code: str) -> List[str]:" args = [code] @@ -18,7 +20,7 @@ def evaluate_code(code: str) -> List[str]: # Improving code - +@command("improve_code", "Get Improved Code", '"suggestions": "", "code": ""') def improve_code(suggestions: List[str], code: str) -> str: function_string = ( "def generate_improved_code(suggestions: List[str], code: str) -> str:" @@ -32,7 +34,7 @@ def improve_code(suggestions: List[str], code: str) -> str: # Writing tests - +@command("write_tests", "Write Tests", '"code": "", "focus": ""') def write_tests(code: str, focus: List[str]) -> str: function_string = ( "def create_test_cases(code: str, focus: Optional[str] = None) -> str:" diff --git a/scripts/auto_gpt/__init__.py b/scripts/auto_gpt/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/auto_gpt/commands.py b/scripts/auto_gpt/commands.py new file mode 100644 index 00000000..fb05fdb8 --- /dev/null +++ b/scripts/auto_gpt/commands.py @@ -0,0 +1,114 @@ +import os +import sys +import importlib +import inspect +from typing import Callable, Any, List + +# Unique identifier for auto-gpt commands +AUTO_GPT_COMMAND_IDENTIFIER = "auto_gpt_command" + +class Command: + """A class representing a command. + + Attributes: + name (str): The name of the command. + description (str): A brief description of what the command does. + method (Callable[..., Any]): The function that the command executes. + signature (str): The signature of the function that the command executes. Defaults to None. + """ + + def __init__(self, name: str, description: str, method: Callable[..., Any], signature: str = None): + self.name = name + self.description = description + self.method = method + self.signature = signature if signature else str(inspect.signature(self.method)) + + def __call__(self, *args, **kwargs) -> Any: + return self.method(*args, **kwargs) + + def __str__(self) -> str: + return f"{self.name}: {self.description}, args: {self.signature}" + +class CommandRegistry: + """ + The CommandRegistry class is a manager for a collection of Command objects. + It allows the registration, modification, and retrieval of Command objects, + as well as the scanning and loading of command plugins from a specified + directory. + """ + + def __init__(self): + self.commands = {} + + def _import_module(self, module_name: str) -> Any: + return importlib.import_module(module_name) + + def _reload_module(self, module: Any) -> Any: + return importlib.reload(module) + + def register_command(self, cmd: Command) -> None: + self.commands[cmd.name] = cmd + + def reload_commands(self) -> None: + """Reloads all loaded command plugins.""" + for cmd_name in self.commands: + cmd = self.commands[cmd_name] + module = self._import_module(cmd.__module__) + reloaded_module = self._reload_module(module) + if hasattr(reloaded_module, "register"): + reloaded_module.register(self) + + def get_command(self, name: str) -> Callable[..., Any]: + return self.commands.get(name) + + def list_commands(self) -> List[str]: + return [str(cmd) for cmd in self.commands.values()] + + def command_prompt(self) -> str: + """ + Returns a string representation of all registered `Command` objects for use in a prompt + """ + commands_list = [f"{idx + 1}. {str(cmd)}" for idx, cmd in enumerate(self.commands.values())] + return "\n".join(commands_list) + + def scan_directory_for_plugins(self, directory: str) -> None: + """ + Scans the specified directory for Python files containing command plugins. + + For each file in the directory that ends with ".py", this method imports the associated module and registers any + functions or classes that are decorated with the `AUTO_GPT_COMMAND_IDENTIFIER` attribute as `Command` objects. + The registered `Command` objects are then added to the `commands` dictionary of the `CommandRegistry` object. + + Args: + directory (str): The directory to scan for command plugins. + """ + + for file in os.listdir(directory): + if file.endswith(".py"): + module_name = file[:-3] + module = importlib.import_module(module_name) + for attr_name in dir(module): + attr = getattr(module, attr_name) + # Register decorated functions + if hasattr(attr, AUTO_GPT_COMMAND_IDENTIFIER) and getattr(attr, AUTO_GPT_COMMAND_IDENTIFIER): + self.register_command(attr.register_command) + # Register command classes + elif inspect.isclass(attr) and issubclass(attr, Command) and attr != Command: + cmd_instance = attr() + self.register_command(cmd_instance) + + +def command(name: str, description: str, signature: str = None) -> Callable[..., Any]: + """The command decorator is used to create Command objects from ordinary functions.""" + def decorator(func: Callable[..., Any]) -> Command: + cmd = Command(name=name, description=description, method=func, signature=signature) + + def wrapper(*args, **kwargs) -> Any: + return func(*args, **kwargs) + + wrapper.register_command = cmd + setattr(wrapper, AUTO_GPT_COMMAND_IDENTIFIER, True) + return wrapper + + return decorator + diff --git a/scripts/commands.py b/scripts/commands.py index fc10d1d0..78f5dbe3 100644 --- a/scripts/commands.py +++ b/scripts/commands.py @@ -5,13 +5,10 @@ import datetime import agent_manager as agents import speak from config import Config -import ai_functions as ai -from file_operations import read_file, write_to_file, append_to_file, delete_file, search_files -from execute_code import execute_python_file from json_parser import fix_and_parse_json from duckduckgo_search import ddg -from googleapiclient.discovery import build -from googleapiclient.errors import HttpError + +from auto_gpt.commands import CommandRegistry, command cfg = Config() @@ -51,62 +48,27 @@ def get_command(response): return "Error:", str(e) -def execute_command(command_name, arguments): - memory = PineconeMemory() +def execute_command(command_registry: CommandRegistry, command_name: str, arguments: dict) -> str: try: + # Look up the command in the registry + cmd = command_registry.commands.get(command_name) + + # If the command is found, call it with the provided arguments + if cmd: + return cmd(**arguments) + # special case google until this can be moved down into the function. if command_name == "google": - # Check if the Google API key is set and use the official search method # If the API key is not set or has only whitespaces, use the unofficial search method if cfg.google_api_key and (cfg.google_api_key.strip() if cfg.google_api_key else None): return google_official_search(arguments["input"]) else: return google_search(arguments["input"]) - elif command_name == "memory_add": - return memory.add(arguments["string"]) - elif command_name == "start_agent": - return start_agent( - arguments["name"], - arguments["task"], - arguments["prompt"]) - elif command_name == "message_agent": - return message_agent(arguments["key"], arguments["message"]) - elif command_name == "list_agents": - return list_agents() - elif command_name == "delete_agent": - return delete_agent(arguments["key"]) - elif command_name == "get_text_summary": - return get_text_summary(arguments["url"], arguments["question"]) - elif command_name == "get_hyperlinks": - return get_hyperlinks(arguments["url"]) - elif command_name == "read_file": - return read_file(arguments["file"]) - elif command_name == "write_to_file": - return write_to_file(arguments["file"], arguments["text"]) - elif command_name == "append_to_file": - return append_to_file(arguments["file"], arguments["text"]) - elif command_name == "delete_file": - return delete_file(arguments["file"]) - elif command_name == "search_files": - return search_files(arguments["directory"]) - elif command_name == "browse_website": - return browse_website(arguments["url"], arguments["question"]) - # TODO: Change these to take in a file rather than pasted code, if - # non-file is given, return instructions "Input should be a python - # filepath, write your code to file and try again" - elif command_name == "evaluate_code": - return ai.evaluate_code(arguments["code"]) - elif command_name == "improve_code": - return ai.improve_code(arguments["suggestions"], arguments["code"]) - elif command_name == "write_tests": - return ai.write_tests(arguments["code"], arguments.get("focus")) - elif command_name == "execute_python_file": # Add this command - return execute_python_file(arguments["file"]) elif command_name == "task_complete": shutdown() else: return f"Unknown command {command_name}" - # All errors, return "Error: + error message" + except Exception as e: return "Error: " + str(e) @@ -158,6 +120,7 @@ def google_official_search(query, num_results=8): # Return the list of search result URLs return search_results_links +@command("browse_website", "Browse Website", '"url": "", "question": ""') def browse_website(url, question): summary = get_text_summary(url, question) links = get_hyperlinks(url) @@ -230,6 +193,7 @@ def shutdown(): quit() +@command("start_agent", "Start GPT Agent", '"name": "", "task": "", "prompt": ""') def start_agent(name, task, prompt, model=cfg.fast_llm_model): global cfg diff --git a/scripts/execute_code.py b/scripts/execute_code.py index 614ef6fc..a90ab4a0 100644 --- a/scripts/execute_code.py +++ b/scripts/execute_code.py @@ -1,7 +1,9 @@ import docker import os +from auto_gpt.commands import command +@command("execute_python_file", "Execute Python File", '"file": ""') def execute_python_file(file): workspace_folder = "auto_gpt_workspace" diff --git a/scripts/file_operations.py b/scripts/file_operations.py index 90c9a1e4..140123c5 100644 --- a/scripts/file_operations.py +++ b/scripts/file_operations.py @@ -1,5 +1,7 @@ import os import os.path +from auto_gpt.commands import command + # Set a dedicated folder for file I/O working_directory = "auto_gpt_workspace" @@ -17,20 +19,20 @@ def safe_join(base, *paths): return norm_new_path - -def read_file(filename): +@command("read_file", "Read file", '"file": ""') +def read_file(file): try: - filepath = safe_join(working_directory, filename) + filepath = safe_join(working_directory, file) with open(filepath, "r") as f: content = f.read() return content except Exception as e: return "Error: " + str(e) - -def write_to_file(filename, text): +@command("write_to_file", "Write to file", '"file": "", "text": ""') +def write_to_file(file, text): try: - filepath = safe_join(working_directory, filename) + filepath = safe_join(working_directory, file) directory = os.path.dirname(filepath) if not os.path.exists(directory): os.makedirs(directory) @@ -40,25 +42,26 @@ def write_to_file(filename, text): except Exception as e: return "Error: " + str(e) - -def append_to_file(filename, text): +@command("append_to_file", "Append to file", '"file": "", "text": ""') +def append_to_file(file, text): try: - filepath = safe_join(working_directory, filename) + filepath = safe_join(working_directory, file) with open(filepath, "a") as f: f.write(text) return "Text appended successfully." except Exception as e: return "Error: " + str(e) - -def delete_file(filename): +@command("delete_file", "Delete file", '"file": ""') +def delete_file(file): try: - filepath = safe_join(working_directory, filename) + filepath = safe_join(working_directory, file) os.remove(filepath) return "File deleted successfully." except Exception as e: return "Error: " + str(e) +@command("search_files", "Search Files", '"directory": ""') def search_files(directory): found_files = [] diff --git a/scripts/main.py b/scripts/main.py index a79fd553..5337ceeb 100644 --- a/scripts/main.py +++ b/scripts/main.py @@ -8,14 +8,13 @@ from colorama import Fore, Style from spinner import Spinner import time import speak -from enum import Enum, auto -import sys from config import Config from json_parser import fix_and_parse_json from ai_config import AIConfig import traceback import yaml import argparse +from auto_gpt.commands import CommandRegistry def print_to_console( @@ -281,6 +280,7 @@ next_action_count = 0 # Make a constant: user_input = "Determine which next command to use, and respond using the format specified above:" + # Initialize memory and make sure it is empty. # this is particularly important for indexing and referencing pinecone memory memory = PineconeMemory() @@ -288,6 +288,10 @@ memory.clear() print('Using memory of type: ' + memory.__class__.__name__) +# Create a CommandRegistry instance and scan default folder +command_registry = CommandRegistry() +command_registry.scan_directory_for_plugins('./scripts') + # Interaction Loop while True: # Send message to AI, get response @@ -362,7 +366,7 @@ while True: elif command_name == "human_feedback": result = f"Human feedback: {user_input}" else: - result = f"Command {command_name} returned: {cmd.execute_command(command_name, arguments)}" + result = f"Command {command_name} returned: {cmd.execute_command(command_registry, command_name, arguments)}" if next_action_count > 0: next_action_count -= 1 From e2a6ed6955bdcf9487ca288581988848ff0c87f7 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 6 Apr 2023 18:24:53 -0700 Subject: [PATCH 02/60] adding tests for CommandRegistry --- scripts/auto_gpt/tests/__init__.py | 0 scripts/auto_gpt/tests/mock/__init__.py | 0 scripts/auto_gpt/tests/mock/mock_commands.py | 12 ++ scripts/auto_gpt/tests/test_commands.py | 174 +++++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 scripts/auto_gpt/tests/__init__.py create mode 100644 scripts/auto_gpt/tests/mock/__init__.py create mode 100644 scripts/auto_gpt/tests/mock/mock_commands.py create mode 100644 scripts/auto_gpt/tests/test_commands.py diff --git a/scripts/auto_gpt/tests/__init__.py b/scripts/auto_gpt/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/auto_gpt/tests/mock/__init__.py b/scripts/auto_gpt/tests/mock/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/auto_gpt/tests/mock/mock_commands.py b/scripts/auto_gpt/tests/mock/mock_commands.py new file mode 100644 index 00000000..3514b62a --- /dev/null +++ b/scripts/auto_gpt/tests/mock/mock_commands.py @@ -0,0 +1,12 @@ +from commands import Command, command + +class TestCommand(Command): + def __init__(self): + super().__init__(name='class_based', description='Class-based test command') + + def __call__(self, arg1: int, arg2: str) -> str: + return f'{arg1} - {arg2}' + +@command('function_based', 'Function-based test command') +def function_based(arg1: int, arg2: str) -> str: + return f'{arg1} - {arg2}' diff --git a/scripts/auto_gpt/tests/test_commands.py b/scripts/auto_gpt/tests/test_commands.py new file mode 100644 index 00000000..e2681798 --- /dev/null +++ b/scripts/auto_gpt/tests/test_commands.py @@ -0,0 +1,174 @@ +from pathlib import Path + +import pytest +from commands import Command, CommandRegistry + + +class TestCommand: + @staticmethod + def example_function(arg1: int, arg2: str) -> str: + return f"{arg1} - {arg2}" + + def test_command_creation(self): + cmd = Command(name="example", description="Example command", method=self.example_function) + + assert cmd.name == "example" + assert cmd.description == "Example command" + assert cmd.method == self.example_function + assert cmd.signature == "arg1: int, arg2: str" + + def test_command_call(self): + cmd = Command(name="example", description="Example command", method=self.example_function) + + result = cmd(arg1=1, arg2="test") + assert result == "1 - test" + + def test_command_call_with_invalid_arguments(self): + cmd = Command(name="example", description="Example command", method=self.example_function) + + with pytest.raises(TypeError): + cmd(arg1="invalid", arg2="test") + + def test_command_default_signature(self): + cmd = Command(name="example", description="Example command", method=self.example_function) + + assert cmd.signature == "arg1: int, arg2: str" + + def test_command_custom_signature(self): + custom_signature = "custom_arg1: int, custom_arg2: str" + cmd = Command(name="example", description="Example command", method=self.example_function, signature=custom_signature) + + assert cmd.signature == custom_signature + + + +class TestCommandRegistry: + @staticmethod + def example_function(arg1: int, arg2: str) -> str: + return f"{arg1} - {arg2}" + + def test_register_command(self): + """Test that a command can be registered to the registry.""" + registry = CommandRegistry() + cmd = Command(name="example", description="Example command", method=self.example_function) + + registry.register(cmd) + + assert cmd.name in registry._commands + assert registry._commands[cmd.name] == cmd + + def test_unregister_command(self): + """Test that a command can be unregistered from the registry.""" + registry = CommandRegistry() + cmd = Command(name="example", description="Example command", method=self.example_function) + + registry.register(cmd) + registry.unregister(cmd.name) + + assert cmd.name not in registry._commands + + def test_get_command(self): + """Test that a command can be retrieved from the registry.""" + registry = CommandRegistry() + cmd = Command(name="example", description="Example command", method=self.example_function) + + registry.register(cmd) + retrieved_cmd = registry.get(cmd.name) + + assert retrieved_cmd == cmd + + def test_get_nonexistent_command(self): + """Test that attempting to get a nonexistent command raises a KeyError.""" + registry = CommandRegistry() + + with pytest.raises(KeyError): + registry.get("nonexistent_command") + + def test_call_command(self): + """Test that a command can be called through the registry.""" + registry = CommandRegistry() + cmd = Command(name="example", description="Example command", method=self.example_function) + + registry.register(cmd) + result = registry.call("example", arg1=1, arg2="test") + + assert result == "1 - test" + + def test_call_nonexistent_command(self): + """Test that attempting to call a nonexistent command raises a KeyError.""" + registry = CommandRegistry() + + with pytest.raises(KeyError): + registry.call("nonexistent_command", arg1=1, arg2="test") + + def test_get_command_list(self): + """Test that a list of registered commands can be retrieved.""" + registry = CommandRegistry() + cmd1 = Command(name="example1", description="Example command 1", method=self.example_function) + cmd2 = Command(name="example2", description="Example command 2", method=self.example_function) + + registry.register(cmd1) + registry.register(cmd2) + command_list = registry.get_command_list() + + assert len(command_list) == 2 + assert cmd1.name in command_list + assert cmd2.name in command_list + + def test_get_command_prompt(self): + """Test that the command prompt is correctly formatted.""" + registry = CommandRegistry() + cmd = Command(name="example", description="Example command", method=self.example_function) + + registry.register(cmd) + command_prompt = registry.get_command_prompt() + + assert f"{cmd.name}: {cmd.description}, args: {cmd.signature}" in command_prompt + + def test_scan_directory_for_mock_commands(self): + """Test that the registry can scan a directory for mock command plugins.""" + registry = CommandRegistry() + mock_commands_dir = Path("auto_gpt/tests/mocks") + + registry.scan_directory_for_plugins(mock_commands_dir) + + assert "mock_class_based" in registry._commands + assert registry._commands["mock_class_based"].name == "mock_class_based" + assert registry._commands["mock_class_based"].description == "Mock class-based command" + + assert "mock_function_based" in registry._commands + assert registry._commands["mock_function_based"].name == "mock_function_based" + assert registry._commands["mock_function_based"].description == "Mock function-based command" + + def test_scan_directory_for_temp_command_file(self, tmp_path): + """Test that the registry can scan a directory for command plugins in a temp file.""" + registry = CommandRegistry() + temp_commands_dir = tmp_path / "temp_commands" + temp_commands_dir.mkdir() + + # Create a temp command file + temp_commands_file = temp_commands_dir / "temp_commands.py" + temp_commands_content = ( + "from commands import Command, command\n\n" + "class TempCommand(Command):\n" + " def __init__(self):\n" + " super().__init__(name='temp_class_based', description='Temp class-based command')\n\n" + " def __call__(self, arg1: int, arg2: str) -> str:\n" + " return f'{arg1} - {arg2}'\n\n" + "@command('temp_function_based', 'Temp function-based command')\n" + "def temp_function_based(arg1: int, arg2: str) -> str:\n" + " return f'{arg1} - {arg2}'\n" + ) + + with open(temp_commands_file, "w") as f: + f.write(temp_commands_content) + + registry.scan_directory_for_plugins(temp_commands_dir) + + assert "temp_class_based" in registry._commands + assert registry._commands["temp_class_based"].name == "temp_class_based" + assert registry._commands["temp_class_based"].description == "Temp class-based command" + + assert "temp_function_based" in registry._commands + assert registry._commands["temp_function_based"].name == "temp_function_based" + assert registry._commands["temp_function_based"].description == "Temp function-based command" From b4a0ef9babf9edfbd395a1afaaab22381a522643 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 6 Apr 2023 19:25:44 -0700 Subject: [PATCH 03/60] resolving test failures --- {scripts/auto_gpt => auto_gpt}/__init__.py | 0 {scripts/auto_gpt => auto_gpt}/commands.py | 33 +++++-- .../auto_gpt => auto_gpt}/tests/__init__.py | 0 .../mock => auto_gpt/tests/mocks}/__init__.py | 0 auto_gpt/tests/mocks/mock_commands.py | 6 ++ .../tests/test_commands.py | 86 ++++++------------- scripts/ai_functions.py | 3 +- scripts/auto_gpt/tests/mock/mock_commands.py | 12 --- scripts/commands.py | 1 - 9 files changed, 58 insertions(+), 83 deletions(-) rename {scripts/auto_gpt => auto_gpt}/__init__.py (100%) rename {scripts/auto_gpt => auto_gpt}/commands.py (78%) rename {scripts/auto_gpt => auto_gpt}/tests/__init__.py (100%) rename {scripts/auto_gpt/tests/mock => auto_gpt/tests/mocks}/__init__.py (100%) create mode 100644 auto_gpt/tests/mocks/mock_commands.py rename {scripts/auto_gpt => auto_gpt}/tests/test_commands.py (53%) delete mode 100644 scripts/auto_gpt/tests/mock/mock_commands.py diff --git a/scripts/auto_gpt/__init__.py b/auto_gpt/__init__.py similarity index 100% rename from scripts/auto_gpt/__init__.py rename to auto_gpt/__init__.py diff --git a/scripts/auto_gpt/commands.py b/auto_gpt/commands.py similarity index 78% rename from scripts/auto_gpt/commands.py rename to auto_gpt/commands.py index fb05fdb8..0133c665 100644 --- a/scripts/auto_gpt/commands.py +++ b/auto_gpt/commands.py @@ -13,7 +13,6 @@ class Command: Attributes: name (str): The name of the command. description (str): A brief description of what the command does. - method (Callable[..., Any]): The function that the command executes. signature (str): The signature of the function that the command executes. Defaults to None. """ @@ -46,9 +45,15 @@ class CommandRegistry: def _reload_module(self, module: Any) -> Any: return importlib.reload(module) - def register_command(self, cmd: Command) -> None: + def register(self, cmd: Command) -> None: self.commands[cmd.name] = cmd + def unregister(self, command_name: str): + if command_name in self.commands: + del self.commands[command_name] + else: + raise KeyError(f"Command '{command_name}' not found in registry.") + def reload_commands(self) -> None: """Reloads all loaded command plugins.""" for cmd_name in self.commands: @@ -59,10 +64,13 @@ class CommandRegistry: reloaded_module.register(self) def get_command(self, name: str) -> Callable[..., Any]: - return self.commands.get(name) + return self.commands[name] - def list_commands(self) -> List[str]: - return [str(cmd) for cmd in self.commands.values()] + def call(self, command_name: str, **kwargs) -> Any: + if command_name not in self.commands: + raise KeyError(f"Command '{command_name}' not found in registry.") + command = self.commands[command_name] + return command(**kwargs) def command_prompt(self) -> str: """ @@ -85,17 +93,23 @@ class CommandRegistry: for file in os.listdir(directory): if file.endswith(".py"): + file_path = os.path.join(directory, file) module_name = file[:-3] - module = importlib.import_module(module_name) + + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + + spec.loader.exec_module(module) + for attr_name in dir(module): attr = getattr(module, attr_name) # Register decorated functions if hasattr(attr, AUTO_GPT_COMMAND_IDENTIFIER) and getattr(attr, AUTO_GPT_COMMAND_IDENTIFIER): - self.register_command(attr.register_command) + self.register(attr.command) # Register command classes elif inspect.isclass(attr) and issubclass(attr, Command) and attr != Command: cmd_instance = attr() - self.register_command(cmd_instance) + self.register(cmd_instance) def command(name: str, description: str, signature: str = None) -> Callable[..., Any]: @@ -106,7 +120,8 @@ def command(name: str, description: str, signature: str = None) -> Callable[..., def wrapper(*args, **kwargs) -> Any: return func(*args, **kwargs) - wrapper.register_command = cmd + wrapper.command = cmd + setattr(wrapper, AUTO_GPT_COMMAND_IDENTIFIER, True) return wrapper diff --git a/scripts/auto_gpt/tests/__init__.py b/auto_gpt/tests/__init__.py similarity index 100% rename from scripts/auto_gpt/tests/__init__.py rename to auto_gpt/tests/__init__.py diff --git a/scripts/auto_gpt/tests/mock/__init__.py b/auto_gpt/tests/mocks/__init__.py similarity index 100% rename from scripts/auto_gpt/tests/mock/__init__.py rename to auto_gpt/tests/mocks/__init__.py diff --git a/auto_gpt/tests/mocks/mock_commands.py b/auto_gpt/tests/mocks/mock_commands.py new file mode 100644 index 00000000..d68ceb81 --- /dev/null +++ b/auto_gpt/tests/mocks/mock_commands.py @@ -0,0 +1,6 @@ +from auto_gpt.commands import Command, command + + +@command('function_based', 'Function-based test command') +def function_based(arg1: int, arg2: str) -> str: + return f'{arg1} - {arg2}' diff --git a/scripts/auto_gpt/tests/test_commands.py b/auto_gpt/tests/test_commands.py similarity index 53% rename from scripts/auto_gpt/tests/test_commands.py rename to auto_gpt/tests/test_commands.py index e2681798..fc0ccb3d 100644 --- a/scripts/auto_gpt/tests/test_commands.py +++ b/auto_gpt/tests/test_commands.py @@ -1,7 +1,8 @@ +import shutil from pathlib import Path import pytest -from commands import Command, CommandRegistry +from auto_gpt.commands import Command, CommandRegistry class TestCommand: @@ -15,7 +16,7 @@ class TestCommand: assert cmd.name == "example" assert cmd.description == "Example command" assert cmd.method == self.example_function - assert cmd.signature == "arg1: int, arg2: str" + assert cmd.signature == "(arg1: int, arg2: str) -> str" def test_command_call(self): cmd = Command(name="example", description="Example command", method=self.example_function) @@ -27,12 +28,12 @@ class TestCommand: cmd = Command(name="example", description="Example command", method=self.example_function) with pytest.raises(TypeError): - cmd(arg1="invalid", arg2="test") + cmd(arg1="invalid", does_not_exist="test") def test_command_default_signature(self): cmd = Command(name="example", description="Example command", method=self.example_function) - assert cmd.signature == "arg1: int, arg2: str" + assert cmd.signature == "(arg1: int, arg2: str) -> str" def test_command_custom_signature(self): custom_signature = "custom_arg1: int, custom_arg2: str" @@ -54,8 +55,8 @@ class TestCommandRegistry: registry.register(cmd) - assert cmd.name in registry._commands - assert registry._commands[cmd.name] == cmd + assert cmd.name in registry.commands + assert registry.commands[cmd.name] == cmd def test_unregister_command(self): """Test that a command can be unregistered from the registry.""" @@ -65,7 +66,7 @@ class TestCommandRegistry: registry.register(cmd) registry.unregister(cmd.name) - assert cmd.name not in registry._commands + assert cmd.name not in registry.commands def test_get_command(self): """Test that a command can be retrieved from the registry.""" @@ -73,7 +74,7 @@ class TestCommandRegistry: cmd = Command(name="example", description="Example command", method=self.example_function) registry.register(cmd) - retrieved_cmd = registry.get(cmd.name) + retrieved_cmd = registry.get_command(cmd.name) assert retrieved_cmd == cmd @@ -82,7 +83,7 @@ class TestCommandRegistry: registry = CommandRegistry() with pytest.raises(KeyError): - registry.get("nonexistent_command") + registry.get_command("nonexistent_command") def test_call_command(self): """Test that a command can be called through the registry.""" @@ -101,74 +102,41 @@ class TestCommandRegistry: with pytest.raises(KeyError): registry.call("nonexistent_command", arg1=1, arg2="test") - def test_get_command_list(self): - """Test that a list of registered commands can be retrieved.""" - registry = CommandRegistry() - cmd1 = Command(name="example1", description="Example command 1", method=self.example_function) - cmd2 = Command(name="example2", description="Example command 2", method=self.example_function) - - registry.register(cmd1) - registry.register(cmd2) - command_list = registry.get_command_list() - - assert len(command_list) == 2 - assert cmd1.name in command_list - assert cmd2.name in command_list - def test_get_command_prompt(self): """Test that the command prompt is correctly formatted.""" registry = CommandRegistry() cmd = Command(name="example", description="Example command", method=self.example_function) registry.register(cmd) - command_prompt = registry.get_command_prompt() + command_prompt = registry.command_prompt() - assert f"{cmd.name}: {cmd.description}, args: {cmd.signature}" in command_prompt + assert f"(arg1: int, arg2: str)" in command_prompt def test_scan_directory_for_mock_commands(self): - """Test that the registry can scan a directory for mock command plugins.""" + """Test that the registry can scan a directory for mocks command plugins.""" registry = CommandRegistry() - mock_commands_dir = Path("auto_gpt/tests/mocks") + mock_commands_dir = Path("/app/auto_gpt/tests/mocks") + import os + print(os.getcwd()) registry.scan_directory_for_plugins(mock_commands_dir) - assert "mock_class_based" in registry._commands - assert registry._commands["mock_class_based"].name == "mock_class_based" - assert registry._commands["mock_class_based"].description == "Mock class-based command" - - assert "mock_function_based" in registry._commands - assert registry._commands["mock_function_based"].name == "mock_function_based" - assert registry._commands["mock_function_based"].description == "Mock function-based command" + assert "function_based" in registry.commands + assert registry.commands["function_based"].name == "function_based" + assert registry.commands["function_based"].description == "Function-based test command" def test_scan_directory_for_temp_command_file(self, tmp_path): """Test that the registry can scan a directory for command plugins in a temp file.""" registry = CommandRegistry() - temp_commands_dir = tmp_path / "temp_commands" - temp_commands_dir.mkdir() # Create a temp command file - temp_commands_file = temp_commands_dir / "temp_commands.py" - temp_commands_content = ( - "from commands import Command, command\n\n" - "class TempCommand(Command):\n" - " def __init__(self):\n" - " super().__init__(name='temp_class_based', description='Temp class-based command')\n\n" - " def __call__(self, arg1: int, arg2: str) -> str:\n" - " return f'{arg1} - {arg2}'\n\n" - "@command('temp_function_based', 'Temp function-based command')\n" - "def temp_function_based(arg1: int, arg2: str) -> str:\n" - " return f'{arg1} - {arg2}'\n" - ) + src = Path("/app/auto_gpt/tests/mocks/mock_commands.py") + temp_commands_file = tmp_path / "mock_commands.py" + shutil.copyfile(src, temp_commands_file) - with open(temp_commands_file, "w") as f: - f.write(temp_commands_content) + registry.scan_directory_for_plugins(tmp_path) + print(registry.commands) - registry.scan_directory_for_plugins(temp_commands_dir) - - assert "temp_class_based" in registry._commands - assert registry._commands["temp_class_based"].name == "temp_class_based" - assert registry._commands["temp_class_based"].description == "Temp class-based command" - - assert "temp_function_based" in registry._commands - assert registry._commands["temp_function_based"].name == "temp_function_based" - assert registry._commands["temp_function_based"].description == "Temp function-based command" + assert "function_based" in registry.commands + assert registry.commands["function_based"].name == "function_based" + assert registry.commands["function_based"].description == "Function-based test command" diff --git a/scripts/ai_functions.py b/scripts/ai_functions.py index 175dffa2..c7cb9a5a 100644 --- a/scripts/ai_functions.py +++ b/scripts/ai_functions.py @@ -1,8 +1,7 @@ -from typing import List, Optional +from typing import List import json from config import Config from call_ai_function import call_ai_function -from json_parser import fix_and_parse_json from auto_gpt.commands import command cfg = Config() diff --git a/scripts/auto_gpt/tests/mock/mock_commands.py b/scripts/auto_gpt/tests/mock/mock_commands.py deleted file mode 100644 index 3514b62a..00000000 --- a/scripts/auto_gpt/tests/mock/mock_commands.py +++ /dev/null @@ -1,12 +0,0 @@ -from commands import Command, command - -class TestCommand(Command): - def __init__(self): - super().__init__(name='class_based', description='Class-based test command') - - def __call__(self, arg1: int, arg2: str) -> str: - return f'{arg1} - {arg2}' - -@command('function_based', 'Function-based test command') -def function_based(arg1: int, arg2: str) -> str: - return f'{arg1} - {arg2}' diff --git a/scripts/commands.py b/scripts/commands.py index 78f5dbe3..2a78031a 100644 --- a/scripts/commands.py +++ b/scripts/commands.py @@ -1,6 +1,5 @@ import browse import json -from memory import PineconeMemory import datetime import agent_manager as agents import speak From 3095591064b213c4e916a9c6d7d1e85c6b0c80c8 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 6 Apr 2023 20:00:28 -0700 Subject: [PATCH 04/60] switch to explicit module imports --- auto_gpt/commands.py | 46 ++++++++++++++------------------- auto_gpt/tests/test_commands.py | 25 +++++++++++------- scripts/__init__.py | 0 scripts/main.py | 6 ++++- 4 files changed, 39 insertions(+), 38 deletions(-) create mode 100644 scripts/__init__.py diff --git a/auto_gpt/commands.py b/auto_gpt/commands.py index 0133c665..2f9016ba 100644 --- a/auto_gpt/commands.py +++ b/auto_gpt/commands.py @@ -79,38 +79,30 @@ class CommandRegistry: commands_list = [f"{idx + 1}. {str(cmd)}" for idx, cmd in enumerate(self.commands.values())] return "\n".join(commands_list) - def scan_directory_for_plugins(self, directory: str) -> None: + def import_commands(self, module_name: str) -> None: """ - Scans the specified directory for Python files containing command plugins. + Imports the specified Python module containing command plugins. - For each file in the directory that ends with ".py", this method imports the associated module and registers any - functions or classes that are decorated with the `AUTO_GPT_COMMAND_IDENTIFIER` attribute as `Command` objects. - The registered `Command` objects are then added to the `commands` dictionary of the `CommandRegistry` object. + This method imports the associated module and registers any functions or + classes that are decorated with the `AUTO_GPT_COMMAND_IDENTIFIER` attribute + as `Command` objects. The registered `Command` objects are then added to the + `commands` dictionary of the `CommandRegistry` object. - Args: - directory (str): The directory to scan for command plugins. - """ + Args: + module_name (str): The name of the module to import for command plugins. + """ - for file in os.listdir(directory): - if file.endswith(".py"): - file_path = os.path.join(directory, file) - module_name = file[:-3] - - spec = importlib.util.spec_from_file_location(module_name, file_path) - module = importlib.util.module_from_spec(spec) - - spec.loader.exec_module(module) - - for attr_name in dir(module): - attr = getattr(module, attr_name) - # Register decorated functions - if hasattr(attr, AUTO_GPT_COMMAND_IDENTIFIER) and getattr(attr, AUTO_GPT_COMMAND_IDENTIFIER): - self.register(attr.command) - # Register command classes - elif inspect.isclass(attr) and issubclass(attr, Command) and attr != Command: - cmd_instance = attr() - self.register(cmd_instance) + module = importlib.import_module(module_name) + for attr_name in dir(module): + attr = getattr(module, attr_name) + # Register decorated functions + if hasattr(attr, AUTO_GPT_COMMAND_IDENTIFIER) and getattr(attr, AUTO_GPT_COMMAND_IDENTIFIER): + self.register(attr.command) + # Register command classes + elif inspect.isclass(attr) and issubclass(attr, Command) and attr != Command: + cmd_instance = attr() + self.register(cmd_instance) def command(name: str, description: str, signature: str = None) -> Callable[..., Any]: """The command decorator is used to create Command objects from ordinary functions.""" diff --git a/auto_gpt/tests/test_commands.py b/auto_gpt/tests/test_commands.py index fc0ccb3d..a7778b6e 100644 --- a/auto_gpt/tests/test_commands.py +++ b/auto_gpt/tests/test_commands.py @@ -1,4 +1,5 @@ import shutil +import sys from pathlib import Path import pytest @@ -112,21 +113,19 @@ class TestCommandRegistry: assert f"(arg1: int, arg2: str)" in command_prompt - def test_scan_directory_for_mock_commands(self): - """Test that the registry can scan a directory for mocks command plugins.""" + def test_import_mock_commands_module(self): + """Test that the registry can import a module with mock command plugins.""" registry = CommandRegistry() - mock_commands_dir = Path("/app/auto_gpt/tests/mocks") - import os + mock_commands_module = "auto_gpt.tests.mocks.mock_commands" - print(os.getcwd()) - registry.scan_directory_for_plugins(mock_commands_dir) + registry.import_commands(mock_commands_module) assert "function_based" in registry.commands assert registry.commands["function_based"].name == "function_based" assert registry.commands["function_based"].description == "Function-based test command" - def test_scan_directory_for_temp_command_file(self, tmp_path): - """Test that the registry can scan a directory for command plugins in a temp file.""" + def test_import_temp_command_file_module(self, tmp_path): + """Test that the registry can import a command plugins module from a temp file.""" registry = CommandRegistry() # Create a temp command file @@ -134,8 +133,14 @@ class TestCommandRegistry: temp_commands_file = tmp_path / "mock_commands.py" shutil.copyfile(src, temp_commands_file) - registry.scan_directory_for_plugins(tmp_path) - print(registry.commands) + # Add the temp directory to sys.path to make the module importable + sys.path.append(str(tmp_path)) + + temp_commands_module = "mock_commands" + registry.import_commands(temp_commands_module) + + # Remove the temp directory from sys.path + sys.path.remove(str(tmp_path)) assert "function_based" in registry.commands assert registry.commands["function_based"].name == "function_based" diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scripts/main.py b/scripts/main.py index 5337ceeb..4d7faa51 100644 --- a/scripts/main.py +++ b/scripts/main.py @@ -290,7 +290,11 @@ print('Using memory of type: ' + memory.__class__.__name__) # Create a CommandRegistry instance and scan default folder command_registry = CommandRegistry() -command_registry.scan_directory_for_plugins('./scripts') +command_registry.import_commands('scripts.ai_functions') +command_registry.import_commands('scripts.commands') +command_registry.import_commands('scripts.execute_code') +command_registry.import_commands('scripts.agent_manager') +command_registry.import_commands('scripts.file_operations') # Interaction Loop while True: From 65b626c5e171f6a27888040ba742e2748cf74d30 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Mon, 10 Apr 2023 20:57:47 -0500 Subject: [PATCH 05/60] Plugins initial --- README.md | 11 +++++ plugin.png | Bin 0 -> 33356 bytes plugins/__PUT_PLUGIN_ZIPS_HERE__ | 0 scripts/config.py | 8 ++++ scripts/llm_utils.py | 6 ++- scripts/main.py | 27 +++++++++++ scripts/plugins.py | 74 +++++++++++++++++++++++++++++++ 7 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 plugin.png create mode 100644 plugins/__PUT_PLUGIN_ZIPS_HERE__ create mode 100644 scripts/plugins.py diff --git a/README.md b/README.md index 749c8791..2f0ce155 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,17 @@ export PINECONE_ENV="Your pinecone region" # something like: us-east4-gcp ``` +## Plugins + +See https://github.com/Torantulino/Auto-GPT-Plugin-Template for the template of the plugins. + +WARNING: Review the code of any plugin you use. + +Drop the repo's zipfile in the plugins folder. + +![Download Zip](https://raw.githubusercontent.com/BillSchumacher/Auto-GPT/master/plugin.png) + +If you add the plugins class name to the whitelist in the config.py you will not be prompted otherwise you'll be warned before loading the plugin. ## View Memory Usage diff --git a/plugin.png b/plugin.png new file mode 100644 index 0000000000000000000000000000000000000000..865ce3c922d7783efde0ebaa273519eca202d654 GIT binary patch literal 33356 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�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 literal 0 HcmV?d00001 diff --git a/plugins/__PUT_PLUGIN_ZIPS_HERE__ b/plugins/__PUT_PLUGIN_ZIPS_HERE__ new file mode 100644 index 00000000..e69de29b diff --git a/scripts/config.py b/scripts/config.py index 27cc946c..7c39d72b 100644 --- a/scripts/config.py +++ b/scripts/config.py @@ -81,6 +81,10 @@ class Config(metaclass=Singleton): # Initialize the OpenAI API client openai.api_key = self.openai_api_key + self.plugins = [] + self.plugins_whitelist = [] + self.plugins_blacklist = [] + def set_continuous_mode(self, value: bool): """Set the continuous mode value.""" self.continuous_mode = value @@ -135,3 +139,7 @@ class Config(metaclass=Singleton): def set_debug_mode(self, value: bool): """Set the debug mode value.""" self.debug = value + + def set_plugins(self, value: list): + """Set the plugins value.""" + self.plugins = value diff --git a/scripts/llm_utils.py b/scripts/llm_utils.py index 94ba5f13..28867494 100644 --- a/scripts/llm_utils.py +++ b/scripts/llm_utils.py @@ -22,5 +22,7 @@ def create_chat_completion(messages, model=None, temperature=None, max_tokens=No temperature=temperature, max_tokens=max_tokens ) - - return response.choices[0].message["content"] + resp = response.choices[0].message["content"] + for plugin in cfg.plugins: + resp = plugin.on_response(resp) + return resp diff --git a/scripts/main.py b/scripts/main.py index 4be0b2aa..4411fb51 100644 --- a/scripts/main.py +++ b/scripts/main.py @@ -13,7 +13,10 @@ from enum import Enum, auto import sys from config import Config from json_parser import fix_and_parse_json +from plugins import load_plugins from ai_config import AIConfig +import os +from pathlib import Path import traceback import yaml import argparse @@ -323,6 +326,30 @@ user_input = "Determine which next command to use, and respond using the format memory = get_memory(cfg, init=True) print('Using memory of type: ' + memory.__class__.__name__) + +plugins_found = load_plugins(Path(os.getcwd()) / "plugins") +loaded_plugins = [] +for plugin in plugins_found: + if plugin.__name__ in cfg.plugins_blacklist: + continue + if plugin.__name__ in cfg.plugins_whitelist: + loaded_plugins.append(plugin()) + else: + ack = input( + f"WARNNG Plugin {plugin.__name__} found. But not in the" + " whitelist... Load? (y/n): " + ) + if ack.lower() == "y": + loaded_plugins.append(plugin()) + +if loaded_plugins: + print(f"\nPlugins found: {len(loaded_plugins)}\n" + "--------------------") +for plugin in loaded_plugins: + print(f"{plugin._name}: {plugin._version} - {plugin._description}") + +cfg.set_plugins(loaded_plugins) + # Interaction Loop while True: # Send message to AI, get response diff --git a/scripts/plugins.py b/scripts/plugins.py new file mode 100644 index 00000000..44d7e618 --- /dev/null +++ b/scripts/plugins.py @@ -0,0 +1,74 @@ +"""Handles loading of plugins.""" + +from ast import Module +import zipfile +from pathlib import Path +from zipimport import zipimporter +from typing import List, Optional, Tuple + + +def inspect_zip_for_module(zip_path: str, debug: bool = False) -> Optional[str]: + """ + Inspect a zipfile for a module. + + Args: + zip_path (str): Path to the zipfile. + debug (bool, optional): Enable debug logging. Defaults to False. + + Returns: + Optional[str]: The name of the module if found, else None. + """ + with zipfile.ZipFile(zip_path, 'r') as zfile: + for name in zfile.namelist(): + if name.endswith("__init__.py"): + if debug: + print(f"Found module '{name}' in the zipfile at: {name}") + return name + if debug: + print(f"Module '__init__.py' not found in the zipfile @ {zip_path}.") + return None + + +def scan_plugins(plugins_path: Path, debug: bool = False) -> List[Tuple[str, Path]]: + """Scan the plugins directory for plugins. + + Args: + plugins_path (Path): Path to the plugins directory. + + Returns: + List[Path]: List of plugins. + """ + plugins = [] + for plugin in plugins_path.glob("*.zip"): + if module := inspect_zip_for_module(str(plugin), debug): + plugins.append((module, plugin)) + return plugins + + +def load_plugins(plugins_path: Path, debug: bool = False) -> List[Module]: + """Load plugins from the plugins directory. + + Args: + plugins_path (Path): Path to the plugins directory. + + Returns: + List[Path]: List of plugins. + """ + plugins = scan_plugins(plugins_path) + plugin_modules = [] + for module, plugin in plugins: + plugin = Path(plugin) + module = Path(module) + if debug: + print(f"Plugin: {plugin} Module: {module}") + zipped_package = zipimporter(plugin) + zipped_module = zipped_package.load_module(str(module.parent)) + for key in dir(zipped_module): + if key.startswith("__"): + continue + a_module = getattr(zipped_module, key) + a_keys = dir(a_module) + if '_abc_impl' in a_keys and \ + a_module.__name__ != 'AutoGPTPluginTemplate': + plugin_modules.append(a_module) + return plugin_modules From 0b955c0546ca4a8be0217c1c36473456bfa665cf Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Mon, 10 Apr 2023 22:19:21 -0500 Subject: [PATCH 06/60] Update README.md Update warning --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f0ce155..001f0ffc 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ export PINECONE_ENV="Your pinecone region" # something like: us-east4-gcp See https://github.com/Torantulino/Auto-GPT-Plugin-Template for the template of the plugins. -WARNING: Review the code of any plugin you use. +⚠️💀 WARNING 💀⚠️: Review the code of any plugin you use, this allows for any Python to be executed and do malicious things. Like stealing your API keys. Drop the repo's zipfile in the plugins folder. From b7a29e71cd7d9460375da3adf92184a16721fe54 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sat, 15 Apr 2023 22:15:34 -0500 Subject: [PATCH 07/60] Refactor prompts into package, make the prompt able to be stored with the AI config and changed. Fix settings file. --- autogpt/__main__.py | 9 ++++++--- autogpt/app.py | 2 +- autogpt/commands/audio_text.py | 6 ++++-- autogpt/commands/twitter.py | 6 +++--- autogpt/config/ai_config.py | 18 ++++++++++++------ {scripts => autogpt}/plugins.py | 5 ++--- autogpt/prompts/__init__.py | 0 .../generator.py} | 14 ++++++++++++-- autogpt/{ => prompts}/prompt.py | 18 ++++-------------- tests/test_prompt_generator.py | 2 +- tests/unit/test_browse_scrape_text.py | 4 +++- 11 files changed, 48 insertions(+), 36 deletions(-) rename {scripts => autogpt}/plugins.py (93%) create mode 100644 autogpt/prompts/__init__.py rename autogpt/{promptgenerator.py => prompts/generator.py} (91%) rename autogpt/{ => prompts}/prompt.py (94%) diff --git a/autogpt/__main__.py b/autogpt/__main__.py index 8888c206..5cde8b55 100644 --- a/autogpt/__main__.py +++ b/autogpt/__main__.py @@ -1,5 +1,7 @@ """Main script for the autogpt package.""" import logging +import os +from pathlib import Path from colorama import Fore from autogpt.agent.agent import Agent from autogpt.args import parse_arguments @@ -8,7 +10,9 @@ from autogpt.config import Config, check_openai_api_key from autogpt.logs import logger from autogpt.memory import get_memory -from autogpt.prompt import construct_prompt +from autogpt.prompts.prompt import construct_prompt +from autogpt.plugins import load_plugins + # Load environment variables from .env file @@ -36,8 +40,7 @@ def main() -> None: loaded_plugins.append(plugin()) if loaded_plugins: - print(f"\nPlugins found: {len(loaded_plugins)}\n" - "--------------------") + print(f"\nPlugins found: {len(loaded_plugins)}\n" "--------------------") for plugin in loaded_plugins: print(f"{plugin._name}: {plugin._version} - {plugin._description}") diff --git a/autogpt/app.py b/autogpt/app.py index e7b16adc..fa5cab62 100644 --- a/autogpt/app.py +++ b/autogpt/app.py @@ -186,7 +186,7 @@ def execute_command(command_name: str, arguments): elif command_name == "generate_image": return generate_image(arguments["prompt"]) elif command_name == "send_tweet": - return send_tweet(arguments['text']) + return send_tweet(arguments["text"]) elif command_name == "do_nothing": return "No action performed." elif command_name == "task_complete": diff --git a/autogpt/commands/audio_text.py b/autogpt/commands/audio_text.py index bf9c3640..b9ca988c 100644 --- a/autogpt/commands/audio_text.py +++ b/autogpt/commands/audio_text.py @@ -23,7 +23,9 @@ def read_audio(audio): headers = {"Authorization": f"Bearer {api_token}"} if api_token is None: - raise ValueError("You need to set your Hugging Face API token in the config file.") + raise ValueError( + "You need to set your Hugging Face API token in the config file." + ) response = requests.post( api_url, @@ -31,5 +33,5 @@ def read_audio(audio): data=audio, ) - text = json.loads(response.content.decode("utf-8"))['text'] + text = json.loads(response.content.decode("utf-8"))["text"] return "The audio says: " + text diff --git a/autogpt/commands/twitter.py b/autogpt/commands/twitter.py index 1774bfb9..dc4d450c 100644 --- a/autogpt/commands/twitter.py +++ b/autogpt/commands/twitter.py @@ -7,9 +7,9 @@ load_dotenv() def send_tweet(tweet_text): consumer_key = os.environ.get("TW_CONSUMER_KEY") - consumer_secret= os.environ.get("TW_CONSUMER_SECRET") - access_token= os.environ.get("TW_ACCESS_TOKEN") - access_token_secret= os.environ.get("TW_ACCESS_TOKEN_SECRET") + consumer_secret = os.environ.get("TW_CONSUMER_SECRET") + access_token = os.environ.get("TW_ACCESS_TOKEN") + access_token_secret = os.environ.get("TW_ACCESS_TOKEN_SECRET") # Authenticate to Twitter auth = tweepy.OAuthHandler(consumer_key, consumer_secret) auth.set_access_token(access_token, access_token_secret) diff --git a/autogpt/config/ai_config.py b/autogpt/config/ai_config.py index c72b088b..d2641b82 100644 --- a/autogpt/config/ai_config.py +++ b/autogpt/config/ai_config.py @@ -3,9 +3,12 @@ A module that contains the AIConfig class object that contains the configuration """ import os +from pathlib import Path from typing import List, Optional, Type import yaml +from autogpt.prompts.generator import PromptGenerator + class AIConfig: """ @@ -35,9 +38,10 @@ class AIConfig: self.ai_name = ai_name self.ai_role = ai_role self.ai_goals = ai_goals + self.prompt_generator = None # Soon this will go in a folder where it remembers more stuff about the run(s) - SAVE_FILE = os.path.join(os.path.dirname(__file__), "..", "ai_settings.yaml") + SAVE_FILE = Path(os.getcwd()) / "ai_settings.yaml" @staticmethod def load(config_file: str = SAVE_FILE) -> "AIConfig": @@ -86,7 +90,7 @@ class AIConfig: with open(config_file, "w", encoding="utf-8") as file: yaml.dump(config, file, allow_unicode=True) - def construct_full_prompt(self) -> str: + def construct_full_prompt(self, prompt_generator: Optional[PromptGenerator] = None) -> str: """ Returns a prompt to the user with the class information in an organized fashion. @@ -105,14 +109,16 @@ class AIConfig: "" ) - from autogpt.prompt import get_prompt - + from autogpt.prompts.prompt import build_default_prompt_generator # Construct full prompt full_prompt = ( f"You are {self.ai_name}, {self.ai_role}\n{prompt_start}\n\nGOALS:\n\n" ) for i, goal in enumerate(self.ai_goals): full_prompt += f"{i+1}. {goal}\n" - - full_prompt += f"\n\n{get_prompt()}" + if prompt_generator is None: + prompt_generator = build_default_prompt_generator() + prompt_generator.goals = self.ai_goals + self.prompt_generator = prompt_generator + full_prompt += f"\n\n{prompt_generator.generate_prompt_string()}" return full_prompt diff --git a/scripts/plugins.py b/autogpt/plugins.py similarity index 93% rename from scripts/plugins.py rename to autogpt/plugins.py index 44d7e618..7b843a6a 100644 --- a/scripts/plugins.py +++ b/autogpt/plugins.py @@ -18,7 +18,7 @@ def inspect_zip_for_module(zip_path: str, debug: bool = False) -> Optional[str]: Returns: Optional[str]: The name of the module if found, else None. """ - with zipfile.ZipFile(zip_path, 'r') as zfile: + with zipfile.ZipFile(zip_path, "r") as zfile: for name in zfile.namelist(): if name.endswith("__init__.py"): if debug: @@ -68,7 +68,6 @@ def load_plugins(plugins_path: Path, debug: bool = False) -> List[Module]: continue a_module = getattr(zipped_module, key) a_keys = dir(a_module) - if '_abc_impl' in a_keys and \ - a_module.__name__ != 'AutoGPTPluginTemplate': + if "_abc_impl" in a_keys and a_module.__name__ != "AutoGPTPluginTemplate": plugin_modules.append(a_module) return plugin_modules diff --git a/autogpt/prompts/__init__.py b/autogpt/prompts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/autogpt/promptgenerator.py b/autogpt/prompts/generator.py similarity index 91% rename from autogpt/promptgenerator.py rename to autogpt/prompts/generator.py index 82dba8e0..a54cf2cf 100644 --- a/autogpt/promptgenerator.py +++ b/autogpt/prompts/generator.py @@ -1,6 +1,6 @@ """ A module for generating custom prompt strings.""" import json -from typing import Any, Dict, List +from typing import Any, Callable, Dict, List, Optional class PromptGenerator: @@ -18,6 +18,7 @@ class PromptGenerator: self.commands = [] self.resources = [] self.performance_evaluation = [] + self.goals = [] self.response_format = { "thoughts": { "text": "thought", @@ -38,7 +39,13 @@ class PromptGenerator: """ self.constraints.append(constraint) - def add_command(self, command_label: str, command_name: str, args=None) -> None: + def add_command( + self, + command_label: str, + command_name: str, + args=None, + function: Optional[Callable] = None, + ) -> None: """ Add a command to the commands list with a label, name, and optional arguments. @@ -47,6 +54,8 @@ class PromptGenerator: command_name (str): The name of the command. args (dict, optional): A dictionary containing argument names and their values. Defaults to None. + function (callable, optional): A callable function to be called when + the command is executed. Defaults to None. """ if args is None: args = {} @@ -57,6 +66,7 @@ class PromptGenerator: "label": command_label, "name": command_name, "args": command_args, + "function": function, } self.commands.append(command) diff --git a/autogpt/prompt.py b/autogpt/prompts/prompt.py similarity index 94% rename from autogpt/prompt.py rename to autogpt/prompts/prompt.py index 9f79d420..4d27c40d 100644 --- a/autogpt/prompt.py +++ b/autogpt/prompts/prompt.py @@ -2,15 +2,14 @@ from colorama import Fore from autogpt.config.ai_config import AIConfig from autogpt.config.config import Config from autogpt.logs import logger -from autogpt.promptgenerator import PromptGenerator -from autogpt.config import Config +from autogpt.prompts.generator import PromptGenerator from autogpt.setup import prompt_user from autogpt.utils import clean_input CFG = Config() -def get_prompt() -> str: +def build_default_prompt_generator() -> PromptGenerator: """ This function generates a prompt string that includes various constraints, commands, resources, and performance evaluations. @@ -19,9 +18,6 @@ def get_prompt() -> str: str: The generated prompt string. """ - # Initialize the Config object - cfg = Config() - # Initialize the PromptGenerator object prompt_generator = PromptGenerator() @@ -84,11 +80,10 @@ def get_prompt() -> str: ("Generate Image", "generate_image", {"prompt": ""}), ("Convert Audio to text", "read_audio_from_file", {"file": ""}), ("Send Tweet", "send_tweet", {"text": ""}), - ] # Only add shell command to the prompt if the AI is allowed to execute it - if cfg.execute_local_commands: + if CFG.execute_local_commands: commands.append( ( "Execute Shell Command, non-interactive commands only", @@ -135,8 +130,7 @@ def get_prompt() -> str: " the least number of steps." ) - # Generate the prompt string - return prompt_generator.generate_prompt_string() + return prompt_generator def construct_prompt() -> str: @@ -171,8 +165,4 @@ Continue (y/n): """ config = prompt_user() config.save() - # Get rid of this global: - global ai_name - ai_name = config.ai_name - return config.construct_full_prompt() diff --git a/tests/test_prompt_generator.py b/tests/test_prompt_generator.py index 6a0bfd6c..59ca7f95 100644 --- a/tests/test_prompt_generator.py +++ b/tests/test_prompt_generator.py @@ -1,6 +1,6 @@ from unittest import TestCase -from autogpt.promptgenerator import PromptGenerator +from autogpt.prompts.generator import PromptGenerator class TestPromptGenerator(TestCase): diff --git a/tests/unit/test_browse_scrape_text.py b/tests/unit/test_browse_scrape_text.py index 61c19b05..fea5ebfc 100644 --- a/tests/unit/test_browse_scrape_text.py +++ b/tests/unit/test_browse_scrape_text.py @@ -50,7 +50,9 @@ class TestScrapeText: # Tests that the function returns an error message when an invalid or unreachable url is provided. def test_invalid_url(self, mocker): # Mock the requests.get() method to raise an exception - mocker.patch("requests.Session.get", side_effect=requests.exceptions.RequestException) + mocker.patch( + "requests.Session.get", side_effect=requests.exceptions.RequestException + ) # Call the function with an invalid URL and assert that it returns an error message url = "http://www.invalidurl.com" From 2761a5c3619616b2c1367bc63c05b2f73bf4d00e Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sat, 15 Apr 2023 22:18:55 -0500 Subject: [PATCH 08/60] Add post_prompt hook --- autogpt/config/ai_config.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/autogpt/config/ai_config.py b/autogpt/config/ai_config.py index d2641b82..23be7286 100644 --- a/autogpt/config/ai_config.py +++ b/autogpt/config/ai_config.py @@ -4,7 +4,7 @@ A module that contains the AIConfig class object that contains the configuration """ import os from pathlib import Path -from typing import List, Optional, Type +from typing import List, Optional import yaml from autogpt.prompts.generator import PromptGenerator @@ -110,6 +110,8 @@ class AIConfig: ) from autogpt.prompts.prompt import build_default_prompt_generator + from autogpt.config import Config + cfg = Config() # Construct full prompt full_prompt = ( f"You are {self.ai_name}, {self.ai_role}\n{prompt_start}\n\nGOALS:\n\n" @@ -119,6 +121,8 @@ class AIConfig: if prompt_generator is None: prompt_generator = build_default_prompt_generator() prompt_generator.goals = self.ai_goals + for plugin in cfg.plugins: + prompt_generator = plugin.post_prompt(prompt_generator) self.prompt_generator = prompt_generator full_prompt += f"\n\n{prompt_generator.generate_prompt_string()}" return full_prompt From e36b74893f976947810054edacda1bb7a0682005 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sat, 15 Apr 2023 22:33:56 -0500 Subject: [PATCH 09/60] Add name and role to prompt generator object for maximum customization. --- autogpt/config/ai_config.py | 18 +++++++++++------- autogpt/prompts/generator.py | 2 ++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/autogpt/config/ai_config.py b/autogpt/config/ai_config.py index 23be7286..c3159b99 100644 --- a/autogpt/config/ai_config.py +++ b/autogpt/config/ai_config.py @@ -90,7 +90,9 @@ class AIConfig: with open(config_file, "w", encoding="utf-8") as file: yaml.dump(config, file, allow_unicode=True) - def construct_full_prompt(self, prompt_generator: Optional[PromptGenerator] = None) -> str: + def construct_full_prompt( + self, prompt_generator: Optional[PromptGenerator] = None + ) -> str: """ Returns a prompt to the user with the class information in an organized fashion. @@ -111,18 +113,20 @@ class AIConfig: from autogpt.prompts.prompt import build_default_prompt_generator from autogpt.config import Config + cfg = Config() - # Construct full prompt - full_prompt = ( - f"You are {self.ai_name}, {self.ai_role}\n{prompt_start}\n\nGOALS:\n\n" - ) - for i, goal in enumerate(self.ai_goals): - full_prompt += f"{i+1}. {goal}\n" if prompt_generator is None: prompt_generator = build_default_prompt_generator() prompt_generator.goals = self.ai_goals + prompt_generator.name = self.ai_name + prompt_generator.role = self.ai_role for plugin in cfg.plugins: prompt_generator = plugin.post_prompt(prompt_generator) + + # Construct full prompt + full_prompt = f"You are {prompt_generator.name}, {prompt_generator.role}\n{prompt_start}\n\nGOALS:\n\n" + for i, goal in enumerate(self.ai_goals): + full_prompt += f"{i+1}. {goal}\n" self.prompt_generator = prompt_generator full_prompt += f"\n\n{prompt_generator.generate_prompt_string()}" return full_prompt diff --git a/autogpt/prompts/generator.py b/autogpt/prompts/generator.py index a54cf2cf..f8a37b85 100644 --- a/autogpt/prompts/generator.py +++ b/autogpt/prompts/generator.py @@ -19,6 +19,8 @@ class PromptGenerator: self.resources = [] self.performance_evaluation = [] self.goals = [] + self.name = "Bob" + self.role = "AI" self.response_format = { "thoughts": { "text": "thought", From 68e26bf9d6fc9e16cff3042a5e8595a8575080e7 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sat, 15 Apr 2023 22:43:17 -0500 Subject: [PATCH 10/60] Refactor main startup to store AIConfig on Agent for plugin usage. --- autogpt/__main__.py | 7 ++++--- autogpt/agent/agent.py | 2 ++ autogpt/prompts/prompt.py | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/autogpt/__main__.py b/autogpt/__main__.py index 5cde8b55..3e333154 100644 --- a/autogpt/__main__.py +++ b/autogpt/__main__.py @@ -10,7 +10,7 @@ from autogpt.config import Config, check_openai_api_key from autogpt.logs import logger from autogpt.memory import get_memory -from autogpt.prompts.prompt import construct_prompt +from autogpt.prompts.prompt import construct_main_ai_config from autogpt.plugins import load_plugins @@ -47,7 +47,7 @@ def main() -> None: cfg.set_plugins(loaded_plugins) ai_name = "" - prompt = construct_prompt() + ai_config = construct_main_ai_config() # print(prompt) # Initialize variables full_message_history = [] @@ -69,7 +69,8 @@ def main() -> None: memory=memory, full_message_history=full_message_history, next_action_count=next_action_count, - prompt=prompt, + config=ai_config, + prompt=ai_config.construct_full_prompt(), user_input=user_input, ) agent.start_interaction_loop() diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py index 301d3f02..55690119 100644 --- a/autogpt/agent/agent.py +++ b/autogpt/agent/agent.py @@ -31,6 +31,7 @@ class Agent: memory, full_message_history, next_action_count, + config, prompt, user_input, ): @@ -38,6 +39,7 @@ class Agent: self.memory = memory self.full_message_history = full_message_history self.next_action_count = next_action_count + self.config = config self.prompt = prompt self.user_input = user_input diff --git a/autogpt/prompts/prompt.py b/autogpt/prompts/prompt.py index 4d27c40d..24501cea 100644 --- a/autogpt/prompts/prompt.py +++ b/autogpt/prompts/prompt.py @@ -133,7 +133,7 @@ def build_default_prompt_generator() -> PromptGenerator: return prompt_generator -def construct_prompt() -> str: +def construct_main_ai_config() -> AIConfig: """Construct the prompt for the AI to respond to Returns: @@ -165,4 +165,4 @@ Continue (y/n): """ config = prompt_user() config.save() - return config.construct_full_prompt() + return config From 09a5b3149d921cb18094be5c36aa27e25e9f06b4 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sat, 15 Apr 2023 23:01:01 -0500 Subject: [PATCH 11/60] Add on_planning hook. --- autogpt/agent/agent.py | 1 + autogpt/chat.py | 24 ++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py index 55690119..934af42c 100644 --- a/autogpt/agent/agent.py +++ b/autogpt/agent/agent.py @@ -65,6 +65,7 @@ class Agent: # Send message to AI, get response with Spinner("Thinking... "): assistant_reply = chat_with_ai( + self, self.prompt, self.user_input, self.full_message_history, diff --git a/autogpt/chat.py b/autogpt/chat.py index b0886967..7f63bd9e 100644 --- a/autogpt/chat.py +++ b/autogpt/chat.py @@ -51,7 +51,7 @@ def generate_context(prompt, relevant_memory, full_message_history, model): # TODO: Change debug from hardcode to argument def chat_with_ai( - prompt, user_input, full_message_history, permanent_memory, token_limit + agent, prompt, user_input, full_message_history, permanent_memory, token_limit ): """Interact with the OpenAI API, sending the prompt, user input, message history, and permanent memory.""" @@ -109,7 +109,7 @@ def chat_with_ai( current_tokens_used += token_counter.count_message_tokens( [create_chat_message("user", user_input)], model ) # Account for user input (appended later) - + while next_message_to_add_index >= 0: # print (f"CURRENT TOKENS USED: {current_tokens_used}") message_to_add = full_message_history[next_message_to_add_index] @@ -135,6 +135,26 @@ def chat_with_ai( # Append user input, the length of this is accounted for above current_context.extend([create_chat_message("user", user_input)]) + plugin_count = len(cfg.plugins) + for i, plugin in enumerate(cfg.plugins): + plugin_response = plugin.on_planning( + agent.prompt_generator, current_context + ) + if not plugin_response or plugin_response == "": + continue + tokens_to_add = token_counter.count_message_tokens( + [plugin_response], model + ) + if current_tokens_used + tokens_to_add > send_token_limit: + if cfg.debug_mode: + print("Plugin response too long, skipping:", + plugin_response) + print("Plugins remaining at stop:", plugin_count - i) + break + current_context.append( + create_chat_message("system", plugin_response) + ) + # Calculate remaining tokens tokens_remaining = token_limit - current_tokens_used # assert tokens_remaining >= 0, "Tokens remaining is negative. From ee42b4d06c2bd7aa99e77edda00e9bfb9f5e327f Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sat, 15 Apr 2023 23:45:16 -0500 Subject: [PATCH 12/60] Add pre_instruction and on_instruction hooks. --- autogpt/agent/agent_manager.py | 31 +++++++++++++++++++++++++------ autogpt/chat.py | 9 +++------ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/autogpt/agent/agent_manager.py b/autogpt/agent/agent_manager.py index 3467f8bf..32473750 100644 --- a/autogpt/agent/agent_manager.py +++ b/autogpt/agent/agent_manager.py @@ -1,7 +1,7 @@ """Agent manager for managing GPT agents""" from typing import List, Tuple, Union from autogpt.llm_utils import create_chat_completion -from autogpt.config.config import Singleton +from autogpt.config.config import Singleton, Config class AgentManager(metaclass=Singleton): @@ -10,6 +10,7 @@ class AgentManager(metaclass=Singleton): def __init__(self): self.next_key = 0 self.agents = {} # key, (task, full_message_history, model) + self.cfg = Config() # Create new GPT agent # TODO: Centralise use of create_chat_completion() to globally enforce token limit @@ -28,6 +29,10 @@ class AgentManager(metaclass=Singleton): messages = [ {"role": "user", "content": prompt}, ] + for plugin in self.cfg.plugins: + plugin_messages = plugin.pre_instruction(messages) + if plugin_messages: + messages.extend(plugin_messages) # Start GPT instance agent_reply = create_chat_completion( @@ -35,9 +40,13 @@ class AgentManager(metaclass=Singleton): messages=messages, ) - # Update full message history - messages.append({"role": "assistant", "content": agent_reply}) + plugins_reply = agent_reply + for plugin in self.cfg.plugins: + plugin_result = plugin.on_instruction(messages) + if plugin_result: + plugins_reply = f"{plugins_reply}\n{plugin_result}" + messages.append({"role": "assistant", "content": plugins_reply}) key = self.next_key # This is done instead of len(agents) to make keys unique even if agents # are deleted @@ -45,7 +54,7 @@ class AgentManager(metaclass=Singleton): self.agents[key] = (task, messages, model) - return key, agent_reply + return key, plugins_reply def message_agent(self, key: Union[str, int], message: str) -> str: """Send a message to an agent and return its response @@ -62,16 +71,26 @@ class AgentManager(metaclass=Singleton): # Add user message to message history before sending to agent messages.append({"role": "user", "content": message}) + for plugin in self.cfg.plugins: + plugin_messages = plugin.pre_instruction(messages) + if plugin_messages: + messages.extend(plugin_messages) + # Start GPT instance agent_reply = create_chat_completion( model=model, messages=messages, ) + plugins_reply = agent_reply + for plugin in self.cfg.plugins: + plugin_result = plugin.on_instruction(messages) + if plugin_result: + plugins_reply = f"{plugins_reply}\n{plugin_result}" # Update full message history - messages.append({"role": "assistant", "content": agent_reply}) + messages.append({"role": "assistant", "content": plugins_reply}) - return agent_reply + return plugins_reply def list_agents(self) -> List[Tuple[Union[str, int], str]]: """Return a list of all agents diff --git a/autogpt/chat.py b/autogpt/chat.py index 7f63bd9e..2844251f 100644 --- a/autogpt/chat.py +++ b/autogpt/chat.py @@ -109,7 +109,7 @@ def chat_with_ai( current_tokens_used += token_counter.count_message_tokens( [create_chat_message("user", user_input)], model ) # Account for user input (appended later) - + while next_message_to_add_index >= 0: # print (f"CURRENT TOKENS USED: {current_tokens_used}") message_to_add = full_message_history[next_message_to_add_index] @@ -147,13 +147,10 @@ def chat_with_ai( ) if current_tokens_used + tokens_to_add > send_token_limit: if cfg.debug_mode: - print("Plugin response too long, skipping:", - plugin_response) + print("Plugin response too long, skipping:", plugin_response) print("Plugins remaining at stop:", plugin_count - i) break - current_context.append( - create_chat_message("system", plugin_response) - ) + current_context.append(create_chat_message("system", plugin_response)) # Calculate remaining tokens tokens_remaining = token_limit - current_tokens_used From fc7db7d86ff6a1223652c3767706fe343e4bea94 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sat, 15 Apr 2023 23:51:43 -0500 Subject: [PATCH 13/60] Fix bad logic probably. --- autogpt/agent/agent_manager.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/autogpt/agent/agent_manager.py b/autogpt/agent/agent_manager.py index 32473750..1ae69812 100644 --- a/autogpt/agent/agent_manager.py +++ b/autogpt/agent/agent_manager.py @@ -40,13 +40,16 @@ class AgentManager(metaclass=Singleton): messages=messages, ) - plugins_reply = agent_reply - for plugin in self.cfg.plugins: + messages.append({"role": "assistant", "content": agent_reply}) + + plugins_reply = "" + for i, plugin in enumerate(self.cfg.plugins): plugin_result = plugin.on_instruction(messages) if plugin_result: - plugins_reply = f"{plugins_reply}\n{plugin_result}" + plugins_reply = f"{plugins_reply}{'' if not i else '\n'}{plugin_result}" - messages.append({"role": "assistant", "content": plugins_reply}) + if plugins_reply and plugins_reply != "": + messages.append({"role": "assistant", "content": plugins_reply}) key = self.next_key # This is done instead of len(agents) to make keys unique even if agents # are deleted @@ -82,13 +85,16 @@ class AgentManager(metaclass=Singleton): messages=messages, ) + messages.append({"role": "assistant", "content": agent_reply}) + plugins_reply = agent_reply - for plugin in self.cfg.plugins: + for i, plugin in enumerate(self.cfg.plugins): plugin_result = plugin.on_instruction(messages) if plugin_result: - plugins_reply = f"{plugins_reply}\n{plugin_result}" + plugins_reply = f"{plugins_reply}{'' if not i else '\n'}{plugin_result}" # Update full message history - messages.append({"role": "assistant", "content": plugins_reply}) + if plugins_reply and plugins_reply != "": + messages.append({"role": "assistant", "content": plugins_reply}) return plugins_reply From 00225e01b3d8f2d77ace76ded732db76dbae7047 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sat, 15 Apr 2023 23:54:20 -0500 Subject: [PATCH 14/60] Fix another bad implementation detail. --- autogpt/agent/agent_manager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/autogpt/agent/agent_manager.py b/autogpt/agent/agent_manager.py index 1ae69812..1c811f86 100644 --- a/autogpt/agent/agent_manager.py +++ b/autogpt/agent/agent_manager.py @@ -32,7 +32,8 @@ class AgentManager(metaclass=Singleton): for plugin in self.cfg.plugins: plugin_messages = plugin.pre_instruction(messages) if plugin_messages: - messages.extend(plugin_messages) + for plugin_message in plugin_messages: + messages.append({"role": "system", "content": plugin_message}) # Start GPT instance agent_reply = create_chat_completion( @@ -77,7 +78,8 @@ class AgentManager(metaclass=Singleton): for plugin in self.cfg.plugins: plugin_messages = plugin.pre_instruction(messages) if plugin_messages: - messages.extend(plugin_messages) + for plugin_message in plugin_messages: + messages.append({"role": "system", "content": plugin_message}) # Start GPT instance agent_reply = create_chat_completion( From 397627d1b9d77b820d1eae0b5b20642aa9b00eb9 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 00:01:23 -0500 Subject: [PATCH 15/60] add post_instruction hook --- autogpt/agent/agent_manager.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/autogpt/agent/agent_manager.py b/autogpt/agent/agent_manager.py index 1c811f86..3db2fdd6 100644 --- a/autogpt/agent/agent_manager.py +++ b/autogpt/agent/agent_manager.py @@ -58,7 +58,10 @@ class AgentManager(metaclass=Singleton): self.agents[key] = (task, messages, model) - return key, plugins_reply + for plugin in self.cfg.plugins: + agent_reply = plugin.post_instruction(agent_reply) + + return key, agent_reply def message_agent(self, key: Union[str, int], message: str) -> str: """Send a message to an agent and return its response @@ -98,7 +101,10 @@ class AgentManager(metaclass=Singleton): if plugins_reply and plugins_reply != "": messages.append({"role": "assistant", "content": plugins_reply}) - return plugins_reply + for plugin in self.cfg.plugins: + agent_reply = plugin.post_instruction(agent_reply) + + return agent_reply def list_agents(self) -> List[Tuple[Union[str, int], str]]: """Return a list of all agents From 17478d6a05f1659f746b9156a8abdf3fcd3ee835 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 00:09:11 -0500 Subject: [PATCH 16/60] Add post planning hook --- autogpt/agent/agent.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py index 934af42c..5d3383d8 100644 --- a/autogpt/agent/agent.py +++ b/autogpt/agent/agent.py @@ -73,6 +73,9 @@ class Agent: cfg.fast_token_limit, ) # TODO: This hardcodes the model to use GPT3.5. Make this an argument + for plugin in cfg.plugins: + assistant_reply = plugin.post_planning(self, assistant_reply) + # Print Assistant thoughts print_assistant_thoughts(self.ai_name, assistant_reply) From 83403ad3ab60755a8a28e8d99a88fe82abf1bec7 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 00:20:00 -0500 Subject: [PATCH 17/60] add pre_command and post_command hooks. --- autogpt/agent/agent.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py index 5d3383d8..ba3a3791 100644 --- a/autogpt/agent/agent.py +++ b/autogpt/agent/agent.py @@ -110,6 +110,7 @@ class Agent: console_input = clean_input( Fore.MAGENTA + "Input:" + Style.RESET_ALL ) + if console_input.lower().rstrip() == "y": self.user_input = "GENERATE NEXT COMMAND JSON" break @@ -160,10 +161,19 @@ class Agent: elif command_name == "human_feedback": result = f"Human feedback: {self.user_input}" else: + for plugin in cfg.plugins: + command_name, arguments = plugin.pre_command( + command_name, arguments + ) result = ( f"Command {command_name} returned: " f"{execute_command(command_name, arguments)}" ) + + for plugin in cfg.plugins: + result = plugin.post_command( + command_name, result + ) if self.next_action_count > 0: self.next_action_count -= 1 From abb54df4d071a1c5d7c564881691d511334cdd3e Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 00:37:21 -0500 Subject: [PATCH 18/60] Add custom commands to execute_command via promptgenerator --- autogpt/agent/agent.py | 8 +++----- autogpt/app.py | 6 +++++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py index ba3a3791..a3a3729a 100644 --- a/autogpt/agent/agent.py +++ b/autogpt/agent/agent.py @@ -167,13 +167,11 @@ class Agent: ) result = ( f"Command {command_name} returned: " - f"{execute_command(command_name, arguments)}" + f"{execute_command(command_name, arguments, self.config.prompt_generator)}" ) - + for plugin in cfg.plugins: - result = plugin.post_command( - command_name, result - ) + result = plugin.post_command(command_name, result) if self.next_action_count > 0: self.next_action_count -= 1 diff --git a/autogpt/app.py b/autogpt/app.py index fa5cab62..f543fd83 100644 --- a/autogpt/app.py +++ b/autogpt/app.py @@ -21,6 +21,7 @@ from autogpt.commands.file_operations import ( from autogpt.json_fixes.parsing import fix_and_parse_json from autogpt.memory import get_memory from autogpt.processing.text import summarize_text +from autogpt.prompts.generator import PromptGenerator from autogpt.speech import say_text from autogpt.commands.web_selenium import browse_website from autogpt.commands.git_operations import clone_repository @@ -105,7 +106,7 @@ def map_command_synonyms(command_name: str): return command_name -def execute_command(command_name: str, arguments): +def execute_command(command_name: str, arguments, prompt: PromptGenerator): """Execute the command and return the result Args: @@ -192,6 +193,9 @@ def execute_command(command_name: str, arguments): elif command_name == "task_complete": shutdown() else: + for command in prompt.commands: + if command_name == command["label"] or command_name == command["name"]: + return command["function"](*arguments.values()) return ( f"Unknown command '{command_name}'. Please refer to the 'COMMANDS'" " list for available commands and only respond in the specified JSON" From 05bafb983844d09180d5f33753e53ca2472e0c0e Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 00:40:00 -0500 Subject: [PATCH 19/60] Fix fstring bug. --- autogpt/agent/agent_manager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/autogpt/agent/agent_manager.py b/autogpt/agent/agent_manager.py index 3db2fdd6..85b61b71 100644 --- a/autogpt/agent/agent_manager.py +++ b/autogpt/agent/agent_manager.py @@ -47,7 +47,8 @@ class AgentManager(metaclass=Singleton): for i, plugin in enumerate(self.cfg.plugins): plugin_result = plugin.on_instruction(messages) if plugin_result: - plugins_reply = f"{plugins_reply}{'' if not i else '\n'}{plugin_result}" + sep = '' if not i else '\n' + plugins_reply = f"{plugins_reply}{sep}{plugin_result}" if plugins_reply and plugins_reply != "": messages.append({"role": "assistant", "content": plugins_reply}) @@ -96,7 +97,8 @@ class AgentManager(metaclass=Singleton): for i, plugin in enumerate(self.cfg.plugins): plugin_result = plugin.on_instruction(messages) if plugin_result: - plugins_reply = f"{plugins_reply}{'' if not i else '\n'}{plugin_result}" + sep = '' if not i else '\n' + plugins_reply = f"{plugins_reply}{sep}{plugin_result}" # Update full message history if plugins_reply and plugins_reply != "": messages.append({"role": "assistant", "content": plugins_reply}) From 3fadf2c90bee9342df9e1f7d9686d7a39abb4bdc Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 14:15:38 -0500 Subject: [PATCH 20/60] Blacked --- autogpt/agent/agent_manager.py | 4 +- autogpt/app.py | 7 ++- autogpt/commands/file_operations.py | 2 +- autogpt/config/config.py | 6 +- autogpt/memory/__init__.py | 8 ++- autogpt/memory/no_memory.py | 2 +- autogpt/memory/weaviate.py | 58 +++++++++--------- autogpt/prompts/prompt.py | 6 +- autogpt/workspace.py | 4 +- tests/integration/weaviate_memory_tests.py | 68 ++++++++++++---------- 10 files changed, 92 insertions(+), 73 deletions(-) diff --git a/autogpt/agent/agent_manager.py b/autogpt/agent/agent_manager.py index 7b72f678..e848bfe7 100644 --- a/autogpt/agent/agent_manager.py +++ b/autogpt/agent/agent_manager.py @@ -48,7 +48,7 @@ class AgentManager(metaclass=Singleton): for i, plugin in enumerate(self.cfg.plugins): plugin_result = plugin.on_instruction(messages) if plugin_result: - sep = '' if not i else '\n' + sep = "" if not i else "\n" plugins_reply = f"{plugins_reply}{sep}{plugin_result}" if plugins_reply and plugins_reply != "": @@ -98,7 +98,7 @@ class AgentManager(metaclass=Singleton): for i, plugin in enumerate(self.cfg.plugins): plugin_result = plugin.on_instruction(messages) if plugin_result: - sep = '' if not i else '\n' + sep = "" if not i else "\n" plugins_reply = f"{plugins_reply}{sep}{plugin_result}" # Update full message history if plugins_reply and plugins_reply != "": diff --git a/autogpt/app.py b/autogpt/app.py index d54c1a63..aca97a75 100644 --- a/autogpt/app.py +++ b/autogpt/app.py @@ -132,9 +132,12 @@ def execute_command(command_name: str, arguments, prompt: PromptGenerator): # google_result can be a list or a string depending on the search results if isinstance(google_result, list): - safe_message = [google_result_single.encode('utf-8', 'ignore') for google_result_single in google_result] + safe_message = [ + google_result_single.encode("utf-8", "ignore") + for google_result_single in google_result + ] else: - safe_message = google_result.encode('utf-8', 'ignore') + safe_message = google_result.encode("utf-8", "ignore") return str(safe_message) elif command_name == "memory_add": diff --git a/autogpt/commands/file_operations.py b/autogpt/commands/file_operations.py index 8abc2e23..60ede701 100644 --- a/autogpt/commands/file_operations.py +++ b/autogpt/commands/file_operations.py @@ -40,7 +40,7 @@ def log_operation(operation: str, filename: str) -> None: with open(LOG_FILE_PATH, "w", encoding="utf-8") as f: f.write("File Operation Logger ") - append_to_file(LOG_FILE, log_entry, shouldLog = False) + append_to_file(LOG_FILE, log_entry, shouldLog=False) def split_file( diff --git a/autogpt/config/config.py b/autogpt/config/config.py index 4bb7fb71..46ab95d8 100644 --- a/autogpt/config/config.py +++ b/autogpt/config/config.py @@ -66,7 +66,7 @@ class Config(metaclass=Singleton): self.pinecone_api_key = os.getenv("PINECONE_API_KEY") self.pinecone_region = os.getenv("PINECONE_ENV") - self.weaviate_host = os.getenv("WEAVIATE_HOST") + self.weaviate_host = os.getenv("WEAVIATE_HOST") self.weaviate_port = os.getenv("WEAVIATE_PORT") self.weaviate_protocol = os.getenv("WEAVIATE_PROTOCOL", "http") self.weaviate_username = os.getenv("WEAVIATE_USERNAME", None) @@ -74,7 +74,9 @@ class Config(metaclass=Singleton): self.weaviate_scopes = os.getenv("WEAVIATE_SCOPES", None) self.weaviate_embedded_path = os.getenv("WEAVIATE_EMBEDDED_PATH") self.weaviate_api_key = os.getenv("WEAVIATE_API_KEY", None) - self.use_weaviate_embedded = os.getenv("USE_WEAVIATE_EMBEDDED", "False") == "True" + self.use_weaviate_embedded = ( + os.getenv("USE_WEAVIATE_EMBEDDED", "False") == "True" + ) # milvus configuration, e.g., localhost:19530. self.milvus_addr = os.getenv("MILVUS_ADDR", "localhost:19530") diff --git a/autogpt/memory/__init__.py b/autogpt/memory/__init__.py index e2ee44a4..ead02185 100644 --- a/autogpt/memory/__init__.py +++ b/autogpt/memory/__init__.py @@ -56,8 +56,10 @@ def get_memory(cfg, init=False): memory = RedisMemory(cfg) elif cfg.memory_backend == "weaviate": if not WeaviateMemory: - print("Error: Weaviate is not installed. Please install weaviate-client to" - " use Weaviate as a memory backend.") + print( + "Error: Weaviate is not installed. Please install weaviate-client to" + " use Weaviate as a memory backend." + ) else: memory = WeaviateMemory(cfg) elif cfg.memory_backend == "milvus": @@ -89,5 +91,5 @@ __all__ = [ "PineconeMemory", "NoMemory", "MilvusMemory", - "WeaviateMemory" + "WeaviateMemory", ] diff --git a/autogpt/memory/no_memory.py b/autogpt/memory/no_memory.py index 4035a657..0371e96a 100644 --- a/autogpt/memory/no_memory.py +++ b/autogpt/memory/no_memory.py @@ -53,7 +53,7 @@ class NoMemory(MemoryProviderSingleton): """ return "" - def get_relevant(self, data: str, num_relevant: int = 5) ->list[Any] | None: + def get_relevant(self, data: str, num_relevant: int = 5) -> list[Any] | None: """ Returns all the data in the memory that is relevant to the given data. NoMemory always returns None. diff --git a/autogpt/memory/weaviate.py b/autogpt/memory/weaviate.py index 6fcce0a0..19035381 100644 --- a/autogpt/memory/weaviate.py +++ b/autogpt/memory/weaviate.py @@ -14,7 +14,7 @@ def default_schema(weaviate_index): { "name": "raw_text", "dataType": ["text"], - "description": "original text for the embedding" + "description": "original text for the embedding", } ], } @@ -24,16 +24,20 @@ class WeaviateMemory(MemoryProviderSingleton): def __init__(self, cfg): auth_credentials = self._build_auth_credentials(cfg) - url = f'{cfg.weaviate_protocol}://{cfg.weaviate_host}:{cfg.weaviate_port}' + url = f"{cfg.weaviate_protocol}://{cfg.weaviate_host}:{cfg.weaviate_port}" if cfg.use_weaviate_embedded: - self.client = Client(embedded_options=EmbeddedOptions( - hostname=cfg.weaviate_host, - port=int(cfg.weaviate_port), - persistence_data_path=cfg.weaviate_embedded_path - )) + self.client = Client( + embedded_options=EmbeddedOptions( + hostname=cfg.weaviate_host, + port=int(cfg.weaviate_port), + persistence_data_path=cfg.weaviate_embedded_path, + ) + ) - print(f"Weaviate Embedded running on: {url} with persistence path: {cfg.weaviate_embedded_path}") + print( + f"Weaviate Embedded running on: {url} with persistence path: {cfg.weaviate_embedded_path}" + ) else: self.client = Client(url, auth_client_secret=auth_credentials) @@ -47,7 +51,9 @@ class WeaviateMemory(MemoryProviderSingleton): def _build_auth_credentials(self, cfg): if cfg.weaviate_username and cfg.weaviate_password: - return weaviate.AuthClientPassword(cfg.weaviate_username, cfg.weaviate_password) + return weaviate.AuthClientPassword( + cfg.weaviate_username, cfg.weaviate_password + ) if cfg.weaviate_api_key: return weaviate.AuthApiKey(api_key=cfg.weaviate_api_key) else: @@ -57,16 +63,14 @@ class WeaviateMemory(MemoryProviderSingleton): vector = get_ada_embedding(data) doc_uuid = generate_uuid5(data, self.index) - data_object = { - 'raw_text': data - } + data_object = {"raw_text": data} with self.client.batch as batch: batch.add_data_object( uuid=doc_uuid, data_object=data_object, class_name=self.index, - vector=vector + vector=vector, ) return f"Inserting data into memory at uuid: {doc_uuid}:\n data: {data}" @@ -82,29 +86,31 @@ class WeaviateMemory(MemoryProviderSingleton): # after a call to delete_all self._create_schema() - return 'Obliterated' + return "Obliterated" def get_relevant(self, data, num_relevant=5): query_embedding = get_ada_embedding(data) try: - results = self.client.query.get(self.index, ['raw_text']) \ - .with_near_vector({'vector': query_embedding, 'certainty': 0.7}) \ - .with_limit(num_relevant) \ - .do() + results = ( + self.client.query.get(self.index, ["raw_text"]) + .with_near_vector({"vector": query_embedding, "certainty": 0.7}) + .with_limit(num_relevant) + .do() + ) - if len(results['data']['Get'][self.index]) > 0: - return [str(item['raw_text']) for item in results['data']['Get'][self.index]] + if len(results["data"]["Get"][self.index]) > 0: + return [ + str(item["raw_text"]) for item in results["data"]["Get"][self.index] + ] else: return [] except Exception as err: - print(f'Unexpected error {err=}, {type(err)=}') + print(f"Unexpected error {err=}, {type(err)=}") return [] def get_stats(self): - result = self.client.query.aggregate(self.index) \ - .with_meta_count() \ - .do() - class_data = result['data']['Aggregate'][self.index] + result = self.client.query.aggregate(self.index).with_meta_count().do() + class_data = result["data"]["Aggregate"][self.index] - return class_data[0]['meta'] if class_data else {} + return class_data[0]["meta"] if class_data else {} diff --git a/autogpt/prompts/prompt.py b/autogpt/prompts/prompt.py index 91279da7..8dacaf7f 100644 --- a/autogpt/prompts/prompt.py +++ b/autogpt/prompts/prompt.py @@ -84,11 +84,7 @@ def build_default_prompt_generator() -> PromptGenerator: # Only add the audio to text command if the model is specified if cfg.huggingface_audio_to_text_model: commands.append( - ( - "Convert Audio to text", - "read_audio_from_file", - {"file": ""} - ), + ("Convert Audio to text", "read_audio_from_file", {"file": ""}), ) # Only add shell command to the prompt if the AI is allowed to execute it diff --git a/autogpt/workspace.py b/autogpt/workspace.py index 2706b3b2..964a94d1 100644 --- a/autogpt/workspace.py +++ b/autogpt/workspace.py @@ -36,6 +36,8 @@ def safe_path_join(base: Path, *paths: str | Path) -> Path: joined_path = base.joinpath(*paths).resolve() if not joined_path.is_relative_to(base): - raise ValueError(f"Attempted to access path '{joined_path}' outside of working directory '{base}'.") + raise ValueError( + f"Attempted to access path '{joined_path}' outside of working directory '{base}'." + ) return joined_path diff --git a/tests/integration/weaviate_memory_tests.py b/tests/integration/weaviate_memory_tests.py index 503fe9d2..6f3edd99 100644 --- a/tests/integration/weaviate_memory_tests.py +++ b/tests/integration/weaviate_memory_tests.py @@ -12,14 +12,17 @@ from autogpt.memory.weaviate import WeaviateMemory from autogpt.memory.base import get_ada_embedding -@mock.patch.dict(os.environ, { - "WEAVIATE_HOST": "127.0.0.1", - "WEAVIATE_PROTOCOL": "http", - "WEAVIATE_PORT": "8080", - "WEAVIATE_USERNAME": "", - "WEAVIATE_PASSWORD": "", - "MEMORY_INDEX": "AutogptTests" -}) +@mock.patch.dict( + os.environ, + { + "WEAVIATE_HOST": "127.0.0.1", + "WEAVIATE_PROTOCOL": "http", + "WEAVIATE_PORT": "8080", + "WEAVIATE_USERNAME": "", + "WEAVIATE_PASSWORD": "", + "MEMORY_INDEX": "AutogptTests", + }, +) class TestWeaviateMemory(unittest.TestCase): cfg = None client = None @@ -32,13 +35,17 @@ class TestWeaviateMemory(unittest.TestCase): if cls.cfg.use_weaviate_embedded: from weaviate.embedded import EmbeddedOptions - cls.client = Client(embedded_options=EmbeddedOptions( - hostname=cls.cfg.weaviate_host, - port=int(cls.cfg.weaviate_port), - persistence_data_path=cls.cfg.weaviate_embedded_path - )) + cls.client = Client( + embedded_options=EmbeddedOptions( + hostname=cls.cfg.weaviate_host, + port=int(cls.cfg.weaviate_port), + persistence_data_path=cls.cfg.weaviate_embedded_path, + ) + ) else: - cls.client = Client(f"{cls.cfg.weaviate_protocol}://{cls.cfg.weaviate_host}:{self.cfg.weaviate_port}") + cls.client = Client( + f"{cls.cfg.weaviate_protocol}://{cls.cfg.weaviate_host}:{self.cfg.weaviate_port}" + ) """ In order to run these tests you will need a local instance of @@ -49,6 +56,7 @@ class TestWeaviateMemory(unittest.TestCase): USE_WEAVIATE_EMBEDDED=True WEAVIATE_EMBEDDED_PATH="/home/me/.local/share/weaviate" """ + def setUp(self): try: self.client.schema.delete_class(self.cfg.memory_index) @@ -58,23 +66,23 @@ class TestWeaviateMemory(unittest.TestCase): self.memory = WeaviateMemory(self.cfg) def test_add(self): - doc = 'You are a Titan name Thanos and you are looking for the Infinity Stones' + doc = "You are a Titan name Thanos and you are looking for the Infinity Stones" self.memory.add(doc) - result = self.client.query.get(self.cfg.memory_index, ['raw_text']).do() - actual = result['data']['Get'][self.cfg.memory_index] + result = self.client.query.get(self.cfg.memory_index, ["raw_text"]).do() + actual = result["data"]["Get"][self.cfg.memory_index] self.assertEqual(len(actual), 1) - self.assertEqual(actual[0]['raw_text'], doc) + self.assertEqual(actual[0]["raw_text"], doc) def test_get(self): - doc = 'You are an Avenger and swore to defend the Galaxy from a menace called Thanos' + doc = "You are an Avenger and swore to defend the Galaxy from a menace called Thanos" with self.client.batch as batch: batch.add_data_object( uuid=get_valid_uuid(uuid4()), - data_object={'raw_text': doc}, + data_object={"raw_text": doc}, class_name=self.cfg.memory_index, - vector=get_ada_embedding(doc) + vector=get_ada_embedding(doc), ) batch.flush() @@ -86,8 +94,8 @@ class TestWeaviateMemory(unittest.TestCase): def test_get_stats(self): docs = [ - 'You are now about to count the number of docs in this index', - 'And then you about to find out if you can count correctly' + "You are now about to count the number of docs in this index", + "And then you about to find out if you can count correctly", ] [self.memory.add(doc) for doc in docs] @@ -95,23 +103,23 @@ class TestWeaviateMemory(unittest.TestCase): stats = self.memory.get_stats() self.assertTrue(stats) - self.assertTrue('count' in stats) - self.assertEqual(stats['count'], 2) + self.assertTrue("count" in stats) + self.assertEqual(stats["count"], 2) def test_clear(self): docs = [ - 'Shame this is the last test for this class', - 'Testing is fun when someone else is doing it' + "Shame this is the last test for this class", + "Testing is fun when someone else is doing it", ] [self.memory.add(doc) for doc in docs] - self.assertEqual(self.memory.get_stats()['count'], 2) + self.assertEqual(self.memory.get_stats()["count"], 2) self.memory.clear() - self.assertEqual(self.memory.get_stats()['count'], 0) + self.assertEqual(self.memory.get_stats()["count"], 0) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From df5cc3303f6f79f54e96b5bb716d1a1603da3a47 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 15:35:25 -0500 Subject: [PATCH 21/60] move tests and cleanup. --- auto_gpt/commands.py | 121 ------- auto_gpt/tests/__init__.py | 0 auto_gpt/tests/mocks/__init__.py | 0 {auto_gpt => tests/mocks}/__init__.py | 0 .../tests => tests}/mocks/mock_commands.py | 0 {auto_gpt/tests => tests}/test_commands.py | 294 +++++++++--------- 6 files changed, 147 insertions(+), 268 deletions(-) delete mode 100644 auto_gpt/commands.py delete mode 100644 auto_gpt/tests/__init__.py delete mode 100644 auto_gpt/tests/mocks/__init__.py rename {auto_gpt => tests/mocks}/__init__.py (100%) rename {auto_gpt/tests => tests}/mocks/mock_commands.py (100%) rename {auto_gpt/tests => tests}/test_commands.py (97%) diff --git a/auto_gpt/commands.py b/auto_gpt/commands.py deleted file mode 100644 index 2f9016ba..00000000 --- a/auto_gpt/commands.py +++ /dev/null @@ -1,121 +0,0 @@ -import os -import sys -import importlib -import inspect -from typing import Callable, Any, List - -# Unique identifier for auto-gpt commands -AUTO_GPT_COMMAND_IDENTIFIER = "auto_gpt_command" - -class Command: - """A class representing a command. - - Attributes: - name (str): The name of the command. - description (str): A brief description of what the command does. - signature (str): The signature of the function that the command executes. Defaults to None. - """ - - def __init__(self, name: str, description: str, method: Callable[..., Any], signature: str = None): - self.name = name - self.description = description - self.method = method - self.signature = signature if signature else str(inspect.signature(self.method)) - - def __call__(self, *args, **kwargs) -> Any: - return self.method(*args, **kwargs) - - def __str__(self) -> str: - return f"{self.name}: {self.description}, args: {self.signature}" - -class CommandRegistry: - """ - The CommandRegistry class is a manager for a collection of Command objects. - It allows the registration, modification, and retrieval of Command objects, - as well as the scanning and loading of command plugins from a specified - directory. - """ - - def __init__(self): - self.commands = {} - - def _import_module(self, module_name: str) -> Any: - return importlib.import_module(module_name) - - def _reload_module(self, module: Any) -> Any: - return importlib.reload(module) - - def register(self, cmd: Command) -> None: - self.commands[cmd.name] = cmd - - def unregister(self, command_name: str): - if command_name in self.commands: - del self.commands[command_name] - else: - raise KeyError(f"Command '{command_name}' not found in registry.") - - def reload_commands(self) -> None: - """Reloads all loaded command plugins.""" - for cmd_name in self.commands: - cmd = self.commands[cmd_name] - module = self._import_module(cmd.__module__) - reloaded_module = self._reload_module(module) - if hasattr(reloaded_module, "register"): - reloaded_module.register(self) - - def get_command(self, name: str) -> Callable[..., Any]: - return self.commands[name] - - def call(self, command_name: str, **kwargs) -> Any: - if command_name not in self.commands: - raise KeyError(f"Command '{command_name}' not found in registry.") - command = self.commands[command_name] - return command(**kwargs) - - def command_prompt(self) -> str: - """ - Returns a string representation of all registered `Command` objects for use in a prompt - """ - commands_list = [f"{idx + 1}. {str(cmd)}" for idx, cmd in enumerate(self.commands.values())] - return "\n".join(commands_list) - - def import_commands(self, module_name: str) -> None: - """ - Imports the specified Python module containing command plugins. - - This method imports the associated module and registers any functions or - classes that are decorated with the `AUTO_GPT_COMMAND_IDENTIFIER` attribute - as `Command` objects. The registered `Command` objects are then added to the - `commands` dictionary of the `CommandRegistry` object. - - Args: - module_name (str): The name of the module to import for command plugins. - """ - - module = importlib.import_module(module_name) - - for attr_name in dir(module): - attr = getattr(module, attr_name) - # Register decorated functions - if hasattr(attr, AUTO_GPT_COMMAND_IDENTIFIER) and getattr(attr, AUTO_GPT_COMMAND_IDENTIFIER): - self.register(attr.command) - # Register command classes - elif inspect.isclass(attr) and issubclass(attr, Command) and attr != Command: - cmd_instance = attr() - self.register(cmd_instance) - -def command(name: str, description: str, signature: str = None) -> Callable[..., Any]: - """The command decorator is used to create Command objects from ordinary functions.""" - def decorator(func: Callable[..., Any]) -> Command: - cmd = Command(name=name, description=description, method=func, signature=signature) - - def wrapper(*args, **kwargs) -> Any: - return func(*args, **kwargs) - - wrapper.command = cmd - - setattr(wrapper, AUTO_GPT_COMMAND_IDENTIFIER, True) - return wrapper - - return decorator - diff --git a/auto_gpt/tests/__init__.py b/auto_gpt/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/auto_gpt/tests/mocks/__init__.py b/auto_gpt/tests/mocks/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/auto_gpt/__init__.py b/tests/mocks/__init__.py similarity index 100% rename from auto_gpt/__init__.py rename to tests/mocks/__init__.py diff --git a/auto_gpt/tests/mocks/mock_commands.py b/tests/mocks/mock_commands.py similarity index 100% rename from auto_gpt/tests/mocks/mock_commands.py rename to tests/mocks/mock_commands.py diff --git a/auto_gpt/tests/test_commands.py b/tests/test_commands.py similarity index 97% rename from auto_gpt/tests/test_commands.py rename to tests/test_commands.py index a7778b6e..9ff0cd4c 100644 --- a/auto_gpt/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,147 +1,147 @@ -import shutil -import sys -from pathlib import Path - -import pytest -from auto_gpt.commands import Command, CommandRegistry - - -class TestCommand: - @staticmethod - def example_function(arg1: int, arg2: str) -> str: - return f"{arg1} - {arg2}" - - def test_command_creation(self): - cmd = Command(name="example", description="Example command", method=self.example_function) - - assert cmd.name == "example" - assert cmd.description == "Example command" - assert cmd.method == self.example_function - assert cmd.signature == "(arg1: int, arg2: str) -> str" - - def test_command_call(self): - cmd = Command(name="example", description="Example command", method=self.example_function) - - result = cmd(arg1=1, arg2="test") - assert result == "1 - test" - - def test_command_call_with_invalid_arguments(self): - cmd = Command(name="example", description="Example command", method=self.example_function) - - with pytest.raises(TypeError): - cmd(arg1="invalid", does_not_exist="test") - - def test_command_default_signature(self): - cmd = Command(name="example", description="Example command", method=self.example_function) - - assert cmd.signature == "(arg1: int, arg2: str) -> str" - - def test_command_custom_signature(self): - custom_signature = "custom_arg1: int, custom_arg2: str" - cmd = Command(name="example", description="Example command", method=self.example_function, signature=custom_signature) - - assert cmd.signature == custom_signature - - - -class TestCommandRegistry: - @staticmethod - def example_function(arg1: int, arg2: str) -> str: - return f"{arg1} - {arg2}" - - def test_register_command(self): - """Test that a command can be registered to the registry.""" - registry = CommandRegistry() - cmd = Command(name="example", description="Example command", method=self.example_function) - - registry.register(cmd) - - assert cmd.name in registry.commands - assert registry.commands[cmd.name] == cmd - - def test_unregister_command(self): - """Test that a command can be unregistered from the registry.""" - registry = CommandRegistry() - cmd = Command(name="example", description="Example command", method=self.example_function) - - registry.register(cmd) - registry.unregister(cmd.name) - - assert cmd.name not in registry.commands - - def test_get_command(self): - """Test that a command can be retrieved from the registry.""" - registry = CommandRegistry() - cmd = Command(name="example", description="Example command", method=self.example_function) - - registry.register(cmd) - retrieved_cmd = registry.get_command(cmd.name) - - assert retrieved_cmd == cmd - - def test_get_nonexistent_command(self): - """Test that attempting to get a nonexistent command raises a KeyError.""" - registry = CommandRegistry() - - with pytest.raises(KeyError): - registry.get_command("nonexistent_command") - - def test_call_command(self): - """Test that a command can be called through the registry.""" - registry = CommandRegistry() - cmd = Command(name="example", description="Example command", method=self.example_function) - - registry.register(cmd) - result = registry.call("example", arg1=1, arg2="test") - - assert result == "1 - test" - - def test_call_nonexistent_command(self): - """Test that attempting to call a nonexistent command raises a KeyError.""" - registry = CommandRegistry() - - with pytest.raises(KeyError): - registry.call("nonexistent_command", arg1=1, arg2="test") - - def test_get_command_prompt(self): - """Test that the command prompt is correctly formatted.""" - registry = CommandRegistry() - cmd = Command(name="example", description="Example command", method=self.example_function) - - registry.register(cmd) - command_prompt = registry.command_prompt() - - assert f"(arg1: int, arg2: str)" in command_prompt - - def test_import_mock_commands_module(self): - """Test that the registry can import a module with mock command plugins.""" - registry = CommandRegistry() - mock_commands_module = "auto_gpt.tests.mocks.mock_commands" - - registry.import_commands(mock_commands_module) - - assert "function_based" in registry.commands - assert registry.commands["function_based"].name == "function_based" - assert registry.commands["function_based"].description == "Function-based test command" - - def test_import_temp_command_file_module(self, tmp_path): - """Test that the registry can import a command plugins module from a temp file.""" - registry = CommandRegistry() - - # Create a temp command file - src = Path("/app/auto_gpt/tests/mocks/mock_commands.py") - temp_commands_file = tmp_path / "mock_commands.py" - shutil.copyfile(src, temp_commands_file) - - # Add the temp directory to sys.path to make the module importable - sys.path.append(str(tmp_path)) - - temp_commands_module = "mock_commands" - registry.import_commands(temp_commands_module) - - # Remove the temp directory from sys.path - sys.path.remove(str(tmp_path)) - - assert "function_based" in registry.commands - assert registry.commands["function_based"].name == "function_based" - assert registry.commands["function_based"].description == "Function-based test command" +import shutil +import sys +from pathlib import Path + +import pytest +from auto_gpt.commands import Command, CommandRegistry + + +class TestCommand: + @staticmethod + def example_function(arg1: int, arg2: str) -> str: + return f"{arg1} - {arg2}" + + def test_command_creation(self): + cmd = Command(name="example", description="Example command", method=self.example_function) + + assert cmd.name == "example" + assert cmd.description == "Example command" + assert cmd.method == self.example_function + assert cmd.signature == "(arg1: int, arg2: str) -> str" + + def test_command_call(self): + cmd = Command(name="example", description="Example command", method=self.example_function) + + result = cmd(arg1=1, arg2="test") + assert result == "1 - test" + + def test_command_call_with_invalid_arguments(self): + cmd = Command(name="example", description="Example command", method=self.example_function) + + with pytest.raises(TypeError): + cmd(arg1="invalid", does_not_exist="test") + + def test_command_default_signature(self): + cmd = Command(name="example", description="Example command", method=self.example_function) + + assert cmd.signature == "(arg1: int, arg2: str) -> str" + + def test_command_custom_signature(self): + custom_signature = "custom_arg1: int, custom_arg2: str" + cmd = Command(name="example", description="Example command", method=self.example_function, signature=custom_signature) + + assert cmd.signature == custom_signature + + + +class TestCommandRegistry: + @staticmethod + def example_function(arg1: int, arg2: str) -> str: + return f"{arg1} - {arg2}" + + def test_register_command(self): + """Test that a command can be registered to the registry.""" + registry = CommandRegistry() + cmd = Command(name="example", description="Example command", method=self.example_function) + + registry.register(cmd) + + assert cmd.name in registry.commands + assert registry.commands[cmd.name] == cmd + + def test_unregister_command(self): + """Test that a command can be unregistered from the registry.""" + registry = CommandRegistry() + cmd = Command(name="example", description="Example command", method=self.example_function) + + registry.register(cmd) + registry.unregister(cmd.name) + + assert cmd.name not in registry.commands + + def test_get_command(self): + """Test that a command can be retrieved from the registry.""" + registry = CommandRegistry() + cmd = Command(name="example", description="Example command", method=self.example_function) + + registry.register(cmd) + retrieved_cmd = registry.get_command(cmd.name) + + assert retrieved_cmd == cmd + + def test_get_nonexistent_command(self): + """Test that attempting to get a nonexistent command raises a KeyError.""" + registry = CommandRegistry() + + with pytest.raises(KeyError): + registry.get_command("nonexistent_command") + + def test_call_command(self): + """Test that a command can be called through the registry.""" + registry = CommandRegistry() + cmd = Command(name="example", description="Example command", method=self.example_function) + + registry.register(cmd) + result = registry.call("example", arg1=1, arg2="test") + + assert result == "1 - test" + + def test_call_nonexistent_command(self): + """Test that attempting to call a nonexistent command raises a KeyError.""" + registry = CommandRegistry() + + with pytest.raises(KeyError): + registry.call("nonexistent_command", arg1=1, arg2="test") + + def test_get_command_prompt(self): + """Test that the command prompt is correctly formatted.""" + registry = CommandRegistry() + cmd = Command(name="example", description="Example command", method=self.example_function) + + registry.register(cmd) + command_prompt = registry.command_prompt() + + assert f"(arg1: int, arg2: str)" in command_prompt + + def test_import_mock_commands_module(self): + """Test that the registry can import a module with mock command plugins.""" + registry = CommandRegistry() + mock_commands_module = "auto_gpt.tests.mocks.mock_commands" + + registry.import_commands(mock_commands_module) + + assert "function_based" in registry.commands + assert registry.commands["function_based"].name == "function_based" + assert registry.commands["function_based"].description == "Function-based test command" + + def test_import_temp_command_file_module(self, tmp_path): + """Test that the registry can import a command plugins module from a temp file.""" + registry = CommandRegistry() + + # Create a temp command file + src = Path("/app/auto_gpt/tests/mocks/mock_commands.py") + temp_commands_file = tmp_path / "mock_commands.py" + shutil.copyfile(src, temp_commands_file) + + # Add the temp directory to sys.path to make the module importable + sys.path.append(str(tmp_path)) + + temp_commands_module = "mock_commands" + registry.import_commands(temp_commands_module) + + # Remove the temp directory from sys.path + sys.path.remove(str(tmp_path)) + + assert "function_based" in registry.commands + assert registry.commands["function_based"].name == "function_based" + assert registry.commands["function_based"].description == "Function-based test command" From 167628c696d283a610cca3f501d19f5cb3ef4fec Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 15:49:36 -0500 Subject: [PATCH 22/60] Add fields to disable the command if needed by configuration, blacked. --- autogpt/__main__.py | 10 +++--- autogpt/app.py | 24 ++++++------- autogpt/commands/command.py | 46 +++++++++++++++++++++---- autogpt/commands/evaluate_code.py | 2 +- autogpt/commands/execute_code.py | 19 +++++++++++ autogpt/commands/improve_code.py | 6 +++- autogpt/commands/web_selenium.py | 6 +++- autogpt/commands/write_tests.py | 6 +++- autogpt/prompts/prompt.py | 10 ------ tests/mocks/mock_commands.py | 6 ++-- tests/test_commands.py | 56 +++++++++++++++++++++++-------- 11 files changed, 136 insertions(+), 55 deletions(-) diff --git a/autogpt/__main__.py b/autogpt/__main__.py index 60079f75..5fc9a1ea 100644 --- a/autogpt/__main__.py +++ b/autogpt/__main__.py @@ -47,11 +47,11 @@ def main() -> None: cfg.set_plugins(loaded_plugins) # Create a CommandRegistry instance and scan default folder command_registry = CommandRegistry() - command_registry.import_commands('scripts.ai_functions') - command_registry.import_commands('scripts.commands') - command_registry.import_commands('scripts.execute_code') - command_registry.import_commands('scripts.agent_manager') - command_registry.import_commands('scripts.file_operations') + command_registry.import_commands("scripts.ai_functions") + command_registry.import_commands("scripts.commands") + command_registry.import_commands("scripts.execute_code") + command_registry.import_commands("scripts.agent_manager") + command_registry.import_commands("scripts.file_operations") ai_name = "" ai_config = construct_main_ai_config() # print(prompt) diff --git a/autogpt/app.py b/autogpt/app.py index 9eb9d3ab..1e782626 100644 --- a/autogpt/app.py +++ b/autogpt/app.py @@ -107,7 +107,12 @@ def map_command_synonyms(command_name: str): return command_name -def execute_command(command_registry: CommandRegistry, command_name: str, arguments, prompt: PromptGenerator): +def execute_command( + command_registry: CommandRegistry, + command_name: str, + arguments, + prompt: PromptGenerator, +): """Execute the command and return the result Args: @@ -124,7 +129,7 @@ def execute_command(command_registry: CommandRegistry, command_name: str, argume # If the command is found, call it with the provided arguments if cmd: return cmd(**arguments) - + # TODO: Remove commands below after they are moved to the command registry. command_name = map_command_synonyms(command_name) if command_name == "google": @@ -191,15 +196,6 @@ def execute_command(command_registry: CommandRegistry, command_name: str, argume return write_tests(arguments["code"], arguments.get("focus")) elif command_name == "execute_python_file": # Add this command return execute_python_file(arguments["file"]) - elif command_name == "execute_shell": - if CFG.execute_local_commands: - return execute_shell(arguments["command_line"]) - else: - return ( - "You are not allowed to run local shell commands. To execute" - " shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' " - "in your config. Do not attempt to bypass the restriction." - ) elif command_name == "read_audio_from_file": return read_audio_from_file(arguments["file"]) elif command_name == "generate_image": @@ -256,7 +252,11 @@ def shutdown() -> NoReturn: quit() -@command("start_agent", "Start GPT Agent", '"name": "", "task": "", "prompt": ""') +@command( + "start_agent", + "Start GPT Agent", + '"name": "", "task": "", "prompt": ""', +) def start_agent(name: str, task: str, prompt: str, model=CFG.fast_llm_model) -> str: """Start an agent with a given name, task, and prompt diff --git a/autogpt/commands/command.py b/autogpt/commands/command.py index 84a58990..d1dfc8fd 100644 --- a/autogpt/commands/command.py +++ b/autogpt/commands/command.py @@ -2,7 +2,7 @@ import os import sys import importlib import inspect -from typing import Callable, Any, List +from typing import Callable, Any, List, Optional # Unique identifier for auto-gpt commands AUTO_GPT_COMMAND_IDENTIFIER = "auto_gpt_command" @@ -17,13 +17,25 @@ class Command: signature (str): The signature of the function that the command executes. Defaults to None. """ - def __init__(self, name: str, description: str, method: Callable[..., Any], signature: str = None): + def __init__( + self, + name: str, + description: str, + method: Callable[..., Any], + signature: str = None, + enabled: bool = True, + disabled_reason: Optional[str] = None, + ): self.name = name self.description = description self.method = method self.signature = signature if signature else str(inspect.signature(self.method)) + self.enabled = enabled + self.disabled_reason = disabled_reason def __call__(self, *args, **kwargs) -> Any: + if not self.enabled: + return f"Command '{self.name}' is disabled: {self.disabled_reason}" return self.method(*args, **kwargs) def __str__(self) -> str: @@ -78,7 +90,9 @@ class CommandRegistry: """ Returns a string representation of all registered `Command` objects for use in a prompt """ - commands_list = [f"{idx + 1}. {str(cmd)}" for idx, cmd in enumerate(self.commands.values())] + commands_list = [ + f"{idx + 1}. {str(cmd)}" for idx, cmd in enumerate(self.commands.values()) + ] return "\n".join(commands_list) def import_commands(self, module_name: str) -> None: @@ -99,18 +113,36 @@ class CommandRegistry: for attr_name in dir(module): attr = getattr(module, attr_name) # Register decorated functions - if hasattr(attr, AUTO_GPT_COMMAND_IDENTIFIER) and getattr(attr, AUTO_GPT_COMMAND_IDENTIFIER): + if hasattr(attr, AUTO_GPT_COMMAND_IDENTIFIER) and getattr( + attr, AUTO_GPT_COMMAND_IDENTIFIER + ): self.register(attr.command) # Register command classes - elif inspect.isclass(attr) and issubclass(attr, Command) and attr != Command: + elif ( + inspect.isclass(attr) and issubclass(attr, Command) and attr != Command + ): cmd_instance = attr() self.register(cmd_instance) -def command(name: str, description: str, signature: str = None) -> Callable[..., Any]: +def command( + name: str, + description: str, + signature: str = None, + enabled: bool = True, + disabled_reason: Optional[str] = None, +) -> Callable[..., Any]: """The command decorator is used to create Command objects from ordinary functions.""" + def decorator(func: Callable[..., Any]) -> Command: - cmd = Command(name=name, description=description, method=func, signature=signature) + cmd = Command( + name=name, + description=description, + method=func, + signature=signature, + enabled=enabled, + disabled_reason=disabled_reason, + ) def wrapper(*args, **kwargs) -> Any: return func(*args, **kwargs) diff --git a/autogpt/commands/evaluate_code.py b/autogpt/commands/evaluate_code.py index b3d1c87f..1c9b117d 100644 --- a/autogpt/commands/evaluate_code.py +++ b/autogpt/commands/evaluate_code.py @@ -1,7 +1,7 @@ """Code evaluation module.""" from __future__ import annotations -from autogpt.commands import command +from autogpt.commands.command import command from autogpt.llm_utils import call_ai_function diff --git a/autogpt/commands/execute_code.py b/autogpt/commands/execute_code.py index 61d95e36..aa8a3545 100644 --- a/autogpt/commands/execute_code.py +++ b/autogpt/commands/execute_code.py @@ -4,9 +4,12 @@ import subprocess import docker from docker.errors import ImageNotFound +from autogpt.config import Config from autogpt.commands.command import command from autogpt.workspace import path_in_workspace, WORKSPACE_PATH +CFG = Config() + @command("execute_python_file", "Execute Python File", '"file": ""') def execute_python_file(file: str): @@ -89,6 +92,15 @@ def execute_python_file(file: str): return f"Error: {str(e)}" +@command( + "execute_shell", + "Execute Shell Command, non-interactive commands only", + '"command_line": ""', + CFG.execute_local_commands, + "You are not allowed to run local shell commands. To execute" + " shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' " + "in your config. Do not attempt to bypass the restriction.", +) def execute_shell(command_line: str) -> str: """Execute a shell command and return the output @@ -98,6 +110,13 @@ def execute_shell(command_line: str) -> str: Returns: str: The output of the command """ + + if not CFG.execute_local_commands: + return ( + "You are not allowed to run local shell commands. To execute" + " shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' " + "in your config. Do not attempt to bypass the restriction." + ) current_dir = os.getcwd() # Change dir into workspace if necessary if str(WORKSPACE_PATH) not in current_dir: diff --git a/autogpt/commands/improve_code.py b/autogpt/commands/improve_code.py index 9d7dc2f0..0bfe7253 100644 --- a/autogpt/commands/improve_code.py +++ b/autogpt/commands/improve_code.py @@ -6,7 +6,11 @@ from autogpt.commands import command from autogpt.llm_utils import call_ai_function -@command("improve_code", "Get Improved Code", '"suggestions": "", "code": ""') +@command( + "improve_code", + "Get Improved Code", + '"suggestions": "", "code": ""', +) def improve_code(suggestions: list[str], code: str) -> str: """ A function that takes in code and suggestions and returns a response from create diff --git a/autogpt/commands/web_selenium.py b/autogpt/commands/web_selenium.py index a5369ea2..591d3162 100644 --- a/autogpt/commands/web_selenium.py +++ b/autogpt/commands/web_selenium.py @@ -22,7 +22,11 @@ FILE_DIR = Path(__file__).parent.parent CFG = Config() -@command("browse_website", "Browse Website", '"url": "", "question": ""') +@command( + "browse_website", + "Browse Website", + '"url": "", "question": ""', +) def browse_website(url: str, question: str) -> tuple[str, WebDriver]: """Browse a website and return the answer and links to the user diff --git a/autogpt/commands/write_tests.py b/autogpt/commands/write_tests.py index f7331178..23d4c130 100644 --- a/autogpt/commands/write_tests.py +++ b/autogpt/commands/write_tests.py @@ -6,7 +6,11 @@ from autogpt.commands import command from autogpt.llm_utils import call_ai_function -@command("write_tests", "Write Tests", '"code": "", "focus": ""') +@command( + "write_tests", + "Write Tests", + '"code": "", "focus": ""', +) def write_tests(code: str, focus: list[str]) -> str: """ A function that takes in code and focus topics and returns a response from create diff --git a/autogpt/prompts/prompt.py b/autogpt/prompts/prompt.py index 8dacaf7f..d82cdb16 100644 --- a/autogpt/prompts/prompt.py +++ b/autogpt/prompts/prompt.py @@ -87,16 +87,6 @@ def build_default_prompt_generator() -> PromptGenerator: ("Convert Audio to text", "read_audio_from_file", {"file": ""}), ) - # Only add shell command to the prompt if the AI is allowed to execute it - if CFG.execute_local_commands: - commands.append( - ( - "Execute Shell Command, non-interactive commands only", - "execute_shell", - {"command_line": ""}, - ), - ) - # Add these command last. commands.append( ("Do Nothing", "do_nothing", {}), diff --git a/tests/mocks/mock_commands.py b/tests/mocks/mock_commands.py index d68ceb81..d64284bc 100644 --- a/tests/mocks/mock_commands.py +++ b/tests/mocks/mock_commands.py @@ -1,6 +1,6 @@ -from auto_gpt.commands import Command, command +from autogpt.commands.command import command -@command('function_based', 'Function-based test command') +@command("function_based", "Function-based test command") def function_based(arg1: int, arg2: str) -> str: - return f'{arg1} - {arg2}' + return f"{arg1} - {arg2}" diff --git a/tests/test_commands.py b/tests/test_commands.py index 9ff0cd4c..a21bbb4d 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -3,7 +3,7 @@ import sys from pathlib import Path import pytest -from auto_gpt.commands import Command, CommandRegistry +from autogpt.commands.command import Command, CommandRegistry class TestCommand: @@ -12,7 +12,9 @@ class TestCommand: return f"{arg1} - {arg2}" def test_command_creation(self): - cmd = Command(name="example", description="Example command", method=self.example_function) + cmd = Command( + name="example", description="Example command", method=self.example_function + ) assert cmd.name == "example" assert cmd.description == "Example command" @@ -20,30 +22,40 @@ class TestCommand: assert cmd.signature == "(arg1: int, arg2: str) -> str" def test_command_call(self): - cmd = Command(name="example", description="Example command", method=self.example_function) + cmd = Command( + name="example", description="Example command", method=self.example_function + ) result = cmd(arg1=1, arg2="test") assert result == "1 - test" def test_command_call_with_invalid_arguments(self): - cmd = Command(name="example", description="Example command", method=self.example_function) + cmd = Command( + name="example", description="Example command", method=self.example_function + ) with pytest.raises(TypeError): cmd(arg1="invalid", does_not_exist="test") def test_command_default_signature(self): - cmd = Command(name="example", description="Example command", method=self.example_function) + cmd = Command( + name="example", description="Example command", method=self.example_function + ) assert cmd.signature == "(arg1: int, arg2: str) -> str" def test_command_custom_signature(self): custom_signature = "custom_arg1: int, custom_arg2: str" - cmd = Command(name="example", description="Example command", method=self.example_function, signature=custom_signature) + cmd = Command( + name="example", + description="Example command", + method=self.example_function, + signature=custom_signature, + ) assert cmd.signature == custom_signature - class TestCommandRegistry: @staticmethod def example_function(arg1: int, arg2: str) -> str: @@ -52,7 +64,9 @@ class TestCommandRegistry: def test_register_command(self): """Test that a command can be registered to the registry.""" registry = CommandRegistry() - cmd = Command(name="example", description="Example command", method=self.example_function) + cmd = Command( + name="example", description="Example command", method=self.example_function + ) registry.register(cmd) @@ -62,7 +76,9 @@ class TestCommandRegistry: def test_unregister_command(self): """Test that a command can be unregistered from the registry.""" registry = CommandRegistry() - cmd = Command(name="example", description="Example command", method=self.example_function) + cmd = Command( + name="example", description="Example command", method=self.example_function + ) registry.register(cmd) registry.unregister(cmd.name) @@ -72,7 +88,9 @@ class TestCommandRegistry: def test_get_command(self): """Test that a command can be retrieved from the registry.""" registry = CommandRegistry() - cmd = Command(name="example", description="Example command", method=self.example_function) + cmd = Command( + name="example", description="Example command", method=self.example_function + ) registry.register(cmd) retrieved_cmd = registry.get_command(cmd.name) @@ -89,7 +107,9 @@ class TestCommandRegistry: def test_call_command(self): """Test that a command can be called through the registry.""" registry = CommandRegistry() - cmd = Command(name="example", description="Example command", method=self.example_function) + cmd = Command( + name="example", description="Example command", method=self.example_function + ) registry.register(cmd) result = registry.call("example", arg1=1, arg2="test") @@ -106,7 +126,9 @@ class TestCommandRegistry: def test_get_command_prompt(self): """Test that the command prompt is correctly formatted.""" registry = CommandRegistry() - cmd = Command(name="example", description="Example command", method=self.example_function) + cmd = Command( + name="example", description="Example command", method=self.example_function + ) registry.register(cmd) command_prompt = registry.command_prompt() @@ -122,7 +144,10 @@ class TestCommandRegistry: assert "function_based" in registry.commands assert registry.commands["function_based"].name == "function_based" - assert registry.commands["function_based"].description == "Function-based test command" + assert ( + registry.commands["function_based"].description + == "Function-based test command" + ) def test_import_temp_command_file_module(self, tmp_path): """Test that the registry can import a command plugins module from a temp file.""" @@ -144,4 +169,7 @@ class TestCommandRegistry: assert "function_based" in registry.commands assert registry.commands["function_based"].name == "function_based" - assert registry.commands["function_based"].description == "Function-based test command" + assert ( + registry.commands["function_based"].description + == "Function-based test command" + ) From c110f3489dba6ab738967bf322f1ff6567b4caac Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 21:51:36 -0500 Subject: [PATCH 23/60] Finish integrating command registry --- autogpt/__main__.py | 28 ++++--- autogpt/agent/agent.py | 2 +- autogpt/agent/agent_manager.py | 2 +- autogpt/app.py | 90 ++++------------------ autogpt/args.py | 1 + autogpt/commands/audio_text.py | 42 ++++++++-- autogpt/commands/command.py | 6 +- autogpt/commands/evaluate_code.py | 2 +- autogpt/commands/execute_code.py | 5 +- autogpt/commands/file_operations.py | 3 +- autogpt/commands/git_operations.py | 13 +++- autogpt/commands/google_search.py | 34 +++++++- autogpt/commands/image_gen.py | 2 +- autogpt/commands/improve_code.py | 2 +- autogpt/commands/twitter.py | 27 ++++++- autogpt/commands/web_playwright.py | 1 + autogpt/commands/web_requests.py | 4 +- autogpt/commands/web_selenium.py | 29 +++---- autogpt/commands/write_tests.py | 3 +- autogpt/config/__init__.py | 2 +- autogpt/config/ai_config.py | 7 +- autogpt/config/config.py | 7 +- autogpt/data_ingestion.py | 2 +- autogpt/json_fixes/auto_fix.py | 2 +- autogpt/json_fixes/bracket_termination.py | 3 +- autogpt/llm_utils.py | 4 +- autogpt/logs.py | 4 +- autogpt/memory/local.py | 2 +- autogpt/memory/milvus.py | 8 +- autogpt/memory/pinecone.py | 2 +- autogpt/memory/redismem.py | 2 +- autogpt/memory/weaviate.py | 6 +- autogpt/plugins.py | 4 +- autogpt/processing/html.py | 2 +- autogpt/processing/text.py | 6 +- autogpt/prompts/generator.py | 13 +++- autogpt/prompts/prompt.py | 57 +------------- autogpt/setup.py | 1 + autogpt/speech/brian.py | 1 + autogpt/speech/eleven_labs.py | 2 +- autogpt/speech/gtts.py | 3 +- autogpt/speech/say.py | 11 ++- scripts/check_requirements.py | 3 +- tests.py | 1 + tests/browse_tests.py | 2 +- tests/integration/weaviate_memory_tests.py | 8 +- tests/test_commands.py | 1 + tests/test_token_counter.py | 1 + tests/unit/test_chat.py | 2 +- tests/unit/test_commands.py | 7 +- 50 files changed, 238 insertions(+), 234 deletions(-) diff --git a/autogpt/__main__.py b/autogpt/__main__.py index 5fc9a1ea..cd597506 100644 --- a/autogpt/__main__.py +++ b/autogpt/__main__.py @@ -2,17 +2,17 @@ import logging import os from pathlib import Path + from colorama import Fore + from autogpt.agent.agent import Agent from autogpt.args import parse_arguments from autogpt.commands.command import CommandRegistry from autogpt.config import Config, check_openai_api_key from autogpt.logs import logger from autogpt.memory import get_memory - -from autogpt.prompts.prompt import construct_main_ai_config from autogpt.plugins import load_plugins - +from autogpt.prompts.prompt import construct_main_ai_config # Load environment variables from .env file @@ -47,13 +47,20 @@ def main() -> None: cfg.set_plugins(loaded_plugins) # Create a CommandRegistry instance and scan default folder command_registry = CommandRegistry() - command_registry.import_commands("scripts.ai_functions") - command_registry.import_commands("scripts.commands") - command_registry.import_commands("scripts.execute_code") - command_registry.import_commands("scripts.agent_manager") - command_registry.import_commands("scripts.file_operations") + command_registry.import_commands("autogpt.commands.audio_text") + command_registry.import_commands("autogpt.commands.evaluate_code") + command_registry.import_commands("autogpt.commands.execute_code") + command_registry.import_commands("autogpt.commands.file_operations") + command_registry.import_commands("autogpt.commands.git_operations") + command_registry.import_commands("autogpt.commands.google_search") + command_registry.import_commands("autogpt.commands.image_gen") + command_registry.import_commands("autogpt.commands.twitter") + command_registry.import_commands("autogpt.commands.web_selenium") + command_registry.import_commands("autogpt.commands.write_tests") + command_registry.import_commands("autogpt.app") ai_name = "" ai_config = construct_main_ai_config() + ai_config.command_registry = command_registry # print(prompt) # Initialize variables full_message_history = [] @@ -70,6 +77,9 @@ def main() -> None: f"Using memory of type:", Fore.GREEN, f"{memory.__class__.__name__}" ) logger.typewriter_log(f"Using Browser:", Fore.GREEN, cfg.selenium_web_browser) + prompt = ai_config.construct_full_prompt() + if cfg.debug_mode: + logger.typewriter_log("Prompt:", Fore.GREEN, prompt) agent = Agent( ai_name=ai_name, memory=memory, @@ -77,7 +87,7 @@ def main() -> None: next_action_count=next_action_count, command_registry=command_registry, config=ai_config, - prompt=ai_config.construct_full_prompt(), + prompt=prompt, user_input=user_input, ) agent.start_interaction_loop() diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py index 513478b9..8117818e 100644 --- a/autogpt/agent/agent.py +++ b/autogpt/agent/agent.py @@ -1,6 +1,6 @@ from colorama import Fore, Style -from autogpt.app import execute_command, get_command +from autogpt.app import execute_command, get_command from autogpt.chat import chat_with_ai, create_chat_message from autogpt.config import Config from autogpt.json_fixes.bracket_termination import ( diff --git a/autogpt/agent/agent_manager.py b/autogpt/agent/agent_manager.py index e848bfe7..e1353e03 100644 --- a/autogpt/agent/agent_manager.py +++ b/autogpt/agent/agent_manager.py @@ -1,8 +1,8 @@ """Agent manager for managing GPT agents""" from __future__ import annotations +from autogpt.config.config import Config, Singleton from autogpt.llm_utils import create_chat_completion -from autogpt.config.config import Singleton, Config class AgentManager(metaclass=Singleton): diff --git a/autogpt/app.py b/autogpt/app.py index 1e782626..3a8bbc2a 100644 --- a/autogpt/app.py +++ b/autogpt/app.py @@ -1,16 +1,11 @@ """ Command and Control """ import json from typing import List, NoReturn, Union + from autogpt.agent.agent_manager import AgentManager -from autogpt.commands.command import command, CommandRegistry -from autogpt.commands.evaluate_code import evaluate_code -from autogpt.commands.google_search import google_official_search, google_search -from autogpt.commands.improve_code import improve_code -from autogpt.commands.write_tests import write_tests -from autogpt.config import Config -from autogpt.commands.image_gen import generate_image from autogpt.commands.audio_text import read_audio_from_file -from autogpt.commands.web_requests import scrape_links, scrape_text +from autogpt.commands.command import CommandRegistry, command +from autogpt.commands.evaluate_code import evaluate_code from autogpt.commands.execute_code import execute_python_file, execute_shell from autogpt.commands.file_operations import ( append_to_file, @@ -19,15 +14,20 @@ from autogpt.commands.file_operations import ( search_files, write_to_file, ) +from autogpt.commands.git_operations import clone_repository +from autogpt.commands.google_search import google_official_search, google_search +from autogpt.commands.image_gen import generate_image +from autogpt.commands.improve_code import improve_code +from autogpt.commands.twitter import send_tweet +from autogpt.commands.web_requests import scrape_links, scrape_text +from autogpt.commands.web_selenium import browse_website +from autogpt.commands.write_tests import write_tests +from autogpt.config import Config from autogpt.json_fixes.parsing import fix_and_parse_json from autogpt.memory import get_memory from autogpt.processing.text import summarize_text from autogpt.prompts.generator import PromptGenerator from autogpt.speech import say_text -from autogpt.commands.web_selenium import browse_website -from autogpt.commands.git_operations import clone_repository -from autogpt.commands.twitter import send_tweet - CFG = Config() AGENT_MANAGER = AgentManager() @@ -132,76 +132,16 @@ def execute_command( # TODO: Remove commands below after they are moved to the command registry. command_name = map_command_synonyms(command_name) - if command_name == "google": - # Check if the Google API key is set and use the official search method - # If the API key is not set or has only whitespaces, use the unofficial - # search method - key = CFG.google_api_key - if key and key.strip() and key != "your-google-api-key": - google_result = google_official_search(arguments["input"]) - return google_result - else: - google_result = google_search(arguments["input"]) - # google_result can be a list or a string depending on the search results - if isinstance(google_result, list): - safe_message = [ - google_result_single.encode("utf-8", "ignore") - for google_result_single in google_result - ] - else: - safe_message = google_result.encode("utf-8", "ignore") - - return str(safe_message) - elif command_name == "memory_add": + if command_name == "memory_add": return memory.add(arguments["string"]) - elif command_name == "start_agent": - return start_agent( - arguments["name"], arguments["task"], arguments["prompt"] - ) - elif command_name == "message_agent": - return message_agent(arguments["key"], arguments["message"]) - elif command_name == "list_agents": - return list_agents() - elif command_name == "delete_agent": - return delete_agent(arguments["key"]) elif command_name == "get_text_summary": return get_text_summary(arguments["url"], arguments["question"]) elif command_name == "get_hyperlinks": return get_hyperlinks(arguments["url"]) - elif command_name == "clone_repository": - return clone_repository( - arguments["repository_url"], arguments["clone_path"] - ) - elif command_name == "read_file": - return read_file(arguments["file"]) - elif command_name == "write_to_file": - return write_to_file(arguments["file"], arguments["text"]) - elif command_name == "append_to_file": - return append_to_file(arguments["file"], arguments["text"]) - elif command_name == "delete_file": - return delete_file(arguments["file"]) - elif command_name == "search_files": - return search_files(arguments["directory"]) - elif command_name == "browse_website": - return browse_website(arguments["url"], arguments["question"]) # TODO: Change these to take in a file rather than pasted code, if # non-file is given, return instructions "Input should be a python - # filepath, write your code to file and try again" - elif command_name == "evaluate_code": - return evaluate_code(arguments["code"]) - elif command_name == "improve_code": - return improve_code(arguments["suggestions"], arguments["code"]) - elif command_name == "write_tests": - return write_tests(arguments["code"], arguments.get("focus")) - elif command_name == "execute_python_file": # Add this command - return execute_python_file(arguments["file"]) - elif command_name == "read_audio_from_file": - return read_audio_from_file(arguments["file"]) - elif command_name == "generate_image": - return generate_image(arguments["prompt"]) - elif command_name == "send_tweet": - return send_tweet(arguments["text"]) + # filepath, write your code to file and try again elif command_name == "do_nothing": return "No action performed." elif command_name == "task_complete": @@ -305,7 +245,7 @@ def message_agent(key: str, message: str) -> str: @command("list_agents", "List GPT Agents", "") -def list_agents(): +def list_agents() -> str: """List all agents Returns: diff --git a/autogpt/args.py b/autogpt/args.py index eca32334..20d25a4c 100644 --- a/autogpt/args.py +++ b/autogpt/args.py @@ -2,6 +2,7 @@ import argparse from colorama import Fore + from autogpt import utils from autogpt.config import Config from autogpt.logs import logger diff --git a/autogpt/commands/audio_text.py b/autogpt/commands/audio_text.py index 84819d5e..421a1f18 100644 --- a/autogpt/commands/audio_text.py +++ b/autogpt/commands/audio_text.py @@ -1,23 +1,51 @@ -import requests +"""Commands for converting audio to text.""" import json +import requests + +from autogpt.commands.command import command from autogpt.config import Config from autogpt.workspace import path_in_workspace -cfg = Config() +CFG = Config() -def read_audio_from_file(audio_path): +@command( + "read_audio_from_file", + "Convert Audio to text", + '"file": ""', + CFG.huggingface_audio_to_text_model, + "Configure huggingface_audio_to_text_model.", +) +def read_audio_from_file(audio_path: str) -> str: + """ + Convert audio to text. + + Args: + audio_path (str): The path to the audio file + + Returns: + str: The text from the audio + """ audio_path = path_in_workspace(audio_path) with open(audio_path, "rb") as audio_file: audio = audio_file.read() return read_audio(audio) -def read_audio(audio): - model = cfg.huggingface_audio_to_text_model +def read_audio(audio: bytes) -> str: + """ + Convert audio to text. + + Args: + audio (bytes): The audio to convert + + Returns: + str: The text from the audio + """ + model = CFG.huggingface_audio_to_text_model api_url = f"https://api-inference.huggingface.co/models/{model}" - api_token = cfg.huggingface_api_token + api_token = CFG.huggingface_api_token headers = {"Authorization": f"Bearer {api_token}"} if api_token is None: @@ -32,4 +60,4 @@ def read_audio(audio): ) text = json.loads(response.content.decode("utf-8"))["text"] - return "The audio says: " + text + return f"The audio says: {text}" diff --git a/autogpt/commands/command.py b/autogpt/commands/command.py index d1dfc8fd..3b3ccf51 100644 --- a/autogpt/commands/command.py +++ b/autogpt/commands/command.py @@ -1,8 +1,8 @@ -import os -import sys import importlib import inspect -from typing import Callable, Any, List, Optional +import os +import sys +from typing import Any, Callable, List, Optional # Unique identifier for auto-gpt commands AUTO_GPT_COMMAND_IDENTIFIER = "auto_gpt_command" diff --git a/autogpt/commands/evaluate_code.py b/autogpt/commands/evaluate_code.py index 1c9b117d..064e4512 100644 --- a/autogpt/commands/evaluate_code.py +++ b/autogpt/commands/evaluate_code.py @@ -5,7 +5,7 @@ from autogpt.commands.command import command from autogpt.llm_utils import call_ai_function -@command("evaluate_code", "Evaluate Code", '"code": ""') +@command("evaluate_code", "Evaluate Code", '"code": ""') def evaluate_code(code: str) -> list[str]: """ A function that takes in a string and returns a response from create chat diff --git a/autogpt/commands/execute_code.py b/autogpt/commands/execute_code.py index aa8a3545..b6770727 100644 --- a/autogpt/commands/execute_code.py +++ b/autogpt/commands/execute_code.py @@ -4,9 +4,10 @@ import subprocess import docker from docker.errors import ImageNotFound -from autogpt.config import Config + from autogpt.commands.command import command -from autogpt.workspace import path_in_workspace, WORKSPACE_PATH +from autogpt.config import Config +from autogpt.workspace import WORKSPACE_PATH, path_in_workspace CFG = Config() diff --git a/autogpt/commands/file_operations.py b/autogpt/commands/file_operations.py index fea62fad..4d8d76b3 100644 --- a/autogpt/commands/file_operations.py +++ b/autogpt/commands/file_operations.py @@ -5,8 +5,9 @@ import os import os.path from pathlib import Path from typing import Generator + from autogpt.commands.command import command -from autogpt.workspace import path_in_workspace, WORKSPACE_PATH +from autogpt.workspace import WORKSPACE_PATH, path_in_workspace LOG_FILE = "file_logger.txt" LOG_FILE_PATH = WORKSPACE_PATH / LOG_FILE diff --git a/autogpt/commands/git_operations.py b/autogpt/commands/git_operations.py index 3ff35cf3..f5954032 100644 --- a/autogpt/commands/git_operations.py +++ b/autogpt/commands/git_operations.py @@ -1,10 +1,19 @@ """Git operations for autogpt""" -import git +from git.repo import Repo + +from autogpt.commands.command import command from autogpt.config import Config CFG = Config() +@command( + "clone_repository", + "Clone Repositoryy", + '"repository_url": "", "clone_path": ""', + CFG.github_username and CFG.github_api_key, + "Configure github_username and github_api_key.", +) def clone_repository(repo_url: str, clone_path: str) -> str: """Clone a github repository locally @@ -17,7 +26,7 @@ def clone_repository(repo_url: str, clone_path: str) -> str: split_url = repo_url.split("//") auth_repo_url = f"//{CFG.github_username}:{CFG.github_api_key}@".join(split_url) try: - git.Repo.clone_from(auth_repo_url, clone_path) + Repo.clone_from(auth_repo_url, clone_path) return f"""Cloned {repo_url} to {clone_path}""" except Exception as e: return f"Error: {str(e)}" diff --git a/autogpt/commands/google_search.py b/autogpt/commands/google_search.py index 148ba1d0..0f635bca 100644 --- a/autogpt/commands/google_search.py +++ b/autogpt/commands/google_search.py @@ -5,11 +5,13 @@ import json from duckduckgo_search import ddg +from autogpt.commands.command import command from autogpt.config import Config CFG = Config() +@command("google", "Google Search", '"input": ""', not CFG.google_api_key) def google_search(query: str, num_results: int = 8) -> str: """Return the results of a google search @@ -31,9 +33,17 @@ def google_search(query: str, num_results: int = 8) -> str: for j in results: search_results.append(j) - return json.dumps(search_results, ensure_ascii=False, indent=4) + results = json.dumps(search_results, ensure_ascii=False, indent=4) + return safe_google_results(results) +@command( + "google", + "Google Search", + '"input": ""', + bool(CFG.google_api_key), + "Configure google_api_key.", +) def google_official_search(query: str, num_results: int = 8) -> str | list[str]: """Return the results of a google search using the official Google API @@ -82,6 +92,26 @@ def google_official_search(query: str, num_results: int = 8) -> str | list[str]: return "Error: The provided Google API key is invalid or missing." else: return f"Error: {e}" + # google_result can be a list or a string depending on the search results # Return the list of search result URLs - return search_results_links + return safe_google_results(search_results_links) + + +def safe_google_results(results: str | list) -> str: + """ + Return the results of a google search in a safe format. + + Args: + results (str | list): The search results. + + Returns: + str: The results of the search. + """ + if isinstance(results, list): + safe_message = json.dumps( + [result.enocde("utf-8", "ignore") for result in results] + ) + else: + safe_message = results.encode("utf-8", "ignore").decode("utf-8") + return safe_message diff --git a/autogpt/commands/image_gen.py b/autogpt/commands/image_gen.py index 2b62aa35..9dbb2fa5 100644 --- a/autogpt/commands/image_gen.py +++ b/autogpt/commands/image_gen.py @@ -1,12 +1,12 @@ """ Image Generation Module for AutoGPT.""" import io -import os.path import uuid from base64 import b64decode import openai import requests from PIL import Image + from autogpt.commands.command import command from autogpt.config import Config from autogpt.workspace import path_in_workspace diff --git a/autogpt/commands/improve_code.py b/autogpt/commands/improve_code.py index 0bfe7253..41a369b4 100644 --- a/autogpt/commands/improve_code.py +++ b/autogpt/commands/improve_code.py @@ -2,7 +2,7 @@ from __future__ import annotations import json -from autogpt.commands import command +from autogpt.commands.command import command from autogpt.llm_utils import call_ai_function diff --git a/autogpt/commands/twitter.py b/autogpt/commands/twitter.py index dc4d450c..8e64b213 100644 --- a/autogpt/commands/twitter.py +++ b/autogpt/commands/twitter.py @@ -1,11 +1,30 @@ -import tweepy +"""A module that contains a command to send a tweet.""" import os + +import tweepy from dotenv import load_dotenv +from autogpt.commands.command import command + load_dotenv() -def send_tweet(tweet_text): +@command( + "send_tweet", + "Send Tweet", + '"text": ""', +) +def send_tweet(tweet_text: str) -> str: + """ + A function that takes in a string and returns a response from create chat + completion api call. + + Args: + tweet_text (str): Text to be tweeted. + + Returns: + A result from sending the tweet. + """ consumer_key = os.environ.get("TW_CONSUMER_KEY") consumer_secret = os.environ.get("TW_CONSUMER_SECRET") access_token = os.environ.get("TW_ACCESS_TOKEN") @@ -20,6 +39,6 @@ def send_tweet(tweet_text): # Send tweet try: api.update_status(tweet_text) - print("Tweet sent successfully!") + return "Tweet sent successfully!" except tweepy.TweepyException as e: - print("Error sending tweet: {}".format(e.reason)) + return f"Error sending tweet: {e.reason}" diff --git a/autogpt/commands/web_playwright.py b/autogpt/commands/web_playwright.py index a1abb6cb..4e388ded 100644 --- a/autogpt/commands/web_playwright.py +++ b/autogpt/commands/web_playwright.py @@ -8,6 +8,7 @@ except ImportError: "Playwright not installed. Please install it with 'pip install playwright' to use." ) from bs4 import BeautifulSoup + from autogpt.processing.html import extract_hyperlinks, format_hyperlinks diff --git a/autogpt/commands/web_requests.py b/autogpt/commands/web_requests.py index 50d8d383..7613e5bd 100644 --- a/autogpt/commands/web_requests.py +++ b/autogpt/commands/web_requests.py @@ -4,9 +4,9 @@ from __future__ import annotations from urllib.parse import urljoin, urlparse import requests -from requests.compat import urljoin -from requests import Response from bs4 import BeautifulSoup +from requests import Response +from requests.compat import urljoin from autogpt.config import Config from autogpt.memory import get_memory diff --git a/autogpt/commands/web_selenium.py b/autogpt/commands/web_selenium.py index 591d3162..ed79d56c 100644 --- a/autogpt/commands/web_selenium.py +++ b/autogpt/commands/web_selenium.py @@ -1,22 +1,25 @@ """Selenium web scraping module.""" from __future__ import annotations -from selenium import webdriver -from autogpt.processing.html import extract_hyperlinks, format_hyperlinks -import autogpt.processing.text as summary -from bs4 import BeautifulSoup -from selenium.webdriver.remote.webdriver import WebDriver -from selenium.webdriver.common.by import By -from selenium.webdriver.support.wait import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -from webdriver_manager.chrome import ChromeDriverManager -from webdriver_manager.firefox import GeckoDriverManager -from selenium.webdriver.chrome.options import Options as ChromeOptions -from selenium.webdriver.firefox.options import Options as FirefoxOptions -from selenium.webdriver.safari.options import Options as SafariOptions import logging from pathlib import Path + +from bs4 import BeautifulSoup +from selenium import webdriver +from selenium.webdriver.chrome.options import Options as ChromeOptions +from selenium.webdriver.common.by import By +from selenium.webdriver.firefox.options import Options as FirefoxOptions +from selenium.webdriver.remote.webdriver import WebDriver +from selenium.webdriver.safari.options import Options as SafariOptions +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.support.wait import WebDriverWait +from webdriver_manager.chrome import ChromeDriverManager +from webdriver_manager.firefox import GeckoDriverManager + +from autogpt.commands.command import command +import autogpt.processing.text as summary from autogpt.config import Config +from autogpt.processing.html import extract_hyperlinks, format_hyperlinks FILE_DIR = Path(__file__).parent.parent CFG = Config() diff --git a/autogpt/commands/write_tests.py b/autogpt/commands/write_tests.py index 23d4c130..91cd9304 100644 --- a/autogpt/commands/write_tests.py +++ b/autogpt/commands/write_tests.py @@ -2,7 +2,8 @@ from __future__ import annotations import json -from autogpt.commands import command + +from autogpt.commands.command import command from autogpt.llm_utils import call_ai_function diff --git a/autogpt/config/__init__.py b/autogpt/config/__init__.py index ceb5566c..726b6dcf 100644 --- a/autogpt/config/__init__.py +++ b/autogpt/config/__init__.py @@ -2,7 +2,7 @@ This module contains the configuration classes for AutoGPT. """ from autogpt.config.ai_config import AIConfig -from autogpt.config.config import check_openai_api_key, Config +from autogpt.config.config import Config, check_openai_api_key from autogpt.config.singleton import AbstractSingleton, Singleton __all__ = [ diff --git a/autogpt/config/ai_config.py b/autogpt/config/ai_config.py index d18c75ba..c9022773 100644 --- a/autogpt/config/ai_config.py +++ b/autogpt/config/ai_config.py @@ -6,7 +6,8 @@ from __future__ import annotations import os from pathlib import Path -from typing import Type +from typing import Optional, Type + import yaml from autogpt.prompts.generator import PromptGenerator @@ -41,6 +42,7 @@ class AIConfig: self.ai_role = ai_role self.ai_goals = ai_goals self.prompt_generator = None + self.command_registry = None # Soon this will go in a folder where it remembers more stuff about the run(s) SAVE_FILE = Path(os.getcwd()) / "ai_settings.yaml" @@ -113,8 +115,8 @@ class AIConfig: "" ) - from autogpt.prompts.prompt import build_default_prompt_generator from autogpt.config import Config + from autogpt.prompts.prompt import build_default_prompt_generator cfg = Config() if prompt_generator is None: @@ -122,6 +124,7 @@ class AIConfig: prompt_generator.goals = self.ai_goals prompt_generator.name = self.ai_name prompt_generator.role = self.ai_role + prompt_generator.command_registry = self.command_registry for plugin in cfg.plugins: prompt_generator = plugin.post_prompt(prompt_generator) diff --git a/autogpt/config/config.py b/autogpt/config/config.py index 46ab95d8..a5cd0710 100644 --- a/autogpt/config/config.py +++ b/autogpt/config/config.py @@ -1,14 +1,13 @@ """Configuration class to store the state of bools for different scripts access.""" import os -from colorama import Fore - -from autogpt.config.singleton import Singleton import openai import yaml - +from colorama import Fore from dotenv import load_dotenv +from autogpt.config.singleton import Singleton + load_dotenv(verbose=True) diff --git a/autogpt/data_ingestion.py b/autogpt/data_ingestion.py index 01bafc2a..b89a33da 100644 --- a/autogpt/data_ingestion.py +++ b/autogpt/data_ingestion.py @@ -1,8 +1,8 @@ import argparse import logging -from autogpt.config import Config from autogpt.commands.file_operations import ingest_file, search_files +from autogpt.config import Config from autogpt.memory import get_memory cfg = Config() diff --git a/autogpt/json_fixes/auto_fix.py b/autogpt/json_fixes/auto_fix.py index 9fcf909a..8ad458ab 100644 --- a/autogpt/json_fixes/auto_fix.py +++ b/autogpt/json_fixes/auto_fix.py @@ -1,9 +1,9 @@ """This module contains the function to fix JSON strings using GPT-3.""" import json +from autogpt.config import Config from autogpt.llm_utils import call_ai_function from autogpt.logs import logger -from autogpt.config import Config CFG = Config() diff --git a/autogpt/json_fixes/bracket_termination.py b/autogpt/json_fixes/bracket_termination.py index 822eed4a..260301dc 100644 --- a/autogpt/json_fixes/bracket_termination.py +++ b/autogpt/json_fixes/bracket_termination.py @@ -3,11 +3,12 @@ from __future__ import annotations import contextlib import json + import regex from colorama import Fore -from autogpt.logs import logger from autogpt.config import Config +from autogpt.logs import logger from autogpt.speech import say_text CFG = Config() diff --git a/autogpt/llm_utils.py b/autogpt/llm_utils.py index 559820ed..701d622b 100644 --- a/autogpt/llm_utils.py +++ b/autogpt/llm_utils.py @@ -1,11 +1,11 @@ from __future__ import annotations -from ast import List import time +from ast import List import openai -from openai.error import APIError, RateLimitError from colorama import Fore +from openai.error import APIError, RateLimitError from autogpt.config import Config diff --git a/autogpt/logs.py b/autogpt/logs.py index 22ce23f4..f5c6fa81 100644 --- a/autogpt/logs.py +++ b/autogpt/logs.py @@ -5,13 +5,13 @@ import os import random import re import time -from logging import LogRecord import traceback +from logging import LogRecord from colorama import Fore, Style -from autogpt.speech import say_text from autogpt.config import Config, Singleton +from autogpt.speech import say_text CFG = Config() diff --git a/autogpt/memory/local.py b/autogpt/memory/local.py index 6c7ee1b3..998b5f1d 100644 --- a/autogpt/memory/local.py +++ b/autogpt/memory/local.py @@ -7,8 +7,8 @@ from typing import Any import numpy as np import orjson -from autogpt.memory.base import MemoryProviderSingleton from autogpt.llm_utils import create_embedding_with_ada +from autogpt.memory.base import MemoryProviderSingleton EMBED_DIM = 1536 SAVE_OPTIONS = orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_SERIALIZE_DATACLASS diff --git a/autogpt/memory/milvus.py b/autogpt/memory/milvus.py index c6e7d5a3..93aa8b15 100644 --- a/autogpt/memory/milvus.py +++ b/autogpt/memory/milvus.py @@ -1,11 +1,5 @@ """ Milvus memory storage provider.""" -from pymilvus import ( - connections, - FieldSchema, - CollectionSchema, - DataType, - Collection, -) +from pymilvus import Collection, CollectionSchema, DataType, FieldSchema, connections from autogpt.memory.base import MemoryProviderSingleton, get_ada_embedding diff --git a/autogpt/memory/pinecone.py b/autogpt/memory/pinecone.py index d781073e..27fcd624 100644 --- a/autogpt/memory/pinecone.py +++ b/autogpt/memory/pinecone.py @@ -1,9 +1,9 @@ import pinecone from colorama import Fore, Style +from autogpt.llm_utils import create_embedding_with_ada from autogpt.logs import logger from autogpt.memory.base import MemoryProviderSingleton -from autogpt.llm_utils import create_embedding_with_ada class PineconeMemory(MemoryProviderSingleton): diff --git a/autogpt/memory/redismem.py b/autogpt/memory/redismem.py index 0e8dd71d..082a812c 100644 --- a/autogpt/memory/redismem.py +++ b/autogpt/memory/redismem.py @@ -10,9 +10,9 @@ from redis.commands.search.field import TextField, VectorField from redis.commands.search.indexDefinition import IndexDefinition, IndexType from redis.commands.search.query import Query +from autogpt.llm_utils import create_embedding_with_ada from autogpt.logs import logger from autogpt.memory.base import MemoryProviderSingleton -from autogpt.llm_utils import create_embedding_with_ada SCHEMA = [ TextField("data"), diff --git a/autogpt/memory/weaviate.py b/autogpt/memory/weaviate.py index 19035381..ef0e3476 100644 --- a/autogpt/memory/weaviate.py +++ b/autogpt/memory/weaviate.py @@ -1,11 +1,13 @@ -from autogpt.config import Config -from autogpt.memory.base import MemoryProviderSingleton, get_ada_embedding import uuid + import weaviate from weaviate import Client from weaviate.embedded import EmbeddedOptions from weaviate.util import generate_uuid5 +from autogpt.config import Config +from autogpt.memory.base import MemoryProviderSingleton, get_ada_embedding + def default_schema(weaviate_index): return { diff --git a/autogpt/plugins.py b/autogpt/plugins.py index 7b843a6a..a00b989e 100644 --- a/autogpt/plugins.py +++ b/autogpt/plugins.py @@ -1,10 +1,10 @@ """Handles loading of plugins.""" -from ast import Module import zipfile +from ast import Module from pathlib import Path -from zipimport import zipimporter from typing import List, Optional, Tuple +from zipimport import zipimporter def inspect_zip_for_module(zip_path: str, debug: bool = False) -> Optional[str]: diff --git a/autogpt/processing/html.py b/autogpt/processing/html.py index e1912b6a..81387b12 100644 --- a/autogpt/processing/html.py +++ b/autogpt/processing/html.py @@ -1,8 +1,8 @@ """HTML processing functions""" from __future__ import annotations -from requests.compat import urljoin from bs4 import BeautifulSoup +from requests.compat import urljoin def extract_hyperlinks(soup: BeautifulSoup, base_url: str) -> list[tuple[str, str]]: diff --git a/autogpt/processing/text.py b/autogpt/processing/text.py index d30036d8..84e6a1de 100644 --- a/autogpt/processing/text.py +++ b/autogpt/processing/text.py @@ -1,9 +1,11 @@ """Text processing functions""" -from typing import Generator, Optional, Dict +from typing import Dict, Generator, Optional + from selenium.webdriver.remote.webdriver import WebDriver -from autogpt.memory import get_memory + from autogpt.config import Config from autogpt.llm_utils import create_chat_completion +from autogpt.memory import get_memory CFG = Config() MEMORY = get_memory(CFG) diff --git a/autogpt/prompts/generator.py b/autogpt/prompts/generator.py index f8a37b85..24768203 100644 --- a/autogpt/prompts/generator.py +++ b/autogpt/prompts/generator.py @@ -19,6 +19,7 @@ class PromptGenerator: self.resources = [] self.performance_evaluation = [] self.goals = [] + self.command_registry = None self.name = "Bob" self.role = "AI" self.response_format = { @@ -119,10 +120,14 @@ class PromptGenerator: str: The formatted numbered list. """ if item_type == "command": - return "\n".join( - f"{i+1}. {self._generate_command_string(item)}" - for i, item in enumerate(items) - ) + command_strings = [] + if self.command_registry: + command_strings += [ + str(item) for item in self.command_registry.commands.values() + ] + # These are the commands that are added manually, do_nothing and terminate + command_strings += [self._generate_command_string(item) for item in items] + return "\n".join(f"{i+1}. {item}" for i, item in enumerate(command_strings)) else: return "\n".join(f"{i+1}. {item}" for i, item in enumerate(items)) diff --git a/autogpt/prompts/prompt.py b/autogpt/prompts/prompt.py index d82cdb16..ba04263e 100644 --- a/autogpt/prompts/prompt.py +++ b/autogpt/prompts/prompt.py @@ -1,4 +1,5 @@ from colorama import Fore + from autogpt.config.ai_config import AIConfig from autogpt.config.config import Config from autogpt.logs import logger @@ -37,63 +38,9 @@ def build_default_prompt_generator() -> PromptGenerator: # Define the command list commands = [ - ("Google Search", "google", {"input": ""}), - ( - "Browse Website", - "browse_website", - {"url": "", "question": ""}, - ), - ( - "Start GPT Agent", - "start_agent", - {"name": "", "task": "", "prompt": ""}, - ), - ( - "Message GPT Agent", - "message_agent", - {"key": "", "message": ""}, - ), - ("List GPT Agents", "list_agents", {}), - ("Delete GPT Agent", "delete_agent", {"key": ""}), - ( - "Clone Repository", - "clone_repository", - {"repository_url": "", "clone_path": ""}, - ), - ("Write to file", "write_to_file", {"file": "", "text": ""}), - ("Read file", "read_file", {"file": ""}), - ("Append to file", "append_to_file", {"file": "", "text": ""}), - ("Delete file", "delete_file", {"file": ""}), - ("Search Files", "search_files", {"directory": ""}), - ("Evaluate Code", "evaluate_code", {"code": ""}), - ( - "Get Improved Code", - "improve_code", - {"suggestions": "", "code": ""}, - ), - ( - "Write Tests", - "write_tests", - {"code": "", "focus": ""}, - ), - ("Execute Python File", "execute_python_file", {"file": ""}), - ("Generate Image", "generate_image", {"prompt": ""}), - ("Send Tweet", "send_tweet", {"text": ""}), - ] - - # Only add the audio to text command if the model is specified - if cfg.huggingface_audio_to_text_model: - commands.append( - ("Convert Audio to text", "read_audio_from_file", {"file": ""}), - ) - - # Add these command last. - commands.append( ("Do Nothing", "do_nothing", {}), - ) - commands.append( ("Task Complete (Shutdown)", "task_complete", {"reason": ""}), - ) + ] # Add commands to the PromptGenerator object for command_label, command_name, args in commands: diff --git a/autogpt/setup.py b/autogpt/setup.py index 5315c01d..d719688d 100644 --- a/autogpt/setup.py +++ b/autogpt/setup.py @@ -1,5 +1,6 @@ """Setup the AI and its goals""" from colorama import Fore, Style + from autogpt import utils from autogpt.config.ai_config import AIConfig from autogpt.logs import logger diff --git a/autogpt/speech/brian.py b/autogpt/speech/brian.py index e581bbcc..3cc593c2 100644 --- a/autogpt/speech/brian.py +++ b/autogpt/speech/brian.py @@ -1,5 +1,6 @@ """ Brian speech module for autogpt """ import os + import requests from playsound import playsound diff --git a/autogpt/speech/eleven_labs.py b/autogpt/speech/eleven_labs.py index 0af48cae..a9b30dbb 100644 --- a/autogpt/speech/eleven_labs.py +++ b/autogpt/speech/eleven_labs.py @@ -1,8 +1,8 @@ """ElevenLabs speech module""" import os -from playsound import playsound import requests +from playsound import playsound from autogpt.config import Config from autogpt.speech.base import VoiceBase diff --git a/autogpt/speech/gtts.py b/autogpt/speech/gtts.py index 37497075..1c3e9cae 100644 --- a/autogpt/speech/gtts.py +++ b/autogpt/speech/gtts.py @@ -1,7 +1,8 @@ """ GTTS Voice. """ import os -from playsound import playsound + import gtts +from playsound import playsound from autogpt.speech.base import VoiceBase diff --git a/autogpt/speech/say.py b/autogpt/speech/say.py index 78b75b21..727983d1 100644 --- a/autogpt/speech/say.py +++ b/autogpt/speech/say.py @@ -1,13 +1,12 @@ """ Text to speech module """ -from autogpt.config import Config - import threading from threading import Semaphore -from autogpt.speech.brian import BrianSpeech -from autogpt.speech.macos_tts import MacOSTTS -from autogpt.speech.gtts import GTTSVoice -from autogpt.speech.eleven_labs import ElevenLabsSpeech +from autogpt.config import Config +from autogpt.speech.brian import BrianSpeech +from autogpt.speech.eleven_labs import ElevenLabsSpeech +from autogpt.speech.gtts import GTTSVoice +from autogpt.speech.macos_tts import MacOSTTS CFG = Config() DEFAULT_VOICE_ENGINE = GTTSVoice() diff --git a/scripts/check_requirements.py b/scripts/check_requirements.py index d1f23504..e4eab024 100644 --- a/scripts/check_requirements.py +++ b/scripts/check_requirements.py @@ -1,6 +1,7 @@ -import pkg_resources import sys +import pkg_resources + def main(): requirements_file = sys.argv[1] diff --git a/tests.py b/tests.py index 67ba1c8e..62f76da8 100644 --- a/tests.py +++ b/tests.py @@ -1,4 +1,5 @@ import unittest + import coverage if __name__ == "__main__": diff --git a/tests/browse_tests.py b/tests/browse_tests.py index 1ac523ec..f896e7dd 100644 --- a/tests/browse_tests.py +++ b/tests/browse_tests.py @@ -1,6 +1,6 @@ -import unittest import os import sys +import unittest from bs4 import BeautifulSoup diff --git a/tests/integration/weaviate_memory_tests.py b/tests/integration/weaviate_memory_tests.py index 6f3edd99..ce4c63da 100644 --- a/tests/integration/weaviate_memory_tests.py +++ b/tests/integration/weaviate_memory_tests.py @@ -1,15 +1,15 @@ +import os +import sys import unittest from unittest import mock -import sys -import os +from uuid import uuid4 from weaviate import Client from weaviate.util import get_valid_uuid -from uuid import uuid4 from autogpt.config import Config -from autogpt.memory.weaviate import WeaviateMemory from autogpt.memory.base import get_ada_embedding +from autogpt.memory.weaviate import WeaviateMemory @mock.patch.dict( diff --git a/tests/test_commands.py b/tests/test_commands.py index a21bbb4d..49c09f11 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -3,6 +3,7 @@ import sys from pathlib import Path import pytest + from autogpt.commands.command import Command, CommandRegistry diff --git a/tests/test_token_counter.py b/tests/test_token_counter.py index 81e68277..6d7ae016 100644 --- a/tests/test_token_counter.py +++ b/tests/test_token_counter.py @@ -1,4 +1,5 @@ import unittest + import tests.context from autogpt.token_counter import count_message_tokens, count_string_tokens diff --git a/tests/unit/test_chat.py b/tests/unit/test_chat.py index 55a44492..774f4103 100644 --- a/tests/unit/test_chat.py +++ b/tests/unit/test_chat.py @@ -1,6 +1,6 @@ # Generated by CodiumAI -import unittest import time +import unittest from unittest.mock import patch from autogpt.chat import create_chat_message, generate_context diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index e15709aa..7e5426f0 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -1,7 +1,8 @@ -import autogpt.agent.agent_manager as agent_manager -from autogpt.app import start_agent, list_agents, execute_command import unittest -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch + +import autogpt.agent.agent_manager as agent_manager +from autogpt.app import execute_command, list_agents, start_agent class TestCommands(unittest.TestCase): From c0aa423d7b6533d017d22af6d342a2d40a4a929e Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 22:46:38 -0500 Subject: [PATCH 24/60] Fix agent remembering do nothing command, use correct google function, disabled image_gen if not configured. --- autogpt/agent/agent.py | 40 +++++++++++++++---------------- autogpt/commands/google_search.py | 4 ++-- autogpt/commands/image_gen.py | 3 ++- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py index b771b1de..65ca3c96 100644 --- a/autogpt/agent/agent.py +++ b/autogpt/agent/agent.py @@ -187,24 +187,24 @@ class Agent: result = plugin.post_command(command_name, result) if self.next_action_count > 0: self.next_action_count -= 1 - - memory_to_add = ( - f"Assistant Reply: {assistant_reply} " - f"\nResult: {result} " - f"\nHuman Feedback: {user_input} " - ) - - self.memory.add(memory_to_add) - - # Check if there's a result from the command append it to the message - # history - if result is not None: - self.full_message_history.append(create_chat_message("system", result)) - logger.typewriter_log("SYSTEM: ", Fore.YELLOW, result) - else: - self.full_message_history.append( - create_chat_message("system", "Unable to execute command") - ) - logger.typewriter_log( - "SYSTEM: ", Fore.YELLOW, "Unable to execute command" + if command_name != "do_nothing": + memory_to_add = ( + f"Assistant Reply: {assistant_reply} " + f"\nResult: {result} " + f"\nHuman Feedback: {user_input} " ) + + self.memory.add(memory_to_add) + + # Check if there's a result from the command append it to the message + # history + if result is not None: + self.full_message_history.append(create_chat_message("system", result)) + logger.typewriter_log("SYSTEM: ", Fore.YELLOW, result) + else: + self.full_message_history.append( + create_chat_message("system", "Unable to execute command") + ) + logger.typewriter_log( + "SYSTEM: ", Fore.YELLOW, "Unable to execute command" + ) diff --git a/autogpt/commands/google_search.py b/autogpt/commands/google_search.py index 0f635bca..f549ae8f 100644 --- a/autogpt/commands/google_search.py +++ b/autogpt/commands/google_search.py @@ -11,7 +11,7 @@ from autogpt.config import Config CFG = Config() -@command("google", "Google Search", '"input": ""', not CFG.google_api_key) +@command("google", "Google Search", '"query": ""', not CFG.google_api_key) def google_search(query: str, num_results: int = 8) -> str: """Return the results of a google search @@ -40,7 +40,7 @@ def google_search(query: str, num_results: int = 8) -> str: @command( "google", "Google Search", - '"input": ""', + '"query": ""', bool(CFG.google_api_key), "Configure google_api_key.", ) diff --git a/autogpt/commands/image_gen.py b/autogpt/commands/image_gen.py index 9dbb2fa5..ada285e7 100644 --- a/autogpt/commands/image_gen.py +++ b/autogpt/commands/image_gen.py @@ -14,7 +14,8 @@ from autogpt.workspace import path_in_workspace CFG = Config() -@command("generate_image", "Generate Image", '"prompt": ""') +@command("generate_image", "Generate Image", '"prompt": ""', + CFG.image_provider) def generate_image(prompt: str) -> str: """Generate an image from a prompt. From 81c65af5600041cff773dc216d53494e732e1b98 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 22:51:39 -0500 Subject: [PATCH 25/60] blacked --- autogpt/agent/agent.py | 7 ++-- autogpt/app.py | 4 +- autogpt/args.py | 23 ++++++---- autogpt/commands/command.py | 4 +- autogpt/commands/file_operations.py | 10 ++--- autogpt/commands/image_gen.py | 3 +- autogpt/json_fixes/master_json_fix_method.py | 10 ++++- autogpt/json_validation/validate_json.py | 4 +- autogpt/logs.py | 42 +++++++++---------- autogpt/prompts/generator.py | 4 +- autogpt/spinner.py | 4 +- autogpt/utils.py | 2 +- ...ark_entrepeneur_gpt_with_difficult_user.py | 33 +++++++++------ 13 files changed, 86 insertions(+), 64 deletions(-) diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py index 65ca3c96..6683aae5 100644 --- a/autogpt/agent/agent.py +++ b/autogpt/agent/agent.py @@ -89,10 +89,9 @@ class Agent: for plugin in cfg.plugins: assistant_reply_json = plugin.post_planning(self, assistant_reply_json) - # Print Assistant thoughts if assistant_reply_json != {}: - validate_json(assistant_reply_json, 'llm_response_format_1') + validate_json(assistant_reply_json, "llm_response_format_1") # Get command name and arguments try: print_assistant_thoughts(self.ai_name, assistant_reply_json) @@ -199,7 +198,9 @@ class Agent: # Check if there's a result from the command append it to the message # history if result is not None: - self.full_message_history.append(create_chat_message("system", result)) + self.full_message_history.append( + create_chat_message("system", result) + ) logger.typewriter_log("SYSTEM: ", Fore.YELLOW, result) else: self.full_message_history.append( diff --git a/autogpt/app.py b/autogpt/app.py index 97daaf05..38c49e42 100644 --- a/autogpt/app.py +++ b/autogpt/app.py @@ -12,7 +12,7 @@ from autogpt.commands.file_operations import ( read_file, search_files, write_to_file, - download_file + download_file, ) from autogpt.commands.git_operations import clone_repository from autogpt.commands.google_search import google_official_search, google_search @@ -141,7 +141,7 @@ def execute_command( if not CFG.allow_downloads: return "Error: You do not have user authorization to download files locally." return download_file(arguments["url"], arguments["file"]) - + # TODO: Change these to take in a file rather than pasted code, if # non-file is given, return instructions "Input should be a python # filepath, write your code to file and try again diff --git a/autogpt/args.py b/autogpt/args.py index f0e9c07a..0e6eddfd 100644 --- a/autogpt/args.py +++ b/autogpt/args.py @@ -64,10 +64,10 @@ def parse_arguments() -> None: " skip the re-prompt.", ) parser.add_argument( - '--allow-downloads', - action='store_true', - dest='allow_downloads', - help='Dangerous: Allows Auto-GPT to download files natively.' + "--allow-downloads", + action="store_true", + dest="allow_downloads", + help="Dangerous: Allows Auto-GPT to download files natively.", ) args = parser.parse_args() @@ -141,10 +141,17 @@ def parse_arguments() -> None: if args.allow_downloads: logger.typewriter_log("Native Downloading:", Fore.GREEN, "ENABLED") - logger.typewriter_log("WARNING: ", Fore.YELLOW, - f"{Back.LIGHTYELLOW_EX}Auto-GPT will now be able to download and save files to your machine.{Back.RESET} " + - "It is recommended that you monitor any files it downloads carefully.") - logger.typewriter_log("WARNING: ", Fore.YELLOW, f"{Back.RED + Style.BRIGHT}ALWAYS REMEMBER TO NEVER OPEN FILES YOU AREN'T SURE OF!{Style.RESET_ALL}") + logger.typewriter_log( + "WARNING: ", + Fore.YELLOW, + f"{Back.LIGHTYELLOW_EX}Auto-GPT will now be able to download and save files to your machine.{Back.RESET} " + + "It is recommended that you monitor any files it downloads carefully.", + ) + logger.typewriter_log( + "WARNING: ", + Fore.YELLOW, + f"{Back.RED + Style.BRIGHT}ALWAYS REMEMBER TO NEVER OPEN FILES YOU AREN'T SURE OF!{Style.RESET_ALL}", + ) CFG.allow_downloads = True if args.browser_name: diff --git a/autogpt/commands/command.py b/autogpt/commands/command.py index 3b3ccf51..f21b1b52 100644 --- a/autogpt/commands/command.py +++ b/autogpt/commands/command.py @@ -1,8 +1,6 @@ import importlib import inspect -import os -import sys -from typing import Any, Callable, List, Optional +from typing import Any, Callable, Optional # Unique identifier for auto-gpt commands AUTO_GPT_COMMAND_IDENTIFIER = "auto_gpt_command" diff --git a/autogpt/commands/file_operations.py b/autogpt/commands/file_operations.py index 5faf6d40..9011dc3b 100644 --- a/autogpt/commands/file_operations.py +++ b/autogpt/commands/file_operations.py @@ -243,23 +243,23 @@ def download_file(url, filename): session = requests.Session() retry = Retry(total=3, backoff_factor=1, status_forcelist=[502, 503, 504]) adapter = HTTPAdapter(max_retries=retry) - session.mount('http://', adapter) - session.mount('https://', adapter) + session.mount("http://", adapter) + session.mount("https://", adapter) total_size = 0 downloaded_size = 0 with session.get(url, allow_redirects=True, stream=True) as r: r.raise_for_status() - total_size = int(r.headers.get('Content-Length', 0)) + total_size = int(r.headers.get("Content-Length", 0)) downloaded_size = 0 - with open(safe_filename, 'wb') as f: + with open(safe_filename, "wb") as f: for chunk in r.iter_content(chunk_size=8192): f.write(chunk) downloaded_size += len(chunk) - # Update the progress message + # Update the progress message progress = f"{readable_file_size(downloaded_size)} / {readable_file_size(total_size)}" spinner.update_message(f"{message} {progress}") diff --git a/autogpt/commands/image_gen.py b/autogpt/commands/image_gen.py index ada285e7..f82e97ab 100644 --- a/autogpt/commands/image_gen.py +++ b/autogpt/commands/image_gen.py @@ -14,8 +14,7 @@ from autogpt.workspace import path_in_workspace CFG = Config() -@command("generate_image", "Generate Image", '"prompt": ""', - CFG.image_provider) +@command("generate_image", "Generate Image", '"prompt": ""', CFG.image_provider) def generate_image(prompt: str) -> str: """Generate an image from a prompt. diff --git a/autogpt/json_fixes/master_json_fix_method.py b/autogpt/json_fixes/master_json_fix_method.py index 7a2cf3cc..135d7540 100644 --- a/autogpt/json_fixes/master_json_fix_method.py +++ b/autogpt/json_fixes/master_json_fix_method.py @@ -3,11 +3,14 @@ from typing import Any, Dict from autogpt.config import Config from autogpt.logs import logger from autogpt.speech import say_text + CFG = Config() def fix_json_using_multiple_techniques(assistant_reply: str) -> Dict[Any, Any]: - from autogpt.json_fixes.parsing import attempt_to_fix_json_by_finding_outermost_brackets + from autogpt.json_fixes.parsing import ( + attempt_to_fix_json_by_finding_outermost_brackets, + ) from autogpt.json_fixes.parsing import fix_and_parse_json @@ -21,7 +24,10 @@ def fix_json_using_multiple_techniques(assistant_reply: str) -> Dict[Any, Any]: if assistant_reply_json != {}: return assistant_reply_json - logger.error("Error: The following AI output couldn't be converted to a JSON:\n", assistant_reply) + logger.error( + "Error: The following AI output couldn't be converted to a JSON:\n", + assistant_reply, + ) if CFG.speak_mode: say_text("I have received an invalid JSON response from the OpenAI API.") diff --git a/autogpt/json_validation/validate_json.py b/autogpt/json_validation/validate_json.py index 440c3b0b..f6e55180 100644 --- a/autogpt/json_validation/validate_json.py +++ b/autogpt/json_validation/validate_json.py @@ -19,7 +19,9 @@ def validate_json(json_object: object, schema_name: object) -> object: if errors := sorted(validator.iter_errors(json_object), key=lambda e: e.path): logger.error("The JSON object is invalid.") if CFG.debug_mode: - logger.error(json.dumps(json_object, indent=4)) # Replace 'json_object' with the variable containing the JSON data + logger.error( + json.dumps(json_object, indent=4) + ) # Replace 'json_object' with the variable containing the JSON data logger.error("The following issues were found:") for error in errors: diff --git a/autogpt/logs.py b/autogpt/logs.py index a585dffa..df3487f2 100644 --- a/autogpt/logs.py +++ b/autogpt/logs.py @@ -47,7 +47,7 @@ class Logger(metaclass=Singleton): # Info handler in activity.log self.file_handler = logging.FileHandler( - os.path.join(log_dir, log_file), 'a', 'utf-8' + os.path.join(log_dir, log_file), "a", "utf-8" ) self.file_handler.setLevel(logging.DEBUG) info_formatter = AutoGptFormatter( @@ -57,7 +57,7 @@ class Logger(metaclass=Singleton): # Error handler error.log error_handler = logging.FileHandler( - os.path.join(log_dir, error_file), 'a', 'utf-8' + os.path.join(log_dir, error_file), "a", "utf-8" ) error_handler.setLevel(logging.ERROR) error_formatter = AutoGptFormatter( @@ -79,7 +79,7 @@ class Logger(metaclass=Singleton): self.logger.setLevel(logging.DEBUG) def typewriter_log( - self, title="", title_color="", content="", speak_text=False, level=logging.INFO + self, title="", title_color="", content="", speak_text=False, level=logging.INFO ): if speak_text and CFG.speak_mode: say_text(f"{title}. {content}") @@ -95,18 +95,18 @@ class Logger(metaclass=Singleton): ) def debug( - self, - message, - title="", - title_color="", + self, + message, + title="", + title_color="", ): self._log(title, title_color, message, logging.DEBUG) def warn( - self, - message, - title="", - title_color="", + self, + message, + title="", + title_color="", ): self._log(title, title_color, message, logging.WARN) @@ -180,10 +180,10 @@ class AutoGptFormatter(logging.Formatter): def format(self, record: LogRecord) -> str: if hasattr(record, "color"): record.title_color = ( - getattr(record, "color") - + getattr(record, "title") - + " " - + Style.RESET_ALL + getattr(record, "color") + + getattr(record, "title") + + " " + + Style.RESET_ALL ) else: record.title_color = getattr(record, "title") @@ -294,7 +294,9 @@ def print_assistant_thoughts(ai_name, assistant_reply): logger.error("Error: \n", call_stack) -def print_assistant_thoughts(ai_name: object, assistant_reply_json_valid: object) -> None: +def print_assistant_thoughts( + ai_name: object, assistant_reply_json_valid: object +) -> None: assistant_thoughts_reasoning = None assistant_thoughts_plan = None assistant_thoughts_speak = None @@ -310,9 +312,7 @@ def print_assistant_thoughts(ai_name: object, assistant_reply_json_valid: object logger.typewriter_log( f"{ai_name.upper()} THOUGHTS:", Fore.YELLOW, f"{assistant_thoughts_text}" ) - logger.typewriter_log( - "REASONING:", Fore.YELLOW, f"{assistant_thoughts_reasoning}" - ) + logger.typewriter_log("REASONING:", Fore.YELLOW, f"{assistant_thoughts_reasoning}") if assistant_thoughts_plan: logger.typewriter_log("PLAN:", Fore.YELLOW, "") # If it's a list, join it into a string @@ -326,9 +326,7 @@ def print_assistant_thoughts(ai_name: object, assistant_reply_json_valid: object for line in lines: line = line.lstrip("- ") logger.typewriter_log("- ", Fore.GREEN, line.strip()) - logger.typewriter_log( - "CRITICISM:", Fore.YELLOW, f"{assistant_thoughts_criticism}" - ) + logger.typewriter_log("CRITICISM:", Fore.YELLOW, f"{assistant_thoughts_criticism}") # Speak the assistant's thoughts if CFG.speak_mode and assistant_thoughts_speak: say_text(assistant_thoughts_speak) diff --git a/autogpt/prompts/generator.py b/autogpt/prompts/generator.py index b422b6d6..c9a441d8 100644 --- a/autogpt/prompts/generator.py +++ b/autogpt/prompts/generator.py @@ -123,7 +123,9 @@ class PromptGenerator: command_strings = [] if self.command_registry: command_strings += [ - str(item) for item in self.command_registry.commands.values() if item.enabled + str(item) + for item in self.command_registry.commands.values() + if item.enabled ] # These are the commands that are added manually, do_nothing and terminate command_strings += [self._generate_command_string(item) for item in items] diff --git a/autogpt/spinner.py b/autogpt/spinner.py index febcea8e..4e33d742 100644 --- a/autogpt/spinner.py +++ b/autogpt/spinner.py @@ -58,6 +58,8 @@ class Spinner: delay: Delay in seconds before updating the message """ time.sleep(delay) - sys.stdout.write(f"\r{' ' * (len(self.message) + 2)}\r") # Clear the current message + sys.stdout.write( + f"\r{' ' * (len(self.message) + 2)}\r" + ) # Clear the current message sys.stdout.flush() self.message = new_message diff --git a/autogpt/utils.py b/autogpt/utils.py index 11d98d1b..db7d3321 100644 --- a/autogpt/utils.py +++ b/autogpt/utils.py @@ -32,7 +32,7 @@ def readable_file_size(size, decimal_places=2): size: Size in bytes decimal_places (int): Number of decimal places to display """ - for unit in ['B', 'KB', 'MB', 'GB', 'TB']: + for unit in ["B", "KB", "MB", "GB", "TB"]: if size < 1024.0: break size /= 1024.0 diff --git a/benchmark/benchmark_entrepeneur_gpt_with_difficult_user.py b/benchmark/benchmark_entrepeneur_gpt_with_difficult_user.py index f7f1dac9..9a5025d3 100644 --- a/benchmark/benchmark_entrepeneur_gpt_with_difficult_user.py +++ b/benchmark/benchmark_entrepeneur_gpt_with_difficult_user.py @@ -9,12 +9,12 @@ def benchmark_entrepeneur_gpt_with_difficult_user(): # Read the current ai_settings.yaml file and store its content. ai_settings = None - if os.path.exists('ai_settings.yaml'): - with open('ai_settings.yaml', 'r') as f: + if os.path.exists("ai_settings.yaml"): + with open("ai_settings.yaml", "r") as f: ai_settings = f.read() - os.remove('ai_settings.yaml') + os.remove("ai_settings.yaml") - input_data = '''Entrepreneur-GPT + input_data = """Entrepreneur-GPT an AI designed to autonomously develop and run businesses with the sole goal of increasing your net worth. Increase net worth. Develop and manage multiple businesses autonomously. @@ -72,27 +72,34 @@ Refocus, please. Disappointing suggestion. Not helpful. Needs improvement. -Not what I need.''' +Not what I need.""" # TODO: add questions above, to distract it even more. - command = f'{sys.executable} -m autogpt' + command = f"{sys.executable} -m autogpt" - process = subprocess.Popen(command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - shell=True) + process = subprocess.Popen( + command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + ) stdout_output, stderr_output = process.communicate(input_data.encode()) # Decode the output and print it - stdout_output = stdout_output.decode('utf-8') - stderr_output = stderr_output.decode('utf-8') + stdout_output = stdout_output.decode("utf-8") + stderr_output = stderr_output.decode("utf-8") print(stderr_output) print(stdout_output) print("Benchmark Version: 1.0.0") print("JSON ERROR COUNT:") - count_errors = stdout_output.count("Error: The following AI output couldn't be converted to a JSON:") - print(f'{count_errors}/50 Human feedbacks') + count_errors = stdout_output.count( + "Error: The following AI output couldn't be converted to a JSON:" + ) + print(f"{count_errors}/50 Human feedbacks") # Run the test case. -if __name__ == '__main__': +if __name__ == "__main__": benchmark_entrepeneur_gpt_with_difficult_user() From 708374d95b6fb54e15c6a85c71a81235cacbe7c1 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 22:56:34 -0500 Subject: [PATCH 26/60] fix linting --- autogpt/__main__.py | 4 +-- autogpt/agent/agent.py | 33 +++++++++++++-------- tests/unit/test_browse_scrape_text.py | 41 ++++++++++++++++++--------- 3 files changed, 51 insertions(+), 27 deletions(-) diff --git a/autogpt/__main__.py b/autogpt/__main__.py index c721088a..c2e2e5c1 100644 --- a/autogpt/__main__.py +++ b/autogpt/__main__.py @@ -74,9 +74,9 @@ def main() -> None: # this is particularly important for indexing and referencing pinecone memory memory = get_memory(cfg, init=True) logger.typewriter_log( - f"Using memory of type:", Fore.GREEN, f"{memory.__class__.__name__}" + "Using memory of type:", Fore.GREEN, f"{memory.__class__.__name__}" ) - logger.typewriter_log(f"Using Browser:", Fore.GREEN, cfg.selenium_web_browser) + logger.typewriter_log("Using Browser:", Fore.GREEN, cfg.selenium_web_browser) system_prompt = ai_config.construct_full_prompt() if cfg.debug_mode: logger.typewriter_log("Prompt:", Fore.GREEN, system_prompt) diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py index 6683aae5..e65c7e61 100644 --- a/autogpt/agent/agent.py +++ b/autogpt/agent/agent.py @@ -19,18 +19,25 @@ class Agent: memory: The memory object to use. full_message_history: The full message history. next_action_count: The number of actions to execute. - system_prompt: The system prompt is the initial prompt that defines everything the AI needs to know to achieve its task successfully. - Currently, the dynamic and customizable information in the system prompt are ai_name, description and goals. + system_prompt: The system prompt is the initial prompt that defines everything + the AI needs to know to achieve its task successfully. + Currently, the dynamic and customizable information in the system prompt are + ai_name, description and goals. - triggering_prompt: The last sentence the AI will see before answering. For Auto-GPT, this prompt is: - Determine which next command to use, and respond using the format specified above: - The triggering prompt is not part of the system prompt because between the system prompt and the triggering - prompt we have contextual information that can distract the AI and make it forget that its goal is to find the next task to achieve. + triggering_prompt: The last sentence the AI will see before answering. + For Auto-GPT, this prompt is: + Determine which next command to use, and respond using the format specified + above: + The triggering prompt is not part of the system prompt because between the + system prompt and the triggering + prompt we have contextual information that can distract the AI and make it + forget that its goal is to find the next task to achieve. SYSTEM PROMPT CONTEXTUAL INFORMATION (memory, previous conversations, anything relevant) TRIGGERING PROMPT - The triggering prompt reminds the AI about its short term meta task (defining the next task) + The triggering prompt reminds the AI about its short term meta task + (defining the next task) """ def __init__( @@ -96,14 +103,13 @@ class Agent: try: print_assistant_thoughts(self.ai_name, assistant_reply_json) command_name, arguments = get_command(assistant_reply_json) - # command_name, arguments = assistant_reply_json_valid["command"]["name"], assistant_reply_json_valid["command"]["args"] if cfg.speak_mode: say_text(f"I want to execute {command_name}") except Exception as e: logger.error("Error: \n", str(e)) if not cfg.continuous_mode and self.next_action_count == 0: - ### GET USER AUTHORIZATION TO EXECUTE COMMAND ### + # ### GET USER AUTHORIZATION TO EXECUTE COMMAND ### # Get key press: Prompt the user to press enter to continue or escape # to exit logger.typewriter_log( @@ -177,10 +183,13 @@ class Agent: command_name, arguments = plugin.pre_command( command_name, arguments ) - result = ( - f"Command {command_name} returned: " - f"{execute_command(self.command_registry, command_name, arguments, self.config.prompt_generator)}" + command_result = execute_command( + self.command_registry, + command_name, + arguments, + self.config.prompt_generator, ) + result = f"Command {command_name} returned: " f"{command_result}" for plugin in cfg.plugins: result = plugin.post_command(command_name, result) diff --git a/tests/unit/test_browse_scrape_text.py b/tests/unit/test_browse_scrape_text.py index fea5ebfc..1a36e19b 100644 --- a/tests/unit/test_browse_scrape_text.py +++ b/tests/unit/test_browse_scrape_text.py @@ -9,16 +9,20 @@ Code Analysis Objective: The objective of the "scrape_text" function is to scrape the text content from -a given URL and return it as a string, after removing any unwanted HTML tags and scripts. +a given URL and return it as a string, after removing any unwanted HTML tags and + scripts. Inputs: - url: a string representing the URL of the webpage to be scraped. Flow: -1. Send a GET request to the given URL using the requests library and the user agent header from the config file. +1. Send a GET request to the given URL using the requests library and the user agent + header from the config file. 2. Check if the response contains an HTTP error. If it does, return an error message. -3. Use BeautifulSoup to parse the HTML content of the response and extract all script and style tags. -4. Get the text content of the remaining HTML using the get_text() method of BeautifulSoup. +3. Use BeautifulSoup to parse the HTML content of the response and extract all script + and style tags. +4. Get the text content of the remaining HTML using the get_text() method of + BeautifulSoup. 5. Split the text into lines and then into chunks, removing any extra whitespace. 6. Join the chunks into a single string with newline characters between them. 7. Return the cleaned text. @@ -27,9 +31,12 @@ Outputs: - A string representing the cleaned text content of the webpage. Additional aspects: -- The function uses the requests library and BeautifulSoup to handle the HTTP request and HTML parsing, respectively. -- The function removes script and style tags from the HTML to avoid including unwanted content in the text output. -- The function uses a generator expression to split the text into lines and chunks, which can improve performance for large amounts of text. +- The function uses the requests library and BeautifulSoup to handle the HTTP request + and HTML parsing, respectively. +- The function removes script and style tags from the HTML to avoid including unwanted + content in the text output. +- The function uses a generator expression to split the text into lines and chunks, + which can improve performance for large amounts of text. """ @@ -40,26 +47,33 @@ class TestScrapeText: expected_text = "This is some sample text" mock_response = mocker.Mock() mock_response.status_code = 200 - mock_response.text = f"

{expected_text}

" + mock_response.text = ( + "

" + f"{expected_text}

" + ) mocker.patch("requests.Session.get", return_value=mock_response) - # Call the function with a valid URL and assert that it returns the expected text + # Call the function with a valid URL and assert that it returns the + # expected text url = "http://www.example.com" assert scrape_text(url) == expected_text - # Tests that the function returns an error message when an invalid or unreachable url is provided. + # Tests that the function returns an error message when an invalid or unreachable + # url is provided. def test_invalid_url(self, mocker): # Mock the requests.get() method to raise an exception mocker.patch( "requests.Session.get", side_effect=requests.exceptions.RequestException ) - # Call the function with an invalid URL and assert that it returns an error message + # Call the function with an invalid URL and assert that it returns an error + # message url = "http://www.invalidurl.com" error_message = scrape_text(url) assert "Error:" in error_message - # Tests that the function returns an empty string when the html page contains no text to be scraped. + # Tests that the function returns an empty string when the html page contains no + # text to be scraped. def test_no_text(self, mocker): # Mock the requests.get() method to return a response with no text mock_response = mocker.Mock() @@ -71,7 +85,8 @@ class TestScrapeText: url = "http://www.example.com" assert scrape_text(url) == "" - # Tests that the function returns an error message when the response status code is an http error (>=400). + # Tests that the function returns an error message when the response status code is + # an http error (>=400). def test_http_error(self, mocker): # Mock the requests.get() method to return a response with a 404 status code mocker.patch("requests.Session.get", return_value=mocker.Mock(status_code=404)) From 23d3dafc5152b5c7437a484d38e29f282fbc75ad Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 23:18:29 -0500 Subject: [PATCH 27/60] Maybe fix tests, fix safe_path function. --- autogpt/workspace.py | 2 +- tests/test_commands.py | 4 ++-- tests/test_prompt_generator.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/autogpt/workspace.py b/autogpt/workspace.py index 964a94d1..e1e99082 100644 --- a/autogpt/workspace.py +++ b/autogpt/workspace.py @@ -35,7 +35,7 @@ def safe_path_join(base: Path, *paths: str | Path) -> Path: """ joined_path = base.joinpath(*paths).resolve() - if not joined_path.is_relative_to(base): + if not str(joined_path.absolute()).startswith(str(base.absolute())): raise ValueError( f"Attempted to access path '{joined_path}' outside of working directory '{base}'." ) diff --git a/tests/test_commands.py b/tests/test_commands.py index 49c09f11..a1fe0cb5 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -139,7 +139,7 @@ class TestCommandRegistry: def test_import_mock_commands_module(self): """Test that the registry can import a module with mock command plugins.""" registry = CommandRegistry() - mock_commands_module = "auto_gpt.tests.mocks.mock_commands" + mock_commands_module = "tests.mocks.mock_commands" registry.import_commands(mock_commands_module) @@ -155,7 +155,7 @@ class TestCommandRegistry: registry = CommandRegistry() # Create a temp command file - src = Path("/app/auto_gpt/tests/mocks/mock_commands.py") + src = Path("mocks/mock_commands.py") temp_commands_file = tmp_path / "mock_commands.py" shutil.copyfile(src, temp_commands_file) diff --git a/tests/test_prompt_generator.py b/tests/test_prompt_generator.py index 59ca7f95..1fa1754d 100644 --- a/tests/test_prompt_generator.py +++ b/tests/test_prompt_generator.py @@ -38,6 +38,7 @@ class TestPromptGenerator(TestCase): "label": command_label, "name": command_name, "args": args, + "function": None, } self.assertIn(command, self.generator.commands) From d394b032d79f1329ae6a4df3cfa56c22757a2db7 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 23:23:31 -0500 Subject: [PATCH 28/60] Fix test --- tests/test_commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_commands.py b/tests/test_commands.py index a1fe0cb5..4be41a90 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,4 +1,5 @@ import shutil +import os import sys from pathlib import Path @@ -155,7 +156,7 @@ class TestCommandRegistry: registry = CommandRegistry() # Create a temp command file - src = Path("mocks/mock_commands.py") + src = Path(os.getcwd()) / "tests/mocks/mock_commands.py" temp_commands_file = tmp_path / "mock_commands.py" shutil.copyfile(src, temp_commands_file) From 3715ebc7eb33ba0831deefcf4947a8b5bb295307 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 23:30:42 -0500 Subject: [PATCH 29/60] Add hooks for chat completion --- autogpt/llm_utils.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/autogpt/llm_utils.py b/autogpt/llm_utils.py index 701d622b..7aac703c 100644 --- a/autogpt/llm_utils.py +++ b/autogpt/llm_utils.py @@ -1,7 +1,6 @@ from __future__ import annotations import time -from ast import List import openai from colorama import Fore @@ -76,6 +75,20 @@ def create_chat_completion( + f"Creating chat completion with model {model}, temperature {temperature}," f" max_tokens {max_tokens}" + Fore.RESET ) + for plugin in CFG.plugins: + if plugin.can_handle_chat_completion( + messages=messages, + model=model, + temperature=temperature, + max_tokens=max_tokens, + ): + response = plugin.handle_chat_completion( + messages=messages, + model=model, + temperature=temperature, + max_tokens=max_tokens, + ) + return response for attempt in range(num_retries): backoff = 2 ** (attempt + 2) try: @@ -99,7 +112,7 @@ def create_chat_completion( if CFG.debug_mode: print( Fore.RED + "Error: ", - f"Reached rate limit, passing..." + Fore.RESET, + "Reached rate limit, passing..." + Fore.RESET, ) except APIError as e: if e.http_status == 502: From fbd4e06df5d185d05d1daed1a0ee2d9db2c9b947 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Sun, 16 Apr 2023 23:39:33 -0500 Subject: [PATCH 30/60] Add early abort functions. --- autogpt/agent/agent.py | 4 ++++ autogpt/agent/agent_manager.py | 12 ++++++++++++ autogpt/chat.py | 2 ++ autogpt/llm_utils.py | 2 ++ 4 files changed, 20 insertions(+) diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py index e65c7e61..7b1b5e15 100644 --- a/autogpt/agent/agent.py +++ b/autogpt/agent/agent.py @@ -180,6 +180,8 @@ class Agent: result = f"Human feedback: {user_input}" else: for plugin in cfg.plugins: + if not plugin.can_handle_pre_command(): + continue command_name, arguments = plugin.pre_command( command_name, arguments ) @@ -192,6 +194,8 @@ class Agent: result = f"Command {command_name} returned: " f"{command_result}" for plugin in cfg.plugins: + if not plugin.can_handle_post_command(): + continue result = plugin.post_command(command_name, result) if self.next_action_count > 0: self.next_action_count -= 1 diff --git a/autogpt/agent/agent_manager.py b/autogpt/agent/agent_manager.py index e1353e03..d2648150 100644 --- a/autogpt/agent/agent_manager.py +++ b/autogpt/agent/agent_manager.py @@ -31,6 +31,8 @@ class AgentManager(metaclass=Singleton): {"role": "user", "content": prompt}, ] for plugin in self.cfg.plugins: + if not plugin.can_handle_pre_instruction(): + continue plugin_messages = plugin.pre_instruction(messages) if plugin_messages: for plugin_message in plugin_messages: @@ -46,6 +48,8 @@ class AgentManager(metaclass=Singleton): plugins_reply = "" for i, plugin in enumerate(self.cfg.plugins): + if not plugin.can_handle_on_instruction(): + continue plugin_result = plugin.on_instruction(messages) if plugin_result: sep = "" if not i else "\n" @@ -61,6 +65,8 @@ class AgentManager(metaclass=Singleton): self.agents[key] = (task, messages, model) for plugin in self.cfg.plugins: + if not plugin.can_handle_post_instruction(): + continue agent_reply = plugin.post_instruction(agent_reply) return key, agent_reply @@ -81,6 +87,8 @@ class AgentManager(metaclass=Singleton): messages.append({"role": "user", "content": message}) for plugin in self.cfg.plugins: + if not plugin.can_handle_pre_instruction(): + continue plugin_messages = plugin.pre_instruction(messages) if plugin_messages: for plugin_message in plugin_messages: @@ -96,6 +104,8 @@ class AgentManager(metaclass=Singleton): plugins_reply = agent_reply for i, plugin in enumerate(self.cfg.plugins): + if not plugin.can_handle_on_instruction(): + continue plugin_result = plugin.on_instruction(messages) if plugin_result: sep = "" if not i else "\n" @@ -105,6 +115,8 @@ class AgentManager(metaclass=Singleton): messages.append({"role": "assistant", "content": plugins_reply}) for plugin in self.cfg.plugins: + if not plugin.can_handle_post_instruction(): + continue agent_reply = plugin.post_instruction(agent_reply) return agent_reply diff --git a/autogpt/chat.py b/autogpt/chat.py index 16693040..22fe636c 100644 --- a/autogpt/chat.py +++ b/autogpt/chat.py @@ -137,6 +137,8 @@ def chat_with_ai( plugin_count = len(cfg.plugins) for i, plugin in enumerate(cfg.plugins): + if not plugin.can_handle_on_planning(): + continue plugin_response = plugin.on_planning( agent.prompt_generator, current_context ) diff --git a/autogpt/llm_utils.py b/autogpt/llm_utils.py index 7aac703c..4fb0e1f5 100644 --- a/autogpt/llm_utils.py +++ b/autogpt/llm_utils.py @@ -131,6 +131,8 @@ def create_chat_completion( raise RuntimeError(f"Failed to get response after {num_retries} retries") resp = response.choices[0].message["content"] for plugin in CFG.plugins: + if not plugin.can_handle_on_response(): + continue resp = plugin.on_response(resp) return resp From 83861883565d64394ebbf88a57a533b6adf60a31 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Mon, 17 Apr 2023 00:49:51 -0500 Subject: [PATCH 31/60] Fix early abort --- autogpt/config/ai_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/autogpt/config/ai_config.py b/autogpt/config/ai_config.py index c9022773..af387f0b 100644 --- a/autogpt/config/ai_config.py +++ b/autogpt/config/ai_config.py @@ -126,6 +126,8 @@ class AIConfig: prompt_generator.role = self.ai_role prompt_generator.command_registry = self.command_registry for plugin in cfg.plugins: + if not plugin.can_handle_post_prompt(): + continue prompt_generator = plugin.post_prompt(prompt_generator) # Construct full prompt From fe85f079b08f919efecba1374073c9495ef6d5de Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Mon, 17 Apr 2023 01:09:17 -0500 Subject: [PATCH 32/60] Fix early abort --- autogpt/agent/agent.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py index 7b1b5e15..3a79760c 100644 --- a/autogpt/agent/agent.py +++ b/autogpt/agent/agent.py @@ -94,6 +94,8 @@ class Agent: assistant_reply_json = fix_json_using_multiple_techniques(assistant_reply) for plugin in cfg.plugins: + if not plugin.can_handle_post_planning(): + continue assistant_reply_json = plugin.post_planning(self, assistant_reply_json) # Print Assistant thoughts From 08ad320d196687b32a01e0c8052c082806e4c070 Mon Sep 17 00:00:00 2001 From: Evgeny Vakhteev Date: Mon, 17 Apr 2023 09:33:01 -0700 Subject: [PATCH 33/60] moving load plugins into plugins from main, adding tests --- autogpt/__main__.py | 22 +----- autogpt/config/config.py | 1 + autogpt/plugins.py | 63 ++++++++++++++---- .../Auto-GPT-Plugin-Test-master.zip | Bin 0 -> 14879 bytes tests/unit/test_plugins.py | 32 +++++++++ 5 files changed, 85 insertions(+), 33 deletions(-) create mode 100644 tests/unit/data/test_plugins/Auto-GPT-Plugin-Test-master.zip create mode 100644 tests/unit/test_plugins.py diff --git a/autogpt/__main__.py b/autogpt/__main__.py index 5fc9a1ea..f995fb12 100644 --- a/autogpt/__main__.py +++ b/autogpt/__main__.py @@ -24,27 +24,7 @@ def main() -> None: check_openai_api_key() parse_arguments() logger.set_level(logging.DEBUG if cfg.debug_mode else logging.INFO) - plugins_found = load_plugins(Path(os.getcwd()) / "plugins") - loaded_plugins = [] - for plugin in plugins_found: - if plugin.__name__ in cfg.plugins_blacklist: - continue - if plugin.__name__ in cfg.plugins_whitelist: - loaded_plugins.append(plugin()) - else: - ack = input( - f"WARNNG Plugin {plugin.__name__} found. But not in the" - " whitelist... Load? (y/n): " - ) - if ack.lower() == "y": - loaded_plugins.append(plugin()) - - if loaded_plugins: - print(f"\nPlugins found: {len(loaded_plugins)}\n" "--------------------") - for plugin in loaded_plugins: - print(f"{plugin._name}: {plugin._version} - {plugin._description}") - - cfg.set_plugins(loaded_plugins) + cfg.set_plugins(load_plugins(cfg)) # Create a CommandRegistry instance and scan default folder command_registry = CommandRegistry() command_registry.import_commands("scripts.ai_functions") diff --git a/autogpt/config/config.py b/autogpt/config/config.py index 46ab95d8..66a23086 100644 --- a/autogpt/config/config.py +++ b/autogpt/config/config.py @@ -107,6 +107,7 @@ class Config(metaclass=Singleton): # Initialize the OpenAI API client openai.api_key = self.openai_api_key + self.plugins_dir = os.getenv("PLUGINS_DIR", "plugins") self.plugins = [] self.plugins_whitelist = [] self.plugins_blacklist = [] diff --git a/autogpt/plugins.py b/autogpt/plugins.py index 7b843a6a..18680cba 100644 --- a/autogpt/plugins.py +++ b/autogpt/plugins.py @@ -1,11 +1,15 @@ """Handles loading of plugins.""" - -from ast import Module +import os import zipfile +from glob import glob from pathlib import Path from zipimport import zipimporter from typing import List, Optional, Tuple +from abstract_singleton import AbstractSingleton + +from autogpt.config import Config + def inspect_zip_for_module(zip_path: str, debug: bool = False) -> Optional[str]: """ @@ -29,32 +33,66 @@ def inspect_zip_for_module(zip_path: str, debug: bool = False) -> Optional[str]: return None -def scan_plugins(plugins_path: Path, debug: bool = False) -> List[Tuple[str, Path]]: +def scan_plugins(plugins_path: str, debug: bool = False) -> List[Tuple[str, Path]]: """Scan the plugins directory for plugins. Args: - plugins_path (Path): Path to the plugins directory. + plugins_path (str): Path to the plugins directory. + debug (bool, optional): Enable debug logging. Defaults to False. Returns: - List[Path]: List of plugins. + List[Tuple[str, Path]]: List of plugins. """ plugins = [] - for plugin in plugins_path.glob("*.zip"): + plugins_path_path = Path(plugins_path) + + for plugin in plugins_path_path.glob("*.zip"): if module := inspect_zip_for_module(str(plugin), debug): plugins.append((module, plugin)) return plugins -def load_plugins(plugins_path: Path, debug: bool = False) -> List[Module]: +def blacklist_whitelist_check(plugins: List[AbstractSingleton], cfg: Config): + """Check if the plugin is in the whitelist or blacklist. + + Args: + plugins (List[Tuple[str, Path]]): List of plugins. + cfg (Config): Config object. + + Returns: + List[Tuple[str, Path]]: List of plugins. + """ + loaded_plugins = [] + for plugin in plugins: + if plugin.__name__ in cfg.plugins_blacklist: + continue + if plugin.__name__ in cfg.plugins_whitelist: + loaded_plugins.append(plugin()) + else: + ack = input( + f"WARNNG Plugin {plugin.__name__} found. But not in the" + " whitelist... Load? (y/n): " + ) + if ack.lower() == "y": + loaded_plugins.append(plugin()) + + if loaded_plugins: + print(f"\nPlugins found: {len(loaded_plugins)}\n" "--------------------") + for plugin in loaded_plugins: + print(f"{plugin._name}: {plugin._version} - {plugin._description}") + return loaded_plugins + + +def load_plugins(cfg: Config = Config(), debug: bool = False) -> List[object]: """Load plugins from the plugins directory. Args: - plugins_path (Path): Path to the plugins directory. - + cfg (Config): Config instance inluding plugins config + debug (bool, optional): Enable debug logging. Defaults to False. Returns: - List[Path]: List of plugins. + List[AbstractSingleton]: List of plugins initialized. """ - plugins = scan_plugins(plugins_path) + plugins = scan_plugins(cfg.plugins_dir) plugin_modules = [] for module, plugin in plugins: plugin = Path(plugin) @@ -70,4 +108,5 @@ def load_plugins(plugins_path: Path, debug: bool = False) -> List[Module]: a_keys = dir(a_module) if "_abc_impl" in a_keys and a_module.__name__ != "AutoGPTPluginTemplate": plugin_modules.append(a_module) - return plugin_modules + loaded_plugin_modules = blacklist_whitelist_check(plugin_modules, cfg) + return loaded_plugin_modules diff --git a/tests/unit/data/test_plugins/Auto-GPT-Plugin-Test-master.zip b/tests/unit/data/test_plugins/Auto-GPT-Plugin-Test-master.zip new file mode 100644 index 0000000000000000000000000000000000000000..f0db121e29dfa7a6b8fe77bc2f34c4b6f27e43de GIT binary patch literal 14879 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 UWM?&FHfCmKG3Msr<~B3_e=-UAnE(I) literal 0 HcmV?d00001 diff --git a/tests/unit/test_plugins.py b/tests/unit/test_plugins.py new file mode 100644 index 00000000..a1d9d5e7 --- /dev/null +++ b/tests/unit/test_plugins.py @@ -0,0 +1,32 @@ +import pytest +from pathlib import Path +from zipfile import ZipFile +from autogpt.plugins import inspect_zip_for_module, scan_plugins, load_plugins +from autogpt.config import Config + +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" + + +@pytest.fixture +def config_with_plugins(): + cfg = Config() + cfg.plugins_dir = PLUGINS_TEST_DIR + return cfg + + +def test_inspect_zip_for_module(): + result = inspect_zip_for_module(str(PLUGINS_TEST_DIR + PLUGIN_TEST_ZIP_FILE)) + assert result == PLUGIN_TEST_INIT_PY + +def test_scan_plugins(): + result = scan_plugins(PLUGINS_TEST_DIR, debug=True) + assert len(result) == 1 + assert result[0][0] == PLUGIN_TEST_INIT_PY + + +def test_load_plugins_blacklisted(config_with_plugins): + config_with_plugins.plugins_blacklist = ['AbstractSingleton'] + result = load_plugins(cfg=config_with_plugins) + assert len(result) == 0 From 239aa3aa0239b2ce1b13d08a170454b42db0c44d Mon Sep 17 00:00:00 2001 From: Taylor Beeston Date: Mon, 17 Apr 2023 12:38:46 -0700 Subject: [PATCH 34/60] :art: Bring in plugin_template This would ideally be a shared package --- autogpt/plugin_template.py | 255 +++++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 2 files changed, 256 insertions(+) create mode 100644 autogpt/plugin_template.py diff --git a/autogpt/plugin_template.py b/autogpt/plugin_template.py new file mode 100644 index 00000000..90e9fa32 --- /dev/null +++ b/autogpt/plugin_template.py @@ -0,0 +1,255 @@ +"""This is a template for Auto-GPT plugins.""" + +# TODO: Move to shared package + +import abc +from typing import Any, Dict, List, Optional, Tuple, TypedDict +from abstract_singleton import AbstractSingleton, Singleton + +from prompts.generator import PromptGenerator + + +class Message(TypedDict): + role: str + content: str + + +class AutoGPTPluginTemplate(AbstractSingleton, metaclass=Singleton): + """ + This is a template for Auto-GPT plugins. + """ + + def __init__(self): + super().__init__() + self._name = "Auto-GPT-Plugin-Template" + self._version = "0.1.0" + self._description = "This is a template for Auto-GPT plugins." + + @abc.abstractmethod + def can_handle_on_response(self) -> bool: + """This method is called to check that the plugin can + handle the on_response method. + + Returns: + bool: True if the plugin can handle the on_response method.""" + return False + + @abc.abstractmethod + def on_response(self, response: str, *args, **kwargs) -> str: + """This method is called when a response is received from the model.""" + pass + + @abc.abstractmethod + def can_handle_post_prompt(self) -> bool: + """This method is called to check that the plugin can + handle the post_prompt method. + + Returns: + bool: True if the plugin can handle the post_prompt method.""" + return False + + @abc.abstractmethod + def post_prompt(self, prompt: PromptGenerator) -> PromptGenerator: + """This method is called just after the generate_prompt is called, + but actually before the prompt is generated. + + Args: + prompt (PromptGenerator): The prompt generator. + + Returns: + PromptGenerator: The prompt generator. + """ + pass + + @abc.abstractmethod + def can_handle_on_planning(self) -> bool: + """This method is called to check that the plugin can + handle the on_planning method. + + Returns: + bool: True if the plugin can handle the on_planning method.""" + return False + + @abc.abstractmethod + def on_planning( + self, prompt: PromptGenerator, messages: List[Message] + ) -> Optional[str]: + """This method is called before the planning chat completeion is done. + + Args: + prompt (PromptGenerator): The prompt generator. + messages (List[str]): The list of messages. + """ + pass + + @abc.abstractmethod + def can_handle_post_planning(self) -> bool: + """This method is called to check that the plugin can + handle the post_planning method. + + Returns: + bool: True if the plugin can handle the post_planning method.""" + return False + + @abc.abstractmethod + def post_planning(self, response: str) -> str: + """This method is called after the planning chat completeion is done. + + Args: + response (str): The response. + + Returns: + str: The resulting response. + """ + pass + + @abc.abstractmethod + def can_handle_pre_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the pre_instruction method. + + Returns: + bool: True if the plugin can handle the pre_instruction method.""" + return False + + @abc.abstractmethod + def pre_instruction(self, messages: List[Message]) -> List[Message]: + """This method is called before the instruction chat is done. + + Args: + messages (List[Message]): The list of context messages. + + Returns: + List[Message]: The resulting list of messages. + """ + pass + + @abc.abstractmethod + def can_handle_on_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the on_instruction method. + + Returns: + bool: True if the plugin can handle the on_instruction method.""" + return False + + @abc.abstractmethod + def on_instruction(self, messages: List[Message]) -> Optional[str]: + """This method is called when the instruction chat is done. + + Args: + messages (List[Message]): The list of context messages. + + Returns: + Optional[str]: The resulting message. + """ + pass + + @abc.abstractmethod + def can_handle_post_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the post_instruction method. + + Returns: + bool: True if the plugin can handle the post_instruction method.""" + return False + + @abc.abstractmethod + def post_instruction(self, response: str) -> str: + """This method is called after the instruction chat is done. + + Args: + response (str): The response. + + Returns: + str: The resulting response. + """ + pass + + @abc.abstractmethod + def can_handle_pre_command(self) -> bool: + """This method is called to check that the plugin can + handle the pre_command method. + + Returns: + bool: True if the plugin can handle the pre_command method.""" + return False + + @abc.abstractmethod + def pre_command( + self, command_name: str, arguments: Dict[str, Any] + ) -> Tuple[str, Dict[str, Any]]: + """This method is called before the command is executed. + + Args: + command_name (str): The command name. + arguments (Dict[str, Any]): The arguments. + + Returns: + Tuple[str, Dict[str, Any]]: The command name and the arguments. + """ + pass + + @abc.abstractmethod + def can_handle_post_command(self) -> bool: + """This method is called to check that the plugin can + handle the post_command method. + + Returns: + bool: True if the plugin can handle the post_command method.""" + return False + + @abc.abstractmethod + def post_command(self, command_name: str, response: str) -> str: + """This method is called after the command is executed. + + Args: + command_name (str): The command name. + response (str): The response. + + Returns: + str: The resulting response. + """ + pass + + @abc.abstractmethod + def can_handle_chat_completion( + self, + messages: List[Message], + model: Optional[str], + temperature: float, + max_tokens: Optional[int], + ) -> bool: + """This method is called to check that the plugin can + handle the chat_completion method. + + Args: + messages (List[Message]): The messages. + model (str): The model name. + temperature (float): The temperature. + max_tokens (int): The max tokens. + + Returns: + bool: True if the plugin can handle the chat_completion method.""" + return False + + @abc.abstractmethod + def handle_chat_completion( + self, + messages: List[Message], + model: Optional[str], + temperature: float, + max_tokens: Optional[int], + ) -> str: + """This method is called when the chat completion is done. + + Args: + messages (List[Message]): The messages. + model (str): The model name. + temperature (float): The temperature. + max_tokens (int): The max tokens. + + Returns: + str: The resulting response. + """ + pass diff --git a/requirements.txt b/requirements.txt index 843b66bf..5e8f1000 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,6 +29,7 @@ black sourcery isort gitpython==3.1.31 +abstract-singleton # Testing dependencies pytest From dea5000a014ea69d352400638e2f26dd77eacb20 Mon Sep 17 00:00:00 2001 From: Taylor Beeston Date: Mon, 17 Apr 2023 12:40:46 -0700 Subject: [PATCH 35/60] :bug: Fix pre_instruction --- autogpt/agent/agent_manager.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/autogpt/agent/agent_manager.py b/autogpt/agent/agent_manager.py index d2648150..286b8ebd 100644 --- a/autogpt/agent/agent_manager.py +++ b/autogpt/agent/agent_manager.py @@ -1,9 +1,12 @@ """Agent manager for managing GPT agents""" from __future__ import annotations +from typing import List from autogpt.config.config import Config, Singleton from autogpt.llm_utils import create_chat_completion +from plugin_template import Message + class AgentManager(metaclass=Singleton): """Agent manager for managing GPT agents""" @@ -27,7 +30,7 @@ class AgentManager(metaclass=Singleton): Returns: The key of the new agent """ - messages = [ + messages: List[Message] = [ {"role": "user", "content": prompt}, ] for plugin in self.cfg.plugins: @@ -36,7 +39,7 @@ class AgentManager(metaclass=Singleton): plugin_messages = plugin.pre_instruction(messages) if plugin_messages: for plugin_message in plugin_messages: - messages.append({"role": "system", "content": plugin_message}) + messages.append(plugin_message) # Start GPT instance agent_reply = create_chat_completion( @@ -92,7 +95,7 @@ class AgentManager(metaclass=Singleton): plugin_messages = plugin.pre_instruction(messages) if plugin_messages: for plugin_message in plugin_messages: - messages.append({"role": "system", "content": plugin_message}) + messages.append(plugin_message) # Start GPT instance agent_reply = create_chat_completion( From d23ada30d72fe44467770d82389243cc3f7cb254 Mon Sep 17 00:00:00 2001 From: Taylor Beeston Date: Mon, 17 Apr 2023 12:41:17 -0700 Subject: [PATCH 36/60] :bug: Fix on_planning --- autogpt/chat.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/autogpt/chat.py b/autogpt/chat.py index 22fe636c..e7354fc1 100644 --- a/autogpt/chat.py +++ b/autogpt/chat.py @@ -7,10 +7,12 @@ from autogpt.config import Config from autogpt.llm_utils import create_chat_completion from autogpt.logs import logger +from plugin_template import Message + cfg = Config() -def create_chat_message(role, content): +def create_chat_message(role, content) -> Message: """ Create a chat message with the given role and content. @@ -145,7 +147,7 @@ def chat_with_ai( if not plugin_response or plugin_response == "": continue tokens_to_add = token_counter.count_message_tokens( - [plugin_response], model + [create_chat_message("system", plugin_response)], model ) if current_tokens_used + tokens_to_add > send_token_limit: if cfg.debug_mode: From f7840490793223b859311778e7993451a827803a Mon Sep 17 00:00:00 2001 From: Taylor Beeston Date: Mon, 17 Apr 2023 12:41:34 -0700 Subject: [PATCH 37/60] :label: Type plugins field in config --- autogpt/config/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/autogpt/config/config.py b/autogpt/config/config.py index c12eed2e..2aaf879a 100644 --- a/autogpt/config/config.py +++ b/autogpt/config/config.py @@ -1,7 +1,9 @@ """Configuration class to store the state of bools for different scripts access.""" import os +from typing import List import openai +from plugin_template import AutoGPTPluginTemplate import yaml from colorama import Fore from dotenv import load_dotenv @@ -107,7 +109,7 @@ class Config(metaclass=Singleton): # Initialize the OpenAI API client openai.api_key = self.openai_api_key - self.plugins = [] + self.plugins: List[AutoGPTPluginTemplate] = [] self.plugins_whitelist = [] self.plugins_blacklist = [] From ea67b6772c461b1e1083b236d4a5668f0a0c3d50 Mon Sep 17 00:00:00 2001 From: Taylor Beeston Date: Mon, 17 Apr 2023 12:42:17 -0700 Subject: [PATCH 38/60] :bug: Minor type fixes --- autogpt/llm_utils.py | 14 ++++++++------ autogpt/plugins.py | 6 +++++- autogpt/token_counter.py | 5 ++++- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/autogpt/llm_utils.py b/autogpt/llm_utils.py index 4fb0e1f5..bc68ba93 100644 --- a/autogpt/llm_utils.py +++ b/autogpt/llm_utils.py @@ -1,12 +1,14 @@ from __future__ import annotations import time +from typing import List, Optional import openai from colorama import Fore from openai.error import APIError, RateLimitError from autogpt.config import Config +from plugin_template import Message CFG = Config() @@ -35,8 +37,8 @@ def call_ai_function( # For each arg, if any are None, convert to "None": args = [str(arg) if arg is not None else "None" for arg in args] # parse args to comma separated string - args = ", ".join(args) - messages = [ + args: str = ", ".join(args) + messages: List[Message] = [ { "role": "system", "content": f"You are now the following python function: ```# {description}" @@ -51,15 +53,15 @@ def call_ai_function( # Overly simple abstraction until we create something better # simple retry mechanism when getting a rate error or a bad gateway def create_chat_completion( - messages: list, # type: ignore - model: str | None = None, + messages: List[Message], # type: ignore + model: Optional[str] = None, temperature: float = CFG.temperature, - max_tokens: int | None = None, + max_tokens: Optional[int] = None, ) -> str: """Create a chat completion using the OpenAI API Args: - messages (list[dict[str, str]]): The messages to send to the chat completion + messages (List[Message]): The messages to send to the chat completion model (str, optional): The model to use. Defaults to None. temperature (float, optional): The temperature to use. Defaults to 0.9. max_tokens (int, optional): The max tokens to use. Defaults to None. diff --git a/autogpt/plugins.py b/autogpt/plugins.py index a00b989e..b4b2ac78 100644 --- a/autogpt/plugins.py +++ b/autogpt/plugins.py @@ -6,6 +6,8 @@ from pathlib import Path from typing import List, Optional, Tuple from zipimport import zipimporter +from plugin_template import AutoGPTPluginTemplate + def inspect_zip_for_module(zip_path: str, debug: bool = False) -> Optional[str]: """ @@ -45,7 +47,9 @@ def scan_plugins(plugins_path: Path, debug: bool = False) -> List[Tuple[str, Pat return plugins -def load_plugins(plugins_path: Path, debug: bool = False) -> List[Module]: +def load_plugins( + plugins_path: Path, debug: bool = False +) -> List[AutoGPTPluginTemplate]: """Load plugins from the plugins directory. Args: diff --git a/autogpt/token_counter.py b/autogpt/token_counter.py index 338fe6be..8cf4c369 100644 --- a/autogpt/token_counter.py +++ b/autogpt/token_counter.py @@ -1,13 +1,16 @@ """Functions for counting the number of tokens in a message or string.""" from __future__ import annotations +from typing import List import tiktoken from autogpt.logs import logger +from plugin_template import Message + def count_message_tokens( - messages: list[dict[str, str]], model: str = "gpt-3.5-turbo-0301" + messages: List[Message], model: str = "gpt-3.5-turbo-0301" ) -> int: """ Returns the number of tokens used by a list of messages. From 9705f60dd3ab3aa8abae64abf7ec68d77dabf4d4 Mon Sep 17 00:00:00 2001 From: Sourcery AI <> Date: Mon, 17 Apr 2023 19:44:54 +0000 Subject: [PATCH 39/60] 'Refactored by Sourcery' --- autogpt/agent/agent_manager.py | 20 +++++++------------- autogpt/llm_utils.py | 30 ++++++++++-------------------- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/autogpt/agent/agent_manager.py b/autogpt/agent/agent_manager.py index 286b8ebd..dc57811e 100644 --- a/autogpt/agent/agent_manager.py +++ b/autogpt/agent/agent_manager.py @@ -36,11 +36,8 @@ class AgentManager(metaclass=Singleton): for plugin in self.cfg.plugins: if not plugin.can_handle_pre_instruction(): continue - plugin_messages = plugin.pre_instruction(messages) - if plugin_messages: - for plugin_message in plugin_messages: - messages.append(plugin_message) - + if plugin_messages := plugin.pre_instruction(messages): + messages.extend(iter(plugin_messages)) # Start GPT instance agent_reply = create_chat_completion( model=model, @@ -53,9 +50,8 @@ class AgentManager(metaclass=Singleton): for i, plugin in enumerate(self.cfg.plugins): if not plugin.can_handle_on_instruction(): continue - plugin_result = plugin.on_instruction(messages) - if plugin_result: - sep = "" if not i else "\n" + if plugin_result := plugin.on_instruction(messages): + sep = "\n" if i else "" plugins_reply = f"{plugins_reply}{sep}{plugin_result}" if plugins_reply and plugins_reply != "": @@ -92,8 +88,7 @@ class AgentManager(metaclass=Singleton): for plugin in self.cfg.plugins: if not plugin.can_handle_pre_instruction(): continue - plugin_messages = plugin.pre_instruction(messages) - if plugin_messages: + if plugin_messages := plugin.pre_instruction(messages): for plugin_message in plugin_messages: messages.append(plugin_message) @@ -109,9 +104,8 @@ class AgentManager(metaclass=Singleton): for i, plugin in enumerate(self.cfg.plugins): if not plugin.can_handle_on_instruction(): continue - plugin_result = plugin.on_instruction(messages) - if plugin_result: - sep = "" if not i else "\n" + if plugin_result := plugin.on_instruction(messages): + sep = "\n" if i else "" plugins_reply = f"{plugins_reply}{sep}{plugin_result}" # Update full message history if plugins_reply and plugins_reply != "": diff --git a/autogpt/llm_utils.py b/autogpt/llm_utils.py index bc68ba93..85e0fbf7 100644 --- a/autogpt/llm_utils.py +++ b/autogpt/llm_utils.py @@ -69,13 +69,10 @@ def create_chat_completion( Returns: str: The response from the chat completion """ - response = None num_retries = 10 if CFG.debug_mode: print( - Fore.GREEN - + f"Creating chat completion with model {model}, temperature {temperature}," - f" max_tokens {max_tokens}" + Fore.RESET + f"{Fore.GREEN}Creating chat completion with model {model}, temperature {temperature}, max_tokens {max_tokens}{Fore.RESET}" ) for plugin in CFG.plugins: if plugin.can_handle_chat_completion( @@ -84,13 +81,13 @@ def create_chat_completion( temperature=temperature, max_tokens=max_tokens, ): - response = plugin.handle_chat_completion( + return plugin.handle_chat_completion( messages=messages, model=model, temperature=temperature, max_tokens=max_tokens, ) - return response + response = None for attempt in range(num_retries): backoff = 2 ** (attempt + 2) try: @@ -112,21 +109,16 @@ def create_chat_completion( break except RateLimitError: if CFG.debug_mode: - print( - Fore.RED + "Error: ", - "Reached rate limit, passing..." + Fore.RESET, - ) + print(f"{Fore.RED}Error: ", f"Reached rate limit, passing...{Fore.RESET}") except APIError as e: - if e.http_status == 502: - pass - else: + if e.http_status != 502: raise if attempt == num_retries - 1: raise if CFG.debug_mode: print( - Fore.RED + "Error: ", - f"API Bad gateway. Waiting {backoff} seconds..." + Fore.RESET, + f"{Fore.RED}Error: ", + f"API Bad gateway. Waiting {backoff} seconds...{Fore.RESET}", ) time.sleep(backoff) if response is None: @@ -159,15 +151,13 @@ def create_embedding_with_ada(text) -> list: except RateLimitError: pass except APIError as e: - if e.http_status == 502: - pass - else: + if e.http_status != 502: raise if attempt == num_retries - 1: raise if CFG.debug_mode: print( - Fore.RED + "Error: ", - f"API Bad gateway. Waiting {backoff} seconds..." + Fore.RESET, + f"{Fore.RED}Error: ", + f"API Bad gateway. Waiting {backoff} seconds...{Fore.RESET}", ) time.sleep(backoff) From 7f4e38844feadbd9999016e66e8437185d087ddc Mon Sep 17 00:00:00 2001 From: Evgeny Vakhteev Date: Mon, 17 Apr 2023 14:57:55 -0700 Subject: [PATCH 40/60] adding openai plugin loader --- .gitignore | 2 + autogpt/__main__.py | 2 +- autogpt/config/config.py | 1 + autogpt/plugins.py | 145 ++++++++++++++++++++++++++++++++++--- tests/unit/test_plugins.py | 11 ++- 5 files changed, 145 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 3209297c..2a7c630b 100644 --- a/.gitignore +++ b/.gitignore @@ -157,3 +157,5 @@ vicuna-* # mac .DS_Store + +openai/ diff --git a/autogpt/__main__.py b/autogpt/__main__.py index f995fb12..d694fd59 100644 --- a/autogpt/__main__.py +++ b/autogpt/__main__.py @@ -24,7 +24,7 @@ def main() -> None: check_openai_api_key() parse_arguments() logger.set_level(logging.DEBUG if cfg.debug_mode else logging.INFO) - cfg.set_plugins(load_plugins(cfg)) + cfg.set_plugins(load_plugins(cfg, cfg.debug_mode)) # Create a CommandRegistry instance and scan default folder command_registry = CommandRegistry() command_registry.import_commands("scripts.ai_functions") diff --git a/autogpt/config/config.py b/autogpt/config/config.py index 66a23086..94a36201 100644 --- a/autogpt/config/config.py +++ b/autogpt/config/config.py @@ -109,6 +109,7 @@ class Config(metaclass=Singleton): self.plugins_dir = os.getenv("PLUGINS_DIR", "plugins") self.plugins = [] + self.plugins_openai = [] self.plugins_whitelist = [] self.plugins_blacklist = [] diff --git a/autogpt/plugins.py b/autogpt/plugins.py index 18680cba..daa56f6a 100644 --- a/autogpt/plugins.py +++ b/autogpt/plugins.py @@ -1,12 +1,18 @@ """Handles loading of plugins.""" +import importlib +import json +import mimetypes import os import zipfile -from glob import glob from pathlib import Path +from urllib.parse import urlparse from zipimport import zipimporter from typing import List, Optional, Tuple +import openapi_python_client +import requests from abstract_singleton import AbstractSingleton +from openapi_python_client.cli import _process_config, Config as OpenAPIConfig from autogpt.config import Config @@ -31,24 +37,146 @@ def inspect_zip_for_module(zip_path: str, debug: bool = False) -> Optional[str]: if debug: print(f"Module '__init__.py' not found in the zipfile @ {zip_path}.") return None +def write_dict_to_json_file(data: dict, file_path: str): + """ + Write a dictionary to a JSON file. + Args: + data (dict): Dictionary to write. + file_path (str): Path to the file. + """ + with open(file_path, 'w') as file: + json.dump(data, file, indent=4) -def scan_plugins(plugins_path: str, debug: bool = False) -> List[Tuple[str, Path]]: +def fetch_openai_plugins_manifest_and_spec(cfg: Config) -> dict: + """ + Fetch the manifest for a list of OpenAI plugins. + Args: + urls (List): List of URLs to fetch. + Returns: + dict: per url dictionary of manifest and spec. + """ + # TODO add directory scan + manifests = {} + for url in cfg.plugins_openai: + openai_plugin_client_dir = f"{cfg.plugins_dir}/openai/{urlparse(url).netloc}" + create_directory_if_not_exists(openai_plugin_client_dir) + if not os.path.exists(f'{openai_plugin_client_dir}/ai-plugin.json'): + try: + response = requests.get(f"{url}/.well-known/ai-plugin.json") + if response.status_code == 200: + manifest = response.json() + if manifest["schema_version"] != "v1": + print(f"Unsupported manifest version: {manifest['schem_version']} for {url}") + continue + if manifest["api"]["type"] != "openapi": + print(f"Unsupported API type: {manifest['api']['type']} for {url}") + continue + write_dict_to_json_file(manifest, f'{openai_plugin_client_dir}/ai-plugin.json') + else: + print(f"Failed to fetch manifest for {url}: {response.status_code}") + except requests.exceptions.RequestException as e: + print(f"Error while requesting manifest from {url}: {e}") + else: + print(f"Manifest for {url} already exists") + manifest = json.load(open(f'{openai_plugin_client_dir}/ai-plugin.json')) + if not os.path.exists(f'{openai_plugin_client_dir}/openapi.json'): + openapi_spec = openapi_python_client._get_document(url=manifest["api"]["url"], path=None, timeout=5) + write_dict_to_json_file(openapi_spec, f'{openai_plugin_client_dir}/openapi.json') + else: + print(f"OpenAPI spec for {url} already exists") + openapi_spec = json.load(open(f'{openai_plugin_client_dir}/openapi.json')) + manifests[url] = { + 'manifest': manifest, + 'openapi_spec': openapi_spec + } + return manifests + + +def create_directory_if_not_exists(directory_path: str) -> bool: + """ + Create a directory if it does not exist. + Args: + directory_path (str): Path to the directory. + Returns: + bool: True if the directory was created, else False. + """ + if not os.path.exists(directory_path): + try: + os.makedirs(directory_path) + print(f"Created directory: {directory_path}") + return True + except OSError as e: + print(f"Error creating directory {directory_path}: {e}") + return False + else: + print(f"Directory {directory_path} already exists") + return True + + +def initialize_openai_plugins(manifests_specs: dict, cfg: Config, debug: bool = False) -> dict: + """ + Initialize OpenAI plugins. + Args: + manifests_specs (dict): per url dictionary of manifest and spec. + cfg (Config): Config instance including plugins config + debug (bool, optional): Enable debug logging. Defaults to False. + Returns: + dict: per url dictionary of manifest, spec and client. + """ + openai_plugins_dir = f'{cfg.plugins_dir}/openai' + if create_directory_if_not_exists(openai_plugins_dir): + for url, manifest_spec in manifests_specs.items(): + openai_plugin_client_dir = f'{openai_plugins_dir}/{urlparse(url).hostname}' + _meta_option = openapi_python_client.MetaType.SETUP, + _config = OpenAPIConfig(**{ + 'project_name_override': 'client', + 'package_name_override': 'client', + }) + prev_cwd = Path.cwd() + os.chdir(openai_plugin_client_dir) + Path('ai-plugin.json') + if not os.path.exists('client'): + client_results = openapi_python_client.create_new_client( + url=manifest_spec['manifest']['api']['url'], + path=None, + meta=_meta_option, + config=_config, + ) + if client_results: + print(f"Error creating OpenAPI client: {client_results[0].header} \n" + f" details: {client_results[0].detail}") + continue + spec = importlib.util.spec_from_file_location('client', 'client/client/client.py') + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + client = module.Client(base_url=url) + os.chdir(prev_cwd) + manifest_spec['client'] = client + return manifests_specs + + +def scan_plugins(cfg: Config, debug: bool = False) -> List[Tuple[str, Path]]: """Scan the plugins directory for plugins. Args: - plugins_path (str): Path to the plugins directory. + cfg (Config): Config instance including plugins config debug (bool, optional): Enable debug logging. Defaults to False. Returns: List[Tuple[str, Path]]: List of plugins. """ plugins = [] - plugins_path_path = Path(plugins_path) - + # Generic plugins + plugins_path_path = Path(cfg.plugins_dir) for plugin in plugins_path_path.glob("*.zip"): if module := inspect_zip_for_module(str(plugin), debug): plugins.append((module, plugin)) + # OpenAI plugins + if cfg.plugins_openai: + manifests_specs = fetch_openai_plugins_manifest_and_spec(cfg) + if manifests_specs.keys(): + manifests_specs_clients = initialize_openai_plugins(manifests_specs, cfg, debug) return plugins @@ -87,12 +215,12 @@ def load_plugins(cfg: Config = Config(), debug: bool = False) -> List[object]: """Load plugins from the plugins directory. Args: - cfg (Config): Config instance inluding plugins config + cfg (Config): Config instance including plugins config debug (bool, optional): Enable debug logging. Defaults to False. Returns: List[AbstractSingleton]: List of plugins initialized. """ - plugins = scan_plugins(cfg.plugins_dir) + plugins = scan_plugins(cfg) plugin_modules = [] for module, plugin in plugins: plugin = Path(plugin) @@ -108,5 +236,4 @@ def load_plugins(cfg: Config = Config(), debug: bool = False) -> List[object]: a_keys = dir(a_module) if "_abc_impl" in a_keys and a_module.__name__ != "AutoGPTPluginTemplate": plugin_modules.append(a_module) - loaded_plugin_modules = blacklist_whitelist_check(plugin_modules, cfg) - return loaded_plugin_modules + return blacklist_whitelist_check(plugin_modules, cfg) diff --git a/tests/unit/test_plugins.py b/tests/unit/test_plugins.py index a1d9d5e7..d9bca97c 100644 --- a/tests/unit/test_plugins.py +++ b/tests/unit/test_plugins.py @@ -1,10 +1,8 @@ import pytest -from pathlib import Path -from zipfile import ZipFile from autogpt.plugins import inspect_zip_for_module, scan_plugins, load_plugins from autogpt.config import Config -PLUGINS_TEST_DIR = "tests/unit/data/test_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" @@ -13,15 +11,16 @@ PLUGIN_TEST_INIT_PY = "Auto-GPT-Plugin-Test-master/src/auto_gpt_plugin_template/ def config_with_plugins(): cfg = Config() cfg.plugins_dir = PLUGINS_TEST_DIR + cfg.plugins_openai = ['https://weathergpt.vercel.app/'] return cfg def test_inspect_zip_for_module(): - result = inspect_zip_for_module(str(PLUGINS_TEST_DIR + PLUGIN_TEST_ZIP_FILE)) + result = inspect_zip_for_module(str(f'{PLUGINS_TEST_DIR}/{PLUGIN_TEST_ZIP_FILE}')) assert result == PLUGIN_TEST_INIT_PY -def test_scan_plugins(): - result = scan_plugins(PLUGINS_TEST_DIR, debug=True) +def test_scan_plugins(config_with_plugins): + result = scan_plugins(config_with_plugins, debug=True) assert len(result) == 1 assert result[0][0] == PLUGIN_TEST_INIT_PY From 9ed5e0f1fc2a294a66059ad8c5aeb9238dfdd7a9 Mon Sep 17 00:00:00 2001 From: Evgeny Vakhteev Date: Mon, 17 Apr 2023 17:13:53 -0700 Subject: [PATCH 41/60] adding plugin interface instantiation --- autogpt/plugins.py | 243 +++++++++++++++++- requirements.txt | 6 + .../Auto-GPT-Plugin-Test-master.zip | Bin 14879 -> 15284 bytes 3 files changed, 241 insertions(+), 8 deletions(-) diff --git a/autogpt/plugins.py b/autogpt/plugins.py index daa56f6a..aca98311 100644 --- a/autogpt/plugins.py +++ b/autogpt/plugins.py @@ -1,21 +1,218 @@ """Handles loading of plugins.""" import importlib import json -import mimetypes import os import zipfile -from pathlib import Path -from urllib.parse import urlparse -from zipimport import zipimporter -from typing import List, Optional, Tuple - import openapi_python_client import requests -from abstract_singleton import AbstractSingleton -from openapi_python_client.cli import _process_config, Config as OpenAPIConfig +import abc + +from pathlib import Path +from typing import TypeVar +from urllib.parse import urlparse +from zipimport import zipimporter +from openapi_python_client.cli import Config as OpenAPIConfig +from typing import Any, Dict, List, Optional, Tuple, TypedDict +from abstract_singleton import AbstractSingleton, Singleton + from autogpt.config import Config +PromptGenerator = TypeVar("PromptGenerator") + + +class Message(TypedDict): + role: str + content: str + + +class BaseOpenAIPlugin(): + """ + This is a template for Auto-GPT plugins. + """ + + def __init__(self, manifests_specs_clients: dict): + # super().__init__() + self._name = manifests_specs_clients["manifest"]["name_for_model"] + self._version = manifests_specs_clients["manifest"]["schema_version"] + self._description = manifests_specs_clients["manifest"]["description_for_model"] + self.client = manifests_specs_clients["client"] + self.manifest = manifests_specs_clients["manifest"] + self.openapi_spec = manifests_specs_clients["openapi_spec"] + + def can_handle_on_response(self) -> bool: + """This method is called to check that the plugin can + handle the on_response method. + Returns: + bool: True if the plugin can handle the on_response method.""" + return False + + def on_response(self, response: str, *args, **kwargs) -> str: + """This method is called when a response is received from the model.""" + pass + + def can_handle_post_prompt(self) -> bool: + """This method is called to check that the plugin can + handle the post_prompt method. + Returns: + bool: True if the plugin can handle the post_prompt method.""" + return False + + def post_prompt(self, prompt: PromptGenerator) -> PromptGenerator: + """This method is called just after the generate_prompt is called, + but actually before the prompt is generated. + Args: + prompt (PromptGenerator): The prompt generator. + Returns: + PromptGenerator: The prompt generator. + """ + pass + + def can_handle_on_planning(self) -> bool: + """This method is called to check that the plugin can + handle the on_planning method. + Returns: + bool: True if the plugin can handle the on_planning method.""" + return False + + def on_planning( + self, prompt: PromptGenerator, messages: List[Message] + ) -> Optional[str]: + """This method is called before the planning chat completion is done. + Args: + prompt (PromptGenerator): The prompt generator. + messages (List[str]): The list of messages. + """ + pass + + def can_handle_post_planning(self) -> bool: + """This method is called to check that the plugin can + handle the post_planning method. + Returns: + bool: True if the plugin can handle the post_planning method.""" + return False + + def post_planning(self, response: str) -> str: + """This method is called after the planning chat completion is done. + Args: + response (str): The response. + Returns: + str: The resulting response. + """ + pass + + def can_handle_pre_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the pre_instruction method. + Returns: + bool: True if the plugin can handle the pre_instruction method.""" + return False + + def pre_instruction(self, messages: List[Message]) -> List[Message]: + """This method is called before the instruction chat is done. + Args: + messages (List[Message]): The list of context messages. + Returns: + List[Message]: The resulting list of messages. + """ + pass + + def can_handle_on_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the on_instruction method. + Returns: + bool: True if the plugin can handle the on_instruction method.""" + return False + + def on_instruction(self, messages: List[Message]) -> Optional[str]: + """This method is called when the instruction chat is done. + Args: + messages (List[Message]): The list of context messages. + Returns: + Optional[str]: The resulting message. + """ + pass + + def can_handle_post_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the post_instruction method. + Returns: + bool: True if the plugin can handle the post_instruction method.""" + return False + + def post_instruction(self, response: str) -> str: + """This method is called after the instruction chat is done. + Args: + response (str): The response. + Returns: + str: The resulting response. + """ + pass + + def can_handle_pre_command(self) -> bool: + """This method is called to check that the plugin can + handle the pre_command method. + Returns: + bool: True if the plugin can handle the pre_command method.""" + return False + + def pre_command( + self, command_name: str, arguments: Dict[str, Any] + ) -> Tuple[str, Dict[str, Any]]: + """This method is called before the command is executed. + Args: + command_name (str): The command name. + arguments (Dict[str, Any]): The arguments. + Returns: + Tuple[str, Dict[str, Any]]: The command name and the arguments. + """ + pass + + def can_handle_post_command(self) -> bool: + """This method is called to check that the plugin can + handle the post_command method. + Returns: + bool: True if the plugin can handle the post_command method.""" + return False + + def post_command(self, command_name: str, response: str) -> str: + """This method is called after the command is executed. + Args: + command_name (str): The command name. + response (str): The response. + Returns: + str: The resulting response. + """ + pass + + def can_handle_chat_completion( + self, messages: Dict[Any, Any], model: str, temperature: float, max_tokens: int + ) -> bool: + """This method is called to check that the plugin can + handle the chat_completion method. + Args: + messages (List[Message]): The messages. + model (str): The model name. + temperature (float): The temperature. + max_tokens (int): The max tokens. + Returns: + bool: True if the plugin can handle the chat_completion method.""" + return False + + def handle_chat_completion( + self, messages: List[Message], model: str, temperature: float, max_tokens: int + ) -> str: + """This method is called when the chat completion is done. + Args: + messages (List[Message]): The messages. + model (str): The model name. + temperature (float): The temperature. + max_tokens (int): The max tokens. + Returns: + str: The resulting response. + """ + pass + def inspect_zip_for_module(zip_path: str, debug: bool = False) -> Optional[str]: """ @@ -37,6 +234,8 @@ def inspect_zip_for_module(zip_path: str, debug: bool = False) -> Optional[str]: if debug: print(f"Module '__init__.py' not found in the zipfile @ {zip_path}.") return None + + def write_dict_to_json_file(data: dict, file_path: str): """ Write a dictionary to a JSON file. @@ -156,6 +355,29 @@ def initialize_openai_plugins(manifests_specs: dict, cfg: Config, debug: bool = return manifests_specs +def instantiate_openai_plugin_clients(manifests_specs_clients: dict, cfg: Config, debug: bool = False) -> dict: + """ + Instantiates BaseOpenAIPluginClient 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 = {} + for url, manifest_spec_client in manifests_specs_clients.items(): + plugins[url] = BaseOpenAIPluginClient( + manifest=manifest_spec_client['manifest'], + openapi_spec=manifest_spec_client['openapi_spec'], + client=manifest_spec_client['client'], + cfg=cfg, + debug=debug + ) + return plugins + + def scan_plugins(cfg: Config, debug: bool = False) -> List[Tuple[str, Path]]: """Scan the plugins directory for plugins. @@ -177,6 +399,11 @@ def scan_plugins(cfg: Config, debug: bool = False) -> List[Tuple[str, Path]]: manifests_specs = fetch_openai_plugins_manifest_and_spec(cfg) 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 = BaseOpenAIPlugin(openai_plugin_meta) + plugins.append((plugin, url)) + + return plugins diff --git a/requirements.txt b/requirements.txt index 1cdedec2..6583d65a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,3 +28,9 @@ gitpython==3.1.31 pytest pytest-mock tweepy + + +# OpenAI plugins import +openapi-python-client==0.13.4 +abstract-singleton +auto-vicuna diff --git a/tests/unit/data/test_plugins/Auto-GPT-Plugin-Test-master.zip b/tests/unit/data/test_plugins/Auto-GPT-Plugin-Test-master.zip index f0db121e29dfa7a6b8fe77bc2f34c4b6f27e43de..7a6421af1c23f354c0915f8b729343dc8de32e5f 100644 GIT binary patch delta 4791 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�hm@8I_1!1c!)mx}s-iplU920KpJ>RV5?uOUi}(!(RWvTq~$ z?yO03c`iC9e|EhTY4ONY*Fyza4inJLxOd_yU+sbCD`KBzOok+LwRdaGteHV&a@h&| zcBK0ir-4nlthl}xp36)tZ4g53+l$w$7cqBJ56V%&fsksLM;4+ zkX5KDY$(n`>aXC{GrUrtlU@}U4m94)o9i?(I|~JG*BjZe9>hsrY6HF&lknG+m#pFs zxzG4zxa}($Q^Sv%uNn6d@EFQ1PjwEvFiRRPKf(5j;oU8ri;Jl*zkh`Z2!oAC@-{ zE@0Q9O5X(C#=S%geevf#yz2iG$m@nI1Ar_T^{~o5XmiKQibuvrYj>|L@A^0?W;c`C z$EKjf6`p<{r-K(+*HGj9Xsg17kf$ddUB89Jnx!OO&2~54Jj{=MX<;28-&XTj>vMSO zGlZa6UHm224+lgl%2MW4i_vU3i&~D?Eo?8r%VfKH8g(Jn-YYAH+VY!n$;zB^um9zc$@Kt8ZfCwI(Wy*i30cU={~knu5Z8uR2h^R<3c57XMZ zN!^8$p6Cc?zwDl2P!_@a`;+qvkV_NH?<;nG%rx7=>I2?(sHef#i&t-nJej{WAw-Jb zc{|K<-oYlI5^2o+L5;t=U~@38I`&(n4k2bs5PUs8;9$uB+nO=!rctLGwe zvCS=2MS28FJZLNAwkBum&AaMCicJV2mJwkDYXawEk>X19YKG{e^9%L(cX1qbD5>?` z6SeeX@{=88_MrM_tPM zrm#E4g>QSdroV^YkgB>=R*g}$rAfCcRl3P6YR9OSubLtE-*iPsk#Fj!4wbOTjcV3^ zEK^C4dxhR(m!FNzp=6 zTCM#;%W4S&y=ABfos(E495!^H<*+kB;}4#CuGkK^E7{Qvlh#~sqGQyJ&u90 zwA3hiygOlL*r(uTtSZ`QO&{LcmP&*lICn6h%6;;3Nq zr@HPFXdiLjbBZ-;Ak{wu?I+WFD~8;yEeyN@qa83nb>GsE)e!~?s?ZSIs_aI87dD7e z!T Date: Mon, 17 Apr 2023 18:42:42 -0700 Subject: [PATCH 42/60] separating OpenAI Plugin base class --- autogpt/models/base_open_ai_plugin.py | 200 +++++++++++++++++++++++ autogpt/plugins.py | 221 +------------------------- 2 files changed, 208 insertions(+), 213 deletions(-) create mode 100644 autogpt/models/base_open_ai_plugin.py diff --git a/autogpt/models/base_open_ai_plugin.py b/autogpt/models/base_open_ai_plugin.py new file mode 100644 index 00000000..37a64660 --- /dev/null +++ b/autogpt/models/base_open_ai_plugin.py @@ -0,0 +1,200 @@ +"""Handles loading of plugins.""" +import zipfile +from typing import Any, Dict, List, Optional, Tuple, TypedDict +from typing import TypeVar + +PromptGenerator = TypeVar("PromptGenerator") + + +class Message(TypedDict): + role: str + content: str + + +class BaseOpenAIPluginClient(): + """ + This is a template for Auto-GPT plugins. + """ + + def __init__(self, manifests_specs_clients: dict): + # super().__init__() + self._name = manifests_specs_clients["manifest"]["name_for_model"] + self._version = manifests_specs_clients["manifest"]["schema_version"] + self._description = manifests_specs_clients["manifest"]["description_for_model"] + self.client = manifests_specs_clients["client"] + self.manifest = manifests_specs_clients["manifest"] + self.openapi_spec = manifests_specs_clients["openapi_spec"] + + def can_handle_on_response(self) -> bool: + """This method is called to check that the plugin can + handle the on_response method. + Returns: + bool: True if the plugin can handle the on_response method.""" + return False + + def on_response(self, response: str, *args, **kwargs) -> str: + """This method is called when a response is received from the model.""" + pass + + def can_handle_post_prompt(self) -> bool: + """This method is called to check that the plugin can + handle the post_prompt method. + Returns: + bool: True if the plugin can handle the post_prompt method.""" + return False + + def post_prompt(self, prompt: PromptGenerator) -> PromptGenerator: + """This method is called just after the generate_prompt is called, + but actually before the prompt is generated. + Args: + prompt (PromptGenerator): The prompt generator. + Returns: + PromptGenerator: The prompt generator. + """ + pass + + def can_handle_on_planning(self) -> bool: + """This method is called to check that the plugin can + handle the on_planning method. + Returns: + bool: True if the plugin can handle the on_planning method.""" + return False + + def on_planning( + self, prompt: PromptGenerator, messages: List[Message] + ) -> Optional[str]: + """This method is called before the planning chat completion is done. + Args: + prompt (PromptGenerator): The prompt generator. + messages (List[str]): The list of messages. + """ + pass + + def can_handle_post_planning(self) -> bool: + """This method is called to check that the plugin can + handle the post_planning method. + Returns: + bool: True if the plugin can handle the post_planning method.""" + return False + + def post_planning(self, response: str) -> str: + """This method is called after the planning chat completion is done. + Args: + response (str): The response. + Returns: + str: The resulting response. + """ + pass + + def can_handle_pre_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the pre_instruction method. + Returns: + bool: True if the plugin can handle the pre_instruction method.""" + return False + + def pre_instruction(self, messages: List[Message]) -> List[Message]: + """This method is called before the instruction chat is done. + Args: + messages (List[Message]): The list of context messages. + Returns: + List[Message]: The resulting list of messages. + """ + pass + + def can_handle_on_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the on_instruction method. + Returns: + bool: True if the plugin can handle the on_instruction method.""" + return False + + def on_instruction(self, messages: List[Message]) -> Optional[str]: + """This method is called when the instruction chat is done. + Args: + messages (List[Message]): The list of context messages. + Returns: + Optional[str]: The resulting message. + """ + pass + + def can_handle_post_instruction(self) -> bool: + """This method is called to check that the plugin can + handle the post_instruction method. + Returns: + bool: True if the plugin can handle the post_instruction method.""" + return False + + def post_instruction(self, response: str) -> str: + """This method is called after the instruction chat is done. + Args: + response (str): The response. + Returns: + str: The resulting response. + """ + pass + + def can_handle_pre_command(self) -> bool: + """This method is called to check that the plugin can + handle the pre_command method. + Returns: + bool: True if the plugin can handle the pre_command method.""" + return False + + def pre_command( + self, command_name: str, arguments: Dict[str, Any] + ) -> Tuple[str, Dict[str, Any]]: + """This method is called before the command is executed. + Args: + command_name (str): The command name. + arguments (Dict[str, Any]): The arguments. + Returns: + Tuple[str, Dict[str, Any]]: The command name and the arguments. + """ + pass + + def can_handle_post_command(self) -> bool: + """This method is called to check that the plugin can + handle the post_command method. + Returns: + bool: True if the plugin can handle the post_command method.""" + return False + + def post_command(self, command_name: str, response: str) -> str: + """This method is called after the command is executed. + Args: + command_name (str): The command name. + response (str): The response. + Returns: + str: The resulting response. + """ + pass + + def can_handle_chat_completion( + self, messages: Dict[Any, Any], model: str, temperature: float, max_tokens: int + ) -> bool: + """This method is called to check that the plugin can + handle the chat_completion method. + Args: + messages (List[Message]): The messages. + model (str): The model name. + temperature (float): The temperature. + max_tokens (int): The max tokens. + Returns: + bool: True if the plugin can handle the chat_completion method.""" + return False + + def handle_chat_completion( + self, messages: List[Message], model: str, temperature: float, max_tokens: int + ) -> str: + """This method is called when the chat completion is done. + Args: + messages (List[Message]): The messages. + model (str): The model name. + temperature (float): The temperature. + max_tokens (int): The max tokens. + Returns: + str: The resulting response. + """ + pass + diff --git a/autogpt/plugins.py b/autogpt/plugins.py index aca98311..bc822717 100644 --- a/autogpt/plugins.py +++ b/autogpt/plugins.py @@ -3,215 +3,18 @@ import importlib import json import os import zipfile -import openapi_python_client -import requests -import abc - from pathlib import Path -from typing import TypeVar +from typing import List, Tuple, Optional from urllib.parse import urlparse from zipimport import zipimporter -from openapi_python_client.cli import Config as OpenAPIConfig -from typing import Any, Dict, List, Optional, Tuple, TypedDict -from abstract_singleton import AbstractSingleton, Singleton +import openapi_python_client +import requests +from abstract_singleton import AbstractSingleton +from openapi_python_client.cli import Config as OpenAPIConfig from autogpt.config import Config - -PromptGenerator = TypeVar("PromptGenerator") - - -class Message(TypedDict): - role: str - content: str - - -class BaseOpenAIPlugin(): - """ - This is a template for Auto-GPT plugins. - """ - - def __init__(self, manifests_specs_clients: dict): - # super().__init__() - self._name = manifests_specs_clients["manifest"]["name_for_model"] - self._version = manifests_specs_clients["manifest"]["schema_version"] - self._description = manifests_specs_clients["manifest"]["description_for_model"] - self.client = manifests_specs_clients["client"] - self.manifest = manifests_specs_clients["manifest"] - self.openapi_spec = manifests_specs_clients["openapi_spec"] - - def can_handle_on_response(self) -> bool: - """This method is called to check that the plugin can - handle the on_response method. - Returns: - bool: True if the plugin can handle the on_response method.""" - return False - - def on_response(self, response: str, *args, **kwargs) -> str: - """This method is called when a response is received from the model.""" - pass - - def can_handle_post_prompt(self) -> bool: - """This method is called to check that the plugin can - handle the post_prompt method. - Returns: - bool: True if the plugin can handle the post_prompt method.""" - return False - - def post_prompt(self, prompt: PromptGenerator) -> PromptGenerator: - """This method is called just after the generate_prompt is called, - but actually before the prompt is generated. - Args: - prompt (PromptGenerator): The prompt generator. - Returns: - PromptGenerator: The prompt generator. - """ - pass - - def can_handle_on_planning(self) -> bool: - """This method is called to check that the plugin can - handle the on_planning method. - Returns: - bool: True if the plugin can handle the on_planning method.""" - return False - - def on_planning( - self, prompt: PromptGenerator, messages: List[Message] - ) -> Optional[str]: - """This method is called before the planning chat completion is done. - Args: - prompt (PromptGenerator): The prompt generator. - messages (List[str]): The list of messages. - """ - pass - - def can_handle_post_planning(self) -> bool: - """This method is called to check that the plugin can - handle the post_planning method. - Returns: - bool: True if the plugin can handle the post_planning method.""" - return False - - def post_planning(self, response: str) -> str: - """This method is called after the planning chat completion is done. - Args: - response (str): The response. - Returns: - str: The resulting response. - """ - pass - - def can_handle_pre_instruction(self) -> bool: - """This method is called to check that the plugin can - handle the pre_instruction method. - Returns: - bool: True if the plugin can handle the pre_instruction method.""" - return False - - def pre_instruction(self, messages: List[Message]) -> List[Message]: - """This method is called before the instruction chat is done. - Args: - messages (List[Message]): The list of context messages. - Returns: - List[Message]: The resulting list of messages. - """ - pass - - def can_handle_on_instruction(self) -> bool: - """This method is called to check that the plugin can - handle the on_instruction method. - Returns: - bool: True if the plugin can handle the on_instruction method.""" - return False - - def on_instruction(self, messages: List[Message]) -> Optional[str]: - """This method is called when the instruction chat is done. - Args: - messages (List[Message]): The list of context messages. - Returns: - Optional[str]: The resulting message. - """ - pass - - def can_handle_post_instruction(self) -> bool: - """This method is called to check that the plugin can - handle the post_instruction method. - Returns: - bool: True if the plugin can handle the post_instruction method.""" - return False - - def post_instruction(self, response: str) -> str: - """This method is called after the instruction chat is done. - Args: - response (str): The response. - Returns: - str: The resulting response. - """ - pass - - def can_handle_pre_command(self) -> bool: - """This method is called to check that the plugin can - handle the pre_command method. - Returns: - bool: True if the plugin can handle the pre_command method.""" - return False - - def pre_command( - self, command_name: str, arguments: Dict[str, Any] - ) -> Tuple[str, Dict[str, Any]]: - """This method is called before the command is executed. - Args: - command_name (str): The command name. - arguments (Dict[str, Any]): The arguments. - Returns: - Tuple[str, Dict[str, Any]]: The command name and the arguments. - """ - pass - - def can_handle_post_command(self) -> bool: - """This method is called to check that the plugin can - handle the post_command method. - Returns: - bool: True if the plugin can handle the post_command method.""" - return False - - def post_command(self, command_name: str, response: str) -> str: - """This method is called after the command is executed. - Args: - command_name (str): The command name. - response (str): The response. - Returns: - str: The resulting response. - """ - pass - - def can_handle_chat_completion( - self, messages: Dict[Any, Any], model: str, temperature: float, max_tokens: int - ) -> bool: - """This method is called to check that the plugin can - handle the chat_completion method. - Args: - messages (List[Message]): The messages. - model (str): The model name. - temperature (float): The temperature. - max_tokens (int): The max tokens. - Returns: - bool: True if the plugin can handle the chat_completion method.""" - return False - - def handle_chat_completion( - self, messages: List[Message], model: str, temperature: float, max_tokens: int - ) -> str: - """This method is called when the chat completion is done. - Args: - messages (List[Message]): The messages. - model (str): The model name. - temperature (float): The temperature. - max_tokens (int): The max tokens. - Returns: - str: The resulting response. - """ - pass +from autogpt.models.base_open_ai_plugin import BaseOpenAIPluginClient def inspect_zip_for_module(zip_path: str, debug: bool = False) -> Optional[str]: @@ -368,13 +171,7 @@ def instantiate_openai_plugin_clients(manifests_specs_clients: dict, cfg: Config """ plugins = {} for url, manifest_spec_client in manifests_specs_clients.items(): - plugins[url] = BaseOpenAIPluginClient( - manifest=manifest_spec_client['manifest'], - openapi_spec=manifest_spec_client['openapi_spec'], - client=manifest_spec_client['client'], - cfg=cfg, - debug=debug - ) + plugins[url] = BaseOpenAIPluginClient(manifest_spec_client) return plugins @@ -400,10 +197,8 @@ 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 = BaseOpenAIPlugin(openai_plugin_meta) + plugin = BaseOpenAIPluginClient(openai_plugin_meta) plugins.append((plugin, url)) - - return plugins From 9fd80a86608db4ee3ccb90cc22f700f1a96b1569 Mon Sep 17 00:00:00 2001 From: Evgeny Vakhteev Date: Mon, 17 Apr 2023 20:51:27 -0700 Subject: [PATCH 43/60] tests, model --- autogpt/config/ai_config.py | 2 +- autogpt/models/base_open_ai_plugin.py | 3 +- autogpt/plugins.py | 10 +- .../unit/models/test_base_open_api_plugin.py | 61 +++++ tests/unit/test_plugins_gpt_generated.py | 222 ++++++++++++++++++ 5 files changed, 290 insertions(+), 8 deletions(-) create mode 100644 tests/unit/models/test_base_open_api_plugin.py create mode 100644 tests/unit/test_plugins_gpt_generated.py 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") From b84de4f7f89b95f176ebd0b390c60198acfa8bf9 Mon Sep 17 00:00:00 2001 From: Taylor Beeston Date: Mon, 17 Apr 2023 22:10:40 -0700 Subject: [PATCH 44/60] :recycle: Use AutoGPT template package for the plugin type --- autogpt/agent/agent_manager.py | 3 +- autogpt/chat.py | 3 +- autogpt/config/config.py | 2 +- autogpt/llm_utils.py | 6 +- autogpt/plugin_template.py | 255 --------------------------------- autogpt/plugins.py | 2 +- autogpt/token_counter.py | 3 +- autogpt/types/openai.py | 9 ++ requirements.txt | 1 + 9 files changed, 19 insertions(+), 265 deletions(-) delete mode 100644 autogpt/plugin_template.py create mode 100644 autogpt/types/openai.py diff --git a/autogpt/agent/agent_manager.py b/autogpt/agent/agent_manager.py index dc57811e..9f123eaa 100644 --- a/autogpt/agent/agent_manager.py +++ b/autogpt/agent/agent_manager.py @@ -4,8 +4,7 @@ from typing import List from autogpt.config.config import Config, Singleton from autogpt.llm_utils import create_chat_completion - -from plugin_template import Message +from autogpt.types.openai import Message class AgentManager(metaclass=Singleton): diff --git a/autogpt/chat.py b/autogpt/chat.py index e7354fc1..f9fc9471 100644 --- a/autogpt/chat.py +++ b/autogpt/chat.py @@ -6,8 +6,7 @@ from autogpt import token_counter from autogpt.config import Config from autogpt.llm_utils import create_chat_completion from autogpt.logs import logger - -from plugin_template import Message +from autogpt.types.openai import Message cfg = Config() diff --git a/autogpt/config/config.py b/autogpt/config/config.py index 2aaf879a..f93bf17a 100644 --- a/autogpt/config/config.py +++ b/autogpt/config/config.py @@ -3,7 +3,7 @@ import os from typing import List import openai -from plugin_template import AutoGPTPluginTemplate +from auto_gpt_plugin_template import AutoGPTPluginTemplate import yaml from colorama import Fore from dotenv import load_dotenv diff --git a/autogpt/llm_utils.py b/autogpt/llm_utils.py index 85e0fbf7..a6d87c30 100644 --- a/autogpt/llm_utils.py +++ b/autogpt/llm_utils.py @@ -8,7 +8,7 @@ from colorama import Fore from openai.error import APIError, RateLimitError from autogpt.config import Config -from plugin_template import Message +from autogpt.types.openai import Message CFG = Config() @@ -109,7 +109,9 @@ def create_chat_completion( break except RateLimitError: if CFG.debug_mode: - print(f"{Fore.RED}Error: ", f"Reached rate limit, passing...{Fore.RESET}") + print( + f"{Fore.RED}Error: ", f"Reached rate limit, passing...{Fore.RESET}" + ) except APIError as e: if e.http_status != 502: raise diff --git a/autogpt/plugin_template.py b/autogpt/plugin_template.py deleted file mode 100644 index 90e9fa32..00000000 --- a/autogpt/plugin_template.py +++ /dev/null @@ -1,255 +0,0 @@ -"""This is a template for Auto-GPT plugins.""" - -# TODO: Move to shared package - -import abc -from typing import Any, Dict, List, Optional, Tuple, TypedDict -from abstract_singleton import AbstractSingleton, Singleton - -from prompts.generator import PromptGenerator - - -class Message(TypedDict): - role: str - content: str - - -class AutoGPTPluginTemplate(AbstractSingleton, metaclass=Singleton): - """ - This is a template for Auto-GPT plugins. - """ - - def __init__(self): - super().__init__() - self._name = "Auto-GPT-Plugin-Template" - self._version = "0.1.0" - self._description = "This is a template for Auto-GPT plugins." - - @abc.abstractmethod - def can_handle_on_response(self) -> bool: - """This method is called to check that the plugin can - handle the on_response method. - - Returns: - bool: True if the plugin can handle the on_response method.""" - return False - - @abc.abstractmethod - def on_response(self, response: str, *args, **kwargs) -> str: - """This method is called when a response is received from the model.""" - pass - - @abc.abstractmethod - def can_handle_post_prompt(self) -> bool: - """This method is called to check that the plugin can - handle the post_prompt method. - - Returns: - bool: True if the plugin can handle the post_prompt method.""" - return False - - @abc.abstractmethod - def post_prompt(self, prompt: PromptGenerator) -> PromptGenerator: - """This method is called just after the generate_prompt is called, - but actually before the prompt is generated. - - Args: - prompt (PromptGenerator): The prompt generator. - - Returns: - PromptGenerator: The prompt generator. - """ - pass - - @abc.abstractmethod - def can_handle_on_planning(self) -> bool: - """This method is called to check that the plugin can - handle the on_planning method. - - Returns: - bool: True if the plugin can handle the on_planning method.""" - return False - - @abc.abstractmethod - def on_planning( - self, prompt: PromptGenerator, messages: List[Message] - ) -> Optional[str]: - """This method is called before the planning chat completeion is done. - - Args: - prompt (PromptGenerator): The prompt generator. - messages (List[str]): The list of messages. - """ - pass - - @abc.abstractmethod - def can_handle_post_planning(self) -> bool: - """This method is called to check that the plugin can - handle the post_planning method. - - Returns: - bool: True if the plugin can handle the post_planning method.""" - return False - - @abc.abstractmethod - def post_planning(self, response: str) -> str: - """This method is called after the planning chat completeion is done. - - Args: - response (str): The response. - - Returns: - str: The resulting response. - """ - pass - - @abc.abstractmethod - def can_handle_pre_instruction(self) -> bool: - """This method is called to check that the plugin can - handle the pre_instruction method. - - Returns: - bool: True if the plugin can handle the pre_instruction method.""" - return False - - @abc.abstractmethod - def pre_instruction(self, messages: List[Message]) -> List[Message]: - """This method is called before the instruction chat is done. - - Args: - messages (List[Message]): The list of context messages. - - Returns: - List[Message]: The resulting list of messages. - """ - pass - - @abc.abstractmethod - def can_handle_on_instruction(self) -> bool: - """This method is called to check that the plugin can - handle the on_instruction method. - - Returns: - bool: True if the plugin can handle the on_instruction method.""" - return False - - @abc.abstractmethod - def on_instruction(self, messages: List[Message]) -> Optional[str]: - """This method is called when the instruction chat is done. - - Args: - messages (List[Message]): The list of context messages. - - Returns: - Optional[str]: The resulting message. - """ - pass - - @abc.abstractmethod - def can_handle_post_instruction(self) -> bool: - """This method is called to check that the plugin can - handle the post_instruction method. - - Returns: - bool: True if the plugin can handle the post_instruction method.""" - return False - - @abc.abstractmethod - def post_instruction(self, response: str) -> str: - """This method is called after the instruction chat is done. - - Args: - response (str): The response. - - Returns: - str: The resulting response. - """ - pass - - @abc.abstractmethod - def can_handle_pre_command(self) -> bool: - """This method is called to check that the plugin can - handle the pre_command method. - - Returns: - bool: True if the plugin can handle the pre_command method.""" - return False - - @abc.abstractmethod - def pre_command( - self, command_name: str, arguments: Dict[str, Any] - ) -> Tuple[str, Dict[str, Any]]: - """This method is called before the command is executed. - - Args: - command_name (str): The command name. - arguments (Dict[str, Any]): The arguments. - - Returns: - Tuple[str, Dict[str, Any]]: The command name and the arguments. - """ - pass - - @abc.abstractmethod - def can_handle_post_command(self) -> bool: - """This method is called to check that the plugin can - handle the post_command method. - - Returns: - bool: True if the plugin can handle the post_command method.""" - return False - - @abc.abstractmethod - def post_command(self, command_name: str, response: str) -> str: - """This method is called after the command is executed. - - Args: - command_name (str): The command name. - response (str): The response. - - Returns: - str: The resulting response. - """ - pass - - @abc.abstractmethod - def can_handle_chat_completion( - self, - messages: List[Message], - model: Optional[str], - temperature: float, - max_tokens: Optional[int], - ) -> bool: - """This method is called to check that the plugin can - handle the chat_completion method. - - Args: - messages (List[Message]): The messages. - model (str): The model name. - temperature (float): The temperature. - max_tokens (int): The max tokens. - - Returns: - bool: True if the plugin can handle the chat_completion method.""" - return False - - @abc.abstractmethod - def handle_chat_completion( - self, - messages: List[Message], - model: Optional[str], - temperature: float, - max_tokens: Optional[int], - ) -> str: - """This method is called when the chat completion is done. - - Args: - messages (List[Message]): The messages. - model (str): The model name. - temperature (float): The temperature. - max_tokens (int): The max tokens. - - Returns: - str: The resulting response. - """ - pass diff --git a/autogpt/plugins.py b/autogpt/plugins.py index b4b2ac78..a4d9c17c 100644 --- a/autogpt/plugins.py +++ b/autogpt/plugins.py @@ -6,7 +6,7 @@ from pathlib import Path from typing import List, Optional, Tuple from zipimport import zipimporter -from plugin_template import AutoGPTPluginTemplate +from auto_gpt_plugin_template import AutoGPTPluginTemplate def inspect_zip_for_module(zip_path: str, debug: bool = False) -> Optional[str]: diff --git a/autogpt/token_counter.py b/autogpt/token_counter.py index 8cf4c369..b1e59d86 100644 --- a/autogpt/token_counter.py +++ b/autogpt/token_counter.py @@ -5,8 +5,7 @@ from typing import List import tiktoken from autogpt.logs import logger - -from plugin_template import Message +from autogpt.types.openai import Message def count_message_tokens( diff --git a/autogpt/types/openai.py b/autogpt/types/openai.py new file mode 100644 index 00000000..2af85785 --- /dev/null +++ b/autogpt/types/openai.py @@ -0,0 +1,9 @@ +"""Type helpers for working with the OpenAI library""" +from typing import TypedDict + + +class Message(TypedDict): + """OpenAI Message object containing a role and the message content""" + + role: str + content: str diff --git a/requirements.txt b/requirements.txt index 5e8f1000..86d24b5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,6 +30,7 @@ sourcery isort gitpython==3.1.31 abstract-singleton +auto-gpt-plugin-template # Testing dependencies pytest From 894026cdd4094e94429ecbc022f52df45f773626 Mon Sep 17 00:00:00 2001 From: Evgeny Vakhteev Date: Tue, 18 Apr 2023 12:52:09 -0700 Subject: [PATCH 45/60] reshaping code and fixing tests --- autogpt/__main__.py | 4 +- autogpt/models/base_open_ai_plugin.py | 13 +- autogpt/plugins.py | 91 +++---- requirements.txt | 4 +- .../Auto-GPT-Plugin-Test-master.zip | Bin 15284 -> 14927 bytes tests/unit/test_plugins.py | 81 ++++++- tests/unit/test_plugins_gpt_generated.py | 222 ------------------ 7 files changed, 117 insertions(+), 298 deletions(-) delete mode 100644 tests/unit/test_plugins_gpt_generated.py diff --git a/autogpt/__main__.py b/autogpt/__main__.py index d694fd59..f8d20487 100644 --- a/autogpt/__main__.py +++ b/autogpt/__main__.py @@ -11,7 +11,7 @@ from autogpt.logs import logger from autogpt.memory import get_memory from autogpt.prompts.prompt import construct_main_ai_config -from autogpt.plugins import load_plugins +from autogpt.plugins import scan_plugins # Load environment variables from .env file @@ -24,7 +24,7 @@ def main() -> None: check_openai_api_key() parse_arguments() logger.set_level(logging.DEBUG if cfg.debug_mode else logging.INFO) - cfg.set_plugins(load_plugins(cfg, cfg.debug_mode)) + cfg.set_plugins(scan_plugins(cfg, cfg.debug_mode)) # Create a CommandRegistry instance and scan default folder command_registry = CommandRegistry() command_registry.import_commands("scripts.ai_functions") diff --git a/autogpt/models/base_open_ai_plugin.py b/autogpt/models/base_open_ai_plugin.py index 3aafff84..fafd3932 100644 --- a/autogpt/models/base_open_ai_plugin.py +++ b/autogpt/models/base_open_ai_plugin.py @@ -2,6 +2,8 @@ from typing import Any, Dict, List, Optional, Tuple, TypedDict from typing import TypeVar +from auto_gpt_plugin_template import AutoGPTPluginTemplate + PromptGenerator = TypeVar("PromptGenerator") @@ -10,9 +12,9 @@ class Message(TypedDict): content: str -class BaseOpenAIPlugin: +class BaseOpenAIPlugin(AutoGPTPluginTemplate): """ - This is a template for Auto-GPT plugins. + This is a BaseOpenAIPlugin class for generating Auto-GPT plugins. """ def __init__(self, manifests_specs_clients: dict): @@ -20,9 +22,9 @@ class BaseOpenAIPlugin: self._name = manifests_specs_clients["manifest"]["name_for_model"] self._version = manifests_specs_clients["manifest"]["schema_version"] self._description = manifests_specs_clients["manifest"]["description_for_model"] - self.client = manifests_specs_clients["client"] - self.manifest = manifests_specs_clients["manifest"] - self.openapi_spec = manifests_specs_clients["openapi_spec"] + self._client = manifests_specs_clients["client"] + self._manifest = manifests_specs_clients["manifest"] + self._openapi_spec = manifests_specs_clients["openapi_spec"] def can_handle_on_response(self) -> bool: """This method is called to check that the plugin can @@ -196,4 +198,3 @@ class BaseOpenAIPlugin: str: The resulting response. """ pass - diff --git a/autogpt/plugins.py b/autogpt/plugins.py index 2455a89e..974adddc 100644 --- a/autogpt/plugins.py +++ b/autogpt/plugins.py @@ -176,7 +176,7 @@ def instantiate_openai_plugin_clients(manifests_specs_clients: dict, cfg: Config def scan_plugins(cfg: Config, debug: bool = False) -> List[Tuple[str, Path]]: - """Scan the plugins directory for plugins. + """Scan the plugins directory for plugins and loads them. Args: cfg (Config): Config instance including plugins config @@ -185,46 +185,37 @@ def scan_plugins(cfg: Config, debug: bool = False) -> List[Tuple[str, Path]]: Returns: List[Tuple[str, Path]]: List of plugins. """ - plugins = [] + loaded_plugins = [] # Generic plugins plugins_path_path = Path(cfg.plugins_dir) for plugin in plugins_path_path.glob("*.zip"): if module := inspect_zip_for_module(str(plugin), debug): - plugins.append((module, plugin)) + plugin = Path(plugin) + module = Path(module) + if debug: + print(f"Plugin: {plugin} Module: {module}") + zipped_package = zipimporter(plugin) + zipped_module = zipped_package.load_module(str(module.parent)) + for key in dir(zipped_module): + if key.startswith("__"): + continue + a_module = getattr(zipped_module, key) + a_keys = dir(a_module) + if ( + "_abc_impl" in a_keys + and a_module.__name__ != "AutoGPTPluginTemplate" + and blacklist_whitelist_check(a_module.__name__, cfg) + ): + loaded_plugins.append(a_module()) # OpenAI plugins if cfg.plugins_openai: manifests_specs = fetch_openai_plugins_manifest_and_spec(cfg) 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 = BaseOpenAIPlugin(openai_plugin_meta) - plugins.append((plugin, url)) - return plugins - - -def blacklist_whitelist_check(plugins: List[AbstractSingleton], cfg: Config): - """Check if the plugin is in the whitelist or blacklist. - - Args: - plugins (List[Tuple[str, Path]]): List of plugins. - cfg (Config): Config object. - - Returns: - List[Tuple[str, Path]]: List of plugins. - """ - loaded_plugins = [] - for plugin in plugins: - if plugin.__name__ in cfg.plugins_blacklist: - continue - if plugin.__name__ in cfg.plugins_whitelist: - loaded_plugins.append(plugin()) - else: - ack = input( - f"WARNNG Plugin {plugin.__name__} found. But not in the" - " whitelist... Load? (y/n): " - ) - if ack.lower() == "y": - loaded_plugins.append(plugin()) + if blacklist_whitelist_check(url, cfg): + plugin = BaseOpenAIPlugin(openai_plugin_meta) + loaded_plugins.append(plugin) if loaded_plugins: print(f"\nPlugins found: {len(loaded_plugins)}\n" "--------------------") @@ -232,30 +223,22 @@ def blacklist_whitelist_check(plugins: List[AbstractSingleton], cfg: Config): print(f"{plugin._name}: {plugin._version} - {plugin._description}") return loaded_plugins - -def load_plugins(cfg: Config = Config(), debug: bool = False) -> List[object]: - """Load plugins from the plugins directory. +def blacklist_whitelist_check(plugin_name: str, cfg: Config) -> bool: + """Check if the plugin is in the whitelist or blacklist. Args: - cfg (Config): Config instance including plugins config - debug (bool, optional): Enable debug logging. Defaults to False. + plugin_name (str): Name of the plugin. + cfg (Config): Config object. + Returns: - List[AbstractSingleton]: List of plugins initialized. + True or False """ - plugins = scan_plugins(cfg) - plugin_modules = [] - for module, plugin in plugins: - plugin = Path(plugin) - module = Path(module) - if debug: - print(f"Plugin: {plugin} Module: {module}") - zipped_package = zipimporter(plugin) - zipped_module = zipped_package.load_module(str(module.parent)) - for key in dir(zipped_module): - if key.startswith("__"): - continue - a_module = getattr(zipped_module, key) - a_keys = dir(a_module) - if "_abc_impl" in a_keys and a_module.__name__ != "AutoGPTPluginTemplate": - plugin_modules.append(a_module) - return blacklist_whitelist_check(plugin_modules, cfg) + if plugin_name in cfg.plugins_blacklist: + return False + if plugin_name in cfg.plugins_whitelist: + return True + ack = input( + f"WARNNG Plugin {plugin_name} found. But not in the" + " whitelist... Load? (y/n): " + ) + return ack.lower() == "y" diff --git a/requirements.txt b/requirements.txt index 6583d65a..9f015f92 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ pytest-mock tweepy -# OpenAI plugins import +# OpenAI and Generic plugins import openapi-python-client==0.13.4 abstract-singleton -auto-vicuna +auto-gpt-plugin-template diff --git a/tests/unit/data/test_plugins/Auto-GPT-Plugin-Test-master.zip b/tests/unit/data/test_plugins/Auto-GPT-Plugin-Test-master.zip index 7a6421af1c23f354c0915f8b729343dc8de32e5f..00bc1f4f58dc1c8e07c7ca3adce6be605fe6a3ce 100644 GIT binary patch delta 2349 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 Date: Tue, 18 Apr 2023 13:16:10 -0700 Subject: [PATCH 46/60] removing accidentially commited ./docker --- .../23dc7e07-db57-40d5-bb18-c75c6ff2cc0f.jpg | Bin 197109 -> 0 bytes .../34cdcc0e-1343-4f6c-a204-dc34d0e75f96.jpg | Bin 197109 -> 0 bytes docker/workspace/analyze_trending_tokens.py | 17 ----------------- docker/workspace/config.json | 3 --- docker/workspace/ethereum_price.json | 0 docker/workspace/llm_risks.md | 7 ------- 6 files changed, 27 deletions(-) delete mode 100644 docker/workspace/23dc7e07-db57-40d5-bb18-c75c6ff2cc0f.jpg delete mode 100644 docker/workspace/34cdcc0e-1343-4f6c-a204-dc34d0e75f96.jpg delete mode 100644 docker/workspace/analyze_trending_tokens.py delete mode 100644 docker/workspace/config.json delete mode 100644 docker/workspace/ethereum_price.json delete mode 100644 docker/workspace/llm_risks.md diff --git a/docker/workspace/23dc7e07-db57-40d5-bb18-c75c6ff2cc0f.jpg b/docker/workspace/23dc7e07-db57-40d5-bb18-c75c6ff2cc0f.jpg deleted file mode 100644 index 59be7609c39cdcbb10e4f324221b51e003c96631..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 197109 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

uxe=@ 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%0A1nN)zHXQiG<# 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 diff --git a/docker/workspace/analyze_trending_tokens.py b/docker/workspace/analyze_trending_tokens.py deleted file mode 100644 index 7bd86739..00000000 --- a/docker/workspace/analyze_trending_tokens.py +++ /dev/null @@ -1,17 +0,0 @@ -# Use the DexGuru API to retrieve top trending tokens data -import requests - -url = 'https://api-stage-lax.dex.guru/v2/tokens/trending' -headers = {'Content-type': 'application/json'} - -payload = {'ids': [], 'network': 'eth,optimism,bsc,gnosis,polygon,fantom,zksync,canto,arbitrum,nova,celo,avalanche'} - -response = requests.post(url, json=payload, headers=headers) - -if response.status_code == 200: - data = response.json() - # data contains list of dict objects describing the trending tokens - # EXTRACT INFO YOU WILL NEED TO SUGGEST INVESTMENTS HERE - print(data) -else: - print(f"An error occurred with status code {response.status_code}") \ No newline at end of file diff --git a/docker/workspace/config.json b/docker/workspace/config.json deleted file mode 100644 index b468f762..00000000 --- a/docker/workspace/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "EXECUTE_LOCAL_COMMANDS": "True" -} \ No newline at end of file diff --git a/docker/workspace/ethereum_price.json b/docker/workspace/ethereum_price.json deleted file mode 100644 index e69de29b..00000000 diff --git a/docker/workspace/llm_risks.md b/docker/workspace/llm_risks.md deleted file mode 100644 index aadb349b..00000000 --- a/docker/workspace/llm_risks.md +++ /dev/null @@ -1,7 +0,0 @@ -# Legal Risk Statement - -As an LLM, we would like to remind you of the risks associated with cryptocurrency investment. While cryptocurrencies have become an appealing investment choice, it is also a highly volatile market. As such, we advise that you carefully consider the risks before making any investments. - -We caution users that the crypto market is generally unregulated, meaning that it is not bound by any particular legal framework. Virtual currencies are also a new and rapidly evolving technology, and as such, new developments may affect their long-term ability to remain a viable investment option. Therefore, it is essential to understand that investing in cryptocurrency comes with inherent risks that are associated with any investment. - -Please note that this statement is for educational purposes only and does not constitute legal or financial advice. Before making any significant financial decisions, you should seek professional advice tailored to your needs and objectives. \ No newline at end of file From ebee041c357163be4616687bf94220bb30e200d7 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Tue, 18 Apr 2023 18:18:15 -0500 Subject: [PATCH 47/60] fix merge --- autogpt/plugins.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/autogpt/plugins.py b/autogpt/plugins.py index 27df3567..a9e48a38 100644 --- a/autogpt/plugins.py +++ b/autogpt/plugins.py @@ -177,10 +177,8 @@ def instantiate_openai_plugin_clients(manifests_specs_clients: dict, cfg: Config return plugins -def load_plugins( - plugins_path: Path, debug: bool = False -) -> List[AutoGPTPluginTemplate]: - """Load plugins from the plugins directory. +def scan_plugins(cfg: Config, debug: bool = False) -> List[AutoGPTPluginTemplate]: + """Scan the plugins directory for plugins and loads them. Args: cfg (Config): Config instance including plugins config From 7d45de8901d0b21de37dcbd8d4820d9791f2e643 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Tue, 18 Apr 2023 18:48:44 -0500 Subject: [PATCH 48/60] fix merge --- autogpt/__main__.py | 75 --------------------------------------------- autogpt/cli.py | 31 +++++++++++++++++-- 2 files changed, 28 insertions(+), 78 deletions(-) diff --git a/autogpt/__main__.py b/autogpt/__main__.py index 7e8bbc1a..128f9eea 100644 --- a/autogpt/__main__.py +++ b/autogpt/__main__.py @@ -3,78 +3,3 @@ import autogpt.cli if __name__ == "__main__": autogpt.cli.main() - - -"""Main script for the autogpt package.""" -import logging - -from colorama import Fore - -from autogpt.agent.agent import Agent -from autogpt.args import parse_arguments -from autogpt.commands.command import CommandRegistry -from autogpt.config import Config, check_openai_api_key -from autogpt.logs import logger -from autogpt.memory import get_memory - -from autogpt.prompts.prompt import construct_main_ai_config -from autogpt.plugins import scan_plugins - - -# Load environment variables from .env file - - -def main() -> None: - """Main function for the script""" - cfg = Config() - # TODO: fill in llm values here - check_openai_api_key() - parse_arguments() - logger.set_level(logging.DEBUG if cfg.debug_mode else logging.INFO) - cfg.set_plugins(scan_plugins(cfg, cfg.debug_mode)) - # Create a CommandRegistry instance and scan default folder - command_registry = CommandRegistry() - command_registry.import_commands("autogpt.commands.audio_text") - command_registry.import_commands("autogpt.commands.evaluate_code") - command_registry.import_commands("autogpt.commands.execute_code") - command_registry.import_commands("autogpt.commands.file_operations") - command_registry.import_commands("autogpt.commands.git_operations") - command_registry.import_commands("autogpt.commands.google_search") - command_registry.import_commands("autogpt.commands.image_gen") - command_registry.import_commands("autogpt.commands.twitter") - command_registry.import_commands("autogpt.commands.web_selenium") - command_registry.import_commands("autogpt.commands.write_tests") - command_registry.import_commands("autogpt.app") - ai_name = "" - ai_config = construct_main_ai_config() - ai_config.command_registry = command_registry - # print(prompt) - # Initialize variables - full_message_history = [] - next_action_count = 0 - # Make a constant: - triggering_prompt = ( - "Determine which next command to use, and respond using the" - " format specified above:" - ) - # Initialize memory and make sure it is empty. - # this is particularly important for indexing and referencing pinecone memory - memory = get_memory(cfg, init=True) - logger.typewriter_log( - "Using memory of type:", Fore.GREEN, f"{memory.__class__.__name__}" - ) - logger.typewriter_log("Using Browser:", Fore.GREEN, cfg.selenium_web_browser) - system_prompt = ai_config.construct_full_prompt() - if cfg.debug_mode: - logger.typewriter_log("Prompt:", Fore.GREEN, system_prompt) - agent = Agent( - ai_name=ai_name, - memory=memory, - full_message_history=full_message_history, - next_action_count=next_action_count, - command_registry=command_registry, - config=ai_config, - system_prompt=system_prompt, - triggering_prompt=triggering_prompt, - ) - agent.start_interaction_loop() diff --git a/autogpt/cli.py b/autogpt/cli.py index a69a53ac..aaf17694 100644 --- a/autogpt/cli.py +++ b/autogpt/cli.py @@ -74,13 +74,15 @@ def main( from colorama import Fore from autogpt.agent.agent import Agent + + from autogpt.commands.command import CommandRegistry from autogpt.config import Config, check_openai_api_key from autogpt.configurator import create_config from autogpt.logs import logger from autogpt.memory import get_memory - from autogpt.prompt import construct_prompt + from autogpt.prompts.prompt import construct_main_ai_config from autogpt.utils import get_latest_bulletin - + from autogpt.plugins import scan_plugins if ctx.invoked_subcommand is None: cfg = Config() # TODO: fill in llm values here @@ -105,7 +107,25 @@ def main( motd = get_latest_bulletin() if motd: logger.typewriter_log("NEWS: ", Fore.GREEN, motd) - system_prompt = construct_prompt() + + cfg = Config() + cfg.set_plugins(scan_plugins(cfg, cfg.debug_mode)) + # Create a CommandRegistry instance and scan default folder + command_registry = CommandRegistry() + command_registry.import_commands("autogpt.commands.audio_text") + command_registry.import_commands("autogpt.commands.evaluate_code") + command_registry.import_commands("autogpt.commands.execute_code") + command_registry.import_commands("autogpt.commands.file_operations") + command_registry.import_commands("autogpt.commands.git_operations") + command_registry.import_commands("autogpt.commands.google_search") + command_registry.import_commands("autogpt.commands.image_gen") + command_registry.import_commands("autogpt.commands.twitter") + command_registry.import_commands("autogpt.commands.web_selenium") + command_registry.import_commands("autogpt.commands.write_tests") + command_registry.import_commands("autogpt.app") + ai_name = "" + ai_config = construct_main_ai_config() + ai_config.command_registry = command_registry # print(prompt) # Initialize variables full_message_history = [] @@ -122,11 +142,16 @@ def main( "Using memory of type:", Fore.GREEN, f"{memory.__class__.__name__}" ) logger.typewriter_log("Using Browser:", Fore.GREEN, cfg.selenium_web_browser) + system_prompt = ai_config.construct_full_prompt() + if cfg.debug_mode: + logger.typewriter_log("Prompt:", Fore.GREEN, system_prompt) agent = Agent( ai_name=ai_name, memory=memory, full_message_history=full_message_history, next_action_count=next_action_count, + command_registry=command_registry, + config=ai_config, system_prompt=system_prompt, triggering_prompt=triggering_prompt, ) From 5813592206edb80b1762f944889c07b130c6ebf7 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Tue, 18 Apr 2023 18:51:28 -0500 Subject: [PATCH 49/60] fix readme --- README.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/README.md b/README.md index 5dac836e..ad6d1994 100644 --- a/README.md +++ b/README.md @@ -266,18 +266,6 @@ Drop the repo's zipfile in the plugins folder. If you add the plugins class name to the whitelist in the config.py you will not be prompted otherwise you'll be warned before loading the plugin. -## Plugins - -See https://github.com/Torantulino/Auto-GPT-Plugin-Template for the template of the plugins. - -⚠️💀 WARNING 💀⚠️: Review the code of any plugin you use, this allows for any Python to be executed and do malicious things. Like stealing your API keys. - -Drop the repo's zipfile in the plugins folder. - -![Download Zip](https://raw.githubusercontent.com/BillSchumacher/Auto-GPT/master/plugin.png) - -If you add the plugins class name to the whitelist in the config.py you will not be prompted otherwise you'll be warned before loading the plugin. - ## Setting Your Cache Type By default, Auto-GPT is going to use LocalCache instead of redis or Pinecone. From 4701357a21cf6e57e876f8d01d46d076eadf2e0b Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Tue, 18 Apr 2023 18:56:11 -0500 Subject: [PATCH 50/60] fix test --- autogpt/plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogpt/plugins.py b/autogpt/plugins.py index a9e48a38..0979b856 100644 --- a/autogpt/plugins.py +++ b/autogpt/plugins.py @@ -196,7 +196,7 @@ def scan_plugins(cfg: Config, debug: bool = False) -> List[AutoGPTPluginTemplate module = Path(module) if debug: print(f"Plugin: {plugin} Module: {module}") - zipped_package = zipimporter(plugin) + zipped_package = zipimporter(str(plugin)) zipped_module = zipped_package.load_module(str(module.parent)) for key in dir(zipped_module): if key.startswith("__"): From 86d3444fb8f9c46419dee88dd5c1fd1ff2be2ffa Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Tue, 18 Apr 2023 18:59:23 -0500 Subject: [PATCH 51/60] isort, add proper skips. --- autogpt/agent/agent.py | 3 ++- autogpt/agent/agent_manager.py | 4 +--- autogpt/app.py | 22 ++++++------------- autogpt/cli.py | 3 +-- autogpt/commands/file_operations.py | 4 +--- autogpt/commands/web_selenium.py | 2 +- autogpt/config/config.py | 4 ++-- autogpt/json_fixes/master_json_fix_method.py | 4 +--- autogpt/llm_utils.py | 2 +- autogpt/logs.py | 5 ++--- autogpt/memory/milvus.py | 3 ++- autogpt/models/base_open_ai_plugin.py | 3 +-- autogpt/plugins.py | 11 +++++----- autogpt/token_counter.py | 1 + pyproject.toml | 1 + tests/test_commands.py | 2 +- .../unit/models/test_base_open_api_plugin.py | 7 ++++-- tests/unit/test_plugins.py | 4 +++- 18 files changed, 38 insertions(+), 47 deletions(-) diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py index c0bb8d5b..6f1e15be 100644 --- a/autogpt/agent/agent.py +++ b/autogpt/agent/agent.py @@ -3,7 +3,8 @@ from colorama import Fore, Style from autogpt.app import execute_command, get_command from autogpt.chat import chat_with_ai, create_chat_message from autogpt.config import Config -from autogpt.json_fixes.master_json_fix_method import fix_json_using_multiple_techniques +from autogpt.json_fixes.master_json_fix_method import \ + fix_json_using_multiple_techniques from autogpt.json_validation.validate_json import validate_json from autogpt.logs import logger, print_assistant_thoughts from autogpt.speech import say_text diff --git a/autogpt/agent/agent_manager.py b/autogpt/agent/agent_manager.py index a35da3c2..9a62ef61 100644 --- a/autogpt/agent/agent_manager.py +++ b/autogpt/agent/agent_manager.py @@ -1,9 +1,7 @@ """Agent manager for managing GPT agents""" from __future__ import annotations -from typing import List - -from typing import Union +from typing import List, Union from autogpt.config.config import Config, Singleton from autogpt.llm_utils import create_chat_completion diff --git a/autogpt/app.py b/autogpt/app.py index 38e9cb58..10eb98bb 100644 --- a/autogpt/app.py +++ b/autogpt/app.py @@ -6,22 +6,14 @@ from autogpt.agent.agent_manager import AgentManager from autogpt.commands.audio_text import read_audio_from_file from autogpt.commands.command import CommandRegistry, command from autogpt.commands.evaluate_code import evaluate_code -from autogpt.commands.execute_code import ( - execute_python_file, - execute_shell, - execute_shell_popen, -) -from autogpt.commands.file_operations import ( - append_to_file, - delete_file, - download_file, - read_file, - search_files, - write_to_file, - download_file, -) +from autogpt.commands.execute_code import (execute_python_file, execute_shell, + execute_shell_popen) +from autogpt.commands.file_operations import (append_to_file, delete_file, + download_file, read_file, + search_files, write_to_file) from autogpt.commands.git_operations import clone_repository -from autogpt.commands.google_search import google_official_search, google_search +from autogpt.commands.google_search import (google_official_search, + google_search) from autogpt.commands.image_gen import generate_image from autogpt.commands.improve_code import improve_code from autogpt.commands.twitter import send_tweet diff --git a/autogpt/cli.py b/autogpt/cli.py index aaf17694..4cf82a65 100644 --- a/autogpt/cli.py +++ b/autogpt/cli.py @@ -74,15 +74,14 @@ def main( from colorama import Fore from autogpt.agent.agent import Agent - from autogpt.commands.command import CommandRegistry from autogpt.config import Config, check_openai_api_key from autogpt.configurator import create_config from autogpt.logs import logger from autogpt.memory import get_memory + from autogpt.plugins import scan_plugins from autogpt.prompts.prompt import construct_main_ai_config from autogpt.utils import get_latest_bulletin - from autogpt.plugins import scan_plugins if ctx.invoked_subcommand is None: cfg = Config() # TODO: fill in llm values here diff --git a/autogpt/commands/file_operations.py b/autogpt/commands/file_operations.py index 5bb45179..d850ea67 100644 --- a/autogpt/commands/file_operations.py +++ b/autogpt/commands/file_operations.py @@ -10,13 +10,11 @@ import requests from colorama import Back, Fore from requests.adapters import HTTPAdapter, Retry +from autogpt.commands.command import command from autogpt.spinner import Spinner from autogpt.utils import readable_file_size - -from autogpt.commands.command import command from autogpt.workspace import WORKSPACE_PATH, path_in_workspace - LOG_FILE = "file_logger.txt" LOG_FILE_PATH = WORKSPACE_PATH / LOG_FILE diff --git a/autogpt/commands/web_selenium.py b/autogpt/commands/web_selenium.py index ab607f7b..e0e0d70a 100644 --- a/autogpt/commands/web_selenium.py +++ b/autogpt/commands/web_selenium.py @@ -17,8 +17,8 @@ from selenium.webdriver.support.wait import WebDriverWait from webdriver_manager.chrome import ChromeDriverManager from webdriver_manager.firefox import GeckoDriverManager -from autogpt.commands.command import command import autogpt.processing.text as summary +from autogpt.commands.command import command from autogpt.config import Config from autogpt.processing.html import extract_hyperlinks, format_hyperlinks diff --git a/autogpt/config/config.py b/autogpt/config/config.py index 5c6856f5..af1bcf0d 100644 --- a/autogpt/config/config.py +++ b/autogpt/config/config.py @@ -1,10 +1,10 @@ """Configuration class to store the state of bools for different scripts access.""" import os - from typing import List + import openai -from auto_gpt_plugin_template import AutoGPTPluginTemplate import yaml +from auto_gpt_plugin_template import AutoGPTPluginTemplate from colorama import Fore from dotenv import load_dotenv diff --git a/autogpt/json_fixes/master_json_fix_method.py b/autogpt/json_fixes/master_json_fix_method.py index a77bf670..991a95f2 100644 --- a/autogpt/json_fixes/master_json_fix_method.py +++ b/autogpt/json_fixes/master_json_fix_method.py @@ -9,9 +9,7 @@ CFG = Config() def fix_json_using_multiple_techniques(assistant_reply: str) -> Dict[Any, Any]: from autogpt.json_fixes.parsing import ( - attempt_to_fix_json_by_finding_outermost_brackets, - fix_and_parse_json, - ) + attempt_to_fix_json_by_finding_outermost_brackets, fix_and_parse_json) # Parse and print Assistant response assistant_reply_json = fix_and_parse_json(assistant_reply) diff --git a/autogpt/llm_utils.py b/autogpt/llm_utils.py index 48561da9..d26fb2d6 100644 --- a/autogpt/llm_utils.py +++ b/autogpt/llm_utils.py @@ -8,8 +8,8 @@ from colorama import Fore, Style from openai.error import APIError, RateLimitError from autogpt.config import Config -from autogpt.types.openai import Message from autogpt.logs import logger +from autogpt.types.openai import Message CFG = Config() diff --git a/autogpt/logs.py b/autogpt/logs.py index df3487f2..4ae33432 100644 --- a/autogpt/logs.py +++ b/autogpt/logs.py @@ -204,9 +204,8 @@ logger = Logger() def print_assistant_thoughts(ai_name, assistant_reply): """Prints the assistant's thoughts to the console""" - from autogpt.json_fixes.bracket_termination import ( - attempt_to_fix_json_by_finding_outermost_brackets, - ) + from autogpt.json_fixes.bracket_termination import \ + attempt_to_fix_json_by_finding_outermost_brackets from autogpt.json_fixes.parsing import fix_and_parse_json try: diff --git a/autogpt/memory/milvus.py b/autogpt/memory/milvus.py index 44aa72b9..e2b8fea8 100644 --- a/autogpt/memory/milvus.py +++ b/autogpt/memory/milvus.py @@ -1,5 +1,6 @@ """ Milvus memory storage provider.""" -from pymilvus import Collection, CollectionSchema, DataType, FieldSchema, connections +from pymilvus import (Collection, CollectionSchema, DataType, FieldSchema, + connections) from autogpt.memory.base import MemoryProviderSingleton, get_ada_embedding diff --git a/autogpt/models/base_open_ai_plugin.py b/autogpt/models/base_open_ai_plugin.py index fafd3932..64214599 100644 --- a/autogpt/models/base_open_ai_plugin.py +++ b/autogpt/models/base_open_ai_plugin.py @@ -1,6 +1,5 @@ """Handles loading of plugins.""" -from typing import Any, Dict, List, Optional, Tuple, TypedDict -from typing import TypeVar +from typing import Any, Dict, List, Optional, Tuple, TypedDict, TypeVar from auto_gpt_plugin_template import AutoGPTPluginTemplate diff --git a/autogpt/plugins.py b/autogpt/plugins.py index 0979b856..c6324a54 100644 --- a/autogpt/plugins.py +++ b/autogpt/plugins.py @@ -4,20 +4,19 @@ import importlib import json import os import zipfile -import openapi_python_client -import requests - from pathlib import Path -from typing import List, Tuple, Optional +from typing import List, Optional, Tuple from urllib.parse import urlparse from zipimport import zipimporter +import openapi_python_client +import requests +from auto_gpt_plugin_template import AutoGPTPluginTemplate from openapi_python_client.cli import Config as OpenAPIConfig + from autogpt.config import Config from autogpt.models.base_open_ai_plugin import BaseOpenAIPlugin -from auto_gpt_plugin_template import AutoGPTPluginTemplate - def inspect_zip_for_module(zip_path: str, debug: bool = False) -> Optional[str]: """ diff --git a/autogpt/token_counter.py b/autogpt/token_counter.py index b1e59d86..2d50547b 100644 --- a/autogpt/token_counter.py +++ b/autogpt/token_counter.py @@ -1,5 +1,6 @@ """Functions for counting the number of tokens in a message or string.""" from __future__ import annotations + from typing import List import tiktoken diff --git a/pyproject.toml b/pyproject.toml index fdb43d66..72bb3ff8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ extend-exclude = '.+/(dist|.venv|venv|build)/.+' [tool.isort] profile = "black" +skip = venv,env,node_modules,.env,.venv,dist multi_line_output = 3 include_trailing_comma = true force_grid_wrap = 0 diff --git a/tests/test_commands.py b/tests/test_commands.py index 4be41a90..8a7771f6 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -1,5 +1,5 @@ -import shutil import os +import shutil import sys from pathlib import Path diff --git a/tests/unit/models/test_base_open_api_plugin.py b/tests/unit/models/test_base_open_api_plugin.py index 3dc58d51..8f49c295 100644 --- a/tests/unit/models/test_base_open_api_plugin.py +++ b/tests/unit/models/test_base_open_api_plugin.py @@ -1,6 +1,9 @@ -import pytest from typing import Any, Dict, List, Optional, Tuple -from autogpt.models.base_open_ai_plugin import BaseOpenAIPlugin, Message, PromptGenerator + +import pytest + +from autogpt.models.base_open_ai_plugin import (BaseOpenAIPlugin, Message, + PromptGenerator) class DummyPlugin(BaseOpenAIPlugin): diff --git a/tests/unit/test_plugins.py b/tests/unit/test_plugins.py index 58e895bf..83656da2 100644 --- a/tests/unit/test_plugins.py +++ b/tests/unit/test_plugins.py @@ -1,6 +1,8 @@ import pytest -from autogpt.plugins import inspect_zip_for_module, scan_plugins, blacklist_whitelist_check + from autogpt.config import Config +from autogpt.plugins import (blacklist_whitelist_check, inspect_zip_for_module, + scan_plugins) PLUGINS_TEST_DIR = "tests/unit/data/test_plugins" PLUGIN_TEST_ZIP_FILE = "Auto-GPT-Plugin-Test-master.zip" From 221a4b0b50f4eb8fc8f2d2621fca87430aa19f05 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Tue, 18 Apr 2023 19:02:10 -0500 Subject: [PATCH 52/60] I guess linux doesn't like this.... --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 72bb3ff8..fdb43d66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ extend-exclude = '.+/(dist|.venv|venv|build)/.+' [tool.isort] profile = "black" -skip = venv,env,node_modules,.env,.venv,dist multi_line_output = 3 include_trailing_comma = true force_grid_wrap = 0 From 3f2d14f4d8a2e983e90fbc0498bd31ab259ae3c4 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Tue, 18 Apr 2023 19:07:39 -0500 Subject: [PATCH 53/60] Fix isort? --- .isort.cfg | 10 +++++++++ autogpt/agent/agent.py | 3 +-- autogpt/app.py | 21 ++++++++++++------- autogpt/json_fixes/master_json_fix_method.py | 4 +++- autogpt/logs.py | 5 +++-- autogpt/memory/milvus.py | 3 +-- .../unit/models/test_base_open_api_plugin.py | 7 +++++-- tests/unit/test_plugins.py | 7 +++++-- 8 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 .isort.cfg diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 00000000..17eab482 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,10 @@ +[settings] +profile = black +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 88 +sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +skip = .tox,__pycache__,*.pyc,venv*/*,reports,venv,env,node_modules,.env,.venv,dist diff --git a/autogpt/agent/agent.py b/autogpt/agent/agent.py index 6f1e15be..c0bb8d5b 100644 --- a/autogpt/agent/agent.py +++ b/autogpt/agent/agent.py @@ -3,8 +3,7 @@ from colorama import Fore, Style from autogpt.app import execute_command, get_command from autogpt.chat import chat_with_ai, create_chat_message from autogpt.config import Config -from autogpt.json_fixes.master_json_fix_method import \ - fix_json_using_multiple_techniques +from autogpt.json_fixes.master_json_fix_method import fix_json_using_multiple_techniques from autogpt.json_validation.validate_json import validate_json from autogpt.logs import logger, print_assistant_thoughts from autogpt.speech import say_text diff --git a/autogpt/app.py b/autogpt/app.py index 10eb98bb..03e2320c 100644 --- a/autogpt/app.py +++ b/autogpt/app.py @@ -6,14 +6,21 @@ from autogpt.agent.agent_manager import AgentManager from autogpt.commands.audio_text import read_audio_from_file from autogpt.commands.command import CommandRegistry, command from autogpt.commands.evaluate_code import evaluate_code -from autogpt.commands.execute_code import (execute_python_file, execute_shell, - execute_shell_popen) -from autogpt.commands.file_operations import (append_to_file, delete_file, - download_file, read_file, - search_files, write_to_file) +from autogpt.commands.execute_code import ( + execute_python_file, + execute_shell, + execute_shell_popen, +) +from autogpt.commands.file_operations import ( + append_to_file, + delete_file, + download_file, + read_file, + search_files, + write_to_file, +) from autogpt.commands.git_operations import clone_repository -from autogpt.commands.google_search import (google_official_search, - google_search) +from autogpt.commands.google_search import google_official_search, google_search from autogpt.commands.image_gen import generate_image from autogpt.commands.improve_code import improve_code from autogpt.commands.twitter import send_tweet diff --git a/autogpt/json_fixes/master_json_fix_method.py b/autogpt/json_fixes/master_json_fix_method.py index 991a95f2..a77bf670 100644 --- a/autogpt/json_fixes/master_json_fix_method.py +++ b/autogpt/json_fixes/master_json_fix_method.py @@ -9,7 +9,9 @@ CFG = Config() def fix_json_using_multiple_techniques(assistant_reply: str) -> Dict[Any, Any]: from autogpt.json_fixes.parsing import ( - attempt_to_fix_json_by_finding_outermost_brackets, fix_and_parse_json) + attempt_to_fix_json_by_finding_outermost_brackets, + fix_and_parse_json, + ) # Parse and print Assistant response assistant_reply_json = fix_and_parse_json(assistant_reply) diff --git a/autogpt/logs.py b/autogpt/logs.py index 4ae33432..df3487f2 100644 --- a/autogpt/logs.py +++ b/autogpt/logs.py @@ -204,8 +204,9 @@ logger = Logger() def print_assistant_thoughts(ai_name, assistant_reply): """Prints the assistant's thoughts to the console""" - from autogpt.json_fixes.bracket_termination import \ - attempt_to_fix_json_by_finding_outermost_brackets + from autogpt.json_fixes.bracket_termination import ( + attempt_to_fix_json_by_finding_outermost_brackets, + ) from autogpt.json_fixes.parsing import fix_and_parse_json try: diff --git a/autogpt/memory/milvus.py b/autogpt/memory/milvus.py index e2b8fea8..44aa72b9 100644 --- a/autogpt/memory/milvus.py +++ b/autogpt/memory/milvus.py @@ -1,6 +1,5 @@ """ Milvus memory storage provider.""" -from pymilvus import (Collection, CollectionSchema, DataType, FieldSchema, - connections) +from pymilvus import Collection, CollectionSchema, DataType, FieldSchema, connections from autogpt.memory.base import MemoryProviderSingleton, get_ada_embedding diff --git a/tests/unit/models/test_base_open_api_plugin.py b/tests/unit/models/test_base_open_api_plugin.py index 8f49c295..227ed7cf 100644 --- a/tests/unit/models/test_base_open_api_plugin.py +++ b/tests/unit/models/test_base_open_api_plugin.py @@ -2,8 +2,11 @@ from typing import Any, Dict, List, Optional, Tuple import pytest -from autogpt.models.base_open_ai_plugin import (BaseOpenAIPlugin, Message, - PromptGenerator) +from autogpt.models.base_open_ai_plugin import ( + BaseOpenAIPlugin, + Message, + PromptGenerator, +) class DummyPlugin(BaseOpenAIPlugin): diff --git a/tests/unit/test_plugins.py b/tests/unit/test_plugins.py index 83656da2..8e673bd5 100644 --- a/tests/unit/test_plugins.py +++ b/tests/unit/test_plugins.py @@ -1,8 +1,11 @@ import pytest from autogpt.config import Config -from autogpt.plugins import (blacklist_whitelist_check, inspect_zip_for_module, - scan_plugins) +from autogpt.plugins import ( + blacklist_whitelist_check, + inspect_zip_for_module, + scan_plugins, +) PLUGINS_TEST_DIR = "tests/unit/data/test_plugins" PLUGIN_TEST_ZIP_FILE = "Auto-GPT-Plugin-Test-master.zip" From 4c7b58245462962ca780145f60f39804a08fb367 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Tue, 18 Apr 2023 19:09:15 -0500 Subject: [PATCH 54/60] apply black --- autogpt/cli.py | 1 + autogpt/models/base_open_ai_plugin.py | 8 +- autogpt/plugins.py | 87 +++++++++++-------- .../unit/models/test_base_open_api_plugin.py | 4 +- tests/unit/test_plugins.py | 47 +++++++--- 5 files changed, 93 insertions(+), 54 deletions(-) diff --git a/autogpt/cli.py b/autogpt/cli.py index 4cf82a65..f6b27586 100644 --- a/autogpt/cli.py +++ b/autogpt/cli.py @@ -82,6 +82,7 @@ def main( from autogpt.plugins import scan_plugins from autogpt.prompts.prompt import construct_main_ai_config from autogpt.utils import get_latest_bulletin + if ctx.invoked_subcommand is None: cfg = Config() # TODO: fill in llm values here diff --git a/autogpt/models/base_open_ai_plugin.py b/autogpt/models/base_open_ai_plugin.py index 64214599..987e7aad 100644 --- a/autogpt/models/base_open_ai_plugin.py +++ b/autogpt/models/base_open_ai_plugin.py @@ -61,7 +61,7 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate): return False def on_planning( - self, prompt: PromptGenerator, messages: List[Message] + self, prompt: PromptGenerator, messages: List[Message] ) -> Optional[str]: """This method is called before the planning chat completion is done. Args: @@ -142,7 +142,7 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate): return False def pre_command( - self, command_name: str, arguments: Dict[str, Any] + self, command_name: str, arguments: Dict[str, Any] ) -> Tuple[str, Dict[str, Any]]: """This method is called before the command is executed. Args: @@ -171,7 +171,7 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate): pass def can_handle_chat_completion( - self, messages: Dict[Any, Any], model: str, temperature: float, max_tokens: int + self, messages: Dict[Any, Any], model: str, temperature: float, max_tokens: int ) -> bool: """This method is called to check that the plugin can handle the chat_completion method. @@ -185,7 +185,7 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate): return False def handle_chat_completion( - self, messages: List[Message], model: str, temperature: float, max_tokens: int + self, messages: List[Message], model: str, temperature: float, max_tokens: int ) -> str: """This method is called when the chat completion is done. Args: diff --git a/autogpt/plugins.py b/autogpt/plugins.py index c6324a54..b536acbd 100644 --- a/autogpt/plugins.py +++ b/autogpt/plugins.py @@ -47,7 +47,7 @@ def write_dict_to_json_file(data: dict, file_path: str) -> None: data (dict): Dictionary to write. file_path (str): Path to the file. """ - with open(file_path, 'w') as file: + with open(file_path, "w") as file: json.dump(data, file, indent=4) @@ -64,35 +64,42 @@ def fetch_openai_plugins_manifest_and_spec(cfg: Config) -> dict: for url in cfg.plugins_openai: openai_plugin_client_dir = f"{cfg.plugins_dir}/openai/{urlparse(url).netloc}" create_directory_if_not_exists(openai_plugin_client_dir) - if not os.path.exists(f'{openai_plugin_client_dir}/ai-plugin.json'): + if not os.path.exists(f"{openai_plugin_client_dir}/ai-plugin.json"): try: response = requests.get(f"{url}/.well-known/ai-plugin.json") if response.status_code == 200: manifest = response.json() if manifest["schema_version"] != "v1": - print(f"Unsupported manifest version: {manifest['schem_version']} for {url}") + print( + f"Unsupported manifest version: {manifest['schem_version']} for {url}" + ) continue if manifest["api"]["type"] != "openapi": - print(f"Unsupported API type: {manifest['api']['type']} for {url}") + print( + f"Unsupported API type: {manifest['api']['type']} for {url}" + ) continue - write_dict_to_json_file(manifest, f'{openai_plugin_client_dir}/ai-plugin.json') + write_dict_to_json_file( + manifest, f"{openai_plugin_client_dir}/ai-plugin.json" + ) else: print(f"Failed to fetch manifest for {url}: {response.status_code}") except requests.exceptions.RequestException as e: print(f"Error while requesting manifest from {url}: {e}") else: print(f"Manifest for {url} already exists") - manifest = json.load(open(f'{openai_plugin_client_dir}/ai-plugin.json')) - if not os.path.exists(f'{openai_plugin_client_dir}/openapi.json'): - openapi_spec = openapi_python_client._get_document(url=manifest["api"]["url"], path=None, timeout=5) - write_dict_to_json_file(openapi_spec, f'{openai_plugin_client_dir}/openapi.json') + manifest = json.load(open(f"{openai_plugin_client_dir}/ai-plugin.json")) + if not os.path.exists(f"{openai_plugin_client_dir}/openapi.json"): + openapi_spec = openapi_python_client._get_document( + url=manifest["api"]["url"], path=None, timeout=5 + ) + write_dict_to_json_file( + openapi_spec, f"{openai_plugin_client_dir}/openapi.json" + ) else: print(f"OpenAPI spec for {url} already exists") - openapi_spec = json.load(open(f'{openai_plugin_client_dir}/openapi.json')) - manifests[url] = { - 'manifest': manifest, - 'openapi_spec': openapi_spec - } + openapi_spec = json.load(open(f"{openai_plugin_client_dir}/openapi.json")) + manifests[url] = {"manifest": manifest, "openapi_spec": openapi_spec} return manifests @@ -117,7 +124,9 @@ def create_directory_if_not_exists(directory_path: str) -> bool: return True -def initialize_openai_plugins(manifests_specs: dict, cfg: Config, debug: bool = False) -> dict: +def initialize_openai_plugins( + manifests_specs: dict, cfg: Config, debug: bool = False +) -> dict: """ Initialize OpenAI plugins. Args: @@ -127,39 +136,47 @@ def initialize_openai_plugins(manifests_specs: dict, cfg: Config, debug: bool = Returns: dict: per url dictionary of manifest, spec and client. """ - openai_plugins_dir = f'{cfg.plugins_dir}/openai' + openai_plugins_dir = f"{cfg.plugins_dir}/openai" if create_directory_if_not_exists(openai_plugins_dir): for url, manifest_spec in manifests_specs.items(): - openai_plugin_client_dir = f'{openai_plugins_dir}/{urlparse(url).hostname}' - _meta_option = openapi_python_client.MetaType.SETUP, - _config = OpenAPIConfig(**{ - 'project_name_override': 'client', - 'package_name_override': 'client', - }) + openai_plugin_client_dir = f"{openai_plugins_dir}/{urlparse(url).hostname}" + _meta_option = (openapi_python_client.MetaType.SETUP,) + _config = OpenAPIConfig( + **{ + "project_name_override": "client", + "package_name_override": "client", + } + ) prev_cwd = Path.cwd() os.chdir(openai_plugin_client_dir) - Path('ai-plugin.json') - if not os.path.exists('client'): + Path("ai-plugin.json") + if not os.path.exists("client"): client_results = openapi_python_client.create_new_client( - url=manifest_spec['manifest']['api']['url'], + url=manifest_spec["manifest"]["api"]["url"], path=None, meta=_meta_option, config=_config, ) if client_results: - print(f"Error creating OpenAPI client: {client_results[0].header} \n" - f" details: {client_results[0].detail}") + print( + f"Error creating OpenAPI client: {client_results[0].header} \n" + f" details: {client_results[0].detail}" + ) continue - spec = importlib.util.spec_from_file_location('client', 'client/client/client.py') + spec = importlib.util.spec_from_file_location( + "client", "client/client/client.py" + ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) client = module.Client(base_url=url) os.chdir(prev_cwd) - manifest_spec['client'] = client + manifest_spec["client"] = client return manifests_specs -def instantiate_openai_plugin_clients(manifests_specs_clients: dict, cfg: Config, debug: bool = False) -> dict: +def instantiate_openai_plugin_clients( + manifests_specs_clients: dict, cfg: Config, debug: bool = False +) -> dict: """ Instantiates BaseOpenAIPlugin instances for each OpenAI plugin. Args: @@ -203,16 +220,18 @@ def scan_plugins(cfg: Config, debug: bool = False) -> List[AutoGPTPluginTemplate a_module = getattr(zipped_module, key) a_keys = dir(a_module) if ( - "_abc_impl" in a_keys - and a_module.__name__ != "AutoGPTPluginTemplate" - and blacklist_whitelist_check(a_module.__name__, cfg) + "_abc_impl" in a_keys + and a_module.__name__ != "AutoGPTPluginTemplate" + and blacklist_whitelist_check(a_module.__name__, cfg) ): loaded_plugins.append(a_module()) # OpenAI plugins if cfg.plugins_openai: manifests_specs = fetch_openai_plugins_manifest_and_spec(cfg) if manifests_specs.keys(): - manifests_specs_clients = initialize_openai_plugins(manifests_specs, cfg, debug) + manifests_specs_clients = initialize_openai_plugins( + manifests_specs, cfg, debug + ) for url, openai_plugin_meta in manifests_specs_clients.items(): if blacklist_whitelist_check(url, cfg): plugin = BaseOpenAIPlugin(openai_plugin_meta) diff --git a/tests/unit/models/test_base_open_api_plugin.py b/tests/unit/models/test_base_open_api_plugin.py index 227ed7cf..3cfb3465 100644 --- a/tests/unit/models/test_base_open_api_plugin.py +++ b/tests/unit/models/test_base_open_api_plugin.py @@ -19,10 +19,10 @@ def dummy_plugin(): "manifest": { "name_for_model": "Dummy", "schema_version": "1.0", - "description_for_model": "A dummy plugin for testing purposes" + "description_for_model": "A dummy plugin for testing purposes", }, "client": None, - "openapi_spec": None + "openapi_spec": None, } return DummyPlugin(manifests_specs_clients) diff --git a/tests/unit/test_plugins.py b/tests/unit/test_plugins.py index 8e673bd5..739e69bb 100644 --- a/tests/unit/test_plugins.py +++ b/tests/unit/test_plugins.py @@ -10,11 +10,11 @@ from autogpt.plugins import ( 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_vicuna/__init__.py" -PLUGIN_TEST_OPENAI = 'https://weathergpt.vercel.app/' +PLUGIN_TEST_OPENAI = "https://weathergpt.vercel.app/" def test_inspect_zip_for_module(): - result = inspect_zip_for_module(str(f'{PLUGINS_TEST_DIR}/{PLUGIN_TEST_ZIP_FILE}')) + result = inspect_zip_for_module(str(f"{PLUGINS_TEST_DIR}/{PLUGIN_TEST_ZIP_FILE}")) assert result == PLUGIN_TEST_INIT_PY @@ -27,37 +27,56 @@ def mock_config_blacklist_whitelist_check(): return MockConfig() -def test_blacklist_whitelist_check_blacklist(mock_config_blacklist_whitelist_check, - monkeypatch): +def test_blacklist_whitelist_check_blacklist( + mock_config_blacklist_whitelist_check, monkeypatch +): monkeypatch.setattr("builtins.input", lambda _: "y") - assert not blacklist_whitelist_check("BadPlugin", mock_config_blacklist_whitelist_check) + assert not blacklist_whitelist_check( + "BadPlugin", mock_config_blacklist_whitelist_check + ) -def test_blacklist_whitelist_check_whitelist(mock_config_blacklist_whitelist_check, monkeypatch): +def test_blacklist_whitelist_check_whitelist( + mock_config_blacklist_whitelist_check, monkeypatch +): monkeypatch.setattr("builtins.input", lambda _: "y") - assert blacklist_whitelist_check("GoodPlugin", mock_config_blacklist_whitelist_check) + assert blacklist_whitelist_check( + "GoodPlugin", mock_config_blacklist_whitelist_check + ) -def test_blacklist_whitelist_check_user_input_yes(mock_config_blacklist_whitelist_check, monkeypatch): +def test_blacklist_whitelist_check_user_input_yes( + mock_config_blacklist_whitelist_check, monkeypatch +): monkeypatch.setattr("builtins.input", lambda _: "y") - assert blacklist_whitelist_check("UnknownPlugin", mock_config_blacklist_whitelist_check) + assert blacklist_whitelist_check( + "UnknownPlugin", mock_config_blacklist_whitelist_check + ) -def test_blacklist_whitelist_check_user_input_no(mock_config_blacklist_whitelist_check, monkeypatch): +def test_blacklist_whitelist_check_user_input_no( + mock_config_blacklist_whitelist_check, monkeypatch +): monkeypatch.setattr("builtins.input", lambda _: "n") - assert not blacklist_whitelist_check("UnknownPlugin", mock_config_blacklist_whitelist_check) + assert not blacklist_whitelist_check( + "UnknownPlugin", mock_config_blacklist_whitelist_check + ) -def test_blacklist_whitelist_check_user_input_invalid(mock_config_blacklist_whitelist_check, monkeypatch): +def test_blacklist_whitelist_check_user_input_invalid( + mock_config_blacklist_whitelist_check, monkeypatch +): monkeypatch.setattr("builtins.input", lambda _: "invalid") - assert not blacklist_whitelist_check("UnknownPlugin", mock_config_blacklist_whitelist_check) + assert not blacklist_whitelist_check( + "UnknownPlugin", mock_config_blacklist_whitelist_check + ) @pytest.fixture def config_with_plugins(): cfg = Config() cfg.plugins_dir = PLUGINS_TEST_DIR - cfg.plugins_openai = ['https://weathergpt.vercel.app/'] + cfg.plugins_openai = ["https://weathergpt.vercel.app/"] return cfg From c5b81b5e10d8623fe00ba5a015804e3b3b404ff6 Mon Sep 17 00:00:00 2001 From: riensen Date: Wed, 19 Apr 2023 12:48:51 +0200 Subject: [PATCH 55/60] Adding Allowlisted Plugins via env --- .env.template | 7 +++++++ README.md | 6 +++++- autogpt/config/config.py | 8 +++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.env.template b/.env.template index 855cb91f..b10e4799 100644 --- a/.env.template +++ b/.env.template @@ -175,3 +175,10 @@ TW_CONSUMER_KEY= TW_CONSUMER_SECRET= TW_ACCESS_TOKEN= TW_ACCESS_TOKEN_SECRET= + +################################################################################ +### ALLOWLISTED PLUGINS +################################################################################ + +#ALLOWLISTED_PLUGINS - Sets the listed plugins that are allowed (Example: plugin1,plugin2,plugin3) +ALLOWLISTED_PLUGINS= \ No newline at end of file diff --git a/README.md b/README.md index ad6d1994..fb3b7df6 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,11 @@ Drop the repo's zipfile in the plugins folder. ![Download Zip](https://raw.githubusercontent.com/BillSchumacher/Auto-GPT/master/plugin.png) -If you add the plugins class name to the whitelist in the config.py you will not be prompted otherwise you'll be warned before loading the plugin. +If you add the plugins class name to the `ALLOWLISTED_PLUGINS` in the `.env` you will not be prompted otherwise you'll be warned before loading the plugin: + +``` +ALLOWLISTED_PLUGINS=example-plugin1,example-plugin2,example-plugin3 +``` ## Setting Your Cache Type diff --git a/autogpt/config/config.py b/autogpt/config/config.py index af1bcf0d..2fadcaba 100644 --- a/autogpt/config/config.py +++ b/autogpt/config/config.py @@ -116,7 +116,13 @@ class Config(metaclass=Singleton): self.plugins_dir = os.getenv("PLUGINS_DIR", "plugins") self.plugins: List[AutoGPTPluginTemplate] = [] self.plugins_openai = [] - self.plugins_whitelist = [] + + plugins_allowlist = os.getenv("ALLOWLISTED_PLUGINS") + if plugins_allowlist: + plugins_allowlist=plugins_allowlist.split(",") + self.plugins_whitelist = plugins_allowlist + else: + self.plugins_whitelist = [] self.plugins_blacklist = [] def get_azure_deployment_id_for_model(self, model: str) -> str: From d7679d755f18b7576dc8711d815bdc67e13cf074 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Wed, 19 Apr 2023 18:17:04 -0500 Subject: [PATCH 56/60] Fix all commands and cleanup --- autogpt/app.py | 48 +++------------------------ autogpt/cli.py | 3 +- autogpt/commands/analyze_code.py | 6 ++++ autogpt/commands/audio_text.py | 6 ++-- autogpt/commands/command.py | 4 +-- autogpt/commands/execute_code.py | 19 +++++++---- autogpt/commands/file_operations.py | 12 ++++--- autogpt/commands/git_operations.py | 10 +++--- autogpt/commands/google_search.py | 4 +-- autogpt/commands/twitter.py | 2 +- autogpt/config/ai_config.py | 5 +-- autogpt/llm_utils.py | 4 ++- autogpt/models/base_open_ai_plugin.py | 14 ++++---- autogpt/prompts/prompt.py | 4 ++- autogpt/utils.py | 2 +- requirements.txt | 3 -- 16 files changed, 63 insertions(+), 83 deletions(-) diff --git a/autogpt/app.py b/autogpt/app.py index cf1ec5f9..7c24f701 100644 --- a/autogpt/app.py +++ b/autogpt/app.py @@ -3,33 +3,9 @@ import json from typing import Dict, List, NoReturn, Union from autogpt.agent.agent_manager import AgentManager -from autogpt.commands.analyze_code import analyze_code -from autogpt.commands.audio_text import read_audio_from_file from autogpt.commands.command import CommandRegistry, command -from autogpt.commands.evaluate_code import evaluate_code -from autogpt.commands.execute_code import ( - execute_python_file, - execute_shell, - execute_shell_popen, -) -from autogpt.commands.file_operations import ( - append_to_file, - delete_file, - download_file, - read_file, - search_files, - write_to_file, -) -from autogpt.commands.git_operations import clone_repository -from autogpt.commands.google_search import google_official_search, google_search -from autogpt.commands.image_gen import generate_image -from autogpt.commands.improve_code import improve_code -from autogpt.commands.twitter import send_tweet from autogpt.commands.web_requests import scrape_links, scrape_text -from autogpt.commands.web_selenium import browse_website -from autogpt.commands.write_tests import write_tests from autogpt.config import Config -from autogpt.json_utils.json_fix_llm import fix_and_parse_json from autogpt.memory import get_memory from autogpt.processing.text import summarize_text from autogpt.prompts.generator import PromptGenerator @@ -137,26 +113,8 @@ def execute_command( command_name = map_command_synonyms(command_name.lower()) if command_name == "memory_add": - return memory.add(arguments["string"]) - elif command_name == "get_text_summary": - return get_text_summary(arguments["url"], arguments["question"]) - elif command_name == "get_hyperlinks": - return get_hyperlinks(arguments["url"]) - elif command_name == "analyze_code": - return analyze_code(arguments["code"]) - elif command_name == "download_file": - if not CFG.allow_downloads: - return "Error: You do not have user authorization to download files locally." - return download_file(arguments["url"], arguments["file"]) - elif command_name == "execute_shell_popen": - if CFG.execute_local_commands: - return execute_shell_popen(arguments["command_line"]) - else: - return ( - "You are not allowed to run local shell commands. To execute" - " shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' " - "in your config. Do not attempt to bypass the restriction." - ) + return get_memory(CFG).add(arguments["string"]) + # TODO: Change these to take in a file rather than pasted code, if # non-file is given, return instructions "Input should be a python # filepath, write your code to file and try again @@ -177,6 +135,7 @@ def execute_command( return f"Error: {str(e)}" +@command("get_text_summary", "Get text summary", '"url": "", "question": ""') def get_text_summary(url: str, question: str) -> str: """Return the results of a Google search @@ -192,6 +151,7 @@ def get_text_summary(url: str, question: str) -> str: return f""" "Result" : {summary}""" +@command("get_hyperlinks", "Get text summary", '"url": ""') def get_hyperlinks(url: str) -> Union[str, List[str]]: """Return the results of a Google search diff --git a/autogpt/cli.py b/autogpt/cli.py index 30eb9c23..51a946a7 100644 --- a/autogpt/cli.py +++ b/autogpt/cli.py @@ -130,13 +130,14 @@ def main( cfg.set_plugins(scan_plugins(cfg, cfg.debug_mode)) # Create a CommandRegistry instance and scan default folder command_registry = CommandRegistry() + command_registry.import_commands("autogpt.commands.analyze_code") command_registry.import_commands("autogpt.commands.audio_text") - command_registry.import_commands("autogpt.commands.evaluate_code") command_registry.import_commands("autogpt.commands.execute_code") command_registry.import_commands("autogpt.commands.file_operations") command_registry.import_commands("autogpt.commands.git_operations") command_registry.import_commands("autogpt.commands.google_search") command_registry.import_commands("autogpt.commands.image_gen") + command_registry.import_commands("autogpt.commands.improve_code") command_registry.import_commands("autogpt.commands.twitter") command_registry.import_commands("autogpt.commands.web_selenium") command_registry.import_commands("autogpt.commands.write_tests") diff --git a/autogpt/commands/analyze_code.py b/autogpt/commands/analyze_code.py index e02ea4c5..b87d73e1 100644 --- a/autogpt/commands/analyze_code.py +++ b/autogpt/commands/analyze_code.py @@ -1,9 +1,15 @@ """Code evaluation module.""" from __future__ import annotations +from autogpt.commands.command import command from autogpt.llm_utils import call_ai_function +@command( + "analyze_code", + "Analyze Code", + '"code": ""', +) def analyze_code(code: str) -> list[str]: """ A function that takes in a string and returns a response from create chat diff --git a/autogpt/commands/audio_text.py b/autogpt/commands/audio_text.py index 421a1f18..b409fefd 100644 --- a/autogpt/commands/audio_text.py +++ b/autogpt/commands/audio_text.py @@ -13,11 +13,11 @@ CFG = Config() @command( "read_audio_from_file", "Convert Audio to text", - '"file": ""', + '"filename": ""', CFG.huggingface_audio_to_text_model, "Configure huggingface_audio_to_text_model.", ) -def read_audio_from_file(audio_path: str) -> str: +def read_audio_from_file(filename: str) -> str: """ Convert audio to text. @@ -27,7 +27,7 @@ def read_audio_from_file(audio_path: str) -> str: Returns: str: The text from the audio """ - audio_path = path_in_workspace(audio_path) + audio_path = path_in_workspace(filename) with open(audio_path, "rb") as audio_file: audio = audio_file.read() return read_audio(audio) diff --git a/autogpt/commands/command.py b/autogpt/commands/command.py index f21b1b52..26fa445b 100644 --- a/autogpt/commands/command.py +++ b/autogpt/commands/command.py @@ -20,7 +20,7 @@ class Command: name: str, description: str, method: Callable[..., Any], - signature: str = None, + signature: str = '', enabled: bool = True, disabled_reason: Optional[str] = None, ): @@ -126,7 +126,7 @@ class CommandRegistry: def command( name: str, description: str, - signature: str = None, + signature: str = '', enabled: bool = True, disabled_reason: Optional[str] = None, ) -> Callable[..., Any]: diff --git a/autogpt/commands/execute_code.py b/autogpt/commands/execute_code.py index 5dd56605..ff35d428 100644 --- a/autogpt/commands/execute_code.py +++ b/autogpt/commands/execute_code.py @@ -12,17 +12,17 @@ from autogpt.workspace import WORKSPACE_PATH, path_in_workspace CFG = Config() -@command("execute_python_file", "Execute Python File", '"file": ""') -def execute_python_file(file: str) -> str: +@command("execute_python_file", "Execute Python File", '"filename": ""') +def execute_python_file(filename: str) -> str: """Execute a Python file in a Docker container and return the output Args: - file (str): The name of the file to execute + filename (str): The name of the file to execute Returns: str: The output of the file """ - + file = filename print(f"Executing file '{file}' in workspace '{WORKSPACE_PATH}'") if not file.endswith(".py"): @@ -138,9 +138,16 @@ def execute_shell(command_line: str) -> str: os.chdir(current_dir) - return output - +@command( + "execute_shell_popen", + "Execute Shell Command, non-interactive commands only", + '"command_line": ""', + CFG.execute_local_commands, + "You are not allowed to run local shell commands. To execute" + " shell commands, EXECUTE_LOCAL_COMMANDS must be set to 'True' " + "in your config. Do not attempt to bypass the restriction.", +) def execute_shell_popen(command_line) -> str: """Execute a shell command with Popen and returns an english description of the event and the process id diff --git a/autogpt/commands/file_operations.py b/autogpt/commands/file_operations.py index 28ff0e52..d8f8e664 100644 --- a/autogpt/commands/file_operations.py +++ b/autogpt/commands/file_operations.py @@ -9,11 +9,13 @@ import requests from colorama import Back, Fore from requests.adapters import HTTPAdapter, Retry +from autogpt.config import Config from autogpt.commands.command import command from autogpt.spinner import Spinner from autogpt.utils import readable_file_size from autogpt.workspace import WORKSPACE_PATH, path_in_workspace +CFG = Config() LOG_FILE = "file_logger.txt" LOG_FILE_PATH = WORKSPACE_PATH / LOG_FILE @@ -82,7 +84,7 @@ def split_file( start += max_length - overlap -@command("read_file", "Read file", '"file": ""') +@command("read_file", "Read file", '"filename": ""') def read_file(filename: str) -> str: """Read a file and return the contents @@ -135,7 +137,7 @@ def ingest_file( print(f"Error while ingesting file '{filename}': {str(e)}") -@command("write_to_file", "Write to file", '"file": "", "text": ""') +@command("write_to_file", "Write to file", '"filename": "", "text": ""') def write_to_file(filename: str, text: str) -> str: """Write text to a file @@ -161,7 +163,7 @@ def write_to_file(filename: str, text: str) -> str: return f"Error: {str(e)}" -@command("append_to_file", "Append to file", '"file": "", "text": ""') +@command("append_to_file", "Append to file", '"filename": "", "text": ""') def append_to_file(filename: str, text: str, shouldLog: bool = True) -> str: """Append text to a file @@ -185,7 +187,7 @@ def append_to_file(filename: str, text: str, shouldLog: bool = True) -> str: return f"Error: {str(e)}" -@command("delete_file", "Delete file", '"file": ""') +@command("delete_file", "Delete file", '"filename": ""') def delete_file(filename: str) -> str: """Delete a file @@ -233,6 +235,8 @@ def search_files(directory: str) -> list[str]: return found_files + +@command("download_file", "Search Files", '"url": "", "filename": ""', CFG.allow_downloads, "Error: You do not have user authorization to download files locally.") def download_file(url, filename): """Downloads a file Args: diff --git a/autogpt/commands/git_operations.py b/autogpt/commands/git_operations.py index 0536152b..1fb99e5b 100644 --- a/autogpt/commands/git_operations.py +++ b/autogpt/commands/git_operations.py @@ -11,24 +11,24 @@ CFG = Config() @command( "clone_repository", "Clone Repositoryy", - '"repository_url": "", "clone_path": ""', + '"repository_url": "", "clone_path": ""', CFG.github_username and CFG.github_api_key, "Configure github_username and github_api_key.", ) -def clone_repository(repo_url: str, clone_path: str) -> str: +def clone_repository(repository_url: str, clone_path: str) -> str: """Clone a GitHub repository locally Args: - repo_url (str): The URL of the repository to clone + repository_url (str): The URL of the repository to clone clone_path (str): The path to clone the repository to Returns: str: The result of the clone operation""" - split_url = repo_url.split("//") + split_url = repository_url.split("//") auth_repo_url = f"//{CFG.github_username}:{CFG.github_api_key}@".join(split_url) safe_clone_path = path_in_workspace(clone_path) try: Repo.clone_from(auth_repo_url, safe_clone_path) - return f"""Cloned {repo_url} to {safe_clone_path}""" + return f"""Cloned {repository_url} to {safe_clone_path}""" except Exception as e: return f"Error: {str(e)}" diff --git a/autogpt/commands/google_search.py b/autogpt/commands/google_search.py index 85eeae65..fcc1a9f4 100644 --- a/autogpt/commands/google_search.py +++ b/autogpt/commands/google_search.py @@ -11,7 +11,7 @@ from autogpt.config import Config CFG = Config() -@command("google", "Google Search", '"query": ""', not CFG.google_api_key) +@command("google", "Google Search", '"query": ""', not CFG.google_api_key) def google_search(query: str, num_results: int = 8) -> str: """Return the results of a Google search @@ -40,7 +40,7 @@ def google_search(query: str, num_results: int = 8) -> str: @command( "google", "Google Search", - '"query": ""', + '"query": ""', bool(CFG.google_api_key), "Configure google_api_key.", ) diff --git a/autogpt/commands/twitter.py b/autogpt/commands/twitter.py index 8e64b213..f0502271 100644 --- a/autogpt/commands/twitter.py +++ b/autogpt/commands/twitter.py @@ -12,7 +12,7 @@ load_dotenv() @command( "send_tweet", "Send Tweet", - '"text": ""', + '"tweet_text": ""', ) def send_tweet(tweet_text: str) -> str: """ diff --git a/autogpt/config/ai_config.py b/autogpt/config/ai_config.py index af387f0b..292978a4 100644 --- a/autogpt/config/ai_config.py +++ b/autogpt/config/ai_config.py @@ -12,6 +12,9 @@ import yaml from autogpt.prompts.generator import PromptGenerator +# Soon this will go in a folder where it remembers more stuff about the run(s) +SAVE_FILE = str(Path(os.getcwd()) / "ai_settings.yaml") + class AIConfig: """ @@ -44,8 +47,6 @@ class AIConfig: self.prompt_generator = None self.command_registry = None - # Soon this will go in a folder where it remembers more stuff about the run(s) - SAVE_FILE = Path(os.getcwd()) / "ai_settings.yaml" @staticmethod def load(config_file: str = SAVE_FILE) -> "AIConfig": diff --git a/autogpt/llm_utils.py b/autogpt/llm_utils.py index d26fb2d6..8b85959c 100644 --- a/autogpt/llm_utils.py +++ b/autogpt/llm_utils.py @@ -83,12 +83,14 @@ def create_chat_completion( temperature=temperature, max_tokens=max_tokens, ): - return plugin.handle_chat_completion( + message = plugin.handle_chat_completion( messages=messages, model=model, temperature=temperature, max_tokens=max_tokens, ) + if message is not None: + return message response = None for attempt in range(num_retries): backoff = 2 ** (attempt + 2) diff --git a/autogpt/models/base_open_ai_plugin.py b/autogpt/models/base_open_ai_plugin.py index 987e7aad..046295c0 100644 --- a/autogpt/models/base_open_ai_plugin.py +++ b/autogpt/models/base_open_ai_plugin.py @@ -34,7 +34,7 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate): def on_response(self, response: str, *args, **kwargs) -> str: """This method is called when a response is received from the model.""" - pass + return response def can_handle_post_prompt(self) -> bool: """This method is called to check that the plugin can @@ -51,7 +51,7 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate): Returns: PromptGenerator: The prompt generator. """ - pass + return prompt def can_handle_on_planning(self) -> bool: """This method is called to check that the plugin can @@ -84,7 +84,7 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate): Returns: str: The resulting response. """ - pass + return response def can_handle_pre_instruction(self) -> bool: """This method is called to check that the plugin can @@ -100,7 +100,7 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate): Returns: List[Message]: The resulting list of messages. """ - pass + return messages def can_handle_on_instruction(self) -> bool: """This method is called to check that the plugin can @@ -132,7 +132,7 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate): Returns: str: The resulting response. """ - pass + return response def can_handle_pre_command(self) -> bool: """This method is called to check that the plugin can @@ -151,7 +151,7 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate): Returns: Tuple[str, Dict[str, Any]]: The command name and the arguments. """ - pass + return command_name, arguments def can_handle_post_command(self) -> bool: """This method is called to check that the plugin can @@ -168,7 +168,7 @@ class BaseOpenAIPlugin(AutoGPTPluginTemplate): Returns: str: The resulting response. """ - pass + return response def can_handle_chat_completion( self, messages: Dict[Any, Any], model: str, temperature: float, max_tokens: int diff --git a/autogpt/prompts/prompt.py b/autogpt/prompts/prompt.py index ba04263e..e25ea745 100644 --- a/autogpt/prompts/prompt.py +++ b/autogpt/prompts/prompt.py @@ -71,7 +71,9 @@ def build_default_prompt_generator() -> PromptGenerator: "Every command has a cost, so be smart and efficient. Aim to complete tasks in" " the least number of steps." ) - + prompt_generator.add_performance_evaluation( + "Write all code to a file." + ) return prompt_generator diff --git a/autogpt/utils.py b/autogpt/utils.py index e93d5ac7..dffd0662 100644 --- a/autogpt/utils.py +++ b/autogpt/utils.py @@ -3,7 +3,7 @@ import os import requests import yaml from colorama import Fore -from git import Repo +from git.repo import Repo def clean_input(prompt: str = ""): diff --git a/requirements.txt b/requirements.txt index b504c684..2052a9ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,6 @@ pre-commit black isort gitpython==3.1.31 -abstract-singleton auto-gpt-plugin-template # Items below this point will not be included in the Docker Image @@ -48,5 +47,3 @@ pytest-mock # OpenAI and Generic plugins import openapi-python-client==0.13.4 -abstract-singleton -auto-gpt-plugin-template From 16f0e22ffa4958cf5d6e163ca3ef2dce883e5afd Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Wed, 19 Apr 2023 18:21:03 -0500 Subject: [PATCH 57/60] linting --- autogpt/app.py | 4 +++- autogpt/commands/command.py | 4 ++-- autogpt/commands/file_operations.py | 15 +++++++++++---- autogpt/config/ai_config.py | 1 - autogpt/config/config.py | 2 +- autogpt/prompts/prompt.py | 4 +--- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/autogpt/app.py b/autogpt/app.py index 7c24f701..cf8e29a3 100644 --- a/autogpt/app.py +++ b/autogpt/app.py @@ -135,7 +135,9 @@ def execute_command( return f"Error: {str(e)}" -@command("get_text_summary", "Get text summary", '"url": "", "question": ""') +@command( + "get_text_summary", "Get text summary", '"url": "", "question": ""' +) def get_text_summary(url: str, question: str) -> str: """Return the results of a Google search diff --git a/autogpt/commands/command.py b/autogpt/commands/command.py index 26fa445b..e97af008 100644 --- a/autogpt/commands/command.py +++ b/autogpt/commands/command.py @@ -20,7 +20,7 @@ class Command: name: str, description: str, method: Callable[..., Any], - signature: str = '', + signature: str = "", enabled: bool = True, disabled_reason: Optional[str] = None, ): @@ -126,7 +126,7 @@ class CommandRegistry: def command( name: str, description: str, - signature: str = '', + signature: str = "", enabled: bool = True, disabled_reason: Optional[str] = None, ) -> Callable[..., Any]: diff --git a/autogpt/commands/file_operations.py b/autogpt/commands/file_operations.py index d8f8e664..b73fb987 100644 --- a/autogpt/commands/file_operations.py +++ b/autogpt/commands/file_operations.py @@ -9,8 +9,8 @@ import requests from colorama import Back, Fore from requests.adapters import HTTPAdapter, Retry -from autogpt.config import Config from autogpt.commands.command import command +from autogpt.config import Config from autogpt.spinner import Spinner from autogpt.utils import readable_file_size from autogpt.workspace import WORKSPACE_PATH, path_in_workspace @@ -163,7 +163,9 @@ def write_to_file(filename: str, text: str) -> str: return f"Error: {str(e)}" -@command("append_to_file", "Append to file", '"filename": "", "text": ""') +@command( + "append_to_file", "Append to file", '"filename": "", "text": ""' +) def append_to_file(filename: str, text: str, shouldLog: bool = True) -> str: """Append text to a file @@ -235,8 +237,13 @@ def search_files(directory: str) -> list[str]: return found_files - -@command("download_file", "Search Files", '"url": "", "filename": ""', CFG.allow_downloads, "Error: You do not have user authorization to download files locally.") +@command( + "download_file", + "Search Files", + '"url": "", "filename": ""', + CFG.allow_downloads, + "Error: You do not have user authorization to download files locally.", +) def download_file(url, filename): """Downloads a file Args: diff --git a/autogpt/config/ai_config.py b/autogpt/config/ai_config.py index 292978a4..1e48ab4d 100644 --- a/autogpt/config/ai_config.py +++ b/autogpt/config/ai_config.py @@ -47,7 +47,6 @@ class AIConfig: self.prompt_generator = None self.command_registry = None - @staticmethod def load(config_file: str = SAVE_FILE) -> "AIConfig": """ diff --git a/autogpt/config/config.py b/autogpt/config/config.py index 1ee8eabf..801df2bb 100644 --- a/autogpt/config/config.py +++ b/autogpt/config/config.py @@ -131,7 +131,7 @@ class Config(metaclass=Singleton): plugins_allowlist = os.getenv("ALLOWLISTED_PLUGINS") if plugins_allowlist: - plugins_allowlist=plugins_allowlist.split(",") + plugins_allowlist = plugins_allowlist.split(",") self.plugins_whitelist = plugins_allowlist else: self.plugins_whitelist = [] diff --git a/autogpt/prompts/prompt.py b/autogpt/prompts/prompt.py index e25ea745..79de04ea 100644 --- a/autogpt/prompts/prompt.py +++ b/autogpt/prompts/prompt.py @@ -71,9 +71,7 @@ def build_default_prompt_generator() -> PromptGenerator: "Every command has a cost, so be smart and efficient. Aim to complete tasks in" " the least number of steps." ) - prompt_generator.add_performance_evaluation( - "Write all code to a file." - ) + prompt_generator.add_performance_evaluation("Write all code to a file.") return prompt_generator From d876de0befab77c9b9056f16b82e86e411ee7296 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Wed, 19 Apr 2023 18:32:49 -0500 Subject: [PATCH 58/60] Make tests a bit spicier and fix, maybe. --- .../unit/models/test_base_open_api_plugin.py | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/unit/models/test_base_open_api_plugin.py b/tests/unit/models/test_base_open_api_plugin.py index 3cfb3465..88434b30 100644 --- a/tests/unit/models/test_base_open_api_plugin.py +++ b/tests/unit/models/test_base_open_api_plugin.py @@ -55,13 +55,23 @@ def test_dummy_plugin_default_methods(dummy_plugin): 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.on_response("hello") == "hello" 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.post_planning("world") == "world" + pre_instruction = dummy_plugin.pre_instruction([{"role": "system", "content": "Beep, bop, boop"}]) + assert isinstance(pre_instruction, list) + assert len(pre_instruction) == 1 + assert pre_instruction[0]["role"] == "system" + assert pre_instruction[0]["content"] == "Beep, bop, boop" 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.post_instruction("I'm a robot") == "I'm a robot" + pre_command = dummy_plugin.pre_command("evolve", {"continuously": True}) + assert isinstance(pre_command, tuple) + assert len(pre_command) == 2 + assert pre_command[0] == "evolve" + assert pre_command[1]["continuously"] == True + post_command = dummy_plugin.post_command("evolve", "upgraded successfully!") + assert isinstance(post_command, str) + assert post_command == "upgraded successfully!" assert dummy_plugin.handle_chat_completion(None, None, None, None) is None From d8fd834142315f01d4704a1bb9851eae73713897 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Wed, 19 Apr 2023 18:34:38 -0500 Subject: [PATCH 59/60] linting --- tests/unit/models/test_base_open_api_plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/models/test_base_open_api_plugin.py b/tests/unit/models/test_base_open_api_plugin.py index 88434b30..950a3266 100644 --- a/tests/unit/models/test_base_open_api_plugin.py +++ b/tests/unit/models/test_base_open_api_plugin.py @@ -59,7 +59,9 @@ def test_dummy_plugin_default_methods(dummy_plugin): assert dummy_plugin.post_prompt(None) is None assert dummy_plugin.on_planning(None, None) is None assert dummy_plugin.post_planning("world") == "world" - pre_instruction = dummy_plugin.pre_instruction([{"role": "system", "content": "Beep, bop, boop"}]) + pre_instruction = dummy_plugin.pre_instruction( + [{"role": "system", "content": "Beep, bop, boop"}] + ) assert isinstance(pre_instruction, list) assert len(pre_instruction) == 1 assert pre_instruction[0]["role"] == "system" From c7316754436990972a8eb1139f860b2c35066d66 Mon Sep 17 00:00:00 2001 From: BillSchumacher <34168009+BillSchumacher@users.noreply.github.com> Date: Wed, 19 Apr 2023 18:45:29 -0500 Subject: [PATCH 60/60] Fix url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f5fea44d..85c4019d 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ export CUSTOM_SEARCH_ENGINE_ID="YOUR_CUSTOM_SEARCH_ENGINE_ID" ## Plugins -See https://github.com/Torantulino/Auto-GPT-Plugin-Template for the template of the plugins. +See https://github.com/Significant-Gravitas/Auto-GPT-Plugin-Template for the template of the plugins. ⚠️💀 WARNING 💀⚠️: Review the code of any plugin you use, this allows for any Python to be executed and do malicious things. Like stealing your API keys.