@langchain/core
Version:
Core LangChain.js abstractions and schemas
148 lines (147 loc) • 6.06 kB
JavaScript
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(" <br> ");
}
edgeLabel = conditional
? ` -. ${edgeData} .-> `
: ` -- ${edgeData} --> `;
}
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;
}