diff --git a/.gitignore b/.gitignore index 30b491c6..b73bfe8c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ impact_evaluator temp/ .1env + +impact_evaluator_local/ diff --git a/.gitleaksignore b/.gitleaksignore index 136faefe..cbc97235 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -29,3 +29,4 @@ e271cd3303d407521c714253870de451f8617150:packages/valory/skills/decision_making_ d47f7de20573d541505169665c2ae779ea57a4b0:packages/valory/services/impact_evaluator/service.yaml:generic-api-key:245 f91f017c41d644b87699fb06619d5bdb3b568838:packages/valory/services/impact_evaluator_local/service.yaml:generic-api-key:72 32259d88007014423881fa2695c363b84c1eb667:packages/valory/skills/mech_interact_abci/skill.yaml:generic-api-key:80 +549de14725c5e9059ce72242aa00451714ff2296:packages/valory/skills/olas_week_abci/skill.yaml:generic-api-key:88 diff --git a/Makefile b/Makefile index cca29b74..cdd3b9aa 100644 --- a/Makefile +++ b/Makefile @@ -90,6 +90,7 @@ test: -rfE packages/valory/skills/decision_making_abci \ -rfE packages/valory/skills/llm_abci \ -rfE packages/valory/skills/twitter_write_abci \ + -rfE packages/valory/skills/olas_week_abci \ --cov=packages.valory.skills.ceramic_read_abci \ --cov=packages.valory.skills.generic_scoring_abci \ --cov=packages.valory.skills.twitter_scoring_abci \ @@ -99,6 +100,7 @@ test: --cov=packages.valory.skills.decision_making_abci \ --cov=packages.valory.skills.llm_abci \ --cov=packages.valory.skills.twitter_write_abci \ + --cov=packages.valory.skills.olas_week_abci \ --cov-report=xml --cov-report=term --cov-report=term-missing --cov-config=.coveragerc find . -name ".coverage*" -not -name ".coveragerc" -exec rm -fr "{}" \; @@ -135,6 +137,7 @@ fix-abci-app-specs: autonomy analyse fsm-specs --update --app-class LLMAbciApp --package packages/valory/skills/llm_abci/ || (echo "Failed to check llm_abci abci consistency" && exit 1) autonomy analyse fsm-specs --update --app-class TwitterWriteAbciApp --package packages/valory/skills/twitter_write_abci/ || (echo "Failed to check twitter_write_abci abci consistency" && exit 1) autonomy analyse fsm-specs --update --app-class ImpactEvaluatorSkillAbciApp --package packages/valory/skills/impact_evaluator_abci/ || (echo "Failed to check impact_evaluator_abci abci consistency" && exit 1) + autonomy analyse fsm-specs --update --app-class WeekInOlasAbciApp --package packages/valory/skills/olas_week_abci/ || (echo "Failed to check olas_week_abci abci consistency" && exit 1) .PHONY: all-linters diff --git a/docs/index.md b/docs/index.md index f6d64be9..d440bc96 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:bafybeiechrafvles27zw5melefdom6up6m2xsuaxzwy4zl3z47narm5biq --service + autonomy fetch valory/impact_evaluator:0.1.0:bafybeiaapq7a5g464bya2ic7rkidai23whm7jn4mie54nrc6kc2jpb7bf4 --service ``` 3. Build the Docker image of the service agents diff --git a/packages/packages.json b/packages/packages.json index edbd6bee..cda71c0d 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -1,13 +1,13 @@ { "dev": { - "agent/valory/impact_evaluator/0.1.0": "bafybeicrf6rtgehmch3frbypmtoyezgk55wbjd2tmep4se55kk24takkwa", - "contract/valory/dynamic_contribution/0.1.0": "bafybeihjnddpy24wpqtsrf57qdxvku7l5w6sm36u6hkm7i2um7ln664ice", - "service/valory/impact_evaluator/0.1.0": "bafybeiechrafvles27zw5melefdom6up6m2xsuaxzwy4zl3z47narm5biq", - "skill/valory/dynamic_nft_abci/0.1.0": "bafybeihe7rav55tlymizd5ltiwwvqzqrfqswm5sxsaw6bioal35lecwhny", - "skill/valory/twitter_scoring_abci/0.1.0": "bafybeidccv4tsye4i45f443p7dkidfpw3ifu4jzi4gk4jiz2cscusondhy", + "agent/valory/impact_evaluator/0.1.0": "bafybeihirytmmua4677vv5avrzias2vd47hvziogaq2mfucg5lmlqctz24", + "contract/valory/dynamic_contribution/0.1.0": "bafybeiht364qavknmnuz7pwkw4zjzrkkugyzymgxrjvtasfcy7eya3nbxa", + "service/valory/impact_evaluator/0.1.0": "bafybeiaapq7a5g464bya2ic7rkidai23whm7jn4mie54nrc6kc2jpb7bf4", + "skill/valory/dynamic_nft_abci/0.1.0": "bafybeiexenlxvf32jjlxrow35qlo4xtmi2ezznulczhayux2n2mx27ya6i", + "skill/valory/twitter_scoring_abci/0.1.0": "bafybeicngrngwq4fna57rj4lo4nsvt73t3x3d33jb52po5e6kc7stbkl3q", "skill/valory/ceramic_read_abci/0.1.0": "bafybeide5tbobzkwfdi7yzw6bsau36h43hdlgbnk2bfqvvhmq3hl3ucjmm", "skill/valory/ceramic_write_abci/0.1.0": "bafybeihnalj6zhl5pbf7x2lkznuhivwj45jzrafbtxatwebrpb7st2v6oa", - "skill/valory/impact_evaluator_abci/0.1.0": "bafybeidro7arpqvceouk6qetxidtfgfrpc7m5zd5ojkufk6aaybznwscfi", + "skill/valory/impact_evaluator_abci/0.1.0": "bafybeiexzqy6nrkfw2b6fbhqk5b4yipvj246lxzhipndeuub2zrlytwtgi", "skill/valory/generic_scoring_abci/0.1.0": "bafybeig4hoc5gjiyuwh2ye3mwneegq3s3cghl5j7yokvngacvn7ixwdoga", "protocol/valory/twitter/0.1.0": "bafybeib4eyf7qbs7kdntqzhwqsaaj4o2mzcokcztaza6qgwt7sbxgkqu2m", "protocol/valory/llm/1.0.0": "bafybeigqybmg75vsxexmp57hkms7lkp7iwpf54r7wpygizxryvrhfqqpb4", @@ -15,8 +15,9 @@ "connection/valory/openai/0.1.0": "bafybeibn42k4zebenmjsumkngfxllxbofkwdno2fm65o6zf5y2uihmo2dq", "skill/valory/twitter_write_abci/0.1.0": "bafybeidjumloxy2yvoyymgzuwbua3ks2p66fyybna2qhrz2upzkxzjrznq", "skill/valory/llm_abci/0.1.0": "bafybeifesvwe5ya7imxefijpawgtd7aje5hhr4oh2bzgfsm2ucjp3lxije", - "skill/valory/decision_making_abci/0.1.0": "bafybeigx57k6zwiatveoiyczbhz6xbdtfhhbxg7442tjqyb7ljjfjusn5e", - "service/valory/impact_evaluator_local/0.1.0": "bafybeiespcdcu22mpeyzjeofat6rrly2jfrrcr4d62t7bs3hflem3oy4fi" + "skill/valory/decision_making_abci/0.1.0": "bafybeicjrztkcvqjnk6odukj3j2dr6ee4j2jaaw5nsga6byjhjyuxhng5m", + "service/valory/impact_evaluator_local/0.1.0": "bafybeih35wij4e32htxntga7svxddkgrrmcq36jae3xrmf4s6yuuovvcme", + "skill/valory/olas_week_abci/0.1.0": "bafybeidegg3yi5qww6q6r24pgjmmbkgconyfnecuohacljvferqqg3tc2e" }, "third_party": { "protocol/open_aea/signing/1.0.0": "bafybeifuxs7gdg2okbn7uofymenjlmnih2wxwkym44lsgwmklgwuckxm2m", diff --git a/packages/valory/agents/impact_evaluator/aea-config.yaml b/packages/valory/agents/impact_evaluator/aea-config.yaml index fd4c2226..9884d1a3 100644 --- a/packages/valory/agents/impact_evaluator/aea-config.yaml +++ b/packages/valory/agents/impact_evaluator/aea-config.yaml @@ -23,7 +23,7 @@ connections: - valory/openai:0.1.0:bafybeibn42k4zebenmjsumkngfxllxbofkwdno2fm65o6zf5y2uihmo2dq - valory/twitter:0.1.0:bafybeifm7tpjdd2y5dy5xzuxt3tebyqxmgn56agkqv64nloggqo5cr3sku contracts: -- valory/dynamic_contribution:0.1.0:bafybeihjnddpy24wpqtsrf57qdxvku7l5w6sm36u6hkm7i2um7ln664ice +- valory/dynamic_contribution:0.1.0:bafybeiht364qavknmnuz7pwkw4zjzrkkugyzymgxrjvtasfcy7eya3nbxa - valory/gnosis_safe:0.1.0:bafybeih6d3vxz3jlgodxm5b2qcwsmansqj4xobuyd6hjnhzremuvd65yrm - valory/gnosis_safe_proxy_factory:0.1.0:bafybeid6glyjikjxmefwmhn62cxiofophegjmg2z5vqqsvk6tmyunwc274 - valory/multisend:0.1.0:bafybeieg4tywd5lww2vygvpkilg3hcepa4rmhehjuamyvdf6vazt554v6u @@ -42,19 +42,20 @@ protocols: skills: - valory/abstract_abci:0.1.0:bafybeigafjci7m7ezwzasav5xqo7v2mbxxn7qb4y7vnuc2wr2irzvn7wsy - valory/abstract_round_abci:0.1.0:bafybeih2fyfb6kkf7r45pvdk7pyyebr5xloia4xiqxtb3qsrasnstqmepq -- valory/impact_evaluator_abci:0.1.0:bafybeidro7arpqvceouk6qetxidtfgfrpc7m5zd5ojkufk6aaybznwscfi +- valory/impact_evaluator_abci:0.1.0:bafybeiexzqy6nrkfw2b6fbhqk5b4yipvj246lxzhipndeuub2zrlytwtgi - valory/generic_scoring_abci:0.1.0:bafybeig4hoc5gjiyuwh2ye3mwneegq3s3cghl5j7yokvngacvn7ixwdoga -- valory/twitter_scoring_abci:0.1.0:bafybeidccv4tsye4i45f443p7dkidfpw3ifu4jzi4gk4jiz2cscusondhy +- valory/twitter_scoring_abci:0.1.0:bafybeicngrngwq4fna57rj4lo4nsvt73t3x3d33jb52po5e6kc7stbkl3q - valory/ceramic_read_abci:0.1.0:bafybeide5tbobzkwfdi7yzw6bsau36h43hdlgbnk2bfqvvhmq3hl3ucjmm - valory/ceramic_write_abci:0.1.0:bafybeihnalj6zhl5pbf7x2lkznuhivwj45jzrafbtxatwebrpb7st2v6oa -- valory/dynamic_nft_abci:0.1.0:bafybeihe7rav55tlymizd5ltiwwvqzqrfqswm5sxsaw6bioal35lecwhny +- valory/dynamic_nft_abci:0.1.0:bafybeiexenlxvf32jjlxrow35qlo4xtmi2ezznulczhayux2n2mx27ya6i - valory/registration_abci:0.1.0:bafybeibndt6vxiwc2edgtaxpjbhwto3eyu33ujz3zmvtygvo2qz7xme5li - valory/reset_pause_abci:0.1.0:bafybeigeoc363gv3wp2rrmk6p2fdxney33nxd3owtpfugzapgruwe4klyu - valory/termination_abci:0.1.0:bafybeigqpij2sgrpnilqjljfciixop4fldq5qceixc7534q6af4potdmdm - valory/transaction_settlement_abci:0.1.0:bafybeia7rzsbea3ch4gcafyp3z6uvqh4npws2xpdwbkkdbrqqpjops7nui - valory/twitter_write_abci:0.1.0:bafybeidjumloxy2yvoyymgzuwbua3ks2p66fyybna2qhrz2upzkxzjrznq -- valory/decision_making_abci:0.1.0:bafybeigx57k6zwiatveoiyczbhz6xbdtfhhbxg7442tjqyb7ljjfjusn5e +- valory/decision_making_abci:0.1.0:bafybeicjrztkcvqjnk6odukj3j2dr6ee4j2jaaw5nsga6byjhjyuxhng5m - valory/llm_abci:0.1.0:bafybeifesvwe5ya7imxefijpawgtd7aje5hhr4oh2bzgfsm2ucjp3lxije +- valory/olas_week_abci:0.1.0:bafybeidegg3yi5qww6q6r24pgjmmbkgconyfnecuohacljvferqqg3tc2e default_ledger: ethereum required_ledgers: - ethereum @@ -213,6 +214,8 @@ models: max_tweet_pulls_allowed: ${int:80} twitter_search_endpoint: ${str:2/tweets/search/recent?} twitter_search_args: ${str:query=%23olas&tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results={max_results}&since_id={since_id}} + twitter_tweets_endpoint: ${str:2/users/1450081635559428107/tweets?} + twitter_tweets_args: ${str:tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results=50&start_time={start_time}} openai_call_window_size: ${float:3600.0} openai_calls_allowed_in_window: ${int:100} tx_timeout: 10.0 diff --git a/packages/valory/contracts/dynamic_contribution/contract.py b/packages/valory/contracts/dynamic_contribution/contract.py index c4f8c4d7..5e552a35 100644 --- a/packages/valory/contracts/dynamic_contribution/contract.py +++ b/packages/valory/contracts/dynamic_contribution/contract.py @@ -111,7 +111,7 @@ def get_all_erc721_transfers( # the connection could time out. MAX_BLOCKS = 300000 to_block = ( - ledger_api.api.eth.get_block_number() if to_block == "latest" else to_block + ledger_api.api.eth.get_block_number() - 1 if to_block == "latest" else to_block ) ranges = list(range(from_block, to_block, MAX_BLOCKS)) + [to_block] diff --git a/packages/valory/contracts/dynamic_contribution/contract.yaml b/packages/valory/contracts/dynamic_contribution/contract.yaml index 7417ac80..6cf90ecd 100644 --- a/packages/valory/contracts/dynamic_contribution/contract.yaml +++ b/packages/valory/contracts/dynamic_contribution/contract.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeidk77j5zmvjhf42ie7grv33hzjedgc6vdiwn22uzhlwuo6xh6hmsi build/DynamicContribution.json: bafybeicq5ee4hba3h6tsluzvdrtyvzllpqsseqys66e24bfrpjlegnxome - contract.py: bafybeidiroajv6euw4l2w4qyjgsqvwrktuzoaywjhx5wcqavdrmhvspeue + contract.py: bafybeihe45m6zeuymlanetr6purk4bx6vcxh5efjvn3rteovhexkksdpqy fingerprint_ignore_patterns: [] class_name: DynamicContributionContract contract_interface_paths: diff --git a/packages/valory/services/impact_evaluator/service.yaml b/packages/valory/services/impact_evaluator/service.yaml index bdcd9075..fdf02a8e 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:bafybeicrf6rtgehmch3frbypmtoyezgk55wbjd2tmep4se55kk24takkwa +agent: valory/impact_evaluator:0.1.0:bafybeihirytmmua4677vv5avrzias2vd47hvziogaq2mfucg5lmlqctz24 number_of_agents: 4 deployment: agent: @@ -107,6 +107,8 @@ extra: twitter_max_pages: ${TWITTER_MAX_PAGES:int:1} twitter_search_endpoint: ${TWITTER_SEARCH_ENDPOINT:str:2/tweets/search/recent?} twitter_search_args: ${TWITTER_SEARCH_ARGS:str:query=%23OlasNetwork&tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results=25&since_id={since_id}} + twitter_tweets_endpoint: ${TWITTER_TWEETS_ENDPOINT:str:2/users/1450081635559428107/tweets?} + twitter_tweets_args: ${TWITTER_TWEETS_ARGS:str:tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results=50&start_time={start_time}} tx_timeout: 10.0 use_termination: ${USE_TERMINATION:bool:false} validate_timeout: 1205 @@ -179,6 +181,8 @@ extra: twitter_max_pages: ${TWITTER_MAX_PAGES:int:1} twitter_search_endpoint: ${TWITTER_SEARCH_ENDPOINT:str:2/tweets/search/recent?} twitter_search_args: ${TWITTER_SEARCH_ARGS:str:query=%23OlasNetwork&tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results=25&since_id={since_id}} + twitter_tweets_endpoint: ${TWITTER_TWEETS_ENDPOINT:str:2/users/1450081635559428107/tweets?} + twitter_tweets_args: ${TWITTER_TWEETS_ARGS:str:tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results=50&start_time={start_time}} tx_timeout: 10.0 use_termination: ${USE_TERMINATION:bool:false} validate_timeout: 1205 @@ -251,6 +255,8 @@ extra: twitter_max_pages: ${TWITTER_MAX_PAGES:int:1} twitter_search_endpoint: ${TWITTER_SEARCH_ENDPOINT:str:2/tweets/search/recent?} twitter_search_args: ${TWITTER_SEARCH_ARGS:str:query=%23OlasNetwork&tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results=25&since_id={since_id}} + twitter_tweets_endpoint: ${TWITTER_TWEETS_ENDPOINT:str:2/users/1450081635559428107/tweets?} + twitter_tweets_args: ${TWITTER_TWEETS_ARGS:str:tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results=50&start_time={start_time}} tx_timeout: 10.0 use_termination: ${USE_TERMINATION:bool:false} validate_timeout: 1205 @@ -325,6 +331,8 @@ extra: twitter_max_pages: ${TWITTER_MAX_PAGES:int:1} twitter_search_endpoint: ${TWITTER_SEARCH_ENDPOINT:str:2/tweets/search/recent?} twitter_search_args: ${TWITTER_SEARCH_ARGS:str:query=%23OlasNetwork&tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results=25&since_id={since_id}} + twitter_tweets_endpoint: ${TWITTER_TWEETS_ENDPOINT:str:2/users/1450081635559428107/tweets?} + twitter_tweets_args: ${TWITTER_TWEETS_ARGS:str:tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results=50&start_time={start_time}} tx_timeout: 10.0 use_termination: ${USE_TERMINATION:bool:false} validate_timeout: 1205 diff --git a/packages/valory/services/impact_evaluator_local/service.yaml b/packages/valory/services/impact_evaluator_local/service.yaml index c4dc86a3..581892b4 100644 --- a/packages/valory/services/impact_evaluator_local/service.yaml +++ b/packages/valory/services/impact_evaluator_local/service.yaml @@ -8,7 +8,7 @@ license: Apache-2.0 fingerprint: README.md: bafybeign56hilwuoa6bgos3uqabss4gew4vadkik7vhj3ucpqw6nxtqtpe fingerprint_ignore_patterns: [] -agent: valory/impact_evaluator:0.1.0:bafybeicrf6rtgehmch3frbypmtoyezgk55wbjd2tmep4se55kk24takkwa +agent: valory/impact_evaluator:0.1.0:bafybeihirytmmua4677vv5avrzias2vd47hvziogaq2mfucg5lmlqctz24 number_of_agents: 1 deployment: agent: @@ -106,6 +106,8 @@ models: twitter_max_pages: ${TWITTER_MAX_PAGES:int:1} twitter_search_endpoint: ${TWITTER_SEARCH_ENDPOINT:str:2/tweets/search/recent?} twitter_search_args: ${TWITTER_SEARCH_ARGS:str:query=%23OlasNetwork&tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results=25&since_id={since_id}} + twitter_tweets_endpoint: ${TWITTER_TWEETS_ENDPOINT:str:2/users/1450081635559428107/tweets?} + twitter_tweets_args: ${TWITTER_TWEETS_ARGS:str:tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results=50&start_time={start_time}} tx_timeout: 10.0 use_termination: ${USE_TERMINATION:bool:false} validate_timeout: 1205 diff --git a/packages/valory/skills/decision_making_abci/behaviours.py b/packages/valory/skills/decision_making_abci/behaviours.py index bddd0ef1..16b4eb98 100644 --- a/packages/valory/skills/decision_making_abci/behaviours.py +++ b/packages/valory/skills/decision_making_abci/behaviours.py @@ -55,6 +55,9 @@ DailyTweetPreparation, ScheduledTweetPreparation, ) +from packages.valory.skills.decision_making_abci.tasks.week_in_olas_preparations import ( + WeekInOlasCreatePreparation, +) from packages.valory.skills.decision_making_abci.tasks.write_stream_preparation import ( DailyOrbisPreparation, UpdateCentaursPreparation, @@ -66,6 +69,14 @@ previous_event_to_task_preparation_cls = { None: { "prev": None, + "next": ReadContributeDBPreparation, + }, + Event.READ_CONTRIBUTE_DB.value: { + "prev": ReadContributeDBPreparation, + "next": ReadManualPointsPreparation, + }, + Event.READ_MANUAL_POINTS.value: { + "prev": ReadManualPointsPreparation, "next": ReadCentaursPreparation, }, Event.READ_CENTAURS.value: { @@ -82,6 +93,10 @@ }, Event.DAILY_ORBIS.value: { "prev": DailyOrbisPreparation, + "next": WeekInOlasCreatePreparation, + }, + Event.WEEK_IN_OLAS_CREATE.value: { + "prev": WeekInOlasCreatePreparation, "next": ScheduledTweetPreparation, }, Event.SCHEDULED_TWEET.value: { @@ -90,14 +105,6 @@ }, Event.UPDATE_CENTAURS.value: { "prev": UpdateCentaursPreparation, - "next": ReadContributeDBPreparation, - }, - Event.READ_CONTRIBUTE_DB.value: { - "prev": ReadContributeDBPreparation, - "next": ReadManualPointsPreparation, - }, - Event.READ_MANUAL_POINTS.value: { - "prev": ReadManualPointsPreparation, "next": ScorePreparation, }, Event.SCORE.value: { @@ -110,7 +117,7 @@ }, Event.NEXT_CENTAUR.value: { "prev": None, - "next": LLMPreparation, + "next": ReadContributeDBPreparation, }, } diff --git a/packages/valory/skills/decision_making_abci/fsm_specification.yaml b/packages/valory/skills/decision_making_abci/fsm_specification.yaml index 58943ce0..ea8616ce 100644 --- a/packages/valory/skills/decision_making_abci/fsm_specification.yaml +++ b/packages/valory/skills/decision_making_abci/fsm_specification.yaml @@ -12,6 +12,7 @@ alphabet_in: - SCHEDULED_TWEET - SCORE - UPDATE_CENTAURS +- WEEK_IN_OLAS_CREATE - WRITE_CONTRIBUTE_DB default_start_state: DecisionMakingRound final_states: @@ -22,6 +23,7 @@ final_states: - FinishedDecisionMakingReadManualPointsRound - FinishedDecisionMakingScoreRound - FinishedDecisionMakingUpdateCentaurRound +- FinishedDecisionMakingWeekInOlasRound - FinishedDecisionMakingWriteContributeDBRound - FinishedDecisionMakingWriteOrbisRound - FinishedDecisionMakingWriteTwitterRound @@ -37,6 +39,7 @@ states: - FinishedDecisionMakingReadManualPointsRound - FinishedDecisionMakingScoreRound - FinishedDecisionMakingUpdateCentaurRound +- FinishedDecisionMakingWeekInOlasRound - FinishedDecisionMakingWriteContributeDBRound - FinishedDecisionMakingWriteOrbisRound - FinishedDecisionMakingWriteTwitterRound @@ -54,4 +57,5 @@ transition_func: (DecisionMakingRound, SCHEDULED_TWEET): FinishedDecisionMakingWriteTwitterRound (DecisionMakingRound, SCORE): FinishedDecisionMakingScoreRound (DecisionMakingRound, UPDATE_CENTAURS): FinishedDecisionMakingUpdateCentaurRound + (DecisionMakingRound, WEEK_IN_OLAS_CREATE): FinishedDecisionMakingWeekInOlasRound (DecisionMakingRound, WRITE_CONTRIBUTE_DB): FinishedDecisionMakingWriteContributeDBRound diff --git a/packages/valory/skills/decision_making_abci/rounds.py b/packages/valory/skills/decision_making_abci/rounds.py index c11bbbb5..ed8dab1f 100644 --- a/packages/valory/skills/decision_making_abci/rounds.py +++ b/packages/valory/skills/decision_making_abci/rounds.py @@ -56,6 +56,7 @@ class Event(Enum): READ_CONTRIBUTE_DB = "read_contribute_db" READ_MANUAL_POINTS = "read_manual_points" WRITE_CONTRIBUTE_DB = "write_contribute_db" + WEEK_IN_OLAS_CREATE = "week_in_olas_create" class SynchronizedData(BaseSynchronizedData): @@ -145,6 +146,11 @@ def pending_write(self) -> bool: """Checks whether there are changes pending to be written to Ceramic.""" return cast(bool, self.db.get("pending_write", False)) + @property + def summary_tweets(self) -> list: + """Get the summary_tweets.""" + return cast(list, self.db.get("summary_tweets", [])) + class DecisionMakingRound(CollectSameUntilThresholdRound): """DecisionMakingRound""" @@ -161,6 +167,7 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: # Event.NO_MAJORITY, Event.DONE, Event.UPDATE_CENTAURS, Event.READ_CENTAURS, # Event.SCHEDULED_TWEET, Event.LLM, Event.DAILY_ORBIS, Event.DAILY_TWEET, Event.NEXT_CENTAUR # Event.SCORE, Event.READ_CONTRIBUTE_DB, Event.READ_MANUAL_POINTS, Event.WRITE_CONTRIBUTE_DB + # Event.WEEK_IN_OLAS_CREATE payload = json.loads(self.most_voted_payload) event = Event(payload["event"]) @@ -230,6 +237,10 @@ class FinishedDecisionMakingDoneRound(DegenerateRound): """FinishedDecisionMakingDoneRound""" +class FinishedDecisionMakingWeekInOlasRound(DegenerateRound): + """FinishedDecisionMakingWeekInOlasRound""" + + class DecisionMakingAbciApp(AbciApp[Event]): """DecisionMakingAbciApp""" @@ -241,6 +252,7 @@ class DecisionMakingAbciApp(AbciApp[Event]): Event.LLM: FinishedDecisionMakingLLMRound, Event.DAILY_TWEET: FinishedDecisionMakingWriteTwitterRound, Event.SCHEDULED_TWEET: FinishedDecisionMakingWriteTwitterRound, + Event.WEEK_IN_OLAS_CREATE: FinishedDecisionMakingWeekInOlasRound, Event.DAILY_ORBIS: FinishedDecisionMakingWriteOrbisRound, Event.UPDATE_CENTAURS: FinishedDecisionMakingUpdateCentaurRound, Event.SCORE: FinishedDecisionMakingScoreRound, @@ -262,6 +274,7 @@ class DecisionMakingAbciApp(AbciApp[Event]): FinishedDecisionMakingReadContributeDBRound: {}, FinishedDecisionMakingWriteContributeDBRound: {}, FinishedDecisionMakingReadManualPointsRound: {}, + FinishedDecisionMakingWeekInOlasRound: {}, } final_states: Set[AppState] = { FinishedDecisionMakingReadCentaursRound, @@ -274,6 +287,7 @@ class DecisionMakingAbciApp(AbciApp[Event]): FinishedDecisionMakingReadContributeDBRound, FinishedDecisionMakingWriteContributeDBRound, FinishedDecisionMakingReadManualPointsRound, + FinishedDecisionMakingWeekInOlasRound, } event_to_timeout: EventToTimeout = { Event.ROUND_TIMEOUT: 30.0, @@ -293,4 +307,5 @@ class DecisionMakingAbciApp(AbciApp[Event]): FinishedDecisionMakingReadContributeDBRound: set(), FinishedDecisionMakingWriteContributeDBRound: set(), FinishedDecisionMakingReadManualPointsRound: set(), + FinishedDecisionMakingWeekInOlasRound: set(), } diff --git a/packages/valory/skills/decision_making_abci/skill.yaml b/packages/valory/skills/decision_making_abci/skill.yaml index 579313e8..8eb81cd6 100644 --- a/packages/valory/skills/decision_making_abci/skill.yaml +++ b/packages/valory/skills/decision_making_abci/skill.yaml @@ -7,19 +7,20 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeicq2o3mkcgbmi4hkmpr3c6fieemutea2gxthymxnbhpemrzlcp2la - behaviours.py: bafybeiecgeionwiwb7xmrxyuypmkltpz7kqnl4dnpfind5niocdrcbytca + behaviours.py: bafybeidlfdovcj37dowftf6xswhsf2rawx7issrkwgx2xz4mseo66zhtwu dialogues.py: bafybeic5nwnax5tz6plxi5rww6davp23vgb6fixnlhzv2isoknuvb2bhye - fsm_specification.yaml: bafybeib3knchqkllaz5n5f5mkprr5g6uz5seiulodrowcr4zsfo3fujm2m + fsm_specification.yaml: bafybeiaz2ejzn654bckao54i3dicbm3ehdpq4obc6lfo4hvngjxteld7ye handlers.py: bafybeibo54pwzdc5o4zwsqqaveiebxsfqtk2px5nh662bb5mxydtc2oz5q models.py: bafybeictln7abrfqkm42euj2ldb2aqh3j3vuz45gltlepldzucgqgjy3va payloads.py: bafybeif5bomwy74bwnphsk6bty6qb6efugvjnnopsz7qjunpe6gtgjaxya - rounds.py: bafybeiaubhslddudepusvgxmwsy3z4hhzt74gosxgxwk35lymqxyklxgne + rounds.py: bafybeiaj7j5ncphpsetljcl72yzpbzt6hwd663aoydsgh45rhtkzgqwqou tasks/finished_pipeline_preparation.py: bafybeifvwzvnw3d2lxlk4p5hd6j2tcxr7bwjewm667ubssoqyfsk4xafd4 tasks/llm_preparation.py: bafybeigi7i5yp7w5hi5mcbzuzdfbfm4j5etsvplbhb2pdjcb3jfcy6qzrq tasks/read_stream_preparation.py: bafybeiespz6kzkzvrjjf3d6ffwse3rufuyrrab3axvg64pxw4jj6zzhwny tasks/score_preparations.py: bafybeieiesjkyjbcvqut3l6nlzcwpidibsmo3mz7j6p54lbqzspfcf3ram - tasks/task_preparations.py: bafybeieqd2jhzmmjomzmcw72iptogrb6koche6dqmtcuz6lqktaxvi3dxe + tasks/task_preparations.py: bafybeiatxqkfrqgeclg4jex4a6veboqipnukmgett7hwwqprrvdlgh7ana tasks/twitter_preparation.py: bafybeieoa5wkhg55w2bdbkwawdz4kh4wmifzgtcmzi3ursi3oaxvpu4mom + tasks/week_in_olas_preparations.py: bafybeiakt43xrdglfmsrmhw3i3donzsfrqhgxzygdyi26tehpaeyeoz7n4 tasks/write_stream_preparation.py: bafybeifl4j7s575ts6x4msntymkempwguj4yypmxy2xjmffknn7qhkzfo4 tests/__init__.py: bafybeiff447fuzkdgyp5yoqqstzv2pyi2uiokng6lzrtfnsgspocghwypi tests/centaur_configs.py: bafybeifgmz5nlpfj4ynpzywbwnnuhz2dxookezwhxmvzlucjbhugv3tgma diff --git a/packages/valory/skills/decision_making_abci/tasks/task_preparations.py b/packages/valory/skills/decision_making_abci/tasks/task_preparations.py index ba4a5631..0e5ed380 100644 --- a/packages/valory/skills/decision_making_abci/tasks/task_preparations.py +++ b/packages/valory/skills/decision_making_abci/tasks/task_preparations.py @@ -21,6 +21,9 @@ from datetime import datetime, timezone +SECONDS_IN_DAY = 24 * 3600 + + class TaskPreparation: """Represents the work required before and after running a Centaur task""" @@ -36,6 +39,7 @@ def __init__(self, synchronized_data, params, logger, now_utc) -> None: self.now_utc = now_utc self.set_config() self.logger.info(f"Instantiated task {self.__class__.__name__}") + self.log_config() def set_config(self): """Set the configuration""" @@ -52,26 +56,42 @@ def set_config(self): if self.task_name in plugins_config: plugin_config = plugins_config[self.task_name] self.enabled = plugin_config["enabled"] - self.daily = plugin_config["daily"] + self.daily = plugin_config["daily"] if "daily" in plugin_config else False + self.weekly = ( + int(plugin_config["weekly"]) if "weekly" in plugin_config else None + ) self.last_run = ( datetime.strptime( plugin_config["last_run"], "%Y-%m-%d %H:%M:%S %Z" ).replace(tzinfo=timezone.utc) - if self.daily and plugin_config["last_run"] + if (self.daily or (self.weekly is not None)) + and plugin_config["last_run"] + else None + ) + self.run_hour_utc = ( + plugin_config["run_hour_utc"] + if self.daily or (self.weekly is not None) else None ) - self.run_hour_utc = plugin_config["run_hour_utc"] if self.daily else None return self.set_default_config() def set_default_config(self): """Set the default configuration""" + self.logger.info("Setting the default configuration") self.enabled = True self.daily = False + self.weekly = None self.last_run = None self.run_hour_utc = None + def log_config(self): + """Log configuration""" + self.logger.info( + f"Config: enabled={self.enabled} daily={self.daily} weekly={self.weekly} last_run={self.last_run} run_hour_utc={self.run_hour_utc}" + ) + def check_conditions(self): """Check wether the task needs to be run""" @@ -87,8 +107,27 @@ def check_conditions(self): ) return False + # Does the task run every week? + if self.weekly is not None and self.weekly != self.now_utc.weekday(): + self.logger.info( + f"[{self.__class__.__name__}]: task is a weekly task but today is not the configured run day: {self.now_utc.weekday()} != {self.weekly}" + ) + return False + + if ( + self.weekly is not None + and self.last_run + and (self.now_utc - self.last_run).seconds < SECONDS_IN_DAY + ): + self.logger.info( + f"[{self.__class__.__name__}]: task is a weekly task and was already ran less than a day ago" + ) + return False + # Does the task run at a specific time? - if self.daily and self.now_utc.hour < self.run_hour_utc: + if ( + self.daily or (self.weekly is not None) + ) and self.now_utc.hour < self.run_hour_utc: self.logger.info( f"[{self.__class__.__name__}]: not time to run yet [{self.now_utc.hour}!={self.run_hour_utc}]" ) diff --git a/packages/valory/skills/decision_making_abci/tasks/week_in_olas_preparations.py b/packages/valory/skills/decision_making_abci/tasks/week_in_olas_preparations.py new file mode 100644 index 00000000..294482e2 --- /dev/null +++ b/packages/valory/skills/decision_making_abci/tasks/week_in_olas_preparations.py @@ -0,0 +1,69 @@ +# -*- 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 logic for task preparations.""" +import uuid + +from packages.valory.skills.decision_making_abci.rounds import Event +from packages.valory.skills.decision_making_abci.tasks.task_preparations import ( + TaskPreparation, +) + + +class WeekInOlasCreatePreparation(TaskPreparation): + """WeekInOlasCreatePreparation""" + + task_name = "week_in_olas" + task_event = Event.WEEK_IN_OLAS_CREATE.value + + def check_extra_conditions(self): + """Check extra conditions""" + return True + + def _pre_task(self): + """Preparations before running the task""" + updates = {} + return updates, self.task_event + + def _post_task(self): + """Task postprocessing""" + centaurs_data = self.synchronized_data.centaurs_data + current_centaur = centaurs_data[self.synchronized_data.current_centaur_index] + + # Update the last run time + current_centaur["configuration"]["plugins"]["week_in_olas"][ + "last_run" + ] = self.now_utc.strftime("%Y-%m-%d %H:%M:%S %Z") + + # Add the new thread to proposed tweets + thread = { + "text": self.synchronized_data.summary_tweets, + "posted": False, + "voters": [], + "execute": False, + "proposer": "0x12b680F1Ffb678598eFC0C57BB2edCAebB762A9A", # safe address + "request_id": str(uuid.UUID(int=int(self.now_utc.timestamp()))), + "createdDate": self.now_utc.timestamp(), + } + + current_centaur["plugins_data"]["scheduled_tweet"]["tweets"].append(thread) + + updates = {"centaurs_data": centaurs_data, "has_centaurs_changes": True} + + return updates, None diff --git a/packages/valory/skills/dynamic_nft_abci/behaviours.py b/packages/valory/skills/dynamic_nft_abci/behaviours.py index 535fcbc8..e6f83ced 100644 --- a/packages/valory/skills/dynamic_nft_abci/behaviours.py +++ b/packages/valory/skills/dynamic_nft_abci/behaviours.py @@ -108,11 +108,14 @@ def get_token_id_to_address( ] ) except KeyError: + self.context.logger.warning( + f"last_parsed_block is not set. Using default earliest_block_to_monitor={self.params.earliest_block_to_monitor}" + ) from_block = self.params.earliest_block_to_monitor self.context.logger.info( f"Retrieving Transfer events later than block {from_block}" - f" for contract at {self.params.dynamic_contribution_contract_address}" + f" for contract at {self.params.dynamic_contribution_contract_address}. Retries={self.synchronized_data.token_event_retries}" ) contract_api_msg = yield from self.get_contract_api_response( performative=ContractApiMessage.Performative.GET_STATE, # type: ignore @@ -123,7 +126,9 @@ def get_token_id_to_address( from_block=from_block, ) if contract_api_msg.performative != ContractApiMessage.Performative.STATE: - self.context.logger.info("Error retrieving the token_id to address data") + self.context.logger.info( + f"Error retrieving the token_id to address data [{contract_api_msg.performative}]" + ) return TokenTrackRound.ERROR_PAYLOAD, from_block data = cast(dict, contract_api_msg.state.body["token_id_to_member"]) last_block = cast(int, contract_api_msg.state.body["last_block"]) diff --git a/packages/valory/skills/dynamic_nft_abci/rounds.py b/packages/valory/skills/dynamic_nft_abci/rounds.py index 25e368a9..f5fb7b40 100644 --- a/packages/valory/skills/dynamic_nft_abci/rounds.py +++ b/packages/valory/skills/dynamic_nft_abci/rounds.py @@ -37,6 +37,9 @@ from packages.valory.skills.dynamic_nft_abci.payloads import TokenTrackPayload +MAX_TOKEN_EVENT_RETRIES = 3 + + class Event(Enum): """DynamicNFTAbciApp Events""" @@ -73,6 +76,11 @@ def pending_write(self) -> bool: """Checks whether there are changes pending to be written to Ceramic.""" return cast(bool, self.db.get("pending_write", False)) + @property + def token_event_retries(self) -> int: + """Get the token id to points mapping.""" + return cast(int, self.db.get("token_event_retries", 0)) + class TokenTrackRound(CollectSameUntilThresholdRound): """TokenTrackRound""" @@ -88,6 +96,24 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: payload = json.loads(self.most_voted_payload) if payload == TokenTrackRound.ERROR_PAYLOAD: + + token_event_retries = ( + cast(SynchronizedData, self.synchronized_data).token_event_retries + + 1 + ) + + if token_event_retries >= MAX_TOKEN_EVENT_RETRIES: + return self.synchronized_data, Event.DONE + + synchronized_data = self.synchronized_data.update( + synchronized_data_class=SynchronizedData, + **{ + get_name( + SynchronizedData.token_event_retries + ): token_event_retries, + } + ) + return self.synchronized_data, Event.CONTRACT_ERROR token_id_to_points = payload["token_id_to_points"] @@ -102,6 +128,7 @@ def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: get_name(SynchronizedData.last_update_time): last_update_time, get_name(SynchronizedData.ceramic_db): ceramic_db, get_name(SynchronizedData.pending_write): pending_write, + get_name(SynchronizedData.token_event_retries): 0, } ) return (synchronized_data, Event.DONE) diff --git a/packages/valory/skills/dynamic_nft_abci/skill.yaml b/packages/valory/skills/dynamic_nft_abci/skill.yaml index 502e1b3c..f7d316c7 100644 --- a/packages/valory/skills/dynamic_nft_abci/skill.yaml +++ b/packages/valory/skills/dynamic_nft_abci/skill.yaml @@ -7,14 +7,14 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeiasfbratsbsxyji2k2xeft3jdqaajwqskjsvbjsqht4mrew6wb2km - behaviours.py: bafybeifvzua5ftti42czon4gwly7heunxagpk3p5xsomfyr32ronhq7coe + behaviours.py: bafybeib2zfbow4ft64w3ib53u4hkanzvoubd2vq5mjrehg2x3omccwg4ni ceramic_db.py: bafybeicusdonrdq6kirgkpdqmi3a6kmeal4nctnm5ozjqf5s5se6jpitjm dialogues.py: bafybeiabtq2to7x6bbbnxzrvupz5rky4b6k73y2lw5dmclb6xx74nuzxe4 fsm_specification.yaml: bafybeibkm4iniyjt7ofqredclpvvudtfjbmuatccnblygvqnfucsuymbxy handlers.py: bafybeiciwdw6ori5sld5ww3znevwgfoiiiwcquv7imq2s7z6a4ybmb2c54 models.py: bafybeianeimox27xwxbm5yq6go3ygf3oa2cbuv6h4y64oyjo3m2ua23sxq payloads.py: bafybeiggpj2qmh73nlr2rscisscxovf7bfrczlut7k33jujvutzgszjcwi - rounds.py: bafybeie66w2czx57wcfhek5cqw5ecnhkqxr3ycdlnyqkrfk6sljgvkv3xq + rounds.py: bafybeia7fehb5yuxr4p3lakgfohtgwmw24kjfvor4tbhkm3v7yx2uxcg3m tests/__init__.py: bafybeidxte5jeugotf25yogfbsoivyokeqffrvzo7lqgspm4kzrgbhvc3u tests/test_behaviours.py: bafybeibojamjtso4gc5zxuvssqwdhigpaeixz2f4s7jkeiwxi5g5vvhaqm tests/test_ceramic_db.py: bafybeiaemz76p55aicazhysbgisrjm5sls55jbbnyfbbyye5fpsk6ltxz4 @@ -27,7 +27,7 @@ fingerprint_ignore_patterns: [] connections: - fetchai/http_server:0.22.0:bafybeihp5umafxzx45aad5pj7s3343se2wjkgnbirt4pybrape22swm6de contracts: -- valory/dynamic_contribution:0.1.0:bafybeihjnddpy24wpqtsrf57qdxvku7l5w6sm36u6hkm7i2um7ln664ice +- valory/dynamic_contribution:0.1.0:bafybeiht364qavknmnuz7pwkw4zjzrkkugyzymgxrjvtasfcy7eya3nbxa protocols: - valory/contract_api:1.0.0:bafybeiasywsvax45qmugus5kxogejj66c5taen27h4voriodz7rgushtqa - valory/http:1.0.0:bafybeia5bxdua2i6chw6pg47bvoljzcpuqxzy4rdrorbdmcbnwmnfdobtu diff --git a/packages/valory/skills/impact_evaluator_abci/behaviours.py b/packages/valory/skills/impact_evaluator_abci/behaviours.py index 13f95c57..4914d066 100644 --- a/packages/valory/skills/impact_evaluator_abci/behaviours.py +++ b/packages/valory/skills/impact_evaluator_abci/behaviours.py @@ -41,6 +41,7 @@ ImpactEvaluatorSkillAbciApp, ) from packages.valory.skills.llm_abci.behaviours import LLMRoundBehaviour +from packages.valory.skills.olas_week_abci.behaviours import OlasWeekRoundBehaviour from packages.valory.skills.registration_abci.behaviours import ( AgentRegistrationRoundBehaviour, RegistrationStartupBehaviour, @@ -81,5 +82,6 @@ class ImpactEvaluatorConsensusBehaviour(AbstractRoundBehaviour): *ResetPauseABCIConsensusBehaviour.behaviours, *TransactionSettlementRoundBehaviour.behaviours, *TerminationAbciBehaviours.behaviours, + *OlasWeekRoundBehaviour.behaviours, } background_behaviours_cls = {BackgroundBehaviour} diff --git a/packages/valory/skills/impact_evaluator_abci/composition.py b/packages/valory/skills/impact_evaluator_abci/composition.py index 6132e7c1..13c1652f 100644 --- a/packages/valory/skills/impact_evaluator_abci/composition.py +++ b/packages/valory/skills/impact_evaluator_abci/composition.py @@ -24,6 +24,7 @@ import packages.valory.skills.dynamic_nft_abci.rounds as DynamicNFTAbci import packages.valory.skills.generic_scoring_abci.rounds as GenericScoringAbci import packages.valory.skills.llm_abci.rounds as LLMAbciApp +import packages.valory.skills.olas_week_abci.rounds as WeekInOlasAbciApp import packages.valory.skills.registration_abci.rounds as RegistrationAbci import packages.valory.skills.reset_pause_abci.rounds as ResetAndPauseAbci import packages.valory.skills.twitter_scoring_abci.rounds as TwitterScoringAbci @@ -54,8 +55,10 @@ DecisionMakingAbci.FinishedDecisionMakingReadManualPointsRound: CeramicReadAbci.StreamReadRound, DecisionMakingAbci.FinishedDecisionMakingScoreRound: GenericScoringAbci.GenericScoringRound, DecisionMakingAbci.FinishedDecisionMakingDoneRound: ResetAndPauseAbci.ResetAndPauseRound, + DecisionMakingAbci.FinishedDecisionMakingWeekInOlasRound: WeekInOlasAbciApp.OlasWeekDecisionMakingRound, GenericScoringAbci.FinishedGenericScoringRound: TwitterScoringAbci.TwitterDecisionMakingRound, TwitterScoringAbci.FinishedTwitterScoringRound: DynamicNFTAbci.TokenTrackRound, + WeekInOlasAbciApp.FinishedWeekInOlasRound: DecisionMakingAbci.DecisionMakingRound, DynamicNFTAbci.FinishedTokenTrackRound: DecisionMakingAbci.DecisionMakingRound, LLMAbciApp.FinishedLLMRound: DecisionMakingAbci.DecisionMakingRound, TwitterWriteAbciApp.FinishedTwitterWriteRound: DecisionMakingAbci.DecisionMakingRound, @@ -85,6 +88,7 @@ DynamicNFTAbci.DynamicNFTAbciApp, CeramicWriteAbci.CeramicWriteAbciApp, ResetAndPauseAbci.ResetPauseAbciApp, + WeekInOlasAbciApp.WeekInOlasAbciApp, ), abci_app_transition_mapping, ).add_background_app(termination_config) diff --git a/packages/valory/skills/impact_evaluator_abci/fsm_specification.yaml b/packages/valory/skills/impact_evaluator_abci/fsm_specification.yaml index 0317da4d..172949bd 100644 --- a/packages/valory/skills/impact_evaluator_abci/fsm_specification.yaml +++ b/packages/valory/skills/impact_evaluator_abci/fsm_specification.yaml @@ -25,6 +25,7 @@ alphabet_in: - RESET_AND_PAUSE_TIMEOUT - RETRIEVE_HASHTAGS - RETRIEVE_MENTIONS +- RETRIEVE_TWEETS - ROUND_TIMEOUT - SCHEDULED_TWEET - SCORE @@ -32,6 +33,7 @@ alphabet_in: - TWEET_EVALUATION_ROUND_TIMEOUT - UPDATE_CENTAURS - VERIFICATION_ERROR +- WEEK_IN_OLAS_CREATE - WRITE_CONTRIBUTE_DB default_start_state: RegistrationStartupRound final_states: [] @@ -46,6 +48,12 @@ states: - LLMRandomnessRound - LLMRound - LLMSelectKeeperRound +- OlasWeekDecisionMakingRound +- OlasWeekEvaluationRound +- OlasWeekOpenAICallCheckRound +- OlasWeekRandomnessRound +- OlasWeekSelectKeepersRound +- OlasWeekTweetCollectionRound - OpenAICallCheckRound - RandomnessRound - RandomnessTwitterRound @@ -82,6 +90,7 @@ transition_func: (DecisionMakingRound, SCHEDULED_TWEET): RandomnessTwitterRound (DecisionMakingRound, SCORE): GenericScoringRound (DecisionMakingRound, UPDATE_CENTAURS): RandomnessRound + (DecisionMakingRound, WEEK_IN_OLAS_CREATE): OlasWeekDecisionMakingRound (DecisionMakingRound, WRITE_CONTRIBUTE_DB): RandomnessRound (GenericScoringRound, DONE): TwitterDecisionMakingRound (GenericScoringRound, NO_MAJORITY): GenericScoringRound @@ -96,6 +105,32 @@ transition_func: (LLMSelectKeeperRound, DONE): LLMRound (LLMSelectKeeperRound, NO_MAJORITY): LLMRandomnessRound (LLMSelectKeeperRound, ROUND_TIMEOUT): LLMRandomnessRound + (OlasWeekDecisionMakingRound, DONE): DecisionMakingRound + (OlasWeekDecisionMakingRound, DONE_SKIP): DecisionMakingRound + (OlasWeekDecisionMakingRound, EVALUATE): OlasWeekEvaluationRound + (OlasWeekDecisionMakingRound, NO_MAJORITY): OlasWeekDecisionMakingRound + (OlasWeekDecisionMakingRound, OPENAI_CALL_CHECK): OlasWeekOpenAICallCheckRound + (OlasWeekDecisionMakingRound, RETRIEVE_TWEETS): OlasWeekTweetCollectionRound + (OlasWeekDecisionMakingRound, ROUND_TIMEOUT): OlasWeekDecisionMakingRound + (OlasWeekDecisionMakingRound, SELECT_KEEPERS): OlasWeekRandomnessRound + (OlasWeekEvaluationRound, DONE): OlasWeekDecisionMakingRound + (OlasWeekEvaluationRound, TWEET_EVALUATION_ROUND_TIMEOUT): OlasWeekEvaluationRound + (OlasWeekOpenAICallCheckRound, DONE): OlasWeekDecisionMakingRound + (OlasWeekOpenAICallCheckRound, NO_ALLOWANCE): OlasWeekDecisionMakingRound + (OlasWeekOpenAICallCheckRound, NO_MAJORITY): OlasWeekOpenAICallCheckRound + (OlasWeekOpenAICallCheckRound, ROUND_TIMEOUT): OlasWeekOpenAICallCheckRound + (OlasWeekRandomnessRound, DONE): OlasWeekSelectKeepersRound + (OlasWeekRandomnessRound, NO_MAJORITY): OlasWeekRandomnessRound + (OlasWeekRandomnessRound, ROUND_TIMEOUT): OlasWeekRandomnessRound + (OlasWeekSelectKeepersRound, DONE): OlasWeekDecisionMakingRound + (OlasWeekSelectKeepersRound, NO_MAJORITY): OlasWeekRandomnessRound + (OlasWeekSelectKeepersRound, ROUND_TIMEOUT): OlasWeekRandomnessRound + (OlasWeekTweetCollectionRound, API_ERROR): OlasWeekTweetCollectionRound + (OlasWeekTweetCollectionRound, DONE): OlasWeekDecisionMakingRound + (OlasWeekTweetCollectionRound, DONE_API_LIMITS): OlasWeekDecisionMakingRound + (OlasWeekTweetCollectionRound, DONE_MAX_RETRIES): OlasWeekDecisionMakingRound + (OlasWeekTweetCollectionRound, NO_MAJORITY): OlasWeekRandomnessRound + (OlasWeekTweetCollectionRound, ROUND_TIMEOUT): OlasWeekRandomnessRound (OpenAICallCheckRound, DONE): TwitterDecisionMakingRound (OpenAICallCheckRound, NO_ALLOWANCE): TwitterDecisionMakingRound (OpenAICallCheckRound, NO_MAJORITY): OpenAICallCheckRound diff --git a/packages/valory/skills/impact_evaluator_abci/models.py b/packages/valory/skills/impact_evaluator_abci/models.py index 4ae3f74e..945f29af 100644 --- a/packages/valory/skills/impact_evaluator_abci/models.py +++ b/packages/valory/skills/impact_evaluator_abci/models.py @@ -56,6 +56,8 @@ ) from packages.valory.skills.llm_abci.models import Params as LLMAbciParams from packages.valory.skills.llm_abci.rounds import Event as LLMEvent +from packages.valory.skills.olas_week_abci.models import Params as OlasWeekAbciParams +from packages.valory.skills.olas_week_abci.rounds import Event as OlasWeekEvent from packages.valory.skills.reset_pause_abci.rounds import Event as ResetPauseEvent from packages.valory.skills.termination_abci.models import TerminationParams from packages.valory.skills.twitter_scoring_abci.models import ( @@ -72,6 +74,7 @@ TwitterScoringParams = TwitterScoringAbciParams LLMParams = LLMAbciParams DecisionMakingParams = DecisionMakingAbciParams +OlasWeekParams = OlasWeekAbciParams Requests = BaseRequests BenchmarkTool = BaseBenchmarkTool @@ -123,6 +126,9 @@ def setup(self) -> None: ImpactEvaluatorSkillAbciApp.event_to_timeout[ DecisionMakingEvent.ROUND_TIMEOUT ] = self.context.params.round_timeout_seconds + ImpactEvaluatorSkillAbciApp.event_to_timeout[ + OlasWeekEvent.ROUND_TIMEOUT + ] = self.context.params.round_timeout_seconds class Params( @@ -132,5 +138,6 @@ class Params( DynamicNFTParams, DecisionMakingParams, TerminationParams, + OlasWeekParams, ): """A model to represent params for multiple abci apps.""" diff --git a/packages/valory/skills/impact_evaluator_abci/skill.yaml b/packages/valory/skills/impact_evaluator_abci/skill.yaml index 9fff0970..a0d348bb 100644 --- a/packages/valory/skills/impact_evaluator_abci/skill.yaml +++ b/packages/valory/skills/impact_evaluator_abci/skill.yaml @@ -7,12 +7,12 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeigmhxenrqb2ysjhcm2au2lang4abyny6irkzqqg4dtclz6net6zpy - behaviours.py: bafybeibcqolnjh2e7epjt5npmw3denyxgyyxmugirhn5pdwnw5e4ege3xy - composition.py: bafybeifjy4sknt45etzud543l54c6dp6vf7nschuvccqhcbt3owfliymae + behaviours.py: bafybeia53iegibc2svkyw6ovhs7f32tig2ut2okvogyaaeoijo3uj5l6ke + composition.py: bafybeialteytmbpts5e4fxeivs2fatvujyultp6tpbmvsinlu73dfgjvji dialogues.py: bafybeigjknz4qqynbsltjje46gidg4rftsqw6ybjwegz24wetmycutpzh4 - fsm_specification.yaml: bafybeifpy7qk3psx22xj46izrqznetlgnukanb23ju25q53ydx6sfcctk4 + fsm_specification.yaml: bafybeig7tva5gvvk3tpbhb3z442q7755cur5q5u5cgrxgf2v3543pt3lwy handlers.py: bafybeidkli6fphcmdgwsys4lkyf3fx6fbawet4nt2pnixfypzijhg6b3ze - models.py: bafybeifhcshu4iwqvc2fz3auu5ngfykkhvqnroeug52ilrwwc4kowkeg5a + models.py: bafybeigtb7k4gk22574epnpitr6lqv2coaf2j5pgwuymh2xsvtj4qta2t4 tests/__init__.py: bafybeievwzwojvq4aofk5kjpf4jzygfes7ew6s6svc6b6frktjnt3sicce tests/test_behaviours.py: bafybeifkazsevd6vsfaapulouxepez3rl24y3rxgja5zhmj7s323zdjlmq tests/test_dialogues.py: bafybeieaos2byphju6i6xvytppqqcuqqvnpilnflsy73l3wqazzjttbg7m @@ -26,16 +26,17 @@ skills: - valory/abstract_round_abci:0.1.0:bafybeih2fyfb6kkf7r45pvdk7pyyebr5xloia4xiqxtb3qsrasnstqmepq - valory/ceramic_read_abci:0.1.0:bafybeide5tbobzkwfdi7yzw6bsau36h43hdlgbnk2bfqvvhmq3hl3ucjmm - valory/generic_scoring_abci:0.1.0:bafybeig4hoc5gjiyuwh2ye3mwneegq3s3cghl5j7yokvngacvn7ixwdoga -- valory/twitter_scoring_abci:0.1.0:bafybeidccv4tsye4i45f443p7dkidfpw3ifu4jzi4gk4jiz2cscusondhy +- valory/twitter_scoring_abci:0.1.0:bafybeicngrngwq4fna57rj4lo4nsvt73t3x3d33jb52po5e6kc7stbkl3q - valory/ceramic_write_abci:0.1.0:bafybeihnalj6zhl5pbf7x2lkznuhivwj45jzrafbtxatwebrpb7st2v6oa -- valory/dynamic_nft_abci:0.1.0:bafybeihe7rav55tlymizd5ltiwwvqzqrfqswm5sxsaw6bioal35lecwhny +- valory/dynamic_nft_abci:0.1.0:bafybeiexenlxvf32jjlxrow35qlo4xtmi2ezznulczhayux2n2mx27ya6i - valory/registration_abci:0.1.0:bafybeibndt6vxiwc2edgtaxpjbhwto3eyu33ujz3zmvtygvo2qz7xme5li - valory/reset_pause_abci:0.1.0:bafybeigeoc363gv3wp2rrmk6p2fdxney33nxd3owtpfugzapgruwe4klyu - valory/termination_abci:0.1.0:bafybeigqpij2sgrpnilqjljfciixop4fldq5qceixc7534q6af4potdmdm - valory/transaction_settlement_abci:0.1.0:bafybeia7rzsbea3ch4gcafyp3z6uvqh4npws2xpdwbkkdbrqqpjops7nui -- valory/decision_making_abci:0.1.0:bafybeigx57k6zwiatveoiyczbhz6xbdtfhhbxg7442tjqyb7ljjfjusn5e +- valory/decision_making_abci:0.1.0:bafybeicjrztkcvqjnk6odukj3j2dr6ee4j2jaaw5nsga6byjhjyuxhng5m - valory/llm_abci:0.1.0:bafybeifesvwe5ya7imxefijpawgtd7aje5hhr4oh2bzgfsm2ucjp3lxije - valory/twitter_write_abci:0.1.0:bafybeidjumloxy2yvoyymgzuwbua3ks2p66fyybna2qhrz2upzkxzjrznq +- valory/olas_week_abci:0.1.0:bafybeidegg3yi5qww6q6r24pgjmmbkgconyfnecuohacljvferqqg3tc2e behaviours: main: args: {} @@ -170,6 +171,8 @@ models: max_tweet_pulls_allowed: 80 twitter_search_endpoint: 2/tweets/search/recent? twitter_search_args: query=%23olas&tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results={max_results}&since_id={since_id} + twitter_tweets_endpoint: 2/users/1450081635559428107/tweets? + twitter_tweets_args: tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results=50&start_time={start_time} openai_call_window_size: 3600.0 openai_calls_allowed_in_window: 100 max_points_per_period: 5000 diff --git a/packages/valory/skills/olas_week_abci/__init__.py b/packages/valory/skills/olas_week_abci/__init__.py new file mode 100644 index 00000000..41b26021 --- /dev/null +++ b/packages/valory/skills/olas_week_abci/__init__.py @@ -0,0 +1,25 @@ +# -*- 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 implementation of the default skill.""" + +from aea.configurations.base import PublicId + + +PUBLIC_ID = PublicId.from_str("valory/olas_week_abci:0.1.0") diff --git a/packages/valory/skills/olas_week_abci/behaviours.py b/packages/valory/skills/olas_week_abci/behaviours.py new file mode 100644 index 00000000..d221e8a1 --- /dev/null +++ b/packages/valory/skills/olas_week_abci/behaviours.py @@ -0,0 +1,707 @@ +# -*- 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 WeekInOlasAbciApp.""" + +import json +import math +import random +from abc import ABC +from datetime import datetime, timedelta +from typing import Dict, Generator, List, Optional, Set, Tuple, Type, cast + +from packages.valory.connections.openai.connection import ( + PUBLIC_ID as LLM_CONNECTION_PUBLIC_ID, +) +from packages.valory.protocols.llm.message import LlmMessage +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.abstract_round_abci.common import RandomnessBehaviour +from packages.valory.skills.abstract_round_abci.models import Requests +from packages.valory.skills.olas_week_abci.dialogues import LlmDialogue, LlmDialogues +from packages.valory.skills.olas_week_abci.models import ( + OpenAICalls, + Params, + SharedState, +) +from packages.valory.skills.olas_week_abci.payloads import ( + OlasWeekDecisionMakingPayload, + OlasWeekEvaluationPayload, + OlasWeekRandomnessPayload, + OlasWeekSelectKeepersPayload, + OlasWeekTweetCollectionPayload, + OpenAICallCheckPayload, +) +from packages.valory.skills.olas_week_abci.prompts import tweet_summarizer_prompt +from packages.valory.skills.olas_week_abci.rounds import ( + ERROR_API_LIMITS, + ERROR_GENERIC, + Event, + OlasWeekDecisionMakingRound, + OlasWeekEvaluationRound, + OlasWeekOpenAICallCheckRound, + OlasWeekRandomnessRound, + OlasWeekSelectKeepersRound, + OlasWeekTweetCollectionRound, + SynchronizedData, + WeekInOlasAbciApp, +) + + +ONE_DAY = 86400.0 +ADDRESS_REGEX = r"0x[a-fA-F0-9]{40}" +TAGLINE = "I'm linking my wallet to @Autonolas Contribute:" +DEFAULT_TWEET_POINTS = 100 +TWEET_QUALITY_TO_POINTS = {"LOW": 1, "AVERAGE": 2, "HIGH": 3} +TWEET_RELATIONSHIP_TO_POINTS = {"LOW": 100, "AVERAGE": 200, "HIGH": 300} +HTTP_OK = 200 +HTTP_TOO_MANY_REQUESTS = 429 +MAX_TWEET_CHARS = 250 # do not use 280 as not every char counts the same + + +def extract_headers(header_str: str) -> dict: + """Extracts HTTP headers""" + header_separator = "\r\n" if "\r\n" in header_str else "\n" + headers = [ + header.split(": ") for header in header_str.split(header_separator) if header + ] + return {key: value for key, value in headers} + + +def parse_summary(summary: str) -> list: + """Parse the tweet summary""" + highlights = [h[1:].strip() for h in summary.split("\n") if h.startswith("-")] + + tweets = ["Week in Olas\n\nHighlights included:"] + for highlight in highlights: + if len(highlight) > MAX_TWEET_CHARS: + while len(highlight) > MAX_TWEET_CHARS: + tweets.append(highlight[:MAX_TWEET_CHARS]) + highlight = highlight[MAX_TWEET_CHARS:] + else: + tweets.append(highlight) + + return tweets + + +class OlasWeekBaseBehaviour(BaseBehaviour, ABC): + """Base behaviour for the common apps' 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 openai_calls(self) -> OpenAICalls: + """Return the params.""" + return self.params.openai_calls + + def _check_twitter_limits(self) -> Tuple: + """Check if the daily limit has exceeded or not""" + try: + number_of_tweets_pulled_today = int( + self.synchronized_data.ceramic_db["module_data"]["twitter"][ + "number_of_tweets_pulled_today" + ] + ) + last_tweet_pull_window_reset = float( + self.synchronized_data.ceramic_db["module_data"]["twitter"][ + "last_tweet_pull_window_reset" + ] + ) + except KeyError: + number_of_tweets_pulled_today = 0 + last_tweet_pull_window_reset = cast( + SharedState, self.context.state + ).round_sequence.last_round_transition_timestamp.timestamp() + + current_time = cast( + SharedState, self.context.state + ).round_sequence.last_round_transition_timestamp.timestamp() + + # 15 min window limit + if self.synchronized_data.sleep_until: + time_window_close = self.synchronized_data.sleep_until + if current_time < time_window_close: + return True, 0, current_time + + # Window has expired + if current_time >= last_tweet_pull_window_reset + ONE_DAY: + return False, 0, current_time + + # Reached max number of tweets + if number_of_tweets_pulled_today >= self.params.max_tweet_pulls_allowed: + return True, number_of_tweets_pulled_today, last_tweet_pull_window_reset + + # Window has not expired and we have not reached the max number of tweets + return False, number_of_tweets_pulled_today, last_tweet_pull_window_reset + + +class OlasWeekRandomnessBehaviour(RandomnessBehaviour): + """Retrieve randomness.""" + + matching_round = OlasWeekRandomnessRound + payload_class = OlasWeekRandomnessPayload + + +class OlasWeekSelectKeepersBehaviour(OlasWeekBaseBehaviour): + """Select the keeper agent.""" + + matching_round = OlasWeekSelectKeepersRound + payload_class = OlasWeekSelectKeepersPayload + + 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 = OlasWeekSelectKeepersPayload( # 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 OlasWeekDecisionMakingBehaviour(OlasWeekBaseBehaviour): + """OlasWeekDecisionMakingBehaviour""" + + matching_round: Type[AbstractRound] = OlasWeekDecisionMakingRound + + def async_act(self) -> Generator: + """Do the act, supporting asynchronous execution.""" + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + event = self.get_next_event() + self.context.logger.info(f"Next event: {event}") + + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction( + payload=OlasWeekDecisionMakingPayload( + sender=self.context.agent_address, + event=event, + ) + ) + yield from self.wait_until_round_end() + self.set_done() + + def get_next_event(self) -> str: + """Decide what is the next round""" + + performed_tasks = self.synchronized_data.performed_olas_week_tasks + + self.context.logger.info(f"Performed tasks: {performed_tasks}") + + if Event.OPENAI_CALL_CHECK.value not in performed_tasks: + return Event.OPENAI_CALL_CHECK.value + + 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_TWEETS.value not in performed_tasks: + return Event.RETRIEVE_TWEETS.value + + if performed_tasks[Event.RETRIEVE_TWEETS.value] == Event.DONE_MAX_RETRIES.value: + return Event.DONE_SKIP.value + + if Event.EVALUATE.value not in performed_tasks: + return Event.EVALUATE.value + + return Event.DONE.value + + +class OlasWeekOpenAICallCheckBehaviour(OlasWeekBaseBehaviour): + """OlasWeekTweetCollectionBehaviour""" + + matching_round: Type[AbstractRound] = OlasWeekOpenAICallCheckRound + + def async_act(self) -> Generator: + """Do the act, supporting asynchronous execution.""" + with self.context.benchmark_tool.measure(self.behaviour_id).local(): + current_time = cast( + SharedState, self.context.state + ).round_sequence.last_round_transition_timestamp.timestamp() + # Reset the window if the window expired before checking + self.openai_calls.reset(current_time=current_time) + if self.openai_calls.max_calls_reached(): + content = None + else: + content = OlasWeekOpenAICallCheckRound.CALLS_REMAINING + with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): + yield from self.send_a2a_transaction( + payload=OpenAICallCheckPayload( + sender=self.context.agent_address, + content=content, + ) + ) + yield from self.wait_until_round_end() + self.set_done() + + +class OlasWeekTweetCollectionBehaviour(OlasWeekBaseBehaviour): + """OlasWeekTweetCollectionBehaviour""" + + matching_round: Type[AbstractRound] = OlasWeekTweetCollectionRound + + 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, + number_of_tweets_pulled_today, + last_tweet_pull_window_reset, + ) = self._check_twitter_limits() + + if has_limit_reached: + self.context.logger.info( + "Cannot retrieve tweets, max number of tweets reached for today or 15-min request amount reached" + ) + payload_data = { + "tweets": None, + "error": ERROR_API_LIMITS, + "number_of_tweets_pulled_today": number_of_tweets_pulled_today, + "sleep_until": self.synchronized_data.sleep_until, + } + + else: + # Get tweets from Twitter + payload_data = yield from self._get_tweets( + number_of_tweets_pulled_today=number_of_tweets_pulled_today + ) + + payload_data["last_tweet_pull_window_reset"] = last_tweet_pull_window_reset + sender = self.context.agent_address + payload = OlasWeekTweetCollectionPayload( + sender=sender, content=json.dumps(payload_data, 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() + + def _get_tweets( + self, + number_of_tweets_pulled_today: int, + ) -> Generator[None, None, Dict]: + """Get Tweets""" + + api_base = self.params.twitter_api_base + api_endpoint = self.params.twitter_tweets_endpoint + + number_of_tweets_remaining_today = ( + self.params.max_tweet_pulls_allowed - number_of_tweets_pulled_today + ) + if number_of_tweets_remaining_today <= 0: + self.context.logger.info( + "Cannot retrieve tweets, max number of tweets reached for today" + ) + return { + "tweets": None, + "error": ERROR_API_LIMITS, + "number_of_tweets_pulled_today": number_of_tweets_pulled_today, + "sleep_until": self.synchronized_data.sleep_until, + } + + # Calculate the starting time (7 days ago) + now_ts = cast( + SharedState, self.context.state + ).round_sequence.last_round_transition_timestamp.timestamp() + + start_time = datetime.fromtimestamp(now_ts) - timedelta(days=7) + + start_time_str = start_time.strftime("%Y-%m-%dT%H:%M:%SZ") + + # Build the args + api_args = self.params.twitter_tweets_args.replace( + "{start_time}", start_time_str + ) + api_args = api_args.replace( + "{max_results}", str(number_of_tweets_remaining_today) + ) + api_url = api_base + api_endpoint + api_args + headers = dict(Authorization=f"Bearer {self.params.twitter_api_bearer_token}") + + self.context.logger.info( + f"Retrieving tweets from Twitter API [{api_url}]\nBearer token {self.params.twitter_api_bearer_token[:5]}*******{self.params.twitter_api_bearer_token[-5:]}" + ) + + tweets = {} + next_token = None + + # Pagination loop: we read a max of pages each period + # Each page contains 100 tweets. The default value for twitter_max_pages is 10 + for _ in range(self.params.twitter_max_pages): + self.context.logger.info( + f"Retrieving a new page. max_pages={self.params.twitter_max_pages}" + ) + url = api_url + # Add the pagination token if it exists + if next_token: + url += f"&pagination_token={next_token}" + + # Make the request + response = yield from self.get_http_response( + method="GET", url=url, headers=headers + ) + + # Check response status + if response.status_code != 200: + header_dict = extract_headers(response.headers) + + remaining, limit, reset_ts = [ + header_dict.get(header, "?") + for header in [ + "x-rate-limit-remaining", + "x-rate-limit-limit", + "x-rate-limit-reset", + ] + ] + reset = ( + datetime.fromtimestamp(int(reset_ts)).strftime("%Y-%m-%d %H:%M:%S") + if reset_ts != "?" + else None + ) + + self.context.logger.error( + f"Error retrieving tweets from Twitter [{response.status_code}]: {response.body}" + f"API limits: {remaining}/{limit}. Window reset: {reset}" + ) + + return { + "tweets": None, + "error": ERROR_API_LIMITS + if response.status_code == HTTP_TOO_MANY_REQUESTS + else ERROR_GENERIC, + "number_of_tweets_pulled_today": number_of_tweets_pulled_today, + "sleep_until": reset_ts + if response.status_code == HTTP_TOO_MANY_REQUESTS + else self.synchronized_data.sleep_until, + } + + api_data = json.loads(response.body) + + # Check the meta field + if "meta" not in api_data: + self.context.logger.error( + f"Twitter API response does not contain the required 'meta' field: {api_data!r}" + ) + return { + "tweets": None, + "error": ERROR_GENERIC, + "number_of_tweets_pulled_today": number_of_tweets_pulled_today, + "sleep_until": None, # we reset this on a successful request + } + + # Check if there are no more results + if ( + "result_count" in api_data["meta"] + and int(api_data["meta"]["result_count"]) == 0 + ): + break + + # Check that the data exists + if "data" not in api_data or "newest_id" not in api_data["meta"]: + self.context.logger.error( + f"Twitter API response does not contain the required 'meta' field: {api_data!r}" + ) + return { + "tweets": None, + "error": ERROR_GENERIC, + "number_of_tweets_pulled_today": number_of_tweets_pulled_today, + "sleep_until": None, # we reset this on a successful request + } + + if "includes" not in api_data or "users" not in api_data["includes"]: + self.context.logger.error( + f"Twitter API response does not contain the required 'includes/users' field: {api_data!r}" + ) + return { + "tweets": None, + "error": ERROR_GENERIC, + "number_of_tweets_pulled_today": number_of_tweets_pulled_today, + "sleep_until": None, # we reset this on a successful request + } + + # Add the retrieved tweets + for tweet in api_data["data"]: + tweets[tweet["id"]] = tweet + + # Set the author handle + for user in api_data["includes"]["users"]: + if user["id"] == tweet["author_id"]: + tweets[tweet["id"]]["username"] = user["username"] + break + number_of_tweets_pulled_today += 1 + + if "next_token" in api_data["meta"]: + next_token = api_data["meta"]["next_token"] + continue + + break + + self.context.logger.info(f"Got {len(tweets)} new tweets") + + return { + "tweets": list(tweets.values()), + "number_of_tweets_pulled_today": number_of_tweets_pulled_today, + "sleep_until": None, # we reset this on a successful request + } + + +class OlasWeekEvaluationBehaviour(OlasWeekBaseBehaviour): + """OlasWeekEvaluationBehaviour""" + + matching_round: Type[AbstractRound] = OlasWeekEvaluationRound + + def _i_am_not_sending(self) -> bool: + """Indicates if the current agent is one of the sender or not.""" + return ( + self.context.agent_address + != self.synchronized_data.most_voted_keeper_address + ) + + 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(): + text = "\n\n".join( + [ + f"tweet_{i}: {tweet}" + for i, tweet in enumerate(self.synchronized_data.weekly_tweets) + ] + ) + + summary_tweets = yield from self.evaluate_summary(text) + + sender = self.context.agent_address + payload = OlasWeekEvaluationPayload( + sender=sender, + content=json.dumps({"summary_tweets": summary_tweets}, 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() + + def evaluate_summary(self, text: str) -> Generator[None, None, list]: + """Create the tweet summary using a LLM.""" + + self.context.logger.info(f"Summarizing text: {text}") + + llm_dialogues = cast(LlmDialogues, self.context.llm_dialogues) + + # llm request message + request_llm_message, llm_dialogue = llm_dialogues.create( + counterparty=str(LLM_CONNECTION_PUBLIC_ID), + performative=LlmMessage.Performative.REQUEST, + prompt_template=tweet_summarizer_prompt.replace("{user_tweets}", text), + prompt_values={}, + ) + request_llm_message = cast(LlmMessage, request_llm_message) + llm_dialogue = cast(LlmDialogue, llm_dialogue) + llm_response_message = yield from self._do_request( + request_llm_message, llm_dialogue + ) + data = llm_response_message.value + self.openai_calls.increase_call_count() + self.context.logger.info(f"Got summary: {repr(data)}") + summary = parse_summary(data) + self.context.logger.info(f"Parsed summary: {summary}") + return summary + + def _do_request( + self, + llm_message: LlmMessage, + llm_dialogue: LlmDialogue, + timeout: Optional[float] = None, + ) -> Generator[None, None, LlmMessage]: + """ + Do a request and wait the response, asynchronously. + + :param llm_message: The request message + :param llm_dialogue: the HTTP dialogue associated to the request + :param timeout: seconds to wait for the reply. + :yield: LLMMessage object + :return: the response message + """ + self.context.outbox.put_message(message=llm_message) + request_nonce = self._get_request_nonce_from_dialogue(llm_dialogue) + cast(Requests, self.context.requests).request_id_to_callback[ + request_nonce + ] = self.get_callback_request() + # notify caller by propagating potential timeout exception. + response = yield from self.wait_for_message(timeout=timeout) + return response + + +class OlasWeekRoundBehaviour(AbstractRoundBehaviour): + """OlasWeekRoundBehaviour""" + + initial_behaviour_cls = OlasWeekTweetCollectionBehaviour + abci_app_cls = WeekInOlasAbciApp # type: ignore + behaviours: Set[Type[BaseBehaviour]] = [ + OlasWeekDecisionMakingBehaviour, + OlasWeekOpenAICallCheckBehaviour, + OlasWeekTweetCollectionBehaviour, + OlasWeekEvaluationBehaviour, + OlasWeekRandomnessBehaviour, + OlasWeekSelectKeepersBehaviour, + ] diff --git a/packages/valory/skills/olas_week_abci/ceramic_db.py b/packages/valory/skills/olas_week_abci/ceramic_db.py new file mode 100644 index 00000000..2980d0bd --- /dev/null +++ b/packages/valory/skills/olas_week_abci/ceramic_db.py @@ -0,0 +1,184 @@ +# -*- 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 implements user db handling.""" + +import itertools +import json +from typing import Any, Dict, List, Optional, Tuple + + +class CeramicDB: + """A class that represents the user database""" + + USER_FIELDS = { + "twitter_id", + "twitter_handle", + "discord_id", + "discord_handle", + "wallet_address", + "token_id", + "points", + "current_period_points", + } + + def __init__( + self, + data: Optional[Dict] = None, + logger: Optional[Any] = None, + ) -> None: + """Create a database""" + self.data = ( + data + if data not in (None, {}) + else { + "users": [], + "module_data": { + "twitter": { + "latest_mention_tweet_id": 0, + "current_period": "1970-01-01", + }, + "dynamic_nft": {}, + "generic": {"latest_update_id": 0}, + }, + } + ) + + self.logger = logger + if self.logger: + self.logger.info("DB: created new db") + + def create_user(self, user_data): + """Create a new user""" + + fields = self.USER_FIELDS.union(user_data.keys()) + + new_user = { + field: user_data.get( + field, 0 if field in ("points", "current_period_points") else None + ) + for field in fields + } + + self.data["users"].append(new_user) + + if self.logger: + self.logger.info(f"DB: created new user: {new_user}") # pragma: nocover + + def get_user_by_field(self, field, value) -> Tuple[Optional[Dict], Optional[int]]: + """Search users""" + + for index, user in enumerate(self.data["users"]): + if user[field] == value: + return user, index # returns the first user that marches + + return None, None + + def get_users_by_field(self, field, value) -> List[Tuple[Dict, int]]: + """Search users""" + users = [] + for index, user in enumerate(self.data["users"]): + if user[field] == value: + users.append((user, index)) + return users + + def update_or_create_user(self, field: str, value: str, new_data: Dict): + """Update an existing user""" + user, index = self.get_user_by_field(field, value) + + if user is None or index is None: + self.create_user({field: value, **new_data}) + return + + fields = set(user).union(new_data.keys()) + + updated_user = { + field: new_data.get(field, user.get(field)) + if field != "points" + else user["points"] + new_data.get("points", 0) + for field in fields + } + + if self.logger: + self.logger.info( + f"DB: updated user: from {json.dumps(user, sort_keys=True)} to {json.dumps(updated_user, sort_keys=True)}" + ) + + self.data["users"][index] = updated_user + + def merge_by_wallet(self): + """Merges users that share the wallet""" + wallet_addresses = set( + [ + user["wallet_address"] + for user in self.data["users"] + if user["wallet_address"] + ] + ) + + for wallet_address in wallet_addresses: + users = self.get_users_by_field("wallet_address", wallet_address) + + if len(users) > 1: + # Get the set of fields + fields = set(itertools.chain(*[list(user.keys()) for user, _ in users])) + fields.remove( + "wallet_address" + ) # we already know this one is duplicated + + # Build the merged user + merged_user = {} + for field in fields: + # Get all the non None values from all users + values = [ + user[field] + for user, _ in users + if field in user and user[field] is not None + ] + + # Points must be added + if field == "points": + values = [sum(values)] + + # We just keep the max current_period_points + if field == "current_period_points": + values = [max(values)] + + # Check whether all values are the same + if len(values) > 1: + values = ( + [values[0]] + if all([v == values[0] for v in values]) + else values + ) + + # Raise on multiple different valid values + if len(values) > 1: + raise ValueError( + f"DB: multiple valid values found for {field} [{values}] while merging users: {users}" + ) + merged_user[field] = values[0] if values else None + merged_user["wallet_address"] = wallet_address + + # Remove duplicated users + for index in sorted([index for _, index in users], reverse=True): + self.data["users"].pop(index) + + # Add merged user + self.data["users"].append(merged_user) diff --git a/packages/valory/skills/olas_week_abci/dialogues.py b/packages/valory/skills/olas_week_abci/dialogues.py new file mode 100644 index 00000000..a6f8da1e --- /dev/null +++ b/packages/valory/skills/olas_week_abci/dialogues.py @@ -0,0 +1,120 @@ +# -*- 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 WeekInOlasAbciApp.""" + +from typing import Any + +from aea.protocols.base import Address, Message +from aea.protocols.dialogue.base import Dialogue as BaseDialogue +from aea.skills.base import Model + +from packages.valory.protocols.llm.dialogues import LlmDialogue as BaseLlmDialogue +from packages.valory.protocols.llm.dialogues import LlmDialogues as BaseLlmDialogues +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 ( + 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 + +LlmDialogue = BaseLlmDialogue + + +class LlmDialogues(Model, BaseLlmDialogues): + """A class to keep track of LLM 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 LlmDialogue.Role.SKILL + + BaseLlmDialogues.__init__( + self, + self_address=str(self.skill_id), + role_from_first_message=role_from_first_message, + ) diff --git a/packages/valory/skills/olas_week_abci/fsm_specification.yaml b/packages/valory/skills/olas_week_abci/fsm_specification.yaml new file mode 100644 index 00000000..03cd1d72 --- /dev/null +++ b/packages/valory/skills/olas_week_abci/fsm_specification.yaml @@ -0,0 +1,55 @@ +alphabet_in: +- API_ERROR +- DONE +- DONE_API_LIMITS +- DONE_MAX_RETRIES +- DONE_SKIP +- EVALUATE +- NO_ALLOWANCE +- NO_MAJORITY +- OPENAI_CALL_CHECK +- RETRIEVE_TWEETS +- ROUND_TIMEOUT +- SELECT_KEEPERS +- TWEET_EVALUATION_ROUND_TIMEOUT +default_start_state: OlasWeekDecisionMakingRound +final_states: +- FinishedWeekInOlasRound +label: WeekInOlasAbciApp +start_states: +- OlasWeekDecisionMakingRound +states: +- FinishedWeekInOlasRound +- OlasWeekDecisionMakingRound +- OlasWeekEvaluationRound +- OlasWeekOpenAICallCheckRound +- OlasWeekRandomnessRound +- OlasWeekSelectKeepersRound +- OlasWeekTweetCollectionRound +transition_func: + (OlasWeekDecisionMakingRound, DONE): FinishedWeekInOlasRound + (OlasWeekDecisionMakingRound, DONE_SKIP): FinishedWeekInOlasRound + (OlasWeekDecisionMakingRound, EVALUATE): OlasWeekEvaluationRound + (OlasWeekDecisionMakingRound, NO_MAJORITY): OlasWeekDecisionMakingRound + (OlasWeekDecisionMakingRound, OPENAI_CALL_CHECK): OlasWeekOpenAICallCheckRound + (OlasWeekDecisionMakingRound, RETRIEVE_TWEETS): OlasWeekTweetCollectionRound + (OlasWeekDecisionMakingRound, ROUND_TIMEOUT): OlasWeekDecisionMakingRound + (OlasWeekDecisionMakingRound, SELECT_KEEPERS): OlasWeekRandomnessRound + (OlasWeekEvaluationRound, DONE): OlasWeekDecisionMakingRound + (OlasWeekEvaluationRound, TWEET_EVALUATION_ROUND_TIMEOUT): OlasWeekEvaluationRound + (OlasWeekOpenAICallCheckRound, DONE): OlasWeekDecisionMakingRound + (OlasWeekOpenAICallCheckRound, NO_ALLOWANCE): OlasWeekDecisionMakingRound + (OlasWeekOpenAICallCheckRound, NO_MAJORITY): OlasWeekOpenAICallCheckRound + (OlasWeekOpenAICallCheckRound, ROUND_TIMEOUT): OlasWeekOpenAICallCheckRound + (OlasWeekRandomnessRound, DONE): OlasWeekSelectKeepersRound + (OlasWeekRandomnessRound, NO_MAJORITY): OlasWeekRandomnessRound + (OlasWeekRandomnessRound, ROUND_TIMEOUT): OlasWeekRandomnessRound + (OlasWeekSelectKeepersRound, DONE): OlasWeekDecisionMakingRound + (OlasWeekSelectKeepersRound, NO_MAJORITY): OlasWeekRandomnessRound + (OlasWeekSelectKeepersRound, ROUND_TIMEOUT): OlasWeekRandomnessRound + (OlasWeekTweetCollectionRound, API_ERROR): OlasWeekTweetCollectionRound + (OlasWeekTweetCollectionRound, DONE): OlasWeekDecisionMakingRound + (OlasWeekTweetCollectionRound, DONE_API_LIMITS): OlasWeekDecisionMakingRound + (OlasWeekTweetCollectionRound, DONE_MAX_RETRIES): OlasWeekDecisionMakingRound + (OlasWeekTweetCollectionRound, NO_MAJORITY): OlasWeekRandomnessRound + (OlasWeekTweetCollectionRound, ROUND_TIMEOUT): OlasWeekRandomnessRound diff --git a/packages/valory/skills/olas_week_abci/handlers.py b/packages/valory/skills/olas_week_abci/handlers.py new file mode 100644 index 00000000..8c4a9883 --- /dev/null +++ b/packages/valory/skills/olas_week_abci/handlers.py @@ -0,0 +1,65 @@ +# -*- 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 WeekInOlasAbciApp.""" + +from typing import Optional + +from aea.configurations.data_types import PublicId + +from packages.valory.protocols.llm import LlmMessage +from packages.valory.skills.abstract_round_abci.handlers import ( + ABCIRoundHandler as BaseABCIRoundHandler, +) +from packages.valory.skills.abstract_round_abci.handlers import AbstractResponseHandler +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 ( + 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, +) + + +ABCIRoundHandler = BaseABCIRoundHandler +HttpHandler = BaseHttpHandler +SigningHandler = BaseSigningHandler +LedgerApiHandler = BaseLedgerApiHandler +ContractApiHandler = BaseContractApiHandler +TendermintHandler = BaseTendermintHandler + + +class LlmHandler(AbstractResponseHandler): + """A class for handling LLLM messages.""" + + SUPPORTED_PROTOCOL: Optional[PublicId] = LlmMessage.protocol_id + allowed_response_performatives = frozenset( + { + LlmMessage.Performative.REQUEST, + LlmMessage.Performative.RESPONSE, + } + ) diff --git a/packages/valory/skills/olas_week_abci/models.py b/packages/valory/skills/olas_week_abci/models.py new file mode 100644 index 00000000..176ea268 --- /dev/null +++ b/packages/valory/skills/olas_week_abci/models.py @@ -0,0 +1,106 @@ +# -*- 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 WeekInOlasAbciApp.""" + +from datetime import datetime +from typing import Any + +from packages.valory.skills.abstract_round_abci.models import ApiSpecs, 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.olas_week_abci.rounds import WeekInOlasAbciApp + + +class SharedState(BaseSharedState): + """Keep the current shared state of the skill.""" + + abci_app_cls = WeekInOlasAbciApp + + +class RandomnessApi(ApiSpecs): + """A model that wraps ApiSpecs for randomness api specifications.""" + + +class OpenAICalls: + """OpenAI call window.""" + + def __init__( + self, + openai_call_window_size: float, + openai_calls_allowed_in_window: int, + ) -> None: + """Initialize object.""" + self._calls_made_in_window = 0 + self._calls_allowed_in_window = openai_calls_allowed_in_window + self._call_window_size = openai_call_window_size + self._call_window_start = datetime.now().timestamp() + + def increase_call_count(self) -> None: + """Increase call count.""" + self._calls_made_in_window += 1 + + def has_window_expired(self, current_time: float) -> bool: + """Increase tweet count.""" + return current_time > (self._call_window_start + self._call_window_size) + + def max_calls_reached(self) -> bool: + """Increase tweet count.""" + return self._calls_made_in_window >= self._calls_allowed_in_window + + def reset(self, current_time: float) -> None: + """Reset the window if required..""" + if not self.has_window_expired(current_time=current_time): + return + self._calls_made_in_window = 0 + self._call_window_start = current_time + + +class Params(BaseParams): + """Parameters.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize the parameters object.""" + self.twitter_api_base = kwargs.get("twitter_api_base") + self.twitter_api_bearer_token = kwargs.get("twitter_api_bearer_token") + self.twitter_tweets_endpoint = kwargs.get("twitter_tweets_endpoint") + self.twitter_tweets_args = kwargs.get("twitter_tweets_args") + self.twitter_max_pages = kwargs.get("twitter_max_pages") + self.tweet_evaluation_round_timeout = kwargs.get( + "tweet_evaluation_round_timeout" + ) + self.max_tweet_pulls_allowed = kwargs.get("max_tweet_pulls_allowed") + self.openai_call_window_size = kwargs.get("openai_call_window_size") + self.openai_calls_allowed_in_window = kwargs.get( + "openai_calls_allowed_in_window" + ) + self.openai_calls = OpenAICalls( + openai_call_window_size=self.openai_call_window_size, + openai_calls_allowed_in_window=self.openai_calls_allowed_in_window, + ) + super().__init__(*args, **kwargs) + + +Requests = BaseRequests +BenchmarkTool = BaseBenchmarkTool diff --git a/packages/valory/skills/olas_week_abci/payloads.py b/packages/valory/skills/olas_week_abci/payloads.py new file mode 100644 index 00000000..068e0021 --- /dev/null +++ b/packages/valory/skills/olas_week_abci/payloads.py @@ -0,0 +1,68 @@ +# -*- 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 transaction payloads of the WeekInOlasAbciApp.""" + +from dataclasses import dataclass +from typing import Optional + +from packages.valory.skills.abstract_round_abci.base import BaseTxPayload + + +@dataclass(frozen=True) +class OlasWeekDecisionMakingPayload(BaseTxPayload): + """Represent a transaction payload for the OlasWeekDecisionMakingRound.""" + + event: str + + +@dataclass(frozen=True) +class OpenAICallCheckPayload(BaseTxPayload): + """Represent a transaction payload for the OlasWeekOpenAICallCheckRound.""" + + content: Optional[str] + + +@dataclass(frozen=True) +class OlasWeekRandomnessPayload(BaseTxPayload): + """Represent a transaction payload of type 'randomness'.""" + + round_id: int + randomness: str + + +@dataclass(frozen=True) +class OlasWeekSelectKeepersPayload(BaseTxPayload): + """Represent a transaction payload of type 'select_keeper'.""" + + keepers: str + + +@dataclass(frozen=True) +class OlasWeekTweetCollectionPayload(BaseTxPayload): + """Represent a transaction payload for the OlasWeekTweetCollectionRound.""" + + content: str + + +@dataclass(frozen=True) +class OlasWeekEvaluationPayload(BaseTxPayload): + """Represent a transaction payload for the OlasWeekEvaluationRound.""" + + content: str diff --git a/packages/valory/skills/olas_week_abci/prompts.py b/packages/valory/skills/olas_week_abci/prompts.py new file mode 100644 index 00000000..2f49c1f9 --- /dev/null +++ b/packages/valory/skills/olas_week_abci/prompts.py @@ -0,0 +1,75 @@ +# -*- 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 LLM prompts for WeekInOlasAbciApp.""" + +tweet_summarizer_prompt = """ +You are an AI tweet summarizer that needs to create concise pieces of content using tweets from your users. +These users will write about what happened during the last week in Olas, a web3 protocol focused on building decentralized autonomous services. +Your task is to summarize all the content from your users in a few short sentences that tells the story of what happened during the last week in the Olas protocol. +You will be given a text about Olas as well as the user tweets. + +GOALS: + +1. Summarize the user tweets in the context of what happened during last week in the Olas space, using a list of highlights. + +For the given goal, only respond with a short item list that summarizes all the Olas news. + +Olas text: + +Technical Architecture: +Olas autonomous software services are embodied as agent services, which are groups of independent computer programs that interact with each other to achieve a predetermined goal. They can be understood as logically centralized applications (with only one application state and logic) that are replicated in a distributed system. Agent services are made of code components that can be combined like Lego bricks through software composition. This is enabled and incentivized by the on-chain protocol, which facilitates developers publishing and finding code components to build and extend new services. The on-chain protocol implements registries that enable code components, agents, and services to be found, reused, and economically compensated. + +The main elements of the Olas tech stack are: Agent services maintained by a service owner and run by multiple operators, who execute independent agent instances (that run the same code); these instances coordinate through a consensus gadget. Composable autonomous apps built out of basic applications that are easily extendable and composable into higher-order applications. An on-chain protocol on a programmable blockchain that secures agent services and incentivizes developers to contribute code to this protocol. + +Tokenomics: +Olas tokenomics focuses on three objectives: + +1/ Growing capital and code proportionally: On-chain mechanisms ensure that the code provided by developers is rewarded according to its usefulness to the services operated on the protocol. The protocol acquires protocol-owned liquidity (PoL) in proportion to code usefulness, allowing the protocol to generate returns, invest in services, and guarantee its long-term financial health. + +2/ Enabling intra- and inter-protocol composability: NFTs representing code and services are used to track contributions inside the protocol, accrue rewards, and can be used productively as collateral across DeFi. + +3/ Incentivizing the donation of profits from Protocol-owned Services (PoSe): Autonomous services owned by governance of various DAOs, operated by the ecosystem, and developed by agent developers worldwide donate some of their profits to the protocol. + +Use Cases for Autonomous Services: +A large market for autonomous agent services is emerging, primarily focused on improving DAO operations. Autonomous services make DAOs more competitive by providing richer means for transparently coordinating human actors and executing processes with little or no marginal human input. Autonomous services can be composed of three fundamental Lego blocks: Keepers, Oracles, and Bridges. + +This composability leads to combinatorial expansion and unprecedented new applications. + +Governance: +A crucial element of the success of Olas is to have an active community and ecosystem that both build, evolve, promote, and make use of Olas technology. For this reason, Olas is organized as a DAO where meaningful contributors and supporters participate in the decision-making process. + +Initially, holders of the virtualized veOLAS token can participate in any governance activities. The veOLAS token is obtained by locking OLAS, which is the native token of Olas. Governance participation is proportional to veOLAS holdings and their locking duration. Governance proposals can notably modify system parameters, support new technological directions, or add entirely new functionality to the on-chain protocol. + +Once a governance proposal is approved, the Timelock adds a delay for the proposal to be executed. + +Exceptionally, some changes to the Olas on-chain protocol could be executed by a community-owned multi-sig wallet, bypassing the governance process. + +This allows a set of trusted actors to overrule governance in certain aspects, e.g., a security exploit that needs to be patched without governance discussion. + + +You should only respond in the format described below: +- Highlight 1 +- Highligth 2 +- ... + + +User tweets: +{user_tweets} +""" diff --git a/packages/valory/skills/olas_week_abci/rounds.py b/packages/valory/skills/olas_week_abci/rounds.py new file mode 100644 index 00000000..53c6822d --- /dev/null +++ b/packages/valory/skills/olas_week_abci/rounds.py @@ -0,0 +1,481 @@ +# -*- 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 WeekInOlasAbciApp.""" + +import json +import math +from enum import Enum +from typing import Any, Dict, FrozenSet, Optional, Set, Tuple, cast + +from packages.valory.skills.abstract_round_abci.base import ( + ABCIAppInternalError, + AbciApp, + AbciAppTransitionFunction, + AppState, + BaseSynchronizedData, + CollectSameUntilThresholdRound, + DegenerateRound, + EventToTimeout, + OnlyKeeperSendsRound, + get_name, +) +from packages.valory.skills.olas_week_abci.payloads import ( + OlasWeekDecisionMakingPayload, + OlasWeekEvaluationPayload, + OlasWeekRandomnessPayload, + OlasWeekSelectKeepersPayload, + OlasWeekTweetCollectionPayload, + OpenAICallCheckPayload, +) + + +MAX_API_RETRIES = 1 +ERROR_GENERIC = "generic" +ERROR_API_LIMITS = "too many requests" + + +class Event(Enum): + """WeekInOlasAbciApp Events""" + + DONE = "done" + DONE_SKIP = "done_skip" + DONE_MAX_RETRIES = "done_max_retries" + DONE_API_LIMITS = "done_api_limits" + NO_MAJORITY = "no_majority" + ROUND_TIMEOUT = "round_timeout" + OPENAI_CALL_CHECK = "openai_call_check" + NO_ALLOWANCE = "no_allowance" + SELECT_KEEPERS = "select_keepers" + RETRIEVE_TWEETS = "retrieve_tweets" + TWEET_EVALUATION_ROUND_TIMEOUT = "tweet_evaluation_round_timeout" + API_ERROR = "api_error" + EVALUATE = "evaluate" + + +class SynchronizedData(BaseSynchronizedData): + """ + Class to represent the synchronized data. + + This data is replicated by the tendermint application. + """ + + @property + def ceramic_db(self) -> dict: + """Get the data stored in the main stream.""" + return cast(dict, self.db.get_strict("ceramic_db")) + + @property + def pending_write(self) -> bool: + """Checks whether there are changes pending to be written to Ceramic.""" + return cast(bool, self.db.get("pending_write", False)) + + @property + def api_retries(self) -> int: + """Gets the number of API retries.""" + return cast(int, self.db.get("api_retries", 0)) + + @property + def sleep_until(self) -> Optional[int]: + """Gets the timestamp of the next Twitter time window for rate limits.""" + return cast(int, self.db.get("sleep_until", None)) + + @property + def weekly_tweets(self) -> list: + """Get the weekly_tweets.""" + return cast(list, self.db.get("weekly_tweets", [])) + + @property + def summary_tweets(self) -> list: + """Get the summary_tweets.""" + return cast(list, self.db.get("summary_tweets", [])) + + @property + def number_of_tweets_pulled_today(self) -> dict: + """Get the 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("last_tweet_pull_window_reset", None)) + + @property + def performed_olas_week_tasks(self) -> dict: + """Get the twitter_tasks.""" + return cast(dict, self.db.get("performed_olas_week_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 most_voted_keeper_address(self) -> list: + """Get the most_voted_keeper_address.""" + return cast(list, self.db.get_strict("most_voted_keeper_address")) + + @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 OlasWeekDecisionMakingRound(CollectSameUntilThresholdRound): + """OlasWeekDecisionMakingRound""" + + payload_class = OlasWeekDecisionMakingPayload + synchronized_data_class = SynchronizedData + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: + """Process the end of the block.""" + if self.threshold_reached: + event = Event(self.most_voted_payload) + # Reference events to avoid tox -e check-abciapp-specs failures + # Event.DONE, Event.RETRIEVE_TWEETS, 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 + ): + return self.synchronized_data, Event.NO_MAJORITY + return None + + +class OlasWeekOpenAICallCheckRound(CollectSameUntilThresholdRound): + """OlasWeekOpenAICallCheckRound""" + + payload_class = OpenAICallCheckPayload + synchronized_data_class = SynchronizedData + + CALLS_REMAINING = "CALLS_REMAINING" + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: + """Process the end of the block.""" + if self.threshold_reached: + performed_olas_week_tasks = cast( + SynchronizedData, self.synchronized_data + ).performed_olas_week_tasks + + # Happy path + if self.most_voted_payload == self.CALLS_REMAINING: + performed_olas_week_tasks["openai_call_check"] = Event.DONE.value + synchronized_data = self.synchronized_data.update( + synchronized_data_class=SynchronizedData, + **{ + get_name( + SynchronizedData.performed_olas_week_tasks + ): performed_olas_week_tasks + }, + ) + return synchronized_data, Event.DONE + + # No allowance + performed_olas_week_tasks["openai_call_check"] = Event.NO_ALLOWANCE.value + synchronized_data = self.synchronized_data.update( + synchronized_data_class=SynchronizedData, + **{ + get_name( + SynchronizedData.performed_olas_week_tasks + ): performed_olas_week_tasks + }, + ) + return synchronized_data, Event.NO_ALLOWANCE + if not self.is_majority_possible( + self.collection, self.synchronized_data.nb_participants + ): + return self.synchronized_data, Event.NO_MAJORITY + return None + + +class OlasWeekTweetCollectionRound(CollectSameUntilThresholdRound): + """OlasWeekTweetCollectionRound""" + + payload_class = OlasWeekTweetCollectionPayload + synchronized_data_class = SynchronizedData + + @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: + performed_olas_week_tasks = cast( + SynchronizedData, self.synchronized_data + ).performed_olas_week_tasks + + payload = json.loads(self.most_voted_payload) + + # API error + if "error" in payload: + + # API limits + if payload["error"] == ERROR_API_LIMITS: + performed_olas_week_tasks[ + "retrieve_tweets" + ] = Event.DONE_MAX_RETRIES.value + + synchronized_data = self.synchronized_data.update( + synchronized_data_class=SynchronizedData, + **{ + get_name(SynchronizedData.sleep_until): payload[ + "sleep_until" + ], + get_name( + SynchronizedData.performed_olas_week_tasks + ): performed_olas_week_tasks, + }, + ) + return synchronized_data, Event.DONE_API_LIMITS + + api_retries = ( + cast(SynchronizedData, self.synchronized_data).api_retries + 1 + ) + + # Other API errors + if api_retries >= MAX_API_RETRIES: + performed_olas_week_tasks[ + "retrieve_tweets" + ] = Event.DONE_MAX_RETRIES.value + synchronized_data = self.synchronized_data.update( + synchronized_data_class=SynchronizedData, + **{ + get_name(SynchronizedData.api_retries): 0, # reset retries + get_name( + SynchronizedData.performed_olas_week_tasks + ): performed_olas_week_tasks, + get_name(SynchronizedData.sleep_until): payload[ + "sleep_until" + ], + }, + ) + return self.synchronized_data, Event.DONE_MAX_RETRIES + + synchronized_data = self.synchronized_data.update( + synchronized_data_class=SynchronizedData, + **{ + get_name(SynchronizedData.api_retries): api_retries, + get_name(SynchronizedData.sleep_until): payload["sleep_until"], + }, + ) + return synchronized_data, Event.API_ERROR + + # Happy path + weekly_tweets = payload["tweets"] + performed_olas_week_tasks["retrieve_tweets"] = Event.DONE.value + + updates = { + get_name(SynchronizedData.weekly_tweets): weekly_tweets, + get_name(SynchronizedData.number_of_tweets_pulled_today): payload[ + "number_of_tweets_pulled_today" + ], + get_name(SynchronizedData.last_tweet_pull_window_reset): payload[ + "last_tweet_pull_window_reset" + ], + get_name( + SynchronizedData.performed_olas_week_tasks + ): performed_olas_week_tasks, + get_name(SynchronizedData.sleep_until): payload["sleep_until"], + } + + synchronized_data = self.synchronized_data.update( + synchronized_data_class=SynchronizedData, + **updates, + ) + 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 OlasWeekEvaluationRound(OnlyKeeperSendsRound): + """OlasWeekEvaluationRound""" + + payload_class = OlasWeekEvaluationPayload + synchronized_data_class = SynchronizedData + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Enum]]: + """Process the end of the block.""" + if self.keeper_payload is None: + return None + + payload = json.loads( + cast(OlasWeekEvaluationPayload, self.keeper_payload).content + ) + + performed_olas_week_tasks = cast( + SynchronizedData, self.synchronized_data + ).performed_olas_week_tasks + performed_olas_week_tasks["evaluate"] = Event.DONE.value + + synchronized_data = self.synchronized_data.update( + synchronized_data_class=SynchronizedData, + **{ + get_name(SynchronizedData.summary_tweets): payload["summary_tweets"], + get_name( + SynchronizedData.performed_olas_week_tasks + ): performed_olas_week_tasks, + }, + ) + + return synchronized_data, Event.DONE + + +class OlasWeekRandomnessRound(CollectSameUntilThresholdRound): + """A round for generating randomness""" + + payload_class = OlasWeekRandomnessPayload + 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 OlasWeekSelectKeepersRound(CollectSameUntilThresholdRound): + """A round in which a keeper is selected for transaction submission""" + + payload_class = OlasWeekSelectKeepersPayload + synchronized_data_class = SynchronizedData + + def end_block(self) -> Optional[Tuple[BaseSynchronizedData, Event]]: + """Process the end of the block.""" + if self.threshold_reached: + + performed_olas_week_tasks = cast( + SynchronizedData, self.synchronized_data + ).performed_olas_week_tasks + performed_olas_week_tasks["select_keepers"] = Event.DONE.value + + keepers = json.loads(self.most_voted_payload) + + synchronized_data = self.synchronized_data.update( + synchronized_data_class=SynchronizedData, + **{ + get_name(SynchronizedData.most_voted_keeper_addresses): keepers, + get_name(SynchronizedData.most_voted_keeper_address): keepers[ + 0 + ], # we also set this for reusing the keeper + get_name( + SynchronizedData.performed_olas_week_tasks + ): performed_olas_week_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 FinishedWeekInOlasRound(DegenerateRound): + """FinishedWeekInOlasRound""" + + +class WeekInOlasAbciApp(AbciApp[Event]): + """WeekInOlasAbciApp""" + + initial_round_cls: AppState = OlasWeekDecisionMakingRound + initial_states: Set[AppState] = {OlasWeekDecisionMakingRound} + transition_function: AbciAppTransitionFunction = { + OlasWeekDecisionMakingRound: { + Event.OPENAI_CALL_CHECK: OlasWeekOpenAICallCheckRound, + Event.DONE_SKIP: FinishedWeekInOlasRound, + Event.SELECT_KEEPERS: OlasWeekRandomnessRound, + Event.RETRIEVE_TWEETS: OlasWeekTweetCollectionRound, + Event.EVALUATE: OlasWeekEvaluationRound, + Event.DONE: FinishedWeekInOlasRound, + Event.ROUND_TIMEOUT: OlasWeekDecisionMakingRound, + Event.NO_MAJORITY: OlasWeekDecisionMakingRound, + }, + OlasWeekOpenAICallCheckRound: { + Event.DONE: OlasWeekDecisionMakingRound, + Event.NO_ALLOWANCE: OlasWeekDecisionMakingRound, + Event.NO_MAJORITY: OlasWeekOpenAICallCheckRound, + Event.ROUND_TIMEOUT: OlasWeekOpenAICallCheckRound, + }, + OlasWeekRandomnessRound: { + Event.DONE: OlasWeekSelectKeepersRound, + Event.NO_MAJORITY: OlasWeekRandomnessRound, + Event.ROUND_TIMEOUT: OlasWeekRandomnessRound, + }, + OlasWeekSelectKeepersRound: { + Event.DONE: OlasWeekDecisionMakingRound, + Event.NO_MAJORITY: OlasWeekRandomnessRound, + Event.ROUND_TIMEOUT: OlasWeekRandomnessRound, + }, + OlasWeekTweetCollectionRound: { + Event.DONE: OlasWeekDecisionMakingRound, + Event.DONE_MAX_RETRIES: OlasWeekDecisionMakingRound, + Event.DONE_API_LIMITS: OlasWeekDecisionMakingRound, + Event.API_ERROR: OlasWeekTweetCollectionRound, + Event.NO_MAJORITY: OlasWeekRandomnessRound, + Event.ROUND_TIMEOUT: OlasWeekRandomnessRound, + }, + OlasWeekEvaluationRound: { + Event.DONE: OlasWeekDecisionMakingRound, + Event.TWEET_EVALUATION_ROUND_TIMEOUT: OlasWeekEvaluationRound, + }, + FinishedWeekInOlasRound: {}, + } + final_states: Set[AppState] = { + FinishedWeekInOlasRound, + } + event_to_timeout: EventToTimeout = { + Event.ROUND_TIMEOUT: 30.0, + Event.TWEET_EVALUATION_ROUND_TIMEOUT: 600.0, + } + cross_period_persisted_keys: FrozenSet[str] = frozenset( + ["ceramic_db", "pending_write"] + ) + db_pre_conditions: Dict[AppState, Set[str]] = { + OlasWeekDecisionMakingRound: set(), + } + db_post_conditions: Dict[AppState, Set[str]] = { + FinishedWeekInOlasRound: { + get_name(SynchronizedData.ceramic_db), + }, + } diff --git a/packages/valory/skills/olas_week_abci/skill.yaml b/packages/valory/skills/olas_week_abci/skill.yaml new file mode 100644 index 00000000..08b16d95 --- /dev/null +++ b/packages/valory/skills/olas_week_abci/skill.yaml @@ -0,0 +1,170 @@ +name: olas_week_abci +author: valory +version: 0.1.0 +type: skill +description: A skill that weekly tweets from a Twitter account and creates a summary + using a LLM. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + __init__.py: bafybeibvgxh4baqhxapbbryg7qlglf4czricwqqs34gjs5vwwppg5nuvsq + behaviours.py: bafybeicemkdzirevjo2vkuay4zrb2scfaosrw2wixt2vcmk7ewufjtofla + ceramic_db.py: bafybeicusdonrdq6kirgkpdqmi3a6kmeal4nctnm5ozjqf5s5se6jpitjm + dialogues.py: bafybeia7zebqkgbubzenskcrgjocgyd2pc3q5yxcjddte4emys32qt6xpm + fsm_specification.yaml: bafybeienycmc4m3wurhlsuj7hfumrsx5id3qfdn6eisoze4gdzeeojlrbu + handlers.py: bafybeialoqm2byetvqqrjujhk3qzgdgsrzmhakmzwdop2cr24kswipl6ti + models.py: bafybeihsqzu4n2r5a4wlvcpmpnwkbtp3sr7axry53to5gpxoacqbkhx5cq + payloads.py: bafybeietg3ceghstjx5bytqv2t377k5pyww3cdq7zg7d7lxhscwmfelai4 + prompts.py: bafybeicz63x6rhrgm5amjkfsovnqvxqyzicfhhae3orw7jaubme2npnn2a + rounds.py: bafybeihy72ld66pzdamubpuj6obtbnr4tbzsxl4axb36maw4eru6bb66em +fingerprint_ignore_patterns: [] +connections: +- valory/openai:0.1.0:bafybeibn42k4zebenmjsumkngfxllxbofkwdno2fm65o6zf5y2uihmo2dq +contracts: [] +protocols: +- valory/llm:1.0.0:bafybeigqybmg75vsxexmp57hkms7lkp7iwpf54r7wpygizxryvrhfqqpb4 +skills: +- valory/abstract_round_abci:0.1.0:bafybeih2fyfb6kkf7r45pvdk7pyyebr5xloia4xiqxtb3qsrasnstqmepq +behaviours: + main: + args: {} + class_name: OlasWeekRoundBehaviour +handlers: + abci: + args: {} + class_name: ABCIRoundHandler + contract_api: + args: {} + class_name: ContractApiHandler + http: + args: {} + class_name: HttpHandler + ledger_api: + args: {} + class_name: LedgerApiHandler + llm: + args: {} + class_name: LlmHandler + 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 + ledger_api_dialogues: + args: {} + class_name: LedgerApiDialogues + llm_dialogues: + args: {} + class_name: LlmDialogues + params: + args: + multisend_address: '0x0000000000000000000000000000000000000000' + termination_sleep: 900 + 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 + keeper_allowed_retries: 3 + keeper_timeout: 30.0 + max_attempts: 10 + max_healthcheck: 120 + reset_pause_duration: 10 + on_chain_service_id: null + request_retry_delay: 1.0 + request_timeout: 10.0 + reset_tendermint_after: 2 + retry_attempts: 400 + retry_timeout: 3 + round_timeout_seconds: 30.0 + tweet_evaluation_round_timeout: 600.0 + service_id: score_read + 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 + twitter_api_base: https://api.twitter.com/ + twitter_api_bearer_token: + twitter_tweets_endpoint: 2/users/1450081635559428107/tweets? + twitter_tweets_args: tweet.fields=author_id,created_at&user.fields=name&expansions=author_id&max_results=50&start_time={start_time} + twitter_max_pages: 1 + max_tweet_pulls_allowed: 80 + openai_call_window_size: 3600.0 + openai_calls_allowed_in_window: 100 + max_points_per_period: 5000 + validate_timeout: 1205 + use_termination: false + use_slashing: false + slash_cooldown_hours: 3 + slash_threshold_amount: 10000000000000000 + light_slash_unit_amount: 5000000000000000 + serious_slash_unit_amount: 8000000000000000 + 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 + signing_dialogues: + args: {} + class_name: SigningDialogues + state: + args: {} + class_name: SharedState + tendermint_dialogues: + args: {} + class_name: TendermintDialogues +dependencies: + open-aea-web3: + version: ==6.0.1 +is_abstract: true diff --git a/packages/valory/skills/twitter_scoring_abci/behaviours.py b/packages/valory/skills/twitter_scoring_abci/behaviours.py index 4ddb4387..0dd9fc99 100644 --- a/packages/valory/skills/twitter_scoring_abci/behaviours.py +++ b/packages/valory/skills/twitter_scoring_abci/behaviours.py @@ -88,6 +88,15 @@ HTTP_TOO_MANY_REQUESTS = 429 +def extract_headers(header_str: str) -> dict: + """Extracts HTTP headers""" + header_separator = "\r\n" if "\r\n" in header_str else "\n" + headers = [ + header.split(": ") for header in header_str.split(header_separator) if header + ] + return {key: value for key, value in headers} + + class TwitterScoringBaseBehaviour(BaseBehaviour, ABC): """Base behaviour for the common apps' skill.""" @@ -470,13 +479,8 @@ def _get_twitter_mentions( ) # Check response status - if response.status_code != HTTP_OK: - headers = [ - header.split(": ") - for header in response.headers.split("\r\n") - if header - ] - header_dict = {key: value for key, value in headers} + if response.status_code != 200: + header_dict = extract_headers(response.headers) remaining, limit, reset_ts = [ header_dict.get(header, "?") @@ -733,12 +737,7 @@ def _get_twitter_hashtag_search( # Check response status if response.status_code != 200: - headers = [ - header.split(": ") - for header in response.headers.split("\r\n") - if header - ] - header_dict = {key: value for key, value in headers} + header_dict = extract_headers(response.headers) remaining, limit, reset_ts = [ header_dict.get(header, "?") diff --git a/packages/valory/skills/twitter_scoring_abci/models.py b/packages/valory/skills/twitter_scoring_abci/models.py index c10d8dcd..83a0e376 100644 --- a/packages/valory/skills/twitter_scoring_abci/models.py +++ b/packages/valory/skills/twitter_scoring_abci/models.py @@ -82,31 +82,21 @@ class Params(BaseParams): def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the parameters object.""" - self.twitter_api_base = self._ensure("twitter_api_base", kwargs, str) - self.twitter_api_bearer_token = self._ensure( - "twitter_api_bearer_token", kwargs, str + self.twitter_api_base = kwargs.get("twitter_api_base") + self.twitter_api_bearer_token = kwargs.get("twitter_api_bearer_token") + self.twitter_mentions_endpoint = kwargs.get("twitter_mentions_endpoint") + self.twitter_mentions_args = kwargs.get("twitter_mentions_args") + self.twitter_max_pages = kwargs.get("twitter_max_pages") + self.twitter_search_endpoint = kwargs.get("twitter_search_endpoint") + self.twitter_search_args = kwargs.get("twitter_search_args") + self.max_points_per_period = kwargs.get("max_points_per_period") + self.tweet_evaluation_round_timeout = kwargs.get( + "tweet_evaluation_round_timeout" ) - self.twitter_mentions_endpoint = self._ensure( - "twitter_mentions_endpoint", kwargs, str - ) - self.twitter_mentions_args = self._ensure("twitter_mentions_args", kwargs, str) - self.twitter_max_pages = self._ensure("twitter_max_pages", kwargs, int) - self.twitter_search_endpoint = self._ensure( - "twitter_search_endpoint", kwargs, str - ) - self.twitter_search_args = self._ensure("twitter_search_args", kwargs, str) - self.max_points_per_period = self._ensure("max_points_per_period", kwargs, int) - self.tweet_evaluation_round_timeout = self._ensure( - "tweet_evaluation_round_timeout", kwargs, float - ) - self.max_tweet_pulls_allowed = self._ensure( - "max_tweet_pulls_allowed", kwargs, int - ) - self.openai_call_window_size = self._ensure( - "openai_call_window_size", kwargs, float - ) - self.openai_calls_allowed_in_window = self._ensure( - "openai_calls_allowed_in_window", kwargs, int + self.max_tweet_pulls_allowed = kwargs.get("max_tweet_pulls_allowed") + self.openai_call_window_size = kwargs.get("openai_call_window_size") + self.openai_calls_allowed_in_window = kwargs.get( + "openai_calls_allowed_in_window" ) self.openai_calls = OpenAICalls( openai_call_window_size=self.openai_call_window_size, diff --git a/packages/valory/skills/twitter_scoring_abci/skill.yaml b/packages/valory/skills/twitter_scoring_abci/skill.yaml index ad493b9d..723cd731 100644 --- a/packages/valory/skills/twitter_scoring_abci/skill.yaml +++ b/packages/valory/skills/twitter_scoring_abci/skill.yaml @@ -8,12 +8,12 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeifudgakkjoyahuewp2o4gvqayw7nsgpyxw2ayrpgmzexurh2xomaq - behaviours.py: bafybeice3hmo4pz6rmrtaxqhvlgavrn4n6ns456xjqjmgy4zfp7wdejk2u + behaviours.py: bafybeif7x53ygd3jyoghbnxavzhhdhliaa3mcxe5n5uui3aa6vgd6v5t5e ceramic_db.py: bafybeicusdonrdq6kirgkpdqmi3a6kmeal4nctnm5ozjqf5s5se6jpitjm dialogues.py: bafybeibdqzn37hbo2cq4skww4uh2zvvsjyaxxvdhxisefbdvmjp7rh53si fsm_specification.yaml: bafybeie6k4aanmwcrghspbqywdh2srrudtkhnbbhqm3ovcz5k4fa5dybrq handlers.py: bafybeid3nqvcyotqj5g5hlgrz57nf7vpjysmgvsxe3p7644f4z5dcwqn6u - models.py: bafybeiajis7l5sv7b3fofuj3ehxai5d2uy6h6p4kabhrekjwkjs77lopxe + models.py: bafybeigag7tmzi7caob46mowxnqhhcpyxqqxhchj4mqyjmgtcglx45dyua payloads.py: bafybeibeqiwnua7uewbv5a7epebshjpueuqpcbw6s2y3u62kasdhiijs5i prompts.py: bafybeieiuqn427bgwfnzynxf3vtqfpvmqqscs5tyw4oibfofwropifotke rounds.py: bafybeigtzsvklrwxyah2syrzjruufr6uuninyh6flbrwetcgcb5pl4czra diff --git a/schemas/centaurs_stream_schema.json b/schemas/centaurs_stream_schema.json index d36fbb71..b50212da 100644 --- a/schemas/centaurs_stream_schema.json +++ b/schemas/centaurs_stream_schema.json @@ -103,6 +103,25 @@ "required": ["daily", "enabled", "last_run", "run_hour_utc"], "additionalProperties": false }, + "week_in_olas_config": { + "type": "object", + "properties": { + "weekly": { + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "last_run": { + "type": ["integer", "null"] + }, + "run_hour_utc": { + "type": "integer" + } + }, + "required": ["weekly", "enabled", "last_run", "run_hour_utc"], + "additionalProperties": false + }, "scheduled_tweet_data": { "type": "object", "properties": { @@ -198,9 +217,12 @@ }, "daily_orbis": { "$ref": "#/definitions/daily_orbis_config" + }, + "week_in_olas": { + "$ref": "#/definitions/week_in_olas_config" } }, - "required": ["daily_tweet", "scheduled_tweet", "daily_orbis"], + "required": ["daily_tweet", "scheduled_tweet", "daily_orbis", "week_in_olas"], "additionalProperties": false }, "memberWhitelist": { diff --git a/tox.ini b/tox.ini index 4bc8b124..a8e231a5 100644 --- a/tox.ini +++ b/tox.ini @@ -241,7 +241,8 @@ commands = black \ {env:SKILLS_PATHS}/impact_evaluator_abci \ {env:SKILLS_PATHS}/decision_making_abci \ {env:SKILLS_PATHS}/llm_abci \ - {env:SKILLS_PATHS}/twitter_write_abci + {env:SKILLS_PATHS}/twitter_write_abci \ + {env:SKILLS_PATHS}/olas_week_abci [testenv:black-check] skipsdist = True @@ -257,7 +258,8 @@ commands = black --check \ {env:SKILLS_PATHS}/impact_evaluator_abci \ {env:SKILLS_PATHS}/decision_making_abci \ {env:SKILLS_PATHS}/llm_abci \ - {env:SKILLS_PATHS}/twitter_write_abci + {env:SKILLS_PATHS}/twitter_write_abci \ + {env:SKILLS_PATHS}/olas_week_abci [testenv:isort] skipsdist = True @@ -305,7 +307,8 @@ commands = flake8 \ {env:SKILLS_PATHS}/impact_evaluator_abci \ {env:SKILLS_PATHS}/decision_making_abci \ {env:SKILLS_PATHS}/llm_abci \ - {env:SKILLS_PATHS}/twitter_write_abci + {env:SKILLS_PATHS}/twitter_write_abci \ + {env:SKILLS_PATHS}/olas_week_abci [testenv:mypy] skipsdist = True @@ -323,6 +326,7 @@ commands = mypy --disallow-untyped-defs \ {env:SKILLS_PATHS}/decision_making_abci \ {env:SKILLS_PATHS}/llm_abci \ {env:SKILLS_PATHS}/twitter_write_abci \ + {env:SKILLS_PATHS}/olas_week_abci \ --config-file tox.ini [testenv:pylint] @@ -344,6 +348,7 @@ commands = pylint \ {env:SKILLS_PATHS}/decision_making_abci \ {env:SKILLS_PATHS}/llm_abci \ {env:SKILLS_PATHS}/twitter_write_abci \ + {env:SKILLS_PATHS}/olas_week_abci \ scripts [testenv:safety] @@ -366,7 +371,8 @@ commands = darglint \ {env:SKILLS_PATHS}/impact_evaluator_abci/* \ {env:SKILLS_PATHS}/decision_making_abci/* \ {env:SKILLS_PATHS}/llm_abci/* \ - {env:SKILLS_PATHS}/twitter_write_abci/* + {env:SKILLS_PATHS}/twitter_write_abci/* \ + {env:SKILLS_PATHS}/olas_week_abci/* [testenv:check-generate-all-protocols] skipsdist = True