mirror of
https://github.com/aljazceru/Auto-GPT.git
synced 2026-01-13 11:14:25 +01:00
591 lines
20 KiB
Python
591 lines
20 KiB
Python
"""The application entry point. Can be invoked by a CLI or any other front end application."""
|
|
import enum
|
|
import logging
|
|
import math
|
|
import signal
|
|
import sys
|
|
from pathlib import Path
|
|
from types import FrameType
|
|
from typing import Optional
|
|
|
|
from colorama import Fore, Style
|
|
|
|
from autogpt.agents import Agent, AgentThoughts, CommandArgs, CommandName
|
|
from autogpt.agents.utils.exceptions import InvalidAgentResponseError
|
|
from autogpt.app.configurator import create_config
|
|
from autogpt.app.setup import interactive_ai_config_setup
|
|
from autogpt.app.spinner import Spinner
|
|
from autogpt.app.utils import (
|
|
clean_input,
|
|
get_current_git_branch,
|
|
get_latest_bulletin,
|
|
get_legal_warning,
|
|
markdown_to_ansi_style,
|
|
)
|
|
from autogpt.commands import COMMAND_CATEGORIES
|
|
from autogpt.config import AIConfig, Config, ConfigBuilder, check_openai_api_key
|
|
from autogpt.llm.api_manager import ApiManager
|
|
from autogpt.logs.config import configure_chat_plugins, configure_logging
|
|
from autogpt.logs.helpers import print_attribute
|
|
from autogpt.memory.vector import get_memory
|
|
from autogpt.models.command_registry import CommandRegistry
|
|
from autogpt.plugins import scan_plugins
|
|
from autogpt.prompts.prompt import DEFAULT_TRIGGERING_PROMPT
|
|
from autogpt.speech import say_text
|
|
from autogpt.workspace import Workspace
|
|
from scripts.install_plugin_deps import install_plugin_dependencies
|
|
|
|
|
|
def run_auto_gpt(
|
|
continuous: bool,
|
|
continuous_limit: int,
|
|
ai_settings: str,
|
|
prompt_settings: str,
|
|
skip_reprompt: bool,
|
|
speak: bool,
|
|
debug: bool,
|
|
gpt3only: bool,
|
|
gpt4only: bool,
|
|
memory_type: str,
|
|
browser_name: str,
|
|
allow_downloads: bool,
|
|
skip_news: bool,
|
|
working_directory: Path,
|
|
workspace_directory: str | Path,
|
|
install_plugin_deps: bool,
|
|
ai_name: Optional[str] = None,
|
|
ai_role: Optional[str] = None,
|
|
ai_goals: tuple[str] = tuple(),
|
|
):
|
|
config = ConfigBuilder.build_config_from_env(workdir=working_directory)
|
|
|
|
# TODO: fill in llm values here
|
|
check_openai_api_key(config)
|
|
|
|
create_config(
|
|
config,
|
|
continuous,
|
|
continuous_limit,
|
|
ai_settings,
|
|
prompt_settings,
|
|
skip_reprompt,
|
|
speak,
|
|
debug,
|
|
gpt3only,
|
|
gpt4only,
|
|
memory_type,
|
|
browser_name,
|
|
allow_downloads,
|
|
skip_news,
|
|
)
|
|
|
|
# Set up logging module
|
|
configure_logging(config)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
if config.continuous_mode:
|
|
for line in get_legal_warning().split("\n"):
|
|
logger.warn(
|
|
extra={
|
|
"title": "LEGAL:",
|
|
"title_color": Fore.RED,
|
|
"preserve_color": True,
|
|
},
|
|
msg=markdown_to_ansi_style(line),
|
|
)
|
|
|
|
if not config.skip_news:
|
|
motd, is_new_motd = get_latest_bulletin()
|
|
if motd:
|
|
motd = markdown_to_ansi_style(motd)
|
|
for motd_line in motd.split("\n"):
|
|
logger.info(
|
|
extra={
|
|
"title": "NEWS:",
|
|
"title_color": Fore.GREEN,
|
|
"preserve_color": True,
|
|
},
|
|
msg=motd_line,
|
|
)
|
|
if is_new_motd and not config.chat_messages_enabled:
|
|
input(
|
|
Fore.MAGENTA
|
|
+ Style.BRIGHT
|
|
+ "NEWS: Bulletin was updated! Press Enter to continue..."
|
|
+ Style.RESET_ALL
|
|
)
|
|
|
|
git_branch = get_current_git_branch()
|
|
if git_branch and git_branch != "stable":
|
|
logger.warn(
|
|
f"You are running on `{git_branch}` branch"
|
|
" - this is not a supported branch."
|
|
)
|
|
if sys.version_info < (3, 10):
|
|
logger.error(
|
|
"WARNING: You are running on an older version of Python. "
|
|
"Some people have observed problems with certain "
|
|
"parts of Auto-GPT with this version. "
|
|
"Please consider upgrading to Python 3.10 or higher.",
|
|
)
|
|
|
|
if install_plugin_deps:
|
|
install_plugin_dependencies()
|
|
|
|
# TODO: have this directory live outside the repository (e.g. in a user's
|
|
# home directory) and have it come in as a command line argument or part of
|
|
# the env file.
|
|
config.workspace_path = Workspace.init_workspace_directory(
|
|
config, workspace_directory
|
|
)
|
|
|
|
# HACK: doing this here to collect some globals that depend on the workspace.
|
|
config.file_logger_path = Workspace.build_file_logger_path(config.workspace_path)
|
|
|
|
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_config = construct_main_ai_config(
|
|
config,
|
|
name=ai_name,
|
|
role=ai_role,
|
|
goals=ai_goals,
|
|
)
|
|
ai_config.command_registry = command_registry
|
|
# print(prompt)
|
|
|
|
# Initialize memory and make sure it is empty.
|
|
# this is particularly important for indexing and referencing pinecone memory
|
|
memory = get_memory(config)
|
|
memory.clear()
|
|
print_attribute("Configured Memory", memory.__class__.__name__)
|
|
|
|
print_attribute("Configured Browser", config.selenium_web_browser)
|
|
|
|
agent = Agent(
|
|
memory=memory,
|
|
command_registry=command_registry,
|
|
triggering_prompt=DEFAULT_TRIGGERING_PROMPT,
|
|
ai_config=ai_config,
|
|
config=config,
|
|
)
|
|
|
|
run_interaction_loop(agent)
|
|
|
|
|
|
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"
|
|
|
|
|
|
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.
|
|
config = agent.config
|
|
ai_config = agent.ai_config
|
|
logger = logging.getLogger(__name__)
|
|
|
|
logger.debug(f"{ai_config.ai_name} System Prompt: {agent.system_prompt}")
|
|
|
|
cycle_budget = cycles_remaining = _get_cycle_budget(
|
|
config.continuous_mode, config.continuous_limit
|
|
)
|
|
spinner = Spinner("Thinking...", plain_output=config.plain_output)
|
|
|
|
def graceful_agent_interrupt(signum: int, frame: Optional[FrameType]) -> None:
|
|
nonlocal cycle_budget, cycles_remaining, spinner
|
|
if cycles_remaining in [0, 1]:
|
|
logger.error("Interrupt signal received. Stopping Auto-GPT immediately.")
|
|
sys.exit()
|
|
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()
|
|
|
|
# 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 #
|
|
########
|
|
# Have the agent determine the next action to take.
|
|
with spinner:
|
|
try:
|
|
command_name, command_args, assistant_reply_dict = agent.think()
|
|
except InvalidAgentResponseError as e:
|
|
logger.warn(f"The agent's thoughts could not be parsed: {e}")
|
|
consecutive_failures += 1
|
|
if consecutive_failures >= 3:
|
|
logger.error(
|
|
f"The agent failed to output valid thoughts {consecutive_failures} "
|
|
"times in a row. Terminating..."
|
|
)
|
|
sys.exit()
|
|
continue
|
|
|
|
consecutive_failures = 0
|
|
|
|
###############
|
|
# Update User #
|
|
###############
|
|
# Print the assistant's thoughts and the next command to the user.
|
|
update_user(config, ai_config, command_name, command_args, assistant_reply_dict)
|
|
|
|
##################
|
|
# Get user input #
|
|
##################
|
|
if cycles_remaining == 1: # Last cycle
|
|
user_feedback, user_input, new_cycles_remaining = get_user_feedback(
|
|
config,
|
|
ai_config,
|
|
)
|
|
|
|
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.warn("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
|
|
|
|
result = agent.execute(command_name, command_args, user_input)
|
|
|
|
if result.status == "success":
|
|
logger.info(
|
|
str(result.results),
|
|
extra={"title": "SYSTEM:", "title_color": Fore.YELLOW},
|
|
)
|
|
elif result.status == "error":
|
|
logger.warn(
|
|
f"Command {command_name} returned an error: {result.error or result.reason}"
|
|
)
|
|
|
|
|
|
def update_user(
|
|
config: Config,
|
|
ai_config: AIConfig,
|
|
command_name: CommandName,
|
|
command_args: CommandArgs,
|
|
assistant_reply_dict: AgentThoughts,
|
|
) -> None:
|
|
"""Prints the assistant's thoughts and the next command to the user.
|
|
|
|
Args:
|
|
config: The program's configuration.
|
|
ai_config: The AI's configuration.
|
|
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_config.ai_name, assistant_reply_dict, config)
|
|
|
|
if config.speak_mode:
|
|
say_text(f"I want to execute {command_name}", config)
|
|
|
|
# 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,
|
|
},
|
|
)
|
|
|
|
|
|
def get_user_feedback(
|
|
config: Config,
|
|
ai_config: AIConfig,
|
|
) -> tuple[UserFeedback, str, int | None]:
|
|
"""Gets the user's feedback on the assistant's reply.
|
|
|
|
Args:
|
|
config: The program's configuration.
|
|
ai_config: 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_config.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 = clean_input(config, "Waiting for your response...")
|
|
else:
|
|
console_input = 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.warn("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.warn(
|
|
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 construct_main_ai_config(
|
|
config: Config,
|
|
name: Optional[str] = None,
|
|
role: Optional[str] = None,
|
|
goals: tuple[str] = tuple(),
|
|
) -> AIConfig:
|
|
"""Construct the prompt for the AI to respond to
|
|
|
|
Returns:
|
|
str: The prompt string
|
|
"""
|
|
logger = logging.getLogger(__name__)
|
|
|
|
ai_config = AIConfig.load(config.workdir / config.ai_settings_file)
|
|
|
|
# Apply overrides
|
|
if name:
|
|
ai_config.ai_name = name
|
|
if role:
|
|
ai_config.ai_role = role
|
|
if goals:
|
|
ai_config.ai_goals = list(goals)
|
|
|
|
if (
|
|
all([name, role, goals])
|
|
or config.skip_reprompt
|
|
and all([ai_config.ai_name, ai_config.ai_role, ai_config.ai_goals])
|
|
):
|
|
print_attribute("Name :", ai_config.ai_name)
|
|
print_attribute("Role :", ai_config.ai_role)
|
|
print_attribute("Goals:", ai_config.ai_goals)
|
|
print_attribute(
|
|
"API Budget:",
|
|
"infinite" if ai_config.api_budget <= 0 else f"${ai_config.api_budget}",
|
|
)
|
|
elif all([ai_config.ai_name, ai_config.ai_role, ai_config.ai_goals]):
|
|
logger.info(
|
|
extra={"title": f"{Fore.GREEN}Welcome back!{Fore.RESET}"},
|
|
msg=f"Would you like me to return to being {ai_config.ai_name}?",
|
|
)
|
|
should_continue = clean_input(
|
|
config,
|
|
f"""Continue with the last settings?
|
|
Name: {ai_config.ai_name}
|
|
Role: {ai_config.ai_role}
|
|
Goals: {ai_config.ai_goals}
|
|
API Budget: {"infinite" if ai_config.api_budget <= 0 else f"${ai_config.api_budget}"}
|
|
Continue ({config.authorise_key}/{config.exit_key}): """,
|
|
)
|
|
if should_continue.lower() == config.exit_key:
|
|
ai_config = AIConfig()
|
|
|
|
if any([not ai_config.ai_name, not ai_config.ai_role, not ai_config.ai_goals]):
|
|
ai_config = interactive_ai_config_setup(config)
|
|
ai_config.save(config.workdir / config.ai_settings_file)
|
|
|
|
if config.restrict_to_workspace:
|
|
logger.info(
|
|
f"{Fore.YELLOW}NOTE: All files/directories created by this agent"
|
|
f" can be found inside its workspace at:{Fore.RESET} {config.workspace_path}",
|
|
extra={"preserve_color": True},
|
|
)
|
|
# set the total api budget
|
|
api_manager = ApiManager()
|
|
api_manager.set_total_budget(ai_config.api_budget)
|
|
|
|
# Agent Created, print message
|
|
logger.info(
|
|
f"{Fore.LIGHTBLUE_EX}{ai_config.ai_name}{Fore.RESET} has been created with the following details:",
|
|
extra={"preserve_color": True},
|
|
)
|
|
|
|
# Print the ai_config details
|
|
print_attribute("Name :", ai_config.ai_name)
|
|
print_attribute("Role :", ai_config.ai_role)
|
|
print_attribute("Goals:", "")
|
|
for goal in ai_config.ai_goals:
|
|
logger.info(f"- {goal}")
|
|
|
|
return ai_config
|
|
|
|
|
|
def print_assistant_thoughts(
|
|
ai_name: str,
|
|
assistant_reply_json_valid: dict,
|
|
config: Config,
|
|
) -> None:
|
|
from autogpt.speech import say_text
|
|
|
|
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("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 config.speak_mode:
|
|
say_text(assistant_thoughts_speak, config)
|
|
else:
|
|
print_attribute("SPEAK", assistant_thoughts_speak, title_color=Fore.YELLOW)
|
|
|
|
|
|
def remove_ansi_escape(s: str) -> str:
|
|
return s.replace("\x1B", "")
|