diff --git a/langgraph/graph/graph.py b/langgraph/graph/graph.py index 4bb5f955e..e8c281bdc 100644 --- a/langgraph/graph/graph.py +++ b/langgraph/graph/graph.py @@ -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 diff --git a/tests/__snapshots__/test_pregel.ambr b/tests/__snapshots__/test_pregel.ambr index 7277e8cf4..26381cdbf 100644 --- a/tests/__snapshots__/test_pregel.ambr +++ b/tests/__snapshots__/test_pregel.ambr @@ -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 ''' { diff --git a/tests/test_pregel.py b/tests/test_pregel.py index 3fe2a6973..d5ebffcbd 100644 --- a/tests/test_pregel.py +++ b/tests/test_pregel.py @@ -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")