diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index b8cda209..1a51e84c 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -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) diff --git a/docs/docs/troubleshooting/errors/UNREACHABLE_NODE.ipynb b/docs/docs/troubleshooting/errors/UNREACHABLE_NODE.ipynb new file mode 100644 index 00000000..1f5e5ee3 --- /dev/null +++ b/docs/docs/troubleshooting/errors/UNREACHABLE_NODE.ipynb @@ -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,\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 +} diff --git a/docs/docs/troubleshooting/errors/index.md b/docs/docs/troubleshooting/errors/index.md index 7c6ea5f1..e7ca49d5 100644 --- a/docs/docs/troubleshooting/errors/index.md +++ b/docs/docs/troubleshooting/errors/index.md @@ -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) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 4b4a95a6..8501cf29 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -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 diff --git a/examples/how-tos/command.ipynb b/examples/how-tos/command.ipynb index 64c2719c..d72009bc 100644 --- a/examples/how-tos/command.ipynb +++ b/examples/how-tos/command.ipynb @@ -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", diff --git a/libs/langgraph/src/constants.ts b/libs/langgraph/src/constants.ts index a987e240..186199ed 100644 --- a/libs/langgraph/src/constants.ts +++ b/libs/langgraph/src/constants.ts @@ -155,6 +155,65 @@ export type CommandParams = { /** * 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, + * }); + * + * // 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 { lg_name = "Command"; diff --git a/libs/langgraph/src/errors.ts b/libs/langgraph/src/errors.ts index a54a2a84..ca471bc4 100644 --- a/libs/langgraph/src/errors.ts +++ b/libs/langgraph/src/errors.ts @@ -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. */ diff --git a/libs/langgraph/src/graph/graph.ts b/libs/langgraph/src/graph/graph.ts index aabb19a8..f5e55f75 100644 --- a/libs/langgraph/src/graph/graph.ts +++ b/libs/langgraph/src/graph/graph.ts @@ -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"; @@ -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) {