Skip to content

Commit

Permalink
feat: Added main business logic
Browse files Browse the repository at this point in the history
  • Loading branch information
jmoreira-valory committed Jul 10, 2023
1 parent 29764ee commit d9c527e
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 15 deletions.
4 changes: 3 additions & 1 deletion packages/packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
}
}
Binary file not shown.
Binary file not shown.
210 changes: 207 additions & 3 deletions packages/valory/skills/market_creation_manager_abci/behaviours.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,90 @@
"""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,
)
from packages.valory.skills.abstract_round_abci.common import (
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."""
Expand Down Expand Up @@ -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)
Expand All @@ -88,24 +146,170 @@ 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)
yield from self.wait_until_round_end()

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"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,20 @@ start_states:
states:
- CollectRandomnessRound
- DataGatheringRound
- SelectKeeperRound
- MarketIdentificationRound
- PrepareTransactionRound
- FinishedMarketCreationManagerRound
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
19 changes: 18 additions & 1 deletion packages/valory/skills/market_creation_manager_abci/rounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from packages.valory.skills.market_creation_manager_abci.payloads import (
CollectRandomnessPayload,
DataGatheringPayload,
SelectKeeperPayload,
MarketIdentificationPayload,
PrepareTransactionPayload,
)
Expand All @@ -48,6 +49,7 @@ class Event(Enum):
NO_MAJORITY = "no_majority"
DONE = "done"
ROUND_TIMEOUT = "round_timeout"
API_ERROR = "api_error"


class SynchronizedData(BaseSynchronizedData):
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit d9c527e

Please sign in to comment.