Skip to content

Commit

Permalink
Merge pull request #60 from KahanMajmudar/feat/mech-marketplace-support
Browse files Browse the repository at this point in the history
WIP: adds support to send request to the mech marketplace
  • Loading branch information
0xArdi authored Dec 10, 2024
2 parents 9a5687e + 1b81fe3 commit 02dad93
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 17 deletions.
55 changes: 38 additions & 17 deletions mech_client/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
from mech_client import __version__
from mech_client.interact import ConfirmationType
from mech_client.interact import interact as interact_
from mech_client.marketplace_interact import (
marketplace_interact as marketplace_interact_,
)
from mech_client.mech_tool_management import (
get_tool_description,
get_tool_io_schema,
Expand All @@ -45,7 +48,7 @@ def cli() -> None:

@click.command()
@click.argument("prompt")
@click.argument("agent_id", type=int)
@click.option("--agent_id", type=int, help="Id of the agent to be used")
@click.option(
"--key",
type=click.Path(exists=True, file_okay=True, dir_okay=False),
Expand Down Expand Up @@ -110,22 +113,40 @@ def interact( # pylint: disable=too-many-arguments
k, v = pair.split("=")
extra_attributes_dict[k] = v

interact_(
prompt=prompt,
agent_id=agent_id,
private_key_path=key,
tool=tool,
extra_attributes=extra_attributes_dict,
confirmation_type=(
ConfirmationType(confirm)
if confirm is not None
else ConfirmationType.WAIT_FOR_BOTH
),
retries=retries,
timeout=timeout,
sleep=sleep,
chain_config=chain_config,
)
if agent_id is None:
marketplace_interact_(
prompt=prompt,
private_key_path=key,
tool=tool,
extra_attributes=extra_attributes_dict,
confirmation_type=(
ConfirmationType(confirm)
if confirm is not None
else ConfirmationType.WAIT_FOR_BOTH
),
retries=retries,
timeout=timeout,
sleep=sleep,
chain_config=chain_config,
)

else:
interact_(
prompt=prompt,
agent_id=agent_id,
private_key_path=key,
tool=tool,
extra_attributes=extra_attributes_dict,
confirmation_type=(
ConfirmationType(confirm)
if confirm is not None
else ConfirmationType.WAIT_FOR_BOTH
),
retries=retries,
timeout=timeout,
sleep=sleep,
chain_config=chain_config,
)
except (ValueError, FileNotFoundError) as e:
raise click.ClickException(str(e)) from e

Expand Down
2 changes: 2 additions & 0 deletions mech_client/configs/mechs.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"gnosis": {
"agent_registry_contract": "0xE49CB081e8d96920C38aA7AB90cb0294ab4Bc8EA",
"mech_marketplace_contract": "0x4554fE75c1f5576c1d7F765B2A036c199Adae329",
"rpc_url": "https://rpc.eu-central-2.gateway.fm/v4/gnosis/non-archival/mainnet",
"wss_endpoint": "wss://rpc.eu-central-2.gateway.fm/ws/v4/gnosis/non-archival/mainnet",
"ledger_config": {
Expand All @@ -11,6 +12,7 @@
"is_gas_estimation_enabled": false
},
"gas_limit": 100000,
"price": 10000000000000000,
"contract_abi_url": "https://gnosis.blockscout.com/api/v2/smart-contracts/{contract_address}",
"transaction_url": "https://gnosisscan.io/tx/{transaction_digest}",
"subgraph_url": "https://api.studio.thegraph.com/query/57238/mech/version/latest"
Expand Down
5 changes: 5 additions & 0 deletions mech_client/interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class MechConfig: # pylint: disable=too-many-instance-attributes
"""Mech configuration"""

agent_registry_contract: str
mech_marketplace_contract: str
rpc_url: str
wss_endpoint: str
ledger_config: LedgerConfig
Expand All @@ -120,6 +121,10 @@ def __post_init__(self) -> None:
if agent_registry_contract:
self.agent_registry_contract = agent_registry_contract

mech_marketplace_contract = os.getenv("MECHX_MECH_MARKETPLACE_CONTRACT")
if mech_marketplace_contract:
self.mech_marketplace_contract = mech_marketplace_contract

rpc_url = os.getenv("MECHX_CHAIN_RPC")
if rpc_url:
self.rpc_url = rpc_url
Expand Down
235 changes: 235 additions & 0 deletions mech_client/marketplace_interact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import time
from dataclasses import asdict
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, Optional, List, Tuple

import websocket
from aea_ledger_ethereum import EthereumApi, EthereumCrypto
from web3.contract import Contract as Web3Contract
from web3.constants import ADDRESS_ZERO

from mech_client.prompt_to_ipfs import push_metadata_to_ipfs
from mech_client.wss import (
register_event_handlers,
)
from mech_client.interact import (
PRIVATE_KEY_FILE_PATH,
MECH_CONFIGS,
MAX_RETRIES,
WAIT_SLEEP,
TIMEOUT,
ConfirmationType,
calculate_topic_id,
get_mech_config,
get_abi,
get_contract,
)


def get_event_signatures(abi: List) -> Tuple[str, str]:
"""Calculate `Marketplace Request` and `Marketplace Deliver` event topics"""
marketplace_request, marketplace_deliver = "", ""
for obj in abi:
if obj["type"] != "event":
continue
if obj["name"] == "MarketplaceDeliver":
marketplace_deliver = calculate_topic_id(event=obj)
if obj["name"] == "MarketplaceRequest":
marketplace_request = calculate_topic_id(event=obj)
return marketplace_request, marketplace_deliver


def send_marketplace_request( # pylint: disable=too-many-arguments,too-many-locals
crypto: EthereumCrypto,
ledger_api: EthereumApi,
marketplace_contract: Web3Contract,
gas_limit: int,
prompt: str,
tool: str,
extra_attributes: Optional[Dict[str, Any]] = None,
price: int = 10_000_000_000_000_000,
retries: Optional[int] = None,
timeout: Optional[float] = None,
sleep: Optional[float] = None,
) -> Optional[str]:
"""
Sends a request to the mech.
:param crypto: The Ethereum crypto object.
:type crypto: EthereumCrypto
:param ledger_api: The Ethereum API used for interacting with the ledger.
:type ledger_api: EthereumApi
:param marketplace_contract: The mech marketplace contract instance.
:type marketplace_contract: Web3Contract
:param gas_limit: Gas limit.
:type gas_limit: int
:param prompt: The request prompt.
:type prompt: str
:param tool: The requested tool.
:type tool: str
:param extra_attributes: Extra attributes to be included in the request metadata.
:type extra_attributes: Optional[Dict[str,Any]]
:param price: The price for the request (default: 10_000_000_000_000_000).
:type price: int
:param retries: Number of retries for sending a transaction
:type retries: int
:param timeout: Timeout to wait for the transaction
:type timeout: float
:param sleep: Amount of sleep before retrying the transaction
:type sleep: float
:return: The transaction hash.
:rtype: Optional[str]
"""
v1_file_hash_hex_truncated, v1_file_hash_hex = push_metadata_to_ipfs(
prompt, tool, extra_attributes
)
print(
f" - Prompt uploaded: https://gateway.autonolas.tech/ipfs/{v1_file_hash_hex}"
)
method_name = "request"
methord_args = {
"data": v1_file_hash_hex_truncated,
"priorityMech": ADDRESS_ZERO,
"priorityMechStakingInstance": ADDRESS_ZERO,
"priorityMechServiceId": 0,
"requesterStakingInstance": ADDRESS_ZERO,
"requesterServiceId": 0,
"responseTimeout": 300,
}
tx_args = {
"sender_address": crypto.address,
"value": price,
"gas": gas_limit,
}

tries = 0
retries = retries or MAX_RETRIES
timeout = timeout or TIMEOUT
sleep = sleep or WAIT_SLEEP
deadline = datetime.now().timestamp() + timeout

while tries < retries and datetime.now().timestamp() < deadline:
tries += 1
try:
raw_transaction = ledger_api.build_transaction(
contract_instance=marketplace_contract,
method_name=method_name,
method_args=methord_args,
tx_args=tx_args,
raise_on_try=True,
)
signed_transaction = crypto.sign_transaction(raw_transaction)
transaction_digest = ledger_api.send_signed_transaction(
signed_transaction,
raise_on_try=True,
)
return transaction_digest
except Exception as e: # pylint: disable=broad-except
print(
f"Error occured while sending the transaction: {e}; Retrying in {sleep}"
)
time.sleep(sleep)
return None


def marketplace_interact(
prompt: str,
tool: Optional[str] = None,
extra_attributes: Optional[Dict[str, Any]] = None,
private_key_path: Optional[str] = None,
confirmation_type: ConfirmationType = ConfirmationType.WAIT_FOR_BOTH,
retries: Optional[int] = None,
timeout: Optional[float] = None,
sleep: Optional[float] = None,
chain_config: Optional[str] = None,
) -> Any:
"""
Interact with mech marketplace contract.
:param prompt: The interaction prompt.
:type prompt: str
:param tool: The tool to interact with (optional).
:type tool: Optional[str]
:param extra_attributes: Extra attributes to be included in the request metadata (optional).
:type extra_attributes: Optional[Dict[str, Any]]
:param private_key_path: The path to the private key file (optional).
:type private_key_path: Optional[str]
:param confirmation_type: The confirmation type for the interaction (default: ConfirmationType.WAIT_FOR_BOTH).
:type confirmation_type: ConfirmationType
:return: The data received from on-chain/off-chain.
:param retries: Number of retries for sending a transaction
:type retries: int
:param timeout: Timeout to wait for the transaction
:type timeout: float
:param sleep: Amount of sleep before retrying the transaction
:type sleep: float
:param chain_config: Id of the mech's chain configuration (stored configs/mechs.json)
:type chain_config: str:
:rtype: Any
"""

mech_config = get_mech_config(chain_config)
ledger_config = mech_config.ledger_config
contract_address = mech_config.mech_marketplace_contract
if contract_address is None:
raise ValueError(f"Mech Marketplace does not exist!")

private_key_path = private_key_path or PRIVATE_KEY_FILE_PATH
if not Path(private_key_path).exists():
raise FileNotFoundError(
f"Private key file `{private_key_path}` does not exist!"
)

wss = websocket.create_connection(mech_config.wss_endpoint)
crypto = EthereumCrypto(private_key_path=private_key_path)
ledger_api = EthereumApi(**asdict(ledger_config))

abi = get_abi(
contract_address=contract_address,
contract_abi_url=mech_config.contract_abi_url,
)
mech_marketplace_contract = get_contract(
contract_address=contract_address, abi=abi, ledger_api=ledger_api
)

marketplace_request_event_signature, marketplace_deliver_event_signature = (
get_event_signatures(abi=abi)
)

register_event_handlers(
wss=wss,
contract_address=contract_address,
crypto=crypto,
request_signature=marketplace_request_event_signature,
deliver_signature=marketplace_deliver_event_signature,
)

print("Sending Mech Marketplace request...")
price = mech_config.price or 10_000_000_000_000_000

transaction_digest = send_marketplace_request(
crypto=crypto,
ledger_api=ledger_api,
marketplace_contract=mech_marketplace_contract,
gas_limit=mech_config.gas_limit,
price=price,
prompt=prompt,
tool=tool,
extra_attributes=extra_attributes,
retries=retries,
timeout=timeout,
sleep=sleep,
)

if not transaction_digest:
print("Unable to send request")
return None

transaction_url_formatted = mech_config.transaction_url.format(
transaction_digest=transaction_digest
)
print(f" - Transaction sent: {transaction_url_formatted}")
print(" - Waiting for transaction receipt...")

return

0 comments on commit 02dad93

Please sign in to comment.