diff --git a/autogpts/autogpt/autogpt/agents/base.py b/autogpts/autogpt/autogpt/agents/base.py index ab063fcedd3..ea81db37122 100644 --- a/autogpts/autogpt/autogpt/agents/base.py +++ b/autogpts/autogpt/autogpt/agents/base.py @@ -328,9 +328,9 @@ async def foreach_components( except ProtocolError: self._trace.append( f"❌ {Fore.LIGHTRED_EX}{component.__class__.__name__}: " - f"PipelineError{Fore.RESET}" + f"ProtocolError{Fore.RESET}" ) - # Restart from the beginning on PipelineError + # Restart from the beginning on ProtocolError # Revert to original parameters args = self._selective_copy(original_args) pipeline_attempts += 1 diff --git a/autogpts/autogpt/autogpt/agents/components.py b/autogpts/autogpt/autogpt/agents/components.py index 76f898e3c8e..db31237bcda 100644 --- a/autogpts/autogpt/autogpt/agents/components.py +++ b/autogpts/autogpt/autogpt/agents/components.py @@ -32,9 +32,9 @@ def __init__(self, message: str = ""): super().__init__(message) -class ComponentGroupError(ComponentError): +class PipelineError(ComponentError): """Error of a group of component types; - multiple pipelines.""" + multiple protocols.""" def __init__(self, message: str = ""): self.message = message diff --git a/docs/content/AutoGPT/component agent/advanced.md b/docs/content/AutoGPT/component agent/advanced.md new file mode 100644 index 00000000000..090b811c2e3 --- /dev/null +++ b/docs/content/AutoGPT/component agent/advanced.md @@ -0,0 +1,8 @@ +### Other stuff +Debugging may be easier because we can inspect the exact components that were called and where the pipeline failed (current WIP pipeline): + +![](../imgs/modular-pipeline.png) + +Also that makes it possible to call component/pipeline/function again when failed and recover. + +If it's necessary to get a component in a random place, agent provides generic, type safe `get_component(type[T]) -> T | None` \ No newline at end of file diff --git a/docs/content/AutoGPT/component agent/agents.md b/docs/content/AutoGPT/component agent/agents.md new file mode 100644 index 00000000000..213f4334e4a --- /dev/null +++ b/docs/content/AutoGPT/component agent/agents.md @@ -0,0 +1,3 @@ +# 🤖 Agents + +is composed of components. It's responsible for executing pipelines and managing the components. \ No newline at end of file diff --git a/docs/content/AutoGPT/component agent/commands.md b/docs/content/AutoGPT/component agent/commands.md new file mode 100644 index 00000000000..4972fcade7d --- /dev/null +++ b/docs/content/AutoGPT/component agent/commands.md @@ -0,0 +1,30 @@ +# 🛠️ Commands + +Commands a way for the agent to do anything; e.g. intercting with user or APIs and using tools. They are provided by components that implement the `CommandProvider` protocol. + +```py +class CommandProvider(Protocol): + def get_commands(self) -> Iterator[Command]: + ... +``` + +## Command decorator + +The easiest way to provide a command is to use `command` decorator on a component method and then yield `Command.from_decorated_function(...)`. Each command needs a name, description and a parameter schema using `JSONSchema`. By default method name is used as a command name, and first part of docstring for the description (before `Args:` or `Returns:`) and schema can be provided in the decorator. + +- Simplified +- Full + +## Direct construction + + + + +```py +from autogpt.agents.components import Component +from autogpt.agents.protocols import CommandProvider +from autogpt.core.utils.json_schema import JSONSchema +from autogpt.command_decorator import command + + +``` diff --git a/docs/content/AutoGPT/component agent/components.md b/docs/content/AutoGPT/component agent/components.md new file mode 100644 index 00000000000..f3086f1d0c4 --- /dev/null +++ b/docs/content/AutoGPT/component agent/components.md @@ -0,0 +1,105 @@ +# 🧩 Components + +Components are the building blocks of [🤖 Agents](./agents.md). They are classes inherited from `Component` that implement one or more [⚙️ Protocols](./protocols.md) that give agent additional abilities or processing. + +Components assigned to attributes (fields) in agent's `__init__` are automatically discovered upon instantiation. +Each component can implement multiple protocols and can rely on other components if needed. + +```py +from autogpt.agents import Agent +from autogpt.agents.components import Component + +class MyAgent(Agent): + def __init__(self): + # These components will be automatically discovered and used + self.hello_component = HelloComponent() + # We pass HelloComponent to CalculatorComponent + self.calculator_component = CalculatorComponent(self.hello_component) +``` +## Ordering components + +For some protocols, the order of components is important because the latter ones may depend on the results of the former ones. + +### Implicit order + +Components can be ordered implicitly by the agent; each component can set `run_after` list to specify which components should run before it. This is useful when components rely on each other or need to be executed in a specific order. Otherwise, the order of components is alphabetical. + +```py +# This component will run after HelloComponent +class CalculatorComponent(Component): + run_after = [HelloComponent] + + def __init__(self, hello_component: HelloComponent): + self.hello_component = hello_component +``` + +### Explicit order + +Sometimes it may be easier to order components explicitly by setting `self.components` list in the agent's `__init__` method. This way you can also ensure there's no circular dependencies and `run_after` is ignored. + +> ⚠️ Be sure to include all components - by setting `self.components` list, you're overriding the default behavior of discovering components automatically. Since it's usually not intended agent will inform you in the terminal if some components were skipped. + +```py +class MyAgent(Agent): + def __init__(self): + self.hello_component = HelloComponent() + self.calculator_component = CalculatorComponent(self.hello_component) + # Explicitly set components list + self.components = [self.hello_component, self.calculator_component] +``` + +## Disabling components + +You can control which components are enabled by setting their `enabled` attribute. You can either provide a `bool` value or a `callable[[], bool]` that will be called each time the component is about to be executed. This way you can dynamically enable or disable components based on some conditions. +You can also provide a reason for disabling the component by setting `disabled_reason`. The reason will be visible in the debug information. + +```py +class DisabledComponent(Component, MessageProvider): + def __init__(self): + # Disable this component + self.enabled = False + self.disabled_reason = "This component is disabled because of reasons." + # Or disable based on some condition + self.enabled = self.some_condition + + # This method will never be called + def get_messages(self) -> Iterator[ChatMessage]: + yield ChatMessage.user("This message won't be seen!") + + def some_condition(self) -> bool: + return False +``` + +If you don't want the component at all, you can just remove it from the agent's `__init__` method. If you want to remove components you inherit from the parent class you can set the relevant attribute to `None`: + +```py +class MyAgent(Agent): + def __init__(self): + super().__init__(...) + # Disable WatchdogComponent that is in the parent class + self.watchdog = None + +``` + +## Exceptions + +Custom errors are provided which can be used to control the execution flow in case something went wrong. All those errors can be raised in protocol methods and will be caught by the agent. +By default agent will retry three times and then re-raise an exception if it's still not resolved. All passed arguments are automatically handled and the values are reverted when needed. +All errors accept an optional `str` message. + +1. `ComponentError`: A single component failed to execute. Agent will retry the execution of the component. +2. `ProtocolError`: An entire protocol failed to execute. Agent will retry the execution of the protocol method for all components. +3. `PipelineError`: An entire pipeline failed to execute. Agent will retry the execution of the pipeline for all protocols. This isn't implemented yet. +4. `ComponentSystemError`: The highest-level error occurred in the component system. This isn't used. + +**Example** + +```py +from autogpt.agents.components import Component, ComponentError +from autogpt.agents.protocols import MessageProvider + +# Example of raising an error +class MyComponent(Component, MessageProvider): + def get_messages(self) -> Iterator[ChatMessage]: + raise ComponentError("Component error!") +``` \ No newline at end of file diff --git a/docs/content/AutoGPT/component agent/creating-components.md b/docs/content/AutoGPT/component agent/creating-components.md new file mode 100644 index 00000000000..31d291f6d91 --- /dev/null +++ b/docs/content/AutoGPT/component agent/creating-components.md @@ -0,0 +1,44 @@ +# Creating Components + +## The minimal component + +Let's create a simple component that adds "Hello World!" message to the agent prompt. +To create a component you just make a class that inherits from `Component`: + +```py +# We recommend *Component suffix to make the type clear +class HelloComponent(Component): + pass +``` + +This is already a valid component but it doesn't have any functionality yet. +To make it do something we need to write a method that can be found and called by the agent. To put messages to the agent's prompt we need to implement `MessageProvider` Protocol in our component. `MessageProvider` is an interface with `get_messages` method: + +```py +class HelloComponent(Component, MessageProvider): + def get_messages(self) -> Iterator[ChatMessage]: + yield ChatMessage.user("Hello World!") +``` + +Now we can add our component to an existing agent or create a new Agent class and add it there: + +```py +class MyAgent(Agent): + self.hello_component = HelloComponent() +``` + +`get_messsages` will called by the agent each time it needs to build a new prompt and the yielded messages will be added accordingly. + +## Full example + + + +```py + +``` + +## Learn more + +Guide on how to extend the built-in agent and build your own: [🤖 Agents](./agents.md) +Order of some components matters, see [🧩 Components](./components.md) to learn more about components and how they can be customized. +To see built-in protocols with accompanying examples visit [⚙️ Protocols](./protocols.md). diff --git a/docs/content/AutoGPT/component agent/introduction.md b/docs/content/AutoGPT/component agent/introduction.md new file mode 100644 index 00000000000..3f3ee340f10 --- /dev/null +++ b/docs/content/AutoGPT/component agent/introduction.md @@ -0,0 +1,17 @@ +# Component Agents + +This guide explains the component-based architecture of AutoGPT agents. It's a new way of building agents that is more flexible and easier to extend. Components replace plugins with a more modular and composable system. + +Agent is composed of *components*, and each `Component` implements a range of `Protocol`s (interfaces), each one providing a specific functionality, e.g. additional commands or messages. Each *protocol* is handled in a specific order, defined by the agent. This allows for a clear separation of concerns and a more modular design. + +This system is simple, flexible, requires basically no configuration, and doesn't hide any data - anything can still be passed or accessed directly from or between components. + +### Definitions & Guides + +See quick guide [Creating Components](./creating-components.md) to get started! Or you can explore the following topics in detail: + +- [🧩 Component](./components.md): a class that implements one or more *protocols*. It can be added to an agent to provide additional functionality. +- [⚙️ Protocol](./protocols.md): an interface that defines a set of methods that a component must implement. Protocols are used to group related functionality. +- [🛠️ Command](./commands.md): +- [🤖 Agent](./agents.md): a class that is composed of components. It's responsible for executing pipelines and managing the components. +- **Pipeline**: a sequence of method calls on components. Pipelines are used to execute a series of actions in a specific order. As of now there's no formal class for a pipeline, it's just a sequence of method calls on components. There are two default pipelines implemented in the default agent: `propose_action` and `execute`. See [🤖 Agent](./agents.md) to learn more. diff --git a/docs/content/AutoGPT/component agent/protocols.md b/docs/content/AutoGPT/component agent/protocols.md new file mode 100644 index 00000000000..e0c44717805 --- /dev/null +++ b/docs/content/AutoGPT/component agent/protocols.md @@ -0,0 +1,183 @@ +# ⚙️ Protocols + +Protocols are *interfaces* implemented by [Components](./components.md) used to group related functionality. Each protocol needs to be handled explicitly by the agent at some point of the execution. We provide a comprehensive list of built-in protocols that are already handled in the built-in `Agent`, so when you inherit from the base agent all built-in protocols will work! + +**Protocols are listed in the order of the default execution.** + +## Order-independent protocols + +Components implementing exclusively order-independent protocols can added in any order, including in-between ordered protocols. + +### `DirectiveProvider` + +Yields constraints, resources and best practices for the agent. This is purely informational and will be passed to a llm after prompt is ready using `BuildPrompt` protocol. + +```py +class DirectiveProvider(Protocol): + def get_contraints(self) -> Iterator[str]: + return iter([]) + + def get_resources(self) -> Iterator[str]: + return iter([]) + + def get_best_practices(self) -> Iterator[str]: + return iter([]) +``` + +**Example** A web-search component can provide a resource information. Keep in mind that this actually doesn't allow the agent to access the internet. To do this a relevant `Command` needs to be provided. + +```py +class WebSearchComponent(Component, DirectiveProvider): + def get_resources(self) -> Iterator[str]: + yield "Internet access for searches and information gathering." + # We can skip "get_constraints" and "get_best_practices" if they aren't needed +``` + +### `CommandProvider` + +Provides a command that can be executed by the agent. + +```py +class CommandProvider(Protocol): + def get_commands(self) -> Iterator[Command]: + ... +``` + +The easiest way to provide a command is to use `command` decorator on a component method and then yield `Command.from_decorated_function(...)`. Each command needs a name, description and a parameter schema using `JSONSchema`. By default method name is used as a command name, and first part of docstring for the description (before `Args:` or `Returns:`) and schema can be provided in the decorator. + +**Example** Calculator component that can perform multiplication. Agent is able to call this command if it's relevant to a current task and will see the returned result. + +```py +from autogpt.agents.components import Component +from autogpt.agents.protocols import CommandProvider +from autogpt.core.utils.json_schema import JSONSchema +from autogpt.command_decorator import command + + +class CalculatorComponent(Component, CommandProvider): + get_commands(self) -> Iterator[Command]: + yield Command.from_decorated_function(self.add) + + @command(parameters={ + "a": JSONSchema( + type=JSONSchema.Type.INTEGER, + description="The first number", + required=True, + ), + "b": JSONSchema( + type=JSONSchema.Type.INTEGER, + description="The second number", + required=True, + )}) + def multiply(self, a: int, b: int) -> str: + """ + Multiplies two numbers. + + Args: + a: First number + b: Second number + + Returns: + Result of multiplication + """ + return str(a * b) +``` + +The agent will be able to call this command, named `multiply` with two arguments and will receive the result. The command description will be: `Multiplies two numbers.` + +To learn more about commands see [🛠️ Commands](./commands.md). + +## Order-dependent protocols + +The order of components implementing order-dependent protocols is important because the latter ones may depend on the results of the former ones. + +### `MessageProvider` + +Yields messages that will be added to the agent's prompt. You can use either `ChatMessage.user()`: this will interpreted as a user-sent message or `ChatMessage.system()`: that will be more important. + +```py +class MessageProvider(Protocol): + def get_messages(self) -> Iterator[ChatMessage]: + ... +``` + +**Example** Component that provides a message to the agent's prompt. + +```py +class HelloComponent(Component, MessageProvider): + def get_messages(self) -> Iterator[ChatMessage]: + yield ChatMessage.user("Hello World!") +``` + +### `BuildPrompt` + +Is responsible to connect messages, commands and directives to the agent's prompt that is ready to be sent to a llm. There usually is only one component implementing this protocol. +The result of this protocol is a `ChatPrompt` object wrapped inside `Single` for architectural reasons. This may change in the future. + +```py +class BuildPrompt(Protocol): + def build_prompt(self, messages: List[ChatMessage], commands: List[Command], directives: List[str]) -> Single[ChatPrompt]: + ... +``` + +**Example** Component that builds a prompt from messages, commands and directives. + +```py +class PromptBuilderComponent(Component, BuildPrompt): + def build_prompt(self, messages: List[ChatMessage], commands: List[Command], task: str, profile: AIProfile, directives: AIDirectives) -> Single[ChatPrompt]: + messages.insert( + 0, + ChatMessage.system( + f"You are {profile.ai_name}, {profile.ai_role.rstrip('.')}." + "## Constraints\n" + f"{format_numbered_list(directives.constraints)}\n" + "## Resources\n" + f"{format_numbered_list(directives.resources)}\n" + "## Best practices\n" + f"{format_numbered_list(directives.best_practices)}\n" + ), + ) + messages.insert(1, ChatMessage.user(f'"""{task}"""')) + return Single(ChatPrompt(messages=messages, functions=commands)) +``` + +### `ParseResponse` + + +*Depracated* + +### `AfterParse` + +Protocol called after the response is parsed. + +```py +class AfterParse(Protocol): + def after_parse(self, response: ThoughtProcessOutput) -> None: + ... +``` + +**Example** Component that logs the response after it's parsed. + +```py +class LoggerComponent(Component, AfterParse): + def after_parse(self, response: ThoughtProcessOutput) -> None: + logger.info(f"Response: {response}") +``` + +### `AfterExecute` + +Protocol called after the command is executed by the agent. + +```py +class AfterExecute(Protocol): + def after_execute(self, result: ActionResult) -> None: + ... +``` + +**Example** Component that logs the result after the command is executed. + +```py +class LoggerComponent(Component, AfterExecute): + def after_execute(self, result: ActionResult) -> None: + logger.info(f"Result: {result}") +``` \ No newline at end of file diff --git a/docs/content/AutoGPT/modular-agent.md b/docs/content/AutoGPT/modular-agent.md deleted file mode 100644 index 480b7047b57..00000000000 --- a/docs/content/AutoGPT/modular-agent.md +++ /dev/null @@ -1,93 +0,0 @@ -# Modular Agents - -This incremental and realtively small re-architecture rebuilds `*Agent` classes so they are composed of `Components` instead of current multiple inheritance with Mixins. - ->Due to technical debt the state of current codebase becomes increasingly unwieldy and is harder to implement new features. I'm aware of the failed past re-arch attempt and I took that into account when considering this change. Currently Mixins are very confusing - it's hard to track the order of execution and what's happening in the code. Also, plugins feel like additional baggage that needs to be maintained and not as a useful part of the system. ~kcze - -This re-arch reuses much current code and is done in-place (not from scratch). With this fully implemented, Agent becomes a set of components, and that unifies Agent code and Plugins, now *everything* is just components. - -This change directly addresses point 2 of the Roadmap [Empowering Agent Builders](https://github.com/Significant-Gravitas/AutoGPT/discussions/6970) and may also have a positive impact on others (due to ease of use and extension). - -## Main goals: -- Simplify Agent and cut out redundant code -- Make it easy to create new Agents and Components (plugins) - -### Extra consequences: -- Less config and env vars -- Reduce coupling - -### Tasks -- [x] Simplify commands -- [x] Restructure `OneShotAgentPromptStrategy` into smaller components -- [x] Port Commands to Components - - [x] System - - [x] Event history - - [x] User interaction - - [x] Context - - [x] File operations - - [x] Code execution - - [x] Git operations - - [x] Image generation - - [x] Web search - - [x] Selenium -- [x] TODOs -- [ ] Tests -- [ ] Documentation - -## How it works -Agent is composed of *components*, and each `Component` implements a range of `Protocol`s (interfaces), each one providing a specific functionality, e.g. additional commands or messages. Each Protocol needs to be handled by BaseAgent's logic, so preferably an exhaustive set of Protocols should be provided. - -Agent has methods (currently `propose_action` and `execute`) that execute *pipelines* that call methods on components in a specified order. - -This system is simple, flexible, requires basically no configuration, and doesn't hide any data (anything can still be passed or accessed directly from or between components). - -Example component [ported](https://github.com/kcze/AutoGPT/blob/kpczerwinski/open-440-modular-agents/autogpts/autogpt/autogpt/components/context.py): - -Example Protocol (i.e. interface): -```py -@runtime_checkable -class MessageProvider(Protocol): - def get_messages(self) -> Iterator[ChatMessage]: - ... -``` -Component (i.e. plugin) that implements it: -```py -class MyComponent(Component, MessageProvider): - def get_messages(self) -> Iterator[ChatMessage]: - yield ChatMessage("This will be injected to prompt!") -``` -Custom Agent that uses the component: -```py -class MyAgent(Agent): - def __init__(self): - # Optional super call to bring default components - super().__init__(...) - self.my_component = MyComponent() - # Can define explicit ordering, otherwise components are sorted automatically - # self.components = [self.my_component] -``` -And that's it! Components are automatically collected from the agent using python metaclass magic and are called when needed. - -Now purpose-related things will be bundled together: so `FileManagerComponent` provides file-system related commands and resources information. This should cut out a lot of code and also make system more type safe. - ->Everything is plug-and-play, disabling anything = disabling a component; don't want file access? Just remove the `FileManagerComponent`, no changes to configuration, banning commands, etc... Assuming that no other components rely on it but you would know because it would have to be *explicitly* passed to the component, no more `isinstance`, `agent_implements_x`.. - -### Other stuff -Debugging may be easier because we can inspect the exact components that were called and where the pipeline failed (current WIP pipeline): - -![](../imgs/modular-pipeline.png) - -Also that makes it possible to call component/pipeline/function again when failed and recover. - -If it's necessary to get a component in a random place, agent provides generic, type safe `get_component(type[T]) -> T | None` - -## Challenges -- Ordering: previously there was really no ordering, everything was hardcoded (not sure about plugins). Components can be now ordered explicitly or automatically (each component will be sorted according to its `run_after` attribute). This can cause issues with circular dependencies. -- Efficient and type safe pipelines code -- There is some logic that can be challenging to adapt -- Pipeline logic is tricky, some Protcols return iterables, some single objects, some None - -## Future possibilities -- Adding or removing components during runtime -- Parallel component execution -- Cacheable pipelines \ No newline at end of file