diff --git a/packages/packages.json b/packages/packages.json index 74ce774..b6ca020 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -17,6 +17,7 @@ "protocol/valory/tendermint/0.1.0": "bafybeicusvezoqlmyt6iqomcbwaz3xkhk2qf3d56q5zprmj3xdxfy64k54", "connection/valory/ledger/0.19.0": "bafybeicgfupeudtmvehbwziqfxiz6ztsxr5rxzvalzvsdsspzz73o5fzfi", "connection/valory/http_client/0.23.0": "bafybeidykl4elwbcjkqn32wt5h4h7tlpeqovrcq3c5bcplt6nhpznhgczi", + "connection/valory/openai/0.1.0": "bafybeicqubow644xihuov2vypmmdd4zf6jvvbjb4f2w6hhekxrhb4chjaa", "contract/valory/service_registry/0.1.0": "bafybeibdy55edqs3djptv77ljkmbf6m3zizhutmvwgj3hpsagvmzhr4jbm", "contract/valory/gnosis_safe/0.1.0": "bafybeif5fdwoxq5mscrurtuimadmtctyxxeeui45u4g6leqobzls7bsl3u", "contract/valory/gnosis_safe_proxy_factory/0.1.0": "bafybeiaa6fgwtykrti6i7sbt22raavpsbobsq2xgem4nkbcg744agnmkae", @@ -26,6 +27,7 @@ "skill/valory/transaction_settlement_abci/0.1.0": "bafybeiapapnnfwuto7gu2izdyciir7tbegktizccoe5ya63gp2gyt4p4ki", "skill/valory/registration_abci/0.1.0": "bafybeift43mkwgakbx4j76lrp6ixqgk33wgy6wti7uoq2evny6h2pewkti", "skill/valory/reset_pause_abci/0.1.0": "bafybeibs25kzoyxcxdnczdvvhnllratimhwuuytgu4zybzkmpglkice4um", - "skill/valory/termination_abci/0.1.0": "bafybeiabozz2ta2hizq5u2lnkrp3v345unb6xug7ncls6ad4st57lg5seq" + "skill/valory/termination_abci/0.1.0": "bafybeiabozz2ta2hizq5u2lnkrp3v345unb6xug7ncls6ad4st57lg5seq", + "protocol/valory/llm/1.0.0": "bafybeigafqnus3kvaashfdzrpybpqiumhvbs2dmdnukqzn7yqedga4x7ui" } } \ No newline at end of file diff --git a/packages/valory/skills/market_creation_manager_abci/__pycache__/payloads.cpython-310.pyc b/packages/valory/skills/market_creation_manager_abci/__pycache__/payloads.cpython-310.pyc index 34f6378..0900bf9 100644 Binary files a/packages/valory/skills/market_creation_manager_abci/__pycache__/payloads.cpython-310.pyc and b/packages/valory/skills/market_creation_manager_abci/__pycache__/payloads.cpython-310.pyc differ diff --git a/packages/valory/skills/market_creation_manager_abci/__pycache__/rounds.cpython-310.pyc b/packages/valory/skills/market_creation_manager_abci/__pycache__/rounds.cpython-310.pyc index dcc42b3..09a39f7 100644 Binary files a/packages/valory/skills/market_creation_manager_abci/__pycache__/rounds.cpython-310.pyc and b/packages/valory/skills/market_creation_manager_abci/__pycache__/rounds.cpython-310.pyc differ diff --git a/packages/valory/skills/market_creation_manager_abci/behaviours.py b/packages/valory/skills/market_creation_manager_abci/behaviours.py index e544f8c..cf0fe1d 100644 --- a/packages/valory/skills/market_creation_manager_abci/behaviours.py +++ b/packages/valory/skills/market_creation_manager_abci/behaviours.py @@ -20,26 +20,40 @@ """This package contains round behaviours of MarketCreationManagerAbciApp.""" from abc import ABC +import datetime +import json +import random from typing import Generator, Set, Type, cast +from packages.valory.protocols.llm.message import LlmMessage +from packages.valory.skills.llm_abci.dialogues import LlmDialogue, LlmDialogues +from packages.valory.skills.abstract_round_abci.models import Requests + + from packages.valory.skills.abstract_round_abci.base import AbstractRound from packages.valory.skills.abstract_round_abci.behaviours import ( AbstractRoundBehaviour, + SelectKeeperBehaviour, BaseBehaviour, ) +from packages.valory.connections.openai.connection import ( + PUBLIC_ID as LLM_CONNECTION_PUBLIC_ID, +) from packages.valory.skills.market_creation_manager_abci.models import MarketCreationManagerParams from packages.valory.skills.market_creation_manager_abci.rounds import ( SynchronizedData, MarketCreationManagerAbciApp, CollectRandomnessRound, DataGatheringRound, + SelectKeeperRound, MarketIdentificationRound, PrepareTransactionRound, ) from packages.valory.skills.market_creation_manager_abci.rounds import ( CollectRandomnessPayload, DataGatheringPayload, + SelectKeeperPayload, MarketIdentificationPayload, PrepareTransactionPayload, ) @@ -47,6 +61,49 @@ RandomnessBehaviour ) +HTTP_OK = 200 +MAX_RETRIES = 3 + +MARKET_IDENTIFICATION_PROMPT = """ +You are an LLM inside a multi-agent system. Your task is to propose a collection of prediction market questions based +on your input. Your input is under the label "INPUT". You must follow the instructions under "INSTRUCTIONS". +You must provide your response in the format specified under "OUTPUT_FORMAT". + +INSTRUCTIONS +* Read the input under the label "INPUT" delimited by three backticks. +* The "INPUT" specifies a list of recent news headlines, their date, and short descriptions. +* Based on the "INPUT" and your training data, you must provide a list of binary questions, valid answers and resolution dates to create prediction markets. + Each market must satisfy the following conditions: + - The outcome of the market is unknown at the present date. + - The outcome of the market must be known by its resolution date. + - The outcome of the market must be related to a deterministic, measurable or verifiable fact. + - Questions whose answer is known at the present date are invalid. + - Questions whose answer is subjective or opinionated are invalid. + - Questions with relative dates are invalid. + - Questions about moral values, subjective opinions and not facts are invalid. + - Questions in which none of the answers are valid will resolve as invalid. + - Questions with multiple valid answers are invalid. + - Questions must not incentive to commit an immoral violent action. +* The created markets must be different and not overlap semantically. +* You must provide your response in the format specified under "OUTPUT_FORMAT". +* Do not include any other contents in your response. + +INPUT: +``` +{input_news} +``` + +OUTPUT_FORMAT: +* Your output response must be only a single JSON array to be parsed by Python's "json.loads()". +* The JSON array must be of length 10. +* Each entry of the JSON array must be a JSON object containing the fields: + - question: The binary question to open a prediction market. + - answers: The possible answers to the question. + - resolution_date: The resolution date for the outcome of the market to be verified. +* Output only the JSON object. Do not include any other contents in your response. +""" + + class MarketCreationManagerBaseBehaviour(BaseBehaviour, ABC): """Base behaviour for the market_creation_manager_abci skill.""" @@ -79,7 +136,8 @@ def async_act(self) -> Generator: with self.context.benchmark_tool.measure(self.behaviour_id).local(): sender = self.context.agent_address - payload = DataGatheringPayload(sender=sender, content="DataGatheringPayloadContent") + gathered_data = yield from self._gather_data() + payload = DataGatheringPayload(sender=sender, gathered_data=gathered_data) with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): yield from self.send_a2a_transaction(payload) @@ -88,17 +146,95 @@ def async_act(self) -> Generator: self.set_done() + def _gather_data(self) -> Generator: + """Auxiliary method to collect data from endpoint.""" + + headers = { + 'X-Api-Key': self.params.newsapi_api_key + } + + today = datetime.date.today() + from_date = today - datetime.timedelta(days=7) + to_date = today + + parameters = { + "q": "arts OR business OR finance OR cryptocurrency OR politics OR science OR technology OR sports OR weather OR entertainment", + "language": "en", + "sortBy": "popularity", + "from": from_date, + "to": to_date, + } + + response = yield from self.get_http_response( + method="GET", + url=self.params.newsapi_endpoint, + headers=headers, + parameters=parameters + ) + + if response.status_code != HTTP_OK: + self.context.logger.error( + f"Could not retrieve response from {self.params.newsapi_endpoint}." + f"Received status code {response.status_code}.\n{response}" + ) + #retries = self.synchronized_data.newsapi_endpoint_retries + 1 + # TODO Error handling. + return "{ }" + + return response.json() + + +class SelectKeeperOracleBehaviour(SelectKeeperBehaviour): + """Select the keeper agent.""" + + matching_round = SelectKeeperRound + payload_class = SelectKeeperPayload + class MarketIdentificationBehaviour(MarketCreationManagerBaseBehaviour): """MarketIdentificationBehaviour""" matching_round: Type[AbstractRound] = MarketIdentificationRound - def async_act(self) -> Generator: + def _i_am_not_sending(self) -> bool: + """Indicates if the current agent is 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: {self.synchronized_data.most_voted_keeper_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(): + payload_data = yield from self._get_llm_response() sender = self.context.agent_address - payload = MarketIdentificationPayload(sender=sender, content="MarketIdentificationPayloadContent") + payload = MarketIdentificationPayload( + 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) @@ -106,6 +242,74 @@ def async_act(self) -> Generator: self.set_done() + def _get_llm_response(self) -> Generator[None, None, dict]: + """Get the LLM response""" + + articles = self.synchronized_data.gathered_data['articles'] + random.seed(self.synchronized_data.most_voted_randomness, 2) # nosec + random.shuffle(articles) + articles = articles[:20] + + input_news = '' + for article in articles: + title = article['title'] + content = article['content'] + date = article['publishedAt'] + input_news += f"- ({date}) {title}\n {content}\n\n" + + + prompt_template = MARKET_IDENTIFICATION_PROMPT + prompt_values = {input_news: input_news} + + self.context.logger.info( + f"Sending LLM request...\nprompt_template={prompt_template}\nprompt_values={prompt_values}" + ) + + 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=prompt_template, + prompt_values=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 + ) + result = llm_response_message.value + + self.context.logger.info(f"Got LLM response: {result}") + + return {"result": result.strip()} + + 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 PrepareTransactionBehaviour(MarketCreationManagerBaseBehaviour): """PrepareTransactionBehaviour""" diff --git a/packages/valory/skills/market_creation_manager_abci/fsm_specification.yaml b/packages/valory/skills/market_creation_manager_abci/fsm_specification.yaml index 3ae17e0..675ed21 100644 --- a/packages/valory/skills/market_creation_manager_abci/fsm_specification.yaml +++ b/packages/valory/skills/market_creation_manager_abci/fsm_specification.yaml @@ -11,6 +11,7 @@ start_states: states: - CollectRandomnessRound - DataGatheringRound +- SelectKeeperRound - MarketIdentificationRound - PrepareTransactionRound - FinishedMarketCreationManagerRound @@ -18,9 +19,12 @@ transition_func: (CollectRandomnessRound, DONE): DataGatheringRound (CollectRandomnessRound, NO_MAJORITY): CollectRandomnessRound (CollectRandomnessRound, ROUND_TIMEOUT): CollectRandomnessRound - (DataGatheringRound, DONE): MarketIdentificationRound + (DataGatheringRound, DONE): SelectKeeperRound (DataGatheringRound, NO_MAJORITY): CollectRandomnessRound (DataGatheringRound, ROUND_TIMEOUT): CollectRandomnessRound + (SelectKeeperRound, DONE): MarketIdentificationRound + (SelectKeeperRound, NO_MAJORITY): CollectRandomnessRound + (SelectKeeperRound, ROUND_TIMEOUT): CollectRandomnessRound (MarketIdentificationRound, DONE): PrepareTransactionRound (MarketIdentificationRound, NO_MAJORITY): CollectRandomnessRound (MarketIdentificationRound, ROUND_TIMEOUT): CollectRandomnessRound diff --git a/packages/valory/skills/market_creation_manager_abci/models.py b/packages/valory/skills/market_creation_manager_abci/models.py index 2d6e6b2..a996b58 100644 --- a/packages/valory/skills/market_creation_manager_abci/models.py +++ b/packages/valory/skills/market_creation_manager_abci/models.py @@ -45,7 +45,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize the parameters object.""" self.newsapi_api_key = kwargs.get("newsapi_api_key") - + self.newsapi_endpoint = kwargs.get("newsapi_endpoint") super().__init__(*args, **kwargs) class RandomnessApi(ApiSpecs): diff --git a/packages/valory/skills/market_creation_manager_abci/payloads.py b/packages/valory/skills/market_creation_manager_abci/payloads.py index ddbc7ba..5f1eb82 100644 --- a/packages/valory/skills/market_creation_manager_abci/payloads.py +++ b/packages/valory/skills/market_creation_manager_abci/payloads.py @@ -36,8 +36,13 @@ class CollectRandomnessPayload(BaseTxPayload): class DataGatheringPayload(BaseTxPayload): """Represent a transaction payload for the DataGatheringRound.""" - content: str + gathered_data: str + +@dataclass(frozen=True) +class SelectKeeperPayload(BaseTxPayload): + """Represent a transaction payload of type 'select_keeper'.""" + keeper: str @dataclass(frozen=True) class MarketIdentificationPayload(BaseTxPayload): diff --git a/packages/valory/skills/market_creation_manager_abci/rounds.py b/packages/valory/skills/market_creation_manager_abci/rounds.py index 4b863d9..c696729 100644 --- a/packages/valory/skills/market_creation_manager_abci/rounds.py +++ b/packages/valory/skills/market_creation_manager_abci/rounds.py @@ -37,6 +37,7 @@ from packages.valory.skills.market_creation_manager_abci.payloads import ( CollectRandomnessPayload, DataGatheringPayload, + SelectKeeperPayload, MarketIdentificationPayload, PrepareTransactionPayload, ) @@ -48,6 +49,7 @@ class Event(Enum): NO_MAJORITY = "no_majority" DONE = "done" ROUND_TIMEOUT = "round_timeout" + API_ERROR = "api_error" class SynchronizedData(BaseSynchronizedData): @@ -79,6 +81,16 @@ class DataGatheringRound(CollectSameUntilThresholdRound): no_majority_event = Event.NO_MAJORITY +class SelectKeeperRound(CollectSameUntilThresholdRound): + """A round in a which keeper is selected""" + + payload_class = SelectKeeperPayload + synchronized_data_class = SynchronizedData + done_event = Event.DONE + no_majority_event = Event.NO_MAJORITY + collection_key = get_name(SynchronizedData.participant_to_selection) + selection_key = get_name(SynchronizedData.most_voted_keeper_address) + class MarketIdentificationRound(CollectSameUntilThresholdRound): """MarketIdentificationRound""" @@ -117,10 +129,15 @@ class MarketCreationManagerAbciApp(AbciApp[Event]): Event.ROUND_TIMEOUT: CollectRandomnessRound }, DataGatheringRound: { - Event.DONE: MarketIdentificationRound, + Event.DONE: SelectKeeperRound, Event.NO_MAJORITY: CollectRandomnessRound, Event.ROUND_TIMEOUT: CollectRandomnessRound }, + SelectKeeperRound: { + Event.DONE: MarketIdentificationRound, + Event.NO_MAJORITY: CollectRandomnessRound, + Event.ROUND_TIMEOUT: CollectRandomnessRound + }, MarketIdentificationRound: { Event.DONE: PrepareTransactionRound, Event.NO_MAJORITY: CollectRandomnessRound, diff --git a/packages/valory/skills/market_creation_manager_abci/skill.yaml b/packages/valory/skills/market_creation_manager_abci/skill.yaml index 90260ba..5c5ab88 100644 --- a/packages/valory/skills/market_creation_manager_abci/skill.yaml +++ b/packages/valory/skills/market_creation_manager_abci/skill.yaml @@ -7,13 +7,13 @@ license: Apache-2.0 aea_version: '>=1.0.0, <2.0.0' fingerprint: __init__.py: bafybeifotgt5johrebg3iaokvi5jkfiy3mk5dtkfyuersobinhzxp3ilba - behaviours.py: bafybeicrm6p2uplmvbva6burejq6je7j2c7w6hcb4l67gwgl4kiktvbwgy + behaviours.py: bafybeife4qeqoygyv5fjemzmrbqoqn2jlj7crsacksprmht7wvnxllpepe dialogues.py: bafybeignhvmpiyjyby5ls726hkeawtw6q4ob6br7bqtwf5ujk2z6mlkyiq - fsm_specification.yaml: bafybeig3ro2adoi6wd2xoxkp4dxvqe4kypatvw4nw7wrhuzbfvyp2anlca + fsm_specification.yaml: bafybeiacanirz5htb7uieurfjqbssdn7j73qkse77ype56fqxaxe34nm4e handlers.py: bafybeihteow3fq4pt23ivbdoxsnyilvyvtr5uakz42dkyltvgik5zkrshe - models.py: bafybeigqixvhbcjzlcdeueut6fq2s2da2fhnx5kr4j7ln64vyqplcyrdde - payloads.py: bafybeiezykfzgehmemkkmca5umpwush4b7rre5xrp5gbf3mv6plksmwjzi - rounds.py: bafybeigkgfscocq74yirgu7oevfg2qbo2uuyr2qotyxcdowqyxbkux335u + models.py: bafybeidpe2w3un7ds4rhlzvxgxhlitkngiwcydsp4674mhpnweazogy4ji + payloads.py: bafybeigiwevmsujskfpgyplg6ss45ckfx77i4bu63g7dnxzna2kwdhteh4 + rounds.py: bafybeieqhbzvhdmtxxssihpewibj6siqefukq5ms7cu75stsr2jk2dqhpm tests/__init__.py: bafybeic3534aro4pdtsdcuxywzlgkccczx2elhnwisiq3r6okvnrdu4aia fingerprint_ignore_patterns: [] connections: [] @@ -69,7 +69,8 @@ models: class_name: LedgerApiDialogues params: args: - newsapi_api_key: f1ea36ae6175474bbcbae11eca64d0b9 + newsapi_endpoint: https://newsapi.org/v2/everything + newsapi_api_key: 0 cleanup_history_depth: 1 cleanup_history_depth_current: null drand_public_key: 868f005eb8e6e4ca0a47c8a77ceaa5309a47978a7c71bc5cce96366b5d7a569937c529eeda66c7293784a9402801af31 diff --git a/packages/valory/skills/market_creator_abci/skill.yaml b/packages/valory/skills/market_creator_abci/skill.yaml index 7849b66..02d0145 100644 --- a/packages/valory/skills/market_creator_abci/skill.yaml +++ b/packages/valory/skills/market_creator_abci/skill.yaml @@ -25,7 +25,7 @@ protocols: - valory/ledger_api:1.0.0:bafybeibo4bdtcrxi2suyzldwoetjar6pqfzm6vt5xal22ravkkcvdmtksi skills: - valory/abstract_round_abci:0.1.0:bafybeihlcferdpjhcorbrgp3zplikxkuxmlx3x7mc4g5sjxxm2mgzryfia -- valory/market_creation_manager_abci:0.1.0:bafybeibx7rarfk3lxur3r2oqgi7ehasusrv2dvxesvclh2xxtghxmpg64i +- valory/market_creation_manager_abci:0.1.0:bafybeibdstgecwxibliw4emwspdoxg3gdtg25qi7cktkjqyzim7pv5clz4 - valory/registration_abci:0.1.0:bafybeift43mkwgakbx4j76lrp6ixqgk33wgy6wti7uoq2evny6h2pewkti - valory/reset_pause_abci:0.1.0:bafybeibs25kzoyxcxdnczdvvhnllratimhwuuytgu4zybzkmpglkice4um - valory/termination_abci:0.1.0:bafybeiabozz2ta2hizq5u2lnkrp3v345unb6xug7ncls6ad4st57lg5seq