prediction-market-agent-tooling 0.55.2.dev120__py3-none-any.whl → 0.56.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. prediction_market_agent_tooling/deploy/agent.py +17 -7
  2. prediction_market_agent_tooling/jobs/jobs_models.py +27 -2
  3. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +67 -41
  4. prediction_market_agent_tooling/markets/agent_market.py +8 -2
  5. prediction_market_agent_tooling/markets/markets.py +12 -0
  6. prediction_market_agent_tooling/markets/metaculus/metaculus.py +1 -1
  7. prediction_market_agent_tooling/markets/omen/data_models.py +11 -2
  8. prediction_market_agent_tooling/markets/omen/omen.py +16 -9
  9. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +14 -0
  10. prediction_market_agent_tooling/tools/caches/db_cache.py +351 -0
  11. prediction_market_agent_tooling/tools/google.py +3 -2
  12. prediction_market_agent_tooling/tools/is_invalid.py +2 -2
  13. prediction_market_agent_tooling/tools/is_predictable.py +3 -3
  14. prediction_market_agent_tooling/tools/relevant_news_analysis/relevant_news_analysis.py +6 -10
  15. prediction_market_agent_tooling/tools/tavily/tavily_models.py +0 -66
  16. prediction_market_agent_tooling/tools/tavily/tavily_search.py +12 -44
  17. prediction_market_agent_tooling/tools/utils.py +2 -0
  18. {prediction_market_agent_tooling-0.55.2.dev120.dist-info → prediction_market_agent_tooling-0.56.0.dist-info}/METADATA +2 -1
  19. {prediction_market_agent_tooling-0.55.2.dev120.dist-info → prediction_market_agent_tooling-0.56.0.dist-info}/RECORD +23 -24
  20. prediction_market_agent_tooling/jobs/jobs.py +0 -45
  21. prediction_market_agent_tooling/tools/tavily/tavily_storage.py +0 -105
  22. /prediction_market_agent_tooling/tools/{cache.py → caches/inmemory_cache.py} +0 -0
  23. {prediction_market_agent_tooling-0.55.2.dev120.dist-info → prediction_market_agent_tooling-0.56.0.dist-info}/LICENSE +0 -0
  24. {prediction_market_agent_tooling-0.55.2.dev120.dist-info → prediction_market_agent_tooling-0.56.0.dist-info}/WHEEL +0 -0
  25. {prediction_market_agent_tooling-0.55.2.dev120.dist-info → prediction_market_agent_tooling-0.56.0.dist-info}/entry_points.txt +0 -0
@@ -153,10 +153,10 @@ class DeployableAgent:
153
153
  input=input,
154
154
  output=output,
155
155
  user_id=user_id or getpass.getuser(),
156
- session_id=session_id
157
- or self.session_id, # All traces within a single run execution will be grouped under a single session.
158
- version=version
159
- or APIKeys().LANGFUSE_DEPLOYMENT_VERSION, # Optionally, mark the current deployment with version (e.g. add git commit hash during docker building).
156
+ session_id=session_id or self.session_id,
157
+ # All traces within a single run execution will be grouped under a single session.
158
+ version=version or APIKeys().LANGFUSE_DEPLOYMENT_VERSION,
159
+ # Optionally, mark the current deployment with version (e.g. add git commit hash during docker building).
160
160
  release=release,
161
161
  metadata=metadata,
162
162
  tags=tags,
@@ -342,6 +342,10 @@ class DeployablePredictionAgent(DeployableAgent):
342
342
  ]
343
343
  )
344
344
 
345
+ @property
346
+ def agent_name(self) -> str:
347
+ return self.__class__.__name__
348
+
345
349
  def check_min_required_balance_to_operate(self, market_type: MarketType) -> None:
346
350
  api_keys = APIKeys()
347
351
 
@@ -444,7 +448,9 @@ class DeployablePredictionAgent(DeployableAgent):
444
448
  ) -> None:
445
449
  keys = APIKeys()
446
450
  if self.store_prediction:
