diff --git a/agbenchmark/benchmarks.py b/agbenchmark/benchmarks.py index b3df802092c..6a646f37049 100644 --- a/agbenchmark/benchmarks.py +++ b/agbenchmark/benchmarks.py @@ -1,3 +1,4 @@ +import os import sys from pathlib import Path from typing import Tuple @@ -25,7 +26,7 @@ def bootstrap_agent(task, continuous_mode) -> Agent: config.continuous_mode = continuous_mode config.temperature = 0 config.plain_output = True - command_registry = get_command_registry(config) + command_registry = CommandRegistry.with_command_modules(COMMAND_CATEGORIES, config) config.memory_backend = "no_memory" config.workspace_path = Workspace.init_workspace_directory(config) config.file_logger_path = Workspace.build_file_logger_path(config.workspace_path) @@ -44,16 +45,6 @@ def bootstrap_agent(task, continuous_mode) -> Agent: ) -def get_command_registry(config: Config): - command_registry = CommandRegistry() - enabled_command_categories = [ - x for x in COMMAND_CATEGORIES if x not in config.disabled_command_categories - ] - for command_category in enabled_command_categories: - command_registry.import_commands(command_category) - return command_registry - - if __name__ == "__main__": # The first argument is the script name itself, second is the task if len(sys.argv) != 2: diff --git a/autogpt/agents/agent.py b/autogpt/agents/agent.py index 563c682385c..fa20ea587f1 100644 --- a/autogpt/agents/agent.py +++ b/autogpt/agents/agent.py @@ -293,10 +293,10 @@ def execute_command( # Handle non-native commands (e.g. from plugins) for command in agent.ai_config.prompt_generator.commands: if ( - command_name == command["label"].lower() - or command_name == command["name"].lower() + command_name == command.label.lower() + or command_name == command.name.lower() ): - return command["function"](**arguments) + return command.function(**arguments) raise RuntimeError( f"Cannot execute '{command_name}': unknown command." diff --git a/autogpt/agents/base.py b/autogpt/agents/base.py index e6b24be12ba..bf43b376982 100644 --- a/autogpt/agents/base.py +++ b/autogpt/agents/base.py @@ -1,7 +1,8 @@ from __future__ import annotations +import re from abc import ABCMeta, abstractmethod -from typing import TYPE_CHECKING, Any, Optional +from typing import TYPE_CHECKING, Any, Literal, Optional if TYPE_CHECKING: from autogpt.config import AIConfig, Config @@ -23,6 +24,8 @@ class BaseAgent(metaclass=ABCMeta): """Base class for all Auto-GPT agents.""" + ThoughtProcessID = Literal["one-shot"] + def __init__( self, ai_config: AIConfig, @@ -91,6 +94,7 @@ def __init__( def think( self, instruction: Optional[str] = None, + thought_process_id: ThoughtProcessID = "one-shot", ) -> tuple[CommandName | None, CommandArgs | None, AgentThoughts]: """Runs the agent for one cycle. @@ -103,8 +107,8 @@ def think( instruction = instruction or self.default_cycle_instruction - prompt: ChatSequence = self.construct_prompt(instruction) - prompt = self.on_before_think(prompt, instruction) + prompt: ChatSequence = self.construct_prompt(instruction, thought_process_id) + prompt = self.on_before_think(prompt, thought_process_id, instruction) raw_response = create_chat_completion( prompt, self.config, @@ -114,7 +118,7 @@ def think( ) self.cycle_count += 1 - return self.on_response(raw_response, prompt, instruction) + return self.on_response(raw_response, thought_process_id, prompt, instruction) @abstractmethod def execute( @@ -137,6 +141,7 @@ def execute( def construct_base_prompt( self, + thought_process_id: ThoughtProcessID, prepend_messages: list[Message] = [], append_messages: list[Message] = [], reserve_tokens: int = 0, @@ -178,7 +183,11 @@ def construct_base_prompt( return prompt - def construct_prompt(self, cycle_instruction: str) -> ChatSequence: + def construct_prompt( + self, + cycle_instruction: str, + thought_process_id: ThoughtProcessID, + ) -> ChatSequence: """Constructs and returns a prompt with the following structure: 1. System prompt 2. Message history of the agent, truncated & prepended with running summary as needed @@ -195,14 +204,86 @@ def construct_prompt(self, cycle_instruction: str) -> ChatSequence: cycle_instruction_tlength = count_message_tokens( cycle_instruction_msg, self.llm.name ) - prompt = self.construct_base_prompt(reserve_tokens=cycle_instruction_tlength) + + append_messages: list[Message] = [] + + response_format_instr = self.response_format_instruction(thought_process_id) + if response_format_instr: + append_messages.append(Message("system", response_format_instr)) + + prompt = self.construct_base_prompt( + thought_process_id, + append_messages=append_messages, + reserve_tokens=cycle_instruction_tlength, + ) # ADD user input message ("triggering prompt") prompt.append(cycle_instruction_msg) return prompt - def on_before_think(self, prompt: ChatSequence, instruction: str) -> ChatSequence: + # This can be expanded to support multiple types of (inter)actions within an agent + def response_format_instruction(self, thought_process_id: ThoughtProcessID) -> str: + if thought_process_id != "one-shot": + raise NotImplementedError(f"Unknown thought process '{thought_process_id}'") + + RESPONSE_FORMAT_WITH_COMMAND = """```ts + interface Response { + thoughts: { + // Thoughts + text: string; + reasoning: string; + // Short markdown-style bullet list that conveys the long-term plan + plan: string; + // Constructive self-criticism + criticism: string; + // Summary of thoughts to say to the user + speak: string; + }; + command: { + name: string; + args: Record; + }; + } + ```""" + + RESPONSE_FORMAT_WITHOUT_COMMAND = """```ts + interface Response { + thoughts: { + // Thoughts + text: string; + reasoning: string; + // Short markdown-style bullet list that conveys the long-term plan + plan: string; + // Constructive self-criticism + criticism: string; + // Summary of thoughts to say to the user + speak: string; + }; + } + ```""" + + response_format = re.sub( + r"\n\s+", + "\n", + RESPONSE_FORMAT_WITHOUT_COMMAND + if self.config.openai_functions + else RESPONSE_FORMAT_WITH_COMMAND, + ) + + use_functions = self.config.openai_functions and self.command_registry.commands + return ( + f"Respond strictly with JSON{', and also specify a command to use through a function_call' if use_functions else ''}. " + "The JSON should be compatible with the TypeScript type `Response` from the following:\n" + f"{response_format}\n" + ) + + def on_before_think( + self, + prompt: ChatSequence, + thought_process_id: ThoughtProcessID, + instruction: str, + ) -> ChatSequence: """Called after constructing the prompt but before executing it. Calls the `on_planning` hook of any enabled and capable plugins, adding their @@ -237,7 +318,11 @@ def on_before_think(self, prompt: ChatSequence, instruction: str) -> ChatSequenc return prompt def on_response( - self, llm_response: ChatModelResponse, prompt: ChatSequence, instruction: str + self, + llm_response: ChatModelResponse, + thought_process_id: ThoughtProcessID, + prompt: ChatSequence, + instruction: str, ) -> tuple[CommandName | None, CommandArgs | None, AgentThoughts]: """Called upon receiving a response from the chat model. @@ -260,7 +345,9 @@ def on_response( ) # FIXME: support function calls try: - return self.parse_and_process_response(llm_response, prompt, instruction) + return self.parse_and_process_response( + llm_response, thought_process_id, prompt, instruction + ) except SyntaxError as e: logger.error(f"Response could not be parsed: {e}") # TODO: tune this message @@ -275,7 +362,11 @@ def on_response( @abstractmethod def parse_and_process_response( - self, llm_response: ChatModelResponse, prompt: ChatSequence, instruction: str + self, + llm_response: ChatModelResponse, + thought_process_id: ThoughtProcessID, + prompt: ChatSequence, + instruction: str, ) -> tuple[CommandName | None, CommandArgs | None, AgentThoughts]: """Validate, parse & process the LLM's response. diff --git a/autogpt/app/main.py b/autogpt/app/main.py index d73a511d21b..f8ac3ca4b0a 100644 --- a/autogpt/app/main.py +++ b/autogpt/app/main.py @@ -134,36 +134,9 @@ def run_auto_gpt( config.file_logger_path = Workspace.build_file_logger_path(config.workspace_path) config.plugins = scan_plugins(config, config.debug_mode) - # Create a CommandRegistry instance and scan default folder - command_registry = CommandRegistry() - - logger.debug( - f"The following command categories are disabled: {config.disabled_command_categories}" - ) - enabled_command_categories = [ - x for x in COMMAND_CATEGORIES if x not in config.disabled_command_categories - ] - logger.debug( - f"The following command categories are enabled: {enabled_command_categories}" - ) - - for command_category in enabled_command_categories: - command_registry.import_commands(command_category) - - # Unregister commands that are incompatible with the current config - incompatible_commands = [] - for command in command_registry.commands.values(): - if callable(command.enabled) and not command.enabled(config): - command.enabled = False - incompatible_commands.append(command) - - for command in incompatible_commands: - command_registry.unregister(command) - logger.debug( - f"Unregistering incompatible command: {command.name}, " - f"reason - {command.disabled_reason or 'Disabled by current config.'}" - ) + # Create a CommandRegistry instance and scan default folder + command_registry = CommandRegistry.with_command_modules(COMMAND_CATEGORIES, config) ai_config = construct_main_ai_config( config, diff --git a/autogpt/app/setup.py b/autogpt/app/setup.py index f2b52916cfe..cb6073adc0d 100644 --- a/autogpt/app/setup.py +++ b/autogpt/app/setup.py @@ -83,6 +83,7 @@ def prompt_user( "Falling back to manual mode.", speak_text=True, ) + logger.debug(f"Error during AIConfig generation: {e}") return generate_aiconfig_manual(config) diff --git a/autogpt/commands/__init__.py b/autogpt/commands/__init__.py index 9a932b175f0..018f5b8fcfb 100644 --- a/autogpt/commands/__init__.py +++ b/autogpt/commands/__init__.py @@ -3,5 +3,5 @@ "autogpt.commands.file_operations", "autogpt.commands.web_search", "autogpt.commands.web_selenium", - "autogpt.commands.task_statuses", + "autogpt.commands.system", ] diff --git a/autogpt/commands/execute_code.py b/autogpt/commands/execute_code.py index dd35f859325..3d52eb0a58b 100644 --- a/autogpt/commands/execute_code.py +++ b/autogpt/commands/execute_code.py @@ -1,4 +1,8 @@ -"""Execute code in a Docker container""" +"""Commands to execute code""" + +COMMAND_CATEGORY = "execute_code" +COMMAND_CATEGORY_TITLE = "Execute Code" + import os import subprocess from pathlib import Path @@ -251,9 +255,9 @@ def execute_shell(command_line: str, agent: Agent) -> str: "execute_shell_popen", "Executes a Shell Command, non-interactive commands only", { - "query": { + "command_line": { "type": "string", - "description": "The search query", + "description": "The command line to execute", "required": True, } }, diff --git a/autogpt/commands/file_operations.py b/autogpt/commands/file_operations.py index adafe14edee..41da057e378 100644 --- a/autogpt/commands/file_operations.py +++ b/autogpt/commands/file_operations.py @@ -1,6 +1,10 @@ -"""File operations for AutoGPT""" +"""Commands to perform operations on files""" + from __future__ import annotations +COMMAND_CATEGORY = "file_operations" +COMMAND_CATEGORY_TITLE = "File Operations" + import contextlib import hashlib import os @@ -228,22 +232,6 @@ def write_to_file(filename: str, text: str, agent: Agent) -> str: return f"Error: {err}" -@command( - "append_to_file", - "Appends to a file", - { - "filename": { - "type": "string", - "description": "The name of the file to write to", - "required": True, - }, - "text": { - "type": "string", - "description": "The text to write to the file", - "required": True, - }, - }, -) @sanitize_path_arg("filename") def append_to_file( filename: str, text: str, agent: Agent, should_log: bool = True diff --git a/autogpt/commands/git_operations.py b/autogpt/commands/git_operations.py index 021157fbbd5..f7f8186be16 100644 --- a/autogpt/commands/git_operations.py +++ b/autogpt/commands/git_operations.py @@ -1,4 +1,7 @@ -"""Git operations for autogpt""" +"""Commands to perform Git operations""" + +COMMAND_CATEGORY = "git_operations" +COMMAND_CATEGORY_TITLE = "Git Operations" from git.repo import Repo diff --git a/autogpt/commands/image_gen.py b/autogpt/commands/image_gen.py index e02400a8189..3f6c1d98de4 100644 --- a/autogpt/commands/image_gen.py +++ b/autogpt/commands/image_gen.py @@ -1,4 +1,8 @@ -""" Image Generation Module for AutoGPT.""" +"""Commands to generate images based on text input""" + +COMMAND_CATEGORY = "text_to_image" +COMMAND_CATEGORY_TITLE = "Text to Image" + import io import json import time diff --git a/autogpt/commands/task_statuses.py b/autogpt/commands/system.py similarity index 87% rename from autogpt/commands/task_statuses.py rename to autogpt/commands/system.py index 34908928fee..08bfd5e57ea 100644 --- a/autogpt/commands/task_statuses.py +++ b/autogpt/commands/system.py @@ -1,6 +1,10 @@ -"""Task Statuses module.""" +"""Commands to control the internal state of the program""" + from __future__ import annotations +COMMAND_CATEGORY = "system" +COMMAND_CATEGORY_TITLE = "System" + from typing import NoReturn from autogpt.agents.agent import Agent diff --git a/autogpt/commands/web_search.py b/autogpt/commands/web_search.py index 9ea0d206116..49712049d47 100644 --- a/autogpt/commands/web_search.py +++ b/autogpt/commands/web_search.py @@ -1,6 +1,10 @@ -"""Google search command for Autogpt.""" +"""Commands to search the web with""" + from __future__ import annotations +COMMAND_CATEGORY = "web_search" +COMMAND_CATEGORY_TITLE = "Web Search" + import json import time from itertools import islice diff --git a/autogpt/commands/web_selenium.py b/autogpt/commands/web_selenium.py index 948d799e9c9..2d978494a9d 100644 --- a/autogpt/commands/web_selenium.py +++ b/autogpt/commands/web_selenium.py @@ -1,6 +1,10 @@ -"""Selenium web scraping module.""" +"""Commands for browsing a website""" + from __future__ import annotations +COMMAND_CATEGORY = "web_browse" +COMMAND_CATEGORY_TITLE = "Web Browsing" + import logging from pathlib import Path from sys import platform diff --git a/autogpt/config/ai_config.py b/autogpt/config/ai_config.py index b47740f6a8d..ce26e23dd48 100644 --- a/autogpt/config/ai_config.py +++ b/autogpt/config/ai_config.py @@ -1,7 +1,4 @@ -# sourcery skip: do-not-use-staticmethod -""" -A module that contains the AIConfig class object that contains the configuration -""" +"""A module that contains the AIConfig class object that contains the configuration""" from __future__ import annotations import platform @@ -15,6 +12,8 @@ from autogpt.models.command_registry import CommandRegistry from autogpt.prompts.generator import PromptGenerator + from .config import Config + class AIConfig: """ @@ -104,7 +103,7 @@ def save(self, ai_settings_file: str | Path) -> None: yaml.dump(config, file, allow_unicode=True) def construct_full_prompt( - self, config, prompt_generator: Optional[PromptGenerator] = None + self, config: Config, prompt_generator: Optional[PromptGenerator] = None ) -> str: """ Returns a prompt to the user with the class information in an organized fashion. @@ -117,26 +116,27 @@ def construct_full_prompt( including the ai_name, ai_role, ai_goals, and api_budget. """ - prompt_start = ( - "Your decisions must always be made independently without" - " seeking user assistance. Play to your strengths as an LLM and pursue" - " simple strategies with no legal complications." - "" - ) - from autogpt.prompts.prompt import build_default_prompt_generator + prompt_generator = prompt_generator or self.prompt_generator if prompt_generator is None: prompt_generator = build_default_prompt_generator(config) - 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 + prompt_generator.command_registry = self.command_registry + self.prompt_generator = prompt_generator + for plugin in config.plugins: if not plugin.can_handle_post_prompt(): continue prompt_generator = plugin.post_prompt(prompt_generator) + # Construct full prompt + full_prompt_parts = [ + f"You are {self.ai_name}, {self.ai_role.rstrip('.')}.", + "Your decisions must always be made independently without seeking " + "user assistance. Play to your strengths as an LLM and pursue " + "simple strategies with no legal complications.", + ] + if config.execute_local_commands: # add OS info to prompt os_name = platform.system() @@ -146,14 +146,30 @@ def construct_full_prompt( else distro.name(pretty=True) ) - prompt_start += f"\nThe OS you are running on is: {os_info}" + full_prompt_parts.append(f"The OS you are running on is: {os_info}") - # 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" + additional_constraints: list[str] = [] if self.api_budget > 0.0: - full_prompt += f"\nIt takes money to let you run. Your API budget is ${self.api_budget:.3f}" - self.prompt_generator = prompt_generator - full_prompt += f"\n\n{prompt_generator.generate_prompt_string(config)}" - return full_prompt + additional_constraints.append( + f"It takes money to let you run. " + f"Your API budget is ${self.api_budget:.3f}" + ) + + full_prompt_parts.append( + prompt_generator.generate_prompt_string( + additional_constraints=additional_constraints + ) + ) + + if self.ai_goals: + full_prompt_parts.append( + "\n".join( + [ + "## Goals", + "For your task, you must fulfill the following goals:", + *[f"{i+1}. {goal}" for i, goal in enumerate(self.ai_goals)], + ] + ) + ) + + return "\n\n".join(full_prompt_parts).strip("\n") diff --git a/autogpt/config/config.py b/autogpt/config/config.py index 66f2e871394..c0d30910239 100644 --- a/autogpt/config/config.py +++ b/autogpt/config/config.py @@ -54,7 +54,7 @@ class Config(SystemSettings, arbitrary_types_allowed=True): file_logger_path: Optional[Path] = None # Model configuration fast_llm: str = "gpt-3.5-turbo" - smart_llm: str = "gpt-4" + smart_llm: str = "gpt-4-0314" temperature: float = 0 openai_functions: bool = False embedding_model: str = "text-embedding-ada-002" diff --git a/autogpt/config/prompt_config.py b/autogpt/config/prompt_config.py index 793bb444043..055e7897b16 100644 --- a/autogpt/config/prompt_config.py +++ b/autogpt/config/prompt_config.py @@ -44,4 +44,4 @@ def __init__(self, prompt_settings_file: str) -> None: self.constraints = config_params.get("constraints", []) self.resources = config_params.get("resources", []) - self.performance_evaluations = config_params.get("performance_evaluations", []) + self.best_practices = config_params.get("best_practices", []) diff --git a/autogpt/core/runner/cli_web_app/server/api.py b/autogpt/core/runner/cli_web_app/server/api.py index 1ba0974b495..7a5ae9a74f5 100644 --- a/autogpt/core/runner/cli_web_app/server/api.py +++ b/autogpt/core/runner/cli_web_app/server/api.py @@ -6,7 +6,7 @@ from autogpt.agents import Agent from autogpt.app.main import UserFeedback from autogpt.commands import COMMAND_CATEGORIES -from autogpt.config import AIConfig, Config, ConfigBuilder +from autogpt.config import AIConfig, ConfigBuilder from autogpt.logs import logger from autogpt.memory.vector import get_memory from autogpt.models.command_registry import CommandRegistry @@ -85,7 +85,7 @@ def bootstrap_agent(task, continuous_mode) -> Agent: config.continuous_mode = continuous_mode config.temperature = 0 config.plain_output = True - command_registry = get_command_registry(config) + command_registry = CommandRegistry.with_command_modules(COMMAND_CATEGORIES, config) config.memory_backend = "no_memory" config.workspace_path = Workspace.init_workspace_directory(config) config.file_logger_path = Workspace.build_file_logger_path(config.workspace_path) @@ -102,13 +102,3 @@ def bootstrap_agent(task, continuous_mode) -> Agent: config=config, triggering_prompt=DEFAULT_TRIGGERING_PROMPT, ) - - -def get_command_registry(config: Config): - command_registry = CommandRegistry() - enabled_command_categories = [ - x for x in COMMAND_CATEGORIES if x not in config.disabled_command_categories - ] - for command_category in enabled_command_categories: - command_registry.import_commands(command_category) - return command_registry diff --git a/autogpt/llm/utils/__init__.py b/autogpt/llm/utils/__init__.py index e433476ec0b..5438bdd853c 100644 --- a/autogpt/llm/utils/__init__.py +++ b/autogpt/llm/utils/__init__.py @@ -119,7 +119,9 @@ def create_chat_completion( temperature = config.temperature if max_tokens is None: prompt_tlength = prompt.token_length - max_tokens = OPEN_AI_CHAT_MODELS[model].max_tokens - prompt_tlength + max_tokens = ( + OPEN_AI_CHAT_MODELS[model].max_tokens - prompt_tlength - 1 + ) # the -1 is just here because we have a bug and we don't know how to fix it. When using gpt-4-0314 we get a token error. logger.debug(f"Prompt length: {prompt_tlength} tokens") if functions: functions_tlength = count_openai_functions_tokens(functions, model) @@ -154,6 +156,9 @@ def create_chat_completion( function.schema for function in functions ] + # Print full prompt to debug log + logger.debug(prompt.dump()) + response = iopenai.create_chat_completion( messages=prompt.raw(), **chat_completion_kwargs, diff --git a/autogpt/models/command_registry.py b/autogpt/models/command_registry.py index f54f4adb503..9dfb35bd3ae 100644 --- a/autogpt/models/command_registry.py +++ b/autogpt/models/command_registry.py @@ -1,6 +1,13 @@ +from __future__ import annotations + import importlib import inspect -from typing import Any +from dataclasses import dataclass, field +from types import ModuleType +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from autogpt.config import Config from autogpt.command_decorator import AUTO_GPT_COMMAND_IDENTIFIER from autogpt.logs import logger @@ -18,9 +25,21 @@ class CommandRegistry: commands: dict[str, Command] commands_aliases: dict[str, Command] + # Alternative way to structure the registry; currently redundant with self.commands + categories: dict[str, CommandCategory] + + @dataclass + class CommandCategory: + name: str + title: str + description: str + commands: list[Command] = field(default_factory=list[Command]) + modules: list[ModuleType] = field(default_factory=list[ModuleType]) + def __init__(self): self.commands = {} self.commands_aliases = {} + self.categories = {} def __contains__(self, command_name: str): return command_name in self.commands or command_name in self.commands_aliases @@ -84,7 +103,41 @@ def command_prompt(self) -> str: ] return "\n".join(commands_list) - def import_commands(self, module_name: str) -> None: + @staticmethod + def with_command_modules(modules: list[str], config: Config) -> CommandRegistry: + new_registry = CommandRegistry() + + logger.debug( + f"The following command categories are disabled: {config.disabled_command_categories}" + ) + enabled_command_modules = [ + x for x in modules if x not in config.disabled_command_categories + ] + + logger.debug( + f"The following command categories are enabled: {enabled_command_modules}" + ) + + for command_module in enabled_command_modules: + new_registry.import_command_module(command_module) + + # Unregister commands that are incompatible with the current config + incompatible_commands: list[Command] = [] + for command in new_registry.commands.values(): + if callable(command.enabled) and not command.enabled(config): + command.enabled = False + incompatible_commands.append(command) + + for command in incompatible_commands: + new_registry.unregister(command) + logger.debug( + f"Unregistering incompatible command: {command.name}, " + f"reason - {command.disabled_reason or 'Disabled by current config.'}" + ) + + return new_registry + + def import_command_module(self, module_name: str) -> None: """ Imports the specified Python module containing command plugins. @@ -99,16 +152,42 @@ def import_commands(self, module_name: str) -> None: module = importlib.import_module(module_name) + category = self.register_module_category(module) + for attr_name in dir(module): attr = getattr(module, attr_name) + + command = None + # Register decorated functions - if hasattr(attr, AUTO_GPT_COMMAND_IDENTIFIER) and getattr( - attr, AUTO_GPT_COMMAND_IDENTIFIER - ): - self.register(attr.command) + if getattr(attr, AUTO_GPT_COMMAND_IDENTIFIER, False): + command = attr.command + # Register command classes elif ( inspect.isclass(attr) and issubclass(attr, Command) and attr != Command ): - cmd_instance = attr() - self.register(cmd_instance) + command = attr() + + if command: + self.register(command) + category.commands.append(command) + + def register_module_category(self, module: ModuleType) -> CommandCategory: + if not (category_name := getattr(module, "COMMAND_CATEGORY", None)): + raise ValueError(f"Cannot import invalid command module {module.__name__}") + + if category_name not in self.categories: + self.categories[category_name] = CommandRegistry.CommandCategory( + name=category_name, + title=getattr( + module, "COMMAND_CATEGORY_TITLE", category_name.capitalize() + ), + description=getattr(module, "__doc__", ""), + ) + + category = self.categories[category_name] + if module not in category.modules: + category.modules.append(module) + + return category diff --git a/autogpt/prompts/generator.py b/autogpt/prompts/generator.py index bc836f30c59..a8217953dbb 100644 --- a/autogpt/prompts/generator.py +++ b/autogpt/prompts/generator.py @@ -1,11 +1,8 @@ """ A module for generating custom prompt strings.""" from __future__ import annotations -import json -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, TypedDict - -from autogpt.config import Config -from autogpt.json_utils.utilities import llm_response_schema +from dataclasses import dataclass +from typing import TYPE_CHECKING, Callable, Optional if TYPE_CHECKING: from autogpt.models.command_registry import CommandRegistry @@ -17,34 +14,33 @@ class PromptGenerator: resources, and performance evaluations. """ - class Command(TypedDict): + @dataclass + class Command: label: str name: str params: dict[str, str] function: Optional[Callable] + def __str__(self) -> str: + """Returns a string representation of the command.""" + params_string = ", ".join( + f'"{key}": "{value}"' for key, value in self.params.items() + ) + return f'{self.label}: "{self.name}", params: ({params_string})' + constraints: list[str] commands: list[Command] resources: list[str] - performance_evaluation: list[str] + best_practices: list[str] command_registry: CommandRegistry | None - # TODO: replace with AIConfig - name: str - role: str - goals: list[str] - def __init__(self): self.constraints = [] self.commands = [] self.resources = [] - self.performance_evaluation = [] + self.best_practices = [] self.command_registry = None - self.name = "Bob" - self.role = "AI" - self.goals = [] - def add_constraint(self, constraint: str) -> None: """ Add a constraint to the constraints list. @@ -75,31 +71,15 @@ def add_command( function (callable, optional): A callable function to be called when the command is executed. Defaults to None. """ - command_params = {name: type for name, type in params.items()} - - command: PromptGenerator.Command = { - "label": command_label, - "name": command_name, - "params": command_params, - "function": function, - } - self.commands.append(command) - - def _generate_command_string(self, command: Dict[str, Any]) -> str: - """ - Generate a formatted string representation of a command. - - Args: - command (dict): A dictionary containing command information. - - Returns: - str: The formatted command string. - """ - params_string = ", ".join( - f'"{key}": "{value}"' for key, value in command["params"].items() + self.commands.append( + PromptGenerator.Command( + label=command_label, + name=command_name, + params={name: type for name, type in params.items()}, + function=function, + ) ) - return f'{command["label"]}: "{command["name"]}", params: {params_string}' def add_resource(self, resource: str) -> None: """ @@ -110,71 +90,67 @@ def add_resource(self, resource: str) -> None: """ self.resources.append(resource) - def add_performance_evaluation(self, evaluation: str) -> None: + def add_best_practice(self, best_practice: str) -> None: """ - Add a performance evaluation item to the performance_evaluation list. + Add an item to the list of best practices. Args: - evaluation (str): The evaluation item to be added. + best_practice (str): The best practice item to be added. """ - self.performance_evaluation.append(evaluation) + self.best_practices.append(best_practice) - def _generate_numbered_list(self, items: List[Any], item_type="list") -> str: + def _generate_numbered_list(self, items: list[str], start_at: int = 1) -> str: """ - Generate a numbered list from given items based on the item_type. + Generate a numbered list containing the given items. Args: items (list): A list of items to be numbered. - item_type (str, optional): The type of items in the list. - Defaults to 'list'. + start_at (int, optional): The number to start the sequence with; defaults to 1. Returns: str: The formatted numbered list. """ - if item_type == "command": - command_strings = [] - if self.command_registry: - command_strings += [ - str(item) - for item in self.command_registry.commands.values() - if item.enabled - ] - # terminate command is added manually - 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)) - - def generate_prompt_string(self, config: Config) -> str: + return "\n".join(f"{i}. {item}" for i, item in enumerate(items, start_at)) + + def generate_prompt_string( + self, + *, + additional_constraints: list[str] = [], + additional_resources: list[str] = [], + additional_best_practices: list[str] = [], + ) -> str: """ Generate a prompt string based on the constraints, commands, resources, - and performance evaluations. + and best practices. Returns: str: The generated prompt string. """ + return ( - f"Constraints:\n{self._generate_numbered_list(self.constraints)}\n\n" - f"{generate_commands(self, config)}" - f"Resources:\n{self._generate_numbered_list(self.resources)}\n\n" - "Performance Evaluation:\n" - f"{self._generate_numbered_list(self.performance_evaluation)}\n\n" - "Respond with only valid JSON conforming to the following schema: \n" - f"{json.dumps(llm_response_schema(config))}\n" + "## Constraints\n" + "You operate within the following constraints:\n" + f"{self._generate_numbered_list(self.constraints + additional_constraints)}\n\n" + "## Commands\n" + "You have access to the following commands:\n" + f"{self._generate_commands()}\n\n" + "## Resources\n" + "You can leverage access to the following resources:\n" + f"{self._generate_numbered_list(self.resources + additional_resources)}\n\n" + "## Best practices\n" + f"{self._generate_numbered_list(self.best_practices + additional_best_practices)}" ) + def _generate_commands(self) -> str: + command_strings = [] + if self.command_registry: + command_strings += [ + str(cmd) + for cmd in self.command_registry.commands.values() + if cmd.enabled + ] -def generate_commands(self, config: Config) -> str: - """ - Generate a prompt string based on the constraints, commands, resources, - and performance evaluations. + # Add commands from plugins etc. + command_strings += [str(cmd) for cmd in self.commands] - Returns: - str: The generated prompt string. - """ - if config.openai_functions: - return "" - return ( - "Commands:\n" - f"{self._generate_numbered_list(self.commands, item_type='command')}\n\n" - ) + return self._generate_numbered_list(command_strings) diff --git a/autogpt/prompts/prompt.py b/autogpt/prompts/prompt.py index b64f11f599a..627b6c50f18 100644 --- a/autogpt/prompts/prompt.py +++ b/autogpt/prompts/prompt.py @@ -2,13 +2,17 @@ from autogpt.config.prompt_config import PromptConfig from autogpt.prompts.generator import PromptGenerator -DEFAULT_TRIGGERING_PROMPT = "Determine exactly one command to use, and respond using the JSON schema specified previously:" +DEFAULT_TRIGGERING_PROMPT = ( + "Determine exactly one command to use based on the given goals " + "and the progress you have made so far, " + "and respond using the JSON schema specified previously:" +) def build_default_prompt_generator(config: Config) -> PromptGenerator: """ This function generates a prompt string that includes various constraints, - commands, resources, and performance evaluations. + commands, resources, and best practices. Returns: str: The generated prompt string. @@ -28,8 +32,8 @@ def build_default_prompt_generator(config: Config) -> PromptGenerator: for resource in prompt_config.resources: prompt_generator.add_resource(resource) - # Add performance evaluations to the PromptGenerator object - for performance_evaluation in prompt_config.performance_evaluations: - prompt_generator.add_performance_evaluation(performance_evaluation) + # Add best practices to the PromptGenerator object + for best_practice in prompt_config.best_practices: + prompt_generator.add_best_practice(best_practice) return prompt_generator diff --git a/benchmarks.py b/benchmarks.py index 9cf93acaef4..62f89662e51 100644 --- a/benchmarks.py +++ b/benchmarks.py @@ -22,7 +22,7 @@ def bootstrap_agent(task): config.continuous_mode = False config.temperature = 0 config.plain_output = True - command_registry = get_command_registry(config) + command_registry = CommandRegistry.with_command_modules(COMMAND_CATEGORIES, config) config.memory_backend = "no_memory" config.workspace_path = Workspace.init_workspace_directory(config) config.file_logger_path = Workspace.build_file_logger_path(config.workspace_path) @@ -39,13 +39,3 @@ def bootstrap_agent(task): config=config, triggering_prompt=DEFAULT_TRIGGERING_PROMPT, ) - - -def get_command_registry(config: Config): - command_registry = CommandRegistry() - enabled_command_categories = [ - x for x in COMMAND_CATEGORIES if x not in config.disabled_command_categories - ] - for command_category in enabled_command_categories: - command_registry.import_commands(command_category) - return command_registry diff --git a/prompt_settings.yaml b/prompt_settings.yaml index 342d67b9ebb..a83ca6225bb 100644 --- a/prompt_settings.yaml +++ b/prompt_settings.yaml @@ -7,9 +7,10 @@ constraints: [ resources: [ 'Internet access for searches and information gathering.', 'Long Term memory management.', - 'File output.' + 'File output.', + 'Command execution' ] -performance_evaluations: [ +best_practices: [ 'Continuously review and analyze your actions to ensure you are performing to the best of your abilities.', 'Constructively self-criticize your big-picture behavior constantly.', 'Reflect on past decisions and strategies to refine your approach.', diff --git a/tests/Auto-GPT-test-cassettes b/tests/Auto-GPT-test-cassettes index 6b4f855269d..0e4b46dc515 160000 --- a/tests/Auto-GPT-test-cassettes +++ b/tests/Auto-GPT-test-cassettes @@ -1 +1 @@ -Subproject commit 6b4f855269dfc7ec220cc7774d675940dcaa78ef +Subproject commit 0e4b46dc515585902eaae068dcbc3f182dd263ba diff --git a/tests/mocks/mock_commands.py b/tests/mocks/mock_commands.py index 278894c4d09..3758c1da2bb 100644 --- a/tests/mocks/mock_commands.py +++ b/tests/mocks/mock_commands.py @@ -1,5 +1,7 @@ from autogpt.command_decorator import command +COMMAND_CATEGORY = "mock" + @command( "function_based", diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index 2cdf8701a69..57de732a626 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -193,7 +193,7 @@ def test_import_mock_commands_module(): registry = CommandRegistry() mock_commands_module = "tests.mocks.mock_commands" - registry.import_commands(mock_commands_module) + registry.import_command_module(mock_commands_module) assert "function_based" in registry assert registry.commands["function_based"].name == "function_based" @@ -219,7 +219,7 @@ def test_import_temp_command_file_module(tmp_path: Path): sys.path.append(str(tmp_path)) temp_commands_module = "mock_commands" - registry.import_commands(temp_commands_module) + registry.import_command_module(temp_commands_module) # Remove the temp directory from sys.path sys.path.remove(str(tmp_path)) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 6445ae786e3..80de7073a79 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -21,7 +21,7 @@ def test_initial_values(config: Config): assert config.continuous_mode == False assert config.speak_mode == False assert config.fast_llm == "gpt-3.5-turbo" - assert config.smart_llm == "gpt-4" + assert config.smart_llm == "gpt-4-0314" def test_set_continuous_mode(config: Config): diff --git a/tests/unit/test_prompt_config.py b/tests/unit/test_prompt_config.py index 4616db971b3..b83efd0d520 100644 --- a/tests/unit/test_prompt_config.py +++ b/tests/unit/test_prompt_config.py @@ -18,10 +18,10 @@ def test_prompt_config_loading(tmp_path): - A test resource - Another test resource - A third test resource -performance_evaluations: -- A test performance evaluation -- Another test performance evaluation -- A third test performance evaluation +best_practices: +- A test best-practice +- Another test best-practice +- A third test best-practice """ prompt_settings_file = tmp_path / "test_prompt_settings.yaml" prompt_settings_file.write_text(yaml_content) @@ -36,13 +36,7 @@ def test_prompt_config_loading(tmp_path): assert prompt_config.resources[0] == "A test resource" assert prompt_config.resources[1] == "Another test resource" assert prompt_config.resources[2] == "A third test resource" - assert len(prompt_config.performance_evaluations) == 3 - assert prompt_config.performance_evaluations[0] == "A test performance evaluation" - assert ( - prompt_config.performance_evaluations[1] - == "Another test performance evaluation" - ) - assert ( - prompt_config.performance_evaluations[2] - == "A third test performance evaluation" - ) + assert len(prompt_config.best_practices) == 3 + assert prompt_config.best_practices[0] == "A test best-practice" + assert prompt_config.best_practices[1] == "Another test best-practice" + assert prompt_config.best_practices[2] == "A third test best-practice" diff --git a/tests/unit/test_prompt_generator.py b/tests/unit/test_prompt_generator.py index 44147e6dbce..d1b08f1a041 100644 --- a/tests/unit/test_prompt_generator.py +++ b/tests/unit/test_prompt_generator.py @@ -20,13 +20,12 @@ def test_add_command(): params = {"arg1": "value1", "arg2": "value2"} generator = PromptGenerator() generator.add_command(command_label, command_name, params) - command = { + assert generator.commands[0].__dict__ == { "label": command_label, "name": command_name, "params": params, "function": None, } - assert command in generator.commands def test_add_resource(): @@ -39,18 +38,18 @@ def test_add_resource(): assert resource in generator.resources -def test_add_performance_evaluation(): +def test_add_best_practice(): """ - Test if the add_performance_evaluation() method adds an evaluation to the generator's - performance_evaluation list. + Test if the add_best_practice() method adds a best practice to the generator's + best_practices list. """ - evaluation = "Evaluation1" + practice = "Practice1" generator = PromptGenerator() - generator.add_performance_evaluation(evaluation) - assert evaluation in generator.performance_evaluation + generator.add_best_practice(practice) + assert practice in generator.best_practices -def test_generate_prompt_string(config): +def test_generate_prompt_string(): """ Test if the generate_prompt_string() method generates a prompt string with all the added constraints, commands, resources, and evaluations. @@ -82,10 +81,10 @@ def test_generate_prompt_string(config): for resource in resources: generator.add_resource(resource) for evaluation in evaluations: - generator.add_performance_evaluation(evaluation) + generator.add_best_practice(evaluation) # Generate the prompt string and verify its correctness - prompt_string = generator.generate_prompt_string(config) + prompt_string = generator.generate_prompt_string() assert prompt_string is not None # Check if all constraints, commands, resources, and evaluations are present in the prompt string