agent-lab-sdk 0.1.42.1__tar.gz → 0.1.44__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 (27) hide show
  1. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/PKG-INFO +1 -1
  2. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/langgraph/checkpoint/agw_saver.py +87 -28
  3. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk.egg-info/PKG-INFO +1 -1
  4. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/pyproject.toml +1 -1
  5. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/LICENSE +0 -0
  6. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/README.md +0 -0
  7. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/__init__.py +0 -0
  8. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/langgraph/checkpoint/__init__.py +0 -0
  9. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/langgraph/checkpoint/serde.py +0 -0
  10. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/llm/__init__.py +0 -0
  11. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/llm/agw_token_manager.py +0 -0
  12. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/llm/gigachat_token_manager.py +0 -0
  13. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/llm/llm.py +0 -0
  14. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/llm/throttled.py +0 -0
  15. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/metrics/__init__.py +0 -0
  16. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/metrics/metrics.py +0 -0
  17. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/schema/__init__.py +0 -0
  18. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/schema/input_types.py +0 -0
  19. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/schema/log_message.py +0 -0
  20. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/storage/__init__.py +0 -0
  21. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/storage/storage.py +0 -0
  22. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk/storage/storage_v2.py +0 -0
  23. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk.egg-info/SOURCES.txt +0 -0
  24. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk.egg-info/dependency_links.txt +0 -0
  25. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk.egg-info/requires.txt +0 -0
  26. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/agent_lab_sdk.egg-info/top_level.txt +0 -0
  27. {agent_lab_sdk-0.1.42.1 → agent_lab_sdk-0.1.44}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-lab-sdk
3
- Version: 0.1.42.1
3
+ Version: 0.1.44
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,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import threading
4
5
  import base64
5
6
  import logging
6
7
  import os
@@ -102,7 +103,10 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
102
103
  super().__init__(serde=serde)
103
104
  self.base_url = base_url.rstrip("/")
104
105
  self.timeout = timeout
105
- self.loop = asyncio.get_running_loop()
106
+ # Фоновый loop для sync-обёрток
107
+ self._bg_loop: asyncio.AbstractEventLoop | None = None
108
+ self._bg_thread: threading.Thread | None = None
109
+ self._loop_lock = threading.Lock()
106
110
 
107
111
  raw_attempts = os.getenv("AGW_HTTP_MAX_RETRIES")
108
112
  if raw_attempts is None:
@@ -175,6 +179,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
175
179
 
176
180
  self._verify = verify
177
181
  self._client: httpx.AsyncClient | None = None
182
+ self._client_loop: asyncio.AbstractEventLoop | None = None
178
183
 
179
184
  def _create_client(self) -> httpx.AsyncClient:
180
185
  return httpx.AsyncClient(
@@ -185,14 +190,23 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
185
190
  trust_env=True,
186
191
  )
187
192
 
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
193
+ def _ensure_bg_loop(self) -> asyncio.AbstractEventLoop:
194
+ with self._loop_lock:
195
+ if self._bg_loop and self._bg_loop.is_running():
196
+ return self._bg_loop
197
+
198
+ loop = asyncio.new_event_loop()
199
+
200
+ def runner():
201
+ asyncio.set_event_loop(loop)
202
+ loop.run_forever()
203
+
204
+ t = threading.Thread(target=runner, name="agw-checkpoint-loop", daemon=True)
205
+ t.start()
206
+
207
+ self._bg_loop = loop
208
+ self._bg_thread = t
209
+ return loop
196
210
 
197
211
  def _compute_retry_delay(self, attempt: int) -> float:
198
212
  if attempt <= 0:
@@ -218,6 +232,14 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
218
232
  logger.debug("Failed to close AGW httpx.AsyncClient: %s", close_exc)
219
233
  finally:
220
234
  self._client = None
235
+ self._client_loop = None
236
+ # останавливаем фоновый loop, если поднимали
237
+ if self._bg_loop is not None:
238
+ try:
239
+ self._bg_loop.call_soon_threadsafe(self._bg_loop.stop)
240
+ finally:
241
+ self._bg_loop = None
242
+ self._bg_thread = None
221
243
 
222
244
  # ----------------------- universal dump/load ---------------------
