agent-lab-sdk 0.1.35.dev1__py3-none-any.whl → 0.1.37__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.

@@ -1,15 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import orjson
4
+ from random import random
5
+ from langgraph.checkpoint.serde.types import ChannelProtocol
3
6
  import asyncio
4
7
  import base64
5
8
  import logging
6
9
  import os
7
10
  from contextlib import asynccontextmanager
8
- from random import random
9
11
  from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple
10
12
 
11
13
  import httpx
12
- import orjson
13
14
  from langchain_core.runnables import RunnableConfig
14
15
 
15
16
  from langgraph.checkpoint.base import (
@@ -23,7 +24,7 @@ from langgraph.checkpoint.base import (
23
24
  )
24
25
  from langgraph.checkpoint.serde.base import SerializerProtocol
25
26
  from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
26
- from langgraph.checkpoint.serde.types import ChannelProtocol
27
+
27
28
  from .serde import Serializer
28
29
 
29
30
  __all__ = ["AsyncAGWCheckpointSaver"]
@@ -33,17 +34,22 @@ logger = logging.getLogger(__name__)
33
34
  TYPED_KEYS = ("type", "blob")
34
35
 
35
36
 
36
- def _to_b64(b: bytes | None) -> str | None:
37
- return base64.b64encode(b).decode() if b is not None else None
38
-
39
-
40
- def _b64decode_strict(s: str) -> bytes | None:
41
- """Возвращает bytes только если строка действительно корректная base64."""
37
+ def _b64decode_strict(value: str) -> bytes | None:
42
38
  try:
43
- return base64.b64decode(s, validate=True)
39
+ return base64.b64decode(value, validate=True)
44
40
  except Exception:
45
41
  return None
46
42
 
43
+ # ------------------------------------------------------------------ #
44
+ # helpers for Py < 3.10
45
+ # ------------------------------------------------------------------ #
46
+ try:
47
+ anext # type: ignore[name-defined]
48
+ except NameError: # pragma: no cover
49
+
50
+ async def anext(it):
51
+ return await it.__anext__()
52
+
47
53
 
48
54
  class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
49
55
  """Persist checkpoints in Agent-Gateway с помощью `httpx` async client."""
@@ -61,15 +67,14 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
61
67
  ):
62
68
  if not serde:
63
69
  base_serde: SerializerProtocol = Serializer()
64
- # опционально оборачиваем в AES по ENV
65
- _aes_key = (
70
+ aes_key = (
66
71
  os.getenv("LANGGRAPH_AES_KEY")
67
72
  or os.getenv("AGW_AES_KEY")
68
73
  or os.getenv("AES_KEY")
69
74
  )
70
- if _aes_key:
75
+ if aes_key:
71
76
  base_serde = EncryptedSerializer.from_pycryptodome_aes(
72
- base_serde, key=_aes_key
77
+ base_serde, key=aes_key
73
78
  )
74
79
  serde = base_serde
75
80
  super().__init__(serde=serde)
@@ -91,7 +96,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
91
96
  headers=self.headers,
92
97
  timeout=self.timeout,
93
98
  verify=verify,
94
- trust_env=True,
99
+ trust_env=True
95
100
  )
96
101
 
97
102
  async def __aenter__(self): # noqa: D401
@@ -100,63 +105,105 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
100
105
  async def __aexit__(self, exc_type, exc, tb): # noqa: D401
101
106
  await self._client.aclose()
102
107
 
