Skip to content

Commit

Permalink
feat: initial client implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidMinarsch committed May 27, 2023
1 parent 110f77e commit 2ffbe20
Show file tree
Hide file tree
Showing 11 changed files with 2,210 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ethereum_private_key.txt

*.DS_Store
dist/
__pycache__
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright [yyyy] [name of copyright owner]
Copyright 2023 Valory AG

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,58 @@
# mech-client
Basic client to interact with a mech

> **Warning**<br />
> **This is a hacky alpha version of the client - don't rely on it as production software.**
## Installation

```bash
pip install mech-client
```

## CLI:

```bash
Usage: mechx [OPTIONS] COMMAND [ARGS]...

Command-line tool for interacting with mechs.

Options:
--version Show the version and exit.
--help Show this message and exit.

Commands:
interact Interact with a mech specifying a prompt and tool.
prompt-to-ipfs Upload a prompt and tool to IPFS as metadata.
push-to-ipfs Upload a file to IPFS.
```

## Usage:

First, create a private key in file `ethereum_private_key.txt` with this command:

```bash
aea generate-key ethereum
```

Ensure the private key carries funds on Gnosis Chain.

Second, run the following command to instruct the mech with `<prompt>` and `<tool>`:

```bash
mechx interact <prompt> <tool>
```

Example output:
```bash
mechx interact "write a short poem" "openai-text-davinci-003"
Prompt uploaded: https://gateway.autonolas.tech/ipfs/f01701220ad9e2d5698fbd6c3a4ce61f329590e68a23181772669e543e69decdae316423b
Transaction sent: https://gnosisscan.io/tx/0xb3a17ef90da6cc7a86e008a3a91bd367d573b406eae53405a4aa981001a5eaf3
Request on-chain with id: 15263135923206312300456917202469137903009897852865973093832667165921851537677
Data arrived: https://gateway.autonolas.tech/ipfs/f017012205053a4ae3ef0cf4ed7eff0c2d74dbaf3479fbdeb292472560e7bfaa4cfecfcdc
Data: {'requestId': 15263135923206312300456917202469137903009897852865973093832667165921851537677, 'result': "\n\nA sun-filled sky,\nA soft breeze blowing by,\nWhere the trees sway in the wind,\nA peaceful moment I can't rewind."}
```

## Release guide:

Finish edits, bump versions in `pyproject.toml` and `mech_client/__init__.py`, then `poetry lock`, then `rm -rf dist`, then `poetry publish --build --username=<username> --password=<password>`.
1 change: 1 addition & 0 deletions mech_client/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.1.0"
47 changes: 47 additions & 0 deletions mech_client/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import click

from mech_client import __version__
from mech_client.interact import interact as interact_
from mech_client.prompt_to_ipfs import main as prompt_to_ipfs_main
from mech_client.push_to_ipfs import main as push_to_ipfs_main


@click.group(name="mechx") # type: ignore
@click.version_option(__version__, prog_name="mechx")
def cli() -> None:
"""Command-line tool for interacting with mechs."""


@click.command()
@click.argument("prompt")
@click.argument("tool")
def interact(prompt: str, tool: str) -> None:
"""Interact with a mech specifying a prompt and tool."""
interact_(prompt=prompt, tool=tool)
import pdb

pdb.set_trace()


@click.command()
@click.argument("prompt")
@click.argument("tool")
def prompt_to_ipfs(prompt: str, tool: str) -> None:
"""Upload a prompt and tool to IPFS as metadata."""
prompt_to_ipfs_main(prompt=prompt, tool=tool)


@click.command()
@click.argument("file_path")
def push_to_ipfs(file_path: str) -> None:
"""Upload a file to IPFS."""
push_to_ipfs_main(file_path=file_path)


cli.add_command(interact)
cli.add_command(prompt_to_ipfs)
cli.add_command(push_to_ipfs)