223
245
  def _safe_dump(self, obj: Any) -> Any:
@@ -528,6 +550,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
528
550
  path: str,
529
551
  *,
530
552
  ok_statuses: Iterable[int] | None = None,
553
+ label_path: str | None = None,
531
554
  **kw,
532
555
  ) -> httpx.Response:
533
556
  if "json" in kw:
@@ -536,14 +559,26 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
536
559
  logger.debug("AGW HTTP payload: %s", kw["data"].decode())
537
560
 
538
561
  ok_set = set(ok_statuses) if ok_statuses is not None else set()
562
+ metric_path = label_path or path
539
563
 
540
564
  attempt = 1
541
565
  while True:
542
- client = self._ensure_client()
566
+ # клиент должен принадлежать текущему loop
567
+ current_loop = asyncio.get_running_loop()
568
+ client = self._client
569
+ if client is None or client.is_closed or self._client_loop is not current_loop:
570
+ if client is not None:
571
+ try:
572
+ await client.aclose()
573
+ except Exception as e:
574
+ logger.exception("ошибка при закрытии клиента", e)
575
+ client = self._create_client()
576
+ self._client = client
577
+ self._client_loop = current_loop
543
578
  try:
544
579
  resp = await client.request(method, path, **kw)
545
580
  except httpx.RequestError as exc:
546
- AGW_HTTP_ERROR.labels(method, path).inc()
581
+ AGW_HTTP_ERROR.labels(method, metric_path).inc()
547
582
  logger.warning(
548
583
  "AGW request %s %s failed on attempt %d/%d: %s",
549
584
  method,
@@ -553,7 +588,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
553
588
  exc,
554
589
  )
555
590
  if attempt >= self.retry_max_attempts:
556
- AGW_HTTP_FINAL_ERROR.labels(method, path).inc()
591
+ AGW_HTTP_FINAL_ERROR.labels(method, metric_path).inc()
557
592
  if self._client is not None:
558
593
  try:
559
594
  await self._client.aclose()
@@ -564,6 +599,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
564
599
  )
565
600
  finally:
566
601
  self._client = None
602
+ self._client_loop = None
567
603
  raise
568
604
 
569
605
  if self._client is not None:
@@ -576,6 +612,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
576
612
  )
577
613
  finally:
578
614
  self._client = None
615
+ self._client_loop = None
579
616
  delay = self._compute_retry_delay(attempt)
580
617
  if delay > 0:
581
618
  await asyncio.sleep(delay)
@@ -584,16 +621,16 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
584
621
 
585
622
  status = resp.status_code
586
623
  if status < 400 or status in ok_set:
587
- AGW_HTTP_SUCCESS.labels(method, path).inc()
624
+ AGW_HTTP_SUCCESS.labels(method, metric_path).inc()
588
625
  return resp
589
626
 
590
- AGW_HTTP_ERROR.labels(method, path).inc()
627
+ AGW_HTTP_ERROR.labels(method, metric_path).inc()
591
628
  if status in (404, 406):
592
- AGW_HTTP_FINAL_ERROR.labels(method, path).inc()
629
+ AGW_HTTP_FINAL_ERROR.labels(method, metric_path).inc()
593
630
  return resp
594
631
 
595
632
  if attempt >= self.retry_max_attempts:
596
- AGW_HTTP_FINAL_ERROR.labels(method, path).inc()
633
+ AGW_HTTP_FINAL_ERROR.labels(method, metric_path).inc()
597
634
  return resp
598
635
 
599
636
  try:
@@ -611,6 +648,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
611
648
  )
612
649
  finally:
613
650
  self._client = None
651
+ self._client_loop = None
614
652
  delay = self._compute_retry_delay(attempt)
615
653
  if delay > 0:
616
654
  await asyncio.sleep(delay)
@@ -657,13 +695,15 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
657
695
  tid = api_cfg["threadId"]
658
696
 
659
697
  if cid:
698
+ path_template = "/checkpoint/{threadId}/{checkpointId}"
660
699
  path = f"/checkpoint/{tid}/{cid}"
661
700
  params = {"checkpointNs": api_cfg.get("checkpointNs", "")}
