mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-03 06:55:13 +02:00
Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
febf241b30 | ||
|
|
7ab5788f25 | ||
|
|
b0a1029d55 | ||
|
|
c2ef3f3fd3 | ||
|
|
d455bd841d | ||
|
|
69a09adef6 | ||
|
|
35aa98b110 | ||
|
|
0f83d9fafe | ||
|
|
52d66df92c | ||
|
|
4ec92f9fb1 | ||
|
|
2b72953064 | ||
|
|
232014e8ef | ||
|
|
9fd3dfc542 | ||
|
|
ff38f75594 | ||
|
|
bbdd007341 | ||
|
|
3c75e414e5 |
@@ -81,7 +81,7 @@ REDIRECT_MAP = {
|
||||
# cloud redirects
|
||||
"cloud/index.md": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"cloud/how-tos/index.md": "https://docs.langchain.com/langsmith/home",
|
||||
"cloud/concepts/api.md": "https://docs.langchain.com/langsmith/langgraph-server",
|
||||
"cloud/concepts/api.md": "https://docs.langchain.com/langsmith/agent-server",
|
||||
"cloud/concepts/cloud.md": "https://docs.langchain.com/langsmith/cloud",
|
||||
"cloud/faq/studio.md": "https://docs.langchain.com/langsmith/studio",
|
||||
"cloud/how-tos/human_in_the_loop_edit_state.md": "https://docs.langchain.com/langsmith/add-human-in-the-loop",
|
||||
@@ -111,8 +111,8 @@ REDIRECT_MAP = {
|
||||
"tutorials/introduction.ipynb": "https://docs.langchain.com/oss/python/langgraph/overview",
|
||||
"agents/deployment.md": "https://docs.langchain.com/oss/python/langgraph/local-server",
|
||||
# deployment redirects
|
||||
"how-tos/deploy-self-hosted.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"concepts/self_hosted.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"how-tos/deploy-self-hosted.md": "https://docs.langchain.com/langsmith/platform-setup",
|
||||
"concepts/self_hosted.md": "https://docs.langchain.com/langsmith/platform-setup",
|
||||
"tutorials/deployment.md": "https://docs.langchain.com/langsmith/deployments",
|
||||
# assistant redirects
|
||||
"cloud/how-tos/assistant_versioning.md": "https://docs.langchain.com/langsmith/configuration-cloud",
|
||||
@@ -138,7 +138,7 @@ REDIRECT_MAP = {
|
||||
"cloud/how-tos/generative_ui_react.md": "https://docs.langchain.com/langsmith/generative-ui-react",
|
||||
"concepts/langgraph_platform.md": "https://docs.langchain.com/langsmith/deployments",
|
||||
"concepts/langgraph_components.md": "https://docs.langchain.com/langsmith/components",
|
||||
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/langgraph-server",
|
||||
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/agent-server",
|
||||
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langsmith/data-plane",
|
||||
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langsmith/control-plane",
|
||||
"concepts/langgraph_cli.md": "https://docs.langchain.com/langsmith/cli",
|
||||
@@ -201,7 +201,7 @@ REDIRECT_MAP = {
|
||||
"cloud/deployment/egress.md": "https://docs.langchain.com/langsmith/env-var",
|
||||
"cloud/how-tos/streaming.md": "https://docs.langchain.com/langsmith/streaming",
|
||||
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langsmith/server-api-ref",
|
||||
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/langgraph-server-changelog",
|
||||
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/agent-server-changelog",
|
||||
"cloud/reference/api/api_ref_control_plane.md": "https://docs.langchain.com/langsmith/api-ref-control-plane",
|
||||
"cloud/reference/cli.md": "https://docs.langchain.com/langsmith/cli",
|
||||
"cloud/reference/env_var.md": "https://docs.langchain.com/langsmith/env-var",
|
||||
@@ -294,8 +294,8 @@ REDIRECT_MAP = {
|
||||
"cloud/deployment/custom_docker.md": "https://docs.langchain.com/langsmith/custom-docker",
|
||||
"cloud/deployment/egress.md": "https://docs.langchain.com/langsmith/env-var",
|
||||
"cloud/deployment/graph_rebuild.md": "https://docs.langchain.com/langsmith/graph-rebuild",
|
||||
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"cloud/deployment/self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/platform-setup",
|
||||
"cloud/deployment/self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/platform-setup",
|
||||
"cloud/deployment/semantic_search.md": "https://docs.langchain.com/langsmith/semantic-search",
|
||||
"cloud/deployment/setup_javascript.md": "https://docs.langchain.com/langsmith/setup-javascript",
|
||||
"cloud/deployment/setup_pyproject.md": "https://docs.langchain.com/langsmith/setup-pyproject",
|
||||
@@ -331,7 +331,7 @@ REDIRECT_MAP = {
|
||||
"cloud/reference/api/api_ref.md": "https://docs.langchain.com/langsmith/server-api-ref",
|
||||
"cloud/reference/cli.md": "https://docs.langchain.com/langsmith/cli",
|
||||
"cloud/reference/env_var.md": "https://docs.langchain.com/langsmith/env-var",
|
||||
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/langgraph-server-changelog",
|
||||
"cloud/reference/langgraph_server_changelog.md": "https://docs.langchain.com/langsmith/agent-server-changelog",
|
||||
"cloud/reference/sdk/js_ts_sdk_ref.md": "https://reference.langchain.com/javascript/modules/langsmith.html",
|
||||
"concepts/application_structure.md": "https://docs.langchain.com/langsmith/application-structure",
|
||||
"concepts/assistants.md": "https://docs.langchain.com/langsmith/assistants",
|
||||
@@ -345,9 +345,9 @@ REDIRECT_MAP = {
|
||||
"concepts/langgraph_control_plane.md": "https://docs.langchain.com/langsmith/control-plane",
|
||||
"concepts/langgraph_data_plane.md": "https://docs.langchain.com/langsmith/data-plane",
|
||||
"concepts/langgraph_platform.md": "https://docs.langchain.com/langsmith/home",
|
||||
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/hosting",
|
||||
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/langgraph-server",
|
||||
"concepts/langgraph_self_hosted_control_plane.md": "https://docs.langchain.com/langsmith/platform-setup",
|
||||
"concepts/langgraph_self_hosted_data_plane.md": "https://docs.langchain.com/langsmith/platform-setup",
|
||||
"concepts/langgraph_server.md": "https://docs.langchain.com/langsmith/agent-server",
|
||||
"concepts/langgraph_standalone_container.md": "https://docs.langchain.com/langsmith/docker",
|
||||
"concepts/langgraph_studio.md": "https://docs.langchain.com/langsmith/studio",
|
||||
"concepts/plans.md": "https://docs.langchain.com/langsmith/home",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 LangChain, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -97,7 +97,7 @@ class PostgresSaver(BasePostgresSaver):
|
||||
strict=False,
|
||||
):
|
||||
cur.execute(migration)
|
||||
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
|
||||
if self.pipe:
|
||||
self.pipe.sync()
|
||||
|
||||
|
||||
@@ -102,7 +102,9 @@ class AsyncPostgresSaver(BasePostgresSaver):
|
||||
strict=False,
|
||||
):
|
||||
await cur.execute(migration)
|
||||
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||
await cur.execute(
|
||||
"INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
|
||||
)
|
||||
if self.pipe:
|
||||
await self.pipe.sync()
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ MIGRATIONS = [
|
||||
"""
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
|
||||
""",
|
||||
"""ALTER TABLE checkpoint_writes ADD COLUMN task_path TEXT NOT NULL DEFAULT '';""",
|
||||
"""ALTER TABLE checkpoint_writes ADD COLUMN IF NOT EXISTS task_path TEXT NOT NULL DEFAULT '';""",
|
||||
]
|
||||
|
||||
SELECT_SQL = """
|
||||
|
||||
@@ -77,7 +77,7 @@ MIGRATIONS = [
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS checkpoint_writes_thread_id_idx ON checkpoint_writes(thread_id);
|
||||
""",
|
||||
"""
|
||||
ALTER TABLE checkpoint_writes ADD COLUMN task_path TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE checkpoint_writes ADD COLUMN IF NOT EXISTS task_path TEXT NOT NULL DEFAULT '';
|
||||
""",
|
||||
]
|
||||
|
||||
@@ -252,7 +252,7 @@ class ShallowPostgresSaver(BasePostgresSaver):
|
||||
strict=False,
|
||||
):
|
||||
cur.execute(migration)
|
||||
cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||
cur.execute("INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,))
|
||||
if self.pipe:
|
||||
self.pipe.sync()
|
||||
|
||||
@@ -614,7 +614,9 @@ class AsyncShallowPostgresSaver(BasePostgresSaver):
|
||||
strict=False,
|
||||
):
|
||||
await cur.execute(migration)
|
||||
await cur.execute(f"INSERT INTO checkpoint_migrations (v) VALUES ({v})")
|
||||
await cur.execute(
|
||||
"INSERT INTO checkpoint_migrations (v) VALUES (%s)", (v,)
|
||||
)
|
||||
if self.pipe:
|
||||
await self.pipe.sync()
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "3.0.0"
|
||||
version = "3.0.1"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
requires-python = ">=3.10"
|
||||
@@ -19,7 +19,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-postgres"
|
||||
Twitter = "https://x.com/LangChainAI"
|
||||
Slack = "https://www.langchain.com/join-community"
|
||||
Reddit = "https://www.reddit.com/r/LangChain/"
|
||||
|
||||
[dependency-groups]
|
||||
test = [
|
||||
|
||||
Generated
+1
-1
@@ -281,7 +281,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "3.0.0"
|
||||
version = "3.0.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "langgraph-checkpoint" },
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 LangChain, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -18,7 +18,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint-sqlite"
|
||||
Twitter = "https://x.com/LangChainAI"
|
||||
Slack = "https://www.langchain.com/join-community"
|
||||
Reddit = "https://www.reddit.com/r/LangChain/"
|
||||
|
||||
[dependency-groups]
|
||||
test = [
|
||||
|
||||
@@ -17,7 +17,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint"
|
||||
Twitter = "https://x.com/LangChainAI"
|
||||
Slack = "https://www.langchain.com/join-community"
|
||||
Reddit = "https://www.reddit.com/r/LangChain/"
|
||||
|
||||
[dependency-groups]
|
||||
test = [
|
||||
|
||||
@@ -25,7 +25,10 @@ inmem = [
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/cli"
|
||||
Twitter = "https://x.com/LangChainAI"
|
||||
Slack = "https://www.langchain.com/join-community"
|
||||
Reddit = "https://www.reddit.com/r/LangChain/"
|
||||
|
||||
[project.scripts]
|
||||
langgraph = "langgraph_cli.cli:cli"
|
||||
|
||||
@@ -30,6 +30,7 @@ __all__ = (
|
||||
"add_messages",
|
||||
"MessagesState",
|
||||
"MessageGraph",
|
||||
"REMOVE_ALL_MESSAGES",
|
||||
)
|
||||
|
||||
Messages = list[MessageLikeRepresentation] | MessageLikeRepresentation
|
||||
|
||||
@@ -289,7 +289,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
destinations: dict[str, str] | tuple[str, ...] | None = None,
|
||||
**kwargs: Unpack[DeprecatedKwargs],
|
||||
) -> Self:
|
||||
"""Add a new node to the state graph, input schema is inferred as the state schema.
|
||||
"""Add a new node to the `StateGraph`, input schema is inferred as the state schema.
|
||||
Will take the name of the function/runnable as the node name.
|
||||
"""
|
||||
...
|
||||
@@ -307,7 +307,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
destinations: dict[str, str] | tuple[str, ...] | None = None,
|
||||
**kwargs: Unpack[DeprecatedKwargs],
|
||||
) -> Self:
|
||||
"""Add a new node to the state graph, input schema is specified.
|
||||
"""Add a new node to the `StateGraph`, input schema is specified.
|
||||
Will take the name of the function/runnable as the node name.
|
||||
"""
|
||||
...
|
||||
@@ -326,7 +326,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
destinations: dict[str, str] | tuple[str, ...] | None = None,
|
||||
**kwargs: Unpack[DeprecatedKwargs],
|
||||
) -> Self:
|
||||
"""Add a new node to the state graph, input schema is inferred as the state schema."""
|
||||
"""Add a new node to the `StateGraph`, input schema is inferred as the state schema."""
|
||||
...
|
||||
|
||||
@overload
|
||||
@@ -343,7 +343,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
destinations: dict[str, str] | tuple[str, ...] | None = None,
|
||||
**kwargs: Unpack[DeprecatedKwargs],
|
||||
) -> Self:
|
||||
"""Add a new node to the state graph, input schema is specified."""
|
||||
"""Add a new node to the `StateGraph`, input schema is specified."""
|
||||
...
|
||||
|
||||
def add_node(
|
||||
@@ -359,7 +359,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
destinations: dict[str, str] | tuple[str, ...] | None = None,
|
||||
**kwargs: Unpack[DeprecatedKwargs],
|
||||
) -> Self:
|
||||
"""Add a new node to the state graph.
|
||||
"""Add a new node to the `StateGraph`.
|
||||
|
||||
Args:
|
||||
node: The function or runnable this node will run.
|
||||
@@ -416,7 +416,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
```
|
||||
|
||||
Returns:
|
||||
Self: The instance of the state graph, allowing for method chaining.
|
||||
Self: The instance of the `StateGraph`, allowing for method chaining.
|
||||
"""
|
||||
if (retry := kwargs.get("retry", MISSING)) is not MISSING:
|
||||
warnings.warn(
|
||||
@@ -571,7 +571,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
ValueError: If the start key is `'END'` or if the start key or end key is not present in the graph.
|
||||
|
||||
Returns:
|
||||
Self: The instance of the state graph, allowing for method chaining.
|
||||
Self: The instance of the `StateGraph`, allowing for method chaining.
|
||||
"""
|
||||
if self.compiled:
|
||||
logger.warning(
|
||||
@@ -676,7 +676,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
ValueError: If the sequence contains duplicate node names.
|
||||
|
||||
Returns:
|
||||
Self: The instance of the state graph, allowing for method chaining.
|
||||
Self: The instance of the `StateGraph`, allowing for method chaining.
|
||||
"""
|
||||
if len(nodes) < 1:
|
||||
raise ValueError("Sequence requires at least one node.")
|
||||
@@ -809,7 +809,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
debug: bool = False,
|
||||
name: str | None = None,
|
||||
) -> CompiledStateGraph[StateT, ContextT, InputT, OutputT]:
|
||||
"""Compiles the state graph into a `CompiledStateGraph` object.
|
||||
"""Compiles the `StateGraph` into a `CompiledStateGraph` object.
|
||||
|
||||
The compiled graph implements the `Runnable` interface and can be invoked,
|
||||
streamed, batched, and run asynchronously.
|
||||
@@ -826,7 +826,7 @@ class StateGraph(Generic[StateT, ContextT, InputT, OutputT]):
|
||||
name: The name to use for the compiled graph.
|
||||
|
||||
Returns:
|
||||
CompiledStateGraph: The compiled state graph.
|
||||
CompiledStateGraph: The compiled `StateGraph`.
|
||||
"""
|
||||
# assign default values
|
||||
interrupt_before = interrupt_before or []
|
||||
|
||||
@@ -459,9 +459,6 @@ class PregelLoop:
|
||||
def tick(self) -> bool:
|
||||
"""Execute a single iteration of the Pregel loop.
|
||||
|
||||
Args:
|
||||
input_keys: The key(s) to read input from.
|
||||
|
||||
Returns:
|
||||
True if more iterations are needed.
|
||||
"""
|
||||
|
||||
@@ -9,7 +9,7 @@ from collections.abc import Awaitable, Callable, Sequence
|
||||
from dataclasses import replace
|
||||
from typing import Any
|
||||
|
||||
from langgraph._internal._config import patch_configurable
|
||||
from langgraph._internal._config import patch_configurable, recast_checkpoint_ns
|
||||
from langgraph._internal._constants import (
|
||||
CONF,
|
||||
CONFIG_KEY_CHECKPOINT_NS,
|
||||
@@ -43,16 +43,16 @@ def run_with_retry(
|
||||
except ParentCommand as exc:
|
||||
ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
|
||||
cmd = exc.args[0]
|
||||
if cmd.graph in (ns, task.name):
|
||||
if cmd.graph in (recast_checkpoint_ns(ns), task.name):
|
||||
# this command is for the current graph, handle it
|
||||
for w in task.writers:
|
||||
w.invoke(cmd, config)
|
||||
break
|
||||
elif cmd.graph == Command.PARENT:
|
||||
# this command is for the parent graph, assign it to the parent
|
||||
parts = ns.split(NS_SEP)
|
||||
if parts[-1].isdigit():
|
||||
parts.pop()
|
||||
# normalize namespace by removing task IDs
|
||||
recast_ns = recast_checkpoint_ns(ns)
|
||||
parts = recast_ns.split(NS_SEP)
|
||||
parent_ns = NS_SEP.join(parts[:-1])
|
||||
exc.args = (replace(cmd, graph=parent_ns),)
|
||||
# bubble up
|
||||
@@ -138,16 +138,16 @@ async def arun_with_retry(
|
||||
except ParentCommand as exc:
|
||||
ns: str = config[CONF][CONFIG_KEY_CHECKPOINT_NS]
|
||||
cmd = exc.args[0]
|
||||
if cmd.graph in (ns, task.name):
|
||||
if cmd.graph in (recast_checkpoint_ns(ns), task.name):
|
||||
# this command is for the current graph, handle it
|
||||
for w in task.writers:
|
||||
w.invoke(cmd, config)
|
||||
break
|
||||
elif cmd.graph == Command.PARENT:
|
||||
# this command is for the parent graph, assign it to the parent
|
||||
parts = ns.split(NS_SEP)
|
||||
if parts[-1].isdigit():
|
||||
parts.pop()
|
||||
# normalize namespace by removing task IDs
|
||||
recast_ns = recast_checkpoint_ns(ns)
|
||||
parts = recast_ns.split(NS_SEP)
|
||||
parent_ns = NS_SEP.join(parts[:-1])
|
||||
exc.args = (replace(cmd, graph=parent_ns),)
|
||||
# bubble up
|
||||
|
||||
@@ -1702,7 +1702,9 @@ class Pregel(
|
||||
# we use the task id generated by prepare_next_tasks
|
||||
node_to_task_ids: dict[str, deque[str]] = defaultdict(deque)
|
||||
if saved is not None and saved.pending_writes is not None:
|
||||
# tasks for this checkpoint
|
||||
# we call prepare_next_tasks to discover the task IDs that
|
||||
# would have been generated, so we can reuse them and
|
||||
# properly populate task.result in state history
|
||||
next_tasks = prepare_next_tasks(
|
||||
checkpoint,
|
||||
saved.pending_writes,
|
||||
@@ -1721,32 +1723,6 @@ class Pregel(
|
||||
for t in next_tasks.values():
|
||||
node_to_task_ids[t.name].append(t.id)
|
||||
|
||||
# apply null writes
|
||||
if null_writes := [
|
||||
w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
|
||||
]:
|
||||
apply_writes(
|
||||
checkpoint,
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
checkpointer.get_next_version,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
# apply writes
|
||||
for tid, k, v in saved.pending_writes:
|
||||
if k in (ERROR, INTERRUPT):
|
||||
continue
|
||||
if tid not in next_tasks:
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
if tasks := [t for t in next_tasks.values() if t.writes]:
|
||||
apply_writes(
|
||||
checkpoint,
|
||||
channels,
|
||||
tasks,
|
||||
checkpointer.get_next_version,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
valid_updates: list[tuple[str, dict[str, Any] | None, str | None]] = []
|
||||
if len(updates) == 1:
|
||||
values, as_node, task_id = updates[0]
|
||||
@@ -2167,7 +2143,9 @@ class Pregel(
|
||||
# we use the task id generated by prepare_next_tasks
|
||||
node_to_task_ids: dict[str, deque[str]] = defaultdict(deque)
|
||||
if saved is not None and saved.pending_writes is not None:
|
||||
# tasks for this checkpoint
|
||||
# we call prepare_next_tasks to discover the task IDs that
|
||||
# would have been generated, so we can reuse them and
|
||||
# properly populate task.result in state history
|
||||
next_tasks = prepare_next_tasks(
|
||||
checkpoint,
|
||||
saved.pending_writes,
|
||||
@@ -2186,31 +2164,6 @@ class Pregel(
|
||||
for t in next_tasks.values():
|
||||
node_to_task_ids[t.name].append(t.id)
|
||||
|
||||
# apply null writes
|
||||
if null_writes := [
|
||||
w[1:] for w in saved.pending_writes or [] if w[0] == NULL_TASK_ID
|
||||
]:
|
||||
apply_writes(
|
||||
checkpoint,
|
||||
channels,
|
||||
[PregelTaskWrites((), INPUT, null_writes, [])],
|
||||
checkpointer.get_next_version,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
for tid, k, v in saved.pending_writes:
|
||||
if k in (ERROR, INTERRUPT):
|
||||
continue
|
||||
if tid not in next_tasks:
|
||||
continue
|
||||
next_tasks[tid].writes.append((k, v))
|
||||
if tasks := [t for t in next_tasks.values() if t.writes]:
|
||||
apply_writes(
|
||||
checkpoint,
|
||||
channels,
|
||||
tasks,
|
||||
checkpointer.get_next_version,
|
||||
self.trigger_to_nodes,
|
||||
)
|
||||
valid_updates: list[tuple[str, dict[str, Any] | None, str | None]] = []
|
||||
if len(updates) == 1:
|
||||
values, as_node, task_id = updates[0]
|
||||
@@ -2401,7 +2354,7 @@ class Pregel(
|
||||
validate_keys(output_keys, self.channels)
|
||||
interrupt_before = interrupt_before or self.interrupt_before_nodes
|
||||
interrupt_after = interrupt_after or self.interrupt_after_nodes
|
||||
if not isinstance(stream_mode, list):
|
||||
if isinstance(stream_mode, str):
|
||||
stream_modes = {stream_mode}
|
||||
else:
|
||||
stream_modes = set(stream_mode)
|
||||
@@ -2482,7 +2435,7 @@ class Pregel(
|
||||
You can pass a list as the `stream_mode` parameter to stream multiple modes at once.
|
||||
The streamed outputs will be tuples of `(mode, data)`.
|
||||
|
||||
See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details.
|
||||
See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
|
||||
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way.
|
||||
output_keys: The keys to stream, defaults to all non-context channels.
|
||||
interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph.
|
||||
@@ -2499,7 +2452,7 @@ class Pregel(
|
||||
where `namespace` is a tuple with the path to the node where a subgraph is invoked,
|
||||
e.g. `("parent_node:<task_id>", "child_node:<task_id>")`.
|
||||
|
||||
See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details.
|
||||
See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
|
||||
|
||||
Yields:
|
||||
The output of each step in the graph. The output shape depends on the `stream_mode`.
|
||||
@@ -2749,7 +2702,7 @@ class Pregel(
|
||||
You can pass a list as the `stream_mode` parameter to stream multiple modes at once.
|
||||
The streamed outputs will be tuples of `(mode, data)`.
|
||||
|
||||
See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details.
|
||||
See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
|
||||
print_mode: Accepts the same values as `stream_mode`, but only prints the output to the console, for debugging purposes. Does not affect the output of the graph in any way.
|
||||
output_keys: The keys to stream, defaults to all non-context channels.
|
||||
interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph.
|
||||
@@ -2766,7 +2719,7 @@ class Pregel(
|
||||
where `namespace` is a tuple with the path to the node where a subgraph is invoked,
|
||||
e.g. `("parent_node:<task_id>", "child_node:<task_id>")`.
|
||||
|
||||
See [LangGraph streaming guide](https://langchain-ai.github.io/langgraph/how-tos/streaming/) for more details.
|
||||
See [LangGraph streaming guide](https://docs.langchain.com/oss/python/langgraph/streaming) for more details.
|
||||
|
||||
Yields:
|
||||
The output of each step in the graph. The output shape depends on the `stream_mode`.
|
||||
|
||||
@@ -111,7 +111,7 @@ class Runtime(Generic[ContextT]):
|
||||
stream_writer=other.stream_writer
|
||||
if other.stream_writer is not _no_op_stream_writer
|
||||
else self.stream_writer,
|
||||
previous=other.previous or self.previous,
|
||||
previous=self.previous if other.previous is None else other.previous,
|
||||
)
|
||||
|
||||
def override(
|
||||
|
||||
@@ -31,13 +31,13 @@ ContextT_contra = TypeVar(
|
||||
)
|
||||
|
||||
InputT = TypeVar("InputT", bound=StateLike, default=StateT)
|
||||
"""Type variable used to represent the input to a state graph.
|
||||
"""Type variable used to represent the input to a `StateGraph`.
|
||||
|
||||
Defaults to `StateT`.
|
||||
"""
|
||||
|
||||
OutputT = TypeVar("OutputT", bound=StateLike, default=StateT)
|
||||
"""Type variable used to represent the output of a state graph.
|
||||
"""Type variable used to represent the output of a `StateGraph`.
|
||||
|
||||
Defaults to `StateT`.
|
||||
"""
|
||||
|
||||
@@ -32,8 +32,15 @@ dependencies = [
|
||||
"pydantic>=2.7.4",
|
||||
]
|
||||
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
Homepage = "https://docs.langchain.com/oss/python/langgraph/overview"
|
||||
Documentation = "https://reference.langchain.com/python/langgraph/"
|
||||
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/langgraph"
|
||||
Changelog = "https://github.com/langchain-ai/langgraph/releases"
|
||||
Twitter = "https://x.com/LangChainAI"
|
||||
Slack = "https://www.langchain.com/join-community"
|
||||
Reddit = "https://www.reddit.com/r/LangChain/"
|
||||
|
||||
[dependency-groups]
|
||||
test = [
|
||||
|
||||
@@ -7911,6 +7911,78 @@ def test_parent_command_goto(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("subgraph_persist", [True, False])
|
||||
def test_parent_command_goto_deeply_nested(
|
||||
sync_checkpointer: BaseCheckpointSaver, subgraph_persist: bool
|
||||
) -> None:
|
||||
"""Test Command.PARENT with goto in deeply nested graphs (3+ levels).
|
||||
|
||||
This tests the fix for issue #6409 where Command.PARENT with goto
|
||||
would fail in graphs with 3 or more levels of nesting due to
|
||||
namespace comparison issues.
|
||||
"""
|
||||
class State(TypedDict):
|
||||
messages: Annotated[list[str], operator.add]
|
||||
|
||||
# Level 3 (deepest): sub_sub_graph
|
||||
def sub_sub_node(state):
|
||||
"""Returns Command.PARENT to jump to grandparent's node."""
|
||||
return Command(
|
||||
graph=Command.PARENT,
|
||||
goto="sub_node_3",
|
||||
update={"messages": ["sub_sub_node"]},
|
||||
)
|
||||
|
||||
sub_sub_builder = StateGraph(State)
|
||||
sub_sub_builder.add_node("sub_sub_node", sub_sub_node)
|
||||
sub_sub_builder.add_edge(START, "sub_sub_node")
|
||||
sub_sub_graph = sub_sub_builder.compile(checkpointer=subgraph_persist)
|
||||
|
||||
# Level 2 (middle): sub_graph
|
||||
def sub_node_1(state):
|
||||
return {"messages": ["sub_node_1"]}
|
||||
|
||||
def sub_node_3(state):
|
||||
"""Target node for Command.PARENT goto."""
|
||||
return {"messages": ["sub_node_3"]}
|
||||
|
||||
sub_builder = StateGraph(State)
|
||||
sub_builder.add_node("sub_node_1", sub_node_1)
|
||||
sub_builder.add_node("sub_node_2", sub_sub_graph)
|
||||
sub_builder.add_node("sub_node_3", sub_node_3)
|
||||
sub_builder.add_edge(START, "sub_node_1")
|
||||
sub_builder.add_edge("sub_node_1", "sub_node_2")
|
||||
sub_graph = sub_builder.compile(checkpointer=subgraph_persist)
|
||||
|
||||
# Level 1 (top): main_graph
|
||||
def main_node_1(state):
|
||||
return {"messages": ["main_node_1"]}
|
||||
|
||||
main_builder = StateGraph(State)
|
||||
main_builder.add_node("main_node_1", main_node_1)
|
||||
main_builder.add_node("main_node_2", sub_graph)
|
||||
main_builder.add_edge(START, "main_node_1")
|
||||
main_builder.add_edge("main_node_1", "main_node_2")
|
||||
main_graph = main_builder.compile(sync_checkpointer, name="main")
|
||||
|
||||
config = {"configurable": {"thread_id": 1}}
|
||||
|
||||
result = main_graph.invoke(input={"messages": ["start"]}, config=config)
|
||||
|
||||
# Verify the execution order includes all expected nodes.
|
||||
# Note: When subgraphs have persistent checkpointers, parent state
|
||||
# is passed down, which may cause message duplication at subgraph boundaries.
|
||||
# The key assertion is that:
|
||||
# 1. All expected messages appear in order
|
||||
# 2. sub_node_3 executed (proving Command.PARENT goto worked)
|
||||
expected_messages = ["main_node_1", "sub_node_1", "sub_sub_node", "sub_node_3"]
|
||||
assert all(msg in result["messages"] for msg in expected_messages), (
|
||||
f"Expected all messages {expected_messages} to be in result {result['messages']}"
|
||||
)
|
||||
# Verify sub_node_3 executed last (the Command.PARENT goto target)
|
||||
assert result["messages"][-1] == "sub_node_3"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("with_timeout", [True, False])
|
||||
def test_timeout_with_parent_command(
|
||||
sync_checkpointer: BaseCheckpointSaver, with_timeout: bool
|
||||
@@ -8805,3 +8877,43 @@ def test_overwrite_parallel_error(
|
||||
InvalidUpdateError, match="Can receive only one Overwrite value per super-step."
|
||||
):
|
||||
graph.invoke({"messages": ["START"]}, config)
|
||||
|
||||
|
||||
def test_fork_does_not_apply_pending_writes(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Test that forking with update_state does not apply pending writes from original execution."""
|
||||
|
||||
class State(TypedDict):
|
||||
value: Annotated[int, operator.add]
|
||||
|
||||
def node_a(state: State) -> State:
|
||||
return {"value": 10}
|
||||
|
||||
def node_b(state: State) -> State:
|
||||
return {"value": 100}
|
||||
|
||||
graph = (
|
||||
StateGraph(State)
|
||||
.add_node("node_a", node_a)
|
||||
.add_node("node_b", node_b)
|
||||
.add_edge(START, "node_a")
|
||||
.add_edge("node_a", "node_b")
|
||||
.compile(checkpointer=sync_checkpointer)
|
||||
)
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
graph.invoke({"value": 1}, thread1)
|
||||
|
||||
history = list(graph.get_state_history(thread1))
|
||||
checkpoint_before_a = next(s for s in history if s.next == ("node_a",))
|
||||
|
||||
fork_config = graph.update_state(
|
||||
checkpoint_before_a.config, {"value": 20}, as_node="node_a"
|
||||
)
|
||||
|
||||
# Continue from fork (should run node_b)
|
||||
result = graph.invoke(None, fork_config)
|
||||
|
||||
# Should be: 1 (input) + 20 (forked node_a) + 100 (node_b) = 121
|
||||
assert result == {"value": 121}
|
||||
|
||||
@@ -9249,3 +9249,41 @@ async def test_supersteps_populate_task_results(
|
||||
|
||||
assert bulk_start_result == ref_start_result == {"num": 1, "text": "one"}
|
||||
assert bulk_double_result == ref_double_result == {"num": 2, "text": "oneone"}
|
||||
|
||||
|
||||
async def test_fork_does_not_apply_pending_writes(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""Test that forking with aupdate_state does not apply pending writes from original execution."""
|
||||
|
||||
class State(TypedDict):
|
||||
value: Annotated[int, operator.add]
|
||||
|
||||
def node_a(state: State) -> State:
|
||||
return {"value": 10}
|
||||
|
||||
def node_b(state: State) -> State:
|
||||
return {"value": 100}
|
||||
|
||||
graph = (
|
||||
StateGraph(State)
|
||||
.add_node("node_a", node_a)
|
||||
.add_node("node_b", node_b)
|
||||
.add_edge(START, "node_a")
|
||||
.add_edge("node_a", "node_b")
|
||||
.compile(checkpointer=async_checkpointer)
|
||||
)
|
||||
|
||||
thread1 = {"configurable": {"thread_id": "1"}}
|
||||
await graph.ainvoke({"value": 1}, thread1)
|
||||
|
||||
history = [c async for c in graph.aget_state_history(thread1)]
|
||||
checkpoint_before_a = next(s for s in history if s.next == ("node_a",))
|
||||
|
||||
fork_config = await graph.aupdate_state(
|
||||
checkpoint_before_a.config, {"value": 20}, as_node="node_a"
|
||||
)
|
||||
result = await graph.ainvoke(None, fork_config)
|
||||
|
||||
# 1 (input) + 20 (forked node_a) + 100 (node_b) = 121
|
||||
assert result == {"value": 121}
|
||||
|
||||
Generated
+1
-1
@@ -1571,7 +1571,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "3.0.0"
|
||||
version = "3.0.1"
|
||||
source = { editable = "../checkpoint-postgres" }
|
||||
dependencies = [
|
||||
{ name = "langgraph-checkpoint" },
|
||||
|
||||
@@ -29,7 +29,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/prebuilt"
|
||||
Twitter = "https://x.com/LangChainAI"
|
||||
Slack = "https://www.langchain.com/join-community"
|
||||
Reddit = "https://www.reddit.com/r/LangChain/"
|
||||
|
||||
[dependency-groups]
|
||||
test = [
|
||||
|
||||
Generated
+1
-1
@@ -377,7 +377,7 @@ test = [
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "3.0.0"
|
||||
version = "3.0.1"
|
||||
source = { editable = "../checkpoint-postgres" }
|
||||
dependencies = [
|
||||
{ name = "langgraph-checkpoint" },
|
||||
|
||||
@@ -259,7 +259,7 @@ class Auth:
|
||||
"""
|
||||
if self._authenticate_handler is not None:
|
||||
raise ValueError(
|
||||
"Authentication handler already set as {self._authenticate_handler}."
|
||||
f"Authentication handler already set as {self._authenticate_handler}."
|
||||
)
|
||||
self._authenticate_handler = fn
|
||||
return fn
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""The LangGraph client implementations connect to the LangGraph API.
|
||||
|
||||
This module provides both asynchronous ([get_client(url="http://localhost:2024"))](#get_client) or [LangGraphClient](#LangGraphClient))
|
||||
and synchronous ([get_sync_client(url="http://localhost:2024"))](#get_sync_client) or [SyncLanggraphClient](#SyncLanggraphClient))
|
||||
clients to interacting with the LangGraph API's core resources such as
|
||||
Assistants, Threads, Runs, and Cron jobs, as well as its persistent
|
||||
document Store.
|
||||
""" # noqa: E501
|
||||
This module provides both asynchronous (`get_client(url="http://localhost:2024")` or
|
||||
`LangGraphClient`) and synchronous (`get_sync_client(url="http://localhost:2024")` or
|
||||
`SyncLanggraphClient`) clients to interacting with the LangGraph API's core resources
|
||||
such as Assistants, Threads, Runs, and Cron jobs, as well as its persistent document
|
||||
Store.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
@@ -20,7 +20,10 @@ dependencies = [
|
||||
path = "langgraph_sdk/__init__.py"
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
Source = "https://github.com/langchain-ai/langgraph/tree/main/libs/sdk-py"
|
||||
Twitter = "https://x.com/LangChainAI"
|
||||
Slack = "https://www.langchain.com/join-community"
|
||||
Reddit = "https://www.reddit.com/r/LangChain/"
|
||||
|
||||
[dependency-groups]
|
||||
test = [
|
||||
|
||||
Reference in New Issue
Block a user