if __name__ == "__main__":
cli()
160 changes: 160 additions & 0 deletions mech_client/interact.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
#
# Copyright 2023 Valory AG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# ------------------------------------------------------------------------------

"""
This script allows sending a Request to an on-chain mech and waiting for the Deliver.
Usage:
python client.py <prompt> <tool>
"""

import json
import sys
import time
from typing import Optional

import requests
import websocket
from aea.contracts.base import Contract
from aea_ledger_ethereum import EthereumApi, EthereumCrypto

from mech_client.prompt_to_ipfs import push_metadata_to_ipfs

CONTRACT_ADDRESS = "0xFf82123dFB52ab75C417195c5fDB87630145ae81"
ETHEREUM_TESTNET_CONFIG = {
"address": "https://rpc.eu-central-2.gateway.fm/v4/gnosis/non-archival/mainnet",
"chain_id": 100,
"poa_chain": False,
"default_gas_price_strategy": "eip1559",
}
PRIVATE_KEY_FILE_PATH = "ethereum_private_key.txt"


def check_for_tools(tool: str) -> Optional[int]:
"""Checks if the tool is valid and for what agent."""
# TODO - replace hardcoded logic with on-chain check against agent mech registry
return (
3
if tool
in [
"openai-text-davinci-002",
"openai-text-davinci-003",
"openai-gpt-3.5-turbo",
"openai-gpt-4",
]
else None
)


def send_request(
prompt: str,
tool: str,
contract_address: str = CONTRACT_ADDRESS,
price: int = 10_000_000_000_000_000,
) -> Contract:
"""Sends a request to the mech."""
mech = check_for_tools(tool)
if mech is None:
raise ValueError(f"Tool {tool} is not supported by any mech.")
v1_file_hash_hex_truncated, v1_file_hash_hex = push_metadata_to_ipfs(prompt, tool)
print(f"Prompt uploaded: https://gateway.autonolas.tech/ipfs/{v1_file_hash_hex}")

ethereum_crypto = EthereumCrypto(private_key_path=PRIVATE_KEY_FILE_PATH)
ethereum_ledger_api = EthereumApi(**ETHEREUM_TESTNET_CONFIG)

gnosisscan_api_url = f"https://api.gnosisscan.io/api?module=contract&action=getabi&address={contract_address}"
response = requests.get(gnosisscan_api_url)
abi = response.json()["result"]
abi = json.loads(abi)

contract_instance = ethereum_ledger_api.get_contract_instance(
{"abi": abi, "bytecode": "0x"}, contract_address
)
method_name = "request"
methord_args = {
"data": v1_file_hash_hex_truncated
} # bytes.fromhex(v1_file_hash_hex_truncated[2:])}
tx_args = {"sender_address": ethereum_crypto.address, "value": price}

raw_transaction = ethereum_ledger_api.build_transaction(
contract_instance=contract_instance,
method_name=method_name,
method_args=methord_args,
tx_args=tx_args,
)
raw_transaction["gas"] = 50_000
# raw_transaction = ethereum_ledger_api.update_with_gas_estimate(raw_transaction)
signed_transaction = ethereum_crypto.sign_transaction(raw_transaction)
transaction_digest = ethereum_ledger_api.send_signed_transaction(signed_transaction)
print(f"Transaction sent: https://gnosisscan.io/tx/{transaction_digest}")
return contract_instance, ethereum_ledger_api


