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.
- prediction_market_agent_tooling/config.py +3 -3
- prediction_market_agent_tooling/deploy/agent.py +172 -90
- prediction_market_agent_tooling/deploy/betting_strategy.py +5 -1
- prediction_market_agent_tooling/deploy/trade_interval.py +46 -0
- prediction_market_agent_tooling/markets/agent_market.py +12 -1
- prediction_market_agent_tooling/markets/metaculus/data_models.py +4 -1
- prediction_market_agent_tooling/markets/metaculus/metaculus.py +30 -8
- prediction_market_agent_tooling/markets/omen/omen.py +14 -6
- prediction_market_agent_tooling/tools/is_invalid.py +4 -3
- prediction_market_agent_tooling/tools/relevant_news_analysis/data_models.py +44 -0
- prediction_market_agent_tooling/tools/relevant_news_analysis/relevant_news_analysis.py +162 -0
- prediction_market_agent_tooling/tools/relevant_news_analysis/relevant_news_cache.py +90 -0
- prediction_market_agent_tooling/tools/safe.py +6 -6
- prediction_market_agent_tooling/tools/tavily/tavily_search.py +12 -2
- prediction_market_agent_tooling/tools/web3_utils.py +4 -4
- {prediction_market_agent_tooling-0.54.0.dist-info → prediction_market_agent_tooling-0.55.1.dist-info}/METADATA +3 -3
- {prediction_market_agent_tooling-0.54.0.dist-info → prediction_market_agent_tooling-0.55.1.dist-info}/RECORD +20 -16
- {prediction_market_agent_tooling-0.54.0.dist-info → prediction_market_agent_tooling-0.55.1.dist-info}/LICENSE +0 -0
- {prediction_market_agent_tooling-0.54.0.dist-info → prediction_market_agent_tooling-0.55.1.dist-info}/WHEEL +0 -0
- {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 =
|
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
|
169
|
-
cls.__init__
|
170
|
-
|
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
|
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
|
-
|
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
|
-
|
296
|
+
store_prediction: bool = True,
|
287
297
|
) -> None:
|
288
298
|
super().__init__(enable_langfuse=enable_langfuse)
|
289
|
-
self.
|
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(
|
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=
|
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
|
-
|
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
|
-
|
444
|
-
|
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
|
-
|
454
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
31
|
-
|
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
|
107
|
-
|
108
|
-
|
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
|
-
|
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
|
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(
|
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
|
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? (
|
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:
|
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 =
|
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
|
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(
|
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 =
|
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.
|
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.
|
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.
|
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=
|
20
|
-
prediction_market_agent_tooling/deploy/agent.py,sha256=
|
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=
|
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=
|
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=
|
45
|
-
prediction_market_agent_tooling/markets/metaculus/metaculus.py,sha256=
|
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
|
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=
|
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/
|
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=
|
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=
|
96
|
-
prediction_market_agent_tooling-0.
|
97
|
-
prediction_market_agent_tooling-0.
|
98
|
-
prediction_market_agent_tooling-0.
|
99
|
-
prediction_market_agent_tooling-0.
|
100
|
-
prediction_market_agent_tooling-0.
|
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,,
|
File without changes
|
File without changes
|