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

@@ -4,10 +4,10 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import functools
7
- from collections.abc import AsyncIterator, Sequence
7
+ from collections.abc import AsyncIterator
8
8
  from datetime import UTC
9
9
  from http import HTTPStatus
10
- from typing import Any
10
+ from typing import Any, overload
11
11
  from uuid import UUID
12
12
 
13
13
  import orjson
@@ -16,10 +16,10 @@ from google.protobuf.json_format import MessageToDict
16
16
  from google.protobuf.struct_pb2 import Struct # type: ignore[import]
17
17
  from grpc import StatusCode
18
18
  from grpc.aio import AioRpcError
19
- from langgraph.pregel.debug import CheckpointPayload
20
19
  from langgraph_sdk.schema import Config
21
20
  from starlette.exceptions import HTTPException
22
21
 
22
+ from langgraph_api.grpc_ops import config_conversion
23
23
  from langgraph_api.schema import (
24
24
  Assistant,
25
25
  AssistantSelectField,
@@ -50,30 +50,16 @@ def map_if_exists(if_exists: str) -> pb.OnConflictBehavior:
50
50
  return pb.OnConflictBehavior.RAISE
51
51
 
52
52
 
53
- def map_configurable(config: Config) -> Struct:
54
- """Build pb.Config, placing non-standard keys into `extra` bytes.
53
+ @overload
54
+ def consolidate_config_and_context(
55
+ config: Config | None, context: None
56
+ ) -> tuple[Config, None]: ...
55
57
 
56
- The `extra` field mirrors any keys that are not first-class in
57
- Config (e.g., "tags", "recursion_limit", "configurable").
58
- It is JSON-encoded bytes to minimize serde overhead; the server will
59
- unpack and persist them as top-level keys.
60
- """
61
- base_keys = {"tags", "recursion_limit", "configurable"}
62
- extra_dict = {k: v for k, v in (config or {}).items() if k not in base_keys}
63
-
64
- kwargs: dict[str, Any] = dict(
65
- tags=pb.Tags(values=config.get("tags")),
66
- recursion_limit=config.get("recursion_limit"),
67
- configurable=(
68
- dict_to_struct(config.get("configurable", {}))
69
- if config.get("configurable")
70
- else None
71
- ),
72
- )
73
- if extra_dict:
74
- kwargs["extra"] = orjson.dumps(extra_dict)
75
58
 
76
- return pb.Config(**kwargs)
59
+ @overload
60
+ def consolidate_config_and_context(
61
+ config: Config | None, context: Context
62
+ ) -> tuple[Config, Context]: ...
77
63
 
78
64
 
79
65
  def consolidate_config_and_context(
@@ -84,17 +70,20 @@ def consolidate_config_and_context(
84
70
  Does not mutate the passed-in objects. If both configurable and context
85
71
  are provided, raises 400. If only one is provided, mirrors it to the other.
86
72
  """
87
- cfg: Config = dict(config or {})
73
+ cfg: Config = Config(config or {})
88
74
  ctx: Context | None = dict(context) if context is not None else None
75
+ configurable = cfg.get("configurable")
89
76
 
90
- if cfg.get("configurable") and ctx:
77
+ if configurable and ctx:
91
78
  raise HTTPException(
92
79
  status_code=400,
93
- 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.",
80
+ detail="Cannot specify both configurable and context. Prefer setting context alone."
81
+ " Context was introduced in LangGraph 0.6.0 and "
82
+ "is the long term planned replacement for configurable.",
94
83
  )
95
84
 
96
- if cfg.get("configurable"):
97
- ctx = cfg["configurable"]
85
+ if configurable:
86
+ ctx = configurable
98
87
  elif ctx is not None:
99
88
  cfg["configurable"] = ctx
100
89
 
@@ -114,38 +103,6 @@ def struct_to_dict(struct: Struct) -> dict[str, Any]:
114
103
  return MessageToDict(struct) if struct else {}
115
104
 
116
105
 
117
- def _runnable_config_to_user_dict(cfg: pb.Config | None) -> dict[str, Any]:
118
- """Convert pb.Config to user-visible dict, unpacking `extra`.
119
-
120
- - Keeps top-level known keys: tags, recursion_limit, configurable.
121
- - Merges keys from `extra` into the top-level dict.
122
- """
123
- if not cfg:
124
- return {}
125
-
126
- out: dict[str, Any] = {}
127
- # tags
128
- if cfg.tags and cfg.tags.values:
129
- out["tags"] = list(cfg.tags.values)
130
- # recursion_limit (preserve presence of 0 if set)
131
- try:
132
- if cfg.HasField("recursion_limit"):
133
- out["recursion_limit"] = cfg.recursion_limit
134
- except ValueError:
135
- # Some runtimes may not support HasField on certain builds; fallback
136
- if getattr(cfg, "recursion_limit", None) is not None:
137
- out["recursion_limit"] = cfg.recursion_limit
138
- # configurable
139
- if cfg.HasField("configurable"):
140
- out["configurable"] = struct_to_dict(cfg.configurable)
141
- # extra (bytes: JSON-encoded object)
142
- if cfg.HasField("extra") and cfg.extra:
143
- extra = orjson.loads(cfg.extra)
144
- if isinstance(extra, dict) and extra:
145
- out.update(extra)
146
- return out
147
-
148
-
149
106
  def proto_to_assistant(proto_assistant: pb.Assistant) -> Assistant:
150
107
  """Convert protobuf Assistant to dictionary format."""
151
108
  # Preserve None for optional scalar fields by checking presence via HasField
@@ -158,7 +115,7 @@ def proto_to_assistant(proto_assistant: pb.Assistant) -> Assistant:
158
115
  "version": proto_assistant.version,
159
116
  "created_at": proto_assistant.created_at.ToDatetime(tzinfo=UTC),
160
117
  "updated_at": proto_assistant.updated_at.ToDatetime(tzinfo=UTC),
161
- "config": _runnable_config_to_user_dict(proto_assistant.config),
118
+ "config": config_conversion.config_from_proto(proto_assistant.config),
162
119
  "context": struct_to_dict(proto_assistant.context),
163
120
  "metadata": struct_to_dict(proto_assistant.metadata),
164
121
  "name": proto_assistant.name,
@@ -299,93 +256,6 @@ def proto_to_thread(proto_thread: pb.Thread) -> Thread:
299
256
  }
300
257
 
301
258
 
302
- def _checkpoint_metadata_to_pb(
303
- metadata: dict[str, Any] | None,
304
- ) -> pb.CheckpointMetadata | None:
305
- if not metadata:
306
- return None
307
-
308
- message = pb.CheckpointMetadata()
309
- source = metadata.get("source")
310
- if source is not None:
311
- if isinstance(source, str):
312
- enum_key = f"CHECKPOINT_SOURCE_{source.upper()}"
313
- try:
314
- message.source = pb.CheckpointSource.Value(enum_key)
315
- except ValueError:
316
- logger.warning(
317
- "Unknown checkpoint source enum, defaulting to unspecified",
318
- source=source,
319
- )
320
- elif isinstance(source, int):
321
- try:
322
- message.source = pb.CheckpointSource(source)
323
- except ValueError:
324
- logger.warning(
325
- "Unknown checkpoint source value, defaulting to unspecified",
326
- source=source,
327
- )
328
- if step := metadata.get("step"):
329
- message.step = int(step)
330
- parents = metadata.get("parents")
331
- if isinstance(parents, dict):
332
- message.parents.update({str(k): str(v) for k, v in parents.items()})
333
- return message
334
-
335
-
336
- def _checkpoint_tasks_to_pb(tasks: Sequence[dict[str, Any]]) -> list[pb.CheckpointTask]:
337
- task_messages: list[pb.CheckpointTask] = []
338
- for task in tasks:
339
- message = pb.CheckpointTask(
340
- id=task.get("id", ""),
341
- name=task.get("name", ""),
342
- )
343
- if task.get("error"):
344
- message.error = str(task["error"])
345
- interrupts = task.get("interrupts") or []
346
- for interrupt in interrupts:
347
- message.interrupts.append(dict_to_struct(interrupt))
348
- if task.get("state"):
349
- message.state.CopyFrom(dict_to_struct(task["state"]))
350
- task_messages.append(message)
351
- return task_messages
352
-
353
-
354
- def checkpoint_to_pb(
355
- checkpoint: CheckpointPayload | None,
356
- ) -> pb.CheckpointPayload | None:
357
- if checkpoint is None:
358
- return None
359
-
360
- message = pb.CheckpointPayload()
361
-
362
- config = checkpoint.get("config")
363
- if config:
364
- message.config.CopyFrom(map_configurable(config))
365
-
366
- metadata = _checkpoint_metadata_to_pb(checkpoint.get("metadata"))
367
- if metadata:
368
- message.metadata.CopyFrom(metadata)
369
-
370
- values = checkpoint.get("values")
371
- if values:
372
- message.values.CopyFrom(dict_to_struct(values))
373
-
374
- next_nodes = checkpoint.get("next")
375
- if next_nodes:
376
- message.next.extend([str(n) for n in next_nodes])
377
-
378
- parent_config = checkpoint.get("parent_config")
379
- if parent_config:
380
- message.parent_config.CopyFrom(map_configurable(parent_config))
381
-
382
- tasks = checkpoint.get("tasks")
383
- if tasks:
384
- message.tasks.extend(_checkpoint_tasks_to_pb(tasks))
385
-
386
- return message
387
-
388
-
389
259
  def exception_to_struct(exception: BaseException | None) -> Struct | None:
390
260
  if exception is None:
391
261
  return None
@@ -400,7 +270,7 @@ def _filter_thread_fields(
400
270
  thread: Thread, select: list[ThreadSelectField] | None
401
271
  ) -> dict[str, Any]:
402
272
  if not select:
403
- return thread
273
+ return dict(thread)
404
274
  return {field: thread[field] for field in select if field in thread}
405
275
 
406
276
 
@@ -628,7 +498,7 @@ class Assistants(Authenticated):
628
498
  graph_id=graph_id,
629
499
  filters=auth_filters or {},
630
500
  if_exists=on_conflict,
631
- config=map_configurable(config),
501
+ config=config_conversion.config_to_proto(config),
632
502
  context=dict_to_struct(context or {}),
633
503
  name=name,
634
504
  description=description,
@@ -651,7 +521,7 @@ class Assistants(Authenticated):
651
521
  conn, # Not used in gRPC implementation
652
522
  assistant_id: UUID | str,
653
523
  *,
654
- config: dict | None = None,
524
+ config: Config | None = None,
655
525
  context: Context | None = None,
656
526
  graph_id: str | None = None,
657
527
  metadata: MetadataInput | None = None,
@@ -661,7 +531,7 @@ class Assistants(Authenticated):
661
531
  ) -> AsyncIterator[Assistant]: # type: ignore[return-value]
662
532
  """Update assistant via gRPC."""
663
533
  metadata = metadata if metadata is not None else {}
664
- config = config if config is not None else {}
534
+ config = config if config is not None else Config()
665
535
  # Handle auth filters
666
536
  auth_filters = await Assistants.handle_event(
667
537
  ctx,
@@ -691,7 +561,7 @@ class Assistants(Authenticated):
691
561
 
692
562
  # Add optional config if provided
693
563
  if config:
694
- request.config.CopyFrom(map_configurable(config))
564
+ request.config.CopyFrom(config_conversion.config_to_proto(config))
695
565
 
696
566
  # Add optional context if provided
697
567
  if context:
@@ -812,7 +682,7 @@ class Assistants(Authenticated):
812
682
  "graph_id": version.graph_id,
813
683
  "version": version.version,
814
684
  "created_at": version.created_at.ToDatetime(tzinfo=UTC),
815
- "config": _runnable_config_to_user_dict(version.config),
685
+ "config": config_conversion.config_from_proto(version.config),
816
686
  "context": struct_to_dict(version.context),
817
687
  "metadata": struct_to_dict(version.metadata),
818
688
  "name": version.name,
@@ -16,12 +16,12 @@
16
16
  "@langchain/langgraph-checkpoint": "^1.0.0",
17
17
  "@types/json-schema": "^7.0.15",
18
18
  "@typescript/vfs": "^1.6.0",
19
- "dedent": "^1.5.3",
19
+ "dedent": "^1.7.0",
20
20
  "exit-hook": "^4.0.0",
21
- "hono": "^4.10.3",
21
+ "hono": "^4.10.4",
22
22
  "p-queue": "^8.0.1",
23
23
  "p-retry": "^6.2.0",
24
- "tsx": "^4.19.3",
24
+ "tsx": "^4.20.6",
25
25
  "typescript": "^5.5.4",
26
26
  "undici": "^6.21.2",
27
27
  "uuid": "^10.0.0",
@@ -40,8 +40,8 @@
40
40
  "@types/react-dom": "^19.0.3",
41
41
  "jose": "^6.0.10",
42
42
  "postgres": "^3.4.4",
43
- "prettier": "^3.3.3",
44
- "vitest": "^3.0.5"
43
+ "prettier": "^3.6.2",
44
+ "vitest": "^4.0.6"
45
45
  },
46
46
  "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
47
47
  }