Skip to content

Commit

Permalink
feat(langgraph): Adds error page for unreachable nodes (#741)
Browse files Browse the repository at this point in the history
  • Loading branch information
jacoblee93 authored Dec 13, 2024
1 parent 3ef8ace commit 403cd51
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 3 deletions.
1 change: 1 addition & 0 deletions docs/docs/how-tos/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,4 @@ These are the guides for resolving common errors you may find while building wit
- [INVALID_CONCURRENT_GRAPH_UPDATE](../troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.ipynb)
- [INVALID_GRAPH_NODE_RETURN_VALUE](../troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.ipynb)
- [MULTIPLE_SUBGRAPHS](../troubleshooting/errors/MULTIPLE_SUBGRAPHS.ipynb)
- [UNREACHABLE_NODE](../troubleshooting/errors/UNREACHABLE_NODE.ipynb)
95 changes: 95 additions & 0 deletions docs/docs/troubleshooting/errors/UNREACHABLE_NODE.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# UNREACHABLE_NODE\n",
"\n",
"LangGraph cannot identify an incoming edge to one of your nodes. Check to ensure you have added sufficient edges when constructing your graph.\n",
"\n",
"Alternatively, if you are returning [`Command`](/langgraphjs/how-tos/command/) instances from your nodes to make your graphs edgeless, you will need to add an additional `ends` parameter when calling `addNode` to help LangGraph determine the destinations for your node.\n",
"\n",
"Here's an example:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"import { Annotation, Command } from \"@langchain/langgraph\";\n",
"\n",
"const StateAnnotation = Annotation.Root({\n",
" foo: Annotation<string>,\n",
"});\n",
"\n",
"const nodeA = async (_state: typeof StateAnnotation.State) => {\n",
" const goto = Math.random() > .5 ? \"nodeB\" : \"nodeC\";\n",
" return new Command({\n",
" update: { foo: \"a\" },\n",
" goto,\n",
" });\n",
"};\n",
"\n",
"const nodeB = async (state: typeof StateAnnotation.State) => {\n",
" return {\n",
" foo: state.foo + \"|b\",\n",
" };\n",
"}\n",
"\n",
"const nodeC = async (state: typeof StateAnnotation.State) => {\n",
" return {\n",
" foo: state.foo + \"|c\",\n",
" };\n",
"}\n",
"\n",
"import { StateGraph } from \"@langchain/langgraph\";\n",
"\n",
"// NOTE: there are no edges between nodes A, B and C!\n",
"const graph = new StateGraph(StateAnnotation)\n",
" .addNode(\"nodeA\", nodeA, {\n",
" // Explicitly specify \"nodeB\" and \"nodeC\" as potential destinations for nodeA\n",
" ends: [\"nodeB\", \"nodeC\"],\n",
" })\n",
" .addNode(\"nodeB\", nodeB)\n",
" .addNode(\"nodeC\", nodeC)\n",
" .addEdge(\"__start__\", \"nodeA\")\n",
" .compile();"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Troubleshooting\n",
"\n",
"The following may help resolve this error:\n",
"\n",
"- Make sure that you have not forgotten to add edges between some of your nodes.\n",
"- If you are returning `Commands` from your nodes, make sure that you're passing an `ends` array with the names of potential destination nodes as shown above."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "TypeScript",
"language": "typescript",
"name": "tslab"
},
"language_info": {
"codemirror_mode": {
"mode": "typescript",
"name": "javascript",
"typescript": true
},
"file_extension": ".ts",
"mimetype": "text/typescript",
"name": "typescript",
"version": "3.7.2"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
1 change: 1 addition & 0 deletions docs/docs/troubleshooting/errors/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Errors referenced below will have an `lc_error_code` property corresponding to o
- [INVALID_CONCURRENT_GRAPH_UPDATE](/langgraphjs/troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE)
- [INVALID_GRAPH_NODE_RETURN_VALUE](/langgraphjs/troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE)
- [MULTIPLE_SUBGRAPHS](/langgraphjs/troubleshooting/errors/MULTIPLE_SUBGRAPHS)
- [UNREACHABLE_NODE](/langgraphjs/troubleshooting/errors/UNREACHABLE_NODE)
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ nav:
- troubleshooting/errors/INVALID_CONCURRENT_GRAPH_UPDATE.ipynb
- troubleshooting/errors/INVALID_GRAPH_NODE_RETURN_VALUE.ipynb
- troubleshooting/errors/MULTIPLE_SUBGRAPHS.ipynb
- troubleshooting/errors/UNREACHABLE_NODE.ipynb

- Conceptual Guides:
- concepts/index.md
Expand Down
1 change: 0 additions & 1 deletion examples/how-tos/command.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,6 @@
"};\n",
"\n",
"// Nodes B and C are unchanged\n",
"\n",
"const nodeB = async (state: typeof StateAnnotation.State) => {\n",
" console.log(\"Called B\");\n",
" return {\n",
Expand Down
59 changes: 59 additions & 0 deletions libs/langgraph/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,65 @@ export type CommandParams<R> = {

/**
* One or more commands to update the graph's state and send messages to nodes.
* Can be used to combine routing logic with state updates in lieu of conditional edges
*
* @example
* ```ts
* import { Annotation, Command } from "@langchain/langgraph";
*
* // Define graph state
* const StateAnnotation = Annotation.Root({
* foo: Annotation<string>,
* });
*
* // Define the nodes
* const nodeA = async (_state: typeof StateAnnotation.State) => {
* console.log("Called A");
* // this is a replacement for a real conditional edge function
* const goto = Math.random() > .5 ? "nodeB" : "nodeC";
* // note how Command allows you to BOTH update the graph state AND route to the next node
* return new Command({
* // this is the state update
* update: {
* foo: "a",
* },
* // this is a replacement for an edge
* goto,
* });
* };
*
* // Nodes B and C are unchanged
* const nodeB = async (state: typeof StateAnnotation.State) => {
* console.log("Called B");
* return {
* foo: state.foo + "|b",
* };
* }
*
* const nodeC = async (state: typeof StateAnnotation.State) => {
* console.log("Called C");
* return {
* foo: state.foo + "|c",
* };
* }
*
* import { StateGraph } from "@langchain/langgraph";
* // NOTE: there are no edges between nodes A, B and C!
* const graph = new StateGraph(StateAnnotation)
* .addNode("nodeA", nodeA, {
* ends: ["nodeB", "nodeC"],
* })
* .addNode("nodeB", nodeB)
* .addNode("nodeC", nodeC)
* .addEdge("__start__", "nodeA")
* .compile();
*
* await graph.invoke({ foo: "" });
*
* // Randomly oscillates between
* // { foo: 'a|c' } and { foo: 'a|b' }
* ```
*/
export class Command<R = unknown> {
lg_name = "Command";
Expand Down
11 changes: 11 additions & 0 deletions libs/langgraph/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,17 @@ export class MultipleSubgraphsError extends BaseLangGraphError {
}
}

export class UnreachableNodeError extends BaseLangGraphError {
constructor(message?: string, fields?: BaseLangGraphErrorFields) {
super(message, fields);
this.name = "UnreachableNodeError";
}

static get unminifiable_name() {
return "UnreachableNodeError";
}
}

/**
* Exception raised when an error occurs in the remote graph.
*/
Expand Down
19 changes: 17 additions & 2 deletions libs/langgraph/src/graph/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ import {
gatherIteratorSync,
RunnableCallable,
} from "../utils.js";
import { InvalidUpdateError, NodeInterrupt } from "../errors.js";
import {
InvalidUpdateError,
NodeInterrupt,
UnreachableNodeError,
} from "../errors.js";
import { StateDefinition, StateType } from "./annotation.js";
import type { LangGraphRunnableConfig } from "../pregel/runnable_types.js";
import { isPregelLike } from "../pregel/utils/subgraph.js";
Expand Down Expand Up @@ -463,7 +467,18 @@ export class Graph<
// validate targets
for (const node of Object.keys(this.nodes)) {
if (!allTargets.has(node)) {
throw new Error(`Node \`${node}\` is not reachable`);
throw new UnreachableNodeError(
[
`Node \`${node}\` is not reachable.`,
"",
"If you are returning Command objects from your node,",
'make sure you are passing names of potential destination nodes as an "ends" array',
'into ".addNode(..., { ends: ["node1", "node2"] })".',
].join("\n"),
{
lc_error_code: "UNREACHABLE_NODE",
}
);
}
}
for (const target of allTargets) {
Expand Down

0 comments on commit 403cd51

Please sign in to comment.