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.
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/PKG-INFO +1 -1
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/langgraph/checkpoint/agw_saver.py +218 -12
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/llm/llm.py +4 -1
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk.egg-info/PKG-INFO +1 -1
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/pyproject.toml +1 -1
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/LICENSE +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/README.md +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/__init__.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/langgraph/checkpoint/__init__.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/langgraph/checkpoint/serde.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/llm/__init__.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/llm/agw_token_manager.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/llm/gigachat_token_manager.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/llm/throttled.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/metrics/__init__.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/metrics/metrics.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/schema/__init__.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/schema/input_types.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/schema/log_message.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/storage/__init__.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/storage/storage.py +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk.egg-info/SOURCES.txt +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk.egg-info/dependency_links.txt +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk.egg-info/requires.txt +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk.egg-info/top_level.txt +0 -0
- {agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/setup.cfg +0 -0
{agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/agent_lab_sdk/langgraph/checkpoint/agw_saver.py
RENAMED
|
@@ -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
|
|
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.
|
|
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=
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
41
|
+
if access_token:
|
|
42
|
+
kwargs["access_token"] = access_token
|
|
43
|
+
|
|
44
|
+
return _class(verify_ssl_certs=verify_ssl_certs, **kwargs)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_lab_sdk-0.1.37 → agent_lab_sdk-0.1.39}/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
|