diff --git a/.github/workflows/common_checks.yaml b/.github/workflows/common_checks.yaml index b7fef649..6f156bef 100644 --- a/.github/workflows/common_checks.yaml +++ b/.github/workflows/common_checks.yaml @@ -129,7 +129,7 @@ jobs: run: | tox -e check-abci-docstrings tox -e check-abciapp-specs - tox -e check-handlers + # tox -e check-handlers # ignore for now due to https://github.com/valory-xyz/open-autonomy/issues/1988 # tox -e analyse-service diff --git a/.gitignore b/.gitignore index 2e8edf30..7c27f657 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,4 @@ keys.json leak_report agent/ -mech/ +backup_mech/ diff --git a/packages/packages.json b/packages/packages.json index 2e17400c..ad12d11a 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -2,14 +2,14 @@ "dev": { "connection/valory/websocket_client/0.1.0": "bafybeicz53kzs5uvyiod2azntl76zwgmpgr22ven4wl5fnwt2m546j3wsu", "skill/valory/contract_subscription/0.1.0": "bafybeifbgzfrhtdtendqzwmh3o436nyexwkif6mbvsouvk2ktdfk5lhe7y", - "agent/valory/mech/0.1.0": "bafybeiafuo66iikppev3f5u4veky5ouytrzwjtc3bjupdmbvlnamu7fjnu", - "skill/valory/multiplexer_abci/0.1.0": "bafybeihs5fotoof627iqg2h5tsarbrvsla73agrmtqv2lnekinkrqypp2q", - "skill/valory/task_execution_abci/0.1.0": "bafybeiapixlxbnln52cgv6ibuxnei2yikbxgmsf2jtmrvane7s6fqidhwe", - "skill/valory/mech_abci/0.1.0": "bafybeiarmkauku6kv3ejt4l3derkgec6hfii7jr7eekrrfe36o6hycag5u", - "contract/valory/agent_mech/0.1.0": "bafybeigzl5sjks2tqszum6axrkwjlmybsgp54om5auybbdp3uyfx3zef7q", - "service/valory/mech/0.1.0": "bafybeigxdspczjnjiutd4olv2jcbqh6by4sbqpmxyvkz6irbgzc6fptipm", + "agent/valory/mech/0.1.0": "bafybeigfkzy7beir3tasc7ndcgfj3vcg6vrsjptojerum3ajccwuglczg4", + "skill/valory/mech_abci/0.1.0": "bafybeigzfa5pr647fg4nflzgie3xla4araotperomibfp2totzpaxwgmje", + "contract/valory/agent_mech/0.1.0": "bafybeidl6kwc3sgcxiphgb3osjqlqwylhqetv2nyv2fu6zxcgn5qctv2ju", + "service/valory/mech/0.1.0": "bafybeibulanqkh6hvo4cofquwtyvveala2bh7xtmembuq5zdzhgxdeet7a", "protocol/valory/acn_data_share/0.1.0": "bafybeieyixetwvz767zekhvg7r6etumyanzys6xbalx2brrfswybinnlhi", - "protocol/valory/default/1.0.0": "bafybeiecmut3235aen7wxukllv424f3dysvvlgfmn562kzdunc5hdj3hxu" + "protocol/valory/default/1.0.0": "bafybeiecmut3235aen7wxukllv424f3dysvvlgfmn562kzdunc5hdj3hxu", + "skill/valory/task_submission_abci/0.1.0": "bafybeidcjfmhtgwh24sgf3gmk6soiyr2fmaebjvphhz6xob6d5m6aeguce", + "skill/valory/task_execution/0.1.0": "bafybeih6caazog2vq34dupe4cbkv2v3zrffsmfztuvvshtku7tnhmvxcrq" }, "third_party": { "protocol/open_aea/signing/1.0.0": "bafybeifuxs7gdg2okbn7uofymenjlmnih2wxwkym44lsgwmklgwuckxm2m", diff --git a/packages/valory/agents/mech/aea-config.yaml b/packages/valory/agents/mech/aea-config.yaml index 7ce29aa9..46f1c9f7 100644 --- a/packages/valory/agents/mech/aea-config.yaml +++ b/packages/valory/agents/mech/aea-config.yaml @@ -14,31 +14,31 @@ connections: - valory/p2p_libp2p_client:0.1.0:bafybeihdnfdth3qgltefgrem7xyi4b3ejzaz67xglm2hbma2rfvpl2annq - valory/websocket_client:0.1.0:bafybeicz53kzs5uvyiod2azntl76zwgmpgr22ven4wl5fnwt2m546j3wsu contracts: -- valory/agent_mech:0.1.0:bafybeigzl5sjks2tqszum6axrkwjlmybsgp54om5auybbdp3uyfx3zef7q +- valory/agent_mech:0.1.0:bafybeidl6kwc3sgcxiphgb3osjqlqwylhqetv2nyv2fu6zxcgn5qctv2ju - valory/gnosis_safe:0.1.0:bafybeigvqg4lapdaa23dpc3pv67rdptdhey6e435mxqsw2gb2u74yw4yei - valory/gnosis_safe_proxy_factory:0.1.0:bafybeie4iivrxcd5dcwzj3y2t66mc5mdvtsuqu426gk2kcdc6fxbki6neu - valory/multisend:0.1.0:bafybeie7m7pjbnw7cccpbvmbgkut24dtlt4cgvug3tbac7gej37xvwbv3a - valory/service_registry:0.1.0:bafybeif6x4zvsokwcetbrjdb4uyv4l3pqx756cg2ohv2zgcky5yuiwuqvi protocols: -- valory/default:1.0.0:bafybeiecmut3235aen7wxukllv424f3dysvvlgfmn562kzdunc5hdj3hxu - open_aea/signing:1.0.0:bafybeifuxs7gdg2okbn7uofymenjlmnih2wxwkym44lsgwmklgwuckxm2m - valory/abci:0.1.0:bafybeigootsvqpk6th5xpdtzanxum3earifrrezfyhylfrit7yvqdrtgpe - valory/acn:1.1.0:bafybeiapa5ilsobggnspoqhspftwolrx52udrwmaxdxgrk26heuvl4oooa +- valory/acn_data_share:0.1.0:bafybeieyixetwvz767zekhvg7r6etumyanzys6xbalx2brrfswybinnlhi - valory/contract_api:1.0.0:bafybeiasywsvax45qmugus5kxogejj66c5taen27h4voriodz7rgushtqa +- valory/default:1.0.0:bafybeiecmut3235aen7wxukllv424f3dysvvlgfmn562kzdunc5hdj3hxu - valory/http:1.0.0:bafybeia5bxdua2i6chw6pg47bvoljzcpuqxzy4rdrorbdmcbnwmnfdobtu - valory/ipfs:0.1.0:bafybeibjzhsengtxfofqpxy6syamplevp35obemwfp4c5lhag3v2bvgysa - valory/ledger_api:1.0.0:bafybeigsvceac33asd6ecbqev34meyyjwu3rangenv6xp5rkxyz4krvcby - valory/tendermint:0.1.0:bafybeidjqmwvgi4rqgp65tbkhmi45fwn2odr5ecezw6q47hwitsgyw4jpa -- valory/acn_data_share:0.1.0:bafybeieyixetwvz767zekhvg7r6etumyanzys6xbalx2brrfswybinnlhi skills: - valory/abstract_abci:0.1.0:bafybeicg7dv7cff34nv2k2z47c4yp4kddsxp3wozonzow6tnvfvwndz3cy - valory/abstract_round_abci:0.1.0:bafybeigxjcci53vwytymzlhr37436yvenh7jup4astrn7dgyixo24aq2pq - valory/contract_subscription:0.1.0:bafybeifbgzfrhtdtendqzwmh3o436nyexwkif6mbvsouvk2ktdfk5lhe7y -- valory/mech_abci:0.1.0:bafybeiarmkauku6kv3ejt4l3derkgec6hfii7jr7eekrrfe36o6hycag5u -- valory/multiplexer_abci:0.1.0:bafybeihs5fotoof627iqg2h5tsarbrvsla73agrmtqv2lnekinkrqypp2q +- valory/mech_abci:0.1.0:bafybeigzfa5pr647fg4nflzgie3xla4araotperomibfp2totzpaxwgmje +- valory/task_execution:0.1.0:bafybeih6caazog2vq34dupe4cbkv2v3zrffsmfztuvvshtku7tnhmvxcrq - valory/registration_abci:0.1.0:bafybeibc4kczqbh23sc6tufrzn3axmhp3vjav7fa3u6cnpvolrbbc2fd7i - valory/reset_pause_abci:0.1.0:bafybeid445uy6wwvugf3byzl7r73c7teu6xr5ezxb4h7cxbenghg3copvy -- valory/task_execution_abci:0.1.0:bafybeiapixlxbnln52cgv6ibuxnei2yikbxgmsf2jtmrvane7s6fqidhwe +- valory/task_submission_abci:0.1.0:bafybeidcjfmhtgwh24sgf3gmk6soiyr2fmaebjvphhz6xob6d5m6aeguce - valory/termination_abci:0.1.0:bafybeiguy7pkrcptg6c754ioig4mlkr7truccym3fpv6jwpjx2tmpdbzhi - valory/transaction_settlement_abci:0.1.0:bafybeidpsnguxizkpihtkqzojr3em7yy7c6qc7gxpbh5vglmwws5wke7bi default_ledger: ethereum @@ -85,6 +85,7 @@ type: connection config: endpoint: ${str:wss://rpc.gnosischain.com/wss} target_skill_id: valory/contract_subscription:0.1.0 +is_abstract: true --- public_id: valory/contract_subscription:0.1.0:bafybeiby5ajjc7a3m2uq73d2pprx6enqt4ghfcq2gkmrtsr75e4d4napi4 type: skill @@ -101,6 +102,7 @@ models: params: args: use_polling: ${bool:false} +is_abstract: true --- public_id: valory/abci:0.1.0 type: connection @@ -131,28 +133,39 @@ type: skill models: params: args: - sleep_time: 1 + sleep_time: ${int:1} ipfs_fetch_timeout: ${float:15.0} - tendermint_check_sleep_delay: 3 + tendermint_check_sleep_delay: ${int:3} tendermint_p2p_url: ${str:localhost:26656} tendermint_com_url: ${str:http://localhost:8080} - tendermint_max_retries: 5 + tendermint_max_retries: ${int:5} tendermint_url: ${str:http://localhost:26657} use_termination: ${bool:false} agent_mech_contract_address: ${str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} round_timeout_seconds: ${float:30.0} reset_period_count: ${int:1000} on_chain_service_id: ${int:1} + share_tm_config_on_startup: ${bool:false} multisend_address: ${str:0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761} service_registry_address: ${str:0x9338b5153AE39BB89f50468E608eD9d764B755fD} setup: all_participants: ${list:["0x10E867Ac2Fb0Aa156ca81eF440a5cdf373bE1AaC"]} safe_contract_address: ${str:0x5e1D1eb61E1164D5a50b28C575dA73A29595dFf7} + consensus_threshold: ${int:null} +--- +public_id: valory/task_execution:0.1.0 +type: skill +models: + params: + args: + agent_mech_contract_address: ${str: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", "dummy_api_key"],["google_engine_id", "dummy_api_key"]]} - use_polling: ${bool:false} - polling_interval: ${int:25} + polling_interval: ${float:30.0} + agent_index: ${int:0} + num_agents: ${int:4} --- public_id: valory/ledger:0.19.0 type: connection @@ -160,6 +173,6 @@ config: ledger_apis: ethereum: address: ${str:https://rpc.gnosischain.com/} - chain_id: 100 - poa_chain: false - default_gas_price_strategy: eip1559 + chain_id: ${int:100} + poa_chain: ${bool:false} + default_gas_price_strategy: ${str:eip1559} diff --git a/packages/valory/contracts/agent_mech/contract.py b/packages/valory/contracts/agent_mech/contract.py index 05d62c93..6b9c2bf4 100644 --- a/packages/valory/contracts/agent_mech/contract.py +++ b/packages/valory/contracts/agent_mech/contract.py @@ -19,7 +19,7 @@ """This module contains the dynamic_contribution contract definition.""" -from typing import Any, cast +from typing import Any, Dict, List, cast from aea.common import JSONLike from aea.configurations.base import PublicId @@ -87,7 +87,7 @@ def get_state( @classmethod def get_deliver_data( - cls, ledger_api: LedgerApi, contract_address: str, request_id: int, data: bytes + cls, ledger_api: LedgerApi, contract_address: str, request_id: int, data: str ) -> JSONLike: """ Deliver a response to a request. @@ -104,7 +104,9 @@ def get_deliver_data( raise ValueError(f"Only EthereumApi is supported, got {type(ledger_api)}") contract_instance = cls.get_instance(ledger_api, contract_address) - data = contract_instance.encodeABI(fn_name="deliver", args=[request_id, data]) + data = contract_instance.encodeABI( + fn_name="deliver", args=[request_id, bytes.fromhex(data)] + ) return {"data": bytes.fromhex(data[2:])} # type: ignore @classmethod @@ -118,7 +120,7 @@ def get_request_events( """Get the Request events emitted by the contract.""" ledger_api = cast(EthereumApi, ledger_api) contract_instance = cls.get_instance(ledger_api, contract_address) - entries = contract_instance.events.Request.createFilter( + entries = contract_instance.events.Request.create_filter( fromBlock=from_block, toBlock=to_block, ).get_all_entries() @@ -143,7 +145,7 @@ def get_deliver_events( """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.createFilter( + entries = contract_instance.events.Deliver.create_filter( fromBlock=from_block, toBlock=to_block, ).get_all_entries() @@ -156,3 +158,27 @@ def get_deliver_events( for entry in entries ) return {"data": deliver_events} + + @classmethod + def get_undelivered_reqs( + cls, + ledger_api: LedgerApi, + contract_address: str, + from_block: BlockIdentifier = "earliest", + to_block: BlockIdentifier = "latest", + ) -> JSONLike: + """Get the requests that are not delivered.""" + requests: List[Dict[str, Any]] = cls.get_request_events( + ledger_api, contract_address, from_block, to_block + )["data"] + delivers: List[Dict[str, Any]] = cls.get_deliver_events( + ledger_api, contract_address, from_block, to_block + )["data"] + pending_tasks: List[Dict[str, Any]] = [] + for request in requests: + if request["requestId"] not in [ + deliver["requestId"] for deliver in delivers + ]: + # store each requests in the pending_tasks list, make sure each req is stored once + pending_tasks.append(request) + return {"data": pending_tasks} diff --git a/packages/valory/contracts/agent_mech/contract.yaml b/packages/valory/contracts/agent_mech/contract.yaml index ff203370..4813c85a 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: bafybeibexz4fzky74iss323khhvpqx2phetekwxdhx34o6darplliwfjxy + contract.py: bafybeidyh53cztzwsjndfgepmx57fc6swjk5qjzed24qavppjmruteny7q fingerprint_ignore_patterns: [] class_name: AgentMechContract contract_interface_paths: diff --git a/packages/valory/services/mech/service.yaml b/packages/valory/services/mech/service.yaml index ff05904c..a00d2edb 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:bafybeiafuo66iikppev3f5u4veky5ouytrzwjtc3bjupdmbvlnamu7fjnu +agent: valory/mech:0.1.0:bafybeigfkzy7beir3tasc7ndcgfj3vcg6vrsjptojerum3ajccwuglczg4 number_of_agents: 4 deployment: agent: @@ -25,205 +25,130 @@ public_id: valory/mech_abci:0.1.0 type: skill 0: models: - benchmark_tool: &id001 - args: - log_dir: /logs params: args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - finalize_timeout: 60.0 - genesis_config: &id002 - genesis_time: '2022-09-26T00:00:00.000000000Z' - chain_id: chain-c4daS1 - consensus_params: - block: - max_bytes: '22020096' - max_gas: '-1' - time_iota_ms: '1000' - evidence: - max_age_num_blocks: '100000' - max_age_duration: '172800000000000' - max_bytes: '1048576' - validator: - pub_key_types: - - ed25519 - version: {} - voting_power: '10' - history_check_timeout: 1205 - init_fallback_gas: 0 - keeper_allowed_retries: 3 - keeper_timeout: 30.0 - max_attempts: 10 - max_healthcheck: 120 multisend_address: ${MULTISEND_ADDRESS:str:0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761} on_chain_service_id: ${ON_CHAIN_SERVICE_ID:int:null} - reset_tendermint_after: 2 - retry_attempts: 400 - retry_timeout: 3 reset_pause_duration: ${RESET_PAUSE_DURATION:int:10} - request_retry_delay: 1.0 - request_timeout: 10.0 round_timeout_seconds: ${ROUND_TIMEOUT:float:150.0} - service_id: mech use_polling: ${USE_POLLING:bool:false} - polling_interval: ${POLLING_INTERVAL:int:25} service_registry_address: ${SERVICE_REGISTRY_ADDRESS:str:0x0000000000000000000000000000000000000000} - setup: &id003 + setup: &id001 all_participants: ${ALL_PARTICIPANTS:list:[]} safe_contract_address: ${SAFE_CONTRACT_ADDRESS:str:0x0000000000000000000000000000000000000000} - consensus_threshold: null + consensus_threshold: ${CONSENSUS_THRESHOLD:int:null} share_tm_config_on_startup: ${USE_ACN:bool:false} - sleep_time: 1 - tendermint_check_sleep_delay: 3 tendermint_com_url: ${TENDERMINT_COM_URL:str:http://localhost:8080} - tendermint_max_retries: 5 tendermint_url: ${TENDERMINT_URL:str:http://localhost:26657} tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} termination_sleep: ${TERMINATION_SLEEP:int:900} - tx_timeout: 10.0 use_termination: ${USE_TERMINATION:bool:false} - validate_timeout: 1205 agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} reset_period_count: ${RESET_PERIOD_COUNT:int:1000} - file_hash_to_tools_json: ${FILE_HASH_TO_TOOLS:list:[]} - api_keys_json: ${API_KEYS:list:[]} 1: models: - benchmark_tool: *id001 params: args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - finalize_timeout: 60.0 - genesis_config: *id002 - history_check_timeout: 1205 - init_fallback_gas: 0 - keeper_allowed_retries: 3 - keeper_timeout: 30.0 - max_attempts: 10 - max_healthcheck: 120 multisend_address: ${MULTISEND_ADDRESS:str:0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761} on_chain_service_id: ${ON_CHAIN_SERVICE_ID:int:null} - reset_tendermint_after: 2 - retry_attempts: 400 - retry_timeout: 3 reset_pause_duration: ${RESET_PAUSE_DURATION:int:10} - request_retry_delay: 1.0 - request_timeout: 10.0 round_timeout_seconds: ${ROUND_TIMEOUT:float:150.0} - service_id: mech + use_polling: ${USE_POLLING:bool:false} service_registry_address: ${SERVICE_REGISTRY_ADDRESS:str:0x0000000000000000000000000000000000000000} - setup: *id003 + setup: *id001 share_tm_config_on_startup: ${USE_ACN:bool:false} - sleep_time: 1 - tendermint_check_sleep_delay: 3 - use_polling: ${USE_POLLING:str:false} - polling_interval: ${POLLING_INTERVAL:int:25} tendermint_com_url: ${TENDERMINT_COM_URL:str:http://localhost:8080} - tendermint_max_retries: 5 tendermint_url: ${TENDERMINT_URL:str:http://localhost:26657} - tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_1:str:node0:26656} + tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} termination_sleep: ${TERMINATION_SLEEP:int:900} - tx_timeout: 10.0 use_termination: ${USE_TERMINATION:bool:false} - validate_timeout: 1205 agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} reset_period_count: ${RESET_PERIOD_COUNT:int:1000} - file_hash_to_tools_json: ${FILE_HASH_TO_TOOLS:str:null} - api_keys_json: ${API_KEYS:list:[]} 2: models: - benchmark_tool: *id001 params: args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - finalize_timeout: 60.0 - genesis_config: *id002 - history_check_timeout: 1205 - init_fallback_gas: 0 - keeper_allowed_retries: 3 - keeper_timeout: 30.0 - max_attempts: 10 - max_healthcheck: 120 multisend_address: ${MULTISEND_ADDRESS:str:0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761} on_chain_service_id: ${ON_CHAIN_SERVICE_ID:int:null} - reset_tendermint_after: 2 - retry_attempts: 400 - retry_timeout: 3 reset_pause_duration: ${RESET_PAUSE_DURATION:int:10} - request_retry_delay: 1.0 - request_timeout: 10.0 round_timeout_seconds: ${ROUND_TIMEOUT:float:150.0} - service_id: mech + use_polling: ${USE_POLLING:bool:false} service_registry_address: ${SERVICE_REGISTRY_ADDRESS:str:0x0000000000000000000000000000000000000000} - setup: *id003 + setup: *id001 share_tm_config_on_startup: ${USE_ACN:bool:false} - use_polling: ${USE_POLLING:str:false} - sleep_time: 1 - tendermint_check_sleep_delay: 3 tendermint_com_url: ${TENDERMINT_COM_URL:str:http://localhost:8080} - tendermint_max_retries: 5 tendermint_url: ${TENDERMINT_URL:str:http://localhost:26657} - tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_2:str:node0:26656} + tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} termination_sleep: ${TERMINATION_SLEEP:int:900} - polling_interval: ${POLLING_INTERVAL:int:25} - tx_timeout: 10.0 use_termination: ${USE_TERMINATION:bool:false} - validate_timeout: 1205 agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} reset_period_count: ${RESET_PERIOD_COUNT:int:1000} - file_hash_to_tools_json: ${FILE_HASH_TO_TOOLS:str:null} - api_keys_json: ${API_KEYS:list:[]} 3: models: - benchmark_tool: *id001 params: args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - finalize_timeout: 60.0 - genesis_config: *id002 - history_check_timeout: 1205 - init_fallback_gas: 0 - keeper_allowed_retries: 3 - keeper_timeout: 30.0 - max_attempts: 10 - max_healthcheck: 120 multisend_address: ${MULTISEND_ADDRESS:str:0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761} on_chain_service_id: ${ON_CHAIN_SERVICE_ID:int:null} - reset_tendermint_after: 2 - retry_attempts: 400 - retry_timeout: 3 reset_pause_duration: ${RESET_PAUSE_DURATION:int:10} - request_retry_delay: 1.0 - request_timeout: 10.0 round_timeout_seconds: ${ROUND_TIMEOUT:float:150.0} use_polling: ${USE_POLLING:bool:false} - polling_interval: ${POLLING_INTERVAL:int:25} - service_id: mech service_registry_address: ${SERVICE_REGISTRY_ADDRESS:str:0x0000000000000000000000000000000000000000} - setup: *id003 + setup: *id001 share_tm_config_on_startup: ${USE_ACN:bool:false} - sleep_time: 1 - tendermint_check_sleep_delay: 3 tendermint_com_url: ${TENDERMINT_COM_URL:str:http://localhost:8080} - tendermint_max_retries: 5 tendermint_url: ${TENDERMINT_URL:str:http://localhost:26657} - tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_3:str:node0:26656} + tendermint_p2p_url: ${TM_P2P_ENDPOINT_NODE_0:str:node0:26656} termination_sleep: ${TERMINATION_SLEEP:int:900} - tx_timeout: 10.0 use_termination: ${USE_TERMINATION:bool:false} - validate_timeout: 1205 agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} reset_period_count: ${RESET_PERIOD_COUNT:int:1000} - file_hash_to_tools_json: ${FILE_HASH_TO_TOOLS:str:null} +--- +public_id: valory/task_execution:0.1.0 +type: skill +0: + models: + params: + args: + agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + 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} +1: + models: + params: + args: + agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + 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} +2: + models: + params: + args: + agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + 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} +3: + models: + params: + args: + agent_mech_contract_address: ${AGENT_MECH_CONTRACT_ADDRESS:str:0xFf82123dFB52ab75C417195c5fDB87630145ae81} + 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} --- public_id: valory/ledger:0.19.0 type: connection diff --git a/packages/valory/skills/mech_abci/behaviours.py b/packages/valory/skills/mech_abci/behaviours.py index a55f9c5d..41895897 100644 --- a/packages/valory/skills/mech_abci/behaviours.py +++ b/packages/valory/skills/mech_abci/behaviours.py @@ -26,7 +26,6 @@ BaseBehaviour, ) from packages.valory.skills.mech_abci.composition import MechAbciApp -from packages.valory.skills.multiplexer_abci.behaviours import MultiplexerRoundBehaviour from packages.valory.skills.registration_abci.behaviours import ( AgentRegistrationRoundBehaviour, RegistrationStartupBehaviour, @@ -34,8 +33,8 @@ from packages.valory.skills.reset_pause_abci.behaviours import ( ResetPauseABCIConsensusBehaviour, ) -from packages.valory.skills.task_execution_abci.behaviours import ( - TaskExecutionRoundBehaviour, +from packages.valory.skills.task_submission_abci.behaviours import ( + TaskSubmissionRoundBehaviour, ) from packages.valory.skills.termination_abci.behaviours import ( BackgroundBehaviour, @@ -52,8 +51,7 @@ class MechConsensusBehaviour(AbstractRoundBehaviour): initial_behaviour_cls = RegistrationStartupBehaviour abci_app_cls = MechAbciApp behaviours: Set[Type[BaseBehaviour]] = { - *MultiplexerRoundBehaviour.behaviours, - *TaskExecutionRoundBehaviour.behaviours, + *TaskSubmissionRoundBehaviour.behaviours, *AgentRegistrationRoundBehaviour.behaviours, *ResetPauseABCIConsensusBehaviour.behaviours, *TransactionSettlementRoundBehaviour.behaviours, diff --git a/packages/valory/skills/mech_abci/composition.py b/packages/valory/skills/mech_abci/composition.py index 4cc67709..bf21245f 100644 --- a/packages/valory/skills/mech_abci/composition.py +++ b/packages/valory/skills/mech_abci/composition.py @@ -19,10 +19,9 @@ """This package contains round behaviours of MechAbciApp.""" -import packages.valory.skills.multiplexer_abci.rounds as MultiplexerAbciApp import packages.valory.skills.registration_abci.rounds as RegistrationAbci import packages.valory.skills.reset_pause_abci.rounds as ResetAndPauseAbci -import packages.valory.skills.task_execution_abci.rounds as TaskExecutionAbciApp +import packages.valory.skills.task_submission_abci.rounds as TaskSubmissionAbciApp import packages.valory.skills.transaction_settlement_abci.rounds as TransactionSubmissionAbciApp from packages.valory.skills.abstract_round_abci.abci_app_chain import ( AbciAppTransitionMapping, @@ -36,22 +35,20 @@ # Here we define how the transition between the FSMs should happen # more information here: https://docs.autonolas.network/fsm_app_introduction/#composition-of-fsm-apps abci_app_transition_mapping: AbciAppTransitionMapping = { - RegistrationAbci.FinishedRegistrationRound: MultiplexerAbciApp.MultiplexerRound, - MultiplexerAbciApp.FinishedMultiplexerResetRound: ResetAndPauseAbci.ResetAndPauseRound, - MultiplexerAbciApp.FinishedMultiplexerExecuteRound: TaskExecutionAbciApp.TaskExecutionRound, - TaskExecutionAbciApp.FinishedTaskExecutionRound: TransactionSubmissionAbciApp.RandomnessTransactionSubmissionRound, # pylint: disable=C0301 - TaskExecutionAbciApp.FinishedTaskExecutionWithErrorRound: MultiplexerAbciApp.MultiplexerRound, - TransactionSubmissionAbciApp.FinishedTransactionSubmissionRound: MultiplexerAbciApp.MultiplexerRound, - TransactionSubmissionAbciApp.FailedRound: TaskExecutionAbciApp.TaskExecutionRound, - ResetAndPauseAbci.FinishedResetAndPauseRound: MultiplexerAbciApp.MultiplexerRound, + RegistrationAbci.FinishedRegistrationRound: TaskSubmissionAbciApp.TaskPoolingRound, + TaskSubmissionAbciApp.FinishedTaskPoolingRound: TransactionSubmissionAbciApp.RandomnessTransactionSubmissionRound, # pylint: disable=C0301 + TaskSubmissionAbciApp.FinishedTaskExecutionWithErrorRound: ResetAndPauseAbci.ResetAndPauseRound, + TaskSubmissionAbciApp.FinishedWithoutTasksRound: ResetAndPauseAbci.ResetAndPauseRound, + TransactionSubmissionAbciApp.FinishedTransactionSubmissionRound: ResetAndPauseAbci.ResetAndPauseRound, + TransactionSubmissionAbciApp.FailedRound: ResetAndPauseAbci.ResetAndPauseRound, + ResetAndPauseAbci.FinishedResetAndPauseRound: TaskSubmissionAbciApp.TaskPoolingRound, ResetAndPauseAbci.FinishedResetAndPauseErrorRound: RegistrationAbci.RegistrationRound, } MechAbciApp = chain( ( RegistrationAbci.AgentRegistrationAbciApp, - MultiplexerAbciApp.MultiplexerAbciApp, - TaskExecutionAbciApp.TaskExecutionAbciApp, + TaskSubmissionAbciApp.TaskSubmissionAbciApp, ResetAndPauseAbci.ResetPauseAbciApp, TransactionSubmissionAbciApp.TransactionSubmissionAbciApp, ), diff --git a/packages/valory/skills/mech_abci/dialogues.py b/packages/valory/skills/mech_abci/dialogues.py index e628cec7..c5eca099 100644 --- a/packages/valory/skills/mech_abci/dialogues.py +++ b/packages/valory/skills/mech_abci/dialogues.py @@ -61,10 +61,10 @@ from packages.valory.skills.abstract_round_abci.dialogues import ( TendermintDialogues as BaseTendermintDialogues, ) -from packages.valory.skills.task_execution_abci.dialogues import ( +from packages.valory.skills.task_submission_abci.dialogues import ( AcnDataShareDialogue as BaseAcnDataShareDialogue, ) -from packages.valory.skills.task_execution_abci.dialogues import ( +from packages.valory.skills.task_submission_abci.dialogues import ( AcnDataShareDialogues as BaseAcnDataShareDialogues, ) diff --git a/packages/valory/skills/mech_abci/fsm_specification.yaml b/packages/valory/skills/mech_abci/fsm_specification.yaml index 2d498ede..27d8c52e 100644 --- a/packages/valory/skills/mech_abci/fsm_specification.yaml +++ b/packages/valory/skills/mech_abci/fsm_specification.yaml @@ -4,7 +4,6 @@ alphabet_in: - CHECK_TIMEOUT - DONE - ERROR -- EXECUTE - FINALIZATION_FAILED - FINALIZE_TIMEOUT - INCORRECT_SERIALIZATION @@ -12,14 +11,12 @@ alphabet_in: - NEGATIVE - NONE - NO_MAJORITY -- RESET +- NO_TASKS - RESET_AND_PAUSE_TIMEOUT - RESET_TIMEOUT - ROUND_TIMEOUT - SUSPICIOUS_ACTIVITY -- TASK_EXECUTION_ROUND_TIMEOUT - VALIDATE_TIMEOUT -- WAIT default_start_state: RegistrationStartupRound final_states: [] label: MechAbciApp @@ -31,7 +28,6 @@ states: - CheckTransactionHistoryRound - CollectSignatureRound - FinalizationRound -- MultiplexerRound - RandomnessTransactionSubmissionRound - RegistrationRound - RegistrationStartupRound @@ -41,20 +37,21 @@ states: - SelectKeeperTransactionSubmissionBAfterTimeoutRound - SelectKeeperTransactionSubmissionBRound - SynchronizeLateMessagesRound -- TaskExecutionRound +- TaskPoolingRound +- TransactionPreparationRound - ValidateTransactionRound transition_func: (CheckLateTxHashesRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound (CheckLateTxHashesRound, CHECK_TIMEOUT): CheckLateTxHashesRound - (CheckLateTxHashesRound, DONE): MultiplexerRound - (CheckLateTxHashesRound, NEGATIVE): TaskExecutionRound - (CheckLateTxHashesRound, NONE): TaskExecutionRound - (CheckLateTxHashesRound, NO_MAJORITY): TaskExecutionRound + (CheckLateTxHashesRound, DONE): ResetAndPauseRound + (CheckLateTxHashesRound, NEGATIVE): ResetAndPauseRound + (CheckLateTxHashesRound, NONE): ResetAndPauseRound + (CheckLateTxHashesRound, NO_MAJORITY): ResetAndPauseRound (CheckTransactionHistoryRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound (CheckTransactionHistoryRound, CHECK_TIMEOUT): CheckTransactionHistoryRound - (CheckTransactionHistoryRound, DONE): MultiplexerRound + (CheckTransactionHistoryRound, DONE): ResetAndPauseRound (CheckTransactionHistoryRound, NEGATIVE): SelectKeeperTransactionSubmissionBRound - (CheckTransactionHistoryRound, NONE): TaskExecutionRound + (CheckTransactionHistoryRound, NONE): ResetAndPauseRound (CheckTransactionHistoryRound, NO_MAJORITY): CheckTransactionHistoryRound (CollectSignatureRound, DONE): FinalizationRound (CollectSignatureRound, NO_MAJORITY): ResetRound @@ -65,45 +62,42 @@ transition_func: (FinalizationRound, FINALIZATION_FAILED): SelectKeeperTransactionSubmissionBRound (FinalizationRound, FINALIZE_TIMEOUT): SelectKeeperTransactionSubmissionBAfterTimeoutRound (FinalizationRound, INSUFFICIENT_FUNDS): SelectKeeperTransactionSubmissionBRound - (MultiplexerRound, EXECUTE): TaskExecutionRound - (MultiplexerRound, NO_MAJORITY): MultiplexerRound - (MultiplexerRound, RESET): ResetAndPauseRound - (MultiplexerRound, ROUND_TIMEOUT): MultiplexerRound - (MultiplexerRound, WAIT): MultiplexerRound (RandomnessTransactionSubmissionRound, DONE): SelectKeeperTransactionSubmissionARound (RandomnessTransactionSubmissionRound, NO_MAJORITY): RandomnessTransactionSubmissionRound (RandomnessTransactionSubmissionRound, ROUND_TIMEOUT): RandomnessTransactionSubmissionRound - (RegistrationRound, DONE): MultiplexerRound + (RegistrationRound, DONE): TaskPoolingRound (RegistrationRound, NO_MAJORITY): RegistrationRound - (RegistrationStartupRound, DONE): MultiplexerRound - (ResetAndPauseRound, DONE): MultiplexerRound + (RegistrationStartupRound, DONE): TaskPoolingRound + (ResetAndPauseRound, DONE): TaskPoolingRound (ResetAndPauseRound, NO_MAJORITY): RegistrationRound (ResetAndPauseRound, RESET_AND_PAUSE_TIMEOUT): RegistrationRound (ResetRound, DONE): RandomnessTransactionSubmissionRound - (ResetRound, NO_MAJORITY): TaskExecutionRound - (ResetRound, RESET_TIMEOUT): TaskExecutionRound + (ResetRound, NO_MAJORITY): ResetAndPauseRound + (ResetRound, RESET_TIMEOUT): ResetAndPauseRound (SelectKeeperTransactionSubmissionARound, DONE): CollectSignatureRound - (SelectKeeperTransactionSubmissionARound, INCORRECT_SERIALIZATION): TaskExecutionRound + (SelectKeeperTransactionSubmissionARound, INCORRECT_SERIALIZATION): ResetAndPauseRound (SelectKeeperTransactionSubmissionARound, NO_MAJORITY): ResetRound (SelectKeeperTransactionSubmissionARound, ROUND_TIMEOUT): SelectKeeperTransactionSubmissionARound (SelectKeeperTransactionSubmissionBAfterTimeoutRound, CHECK_HISTORY): CheckTransactionHistoryRound (SelectKeeperTransactionSubmissionBAfterTimeoutRound, CHECK_LATE_ARRIVING_MESSAGE): SynchronizeLateMessagesRound (SelectKeeperTransactionSubmissionBAfterTimeoutRound, DONE): FinalizationRound - (SelectKeeperTransactionSubmissionBAfterTimeoutRound, INCORRECT_SERIALIZATION): TaskExecutionRound + (SelectKeeperTransactionSubmissionBAfterTimeoutRound, INCORRECT_SERIALIZATION): ResetAndPauseRound (SelectKeeperTransactionSubmissionBAfterTimeoutRound, NO_MAJORITY): ResetRound (SelectKeeperTransactionSubmissionBAfterTimeoutRound, ROUND_TIMEOUT): SelectKeeperTransactionSubmissionBAfterTimeoutRound (SelectKeeperTransactionSubmissionBRound, DONE): FinalizationRound - (SelectKeeperTransactionSubmissionBRound, INCORRECT_SERIALIZATION): TaskExecutionRound + (SelectKeeperTransactionSubmissionBRound, INCORRECT_SERIALIZATION): ResetAndPauseRound (SelectKeeperTransactionSubmissionBRound, NO_MAJORITY): ResetRound (SelectKeeperTransactionSubmissionBRound, ROUND_TIMEOUT): SelectKeeperTransactionSubmissionBRound (SynchronizeLateMessagesRound, DONE): CheckLateTxHashesRound (SynchronizeLateMessagesRound, NONE): SelectKeeperTransactionSubmissionBRound (SynchronizeLateMessagesRound, ROUND_TIMEOUT): SynchronizeLateMessagesRound - (SynchronizeLateMessagesRound, SUSPICIOUS_ACTIVITY): TaskExecutionRound - (TaskExecutionRound, DONE): RandomnessTransactionSubmissionRound - (TaskExecutionRound, ERROR): MultiplexerRound - (TaskExecutionRound, TASK_EXECUTION_ROUND_TIMEOUT): TaskExecutionRound - (ValidateTransactionRound, DONE): MultiplexerRound + (SynchronizeLateMessagesRound, SUSPICIOUS_ACTIVITY): ResetAndPauseRound + (TaskPoolingRound, DONE): TransactionPreparationRound + (TaskPoolingRound, NO_TASKS): ResetAndPauseRound + (TransactionPreparationRound, DONE): RandomnessTransactionSubmissionRound + (TransactionPreparationRound, ERROR): ResetAndPauseRound + (TransactionPreparationRound, NO_MAJORITY): ResetAndPauseRound + (ValidateTransactionRound, DONE): ResetAndPauseRound (ValidateTransactionRound, NEGATIVE): CheckTransactionHistoryRound (ValidateTransactionRound, NONE): SelectKeeperTransactionSubmissionBRound (ValidateTransactionRound, NO_MAJORITY): ValidateTransactionRound diff --git a/packages/valory/skills/mech_abci/models.py b/packages/valory/skills/mech_abci/models.py index 01c9ae64..4beb7250 100644 --- a/packages/valory/skills/mech_abci/models.py +++ b/packages/valory/skills/mech_abci/models.py @@ -26,18 +26,14 @@ ) from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests from packages.valory.skills.mech_abci.composition import MechAbciApp -from packages.valory.skills.multiplexer_abci.models import ( - Params as MultiplexerAbciParams, -) -from packages.valory.skills.multiplexer_abci.rounds import Event as MultiplexerEvent from packages.valory.skills.reset_pause_abci.rounds import Event as ResetPauseEvent -from packages.valory.skills.task_execution_abci.models import ( +from packages.valory.skills.task_submission_abci.models import ( Params as TaskExecutionAbciParams, ) -from packages.valory.skills.task_execution_abci.models import ( +from packages.valory.skills.task_submission_abci.models import ( SharedState as TaskExecSharedState, ) -from packages.valory.skills.task_execution_abci.rounds import ( +from packages.valory.skills.task_submission_abci.rounds import ( Event as TaskExecutionEvent, ) from packages.valory.skills.termination_abci.models import TerminationParams @@ -46,7 +42,6 @@ ) -MultiplexerParams = MultiplexerAbciParams TaskExecutionParams = TaskExecutionAbciParams @@ -75,10 +70,6 @@ def setup(self) -> None: """Set up.""" super().setup() - MechAbciApp.event_to_timeout[ - MultiplexerEvent.ROUND_TIMEOUT - ] = self.context.params.round_timeout_seconds - MechAbciApp.event_to_timeout[ TaskExecutionEvent.ROUND_TIMEOUT ] = self.context.params.round_timeout_seconds @@ -108,5 +99,5 @@ def setup(self) -> None: ) -class Params(MultiplexerParams, TaskExecutionParams, TerminationParams): # type: ignore +class Params(TaskExecutionParams, TerminationParams): # type: ignore """A model to represent params for multiple abci apps.""" diff --git a/packages/valory/skills/mech_abci/skill.yaml b/packages/valory/skills/mech_abci/skill.yaml index 6fa2d791..3e385734 100644 --- a/packages/valory/skills/mech_abci/skill.yaml +++ b/packages/valory/skills/mech_abci/skill.yaml @@ -7,22 +7,21 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeihscyr3poal6eyk6jeywtbdq552piwpbh2uo5h7bepjqdeivgiyem - behaviours.py: bafybeihgtg4l7qcu33ptyfn6cnohm3tcxlpkdqijyc5zjxmci6lqzxmogy - composition.py: bafybeiektjao3czojlipbcj2oglrk4hsch76d7ft3zw3vpcerewvvi6idy - dialogues.py: bafybeidhmgdnhxcgv35vahz3ycoiduug23kfyqvttqnywjp3eiuqal4bzy - fsm_specification.yaml: bafybeid6lveecd7oaudgasv36yta7pfajhgx6qsxl5cxmc7pcb23s5a4se + behaviours.py: bafybeigpumxxutdrrgot44mqhqddfms3exrtsscvxfyl45pxufogjskweu + composition.py: bafybeiga6prq6yftjkh2bol6vym4iarvo4i4senk3avmukneap2drtjjz4 + dialogues.py: bafybeifhydd6xmstbh2jx5igj33upip5a3hhlcaxttfsc77heszqmru7ri + fsm_specification.yaml: bafybeib6j6etn6jd2cvggiswag2jrvlsxlnxb6sfgaf7oibhsd2mk65vw4 handlers.py: bafybeiffuduhg433qsu6lbet5jsaub63bzv2l4x756aj2fbnu5bnfu4ble - models.py: bafybeihdti6xgdtbd5usjwwgpl2mhop7mdsdsj3hm2325b2iscpka7jxlq + models.py: bafybeic3pjxw7py6jpiaaxjtcufzcjmyldj2fdhpkik5qnj4hpruuxcu4q fingerprint_ignore_patterns: [] connections: [] contracts: [] protocols: [] skills: - valory/abstract_round_abci:0.1.0:bafybeigxjcci53vwytymzlhr37436yvenh7jup4astrn7dgyixo24aq2pq -- valory/multiplexer_abci:0.1.0:bafybeihs5fotoof627iqg2h5tsarbrvsla73agrmtqv2lnekinkrqypp2q - valory/registration_abci:0.1.0:bafybeibc4kczqbh23sc6tufrzn3axmhp3vjav7fa3u6cnpvolrbbc2fd7i - valory/reset_pause_abci:0.1.0:bafybeid445uy6wwvugf3byzl7r73c7teu6xr5ezxb4h7cxbenghg3copvy -- valory/task_execution_abci:0.1.0:bafybeiapixlxbnln52cgv6ibuxnei2yikbxgmsf2jtmrvane7s6fqidhwe +- valory/task_submission_abci:0.1.0:bafybeidcjfmhtgwh24sgf3gmk6soiyr2fmaebjvphhz6xob6d5m6aeguce - valory/termination_abci:0.1.0:bafybeiguy7pkrcptg6c754ioig4mlkr7truccym3fpv6jwpjx2tmpdbzhi - valory/transaction_settlement_abci:0.1.0:bafybeidpsnguxizkpihtkqzojr3em7yy7c6qc7gxpbh5vglmwws5wke7bi behaviours: @@ -127,7 +126,7 @@ models: request_timeout: 10.0 reset_pause_duration: 10 reset_period_count: 100 - reset_tendermint_after: 2 + reset_tendermint_after: 10 retry_attempts: 400 retry_timeout: 3 round_timeout_seconds: 30.0 @@ -150,6 +149,7 @@ models: use_polling: false use_termination: false validate_timeout: 1205 + task_wait_timeout: 15.0 class_name: Params randomness_api: args: diff --git a/packages/valory/skills/multiplexer_abci/behaviours.py b/packages/valory/skills/multiplexer_abci/behaviours.py deleted file mode 100644 index dbfe71e9..00000000 --- a/packages/valory/skills/multiplexer_abci/behaviours.py +++ /dev/null @@ -1,172 +0,0 @@ -# -*- 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 package contains round behaviours of MultiplexerAbciApp.""" - -from abc import ABC -from typing import Dict, Generator, List, Set, Type, cast - -from packages.valory.contracts.agent_mech.contract import AgentMechContract -from packages.valory.protocols.contract_api import ContractApiMessage -from packages.valory.skills.abstract_round_abci.base import AbstractRound -from packages.valory.skills.abstract_round_abci.behaviours import ( - AbstractRoundBehaviour, - BaseBehaviour, -) -from packages.valory.skills.multiplexer_abci.models import Params, SharedState -from packages.valory.skills.multiplexer_abci.rounds import ( - MultiplexerAbciApp, - MultiplexerPayload, - MultiplexerRound, - SynchronizedData, -) - - -class MultiplexerBaseBehaviour(BaseBehaviour, ABC): - """Base behaviour for the multiplexer_abci skill.""" - - @property - def synchronized_data(self) -> SynchronizedData: - """Return the synchronized data.""" - return cast(SynchronizedData, super().synchronized_data) - - @property - def params(self) -> Params: - """Return the params.""" - return cast(Params, super().params) - - @property - def shared_state(self) -> SharedState: - """Return the params.""" - return cast(SharedState, self.context.state) - - def _get_requests(self, from_block: int = 0) -> Generator[None, None, List[Dict]]: - """Get the contract requests.""" - response = yield from self.get_contract_api_response( - performative=ContractApiMessage.Performative.GET_STATE, # type: ignore - contract_id=str(AgentMechContract.contract_id), - contract_callable="get_request_events", - contract_address=self.params.agent_mech_contract_address, - from_block=from_block, - ) - if response.performative != ContractApiMessage.Performative.STATE: - self.context.logger.error( - f"Couldn't get the latest `Request` event. " - f"Expected response performative {ContractApiMessage.Performative.STATE.value}, " # type: ignore - f"received {response.performative.value}." - ) - return [] - - requests = cast(List[Dict], response.state.body.get("data")) - return requests - - def _get_deliver(self, from_block: int = 0) -> Generator[None, None, List[Dict]]: - """Get the contract delivers.""" - response = yield from self.get_contract_api_response( - performative=ContractApiMessage.Performative.GET_STATE, # type: ignore - contract_id=str(AgentMechContract.contract_id), - contract_callable="get_deliver_events", - contract_address=self.params.agent_mech_contract_address, - from_block=from_block, - ) - if response.performative != ContractApiMessage.Performative.STATE: - self.context.logger.error( - f"Couldn't get the latest `Deliver` event. " - f"Expected response performative {ContractApiMessage.Performative.STATE.value}, " # type: ignore - f"received {response.performative.value}." - ) - return [] - - delivers = cast(List[Dict], response.state.body.get("data")) - return delivers - - def extend_pending_tasks(self) -> Generator[None, None, List[Dict]]: - """Get the requests to send to the mech service.""" - from_block = self.shared_state.last_processed_request_block_number - requests = yield from self._get_requests(from_block) - delivers = yield from self._get_deliver(from_block) - pending_tasks = self.context.shared_state.get("pending_tasks", []) - for request in requests: - if ( - request["block_number"] - > self.shared_state.last_processed_request_block_number - ): - self.shared_state.last_processed_request_block_number = request[ - "block_number" - ] - - if request["requestId"] not in [ - deliver["requestId"] for deliver in delivers - ] and request["requestId"] not in [ - pending_req["requestId"] for pending_req in pending_tasks - ]: - # store each requests in the pending_tasks list, make sure each req is stored once - pending_tasks.append(request) - pending_tasks.sort(key=lambda x: x["block_number"]) - return pending_tasks - - -class MultiplexerBehaviour(MultiplexerBaseBehaviour): - """MultiplexerBehaviour""" - - matching_round: Type[AbstractRound] = MultiplexerRound - - def async_act(self) -> Generator: - """Do the act, supporting asynchronous execution.""" - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - - payload_content = MultiplexerRound.WAIT_PAYLOAD - - period_counter = self.synchronized_data.period_counter - do_reset = period_counter % self.params.reset_period_count == 0 - should_poll_events = self.params.use_polling and ( - period_counter % self.params.polling_interval == 0 - ) - self.context.logger.info( - f"Period counter: {period_counter}/{self.params.reset_period_count}. Do reset? {do_reset}" - ) - self.context.logger.info( - f"Pending tasks: {self.context.shared_state.get('pending_tasks', [])}" - ) - if should_poll_events: - pending_tasks = yield from self.extend_pending_tasks() - self.context.shared_state["pending_tasks"] = pending_tasks - - if self.context.shared_state.get("pending_tasks", []): - payload_content = MultiplexerRound.EXECUTE_PAYLOAD - elif do_reset: - payload_content = MultiplexerRound.RESET_PAYLOAD - - sender = self.context.agent_address - payload = MultiplexerPayload(sender=sender, content=payload_content) - - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - - self.set_done() - - -class MultiplexerRoundBehaviour(AbstractRoundBehaviour): - """MultiplexerRoundBehaviour""" - - initial_behaviour_cls = MultiplexerBehaviour - abci_app_cls = MultiplexerAbciApp # type: ignore - behaviours: Set[Type[BaseBehaviour]] = {MultiplexerBehaviour} # type: ignore diff --git a/packages/valory/skills/multiplexer_abci/dialogues.py b/packages/valory/skills/multiplexer_abci/dialogues.py deleted file mode 100644 index b6775562..00000000 --- a/packages/valory/skills/multiplexer_abci/dialogues.py +++ /dev/null @@ -1,91 +0,0 @@ -# -*- 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 dialogues of the MultiplexerAbciApp.""" - -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogue as BaseAbciDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - AbciDialogues as BaseAbciDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogue as BaseContractApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - ContractApiDialogues as BaseContractApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogue as BaseHttpDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - HttpDialogues as BaseHttpDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogue as BaseIpfsDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - IpfsDialogues as BaseIpfsDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogue as BaseLedgerApiDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - LedgerApiDialogues as BaseLedgerApiDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogue as BaseSigningDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - SigningDialogues as BaseSigningDialogues, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogue as BaseTendermintDialogue, -) -from packages.valory.skills.abstract_round_abci.dialogues import ( - TendermintDialogues as BaseTendermintDialogues, -) - - -AbciDialogue = BaseAbciDialogue -AbciDialogues = BaseAbciDialogues - - -HttpDialogue = BaseHttpDialogue -HttpDialogues = BaseHttpDialogues - - -SigningDialogue = BaseSigningDialogue -SigningDialogues = BaseSigningDialogues - - -LedgerApiDialogue = BaseLedgerApiDialogue -LedgerApiDialogues = BaseLedgerApiDialogues - - -ContractApiDialogue = BaseContractApiDialogue -ContractApiDialogues = BaseContractApiDialogues - - -TendermintDialogue = BaseTendermintDialogue -TendermintDialogues = BaseTendermintDialogues - - -IpfsDialogue = BaseIpfsDialogue -IpfsDialogues = BaseIpfsDialogues diff --git a/packages/valory/skills/multiplexer_abci/fsm_specification.yaml b/packages/valory/skills/multiplexer_abci/fsm_specification.yaml deleted file mode 100644 index 658a8b03..00000000 --- a/packages/valory/skills/multiplexer_abci/fsm_specification.yaml +++ /dev/null @@ -1,23 +0,0 @@ -alphabet_in: -- EXECUTE -- NO_MAJORITY -- RESET -- ROUND_TIMEOUT -- WAIT -default_start_state: MultiplexerRound -final_states: -- FinishedMultiplexerExecuteRound -- FinishedMultiplexerResetRound -label: MultiplexerAbciApp -start_states: -- MultiplexerRound -states: -- FinishedMultiplexerExecuteRound -- FinishedMultiplexerResetRound -- MultiplexerRound -transition_func: - (MultiplexerRound, EXECUTE): FinishedMultiplexerExecuteRound - (MultiplexerRound, NO_MAJORITY): MultiplexerRound - (MultiplexerRound, RESET): FinishedMultiplexerResetRound - (MultiplexerRound, ROUND_TIMEOUT): MultiplexerRound - (MultiplexerRound, WAIT): MultiplexerRound diff --git a/packages/valory/skills/multiplexer_abci/handlers.py b/packages/valory/skills/multiplexer_abci/handlers.py deleted file mode 100644 index 7ed387ca..00000000 --- a/packages/valory/skills/multiplexer_abci/handlers.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- 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 handlers for the skill of MultiplexerAbciApp.""" - -from packages.valory.skills.abstract_round_abci.handlers import ( - ABCIRoundHandler as BaseABCIRoundHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - ContractApiHandler as BaseContractApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - HttpHandler as BaseHttpHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - IpfsHandler as BaseIpfsHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - LedgerApiHandler as BaseLedgerApiHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - SigningHandler as BaseSigningHandler, -) -from packages.valory.skills.abstract_round_abci.handlers import ( - TendermintHandler as BaseTendermintHandler, -) - - -ABCIHandler = BaseABCIRoundHandler -HttpHandler = BaseHttpHandler -SigningHandler = BaseSigningHandler -LedgerApiHandler = BaseLedgerApiHandler -ContractApiHandler = BaseContractApiHandler -TendermintHandler = BaseTendermintHandler -IpfsHandler = BaseIpfsHandler diff --git a/packages/valory/skills/multiplexer_abci/rounds.py b/packages/valory/skills/multiplexer_abci/rounds.py deleted file mode 100644 index 24393ba0..00000000 --- a/packages/valory/skills/multiplexer_abci/rounds.py +++ /dev/null @@ -1,162 +0,0 @@ -# -*- 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 package contains the rounds of MultiplexerAbciApp.""" - -from enum import Enum -from typing import Dict, FrozenSet, Optional, Set, Tuple, cast - -from packages.valory.skills.abstract_round_abci.base import ( - AbciApp, - AbciAppTransitionFunction, - AppState, - BaseSynchronizedData, - CollectSameUntilThresholdRound, - DegenerateRound, - EventToTimeout, - get_name, -) -from packages.valory.skills.multiplexer_abci.payloads import MultiplexerPayload - - -class Event(Enum): - """MultiplexerAbciApp Events""" - - ROUND_TIMEOUT = "round_timeout" - NO_MAJORITY = "no_majority" - WAIT = "done_post" - EXECUTE = "execute" - RESET = "reset" - - -class SynchronizedData(BaseSynchronizedData): - """ - Class to represent the synchronized data. - - This data is replicated by the tendermint application. - """ - - @property - def period_counter(self) -> int: - """Get the period_counter.""" - return cast(int, self.db.get("period_counter", 1)) - - -class MultiplexerRound(CollectSameUntilThresholdRound): - """MultiplexerRound""" - - payload_class = MultiplexerPayload - synchronized_data_class = SynchronizedData - - WAIT_PAYLOAD = "wait" - RESET_PAYLOAD = "reset" - EXECUTE_PAYLOAD = "transact" - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: - """Process the end of the block.""" - if self.threshold_reached: - - period_counter = cast( - SynchronizedData, self.synchronized_data - ).period_counter - - event = Event.WAIT - - if self.most_voted_payload == self.RESET_PAYLOAD: - period_counter = -1 - event = Event.RESET - - if self.most_voted_payload == self.EXECUTE_PAYLOAD: - event = Event.EXECUTE - - synchronized_data = self.synchronized_data.update( - synchronized_data_class=SynchronizedData, - **{ - get_name(SynchronizedData.period_counter): period_counter + 1, - } - ) - - return synchronized_data, event - - if not self.is_majority_possible( - self.collection, self.synchronized_data.nb_participants - ): - return self.synchronized_data, Event.NO_MAJORITY - return None - - -class FinishedMultiplexerResetRound(DegenerateRound): - """FinishedDecisionMakingPostRound""" - - -class FinishedMultiplexerExecuteRound(DegenerateRound): - """FinishedMultiplexerExecuteRound""" - - -class MultiplexerAbciApp(AbciApp[Event]): - """MultiplexerAbciApp - - Initial round: MultiplexerRound - - Initial states: {MultiplexerRound} - - Transition states: - 0. MultiplexerRound - - done post: 0. - - reset: 1. - - execute: 2. - - no majority: 0. - - round timeout: 0. - 1. FinishedMultiplexerResetRound - 2. FinishedMultiplexerExecuteRound - - Final states: {FinishedMultiplexerExecuteRound, FinishedMultiplexerResetRound} - - Timeouts: - round timeout: 30.0 - """ - - initial_round_cls: AppState = MultiplexerRound - initial_states: Set[AppState] = {MultiplexerRound} - transition_function: AbciAppTransitionFunction = { - MultiplexerRound: { - Event.WAIT: MultiplexerRound, - Event.RESET: FinishedMultiplexerResetRound, - Event.EXECUTE: FinishedMultiplexerExecuteRound, - Event.NO_MAJORITY: MultiplexerRound, - Event.ROUND_TIMEOUT: MultiplexerRound, - }, - FinishedMultiplexerResetRound: {}, - FinishedMultiplexerExecuteRound: {}, - } - final_states: Set[AppState] = { - FinishedMultiplexerResetRound, - FinishedMultiplexerExecuteRound, - } - event_to_timeout: EventToTimeout = { - Event.ROUND_TIMEOUT: 30.0, - } - cross_period_persisted_keys: FrozenSet[str] = frozenset() - db_pre_conditions: Dict[AppState, Set[str]] = { - MultiplexerRound: set(), - } - db_post_conditions: Dict[AppState, Set[str]] = { - FinishedMultiplexerResetRound: set(), - FinishedMultiplexerExecuteRound: set(), - } diff --git a/packages/valory/skills/multiplexer_abci/skill.yaml b/packages/valory/skills/multiplexer_abci/skill.yaml deleted file mode 100644 index 3fe5a158..00000000 --- a/packages/valory/skills/multiplexer_abci/skill.yaml +++ /dev/null @@ -1,141 +0,0 @@ -name: multiplexer_abci -author: valory -version: 0.1.0 -type: skill -description: An abci skill that implements decision logic for the mech. -license: Apache-2.0 -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - __init__.py: bafybeifx5c6xdzvj5v6old2ek56fek6zapsfuxgdiokpacjp57td3wbalm - behaviours.py: bafybeibevecu5s3ewnq2btswcsnwqfslw5hz5spkkjtnkggclqa23h222a - dialogues.py: bafybeie777tjh4xvxo5rrig4kq66vxg5vvmyie576ptot43olwrzfrc64a - fsm_specification.yaml: bafybeibmbpdgq7h6sgaxtdb2aawha5xdwd6oszbn3nwr2tolaijoswkfly - handlers.py: bafybeic77bbhvm7yqhimzosdjnmocy4gm677t3f4gp73c5dll5qhn7xeqy - models.py: bafybeiets4yg4p7g7mclpdmpekfmbgd6z3dy4x2kvd6rhqhjqr3njdxlcy - payloads.py: bafybeibhg7q5ejfhjkjvcfeqjyzp32msn4alu5btnywimh2zd5arr2f2mm - rounds.py: bafybeibsi2tm4sovnaevbc6dw3ljxlc6jkta33k4aqest4q423kb5okrui -fingerprint_ignore_patterns: [] -connections: [] -contracts: -- valory/agent_mech:0.1.0:bafybeigzl5sjks2tqszum6axrkwjlmybsgp54om5auybbdp3uyfx3zef7q -protocols: -- valory/contract_api:1.0.0:bafybeiasywsvax45qmugus5kxogejj66c5taen27h4voriodz7rgushtqa -skills: -- valory/abstract_round_abci:0.1.0:bafybeigxjcci53vwytymzlhr37436yvenh7jup4astrn7dgyixo24aq2pq -behaviours: - main: - args: {} - class_name: MultiplexerRoundBehaviour -handlers: - abci: - args: {} - class_name: ABCIHandler - contract_api: - args: {} - class_name: ContractApiHandler - http: - args: {} - class_name: HttpHandler - ipfs: - args: {} - class_name: IpfsHandler - ledger_api: - args: {} - class_name: LedgerApiHandler - signing: - args: {} - class_name: SigningHandler - tendermint: - args: {} - class_name: TendermintHandler -models: - abci_dialogues: - args: {} - class_name: AbciDialogues - benchmark_tool: - args: - log_dir: /logs - class_name: BenchmarkTool - contract_api_dialogues: - args: {} - class_name: ContractApiDialogues - http_dialogues: - args: {} - class_name: HttpDialogues - ipfs_dialogues: - args: {} - class_name: IpfsDialogues - ledger_api_dialogues: - args: {} - class_name: LedgerApiDialogues - params: - args: - cleanup_history_depth: 1 - cleanup_history_depth_current: null - drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - finalize_timeout: 60.0 - genesis_config: - chain_id: chain-c4daS1 - consensus_params: - block: - max_bytes: '22020096' - max_gas: '-1' - time_iota_ms: '1000' - evidence: - max_age_duration: '172800000000000' - max_age_num_blocks: '100000' - max_bytes: '1048576' - validator: - pub_key_types: - - ed25519 - version: {} - genesis_time: '2022-05-20T16:00:21.735122717Z' - voting_power: '10' - history_check_timeout: 1205 - ipfs_domain_name: null - keeper_allowed_retries: 3 - keeper_timeout: 30.0 - max_attempts: 10 - max_healthcheck: 120 - on_chain_service_id: null - polling_interval: 25 - request_retry_delay: 1.0 - request_timeout: 10.0 - reset_pause_duration: 10 - reset_period_count: 100 - reset_tendermint_after: 2 - retry_attempts: 400 - retry_timeout: 3 - round_timeout_seconds: 30.0 - service_id: multiplexer - service_registry_address: null - setup: - all_participants: [] - safe_contract_address: '0x0000000000000000000000000000000000000000' - consensus_threshold: null - share_tm_config_on_startup: false - sleep_time: 1 - 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 - use_polling: false - use_termination: false - validate_timeout: 1205 - class_name: Params - requests: - args: {} - class_name: Requests - signing_dialogues: - args: {} - class_name: SigningDialogues - state: - args: {} - class_name: SharedState - tendermint_dialogues: - args: {} - class_name: TendermintDialogues -dependencies: {} -is_abstract: true diff --git a/packages/valory/skills/multiplexer_abci/__init__.py b/packages/valory/skills/task_execution/__init__.py similarity index 85% rename from packages/valory/skills/multiplexer_abci/__init__.py rename to packages/valory/skills/task_execution/__init__.py index 0c16e8c5..5a6248eb 100644 --- a/packages/valory/skills/multiplexer_abci/__init__.py +++ b/packages/valory/skills/task_execution/__init__.py @@ -17,9 +17,9 @@ # # ------------------------------------------------------------------------------ -"""This module contains the implementation of the default skill.""" +"""This module contains the implementation of the task execution skill.""" from aea.configurations.base import PublicId -PUBLIC_ID = PublicId.from_str("valory/multiplexer_abci:0.1.0") +PUBLIC_ID = PublicId.from_str("valory/task_execution:0.1.0") diff --git a/packages/valory/skills/task_execution/behaviours.py b/packages/valory/skills/task_execution/behaviours.py new file mode 100644 index 00000000..e528588b --- /dev/null +++ b/packages/valory/skills/task_execution/behaviours.py @@ -0,0 +1,385 @@ +# -*- 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 package contains the implementation of .""" +import json +import threading +import time +from typing import Any, Callable, Dict, List, Optional, Tuple, cast + +from aea.helpers.cid import to_v1 +from aea.mail.base import EnvelopeContext +from aea.protocols.base import Message +from aea.protocols.dialogue.base import Dialogue +from aea.skills.behaviours import SimpleBehaviour + +from packages.valory.connections.ipfs.connection import IpfsDialogues +from packages.valory.connections.ipfs.connection import PUBLIC_ID as IPFS_CONNECTION_ID +from packages.valory.connections.ledger.connection import ( + PUBLIC_ID as LEDGER_CONNECTION_PUBLIC_ID, +) +from packages.valory.connections.p2p_libp2p_client.connection import ( + PUBLIC_ID as P2P_CLIENT_PUBLIC_ID, +) +from packages.valory.contracts.agent_mech.contract import AgentMechContract +from packages.valory.protocols.acn_data_share import AcnDataShareMessage +from packages.valory.protocols.acn_data_share.dialogues import AcnDataShareDialogues +from packages.valory.protocols.contract_api import ContractApiMessage +from packages.valory.protocols.ipfs import IpfsMessage +from packages.valory.protocols.ipfs.dialogues import IpfsDialogue +from packages.valory.skills.task_execution.models import Params +from packages.valory.skills.task_execution.utils.ipfs import ( + get_ipfs_file_hash, + to_multihash, +) +from packages.valory.skills.task_execution.utils.task import AnyToolAsTask + + +PENDING_TASKS = "pending_tasks" +DONE_TASKS = "ready_tasks" +DONE_TASKS_LOCK = "lock" + + +LEDGER_API_ADDRESS = str(LEDGER_CONNECTION_PUBLIC_ID) + + +class TaskExecutionBehaviour(SimpleBehaviour): + """A class to execute tasks.""" + + def __init__(self, **kwargs: Any): + """Initialise the agent.""" + super().__init__(**kwargs) + self._executing_task: Optional[Dict[str, Any]] = None + self._tools_to_file_hash: Dict[str, str] = {} + self._all_tools: Dict[str, str] = {} + self._inflight_tool_req: Optional[str] = None + self._done_task: Optional[Dict[str, Any]] = None + self._last_polling: Optional[float] = None + self._invalid_request = False + + def setup(self) -> None: + """Implement the setup.""" + self.context.logger.info("Setting up TaskExecutionBehaviour") + self._tools_to_file_hash = { + value: key + for key, values in self.params.file_hash_to_tools.items() + for value in values + } + + def act(self) -> None: + """Implement the act.""" + self._download_tools() + self._execute_task() + self._check_for_new_reqs() + + @property + def done_tasks_lock(self) -> threading.Lock: + """Get done_tasks_lock.""" + return self.context.shared_state[DONE_TASKS_LOCK] + + @property + def params(self) -> Params: + """Get the parameters.""" + return cast(Params, self.context.params) + + @property + def pending_tasks(self) -> List[Dict[str, Any]]: + """Get pending_tasks.""" + return self.context.shared_state[PENDING_TASKS] + + @property + def done_tasks(self) -> List[Dict[str, Any]]: + """Get done_tasks.""" + return self.context.shared_state[DONE_TASKS] + + def _should_poll(self) -> bool: + """If we should poll the contract.""" + if self._last_polling is None: + return True + return self._last_polling + self.params.polling_interval <= time.time() + + def _is_executing_task_ready(self) -> bool: + """Check if the executing task is ready.""" + if self._executing_task is None: + return False + task_id = self._executing_task.get("async_task_id", None) + if task_id is None: + return False + + return self.context.task_manager.get_task_result(task_id).ready() + + def _has_executing_task_timed_out(self) -> bool: + """Check if the executing task timed out.""" + if self._executing_task is None: + return False + timeout_deadline = self._executing_task.get("timeout_deadline", None) + if timeout_deadline is None: + return False + return timeout_deadline <= time.time() + + def _get_executing_task_result(self) -> Any: + """Get the executing task result.""" + if self._executing_task is None: + raise ValueError("Executing task is None") + if self._invalid_request: + return None + task_id = self._executing_task.get("async_task_id", None) + if task_id is None: + raise ValueError("Executing task has no async_task_id") + return self.context.task_manager.get_task_result(task_id).get() + + def _download_tools(self) -> None: + """Download tools.""" + if self._inflight_tool_req is not None: + # there already is a req in flight + return + if len(self._tools_to_file_hash) == len(self._all_tools): + # we already have all the tools + return + for tool, file_hash in self._tools_to_file_hash.items(): + if tool in self._all_tools: + continue + # read one at a time + ipfs_msg, message = self._build_ipfs_get_file_req(file_hash) + self._inflight_tool_req = tool + self.send_message(ipfs_msg, message, self._handle_get_tool) + return + + def _handle_get_tool(self, message: IpfsMessage, dialogue: Dialogue) -> None: + """Handle get tool response""" + tool_py = list(message.files.values())[0] + tool_req = cast(str, self._inflight_tool_req) + local_namespace: Dict[str, Any] = globals().copy() + if "run" in local_namespace: + del local_namespace["run"] + exec(tool_py, local_namespace) # pylint: disable=W0122 # nosec + self._all_tools[tool_req] = local_namespace["run"] + self._inflight_tool_req = None + + def _check_for_new_reqs(self) -> None: + """Check for new reqs.""" + if self.params.in_flight_req or not self._should_poll(): + # do nothing if there is an in flight request + # or if we should not poll yet + return + + contract_api_msg, _ = self.context.contract_dialogues.create( + performative=ContractApiMessage.Performative.GET_STATE, + contract_address=self.params.agent_mech_contract_address, + contract_id=str(AgentMechContract.contract_id), + callable="get_undelivered_reqs", + kwargs=ContractApiMessage.Kwargs(dict(from_block=self.params.from_block)), + counterparty=LEDGER_API_ADDRESS, + ledger_id=self.context.default_ledger_id, + ) + self.context.outbox.put_message(message=contract_api_msg) + self.params.in_flight_req = True + self._last_polling = time.time() + + def _execute_task(self) -> None: + """Execute tasks.""" + # check if there is a task already executing + if self.params.in_flight_req: + # there is an in flight request + return + + if self._executing_task is not None: + if self._is_executing_task_ready() or self._invalid_request: + self._handle_done_task() + elif self._has_executing_task_timed_out(): + self._handle_timeout_task() + return + + if len(self.pending_tasks) == 0: + # not tasks (requests) to execute + return + + # create new task + task_data = self.pending_tasks.pop(0) + self.context.logger.info(f"Preparing task with data: {task_data}") + self._executing_task = task_data + task_data_ = task_data["data"] + ipfs_hash = get_ipfs_file_hash(task_data_) + self.context.logger.info(f"IPFS hash: {ipfs_hash}") + ipfs_msg, message = self._build_ipfs_get_file_req(ipfs_hash) + self.send_message(ipfs_msg, message, self._handle_get_task) + + def send_message( + self, msg: Message, dialogue: Dialogue, callback: Callable + ) -> None: + """Send message.""" + self.context.outbox.put_message(message=msg) + nonce = dialogue.dialogue_label.dialogue_reference[0] + self.params.req_to_callback[nonce] = callback + self.params.in_flight_req = True + + def _handle_done_task(self) -> None: + """Handle done tasks""" + executing_task = cast(Dict[str, Any], self._executing_task) + req_id = executing_task.get("requestId", None) + task_result = self._get_executing_task_result() + response = {"requestId": req_id, "result": "Invalid response"} + self._done_task = {"request_id": req_id} + if task_result is not None: + # task succeeded + deliver_msg, transaction = task_result + response = {**response, "result": deliver_msg} + self._done_task["transaction"] = transaction + + self.context.logger.info(f"Task result for request {req_id}: {task_result}") + msg, dialogue = self._build_ipfs_store_file_req( + {str(req_id): json.dumps(response)} + ) + self.send_message(msg, dialogue, self._handle_store_response) + + 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.context.logger.info(f"Task timed out for request {req_id}") + # added to end of queue + self.pending_tasks.append(executing_task) + self._executing_task = None + + def _handle_get_task(self, message: IpfsMessage, dialogue: Dialogue) -> None: + """Handle the response from ipfs for a task request.""" + task_data = [json.loads(content) for content in message.files.values()][0] + is_data_valid = ( + task_data + and isinstance(task_data, dict) + and "prompt" in task_data + and "tool" in task_data + ) # pylint: disable=C0301 + if is_data_valid and task_data["tool"] in self._tools_to_file_hash: + self._prepare_task(task_data) + elif is_data_valid: + tool = task_data["tool"] + self.context.logger.warning(f"Tool {tool} is not valid.") + self._invalid_request = True + else: + self.context.logger.warning("Data for task is not valid.") + self._invalid_request = True + + def _prepare_task(self, task_data: Dict[str, Any]) -> None: + """Prepare the task.""" + tool_task = AnyToolAsTask() + task_data["method"] = self._all_tools[task_data["tool"]] + task_data["api_keys"] = self.params.api_keys + task_id = self.context.task_manager.enqueue_task(tool_task, kwargs=task_data) + executing_task = cast(Dict[str, Any], self._executing_task) + executing_task["async_task_id"] = task_id + executing_task["timeout_deadline"] = time.time() + self.params.task_deadline + self._async_result = self.context.task_manager.get_task_result(task_id) + + def _build_ipfs_message( + self, + performative: IpfsMessage.Performative, + timeout: Optional[float] = None, + **kwargs: Any, + ) -> Tuple[IpfsMessage, IpfsDialogue]: + """Builds an IPFS message.""" + ipfs_dialogues = cast(IpfsDialogues, self.context.ipfs_dialogues) + message, dialogue = ipfs_dialogues.create( + counterparty=str(IPFS_CONNECTION_ID), + performative=performative, + timeout=timeout, + **kwargs, + ) + return message, dialogue + + def _build_ipfs_store_file_req( # pylint: disable=too-many-arguments + self, + filename_to_obj: Dict[str, str], + timeout: Optional[float] = None, + **kwargs: Any, + ) -> Tuple[IpfsMessage, IpfsDialogue]: + """Builds a STORE_FILES ipfs message.""" + message, dialogue = self._build_ipfs_message( + performative=IpfsMessage.Performative.STORE_FILES, # type: ignore + files=filename_to_obj, + timeout=timeout, + **kwargs, + ) + return message, dialogue + + def _build_ipfs_get_file_req( + self, + ipfs_hash: str, + timeout: Optional[float] = None, + ) -> Tuple[IpfsMessage, IpfsDialogue]: + """ + Builds a GET_FILES IPFS request. + + :param ipfs_hash: the ipfs hash of the file/dir to download. + :param timeout: timeout for the request. + :returns: the ipfs message, and its corresponding dialogue. + """ + message, dialogue = self._build_ipfs_message( + performative=IpfsMessage.Performative.GET_FILES, # type: ignore + ipfs_hash=ipfs_hash, + timeout=timeout, + ) + return message, dialogue + + def _handle_store_response(self, message: IpfsMessage, dialogue: Dialogue) -> None: + """Handle the response from ipfs for a store response request.""" + executing_task = cast(Dict[str, Any], self._executing_task) + req_id, sender = ( + 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.send_data_via_acn( + sender_address=sender, + request_id=str(req_id), + data=ipfs_hash, + ) + done_task = cast(Dict[str, Any], self._done_task) + done_task["task_result"] = to_multihash(ipfs_hash) + # add to done tasks, in thread safe way + with self.done_tasks_lock: + self.done_tasks.append(done_task) + # reset tasks + self._executing_task = None + self._done_task = None + self._invalid_request = False + + def send_data_via_acn( + self, + sender_address: str, + request_id: str, + data: Any, + ) -> None: + """Handle callbacks.""" + self.context.logger.info( + f"Sending data to {sender_address} via ACN for request ID {request_id}" + ) + response, _ = cast( + AcnDataShareDialogues, self.context.acn_data_share_dialogues + ).create( + counterparty=sender_address, + performative=AcnDataShareMessage.Performative.DATA, + request_id=request_id, + content=data, + ) + self.context.outbox.put_message( + message=response, + context=EnvelopeContext(connection_id=P2P_CLIENT_PUBLIC_ID), + ) diff --git a/packages/valory/skills/task_execution/dialogues.py b/packages/valory/skills/task_execution/dialogues.py new file mode 100644 index 00000000..77557a76 --- /dev/null +++ b/packages/valory/skills/task_execution/dialogues.py @@ -0,0 +1,163 @@ +# -*- 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 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.acn_data_share.dialogues import ( + AcnDataShareDialogue as BaseAcnDataShareDialogue, +) +from packages.valory.protocols.acn_data_share.dialogues import ( + AcnDataShareDialogues as BaseAcnDataShareDialogues, +) +from packages.valory.protocols.contract_api.dialogues import ( + ContractApiDialogue as BaseContractApiDialogue, +) +from packages.valory.protocols.contract_api.dialogues import ( + ContractApiDialogues as BaseContractApiDialogues, +) +from packages.valory.protocols.default.dialogues import ( + DefaultDialogue as BaseDefaultDialogue, +) +from packages.valory.protocols.default.dialogues import ( + DefaultDialogues as BaseDefaultDialogues, +) +from packages.valory.protocols.ipfs.dialogues import IpfsDialogue as BaseIpfsDialogue +from packages.valory.protocols.ipfs.dialogues import IpfsDialogues as BaseIpfsDialogues + + +ContractApiDialogue = BaseContractApiDialogue +DefaultDialogue = BaseDefaultDialogue +IpfsDialogue = BaseIpfsDialogue +AcnDataShareDialogue = BaseAcnDataShareDialogue + + +class IpfsDialogues(Model, BaseIpfsDialogues): + """A class to keep track of IPFS 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 IpfsDialogue.Role.SKILL + + BaseIpfsDialogues.__init__( + self, + self_address=str(self.skill_id), + role_from_first_message=role_from_first_message, + ) + + +class ContractDialogues(Model, BaseContractApiDialogues): + """The dialogues class keeps track of all dialogues.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize dialogues.""" + 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 ContractApiDialogue.Role.AGENT + + BaseContractApiDialogues.__init__( + self, + self_address=str(self.skill_id), + role_from_first_message=role_from_first_message, + dialogue_class=ContractApiDialogue, + ) + + +class DefaultDialogues(Model, BaseDefaultDialogues): + """The dialogues class keeps track of all dialogues.""" + + def __init__(self, **kwargs: Any) -> None: + """Initialize dialogues.""" + Model.__init__(self, **kwargs) + + def role_from_first_message( + 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 + """ + self.context.logger.debug(f"{message} {receiver_address}") + return DefaultDialogue.Role.AGENT + + BaseDefaultDialogues.__init__( + self, + self_address=self.context.agent_address, + role_from_first_message=role_from_first_message, + ) + + +class AcnDataShareDialogues(Model, BaseAcnDataShareDialogues): + """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 AcnDataShareDialogue.Role.AGENT + + BaseAcnDataShareDialogues.__init__( + self, + self_address=str(self.context.agent_address), + role_from_first_message=role_from_first_message, + ) diff --git a/packages/valory/skills/task_execution/handlers.py b/packages/valory/skills/task_execution/handlers.py new file mode 100644 index 00000000..35ad2053 --- /dev/null +++ b/packages/valory/skills/task_execution/handlers.py @@ -0,0 +1,170 @@ +# -*- 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 package contains a scaffold of a handler.""" +import threading +from typing import Any, Dict, List, cast + +from aea.protocols.base import Message +from aea.skills.base import Handler + +from packages.valory.connections.ledger.connection import ( + PUBLIC_ID as LEDGER_CONNECTION_PUBLIC_ID, +) +from packages.valory.protocols.acn_data_share import AcnDataShareMessage +from packages.valory.protocols.contract_api import ContractApiMessage +from packages.valory.protocols.ipfs import IpfsMessage +from packages.valory.skills.task_execution.models import Params + + +PENDING_TASKS = "pending_tasks" +DONE_TASKS = "ready_tasks" +DONE_TASKS_LOCK = "lock" + +LEDGER_API_ADDRESS = str(LEDGER_CONNECTION_PUBLIC_ID) + + +class BaseHandler(Handler): + """Base Handler""" + + def setup(self) -> None: + """Set up the handler.""" + self.context.logger.info(f"{self.__class__.__name__}: setup method called.") + + def cleanup_dialogues(self) -> None: + """Clean up all dialogues.""" + for handler_name in self.context.handlers.__dict__.keys(): + dialogues_name = handler_name.replace("_handler", "_dialogues") + dialogues = getattr(self.context, dialogues_name) + dialogues.cleanup() + + @property + def params(self) -> Params: + """Get the parameters.""" + return cast(Params, self.context.params) + + def teardown(self) -> None: + """Teardown the handler.""" + self.context.logger.info(f"{self.__class__.__name__}: teardown called.") + + def on_message_handled(self, _message: Message) -> None: + """Callback after a message has been handled.""" + self.params.request_count += 1 + if self.params.request_count % self.params.cleanup_freq == 0: + self.context.logger.info( + f"{self.params.request_count} requests processed. Cleaning up dialogues." + ) + self.cleanup_dialogues() + + +class AcnHandler(BaseHandler): + """ACN API message handler.""" + + SUPPORTED_PROTOCOL = AcnDataShareMessage.protocol_id + + def handle(self, message: Message) -> None: + """Handle the message.""" + # we don't respond to ACN messages at this point + self.context.logger.info(f"Received message: {message}") + self.on_message_handled(message) + + +class IpfsHandler(BaseHandler): + """IPFS API message handler.""" + + SUPPORTED_PROTOCOL = IpfsMessage.protocol_id + + def handle(self, message: Message) -> None: + """ + Implement the reaction to an IPFS message. + + :param message: the message + """ + self.context.logger.info(f"Received message: {message}") + ipfs_msg = cast(IpfsMessage, message) + if ipfs_msg.performative == IpfsMessage.Performative.ERROR: + self.context.logger.warning( + f"IPFS Message performative not recognized: {ipfs_msg.performative}" + ) + self.params.in_flight_req = False + return + + dialogue = self.context.ipfs_dialogues.update(ipfs_msg) + nonce = dialogue.dialogue_label.dialogue_reference[0] + callback = self.params.req_to_callback.pop(nonce) + callback(ipfs_msg, dialogue) + self.params.in_flight_req = False + self.on_message_handled(message) + + +class ContractHandler(BaseHandler): + """Contract API message handler.""" + + SUPPORTED_PROTOCOL = ContractApiMessage.protocol_id + + def setup(self) -> None: + """Setup the contract handler.""" + self.context.shared_state[PENDING_TASKS] = [] + self.context.shared_state[DONE_TASKS] = [] + self.context.shared_state[DONE_TASKS_LOCK] = threading.Lock() + super().setup() + + @property + def pending_tasks(self) -> List[Dict[str, Any]]: + """Get pending_tasks.""" + return self.context.shared_state[PENDING_TASKS] + + def handle(self, message: Message) -> None: + """ + Implement the reaction to a contract message. + + :param message: the message + """ + self.context.logger.info(f"Received message: {message}") + contract_api_msg = cast(ContractApiMessage, message) + if contract_api_msg.performative != ContractApiMessage.Performative.STATE: + self.context.logger.warning( + f"Contract API Message performative not recognized: {contract_api_msg.performative}" + ) + self.params.in_flight_req = False + return + + body = contract_api_msg.state.body + self._handle_get_undelivered_reqs(body) + self.params.in_flight_req = False + self.on_message_handled(message) + + def _handle_get_undelivered_reqs(self, body: Dict[str, Any]) -> None: + """Handle get undelivered reqs.""" + reqs = body.get("data", []) + if len(reqs) == 0: + return + + self.params.from_block = max([req["block_number"] for req in reqs]) + 1 + self.context.logger.info(f"Received {len(reqs)} new requests.") + reqs = [ + req + for req in reqs + if req["block_number"] % self.params.num_agents == self.params.agent_index + ] + self.context.logger.info(f"Processing only {len(reqs)} of the new requests.") + self.pending_tasks.extend(reqs) + self.context.logger.info( + f"Monitoring new reqs from block {self.params.from_block}" + ) diff --git a/packages/valory/skills/task_execution/models.py b/packages/valory/skills/task_execution/models.py new file mode 100644 index 00000000..6a4ef079 --- /dev/null +++ b/packages/valory/skills/task_execution/models.py @@ -0,0 +1,71 @@ +# -*- 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, Callable, Dict, List, cast + +from aea.exceptions import enforce +from aea.skills.base import Model + + +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.agent_mech_contract_address = kwargs.get( + "agent_mech_contract_address", None + ) + enforce( + self.agent_mech_contract_address is not None, + "agent_mech_contract_address must be set!", + ) + + self.in_flight_req: bool = False + self.from_block: int = 0 + self.req_to_callback: Dict[str, Callable] = {} + self.api_keys: Dict = self._nested_list_todict_workaround( + kwargs, "api_keys_json" + ) + self.file_hash_to_tools: Dict[ + str, List[str] + ] = self._nested_list_todict_workaround( + kwargs, + "file_hash_to_tools_json", + ) + self.polling_interval = kwargs.get("polling_interval", 30.0) + self.task_deadline = kwargs.get("task_deadline", 240.0) + self.num_agents = kwargs.get("num_agents", None) + self.request_count: int = 0 + self.cleanup_freq = kwargs.get("cleanup_freq", 50) + enforce(self.num_agents is not None, "num_agents must be set!") + self.agent_index = kwargs.get("agent_index", None) + enforce(self.agent_index is not None, "agent_index must be set!") + super().__init__(*args, **kwargs) + + def _nested_list_todict_workaround( + self, + kwargs: Dict, + key: str, + ) -> Dict: + """Get a nested list from the kwargs and convert it to a dictionary.""" + values = cast(List, kwargs.get(key)) + if len(values) == 0: + raise ValueError(f"No {key} specified!") + return {value[0]: value[1] for value in values} diff --git a/packages/valory/skills/task_execution/skill.yaml b/packages/valory/skills/task_execution/skill.yaml new file mode 100644 index 00000000..6b466aa5 --- /dev/null +++ b/packages/valory/skills/task_execution/skill.yaml @@ -0,0 +1,92 @@ +name: task_execution +author: valory +version: 0.1.0 +type: skill +description: A skill used for monitoring and executing tasks. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + __init__.py: bafybeidqhvvlnthkbnmrdkdeyjyx2f2ab6z4xdgmagh7welqnh2v6wczx4 + behaviours.py: bafybeifcdxzmpj7m642xn27wyogtgmfa4o5pwy6db2bcowh24gs5u3vsxe + dialogues.py: bafybeihw3nvl2xqxgfgtbhskzxd2awvhiujoi5o7mefokn4ew3o3vo2t4u + handlers.py: bafybeiatb4zd3pg577leguyiqi3cjceu7ibp7fafctm4pte4jckbddmfby + models.py: bafybeiauvac5fxeiqxujfl6ocd4u2l2otb2xntygmn2b3k5v7tcj7ptaju + utils/__init__.py: bafybeiccdijaigu6e5p2iruwo5mkk224o7ywedc7nr6xeu5fpmhjqgk24e + utils/ipfs.py: bafybeicuaj23qrcdv6ly4j7yo6il2r5plozhd6mwvcp5acwqbjxb2t3u2i + utils/task.py: bafybeiayyt22ysncqmxf3bowbsxqgym4xvx6ukap5csmuofkaozydu3oxi +fingerprint_ignore_patterns: [] +connections: +- valory/ledger:0.19.0:bafybeigfoz7d7si7s4jehvloq2zmiiocpbxcaathl3bxkyarxoerxq7g3a +- valory/ipfs:0.1.0:bafybeiau32pzy55ta6ugl2bebevlxudal6pnlfomhplfm5mph6reaw3krq +- valory/p2p_libp2p_client:0.1.0:bafybeihdnfdth3qgltefgrem7xyi4b3ejzaz67xglm2hbma2rfvpl2annq +contracts: +- valory/agent_mech:0.1.0:bafybeidl6kwc3sgcxiphgb3osjqlqwylhqetv2nyv2fu6zxcgn5qctv2ju +protocols: +- valory/contract_api:1.0.0:bafybeiasywsvax45qmugus5kxogejj66c5taen27h4voriodz7rgushtqa +- valory/default:1.0.0:bafybeiecmut3235aen7wxukllv424f3dysvvlgfmn562kzdunc5hdj3hxu +- valory/acn_data_share:0.1.0:bafybeieyixetwvz767zekhvg7r6etumyanzys6xbalx2brrfswybinnlhi +- valory/ipfs:0.1.0:bafybeibjzhsengtxfofqpxy6syamplevp35obemwfp4c5lhag3v2bvgysa +skills: [] +behaviours: + task_execution: + args: {} + class_name: TaskExecutionBehaviour +handlers: + contract_handler: + args: {} + class_name: ContractHandler + ipfs_handler: + args: {} + class_name: IpfsHandler + acn_data_share_handler: + args: {} + class_name: AcnHandler +models: + contract_dialogues: + args: {} + class_name: ContractDialogues + default_dialogues: + args: {} + class_name: DefaultDialogues + ipfs_dialogues: + args: {} + class_name: IpfsDialogues + acn_data_share_dialogues: + args: {} + class_name: AcnDataShareDialogues + params: + args: + agent_mech_contract_address: '0x9A676e781A523b5d0C0e43731313A708CB607508' + task_deadline: 240.0 + file_hash_to_tools_json: + - - bafybeif3izkobmvaoen23ine6tiqx55eaf4g3r56hdalnig656xivzpf3m + - - 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 + api_keys_json: + - - openai + - dummy_api_key + - - stabilityai + - dummy_api_key + polling_interval: 30.0 + agent_index: 0 + num_agents: 4 + class_name: Params +dependencies: + 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_execution_abci/io_/__init__.py b/packages/valory/skills/task_execution/utils/__init__.py similarity index 92% rename from packages/valory/skills/task_execution_abci/io_/__init__.py rename to packages/valory/skills/task_execution/utils/__init__.py index fd05584d..a547ea37 100644 --- a/packages/valory/skills/task_execution_abci/io_/__init__.py +++ b/packages/valory/skills/task_execution/utils/__init__.py @@ -16,4 +16,4 @@ # limitations under the License. # # ------------------------------------------------------------------------------ -"""This module contains helper classes for IPFS interaction.""" +"""This module contains helper classes.""" diff --git a/packages/valory/skills/task_execution/utils/ipfs.py b/packages/valory/skills/task_execution/utils/ipfs.py new file mode 100644 index 00000000..3882a049 --- /dev/null +++ b/packages/valory/skills/task_execution/utils/ipfs.py @@ -0,0 +1,48 @@ +# -*- 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 helpers for IPFS interaction.""" +from aea.helpers.cid import CID +from multibase import multibase +from multicodec import multicodec + + +CID_PREFIX = "f01701220" + + +def get_ipfs_file_hash(data: bytes) -> str: + """Get hash from bytes""" + try: + return str(CID.from_string(data.decode())) + except Exception: # noqa + # if something goes wrong, fallback to sha256 + file_hash = data.hex() + file_hash = CID_PREFIX + file_hash + file_hash = str(CID.from_string(file_hash)) + return file_hash + + +def to_multihash(hash_string: str) -> bytes: + """To multihash string.""" + # Decode the Base32 CID to bytes + cid_bytes = multibase.decode(hash_string) + # Remove the multicodec prefix (0x01) from the bytes + multihash_bytes = multicodec.remove_prefix(cid_bytes) + # Convert the multihash bytes to a hexadecimal string + hex_multihash = multihash_bytes.hex() + return hex_multihash[6:] diff --git a/packages/valory/skills/multiplexer_abci/payloads.py b/packages/valory/skills/task_execution/utils/task.py similarity index 69% rename from packages/valory/skills/multiplexer_abci/payloads.py rename to packages/valory/skills/task_execution/utils/task.py index de7a5247..491b1eac 100644 --- a/packages/valory/skills/multiplexer_abci/payloads.py +++ b/packages/valory/skills/task_execution/utils/task.py @@ -17,15 +17,17 @@ # # ------------------------------------------------------------------------------ -"""This module contains the transaction payloads of the MultiplexerAbciApp.""" +"""This package contains a custom Loader for the ipfs connection.""" -from dataclasses import dataclass +from typing import Any -from packages.valory.skills.abstract_round_abci.base import BaseTxPayload +from aea.skills.tasks import Task -@dataclass(frozen=True) -class MultiplexerPayload(BaseTxPayload): - """Represent a transaction payload for the MultiplexerRound.""" +class AnyToolAsTask(Task): + """AnyToolAsTask""" - content: str + def execute(self, *args: Any, **kwargs: Any) -> Any: + """Execute the task.""" + method = kwargs.pop("method") + return method(*args, **kwargs) diff --git a/packages/valory/skills/task_execution_abci/behaviours.py b/packages/valory/skills/task_execution_abci/behaviours.py deleted file mode 100644 index 15208c04..00000000 --- a/packages/valory/skills/task_execution_abci/behaviours.py +++ /dev/null @@ -1,472 +0,0 @@ -# -*- 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 package contains round behaviours of TaskExecutionAbciApp.""" -import os -from abc import ABC -from multiprocessing.pool import AsyncResult -from typing import Any, Dict, Generator, List, Optional, Set, Tuple, Type, cast - -import multibase -import multicodec -import openai # noqa -from aea.helpers.cid import CID, to_v1 -from aea.mail.base import EnvelopeContext - -from packages.valory.connections.p2p_libp2p_client.connection import ( - PUBLIC_ID as P2P_CLIENT_PUBLIC_ID, -) -from packages.valory.contracts.agent_mech.contract import AgentMechContract -from packages.valory.contracts.gnosis_safe.contract import ( - GnosisSafeContract, - SafeOperation, -) -from packages.valory.contracts.multisend.contract import ( - MultiSendContract, - MultiSendOperation, -) -from packages.valory.protocols.acn_data_share.dialogues import AcnDataShareDialogues -from packages.valory.protocols.acn_data_share.message import AcnDataShareMessage -from packages.valory.protocols.contract_api import ContractApiMessage -from packages.valory.skills.abstract_round_abci.base import AbstractRound -from packages.valory.skills.abstract_round_abci.behaviour_utils import ( - SupportedObjectType, -) -from packages.valory.skills.abstract_round_abci.behaviours import ( - AbstractRoundBehaviour, - BaseBehaviour, -) -from packages.valory.skills.abstract_round_abci.io_.store import SupportedFiletype -from packages.valory.skills.task_execution_abci.models import Params -from packages.valory.skills.task_execution_abci.rounds import ( - SynchronizedData, - TaskExecutionAbciApp, - TaskExecutionAbciPayload, - TaskExecutionRound, -) -from packages.valory.skills.task_execution_abci.tasks import AnyToolAsTask -from packages.valory.skills.transaction_settlement_abci.payload_tools import ( - hash_payload_to_hex, -) - - -CID_PREFIX = "f01701220" -ZERO_ETHER_VALUE = 0 -SAFE_GAS = 0 - - -class TaskExecutionBaseBehaviour(BaseBehaviour, ABC): - """Base behaviour for the task_execution_abci skill.""" - - @property - def synchronized_data(self) -> SynchronizedData: - """Return the synchronized data.""" - return cast(SynchronizedData, super().synchronized_data) - - @property - def params(self) -> Params: - """Return the params.""" - return cast(Params, super().params) - - -class TaskExecutionAbciBehaviour(TaskExecutionBaseBehaviour): - """TaskExecutionAbciBehaviour""" - - matching_round: Type[AbstractRound] = TaskExecutionRound - - def __init__(self, **kwargs: Any) -> None: - """Initialize Behaviour.""" - super().__init__(**kwargs) - self._async_result: Optional[AsyncResult] = None - self.request_id = None - self._is_task_prepared = False - self._invalid_request = False - - def _AsyncBehaviour__handle_waiting_for_message(self) -> None: - """Handle an 'act' tick, when waiting for a message.""" - # if there is no message coming, skip. - if self._AsyncBehaviour__notified: # type: ignore - try: - self._AsyncBehaviour__get_generator_act().send( - self._AsyncBehaviour__message # type: ignore - ) - except StopIteration: - self._AsyncBehaviour__handle_stop_iteration() - finally: - # wait for the next message - self._AsyncBehaviour__notified = False - self._AsyncBehaviour__message = None - else: - self._AsyncBehaviour__get_generator_act().send(None) - - def async_act(self) -> Generator: # pylint: disable=R0914,R0915 - """Do the act, supporting asynchronous execution.""" - - if not self.context.params.all_tools: - all_tools = {} - for file_hash, tools in self.context.params.file_hash_to_tools.items(): - tool_py = yield from self.get_from_ipfs( - file_hash, custom_loader=lambda plain: plain - ) - if tool_py is None: - self.context.logger.error( - f"Failed to get the tools {tools} with file_hash {file_hash} from IPFS!" - ) - all_tools.update({tool: tool_py for tool in tools}) - self.context.params.__dict__["_frozen"] = False - self.context.params.all_tools = all_tools - self.context.params.__dict__["_frozen"] = True - - with self.context.benchmark_tool.measure(self.behaviour_id).local(): - task_result = yield from self.get_task_result() - if task_result is None: - # the task is not ready yet, check in the next iteration - return - payload_content = yield from self.get_payload_content(task_result) - sender = self.context.agent_address - payload = TaskExecutionAbciPayload(sender=sender, content=payload_content) - with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): - yield from self.send_a2a_transaction(payload) - yield from self.wait_until_round_end() - self.set_done() - - def get_payload_content( - self, task_result: Optional[Tuple[str, bytes, Optional[Dict[str, Any]]]] - ) -> Generator[None, None, str]: - """Get the payload content.""" - if task_result is None: - # something went wrong, respond with ERROR payload for now - return TaskExecutionRound.ERROR_PAYLOAD - - request_id, deliver_msg_hash, response_tx = task_result - deliver_tx = yield from self._get_deliver_tx( - {"request_id": request_id, "task_result": deliver_msg_hash} - ) - if deliver_tx is None: - # something went wrong, respond with ERROR payload for now - return TaskExecutionRound.ERROR_PAYLOAD - - all_txs = [deliver_tx] - if response_tx is not None: - all_txs.append(response_tx) - multisend_tx_str = yield from self._to_multisend(all_txs) - if multisend_tx_str is None: - # something went wrong, respond with ERROR payload for now - return TaskExecutionRound.ERROR_PAYLOAD - - return multisend_tx_str - - def get_task_result( # pylint: disable=R0914,R1710 - self, - ) -> Generator[None, None, Optional[Tuple[str, bytes, Optional[Dict[str, Any]]]]]: - """ - Execute a task in the background and wait for the result asynchronously. - - :return: A tuple containing request_id, deliver_msg_hash and multisend transactions. - :yields: None - """ - # Check whether the task already exists - if not self._is_task_prepared and not self._invalid_request: - # Get the first task in the queue - format: {"requestId": , "data": } - pending_tasks = self.context.shared_state.get("pending_tasks") - if len(pending_tasks) == 0: - # something went wrong, we should not be here, send an error payload - return None - - task_data = self.context.shared_state.get("pending_tasks").pop(0) - self.context.logger.info(f"Preparing task with data: {task_data}") - self.request_id = task_data["requestId"] - self.sender_address = task_data["sender"] - task_data_ = task_data["data"] - - # Verify the data hash and handle encoding - try: - file_hash = self._get_ipfs_file_hash(task_data_) - # Get the file from IPFS - self.context.logger.info(f"Getting data from IPFS: {file_hash}") - task_data = yield from self.get_from_ipfs( - ipfs_hash=file_hash, - filetype=SupportedFiletype.JSON, - timeout=self.params.ipfs_fetch_timeout, - ) - self.context.logger.info(f"Got data from IPFS: {task_data}") - - # Verify the file data - is_data_valid = ( - task_data - and isinstance(task_data, dict) - and "prompt" in task_data - and "tool" in task_data - ) # pylint: disable=C0301 - if ( - is_data_valid - and task_data["tool"] in self.context.params.tools_to_file_hash - ): - self._prepare_task(task_data) - elif is_data_valid: - tool = task_data["tool"] - self.context.logger.warning(f"Tool {tool} is not valid.") - self._invalid_request = True - else: - self.context.logger.warning("Data is not valid.") - self._invalid_request = True - except Exception as e: # pylint: disable=W0718 - self.context.logger.warning(f"Exception when handling data:\n{e}") - self._invalid_request = True - - response_obj = None - - # Handle invalid requests - if self._invalid_request: - # respond with no_op and no multisend transactions - obj_hash = yield from self.write_response_to_ipfs( - data={"requestId": self.request_id, "result": "invalid request"} - ) - self.send_data_via_acn( - sender_address=self.sender_address, - request_id=str(self.request_id), - data=obj_hash, - ) - hex_multihash = self.to_multihash(hash_string=obj_hash) - request_id = cast(str, self.request_id) - return request_id, hex_multihash, None - - self._async_result = cast(AsyncResult, self._async_result) - - # Handle unfinished task - if not self._invalid_request and not self._async_result.ready(): - self.context.logger.debug("The task is not finished yet.") - yield from self.sleep(self.params.sleep_time) - return None - - # Handle finished task - transaction: Optional[Dict[str, Any]] = None - if not self._invalid_request and self._async_result.ready(): - # the expected response for the task is: Tuple[str, List[Dict]] = (deliver_msg, transactions) - # deliver_msg: str = is the string containing the deliver message. - # transaction: List[Dict] = is the list of transactions to be multisent. - # Should be an empty list if no transactions are needed. - # example response: ("task_result", {"to": "0x123", "value": 0, "data": "0x123"}) - task_result: Tuple[str, Dict[str, Any]] = self._async_result.get() - if task_result is None: - self.context.logger.warning( - "Task result is None. Something went wrong while processing the task. " - "An error should have been raised before." - ) - self._invalid_request = True - return None - deliver_msg, transaction = task_result - response_obj = {"requestId": self.request_id, "result": deliver_msg} - - self.context.logger.info(f"Response object: {response_obj}") - obj_hash = yield from self.write_response_to_ipfs(data=response_obj) - self.send_data_via_acn( - sender_address=self.sender_address, - request_id=str(self.request_id), - data=obj_hash, - ) - hex_multihash = self.to_multihash(hash_string=obj_hash) - request_id = cast(str, self.request_id) - return request_id, hex_multihash, transaction - - def to_multihash(self, hash_string: str) -> bytes: - """To multihash string.""" - # Decode the Base32 CID to bytes - cid_bytes = multibase.decode(hash_string) - # Remove the multicodec prefix (0x01) from the bytes - multihash_bytes = multicodec.remove_prefix(cid_bytes) - # Convert the multihash bytes to a hexadecimal string - hex_multihash = multihash_bytes.hex() - return hex_multihash[6:] - - def write_response_to_ipfs( - self, data: SupportedObjectType - ) -> Generator[None, None, str]: - """Write response data to IPFS and return IPFS hash.""" - file_path = os.path.join(self.context.data_dir, str(self.request_id)) - obj_hash = yield from self.send_to_ipfs( - filename=file_path, - obj=data, - filetype=SupportedFiletype.JSON, - ) - return to_v1(obj_hash) - - def send_data_via_acn( - self, - sender_address: str, - request_id: str, - data: Any, - ) -> None: - """Handle callbacks.""" - self.context.logger.info( - f"Sending data to {sender_address} via ACN for request ID {request_id}" - ) - response, _ = cast( - AcnDataShareDialogues, self.context.acn_data_share_dialogues - ).create( - counterparty=sender_address, - performative=AcnDataShareMessage.Performative.DATA, - request_id=request_id, - content=data, - ) - self.context.outbox.put_message( - message=response, - context=EnvelopeContext(connection_id=P2P_CLIENT_PUBLIC_ID), - ) - - def _get_ipfs_file_hash(self, data: bytes) -> str: - """Get hash from bytes""" - try: - return str(CID.from_string(data.decode())) - except Exception: # noqa - # if something goes wrong, fallback to sha256 - file_hash = data.hex() - file_hash = CID_PREFIX + file_hash - file_hash = str(CID.from_string(file_hash)) - return file_hash - - def _prepare_task(self, task_data: Dict[str, Any]) -> None: - """Prepare the task.""" - tool_task = AnyToolAsTask() - tool_py = self.context.params.all_tools[task_data["tool"]] - local_namespace: Dict[str, Any] = globals().copy() - if "run" in local_namespace: - del local_namespace["run"] - exec(tool_py, local_namespace) # pylint: disable=W0122 # nosec - task_data["method"] = local_namespace["run"] - task_data["api_keys"] = self.params.api_keys - task_id = self.context.task_manager.enqueue_task(tool_task, kwargs=task_data) - self._async_result = self.context.task_manager.get_task_result(task_id) - self._is_task_prepared = True - - def _to_multisend( - self, transactions: List[Dict] - ) -> Generator[None, None, Optional[str]]: - """Transform payload to MultiSend.""" - multi_send_txs = [] - for transaction in transactions: - transaction = { - "operation": transaction.get("operation", MultiSendOperation.CALL), - "to": transaction["to"], - "value": transaction["value"], - "data": transaction.get("data", b""), - } - multi_send_txs.append(transaction) - - response = yield from self.get_contract_api_response( - performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, # type: ignore - contract_address=self.params.multisend_address, - contract_id=str(MultiSendContract.contract_id), - contract_callable="get_tx_data", - multi_send_txs=multi_send_txs, - ) - if response.performative != ContractApiMessage.Performative.RAW_TRANSACTION: - self.context.logger.error( - f"Couldn't compile the multisend tx. " - f"Expected performative {ContractApiMessage.Performative.RAW_TRANSACTION.value}, " # type: ignore - f"received {response.performative.value}." - ) - return None - - # strip "0x" from the response - multisend_data_str = cast(str, response.raw_transaction.body["data"])[2:] - tx_data = bytes.fromhex(multisend_data_str) - tx_hash = yield from self._get_safe_tx_hash(tx_data) - if tx_hash is None: - # something went wrong - return None - - payload_data = hash_payload_to_hex( - safe_tx_hash=tx_hash, - ether_value=ZERO_ETHER_VALUE, - safe_tx_gas=SAFE_GAS, - operation=SafeOperation.DELEGATE_CALL.value, - to_address=self.params.multisend_address, - data=tx_data, - ) - return payload_data - - def _get_safe_tx_hash(self, data: bytes) -> Generator[None, None, Optional[str]]: - """ - Prepares and returns the safe tx hash. - - This hash will be signed later by the agents, and submitted to the safe contract. - Note that this is the transaction that the safe will execute, with the provided data. - - :param data: the safe tx data. - :return: the tx hash - """ - response = yield from self.get_contract_api_response( - performative=ContractApiMessage.Performative.GET_STATE, # type: ignore - contract_address=self.synchronized_data.safe_contract_address, - contract_id=str(GnosisSafeContract.contract_id), - contract_callable="get_raw_safe_transaction_hash", - to_address=self.params.multisend_address, # we send the tx to the multisend address - value=ZERO_ETHER_VALUE, - data=data, - safe_tx_gas=SAFE_GAS, - operation=SafeOperation.DELEGATE_CALL.value, - ) - - if response.performative != ContractApiMessage.Performative.STATE: - self.context.logger.error( - f"Couldn't get safe hash. " - f"Expected response performative {ContractApiMessage.Performative.STATE.value}, " # type: ignore - f"received {response.performative.value}." - ) - return None - - # strip "0x" from the response hash - tx_hash = cast(str, response.state.body["tx_hash"])[2:] - return tx_hash - - def _get_deliver_tx( - self, task_data: Dict[str, Any] - ) -> Generator[None, None, Optional[Dict]]: - """Get the deliver tx.""" - contract_api_msg = yield from self.get_contract_api_response( - performative=ContractApiMessage.Performative.GET_STATE, # type: ignore - contract_address=self.params.agent_mech_contract_address, - contract_id=str(AgentMechContract.contract_id), - contract_callable="get_deliver_data", - request_id=task_data["request_id"], - data=task_data["task_result"], - ) - if ( - contract_api_msg.performative != ContractApiMessage.Performative.STATE - ): # pragma: nocover - self.context.logger.warning( - f"get_deliver_data unsuccessful!: {contract_api_msg}" - ) - return None - - data = cast(bytes, contract_api_msg.state.body["data"]) - return { - "to": self.params.agent_mech_contract_address, - "value": ZERO_ETHER_VALUE, - "data": data, - } - - -class TaskExecutionRoundBehaviour(AbstractRoundBehaviour): - """TaskExecutionRoundBehaviour""" - - initial_behaviour_cls = TaskExecutionAbciBehaviour - abci_app_cls = TaskExecutionAbciApp # type: ignore - behaviours: Set[Type[BaseBehaviour]] = {TaskExecutionAbciBehaviour} diff --git a/packages/valory/skills/task_execution_abci/fsm_specification.yaml b/packages/valory/skills/task_execution_abci/fsm_specification.yaml deleted file mode 100644 index 349015c8..00000000 --- a/packages/valory/skills/task_execution_abci/fsm_specification.yaml +++ /dev/null @@ -1,19 +0,0 @@ -alphabet_in: -- DONE -- ERROR -- TASK_EXECUTION_ROUND_TIMEOUT -default_start_state: TaskExecutionRound -final_states: -- FinishedTaskExecutionRound -- FinishedTaskExecutionWithErrorRound -label: TaskExecutionAbciApp -start_states: -- TaskExecutionRound -states: -- FinishedTaskExecutionRound -- FinishedTaskExecutionWithErrorRound -- TaskExecutionRound -transition_func: - (TaskExecutionRound, DONE): FinishedTaskExecutionRound - (TaskExecutionRound, ERROR): FinishedTaskExecutionWithErrorRound - (TaskExecutionRound, TASK_EXECUTION_ROUND_TIMEOUT): TaskExecutionRound diff --git a/packages/valory/skills/task_execution_abci/io_/naive_loader.py b/packages/valory/skills/task_execution_abci/io_/naive_loader.py deleted file mode 100644 index 886c4b8e..00000000 --- a/packages/valory/skills/task_execution_abci/io_/naive_loader.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- 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 package contains a custom Loader for the ipfs connection.""" - -from typing import Dict - -from packages.valory.skills.abstract_round_abci.io_.load import Loader -from packages.valory.skills.abstract_round_abci.io_.store import SupportedObjectType - - -class NaiveLoader(Loader): - """A simple loader that doesn't deserialize the response from ipfs, but returns it as is.""" - - def load(self, serialized_objects: Dict[str, str]) -> SupportedObjectType: - """ - Return the objects as is. - - :param serialized_objects: A mapping of filenames to serialized object they contained. - :return: the loaded file(s). - """ - return serialized_objects diff --git a/packages/valory/skills/task_execution_abci/models.py b/packages/valory/skills/task_execution_abci/models.py deleted file mode 100644 index 6130bb04..00000000 --- a/packages/valory/skills/task_execution_abci/models.py +++ /dev/null @@ -1,100 +0,0 @@ -# -*- 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 TaskExecutionAbciApp.""" - -from typing import Any, Dict, List, Type, Union - -from packages.valory.skills.abstract_round_abci.base import AbciApp -from packages.valory.skills.abstract_round_abci.models import BaseParams -from packages.valory.skills.abstract_round_abci.models import ( - BenchmarkTool as BaseBenchmarkTool, -) -from packages.valory.skills.abstract_round_abci.models import Requests as BaseRequests -from packages.valory.skills.abstract_round_abci.models import ( - SharedState as BaseSharedState, -) -from packages.valory.skills.task_execution_abci.rounds import TaskExecutionAbciApp - - -class SharedState(BaseSharedState): - """Keep the current shared state of the skill.""" - - abci_app_cls: Type[AbciApp] = TaskExecutionAbciApp - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize the shared state object.""" - self.all_tools: Dict[str, str] = {} - super().__init__(*args, **kwargs) - - -class Params(BaseParams): - """Parameters.""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - """Initialize the parameters object.""" - - self.api_keys: Dict[str, str] = self._nested_list_todict_workaround( - kwargs, "api_keys_json", List[List[str]] - ) - self.file_hash_to_tools: Dict[ - str, List[str] - ] = self._nested_list_todict_workaround( - kwargs, "file_hash_to_tools_json", List[List[Union[str, List[str]]]] - ) - self.tools_to_file_hash = { - value: key - for key, values in self.file_hash_to_tools.items() - for value in values - } - self.all_tools: Dict[str, str] = {} - self.multisend_address = kwargs.get("multisend_address", None) - if self.multisend_address is None: - raise ValueError("No multisend_address specified!") - self.agent_mech_contract_address = kwargs.get( - "agent_mech_contract_address", None - ) - if self.agent_mech_contract_address is None: - raise ValueError("agent_mech_contract_address is required") - self.ipfs_fetch_timeout = self._ensure( - "ipfs_fetch_timeout", kwargs=kwargs, type_=float - ) - super().__init__(*args, **kwargs) - - def _nested_list_todict_workaround( - self, kwargs: Dict, key: str, type_: Any - ) -> Dict: - """ - Get a nested list from the kwargs and convert it to a dictionary. - - This is a workaround because we currently cannot input a json string on Propel. - - :param kwargs: the keyword arguments parsed from the yaml configuration. - :param key: the key for which we want to try to retrieve its value from the kwargs. - :param type_: the expected type of the retrieved value. - :return: the nested list converted to a dictionary. - """ - values = self._ensure(key, kwargs, type_) - if len(values) == 0: - raise ValueError(f"No {key} specified!") - return {value[0]: value[1] for value in values} - - -Requests = BaseRequests -BenchmarkTool = BaseBenchmarkTool diff --git a/packages/valory/skills/task_execution_abci/rounds.py b/packages/valory/skills/task_execution_abci/rounds.py deleted file mode 100644 index dbf2d250..00000000 --- a/packages/valory/skills/task_execution_abci/rounds.py +++ /dev/null @@ -1,157 +0,0 @@ -# -*- 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 package contains the rounds of TaskExecutionAbciApp.""" -from enum import Enum -from typing import Dict, FrozenSet, Optional, Set, Tuple, cast - -from packages.valory.skills.abstract_round_abci.base import ( - AbciApp, - AbciAppTransitionFunction, - AppState, - BaseSynchronizedData, - BaseTxPayload, - CollectionRound, - DegenerateRound, - EventToTimeout, - get_name, -) -from packages.valory.skills.task_execution_abci.payloads import TaskExecutionAbciPayload - - -class Event(Enum): - """TaskExecutionAbciApp Events""" - - TASK_EXECUTION_ROUND_TIMEOUT = "task_execution_round_timeout" - ROUND_TIMEOUT = "round_timeout" - NO_MAJORITY = "no_majority" - DONE = "done" - ERROR = "error" - - -class SynchronizedData(BaseSynchronizedData): - """ - Class to represent the synchronized data. - - This data is replicated by the tendermint application. - """ - - @property - def most_voted_tx_hash(self) -> str: - """Get the most_voted_tx_hash.""" - return cast(str, self.db.get_strict("most_voted_tx_hash")) - - -class TaskExecutionRound(CollectionRound): - """TaskExecutionRound""" - - payload_class = TaskExecutionAbciPayload - synchronized_data_class = SynchronizedData - - move_forward_payload: Optional[TaskExecutionAbciPayload] = None - - ERROR_PAYLOAD = "ERROR" - - @property - def collection_threshold_reached( - self, - ) -> bool: - """Check that the collection threshold has been reached.""" - return len(self.collection) >= self.synchronized_data.max_participants - - def process_payload(self, payload: BaseTxPayload) -> None: - """Process payload.""" - super().process_payload(payload) - if cast(TaskExecutionAbciPayload, payload).content != self.ERROR_PAYLOAD: - self.move_forward_payload = cast(TaskExecutionAbciPayload, payload) - - def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: - """Process the end of the block.""" - if self.collection_threshold_reached and self.move_forward_payload is None: - return self.synchronized_data, Event.ERROR - - if self.move_forward_payload is not None: - synchronized_data = self.synchronized_data.update( - synchronized_data_class=SynchronizedData, - **{ - get_name( - SynchronizedData.most_voted_tx_hash - ): self.move_forward_payload.content, - } - ) - return synchronized_data, Event.DONE - - return None - - -class FinishedTaskExecutionRound(DegenerateRound): - """FinishedTaskExecutionRound""" - - -class FinishedTaskExecutionWithErrorRound(DegenerateRound): - """FinishedTaskExecutionWithErrorRound""" - - -class TaskExecutionAbciApp(AbciApp[Event]): - """TaskExecutionAbciApp - - Initial round: TaskExecutionRound - - Initial states: {TaskExecutionRound} - - Transition states: - 0. TaskExecutionRound - - done: 1. - - task execution round timeout: 0. - - error: 2. - 1. FinishedTaskExecutionRound - 2. FinishedTaskExecutionWithErrorRound - - Final states: {FinishedTaskExecutionRound, FinishedTaskExecutionWithErrorRound} - - Timeouts: - task execution round timeout: 60.0 - """ - - initial_round_cls: AppState = TaskExecutionRound - initial_states: Set[AppState] = {TaskExecutionRound} - transition_function: AbciAppTransitionFunction = { - TaskExecutionRound: { - Event.DONE: FinishedTaskExecutionRound, - Event.TASK_EXECUTION_ROUND_TIMEOUT: TaskExecutionRound, - Event.ERROR: FinishedTaskExecutionWithErrorRound, - }, - FinishedTaskExecutionRound: {}, - FinishedTaskExecutionWithErrorRound: {}, - } - final_states: Set[AppState] = { - FinishedTaskExecutionRound, - FinishedTaskExecutionWithErrorRound, - } - event_to_timeout: EventToTimeout = { - Event.TASK_EXECUTION_ROUND_TIMEOUT: 60.0, - } - cross_period_persisted_keys: FrozenSet[str] = frozenset() - db_pre_conditions: Dict[AppState, Set[str]] = { - TaskExecutionRound: set(), - } - db_post_conditions: Dict[AppState, Set[str]] = { - FinishedTaskExecutionRound: {"most_voted_tx_hash"}, - FinishedTaskExecutionWithErrorRound: set(), - } diff --git a/packages/valory/skills/task_execution_abci/__init__.py b/packages/valory/skills/task_submission_abci/__init__.py similarity index 85% rename from packages/valory/skills/task_execution_abci/__init__.py rename to packages/valory/skills/task_submission_abci/__init__.py index 4f1f9002..b5cacd30 100644 --- a/packages/valory/skills/task_execution_abci/__init__.py +++ b/packages/valory/skills/task_submission_abci/__init__.py @@ -17,9 +17,9 @@ # # ------------------------------------------------------------------------------ -"""This module contains the implementation of the default skill.""" +"""This module contains the implementation of the task submission skill.""" from aea.configurations.base import PublicId -PUBLIC_ID = PublicId.from_str("valory/task_execution_abci:0.1.0") +PUBLIC_ID = PublicId.from_str("valory/task_submission_abci:0.1.0") diff --git a/packages/valory/skills/task_submission_abci/behaviours.py b/packages/valory/skills/task_submission_abci/behaviours.py new file mode 100644 index 00000000..31d0e701 --- /dev/null +++ b/packages/valory/skills/task_submission_abci/behaviours.py @@ -0,0 +1,319 @@ +# -*- 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 package contains round behaviours of TaskExecutionAbciApp.""" +import abc +import json +import threading +import time +from copy import deepcopy +from typing import Any, Dict, Generator, List, Optional, Set, Type, cast + +import openai # noqa + +from packages.valory.contracts.agent_mech.contract import AgentMechContract +from packages.valory.contracts.gnosis_safe.contract import ( + GnosisSafeContract, + SafeOperation, +) +from packages.valory.contracts.multisend.contract import ( + MultiSendContract, + MultiSendOperation, +) +from packages.valory.protocols.contract_api import ContractApiMessage +from packages.valory.skills.abstract_round_abci.base import AbstractRound +from packages.valory.skills.abstract_round_abci.behaviours import ( + AbstractRoundBehaviour, + BaseBehaviour, +) +from packages.valory.skills.task_submission_abci.models import Params +from packages.valory.skills.task_submission_abci.payloads import TransactionPayload +from packages.valory.skills.task_submission_abci.rounds import ( + SynchronizedData, + TaskPoolingPayload, + TaskPoolingRound, + TaskSubmissionAbciApp, + TransactionPreparationRound, +) +from packages.valory.skills.transaction_settlement_abci.payload_tools import ( + hash_payload_to_hex, +) + + +ZERO_ETHER_VALUE = 0 +SAFE_GAS = 0 +DONE_TASKS = "ready_tasks" +DONE_TASKS_LOCK = "lock" + + +class TaskExecutionBaseBehaviour(BaseBehaviour, abc.ABC): + """Base behaviour for the task_execution_abci skill.""" + + @property + def synchronized_data(self) -> SynchronizedData: + """Return the synchronized data.""" + return cast(SynchronizedData, super().synchronized_data) + + @property + def params(self) -> Params: + """Return the params.""" + return cast(Params, super().params) + + @property + def done_tasks(self) -> List[Dict[str, Any]]: + """ + Return the done (ready) tasks from shared state. + + Use with care, the returned data here is NOT synchronized with the rest of the agents. + + :returns: the tasks + """ + done_tasks = deepcopy(self.context.shared_state.get(DONE_TASKS, [])) + return cast(List[Dict[str, Any]], done_tasks) + + def done_tasks_lock(self) -> threading.Lock: + """Get done_tasks_lock.""" + return self.context.shared_state[DONE_TASKS_LOCK] + + def remove_tasks(self, submitted_tasks: List[Dict[str, Any]]) -> None: + """ + Pop the tasks from shared state. + + :param submitted_tasks: the done tasks that have already been submitted + """ + # run this in a lock + # the amount of done tasks will always be relatively low (<<20) + # we can afford to do this in a lock + with self.done_tasks_lock(): + done_tasks = self.done_tasks + not_submitted = [] + for done_task in done_tasks: + is_submitted = False + for submitted_task in submitted_tasks: + if submitted_task["request_id"] == done_task["request_id"]: + is_submitted = True + break + if not is_submitted: + not_submitted.append(done_task) + self.context.shared_state[DONE_TASKS] = not_submitted + + +class TaskPoolingBehaviour(TaskExecutionBaseBehaviour): + """TaskPoolingBehaviour""" + + matching_round: Type[AbstractRound] = TaskPoolingRound + + def async_act(self) -> Generator: # pylint: disable=R0914,R0915 + """Do the act, supporting asynchronous execution.""" + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + # clean up the queue based on the outcome of the previous period + self.handle_submitted_tasks() + # sync new tasks + payload_content = yield from self.get_payload_content() + sender = self.context.agent_address + payload = TaskPoolingPayload(sender=sender, content=payload_content) + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + self.set_done() + + def get_payload_content(self) -> Generator[None, None, str]: + """Get the payload content.""" + done_tasks = yield from self.get_done_tasks(self.params.task_wait_timeout) + return json.dumps(done_tasks) + + def get_done_tasks(self, timeout: float) -> Generator[None, None, List[Dict]]: + """Wait for tasks to get done in the specified timeout.""" + deadline = time.time() + timeout + while time.time() < deadline: + if len(self.done_tasks) == 0: + one_second = 1.0 + yield from self.sleep(one_second) + continue + # there are done tasks, return all of them + return self.done_tasks + + # no tasks are ready for this agent + self.context.logger.info("No tasks were ready within the timeout") + return [] + + def handle_submitted_tasks(self) -> None: + """Handle tasks that have been already submitted before (in a prev. period).""" + submitted_tasks = cast(List[Dict[str, Any]], self.synchronized_data.done_tasks) + self.context.logger.info( + f"Tasks {submitted_tasks} has already been submitted. " + f"Removing them from the list of tasks to be processed." + ) + self.remove_tasks(submitted_tasks) + + +class TransactionPreparationBehaviour(TaskExecutionBaseBehaviour): + """TransactionPreparationBehaviour""" + + matching_round: Type[AbstractRound] = TransactionPreparationRound + + def async_act(self) -> Generator: # pylint: disable=R0914,R0915 + """Do the act, supporting asynchronous execution.""" + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + payload_content = yield from self.get_payload_content() + sender = self.context.agent_address + payload = TransactionPayload(sender=sender, content=payload_content) + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction(payload) + yield from self.wait_until_round_end() + self.set_done() + + def get_payload_content(self) -> Generator[None, None, str]: + """Prepare the transaction""" + all_txs = [] + for task in self.synchronized_data.done_tasks: + deliver_tx = yield from self._get_deliver_tx(task) + if deliver_tx is None: + # something went wrong, respond with ERROR payload for now + return TransactionPreparationRound.ERROR_PAYLOAD + all_txs.append(deliver_tx) + response_tx = task.get("transaction", None) + if response_tx is not None: + all_txs.append(response_tx) + + multisend_tx_str = yield from self._to_multisend(all_txs) + if multisend_tx_str is None: + # something went wrong, respond with ERROR payload for now + return TransactionPreparationRound.ERROR_PAYLOAD + + return multisend_tx_str + + def _to_multisend( + self, transactions: List[Dict] + ) -> Generator[None, None, Optional[str]]: + """Transform payload to MultiSend.""" + multi_send_txs = [] + for transaction in transactions: + transaction = { + "operation": transaction.get("operation", MultiSendOperation.CALL), + "to": transaction["to"], + "value": transaction["value"], + "data": transaction.get("data", b""), + } + multi_send_txs.append(transaction) + + response = yield from self.get_contract_api_response( + performative=ContractApiMessage.Performative.GET_RAW_TRANSACTION, # type: ignore + contract_address=self.params.multisend_address, + contract_id=str(MultiSendContract.contract_id), + contract_callable="get_tx_data", + multi_send_txs=multi_send_txs, + ) + if response.performative != ContractApiMessage.Performative.RAW_TRANSACTION: + self.context.logger.error( + f"Couldn't compile the multisend tx. " + f"Expected performative {ContractApiMessage.Performative.RAW_TRANSACTION.value}, " # type: ignore + f"received {response.performative.value}." + ) + return None + + # strip "0x" from the response + multisend_data_str = cast(str, response.raw_transaction.body["data"])[2:] + tx_data = bytes.fromhex(multisend_data_str) + tx_hash = yield from self._get_safe_tx_hash(tx_data) + if tx_hash is None: + # something went wrong + return None + + payload_data = hash_payload_to_hex( + safe_tx_hash=tx_hash, + ether_value=ZERO_ETHER_VALUE, + safe_tx_gas=SAFE_GAS, + operation=SafeOperation.DELEGATE_CALL.value, + to_address=self.params.multisend_address, + data=tx_data, + ) + return payload_data + + def _get_safe_tx_hash(self, data: bytes) -> Generator[None, None, Optional[str]]: + """ + Prepares and returns the safe tx hash. + + This hash will be signed later by the agents, and submitted to the safe contract. + Note that this is the transaction that the safe will execute, with the provided data. + + :param data: the safe tx data. + :return: the tx hash + """ + response = yield from self.get_contract_api_response( + performative=ContractApiMessage.Performative.GET_STATE, # type: ignore + contract_address=self.synchronized_data.safe_contract_address, + contract_id=str(GnosisSafeContract.contract_id), + contract_callable="get_raw_safe_transaction_hash", + to_address=self.params.multisend_address, # we send the tx to the multisend address + value=ZERO_ETHER_VALUE, + data=data, + safe_tx_gas=SAFE_GAS, + operation=SafeOperation.DELEGATE_CALL.value, + ) + + if response.performative != ContractApiMessage.Performative.STATE: + self.context.logger.error( + f"Couldn't get safe hash. " + f"Expected response performative {ContractApiMessage.Performative.STATE.value}, " # type: ignore + f"received {response.performative.value}." + ) + return None + + # strip "0x" from the response hash + tx_hash = cast(str, response.state.body["tx_hash"])[2:] + return tx_hash + + def _get_deliver_tx( + self, task_data: Dict[str, Any] + ) -> Generator[None, None, Optional[Dict]]: + """Get the deliver tx.""" + contract_api_msg = yield from self.get_contract_api_response( + performative=ContractApiMessage.Performative.GET_STATE, # type: ignore + contract_address=self.params.agent_mech_contract_address, + contract_id=str(AgentMechContract.contract_id), + contract_callable="get_deliver_data", + request_id=task_data["request_id"], + data=task_data["task_result"], + ) + if ( + contract_api_msg.performative != ContractApiMessage.Performative.STATE + ): # pragma: nocover + self.context.logger.warning( + f"get_deliver_data unsuccessful!: {contract_api_msg}" + ) + return None + + data = cast(bytes, contract_api_msg.state.body["data"]) + return { + "to": self.params.agent_mech_contract_address, + "value": ZERO_ETHER_VALUE, + "data": data, + } + + +class TaskSubmissionRoundBehaviour(AbstractRoundBehaviour): + """TaskSubmissionRoundBehaviour""" + + initial_behaviour_cls = TaskPoolingBehaviour + abci_app_cls = TaskSubmissionAbciApp + behaviours: Set[Type[BaseBehaviour]] = { + TaskPoolingBehaviour, # type: ignore + TransactionPreparationBehaviour, # type: ignore + } diff --git a/packages/valory/skills/task_execution_abci/dialogues.py b/packages/valory/skills/task_submission_abci/dialogues.py similarity index 100% rename from packages/valory/skills/task_execution_abci/dialogues.py rename to packages/valory/skills/task_submission_abci/dialogues.py diff --git a/packages/valory/skills/task_submission_abci/fsm_specification.yaml b/packages/valory/skills/task_submission_abci/fsm_specification.yaml new file mode 100644 index 00000000..1257678f --- /dev/null +++ b/packages/valory/skills/task_submission_abci/fsm_specification.yaml @@ -0,0 +1,25 @@ +alphabet_in: +- DONE +- ERROR +- NO_MAJORITY +- NO_TASKS +default_start_state: TaskPoolingRound +final_states: +- FinishedTaskExecutionWithErrorRound +- FinishedTaskPoolingRound +- FinishedWithoutTasksRound +label: TaskSubmissionAbciApp +start_states: +- TaskPoolingRound +states: +- FinishedTaskExecutionWithErrorRound +- FinishedTaskPoolingRound +- FinishedWithoutTasksRound +- TaskPoolingRound +- TransactionPreparationRound +transition_func: + (TaskPoolingRound, DONE): TransactionPreparationRound + (TaskPoolingRound, NO_TASKS): FinishedWithoutTasksRound + (TransactionPreparationRound, DONE): FinishedTaskPoolingRound + (TransactionPreparationRound, ERROR): FinishedTaskExecutionWithErrorRound + (TransactionPreparationRound, NO_MAJORITY): FinishedTaskExecutionWithErrorRound diff --git a/packages/valory/skills/task_execution_abci/handlers.py b/packages/valory/skills/task_submission_abci/handlers.py similarity index 100% rename from packages/valory/skills/task_execution_abci/handlers.py rename to packages/valory/skills/task_submission_abci/handlers.py diff --git a/packages/valory/skills/multiplexer_abci/models.py b/packages/valory/skills/task_submission_abci/models.py similarity index 69% rename from packages/valory/skills/multiplexer_abci/models.py rename to packages/valory/skills/task_submission_abci/models.py index 79f00477..4eb82c4a 100644 --- a/packages/valory/skills/multiplexer_abci/models.py +++ b/packages/valory/skills/task_submission_abci/models.py @@ -17,12 +17,11 @@ # # ------------------------------------------------------------------------------ -"""This module contains the shared state for the abci skill of MultiplexerAbciApp.""" +"""This module contains the shared state for the abci skill of TaskExecutionAbciApp.""" -from typing import Any, List - -from aea.skills.base import SkillContext +from typing import Any, Type +from packages.valory.skills.abstract_round_abci.base import AbciApp from packages.valory.skills.abstract_round_abci.models import BaseParams from packages.valory.skills.abstract_round_abci.models import ( BenchmarkTool as BaseBenchmarkTool, @@ -31,20 +30,13 @@ from packages.valory.skills.abstract_round_abci.models import ( SharedState as BaseSharedState, ) -from packages.valory.skills.multiplexer_abci.rounds import MultiplexerAbciApp +from packages.valory.skills.task_submission_abci.rounds import TaskSubmissionAbciApp class SharedState(BaseSharedState): """Keep the current shared state of the skill.""" - abci_app_cls = MultiplexerAbciApp - - def __init__(self, *args: Any, skill_context: SkillContext, **kwargs: Any) -> None: - """Init""" - super().__init__(*args, skill_context=skill_context, **kwargs) - - self.pending_tasks: List = [] - self.last_processed_request_block_number: int = 0 + abci_app_cls: Type[AbciApp] = TaskSubmissionAbciApp class Params(BaseParams): @@ -53,9 +45,10 @@ class Params(BaseParams): def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the parameters object.""" - self.reset_period_count = self._ensure("reset_period_count", kwargs, int) - self.use_polling = self._ensure("use_polling", kwargs, bool) - self.polling_interval = self._ensure("polling_interval", kwargs, int) + self.task_wait_timeout = self._ensure("task_wait_timeout", kwargs, float) + self.multisend_address = kwargs.get("multisend_address", None) + if self.multisend_address is None: + raise ValueError("No multisend_address specified!") self.agent_mech_contract_address = kwargs.get( "agent_mech_contract_address", None ) diff --git a/packages/valory/skills/task_execution_abci/payloads.py b/packages/valory/skills/task_submission_abci/payloads.py similarity index 78% rename from packages/valory/skills/task_execution_abci/payloads.py rename to packages/valory/skills/task_submission_abci/payloads.py index 3248b3ef..e4be15d5 100644 --- a/packages/valory/skills/task_execution_abci/payloads.py +++ b/packages/valory/skills/task_submission_abci/payloads.py @@ -25,7 +25,14 @@ @dataclass(frozen=True) -class TaskExecutionAbciPayload(BaseTxPayload): - """Represent a transaction payload for the TaskExecutionRound.""" +class TaskPoolingPayload(BaseTxPayload): + """Represent a transaction payload for the TaskPoolingRound.""" + + content: str + + +@dataclass(frozen=True) +class TransactionPayload(BaseTxPayload): + """Represent a transaction payload for the TransactionPreparationRound.""" content: str diff --git a/packages/valory/skills/task_submission_abci/rounds.py b/packages/valory/skills/task_submission_abci/rounds.py new file mode 100644 index 00000000..6219c72b --- /dev/null +++ b/packages/valory/skills/task_submission_abci/rounds.py @@ -0,0 +1,230 @@ +# -*- 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 package contains the rounds of TaskSubmissionAbciApp.""" +import json +from enum import Enum +from typing import Any, Dict, FrozenSet, List, Optional, Set, Tuple, cast + +from packages.valory.skills.abstract_round_abci.base import ( + AbciApp, + AbciAppTransitionFunction, + AppState, + BaseSynchronizedData, + CollectSameUntilThresholdRound, + CollectionRound, + DegenerateRound, + EventToTimeout, + get_name, +) +from packages.valory.skills.task_submission_abci.payloads import ( + TaskPoolingPayload, + TransactionPayload, +) + + +class Event(Enum): + """TaskSubmissionAbciApp Events""" + + TASK_EXECUTION_ROUND_TIMEOUT = "task_execution_round_timeout" + ROUND_TIMEOUT = "round_timeout" + NO_MAJORITY = "no_majority" + DONE = "done" + NO_TASKS = "no_tasks" + ERROR = "error" + + +class SynchronizedData(BaseSynchronizedData): + """ + Class to represent the synchronized data. + + This data is replicated by the tendermint application. + """ + + @property + def most_voted_tx_hash(self) -> str: + """Get the most_voted_tx_hash.""" + return cast(str, self.db.get_strict("most_voted_tx_hash")) + + @property + def done_tasks(self) -> List[Dict[str, Any]]: + """Done tasks.""" + return cast(List[Dict[str, Any]], self.db.get("done_tasks", [])) + + +class TaskPoolingRound(CollectionRound): + """TaskPoolingRound""" + + payload_class = TaskPoolingPayload + synchronized_data_class = SynchronizedData + + move_forward_payload: Optional[TaskPoolingPayload] = None + + ERROR_PAYLOAD = "ERROR" + + @property + def collection_threshold_reached( + self, + ) -> bool: + """Check that the collection threshold has been reached.""" + return len(self.collection) >= self.synchronized_data.consensus_threshold + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: + """Process the end of the block.""" + if self.collection_threshold_reached: + all_done_tasks = [] + for payload in self.collection.values(): + done_tasks_str = cast(TaskPoolingPayload, payload).content + done_tasks = json.loads(done_tasks_str) + all_done_tasks.extend(done_tasks) + all_done_tasks = sorted(all_done_tasks, key=lambda x: x["request_id"]) + synchronized_data = self.synchronized_data.update( + synchronized_data_class=SynchronizedData, + **{ + get_name(SynchronizedData.done_tasks): all_done_tasks, + } + ) + if len(all_done_tasks) > 0: + return synchronized_data, Event.DONE + return synchronized_data, Event.NO_TASKS + + return None + + +class TransactionPreparationRound(CollectSameUntilThresholdRound): + """TransactionPreparationRound""" + + payload_class = TransactionPayload + payload_attribute = "content" + synchronized_data_class = SynchronizedData + + ERROR_PAYLOAD = "error" + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + if self.threshold_reached: + if self.most_voted_payload == self.ERROR_PAYLOAD: + return ( + self.synchronized_data.update( + synchronized_data_class=SynchronizedData, + **{ + get_name(SynchronizedData.done_tasks): [], + } + ), + Event.ERROR, + ) + + state = self.synchronized_data.update( + synchronized_data_class=self.synchronized_data_class, + **{ + get_name( + SynchronizedData.most_voted_tx_hash + ): self.most_voted_payload, + } + ) + return state, Event.DONE + if not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ): + # in case we cant submit this tx, we need to make sure we don't account the tasks as done + return ( + self.synchronized_data.update( + synchronized_data_class=SynchronizedData, + **{ + get_name(SynchronizedData.done_tasks): [], + } + ), + Event.NO_MAJORITY, + ) + + return None + + +class FinishedTaskPoolingRound(DegenerateRound): + """FinishedTaskPoolingRound""" + + +class FinishedTaskExecutionWithErrorRound(DegenerateRound): + """FinishedTaskExecutionWithErrorRound""" + + +class FinishedWithoutTasksRound(DegenerateRound): + """FinishedWithoutTasksRound""" + + +class TaskSubmissionAbciApp(AbciApp[Event]): + """TaskSubmissionAbciApp + + Initial round: TaskPoolingRound + + Initial states: {TaskPoolingRound} + + Transition states: + 0. TaskPoolingRound + - done: 1. + - no tasks: 4. + 1. TransactionPreparationRound + - done: 2. + - error: 3. + - no majority: 3. + 2. FinishedTaskPoolingRound + 3. FinishedTaskExecutionWithErrorRound + 4. FinishedWithoutTasksRound + + Final states: {FinishedTaskExecutionWithErrorRound, FinishedTaskPoolingRound, FinishedWithoutTasksRound} + + Timeouts: + task execution round timeout: 60.0 + """ + + initial_round_cls: AppState = TaskPoolingRound + initial_states: Set[AppState] = {TaskPoolingRound} + transition_function: AbciAppTransitionFunction = { + TaskPoolingRound: { + Event.DONE: TransactionPreparationRound, + Event.NO_TASKS: FinishedWithoutTasksRound, + }, + TransactionPreparationRound: { + Event.DONE: FinishedTaskPoolingRound, + Event.ERROR: FinishedTaskExecutionWithErrorRound, + Event.NO_MAJORITY: FinishedTaskExecutionWithErrorRound, + }, + FinishedTaskPoolingRound: {}, + FinishedTaskExecutionWithErrorRound: {}, + FinishedWithoutTasksRound: {}, + } + final_states: Set[AppState] = { + FinishedTaskPoolingRound, + FinishedWithoutTasksRound, + FinishedTaskExecutionWithErrorRound, + } + event_to_timeout: EventToTimeout = { + Event.TASK_EXECUTION_ROUND_TIMEOUT: 60.0, + } + cross_period_persisted_keys: FrozenSet[str] = frozenset( + [get_name(SynchronizedData.done_tasks)] + ) + db_pre_conditions: Dict[AppState, Set[str]] = { + TaskPoolingRound: set(), + } + db_post_conditions: Dict[AppState, Set[str]] = { + FinishedTaskPoolingRound: {"most_voted_tx_hash"}, + FinishedTaskExecutionWithErrorRound: set(), + FinishedWithoutTasksRound: set(), + } diff --git a/packages/valory/skills/task_execution_abci/skill.yaml b/packages/valory/skills/task_submission_abci/skill.yaml similarity index 73% rename from packages/valory/skills/task_execution_abci/skill.yaml rename to packages/valory/skills/task_submission_abci/skill.yaml index 5f61d002..91ed3086 100644 --- a/packages/valory/skills/task_execution_abci/skill.yaml +++ b/packages/valory/skills/task_submission_abci/skill.yaml @@ -1,4 +1,4 @@ -name: task_execution_abci +name: task_submission_abci author: valory version: 0.1.0 type: skill @@ -7,22 +7,19 @@ description: An abci skill that implements task execution and transaction prepar license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: - __init__.py: bafybeihrkpey6kxur2uoimrskq2wfpelqidxeapdxie6iuv2x7dk77ksvu - behaviours.py: bafybeihhddcm6qdavd5w4fshyknn3ukjmjpswwcdem6l4no62nqhbgqkda + __init__.py: bafybeiholqak7ltw6bbmn2c5tn3j7xgzkdlfzp3kcskiqsvmxoih6m4muq + behaviours.py: bafybeiekpqtkafxmxwd4cktoguz2fwdwohhmwginznvsnzawrurofx2d3a dialogues.py: bafybeibmac3m5u5h6ucoyjr4dazay72dyga656wvjl6z6saapluvjo54ne - fsm_specification.yaml: bafybeia66ok2ll4kjbbmgbocjfape6u6ctacgexrnpgmru6zudr5em7vty + fsm_specification.yaml: bafybeig6bhn554qyou7kef5bstnlv54zke32avyti63uu4hvsol3lzqkoi handlers.py: bafybeibe5n7my2vd2wlwo73sbma65epjqc7kxgtittewlylcmvnmoxtxzq - io_/__init__.py: bafybeifxgmmwjqzezzn3e6keh2bfo4cyo7y5dq2ept3stfmgglbrzfl5rq - io_/naive_loader.py: bafybeihqrt34jso7dwfcedh7itmmovfv55tdjhw2tkqifsbiohetbonynu - models.py: bafybeihavofxq3nxt46x74idm2mjl5xxghoqzjtuxnx5i255k6mdwsyyaq - payloads.py: bafybeigptsnusjowmqjcxnzc4ct7n2iczuiorlwqsg7dl6ipnwkjb6iqoe - rounds.py: bafybeifaza7nzpn7fv6xuk6pcamxne3b5tzqogricjkcvbek5cso2emcnm + models.py: bafybeigtexvivmi5egiglmg2s6qc3eceb6z7kklgxk3mvybyqu5okx7eni + payloads.py: bafybeia2yorri2u5rwh6vukb6iwdrbn53ygsuuhthns2txptvjipyb6f4e + rounds.py: bafybeicstmau4vlzpxz3kjgiwwsetwmotdk4un4iucmdddzvot5dgdkg2a tasks.py: bafybeicu5t5cvfhbndgpxbbtmp4vbmtyb6fba6vsnlewftvuderxp5lwcy fingerprint_ignore_patterns: [] -connections: -- valory/p2p_libp2p_client:0.1.0:bafybeihdnfdth3qgltefgrem7xyi4b3ejzaz67xglm2hbma2rfvpl2annq +connections: [] contracts: -- valory/agent_mech:0.1.0:bafybeigzl5sjks2tqszum6axrkwjlmybsgp54om5auybbdp3uyfx3zef7q +- valory/agent_mech:0.1.0:bafybeidl6kwc3sgcxiphgb3osjqlqwylhqetv2nyv2fu6zxcgn5qctv2ju - valory/gnosis_safe:0.1.0:bafybeigvqg4lapdaa23dpc3pv67rdptdhey6e435mxqsw2gb2u74yw4yei - valory/multisend:0.1.0:bafybeie7m7pjbnw7cccpbvmbgkut24dtlt4cgvug3tbac7gej37xvwbv3a protocols: @@ -34,7 +31,7 @@ skills: behaviours: main: args: {} - class_name: TaskExecutionRoundBehaviour + class_name: TaskSubmissionRoundBehaviour handlers: abci: args: {} @@ -82,25 +79,8 @@ models: class_name: AcnDataShareDialogue params: args: - api_keys_json: - - - openai - - dummy_api_key - - - stabilityai - - dummy_api_key - cleanup_history_depth: 1 cleanup_history_depth_current: null drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 - file_hash_to_tools_json: - - - bafybeif3izkobmvaoen23ine6tiqx55eaf4g3r56hdalnig656xivzpf3m - - - 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 finalize_timeout: 60.0 genesis_config: chain_id: chain-c4daS1 @@ -149,6 +129,7 @@ models: tendermint_p2p_url: localhost:26656 tendermint_url: http://localhost:26657 tx_timeout: 10.0 + task_wait_timeout: 15 use_termination: false validate_timeout: 1205 class_name: Params diff --git a/packages/valory/skills/task_execution_abci/tasks.py b/packages/valory/skills/task_submission_abci/tasks.py similarity index 100% rename from packages/valory/skills/task_execution_abci/tasks.py rename to packages/valory/skills/task_submission_abci/tasks.py diff --git a/tox.ini b/tox.ini index 7468c38d..28d28e18 100644 --- a/tox.ini +++ b/tox.ini @@ -65,7 +65,7 @@ setenv = PYTHONPATH={env:PWD:%CD%} PACKAGES_PATHS = packages/valory SKILLS_PATHS = {env:PACKAGES_PATHS}/skills - SERVICE_SPECIFIC_PACKAGES = {env:PACKAGES_PATHS}/connections/websocket_client {env:PACKAGES_PATHS}/contracts/agent_mech {env:SKILLS_PATHS}/contract_subscription {env:SKILLS_PATHS}/mech_abci {env:SKILLS_PATHS}/multiplexer_abci {env:SKILLS_PATHS}/task_execution_abci + SERVICE_SPECIFIC_PACKAGES = {env:PACKAGES_PATHS}/connections/websocket_client {env:PACKAGES_PATHS}/contracts/agent_mech {env:SKILLS_PATHS}/contract_subscription {env:SKILLS_PATHS}/mech_abci {env:SKILLS_PATHS}/task_execution {env:SKILLS_PATHS}/task_submission_abci [testenv:bandit] skipsdist = True