mirror of
https://github.com/aljazceru/Auto-GPT.git
synced 2025-12-18 22:44:21 +01:00
* Move rename module `agent` -> `agents`
* WIP: abstract agent structure into base class and port Agent
* Move command arg path sanitization to decorator
* Add fallback token limit in llm.utils.create_chat_completion
* Rebase `MessageHistory` class on `ChatSequence` class
* Fix linting
* Consolidate logging modules
* Wham Bam Boom
* Fix tests & linting complaints
* Update Agent class docstring
* Fix Agent import in autogpt.llm.providers.openai
* Fix agent kwarg in test_execute_code.py
* Fix benchmarks.py
* Clean up lingering Agent(ai_name=...) initializations
* Fix agent kwarg
* Make sanitize_path_arg decorator more robust
* Fix linting
* Fix command enabling lambda's
* Use relative paths in file ops logger
* Fix test_execute_python_file_not_found
* Fix Config model validation breaking on .plugins
* Define validator for Config.plugins
* Fix Config model issues
* Fix agent iteration budget in testing
* Fix declaration of context_while_think
* Fix Agent.parse_and_process_response signature
* Fix Agent cycle_budget usages
* Fix budget checking in BaseAgent.__next__
* Fix cycle budget initialization
* Fix function calling in BaseAgent.think()
* Include functions in token length calculation
* Fix Config errors
* Add debug thing to patched_api_requestor to investigate HTTP 400 errors
* If this works I'm gonna be sad
* Fix BaseAgent cycle budget logic and document attributes
* Document attributes on `Agent`
* Fix import issues between Agent and MessageHistory
* Improve typing
* Extract application code from the agent (#4982)
* Extract application code from the agent
* Wrap interaction loop in a function and call in benchmarks
* Forgot the important function call
* Add docstrings and inline comments to run loop
* Update typing and docstrings in agent
* Docstring formatting
* Separate prompt construction from on_before_think
* Use `self.default_cycle_instruction` in `Agent.think()`
* Fix formatting
* hot fix the SIGINT handler (#4997)
The signal handler in the autogpt/main.py doesn't work properly because
of the clean_input(...) func. This commit remedies this issue. The issue
is mentioned in
3966cdfd69 (r1264278776)
* Update the sigint handler to be smart enough to actually work (#4999)
* Update the sigint handler to be smart enough to actually work
* Update autogpt/main.py
Co-authored-by: Reinier van der Leer <github@pwuts.nl>
* Can still use context manager
* Merge in upstream
---------
Co-authored-by: Reinier van der Leer <github@pwuts.nl>
* Fix CI
* Fix initial prompt construction
* off by one error
* allow exit/EXIT to shut down app
* Remove dead code
---------
Co-authored-by: collijk <collijk@uw.edu>
Co-authored-by: Cyrus <39694513+cyrus-hawk@users.noreply.github.com>
459 lines
16 KiB
Python
459 lines
16 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.config import AIConfig, Config, ConfigBuilder, check_openai_api_key
|
|
from autogpt.configurator import create_config
|
|
from autogpt.logs import logger, print_assistant_thoughts, remove_ansi_escape
|
|
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, construct_main_ai_config
|
|
from autogpt.speech import say_text
|
|
from autogpt.spinner import Spinner
|
|
from autogpt.utils import (
|
|
clean_input,
|
|
get_current_git_branch,
|
|
get_latest_bulletin,
|
|
get_legal_warning,
|
|
markdown_to_ansi_style,
|
|
)
|
|
from autogpt.workspace import Workspace
|
|
from scripts.install_plugin_deps import install_plugin_dependencies
|
|
|
|
COMMAND_CATEGORIES = [
|
|
"autogpt.commands.execute_code",
|
|
"autogpt.commands.file_operations",
|
|
"autogpt.commands.web_search",
|
|
"autogpt.commands.web_selenium",
|
|
"autogpt.commands.task_statuses",
|
|
]
|
|
|
|
|
|
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,
|
|
workspace_directory: str | Path,
|
|
install_plugin_deps: bool,
|
|
ai_name: Optional[str] = None,
|
|
ai_role: Optional[str] = None,
|
|
ai_goals: tuple[str] = tuple(),
|
|
):
|
|
# Configure logging before we do anything else.
|
|
logger.set_level(logging.DEBUG if debug else logging.INFO)
|
|
|
|
config = ConfigBuilder.build_config_from_env()
|
|
# HACK: This is a hack to allow the config into the logger without having to pass it around everywhere
|
|
# or import it directly.
|
|
logger.config = config
|
|
|
|
# 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,
|
|
)
|
|
|
|
if config.continuous_mode:
|
|
for line in get_legal_warning().split("\n"):
|
|
logger.warn(markdown_to_ansi_style(line), "LEGAL:", Fore.RED)
|
|
|
|
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(motd_line, "NEWS:", Fore.GREEN)
|
|
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.typewriter_log(
|
|
"WARNING: ",
|
|
Fore.RED,
|
|
f"You are running on `{git_branch}` branch "
|
|
"- this is not a supported branch.",
|
|
)
|
|
if sys.version_info < (3, 10):
|
|
logger.typewriter_log(
|
|
"WARNING: ",
|
|
Fore.RED,
|
|
"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.
|
|
workspace_directory = Workspace.get_workspace_directory(config, workspace_directory)
|
|
|
|
# HACK: doing this here to collect some globals that depend on the workspace.
|
|
Workspace.build_file_logger_path(config, workspace_directory)
|
|
|
|
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.'}"
|
|
)
|
|
|
|
ai_config = construct_main_ai_config(
|
|
config,
|
|
name=ai_name,
|
|
role=ai_role,
|
|
goals=ai_goals,
|
|
)
|
|
ai_config.command_registry = command_registry
|
|
# print(prompt)
|
|
|
|
# add chat plugins capable of report to logger
|
|
if config.chat_messages_enabled:
|
|
for plugin in config.plugins:
|
|
if hasattr(plugin, "can_handle_report") and plugin.can_handle_report():
|
|
logger.info(f"Loaded plugin into logger: {plugin.__class__.__name__}")
|
|
logger.chat_plugins.append(plugin)
|
|
|
|
# Initialize memory and make sure it is empty.
|
|
# this is particularly important for indexing and referencing pinecone memory
|
|
memory = get_memory(config)
|
|
memory.clear()
|
|
logger.typewriter_log(
|
|
"Using memory of type:", Fore.GREEN, f"{memory.__class__.__name__}"
|
|
)
|
|
logger.typewriter_log("Using Browser:", Fore.GREEN, config.selenium_web_browser)
|
|
|
|
agent = Agent(
|
|
memory=memory,
|
|
command_registry=command_registry,
|
|
triggering_prompt=DEFAULT_TRIGGERING_PROMPT,
|
|
workspace_directory=workspace_directory,
|
|
ai_config=ai_config,
|
|
config=config,
|
|
)
|
|
|
|
run_interaction_loop(agent)
|
|
|
|
|
|
def _get_cycle_budget(continuous_mode: bool, continuous_limit: int) -> int | None:
|
|
# 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.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, math.inf]:
|
|
logger.typewriter_log(
|
|
"Interrupt signal received. Stopping continuous command execution "
|
|
"immediately.",
|
|
Fore.RED,
|
|
)
|
|
sys.exit()
|
|
else:
|
|
restart_spinner = spinner.running
|
|
if spinner.running:
|
|
spinner.stop()
|
|
|
|
logger.typewriter_log(
|
|
"Interrupt signal received. Stopping continuous command execution.",
|
|
Fore.RED,
|
|
)
|
|
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 #
|
|
#########################
|
|
|
|
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:
|
|
command_name, command_args, assistant_reply_dict = agent.think()
|
|
|
|
###############
|
|
# 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.typewriter_log(
|
|
"RESUMING CONTINUOUS EXECUTION: ",
|
|
Fore.MAGENTA,
|
|
f"The cycle budget is {cycle_budget}.",
|
|
)
|
|
# Case 2: The agent used up its cycle budget -> reset
|
|
cycles_remaining = cycle_budget + 1
|
|
logger.typewriter_log(
|
|
"-=-=-=-=-=-=-= COMMAND AUTHORISED BY USER -=-=-=-=-=-=-=",
|
|
Fore.MAGENTA,
|
|
"",
|
|
)
|
|
elif user_feedback == UserFeedback.EXIT:
|
|
logger.typewriter_log("Exiting...", Fore.YELLOW)
|
|
exit()
|
|
else: # user_feedback == UserFeedback.TEXT
|
|
command_name = "human_feedback"
|
|
else:
|
|
user_input = None
|
|
# First log new-line so user can differentiate sections better in console
|
|
logger.typewriter_log("\n")
|
|
if cycles_remaining != math.inf:
|
|
# Print authorized commands left value
|
|
logger.typewriter_log(
|
|
"AUTHORISED COMMANDS LEFT: ", Fore.CYAN, f"{cycles_remaining}"
|
|
)
|
|
|
|
###################
|
|
# 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
|
|
result = agent.execute(command_name, command_args, user_input)
|
|
|
|
if result is not None:
|
|
logger.typewriter_log("SYSTEM: ", Fore.YELLOW, result)
|
|
else:
|
|
logger.typewriter_log("SYSTEM: ", Fore.YELLOW, "Unable to execute command")
|
|
|
|
|
|
def update_user(
|
|
config: Config,
|
|
ai_config: AIConfig,
|
|
command_name: CommandName | None,
|
|
command_args: CommandArgs | None,
|
|
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.
|
|
"""
|
|
|
|
print_assistant_thoughts(ai_config.ai_name, assistant_reply_dict, config)
|
|
|
|
if command_name is not None:
|
|
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
|
|
logger.typewriter_log("\n")
|
|
logger.typewriter_log(
|
|
"NEXT ACTION: ",
|
|
Fore.CYAN,
|
|
f"COMMAND = {Fore.CYAN}{remove_ansi_escape(command_name)}{Style.RESET_ALL} "
|
|
f"ARGUMENTS = {Fore.CYAN}{command_args}{Style.RESET_ALL}",
|
|
)
|
|
elif command_name.lower().startswith("error"):
|
|
logger.typewriter_log(
|
|
"ERROR: ",
|
|
Fore.RED,
|
|
f"The Agent failed to select an action. " f"Error message: {command_name}",
|
|
)
|
|
else:
|
|
logger.typewriter_log(
|
|
"NO ACTION SELECTED: ",
|
|
Fore.RED,
|
|
f"The Agent failed to select an action.",
|
|
)
|
|
|
|
|
|
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.
|
|
"""
|
|
# ### 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
|