agent-lab-sdk 0.1.40__tar.gz → 0.1.42__tar.gz
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.
Potentially problematic release.
This version of agent-lab-sdk might be problematic. Click here for more details.
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/PKG-INFO +1 -1
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/langgraph/checkpoint/agw_saver.py +148 -40
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk.egg-info/PKG-INFO +1 -1
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/pyproject.toml +1 -1
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/LICENSE +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/README.md +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/__init__.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/langgraph/checkpoint/__init__.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/langgraph/checkpoint/serde.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/llm/__init__.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/llm/agw_token_manager.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/llm/gigachat_token_manager.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/llm/llm.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/llm/throttled.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/metrics/__init__.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/metrics/metrics.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/schema/__init__.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/schema/input_types.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/schema/log_message.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/storage/__init__.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/storage/storage.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/storage/storage_v2.py +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk.egg-info/SOURCES.txt +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk.egg-info/dependency_links.txt +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk.egg-info/requires.txt +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk.egg-info/top_level.txt +0 -0
- {agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/setup.cfg +0 -0
{agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/langgraph/checkpoint/agw_saver.py
RENAMED
|
@@ -220,24 +220,33 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
|
|
|
220
220
|
self._client = None
|
|
221
221
|
|
|
222
222
|
# ----------------------- universal dump/load ---------------------
|
|
223
|
-
# def _safe_dump(self, obj: Any) -> Any:
|
|
224
|
-
# """self.serde.dump → гарантированная JSON-строка."""
|
|
225
|
-
# dumped = self.serde.dumps(obj)
|
|
226
|
-
# if isinstance(dumped, (bytes, bytearray)):
|
|
227
|
-
# return base64.b64encode(dumped).decode() # str
|
|
228
|
-
# return dumped # уже json-совместимо
|
|
229
|
-
|
|
230
223
|
def _safe_dump(self, obj: Any) -> Any:
|
|
231
|
-
"""
|
|
232
|
-
|
|
224
|
+
"""
|
|
225
|
+
JSON-first сериализация:
|
|
226
|
+
1) Пытаемся через self.serde.dumps(obj).
|
|
227
|
+
- Если вернул bytes: пробуем декодировать в JSON-строку и распарсить.
|
|
228
|
+
- Если не JSON/не UTF-8 — заворачиваем как base64-строку.
|
|
229
|
+
- Если вернул dict/list/scalar — они уже JSON-совместимы.
|
|
230
|
+
2) Если self.serde.dumps(obj) бросает исключение (например, для Send),
|
|
231
|
+
делаем типизированный фолбэк {"type": str, "blob": base64 | None}.
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
dumped = self.serde.dumps(obj)
|
|
235
|
+
except Exception:
|
|
236
|
+
# typed fallback (как рекомендуют в LangGraph для нетривиальных типов)
|
|
237
|
+
# https://langchain-ai.github.io/langgraph/reference/checkpoints/
|
|
238
|
+
try:
|
|
239
|
+
t, b = self.serde.dumps_typed(obj)
|
|
240
|
+
except Exception:
|
|
241
|
+
# крайний случай: строковое представление
|
|
242
|
+
t, b = type(obj).__name__, str(obj).encode()
|
|
243
|
+
return {"type": t, "blob": base64.b64encode(b).decode() if b is not None else None}
|
|
244
|
+
|
|
233
245
|
if isinstance(dumped, (bytes, bytearray)):
|
|
234
246
|
try:
|
|
235
|
-
# 1) bytes → str
|
|
236
247
|
s = dumped.decode()
|
|
237
|
-
# 2) str JSON → python (list/dict/scalar)
|
|
238
248
|
return orjson.loads(s)
|
|
239
249
|
except (UnicodeDecodeError, orjson.JSONDecodeError):
|
|
240
|
-
# не UTF-8 или не JSON → base64
|
|
241
250
|
return base64.b64encode(dumped).decode()
|
|
242
251
|
return dumped
|
|
243
252
|
|
|
@@ -308,15 +317,116 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
|
|
|
308
317
|
except Exception:
|
|
309
318
|
return obj
|
|
310
319
|
|
|
311
|
-
#
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
+
# ----------------------- deep dump/load (leaf-first) -------------
|
|
321
|
+
@staticmethod
|
|
322
|
+
def _is_json_scalar(x: Any) -> bool:
|
|
323
|
+
return x is None or isinstance(x, (str, int, float, bool))
|
|
324
|
+
|
|
325
|
+
@staticmethod
|
|
326
|
+
def _coerce_key(k: Any) -> str:
|
|
327
|
+
return k if isinstance(k, str) else str(k)
|
|
328
|
+
|
|
329
|
+
def _safe_dump_deep(self, obj: Any, _seen: set[int] | None = None) -> Any:
|
|
330
|
+
"""
|
|
331
|
+
Идём от листьев к корню:
|
|
332
|
+
- Для контейнеров рекурсируем внутрь и сохраняем форму контейнера.
|
|
333
|
+
- Для листьев вызываем _safe_dump (fallback на serde/typed + base64).
|
|
334
|
+
"""
|
|
335
|
+
if _seen is None:
|
|
336
|
+
_seen = set()
|
|
337
|
+
|
|
338
|
+
if self._is_json_scalar(obj):
|
|
339
|
+
return obj
|
|
340
|
+
|
|
341
|
+
if isinstance(obj, dict):
|
|
342
|
+
oid = id(obj)
|
|
343
|
+
if oid in _seen:
|
|
344
|
+
return {"type": "Cycle", "blob": None}
|
|
345
|
+
_seen.add(oid)
|
|
346
|
+
return {
|
|
347
|
+
self._coerce_key(k): self._safe_dump_deep(v, _seen) for k, v in obj.items()
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if isinstance(obj, (list, tuple, set)):
|
|
351
|
+
oid = id(obj)
|
|
352
|
+
if oid in _seen:
|
|
353
|
+
return ["<cycle>"]
|
|
354
|
+
_seen.add(oid)
|
|
355
|
+
return [self._safe_dump_deep(v, _seen) for v in obj]
|
|
356
|
+
|
|
357
|
+
# лист: доверяем универсальному дамперу
|
|
358
|
+
return self._safe_dump(obj)
|
|
359
|
+
|
|
360
|
+
def _safe_load_deep(self, obj: Any, _seen: set[int] | None = None) -> Any:
|
|
361
|
+
"""
|
|
362
|
+
Обратная операция:
|
|
363
|
+
- Контейнеры сначала пробуем целиком скормить serde.loads(...).
|
|
364
|
+
Если вернулся НЕ JSON-контейнер (например, объект сообщения) — возвращаем его.
|
|
365
|
+
Иначе рекурсивно обходим внутрь и листья скармливаем _safe_load.
|
|
366
|
+
- typed {"type","blob"} обрабатываем как раньше.
|
|
367
|
+
"""
|
|
368
|
+
if _seen is None:
|
|
369
|
+
_seen = set()
|
|
370
|
+
|
|
371
|
+
# Примитивы: просто через _safe_load (декод base64/bytes и т.п.)
|
|
372
|
+
if self._is_json_scalar(obj):
|
|
373
|
+
return self._safe_load(obj)
|
|
374
|
+
|
|
375
|
+
# dict
|
|
376
|
+
if isinstance(obj, dict):
|
|
377
|
+
# типизированная обёртка — сразу разворачиваем
|
|
378
|
+
if all(k in obj for k in TYPED_KEYS):
|
|
379
|
+
return self._safe_load(obj)
|
|
380
|
+
|
|
381
|
+
# 1) parse-first: пробуем целиком восстановить объект через serde
|
|
382
|
+
try:
|
|
383
|
+
parsed = self.serde.loads(orjson.dumps(obj))
|
|
384
|
+
# если получили не-JSON-контейнер (объект), возвращаем
|
|
385
|
+
if not isinstance(parsed, (dict, list, tuple, str, int, float, bool, type(None))):
|
|
386
|
+
return parsed
|
|
387
|
+
except Exception:
|
|
388
|
+
pass
|
|
389
|
+
|
|
390
|
+
# 2) иначе — рекурсивно
|
|
391
|
+
oid = id(obj)
|
|
392
|
+
if oid in _seen:
|
|
393
|
+
return obj
|
|
394
|
+
_seen.add(oid)
|
|
395
|
+
return {k: self._safe_load_deep(v, _seen) for k, v in obj.items()}
|
|
396
|
+
|
|
397
|
+
# list
|
|
398
|
+
if isinstance(obj, list):
|
|
399
|
+
# parse-first: пытаемся восстановить весь список одной операцией
|
|
400
|
+
try:
|
|
401
|
+
parsed = self.serde.loads(orjson.dumps(obj))
|
|
402
|
+
if not isinstance(parsed, (dict, list, tuple, str, int, float, bool, type(None))):
|
|
403
|
+
return parsed
|
|
404
|
+
except Exception:
|
|
405
|
+
pass
|
|
406
|
+
|
|
407
|
+
oid = id(obj)
|
|
408
|
+
if oid in _seen:
|
|
409
|
+
return obj
|
|
410
|
+
_seen.add(oid)
|
|
411
|
+
return [self._safe_load_deep(v, _seen) for v in obj]
|
|
412
|
+
|
|
413
|
+
# tuple — аналогично list, но вернём list (JSON-совместимо)
|
|
414
|
+
if isinstance(obj, tuple):
|
|
415
|
+
try:
|
|
416
|
+
parsed = self.serde.loads(orjson.dumps(obj))
|
|
417
|
+
if not isinstance(parsed, (dict, list, tuple, str, int, float, bool, type(None))):
|
|
418
|
+
return parsed
|
|
419
|
+
except Exception:
|
|
420
|
+
pass
|
|
421
|
+
|
|
422
|
+
oid = id(obj)
|
|
423
|
+
if oid in _seen:
|
|
424
|
+
return obj
|
|
425
|
+
_seen.add(oid)
|
|
426
|
+
return [self._safe_load_deep(v, _seen) for v in obj]
|
|
427
|
+
|
|
428
|
+
# Всё остальное — лист, через _safe_load
|
|
429
|
+
return self._safe_load(obj)
|
|
320
430
|
|
|
321
431
|
# ----------------------- config <-> api --------------------------
|
|
322
432
|
def _to_api_config(self, cfg: RunnableConfig | None) -> Dict[str, Any]:
|
|
@@ -335,25 +445,21 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
|
|
|
335
445
|
|
|
336
446
|
# --------------------- checkpoint (de)ser ------------------------
|
|
337
447
|
def _encode_cp(self, cp: Checkpoint) -> Dict[str, Any]:
|
|
338
|
-
pending: list[Any] = []
|
|
339
|
-
for item in cp.get("pending_sends", []) or []:
|
|
340
|
-
try:
|
|
341
|
-
channel, value = item
|
|
342
|
-
except Exception:
|
|
343
|
-
pending.append(item)
|
|
344
|
-
continue
|
|
345
|
-
pending.append([channel, self._safe_dump(value)])
|
|
346
448
|
return {
|
|
347
449
|
"v": cp["v"],
|
|
348
450
|
"id": cp["id"],
|
|
349
451
|
"ts": cp["ts"],
|
|
350
|
-
"channelValues": {
|
|
452
|
+
"channelValues": {
|
|
453
|
+
k: self._safe_dump_deep(v) for k, v in cp["channel_values"].items()
|
|
454
|
+
},
|
|
351
455
|
"channelVersions": cp["channel_versions"],
|
|
352
456
|
"versionsSeen": cp["versions_seen"],
|
|
353
|
-
"pendingSends":
|
|
457
|
+
"pendingSends": [] # как в BasePostgresSaver, они внутри checkpoint не нужны
|
|
354
458
|
}
|
|
355
459
|
|
|
356
460
|
def _decode_cp(self, raw: Dict[str, Any]) -> Checkpoint:
|
|
461
|
+
# Поддерживаем приём pendingSends (если сервер их отдаёт),
|
|
462
|
+
# но сами их не шлём при записи.
|
|
357
463
|
pending_sends: list[Tuple[str, Any]] = []
|
|
358
464
|
for obj in raw.get("pendingSends", []) or []:
|
|
359
465
|
if isinstance(obj, dict) and "channel" in obj:
|
|
@@ -361,18 +467,20 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
|
|
|
361
467
|
value_payload: Any = obj.get("value")
|
|
362
468
|
if value_payload is None and all(k in obj for k in TYPED_KEYS):
|
|
363
469
|
value_payload = {k: obj[k] for k in TYPED_KEYS}
|
|
364
|
-
pending_sends.append((channel, self.
|
|
470
|
+
pending_sends.append((channel, self._safe_load_deep(value_payload)))
|
|
365
471
|
elif isinstance(obj, (list, tuple)) and len(obj) >= 2:
|
|
366
472
|
channel = obj[0]
|
|
367
473
|
value_payload = obj[1]
|
|
368
|
-
pending_sends.append((channel, self.
|
|
474
|
+
pending_sends.append((channel, self._safe_load_deep(value_payload)))
|
|
369
475
|
else:
|
|
370
|
-
pending_sends.append(obj)
|
|
476
|
+
pending_sends.append(obj)
|
|
371
477
|
return Checkpoint(
|
|
372
478
|
v=raw["v"],
|
|
373
479
|
id=raw["id"],
|
|
374
480
|
ts=raw["ts"],
|
|
375
|
-
channel_values={
|
|
481
|
+
channel_values={
|
|
482
|
+
k: self._safe_load_deep(v) for k, v in raw["channelValues"].items()
|
|
483
|
+
},
|
|
376
484
|
channel_versions=raw["channelVersions"],
|
|
377
485
|
versions_seen=raw["versionsSeen"],
|
|
378
486
|
pending_sends=pending_sends,
|
|
@@ -405,13 +513,13 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
|
|
|
405
513
|
return {}
|
|
406
514
|
out: CheckpointMetadata = {}
|
|
407
515
|
for k, v in md.items():
|
|
408
|
-
out[k] = self._enc_meta(v) if isinstance(v, dict) else self.
|
|
516
|
+
out[k] = self._enc_meta(v) if isinstance(v, dict) else self._safe_dump_deep(v) # type: ignore[assignment]
|
|
409
517
|
return out
|
|
410
518
|
|
|
411
519
|
def _dec_meta(self, md: Any) -> Any:
|
|
412
520
|
if isinstance(md, dict):
|
|
413
521
|
return {k: self._dec_meta(v) for k, v in md.items()}
|
|
414
|
-
return self.
|
|
522
|
+
return self._safe_load_deep(md)
|
|
415
523
|
|
|
416
524
|
# ------------------------ HTTP wrapper ---------------------------
|
|
417
525
|
async def _http(
|
|
@@ -522,7 +630,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
|
|
|
522
630
|
k in second for k in TYPED_KEYS
|
|
523
631
|
):
|
|
524
632
|
third = second
|
|
525
|
-
pending.append((first, second, self.
|
|
633
|
+
pending.append((first, second, self._safe_load_deep(third)))
|
|
526
634
|
elif isinstance(w, (list, tuple)):
|
|
527
635
|
if len(w) == 3:
|
|
528
636
|
first, second, third = w
|
|
@@ -531,7 +639,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
|
|
|
531
639
|
third = None
|
|
532
640
|
else:
|
|
533
641
|
continue
|
|
534
|
-
pending.append((first, second, self.
|
|
642
|
+
pending.append((first, second, self._safe_load_deep(third)))
|
|
535
643
|
return CheckpointTuple(
|
|
536
644
|
config=self._decode_config(node.get("config")),
|
|
537
645
|
checkpoint=self._decode_cp(node["checkpoint"]),
|
|
@@ -610,7 +718,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
|
|
|
610
718
|
task_id: str,
|
|
611
719
|
task_path: str = "",
|
|
612
720
|
) -> None:
|
|
613
|
-
enc = [{"first": ch, "second": self.
|
|
721
|
+
enc = [{"first": ch, "second": self._safe_dump_deep(v)} for ch, v in writes]
|
|
614
722
|
payload = {
|
|
615
723
|
"config": self._to_api_config(cfg),
|
|
616
724
|
"writes": enc,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_lab_sdk-0.1.40 → agent_lab_sdk-0.1.42}/agent_lab_sdk/langgraph/checkpoint/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|