diff --git a/.github/workflows/common_checks.yaml b/.github/workflows/common_checks.yaml index 6f156bef..b2f558f8 100644 --- a/.github/workflows/common_checks.yaml +++ b/.github/workflows/common_checks.yaml @@ -63,8 +63,9 @@ jobs: run: tomte check-copyright --author valory --exclude-part abci --exclude-part http_client --exclude-part ipfs --exclude-part ledger --exclude-part p2p_libp2p_client --exclude-part gnosis_safe --exclude-part gnosis_safe_proxy_factory --exclude-part multisend --exclude-part service_registry --exclude-part protocols --exclude-part abstract_abci --exclude-part abstract_round_abci --exclude-part registration_abci --exclude-part reset_pause_abci --exclude-part termination_abci --exclude-part transaction_settlement_abci --exclude-part websocket_client --exclude-part contract_subscription - name: License compatibility check run: tox -e liccheck - - name: Check dependencies - run: tox -e check-dependencies +# TODO: reactivate once false positives are fixed +# - name: Check dependencies +# run: tox -e check-dependencies - name: Check doc links run: tomte check-doc-links - name: Check doc IPFS hashes @@ -98,8 +99,8 @@ jobs: pip install --user --upgrade setuptools # install Protobuf compiler - wget https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-linux-x86_64.zip - unzip protoc-3.19.4-linux-x86_64.zip -d protoc + wget https://github.com/protocolbuffers/protobuf/releases/download/v24.3/protoc-24.3-linux-x86_64.zip + unzip protoc-24.3-linux-x86_64.zip -d protoc sudo mv protoc/bin/protoc /usr/local/bin/protoc # install IPFS diff --git a/.gitignore b/.gitignore index 4d3ef53a..47d4ede1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ packages/valory/connections/abci/ packages/valory/connections/ipfs/ packages/valory/connections/ledger/ packages/valory/connections/p2p_libp2p_client/ - packages/valory/skills/abstract_abci/ packages/valory/contracts/gnosis_safe_proxy_factory/ @@ -31,6 +30,15 @@ packages/valory/protocols/tendermint .mypy_cache /.tox/ +packages/valory/protocols/abci +packages/valory/protocols/acn +packages/valory/protocols/contract_api +packages/valory/protocols/http +packages/valory/protocols/ipfs +packages/valory/protocols/ledger_api +packages/valory/protocols/tendermint + + .env .1env keys.json diff --git a/packages/packages.json b/packages/packages.json index 49f21024..06cdcc82 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -1,22 +1,24 @@ { "dev": { - "connection/valory/websocket_client/0.1.0": "bafybeia3lo6hyhom2ht56gzgkv4gdsul5s4qoelljyks2wrcfvx5dtvsiq", - "skill/valory/contract_subscription/0.1.0": "bafybeigl52zsiduaabccpeqtii2f7uorzdnb6xgpoimo3mudvbudifmwtu", - "agent/valory/mech/0.1.0": "bafybeidsxvvyks3z5lq7ejkb4rj2ncqpxtqksj7xi2favs7u72syple4ji", - "skill/valory/mech_abci/0.1.0": "bafybeidh4ro4u6edng5oxggn27rzylo53yxxjujope2srrbdz3vkjvg2um", - "contract/valory/agent_mech/0.1.0": "bafybeiektlfcs66jmprajmfg45rvyxbq7wqwj2yzpohyvlux4447talgsa", - "service/valory/mech/0.1.0": "bafybeicn2biozztigz5lxgjqou6mgtpfm6cduqmqsd7nvnki2qxlcgwzse", + "connection/valory/websocket_client/0.1.0": "bafybeiako6kyllvgdmqio5y7zxqysaqlwfzjbdij4dkpsibuz3cznh3oza", + "skill/valory/contract_subscription/0.1.0": "bafybeibccykzca4ybe3ozbdfuxkbialdgsrr4zknxbowarbqt5onsffblq", + "agent/valory/mech/0.1.0": "bafybeiag6kzwog6zgdsk5v3sk6m636c67fipbcxuajfkcoozdt2knlt5da", + "skill/valory/mech_abci/0.1.0": "bafybeihmnjvk33yqhfptpmrayzx6fnfiwkyqwlr6xnlahqdrq34g7drccy", + "contract/valory/agent_mech/0.1.0": "bafybeicshvlc2slopzidzblf2zhdcw2uuav3ntxcgqduxskjujvebikg5u", + "service/valory/mech/0.1.0": "bafybeiei5uox7azc6otm5stj4hfggxzhyqh7mhv6erwftb7ok3i4wlplym", "protocol/valory/acn_data_share/0.1.0": "bafybeih5ydonnvrwvy2ygfqgfabkr47s4yw3uqxztmwyfprulwfsoe7ipq", "protocol/valory/default/1.0.0": "bafybeifqcqy5hfbnd7fjv4mqdjrtujh2vx3p2xhe33y67zoxa6ph7wdpaq", - "skill/valory/task_submission_abci/0.1.0": "bafybeidis6ncqusjvhe4o3kwb2qjfc6kxsa3l5j635iaeuke6wm7ezylpi", - "skill/valory/task_execution/0.1.0": "bafybeihah7eanflznxfowubqbvfz7epqz5vnimylcpyelxukxzbt3jiaqi", + "skill/valory/task_submission_abci/0.1.0": "bafybeifzxp4rmqmobgzl5mcfsg3kuvwwzq7c456u2tzlpfq7s7j3tmzk6u", + "skill/valory/task_execution/0.1.0": "bafybeianf56ypn6pjrqvj24uhmty2b5vtsewzjcrqxnrqyjsuudhyjpiue", "skill/valory/reset_pause_abci/0.1.0": "bafybeidgizxuqcmsznigog3ly5ffqk6wohthw3ohti4xcsalp7erxgxowe", "skill/valory/registration_abci/0.1.0": "bafybeib2flpet4krdafrh55jljxlpplkzf4vfuideu6srjma23h7nf55yi", "skill/valory/abstract_round_abci/0.1.0": "bafybeifutndycs3n4xxru76bzxmmr26v6sakbaaa3qgbpsxlkknsnzs6ve", "connection/valory/http_client/0.23.0": "bafybeicc4msyohrmjzqiu7pgpqvxmyqd7mmp3vfuairdeea2o2pblpzcui", - "skill/valory/termination_abci/0.1.0": "bafybeign7rs4qg5pucqf6sqgwkirjomamnlp4zgai3a4q3xsq37z4mmonm", - "skill/valory/transaction_settlement_abci/0.1.0": "bafybeieomj6ecrrolnhxrgygpdra3rjhgllursknsvup2m2illirjxaqu4", - "contract/valory/agent_registry/0.1.0": "bafybeiargayav6yiztdnwzejoejstcx4idssch2h4f5arlgtzj3tgsgfmu" + "skill/valory/termination_abci/0.1.0": "bafybeieupcqf7v4e77w7kpsgmh66vilsl4rlhytaoacg5txbw6qqcihcoy", + "skill/valory/transaction_settlement_abci/0.1.0": "bafybeibei2b6l3s2msmirbgru6atbbr6ezneei2txwe3ya3aijgzcgfac4", + "contract/valory/agent_registry/0.1.0": "bafybeiargayav6yiztdnwzejoejstcx4idssch2h4f5arlgtzj3tgsgfmu", + "protocol/valory/websocket_client/0.1.0": "bafybeih43mnztdv3v2hetr2k3gezg7d3yj4ur7cxdvcyaqhg65e52s5sf4", + "skill/valory/websocket_client/0.1.0": "bafybeiaazkuyvkg5xaz62tkmstqc65oyerwpdfe3vl2morsvxdolq4p2le" }, "third_party": { "protocol/open_aea/signing/1.0.0": "bafybeie7xyems76v5b4wc2lmaidcujizpxfzjnnwdeokmhje53g7ym25ii", diff --git a/packages/valory/agents/mech/aea-config.yaml b/packages/valory/agents/mech/aea-config.yaml index 18d7ed54..5c9c9fb9 100644 --- a/packages/valory/agents/mech/aea-config.yaml +++ b/packages/valory/agents/mech/aea-config.yaml @@ -12,13 +12,14 @@ connections: - valory/ipfs:0.1.0:bafybeigfmqvlzbp67fttccpl4hsu3zaztbxv6vd7ikzra2hfppfkalgpji - valory/ledger:0.19.0:bafybeigdckv3e6bz6kfloz4ucqrsufft6k4jp6bwkbbcvh4fxvgbmzq3dm - valory/p2p_libp2p_client:0.1.0:bafybeihge56dn3xep2dzomu7rtvbgo4uc2qqh7ljl3fubqdi2lq44gs5lq -- valory/websocket_client:0.1.0:bafybeia3lo6hyhom2ht56gzgkv4gdsul5s4qoelljyks2wrcfvx5dtvsiq +- valory/websocket_client:0.1.0:bafybeiako6kyllvgdmqio5y7zxqysaqlwfzjbdij4dkpsibuz3cznh3oza contracts: -- valory/agent_mech:0.1.0:bafybeiektlfcs66jmprajmfg45rvyxbq7wqwj2yzpohyvlux4447talgsa +- valory/agent_mech:0.1.0:bafybeicshvlc2slopzidzblf2zhdcw2uuav3ntxcgqduxskjujvebikg5u - valory/gnosis_safe:0.1.0:bafybeifmsjpgbifvk7y462rhfczvjvpigkdniavghhg5utza3hbnffioq4 - valory/gnosis_safe_proxy_factory:0.1.0:bafybeigejiv4fkksyjwmr6doo23kfpicfbktuwspbamasyvjusfdyjtrxy - valory/multisend:0.1.0:bafybeig5byt5urg2d2bsecufxe5ql7f4mezg3mekfleeh32nmuusx66p4y - valory/service_registry:0.1.0:bafybeic4bgql6x5jotp43ddazybmyb7macifjzudavqll3547ayhawttpi +- valory/agent_registry:0.1.0:bafybeiargayav6yiztdnwzejoejstcx4idssch2h4f5arlgtzj3tgsgfmu protocols: - open_aea/signing:1.0.0:bafybeie7xyems76v5b4wc2lmaidcujizpxfzjnnwdeokmhje53g7ym25ii - valory/abci:0.1.0:bafybeihmzlmmb4pdo3zkhg6ehuyaa4lhw7bfpclln2o2z7v3o6fcep26iu @@ -33,14 +34,15 @@ protocols: skills: - valory/abstract_abci:0.1.0:bafybeifmfv4bgt5vzvgawlocksacqeadzg72zs4usvgjaf245hbbptpiki - valory/abstract_round_abci:0.1.0:bafybeifutndycs3n4xxru76bzxmmr26v6sakbaaa3qgbpsxlkknsnzs6ve -- valory/contract_subscription:0.1.0:bafybeigl52zsiduaabccpeqtii2f7uorzdnb6xgpoimo3mudvbudifmwtu -- valory/mech_abci:0.1.0:bafybeidh4ro4u6edng5oxggn27rzylo53yxxjujope2srrbdz3vkjvg2um +- valory/contract_subscription:0.1.0:bafybeibccykzca4ybe3ozbdfuxkbialdgsrr4zknxbowarbqt5onsffblq +- valory/mech_abci:0.1.0:bafybeihmnjvk33yqhfptpmrayzx6fnfiwkyqwlr6xnlahqdrq34g7drccy +- valory/task_execution:0.1.0:bafybeianf56ypn6pjrqvj24uhmty2b5vtsewzjcrqxnrqyjsuudhyjpiue - valory/registration_abci:0.1.0:bafybeib2flpet4krdafrh55jljxlpplkzf4vfuideu6srjma23h7nf55yi - valory/reset_pause_abci:0.1.0:bafybeidgizxuqcmsznigog3ly5ffqk6wohthw3ohti4xcsalp7erxgxowe -- valory/task_execution:0.1.0:bafybeihah7eanflznxfowubqbvfz7epqz5vnimylcpyelxukxzbt3jiaqi -- valory/task_submission_abci:0.1.0:bafybeidis6ncqusjvhe4o3kwb2qjfc6kxsa3l5j635iaeuke6wm7ezylpi -- valory/termination_abci:0.1.0:bafybeign7rs4qg5pucqf6sqgwkirjomamnlp4zgai3a4q3xsq37z4mmonm -- valory/transaction_settlement_abci:0.1.0:bafybeieomj6ecrrolnhxrgygpdra3rjhgllursknsvup2m2illirjxaqu4 +- valory/task_submission_abci:0.1.0:bafybeifzxp4rmqmobgzl5mcfsg3kuvwwzq7c456u2tzlpfq7s7j3tmzk6u +- valory/termination_abci:0.1.0:bafybeieupcqf7v4e77w7kpsgmh66vilsl4rlhytaoacg5txbw6qqcihcoy +- valory/transaction_settlement_abci:0.1.0:bafybeibei2b6l3s2msmirbgru6atbbr6ezneei2txwe3ya3aijgzcgfac4 +- valory/websocket_client:0.1.0:bafybeiaazkuyvkg5xaz62tkmstqc65oyerwpdfe3vl2morsvxdolq4p2le default_ledger: ethereum required_ledgers: - ethereum @@ -87,6 +89,16 @@ dependencies: version: ==0.0.303 scikit-learn: version: ==1.3.1 + pandas: + version: ==2.1.1 + hypothesis: + version: ==6.21.6 + spacy: + version: ==3.7.2 + tiktoken: + version: ==0.5.1 + python-dateutil: + version: ==2.8.2 default_connection: null --- public_id: valory/websocket_client:0.1.0:bafybeiexove4oqyhoae5xmk2hilskthosov5imdp65olpgj3cfrepbouyy @@ -99,9 +111,8 @@ is_abstract: true public_id: valory/contract_subscription:0.1.0:bafybeiby5ajjc7a3m2uq73d2pprx6enqt4ghfcq2gkmrtsr75e4d4napi4 type: skill behaviours: - subscriptions: - args: - contracts: ${list:["0xFf82123dFB52ab75C417195c5fDB87630145ae81"]} + contract_subscriptions: + args: {} handlers: new_event: args: @@ -150,13 +161,17 @@ models: tendermint_max_retries: ${int:5} tendermint_url: ${str:http://localhost:26657} use_termination: ${bool:false} - agent_mech_contract_address: ${str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + agent_mech_contract_addresses: ${list:["0xFf82123dFB52ab75C417195c5fDB87630145ae81"]} round_timeout_seconds: ${float:30.0} reset_period_count: ${int:1000} on_chain_service_id: ${int:1} + agent_registry_address: ${str:0x0000000000000000000000000000000000000000} + agent_id: ${int:3} + metadata_hash: ${str:null} share_tm_config_on_startup: ${bool:false} multisend_address: ${str:0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761} service_registry_address: ${str:0x9338b5153AE39BB89f50468E608eD9d764B755fD} + init_fallback_gas: ${int:500000} setup: all_participants: ${list:["0x10E867Ac2Fb0Aa156ca81eF440a5cdf373bE1AaC"]} safe_contract_address: ${str:0x5e1D1eb61E1164D5a50b28C575dA73A29595dFf7} @@ -167,7 +182,7 @@ type: skill models: params: args: - agent_mech_contract_address: ${str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + agent_mech_contract_addresses: ${list:["0xFf82123dFB52ab75C417195c5fDB87630145ae81"]} task_deadline: ${float:240.0} file_hash_to_tools_json: ${list:[["bafybeibi34bhbvesmvd6o24jxvuldrwen4wj62na3lhva7k4afkg2shinu",["openai-text-davinci-002","openai-text-davinci-003","openai-gpt-3.5-turbo","openai-gpt-4"]],["bafybeiafdm3jctiz6wwo3rmo3vdubk7j7l5tumoxi5n5rc3x452mtkgyua",["stabilityai-stable-diffusion-v1-5","stabilityai-stable-diffusion-xl-beta-v2-2-2","stabilityai-stable-diffusion-512-v2-1","stabilityai-stable-diffusion-768-v2-1"]],["bafybeidpbnqbruzqlq424qt3i5dcvyqmcimshjilftabnrroujmjhdmteu",["transfer-native"]],["bafybeiglhy5epaytvt5qqdx77ld23ekouli53qrf2hjyebd5xghlunidfi",["prediction-online","prediction-offline"]]]} api_keys_json: ${list:[["openai", "dummy_api_key"],["stabilityai", "dummy_api_key"],["google_api_key", @@ -175,6 +190,8 @@ models: polling_interval: ${float:30.0} agent_index: ${int:0} num_agents: ${int:4} + from_block_range: ${int:50000} + timeout_limit: ${int:3} --- public_id: valory/ledger:0.19.0 type: connection @@ -185,3 +202,8 @@ config: chain_id: ${int:100} poa_chain: ${bool:false} default_gas_price_strategy: ${str:eip1559} + gnosis: + address: ${str:https://rpc.gnosischain.com/} + chain_id: ${int:100} + poa_chain: ${bool:false} + default_gas_price_strategy: ${str:eip1559} diff --git a/packages/valory/connections/websocket_client/connection.py b/packages/valory/connections/websocket_client/connection.py index 52012c35..025835ad 100644 --- a/packages/valory/connections/websocket_client/connection.py +++ b/packages/valory/connections/websocket_client/connection.py @@ -21,56 +21,279 @@ """Websocket client connection.""" import asyncio +import logging from concurrent.futures import ThreadPoolExecutor -from threading import Thread -from typing import Any, Optional +from typing import Any, Dict, Optional, cast import websocket from aea.configurations.base import PublicId from aea.connections.base import Connection, ConnectionStates from aea.mail.base import Envelope +from aea.protocols.base import Address, Message +from aea.protocols.dialogue.base import Dialogue -from packages.valory.protocols.default.message import DefaultMessage +from packages.valory.protocols.websocket_client.dialogues import WebsocketClientDialogue +from packages.valory.protocols.websocket_client.dialogues import ( + WebsocketClientDialogues as BaseWebsocketClientDialogues, +) +from packages.valory.protocols.websocket_client.message import WebsocketClientMessage PUBLIC_ID = PublicId.from_str("valory/websocket_client:0.1.0") +DEFAULT_MAX_RETRIES = 5 + + +class WebsocketClientDialogues(BaseWebsocketClientDialogues): + """A class to keep track of IPFS dialogues.""" + + def __init__(self, **kwargs: Any) -> None: + """ + Initialize dialogues. + + :param kwargs: keyword arguments + """ + + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> Dialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return WebsocketClientDialogue.Role.CONNECTION + + BaseWebsocketClientDialogues.__init__( + self, + self_address=str(kwargs.pop("connection_id")), + role_from_first_message=role_from_first_message, + **kwargs, + ) + + +class WebsocketSubcription: + """Websocket subscription.""" + + _wss: websocket.WebSocket + _outbox: Optional[asyncio.Queue] + _envelope: Envelope + _dialogue: WebsocketClientDialogue + + def __init__( + self, + subscription_id: str, + outbox: asyncio.Queue, + to: str, + sender: str, + logger: Optional[logging.Logger] = None, + loop: Optional[asyncio.AbstractEventLoop] = None, + executor: Optional[ThreadPoolExecutor] = None, + ) -> None: + """Create a websocket subscription.""" + + self._id = subscription_id + self._url = None + self._status = ConnectionStates.disconnected + + self._to = to + self._sender = sender + + self._outbox = outbox + self._executor = executor or ThreadPoolExecutor() + self._loop = loop or asyncio.get_running_loop() + + self.logger = logger or logging.getLogger() + + @property + def id(self) -> str: + """Websocket id.""" + return self._id + + @property + def url(self) -> str: + """Returns the URL""" + if self._url is None: + raise ValueError(f"URL not set for websocket subscription {self.id}") + return self._url + + @property + def status(self) -> ConnectionStates: + """Current status of the subscription.""" + return self._status + + def send(self, payload: str) -> int: + """Send and return send length.""" + + try: + return self._wss.send(payload=payload) + except websocket.WebSocketConnectionClosedException: + self._status = ConnectionStates.disconnected + return -1 + + async def recv(self) -> None: + """Run recv loop.""" + while self.status == ConnectionStates.connected: + try: + data = await self._loop.run_in_executor( + self._executor, + self._wss.recv, + ) + message = WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.RECV, + subscription_id=self.id, + data=data, + ) + except (websocket.WebSocketConnectionClosedException, OSError) as e: + self._status = ConnectionStates.disconnected + message = WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.ERROR, + message=f"Websocket connection disconnected with error {e}", + subscription_id=self.id, + alive=False, + ) + + await self._outbox.put( + Envelope( + to=self._to, # self._envelope.sender, + sender=self._sender, # self._envelope.to, + message=message, + ) + ) + + async def subscribe(self, url: str) -> "WebsocketSubcription": + """Subscribe to websocket.""" + # TODO: make these configurable + tries = 0 + sleep = 3 + while tries < DEFAULT_MAX_RETRIES: + try: + self._status = ConnectionStates.connecting + self._url = url + self._wss = await self._loop.run_in_executor( + self._executor, + websocket.create_connection, + self.url, + ) + self._status = ConnectionStates.connected + return self + except Exception as exception: # pylint: disable=W0718 + tries += 1 + self.logger.error( + f"Failed to establish WebSocket connection: {exception}; " + f"URL: {self.url}; Try: {tries}; Will retry in {sleep} ..." + ) + await asyncio.sleep(sleep) + + self._status = ConnectionStates.disconnected + return self + + async def unsubscribe(self, payload: str = "") -> "WebsocketSubcription": + """Unsubscribe from websocket.""" + self._status = ConnectionStates.disconnecting + await self._loop.run_in_executor( + self._executor, + self._wss.close, + websocket.STATUS_NORMAL, + payload.encode(), + ) + self._status = ConnectionStates.disconnected + + +class SubscriptionManager: + """Websocket subscription manager.""" + + _subscriptions: Dict[str, WebsocketSubcription] + + def __init__( + self, + outbox: asyncio.Queue, + logger: Optional[logging.Logger] = None, + loop: Optional[asyncio.AbstractEventLoop] = None, + executor: Optional[ThreadPoolExecutor] = None, + ) -> None: + """Websocket subscription manager.""" + + self._subscriptions = {} + self._executor = executor or ThreadPoolExecutor() + self._loop = loop or asyncio.get_running_loop() + self._outbox = outbox + + self.logger = logger or logging.getLogger() + + @property + def outbox(self) -> asyncio.Queue: + """Outbox.""" + return self._outbox + + def get(self, subscription_id: str) -> Optional[WebsocketSubcription]: + """Returns a subscription by id""" + return self._subscriptions.get( + subscription_id, + ) + + async def create_subscription( + self, + url: str, + subscription_id: str, + to: str, + sender: str, + ) -> "WebsocketSubcription": + """Create a websocket subscription.""" + self._subscriptions[subscription_id] = await WebsocketSubcription( + subscription_id=subscription_id, + to=to, + sender=sender, + outbox=self._outbox, + logger=self.logger, + loop=self._loop, + executor=self._executor, + ).subscribe( + url=url, + ) + return self._subscriptions[subscription_id] + + async def remove_subscription( + self, + subscription_id: int, + payload: str = "", + ) -> None: + """Create a websocket subscription.""" + subscription = self._subscriptions.pop(subscription_id, None) + if subscription is None: + return + await subscription.unsubscribe(payload=payload) + + async def remove_all_subscriptions(self) -> None: + """Remove all subscriptions""" + for sid, wss in self._subscriptions.items(): + self.logger.info(f"Unsubscribing from {sid}") + await wss.unsubscribe() + class WebSocketClient(Connection): # pylint: disable=Too many instance attributes """Proxy to the functionality of the SDK or API.""" connection_id = PUBLIC_ID - _new_messages: list - _endpoint: str - _wss: websocket.WebSocket + + _executor: ThreadPoolExecutor + _manager: SubscriptionManager + _outbox: asyncio.Queue MAX_RETRIES = 3 RETRY_DELAY = 5 # seconds def __init__(self, **kwargs: Any) -> None: - """ - Initialize the connection. - - The configuration must be specified if and only if the following - parameters are None: connection_id, excluded_protocols or restricted_to_protocols. + """Initialize the connection.""" + super().__init__(**kwargs) - Possible keyword arguments: - - configuration: the connection configuration. - - data_dir: directory where to put local files. - - identity: the identity object held by the agent. - - crypto_store: the crypto store for encrypted communication. - - restricted_to_protocols: the set of protocols ids of the only supported protocols for this connection. - - excluded_protocols: the set of protocols ids that we want to exclude for this connection. + self.dialogues = WebsocketClientDialogues(connection_id=PUBLIC_ID) - :param kwargs: keyword arguments passed to component base - """ - self._new_messages = [] - self._endpoint = kwargs["configuration"].config["endpoint"] - self._target_skill_id = kwargs["configuration"].config["target_skill_id"] - self._attempt_reconnect: bool = True - self._thread: Optional[Thread] = None - self._executor = ThreadPoolExecutor() - super().__init__(**kwargs) # pragma: no cover + @property + def manager(self) -> SubscriptionManager: + """Subscription manager.""" + return self._manager async def connect(self) -> None: """ @@ -78,46 +301,18 @@ async def connect(self) -> None: In the implementation, remember to update 'connection_status' accordingly. """ - self._attempt_reconnect = True - retries = 0 - while retries < self.MAX_RETRIES: - try: - self._wss = websocket.create_connection(self._endpoint) - self.state = ConnectionStates.connected - self.logger.info("Websocket connection established.") - return - except Exception as exception: # pylint: disable=W0718 - self.logger.error( - f"Failed to establish WebSocket connection: {exception}; Using endpoint: {self._endpoint}" - ) - retries += 1 - await asyncio.sleep(self.RETRY_DELAY) - self.state = ConnectionStates.disconnected - raise Exception( # pylint: disable=W0719 - f"Failed to establish connection after {self.MAX_RETRIES} attempts." + self._executor = ThreadPoolExecutor() + self._outbox = asyncio.Queue() + self._manager = SubscriptionManager( + outbox=self._outbox, + logger=self.logger, + loop=self.loop, + executor=self._executor, ) - async def _reconnect(self): - """Attempt to reconnect.""" - retries = 0 - # these are the retry attempts for reconnecting; also the called connection logic has its own retry logic - while retries < self.MAX_RETRIES: - try: - self.state = ConnectionStates.connecting - await self.connect() - self.logger.info("Reconnected successfully.") - return - except Exception as exception: # pylint: disable=W0718 - self.logger.error(f"Failed to reconnect: {exception}") - retries += 1 - await asyncio.sleep(self.RETRY_DELAY) - - self.state = ConnectionStates.disconnected - self.logger.error(f"Failed to reconnect after {self.MAX_RETRIES} attempts.") - raise Exception( # pylint: disable=W0719 - f"Failed to reconnect after {self.MAX_RETRIES} attempts." - ) + self.state = ConnectionStates.connected + self.logger.info("Websocket client established.") async def disconnect(self) -> None: """ @@ -125,34 +320,164 @@ async def disconnect(self) -> None: In the implementation, remember to update 'connection_status' accordingly. """ - self.logger.debug("Disconnecting...") # pragma: no cover - self._wss.close() - self._attempt_reconnect = False + + await self._manager.remove_all_subscriptions() + self._outbox.empty() self.state = ConnectionStates.disconnected - async def send(self, envelope: Envelope): + async def send(self, envelope: Envelope) -> None: """ Send an envelope. :param envelope: the envelope to send. """ - if self.state == ConnectionStates.disconnected: - raise Exception( # pylint: disable=W0719 - "Cannot receive message. Connection is not established." + + message = cast(WebsocketClientMessage, envelope.message) + dialogue = self.dialogues.update(message) + if message.performative == WebsocketClientMessage.Performative.SUBSCRIBE: + response = await self.ws_subscribe(message=message, dialogue=dialogue) + elif ( + message.performative + == WebsocketClientMessage.Performative.CHECK_SUBSCRIPTION + ): + response = self.ws_check_subscription(message=message, dialogue=dialogue) + elif message.performative == WebsocketClientMessage.Performative.SEND: + response = self.ws_send(message=message, dialogue=dialogue) + else: + raise ValueError(f"Invalid performative {message.performative}") + + await self._outbox.put( + Envelope( + to=envelope.sender, + sender=envelope.to, + message=response, + context=envelope.context, ) + ) - while self.is_connecting: - return + async def ws_subscribe( + self, + message: WebsocketClientMessage, + dialogue: WebsocketClientDialogue, + ) -> Envelope: + """Subscribe to a websocket.""" + wss = await self.manager.create_subscription( + url=message.url, + subscription_id=message.subscription_id, + to=message.sender, + sender=message.to, + ) + if wss.status != ConnectionStates.connected: + return self.error_message( + message=message, + dialogue=dialogue, + error=f"Error subscribing to the websocket with id {message.subscription_id}", + ) - self.logger.debug("Sending content from envelope...") - context = envelope.message.content - try: - self._wss.send(context) - except websocket.WebSocketConnectionClosedException: - self.logger.error("Websocket connection closed.") - self.state = ConnectionStates.disconnected + self.loop.create_task(wss.recv()) + if message.subscription_payload is not None: + wss.send(payload=message.subscription_payload) + + return cast( + WebsocketClientMessage, + dialogue.reply( + performative=WebsocketClientMessage.Performative.SUBSCRIPTION, + target_message=message, + subscription_id=wss.id, + alive=wss.status == ConnectionStates.connected, + ), + ) + + def ws_check_subscription( + self, + message: WebsocketClientMessage, + dialogue: WebsocketClientDialogue, + ) -> Envelope: + """Check websocket subscription.""" + wss = self.manager.get(subscription_id=message.subscription_id) + if wss is None: + return self.subscription_not_found_message( + message=message, dialogue=dialogue + ) + + return cast( + WebsocketClientMessage, + dialogue.reply( + performative=WebsocketClientMessage.Performative.SUBSCRIPTION, + target_message=message, + subscription_id=wss.id, + alive=wss.status == ConnectionStates.connected, + ), + ) + + def ws_send( + self, + message: WebsocketClientMessage, + dialogue: WebsocketClientDialogue, + ) -> Envelope: + """Send data to subscription.""" + wss = self.manager.get(subscription_id=message.subscription_id) + if wss is None: + return self.subscription_not_found_message( + message=message, dialogue=dialogue + ) + + send_length = wss.send(payload=message.payload) + if send_length > -1: + return cast( + WebsocketClientMessage, + dialogue.reply( + performative=WebsocketClientMessage.Performative.SEND_SUCCESS, + target_message=message, + subscription_id=message.subscription_id, + send_length=send_length, + ), + ) + + return self.error_message( + message=message, + dialogue=dialogue, + error=f"Error sending message; payload={message.payload}; subscription_id={message.subscription_id}", + alive=wss.status == ConnectionStates.connected, + ) + + def subscription_not_found_message( + self, + message: WebsocketClientMessage, + dialogue: WebsocketClientDialogue, + ) -> WebsocketClientMessage: + """Generate subscription not found message.""" + return cast( + WebsocketClientMessage, + dialogue.reply( + performative=WebsocketClientMessage.Performative.ERROR, + target_message=message, + message=f"Subscription with ID {message.subscription_id} does not exist", + subscription_id=message.subscription_id, + alive=False, + ), + ) + + def error_message( + self, + message: WebsocketClientMessage, + dialogue: WebsocketClientDialogue, + error: str, + alive: bool = False, + ) -> WebsocketClientMessage: + """Generate error message.""" + return cast( + WebsocketClientMessage, + dialogue.reply( + performative=WebsocketClientMessage.Performative.ERROR, + target_message=message, + subscription_id=message.subscription_id, + message=error, + alive=alive, + ), + ) - async def receive(self, *args: Any, **kwargs: Any): # noqa: V107 + async def receive(self, *args: Any, **kwargs: Any) -> Envelope: """ Receive an envelope. Blocking. @@ -161,30 +486,4 @@ async def receive(self, *args: Any, **kwargs: Any): # noqa: V107 :return: the envelope received, if present. # noqa: DAR202 """ - if self.state == ConnectionStates.disconnected: - raise Exception( # pylint: disable=W0719 - "Cannot receive message. Connection is not established." - ) - - while True: - try: - msg = await self.loop.run_in_executor( - executor=self._executor, func=self._wss.recv - ) - return self._from_wss_msg_to_envelope(msg) - except websocket.WebSocketConnectionClosedException: - self.logger.error("Websocket connection closed.") - self.state = ConnectionStates.disconnecting - await self.disconnect() - await self._reconnect() - - def _from_wss_msg_to_envelope(self, msg: str): - """Convert a message from the wss to an envelope.""" - msg = DefaultMessage( - performative=DefaultMessage.Performative.BYTES, - content=bytes(msg, "utf-8"), - ) - envelope = Envelope( - to=self._target_skill_id, sender=str(self.connection_id), message=msg - ) - return envelope + return await cast(Envelope, self._outbox.get()) diff --git a/packages/valory/connections/websocket_client/connection.yaml b/packages/valory/connections/websocket_client/connection.yaml index 1c12d94b..fef27027 100644 --- a/packages/valory/connections/websocket_client/connection.yaml +++ b/packages/valory/connections/websocket_client/connection.yaml @@ -8,7 +8,7 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeicyrebbic2h3ytyxeg776zelg2bpshcepnkm4qc5oypqqqfq3sqmq - connection.py: bafybeigj7ii4ewbibwc55i3ekji2gmbjgtpcqavmxrlzqf262xrm7ncfiy + connection.py: bafybeiakisvetjqs3qqlpdo5asm45gfygx35lw7cciypaneyqieyf6ul34 readme.md: bafybeihg5yfzgqvg5ngy7r2o5tfeqnelx2ffxw4po5hmheqjfhumpmxpoq fingerprint_ignore_patterns: [] connections: [] diff --git a/packages/valory/contracts/agent_mech/contract.py b/packages/valory/contracts/agent_mech/contract.py index a29cdd71..00392d8b 100644 --- a/packages/valory/contracts/agent_mech/contract.py +++ b/packages/valory/contracts/agent_mech/contract.py @@ -26,7 +26,59 @@ from aea.contracts.base import Contract from aea.crypto.base import LedgerApi from aea_ledger_ethereum import EthereumApi -from web3.types import BlockIdentifier +from web3.types import BlockIdentifier, TxReceipt + + +partial_abis = [ + [ + { + "anonymous": False, + "inputs": [ + { + "indexed": False, + "internalType": "uint256", + "name": "requestId", + "type": "uint256", + }, + { + "indexed": False, + "internalType": "bytes", + "name": "data", + "type": "bytes", + }, + ], + "name": "Deliver", + "type": "event", + } + ], + [ + { + "anonymous": False, + "inputs": [ + { + "indexed": True, + "internalType": "address", + "name": "sender", + "type": "address", + }, + { + "indexed": False, + "internalType": "uint256", + "name": "requestId", + "type": "uint256", + }, + { + "indexed": False, + "internalType": "bytes", + "name": "data", + "type": "bytes", + }, + ], + "name": "Deliver", + "type": "event", + } + ], +] class AgentMechContract(Contract): @@ -145,21 +197,39 @@ def get_deliver_events( ) -> JSONLike: """Get the Deliver events emitted by the contract.""" ledger_api = cast(EthereumApi, ledger_api) - contract_instance = cls.get_instance(ledger_api, contract_address) - entries = contract_instance.events.Deliver.create_filter( - fromBlock=from_block, - toBlock=to_block, - ).get_all_entries() + all_entries = [] + for abi in partial_abis: + contract_instance = ledger_api.api.eth.contract(contract_address, abi=abi) + entries = contract_instance.events.Deliver.create_filter( + fromBlock=from_block, + toBlock=to_block, + ).get_all_entries() + all_entries.extend(entries) + deliver_events = list( { "tx_hash": entry.transactionHash.hex(), "block_number": entry.blockNumber, **entry["args"], } - for entry in entries + for entry in all_entries ) return {"data": deliver_events} + @classmethod + def process_tx_receipt( + cls, + ledger_api: LedgerApi, + contract_address: str, + tx_receipt: TxReceipt, + ) -> JSONLike: + """Process transaction receipt to filter contract events.""" + + ledger_api = cast(EthereumApi, ledger_api) + contract_instance = cls.get_instance(ledger_api, contract_address) + event, *_ = contract_instance.events.Request().processReceipt(tx_receipt) + return dict(event["args"]) + @classmethod def get_undelivered_reqs( cls, @@ -196,11 +266,9 @@ def get_multiple_undelivered_reqs( ) -> JSONLike: """Get the requests that are not delivered.""" pending_tasks: List[Dict[str, Any]] = [] - # ensure we get the same range on all contracts - to_block = ledger_api.api.eth.block_number for contract_address in contract_addresses: pending_tasks_batch = cls.get_undelivered_reqs( - ledger_api, contract_address, from_block, to_block + ledger_api, contract_address, from_block ).get("data") pending_tasks.extend(pending_tasks_batch) return {"data": pending_tasks} diff --git a/packages/valory/contracts/agent_mech/contract.yaml b/packages/valory/contracts/agent_mech/contract.yaml index 7682fbfa..89f578b6 100644 --- a/packages/valory/contracts/agent_mech/contract.yaml +++ b/packages/valory/contracts/agent_mech/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeigpq5lxfj2aza6ok3fjuywtdafelkbvoqwaits7regfbgu4oynmku build/AgentMech.json: bafybeidrlu7vpusp2tzovyf5rbnqy2jicuq3e6czizfkzswjq4rjusu72i - contract.py: bafybeibexkfobufuooyzton3up7nq7alr2wlinkflegpsa7tnvafcdzk34 + contract.py: bafybeihm4bmidifvp225rpxtg5xu5dyd5k65gm33jkoms2brrshvu5lige fingerprint_ignore_patterns: [] class_name: AgentMechContract contract_interface_paths: diff --git a/packages/valory/protocols/__init__.py b/packages/valory/protocols/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/valory/protocols/websocket_client/README.md b/packages/valory/protocols/websocket_client/README.md new file mode 100644 index 00000000..b2417950 --- /dev/null +++ b/packages/valory/protocols/websocket_client/README.md @@ -0,0 +1,61 @@ +# Websocket Client Protocol + +## Description + +This is a protocol for communicating with websocket servers. + +## Specification + +```yaml +--- +name: websocket_client +author: valory +version: 0.1.0 +description: A protocol for websocket client. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +protocol_specification_id: valory/websocket_client:1.0.0 +speech_acts: + subscribe: + url: pt:str + subscription_id: pt:str + subscription_payload: pt:optional[pt:str] + subscription: + alive: pt:bool + subscription_id: pt:str + check_subscription: + alive: pt:bool + subscription_id: pt:str + send: + payload: pt:str + subscription_id: pt:str + send_success: + send_length: pt:int + subscription_id: pt:str + recv: + data: pt:str + subscription_id: pt:str + error: + alive: pt:bool + message: pt:str + subscription_id: pt:str +... +--- +initiation: [subscribe,check_subscription,send] +reply: + subscribe: [subscription, recv, error] + subscription: [] + check_subscription: [subscription, error] + send: [send_success, recv, error] + send_success: [] + recv: [] + error: [] +termination: [recv,send_success,subscription,error] +roles: {skill, connection} +end_states: [successful] +keep_terminal_state_dialogues: false +... +``` + +## Links + diff --git a/packages/valory/protocols/websocket_client/__init__.py b/packages/valory/protocols/websocket_client/__init__.py new file mode 100644 index 00000000..12836897 --- /dev/null +++ b/packages/valory/protocols/websocket_client/__init__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 valory +# +# 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 module contains the support resources for the websocket_client protocol. + +It was created with protocol buffer compiler version `libprotoc 3.19.4` and aea protocol generator version `1.0.0`. +""" + +from packages.valory.protocols.websocket_client.message import WebsocketClientMessage +from packages.valory.protocols.websocket_client.serialization import ( + WebsocketClientSerializer, +) + + +WebsocketClientMessage.serializer = WebsocketClientSerializer diff --git a/packages/valory/protocols/websocket_client/dialogues.py b/packages/valory/protocols/websocket_client/dialogues.py new file mode 100644 index 00000000..e360fc19 --- /dev/null +++ b/packages/valory/protocols/websocket_client/dialogues.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 valory +# +# 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 module contains the classes required for websocket_client dialogue management. + +- WebsocketClientDialogue: The dialogue class maintains state of a dialogue and manages it. +- WebsocketClientDialogues: The dialogues class keeps track of all dialogues. +""" + +from abc import ABC +from typing import Callable, Dict, FrozenSet, Type, cast + +from aea.common import Address +from aea.protocols.base import Message +from aea.protocols.dialogue.base import Dialogue, DialogueLabel, Dialogues + +from packages.valory.protocols.websocket_client.message import WebsocketClientMessage + + +class WebsocketClientDialogue(Dialogue): + """The websocket_client dialogue class maintains state of a dialogue and manages it.""" + + INITIAL_PERFORMATIVES: FrozenSet[Message.Performative] = frozenset( + { + WebsocketClientMessage.Performative.SUBSCRIBE, + WebsocketClientMessage.Performative.CHECK_SUBSCRIPTION, + WebsocketClientMessage.Performative.SEND, + } + ) + TERMINAL_PERFORMATIVES: FrozenSet[Message.Performative] = frozenset( + { + WebsocketClientMessage.Performative.RECV, + WebsocketClientMessage.Performative.SEND_SUCCESS, + WebsocketClientMessage.Performative.SUBSCRIPTION, + WebsocketClientMessage.Performative.ERROR, + } + ) + VALID_REPLIES: Dict[Message.Performative, FrozenSet[Message.Performative]] = { + WebsocketClientMessage.Performative.CHECK_SUBSCRIPTION: frozenset( + { + WebsocketClientMessage.Performative.SUBSCRIPTION, + WebsocketClientMessage.Performative.ERROR, + } + ), + WebsocketClientMessage.Performative.ERROR: frozenset(), + WebsocketClientMessage.Performative.RECV: frozenset(), + WebsocketClientMessage.Performative.SEND: frozenset( + { + WebsocketClientMessage.Performative.SEND_SUCCESS, + WebsocketClientMessage.Performative.RECV, + WebsocketClientMessage.Performative.ERROR, + } + ), + WebsocketClientMessage.Performative.SEND_SUCCESS: frozenset(), + WebsocketClientMessage.Performative.SUBSCRIBE: frozenset( + { + WebsocketClientMessage.Performative.SUBSCRIPTION, + WebsocketClientMessage.Performative.RECV, + WebsocketClientMessage.Performative.ERROR, + } + ), + WebsocketClientMessage.Performative.SUBSCRIPTION: frozenset(), + } + + class Role(Dialogue.Role): + """This class defines the agent's role in a websocket_client dialogue.""" + + CONNECTION = "connection" + SKILL = "skill" + + class EndState(Dialogue.EndState): + """This class defines the end states of a websocket_client dialogue.""" + + SUCCESSFUL = 0 + + def __init__( + self, + dialogue_label: DialogueLabel, + self_address: Address, + role: Dialogue.Role, + message_class: Type[WebsocketClientMessage] = WebsocketClientMessage, + ) -> None: + """ + Initialize a dialogue. + + :param dialogue_label: the identifier of the dialogue + :param self_address: the address of the entity for whom this dialogue is maintained + :param role: the role of the agent this dialogue is maintained for + :param message_class: the message class used + """ + Dialogue.__init__( + self, + dialogue_label=dialogue_label, + message_class=message_class, + self_address=self_address, + role=role, + ) + + +class WebsocketClientDialogues(Dialogues, ABC): + """This class keeps track of all websocket_client dialogues.""" + + END_STATES = frozenset({WebsocketClientDialogue.EndState.SUCCESSFUL}) + + _keep_terminal_state_dialogues = False + + def __init__( + self, + self_address: Address, + role_from_first_message: Callable[[Message, Address], Dialogue.Role], + dialogue_class: Type[WebsocketClientDialogue] = WebsocketClientDialogue, + ) -> None: + """ + Initialize dialogues. + + :param self_address: the address of the entity for whom dialogues are maintained + :param dialogue_class: the dialogue class used + :param role_from_first_message: the callable determining role from first message + """ + Dialogues.__init__( + self, + self_address=self_address, + end_states=cast(FrozenSet[Dialogue.EndState], self.END_STATES), + message_class=WebsocketClientMessage, + dialogue_class=dialogue_class, + role_from_first_message=role_from_first_message, + ) diff --git a/packages/valory/protocols/websocket_client/message.py b/packages/valory/protocols/websocket_client/message.py new file mode 100644 index 00000000..27ae91af --- /dev/null +++ b/packages/valory/protocols/websocket_client/message.py @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 valory +# +# 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 module contains websocket_client's message definition.""" + +# pylint: disable=too-many-statements,too-many-locals,no-member,too-few-public-methods,too-many-branches,not-an-iterable,unidiomatic-typecheck,unsubscriptable-object +import logging +from typing import Any, Optional, Set, Tuple, cast + +from aea.configurations.base import PublicId +from aea.exceptions import AEAEnforceError, enforce +from aea.protocols.base import Message + + +_default_logger = logging.getLogger( + "aea.packages.valory.protocols.websocket_client.message" +) + +DEFAULT_BODY_SIZE = 4 + + +class WebsocketClientMessage(Message): + """A protocol for websocket client.""" + + protocol_id = PublicId.from_str("valory/websocket_client:0.1.0") + protocol_specification_id = PublicId.from_str("valory/websocket_client:1.0.0") + + class Performative(Message.Performative): + """Performatives for the websocket_client protocol.""" + + CHECK_SUBSCRIPTION = "check_subscription" + ERROR = "error" + RECV = "recv" + SEND = "send" + SEND_SUCCESS = "send_success" + SUBSCRIBE = "subscribe" + SUBSCRIPTION = "subscription" + + def __str__(self) -> str: + """Get the string representation.""" + return str(self.value) + + _performatives = { + "check_subscription", + "error", + "recv", + "send", + "send_success", + "subscribe", + "subscription", + } + __slots__: Tuple[str, ...] = tuple() + + class _SlotsCls: + __slots__ = ( + "alive", + "data", + "dialogue_reference", + "message", + "message_id", + "payload", + "performative", + "send_length", + "subscription_id", + "subscription_payload", + "target", + "url", + ) + + def __init__( + self, + performative: Performative, + dialogue_reference: Tuple[str, str] = ("", ""), + message_id: int = 1, + target: int = 0, + **kwargs: Any, + ): + """ + Initialise an instance of WebsocketClientMessage. + + :param message_id: the message id. + :param dialogue_reference: the dialogue reference. + :param target: the message target. + :param performative: the message performative. + :param **kwargs: extra options. + """ + super().__init__( + dialogue_reference=dialogue_reference, + message_id=message_id, + target=target, + performative=WebsocketClientMessage.Performative(performative), + **kwargs, + ) + + @property + def valid_performatives(self) -> Set[str]: + """Get valid performatives.""" + return self._performatives + + @property + def dialogue_reference(self) -> Tuple[str, str]: + """Get the dialogue_reference of the message.""" + enforce(self.is_set("dialogue_reference"), "dialogue_reference is not set.") + return cast(Tuple[str, str], self.get("dialogue_reference")) + + @property + def message_id(self) -> int: + """Get the message_id of the message.""" + enforce(self.is_set("message_id"), "message_id is not set.") + return cast(int, self.get("message_id")) + + @property + def performative(self) -> Performative: # type: ignore # noqa: F821 + """Get the performative of the message.""" + enforce(self.is_set("performative"), "performative is not set.") + return cast(WebsocketClientMessage.Performative, self.get("performative")) + + @property + def target(self) -> int: + """Get the target of the message.""" + enforce(self.is_set("target"), "target is not set.") + return cast(int, self.get("target")) + + @property + def alive(self) -> bool: + """Get the 'alive' content from the message.""" + enforce(self.is_set("alive"), "'alive' content is not set.") + return cast(bool, self.get("alive")) + + @property + def data(self) -> str: + """Get the 'data' content from the message.""" + enforce(self.is_set("data"), "'data' content is not set.") + return cast(str, self.get("data")) + + @property + def message(self) -> str: + """Get the 'message' content from the message.""" + enforce(self.is_set("message"), "'message' content is not set.") + return cast(str, self.get("message")) + + @property + def payload(self) -> str: + """Get the 'payload' content from the message.""" + enforce(self.is_set("payload"), "'payload' content is not set.") + return cast(str, self.get("payload")) + + @property + def send_length(self) -> int: + """Get the 'send_length' content from the message.""" + enforce(self.is_set("send_length"), "'send_length' content is not set.") + return cast(int, self.get("send_length")) + + @property + def subscription_id(self) -> str: + """Get the 'subscription_id' content from the message.""" + enforce(self.is_set("subscription_id"), "'subscription_id' content is not set.") + return cast(str, self.get("subscription_id")) + + @property + def subscription_payload(self) -> Optional[str]: + """Get the 'subscription_payload' content from the message.""" + return cast(Optional[str], self.get("subscription_payload")) + + @property + def url(self) -> str: + """Get the 'url' content from the message.""" + enforce(self.is_set("url"), "'url' content is not set.") + return cast(str, self.get("url")) + + def _is_consistent(self) -> bool: + """Check that the message follows the websocket_client protocol.""" + try: + enforce( + isinstance(self.dialogue_reference, tuple), + "Invalid type for 'dialogue_reference'. Expected 'tuple'. Found '{}'.".format( + type(self.dialogue_reference) + ), + ) + enforce( + isinstance(self.dialogue_reference[0], str), + "Invalid type for 'dialogue_reference[0]'. Expected 'str'. Found '{}'.".format( + type(self.dialogue_reference[0]) + ), + ) + enforce( + isinstance(self.dialogue_reference[1], str), + "Invalid type for 'dialogue_reference[1]'. Expected 'str'. Found '{}'.".format( + type(self.dialogue_reference[1]) + ), + ) + enforce( + type(self.message_id) is int, + "Invalid type for 'message_id'. Expected 'int'. Found '{}'.".format( + type(self.message_id) + ), + ) + enforce( + type(self.target) is int, + "Invalid type for 'target'. Expected 'int'. Found '{}'.".format( + type(self.target) + ), + ) + + # Light Protocol Rule 2 + # Check correct performative + enforce( + isinstance(self.performative, WebsocketClientMessage.Performative), + "Invalid 'performative'. Expected either of '{}'. Found '{}'.".format( + self.valid_performatives, self.performative + ), + ) + + # Check correct contents + actual_nb_of_contents = len(self._body) - DEFAULT_BODY_SIZE + expected_nb_of_contents = 0 + if self.performative == WebsocketClientMessage.Performative.SUBSCRIBE: + expected_nb_of_contents = 2 + enforce( + isinstance(self.url, str), + "Invalid type for content 'url'. Expected 'str'. Found '{}'.".format( + type(self.url) + ), + ) + enforce( + isinstance(self.subscription_id, str), + "Invalid type for content 'subscription_id'. Expected 'str'. Found '{}'.".format( + type(self.subscription_id) + ), + ) + if self.is_set("subscription_payload"): + expected_nb_of_contents += 1 + subscription_payload = cast(str, self.subscription_payload) + enforce( + isinstance(subscription_payload, str), + "Invalid type for content 'subscription_payload'. Expected 'str'. Found '{}'.".format( + type(subscription_payload) + ), + ) + elif self.performative == WebsocketClientMessage.Performative.SUBSCRIPTION: + expected_nb_of_contents = 2 + enforce( + isinstance(self.alive, bool), + "Invalid type for content 'alive'. Expected 'bool'. Found '{}'.".format( + type(self.alive) + ), + ) + enforce( + isinstance(self.subscription_id, str), + "Invalid type for content 'subscription_id'. Expected 'str'. Found '{}'.".format( + type(self.subscription_id) + ), + ) + elif ( + self.performative + == WebsocketClientMessage.Performative.CHECK_SUBSCRIPTION + ): + expected_nb_of_contents = 2 + enforce( + isinstance(self.alive, bool), + "Invalid type for content 'alive'. Expected 'bool'. Found '{}'.".format( + type(self.alive) + ), + ) + enforce( + isinstance(self.subscription_id, str), + "Invalid type for content 'subscription_id'. Expected 'str'. Found '{}'.".format( + type(self.subscription_id) + ), + ) + elif self.performative == WebsocketClientMessage.Performative.SEND: + expected_nb_of_contents = 2 + enforce( + isinstance(self.payload, str), + "Invalid type for content 'payload'. Expected 'str'. Found '{}'.".format( + type(self.payload) + ), + ) + enforce( + isinstance(self.subscription_id, str), + "Invalid type for content 'subscription_id'. Expected 'str'. Found '{}'.".format( + type(self.subscription_id) + ), + ) + elif self.performative == WebsocketClientMessage.Performative.SEND_SUCCESS: + expected_nb_of_contents = 2 + enforce( + type(self.send_length) is int, + "Invalid type for content 'send_length'. Expected 'int'. Found '{}'.".format( + type(self.send_length) + ), + ) + enforce( + isinstance(self.subscription_id, str), + "Invalid type for content 'subscription_id'. Expected 'str'. Found '{}'.".format( + type(self.subscription_id) + ), + ) + elif self.performative == WebsocketClientMessage.Performative.RECV: + expected_nb_of_contents = 2 + enforce( + isinstance(self.data, str), + "Invalid type for content 'data'. Expected 'str'. Found '{}'.".format( + type(self.data) + ), + ) + enforce( + isinstance(self.subscription_id, str), + "Invalid type for content 'subscription_id'. Expected 'str'. Found '{}'.".format( + type(self.subscription_id) + ), + ) + elif self.performative == WebsocketClientMessage.Performative.ERROR: + expected_nb_of_contents = 3 + enforce( + isinstance(self.alive, bool), + "Invalid type for content 'alive'. Expected 'bool'. Found '{}'.".format( + type(self.alive) + ), + ) + enforce( + isinstance(self.message, str), + "Invalid type for content 'message'. Expected 'str'. Found '{}'.".format( + type(self.message) + ), + ) + enforce( + isinstance(self.subscription_id, str), + "Invalid type for content 'subscription_id'. Expected 'str'. Found '{}'.".format( + type(self.subscription_id) + ), + ) + + # Check correct content count + enforce( + expected_nb_of_contents == actual_nb_of_contents, + "Incorrect number of contents. Expected {}. Found {}".format( + expected_nb_of_contents, actual_nb_of_contents + ), + ) + + # Light Protocol Rule 3 + if self.message_id == 1: + enforce( + self.target == 0, + "Invalid 'target'. Expected 0 (because 'message_id' is 1). Found {}.".format( + self.target + ), + ) + except (AEAEnforceError, ValueError, KeyError) as e: + _default_logger.error(str(e)) + return False + + return True diff --git a/packages/valory/protocols/websocket_client/protocol.yaml b/packages/valory/protocols/websocket_client/protocol.yaml new file mode 100644 index 00000000..7872ef30 --- /dev/null +++ b/packages/valory/protocols/websocket_client/protocol.yaml @@ -0,0 +1,21 @@ +name: websocket_client +author: valory +version: 0.1.0 +protocol_specification_id: valory/websocket_client:1.0.0 +type: protocol +description: A protocol for websocket client. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + README.md: bafybeieot47xhqlvc3dwl2hc4yq3f6wcva7xf5dx5wz5n4ir6woknyqrsi + __init__.py: bafybeifhhqtcfac5lcrjxrcdpsrs3kyyxt7uf2qv2a4ivllyyt3c62q4aq + dialogues.py: bafybeienrjdbwqsx4dikqo3wjfgner5pz5qtbwfb2qtzsukn6iog6e4iyi + message.py: bafybeigpuhz4fw5ng3qay7duci7qhb4rj336wb2qwiyszddkjmnjebf72e + serialization.py: bafybeiar6guqw2qgofycbjuyhy7puxill5wcy6hsyrqmbkzcjzitxuws5i + tests/test_websocket_client_dialogues.py: bafybeig7cd7mllqmk6w4oaz7wqwubueaa4hpofgplhzeivqtwihalhpnne + tests/test_websocket_client_messages.py: bafybeibboz4j4itkwjh5ahza53v4d4dew3qxqq6fsbzragz76wnjin254u + websocket_client.proto: bafybeifqkmkqzbepxkj56o45ljysxujmoftjlm6pkllj7t4tbryqt42ql4 + websocket_client_pb2.py: bafybeickjzx5ikfg5hwisfiypifzvnp4dxecqrqf6op2gzcaojmuvkcdlq +fingerprint_ignore_patterns: [] +dependencies: + protobuf: {} diff --git a/packages/valory/protocols/websocket_client/serialization.py b/packages/valory/protocols/websocket_client/serialization.py new file mode 100644 index 00000000..18b74ec6 --- /dev/null +++ b/packages/valory/protocols/websocket_client/serialization.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 valory +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""Serialization module for websocket_client protocol.""" + +# pylint: disable=too-many-statements,too-many-locals,no-member,too-few-public-methods,redefined-builtin +from typing import Any, Dict, cast + +from aea.mail.base_pb2 import DialogueMessage +from aea.mail.base_pb2 import Message as ProtobufMessage +from aea.protocols.base import Message, Serializer + +from packages.valory.protocols.websocket_client import websocket_client_pb2 +from packages.valory.protocols.websocket_client.message import WebsocketClientMessage + + +class WebsocketClientSerializer(Serializer): + """Serialization for the 'websocket_client' protocol.""" + + @staticmethod + def encode(msg: Message) -> bytes: + """ + Encode a 'WebsocketClient' message into bytes. + + :param msg: the message object. + :return: the bytes. + """ + msg = cast(WebsocketClientMessage, msg) + message_pb = ProtobufMessage() + dialogue_message_pb = DialogueMessage() + websocket_client_msg = websocket_client_pb2.WebsocketClientMessage() + + dialogue_message_pb.message_id = msg.message_id + dialogue_reference = msg.dialogue_reference + dialogue_message_pb.dialogue_starter_reference = dialogue_reference[0] + dialogue_message_pb.dialogue_responder_reference = dialogue_reference[1] + dialogue_message_pb.target = msg.target + + performative_id = msg.performative + if performative_id == WebsocketClientMessage.Performative.SUBSCRIBE: + performative = websocket_client_pb2.WebsocketClientMessage.Subscribe_Performative() # type: ignore + url = msg.url + performative.url = url + subscription_id = msg.subscription_id + performative.subscription_id = subscription_id + if msg.is_set("subscription_payload"): + performative.subscription_payload_is_set = True + subscription_payload = msg.subscription_payload + performative.subscription_payload = subscription_payload + websocket_client_msg.subscribe.CopyFrom(performative) + elif performative_id == WebsocketClientMessage.Performative.SUBSCRIPTION: + performative = websocket_client_pb2.WebsocketClientMessage.Subscription_Performative() # type: ignore + alive = msg.alive + performative.alive = alive + subscription_id = msg.subscription_id + performative.subscription_id = subscription_id + websocket_client_msg.subscription.CopyFrom(performative) + elif performative_id == WebsocketClientMessage.Performative.CHECK_SUBSCRIPTION: + performative = websocket_client_pb2.WebsocketClientMessage.Check_Subscription_Performative() # type: ignore + alive = msg.alive + performative.alive = alive + subscription_id = msg.subscription_id + performative.subscription_id = subscription_id + websocket_client_msg.check_subscription.CopyFrom(performative) + elif performative_id == WebsocketClientMessage.Performative.SEND: + performative = websocket_client_pb2.WebsocketClientMessage.Send_Performative() # type: ignore + payload = msg.payload + performative.payload = payload + subscription_id = msg.subscription_id + performative.subscription_id = subscription_id + websocket_client_msg.send.CopyFrom(performative) + elif performative_id == WebsocketClientMessage.Performative.SEND_SUCCESS: + performative = websocket_client_pb2.WebsocketClientMessage.Send_Success_Performative() # type: ignore + send_length = msg.send_length + performative.send_length = send_length + subscription_id = msg.subscription_id + performative.subscription_id = subscription_id + websocket_client_msg.send_success.CopyFrom(performative) + elif performative_id == WebsocketClientMessage.Performative.RECV: + performative = websocket_client_pb2.WebsocketClientMessage.Recv_Performative() # type: ignore + data = msg.data + performative.data = data + subscription_id = msg.subscription_id + performative.subscription_id = subscription_id + websocket_client_msg.recv.CopyFrom(performative) + elif performative_id == WebsocketClientMessage.Performative.ERROR: + performative = websocket_client_pb2.WebsocketClientMessage.Error_Performative() # type: ignore + alive = msg.alive + performative.alive = alive + message = msg.message + performative.message = message + subscription_id = msg.subscription_id + performative.subscription_id = subscription_id + websocket_client_msg.error.CopyFrom(performative) + else: + raise ValueError("Performative not valid: {}".format(performative_id)) + + dialogue_message_pb.content = websocket_client_msg.SerializeToString() + + message_pb.dialogue_message.CopyFrom(dialogue_message_pb) + message_bytes = message_pb.SerializeToString() + return message_bytes + + @staticmethod + def decode(obj: bytes) -> Message: + """ + Decode bytes into a 'WebsocketClient' message. + + :param obj: the bytes object. + :return: the 'WebsocketClient' message. + """ + message_pb = ProtobufMessage() + websocket_client_pb = websocket_client_pb2.WebsocketClientMessage() + message_pb.ParseFromString(obj) + message_id = message_pb.dialogue_message.message_id + dialogue_reference = ( + message_pb.dialogue_message.dialogue_starter_reference, + message_pb.dialogue_message.dialogue_responder_reference, + ) + target = message_pb.dialogue_message.target + + websocket_client_pb.ParseFromString(message_pb.dialogue_message.content) + performative = websocket_client_pb.WhichOneof("performative") + performative_id = WebsocketClientMessage.Performative(str(performative)) + performative_content = dict() # type: Dict[str, Any] + if performative_id == WebsocketClientMessage.Performative.SUBSCRIBE: + url = websocket_client_pb.subscribe.url + performative_content["url"] = url + subscription_id = websocket_client_pb.subscribe.subscription_id + performative_content["subscription_id"] = subscription_id + if websocket_client_pb.subscribe.subscription_payload_is_set: + subscription_payload = ( + websocket_client_pb.subscribe.subscription_payload + ) + performative_content["subscription_payload"] = subscription_payload + elif performative_id == WebsocketClientMessage.Performative.SUBSCRIPTION: + alive = websocket_client_pb.subscription.alive + performative_content["alive"] = alive + subscription_id = websocket_client_pb.subscription.subscription_id + performative_content["subscription_id"] = subscription_id + elif performative_id == WebsocketClientMessage.Performative.CHECK_SUBSCRIPTION: + alive = websocket_client_pb.check_subscription.alive + performative_content["alive"] = alive + subscription_id = websocket_client_pb.check_subscription.subscription_id + performative_content["subscription_id"] = subscription_id + elif performative_id == WebsocketClientMessage.Performative.SEND: + payload = websocket_client_pb.send.payload + performative_content["payload"] = payload + subscription_id = websocket_client_pb.send.subscription_id + performative_content["subscription_id"] = subscription_id + elif performative_id == WebsocketClientMessage.Performative.SEND_SUCCESS: + send_length = websocket_client_pb.send_success.send_length + performative_content["send_length"] = send_length + subscription_id = websocket_client_pb.send_success.subscription_id + performative_content["subscription_id"] = subscription_id + elif performative_id == WebsocketClientMessage.Performative.RECV: + data = websocket_client_pb.recv.data + performative_content["data"] = data + subscription_id = websocket_client_pb.recv.subscription_id + performative_content["subscription_id"] = subscription_id + elif performative_id == WebsocketClientMessage.Performative.ERROR: + alive = websocket_client_pb.error.alive + performative_content["alive"] = alive + message = websocket_client_pb.error.message + performative_content["message"] = message + subscription_id = websocket_client_pb.error.subscription_id + performative_content["subscription_id"] = subscription_id + else: + raise ValueError("Performative not valid: {}.".format(performative_id)) + + return WebsocketClientMessage( + message_id=message_id, + dialogue_reference=dialogue_reference, + target=target, + performative=performative, + **performative_content + ) diff --git a/packages/valory/protocols/websocket_client/tests/test_websocket_client_dialogues.py b/packages/valory/protocols/websocket_client/tests/test_websocket_client_dialogues.py new file mode 100644 index 00000000..3f7eb0c8 --- /dev/null +++ b/packages/valory/protocols/websocket_client/tests/test_websocket_client_dialogues.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 valory +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""Test dialogues module for websocket_client protocol.""" + +# pylint: disable=too-many-statements,too-many-locals,no-member,too-few-public-methods,redefined-builtin +from aea.test_tools.test_protocol import BaseProtocolDialoguesTestCase + +from packages.valory.protocols.websocket_client.dialogues import ( + WebsocketClientDialogue, + WebsocketClientDialogues, +) +from packages.valory.protocols.websocket_client.message import WebsocketClientMessage + + +class TestDialoguesWebsocketClient(BaseProtocolDialoguesTestCase): + """Test for the 'websocket_client' protocol dialogues.""" + + MESSAGE_CLASS = WebsocketClientMessage + + DIALOGUE_CLASS = WebsocketClientDialogue + + DIALOGUES_CLASS = WebsocketClientDialogues + + ROLE_FOR_THE_FIRST_MESSAGE = WebsocketClientDialogue.Role.CONNECTION # CHECK + + def make_message_content(self) -> dict: + """Make a dict with message contruction content for dialogues.create.""" + return dict( + performative=WebsocketClientMessage.Performative.SUBSCRIBE, + url="some str", + subscription_id="some str", + subscription_payload="some str", + ) diff --git a/packages/valory/protocols/websocket_client/tests/test_websocket_client_messages.py b/packages/valory/protocols/websocket_client/tests/test_websocket_client_messages.py new file mode 100644 index 00000000..f0f7b63c --- /dev/null +++ b/packages/valory/protocols/websocket_client/tests/test_websocket_client_messages.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 valory +# +# 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. +# +# ------------------------------------------------------------------------------ + +"""Test messages module for websocket_client protocol.""" + +# pylint: disable=too-many-statements,too-many-locals,no-member,too-few-public-methods,redefined-builtin +from typing import List + +from aea.test_tools.test_protocol import BaseProtocolMessagesTestCase + +from packages.valory.protocols.websocket_client.message import WebsocketClientMessage + + +class TestMessageWebsocketClient(BaseProtocolMessagesTestCase): + """Test for the 'websocket_client' protocol message.""" + + MESSAGE_CLASS = WebsocketClientMessage + + def build_messages(self) -> List[WebsocketClientMessage]: # type: ignore[override] + """Build the messages to be used for testing.""" + return [ + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.SUBSCRIBE, + url="some str", + subscription_id="some str", + subscription_payload="some str", + ), + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.SUBSCRIPTION, + alive=True, + subscription_id="some str", + ), + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.CHECK_SUBSCRIPTION, + alive=True, + subscription_id="some str", + ), + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.SEND, + payload="some str", + subscription_id="some str", + ), + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.SEND_SUCCESS, + send_length=12, + subscription_id="some str", + ), + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.RECV, + data="some str", + subscription_id="some str", + ), + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.ERROR, + alive=True, + message="some str", + subscription_id="some str", + ), + ] + + def build_inconsistent(self) -> List[WebsocketClientMessage]: # type: ignore[override] + """Build inconsistent messages to be used for testing.""" + return [ + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.SUBSCRIBE, + # skip content: url + subscription_id="some str", + subscription_payload="some str", + ), + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.SUBSCRIPTION, + # skip content: alive + subscription_id="some str", + ), + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.CHECK_SUBSCRIPTION, + # skip content: alive + subscription_id="some str", + ), + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.SEND, + # skip content: payload + subscription_id="some str", + ), + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.SEND_SUCCESS, + # skip content: send_length + subscription_id="some str", + ), + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.RECV, + # skip content: data + subscription_id="some str", + ), + WebsocketClientMessage( + performative=WebsocketClientMessage.Performative.ERROR, + # skip content: alive + message="some str", + subscription_id="some str", + ), + ] diff --git a/packages/valory/protocols/websocket_client/websocket_client.proto b/packages/valory/protocols/websocket_client/websocket_client.proto new file mode 100644 index 00000000..c28a8741 --- /dev/null +++ b/packages/valory/protocols/websocket_client/websocket_client.proto @@ -0,0 +1,56 @@ +syntax = "proto3"; + +package aea.valory.websocket_client.v1_0_0; + +message WebsocketClientMessage{ + + // Performatives and contents + message Subscribe_Performative{ + string url = 1; + string subscription_id = 2; + string subscription_payload = 3; + bool subscription_payload_is_set = 4; + } + + message Subscription_Performative{ + bool alive = 1; + string subscription_id = 2; + } + + message Check_Subscription_Performative{ + bool alive = 1; + string subscription_id = 2; + } + + message Send_Performative{ + string payload = 1; + string subscription_id = 2; + } + + message Send_Success_Performative{ + int32 send_length = 1; + string subscription_id = 2; + } + + message Recv_Performative{ + string data = 1; + string subscription_id = 2; + } + + message Error_Performative{ + bool alive = 1; + string message = 2; + string subscription_id = 3; + } + + + oneof performative{ + Check_Subscription_Performative check_subscription = 5; + Error_Performative error = 6; + Recv_Performative recv = 7; + Send_Performative send = 8; + Send_Success_Performative send_success = 9; + Subscribe_Performative subscribe = 10; + Subscription_Performative subscription = 11; + } +} diff --git a/packages/valory/protocols/websocket_client/websocket_client_pb2.py b/packages/valory/protocols/websocket_client/websocket_client_pb2.py new file mode 100644 index 00000000..e12dab5a --- /dev/null +++ b/packages/valory/protocols/websocket_client/websocket_client_pb2.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: websocket_client.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\x16websocket_client.proto\x12"aea.valory.websocket_client.v1_0_0"\xab\n\n\x16WebsocketClientMessage\x12x\n\x12\x63heck_subscription\x18\x05 \x01(\x0b\x32Z.aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Check_Subscription_PerformativeH\x00\x12^\n\x05\x65rror\x18\x06 \x01(\x0b\x32M.aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Error_PerformativeH\x00\x12\\\n\x04recv\x18\x07 \x01(\x0b\x32L.aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Recv_PerformativeH\x00\x12\\\n\x04send\x18\x08 \x01(\x0b\x32L.aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Send_PerformativeH\x00\x12l\n\x0csend_success\x18\t \x01(\x0b\x32T.aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Send_Success_PerformativeH\x00\x12\x66\n\tsubscribe\x18\n \x01(\x0b\x32Q.aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Subscribe_PerformativeH\x00\x12l\n\x0csubscription\x18\x0b \x01(\x0b\x32T.aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Subscription_PerformativeH\x00\x1a\x81\x01\n\x16Subscribe_Performative\x12\x0b\n\x03url\x18\x01 \x01(\t\x12\x17\n\x0fsubscription_id\x18\x02 \x01(\t\x12\x1c\n\x14subscription_payload\x18\x03 \x01(\t\x12#\n\x1bsubscription_payload_is_set\x18\x04 \x01(\x08\x1a\x43\n\x19Subscription_Performative\x12\r\n\x05\x61live\x18\x01 \x01(\x08\x12\x17\n\x0fsubscription_id\x18\x02 \x01(\t\x1aI\n\x1f\x43heck_Subscription_Performative\x12\r\n\x05\x61live\x18\x01 \x01(\x08\x12\x17\n\x0fsubscription_id\x18\x02 \x01(\t\x1a=\n\x11Send_Performative\x12\x0f\n\x07payload\x18\x01 \x01(\t\x12\x17\n\x0fsubscription_id\x18\x02 \x01(\t\x1aI\n\x19Send_Success_Performative\x12\x13\n\x0bsend_length\x18\x01 \x01(\x05\x12\x17\n\x0fsubscription_id\x18\x02 \x01(\t\x1a:\n\x11Recv_Performative\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\t\x12\x17\n\x0fsubscription_id\x18\x02 \x01(\t\x1aM\n\x12\x45rror_Performative\x12\r\n\x05\x61live\x18\x01 \x01(\x08\x12\x0f\n\x07message\x18\x02 \x01(\t\x12\x17\n\x0fsubscription_id\x18\x03 \x01(\tB\x0e\n\x0cperformativeb\x06proto3' +) + + +_WEBSOCKETCLIENTMESSAGE = DESCRIPTOR.message_types_by_name["WebsocketClientMessage"] +_WEBSOCKETCLIENTMESSAGE_SUBSCRIBE_PERFORMATIVE = ( + _WEBSOCKETCLIENTMESSAGE.nested_types_by_name["Subscribe_Performative"] +) +_WEBSOCKETCLIENTMESSAGE_SUBSCRIPTION_PERFORMATIVE = ( + _WEBSOCKETCLIENTMESSAGE.nested_types_by_name["Subscription_Performative"] +) +_WEBSOCKETCLIENTMESSAGE_CHECK_SUBSCRIPTION_PERFORMATIVE = ( + _WEBSOCKETCLIENTMESSAGE.nested_types_by_name["Check_Subscription_Performative"] +) +_WEBSOCKETCLIENTMESSAGE_SEND_PERFORMATIVE = ( + _WEBSOCKETCLIENTMESSAGE.nested_types_by_name["Send_Performative"] +) +_WEBSOCKETCLIENTMESSAGE_SEND_SUCCESS_PERFORMATIVE = ( + _WEBSOCKETCLIENTMESSAGE.nested_types_by_name["Send_Success_Performative"] +) +_WEBSOCKETCLIENTMESSAGE_RECV_PERFORMATIVE = ( + _WEBSOCKETCLIENTMESSAGE.nested_types_by_name["Recv_Performative"] +) +_WEBSOCKETCLIENTMESSAGE_ERROR_PERFORMATIVE = ( + _WEBSOCKETCLIENTMESSAGE.nested_types_by_name["Error_Performative"] +) +WebsocketClientMessage = _reflection.GeneratedProtocolMessageType( + "WebsocketClientMessage", + (_message.Message,), + { + "Subscribe_Performative": _reflection.GeneratedProtocolMessageType( + "Subscribe_Performative", + (_message.Message,), + { + "DESCRIPTOR": _WEBSOCKETCLIENTMESSAGE_SUBSCRIBE_PERFORMATIVE, + "__module__": "websocket_client_pb2" + # @@protoc_insertion_point(class_scope:aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Subscribe_Performative) + }, + ), + "Subscription_Performative": _reflection.GeneratedProtocolMessageType( + "Subscription_Performative", + (_message.Message,), + { + "DESCRIPTOR": _WEBSOCKETCLIENTMESSAGE_SUBSCRIPTION_PERFORMATIVE, + "__module__": "websocket_client_pb2" + # @@protoc_insertion_point(class_scope:aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Subscription_Performative) + }, + ), + "Check_Subscription_Performative": _reflection.GeneratedProtocolMessageType( + "Check_Subscription_Performative", + (_message.Message,), + { + "DESCRIPTOR": _WEBSOCKETCLIENTMESSAGE_CHECK_SUBSCRIPTION_PERFORMATIVE, + "__module__": "websocket_client_pb2" + # @@protoc_insertion_point(class_scope:aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Check_Subscription_Performative) + }, + ), + "Send_Performative": _reflection.GeneratedProtocolMessageType( + "Send_Performative", + (_message.Message,), + { + "DESCRIPTOR": _WEBSOCKETCLIENTMESSAGE_SEND_PERFORMATIVE, + "__module__": "websocket_client_pb2" + # @@protoc_insertion_point(class_scope:aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Send_Performative) + }, + ), + "Send_Success_Performative": _reflection.GeneratedProtocolMessageType( + "Send_Success_Performative", + (_message.Message,), + { + "DESCRIPTOR": _WEBSOCKETCLIENTMESSAGE_SEND_SUCCESS_PERFORMATIVE, + "__module__": "websocket_client_pb2" + # @@protoc_insertion_point(class_scope:aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Send_Success_Performative) + }, + ), + "Recv_Performative": _reflection.GeneratedProtocolMessageType( + "Recv_Performative", + (_message.Message,), + { + "DESCRIPTOR": _WEBSOCKETCLIENTMESSAGE_RECV_PERFORMATIVE, + "__module__": "websocket_client_pb2" + # @@protoc_insertion_point(class_scope:aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Recv_Performative) + }, + ), + "Error_Performative": _reflection.GeneratedProtocolMessageType( + "Error_Performative", + (_message.Message,), + { + "DESCRIPTOR": _WEBSOCKETCLIENTMESSAGE_ERROR_PERFORMATIVE, + "__module__": "websocket_client_pb2" + # @@protoc_insertion_point(class_scope:aea.valory.websocket_client.v1_0_0.WebsocketClientMessage.Error_Performative) + }, + ), + "DESCRIPTOR": _WEBSOCKETCLIENTMESSAGE, + "__module__": "websocket_client_pb2" + # @@protoc_insertion_point(class_scope:aea.valory.websocket_client.v1_0_0.WebsocketClientMessage) + }, +) +_sym_db.RegisterMessage(WebsocketClientMessage) +_sym_db.RegisterMessage(WebsocketClientMessage.Subscribe_Performative) +_sym_db.RegisterMessage(WebsocketClientMessage.Subscription_Performative) +_sym_db.RegisterMessage(WebsocketClientMessage.Check_Subscription_Performative) +_sym_db.RegisterMessage(WebsocketClientMessage.Send_Performative) +_sym_db.RegisterMessage(WebsocketClientMessage.Send_Success_Performative) +_sym_db.RegisterMessage(WebsocketClientMessage.Recv_Performative) +_sym_db.RegisterMessage(WebsocketClientMessage.Error_Performative) + +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _WEBSOCKETCLIENTMESSAGE._serialized_start = 63 + _WEBSOCKETCLIENTMESSAGE._serialized_end = 1386 + _WEBSOCKETCLIENTMESSAGE_SUBSCRIBE_PERFORMATIVE._serialized_start = 820 + _WEBSOCKETCLIENTMESSAGE_SUBSCRIBE_PERFORMATIVE._serialized_end = 949 + _WEBSOCKETCLIENTMESSAGE_SUBSCRIPTION_PERFORMATIVE._serialized_start = 951 + _WEBSOCKETCLIENTMESSAGE_SUBSCRIPTION_PERFORMATIVE._serialized_end = 1018 + _WEBSOCKETCLIENTMESSAGE_CHECK_SUBSCRIPTION_PERFORMATIVE._serialized_start = 1020 + _WEBSOCKETCLIENTMESSAGE_CHECK_SUBSCRIPTION_PERFORMATIVE._serialized_end = 1093 + _WEBSOCKETCLIENTMESSAGE_SEND_PERFORMATIVE._serialized_start = 1095 + _WEBSOCKETCLIENTMESSAGE_SEND_PERFORMATIVE._serialized_end = 1156 + _WEBSOCKETCLIENTMESSAGE_SEND_SUCCESS_PERFORMATIVE._serialized_start = 1158 + _WEBSOCKETCLIENTMESSAGE_SEND_SUCCESS_PERFORMATIVE._serialized_end = 1231 + _WEBSOCKETCLIENTMESSAGE_RECV_PERFORMATIVE._serialized_start = 1233 + _WEBSOCKETCLIENTMESSAGE_RECV_PERFORMATIVE._serialized_end = 1291 + _WEBSOCKETCLIENTMESSAGE_ERROR_PERFORMATIVE._serialized_start = 1293 + _WEBSOCKETCLIENTMESSAGE_ERROR_PERFORMATIVE._serialized_end = 1370 +# @@protoc_insertion_point(module_scope) diff --git a/packages/valory/services/mech/service.yaml b/packages/valory/services/mech/service.yaml index 2a85ff1c..b11b79d5 100644 --- a/packages/valory/services/mech/service.yaml +++ b/packages/valory/services/mech/service.yaml @@ -7,7 +7,7 @@ license: Apache-2.0 fingerprint: README.md: bafybeif7ia4jdlazy6745ke2k2x5yoqlwsgwr6sbztbgqtwvs3ndm2p7ba fingerprint_ignore_patterns: [] -agent: valory/mech:0.1.0:bafybeidsxvvyks3z5lq7ejkb4rj2ncqpxtqksj7xi2favs7u72syple4ji +agent: valory/mech:0.1.0:bafybeiag6kzwog6zgdsk5v3sk6m636c67fipbcxuajfkcoozdt2knlt5da number_of_agents: 4 deployment: agent: @@ -43,13 +43,16 @@ type: skill tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} termination_sleep: ${TERMINATION_SLEEP:int:900} use_termination: ${USE_TERMINATION:bool:false} - agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + agent_mech_contract_addresses: ${AGENT_MECH_CONTRACT_ADDRESSES:list:["0xFf82123dFB52ab75C417195c5fDB87630145ae81","0x77af31De935740567Cf4fF1986D04B2c964A786a"]} reset_period_count: ${RESET_PERIOD_COUNT:int:1000} use_slashing: ${USE_SLASHING:bool:false} slash_cooldown_hours: ${SLASH_COOLDOWN_HOURS:int:3} slash_threshold_amount: ${SLASH_THRESHOLD_AMOUNT:int:10000000000000000} light_slash_unit_amount: ${LIGHT_SLASH_UNIT_AMOUNT:int:5000000000000000} serious_slash_unit_amount: ${SERIOUS_SLASH_UNIT_AMOUNT:int:8000000000000000} + agent_registry_address: ${AGENT_REGISTRY_ADDRESS:str:0xE49CB081e8d96920C38aA7AB90cb0294ab4Bc8EA} + agent_id: ${AGENT_ID:int:3} + metadata_hash: ${METADATA_HASH:str:f01701220caa53607238e340da63b296acab232c18a48e954f0af6ff2b835b2d93f1962f0} 1: models: params: @@ -67,13 +70,16 @@ type: skill tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} termination_sleep: ${TERMINATION_SLEEP:int:900} use_termination: ${USE_TERMINATION:bool:false} - agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + agent_mech_contract_addresses: ${AGENT_MECH_CONTRACT_ADDRESSES:list:["0xFf82123dFB52ab75C417195c5fDB87630145ae81","0x77af31De935740567Cf4fF1986D04B2c964A786a"]} reset_period_count: ${RESET_PERIOD_COUNT:int:1000} use_slashing: ${USE_SLASHING:bool:false} slash_cooldown_hours: ${SLASH_COOLDOWN_HOURS:int:3} slash_threshold_amount: ${SLASH_THRESHOLD_AMOUNT:int:10000000000000000} light_slash_unit_amount: ${LIGHT_SLASH_UNIT_AMOUNT:int:5000000000000000} serious_slash_unit_amount: ${SERIOUS_SLASH_UNIT_AMOUNT:int:8000000000000000} + agent_registry_address: ${AGENT_REGISTRY_ADDRESS:str:0xE49CB081e8d96920C38aA7AB90cb0294ab4Bc8EA} + agent_id: ${AGENT_ID:int:3} + metadata_hash: ${METADATA_HASH:str:f01701220caa53607238e340da63b296acab232c18a48e954f0af6ff2b835b2d93f1962f0} 2: models: params: @@ -91,13 +97,16 @@ type: skill tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} termination_sleep: ${TERMINATION_SLEEP:int:900} use_termination: ${USE_TERMINATION:bool:false} - agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + agent_mech_contract_addresses: ${AGENT_MECH_CONTRACT_ADDRESSES:list:["0xFf82123dFB52ab75C417195c5fDB87630145ae81","0x77af31De935740567Cf4fF1986D04B2c964A786a"]} reset_period_count: ${RESET_PERIOD_COUNT:int:1000} use_slashing: ${USE_SLASHING:bool:false} slash_cooldown_hours: ${SLASH_COOLDOWN_HOURS:int:3} slash_threshold_amount: ${SLASH_THRESHOLD_AMOUNT:int:10000000000000000} light_slash_unit_amount: ${LIGHT_SLASH_UNIT_AMOUNT:int:5000000000000000} serious_slash_unit_amount: ${SERIOUS_SLASH_UNIT_AMOUNT:int:8000000000000000} + agent_registry_address: ${AGENT_REGISTRY_ADDRESS:str:0xE49CB081e8d96920C38aA7AB90cb0294ab4Bc8EA} + agent_id: ${AGENT_ID:int:3} + metadata_hash: ${METADATA_HASH:str:f01701220caa53607238e340da63b296acab232c18a48e954f0af6ff2b835b2d93f1962f0} 3: models: params: @@ -115,13 +124,16 @@ type: skill tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} termination_sleep: ${TERMINATION_SLEEP:int:900} use_termination: ${USE_TERMINATION:bool:false} - agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + agent_mech_contract_addresses: ${AGENT_MECH_CONTRACT_ADDRESSES:list:["0xFf82123dFB52ab75C417195c5fDB87630145ae81","0x77af31De935740567Cf4fF1986D04B2c964A786a"]} reset_period_count: ${RESET_PERIOD_COUNT:int:1000} use_slashing: ${USE_SLASHING:bool:false} slash_cooldown_hours: ${SLASH_COOLDOWN_HOURS:int:3} slash_threshold_amount: ${SLASH_THRESHOLD_AMOUNT:int:10000000000000000} light_slash_unit_amount: ${LIGHT_SLASH_UNIT_AMOUNT:int:5000000000000000} serious_slash_unit_amount: ${SERIOUS_SLASH_UNIT_AMOUNT:int:8000000000000000} + agent_registry_address: ${AGENT_REGISTRY_ADDRESS:str:0xE49CB081e8d96920C38aA7AB90cb0294ab4Bc8EA} + agent_id: ${AGENT_ID:int:3} + metadata_hash: ${METADATA_HASH:str:f01701220caa53607238e340da63b296acab232c18a48e954f0af6ff2b835b2d93f1962f0} --- public_id: valory/task_execution:0.1.0 type: skill @@ -129,46 +141,50 @@ type: skill models: params: args: - agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + agent_mech_contract_addresses: ${AGENT_MECH_CONTRACT_ADDRESSES:list:["0xFf82123dFB52ab75C417195c5fDB87630145ae81","0x77af31De935740567Cf4fF1986D04B2c964A786a"]} task_deadline: ${TASK_DEADLINE:float:240.0} file_hash_to_tools_json: ${FILE_HASH_TO_TOOLS:list:[]} api_keys_json: ${API_KEYS:list:[]} polling_interval: ${POLLING_INTERVAL:float:30.0} agent_index: ${AGENT_INDEX_0:int:0} num_agents: ${NUM_AGENTS:int:4} + timeout_limit: ${TIMEOUT_LIMIT:int:3} 1: models: params: args: - agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + agent_mech_contract_addresses: ${AGENT_MECH_CONTRACT_ADDRESSES:list:["0xFf82123dFB52ab75C417195c5fDB87630145ae81","0x77af31De935740567Cf4fF1986D04B2c964A786a"]} task_deadline: ${TASK_DEADLINE:float:240.0} file_hash_to_tools_json: ${FILE_HASH_TO_TOOLS:list:[]} api_keys_json: ${API_KEYS:list:[]} polling_interval: ${POLLING_INTERVAL:float:30.0} agent_index: ${AGENT_INDEX_1:int:1} num_agents: ${NUM_AGENTS:int:4} + timeout_limit: ${TIMEOUT_LIMIT:int:3} 2: models: params: args: - agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + agent_mech_contract_addresses: ${AGENT_MECH_CONTRACT_ADDRESSES:list:["0xFf82123dFB52ab75C417195c5fDB87630145ae81","0x77af31De935740567Cf4fF1986D04B2c964A786a"]} task_deadline: ${TASK_DEADLINE:float:240.0} file_hash_to_tools_json: ${FILE_HASH_TO_TOOLS:list:[]} api_keys_json: ${API_KEYS:list:[]} polling_interval: ${POLLING_INTERVAL:float:30.0} agent_index: ${AGENT_INDEX_2:int:2} num_agents: ${NUM_AGENTS:int:4} + timeout_limit: ${TIMEOUT_LIMIT:int:3} 3: models: params: args: - agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + agent_mech_contract_addresses: ${AGENT_MECH_CONTRACT_ADDRESSES:list:["0xFf82123dFB52ab75C417195c5fDB87630145ae81","0x77af31De935740567Cf4fF1986D04B2c964A786a"]} task_deadline: ${TASK_DEADLINE:float:240.0} file_hash_to_tools_json: ${FILE_HASH_TO_TOOLS:list:[]} api_keys_json: ${API_KEYS:list:[]} polling_interval: ${POLLING_INTERVAL:float:30.0} agent_index: ${AGENT_INDEX_3:int:3} num_agents: ${NUM_AGENTS:int:4} + timeout_limit: ${TIMEOUT_LIMIT:int:3} --- public_id: valory/ledger:0.19.0 type: connection diff --git a/packages/valory/skills/contract_subscription/behaviours.py b/packages/valory/skills/contract_subscription/behaviours.py index f4722ec2..4ca00ab9 100644 --- a/packages/valory/skills/contract_subscription/behaviours.py +++ b/packages/valory/skills/contract_subscription/behaviours.py @@ -21,110 +21,110 @@ """This package contains a scaffold of a behaviour.""" import json -from typing import Any, List, Optional, cast +from typing import Any, cast -from aea.mail.base import Envelope -from aea.skills.behaviours import SimpleBehaviour - -from packages.valory.connections.websocket_client.connection import ( - PUBLIC_ID, - WebSocketClient, -) -from packages.valory.protocols.default.message import DefaultMessage +from packages.valory.connections.websocket_client.connection import WebSocketClient from packages.valory.skills.contract_subscription.handlers import DISCONNECTION_POINT +from packages.valory.skills.contract_subscription.models import Params +from packages.valory.skills.websocket_client.behaviours import ( + SubscriptionBehaviour as BaseSubscriptionBehaviour, +) +from packages.valory.skills.websocket_client.handlers import ( + SubscriptionStatus, + WEBSOCKET_SUBSCRIPTION_STATUS, +) DEFAULT_ENCODING = "utf-8" WEBSOCKET_CLIENT_CONNECTION_NAME = "websocket_client" -class SubscriptionBehaviour(SimpleBehaviour): +class ContractSubscriptionBehaviour(BaseSubscriptionBehaviour): """This class scaffolds a behaviour.""" def __init__(self, **kwargs: Any) -> None: """Initialise the agent.""" - self._contracts: List[str] = kwargs.pop("contracts", []) - self._ws_client_connection: Optional[WebSocketClient] = None - self._subscription_required: bool = True - self._missed_parts: bool = False super().__init__(**kwargs) + @property + def params(self) -> Params: + """Return params model.""" + + return cast(Params, self.context.params) + def setup(self) -> None: """Implement the setup.""" - use_polling = self.context.params.use_polling - if use_polling: - # if we are using polling, then we don't set up an contract subscription + self._last_subscription_check = None + + # if we are using polling, then we don't set up an contract subscription + if self.params.use_polling: return - for ( - connection - ) in self.context.outbox._multiplexer.connections: # pylint: disable=W0212 + + for connection in self.context.outbox._multiplexer.connections: if connection.component_id.name == WEBSOCKET_CLIENT_CONNECTION_NAME: self._ws_client_connection = cast(WebSocketClient, connection) + def create_contract_subscription_payload(self) -> str: + """Create subscription payload.""" + return json.dumps( + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_subscribe", + "params": ["logs", {"address": self.params.contract_address}], + } + ) + + def create_contract_filter_payload(self, disconnection_point: int) -> str: + """Create subscription payload.""" + return json.dumps( + { + "jsonrpc": "2.0", + "id": 1, + "method": "eth_newFilter", + "params": [ + { + "fromBlock": disconnection_point, + "address": self.params.contract_address, + } + ], + } + ) + def act(self) -> None: - """Implement the act.""" - use_polling = self.context.params.use_polling - if use_polling: + """Perform subcription.""" + + if self.params.use_polling: # do nothing if we are polling return - is_connected = cast(WebSocketClient, self._ws_client_connection).is_connected - disconnection_point = self.context.shared_state.get(DISCONNECTION_POINT, None) - if is_connected and self._subscription_required: - # we only subscribe once, because the envelope will remain in the multiplexer until handled - for contract in self._contracts: - subscription_msg_template = { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_subscribe", - "params": ["logs", {"address": contract}], - } - self.context.logger.info(f"Sending subscription to: {contract}") - self._create_call( - bytes(json.dumps(subscription_msg_template), DEFAULT_ENCODING) - ) - self._subscription_required = False - if disconnection_point is not None: - self._missed_parts = True - - if is_connected and self._missed_parts: - # if we are connected and have a disconnection point, then we need to fetch the parts that were missed - for contract in self._contracts: - filter_msg_template = { - "jsonrpc": "2.0", - "id": 1, - "method": "eth_newFilter", - "params": [{"fromBlock": disconnection_point, "address": contract}], - } - self.context.logger.info(f"Creating filter to: {contract}") - self._create_call( - bytes(json.dumps(filter_msg_template), DEFAULT_ENCODING) - ) + if self.subscribing or self.checking_subscription: + return + + disconnection_point = self.context.shared_state.get(DISCONNECTION_POINT, None) + if self.subscribed and disconnection_point is not None: self.context.logger.info( - "Getting parts that were missed while disconnected." + f"Requesting block filter for {disconnection_point}" ) - self._missed_parts = False - - if ( - not is_connected - and not self._subscription_required - and disconnection_point is not None - ): - self.context.logger.warning( - f"Disconnection detected on block {disconnection_point}." + self._ws_send( + payload=self.create_contract_filter_payload( + disconnection_point=disconnection_point + ), + subscription_id=self.params.subscription_id, ) + self.context.shared_state[DISCONNECTION_POINT] = None - if not is_connected: - self._subscription_required = True + if self.subscribed: + self.check_subscription() + return - def _create_call(self, content: bytes) -> None: - """Create a call.""" - msg, _ = self.context.default_dialogues.create( - counterparty=str(PUBLIC_ID), - performative=DefaultMessage.Performative.BYTES, - content=content, - ) - # pylint: disable=W0212 - msg._sender = str(self.context.skill_id) - envelope = Envelope(to=msg.to, sender=msg._sender, message=msg) - self.context.outbox.put(envelope) + if self.unsubscribed: + self._create_subscription( + provider=self.params.websocket_provider, + subscription_id=self.params.subscription_id, + subscription_payload=self.create_contract_subscription_payload(), + ) + self.context.shared_state[WEBSOCKET_SUBSCRIPTION_STATUS][ + self.params.subscription_id + ] = SubscriptionStatus.SUBSCRIBING + return diff --git a/packages/valory/skills/contract_subscription/dialogues.py b/packages/valory/skills/contract_subscription/dialogues.py index aaa2cd95..4b74ce3d 100644 --- a/packages/valory/skills/contract_subscription/dialogues.py +++ b/packages/valory/skills/contract_subscription/dialogues.py @@ -28,25 +28,29 @@ from aea.protocols.dialogue.base import Dialogue as BaseDialogue from aea.skills.base import Model -from packages.valory.protocols.default.dialogues import ( - DefaultDialogue as BaseDefaultDialogue, +from packages.valory.protocols.websocket_client.dialogues import ( + WebsocketClientDialogue as BaseWebsocketClientDialogue, ) -from packages.valory.protocols.default.dialogues import ( - DefaultDialogues as BaseDefaultDialogues, +from packages.valory.protocols.websocket_client.dialogues import ( + WebsocketClientDialogues as BaseWebsocketClientDialogues, ) -DefaultDialogue = BaseDefaultDialogue +WebsocketClientDialogue = BaseWebsocketClientDialogue -class DefaultDialogues(Model, BaseDefaultDialogues): +class WebsocketClientDialogues(Model, BaseWebsocketClientDialogues): """The dialogues class keeps track of all dialogues.""" def __init__(self, **kwargs: Any) -> None: - """Initialize dialogues.""" + """ + Initialize dialogues. + + :param kwargs: keyword arguments + """ Model.__init__(self, **kwargs) - def role_from_first_message( + def role_from_first_message( # pylint: disable=unused-argument message: Message, receiver_address: Address ) -> BaseDialogue.Role: """Infer the role of the agent from an incoming/outgoing first message @@ -55,11 +59,10 @@ def role_from_first_message( :param receiver_address: the address of the receiving agent :return: The role of the agent """ - self.context.logger.debug(f"{message} {receiver_address}") - return DefaultDialogue.Role.AGENT + return WebsocketClientDialogue.Role.SKILL - BaseDefaultDialogues.__init__( + BaseWebsocketClientDialogues.__init__( self, - self_address=self.context.agent_address, + self_address=str(self.skill_id), role_from_first_message=role_from_first_message, ) diff --git a/packages/valory/skills/contract_subscription/handlers.py b/packages/valory/skills/contract_subscription/handlers.py index 781dfff0..880d7afe 100644 --- a/packages/valory/skills/contract_subscription/handlers.py +++ b/packages/valory/skills/contract_subscription/handlers.py @@ -22,24 +22,29 @@ import json import time -from typing import Any, Dict, Tuple +from typing import Any -from aea.protocols.base import Message -from aea.skills.base import Handler from web3 import Web3 from web3.types import TxReceipt -from packages.valory.protocols.default.message import DefaultMessage +from packages.valory.protocols.websocket_client.message import WebsocketClientMessage +from packages.valory.skills.websocket_client.handlers import ( + SubscriptionStatus, + WEBSOCKET_SUBSCRIPTION_STATUS, +) +from packages.valory.skills.websocket_client.handlers import ( + WebSocketHandler as BaseWebSocketHandler, +) JOB_QUEUE = "pending_tasks" DISCONNECTION_POINT = "disconnection_point" -class WebSocketHandler(Handler): +class WebSocketHandler(BaseWebSocketHandler): """This class scaffolds a handler.""" - SUPPORTED_PROTOCOL = DefaultMessage.protocol_id + SUPPORTED_PROTOCOL = WebsocketClientMessage.protocol_id w3: Web3 = None contract = None @@ -51,8 +56,12 @@ def __init__(self, **kwargs: Any) -> None: def setup(self) -> None: """Implement the setup.""" + super().setup() + self.context.shared_state[JOB_QUEUE] = [] self.context.shared_state[DISCONNECTION_POINT] = None + self._last_processed_block = None + # loads the contracts from the config file with open( "vendor/valory/contracts/agent_mech/build/AgentMech.json", @@ -66,21 +75,34 @@ def setup(self) -> None: ) self.contract = self.w3.eth.contract(address=self.contract_to_monitor, abi=abi) - def handle(self, message: Message) -> None: - """ - Implement the reaction to an envelope. + def handle(self, message: WebsocketClientMessage) -> None: + """Handle message.""" + super().handle(message) + if self.context.shared_state[WEBSOCKET_SUBSCRIPTION_STATUS][ + message.subscription_id + ] in (SubscriptionStatus.UNSUBSCRIBED, SubscriptionStatus.SUBSCRIBING): + self.context.logger.info( + f"Setting disconnection point to {self._last_processed_block}" + ) + self.context.shared_state[DISCONNECTION_POINT] = self._last_processed_block - :param message: the message - """ - self.context.logger.info(f"Received message: {message}") + def handle_recv(self, message: WebsocketClientMessage) -> None: + """Handler `RECV` performative""" try: - data = json.loads(message.content) + data = json.loads(message.data) except json.JSONDecodeError: self.context.logger.info( - f"Error decoding data from the websocket connection; data={message.content}" + f"Error decoding data for websocket subscription {message.subscription_id}; data={message.data}" ) + self.context.shared_state[WEBSOCKET_SUBSCRIPTION_STATUS][ + message.subscription_id + ] = SubscriptionStatus.UNSUBSCRIBED return + self.context.logger.info( + f"Received {data} from subscription {message.subscription_id}" + ) + if set(data.keys()) == {"id", "result", "jsonrpc"}: self.context.logger.info(f"Received response: {data}") return @@ -100,18 +122,16 @@ def handle(self, message: Message) -> None: limit += 1 return no_args = False + if len(event_args) != 0: self.context.shared_state[JOB_QUEUE].append(event_args) self.context.logger.info(f"Added job to queue: {event_args}") - def teardown(self) -> None: - """Implement the handler teardown.""" - - def _get_tx_args(self, tx_hash: str) -> Tuple[Dict, bool]: + def _get_tx_args(self, tx_hash: str) -> Any: """Get the transaction arguments.""" try: tx_receipt: TxReceipt = self.w3.eth.get_transaction_receipt(tx_hash) - self.context.shared_state[DISCONNECTION_POINT] = tx_receipt["blockNumber"] + self._last_processed_block = tx_receipt["blockNumber"] rich_logs = self.contract.events.Request().processReceipt(tx_receipt) # type: ignore return dict(rich_logs[0]["args"]), False diff --git a/packages/valory/skills/contract_subscription/models.py b/packages/valory/skills/contract_subscription/models.py index a4e044c0..d198eee3 100644 --- a/packages/valory/skills/contract_subscription/models.py +++ b/packages/valory/skills/contract_subscription/models.py @@ -20,13 +20,19 @@ """This module contains the shared state for the abci skill of Mech.""" from typing import Any -from aea.skills.base import Model +from packages.valory.skills.websocket_client.models import Params as BaseParams -class Params(Model): +DEFAULT_WEBSOCKET_PROVIDER = "ws://localhost:8001" +DEFAULT_CONTRACT_ADDRESS = "0xFf82123dFB52ab75C417195c5fDB87630145ae81" + + +class Params(BaseParams): """A model to represent params for multiple abci apps.""" def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the parameters object.""" - self.use_polling = kwargs.get("use_polling", False) super().__init__(*args, **kwargs) + + self.use_polling = kwargs.get("use_polling", False) + self.contract_address = kwargs.get("contract_address", DEFAULT_CONTRACT_ADDRESS) diff --git a/packages/valory/skills/contract_subscription/skill.yaml b/packages/valory/skills/contract_subscription/skill.yaml index 1c2eec6f..f3414d7d 100644 --- a/packages/valory/skills/contract_subscription/skill.yaml +++ b/packages/valory/skills/contract_subscription/skill.yaml @@ -8,23 +8,22 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeihmbiavlq5ekiat57xuekfuxjkoniizurn77hivqwtsaqydv32owu - behaviours.py: bafybeihxsq5bhffykzlqkxzd6sfz3s6vmbddalgqhnyuokyf4r62iglvhy - dialogues.py: bafybeihqahapiqyvs7if33hscihx5r6o7ymtopfhuraiyg3h5l6frhghdm - handlers.py: bafybeiffpw6miyxhkygjkkhnkrvcnwztlkpy6ko77zcb4726q6vqge2f2y - models.py: bafybeicxiv2m6whj37zffrqmb3zw6tzlouqvziocallca4gcghx3vigs6q + behaviours.py: bafybeihhhfpan6i5vzxaoggmnj5jw556wnxz75ufcmiucq3yygrbmlsdpm + dialogues.py: bafybeigxlbj6mte72ko7osykjfilg4udfmnrnhxtoib5k4xcxde6qi3niu + handlers.py: bafybeiasnq4qlq5qys4ugktetmaeqnreaswvaqyi7zvjjlifmhbylucasu + models.py: bafybeiafdc32u7yjph4kb4tvsdsaz4tpzo25m3gmthssc62newpgvrros4 fingerprint_ignore_patterns: [] connections: -- valory/websocket_client:0.1.0:bafybeia3lo6hyhom2ht56gzgkv4gdsul5s4qoelljyks2wrcfvx5dtvsiq +- valory/websocket_client:0.1.0:bafybeiako6kyllvgdmqio5y7zxqysaqlwfzjbdij4dkpsibuz3cznh3oza contracts: [] protocols: -- valory/default:1.0.0:bafybeifqcqy5hfbnd7fjv4mqdjrtujh2vx3p2xhe33y67zoxa6ph7wdpaq -skills: [] +- valory/websocket_client:0.1.0:bafybeih43mnztdv3v2hetr2k3gezg7d3yj4ur7cxdvcyaqhg65e52s5sf4 +skills: +- valory/websocket_client:0.1.0:bafybeiaazkuyvkg5xaz62tkmstqc65oyerwpdfe3vl2morsvxdolq4p2le behaviours: - subscriptions: - args: - contracts: - - '0xFf82123dFB52ab75C417195c5fDB87630145ae81' - class_name: SubscriptionBehaviour + contract_subscriptions: + args: {} + class_name: ContractSubscriptionBehaviour handlers: new_event: args: @@ -32,17 +31,15 @@ handlers: websocket_provider: https://rpc.gnosischain.com class_name: WebSocketHandler models: - default_dialogues: + websocket_client_dialogues: args: {} - class_name: DefaultDialogues + class_name: WebsocketClientDialogues params: args: use_polling: false - use_slashing: false - slash_cooldown_hours: 3 - slash_threshold_amount: 10000000000000000 - light_slash_unit_amount: 5000000000000000 - serious_slash_unit_amount: 8000000000000000 + websocket_provider: ws://localhost:8001 + contract_address: '0xFf82123dFB52ab75C417195c5fDB87630145ae81' + subscription_id: mech-contract-subscription class_name: Params dependencies: web3: diff --git a/packages/valory/skills/mech_abci/skill.yaml b/packages/valory/skills/mech_abci/skill.yaml index 36fafbf9..f44bb1e0 100644 --- a/packages/valory/skills/mech_abci/skill.yaml +++ b/packages/valory/skills/mech_abci/skill.yaml @@ -21,9 +21,9 @@ skills: - valory/abstract_round_abci:0.1.0:bafybeifutndycs3n4xxru76bzxmmr26v6sakbaaa3qgbpsxlkknsnzs6ve - valory/registration_abci:0.1.0:bafybeib2flpet4krdafrh55jljxlpplkzf4vfuideu6srjma23h7nf55yi - valory/reset_pause_abci:0.1.0:bafybeidgizxuqcmsznigog3ly5ffqk6wohthw3ohti4xcsalp7erxgxowe -- valory/task_submission_abci:0.1.0:bafybeidis6ncqusjvhe4o3kwb2qjfc6kxsa3l5j635iaeuke6wm7ezylpi -- valory/termination_abci:0.1.0:bafybeign7rs4qg5pucqf6sqgwkirjomamnlp4zgai3a4q3xsq37z4mmonm -- valory/transaction_settlement_abci:0.1.0:bafybeieomj6ecrrolnhxrgygpdra3rjhgllursknsvup2m2illirjxaqu4 +- valory/task_submission_abci:0.1.0:bafybeifzxp4rmqmobgzl5mcfsg3kuvwwzq7c456u2tzlpfq7s7j3tmzk6u +- valory/termination_abci:0.1.0:bafybeieupcqf7v4e77w7kpsgmh66vilsl4rlhytaoacg5txbw6qqcihcoy +- valory/transaction_settlement_abci:0.1.0:bafybeibei2b6l3s2msmirbgru6atbbr6ezneei2txwe3ya3aijgzcgfac4 behaviours: main: args: {} @@ -54,6 +54,9 @@ models: abci_dialogues: args: {} class_name: AbciDialogues + acn_data_share_dialogues: + args: {} + class_name: AcnDataShareDialogues benchmark_tool: args: log_dir: /logs @@ -70,12 +73,10 @@ models: ledger_api_dialogues: args: {} class_name: LedgerApiDialogues - acn_data_share_dialogues: - args: {} - class_name: AcnDataShareDialogues params: args: - agent_mech_contract_address: '0xFf82123dFB52ab75C417195c5fDB87630145ae81' + agent_mech_contract_addresses: + - '0xFf82123dFB52ab75C417195c5fDB87630145ae81' api_keys_json: - - openai - dummy_api_key @@ -151,6 +152,9 @@ models: validate_timeout: 1205 task_wait_timeout: 15.0 use_slashing: false + agent_registry_address: '0x0000000000000000000000000000000000000000' + agent_id: 3 + metadata_hash: '00000000000000000000000000000000000000000000000000' slash_cooldown_hours: 3 slash_threshold_amount: 10000000000000000 light_slash_unit_amount: 5000000000000000 @@ -165,7 +169,7 @@ models: response_key: null response_type: dict retries: 5 - url: https://drand.cloudflare.com/public/latest + url: https://api.drand.sh/public/latest class_name: RandomnessApi requests: args: {} diff --git a/packages/valory/skills/task_execution/behaviours.py b/packages/valory/skills/task_execution/behaviours.py index 77c4b370..0c8908ec 100644 --- a/packages/valory/skills/task_execution/behaviours.py +++ b/packages/valory/skills/task_execution/behaviours.py @@ -105,6 +105,19 @@ def params(self) -> Params: """Get the parameters.""" return cast(Params, self.context.params) + @property + def request_id_to_num_timeouts(self) -> Dict[int, int]: + """Maps the request id to the number of times it has timed out.""" + return self.params.request_id_to_num_timeouts + + def count_timeout(self, request_id: int) -> None: + """Increase the timeout for a request.""" + self.request_id_to_num_timeouts[request_id] += 1 + + def timeout_limit_reached(self, request_id: int) -> bool: + """Check if the timeout limit has been reached.""" + return self.params.timeout_limit <= self.request_id_to_num_timeouts[request_id] + @property def pending_tasks(self) -> List[Dict[str, Any]]: """Get pending_tasks.""" @@ -227,7 +240,8 @@ def _execute_task(self) -> None: if self._executing_task is not None: if self._is_executing_task_ready() or self._invalid_request: - self._handle_done_task() + task_result = self._get_executing_task_result() + self._handle_done_task(task_result) elif self._has_executing_task_timed_out(): self._handle_timeout_task() return @@ -255,12 +269,11 @@ def send_message( self.params.req_to_callback[nonce] = callback self.params.in_flight_req = True - def _handle_done_task(self) -> None: + def _handle_done_task(self, task_result: Any) -> None: """Handle done tasks""" executing_task = cast(Dict[str, Any], self._executing_task) req_id = executing_task.get("requestId", None) mech_address = executing_task.get("contract_address", None) - task_result = self._get_executing_task_result() response = {"requestId": req_id, "result": "Invalid response"} self._done_task = {"request_id": req_id, "mech_address": mech_address} if task_result is not None: @@ -279,12 +292,30 @@ def _handle_timeout_task(self) -> None: """Handle timeout tasks""" executing_task = cast(Dict[str, Any], self._executing_task) req_id = executing_task.get("requestId", None) + self.count_timeout(req_id) self.context.logger.info(f"Task timed out for request {req_id}") - # added to end of queue - self.pending_tasks.append(executing_task) + self.context.logger.info( + f"Task {req_id} has timed out {self.request_id_to_num_timeouts[req_id]} times" + ) async_result = cast(Future, self._async_result) async_result.cancel() - self._executing_task = None + if not self.timeout_limit_reached(req_id): + # added to end of queue + self.context.logger.info(f"Adding task {req_id} to the end of the queue") + self.pending_tasks.append(executing_task) + self._executing_task = None + return None + + self.context.logger.info( + f"Task {req_id} has reached the timeout limit of{self.params.timeout_limit}. " + f"It won't be added to the end of the queue again." + ) + task_result = ( + f"Task timed out {self.params.timeout_limit} times during execution. ", + "", + None, + ) + self._handle_done_task(task_result) def _handle_get_task(self, message: IpfsMessage, dialogue: Dialogue) -> None: """Handle the response from ipfs for a task request.""" @@ -386,8 +417,10 @@ def _handle_store_response(self, message: IpfsMessage, dialogue: Dialogue) -> No executing_task["requestId"], executing_task["sender"], ) - self.context.logger.info(f"Response for request {req_id} stored on IPFS.") ipfs_hash = to_v1(message.ipfs_hash) + self.context.logger.info( + f"Response for request {req_id} stored on IPFS with hash {ipfs_hash}." + ) self.send_data_via_acn( sender_address=sender, request_id=str(req_id), diff --git a/packages/valory/skills/task_execution/models.py b/packages/valory/skills/task_execution/models.py index 2b032ac5..43d634a5 100644 --- a/packages/valory/skills/task_execution/models.py +++ b/packages/valory/skills/task_execution/models.py @@ -18,6 +18,7 @@ # ------------------------------------------------------------------------------ """This module contains the shared state for the abci skill of Mech.""" +from collections import defaultdict from typing import Any, Callable, Dict, List, Optional, cast from aea.exceptions import enforce @@ -59,6 +60,10 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: enforce(self.agent_index is not None, "agent_index must be set!") self.from_block_range = kwargs.get("from_block_range", None) enforce(self.from_block_range is not None, "from_block_range must be set!") + self.timeout_limit = kwargs.get("timeout_limit", None) + enforce(self.timeout_limit is not None, "timeout_limit must be set!") + # maps the request id to the number of times it has timed out + self.request_id_to_num_timeouts: Dict[int, int] = defaultdict(lambda: 0) super().__init__(*args, **kwargs) def _nested_list_todict_workaround( diff --git a/packages/valory/skills/task_execution/skill.yaml b/packages/valory/skills/task_execution/skill.yaml index 2b9cc07d..b7ba2c09 100644 --- a/packages/valory/skills/task_execution/skill.yaml +++ b/packages/valory/skills/task_execution/skill.yaml @@ -7,10 +7,10 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeidqhvvlnthkbnmrdkdeyjyx2f2ab6z4xdgmagh7welqnh2v6wczx4 - behaviours.py: bafybeibrvfpdvcdtel3juuxjcbtvrwazgdg3na5h4fzh2akfldk2eipudq + behaviours.py: bafybeie4h4m4py7u3ah4mcotiah32umi7lb5xnxmzpvvhikushaedcglkq dialogues.py: bafybeid4zxalqdlo5mw4yfbuf34hx4jp5ay5z6chm4zviwu4cj7fudtwca handlers.py: bafybeidbt5ezj74cgfogk3w4uw4si2grlnk5g54veyumw7g5yh6gdscywu - models.py: bafybeiavbz7un34qpxbmi3bmvk7yogc4w7d5wd3eymonelsqep5li222y4 + models.py: bafybeihc2kmymmh5oousjddbc7xujqbk5niermuqak2dhtgryukzq5wxeq utils/__init__.py: bafybeiccdijaigu6e5p2iruwo5mkk224o7ywedc7nr6xeu5fpmhjqgk24e utils/ipfs.py: bafybeicuaj23qrcdv6ly4j7yo6il2r5plozhd6mwvcp5acwqbjxb2t3u2i utils/task.py: bafybeiakokty64m5cqp72drrpvfckhruldlwcge5hcc2bsy2ujk6nnrazq @@ -20,11 +20,12 @@ connections: - valory/ipfs:0.1.0:bafybeigfmqvlzbp67fttccpl4hsu3zaztbxv6vd7ikzra2hfppfkalgpji - valory/p2p_libp2p_client:0.1.0:bafybeihge56dn3xep2dzomu7rtvbgo4uc2qqh7ljl3fubqdi2lq44gs5lq contracts: -- valory/agent_mech:0.1.0:bafybeiektlfcs66jmprajmfg45rvyxbq7wqwj2yzpohyvlux4447talgsa +- valory/agent_mech:0.1.0:bafybeicshvlc2slopzidzblf2zhdcw2uuav3ntxcgqduxskjujvebikg5u protocols: +- valory/acn_data_share:0.1.0:bafybeih5ydonnvrwvy2ygfqgfabkr47s4yw3uqxztmwyfprulwfsoe7ipq - valory/contract_api:1.0.0:bafybeialhbjvwiwcnqq3ysxcyemobcbie7xza66gaofcvla5njezkvhcka +- valory/ledger_api:1.0.0:bafybeige5agrztgzfevyglf7mb4o7pzfttmq4f6zi765y4g2zvftbyowru - valory/default:1.0.0:bafybeifqcqy5hfbnd7fjv4mqdjrtujh2vx3p2xhe33y67zoxa6ph7wdpaq -- valory/acn_data_share:0.1.0:bafybeih5ydonnvrwvy2ygfqgfabkr47s4yw3uqxztmwyfprulwfsoe7ipq - valory/ipfs:0.1.0:bafybeiedxeismnx3k5ty4mvvhlqideixlhqmi5mtcki4lxqfa7uqh7p33u skills: [] behaviours: @@ -32,16 +33,22 @@ behaviours: args: {} class_name: TaskExecutionBehaviour handlers: + acn_data_share_handler: + args: {} + class_name: AcnHandler contract_handler: args: {} class_name: ContractHandler ipfs_handler: args: {} class_name: IpfsHandler - acn_data_share_handler: + ledger_handler: args: {} - class_name: AcnHandler + class_name: LedgerHandler models: + acn_data_share_dialogues: + args: {} + class_name: AcnDataShareDialogues contract_dialogues: args: {} class_name: ContractDialogues @@ -51,13 +58,19 @@ models: ipfs_dialogues: args: {} class_name: IpfsDialogues - acn_data_share_dialogues: + ledger_dialogues: args: {} - class_name: AcnDataShareDialogues + class_name: LedgerDialogues params: args: - agent_mech_contract_address: '0x9A676e781A523b5d0C0e43731313A708CB607508' - task_deadline: 240.0 + agent_index: 0 + agent_mech_contract_addresses: + - '0x9A676e781A523b5d0C0e43731313A708CB607508' + api_keys_json: + - - openai + - dummy_api_key + - - stabilityai + - dummy_api_key file_hash_to_tools_json: - - bafybeif3izkobmvaoen23ine6tiqx55eaf4g3r56hdalnig656xivzpf3m - - openai-text-davinci-002 @@ -69,29 +82,26 @@ models: - stabilityai-stable-diffusion-xl-beta-v2-2-2 - stabilityai-stable-diffusion-512-v2-1 - stabilityai-stable-diffusion-768-v2-1 - api_keys_json: - - - openai - - dummy_api_key - - - stabilityai - - dummy_api_key - polling_interval: 30.0 - agent_index: 0 + from_block_range: 50000 num_agents: 4 + polling_interval: 30.0 + task_deadline: 240.0 use_slashing: false + timeout_limit: 3 slash_cooldown_hours: 3 slash_threshold_amount: 10000000000000000 light_slash_unit_amount: 5000000000000000 serious_slash_unit_amount: 8000000000000000 class_name: Params dependencies: + beautifulsoup4: + version: ==4.12.2 + googlesearch-python: + version: ==1.2.3 openai: version: ==0.27.2 py-multibase: version: ==1.0.3 py-multicodec: version: ==0.2.1 - googlesearch-python: - version: ==1.2.3 - beautifulsoup4: - version: ==4.12.2 is_abstract: false diff --git a/packages/valory/skills/task_submission_abci/skill.yaml b/packages/valory/skills/task_submission_abci/skill.yaml index 417773ea..1254881f 100644 --- a/packages/valory/skills/task_submission_abci/skill.yaml +++ b/packages/valory/skills/task_submission_abci/skill.yaml @@ -19,15 +19,16 @@ fingerprint: fingerprint_ignore_patterns: [] connections: [] contracts: -- valory/agent_mech:0.1.0:bafybeiektlfcs66jmprajmfg45rvyxbq7wqwj2yzpohyvlux4447talgsa +- valory/agent_mech:0.1.0:bafybeicshvlc2slopzidzblf2zhdcw2uuav3ntxcgqduxskjujvebikg5u +- valory/agent_registry:0.1.0:bafybeiargayav6yiztdnwzejoejstcx4idssch2h4f5arlgtzj3tgsgfmu - valory/gnosis_safe:0.1.0:bafybeifmsjpgbifvk7y462rhfczvjvpigkdniavghhg5utza3hbnffioq4 - valory/multisend:0.1.0:bafybeig5byt5urg2d2bsecufxe5ql7f4mezg3mekfleeh32nmuusx66p4y protocols: -- valory/contract_api:1.0.0:bafybeialhbjvwiwcnqq3ysxcyemobcbie7xza66gaofcvla5njezkvhcka - valory/acn_data_share:0.1.0:bafybeih5ydonnvrwvy2ygfqgfabkr47s4yw3uqxztmwyfprulwfsoe7ipq +- valory/contract_api:1.0.0:bafybeialhbjvwiwcnqq3ysxcyemobcbie7xza66gaofcvla5njezkvhcka skills: - valory/abstract_round_abci:0.1.0:bafybeifutndycs3n4xxru76bzxmmr26v6sakbaaa3qgbpsxlkknsnzs6ve -- valory/transaction_settlement_abci:0.1.0:bafybeieomj6ecrrolnhxrgygpdra3rjhgllursknsvup2m2illirjxaqu4 +- valory/transaction_settlement_abci:0.1.0:bafybeibei2b6l3s2msmirbgru6atbbr6ezneei2txwe3ya3aijgzcgfac4 behaviours: main: args: {} @@ -58,6 +59,9 @@ models: abci_dialogues: args: {} class_name: AbciDialogues + acn_data_share_dialogues: + args: {} + class_name: AcnDataShareDialogue benchmark_tool: args: log_dir: /logs @@ -74,9 +78,6 @@ models: ledger_api_dialogues: args: {} class_name: LedgerApiDialogues - acn_data_share_dialogues: - args: {} - class_name: AcnDataShareDialogue params: args: cleanup_history_depth_current: null @@ -101,6 +102,9 @@ models: voting_power: '10' history_check_timeout: 1205 ipfs_domain_name: null + agent_registry_address: '0x0000000000000000000000000000000000000000' + agent_id: 3 + metadata_hash: '00000000000000000000000000000000000000000000000000' ipfs_fetch_timeout: 15.0 keeper_allowed_retries: 3 keeper_timeout: 30.0 @@ -123,13 +127,13 @@ models: safe_contract_address: '0x0000000000000000000000000000000000000000' share_tm_config_on_startup: false sleep_time: 1 + task_wait_timeout: 15 tendermint_check_sleep_delay: 3 tendermint_com_url: http://localhost:8080 tendermint_max_retries: 5 tendermint_p2p_url: localhost:26656 tendermint_url: http://localhost:26657 tx_timeout: 10.0 - task_wait_timeout: 15 use_termination: false validate_timeout: 1205 use_slashing: false @@ -151,14 +155,14 @@ models: args: {} class_name: TendermintDialogues dependencies: + beautifulsoup4: + version: ==4.12.2 + googlesearch-python: + version: ==1.2.3 openai: version: ==0.27.2 py-multibase: version: ==1.0.3 py-multicodec: version: ==0.2.1 - googlesearch-python: - version: ==1.2.3 - beautifulsoup4: - version: ==4.12.2 is_abstract: true diff --git a/packages/valory/skills/termination_abci/skill.yaml b/packages/valory/skills/termination_abci/skill.yaml index bc218354..49acf9a7 100644 --- a/packages/valory/skills/termination_abci/skill.yaml +++ b/packages/valory/skills/termination_abci/skill.yaml @@ -30,7 +30,7 @@ protocols: - valory/contract_api:1.0.0:bafybeialhbjvwiwcnqq3ysxcyemobcbie7xza66gaofcvla5njezkvhcka skills: - valory/abstract_round_abci:0.1.0:bafybeifutndycs3n4xxru76bzxmmr26v6sakbaaa3qgbpsxlkknsnzs6ve -- valory/transaction_settlement_abci:0.1.0:bafybeieomj6ecrrolnhxrgygpdra3rjhgllursknsvup2m2illirjxaqu4 +- valory/transaction_settlement_abci:0.1.0:bafybeibei2b6l3s2msmirbgru6atbbr6ezneei2txwe3ya3aijgzcgfac4 behaviours: main: args: {} diff --git a/packages/valory/skills/transaction_settlement_abci/behaviours.py b/packages/valory/skills/transaction_settlement_abci/behaviours.py index b41fea02..422d2120 100644 --- a/packages/valory/skills/transaction_settlement_abci/behaviours.py +++ b/packages/valory/skills/transaction_settlement_abci/behaviours.py @@ -93,6 +93,7 @@ drand_check = VerifyDrand() REVERT_CODE_RE = r"\s(GS\d{3})[^\d]" +MANUAL_GAS = 1_000_000 # This mapping was copied from: # https://github.com/safe-global/safe-contracts/blob/ce5cbd256bf7a8a34538c7e5f1f2366a9d685f34/docs/error_codes.md @@ -196,6 +197,8 @@ def _get_tx_data( ) return tx_data + # TODO: remove once https://github.com/valory-xyz/open-autonomy/pull/2101 is merged and released + message.raw_transaction.body["gas"] = MANUAL_GAS # Send transaction tx_digest, rpc_status = yield from self.send_raw_transaction( message.raw_transaction, use_flashbots diff --git a/packages/valory/skills/transaction_settlement_abci/skill.yaml b/packages/valory/skills/transaction_settlement_abci/skill.yaml index 5fb45fc3..bd07439f 100644 --- a/packages/valory/skills/transaction_settlement_abci/skill.yaml +++ b/packages/valory/skills/transaction_settlement_abci/skill.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: README.md: bafybeihvqvbj2tiiyimz3e27gqhb7ku5rut7hycfahi4qle732kvj5fs7q __init__.py: bafybeicyrp6x2efg43gfdekxuofrlidc3w6aubzmyioqwnryropp6u7sby - behaviours.py: bafybeid7kjfswufgsca23a3ixli6mx5mm2q6omveeiiwewxtlfrlhchste + behaviours.py: bafybeihsnhwxvy2nmdh75hpo2l4fg52wmbg7iqa2tkgp43ssoi6m3ma6ci dialogues.py: bafybeigabhaykiyzbluu4mk6bbrmqhzld2kyp32pg24bvjmzrrb74einwm fsm_specification.yaml: bafybeigdj64py4zjihcxdkvtrydbxyeh4slr2kkghltz3upnupdgad4et4 handlers.py: bafybeie42qa3csgy6oompuqs2qnkat5mnslepbbwmgoxv6ljme4jofa5pe diff --git a/packages/valory/skills/websocket_client/__init__.py b/packages/valory/skills/websocket_client/__init__.py new file mode 100644 index 00000000..00f1495b --- /dev/null +++ b/packages/valory/skills/websocket_client/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 Valory AG +# Copyright 2023 eightballer +# +# 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 module contains the implementation of the default skill.""" + +from aea.configurations.base import PublicId + +PUBLIC_ID = PublicId.from_str("valory/websocket_client:0.1.0") diff --git a/packages/valory/skills/websocket_client/behaviours.py b/packages/valory/skills/websocket_client/behaviours.py new file mode 100644 index 00000000..bbcd1f8c --- /dev/null +++ b/packages/valory/skills/websocket_client/behaviours.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 Valory AG +# Copyright 2023 eightballer +# +# 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 package contains a scaffold of a behaviour.""" + +import json +import re +from abc import ABC +from datetime import datetime +from typing import Any, Dict, Generator, List, Optional, Set, Type, cast + +from aea.mail.base import Envelope +from aea.skills.behaviours import SimpleBehaviour + +from packages.valory.connections.websocket_client.connection import \ + PUBLIC_ID as WEBSOCKET_CLIENT_CONNECTION +from packages.valory.connections.websocket_client.connection import \ + WebSocketClient +from packages.valory.protocols.websocket_client.message import \ + WebsocketClientMessage +from packages.valory.skills.websocket_client.dialogues import ( + WebsocketClientDialogue, WebsocketClientDialogues) +from packages.valory.skills.websocket_client.handlers import ( + WEBSOCKET_SUBSCRIPTION_STATUS, WEBSOCKET_SUBSCRIPTIONS, SubscriptionStatus) +from packages.valory.skills.websocket_client.models import Params + +DEFAULT_ENCODING = "utf-8" +WEBSOCKET_CLIENT_CONNECTION_NAME = "websocket_client" + + +class SubscriptionBehaviour(SimpleBehaviour): + """This class scaffolds a behaviour.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialise the agent.""" + self._contracts: List[str] = kwargs.pop("contracts", []) + self._ws_client_connection: Optional[WebSocketClient] = None + self._subscription_required: bool = True + self._missed_parts: bool = False + self._last_subscription_check = None + super().__init__(**kwargs) + + @property + def params(self) -> Params: + """Return params model.""" + + return cast(Params, self.context.params) + + @property + def subscription_status(self) -> SubscriptionStatus: + """Returns subscription status""" + return ( + self.context.shared_state.get(WEBSOCKET_SUBSCRIPTION_STATUS, {}) + .get(self.params.subscription_id, SubscriptionStatus.UNSUBSCRIBED) + .value + ) + + @property + def subscription_data(self) -> List[str]: + """Returns subscription status""" + return self.context.shared_state.get(WEBSOCKET_SUBSCRIPTIONS, {}).get( + self.params.subscription_id, [] + ) + + @property + def subscribed(self) -> bool: + return ( + SubscriptionStatus(self.subscription_status) + == SubscriptionStatus.SUBSCRIBED + ) + + @property + def subscribing(self) -> bool: + return ( + SubscriptionStatus(self.subscription_status) + == SubscriptionStatus.SUBSCRIBING + ) + + @property + def checking_subscription(self) -> bool: + return ( + SubscriptionStatus(self.subscription_status) + == SubscriptionStatus.CHECKING_SUBSCRIPTION + ) + + @property + def unsubscribed(self) -> bool: + return ( + SubscriptionStatus(self.subscription_status) + == SubscriptionStatus.UNSUBSCRIBED + ) + + @property + def last_subscription_check(self) -> float: + """Return when last subscription was checked.""" + if self._last_subscription_check is None: + self._last_subscription_check = datetime.now().timestamp() + return self._last_subscription_check + + def create_contract_subscription_payload(self, *args: Any, **kwargs: Any) -> str: + """Create subscription payload.""" + + raise NotImplementedError() + + def check_subscription(self) -> None: + """Check for subscription status""" + if datetime.now().timestamp() < self.last_subscription_check + 5: + return + self._check_subscription(subscription_id=self.params.subscription_id) + self.context.shared_state[WEBSOCKET_SUBSCRIPTION_STATUS][ + self.params.subscription_id + ] = SubscriptionStatus.CHECKING_SUBSCRIPTION + self._last_subscription_check = datetime.now().timestamp() + + def act(self) -> None: + """Perform subcription.""" + + if self.subscribing or self.checking_subscription: + return + + if self.subscribed: + self.check_subscription() + return + + if self.unsubscribed: + self._create_subscription( + provider=self.params.websocket_provider, + subscription_id=self.params.subscription_id, + subscription_payload=self.create_contract_subscription_payload(), + ) + self.context.shared_state[WEBSOCKET_SUBSCRIPTION_STATUS][ + self.params.subscription_id + ] = SubscriptionStatus.SUBSCRIBING + return + + def _create_subscription( + self, + provider: str, + subscription_id: str, + subscription_payload: Optional[str] = None, + ) -> Generator[None, None, WebsocketClientMessage]: + """Subscribe to a websocket using websocket client connection.""" + self.context.logger.info( + f"Creating websocket subscription using provider={provider} payload={subscription_payload}" + ) + websocket_client_dialogues = cast( + WebsocketClientDialogues, self.context.websocket_client_dialogues + ) + (websocket_client_message, _) = websocket_client_dialogues.create( + counterparty=str(WEBSOCKET_CLIENT_CONNECTION), + performative=WebsocketClientMessage.Performative.SUBSCRIBE, + subscription_id=subscription_id, + url=provider, + subscription_payload=subscription_payload, + ) + self.context.outbox.put_message( + message=websocket_client_message, + ) + + def _check_subscription( + self, + subscription_id: int, + ) -> Generator[None, None, WebsocketClientMessage]: + """Subscribe to a websocket using websocket client connection.""" + websocket_client_dialogues = cast( + WebsocketClientDialogues, self.context.websocket_client_dialogues + ) + (websocket_client_message, _) = websocket_client_dialogues.create( + counterparty=str(WEBSOCKET_CLIENT_CONNECTION), + performative=WebsocketClientMessage.Performative.CHECK_SUBSCRIPTION, + subscription_id=subscription_id, + ) + self.context.outbox.put_message( + message=websocket_client_message, + ) + + def _ws_send( + self, + payload: str, + subscription_id: int, + ) -> Generator[None, None, WebsocketClientMessage]: + """Subscribe to a websocket using websocket client connection.""" + websocket_client_dialogues = cast( + WebsocketClientDialogues, self.context.websocket_client_dialogues + ) + (websocket_client_message, _) = websocket_client_dialogues.create( + counterparty=str(WEBSOCKET_CLIENT_CONNECTION), + performative=WebsocketClientMessage.Performative.SEND, + payload=payload, + subscription_id=subscription_id, + ) + self.context.outbox.put_message( + message=websocket_client_message, + ) diff --git a/packages/valory/skills/websocket_client/dialogues.py b/packages/valory/skills/websocket_client/dialogues.py new file mode 100644 index 00000000..03269db5 --- /dev/null +++ b/packages/valory/skills/websocket_client/dialogues.py @@ -0,0 +1,48 @@ +""" +Dialogues +""" + +from typing import Any + +from aea.common import Address +from aea.protocols.base import Message +from aea.protocols.dialogue.base import Dialogue as BaseDialogue +from aea.skills.base import Model + +from packages.valory.protocols.websocket_client.dialogues import ( + WebsocketClientDialogue as BaseWebsocketClientDialogue, +) +from packages.valory.protocols.websocket_client.dialogues import ( + WebsocketClientDialogues as BaseWebsocketClientDialogues, +) + +WebsocketClientDialogue = BaseWebsocketClientDialogue + + +class WebsocketClientDialogues(Model, BaseWebsocketClientDialogues): + """The dialogues class keeps track of all dialogues.""" + + def __init__(self, **kwargs: Any) -> None: + """ + Initialize dialogues. + + :param kwargs: keyword arguments + """ + Model.__init__(self, **kwargs) + + def role_from_first_message( # pylint: disable=unused-argument + message: Message, receiver_address: Address + ) -> BaseDialogue.Role: + """Infer the role of the agent from an incoming/outgoing first message + + :param message: an incoming/outgoing first message + :param receiver_address: the address of the receiving agent + :return: The role of the agent + """ + return WebsocketClientDialogue.Role.SKILL + + BaseWebsocketClientDialogues.__init__( + self, + self_address=str(self.skill_id), + role_from_first_message=role_from_first_message, + ) diff --git a/packages/valory/skills/websocket_client/handlers.py b/packages/valory/skills/websocket_client/handlers.py new file mode 100644 index 00000000..20f061b5 --- /dev/null +++ b/packages/valory/skills/websocket_client/handlers.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 Valory AG +# Copyright 2023 eightballer +# +# 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 package contains a scaffold of a handler.""" + +import json +import time + +from aea.protocols.base import Message +from aea.skills.base import Handler +from web3 import Web3 +from web3.types import TxReceipt +from typing import Callable, cast +from packages.valory.protocols.websocket_client.message import WebsocketClientMessage +from enum import Enum + +JOB_QUEUE = "pending_tasks" +SUBSCRIPTION_ID = "subscription_id" +WEBSOCKET_SUBSCRIPTION_STATUS = "websocket_subscription_status" +WEBSOCKET_SUBSCRIPTIONS = "websocket_subscriptions" + + +class SubscriptionStatus(Enum): + """Subscription status.""" + + UNSUBSCRIBED = "unsubscribed" + SUBSCRIBING = "subscribing" + CHECKING_SUBSCRIPTION = "checking_subscription" + SUBSCRIBED = "subscribed" + + +class WebSocketHandler(Handler): + """This class scaffolds a handler.""" + + SUPPORTED_PROTOCOL = WebsocketClientMessage.protocol_id + + def setup(self) -> None: + """Implement the setup.""" + if WEBSOCKET_SUBSCRIPTION_STATUS not in self.context.shared_state: + self.context.shared_state[WEBSOCKET_SUBSCRIPTION_STATUS] = {} + + if WEBSOCKET_SUBSCRIPTIONS not in self.context.shared_state: + self.context.shared_state[WEBSOCKET_SUBSCRIPTIONS] = {} + + self._count = 0 + + def handle(self, message: WebsocketClientMessage) -> None: + """ + Implement the reaction to an envelope. + + :param message: the message + """ + self.context.logger.info(f"Received message: {message}") + handler = cast( + Callable[[WebsocketClientMessage], None], + getattr(self, f"handle_{message.performative.value}"), + ) + handler(message) + + def handle_subscription(self, message: WebsocketClientMessage) -> None: + """Handler `WebsocketClientMessage.Performative.SUBSCRIPTION` response""" + self.context.shared_state[WEBSOCKET_SUBSCRIPTION_STATUS][ + message.subscription_id + ] = ( + SubscriptionStatus.SUBSCRIBED + if message.alive + else SubscriptionStatus.UNSUBSCRIBED + ) + + def handle_send_success(self, message: WebsocketClientMessage) -> None: + """Handler `WebsocketClientMessage.Performative.SEND_SUCCESS` response""" + self.context.logger.info( + f"Sent data to the websocket with id {message.subscription_id}; send_length: {message.send_length}" + ) + + def handle_recv(self, message: WebsocketClientMessage) -> None: + """Handler `WebsocketClientMessage.Performative.RECV` response""" + self.context.logger.info( + f"Received {message.data} from subscription {message.subscription_id}" + ) + subscription_id = message.subscription_id + if subscription_id not in self.context.shared_state[WEBSOCKET_SUBSCRIPTIONS]: + self.context.shared_state[WEBSOCKET_SUBSCRIPTIONS][subscription_id] = [] + + self.context.shared_state[WEBSOCKET_SUBSCRIPTIONS][subscription_id].append( + message.data + ) + + def handle_error(self, message: WebsocketClientMessage) -> None: + """Handler `WebsocketClientMessage.Performative.ERROR` response""" + self.context.logger.info( + f"Error occured on the websocket with id {message.subscription_id}; Error: {message.message}" + ) + self.context.shared_state[WEBSOCKET_SUBSCRIPTION_STATUS][ + message.subscription_id + ] = ( + SubscriptionStatus.SUBSCRIBED + if message.alive + else SubscriptionStatus.UNSUBSCRIBED + ) + + def teardown(self) -> None: + """Implement the handler teardown.""" diff --git a/packages/valory/skills/websocket_client/models.py b/packages/valory/skills/websocket_client/models.py new file mode 100644 index 00000000..de234967 --- /dev/null +++ b/packages/valory/skills/websocket_client/models.py @@ -0,0 +1,39 @@ +# -*- 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 module contains the shared state for the abci skill of Mech.""" +from typing import Any + +from aea.skills.base import Model + +DEFAULT_WEBSOCKET_PROVIDER = "ws://localhost:8001" +DEFAULT_SUBSCRIPTION_ID = "websocket-subscription" + + +class Params(Model): + """A model to represent params for multiple abci apps.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the parameters object.""" + + self.websocket_provider = kwargs.get( + "websocket_provider", DEFAULT_WEBSOCKET_PROVIDER + ) + self.subscription_id = kwargs.get("subscription_id", DEFAULT_SUBSCRIPTION_ID) + super().__init__(*args, **kwargs) diff --git a/packages/valory/skills/websocket_client/skill.yaml b/packages/valory/skills/websocket_client/skill.yaml new file mode 100644 index 00000000..cd0c4cb6 --- /dev/null +++ b/packages/valory/skills/websocket_client/skill.yaml @@ -0,0 +1,41 @@ +name: websocket_client +author: valory +version: 0.1.0 +type: skill +description: Websocket client. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + __init__.py: bafybeibgl4hnpsd3vokfs6cfkg4elgqu7nm4yhs6sh373j6erwvgpjdqeu + behaviours.py: bafybeianfyloqfumjydjgogjg2sr5wrw7epzjt5o7sxv4drfasl2kgmq4i + dialogues.py: bafybeicc26sbiipnfublma3ywvh54elbx5y5sj7xckq3xyqyfmmoamiouy + handlers.py: bafybeibqfinswattz7gu4pijjxvixp5gtrgfz5idsz3uzlgrjw2vanjiu4 + models.py: bafybeicuei7xoozvgr6kyp6cp7b6gqonlkmlgkvhhff37iecnjqzhkvbgi +fingerprint_ignore_patterns: [] +connections: +- valory/websocket_client:0.1.0:bafybeiako6kyllvgdmqio5y7zxqysaqlwfzjbdij4dkpsibuz3cznh3oza +contracts: [] +protocols: +- valory/websocket_client:0.1.0:bafybeih43mnztdv3v2hetr2k3gezg7d3yj4ur7cxdvcyaqhg65e52s5sf4 +skills: [] +behaviours: + websocket_subscription: + args: {} + class_name: SubscriptionBehaviour +handlers: + new_event: + args: {} + class_name: WebSocketHandler +models: + websocket_client_dialogues: + args: {} + class_name: WebsocketClientDialogues + params: + args: + websocket_provider: ws://localhost:8001 + subscription_id: websocket-subscription + class_name: Params +dependencies: + web3: + version: <7,>=6.0.0 +is_abstract: true diff --git a/poetry.lock b/poetry.lock index 397e84a2..3fc68956 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2331,14 +2331,14 @@ data = ["language-data (>=1.1,<2.0)"] [[package]] name = "langsmith" -version = "0.0.64" +version = "0.0.63" description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." category = "main" optional = false python-versions = ">=3.8.1,<4.0" files = [ - {file = "langsmith-0.0.64-py3-none-any.whl", hash = "sha256:461acdcd8332d1325c16dc57e8a2d5ec9d1578490a4eaabe14db74db74ceaf21"}, - {file = "langsmith-0.0.64.tar.gz", hash = "sha256:e78c02501c2cff24fff7bd2d28ff3765b21675c7f0fcf6a09932bc218603c36e"}, + {file = "langsmith-0.0.63-py3-none-any.whl", hash = "sha256:43a521dd10d8405ac21a0b959e3de33e2270e4abe6c73cc4036232a6990a0793"}, + {file = "langsmith-0.0.63.tar.gz", hash = "sha256:ddb2dfadfad3e05151ed8ba1643d1c516024b80fbd0c6263024400ced06a3768"}, ] [package.dependencies] @@ -2809,6 +2809,52 @@ files = [ {file = "numpy-1.25.2.tar.gz", hash = "sha256:fd608e19c8d7c55021dffd43bfe5492fab8cc105cc8986f813f8c3c048b38760"}, ] +[[package]] +name = "numpy" +version = "1.26.2" +description = "Fundamental package for array computing in Python" +category = "main" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3703fc9258a4a122d17043e57b35e5ef1c5a5837c3db8be396c82e04c1cf9b0f"}, + {file = "numpy-1.26.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cc392fdcbd21d4be6ae1bb4475a03ce3b025cd49a9be5345d76d7585aea69440"}, + {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36340109af8da8805d8851ef1d74761b3b88e81a9bd80b290bbfed61bd2b4f75"}, + {file = "numpy-1.26.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcc008217145b3d77abd3e4d5ef586e3bdfba8fe17940769f8aa09b99e856c00"}, + {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3ced40d4e9e18242f70dd02d739e44698df3dcb010d31f495ff00a31ef6014fe"}, + {file = "numpy-1.26.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b272d4cecc32c9e19911891446b72e986157e6a1809b7b56518b4f3755267523"}, + {file = "numpy-1.26.2-cp310-cp310-win32.whl", hash = "sha256:22f8fc02fdbc829e7a8c578dd8d2e15a9074b630d4da29cda483337e300e3ee9"}, + {file = "numpy-1.26.2-cp310-cp310-win_amd64.whl", hash = "sha256:26c9d33f8e8b846d5a65dd068c14e04018d05533b348d9eaeef6c1bd787f9919"}, + {file = "numpy-1.26.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b96e7b9c624ef3ae2ae0e04fa9b460f6b9f17ad8b4bec6d7756510f1f6c0c841"}, + {file = "numpy-1.26.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aa18428111fb9a591d7a9cc1b48150097ba6a7e8299fb56bdf574df650e7d1f1"}, + {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06fa1ed84aa60ea6ef9f91ba57b5ed963c3729534e6e54055fc151fad0423f0a"}, + {file = "numpy-1.26.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ca5482c3dbdd051bcd1fce8034603d6ebfc125a7bd59f55b40d8f5d246832b"}, + {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:854ab91a2906ef29dc3925a064fcd365c7b4da743f84b123002f6139bcb3f8a7"}, + {file = "numpy-1.26.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f43740ab089277d403aa07567be138fc2a89d4d9892d113b76153e0e412409f8"}, + {file = "numpy-1.26.2-cp311-cp311-win32.whl", hash = "sha256:a2bbc29fcb1771cd7b7425f98b05307776a6baf43035d3b80c4b0f29e9545186"}, + {file = "numpy-1.26.2-cp311-cp311-win_amd64.whl", hash = "sha256:2b3fca8a5b00184828d12b073af4d0fc5fdd94b1632c2477526f6bd7842d700d"}, + {file = "numpy-1.26.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a4cd6ed4a339c21f1d1b0fdf13426cb3b284555c27ac2f156dfdaaa7e16bfab0"}, + {file = "numpy-1.26.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d5244aabd6ed7f312268b9247be47343a654ebea52a60f002dc70c769048e75"}, + {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a3cdb4d9c70e6b8c0814239ead47da00934666f668426fc6e94cce869e13fd7"}, + {file = "numpy-1.26.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa317b2325f7aa0a9471663e6093c210cb2ae9c0ad824732b307d2c51983d5b6"}, + {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:174a8880739c16c925799c018f3f55b8130c1f7c8e75ab0a6fa9d41cab092fd6"}, + {file = "numpy-1.26.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f79b231bf5c16b1f39c7f4875e1ded36abee1591e98742b05d8a0fb55d8a3eec"}, + {file = "numpy-1.26.2-cp312-cp312-win32.whl", hash = "sha256:4a06263321dfd3598cacb252f51e521a8cb4b6df471bb12a7ee5cbab20ea9167"}, + {file = "numpy-1.26.2-cp312-cp312-win_amd64.whl", hash = "sha256:b04f5dc6b3efdaab541f7857351aac359e6ae3c126e2edb376929bd3b7f92d7e"}, + {file = "numpy-1.26.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4eb8df4bf8d3d90d091e0146f6c28492b0be84da3e409ebef54349f71ed271ef"}, + {file = "numpy-1.26.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1a13860fdcd95de7cf58bd6f8bc5a5ef81c0b0625eb2c9a783948847abbef2c2"}, + {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64308ebc366a8ed63fd0bf426b6a9468060962f1a4339ab1074c228fa6ade8e3"}, + {file = "numpy-1.26.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf8aab04a2c0e859da118f0b38617e5ee65d75b83795055fb66c0d5e9e9b818"}, + {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d73a3abcac238250091b11caef9ad12413dab01669511779bc9b29261dd50210"}, + {file = "numpy-1.26.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b361d369fc7e5e1714cf827b731ca32bff8d411212fccd29ad98ad622449cc36"}, + {file = "numpy-1.26.2-cp39-cp39-win32.whl", hash = "sha256:bd3f0091e845164a20bd5a326860c840fe2af79fa12e0469a12768a3ec578d80"}, + {file = "numpy-1.26.2-cp39-cp39-win_amd64.whl", hash = "sha256:2beef57fb031dcc0dc8fa4fe297a742027b954949cabb52a2a376c144e5e6060"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1cc3d5029a30fb5f06704ad6b23b35e11309491c999838c31f124fee32107c79"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94cc3c222bb9fb5a12e334d0479b97bb2df446fbe622b470928f5284ffca3f8d"}, + {file = "numpy-1.26.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe6b44fb8fcdf7eda4ef4461b97b3f63c466b27ab151bec2366db8b197387841"}, + {file = "numpy-1.26.2.tar.gz", hash = "sha256:f65738447676ab5777f11e6bbbdb8ce11b785e105f690bc45966574816b6d3ea"}, +] + [[package]] name = "open-aea" version = "1.41.0.post1" @@ -3012,6 +3058,75 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[[package]] +name = "pandas" +version = "2.1.1" +description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pandas-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58d997dbee0d4b64f3cb881a24f918b5f25dd64ddf31f467bb9b67ae4c63a1e4"}, + {file = "pandas-2.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02304e11582c5d090e5a52aec726f31fe3f42895d6bfc1f28738f9b64b6f0614"}, + {file = "pandas-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffa8f0966de2c22de408d0e322db2faed6f6e74265aa0856f3824813cf124363"}, + {file = "pandas-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1f84c144dee086fe4f04a472b5cd51e680f061adf75c1ae4fc3a9275560f8f4"}, + {file = "pandas-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ce97667d06d69396d72be074f0556698c7f662029322027c226fd7a26965cb"}, + {file = "pandas-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:4c3f32fd7c4dccd035f71734df39231ac1a6ff95e8bdab8d891167197b7018d2"}, + {file = "pandas-2.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e2959720b70e106bb1d8b6eadd8ecd7c8e99ccdbe03ee03260877184bb2877d"}, + {file = "pandas-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25e8474a8eb258e391e30c288eecec565bfed3e026f312b0cbd709a63906b6f8"}, + {file = "pandas-2.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8bd1685556f3374520466998929bade3076aeae77c3e67ada5ed2b90b4de7f0"}, + {file = "pandas-2.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc3657869c7902810f32bd072f0740487f9e030c1a3ab03e0af093db35a9d14e"}, + {file = "pandas-2.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:05674536bd477af36aa2effd4ec8f71b92234ce0cc174de34fd21e2ee99adbc2"}, + {file = "pandas-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:b407381258a667df49d58a1b637be33e514b07f9285feb27769cedb3ab3d0b3a"}, + {file = "pandas-2.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c747793c4e9dcece7bb20156179529898abf505fe32cb40c4052107a3c620b49"}, + {file = "pandas-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3bcad1e6fb34b727b016775bea407311f7721db87e5b409e6542f4546a4951ea"}, + {file = "pandas-2.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5ec7740f9ccb90aec64edd71434711f58ee0ea7f5ed4ac48be11cfa9abf7317"}, + {file = "pandas-2.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29deb61de5a8a93bdd033df328441a79fcf8dd3c12d5ed0b41a395eef9cd76f0"}, + {file = "pandas-2.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f99bebf19b7e03cf80a4e770a3e65eee9dd4e2679039f542d7c1ace7b7b1daa"}, + {file = "pandas-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:84e7e910096416adec68075dc87b986ff202920fb8704e6d9c8c9897fe7332d6"}, + {file = "pandas-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:366da7b0e540d1b908886d4feb3d951f2f1e572e655c1160f5fde28ad4abb750"}, + {file = "pandas-2.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9e50e72b667415a816ac27dfcfe686dc5a0b02202e06196b943d54c4f9c7693e"}, + {file = "pandas-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc1ab6a25da197f03ebe6d8fa17273126120874386b4ac11c1d687df288542dd"}, + {file = "pandas-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0dbfea0dd3901ad4ce2306575c54348d98499c95be01b8d885a2737fe4d7a98"}, + {file = "pandas-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0489b0e6aa3d907e909aef92975edae89b1ee1654db5eafb9be633b0124abe97"}, + {file = "pandas-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:4cdb0fab0400c2cb46dafcf1a0fe084c8bb2480a1fa8d81e19d15e12e6d4ded2"}, + {file = "pandas-2.1.1.tar.gz", hash = "sha256:fecb198dc389429be557cde50a2d46da8434a17fe37d7d41ff102e3987fd947b"}, +] + +[package.dependencies] +numpy = [ + {version = ">=1.22.4", markers = "python_version < \"3.11\""}, + {version = ">=1.23.2", markers = "python_version == \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, +] +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.1" + +[package.extras] +all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] +aws = ["s3fs (>=2022.05.0)"] +clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] +compression = ["zstandard (>=0.17.0)"] +computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] +feather = ["pyarrow (>=7.0.0)"] +fss = ["fsspec (>=2022.05.0)"] +gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] +hdf5 = ["tables (>=3.7.0)"] +html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] +mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] +parquet = ["pyarrow (>=7.0.0)"] +performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] +plot = ["matplotlib (>=3.6.1)"] +postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] +spss = ["pyreadstat (>=1.1.5)"] +sql-other = ["SQLAlchemy (>=1.4.36)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.8.0)"] + [[package]] name = "paramiko" version = "3.3.1" @@ -3669,6 +3784,21 @@ files = [ {file = "python-baseconv-1.2.2.tar.gz", hash = "sha256:0539f8bd0464013b05ad62e0a1673f0ac9086c76b43ebf9f833053527cd9931b"}, ] +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "0.17.1" @@ -4111,6 +4241,38 @@ docs = ["Pillow (>=7.1.2)", "matplotlib (>=3.1.3)", "memory-profiler (>=0.57.0)" examples = ["matplotlib (>=3.1.3)", "pandas (>=1.0.5)", "plotly (>=5.14.0)", "pooch (>=1.6.0)", "scikit-image (>=0.16.2)", "seaborn (>=0.9.0)"] tests = ["black (>=23.3.0)", "matplotlib (>=3.1.3)", "mypy (>=1.3)", "numpydoc (>=1.2.0)", "pandas (>=1.0.5)", "pooch (>=1.6.0)", "pyamg (>=4.0.0)", "pytest (>=7.1.2)", "pytest-cov (>=2.9.0)", "ruff (>=0.0.272)", "scikit-image (>=0.16.2)"] +[[package]] +name = "scipy" +version = "1.6.1" +description = "SciPy: Scientific Library for Python" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "scipy-1.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a15a1f3fc0abff33e792d6049161b7795909b40b97c6cc2934ed54384017ab76"}, + {file = "scipy-1.6.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e79570979ccdc3d165456dd62041d9556fb9733b86b4b6d818af7a0afc15f092"}, + {file = "scipy-1.6.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:a423533c55fec61456dedee7b6ee7dce0bb6bfa395424ea374d25afa262be261"}, + {file = "scipy-1.6.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:33d6b7df40d197bdd3049d64e8e680227151673465e5d85723b3b8f6b15a6ced"}, + {file = "scipy-1.6.1-cp37-cp37m-win32.whl", hash = "sha256:6725e3fbb47da428794f243864f2297462e9ee448297c93ed1dcbc44335feb78"}, + {file = "scipy-1.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:5fa9c6530b1661f1370bcd332a1e62ca7881785cc0f80c0d559b636567fab63c"}, + {file = "scipy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bd50daf727f7c195e26f27467c85ce653d41df4358a25b32434a50d8870fc519"}, + {file = "scipy-1.6.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:f46dd15335e8a320b0fb4685f58b7471702234cba8bb3442b69a3e1dc329c345"}, + {file = "scipy-1.6.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0e5b0ccf63155d90da576edd2768b66fb276446c371b73841e3503be1d63fb5d"}, + {file = "scipy-1.6.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:2481efbb3740977e3c831edfd0bd9867be26387cacf24eb5e366a6a374d3d00d"}, + {file = "scipy-1.6.1-cp38-cp38-win32.whl", hash = "sha256:68cb4c424112cd4be886b4d979c5497fba190714085f46b8ae67a5e4416c32b4"}, + {file = "scipy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:5f331eeed0297232d2e6eea51b54e8278ed8bb10b099f69c44e2558c090d06bf"}, + {file = "scipy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0c8a51d33556bf70367452d4d601d1742c0e806cd0194785914daf19775f0e67"}, + {file = "scipy-1.6.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:83bf7c16245c15bc58ee76c5418e46ea1811edcc2e2b03041b804e46084ab627"}, + {file = "scipy-1.6.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:794e768cc5f779736593046c9714e0f3a5940bc6dcc1dba885ad64cbfb28e9f0"}, + {file = "scipy-1.6.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5da5471aed911fe7e52b86bf9ea32fb55ae93e2f0fac66c32e58897cfb02fa07"}, + {file = "scipy-1.6.1-cp39-cp39-win32.whl", hash = "sha256:8e403a337749ed40af60e537cc4d4c03febddcc56cd26e774c9b1b600a70d3e4"}, + {file = "scipy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a5193a098ae9f29af283dcf0041f762601faf2e595c0db1da929875b7570353f"}, + {file = "scipy-1.6.1.tar.gz", hash = "sha256:c4fceb864890b6168e79b0e714c585dbe2fd4222768ee90bc1aa0f8218691b11"}, +] + +[package.dependencies] +numpy = ">=1.16.5" + [[package]] name = "scipy" version = "1.9.3" @@ -4618,6 +4780,52 @@ files = [ {file = "threadpoolctl-3.2.0.tar.gz", hash = "sha256:c96a0ba3bdddeaca37dc4cc7344aafad41cdb8c313f74fdfe387a867bba93355"}, ] +[[package]] +name = "tiktoken" +version = "0.5.1" +description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tiktoken-0.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2b0bae3fd56de1c0a5874fb6577667a3c75bf231a6cef599338820210c16e40a"}, + {file = "tiktoken-0.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e529578d017045e2f0ed12d2e00e7e99f780f477234da4aae799ec4afca89f37"}, + {file = "tiktoken-0.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edd2ffbb789712d83fee19ab009949f998a35c51ad9f9beb39109357416344ff"}, + {file = "tiktoken-0.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4c73d47bdc1a3f1f66ffa019af0386c48effdc6e8797e5e76875f6388ff72e9"}, + {file = "tiktoken-0.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46b8554b9f351561b1989157c6bb54462056f3d44e43aa4e671367c5d62535fc"}, + {file = "tiktoken-0.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92ed3bbf71a175a6a4e5fbfcdb2c422bdd72d9b20407e00f435cf22a68b4ea9b"}, + {file = "tiktoken-0.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:714efb2f4a082635d9f5afe0bf7e62989b72b65ac52f004eb7ac939f506c03a4"}, + {file = "tiktoken-0.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a10488d1d1a5f9c9d2b2052fdb4cf807bba545818cb1ef724a7f5d44d9f7c3d4"}, + {file = "tiktoken-0.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8079ac065572fe0e7c696dbd63e1fdc12ce4cdca9933935d038689d4732451df"}, + {file = "tiktoken-0.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ef730db4097f5b13df8d960f7fdda2744fe21d203ea2bb80c120bb58661b155"}, + {file = "tiktoken-0.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:426e7def5f3f23645dada816be119fa61e587dfb4755de250e136b47a045c365"}, + {file = "tiktoken-0.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:323cec0031358bc09aa965c2c5c1f9f59baf76e5b17e62dcc06d1bb9bc3a3c7c"}, + {file = "tiktoken-0.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5abd9436f02e2c8eda5cce2ff8015ce91f33e782a7423de2a1859f772928f714"}, + {file = "tiktoken-0.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:1fe99953b63aabc0c9536fbc91c3c9000d78e4755edc28cc2e10825372046a2d"}, + {file = "tiktoken-0.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:dcdc630461927718b317e6f8be7707bd0fc768cee1fdc78ddaa1e93f4dc6b2b1"}, + {file = "tiktoken-0.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1f2b3b253e22322b7f53a111e1f6d7ecfa199b4f08f3efdeb0480f4033b5cdc6"}, + {file = "tiktoken-0.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43ce0199f315776dec3ea7bf86f35df86d24b6fcde1babd3e53c38f17352442f"}, + {file = "tiktoken-0.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a84657c083d458593c0235926b5c993eec0b586a2508d6a2020556e5347c2f0d"}, + {file = "tiktoken-0.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c008375c0f3d97c36e81725308699116cd5804fdac0f9b7afc732056329d2790"}, + {file = "tiktoken-0.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:779c4dea5edd1d3178734d144d32231e0b814976bec1ec09636d1003ffe4725f"}, + {file = "tiktoken-0.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:b5dcfcf9bfb798e86fbce76d40a1d5d9e3f92131aecfa3d1e5c9ea1a20f1ef1a"}, + {file = "tiktoken-0.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b180a22db0bbcc447f691ffc3cf7a580e9e0587d87379e35e58b826ebf5bc7b"}, + {file = "tiktoken-0.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b756a65d98b7cf760617a6b68762a23ab8b6ef79922be5afdb00f5e8a9f4e76"}, + {file = "tiktoken-0.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba9873c253ca1f670e662192a0afcb72b41e0ba3e730f16c665099e12f4dac2d"}, + {file = "tiktoken-0.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74c90d2be0b4c1a2b3f7dde95cd976757817d4df080d6af0ee8d461568c2e2ad"}, + {file = "tiktoken-0.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:709a5220891f2b56caad8327fab86281787704931ed484d9548f65598dea9ce4"}, + {file = "tiktoken-0.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d5a187ff9c786fae6aadd49f47f019ff19e99071dc5b0fe91bfecc94d37c686"}, + {file = "tiktoken-0.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:e21840043dbe2e280e99ad41951c00eff8ee3b63daf57cd4c1508a3fd8583ea2"}, + {file = "tiktoken-0.5.1.tar.gz", hash = "sha256:27e773564232004f4f810fd1f85236673ec3a56ed7f1206fc9ed8670ebedb97a"}, +] + +[package.dependencies] +regex = ">=2022.1.18" +requests = ">=2.26.0" + +[package.extras] +blobfile = ["blobfile (>=2)"] + [[package]] name = "tokenizers" version = "0.14.1" @@ -4905,6 +5113,18 @@ files = [ mypy-extensions = ">=0.3.0" typing-extensions = ">=3.7.4" +[[package]] +name = "tzdata" +version = "2023.3" +description = "Provider of IANA time zone data" +category = "main" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, + {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, +] + [[package]] name = "uritemplate" version = "4.1.1" @@ -5327,4 +5547,8 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" +<<<<<<< HEAD content-hash = "6802caf801477788a1abfa55c7df2ff2f0539c69cd9ecd23d36d5a9b1e927d9c" +======= +content-hash = "59cddb7e86aadab080867d519559f3feea82d584a5606388a54915c7948e9c48" +>>>>>>> main diff --git a/pyproject.toml b/pyproject.toml index e6c4c702..bf0f15ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,9 @@ scikit-learn = "==1.3.1" pytest = "==7.2.1" jsonschema = "<=4.19.0,>=4.16.0" spacy = "==3.7.2" +pandas = "==2.1.1" +tiktoken = "==0.5.1" +python-dateutil = "==2.8.2" [tool.poetry.group.dev.dependencies.tomte] version = "==0.2.12" diff --git a/tox.ini b/tox.ini index 712b4d94..5c40b131 100644 --- a/tox.ini +++ b/tox.ini @@ -59,6 +59,9 @@ deps = pytest==7.2.1 jsonschema<=4.19.0,>=4.16.0 spacy==3.7.2 + pandas==2.1.1 + tiktoken==0.5.1 + python-dateutil==2.8.2 [testenv] basepython = python3