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.
- langgraph_api/__init__.py +1 -1
- langgraph_api/api/assistants.py +32 -6
- langgraph_api/api/meta.py +3 -1
- langgraph_api/api/openapi.py +1 -1
- langgraph_api/api/runs.py +50 -10
- langgraph_api/api/threads.py +27 -1
- langgraph_api/api/ui.py +2 -0
- langgraph_api/asgi_transport.py +2 -2
- langgraph_api/asyncio.py +10 -8
- langgraph_api/auth/custom.py +9 -4
- langgraph_api/auth/langsmith/client.py +1 -1
- langgraph_api/cli.py +5 -4
- langgraph_api/config.py +1 -1
- langgraph_api/executor_entrypoint.py +23 -0
- langgraph_api/graph.py +25 -9
- langgraph_api/http.py +10 -7
- langgraph_api/http_metrics.py +4 -1
- langgraph_api/js/build.mts +11 -2
- langgraph_api/js/client.http.mts +2 -0
- langgraph_api/js/client.mts +13 -3
- langgraph_api/js/package.json +2 -2
- langgraph_api/js/remote.py +17 -12
- langgraph_api/js/src/preload.mjs +9 -1
- langgraph_api/js/src/utils/files.mts +5 -2
- langgraph_api/js/sse.py +1 -1
- langgraph_api/js/yarn.lock +9 -9
- langgraph_api/logging.py +3 -3
- langgraph_api/middleware/http_logger.py +2 -1
- langgraph_api/models/run.py +19 -14
- langgraph_api/patch.py +2 -2
- langgraph_api/queue_entrypoint.py +33 -18
- langgraph_api/schema.py +88 -4
- langgraph_api/serde.py +32 -5
- langgraph_api/server.py +5 -3
- langgraph_api/state.py +8 -8
- langgraph_api/store.py +1 -1
- langgraph_api/stream.py +33 -20
- langgraph_api/traceblock.py +1 -1
- langgraph_api/utils/__init__.py +40 -5
- langgraph_api/utils/config.py +13 -4
- langgraph_api/utils/future.py +1 -1
- langgraph_api/utils/uuids.py +87 -0
- langgraph_api/validation.py +9 -0
- langgraph_api/webhook.py +20 -20
- langgraph_api/worker.py +8 -5
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/METADATA +2 -2
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/RECORD +51 -49
- openapi.json +331 -1
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/WHEEL +0 -0
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/entry_points.txt +0 -0
- {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:
|
|
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:
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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 = [
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
269
|
+
msg_, meta = cast(
|
|
265
270
|
tuple[BaseMessage | dict, dict[str, Any]], chunk
|
|
266
271
|
)
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
353
|
+
msg_, meta = cast(
|
|
345
354
|
tuple[BaseMessage | dict, dict[str, Any]], chunk
|
|
346
355
|
)
|
|
347
|
-
|
|
348
|
-
|
|
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(
|
langgraph_api/traceblock.py
CHANGED
langgraph_api/utils/__init__.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import contextvars
|
|
2
2
|
import uuid
|
|
3
|
-
from collections.abc import
|
|
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(
|
|
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:
|
|
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
|
+
]
|
langgraph_api/utils/config.py
CHANGED
|
@@ -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
|
|
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 =
|
|
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] =
|
|
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():
|
langgraph_api/utils/future.py
CHANGED
|
@@ -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
|
langgraph_api/validation.py
CHANGED
|
@@ -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
|