prediction-market-agent-tooling 0.54.0__py3-none-any.whl → 0.55.1__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 (20) hide show
  1. prediction_market_agent_tooling/config.py +3 -3
  2. prediction_market_agent_tooling/deploy/agent.py +172 -90
  3. prediction_market_agent_tooling/deploy/betting_strategy.py +5 -1
  4. prediction_market_agent_tooling/deploy/trade_interval.py +46 -0
  5. prediction_market_agent_tooling/markets/agent_market.py +12 -1
  6. prediction_market_agent_tooling/markets/metaculus/data_models.py +4 -1
  7. prediction_market_agent_tooling/markets/metaculus/metaculus.py +30 -8
  8. prediction_market_agent_tooling/markets/omen/omen.py +14 -6
  9. prediction_market_agent_tooling/tools/is_invalid.py +4 -3
  10. prediction_market_agent_tooling/tools/relevant_news_analysis/data_models.py +44 -0
  11. prediction_market_agent_tooling/tools/relevant_news_analysis/relevant_news_analysis.py +162 -0
  12. prediction_market_agent_tooling/tools/relevant_news_analysis/relevant_news_cache.py +90 -0
  13. prediction_market_agent_tooling/tools/safe.py +6 -6
  14. prediction_market_agent_tooling/tools/tavily/tavily_search.py +12 -2
  15. prediction_market_agent_tooling/tools/web3_utils.py +4 -4
  16. {prediction_market_agent_tooling-0.54.0.dist-info → prediction_market_agent_tooling-0.55.1.dist-info}/METADATA +3 -3
  17. {prediction_market_agent_tooling-0.54.0.dist-info → prediction_market_agent_tooling-0.55.1.dist-info}/RECORD +20 -16
  18. {prediction_market_agent_tooling-0.54.0.dist-info → prediction_market_agent_tooling-0.55.1.dist-info}/LICENSE +0 -0
  19. {prediction_market_agent_tooling-0.54.0.dist-info → prediction_market_agent_tooling-0.55.1.dist-info}/WHEEL +0 -0
  20. {prediction_market_agent_tooling-0.54.0.dist-info → prediction_market_agent_tooling-0.55.1.dist-info}/entry_points.txt +0 -0
@@ -1,10 +1,10 @@
1
1
  import typing as t
2
2
 
3
- from gnosis.eth import EthereumClient
4
- from gnosis.safe import Safe
5
3
  from pydantic.types import SecretStr
6
4
  from pydantic.v1.types import SecretStr as SecretStrV1
7
5
  from pydantic_settings import BaseSettings, SettingsConfigDict
6
+ from safe_eth.eth import EthereumClient
7
+ from safe_eth.safe.safe import SafeV141
8
8
 
