langgraph-api 0.2.130__py3-none-any.whl → 0.2.134__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.
Files changed (51) hide show
  1. langgraph_api/__init__.py +1 -1
  2. langgraph_api/api/assistants.py +32 -6
  3. langgraph_api/api/meta.py +3 -1
  4. langgraph_api/api/openapi.py +1 -1
  5. langgraph_api/api/runs.py +50 -10
  6. langgraph_api/api/threads.py +27 -1
  7. langgraph_api/api/ui.py +2 -0
  8. langgraph_api/asgi_transport.py +2 -2
  9. langgraph_api/asyncio.py +10 -8
  10. langgraph_api/auth/custom.py +9 -4
  11. langgraph_api/auth/langsmith/client.py +1 -1
  12. langgraph_api/cli.py +5 -4
  13. langgraph_api/config.py +1 -1
  14. langgraph_api/executor_entrypoint.py +23 -0
  15. langgraph_api/graph.py +25 -9
  16. langgraph_api/http.py +10 -7
  17. langgraph_api/http_metrics.py +4 -1
  18. langgraph_api/js/build.mts +11 -2
  19. langgraph_api/js/client.http.mts +2 -0
  20. langgraph_api/js/client.mts +13 -3
  21. langgraph_api/js/package.json +2 -2
  22. langgraph_api/js/remote.py +17 -12
  23. langgraph_api/js/src/preload.mjs +9 -1
  24. langgraph_api/js/src/utils/files.mts +5 -2
  25. langgraph_api/js/sse.py +1 -1
  26. langgraph_api/js/yarn.lock +9 -9
  27. langgraph_api/logging.py +3 -3
  28. langgraph_api/middleware/http_logger.py +2 -1
  29. langgraph_api/models/run.py +19 -14
  30. langgraph_api/patch.py +2 -2
  31. langgraph_api/queue_entrypoint.py +33 -18
  32. langgraph_api/schema.py +88 -4
  33. langgraph_api/serde.py +32 -5
  34. langgraph_api/server.py +5 -3
  35. langgraph_api/state.py +8 -8
  36. langgraph_api/store.py +1 -1
  37. langgraph_api/stream.py +33 -20
  38. langgraph_api/traceblock.py +1 -1
  39. langgraph_api/utils/__init__.py +40 -5
  40. langgraph_api/utils/config.py +13 -4
  41. langgraph_api/utils/future.py +1 -1
  42. langgraph_api/utils/uuids.py +87 -0
  43. langgraph_api/validation.py +9 -0
  44. langgraph_api/webhook.py +20 -20
  45. langgraph_api/worker.py +8 -5
  46. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/METADATA +2 -2
  47. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/RECORD +51 -49
  48. openapi.json +331 -1
  49. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/WHEEL +0 -0
  50. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/entry_points.txt +0 -0
  51. {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/licenses/LICENSE +0 -0
langgraph_api/schema.py CHANGED
@@ -1,8 +1,9 @@
1
1
  from collections.abc import Sequence
2
2
  from datetime import datetime
3
- from typing import Any, Literal, Optional, TypeAlias
3
+ from typing import Any, Literal, NotRequired, Optional, TypeAlias
4
4
  from uuid import UUID
5
5
 
6
+ from langchain_core.runnables.config import RunnableConfig
6
7
  from typing_extensions import TypedDict
7
8
 
8
9
  from langgraph_api.serde import Fragment
@@ -120,6 +121,8 @@ class Thread(TypedDict):
120
121
  """The thread metadata."""
121
122
  config: Fragment
122
123
  """The thread config."""
124
+ context: Fragment
125
+ """The thread context."""
123
126
  status: ThreadStatus
124
127
  """The status of the thread. One of 'idle', 'busy', 'interrupted', "error"."""
125
128
  values: Fragment
@@ -157,6 +160,22 @@ class ThreadState(TypedDict):
157
160
  """The interrupts for this state."""
158
161
 
159
162
 
163
+ class RunKwargs(TypedDict):
164
+ config: RunnableConfig
165
+ context: dict[str, Any]
166
+ input: dict[str, Any] | None
167
+ command: dict[str, Any] | None
168
+ stream_mode: StreamMode
169
+ interrupt_before: Sequence[str] | str | None
170
+ interrupt_after: Sequence[str] | str | None
171
+ webhook: str | None
172
+ feedback_keys: Sequence[str] | None
173
+ temporary: bool
174
+ subgraphs: bool
175
+ resumable: bool
176
+ checkpoint_during: bool
177
+
178
+
160
179
  class Run(TypedDict):
161
180
  run_id: UUID
162
181
  """The ID of the run."""
@@ -172,7 +191,7 @@ class Run(TypedDict):
172
191
  """The status of the run. One of 'pending', 'error', 'success'."""
173
192
  metadata: Fragment
174
193
  """The run metadata."""
175
- kwargs: Fragment
194
+ kwargs: RunKwargs
176
195
  """The run kwargs."""
177
196
  multitask_strategy: MultitaskStrategy
178
197
  """Strategy to handle concurrent runs on the same thread."""
@@ -206,14 +225,16 @@ class Cron(TypedDict):
206
225
  """The time the cron was created."""
207
226
  updated_at: datetime
208
227
  """The last time the cron was updated."""
209
- user_id: UUID | None
210
- """The ID of the user."""
228
+ user_id: str | None
229
+ """The ID of the user (string identity)."""
211
230
  payload: Fragment
212
231
  """The run payload to use for creating new run."""
213
232
  next_run_date: datetime
214
233
  """The next run date of the cron."""
215
234
  metadata: Fragment
216
235
  """The cron metadata."""
236
+ now: NotRequired[datetime]
237
+ """The current time (present in internal next() only)."""
217
238
 
218
239
 
219
240
  class ThreadUpdateResponse(TypedDict):
@@ -227,3 +248,66 @@ class QueueStats(TypedDict):
227
248
  n_running: int
228
249
  max_age_secs: datetime | None
229
250
  med_age_secs: datetime | None
251
+
252
+
253
+ # Canonical field sets for select= validation and type aliases for ops
254
+
255
+ # Assistant select fields (intentionally excludes 'context')
256
+ AssistantSelectField = Literal[
257
+ "assistant_id",
258
+ "graph_id",
259
+ "name",
260
+ "description",
261
+ "config",
262
+ "context",
263
+ "created_at",
264
+ "updated_at",
265
+ "metadata",
266
+ "version",
267
+ ]
268
+ ASSISTANT_FIELDS: set[str] = set(AssistantSelectField.__args__) # type: ignore[attr-defined]
269
+
270
+ # Thread select fields
271
+ ThreadSelectField = Literal[
272
+ "thread_id",
273
+ "created_at",
274
+ "updated_at",
275
+ "metadata",
276
+ "config",
277
+ "context",
278
+ "status",
279
+ "values",
280
+ "interrupts",
281
+ ]
282
+ THREAD_FIELDS: set[str] = set(ThreadSelectField.__args__) # type: ignore[attr-defined]
283
+
284
+ # Run select fields
285
+ RunSelectField = Literal[
286
+ "run_id",
287
+ "thread_id",
288
+ "assistant_id",
289
+ "created_at",
290
+ "updated_at",
291
+ "status",
292
+ "metadata",
293
+ "kwargs",
294
+ "multitask_strategy",
295
+ ]
296
+ RUN_FIELDS: set[str] = set(RunSelectField.__args__) # type: ignore[attr-defined]
297
+
298
+ # Cron select fields
299
+ CronSelectField = Literal[
300
+ "cron_id",
301
+ "assistant_id",
302
+ "thread_id",
303
+ "end_time",
304
+ "schedule",
305
+ "created_at",
306
+ "updated_at",
307
+ "user_id",
308
+ "payload",
309
+ "next_run_date",
310
+ "metadata",
311
+ "now",
312
+ ]
313
+ CRON_FIELDS: set[str] = set(CronSelectField.__args__) # type: ignore[attr-defined]
langgraph_api/serde.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import base64
2
3
  import re
3
4
  import uuid
4
5
  from base64 import b64encode
@@ -16,7 +17,7 @@ from ipaddress import (
16
17
  )
17
18
  from pathlib import Path
18
19
  from re import Pattern
19
- from typing import Any, NamedTuple
20
+ from typing import Any, NamedTuple, cast
20
21
  from zoneinfo import ZoneInfo
21
22
 
22
23
  import cloudpickle
@@ -46,10 +47,14 @@ def decimal_encoder(dec_value: Decimal) -> int | float:
46
47
  >>> decimal_encoder(Decimal("1"))
47
48
  1
48
49
  """
49
- if dec_value.as_tuple().exponent >= 0:
50
- return int(dec_value)
51
- else:
50
+ if (
51
+ # maps to float('nan') / float('inf') / float('-inf')
52
+ not dec_value.is_finite()
53
+ # or regular float
54
+ or cast(int, dec_value.as_tuple().exponent) < 0
55
+ ):
52
56
  return float(dec_value)
57
+ return int(dec_value)
53
58
 
54
59
 
55
60
  def default(obj):
@@ -142,7 +147,7 @@ def json_loads(content: bytes | Fragment | dict) -> Any:
142
147
  content = content.buf
143
148
  if isinstance(content, dict):
144
149
  return content
145
- return orjson.loads(content)
150
+ return orjson.loads(cast(bytes, content))
146
151
 
147
152
 
148
153
  async def ajson_loads(content: bytes | Fragment) -> Any:
@@ -170,3 +175,25 @@ class Serializer(JsonPlusSerializer):
170
175
  )
171
176
  return None
172
177
  return super().loads_typed(data)
178
+
179
+
180
+ mpack_keys = {"method", "value"}
181
+ SERIALIZER = Serializer()
182
+
183
+
184
+ # TODO: Make more performant (by removing)
185
+ async def reserialize_message(message: bytes) -> bytes:
186
+ # Stream messages from golang runtime are a byte dict of StreamChunks.
187
+ loaded = await ajson_loads(message)
188
+ converted = {}
189
+ for k, v in loaded.items():
190
+ if isinstance(v, dict) and v.keys() == mpack_keys:
191
+ if v["method"] == "missing":
192
+ converted[k] = v["value"] # oops
193
+ else:
194
+ converted[k] = SERIALIZER.loads_typed(
195
+ (v["method"], base64.b64decode(v["value"]))
196
+ )
197
+ else:
198
+ converted[k] = v
199
+ return json_dumpb(converted)
langgraph_api/server.py CHANGED
@@ -122,17 +122,19 @@ if user_router:
122
122
  # Merge routes
123
123
  app = user_router
124
124
 
125
- meta_route_paths = [route.path for route in meta_routes]
125
+ meta_route_paths = [
126
+ getattr(route, "path", None) for route in meta_routes if hasattr(route, "path")
127
+ ]
126
128
  custom_route_paths = [
127
129
  route.path
128
130
  for route in user_router.router.routes
129
- if route.path not in meta_route_paths
131
+ if hasattr(route, "path") and route.path not in meta_route_paths
130
132
  ]
131
133
  logger.info(f"Custom route paths: {custom_route_paths}")
132
134
 
133
135
  update_openapi_spec(app)
134
136
  for route in routes:
135
- if route.path in ("/docs", "/openapi.json"):
137
+ if getattr(route, "path", None) in ("/docs", "/openapi.json"):
136
138
  # Our handlers for these are inclusive of the custom routes and default API ones
137
139
  # Don't let these be shadowed
138
140
  app.router.routes.insert(0, route)
langgraph_api/state.py CHANGED
@@ -60,11 +60,11 @@ def patch_interrupt(
60
60
  return {"id": id, **interrupt.raw}
61
61
 
62
62
  if USE_NEW_INTERRUPTS:
63
- interrupt = Interrupt(**interrupt) if isinstance(interrupt, dict) else interrupt
63
+ interrupt = Interrupt(**interrupt) if isinstance(interrupt, dict) else interrupt # type: ignore[missing-argument]
64
64
 
65
65
  return {
66
- "id": interrupt.id,
67
- "value": interrupt.value,
66
+ "id": interrupt.id, # type: ignore[unresolved-attribute]
67
+ "value": interrupt.value, # type: ignore[unresolved-attribute]
68
68
  }
69
69
  else:
70
70
  if isinstance(interrupt, dict):
@@ -72,16 +72,16 @@ def patch_interrupt(
72
72
  # id is the new field we use for identification, also not supported on init for old versions
73
73
  interrupt.pop("interrupt_id", None)
74
74
  interrupt.pop("id", None)
75
- interrupt = Interrupt(**interrupt)
75
+ interrupt = Interrupt(**interrupt) # type: ignore[missing-argument]
76
76
 
77
77
  return {
78
78
  "id": interrupt.interrupt_id
79
79
  if hasattr(interrupt, "interrupt_id")
80
80
  else None,
81
- "value": interrupt.value,
82
- "resumable": interrupt.resumable,
83
- "ns": interrupt.ns,
84
- "when": interrupt.when,
81
+ "value": interrupt.value, # type: ignore[unresolved-attribute]
82
+ "resumable": interrupt.resumable, # type: ignore[unresolved-attribute]
83
+ "ns": interrupt.ns, # type: ignore[unresolved-attribute]
84
+ "when": interrupt.when, # type: ignore[unresolved-attribute]
85
85
  }
86
86
 
87
87
 
langgraph_api/store.py CHANGED
@@ -93,7 +93,7 @@ def _load_store(store_path: str) -> Any:
93
93
  raise ValueError(f"Could not find store file: {path_name}")
94
94
  module = importlib.util.module_from_spec(modspec)
95
95
  sys.modules[module_name] = module
96
- modspec.loader.exec_module(module)
96
+ modspec.loader.exec_module(module) # type: ignore[possibly-unbound-attribute]
97
97
 
98
98
  else:
99
99
  path_name, function = store_path.rsplit(".", 1)
langgraph_api/stream.py CHANGED
@@ -1,3 +1,4 @@
1
+ import uuid
1
2
  from collections.abc import AsyncIterator, Callable
2
3
  from contextlib import AsyncExitStack, aclosing, asynccontextmanager
3
4
  from functools import lru_cache
@@ -93,21 +94,23 @@ def _preproces_debug_checkpoint_task(task: dict[str, Any]) -> dict[str, Any]:
93
94
  return task
94
95
 
95
96
 
96
- def _preprocess_debug_checkpoint(payload: CheckpointPayload | None) -> dict[str, Any]:
97
+ def _preprocess_debug_checkpoint(
98
+ payload: CheckpointPayload | None,
99
+ ) -> dict[str, Any] | None:
97
100
  from langgraph_api.state import runnable_config_to_checkpoint
98
101
 
99
102
  if not payload:
100
103
  return None
101
104
 
102
- payload["checkpoint"] = runnable_config_to_checkpoint(payload["config"])
103
- payload["parent_checkpoint"] = runnable_config_to_checkpoint(
104
- payload["parent_config"] if "parent_config" in payload else None
105
- )
106
-
107
- payload["tasks"] = [_preproces_debug_checkpoint_task(t) for t in payload["tasks"]]
108
-
109
105
  # TODO: deprecate the `config`` and `parent_config`` fields
110
- return payload
106
+ return {
107
+ **payload,
108
+ "checkpoint": runnable_config_to_checkpoint(payload["config"]),
109
+ "parent_checkpoint": runnable_config_to_checkpoint(
110
+ payload["parent_config"] if "parent_config" in payload else None
111
+ ),
112
+ "tasks": [_preproces_debug_checkpoint_task(t) for t in payload["tasks"]],
113
+ }
111
114
 
112
115
 
113
116
  @asynccontextmanager
@@ -216,7 +219,7 @@ async def astream_state(
216
219
  if use_astream_events:
217
220
  async with (
218
221
  stack,
219
- aclosing(
222
+ aclosing( # type: ignore[invalid-argument-type]
220
223
  graph.astream_events(
221
224
  input,
222
225
  config,
@@ -231,6 +234,7 @@ async def astream_state(
231
234
  event = await wait_if_not_done(anext(stream, sentinel), done)
232
235
  if event is sentinel:
233
236
  break
237
+ event = cast(dict, event)
234
238
  if event.get("tags") and "langsmith:hidden" in event["tags"]:
235
239
  continue
236
240
  if "messages" in stream_mode and isinstance(graph, BaseRemotePregel):
@@ -251,6 +255,7 @@ async def astream_state(
251
255
  if mode == "debug":
252
256
  if chunk["type"] == "checkpoint":
253
257
  checkpoint = _preprocess_debug_checkpoint(chunk["payload"])
258
+ chunk["payload"] = checkpoint
254
259
  on_checkpoint(checkpoint)
255
260
  elif chunk["type"] == "task_result":
256
261
  on_task_result(chunk["payload"])
@@ -261,11 +266,14 @@ async def astream_state(
261
266
  else:
262
267
  yield "messages", chunk
263
268
  else:
264
- msg, meta = cast(
269
+ msg_, meta = cast(
265
270
  tuple[BaseMessage | dict, dict[str, Any]], chunk
266
271
  )
267
- if isinstance(msg, dict):
268
- msg = convert_to_messages([msg])[0]
272
+ msg = (
273
+ convert_to_messages([msg_])[0]
274
+ if isinstance(msg_, dict)
275
+ else cast(BaseMessage, msg_)
276
+ )
269
277
  if msg.id in messages:
270
278
  messages[msg.id] += msg
271
279
  else:
@@ -323,14 +331,15 @@ async def astream_state(
323
331
  if event is sentinel:
324
332
  break
325
333
  if subgraphs:
326
- ns, mode, chunk = event
334
+ ns, mode, chunk = cast(tuple[str, str, dict[str, Any]], event)
327
335
  else:
328
- mode, chunk = event
336
+ mode, chunk = cast(tuple[str, dict[str, Any]], event)
329
337
  ns = None
330
338
  # --- begin shared logic with astream_events ---
331
339
  if mode == "debug":
332
340
  if chunk["type"] == "checkpoint":
333
341
  checkpoint = _preprocess_debug_checkpoint(chunk["payload"])
342
+ chunk["payload"] = checkpoint
334
343
  on_checkpoint(checkpoint)
335
344
  elif chunk["type"] == "task_result":
336
345
  on_task_result(chunk["payload"])
@@ -341,11 +350,15 @@ async def astream_state(
341
350
  else:
342
351
  yield "messages", chunk
343
352
  else:
344
- msg, meta = cast(
353
+ msg_, meta = cast(
345
354
  tuple[BaseMessage | dict, dict[str, Any]], chunk
346
355
  )
347
- if isinstance(msg, dict):
348
- msg = convert_to_messages([msg])[0]
356
+ msg = (
357
+ convert_to_messages([msg_])[0]
358
+ if isinstance(msg_, dict)
359
+ else cast(BaseMessage, msg_)
360
+ )
361
+
349
362
  if msg.id in messages:
350
363
  messages[msg.id] += msg
351
364
  else:
@@ -399,7 +412,7 @@ async def astream_state(
399
412
 
400
413
  async def consume(
401
414
  stream: AnyStream,
402
- run_id: str,
415
+ run_id: str | uuid.UUID,
403
416
  resumable: bool = False,
404
417
  stream_modes: set[StreamMode] | None = None,
405
418
  ) -> None:
@@ -408,7 +421,7 @@ async def consume(
408
421
  stream_modes.add("messages")
409
422
  stream_modes.add("metadata")
410
423
 
411
- async with aclosing(stream):
424
+ async with aclosing(stream): # type: ignore[invalid-argument-type]
412
425
  try:
413
426
  async for mode, payload in stream:
414
427
  await Runs.Stream.publish(
@@ -19,4 +19,4 @@ def patch_requests():
19
19
  raise RuntimeError(f"POST to {url} blocked by policy")
20
20
  return _orig(self, method, url, *a, **kw)
21
21
 
22
- Session.request = _guard
22
+ Session.request = _guard # type: ignore[invalid-assignment]
@@ -1,9 +1,9 @@
1
1
  import contextvars
2
2
  import uuid
3
- from collections.abc import AsyncGenerator, AsyncIterator
3
+ from collections.abc import AsyncIterator
4
4
  from contextlib import asynccontextmanager
5
5
  from datetime import datetime
6
- from typing import Any, Protocol, TypeAlias, TypeVar
6
+ from typing import Any, Protocol, TypeAlias, TypeVar, cast
7
7
 
8
8
  import structlog
9
9
  from langgraph_sdk import Auth
@@ -12,6 +12,7 @@ from starlette.exceptions import HTTPException
12
12
  from starlette.schemas import BaseSchemaGenerator
13
13
 
14
14
  from langgraph_api.auth.custom import SimpleUser
15
+ from langgraph_api.utils.uuids import uuid7
15
16
 
16
17
  logger = structlog.stdlib.get_logger(__name__)
17
18
 
@@ -32,7 +33,9 @@ async def with_user(
32
33
  yield
33
34
  if current is None:
34
35
  return
35
- set_auth_ctx(current.user, AuthCredentials(scopes=current.permissions))
36
+ set_auth_ctx(
37
+ cast(BaseUser, current.user), AuthCredentials(scopes=current.permissions)
38
+ )
36
39
 
37
40
 
38
41
  def set_auth_ctx(
@@ -99,7 +102,7 @@ def validate_uuid(uuid_str: str, invalid_uuid_detail: str | None) -> uuid.UUID:
99
102
 
100
103
 
101
104
  def next_cron_date(schedule: str, base_time: datetime) -> datetime:
102
- import croniter
105
+ import croniter # type: ignore[unresolved-import]
103
106
 
104
107
  cron_iter = croniter.croniter(schedule, base_time)
105
108
  return cron_iter.get_next(datetime)
@@ -130,7 +133,7 @@ class SchemaGenerator(BaseSchemaGenerator):
130
133
 
131
134
 
132
135
  async def get_pagination_headers(
133
- resource: AsyncGenerator[T],
136
+ resource: AsyncIterator[T],
134
137
  next_offset: int | None,
135
138
  offset: int,
136
139
  ) -> tuple[list[T], dict[str, str]]:
@@ -143,3 +146,35 @@ async def get_pagination_headers(
143
146
  "X-Pagination-Next": str(next_offset),
144
147
  }
145
148
  return resources, response_headers
149
+
150
+
151
+ def validate_select_columns(
152
+ select: list[str] | None, allowed: set[str]
153
+ ) -> list[str] | None:
154
+ """Validate select columns against an allowed set.
155
+
156
+ Returns the input list (or None) if valid, otherwise raises HTTP 422.
157
+ """
158
+ if not select:
159
+ return None
160
+ invalid = [col for col in select if col not in allowed]
161
+ if invalid:
162
+ raise HTTPException(
163
+ status_code=422,
164
+ detail=f"Invalid select columns: {invalid}. Expected: {allowed}",
165
+ )
166
+ return select
167
+
168
+
169
+ __all__ = [
170
+ "AsyncCursorProto",
171
+ "AsyncPipelineProto",
172
+ "AsyncConnectionProto",
173
+ "fetchone",
174
+ "validate_uuid",
175
+ "next_cron_date",
176
+ "SchemaGenerator",
177
+ "get_pagination_headers",
178
+ "uuid7",
179
+ "validate_select_columns",
180
+ ]
@@ -7,9 +7,10 @@ from collections import ChainMap
7
7
  from concurrent.futures import Executor
8
8
  from contextvars import copy_context
9
9
  from os import getenv
10
- from typing import Any, ParamSpec, TypeVar, cast
10
+ from typing import Any, ParamSpec, TypeVar
11
11
 
12
12
  from langgraph.constants import CONF
13
+ from typing_extensions import TypedDict
13
14
 
14
15
  if typing.TYPE_CHECKING:
15
16
  from langchain_core.runnables import RunnableConfig
@@ -19,7 +20,7 @@ try:
19
20
  var_child_runnable_config,
20
21
  )
21
22
  except ImportError:
22
- var_child_runnable_config = None
23
+ var_child_runnable_config = None # type: ignore[invalid-assignment]
23
24
 
24
25
  CONFIG_KEYS = [
25
26
  "tags",
@@ -52,6 +53,14 @@ def _is_not_empty(value: Any) -> bool:
52
53
  return value is not None
53
54
 
54
55
 
56
+ class _Config(TypedDict):
57
+ tags: list[str]
58
+ metadata: ChainMap
59
+ callbacks: None
60
+ recursion_limit: int
61
+ configurable: dict[str, Any]
62
+
63
+
55
64
  def ensure_config(*configs: RunnableConfig | None) -> RunnableConfig:
56
65
  """Return a config with all keys, merging any provided configs.
57
66
 
@@ -61,7 +70,7 @@ def ensure_config(*configs: RunnableConfig | None) -> RunnableConfig:
61
70
  Returns:
62
71
  RunnableConfig: The merged and ensured config.
63
72
  """
64
- empty = dict(
73
+ empty = _Config(
65
74
  tags=[],
66
75
  metadata=ChainMap(),
67
76
  callbacks=None,
@@ -84,7 +93,7 @@ def ensure_config(*configs: RunnableConfig | None) -> RunnableConfig:
84
93
  for k, v in config.items():
85
94
  if _is_not_empty(v) and k in CONFIG_KEYS:
86
95
  if k == CONF:
87
- empty[k] = cast(dict, v).copy()
96
+ empty[k] = v.copy() # type: ignore
88
97
  else:
89
98
  empty[k] = v # type: ignore[literal-required]
90
99
  for k, v in config.items():
@@ -167,7 +167,7 @@ def _ensure_future(
167
167
  elif EAGER_NOT_SUPPORTED or lazy:
168
168
  return loop.create_task(coro_or_future, name=name, context=context)
169
169
  else:
170
- return asyncio.eager_task_factory(
170
+ return asyncio.eager_task_factory( # type: ignore[unresolved-attribute]
171
171
  loop, coro_or_future, name=name, context=context
172
172
  )
173
173
  except RuntimeError:
@@ -0,0 +1,87 @@
1
+ import os
2
+ import time
3
+ from uuid import UUID, SafeUUID
4
+
5
+ _last_timestamp_v7 = None
6
+ _last_counter_v7 = 0 # 42-bit counter
7
+ _RFC_4122_VERSION_7_FLAGS = (7 << 76) | (0x8000 << 48)
8
+
9
+
10
+ def _uuid7_get_counter_and_tail():
11
+ rand = int.from_bytes(os.urandom(10))
12
+ # 42-bit counter with MSB set to 0
13
+ counter = (rand >> 32) & 0x1FF_FFFF_FFFF
14
+ # 32-bit random data
15
+ tail = rand & 0xFFFF_FFFF
16
+ return counter, tail
17
+
18
+
19
+ def _from_int(value: int) -> UUID:
20
+ uid = object.__new__(UUID)
21
+ object.__setattr__(uid, "int", value)
22
+ object.__setattr__(uid, "is_safe", SafeUUID.unknown)
23
+ return uid
24
+
25
+
26
+ def uuid7():
27
+ """Generate a UUID from a Unix timestamp in milliseconds and random bits.
28
+
29
+ UUIDv7 objects feature monotonicity within a millisecond.
30
+ """
31
+ # --- 48 --- -- 4 -- --- 12 --- -- 2 -- --- 30 --- - 32 -
32
+ # unix_ts_ms | version | counter_hi | variant | counter_lo | random
33
+ #
34
+ # 'counter = counter_hi | counter_lo' is a 42-bit counter constructed
35
+ # with Method 1 of RFC 9562, §6.2, and its MSB is set to 0.
36
+ #
37
+ # 'random' is a 32-bit random value regenerated for every new UUID.
38
+ #
39
+ # If multiple UUIDs are generated within the same millisecond, the LSB
40
+ # of 'counter' is incremented by 1. When overflowing, the timestamp is
41
+ # advanced and the counter is reset to a random 42-bit integer with MSB
42
+ # set to 0.
43
+
44
+ global _last_timestamp_v7
45
+ global _last_counter_v7
46
+
47
+ nanoseconds = time.time_ns()
48
+ timestamp_ms = nanoseconds // 1_000_000
49
+
50
+ if _last_timestamp_v7 is None or timestamp_ms > _last_timestamp_v7:
51
+ counter, tail = _uuid7_get_counter_and_tail()
52
+ else:
53
+ if timestamp_ms < _last_timestamp_v7:
54
+ timestamp_ms = _last_timestamp_v7 + 1
55
+ # advance the 42-bit counter
56
+ counter = _last_counter_v7 + 1
57
+ if counter > 0x3FF_FFFF_FFFF:
58
+ # advance the 48-bit timestamp
59
+ timestamp_ms += 1
60
+ counter, tail = _uuid7_get_counter_and_tail()
61
+ else:
62
+ # 32-bit random data
63
+ tail = int.from_bytes(os.urandom(4))
64
+
65
+ unix_ts_ms = timestamp_ms & 0xFFFF_FFFF_FFFF
66
+ counter_msbs = counter >> 30
67
+ # keep 12 counter's MSBs and clear variant bits
68
+ counter_hi = counter_msbs & 0x0FFF
69
+ # keep 30 counter's LSBs and clear version bits
70
+ counter_lo = counter & 0x3FFF_FFFF
71
+ # ensure that the tail is always a 32-bit integer (by construction,
72
+ # it is already the case, but future interfaces may allow the user
73
+ # to specify the random tail)
74
+ tail &= 0xFFFF_FFFF
75
+
76
+ int_uuid_7 = unix_ts_ms << 80
77
+ int_uuid_7 |= counter_hi << 64
78
+ int_uuid_7 |= counter_lo << 32
79
+ int_uuid_7 |= tail
80
+ # by construction, the variant and version bits are already cleared
81
+ int_uuid_7 |= _RFC_4122_VERSION_7_FLAGS
82
+ res = _from_int(int_uuid_7)
83
+
84
+ # defer global update until all computations are done
85
+ _last_timestamp_v7 = timestamp_ms
86
+ _last_counter_v7 = counter
87
+ return res
@@ -14,9 +14,15 @@ AssistantVersionsSearchRequest = jsonschema_rs.validator_for(
14
14
  AssistantSearchRequest = jsonschema_rs.validator_for(
15
15
  openapi["components"]["schemas"]["AssistantSearchRequest"]
16
16
  )
17
+ AssistantCountRequest = jsonschema_rs.validator_for(
18
+ openapi["components"]["schemas"]["AssistantCountRequest"]
19
+ )
17
20
  ThreadSearchRequest = jsonschema_rs.validator_for(
18
21
  openapi["components"]["schemas"]["ThreadSearchRequest"]
19
22
  )
23
+ ThreadCountRequest = jsonschema_rs.validator_for(
24
+ openapi["components"]["schemas"]["ThreadCountRequest"]
25
+ )
20
26
  AssistantCreate = jsonschema_rs.validator_for(
21
27
  openapi["components"]["schemas"]["AssistantCreate"]
22
28
  )
@@ -116,6 +122,9 @@ RunCreateStateful = jsonschema_rs.validator_for(
116
122
  RunsCancel = jsonschema_rs.validator_for(openapi["components"]["schemas"]["RunsCancel"])
117
123
  CronCreate = jsonschema_rs.validator_for(openapi["components"]["schemas"]["CronCreate"])
118
124
  CronSearch = jsonschema_rs.validator_for(openapi["components"]["schemas"]["CronSearch"])
125
+ CronCountRequest = jsonschema_rs.validator_for(
126
+ openapi["components"]["schemas"]["CronCountRequest"]
127
+ )
119
128
 
120
129
 
121
130
  # Stuff around storage/BaseStore API