103
- # ----------------------- typed (de)serialize ---------------------
104
- def _encode_typed(self, value: Any) -> dict[str, Any]:
105
- """value -> {"type": str, "blob": base64str | null}"""
106
- t, b = self.serde.dumps_typed(value)
107
- return {"type": t, "blob": _to_b64(b)}
108
-
109
- def _decode_typed(self, obj: Any) -> Any:
110
- """{type, blob} | [type, blob] | legacy -> python."""
111
- # Новый формат: dict с ключами type/blob — только если blob валидная base64 или None
112
- if isinstance(obj, dict) and all(k in obj for k in TYPED_KEYS):
113
- t = obj.get("type")
114
- b64 = obj.get("blob")
115
- if b64 is None:
116
- return self.serde.loads_typed((t, None))
117
- if isinstance(b64, str):
118
- b = _b64decode_strict(b64)
119
- if b is not None:
120
- return self.serde.loads_typed((t, b))
121
- # если невалидно — падаем ниже на общую обработку
122
-
123
- # Допускаем tuple/list вида [type, base64] — только при валидной base64
124
- if isinstance(obj, (list, tuple)) and len(obj) == 2 and isinstance(obj[0], str):
125
- t, b64 = obj
126
- if b64 is None and t == "empty":
127
- return self.serde.loads_typed((t, None))
128
- if isinstance(b64, str):
129
- b = _b64decode_strict(b64)
130
- if b is not None:
131
- return self.serde.loads_typed((t, b))
132
- # иначе это не typed-пара
133
-
134
- # Если это строка — пробуем как base64 строго, затем как JSON-строку
135
- if isinstance(obj, str):
136
- b = _b64decode_strict(obj)
137
- if b is not None:
138
- try:
139
- return self.serde.loads(b)
140
- except Exception:
141
- pass
108
+ # ----------------------- universal dump/load ---------------------
109
+ # def _safe_dump(self, obj: Any) -> Any:
110
+ # """self.serde.dump гарантированная JSON-строка."""
111
+ # dumped = self.serde.dumps(obj)
112
+ # if isinstance(dumped, (bytes, bytearray)):
113
+ # return base64.b64encode(dumped).decode() # str
114
+ # return dumped # уже json-совместимо
115
+
116
+ def _safe_dump(self, obj: Any) -> Any:
117
+ """bytes python-object; fallback base64 для реально бинарных данных."""
118
+ dumped = self.serde.dumps(obj)
119
+ if isinstance(dumped, (bytes, bytearray)):
142
120
  try:
143
- return self.serde.loads(obj.encode())
121
+ # 1) bytes → str
122
+ s = dumped.decode()
123
+ # 2) str JSON → python (list/dict/scalar)
124
+ return orjson.loads(s)
125
+ except (UnicodeDecodeError, orjson.JSONDecodeError):
126
+ # не UTF-8 или не JSON → base64
127
+ return base64.b64encode(dumped).decode()
128
+ return dumped
129
+
130
+ def _safe_load(self, obj: Any) -> Any:
131
+ if obj is None:
132
+ return None
133
+
134
+ if isinstance(obj, dict):
135
+ if all(k in obj for k in TYPED_KEYS):
136
+ t = obj.get("type")
137
+ blob = obj.get("blob")
138
+ if blob is None:
139
+ try:
140
+ return self.serde.loads_typed((t, None))
141
+ except Exception:
142
+ return obj
143
+ if isinstance(blob, str):
144
+ payload = _b64decode_strict(blob)
145
+ if payload is not None:
146
+ try:
147
+ return self.serde.loads_typed((t, payload))
148
+ except Exception:
149
+ # fall back to generic handling below
150
+ pass
151
+ try:
152
+ return self.serde.loads(orjson.dumps(obj))
144
153
  except Exception:
145
154
  return obj
146
155
 
147
- # dict/list -> считаем это уже JSON и грузим через serde
148
- if isinstance(obj, (dict, list)):
156
+ if isinstance(obj, (list, tuple)):
157
+ if (
158
+ len(obj) == 2
159
+ and isinstance(obj[0], str)
160
+ and (obj[1] is None or isinstance(obj[1], str))
161
+ ):
162
+ blob = obj[1]
163
+ if blob is None:
164
+ try:
165
+ return self.serde.loads_typed((obj[0], None))
166
+ except Exception:
167
+ pass
168
+ elif isinstance(blob, str):
169
+ payload = _b64decode_strict(blob)
170
+ if payload is not None:
171
+ try:
172
+ return self.serde.loads_typed((obj[0], payload))
173
+ except Exception:
174
+ pass
149
175
  try:
150
- return self.serde.loads(orjson.dumps(obj))
176
+ return self.serde.loads(orjson.dumps(list(obj)))
151
177
  except Exception:
152
178
  return obj
153
179
 
