mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-11-06 22:31:17 +01:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5e37b5d70 |
No files matched your search
@@ -905,14 +905,10 @@ class Pregel(
|
||||
if not checkpoint_ns or CONFIG_KEY_CHECKPOINTER in config[CONF]:
|
||||
return None
|
||||
recast = recast_checkpoint_ns(checkpoint_ns)
|
||||
# Walk every subgraph rather than filter by `namespace=`: that filter
|
||||
# matches by prefix, so for `research_review` it can yield
|
||||
# `research|review` and skip a declared `research_review`.
|
||||
for name, pregel in self.get_subgraphs(recurse=True):
|
||||
if name == recast:
|
||||
return pregel, patch_configurable(
|
||||
config, {CONFIG_KEY_CHECKPOINTER: checkpointer}
|
||||
)
|
||||
for _, pregel in self.get_subgraphs(namespace=recast, recurse=True):
|
||||
return pregel, patch_configurable(
|
||||
config, {CONFIG_KEY_CHECKPOINTER: checkpointer}
|
||||
)
|
||||
if for_update:
|
||||
raise ValueError(f"Subgraph {recast} not found")
|
||||
return None
|
||||
@@ -1161,7 +1157,9 @@ class Pregel(
|
||||
"""Get the subgraphs of the graph.
|
||||
|
||||
Args:
|
||||
namespace: The namespace to filter the subgraphs by.
|
||||
namespace: Yield only the subgraph at this exact namespace, such
|
||||
as `parent|child` (without task IDs). A nested namespace needs
|
||||
`recurse=True`.
|
||||
recurse: Whether to recurse into the subgraphs.
|
||||
If `False`, only the immediate subgraphs will be returned.
|
||||
|
||||
@@ -1169,9 +1167,9 @@ class Pregel(
|
||||
An iterator of the `(namespace, subgraph)` pairs.
|
||||
"""
|
||||
for name, node in self.nodes.items():
|
||||
# filter by prefix
|
||||
# filter by the namespace's first segment, matched whole
|
||||
if namespace is not None:
|
||||
if not namespace.startswith(name):
|
||||
if namespace != name and not namespace.startswith(name + NS_SEP):
|
||||
continue
|
||||
|
||||
# find the subgraph, if any
|
||||
@@ -1185,12 +1183,13 @@ class Pregel(
|
||||
if namespace is None:
|
||||
yield name, graph
|
||||
if recurse and isinstance(graph, Pregel):
|
||||
if namespace is not None:
|
||||
namespace = namespace[len(name) + 1 :]
|
||||
child_namespace = (
|
||||
None if namespace is None else namespace[len(name) + 1 :]
|
||||
)
|
||||
yield from (
|
||||
(f"{name}{NS_SEP}{n}", s)
|
||||
for n, s in graph.get_subgraphs(
|
||||
namespace=namespace, recurse=recurse
|
||||
namespace=child_namespace, recurse=recurse
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1200,7 +1199,9 @@ class Pregel(
|
||||
"""Get the subgraphs of the graph.
|
||||
|
||||
Args:
|
||||
namespace: The namespace to filter the subgraphs by.
|
||||
namespace: Yield only the subgraph at this exact namespace, such
|
||||
as `parent|child` (without task IDs). A nested namespace needs
|
||||
`recurse=True`.
|
||||
recurse: Whether to recurse into the subgraphs.
|
||||
If `False`, only the immediate subgraphs will be returned.
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ Covers three checkpointer settings for subgraph state:
|
||||
- checkpointer=True: "stateful" — state accumulates across invocations on the same thread id
|
||||
|
||||
Also covers reading a subgraph's namespace from the parent graph, including
|
||||
one where no subgraph is declared, such as a subagent a tool invokes.
|
||||
one where no subgraph is declared, such as a subagent a tool invokes, and
|
||||
finding the subgraph a namespace names.
|
||||
"""
|
||||
|
||||
from typing import Annotated, Any
|
||||
@@ -916,3 +917,77 @@ def test_prefix_named_namespaces_resolve_to_their_own_graph(
|
||||
else:
|
||||
# The parent's channels have no `notes` or `verdict`.
|
||||
assert values("research_review") == {"messages": ["check it", "review done"]}
|
||||
|
||||
|
||||
def _prefix_named_subgraphs() -> tuple[Any, dict[str, Any]]:
|
||||
"""A parent whose subgraph namespaces start with one another's names:
|
||||
`research` has a `review` subgraph, and the parent declares
|
||||
`research_review` and `review` too. Returns the parent and each subgraph,
|
||||
keyed by its namespace."""
|
||||
graphs = {
|
||||
"research|review": _notes_graph("research's review"),
|
||||
"research_review": _notes_graph("research_review"),
|
||||
"review": _notes_graph("review"),
|
||||
}
|
||||
graphs["research"] = (
|
||||
StateGraph(NotesState)
|
||||
.add_node("review", graphs["research|review"])
|
||||
.add_edge(START, "review")
|
||||
.compile()
|
||||
)
|
||||
parent = (
|
||||
StateGraph(NotesState)
|
||||
.add_node("research", graphs["research"])
|
||||
.add_node("research_review", graphs["research_review"])
|
||||
.add_node("review", graphs["review"])
|
||||
.add_edge(START, "research")
|
||||
.add_edge("research", "research_review")
|
||||
.add_edge("research_review", "review")
|
||||
.compile()
|
||||
)
|
||||
return parent, graphs
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"namespace", ["research", "research|review", "research_review", "review"]
|
||||
)
|
||||
def test_get_subgraphs_matches_whole_namespace_segments(namespace: str) -> None:
|
||||
"""`get_subgraphs(namespace=...)` yields only the subgraph at that
|
||||
namespace. `research_review` isn't `research` then `review`, and
|
||||
`research|review` doesn't also find the parent's own `review`."""
|
||||
parent, graphs = _prefix_named_subgraphs()
|
||||
|
||||
assert list(parent.get_subgraphs(namespace=namespace, recurse=True)) == [
|
||||
(namespace, graphs[namespace])
|
||||
]
|
||||
|
||||
|
||||
def test_nested_namespace_update_raises_despite_top_level_namesake(
|
||||
sync_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""`research|review` names a `review` inside `research`'s subgraph, which
|
||||
declares no subgraph there. An update to it raises rather than writing
|
||||
through the parent's own `review`."""
|
||||
research = (
|
||||
StateGraph(NotesState)
|
||||
.add_node("review", lambda state: {})
|
||||
.add_edge(START, "review")
|
||||
.compile()
|
||||
)
|
||||
parent = (
|
||||
StateGraph(NotesState)
|
||||
.add_node("research", research)
|
||||
.add_node("review", _notes_graph("review"))
|
||||
.add_edge(START, "research")
|
||||
.add_edge("research", "review")
|
||||
.compile(checkpointer=sync_checkpointer)
|
||||
)
|
||||
config: RunnableConfig = {
|
||||
"configurable": {
|
||||
"thread_id": str(uuid4()),
|
||||
"checkpoint_ns": "research:a|review:b",
|
||||
}
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match=r"Subgraph research\|review not found"):
|
||||
parent.update_state(config, {"notes": "edit"})
|
||||
@@ -8,7 +8,8 @@ Covers three checkpointer settings for subgraph state:
|
||||
- checkpointer=True: "stateful" — state accumulates across invocations on the same thread id
|
||||
|
||||
Also covers reading a subgraph's namespace from the parent graph, including
|
||||
one where no subgraph is declared, such as a subagent a tool invokes.
|
||||
one where no subgraph is declared, such as a subagent a tool invokes, and
|
||||
finding the subgraph a namespace names.
|
||||
"""
|
||||
|
||||
import sys
|
||||
@@ -949,3 +950,79 @@ async def test_prefix_named_namespaces_resolve_to_their_own_graph_async(
|
||||
assert await values("research_review") == {
|
||||
"messages": ["check it", "review done"]
|
||||
}
|
||||
|
||||
|
||||
def _prefix_named_subgraphs() -> tuple[Any, dict[str, Any]]:
|
||||
"""A parent whose subgraph namespaces start with one another's names:
|
||||
`research` has a `review` subgraph, and the parent declares
|
||||
`research_review` and `review` too. Returns the parent and each subgraph,
|
||||
keyed by its namespace."""
|
||||
graphs = {
|
||||
"research|review": _notes_graph("research's review"),
|
||||
"research_review": _notes_graph("research_review"),
|
||||
"review": _notes_graph("review"),
|
||||
}
|
||||
graphs["research"] = (
|
||||
StateGraph(NotesState)
|
||||
.add_node("review", graphs["research|review"])
|
||||
.add_edge(START, "review")
|
||||
.compile()
|
||||
)
|
||||
parent = (
|
||||
StateGraph(NotesState)
|
||||
.add_node("research", graphs["research"])
|
||||
.add_node("research_review", graphs["research_review"])
|
||||
.add_node("review", graphs["review"])
|
||||
.add_edge(START, "research")
|
||||
.add_edge("research", "research_review")
|
||||
.add_edge("research_review", "review")
|
||||
.compile()
|
||||
)
|
||||
return parent, graphs
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"namespace", ["research", "research|review", "research_review", "review"]
|
||||
)
|
||||
async def test_get_subgraphs_matches_whole_namespace_segments_async(
|
||||
namespace: str,
|
||||
) -> None:
|
||||
"""`aget_subgraphs(namespace=...)` yields only the subgraph at that
|
||||
namespace. `research_review` isn't `research` then `review`, and
|
||||
`research|review` doesn't also find the parent's own `review`."""
|
||||
parent, graphs = _prefix_named_subgraphs()
|
||||
|
||||
assert [
|
||||
s async for s in parent.aget_subgraphs(namespace=namespace, recurse=True)
|
||||
] == [(namespace, graphs[namespace])]
|
||||
|
||||
|
||||
async def test_nested_namespace_update_raises_despite_top_level_namesake_async(
|
||||
async_checkpointer: BaseCheckpointSaver,
|
||||
) -> None:
|
||||
"""`research|review` names a `review` inside `research`'s subgraph, which
|
||||
declares no subgraph there. An update to it raises rather than writing
|
||||
through the parent's own `review`."""
|
||||
research = (
|
||||
StateGraph(NotesState)
|
||||
.add_node("review", lambda state: {})
|
||||
.add_edge(START, "review")
|
||||
.compile()
|
||||
)
|
||||
parent = (
|
||||
StateGraph(NotesState)
|
||||
.add_node("research", research)
|
||||
.add_node("review", _notes_graph("review"))
|
||||
.add_edge(START, "research")
|
||||
.add_edge("research", "review")
|
||||
.compile(checkpointer=async_checkpointer)
|
||||
)
|
||||
config: RunnableConfig = {
|
||||
"configurable": {
|
||||
"thread_id": str(uuid4()),
|
||||
"checkpoint_ns": "research:a|review:b",
|
||||
}
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match=r"Subgraph research\|review not found"):
|
||||
await parent.aupdate_state(config, {"notes": "edit"})
|
||||
Reference in new issue
Block a user