langgraph-api 0.0.11__py3-none-any.whl → 0.0.13__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/api/assistants.py +74 -76
- langgraph_api/api/openapi.py +1 -1
- langgraph_api/auth/custom.py +75 -21
- langgraph_api/auth/langsmith/backend.py +4 -2
- langgraph_api/auth/middleware.py +6 -6
- langgraph_api/auth/studio_user.py +6 -0
- langgraph_api/config.py +0 -3
- langgraph_api/graph.py +44 -24
- langgraph_api/stream.py +15 -10
- {langgraph_api-0.0.11.dist-info → langgraph_api-0.0.13.dist-info}/METADATA +2 -2
- {langgraph_api-0.0.11.dist-info → langgraph_api-0.0.13.dist-info}/RECORD +16 -15
- langgraph_storage/ops.py +53 -50
- openapi.json +18 -5
- {langgraph_api-0.0.11.dist-info → langgraph_api-0.0.13.dist-info}/LICENSE +0 -0
- {langgraph_api-0.0.11.dist-info → langgraph_api-0.0.13.dist-info}/WHEEL +0 -0
- {langgraph_api-0.0.11.dist-info → langgraph_api-0.0.13.dist-info}/entry_points.txt +0 -0
langgraph_api/api/assistants.py
CHANGED
|
@@ -121,28 +121,27 @@ async def get_assistant_graph(
|
|
|
121
121
|
assistant_ = await Assistants.get(conn, assistant_id)
|
|
122
122
|
assistant = await fetchone(assistant_)
|
|
123
123
|
config = await ajson_loads(assistant["config"])
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return ApiResponse(
|
|
145
|
-
return ApiResponse(graph.get_graph(xray=xray).to_json())
|
|
124
|
+
async with get_graph(assistant["graph_id"], config) as graph:
|
|
125
|
+
xray: bool | int = False
|
|
126
|
+
xray_query = request.query_params.get("xray")
|
|
127
|
+
if xray_query:
|
|
128
|
+
if xray_query in ("true", "True"):
|
|
129
|
+
xray = True
|
|
130
|
+
elif xray_query in ("false", "False"):
|
|
131
|
+
xray = False
|
|
132
|
+
else:
|
|
133
|
+
try:
|
|
134
|
+
xray = int(xray_query)
|
|
135
|
+
except ValueError:
|
|
136
|
+
raise HTTPException(422, detail="Invalid xray value") from None
|
|
137
|
+
|
|
138
|
+
if xray <= 0:
|
|
139
|
+
raise HTTPException(422, detail="Invalid xray value") from None
|
|
140
|
+
|
|
141
|
+
if isinstance(graph, RemotePregel):
|
|
142
|
+
drawable_graph = await graph.fetch_graph(xray=xray)
|
|
143
|
+
return ApiResponse(drawable_graph.to_json())
|
|
144
|
+
return ApiResponse(graph.get_graph(xray=xray).to_json())
|
|
146
145
|
|
|
147
146
|
|
|
148
147
|
@retry_db
|
|
@@ -156,29 +155,29 @@ async def get_assistant_subgraphs(
|
|
|
156
155
|
assistant_ = await Assistants.get(conn, assistant_id)
|
|
157
156
|
assistant = await fetchone(assistant_)
|
|
158
157
|
config = await ajson_loads(assistant["config"])
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
async with get_graph(assistant["graph_id"], config) as graph:
|
|
159
|
+
namespace = request.path_params.get("namespace")
|
|
160
|
+
|
|
161
|
+
if isinstance(graph, RemotePregel):
|
|
162
|
+
return ApiResponse(
|
|
163
|
+
await graph.fetch_subgraphs(
|
|
164
|
+
namespace=namespace,
|
|
165
|
+
recurse=request.query_params.get("recurse", "False")
|
|
166
|
+
in ("true", "True"),
|
|
167
|
+
)
|
|
168
|
+
)
|
|
161
169
|
|
|
162
|
-
if isinstance(graph, RemotePregel):
|
|
163
170
|
return ApiResponse(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
171
|
+
{
|
|
172
|
+
ns: _graph_schemas(subgraph)
|
|
173
|
+
async for ns, subgraph in graph.aget_subgraphs(
|
|
174
|
+
namespace=namespace,
|
|
175
|
+
recurse=request.query_params.get("recurse", "False")
|
|
176
|
+
in ("true", "True"),
|
|
177
|
+
)
|
|
178
|
+
}
|
|
169
179
|
)
|
|
170
180
|
|
|
171
|
-
return ApiResponse(
|
|
172
|
-
{
|
|
173
|
-
ns: _graph_schemas(subgraph)
|
|
174
|
-
async for ns, subgraph in graph.aget_subgraphs(
|
|
175
|
-
namespace=namespace,
|
|
176
|
-
recurse=request.query_params.get("recurse", "False")
|
|
177
|
-
in ("true", "True"),
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
)
|
|
181
|
-
|
|
182
181
|
|
|
183
182
|
@retry_db
|
|
184
183
|
async def get_assistant_schemas(
|
|
@@ -191,48 +190,47 @@ async def get_assistant_schemas(
|
|
|
191
190
|
assistant_ = await Assistants.get(conn, assistant_id)
|
|
192
191
|
assistant = await fetchone(assistant_)
|
|
193
192
|
config = await ajson_loads(assistant["config"])
|
|
194
|
-
|
|
193
|
+
async with get_graph(assistant["graph_id"], config) as graph:
|
|
194
|
+
if isinstance(graph, RemotePregel):
|
|
195
|
+
schemas = await graph.fetch_state_schema()
|
|
196
|
+
return ApiResponse(
|
|
197
|
+
{
|
|
198
|
+
"graph_id": assistant["graph_id"],
|
|
199
|
+
"input_schema": schemas.get("input"),
|
|
200
|
+
"output_schema": schemas.get("output"),
|
|
201
|
+
"state_schema": schemas.get("state"),
|
|
202
|
+
"config_schema": schemas.get("config"),
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
input_schema = graph.get_input_schema().schema()
|
|
208
|
+
except Exception:
|
|
209
|
+
input_schema = None
|
|
210
|
+
try:
|
|
211
|
+
output_schema = graph.get_output_schema().schema()
|
|
212
|
+
except Exception:
|
|
213
|
+
output_schema = None
|
|
195
214
|
|
|
196
|
-
|
|
197
|
-
|
|
215
|
+
state_schema = _state_jsonschema(graph)
|
|
216
|
+
try:
|
|
217
|
+
config_schema = (
|
|
218
|
+
graph.config_schema().__fields__["configurable"].annotation.schema()
|
|
219
|
+
if "configurable" in graph.config_schema().__fields__
|
|
220
|
+
else {}
|
|
221
|
+
)
|
|
222
|
+
except Exception:
|
|
223
|
+
config_schema = None
|
|
198
224
|
return ApiResponse(
|
|
199
225
|
{
|
|
200
226
|
"graph_id": assistant["graph_id"],
|
|
201
|
-
"input_schema":
|
|
202
|
-
"output_schema":
|
|
203
|
-
"state_schema":
|
|
204
|
-
"config_schema":
|
|
227
|
+
"input_schema": input_schema,
|
|
228
|
+
"output_schema": output_schema,
|
|
229
|
+
"state_schema": state_schema,
|
|
230
|
+
"config_schema": config_schema,
|
|
205
231
|
}
|
|
206
232
|
)
|
|
207
233
|
|
|
208
|
-
try:
|
|
209
|
-
input_schema = graph.get_input_schema().schema()
|
|
210
|
-
except Exception:
|
|
211
|
-
input_schema = None
|
|
212
|
-
try:
|
|
213
|
-
output_schema = graph.get_output_schema().schema()
|
|
214
|
-
except Exception:
|
|
215
|
-
output_schema = None
|
|
216
|
-
|
|
217
|
-
state_schema = _state_jsonschema(graph)
|
|
218
|
-
try:
|
|
219
|
-
config_schema = (
|
|
220
|
-
graph.config_schema().__fields__["configurable"].annotation.schema()
|
|
221
|
-
if "configurable" in graph.config_schema().__fields__
|
|
222
|
-
else {}
|
|
223
|
-
)
|
|
224
|
-
except Exception:
|
|
225
|
-
config_schema = None
|
|
226
|
-
return ApiResponse(
|
|
227
|
-
{
|
|
228
|
-
"graph_id": assistant["graph_id"],
|
|
229
|
-
"input_schema": input_schema,
|
|
230
|
-
"output_schema": output_schema,
|
|
231
|
-
"state_schema": state_schema,
|
|
232
|
-
"config_schema": config_schema,
|
|
233
|
-
}
|
|
234
|
-
)
|
|
235
|
-
|
|
236
234
|
|
|
237
235
|
@retry_db
|
|
238
236
|
async def patch_assistant(
|
langgraph_api/api/openapi.py
CHANGED
|
@@ -32,7 +32,7 @@ def get_openapi_spec() -> str:
|
|
|
32
32
|
openapi["components"]["securitySchemes"] = {
|
|
33
33
|
"x-api-key": {"type": "apiKey", "in": "header", "name": "x-api-key"}
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
if LANGGRAPH_AUTH:
|
|
36
36
|
# Allow user to specify OpenAPI security configuration
|
|
37
37
|
if isinstance(LANGGRAPH_AUTH, dict) and "openapi" in LANGGRAPH_AUTH:
|
|
38
38
|
openapi_config = LANGGRAPH_AUTH["openapi"]
|
langgraph_api/auth/custom.py
CHANGED
|
@@ -24,7 +24,8 @@ from starlette.requests import HTTPConnection, Request
|
|
|
24
24
|
from starlette.responses import Response
|
|
25
25
|
|
|
26
26
|
from langgraph_api.auth.langsmith.backend import LangsmithAuthBackend
|
|
27
|
-
from langgraph_api.
|
|
27
|
+
from langgraph_api.auth.studio_user import StudioUser
|
|
28
|
+
from langgraph_api.config import LANGGRAPH_AUTH, LANGGRAPH_AUTH_TYPE
|
|
28
29
|
|
|
29
30
|
logger = structlog.stdlib.get_logger(__name__)
|
|
30
31
|
|
|
@@ -91,12 +92,23 @@ async def handle_event(
|
|
|
91
92
|
handler = _get_handler(auth, ctx)
|
|
92
93
|
if not handler:
|
|
93
94
|
return
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
try:
|
|
96
|
+
result = await handler(ctx=ctx, value=value)
|
|
97
|
+
except Auth.exceptions.HTTPException as e:
|
|
98
|
+
raise HTTPException(
|
|
99
|
+
status_code=e.status_code,
|
|
100
|
+
detail=e.detail,
|
|
101
|
+
headers=dict(e.headers) if e.headers else None,
|
|
102
|
+
) from e
|
|
103
|
+
except AssertionError as e:
|
|
104
|
+
raise HTTPException(
|
|
105
|
+
status_code=403,
|
|
106
|
+
detail=str(e),
|
|
107
|
+
) from e
|
|
96
108
|
|
|
97
109
|
if result in (None, True):
|
|
98
110
|
return
|
|
99
|
-
if result is
|
|
111
|
+
if result is False:
|
|
100
112
|
raise HTTPException(403, "Forbidden")
|
|
101
113
|
|
|
102
114
|
if not isinstance(result, dict):
|
|
@@ -111,30 +123,31 @@ async def handle_event(
|
|
|
111
123
|
class CustomAuthBackend(AuthenticationBackend):
|
|
112
124
|
def __init__(
|
|
113
125
|
self,
|
|
114
|
-
fn:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
]
|
|
119
|
-
| None
|
|
120
|
-
) = None,
|
|
126
|
+
fn: Callable[
|
|
127
|
+
[Request],
|
|
128
|
+
Awaitable[tuple[list[str], Any]],
|
|
129
|
+
],
|
|
121
130
|
disable_studio_auth: bool = False,
|
|
122
131
|
):
|
|
123
|
-
if fn
|
|
124
|
-
self.fn = None
|
|
125
|
-
elif not inspect.iscoroutinefunction(fn):
|
|
132
|
+
if not inspect.iscoroutinefunction(fn):
|
|
126
133
|
self.fn = functools.partial(run_in_threadpool, fn)
|
|
127
134
|
else:
|
|
128
135
|
self.fn = fn
|
|
129
136
|
self._param_names = (
|
|
130
|
-
|
|
137
|
+
_get_named_arguments(fn, supported_params=SUPPORTED_PARAMETERS)
|
|
131
138
|
if fn
|
|
132
139
|
else None
|
|
133
140
|
)
|
|
141
|
+
self.ls_auth = None
|
|
134
142
|
if not disable_studio_auth:
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
if LANGGRAPH_AUTH_TYPE == "langsmith":
|
|
144
|
+
self.ls_auth = LangsmithAuthBackend()
|
|
145
|
+
elif (
|
|
146
|
+
LANGGRAPH_AUTH_TYPE == "noop"
|
|
147
|
+
and (auth_type := os.environ.get("LANGSMITH_LANGGRAPH_API_VARIANT"))
|
|
148
|
+
and auth_type == "local_dev"
|
|
149
|
+
):
|
|
150
|
+
self.ls_auth = StudioNoopAuthBackend()
|
|
138
151
|
|
|
139
152
|
def __str__(self):
|
|
140
153
|
return (
|
|
@@ -160,8 +173,24 @@ class CustomAuthBackend(AuthenticationBackend):
|
|
|
160
173
|
)
|
|
161
174
|
response = await self.fn(**args)
|
|
162
175
|
return _normalize_auth_response(response)
|
|
176
|
+
except (AuthenticationError, HTTPException):
|
|
177
|
+
raise
|
|
178
|
+
except Auth.exceptions.HTTPException as e:
|
|
179
|
+
raise HTTPException(
|
|
180
|
+
status_code=e.status_code,
|
|
181
|
+
detail=e.detail,
|
|
182
|
+
headers=dict(e.headers) if e.headers else None,
|
|
183
|
+
) from None
|
|
163
184
|
except AssertionError as e:
|
|
164
185
|
raise AuthenticationError(str(e)) from None
|
|
186
|
+
except Exception as e:
|
|
187
|
+
await logger.aerror("Error authenticating request", exc_info=e)
|
|
188
|
+
status_code = getattr(e, "status_code", 401)
|
|
189
|
+
detail = getattr(e, "detail", "Unauthorized")
|
|
190
|
+
headers = getattr(e, "headers", None)
|
|
191
|
+
raise HTTPException(
|
|
192
|
+
status_code=status_code, detail=detail, headers=headers
|
|
193
|
+
) from None
|
|
165
194
|
|
|
166
195
|
|
|
167
196
|
def _get_custom_auth_middleware(
|
|
@@ -174,8 +203,26 @@ def _get_custom_auth_middleware(
|
|
|
174
203
|
path = config.get("path")
|
|
175
204
|
disable_studio_auth = config.get("disable_studio_auth", disable_studio_auth)
|
|
176
205
|
auth_instance = _get_auth_instance(path)
|
|
206
|
+
if auth_instance is None:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
f"Custom Auth object not found at path: {path}. "
|
|
209
|
+
"Check that the path is correct and the file is available."
|
|
210
|
+
"Auth objects are created like:\n"
|
|
211
|
+
"from langgraph_sdk import Auth\n"
|
|
212
|
+
"auth = Auth()"
|
|
213
|
+
)
|
|
214
|
+
if auth_instance._authenticate_handler is None:
|
|
215
|
+
raise ValueError(
|
|
216
|
+
f"Custom Auth object at path: {path} does not have an authenticate handler."
|
|
217
|
+
"Please define one like:\n"
|
|
218
|
+
"from langgraph_sdk import Auth\n"
|
|
219
|
+
"auth = Auth()\n"
|
|
220
|
+
"@auth.authenticate\n"
|
|
221
|
+
"async def authenticate(request):\n"
|
|
222
|
+
' return "my-user-id"'
|
|
223
|
+
)
|
|
177
224
|
result = CustomAuthBackend(
|
|
178
|
-
auth_instance._authenticate_handler
|
|
225
|
+
auth_instance._authenticate_handler,
|
|
179
226
|
disable_studio_auth,
|
|
180
227
|
)
|
|
181
228
|
logger.info(f"Loaded custom auth middleware: {str(result)}")
|
|
@@ -288,7 +335,7 @@ def _solve_fastapi_dependencies(
|
|
|
288
335
|
|
|
289
336
|
_param_names = {
|
|
290
337
|
k
|
|
291
|
-
for k in
|
|
338
|
+
for k in _get_named_arguments(
|
|
292
339
|
fn, supported_params=SUPPORTED_PARAMETERS | dict(deps)
|
|
293
340
|
)
|
|
294
341
|
if k not in dependents
|
|
@@ -535,7 +582,14 @@ def _get_handler(auth: Auth, ctx: Auth.types.AuthContext) -> Auth.types.Handler
|
|
|
535
582
|
return None
|
|
536
583
|
|
|
537
584
|
|
|
538
|
-
|
|
585
|
+
class StudioNoopAuthBackend(AuthenticationBackend):
|
|
586
|
+
async def authenticate(
|
|
587
|
+
self, conn: HTTPConnection
|
|
588
|
+
) -> tuple[AuthCredentials, BaseUser] | None:
|
|
589
|
+
return AuthCredentials(), StudioUser("langgraph-studio-user")
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _get_named_arguments(fn: Callable, supported_params: dict) -> set[str]:
|
|
539
593
|
"""Get the named arguments that a function accepts, ensuring they're supported."""
|
|
540
594
|
sig = inspect.signature(fn)
|
|
541
595
|
# Check for unsupported required parameters
|
|
@@ -5,11 +5,11 @@ from starlette.authentication import (
|
|
|
5
5
|
AuthenticationBackend,
|
|
6
6
|
AuthenticationError,
|
|
7
7
|
BaseUser,
|
|
8
|
-
SimpleUser,
|
|
9
8
|
)
|
|
10
9
|
from starlette.requests import HTTPConnection
|
|
11
10
|
|
|
12
11
|
from langgraph_api.auth.langsmith.client import auth_client
|
|
12
|
+
from langgraph_api.auth.studio_user import StudioUser
|
|
13
13
|
from langgraph_api.config import (
|
|
14
14
|
LANGSMITH_AUTH_VERIFY_TENANT_ID,
|
|
15
15
|
LANGSMITH_TENANT_ID,
|
|
@@ -64,4 +64,6 @@ class LangsmithAuthBackend(AuthenticationBackend):
|
|
|
64
64
|
if auth_dict["tenant_id"] != LANGSMITH_TENANT_ID:
|
|
65
65
|
raise AuthenticationError("Invalid tenant ID")
|
|
66
66
|
|
|
67
|
-
return AuthCredentials(["authenticated"]),
|
|
67
|
+
return AuthCredentials(["authenticated"]), StudioUser(
|
|
68
|
+
auth_dict.get("user_id"), is_authenticated=True
|
|
69
|
+
)
|
langgraph_api/auth/middleware.py
CHANGED
|
@@ -8,23 +8,23 @@ from starlette.requests import HTTPConnection
|
|
|
8
8
|
from starlette.responses import JSONResponse
|
|
9
9
|
from starlette.types import Receive, Scope, Send
|
|
10
10
|
|
|
11
|
-
from langgraph_api.config import LANGGRAPH_AUTH_TYPE
|
|
11
|
+
from langgraph_api.config import LANGGRAPH_AUTH, LANGGRAPH_AUTH_TYPE
|
|
12
12
|
|
|
13
13
|
logger = structlog.stdlib.get_logger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def get_auth_backend():
|
|
17
17
|
logger.info(f"Using auth of type={LANGGRAPH_AUTH_TYPE}")
|
|
18
|
+
if LANGGRAPH_AUTH:
|
|
19
|
+
from langgraph_api.auth.custom import get_custom_auth_middleware
|
|
20
|
+
|
|
21
|
+
return get_custom_auth_middleware()
|
|
22
|
+
|
|
18
23
|
if LANGGRAPH_AUTH_TYPE == "langsmith":
|
|
19
24
|
from langgraph_api.auth.langsmith.backend import LangsmithAuthBackend
|
|
20
25
|
|
|
21
26
|
return LangsmithAuthBackend()
|
|
22
27
|
|
|
23
|
-
if LANGGRAPH_AUTH_TYPE == "custom":
|
|
24
|
-
from langgraph_api.auth.custom import get_custom_auth_middleware
|
|
25
|
-
|
|
26
|
-
return get_custom_auth_middleware()
|
|
27
|
-
|
|
28
28
|
from langgraph_api.auth.noop import NoopAuthBackend
|
|
29
29
|
|
|
30
30
|
return NoopAuthBackend()
|
langgraph_api/config.py
CHANGED
|
@@ -51,9 +51,6 @@ LANGSMITH_AUTH_VERIFY_TENANT_ID = env(
|
|
|
51
51
|
default=LANGSMITH_TENANT_ID is not None,
|
|
52
52
|
)
|
|
53
53
|
|
|
54
|
-
if LANGGRAPH_AUTH:
|
|
55
|
-
LANGGRAPH_AUTH_TYPE = "custom"
|
|
56
|
-
|
|
57
54
|
|
|
58
55
|
if LANGGRAPH_AUTH_TYPE == "langsmith":
|
|
59
56
|
LANGSMITH_AUTH_ENDPOINT = env("LANGSMITH_AUTH_ENDPOINT", cast=str)
|
langgraph_api/graph.py
CHANGED
|
@@ -6,10 +6,11 @@ import inspect
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import sys
|
|
9
|
-
from collections.abc import Callable
|
|
9
|
+
from collections.abc import AsyncIterator, Callable
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
10
11
|
from itertools import filterfalse
|
|
11
12
|
from random import choice
|
|
12
|
-
from typing import TYPE_CHECKING, NamedTuple
|
|
13
|
+
from typing import TYPE_CHECKING, Any, NamedTuple
|
|
13
14
|
from uuid import UUID, uuid5
|
|
14
15
|
|
|
15
16
|
import structlog
|
|
@@ -59,39 +60,58 @@ async def register_graph(graph_id: str, graph: GraphValue, config: dict | None)
|
|
|
59
60
|
)
|
|
60
61
|
|
|
61
62
|
|
|
63
|
+
@asynccontextmanager
|
|
64
|
+
async def _generate_graph(value: Any) -> AsyncIterator[Any]:
|
|
65
|
+
"""Yield a graph object regardless of its type."""
|
|
66
|
+
if isinstance(value, Pregel | RemotePregel):
|
|
67
|
+
yield value
|
|
68
|
+
elif hasattr(value, "__aenter__") and hasattr(value, "__aexit__"):
|
|
69
|
+
async with value as ctx_value:
|
|
70
|
+
yield ctx_value
|
|
71
|
+
elif hasattr(value, "__enter__") and hasattr(value, "__exit__"):
|
|
72
|
+
with value as ctx_value:
|
|
73
|
+
yield ctx_value
|
|
74
|
+
elif asyncio.iscoroutine(value):
|
|
75
|
+
yield await value
|
|
76
|
+
else:
|
|
77
|
+
yield value
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@asynccontextmanager
|
|
62
81
|
async def get_graph(
|
|
63
82
|
graph_id: str,
|
|
64
83
|
config: Config,
|
|
65
84
|
*,
|
|
66
85
|
checkpointer: BaseCheckpointSaver | None = None,
|
|
67
86
|
store: BaseStore | None = None,
|
|
68
|
-
) -> Pregel:
|
|
87
|
+
) -> AsyncIterator[Pregel]:
|
|
69
88
|
"""Return the runnable."""
|
|
70
89
|
assert_graph_exists(graph_id)
|
|
71
90
|
value = GRAPHS[graph_id]
|
|
72
91
|
if graph_id in FACTORY_ACCEPTS_CONFIG:
|
|
73
92
|
value = value(config) if FACTORY_ACCEPTS_CONFIG[graph_id] else value()
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
|
|
94
|
+
async with _generate_graph(value) as graph_obj:
|
|
95
|
+
if isinstance(graph_obj, Graph):
|
|
96
|
+
graph_obj = graph_obj.compile()
|
|
97
|
+
if not isinstance(graph_obj, Pregel | RemotePregel):
|
|
98
|
+
raise HTTPException(
|
|
99
|
+
status_code=424,
|
|
100
|
+
detail=f"Graph '{graph_id}' is not valid. Review graph registration.",
|
|
101
|
+
)
|
|
102
|
+
if isinstance(graph_obj, RemotePregel):
|
|
103
|
+
graph_obj.checkpointer = checkpointer
|
|
104
|
+
graph_obj.name = graph_id
|
|
105
|
+
yield graph_obj
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
update = {
|
|
109
|
+
"checkpointer": checkpointer,
|
|
110
|
+
"store": store,
|
|
111
|
+
}
|
|
112
|
+
if graph_obj.name == "LangGraph":
|
|
113
|
+
update["name"] = graph_id
|
|
114
|
+
yield graph_obj.copy(update=update)
|
|
95
115
|
|
|
96
116
|
|
|
97
117
|
def graph_exists(graph_id: str) -> bool:
|
langgraph_api/stream.py
CHANGED
|
@@ -77,11 +77,14 @@ def _map_cmd(cmd: RunCommand) -> Command:
|
|
|
77
77
|
|
|
78
78
|
return Command(
|
|
79
79
|
update=cmd.get("update"),
|
|
80
|
-
goto=
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
goto=(
|
|
81
|
+
[
|
|
82
|
+
it if isinstance(it, str) else Send(it["node"], it["input"])
|
|
83
|
+
for it in goto
|
|
84
|
+
]
|
|
85
|
+
if goto
|
|
86
|
+
else None
|
|
87
|
+
),
|
|
85
88
|
resume=cmd.get("resume"),
|
|
86
89
|
)
|
|
87
90
|
|
|
@@ -104,11 +107,13 @@ async def astream_state(
|
|
|
104
107
|
subgraphs = kwargs.get("subgraphs", False)
|
|
105
108
|
temporary = kwargs.pop("temporary", False)
|
|
106
109
|
config = kwargs.pop("config")
|
|
107
|
-
graph = await
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
graph = await stack.enter_async_context(
|
|
111
|
+
get_graph(
|
|
112
|
+
config["configurable"]["graph_id"],
|
|
113
|
+
config,
|
|
114
|
+
store=Store(),
|
|
115
|
+
checkpointer=None if temporary else Checkpointer(conn),
|
|
116
|
+
)
|
|
112
117
|
)
|
|
113
118
|
input = kwargs.pop("input")
|
|
114
119
|
if cmd := kwargs.pop("command"):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: langgraph-api
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.13
|
|
4
4
|
Summary:
|
|
5
5
|
License: Elastic-2.0
|
|
6
6
|
Author: Nuno Campos
|
|
@@ -16,7 +16,7 @@ Requires-Dist: jsonschema-rs (>=0.25.0,<0.26.0)
|
|
|
16
16
|
Requires-Dist: langchain-core (>=0.2.38,<0.4.0)
|
|
17
17
|
Requires-Dist: langgraph (>=0.2.56,<0.3.0)
|
|
18
18
|
Requires-Dist: langgraph-checkpoint (>=2.0.7,<3.0)
|
|
19
|
-
Requires-Dist: langgraph-sdk (>=0.1.
|
|
19
|
+
Requires-Dist: langgraph-sdk (>=0.1.48,<0.2.0)
|
|
20
20
|
Requires-Dist: langsmith (>=0.1.63,<0.3.0)
|
|
21
21
|
Requires-Dist: orjson (>=3.10.1)
|
|
22
22
|
Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
|
|
2
2
|
langgraph_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
langgraph_api/api/__init__.py,sha256=tlMXuqnyJt99aSlUXwR-dS3w5X6sDDczJu4hbm2LP30,2057
|
|
4
|
-
langgraph_api/api/assistants.py,sha256=
|
|
4
|
+
langgraph_api/api/assistants.py,sha256=Cxryr4K4qFeRoT0gQTu-gld_jSPzMDRZRHoazlSBZVo,11585
|
|
5
5
|
langgraph_api/api/meta.py,sha256=hueasWpTDQ6xYLo9Bzt2jhNH8XQRzreH8FTeFfnRoxQ,2700
|
|
6
|
-
langgraph_api/api/openapi.py,sha256=
|
|
6
|
+
langgraph_api/api/openapi.py,sha256=AUxfnD5hlRp7s-0g2hBC5dNSNk3HTwOLeJiF489DT44,2762
|
|
7
7
|
langgraph_api/api/runs.py,sha256=wAzPXi_kcYB9BcLBL4FXgkBohWwCPIpe4XERnsnWnsA,16042
|
|
8
8
|
langgraph_api/api/store.py,sha256=y7VIejpsE7rpPF-tiMGBqqBwWPZ1wb3o48th6NUvb5I,3849
|
|
9
9
|
langgraph_api/api/threads.py,sha256=taU61XPcCEhBPCYPZcMDsgVDwwWUWJs8p-PrXFXWY48,8661
|
|
10
10
|
langgraph_api/asyncio.py,sha256=XiFEllu-Kg4zAO084npHPYOPnLQRire3V75XrVQYMxE,6023
|
|
11
11
|
langgraph_api/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
langgraph_api/auth/custom.py,sha256=
|
|
12
|
+
langgraph_api/auth/custom.py,sha256=_BlII18X8ji_t8qA9FIUq_ip99dMQGkNscjsuEiLHus,20527
|
|
13
13
|
langgraph_api/auth/langsmith/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
-
langgraph_api/auth/langsmith/backend.py,sha256=
|
|
14
|
+
langgraph_api/auth/langsmith/backend.py,sha256=InScaL-HYCnxYEauhxU198gRZV9pJn9SzzBoR9Edn7g,2654
|
|
15
15
|
langgraph_api/auth/langsmith/client.py,sha256=eKchvAom7hdkUXauD8vHNceBDDUijrFgdTV8bKd7x4Q,3998
|
|
16
|
-
langgraph_api/auth/middleware.py,sha256=
|
|
16
|
+
langgraph_api/auth/middleware.py,sha256=qc7SbaFoeWaqxS1wbjZ2PPQ4iI2p9T0shWL7c6g0ed4,1636
|
|
17
17
|
langgraph_api/auth/noop.py,sha256=vDJmzG2vArJxVzdHePvrJWahEa0dvGnhc2LEMMeiFz0,391
|
|
18
|
+
langgraph_api/auth/studio_user.py,sha256=FzFQRROKDlA9JjtBuwyZvk6Mbwno5M9RVYjDO6FU3F8,186
|
|
18
19
|
langgraph_api/cli.py,sha256=7vQQiD3F50r-8KkbuFjwIz8LLbdKUTd4xZGUJPiO3yQ,11688
|
|
19
|
-
langgraph_api/config.py,sha256=
|
|
20
|
+
langgraph_api/config.py,sha256=JueNW95UDn7uId3atctyC9BPZTzmUqwF2Gtr2i-MZ-g,2739
|
|
20
21
|
langgraph_api/cron_scheduler.py,sha256=CybK-9Jwopi_scObTHRyB7gyb0KjC4gqaT2GLe-WOFg,2587
|
|
21
22
|
langgraph_api/errors.py,sha256=Bu_i5drgNTyJcLiyrwVE_6-XrSU50BHf9TDpttki9wQ,1690
|
|
22
|
-
langgraph_api/graph.py,sha256=
|
|
23
|
+
langgraph_api/graph.py,sha256=zjcjgtlvfPIEJws8RksC6wKvt3g-8IGaJj79ScSs6GE,16561
|
|
23
24
|
langgraph_api/http.py,sha256=XrbyxpjtfSvnaWWh5ZLGpgZmY83WoDCrP_1GPguNiXI,4712
|
|
24
25
|
langgraph_api/http_logger.py,sha256=Sxo_q-65tElauRvkzVLt9lJojgNdgtcHGBYD0IRyX7M,3146
|
|
25
26
|
langgraph_api/js/.gitignore,sha256=qAah3Fq0HWAlfRj5ktZyC6QRQIsAolGLRGcRukA1XJI,33
|
|
@@ -64,7 +65,7 @@ langgraph_api/serde.py,sha256=VoJ7Z1IuqrQGXFzEP1qijAITtWCrmjtVqlCRuScjXJI,3533
|
|
|
64
65
|
langgraph_api/server.py,sha256=afHDnL6b_fAIu_q4icnK60a74lHTTZOMIe1egdhRXIk,1522
|
|
65
66
|
langgraph_api/sse.py,sha256=2wNodCOP2eg7a9mpSu0S3FQ0CHk2BBV_vv0UtIgJIcc,4034
|
|
66
67
|
langgraph_api/state.py,sha256=8jx4IoTCOjTJuwzuXJKKFwo1VseHjNnw_CCq4x1SW14,2284
|
|
67
|
-
langgraph_api/stream.py,sha256=
|
|
68
|
+
langgraph_api/stream.py,sha256=uK1MFr3hp08o3yE-W5V36CljaPmo97VfpDtexa5eqOQ,11663
|
|
68
69
|
langgraph_api/utils.py,sha256=o7TFlY25IjujeKdXgtyE2mMLPETIlrbOc3w6giYBq2Y,2509
|
|
69
70
|
langgraph_api/validation.py,sha256=McizHlz-Ez8Jhdbc79mbPSde7GIuf2Jlbjx2yv_l6dA,4475
|
|
70
71
|
langgraph_license/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -73,15 +74,15 @@ langgraph_license/validation.py,sha256=Uu_G8UGO_WTlLsBEY0gTVWjRR4czYGfw5YAD3HLZo
|
|
|
73
74
|
langgraph_storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
74
75
|
langgraph_storage/checkpoint.py,sha256=V4t2GwYEJdPCHbhq_4Udhlv0TWKDzlMu_rlNPdTDc50,3589
|
|
75
76
|
langgraph_storage/database.py,sha256=Nr5zE9Fur3-tESkqe7xNXMf2QlBuw3H0CUie7jVa6Q4,6003
|
|
76
|
-
langgraph_storage/ops.py,sha256=
|
|
77
|
+
langgraph_storage/ops.py,sha256=vinc095b_eZYSWAfK_trZbmb_IIGcp55lWJEzqRwTPU,67967
|
|
77
78
|
langgraph_storage/queue.py,sha256=6cTZ0ubHu3S1T43yxHMVOwsQsDaJupByiU0sTUFFls8,3261
|
|
78
79
|
langgraph_storage/retry.py,sha256=uvYFuXJ-T6S1QY1ZwkZHyZQbsvS-Ab68LSbzbUUSI2E,696
|
|
79
80
|
langgraph_storage/store.py,sha256=D-p3cWc_umamkKp-6Cz3cAriSACpvM5nxUIvND6PuxE,2710
|
|
80
81
|
langgraph_storage/ttl_dict.py,sha256=FlpEY8EANeXWKo_G5nmIotPquABZGyIJyk6HD9u6vqY,1533
|
|
81
82
|
logging.json,sha256=3RNjSADZmDq38eHePMm1CbP6qZ71AmpBtLwCmKU9Zgo,379
|
|
82
|
-
openapi.json,sha256=
|
|
83
|
-
langgraph_api-0.0.
|
|
84
|
-
langgraph_api-0.0.
|
|
85
|
-
langgraph_api-0.0.
|
|
86
|
-
langgraph_api-0.0.
|
|
87
|
-
langgraph_api-0.0.
|
|
83
|
+
openapi.json,sha256=gh6FxpyQqspAuQQH3O22qqGW5owtFj45gyR15QAcS9k,124729
|
|
84
|
+
langgraph_api-0.0.13.dist-info/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
|
|
85
|
+
langgraph_api-0.0.13.dist-info/METADATA,sha256=F_D67LIP0IP8jFVJ5wUwieyIqFuAP_mtrDNe8uZS8Rs,4041
|
|
86
|
+
langgraph_api-0.0.13.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
87
|
+
langgraph_api-0.0.13.dist-info/entry_points.txt,sha256=3EYLgj89DfzqJHHYGxPH4A_fEtClvlRbWRUHaXO7hj4,77
|
|
88
|
+
langgraph_api-0.0.13.dist-info/RECORD,,
|
langgraph_storage/ops.py
CHANGED
|
@@ -976,17 +976,17 @@ class Threads(Authenticated):
|
|
|
976
976
|
if graph_id := metadata.get("graph_id"):
|
|
977
977
|
# format latest checkpoint for response
|
|
978
978
|
checkpointer.latest_iter = checkpoint
|
|
979
|
-
|
|
979
|
+
async with get_graph(
|
|
980
980
|
graph_id, thread_config, checkpointer=checkpointer
|
|
981
|
-
)
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
981
|
+
) as graph:
|
|
982
|
+
result = await graph.aget_state(config, subgraphs=subgraphs)
|
|
983
|
+
if (
|
|
984
|
+
result.metadata is not None
|
|
985
|
+
and "checkpoint_ns" in result.metadata
|
|
986
|
+
and result.metadata["checkpoint_ns"] == ""
|
|
987
|
+
):
|
|
988
|
+
result.metadata.pop("checkpoint_ns")
|
|
989
|
+
return result
|
|
990
990
|
else:
|
|
991
991
|
return StateSnapshot(
|
|
992
992
|
values={},
|
|
@@ -1034,32 +1034,36 @@ class Threads(Authenticated):
|
|
|
1034
1034
|
config["configurable"].setdefault("graph_id", graph_id)
|
|
1035
1035
|
|
|
1036
1036
|
checkpointer.latest_iter = checkpoint
|
|
1037
|
-
|
|
1037
|
+
async with get_graph(
|
|
1038
1038
|
graph_id, thread_config, checkpointer=checkpointer
|
|
1039
|
-
)
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1039
|
+
) as graph:
|
|
1040
|
+
update_config = config.copy()
|
|
1041
|
+
update_config["configurable"] = {
|
|
1042
|
+
**config["configurable"],
|
|
1043
|
+
"checkpoint_ns": config["configurable"].get(
|
|
1044
|
+
"checkpoint_ns", ""
|
|
1045
|
+
),
|
|
1046
|
+
}
|
|
1047
|
+
next_config = await graph.aupdate_state(
|
|
1048
|
+
update_config, values, as_node=as_node
|
|
1049
|
+
)
|
|
1048
1050
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1051
|
+
# Get current state
|
|
1052
|
+
state = await Threads.State.get(
|
|
1053
|
+
conn, config, subgraphs=False, ctx=ctx
|
|
1054
|
+
)
|
|
1055
|
+
# Update thread values
|
|
1056
|
+
for thread in conn.store["threads"]:
|
|
1057
|
+
if thread["thread_id"] == thread_id:
|
|
1058
|
+
thread["values"] = state.values
|
|
1059
|
+
break
|
|
1060
|
+
|
|
1061
|
+
return ThreadUpdateResponse(
|
|
1062
|
+
checkpoint=next_config["configurable"],
|
|
1063
|
+
# Including deprecated fields
|
|
1064
|
+
configurable=next_config["configurable"],
|
|
1065
|
+
checkpoint_id=next_config["configurable"]["checkpoint_id"],
|
|
1066
|
+
)
|
|
1063
1067
|
else:
|
|
1064
1068
|
raise HTTPException(status_code=400, detail="Thread has no graph ID.")
|
|
1065
1069
|
|
|
@@ -1094,25 +1098,24 @@ class Threads(Authenticated):
|
|
|
1094
1098
|
thread_config = thread["config"]
|
|
1095
1099
|
# If graph_id exists, get state history
|
|
1096
1100
|
if graph_id := thread_metadata.get("graph_id"):
|
|
1097
|
-
|
|
1101
|
+
async with get_graph(
|
|
1098
1102
|
graph_id, thread_config, checkpointer=Checkpointer(conn)
|
|
1099
|
-
)
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
else before
|
|
1106
|
-
)
|
|
1107
|
-
|
|
1108
|
-
states = [
|
|
1109
|
-
state
|
|
1110
|
-
async for state in graph.aget_state_history(
|
|
1111
|
-
config, limit=limit, filter=metadata, before=before_param
|
|
1103
|
+
) as graph:
|
|
1104
|
+
# Convert before parameter if it's a string
|
|
1105
|
+
before_param = (
|
|
1106
|
+
{"configurable": {"checkpoint_id": before}}
|
|
1107
|
+
if isinstance(before, str)
|
|
1108
|
+
else before
|
|
1112
1109
|
)
|
|
1113
|
-
]
|
|
1114
1110
|
|
|
1115
|
-
|
|
1111
|
+
states = [
|
|
1112
|
+
state
|
|
1113
|
+
async for state in graph.aget_state_history(
|
|
1114
|
+
config, limit=limit, filter=metadata, before=before_param
|
|
1115
|
+
)
|
|
1116
|
+
]
|
|
1117
|
+
|
|
1118
|
+
return states
|
|
1116
1119
|
|
|
1117
1120
|
return []
|
|
1118
1121
|
|
openapi.json
CHANGED
|
@@ -3242,7 +3242,11 @@
|
|
|
3242
3242
|
"description": "The command to run.",
|
|
3243
3243
|
"properties": {
|
|
3244
3244
|
"update": {
|
|
3245
|
-
"type":
|
|
3245
|
+
"type": [
|
|
3246
|
+
"object",
|
|
3247
|
+
"array",
|
|
3248
|
+
"null"
|
|
3249
|
+
],
|
|
3246
3250
|
"title": "Update",
|
|
3247
3251
|
"description": "An update to the state."
|
|
3248
3252
|
},
|
|
@@ -3268,13 +3272,22 @@
|
|
|
3268
3272
|
"$ref": "#/components/schemas/Send"
|
|
3269
3273
|
}
|
|
3270
3274
|
},
|
|
3271
|
-
{
|
|
3272
|
-
|
|
3273
|
-
|
|
3275
|
+
{
|
|
3276
|
+
"type": "string"
|
|
3277
|
+
},
|
|
3278
|
+
{
|
|
3279
|
+
"type": "array",
|
|
3280
|
+
"items": {
|
|
3281
|
+
"type": "string"
|
|
3282
|
+
}
|
|
3283
|
+
},
|
|
3284
|
+
{
|
|
3285
|
+
"type": "null"
|
|
3286
|
+
}
|
|
3274
3287
|
],
|
|
3275
3288
|
"title": "Goto",
|
|
3276
3289
|
"description": "Name of the node(s) to navigate to next or node(s) to be executed with a provided input."
|
|
3277
|
-
|
|
3290
|
+
}
|
|
3278
3291
|
}
|
|
3279
3292
|
},
|
|
3280
3293
|
"RunCreateStateful": {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|