prediction-market-agent-tooling 0.65.5__py3-none-any.whl → 0.69.17.dev1149__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. prediction_market_agent_tooling/abis/agentresultmapping.abi.json +192 -0
  2. prediction_market_agent_tooling/abis/erc1155.abi.json +352 -0
  3. prediction_market_agent_tooling/abis/processor.abi.json +16 -0
  4. prediction_market_agent_tooling/abis/swapr_quoter.abi.json +221 -0
  5. prediction_market_agent_tooling/abis/swapr_router.abi.json +634 -0
  6. prediction_market_agent_tooling/benchmark/benchmark.py +1 -1
  7. prediction_market_agent_tooling/benchmark/utils.py +13 -0
  8. prediction_market_agent_tooling/chains.py +1 -0
  9. prediction_market_agent_tooling/config.py +61 -2
  10. prediction_market_agent_tooling/data_download/langfuse_data_downloader.py +405 -0
  11. prediction_market_agent_tooling/deploy/agent.py +199 -67
  12. prediction_market_agent_tooling/deploy/agent_example.py +1 -1
  13. prediction_market_agent_tooling/deploy/betting_strategy.py +412 -68
  14. prediction_market_agent_tooling/deploy/constants.py +6 -0
  15. prediction_market_agent_tooling/gtypes.py +11 -1
  16. prediction_market_agent_tooling/jobs/jobs_models.py +2 -2
  17. prediction_market_agent_tooling/jobs/omen/omen_jobs.py +19 -20
  18. prediction_market_agent_tooling/loggers.py +9 -1
  19. prediction_market_agent_tooling/logprobs_parser.py +2 -1
  20. prediction_market_agent_tooling/markets/agent_market.py +106 -18
  21. prediction_market_agent_tooling/markets/blockchain_utils.py +37 -19
  22. prediction_market_agent_tooling/markets/data_models.py +120 -7
  23. prediction_market_agent_tooling/markets/manifold/data_models.py +5 -3
  24. prediction_market_agent_tooling/markets/manifold/manifold.py +21 -2
  25. prediction_market_agent_tooling/markets/manifold/utils.py +8 -2
  26. prediction_market_agent_tooling/markets/market_type.py +74 -0
  27. prediction_market_agent_tooling/markets/markets.py +7 -99
  28. prediction_market_agent_tooling/markets/metaculus/data_models.py +3 -3
  29. prediction_market_agent_tooling/markets/metaculus/metaculus.py +5 -8
  30. prediction_market_agent_tooling/markets/omen/cow_contracts.py +5 -1
  31. prediction_market_agent_tooling/markets/omen/data_models.py +63 -32
  32. prediction_market_agent_tooling/markets/omen/omen.py +112 -23
  33. prediction_market_agent_tooling/markets/omen/omen_constants.py +8 -0
  34. prediction_market_agent_tooling/markets/omen/omen_contracts.py +18 -203
  35. prediction_market_agent_tooling/markets/omen/omen_resolving.py +33 -13
  36. prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +23 -18
  37. prediction_market_agent_tooling/markets/polymarket/api.py +123 -100
  38. prediction_market_agent_tooling/markets/polymarket/clob_manager.py +156 -0
  39. prediction_market_agent_tooling/markets/polymarket/constants.py +15 -0
  40. prediction_market_agent_tooling/markets/polymarket/data_models.py +95 -19
  41. prediction_market_agent_tooling/markets/polymarket/polymarket.py +373 -29
  42. prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py +35 -0
  43. prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +91 -0
  44. prediction_market_agent_tooling/markets/polymarket/utils.py +1 -22
  45. prediction_market_agent_tooling/markets/seer/data_models.py +111 -17
  46. prediction_market_agent_tooling/markets/seer/exceptions.py +2 -0
  47. prediction_market_agent_tooling/markets/seer/price_manager.py +165 -50
  48. prediction_market_agent_tooling/markets/seer/seer.py +393 -106
  49. prediction_market_agent_tooling/markets/seer/seer_api.py +28 -0
  50. prediction_market_agent_tooling/markets/seer/seer_contracts.py +115 -5
  51. prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +297 -66
  52. prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +43 -8
  53. prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +80 -0
  54. prediction_market_agent_tooling/tools/_generic_value.py +8 -2
  55. prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +271 -8
  56. prediction_market_agent_tooling/tools/betting_strategies/utils.py +6 -1
  57. prediction_market_agent_tooling/tools/caches/db_cache.py +219 -117
  58. prediction_market_agent_tooling/tools/caches/serializers.py +11 -2
  59. prediction_market_agent_tooling/tools/contract.py +480 -38
  60. prediction_market_agent_tooling/tools/contract_utils.py +61 -0
  61. prediction_market_agent_tooling/tools/cow/cow_order.py +218 -45
  62. prediction_market_agent_tooling/tools/cow/models.py +122 -0
  63. prediction_market_agent_tooling/tools/cow/semaphore.py +104 -0
  64. prediction_market_agent_tooling/tools/datetime_utc.py +14 -2
  65. prediction_market_agent_tooling/tools/db/db_manager.py +59 -0
  66. prediction_market_agent_tooling/tools/hexbytes_custom.py +4 -1
  67. prediction_market_agent_tooling/tools/httpx_cached_client.py +15 -6
  68. prediction_market_agent_tooling/tools/langfuse_client_utils.py +21 -8
  69. prediction_market_agent_tooling/tools/openai_utils.py +31 -0
  70. prediction_market_agent_tooling/tools/perplexity/perplexity_client.py +86 -0
  71. prediction_market_agent_tooling/tools/perplexity/perplexity_models.py +26 -0
  72. prediction_market_agent_tooling/tools/perplexity/perplexity_search.py +73 -0
  73. prediction_market_agent_tooling/tools/rephrase.py +71 -0
  74. prediction_market_agent_tooling/tools/singleton.py +11 -6
  75. prediction_market_agent_tooling/tools/streamlit_utils.py +188 -0
  76. prediction_market_agent_tooling/tools/tokens/auto_deposit.py +64 -0
  77. prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +8 -0
  78. prediction_market_agent_tooling/tools/tokens/slippage.py +21 -0
  79. prediction_market_agent_tooling/tools/tokens/usd.py +5 -2
  80. prediction_market_agent_tooling/tools/utils.py +61 -3
  81. prediction_market_agent_tooling/tools/web3_utils.py +63 -9
  82. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/METADATA +13 -9
  83. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/RECORD +86 -64
  84. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/WHEEL +1 -1
  85. prediction_market_agent_tooling/abis/omen_agentresultmapping.abi.json +0 -171
  86. prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -420
  87. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/entry_points.txt +0 -0
  88. {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info/licenses}/LICENSE +0 -0
