diff --git a/docs/index.md b/docs/index.md index 102c5606..9d304a8f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,7 @@ In order to run a local demo service based on the IEKit: 2. Fetch the IEKit. ```bash - autonomy fetch valory/impact_evaluator:0.1.0:bafybeidtxoqpimts5erkcpuscojhmy3f7pyhr7ivbt7ynorro3zx7pmzwy --service + autonomy fetch valory/impact_evaluator:0.1.0:bafybeiewo55hyk4tgw53c2h7jszttk35lx3thuiodcpkzyi3fmrv74o7bq --service ``` 3. Build the Docker image of the service agents diff --git a/packages/packages.json b/packages/packages.json index abf5d228..13b82d0f 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -1,13 +1,13 @@ { "dev": { - "agent/valory/impact_evaluator/0.1.0": "bafybeifcppzj5u5fgic5fjqliignzrc6dkejgjh7zfbgrumap42nx3lgqm", + "agent/valory/impact_evaluator/0.1.0": "bafybeif6lpdpuq2pcocmre4iswp6puraxrlib6b2ydzew2daboei6pduju", "contract/valory/dynamic_contribution/0.1.0": "bafybeie76ynpueo3hh2ujzcfgpqcsbyqwa4pdcc6g44a4he6bueyb4tqiy", - "service/valory/impact_evaluator/0.1.0": "bafybeidtxoqpimts5erkcpuscojhmy3f7pyhr7ivbt7ynorro3zx7pmzwy", + "service/valory/impact_evaluator/0.1.0": "bafybeiewo55hyk4tgw53c2h7jszttk35lx3thuiodcpkzyi3fmrv74o7bq", "skill/valory/dynamic_nft_abci/0.1.0": "bafybeie5vfscnjpvuwqe5vk7i2rzzlqciojswdg7uh34fjnuk3bspca5zq", - "skill/valory/twitter_scoring_abci/0.1.0": "bafybeihplombdirefn37vtnugn55f2b4khcywtboi4w72iczxybf4x5pwm", + "skill/valory/twitter_scoring_abci/0.1.0": "bafybeifu7vbznnx42oayoh24malmebchfvbitndzpbmf6xmp7hgq26buyy", "skill/valory/ceramic_read_abci/0.1.0": "bafybeidruvxgpbggchuvlnssuswdxuoz2ep6sjvzzqjeyckybb6gpjx3ia", "skill/valory/ceramic_write_abci/0.1.0": "bafybeigtkv57h5suaqqpjfknimfegbidovxna7cok7znsgzrfb4nftqvay", - "skill/valory/impact_evaluator_abci/0.1.0": "bafybeifjst43yn2bn6ibljwbfcjirc7yhbelwsugntpan4k666xin3ebom", + "skill/valory/impact_evaluator_abci/0.1.0": "bafybeig5kiim7x6svzbrxsmwjqntlfqbdmaod5n2vjbs7yqwj5sldgi6y4", "skill/valory/generic_scoring_abci/0.1.0": "bafybeiemmcnduhdj427kgafkxcbn2rfhaihabtpda34yqnks7utm3g4xiq", "protocol/valory/twitter/0.1.0": "bafybeib4eyf7qbs7kdntqzhwqsaaj4o2mzcokcztaza6qgwt7sbxgkqu2m", "protocol/valory/llm/1.0.0": "bafybeigqybmg75vsxexmp57hkms7lkp7iwpf54r7wpygizxryvrhfqqpb4", diff --git a/packages/valory/agents/impact_evaluator/aea-config.yaml b/packages/valory/agents/impact_evaluator/aea-config.yaml index 189e0ddd..4c273b72 100644 --- a/packages/valory/agents/impact_evaluator/aea-config.yaml +++ b/packages/valory/agents/impact_evaluator/aea-config.yaml @@ -12,7 +12,7 @@ fingerprint: tests/helpers/data/json_server/data.json: bafybeiapboptlarlpc2lboj4g2526vev7fwqpr267tb2qn6cevbblpnewe tests/helpers/docker.py: bafybeihlg5thdrcaiuzyte5s7x25mikqfyxdjwuqvhmeddluyjdkzhuhqi tests/helpers/fixtures.py: bafybeidfsamzdrqqkdra4ektollyfkhiyb2iqymy6djavgewon2cb23vwu - tests/test_impact_evaluator.py: bafybeigo4mbjppcd24f47zykj7icq2vatfddskqrfpyylqqdphcfd5hbeu + tests/test_impact_evaluator.py: bafybeic55qbou5gfl47v5lw7hg3hawrlio667z5tjv66wfp5ue2gfsc4n4 fingerprint_ignore_patterns: [] connections: - fetchai/http_server:0.22.0:bafybeihp5umafxzx45aad5pj7s3343se2wjkgnbirt4pybrape22swm6de @@ -42,9 +42,9 @@ protocols: skills: - valory/abstract_abci:0.1.0:bafybeicg7dv7cff34nv2k2z47c4yp4kddsxp3wozonzow6tnvfvwndz3cy - valory/abstract_round_abci:0.1.0:bafybeigxjcci53vwytymzlhr37436yvenh7jup4astrn7dgyixo24aq2pq -- valory/impact_evaluator_abci:0.1.0:bafybeifjst43yn2bn6ibljwbfcjirc7yhbelwsugntpan4k666xin3ebom +- valory/impact_evaluator_abci:0.1.0:bafybeig5kiim7x6svzbrxsmwjqntlfqbdmaod5n2vjbs7yqwj5sldgi6y4 - valory/generic_scoring_abci:0.1.0:bafybeiemmcnduhdj427kgafkxcbn2rfhaihabtpda34yqnks7utm3g4xiq -- valory/twitter_scoring_abci:0.1.0:bafybeihplombdirefn37vtnugn55f2b4khcywtboi4w72iczxybf4x5pwm +- valory/twitter_scoring_abci:0.1.0:bafybeifu7vbznnx42oayoh24malmebchfvbitndzpbmf6xmp7hgq26buyy - valory/ceramic_read_abci:0.1.0:bafybeidruvxgpbggchuvlnssuswdxuoz2ep6sjvzzqjeyckybb6gpjx3ia - valory/ceramic_write_abci:0.1.0:bafybeigtkv57h5suaqqpjfknimfegbidovxna7cok7znsgzrfb4nftqvay - valory/dynamic_nft_abci:0.1.0:bafybeie5vfscnjpvuwqe5vk7i2rzzlqciojswdg7uh34fjnuk3bspca5zq diff --git a/packages/valory/agents/impact_evaluator/tests/test_impact_evaluator.py b/packages/valory/agents/impact_evaluator/tests/test_impact_evaluator.py index e6628cac..d8a99350 100644 --- a/packages/valory/agents/impact_evaluator/tests/test_impact_evaluator.py +++ b/packages/valory/agents/impact_evaluator/tests/test_impact_evaluator.py @@ -72,30 +72,44 @@ TwitterDecisionMakingRound, TwitterHashtagsCollectionRound, TwitterMentionsCollectionRound, + TwitterRandomnessRound, + TwitterSelectKeepersRound, ) HAPPY_PATH: Tuple[RoundChecks, ...] = ( + # Start, read data and generic scoring RoundChecks(RegistrationStartupRound.auto_round_id(), n_periods=1), RoundChecks(StreamReadRound.auto_round_id(), n_periods=3), RoundChecks(GenericScoringRound.auto_round_id(), n_periods=2), + # Keeper + RoundChecks(TwitterDecisionMakingRound.auto_round_id(), n_periods=2), + RoundChecks(TwitterRandomnessRound.auto_round_id(), n_periods=2), + RoundChecks(TwitterSelectKeepersRound.auto_round_id(), n_periods=2), + + # OpenAI check RoundChecks(TwitterDecisionMakingRound.auto_round_id(), n_periods=2), RoundChecks(OpenAICallCheckRound.auto_round_id(), n_periods=2), + # Twitter API RoundChecks(TwitterDecisionMakingRound.auto_round_id(), n_periods=2), RoundChecks(TwitterMentionsCollectionRound.auto_round_id(), n_periods=2), - RoundChecks(TwitterDecisionMakingRound.auto_round_id(), n_periods=2), RoundChecks(TwitterHashtagsCollectionRound.auto_round_id(), n_periods=2), + # Evaluation RoundChecks(TwitterDecisionMakingRound.auto_round_id(), n_periods=2), RoundChecks(TweetEvaluationRound.auto_round_id(), n_periods=2), + # DB update RoundChecks(TwitterDecisionMakingRound.auto_round_id(), n_periods=2), RoundChecks(DBUpdateRound.auto_round_id(), n_periods=2), + # Check token RoundChecks(TokenTrackRound.auto_round_id(), n_periods=2), + + # Write db and reset RoundChecks(RandomnessRound.auto_round_id(), n_periods=2), RoundChecks(SelectKeeperRound.auto_round_id(), n_periods=2), RoundChecks(StreamWriteRound.auto_round_id(), n_periods=2), diff --git a/packages/valory/services/impact_evaluator/service.yaml b/packages/valory/services/impact_evaluator/service.yaml index 86bdbbfa..ddab5be5 100644 --- a/packages/valory/services/impact_evaluator/service.yaml +++ b/packages/valory/services/impact_evaluator/service.yaml @@ -8,7 +8,7 @@ license: Apache-2.0 fingerprint: README.md: bafybeign56hilwuoa6bgos3uqabss4gew4vadkik7vhj3ucpqw6nxtqtpe fingerprint_ignore_patterns: [] -agent: valory/impact_evaluator:0.1.0:bafybeifcppzj5u5fgic5fjqliignzrc6dkejgjh7zfbgrumap42nx3lgqm +agent: valory/impact_evaluator:0.1.0:bafybeif6lpdpuq2pcocmre4iswp6puraxrlib6b2ydzew2daboei6pduju number_of_agents: 4 deployment: agent: diff --git a/packages/valory/skills/impact_evaluator_abci/fsm_specification.yaml b/packages/valory/skills/impact_evaluator_abci/fsm_specification.yaml index eb04dff3..8de4f68e 100644 --- a/packages/valory/skills/impact_evaluator_abci/fsm_specification.yaml +++ b/packages/valory/skills/impact_evaluator_abci/fsm_specification.yaml @@ -27,6 +27,7 @@ alphabet_in: - ROUND_TIMEOUT - SCHEDULED_TWEET - SCORE +- SELECT_KEEPERS - TWEET_EVALUATION_ROUND_TIMEOUT - UPDATE_CENTAURS - VERIFICATION_ERROR @@ -59,6 +60,8 @@ states: - TwitterDecisionMakingRound - TwitterHashtagsCollectionRound - TwitterMentionsCollectionRound +- TwitterRandomnessRound +- TwitterSelectKeepersRound - TwitterWriteRound - VerificationRound transition_func: @@ -138,6 +141,7 @@ transition_func: (TwitterDecisionMakingRound, RETRIEVE_HASHTAGS): TwitterHashtagsCollectionRound (TwitterDecisionMakingRound, RETRIEVE_MENTIONS): TwitterMentionsCollectionRound (TwitterDecisionMakingRound, ROUND_TIMEOUT): TwitterDecisionMakingRound + (TwitterDecisionMakingRound, SELECT_KEEPERS): TwitterRandomnessRound (TwitterHashtagsCollectionRound, API_ERROR): TwitterHashtagsCollectionRound (TwitterHashtagsCollectionRound, DONE): TwitterDecisionMakingRound (TwitterHashtagsCollectionRound, DONE_MAX_RETRIES): TwitterDecisionMakingRound @@ -148,6 +152,12 @@ transition_func: (TwitterMentionsCollectionRound, DONE_MAX_RETRIES): TwitterDecisionMakingRound (TwitterMentionsCollectionRound, NO_MAJORITY): TwitterMentionsCollectionRound (TwitterMentionsCollectionRound, ROUND_TIMEOUT): TwitterMentionsCollectionRound + (TwitterRandomnessRound, DONE): TwitterSelectKeepersRound + (TwitterRandomnessRound, NO_MAJORITY): TwitterRandomnessRound + (TwitterRandomnessRound, ROUND_TIMEOUT): TwitterRandomnessRound + (TwitterSelectKeepersRound, DONE): TwitterDecisionMakingRound + (TwitterSelectKeepersRound, NO_MAJORITY): TwitterRandomnessRound + (TwitterSelectKeepersRound, ROUND_TIMEOUT): TwitterRandomnessRound (TwitterWriteRound, API_ERROR): RandomnessTwitterRound (TwitterWriteRound, CONTINUE): TwitterWriteRound (TwitterWriteRound, DONE): DecisionMakingRound diff --git a/packages/valory/skills/impact_evaluator_abci/skill.yaml b/packages/valory/skills/impact_evaluator_abci/skill.yaml index 58c9247c..8a46962b 100644 --- a/packages/valory/skills/impact_evaluator_abci/skill.yaml +++ b/packages/valory/skills/impact_evaluator_abci/skill.yaml @@ -10,7 +10,7 @@ fingerprint: behaviours.py: bafybeifhoufesr4r4re65mxjadqasjymvz6tkc25xn5yyn473yunhjiufe composition.py: bafybeifmf52jhbiyyqyxzd67gxrydedm44ouzib3jh56kzoe5xohjrhm34 dialogues.py: bafybeigjknz4qqynbsltjje46gidg4rftsqw6ybjwegz24wetmycutpzh4 - fsm_specification.yaml: bafybeibgyjc25ukeorz3w3vxnmnjhczvy3psobl4gtivcbhnfq5qetwh4y + fsm_specification.yaml: bafybeiczfb5nrqfuugkhi3pa4gyw5odtv5bwl6twdh6rlmmq7dwxsxud7a handlers.py: bafybeidkli6fphcmdgwsys4lkyf3fx6fbawet4nt2pnixfypzijhg6b3ze models.py: bafybeifhcshu4iwqvc2fz3auu5ngfykkhvqnroeug52ilrwwc4kowkeg5a tests/__init__.py: bafybeievwzwojvq4aofk5kjpf4jzygfes7ew6s6svc6b6frktjnt3sicce @@ -26,7 +26,7 @@ skills: - valory/abstract_round_abci:0.1.0:bafybeigxjcci53vwytymzlhr37436yvenh7jup4astrn7dgyixo24aq2pq - valory/ceramic_read_abci:0.1.0:bafybeidruvxgpbggchuvlnssuswdxuoz2ep6sjvzzqjeyckybb6gpjx3ia - valory/generic_scoring_abci:0.1.0:bafybeiemmcnduhdj427kgafkxcbn2rfhaihabtpda34yqnks7utm3g4xiq -- valory/twitter_scoring_abci:0.1.0:bafybeihplombdirefn37vtnugn55f2b4khcywtboi4w72iczxybf4x5pwm +- valory/twitter_scoring_abci:0.1.0:bafybeifu7vbznnx42oayoh24malmebchfvbitndzpbmf6xmp7hgq26buyy - valory/ceramic_write_abci:0.1.0:bafybeigtkv57h5suaqqpjfknimfegbidovxna7cok7znsgzrfb4nftqvay - valory/dynamic_nft_abci:0.1.0:bafybeie5vfscnjpvuwqe5vk7i2rzzlqciojswdg7uh34fjnuk3bspca5zq - valory/registration_abci:0.1.0:bafybeibc4kczqbh23sc6tufrzn3axmhp3vjav7fa3u6cnpvolrbbc2fd7i diff --git a/packages/valory/skills/twitter_scoring_abci/behaviours.py b/packages/valory/skills/twitter_scoring_abci/behaviours.py index c683e4ab..d72de09b 100644 --- a/packages/valory/skills/twitter_scoring_abci/behaviours.py +++ b/packages/valory/skills/twitter_scoring_abci/behaviours.py @@ -20,10 +20,12 @@ """This package contains round behaviours of TwitterScoringAbciApp.""" import json +import math +import random import re from abc import ABC from datetime import datetime -from typing import Dict, Generator, Optional, Set, Tuple, Type, cast +from typing import Dict, Generator, List, Optional, Set, Tuple, Type, cast from web3 import Web3 @@ -36,6 +38,7 @@ AbstractRoundBehaviour, BaseBehaviour, ) +from packages.valory.skills.abstract_round_abci.common import RandomnessBehaviour from packages.valory.skills.abstract_round_abci.models import Requests from packages.valory.skills.twitter_scoring_abci.ceramic_db import CeramicDB from packages.valory.skills.twitter_scoring_abci.dialogues import ( @@ -54,6 +57,8 @@ TwitterDecisionMakingPayload, TwitterHashtagsCollectionPayload, TwitterMentionsCollectionPayload, + TwitterRandomnessPayload, + TwitterSelectKeepersPayload, ) from packages.valory.skills.twitter_scoring_abci.prompts import tweet_evaluation_prompt from packages.valory.skills.twitter_scoring_abci.rounds import ( @@ -65,7 +70,9 @@ TwitterDecisionMakingRound, TwitterHashtagsCollectionRound, TwitterMentionsCollectionRound, + TwitterRandomnessRound, TwitterScoringAbciApp, + TwitterSelectKeepersRound, ) @@ -130,6 +137,103 @@ def _check_daily_limit(self) -> Tuple: return False, number_of_tweets_pulled_today, last_tweet_pull_window_reset +class TwitterRandomnessBehaviour(RandomnessBehaviour): + """Retrieve randomness.""" + + matching_round = TwitterRandomnessRound + payload_class = TwitterRandomnessPayload + + +class TwitterSelectKeepersBehaviour(TwitterScoringBaseBehaviour): + """Select the keeper agent.""" + + matching_round = TwitterSelectKeepersRound + payload_class = TwitterSelectKeepersPayload + + def _select_keepers(self) -> List[str]: + """ + Select new keepers randomly. + + 1. Sort the list of participants who are not blacklisted as keepers. + 2. Randomly shuffle it. + 3. Pick the first keepers in order. + 4. If they have already been selected, pick the next ones. + + :return: the selected keepers' addresses. + """ + # Get all the participants who have not been blacklisted as keepers + non_blacklisted = ( + self.synchronized_data.participants + - self.synchronized_data.blacklisted_keepers + ) + if not non_blacklisted: + raise RuntimeError( + "Cannot continue if all the keepers have been blacklisted!" + ) + + # Sorted list of participants who are not blacklisted as keepers + relevant_set = sorted(list(non_blacklisted)) + + needed_keepers = math.ceil( + self.synchronized_data.nb_participants / 2 + ) # half or 1 + + # Check if we need random selection + if len(relevant_set) <= needed_keepers: + keeper_addresses = list(relevant_set) + self.context.logger.info(f"Selected new keepers: {keeper_addresses}.") + return keeper_addresses + + # Random seeding and shuffling of the set + random.seed(self.synchronized_data.keeper_randomness) + random.shuffle(relevant_set) + + # If the keeper is not set yet, pick the first address + keeper_addresses = relevant_set[0:2] + + # If the keepers have been already set, select the next ones. + if ( + self.synchronized_data.are_keepers_set + and len(self.synchronized_data.participants) > 2 + ): + old_keeper_index = relevant_set.index( + self.synchronized_data.most_voted_keeper_addresses[0] + ) + keeper_addresses = [ + relevant_set[ + (old_keeper_index + 2) % len(relevant_set) + ], # skip the previous 2 + relevant_set[(old_keeper_index + 3) % len(relevant_set)], + ] + + self.context.logger.info(f"Selected new keepers: {keeper_addresses}.") + + return keeper_addresses + + def async_act(self) -> Generator: + """ + Do the action. + + Steps: + - Select a keeper randomly. + - Send the transaction with the keeper and wait for it to be mined. + - Wait until ABCI application transitions to the next round. + - Go to the next behaviour state (set done event). + """ + + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + payload = TwitterSelectKeepersPayload( # type: ignore + self.context.agent_address, + json.dumps(self._select_keepers(), sort_keys=True), + ) + + 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 TwitterDecisionMakingBehaviour(TwitterScoringBaseBehaviour): """TwitterDecisionMakingBehaviour""" @@ -164,6 +268,9 @@ def get_next_event(self) -> str: if performed_tasks[Event.OPENAI_CALL_CHECK.value] == Event.NO_ALLOWANCE.value: return Event.DONE_SKIP.value + if Event.SELECT_KEEPERS.value not in performed_tasks: + return Event.SELECT_KEEPERS.value + if Event.RETRIEVE_HASHTAGS.value not in performed_tasks: return Event.RETRIEVE_HASHTAGS.value @@ -212,10 +319,41 @@ class TwitterMentionsCollectionBehaviour(TwitterScoringBaseBehaviour): matching_round: Type[AbstractRound] = TwitterMentionsCollectionRound - def async_act(self) -> Generator: + def _i_am_not_sending(self) -> bool: + """Indicates if the current agent is one of the sender or not.""" + return ( + self.context.agent_address + not in self.synchronized_data.most_voted_keeper_addresses + ) + + def async_act(self) -> Generator[None, None, None]: + """ + Do the action. + + Steps: + - If the agent is the keeper, then prepare the transaction and send it. + - Otherwise, wait until the next round. + - If a timeout is hit, set exit A event, otherwise set done event. + """ + if self._i_am_not_sending(): + yield from self._not_sender_act() + else: + yield from self._sender_act() + + def _not_sender_act(self) -> Generator: + """Do the non-sender action.""" + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + self.context.logger.info( + f"Waiting for the keeper to do its keeping: keepers={self.synchronized_data.most_voted_keeper_addresses}, me={self.context.agent_address}" + ) + yield from self.wait_until_round_end() + self.set_done() + + def _sender_act(self) -> Generator: """Do the act, supporting asynchronous execution.""" with self.context.benchmark_tool.measure(self.behaviour_id).local(): + self.context.logger.info("I am a keeper") ( has_limit_reached, @@ -407,10 +545,41 @@ class TwitterHashtagsCollectionBehaviour(TwitterScoringBaseBehaviour): matching_round: Type[AbstractRound] = TwitterHashtagsCollectionRound - def async_act(self) -> Generator: + def _i_am_not_sending(self) -> bool: + """Indicates if the current agent is one of the sender or not.""" + return ( + self.context.agent_address + not in self.synchronized_data.most_voted_keeper_addresses + ) + + def async_act(self) -> Generator[None, None, None]: + """ + Do the action. + + Steps: + - If the agent is the keeper, then prepare the transaction and send it. + - Otherwise, wait until the next round. + - If a timeout is hit, set exit A event, otherwise set done event. + """ + if self._i_am_not_sending(): + yield from self._not_sender_act() + else: + yield from self._sender_act() + + def _not_sender_act(self) -> Generator: + """Do the non-sender action.""" + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + self.context.logger.info( + f"Waiting for the keeper to do its keeping: keepers={self.synchronized_data.most_voted_keeper_addresses}, me={self.context.agent_address}" + ) + yield from self.wait_until_round_end() + self.set_done() + + def _sender_act(self) -> Generator: """Do the act, supporting asynchronous execution.""" with self.context.benchmark_tool.measure(self.behaviour_id).local(): + self.context.logger.info("I am a keeper") ( has_limit_reached, @@ -732,14 +901,6 @@ def update_ceramic_db(self) -> Dict: """Calculate the new content of the DB""" tweets = self.synchronized_data.tweets - latest_mention_tweet_id = self.synchronized_data.latest_mention_tweet_id - latest_hashtag_tweet_id = self.synchronized_data.latest_hashtag_tweet_id - number_of_tweets_pulled_today = ( - self.synchronized_data.number_of_tweets_pulled_today - ) - last_tweet_pull_window_reset = ( - self.synchronized_data.last_tweet_pull_window_reset - ) # Instantiate the db ceramic_db = CeramicDB(self.synchronized_data.ceramic_db, self.context.logger) @@ -816,21 +977,35 @@ def update_ceramic_db(self) -> Dict: ceramic_db.merge_by_wallet() # Update the latest_hashtag_tweet_id - ceramic_db.data["module_data"]["twitter"]["latest_hashtag_tweet_id"] = str( - latest_hashtag_tweet_id - ) + latest_hashtag_tweet_id = self.synchronized_data.latest_hashtag_tweet_id + if latest_hashtag_tweet_id: + ceramic_db.data["module_data"]["twitter"]["latest_hashtag_tweet_id"] = str( + latest_hashtag_tweet_id + ) + # Update the latest_mention_tweet_id - ceramic_db.data["module_data"]["twitter"]["latest_mention_tweet_id"] = str( - latest_mention_tweet_id - ) + latest_mention_tweet_id = self.synchronized_data.latest_mention_tweet_id + if latest_mention_tweet_id: + ceramic_db.data["module_data"]["twitter"]["latest_mention_tweet_id"] = str( + latest_mention_tweet_id + ) # Update the number of tweets made today - ceramic_db.data["module_data"]["twitter"][ - "number_of_tweets_pulled_today" - ] = str(number_of_tweets_pulled_today) - ceramic_db.data["module_data"]["twitter"]["last_tweet_pull_window_reset"] = str( - last_tweet_pull_window_reset + number_of_tweets_pulled_today = ( + self.synchronized_data.number_of_tweets_pulled_today + ) + if number_of_tweets_pulled_today: + ceramic_db.data["module_data"]["twitter"][ + "number_of_tweets_pulled_today" + ] = str(number_of_tweets_pulled_today) + + last_tweet_pull_window_reset = ( + self.synchronized_data.last_tweet_pull_window_reset ) + if last_tweet_pull_window_reset: + ceramic_db.data["module_data"]["twitter"][ + "last_tweet_pull_window_reset" + ] = str(last_tweet_pull_window_reset) # Update the current_period ceramic_db.data["module_data"]["twitter"]["current_period"] = today @@ -880,4 +1055,6 @@ class TwitterScoringRoundBehaviour(AbstractRoundBehaviour): TwitterHashtagsCollectionBehaviour, TweetEvaluationBehaviour, DBUpdateBehaviour, + TwitterRandomnessBehaviour, + TwitterSelectKeepersBehaviour, ] diff --git a/packages/valory/skills/twitter_scoring_abci/fsm_specification.yaml b/packages/valory/skills/twitter_scoring_abci/fsm_specification.yaml index 7bd7748a..2ff1da17 100644 --- a/packages/valory/skills/twitter_scoring_abci/fsm_specification.yaml +++ b/packages/valory/skills/twitter_scoring_abci/fsm_specification.yaml @@ -11,6 +11,7 @@ alphabet_in: - RETRIEVE_HASHTAGS - RETRIEVE_MENTIONS - ROUND_TIMEOUT +- SELECT_KEEPERS - TWEET_EVALUATION_ROUND_TIMEOUT default_start_state: TwitterDecisionMakingRound final_states: @@ -26,6 +27,8 @@ states: - TwitterDecisionMakingRound - TwitterHashtagsCollectionRound - TwitterMentionsCollectionRound +- TwitterRandomnessRound +- TwitterSelectKeepersRound transition_func: (DBUpdateRound, DONE): TwitterDecisionMakingRound (DBUpdateRound, NO_MAJORITY): DBUpdateRound @@ -45,6 +48,7 @@ transition_func: (TwitterDecisionMakingRound, RETRIEVE_HASHTAGS): TwitterHashtagsCollectionRound (TwitterDecisionMakingRound, RETRIEVE_MENTIONS): TwitterMentionsCollectionRound (TwitterDecisionMakingRound, ROUND_TIMEOUT): TwitterDecisionMakingRound + (TwitterDecisionMakingRound, SELECT_KEEPERS): TwitterRandomnessRound (TwitterHashtagsCollectionRound, API_ERROR): TwitterHashtagsCollectionRound (TwitterHashtagsCollectionRound, DONE): TwitterDecisionMakingRound (TwitterHashtagsCollectionRound, DONE_MAX_RETRIES): TwitterDecisionMakingRound @@ -55,3 +59,9 @@ transition_func: (TwitterMentionsCollectionRound, DONE_MAX_RETRIES): TwitterDecisionMakingRound (TwitterMentionsCollectionRound, NO_MAJORITY): TwitterMentionsCollectionRound (TwitterMentionsCollectionRound, ROUND_TIMEOUT): TwitterMentionsCollectionRound + (TwitterRandomnessRound, DONE): TwitterSelectKeepersRound + (TwitterRandomnessRound, NO_MAJORITY): TwitterRandomnessRound + (TwitterRandomnessRound, ROUND_TIMEOUT): TwitterRandomnessRound + (TwitterSelectKeepersRound, DONE): TwitterDecisionMakingRound + (TwitterSelectKeepersRound, NO_MAJORITY): TwitterRandomnessRound + (TwitterSelectKeepersRound, ROUND_TIMEOUT): TwitterRandomnessRound diff --git a/packages/valory/skills/twitter_scoring_abci/models.py b/packages/valory/skills/twitter_scoring_abci/models.py index 8af452dc..c10d8dcd 100644 --- a/packages/valory/skills/twitter_scoring_abci/models.py +++ b/packages/valory/skills/twitter_scoring_abci/models.py @@ -22,7 +22,7 @@ from datetime import datetime from typing import Any -from packages.valory.skills.abstract_round_abci.models import BaseParams +from packages.valory.skills.abstract_round_abci.models import ApiSpecs, BaseParams from packages.valory.skills.abstract_round_abci.models import ( BenchmarkTool as BaseBenchmarkTool, ) @@ -39,6 +39,10 @@ class SharedState(BaseSharedState): abci_app_cls = TwitterScoringAbciApp +class RandomnessApi(ApiSpecs): + """A model that wraps ApiSpecs for randomness api specifications.""" + + class OpenAICalls: """OpenAI call window.""" diff --git a/packages/valory/skills/twitter_scoring_abci/payloads.py b/packages/valory/skills/twitter_scoring_abci/payloads.py index 3b70298f..971f7c20 100644 --- a/packages/valory/skills/twitter_scoring_abci/payloads.py +++ b/packages/valory/skills/twitter_scoring_abci/payloads.py @@ -65,3 +65,18 @@ class TwitterDecisionMakingPayload(BaseTxPayload): """Represent a transaction payload for the TwitterDecisionMakingRound.""" event: str + + +@dataclass(frozen=True) +class TwitterRandomnessPayload(BaseTxPayload): + """Represent a transaction payload of type 'randomness'.""" + + round_id: int + randomness: str + + +@dataclass(frozen=True) +class TwitterSelectKeepersPayload(BaseTxPayload): + """Represent a transaction payload of type 'select_keeper'.""" + + keepers: str diff --git a/packages/valory/skills/twitter_scoring_abci/rounds.py b/packages/valory/skills/twitter_scoring_abci/rounds.py index 946c912e..5b3297a5 100644 --- a/packages/valory/skills/twitter_scoring_abci/rounds.py +++ b/packages/valory/skills/twitter_scoring_abci/rounds.py @@ -20,11 +20,13 @@ """This package contains the rounds of TwitterScoringAbciApp.""" import json +import math import statistics from enum import Enum -from typing import Dict, FrozenSet, Optional, Set, Tuple, cast +from typing import Any, Dict, FrozenSet, Optional, Set, Tuple, cast from packages.valory.skills.abstract_round_abci.base import ( + ABCIAppInternalError, AbciApp, AbciAppTransitionFunction, AppState, @@ -42,6 +44,8 @@ TwitterDecisionMakingPayload, TwitterHashtagsCollectionPayload, TwitterMentionsCollectionPayload, + TwitterRandomnessPayload, + TwitterSelectKeepersPayload, ) @@ -64,6 +68,7 @@ class Event(Enum): RETRIEVE_MENTIONS = "retrieve_mentions" EVALUATE = "evaluate" DB_UPDATE = "db_update" + SELECT_KEEPERS = "select_keepers" class SynchronizedData(BaseSynchronizedData): @@ -96,28 +101,38 @@ def tweets(self) -> dict: @property def latest_mention_tweet_id(self) -> dict: """Get the latest_mention_tweet_id.""" - return cast(dict, self.db.get_strict("latest_mention_tweet_id")) + return cast(dict, self.db.get("latest_mention_tweet_id", None)) @property def latest_hashtag_tweet_id(self) -> dict: """Get the latest_hashtag_tweet_id.""" - return cast(dict, self.db.get_strict("latest_hashtag_tweet_id")) + return cast(dict, self.db.get("latest_hashtag_tweet_id", None)) @property def number_of_tweets_pulled_today(self) -> dict: """Get the number_of_tweets_pulled_today.""" - return cast(dict, self.db.get_strict("number_of_tweets_pulled_today")) + return cast(dict, self.db.get("number_of_tweets_pulled_today", None)) @property def last_tweet_pull_window_reset(self) -> dict: """Get the last_tweet_pull_window_reset.""" - return cast(dict, self.db.get_strict("last_tweet_pull_window_reset")) + return cast(dict, self.db.get("last_tweet_pull_window_reset", None)) @property def performed_twitter_tasks(self) -> dict: """Get the twitter_tasks.""" return cast(dict, self.db.get("performed_twitter_tasks", {})) + @property + def most_voted_keeper_addresses(self) -> list: + """Get the most_voted_keeper_addresses.""" + return cast(list, self.db.get_strict("most_voted_keeper_addresses")) + + @property + def are_keepers_set(self) -> bool: + """Check whether keepers are set.""" + return self.db.get("most_voted_keeper_addresses", None) is not None + class TwitterDecisionMakingRound(CollectSameUntilThresholdRound): """TwitterDecisionMakingRound""" @@ -130,7 +145,7 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: if self.threshold_reached: event = Event(self.most_voted_payload) # Reference events to avoid tox -e check-abciapp-specs failures - # Event.DONE, Event.DB_UPDATE, Event.RETRIEVE_MENTIONS, Event.RETRIEVE_HASHTAGS, Event.OPENAI_CALL_CHECK, Event.EVALUATE, Event.DONE_SKIP + # Event.DONE, Event.DB_UPDATE, Event.RETRIEVE_MENTIONS, Event.RETRIEVE_HASHTAGS, Event.OPENAI_CALL_CHECK, Event.EVALUATE, Event.DONE_SKIP, Event.SELECT_KEEPERS return self.synchronized_data, event if not self.is_majority_possible( self.collection, self.synchronized_data.nb_participants @@ -193,6 +208,31 @@ class TwitterMentionsCollectionRound(CollectSameUntilThresholdRound): ERROR_PAYLOAD = {"error": "true"} + @property + def consensus_threshold(self): + """Consensus threshold""" + return math.ceil(self.synchronized_data.nb_participants / 2) # half or 1 + + @property + def threshold_reached( + self, + ) -> bool: + """Check if the threshold has been reached.""" + counts = self.payload_values_count.values() + return any(count >= self.consensus_threshold for count in counts) + + @property + def most_voted_payload_values( + self, + ) -> Tuple[Any, ...]: + """Get the most voted payload values.""" + most_voted_payload_values, max_votes = self.payload_values_count.most_common()[ + 0 + ] + if max_votes < self.consensus_threshold: + raise ABCIAppInternalError("not enough votes") + return most_voted_payload_values + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: """Process the end of the block.""" if self.threshold_reached: @@ -284,6 +324,31 @@ class TwitterHashtagsCollectionRound(CollectSameUntilThresholdRound): ERROR_PAYLOAD = {"error": "true"} + @property + def consensus_threshold(self): + """Consensus threshold""" + return math.ceil(self.synchronized_data.nb_participants / 2) # half or 1 + + @property + def threshold_reached( + self, + ) -> bool: + """Check if the threshold has been reached.""" + counts = self.payload_values_count.values() + return any(count >= self.consensus_threshold for count in counts) + + @property + def most_voted_payload_values( + self, + ) -> Tuple[Any, ...]: + """Get the most voted payload values.""" + most_voted_payload_values, max_votes = self.payload_values_count.most_common()[ + 0 + ] + if max_votes < self.consensus_threshold: + raise ABCIAppInternalError("not enough votes") + return most_voted_payload_values + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: """Process the end of the block.""" if self.threshold_reached: @@ -452,6 +517,54 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: return None +class TwitterRandomnessRound(CollectSameUntilThresholdRound): + """A round for generating randomness""" + + payload_class = TwitterRandomnessPayload + synchronized_data_class = SynchronizedData + done_event = Event.DONE + no_majority_event = Event.NO_MAJORITY + collection_key = get_name(SynchronizedData.participant_to_randomness) + selection_key = ( + get_name(SynchronizedData.most_voted_randomness), + get_name(SynchronizedData.most_voted_randomness), + ) + + +class TwitterSelectKeepersRound(CollectSameUntilThresholdRound): + """A round in which a keeper is selected for transaction submission""" + + payload_class = TwitterSelectKeepersPayload + synchronized_data_class = SynchronizedData + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: + """Process the end of the block.""" + if self.threshold_reached: + + performed_twitter_tasks = cast( + SynchronizedData, self.synchronized_data + ).performed_twitter_tasks + performed_twitter_tasks["select_keepers"] = Event.DONE.value + + synchronized_data = self.synchronized_data.update( + synchronized_data_class=SynchronizedData, + **{ + get_name(SynchronizedData.most_voted_keeper_addresses): json.loads( + self.most_voted_payload + ), + get_name( + SynchronizedData.performed_twitter_tasks + ): performed_twitter_tasks, + }, + ) + return synchronized_data, Event.DONE + if not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ): + return self.synchronized_data, Event.NO_MAJORITY + return None + + class FinishedTwitterScoringRound(DegenerateRound): """FinishedTwitterScoringRound""" @@ -465,6 +578,7 @@ class TwitterScoringAbciApp(AbciApp[Event]): TwitterDecisionMakingRound: { Event.OPENAI_CALL_CHECK: OpenAICallCheckRound, Event.DONE_SKIP: FinishedTwitterScoringRound, + Event.SELECT_KEEPERS: TwitterRandomnessRound, Event.RETRIEVE_HASHTAGS: TwitterHashtagsCollectionRound, Event.RETRIEVE_MENTIONS: TwitterMentionsCollectionRound, Event.EVALUATE: TweetEvaluationRound, @@ -479,6 +593,16 @@ class TwitterScoringAbciApp(AbciApp[Event]): Event.NO_MAJORITY: OpenAICallCheckRound, Event.ROUND_TIMEOUT: OpenAICallCheckRound, }, + TwitterRandomnessRound: { + Event.DONE: TwitterSelectKeepersRound, + Event.NO_MAJORITY: TwitterRandomnessRound, + Event.ROUND_TIMEOUT: TwitterRandomnessRound, + }, + TwitterSelectKeepersRound: { + Event.DONE: TwitterDecisionMakingRound, + Event.NO_MAJORITY: TwitterRandomnessRound, + Event.ROUND_TIMEOUT: TwitterRandomnessRound, + }, TwitterMentionsCollectionRound: { Event.DONE: TwitterDecisionMakingRound, Event.DONE_MAX_RETRIES: TwitterDecisionMakingRound, diff --git a/packages/valory/skills/twitter_scoring_abci/skill.yaml b/packages/valory/skills/twitter_scoring_abci/skill.yaml index 829f6a59..816137c8 100644 --- a/packages/valory/skills/twitter_scoring_abci/skill.yaml +++ b/packages/valory/skills/twitter_scoring_abci/skill.yaml @@ -8,23 +8,23 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeifudgakkjoyahuewp2o4gvqayw7nsgpyxw2ayrpgmzexurh2xomaq - behaviours.py: bafybeigpjra4vwqro2i6wfljaarj7jcxpsu6gc7rutrlmtbxvn45gsdtnu + behaviours.py: bafybeia4ygnvr3qtw7o3ory5inwir2rtesnt7kswyftfsjak3bfhrhsaki ceramic_db.py: bafybeicusdonrdq6kirgkpdqmi3a6kmeal4nctnm5ozjqf5s5se6jpitjm dialogues.py: bafybeibdqzn37hbo2cq4skww4uh2zvvsjyaxxvdhxisefbdvmjp7rh53si - fsm_specification.yaml: bafybeihpy75zlftkveim56lhsuyfoxd4o54cx3m5seephrihakvvqthy4a + fsm_specification.yaml: bafybeidsw6qckqnput57ilb5h3ywk23u3ado33fzk5scag3lptf7ldn4wa handlers.py: bafybeid3nqvcyotqj5g5hlgrz57nf7vpjysmgvsxe3p7644f4z5dcwqn6u - models.py: bafybeihqlxg6tbkpkjr5kfdcxwopykvoimczfn22q3owlzgw2elsg76cf4 - payloads.py: bafybeihzjioqknhekm7fzlhl52iklkwzuuht6w2qqk5ptutv5s2wqtoedy + models.py: bafybeiajis7l5sv7b3fofuj3ehxai5d2uy6h6p4kabhrekjwkjs77lopxe + payloads.py: bafybeibeqiwnua7uewbv5a7epebshjpueuqpcbw6s2y3u62kasdhiijs5i prompts.py: bafybeieiuqn427bgwfnzynxf3vtqfpvmqqscs5tyw4oibfofwropifotke - rounds.py: bafybeia24avvmoqf3vmsdrz2bvrdynlzrjohnyiw47acmabmvzj7qygdx4 + rounds.py: bafybeifb74pke5oymrid7xe53idcwmxfkzch2x2pcu3m7rf2l74izndgly tests/__init__.py: bafybeidwzzd4ejsyf3aryd5kmrvd63h7ajgqyrxphmfaacvpjnneacejay - tests/test_behaviours.py: bafybeid3m7duxfaholuxj6obctzv4ao3ci5zruscaqfgeahehrdqrak4ja + tests/test_behaviours.py: bafybeihp46fd44n34bqyyqixm2g3khraouofefe54eatmw2ktw7poqqllq tests/test_ceramic_db.py: bafybeif2v7btjphbqabq6qdmfeyweg765seon74acg5vrrvznivzg2prey tests/test_dialogues.py: bafybeiheyq7klonzb7rnjub2i22h7bmsnoimn2pq4j7ofikt3yovstvgt4 tests/test_handlers.py: bafybeigevirvi3saepukke2zmp334btgsdxhj55o2vawj3hqam63miirg4 tests/test_models.py: bafybeihaiirqmcsshghztl5lvqjzem6y6rdeefhtiycrhh45v7cuwnmdz4 - tests/test_payloads.py: bafybeihzltj7fvfl6n3cylfg6jqaqpiwchfimwys4732hurvkjnwmewksy - tests/test_rounds.py: bafybeiajkmlxansdovt2xojnewrgz5jkgjpktd5w5xofy4rqp7tb74il2y + tests/test_payloads.py: bafybeihg7ndxuj4lehpmgpdngpzihhmaiy7u5kaylukneavsklfwki5j6q + tests/test_rounds.py: bafybeiel2tiptr7jasfe7yr7raoj5p3s6nh6zdayhtws54nxquvjqktn5a fingerprint_ignore_patterns: [] connections: - valory/openai:0.1.0:bafybeih7w2xmaecgznuhxpnbxcxjhfla6wlmx3umytindfgk6ibbwqq2lu @@ -146,6 +146,17 @@ models: validate_timeout: 1205 use_termination: false class_name: Params + randomness_api: + args: + api_id: cloudflare + headers: {} + method: GET + parameters: {} + response_key: null + response_type: dict + retries: 5 + url: https://drand.cloudflare.com/public/latest + class_name: RandomnessApi requests: args: {} class_name: Requests diff --git a/packages/valory/skills/twitter_scoring_abci/tests/test_behaviours.py b/packages/valory/skills/twitter_scoring_abci/tests/test_behaviours.py index f7ac13f3..4161dc68 100644 --- a/packages/valory/skills/twitter_scoring_abci/tests/test_behaviours.py +++ b/packages/valory/skills/twitter_scoring_abci/tests/test_behaviours.py @@ -23,7 +23,7 @@ from dataclasses import dataclass from datetime import datetime from pathlib import Path -from typing import Any, Dict, Optional, Type +from typing import Any, Dict, Optional, Type, cast import pytest @@ -33,11 +33,15 @@ from packages.valory.protocols.llm.message import LlmMessage from packages.valory.skills.abstract_round_abci.base import AbciAppDB from packages.valory.skills.abstract_round_abci.behaviour_utils import ( + BaseBehaviour, make_degenerate_behaviour, ) from packages.valory.skills.abstract_round_abci.test_tools.base import ( FSMBehaviourBaseCase, ) +from packages.valory.skills.abstract_round_abci.test_tools.common import ( + BaseRandomnessBehaviourTest, +) from packages.valory.skills.twitter_scoring_abci.behaviours import ( DBUpdateBehaviour, OpenAICallCheckBehaviour, @@ -46,8 +50,10 @@ TwitterDecisionMakingBehaviour, TwitterHashtagsCollectionBehaviour, TwitterMentionsCollectionBehaviour, + TwitterRandomnessBehaviour, TwitterScoringBaseBehaviour, TwitterScoringRoundBehaviour, + TwitterSelectKeepersBehaviour, ) from packages.valory.skills.twitter_scoring_abci.rounds import ( Event, @@ -56,6 +62,7 @@ ) +PACKAGE_DIR = Path(__file__).parent.parent ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" TWITTER_MENTIONS_URL = "https://api.twitter.com/2/users/1450081635559428107/mentions?tweet.fields=author_id&user.fields=name&expansions=author_id&max_results={max_results}&since_id=0" @@ -371,7 +378,13 @@ class TestMentionsCollectionBehaviour(BaseBehaviourTest): ( BehaviourTestCase( "Happy path", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.DONE, ), { @@ -395,7 +408,13 @@ class TestMentionsCollectionBehaviour(BaseBehaviourTest): ( BehaviourTestCase( "Happy path, multi-page", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.DONE, ), { @@ -423,7 +442,13 @@ class TestMentionsCollectionBehaviour(BaseBehaviourTest): ( BehaviourTestCase( "Happy path, result_count=0", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.DONE, ), { @@ -453,7 +478,11 @@ class TestMentionsCollectionBehaviour(BaseBehaviourTest): "last_tweet_pull_window_reset": 1993903085, } } - } + }, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], ), event=Event.DONE, ), @@ -498,7 +527,13 @@ class TestHashtagsCollectionBehaviour(BaseBehaviourTest): ( BehaviourTestCase( "Happy path", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.DONE, ), { @@ -523,7 +558,13 @@ class TestHashtagsCollectionBehaviour(BaseBehaviourTest): ( BehaviourTestCase( "Happy path, multi-page", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.DONE, ), { @@ -551,7 +592,13 @@ class TestHashtagsCollectionBehaviour(BaseBehaviourTest): ( BehaviourTestCase( "Happy path, result_count=0", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.DONE, ), { @@ -581,7 +628,11 @@ class TestHashtagsCollectionBehaviour(BaseBehaviourTest): "last_tweet_pull_window_reset": 1993903085, } } - } + }, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], ), event=Event.DONE, ), @@ -626,7 +677,13 @@ class TestMentionsCollectionBehaviourAPIError(BaseBehaviourTest): ( BehaviourTestCase( "API error mentions: 404", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.API_ERROR, ), { @@ -643,7 +700,13 @@ class TestMentionsCollectionBehaviourAPIError(BaseBehaviourTest): ( BehaviourTestCase( "API error mentions: missing data", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.API_ERROR, ), { @@ -660,7 +723,13 @@ class TestMentionsCollectionBehaviourAPIError(BaseBehaviourTest): ( BehaviourTestCase( "API error mentions: missing meta", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.API_ERROR, ), { @@ -677,7 +746,13 @@ class TestMentionsCollectionBehaviourAPIError(BaseBehaviourTest): ( BehaviourTestCase( "API error mentions: missing includes", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.API_ERROR, ), { @@ -727,7 +802,13 @@ class TestHashtagsCollectionBehaviourAPIError(BaseBehaviourTest): ( BehaviourTestCase( "API error registrations: 404", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.API_ERROR, ), { @@ -743,7 +824,13 @@ class TestHashtagsCollectionBehaviourAPIError(BaseBehaviourTest): ( BehaviourTestCase( "API error registrations: missing data", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.API_ERROR, ), { @@ -761,7 +848,13 @@ class TestHashtagsCollectionBehaviourAPIError(BaseBehaviourTest): ( BehaviourTestCase( "API error registrations: missing meta", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.API_ERROR, ), { @@ -777,7 +870,13 @@ class TestHashtagsCollectionBehaviourAPIError(BaseBehaviourTest): ( BehaviourTestCase( "API error mentions: missing includes", - initial_data=dict(ceramic_db={}), + initial_data=dict( + ceramic_db={}, + most_voted_keeper_addresses=[ + "test_agent_address", + "test_agent_address", + ], + ), event=Event.API_ERROR, ), { @@ -1038,3 +1137,61 @@ def test_run(self, test_case: BehaviourTestCase, kwargs: Any) -> None: self.fast_forward(test_case.initial_data) self.behaviour.act_wrapper() self.complete(test_case.event) + + +class TestRandomnessBehaviour(BaseRandomnessBehaviourTest): + """Test randomness in operation.""" + + path_to_skill = PACKAGE_DIR + + randomness_behaviour_class = TwitterRandomnessBehaviour + next_behaviour_class = TwitterSelectKeepersBehaviour + done_event = Event.DONE + + +class BaseSelectKeepersBehaviourTest(BaseBehaviourTest): + """Test SelectKeepersBehaviour.""" + + select_keeper_behaviour_class: Type[BaseBehaviour] + next_behaviour_class: Type[BaseBehaviour] + + def test_select_keeper( + self, + ) -> None: + """Test select keeper agent.""" + participants = [self.skill.skill_context.agent_address, "a_1", "a_2"] + self.fast_forward_to_behaviour( + behaviour=self.behaviour, + behaviour_id=self.select_keeper_behaviour_class.auto_behaviour_id(), + synchronized_data=SynchronizedData( + AbciAppDB( + setup_data=dict( + participants=[participants], + most_voted_randomness=[ + "56cbde9e9bbcbdcaf92f183c678eaa5288581f06b1c9c7f884ce911776727688" + ], + most_voted_keeper_addresses=[["a_1", "a_2"]], + ), + ) + ), + ) + assert ( + cast( + BaseBehaviour, + cast(BaseBehaviour, self.behaviour.current_behaviour), + ).behaviour_id + == self.select_keeper_behaviour_class.auto_behaviour_id() + ) + self.behaviour.act_wrapper() + self.mock_a2a_transaction() + self._test_done_flag_set() + self.end_round(done_event=Event.DONE) + behaviour = cast(BaseBehaviour, self.behaviour.current_behaviour) + assert behaviour.behaviour_id == self.next_behaviour_class.auto_behaviour_id() + + +class TestTwitterSelectKeepersCeramicBehaviour(BaseSelectKeepersBehaviourTest): + """Test SelectKeeperBehaviour.""" + + select_keeper_behaviour_class = TwitterSelectKeepersBehaviour + next_behaviour_class = TwitterDecisionMakingBehaviour diff --git a/packages/valory/skills/twitter_scoring_abci/tests/test_payloads.py b/packages/valory/skills/twitter_scoring_abci/tests/test_payloads.py index 5918768e..55ff561f 100644 --- a/packages/valory/skills/twitter_scoring_abci/tests/test_payloads.py +++ b/packages/valory/skills/twitter_scoring_abci/tests/test_payloads.py @@ -32,6 +32,8 @@ TwitterDecisionMakingPayload, TwitterHashtagsCollectionPayload, TwitterMentionsCollectionPayload, + TwitterRandomnessPayload, + TwitterSelectKeepersPayload, ) @@ -90,3 +92,24 @@ def test_decision_making_payload() -> None: assert payload.sender == "sender" assert payload.event == "event" assert payload.from_json(payload.json) == payload + + +def test_randomness_payload() -> None: + """Tests for TwitterScoringAbciApp payloads""" + + payload = TwitterRandomnessPayload( + sender="sender", + round_id=1, + randomness="randomness", + ) + assert payload.sender == "sender" + assert payload.round_id == 1 + assert payload.randomness == "randomness" + + +def test_select_keeper() -> None: + """Tests for payloads""" + + payload = TwitterSelectKeepersPayload(sender="sender", keepers="keepers") + assert payload.sender == "sender" + assert payload.keepers == "keepers" diff --git a/packages/valory/skills/twitter_scoring_abci/tests/test_rounds.py b/packages/valory/skills/twitter_scoring_abci/tests/test_rounds.py index 569fbaae..07c47cb0 100644 --- a/packages/valory/skills/twitter_scoring_abci/tests/test_rounds.py +++ b/packages/valory/skills/twitter_scoring_abci/tests/test_rounds.py @@ -32,10 +32,15 @@ Optional, cast, ) +from unittest import mock import pytest -from packages.valory.skills.abstract_round_abci.base import BaseTxPayload +from packages.valory.skills.abstract_round_abci.base import ( + AbciAppDB, + AbstractRound, + BaseTxPayload, +) from packages.valory.skills.abstract_round_abci.test_tools.rounds import ( BaseCollectNonEmptyUntilThresholdRound, BaseCollectSameUntilThresholdRoundTest, @@ -49,6 +54,8 @@ TwitterDecisionMakingPayload, TwitterHashtagsCollectionPayload, TwitterMentionsCollectionPayload, + TwitterRandomnessPayload, + TwitterSelectKeepersPayload, ) from packages.valory.skills.twitter_scoring_abci.rounds import ( DBUpdateRound, @@ -59,6 +66,8 @@ TwitterDecisionMakingRound, TwitterHashtagsCollectionRound, TwitterMentionsCollectionRound, + TwitterRandomnessRound, + TwitterSelectKeepersRound, ) @@ -449,3 +458,142 @@ def run_test(self, test_case: RoundTestCase) -> None: def test_run(self, test_case: RoundTestCase) -> None: """Run tests.""" self.run_test(test_case) + + +# ----------------------------------------------------------------------- +# Randomness and select keeper tests are ported from Hello World abci app +# ----------------------------------------------------------------------- + +RANDOMNESS: str = "d1c29dce46f979f9748210d24bce4eae8be91272f5ca1a6aea2832d3dd676f51" + + +def get_participant_list() -> List[str]: + """Participants""" + return [f"agent_{i}" for i in range(MAX_PARTICIPANTS)] + + +class BaseRoundTestClass: + """Base test class for Rounds.""" + + synchronized_data: SynchronizedData + participants: List[str] + + @classmethod + def setup( + cls, + ) -> None: + """Setup the test class.""" + + cls.participants = get_participant_list() + cls.synchronized_data = SynchronizedData( + AbciAppDB( + setup_data=dict( + participants=[cls.participants], + all_participants=[cls.participants], + consensus_threshold=[3], + ), + ) + ) + + def _test_no_majority_event(self, round_obj: AbstractRound) -> None: + """Test the NO_MAJORITY event.""" + with mock.patch.object(round_obj, "is_majority_possible", return_value=False): + result = round_obj.end_block() + assert result is not None + synchronized_data, event = result + assert event == Event.NO_MAJORITY + + +class TestCollectRandomnessRound(BaseRoundTestClass): + """Tests for CollectRandomnessRound.""" + + def test_run( + self, + ) -> None: + """Run tests.""" + + test_round = TwitterRandomnessRound( + synchronized_data=self.synchronized_data, + ) + first_payload, *payloads = [ + TwitterRandomnessPayload( + sender=participant, randomness=RANDOMNESS, round_id=0 + ) + for participant in self.participants + ] + + test_round.process_payload(first_payload) + assert test_round.collection[first_payload.sender] == first_payload + assert test_round.end_block() is None + + self._test_no_majority_event(test_round) + + for payload in payloads: + test_round.process_payload(payload) + + actual_next_behaviour = self.synchronized_data.update( + participant_to_randomness=test_round.serialized_collection, + most_voted_randomness=test_round.most_voted_payload, + ) + + res = test_round.end_block() + assert res is not None + synchronized_data, event = res + assert all( + [ + key + in cast(SynchronizedData, synchronized_data).participant_to_randomness + for key in cast( + SynchronizedData, actual_next_behaviour + ).participant_to_randomness + ] + ) + assert event == Event.DONE + + +class TestSelectKeeperRound(BaseRoundTestClass): + """Tests for SelectKeeperRound.""" + + def test_run( + self, + ) -> None: + """Run tests.""" + + test_round = TwitterSelectKeepersRound( + synchronized_data=self.synchronized_data, + ) + + first_payload, *payloads = [ + TwitterSelectKeepersPayload( + sender=participant, keepers='["keeper","keeper"]' + ) + for participant in self.participants + ] + + test_round.process_payload(first_payload) + assert test_round.collection[first_payload.sender] == first_payload + assert test_round.end_block() is None + + self._test_no_majority_event(test_round) + + for payload in payloads: + test_round.process_payload(payload) + + actual_next_behaviour = self.synchronized_data.update( + participant_to_selection=test_round.serialized_collection, + most_voted_keeper_address=test_round.most_voted_payload, + ) + + res = test_round.end_block() + assert res is not None + synchronized_data, event = res + assert all( + [ + key + in cast(SynchronizedData, synchronized_data).participant_to_selection + for key in cast( + SynchronizedData, actual_next_behaviour + ).participant_to_selection + ] + ) + assert event == Event.DONE