Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Agent loop v2: Planning & Task Management (part 1: refactoring) #4799

Merged
merged 73 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
d84936e
Move rename module `agent` -> `agents`
Pwuts Jun 26, 2023
02141b6
WIP: abstract agent structure into base class and port Agent
Pwuts Jun 27, 2023
e848591
Move command arg path sanitization to decorator
Pwuts Jun 29, 2023
bde81e1
Add fallback token limit in llm.utils.create_chat_completion
Pwuts Jun 29, 2023
57d9585
Rebase `MessageHistory` class on `ChatSequence` class
Pwuts Jun 29, 2023
3038e29
Merge branch 'master' into performance/planning
Pwuts Jun 29, 2023
3d3ac2e
Merge branch 'master' into performance/planning
Pwuts Jul 1, 2023
a8c5913
Fix linting
Pwuts Jul 1, 2023
b694448
Consolidate logging modules
Pwuts Jul 4, 2023
5aa9127
Wham Bam Boom
Pwuts Jul 4, 2023
94e1923
Merge branch 'master' into performance/planning
Pwuts Jul 4, 2023
c162fb6
Fix tests & linting complaints
Pwuts Jul 4, 2023
513f319
Update Agent class docstring
Pwuts Jul 4, 2023
a887eea
Fix Agent import in autogpt.llm.providers.openai
Pwuts Jul 4, 2023
077a942
Fix agent kwarg in test_execute_code.py
Pwuts Jul 4, 2023
12b547f
Fix benchmarks.py
Pwuts Jul 4, 2023
02bf67c
Clean up lingering Agent(ai_name=...) initializations
Pwuts Jul 4, 2023
627b609
Fix agent kwarg
Pwuts Jul 4, 2023
933ff5e
Make sanitize_path_arg decorator more robust
Pwuts Jul 5, 2023
7936c0f
Fix linting
Pwuts Jul 5, 2023
e43aec6
Fix command enabling lambda's
Pwuts Jul 5, 2023
e287fee
Use relative paths in file ops logger
Pwuts Jul 5, 2023
345ab14
Fix test_execute_python_file_not_found
Pwuts Jul 5, 2023
2821273
Merge branch 'master' into performance/planning
Pwuts Jul 5, 2023
db5a793
Fix Config model validation breaking on .plugins
Pwuts Jul 5, 2023
d568f82
Define validator for Config.plugins
Pwuts Jul 5, 2023
8f0679a
Fix Config model issues
Pwuts Jul 5, 2023
2699777
Merge branch 'master' into re-arch/agent-abstraction
Pwuts Jul 5, 2023
9c39504
Fix agent iteration budget in testing
Pwuts Jul 5, 2023
135921d
Fix declaration of context_while_think
Pwuts Jul 5, 2023
e28dbdb
Fix Agent.parse_and_process_response signature
Pwuts Jul 5, 2023
6ad5f55
Fix Agent cycle_budget usages
Pwuts Jul 5, 2023
d8a51c3
Merge branch 'master' into re-arch/agent-abstraction
Pwuts Jul 5, 2023
35de814
Fix budget checking in BaseAgent.__next__
Pwuts Jul 5, 2023
63bcdc2
Fix cycle budget initialization
Pwuts Jul 5, 2023
c7a1aff
Fix function calling in BaseAgent.think()
Pwuts Jul 6, 2023
3425b06
Include functions in token length calculation
Pwuts Jul 6, 2023
2b88f7a
Merge branch 'master' into re-arch/agent-abstraction
Pwuts Jul 8, 2023
67ee07d
Merge branch 'master' into re-arch/agent-abstraction
Pwuts Jul 8, 2023
91237e2
Merge branch 'master' into re-arch/agent-abstraction
Pwuts Jul 8, 2023
aa3b613
Fix Config errors
Pwuts Jul 8, 2023
e04c69c
Merge branch 'master' into re-arch/agent-abstraction
Pwuts Jul 8, 2023
92bf3c6
Merge branch 'master' into re-arch/agent-abstraction
Pwuts Jul 9, 2023
d8643f7
Merge branch 'master' into re-arch/agent-abstraction
Pwuts Jul 9, 2023
b5227bb
Merge branch 'master' into performance/planning
Pwuts Jul 11, 2023
c206ee2
Merge branch 'master' into re-arch/agent-abstraction
Pwuts Jul 12, 2023
5d78b0e
Add debug thing to patched_api_requestor to investigate HTTP 400 errors
Pwuts Jul 12, 2023
ca7f095
If this works I'm gonna be sad
Pwuts Jul 12, 2023
82f5e86
Merge branch 'master' into performance/planning
Pwuts Jul 12, 2023
71da919
Merge branch 'master' into performance/planning
Pwuts Jul 12, 2023
e1a4312
Fix BaseAgent cycle budget logic and document attributes
Pwuts Jul 13, 2023
1912d3b
Document attributes on `Agent`
Pwuts Jul 13, 2023
76d76de
Merge in upstream
collijk Jul 13, 2023
b344ab2
Merge branch 'performance/planning' of github.com:Significant-Gravita…
collijk Jul 13, 2023
6aa38b2
Merge remote-tracking branch 'origin/master' into re-arch/agent-abstr…
Pwuts Jul 13, 2023
3ae4b0f
Fix import issues between Agent and MessageHistory
Pwuts Jul 13, 2023
273fd9b
Improve typing
Pwuts Jul 13, 2023
26365c3
Extract application code from the agent (#4982)
collijk Jul 14, 2023
4839739
Use `self.default_cycle_instruction` in `Agent.think()`
Pwuts Jul 14, 2023
93e019f
Fix formatting
Pwuts Jul 14, 2023
3966cdf
Merge branch 'master' into performance/planning
Pwuts Jul 14, 2023
01ac8d5
hot fix the SIGINT handler (#4997)
cyrus-hawk Jul 17, 2023
bce8878
Update the sigint handler to be smart enough to actually work (#4999)
collijk Jul 18, 2023
3bb890f
Merge branch 'master' into performance/planning
Pwuts Jul 19, 2023
0296ae6
Fix CI
Pwuts Jul 19, 2023
d0105f0
Fix initial prompt construction
Pwuts Jul 19, 2023
dde4348
off by one error
collijk Jul 20, 2023
9570ff3
Merge branch 'master' into performance/planning
collijk Jul 20, 2023
8603d2e
allow exit/EXIT to shut down app
collijk Jul 20, 2023
4fc7bcf
Merge branch 'performance/planning' of github.com:Significant-Gravita…
collijk Jul 20, 2023
30b23ad
Merge branch 'master' into performance/planning
collijk Jul 20, 2023
020dc52
Remove dead code
collijk Jul 20, 2023
e9394c5
Merge branch 'performance/planning' of github.com:Significant-Gravita…
collijk Jul 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion autogpt/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .agent import Agent
from .base import AgentThoughts, BaseAgent, CommandArgs, CommandName

__all__ = ["Agent"]
__all__ = ["BaseAgent", "Agent", "CommandName", "CommandArgs", "AgentThoughts"]
448 changes: 175 additions & 273 deletions autogpt/agents/agent.py

Large diffs are not rendered by default.

318 changes: 318 additions & 0 deletions autogpt/agents/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
from __future__ import annotations

from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING, Any, Optional

if TYPE_CHECKING:
from autogpt.config import AIConfig, Config

from autogpt.models.command_registry import CommandRegistry

from autogpt.llm.base import ChatModelResponse, ChatSequence, Message
from autogpt.llm.providers.openai import OPEN_AI_CHAT_MODELS, get_openai_command_specs
from autogpt.llm.utils import count_message_tokens, create_chat_completion
from autogpt.logs import logger
from autogpt.memory.message_history import MessageHistory
from autogpt.prompts.prompt import DEFAULT_TRIGGERING_PROMPT

CommandName = str
CommandArgs = dict[str, str]
AgentThoughts = dict[str, Any]


class BaseAgent(metaclass=ABCMeta):
"""Base class for all Auto-GPT agents."""

def __init__(
self,
ai_config: AIConfig,
command_registry: CommandRegistry,
config: Config,
big_brain: bool = True,
default_cycle_instruction: str = DEFAULT_TRIGGERING_PROMPT,
cycle_budget: Optional[int] = 1,
send_token_limit: Optional[int] = None,
summary_max_tlength: Optional[int] = None,
):
self.ai_config = ai_config
"""The AIConfig or "personality" object associated with this agent."""

self.command_registry = command_registry
"""The registry containing all commands available to the agent."""

self.config = config
"""The applicable application configuration."""

self.big_brain = big_brain
"""
Whether this agent uses the configured smart LLM (default) to think,
as opposed to the configured fast LLM.
"""

self.default_cycle_instruction = default_cycle_instruction
"""The default instruction passed to the AI for a thinking cycle."""

self.cycle_budget = cycle_budget
"""
The number of cycles that the agent is allowed to run unsupervised.

`None` for unlimited continuous execution,
`1` to require user approval for every step,
`0` to stop the agent.
"""

self.cycles_remaining = cycle_budget
"""The number of cycles remaining within the `cycle_budget`."""

self.cycle_count = 0
"""The number of cycles that the agent has run since its initialization."""

self.system_prompt = ai_config.construct_full_prompt(config)
"""
The system prompt sets up the AI's personality and explains its goals,
available resources, and restrictions.
"""

llm_name = self.config.smart_llm if self.big_brain else self.config.fast_llm
self.llm = OPEN_AI_CHAT_MODELS[llm_name]
"""The LLM that the agent uses to think."""

self.send_token_limit = send_token_limit or self.llm.max_tokens * 3 // 4
"""
The token limit for prompt construction. Should leave room for the completion;
defaults to 75% of `llm.max_tokens`.
"""

self.history = MessageHistory(
self.llm,
max_summary_tlength=summary_max_tlength or self.send_token_limit // 6,
)

def think(
self,
instruction: Optional[str] = None,
) -> tuple[CommandName | None, CommandArgs | None, AgentThoughts]:
"""Runs the agent for one cycle.

Params:
instruction: The instruction to put at the end of the prompt.

Returns:
The command name and arguments, if any, and the agent's thoughts.
"""

instruction = instruction or self.default_cycle_instruction

prompt: ChatSequence = self.construct_prompt(instruction)
prompt = self.on_before_think(prompt, instruction)

raw_response = create_chat_completion(
prompt,
self.config,
functions=get_openai_command_specs(self.command_registry)
if self.config.openai_functions
else None,
)
self.cycle_count += 1

return self.on_response(raw_response, prompt, instruction)

@abstractmethod
def execute(
self,
command_name: str | None,
command_args: dict[str, str] | None,
user_input: str | None,
) -> str:
"""Executes the given command, if any, and returns the agent's response.

Params:
command_name: The name of the command to execute, if any.
command_args: The arguments to pass to the command, if any.
user_input: The user's input, if any.

Returns:
The results of the command.
"""
...

def construct_base_prompt(
self,
prepend_messages: list[Message] = [],
append_messages: list[Message] = [],
reserve_tokens: int = 0,
) -> ChatSequence:
"""Constructs and returns a prompt with the following structure:
1. System prompt
2. `prepend_messages`
3. Message history of the agent, truncated & prepended with running summary as needed
4. `append_messages`

Params:
prepend_messages: Messages to insert between the system prompt and message history
append_messages: Messages to insert after the message history
reserve_tokens: Number of tokens to reserve for content that is added later
"""

prompt = ChatSequence.for_model(
self.llm.name,
[Message("system", self.system_prompt)] + prepend_messages,
)

# Reserve tokens for messages to be appended later, if any
reserve_tokens += self.history.max_summary_tlength
if append_messages:
reserve_tokens += count_message_tokens(append_messages, self.llm.name)

# Fill message history, up to a margin of reserved_tokens.
# Trim remaining historical messages and add them to the running summary.
history_start_index = len(prompt)
trimmed_history = add_history_upto_token_limit(
prompt, self.history, self.send_token_limit - reserve_tokens
)
if trimmed_history:
new_summary_msg, _ = self.history.trim_messages(list(prompt), self.config)
prompt.insert(history_start_index, new_summary_msg)

if append_messages:
prompt.extend(append_messages)

return prompt

def construct_prompt(self, cycle_instruction: str) -> ChatSequence:
"""Constructs and returns a prompt with the following structure:
1. System prompt
2. Message history of the agent, truncated & prepended with running summary as needed
3. `cycle_instruction`

Params:
cycle_instruction: The final instruction for a thinking cycle
"""

if not cycle_instruction:
raise ValueError("No instruction given")

cycle_instruction_msg = Message("user", cycle_instruction)
cycle_instruction_tlength = count_message_tokens(
cycle_instruction_msg, self.llm.name
)
prompt = self.construct_base_prompt(reserve_tokens=cycle_instruction_tlength)

# ADD user input message ("triggering prompt")
prompt.append(cycle_instruction_msg)

return prompt

def on_before_think(self, prompt: ChatSequence, instruction: str) -> ChatSequence:
"""Called after constructing the prompt but before executing it.

Calls the `on_planning` hook of any enabled and capable plugins, adding their
output to the prompt.

Params:
instruction: The instruction for the current cycle, also used in constructing the prompt

Returns:
The prompt to execute
"""
current_tokens_used = prompt.token_length
plugin_count = len(self.config.plugins)
for i, plugin in enumerate(self.config.plugins):
if not plugin.can_handle_on_planning():
continue
plugin_response = plugin.on_planning(
self.ai_config.prompt_generator, prompt.raw()
)
if not plugin_response or plugin_response == "":
continue
message_to_add = Message("system", plugin_response)
tokens_to_add = count_message_tokens(message_to_add, self.llm.name)
if current_tokens_used + tokens_to_add > self.send_token_limit:
logger.debug(f"Plugin response too long, skipping: {plugin_response}")
logger.debug(f"Plugins remaining at stop: {plugin_count - i}")
break
prompt.insert(
-1, message_to_add
) # HACK: assumes cycle instruction to be at the end
current_tokens_used += tokens_to_add
return prompt

def on_response(
self, llm_response: ChatModelResponse, prompt: ChatSequence, instruction: str
) -> tuple[CommandName | None, CommandArgs | None, AgentThoughts]:
"""Called upon receiving a response from the chat model.

Adds the last/newest message in the prompt and the response to `history`,
and calls `self.parse_and_process_response()` to do the rest.

Params:
llm_response: The raw response from the chat model
prompt: The prompt that was executed
instruction: The instruction for the current cycle, also used in constructing the prompt

Returns:
The parsed command name and command args, if any, and the agent thoughts.
"""

# Save assistant reply to message history
self.history.append(prompt[-1])
self.history.add(
"assistant", llm_response.content, "ai_response"
) # FIXME: support function calls

try:
return self.parse_and_process_response(llm_response, prompt, instruction)
except SyntaxError as e:
logger.error(f"Response could not be parsed: {e}")
# TODO: tune this message
self.history.add(
"system",
f"Your response could not be parsed: {e}"
"\n\nRemember to only respond using the specified format above!",
)
return None, None, {}

# TODO: update memory/context

@abstractmethod
def parse_and_process_response(
self, llm_response: ChatModelResponse, prompt: ChatSequence, instruction: str
) -> tuple[CommandName | None, CommandArgs | None, AgentThoughts]:
"""Validate, parse & process the LLM's response.

Must be implemented by derivative classes: no base implementation is provided,
since the implementation depends on the role of the derivative Agent.

Params:
llm_response: The raw response from the chat model
prompt: The prompt that was executed
instruction: The instruction for the current cycle, also used in constructing the prompt

Returns:
The parsed command name and command args, if any, and the agent thoughts.
"""
pass


def add_history_upto_token_limit(
prompt: ChatSequence, history: MessageHistory, t_limit: int
) -> list[Message]:
current_prompt_length = prompt.token_length
insertion_index = len(prompt)
limit_reached = False
trimmed_messages: list[Message] = []
for cycle in reversed(list(history.per_cycle())):
messages_to_add = [msg for msg in cycle if msg is not None]
tokens_to_add = count_message_tokens(messages_to_add, prompt.model.name)
if current_prompt_length + tokens_to_add > t_limit:
limit_reached = True

if not limit_reached:
# Add the most recent message to the start of the chain,
# after the system prompts.
prompt.insert(insertion_index, *messages_to_add)
current_prompt_length += tokens_to_add
else:
trimmed_messages = messages_to_add + trimmed_messages

return trimmed_messages
26 changes: 14 additions & 12 deletions autogpt/json_utils/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import ast
import json
import os.path
from typing import Any
from typing import Any, Literal

from jsonschema import Draft7Validator

Expand All @@ -12,7 +12,7 @@
LLM_DEFAULT_RESPONSE_FORMAT = "llm_response_format_1"


def extract_json_from_response(response_content: str) -> dict:
def extract_dict_from_response(response_content: str) -> dict[str, Any]:
# Sometimes the response includes the JSON in a code block with ```
if response_content.startswith("```") and response_content.endswith("```"):
# Discard the first and last ```, then re-join in case the response naturally included ```
Expand All @@ -33,41 +33,43 @@ def llm_response_schema(
) -> dict[str, Any]:
filename = os.path.join(os.path.dirname(__file__), f"{schema_name}.json")
with open(filename, "r") as f:
json_schema = json.load(f)
try:
json_schema = json.load(f)
except Exception as e:
raise RuntimeError(f"Failed to load JSON schema: {e}")
if config.openai_functions:
del json_schema["properties"]["command"]
json_schema["required"].remove("command")
return json_schema


def validate_json(
json_object: object, config: Config, schema_name: str = LLM_DEFAULT_RESPONSE_FORMAT
) -> bool:
def validate_dict(
object: object, config: Config, schema_name: str = LLM_DEFAULT_RESPONSE_FORMAT
) -> tuple[Literal[True], None] | tuple[Literal[False], list]:
"""
:type schema_name: object
:param schema_name: str
:type json_object: object

Returns:
bool: Whether the json_object is valid or not
list: Errors found in the json_object, or None if the object is valid
"""
schema = llm_response_schema(config, schema_name)
validator = Draft7Validator(schema)

if errors := sorted(validator.iter_errors(json_object), key=lambda e: e.path):
if errors := sorted(validator.iter_errors(object), key=lambda e: e.path):
for error in errors:
logger.debug(f"JSON Validation Error: {error}")

if config.debug_mode:
logger.error(
json.dumps(json_object, indent=4)
) # Replace 'json_object' with the variable containing the JSON data
logger.error(json.dumps(object, indent=4))
logger.error("The following issues were found:")

for error in errors:
logger.error(f"Error: {error.message}")
return False
return False, errors

logger.debug("The JSON object is valid.")

return True
return True, None
Loading