agent-lab-sdk 0.1.37__tar.gz → 0.1.39__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.

Files changed (26) hide show
  1. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/PKG-INFO +1 -1
  2. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/langgraph/checkpoint/agw_saver.py +218 -12
  3. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/llm/llm.py +4 -1
  4. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk.egg-info/PKG-INFO +1 -1
  5. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/pyproject.toml +1 -1
  6. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/LICENSE +0 -0
  7. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/README.md +0 -0
  8. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/__init__.py +0 -0
  9. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/langgraph/checkpoint/__init__.py +0 -0
  10. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/langgraph/checkpoint/serde.py +0 -0
  11. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/llm/__init__.py +0 -0
  12. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/llm/agw_token_manager.py +0 -0
  13. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/llm/gigachat_token_manager.py +0 -0
  14. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/llm/throttled.py +0 -0
  15. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/metrics/__init__.py +0 -0
  16. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/metrics/metrics.py +0 -0
  17. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/schema/__init__.py +0 -0
  18. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/schema/input_types.py +0 -0
  19. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/schema/log_message.py +0 -0
  20. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/storage/__init__.py +0 -0
  21. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/storage/storage.py +0 -0
  22. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk.egg-info/SOURCES.txt +0 -0
  23. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk.egg-info/dependency_links.txt +0 -0
  24. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk.egg-info/requires.txt +0 -0
  25. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk.egg-info/top_level.txt +0 -0
  26. {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-lab-sdk
3
- Version: 0.1.37
3
+ Version: 0.1.39
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,14 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
- import orjson
4
- from random import random
5
- from langgraph.checkpoint.serde.types import ChannelProtocol
6
3
  import asyncio
7
4
  import base64
8
5
  import logging
9
6
  import os
10
7
  from contextlib import asynccontextmanager
11
- from typing import Any, AsyncIterator, Dict, Iterator, Optional, Sequence, Tuple
8
+ from random import random
9
+ from typing import Any, AsyncIterator, Dict, Iterable, Iterator, Optional, Sequence, Tuple
10
+
11
+ import orjson
12
+ from langgraph.checkpoint.serde.types import ChannelProtocol
12
13
 
13
14
  import httpx
14
15
  from langchain_core.runnables import RunnableConfig
@@ -26,11 +27,32 @@ from langgraph.checkpoint.serde.base import SerializerProtocol
26
27
  from langgraph.checkpoint.serde.encrypted import EncryptedSerializer
27
28
 
28
29
  from .serde import Serializer
30
+ from agent_lab_sdk.metrics import get_metric
29
31
 
30
32
  __all__ = ["AsyncAGWCheckpointSaver"]
31
33
 
32
34
  logger = logging.getLogger(__name__)
33
35
 
36
+ AGW_METRIC_LABELS = ["method", "endpoint"]
37
+ AGW_HTTP_SUCCESS = get_metric(
38
+ "counter",
39
+ "agw_http_success_total",
40
+ "Number of successful AGW HTTP requests",
41
+ labelnames=AGW_METRIC_LABELS,
42
+ )
43
+ AGW_HTTP_ERROR = get_metric(
44
+ "counter",
45
+ "agw_http_error_total",
46
+ "Number of failed AGW HTTP request attempts",
47
+ labelnames=AGW_METRIC_LABELS,
48
+ )
49
+ AGW_HTTP_FINAL_ERROR = get_metric(
50
+ "counter",
51
+ "agw_http_final_error_total",
52
+ "Number of AGW HTTP requests that failed after retries",
53
+ labelnames=AGW_METRIC_LABELS,
54
+ )
55
+
34
56
  TYPED_KEYS = ("type", "blob")
35
57
 
36
58
 
@@ -82,6 +104,66 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
82
104
  self.timeout = timeout
83
105
  self.loop = asyncio.get_running_loop()
84
106
 
107
+ raw_attempts = os.getenv("AGW_HTTP_MAX_RETRIES")
108
+ if raw_attempts is None:
109
+ self.retry_max_attempts = 3
110
+ else:
111
+ try:
112
+ self.retry_max_attempts = max(int(raw_attempts), 1)
113
+ except ValueError:
114
+ logger.warning(
115
+ "Env %s expected int, got %r; using default %s",
116
+ "AGW_HTTP_MAX_RETRIES",
117
+ raw_attempts,
118
+ 3,
119
+ )
120
+ self.retry_max_attempts = 3
121
+
122
+ raw_backoff_base = os.getenv("AGW_HTTP_RETRY_BACKOFF_BASE")
123
+ if raw_backoff_base is None:
124
+ self.retry_backoff_base = 0.5
125
+ else:
126
+ try:
127
+ self.retry_backoff_base = max(float(raw_backoff_base), 0.0)
128
+ except ValueError:
129
+ logger.warning(
130
+ "Env %s expected float, got %r; using default %.3f",
131
+ "AGW_HTTP_RETRY_BACKOFF_BASE",
132
+ raw_backoff_base,
133
+ 0.5,
134
+ )
135
+ self.retry_backoff_base = 0.5
136
+
137
+ raw_backoff_max = os.getenv("AGW_HTTP_RETRY_BACKOFF_MAX")
138
+ if raw_backoff_max is None:
139
+ self.retry_backoff_max = 5.0
140
+ else:
141
+ try:
142
+ self.retry_backoff_max = max(float(raw_backoff_max), 0.0)
143
+ except ValueError:
144
+ logger.warning(
145
+ "Env %s expected float, got %r; using default %.3f",
146
+ "AGW_HTTP_RETRY_BACKOFF_MAX",
147
+ raw_backoff_max,
148
+ 5.0,
149
+ )
150
+ self.retry_backoff_max = 5.0
151
+
152
+ raw_jitter = os.getenv("AGW_HTTP_RETRY_JITTER")
153
+ if raw_jitter is None:
154
+ self.retry_jitter = 0.25
155
+ else:
156
+ try:
157
+ self.retry_jitter = max(float(raw_jitter), 0.0)
158
+ except ValueError:
159
+ logger.warning(
160
+ "Env %s expected float, got %r; using default %.3f",
161
+ "AGW_HTTP_RETRY_JITTER",
162
+ raw_jitter,
163
+ 0.25,
164
+ )
165
+ self.retry_jitter = 0.25
166
+
85
167
  self.headers: Dict[str, str] = {
86
168
  "Accept": "application/json",
87
169
  "Content-Type": "application/json",
@@ -91,19 +173,51 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
91
173
  if api_key:
92
174
  self.headers["Authorization"] = f"Bearer {api_key}"
93
175
 
94
- self._client = httpx.AsyncClient(
176
+ self._verify = verify
177
+ self._client: httpx.AsyncClient | None = None
178
+
179
+ def _create_client(self) -> httpx.AsyncClient:
180
+ return httpx.AsyncClient(
95
181
  base_url=self.base_url,
96
182
  headers=self.headers,
97
183
  timeout=self.timeout,
98
- verify=verify,
99
- trust_env=True
184
+ verify=self._verify,
185
+ trust_env=True,
100
186
  )
101
187
 
188
+ def _ensure_client(self) -> httpx.AsyncClient:
189
+ client = self._client
190
+ if client is None or client.is_closed:
191
+ if client is not None and client.is_closed:
192
+ logger.debug("Recreating closed httpx.AsyncClient for AGW")
193
+ client = self._create_client()
194
+ self._client = client
195
+ return client
196
+
197
+ def _compute_retry_delay(self, attempt: int) -> float:
198
+ if attempt <= 0:
199
+ attempt = 1
200
+ if self.retry_backoff_base <= 0:
201
+ delay = 0.0
202
+ else:
203
+ delay = self.retry_backoff_base * (2 ** (attempt - 1))
204
+ if self.retry_backoff_max > 0:
205
+ delay = min(delay, self.retry_backoff_max)
206
+ if self.retry_jitter > 0:
207
+ delay += self.retry_jitter * random()
208
+ return delay
209
+
102
210
  async def __aenter__(self): # noqa: D401
103
211
  return self
104
212
 
105
213
  async def __aexit__(self, exc_type, exc, tb): # noqa: D401
106
- await self._client.aclose()
214
+ if self._client is not None:
215
+ try:
216
+ await self._client.aclose()
217
+ except Exception as close_exc: # pragma: no cover - best effort
218
+ logger.debug("Failed to close AGW httpx.AsyncClient: %s", close_exc)
219
+ finally:
220
+ self._client = None
107
221
 
108
222
  # ----------------------- universal dump/load ---------------------
109
223
  # def _safe_dump(self, obj: Any) -> Any:
@@ -300,13 +414,99 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
300
414
  return self._safe_load(md)
301
415
 
302
416
  # ------------------------ HTTP wrapper ---------------------------
303
- async def _http(self, method: str, path: str, **kw) -> httpx.Response:
417
+ async def _http(
418
+ self,
419
+ method: str,
420
+ path: str,
421
+ *,
422
+ ok_statuses: Iterable[int] | None = None,
423
+ **kw,
424
+ ) -> httpx.Response:
304
425
  if "json" in kw:
305
426
  payload = kw.pop("json")
306
427
  kw["data"] = orjson.dumps(payload)
307
428
  logger.debug("AGW HTTP payload: %s", kw["data"].decode())
308
-
309
- return await self._client.request(method, path, **kw)
429
+
430
+ ok_set = set(ok_statuses) if ok_statuses is not None else set()
431
+
432
+ attempt = 1
433
+ while True:
434
+ client = self._ensure_client()
435
+ try:
436
+ resp = await client.request(method, path, **kw)
437
+ except httpx.RequestError as exc:
438
+ AGW_HTTP_ERROR.labels(method, path).inc()
439
+ logger.warning(
440
+ "AGW request %s %s failed on attempt %d/%d: %s",
441
+ method,
442
+ path,
443
+ attempt,
444
+ self.retry_max_attempts,
445
+ exc,
446
+ )
447
+ if attempt >= self.retry_max_attempts:
448
+ AGW_HTTP_FINAL_ERROR.labels(method, path).inc()
449
+ if self._client is not None:
450
+ try:
451
+ await self._client.aclose()
452
+ except Exception as close_exc: # pragma: no cover
453
+ logger.debug(
454
+ "Failed to close AGW httpx.AsyncClient: %s",
455
+ close_exc,
456
+ )
457
+ finally:
458
+ self._client = None
459
+ raise
460
+
461
+ if self._client is not None:
462
+ try:
463
+ await self._client.aclose()
464
+ except Exception as close_exc: # pragma: no cover
465
+ logger.debug(
466
+ "Failed to close AGW httpx.AsyncClient: %s",
467
+ close_exc,
468
+ )
469
+ finally:
470
+ self._client = None
471
+ delay = self._compute_retry_delay(attempt)
472
+ if delay > 0:
473
+ await asyncio.sleep(delay)
474
+ attempt += 1
475
+ continue
476
+
477
+ status = resp.status_code
478
+ if status < 400 or status in ok_set:
479
+ AGW_HTTP_SUCCESS.labels(method, path).inc()
480
+ return resp
481
+
482
+ AGW_HTTP_ERROR.labels(method, path).inc()
483
+ if status in (404, 406):
484
+ AGW_HTTP_FINAL_ERROR.labels(method, path).inc()
485
+ return resp
486
+
487
+ if attempt >= self.retry_max_attempts:
488
+ AGW_HTTP_FINAL_ERROR.labels(method, path).inc()
489
+ return resp
490
+
491
+ try:
492
+ await resp.aclose()
493
+ except Exception as exc: # pragma: no cover - best effort
494
+ logger.debug("Failed to close AGW httpx.Response before retry: %s", exc)
495
+
496
+ if self._client is not None:
497
+ try:
498
+ await self._client.aclose()
499
+ except Exception as close_exc: # pragma: no cover
500
+ logger.debug(
501
+ "Failed to close AGW httpx.AsyncClient: %s",
502
+ close_exc,
503
+ )
504
+ finally:
505
+ self._client = None
506
+ delay = self._compute_retry_delay(attempt)
507
+ if delay > 0:
508
+ await asyncio.sleep(delay)
509
+ attempt += 1
310
510
 
311
511
  # -------------------- api -> CheckpointTuple ----------------------
312
512
  def _to_tuple(self, node: Dict[str, Any]) -> CheckpointTuple:
@@ -497,4 +697,10 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
497
697
  try:
498
698
  yield saver
499
699
  finally:
500
- await saver._client.aclose()
700
+ if saver._client is not None:
701
+ try:
702
+ await saver._client.aclose()
703
+ except Exception as close_exc: # pragma: no cover - best effort
704
+ logger.debug("Failed to close AGW httpx.AsyncClient: %s", close_exc)
705
+ finally:
706
+ saver._client = None
@@ -38,4 +38,7 @@ def get_model(
38
38
  # Включаем режим авто-обновления токена в обёртках
39
39
  kwargs["manage_access_token"] = True
40
40
 
41
- return _class(verify_ssl_certs=verify_ssl_certs, **kwargs)
41
+ if access_token:
42
+ kwargs["access_token"] = access_token
43
+
44
+ return _class(verify_ssl_certs=verify_ssl_certs, **kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-lab-sdk
3
- Version: 0.1.37
3
+ Version: 0.1.39
4
4
  Summary: SDK для работы с Agent Lab
5
5
  Author-email: Andrew Ohurtsov <andermirik@yandex.com>
6
6
  License: Proprietary and Confidential — All Rights Reserved
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agent-lab-sdk"
7
- version = "0.1.37"
7
+ version = "0.1.39"
8
8
  description = "SDK для работы с Agent Lab"
9
9
  readme = "README.md"
10
10
  license = { text = "Proprietary and Confidential — All Rights Reserved" }
File without changes
File without changes
File without changes