154
- # как есть пробуем через serde
180
+ if isinstance(obj, str):
181
+ try:
182
+ return self.serde.loads(obj.encode())
183
+ except Exception:
184
+ payload = _b64decode_strict(obj)
185
+ if payload is not None:
186
+ try:
187
+ return self.serde.loads(payload)
188
+ except Exception:
189
+ pass
190
+ return obj
191
+
155
192
  try:
156
193
  return self.serde.loads(obj)
157
194
  except Exception:
158
195
  return obj
159
196
 
197
+ # def _safe_load(self, obj: Any) -> Any:
198
+ # """Обратная операция к _safe_dump."""
199
+ # if isinstance(obj, str):
200
+ # try:
201
+ # return self.serde.load(base64.b64decode(obj))
202
+ # except Exception:
203
+ # # не base64 — обычная строка
204
+ # return self.serde.load(obj)
205
+ # return self.serde.load(obj)
206
+
160
207
  # ----------------------- config <-> api --------------------------
161
208
  def _to_api_config(self, cfg: RunnableConfig | None) -> Dict[str, Any]:
162
209
  if not cfg:
@@ -174,47 +221,44 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
174
221
 
175
222
  # --------------------- checkpoint (de)ser ------------------------
176
223
  def _encode_cp(self, cp: Checkpoint) -> Dict[str, Any]:
177
- channel_values = {
178
- k: self._encode_typed(v) for k, v in cp.get("channel_values", {}).items()
179
- }
180
- pending = []
224
+ pending: list[Any] = []
181
225
  for item in cp.get("pending_sends", []) or []:
182
226
  try:
183
227
  channel, value = item
184
- pending.append({"channel": channel, **self._encode_typed(value)})
185
228
  except Exception:
229
+ pending.append(item)
186
230
  continue
231
+ pending.append([channel, self._safe_dump(value)])
187
232
  return {
188
233
  "v": cp["v"],
189
234
  "id": cp["id"],
190
235
  "ts": cp["ts"],
191
- "channelValues": channel_values,
236
+ "channelValues": {k: self._safe_dump(v) for k, v in cp["channel_values"].items()},
192
237
  "channelVersions": cp["channel_versions"],
193
238
  "versionsSeen": cp["versions_seen"],
194
239
  "pendingSends": pending,
195
240
  }
196
241
 
197
242
  def _decode_cp(self, raw: Dict[str, Any]) -> Checkpoint:
198
- cv_raw = raw.get("channelValues") or {}
199
- channel_values = {k: self._decode_typed(v) for k, v in cv_raw.items()}
200
- ps_raw = raw.get("pendingSends") or []
201
- pending_sends = []
202
- for obj in ps_raw:
203
- # ожидаем {channel, type, blob}
243
+ pending_sends: list[Tuple[str, Any]] = []
244
+ for obj in raw.get("pendingSends", []) or []:
204
245
  if isinstance(obj, dict) and "channel" in obj:
205
- ch = obj["channel"]
206
- typed = {k: obj[k] for k in obj.keys() if k in TYPED_KEYS}
207
- val = self._decode_typed(typed)
208
- pending_sends.append((ch, val))
209
- elif isinstance(obj, (list, tuple)) and len(obj) == 2:
210
- ch, val = obj
211
- pending_sends.append((ch, self._decode_typed(val)))
212
-
246
+ channel = obj["channel"]
247
+ value_payload: Any = obj.get("value")
248
+ if value_payload is None and all(k in obj for k in TYPED_KEYS):
249
+ value_payload = {k: obj[k] for k in TYPED_KEYS}
250
+ pending_sends.append((channel, self._safe_load(value_payload)))
251
+ elif isinstance(obj, (list, tuple)) and len(obj) >= 2:
252
+ channel = obj[0]
253
+ value_payload = obj[1]
254
+ pending_sends.append((channel, self._safe_load(value_payload)))
255
+ else:
256
+ pending_sends.append(obj) # сохраняем как есть, если формат неизвестен
213
257
  return Checkpoint(
214
258
  v=raw["v"],
215
259
  id=raw["id"],
216
260
  ts=raw["ts"],
217
- channel_values=channel_values,
261
+ channel_values={k: self._safe_load(v) for k, v in raw["channelValues"].items()},
218
262
  channel_versions=raw["channelVersions"],
219
263
  versions_seen=raw["versionsSeen"],
220
264
  pending_sends=pending_sends,
@@ -238,15 +282,22 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
238
282
  "thread_id": raw.get("threadId"),
239
283
  "thread_ts": raw.get("threadTs"),
240
284
  "checkpoint_ns": raw.get("checkpointNs"),
241
- "checkpoint_id": raw.get("checkpointId"),
285
+ "checkpoint_id": raw.get("checkpointId")
242
286
  }
