UNPKG

@langchain/langgraph

Version:

LangGraph

499 lines (497 loc) 20.9 kB
import { InvalidUpdateError, ParentCommand } from "../errors.js"; import { isBaseChannel } from "../channels/base.js"; import { LastValueAfterFinish } from "../channels/last_value.js"; import { getChannel } from "./annotation.js"; import { CHECKPOINT_NAMESPACE_END, CHECKPOINT_NAMESPACE_SEPARATOR, Command, END, SELF, START, TAG_HIDDEN, _isSend, isCommand, isInterrupted } from "../constants.js"; import { RunnableCallable } from "../utils.js"; import { ChannelWrite, PASSTHROUGH } from "../pregel/write.js"; import { ChannelRead, PregelNode } from "../pregel/read.js"; import { isPregelLike } from "../pregel/utils/subgraph.js"; import { EphemeralValue } from "../channels/ephemeral_value.js"; import { Branch, CompiledGraph, Graph } from "./graph.js"; import { NamedBarrierValue, NamedBarrierValueAfterFinish } from "../channels/named_barrier_value.js"; import { schemaMetaRegistry } from "./zod/meta.js"; import { Runnable, _coerceToRunnable } from "@langchain/core/runnables"; import { interopParse, interopZodObjectPartial, isInteropZodObject } from "@langchain/core/utils/types"; //#region src/graph/state.ts const ROOT = "__root__"; const PartialStateSchema = Symbol.for("langgraph.state.partial"); /** * A graph whose nodes communicate by reading and writing to a shared state. * Each node takes a defined `State` as input and returns a `Partial<State>`. * * Each state key can optionally be annotated with a reducer function that * will be used to aggregate the values of that key received from multiple nodes. * The signature of a reducer function is (left: Value, right: UpdateValue) => Value. * * See {@link Annotation} for more on defining state. * * After adding nodes and edges to your graph, you must call `.compile()` on it before * you can use it. * * @example * ```ts * import { * type BaseMessage, * AIMessage, * HumanMessage, * } from "@langchain/core/messages"; * import { StateGraph, Annotation } from "@langchain/langgraph"; * * // Define a state with a single key named "messages" that will * // combine a returned BaseMessage or arrays of BaseMessages * const StateAnnotation = Annotation.Root({ * sentiment: Annotation<string>, * messages: Annotation<BaseMessage[]>({ * reducer: (left: BaseMessage[], right: BaseMessage | BaseMessage[]) => { * if (Array.isArray(right)) { * return left.concat(right); * } * return left.concat([right]); * }, * default: () => [], * }), * }); * * const graphBuilder = new StateGraph(StateAnnotation); * * // A node in the graph that returns an object with a "messages" key * // will update the state by combining the existing value with the returned one. * const myNode = (state: typeof StateAnnotation.State) => { * return { * messages: [new AIMessage("Some new response")], * sentiment: "positive", * }; * }; * * const graph = graphBuilder * .addNode("myNode", myNode) * .addEdge("__start__", "myNode") * .addEdge("myNode", "__end__") * .compile(); * * await graph.invoke({ messages: [new HumanMessage("how are you?")] }); * * // { * // messages: [HumanMessage("how are you?"), AIMessage("Some new response")], * // sentiment: "positive", * // } * ``` */ var StateGraph = class extends Graph { channels = {}; waitingEdges = /* @__PURE__ */ new Set(); /** @internal */ _schemaDefinition; /** @internal */ _schemaRuntimeDefinition; /** @internal */ _inputDefinition; /** @internal */ _inputRuntimeDefinition; /** @internal */ _outputDefinition; /** @internal */ _outputRuntimeDefinition; /** * Map schemas to managed values * @internal */ _schemaDefinitions = /* @__PURE__ */ new Map(); /** @internal */ _metaRegistry = schemaMetaRegistry; /** @internal Used only for typing. */ _configSchema; /** @internal */ _configRuntimeSchema; /** @internal */ _interrupt; /** @internal */ _writer; constructor(fields, contextSchema) { super(); if (isZodStateGraphArgsWithStateSchema(fields)) { const stateDef = this._metaRegistry.getChannelsForSchema(fields.state); const inputDef = fields.input != null ? this._metaRegistry.getChannelsForSchema(fields.input) : stateDef; const outputDef = fields.output != null ? this._metaRegistry.getChannelsForSchema(fields.output) : stateDef; this._schemaDefinition = stateDef; this._schemaRuntimeDefinition = fields.state; this._inputDefinition = inputDef; this._inputRuntimeDefinition = fields.input ?? PartialStateSchema; this._outputDefinition = outputDef; this._outputRuntimeDefinition = fields.output ?? fields.state; } else if (isInteropZodObject(fields)) { const stateDef = this._metaRegistry.getChannelsForSchema(fields); this._schemaDefinition = stateDef; this._schemaRuntimeDefinition = fields; this._inputDefinition = stateDef; this._inputRuntimeDefinition = PartialStateSchema; this._outputDefinition = stateDef; this._outputRuntimeDefinition = fields; } else if (isStateGraphArgsWithInputOutputSchemas(fields)) { this._schemaDefinition = fields.input.spec; this._inputDefinition = fields.input.spec; this._outputDefinition = fields.output.spec; } else if (isStateGraphArgsWithStateSchema(fields)) { this._schemaDefinition = fields.stateSchema.spec; this._inputDefinition = fields.input?.spec ?? this._schemaDefinition; this._outputDefinition = fields.output?.spec ?? this._schemaDefinition; } else if (isStateDefinition(fields) || isAnnotationRoot(fields)) { const spec = isAnnotationRoot(fields) ? fields.spec : fields; this._schemaDefinition = spec; } else if (isStateGraphArgs(fields)) { const spec = _getChannels(fields.channels); this._schemaDefinition = spec; } else throw new Error("Invalid StateGraph input. Make sure to pass a valid Annotation.Root or Zod schema."); this._inputDefinition ??= this._schemaDefinition; this._outputDefinition ??= this._schemaDefinition; this._addSchema(this._schemaDefinition); this._addSchema(this._inputDefinition); this._addSchema(this._outputDefinition); function isOptions(options) { return typeof options === "object" && options != null && !("spec" in options) && !isInteropZodObject(options); } if (isOptions(contextSchema)) { if (isInteropZodObject(contextSchema.context)) this._configRuntimeSchema = contextSchema.context; this._interrupt = contextSchema.interrupt; this._writer = contextSchema.writer; } else if (isInteropZodObject(contextSchema)) this._configRuntimeSchema = contextSchema; } get allEdges() { return new Set([...this.edges, ...Array.from(this.waitingEdges).flatMap(([starts, end]) => starts.map((start) => [start, end]))]); } _addSchema(stateDefinition) { if (this._schemaDefinitions.has(stateDefinition)) return; this._schemaDefinitions.set(stateDefinition, stateDefinition); for (const [key, val] of Object.entries(stateDefinition)) { let channel; if (typeof val === "function") channel = val(); else channel = val; if (this.channels[key] !== void 0) { if (this.channels[key] !== channel) { if (channel.lc_graph_name !== "LastValue") throw new Error(`Channel "${key}" already exists with a different type.`); } } else this.channels[key] = channel; } } addNode(...args) { function isMultipleNodes(args$1) { return args$1.length >= 1 && typeof args$1[0] !== "string"; } const nodes = isMultipleNodes(args) ? Array.isArray(args[0]) ? args[0] : Object.entries(args[0]).map(([key, action]) => [key, action]) : [[ args[0], args[1], args[2] ]]; if (nodes.length === 0) throw new Error("No nodes provided in `addNode`"); for (const [key, action, options] of nodes) { if (key in this.channels) throw new Error(`${key} is already being used as a state attribute (a.k.a. a channel), cannot also be used as a node name.`); for (const reservedChar of [CHECKPOINT_NAMESPACE_SEPARATOR, CHECKPOINT_NAMESPACE_END]) if (key.includes(reservedChar)) throw new Error(`"${reservedChar}" is a reserved character and is not allowed in node names.`); this.warnIfCompiled(`Adding a node to a graph that has already been compiled. This will not be reflected in the compiled graph.`); if (key in this.nodes) throw new Error(`Node \`${key}\` already present.`); if (key === END || key === START) throw new Error(`Node \`${key}\` is reserved.`); let inputSpec = this._schemaDefinition; if (options?.input !== void 0) { if (isInteropZodObject(options.input)) inputSpec = this._metaRegistry.getChannelsForSchema(options.input); else if (options.input.spec !== void 0) inputSpec = options.input.spec; } if (inputSpec !== void 0) this._addSchema(inputSpec); let runnable; if (Runnable.isRunnable(action)) runnable = action; else if (typeof action === "function") runnable = new RunnableCallable({ func: action, name: key, trace: false }); else runnable = _coerceToRunnable(action); let cachePolicy = options?.cachePolicy; if (typeof cachePolicy === "boolean") cachePolicy = cachePolicy ? {} : void 0; const nodeSpec = { runnable, retryPolicy: options?.retryPolicy, cachePolicy, metadata: options?.metadata, input: inputSpec ?? this._schemaDefinition, subgraphs: isPregelLike(runnable) ? [runnable] : options?.subgraphs, ends: options?.ends, defer: options?.defer }; this.nodes[key] = nodeSpec; } return this; } addEdge(startKey, endKey) { if (typeof startKey === "string") return super.addEdge(startKey, endKey); if (this.compiled) console.warn("Adding an edge to a graph that has already been compiled. This will not be reflected in the compiled graph."); for (const start of startKey) { if (start === END) throw new Error("END cannot be a start node"); if (!Object.keys(this.nodes).some((node) => node === start)) throw new Error(`Need to add a node named "${start}" first`); } if (endKey === END) throw new Error("END cannot be an end node"); if (!Object.keys(this.nodes).some((node) => node === endKey)) throw new Error(`Need to add a node named "${endKey}" first`); this.waitingEdges.add([startKey, endKey]); return this; } addSequence(nodes) { const parsedNodes = Array.isArray(nodes) ? nodes : Object.entries(nodes); if (parsedNodes.length === 0) throw new Error("Sequence requires at least one node."); let previousNode; for (const [key, action, options] of parsedNodes) { if (key in this.nodes) throw new Error(`Node names must be unique: node with the name "${key}" already exists.`); const validKey = key; this.addNode(validKey, action, options); if (previousNode != null) this.addEdge(previousNode, validKey); previousNode = validKey; } return this; } compile({ checkpointer, store, cache, interruptBefore, interruptAfter, name, description } = {}) { this.validate([...Array.isArray(interruptBefore) ? interruptBefore : [], ...Array.isArray(interruptAfter) ? interruptAfter : []]); const outputKeys = Object.keys(this._schemaDefinitions.get(this._outputDefinition)); const outputChannels = outputKeys.length === 1 && outputKeys[0] === ROOT ? ROOT : outputKeys; const streamKeys = Object.keys(this.channels); const streamChannels = streamKeys.length === 1 && streamKeys[0] === ROOT ? ROOT : streamKeys; const userInterrupt = this._interrupt; const compiled = new CompiledStateGraph({ builder: this, checkpointer, interruptAfter, interruptBefore, autoValidate: false, nodes: {}, channels: { ...this.channels, [START]: new EphemeralValue() }, inputChannels: START, outputChannels, streamChannels, streamMode: "updates", store, cache, name, description, userInterrupt }); compiled.attachNode(START); for (const [key, node] of Object.entries(this.nodes)) compiled.attachNode(key, node); compiled.attachBranch(START, SELF, _getControlBranch(), { withReader: false }); for (const [key] of Object.entries(this.nodes)) compiled.attachBranch(key, SELF, _getControlBranch(), { withReader: false }); for (const [start, end] of this.edges) compiled.attachEdge(start, end); for (const [starts, end] of this.waitingEdges) compiled.attachEdge(starts, end); for (const [start, branches] of Object.entries(this.branches)) for (const [name$1, branch] of Object.entries(branches)) compiled.attachBranch(start, name$1, branch); return compiled.validate(); } }; function _getChannels(schema) { const channels = {}; for (const [name, val] of Object.entries(schema)) if (name === ROOT) channels[name] = getChannel(val); else channels[name] = getChannel(val); return channels; } /** * Final result from building and compiling a {@link StateGraph}. * Should not be instantiated directly, only using the StateGraph `.compile()` * instance method. */ var CompiledStateGraph = class extends CompiledGraph { /** * The description of the compiled graph. * This is used by the supervisor agent to describe the handoff to the agent. */ description; /** @internal */ _metaRegistry = schemaMetaRegistry; constructor({ description,...rest }) { super(rest); this.description = description; } attachNode(key, node) { let outputKeys; if (key === START) outputKeys = Object.entries(this.builder._schemaDefinitions.get(this.builder._inputDefinition)).map(([k]) => k); else outputKeys = Object.keys(this.builder.channels); function _getRoot(input) { if (isCommand(input)) { if (input.graph === Command.PARENT) return null; return input._updateAsTuples(); } else if (Array.isArray(input) && input.length > 0 && input.some((i) => isCommand(i))) { const updates = []; for (const i of input) if (isCommand(i)) { if (i.graph === Command.PARENT) continue; updates.push(...i._updateAsTuples()); } else updates.push([ROOT, i]); return updates; } else if (input != null) return [[ROOT, input]]; return null; } const nodeKey = key; function _getUpdates(input) { if (!input) return null; else if (isCommand(input)) { if (input.graph === Command.PARENT) return null; return input._updateAsTuples().filter(([k]) => outputKeys.includes(k)); } else if (Array.isArray(input) && input.length > 0 && input.some(isCommand)) { const updates = []; for (const item of input) if (isCommand(item)) { if (item.graph === Command.PARENT) continue; updates.push(...item._updateAsTuples().filter(([k]) => outputKeys.includes(k))); } else { const itemUpdates = _getUpdates(item); if (itemUpdates) updates.push(...itemUpdates ?? []); } return updates; } else if (typeof input === "object" && !Array.isArray(input)) return Object.entries(input).filter(([k]) => outputKeys.includes(k)); else { const typeofInput = Array.isArray(input) ? "array" : typeof input; throw new InvalidUpdateError(`Expected node "${nodeKey.toString()}" to return an object or an array containing at least one Command object, received ${typeofInput}`, { lc_error_code: "INVALID_GRAPH_NODE_RETURN_VALUE" }); } } const stateWriteEntries = [{ value: PASSTHROUGH, mapper: new RunnableCallable({ func: outputKeys.length && outputKeys[0] === ROOT ? _getRoot : _getUpdates, trace: false, recurse: false }) }]; if (key === START) this.nodes[key] = new PregelNode({ tags: [TAG_HIDDEN], triggers: [START], channels: [START], writers: [new ChannelWrite(stateWriteEntries, [TAG_HIDDEN])] }); else { const inputDefinition = node?.input ?? this.builder._schemaDefinition; const inputValues = Object.fromEntries(Object.keys(this.builder._schemaDefinitions.get(inputDefinition)).map((k) => [k, k])); const isSingleInput = Object.keys(inputValues).length === 1 && ROOT in inputValues; const branchChannel = `branch:to:${key}`; this.channels[branchChannel] = node?.defer ? new LastValueAfterFinish() : new EphemeralValue(false); this.nodes[key] = new PregelNode({ triggers: [branchChannel], channels: isSingleInput ? Object.keys(inputValues) : inputValues, writers: [new ChannelWrite(stateWriteEntries, [TAG_HIDDEN])], mapper: isSingleInput ? void 0 : (input) => { return Object.fromEntries(Object.entries(input).filter(([k]) => k in inputValues)); }, bound: node?.runnable, metadata: node?.metadata, retryPolicy: node?.retryPolicy, cachePolicy: node?.cachePolicy, subgraphs: node?.subgraphs, ends: node?.ends }); } } attachEdge(starts, end) { if (end === END) return; if (typeof starts === "string") this.nodes[starts].writers.push(new ChannelWrite([{ channel: `branch:to:${end}`, value: null }], [TAG_HIDDEN])); else if (Array.isArray(starts)) { const channelName = `join:${starts.join("+")}:${end}`; this.channels[channelName] = this.builder.nodes[end].defer ? new NamedBarrierValueAfterFinish(new Set(starts)) : new NamedBarrierValue(new Set(starts)); this.nodes[end].triggers.push(channelName); for (const start of starts) this.nodes[start].writers.push(new ChannelWrite([{ channel: channelName, value: start }], [TAG_HIDDEN])); } } attachBranch(start, _, branch, options = { withReader: true }) { const branchWriter = async (packets, config) => { const filteredPackets = packets.filter((p) => p !== END); if (!filteredPackets.length) return; const writes = filteredPackets.map((p) => { if (_isSend(p)) return p; return { channel: p === END ? p : `branch:to:${p}`, value: start }; }); await ChannelWrite.doWrite({ ...config, tags: (config.tags ?? []).concat([TAG_HIDDEN]) }, writes); }; this.nodes[start].writers.push(branch.run(branchWriter, options.withReader ? (config) => ChannelRead.doRead(config, this.streamChannels ?? this.outputChannels, true) : void 0)); } async _validateInput(input) { if (input == null) return input; const schema = (() => { const input$1 = this.builder._inputRuntimeDefinition; const schema$1 = this.builder._schemaRuntimeDefinition; const apply = (schema$2) => { if (schema$2 == null) return void 0; return this._metaRegistry.getExtendedChannelSchemas(schema$2, { withReducerSchema: true }); }; if (isInteropZodObject(input$1)) return apply(input$1); if (input$1 === PartialStateSchema) return interopZodObjectPartial(apply(schema$1)); return void 0; })(); if (isCommand(input)) { const parsedInput = input; if (input.update && schema != null) parsedInput.update = interopParse(schema, input.update); return parsedInput; } if (schema != null) return interopParse(schema, input); return input; } isInterrupted(input) { return isInterrupted(input); } async _validateContext(config) { const configSchema = this.builder._configRuntimeSchema; if (isInteropZodObject(configSchema)) interopParse(configSchema, config); return config; } }; function isStateDefinition(obj) { return typeof obj === "object" && obj !== null && !Array.isArray(obj) && Object.keys(obj).length > 0 && Object.values(obj).every((v) => typeof v === "function" || isBaseChannel(v)); } function isAnnotationRoot(obj) { return typeof obj === "object" && obj !== null && "lc_graph_name" in obj && obj.lc_graph_name === "AnnotationRoot"; } function isStateGraphArgs(obj) { return typeof obj === "object" && obj !== null && obj.channels !== void 0; } function isStateGraphArgsWithStateSchema(obj) { return typeof obj === "object" && obj !== null && obj.stateSchema !== void 0; } function isStateGraphArgsWithInputOutputSchemas(obj) { return typeof obj === "object" && obj !== null && obj.stateSchema === void 0 && obj.input !== void 0 && obj.output !== void 0; } function isZodStateGraphArgsWithStateSchema(value) { if (typeof value !== "object" || value == null) return false; if (!("state" in value) || !isInteropZodObject(value.state)) return false; if ("input" in value && !isInteropZodObject(value.input)) return false; if ("output" in value && !isInteropZodObject(value.output)) return false; return true; } function _controlBranch(value) { if (_isSend(value)) return [value]; const commands = []; if (isCommand(value)) commands.push(value); else if (Array.isArray(value)) commands.push(...value.filter(isCommand)); const destinations = []; for (const command of commands) { if (command.graph === Command.PARENT) throw new ParentCommand(command); if (_isSend(command.goto)) destinations.push(command.goto); else if (typeof command.goto === "string") destinations.push(command.goto); else if (Array.isArray(command.goto)) destinations.push(...command.goto); } return destinations; } function _getControlBranch() { const CONTROL_BRANCH_PATH = new RunnableCallable({ func: _controlBranch, tags: [TAG_HIDDEN], trace: false, recurse: false, name: "<control_branch>" }); return new Branch({ path: CONTROL_BRANCH_PATH }); } //#endregion export { CompiledStateGraph, StateGraph }; //# sourceMappingURL=state.js.map