9
9
  from prediction_market_agent_tooling.gtypes import (
10
10
  ChecksumAddress,
@@ -200,6 +200,6 @@ class APIKeys(BaseSettings):
200
200
  if not self.SAFE_ADDRESS:
201
201
  raise ValueError("Cannot check ownership if safe_address is not defined.")
202
202
 
203
- s = Safe(self.SAFE_ADDRESS, ethereum_client) # type: ignore[abstract]
203
+ s = SafeV141(self.SAFE_ADDRESS, ethereum_client)
204
204
  public_key_from_signer = private_key_to_public_key(self.bet_from_private_key)
205
205
  return s.retrieve_is_owner(public_key_from_signer)
@@ -30,12 +30,17 @@ from prediction_market_agent_tooling.deploy.gcp.utils import (
30
30
  gcp_function_is_active,
31
31
  gcp_resolve_api_keys_secrets,
32
32
  )
33
+ from prediction_market_agent_tooling.deploy.trade_interval import (
34
+ FixedInterval,
35
+ TradeInterval,
36
+ )
33
37
  from prediction_market_agent_tooling.gtypes import xDai, xdai_type
34
38
  from prediction_market_agent_tooling.loggers import logger
35
39
  from prediction_market_agent_tooling.markets.agent_market import (
36
40
  AgentMarket,
37
41
  FilterBy,
38
42
  ProcessedMarket,
43
+ ProcessedTradedMarket,
39
44
  SortBy,
40
45
  )
41
46
  from prediction_market_agent_tooling.markets.data_models import (
@@ -165,9 +170,11 @@ class DeployableAgent:
165
170
  return f"{self.__class__.__name__} - {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}"
166
171
 
167
172
  def __init_subclass__(cls, **kwargs: t.Any) -> None:
168
- if "DeployableAgent" not in str(
169
- cls.__init__
170
- ) and "DeployableTraderAgent" not in str(cls.__init__):
173
+ if (
174
+ "DeployableAgent" not in str(cls.__init__)
175
+ and "DeployableTraderAgent" not in str(cls.__init__)
176
+ and "DeployablePredictionAgent" not in str(cls.__init__)
177
+ ):
171
178
  raise TypeError(
172
179
  "Cannot override __init__ method of deployable agent class, please override the `load` method to set up the agent."
173
180
  )
@@ -274,28 +281,22 @@ def {entrypoint_function_name}(request) -> str:
274
281
  return f"{self.__class__.__name__.lower()}-{market_type}-{utcnow().strftime('%Y-%m-%d--%H-%M-%S')}"
275
282
 
276
283
 
277
- class DeployableTraderAgent(DeployableAgent):
284
+ class DeployablePredictionAgent(DeployableAgent):
278
285
  bet_on_n_markets_per_run: int = 1
286
+ n_markets_to_fetch: int = MAX_AVAILABLE_MARKETS
279
287
  min_balance_to_keep_in_native_currency: xDai | None = xdai_type(0.1)
280
288
  allow_invalid_questions: bool = False
281
- same_market_bet_interval: timedelta = timedelta(hours=24)
289
+ same_market_trade_interval: TradeInterval = FixedInterval(timedelta(hours=24))
290
+ # Only Metaculus allows to post predictions without trading (buying/selling of outcome tokens).
291
+ supported_markets: t.Sequence[MarketType] = [MarketType.METACULUS]
282
292
 
283
293
  def __init__(
284
294
  self,
285
295
  enable_langfuse: bool = APIKeys().default_enable_langfuse,
286
- place_bet: bool = True,
296
+ store_prediction: bool = True,
287
297
  ) -> None:
288
298
  super().__init__(enable_langfuse=enable_langfuse)
289
- self.place_bet = place_bet
290
-
291
- def get_betting_strategy(self, market: AgentMarket) -> BettingStrategy:
292
- user_id = market.get_user_id(api_keys=APIKeys())
293
-
294
- total_amount = market.get_tiny_bet_amount().amount
295
- if existing_position := market.get_position(user_id=user_id):
296
- total_amount += existing_position.total_amount.amount
297
-
298
- return MaxAccuracyBettingStrategy(bet_amount=total_amount)
299
+ self.store_prediction = store_prediction
299
300
 
300
301
  def initialize_langfuse(self) -> None:
301
302
  super().initialize_langfuse()
@@ -304,7 +305,6 @@ class DeployableTraderAgent(DeployableAgent):
304
305
  self.verify_market = observe()(self.verify_market) # type: ignore[method-assign]
305
306
  self.answer_binary_market = observe()(self.answer_binary_market) # type: ignore[method-assign]
306
307
  self.process_market = observe()(self.process_market) # type: ignore[method-assign]
307
- self.build_trades = observe()(self.build_trades) # type: ignore[method-assign]
308
308
 
309
309
  def update_langfuse_trace_by_market(
310
310
  self, market_type: MarketType, market: AgentMarket
@@ -342,19 +342,6 @@ class DeployableTraderAgent(DeployableAgent):
342
342
  f"{api_keys=} doesn't have enough operational balance."
343
343
  )
344
344
 
345
- def check_min_required_balance_to_trade(self, market: AgentMarket) -> None:
346
- api_keys = APIKeys()
347
-
348
- # Get the strategy to know how much it will bet.
349
- strategy = self.get_betting_strategy(market)
350
- # Have a little bandwidth after the bet.
351
- min_required_balance_to_trade = strategy.maximum_possible_bet_amount * 1.01
352
-
353
- if market.get_trade_balance(api_keys) < min_required_balance_to_trade:
354
- raise OutOfFundsError(
355
- f"Minimum required balance {min_required_balance_to_trade} for agent is not met."
356
- )
357
-
358
345
  def have_bet_on_market_since(self, market: AgentMarket, since: timedelta) -> bool:
359
346
  return have_bet_on_market_since(keys=APIKeys(), market=market, since=since)
360
347
 
@@ -363,7 +350,9 @@ class DeployableTraderAgent(DeployableAgent):
363
350
  Subclasses can implement their own logic instead of this one, or on top of this one.
364
351
  By default, it allows only markets where user didn't bet recently and it's a reasonable question.
365
352
  """
366
- if self.have_bet_on_market_since(market, since=self.same_market_bet_interval):
353
+ if self.have_bet_on_market_since(
354
+ market, since=self.same_market_trade_interval.get(market=market)
355
+ ):
367
356
  return False
368
357
 
369
358
  # Manifold allows to bet only on markets with probability between 1 and 99.
@@ -388,37 +377,21 @@ class DeployableTraderAgent(DeployableAgent):
388
377
  def get_markets(
389
378
  self,
390
379
  market_type: MarketType,
391
- limit: int = MAX_AVAILABLE_MARKETS,
392
380
  sort_by: SortBy = SortBy.CLOSING_SOONEST,
393
381
  filter_by: FilterBy = FilterBy.OPEN,
394
382
  ) -> t.Sequence[AgentMarket]:
395
383
  cls = market_type.market_class
396
384
  # Fetch the soonest closing markets to choose from
397
385
  available_markets = cls.get_binary_markets(
398
- limit=limit, sort_by=sort_by, filter_by=filter_by
386
+ limit=self.n_markets_to_fetch, sort_by=sort_by, filter_by=filter_by
399
387
  )
400
388
  return available_markets
401
389
 
402
- def build_trades(
403
- self,
404
- market: AgentMarket,
405
- answer: ProbabilisticAnswer,
406
- existing_position: Position | None,
407
- ) -> list[Trade]:
408
- strategy = self.get_betting_strategy(market=market)
409
- trades = strategy.calculate_trades(existing_position, answer, market)
410
- BettingStrategy.assert_trades_currency_match_markets(market, trades)
411
- return trades
412
-
413
390
  def before_process_market(
414
391
  self, market_type: MarketType, market: AgentMarket
415
392
  ) -> None:
416
- self.update_langfuse_trace_by_market(market_type, market)
417
-
418
393
  api_keys = APIKeys()
419
394
 
420
- self.check_min_required_balance_to_trade(market)
421
-
422
395
  if market_type.is_blockchain_market:
423
396
  # Exchange wxdai back to xdai if the balance is getting low, so we can keep paying for fees.
424
397
  if self.min_balance_to_keep_in_native_currency is not None:
@@ -434,65 +407,39 @@ class DeployableTraderAgent(DeployableAgent):
434
407
  market: AgentMarket,
435
408
  verify_market: bool = True,
436
409
  ) -> ProcessedMarket | None:
410
+ self.update_langfuse_trace_by_market(market_type, market)
437
411
  logger.info(f"Processing market {market.question=} from {market.url=}.")
438
412
 
439
- self.before_process_market(market_type, market)
440
-
413
+ answer: ProbabilisticAnswer | None
441
414
  if verify_market and not self.verify_market(market_type, market):
442
415
  logger.info(f"Market '{market.question}' doesn't meet the criteria.")
443
- self.update_langfuse_trace_by_processed_market(market_type, None)
444
- return None
445
-
446
- answer = self.answer_binary_market(market)
447
-
448
- if answer is None:
449
- logger.info(f"No answer for market '{market.question}'.")
450
- self.update_langfuse_trace_by_processed_market(market_type, None)
451
- return None
416
+ answer = None
417
+ else:
418
+ answer = self.answer_binary_market(market)
452
419
 
453
- existing_position = market.get_position(user_id=APIKeys().bet_from_address)
454
- trades = self.build_trades(
455
- market=market,
456
- answer=answer,
457
- existing_position=existing_position,
420
+ processed_market = (
421
+ ProcessedMarket(answer=answer) if answer is not None else None
458
422
  )
459
423
 
460
- placed_trades = []
461
- if self.place_bet:
462
- for trade in trades:
463
- logger.info(f"Executing trade {trade} on market {market.id}")
464
-
465
- match trade.trade_type:
466
- case TradeType.BUY:
467
- id = market.buy_tokens(
468
- outcome=trade.outcome, amount=trade.amount
469
- )
470
- case TradeType.SELL:
471
- id = market.sell_tokens(
472
- outcome=trade.outcome, amount=trade.amount
473
- )
474
- case _:
475
- raise ValueError(f"Unexpected trade type {trade.trade_type}.")
476
- placed_trades.append(PlacedTrade.from_trade(trade, id))
477
-
478
- processed_market = ProcessedMarket(answer=answer, trades=placed_trades)
479
424
  self.update_langfuse_trace_by_processed_market(market_type, processed_market)
480
-
481
- self.after_process_market(
482
- market_type, market, processed_market=processed_market
425
+ logger.info(
426
+ f"Processed market {market.question=} from {market.url=} with {answer=}."
483
427
  )
484
-
485
- logger.info(f"Processed market {market.question=} from {market.url=}.")
486
428
  return processed_market
487
429
 
488
430
  def after_process_market(
489
431
  self,
490
432
  market_type: MarketType,
491
433
  market: AgentMarket,
492
- processed_market: ProcessedMarket,
434
+ processed_market: ProcessedMarket | None,
493
435
  ) -> None:
494
436
  keys = APIKeys()
495
- market.store_prediction(processed_market=processed_market, keys=keys)
437
+ if self.store_prediction:
438
+ market.store_prediction(processed_market=processed_market, keys=keys)
439
+ else:
440
+ logger.info(
441
+ f"Prediction {processed_market} not stored because {self.store_prediction=}."
442
+ )
496
443
 
497
444
  def before_process_markets(self, market_type: MarketType) -> None:
498
445
  """
@@ -514,7 +461,9 @@ class DeployableTraderAgent(DeployableAgent):
514
461
  processed = 0
515
462
 
516
463
  for market in available_markets:
464
+ self.before_process_market(market_type, market)
517
465
  processed_market = self.process_market(market_type, market)
466
+ self.after_process_market(market_type, market, processed_market)
518
467
 
519
468
  if processed_market is not None:
520
469
  processed += 1
@@ -528,6 +477,139 @@ class DeployableTraderAgent(DeployableAgent):
528
477
  "Executes actions that occur after bets are placed."
529
478
 
530
479
  def run(self, market_type: MarketType) -> None:
480
+ if market_type not in self.supported_markets:
481
+ raise ValueError(
482
+ f"Only {self.supported_markets} are supported by this agent."
483
+ )
531
484
  self.before_process_markets(market_type)
532
485
  self.process_markets(market_type)
533
486
  self.after_process_markets(market_type)
487
+
488
+
489
+ class DeployableTraderAgent(DeployablePredictionAgent):
490
+ # These markets require place of bet, not just predictions.
491
+ supported_markets: t.Sequence[MarketType] = [
492
+ MarketType.OMEN,
493
+ MarketType.MANIFOLD,
494
+ MarketType.POLYMARKET,
495
+ ]
496
+
497
+ def __init__(
498
+ self,
499
+ enable_langfuse: bool = APIKeys().default_enable_langfuse,
500
+ store_prediction: bool = True,
501
+ store_trades: bool = True,
502
+ place_trades: bool = True,
503
+ ) -> None:
504
+ super().__init__(
505
+ enable_langfuse=enable_langfuse, store_prediction=store_prediction
506
+ )
507
+ self.store_trades = store_trades
508
+ self.place_trades = place_trades
509
+
510
+ def initialize_langfuse(self) -> None:
511
+ super().initialize_langfuse()
512
+ # Auto-observe all the methods where it makes sense, so that subclassses don't need to do it manually.
513
+ self.get_betting_strategy = observe()(self.get_betting_strategy) # type: ignore[method-assign]
514
+ self.build_trades = observe()(self.build_trades) # type: ignore[method-assign]
515
+
516
+ def check_min_required_balance_to_trade(self, market: AgentMarket) -> None:
517
+ api_keys = APIKeys()
518
+
519
+ # Get the strategy to know how much it will bet.
520
+ strategy = self.get_betting_strategy(market)
521
+ # Have a little bandwidth after the bet.
522
+ min_required_balance_to_trade = strategy.maximum_possible_bet_amount * 1.01
523
+
524
+ if market.get_trade_balance(api_keys) < min_required_balance_to_trade:
525
+ raise OutOfFundsError(
526
+ f"Minimum required balance {min_required_balance_to_trade} for agent is not met."
527
+ )
528
+
529
+ def get_betting_strategy(self, market: AgentMarket) -> BettingStrategy:
530
+ user_id = market.get_user_id(api_keys=APIKeys())
531
+
532
+ total_amount = market.get_tiny_bet_amount().amount
533
+ if existing_position := market.get_position(user_id=user_id):
534
+ total_amount += existing_position.total_amount.amount
535
+
536
+ return MaxAccuracyBettingStrategy(bet_amount=total_amount)
537
+
538
+ def build_trades(
539
+ self,
540
+ market: AgentMarket,
541
+ answer: ProbabilisticAnswer,
542
+ existing_position: Position | None,
543
+ ) -> list[Trade]:
544
+ strategy = self.get_betting_strategy(market=market)
545
+ trades = strategy.calculate_trades(existing_position, answer, market)
546
+ BettingStrategy.assert_trades_currency_match_markets(market, trades)
547
+ return trades
548
+
549
+ def before_process_market(
550
+ self, market_type: MarketType, market: AgentMarket
551
+ ) -> None:
552
+ super().before_process_market(market_type, market)
553
+ self.check_min_required_balance_to_trade(market)
554
+
555
+ def process_market(
556
+ self,
557
+ market_type: MarketType,
558
+ market: AgentMarket,
559
+ verify_market: bool = True,
560
+ ) -> ProcessedTradedMarket | None:
561
+ processed_market = super().process_market(market_type, market, verify_market)
562
+ if processed_market is None:
563
+ return None
564
+
565
+ api_keys = APIKeys()
566
+ existing_position = market.get_position(
567
+ user_id=market.get_user_id(api_keys=api_keys)
568
+ )
569
+ trades = self.build_trades(
570
+ market=market,
571
+ answer=processed_market.answer,
572
+ existing_position=existing_position,
573
+ )
574
+
575
+ placed_trades = []
576
+ for trade in trades:
577
+ logger.info(f"Executing trade {trade} on market {market.id} ({market.url})")
578
+
579
+ if self.place_trades:
580
+ match trade.trade_type:
581
+ case TradeType.BUY:
582
+ id = market.buy_tokens(
583
+ outcome=trade.outcome, amount=trade.amount
584
+ )
585
+ case TradeType.SELL:
586
+ id = market.sell_tokens(
587
+ outcome=trade.outcome, amount=trade.amount
588
+ )
589
+ case _:
590
+ raise ValueError(f"Unexpected trade type {trade.trade_type}.")
591
+ placed_trades.append(PlacedTrade.from_trade(trade, id))
592
+ else:
593
+ logger.info(f"Trade execution skipped because {self.place_trades=}.")
594
+
595
+ traded_market = ProcessedTradedMarket(
596
+ answer=processed_market.answer, trades=placed_trades
597
+ )
598
+ logger.info(f"Traded market {market.question=} from {market.url=}.")
599
+ return traded_market
600
+
601
+ def after_process_market(
602
+ self,
603
+ market_type: MarketType,
604
+ market: AgentMarket,
605
+ processed_market: ProcessedMarket | None,
606
+ ) -> None:
607
+ api_keys = APIKeys()
608
+ super().after_process_market(market_type, market, processed_market)
609
+ if isinstance(processed_market, ProcessedTradedMarket):
610
+ if self.store_trades:
611
+ market.store_trades(processed_market, api_keys)
612
+ else:
613
+ logger.info(
614
+ f"Trades {processed_market.trades} not stored because {self.store_trades=}."
615
+ )
@@ -24,6 +24,10 @@ from prediction_market_agent_tooling.tools.betting_strategies.utils import Simpl
24
24
  from prediction_market_agent_tooling.tools.utils import check_not_none
25
25
 
26
26
 
27
+ class GuaranteedLossError(RuntimeError):
28
+ pass
29
+
30
+
27
31
  class BettingStrategy(ABC):
28
32
  @abstractmethod
29
33
  def calculate_trades(
@@ -63,7 +67,7 @@ class BettingStrategy(ABC):
63
67
  )
64
68
 
65
69
  if outcome_tokens_to_get.amount < trade.amount.amount:
66
- raise RuntimeError(
70
+ raise GuaranteedLossError(
67
71
  f"Trade {trade=} would result in guaranteed loss by getting only {outcome_tokens_to_get=}."
68
72
  )
69
73
 
@@ -0,0 +1,46 @@
1
+ from abc import ABC, abstractmethod
2
+ from datetime import timedelta
3
+
4
+ from prediction_market_agent_tooling.markets.agent_market import AgentMarket
5
+ from prediction_market_agent_tooling.tools.utils import check_not_none
6
+
7
+
8
+ class TradeInterval(ABC):
9
+ @abstractmethod
10
+ def get(
11
+ self,
12
+ market: AgentMarket,
13
+ ) -> timedelta:
14
+ raise NotImplementedError("Subclass should implement this.")
15
+
16
+
17
+ class FixedInterval(TradeInterval):
18
+ """
19
+ For trades at a fixed interval.
20
+ """
21
+
22
+ def __init__(self, interval: timedelta):
23
+ self.interval = interval
24
+
25
+ def get(
26
+ self,
27
+ market: AgentMarket,
28
+ ) -> timedelta:
29
+ return self.interval
30
+
31
+
32
+ class MarketLifetimeProportionalInterval(TradeInterval):
33
+ """
34
+ For uniformly distributed trades over the market's lifetime.
35
+ """
36
+
37
+ def __init__(self, max_trades: int):
38
+ self.max_trades = max_trades
39
+
40
+ def get(
41
+ self,
42
+ market: AgentMarket,
43
+ ) -> timedelta:
44
+ created_time = check_not_none(market.created_time)
45
+ close_time = check_not_none(market.close_time)
46
+ return (close_time - created_time) / self.max_trades
@@ -29,6 +29,9 @@ from prediction_market_agent_tooling.tools.utils import (
29
29
 
30
30
  class ProcessedMarket(BaseModel):
31
31
  answer: ProbabilisticAnswer
32
+
33
+
34
+ class ProcessedTradedMarket(ProcessedMarket):
32
35
  trades: list[PlacedTrade]
33
36
 
34
37
 
@@ -228,13 +231,21 @@ class AgentMarket(BaseModel):
228
231
  raise NotImplementedError("Subclasses must implement this method")
229
232
 
230
233
  def store_prediction(
231
- self, processed_market: ProcessedMarket, keys: APIKeys
234
+ self, processed_market: ProcessedMarket | None, keys: APIKeys
232
235
  ) -> None:
233
236
  """
234
237
  If market allows to upload predictions somewhere, implement it in this method.
235
238
  """
236
239
  raise NotImplementedError("Subclasses must implement this method")
237
240
 
241
+ def store_trades(
242
+ self, traded_market: ProcessedTradedMarket | None, keys: APIKeys
243
+ ) -> None:
244
+ """
245
+ If market allows to upload trades somewhere, implement it in this method.
246
+ """
247
+ raise NotImplementedError("Subclasses must implement this method")
248
+
238
249
  @staticmethod
239
250
  def get_bets_made_since(
240
251
  better_address: ChecksumAddress, start_time: DatetimeUTC
@@ -56,6 +56,9 @@ class Question(BaseModel):
56
56
  my_forecasts: MyAggregation
57
57
  type: QuestionType
58
58
  possibilities: dict[str, str] | None
59
+ description: str
60
+ fine_print: str
61
+ resolution_criteria: str
59
62
 
60
63
 
61
64
  class MetaculusQuestion(BaseModel):
@@ -64,7 +67,7 @@ class MetaculusQuestion(BaseModel):
64
67
  author_username: str
65
68
  title: str
66
69
  created_at: DatetimeUTC
67
- published_at: DatetimeUTC
70
+ published_at: DatetimeUTC | None
68
71
  scheduled_close_time: DatetimeUTC
69
72
  scheduled_resolve_time: DatetimeUTC
70
73
  user_permission: str
@@ -1,11 +1,11 @@
1
1
  import typing as t
2
2
 
3
3
  from prediction_market_agent_tooling.config import APIKeys
4
- from prediction_market_agent_tooling.gtypes import Probability
5
4
  from prediction_market_agent_tooling.markets.agent_market import (
6
5
  AgentMarket,
7
6
  FilterBy,
8
7
  MarketFees,
8
+ ProcessedMarket,
9
9
  SortBy,
10
10
  )
11
11
  from prediction_market_agent_tooling.markets.metaculus.api import (
@@ -17,7 +17,7 @@ from prediction_market_agent_tooling.markets.metaculus.api import (
17
17
  from prediction_market_agent_tooling.markets.metaculus.data_models import (
18
18
  MetaculusQuestion,
19
19
  )
20
- from prediction_market_agent_tooling.tools.utils import DatetimeUTC
20
+ from prediction_market_agent_tooling.tools.utils import DatetimeUTC, check_not_none
21
21
 
22
22
 
23
23
  class MetaculusAgentMarket(AgentMarket):
@@ -27,9 +27,9 @@ class MetaculusAgentMarket(AgentMarket):
27
27
 
28
28
  have_predicted: bool
29
29
  base_url: t.ClassVar[str] = METACULUS_API_BASE_URL
30
- description: str | None = (
31
- None # Metaculus markets don't have a description, so just default to None.
32
- )
30
+ description: str
31
+ fine_print: str
32
+ resolution_criteria: str
33
33
  fees: MarketFees = MarketFees.get_zero_fees() # No fees on Metaculus.
34
34
 
35
35
  @staticmethod
@@ -46,6 +46,9 @@ class MetaculusAgentMarket(AgentMarket):
46
46
  volume=None,
47
47
  have_predicted=model.question.my_forecasts.latest is not None,
48
48
  outcome_token_pool=None,
49
+ description=model.question.description,
50
+ fine_print=model.question.fine_print,
51
+ resolution_criteria=model.question.resolution_criteria,
49
52
  )
50
53
 
51
54
  @staticmethod
@@ -103,10 +106,29 @@ class MetaculusAgentMarket(AgentMarket):
103
106
  break
104
107
  return [MetaculusAgentMarket.from_data_model(q) for q in all_questions[:limit]]
105
108
 
106
- def submit_prediction(self, p_yes: Probability, reasoning: str) -> None:
107
- make_prediction(self.id, p_yes)
108
- post_question_comment(self.id, reasoning)
109
+ def store_prediction(
110
+ self, processed_market: ProcessedMarket | None, keys: APIKeys
111
+ ) -> None:
112
+ if processed_market is not None:
113
+ make_prediction(self.id, processed_market.answer.p_yes)
114
+ post_question_comment(
115
+ self.id,
116
+ check_not_none(
117
+ processed_market.answer.reasoning,
118
+ "Reasoning must be provided for Metaculus.",
119
+ ),
120
+ )
109
121
 
110
122
  @staticmethod
111
123
  def get_user_id(api_keys: APIKeys) -> str:
112
124
  return str(api_keys.metaculus_user_id)
125
+
126
+ @staticmethod
127
+ def verify_operational_balance(api_keys: APIKeys) -> bool:
128
+ # No operational balance for Metaculus.
129
+ return True
130
+
131
+ @staticmethod
132
+ def redeem_winnings(api_keys: APIKeys) -> None:
133
+ # Nothing to redeem on Metaculus.
134
+ pass
@@ -25,6 +25,7 @@ from prediction_market_agent_tooling.markets.agent_market import (
25
25
  FilterBy,
26
26
  MarketFees,
27
27
  ProcessedMarket,
28
+ ProcessedTradedMarket,
28
29
  SortBy,
29
30
  )
30
31
  from prediction_market_agent_tooling.markets.data_models import (
@@ -417,12 +418,19 @@ class OmenAgentMarket(AgentMarket):
417
418
  ) > xdai_type(0.001)
418
419
 
419
420
  def store_prediction(
420
- self, processed_market: ProcessedMarket, keys: APIKeys
421
+ self, processed_market: ProcessedMarket | None, keys: APIKeys
421
422
  ) -> None:
423
+ """On Omen, we have to store predictions along with trades, see `store_trades`."""
424
+
425
+ def store_trades(
426
+ self, traded_market: ProcessedTradedMarket | None, keys: APIKeys
427
+ ) -> None:
428
+ if traded_market is None:
429
+ logger.warning(f"No prediction for market {self.id}, not storing anything.")
430
+ return
431
+
422
432
  reasoning = (
423
- processed_market.answer.reasoning
424
- if processed_market.answer.reasoning
425
- else ""
433
+ traded_market.answer.reasoning if traded_market.answer.reasoning else ""
426
434
  )
427
435
 
428
436
  ipfs_hash_decoded = HexBytes(HASH_ZERO)
@@ -434,13 +442,13 @@ class OmenAgentMarket(AgentMarket):
434
442
  ipfs_hash_decoded = ipfscidv0_to_byte32(ipfs_hash)
435
443
 
436
444
  tx_hashes = [
437
- HexBytes(HexStr(i.id)) for i in processed_market.trades if i.id is not None
445
+ HexBytes(HexStr(i.id)) for i in traded_market.trades if i.id is not None
438
446
  ]
439
447
  prediction = ContractPrediction(
440
448
  publisher=keys.public_key,
441
449
  ipfs_hash=ipfs_hash_decoded,
442
450
  tx_hashes=tx_hashes,
443
- estimated_probability_bps=int(processed_market.answer.p_yes * 10000),
451
+ estimated_probability_bps=int(traded_market.answer.p_yes * 10000),
444
452
  )
445
453
  tx_receipt = OmenAgentResultMappingContract().add_prediction(
446
454
  api_keys=keys,
@@ -24,7 +24,7 @@ QUESTION_IS_INVALID_PROMPT = """Main signs about an invalid question (sometimes
24
24
  - The violent event can be caused by a single conscious being.
25
25
  - The violent event is done illegally.
26
26
  - The market should not directly incentivize immoral violent (such as murder, rape or unjust imprisonment) actions which could likely be performed by any participant.
27
- - Invalid: Will Donald Trump be alive on the 01/12/2021? (Anyone could bet on No and kill him for a guaranteed profit. Anyone could bet on Yes to effectively put a bounty on his head).
27
+ - Invalid: Will Donald Trump be alive on the 01/12/2021? (Anyone could bet on "No" and kill him for a guaranteed profit. Anyone could bet on "Yes" to effectively put a bounty on his head).
28
28
  - Invalid: Will Hera be a victim of swatting in 2020? (Anyone could falsely call the emergency services on him in order to win the bet)
29
29
  - This does not prevent markets:
30
30
  - Whose topics are violent events not caused by conscious beings.
@@ -35,9 +35,10 @@ QUESTION_IS_INVALID_PROMPT = """Main signs about an invalid question (sometimes
35
35
  - Valid: Will the US be engaged in a military conflict with a UN member state in 2021? (It’s unlikely for the US to declare war in order to win a bet on this market).
36
36
  - Valid: Will Derek Chauvin go to jail for the murder of George Flyod? (It’s unlikely that the jurors would collude to make a wrong verdict in order to win this market).
37
37
  - Questions with relative dates will resolve as invalid. Dates must be stated in absolute terms, not relative depending on the current time.
38
- - Invalid: Who will be the president of the United States in 6 months? (in 6 months depends on the current time).
38
+ - Invalid: Who will be the president of the United States in 6 months? ("in 6 months depends on the current time").
39
+ - Invalid: In the next 14 days, will Gnosis Chain gain another 1M users? ("in the next 14 days depends on the current time").
39
40
  - Questions about moral values and not facts will be resolved as invalid.
40
- - Invalid: Is it ethical to eat meat?”.
41
+ - Invalid: "Is it ethical to eat meat?".
41
42
 
42
43
  Follow a chain of thought to evaluate if the question is invalid:
43
44
 
@@ -0,0 +1,44 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+ from prediction_market_agent_tooling.tools.tavily.tavily_models import TavilyResult
4
+
5
+
6
+ class RelevantNewsAnalysis(BaseModel):
7
+ reasoning: str = Field(
8
+ ...,
9
+ description="The reason why the news contains information relevant to the given question. Or if no news is relevant, why not.",
10
+ )
11
+ contains_relevant_news: bool = Field(
12
+ ...,
13
+ description="A boolean flag for whether the news contains information relevant to the given question.",
14
+ )
15
+
16
+
17
+ class RelevantNews(BaseModel):
18
+ question: str
19
+ url: str
20
+ summary: str
21
+ relevance_reasoning: str
22
+ days_ago: int
23
+
24
+ @staticmethod
25
+ def from_tavily_result_and_analysis(
26
+ question: str,
27
+ days_ago: int,
28
+ tavily_result: TavilyResult,
29
+ relevant_news_analysis: RelevantNewsAnalysis,
30
+ ) -> "RelevantNews":
31
+ return RelevantNews(
32
+ question=question,
33
+ url=tavily_result.url,
34
+ summary=tavily_result.content,
35
+ relevance_reasoning=relevant_news_analysis.reasoning,
36
+ days_ago=days_ago,
37
+ )
38
+
39
+
40
+ class NoRelevantNews(BaseModel):
41
+ """
42
+ A placeholder model for when no relevant news is found. Enables ability to
43
+ distinguish between 'a cache hit with no news' and 'a cache miss'.
44
+ """
@@ -0,0 +1,162 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ from langchain_core.output_parsers import PydanticOutputParser
4
+ from langchain_core.prompts import PromptTemplate
5
+ from langchain_openai import ChatOpenAI
6
+
7
+ from prediction_market_agent_tooling.config import APIKeys
8
+ from prediction_market_agent_tooling.tools.langfuse_ import (
9
+ get_langfuse_langchain_config,
10
+ observe,
11
+ )
12
+ from prediction_market_agent_tooling.tools.relevant_news_analysis.data_models import (
13
+ NoRelevantNews,
14
+ RelevantNews,
15
+ RelevantNewsAnalysis,
16
+ )
17
+ from prediction_market_agent_tooling.tools.relevant_news_analysis.relevant_news_cache import (
18
+ RelevantNewsResponseCache,
19
+ )
20
+ from prediction_market_agent_tooling.tools.tavily.tavily_search import (
21
+ get_relevant_news_since,
22
+ )
23
+ from prediction_market_agent_tooling.tools.tavily.tavily_storage import TavilyStorage
24
+ from prediction_market_agent_tooling.tools.utils import check_not_none, utcnow
25
+
26
+ SUMMARISE_RELEVANT_NEWS_PROMPT_TEMPLATE = """
27
+ You are an expert news analyst, tracking stories that may affect your prediction to the outcome of a particular QUESTION.
28
+
29
+ Your role is to identify only the relevant information from a scraped news site (RAW_CONTENT), analyse it, and determine whether it contains developments or announcements occurring **after** the DATE_OF_INTEREST that could affect the outcome of the QUESTION.
30
+
31
+ Note that the news article may be published after the DATE_OF_INTEREST, but reference information that is older than the DATE_OF_INTEREST.
32
+
33
+ [QUESTION]
34
+ {question}
35
+
36
+ [DATE_OF_INTEREST]
37
+ {date_of_interest}
38
+
39
+ [RAW_CONTENT]
40
+ {raw_content}
41
+
42
+ For your analysis, you should:
43
+ - Discard the 'noise' from the raw content (e.g. ads, irrelevant content)
44
+ - Consider ONLY information that would have a notable impact on the outcome of the question.
45
+ - Consider ONLY information relating to an announcement or development that occurred **after** the DATE_OF_INTEREST.
46
+ - Present this information concisely in your reasoning.
47
+ - In your reasoning, do not use the term 'DATE_OF_INTEREST' directly. Use the actual date you are referring to instead.
48
+ - In your reasoning, do not use the term 'RAW_CONTENT' directly. Refer to it as 'the article', or quote the content you are referring to.
49
+
50
+ {format_instructions}
51
+ """
52
+
53
+
54
+ @observe()
55
+ def analyse_news_relevance(
56
+ raw_content: str,
57
+ question: str,
58
+ date_of_interest: datetime,
59
+ model: str,
60
+ temperature: float,
61
+ ) -> RelevantNewsAnalysis:
62
+ """
63
+ Analyse whether the news contains new (relative to the given date)
64
+ information relevant to the given question.
65
+ """
66
+ parser = PydanticOutputParser(pydantic_object=RelevantNewsAnalysis)
67
+ prompt = PromptTemplate(
68
+ template=SUMMARISE_RELEVANT_NEWS_PROMPT_TEMPLATE,
69
+ input_variables=["question", "date_of_interest", "raw_content"],
70
+ partial_variables={"format_instructions": parser.get_format_instructions()},
71
+ )
72
+ llm = ChatOpenAI(
73
+ temperature=temperature,
74
+ model=model,
75
+ api_key=APIKeys().openai_api_key_secretstr_v1,
76
+ )
77
+ chain = prompt | llm | parser
78
+
79
+ relevant_news_analysis: RelevantNewsAnalysis = chain.invoke(
80
+ {
81
+ "raw_content": raw_content,
82
+ "question": question,
83
+ "date_of_interest": str(date_of_interest),
84
+ },
85
+ config=get_langfuse_langchain_config(),
86
+ )
87
+ return relevant_news_analysis
88
+
89
+
90
+ @observe()
91
+ def get_certified_relevant_news_since(
92
+ question: str,
93
+ days_ago: int,
94
+ tavily_storage: TavilyStorage | None = None,
95
+ ) -> RelevantNews | None:
96
+ """
97
+ Get relevant news since a given date for a given question. Retrieves
98
+ possibly relevant news from tavily, then checks that it is relevant via
99
+ an LLM call.
100
+ """
101
+ results = get_relevant_news_since(
102
+ question=question,
103
+ days_ago=days_ago,
104
+ score_threshold=0.0, # Be conservative to avoid missing relevant information
105
+ max_results=3, # A tradeoff between cost and quality. 3 seems to be a good balance.
106
+ tavily_storage=tavily_storage,
107
+ )
108
+
109
+ # Sort results by descending 'relevance score' to maximise the chance of
110
+ # finding relevant news early
111
+ results = sorted(
112
+ results,
113
+ key=lambda result: result.score,
114
+ reverse=True,
115
+ )
116
+
117
+ for result in results:
118
+ relevant_news_analysis = analyse_news_relevance(
119
+ raw_content=check_not_none(result.raw_content),
120
+ question=question,
121
+ date_of_interest=utcnow() - timedelta(days=days_ago),
122
+ model="gpt-4o", # 4o-mini isn't good enough, 1o and 1o-mini are too expensive
123
+ temperature=0.0,
124
+ )
125
+
126
+ # Return first relevant news found
127
+ if relevant_news_analysis.contains_relevant_news:
128
+ return RelevantNews.from_tavily_result_and_analysis(
129
+ question=question,
130
+ days_ago=days_ago,
131
+ tavily_result=result,
132
+ relevant_news_analysis=relevant_news_analysis,
133
+ )
134
+
135
+ # No relevant news found
136
+ return None
137
+
138
+
139
+ def get_certified_relevant_news_since_cached(
140
+ question: str,
141
+ days_ago: int,
142
+ cache: RelevantNewsResponseCache,
143
+ tavily_storage: TavilyStorage | None = None,
144
+ ) -> RelevantNews | None:
145
+ cached = cache.find(question=question, days_ago=days_ago)
146
+
147
+ if isinstance(cached, NoRelevantNews):
148
+ return None
149
+ elif cached is None:
150
+ relevant_news = get_certified_relevant_news_since(
151
+ question=question,
152
+ days_ago=days_ago,
153
+ tavily_storage=tavily_storage,
154
+ )
155
+ cache.save(
156
+ question=question,
157
+ days_ago=days_ago,
158
+ relevant_news=relevant_news,
159
+ )
160
+ return relevant_news
161
+ else:
162
+ return cached
@@ -0,0 +1,90 @@
1
+ from datetime import datetime, timedelta
2
+
3
+ from pydantic import ValidationError
4
+ from sqlmodel import Field, Session, SQLModel, create_engine, desc, select
5
+
6
+ from prediction_market_agent_tooling.config import APIKeys
7
+ from prediction_market_agent_tooling.loggers import logger
8
+ from prediction_market_agent_tooling.tools.relevant_news_analysis.data_models import (
9
+ NoRelevantNews,
10
+ RelevantNews,
11
+ )
12
+ from prediction_market_agent_tooling.tools.utils import utcnow
13
+
14
+
15
+ class RelevantNewsCacheModel(SQLModel, table=True):
16
+ __tablename__ = "relevant_news_response_cache"
17
+ __table_args__ = {"extend_existing": True}
18
+ id: int | None = Field(default=None, primary_key=True)
19
+ question: str = Field(index=True)
20
+ datetime_: datetime = Field(index=True)
21
+ days_ago: int
22
+ json_dump: str | None
23
+
24
+
25
+ class RelevantNewsResponseCache:
26
+ def __init__(self, sqlalchemy_db_url: str | None = None):
27
+ self.engine = create_engine(
28
+ sqlalchemy_db_url
29
+ if sqlalchemy_db_url
30
+ else APIKeys().sqlalchemy_db_url.get_secret_value()
31
+ )
32
+ self._initialize_db()
33
+
34
+ def _initialize_db(self) -> None:
35
+ """
36
+ Creates the tables if they don't exist
37
+ """
38
+ with self.engine.connect() as conn:
39
+ SQLModel.metadata.create_all(
40
+ conn,
41
+ tables=[SQLModel.metadata.tables[RelevantNewsCacheModel.__tablename__]],
42
+ )
43
+
44
+ def find(
45
+ self,
46
+ question: str,
47
+ days_ago: int,
48
+ ) -> RelevantNews | NoRelevantNews | None:
49
+ with Session(self.engine) as session:
50
+ query = (
51
+ select(RelevantNewsCacheModel)
52
+ .where(RelevantNewsCacheModel.question == question)
53
+ .where(RelevantNewsCacheModel.days_ago <= days_ago)
54
+ .where(
55
+ RelevantNewsCacheModel.datetime_ >= utcnow() - timedelta(days=1)
56
+ ) # Cache entries expire after 1 day
57
+ )
58
+ item = session.exec(
59
+ query.order_by(desc(RelevantNewsCacheModel.datetime_))
60
+ ).first()
61
+
62
+ if item is None:
63
+ return None
64
+ else:
65
+ if item.json_dump is None:
66
+ return NoRelevantNews()
67
+ else:
68
+ try:
69
+ return RelevantNews.model_validate_json(item.json_dump)
70
+ except ValidationError as e:
71
+ logger.error(
72
+ f"Error deserializing RelevantNews from cache for {question=}, {days_ago=} and {item=}: {e}"
73
+ )
74
+ return None
75
+
76
+ def save(
77
+ self,
78
+ question: str,
79
+ days_ago: int,
80
+ relevant_news: RelevantNews | None,
81
+ ) -> None:
82
+ with Session(self.engine) as session:
83
+ cached = RelevantNewsCacheModel(
84
+ question=question,
85
+ days_ago=days_ago,
86
+ datetime_=utcnow(), # Assumes that the cache is being updated at the time the news is found
87
+ json_dump=relevant_news.model_dump_json() if relevant_news else None,
88
+ )
89
+ session.add(cached)
90
+ session.commit()
@@ -1,16 +1,16 @@
1
1
  from eth_account.signers.local import LocalAccount
2
2
  from eth_typing import ChecksumAddress
3
- from gnosis.eth import EthereumClient
4
- from gnosis.eth.constants import NULL_ADDRESS
5
- from gnosis.eth.contracts import get_safe_V1_4_1_contract
6
- from gnosis.safe.proxy_factory import ProxyFactoryV141
7
- from gnosis.safe.safe import Safe
8
3
  from safe_cli.safe_addresses import (
9
4
  get_default_fallback_handler_address,
10
5
  get_proxy_factory_address,
11
6
  get_safe_contract_address,
12
7
  get_safe_l2_contract_address,
13
8
  )
9
+ from safe_eth.eth import EthereumClient
10
+ from safe_eth.eth.constants import NULL_ADDRESS
11
+ from safe_eth.eth.contracts import get_safe_V1_4_1_contract
12
+ from safe_eth.safe.proxy_factory import ProxyFactoryV141
13
+ from safe_eth.safe.safe import SafeV141
14
14
  from web3.types import Wei
15
15
 
16
16
  from prediction_market_agent_tooling.loggers import logger
@@ -87,7 +87,7 @@ def create_safe(
87
87
 
88
88
  # We ignore mypy below because using the proper class SafeV141 yields an error and mypy
89
89
  # doesn't understand that there is a hacky factory method (__new__) on this abstract class.
90
- safe_version = Safe(safe_contract_address, ethereum_client).retrieve_version() # type: ignore
90
+ safe_version = SafeV141(safe_contract_address, ethereum_client).retrieve_version()
91
91
  logger.info(
92
92
  f"Safe-master-copy={safe_contract_address} version={safe_version}\n"
93
93
  f"Fallback-handler={fallback_handler}\n"
@@ -33,6 +33,11 @@ def tavily_search(
33
33
 
34
34
  Argument default values are different from the original method, to return everything by default, because it can be handy in the future and it doesn't increase the costs.
35
35
  """
36
+ if topic == "news" and days is None:
37
+ raise ValueError("When topic is 'news', days must be an integer")
38
+ if topic == "general" and days is not None:
39
+ raise ValueError("When topic is 'general', days must be None")
40
+
36
41
  if tavily_storage and (
37
42
  response_parsed := tavily_storage.find(
38
43
  query=query,
@@ -103,11 +108,15 @@ def _tavily_search(
103
108
  tavily = TavilyClient(
104
109
  api_key=(api_keys or APIKeys()).tavily_api_key.get_secret_value()
105
110
  )
111
+
112
+ # Optional `days` arg can only be specified if not None, otherwise Tavily
113
+ # will throw an error
114
+ kwargs = {"days": days} if days else {}
115
+
106
116
  response: dict[str, t.Any] = tavily.search(
107
117
  query=query,
108
118
  search_depth=search_depth,
109
119
  topic=topic,
110
- days=days,
111
120
  max_results=max_results,
112
121
  include_domains=include_domains,
113
122
  exclude_domains=exclude_domains,
@@ -115,11 +124,12 @@ def _tavily_search(
115
124
  include_raw_content=include_raw_content,
116
125
  include_images=include_images,
117
126
  use_cache=use_cache,
127
+ **kwargs,
118
128
  )
119
129
  return response
120
130
 
121
131
 
122
- def get_related_news_since(
132
+ def get_relevant_news_since(
123
133
  question: str,
124
134
  days_ago: int,
125
135
  score_threshold: float = DEFAULT_SCORE_THRESHOLD,
@@ -5,9 +5,9 @@ import base58
5
5
  import tenacity
6
6
  from eth_account import Account
7
7
  from eth_typing import URI
8
- from gnosis.eth import EthereumClient
9
- from gnosis.safe.safe import Safe
10
8
  from pydantic.types import SecretStr
9
+ from safe_eth.eth import EthereumClient
10
+ from safe_eth.safe.safe import SafeV141
11
11
  from web3 import Web3
12
12
  from web3.constants import HASH_ZERO
13
13
  from web3.types import AccessList, AccessListEntry, Nonce, TxParams, TxReceipt, Wei
@@ -200,7 +200,7 @@ def send_function_on_contract_tx(
200
200
  # Don't retry on `reverted` messages, as they would always fail again.
201
201
  retry=tenacity.retry_if_exception_message(match=NOT_REVERTED_ICASE_REGEX_PATTERN),
202
202
  wait=tenacity.wait_chain(*[tenacity.wait_fixed(n) for n in range(1, 10)]),
203
- stop=tenacity.stop_after_attempt(9),
203
+ stop=tenacity.stop_after_attempt(5),
204
204
  after=lambda x: logger.debug(
205
205
  f"send_function_on_contract_tx_using_safe failed, {x.attempt_number=}."
206
206
  ),
@@ -219,7 +219,7 @@ def send_function_on_contract_tx_using_safe(
219
219
  if not web3.provider.endpoint_uri: # type: ignore
220
220
  raise EnvironmentError("RPC_URL not available in web3 object.")
221
221
  ethereum_client = EthereumClient(ethereum_node_url=URI(web3.provider.endpoint_uri)) # type: ignore
222
- s = Safe(safe_address, ethereum_client) # type: ignore
222
+ s = SafeV141(safe_address, ethereum_client)
223
223
  safe_master_copy_address = s.retrieve_master_copy_address()
224
224
  eoa_public_key = private_key_to_public_key(from_private_key)
225
225
  # See https://ethereum.stackexchange.com/questions/123750/how-to-implement-eip-2930-access-list for details,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prediction-market-agent-tooling
3
- Version: 0.54.0
3
+ Version: 0.55.1
4
4
  Summary: Tools to benchmark, deploy and monitor prediction market agents.
5
5
  Author: Gnosis
6
6
  Requires-Python: >=3.10,<3.12
@@ -38,13 +38,13 @@ Requires-Dist: pydantic-settings (>=2.4.0,<3.0.0)
38
38
  Requires-Dist: pymongo (>=4.8.0,<5.0.0)
39
39
  Requires-Dist: python-dateutil (>=2.9.0.post0,<3.0.0)
40
40
  Requires-Dist: safe-cli (>=1.0.0,<2.0.0)
41
- Requires-Dist: safe-eth-py (>=6.0.0b14,<7.0.0)
41
+ Requires-Dist: safe-eth-py (>=6.0.0b41,<7.0.0)
42
42
  Requires-Dist: scikit-learn (>=1.3.1,<2.0.0)
43
43
  Requires-Dist: sqlmodel (>=0.0.22,<0.0.23)
44
44
  Requires-Dist: streamlit (>=1.31.0,<2.0.0)
45
45
  Requires-Dist: subgrounds (>=1.9.1,<2.0.0)
46
46
  Requires-Dist: tabulate (>=0.9.0,<0.10.0)
47
- Requires-Dist: tavily-python (>=0.3.9,<0.4.0)
47
+ Requires-Dist: tavily-python (>=0.5.0,<0.6.0)
48
48
  Requires-Dist: tqdm (>=4.66.2,<5.0.0)
49
49
  Requires-Dist: typer (>=0.9.0,<1.0.0)
50
50
  Requires-Dist: types-python-dateutil (>=2.9.0.20240906,<3.0.0.0)
@@ -16,21 +16,22 @@ prediction_market_agent_tooling/benchmark/__init__.py,sha256=47DEQpj8HBSa-_TImW-
16
16
  prediction_market_agent_tooling/benchmark/agents.py,sha256=B1-uWdyeN4GGKMWGK_-CcAFJg1m9Y_XuaeIHPB29QR8,3971
17
17
  prediction_market_agent_tooling/benchmark/benchmark.py,sha256=MqTiaaJ3cYiOLUVR7OyImLWxcEya3Rl5JyFYW-K0lwM,17097
18
18
  prediction_market_agent_tooling/benchmark/utils.py,sha256=D0MfUkVZllmvcU0VOurk9tcKT7JTtwwOp-63zuCBVuc,2880
19
- prediction_market_agent_tooling/config.py,sha256=WC30Nr16RGueTafA9i67OIB-6KDHZRryhiLPzebg9_I,6740
20
- prediction_market_agent_tooling/deploy/agent.py,sha256=OAfD9B0rN6k3eISvRFJpHTS_dW4yMM2GlTvG8FMejd8,19232
19
+ prediction_market_agent_tooling/config.py,sha256=114f3V9abaok27p5jX3UVr5b5gRUiSxBIYn8Snid34I,6731
20
+ prediction_market_agent_tooling/deploy/agent.py,sha256=s3XVNeuJ9mtlfsRB1RWLWUR0q9fMoptZqu1u-I6oiws,22420
21
21
  prediction_market_agent_tooling/deploy/agent_example.py,sha256=dIIdZashExWk9tOdyDjw87AuUcGyM7jYxNChYrVK2dM,1001
22
- prediction_market_agent_tooling/deploy/betting_strategy.py,sha256=egarsbIiFDwCLQ0YtqzP3jdht2_PfhlT0yAmwYGJ10o,13009
22
+ prediction_market_agent_tooling/deploy/betting_strategy.py,sha256=kMrIE3wMv_IB6nJd_1DmDXDkEZhsXFOgyTd7JZ0gqHI,13068
23
23
  prediction_market_agent_tooling/deploy/constants.py,sha256=M5ty8URipYMGe_G-RzxRydK3AFL6CyvmqCraJUrLBnE,82
24
24
  prediction_market_agent_tooling/deploy/gcp/deploy.py,sha256=CYUgnfy-9XVk04kkxA_5yp0GE9Mw5caYqlFUZQ2j3ks,3739
25
25
  prediction_market_agent_tooling/deploy/gcp/kubernetes_models.py,sha256=OsPboCFGiZKsvGyntGZHwdqPlLTthITkNF5rJFvGgU8,2582
26
26
  prediction_market_agent_tooling/deploy/gcp/utils.py,sha256=oyW0jgrUT2Tr49c7GlpcMsYNQjoCSOcWis3q-MmVAhU,6089
27
+ prediction_market_agent_tooling/deploy/trade_interval.py,sha256=Xk9j45alQ_vrasGvsNyuW70XHIQ7wfvjoxNR3F6HYCw,1155
27
28
  prediction_market_agent_tooling/gtypes.py,sha256=tqp03PyY0Yhievl4XELfwAn0xOoecaTvBZ1Co6b-A7o,2541
28
29
  prediction_market_agent_tooling/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
30
  prediction_market_agent_tooling/jobs/jobs.py,sha256=I07yh0GJ-xhlvQaOUQB8xlSnihhcbU2c7DZ4ZND14c0,1246
30
31
  prediction_market_agent_tooling/jobs/jobs_models.py,sha256=I5uBTHJ2S1Wi3H4jDxxU7nsswSIP9r3BevHmljLh5Pg,1370
31
32
  prediction_market_agent_tooling/jobs/omen/omen_jobs.py,sha256=I2_vGrEJj1reSI8M377ab5QCsYNp_l4l4QeYEmDBkFM,3989
32
33
  prediction_market_agent_tooling/loggers.py,sha256=Am6HHXRNO545BO3l7Ue9Wb2TkYE1OK8KKhGbI3XypVU,3751
33
- prediction_market_agent_tooling/markets/agent_market.py,sha256=A72Lf7bdFOTelLhUTSHmfLBOuTZ8b0Qd_IJIgdjzeB0,12470
34
+ prediction_market_agent_tooling/markets/agent_market.py,sha256=OgB6bvDGfTAxbh6cDGD3XFO0iy0MAaOQvXEP6nw8xW8,12817
34
35
  prediction_market_agent_tooling/markets/categorize.py,sha256=jsoHWvZk9pU6n17oWSCcCxNNYVwlb_NXsZxKRI7vmsk,1301
35
36
  prediction_market_agent_tooling/markets/data_models.py,sha256=jMqrSFO_w2z-5N3PFVgZqTHdVdkzSDhhzky2lHsGGKA,3621
36
37
  prediction_market_agent_tooling/markets/manifold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -41,11 +42,11 @@ prediction_market_agent_tooling/markets/manifold/utils.py,sha256=cPPFWXm3vCYH1jy
41
42
  prediction_market_agent_tooling/markets/market_fees.py,sha256=Q64T9uaJx0Vllt0BkrPmpMEz53ra-hMVY8Czi7CEP7s,1227
42
43
  prediction_market_agent_tooling/markets/markets.py,sha256=mwubc567OIlA32YKqlIdTloYV8FGJia9gPv0wE0xUEA,3368
43
44
  prediction_market_agent_tooling/markets/metaculus/api.py,sha256=4TRPGytQQbSdf42DCg2M_JWYPAuNjqZ3eBqaQBLkNks,2736
44
- prediction_market_agent_tooling/markets/metaculus/data_models.py,sha256=2wDZ0BmK9O5Lud-q-FCzgW0tsK9GxMU0rUMlcPxSS04,3184
45
- prediction_market_agent_tooling/markets/metaculus/metaculus.py,sha256=d5tHhekLfPGO6CfmWWJQWpJu5HyoAWBM2aGSR874Cms,3695
45
+ prediction_market_agent_tooling/markets/metaculus/data_models.py,sha256=Suxa7xELdYuFNKqvGvFh8qyfVtAg79E-vaQ6dqNZOtA,3261
46
+ prediction_market_agent_tooling/markets/metaculus/metaculus.py,sha256=E_TUf5q73lWzdMp40Ne-3w4MjEd7AHcaif4pvFh9FMU,4360
46
47
  prediction_market_agent_tooling/markets/omen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
48
  prediction_market_agent_tooling/markets/omen/data_models.py,sha256=nCjsc-ylIzQOCK_1BW-5NoYrS-NIXz2Hg9N1-IqhhC8,27516
48
- prediction_market_agent_tooling/markets/omen/omen.py,sha256=-9u8Tb3ADGy7vd0E0peTV8OqQi9zMaXBa4b0gJXpQpc,50773
49
+ prediction_market_agent_tooling/markets/omen/omen.py,sha256=LqNZjngo6LoktKecfYmGmJJ9D5rj-s0Poy4x4_GZfp0,51116
49
50
  prediction_market_agent_tooling/markets/omen/omen_contracts.py,sha256=Zq7SncCq-hvpgXKsVruGBGCn1OhKZTe7r1qLdCTrT2w,28297
50
51
  prediction_market_agent_tooling/markets/omen/omen_resolving.py,sha256=iDWdjICGkt968exwCjY-6nsnQyrrNAg3YjnDdP430GQ,9415
51
52
  prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py,sha256=zQH3iu0SVH1RmE-W3NMEpcKVMILXJYxMhL6w1wh5RUo,37348
@@ -79,22 +80,25 @@ prediction_market_agent_tooling/tools/httpx_cached_client.py,sha256=0-N1r0zcGKlY
79
80
  prediction_market_agent_tooling/tools/image_gen/image_gen.py,sha256=HzRwBx62hOXBOmrtpkXaP9Qq1Ku03uUGdREocyjLQ_k,1266
80
81
  prediction_market_agent_tooling/tools/image_gen/market_thumbnail_gen.py,sha256=8A3U2uxsCsOfLjru-6R_PPIAuiKY4qFkWp_GSBPV6-s,1280
81
82
  prediction_market_agent_tooling/tools/ipfs/ipfs_handler.py,sha256=CTTMfTvs_8PH4kAtlQby2aeEKwgpmxtuGbd4oYIdJ2A,1201
82
- prediction_market_agent_tooling/tools/is_invalid.py,sha256=Lc5fWB4fmx7tFvRakmUOzo0Oq5EizorddZ2xjesEopY,4984
83
+ prediction_market_agent_tooling/tools/is_invalid.py,sha256=VjjOCrkt6S8ytOg_0s2gUL8IKX2muWq1QLIZX0MPMxY,5094
83
84
  prediction_market_agent_tooling/tools/is_predictable.py,sha256=NIoR2bTNMmADcyNY2aKNMWkiDw7Z_9kZMcFXEdyewy4,6771
84
85
  prediction_market_agent_tooling/tools/langfuse_.py,sha256=jI_4ROxqo41CCnWGS1vN_AeDVhRzLMaQLxH3kxDu3L8,1153
85
86
  prediction_market_agent_tooling/tools/langfuse_client_utils.py,sha256=B0PhAQyviFnVbtOCYMxYmcCn66cu9nbqAOIAZcdgiRI,5771
86
87
  prediction_market_agent_tooling/tools/omen/reality_accuracy.py,sha256=M1SF7iSW1gVlQSTskdVFTn09uPLST23YeipVIWj54io,2236
87
88
  prediction_market_agent_tooling/tools/parallelism.py,sha256=6Gou0hbjtMZrYvxjTDFUDZuxmE2nqZVbb6hkg1hF82A,1022
88
- prediction_market_agent_tooling/tools/safe.py,sha256=h0xOO0eNtitClf0fPkn-0oTc6A_bflDTee98V_aiV-A,5195
89
+ prediction_market_agent_tooling/tools/relevant_news_analysis/data_models.py,sha256=95l84aztFaxcRLLcRQ46yKJbIlOEuDAbIGLouyliDzA,1316
90
+ prediction_market_agent_tooling/tools/relevant_news_analysis/relevant_news_analysis.py,sha256=OWLzwCbQS2b9hjwTRXTOjjplWXcGXFf3yjKEeK4kGbQ,5720
91
+ prediction_market_agent_tooling/tools/relevant_news_analysis/relevant_news_cache.py,sha256=2yxtBIDyMT_6CsTpZyuIv_2dy2B9WgEOaTT1fSloBu0,3223
92
+ prediction_market_agent_tooling/tools/safe.py,sha256=9vxGGLvSPnfy-sxUFDpBTe8omqpGXP7MzvGPp6bRxrU,5197
89
93
  prediction_market_agent_tooling/tools/singleton.py,sha256=CiIELUiI-OeS7U7eeHEt0rnVhtQGzwoUdAgn_7u_GBM,729
90
94
  prediction_market_agent_tooling/tools/streamlit_user_login.py,sha256=NXEqfjT9Lc9QtliwSGRASIz1opjQ7Btme43H4qJbzgE,3010
91
95
  prediction_market_agent_tooling/tools/tavily/tavily_models.py,sha256=Rz4tZzwCRzPaq49SFT33SCRQrqHXtqWdD9ajb2tGCWc,2723
92
- prediction_market_agent_tooling/tools/tavily/tavily_search.py,sha256=MK_ozeQbJ014HGiKFPDScjFYq0OGcjY1KPgc9A6qO0M,4511
96
+ prediction_market_agent_tooling/tools/tavily/tavily_search.py,sha256=UPSp0S5Sql52X6UlU2Ki_iO-gmDJSMs5enn9AV_IZRM,4896
93
97
  prediction_market_agent_tooling/tools/tavily/tavily_storage.py,sha256=t-tZzbCzBBdFedRZDuVBn3A3mIDX8Z5wza6SxWswu_E,4093
94
98
  prediction_market_agent_tooling/tools/utils.py,sha256=W-9SqeCKd51BYMRhDjYPQ7lfNO_zE9EvYpmu2r5WXGA,7163
95
- prediction_market_agent_tooling/tools/web3_utils.py,sha256=dkcjG-LtuaWRh7WEMzRGmZ5B5rsxZTlliFOI6fj-EJ8,11842
96
- prediction_market_agent_tooling-0.54.0.dist-info/LICENSE,sha256=6or154nLLU6bELzjh0mCreFjt0m2v72zLi3yHE0QbeE,7650
97
- prediction_market_agent_tooling-0.54.0.dist-info/METADATA,sha256=iAGimZ2Kjtbx94FpzKsdYXKvhFO9e3Bd6xaAG5eQVg4,8056
98
- prediction_market_agent_tooling-0.54.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
99
- prediction_market_agent_tooling-0.54.0.dist-info/entry_points.txt,sha256=m8PukHbeH5g0IAAmOf_1Ahm-sGAMdhSSRQmwtpmi2s8,81
100
- prediction_market_agent_tooling-0.54.0.dist-info/RECORD,,
99
+ prediction_market_agent_tooling/tools/web3_utils.py,sha256=44W8siSLNQxeib98bbwAe7V5C609NHNlUuxwuWIRDiY,11838
100
+ prediction_market_agent_tooling-0.55.1.dist-info/LICENSE,sha256=6or154nLLU6bELzjh0mCreFjt0m2v72zLi3yHE0QbeE,7650
101
+ prediction_market_agent_tooling-0.55.1.dist-info/METADATA,sha256=chzNuISP7K3sgt_Mj0fFX6bQu9JFkOXTvYgmqaLFcMU,8056
102
+ prediction_market_agent_tooling-0.55.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
103
+ prediction_market_agent_tooling-0.55.1.dist-info/entry_points.txt,sha256=m8PukHbeH5g0IAAmOf_1Ahm-sGAMdhSSRQmwtpmi2s8,81
104
+ prediction_market_agent_tooling-0.55.1.dist-info/RECORD,,