243
287
 
244
- # metadata (de)ser — передаём как есть (JSON-совместимый словарь)
288
+ # metadata (de)ser
245
289
  def _enc_meta(self, md: CheckpointMetadata) -> CheckpointMetadata:
246
- return md or {}
290
+ if not md:
291
+ return {}
292
+ out: CheckpointMetadata = {}
293
+ for k, v in md.items():
294
+ out[k] = self._enc_meta(v) if isinstance(v, dict) else self._safe_dump(v) # type: ignore[assignment]
295
+ return out
247
296
 
248
297
  def _dec_meta(self, md: Any) -> Any:
249
- return md
298
+ if isinstance(md, dict):
299
+ return {k: self._dec_meta(v) for k, v in md.items()}
300
+ return self._safe_load(md)
250
301
 
251
302
  # ------------------------ HTTP wrapper ---------------------------
252
303
  async def _http(self, method: str, path: str, **kw) -> httpx.Response:
@@ -254,36 +305,39 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
254
305
  payload = kw.pop("json")
255
306
  kw["data"] = orjson.dumps(payload)
256
307
  logger.debug("AGW HTTP payload: %s", kw["data"].decode())
308
+
257
309
  return await self._client.request(method, path, **kw)
258
310
 
259
311
  # -------------------- api -> CheckpointTuple ----------------------
260
312
  def _to_tuple(self, node: Dict[str, Any]) -> CheckpointTuple:
261
- pending_writes = None
262
- raw_pw = node.get("pendingWrites")
263
- if raw_pw:
264
- decoded: list[tuple[str, str, Any]] = []
265
- for w in raw_pw:
266
- if isinstance(w, dict) and "first" in w and "second" in w:
267
- # ожидаем формат, который возвращает бек: first=task_id, second=channel, third=typed
268
- task_id = w["first"]
269
- channel = w["second"]
270
- tv = w.get("third")
271
- value = self._decode_typed(tv)
272
- decoded.append((task_id, channel, value))
313
+ pending = None
314
+ if node.get("pendingWrites"):
315
+ pending = []
316
+ for w in node["pendingWrites"]:
317
+ if isinstance(w, dict):
318
+ first = w.get("first")
319
+ second = w.get("second")
320
+ third = w.get("third")
321
+ if third is None and isinstance(second, dict) and all(
322
+ k in second for k in TYPED_KEYS
323
+ ):
324
+ third = second
325
+ pending.append((first, second, self._safe_load(third)))
273
326
  elif isinstance(w, (list, tuple)):
274
- try:
275
- first, channel, tv = w
276
- decoded.append((first, channel, self._decode_typed(tv)))
277
- except Exception: # pragma: no cover
327
+ if len(w) == 3:
328
+ first, second, third = w
329
+ elif len(w) == 2:
330
+ first, second = w
331
+ third = None
332
+ else:
278
333
  continue
279
- pending_writes = decoded
280
-
334
+ pending.append((first, second, self._safe_load(third)))
281
335
  return CheckpointTuple(
282
336
  config=self._decode_config(node.get("config")),
283
337
  checkpoint=self._decode_cp(node["checkpoint"]),
284
338
  metadata=self._dec_meta(node.get("metadata")),
285
339
  parent_config=self._decode_config(node.get("parentConfig")),
286
- pending_writes=pending_writes,
340
+ pending_writes=pending,
287
341
  )
288
342
 
289
343
  # =================================================================
@@ -292,7 +346,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
292
346
  async def aget_tuple(self, cfg: RunnableConfig) -> CheckpointTuple | None:
293
347
  cid = get_checkpoint_id(cfg)
294
348
  api_cfg = self._to_api_config(cfg)
295
- tid = api_cfg.get("threadId")
349
+ tid = api_cfg["threadId"]
296
350
 
