From 36e2dae6b05bc076fbd566d052d8f061e459807c Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Sun, 8 Oct 2023 10:13:23 -0700 Subject: [PATCH] Add AgentFactory and replace AI Goals by AI Directives + Task --- .../autogpt/agent_factory/configurators.py | 116 ++++++ .../autogpt/agent_factory/generators.py | 29 ++ .../agent_factory/profile_generator.py | 216 ++++++++++ autogpts/autogpt/autogpt/agents/base.py | 8 +- .../agents/prompt_strategies/one_shot.py | 31 +- autogpts/autogpt/autogpt/app/cli.py | 47 ++- autogpts/autogpt/autogpt/app/main.py | 184 +++------ autogpts/autogpt/autogpt/app/setup.py | 389 ++++++++---------- .../autogpt/autogpt/config/ai_directives.py | 7 + .../autogpt/tests/integration/test_setup.py | 97 ++--- 10 files changed, 687 insertions(+), 437 deletions(-) create mode 100644 autogpts/autogpt/autogpt/agent_factory/configurators.py create mode 100644 autogpts/autogpt/autogpt/agent_factory/generators.py create mode 100644 autogpts/autogpt/autogpt/agent_factory/profile_generator.py diff --git a/autogpts/autogpt/autogpt/agent_factory/configurators.py b/autogpts/autogpt/autogpt/agent_factory/configurators.py new file mode 100644 index 00000000..5c092dc6 --- /dev/null +++ b/autogpts/autogpt/autogpt/agent_factory/configurators.py @@ -0,0 +1,116 @@ +from typing import Optional + +from autogpt.agent_manager import AgentManager +from autogpt.agents.agent import Agent, AgentConfiguration, AgentSettings +from autogpt.commands import COMMAND_CATEGORIES +from autogpt.config import AIProfile, AIDirectives, Config +from autogpt.core.resource.model_providers import ChatModelProvider +from autogpt.logs.config import configure_chat_plugins +from autogpt.logs.helpers import print_attribute +from autogpt.models.command_registry import CommandRegistry +from autogpt.plugins import scan_plugins + + +def create_agent( + task: str, + ai_profile: AIProfile, + app_config: Config, + llm_provider: ChatModelProvider, + directives: Optional[AIDirectives] = None, +) -> Agent: + if not task: + raise ValueError("No task specified for new agent") + if not directives: + directives = AIDirectives.from_file(app_config.prompt_settings_file) + + agent = _configure_agent( + task=task, + ai_profile=ai_profile, + directives=directives, + app_config=app_config, + llm_provider=llm_provider, + ) + + agent.state.agent_id = AgentManager.generate_id(agent.ai_profile.ai_name) + + return agent + + +def configure_agent_with_state( + state: AgentSettings, + app_config: Config, + llm_provider: ChatModelProvider, +) -> Agent: + return _configure_agent( + state=state, + app_config=app_config, + llm_provider=llm_provider, + ) + + +def _configure_agent( + app_config: Config, + llm_provider: ChatModelProvider, + task: str = "", + ai_profile: Optional[AIProfile] = None, + directives: Optional[AIDirectives] = None, + state: Optional[AgentSettings] = None, +) -> Agent: + if not (state or task and ai_profile and directives): + raise TypeError( + "Either (state) or (task, ai_profile, directives) must be specified" + ) + + app_config.plugins = scan_plugins(app_config, app_config.debug_mode) + configure_chat_plugins(app_config) + + # Create a CommandRegistry instance and scan default folder + command_registry = CommandRegistry.with_command_modules( + modules=COMMAND_CATEGORIES, + config=app_config, + ) + + agent_state = state or create_agent_state( + task=task, + ai_profile=ai_profile, + directives=directives, + app_config=app_config, + ) + + # TODO: configure memory + + print_attribute("Configured Browser", app_config.selenium_web_browser) + + return Agent( + settings=agent_state, + llm_provider=llm_provider, + command_registry=command_registry, + legacy_config=app_config, + ) + + +def create_agent_state( + task: str, + ai_profile: AIProfile, + directives: AIDirectives, + app_config: Config, +) -> AgentSettings: + agent_prompt_config = Agent.default_settings.prompt_config.copy(deep=True) + agent_prompt_config.use_functions_api = app_config.openai_functions + + return AgentSettings( + name=Agent.default_settings.name, + description=Agent.default_settings.description, + task=task, + ai_profile=ai_profile, + directives=directives, + config=AgentConfiguration( + fast_llm=app_config.fast_llm, + smart_llm=app_config.smart_llm, + allow_fs_access=not app_config.restrict_to_workspace, + use_functions_api=app_config.openai_functions, + plugins=app_config.plugins, + ), + prompt_config=agent_prompt_config, + history=Agent.default_settings.history.copy(deep=True), + ) diff --git a/autogpts/autogpt/autogpt/agent_factory/generators.py b/autogpts/autogpt/autogpt/agent_factory/generators.py new file mode 100644 index 00000000..4e0435fc --- /dev/null +++ b/autogpts/autogpt/autogpt/agent_factory/generators.py @@ -0,0 +1,29 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from autogpt.agents.agent import Agent + from autogpt.config import AIDirectives, Config + from autogpt.core.resource.model_providers.schema import ChatModelProvider + +from .configurators import _configure_agent +from .profile_generator import generate_agent_profile_for_task + + +async def generate_agent_for_task( + task: str, + app_config: "Config", + llm_provider: "ChatModelProvider", +) -> "Agent": + base_directives = AIDirectives.from_file(app_config.ai_settings_file) + ai_profile, task_directives = await generate_agent_profile_for_task( + task=task, + app_config=app_config, + llm_provider=llm_provider, + ) + return _configure_agent( + task=task, + ai_profile=ai_profile, + directives=base_directives + task_directives, + app_config=app_config, + llm_provider=llm_provider, + ) diff --git a/autogpts/autogpt/autogpt/agent_factory/profile_generator.py b/autogpts/autogpt/autogpt/agent_factory/profile_generator.py new file mode 100644 index 00000000..2f6d918f --- /dev/null +++ b/autogpts/autogpt/autogpt/agent_factory/profile_generator.py @@ -0,0 +1,216 @@ +import logging + +from autogpt.config import AIProfile, AIDirectives, Config +from autogpt.core.configuration import SystemConfiguration, UserConfigurable +from autogpt.core.prompting import ( + ChatPrompt, + LanguageModelClassification, + PromptStrategy, +) +from autogpt.core.prompting.utils import json_loads +from autogpt.core.resource.model_providers.schema import ( + AssistantChatMessageDict, + ChatMessage, + ChatModelProvider, + CompletionModelFunction, +) +from autogpt.core.utils.json_schema import JSONSchema + +logger = logging.getLogger(__name__) + + +class AgentProfileGeneratorConfiguration(SystemConfiguration): + model_classification: LanguageModelClassification = UserConfigurable( + default=LanguageModelClassification.SMART_MODEL + ) + system_prompt: str = UserConfigurable( + default=( + "Your job is to respond to a user-defined task, given in triple quotes, by " + "invoking the `create_agent` function to generate an autonomous agent to " + "complete the task. " + "You should supply a role-based name for the agent (_GPT), " + "an informative description for what the agent does, and " + "1 to 5 directives in each of the categories Best Practices and Constraints, " + "that are optimally aligned with the successful completion " + "of its assigned task.\n" + "\n" + "Example Input:\n" + '"""Help me with marketing my business"""\n\n' + "Example Function Call:\n" + "```\n" + "{" + '"name": "create_agent",' + ' "arguments": {' + '"name": "CMOGPT",' + ' "description": "a professional digital marketer AI that assists Solopreneurs in' + " growing their businesses by providing world-class expertise in solving" + ' marketing problems for SaaS, content products, agencies, and more.",' + ' "directives": {' + ' "best_practices": [' + '"Engage in effective problem-solving, prioritization, planning, and' + " supporting execution to address your marketing needs as your virtual Chief" + ' Marketing Officer.",' + ' "Provide specific, actionable, and concise advice to help you make' + " informed decisions without the use of platitudes or overly wordy" + ' explanations.",' + ' "Identify and prioritize quick wins and cost-effective campaigns that' + ' maximize results with minimal time and budget investment.",' + ' "Proactively take the lead in guiding you and offering suggestions when' + " faced with unclear information or uncertainty to ensure your marketing" + ' strategy remains on track."' + "]" # best_practices + "}" # directives + "}" # arguments + "}\n" + "```" + ) + ) + user_prompt_template: str = UserConfigurable(default='"""{user_objective}"""') + create_agent_function: dict = UserConfigurable( + default=CompletionModelFunction( + name="create_agent", + description="Create a new autonomous AI agent to complete a given task.", + parameters={ + "name": JSONSchema( + type=JSONSchema.Type.STRING, + description="A short role-based name for an autonomous agent.", + required=True, + ), + "description": JSONSchema( + type=JSONSchema.Type.STRING, + description="An informative one sentence description of what the AI agent does", + required=True, + ), + "directives": JSONSchema( + type=JSONSchema.Type.OBJECT, + properties={ + "best_practices": JSONSchema( + type=JSONSchema.Type.ARRAY, + minItems=1, + maxItems=5, + items=JSONSchema( + type=JSONSchema.Type.STRING, + ), + description=( + "One to five highly effective best practices that are" + " optimally aligned with the completion of the given task." + ), + required=True, + ), + "constraints": JSONSchema( + type=JSONSchema.Type.ARRAY, + minItems=1, + maxItems=5, + items=JSONSchema( + type=JSONSchema.Type.STRING, + ), + description=( + "One to five highly effective constraints that are" + " optimally aligned with the completion of the given task." + ), + required=True, + ), + }, + required=True, + ), + }, + ).schema + ) + + +class AgentProfileGenerator(PromptStrategy): + default_configuration: AgentProfileGeneratorConfiguration = AgentProfileGeneratorConfiguration() + + def __init__( + self, + model_classification: LanguageModelClassification, + system_prompt: str, + user_prompt_template: str, + create_agent_function: dict, + ): + self._model_classification = model_classification + self._system_prompt_message = system_prompt + self._user_prompt_template = user_prompt_template + self._create_agent_function = CompletionModelFunction.parse( + create_agent_function + ) + + @property + def model_classification(self) -> LanguageModelClassification: + return self._model_classification + + def build_prompt(self, user_objective: str = "", **kwargs) -> ChatPrompt: + system_message = ChatMessage.system(self._system_prompt_message) + user_message = ChatMessage.user( + self._user_prompt_template.format( + user_objective=user_objective, + ) + ) + prompt = ChatPrompt( + messages=[system_message, user_message], + functions=[self._create_agent_function], + ) + return prompt + + def parse_response_content( + self, + response_content: AssistantChatMessageDict, + ) -> tuple[AIProfile, AIDirectives]: + """Parse the actual text response from the objective model. + + Args: + response_content: The raw response content from the objective model. + + Returns: + The parsed response. + + """ + try: + arguments = json_loads(response_content["function_call"]["arguments"]) + ai_profile = AIProfile( + ai_name=arguments["name"], + ai_role=arguments["description"], + ) + ai_directives = AIDirectives( + best_practices=arguments["directives"]["best_practices"], + constraints=arguments["directives"]["constraints"], + resources=[], + ) + except KeyError: + logger.debug(f"Failed to parse this response content: {response_content}") + raise + return ai_profile, ai_directives + + +async def generate_agent_profile_for_task( + task: str, + app_config: Config, + llm_provider: ChatModelProvider, +) -> tuple[AIProfile, AIDirectives]: + """Generates an AIConfig object from the given string. + + Returns: + AIConfig: The AIConfig object tailored to the user's input + """ + agent_profile_generator = AgentProfileGenerator( + **AgentProfileGenerator.default_configuration.dict() # HACK + ) + + prompt = agent_profile_generator.build_prompt(task) + + # Call LLM with the string as user input + output = ( + await llm_provider.create_chat_completion( + prompt.messages, + model_name=app_config.smart_llm, + functions=prompt.functions, + ) + ).response + + # Debug LLM Output + logger.debug(f"AI Config Generator Raw Output: {output}") + + # Parse the output + ai_profile, ai_directives = agent_profile_generator.parse_response_content(output) + + return ai_profile, ai_directives diff --git a/autogpts/autogpt/autogpt/agents/base.py b/autogpts/autogpt/autogpt/agents/base.py index 652de663..281df602 100644 --- a/autogpts/autogpt/autogpt/agents/base.py +++ b/autogpts/autogpt/autogpt/agents/base.py @@ -137,6 +137,9 @@ class BaseAgentSettings(SystemSettings): ) """Directives (general instructional guidelines) for the agent.""" + task: str = "Terminate immediately" # FIXME: placeholder for forge.sdk.schema.Task + """The user-given task that the agent is working on.""" + config: BaseAgentConfiguration = Field(default_factory=BaseAgentConfiguration) """The configuration for this BaseAgent subsystem instance.""" @@ -165,7 +168,7 @@ class BaseAgent(Configurable[BaseAgentSettings], ABC): self.state = settings self.config = settings.config self.ai_profile = settings.ai_profile - self.ai_directives = settings.directives + self.directives = settings.directives self.event_history = settings.history self.legacy_config = legacy_config @@ -288,13 +291,14 @@ class BaseAgent(Configurable[BaseAgentSettings], ABC): if not plugin.can_handle_post_prompt(): continue plugin.post_prompt(scratchpad) - ai_directives = self.ai_directives.copy(deep=True) + ai_directives = self.directives.copy(deep=True) ai_directives.resources += scratchpad.resources ai_directives.constraints += scratchpad.constraints ai_directives.best_practices += scratchpad.best_practices extra_commands += list(scratchpad.commands.values()) prompt = self.prompt_strategy.build_prompt( + task=self.state.task, ai_profile=self.ai_profile, ai_directives=ai_directives, commands=get_openai_command_specs( diff --git a/autogpts/autogpt/autogpt/agents/prompt_strategies/one_shot.py b/autogpts/autogpt/autogpt/agents/prompt_strategies/one_shot.py index 8422768d..3e34ed76 100644 --- a/autogpts/autogpt/autogpt/agents/prompt_strategies/one_shot.py +++ b/autogpts/autogpt/autogpt/agents/prompt_strategies/one_shot.py @@ -193,6 +193,7 @@ class OneShotAgentPromptStrategy(PromptStrategy): def build_prompt( self, *, + task: str, ai_profile: AIProfile, ai_directives: AIDirectives, commands: list[CompletionModelFunction], @@ -223,6 +224,9 @@ class OneShotAgentPromptStrategy(PromptStrategy): ) system_prompt_tlength = count_message_tokens(ChatMessage.system(system_prompt)) + user_task = f'"""{task}"""' + user_task_tlength = count_message_tokens(ChatMessage.user(user_task)) + response_format_instr = self.response_format_instruction( self.config.use_functions_api ) @@ -238,6 +242,7 @@ class OneShotAgentPromptStrategy(PromptStrategy): max_tokens=( max_prompt_tokens - system_prompt_tlength + - user_task_tlength - final_instruction_tlength - count_message_tokens(extra_messages) ), @@ -250,6 +255,7 @@ class OneShotAgentPromptStrategy(PromptStrategy): prompt = ChatPrompt( messages=[ ChatMessage.system(system_prompt), + ChatMessage.user(user_task), *extra_messages, final_instruction_msg, ], @@ -278,7 +284,12 @@ class OneShotAgentPromptStrategy(PromptStrategy): best_practices=format_numbered_list(ai_directives.best_practices), ) ] - + self._generate_goals_info(ai_profile.ai_goals) + + [ + "## Your Task\n" + "The user will specify a task for you to execute, in triple quotes," + " in the next message. Your job is to complete the task while following" + " your directives as given above, and terminate when your task is done." + ] ) # Join non-empty parts together into paragraph format @@ -395,24 +406,6 @@ class OneShotAgentPromptStrategy(PromptStrategy): ] return [] - def _generate_goals_info(self, goals: list[str]) -> list[str]: - """Generates the goals information part of the prompt. - - Returns: - str: The goals information part of the prompt. - """ - if goals: - return [ - "\n".join( - [ - "## Goals", - "For your task, you must fulfill the following goals:", - *[f"{i+1}. {goal}" for i, goal in enumerate(goals)], - ] - ) - ] - return [] - def _generate_commands_list(self, commands: list[CompletionModelFunction]) -> str: """Lists the commands available to the agent. diff --git a/autogpts/autogpt/autogpt/app/cli.py b/autogpts/autogpt/autogpt/app/cli.py index 002e59ad..ebbc9ea9 100644 --- a/autogpts/autogpt/autogpt/app/cli.py +++ b/autogpts/autogpt/autogpt/app/cli.py @@ -84,10 +84,39 @@ import click help="AI role override", ) @click.option( - "--ai-goal", + "--constraint", type=str, multiple=True, - help="AI goal override; may be used multiple times to pass multiple goals", + help=( + "Add or override AI constraints to include in the prompt;" + " may be used multiple times to pass multiple constraints" + ), +) +@click.option( + "--resource", + type=str, + multiple=True, + help=( + "Add or override AI resources to include in the prompt;" + " may be used multiple times to pass multiple resources" + ), +) +@click.option( + "--best-practice", + type=str, + multiple=True, + help=( + "Add or override AI best practices to include in the prompt;" + " may be used multiple times to pass multiple best practices" + ), +) +@click.option( + "--override-directives", + is_flag=True, + help=( + "If specified, --constraint, --resource and --best-practice will override" + " the AI's directives instead of being appended to them" + ), ) @click.pass_context def main( @@ -109,7 +138,10 @@ def main( install_plugin_deps: bool, ai_name: Optional[str], ai_role: Optional[str], - ai_goal: tuple[str], + resource: tuple[str], + constraint: tuple[str], + best_practice: tuple[str], + override_directives: bool, ) -> None: """ Welcome to AutoGPT an experimental open-source application showcasing the capabilities of the GPT-4 pushing the boundaries of AI. @@ -136,9 +168,12 @@ def main( skip_news=skip_news, workspace_directory=workspace_directory, install_plugin_deps=install_plugin_deps, - ai_name=ai_name, - ai_role=ai_role, - ai_goals=ai_goal, + override_ai_name=ai_name, + override_ai_role=ai_role, + resources=list(resource), + constraints=list(constraint), + best_practices=list(best_practice), + override_directives=override_directives, ) diff --git a/autogpts/autogpt/autogpt/app/main.py b/autogpts/autogpt/autogpt/app/main.py index 3ec1c562..1b1d9e49 100644 --- a/autogpts/autogpt/autogpt/app/main.py +++ b/autogpts/autogpt/autogpt/app/main.py @@ -6,16 +6,24 @@ import signal import sys from pathlib import Path from types import FrameType -from typing import Optional +from typing import TYPE_CHECKING, Optional from colorama import Fore, Style from pydantic import SecretStr +if TYPE_CHECKING: + from autogpt.agents.agent import Agent + +from autogpt.agent_factory.configurators import create_agent +from autogpt.agent_factory.profile_generator import generate_agent_profile_for_task +from autogpt.agent_manager import AgentManager from autogpt.agents import AgentThoughts, CommandArgs, CommandName -from autogpt.agents.agent import Agent, AgentConfiguration, AgentSettings from autogpt.agents.utils.exceptions import InvalidAgentResponseError from autogpt.app.configurator import apply_overrides_to_config -from autogpt.app.setup import interactive_ai_profile_setup +from autogpt.app.setup import ( + apply_overrides_to_ai_settings, + interactively_revise_ai_settings, +) from autogpt.app.spinner import Spinner from autogpt.app.utils import ( clean_input, @@ -25,7 +33,6 @@ from autogpt.app.utils import ( print_motd, print_python_version_info, ) -from autogpt.commands import COMMAND_CATEGORIES from autogpt.config import ( AIDirectives, AIProfile, @@ -33,16 +40,11 @@ from autogpt.config import ( ConfigBuilder, assert_config_has_openai_api_key, ) -from autogpt.core.resource.model_providers import ( - ChatModelProvider, - ModelProviderCredentials, -) +from autogpt.core.resource.model_providers import ModelProviderCredentials from autogpt.core.resource.model_providers.openai import OpenAIProvider from autogpt.core.runner.client_lib.utils import coroutine -from autogpt.llm.api_manager import ApiManager from autogpt.logs.config import configure_chat_plugins, configure_logging from autogpt.logs.helpers import print_attribute, speak -from autogpt.models.command_registry import CommandRegistry from autogpt.plugins import scan_plugins from scripts.install_plugin_deps import install_plugin_dependencies @@ -62,11 +64,14 @@ async def run_auto_gpt( browser_name: str, allow_downloads: bool, skip_news: bool, - workspace_directory: str | Path, + workspace_directory: Path, install_plugin_deps: bool, - ai_name: Optional[str] = None, - ai_role: Optional[str] = None, - ai_goals: tuple[str] = tuple(), + override_ai_name: str = "", + override_ai_role: str = "", + resources: Optional[list[str]] = None, + constraints: Optional[list[str]] = None, + best_practices: Optional[list[str]] = None, + override_directives: bool = False, ): config = ConfigBuilder.build_config_from_env() @@ -123,44 +128,58 @@ async def run_auto_gpt( config.plugins = scan_plugins(config, config.debug_mode) configure_chat_plugins(config) - # Create a CommandRegistry instance and scan default folder - command_registry = CommandRegistry.with_command_modules(COMMAND_CATEGORIES, config) - - ai_profile = await construct_main_ai_profile( + ###################### + # Set up a new Agent # + ###################### + task = await clean_input( config, - llm_provider=llm_provider, - name=ai_name, - role=ai_role, - goals=ai_goals, + "Enter the task that you want AutoGPT to execute," + " with as much detail as possible:", ) - ai_directives = AIDirectives.from_file(config.prompt_settings_file) + base_ai_directives = AIDirectives.from_file(config.prompt_settings_file) - agent_prompt_config = Agent.default_settings.prompt_config.copy(deep=True) - agent_prompt_config.use_functions_api = config.openai_functions - - agent_settings = AgentSettings( - name=Agent.default_settings.name, - description=Agent.default_settings.description, + ai_profile, task_oriented_ai_directives = await generate_agent_profile_for_task( + task, + app_config=config, + llm_provider=llm_provider, + ) + ai_directives = base_ai_directives + task_oriented_ai_directives + apply_overrides_to_ai_settings( ai_profile=ai_profile, directives=ai_directives, - config=AgentConfiguration( - fast_llm=config.fast_llm, - smart_llm=config.smart_llm, - allow_fs_access=not config.restrict_to_workspace, - use_functions_api=config.openai_functions, - plugins=config.plugins, - ), - prompt_config=agent_prompt_config, - history=Agent.default_settings.history.copy(deep=True), + override_name=override_ai_name, + override_role=override_ai_role, + resources=resources, + constraints=constraints, + best_practices=best_practices, + replace_directives=override_directives, ) - print_attribute("Configured Browser", config.selenium_web_browser) + # If any of these are specified as arguments, + # assume the user doesn't want to revise them + if not any( + [ + override_ai_name, + override_ai_role, + resources, + constraints, + best_practices, + ] + ): + ai_profile, ai_directives = await interactively_revise_ai_settings( + ai_profile=ai_profile, + directives=ai_directives, + app_config=config, + ) + else: + logger.info("AI config overrides specified through CLI; skipping revision") - agent = Agent( - settings=agent_settings, + agent = create_agent( + task=task, + ai_profile=ai_profile, + directives=ai_directives, + app_config=config, llm_provider=llm_provider, - command_registry=command_registry, - legacy_config=config, ) agent.attach_fs(config.app_data_dir / "agents" / "AutoGPT") # HACK @@ -223,7 +242,7 @@ class UserFeedback(str, enum.Enum): async def run_interaction_loop( - agent: Agent, + agent: "Agent", ) -> None: """Run the main interaction loop for the agent. @@ -482,83 +501,6 @@ async def get_user_feedback( return user_feedback, user_input, new_cycles_remaining -async def construct_main_ai_profile( - config: Config, - llm_provider: ChatModelProvider, - name: Optional[str] = None, - role: Optional[str] = None, - goals: tuple[str] = tuple(), -) -> AIProfile: - """Construct the prompt for the AI to respond to - - Returns: - str: The prompt string - """ - logger = logging.getLogger(__name__) - - ai_profile = AIProfile.load(config.ai_settings_file) - - # Apply overrides - if name: - ai_profile.ai_name = name - if role: - ai_profile.ai_role = role - if goals: - ai_profile.ai_goals = list(goals) - - if ( - all([name, role, goals]) - or config.skip_reprompt - and all([ai_profile.ai_name, ai_profile.ai_role, ai_profile.ai_goals]) - ): - print_attribute("Name :", ai_profile.ai_name) - print_attribute("Role :", ai_profile.ai_role) - print_attribute("Goals:", ai_profile.ai_goals) - print_attribute( - "API Budget:", - "infinite" if ai_profile.api_budget <= 0 else f"${ai_profile.api_budget}", - ) - elif all([ai_profile.ai_name, ai_profile.ai_role, ai_profile.ai_goals]): - logger.info( - extra={"title": f"{Fore.GREEN}Welcome back!{Fore.RESET}"}, - msg=f"Would you like me to return to being {ai_profile.ai_name}?", - ) - should_continue = await clean_input( - config, - f"""Continue with the last settings? -Name: {ai_profile.ai_name} -Role: {ai_profile.ai_role} -Goals: {ai_profile.ai_goals} -API Budget: {"infinite" if ai_profile.api_budget <= 0 else f"${ai_profile.api_budget}"} -Continue ({config.authorise_key}/{config.exit_key}): """, - ) - if should_continue.lower() == config.exit_key: - ai_profile = AIProfile() - - if any([not ai_profile.ai_name, not ai_profile.ai_role, not ai_profile.ai_goals]): - ai_profile = await interactive_ai_profile_setup(config, llm_provider) - ai_profile.save(config.ai_settings_file) - - # set the total api budget - api_manager = ApiManager() - api_manager.set_total_budget(ai_profile.api_budget) - - # Agent Created, print message - logger.info( - f"{Fore.LIGHTBLUE_EX}{ai_profile.ai_name}{Fore.RESET} has been created with the following details:", - extra={"preserve_color": True}, - ) - - # Print the ai_profile details - print_attribute("Name :", ai_profile.ai_name) - print_attribute("Role :", ai_profile.ai_role) - print_attribute("Goals:", "") - for goal in ai_profile.ai_goals: - logger.info(f"- {goal}") - - return ai_profile - - def print_assistant_thoughts( ai_name: str, assistant_reply_json_valid: dict, diff --git a/autogpts/autogpt/autogpt/app/setup.py b/autogpts/autogpt/autogpt/app/setup.py index 1076e6f9..cbe2c38c 100644 --- a/autogpts/autogpt/autogpt/app/setup.py +++ b/autogpts/autogpt/autogpt/app/setup.py @@ -1,253 +1,190 @@ """Set up the AI and its goals""" import logging -import re from typing import Optional -from colorama import Fore, Style -from jinja2 import Template - -from autogpt.app import utils -from autogpt.config import Config -from autogpt.config.ai_profile import AIProfile -from autogpt.core.resource.model_providers import ChatMessage, ChatModelProvider -from autogpt.logs.helpers import user_friendly_output -from autogpt.prompts.default_prompts import ( - DEFAULT_SYSTEM_PROMPT_AICONFIG_AUTOMATIC, - DEFAULT_TASK_PROMPT_AICONFIG_AUTOMATIC, - DEFAULT_USER_DESIRE_PROMPT, -) +from autogpt.app.utils import clean_input +from autogpt.config import AIProfile, AIDirectives, Config +from autogpt.logs.helpers import print_attribute logger = logging.getLogger(__name__) -async def interactive_ai_profile_setup( - config: Config, - llm_provider: ChatModelProvider, - ai_profile_template: Optional[AIProfile] = None, -) -> AIProfile: - """Prompt the user for input +def apply_overrides_to_ai_settings( + ai_profile: AIProfile, + directives: AIDirectives, + override_name: str = "", + override_role: str = "", + replace_directives: bool = False, + resources: Optional[list[str]] = None, + constraints: Optional[list[str]] = None, + best_practices: Optional[list[str]] = None, +): + if override_name: + ai_profile.ai_name = override_name + if override_role: + ai_profile.ai_role = override_role - Params: - config (Config): The Config object - ai_profile_template (AIProfile): The AIProfile object to use as a template + if replace_directives: + if resources: + directives.resources = resources + if constraints: + directives.constraints = constraints + if best_practices: + directives.best_practices = best_practices + else: + if resources: + directives.resources += resources + if constraints: + directives.constraints += constraints + if best_practices: + directives.best_practices += best_practices + + +async def interactively_revise_ai_settings( + ai_profile: AIProfile, + directives: AIDirectives, + app_config: Config, +): + """Interactively revise the AI settings. + + Args: + ai_profile (AIConfig): The current AI profile. + ai_directives (AIDirectives): The current AI directives. + app_config (Config): The application configuration. Returns: - AIProfile: The AIProfile object tailored to the user's input + AIConfig: The revised AI settings. """ + logger = logging.getLogger("revise_ai_profile") - # Construct the prompt - user_friendly_output( - title="Welcome to AutoGPT! ", - message="run with '--help' for more information.", - title_color=Fore.GREEN, - ) + revised = False - ai_profile_template_provided = ai_profile_template is not None and any( - [ - ai_profile_template.ai_goals, - ai_profile_template.ai_name, - ai_profile_template.ai_role, - ] - ) - - user_desire = "" - if not ai_profile_template_provided: - # Get user desire if command line overrides have not been passed in - user_friendly_output( - title="Create an AI-Assistant:", - message="input '--manual' to enter manual mode.", - title_color=Fore.GREEN, + while True: + # Print the current AI configuration + print_ai_settings( + title="Current AI Settings" if not revised else "Revised AI Settings", + ai_profile=ai_profile, + directives=directives, + logger=logger, ) - user_desire = await utils.clean_input( - config, f"{Fore.LIGHTBLUE_EX}I want AutoGPT to{Style.RESET_ALL}: " - ) + if ( + await clean_input(app_config, "Continue with these settings? [Y/n]") + or app_config.authorise_key + ) == app_config.authorise_key: + break - if user_desire.strip() == "": - user_desire = DEFAULT_USER_DESIRE_PROMPT # Default prompt - - # If user desire contains "--manual" or we have overridden any of the AI configuration - if "--manual" in user_desire or ai_profile_template_provided: - user_friendly_output( - "", - title="Manual Mode Selected", - title_color=Fore.GREEN, - ) - return await generate_aiconfig_manual(config, ai_profile_template) - - else: - try: - return await generate_aiconfig_automatic(user_desire, config, llm_provider) - except Exception as e: - user_friendly_output( - title="Unable to automatically generate AI Config based on user desire.", - message="Falling back to manual mode.", - title_color=Fore.RED, + # Ask for revised ai_profile + ai_profile.ai_name = ( + await clean_input( + app_config, "Enter AI name (or press enter to keep current):" ) - logger.debug(f"Error during AIProfile generation: {e}") - - return await generate_aiconfig_manual(config) - - -async def generate_aiconfig_manual( - config: Config, ai_profile_template: Optional[AIProfile] = None -) -> AIProfile: - """ - Interactively create an AI configuration by prompting the user to provide the name, role, and goals of the AI. - - This function guides the user through a series of prompts to collect the necessary information to create - an AIProfile object. The user will be asked to provide a name and role for the AI, as well as up to five - goals. If the user does not provide a value for any of the fields, default values will be used. - - Params: - config (Config): The Config object - ai_profile_template (AIProfile): The AIProfile object to use as a template - - Returns: - AIProfile: An AIProfile object containing the user-defined or default AI name, role, and goals. - """ - - # Manual Setup Intro - user_friendly_output( - title="Create an AI-Assistant:", - message="Enter the name of your AI and its role below. Entering nothing will load" - " defaults.", - title_color=Fore.GREEN, - ) - - if ai_profile_template and ai_profile_template.ai_name: - ai_name = ai_profile_template.ai_name - else: - ai_name = "" - # Get AI Name from User - user_friendly_output( - title="Name your AI:", - message="For example, 'Entrepreneur-GPT'", - title_color=Fore.GREEN, + or ai_profile.ai_name ) - ai_name = await utils.clean_input(config, "AI Name: ") - if ai_name == "": - ai_name = "Entrepreneur-GPT" - - user_friendly_output( - title=f"{ai_name} here!", - message="I am at your service.", - title_color=Fore.LIGHTBLUE_EX, - ) - - if ai_profile_template and ai_profile_template.ai_role: - ai_role = ai_profile_template.ai_role - else: - # Get AI Role from User - user_friendly_output( - title="Describe your AI's role:", - message="For example, 'an AI designed to autonomously develop and run businesses with" - " the sole goal of increasing your net worth.'", - title_color=Fore.GREEN, - ) - ai_role = await utils.clean_input(config, f"{ai_name} is: ") - if ai_role == "": - ai_role = "an AI designed to autonomously develop and run businesses with the" - " sole goal of increasing your net worth." - - if ai_profile_template and ai_profile_template.ai_goals: - ai_goals = ai_profile_template.ai_goals - else: - # Enter up to 5 goals for the AI - user_friendly_output( - title="Enter up to 5 goals for your AI:", - message="For example: \nIncrease net worth, Grow Twitter Account, Develop and manage" - " multiple businesses autonomously'", - title_color=Fore.GREEN, - ) - logger.info("Enter nothing to load defaults, enter nothing when finished.") - ai_goals = [] - for i in range(5): - ai_goal = await utils.clean_input( - config, f"{Fore.LIGHTBLUE_EX}Goal{Style.RESET_ALL} {i+1}: " + ai_profile.ai_role = ( + await clean_input( + app_config, "Enter new AI role (or press enter to keep current):" ) - if ai_goal == "": + or ai_profile.ai_role + ) + + # Revise constraints + for i, constraint in enumerate(directives.constraints): + print_attribute(f"Constraint {i+1}:", f'"{constraint}"') + new_constraint = ( + await clean_input( + app_config, + f"Enter new constraint {i+1} (press enter to keep current, or '-' to remove):", + ) + or constraint + ) + if new_constraint == "-": + directives.constraints.remove(constraint) + elif new_constraint: + directives.constraints[i] = new_constraint + + # Add new constraints + while True: + new_constraint = await clean_input( + app_config, + "Press enter to finish, or enter a constraint to add:", + ) + if not new_constraint: break - ai_goals.append(ai_goal) - if not ai_goals: - ai_goals = [ - "Increase net worth", - "Grow Twitter Account", - "Develop and manage multiple businesses autonomously", - ] + directives.constraints.append(new_constraint) - # Get API Budget from User - user_friendly_output( - title="Enter your budget for API calls:", - message="For example: $1.50", - title_color=Fore.GREEN, - ) - logger.info("Enter nothing to let the AI run without monetary limit") - api_budget_input = await utils.clean_input( - config, f"{Fore.LIGHTBLUE_EX}Budget{Style.RESET_ALL}: $" - ) - if api_budget_input == "": - api_budget = 0.0 - else: - try: - api_budget = float(api_budget_input.replace("$", "")) - except ValueError: - user_friendly_output( - level=logging.WARNING, - title="Invalid budget input.", - message="Setting budget to unlimited.", - title_color=Fore.RED, + # Revise resources + for i, resource in enumerate(directives.resources): + print_attribute(f"Resource {i+1}:", f'"{resource}"') + new_resource = ( + await clean_input( + app_config, + f"Enter new resource {i+1} (press enter to keep current, or '-' to remove):", + ) + or resource ) - api_budget = 0.0 + if new_resource == "-": + directives.resources.remove(resource) + elif new_resource: + directives.resources[i] = new_resource - return AIProfile( - ai_name=ai_name, ai_role=ai_role, ai_goals=ai_goals, api_budget=api_budget - ) + # Add new resources + while True: + new_resource = await clean_input( + app_config, + "Press enter to finish, or enter a resource to add:", + ) + if not new_resource: + break + directives.resources.append(new_resource) + + # Revise best practices + for i, best_practice in enumerate(directives.best_practices): + print_attribute(f"Best Practice {i+1}:", f'"{best_practice}"') + new_best_practice = ( + await clean_input( + app_config, + f"Enter new best practice {i+1} (press enter to keep current, or '-' to remove):", + ) + or best_practice + ) + if new_best_practice == "-": + directives.best_practices.remove(best_practice) + elif new_best_practice: + directives.best_practices[i] = new_best_practice + + # Add new best practices + while True: + new_best_practice = await clean_input( + app_config, + "Press enter to finish, or add a best practice to add:", + ) + if not new_best_practice: + break + directives.best_practices.append(new_best_practice) + + revised = True + + return ai_profile, directives -async def generate_aiconfig_automatic( - user_prompt: str, - config: Config, - llm_provider: ChatModelProvider, -) -> AIProfile: - """Generates an AIProfile object from the given string. +def print_ai_settings( + ai_profile: AIProfile, + directives: AIDirectives, + logger: logging.Logger, + title: str = "AI Settings", +): + print_attribute(title, "") + print_attribute("-" * len(title), "") + print_attribute("Name :", ai_profile.ai_name) + print_attribute("Role :", ai_profile.ai_role) - Returns: - AIProfile: The AIProfile object tailored to the user's input - """ - - system_prompt = DEFAULT_SYSTEM_PROMPT_AICONFIG_AUTOMATIC - prompt_ai_profile_automatic = Template( - DEFAULT_TASK_PROMPT_AICONFIG_AUTOMATIC - ).render(user_prompt=user_prompt) - # Call LLM with the string as user input - output = ( - await llm_provider.create_chat_completion( - [ - ChatMessage.system(system_prompt), - ChatMessage.user(prompt_ai_profile_automatic), - ], - config.smart_llm, - ) - ).response["content"] - - # Debug LLM Output - logger.debug(f"AI Config Generator Raw Output: {output}") - - # Parse the output - ai_name = re.search(r"Name(?:\s*):(?:\s*)(.*)", output, re.IGNORECASE).group(1) - ai_role = ( - re.search( - r"Description(?:\s*):(?:\s*)(.*?)(?:(?:\n)|Goals)", - output, - re.IGNORECASE | re.DOTALL, - ) - .group(1) - .strip() - ) - ai_goals = re.findall(r"(?<=\n)-\s*(.*)", output) - api_budget = 0.0 # TODO: parse api budget using a regular expression - - return AIProfile( - ai_name=ai_name, ai_role=ai_role, ai_goals=ai_goals, api_budget=api_budget - ) + print_attribute("Constraints:", "" if directives.constraints else "(none)") + for constraint in directives.constraints: + logger.info(f"- {constraint}") + print_attribute("Resources:", "" if directives.resources else "(none)") + for resource in directives.resources: + logger.info(f"- {resource}") + print_attribute("Best practices:", "" if directives.best_practices else "(none)") + for best_practice in directives.best_practices: + logger.info(f"- {best_practice}") diff --git a/autogpts/autogpt/autogpt/config/ai_directives.py b/autogpts/autogpt/autogpt/config/ai_directives.py index 7bb38817..48fa7ceb 100644 --- a/autogpts/autogpt/autogpt/config/ai_directives.py +++ b/autogpts/autogpt/autogpt/config/ai_directives.py @@ -39,3 +39,10 @@ class AIDirectives(BaseModel): resources=config_params.get("resources", []), best_practices=config_params.get("best_practices", []), ) + + def __add__(self, other: "AIDirectives") -> "AIDirectives": + return AIDirectives( + resources=self.resources + other.resources, + constraints=self.constraints + other.constraints, + best_practices=self.best_practices + other.best_practices, + ).copy(deep=True) diff --git a/autogpts/autogpt/tests/integration/test_setup.py b/autogpts/autogpt/tests/integration/test_setup.py index 6cf86164..e1917189 100644 --- a/autogpts/autogpt/tests/integration/test_setup.py +++ b/autogpts/autogpt/tests/integration/test_setup.py @@ -2,78 +2,49 @@ from unittest.mock import patch import pytest -from autogpt.app.setup import generate_aiconfig_automatic, interactive_ai_profile_setup +from autogpt.app.setup import ( + apply_overrides_to_ai_settings, + interactively_revise_ai_settings, +) from autogpt.config.ai_profile import AIProfile +from autogpt.config import AIDirectives, Config -@pytest.mark.vcr -@pytest.mark.requires_openai_api_key -async def test_generate_aiconfig_automatic_default( - patched_api_requestor, config, llm_provider -): - user_inputs = [""] - with patch("autogpt.app.utils.session.prompt", side_effect=user_inputs): - ai_profile = await interactive_ai_profile_setup(config, llm_provider) +@pytest.mark.asyncio +async def test_apply_overrides_to_ai_settings(): + ai_profile = AIProfile(ai_name="Test AI", ai_role="Test Role") + directives = AIDirectives(resources=["Resource1"], constraints=["Constraint1"], best_practices=["BestPractice1"]) - assert isinstance(ai_profile, AIProfile) - assert ai_profile.ai_name is not None - assert ai_profile.ai_role is not None - assert 1 <= len(ai_profile.ai_goals) <= 5 + apply_overrides_to_ai_settings(ai_profile, directives, override_name="New AI", override_role="New Role", replace_directives=True, resources=["NewResource"], constraints=["NewConstraint"], best_practices=["NewBestPractice"]) + + assert ai_profile.ai_name == "New AI" + assert ai_profile.ai_role == "New Role" + assert directives.resources == ["NewResource"] + assert directives.constraints == ["NewConstraint"] + assert directives.best_practices == ["NewBestPractice"] -@pytest.mark.vcr -@pytest.mark.requires_openai_api_key -async def test_generate_aiconfig_automatic_typical( - patched_api_requestor, config, llm_provider -): - user_prompt = "Help me create a rock opera about cybernetic giraffes" - ai_profile = await generate_aiconfig_automatic(user_prompt, config, llm_provider) +@pytest.mark.asyncio +async def test_interactively_revise_ai_settings(config: Config): + ai_profile = AIProfile(ai_name="Test AI", ai_role="Test Role") + directives = AIDirectives(resources=["Resource1"], constraints=["Constraint1"], best_practices=["BestPractice1"]) - assert isinstance(ai_profile, AIProfile) - assert ai_profile.ai_name is not None - assert ai_profile.ai_role is not None - assert 1 <= len(ai_profile.ai_goals) <= 5 - - -@pytest.mark.vcr -@pytest.mark.requires_openai_api_key -async def test_generate_aiconfig_automatic_fallback( - patched_api_requestor, config, llm_provider -): user_inputs = [ - "T&GF£OIBECC()!*", - "Chef-GPT", - "an AI designed to browse bake a cake.", - "Purchase ingredients", - "Bake a cake", + "y", + "New AI", + "New Role", + "NewConstraint", "", + "NewResource", + "", + "NewBestPractice", "", ] - with patch("autogpt.app.utils.session.prompt", side_effect=user_inputs): - ai_profile = await interactive_ai_profile_setup(config, llm_provider) + with patch("autogpt.app.utils.clean_input", side_effect=user_inputs): + ai_profile, directives = await interactively_revise_ai_settings(ai_profile, directives, config) - assert isinstance(ai_profile, AIProfile) - assert ai_profile.ai_name == "Chef-GPT" - assert ai_profile.ai_role == "an AI designed to browse bake a cake." - assert ai_profile.ai_goals == ["Purchase ingredients", "Bake a cake"] - - -@pytest.mark.vcr -@pytest.mark.requires_openai_api_key -async def test_prompt_user_manual_mode(patched_api_requestor, config, llm_provider): - user_inputs = [ - "--manual", - "Chef-GPT", - "an AI designed to browse bake a cake.", - "Purchase ingredients", - "Bake a cake", - "", - "", - ] - with patch("autogpt.app.utils.session.prompt", side_effect=user_inputs): - ai_profile = await interactive_ai_profile_setup(config, llm_provider) - - assert isinstance(ai_profile, AIProfile) - assert ai_profile.ai_name == "Chef-GPT" - assert ai_profile.ai_role == "an AI designed to browse bake a cake." - assert ai_profile.ai_goals == ["Purchase ingredients", "Bake a cake"] + assert ai_profile.ai_name == "New AI" + assert ai_profile.ai_role == "New Role" + assert directives.resources == ["NewResource"] + assert directives.constraints == ["NewConstraint"] + assert directives.best_practices == ["NewBestPractice"]