langgraph-api 0.4.40__py3-none-any.whl → 0.5.6__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.

Files changed (41) hide show
  1. langgraph_api/__init__.py +1 -1
  2. langgraph_api/api/assistants.py +65 -61
  3. langgraph_api/api/meta.py +6 -0
  4. langgraph_api/api/threads.py +11 -7
  5. langgraph_api/auth/custom.py +29 -24
  6. langgraph_api/cli.py +2 -49
  7. langgraph_api/config.py +131 -16
  8. langgraph_api/graph.py +1 -1
  9. langgraph_api/grpc/client.py +183 -0
  10. langgraph_api/grpc/config_conversion.py +225 -0
  11. langgraph_api/grpc/generated/core_api_pb2.py +275 -0
  12. langgraph_api/{grpc_ops → grpc}/generated/core_api_pb2.pyi +35 -40
  13. langgraph_api/grpc/generated/engine_common_pb2.py +190 -0
  14. langgraph_api/grpc/generated/engine_common_pb2.pyi +634 -0
  15. langgraph_api/grpc/generated/engine_common_pb2_grpc.py +24 -0
  16. langgraph_api/grpc/ops.py +1045 -0
  17. langgraph_api/js/build.mts +1 -1
  18. langgraph_api/js/client.http.mts +1 -1
  19. langgraph_api/js/client.mts +1 -1
  20. langgraph_api/js/package.json +12 -12
  21. langgraph_api/js/src/graph.mts +20 -0
  22. langgraph_api/js/yarn.lock +176 -234
  23. langgraph_api/metadata.py +29 -21
  24. langgraph_api/queue_entrypoint.py +2 -2
  25. langgraph_api/route.py +14 -4
  26. langgraph_api/schema.py +2 -2
  27. langgraph_api/self_hosted_metrics.py +48 -2
  28. langgraph_api/serde.py +58 -14
  29. langgraph_api/server.py +16 -2
  30. langgraph_api/worker.py +1 -1
  31. {langgraph_api-0.4.40.dist-info → langgraph_api-0.5.6.dist-info}/METADATA +6 -6
  32. {langgraph_api-0.4.40.dist-info → langgraph_api-0.5.6.dist-info}/RECORD +38 -34
  33. langgraph_api/grpc_ops/client.py +0 -80
  34. langgraph_api/grpc_ops/generated/core_api_pb2.py +0 -274
  35. langgraph_api/grpc_ops/ops.py +0 -610
  36. /langgraph_api/{grpc_ops → grpc}/__init__.py +0 -0
  37. /langgraph_api/{grpc_ops → grpc}/generated/__init__.py +0 -0
  38. /langgraph_api/{grpc_ops → grpc}/generated/core_api_pb2_grpc.py +0 -0
  39. {langgraph_api-0.4.40.dist-info → langgraph_api-0.5.6.dist-info}/WHEEL +0 -0
  40. {langgraph_api-0.4.40.dist-info → langgraph_api-0.5.6.dist-info}/entry_points.txt +0 -0
  41. {langgraph_api-0.4.40.dist-info → langgraph_api-0.5.6.dist-info}/licenses/LICENSE +0 -0
