mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-30 21:45:08 +02:00
Accept dead-ends (#626)
Previously, we validated that nodes explicitly route to END. This feels a bit unnecessary, since graphs are expected to keep processing until no work is left to be done Plus fix a small bug in validation.
This commit is contained in:
@@ -287,11 +287,8 @@ class Graph:
|
||||
if node != start and node != branch.then:
|
||||
all_sources.add(node)
|
||||
# validate sources
|
||||
for node in self.nodes:
|
||||
if node not in all_sources:
|
||||
raise ValueError(f"Node '{node}' is a dead-end")
|
||||
for source in all_sources:
|
||||
if node not in self.nodes and source != START:
|
||||
if source not in self.nodes and source != START:
|
||||
raise ValueError(f"Found edge starting at unknown node '{source}'")
|
||||
|
||||
# assemble targets
|
||||
|
||||
@@ -114,67 +114,6 @@
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_conditional_entrypoint_to_multiple_state_graph
|
||||
'{"title": "LangGraphInput", "$ref": "#/definitions/OverallState", "definitions": {"OverallState": {"title": "OverallState", "type": "object", "properties": {"locations": {"title": "Locations", "type": "array", "items": {"type": "string"}}, "results": {"title": "Results", "type": "array", "items": {"type": "string"}}}, "required": ["locations", "results"]}}}'
|
||||
# ---
|
||||
# name: test_conditional_entrypoint_to_multiple_state_graph.1
|
||||
'{"title": "LangGraphOutput", "type": "object", "properties": {"locations": {"title": "Locations", "type": "array", "items": {"type": "string"}}, "results": {"title": "Results", "type": "array", "items": {"type": "string"}}}}'
|
||||
# ---
|
||||
# name: test_conditional_entrypoint_to_multiple_state_graph.2
|
||||
'''
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "__start__",
|
||||
"type": "schema",
|
||||
"data": "__start__"
|
||||
},
|
||||
{
|
||||
"id": "__end__",
|
||||
"type": "schema",
|
||||
"data": "__end__"
|
||||
},
|
||||
{
|
||||
"id": "get_weather",
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langgraph",
|
||||
"utils",
|
||||
"RunnableCallable"
|
||||
],
|
||||
"name": "get_weather"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "get_weather",
|
||||
"target": "__end__"
|
||||
},
|
||||
{
|
||||
"source": "__start__",
|
||||
"target": "get_weather",
|
||||
"conditional": true
|
||||
},
|
||||
{
|
||||
"source": "__start__",
|
||||
"target": "__end__",
|
||||
"conditional": true
|
||||
}
|
||||
]
|
||||
}
|
||||
'''
|
||||
# ---
|
||||
# name: test_conditional_entrypoint_to_multiple_state_graph.3
|
||||
'''
|
||||
graph TD;
|
||||
get_weather --> __end__;
|
||||
__start__ -.-> get_weather;
|
||||
__start__ -.-> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_conditional_entrypoint_graph_state
|
||||
'{"title": "LangGraphInput", "$ref": "#/definitions/AgentState", "definitions": {"AgentState": {"title": "AgentState", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "output": {"title": "Output", "type": "string"}, "steps": {"title": "Steps", "type": "array", "items": {"type": "string"}}}}}}'
|
||||
# ---
|
||||
@@ -256,6 +195,67 @@
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_conditional_entrypoint_to_multiple_state_graph
|
||||
'{"title": "LangGraphInput", "$ref": "#/definitions/OverallState", "definitions": {"OverallState": {"title": "OverallState", "type": "object", "properties": {"locations": {"title": "Locations", "type": "array", "items": {"type": "string"}}, "results": {"title": "Results", "type": "array", "items": {"type": "string"}}}, "required": ["locations", "results"]}}}'
|
||||
# ---
|
||||
# name: test_conditional_entrypoint_to_multiple_state_graph.1
|
||||
'{"title": "LangGraphOutput", "type": "object", "properties": {"locations": {"title": "Locations", "type": "array", "items": {"type": "string"}}, "results": {"title": "Results", "type": "array", "items": {"type": "string"}}}}'
|
||||
# ---
|
||||
# name: test_conditional_entrypoint_to_multiple_state_graph.2
|
||||
'''
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"id": "__start__",
|
||||
"type": "schema",
|
||||
"data": "__start__"
|
||||
},
|
||||
{
|
||||
"id": "__end__",
|
||||
"type": "schema",
|
||||
"data": "__end__"
|
||||
},
|
||||
{
|
||||
"id": "get_weather",
|
||||
"type": "runnable",
|
||||
"data": {
|
||||
"id": [
|
||||
"langgraph",
|
||||
"utils",
|
||||
"RunnableCallable"
|
||||
],
|
||||
"name": "get_weather"
|
||||
}
|
||||
}
|
||||
],
|
||||
"edges": [
|
||||
{
|
||||
"source": "get_weather",
|
||||
"target": "__end__"
|
||||
},
|
||||
{
|
||||
"source": "__start__",
|
||||
"target": "get_weather",
|
||||
"conditional": true
|
||||
},
|
||||
{
|
||||
"source": "__start__",
|
||||
"target": "__end__",
|
||||
"conditional": true
|
||||
}
|
||||
]
|
||||
}
|
||||
'''
|
||||
# ---
|
||||
# name: test_conditional_entrypoint_to_multiple_state_graph.3
|
||||
'''
|
||||
graph TD;
|
||||
get_weather --> __end__;
|
||||
__start__ -.-> get_weather;
|
||||
__start__ -.-> __end__;
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_conditional_graph
|
||||
'''
|
||||
{
|
||||
|
||||
+15
-31
@@ -68,11 +68,11 @@ def test_graph_validation() -> None:
|
||||
workflow.set_finish_point("agent")
|
||||
assert workflow.compile(), "valid graph"
|
||||
|
||||
# Accept a dead-end
|
||||
workflow = Graph()
|
||||
workflow.add_node("agent", logic)
|
||||
workflow.set_entry_point("agent")
|
||||
with pytest.raises(ValueError, match="dead-end"):
|
||||
workflow.compile()
|
||||
workflow.compile()
|
||||
|
||||
workflow = Graph()
|
||||
workflow.add_node("agent", logic)
|
||||
@@ -131,7 +131,9 @@ def test_graph_validation() -> None:
|
||||
workflow.set_entry_point("agent")
|
||||
workflow.add_conditional_edges("agent", logic, {"continue": "tools", "exit": END})
|
||||
workflow.add_edge("tools", "agent")
|
||||
with pytest.raises(ValueError): # extra is dead-end / not reachable
|
||||
with pytest.raises(
|
||||
ValueError, match="Node `extra` is not reachable"
|
||||
): # extra is not reachable
|
||||
workflow.compile()
|
||||
|
||||
workflow = Graph()
|
||||
@@ -141,8 +143,8 @@ def test_graph_validation() -> None:
|
||||
workflow.set_entry_point("agent")
|
||||
workflow.add_conditional_edges("agent", logic)
|
||||
workflow.add_edge("tools", "agent")
|
||||
with pytest.raises(ValueError): # extra is dead-end
|
||||
workflow.compile()
|
||||
# Accept, even though extra is dead-end
|
||||
workflow.compile()
|
||||
|
||||
class State(TypedDict):
|
||||
hello: str
|
||||
@@ -159,6 +161,14 @@ def test_graph_validation() -> None:
|
||||
with pytest.raises(InvalidUpdateError):
|
||||
graph.invoke({"hello": "there"})
|
||||
|
||||
graph = StateGraph(State)
|
||||
graph.add_node("start", lambda x: x)
|
||||
graph.add_edge("__start__", "start")
|
||||
graph.add_edge("unknown", "start")
|
||||
graph.add_edge("start", "__end__")
|
||||
with pytest.raises(ValueError, match="Found edge starting at unknown node "):
|
||||
graph.compile()
|
||||
|
||||
|
||||
def test_invoke_single_process_in_out(mocker: MockerFixture) -> None:
|
||||
add_one = mocker.Mock(side_effect=lambda x: x + 1)
|
||||
@@ -5786,16 +5796,6 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None:
|
||||
my_key: Annotated[str, operator.add]
|
||||
market: str
|
||||
|
||||
# this graph is invalid because there is no path to END
|
||||
invalid_graph = StateGraph(State)
|
||||
invalid_graph.add_node("tool_two_slow", lambda s: {"my_key": "slow"})
|
||||
invalid_graph.add_node("tool_two_fast", lambda s: {"my_key": "fast"})
|
||||
invalid_graph.set_conditional_entry_point(
|
||||
lambda s: "tool_two_slow" if s["market"] == "DE" else "tool_two_fast"
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
invalid_graph.compile()
|
||||
|
||||
tool_two_graph = StateGraph(State)
|
||||
tool_two_graph.add_node("tool_two_slow", lambda s: {"my_key": " slow"})
|
||||
tool_two_graph.add_node("tool_two_fast", lambda s: {"my_key": " fast"})
|
||||
@@ -5939,22 +5939,6 @@ def test_branch_then(snapshot: SnapshotAssertion) -> None:
|
||||
my_key: Annotated[str, operator.add]
|
||||
market: str
|
||||
|
||||
# this graph is invalid because there is no path to "finish"
|
||||
invalid_graph = StateGraph(State)
|
||||
invalid_graph.set_entry_point("prepare")
|
||||
invalid_graph.set_finish_point("finish")
|
||||
invalid_graph.add_conditional_edges(
|
||||
source="prepare",
|
||||
path=lambda s: "tool_two_slow" if s["market"] == "DE" else "tool_two_fast",
|
||||
path_map=["tool_two_slow", "tool_two_fast"],
|
||||
)
|
||||
invalid_graph.add_node("prepare", lambda s: {"my_key": " prepared"})
|
||||
invalid_graph.add_node("tool_two_slow", lambda s: {"my_key": " slow"})
|
||||
invalid_graph.add_node("tool_two_fast", lambda s: {"my_key": " fast"})
|
||||
invalid_graph.add_node("finish", lambda s: {"my_key": " finished"})
|
||||
with pytest.raises(ValueError):
|
||||
invalid_graph.compile()
|
||||
|
||||
tool_two_graph = StateGraph(State)
|
||||
tool_two_graph.set_entry_point("prepare")
|
||||
tool_two_graph.set_finish_point("finish")
|
||||
|
||||
Reference in New Issue
Block a user