diff options
Diffstat (limited to 'autogpts/autogpt/autogpt/app/main.py')
-rw-r--r-- | autogpts/autogpt/autogpt/app/main.py | 759 |
1 files changed, 759 insertions, 0 deletions
diff --git a/autogpts/autogpt/autogpt/app/main.py b/autogpts/autogpt/autogpt/app/main.py new file mode 100644 index 000000000..20f1d9872 --- /dev/null +++ b/autogpts/autogpt/autogpt/app/main.py @@ -0,0 +1,759 @@ +""" +The application entry point. Can be invoked by a CLI or any other front end application. +""" +import enum +import logging +import math +import os +import re +import signal +import sys +from pathlib import Path +from types import FrameType +from typing import TYPE_CHECKING, Optional + +from colorama import Fore, Style +from forge.sdk.db import AgentDB + +if TYPE_CHECKING: + from autogpt.agents.agent import Agent + +from autogpt.agent_factory.configurators import configure_agent_with_state, 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.utils.exceptions import AgentTerminated, InvalidAgentResponseError +from autogpt.config import ( + AIDirectives, + AIProfile, + Config, + ConfigBuilder, + assert_config_has_openai_api_key, +) +from autogpt.core.resource.model_providers.openai import OpenAIProvider +from autogpt.core.runner.client_lib.utils import coroutine +from autogpt.logs.config import configure_chat_plugins, configure_logging +from autogpt.logs.helpers import print_attribute, speak +from autogpt.plugins import scan_plugins +from scripts.install_plugin_deps import install_plugin_dependencies + +from .configurator import apply_overrides_to_config +from .setup import apply_overrides_to_ai_settings, interactively_revise_ai_settings +from .spinner import Spinner +from .utils import ( + clean_input, + get_legal_warning, + markdown_to_ansi_style, + print_git_branch_info, + print_motd, + print_python_version_info, +) + + +@coroutine +async def run_auto_gpt( + continuous: bool = False, + continuous_limit: Optional[int] = None, + ai_settings: Optional[Path] = None, + prompt_settings: Optional[Path] = None, + skip_reprompt: bool = False, + speak: bool = False, + debug: bool = False, + log_level: Optional[str] = None, + log_format: Optional[str] = None, + log_file_format: Optional[str] = None, + gpt3only: bool = False, + gpt4only: bool = False, + browser_name: Optional[str] = None, + allow_downloads: bool = False, + skip_news: bool = False, + workspace_directory: Optional[Path] = None, + install_plugin_deps: bool = False, + override_ai_name: Optional[str] = None, + override_ai_role: Optional[str] = None, + 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() + + # TODO: fill in llm values here + assert_config_has_openai_api_key(config) + + apply_overrides_to_config( + config=config, + continuous=continuous, + continuous_limit=continuous_limit, + ai_settings_file=ai_settings, + prompt_settings_file=prompt_settings, + skip_reprompt=skip_reprompt, + speak=speak, + debug=debug, + log_level=log_level, + log_format=log_format, + log_file_format=log_file_format, + gpt3only=gpt3only, + gpt4only=gpt4only, + browser_name=browser_name, + allow_downloads=allow_downloads, + skip_news=skip_news, + ) + + # Set up logging module + configure_logging( + **config.logging.dict(), + tts_config=config.tts_config, + ) + + llm_provider = _configure_openai_provider(config) + + logger = logging.getLogger(__name__) + + if config.continuous_mode: + for line in get_legal_warning().split("\n"): + logger.warning( + extra={ + "title": "LEGAL:", + "title_color": Fore.RED, + "preserve_color": True, + }, + msg=markdown_to_ansi_style(line), + ) + + if not config.skip_news: + print_motd(config, logger) + print_git_branch_info(logger) + print_python_version_info(logger) + print_attribute("Smart LLM", config.smart_llm) + print_attribute("Fast LLM", config.fast_llm) + print_attribute("Browser", config.selenium_web_browser) + if config.continuous_mode: + print_attribute("Continuous Mode", "ENABLED", title_color=Fore.YELLOW) + if continuous_limit: + print_attribute("Continuous Limit", config.continuous_limit) + if config.tts_config.speak_mode: + print_attribute("Speak Mode", "ENABLED") + if ai_settings: + print_attribute("Using AI Settings File", ai_settings) + if prompt_settings: + print_attribute("Using Prompt Settings File", prompt_settings) + if config.allow_downloads: + print_attribute("Native Downloading", "ENABLED") + + if install_plugin_deps: + install_plugin_dependencies() + + config.plugins = scan_plugins(config) + configure_chat_plugins(config) + + # Let user choose an existing agent to run + agent_manager = AgentManager(config.app_data_dir) + existing_agents = agent_manager.list_agents() + load_existing_agent = "" + if existing_agents: + print( + "Existing agents\n---------------\n" + + "\n".join(f"{i} - {id}" for i, id in enumerate(existing_agents, 1)) + ) + load_existing_agent = await clean_input( + config, + "Enter the number or name of the agent to run," + " or hit enter to create a new one:", + ) + if re.match(r"^\d+$", load_existing_agent): + load_existing_agent = existing_agents[int(load_existing_agent) - 1] + elif load_existing_agent and load_existing_agent not in existing_agents: + raise ValueError(f"Unknown agent '{load_existing_agent}'") + + # Either load existing or set up new agent state + agent = None + agent_state = None + + ############################ + # Resume an Existing Agent # + ############################ + if load_existing_agent: + agent_state = agent_manager.retrieve_state(load_existing_agent) + while True: + answer = await clean_input(config, "Resume? [Y/n]") + if answer.lower() == "y": + break + elif answer.lower() == "n": + agent_state = None + break + else: + print("Please respond with 'y' or 'n'") + + if agent_state: + agent = configure_agent_with_state( + state=agent_state, + app_config=config, + llm_provider=llm_provider, + ) + apply_overrides_to_ai_settings( + ai_profile=agent.state.ai_profile, + directives=agent.state.directives, + override_name=override_ai_name, + override_role=override_ai_role, + resources=resources, + constraints=constraints, + best_practices=best_practices, + replace_directives=override_directives, + ) + + # 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=agent.state.ai_profile, + directives=agent.state.directives, + app_config=config, + ) + else: + logger.info("AI config overrides specified through CLI; skipping revision") + + ###################### + # Set up a new Agent # + ###################### + if not agent: + task = await clean_input( + config, + "Enter the task that you want AutoGPT to execute," + " with as much detail as possible:", + ) + base_ai_directives = AIDirectives.from_file(config.prompt_settings_file) + + 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, + override_name=override_ai_name, + override_role=override_ai_role, + resources=resources, + constraints=constraints, + best_practices=best_practices, + replace_directives=override_directives, + ) + + # 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 = create_agent( + task=task, + ai_profile=ai_profile, + directives=ai_directives, + app_config=config, + llm_provider=llm_provider, + ) + agent.attach_fs(agent_manager.get_agent_dir(agent.state.agent_id)) + + if not agent.config.allow_fs_access: + logger.info( + f"{Fore.YELLOW}" + "NOTE: All files/directories created by this agent can be found " + f"inside its workspace at:{Fore.RESET} {agent.workspace.root}", + extra={"preserve_color": True}, + ) + + ################# + # Run the Agent # + ################# + try: + await run_interaction_loop(agent) + except AgentTerminated: + agent_id = agent.state.agent_id + logger.info(f"Saving state of {agent_id}...") + + # Allow user to Save As other ID + save_as_id = ( + await clean_input( + config, + f"Press enter to save as '{agent_id}'," + " or enter a different ID to save to:", + ) + or agent_id + ) + if save_as_id and save_as_id != agent_id: + agent.set_id( + new_id=save_as_id, + new_agent_dir=agent_manager.get_agent_dir(save_as_id), + ) + # TODO: clone workspace if user wants that + # TODO: ... OR allow many-to-one relations of agents and workspaces + + agent.state.save_to_json_file(agent.file_manager.state_file_path) + + +@coroutine +async def run_auto_gpt_server( + prompt_settings: Optional[Path] = None, + debug: bool = False, + log_level: Optional[str] = None, + log_format: Optional[str] = None, + log_file_format: Optional[str] = None, + gpt3only: bool = False, + gpt4only: bool = False, + browser_name: Optional[str] = None, + allow_downloads: bool = False, + install_plugin_deps: bool = False, +): + from .agent_protocol_server import AgentProtocolServer + + config = ConfigBuilder.build_config_from_env() + + # TODO: fill in llm values here + assert_config_has_openai_api_key(config) + + apply_overrides_to_config( + config=config, + prompt_settings_file=prompt_settings, + debug=debug, + log_level=log_level, + log_format=log_format, + log_file_format=log_file_format, + gpt3only=gpt3only, + gpt4only=gpt4only, + browser_name=browser_name, + allow_downloads=allow_downloads, + ) + + # Set up logging module + configure_logging( + **config.logging.dict(), + tts_config=config.tts_config, + ) + + llm_provider = _configure_openai_provider(config) + + if install_plugin_deps: + install_plugin_dependencies() + + config.plugins = scan_plugins(config) + + # Set up & start server + database = AgentDB( + database_string=os.getenv("AP_SERVER_DB_URL", "sqlite:///data/ap_server.db"), + debug_enabled=debug, + ) + port: int = int(os.getenv("AP_SERVER_PORT", default=8000)) + server = AgentProtocolServer( + app_config=config, database=database, llm_provider=llm_provider + ) + await server.start(port=port) + + logging.getLogger().info( + f"Total OpenAI session cost: " + f"${round(sum(b.total_cost for b in server._task_budgets.values()), 2)}" + ) + + +def _configure_openai_provider(config: Config) -> OpenAIProvider: + """Create a configured OpenAIProvider object. + + Args: + config: The program's configuration. + + Returns: + A configured OpenAIProvider object. + """ + if config.openai_credentials is None: + raise RuntimeError("OpenAI key is not configured") + + openai_settings = OpenAIProvider.default_settings.copy(deep=True) + openai_settings.credentials = config.openai_credentials + return OpenAIProvider( + settings=openai_settings, + logger=logging.getLogger("OpenAIProvider"), + ) + + +def _get_cycle_budget(continuous_mode: bool, continuous_limit: int) -> int | float: + # Translate from the continuous_mode/continuous_limit config + # to a cycle_budget (maximum number of cycles to run without checking in with the + # user) and a count of cycles_remaining before we check in.. + if continuous_mode: + cycle_budget = continuous_limit if continuous_limit else math.inf + else: + cycle_budget = 1 + + return cycle_budget + + +class UserFeedback(str, enum.Enum): + """Enum for user feedback.""" + + AUTHORIZE = "GENERATE NEXT COMMAND JSON" + EXIT = "EXIT" + TEXT = "TEXT" + + +async def run_interaction_loop( + agent: "Agent", +) -> None: + """Run the main interaction loop for the agent. + + Args: + agent: The agent to run the interaction loop for. + + Returns: + None + """ + # These contain both application config and agent config, so grab them here. + legacy_config = agent.legacy_config + ai_profile = agent.ai_profile + logger = logging.getLogger(__name__) + + cycle_budget = cycles_remaining = _get_cycle_budget( + legacy_config.continuous_mode, legacy_config.continuous_limit + ) + spinner = Spinner( + "Thinking...", plain_output=legacy_config.logging.plain_console_output + ) + stop_reason = None + + def graceful_agent_interrupt(signum: int, frame: Optional[FrameType]) -> None: + nonlocal cycle_budget, cycles_remaining, spinner, stop_reason + if stop_reason: + logger.error("Quitting immediately...") + sys.exit() + if cycles_remaining in [0, 1]: + logger.warning("Interrupt signal received: shutting down gracefully.") + logger.warning( + "Press Ctrl+C again if you want to stop AutoGPT immediately." + ) + stop_reason = AgentTerminated("Interrupt signal received") + else: + restart_spinner = spinner.running + if spinner.running: + spinner.stop() + + logger.error( + "Interrupt signal received: stopping continuous command execution." + ) + cycles_remaining = 1 + if restart_spinner: + spinner.start() + + def handle_stop_signal() -> None: + if stop_reason: + raise stop_reason + + # Set up an interrupt signal for the agent. + signal.signal(signal.SIGINT, graceful_agent_interrupt) + + ######################### + # Application Main Loop # + ######################### + + # Keep track of consecutive failures of the agent + consecutive_failures = 0 + + while cycles_remaining > 0: + logger.debug(f"Cycle budget: {cycle_budget}; remaining: {cycles_remaining}") + + ######## + # Plan # + ######## + handle_stop_signal() + # Have the agent determine the next action to take. + with spinner: + try: + ( + command_name, + command_args, + assistant_reply_dict, + ) = await agent.propose_action() + except InvalidAgentResponseError as e: + logger.warning(f"The agent's thoughts could not be parsed: {e}") + consecutive_failures += 1 + if consecutive_failures >= 3: + logger.error( + "The agent failed to output valid thoughts" + f" {consecutive_failures} times in a row. Terminating..." + ) + raise AgentTerminated( + "The agent failed to output valid thoughts" + f" {consecutive_failures} times in a row." + ) + continue + + consecutive_failures = 0 + + ############### + # Update User # + ############### + # Print the assistant's thoughts and the next command to the user. + update_user( + ai_profile, + command_name, + command_args, + assistant_reply_dict, + speak_mode=legacy_config.tts_config.speak_mode, + ) + + ################## + # Get user input # + ################## + handle_stop_signal() + if cycles_remaining == 1: # Last cycle + user_feedback, user_input, new_cycles_remaining = await get_user_feedback( + legacy_config, + ai_profile, + ) + + if user_feedback == UserFeedback.AUTHORIZE: + if new_cycles_remaining is not None: + # Case 1: User is altering the cycle budget. + if cycle_budget > 1: + cycle_budget = new_cycles_remaining + 1 + # Case 2: User is running iteratively and + # has initiated a one-time continuous cycle + cycles_remaining = new_cycles_remaining + 1 + else: + # Case 1: Continuous iteration was interrupted -> resume + if cycle_budget > 1: + logger.info( + f"The cycle budget is {cycle_budget}.", + extra={ + "title": "RESUMING CONTINUOUS EXECUTION", + "title_color": Fore.MAGENTA, + }, + ) + # Case 2: The agent used up its cycle budget -> reset + cycles_remaining = cycle_budget + 1 + logger.info( + "-=-=-=-=-=-=-= COMMAND AUTHORISED BY USER -=-=-=-=-=-=-=", + extra={"color": Fore.MAGENTA}, + ) + elif user_feedback == UserFeedback.EXIT: + logger.warning("Exiting...") + exit() + else: # user_feedback == UserFeedback.TEXT + command_name = "human_feedback" + else: + user_input = "" + # First log new-line so user can differentiate sections better in console + print() + if cycles_remaining != math.inf: + # Print authorized commands left value + print_attribute( + "AUTHORIZED_COMMANDS_LEFT", cycles_remaining, title_color=Fore.CYAN + ) + + ################### + # Execute Command # + ################### + # Decrement the cycle counter first to reduce the likelihood of a SIGINT + # happening during command execution, setting the cycles remaining to 1, + # and then having the decrement set it to 0, exiting the application. + if command_name != "human_feedback": + cycles_remaining -= 1 + + if not command_name: + continue + + handle_stop_signal() + + if command_name: + result = await agent.execute(command_name, command_args, user_input) + + if result.status == "success": + logger.info( + result, extra={"title": "SYSTEM:", "title_color": Fore.YELLOW} + ) + elif result.status == "error": + logger.warning( + f"Command {command_name} returned an error: " + f"{result.error or result.reason}" + ) + + +def update_user( + ai_profile: AIProfile, + command_name: CommandName, + command_args: CommandArgs, + assistant_reply_dict: AgentThoughts, + speak_mode: bool = False, +) -> None: + """Prints the assistant's thoughts and the next command to the user. + + Args: + config: The program's configuration. + ai_profile: The AI's personality/profile + command_name: The name of the command to execute. + command_args: The arguments for the command. + assistant_reply_dict: The assistant's reply. + """ + logger = logging.getLogger(__name__) + + print_assistant_thoughts( + ai_name=ai_profile.ai_name, + assistant_reply_json_valid=assistant_reply_dict, + speak_mode=speak_mode, + ) + + if speak_mode: + speak(f"I want to execute {command_name}") + + # First log new-line so user can differentiate sections better in console + print() + logger.info( + f"COMMAND = {Fore.CYAN}{remove_ansi_escape(command_name)}{Style.RESET_ALL} " + f"ARGUMENTS = {Fore.CYAN}{command_args}{Style.RESET_ALL}", + extra={ + "title": "NEXT ACTION:", + "title_color": Fore.CYAN, + "preserve_color": True, + }, + ) + + +async def get_user_feedback( + config: Config, + ai_profile: AIProfile, +) -> tuple[UserFeedback, str, int | None]: + """Gets the user's feedback on the assistant's reply. + + Args: + config: The program's configuration. + ai_profile: The AI's configuration. + + Returns: + A tuple of the user's feedback, the user's input, and the number of + cycles remaining if the user has initiated a continuous cycle. + """ + logger = logging.getLogger(__name__) + + # ### GET USER AUTHORIZATION TO EXECUTE COMMAND ### + # Get key press: Prompt the user to press enter to continue or escape + # to exit + logger.info( + f"Enter '{config.authorise_key}' to authorise command, " + f"'{config.authorise_key} -N' to run N continuous commands, " + f"'{config.exit_key}' to exit program, or enter feedback for " + f"{ai_profile.ai_name}..." + ) + + user_feedback = None + user_input = "" + new_cycles_remaining = None + + while user_feedback is None: + # Get input from user + if config.chat_messages_enabled: + console_input = await clean_input(config, "Waiting for your response...") + else: + console_input = await clean_input( + config, Fore.MAGENTA + "Input:" + Style.RESET_ALL + ) + + # Parse user input + if console_input.lower().strip() == config.authorise_key: + user_feedback = UserFeedback.AUTHORIZE + elif console_input.lower().strip() == "": + logger.warning("Invalid input format.") + elif console_input.lower().startswith(f"{config.authorise_key} -"): + try: + user_feedback = UserFeedback.AUTHORIZE + new_cycles_remaining = abs(int(console_input.split(" ")[1])) + except ValueError: + logger.warning( + f"Invalid input format. " + f"Please enter '{config.authorise_key} -N'" + " where N is the number of continuous tasks." + ) + elif console_input.lower() in [config.exit_key, "exit"]: + user_feedback = UserFeedback.EXIT + else: + user_feedback = UserFeedback.TEXT + user_input = console_input + + return user_feedback, user_input, new_cycles_remaining + + +def print_assistant_thoughts( + ai_name: str, + assistant_reply_json_valid: dict, + speak_mode: bool = False, +) -> None: + logger = logging.getLogger(__name__) + + assistant_thoughts_reasoning = None + assistant_thoughts_plan = None + assistant_thoughts_speak = None + assistant_thoughts_criticism = None + + assistant_thoughts = assistant_reply_json_valid.get("thoughts", {}) + assistant_thoughts_text = remove_ansi_escape(assistant_thoughts.get("text", "")) + if assistant_thoughts: + assistant_thoughts_reasoning = remove_ansi_escape( + assistant_thoughts.get("reasoning", "") + ) + assistant_thoughts_plan = remove_ansi_escape(assistant_thoughts.get("plan", "")) + assistant_thoughts_criticism = remove_ansi_escape( + assistant_thoughts.get("self_criticism", "") + ) + assistant_thoughts_speak = remove_ansi_escape( + assistant_thoughts.get("speak", "") + ) + print_attribute( + f"{ai_name.upper()} THOUGHTS", assistant_thoughts_text, title_color=Fore.YELLOW + ) + print_attribute("REASONING", assistant_thoughts_reasoning, title_color=Fore.YELLOW) + if assistant_thoughts_plan: + print_attribute("PLAN", "", title_color=Fore.YELLOW) + # If it's a list, join it into a string + if isinstance(assistant_thoughts_plan, list): + assistant_thoughts_plan = "\n".join(assistant_thoughts_plan) + elif isinstance(assistant_thoughts_plan, dict): + assistant_thoughts_plan = str(assistant_thoughts_plan) + + # Split the input_string using the newline character and dashes + lines = assistant_thoughts_plan.split("\n") + for line in lines: + line = line.lstrip("- ") + logger.info(line.strip(), extra={"title": "- ", "title_color": Fore.GREEN}) + print_attribute( + "CRITICISM", f"{assistant_thoughts_criticism}", title_color=Fore.YELLOW + ) + + # Speak the assistant's thoughts + if assistant_thoughts_speak: + if speak_mode: + speak(assistant_thoughts_speak) + else: + print_attribute("SPEAK", assistant_thoughts_speak, title_color=Fore.YELLOW) + + +def remove_ansi_escape(s: str) -> str: + return s.replace("\x1B", "") |