From 9d2600db4e41549258a286d9b08410296bd759dd Mon Sep 17 00:00:00 2001 From: Jacob Lee Date: Tue, 1 Oct 2024 22:58:04 -0700 Subject: [PATCH] feat(core): Update mermaid drawing to support subgraphs (#6917) --- langchain-core/src/runnables/graph.ts | 191 ++++++++++++------ langchain-core/src/runnables/graph_mermaid.ts | 189 +++++++++-------- .../src/runnables/tests/data/mermaid.png | Bin 19879 -> 19659 bytes .../runnables/tests/runnable_graph.test.ts | 16 +- langchain-core/src/runnables/types.ts | 3 + 5 files changed, 250 insertions(+), 149 deletions(-) diff --git a/langchain-core/src/runnables/graph.ts b/langchain-core/src/runnables/graph.ts index 1137b591da5c..9826af0afee9 100644 --- a/langchain-core/src/runnables/graph.ts +++ b/langchain-core/src/runnables/graph.ts @@ -9,31 +9,31 @@ import type { import { isRunnableInterface } from "./utils.js"; import { drawMermaid, drawMermaidPng } from "./graph_mermaid.js"; -const MAX_DATA_DISPLAY_NAME_LENGTH = 42; - export { Node, Edge }; -function nodeDataStr(node: Node): string { - if (!isUuid(node.id)) { - return node.id; - } else if (isRunnableInterface(node.data)) { +function nodeDataStr( + id: string | undefined, + data: RunnableInterface | RunnableIOSchema +): string { + if (id !== undefined && !isUuid(id)) { + return id; + } else if (isRunnableInterface(data)) { try { - let data = node.data.getName(); - data = data.startsWith("Runnable") ? data.slice("Runnable".length) : data; - if (data.length > MAX_DATA_DISPLAY_NAME_LENGTH) { - data = `${data.substring(0, MAX_DATA_DISPLAY_NAME_LENGTH)}...`; - } - return data; + let dataStr = data.getName(); + dataStr = dataStr.startsWith("Runnable") + ? dataStr.slice("Runnable".length) + : dataStr; + return dataStr; } catch (error) { - return node.data.getName(); + return data.getName(); } } else { - return node.data.name ?? "UnknownSchema"; + return data.name ?? "UnknownSchema"; } } function nodeDataJson(node: Node) { - // if node.data is implements Runnable + // if node.data implements Runnable if (isRunnableInterface(node.data)) { return { type: "runnable", @@ -55,6 +55,11 @@ export class Graph { edges: Edge[] = []; + constructor(params?: { nodes: Record; edges: Edge[] }) { + this.nodes = params?.nodes ?? this.nodes; + this.edges = params?.edges ?? this.edges; + } + // Convert the graph to a JSON-serializable format. // eslint-disable-next-line @typescript-eslint/no-explicit-any toJSON(): Record { @@ -86,12 +91,22 @@ export class Graph { }; } - addNode(data: RunnableInterface | RunnableIOSchema, id?: string): Node { + addNode( + data: RunnableInterface | RunnableIOSchema, + id?: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata?: Record + ): Node { if (id !== undefined && this.nodes[id] !== undefined) { throw new Error(`Node with id ${id} already exists`); } - const nodeId = id || uuidv4(); - const node: Node = { id: nodeId, data }; + const nodeId = id ?? uuidv4(); + const node: Node = { + id: nodeId, + data, + name: nodeDataStr(id, data), + metadata, + }; this.nodes[nodeId] = node; return node; } @@ -129,25 +144,11 @@ export class Graph { } firstNode(): Node | undefined { - const targets = new Set(this.edges.map((edge) => edge.target)); - const found: Node[] = []; - Object.values(this.nodes).forEach((node) => { - if (!targets.has(node.id)) { - found.push(node); - } - }); - return found[0]; + return _firstNode(this); } lastNode(): Node | undefined { - const sources = new Set(this.edges.map((edge) => edge.source)); - const found: Node[] = []; - Object.values(this.nodes).forEach((node) => { - if (!sources.has(node.id)) { - found.push(node); - } - }); - return found[0]; + return _lastNode(this); } /** @@ -188,28 +189,55 @@ export class Graph { trimFirstNode(): void { const firstNode = this.firstNode(); - if (firstNode) { - const outgoingEdges = this.edges.filter( - (edge) => edge.source === firstNode.id - ); - if (Object.keys(this.nodes).length === 1 || outgoingEdges.length === 1) { - this.removeNode(firstNode); - } + if (firstNode && _firstNode(this, [firstNode.id])) { + this.removeNode(firstNode); } } trimLastNode(): void { const lastNode = this.lastNode(); - if (lastNode) { - const incomingEdges = this.edges.filter( - (edge) => edge.target === lastNode.id - ); - if (Object.keys(this.nodes).length === 1 || incomingEdges.length === 1) { - this.removeNode(lastNode); - } + if (lastNode && _lastNode(this, [lastNode.id])) { + this.removeNode(lastNode); } } + /** + * Return a new graph with all nodes re-identified, + * using their unique, readable names where possible. + */ + reid(): Graph { + const nodeLabels: Record = Object.fromEntries( + Object.values(this.nodes).map((node) => [node.id, node.name]) + ); + const nodeLabelCounts = new Map(); + Object.values(nodeLabels).forEach((label) => { + nodeLabelCounts.set(label, (nodeLabelCounts.get(label) || 0) + 1); + }); + + const getNodeId = (nodeId: string): string => { + const label = nodeLabels[nodeId]; + if (isUuid(nodeId) && nodeLabelCounts.get(label) === 1) { + return label; + } else { + return nodeId; + } + }; + + return new Graph({ + nodes: Object.fromEntries( + Object.entries(this.nodes).map(([id, node]) => [ + getNodeId(id), + { ...node, id: getNodeId(id) }, + ]) + ), + edges: this.edges.map((edge) => ({ + ...edge, + source: getNodeId(edge.source), + target: getNodeId(edge.target), + })), + }); + } + drawMermaid(params?: { withStyles?: boolean; curveStyle?: string; @@ -219,23 +247,21 @@ export class Graph { const { withStyles, curveStyle, - nodeColors = { start: "#ffdfba", end: "#baffc9", other: "#fad7de" }, + nodeColors = { + default: "fill:#f2f0ff,line-height:1.2", + first: "fill-opacity:0", + last: "fill:#bfb6fc", + }, wrapLabelNWords, } = params ?? {}; - const nodes: Record = {}; - for (const node of Object.values(this.nodes)) { - nodes[node.id] = nodeDataStr(node); - } + const graph = this.reid(); + const firstNode = graph.firstNode(); - const firstNode = this.firstNode(); - const firstNodeLabel = firstNode ? nodeDataStr(firstNode) : undefined; - - const lastNode = this.lastNode(); - const lastNodeLabel = lastNode ? nodeDataStr(lastNode) : undefined; + const lastNode = graph.lastNode(); - return drawMermaid(nodes, this.edges, { - firstNodeLabel, - lastNodeLabel, + return drawMermaid(graph.nodes, graph.edges, { + firstNode: firstNode?.id, + lastNode: lastNode?.id, withStyles, curveStyle, nodeColors, @@ -256,3 +282,46 @@ export class Graph { }); } } +/** + * Find the single node that is not a target of any edge. + * Exclude nodes/sources with ids in the exclude list. + * If there is no such node, or there are multiple, return undefined. + * When drawing the graph, this node would be the origin. + */ +function _firstNode(graph: Graph, exclude: string[] = []): Node | undefined { + const targets = new Set( + graph.edges + .filter((edge) => !exclude.includes(edge.source)) + .map((edge) => edge.target) + ); + + const found: Node[] = []; + for (const node of Object.values(graph.nodes)) { + if (!exclude.includes(node.id) && !targets.has(node.id)) { + found.push(node); + } + } + return found.length === 1 ? found[0] : undefined; +} + +/** + * Find the single node that is not a source of any edge. + * Exclude nodes/targets with ids in the exclude list. + * If there is no such node, or there are multiple, return undefined. + * When drawing the graph, this node would be the destination. + */ +function _lastNode(graph: Graph, exclude: string[] = []): Node | undefined { + const sources = new Set( + graph.edges + .filter((edge) => !exclude.includes(edge.target)) + .map((edge) => edge.source) + ); + + const found: Node[] = []; + for (const node of Object.values(graph.nodes)) { + if (!exclude.includes(node.id) && !sources.has(node.id)) { + found.push(node); + } + } + return found.length === 1 ? found[0] : undefined; +} diff --git a/langchain-core/src/runnables/graph_mermaid.ts b/langchain-core/src/runnables/graph_mermaid.ts index 918fe07c9703..8bf296da29bc 100644 --- a/langchain-core/src/runnables/graph_mermaid.ts +++ b/langchain-core/src/runnables/graph_mermaid.ts @@ -1,23 +1,18 @@ -import { Edge } from "./types.js"; +import { Edge, Node } from "./types.js"; function _escapeNodeLabel(nodeLabel: string): string { // Escapes the node label for Mermaid syntax. return nodeLabel.replace(/[^a-zA-Z-_0-9]/g, "_"); } -// Adjusts Mermaid edge to map conditional nodes to pure nodes. -function _adjustMermaidEdge(edge: Edge, nodes: Record) { - const sourceNodeLabel = nodes[edge.source] ?? edge.source; - const targetNodeLabel = nodes[edge.target] ?? edge.target; - return [sourceNodeLabel, targetNodeLabel]; -} +const MARKDOWN_SPECIAL_CHARS = ["*", "_", "`"]; function _generateMermaidGraphStyles( nodeColors: Record ): string { let styles = ""; for (const [className, color] of Object.entries(nodeColors)) { - styles += `\tclassDef ${className}class fill:${color};\n`; + styles += `\tclassDef ${className} ${color};\n`; } return styles; } @@ -26,11 +21,11 @@ function _generateMermaidGraphStyles( * Draws a Mermaid graph using the provided graph data */ export function drawMermaid( - nodes: Record, + nodes: Record, edges: Edge[], config?: { - firstNodeLabel?: string; - lastNodeLabel?: string; + firstNode?: string; + lastNode?: string; curveStyle?: string; withStyles?: boolean; nodeColors?: Record; @@ -38,8 +33,8 @@ export function drawMermaid( } ): string { const { - firstNodeLabel, - lastNodeLabel, + firstNode, + lastNode, nodeColors, withStyles = true, curveStyle = "linear", @@ -53,92 +48,126 @@ export function drawMermaid( // Node formatting templates const defaultClassLabel = "default"; const formatDict: Record = { - [defaultClassLabel]: "{0}([{1}]):::otherclass", + [defaultClassLabel]: "{0}({1})", }; - if (firstNodeLabel !== undefined) { - formatDict[firstNodeLabel] = "{0}[{0}]:::startclass"; + if (firstNode !== undefined) { + formatDict[firstNode] = "{0}([{1}]):::first"; } - if (lastNodeLabel !== undefined) { - formatDict[lastNodeLabel] = "{0}[{0}]:::endclass"; + if (lastNode !== undefined) { + formatDict[lastNode] = "{0}([{1}]):::last"; } // Add nodes to the graph - for (const node of Object.values(nodes)) { - const nodeLabel = formatDict[node] ?? formatDict[defaultClassLabel]; - const escapedNodeLabel = _escapeNodeLabel(node); - const nodeParts = node.split(":"); - const nodeSplit = nodeParts[nodeParts.length - 1]; - mermaidGraph += `\t${nodeLabel - .replace(/\{0\}/g, escapedNodeLabel) - .replace(/\{1\}/g, nodeSplit)};\n`; + for (const [key, node] of Object.entries(nodes)) { + const nodeName = node.name.split(":").pop() ?? ""; + const label = MARKDOWN_SPECIAL_CHARS.some( + (char) => nodeName.startsWith(char) && nodeName.endsWith(char) + ) + ? `

${nodeName}

` + : nodeName; + + let finalLabel = label; + if (Object.keys(node.metadata ?? {}).length) { + finalLabel += `
${Object.entries(node.metadata ?? {}) + .map(([k, v]) => `${k} = ${v}`) + .join("\n")}`; + } + + const nodeLabel = (formatDict[key] ?? formatDict[defaultClassLabel]) + .replace("{0}", _escapeNodeLabel(key)) + .replace("{1}", finalLabel); + + mermaidGraph += `\t${nodeLabel}\n`; } } - let subgraph = ""; - // Add edges to the graph + + // Group edges by their common prefixes + const edgeGroups: Record = {}; for (const edge of edges) { - const sourcePrefix = edge.source.includes(":") - ? edge.source.split(":")[0] - : undefined; - const targetPrefix = edge.target.includes(":") - ? edge.target.split(":")[0] - : undefined; - // Exit subgraph if source or target is not in the same subgraph - if ( - subgraph !== "" && - (subgraph !== sourcePrefix || subgraph !== targetPrefix) - ) { - mermaidGraph += "\tend\n"; - subgraph = ""; + const srcParts = edge.source.split(":"); + const tgtParts = edge.target.split(":"); + const commonPrefix = srcParts + .filter((src, i) => src === tgtParts[i]) + .join(":"); + if (!edgeGroups[commonPrefix]) { + edgeGroups[commonPrefix] = []; } - // Enter subgraph if source and target are in the same subgraph - if ( - subgraph === "" && - sourcePrefix !== undefined && - sourcePrefix === targetPrefix - ) { - mermaidGraph = `\tsubgraph ${sourcePrefix}\n`; - subgraph = sourcePrefix; - } - const [source, target] = _adjustMermaidEdge(edge, nodes); - let edgeLabel = ""; - // Add BR every wrapLabelNWords words - if (edge.data !== undefined) { - let edgeData = edge.data; - const words = edgeData.split(" "); - // Group words into chunks of wrapLabelNWords size - if (words.length > wrapLabelNWords) { - edgeData = words - .reduce((acc: string[], word: string, i: number) => { - if (i % wrapLabelNWords === 0) acc.push(""); - acc[acc.length - 1] += ` ${word}`; - return acc; - }, []) - .join("
"); + edgeGroups[commonPrefix].push(edge); + } + + const seenSubgraphs = new Set(); + + function addSubgraph(edges: Edge[], prefix: string): void { + const selfLoop = edges.length === 1 && edges[0].source === edges[0].target; + if (prefix && !selfLoop) { + const subgraph = prefix.split(":").pop()!; + if (seenSubgraphs.has(subgraph)) { + throw new Error( + `Found duplicate subgraph '${subgraph}' -- this likely means that ` + + "you're reusing a subgraph node with the same name. " + + "Please adjust your graph to have subgraph nodes with unique names." + ); } - if (edge.conditional) { - edgeLabel = ` -. ${edgeData} .-> `; + + seenSubgraphs.add(subgraph); + mermaidGraph += `\tsubgraph ${subgraph}\n`; + } + + for (const edge of edges) { + const { source, target, data, conditional } = edge; + + let edgeLabel = ""; + if (data !== undefined) { + let edgeData = data; + const words = edgeData.split(" "); + if (words.length > wrapLabelNWords) { + edgeData = Array.from( + { length: Math.ceil(words.length / wrapLabelNWords) }, + (_, i) => + words + .slice(i * wrapLabelNWords, (i + 1) * wrapLabelNWords) + .join(" ") + ).join(" 
 "); + } + edgeLabel = conditional + ? ` -.  ${edgeData}  .-> ` + : ` --  ${edgeData}  --> `; } else { - edgeLabel = ` -- ${edgeData} --> `; + edgeLabel = conditional ? " -.-> " : " --> "; } - } else { - if (edge.conditional) { - edgeLabel = ` -.-> `; - } else { - edgeLabel = ` --> `; + + mermaidGraph += `\t${_escapeNodeLabel( + source + )}${edgeLabel}${_escapeNodeLabel(target)};\n`; + } + + // Recursively add nested subgraphs + for (const nestedPrefix in edgeGroups) { + if (nestedPrefix.startsWith(`${prefix}:`) && nestedPrefix !== prefix) { + addSubgraph(edgeGroups[nestedPrefix], nestedPrefix); } } - mermaidGraph += `\t${_escapeNodeLabel( - source - )}${edgeLabel}${_escapeNodeLabel(target)};\n`; + + if (prefix && !selfLoop) { + mermaidGraph += "\tend\n"; + } } - if (subgraph !== "") { - mermaidGraph += "end\n"; + + // Start with the top-level edges (no common prefix) + addSubgraph(edgeGroups[""] ?? [], ""); + + // Add remaining subgraphs + for (const prefix in edgeGroups) { + if (!prefix.includes(":") && prefix !== "") { + addSubgraph(edgeGroups[prefix], prefix); + } } // Add custom styles for nodes - if (withStyles && nodeColors !== undefined) { - mermaidGraph += _generateMermaidGraphStyles(nodeColors); + if (withStyles) { + mermaidGraph += _generateMermaidGraphStyles(nodeColors ?? {}); } + return mermaidGraph; } diff --git a/langchain-core/src/runnables/tests/data/mermaid.png b/langchain-core/src/runnables/tests/data/mermaid.png index c0db4875bff32ae4dec45fe1e79ae36786ad9ece..2926f7a13d167e3db54dedf13bf3554c8a54902e 100644 GIT binary patch literal 19659 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 MOsz16>2m7-0h)|t-2eap literal 19879 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 Q>;Av?0sj+<>t_@H3;&r|od5s; diff --git a/langchain-core/src/runnables/tests/runnable_graph.test.ts b/langchain-core/src/runnables/tests/runnable_graph.test.ts index ead8eb472e30..877c9d1d5ed6 100644 --- a/langchain-core/src/runnables/tests/runnable_graph.test.ts +++ b/langchain-core/src/runnables/tests/runnable_graph.test.ts @@ -91,17 +91,17 @@ test("Test graph sequence", async () => { expect(graph.drawMermaid()) .toEqual(`%%{init: {'flowchart': {'curve': 'linear'}}}%% graph TD; -\tPromptTemplateInput[PromptTemplateInput]:::startclass; -\tPromptTemplate([PromptTemplate]):::otherclass; -\tFakeLLM([FakeLLM]):::otherclass; -\tCommaSeparatedListOutputParser([CommaSeparatedListOutputParser]):::otherclass; -\tCommaSeparatedListOutputParserOutput[CommaSeparatedListOutputParserOutput]:::endclass; +\tPromptTemplateInput([PromptTemplateInput]):::first +\tPromptTemplate(PromptTemplate) +\tFakeLLM(FakeLLM) +\tCommaSeparatedListOutputParser(CommaSeparatedListOutputParser) +\tCommaSeparatedListOutputParserOutput([CommaSeparatedListOutputParserOutput]):::last \tPromptTemplateInput --> PromptTemplate; \tPromptTemplate --> FakeLLM; \tCommaSeparatedListOutputParser --> CommaSeparatedListOutputParserOutput; \tFakeLLM --> CommaSeparatedListOutputParser; -\tclassDef startclass fill:#ffdfba; -\tclassDef endclass fill:#baffc9; -\tclassDef otherclass fill:#fad7de; +\tclassDef default fill:#f2f0ff,line-height:1.2; +\tclassDef first fill-opacity:0; +\tclassDef last fill:#bfb6fc; `); }); diff --git a/langchain-core/src/runnables/types.ts b/langchain-core/src/runnables/types.ts index e7ddfa8c3852..26c40bcfdd4e 100644 --- a/langchain-core/src/runnables/types.ts +++ b/langchain-core/src/runnables/types.ts @@ -71,7 +71,10 @@ export interface Edge { export interface Node { id: string; + name: string; data: RunnableIOSchema | RunnableInterface; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + metadata?: Record; } export interface RunnableConfig extends BaseCallbackConfig {