prediction-market-agent-tooling 0.48.11__py3-none-any.whl → 0.48.12__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.
@@ -15,6 +15,7 @@ from prediction_market_agent_tooling.config import APIKeys
15
15
  from prediction_market_agent_tooling.deploy.betting_strategy import (
16
16
  BettingStrategy,
17
17
  MaxAccuracyBettingStrategy,
18
+ TradeType,
18
19
  )
19
20
  from prediction_market_agent_tooling.deploy.constants import (
20
21
  MARKET_TYPE_KEY,
@@ -37,10 +38,9 @@ from prediction_market_agent_tooling.markets.agent_market import (
37
38
  SortBy,
38
39
  )
39
40
  from prediction_market_agent_tooling.markets.data_models import (
40
- BetAmount,
41
+ Position,
41
42
  ProbabilisticAnswer,
42
- TokenAmount,
43
- TokenAmountAndDirection,
43
+ Trade,
44
44
  )
45
45
  from prediction_market_agent_tooling.markets.markets import (
46
46
  MarketType,
@@ -110,7 +110,7 @@ class OutOfFundsError(ValueError):
110
110
 
111
111
  class ProcessedMarket(BaseModel):
112
112
  answer: ProbabilisticAnswer
113
- amount: BetAmount
113
+ trades: list[Trade]
114
114
 
115
115
 
116
116
  class AnsweredEnum(str, Enum):
@@ -283,6 +283,7 @@ class DeployableTraderAgent(DeployableAgent):
283
283
  min_required_balance_to_operate: xDai | None = xdai_type(1)
284
284
  min_balance_to_keep_in_native_currency: xDai | None = xdai_type(0.1)
285
285
  strategy: BettingStrategy = MaxAccuracyBettingStrategy()
286
+ allow_opposite_bets: bool = False
286
287
 
287
288
  def __init__(
288
289
  self,
@@ -298,8 +299,8 @@ class DeployableTraderAgent(DeployableAgent):
298
299
  self.have_bet_on_market_since = observe()(self.have_bet_on_market_since) # type: ignore[method-assign]
299
300
  self.verify_market = observe()(self.verify_market) # type: ignore[method-assign]
300
301
  self.answer_binary_market = observe()(self.answer_binary_market) # type: ignore[method-assign]
301
- self.calculate_bet_amount_and_direction = observe()(self.calculate_bet_amount_and_direction) # type: ignore[method-assign]
302
302
  self.process_market = observe()(self.process_market) # type: ignore[method-assign]
303
+ self.build_trades = observe()(self.build_trades) # type: ignore[method-assign]
303
304
 
304
305
  def update_langfuse_trace_by_market(
305
306
  self, market_type: MarketType, market: AgentMarket
@@ -314,18 +315,6 @@ class DeployableTraderAgent(DeployableAgent):
314
315
  },
315
316
  )
316
317
 
317
- def calculate_bet_amount_and_direction(
318
- self, answer: ProbabilisticAnswer, market: AgentMarket
319
- ) -> TokenAmountAndDirection:
320
- amount_and_direction = self.strategy.calculate_bet_amount_and_direction(
321
- answer, market
322
- )
323
- if amount_and_direction.currency != market.currency:
324
- raise ValueError(
325
- f"Currency mismatch. Strategy yields {amount_and_direction.currency}, market has currency {market.currency}"
326
- )
327
- return amount_and_direction
328
-
329
318
  def update_langfuse_trace_by_processed_market(
330
319
  self, market_type: MarketType, processed_market: ProcessedMarket | None
331
320
  ) -> None:
@@ -416,13 +405,26 @@ class DeployableTraderAgent(DeployableAgent):
416
405
  )
417
406
  return available_markets
418
407
 
408
+ def build_trades(
409
+ self,
410
+ market: AgentMarket,
411
+ answer: ProbabilisticAnswer,
412
+ existing_position: Position | None,
413
+ ) -> list[Trade]:
414
+ trades = self.strategy.calculate_trades(existing_position, answer, market)
415
+ BettingStrategy.assert_trades_currency_match_markets(market, trades)
416
+ return trades
417
+
419
418
  def before_process_market(
420
419
  self, market_type: MarketType, market: AgentMarket
421
420
  ) -> None:
422
421
  self.update_langfuse_trace_by_market(market_type, market)
423
422
 
424
423
  def process_market(
425
- self, market_type: MarketType, market: AgentMarket, verify_market: bool = True
424
+ self,
425
+ market_type: MarketType,
426
+ market: AgentMarket,
427
+ verify_market: bool = True,
426
428
  ) -> ProcessedMarket | None:
427
429
  self.before_process_market(market_type, market)
428
430
 
@@ -438,26 +440,26 @@ class DeployableTraderAgent(DeployableAgent):
438
440
  self.update_langfuse_trace_by_processed_market(market_type, None)
439
441
  return None
440
442
 
441
- amount_and_direction = self.calculate_bet_amount_and_direction(answer, market)
443
+ existing_position = market.get_position(user_id=APIKeys().bet_from_address)
444
+ trades = self.build_trades(
445
+ market=market, answer=answer, existing_position=existing_position
446
+ )
442
447
 
443
448
  if self.place_bet:
444
- logger.info(
445
- f"Placing bet on {market} with direction {amount_and_direction.direction} and amount {amount_and_direction.amount}"
446
- )
447
- market.place_bet(
448
- amount=TokenAmount(
449
- amount=amount_and_direction.amount,
450
- currency=amount_and_direction.currency,
451
- ),
452
- outcome=amount_and_direction.direction,
453
- )
449
+ for trade in trades:
450
+ logger.info(f"Executing trade {trade}")
451
+
452
+ match trade.trade_type:
453
+ case TradeType.BUY:
454
+ market.buy_tokens(outcome=trade.outcome, amount=trade.amount)
455
+ case TradeType.SELL:
456
+ market.sell_tokens(outcome=trade.outcome, amount=trade.amount)
457
+ case _:
458
+ raise ValueError(f"Unexpected trade type {trade.trade_type}.")
454
459
 
455
460
  self.after_process_market(market_type, market)
456
461
 
457
- processed_market = ProcessedMarket(
458
- answer=answer,
459
- amount=amount_and_direction,
460
- )
462
+ processed_market = ProcessedMarket(answer=answer, trades=trades)
461
463
  self.update_langfuse_trace_by_processed_market(market_type, processed_market)
462
464
 
463
465
  return processed_market
@@ -2,9 +2,14 @@ from abc import ABC, abstractmethod
2
2
 
3
3
  from prediction_market_agent_tooling.markets.agent_market import AgentMarket
4
4
  from prediction_market_agent_tooling.markets.data_models import (
5
+ Currency,
6
+ Position,
5
7
  ProbabilisticAnswer,
6
- TokenAmountAndDirection,
8
+ TokenAmount,
9
+ Trade,
10
+ TradeType,
7
11
  )
12
+ from prediction_market_agent_tooling.markets.omen.data_models import get_boolean_outcome
8
13
  from prediction_market_agent_tooling.tools.betting_strategies.kelly_criterion import (
9
14
  get_kelly_bet,
10
15
  )
@@ -12,51 +17,171 @@ from prediction_market_agent_tooling.tools.betting_strategies.kelly_criterion im
12
17
 
13
18
  class BettingStrategy(ABC):
14
19
  @abstractmethod
15
- def calculate_bet_amount_and_direction(
16
- self, answer: ProbabilisticAnswer, market: AgentMarket
17
- ) -> TokenAmountAndDirection:
20
+ def calculate_trades(
21
+ self,
22
+ existing_position: Position | None,
23
+ answer: ProbabilisticAnswer,
24
+ market: AgentMarket,
25
+ ) -> list[Trade]:
18
26
  pass
19
27
 
28
+ def build_zero_token_amount(self, currency: Currency) -> TokenAmount:
29
+ return TokenAmount(amount=0, currency=currency)
20
30
 
21
- class MaxAccuracyBettingStrategy(BettingStrategy):
22
- def __init__(self, bet_amount: float | None = None):
23
- self.bet_amount = bet_amount
31
+ @abstractmethod
32
+ def adjust_bet_amount(
33
+ self, existing_position: Position | None, market: AgentMarket
34
+ ) -> float:
35
+ pass
24
36
 
25
37
  @staticmethod
26
- def calculate_direction(market_p_yes: float, estimate_p_yes: float) -> bool:
27
- # If estimate_p_yes >= market.current_p_yes, then bet TRUE, else bet FALSE.
28
- # This is equivalent to saying EXPECTED_VALUE = (estimate_p_yes * num_tokens_obtained_by_betting_yes) -
29
- # ((1 - estimate_p_yes) * num_tokens_obtained_by_betting_no) >= 0
30
- return estimate_p_yes >= market_p_yes
38
+ def assert_trades_currency_match_markets(
39
+ market: AgentMarket, trades: list[Trade]
40
+ ) -> None:
41
+ currencies_match = all([t.amount.currency == market.currency for t in trades])
42
+ if not currencies_match:
43
+ raise ValueError(
44
+ "Cannot handle trades with currencies that deviate from market's currency"
45
+ )
31
46
 
32
- def calculate_bet_amount_and_direction(
33
- self, answer: ProbabilisticAnswer, market: AgentMarket
34
- ) -> TokenAmountAndDirection:
47
+ def _build_rebalance_trades_from_positions(
48
+ self,
49
+ existing_position: Position | None,
50
+ target_position: Position,
51
+ market: AgentMarket,
52
+ ) -> list[Trade]:
53
+ """
54
+ This helper method builds trades by rebalancing token allocations to each outcome.
55
+ For example, if we have an existing position with 10 tokens in outcome 0 and 5 in outcome 1,
56
+ and our target position is 20 tokens in outcome 0 and 0 in outcome 1, we would return these trades:
57
+ trades = [
58
+ Trade(outcome=0, amount=10, trade_type=TradeType.BUY),
59
+ Trade(outcome=1, amount=5, trade_type=TradeType.SELL)
60
+ ]
61
+ Note that we order the trades to first buy then sell, in order to minimally tilt the odds so that
62
+ sell price is higher.
63
+ """
64
+ trades = []
65
+ for outcome in [
66
+ market.get_outcome_str_from_bool(True),
67
+ market.get_outcome_str_from_bool(False),
68
+ ]:
69
+ outcome_bool = get_boolean_outcome(outcome)
70
+ prev_amount: TokenAmount = (
71
+ existing_position.amounts[outcome]
72
+ if existing_position and outcome in existing_position.amounts
73
+ else self.build_zero_token_amount(currency=market.currency)
74
+ )
75
+ new_amount: TokenAmount = target_position.amounts.get(
76
+ outcome, self.build_zero_token_amount(currency=market.currency)
77
+ )
78
+
79
+ if prev_amount.currency != new_amount.currency:
80
+ raise ValueError("Cannot handle positions with different currencies")
81
+ diff_amount = prev_amount.amount - new_amount.amount
82
+ if diff_amount == 0:
83
+ continue
84
+ trade_type = TradeType.SELL if diff_amount < 0 else TradeType.BUY
85
+ trade = Trade(
86
+ amount=TokenAmount(amount=abs(diff_amount), currency=market.currency),
87
+ outcome=outcome_bool,
88
+ trade_type=trade_type,
89
+ )
90
+
91
+ trades.append(trade)
92
+
93
+ # Sort inplace with SELL last
94
+ trades.sort(key=lambda t: t.trade_type == TradeType.SELL)
95
+ BettingStrategy.assert_trades_currency_match_markets(market, trades)
96
+ return trades
97
+
98
+
99
+ class MaxAccuracyBettingStrategy(BettingStrategy):
100
+ def adjust_bet_amount(
101
+ self, existing_position: Position | None, market: AgentMarket
102
+ ) -> float:
103
+ existing_position_total_amount = (
104
+ existing_position.total_amount.amount if existing_position else 0
105
+ )
35
106
  bet_amount = (
36
107
  market.get_tiny_bet_amount().amount
37
108
  if self.bet_amount is None
38
109
  else self.bet_amount
39
110
  )
111
+ return bet_amount + existing_position_total_amount
112
+
113
+ def __init__(self, bet_amount: float | None = None):
114
+ self.bet_amount = bet_amount
115
+
116
+ def calculate_trades(
117
+ self,
118
+ existing_position: Position | None,
119
+ answer: ProbabilisticAnswer,
120
+ market: AgentMarket,
121
+ ) -> list[Trade]:
122
+ adjusted_bet_amount = self.adjust_bet_amount(existing_position, market)
123
+
40
124
  direction = self.calculate_direction(market.current_p_yes, answer.p_yes)
41
- return TokenAmountAndDirection(
42
- amount=bet_amount,
43
- currency=market.currency,
44
- direction=direction,
125
+
126
+ amounts = {
127
+ market.get_outcome_str_from_bool(direction): TokenAmount(
128
+ amount=adjusted_bet_amount,
129
+ currency=market.currency,
130
+ ),
131
+ }
132
+ target_position = Position(market_id=market.id, amounts=amounts)
133
+ trades = self._build_rebalance_trades_from_positions(
134
+ existing_position, target_position, market=market
45
135
  )
136
+ return trades
137
+
138
+ @staticmethod
139
+ def calculate_direction(market_p_yes: float, estimate_p_yes: float) -> bool:
140
+ return estimate_p_yes >= 0.5
141
+
142
+
143
+ class MaxExpectedValueBettingStrategy(MaxAccuracyBettingStrategy):
144
+ @staticmethod
145
+ def calculate_direction(market_p_yes: float, estimate_p_yes: float) -> bool:
146
+ # If estimate_p_yes >= market.current_p_yes, then bet TRUE, else bet FALSE.
147
+ # This is equivalent to saying EXPECTED_VALUE = (estimate_p_yes * num_tokens_obtained_by_betting_yes) -
148
+ # ((1 - estimate_p_yes) * num_tokens_obtained_by_betting_no) >= 0
149
+ return estimate_p_yes >= market_p_yes
46
150
 
47
151
 
48
152
  class KellyBettingStrategy(BettingStrategy):
49
153
  def __init__(self, max_bet_amount: float = 10):
50
154
  self.max_bet_amount = max_bet_amount
51
155
 
52
- def calculate_bet_amount_and_direction(
53
- self, answer: ProbabilisticAnswer, market: AgentMarket
54
- ) -> TokenAmountAndDirection:
156
+ def adjust_bet_amount(
157
+ self, existing_position: Position | None, market: AgentMarket
158
+ ) -> float:
159
+ existing_position_total_amount = (
160
+ existing_position.total_amount.amount if existing_position else 0
161
+ )
162
+ return self.max_bet_amount + existing_position_total_amount
163
+
164
+ def calculate_trades(
165
+ self,
166
+ existing_position: Position | None,
167
+ answer: ProbabilisticAnswer,
168
+ market: AgentMarket,
169
+ ) -> list[Trade]:
170
+ adjusted_bet_amount = self.adjust_bet_amount(existing_position, market)
55
171
  kelly_bet = get_kelly_bet(
56
- self.max_bet_amount, market.current_p_yes, answer.p_yes, answer.confidence
172
+ adjusted_bet_amount,
173
+ market.current_p_yes,
174
+ answer.p_yes,
175
+ answer.confidence,
57
176
  )
58
- return TokenAmountAndDirection(
59
- amount=kelly_bet.size,
60
- currency=market.currency,
61
- direction=kelly_bet.direction,
177
+
178
+ amounts = {
179
+ market.get_outcome_str_from_bool(kelly_bet.direction): TokenAmount(
180
+ amount=kelly_bet.size, currency=market.currency
181
+ ),
182
+ }
183
+ target_position = Position(market_id=market.id, amounts=amounts)
184
+ trades = self._build_rebalance_trades_from_positions(
185
+ existing_position, target_position, market=market
62
186
  )
187
+ return trades
@@ -4,9 +4,10 @@ from enum import Enum
4
4
 
5
5
  from eth_typing import ChecksumAddress
6
6
  from pydantic import BaseModel, field_validator
7
+ from pydantic_core.core_schema import FieldValidationInfo
7
8
 
8
9
  from prediction_market_agent_tooling.config import APIKeys
9
- from prediction_market_agent_tooling.gtypes import Probability
10
+ from prediction_market_agent_tooling.gtypes import OutcomeStr, Probability
10
11
  from prediction_market_agent_tooling.markets.data_models import (
11
12
  Bet,
12
13
  BetAmount,
@@ -49,6 +50,9 @@ class AgentMarket(BaseModel):
49
50
  question: str
50
51
  description: str | None
51
52
  outcomes: list[str]
53
+ outcome_token_pool: dict[
54
+ str, float
55
+ ] | None # Should be in currency of `currency` above.
52
56
  resolution: Resolution | None
53
57
  created_time: datetime | None
54
58
  close_time: datetime | None
@@ -63,6 +67,22 @@ class AgentMarket(BaseModel):
63
67
  add_utc_timezone_validator
64
68
  )
65
69
 
70
+ @field_validator("outcome_token_pool")
71
+ def validate_outcome_token_pool(
72
+ cls,
73
+ outcome_token_pool: dict[str, float] | None,
74
+ info: FieldValidationInfo,
75
+ ) -> dict[str, float] | None:
76
+ outcomes: list[str] = check_not_none(info.data.get("outcomes"))
77
+ if outcome_token_pool is not None:
78
+ outcome_keys = set(outcome_token_pool.keys())
79
+ expected_keys = set(outcomes)
80
+ if outcome_keys != expected_keys:
81
+ raise ValueError(
82
+ f"Keys of outcome_token_pool ({outcome_keys}) do not match outcomes ({expected_keys})."
83
+ )
84
+ return outcome_token_pool
85
+
66
86
  @property
67
87
  def current_p_no(self) -> Probability:
68
88
  return Probability(1 - self.current_p_yes)
@@ -133,10 +153,19 @@ class AgentMarket(BaseModel):
133
153
  def get_bet_amount(self, amount: float) -> BetAmount:
134
154
  return BetAmount(amount=amount, currency=self.currency)
135
155
 
156
+ @classmethod
157
+ def get_liquidatable_amount(cls) -> BetAmount:
158
+ tiny_amount = cls.get_tiny_bet_amount()
159
+ tiny_amount.amount /= 10
160
+ return tiny_amount
161
+
136
162
  @classmethod
137
163
  def get_tiny_bet_amount(cls) -> BetAmount:
138
164
  raise NotImplementedError("Subclasses must implement this method")
139
165
 
166
+ def liquidate_existing_positions(self, outcome: bool) -> None:
167
+ raise NotImplementedError("Subclasses must implement this method")
168
+
140
169
  def place_bet(self, outcome: bool, amount: BetAmount) -> None:
141
170
  raise NotImplementedError("Subclasses must implement this method")
142
171
 
@@ -190,6 +219,9 @@ class AgentMarket(BaseModel):
190
219
  def has_unsuccessful_resolution(self) -> bool:
191
220
  return self.resolution in [Resolution.CANCEL, Resolution.MKT]
192
221
 
222
+ def get_outcome_str_from_bool(self, outcome: bool) -> OutcomeStr:
223
+ raise NotImplementedError("Subclasses must implement this method")
224
+
193
225
  def get_outcome_str(self, outcome_index: int) -> str:
194
226
  try:
195
227
  return self.outcomes[outcome_index]
@@ -207,6 +239,9 @@ class AgentMarket(BaseModel):
207
239
  def get_token_balance(self, user_id: str, outcome: str) -> TokenAmount:
208
240
  raise NotImplementedError("Subclasses must implement this method")
209
241
 
242
+ def get_position(self, user_id: str) -> Position | None:
243
+ raise NotImplementedError("Subclasses must implement this method")
244
+
210
245
  @classmethod
211
246
  def get_positions(
212
247
  cls, user_id: str, liquid_only: bool = False, larger_than: float = 0
@@ -236,3 +271,12 @@ class AgentMarket(BaseModel):
236
271
  @classmethod
237
272
  def get_user_url(cls, keys: APIKeys) -> str:
238
273
  raise NotImplementedError("Subclasses must implement this method")
274
+
275
+ def has_token_pool(self) -> bool:
276
+ return self.outcome_token_pool is not None
277
+
278
+ def get_pool_tokens(self, outcome: str) -> float:
279
+ if not self.outcome_token_pool:
280
+ raise ValueError("Outcome token pool is not available.")
281
+
282
+ return self.outcome_token_pool[outcome]
@@ -115,3 +115,14 @@ class Position(BaseModel):
115
115
  for outcome, amount in self.amounts.items()
116
116
  )
117
117
  return f"Position for market id {self.market_id}: {amounts_str}"
118
+
119
+
120
+ class TradeType(str, Enum):
121
+ SELL = "sell"
122
+ BUY = "buy"
123
+
124
+
125
+ class Trade(BaseModel):
126
+ trade_type: TradeType
127
+ outcome: bool
128
+ amount: TokenAmount
@@ -19,6 +19,12 @@ class ManifoldPool(BaseModel):
19
19
  NO: float
20
20
  YES: float
21
21
 
22
+ def size_for_outcome(self, outcome: str) -> float:
23
+ if hasattr(self, outcome):
24
+ return float(getattr(self, outcome))
25
+ else:
26
+ should_not_happen(f"Unexpected outcome string, '{outcome}'.")
27
+
22
28
 
23
29
  class ManifoldAnswersMode(str, Enum):
24
30
  ANYONE = "ANYONE"
@@ -72,6 +72,9 @@ class ManifoldAgentMarket(AgentMarket):
72
72
  current_p_yes=model.probability,
73
73
  url=model.url,
74
74
  volume=model.volume,
75
+ outcome_token_pool={
76
+ o: model.pool.size_for_outcome(o) for o in model.outcomes
77
+ },
75
78
  )
76
79
 
77
80
  @staticmethod
@@ -43,6 +43,7 @@ class MetaculusAgentMarket(AgentMarket):
43
43
  volume=None,
44
44
  have_predicted=model.my_predictions is not None
45
45
  and len(model.my_predictions.predictions) > 0,
46
+ outcome_token_pool=None,
46
47
  )
47
48
 
48
49
  @staticmethod
@@ -44,6 +44,10 @@ def get_boolean_outcome(outcome_str: str) -> bool:
44
44
  raise ValueError(f"Outcome `{outcome_str}` is not a valid boolean outcome.")
45
45
 
46
46
 
47
+ def get_bet_outcome(binary_outcome: bool) -> str:
48
+ return OMEN_TRUE_OUTCOME if binary_outcome else OMEN_FALSE_OUTCOME
49
+
50
+
47
51
  class Condition(BaseModel):
48
52
  id: HexBytes
49
53
  outcomeSlotCount: int
@@ -40,6 +40,8 @@ from prediction_market_agent_tooling.markets.omen.data_models import (
40
40
  OmenBet,
41
41
  OmenMarket,
42
42
  OmenUserPosition,
43
+ get_bet_outcome,
44
+ get_boolean_outcome,
43
45
  )
44
46
  from prediction_market_agent_tooling.markets.omen.omen_contracts import (
45
47
  OMEN_DEFAULT_MARKET_FEE,
@@ -75,6 +77,7 @@ from prediction_market_agent_tooling.tools.web3_utils import (
75
77
  )
76
78
 
77
79
  OMEN_DEFAULT_REALITIO_BOND_VALUE = xdai_type(0.01)
80
+ OMEN_TINY_BET_AMOUNT = xdai_type(0.00001)
78
81
 
79
82
 
80
83
  class OmenAgentMarket(AgentMarket):
@@ -147,7 +150,41 @@ class OmenAgentMarket(AgentMarket):
147
150
 
148
151
  @classmethod
149
152
  def get_tiny_bet_amount(cls) -> BetAmount:
150
- return BetAmount(amount=0.00001, currency=cls.currency)
153
+ return BetAmount(amount=OMEN_TINY_BET_AMOUNT, currency=cls.currency)
154
+
155
+ def liquidate_existing_positions(
156
+ self,
157
+ bet_outcome: bool,
158
+ web3: Web3 | None = None,
159
+ api_keys: APIKeys | None = None,
160
+ larger_than: float | None = None,
161
+ ) -> None:
162
+ """
163
+ Liquidates all previously existing positions.
164
+ Returns the amount in collateral obtained by selling the positions.
165
+ """
166
+ api_keys = api_keys if api_keys is not None else APIKeys()
167
+ better_address = api_keys.bet_from_address
168
+ larger_than = (
169
+ larger_than
170
+ if larger_than is not None
171
+ else self.get_liquidatable_amount().amount
172
+ )
173
+ prev_positions_for_market = self.get_positions(
174
+ user_id=better_address, liquid_only=True, larger_than=larger_than
175
+ )
176
+
177
+ for prev_position in prev_positions_for_market:
178
+ for position_outcome, token_amount in prev_position.amounts.items():
179
+ position_outcome_bool = get_boolean_outcome(position_outcome)
180
+ if position_outcome_bool != bet_outcome:
181
+ # We keep it as collateral since we want to place a bet immediately after this function.
182
+ self.sell_tokens(
183
+ outcome=position_outcome_bool,
184
+ amount=token_amount,
185
+ auto_withdraw=False,
186
+ web3=web3,
187
+ )
151
188
 
152
189
  def place_bet(
153
190
  self,
@@ -310,6 +347,10 @@ class OmenAgentMarket(AgentMarket):
310
347
  volume=wei_to_xdai(model.collateralVolume),
311
348
  close_time=model.close_time,
312
349
  fee=float(wei_to_xdai(model.fee)) if model.fee is not None else 0.0,
350
+ outcome_token_pool={
351
+ model.outcomes[i]: wei_to_xdai(Wei(model.outcomeTokenAmounts[i]))
352
+ for i in range(len(model.outcomes))
353
+ },
313
354
  )
314
355
 
315
356
  @staticmethod
@@ -381,6 +422,11 @@ class OmenAgentMarket(AgentMarket):
381
422
  cls.get_outcome_str(cls.index_set_to_outcome_index(index_set))
382
423
  )
383
424
 
425
+ def get_outcome_str_from_bool(self, outcome: bool) -> OutcomeStr:
426
+ return (
427
+ OutcomeStr(OMEN_TRUE_OUTCOME) if outcome else OutcomeStr(OMEN_FALSE_OUTCOME)
428
+ )
429
+
384
430
  def get_token_balance(
385
431
  self, user_id: str, outcome: str, web3: Web3 | None = None
386
432
  ) -> TokenAmount:
@@ -393,6 +439,18 @@ class OmenAgentMarket(AgentMarket):
393
439
  currency=self.currency,
394
440
  )