def watch_for_events(
contract_instance: Contract, ethereum_ledger_api: EthereumApi
) -> None:
"""Watches for events on mech."""
wss_endpoint = "wss://rpc.eu-central-2.gateway.fm/ws/v4/gnosis/non-archival/mainnet"
wss = websocket.create_connection(wss_endpoint)
subscription_msg_template = {
"jsonrpc": "2.0",
"id": 1,
"method": "eth_subscribe",
"params": ["logs", {"address": CONTRACT_ADDRESS}],
}
content = bytes(json.dumps(subscription_msg_template), "utf-8")
wss.send(content)
# registration confirmation
msg = wss.recv()
# events
count = 0
while count < 2:
msg = wss.recv()
data = json.loads(msg)
tx_hash = data["params"]["result"]["transactionHash"]
no_receipt = True
while no_receipt:
try:
tx_receipt = ethereum_ledger_api._api.eth.get_transaction_receipt(
tx_hash
)
no_receipt = False
except Exception:
time.sleep(1)
if count == 0:
rich_logs = contract_instance.events.Request().processReceipt(tx_receipt)
request_id = rich_logs[0]["args"]["requestId"]
print(f"Request on-chain with id: {request_id}")
if count == 1:
rich_logs = contract_instance.events.Deliver().processReceipt(tx_receipt)
data = rich_logs[0]["args"]["data"]
data_url = "https://gateway.autonolas.tech/ipfs/f01701220" + data.hex()
print(f"Data arrived: {data_url}")
count += 1
response = requests.get(data_url + "/" + str(request_id))
print(f"Data: {response.json()}")


def interact(prompt: str, tool: str) -> None:
import pdb

pdb.set_trace()
contract_instance, ethereum_ledger_api = send_request(prompt, tool)
watch_for_events(contract_instance, ethereum_ledger_api)
52 changes: 52 additions & 0 deletions mech_client/prompt_to_ipfs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
#
# Copyright 2023 Valory AG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# ------------------------------------------------------------------------------

"""
This script allows pushing a prompt compatible with the on-chain mechs directly to IPFS.
Usage:
python push_to_ipfs.py <prompt> <tool>
"""

import json
import shutil
import sys
import tempfile
import uuid
from typing import Tuple

from mech_client.push_to_ipfs import push_to_ipfs


def push_metadata_to_ipfs(prompt: str, tool: str) -> Tuple[str, str]:
metadata = {"prompt": prompt, "tool": tool, "nonce": str(uuid.uuid4())}
dirpath = tempfile.mkdtemp()
file_name = dirpath + "metadata.json"
with open(file_name, "w") as f:
json.dump(metadata, f)
_, v1_file_hash_hex = push_to_ipfs(file_name)
shutil.rmtree(dirpath)
return "0x" + v1_file_hash_hex[9:], v1_file_hash_hex


def main(prompt: str, tool: str) -> None:
v1_file_hash_hex_truncated, v1_file_hash_hex = push_metadata_to_ipfs(prompt, tool)
print("Visit url: https://gateway.autonolas.tech/ipfs/{}".format(v1_file_hash_hex))
print("Hash for Request method: {}".format(v1_file_hash_hex_truncated))
51 changes: 51 additions & 0 deletions mech_client/push_to_ipfs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------
#
# Copyright 2023 Valory AG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# ------------------------------------------------------------------------------

"""
This script allows pushing a file directly to IPFS.
Usage:
python push_to_ipfs.py <file_path>
"""

import sys
from typing import Tuple

import multibase
import multicodec
from aea.helpers.cid import to_v1
from aea_cli_ipfs.ipfs_utils import IPFSTool


def push_to_ipfs(file_path: str) -> Tuple[str, str]:
response = IPFSTool().client.add(
file_path, pin=True, recursive=True, wrap_with_directory=False
)
v1_file_hash = to_v1(response["Hash"])
cid_bytes = multibase.decode(v1_file_hash)
multihash_bytes = multicodec.remove_prefix(cid_bytes)
v1_file_hash_hex = "f01" + multihash_bytes.hex()
return v1_file_hash, v1_file_hash_hex


def main(file_path: str) -> None:
v1_file_hash, v1_file_hash_hex = push_to_ipfs(file_path)
print("IPFS file hash v1: {}".format(v1_file_hash))
print("IPFS file hash v1 hex: {}".format(v1_file_hash_hex))
Loading

0 comments on commit 2ffbe20

Please sign in to comment.