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.
- prediction_market_agent_tooling/abis/agentresultmapping.abi.json +192 -0
- prediction_market_agent_tooling/abis/erc1155.abi.json +352 -0
- prediction_market_agent_tooling/abis/processor.abi.json +16 -0
- prediction_market_agent_tooling/abis/swapr_quoter.abi.json +221 -0
- prediction_market_agent_tooling/abis/swapr_router.abi.json +634 -0
- prediction_market_agent_tooling/benchmark/benchmark.py +1 -1
- prediction_market_agent_tooling/benchmark/utils.py +13 -0
- prediction_market_agent_tooling/chains.py +1 -0
- prediction_market_agent_tooling/config.py +61 -2
- prediction_market_agent_tooling/data_download/langfuse_data_downloader.py +405 -0
- prediction_market_agent_tooling/deploy/agent.py +199 -67
- prediction_market_agent_tooling/deploy/agent_example.py +1 -1
- prediction_market_agent_tooling/deploy/betting_strategy.py +412 -68
- prediction_market_agent_tooling/deploy/constants.py +6 -0
- prediction_market_agent_tooling/gtypes.py +11 -1
- prediction_market_agent_tooling/jobs/jobs_models.py +2 -2
- prediction_market_agent_tooling/jobs/omen/omen_jobs.py +19 -20
- prediction_market_agent_tooling/loggers.py +9 -1
- prediction_market_agent_tooling/logprobs_parser.py +2 -1
- prediction_market_agent_tooling/markets/agent_market.py +106 -18
- prediction_market_agent_tooling/markets/blockchain_utils.py +37 -19
- prediction_market_agent_tooling/markets/data_models.py +120 -7
- prediction_market_agent_tooling/markets/manifold/data_models.py +5 -3
- prediction_market_agent_tooling/markets/manifold/manifold.py +21 -2
- prediction_market_agent_tooling/markets/manifold/utils.py +8 -2
- prediction_market_agent_tooling/markets/market_type.py +74 -0
- prediction_market_agent_tooling/markets/markets.py +7 -99
- prediction_market_agent_tooling/markets/metaculus/data_models.py +3 -3
- prediction_market_agent_tooling/markets/metaculus/metaculus.py +5 -8
- prediction_market_agent_tooling/markets/omen/cow_contracts.py +5 -1
- prediction_market_agent_tooling/markets/omen/data_models.py +63 -32
- prediction_market_agent_tooling/markets/omen/omen.py +112 -23
- prediction_market_agent_tooling/markets/omen/omen_constants.py +8 -0
- prediction_market_agent_tooling/markets/omen/omen_contracts.py +18 -203
- prediction_market_agent_tooling/markets/omen/omen_resolving.py +33 -13
- prediction_market_agent_tooling/markets/omen/omen_subgraph_handler.py +23 -18
- prediction_market_agent_tooling/markets/polymarket/api.py +123 -100
- prediction_market_agent_tooling/markets/polymarket/clob_manager.py +156 -0
- prediction_market_agent_tooling/markets/polymarket/constants.py +15 -0
- prediction_market_agent_tooling/markets/polymarket/data_models.py +95 -19
- prediction_market_agent_tooling/markets/polymarket/polymarket.py +373 -29
- prediction_market_agent_tooling/markets/polymarket/polymarket_contracts.py +35 -0
- prediction_market_agent_tooling/markets/polymarket/polymarket_subgraph_handler.py +91 -0
- prediction_market_agent_tooling/markets/polymarket/utils.py +1 -22
- prediction_market_agent_tooling/markets/seer/data_models.py +111 -17
- prediction_market_agent_tooling/markets/seer/exceptions.py +2 -0
- prediction_market_agent_tooling/markets/seer/price_manager.py +165 -50
- prediction_market_agent_tooling/markets/seer/seer.py +393 -106
- prediction_market_agent_tooling/markets/seer/seer_api.py +28 -0
- prediction_market_agent_tooling/markets/seer/seer_contracts.py +115 -5
- prediction_market_agent_tooling/markets/seer/seer_subgraph_handler.py +297 -66
- prediction_market_agent_tooling/markets/seer/subgraph_data_models.py +43 -8
- prediction_market_agent_tooling/markets/seer/swap_pool_handler.py +80 -0
- prediction_market_agent_tooling/tools/_generic_value.py +8 -2
- prediction_market_agent_tooling/tools/betting_strategies/kelly_criterion.py +271 -8
- prediction_market_agent_tooling/tools/betting_strategies/utils.py +6 -1
- prediction_market_agent_tooling/tools/caches/db_cache.py +219 -117
- prediction_market_agent_tooling/tools/caches/serializers.py +11 -2
- prediction_market_agent_tooling/tools/contract.py +480 -38
- prediction_market_agent_tooling/tools/contract_utils.py +61 -0
- prediction_market_agent_tooling/tools/cow/cow_order.py +218 -45
- prediction_market_agent_tooling/tools/cow/models.py +122 -0
- prediction_market_agent_tooling/tools/cow/semaphore.py +104 -0
- prediction_market_agent_tooling/tools/datetime_utc.py +14 -2
- prediction_market_agent_tooling/tools/db/db_manager.py +59 -0
- prediction_market_agent_tooling/tools/hexbytes_custom.py +4 -1
- prediction_market_agent_tooling/tools/httpx_cached_client.py +15 -6
- prediction_market_agent_tooling/tools/langfuse_client_utils.py +21 -8
- prediction_market_agent_tooling/tools/openai_utils.py +31 -0
- prediction_market_agent_tooling/tools/perplexity/perplexity_client.py +86 -0
- prediction_market_agent_tooling/tools/perplexity/perplexity_models.py +26 -0
- prediction_market_agent_tooling/tools/perplexity/perplexity_search.py +73 -0
- prediction_market_agent_tooling/tools/rephrase.py +71 -0
- prediction_market_agent_tooling/tools/singleton.py +11 -6
- prediction_market_agent_tooling/tools/streamlit_utils.py +188 -0
- prediction_market_agent_tooling/tools/tokens/auto_deposit.py +64 -0
- prediction_market_agent_tooling/tools/tokens/auto_withdraw.py +8 -0
- prediction_market_agent_tooling/tools/tokens/slippage.py +21 -0
- prediction_market_agent_tooling/tools/tokens/usd.py +5 -2
- prediction_market_agent_tooling/tools/utils.py +61 -3
- prediction_market_agent_tooling/tools/web3_utils.py +63 -9
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/METADATA +13 -9
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/RECORD +86 -64
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/WHEEL +1 -1
- prediction_market_agent_tooling/abis/omen_agentresultmapping.abi.json +0 -171
- prediction_market_agent_tooling/markets/polymarket/data_models_web.py +0 -420
- {prediction_market_agent_tooling-0.65.5.dist-info → prediction_market_agent_tooling-0.69.17.dev1149.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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
|
-
|
|
5
|
-
|
|
10
|
+
|
|
11
|
+
class HttpxCachedClient(metaclass=SingletonMeta):
|
|
12
|
+
def __init__(self, ttl: timedelta = ONE_DAY) -> None:
|
|
6
13
|
storage = hishel.FileStorage(
|
|
7
|
-
ttl=
|
|
8
|
-
check_ttl_every=
|
|
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(
|
|
18
|
+
self.client: httpx.Client = hishel.CacheClient(
|
|
19
|
+
storage=storage, controller=controller
|
|
20
|
+
)
|
|
12
21
|
|
|
13
|
-
def get_client(self) ->
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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[
|
|
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
|
-
|
|
18
|
-
the returned instance.
|
|
21
|
+
Different __init__ arguments will result in different instances.
|
|
19
22
|
"""
|
|
20
|
-
|
|
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[
|
|
23
|
-
return self._instances[
|
|
27
|
+
self._instances[key] = instance
|
|
28
|
+
return self._instances[key]
|