297
351
  if cid:
298
352
  path = f"/checkpoint/{tid}/{cid}"
@@ -304,7 +358,9 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
304
358
  resp = await self._http("GET", path, params=params)
305
359
  logger.debug("AGW aget_tuple response: %s", resp.text)
306
360
 
307
- if not resp.text or resp.status_code in (404, 406):
361
+ if not resp.text:
362
+ return None
363
+ if resp.status_code in (404, 406):
308
364
  return None
309
365
  resp.raise_for_status()
310
366
  return self._to_tuple(resp.json())
@@ -339,7 +395,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
339
395
  payload = {
340
396
  "config": self._to_api_config(cfg),
341
397
  "checkpoint": self._encode_cp(cp),
342
- "metadata": get_checkpoint_metadata(cfg, metadata),
398
+ "metadata": self._enc_meta(get_checkpoint_metadata(cfg, metadata)),
343
399
  "newVersions": new_versions,
344
400
  }
345
401
  resp = await self._http("POST", "/checkpoint", json=payload)
@@ -354,7 +410,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
354
410
  task_id: str,
355
411
  task_path: str = "",
356
412
  ) -> None:
357
- enc = [{"first": ch, "second": self._encode_typed(v)} for ch, v in writes]
413
+ enc = [{"first": ch, "second": self._safe_dump(v)} for ch, v in writes]
358
414
  payload = {
359
415
  "config": self._to_api_config(cfg),
360
416
  "writes": enc,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-lab-sdk
3
- Version: 0.1.35.dev1
3
+ Version: 0.1.37
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=QeYUAGEldw9SNXup3FRI7gNcGXYQDecKhoF1NyXa7yQ,16365
3
+ agent_lab_sdk/langgraph/checkpoint/agw_saver.py,sha256=K07scbnVV_PT2zbn0kH2lk-Jl36ZVQDng_Nl16sR8Nc,18144
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
@@ -14,8 +14,8 @@ agent_lab_sdk/schema/input_types.py,sha256=e75nRW7Dz_RHk5Yia8DkFfbqMafsLQsQrJPfz
14
14
  agent_lab_sdk/schema/log_message.py,sha256=nadi6lZGRuDSPmfbYs9QPpRJUT9Pfy8Y7pGCvyFF5Mw,638
15
15
  agent_lab_sdk/storage/__init__.py,sha256=ik1_v1DMTwehvcAEXIYxuvLuCjJCa3y5qAuJqoQpuSA,81
16
16
  agent_lab_sdk/storage/storage.py,sha256=ELpt7GRwFD-aWa6ctinfA_QwcvzWLvKS0Wz8FlxVqAs,2075
17
- agent_lab_sdk-0.1.35.dev1.dist-info/licenses/LICENSE,sha256=_TRXHkF3S9ilWBPdZcHLI_S-PRjK0L_SeOb2pcPAdV4,417
18
- agent_lab_sdk-0.1.35.dev1.dist-info/METADATA,sha256=MsteJ1L7FktEWd45-MkhTlgxWHevOduYeL1prnNTjJY,17916
19
- agent_lab_sdk-0.1.35.dev1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
- agent_lab_sdk-0.1.35.dev1.dist-info/top_level.txt,sha256=E1efqkJ89KNmPBWdLzdMHeVtH0dYyCo4fhnSb81_15I,14
21
- agent_lab_sdk-0.1.35.dev1.dist-info/RECORD,,
17
+ agent_lab_sdk-0.1.37.dist-info/licenses/LICENSE,sha256=_TRXHkF3S9ilWBPdZcHLI_S-PRjK0L_SeOb2pcPAdV4,417
18
+ agent_lab_sdk-0.1.37.dist-info/METADATA,sha256=XTRYyQQVXpDXxU4BkZvuAnsbZJsr-S5RuPKiDP0WKTc,17911
19
+ agent_lab_sdk-0.1.37.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
20
+ agent_lab_sdk-0.1.37.dist-info/top_level.txt,sha256=E1efqkJ89KNmPBWdLzdMHeVtH0dYyCo4fhnSb81_15I,14
21
+ agent_lab_sdk-0.1.37.dist-info/RECORD,,