From 8e50902de5da677fabbd9009fb2e05268e12e3f3 Mon Sep 17 00:00:00 2001 From: nerfZael Date: Wed, 12 Jun 2024 23:14:46 +0200 Subject: [PATCH 01/26] sending transactions to tx service no longer calculates gas; implemented and using sending multi send tx for tx batch --- autotx/server.py | 2 +- autotx/utils/ethereum/SafeManager.py | 82 ++++++++++++++++++++++++++-- autotx/wallets/safe_smart_wallet.py | 2 +- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/autotx/server.py b/autotx/server.py index 32d09c2..db0b99e 100644 --- a/autotx/server.py +++ b/autotx/server.py @@ -203,7 +203,7 @@ def send_transactions(task_id: str, model: models.SendTransactionsParams, author try: app_config = AppConfig.load(smart_account_addr=task.address, subsidized_chain_id=task.chain_id, agent=agent) - app_config.manager.send_tx_batch( + app_config.manager.send_multisend_tx_batch( task.transactions, require_approval=False, ) diff --git a/autotx/utils/ethereum/SafeManager.py b/autotx/utils/ethereum/SafeManager.py index 835fdb6..4752c1c 100644 --- a/autotx/utils/ethereum/SafeManager.py +++ b/autotx/utils/ethereum/SafeManager.py @@ -136,7 +136,7 @@ def build_multisend_tx(self, txs: list[TxParams], safe_nonce: Optional[int] = No return safe_tx - def build_tx(self, tx: TxParams, safe_nonce: Optional[int] = None) -> SafeTx: + def build_tx(self, tx: TxParams, safe_nonce: Optional[int] = None, skip_estimate_gas: bool = False) -> SafeTx: safe_tx = SafeTx( self.client, self.address.hex, @@ -151,8 +151,10 @@ def build_tx(self, tx: TxParams, safe_nonce: Optional[int] = None) -> SafeTx: self.address.hex, safe_nonce=self.track_nonce(safe_nonce), ) - safe_tx.safe_tx_gas = self.safe.estimate_tx_gas(safe_tx.to, safe_tx.value, safe_tx.data, safe_tx.operation) - safe_tx.base_gas = self.safe.estimate_tx_base_gas(safe_tx.to, safe_tx.value, safe_tx.data, safe_tx.operation, NULL_ADDRESS, safe_tx.safe_tx_gas) + + if not skip_estimate_gas: + safe_tx.safe_tx_gas = self.safe.estimate_tx_gas(safe_tx.to, safe_tx.value, safe_tx.data, safe_tx.operation) + safe_tx.base_gas = self.safe.estimate_tx_base_gas(safe_tx.to, safe_tx.value, safe_tx.data, safe_tx.operation, NULL_ADDRESS, safe_tx.safe_tx_gas) return safe_tx @@ -206,7 +208,7 @@ def post_transaction(self, tx: TxParams, safe_nonce: Optional[int] = None) -> No self.network, ethereum_client=self.client, base_url=self.transaction_service_url ) - safe_tx = self.build_tx(tx, safe_nonce) + safe_tx = self.build_tx(tx, safe_nonce, skip_estimate_gas=True) safe_tx.sign(self.agent.key.hex()) ts_api.post_transaction(safe_tx) @@ -231,7 +233,15 @@ def send_tx(self, tx: TxParams | dict[str, Any], safe_nonce: Optional[int] = Non else: hash = self.execute_tx(cast(TxParams, tx), safe_nonce) return hash.hex() - + + def send_multisend_tx(self, txs: list[TxParams], safe_nonce: Optional[int] = None) -> str | None: + if self.use_tx_service: + self.post_multisend_transaction(txs, safe_nonce) + return None + else: + hash = self.execute_multisend_tx(txs, safe_nonce) + return hash.hex() + def send_tx_batch(self, txs: list[models.Transaction], require_approval: bool, safe_nonce: Optional[int] = None) -> bool | str: # True if sent, False if declined, str if feedback print("=" * 50) @@ -306,6 +316,68 @@ def send_tx_batch(self, txs: list[models.Transaction], require_approval: bool, s return True + def send_multisend_tx_batch(self, txs: list[models.Transaction], require_approval: bool, safe_nonce: Optional[int] = None) -> bool | str: # True if sent, False if declined, str if feedback + print("=" * 50) + + if not txs: + print("No transactions to send.") + return True + + transactions_info = "\n".join( + [ + f"{i + 1}. {tx.summary}" + for i, tx in enumerate(txs) + ] + ) + + print(f"Prepared transactions:\n{transactions_info}") + + if self.use_tx_service: + if require_approval: + response = input("Do you want the above transactions to be sent to your smart account?\nRespond (y/n) or write feedback: ") + + if response.lower() == "n" or response.lower() == "no": + print("Transactions not sent to your smart account (declined).") + + return False + elif response.lower() != "y" and response.lower() != "yes": + + return response + else: + print("Non-interactive mode enabled. Transactions will be sent to your smart account without approval.") + + print("Sending multi-send transaction to your smart account...") + + self.send_multisend_tx([prepared_tx.params for prepared_tx in txs], safe_nonce) + + print("Transactions sent as a single multi-send transaction to your smart account for signing.") + + return True + else: + if require_approval: + response = input("Do you want to execute the above transactions?\nRespond (y/n) or write feedback: ") + + if response.lower() == "n" or response.lower() == "no": + print("Transactions not executed (declined).") + + return False + elif response.lower() != "y" and response.lower() != "yes": + + return response + else: + print("Non-interactive mode enabled. Transactions will be executed without approval.") + + print("Executing transactions...") + + try: + self.send_multisend_tx([prepared_tx.params for prepared_tx in txs], safe_nonce) + except ExecutionRevertedError as e: + raise Exception(f"Executing transactions failed with error: {e}") + + print("Transactions executed as a single multi-send transaction.") + + return True + def send_empty_tx(self, safe_nonce: Optional[int] = None) -> str | None: tx: TxParams = { "to": self.address.hex, diff --git a/autotx/wallets/safe_smart_wallet.py b/autotx/wallets/safe_smart_wallet.py index 5da88f5..d2ae887 100644 --- a/autotx/wallets/safe_smart_wallet.py +++ b/autotx/wallets/safe_smart_wallet.py @@ -17,4 +17,4 @@ def on_transactions_prepared(self, txs: list[models.Transaction]) -> None: pass def on_transactions_ready(self, txs: list[models.Transaction]) -> bool | str: - return self.manager.send_tx_batch(txs, not self.auto_submit_tx) + return self.manager.send_multisend_tx_batch(txs, not self.auto_submit_tx) From 1b9ee05f570351894b439263b3b6b100bc823355 Mon Sep 17 00:00:00 2001 From: nerfZael Date: Thu, 13 Jun 2024 03:02:18 +0200 Subject: [PATCH 02/26] not using a multi send tx if only one tx in batch --- autotx/utils/ethereum/SafeManager.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/autotx/utils/ethereum/SafeManager.py b/autotx/utils/ethereum/SafeManager.py index 4752c1c..9199bdd 100644 --- a/autotx/utils/ethereum/SafeManager.py +++ b/autotx/utils/ethereum/SafeManager.py @@ -236,10 +236,16 @@ def send_tx(self, tx: TxParams | dict[str, Any], safe_nonce: Optional[int] = Non def send_multisend_tx(self, txs: list[TxParams], safe_nonce: Optional[int] = None) -> str | None: if self.use_tx_service: - self.post_multisend_transaction(txs, safe_nonce) + if len(txs) == 1: + self.post_transaction(txs[0], safe_nonce) + elif len(txs) > 1: + self.post_multisend_transaction(txs, safe_nonce) return None else: - hash = self.execute_multisend_tx(txs, safe_nonce) + if len(txs) == 1: + hash = self.execute_tx(txs[0], safe_nonce) + elif len(txs) > 1: + hash = self.execute_multisend_tx(txs, safe_nonce) return hash.hex() def send_tx_batch(self, txs: list[models.Transaction], require_approval: bool, safe_nonce: Optional[int] = None) -> bool | str: # True if sent, False if declined, str if feedback @@ -346,11 +352,14 @@ def send_multisend_tx_batch(self, txs: list[models.Transaction], require_approva else: print("Non-interactive mode enabled. Transactions will be sent to your smart account without approval.") - print("Sending multi-send transaction to your smart account...") + print("Sending batched transactions to your smart account...") self.send_multisend_tx([prepared_tx.params for prepared_tx in txs], safe_nonce) - print("Transactions sent as a single multi-send transaction to your smart account for signing.") + if len(txs) == 1: + print("Transaction sent to your smart account for signing.") + else: + print("Transactions sent as a single multi-send transaction to your smart account for signing.") return True else: @@ -374,7 +383,10 @@ def send_multisend_tx_batch(self, txs: list[models.Transaction], require_approva except ExecutionRevertedError as e: raise Exception(f"Executing transactions failed with error: {e}") - print("Transactions executed as a single multi-send transaction.") + if len(txs) == 1: + print("Transaction executed.") + else: + print("Transactions executed as a single multi-send transaction.") return True From cc292d49a7c1b13399d7e4c88ac8d1ef7fc4bed7 Mon Sep 17 00:00:00 2001 From: nerfZael Date: Thu, 13 Jun 2024 03:07:42 +0200 Subject: [PATCH 03/26] type fixes --- autotx/utils/ethereum/SafeManager.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/autotx/utils/ethereum/SafeManager.py b/autotx/utils/ethereum/SafeManager.py index 9199bdd..eac5c9e 100644 --- a/autotx/utils/ethereum/SafeManager.py +++ b/autotx/utils/ethereum/SafeManager.py @@ -115,7 +115,7 @@ def deploy_multicall(self) -> None: multicall_addr = deploy_multicall(self.client, self.dev_account) self.connect_multicall(multicall_addr) - def build_multisend_tx(self, txs: list[TxParams], safe_nonce: Optional[int] = None) -> SafeTx: + def build_multisend_tx(self, txs: list[TxParams | dict[str, Any]], safe_nonce: Optional[int] = None) -> SafeTx: if not self.multisend: raise Exception("No multisend contract address has been set to SafeManager") @@ -136,7 +136,7 @@ def build_multisend_tx(self, txs: list[TxParams], safe_nonce: Optional[int] = No return safe_tx - def build_tx(self, tx: TxParams, safe_nonce: Optional[int] = None, skip_estimate_gas: bool = False) -> SafeTx: + def build_tx(self, tx: TxParams | dict[str, Any], safe_nonce: Optional[int] = None, skip_estimate_gas: bool = False) -> SafeTx: safe_tx = SafeTx( self.client, self.address.hex, @@ -158,7 +158,7 @@ def build_tx(self, tx: TxParams, safe_nonce: Optional[int] = None, skip_estimate return safe_tx - def execute_tx(self, tx: TxParams, safe_nonce: Optional[int] = None) -> HexBytes: + def execute_tx(self, tx: TxParams | dict[str, Any], safe_nonce: Optional[int] = None) -> HexBytes: if not self.dev_account: raise ValueError("Dev account not set. This function should not be called in production.") @@ -184,7 +184,7 @@ def execute_tx(self, tx: TxParams, safe_nonce: Optional[int] = None) -> HexBytes raise Exception("Unknown error executing transaction", e) - def execute_multisend_tx(self, txs: list[TxParams], safe_nonce: Optional[int] = None) -> HexBytes: + def execute_multisend_tx(self, txs: list[TxParams | dict[str, Any]], safe_nonce: Optional[int] = None) -> HexBytes: if not self.dev_account: raise ValueError("Dev account not set. This function should not be called in production.") @@ -200,7 +200,7 @@ def execute_multisend_tx(self, txs: list[TxParams], safe_nonce: Optional[int] = return tx_hash - def post_transaction(self, tx: TxParams, safe_nonce: Optional[int] = None) -> None: + def post_transaction(self, tx: TxParams | dict[str, Any], safe_nonce: Optional[int] = None) -> None: if not self.network: raise Exception("Network not defined for transaction service") @@ -213,7 +213,7 @@ def post_transaction(self, tx: TxParams, safe_nonce: Optional[int] = None) -> No ts_api.post_transaction(safe_tx) - def post_multisend_transaction(self, txs: list[TxParams], safe_nonce: Optional[int] = None) -> None: + def post_multisend_transaction(self, txs: list[TxParams | dict[str, Any]], safe_nonce: Optional[int] = None) -> None: if not self.network: raise Exception("Network not defined for transaction service") @@ -234,7 +234,7 @@ def send_tx(self, tx: TxParams | dict[str, Any], safe_nonce: Optional[int] = Non hash = self.execute_tx(cast(TxParams, tx), safe_nonce) return hash.hex() - def send_multisend_tx(self, txs: list[TxParams], safe_nonce: Optional[int] = None) -> str | None: + def send_multisend_tx(self, txs: list[TxParams | dict[str, Any]], safe_nonce: Optional[int] = None) -> str | None: if self.use_tx_service: if len(txs) == 1: self.post_transaction(txs[0], safe_nonce) From f0d5da8b89bb6b43615ae0447b9afd5d22e1b83d Mon Sep 17 00:00:00 2001 From: nerfZael Date: Fri, 14 Jun 2024 20:34:32 +0200 Subject: [PATCH 04/26] implemented intents that get built into transactions --- autotx/AutoTx.py | 37 +++--- autotx/agents/ExampleAgent.py | 4 +- autotx/agents/SendTokensAgent.py | 29 ++--- autotx/agents/SwapTokensAgent.py | 40 ++++-- autotx/db.py | 25 ++-- autotx/intents.py | 122 ++++++++++++++++++ autotx/models.py | 81 +----------- autotx/server.py | 83 +++++++++--- autotx/setup.py | 2 +- autotx/tests/api/test_send_transactions.py | 69 +++++++++- autotx/tests/api/test_tasks.py | 12 +- autotx/token.py | 5 + autotx/transactions.py | 67 ++++++++++ autotx/utils/ethereum/SafeManager.py | 24 ++-- autotx/utils/ethereum/lifi/swap.py | 86 ++++++++++-- autotx/wallets/api_smart_wallet.py | 16 +-- autotx/wallets/safe_smart_wallet.py | 13 +- autotx/wallets/smart_wallet.py | 6 +- .../migrations/20240614104522_intents.sql | 8 ++ 19 files changed, 516 insertions(+), 213 deletions(-) create mode 100644 autotx/intents.py create mode 100644 autotx/token.py create mode 100644 autotx/transactions.py create mode 100644 supabase/migrations/20240614104522_intents.sql diff --git a/autotx/AutoTx.py b/autotx/AutoTx.py index bc73a38..3fd8582 100644 --- a/autotx/AutoTx.py +++ b/autotx/AutoTx.py @@ -14,6 +14,7 @@ from autotx import models from autotx.autotx_agent import AutoTxAgent from autotx.helper_agents import clarifier, manager, user_proxy +from autotx.intents import Intent from autotx.utils.color import Color from autotx.utils.logging.Logger import Logger from autotx.utils.ethereum.networks import NetworkInfo @@ -38,7 +39,7 @@ def __init__(self, verbose: bool, get_llm_config: Callable[[], Optional[Dict[str @dataclass class PastRun: feedback: str - transactions_info: str + intents_info: str class EndReason(Enum): TERMINATE = "TERMINATE" @@ -48,7 +49,7 @@ class EndReason(Enum): class RunResult: summary: str chat_history_json: str - transactions: list[models.Transaction] + intents: list[Intent] end_reason: EndReason total_cost_without_cache: float total_cost_with_cache: float @@ -58,7 +59,7 @@ class AutoTx: web3: Web3 wallet: SmartWallet logger: Logger - transactions: list[models.Transaction] + intents: list[Intent] network: NetworkInfo get_llm_config: Callable[[], Optional[Dict[str, Any]]] agents: list[AutoTxAgent] @@ -91,7 +92,7 @@ def __init__( self.max_rounds = config.max_rounds self.verbose = config.verbose self.get_llm_config = config.get_llm_config - self.transactions = [] + self.intents = [] self.current_run_cost_without_cache = 0 self.current_run_cost_with_cache = 0 self.info_messages = [] @@ -128,7 +129,7 @@ async def a_run(self, prompt: str, non_interactive: bool, summary_method: str = return RunResult( result.summary, result.chat_history_json, - result.transactions, + result.intents, result.end_reason, total_cost_without_cache, total_cost_with_cache, @@ -152,13 +153,13 @@ async def try_run(self, prompt: str, non_interactive: bool, summary_method: str while True: if past_runs: - self.transactions.clear() + self.intents.clear() prev_history = "".join( [ dedent(f""" Then you prepared these transactions to accomplish the goal: - {run.transactions_info} + {run.intents_info} Then the user provided feedback: {run.feedback} """) @@ -208,17 +209,17 @@ async def try_run(self, prompt: str, non_interactive: bool, summary_method: str is_goal_supported = chat.chat_history[-1]["content"] != "Goal not supported: TERMINATE" try: - result = self.wallet.on_transactions_ready(self.transactions) + result = self.wallet.on_intents_ready(self.intents) if isinstance(result, str): - transactions_info ="\n".join( + intents_info ="\n".join( [ f"{i + 1}. {tx.summary}" - for i, tx in enumerate(self.transactions) + for i, tx in enumerate(self.intents) ] ) - past_runs.append(PastRun(result, transactions_info)) + past_runs.append(PastRun(result, intents_info)) else: break @@ -228,17 +229,17 @@ async def try_run(self, prompt: str, non_interactive: bool, summary_method: str self.logger.stop() - # Copy transactions to a new list to avoid modifying the original list - transactions = self.transactions.copy() - self.transactions.clear() + # Copy intents to a new list to avoid modifying the original list + intents = self.intents.copy() + self.intents.clear() chat_history = json.dumps(chat.chat_history, indent=4) - return RunResult(chat.summary, chat_history, transactions, EndReason.TERMINATE if is_goal_supported else EndReason.GOAL_NOT_SUPPORTED, float(chat.cost["usage_including_cached_inference"]["total_cost"]), float(chat.cost["usage_excluding_cached_inference"]["total_cost"]), self.info_messages) + return RunResult(chat.summary, chat_history, intents, EndReason.TERMINATE if is_goal_supported else EndReason.GOAL_NOT_SUPPORTED, float(chat.cost["usage_including_cached_inference"]["total_cost"]), float(chat.cost["usage_excluding_cached_inference"]["total_cost"]), self.info_messages) - def add_transactions(self, txs: list[models.Transaction]) -> None: - self.transactions.extend(txs) - self.wallet.on_transactions_prepared(txs) + def add_intents(self, txs: list[Intent]) -> None: + self.intents.extend(txs) + self.wallet.on_intents_prepared(txs) def notify_user(self, message: str, color: Color | None = None) -> None: if color: diff --git a/autotx/agents/ExampleAgent.py b/autotx/agents/ExampleAgent.py index 4f40fc0..2847cd6 100644 --- a/autotx/agents/ExampleAgent.py +++ b/autotx/agents/ExampleAgent.py @@ -26,8 +26,8 @@ def run( # TODO: do something useful autotx.notify_user(f"ExampleTool run: {amount} {receiver}") - # NOTE: you can add transactions to AutoTx's current bundle - # autotx.transactions.append(tx) + # NOTE: you can add intents to AutoTx's current bundle + # autotx.intents.append(tx) return f"Something useful has been done with {amount} to {receiver}" diff --git a/autotx/agents/SendTokensAgent.py b/autotx/agents/SendTokensAgent.py index aeea092..4641379 100644 --- a/autotx/agents/SendTokensAgent.py +++ b/autotx/agents/SendTokensAgent.py @@ -1,17 +1,15 @@ from textwrap import dedent -from typing import Annotated, Any, Callable, cast +from typing import Annotated, Any, Callable -from web3 import Web3 -from autotx import models from autotx.AutoTx import AutoTx from autotx.autotx_agent import AutoTxAgent from autotx.autotx_tool import AutoTxTool +from autotx.intents import SendIntent +from autotx.token import Token from autotx.utils.ethereum import ( build_transfer_erc20, get_erc20_balance, ) -from autotx.utils.ethereum.build_transfer_native import build_transfer_native -from web3.constants import ADDRESS_ZERO from autotx.utils.ethereum.constants import NATIVE_TOKEN_ADDRESS from autotx.utils.ethereum.eth_address import ETHAddress from autotx.utils.ethereum.get_native_balance import get_native_balance @@ -89,26 +87,17 @@ def run( receiver_addr = ETHAddress(receiver) token_address = ETHAddress(autotx.network.tokens[token.lower()]) - tx: TxParams - - if token_address.hex == NATIVE_TOKEN_ADDRESS: - tx = build_transfer_native(autotx.web3, ETHAddress(ADDRESS_ZERO), receiver_addr, amount) - else: - tx = build_transfer_erc20(autotx.web3, token_address, receiver_addr, amount) - - prepared_tx = models.SendTransaction.create( - token_symbol=token, - token_address=str(token_address), + intent = SendIntent.create( + token=Token(symbol=token, address=token_address.hex), amount=amount, - receiver=str(receiver_addr), - params=cast(dict[str, Any], tx), + receiver=receiver_addr ) - autotx.add_transactions([prepared_tx]) + autotx.add_intents([intent]) - autotx.notify_user(f"Prepared transaction: {prepared_tx.summary}") + autotx.notify_user(f"Prepared transaction: {intent.summary}") - return prepared_tx.summary + return intent.summary return run diff --git a/autotx/agents/SwapTokensAgent.py b/autotx/agents/SwapTokensAgent.py index 3d9c079..06e58b9 100644 --- a/autotx/agents/SwapTokensAgent.py +++ b/autotx/agents/SwapTokensAgent.py @@ -1,12 +1,13 @@ from decimal import Decimal from textwrap import dedent from typing import Annotated, Callable -from autotx import models from autotx.AutoTx import AutoTx from autotx.autotx_agent import AutoTxAgent from autotx.autotx_tool import AutoTxTool +from autotx.intents import BuyIntent, Intent, SellIntent +from autotx.token import Token from autotx.utils.ethereum.eth_address import ETHAddress -from autotx.utils.ethereum.lifi.swap import SUPPORTED_NETWORKS_BY_LIFI, build_swap_transaction +from autotx.utils.ethereum.lifi.swap import SUPPORTED_NETWORKS_BY_LIFI, can_build_swap_transaction from autotx.utils.ethereum.networks import NetworkInfo from gnosis.eth import EthereumNetworkNotSupported as ChainIdNotSupported @@ -80,7 +81,7 @@ def get_tokens_address(token_in: str, token_out: str, network_info: NetworkInfo) class InvalidInput(Exception): pass -def swap(autotx: AutoTx, token_to_sell: str, token_to_buy: str) -> list[models.Transaction]: +def swap(autotx: AutoTx, token_to_sell: str, token_to_buy: str) -> Intent: sell_parts = token_to_sell.split(" ") buy_parts = token_to_buy.split(" ") @@ -117,7 +118,7 @@ def swap(autotx: AutoTx, token_to_sell: str, token_to_buy: str) -> list[models.T token_in, token_out, autotx.network ) - swap_transactions = build_swap_transaction( + can_build_swap_transaction( autotx.web3, Decimal(exact_amount), ETHAddress(token_in_address), @@ -126,9 +127,21 @@ def swap(autotx: AutoTx, token_to_sell: str, token_to_buy: str) -> list[models.T is_exact_input, autotx.network.chain_id ) - autotx.add_transactions(swap_transactions) - return swap_transactions + # Create buy intent if amount of token to buy is provided else create sell intent + swap_intent: Intent = BuyIntent.create( + from_token=Token(symbol=token_symbol_to_sell, address=token_in_address), + to_token=Token(symbol=token_symbol_to_buy, address=token_out_address), + amount=float(exact_amount), + ) if len(buy_parts) == 2 else SellIntent.create( + from_token=Token(symbol=token_symbol_to_sell, address=token_in_address), + to_token=Token(symbol=token_symbol_to_buy, address=token_out_address), + amount=float(exact_amount), + ) + + autotx.add_intents([swap_intent]) + + return swap_intent class BulkSwapTool(AutoTxTool): name: str = "prepare_bulk_swap_transactions" @@ -152,14 +165,13 @@ def run( ], ) -> str: swaps = tokens.split("\n") - all_txs = [] + all_intents = [] all_errors: list[Exception] = [] for swap_str in swaps: (token_to_sell, token_to_buy) = swap_str.strip().split(" to ") try: - txs = swap(autotx, token_to_sell, token_to_buy) - all_txs.extend(txs) + all_intents.append(swap(autotx, token_to_sell, token_to_buy)) except InvalidInput as e: all_errors.append(e) except Exception as e: @@ -167,21 +179,21 @@ def run( summary = "".join( - f"Prepared transaction: {swap_transaction.summary}\n" - for swap_transaction in all_txs + f"Prepared transaction: {intent.summary}\n" + for intent in all_intents ) if all_errors: summary += "\n".join(str(e) for e in all_errors) - if len(all_txs) > 0: - summary += f"\n{len(all_errors)} errors occurred. {len(all_txs)} transactions were prepared. There is no need to re-run the transactions that were prepared." + if len(all_intents) > 0: + summary += f"\n{len(all_errors)} errors occurred. {len(all_intents)} transactions were prepared. There is no need to re-run the transactions that were prepared." else: summary += f"\n{len(all_errors)} errors occurred." total_summary = ("\n" + " " * 16).join( [ f"{i + 1}. {tx.summary}" - for i, tx in enumerate(autotx.transactions) + for i, tx in enumerate(autotx.intents) ] ) diff --git a/autotx/db.py b/autotx/db.py index f2fc999..8dd73f6 100644 --- a/autotx/db.py +++ b/autotx/db.py @@ -1,6 +1,7 @@ from datetime import datetime import json import os +from typing import Any import uuid from pydantic import BaseModel from supabase import create_client @@ -8,6 +9,7 @@ from supabase.lib.client_options import ClientOptions from autotx import models +from autotx.transactions import Transaction SUPABASE_URL = os.getenv("SUPABASE_URL") SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY") @@ -51,7 +53,7 @@ def start(self, prompt: str, address: str, chain_id: int, app_user_id: str) -> m "created_at": str(created_at), "updated_at": str(updated_at), "messages": json.dumps([]), - "transactions": json.dumps([]) + "intents": json.dumps([]) } ).execute() @@ -65,7 +67,7 @@ def start(self, prompt: str, address: str, chain_id: int, app_user_id: str) -> m running=True, error=None, messages=[], - transactions=[] + intents=[] ) def stop(self, task_id: str) -> None: @@ -81,7 +83,7 @@ def stop(self, task_id: str) -> None: def update(self, task: models.Task) -> None: client = get_db_client("public") - txs = [json.loads(tx.json()) for tx in task.transactions] + intents = [json.loads(intent.json()) for intent in task.intents] client.table("tasks").update( { @@ -90,7 +92,7 @@ def update(self, task: models.Task) -> None: "updated_at": str(datetime.utcnow()), "messages": json.dumps(task.messages), "error": task.error, - "transactions": json.dumps(txs) + "intents": json.dumps(intents) } ).eq("id", task.id).eq("app_id", self.app_id).execute() @@ -118,7 +120,7 @@ def get(self, task_id: str) -> models.Task | None: running=task_data["running"], error=task_data["error"], messages=json.loads(task_data["messages"]), - transactions=json.loads(task_data["transactions"]) + intents=json.loads(task_data["intents"]) ) def get_all(self) -> list[models.Task]: @@ -140,7 +142,7 @@ def get_all(self) -> list[models.Task]: running=task_data["running"], error=task_data["error"], messages=json.loads(task_data["messages"]), - transactions=json.loads(task_data["transactions"]) + intents=json.loads(task_data["intents"]) ) ) @@ -222,9 +224,11 @@ def get_agent_private_key(app_id: str, user_id: str) -> str | None: return str(result.data[0]["agent_private_key"]) -def submit_transactions(app_id: str, address: str, chain_id: int, app_user_id: str, task_id: str) -> None: +def submit_transactions(app_id: str, address: str, chain_id: int, app_user_id: str, task_id: str, transactions: list[Transaction]) -> None: client = get_db_client("public") + txs = [json.loads(tx.json()) for tx in transactions] + created_at = datetime.utcnow() client.table("submitted_batches") \ .insert( @@ -234,7 +238,8 @@ def submit_transactions(app_id: str, address: str, chain_id: int, app_user_id: s "chain_id": chain_id, "app_user_id": app_user_id, "task_id": task_id, - "created_at": str(created_at) + "created_at": str(created_at), + "transactions": json.dumps(txs) } ).execute() @@ -246,6 +251,7 @@ class SubmittedBatch(BaseModel): app_user_id: str task_id: str created_at: datetime + transactions: list[dict[str, Any]] def get_submitted_batches(app_id: str, task_id: str) -> list[SubmittedBatch]: client = get_db_client("public") @@ -267,7 +273,8 @@ def get_submitted_batches(app_id: str, task_id: str) -> list[SubmittedBatch]: chain_id=batch_data["chain_id"], app_user_id=batch_data["app_user_id"], task_id=batch_data["task_id"], - created_at=batch_data["created_at"] + created_at=batch_data["created_at"], + transactions=json.loads(batch_data["transactions"]) ) ) diff --git a/autotx/intents.py b/autotx/intents.py new file mode 100644 index 0000000..986c6da --- /dev/null +++ b/autotx/intents.py @@ -0,0 +1,122 @@ +from abc import abstractmethod +from decimal import Decimal +from enum import Enum +from pydantic import BaseModel +from typing import Any, Union, cast +from web3.types import TxParams + +from web3 import Web3 + +from autotx.token import Token +from autotx.transactions import SendTransaction, Transaction +from autotx.utils.ethereum.build_transfer_erc20 import build_transfer_erc20 +from autotx.utils.ethereum.build_transfer_native import build_transfer_native +from autotx.utils.ethereum.constants import NATIVE_TOKEN_ADDRESS +from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.utils.ethereum.lifi.swap import build_swap_transaction +from autotx.utils.ethereum.networks import NetworkInfo + +class IntentType(str, Enum): + SEND = "send" + BUY = "buy" + SELL = "sell" + +class IntentBase(BaseModel): + type: IntentType + summary: str + + @abstractmethod + def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: + pass + +class SendIntent(IntentBase): + receiver: str + token: Token + amount: float + + @classmethod + def create(cls, token: Token, amount: float, receiver: ETHAddress) -> 'SendIntent': + return cls( + type=IntentType.SEND, + token=token, + amount=amount, + receiver=receiver.hex, + summary=f"Transfer {amount} {token.symbol} to {receiver}", + ) + + def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: + tx: TxParams + + if self.token.address == NATIVE_TOKEN_ADDRESS: + tx = build_transfer_native(web3, ETHAddress(self.token.address), ETHAddress(self.receiver), self.amount) + else: + tx = build_transfer_erc20(web3, ETHAddress(self.token.address), ETHAddress(self.receiver), self.amount) + + transactions: list[Transaction] = [ + SendTransaction.create( + token=self.token, + amount=self.amount, + receiver=self.receiver, + params=cast(dict[str, Any], tx), + ) + ] + + return transactions + +class BuyIntent(IntentBase): + from_token: Token + to_token: Token + amount: float + + @classmethod + def create(cls, from_token: Token, to_token: Token, amount: float) -> 'BuyIntent': + return cls( + type=IntentType.BUY, + from_token=from_token, + to_token=to_token, + amount=amount, + summary=f"Buy {amount} {to_token.symbol} with {from_token.symbol}", + ) + + def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: + transactions = build_swap_transaction( + web3, + Decimal(self.amount), + ETHAddress(self.from_token.address), + ETHAddress(self.to_token.address), + smart_wallet_address, + False, + network.chain_id + ) + + return transactions + +class SellIntent(IntentBase): + from_token: Token + to_token: Token + amount: float + + @classmethod + def create(cls, from_token: Token, to_token: Token, amount: float) -> 'SellIntent': + return cls( + type=IntentType.SELL, + from_token=from_token, + to_token=to_token, + amount=amount, + summary=f"Sell {amount} {from_token.symbol} for {to_token.symbol}", + ) + + def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: + transactions = build_swap_transaction( + web3, + Decimal(self.amount), + ETHAddress(self.from_token.address), + ETHAddress(self.to_token.address), + smart_wallet_address, + True, + network.chain_id + ) + + return transactions + +Intent = Union[SendIntent, BuyIntent, SellIntent] diff --git a/autotx/models.py b/autotx/models.py index 15654ca..9aed4ca 100644 --- a/autotx/models.py +++ b/autotx/models.py @@ -1,79 +1,8 @@ -from enum import Enum -from pydantic import BaseModel, Field -from typing import Any, List, Optional, Union +from pydantic import BaseModel +from typing import Any, List, Optional from datetime import datetime -class TransactionType(str, Enum): - SEND = "send" - APPROVE = "approve" - SWAP = "swap" - -class TransactionBase(BaseModel): - type: TransactionType - id: str = Field(default="") - task_id: str = Field(default="") - summary: str - params: dict[str, Any] - -class SendTransaction(TransactionBase): - receiver: str - token_symbol: str - token_address: str - amount: float - - @classmethod - def create(cls, token_symbol: str, token_address: str, amount: float, receiver: str, params: dict[str, Any]) -> 'SendTransaction': - return cls( - type=TransactionType.SEND, - token_symbol=token_symbol, - token_address=token_address, - amount=amount, - receiver=receiver, - params=params, - summary=f"Transfer {amount} {token_symbol} to {receiver}", - ) - -class ApproveTransaction(TransactionBase): - token_symbol: str - token_address: str - amount: float - spender: str - - @classmethod - def create(cls, token_symbol: str, token_address: str, amount: float, spender: str, params: dict[str, Any]) -> 'ApproveTransaction': - return cls( - type=TransactionType.APPROVE, - token_symbol=token_symbol, - token_address=token_address, - amount=amount, - spender=spender, - params=params, - summary=f"Approve {amount} {token_symbol} to {spender}" - ) - -class SwapTransaction(TransactionBase): - from_token_symbol: str - to_token_symbol: str - from_token_address: str - to_token_address: str - from_amount: float - to_amount: float - - @classmethod - def create(cls, from_token_symbol: str, to_token_symbol: str, from_token_address: str, to_token_address: str, from_amount: float, to_amount: float, params: dict[str, Any]) -> 'SwapTransaction': - return cls( - type=TransactionType.SWAP, - from_token_symbol=from_token_symbol, - to_token_symbol=to_token_symbol, - from_token_address=from_token_address, - to_token_address=to_token_address, - from_amount=from_amount, - to_amount=to_amount, - params=params, - summary=f"Swap {from_amount} {from_token_symbol} for at least {to_amount} {to_token_symbol}" - ) - -Transaction = Union[SendTransaction, ApproveTransaction, SwapTransaction] +from autotx.intents import Intent class Task(BaseModel): id: str @@ -85,7 +14,7 @@ class Task(BaseModel): error: str | None running: bool messages: List[str] - transactions: List[Transaction] + intents: List[Intent] class App(BaseModel): id: str @@ -109,7 +38,7 @@ class TaskCreate(BaseModel): chain_id: Optional[int] = None user_id: str -class SendTransactionsParams(BaseModel): +class BuildTransactionsParams(BaseModel): address: str chain_id: int user_id: str diff --git a/autotx/server.py b/autotx/server.py index db0b99e..4097b0b 100644 --- a/autotx/server.py +++ b/autotx/server.py @@ -10,6 +10,8 @@ from autotx import models, setup from autotx import db from autotx.AutoTx import AutoTx, Config as AutoTxConfig +from autotx.intents import Intent +from autotx.transactions import Transaction from autotx.utils.configuration import AppConfig from autotx.utils.ethereum.chain_short_names import CHAIN_ID_TO_SHORT_NAME from autotx.utils.ethereum.networks import SUPPORTED_NETWORKS_CONFIGURATION_MAP @@ -162,49 +164,92 @@ def get_task(task_id: str, authorization: Annotated[str | None, Header()] = None task = get_task_or_404(task_id, tasks) return task -@app_router.get("/api/v1/tasks/{task_id}/transactions", response_model=List[models.Transaction]) -def get_transactions(task_id: str, authorization: Annotated[str | None, Header()] = None) -> Any: +@app_router.get("/api/v1/tasks/{task_id}/intents", response_model=List[Intent]) +def get_intents(task_id: str, authorization: Annotated[str | None, Header()] = None) -> Any: app = authorize(authorization) tasks = db.TasksRepository(app.id) task = get_task_or_404(task_id, tasks) - return task.transactions + return task.intents -@app_router.post("/api/v1/tasks/{task_id}/transactions") -def send_transactions(task_id: str, model: models.SendTransactionsParams, authorization: Annotated[str | None, Header()] = None) -> str: +def authorize_app_and_user(authorization: str | None, user_id: str) -> tuple[models.App, models.AppUser]: app = authorize(authorization) - app_user = db.get_app_user(app.id, model.user_id) - if not app_user: + app_user = db.get_app_user(app.id, user_id) + + if not app_user: raise HTTPException(status_code=400, detail="User not found") - tasks = db.TasksRepository(app.id) - - task = get_task_or_404(task_id, tasks) + return (app, app_user) +def build_transactions(app_id: str, user_id: str, chain_id: int, address: str, task: models.Task) -> tuple[List[Transaction], AppConfig]: if task.running: raise HTTPException(status_code=400, detail="Task is still running") - agent_private_key = db.get_agent_private_key(app.id, model.user_id) + agent_private_key = db.get_agent_private_key(app_id, user_id) if not agent_private_key: raise HTTPException(status_code=400, detail="User not found") agent = Account.from_key(agent_private_key) - if task.transactions is None or len(task.transactions) == 0: + app_config = AppConfig.load(smart_account_addr=address, subsidized_chain_id=chain_id, agent=agent) + + if task.intents is None or len(task.intents) == 0: + return ([], app_config) + + transactions: list[Transaction] = [] + + for intent in task.intents: + transactions.extend(intent.build_transactions(app_config.web3, app_config.network_info, app_config.manager.address)) + + return (transactions, app_config) + +@app_router.get("/api/v1/tasks/{task_id}/transactions", response_model=List[Transaction]) +def get_transactions( + task_id: str, + address: str, + chain_id: int, + user_id: str, + authorization: Annotated[str | None, Header()] = None +) -> List[Transaction]: + (app, app_user) = authorize_app_and_user(authorization, user_id) + + tasks = db.TasksRepository(app.id) + + task = get_task_or_404(task_id, tasks) + + (transactions, _) = build_transactions(app.id, app_user.user_id, chain_id, address, task) + + return transactions + +@app_router.post("/api/v1/tasks/{task_id}/transactions") +def send_transactions( + task_id: str, + address: str, + chain_id: int, + user_id: str, + authorization: Annotated[str | None, Header()] = None +) -> str: + (app, app_user) = authorize_app_and_user(authorization, user_id) + + tasks = db.TasksRepository(app.id) + + task = get_task_or_404(task_id, tasks) + + (transactions, app_config) = build_transactions(app.id, app_user.user_id, chain_id, address, task) + + if len(transactions) == 0: raise HTTPException(status_code=400, detail="No transactions to send") global autotx_params if autotx_params.is_dev: print("Dev mode: skipping transaction submission") - db.submit_transactions(app.id, model.address, model.chain_id, app_user.id, task_id) - return f"https://app.safe.global/transactions/queue?safe={CHAIN_ID_TO_SHORT_NAME[str(model.chain_id)]}:{model.address}" + db.submit_transactions(app.id, address, chain_id, app_user.id, task_id, transactions) + return f"https://app.safe.global/transactions/queue?safe={CHAIN_ID_TO_SHORT_NAME[str(chain_id)]}:{address}" try: - app_config = AppConfig.load(smart_account_addr=task.address, subsidized_chain_id=task.chain_id, agent=agent) - app_config.manager.send_multisend_tx_batch( - task.transactions, + transactions, require_approval=False, ) except SafeAPIException as e: @@ -213,9 +258,9 @@ def send_transactions(task_id: str, model: models.SendTransactionsParams, author else: raise e - db.submit_transactions(app.id, model.address, model.chain_id, app_user.id, task_id) + db.submit_transactions(app.id, address, chain_id, app_user.id, task_id, transactions) - return f"https://app.safe.global/transactions/queue?safe={CHAIN_ID_TO_SHORT_NAME[str(model.chain_id)]}:{model.address}" + return f"https://app.safe.global/transactions/queue?safe={CHAIN_ID_TO_SHORT_NAME[str(chain_id)]}:{address}" @app_router.get("/api/v1/networks", response_model=List[models.SupportedNetwork]) def get_supported_networks() -> list[models.SupportedNetwork]: diff --git a/autotx/setup.py b/autotx/setup.py index 0d4fcd9..f84526c 100644 --- a/autotx/setup.py +++ b/autotx/setup.py @@ -46,7 +46,7 @@ def setup_safe(smart_account_addr: ETHAddress | None, agent: LocalAccount, clien print(f"Smart account connected: {smart_account_addr}") manager = SafeManager.connect(client, smart_account_addr, agent) - manager.connect_tx_service(network_info.chain_id, network_info.transaction_service_url) + manager.connect_tx_service(network_info.transaction_service_url) else: print("No smart account connected, deploying a new one...") dev_account = get_dev_account() diff --git a/autotx/tests/api/test_send_transactions.py b/autotx/tests/api/test_send_transactions.py index 89dfa80..a42ab31 100644 --- a/autotx/tests/api/test_send_transactions.py +++ b/autotx/tests/api/test_send_transactions.py @@ -8,17 +8,82 @@ client = TestClient(app) +def test_get_intents_auth(): + response = client.get("/api/v1/tasks/123/intents") + assert response.status_code == 401 + +def test_get_transactions_auth(): + + user_id = uuid.uuid4().hex + + response = client.get("/api/v1/tasks/123/transactions", params={ + "user_id": user_id, + "address": "0x123", + "chain_id": 1, + }) + assert response.status_code == 401 + def test_send_transactions_auth(): user_id = uuid.uuid4().hex - response = client.post("/api/v1/tasks/123/transactions", json={ + response = client.post("/api/v1/tasks/123/transactions", params={ "user_id": user_id, "address": "0x123", "chain_id": 1, }) assert response.status_code == 401 + +def test_get_transactions(): + db.clear_db() + db.create_app("test", "1234") + server.setup_server(verbose=True, logs=None, max_rounds=None, cache=False, is_dev=True, check_valid_safe=False) + + user_id = uuid.uuid4().hex + smart_wallet_address = get_cached_safe_address() + + response = client.post("/api/v1/connect", json={ + "user_id": user_id, + }, headers={ + "Authorization": f"Bearer 1234" + }) + assert response.status_code == 200 + data = response.json() + + response = client.post("/api/v1/tasks", json={ + "prompt": "Send 1 ETH to vitalik.eth", + "address": smart_wallet_address, + "chain_id": 1, + "user_id": user_id + }, headers={ + "Authorization": f"Bearer 1234" + }) + assert response.status_code == 200 + data = response.json() + + task_id = data["id"] + + response = client.get(f"/api/v1/tasks/{task_id}/transactions", params={ + "user_id": user_id, + "address": smart_wallet_address, + "chain_id": 1, + }, headers={ + "Authorization": f"Bearer 1234" + }) + assert response.status_code == 200 + data = response.json() + + assert len(data) == 1 + + response = client.get(f"/api/v1/tasks/{task_id}/intents", headers={ + "Authorization": f"Bearer 1234" + }) + assert response.status_code == 200 + data = response.json() + + assert len(data) == 1 + def test_send_transactions(): db.clear_db() db.create_app("test", "1234") @@ -48,7 +113,7 @@ def test_send_transactions(): task_id = data["id"] - response = client.post(f"/api/v1/tasks/{task_id}/transactions", json={ + response = client.post(f"/api/v1/tasks/{task_id}/transactions", params={ "user_id": user_id, "address": smart_wallet_address, "chain_id": 1, diff --git a/autotx/tests/api/test_tasks.py b/autotx/tests/api/test_tasks.py index 861eb05..4cfb57f 100644 --- a/autotx/tests/api/test_tasks.py +++ b/autotx/tests/api/test_tasks.py @@ -33,10 +33,6 @@ def test_get_task_auth(): response = client.get("/api/v1/tasks/123") assert response.status_code == 401 -def test_get_task_transactions_auth(): - response = client.get("/api/v1/tasks/123/transactions") - assert response.status_code == 401 - def test_create_task(): server.setup_server(verbose=True, logs=None, max_rounds=None, cache=False, is_dev=True, check_valid_safe=False) @@ -65,7 +61,7 @@ def test_create_task(): assert "created_at" in data assert "updated_at" in data assert data["messages"] == [] - assert data["transactions"] == [] + assert data["intents"] == [] assert data["running"] is True response = client.get(f"/api/v1/tasks/{data['id']}", headers={ @@ -74,7 +70,7 @@ def test_create_task(): data = response.json() assert data["running"] is False - assert len(data["transactions"]) > 0 + assert len(data["intents"]) > 0 def test_get_tasks(): response = client.get("/api/v1/tasks", headers={ @@ -100,13 +96,13 @@ def test_get_task(): assert len(data["messages"]) > 0 assert data["running"] is False -def test_get_task_transactions(): +def test_get_task_intents(): response = client.get("/api/v1/tasks", headers={ "Authorization": f"Bearer 1234" }) task_id = response.json()[0]["id"] - response = client.get(f"/api/v1/tasks/{task_id}/transactions", headers={ + response = client.get(f"/api/v1/tasks/{task_id}/intents", headers={ "Authorization": f"Bearer 1234" }) assert response.status_code == 200 diff --git a/autotx/token.py b/autotx/token.py new file mode 100644 index 0000000..15f9da1 --- /dev/null +++ b/autotx/token.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + +class Token(BaseModel): + symbol: str + address: str \ No newline at end of file diff --git a/autotx/transactions.py b/autotx/transactions.py new file mode 100644 index 0000000..920418d --- /dev/null +++ b/autotx/transactions.py @@ -0,0 +1,67 @@ +from enum import Enum +from pydantic import BaseModel +from typing import Any, Union + +from autotx.token import Token + +class TransactionType(str, Enum): + SEND = "send" + APPROVE = "approve" + SWAP = "swap" + +class TransactionBase(BaseModel): + type: TransactionType + summary: str + params: dict[str, Any] + +class SendTransaction(TransactionBase): + receiver: str + token: Token + amount: float + + @classmethod + def create(cls, token: Token, amount: float, receiver: str, params: dict[str, Any]) -> 'SendTransaction': + return cls( + type=TransactionType.SEND, + token=token, + amount=amount, + receiver=receiver, + params=params, + summary=f"Transfer {amount} {token.symbol} to {receiver}", + ) + +class ApproveTransaction(TransactionBase): + token: Token + amount: float + spender: str + + @classmethod + def create(cls, token: Token, amount: float, spender: str, params: dict[str, Any]) -> 'ApproveTransaction': + return cls( + type=TransactionType.APPROVE, + token=token, + amount=amount, + spender=spender, + params=params, + summary=f"Approve {amount} {token.symbol} to {spender}" + ) + +class SwapTransaction(TransactionBase): + from_token: Token + to_token: Token + from_amount: float + to_amount: float + + @classmethod + def create(cls, from_token: Token, to_token: Token, from_amount: float, to_amount: float, params: dict[str, Any]) -> 'SwapTransaction': + return cls( + type=TransactionType.SWAP, + from_token=from_token, + to_token=to_token, + from_amount=from_amount, + to_amount=to_amount, + params=params, + summary=f"Swap {from_amount} {from_token.symbol} for at least {to_amount} {to_token.symbol}" + ) + +Transaction = Union[SendTransaction, ApproveTransaction, SwapTransaction] diff --git a/autotx/utils/ethereum/SafeManager.py b/autotx/utils/ethereum/SafeManager.py index eac5c9e..77177e2 100644 --- a/autotx/utils/ethereum/SafeManager.py +++ b/autotx/utils/ethereum/SafeManager.py @@ -15,11 +15,12 @@ from eth_account.signers.local import LocalAccount from autotx import models +from autotx.transactions import Transaction from autotx.utils.ethereum.get_native_balance import get_native_balance from autotx.utils.ethereum.cached_safe_address import get_cached_safe_address, save_cached_safe_address from autotx.utils.ethereum.eth_address import ETHAddress from autotx.utils.ethereum.is_valid_safe import is_valid_safe -from autotx.utils.ethereum.networks import ChainId +from autotx.utils.ethereum.networks import ChainId, NetworkInfo from .deploy_safe_with_create2 import deploy_safe_with_create2 from .deploy_multicall import deploy_multicall from .get_erc20_balance import get_erc20_balance @@ -37,7 +38,7 @@ class SafeManager: safe_nonce: int | None = None gas_multiplier: float | None = GAS_PRICE_MULTIPLIER dev_account: LocalAccount | None = None - network: ChainId | None = None + network: NetworkInfo transaction_service_url: str | None = None address: ETHAddress use_tx_service: bool @@ -54,6 +55,7 @@ def __init__( self.safe = safe self.use_tx_service = False self.safe_nonce = None + self.network = NetworkInfo(client.w3.eth.chain_id) self.address = ETHAddress(safe.address) @@ -93,14 +95,12 @@ def connect( return manager - def connect_tx_service(self, network: ChainId, transaction_service_url: str) -> None: + def connect_tx_service(self, transaction_service_url: str) -> None: self.use_tx_service = True - self.network = network self.transaction_service_url = transaction_service_url def disconnect_tx_service(self) -> None: self.use_tx_service = False - self.network = None self.transaction_service_url = None def connect_multisend(self, address: ChecksumAddress) -> None: @@ -201,11 +201,8 @@ def execute_multisend_tx(self, txs: list[TxParams | dict[str, Any]], safe_nonce: return tx_hash def post_transaction(self, tx: TxParams | dict[str, Any], safe_nonce: Optional[int] = None) -> None: - if not self.network: - raise Exception("Network not defined for transaction service") - ts_api = TransactionServiceApi( - self.network, ethereum_client=self.client, base_url=self.transaction_service_url + self.network.chain_id, ethereum_client=self.client, base_url=self.transaction_service_url ) safe_tx = self.build_tx(tx, safe_nonce, skip_estimate_gas=True) @@ -214,11 +211,8 @@ def post_transaction(self, tx: TxParams | dict[str, Any], safe_nonce: Optional[i ts_api.post_transaction(safe_tx) def post_multisend_transaction(self, txs: list[TxParams | dict[str, Any]], safe_nonce: Optional[int] = None) -> None: - if not self.network: - raise Exception("Network not defined for transaction service") - ts_api = TransactionServiceApi( - self.network, ethereum_client=self.client, base_url=self.transaction_service_url + self.network.chain_id, ethereum_client=self.client, base_url=self.transaction_service_url ) tx = self.build_multisend_tx(txs, safe_nonce) @@ -248,7 +242,7 @@ def send_multisend_tx(self, txs: list[TxParams | dict[str, Any]], safe_nonce: Op hash = self.execute_multisend_tx(txs, safe_nonce) return hash.hex() - def send_tx_batch(self, txs: list[models.Transaction], require_approval: bool, safe_nonce: Optional[int] = None) -> bool | str: # True if sent, False if declined, str if feedback + def send_tx_batch(self, txs: list[Transaction], require_approval: bool, safe_nonce: Optional[int] = None) -> bool | str: # True if sent, False if declined, str if feedback print("=" * 50) if not txs: @@ -322,7 +316,7 @@ def send_tx_batch(self, txs: list[models.Transaction], require_approval: bool, s return True - def send_multisend_tx_batch(self, txs: list[models.Transaction], require_approval: bool, safe_nonce: Optional[int] = None) -> bool | str: # True if sent, False if declined, str if feedback + def send_multisend_tx_batch(self, txs: list[Transaction], require_approval: bool, safe_nonce: Optional[int] = None) -> bool | str: # True if sent, False if declined, str if feedback print("=" * 50) if not txs: diff --git a/autotx/utils/ethereum/lifi/swap.py b/autotx/utils/ethereum/lifi/swap.py index c1fc71d..03bdd46 100644 --- a/autotx/utils/ethereum/lifi/swap.py +++ b/autotx/utils/ethereum/lifi/swap.py @@ -3,7 +3,8 @@ from typing import Any, cast from web3 import Web3 -from autotx import models +from autotx.token import Token +from autotx.transactions import ApproveTransaction, SwapTransaction, Transaction from autotx.utils.ethereum.constants import GAS_PRICE_MULTIPLIER, NATIVE_TOKEN_ADDRESS from autotx.utils.ethereum.erc20_abi import ERC20_ABI from autotx.utils.ethereum.eth_address import ETHAddress @@ -102,7 +103,6 @@ def get_quote( quote["toolDetails"]["name"], ) - def build_swap_transaction( web3: Web3, amount: Decimal, @@ -111,7 +111,7 @@ def build_swap_transaction( _from: ETHAddress, is_exact_input: bool, chain: ChainId, -) -> list[models.Transaction]: +) -> list[Transaction]: native_token_symbol = get_native_token_symbol(chain) token_in_is_native = token_in_address.hex == NATIVE_TOKEN_ADDRESS token_in = web3.eth.contract( @@ -152,7 +152,7 @@ def build_swap_transaction( _from, ) - transactions: list[models.Transaction] = [] + transactions: list[Transaction] = [] if not token_in_is_native: approval_address = quote.approval_address allowance = token_in.functions.allowance(_from.hex, approval_address).call() @@ -168,23 +168,85 @@ def build_swap_transaction( } ) transactions.append( - models.ApproveTransaction.create( - token_symbol=token_in_symbol, - token_address=str(token_in_address), + ApproveTransaction.create( + token=Token(symbol=token_in_symbol, address=str(token_in_address)), amount=float(Decimal(str(quote.amount_in)) / 10 ** token_in_decimals), spender=approval_address, params=cast(dict[str, Any], tx), ) ) transactions.append( - models.SwapTransaction.create( - from_token_symbol=token_in_symbol, - to_token_symbol=token_out_symbol, - from_token_address=str(token_in_address), - to_token_address=str(token_out_address), + SwapTransaction.create( + from_token=Token(symbol=token_in_symbol, address=str(token_in_address)), + to_token=Token(symbol=token_out_symbol, address=str(token_out_address)), from_amount=float(Decimal(str(quote.amount_in)) / 10 ** token_in_decimals), to_amount=float(Decimal(str(int(quote.to_amount_min))) / 10 ** token_out_decimals), params=cast(dict[str, Any], quote.transaction), ) ) return transactions + +def can_build_swap_transaction( + web3: Web3, + amount: Decimal, + token_in_address: ETHAddress, + token_out_address: ETHAddress, + _from: ETHAddress, + is_exact_input: bool, + chain: ChainId, +) -> bool: + native_token_symbol = get_native_token_symbol(chain) + token_in_is_native = token_in_address.hex == NATIVE_TOKEN_ADDRESS + token_in = web3.eth.contract( + address=token_in_address.hex, abi=ERC20_ABI + ) + token_in_symbol = ( + native_token_symbol + if token_in_is_native + else token_in.functions.symbol().call() + ) + token_in_decimals = ( + 18 if token_in_is_native else token_in.functions.decimals().call() + ) + + token_out_is_native = token_out_address.hex == NATIVE_TOKEN_ADDRESS + token_out = web3.eth.contract( + address=token_out_address.hex, abi=ERC20_ABI + ) + token_out_decimals = ( + 18 if token_out_is_native else token_out.functions.decimals().call() + ) + + token_out_symbol = ( + native_token_symbol + if token_out_is_native + else token_out.functions.symbol().call() + ) + quote = get_quote( + token_in_address, + token_in_decimals, + token_in_symbol, + token_out_address, + token_out_decimals, + token_out_symbol, + chain, + amount, + not is_exact_input, + _from, + ) + + if not token_in_is_native: + approval_address = quote.approval_address + allowance = token_in.functions.allowance(_from.hex, approval_address).call() + if allowance < quote.amount_in: + tx = token_in.functions.approve( + approval_address, quote.amount_in + ).build_transaction( + { + "from": _from.hex, + "gasPrice": Wei( + int(web3.eth.gas_price * GAS_PRICE_MULTIPLIER) + ), + } + ) + return True diff --git a/autotx/wallets/api_smart_wallet.py b/autotx/wallets/api_smart_wallet.py index fb05529..e2f0e40 100644 --- a/autotx/wallets/api_smart_wallet.py +++ b/autotx/wallets/api_smart_wallet.py @@ -1,7 +1,7 @@ -import uuid - from web3 import Web3 -from autotx import db, models +from autotx import db +from autotx.intents import Intent +from autotx.transactions import Transaction from autotx.utils.ethereum.SafeManager import SafeManager from autotx.wallets.smart_wallet import SmartWallet @@ -15,7 +15,7 @@ def __init__(self, web3: Web3, manager: SafeManager, tasks: db.TasksRepository, self.manager = manager self.tasks = tasks - def on_transactions_prepared(self, txs: list[models.Transaction]) -> None: + def on_intents_prepared(self, intents: list[Intent]) -> None: if self.task_id is None: raise ValueError("Task ID is required") @@ -23,12 +23,8 @@ def on_transactions_prepared(self, txs: list[models.Transaction]) -> None: if saved_task is None: raise ValueError("Task not found") - for tx in txs: - tx.id = str(uuid.uuid4()) - tx.task_id = self.task_id - - saved_task.transactions.extend(txs) + saved_task.intents.extend(intents) self.tasks.update(saved_task) - def on_transactions_ready(self, _txs: list[models.Transaction]) -> bool | str: + def on_intents_ready(self, _intents: list[Intent]) -> bool | str: return True \ No newline at end of file diff --git a/autotx/wallets/safe_smart_wallet.py b/autotx/wallets/safe_smart_wallet.py index d2ae887..8b7b147 100644 --- a/autotx/wallets/safe_smart_wallet.py +++ b/autotx/wallets/safe_smart_wallet.py @@ -1,4 +1,4 @@ -from autotx import models +from autotx.intents import Intent from autotx.utils.ethereum import SafeManager from autotx.wallets.smart_wallet import SmartWallet @@ -13,8 +13,13 @@ def __init__(self, manager: SafeManager, auto_submit_tx: bool): self.manager = manager self.auto_submit_tx = auto_submit_tx - def on_transactions_prepared(self, txs: list[models.Transaction]) -> None: + def on_intents_prepared(self, intents: list[Intent]) -> None: pass - def on_transactions_ready(self, txs: list[models.Transaction]) -> bool | str: - return self.manager.send_multisend_tx_batch(txs, not self.auto_submit_tx) + def on_intents_ready(self, intents: list[Intent]) -> bool | str: + transactions = [] + + for intent in intents: + transactions.extend(intent.build_transactions(self.manager.web3, self.manager.network, self.manager.address)) + + return self.manager.send_multisend_tx_batch(transactions, not self.auto_submit_tx) diff --git a/autotx/wallets/smart_wallet.py b/autotx/wallets/smart_wallet.py index fb63966..b29e4e4 100644 --- a/autotx/wallets/smart_wallet.py +++ b/autotx/wallets/smart_wallet.py @@ -1,7 +1,7 @@ from abc import abstractmethod from web3 import Web3 -from autotx import models +from autotx.intents import Intent from autotx.utils.ethereum.get_erc20_balance import get_erc20_balance from autotx.utils.ethereum.eth_address import ETHAddress from autotx.utils.ethereum.get_native_balance import get_native_balance @@ -12,11 +12,11 @@ def __init__(self, web3: Web3, address: ETHAddress): self.web3 = web3 self.address = address - def on_transactions_prepared(self, txs: list[models.Transaction]) -> None: + def on_intents_prepared(self, intents: list[Intent]) -> None: pass @abstractmethod - def on_transactions_ready(self, txs: list[models.Transaction]) -> bool | str: # True if sent, False if declined, str if feedback + def on_intents_ready(self, intents: list[Intent]) -> bool | str: # True if sent, False if declined, str if feedback pass def balance_of(self, token_address: ETHAddress | None = None) -> float: diff --git a/supabase/migrations/20240614104522_intents.sql b/supabase/migrations/20240614104522_intents.sql new file mode 100644 index 0000000..6fb0191 --- /dev/null +++ b/supabase/migrations/20240614104522_intents.sql @@ -0,0 +1,8 @@ +delete from "public"."submitted_batches"; +alter table "public"."submitted_batches" add column "transactions" json not null; + +delete from "public"."tasks"; +alter table "public"."tasks" drop column "transactions"; +alter table "public"."tasks" add column "intents" json not null; + + From 17f3181fb8e787e47329bf2cc99fb7d1540e46ca Mon Sep 17 00:00:00 2001 From: nerfZael Date: Fri, 14 Jun 2024 20:42:21 +0200 Subject: [PATCH 05/26] added check to make sure chain id of txs to create matches the tasks chain id --- autotx/server.py | 6 +++++ autotx/tests/api/test_send_transactions.py | 28 ++++++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/autotx/server.py b/autotx/server.py index 4097b0b..c23f06f 100644 --- a/autotx/server.py +++ b/autotx/server.py @@ -218,6 +218,9 @@ def get_transactions( task = get_task_or_404(task_id, tasks) + if task.chain_id != chain_id: + raise HTTPException(status_code=400, detail="Chain ID does not match task") + (transactions, _) = build_transactions(app.id, app_user.user_id, chain_id, address, task) return transactions @@ -236,6 +239,9 @@ def send_transactions( task = get_task_or_404(task_id, tasks) + if task.chain_id != chain_id: + raise HTTPException(status_code=400, detail="Chain ID does not match task") + (transactions, app_config) = build_transactions(app.id, app_user.user_id, chain_id, address, task) if len(transactions) == 0: diff --git a/autotx/tests/api/test_send_transactions.py b/autotx/tests/api/test_send_transactions.py index a42ab31..b6a8e35 100644 --- a/autotx/tests/api/test_send_transactions.py +++ b/autotx/tests/api/test_send_transactions.py @@ -64,6 +64,14 @@ def test_get_transactions(): task_id = data["id"] + response = client.get(f"/api/v1/tasks/{task_id}/intents", headers={ + "Authorization": f"Bearer 1234" + }) + assert response.status_code == 200 + data = response.json() + + assert len(data) == 1 + response = client.get(f"/api/v1/tasks/{task_id}/transactions", params={ "user_id": user_id, "address": smart_wallet_address, @@ -76,13 +84,14 @@ def test_get_transactions(): assert len(data) == 1 - response = client.get(f"/api/v1/tasks/{task_id}/intents", headers={ + response = client.get(f"/api/v1/tasks/{task_id}/transactions", params={ + "user_id": user_id, + "address": smart_wallet_address, + "chain_id": 2, + }, headers={ "Authorization": f"Bearer 1234" }) - assert response.status_code == 200 - data = response.json() - - assert len(data) == 1 + assert response.status_code == 400 def test_send_transactions(): db.clear_db() @@ -113,6 +122,15 @@ def test_send_transactions(): task_id = data["id"] + response = client.post(f"/api/v1/tasks/{task_id}/transactions", params={ + "user_id": user_id, + "address": smart_wallet_address, + "chain_id": 2, + }, headers={ + "Authorization": f"Bearer 1234" + }) + assert response.status_code == 400 + response = client.post(f"/api/v1/tasks/{task_id}/transactions", params={ "user_id": user_id, "address": smart_wallet_address, From cdcbb865b991630b1b53c7c8542dfef760384a27 Mon Sep 17 00:00:00 2001 From: Cesar Date: Mon, 17 Jun 2024 12:38:37 +0200 Subject: [PATCH 06/26] feat: custom model support --- autotx/AutoTx.py | 27 ++++++++++++++++++--------- autotx/autotx_agent.py | 7 +++++-- autotx/helper_agents/clarifier.py | 14 ++++++++++---- autotx/helper_agents/manager.py | 11 +++++++---- autotx/helper_agents/user_proxy.py | 12 ++++++++++-- 5 files changed, 50 insertions(+), 21 deletions(-) diff --git a/autotx/AutoTx.py b/autotx/AutoTx.py index bc73a38..50570df 100644 --- a/autotx/AutoTx.py +++ b/autotx/AutoTx.py @@ -5,8 +5,8 @@ import os from textwrap import dedent from typing import Any, Dict, Optional, Callable -from dataclasses import dataclass -from autogen import Agent as AutogenAgent +from dataclasses import dataclass, field +from autogen import Agent as AutogenAgent, ModelClient from termcolor import cprint from typing import Optional @@ -20,6 +20,11 @@ from autotx.utils.constants import OPENAI_BASE_URL, OPENAI_MODEL_NAME from autotx.wallets.smart_wallet import SmartWallet +@dataclass(kw_only=True) +class CustomModel: + client: ModelClient + arguments: Optional[Dict[str, Any]] = None + @dataclass(kw_only=True) class Config: verbose: bool @@ -27,13 +32,15 @@ class Config: log_costs: bool max_rounds: int get_llm_config: Callable[[], Optional[Dict[str, Any]]] + custom_model: Optional[CustomModel] = None - def __init__(self, verbose: bool, get_llm_config: Callable[[], Optional[Dict[str, Any]]], logs_dir: Optional[str], max_rounds: Optional[int] = None, log_costs: Optional[bool] = None): + def __init__(self, verbose: bool, get_llm_config: Callable[[], Optional[Dict[str, Any]]], logs_dir: Optional[str], max_rounds: Optional[int] = None, log_costs: Optional[bool] = None, custom_model: Optional[CustomModel] = None): self.verbose = verbose self.get_llm_config = get_llm_config self.logs_dir = logs_dir self.log_costs = log_costs if log_costs is not None else False self.max_rounds = max_rounds if max_rounds is not None else 100 + self.custom_model = custom_model @dataclass class PastRun: @@ -61,6 +68,7 @@ class AutoTx: transactions: list[models.Transaction] network: NetworkInfo get_llm_config: Callable[[], Optional[Dict[str, Any]]] + custom_model: Optional[CustomModel] agents: list[AutoTxAgent] log_costs: bool max_rounds: int @@ -96,6 +104,7 @@ def __init__( self.current_run_cost_with_cache = 0 self.info_messages = [] self.on_notify_user = on_notify_user + self.custom_model = config.custom_model def run(self, prompt: str, non_interactive: bool, summary_method: str = "last_msg") -> RunResult: return asyncio.run(self.a_run(prompt, non_interactive, summary_method)) @@ -174,22 +183,22 @@ async def try_run(self, prompt: str, non_interactive: bool, summary_method: str agents_information = self.get_agents_information(self.agents) - user_proxy_agent = user_proxy.build(prompt, agents_information, self.get_llm_config) - clarifier_agent = clarifier.build(user_proxy_agent, agents_information, not non_interactive, self.get_llm_config, self.notify_user) + user_proxy_agent = user_proxy.build(prompt, agents_information, self.get_llm_config, self.custom_model) helper_agents: list[AutogenAgent] = [ user_proxy_agent, ] if not non_interactive: + clarifier_agent = clarifier.build(user_proxy_agent, agents_information, not non_interactive, self.get_llm_config, self.notify_user, self.custom_model) helper_agents.append(clarifier_agent) - autogen_agents = [agent.build_autogen_agent(self, user_proxy_agent, self.get_llm_config()) for agent in self.agents] - - manager_agent = manager.build(autogen_agents + helper_agents, self.max_rounds, not non_interactive, self.get_llm_config) + autogen_agents = [agent.build_autogen_agent(self, user_proxy_agent, self.get_llm_config(), self.custom_model) for agent in self.agents] + manager_agent = manager.build(autogen_agents + helper_agents, self.max_rounds, not non_interactive, self.get_llm_config, self.custom_model) + recipient_agent = manager_agent if len(autogen_agents) > 1 else autogen_agents[0] chat = await user_proxy_agent.a_initiate_chat( - manager_agent, + recipient_agent, message=dedent( f""" I am currently connected with the following wallet: {self.wallet.address}, on network: {self.network.chain_id.name} diff --git a/autotx/autotx_agent.py b/autotx/autotx_agent.py index d93250a..39f9d36 100644 --- a/autotx/autotx_agent.py +++ b/autotx/autotx_agent.py @@ -4,7 +4,7 @@ from autotx.utils.color import Color if TYPE_CHECKING: from autotx.autotx_tool import AutoTxTool - from autotx.AutoTx import AutoTx + from autotx.AutoTx import AutoTx, CustomModel class AutoTxAgent(): name: str @@ -18,7 +18,7 @@ def __init__(self) -> None: f"{tool.name}: {tool.description}" for tool in self.tools ] - def build_autogen_agent(self, autotx: 'AutoTx', user_proxy: autogen.UserProxyAgent, llm_config: Optional[Dict[str, Any]]) -> autogen.Agent: + def build_autogen_agent(self, autotx: 'AutoTx', user_proxy: autogen.UserProxyAgent, llm_config: Optional[Dict[str, Any]], custom_model: Optional['CustomModel']) -> autogen.Agent: system_message = None if isinstance(self.system_message, str): system_message = self.system_message @@ -58,4 +58,7 @@ def send_message_hook( for tool in self.tools: tool.register_tool(autotx, agent, user_proxy) + if custom_model: + agent.register_model_client(model_client_cls=custom_model.client, **custom_model.arguments) + return agent \ No newline at end of file diff --git a/autotx/helper_agents/clarifier.py b/autotx/helper_agents/clarifier.py index 97c117c..0aad7f0 100644 --- a/autotx/helper_agents/clarifier.py +++ b/autotx/helper_agents/clarifier.py @@ -1,10 +1,13 @@ from textwrap import dedent -from typing import Annotated, Any, Callable, Dict, Optional -from autogen import UserProxyAgent, AssistantAgent +from typing import TYPE_CHECKING, Annotated, Any, Callable, Dict, Optional +from autogen import UserProxyAgent, AssistantAgent, ModelClient from autotx.utils.color import Color -def build(user_proxy: UserProxyAgent, agents_information: str, interactive: bool, get_llm_config: Callable[[], Optional[Dict[str, Any]]], notify_user: Callable[[str, Color | None], None]) -> AssistantAgent: +if TYPE_CHECKING: + from autotx.AutoTx import CustomModel + +def build(user_proxy: UserProxyAgent, agents_information: str, interactive: bool, get_llm_config: Callable[[], Optional[Dict[str, Any]]], notify_user: Callable[[str, Color | None], None], custom_model: Optional['CustomModel']) -> AssistantAgent: missing_1 = dedent(""" If the goal is not clear or missing information, you MUST ask for more information by calling the request_user_input tool. Always ensure you have all the information needed to define the goal that can be executed without prior context. @@ -77,5 +80,8 @@ def goal_outside_scope( clarifier_agent.register_for_llm(name="goal_outside_scope", description="Notify the user about their goal not being in the scope of the agents")(goal_outside_scope) user_proxy.register_for_execution(name="goal_outside_scope")(goal_outside_scope) - + + if custom_model: + clarifier_agent.register_model_client(model_client_cls=custom_model.client, **custom_model.arguments) + return clarifier_agent \ No newline at end of file diff --git a/autotx/helper_agents/manager.py b/autotx/helper_agents/manager.py index 7fa153f..2c1cf6d 100644 --- a/autotx/helper_agents/manager.py +++ b/autotx/helper_agents/manager.py @@ -1,10 +1,12 @@ from textwrap import dedent -from typing import Any, Callable, Dict, Optional +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional from autogen import GroupChat, GroupChatManager, Agent as AutogenAgent +if TYPE_CHECKING: + from autotx.AutoTx import CustomModel -def build(agents: list[AutogenAgent], max_rounds: int, interactive: bool, get_llm_config: Callable[[], Optional[Dict[str, Any]]]) -> AutogenAgent: +def build(agents: list[AutogenAgent], max_rounds: int, interactive: bool, get_llm_config: Callable[[], Optional[Dict[str, Any]]], custom_model: Optional['CustomModel']) -> AutogenAgent: clarifier_prompt = "ALWAYS choose the 'clarifier' role first in the conversation." if interactive else "" - + groupchat = GroupChat( agents=agents, messages=[], @@ -23,5 +25,6 @@ def build(agents: list[AutogenAgent], max_rounds: int, interactive: bool, get_ll ) ) manager = GroupChatManager(groupchat=groupchat, llm_config=get_llm_config()) - + if custom_model: + manager.register_model_client(model_client_cls=custom_model.client, **custom_model.arguments) return manager \ No newline at end of file diff --git a/autotx/helper_agents/user_proxy.py b/autotx/helper_agents/user_proxy.py index 39f40c3..d8fd6ae 100644 --- a/autotx/helper_agents/user_proxy.py +++ b/autotx/helper_agents/user_proxy.py @@ -1,8 +1,11 @@ from textwrap import dedent -from typing import Any, Callable, Dict, Optional +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional from autogen import UserProxyAgent -def build(user_prompt: str, agents_information: str, get_llm_config: Callable[[], Optional[Dict[str, Any]]]) -> UserProxyAgent: +if TYPE_CHECKING: + from autotx.AutoTx import CustomModel + +def build(user_prompt: str, agents_information: str, get_llm_config: Callable[[], Optional[Dict[str, Any]]], custom_model: Optional['CustomModel']) -> UserProxyAgent: user_proxy = UserProxyAgent( name="user_proxy", is_termination_msg=lambda x: x.get("content", "") and "TERMINATE" in x.get("content", ""), @@ -35,4 +38,9 @@ def build(user_prompt: str, agents_information: str, get_llm_config: Callable[[] llm_config=get_llm_config(), code_execution_config=False, ) + + if custom_model: + user_proxy.register_model_client(model_client_cls=custom_model.client, **custom_model.arguments) + + return user_proxy \ No newline at end of file From 2d99e6a160d6230cbdaf1c2cd2778d372ca25641 Mon Sep 17 00:00:00 2001 From: Cesar Date: Mon, 17 Jun 2024 13:21:33 +0200 Subject: [PATCH 07/26] chore: add cd for pypi deployment --- .github/workflows/cd.pypi.yaml | 32 ++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/cd.pypi.yaml diff --git a/.github/workflows/cd.pypi.yaml b/.github/workflows/cd.pypi.yaml new file mode 100644 index 0000000..f598399 --- /dev/null +++ b/.github/workflows/cd.pypi.yaml @@ -0,0 +1,32 @@ +name: ci + +on: + push: + branches: + - feat/custom-model-support + tags: + - "v*.*.*" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Python 3.10 + uses: actions/setup-python@v2 + with: + python-version: "3.10" + + - name: Install Poetry + run: curl -sSL https://install.python-poetry.org | python3 - + + - name: Install dependencies + run: poetry install + + - name: Check types + run: poetry run build-check + + - name: Release to pypi + run: poetry publish --build --username __token__ --password ${{ secrets.PYPI_ACCESS_TOKEN }} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index ccab259..0f013da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "autotx" -version = "0.1.0" +version = "0.1.1-beta.1" description = "" authors = ["Nestor Amesty "] readme = "README.md" From 8c1402b0a8b51eb2687af0e23edf98e1dbf65656 Mon Sep 17 00:00:00 2001 From: Cesar Date: Mon, 17 Jun 2024 13:24:54 +0200 Subject: [PATCH 08/26] chore: fix build --- .github/workflows/cd.pypi.yaml | 4 ++-- autotx/autotx_agent.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cd.pypi.yaml b/.github/workflows/cd.pypi.yaml index f598399..a9c2952 100644 --- a/.github/workflows/cd.pypi.yaml +++ b/.github/workflows/cd.pypi.yaml @@ -1,4 +1,4 @@ -name: ci +name: Publish to Pypi on: push: @@ -8,7 +8,7 @@ on: - "v*.*.*" jobs: - build: + Publish: runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/autotx/autotx_agent.py b/autotx/autotx_agent.py index 39f9d36..a378401 100644 --- a/autotx/autotx_agent.py +++ b/autotx/autotx_agent.py @@ -18,7 +18,7 @@ def __init__(self) -> None: f"{tool.name}: {tool.description}" for tool in self.tools ] - def build_autogen_agent(self, autotx: 'AutoTx', user_proxy: autogen.UserProxyAgent, llm_config: Optional[Dict[str, Any]], custom_model: Optional['CustomModel']) -> autogen.Agent: + def build_autogen_agent(self, autotx: 'AutoTx', user_proxy: autogen.UserProxyAgent, llm_config: Optional[Dict[str, Any]], custom_model: Optional['CustomModel'] = None) -> autogen.Agent: system_message = None if isinstance(self.system_message, str): system_message = self.system_message From 43c18aba816754768668b53cd735a9837648dd32 Mon Sep 17 00:00:00 2001 From: nerfZael Date: Mon, 17 Jun 2024 13:33:57 +0200 Subject: [PATCH 09/26] renamed txs to intents --- autotx/AutoTx.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/autotx/AutoTx.py b/autotx/AutoTx.py index 3fd8582..b5b8382 100644 --- a/autotx/AutoTx.py +++ b/autotx/AutoTx.py @@ -237,9 +237,9 @@ async def try_run(self, prompt: str, non_interactive: bool, summary_method: str return RunResult(chat.summary, chat_history, intents, EndReason.TERMINATE if is_goal_supported else EndReason.GOAL_NOT_SUPPORTED, float(chat.cost["usage_including_cached_inference"]["total_cost"]), float(chat.cost["usage_excluding_cached_inference"]["total_cost"]), self.info_messages) - def add_intents(self, txs: list[Intent]) -> None: - self.intents.extend(txs) - self.wallet.on_intents_prepared(txs) + def add_intents(self, intents: list[Intent]) -> None: + self.intents.extend(intents) + self.wallet.on_intents_prepared(intents) def notify_user(self, message: str, color: Color | None = None) -> None: if color: From 1c994c8ab1963eedd8e9748e0aed8b2b9e839081 Mon Sep 17 00:00:00 2001 From: Cesar Date: Mon, 17 Jun 2024 18:06:16 +0200 Subject: [PATCH 10/26] chore: create `LlamaClient` class --- autotx/AutoTx.py | 11 ++++- autotx/__init__.py | 3 +- autotx/utils/LlamaClient.py | 96 +++++++++++++++++++++++++++++++++++++ poetry.lock | 25 +++++++++- pyproject.toml | 3 +- 5 files changed, 133 insertions(+), 5 deletions(-) create mode 100644 autotx/utils/LlamaClient.py diff --git a/autotx/AutoTx.py b/autotx/AutoTx.py index 227d664..eca832e 100644 --- a/autotx/AutoTx.py +++ b/autotx/AutoTx.py @@ -88,6 +88,9 @@ def __init__( config: Config, on_notify_user: Callable[[str], None] | None = None ): + if len(agents) == 0: + raise Exception("Agents attribute can not be an empty list") + self.web3 = web3 self.wallet = wallet self.network = network @@ -195,9 +198,13 @@ async def try_run(self, prompt: str, non_interactive: bool, summary_method: str helper_agents.append(clarifier_agent) autogen_agents = [agent.build_autogen_agent(self, user_proxy_agent, self.get_llm_config(), self.custom_model) for agent in self.agents] - manager_agent = manager.build(autogen_agents + helper_agents, self.max_rounds, not non_interactive, self.get_llm_config, self.custom_model) - recipient_agent = manager_agent if len(autogen_agents) > 1 else autogen_agents[0] + recipient_agent = None + if len(autogen_agents) > 1: + recipient_agent = manager.build(autogen_agents + helper_agents, self.max_rounds, not non_interactive, self.get_llm_config, self.custom_model) + else: + recipient_agent = autogen_agents[0] + chat = await user_proxy_agent.a_initiate_chat( recipient_agent, message=dedent( diff --git a/autotx/__init__.py b/autotx/__init__.py index a5d7344..de3105c 100644 --- a/autotx/__init__.py +++ b/autotx/__init__.py @@ -1,5 +1,6 @@ from autotx.AutoTx import AutoTx from autotx.autotx_agent import AutoTxAgent from autotx.autotx_tool import AutoTxTool +from autotx.utils.LlamaClient import LlamaClient -__all__ = ['AutoTx', 'AutoTxAgent', 'AutoTxTool'] +__all__ = ['AutoTx', 'AutoTxAgent', 'AutoTxTool', 'LlamaClient'] diff --git a/autotx/utils/LlamaClient.py b/autotx/utils/LlamaClient.py new file mode 100644 index 0000000..8f59dc5 --- /dev/null +++ b/autotx/utils/LlamaClient.py @@ -0,0 +1,96 @@ +from types import SimpleNamespace +from typing import Any, Dict, Union, cast +from autogen import ModelClient +from llama_cpp import ( + ChatCompletion, + ChatCompletionRequestAssistantMessage, + ChatCompletionRequestFunctionMessage, + ChatCompletionRequestMessage, + ChatCompletionRequestToolMessage, + ChatCompletionResponseMessage, + Completion, + CreateChatCompletionResponse, + Llama, +) + + +class LlamaClient(ModelClient): # type: ignore + def __init__(self, _: dict[str, Any], **args: Any): + self.llm: Llama = args["llm"] + + def create(self, params: Dict[str, Any]) -> SimpleNamespace: + sanitized_messages = self._sanitize_chat_completion_messages( + cast(list[ChatCompletionRequestMessage], params.get("messages")) + ) + response = self.llm.create_chat_completion( + messages=sanitized_messages, + tools=params.get("tools"), + model=params.get("model"), + ) + + return SimpleNamespace(**{**response, "cost": "0"}) # type: ignore + + def message_retrieval( + self, response: CreateChatCompletionResponse + ) -> list[ChatCompletionResponseMessage]: + choices = response["choices"] + return [choice["message"] for choice in choices] + + def cost(self, _: Union[ChatCompletion, Completion]) -> float: + return 0.0 + + def get_usage(self, _: Union[ChatCompletion, Completion]) -> dict[str, Any]: + return { + "prompt_tokens": 0, + "completion_tokens": 0, + "total_tokens": 0, + "cost": 0, + "model": "meetkai/functionary-small-v2.4-GGUF", + } + + def _sanitize_chat_completion_messages( + self, messages: list[ChatCompletionRequestMessage] + ) -> list[ChatCompletionRequestMessage]: + sanitized_messages: list[ChatCompletionRequestMessage] = [] + + for message in messages: + if "tool_calls" in message: + function_to_call = message["tool_calls"][0] # type: ignore + sanitized_messages.append( + ChatCompletionRequestAssistantMessage( + role="assistant", + function_call=function_to_call["function"], + content=None, + ) + ) + elif "tool_call_id" in message: + id: str = cast(ChatCompletionRequestToolMessage, message)[ + "tool_call_id" + ] + + def get_tool_name(messages, id: str) -> Union[str, None]: # type: ignore + return next( + ( + message["tool_calls"][0]["function"]["name"] + for message in messages + if "tool_calls" in message + and message["tool_calls"][0]["id"] == id + ), + None, + ) + + function_name = get_tool_name(messages, id) + if function_name is None: + raise Exception(f"No tool response for this tool call with id {id}") + + sanitized_messages.append( + ChatCompletionRequestFunctionMessage( + role="function", + name=function_name, + content=cast(Union[str | None], message["content"]), + ) + ) + else: + sanitized_messages.append(message) + + return sanitized_messages diff --git a/poetry.lock b/poetry.lock index 9cd75a4..fff9099 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1355,6 +1355,28 @@ files = [ [package.dependencies] referencing = ">=0.31.0" +[[package]] +name = "llama-cpp-python" +version = "0.2.78" +description = "Python bindings for the llama.cpp library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "llama_cpp_python-0.2.78.tar.gz", hash = "sha256:3df7cfde84287faaf29675fba8939060c3ab3f0ce8db875dabf7df5d83bd8751"}, +] + +[package.dependencies] +diskcache = ">=5.6.1" +jinja2 = ">=2.11.3" +numpy = ">=1.20.0" +typing-extensions = ">=4.5.0" + +[package.extras] +all = ["llama_cpp_python[dev,server,test]"] +dev = ["black (>=23.3.0)", "httpx (>=0.24.1)", "mkdocs (>=1.4.3)", "mkdocs-material (>=9.1.18)", "mkdocstrings[python] (>=0.22.0)", "pytest (>=7.4.0)", "twine (>=4.0.2)"] +server = ["PyYAML (>=5.1)", "fastapi (>=0.100.0)", "pydantic-settings (>=2.0.1)", "sse-starlette (>=1.6.1)", "starlette-context (>=0.3.6,<0.4)", "uvicorn (>=0.22.0)"] +test = ["httpx (>=0.24.1)", "pytest (>=7.4.0)", "scipy (>=1.10)"] + [[package]] name = "lru-dict" version = "1.2.0" @@ -2336,6 +2358,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -3549,4 +3572,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.13" -content-hash = "57c468f2b88c0a5b9b152634cafef530545725cdeaa9e499c7e7b6c13cd5ce41" +content-hash = "5dfc1bc10f28a24e09c829fbdf2469dfcd0ab7dcd8c51312e7f15e8ffd02601f" diff --git a/pyproject.toml b/pyproject.toml index 0f013da..e5a5a45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "autotx" -version = "0.1.1-beta.1" +version = "0.1.1-beta.2" description = "" authors = ["Nestor Amesty "] readme = "README.md" @@ -19,6 +19,7 @@ web3 = "^6.19.0" safe-eth-py = "^5.8.0" uvicorn = "^0.29.0" supabase = "^2.5.0" +llama-cpp-python = "^0.2.78" [tool.poetry.group.dev.dependencies] mypy = "^1.8.0" From 4ae782d9c0e9f06c4f981758934a5834e8483950 Mon Sep 17 00:00:00 2001 From: Cesar Date: Mon, 17 Jun 2024 18:21:13 +0200 Subject: [PATCH 11/26] chore: fix `message_retrieval` in LlamaClient --- autotx/utils/LlamaClient.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/autotx/utils/LlamaClient.py b/autotx/utils/LlamaClient.py index 8f59dc5..b203008 100644 --- a/autotx/utils/LlamaClient.py +++ b/autotx/utils/LlamaClient.py @@ -33,7 +33,7 @@ def create(self, params: Dict[str, Any]) -> SimpleNamespace: def message_retrieval( self, response: CreateChatCompletionResponse ) -> list[ChatCompletionResponseMessage]: - choices = response["choices"] + choices = response.choices # type: ignore return [choice["message"] for choice in choices] def cost(self, _: Union[ChatCompletion, Completion]) -> float: diff --git a/pyproject.toml b/pyproject.toml index e5a5a45..e7ee3df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "autotx" -version = "0.1.1-beta.2" +version = "0.1.1-beta.3" description = "" authors = ["Nestor Amesty "] readme = "README.md" From 08cd799a1549c8ea397f01d38e2b4d9614a70b73 Mon Sep 17 00:00:00 2001 From: nerfZael Date: Mon, 17 Jun 2024 21:26:51 +0200 Subject: [PATCH 12/26] preparing transactions before submitting --- autotx/db.py | 44 ++++++- autotx/server.py | 112 ++++++++++++------ autotx/tests/api/test_send_transactions.py | 86 +++++++++++++- .../20240617165316_submitting-txs.sql | 3 + 4 files changed, 204 insertions(+), 41 deletions(-) create mode 100644 supabase/migrations/20240617165316_submitting-txs.sql diff --git a/autotx/db.py b/autotx/db.py index 8dd73f6..c4a2738 100644 --- a/autotx/db.py +++ b/autotx/db.py @@ -9,7 +9,7 @@ from supabase.lib.client_options import ClientOptions from autotx import models -from autotx.transactions import Transaction +from autotx.transactions import Transaction, TransactionBase SUPABASE_URL = os.getenv("SUPABASE_URL") SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY") @@ -224,13 +224,13 @@ def get_agent_private_key(app_id: str, user_id: str) -> str | None: return str(result.data[0]["agent_private_key"]) -def submit_transactions(app_id: str, address: str, chain_id: int, app_user_id: str, task_id: str, transactions: list[Transaction]) -> None: +def save_transactions(app_id: str, address: str, chain_id: int, app_user_id: str, task_id: str, transactions: list[Transaction]) -> str: client = get_db_client("public") txs = [json.loads(tx.json()) for tx in transactions] created_at = datetime.utcnow() - client.table("submitted_batches") \ + result = client.table("submitted_batches") \ .insert( { "app_id": app_id, @@ -243,6 +243,42 @@ def submit_transactions(app_id: str, address: str, chain_id: int, app_user_id: s } ).execute() + return result.data[0]["id"] + +def get_transactions(app_id: str, app_user_id: str, task_id: str, address: str, chain_id: str, submitted_batch_id: str) -> tuple[list[TransactionBase], str] | None: + client = get_db_client("public") + + result = client.table("submitted_batches") \ + .select("transactions, task_id") \ + .eq("app_id", app_id) \ + .eq("app_user_id", app_user_id) \ + .eq("address", address) \ + .eq("chain_id", chain_id) \ + .eq("task_id", task_id) \ + .eq("id", submitted_batch_id) \ + .execute() + + if len(result.data) == 0: + return None + + return ( + [TransactionBase(**tx) for tx in json.loads(result.data[0]["transactions"])], + result.data[0]["task_id"] + ) + +def submit_transactions(app_id: str, app_user_id: str, submitted_batch_id: str) -> None: + client = get_db_client("public") + + client.table("submitted_batches") \ + .update( + { + "submitted_on": str(datetime.utcnow()) + } + ).eq("app_id", app_id) \ + .eq("app_user_id", app_user_id) \ + .eq("id", submitted_batch_id) \ + .execute() + class SubmittedBatch(BaseModel): id: str app_id: str @@ -251,6 +287,7 @@ class SubmittedBatch(BaseModel): app_user_id: str task_id: str created_at: datetime + submitted_on: datetime | None transactions: list[dict[str, Any]] def get_submitted_batches(app_id: str, task_id: str) -> list[SubmittedBatch]: @@ -274,6 +311,7 @@ def get_submitted_batches(app_id: str, task_id: str) -> list[SubmittedBatch]: app_user_id=batch_data["app_user_id"], task_id=batch_data["task_id"], created_at=batch_data["created_at"], + submitted_on=batch_data["submitted_on"], transactions=json.loads(batch_data["transactions"]) ) ) diff --git a/autotx/server.py b/autotx/server.py index c23f06f..51485eb 100644 --- a/autotx/server.py +++ b/autotx/server.py @@ -5,6 +5,7 @@ from fastapi import APIRouter, FastAPI, BackgroundTasks, HTTPException, Header from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse +from pydantic import BaseModel import traceback from autotx import models, setup @@ -60,6 +61,43 @@ def authorize(authorization: str | None) -> models.App: return app +def load_config_for_user(app_id: str, user_id: str, address: str, chain_id: int) -> AppConfig: + agent_private_key = db.get_agent_private_key(app_id, user_id) + + if not agent_private_key: + raise HTTPException(status_code=400, detail="User not found") + + agent = Account.from_key(agent_private_key) + + app_config = AppConfig.load(smart_account_addr=address, subsidized_chain_id=chain_id, agent=agent) + + return app_config + +def authorize_app_and_user(authorization: str | None, user_id: str) -> tuple[models.App, models.AppUser]: + app = authorize(authorization) + app_user = db.get_app_user(app.id, user_id) + + if not app_user: + raise HTTPException(status_code=400, detail="User not found") + + return (app, app_user) + +def build_transactions(app_id: str, user_id: str, chain_id: int, address: str, task: models.Task) -> List[Transaction]: + if task.running: + raise HTTPException(status_code=400, detail="Task is still running") + + app_config = load_config_for_user(app_id, user_id, address, chain_id) + + if task.intents is None or len(task.intents) == 0: + return ([], app_config) + + transactions: list[Transaction] = [] + + for intent in task.intents: + transactions.extend(intent.build_transactions(app_config.web3, app_config.network_info, app_config.manager.address)) + + return transactions + @app_router.post("/api/v1/tasks", response_model=models.Task) async def create_task(task: models.TaskCreate, background_tasks: BackgroundTasks, authorization: Annotated[str | None, Header()] = None) -> models.Task: app = authorize(authorization) @@ -172,46 +210,39 @@ def get_intents(task_id: str, authorization: Annotated[str | None, Header()] = N task = get_task_or_404(task_id, tasks) return task.intents -def authorize_app_and_user(authorization: str | None, user_id: str) -> tuple[models.App, models.AppUser]: - app = authorize(authorization) - app_user = db.get_app_user(app.id, user_id) - - if not app_user: - raise HTTPException(status_code=400, detail="User not found") - - return (app, app_user) - -def build_transactions(app_id: str, user_id: str, chain_id: int, address: str, task: models.Task) -> tuple[List[Transaction], AppConfig]: - if task.running: - raise HTTPException(status_code=400, detail="Task is still running") - - agent_private_key = db.get_agent_private_key(app_id, user_id) - - if not agent_private_key: - raise HTTPException(status_code=400, detail="User not found") - - agent = Account.from_key(agent_private_key) +@app_router.get("/api/v1/tasks/{task_id}/transactions", response_model=List[Transaction]) +def get_transactions( + task_id: str, + address: str, + chain_id: int, + user_id: str, + authorization: Annotated[str | None, Header()] = None +) -> List[Transaction]: + (app, app_user) = authorize_app_and_user(authorization, user_id) - app_config = AppConfig.load(smart_account_addr=address, subsidized_chain_id=chain_id, agent=agent) + tasks = db.TasksRepository(app.id) + + task = get_task_or_404(task_id, tasks) - if task.intents is None or len(task.intents) == 0: - return ([], app_config) + if task.chain_id != chain_id: + raise HTTPException(status_code=400, detail="Chain ID does not match task") - transactions: list[Transaction] = [] + transactions = build_transactions(app.id, user_id, chain_id, address, task) - for intent in task.intents: - transactions.extend(intent.build_transactions(app_config.web3, app_config.network_info, app_config.manager.address)) + return transactions - return (transactions, app_config) +class PreparedTransactionsDto(BaseModel): + batch_id: str + transactions: List[Transaction] -@app_router.get("/api/v1/tasks/{task_id}/transactions", response_model=List[Transaction]) -def get_transactions( +@app_router.post("/api/v1/tasks/{task_id}/transactions/prepare", response_model=PreparedTransactionsDto) +def prepare_transactions( task_id: str, address: str, chain_id: int, user_id: str, authorization: Annotated[str | None, Header()] = None -) -> List[Transaction]: +) -> str: (app, app_user) = authorize_app_and_user(authorization, user_id) tasks = db.TasksRepository(app.id) @@ -220,10 +251,15 @@ def get_transactions( if task.chain_id != chain_id: raise HTTPException(status_code=400, detail="Chain ID does not match task") + + transactions = build_transactions(app.id, app_user.user_id, chain_id, address, task) + + if len(transactions) == 0: + raise HTTPException(status_code=400, detail="No transactions to send") - (transactions, _) = build_transactions(app.id, app_user.user_id, chain_id, address, task) + submitted_batch_id = db.save_transactions(app.id, address, chain_id, app_user.id, task_id, transactions) - return transactions + return PreparedTransactionsDto(batch_id=submitted_batch_id, transactions=transactions) @app_router.post("/api/v1/tasks/{task_id}/transactions") def send_transactions( @@ -231,6 +267,7 @@ def send_transactions( address: str, chain_id: int, user_id: str, + batch_id: str, authorization: Annotated[str | None, Header()] = None ) -> str: (app, app_user) = authorize_app_and_user(authorization, user_id) @@ -242,7 +279,12 @@ def send_transactions( if task.chain_id != chain_id: raise HTTPException(status_code=400, detail="Chain ID does not match task") - (transactions, app_config) = build_transactions(app.id, app_user.user_id, chain_id, address, task) + batch = db.get_transactions(app.id, app_user.id, task_id, address, chain_id, batch_id) + + if batch is None: + raise HTTPException(status_code=400, detail="Batch not found") + + (transactions, task_id) = batch if len(transactions) == 0: raise HTTPException(status_code=400, detail="No transactions to send") @@ -250,10 +292,12 @@ def send_transactions( global autotx_params if autotx_params.is_dev: print("Dev mode: skipping transaction submission") - db.submit_transactions(app.id, address, chain_id, app_user.id, task_id, transactions) + db.submit_transactions(app.id, app_user.id, batch_id) return f"https://app.safe.global/transactions/queue?safe={CHAIN_ID_TO_SHORT_NAME[str(chain_id)]}:{address}" try: + app_config = load_config_for_user(app.id, user_id, address, chain_id) + app_config.manager.send_multisend_tx_batch( transactions, require_approval=False, @@ -264,7 +308,7 @@ def send_transactions( else: raise e - db.submit_transactions(app.id, address, chain_id, app_user.id, task_id, transactions) + db.submit_transactions(app.id, app_user.id, batch_id) return f"https://app.safe.global/transactions/queue?safe={CHAIN_ID_TO_SHORT_NAME[str(chain_id)]}:{address}" diff --git a/autotx/tests/api/test_send_transactions.py b/autotx/tests/api/test_send_transactions.py index b6a8e35..19fbbbf 100644 --- a/autotx/tests/api/test_send_transactions.py +++ b/autotx/tests/api/test_send_transactions.py @@ -23,6 +23,17 @@ def test_get_transactions_auth(): }) assert response.status_code == 401 +def test_prepare_transactions_auth(): + + user_id = uuid.uuid4().hex + + response = client.post("/api/v1/tasks/123/transactions/prepare", params={ + "user_id": user_id, + "address": "0x123", + "chain_id": 1, + }) + assert response.status_code == 401 + def test_send_transactions_auth(): user_id = uuid.uuid4().hex @@ -31,6 +42,7 @@ def test_send_transactions_auth(): "user_id": user_id, "address": "0x123", "chain_id": 1, + "batch_id": "123" }) assert response.status_code == 401 @@ -122,7 +134,7 @@ def test_send_transactions(): task_id = data["id"] - response = client.post(f"/api/v1/tasks/{task_id}/transactions", params={ + response = client.post(f"/api/v1/tasks/{task_id}/transactions/prepare", params={ "user_id": user_id, "address": smart_wallet_address, "chain_id": 2, @@ -131,7 +143,17 @@ def test_send_transactions(): }) assert response.status_code == 400 - response = client.post(f"/api/v1/tasks/{task_id}/transactions", params={ + response = client.post(f"/api/v1/tasks/{task_id}/transactions/prepare", params={ + "user_id": user_id, + "address": smart_wallet_address, + "chain_id": 1, + }, headers={ + "Authorization": f"Bearer 1234" + }) + assert response.status_code == 200 + batch1 = response.json() + + response = client.post(f"/api/v1/tasks/{task_id}/transactions/prepare", params={ "user_id": user_id, "address": smart_wallet_address, "chain_id": 1, @@ -139,14 +161,70 @@ def test_send_transactions(): "Authorization": f"Bearer 1234" }) assert response.status_code == 200 + batch2 = response.json() app = db.get_app_by_api_key("1234") batches = db.get_submitted_batches(app.id, task_id) - - assert len(batches) == 1 + assert len(batches) == 2 + assert batches[0].app_id == app.id assert batches[0].address == smart_wallet_address assert batches[0].chain_id == 1 assert batches[0].task_id == task_id assert batches[0].created_at is not None + assert batches[0].submitted_on is None + + assert batches[1].app_id == app.id + assert batches[1].address == smart_wallet_address + assert batches[1].chain_id == 1 + assert batches[1].task_id == task_id + assert batches[1].created_at is not None + assert batches[1].submitted_on is None + + assert batch1["batch_id"] == batches[0].id + assert len(batch1["transactions"]) == 1 + + assert batch2["batch_id"] == batches[1].id + assert len(batch2["transactions"]) == 1 + + response = client.post(f"/api/v1/tasks/{task_id}/transactions", params={ + "user_id": user_id, + "address": smart_wallet_address, + "chain_id": 2, + "batch_id": batch1["batch_id"] + }, headers={ + "Authorization": f"Bearer 1234" + }) + assert response.status_code == 400 + + response = client.post(f"/api/v1/tasks/{task_id}/transactions", params={ + "user_id": user_id, + "address": smart_wallet_address, + "chain_id": 1, + "batch_id": batch1["batch_id"] + }, headers={ + "Authorization": f"Bearer 1234" + }) + assert response.status_code == 200 + + batches = db.get_submitted_batches(app.id, task_id) + batches = sorted(batches, key=lambda x: x.created_at) + assert len(batches) == 2 + assert batches[0].submitted_on is not None + assert batches[1].submitted_on is None + + response = client.post(f"/api/v1/tasks/{task_id}/transactions", params={ + "user_id": user_id, + "address": smart_wallet_address, + "chain_id": 1, + "batch_id": batch2["batch_id"] + }, headers={ + "Authorization": f"Bearer 1234" + }) + assert response.status_code == 200 + + batches = db.get_submitted_batches(app.id, task_id) + assert len(batches) == 2 + assert batches[0].submitted_on is not None + assert batches[1].submitted_on is not None diff --git a/supabase/migrations/20240617165316_submitting-txs.sql b/supabase/migrations/20240617165316_submitting-txs.sql new file mode 100644 index 0000000..f56fed8 --- /dev/null +++ b/supabase/migrations/20240617165316_submitting-txs.sql @@ -0,0 +1,3 @@ +alter table "public"."submitted_batches" add column "submitted_on" timestamp with time zone; + + From f0c0e7acaa0da73e828bdcd0910d508aa433b4a5 Mon Sep 17 00:00:00 2001 From: nerfZael Date: Mon, 17 Jun 2024 21:33:03 +0200 Subject: [PATCH 13/26] type fixes --- autotx/db.py | 6 +++--- autotx/server.py | 4 ++-- autotx/utils/ethereum/SafeManager.py | 7 +++---- autotx/wallets/safe_smart_wallet.py | 3 ++- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/autotx/db.py b/autotx/db.py index c4a2738..2ea56ad 100644 --- a/autotx/db.py +++ b/autotx/db.py @@ -1,7 +1,7 @@ from datetime import datetime import json import os -from typing import Any +from typing import Any, cast import uuid from pydantic import BaseModel from supabase import create_client @@ -243,9 +243,9 @@ def save_transactions(app_id: str, address: str, chain_id: int, app_user_id: str } ).execute() - return result.data[0]["id"] + return cast(str, result.data[0]["id"]) -def get_transactions(app_id: str, app_user_id: str, task_id: str, address: str, chain_id: str, submitted_batch_id: str) -> tuple[list[TransactionBase], str] | None: +def get_transactions(app_id: str, app_user_id: str, task_id: str, address: str, chain_id: int, submitted_batch_id: str) -> tuple[list[TransactionBase], str] | None: client = get_db_client("public") result = client.table("submitted_batches") \ diff --git a/autotx/server.py b/autotx/server.py index 51485eb..c0f10ab 100644 --- a/autotx/server.py +++ b/autotx/server.py @@ -89,7 +89,7 @@ def build_transactions(app_id: str, user_id: str, chain_id: int, address: str, t app_config = load_config_for_user(app_id, user_id, address, chain_id) if task.intents is None or len(task.intents) == 0: - return ([], app_config) + return [] transactions: list[Transaction] = [] @@ -242,7 +242,7 @@ def prepare_transactions( chain_id: int, user_id: str, authorization: Annotated[str | None, Header()] = None -) -> str: +) -> PreparedTransactionsDto: (app, app_user) = authorize_app_and_user(authorization, user_id) tasks = db.TasksRepository(app.id) diff --git a/autotx/utils/ethereum/SafeManager.py b/autotx/utils/ethereum/SafeManager.py index 77177e2..9072532 100644 --- a/autotx/utils/ethereum/SafeManager.py +++ b/autotx/utils/ethereum/SafeManager.py @@ -14,8 +14,7 @@ from gnosis.safe.api import TransactionServiceApi from eth_account.signers.local import LocalAccount -from autotx import models -from autotx.transactions import Transaction +from autotx.transactions import TransactionBase from autotx.utils.ethereum.get_native_balance import get_native_balance from autotx.utils.ethereum.cached_safe_address import get_cached_safe_address, save_cached_safe_address from autotx.utils.ethereum.eth_address import ETHAddress @@ -242,7 +241,7 @@ def send_multisend_tx(self, txs: list[TxParams | dict[str, Any]], safe_nonce: Op hash = self.execute_multisend_tx(txs, safe_nonce) return hash.hex() - def send_tx_batch(self, txs: list[Transaction], require_approval: bool, safe_nonce: Optional[int] = None) -> bool | str: # True if sent, False if declined, str if feedback + def send_tx_batch(self, txs: list[TransactionBase], require_approval: bool, safe_nonce: Optional[int] = None) -> bool | str: # True if sent, False if declined, str if feedback print("=" * 50) if not txs: @@ -316,7 +315,7 @@ def send_tx_batch(self, txs: list[Transaction], require_approval: bool, safe_non return True - def send_multisend_tx_batch(self, txs: list[Transaction], require_approval: bool, safe_nonce: Optional[int] = None) -> bool | str: # True if sent, False if declined, str if feedback + def send_multisend_tx_batch(self, txs: list[TransactionBase], require_approval: bool, safe_nonce: Optional[int] = None) -> bool | str: # True if sent, False if declined, str if feedback print("=" * 50) if not txs: diff --git a/autotx/wallets/safe_smart_wallet.py b/autotx/wallets/safe_smart_wallet.py index 8b7b147..c3db603 100644 --- a/autotx/wallets/safe_smart_wallet.py +++ b/autotx/wallets/safe_smart_wallet.py @@ -1,4 +1,5 @@ from autotx.intents import Intent +from autotx.transactions import TransactionBase from autotx.utils.ethereum import SafeManager from autotx.wallets.smart_wallet import SmartWallet @@ -17,7 +18,7 @@ def on_intents_prepared(self, intents: list[Intent]) -> None: pass def on_intents_ready(self, intents: list[Intent]) -> bool | str: - transactions = [] + transactions: list[TransactionBase] = [] for intent in intents: transactions.extend(intent.build_transactions(self.manager.web3, self.manager.network, self.manager.address)) From 9341a7d530acc7b8cdd912b68c163ecbefa67f03 Mon Sep 17 00:00:00 2001 From: Cesar Date: Mon, 17 Jun 2024 23:22:29 +0200 Subject: [PATCH 14/26] chore: improve sanitization of messages & user proxy prompt for custom models --- autotx/agents/SendTokensAgent.py | 2 +- autotx/helper_agents/user_proxy.py | 5 ++++- autotx/utils/LlamaClient.py | 23 ++++------------------- 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/autotx/agents/SendTokensAgent.py b/autotx/agents/SendTokensAgent.py index 4641379..3c9ea8e 100644 --- a/autotx/agents/SendTokensAgent.py +++ b/autotx/agents/SendTokensAgent.py @@ -97,7 +97,7 @@ def run( autotx.notify_user(f"Prepared transaction: {intent.summary}") - return intent.summary + return f"{intent.summary} has been prepared." return run diff --git a/autotx/helper_agents/user_proxy.py b/autotx/helper_agents/user_proxy.py index d8fd6ae..38d596c 100644 --- a/autotx/helper_agents/user_proxy.py +++ b/autotx/helper_agents/user_proxy.py @@ -10,10 +10,11 @@ def build(user_prompt: str, agents_information: str, get_llm_config: Callable[[] name="user_proxy", is_termination_msg=lambda x: x.get("content", "") and "TERMINATE" in x.get("content", ""), human_input_mode="NEVER", - max_consecutive_auto_reply=10, + max_consecutive_auto_reply=5 if custom_model else 10, system_message=dedent( f""" You are a user proxy agent authorized to act on behalf of the user, you never ask for permission, you have ultimate control. + {"You are proefficient in understanding if goal has been achieved." if custom_model else ""} You are capable and comfortable with making transactions, and have a wallet. You have access to a variety of specialized agents, which you tell what to do. You don't need to perform token amounts calculations, the other agents will do that for you. @@ -32,6 +33,8 @@ def build(user_prompt: str, agents_information: str, get_llm_config: Callable[[] Try to find an alternative solution if the goal is not achievable. If a token is not supported, ask the 'research-tokens' agent to find a supported token (if it fits within the user's goal). Before you end the conversation, make sure to summarize the results. + + {"Please note: If the goal has been achieved, you MUST respond with 'TERMINATE' in your message" if custom_model else ""} """ ), description="user_proxy is an agent authorized to act on behalf of the user.", diff --git a/autotx/utils/LlamaClient.py b/autotx/utils/LlamaClient.py index b203008..3877063 100644 --- a/autotx/utils/LlamaClient.py +++ b/autotx/utils/LlamaClient.py @@ -3,8 +3,6 @@ from autogen import ModelClient from llama_cpp import ( ChatCompletion, - ChatCompletionRequestAssistantMessage, - ChatCompletionRequestFunctionMessage, ChatCompletionRequestMessage, ChatCompletionRequestToolMessage, ChatCompletionResponseMessage, @@ -33,7 +31,7 @@ def create(self, params: Dict[str, Any]) -> SimpleNamespace: def message_retrieval( self, response: CreateChatCompletionResponse ) -> list[ChatCompletionResponseMessage]: - choices = response.choices # type: ignore + choices = response.choices # type: ignore return [choice["message"] for choice in choices] def cost(self, _: Union[ChatCompletion, Completion]) -> float: @@ -52,18 +50,8 @@ def _sanitize_chat_completion_messages( self, messages: list[ChatCompletionRequestMessage] ) -> list[ChatCompletionRequestMessage]: sanitized_messages: list[ChatCompletionRequestMessage] = [] - for message in messages: - if "tool_calls" in message: - function_to_call = message["tool_calls"][0] # type: ignore - sanitized_messages.append( - ChatCompletionRequestAssistantMessage( - role="assistant", - function_call=function_to_call["function"], - content=None, - ) - ) - elif "tool_call_id" in message: + if "tool_call_id" in message: id: str = cast(ChatCompletionRequestToolMessage, message)[ "tool_call_id" ] @@ -84,12 +72,9 @@ def get_tool_name(messages, id: str) -> Union[str, None]: # type: ignore raise Exception(f"No tool response for this tool call with id {id}") sanitized_messages.append( - ChatCompletionRequestFunctionMessage( - role="function", - name=function_name, - content=cast(Union[str | None], message["content"]), - ) + ChatCompletionRequestToolMessage(**message, name=function_name) ) + else: sanitized_messages.append(message) From 81d14d4479528db4cddce9f0cffb0799f8c69962 Mon Sep 17 00:00:00 2001 From: Cesar Date: Tue, 18 Jun 2024 00:07:00 +0200 Subject: [PATCH 15/26] chore: add default auto reply to user proxy if custom model is defined --- autotx/helper_agents/user_proxy.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/autotx/helper_agents/user_proxy.py b/autotx/helper_agents/user_proxy.py index 38d596c..c3cfd98 100644 --- a/autotx/helper_agents/user_proxy.py +++ b/autotx/helper_agents/user_proxy.py @@ -10,11 +10,11 @@ def build(user_prompt: str, agents_information: str, get_llm_config: Callable[[] name="user_proxy", is_termination_msg=lambda x: x.get("content", "") and "TERMINATE" in x.get("content", ""), human_input_mode="NEVER", - max_consecutive_auto_reply=5 if custom_model else 10, + max_consecutive_auto_reply=10, + default_auto_reply="TERMINATE" if custom_model else None, system_message=dedent( f""" You are a user proxy agent authorized to act on behalf of the user, you never ask for permission, you have ultimate control. - {"You are proefficient in understanding if goal has been achieved." if custom_model else ""} You are capable and comfortable with making transactions, and have a wallet. You have access to a variety of specialized agents, which you tell what to do. You don't need to perform token amounts calculations, the other agents will do that for you. @@ -25,7 +25,7 @@ def build(user_prompt: str, agents_information: str, get_llm_config: Callable[[] NEVER ask the user questions. NEVER make up a token, ALWAYS ask the 'research-tokens' agent to first search for the token. - If the goal has been achieved, FIRST reflect on the goal and make sure nothing is missing, then end the conversation with 'TERMINATE' (it MUST be upper case and in the same message). + If the goal has been achieved, end the conversation with 'TERMINATE' (it MUST be upper case and in the same message). Consider the goal met if the other agents have prepared the necessary transactions and all user queries have been answered. If the user's goal involves buying tokens, make sure the correct number of tokens are bought. For buying tokens, you can use the 'swap-tokens' agent. @@ -33,8 +33,6 @@ def build(user_prompt: str, agents_information: str, get_llm_config: Callable[[] Try to find an alternative solution if the goal is not achievable. If a token is not supported, ask the 'research-tokens' agent to find a supported token (if it fits within the user's goal). Before you end the conversation, make sure to summarize the results. - - {"Please note: If the goal has been achieved, you MUST respond with 'TERMINATE' in your message" if custom_model else ""} """ ), description="user_proxy is an agent authorized to act on behalf of the user.", From 90a1fa80621ee8e0ed3dc06c1084e4f4e44fdb17 Mon Sep 17 00:00:00 2001 From: Cesar Date: Tue, 18 Jun 2024 01:21:28 +0200 Subject: [PATCH 16/26] chore: `max_consecutive_auto_reply` set to 4 with custom model --- autotx/helper_agents/user_proxy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/autotx/helper_agents/user_proxy.py b/autotx/helper_agents/user_proxy.py index c3cfd98..062a25c 100644 --- a/autotx/helper_agents/user_proxy.py +++ b/autotx/helper_agents/user_proxy.py @@ -10,8 +10,7 @@ def build(user_prompt: str, agents_information: str, get_llm_config: Callable[[] name="user_proxy", is_termination_msg=lambda x: x.get("content", "") and "TERMINATE" in x.get("content", ""), human_input_mode="NEVER", - max_consecutive_auto_reply=10, - default_auto_reply="TERMINATE" if custom_model else None, + max_consecutive_auto_reply=4 if custom_model else 10, system_message=dedent( f""" You are a user proxy agent authorized to act on behalf of the user, you never ask for permission, you have ultimate control. From 1e4ff827f4b4e19a8e3adfff023423a2c94ac074 Mon Sep 17 00:00:00 2001 From: Cesar Date: Tue, 18 Jun 2024 01:22:08 +0200 Subject: [PATCH 17/26] chore: update version to `0.1.1-beta.4` --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e7ee3df..178462a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "autotx" -version = "0.1.1-beta.3" +version = "0.1.1-beta.4" description = "" authors = ["Nestor Amesty "] readme = "README.md" From 6dc4ca1f9534f23e9b8323347bdc5d2e668529eb Mon Sep 17 00:00:00 2001 From: Cesar Date: Tue, 18 Jun 2024 01:26:00 +0200 Subject: [PATCH 18/26] chore: fix biuld --- autotx/utils/LlamaClient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotx/utils/LlamaClient.py b/autotx/utils/LlamaClient.py index 3877063..d73d108 100644 --- a/autotx/utils/LlamaClient.py +++ b/autotx/utils/LlamaClient.py @@ -72,7 +72,7 @@ def get_tool_name(messages, id: str) -> Union[str, None]: # type: ignore raise Exception(f"No tool response for this tool call with id {id}") sanitized_messages.append( - ChatCompletionRequestToolMessage(**message, name=function_name) + ChatCompletionRequestToolMessage(**message, name=function_name) # type: ignore ) else: From 4a37c0cd1a3dad6147ce3b0f8b8ce8c75e0f05ca Mon Sep 17 00:00:00 2001 From: Cesar Date: Tue, 18 Jun 2024 01:32:32 +0200 Subject: [PATCH 19/26] chore: add back change from user proxy prompt --- .github/workflows/cd.pypi.yaml | 2 -- autotx/helper_agents/user_proxy.py | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/cd.pypi.yaml b/.github/workflows/cd.pypi.yaml index a9c2952..1699058 100644 --- a/.github/workflows/cd.pypi.yaml +++ b/.github/workflows/cd.pypi.yaml @@ -2,8 +2,6 @@ name: Publish to Pypi on: push: - branches: - - feat/custom-model-support tags: - "v*.*.*" diff --git a/autotx/helper_agents/user_proxy.py b/autotx/helper_agents/user_proxy.py index 062a25c..607eb0f 100644 --- a/autotx/helper_agents/user_proxy.py +++ b/autotx/helper_agents/user_proxy.py @@ -24,8 +24,7 @@ def build(user_prompt: str, agents_information: str, get_llm_config: Callable[[] NEVER ask the user questions. NEVER make up a token, ALWAYS ask the 'research-tokens' agent to first search for the token. - If the goal has been achieved, end the conversation with 'TERMINATE' (it MUST be upper case and in the same message). - Consider the goal met if the other agents have prepared the necessary transactions and all user queries have been answered. + If the goal has been achieved, FIRST reflect on the goal and make sure nothing is missing, then end the conversation with 'TERMINATE' (it MUST be upper case and in the same message). Consider the goal met if the other agents have prepared the necessary transactions and all user queries have been answered. If the user's goal involves buying tokens, make sure the correct number of tokens are bought. For buying tokens, you can use the 'swap-tokens' agent. If you encounter an error, try to resolve it (either yourself of with other agents) and only respond with 'TERMINATE' if the goal is truly not achievable. From 041f20839d55a4fd735e9526b9415fc85a109f08 Mon Sep 17 00:00:00 2001 From: nerfZael Date: Tue, 18 Jun 2024 13:49:40 +0200 Subject: [PATCH 20/26] deserialize intent and transaction amount fixes --- autotx/db.py | 39 ++++++++++++++++++++++++++++++++++- autotx/intents.py | 12 ++++++----- autotx/transactions.py | 7 ++++--- autotx/utils/format_amount.py | 5 +++++ 4 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 autotx/utils/format_amount.py diff --git a/autotx/db.py b/autotx/db.py index 2ea56ad..ed20153 100644 --- a/autotx/db.py +++ b/autotx/db.py @@ -9,6 +9,8 @@ from supabase.lib.client_options import ClientOptions from autotx import models +from autotx.intents import BuyIntent, Intent, SellIntent, SendIntent +from autotx.token import Token from autotx.transactions import Transaction, TransactionBase SUPABASE_URL = os.getenv("SUPABASE_URL") @@ -110,6 +112,41 @@ def get(self, task_id: str) -> models.Task | None: task_data = result.data[0] + def load_intent(intent_data: dict[str, Any]) -> Intent: + if intent_data["type"] == "send": + return SendIntent.create( + to_address=intent_data["to_address"], + token=Token( + symbol=intent_data["token"]["symbol"], + address=intent_data["token"]["address"] + ), + amount=intent_data["amount"] + ) + elif intent_data["type"] == "buy": + return BuyIntent.create( + from_token=Token( + symbol=intent_data["from_token"]["symbol"], + address=intent_data["from_token"]["address"] + ), + to_token=Token( + symbol=intent_data["to_token"]["symbol"], + address=intent_data["to_token"]["address"] + ), + amount=intent_data["amount"] + ) + elif intent_data["type"] == "sell": + return SellIntent.create( + from_token=Token( + symbol=intent_data["from_token"]["symbol"], + address=intent_data["from_token"]["address"] + ), + to_token=Token( + symbol=intent_data["to_token"]["symbol"], + address=intent_data["to_token"]["address"] + ), + amount=intent_data["amount"] + ) + return models.Task( id=task_data["id"], prompt=task_data["prompt"], @@ -120,7 +157,7 @@ def get(self, task_id: str) -> models.Task | None: running=task_data["running"], error=task_data["error"], messages=json.loads(task_data["messages"]), - intents=json.loads(task_data["intents"]) + intents=[load_intent(intent) for intent in json.loads(task_data["intents"])] ) def get_all(self) -> list[models.Task]: diff --git a/autotx/intents.py b/autotx/intents.py index 986c6da..a92b19e 100644 --- a/autotx/intents.py +++ b/autotx/intents.py @@ -15,6 +15,7 @@ from autotx.utils.ethereum.eth_address import ETHAddress from autotx.utils.ethereum.lifi.swap import build_swap_transaction from autotx.utils.ethereum.networks import NetworkInfo +from autotx.utils.format_amount import format_amount class IntentType(str, Enum): SEND = "send" @@ -41,7 +42,7 @@ def create(cls, token: Token, amount: float, receiver: ETHAddress) -> 'SendInten token=token, amount=amount, receiver=receiver.hex, - summary=f"Transfer {amount} {token.symbol} to {receiver}", + summary=f"Transfer {format_amount(amount)} {token.symbol} to {receiver}", ) def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: @@ -75,13 +76,13 @@ def create(cls, from_token: Token, to_token: Token, amount: float) -> 'BuyIntent from_token=from_token, to_token=to_token, amount=amount, - summary=f"Buy {amount} {to_token.symbol} with {from_token.symbol}", + summary=f"Buy {format_amount(amount)} {to_token.symbol} with {from_token.symbol}", ) def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: transactions = build_swap_transaction( web3, - Decimal(self.amount), + Decimal(str(self.amount)), ETHAddress(self.from_token.address), ETHAddress(self.to_token.address), smart_wallet_address, @@ -103,13 +104,14 @@ def create(cls, from_token: Token, to_token: Token, amount: float) -> 'SellInten from_token=from_token, to_token=to_token, amount=amount, - summary=f"Sell {amount} {from_token.symbol} for {to_token.symbol}", + summary=f"Sell {format_amount(amount)} {from_token.symbol} for {to_token.symbol}", ) def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_address: ETHAddress) -> list[Transaction]: + transactions = build_swap_transaction( web3, - Decimal(self.amount), + Decimal(str(self.amount)), ETHAddress(self.from_token.address), ETHAddress(self.to_token.address), smart_wallet_address, diff --git a/autotx/transactions.py b/autotx/transactions.py index 920418d..eb42bec 100644 --- a/autotx/transactions.py +++ b/autotx/transactions.py @@ -3,6 +3,7 @@ from typing import Any, Union from autotx.token import Token +from autotx.utils.format_amount import format_amount class TransactionType(str, Enum): SEND = "send" @@ -27,7 +28,7 @@ def create(cls, token: Token, amount: float, receiver: str, params: dict[str, An amount=amount, receiver=receiver, params=params, - summary=f"Transfer {amount} {token.symbol} to {receiver}", + summary=f"Transfer {format_amount(amount)} {token.symbol} to {receiver}", ) class ApproveTransaction(TransactionBase): @@ -43,7 +44,7 @@ def create(cls, token: Token, amount: float, spender: str, params: dict[str, Any amount=amount, spender=spender, params=params, - summary=f"Approve {amount} {token.symbol} to {spender}" + summary=f"Approve {format_amount(amount)} {token.symbol} to {spender}" ) class SwapTransaction(TransactionBase): @@ -61,7 +62,7 @@ def create(cls, from_token: Token, to_token: Token, from_amount: float, to_amoun from_amount=from_amount, to_amount=to_amount, params=params, - summary=f"Swap {from_amount} {from_token.symbol} for at least {to_amount} {to_token.symbol}" + summary=f"Swap {format_amount(from_amount)} {from_token.symbol} for at least {format_amount(to_amount)} {to_token.symbol}" ) Transaction = Union[SendTransaction, ApproveTransaction, SwapTransaction] diff --git a/autotx/utils/format_amount.py b/autotx/utils/format_amount.py new file mode 100644 index 0000000..5740855 --- /dev/null +++ b/autotx/utils/format_amount.py @@ -0,0 +1,5 @@ +from decimal import Decimal + + +def format_amount(amount: float) -> str: + return format(Decimal(str(amount)), "f") \ No newline at end of file From 9dccf7375b982fb95245ab36f836be5139dce60b Mon Sep 17 00:00:00 2001 From: nerfZael Date: Tue, 18 Jun 2024 13:53:55 +0200 Subject: [PATCH 21/26] type fixes --- autotx/db.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/autotx/db.py b/autotx/db.py index ed20153..4bd08b4 100644 --- a/autotx/db.py +++ b/autotx/db.py @@ -115,7 +115,7 @@ def get(self, task_id: str) -> models.Task | None: def load_intent(intent_data: dict[str, Any]) -> Intent: if intent_data["type"] == "send": return SendIntent.create( - to_address=intent_data["to_address"], + receiver=intent_data["to_address"], token=Token( symbol=intent_data["token"]["symbol"], address=intent_data["token"]["address"] @@ -146,6 +146,8 @@ def load_intent(intent_data: dict[str, Any]) -> Intent: ), amount=intent_data["amount"] ) + else: + raise Exception(f"Unknown intent type: {intent_data['type']}") return models.Task( id=task_data["id"], From aca7a32fa8ef81fa11484a06d7ac4c129c2b26f5 Mon Sep 17 00:00:00 2001 From: nerfZael Date: Tue, 18 Jun 2024 14:51:10 +0200 Subject: [PATCH 22/26] fixed issue with deserializing intents --- autotx/db.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/autotx/db.py b/autotx/db.py index 4bd08b4..10184d6 100644 --- a/autotx/db.py +++ b/autotx/db.py @@ -12,6 +12,7 @@ from autotx.intents import BuyIntent, Intent, SellIntent, SendIntent from autotx.token import Token from autotx.transactions import Transaction, TransactionBase +from autotx.utils.ethereum.eth_address import ETHAddress SUPABASE_URL = os.getenv("SUPABASE_URL") SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY") @@ -115,7 +116,7 @@ def get(self, task_id: str) -> models.Task | None: def load_intent(intent_data: dict[str, Any]) -> Intent: if intent_data["type"] == "send": return SendIntent.create( - receiver=intent_data["to_address"], + receiver=ETHAddress(intent_data["receiver"]), token=Token( symbol=intent_data["token"]["symbol"], address=intent_data["token"]["address"] From bc8ac4e978aeaee822075af1a654e396416acc27 Mon Sep 17 00:00:00 2001 From: Cesar Date: Tue, 18 Jun 2024 22:58:02 +0200 Subject: [PATCH 23/26] chore: show llm model based on llm config --- autotx/AutoTx.py | 11 +++++++++-- autotx/utils/LlamaClient.py | 6 ++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/autotx/AutoTx.py b/autotx/AutoTx.py index eca832e..72b6f8e 100644 --- a/autotx/AutoTx.py +++ b/autotx/AutoTx.py @@ -119,8 +119,15 @@ async def a_run(self, prompt: str, non_interactive: bool, summary_method: str = info_messages = [] if self.verbose: - print(f"LLM model: {OPENAI_MODEL_NAME}") - print(f"LLM API URL: {OPENAI_BASE_URL}") + available_config = self.get_llm_config() + if "config_list" in available_config: + print("Available LLM configurations:") + for config in available_config["config_list"]: + if "model" in config: + print(f"LLM model: {config['model']}") + if "base_url" in config: + print(f"LLM API URL: {config['base_url']}") + print("==" * 10) while True: result = await self.try_run(prompt, non_interactive, summary_method) diff --git a/autotx/utils/LlamaClient.py b/autotx/utils/LlamaClient.py index d73d108..8150cd2 100644 --- a/autotx/utils/LlamaClient.py +++ b/autotx/utils/LlamaClient.py @@ -1,3 +1,4 @@ +import logging from types import SimpleNamespace from typing import Any, Dict, Union, cast from autogen import ModelClient @@ -13,8 +14,9 @@ class LlamaClient(ModelClient): # type: ignore - def __init__(self, _: dict[str, Any], **args: Any): + def __init__(self, config: dict[str, Any], **args: Any): self.llm: Llama = args["llm"] + self.model: str = config["model"] def create(self, params: Dict[str, Any]) -> SimpleNamespace: sanitized_messages = self._sanitize_chat_completion_messages( @@ -43,7 +45,7 @@ def get_usage(self, _: Union[ChatCompletion, Completion]) -> dict[str, Any]: "completion_tokens": 0, "total_tokens": 0, "cost": 0, - "model": "meetkai/functionary-small-v2.4-GGUF", + "model": self.model, } def _sanitize_chat_completion_messages( From 83417463d47da737bcf40f3be5c47274275b0e57 Mon Sep 17 00:00:00 2001 From: Cesar Date: Tue, 18 Jun 2024 23:32:43 +0200 Subject: [PATCH 24/26] fix: build script --- autotx/AutoTx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autotx/AutoTx.py b/autotx/AutoTx.py index 72b6f8e..94a11ce 100644 --- a/autotx/AutoTx.py +++ b/autotx/AutoTx.py @@ -120,7 +120,7 @@ async def a_run(self, prompt: str, non_interactive: bool, summary_method: str = if self.verbose: available_config = self.get_llm_config() - if "config_list" in available_config: + if available_config and "config_list" in available_config: print("Available LLM configurations:") for config in available_config["config_list"]: if "model" in config: From bfac4c54b6ce6095c8d32d728cfecded371cc551 Mon Sep 17 00:00:00 2001 From: nerfZael Date: Wed, 19 Jun 2024 00:09:42 +0200 Subject: [PATCH 25/26] ens address fixes in intents and transactions --- autotx/agents/SendTokensAgent.py | 2 +- autotx/agents/SwapTokensAgent.py | 2 +- autotx/db.py | 2 +- autotx/{utils/ethereum => }/eth_address.py | 2 ++ autotx/intents.py | 6 +++--- autotx/setup.py | 2 +- .../tests/agents/regression/token/test_tokens_regression.py | 2 +- autotx/tests/agents/token/research/test_advanced.py | 2 +- autotx/tests/agents/token/research/test_research.py | 2 +- autotx/tests/agents/token/test_swap.py | 2 +- autotx/tests/agents/token/test_swap_and_send.py | 2 +- autotx/tests/conftest.py | 2 +- autotx/tests/integration/test_swap.py | 2 +- autotx/transactions.py | 5 +++-- autotx/utils/configuration.py | 2 +- autotx/utils/ethereum/SafeManager.py | 4 ++-- autotx/utils/ethereum/build_approve_erc20.py | 2 +- autotx/utils/ethereum/build_transfer_erc20.py | 2 +- autotx/utils/ethereum/build_transfer_native.py | 2 +- autotx/utils/ethereum/deploy_multicall.py | 2 +- autotx/utils/ethereum/get_erc20_balance.py | 2 +- autotx/utils/ethereum/get_erc20_info.py | 2 +- autotx/utils/ethereum/get_native_balance.py | 2 +- .../utils/ethereum/helpers/fill_dev_account_with_tokens.py | 2 +- autotx/utils/ethereum/helpers/show_address_balances.py | 2 +- autotx/utils/ethereum/helpers/swap_from_eoa.py | 2 +- autotx/utils/ethereum/is_valid_safe.py | 2 +- autotx/utils/ethereum/lifi/__init__.py | 2 +- autotx/utils/ethereum/lifi/swap.py | 2 +- autotx/utils/ethereum/send_native.py | 2 +- autotx/utils/ethereum/transfer_erc20.py | 2 +- autotx/wallets/smart_wallet.py | 2 +- 32 files changed, 38 insertions(+), 35 deletions(-) rename autotx/{utils/ethereum => }/eth_address.py (93%) diff --git a/autotx/agents/SendTokensAgent.py b/autotx/agents/SendTokensAgent.py index 4641379..b915aa2 100644 --- a/autotx/agents/SendTokensAgent.py +++ b/autotx/agents/SendTokensAgent.py @@ -11,7 +11,7 @@ get_erc20_balance, ) from autotx.utils.ethereum.constants import NATIVE_TOKEN_ADDRESS -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.get_native_balance import get_native_balance from web3.types import TxParams diff --git a/autotx/agents/SwapTokensAgent.py b/autotx/agents/SwapTokensAgent.py index 06e58b9..1cd4cdb 100644 --- a/autotx/agents/SwapTokensAgent.py +++ b/autotx/agents/SwapTokensAgent.py @@ -6,7 +6,7 @@ from autotx.autotx_tool import AutoTxTool from autotx.intents import BuyIntent, Intent, SellIntent from autotx.token import Token -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.lifi.swap import SUPPORTED_NETWORKS_BY_LIFI, can_build_swap_transaction from autotx.utils.ethereum.networks import NetworkInfo from gnosis.eth import EthereumNetworkNotSupported as ChainIdNotSupported diff --git a/autotx/db.py b/autotx/db.py index 10184d6..3cade09 100644 --- a/autotx/db.py +++ b/autotx/db.py @@ -12,7 +12,7 @@ from autotx.intents import BuyIntent, Intent, SellIntent, SendIntent from autotx.token import Token from autotx.transactions import Transaction, TransactionBase -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress SUPABASE_URL = os.getenv("SUPABASE_URL") SUPABASE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY") diff --git a/autotx/utils/ethereum/eth_address.py b/autotx/eth_address.py similarity index 93% rename from autotx/utils/ethereum/eth_address.py rename to autotx/eth_address.py index fe2d01d..a8eebb4 100644 --- a/autotx/utils/ethereum/eth_address.py +++ b/autotx/eth_address.py @@ -6,8 +6,10 @@ class ETHAddress: hex: ChecksumAddress ens_domain: str | None + original_str: str def __init__(self, hex_or_ens: str): + self.original_str = hex_or_ens if hex_or_ens.endswith(".eth"): web3 = Web3(HTTPProvider(MAINNET_DEFAULT_RPC)) address = web3.ens.address(hex_or_ens) # type: ignore diff --git a/autotx/intents.py b/autotx/intents.py index a92b19e..69ee59a 100644 --- a/autotx/intents.py +++ b/autotx/intents.py @@ -12,7 +12,7 @@ from autotx.utils.ethereum.build_transfer_erc20 import build_transfer_erc20 from autotx.utils.ethereum.build_transfer_native import build_transfer_native from autotx.utils.ethereum.constants import NATIVE_TOKEN_ADDRESS -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.lifi.swap import build_swap_transaction from autotx.utils.ethereum.networks import NetworkInfo from autotx.utils.format_amount import format_amount @@ -41,7 +41,7 @@ def create(cls, token: Token, amount: float, receiver: ETHAddress) -> 'SendInten type=IntentType.SEND, token=token, amount=amount, - receiver=receiver.hex, + receiver=receiver.original_str, summary=f"Transfer {format_amount(amount)} {token.symbol} to {receiver}", ) @@ -57,7 +57,7 @@ def build_transactions(self, web3: Web3, network: NetworkInfo, smart_wallet_addr SendTransaction.create( token=self.token, amount=self.amount, - receiver=self.receiver, + receiver=ETHAddress(self.receiver), params=cast(dict[str, Any], tx), ) ] diff --git a/autotx/setup.py b/autotx/setup.py index f84526c..5bd71af 100644 --- a/autotx/setup.py +++ b/autotx/setup.py @@ -11,7 +11,7 @@ from autotx.utils.constants import COINGECKO_API_KEY, OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL_NAME from autotx.utils.ethereum import SafeManager from autotx.utils.ethereum.cached_safe_address import get_cached_safe_address -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.helpers.fill_dev_account_with_tokens import fill_dev_account_with_tokens from autotx.utils.ethereum.helpers.get_dev_account import get_dev_account from autotx.utils.ethereum.helpers.show_address_balances import show_address_balances diff --git a/autotx/tests/agents/regression/token/test_tokens_regression.py b/autotx/tests/agents/regression/token/test_tokens_regression.py index bdead21..d000b8e 100644 --- a/autotx/tests/agents/regression/token/test_tokens_regression.py +++ b/autotx/tests/agents/regression/token/test_tokens_regression.py @@ -1,7 +1,7 @@ import pytest from autotx.utils.ethereum import get_erc20_balance from autotx.utils.ethereum.networks import NetworkInfo -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress @pytest.mark.skip() diff --git a/autotx/tests/agents/token/research/test_advanced.py b/autotx/tests/agents/token/research/test_advanced.py index c78f644..0b72746 100644 --- a/autotx/tests/agents/token/research/test_advanced.py +++ b/autotx/tests/agents/token/research/test_advanced.py @@ -1,5 +1,5 @@ from autotx.tests.agents.token.research.test_research import get_top_token_addresses_by_market_cap -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress def test_research_and_swap_many_tokens_subjective_simple(configuration, auto_tx): (_, _, _, manager, _) = configuration diff --git a/autotx/tests/agents/token/research/test_research.py b/autotx/tests/agents/token/research/test_research.py index 5b36f00..251cc70 100644 --- a/autotx/tests/agents/token/research/test_research.py +++ b/autotx/tests/agents/token/research/test_research.py @@ -3,7 +3,7 @@ get_coingecko, ) -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.networks import ChainId def get_top_token_addresses_by_market_cap(category: str, network: str, count: int, auto_tx) -> list[ETHAddress]: diff --git a/autotx/tests/agents/token/test_swap.py b/autotx/tests/agents/token/test_swap.py index 6cdea3a..9894c27 100644 --- a/autotx/tests/agents/token/test_swap.py +++ b/autotx/tests/agents/token/test_swap.py @@ -1,5 +1,5 @@ from autotx.utils.ethereum.networks import NetworkInfo -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress DIFFERENCE_PERCENTAGE = 1.01 diff --git a/autotx/tests/agents/token/test_swap_and_send.py b/autotx/tests/agents/token/test_swap_and_send.py index c09a51d..d69aaf7 100644 --- a/autotx/tests/agents/token/test_swap_and_send.py +++ b/autotx/tests/agents/token/test_swap_and_send.py @@ -1,6 +1,6 @@ from autotx.utils.ethereum import get_erc20_balance, get_native_balance from autotx.utils.ethereum.networks import NetworkInfo -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress DIFFERENCE_PERCENTAGE = 1.01 diff --git a/autotx/tests/conftest.py b/autotx/tests/conftest.py index 17e7cdc..d416ac0 100644 --- a/autotx/tests/conftest.py +++ b/autotx/tests/conftest.py @@ -14,7 +14,7 @@ from autotx.utils.constants import OPENAI_API_KEY, OPENAI_MODEL_NAME from autotx.utils.ethereum.networks import NetworkInfo -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.helpers.get_dev_account import get_dev_account import pytest diff --git a/autotx/tests/integration/test_swap.py b/autotx/tests/integration/test_swap.py index c0b8b08..32d5b38 100644 --- a/autotx/tests/integration/test_swap.py +++ b/autotx/tests/integration/test_swap.py @@ -1,5 +1,5 @@ from decimal import Decimal -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.lifi.swap import build_swap_transaction from autotx.utils.ethereum.networks import NetworkInfo diff --git a/autotx/transactions.py b/autotx/transactions.py index eb42bec..309866f 100644 --- a/autotx/transactions.py +++ b/autotx/transactions.py @@ -3,6 +3,7 @@ from typing import Any, Union from autotx.token import Token +from autotx.eth_address import ETHAddress from autotx.utils.format_amount import format_amount class TransactionType(str, Enum): @@ -21,12 +22,12 @@ class SendTransaction(TransactionBase): amount: float @classmethod - def create(cls, token: Token, amount: float, receiver: str, params: dict[str, Any]) -> 'SendTransaction': + def create(cls, token: Token, amount: float, receiver: ETHAddress, params: dict[str, Any]) -> 'SendTransaction': return cls( type=TransactionType.SEND, token=token, amount=amount, - receiver=receiver, + receiver=receiver.original_str, params=params, summary=f"Transfer {format_amount(amount)} {token.symbol} to {receiver}", ) diff --git a/autotx/utils/configuration.py b/autotx/utils/configuration.py index 1fd6173..60bd533 100644 --- a/autotx/utils/configuration.py +++ b/autotx/utils/configuration.py @@ -12,7 +12,7 @@ from autotx.utils.ethereum import SafeManager from autotx.utils.ethereum.agent_account import get_or_create_agent_account from autotx.utils.ethereum.constants import DEVNET_RPC_URL -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.networks import NetworkInfo from autotx.utils.is_dev_env import is_dev_env from autotx.wallets.smart_wallet import SmartWallet diff --git a/autotx/utils/ethereum/SafeManager.py b/autotx/utils/ethereum/SafeManager.py index 9072532..9b453f2 100644 --- a/autotx/utils/ethereum/SafeManager.py +++ b/autotx/utils/ethereum/SafeManager.py @@ -17,9 +17,9 @@ from autotx.transactions import TransactionBase from autotx.utils.ethereum.get_native_balance import get_native_balance from autotx.utils.ethereum.cached_safe_address import get_cached_safe_address, save_cached_safe_address -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.is_valid_safe import is_valid_safe -from autotx.utils.ethereum.networks import ChainId, NetworkInfo +from autotx.utils.ethereum.networks import NetworkInfo from .deploy_safe_with_create2 import deploy_safe_with_create2 from .deploy_multicall import deploy_multicall from .get_erc20_balance import get_erc20_balance diff --git a/autotx/utils/ethereum/build_approve_erc20.py b/autotx/utils/ethereum/build_approve_erc20.py index d99f4e3..71b844a 100644 --- a/autotx/utils/ethereum/build_approve_erc20.py +++ b/autotx/utils/ethereum/build_approve_erc20.py @@ -1,6 +1,6 @@ from web3 import Web3 from web3.types import TxParams -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.erc20_abi import ERC20_ABI def build_approve_erc20(web3: Web3, token_address: ETHAddress, spender: ETHAddress, value: float) -> TxParams: diff --git a/autotx/utils/ethereum/build_transfer_erc20.py b/autotx/utils/ethereum/build_transfer_erc20.py index df57e18..115b8d6 100644 --- a/autotx/utils/ethereum/build_transfer_erc20.py +++ b/autotx/utils/ethereum/build_transfer_erc20.py @@ -1,7 +1,7 @@ from web3 import Web3 from web3.types import TxParams, Wei -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from .constants import GAS_PRICE_MULTIPLIER from .erc20_abi import ERC20_ABI diff --git a/autotx/utils/ethereum/build_transfer_native.py b/autotx/utils/ethereum/build_transfer_native.py index 7392dc6..4d83d06 100644 --- a/autotx/utils/ethereum/build_transfer_native.py +++ b/autotx/utils/ethereum/build_transfer_native.py @@ -1,7 +1,7 @@ from web3 import Web3 from web3.types import TxParams -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress def build_transfer_native(web3: Web3, from_address: ETHAddress, to: ETHAddress, value: float) -> TxParams: return { diff --git a/autotx/utils/ethereum/deploy_multicall.py b/autotx/utils/ethereum/deploy_multicall.py index ea6eff3..af58c4c 100644 --- a/autotx/utils/ethereum/deploy_multicall.py +++ b/autotx/utils/ethereum/deploy_multicall.py @@ -4,7 +4,7 @@ from eth_account.signers.local import LocalAccount from hexbytes import HexBytes -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from .cache import cache diff --git a/autotx/utils/ethereum/get_erc20_balance.py b/autotx/utils/ethereum/get_erc20_balance.py index 8ec2edc..2205daf 100644 --- a/autotx/utils/ethereum/get_erc20_balance.py +++ b/autotx/utils/ethereum/get_erc20_balance.py @@ -1,6 +1,6 @@ from web3 import Web3 -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from .erc20_abi import ERC20_ABI def get_erc20_balance(web3: Web3, token_address: ETHAddress, account: ETHAddress) -> float: diff --git a/autotx/utils/ethereum/get_erc20_info.py b/autotx/utils/ethereum/get_erc20_info.py index a5fb430..a3a8452 100644 --- a/autotx/utils/ethereum/get_erc20_info.py +++ b/autotx/utils/ethereum/get_erc20_info.py @@ -1,6 +1,6 @@ from web3 import Web3 -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from .erc20_abi import ERC20_ABI def get_erc20_info(web3: Web3, token_address: ETHAddress) -> tuple[str, str, int]: diff --git a/autotx/utils/ethereum/get_native_balance.py b/autotx/utils/ethereum/get_native_balance.py index 9f86285..dd04c82 100644 --- a/autotx/utils/ethereum/get_native_balance.py +++ b/autotx/utils/ethereum/get_native_balance.py @@ -1,6 +1,6 @@ from web3 import Web3 -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress def get_native_balance(web3: Web3, address: ETHAddress) -> float: return web3.eth.get_balance(address.hex) / 10 ** 18 \ No newline at end of file diff --git a/autotx/utils/ethereum/helpers/fill_dev_account_with_tokens.py b/autotx/utils/ethereum/helpers/fill_dev_account_with_tokens.py index 0ff8b49..8e66679 100644 --- a/autotx/utils/ethereum/helpers/fill_dev_account_with_tokens.py +++ b/autotx/utils/ethereum/helpers/fill_dev_account_with_tokens.py @@ -1,6 +1,6 @@ from autotx.utils.ethereum import transfer_erc20 from autotx.utils.ethereum.constants import NATIVE_TOKEN_ADDRESS -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.helpers.swap_from_eoa import swap from autotx.utils.ethereum.networks import ChainId, NetworkInfo from eth_account.signers.local import LocalAccount diff --git a/autotx/utils/ethereum/helpers/show_address_balances.py b/autotx/utils/ethereum/helpers/show_address_balances.py index d6fd1ea..3894a99 100644 --- a/autotx/utils/ethereum/helpers/show_address_balances.py +++ b/autotx/utils/ethereum/helpers/show_address_balances.py @@ -7,7 +7,7 @@ get_native_token_symbol, ) from autotx.utils.ethereum.networks import SUPPORTED_NETWORKS_CONFIGURATION_MAP, ChainId, NetworkConfiguration -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress def show_address_balances(web3: Web3, network: ChainId, address: ETHAddress) -> None: diff --git a/autotx/utils/ethereum/helpers/swap_from_eoa.py b/autotx/utils/ethereum/helpers/swap_from_eoa.py index 3fdc474..54a1276 100644 --- a/autotx/utils/ethereum/helpers/swap_from_eoa.py +++ b/autotx/utils/ethereum/helpers/swap_from_eoa.py @@ -4,7 +4,7 @@ from gnosis.eth import EthereumClient from web3.types import TxParams -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.lifi.swap import build_swap_transaction from autotx.utils.ethereum.networks import ChainId diff --git a/autotx/utils/ethereum/is_valid_safe.py b/autotx/utils/ethereum/is_valid_safe.py index 10ffa0c..ef37cb8 100644 --- a/autotx/utils/ethereum/is_valid_safe.py +++ b/autotx/utils/ethereum/is_valid_safe.py @@ -3,7 +3,7 @@ from web3 import Web3 from gnosis.safe import Safe -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from .constants import MASTER_COPY_ADDRESS, MASTER_COPY_L2_ADDRESSES def is_valid_safe(client: EthereumClient, safe_address: ETHAddress) -> bool: diff --git a/autotx/utils/ethereum/lifi/__init__.py b/autotx/utils/ethereum/lifi/__init__.py index ba13c02..ccafabc 100644 --- a/autotx/utils/ethereum/lifi/__init__.py +++ b/autotx/utils/ethereum/lifi/__init__.py @@ -5,7 +5,7 @@ import re from autotx.utils.constants import LIFI_API_KEY -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.networks import ChainId diff --git a/autotx/utils/ethereum/lifi/swap.py b/autotx/utils/ethereum/lifi/swap.py index 03bdd46..8a2f163 100644 --- a/autotx/utils/ethereum/lifi/swap.py +++ b/autotx/utils/ethereum/lifi/swap.py @@ -7,7 +7,7 @@ from autotx.transactions import ApproveTransaction, SwapTransaction, Transaction from autotx.utils.ethereum.constants import GAS_PRICE_MULTIPLIER, NATIVE_TOKEN_ADDRESS from autotx.utils.ethereum.erc20_abi import ERC20_ABI -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.helpers.get_native_token_symbol import ( get_native_token_symbol, ) diff --git a/autotx/utils/ethereum/send_native.py b/autotx/utils/ethereum/send_native.py index 4df80e5..41e34cb 100644 --- a/autotx/utils/ethereum/send_native.py +++ b/autotx/utils/ethereum/send_native.py @@ -3,7 +3,7 @@ from web3 import Web3 from web3.types import TxParams, TxReceipt, Wei -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from .constants import GAS_PRICE_MULTIPLIER diff --git a/autotx/utils/ethereum/transfer_erc20.py b/autotx/utils/ethereum/transfer_erc20.py index a09085b..9634e41 100644 --- a/autotx/utils/ethereum/transfer_erc20.py +++ b/autotx/utils/ethereum/transfer_erc20.py @@ -4,7 +4,7 @@ from web3.types import Wei from web3.middleware.signing import construct_sign_and_send_raw_middleware -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from .constants import GAS_PRICE_MULTIPLIER diff --git a/autotx/wallets/smart_wallet.py b/autotx/wallets/smart_wallet.py index b29e4e4..cc54425 100644 --- a/autotx/wallets/smart_wallet.py +++ b/autotx/wallets/smart_wallet.py @@ -3,7 +3,7 @@ from web3 import Web3 from autotx.intents import Intent from autotx.utils.ethereum.get_erc20_balance import get_erc20_balance -from autotx.utils.ethereum.eth_address import ETHAddress +from autotx.eth_address import ETHAddress from autotx.utils.ethereum.get_native_balance import get_native_balance From 2f295d83a3a40f08dd8f28e0c449710d7f636b5c Mon Sep 17 00:00:00 2001 From: Cesar Date: Wed, 19 Jun 2024 00:13:30 +0200 Subject: [PATCH 26/26] chore: remove unneeded changes --- autotx/helper_agents/user_proxy.py | 3 ++- autotx/utils/LlamaClient.py | 1 - pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/autotx/helper_agents/user_proxy.py b/autotx/helper_agents/user_proxy.py index 607eb0f..0c78f07 100644 --- a/autotx/helper_agents/user_proxy.py +++ b/autotx/helper_agents/user_proxy.py @@ -24,7 +24,8 @@ def build(user_prompt: str, agents_information: str, get_llm_config: Callable[[] NEVER ask the user questions. NEVER make up a token, ALWAYS ask the 'research-tokens' agent to first search for the token. - If the goal has been achieved, FIRST reflect on the goal and make sure nothing is missing, then end the conversation with 'TERMINATE' (it MUST be upper case and in the same message). Consider the goal met if the other agents have prepared the necessary transactions and all user queries have been answered. + If the goal has been achieved, FIRST reflect on the goal and make sure nothing is missing, then end the conversation with 'TERMINATE' (it MUST be upper case and in the same message). + Consider the goal met if the other agents have prepared the necessary transactions and all user queries have been answered. If the user's goal involves buying tokens, make sure the correct number of tokens are bought. For buying tokens, you can use the 'swap-tokens' agent. If you encounter an error, try to resolve it (either yourself of with other agents) and only respond with 'TERMINATE' if the goal is truly not achievable. diff --git a/autotx/utils/LlamaClient.py b/autotx/utils/LlamaClient.py index 8150cd2..1c4ded0 100644 --- a/autotx/utils/LlamaClient.py +++ b/autotx/utils/LlamaClient.py @@ -1,4 +1,3 @@ -import logging from types import SimpleNamespace from typing import Any, Dict, Union, cast from autogen import ModelClient diff --git a/pyproject.toml b/pyproject.toml index 178462a..9b27078 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "autotx" -version = "0.1.1-beta.4" +version = "0.1.1" description = "" authors = ["Nestor Amesty "] readme = "README.md"