UNPKG

@langchain/core

Version:
148 lines (147 loc) 6.06 kB
function _escapeNodeLabel(nodeLabel) { // Escapes the node label for Mermaid syntax. return nodeLabel.replace(/[^a-zA-Z-_0-9]/g, "_"); } const MARKDOWN_SPECIAL_CHARS = ["*", "_", "`"]; function _generateMermaidGraphStyles(nodeColors) { let styles = ""; for (const [className, color] of Object.entries(nodeColors)) { styles += `\tclassDef ${className} ${color};\n`; } return styles; } /** * Draws a Mermaid graph using the provided graph data */ export function drawMermaid(nodes, edges, config) { const { firstNode, lastNode, nodeColors, withStyles = true, curveStyle = "linear", wrapLabelNWords = 9, } = config ?? {}; // Initialize Mermaid graph configuration let mermaidGraph = withStyles ? `%%{init: {'flowchart': {'curve': '${curveStyle}'}}}%%\ngraph TD;\n` : "graph TD;\n"; if (withStyles) { // Node formatting templates const defaultClassLabel = "default"; const formatDict = { [defaultClassLabel]: "{0}({1})", }; if (firstNode !== undefined) { formatDict[firstNode] = "{0}([{1}]):::first"; } if (lastNode !== undefined) { formatDict[lastNode] = "{0}([{1}]):::last"; } // Add nodes to the graph 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)) ? `<p>${nodeName}</p>` : nodeName; let finalLabel = label; if (Object.keys(node.metadata ?? {}).length) { finalLabel += `<hr/><small><em>${Object.entries(node.metadata ?? {}) .map(([k, v]) => `${k} = ${v}`) .join("\n")}</em></small>`; } const nodeLabel = (formatDict[key] ?? formatDict[defaultClassLabel]) .replace("{0}", _escapeNodeLabel(key)) .replace("{1}", finalLabel); mermaidGraph += `\t${nodeLabel}\n`; } } // Group edges by their common prefixes const edgeGroups = {}; for (const edge of edges) { 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] = []; } edgeGroups[commonPrefix].push(edge); } const seenSubgraphs = new Set(); function addSubgraph(edges, prefix) { 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."); } 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("&nbsp;<br>&nbsp;"); } edgeLabel = conditional ? ` -. &nbsp;${edgeData}&nbsp; .-> ` : ` -- &nbsp;${edgeData}&nbsp; --> `; } else { edgeLabel = conditional ? " -.-> " : " --> "; } 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); } } if (prefix && !selfLoop) { mermaidGraph += "\tend\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) { mermaidGraph += _generateMermaidGraphStyles(nodeColors ?? {}); } return mermaidGraph; } /** * Renders Mermaid graph using the Mermaid.INK API. */ export async function drawMermaidPng(mermaidSyntax, config) { let { backgroundColor = "white" } = config ?? {}; // Use btoa for compatibility, assume ASCII const mermaidSyntaxEncoded = btoa(mermaidSyntax); // Check if the background color is a hexadecimal color code using regex if (backgroundColor !== undefined) { const hexColorPattern = /^#(?:[0-9a-fA-F]{3}){1,2}$/; if (!hexColorPattern.test(backgroundColor)) { backgroundColor = `!${backgroundColor}`; } } const imageUrl = `https://mermaid.ink/img/${mermaidSyntaxEncoded}?bgColor=${backgroundColor}`; const res = await fetch(imageUrl); if (!res.ok) { throw new Error([ `Failed to render the graph using the Mermaid.INK API.`, `Status code: ${res.status}`, `Status text: ${res.statusText}`, ].join("\n")); } const content = await res.blob(); return content; }