447
- market.store_prediction(processed_market=processed_market, keys=keys)
451
+ market.store_prediction(
452
+ processed_market=processed_market, keys=keys, agent_name=self.agent_name
453
+ )
448
454
  else:
449
455
  logger.info(
450
456
  f"Prediction {processed_market} not stored because {self.store_prediction=}."
@@ -613,10 +619,14 @@ class DeployableTraderAgent(DeployablePredictionAgent):
613
619
  processed_market: ProcessedMarket | None,
614
620
  ) -> None:
615
621
  api_keys = APIKeys()
616
- super().after_process_market(market_type, market, processed_market)
622
+ super().after_process_market(
623
+ market_type,
624
+ market,
625
+ processed_market,
626
+ )
617
627
  if isinstance(processed_market, ProcessedTradedMarket):
618
628
  if self.store_trades:
619
- market.store_trades(processed_market, api_keys)
629
+ market.store_trades(processed_market, api_keys, self.agent_name)
620
630
  else:
621
631
  logger.info(
622
632
  f"Trades {processed_market.trades} not stored because {self.store_trades=}."
@@ -3,7 +3,12 @@ from abc import ABC, abstractmethod
3
3
 
4
4
  from pydantic import BaseModel
5
5
 
6
- from prediction_market_agent_tooling.markets.agent_market import AgentMarket
6
+ from prediction_market_agent_tooling.deploy.betting_strategy import ProbabilisticAnswer
7
+ from prediction_market_agent_tooling.gtypes import Probability
8
+ from prediction_market_agent_tooling.markets.agent_market import (
9
+ AgentMarket,
10
+ ProcessedTradedMarket,
11
+ )
7
12
  from prediction_market_agent_tooling.markets.omen.omen_subgraph_handler import (
8
13
  FilterBy,
9
14
  SortBy,
@@ -39,10 +44,24 @@ class JobAgentMarket(AgentMarket, ABC):
39
44
  @classmethod
40
45
  @abstractmethod
41
46
  def get_jobs(
42
- cls, limit: int | None, filter_by: FilterBy, sort_by: SortBy
47
+ cls,
48
+ limit: int | None,
49
+ filter_by: FilterBy = FilterBy.OPEN,
50
+ sort_by: SortBy = SortBy.CLOSING_SOONEST,
43
51
  ) -> t.Sequence["JobAgentMarket"]:
44
52
  """Get all available jobs."""
45
53
 
54
+ @staticmethod
55
+ @abstractmethod
56
+ def get_job(id: str) -> "JobAgentMarket":
57
+ """Get a single job by its id."""
58
+
59
+ @abstractmethod
60
+ def submit_job_result(
61
+ self, agent_name: str, max_bond: float, result: str
62
+ ) -> ProcessedTradedMarket:
63
+ """Submit the completed result for this job."""
64
+
46
65
  def to_simple_job(self, max_bond: float) -> SimpleJob:
47
66
  return SimpleJob(
48
67
  id=self.id,
@@ -51,3 +70,9 @@ class JobAgentMarket(AgentMarket, ABC):
51
70
  currency=self.currency.value,
52
71
  deadline=self.deadline,
53
72
  )
73
+
74
+ def get_job_answer(self, result: str) -> ProbabilisticAnswer:
75
+ # Just return 100% yes with 100% confidence, because we assume the job is completed correctly.
76
+ return ProbabilisticAnswer(
77
+ p_yes=Probability(1.0), confidence=1.0, reasoning=result
78
+ )
@@ -1,15 +1,14 @@
1
1
  import typing as t
2
2
 
3
- from web3 import Web3
4
-
3
+ from prediction_market_agent_tooling.config import APIKeys
5
4
  from prediction_market_agent_tooling.deploy.betting_strategy import (
6
5
  Currency,
7
6
  KellyBettingStrategy,
8
- ProbabilisticAnswer,
9
7
  TradeType,
10
8
  )
11
- from prediction_market_agent_tooling.gtypes import Probability
12
9
  from prediction_market_agent_tooling.jobs.jobs_models import JobAgentMarket
10
+ from prediction_market_agent_tooling.markets.agent_market import ProcessedTradedMarket
11
+ from prediction_market_agent_tooling.markets.data_models import PlacedTrade, Trade
13
12
  from prediction_market_agent_tooling.markets.omen.omen import (
14
13
  BetAmount,
15
14
  OmenAgentMarket,
@@ -36,11 +35,27 @@ class OmenJobAgentMarket(OmenAgentMarket, JobAgentMarket):
36
35
  return self.close_time
37
36
 
38
37
  def get_reward(self, max_bond: float) -> float:
39
- return compute_job_reward(self, max_bond)
38
+ trade = self.get_job_trade(
39
+ max_bond,
40
+ result="", # Pass empty result, as we are computing only potential reward at this point.
41
+ )
42
+ reward = (
43
+ self.get_buy_token_amount(
44
+ bet_amount=BetAmount(
45
+ amount=trade.amount.amount, currency=trade.amount.currency
46
+ ),
47
+ direction=trade.outcome,
48
+ ).amount
49
+ - trade.amount.amount
50
+ )
51
+ return reward
40
52
 
41
53
  @classmethod
42
54
  def get_jobs(
43
- cls, limit: int | None, filter_by: FilterBy, sort_by: SortBy
55
+ cls,
56
+ limit: int | None,
57
+ filter_by: FilterBy = FilterBy.OPEN,
58
+ sort_by: SortBy = SortBy.CLOSING_SOONEST,
44
59
  ) -> t.Sequence["OmenJobAgentMarket"]:
45
60
  markets = OmenSubgraphHandler().get_omen_binary_markets_simple(
46
61
  limit=limit,
@@ -50,6 +65,52 @@ class OmenJobAgentMarket(OmenAgentMarket, JobAgentMarket):
50
65
  )
51
66
  return [OmenJobAgentMarket.from_omen_market(market) for market in markets]
52
67
 
68
+ @staticmethod
69
+ def get_job(id: str) -> "OmenJobAgentMarket":
70
+ return OmenJobAgentMarket.from_omen_agent_market(
71
+ OmenJobAgentMarket.get_binary_market(id=id)
72
+ )
73
+
74
+ def submit_job_result(
75
+ self, agent_name: str, max_bond: float, result: str
76
+ ) -> ProcessedTradedMarket:
77
+ if not APIKeys().enable_ipfs_upload:
78
+ raise RuntimeError(
79
+ f"ENABLE_IPFS_UPLOAD must be set to True to upload job results."
80
+ )
81
+
82
+ trade = self.get_job_trade(max_bond, result)
83
+ buy_id = self.buy_tokens(outcome=trade.outcome, amount=trade.amount)
84
+
85
+ processed_traded_market = ProcessedTradedMarket(
86
+ answer=self.get_job_answer(result),
87
+ trades=[PlacedTrade.from_trade(trade, id=buy_id)],
88
+ )
89
+
90
+ keys = APIKeys()
91
+ self.store_trades(processed_traded_market, keys, agent_name)
92
+
93
+ return processed_traded_market
94
+
95
+ def get_job_trade(self, max_bond: float, result: str) -> Trade:
96
+ # 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.
97
+ strategy = KellyBettingStrategy(max_bet_amount=max_bond)
98
+ required_trades = strategy.calculate_trades(
99
+ existing_position=None,
100
+ answer=self.get_job_answer(result),
101
+ market=self,
102
+ )
103
+ assert (
104
+ len(required_trades) == 1
105
+ ), f"Shouldn't process same job twice: {required_trades}"
106
+ trade = required_trades[0]
107
+ assert trade.trade_type == TradeType.BUY, "Should only buy on job markets."
108
+ assert trade.outcome, "Should buy only YES on job markets."
109
+ assert (
110
+ trade.amount.currency == Currency.xDai
111
+ ), "Should work only on real-money markets."
112
+ return required_trades[0]
113
+
53
114
  @staticmethod
54
115
  def from_omen_market(market: OmenMarket) -> "OmenJobAgentMarket":
55
116
  return OmenJobAgentMarket.from_omen_agent_market(
@@ -77,38 +138,3 @@ class OmenJobAgentMarket(OmenAgentMarket, JobAgentMarket):
77
138
  finalized_time=market.finalized_time,
78
139
  fees=market.fees,
79
140
  )
80
-
81
-
82
- def compute_job_reward(
83
- market: OmenAgentMarket, max_bond: float, web3: Web3 | None = None
84
- ) -> float:
85
- # 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.
86
- strategy = KellyBettingStrategy(max_bet_amount=max_bond)
87
- required_trades = strategy.calculate_trades(
88
- existing_position=None,
89
- # We assume that we finish the job and so the probability of the market happening will be 100%.
90
- answer=ProbabilisticAnswer(p_yes=Probability(1.0), confidence=1.0),
91
- market=market,
92
- )
93
-
94
- assert (
95
- len(required_trades) == 1
96
- ), f"Shouldn't process same job twice: {required_trades}"
97
- trade = required_trades[0]
98
- assert trade.trade_type == TradeType.BUY, "Should only buy on job markets."
99
- assert trade.outcome, "Should buy only YES on job markets."
100
- assert (
101
- trade.amount.currency == Currency.xDai
102
- ), "Should work only on real-money markets."
103
-
104
- reward = (
105
- market.get_buy_token_amount(
106
- bet_amount=BetAmount(
107
- amount=trade.amount.amount, currency=trade.amount.currency
108
- ),
109
- direction=trade.outcome,
110
- ).amount
111
- - trade.amount.amount
112
- )
113
-
114
- return reward
@@ -231,7 +231,10 @@ class AgentMarket(BaseModel):
231
231
  raise NotImplementedError("Subclasses must implement this method")
232
232
 
233
233
  def store_prediction(
234
- self, processed_market: ProcessedMarket | None, keys: APIKeys
234
+ self,
235
+ processed_market: ProcessedMarket | None,
236
+ keys: APIKeys,
237
+ agent_name: str,
235
238
  ) -> None:
236
239
  """
237
240
  If market allows to upload predictions somewhere, implement it in this method.
@@ -239,7 +242,10 @@ class AgentMarket(BaseModel):
239
242
  raise NotImplementedError("Subclasses must implement this method")
240
243
 
241
244
  def store_trades(
242
- self, traded_market: ProcessedTradedMarket | None, keys: APIKeys
245
+ self,
246
+ traded_market: ProcessedTradedMarket | None,
247
+ keys: APIKeys,
248
+ agent_name: str,
243
249
  ) -> None:
244
250
  """
245
251
  If market allows to upload trades somewhere, implement it in this method.
@@ -3,6 +3,8 @@ from datetime import timedelta
3
3
  from enum import Enum
4
4
 
5
5
  from prediction_market_agent_tooling.config import APIKeys
6
+ from prediction_market_agent_tooling.jobs.jobs_models import JobAgentMarket
7
+ from prediction_market_agent_tooling.jobs.omen.omen_jobs import OmenJobAgentMarket
6
8
  from prediction_market_agent_tooling.markets.agent_market import (
7
9
  AgentMarket,
8
10
  FilterBy,
@@ -46,6 +48,12 @@ class MarketType(str, Enum):
46
48
  raise ValueError(f"Unknown market type: {self}")
47
49
  return MARKET_TYPE_TO_AGENT_MARKET[self]
48
50
 
51
+ @property
52
+ def job_class(self) -> type[JobAgentMarket]:
53
+ if self not in JOB_MARKET_TYPE_TO_JOB_AGENT_MARKET:
54
+ raise ValueError(f"Unknown market type: {self}")
55
+ return JOB_MARKET_TYPE_TO_JOB_AGENT_MARKET[self]
56
+
49
57
  @property
50
58
  def is_blockchain_market(self) -> bool:
51
59
  return self in [MarketType.OMEN, MarketType.POLYMARKET]
@@ -58,6 +66,10 @@ MARKET_TYPE_TO_AGENT_MARKET: dict[MarketType, type[AgentMarket]] = {
58
66
  MarketType.METACULUS: MetaculusAgentMarket,
59
67
  }
60
68
 
69
+ JOB_MARKET_TYPE_TO_JOB_AGENT_MARKET: dict[MarketType, type[JobAgentMarket]] = {
70
+ MarketType.OMEN: OmenJobAgentMarket,
71
+ }
72
+
61
73
 
62
74
  def get_binary_markets(
63
75
  limit: int,
@@ -107,7 +107,7 @@ class MetaculusAgentMarket(AgentMarket):
107
107
  return [MetaculusAgentMarket.from_data_model(q) for q in all_questions[:limit]]
108
108
 
109
109
  def store_prediction(
110
- self, processed_market: ProcessedMarket | None, keys: APIKeys
110
+ self, processed_market: ProcessedMarket | None, keys: APIKeys, agent_name: str
111
111
  ) -> None:
112
112
  if processed_market is not None:
113
113
  make_prediction(self.id, processed_market.answer.p_yes)
@@ -24,6 +24,7 @@ from prediction_market_agent_tooling.markets.data_models import (
24
24
  ResolvedBet,
25
25
  )
26
26
  from prediction_market_agent_tooling.tools.utils import (
27
+ BPS_CONSTANT,
27
28
  DatetimeUTC,
28
29
  check_not_none,
29
30
  should_not_happen,
@@ -502,7 +503,7 @@ class OmenBet(BaseModel):
502
503
  feeAmount: Wei
503
504
  outcomeIndex: int
504
505
  outcomeTokensTraded: Wei
505
- transactionHash: HexAddress
506
+ transactionHash: HexBytes
506
507
  fpmm: OmenMarket
507
508
 
508
509
  @property
@@ -785,6 +786,14 @@ class ContractPrediction(BaseModel):
785
786
  tx_hashes: list[HexBytes] = Field(..., alias="txHashes")
786
787
  estimated_probability_bps: int = Field(..., alias="estimatedProbabilityBps")
787
788
 
789
+ @property
790
+ def estimated_probability(self) -> Probability:
791
+ return Probability(self.estimated_probability_bps / BPS_CONSTANT)
792
+
793
+ @property
794
+ def boolean_outcome(self) -> bool:
795
+ return self.estimated_probability > 0.5
796
+
788
797
  @computed_field # type: ignore[prop-decorator] # Mypy issue: https://github.com/python/mypy/issues/14461
789
798
  @property
790
799
  def publisher_checksummed(self) -> ChecksumAddress:
@@ -798,7 +807,7 @@ class ContractPrediction(BaseModel):
798
807
 
799
808
  class IPFSAgentResult(BaseModel):
800
809
  reasoning: str
801
-
810
+ agent_name: str
802
811
  model_config = ConfigDict(
803
812
  extra="forbid",
804
813
  )
@@ -78,6 +78,7 @@ from prediction_market_agent_tooling.tools.contract import (
78
78
  from prediction_market_agent_tooling.tools.hexbytes_custom import HexBytes
79
79
  from prediction_market_agent_tooling.tools.ipfs.ipfs_handler import IPFSHandler
80
80
  from prediction_market_agent_tooling.tools.utils import (
81
+ BPS_CONSTANT,
81
82
  DatetimeUTC,
82
83
  calculate_sell_amount_in_collateral,
83
84
  check_not_none,
@@ -413,17 +414,21 @@ class OmenAgentMarket(AgentMarket):
413
414
  @staticmethod
414
415
  def verify_operational_balance(api_keys: APIKeys) -> bool:
415
416
  return get_total_balance(
416
- api_keys.public_key, # Use `public_key`, not `bet_from_address` because transaction costs are paid from the EOA wallet.
417
+ api_keys.public_key,
418
+ # Use `public_key`, not `bet_from_address` because transaction costs are paid from the EOA wallet.
417
419
  sum_wxdai=False,
418
420
  ) > xdai_type(0.001)
419
421
 
420
422
  def store_prediction(
421
- self, processed_market: ProcessedMarket | None, keys: APIKeys
423
+ self, processed_market: ProcessedMarket | None, keys: APIKeys, agent_name: str
422
424
  ) -> None:
423
425
  """On Omen, we have to store predictions along with trades, see `store_trades`."""
424
426
 
425
427
  def store_trades(
426
- self, traded_market: ProcessedTradedMarket | None, keys: APIKeys
428
+ self,
429
+ traded_market: ProcessedTradedMarket | None,
430
+ keys: APIKeys,
431
+ agent_name: str,
427
432
  ) -> None:
428
433
  if traded_market is None:
429
434
  logger.warning(f"No prediction for market {self.id}, not storing anything.")
@@ -437,7 +442,7 @@ class OmenAgentMarket(AgentMarket):
437
442
  if keys.enable_ipfs_upload:
438
443
  logger.info("Storing prediction on IPFS.")
439
444
  ipfs_hash = IPFSHandler(keys).store_agent_result(
440
- IPFSAgentResult(reasoning=reasoning)
445
+ IPFSAgentResult(reasoning=reasoning, agent_name=agent_name)
441
446
  )
442
447
  ipfs_hash_decoded = ipfscidv0_to_byte32(ipfs_hash)
443
448
 
@@ -448,7 +453,7 @@ class OmenAgentMarket(AgentMarket):
448
453
  publisher=keys.public_key,
449
454
  ipfs_hash=ipfs_hash_decoded,
450
455
  tx_hashes=tx_hashes,
451
- estimated_probability_bps=int(traded_market.answer.p_yes * 10000),
456
+ estimated_probability_bps=int(traded_market.answer.p_yes * BPS_CONSTANT),
452
457
  )
453
458
  tx_receipt = OmenAgentResultMappingContract().add_prediction(
454
459
  api_keys=keys,
@@ -1237,12 +1242,12 @@ def redeem_from_all_user_positions(
1237
1242
 
1238
1243
  if not conditional_token_contract.is_condition_resolved(condition_id):
1239
1244
  logger.info(
1240
- f"[{index+1} / {len(user_positions)}] Skipping redeem, {user_position.id=} isn't resolved yet."
1245
+ f"[{index + 1} / {len(user_positions)}] Skipping redeem, {user_position.id=} isn't resolved yet."
1241
1246
  )
1242
1247
  continue
1243
1248
 
1244
1249
  logger.info(
1245
- f"[{index+1} / {len(user_positions)}] Processing redeem from {user_position.id=}."
1250
+ f"[{index + 1} / {len(user_positions)}] Processing redeem from {user_position.id=}."
1246
1251
  )
1247
1252
 
1248
1253
  original_balances = get_balances(public_key, web3)
@@ -1263,9 +1268,11 @@ def redeem_from_all_user_positions(
1263
1268
  def get_binary_market_p_yes_history(market: OmenAgentMarket) -> list[Probability]:
1264
1269
  history: list[Probability] = []
1265
1270
  trades = sorted(
1266
- OmenSubgraphHandler().get_trades( # We need to look at price both after buying or selling, so get trades, not bets.
1271
+ OmenSubgraphHandler().get_trades(
1272
+ # We need to look at price both after buying or selling, so get trades, not bets.
1267
1273
  market_id=market.market_maker_contract_address_checksummed,
1268
- end_time=market.close_time, # Even after market is closed, there can be many `Sell` trades which will converge the probability to the true one.
1274
+ end_time=market.close_time,
1275
+ # Even after market is closed, there can be many `Sell` trades which will converge the probability to the true one.
1269
1276
  ),
1270
1277
  key=lambda x: x.creation_datetime,
1271
1278
  )
@@ -908,3 +908,17 @@ class OmenSubgraphHandler(BaseSubgraphHandler):
908
908
  if not items:
909
909
  return []
910
910
  return [ContractPrediction.model_validate(i) for i in items]
911
+
912
+ def get_agent_results_for_bet(self, bet: OmenBet) -> ContractPrediction | None:
913
+ results = [
914
+ result
915
+ for result in self.get_agent_results_for_market(bet.fpmm.id)
916
+ if bet.transactionHash in result.tx_hashes
917
+ ]
918
+
919
+ if not results:
920
+ return None
921
+ elif len(results) > 1:
922
+ raise RuntimeError("Multiple results found for a single bet.")
923
+
924
+ return results[0]