From 7f84da04ed0f414b10a8ade5a6fac55a04a8016f Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 17 Jun 2024 11:11:33 -0700 Subject: [PATCH 01/10] implement message based history in `ActionHistoryComponent` Also: - Run `ActionHistoryComponent` after `SystemComponent` so that history messages are last in the prompt - Omit final instruction message if prompt already contains assistant messages --- autogpt/autogpt/agents/agent.py | 18 ++-- .../action_history/action_history.py | 84 +++++++++++++++++-- 2 files changed, 89 insertions(+), 13 deletions(-) diff --git a/autogpt/autogpt/agents/agent.py b/autogpt/autogpt/agents/agent.py index 31f642390c7c..851e0321624b 100644 --- a/autogpt/autogpt/agents/agent.py +++ b/autogpt/autogpt/agents/agent.py @@ -107,13 +107,17 @@ def __init__( # Components self.system = SystemComponent() - self.history = ActionHistoryComponent( - settings.history, - self.send_token_limit, - lambda x: self.llm_provider.count_tokens(x, self.llm.name), - legacy_config, - llm_provider, - ).run_after(WatchdogComponent) + self.history = ( + ActionHistoryComponent( + settings.history, + self.send_token_limit, + lambda x: self.llm_provider.count_tokens(x, self.llm.name), + legacy_config, + llm_provider, + ) + .run_after(WatchdogComponent) + .run_after(SystemComponent) + ) self.user_interaction = UserInteractionComponent(legacy_config) self.file_manager = FileManagerComponent(settings, file_storage) self.code_executor = CodeExecutorComponent( diff --git a/forge/forge/components/action_history/action_history.py b/forge/forge/components/action_history/action_history.py index 953d7d52936c..66d52ce764df 100644 --- a/forge/forge/components/action_history/action_history.py +++ b/forge/forge/components/action_history/action_history.py @@ -5,12 +5,19 @@ from forge.agent.protocols import AfterExecute, AfterParse, MessageProvider from forge.llm.prompting.utils import indent from forge.llm.providers import ChatMessage, MultiProvider +from forge.llm.providers.schema import ( + AssistantChatMessage, + AssistantToolCall, + ToolResultMessage, +) if TYPE_CHECKING: from forge.config.config import Config from .model import ActionResult, AnyProposal, Episode, EpisodicActionHistory +_AUTOGPT_FAKE_TOOL_CALL_ID = "autogpt-fake-tool-call-id" + class ActionHistoryComponent(MessageProvider, AfterParse[AnyProposal], AfterExecute): """Keeps track of the event history and provides a summary of the steps.""" @@ -30,12 +37,77 @@ def __init__( self.llm_provider = llm_provider def get_messages(self) -> Iterator[ChatMessage]: - if progress := self._compile_progress( - self.event_history.episodes, - self.max_tokens, - self.count_tokens, - ): - yield ChatMessage.system(f"## Progress on your Task so far\n\n{progress}") + messages: list[ChatMessage] = [] + steps: list[str] = [] + tokens: int = 0 + n_episodes = len(self.event_history.episodes) + + # Include a summary for all except the latest 4 steps + for i, episode in enumerate(reversed(self.event_history.episodes)): + # Use full format for the latest 4 steps, summary or format for older steps + if i < 4: + messages.insert( + 0, + AssistantChatMessage( + content=episode.action.json(exclude={"use_tool"}, indent=4), + tool_calls=[ + AssistantToolCall( + type="function", + id=_AUTOGPT_FAKE_TOOL_CALL_ID, + function=episode.action.use_tool, + ) + ], + ), + ) + tokens += self.count_tokens(str(messages[0])) # HACK + if _r := episode.result: + if _r.status == "success": + messages.insert( + 1, + ToolResultMessage( + content=_r.outputs, + tool_call_id=_AUTOGPT_FAKE_TOOL_CALL_ID, + ), + ) + elif _r.status == "error": + messages.insert( + 1, + ToolResultMessage( + content=f"{_r.reason}\n\n{_r.error or ''}".strip(), + is_error=True, + tool_call_id=_AUTOGPT_FAKE_TOOL_CALL_ID, + ), + ) + elif _r.status == "interrupted_by_human": + messages.insert(1, ChatMessage.user(_r.feedback)) + + tokens += self.count_tokens(str(messages[0])) # HACK + continue + elif episode.summary is None: + step_content = indent(episode.format(), 2).strip() + else: + step_content = episode.summary + + step = f"* Step {n_episodes - i}: {step_content}" + + if self.max_tokens and self.count_tokens: + step_tokens = self.count_tokens(step) + if tokens + step_tokens > self.max_tokens: + break + tokens += step_tokens + + steps.insert(0, step) + + if steps: + step_summaries = "\n\n".join(steps) + yield ChatMessage.system( + f"## Progress on your Task so far\n" + "Here is a summary of the steps that you have executed so far, " + "use this as your consideration for determining the next action!\n" + f"{step_summaries}" + ) + + yield from messages def after_parse(self, result: AnyProposal) -> None: self.event_history.register_action(result) From ecbe421a140516857199cd163b6ec662f951b9df Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 17 Jun 2024 18:59:51 -0700 Subject: [PATCH 02/10] fix re-insertion of historical assistant messages in prompt --- forge/forge/agent/forge_agent.py | 5 +- .../action_history/action_history.py | 64 +++++++++---------- forge/forge/models/action.py | 4 +- 3 files changed, 39 insertions(+), 34 deletions(-) diff --git a/forge/forge/agent/forge_agent.py b/forge/forge/agent/forge_agent.py index 2eb6710dd554..f494ccb56723 100644 --- a/forge/forge/agent/forge_agent.py +++ b/forge/forge/agent/forge_agent.py @@ -24,7 +24,7 @@ from forge.file_storage.base import FileStorage from forge.llm.prompting.schema import ChatPrompt from forge.llm.prompting.utils import dump_prompt -from forge.llm.providers.schema import AssistantFunctionCall +from forge.llm.providers.schema import AssistantChatMessage, AssistantFunctionCall from forge.llm.providers.utils import function_specs_from_commands from forge.models.action import ( ActionErrorResult, @@ -178,6 +178,9 @@ async def propose_action(self) -> ActionProposal: use_tool=AssistantFunctionCall( name="finish", arguments={"reason": "Unimplemented logic"} ), + raw_message=AssistantChatMessage( + content="finish(reason='Unimplemented logic')" + ), ) self.config.cycle_count += 1 diff --git a/forge/forge/components/action_history/action_history.py b/forge/forge/components/action_history/action_history.py index 66d52ce764df..a4182a7ed65b 100644 --- a/forge/forge/components/action_history/action_history.py +++ b/forge/forge/components/action_history/action_history.py @@ -5,19 +5,13 @@ from forge.agent.protocols import AfterExecute, AfterParse, MessageProvider from forge.llm.prompting.utils import indent from forge.llm.providers import ChatMessage, MultiProvider -from forge.llm.providers.schema import ( - AssistantChatMessage, - AssistantToolCall, - ToolResultMessage, -) +from forge.llm.providers.schema import ToolResultMessage if TYPE_CHECKING: from forge.config.config import Config from .model import ActionResult, AnyProposal, Episode, EpisodicActionHistory -_AUTOGPT_FAKE_TOOL_CALL_ID = "autogpt-fake-tool-call-id" - class ActionHistoryComponent(MessageProvider, AfterParse[AnyProposal], AfterExecute): """Keeps track of the event history and provides a summary of the steps.""" @@ -46,42 +40,48 @@ def get_messages(self) -> Iterator[ChatMessage]: for i, episode in enumerate(reversed(self.event_history.episodes)): # Use full format for the latest 4 steps, summary or format for older steps if i < 4: - messages.insert( - 0, - AssistantChatMessage( - content=episode.action.json(exclude={"use_tool"}, indent=4), - tool_calls=[ - AssistantToolCall( - type="function", - id=_AUTOGPT_FAKE_TOOL_CALL_ID, - function=episode.action.use_tool, - ) - ], - ), - ) + messages.insert(0, episode.action.raw_message) tokens += self.count_tokens(str(messages[0])) # HACK if _r := episode.result: if _r.status == "success": - messages.insert( - 1, + result_message = ( ToolResultMessage( - content=_r.outputs, - tool_call_id=_AUTOGPT_FAKE_TOOL_CALL_ID, - ), + content=str(_r.outputs), + tool_call_id=( + episode.action.raw_message.tool_calls[0].id + ), + ) + if episode.action.raw_message.tool_calls + else ChatMessage.user( + f"{episode.action.use_tool.name} returned: " + + ( + f"```\n{_r.outputs}\n```" + if "\n" in str(_r.outputs) + else f"`{_r.outputs}`" + ) + ) ) elif _r.status == "error": - messages.insert( - 1, + result_message = ( ToolResultMessage( content=f"{_r.reason}\n\n{_r.error or ''}".strip(), is_error=True, - tool_call_id=_AUTOGPT_FAKE_TOOL_CALL_ID, - ), + tool_call_id=( + episode.action.raw_message.tool_calls[0].id + ), + ) + if episode.action.raw_message.tool_calls + else ChatMessage.user( + f"{episode.action.use_tool.name} raised an error: ```\n" + f"{_r.reason}\n" + "```" + ) ) - elif _r.status == "interrupted_by_human": - messages.insert(1, ChatMessage.user(_r.feedback)) + else: + result_message = ChatMessage.user(_r.feedback) - tokens += self.count_tokens(str(messages[0])) # HACK + messages.insert(1, result_message) + tokens += self.count_tokens(str(result_message)) # HACK continue elif episode.summary is None: step_content = indent(episode.format(), 2).strip() diff --git a/forge/forge/models/action.py b/forge/forge/models/action.py index 4acf7b97f8d0..93546147f633 100644 --- a/forge/forge/models/action.py +++ b/forge/forge/models/action.py @@ -4,7 +4,7 @@ from pydantic import BaseModel -from forge.llm.providers.schema import AssistantFunctionCall +from forge.llm.providers.schema import AssistantChatMessage, AssistantFunctionCall from .utils import ModelWithSummary @@ -13,6 +13,8 @@ class ActionProposal(BaseModel): thoughts: str | ModelWithSummary use_tool: AssistantFunctionCall + raw_message: AssistantChatMessage + AnyProposal = TypeVar("AnyProposal", bound=ActionProposal) From 5b9b21109aa0b94f2ddc501155e591d642d1110d Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 17 Jun 2024 19:08:31 -0700 Subject: [PATCH 03/10] unbreak one_shot prompt strategy --- autogpt/autogpt/agents/prompt_strategies/one_shot.py | 1 + forge/forge/models/action.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/autogpt/autogpt/agents/prompt_strategies/one_shot.py b/autogpt/autogpt/agents/prompt_strategies/one_shot.py index 620f39c0846b..d232e1ee820b 100644 --- a/autogpt/autogpt/agents/prompt_strategies/one_shot.py +++ b/autogpt/autogpt/agents/prompt_strategies/one_shot.py @@ -275,4 +275,5 @@ def parse_response_content( assistant_reply_dict["use_tool"] = response.tool_calls[0].function parsed_response = OneShotAgentActionProposal.parse_obj(assistant_reply_dict) + parsed_response.raw_message = response.copy() return parsed_response diff --git a/forge/forge/models/action.py b/forge/forge/models/action.py index 93546147f633..08a408f720fc 100644 --- a/forge/forge/models/action.py +++ b/forge/forge/models/action.py @@ -13,7 +13,7 @@ class ActionProposal(BaseModel): thoughts: str | ModelWithSummary use_tool: AssistantFunctionCall - raw_message: AssistantChatMessage + raw_message: AssistantChatMessage = None AnyProposal = TypeVar("AnyProposal", bound=ActionProposal) From 90f94b6c277721e02dfb973ec7544e386698f4bf Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 17 Jun 2024 19:14:44 -0700 Subject: [PATCH 04/10] extract _make_result_message in ActionHistoryComponent for readability --- .../action_history/action_history.py | 75 +++++++++---------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/forge/forge/components/action_history/action_history.py b/forge/forge/components/action_history/action_history.py index a4182a7ed65b..792401591f5f 100644 --- a/forge/forge/components/action_history/action_history.py +++ b/forge/forge/components/action_history/action_history.py @@ -42,44 +42,8 @@ def get_messages(self) -> Iterator[ChatMessage]: if i < 4: messages.insert(0, episode.action.raw_message) tokens += self.count_tokens(str(messages[0])) # HACK - if _r := episode.result: - if _r.status == "success": - result_message = ( - ToolResultMessage( - content=str(_r.outputs), - tool_call_id=( - episode.action.raw_message.tool_calls[0].id - ), - ) - if episode.action.raw_message.tool_calls - else ChatMessage.user( - f"{episode.action.use_tool.name} returned: " - + ( - f"```\n{_r.outputs}\n```" - if "\n" in str(_r.outputs) - else f"`{_r.outputs}`" - ) - ) - ) - elif _r.status == "error": - result_message = ( - ToolResultMessage( - content=f"{_r.reason}\n\n{_r.error or ''}".strip(), - is_error=True, - tool_call_id=( - episode.action.raw_message.tool_calls[0].id - ), - ) - if episode.action.raw_message.tool_calls - else ChatMessage.user( - f"{episode.action.use_tool.name} raised an error: ```\n" - f"{_r.reason}\n" - "```" - ) - ) - else: - result_message = ChatMessage.user(_r.feedback) - + if episode.result: + result_message = self._make_result_message(episode, episode.result) messages.insert(1, result_message) tokens += self.count_tokens(str(result_message)) # HACK continue @@ -118,6 +82,41 @@ async def after_execute(self, result: ActionResult) -> None: self.llm_provider, self.legacy_config ) + @staticmethod + def _make_result_message(episode: Episode, result: ActionResult) -> ChatMessage: + if result.status == "success": + return ( + ToolResultMessage( + content=str(result.outputs), + tool_call_id=episode.action.raw_message.tool_calls[0].id, + ) + if episode.action.raw_message.tool_calls + else ChatMessage.user( + f"{episode.action.use_tool.name} returned: " + + ( + f"```\n{result.outputs}\n```" + if "\n" in str(result.outputs) + else f"`{result.outputs}`" + ) + ) + ) + elif result.status == "error": + return ( + ToolResultMessage( + content=f"{result.reason}\n\n{result.error or ''}".strip(), + is_error=True, + tool_call_id=episode.action.raw_message.tool_calls[0].id, + ) + if episode.action.raw_message.tool_calls + else ChatMessage.user( + f"{episode.action.use_tool.name} raised an error: ```\n" + f"{result.reason}\n" + "```" + ) + ) + else: + return ChatMessage.user(result.feedback) + def _compile_progress( self, episode_history: list[Episode[AnyProposal]], From 164353f556fda718f0d4d5d9f7542c7e584963f6 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 17 Jun 2024 19:17:01 -0700 Subject: [PATCH 05/10] minor refactor --- .../forge/components/action_history/action_history.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/forge/forge/components/action_history/action_history.py b/forge/forge/components/action_history/action_history.py index 792401591f5f..0d48339884a9 100644 --- a/forge/forge/components/action_history/action_history.py +++ b/forge/forge/components/action_history/action_history.py @@ -32,7 +32,7 @@ def __init__( def get_messages(self) -> Iterator[ChatMessage]: messages: list[ChatMessage] = [] - steps: list[str] = [] + step_summaries: list[str] = [] tokens: int = 0 n_episodes = len(self.event_history.episodes) @@ -60,15 +60,15 @@ def get_messages(self) -> Iterator[ChatMessage]: break tokens += step_tokens - steps.insert(0, step) + step_summaries.insert(0, step) - if steps: - step_summaries = "\n\n".join(steps) + if step_summaries: + step_summaries_fmt = "\n\n".join(step_summaries) yield ChatMessage.system( f"## Progress on your Task so far\n" "Here is a summary of the steps that you have executed so far, " "use this as your consideration for determining the next action!\n" - f"{step_summaries}" + f"{step_summaries_fmt}" ) yield from messages From bde563a3e6ff1fa555123ba8eee8ad9241ecaad1 Mon Sep 17 00:00:00 2001 From: Krzysztof Czerwinski Date: Wed, 19 Jun 2024 12:49:58 +0200 Subject: [PATCH 06/10] Make non-summarized message count configurable --- .../components/action_history/action_history.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/forge/forge/components/action_history/action_history.py b/forge/forge/components/action_history/action_history.py index dffebb7643ff..cd9ffcd4910e 100644 --- a/forge/forge/components/action_history/action_history.py +++ b/forge/forge/components/action_history/action_history.py @@ -22,6 +22,8 @@ class ActionHistoryConfiguration(BaseModel): """Maximum number of tokens to use up with generated history messages""" spacy_language_model: str = "en_core_web_sm" """Language model used for summary chunking using spacy""" + full_message_count: int = 4 + """Number of latest non-summarized messages to include in the history""" class ActionHistoryComponent( @@ -52,10 +54,10 @@ def get_messages(self) -> Iterator[ChatMessage]: tokens: int = 0 n_episodes = len(self.event_history.episodes) - # Include a summary for all except the latest 4 steps + # Include a summary for all except a few latest steps for i, episode in enumerate(reversed(self.event_history.episodes)): - # Use full format for the latest 4 steps, summary or format for older steps - if i < 4: + # Use full format for a few steps, summary or format for older steps + if i < self.config.full_message_count: messages.insert(0, episode.action.raw_message) tokens += self.count_tokens(str(messages[0])) # HACK if episode.result: @@ -147,8 +149,8 @@ def _compile_progress( n_episodes = len(episode_history) for i, episode in enumerate(reversed(episode_history)): - # Use full format for the latest 4 steps, summary or format for older steps - if i < 4 or episode.summary is None: + # Use full format for a few latest steps, summary or format for older steps + if i < self.config.full_message_count or episode.summary is None: step_content = indent(episode.format(), 2).strip() else: step_content = episode.summary From 2c2ed88d711ef8c8abbec8e9ca63d0ba3010f00f Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 24 Jun 2024 17:24:59 -0700 Subject: [PATCH 07/10] Filter `raw_message` from `ActionProposal.schema()` --- forge/forge/models/action.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/forge/forge/models/action.py b/forge/forge/models/action.py index 08a408f720fc..61d23b1f9ff3 100644 --- a/forge/forge/models/action.py +++ b/forge/forge/models/action.py @@ -3,6 +3,7 @@ from typing import Any, Literal, Optional, TypeVar from pydantic import BaseModel +from pydantic.schema import default_ref_template from forge.llm.providers.schema import AssistantChatMessage, AssistantFunctionCall @@ -13,7 +14,22 @@ class ActionProposal(BaseModel): thoughts: str | ModelWithSummary use_tool: AssistantFunctionCall - raw_message: AssistantChatMessage = None + raw_message: AssistantChatMessage = None # type: ignore + """ + The message from which the action proposal was parsed. To be set by the parser. + """ + + @classmethod + def schema( + cls, by_alias: bool = True, ref_template: str = default_ref_template, **kwargs + ): + """ + The schema for this ActionProposal model, excluding the 'raw_message' property. + """ + schema = super().schema(by_alias=by_alias, ref_template=ref_template, **kwargs) + if "raw_message" in schema["properties"]: # must check because schema is cached + del schema["properties"]["raw_message"] + return schema AnyProposal = TypeVar("AnyProposal", bound=ActionProposal) From 75c45c078cdd8e8361de173175d46d734fa909ab Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 24 Jun 2024 20:21:13 -0700 Subject: [PATCH 08/10] remove unused import --- autogpt/autogpt/agents/agent.py | 1 - 1 file changed, 1 deletion(-) diff --git a/autogpt/autogpt/agents/agent.py b/autogpt/autogpt/agents/agent.py index 2f9615c6cfb8..e6d62f0b4dcb 100644 --- a/autogpt/autogpt/agents/agent.py +++ b/autogpt/autogpt/agents/agent.py @@ -18,7 +18,6 @@ ActionHistoryComponent, EpisodicActionHistory, ) -from forge.components.action_history.action_history import ActionHistoryConfiguration from forge.components.code_executor.code_executor import ( CodeExecutorComponent, CodeExecutorConfiguration, From 2797b7cef628aee848d184274a0307d1012b4d55 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 24 Jun 2024 20:23:56 -0700 Subject: [PATCH 09/10] add config back to Agent.history init --- autogpt/autogpt/agents/agent.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/autogpt/autogpt/agents/agent.py b/autogpt/autogpt/agents/agent.py index e6d62f0b4dcb..650589a0b50f 100644 --- a/autogpt/autogpt/agents/agent.py +++ b/autogpt/autogpt/agents/agent.py @@ -18,6 +18,7 @@ ActionHistoryComponent, EpisodicActionHistory, ) +from forge.components.action_history.action_history import ActionHistoryConfiguration from forge.components.code_executor.code_executor import ( CodeExecutorComponent, CodeExecutorConfiguration, @@ -115,6 +116,9 @@ def __init__( settings.history, lambda x: self.llm_provider.count_tokens(x, self.llm.name), llm_provider, + ActionHistoryConfiguration( + model_name=app_config.fast_llm, max_tokens=self.send_token_limit + ), ) .run_after(WatchdogComponent) .run_after(SystemComponent) From 921b5c984ca57eaf31a65660df25ef8497815867 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 1 Jul 2024 19:36:57 -0600 Subject: [PATCH 10/10] add `ActionHistoryComponent.full_message_count` to docs --- docs/content/forge/components/built-in-components.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/content/forge/components/built-in-components.md b/docs/content/forge/components/built-in-components.md index ceed5f3e15fe..4a34117191fa 100644 --- a/docs/content/forge/components/built-in-components.md +++ b/docs/content/forge/components/built-in-components.md @@ -81,6 +81,7 @@ Keeps track of agent's actions and their outcomes. Provides their summary to the | `model_name` | Name of the llm model used to compress the history | `ModelName` | `"gpt-3.5-turbo"` | | `max_tokens` | Maximum number of tokens to use for the history summary | `int` | `1024` | | `spacy_language_model` | Language model used for summary chunking using spacy | `str` | `"en_core_web_sm"` | +| `full_message_count` | Number of cycles to include unsummarized in the prompt | `int` | `4` | **MessageProvider**