662
701
  else:
702
+ path_template = "/checkpoint/{threadId}"
663
703
  path = f"/checkpoint/{tid}"
664
704
  params = None
665
705
 
666
- resp = await self._http("GET", path, params=params)
706
+ resp = await self._http("GET", path, params=params, label_path=path_template)
667
707
  logger.debug("AGW aget_tuple response: %s", resp.text)
668
708
 
669
709
  if not resp.text:
@@ -687,7 +727,12 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
687
727
  "before": self._to_api_config(before) if before else None,
688
728
  "limit": limit,
689
729
  }
690
- resp = await self._http("POST", "/checkpoint/list", json=payload)
730
+ resp = await self._http(
731
+ "POST",
732
+ "/checkpoint/list",
733
+ json=payload,
734
+ label_path="/checkpoint/list",
735
+ )
691
736
  logger.debug("AGW alist response: %s", resp.text)
692
737
  resp.raise_for_status()
693
738
  for item in resp.json():
@@ -706,7 +751,7 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
706
751
  "metadata": self._enc_meta(get_checkpoint_metadata(cfg, metadata)),
707
752
  "newVersions": new_versions,
708
753
  }
709
- resp = await self._http("POST", "/checkpoint", json=payload)
754
+ resp = await self._http("POST", "/checkpoint", json=payload, label_path="/checkpoint")
710
755
  logger.debug("AGW aput response: %s", resp.text)
711
756
  resp.raise_for_status()
712
757
  return resp.json()["config"]
@@ -725,19 +770,31 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
725
770
  "taskId": task_id,
726
771
  "taskPath": task_path,
727
772
  }
728
- resp = await self._http("POST", "/checkpoint/writes", json=payload)
773
+ resp = await self._http(
774
+ "POST",
775
+ "/checkpoint/writes",
776
+ json=payload,
777
+ label_path="/checkpoint/writes",
778
+ )
729
779
  logger.debug("AGW aput_writes response: %s", resp.text)
730
780
  resp.raise_for_status()
731
781
 
732
782
  async def adelete_thread(self, thread_id: str) -> None:
733
- resp = await self._http("DELETE", f"/checkpoint/{thread_id}")
783
+ resp = await self._http(
784
+ "DELETE",
785
+ f"/checkpoint/{thread_id}",
786
+ label_path="/checkpoint/{threadId}",
787
+ )
734
788
  resp.raise_for_status()
735
789
 
736
790
  # =================================================================
737
791
  # sync-обёртки
738
792
  # =================================================================
739
793
  def _run(self, coro):
740
- return asyncio.run_coroutine_threadsafe(coro, self.loop).result()
794
+ # sync-обёртки всегда выполняем в собственном loop в отдельном потоке
795
+ loop = self._ensure_bg_loop()
796
+ fut = asyncio.run_coroutine_threadsafe(coro, loop)
797
+ return fut.result()
741
798
 
742
799
  def list(
743
800
  self,
@@ -747,12 +804,14 @@ class AsyncAGWCheckpointSaver(BaseCheckpointSaver):
747
804
  before: RunnableConfig | None = None,
748
805
  limit: int | None = None,
749
806
  ) -> Iterator[CheckpointTuple]:
750
- aiter_ = self.alist(cfg, filter=filter, before=before, limit=limit)
751
- while True:
752
- try:
753
- yield self._run(anext(aiter_))
754
- except StopAsyncIteration:
755
- break
807
+ async def _collect():
808
+ out = []
809
+ async for item in self.alist(cfg, filter=filter, before=before, limit=limit):
810
+ out.append(item)
811
+ return out
812
+
813
+ for item in self._run(_collect()):
814
+ yield item
756
815
 
757
816
  def get_tuple(self, cfg: RunnableConfig) -> CheckpointTuple | None:
758
817
  return self._run(self.aget_tuple(cfg))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-lab-sdk
3
- Version: 0.1.42.1
3
+ Version: 0.1.44
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.42.1"
7
+ version = "0.1.44"
8
8
  description = "SDK для работы с Agent Lab"
9
9
  readme = "README.md"
10
10
  license = { text = "Proprietary and Confidential — All Rights Reserved" }
File without changes