agent-lab-sdk 0.1.40__py3-none-any.whl → 0.1.42__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.

Potentially problematic release.


This version of agent-lab-sdk might be problematic. Click here for more details.

@@ -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
- """bytes → python-object; fallback base64 для реально бинарных данных."""
232
- dumped = self.serde.dumps(obj)
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
- # def _safe_load(self, obj: Any) -> Any:
312
- # """Обратная операция к _safe_dump."""
313
- # if isinstance(obj, str):
314
- # try:
315
- # return self.serde.load(base64.b64decode(obj))
316
- # except Exception:
317
- # # не base64 обычная строка
318
- # return self.serde.load(obj)
319
- # return self.serde.load(obj)
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": {k: self._safe_dump(v) for k, v in cp["channel_values"].items()},
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": pending,
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._safe_load(value_payload)))
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._safe_load(value_payload)))
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={k: self._safe_load(v) for k, v in raw["channelValues"].items()},
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._safe_dump(v) # type: ignore[assignment]
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._safe_load(md)
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._safe_load(third)))
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._safe_load(third)))
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._safe_dump(v)} for ch, v in writes]
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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-lab-sdk
3
- Version: 0.1.40
3
+ Version: 0.1.42
4
4
  Summary: SDK для работы с Agent Lab
5
5
  Author-email: Andrew Ohurtsov <andermirik@yandex.com>
6
6
  License: Proprietary and Confidential — All Rights Reserved
@@ -1,6 +1,6 @@
1
1
  agent_lab_sdk/__init__.py,sha256=1Dlmv-wuz1QuciymKtYtX7jXzr_fkeGTe7aENfEDl3E,108
2
2
  agent_lab_sdk/langgraph/checkpoint/__init__.py,sha256=DnKwR1LwbaQ3qhb124lE-tnojrUIVcCdNzHEHwgpL5M,86
3
- agent_lab_sdk/langgraph/checkpoint/agw_saver.py,sha256=Srf8RYcW34_u2s54ABl0Jqm-_Z1gBH97gKqVY7QrKOQ,25631
3
+ agent_lab_sdk/langgraph/checkpoint/agw_saver.py,sha256=8A0nfn2GOpCHlFijxC3VQSiTNotoOftSPjwpqX7kQiE,30392
4
4
  agent_lab_sdk/langgraph/checkpoint/serde.py,sha256=UTSYbTbhBeL1CAr-XMbaH3SSIx9TeiC7ak22duXvqkw,5175
5
5
  agent_lab_sdk/llm/__init__.py,sha256=Yo9MbYdHS1iX05A9XiJGwWN1Hm4IARGav9mNFPrtDeA,376
6
6
  agent_lab_sdk/llm/agw_token_manager.py,sha256=_bPPI8muaEa6H01P8hHQOJHiiivaLd8N_d3OT9UT_80,4787
@@ -15,8 +15,8 @@ agent_lab_sdk/schema/log_message.py,sha256=nadi6lZGRuDSPmfbYs9QPpRJUT9Pfy8Y7pGCv
15
15
  agent_lab_sdk/storage/__init__.py,sha256=HAtUoqg3k0irqPMewayadVA9aXJOmYSxRr6a5J1scT0,174
16
16
  agent_lab_sdk/storage/storage.py,sha256=ELpt7GRwFD-aWa6ctinfA_QwcvzWLvKS0Wz8FlxVqAs,2075
17
17
  agent_lab_sdk/storage/storage_v2.py,sha256=ONseynX59xzWK17dfzxZvnii2rpz3Oo2Zo9Ck-lcGnw,1997
18
- agent_lab_sdk-0.1.40.dist-info/licenses/LICENSE,sha256=_TRXHkF3S9ilWBPdZcHLI_S-PRjK0L_SeOb2pcPAdV4,417
19
- agent_lab_sdk-0.1.40.dist-info/METADATA,sha256=CnbGjkYor6CXT0xxc4PBCd1n14yNLmGeyGYaw_IAYVQ,19099
20
- agent_lab_sdk-0.1.40.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
- agent_lab_sdk-0.1.40.dist-info/top_level.txt,sha256=E1efqkJ89KNmPBWdLzdMHeVtH0dYyCo4fhnSb81_15I,14
22
- agent_lab_sdk-0.1.40.dist-info/RECORD,,
18
+ agent_lab_sdk-0.1.42.dist-info/licenses/LICENSE,sha256=_TRXHkF3S9ilWBPdZcHLI_S-PRjK0L_SeOb2pcPAdV4,417
19
+ agent_lab_sdk-0.1.42.dist-info/METADATA,sha256=-cgSfyjYUfwlxVTFWTpdYEAxd-pCmESXTubfOANl6_o,19099
20
+ agent_lab_sdk-0.1.42.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
21
+ agent_lab_sdk-0.1.42.dist-info/top_level.txt,sha256=E1efqkJ89KNmPBWdLzdMHeVtH0dYyCo4fhnSb81_15I,14
22
+ agent_lab_sdk-0.1.42.dist-info/RECORD,,