langgraph-api 0.4.28__py3-none-any.whl → 0.4.29__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/assistants.py +12 -9
- langgraph_api/feature_flags.py +9 -0
- langgraph_api/graph.py +11 -12
- langgraph_api/grpc_ops/__init__.py +0 -0
- langgraph_api/grpc_ops/client.py +80 -0
- langgraph_api/grpc_ops/generated/__init__.py +5 -0
- langgraph_api/grpc_ops/generated/core_api_pb2.py +274 -0
- langgraph_api/grpc_ops/generated/core_api_pb2.pyi +988 -0
- langgraph_api/grpc_ops/generated/core_api_pb2_grpc.py +1400 -0
- langgraph_api/grpc_ops/ops.py +540 -0
- langgraph_api/grpc_ops/scripts/generate_protos.sh +47 -0
- langgraph_api/schema.py +2 -1
- {langgraph_api-0.4.28.dist-info → langgraph_api-0.4.29.dist-info}/METADATA +4 -1
- {langgraph_api-0.4.28.dist-info → langgraph_api-0.4.29.dist-info}/RECORD +18 -10
- {langgraph_api-0.4.28.dist-info → langgraph_api-0.4.29.dist-info}/WHEEL +0 -0
- {langgraph_api-0.4.28.dist-info → langgraph_api-0.4.29.dist-info}/entry_points.txt +0 -0
- {langgraph_api-0.4.28.dist-info → langgraph_api-0.4.29.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
"""gRPC-based operations for LangGraph API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import functools
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from http import HTTPStatus
|
|
9
|
+
from typing import Any
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
import structlog
|
|
13
|
+
from google.protobuf.json_format import MessageToDict
|
|
14
|
+
from google.protobuf.struct_pb2 import Struct # type: ignore[import]
|
|
15
|
+
from grpc import StatusCode
|
|
16
|
+
from grpc.aio import AioRpcError
|
|
17
|
+
from langgraph_sdk.schema import Config
|
|
18
|
+
from starlette.exceptions import HTTPException
|
|
19
|
+
|
|
20
|
+
from langgraph_api.schema import (
|
|
21
|
+
Assistant,
|
|
22
|
+
AssistantSelectField,
|
|
23
|
+
Context,
|
|
24
|
+
MetadataInput,
|
|
25
|
+
OnConflictBehavior,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from .client import GrpcClient
|
|
29
|
+
from .generated import core_api_pb2 as pb
|
|
30
|
+
|
|
31
|
+
GRPC_STATUS_TO_HTTP_STATUS = {
|
|
32
|
+
StatusCode.NOT_FOUND: HTTPStatus.NOT_FOUND,
|
|
33
|
+
StatusCode.ALREADY_EXISTS: HTTPStatus.CONFLICT,
|
|
34
|
+
StatusCode.INVALID_ARGUMENT: HTTPStatus.UNPROCESSABLE_ENTITY,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def map_if_exists(if_exists: str) -> pb.OnConflictBehavior:
|
|
41
|
+
if if_exists == "do_nothing":
|
|
42
|
+
return pb.OnConflictBehavior.DO_NOTHING
|
|
43
|
+
return pb.OnConflictBehavior.RAISE
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def map_configurable(config: Config) -> Struct:
|
|
47
|
+
return pb.RunnableConfig(
|
|
48
|
+
tags=config.get("tags", []),
|
|
49
|
+
recursion_limit=config.get("recursion_limit"),
|
|
50
|
+
configurable=dict_to_struct(config.get("configurable", {}))
|
|
51
|
+
if config.get("configurable")
|
|
52
|
+
else None,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def consolidate_config_and_context(config: Config, context: Context | None):
|
|
57
|
+
if config and config.get("configurable") and context:
|
|
58
|
+
raise HTTPException(
|
|
59
|
+
status_code=400,
|
|
60
|
+
detail="Cannot specify both configurable and context. Prefer setting context alone. Context was introduced in LangGraph 0.6.0 and is the long term planned replacement for configurable.",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Keep config and context up to date with one another
|
|
64
|
+
if config and config.get("configurable"):
|
|
65
|
+
context = config["configurable"]
|
|
66
|
+
elif context:
|
|
67
|
+
config["configurable"] = context
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def dict_to_struct(data: dict[str, Any]) -> Struct:
|
|
71
|
+
"""Convert a dictionary to a protobuf Struct."""
|
|
72
|
+
struct = Struct()
|
|
73
|
+
if data:
|
|
74
|
+
struct.update(data)
|
|
75
|
+
return struct
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def struct_to_dict(struct: Struct) -> dict[str, Any]:
|
|
79
|
+
"""Convert a protobuf Struct to a dictionary."""
|
|
80
|
+
return MessageToDict(struct) if struct else {}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def proto_to_assistant(proto_assistant: pb.Assistant) -> Assistant:
|
|
84
|
+
"""Convert protobuf Assistant to dictionary format."""
|
|
85
|
+
return {
|
|
86
|
+
"assistant_id": proto_assistant.assistant_id,
|
|
87
|
+
"graph_id": proto_assistant.graph_id,
|
|
88
|
+
"version": proto_assistant.version,
|
|
89
|
+
"created_at": proto_assistant.created_at.ToDatetime(),
|
|
90
|
+
"updated_at": proto_assistant.updated_at.ToDatetime(),
|
|
91
|
+
"config": struct_to_dict(proto_assistant.config),
|
|
92
|
+
"context": struct_to_dict(proto_assistant.context),
|
|
93
|
+
"metadata": struct_to_dict(proto_assistant.metadata),
|
|
94
|
+
"name": proto_assistant.name,
|
|
95
|
+
"description": proto_assistant.description,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _map_sort_by(sort_by: str | None) -> pb.AssistantsSortBy:
|
|
100
|
+
"""Map string sort_by to protobuf enum."""
|
|
101
|
+
if not sort_by:
|
|
102
|
+
return pb.AssistantsSortBy.CREATED_AT
|
|
103
|
+
|
|
104
|
+
sort_by_lower = sort_by.lower()
|
|
105
|
+
mapping = {
|
|
106
|
+
"assistant_id": pb.AssistantsSortBy.ASSISTANT_ID,
|
|
107
|
+
"graph_id": pb.AssistantsSortBy.GRAPH_ID,
|
|
108
|
+
"name": pb.AssistantsSortBy.NAME,
|
|
109
|
+
"created_at": pb.AssistantsSortBy.CREATED_AT,
|
|
110
|
+
"updated_at": pb.AssistantsSortBy.UPDATED_AT,
|
|
111
|
+
}
|
|
112
|
+
return mapping.get(sort_by_lower, pb.AssistantsSortBy.CREATED_AT)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _map_sort_order(sort_order: str | None) -> pb.SortOrder:
|
|
116
|
+
"""Map string sort_order to protobuf enum."""
|
|
117
|
+
if sort_order and sort_order.upper() == "ASC":
|
|
118
|
+
return pb.SortOrder.ASC
|
|
119
|
+
return pb.SortOrder.DESC
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _handle_grpc_error(error: AioRpcError) -> None:
|
|
123
|
+
"""Handle gRPC errors and convert to appropriate exceptions."""
|
|
124
|
+
raise HTTPException(
|
|
125
|
+
status_code=GRPC_STATUS_TO_HTTP_STATUS.get(
|
|
126
|
+
error.code(), HTTPStatus.INTERNAL_SERVER_ERROR
|
|
127
|
+
),
|
|
128
|
+
detail=str(error.details()),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Authenticated:
|
|
133
|
+
"""Base class for authenticated operations (matches storage_postgres interface)."""
|
|
134
|
+
|
|
135
|
+
resource: str = "assistants"
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
async def handle_event(
|
|
139
|
+
cls,
|
|
140
|
+
ctx: Any, # Auth context
|
|
141
|
+
action: str,
|
|
142
|
+
value: Any,
|
|
143
|
+
) -> dict[str, Any] | None:
|
|
144
|
+
"""Handle authentication event - stub implementation for now."""
|
|
145
|
+
# TODO: Implement proper auth handling that converts auth context
|
|
146
|
+
# to gRPC AuthFilter format when needed
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def grpc_error_guard(cls):
|
|
151
|
+
"""Class decorator to wrap async methods and handle gRPC errors uniformly."""
|
|
152
|
+
for name, attr in list(cls.__dict__.items()):
|
|
153
|
+
func = None
|
|
154
|
+
wrapper_type = None
|
|
155
|
+
if isinstance(attr, staticmethod):
|
|
156
|
+
func = attr.__func__
|
|
157
|
+
wrapper_type = staticmethod
|
|
158
|
+
elif isinstance(attr, classmethod):
|
|
159
|
+
func = attr.__func__
|
|
160
|
+
wrapper_type = classmethod
|
|
161
|
+
elif callable(attr):
|
|
162
|
+
func = attr
|
|
163
|
+
|
|
164
|
+
if func and asyncio.iscoroutinefunction(func):
|
|
165
|
+
|
|
166
|
+
def make_wrapper(f):
|
|
167
|
+
@functools.wraps(f)
|
|
168
|
+
async def wrapped(*args, **kwargs):
|
|
169
|
+
try:
|
|
170
|
+
return await f(*args, **kwargs)
|
|
171
|
+
except AioRpcError as e:
|
|
172
|
+
_handle_grpc_error(e)
|
|
173
|
+
|
|
174
|
+
return wrapped # noqa: B023
|
|
175
|
+
|
|
176
|
+
wrapped = make_wrapper(func)
|
|
177
|
+
if wrapper_type is staticmethod:
|
|
178
|
+
setattr(cls, name, staticmethod(wrapped))
|
|
179
|
+
elif wrapper_type is classmethod:
|
|
180
|
+
setattr(cls, name, classmethod(wrapped))
|
|
181
|
+
else:
|
|
182
|
+
setattr(cls, name, wrapped)
|
|
183
|
+
return cls
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@grpc_error_guard
|
|
187
|
+
class Assistants(Authenticated):
|
|
188
|
+
"""gRPC-based assistants operations."""
|
|
189
|
+
|
|
190
|
+
resource = "assistants"
|
|
191
|
+
|
|
192
|
+
@staticmethod
|
|
193
|
+
async def search(
|
|
194
|
+
conn, # Not used in gRPC implementation
|
|
195
|
+
*,
|
|
196
|
+
graph_id: str | None,
|
|
197
|
+
metadata: MetadataInput,
|
|
198
|
+
limit: int,
|
|
199
|
+
offset: int,
|
|
200
|
+
sort_by: str | None = None,
|
|
201
|
+
sort_order: str | None = None,
|
|
202
|
+
select: list[AssistantSelectField] | None = None,
|
|
203
|
+
ctx: Any = None,
|
|
204
|
+
) -> tuple[AsyncIterator[Assistant], int | None]: # type: ignore[return-value]
|
|
205
|
+
"""Search assistants via gRPC."""
|
|
206
|
+
# Handle auth filters
|
|
207
|
+
auth_filters = await Assistants.handle_event(
|
|
208
|
+
ctx,
|
|
209
|
+
"search",
|
|
210
|
+
{
|
|
211
|
+
"graph_id": graph_id,
|
|
212
|
+
"metadata": metadata,
|
|
213
|
+
"limit": limit,
|
|
214
|
+
"offset": offset,
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Build the gRPC request
|
|
219
|
+
request = pb.SearchAssistantsRequest(
|
|
220
|
+
filters=auth_filters,
|
|
221
|
+
graph_id=graph_id,
|
|
222
|
+
metadata=dict_to_struct(metadata or {}),
|
|
223
|
+
limit=limit,
|
|
224
|
+
offset=offset,
|
|
225
|
+
sort_by=_map_sort_by(sort_by),
|
|
226
|
+
sort_order=_map_sort_order(sort_order),
|
|
227
|
+
select=select,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Make the gRPC call
|
|
231
|
+
async with GrpcClient() as client:
|
|
232
|
+
response = await client.assistants.Search(request)
|
|
233
|
+
|
|
234
|
+
# Convert response to expected format
|
|
235
|
+
assistants = [
|
|
236
|
+
proto_to_assistant(assistant) for assistant in response.assistants
|
|
237
|
+
]
|
|
238
|
+
|
|
239
|
+
# Determine if there are more results
|
|
240
|
+
# Note: gRPC doesn't return cursor info, so we estimate based on result count
|
|
241
|
+
cursor = offset + limit if len(assistants) == limit else None
|
|
242
|
+
|
|
243
|
+
async def generate_results():
|
|
244
|
+
for assistant in assistants:
|
|
245
|
+
yield {
|
|
246
|
+
k: v for k, v in assistant.items() if select is None or k in select
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return generate_results(), cursor
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
async def get(
|
|
253
|
+
conn, # Not used in gRPC implementation
|
|
254
|
+
assistant_id: UUID | str,
|
|
255
|
+
ctx: Any = None,
|
|
256
|
+
) -> AsyncIterator[Assistant]: # type: ignore[return-value]
|
|
257
|
+
"""Get assistant by ID via gRPC."""
|
|
258
|
+
# Handle auth filters
|
|
259
|
+
auth_filters = await Assistants.handle_event(
|
|
260
|
+
ctx, "read", {"assistant_id": str(assistant_id)}
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Build the gRPC request
|
|
264
|
+
request = pb.GetAssistantRequest(
|
|
265
|
+
assistant_id=str(assistant_id),
|
|
266
|
+
filters=auth_filters or {},
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Make the gRPC call
|
|
270
|
+
async with GrpcClient() as client:
|
|
271
|
+
response = await client.assistants.Get(request)
|
|
272
|
+
|
|
273
|
+
# Convert and yield the result
|
|
274
|
+
assistant = proto_to_assistant(response)
|
|
275
|
+
|
|
276
|
+
async def generate_result():
|
|
277
|
+
yield assistant
|
|
278
|
+
|
|
279
|
+
return generate_result()
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
async def put(
|
|
283
|
+
conn, # Not used in gRPC implementation
|
|
284
|
+
assistant_id: UUID | str,
|
|
285
|
+
*,
|
|
286
|
+
graph_id: str,
|
|
287
|
+
config: Config,
|
|
288
|
+
context: Context,
|
|
289
|
+
metadata: MetadataInput,
|
|
290
|
+
if_exists: OnConflictBehavior,
|
|
291
|
+
name: str,
|
|
292
|
+
description: str | None = None,
|
|
293
|
+
ctx: Any = None,
|
|
294
|
+
) -> AsyncIterator[Assistant]: # type: ignore[return-value]
|
|
295
|
+
"""Create/update assistant via gRPC."""
|
|
296
|
+
# Handle auth filters
|
|
297
|
+
auth_filters = await Assistants.handle_event(
|
|
298
|
+
ctx,
|
|
299
|
+
"create",
|
|
300
|
+
{
|
|
301
|
+
"assistant_id": str(assistant_id),
|
|
302
|
+
"graph_id": graph_id,
|
|
303
|
+
"config": config,
|
|
304
|
+
"context": context,
|
|
305
|
+
"metadata": metadata,
|
|
306
|
+
"name": name,
|
|
307
|
+
"description": description,
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
consolidate_config_and_context(config, context)
|
|
312
|
+
|
|
313
|
+
on_conflict = map_if_exists(if_exists)
|
|
314
|
+
|
|
315
|
+
# Build the gRPC request
|
|
316
|
+
request = pb.CreateAssistantRequest(
|
|
317
|
+
assistant_id=str(assistant_id),
|
|
318
|
+
graph_id=graph_id,
|
|
319
|
+
filters=auth_filters or {},
|
|
320
|
+
if_exists=on_conflict,
|
|
321
|
+
config=map_configurable(config),
|
|
322
|
+
context=dict_to_struct(context or {}),
|
|
323
|
+
name=name,
|
|
324
|
+
description=description,
|
|
325
|
+
metadata=dict_to_struct(metadata or {}),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Make the gRPC call
|
|
329
|
+
async with GrpcClient() as client:
|
|
330
|
+
response = await client.assistants.Create(request)
|
|
331
|
+
|
|
332
|
+
# Convert and yield the result
|
|
333
|
+
assistant = proto_to_assistant(response)
|
|
334
|
+
|
|
335
|
+
async def generate_result():
|
|
336
|
+
yield assistant
|
|
337
|
+
|
|
338
|
+
return generate_result()
|
|
339
|
+
|
|
340
|
+
@staticmethod
|
|
341
|
+
async def patch(
|
|
342
|
+
conn, # Not used in gRPC implementation
|
|
343
|
+
assistant_id: UUID | str,
|
|
344
|
+
*,
|
|
345
|
+
config: dict | None = None,
|
|
346
|
+
context: Context | None = None,
|
|
347
|
+
graph_id: str | None = None,
|
|
348
|
+
metadata: MetadataInput | None = None,
|
|
349
|
+
name: str | None = None,
|
|
350
|
+
description: str | None = None,
|
|
351
|
+
ctx: Any = None,
|
|
352
|
+
) -> AsyncIterator[Assistant]: # type: ignore[return-value]
|
|
353
|
+
"""Update assistant via gRPC."""
|
|
354
|
+
metadata = metadata if metadata is not None else {}
|
|
355
|
+
config = config if config is not None else {}
|
|
356
|
+
# Handle auth filters
|
|
357
|
+
auth_filters = await Assistants.handle_event(
|
|
358
|
+
ctx,
|
|
359
|
+
"update",
|
|
360
|
+
{
|
|
361
|
+
"assistant_id": str(assistant_id),
|
|
362
|
+
"graph_id": graph_id,
|
|
363
|
+
"config": config,
|
|
364
|
+
"context": context,
|
|
365
|
+
"metadata": metadata,
|
|
366
|
+
"name": name,
|
|
367
|
+
"description": description,
|
|
368
|
+
},
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
consolidate_config_and_context(config, context)
|
|
372
|
+
|
|
373
|
+
# Build the gRPC request
|
|
374
|
+
request = pb.PatchAssistantRequest(
|
|
375
|
+
assistant_id=str(assistant_id),
|
|
376
|
+
filters=auth_filters or {},
|
|
377
|
+
graph_id=graph_id,
|
|
378
|
+
name=name,
|
|
379
|
+
description=description,
|
|
380
|
+
metadata=dict_to_struct(metadata or {}),
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Add optional config if provided
|
|
384
|
+
if config:
|
|
385
|
+
request.config.CopyFrom(map_configurable(config))
|
|
386
|
+
|
|
387
|
+
# Add optional context if provided
|
|
388
|
+
if context:
|
|
389
|
+
request.context.CopyFrom(dict_to_struct(context))
|
|
390
|
+
|
|
391
|
+
# Make the gRPC call
|
|
392
|
+
async with GrpcClient() as client:
|
|
393
|
+
response = await client.assistants.Patch(request)
|
|
394
|
+
|
|
395
|
+
# Convert and yield the result
|
|
396
|
+
assistant = proto_to_assistant(response)
|
|
397
|
+
|
|
398
|
+
async def generate_result():
|
|
399
|
+
yield assistant
|
|
400
|
+
|
|
401
|
+
return generate_result()
|
|
402
|
+
|
|
403
|
+
@staticmethod
|
|
404
|
+
async def delete(
|
|
405
|
+
conn, # Not used in gRPC implementation
|
|
406
|
+
assistant_id: UUID | str,
|
|
407
|
+
ctx: Any = None,
|
|
408
|
+
) -> AsyncIterator[UUID]: # type: ignore[return-value]
|
|
409
|
+
"""Delete assistant via gRPC."""
|
|
410
|
+
# Handle auth filters
|
|
411
|
+
auth_filters = await Assistants.handle_event(
|
|
412
|
+
ctx, "delete", {"assistant_id": str(assistant_id)}
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Build the gRPC request
|
|
416
|
+
request = pb.DeleteAssistantRequest(
|
|
417
|
+
assistant_id=str(assistant_id),
|
|
418
|
+
filters=auth_filters or {},
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Make the gRPC call
|
|
422
|
+
async with GrpcClient() as client:
|
|
423
|
+
await client.assistants.Delete(request)
|
|
424
|
+
|
|
425
|
+
# Return the deleted ID
|
|
426
|
+
async def generate_result():
|
|
427
|
+
yield UUID(str(assistant_id))
|
|
428
|
+
|
|
429
|
+
return generate_result()
|
|
430
|
+
|
|
431
|
+
@staticmethod
|
|
432
|
+
async def set_latest(
|
|
433
|
+
conn, # Not used in gRPC implementation
|
|
434
|
+
assistant_id: UUID | str,
|
|
435
|
+
version: int,
|
|
436
|
+
ctx: Any = None,
|
|
437
|
+
) -> AsyncIterator[Assistant]: # type: ignore[return-value]
|
|
438
|
+
"""Set latest version of assistant via gRPC."""
|
|
439
|
+
# Handle auth filters
|
|
440
|
+
auth_filters = await Assistants.handle_event(
|
|
441
|
+
ctx,
|
|
442
|
+
"update",
|
|
443
|
+
{
|
|
444
|
+
"assistant_id": str(assistant_id),
|
|
445
|
+
"version": version,
|
|
446
|
+
},
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
# Build the gRPC request
|
|
450
|
+
request = pb.SetLatestAssistantRequest(
|
|
451
|
+
assistant_id=str(assistant_id),
|
|
452
|
+
version=version,
|
|
453
|
+
filters=auth_filters or {},
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Make the gRPC call
|
|
457
|
+
async with GrpcClient() as client:
|
|
458
|
+
response = await client.assistants.SetLatest(request)
|
|
459
|
+
|
|
460
|
+
# Convert and yield the result
|
|
461
|
+
assistant = proto_to_assistant(response)
|
|
462
|
+
|
|
463
|
+
async def generate_result():
|
|
464
|
+
yield assistant
|
|
465
|
+
|
|
466
|
+
return generate_result()
|
|
467
|
+
|
|
468
|
+
@staticmethod
|
|
469
|
+
async def get_versions(
|
|
470
|
+
conn, # Not used in gRPC implementation
|
|
471
|
+
assistant_id: UUID | str,
|
|
472
|
+
metadata: MetadataInput,
|
|
473
|
+
limit: int,
|
|
474
|
+
offset: int,
|
|
475
|
+
ctx: Any = None,
|
|
476
|
+
) -> AsyncIterator[Assistant]: # type: ignore[return-value]
|
|
477
|
+
"""Get all versions of assistant via gRPC."""
|
|
478
|
+
# Handle auth filters
|
|
479
|
+
auth_filters = await Assistants.handle_event(
|
|
480
|
+
ctx,
|
|
481
|
+
"search",
|
|
482
|
+
{"assistant_id": str(assistant_id), "metadata": metadata},
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Build the gRPC request
|
|
486
|
+
request = pb.GetAssistantVersionsRequest(
|
|
487
|
+
assistant_id=str(assistant_id),
|
|
488
|
+
filters=auth_filters or {},
|
|
489
|
+
metadata=dict_to_struct(metadata or {}),
|
|
490
|
+
limit=limit,
|
|
491
|
+
offset=offset,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Make the gRPC call
|
|
495
|
+
async with GrpcClient() as client:
|
|
496
|
+
response = await client.assistants.GetVersions(request)
|
|
497
|
+
|
|
498
|
+
# Convert and yield the results
|
|
499
|
+
async def generate_results():
|
|
500
|
+
for version in response.versions:
|
|
501
|
+
yield {
|
|
502
|
+
"assistant_id": version.assistant_id,
|
|
503
|
+
"graph_id": version.graph_id,
|
|
504
|
+
"version": version.version,
|
|
505
|
+
"created_at": version.created_at.ToDatetime(),
|
|
506
|
+
"config": struct_to_dict(version.config),
|
|
507
|
+
"context": struct_to_dict(version.context),
|
|
508
|
+
"metadata": struct_to_dict(version.metadata),
|
|
509
|
+
"name": version.name,
|
|
510
|
+
"description": version.description,
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return generate_results()
|
|
514
|
+
|
|
515
|
+
@staticmethod
|
|
516
|
+
async def count(
|
|
517
|
+
conn, # Not used in gRPC implementation
|
|
518
|
+
*,
|
|
519
|
+
graph_id: str | None = None,
|
|
520
|
+
metadata: MetadataInput = None,
|
|
521
|
+
ctx: Any = None,
|
|
522
|
+
) -> int: # type: ignore[return-value]
|
|
523
|
+
"""Count assistants via gRPC."""
|
|
524
|
+
# Handle auth filters
|
|
525
|
+
auth_filters = await Assistants.handle_event(
|
|
526
|
+
ctx, "search", {"graph_id": graph_id, "metadata": metadata}
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Build the gRPC request
|
|
530
|
+
request = pb.CountAssistantsRequest(
|
|
531
|
+
filters=auth_filters or {},
|
|
532
|
+
graph_id=graph_id,
|
|
533
|
+
metadata=dict_to_struct(metadata or {}),
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Make the gRPC call
|
|
537
|
+
async with GrpcClient() as client:
|
|
538
|
+
response = await client.assistants.Count(request)
|
|
539
|
+
|
|
540
|
+
return int(response.count)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Generate Python protobuf files from core protos
|
|
3
|
+
# Run this from the api/langgraph_api/grpc_ops directory
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
# Get the project root (three levels up from api/langgraph_api/grpc_ops)
|
|
8
|
+
PROJECT_ROOT="$(cd ../../../ && pwd)"
|
|
9
|
+
PROTO_DIR="${PROJECT_ROOT}/core/protos"
|
|
10
|
+
OUTPUT_DIR="generated"
|
|
11
|
+
|
|
12
|
+
# Check if proto file exists
|
|
13
|
+
if [[ ! -f "${PROTO_DIR}/core-api.proto" ]]; then
|
|
14
|
+
echo "Error: Proto file not found at ${PROTO_DIR}/core-api.proto"
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Create output directory if it doesn't exist
|
|
19
|
+
mkdir -p "${OUTPUT_DIR}"
|
|
20
|
+
|
|
21
|
+
# Generate Python protobuf files
|
|
22
|
+
echo "Generating Python protobuf files..."
|
|
23
|
+
uv run python -m grpc_tools.protoc \
|
|
24
|
+
-I"${PROTO_DIR}" \
|
|
25
|
+
--python_out="${OUTPUT_DIR}" \
|
|
26
|
+
--grpc_python_out="${OUTPUT_DIR}" \
|
|
27
|
+
--pyi_out="${OUTPUT_DIR}" \
|
|
28
|
+
"${PROTO_DIR}/core-api.proto"
|
|
29
|
+
|
|
30
|
+
# Fix imports to be relative in the generated gRPC file
|
|
31
|
+
echo "Fixing imports to be relative..."
|
|
32
|
+
if [[ -f "${OUTPUT_DIR}/core_api_pb2_grpc.py" ]]; then
|
|
33
|
+
# Make import of core_api_pb2 relative, preserving whatever alias grpc_tools chose
|
|
34
|
+
sed -i.bak 's/^import core_api_pb2 as /from . import core_api_pb2 as /' "${OUTPUT_DIR}/core_api_pb2_grpc.py"
|
|
35
|
+
rm -f "${OUTPUT_DIR}/core_api_pb2_grpc.py.bak"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Create __init__.py files
|
|
39
|
+
cat > "${OUTPUT_DIR}/__init__.py" << 'EOF'
|
|
40
|
+
# Generated protobuf files
|
|
41
|
+
from . import core_api_pb2
|
|
42
|
+
from . import core_api_pb2_grpc
|
|
43
|
+
|
|
44
|
+
__all__ = ["core_api_pb2", "core_api_pb2_grpc"]
|
|
45
|
+
EOF
|
|
46
|
+
|
|
47
|
+
echo "Python protobuf files generated successfully!"
|
langgraph_api/schema.py
CHANGED
|
@@ -62,7 +62,7 @@ class Checkpoint(TypedDict):
|
|
|
62
62
|
checkpoint_map: dict[str, Any] | None
|
|
63
63
|
|
|
64
64
|
|
|
65
|
-
class Assistant(TypedDict):
|
|
65
|
+
class Assistant(TypedDict, total=False):
|
|
66
66
|
"""Assistant model."""
|
|
67
67
|
|
|
68
68
|
assistant_id: UUID
|
|
@@ -176,6 +176,7 @@ class RunKwargs(TypedDict):
|
|
|
176
176
|
subgraphs: bool
|
|
177
177
|
resumable: bool
|
|
178
178
|
checkpoint_during: bool
|
|
179
|
+
durability: str | None
|
|
179
180
|
|
|
180
181
|
|
|
181
182
|
class Run(TypedDict):
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langgraph-api
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.29
|
|
4
4
|
Author-email: Nuno Campos <nuno@langchain.dev>, Will Fu-Hinthorn <will@langchain.dev>
|
|
5
5
|
License: Elastic-2.0
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Python: >=3.11
|
|
8
8
|
Requires-Dist: cloudpickle>=3.0.0
|
|
9
9
|
Requires-Dist: cryptography<45.0,>=42.0.0
|
|
10
|
+
Requires-Dist: grpcio-tools<2.0.0,>=1.75.0
|
|
11
|
+
Requires-Dist: grpcio<2.0.0,>=1.75.0
|
|
10
12
|
Requires-Dist: httpx>=0.25.0
|
|
11
13
|
Requires-Dist: jsonschema-rs<0.30,>=0.20.0
|
|
12
14
|
Requires-Dist: langchain-core>=0.3.64
|
|
@@ -16,6 +18,7 @@ Requires-Dist: langgraph-sdk>=0.2.0
|
|
|
16
18
|
Requires-Dist: langgraph>=0.4.0
|
|
17
19
|
Requires-Dist: langsmith>=0.3.45
|
|
18
20
|
Requires-Dist: orjson>=3.9.7
|
|
21
|
+
Requires-Dist: protobuf<7.0.0,>=6.32.1
|
|
19
22
|
Requires-Dist: pyjwt>=2.9.0
|
|
20
23
|
Requires-Dist: sse-starlette<2.2.0,>=2.1.0
|
|
21
24
|
Requires-Dist: starlette>=0.38.6
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
langgraph_api/__init__.py,sha256=
|
|
1
|
+
langgraph_api/__init__.py,sha256=LtSKwkqgueDQK6uLnhs8HgKylJHH36EMo48UDeHgv2Y,23
|
|
2
2
|
langgraph_api/asgi_transport.py,sha256=XtiLOu4WWsd-xizagBLzT5xUkxc9ZG9YqwvETBPjBFE,5161
|
|
3
3
|
langgraph_api/asyncio.py,sha256=FEEkLm_N-15cbElo4vQ309MkDKBZuRqAYV8VJ1DocNw,9860
|
|
4
4
|
langgraph_api/cli.py,sha256=o_zD2vkky06dzW87HQgkIR1_h3ZCSZ8tgNvFCK9rKVo,19669
|
|
@@ -7,8 +7,8 @@ langgraph_api/config.py,sha256=eS0lIOMx-UhqEw9zDXcz3W5aOLX7laqxZUQcasuXoAs,12168
|
|
|
7
7
|
langgraph_api/cron_scheduler.py,sha256=25wYzEQrhPEivZrAPYOmzLPDOQa-aFogU37mTXc9TJk,2566
|
|
8
8
|
langgraph_api/errors.py,sha256=zlnl3xXIwVG0oGNKKpXf1an9Rn_SBDHSyhe53hU6aLw,1858
|
|
9
9
|
langgraph_api/executor_entrypoint.py,sha256=CaX813ygtf9CpOaBkfkQXJAHjFtmlScCkrOvTDmu4Aw,750
|
|
10
|
-
langgraph_api/feature_flags.py,sha256=
|
|
11
|
-
langgraph_api/graph.py,sha256=
|
|
10
|
+
langgraph_api/feature_flags.py,sha256=taZRhukeBV8r62EmEo92rxfBwYhIw56-P_UvSzQLzt8,576
|
|
11
|
+
langgraph_api/graph.py,sha256=u10F9a8dxi7_Shx42xHOPi1Ri0occU6lUhWnTAF8_K4,25081
|
|
12
12
|
langgraph_api/http.py,sha256=fyK-H-0UfNy_BzuVW3aWWGvhRavmGAVMkDwDArryJ_4,5659
|
|
13
13
|
langgraph_api/http_metrics.py,sha256=MU9ccXt7aBb0AJ2SWEjwtbtbJEWmeqSdx7-CI51e32o,5594
|
|
14
14
|
langgraph_api/logging.py,sha256=qB6q_cUba31edE4_D6dBGhdiUTpW7sXAOepUjYb_R50,5216
|
|
@@ -16,7 +16,7 @@ langgraph_api/metadata.py,sha256=0eGYhXOW6UIVDj2Y5mOdSJz_RadgJG8xmUsC9WqwsiE,834
|
|
|
16
16
|
langgraph_api/patch.py,sha256=J0MmcfpZG15SUVaVcI0Z4x_c0-0rbbT7Pwh9fDAQOpA,1566
|
|
17
17
|
langgraph_api/queue_entrypoint.py,sha256=k-Lz-HdaM2ICJacf9yCQw21GlJp00dPoHKuhe1PSrSs,6418
|
|
18
18
|
langgraph_api/route.py,sha256=EBhELuJ1He-ZYcAnR5YTImcIeDtWthDae5CHELBxPkM,5056
|
|
19
|
-
langgraph_api/schema.py,sha256=
|
|
19
|
+
langgraph_api/schema.py,sha256=spZ_XPT4AMJfw2YatsdnMZZLzgB9Sm3YR8n0SlgGdJ8,8480
|
|
20
20
|
langgraph_api/serde.py,sha256=Jkww6ixP5o2YZmnXtM7ihuAYC6YSuNDNPvE-8ILoqVo,5499
|
|
21
21
|
langgraph_api/server.py,sha256=C9TO7N0mzyrLulT_2FtaJfgfFbm2B4yyYTdAGPxgIeE,7255
|
|
22
22
|
langgraph_api/sse.py,sha256=SLdtZmTdh5D8fbWrQjuY9HYLd2dg8Rmi6ZMmFMVc2iE,4204
|
|
@@ -30,7 +30,7 @@ langgraph_api/webhook.py,sha256=SvSM1rdnNtiH4q3JQYmAqJUk2Sable5xAcwOLuRhtlo,1723
|
|
|
30
30
|
langgraph_api/worker.py,sha256=FQRw3kL9ynDv_LNgY_OjjPZQBuAvSQpsW6nECnABvDg,15354
|
|
31
31
|
langgraph_api/api/__init__.py,sha256=raFkYH50tsO-KjRmDbGVoHCuxuH58u1lrZbr-MlITIY,6262
|
|
32
32
|
langgraph_api/api/a2a.py,sha256=HIHZkLnIcM1u1FJti-L2NH-h1I9BZ_d-QW9z3gFonn8,53995
|
|
33
|
-
langgraph_api/api/assistants.py,sha256=
|
|
33
|
+
langgraph_api/api/assistants.py,sha256=87FMcq8T4-7PfrAX1adOtBS_qQdomGOFpcFQw6PxZDA,17453
|
|
34
34
|
langgraph_api/api/mcp.py,sha256=qe10ZRMN3f-Hli-9TI8nbQyWvMeBb72YB1PZVbyqBQw,14418
|
|
35
35
|
langgraph_api/api/meta.py,sha256=Qyj6r5czkVJ81tpD6liFY7tlrmFDsiSfBr-4X8HJpRc,4834
|
|
36
36
|
langgraph_api/api/openapi.py,sha256=If-z1ckXt-Yu5bwQytK1LWyX_T7G46UtLfixgEP8hwc,11959
|
|
@@ -46,6 +46,14 @@ langgraph_api/auth/studio_user.py,sha256=fojJpexdIZYI1w3awiqOLSwMUiK_M_3p4mlfQI0
|
|
|
46
46
|
langgraph_api/auth/langsmith/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
47
|
langgraph_api/auth/langsmith/backend.py,sha256=rdkz8IXLHusJqcoacvl2XuMZnQVR7PLpE0SHHcKTqv0,3664
|
|
48
48
|
langgraph_api/auth/langsmith/client.py,sha256=Kn9503en1tmlNtkbvqRxYSRCOUrWaVpqvxyLLb1cgzY,3908
|
|
49
|
+
langgraph_api/grpc_ops/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
50
|
+
langgraph_api/grpc_ops/client.py,sha256=VB740C9QMhJJrpAEjsADmasN-uGd0apGYtuv_ho0Rl8,2452
|
|
51
|
+
langgraph_api/grpc_ops/ops.py,sha256=6xCWc5ylMcuTY_FFUJlnqPuRYOr0aOP96pXgx2X4I44,17139
|
|
52
|
+
langgraph_api/grpc_ops/generated/__init__.py,sha256=dRiB_iGscPKdMpuLp9ueLwAmIfRaNjNXC64ABtb4cg8,135
|
|
53
|
+
langgraph_api/grpc_ops/generated/core_api_pb2.py,sha256=5tvJCY00-ud-IO9OKIqRPEpUw12wjxYO3LQklMXB73o,42143
|
|
54
|
+
langgraph_api/grpc_ops/generated/core_api_pb2.pyi,sha256=cpbtS6nf3aaOnbBbAsyCinttxiVELmje7xf0AaRcYUM,49208
|
|
55
|
+
langgraph_api/grpc_ops/generated/core_api_pb2_grpc.py,sha256=fhlFCIrGpi3NPcyTcRsMQWXaw4Glr2-leMTyWiJer7M,52467
|
|
56
|
+
langgraph_api/grpc_ops/scripts/generate_protos.sh,sha256=hSf1vgS0MoTlavbaUjiDKOSujuJ1RRN19UL6HS24JY4,1486
|
|
49
57
|
langgraph_api/js/.gitignore,sha256=l5yI6G_V6F1600I1IjiUKn87f4uYIrBAYU1MOyBBhg4,59
|
|
50
58
|
langgraph_api/js/.prettierrc,sha256=0es3ovvyNIqIw81rPQsdt1zCQcOdBqyR_DMbFE4Ifms,19
|
|
51
59
|
langgraph_api/js/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -99,8 +107,8 @@ langgraph_runtime/store.py,sha256=7mowndlsIroGHv3NpTSOZDJR0lCuaYMBoTnTrewjslw,11
|
|
|
99
107
|
LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
|
|
100
108
|
logging.json,sha256=3RNjSADZmDq38eHePMm1CbP6qZ71AmpBtLwCmKU9Zgo,379
|
|
101
109
|
openapi.json,sha256=21wu-NxdxyTQwZctNcEfRkLMnSBi0QhGAfwq5kg8XNU,172618
|
|
102
|
-
langgraph_api-0.4.
|
|
103
|
-
langgraph_api-0.4.
|
|
104
|
-
langgraph_api-0.4.
|
|
105
|
-
langgraph_api-0.4.
|
|
106
|
-
langgraph_api-0.4.
|
|
110
|
+
langgraph_api-0.4.29.dist-info/METADATA,sha256=VFgEuF3tQk5UU_jlddc4WCwXsgPp7A9STteH1LyMLzg,4012
|
|
111
|
+
langgraph_api-0.4.29.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
112
|
+
langgraph_api-0.4.29.dist-info/entry_points.txt,sha256=hGedv8n7cgi41PypMfinwS_HfCwA7xJIfS0jAp8htV8,78
|
|
113
|
+
langgraph_api-0.4.29.dist-info/licenses/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
|
|
114
|
+
langgraph_api-0.4.29.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|