@@ -1,7 +1,10 @@
1
+ import asyncio
1
2
  import hashlib
3
+ import threading
2
4
  from contextlib import contextmanager
3
5
  from typing import Generator, Sequence
4
6
 
7
+ from pydantic import SecretStr
5
8
  from sqlalchemy import Connection
6
9
  from sqlmodel import Session, SQLModel, create_engine
7
10
 
@@ -79,3 +82,59 @@ class DBManager:
79
82
  if tables_to_create:
80
83
  for table in tables_to_create:
81
84
  self.cache_table_initialized[table.name] = True
85
+
86
+
87
+ class EnsureTableManager:
88
+ """
89
+ Manages database table initialization with thread-safe and async-safe locking.
90
+ Ensures tables are created only once per database URL.
91
+ """
92
+
93
+ def __init__(self, tables: Sequence[type[SQLModel]]) -> None:
94
+ """
95
+ Initialize the table manager with the tables to manage.
96
+
97
+ Args:
98
+ tables: Sequence of SQLModel table classes to ensure in the database.
99
+ """
100
+ # Ensure tables only once, as it's a time costly operation.
101
+ self._tables = tables
102
+ self._lock_thread = threading.Lock()
103
+ self._lock_async = asyncio.Lock()
104
+ self._ensured: dict[SecretStr, bool] = {}
105
+
106
+ def is_ensured(self, db_url: SecretStr) -> bool:
107
+ """Check if tables have been ensured for the given database URL."""
108
+ return self._ensured.get(db_url, False)
109
+
110
+ def mark_ensured(self, db_url: SecretStr) -> None:
111
+ """Mark tables as ensured for the given database URL."""
112
+ self._ensured[db_url] = True
113
+
114
+ def ensure_tables_sync(self, api_keys: APIKeys) -> None:
115
+ """
116
+ Ensure tables exist for the given API keys (synchronous version).
117
+ Thread-safe with double-checked locking pattern.
118
+ """
119
+ if not self.is_ensured(api_keys.sqlalchemy_db_url):
120
+ with self._lock_thread:
121
+ if not self.is_ensured(api_keys.sqlalchemy_db_url):
122
+ self._create_tables(api_keys)
123
+ self.mark_ensured(api_keys.sqlalchemy_db_url)
124
+
125
+ async def ensure_tables_async(self, api_keys: APIKeys) -> None:
126
+ """
127
+ Ensure tables exist for the given API keys (asynchronous version).
128
+ Async-safe with double-checked locking pattern.
129
+ """
130
+ if not self.is_ensured(api_keys.sqlalchemy_db_url):
131
+ async with self._lock_async:
132
+ if not self.is_ensured(api_keys.sqlalchemy_db_url):
133
+ await asyncio.to_thread(self._create_tables, api_keys)
134
+ self.mark_ensured(api_keys.sqlalchemy_db_url)
135
+
136
+ def _create_tables(self, api_keys: APIKeys) -> None:
137
+ """Create the database tables."""
138
+ DBManager(api_keys.sqlalchemy_db_url.get_secret_value()).create_tables(
139
+ list(self._tables)
140
+ )
@@ -11,7 +11,7 @@ from pydantic_core.core_schema import (
11
11
  with_info_before_validator_function,
12
12
  )
