Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to submit job results #541

Merged
merged 5 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 0 additions & 45 deletions prediction_market_agent_tooling/jobs/jobs.py

This file was deleted.

27 changes: 25 additions & 2 deletions prediction_market_agent_tooling/jobs/jobs_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@

from pydantic import BaseModel

from prediction_market_agent_tooling.markets.agent_market import AgentMarket
from prediction_market_agent_tooling.deploy.betting_strategy import ProbabilisticAnswer
from prediction_market_agent_tooling.gtypes import Probability
from prediction_market_agent_tooling.markets.agent_market import (
AgentMarket,
ProcessedTradedMarket,
)
from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import (
FilterBy,
SortBy,
Expand Down Expand Up @@ -39,10 +44,22 @@ def get_reward(self, max_bond: float) -> float:
@classmethod
@abstractmethod
def get_jobs(
cls, limit: int | None, filter_by: FilterBy, sort_by: SortBy
cls,
limit: int | None,
filter_by: FilterBy = FilterBy.OPEN,
sort_by: SortBy = SortBy.CLOSING_SOONEST,
) -> t.Sequence["JobAgentMarket"]:
"""Get all available jobs."""

@staticmethod
@abstractmethod
def get_job(id: str) -> "JobAgentMarket":
"""Get a single job by its id."""

@abstractmethod
def submit_job_result(self, max_bond: float, result: str) -> ProcessedTradedMarket:
"""Submit the completed result for this job."""

def to_simple_job(self, max_bond: float) -> SimpleJob:
return SimpleJob(
id=self.id,
Expand All @@ -51,3 +68,9 @@ def to_simple_job(self, max_bond: float) -> SimpleJob:
currency=self.currency.value,
deadline=self.deadline,
)

def get_job_answer(self, result: str) -> ProbabilisticAnswer:
# Just return 100% yes with 100% confidence, because we assume the job is completed correctly.
return ProbabilisticAnswer(
p_yes=Probability(1.0), confidence=1.0, reasoning=result
)
Comment on lines +72 to +76
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add basic validation for job result

The method assumes 100% confidence without validating the result. Consider adding basic validation to ensure the result is not empty or malformed before assuming perfect confidence.

     def get_job_answer(self, result: str) -> ProbabilisticAnswer:
+        if not result or not result.strip():
+            raise ValueError("Job result cannot be empty")
+
         # Just return 100% yes with 100% confidence, because we assume the job is completed correctly.
         return ProbabilisticAnswer(
             p_yes=Probability(1.0), confidence=1.0, reasoning=result
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def get_job_answer(self, result: str) -> ProbabilisticAnswer:
# Just return 100% yes with 100% confidence, because we assume the job is completed correctly.
return ProbabilisticAnswer(
p_yes=Probability(1.0), confidence=1.0, reasoning=result
)
def get_job_answer(self, result: str) -> ProbabilisticAnswer:
if not result or not result.strip():
raise ValueError("Job result cannot be empty")
# Just return 100% yes with 100% confidence, because we assume the job is completed correctly.
return ProbabilisticAnswer(
p_yes=Probability(1.0), confidence=1.0, reasoning=result
)

106 changes: 65 additions & 41 deletions prediction_market_agent_tooling/jobs/omen/omen_jobs.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import typing as t

from web3 import Web3

from prediction_market_agent_tooling.config import APIKeys
from prediction_market_agent_tooling.deploy.betting_strategy import (
Currency,
KellyBettingStrategy,
ProbabilisticAnswer,
TradeType,
)
from prediction_market_agent_tooling.gtypes import Probability
from prediction_market_agent_tooling.jobs.jobs_models import JobAgentMarket
from prediction_market_agent_tooling.markets.agent_market import ProcessedTradedMarket
from prediction_market_agent_tooling.markets.data_models import PlacedTrade, Trade
from prediction_market_agent_tooling.markets.omen.omen import (
BetAmount,
OmenAgentMarket,
Expand All @@ -36,11 +35,27 @@ def deadline(self) -> DatetimeUTC:
return self.close_time

def get_reward(self, max_bond: float) -> float:
return compute_job_reward(self, max_bond)
trade = self.get_job_trade(
max_bond,
result="", # Pass empty result, as we are computing only potential reward at this point.
)
reward = (
self.get_buy_token_amount(
bet_amount=BetAmount(
amount=trade.amount.amount, currency=trade.amount.currency
),
direction=trade.outcome,
).amount
- trade.amount.amount
)
return reward

@classmethod
def get_jobs(
cls, limit: int | None, filter_by: FilterBy, sort_by: SortBy
cls,
limit: int | None,
filter_by: FilterBy = FilterBy.OPEN,
sort_by: SortBy = SortBy.CLOSING_SOONEST,
) -> t.Sequence["OmenJobAgentMarket"]:
markets = OmenSubgraphHandler().get_omen_binary_markets_simple(
limit=limit,
Expand All @@ -50,6 +65,50 @@ def get_jobs(
)
return [OmenJobAgentMarket.from_omen_market(market) for market in markets]

@staticmethod
def get_job(id: str) -> "OmenJobAgentMarket":
return OmenJobAgentMarket.from_omen_agent_market(
OmenJobAgentMarket.get_binary_market(id=id)
)

def submit_job_result(self, max_bond: float, result: str) -> ProcessedTradedMarket:
if not APIKeys().enable_ipfs_upload:
raise RuntimeError(
f"ENABLE_IPFS_UPLOAD must be set to True to upload job results."
)

trade = self.get_job_trade(max_bond, result)
buy_id = self.buy_tokens(outcome=trade.outcome, amount=trade.amount)

processed_traded_market = ProcessedTradedMarket(
answer=self.get_job_answer(result),
trades=[PlacedTrade.from_trade(trade, id=buy_id)],
Comment on lines +84 to +85
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Implement the get_job_answer method

The method get_job_answer is called but not implemented in OmenJobAgentMarket. Since it's an abstract method in the base class JobAgentMarket, you need to provide an implementation in this subclass to prevent runtime errors.

Consider adding the following implementation:

def get_job_answer(self, result: str) -> bool:
    # Convert the result string to the expected answer format
    return result.lower() == "yes"

Also applies to: 84-84

)

keys = APIKeys()
self.store_trades(processed_traded_market, keys)

return processed_traded_market

def get_job_trade(self, max_bond: float, result: str) -> Trade:
# Because jobs are powered by prediction markets, potentional reward depends on job's liquidity and our will to bond (bet) our xDai into our job completion.
strategy = KellyBettingStrategy(max_bet_amount=max_bond)
required_trades = strategy.calculate_trades(
existing_position=None,
answer=self.get_job_answer(result),
market=self,
)
assert (
len(required_trades) == 1
), f"Shouldn't process same job twice: {required_trades}"
trade = required_trades[0]
assert trade.trade_type == TradeType.BUY, "Should only buy on job markets."
assert trade.outcome, "Should buy only YES on job markets."
assert (
trade.amount.currency == Currency.xDai
Comment on lines +105 to +108
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace assertions with explicit exception handling

The assertions in get_job_trade check for specific conditions but will raise generic AssertionErrors if they fail. It's better to use explicit exceptions to provide clearer error messages and improve code robustness.

Consider replacing the assertions with exceptions:

if trade.trade_type != TradeType.BUY:
    raise ValueError("Only 'BUY' trade types are supported on job markets.")
if not trade.outcome:
    raise ValueError("Trade outcome must be 'YES' on job markets.")
if trade.amount.currency != Currency.xDai:
    raise ValueError("Only xDai currency is supported for trades.")

), "Should work only on real-money markets."
return required_trades[0]

@staticmethod
def from_omen_market(market: OmenMarket) -> "OmenJobAgentMarket":
return OmenJobAgentMarket.from_omen_agent_market(
Expand Down Expand Up @@ -77,38 +136,3 @@ def from_omen_agent_market(market: OmenAgentMarket) -> "OmenJobAgentMarket":
finalized_time=market.finalized_time,
fees=market.fees,
)


def compute_job_reward(
market: OmenAgentMarket, max_bond: float, web3: Web3 | None = None
) -> float:
# Because jobs are powered by prediction markets, potentional reward depends on job's liquidity and our will to bond (bet) our xDai into our job completion.
strategy = KellyBettingStrategy(max_bet_amount=max_bond)
required_trades = strategy.calculate_trades(
existing_position=None,
# We assume that we finish the job and so the probability of the market happening will be 100%.
answer=ProbabilisticAnswer(p_yes=Probability(1.0), confidence=1.0),
market=market,
)

assert (
len(required_trades) == 1
), f"Shouldn't process same job twice: {required_trades}"
trade = required_trades[0]
assert trade.trade_type == TradeType.BUY, "Should only buy on job markets."
assert trade.outcome, "Should buy only YES on job markets."
assert (
trade.amount.currency == Currency.xDai
), "Should work only on real-money markets."

reward = (
market.get_buy_token_amount(
bet_amount=BetAmount(
amount=trade.amount.amount, currency=trade.amount.currency
),
direction=trade.outcome,
).amount
- trade.amount.amount
)

return reward
12 changes: 12 additions & 0 deletions prediction_market_agent_tooling/markets/markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from enum import Enum

from prediction_market_agent_tooling.config import APIKeys
from prediction_market_agent_tooling.jobs.jobs_models import JobAgentMarket
from prediction_market_agent_tooling.jobs.omen.omen_jobs import OmenJobAgentMarket
from prediction_market_agent_tooling.markets.agent_market import (
AgentMarket,
FilterBy,
Expand Down Expand Up @@ -46,6 +48,12 @@ def market_class(self) -> type[AgentMarket]:
raise ValueError(f"Unknown market type: {self}")
return MARKET_TYPE_TO_AGENT_MARKET[self]

@property
def job_class(self) -> type[JobAgentMarket]:
if self not in JOB_MARKET_TYPE_TO_JOB_AGENT_MARKET:
raise ValueError(f"Unknown market type: {self}")
return JOB_MARKET_TYPE_TO_JOB_AGENT_MARKET[self]

@property
def is_blockchain_market(self) -> bool:
return self in [MarketType.OMEN, MarketType.POLYMARKET]
Expand All @@ -58,6 +66,10 @@ def is_blockchain_market(self) -> bool:
MarketType.METACULUS: MetaculusAgentMarket,
}

JOB_MARKET_TYPE_TO_JOB_AGENT_MARKET: dict[MarketType, type[JobAgentMarket]] = {
MarketType.OMEN: OmenJobAgentMarket,
}


def get_binary_markets(
limit: int,
Expand Down
Loading