@@ -1,610 +0,0 @@
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 datetime import UTC
9
- from http import HTTPStatus
10
- from typing import Any
11
- from uuid import UUID
12
-
13
- import orjson
14
- import structlog
15
- from google.protobuf.json_format import MessageToDict
16
- from google.protobuf.struct_pb2 import Struct # type: ignore[import]
17
- from grpc import StatusCode
18
- from grpc.aio import AioRpcError
19
- from langgraph_sdk.schema import Config
20
- from starlette.exceptions import HTTPException
21
-
22
- from langgraph_api.schema import (
23
- Assistant,
24
- AssistantSelectField,
25
- Context,
26
- MetadataInput,
27
- OnConflictBehavior,
28
- )
29
-
30
- from .client import GrpcClient
31
- from .generated import core_api_pb2 as pb
32
-
33
- GRPC_STATUS_TO_HTTP_STATUS = {
34
- StatusCode.NOT_FOUND: HTTPStatus.NOT_FOUND,
35
- StatusCode.ALREADY_EXISTS: HTTPStatus.CONFLICT,
36
- StatusCode.INVALID_ARGUMENT: HTTPStatus.UNPROCESSABLE_ENTITY,
37
- }
38
-
39
- logger = structlog.stdlib.get_logger(__name__)
40
-
41
-
42
- def map_if_exists(if_exists: str) -> pb.OnConflictBehavior:
43
- if if_exists == "do_nothing":
44
- return pb.OnConflictBehavior.DO_NOTHING
45
- return pb.OnConflictBehavior.RAISE
46
-
47
-
48
- def map_configurable(config: Config) -> Struct:
49
- """Build pb.Config, placing non-standard keys into `extra` bytes.
50
-
51
- The `extra` field mirrors any keys that are not first-class in
52
- Config (e.g., "tags", "recursion_limit", "configurable").
53
- It is JSON-encoded bytes to minimize serde overhead; the server will
54
- unpack and persist them as top-level keys.
55
- """
56
- base_keys = {"tags", "recursion_limit", "configurable"}
57
- extra_dict = {k: v for k, v in (config or {}).items() if k not in base_keys}
58
-
59
- kwargs: dict[str, Any] = dict(
60
- tags=config.get("tags"),
61
- recursion_limit=config.get("recursion_limit"),
62
- configurable=(
63
- dict_to_struct(config.get("configurable", {}))
64
- if config.get("configurable")
65
- else None
66
- ),
67
- )
68
- if extra_dict:
69
- kwargs["extra"] = orjson.dumps(extra_dict)
70
-
71
- return pb.Config(**kwargs)
72
-
73
-
74
- def consolidate_config_and_context(
75
- config: Config | None, context: Context | None
76
- ) -> tuple[Config, Context | None]:
77
- """Return a new (config, context) with consistent configurable/context.
78
-
79
- Does not mutate the passed-in objects. If both configurable and context
80
- are provided, raises 400. If only one is provided, mirrors it to the other.
81
- """
82
- cfg: Config = dict(config or {})
83
- ctx: Context | None = dict(context) if context is not None else None
84
-
85
- if cfg.get("configurable") and ctx:
86
- raise HTTPException(
87
- status_code=400,
88
- 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.",
89
- )
90
-
91
- if cfg.get("configurable"):
92
- ctx = cfg["configurable"]
93
- elif ctx is not None:
94
- cfg["configurable"] = ctx
95
-
96
- return cfg, ctx
97
-
98
-
99
- def dict_to_struct(data: dict[str, Any]) -> Struct:
100
- """Convert a dictionary to a protobuf Struct."""
101
- struct = Struct()
102
- if data:
103
- struct.update(data)
104
- return struct
105
-
106
-
107
- def struct_to_dict(struct: Struct) -> dict[str, Any]:
108
- """Convert a protobuf Struct to a dictionary."""
109
- return MessageToDict(struct) if struct else {}
110
-
111
-
112
- def _runnable_config_to_user_dict(cfg: pb.Config | None) -> dict[str, Any]:
113
- """Convert pb.Config to user-visible dict, unpacking `extra`.
114
-
115
- - Keeps top-level known keys: tags, recursion_limit, configurable.
116
- - Merges keys from `extra` into the top-level dict.
117
- """
118
- if not cfg:
119
- return {}
120
-
121
- out: dict[str, Any] = {}
122
- # tags
123
- if cfg.tags:
124
- out["tags"] = list(cfg.tags)
125
- # recursion_limit (preserve presence of 0 if set)
126
- try:
127
- if cfg.HasField("recursion_limit"):
128
- out["recursion_limit"] = cfg.recursion_limit
129
- except ValueError:
130
- # Some runtimes may not support HasField on certain builds; fallback
131
- if getattr(cfg, "recursion_limit", None) is not None:
132
- out["recursion_limit"] = cfg.recursion_limit
133
- # configurable
134
- if cfg.HasField("configurable"):
135
- out["configurable"] = struct_to_dict(cfg.configurable)
136
- # extra (bytes: JSON-encoded object)
137
- if cfg.HasField("extra") and cfg.extra:
138
- extra = orjson.loads(cfg.extra)
139
- if isinstance(extra, dict) and extra:
140
- out.update(extra)
141
-
142
- return out
143
-
144
-
145
- def proto_to_assistant(proto_assistant: pb.Assistant) -> Assistant:
146
- """Convert protobuf Assistant to dictionary format."""
147
- # Preserve None for optional scalar fields by checking presence via HasField
148
- description = (
149
- proto_assistant.description if proto_assistant.HasField("description") else None
150
- )
151
- return {
152
- "assistant_id": proto_assistant.assistant_id,
153
- "graph_id": proto_assistant.graph_id,
154
- "version": proto_assistant.version,
155
- "created_at": proto_assistant.created_at.ToDatetime(tzinfo=UTC),
156
- "updated_at": proto_assistant.updated_at.ToDatetime(tzinfo=UTC),
157
- "config": _runnable_config_to_user_dict(proto_assistant.config),
158
- "context": struct_to_dict(proto_assistant.context),
159
- "metadata": struct_to_dict(proto_assistant.metadata),
160
- "name": proto_assistant.name,
161
- "description": description,
162
- }
163
-
164
-
165
- def _map_sort_by(sort_by: str | None) -> pb.AssistantsSortBy:
166
- """Map string sort_by to protobuf enum."""
167
- if not sort_by:
168
- return pb.AssistantsSortBy.CREATED_AT
169
-
170
- sort_by_lower = sort_by.lower()
171
- mapping = {
172
- "assistant_id": pb.AssistantsSortBy.ASSISTANT_ID,
173
- "graph_id": pb.AssistantsSortBy.GRAPH_ID,
174
- "name": pb.AssistantsSortBy.NAME,
175
- "created_at": pb.AssistantsSortBy.CREATED_AT,
176
- "updated_at": pb.AssistantsSortBy.UPDATED_AT,
177
- }
178
- return mapping.get(sort_by_lower, pb.AssistantsSortBy.CREATED_AT)
179
-
180
-
181
- def _map_sort_order(sort_order: str | None) -> pb.SortOrder:
182
- """Map string sort_order to protobuf enum."""
183
- if sort_order and sort_order.upper() == "ASC":
184
- return pb.SortOrder.ASC
185
- return pb.SortOrder.DESC
186
-
187
-
188
- def _handle_grpc_error(error: AioRpcError) -> None:
189
- """Handle gRPC errors and convert to appropriate exceptions."""
190
- raise HTTPException(
191
- status_code=GRPC_STATUS_TO_HTTP_STATUS.get(
192
- error.code(), HTTPStatus.INTERNAL_SERVER_ERROR
193
- ),
194
- detail=str(error.details()),
195
- )
196
-
197
-
198
- class Authenticated:
199
- """Base class for authenticated operations (matches storage_postgres interface)."""
200
-
201
- resource: str = "assistants"
202
-
203
- @classmethod
204
- async def handle_event(
205
- cls,
206
- ctx: Any, # Auth context
207
- action: str,
208
- value: Any,
209
- ) -> dict[str, Any] | None:
210
- """Handle authentication event - stub implementation for now."""
211
- # TODO: Implement proper auth handling that converts auth context
212
- # to gRPC AuthFilter format when needed
213
- return None
214
-
215
-
216
- def grpc_error_guard(cls):
217
- """Class decorator to wrap async methods and handle gRPC errors uniformly."""
218
- for name, attr in list(cls.__dict__.items()):
219
- func = None
220
- wrapper_type = None
221
- if isinstance(attr, staticmethod):
222
- func = attr.__func__
223
- wrapper_type = staticmethod
224
- elif isinstance(attr, classmethod):
225
- func = attr.__func__
226
- wrapper_type = classmethod
227
- elif callable(attr):
228
- func = attr
229
-
230
- if func and asyncio.iscoroutinefunction(func):
231
-
232
- def make_wrapper(f):
233
- @functools.wraps(f)
234
- async def wrapped(*args, **kwargs):
235
- try:
236
- return await f(*args, **kwargs)
237
- except AioRpcError as e:
238
- _handle_grpc_error(e)
239
-
240
- return wrapped # noqa: B023
241
-
242
- wrapped = make_wrapper(func)
243
- if wrapper_type is staticmethod:
244
- setattr(cls, name, staticmethod(wrapped))
245
- elif wrapper_type is classmethod:
246
- setattr(cls, name, classmethod(wrapped))
247
- else:
248
- setattr(cls, name, wrapped)
249
- return cls
250
-
251
-
252
- @grpc_error_guard
253
- class Assistants(Authenticated):
254
- """gRPC-based assistants operations."""
255
-
256
- resource = "assistants"
257
-
258
- @staticmethod
259
- async def search(
260
- conn, # Not used in gRPC implementation
261
- *,
262
- graph_id: str | None,
263
- metadata: MetadataInput,
264
- limit: int,
265
- offset: int,
266
- sort_by: str | None = None,
267
- sort_order: str | None = None,
268
- select: list[AssistantSelectField] | None = None,
269
- ctx: Any = None,
270
- ) -> tuple[AsyncIterator[Assistant], int | None]: # type: ignore[return-value]
271
- """Search assistants via gRPC."""
272
- # Handle auth filters
273
- auth_filters = await Assistants.handle_event(
274
- ctx,
275
- "search",
276
- {
277
- "graph_id": graph_id,
278
- "metadata": metadata,
279
- "limit": limit,
280
- "offset": offset,
281
- },
282
- )
283
-
284
- # Build the gRPC request
285
- request = pb.SearchAssistantsRequest(
286
- filters=auth_filters,
287
- graph_id=graph_id,
288
- metadata=dict_to_struct(metadata or {}),
289
- limit=limit,
290
- offset=offset,
291
- sort_by=_map_sort_by(sort_by),
292
- sort_order=_map_sort_order(sort_order),
293
- select=select,
294
- )
295
-
296
- # Make the gRPC call
297
- async with GrpcClient() as client:
298
- response = await client.assistants.Search(request)
299
-
300
- # Convert response to expected format
301
- assistants = [
302
- proto_to_assistant(assistant) for assistant in response.assistants
303
- ]
304
-
305
- # Determine if there are more results
306
- # Note: gRPC doesn't return cursor info, so we estimate based on result count
307
- cursor = offset + limit if len(assistants) == limit else None
308
-
309
- async def generate_results():
310
- for assistant in assistants:
311
- yield {
312
- k: v for k, v in assistant.items() if select is None or k in select
313
- }
314
-
315
- return generate_results(), cursor
316
-
317
- @staticmethod
318
- async def get(
319
- conn, # Not used in gRPC implementation
320
- assistant_id: UUID | str,
321
- ctx: Any = None,
322
- ) -> AsyncIterator[Assistant]: # type: ignore[return-value]
323
- """Get assistant by ID via gRPC."""
324
- # Handle auth filters
325
- auth_filters = await Assistants.handle_event(
326
- ctx, "read", {"assistant_id": str(assistant_id)}
327
- )
328
-
329
- # Build the gRPC request
330
- request = pb.GetAssistantRequest(
331
- assistant_id=str(assistant_id),
332
- filters=auth_filters or {},
333
- )
334
-
335
- # Make the gRPC call
336
- async with GrpcClient() as client:
337
- response = await client.assistants.Get(request)
338
-
339
- # Convert and yield the result
340
- assistant = proto_to_assistant(response)
341
-
342
- async def generate_result():
343
- yield assistant
344
-
345
- return generate_result()
346
-
347
- @staticmethod
348
- async def put(
349
- conn, # Not used in gRPC implementation
350
- assistant_id: UUID | str,
351
- *,
352
- graph_id: str,
353
- config: Config,
354
- context: Context,
355
- metadata: MetadataInput,
356
- if_exists: OnConflictBehavior,
357
- name: str,
358
- description: str | None = None,
359
- ctx: Any = None,
360
- ) -> AsyncIterator[Assistant]: # type: ignore[return-value]
361
- """Create/update assistant via gRPC."""
362
- # Handle auth filters
363
- auth_filters = await Assistants.handle_event(
364
- ctx,
365
- "create",
366
- {
367
- "assistant_id": str(assistant_id),
368
- "graph_id": graph_id,
369
- "config": config,
370
- "context": context,
371
- "metadata": metadata,
372
- "name": name,
373
- "description": description,
374
- },
375
- )
376
-
377
- config, context = consolidate_config_and_context(config, context)
378
-
379
- on_conflict = map_if_exists(if_exists)
380
-
381
- # Build the gRPC request
382
- request = pb.CreateAssistantRequest(
383
- assistant_id=str(assistant_id),
384
- graph_id=graph_id,
385
- filters=auth_filters or {},
386
- if_exists=on_conflict,
387
- config=map_configurable(config),
388
- context=dict_to_struct(context or {}),
389
- name=name,
390
- description=description,
391
- metadata=dict_to_struct(metadata or {}),
392
- )
393
-
394
- # Make the gRPC call
395
- async with GrpcClient() as client:
396
- response = await client.assistants.Create(request)
397
-
398
- # Convert and yield the result
399
- assistant = proto_to_assistant(response)
400
-
401
- async def generate_result():
402
- yield assistant
403
-
404
- return generate_result()
405
-
406
- @staticmethod
407
- async def patch(
408
- conn, # Not used in gRPC implementation
409
- assistant_id: UUID | str,
410
- *,
411
- config: dict | None = None,
412
- context: Context | None = None,
413
- graph_id: str | None = None,
414
- metadata: MetadataInput | None = None,
415
- name: str | None = None,
416
- description: str | None = None,
417
- ctx: Any = None,
418
- ) -> AsyncIterator[Assistant]: # type: ignore[return-value]
419
- """Update assistant via gRPC."""
420
- metadata = metadata if metadata is not None else {}
421
- config = config if config is not None else {}
422
- # Handle auth filters
423
- auth_filters = await Assistants.handle_event(
424
- ctx,
425
- "update",
426
- {
427
- "assistant_id": str(assistant_id),
428
- "graph_id": graph_id,
429
- "config": config,
430
- "context": context,
431
- "metadata": metadata,
432
- "name": name,
433
- "description": description,
434
- },
435
- )
436
-
437
- config, context = consolidate_config_and_context(config, context)
438
-
439
- # Build the gRPC request
440
- request = pb.PatchAssistantRequest(
441
- assistant_id=str(assistant_id),
442
- filters=auth_filters or {},
443
- graph_id=graph_id,
444
- name=name,
445
- description=description,
446
- metadata=dict_to_struct(metadata or {}),
447
- )
448
-
449
- # Add optional config if provided
450
- if config:
451
- request.config.CopyFrom(map_configurable(config))
452
-
453
- # Add optional context if provided
454
- if context:
455
- request.context.CopyFrom(dict_to_struct(context))
456
-
457
- # Make the gRPC call
458
- async with GrpcClient() as client:
459
- response = await client.assistants.Patch(request)
460
-
461
- # Convert and yield the result
462
- assistant = proto_to_assistant(response)
463
-
464
- async def generate_result():
465
- yield assistant
466
-
467
- return generate_result()
468
-
469
- @staticmethod
470
- async def delete(
471
- conn, # Not used in gRPC implementation
472
- assistant_id: UUID | str,
473
- ctx: Any = None,
474
- ) -> AsyncIterator[UUID]: # type: ignore[return-value]
475
- """Delete assistant via gRPC."""
476
- # Handle auth filters
477
- auth_filters = await Assistants.handle_event(
478
- ctx, "delete", {"assistant_id": str(assistant_id)}
479
- )
480
-
481
- # Build the gRPC request
482
- request = pb.DeleteAssistantRequest(
483
- assistant_id=str(assistant_id),
484
- filters=auth_filters or {},
485
- )
486
-
487
- # Make the gRPC call
488
- async with GrpcClient() as client:
489
- await client.assistants.Delete(request)
490
-
491
- # Return the deleted ID
492
- async def generate_result():
493
- yield UUID(str(assistant_id))
494
-
495
- return generate_result()
496
-
497
- @staticmethod
498
- async def set_latest(
499
- conn, # Not used in gRPC implementation
500
- assistant_id: UUID | str,
501
- version: int,
502
- ctx: Any = None,
503
- ) -> AsyncIterator[Assistant]: # type: ignore[return-value]
504
- """Set latest version of assistant via gRPC."""
505
- # Handle auth filters
506
- auth_filters = await Assistants.handle_event(
507
- ctx,
508
- "update",
509
- {
510
- "assistant_id": str(assistant_id),
511
- "version": version,
512
- },
513
- )
514
-
515
- # Build the gRPC request
516
- request = pb.SetLatestAssistantRequest(
517
- assistant_id=str(assistant_id),
518
- version=version,
519
- filters=auth_filters or {},
520
- )
521
-
522
- # Make the gRPC call
523
- async with GrpcClient() as client:
524
- response = await client.assistants.SetLatest(request)
525
-
526
- # Convert and yield the result
527
- assistant = proto_to_assistant(response)
528
-
529
- async def generate_result():
530
- yield assistant
531
-
532
- return generate_result()
533
-
534
- @staticmethod
535
- async def get_versions(
536
- conn, # Not used in gRPC implementation
537
- assistant_id: UUID | str,
538
- metadata: MetadataInput,
539
- limit: int,
540
- offset: int,
541
- ctx: Any = None,
542
- ) -> AsyncIterator[Assistant]: # type: ignore[return-value]
543
- """Get all versions of assistant via gRPC."""
544
- # Handle auth filters
545
- auth_filters = await Assistants.handle_event(
546
- ctx,
547
- "search",
548
- {"assistant_id": str(assistant_id), "metadata": metadata},
549
- )
550
-
551
- # Build the gRPC request
552
- request = pb.GetAssistantVersionsRequest(
553
- assistant_id=str(assistant_id),
554
- filters=auth_filters or {},
555
- metadata=dict_to_struct(metadata or {}),
556
- limit=limit,
557
- offset=offset,
558
- )
559
-
560
- # Make the gRPC call
561
- async with GrpcClient() as client:
562
- response = await client.assistants.GetVersions(request)
563
-
564
- # Convert and yield the results
565
- async def generate_results():
566
- for version in response.versions:
567
- # Preserve None for optional scalar fields by checking presence
568
- version_description = (
569
- version.description if version.HasField("description") else None
570
- )
571
- yield {
572
- "assistant_id": version.assistant_id,
573
- "graph_id": version.graph_id,
574
- "version": version.version,
575
- "created_at": version.created_at.ToDatetime(tzinfo=UTC),
576
- "config": _runnable_config_to_user_dict(version.config),
577
- "context": struct_to_dict(version.context),
578
- "metadata": struct_to_dict(version.metadata),
579
- "name": version.name,
580
- "description": version_description,
581
- }
582
-
583
- return generate_results()
584
-
585
- @staticmethod
586
- async def count(
587
- conn, # Not used in gRPC implementation
588
- *,
589
- graph_id: str | None = None,
590
- metadata: MetadataInput = None,
591
- ctx: Any = None,
592
- ) -> int: # type: ignore[return-value]
593
- """Count assistants via gRPC."""
594
- # Handle auth filters
595
- auth_filters = await Assistants.handle_event(
596
- ctx, "search", {"graph_id": graph_id, "metadata": metadata}
597
- )
598
-
599
- # Build the gRPC request
600
- request = pb.CountAssistantsRequest(
601
- filters=auth_filters or {},
602
- graph_id=graph_id,
603
- metadata=dict_to_struct(metadata or {}),
604
- )
605
-
606
- # Make the gRPC call
607
- async with GrpcClient() as client:
608
- response = await client.assistants.Count(request)
609
-
610
- return int(response.count)
File without changes
File without changes