395
441
 
442
+ def get_position(self, user_id: str) -> Position | None:
443
+ liquidatable_amount = self.get_liquidatable_amount()
444
+ existing_positions = self.get_positions(
445
+ user_id=user_id,
446
+ liquid_only=True,
447
+ larger_than=liquidatable_amount.amount,
448
+ )
449
+ existing_position = next(
450
+ iter([i for i in existing_positions if i.market_id == self.id]), None
451
+ )
452
+ return existing_position
453
+
396
454
  @classmethod
397
455
  def get_positions(
398
456
  cls,
@@ -553,12 +611,15 @@ def omen_buy_outcome_tx(
553
611
  market_contract: OmenFixedProductMarketMakerContract = market.get_contract()
554
612
  collateral_token_contract = market_contract.get_collateral_token_contract()
555
613
 
614
+ # In case of ERC4626, obtained (for example) sDai out of xDai could be lower than the `amount_wei`, so we need to handle it.
615
+ amount_wei_to_buy = collateral_token_contract.get_in_shares(amount_wei, web3)
616
+
556
617
  # Get the index of the outcome we want to buy.
557
618
  outcome_index: int = market.get_outcome_index(outcome)
558
619
 
559
620
  # Calculate the amount of shares we will get for the given investment amount.
560
621
  expected_shares = market_contract.calcBuyAmount(
561
- amount_wei, outcome_index, web3=web3
622
+ amount_wei_to_buy, outcome_index, web3=web3
562
623
  )
563
624
  # Allow 1% slippage.
564
625
  expected_shares = remove_fraction(expected_shares, 0.01)
@@ -566,11 +627,12 @@ def omen_buy_outcome_tx(
566
627
  collateral_token_contract.approve(
567
628
  api_keys=api_keys,
568
629
  for_address=market_contract.address,
569
- amount_wei=amount_wei,
630
+ amount_wei=amount_wei_to_buy,
570
631
  web3=web3,
571
632
  )
572
633
 
573
634
  if auto_deposit:
635
+ # In auto-depositing, we need to deposit the original `amount_wei`, e.g. we can deposit 2 xDai, but receive 1.8 sDai, so for the bet we will use `amount_wei_to_buy`.
574
636
  auto_deposit_collateral_token(
575
637
  collateral_token_contract, amount_wei, api_keys, web3
576
638
  )
@@ -578,7 +640,7 @@ def omen_buy_outcome_tx(
578
640
  # Buy shares using the deposited xDai in the collateral token.
579
641
  market_contract.buy(
580
642
  api_keys=api_keys,
581
- amount_wei=amount_wei,
643
+ amount_wei=amount_wei_to_buy,
582
644
  outcome_index=outcome_index,
583
645
  min_outcome_tokens_to_buy=expected_shares,
584
646
  web3=web3,
@@ -597,7 +659,7 @@ def binary_omen_buy_outcome_tx(
597
659
  api_keys=api_keys,
598
660
  amount=amount,
599
661
  market=market,
600
- outcome=OMEN_TRUE_OUTCOME if binary_outcome else OMEN_FALSE_OUTCOME,
662
+ outcome=get_bet_outcome(binary_outcome),
601
663
  auto_deposit=auto_deposit,
602
664
  web3=web3,
603
665
  )
@@ -686,7 +748,7 @@ def binary_omen_sell_outcome_tx(
686
748
  api_keys=api_keys,
687
749
  amount=amount,
688
750
  market=market,
689
- outcome=OMEN_TRUE_OUTCOME if binary_outcome else OMEN_FALSE_OUTCOME,
751
+ outcome=get_bet_outcome(binary_outcome),
690
752
  auto_withdraw=auto_withdraw,
691
753
  web3=web3,
692
754
  )
@@ -769,6 +831,7 @@ def omen_create_market_tx(
769
831
  web3=web3,
770
832
  )
771
833
 
834
+ # In case of ERC4626, obtained (for example) sDai out of xDai could be lower than the `amount_wei`, so we need to handle it.
772
835
  initial_funds_in_shares = collateral_token_contract.get_in_shares(
773
836
  amount=initial_funds_wei, web3=web3
774
837
  )
@@ -4,13 +4,18 @@ from datetime import datetime
4
4
 
5
5
  import requests
6
6
  import tenacity
7
- from eth_typing import ChecksumAddress
8
7
  from PIL import Image
9
8
  from PIL.Image import Image as ImageType
10
9
  from subgrounds import FieldPath, Subgrounds
11
10
 
12
11
  from prediction_market_agent_tooling.config import APIKeys
13
- from prediction_market_agent_tooling.gtypes import HexAddress, HexBytes, Wei, wei_type
12
+ from prediction_market_agent_tooling.gtypes import (
13
+ ChecksumAddress,
14
+ HexAddress,
15
+ HexBytes,
16
+ Wei,
17
+ wei_type,
18
+ )
14
19
  from prediction_market_agent_tooling.loggers import logger
15
20
  from prediction_market_agent_tooling.markets.agent_market import FilterBy, SortBy
16
21
  from prediction_market_agent_tooling.markets.omen.data_models import (
@@ -36,6 +41,12 @@ from prediction_market_agent_tooling.tools.web3_utils import (
36
41
  byte32_to_ipfscidv0,
37
42
  )
38
43
 
44
+ # TODO: Agents don't know how to convert value between other tokens, we assume 1 unit = 1xDai = $1 (for example if market would be in wETH, betting 1 unit of wETH would be crazy :D)
45
+ SAFE_COLLATERAL_TOKEN_MARKETS = (
46
+ WrappedxDaiContract().address,
47
+ sDaiContract().address,
48
+ )
49
+
39
50
 
40
51
  class OmenSubgraphHandler(metaclass=SingletonMeta):
41
52
  """
@@ -313,6 +324,8 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
313
324
  # Additional filters, these can not be modified by the enums above.
314
325
  created_after: datetime | None = None,
315
326
  excluded_questions: set[str] | None = None, # question titles
327
+ collateral_token_address_in: tuple[ChecksumAddress, ...]
328
+ | None = SAFE_COLLATERAL_TOKEN_MARKETS,
316
329
  ) -> t.List[OmenMarket]:
317
330
  """
318
331
  Simplified `get_omen_binary_markets` method, which allows to fetch markets based on the filter_by and sort_by values.
@@ -349,6 +362,7 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
349
362
  sort_by_field=sort_by_field,
350
363
  created_after=created_after,
351
364
  excluded_questions=excluded_questions,
365
+ collateral_token_address_in=collateral_token_address_in,
352
366
  )
353
367
 
354
368
  def get_omen_binary_markets(
@@ -370,12 +384,8 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
370
384
  sort_by_field: FieldPath | None = None,
371
385
  sort_direction: str | None = None,
372
386
  outcomes: list[str] = [OMEN_TRUE_OUTCOME, OMEN_FALSE_OUTCOME],
373
- # TODO: Agents don't know how to convert value between other tokens, we assume 1 unit = 1xDai = $1 (for example if market would be in wETH, betting 1 unit of wETH would be crazy :D)
374
387
  collateral_token_address_in: tuple[ChecksumAddress, ...]
375
- | None = (
376
- WrappedxDaiContract().address,
377
- sDaiContract().address,
378
- ),
388
+ | None = SAFE_COLLATERAL_TOKEN_MARKETS,
379
389
  ) -> t.List[OmenMarket]:
380
390
  """
381
391
  Complete method to fetch Omen binary markets with various filters, use `get_omen_binary_markets_simple` for simplified version that uses FilterBy and SortBy enums.
@@ -756,7 +766,7 @@ class OmenSubgraphHandler(metaclass=SingletonMeta):
756
766
  def get_market_image(self, market_id: HexAddress) -> ImageType | None:
757
767
  image_url = self.get_market_image_url(market_id)
758
768
  return (
759
- Image.open(requests.get(image_url, stream=True).raw)
769
+ Image.open(requests.get(image_url, stream=True).raw) # type: ignore[arg-type]
760
770
  if image_url is not None
761
771
  else None
762
772
  )
@@ -39,6 +39,7 @@ class PolymarketAgentMarket(AgentMarket):
39
39
  close_time=model.end_date_iso,
40
40
  url=model.url,
41
41
  volume=None,
42
+ outcome_token_pool=None,
42
43
  )
43
44
 
44
45
  @classmethod
@@ -351,7 +351,7 @@ class ContractERC4626BaseClass(ContractERC20BaseClass):
351
351
  web3 = web3 or self.get_web3()
352
352
  contract = init_collateral_token_contract(self.asset(), web3=web3)
353
353
  assert not isinstance(
354
- contract, ContractERC4626OnGnosisChain
354
+ contract, ContractERC4626BaseClass
355
355
  ), "Asset token should be either Depositable Wrapper ERC-20 or ERC-20." # Shrinking down possible types.
356
356
  return contract
357
357
 
@@ -417,6 +417,11 @@ class ContractERC4626OnGnosisChain(
417
417
  ERC-4626 standard base class with Gnosis Chain configuration.
418
418
  """
419
419
 
420
+ def get_asset_token_contract(
421
+ self, web3: Web3 | None = None
422
+ ) -> ContractERC20OnGnosisChain | ContractDepositableWrapperERC20OnGnosisChain:
423
+ return to_gnosis_chain_contract(super().get_asset_token_contract(web3=web3))
424
+
420
425
 
421
426
  def contract_implements_function(
422
427
  contract_address: ChecksumAddress,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: prediction-market-agent-tooling
3
- Version: 0.48.11
3
+ Version: 0.48.12
4
4
  Summary: Tools to benchmark, deploy and monitor prediction market agents.
5
5
  Author: Gnosis
6
6
  Requires-Python: >=3.10,<3.12
@@ -15,37 +15,37 @@ prediction_market_agent_tooling/benchmark/agents.py,sha256=BwE3U11tQq0rfOJBn-Xn5
15
15
  prediction_market_agent_tooling/benchmark/benchmark.py,sha256=xiHKzZx5GHSsDerFHMZ9j_LXAXnSaITSvv67iPe3MEU,21095
16
16
  prediction_market_agent_tooling/benchmark/utils.py,sha256=D0MfUkVZllmvcU0VOurk9tcKT7JTtwwOp-63zuCBVuc,2880
17
17
  prediction_market_agent_tooling/config.py,sha256=9h68Nb9O1YZabZqtOBrH1S-4U5aIdLKfVYLSKspfUeA,6008
18
- prediction_market_agent_tooling/deploy/agent.py,sha256=KsAFknAbMPVSqBSSahuZuu2cX08azZ8EZ481XwWgR_8,18746
18
+ prediction_market_agent_tooling/deploy/agent.py,sha256=vLAfzWXzgIdNft07gqHsGDOWQHb8jTM9jqKECAi44j0,18700
19
19
  prediction_market_agent_tooling/deploy/agent_example.py,sha256=dIIdZashExWk9tOdyDjw87AuUcGyM7jYxNChYrVK2dM,1001
20
- prediction_market_agent_tooling/deploy/betting_strategy.py,sha256=nJizN4d_jsw5WA8uukaKModVezAgVwoLrCOXsaBOwXo,2223
20
+ prediction_market_agent_tooling/deploy/betting_strategy.py,sha256=yVHzJtxSoRfzSycWbXPYKVQX-51DwUdezDaYy-JZFUE,6905
21
21
  prediction_market_agent_tooling/deploy/constants.py,sha256=M5ty8URipYMGe_G-RzxRydK3AFL6CyvmqCraJUrLBnE,82
22
22
  prediction_market_agent_tooling/deploy/gcp/deploy.py,sha256=CYUgnfy-9XVk04kkxA_5yp0GE9Mw5caYqlFUZQ2j3ks,3739
23
23
  prediction_market_agent_tooling/deploy/gcp/kubernetes_models.py,sha256=qYIHRxQLac3yxtZ8ChikiPG9O1aUQucHW0muTSm1nto,2627
24
24
  prediction_market_agent_tooling/deploy/gcp/utils.py,sha256=oyW0jgrUT2Tr49c7GlpcMsYNQjoCSOcWis3q-MmVAhU,6089
25
25
  prediction_market_agent_tooling/gtypes.py,sha256=ezM2iAycTRJ0uHKK03s0z76a8YFSF438kjOwT_BAqz4,2474
26
26
  prediction_market_agent_tooling/loggers.py,sha256=JiBTgvb34O9dKHYKZyQ0UzojPUy6KSFQSTfbBIXopSY,3721
27
- prediction_market_agent_tooling/markets/agent_market.py,sha256=BELq6x3F4xLZwi0XmoN84RA7ttRQIclyHL2CY6a4Ixc,8409
27
+ prediction_market_agent_tooling/markets/agent_market.py,sha256=ygIAXDsBHaLxiGgAfAZrigAYcpPfxA9Pg3kvykXj77M,10164
28
28
  prediction_market_agent_tooling/markets/categorize.py,sha256=jsoHWvZk9pU6n17oWSCcCxNNYVwlb_NXsZxKRI7vmsk,1301
29
- prediction_market_agent_tooling/markets/data_models.py,sha256=muGWJ8Y3UeDhpwAAGovYGfMMdbbVQcJlS5OruFmaKD4,3108
29
+ prediction_market_agent_tooling/markets/data_models.py,sha256=vaJ049j2Anf5sZWjNflsCEag-Y0G1mTu6bCWT74HxqM,3266
30
30
  prediction_market_agent_tooling/markets/manifold/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  prediction_market_agent_tooling/markets/manifold/api.py,sha256=AC2zmkzpBU3P4kyybs7CgPbDg4hLAx3GY5mjgDi7qDo,7221
32
- prediction_market_agent_tooling/markets/manifold/data_models.py,sha256=jHqOzOiN21wYvDNyh4VtbGtj4adWr6vA4liOQmh24cc,6239
33
- prediction_market_agent_tooling/markets/manifold/manifold.py,sha256=7vLi3KT-Qqq523gb4AZxgbvuB0deSqS5KQYiLwCiMl4,4194
32
+ prediction_market_agent_tooling/markets/manifold/data_models.py,sha256=2Rh3j9SM2om-q61Mbxm_UjOzXfxnoej-f_Vyx0LMMfE,6467
33
+ prediction_market_agent_tooling/markets/manifold/manifold.py,sha256=t_FyV7jBMSIV_MA_EWih3SpvChxpmBey5g8HYQrYde0,4316
34
34
  prediction_market_agent_tooling/markets/manifold/utils.py,sha256=cPPFWXm3vCYH1jy7_ctJZuQH9ZDaPL4_AgAYzGWkoow,513
35
35
  prediction_market_agent_tooling/markets/markets.py,sha256=EN0MgOBBk2ASSVTfqlk0MrsHxg3CVRyQl23jtzfeEi8,3222
36
36
  prediction_market_agent_tooling/markets/metaculus/api.py,sha256=gvPQVAM5NlCyWzEMt4WML9saRBsK9eiHAZP6jwirVqc,2750
37
37
  prediction_market_agent_tooling/markets/metaculus/data_models.py,sha256=6TBy17xntdLBR61QCE5wddwTa_k2D0D8ZgK6p7sGUuc,2448
38
- prediction_market_agent_tooling/markets/metaculus/metaculus.py,sha256=uNF7LP4evvubk818g2zbX1VlnFxeUQOkNgx_e_LwaJA,3416
38
+ prediction_market_agent_tooling/markets/metaculus/metaculus.py,sha256=1aaainHlMExDSp6nfKY31iATQsaJx1LdYp9p2PkQVAs,3453
39
39
  prediction_market_agent_tooling/markets/omen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
- prediction_market_agent_tooling/markets/omen/data_models.py,sha256=7KXT8a_WYyXMFR8OtcPlaazeigc8eE_AvBkx2q-CNi8,16764
41
- prediction_market_agent_tooling/markets/omen/omen.py,sha256=0odOGRtuDjB12sXnRglUyW8IdOsuha7X6ALc2OryF3c,40156
40
+ prediction_market_agent_tooling/markets/omen/data_models.py,sha256=vCDr8FX3FA5LIQydormq8q1aU4GxMh2tQ8i59v_X134,16887
41
+ prediction_market_agent_tooling/markets/omen/omen.py,sha256=zPfOFDeI2JakytpkDAmcxiBUmijPBve2lRbUh0dJDcs,42960
42
42
  prediction_market_agent_tooling/markets/omen/omen_contracts.py,sha256=MfaWfDDfEzHYVAbeT3Dgtl8KG7XsqEpdY3m3-rsOPwo,23588
43
43
  prediction_market_agent_tooling/markets/omen/omen_resolving.py,sha256=Awaw1r32IBAClCktrXbYJ24RNyLDWcLb8RqAx6FGSkI,9529
44
- prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py,sha256=Zxxu3HKg8e-uUNywJSaXUjkzHHrZxq3WC0x6pILAavA,29742
44
+ prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py,sha256=EwknHxU_EkK8H5-6fWW5IN5k6OAW5il4T2FhXrSKEJc,29984
45
45
  prediction_market_agent_tooling/markets/polymarket/api.py,sha256=HXmA1akA0qDj0m3e-GEvWG8x75pm6BX4H7YJPQcST7I,4767
46
46
  prediction_market_agent_tooling/markets/polymarket/data_models.py,sha256=9CJzakyEcsn6DQBK2nOXjOMzTZBLAmK_KqevXvW17DI,4292
47
47
  prediction_market_agent_tooling/markets/polymarket/data_models_web.py,sha256=IPsFT3FX9Ge5l5zR1nBd2w-sd5ue7oR8PJSW710vFWY,12479
48
- prediction_market_agent_tooling/markets/polymarket/polymarket.py,sha256=f7r79fAhLzwS22urfuhVW1Si2m2pZrr5r45WNt-Q3VU,2737
48
+ prediction_market_agent_tooling/markets/polymarket/polymarket.py,sha256=wQ_jzwuPNB-u2eB7okw2I6qbvNZbUT0qH4pAIKXHBkI,2774
49
49
  prediction_market_agent_tooling/markets/polymarket/utils.py,sha256=m4JG6WULh5epCJt4XBMHg0ae5NoVhqlOvAl0A7DR9iM,2023
50
50
  prediction_market_agent_tooling/monitor/markets/manifold.py,sha256=GdYpgRX1GahDi-75Mr53jgtEg6nWcs_rHDUkg4o_7dQ,3352
51
51
  prediction_market_agent_tooling/monitor/markets/metaculus.py,sha256=S8zeDVN2aA6yvQykQNPb8GUGohczfJuCYt9Ds9ESCzs,1473
@@ -61,7 +61,7 @@ prediction_market_agent_tooling/tools/betting_strategies/market_moving.py,sha256
61
61
  prediction_market_agent_tooling/tools/betting_strategies/minimum_bet_to_win.py,sha256=-FUSuQQgjcWSSnoFxnlAyTeilY6raJABJVM2QKkFqAY,438
62
62
  prediction_market_agent_tooling/tools/betting_strategies/stretch_bet_between.py,sha256=THMXwFlskvzbjnX_OiYtDSzI8XVFyULWfP2525_9UGc,429
63
63
  prediction_market_agent_tooling/tools/cache.py,sha256=tGHHd9HCiE_hCCtPtloHZQdDfBuiow9YsqJNYi2Tx_0,499
64
- prediction_market_agent_tooling/tools/contract.py,sha256=d6eR1CSSKRxyM-zgUtp5jmj3z8nAfwTdtiTGWjzlvYU,19388
64
+ prediction_market_agent_tooling/tools/contract.py,sha256=0XcT94HSiFMjca3f0WtoxzCGeCcj3Gcw-o0NGi6Fb3g,19627
65
65
  prediction_market_agent_tooling/tools/costs.py,sha256=EaAJ7v9laD4VEV3d8B44M4u3_oEO_H16jRVCdoZ93Uw,954
66
66
  prediction_market_agent_tooling/tools/gnosis_rpc.py,sha256=ctBfB1os-MvZ1tm0Rwdyn9b3dvFnlM9naKvZmzywc3A,197
67
67
  prediction_market_agent_tooling/tools/google.py,sha256=SfVDxb3oEOUK8mpd0l3mTX9ybrdrTPNM6HjfJ7kfNjA,1794
@@ -78,8 +78,8 @@ prediction_market_agent_tooling/tools/tavily_storage/tavily_models.py,sha256=Uq2
78
78
  prediction_market_agent_tooling/tools/tavily_storage/tavily_storage.py,sha256=xrtQH9v5pXycBRyc5j45pWqkSffkoc9efNIU1_G633Q,3706
79
79
  prediction_market_agent_tooling/tools/utils.py,sha256=JE9YWtPPhnTgLiOyGAZDNG5K8nCwUY9IZEuAlm9UcxA,6611
80
80
  prediction_market_agent_tooling/tools/web3_utils.py,sha256=IZDxHhUJH5RsaRkK9DW6z1RYdk2cz5RqLMZG3T6Gv1U,11602
81
- prediction_market_agent_tooling-0.48.11.dist-info/LICENSE,sha256=6or154nLLU6bELzjh0mCreFjt0m2v72zLi3yHE0QbeE,7650
82
- prediction_market_agent_tooling-0.48.11.dist-info/METADATA,sha256=n7UvoWRzs1H7AAlYVSaVQ5zYXaEwUbkrO-j5Uiaev80,7811
83
- prediction_market_agent_tooling-0.48.11.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
84
- prediction_market_agent_tooling-0.48.11.dist-info/entry_points.txt,sha256=m8PukHbeH5g0IAAmOf_1Ahm-sGAMdhSSRQmwtpmi2s8,81
85
- prediction_market_agent_tooling-0.48.11.dist-info/RECORD,,
81
+ prediction_market_agent_tooling-0.48.12.dist-info/LICENSE,sha256=6or154nLLU6bELzjh0mCreFjt0m2v72zLi3yHE0QbeE,7650
82
+ prediction_market_agent_tooling-0.48.12.dist-info/METADATA,sha256=1Ana_qFpbYW-L7MrmyCxeOY33rRyhpgLzXSihHqXiU4,7811
83
+ prediction_market_agent_tooling-0.48.12.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
84
+ prediction_market_agent_tooling-0.48.12.dist-info/entry_points.txt,sha256=m8PukHbeH5g0IAAmOf_1Ahm-sGAMdhSSRQmwtpmi2s8,81
85
+ prediction_market_agent_tooling-0.48.12.dist-info/RECORD,,