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,6 +1,8 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import hashlib
|
|
2
3
|
import inspect
|
|
3
4
|
import json
|
|
5
|
+
from dataclasses import dataclass
|
|
4
6
|
from datetime import timedelta
|
|
5
7
|
from functools import wraps
|
|
6
8
|
from types import UnionType
|
|
@@ -12,6 +14,7 @@ from typing import (
|
|
|
12
14
|
cast,
|
|
13
15
|
get_args,
|
|
14
16
|
get_origin,
|
|
17
|
+
get_type_hints,
|
|
15
18
|
overload,
|
|
16
19
|
)
|
|
17
20
|
|
|
@@ -25,7 +28,10 @@ from sqlmodel import Field, SQLModel, desc, select
|
|
|
25
28
|
from prediction_market_agent_tooling.config import APIKeys
|
|
26
29
|
from prediction_market_agent_tooling.loggers import logger
|
|
27
30
|
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
|
|
28
|
-
from prediction_market_agent_tooling.tools.db.db_manager import
|
|
31
|
+
from prediction_market_agent_tooling.tools.db.db_manager import (
|
|
32
|
+
DBManager,
|
|
33
|
+
EnsureTableManager,
|
|
34
|
+
)
|
|
29
35
|
from prediction_market_agent_tooling.tools.utils import utcnow
|
|
30
36
|
|
|
31
37
|
DB_CACHE_LOG_PREFIX = "[db-cache]"
|
|
@@ -46,6 +52,10 @@ class FunctionCache(SQLModel, table=True):
|
|
|
46
52
|
created_at: DatetimeUTC = Field(default_factory=utcnow, index=True)
|
|
47
53
|
|
|
48
54
|
|
|
55
|
+
# Global instance of the table manager for FunctionCache
|
|
56
|
+
_table_manager = EnsureTableManager([FunctionCache])
|
|
57
|
+
|
|
58
|
+
|
|
49
59
|
@overload
|
|
50
60
|
def db_cache(
|
|
51
61
|
func: None = None,
|
|
@@ -101,136 +111,228 @@ def db_cache(
|
|
|
101
111
|
|
|
102
112
|
api_keys = api_keys if api_keys is not None else APIKeys()
|
|
103
113
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
# If caching is disabled, just call the function and return it
|
|
107
|
-
if not api_keys.ENABLE_CACHE:
|
|
108
|
-
return func(*args, **kwargs)
|
|
109
|
-
|
|
110
|
-
DBManager(api_keys.sqlalchemy_db_url.get_secret_value()).create_tables(
|
|
111
|
-
[FunctionCache]
|
|
112
|
-
)
|
|
114
|
+
# Check if the decorated function is async
|
|
115
|
+
if inspect.iscoroutinefunction(func):
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
# Convert any argument that is Pydantic model into classic dictionary, otherwise it won't be json-serializable.
|
|
120
|
-
args_dict: dict[str, Any] = bound_arguments.arguments
|
|
121
|
-
|
|
122
|
-
# Remove `self` or `cls` if present (in case of class' methods)
|
|
123
|
-
if "self" in args_dict:
|
|
124
|
-
del args_dict["self"]
|
|
125
|
-
if "cls" in args_dict:
|
|
126
|
-
del args_dict["cls"]
|
|
127
|
-
|
|
128
|
-
# Remove ignored arguments
|
|
129
|
-
if ignore_args:
|
|
130
|
-
for arg in ignore_args:
|
|
131
|
-
if arg in args_dict:
|
|
132
|
-
del args_dict[arg]
|
|
133
|
-
|
|
134
|
-
# Remove arguments of ignored types
|
|
135
|
-
if ignore_arg_types:
|
|
136
|
-
args_dict = {
|
|
137
|
-
k: v
|
|
138
|
-
for k, v in args_dict.items()
|
|
139
|
-
if not isinstance(v, tuple(ignore_arg_types))
|
|
140
|
-
}
|
|
117
|
+
@wraps(func)
|
|
118
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
119
|
+
# If caching is disabled, just call the function and return it
|
|
120
|
+
if not api_keys.ENABLE_CACHE:
|
|
121
|
+
return await func(*args, **kwargs)
|
|
141
122
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
args_hash = hashlib.md5(arg_string.encode()).hexdigest()
|
|
123
|
+
# Ensure tables are created before accessing cache
|
|
124
|
+
await _table_manager.ensure_tables_async(api_keys)
|
|
145
125
|
|
|
146
|
-
|
|
147
|
-
full_function_name = func.__module__ + "." + func.__qualname__
|
|
148
|
-
# But also get the standard function name to easily search for it in database
|
|
149
|
-
function_name = func.__name__
|
|
126
|
+
ctx = _build_context(func, args, kwargs, ignore_args, ignore_arg_types)
|
|
150
127
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
is_pydantic_model = return_type is not None and contains_pydantic_model(
|
|
154
|
-
return_type
|
|
155
|
-
)
|
|
128
|
+
# Fetch cached result in thread pool
|
|
129
|
+
lookup = await asyncio.to_thread(_fetch_cached, api_keys, ctx, max_age)
|
|
156
130
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
# Try to get cached result
|
|
161
|
-
statement = (
|
|
162
|
-
select(FunctionCache)
|
|
163
|
-
.where(
|
|
164
|
-
FunctionCache.function_name == function_name,
|
|
165
|
-
FunctionCache.full_function_name == full_function_name,
|
|
166
|
-
FunctionCache.args_hash == args_hash,
|
|
131
|
+
if lookup.hit:
|
|
132
|
+
logger.debug(
|
|
133
|
+
f"{DB_CACHE_LOG_PREFIX} [cache-hit] Cache hit for {ctx.full_function_name}"
|
|
167
134
|
)
|
|
168
|
-
.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
cached_result = session.exec(statement).first()
|
|
174
|
-
|
|
175
|
-
if cached_result:
|
|
176
|
-
logger.info(
|
|
177
|
-
# Keep the special [case-hit] identifier so we can easily track it in GCP.
|
|
178
|
-
f"{DB_CACHE_LOG_PREFIX} [cache-hit] Cache hit for {full_function_name} with args {args_dict} and output {cached_result.result}"
|
|
135
|
+
return lookup.value
|
|
136
|
+
|
|
137
|
+
computed_result = await func(*args, **kwargs)
|
|
138
|
+
logger.debug(
|
|
139
|
+
f"{DB_CACHE_LOG_PREFIX} [cache-miss] Cache miss for {ctx.full_function_name}"
|
|
179
140
|
)
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
141
|
+
|
|
142
|
+
if cache_none or computed_result is not None:
|
|
143
|
+
# Save cached result in thread pool (fire-and-forget)
|
|
144
|
+
asyncio.create_task(
|
|
145
|
+
asyncio.to_thread(
|
|
146
|
+
_save_cached,
|
|
147
|
+
api_keys,
|
|
148
|
+
ctx,
|
|
149
|
+
computed_result,
|
|
150
|
+
log_error_on_unsavable_data,
|
|
190
151
|
)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return computed_result
|
|
155
|
+
|
|
156
|
+
return cast(FunctionT, async_wrapper)
|
|
157
|
+
|
|
158
|
+
@wraps(func)
|
|
159
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
160
|
+
if not api_keys.ENABLE_CACHE:
|
|
161
|
+
return func(*args, **kwargs)
|
|
162
|
+
|
|
163
|
+
# Ensure tables are created before accessing cache
|
|
164
|
+
_table_manager.ensure_tables_sync(api_keys)
|
|
165
|
+
|
|
166
|
+
ctx = _build_context(func, args, kwargs, ignore_args, ignore_arg_types)
|
|
167
|
+
lookup = _fetch_cached(api_keys, ctx, max_age)
|
|
168
|
+
|
|
169
|
+
if lookup.hit:
|
|
170
|
+
logger.debug(
|
|
171
|
+
f"{DB_CACHE_LOG_PREFIX} [cache-hit] Cache hit for {ctx.full_function_name}"
|
|
172
|
+
)
|
|
173
|
+
return lookup.value
|
|
194
174
|
|
|
195
|
-
# On cache miss, compute the result
|
|
196
175
|
computed_result = func(*args, **kwargs)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
f"{DB_CACHE_LOG_PREFIX} [cache-miss] Cache miss for {full_function_name} with args {args_dict}, computed the output {computed_result}"
|
|
176
|
+
logger.debug(
|
|
177
|
+
f"{DB_CACHE_LOG_PREFIX} [cache-miss] Cache miss for {ctx.full_function_name}"
|
|
200
178
|
)
|
|
201
179
|
|
|
202
|
-
# If postgres access was specified, save it.
|
|
203
180
|
if cache_none or computed_result is not None:
|
|
204
|
-
|
|
205
|
-
function_name=function_name,
|
|
206
|
-
full_function_name=full_function_name,
|
|
207
|
-
args_hash=args_hash,
|
|
208
|
-
args=args_dict,
|
|
209
|
-
result=computed_result,
|
|
210
|
-
created_at=utcnow(),
|
|
211
|
-
)
|
|
212
|
-
# Do not raise an exception if saving to the database fails, just log it and let the agent continue the work.
|
|
213
|
-
try:
|
|
214
|
-
with DBManager(
|
|
215
|
-
api_keys.sqlalchemy_db_url.get_secret_value()
|
|
216
|
-
).get_session() as session:
|
|
217
|
-
logger.info(
|
|
218
|
-
f"{DB_CACHE_LOG_PREFIX} [cache-info] Saving {cache_entry} into database."
|
|
219
|
-
)
|
|
220
|
-
session.add(cache_entry)
|
|
221
|
-
session.commit()
|
|
222
|
-
except (DataError, psycopg2.errors.UntranslatableCharacter) as e:
|
|
223
|
-
(logger.error if log_error_on_unsavable_data else logger.warning)(
|
|
224
|
-
f"{DB_CACHE_LOG_PREFIX} [cache-error] Failed to save {cache_entry} into database, ignoring, because: {e}"
|
|
225
|
-
)
|
|
226
|
-
except Exception:
|
|
227
|
-
logger.exception(
|
|
228
|
-
f"{DB_CACHE_LOG_PREFIX} [cache-error] Failed to save {cache_entry} into database, ignoring."
|
|
229
|
-
)
|
|
181
|
+
_save_cached(api_keys, ctx, computed_result, log_error_on_unsavable_data)
|
|
230
182
|
|
|
231
183
|
return computed_result
|
|
232
184
|
|
|
233
|
-
return cast(FunctionT,
|
|
185
|
+
return cast(FunctionT, sync_wrapper)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass
|
|
189
|
+
class CallContext:
|
|
190
|
+
args_dict: dict[str, Any]
|
|
191
|
+
args_hash: str
|
|
192
|
+
function_name: str
|
|
193
|
+
full_function_name: str
|
|
194
|
+
return_type: Any
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def is_pydantic_model(self) -> bool:
|
|
198
|
+
return self.return_type is not None and contains_pydantic_model(
|
|
199
|
+
self.return_type
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@dataclass
|
|
204
|
+
class CacheLookup:
|
|
205
|
+
hit: bool
|
|
206
|
+
value: Any | None = None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _build_context(
|
|
210
|
+
func: Callable[..., Any],
|
|
211
|
+
args: tuple[Any, ...],
|
|
212
|
+
kwargs: dict[str, Any],
|
|
213
|
+
ignore_args: Sequence[str] | None,
|
|
214
|
+
ignore_arg_types: Sequence[type] | None,
|
|
215
|
+
) -> CallContext:
|
|
216
|
+
signature = inspect.signature(func)
|
|
217
|
+
bound_arguments = signature.bind(*args, **kwargs)
|
|
218
|
+
bound_arguments.apply_defaults()
|
|
219
|
+
|
|
220
|
+
args_dict: dict[str, Any] = bound_arguments.arguments
|
|
221
|
+
|
|
222
|
+
if "self" in args_dict:
|
|
223
|
+
del args_dict["self"]
|
|
224
|
+
if "cls" in args_dict:
|
|
225
|
+
del args_dict["cls"]
|
|
226
|
+
|
|
227
|
+
if ignore_args:
|
|
228
|
+
for arg in ignore_args:
|
|
229
|
+
if arg in args_dict:
|
|
230
|
+
del args_dict[arg]
|
|
231
|
+
|
|
232
|
+
if ignore_arg_types:
|
|
233
|
+
args_dict = {
|
|
234
|
+
k: v
|
|
235
|
+
for k, v in args_dict.items()
|
|
236
|
+
if not isinstance(v, tuple(ignore_arg_types))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
arg_string = json.dumps(args_dict, sort_keys=True, default=str)
|
|
240
|
+
args_hash = hashlib.md5(arg_string.encode()).hexdigest()
|
|
241
|
+
|
|
242
|
+
full_function_name = func.__module__ + "." + func.__qualname__
|
|
243
|
+
function_name = func.__name__
|
|
244
|
+
|
|
245
|
+
# Use get_type_hints to resolve forward references instead of __annotations__
|
|
246
|
+
try:
|
|
247
|
+
type_hints = get_type_hints(func)
|
|
248
|
+
return_type = type_hints.get("return", None)
|
|
249
|
+
except (NameError, AttributeError, TypeError) as e:
|
|
250
|
+
# Fallback to raw annotations if get_type_hints fails
|
|
251
|
+
logger.debug(
|
|
252
|
+
f"{DB_CACHE_LOG_PREFIX} Failed to resolve type hints for {full_function_name}, falling back to raw annotations: {e}"
|
|
253
|
+
)
|
|
254
|
+
return_type = func.__annotations__.get("return", None)
|
|
255
|
+
|
|
256
|
+
return CallContext(
|
|
257
|
+
args_dict=args_dict,
|
|
258
|
+
args_hash=args_hash,
|
|
259
|
+
function_name=function_name,
|
|
260
|
+
full_function_name=full_function_name,
|
|
261
|
+
return_type=return_type,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _fetch_cached(
|
|
266
|
+
api_keys: APIKeys,
|
|
267
|
+
ctx: CallContext,
|
|
268
|
+
max_age: timedelta | None,
|
|
269
|
+
) -> CacheLookup:
|
|
270
|
+
with DBManager(
|
|
271
|
+
api_keys.sqlalchemy_db_url.get_secret_value()
|
|
272
|
+
).get_session() as session:
|
|
273
|
+
statement = (
|
|
274
|
+
select(FunctionCache)
|
|
275
|
+
.where(
|
|
276
|
+
FunctionCache.function_name == ctx.function_name,
|
|
277
|
+
FunctionCache.full_function_name == ctx.full_function_name,
|
|
278
|
+
FunctionCache.args_hash == ctx.args_hash,
|
|
279
|
+
)
|
|
280
|
+
.order_by(desc(FunctionCache.created_at))
|
|
281
|
+
)
|
|
282
|
+
if max_age is not None:
|
|
283
|
+
cutoff_time = utcnow() - max_age
|
|
284
|
+
statement = statement.where(FunctionCache.created_at >= cutoff_time)
|
|
285
|
+
cached_result = session.exec(statement).first()
|
|
286
|
+
|
|
287
|
+
if not cached_result:
|
|
288
|
+
return CacheLookup(hit=False)
|
|
289
|
+
|
|
290
|
+
if ctx.is_pydantic_model:
|
|
291
|
+
try:
|
|
292
|
+
value = convert_cached_output_to_pydantic(
|
|
293
|
+
ctx.return_type, cached_result.result
|
|
294
|
+
)
|
|
295
|
+
return CacheLookup(hit=True, value=value)
|
|
296
|
+
except (ValueError, TypeError) as e:
|
|
297
|
+
logger.warning(
|
|
298
|
+
f"{DB_CACHE_LOG_PREFIX} [cache-miss] Failed to validate cached result for {ctx.full_function_name}, treating as cache miss: {e}"
|
|
299
|
+
)
|
|
300
|
+
return CacheLookup(hit=False)
|
|
301
|
+
|
|
302
|
+
return CacheLookup(hit=True, value=cached_result.result)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _save_cached(
|
|
306
|
+
api_keys: APIKeys,
|
|
307
|
+
ctx: CallContext,
|
|
308
|
+
computed_result: Any,
|
|
309
|
+
log_error_on_unsavable_data: bool,
|
|
310
|
+
) -> None:
|
|
311
|
+
cache_entry = FunctionCache(
|
|
312
|
+
function_name=ctx.function_name,
|
|
313
|
+
full_function_name=ctx.full_function_name,
|
|
314
|
+
args_hash=ctx.args_hash,
|
|
315
|
+
args=ctx.args_dict,
|
|
316
|
+
result=computed_result,
|
|
317
|
+
created_at=utcnow(),
|
|
318
|
+
)
|
|
319
|
+
try:
|
|
320
|
+
with DBManager(
|
|
321
|
+
api_keys.sqlalchemy_db_url.get_secret_value()
|
|
322
|
+
).get_session() as session:
|
|
323
|
+
logger.debug(
|
|
324
|
+
f"{DB_CACHE_LOG_PREFIX} [cache-save] Saving cache entry for {ctx.full_function_name}"
|
|
325
|
+
)
|
|
326
|
+
session.add(cache_entry)
|
|
327
|
+
session.commit()
|
|
328
|
+
except (DataError, psycopg2.errors.UntranslatableCharacter) as e:
|
|
329
|
+
(logger.error if log_error_on_unsavable_data else logger.warning)(
|
|
330
|
+
f"{DB_CACHE_LOG_PREFIX} [cache-error] Failed to save cache entry for {ctx.full_function_name}: {e}"
|
|
331
|
+
)
|
|
332
|
+
except Exception:
|
|
333
|
+
logger.exception(
|
|
334
|
+
f"{DB_CACHE_LOG_PREFIX} [cache-error] Failed to save cache entry for {ctx.full_function_name}"
|
|
335
|
+
)
|
|
234
336
|
|
|
235
337
|
|
|
236
338
|
def contains_pydantic_model(return_type: Any) -> bool:
|
|
@@ -261,8 +363,8 @@ def convert_cached_output_to_pydantic(return_type: Any, data: Any) -> Any:
|
|
|
261
363
|
if origin is None:
|
|
262
364
|
if inspect.isclass(return_type) and issubclass(return_type, BaseModel):
|
|
263
365
|
# Convert the dictionary to a Pydantic model
|
|
264
|
-
return return_type(
|
|
265
|
-
|
|
366
|
+
return return_type.model_validate(
|
|
367
|
+
{
|
|
266
368
|
k: convert_cached_output_to_pydantic(
|
|
267
369
|
getattr(return_type, k, None), v
|
|
268
370
|
)
|
|
@@ -4,6 +4,7 @@ from datetime import date, timedelta
|
|
|
4
4
|
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
|
|
7
|
+
from prediction_market_agent_tooling.gtypes import HexBytes
|
|
7
8
|
from prediction_market_agent_tooling.tools.datetime_utc import DatetimeUTC
|
|
8
9
|
|
|
9
10
|
|
|
@@ -12,7 +13,7 @@ def json_serializer(x: t.Any) -> str:
|
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def json_serializer_default_fn(
|
|
15
|
-
y: DatetimeUTC | timedelta | date | BaseModel,
|
|
16
|
+
y: DatetimeUTC | timedelta | date | HexBytes | BaseModel,
|
|
16
17
|
) -> str | dict[str, t.Any]:
|
|
17
18
|
"""
|
|
18
19
|
Used to serialize objects that don't support it by default into a specific string that can be deserialized out later.
|
|
@@ -25,8 +26,13 @@ def json_serializer_default_fn(
|
|
|
25
26
|
return f"timedelta::{y.total_seconds()}"
|
|
26
27
|
elif isinstance(y, date):
|
|
27
28
|
return f"date::{y.isoformat()}"
|
|
29
|
+
elif isinstance(y, HexBytes):
|
|
30
|
+
return f"HexBytes::{y.to_0x_hex()}"
|
|
28
31
|
elif isinstance(y, BaseModel):
|
|
29
|
-
|
|
32
|
+
# For some reason, Pydantic by default serializes using the field names (not alias),
|
|
33
|
+
# but also by default, deserializes only using the aliased names.
|
|
34
|
+
# `by_alias=True` here to work by default with models that have some fields with aliased names.
|
|
35
|
+
return y.model_dump(by_alias=True)
|
|
30
36
|
raise TypeError(
|
|
31
37
|
f"Unsupported type for the default json serialize function, value is {y}."
|
|
32
38
|
)
|
|
@@ -51,6 +57,9 @@ def replace_custom_stringified_objects(obj: t.Any) -> t.Any:
|
|
|
51
57
|
elif obj.startswith("date::"):
|
|
52
58
|
iso_str = obj[len("date::") :]
|
|
53
59
|
return date.fromisoformat(iso_str)
|
|
60
|
+
elif obj.startswith("HexBytes::"):
|
|
61
|
+
hex_str = obj[len("HexBytes::") :]
|
|
62
|
+
return HexBytes(hex_str)
|
|
54
63
|
else:
|
|
55
64
|
return obj
|
|
56
65
|
elif isinstance(obj, dict):
|