Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into nerfzael/swap-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nerfZael committed Apr 12, 2024
2 parents 85a8205 + 8b5eb60 commit 1e779f8
Show file tree
Hide file tree
Showing 14 changed files with 202 additions and 105 deletions.
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@

![](./docs/img/banner.png)

AutoTx is a personal assistant that generates on-chain transactions for you. These transactions are submitted to a smart account so users can easily approve & execute them.

AutoTx is a personal assistant that plans and proposes on-chain transactions for you. These tx bundles are submitted to a smart account so users can easily execute them.
<img src="./docs/img/demo.gif" alt="Demo GIF of AutoTx">

> [!WARNING]
> This project is still early and experimental. Exercise caution when using real funds.
## How It Works

AutoTx employs a multi-agent orchestration architecture to easily compose functionality. Given a user's prompt, it will first create a plan for how it will satisfy the user's intents. During the plan's execution, individual agents are used to complete tasks described within the plan.
AutoTx employs a multi-agent orchestration architecture to easily compose functionality. Given a user prompt, AutoTx will create a new shared context amongst all agents in the form of an [Autogen Group Chat](https://microsoft.github.io/autogen/docs/tutorial/conversation-patterns#group-chat). Individual agents will contribute their unique expert opinions to the shared conversation. Agent tools will be selected and run to progressively solve for the goal(s) defined within the user's original prompt.

Agents can add transactions to the bundle, which will later be proposed to the user's smart account for final approval before on-chain execution. Currently AutoTx supports [Safe](https://safe.global/) smart accounts. AutoTx uses a locally-stored private key to submit transactions to the user's smart account. AutoTx can create a new smart account for the user, or connect to an existing account (instructions below).
Agent tools can add transactions to a batch, which will later be proposed to the user's smart account for final approval before being executed on-chain. Currently AutoTx supports [Safe](https://safe.global/) smart accounts. AutoTx uses a locally-stored private key to submit transactions to the user's smart account.

![](./docs/img/diagram.png)

## Agents

Expand All @@ -26,14 +29,14 @@ Below is a list of existing and anticipated agents that AutoTx can use. If you'd
| [Token Research](./autotx/agents/ResearchTokensAgent.py) | Research tokens, liquidity, prices, graphs, etc. | :rocket: |
| Earn Yield | Stake assets to earn yield. | :memo: [draft](https://github.com/polywrap/AutoTx/issues/98) |
| Bridge Tokens | Bridge tokens from one chain to another. | :memo: [draft](https://github.com/polywrap/AutoTx/issues/46) |
| Social Search | Research accounts, posts, and sentiment across social networks (ex: Twitter, Farcaster) | :memo: [draft](https://github.com/polywrap/AutoTx/issues/204) |
| NFTs | Basic NFT integration: mint, transfer, set approval, etc. | :memo: [draft](https://github.com/polywrap/AutoTx/issues/45) |
| NFT Market | NFT marketplace functionality: list, bid, etc. | :thought_balloon: |
| LP | Provide liquidity to AMMs. | :thought_balloon: |
| Governance | Vote or delegate in DAOs. | :thought_balloon: |
| Predict | Generate future predictions based on research. | :thought_balloon: |
| Donate | Donate to public goods projects. | :thought_balloon: |
| Invest | Participate in LBPs, IDOs, etc. | :thought_balloon: |
| Social | Use social networks (ex: Farcaster). | :thought_balloon: |

# Getting Started
## Pre-Requisites
Expand All @@ -54,11 +57,13 @@ Please install the following:

## Run The Agent

1. AutoTx requires a fork of the blockchain network you want to transact with. You can start the fork by running `poetry run start-fork`, and stop it with `poetry run stop-fork`. This command requires Docker to be running on your computer.
2. Run `poetry run ask` and provide a prompt for AutoTx to work on solving for you (example: `Send 1 ETH to vitalik.eth`). You can also provide the prompt as an argument for non-interactive startup. The `--non-interactive` (or `-n`) flag will disable all requests for user input, including the final approval of the transaction plan. The `--verbose` (or `-v`) flag will enable verbose logging.
1. Run `poetry run start-devnet` if you want to test locally. More information [below](#test-offline).
2. Run `poetry run ask` and AutoTx will ask you for a prompt to start solving for (ex: `Send 1 ETH to vitalik.eth`). Prompts can also be passed as an argument (ex: `poetry run ask "..."`). The `ask` CLI has options: `--verbose, -v` to enable verbose logging, and `--non-interactive, -n` to disable all requests for user input.

### Test Offline
By default, if the `SMART_ACCOUNT_ADDRESS` environment variable is not defined, AutoTx will create and execute transactions within an offline test environment. This test environment includes a new smart account, as well as a development address with test ETH for tx execution.
By default, if the `SMART_ACCOUNT_ADDRESS` environment variable is not defined, AutoTx will create and execute transactions within an offline test environment.
You can start this test environment by running `poetry run start-devnet`, and stop it with `poetry run stop-devnet`. This command requires Docker to be running on your computer and the CHAIN_RPC_URL to be set to the RPC url of the network you want fork.
This test environment includes a new smart account, as well as a development address with test ETH for tx execution.

### Connect a Smart Account
AutoTx can be connected to your existing smart account by doing the following:
Expand Down Expand Up @@ -111,9 +116,10 @@ Connect with us on [Discord](https://discord.gg/k7UCsH3ps9) if you have any ques
## Building Agents

To add agents to AutoTx, we recommend starting with the [`ExampleAgent.py`](./autotx/agents/ExampleAgent.py) starter template. From there you'll want to:
1. Define the agent's `name` and `system_message`.
1. Implement the tools (functions) you want the agent to be able to call.
2. Add all tools to the agent's `tools=[...]` array.
3. Add your new agent to `AutoTx`'s constructor in [`cli.py`](./autotx/cli.py).
1. Add all tools to the agent's `tools=[...]` array.
1. Add your new agent to `AutoTx`'s constructor in [`cli.py`](./autotx/cli.py).

### Testing

Expand Down
172 changes: 106 additions & 66 deletions autotx/AutoTx.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
class Config:
verbose: bool

@dataclass
class PastRun:
feedback: str
transactions_info: str

class AutoTx:
manager: SafeManager
config: Config = Config(verbose=False)
Expand All @@ -37,83 +42,118 @@ def __init__(
self.agents = agents

def run(self, prompt: str, non_interactive: bool, silent: bool = False) -> None:
print("Running AutoTx with the following prompt: ", prompt)

user_proxy = UserProxyAgent(
name="user_proxy",
is_termination_msg=lambda x: x.get("content", "") and x.get("content", "").rstrip().endswith("TERMINATE"),
human_input_mode="NEVER",
max_consecutive_auto_reply=20,
system_message=f"You are a user proxy. You will be interacting with the agents to accomplish the tasks.",
llm_config=self.get_llm_config(),
code_execution_config=False,
)

agents_information = self.get_agents_information(self.agents)

goal = build_goal(prompt, agents_information, self.manager.address, non_interactive)

verifier_agent = AssistantAgent(
name="verifier",
is_termination_msg=lambda x: x.get("content", "") and x.get("content", "").rstrip().endswith("TERMINATE"),
system_message=dedent(
"""
Verifier is an expert in verifiying if user goals are met.
Verifier analyzes chat and responds with TERMINATE if the goal is met.
Verifier can consider the goal met if the other agents have prepared the necessary transactions.
original_prompt = prompt
past_runs: list[PastRun] = []

while True:
if past_runs:
self.transactions.clear()

prev_history = "".join(
[
dedent(f"""
Then you prepared these transactions to accomplish the goal:
{run.transactions_info}
Then the user provided feedback:
{run.feedback}
""")
for run in past_runs
]
)

prompt = (f"\nOriginaly the user said: {original_prompt}"
+ prev_history
+ "Pay close attention to the user's feedback and try again.\n")

print("Running AutoTx with the following prompt: ", prompt)

user_proxy = UserProxyAgent(
name="user_proxy",
is_termination_msg=lambda x: x.get("content", "") and x.get("content", "").rstrip().endswith("TERMINATE"),
human_input_mode="NEVER",
max_consecutive_auto_reply=20,
system_message=f"You are a user proxy. You will be interacting with the agents to accomplish the tasks.",
llm_config=self.get_llm_config(),
code_execution_config=False,
)

agents_information = self.get_agents_information(self.agents)

goal = build_goal(prompt, agents_information, self.manager.address, non_interactive)

verifier_agent = AssistantAgent(
name="verifier",
is_termination_msg=lambda x: x.get("content", "") and x.get("content", "").rstrip().endswith("TERMINATE"),
system_message=dedent(
"""
Verifier is an expert in verifiying if user goals are met.
Verifier analyzes chat and responds with TERMINATE if the goal is met.
Verifier can consider the goal met if the other agents have prepared the necessary transactions.
Let the other agents complete all the necessary parts of the goal before calling TERMINATE.
If some information needs to be returned to the user or if there are any errors encountered during the process, add this in your answer.
Start any error messages with "ERROR:" to clearly indicate the issue. Then say the word TERMINATE.
Make sure to only add information if the user explicitly asks for a question that needs to be answered
or error details if user's request can not be completed.
If some information needs to be returned to the user or if there are any errors encountered during the process, add this in your answer.
Start any error messages with "ERROR:" to clearly indicate the issue. Then say the word TERMINATE.
Make sure to only add information if the user explicitly asks for a question that needs to be answered
or error details if user's request can not be completed.
"""
),
llm_config=self.get_llm_config(),
human_input_mode="NEVER",
code_execution_config=False,
)

autogen_agents = [agent.build_autogen_agent(self, user_proxy, self.get_llm_config()) for agent in self.agents]

groupchat = GroupChat(
agents=autogen_agents + [user_proxy, verifier_agent],
messages=[],
max_round=20,
select_speaker_prompt_template = (
"""
Read the above conversation. Then select the next role from {agentlist} to play. Only return the role and NOTHING else.
"""
),
llm_config=self.get_llm_config(),
human_input_mode="NEVER",
code_execution_config=False,
)

autogen_agents = [agent.build_autogen_agent(self, user_proxy, self.get_llm_config()) for agent in self.agents]

groupchat = GroupChat(
agents=autogen_agents + [user_proxy, verifier_agent],
messages=[],
max_round=20,
select_speaker_prompt_template = (
"""
Read the above conversation. Then select the next role from {agentlist} to play. Only return the role and NOTHING else.
"""
)
)
)
manager = GroupChatManager(groupchat=groupchat, llm_config=self.get_llm_config())

if silent:
IOStream.set_global_default(IOSilent())
else:
IOStream.set_global_default(IOConsole())

chat = user_proxy.initiate_chat(manager, message=dedent(
f"""
My goal is: {prompt}
Advisor reworded: {goal}
"""
))

if chat.summary:
manager = GroupChatManager(groupchat=groupchat, llm_config=self.get_llm_config())

if silent:
IOStream.set_global_default(IOSilent())
else:
IOStream.set_global_default(IOConsole())

chat = user_proxy.initiate_chat(manager, message=dedent(
f"""
My goal is: {prompt}
Advisor reworded: {goal}
"""
))

if "ERROR:" in chat.summary:
error_message = chat.summary.replace("ERROR: ", "").replace("\n", "")
cprint(error_message, "red")
else:
cprint(chat.summary.replace("\n", ""), "green")

try:
self.manager.send_tx_batch(self.transactions, require_approval=not non_interactive)
except Exception as e:
cprint(e, "red")
try:
result = self.manager.send_tx_batch(self.transactions, require_approval=not non_interactive)

if isinstance(result, str):
transactions_info ="\n".join(
[
f"{i + 1}. {tx.summary}"
for i, tx in enumerate(self.transactions)
]
)

past_runs.append(PastRun(result, transactions_info))
else:
break

except Exception as e:
cprint(e, "red")
break

self.transactions.clear()


def get_agents_information(self, agents: list[AutoTxAgent]) -> str:
agent_descriptions = []
Expand Down
3 changes: 2 additions & 1 deletion autotx/build_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ def run() -> None:
result = subprocess.run(["mypy", "."], capture_output=True)
print(result.stdout.decode())
if result.returncode != 0:
print(result.stderr.decode())
print("Type checking failed")
sys.exit(1)
sys.exit(1)
7 changes: 6 additions & 1 deletion autotx/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from dotenv import load_dotenv
import click

from autotx.utils.is_dev_env import is_dev_env

load_dotenv()

from autotx.agents.ResearchTokensAgent import ResearchTokensAgent
Expand Down Expand Up @@ -39,7 +41,10 @@ def run(prompt: str | None, non_interactive: bool, verbose: bool) -> None:

network_info = NetworkInfo(chain_id)

print(f"Network: {network_info.chain_id.name}")
if is_dev_env():
print(f"Connected to fork of {network_info.chain_id.name} network.")
else:
print(f"Connected to {network_info.chain_id.name} network.")

print_agent_address()

Expand Down
24 changes: 18 additions & 6 deletions autotx/utils/configuration.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,39 @@
import os
import sys
from time import sleep

from web3 import Web3
from autotx.get_env_vars import get_env_vars
from gnosis.eth import EthereumClient
from eth_typing import URI
from eth_account.signers.local import LocalAccount
from web3 import Web3, HTTPProvider

from autotx.utils.ethereum.agent_account import get_or_create_agent_account
from autotx.utils.ethereum.constants import FORK_RPC_URL
from autotx.utils.ethereum.constants import DEVNET_RPC_URL
from autotx.utils.ethereum.eth_address import ETHAddress
from autotx.utils.is_dev_env import is_dev_env

smart_account_addr = get_env_vars()

def get_configuration() -> tuple[ETHAddress | None, LocalAccount, EthereumClient]:
w3 = Web3(HTTPProvider(FORK_RPC_URL))
rpc_url = DEVNET_RPC_URL if is_dev_env() else os.getenv("CHAIN_RPC_URL")

if not rpc_url:
sys.exit("CHAIN_RPC_URL is not set")

web3 = Web3(Web3.HTTPProvider(rpc_url))

for i in range(16):
if w3.is_connected():
if web3.is_connected():
break
if i == 15:
sys.exit("Can not connect with local node. Did you run `poetry run start-fork`?")
if is_dev_env():
sys.exit("Can not connect with local node. Did you run `poetry run start-devnet`?")
else:
sys.exit("Can not connect with remote node. Check your CHAIN_RPC_URL")
sleep(0.5)

client = EthereumClient(URI(FORK_RPC_URL))
client = EthereumClient(URI(rpc_url))
agent = get_or_create_agent_account()

smart_account = ETHAddress(smart_account_addr, client.w3) if smart_account_addr else None
Expand Down
Loading

0 comments on commit 1e779f8

Please sign in to comment.