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:
William FH
2024-06-08 00:00:13 -07:00
committed by GitHub
parent eb8e77c8d5
commit 5799d6ca1c
3 changed files with 77 additions and 96 deletions
+1 -4
View File
@@ -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
+61 -61
View File
@@ -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
View File
@@ -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")