13
13
 
14
- hex_serializer = plain_serializer_function_ser_schema(function=lambda x: x.hex())
14
+ hex_serializer = plain_serializer_function_ser_schema(function=lambda x: x.to_0x_hex())
15
15
 
16
16
 
17
17
  class BaseHex:
@@ -60,6 +60,9 @@ class HexBytes(HexBytesBase, BaseHex):
60
60
  value = hex_str[2:] if hex_str.startswith("0x") else hex_str
61
61
  return super().fromhex(value)
62
62
 
63
+ def __repr__(self) -> str:
64
+ return f'HexBytes("{self.to_0x_hex()}")'
65
+
63
66
  @classmethod
64
67
  def __eth_pydantic_validate__(
65
68
  cls, value: t.Any, info: ValidationInfo | None = None
@@ -1,14 +1,23 @@
1
+ from datetime import timedelta
2
+
1
3
  import hishel
4
+ import httpx
5
+
6
+ from prediction_market_agent_tooling.tools.singleton import SingletonMeta
2
7
 
8
+ ONE_DAY = timedelta(days=1)
3
9
 
4
- class HttpxCachedClient:
5
- def __init__(self) -> None:
10
+
11
+ class HttpxCachedClient(metaclass=SingletonMeta):
12
+ def __init__(self, ttl: timedelta = ONE_DAY) -> None:
6
13
  storage = hishel.FileStorage(
7
- ttl=24 * 60 * 60,
8
- check_ttl_every=1 * 60 * 60,
14
+ ttl=ttl.total_seconds(),
15
+ check_ttl_every=60,
9
16
  )
10
17
  controller = hishel.Controller(force_cache=True)
11
- self.client = hishel.CacheClient(storage=storage, controller=controller)
18
+ self.client: httpx.Client = hishel.CacheClient(
19
+ storage=storage, controller=controller
20
+ )
12
21
 
13
- def get_client(self) -> hishel.CacheClient:
22
+ def get_client(self) -> httpx.Client:
14
23
  return self.client
@@ -5,7 +5,9 @@ from langfuse import Langfuse
5
5
  from langfuse.client import TraceWithDetails
6
6
  from pydantic import BaseModel
7
7
 
8
+ from prediction_market_agent_tooling.deploy.agent import MarketType
8
9
  from prediction_market_agent_tooling.loggers import logger
10
+ from prediction_market_agent_tooling.markets.agent_market import AgentMarket
9
11
  from prediction_market_agent_tooling.markets.data_models import (
10
12
  CategoricalProbabilisticAnswer,
11
13
  PlacedTrade,
@@ -16,12 +18,13 @@ from prediction_market_agent_tooling.markets.omen.omen import OmenAgentMarket
16
18
  from prediction_market_agent_tooling.markets.omen.omen_constants import (
17
19
  WRAPPED_XDAI_CONTRACT_ADDRESS,
18
20
  )
21
+ from prediction_market_agent_tooling.markets.seer.seer import SeerAgentMarket
19
22
  from prediction_market_agent_tooling.tools.utils import DatetimeUTC
20
23
 
21
24
 
22
25
  class ProcessMarketTrace(BaseModel):
23
26
  timestamp: int
24
- market: OmenAgentMarket
27
+ market: SeerAgentMarket | OmenAgentMarket
25
28
  answer: CategoricalProbabilisticAnswer
26
29
  trades: list[PlacedTrade]
27
30
 
@@ -40,13 +43,18 @@ class ProcessMarketTrace(BaseModel):
40
43
  def from_langfuse_trace(
41
44
  trace: TraceWithDetails,
42
45
  ) -> t.Optional["ProcessMarketTrace"]:
43
- market = trace_to_omen_agent_market(trace)
46
+ market = trace_to_agent_market(trace)
44
47
  answer = trace_to_answer(trace)
45
48
  trades = trace_to_trades(trace)
46
49
 
47
50
  if not market or not answer or not trades:
48
51
  return None
49
52
 
53
+ if not isinstance(market, (SeerAgentMarket, OmenAgentMarket)):
54
+ raise ValueError(
55
+ f"Market type {type(market)} is not supported for ProcessMarketTrace"
56
+ )
57
+
50
58
  return ProcessMarketTrace(
51
59
  market=market,
52
60
  answer=answer,
@@ -68,6 +76,7 @@ def get_traces_for_agent(
68
76
  client: Langfuse,
69
77
  to_timestamp: DatetimeUTC | None = None,
70
78
  tags: str | list[str] | None = None,
79
+ limit: int | None = None,
71
80
  ) -> list[TraceWithDetails]:
72
81
  """
73
82
  Fetch agent traces using pagination
@@ -98,20 +107,27 @@ def get_traces_for_agent(
98
107
  if has_output:
99
108
  agent_traces = [t for t in agent_traces if t.output is not None]
100
109
  all_agent_traces.extend(agent_traces)
110
+ if limit is not None and len(all_agent_traces) >= limit:
111
+ all_agent_traces = all_agent_traces[:limit]
112
+ break
101
113
  return all_agent_traces
102
114
 
103
115
 
104
- def trace_to_omen_agent_market(trace: TraceWithDetails) -> OmenAgentMarket | None:
116
+ def trace_to_agent_market(trace: TraceWithDetails) -> AgentMarket | None:
105
117
  if not trace.input:
106
118
  logger.warning(f"No input in the trace: {trace}")
107
119
  return None
108
120
  if not trace.input["args"]:
109
121
  logger.warning(f"No args in the trace: {trace}")
110
122
  return None
111
- assert len(trace.input["args"]) == 2 and trace.input["args"][0] == "omen"
123
+ assert len(trace.input["args"]) == 2
124
+
125
+ market_type = MarketType(trace.input["args"][0])
126
+ market_class = market_type.market_class
127
+
112
128
  try:
113
129
  # If the market model is invalid (e.g. outdated), it will raise an exception
114
- market = OmenAgentMarket.model_validate(trace.input["args"][1])
130
+ market = market_class.model_validate(trace.input["args"][1])
115
131
  return market
116
132
  except Exception as e:
117
133
  logger.warning(f"Market not parsed from langfuse because: {e}")
@@ -155,9 +171,6 @@ def get_trace_for_bet(
155
171
  not in WRAPPED_XDAI_CONTRACT_ADDRESS
156
172
  ):
157
173
  # TODO: We need to compute bet amount token in USD here, but at the time of bet placement!
158
- logger.warning(
159
- "This currently works only for WXDAI markets, because we need to compare against USD value."
160
- )
161
174
  continue
162
175
  # Cannot use exact comparison due to gas fees
163
176
  if (
@@ -0,0 +1,31 @@
1
+ from langfuse.openai import AsyncOpenAI
2
+ from openai import DEFAULT_TIMEOUT, DefaultAsyncHttpxClient
3
+ from pydantic import SecretStr
4
+ from pydantic_ai.models.openai import OpenAIModel # noqa: F401 # Just for convenience.
5
+ from pydantic_ai.providers.openai import OpenAIProvider
6
+
7
+ OPENAI_BASE_URL = "https://api.openai.com/v1"
8
+
9
+
10
+ def get_openai_provider(
11
+ api_key: SecretStr,
12
+ base_url: str = OPENAI_BASE_URL,
13
+ ) -> OpenAIProvider:
14
+ """
15
+ For some reason, when OpenAIProvider/AsyncOpenAI is initialised without the http_client directly provided, and it's used with Langfuse observer decorator,
16
+ we are getting false error messages.
17
+
18
+ Unfortunatelly, Langfuse doesn't seem eager to fix this, so this is a workaround. See https://github.com/langfuse/langfuse/issues/5622.
19
+
20
+ Use this function as a helper function to create bug-free OpenAIProvider.
21
+ """
22
+ return OpenAIProvider(
23
+ openai_client=AsyncOpenAI(
24
+ api_key=api_key.get_secret_value(),
25
+ base_url=base_url,
26
+ http_client=DefaultAsyncHttpxClient(
27
+ timeout=DEFAULT_TIMEOUT,
28
+ base_url=base_url,
29
+ ),
30
+ )
31
+ )
@@ -0,0 +1,86 @@
1
+ from typing import Any, Dict, List, Optional
2
+
3
+ import httpx
4
+ from pydantic import SecretStr
5
+
6
+ from prediction_market_agent_tooling.tools.perplexity.perplexity_models import (
7
+ PerplexityModelSettings,
8
+ PerplexityRequestParameters,
9
+ PerplexityResponse,
10
+ )
11
+
12
+
13
+ class PerplexityModel:
14
+ def __init__(
15
+ self,
16
+ model_name: str,
17
+ *,
18
+ api_key: SecretStr,
19
+ completition_endpoint: str = "https://api.perplexity.ai/chat/completions",
20
+ ) -> None:
21
+ self.model_name = model_name
22
+ self.api_key = api_key
23
+ self.completition_endpoint = completition_endpoint
24
+
25
+ async def request(
26
+ self,
27
+ messages: List[dict[str, str]],
28
+ model_settings: Optional[PerplexityModelSettings],
29
+ model_request_parameters: PerplexityRequestParameters,
30
+ ) -> PerplexityResponse:
31
+ payload: Dict[str, Any] = {"model": self.model_name, "messages": messages}
32
+
33
+ if model_settings:
34
+ model_settings_dict = model_settings.model_dump()
35
+ model_settings_dict = {
36
+ k: v for k, v in model_settings_dict.items() if v is not None
37
+ }
38
+ payload.update(model_settings_dict)
39
+
40
+ params_dict = model_request_parameters.model_dump()
41
+ params_dict = {k: v for k, v in params_dict.items() if v is not None}
42
+
43
+ # Extract and handle search_context_size specially
44
+ if "search_context_size" in params_dict:
45
+ search_context_size = params_dict.pop("search_context_size")
46
+ payload["web_search_options"] = {"search_context_size": search_context_size}
47
+
48
+ # Add remaining Perplexity parameters to payload
49
+ payload.update(params_dict)
50
+
51
+ try:
52
+ async with httpx.AsyncClient(timeout=180) as client:
53
+ response = await client.post(
54
+ self.completition_endpoint,
55
+ headers={
56
+ "Authorization": f"Bearer {self.api_key.get_secret_value()}",
57
+ "Content-Type": "application/json",
58
+ },
59
+ json=payload,
60
+ )
61
+ response.raise_for_status()
62
+ result: dict[str, Any] = response.json()
63
+
64
+ choices = result.get("choices", [])
65
+ if not choices:
66
+ raise ValueError("Invalid response: no choices")
67
+
68
+ content = choices[0].get("message", {}).get("content")
69
+ if not content:
70
+ raise ValueError("Invalid response: no content")
71
+
72
+ return PerplexityResponse(
73
+ content=content,
74
+ citations=result.get("citations", []),
75
+ usage=result.get("usage", {}),
76
+ )
77
+ except httpx.HTTPStatusError as e:
78
+ raise ValueError(
79
+ f"HTTP error from Perplexity API: {e.response.status_code} - {e.response.text}"
80
+ ) from e
81
+ except httpx.RequestError as e:
82
+ raise ValueError(f"Request error to Perplexity API: {str(e)}") from e
83
+ except Exception as e:
84
+ raise ValueError(
85
+ f"Unexpected error in Perplexity API request: {str(e)}"
86
+ ) from e
@@ -0,0 +1,26 @@
1
+ from typing import Any, List, Literal, Optional
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class PerplexityRequestParameters(BaseModel):
7
+ search_context_size: Optional[Literal["low", "medium", "high"]]
8
+ search_recency_filter: Optional[Literal["any", "day", "week", "month", "year"]]
9
+ search_return_related_questions: Optional[bool]
10
+ search_domain_filter: Optional[List[str]]
11
+ search_after_date_filter: Optional[str]
12
+ search_before_date_filter: Optional[str]
13
+
14
+
15
+ class PerplexityResponse(BaseModel):
16
+ content: str
17
+ citations: list[str]
18
+ usage: dict[str, Any]
19
+
20
+
21
+ class PerplexityModelSettings(BaseModel):
22
+ max_tokens: Optional[int] = None
23
+ temperature: Optional[float] = None
24
+ top_p: Optional[float] = None
25
+ frequency_penalty: Optional[float] = None
26
+ presence_penalty: Optional[float] = None
@@ -0,0 +1,73 @@
1
+ import asyncio
2
+ import typing as t
3
+ from datetime import date, timedelta
4
+
5
+ import tenacity
6
+
7
+ from prediction_market_agent_tooling.config import APIKeys
8
+ from prediction_market_agent_tooling.tools.caches.db_cache import db_cache
9
+ from prediction_market_agent_tooling.tools.perplexity.perplexity_client import (
10
+ PerplexityModel,
11
+ )
12
+ from prediction_market_agent_tooling.tools.perplexity.perplexity_models import (
13
+ PerplexityModelSettings,
14
+ PerplexityRequestParameters,
15
+ PerplexityResponse,
16
+ )
17
+
18
+ SYSTEM_PROMPT = "You are a helpful search assistant. Your task is to provide accurate information based on web searches."
19
+
20
+
21
+ @tenacity.retry(stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_fixed(1))
22
+ @db_cache(
23
+ max_age=timedelta(days=1),
24
+ ignore_args=["api_keys"],
25
+ log_error_on_unsavable_data=False,
26
+ )
27
+ def perplexity_search(
28
+ query: str,
29
+ api_keys: APIKeys,
30
+ search_context_size: t.Literal["low", "medium", "high"] = "medium",
31
+ search_recency_filter: t.Literal["any", "day", "week", "month", "year"]
32
+ | None = None,
33
+ search_filter_before_date: date | None = None,
34
+ search_filter_after_date: date | None = None,
35
+ search_return_related_questions: bool | None = None,
36
+ include_domains: list[str] | None = None,
37
+ temperature: float = 0,
38
+ model_name: str = "sonar-pro",
39
+ max_tokens: int = 2048,
40
+ ) -> PerplexityResponse:
41
+ # Create messages in ModelMessage format
42
+ messages = [
43
+ {"role": "system", "content": SYSTEM_PROMPT},
44
+ {"role": "user", "content": query},
45
+ ]
46
+
47
+ # Define special parameters for the request and create the settings
48
+ model_settings = PerplexityModelSettings(
49
+ max_tokens=max_tokens, temperature=temperature
50
+ )
51
+
52
+ # Create a basic request parameters object with required base parameters
53
+ request_params = PerplexityRequestParameters(
54
+ search_domain_filter=include_domains,
55
+ search_after_date_filter=search_filter_after_date.strftime("%Y-%m-%d")
56
+ if search_filter_after_date
57
+ else None,
58
+ search_before_date_filter=search_filter_before_date.strftime("%Y-%m-%d")
59
+ if search_filter_before_date
60
+ else None,
61
+ search_recency_filter=search_recency_filter,
62
+ search_context_size=search_context_size,
63
+ search_return_related_questions=search_return_related_questions,
64
+ )
65
+
66
+ model = PerplexityModel(model_name=model_name, api_key=api_keys.perplexity_api_key)
67
+ return asyncio.run(
68
+ model.request(
69
+ messages=messages,
70
+ model_settings=model_settings,
71
+ model_request_parameters=request_params,
72
+ )
73
+ )
@@ -0,0 +1,71 @@
1
+ import tenacity
2
+
3
+ from prediction_market_agent_tooling.config import APIKeys
4
+ from prediction_market_agent_tooling.tools.caches.db_cache import db_cache
5
+ from prediction_market_agent_tooling.tools.langfuse_ import (
6
+ get_langfuse_langchain_config,
7
+ observe,
8
+ )
9
+ from prediction_market_agent_tooling.tools.utils import (
10
+ LLM_SEED,
11
+ LLM_SUPER_LOW_TEMPERATURE,
12
+ )
13
+
14
+ REPHRASE_QUESTION_PROMPT = """Given the following question of main interest: {question}
15
+
16
+ But it's conditioned on `{parent_question}` resolving to `{needed_parent_outcome}`.
17
+
18
+ Rewrite the main question to contain the parent question in the correct form.
19
+
20
+ The main question will be used as a prediction market, so it does need to be rephrased using the parent question properly. Such that the probability of the main question also accounts for the conditioned outcome.
21
+
22
+ For example:
23
+ ```
24
+ Main question: What is the probability of <X> happening before <date>?
25
+ Conditioned on: Will <Y> happen before <another-date>?
26
+ Rephrased: What is the joint probability of Y happening before <another-date> and then X happening before <date>?
27
+ ```
28
+
29
+ Output only the rephrased question.
30
+ """
31
+
32
+
33
+ @tenacity.retry(stop=tenacity.stop_after_attempt(3), wait=tenacity.wait_fixed(1))
34
+ @observe()
35
+ @db_cache
36
+ def rephrase_question_to_unconditional(
37
+ question: str,
38
+ parent_question: str,
39
+ needed_parent_outcome: str,
40
+ engine: str = "gpt-4.1",
41
+ temperature: float = LLM_SUPER_LOW_TEMPERATURE,
42
+ seed: int = LLM_SEED,
43
+ prompt_template: str = REPHRASE_QUESTION_PROMPT,
44
+ max_tokens: int = 1024,
45
+ ) -> str:
46
+ try:
47
+ from langchain.prompts import ChatPromptTemplate
48
+ from langchain_openai import ChatOpenAI
49
+ except ImportError:
50
+ raise ImportError("langchain not installed")
51
+
52
+ llm = ChatOpenAI(
53
+ model_name=engine,
54
+ temperature=temperature,
55
+ seed=seed,
56
+ openai_api_key=APIKeys().openai_api_key,
57
+ )
58
+
59
+ prompt = ChatPromptTemplate.from_template(template=prompt_template)
60
+ messages = prompt.format_messages(
61
+ question=question,
62
+ parent_question=parent_question,
63
+ needed_parent_outcome=needed_parent_outcome,
64
+ )
65
+ completion = str(
66
+ llm.invoke(
67
+ messages, max_tokens=max_tokens, config=get_langfuse_langchain_config()
68
+ ).content
69
+ )
70
+
71
+ return completion
@@ -8,16 +8,21 @@ class SingletonMeta(type, t.Generic[_T]):
8
8
  The Singleton class can be implemented in different ways in Python. Some
9
9
  possible methods include: base class, decorator, metaclass. We will use the
10
10
  metaclass because it is best suited for this purpose.
11
+
12
+ This version creates a unique instance for each unique set of __init__ arguments.
11
13
  """
12
14
 
13
- _instances: dict[t.Any, _T] = {}
15
+ _instances: dict[
16
+ tuple[t.Any, tuple[t.Any, ...], tuple[tuple[str, t.Any], ...]], _T
17
+ ] = {}
14
18
 
15
19
  def __call__(self, *args: t.Any, **kwargs: t.Any) -> _T:
16
20
  """
17
- Possible changes to the value of the `__init__` argument do not affect
18
- the returned instance.
21
+ Different __init__ arguments will result in different instances.
19
22
  """
20
- if self not in self._instances:
23
+ # Create a key based on the class, args, and kwargs (sorted for consistency)
24
+ key = (self, args, tuple(sorted(kwargs.items())))
25
+ if key not in self._instances:
21
26
  instance = super().__call__(*args, **kwargs)
22
- self._instances[self] = instance
23
- return self._instances[self]
27
+ self._instances[key] = instance
28
+ return self._instances[key]