Implement Resuming AutoGPT Agents

* Add AgentManager
This commit is contained in:
Reinier van der Leer
2023-10-08 18:02:54 -07:00
parent 36e2dae6b0
commit aae650fe3a
5 changed files with 272 additions and 76 deletions

View File

@@ -0,0 +1,3 @@
from .agent_manager import AgentManager
__all__ = ["AgentManager"]

View File

@@ -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

View File

@@ -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()

View File

@@ -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"""

View File

@@ -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":