diff --git a/autogpts/autogpt/autogpt/agent_manager/__init__.py b/autogpts/autogpt/autogpt/agent_manager/__init__.py new file mode 100644 index 00000000..a412566b --- /dev/null +++ b/autogpts/autogpt/autogpt/agent_manager/__init__.py @@ -0,0 +1,3 @@ +from .agent_manager import AgentManager + +__all__ = ["AgentManager"] diff --git a/autogpts/autogpt/autogpt/agent_manager/agent_manager.py b/autogpts/autogpt/autogpt/agent_manager/agent_manager.py new file mode 100644 index 00000000..dc3bc646 --- /dev/null +++ b/autogpts/autogpt/autogpt/agent_manager/agent_manager.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import uuid +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from autogpt.agents.agent import AgentSettings + +from autogpt.agents.utils.agent_file_manager import AgentFileManager + + +class AgentManager: + def __init__(self, app_data_dir: Path): + self.agents_dir = app_data_dir / "agents" + if not self.agents_dir.exists(): + self.agents_dir.mkdir() + + @staticmethod + def generate_id(agent_name: str) -> str: + unique_id = str(uuid.uuid4())[:8] + return f"{agent_name}-{unique_id}" + + def list_agents(self) -> list[str]: + return [ + dir.name + for dir in self.agents_dir.iterdir() + if dir.is_dir() and AgentFileManager(dir).state_file_path.exists() + ] + + def get_agent_dir(self, agent_id: str, must_exist: bool = False) -> Path: + agent_dir = self.agents_dir / agent_id + if must_exist and not agent_dir.exists(): + raise FileNotFoundError(f"No agent with ID '{agent_id}'") + return agent_dir + + def retrieve_state(self, agent_id: str) -> AgentSettings: + from autogpt.agents.agent import AgentSettings + + agent_dir = self.get_agent_dir(agent_id, True) + state_file = AgentFileManager(agent_dir).state_file_path + if not state_file.exists(): + raise FileNotFoundError(f"Agent with ID '{agent_id}' has no state.json") + + state = AgentSettings.load_from_json_file(state_file) + state.agent_data_dir = agent_dir + return state diff --git a/autogpts/autogpt/autogpt/agents/base.py b/autogpts/autogpt/autogpt/agents/base.py index 281df602..20f400a6 100644 --- a/autogpts/autogpt/autogpt/agents/base.py +++ b/autogpts/autogpt/autogpt/agents/base.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import logging from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Optional @@ -89,9 +90,8 @@ class BaseAgentConfiguration(SystemConfiguration): defaults to 75% of `llm.max_tokens`. """ - summary_max_tlength: Optional[ - int - ] = None # TODO: move to ActionHistoryConfiguration + summary_max_tlength: Optional[int] = None + # TODO: move to ActionHistoryConfiguration plugins: list[AutoGPTPluginTemplate] = Field(default_factory=list, exclude=True) @@ -125,6 +125,7 @@ class BaseAgentConfiguration(SystemConfiguration): class BaseAgentSettings(SystemSettings): + agent_id: Optional[str] = None agent_data_dir: Optional[Path] = None ai_profile: AIProfile = Field(default_factory=lambda: AIProfile(ai_name="AutoGPT")) @@ -146,6 +147,17 @@ class BaseAgentSettings(SystemSettings): history: EpisodicActionHistory = Field(default_factory=EpisodicActionHistory) """(STATE) The action history of the agent.""" + def save_to_json_file(self, file_path: Path) -> None: + with file_path.open("w") as f: + json.dump(self.dict(), f) + + @classmethod + def load_from_json_file(cls, file_path: Path): + with file_path.open("r") as f: + agent_settings = json.load(f) + + return cls.parse_obj(agent_settings) + class BaseAgent(Configurable[BaseAgentSettings], ABC): """Base class for all AutoGPT agent classes.""" @@ -194,6 +206,15 @@ class BaseAgent(Configurable[BaseAgentSettings], ABC): logger.debug(f"Created {__class__} '{self.ai_profile.ai_name}'") + def set_id(self, new_id: str, new_agent_dir: Optional[Path] = None): + self.state.agent_id = new_id + if self.state.agent_data_dir: + if not new_agent_dir: + raise ValueError( + "new_agent_dir must be specified if one is currently configured" + ) + self.attach_fs(new_agent_dir) + def attach_fs(self, agent_dir: Path) -> AgentFileManager: self.file_manager = AgentFileManager(agent_dir) self.file_manager.initialize() diff --git a/autogpts/autogpt/autogpt/agents/utils/exceptions.py b/autogpts/autogpt/autogpt/agents/utils/exceptions.py index d6f2d74e..efb85341 100644 --- a/autogpts/autogpt/autogpt/agents/utils/exceptions.py +++ b/autogpts/autogpt/autogpt/agents/utils/exceptions.py @@ -14,6 +14,10 @@ class AgentException(Exception): super().__init__(message, *args) +class AgentTerminated(AgentException): + """The agent terminated or was terminated""" + + class ConfigurationError(AgentException): """Error caused by invalid, incompatible or otherwise incorrect configuration""" diff --git a/autogpts/autogpt/autogpt/app/main.py b/autogpts/autogpt/autogpt/app/main.py index 1b1d9e49..8e92148c 100644 --- a/autogpts/autogpt/autogpt/app/main.py +++ b/autogpts/autogpt/autogpt/app/main.py @@ -2,6 +2,7 @@ import enum import logging import math +import re import signal import sys from pathlib import Path @@ -14,11 +15,14 @@ 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.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 InvalidAgentResponseError +from autogpt.agents.utils.exceptions import AgentTerminated, InvalidAgentResponseError from autogpt.app.configurator import apply_overrides_to_config from autogpt.app.setup import ( apply_overrides_to_ai_settings, @@ -79,20 +83,20 @@ async def run_auto_gpt( assert_config_has_openai_api_key(config) apply_overrides_to_config( - config, - continuous, - continuous_limit, - ai_settings, - prompt_settings, - skip_reprompt, - speak, - debug, - gpt3only, - gpt4only, - memory_type, - browser_name, - allow_downloads, - skip_news, + 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, + gpt3only=gpt3only, + gpt4only=gpt4only, + memory_type=memory_type, + browser_name=browser_name, + allow_downloads=allow_downloads, + skip_news=skip_news, ) # Set up logging module @@ -128,69 +132,168 @@ async def run_auto_gpt( config.plugins = scan_plugins(config, config.debug_mode) 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 # ###################### - 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) + 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, - ) + 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( + # 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, ) - else: - logger.info("AI config overrides specified through CLI; skipping revision") + agent.attach_fs(agent_manager.get_agent_dir(agent.state.agent_id)) - agent = create_agent( - task=task, - ai_profile=ai_profile, - directives=ai_directives, - app_config=config, - llm_provider=llm_provider, - ) - agent.attach_fs(config.app_data_dir / "agents" / "AutoGPT") # HACK + if not agent.config.allow_fs_access: + logger.info( + f"{Fore.YELLOW}NOTE: All files/directories created by this agent" + f" can be found inside its workspace at:{Fore.RESET} {agent.workspace.root}", + extra={"preserve_color": True}, + ) - if not agent.config.allow_fs_access: - logger.info( - f"{Fore.YELLOW}NOTE: All files/directories created by this agent" - f" can be found 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 - await run_interaction_loop(agent) + agent.state.save_to_json_file(agent.file_manager.state_file_path) def _configure_openai_provider(config: Config) -> OpenAIProvider: @@ -261,24 +364,35 @@ async def run_interaction_loop( legacy_config.continuous_mode, legacy_config.continuous_limit ) spinner = Spinner("Thinking...", plain_output=legacy_config.plain_output) + stop_reason = None 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 AutoGPT immediately.") + 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." + "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) @@ -295,6 +409,7 @@ async def run_interaction_loop( ######## # Plan # ######## + handle_stop_signal() # Have the agent determine the next action to take. with spinner: try: @@ -308,10 +423,13 @@ async def run_interaction_loop( consecutive_failures += 1 if consecutive_failures >= 3: logger.error( - f"The agent failed to output valid thoughts {consecutive_failures} " - "times in a row. Terminating..." + "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." ) - sys.exit() continue consecutive_failures = 0 @@ -331,6 +449,7 @@ async def run_interaction_loop( ################## # 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, @@ -388,6 +507,8 @@ async def run_interaction_loop( if not command_name: continue + handle_stop_signal() + result = await agent.execute(command_name, command_args, user_input) if result.status == "success":