langgraph-api 0.4.45__py3-none-any.whl → 0.4.47__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.
Potentially problematic release.
This version of langgraph-api might be problematic. Click here for more details.
- langgraph_api/__init__.py +1 -1
- langgraph_api/api/threads.py +11 -7
- langgraph_api/cli.py +2 -49
- langgraph_api/config.py +75 -15
- langgraph_api/grpc_ops/client.py +13 -1
- langgraph_api/grpc_ops/generated/core_api_pb2.py +195 -193
- langgraph_api/grpc_ops/generated/core_api_pb2.pyi +18 -12
- langgraph_api/grpc_ops/generated/core_api_pb2_grpc.py +2 -2
- langgraph_api/grpc_ops/ops.py +582 -5
- langgraph_api/js/build.mts +1 -1
- langgraph_api/js/client.http.mts +1 -1
- langgraph_api/js/client.mts +1 -1
- langgraph_api/js/package.json +7 -7
- langgraph_api/js/yarn.lock +40 -48
- {langgraph_api-0.4.45.dist-info → langgraph_api-0.4.47.dist-info}/METADATA +2 -2
- {langgraph_api-0.4.45.dist-info → langgraph_api-0.4.47.dist-info}/RECORD +19 -19
- {langgraph_api-0.4.45.dist-info → langgraph_api-0.4.47.dist-info}/WHEEL +0 -0
- {langgraph_api-0.4.45.dist-info → langgraph_api-0.4.47.dist-info}/entry_points.txt +0 -0
- {langgraph_api-0.4.45.dist-info → langgraph_api-0.4.47.dist-info}/licenses/LICENSE +0 -0
langgraph_api/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.4.
|
|
1
|
+
__version__ = "0.4.47"
|
langgraph_api/api/threads.py
CHANGED
|
@@ -5,6 +5,8 @@ from starlette.exceptions import HTTPException
|
|
|
5
5
|
from starlette.responses import Response
|
|
6
6
|
from starlette.routing import BaseRoute
|
|
7
7
|
|
|
8
|
+
from langgraph_api.feature_flags import FF_USE_CORE_API
|
|
9
|
+
from langgraph_api.grpc_ops.ops import Threads as GrpcThreads
|
|
8
10
|
from langgraph_api.route import ApiRequest, ApiResponse, ApiRoute
|
|
9
11
|
from langgraph_api.schema import THREAD_FIELDS, ThreadStreamMode
|
|
10
12
|
from langgraph_api.sse import EventSourceResponse
|
|
@@ -30,6 +32,8 @@ from langgraph_runtime.database import connect
|
|
|
30
32
|
from langgraph_runtime.ops import Threads
|
|
31
33
|
from langgraph_runtime.retry import retry_db
|
|
32
34
|
|
|
35
|
+
CrudThreads = GrpcThreads if FF_USE_CORE_API else Threads
|
|
36
|
+
|
|
33
37
|
|
|
34
38
|
@retry_db
|
|
35
39
|
async def create_thread(
|
|
@@ -41,7 +45,7 @@ async def create_thread(
|
|
|
41
45
|
validate_uuid(thread_id, "Invalid thread ID: must be a UUID")
|
|
42
46
|
async with connect() as conn:
|
|
43
47
|
thread_id = thread_id or str(uuid4())
|
|
44
|
-
iter = await
|
|
48
|
+
iter = await CrudThreads.put(
|
|
45
49
|
conn,
|
|
46
50
|
thread_id,
|
|
47
51
|
metadata=payload.get("metadata"),
|
|
@@ -78,7 +82,7 @@ async def search_threads(
|
|
|
78
82
|
limit = int(payload.get("limit") or 10)
|
|
79
83
|
offset = int(payload.get("offset") or 0)
|
|
80
84
|
async with connect() as conn:
|
|
81
|
-
threads_iter, next_offset = await
|
|
85
|
+
threads_iter, next_offset = await CrudThreads.search(
|
|
82
86
|
conn,
|
|
83
87
|
status=payload.get("status"),
|
|
84
88
|
values=payload.get("values"),
|
|
@@ -103,7 +107,7 @@ async def count_threads(
|
|
|
103
107
|
"""Count threads."""
|
|
104
108
|
payload = await request.json(ThreadCountRequest)
|
|
105
109
|
async with connect() as conn:
|
|
106
|
-
count = await
|
|
110
|
+
count = await CrudThreads.count(
|
|
107
111
|
conn,
|
|
108
112
|
status=payload.get("status"),
|
|
109
113
|
values=payload.get("values"),
|
|
@@ -277,7 +281,7 @@ async def get_thread(
|
|
|
277
281
|
thread_id = request.path_params["thread_id"]
|
|
278
282
|
validate_uuid(thread_id, "Invalid thread ID: must be a UUID")
|
|
279
283
|
async with connect() as conn:
|
|
280
|
-
thread = await
|
|
284
|
+
thread = await CrudThreads.get(conn, thread_id)
|
|
281
285
|
return ApiResponse(await fetchone(thread))
|
|
282
286
|
|
|
283
287
|
|
|
@@ -290,7 +294,7 @@ async def patch_thread(
|
|
|
290
294
|
validate_uuid(thread_id, "Invalid thread ID: must be a UUID")
|
|
291
295
|
payload = await request.json(ThreadPatch)
|
|
292
296
|
async with connect() as conn:
|
|
293
|
-
thread = await
|
|
297
|
+
thread = await CrudThreads.patch(
|
|
294
298
|
conn,
|
|
295
299
|
thread_id,
|
|
296
300
|
metadata=payload.get("metadata", {}),
|
|
@@ -305,7 +309,7 @@ async def delete_thread(request: ApiRequest):
|
|
|
305
309
|
thread_id = request.path_params["thread_id"]
|
|
306
310
|
validate_uuid(thread_id, "Invalid thread ID: must be a UUID")
|
|
307
311
|
async with connect() as conn:
|
|
308
|
-
tid = await
|
|
312
|
+
tid = await CrudThreads.delete(conn, thread_id)
|
|
309
313
|
await fetchone(tid)
|
|
310
314
|
return Response(status_code=204)
|
|
311
315
|
|
|
@@ -314,7 +318,7 @@ async def delete_thread(request: ApiRequest):
|
|
|
314
318
|
async def copy_thread(request: ApiRequest):
|
|
315
319
|
thread_id = request.path_params["thread_id"]
|
|
316
320
|
async with connect() as conn:
|
|
317
|
-
iter = await
|
|
321
|
+
iter = await CrudThreads.copy(conn, thread_id)
|
|
318
322
|
return ApiResponse(await fetchone(iter, not_found_code=409))
|
|
319
323
|
|
|
320
324
|
|
langgraph_api/cli.py
CHANGED
|
@@ -8,12 +8,10 @@ import typing
|
|
|
8
8
|
from collections.abc import Mapping, Sequence
|
|
9
9
|
from typing import Literal
|
|
10
10
|
|
|
11
|
-
from typing_extensions import TypedDict
|
|
12
|
-
|
|
13
11
|
if typing.TYPE_CHECKING:
|
|
14
12
|
from packaging.version import Version
|
|
15
13
|
|
|
16
|
-
from langgraph_api.config import HttpConfig, StoreConfig
|
|
14
|
+
from langgraph_api.config import AuthConfig, HttpConfig, StoreConfig
|
|
17
15
|
|
|
18
16
|
logging.basicConfig(level=logging.INFO)
|
|
19
17
|
logger = logging.getLogger(__name__)
|
|
@@ -81,51 +79,6 @@ def patch_environment(**kwargs):
|
|
|
81
79
|
os.environ[key] = value
|
|
82
80
|
|
|
83
81
|
|
|
84
|
-
class SecurityConfig(TypedDict, total=False):
|
|
85
|
-
securitySchemes: dict
|
|
86
|
-
security: list
|
|
87
|
-
# path => {method => security}
|
|
88
|
-
paths: dict[str, dict[str, list]]
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
class CacheConfig(TypedDict, total=False):
|
|
92
|
-
cache_keys: list[str]
|
|
93
|
-
ttl_seconds: int
|
|
94
|
-
max_size: int
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class AuthConfig(TypedDict, total=False):
|
|
98
|
-
path: str
|
|
99
|
-
"""Path to the authentication function in a Python file."""
|
|
100
|
-
disable_studio_auth: bool
|
|
101
|
-
"""Whether to disable auth when connecting from the LangSmith Studio."""
|
|
102
|
-
openapi: SecurityConfig
|
|
103
|
-
"""The schema to use for updating the openapi spec.
|
|
104
|
-
|
|
105
|
-
Example:
|
|
106
|
-
{
|
|
107
|
-
"securitySchemes": {
|
|
108
|
-
"OAuth2": {
|
|
109
|
-
"type": "oauth2",
|
|
110
|
-
"flows": {
|
|
111
|
-
"password": {
|
|
112
|
-
"tokenUrl": "/token",
|
|
113
|
-
"scopes": {
|
|
114
|
-
"me": "Read information about the current user",
|
|
115
|
-
"items": "Access to create and manage items"
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
"security": [
|
|
122
|
-
{"OAuth2": ["me"]} # Default security requirement for all endpoints
|
|
123
|
-
]
|
|
124
|
-
}
|
|
125
|
-
"""
|
|
126
|
-
cache: CacheConfig | None
|
|
127
|
-
|
|
128
|
-
|
|
129
82
|
def run_server(
|
|
130
83
|
host: str = "127.0.0.1",
|
|
131
84
|
port: int = 2024,
|
|
@@ -141,7 +94,7 @@ def run_server(
|
|
|
141
94
|
reload_includes: Sequence[str] | None = None,
|
|
142
95
|
reload_excludes: Sequence[str] | None = None,
|
|
143
96
|
store: typing.Optional["StoreConfig"] = None,
|
|
144
|
-
auth: AuthConfig
|
|
97
|
+
auth: typing.Optional["AuthConfig"] = None,
|
|
145
98
|
http: typing.Optional["HttpConfig"] = None,
|
|
146
99
|
ui: dict | None = None,
|
|
147
100
|
ui_config: dict | None = None,
|
langgraph_api/config.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from collections.abc import Callable
|
|
2
3
|
from os import environ, getenv
|
|
3
|
-
from typing import Literal
|
|
4
|
+
from typing import Literal, TypeVar, cast
|
|
4
5
|
|
|
5
6
|
import orjson
|
|
7
|
+
from pydantic import TypeAdapter
|
|
6
8
|
from starlette.config import Config, undefined
|
|
7
9
|
from starlette.datastructures import CommaSeparatedStrings
|
|
8
10
|
from typing_extensions import TypedDict
|
|
@@ -22,11 +24,14 @@ class CorsConfig(TypedDict, total=False):
|
|
|
22
24
|
max_age: int
|
|
23
25
|
|
|
24
26
|
|
|
25
|
-
class ConfigurableHeaders(TypedDict):
|
|
27
|
+
class ConfigurableHeaders(TypedDict, total=False):
|
|
26
28
|
includes: list[str] | None
|
|
27
29
|
excludes: list[str] | None
|
|
28
30
|
|
|
29
31
|
|
|
32
|
+
MiddlewareOrders = Literal["auth_first", "middleware_first"]
|
|
33
|
+
|
|
34
|
+
|
|
30
35
|
class HttpConfig(TypedDict, total=False):
|
|
31
36
|
app: str
|
|
32
37
|
"""Import path for a custom Starlette/FastAPI app to mount"""
|
|
@@ -52,6 +57,8 @@ class HttpConfig(TypedDict, total=False):
|
|
|
52
57
|
"""Prefix for mounted routes. E.g., "/my-deployment/api"."""
|
|
53
58
|
configurable_headers: ConfigurableHeaders | None
|
|
54
59
|
logging_headers: ConfigurableHeaders | None
|
|
60
|
+
enable_custom_route_auth: bool
|
|
61
|
+
middleware_order: MiddlewareOrders | None
|
|
55
62
|
|
|
56
63
|
|
|
57
64
|
class ThreadTTLConfig(TypedDict, total=False):
|
|
@@ -135,18 +142,72 @@ class CheckpointerConfig(TypedDict, total=False):
|
|
|
135
142
|
"""
|
|
136
143
|
|
|
137
144
|
|
|
145
|
+
class SecurityConfig(TypedDict, total=False):
|
|
146
|
+
securitySchemes: dict
|
|
147
|
+
security: list
|
|
148
|
+
# path => {method => security}
|
|
149
|
+
paths: dict[str, dict[str, list]]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class CacheConfig(TypedDict, total=False):
|
|
153
|
+
cache_keys: list[str]
|
|
154
|
+
ttl_seconds: int
|
|
155
|
+
max_size: int
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class AuthConfig(TypedDict, total=False):
|
|
159
|
+
path: str
|
|
160
|
+
"""Path to the authentication function in a Python file."""
|
|
161
|
+
disable_studio_auth: bool
|
|
162
|
+
"""Whether to disable auth when connecting from the LangSmith Studio."""
|
|
163
|
+
openapi: SecurityConfig
|
|
164
|
+
"""The schema to use for updating the openapi spec.
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
{
|
|
168
|
+
"securitySchemes": {
|
|
169
|
+
"OAuth2": {
|
|
170
|
+
"type": "oauth2",
|
|
171
|
+
"flows": {
|
|
172
|
+
"password": {
|
|
173
|
+
"tokenUrl": "/token",
|
|
174
|
+
"scopes": {
|
|
175
|
+
"me": "Read information about the current user",
|
|
176
|
+
"items": "Access to create and manage items"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
"security": [
|
|
183
|
+
{"OAuth2": ["me"]} # Default security requirement for all endpoints
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
"""
|
|
187
|
+
cache: CacheConfig | None
|
|
188
|
+
|
|
189
|
+
|
|
138
190
|
# env
|
|
139
191
|
|
|
140
192
|
env = Config()
|
|
141
193
|
|
|
142
194
|
|
|
143
|
-
|
|
195
|
+
TD = TypeVar("TD")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _parse_json(json: str | None, schema: TypeAdapter | None = None) -> dict | None:
|
|
144
199
|
if not json:
|
|
145
200
|
return None
|
|
146
|
-
parsed = orjson.loads(json)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
201
|
+
parsed = schema.validate_json(json) if schema else orjson.loads(json)
|
|
202
|
+
return parsed or None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _parse_schema(schema: type[TD]) -> Callable[[str | None], TD | None]:
|
|
206
|
+
def composed(json: str | None) -> TD | None:
|
|
207
|
+
return cast(TD | None, _parse_json(json, schema=TypeAdapter(schema)))
|
|
208
|
+
|
|
209
|
+
composed.__name__ = schema.__name__ # This just gives a nicer error message if the user provides an incompatible value
|
|
210
|
+
return composed
|
|
150
211
|
|
|
151
212
|
|
|
152
213
|
STATS_INTERVAL_SECS = env("STATS_INTERVAL_SECS", cast=int, default=60)
|
|
@@ -189,17 +250,15 @@ ALLOW_PRIVATE_NETWORK = env("ALLOW_PRIVATE_NETWORK", cast=bool, default=False)
|
|
|
189
250
|
See https://developer.chrome.com/blog/private-network-access-update-2024-03
|
|
190
251
|
"""
|
|
191
252
|
|
|
192
|
-
HTTP_CONFIG
|
|
193
|
-
STORE_CONFIG
|
|
194
|
-
"LANGGRAPH_STORE", cast=_parse_json, default=None
|
|
195
|
-
)
|
|
253
|
+
HTTP_CONFIG = env("LANGGRAPH_HTTP", cast=_parse_schema(HttpConfig), default=None)
|
|
254
|
+
STORE_CONFIG = env("LANGGRAPH_STORE", cast=_parse_schema(StoreConfig), default=None)
|
|
196
255
|
|
|
197
256
|
MOUNT_PREFIX: str | None = env("MOUNT_PREFIX", cast=str, default=None) or (
|
|
198
257
|
HTTP_CONFIG.get("mount_prefix") if HTTP_CONFIG else None
|
|
199
258
|
)
|
|
200
259
|
|
|
201
260
|
CORS_ALLOW_ORIGINS = env("CORS_ALLOW_ORIGINS", cast=CommaSeparatedStrings, default="*")
|
|
202
|
-
CORS_CONFIG
|
|
261
|
+
CORS_CONFIG = env("CORS_CONFIG", cast=_parse_schema(CorsConfig), default=None) or (
|
|
203
262
|
HTTP_CONFIG.get("cors") if HTTP_CONFIG else None
|
|
204
263
|
)
|
|
205
264
|
"""
|
|
@@ -277,8 +336,8 @@ def _parse_thread_ttl(value: str | None) -> ThreadTTLConfig | None:
|
|
|
277
336
|
}
|
|
278
337
|
|
|
279
338
|
|
|
280
|
-
CHECKPOINTER_CONFIG
|
|
281
|
-
"LANGGRAPH_CHECKPOINTER", cast=
|
|
339
|
+
CHECKPOINTER_CONFIG = env(
|
|
340
|
+
"LANGGRAPH_CHECKPOINTER", cast=_parse_schema(CheckpointerConfig), default=None
|
|
282
341
|
)
|
|
283
342
|
THREAD_TTL: ThreadTTLConfig | None = env(
|
|
284
343
|
"LANGGRAPH_THREAD_TTL", cast=_parse_thread_ttl, default=None
|
|
@@ -292,6 +351,7 @@ BG_JOB_TIMEOUT_SECS = env("BG_JOB_TIMEOUT_SECS", cast=float, default=3600)
|
|
|
292
351
|
FF_CRONS_ENABLED = env("FF_CRONS_ENABLED", cast=bool, default=True)
|
|
293
352
|
FF_RICH_THREADS = env("FF_RICH_THREADS", cast=bool, default=True)
|
|
294
353
|
FF_LOG_DROPPED_EVENTS = env("FF_LOG_DROPPED_EVENTS", cast=bool, default=False)
|
|
354
|
+
FF_LOG_QUERY_AND_PARAMS = env("FF_LOG_QUERY_AND_PARAMS", cast=bool, default=False)
|
|
295
355
|
|
|
296
356
|
# auth
|
|
297
357
|
|
|
@@ -303,7 +363,7 @@ if LANGGRAPH_POSTGRES_EXTENSIONS not in ("standard", "lite"):
|
|
|
303
363
|
raise ValueError(
|
|
304
364
|
f"Unknown LANGGRAPH_POSTGRES_EXTENSIONS value: {LANGGRAPH_POSTGRES_EXTENSIONS}"
|
|
305
365
|
)
|
|
306
|
-
LANGGRAPH_AUTH = env("LANGGRAPH_AUTH", cast=
|
|
366
|
+
LANGGRAPH_AUTH = env("LANGGRAPH_AUTH", cast=_parse_schema(AuthConfig), default=None)
|
|
307
367
|
LANGSMITH_TENANT_ID = env("LANGSMITH_TENANT_ID", cast=str, default=None)
|
|
308
368
|
LANGSMITH_AUTH_VERIFY_TENANT_ID = env(
|
|
309
369
|
"LANGSMITH_AUTH_VERIFY_TENANT_ID",
|
langgraph_api/grpc_ops/client.py
CHANGED
|
@@ -5,7 +5,7 @@ import os
|
|
|
5
5
|
import structlog
|
|
6
6
|
from grpc import aio # type: ignore[import]
|
|
7
7
|
|
|
8
|
-
from .generated.core_api_pb2_grpc import AdminStub, AssistantsStub
|
|
8
|
+
from .generated.core_api_pb2_grpc import AdminStub, AssistantsStub, ThreadsStub
|
|
9
9
|
|
|
10
10
|
logger = structlog.stdlib.get_logger(__name__)
|
|
11
11
|
|
|
@@ -27,6 +27,7 @@ class GrpcClient:
|
|
|
27
27
|
)
|
|
28
28
|
self._channel: aio.Channel | None = None
|
|
29
29
|
self._assistants_stub: AssistantsStub | None = None
|
|
30
|
+
self._threads_stub: ThreadsStub | None = None
|
|
30
31
|
self._admin_stub: AdminStub | None = None
|
|
31
32
|
|
|
32
33
|
async def __aenter__(self):
|
|
@@ -46,6 +47,7 @@ class GrpcClient:
|
|
|
46
47
|
self._channel = aio.insecure_channel(self.server_address)
|
|
47
48
|
|
|
48
49
|
self._assistants_stub = AssistantsStub(self._channel)
|
|
50
|
+
self._threads_stub = ThreadsStub(self._channel)
|
|
49
51
|
self._admin_stub = AdminStub(self._channel)
|
|
50
52
|
|
|
51
53
|
await logger.adebug(
|
|
@@ -58,6 +60,7 @@ class GrpcClient:
|
|
|
58
60
|
await self._channel.close()
|
|
59
61
|
self._channel = None
|
|
60
62
|
self._assistants_stub = None
|
|
63
|
+
self._threads_stub = None
|
|
61
64
|
self._admin_stub = None
|
|
62
65
|
await logger.adebug("Closed gRPC connection")
|
|
63
66
|
|
|
@@ -70,6 +73,15 @@ class GrpcClient:
|
|
|
70
73
|
)
|
|
71
74
|
return self._assistants_stub
|
|
72
75
|
|
|
76
|
+
@property
|
|
77
|
+
def threads(self) -> ThreadsStub:
|
|
78
|
+
"""Get the threads service stub."""
|
|
79
|
+
if self._threads_stub is None:
|
|
80
|
+
raise RuntimeError(
|
|
81
|
+
"Client not connected. Use async context manager or call connect() first."
|
|
82
|
+
)
|
|
83
|
+
return self._threads_stub
|
|
84
|
+
|
|
73
85
|
@property
|
|
74
86
|
def admin(self) -> AdminStub:
|
|
75
87
|
"""Get the admin service stub."""
|