langgraph-api 0.5.2__py3-none-any.whl → 0.5.4__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/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.5.2"
1
+ __version__ = "0.5.4"
@@ -355,34 +355,39 @@ def _solve_fastapi_dependencies(
355
355
  }
356
356
 
357
357
  async def decorator(scope: dict, request: Request):
358
- async with AsyncExitStack() as stack:
359
- all_solved = await asyncio.gather(
360
- *(
361
- solve_dependencies(
362
- request=request,
363
- dependant=dependent,
364
- async_exit_stack=stack,
365
- embed_body_fields=False,
358
+ async with AsyncExitStack() as request_stack:
359
+ scope["fastapi_inner_astack"] = request_stack
360
+ async with AsyncExitStack() as stack:
361
+ scope["fastapi_function_astack"] = stack
362
+ all_solved = await asyncio.gather(
363
+ *(
364
+ solve_dependencies(
365
+ request=request,
366
+ dependant=dependent,
367
+ async_exit_stack=stack,
368
+ embed_body_fields=False,
369
+ )
370
+ for dependent in dependents.values()
366
371
  )
367
- for dependent in dependents.values()
368
372
  )
369
- )
370
- all_injected = await asyncio.gather(
371
- *(
372
- _run_async(dependent.call, solved.values, is_async)
373
- for dependent, solved in zip(
374
- dependents.values(), all_solved, strict=False
373
+ all_injected = await asyncio.gather(
374
+ *(
375
+ _run_async(dependent.call, solved.values, is_async)
376
+ for dependent, solved in zip(
377
+ dependents.values(), all_solved, strict=False
378
+ )
375
379
  )
376
380
  )
377
- )
378
- kwargs = {
379
- name: value
380
- for name, value in zip(dependents.keys(), all_injected, strict=False)
381
- }
382
- other_params = _extract_arguments_from_scope(
383
- scope, _param_names, request=request
384
- )
385
- return await fn(**(kwargs | other_params))
381
+ kwargs = {
382
+ name: value
383
+ for name, value in zip(
384
+ dependents.keys(), all_injected, strict=False
385
+ )
386
+ }
387
+ other_params = _extract_arguments_from_scope(
388
+ scope, _param_names, request=request
389
+ )
390
+ return await fn(**(kwargs | other_params))
386
391
 
387
392
  return decorator
388
393
 
@@ -0,0 +1,225 @@
1
+ """Conversion utils for the RunnableConfig."""
2
+
3
+ # THIS IS DUPLICATED
4
+ # TODO: WFH - Deduplicate with the executor logic by moving into a separate package
5
+ # Sequencing in the next PR.
6
+ from typing import Any, cast
7
+
8
+ import orjson
9
+ from langchain_core.runnables.config import RunnableConfig
10
+
11
+ from langgraph_api.grpc_ops.generated import engine_common_pb2
12
+
13
+ CONFIG_KEY_SEND = "__pregel_send"
14
+ CONFIG_KEY_READ = "__pregel_read"
15
+ CONFIG_KEY_RESUMING = "__pregel_resuming"
16
+ CONFIG_KEY_TASK_ID = "__pregel_task_id"
17
+ CONFIG_KEY_THREAD_ID = "thread_id"
18
+ CONFIG_KEY_CHECKPOINT_MAP = "checkpoint_map"
19
+ CONFIG_KEY_CHECKPOINT_ID = "checkpoint_id"
20
+ CONFIG_KEY_CHECKPOINT_NS = "checkpoint_ns"
21
+ CONFIG_KEY_SCRATCHPAD = "__pregel_scratchpad"
22
+ CONFIG_KEY_DURABILITY = "__pregel_durability"
23
+ CONFIG_KEY_GRAPH_ID = "graph_id"
24
+
25
+
26
+ def _durability_to_proto(
27
+ durability: str,
28
+ ) -> engine_common_pb2.Durability:
29
+ match durability:
30
+ case "async":
31
+ return engine_common_pb2.Durability.ASYNC
32
+ case "sync":
33
+ return engine_common_pb2.Durability.SYNC
34
+ case "exit":
35
+ return engine_common_pb2.Durability.EXIT
36
+ case _:
37
+ raise ValueError(f"invalid durability: {durability}")
38
+
39
+
40
+ def _durability_from_proto(
41
+ durability: engine_common_pb2.Durability,
42
+ ) -> str:
43
+ match durability:
44
+ case engine_common_pb2.Durability.ASYNC:
45
+ return "async"
46
+ case engine_common_pb2.Durability.SYNC:
47
+ return "sync"
48
+ case engine_common_pb2.Durability.EXIT:
49
+ return "exit"
50
+ case _:
51
+ raise ValueError(f"invalid durability: {durability}")
52
+
53
+
54
+ def config_to_proto(
55
+ config: RunnableConfig,
56
+ ) -> engine_common_pb2.EngineRunnableConfig | None:
57
+ # Prepare kwargs for construction
58
+ if not config:
59
+ return None
60
+ cp = {**config}
61
+ pb_config = engine_common_pb2.EngineRunnableConfig()
62
+ for k, v in (cp.pop("metadata", None) or {}).items():
63
+ if k == "run_attempt":
64
+ pb_config.run_attempt = v
65
+ elif k == "run_id":
66
+ pb_config.server_run_id = str(v)
67
+ else:
68
+ pb_config.metadata_json[k] = orjson.dumps(v)
69
+ if run_name := cp.pop("run_name", None):
70
+ pb_config.run_name = run_name
71
+
72
+ if run_id := cp.pop("run_id", None):
73
+ pb_config.run_id = str(run_id)
74
+
75
+ if (max_concurrency := cp.pop("max_concurrency", None)) and isinstance(
76
+ max_concurrency, int
77
+ ):
78
+ pb_config.max_concurrency = max_concurrency
79
+
80
+ if (recursion_limit := cp.pop("recursion_limit", None)) and isinstance(
81
+ recursion_limit, int
82
+ ):
83
+ pb_config.recursion_limit = recursion_limit
84
+
85
+ # Handle collections after construction
86
+ if (tags := cp.pop("tags", None)) and isinstance(tags, list):
87
+ pb_config.tags.extend(tags)
88
+
89
+ if (configurable := cp.pop("configurable", None)) and isinstance(
90
+ configurable, dict
91
+ ):
92
+ _inject_configurable_into_proto(configurable, pb_config)
93
+ if cp:
94
+ pb_config.extra_json.update({k: orjson.dumps(v) for k, v in cp.items()})
95
+
96
+ return pb_config
97
+
98
+
99
+ RESTRICTED_RESERVED_CONFIGURABLE_KEYS = {
100
+ CONFIG_KEY_SEND,
101
+ CONFIG_KEY_READ,
102
+ CONFIG_KEY_SCRATCHPAD,
103
+ }
104
+
105
+
106
+ def _inject_configurable_into_proto(
107
+ configurable: dict[str, Any], proto: engine_common_pb2.EngineRunnableConfig
108
+ ) -> None:
109
+ extra = {}
110
+ for key, value in configurable.items():
111
+ if key == CONFIG_KEY_RESUMING:
112
+ proto.resuming = bool(value)
113
+ elif key == CONFIG_KEY_TASK_ID:
114
+ proto.task_id = str(value)
115
+ elif key == CONFIG_KEY_THREAD_ID:
116
+ proto.thread_id = str(value)
117
+ elif key == CONFIG_KEY_CHECKPOINT_MAP:
118
+ proto.checkpoint_map.update(cast(dict[str, str], value))
119
+ elif key == CONFIG_KEY_CHECKPOINT_ID:
120
+ proto.checkpoint_id = str(value)
121
+ elif key == CONFIG_KEY_CHECKPOINT_NS:
122
+ proto.checkpoint_ns = str(value)
123
+ elif key == CONFIG_KEY_DURABILITY and value:
124
+ proto.durability = _durability_to_proto(value)
125
+ elif key not in RESTRICTED_RESERVED_CONFIGURABLE_KEYS:
126
+ extra[key] = value
127
+ if extra:
128
+ proto.extra_configurable_json.update(
129
+ {k: orjson.dumps(v) for k, v in extra.items()}
130
+ )
131
+
132
+
133
+ def context_to_json_bytes(context: dict[str, Any] | Any) -> bytes | None:
134
+ """Convert context to JSON bytes for proto serialization."""
135
+ if context is None:
136
+ return None
137
+
138
+ # Convert dataclass or other objects to dict if needed
139
+ if hasattr(context, "__dict__") and not hasattr(context, "items"):
140
+ # Convert dataclass to dict
141
+ context_dict = context.__dict__
142
+ elif hasattr(context, "items"):
143
+ # Already a dict-like object
144
+ context_dict = dict(context)
145
+ else:
146
+ # Try to convert to dict using vars()
147
+ context_dict = vars(context) if hasattr(context, "__dict__") else {}
148
+
149
+ return orjson.dumps(context_dict)
150
+
151
+
152
+ def config_from_proto(
153
+ config_proto: engine_common_pb2.EngineRunnableConfig | None,
154
+ ) -> RunnableConfig:
155
+ if not config_proto:
156
+ return RunnableConfig(tags=[], metadata={}, configurable={})
157
+
158
+ configurable = _configurable_from_proto(config_proto)
159
+
160
+ metadata = {}
161
+ for k, v in config_proto.metadata_json.items():
162
+ metadata[k] = orjson.loads(v)
163
+ if config_proto.HasField("run_attempt"):
164
+ metadata["run_attempt"] = config_proto.run_attempt
165
+ if config_proto.HasField("server_run_id"):
166
+ metadata["run_id"] = config_proto.server_run_id
167
+
168
+ config = RunnableConfig()
169
+ if config_proto.extra_json:
170
+ for k, v in config_proto.extra_json.items():
171
+ config[k] = orjson.loads(v) # type: ignore[invalid-key]
172
+ if config_proto.tags:
173
+ config["tags"] = list(config_proto.tags)
174
+ if metadata:
175
+ config["metadata"] = metadata
176
+ if configurable:
177
+ config["configurable"] = configurable
178
+ if config_proto.HasField("run_name"):
179
+ config["run_name"] = config_proto.run_name
180
+
181
+ if config_proto.HasField("max_concurrency"):
182
+ config["max_concurrency"] = config_proto.max_concurrency
183
+
184
+ if config_proto.HasField("recursion_limit"):
185
+ config["recursion_limit"] = config_proto.recursion_limit
186
+
187
+ return config
188
+
189
+
190
+ def _configurable_from_proto(
191
+ config_proto: engine_common_pb2.EngineRunnableConfig,
192
+ ) -> dict[str, Any]:
193
+ configurable = {}
194
+
195
+ if config_proto.HasField("resuming"):
196
+ configurable[CONFIG_KEY_RESUMING] = config_proto.resuming
197
+
198
+ if config_proto.HasField("task_id"):
199
+ configurable[CONFIG_KEY_TASK_ID] = config_proto.task_id
200
+
201
+ if config_proto.HasField("thread_id"):
202
+ configurable[CONFIG_KEY_THREAD_ID] = config_proto.thread_id
203
+
204
+ if config_proto.HasField("checkpoint_id"):
205
+ configurable[CONFIG_KEY_CHECKPOINT_ID] = config_proto.checkpoint_id
206
+
207
+ if config_proto.HasField("checkpoint_ns"):
208
+ configurable[CONFIG_KEY_CHECKPOINT_NS] = config_proto.checkpoint_ns
209
+
210
+ if config_proto.HasField("durability"):
211
+ durability = _durability_from_proto(config_proto.durability)
212
+ if durability:
213
+ configurable[CONFIG_KEY_DURABILITY] = durability
214
+
215
+ if config_proto.HasField("graph_id"):
216
+ configurable[CONFIG_KEY_GRAPH_ID] = config_proto.graph_id
217
+
218
+ if len(config_proto.checkpoint_map) > 0:
219
+ configurable[CONFIG_KEY_CHECKPOINT_MAP] = dict(config_proto.checkpoint_map)
220
+
221
+ if len(config_proto.extra_configurable_json) > 0:
222
+ for k, v in config_proto.extra_configurable_json.items():
223
+ configurable[k] = orjson.loads(v)
224
+
225
+ return configurable