@langchain/core 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/runnables/graph.cjs +109 -56
- package/dist/runnables/graph.d.ts +10 -1
- package/dist/runnables/graph.js +109 -56
- package/dist/runnables/graph_mermaid.cjs +78 -71
- package/dist/runnables/graph_mermaid.d.ts +4 -4
- package/dist/runnables/graph_mermaid.js +78 -71
- package/dist/runnables/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/runnables/graph.cjs
CHANGED
|
@@ -5,30 +5,28 @@ const zod_to_json_schema_1 = require("zod-to-json-schema");
|
|
|
5
5
|
const uuid_1 = require("uuid");
|
|
6
6
|
const utils_js_1 = require("./utils.cjs");
|
|
7
7
|
const graph_mermaid_js_1 = require("./graph_mermaid.cjs");
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return node.id;
|
|
8
|
+
function nodeDataStr(id, data) {
|
|
9
|
+
if (id !== undefined && !(0, uuid_1.validate)(id)) {
|
|
10
|
+
return id;
|
|
12
11
|
}
|
|
13
|
-
else if ((0, utils_js_1.isRunnableInterface)(
|
|
12
|
+
else if ((0, utils_js_1.isRunnableInterface)(data)) {
|
|
14
13
|
try {
|
|
15
|
-
let
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return data;
|
|
14
|
+
let dataStr = data.getName();
|
|
15
|
+
dataStr = dataStr.startsWith("Runnable")
|
|
16
|
+
? dataStr.slice("Runnable".length)
|
|
17
|
+
: dataStr;
|
|
18
|
+
return dataStr;
|
|
21
19
|
}
|
|
22
20
|
catch (error) {
|
|
23
|
-
return
|
|
21
|
+
return data.getName();
|
|
24
22
|
}
|
|
25
23
|
}
|
|
26
24
|
else {
|
|
27
|
-
return
|
|
25
|
+
return data.name ?? "UnknownSchema";
|
|
28
26
|
}
|
|
29
27
|
}
|
|
30
28
|
function nodeDataJson(node) {
|
|
31
|
-
// if node.data
|
|
29
|
+
// if node.data implements Runnable
|
|
32
30
|
if ((0, utils_js_1.isRunnableInterface)(node.data)) {
|
|
33
31
|
return {
|
|
34
32
|
type: "runnable",
|
|
@@ -46,7 +44,7 @@ function nodeDataJson(node) {
|
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
46
|
class Graph {
|
|
49
|
-
constructor() {
|
|
47
|
+
constructor(params) {
|
|
50
48
|
Object.defineProperty(this, "nodes", {
|
|
51
49
|
enumerable: true,
|
|
52
50
|
configurable: true,
|
|
@@ -59,6 +57,8 @@ class Graph {
|
|
|
59
57
|
writable: true,
|
|
60
58
|
value: []
|
|
61
59
|
});
|
|
60
|
+
this.nodes = params?.nodes ?? this.nodes;
|
|
61
|
+
this.edges = params?.edges ?? this.edges;
|
|
62
62
|
}
|
|
63
63
|
// Convert the graph to a JSON-serializable format.
|
|
64
64
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -87,12 +87,19 @@ class Graph {
|
|
|
87
87
|
}),
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
-
addNode(data, id
|
|
90
|
+
addNode(data, id,
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
metadata) {
|
|
91
93
|
if (id !== undefined && this.nodes[id] !== undefined) {
|
|
92
94
|
throw new Error(`Node with id ${id} already exists`);
|
|
93
95
|
}
|
|
94
|
-
const nodeId = id
|
|
95
|
-
const node = {
|
|
96
|
+
const nodeId = id ?? (0, uuid_1.v4)();
|
|
97
|
+
const node = {
|
|
98
|
+
id: nodeId,
|
|
99
|
+
data,
|
|
100
|
+
name: nodeDataStr(id, data),
|
|
101
|
+
metadata,
|
|
102
|
+
};
|
|
96
103
|
this.nodes[nodeId] = node;
|
|
97
104
|
return node;
|
|
98
105
|
}
|
|
@@ -119,24 +126,10 @@ class Graph {
|
|
|
119
126
|
return edge;
|
|
120
127
|
}
|
|
121
128
|
firstNode() {
|
|
122
|
-
|
|
123
|
-
const found = [];
|
|
124
|
-
Object.values(this.nodes).forEach((node) => {
|
|
125
|
-
if (!targets.has(node.id)) {
|
|
126
|
-
found.push(node);
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
return found[0];
|
|
129
|
+
return _firstNode(this);
|
|
130
130
|
}
|
|
131
131
|
lastNode() {
|
|
132
|
-
|
|
133
|
-
const found = [];
|
|
134
|
-
Object.values(this.nodes).forEach((node) => {
|
|
135
|
-
if (!sources.has(node.id)) {
|
|
136
|
-
found.push(node);
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
return found[0];
|
|
132
|
+
return _lastNode(this);
|
|
140
133
|
}
|
|
141
134
|
/**
|
|
142
135
|
* Add all nodes and edges from another graph.
|
|
@@ -172,35 +165,59 @@ class Graph {
|
|
|
172
165
|
}
|
|
173
166
|
trimFirstNode() {
|
|
174
167
|
const firstNode = this.firstNode();
|
|
175
|
-
if (firstNode) {
|
|
176
|
-
|
|
177
|
-
if (Object.keys(this.nodes).length === 1 || outgoingEdges.length === 1) {
|
|
178
|
-
this.removeNode(firstNode);
|
|
179
|
-
}
|
|
168
|
+
if (firstNode && _firstNode(this, [firstNode.id])) {
|
|
169
|
+
this.removeNode(firstNode);
|
|
180
170
|
}
|
|
181
171
|
}
|
|
182
172
|
trimLastNode() {
|
|
183
173
|
const lastNode = this.lastNode();
|
|
184
|
-
if (lastNode) {
|
|
185
|
-
|
|
186
|
-
if (Object.keys(this.nodes).length === 1 || incomingEdges.length === 1) {
|
|
187
|
-
this.removeNode(lastNode);
|
|
188
|
-
}
|
|
174
|
+
if (lastNode && _lastNode(this, [lastNode.id])) {
|
|
175
|
+
this.removeNode(lastNode);
|
|
189
176
|
}
|
|
190
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Return a new graph with all nodes re-identified,
|
|
180
|
+
* using their unique, readable names where possible.
|
|
181
|
+
*/
|
|
182
|
+
reid() {
|
|
183
|
+
const nodeLabels = Object.fromEntries(Object.values(this.nodes).map((node) => [node.id, node.name]));
|
|
184
|
+
const nodeLabelCounts = new Map();
|
|
185
|
+
Object.values(nodeLabels).forEach((label) => {
|
|
186
|
+
nodeLabelCounts.set(label, (nodeLabelCounts.get(label) || 0) + 1);
|
|
187
|
+
});
|
|
188
|
+
const getNodeId = (nodeId) => {
|
|
189
|
+
const label = nodeLabels[nodeId];
|
|
190
|
+
if ((0, uuid_1.validate)(nodeId) && nodeLabelCounts.get(label) === 1) {
|
|
191
|
+
return label;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
return nodeId;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
return new Graph({
|
|
198
|
+
nodes: Object.fromEntries(Object.entries(this.nodes).map(([id, node]) => [
|
|
199
|
+
getNodeId(id),
|
|
200
|
+
{ ...node, id: getNodeId(id) },
|
|
201
|
+
])),
|
|
202
|
+
edges: this.edges.map((edge) => ({
|
|
203
|
+
...edge,
|
|
204
|
+
source: getNodeId(edge.source),
|
|
205
|
+
target: getNodeId(edge.target),
|
|
206
|
+
})),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
191
209
|
drawMermaid(params) {
|
|
192
|
-
const { withStyles, curveStyle, nodeColors = {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const lastNode =
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
lastNodeLabel,
|
|
210
|
+
const { withStyles, curveStyle, nodeColors = {
|
|
211
|
+
default: "fill:#f2f0ff,line-height:1.2",
|
|
212
|
+
first: "fill-opacity:0",
|
|
213
|
+
last: "fill:#bfb6fc",
|
|
214
|
+
}, wrapLabelNWords, } = params ?? {};
|
|
215
|
+
const graph = this.reid();
|
|
216
|
+
const firstNode = graph.firstNode();
|
|
217
|
+
const lastNode = graph.lastNode();
|
|
218
|
+
return (0, graph_mermaid_js_1.drawMermaid)(graph.nodes, graph.edges, {
|
|
219
|
+
firstNode: firstNode?.id,
|
|
220
|
+
lastNode: lastNode?.id,
|
|
204
221
|
withStyles,
|
|
205
222
|
curveStyle,
|
|
206
223
|
nodeColors,
|
|
@@ -215,3 +232,39 @@ class Graph {
|
|
|
215
232
|
}
|
|
216
233
|
}
|
|
217
234
|
exports.Graph = Graph;
|
|
235
|
+
/**
|
|
236
|
+
* Find the single node that is not a target of any edge.
|
|
237
|
+
* Exclude nodes/sources with ids in the exclude list.
|
|
238
|
+
* If there is no such node, or there are multiple, return undefined.
|
|
239
|
+
* When drawing the graph, this node would be the origin.
|
|
240
|
+
*/
|
|
241
|
+
function _firstNode(graph, exclude = []) {
|
|
242
|
+
const targets = new Set(graph.edges
|
|
243
|
+
.filter((edge) => !exclude.includes(edge.source))
|
|
244
|
+
.map((edge) => edge.target));
|
|
245
|
+
const found = [];
|
|
246
|
+
for (const node of Object.values(graph.nodes)) {
|
|
247
|
+
if (!exclude.includes(node.id) && !targets.has(node.id)) {
|
|
248
|
+
found.push(node);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return found.length === 1 ? found[0] : undefined;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Find the single node that is not a source of any edge.
|
|
255
|
+
* Exclude nodes/targets with ids in the exclude list.
|
|
256
|
+
* If there is no such node, or there are multiple, return undefined.
|
|
257
|
+
* When drawing the graph, this node would be the destination.
|
|
258
|
+
*/
|
|
259
|
+
function _lastNode(graph, exclude = []) {
|
|
260
|
+
const sources = new Set(graph.edges
|
|
261
|
+
.filter((edge) => !exclude.includes(edge.target))
|
|
262
|
+
.map((edge) => edge.source));
|
|
263
|
+
const found = [];
|
|
264
|
+
for (const node of Object.values(graph.nodes)) {
|
|
265
|
+
if (!exclude.includes(node.id) && !sources.has(node.id)) {
|
|
266
|
+
found.push(node);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return found.length === 1 ? found[0] : undefined;
|
|
270
|
+
}
|
|
@@ -3,8 +3,12 @@ export { Node, Edge };
|
|
|
3
3
|
export declare class Graph {
|
|
4
4
|
nodes: Record<string, Node>;
|
|
5
5
|
edges: Edge[];
|
|
6
|
+
constructor(params?: {
|
|
7
|
+
nodes: Record<string, Node>;
|
|
8
|
+
edges: Edge[];
|
|
9
|
+
});
|
|
6
10
|
toJSON(): Record<string, any>;
|
|
7
|
-
addNode(data: RunnableInterface | RunnableIOSchema, id?: string): Node;
|
|
11
|
+
addNode(data: RunnableInterface | RunnableIOSchema, id?: string, metadata?: Record<string, any>): Node;
|
|
8
12
|
removeNode(node: Node): void;
|
|
9
13
|
addEdge(source: Node, target: Node, data?: string, conditional?: boolean): Edge;
|
|
10
14
|
firstNode(): Node | undefined;
|
|
@@ -19,6 +23,11 @@ export declare class Graph {
|
|
|
19
23
|
} | undefined)[];
|
|
20
24
|
trimFirstNode(): void;
|
|
21
25
|
trimLastNode(): void;
|
|
26
|
+
/**
|
|
27
|
+
* Return a new graph with all nodes re-identified,
|
|
28
|
+
* using their unique, readable names where possible.
|
|
29
|
+
*/
|
|
30
|
+
reid(): Graph;
|
|
22
31
|
drawMermaid(params?: {
|
|
23
32
|
withStyles?: boolean;
|
|
24
33
|
curveStyle?: string;
|
package/dist/runnables/graph.js
CHANGED
|
@@ -2,30 +2,28 @@ import { zodToJsonSchema } from "zod-to-json-schema";
|
|
|
2
2
|
import { v4 as uuidv4, validate as isUuid } from "uuid";
|
|
3
3
|
import { isRunnableInterface } from "./utils.js";
|
|
4
4
|
import { drawMermaid, drawMermaidPng } from "./graph_mermaid.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return node.id;
|
|
5
|
+
function nodeDataStr(id, data) {
|
|
6
|
+
if (id !== undefined && !isUuid(id)) {
|
|
7
|
+
return id;
|
|
9
8
|
}
|
|
10
|
-
else if (isRunnableInterface(
|
|
9
|
+
else if (isRunnableInterface(data)) {
|
|
11
10
|
try {
|
|
12
|
-
let
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return data;
|
|
11
|
+
let dataStr = data.getName();
|
|
12
|
+
dataStr = dataStr.startsWith("Runnable")
|
|
13
|
+
? dataStr.slice("Runnable".length)
|
|
14
|
+
: dataStr;
|
|
15
|
+
return dataStr;
|
|
18
16
|
}
|
|
19
17
|
catch (error) {
|
|
20
|
-
return
|
|
18
|
+
return data.getName();
|
|
21
19
|
}
|
|
22
20
|
}
|
|
23
21
|
else {
|
|
24
|
-
return
|
|
22
|
+
return data.name ?? "UnknownSchema";
|
|
25
23
|
}
|
|
26
24
|
}
|
|
27
25
|
function nodeDataJson(node) {
|
|
28
|
-
// if node.data
|
|
26
|
+
// if node.data implements Runnable
|
|
29
27
|
if (isRunnableInterface(node.data)) {
|
|
30
28
|
return {
|
|
31
29
|
type: "runnable",
|
|
@@ -43,7 +41,7 @@ function nodeDataJson(node) {
|
|
|
43
41
|
}
|
|
44
42
|
}
|
|
45
43
|
export class Graph {
|
|
46
|
-
constructor() {
|
|
44
|
+
constructor(params) {
|
|
47
45
|
Object.defineProperty(this, "nodes", {
|
|
48
46
|
enumerable: true,
|
|
49
47
|
configurable: true,
|
|
@@ -56,6 +54,8 @@ export class Graph {
|
|
|
56
54
|
writable: true,
|
|
57
55
|
value: []
|
|
58
56
|
});
|
|
57
|
+
this.nodes = params?.nodes ?? this.nodes;
|
|
58
|
+
this.edges = params?.edges ?? this.edges;
|
|
59
59
|
}
|
|
60
60
|
// Convert the graph to a JSON-serializable format.
|
|
61
61
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -84,12 +84,19 @@ export class Graph {
|
|
|
84
84
|
}),
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
|
-
addNode(data, id
|
|
87
|
+
addNode(data, id,
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
metadata) {
|
|
88
90
|
if (id !== undefined && this.nodes[id] !== undefined) {
|
|
89
91
|
throw new Error(`Node with id ${id} already exists`);
|
|
90
92
|
}
|
|
91
|
-
const nodeId = id
|
|
92
|
-
const node = {
|
|
93
|
+
const nodeId = id ?? uuidv4();
|
|
94
|
+
const node = {
|
|
95
|
+
id: nodeId,
|
|
96
|
+
data,
|
|
97
|
+
name: nodeDataStr(id, data),
|
|
98
|
+
metadata,
|
|
99
|
+
};
|
|
93
100
|
this.nodes[nodeId] = node;
|
|
94
101
|
return node;
|
|
95
102
|
}
|
|
@@ -116,24 +123,10 @@ export class Graph {
|
|
|
116
123
|
return edge;
|
|
117
124
|
}
|
|
118
125
|
firstNode() {
|
|
119
|
-
|
|
120
|
-
const found = [];
|
|
121
|
-
Object.values(this.nodes).forEach((node) => {
|
|
122
|
-
if (!targets.has(node.id)) {
|
|
123
|
-
found.push(node);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
return found[0];
|
|
126
|
+
return _firstNode(this);
|
|
127
127
|
}
|
|
128
128
|
lastNode() {
|
|
129
|
-
|
|
130
|
-
const found = [];
|
|
131
|
-
Object.values(this.nodes).forEach((node) => {
|
|
132
|
-
if (!sources.has(node.id)) {
|
|
133
|
-
found.push(node);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
return found[0];
|
|
129
|
+
return _lastNode(this);
|
|
137
130
|
}
|
|
138
131
|
/**
|
|
139
132
|
* Add all nodes and edges from another graph.
|
|
@@ -169,35 +162,59 @@ export class Graph {
|
|
|
169
162
|
}
|
|
170
163
|
trimFirstNode() {
|
|
171
164
|
const firstNode = this.firstNode();
|
|
172
|
-
if (firstNode) {
|
|
173
|
-
|
|
174
|
-
if (Object.keys(this.nodes).length === 1 || outgoingEdges.length === 1) {
|
|
175
|
-
this.removeNode(firstNode);
|
|
176
|
-
}
|
|
165
|
+
if (firstNode && _firstNode(this, [firstNode.id])) {
|
|
166
|
+
this.removeNode(firstNode);
|
|
177
167
|
}
|
|
178
168
|
}
|
|
179
169
|
trimLastNode() {
|
|
180
170
|
const lastNode = this.lastNode();
|
|
181
|
-
if (lastNode) {
|
|
182
|
-
|
|
183
|
-
if (Object.keys(this.nodes).length === 1 || incomingEdges.length === 1) {
|
|
184
|
-
this.removeNode(lastNode);
|
|
185
|
-
}
|
|
171
|
+
if (lastNode && _lastNode(this, [lastNode.id])) {
|
|
172
|
+
this.removeNode(lastNode);
|
|
186
173
|
}
|
|
187
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Return a new graph with all nodes re-identified,
|
|
177
|
+
* using their unique, readable names where possible.
|
|
178
|
+
*/
|
|
179
|
+
reid() {
|
|
180
|
+
const nodeLabels = Object.fromEntries(Object.values(this.nodes).map((node) => [node.id, node.name]));
|
|
181
|
+
const nodeLabelCounts = new Map();
|
|
182
|
+
Object.values(nodeLabels).forEach((label) => {
|
|
183
|
+
nodeLabelCounts.set(label, (nodeLabelCounts.get(label) || 0) + 1);
|
|
184
|
+
});
|
|
185
|
+
const getNodeId = (nodeId) => {
|
|
186
|
+
const label = nodeLabels[nodeId];
|
|
187
|
+
if (isUuid(nodeId) && nodeLabelCounts.get(label) === 1) {
|
|
188
|
+
return label;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
return nodeId;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
return new Graph({
|
|
195
|
+
nodes: Object.fromEntries(Object.entries(this.nodes).map(([id, node]) => [
|
|
196
|
+
getNodeId(id),
|
|
197
|
+
{ ...node, id: getNodeId(id) },
|
|
198
|
+
])),
|
|
199
|
+
edges: this.edges.map((edge) => ({
|
|
200
|
+
...edge,
|
|
201
|
+
source: getNodeId(edge.source),
|
|
202
|
+
target: getNodeId(edge.target),
|
|
203
|
+
})),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
188
206
|
drawMermaid(params) {
|
|
189
|
-
const { withStyles, curveStyle, nodeColors = {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
const lastNode =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
lastNodeLabel,
|
|
207
|
+
const { withStyles, curveStyle, nodeColors = {
|
|
208
|
+
default: "fill:#f2f0ff,line-height:1.2",
|
|
209
|
+
first: "fill-opacity:0",
|
|
210
|
+
last: "fill:#bfb6fc",
|
|
211
|
+
}, wrapLabelNWords, } = params ?? {};
|
|
212
|
+
const graph = this.reid();
|
|
213
|
+
const firstNode = graph.firstNode();
|
|
214
|
+
const lastNode = graph.lastNode();
|
|
215
|
+
return drawMermaid(graph.nodes, graph.edges, {
|
|
216
|
+
firstNode: firstNode?.id,
|
|
217
|
+
lastNode: lastNode?.id,
|
|
201
218
|
withStyles,
|
|
202
219
|
curveStyle,
|
|
203
220
|
nodeColors,
|
|
@@ -211,3 +228,39 @@ export class Graph {
|
|
|
211
228
|
});
|
|
212
229
|
}
|
|
213
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Find the single node that is not a target of any edge.
|
|
233
|
+
* Exclude nodes/sources with ids in the exclude list.
|
|
234
|
+
* If there is no such node, or there are multiple, return undefined.
|
|
235
|
+
* When drawing the graph, this node would be the origin.
|
|
236
|
+
*/
|
|
237
|
+
function _firstNode(graph, exclude = []) {
|
|
238
|
+
const targets = new Set(graph.edges
|
|
239
|
+
.filter((edge) => !exclude.includes(edge.source))
|
|
240
|
+
.map((edge) => edge.target));
|
|
241
|
+
const found = [];
|
|
242
|
+
for (const node of Object.values(graph.nodes)) {
|
|
243
|
+
if (!exclude.includes(node.id) && !targets.has(node.id)) {
|
|
244
|
+
found.push(node);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return found.length === 1 ? found[0] : undefined;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Find the single node that is not a source of any edge.
|
|
251
|
+
* Exclude nodes/targets with ids in the exclude list.
|
|
252
|
+
* If there is no such node, or there are multiple, return undefined.
|
|
253
|
+
* When drawing the graph, this node would be the destination.
|
|
254
|
+
*/
|
|
255
|
+
function _lastNode(graph, exclude = []) {
|
|
256
|
+
const sources = new Set(graph.edges
|
|
257
|
+
.filter((edge) => !exclude.includes(edge.target))
|
|
258
|
+
.map((edge) => edge.source));
|
|
259
|
+
const found = [];
|
|
260
|
+
for (const node of Object.values(graph.nodes)) {
|
|
261
|
+
if (!exclude.includes(node.id) && !sources.has(node.id)) {
|
|
262
|
+
found.push(node);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return found.length === 1 ? found[0] : undefined;
|
|
266
|
+
}
|
|
@@ -5,16 +5,11 @@ function _escapeNodeLabel(nodeLabel) {
|
|
|
5
5
|
// Escapes the node label for Mermaid syntax.
|
|
6
6
|
return nodeLabel.replace(/[^a-zA-Z-_0-9]/g, "_");
|
|
7
7
|
}
|
|
8
|
-
|
|
9
|
-
function _adjustMermaidEdge(edge, nodes) {
|
|
10
|
-
const sourceNodeLabel = nodes[edge.source] ?? edge.source;
|
|
11
|
-
const targetNodeLabel = nodes[edge.target] ?? edge.target;
|
|
12
|
-
return [sourceNodeLabel, targetNodeLabel];
|
|
13
|
-
}
|
|
8
|
+
const MARKDOWN_SPECIAL_CHARS = ["*", "_", "`"];
|
|
14
9
|
function _generateMermaidGraphStyles(nodeColors) {
|
|
15
10
|
let styles = "";
|
|
16
11
|
for (const [className, color] of Object.entries(nodeColors)) {
|
|
17
|
-
styles += `\tclassDef ${className}
|
|
12
|
+
styles += `\tclassDef ${className} ${color};\n`;
|
|
18
13
|
}
|
|
19
14
|
return styles;
|
|
20
15
|
}
|
|
@@ -22,7 +17,7 @@ function _generateMermaidGraphStyles(nodeColors) {
|
|
|
22
17
|
* Draws a Mermaid graph using the provided graph data
|
|
23
18
|
*/
|
|
24
19
|
function drawMermaid(nodes, edges, config) {
|
|
25
|
-
const {
|
|
20
|
+
const { firstNode, lastNode, nodeColors, withStyles = true, curveStyle = "linear", wrapLabelNWords = 9, } = config ?? {};
|
|
26
21
|
// Initialize Mermaid graph configuration
|
|
27
22
|
let mermaidGraph = withStyles
|
|
28
23
|
? `%%{init: {'flowchart': {'curve': '${curveStyle}'}}}%%\ngraph TD;\n`
|
|
@@ -31,87 +26,99 @@ function drawMermaid(nodes, edges, config) {
|
|
|
31
26
|
// Node formatting templates
|
|
32
27
|
const defaultClassLabel = "default";
|
|
33
28
|
const formatDict = {
|
|
34
|
-
[defaultClassLabel]: "{0}(
|
|
29
|
+
[defaultClassLabel]: "{0}({1})",
|
|
35
30
|
};
|
|
36
|
-
if (
|
|
37
|
-
formatDict[
|
|
31
|
+
if (firstNode !== undefined) {
|
|
32
|
+
formatDict[firstNode] = "{0}([{1}]):::first";
|
|
38
33
|
}
|
|
39
|
-
if (
|
|
40
|
-
formatDict[
|
|
34
|
+
if (lastNode !== undefined) {
|
|
35
|
+
formatDict[lastNode] = "{0}([{1}]):::last";
|
|
41
36
|
}
|
|
42
37
|
// Add nodes to the graph
|
|
43
|
-
for (const node of Object.
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.
|
|
38
|
+
for (const [key, node] of Object.entries(nodes)) {
|
|
39
|
+
const nodeName = node.name.split(":").pop() ?? "";
|
|
40
|
+
const label = MARKDOWN_SPECIAL_CHARS.some((char) => nodeName.startsWith(char) && nodeName.endsWith(char))
|
|
41
|
+
? `<p>${nodeName}</p>`
|
|
42
|
+
: nodeName;
|
|
43
|
+
let finalLabel = label;
|
|
44
|
+
if (Object.keys(node.metadata ?? {}).length) {
|
|
45
|
+
finalLabel += `<hr/><small><em>${Object.entries(node.metadata ?? {})
|
|
46
|
+
.map(([k, v]) => `${k} = ${v}`)
|
|
47
|
+
.join("\n")}</em></small>`;
|
|
48
|
+
}
|
|
49
|
+
const nodeLabel = (formatDict[key] ?? formatDict[defaultClassLabel])
|
|
50
|
+
.replace("{0}", _escapeNodeLabel(key))
|
|
51
|
+
.replace("{1}", finalLabel);
|
|
52
|
+
mermaidGraph += `\t${nodeLabel}\n`;
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
// Group edges by their common prefixes
|
|
56
|
+
const edgeGroups = {};
|
|
55
57
|
for (const edge of edges) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (subgraph !== "" &&
|
|
64
|
-
(subgraph !== sourcePrefix || subgraph !== targetPrefix)) {
|
|
65
|
-
mermaidGraph += "\tend\n";
|
|
66
|
-
subgraph = "";
|
|
58
|
+
const srcParts = edge.source.split(":");
|
|
59
|
+
const tgtParts = edge.target.split(":");
|
|
60
|
+
const commonPrefix = srcParts
|
|
61
|
+
.filter((src, i) => src === tgtParts[i])
|
|
62
|
+
.join(":");
|
|
63
|
+
if (!edgeGroups[commonPrefix]) {
|
|
64
|
+
edgeGroups[commonPrefix] = [];
|
|
67
65
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let edgeData = edge.data;
|
|
80
|
-
const words = edgeData.split(" ");
|
|
81
|
-
// Group words into chunks of wrapLabelNWords size
|
|
82
|
-
if (words.length > wrapLabelNWords) {
|
|
83
|
-
edgeData = words
|
|
84
|
-
.reduce((acc, word, i) => {
|
|
85
|
-
if (i % wrapLabelNWords === 0)
|
|
86
|
-
acc.push("");
|
|
87
|
-
acc[acc.length - 1] += ` ${word}`;
|
|
88
|
-
return acc;
|
|
89
|
-
}, [])
|
|
90
|
-
.join("<br>");
|
|
66
|
+
edgeGroups[commonPrefix].push(edge);
|
|
67
|
+
}
|
|
68
|
+
const seenSubgraphs = new Set();
|
|
69
|
+
function addSubgraph(edges, prefix) {
|
|
70
|
+
const selfLoop = edges.length === 1 && edges[0].source === edges[0].target;
|
|
71
|
+
if (prefix && !selfLoop) {
|
|
72
|
+
const subgraph = prefix.split(":").pop();
|
|
73
|
+
if (seenSubgraphs.has(subgraph)) {
|
|
74
|
+
throw new Error(`Found duplicate subgraph '${subgraph}' -- this likely means that ` +
|
|
75
|
+
"you're reusing a subgraph node with the same name. " +
|
|
76
|
+
"Please adjust your graph to have subgraph nodes with unique names.");
|
|
91
77
|
}
|
|
92
|
-
|
|
93
|
-
|
|
78
|
+
seenSubgraphs.add(subgraph);
|
|
79
|
+
mermaidGraph += `\tsubgraph ${subgraph}\n`;
|
|
80
|
+
}
|
|
81
|
+
for (const edge of edges) {
|
|
82
|
+
const { source, target, data, conditional } = edge;
|
|
83
|
+
let edgeLabel = "";
|
|
84
|
+
if (data !== undefined) {
|
|
85
|
+
let edgeData = data;
|
|
86
|
+
const words = edgeData.split(" ");
|
|
87
|
+
if (words.length > wrapLabelNWords) {
|
|
88
|
+
edgeData = Array.from({ length: Math.ceil(words.length / wrapLabelNWords) }, (_, i) => words
|
|
89
|
+
.slice(i * wrapLabelNWords, (i + 1) * wrapLabelNWords)
|
|
90
|
+
.join(" ")).join(" <br> ");
|
|
91
|
+
}
|
|
92
|
+
edgeLabel = conditional
|
|
93
|
+
? ` -. ${edgeData} .-> `
|
|
94
|
+
: ` -- ${edgeData} --> `;
|
|
94
95
|
}
|
|
95
96
|
else {
|
|
96
|
-
edgeLabel =
|
|
97
|
+
edgeLabel = conditional ? " -.-> " : " --> ";
|
|
97
98
|
}
|
|
99
|
+
mermaidGraph += `\t${_escapeNodeLabel(source)}${edgeLabel}${_escapeNodeLabel(target)};\n`;
|
|
98
100
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
else {
|
|
104
|
-
edgeLabel = ` --> `;
|
|
101
|
+
// Recursively add nested subgraphs
|
|
102
|
+
for (const nestedPrefix in edgeGroups) {
|
|
103
|
+
if (nestedPrefix.startsWith(`${prefix}:`) && nestedPrefix !== prefix) {
|
|
104
|
+
addSubgraph(edgeGroups[nestedPrefix], nestedPrefix);
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
-
|
|
107
|
+
if (prefix && !selfLoop) {
|
|
108
|
+
mermaidGraph += "\tend\n";
|
|
109
|
+
}
|
|
108
110
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
// Start with the top-level edges (no common prefix)
|
|
112
|
+
addSubgraph(edgeGroups[""] ?? [], "");
|
|
113
|
+
// Add remaining subgraphs
|
|
114
|
+
for (const prefix in edgeGroups) {
|
|
115
|
+
if (!prefix.includes(":") && prefix !== "") {
|
|
116
|
+
addSubgraph(edgeGroups[prefix], prefix);
|
|
117
|
+
}
|
|
111
118
|
}
|
|
112
119
|
// Add custom styles for nodes
|
|
113
|
-
if (withStyles
|
|
114
|
-
mermaidGraph += _generateMermaidGraphStyles(nodeColors);
|
|
120
|
+
if (withStyles) {
|
|
121
|
+
mermaidGraph += _generateMermaidGraphStyles(nodeColors ?? {});
|
|
115
122
|
}
|
|
116
123
|
return mermaidGraph;
|
|
117
124
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Edge } from "./types.js";
|
|
1
|
+
import { Edge, Node } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Draws a Mermaid graph using the provided graph data
|
|
4
4
|
*/
|
|
5
|
-
export declare function drawMermaid(nodes: Record<string,
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export declare function drawMermaid(nodes: Record<string, Node>, edges: Edge[], config?: {
|
|
6
|
+
firstNode?: string;
|
|
7
|
+
lastNode?: string;
|
|
8
8
|
curveStyle?: string;
|
|
9
9
|
withStyles?: boolean;
|
|
10
10
|
nodeColors?: Record<string, string>;
|
|
@@ -2,16 +2,11 @@ function _escapeNodeLabel(nodeLabel) {
|
|
|
2
2
|
// Escapes the node label for Mermaid syntax.
|
|
3
3
|
return nodeLabel.replace(/[^a-zA-Z-_0-9]/g, "_");
|
|
4
4
|
}
|
|
5
|
-
|
|
6
|
-
function _adjustMermaidEdge(edge, nodes) {
|
|
7
|
-
const sourceNodeLabel = nodes[edge.source] ?? edge.source;
|
|
8
|
-
const targetNodeLabel = nodes[edge.target] ?? edge.target;
|
|
9
|
-
return [sourceNodeLabel, targetNodeLabel];
|
|
10
|
-
}
|
|
5
|
+
const MARKDOWN_SPECIAL_CHARS = ["*", "_", "`"];
|
|
11
6
|
function _generateMermaidGraphStyles(nodeColors) {
|
|
12
7
|
let styles = "";
|
|
13
8
|
for (const [className, color] of Object.entries(nodeColors)) {
|
|
14
|
-
styles += `\tclassDef ${className}
|
|
9
|
+
styles += `\tclassDef ${className} ${color};\n`;
|
|
15
10
|
}
|
|
16
11
|
return styles;
|
|
17
12
|
}
|
|
@@ -19,7 +14,7 @@ function _generateMermaidGraphStyles(nodeColors) {
|
|
|
19
14
|
* Draws a Mermaid graph using the provided graph data
|
|
20
15
|
*/
|
|
21
16
|
export function drawMermaid(nodes, edges, config) {
|
|
22
|
-
const {
|
|
17
|
+
const { firstNode, lastNode, nodeColors, withStyles = true, curveStyle = "linear", wrapLabelNWords = 9, } = config ?? {};
|
|
23
18
|
// Initialize Mermaid graph configuration
|
|
24
19
|
let mermaidGraph = withStyles
|
|
25
20
|
? `%%{init: {'flowchart': {'curve': '${curveStyle}'}}}%%\ngraph TD;\n`
|
|
@@ -28,87 +23,99 @@ export function drawMermaid(nodes, edges, config) {
|
|
|
28
23
|
// Node formatting templates
|
|
29
24
|
const defaultClassLabel = "default";
|
|
30
25
|
const formatDict = {
|
|
31
|
-
[defaultClassLabel]: "{0}(
|
|
26
|
+
[defaultClassLabel]: "{0}({1})",
|
|
32
27
|
};
|
|
33
|
-
if (
|
|
34
|
-
formatDict[
|
|
28
|
+
if (firstNode !== undefined) {
|
|
29
|
+
formatDict[firstNode] = "{0}([{1}]):::first";
|
|
35
30
|
}
|
|
36
|
-
if (
|
|
37
|
-
formatDict[
|
|
31
|
+
if (lastNode !== undefined) {
|
|
32
|
+
formatDict[lastNode] = "{0}([{1}]):::last";
|
|
38
33
|
}
|
|
39
34
|
// Add nodes to the graph
|
|
40
|
-
for (const node of Object.
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
.
|
|
35
|
+
for (const [key, node] of Object.entries(nodes)) {
|
|
36
|
+
const nodeName = node.name.split(":").pop() ?? "";
|
|
37
|
+
const label = MARKDOWN_SPECIAL_CHARS.some((char) => nodeName.startsWith(char) && nodeName.endsWith(char))
|
|
38
|
+
? `<p>${nodeName}</p>`
|
|
39
|
+
: nodeName;
|
|
40
|
+
let finalLabel = label;
|
|
41
|
+
if (Object.keys(node.metadata ?? {}).length) {
|
|
42
|
+
finalLabel += `<hr/><small><em>${Object.entries(node.metadata ?? {})
|
|
43
|
+
.map(([k, v]) => `${k} = ${v}`)
|
|
44
|
+
.join("\n")}</em></small>`;
|
|
45
|
+
}
|
|
46
|
+
const nodeLabel = (formatDict[key] ?? formatDict[defaultClassLabel])
|
|
47
|
+
.replace("{0}", _escapeNodeLabel(key))
|
|
48
|
+
.replace("{1}", finalLabel);
|
|
49
|
+
mermaidGraph += `\t${nodeLabel}\n`;
|
|
48
50
|
}
|
|
49
51
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
// Group edges by their common prefixes
|
|
53
|
+
const edgeGroups = {};
|
|
52
54
|
for (const edge of edges) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (subgraph !== "" &&
|
|
61
|
-
(subgraph !== sourcePrefix || subgraph !== targetPrefix)) {
|
|
62
|
-
mermaidGraph += "\tend\n";
|
|
63
|
-
subgraph = "";
|
|
55
|
+
const srcParts = edge.source.split(":");
|
|
56
|
+
const tgtParts = edge.target.split(":");
|
|
57
|
+
const commonPrefix = srcParts
|
|
58
|
+
.filter((src, i) => src === tgtParts[i])
|
|
59
|
+
.join(":");
|
|
60
|
+
if (!edgeGroups[commonPrefix]) {
|
|
61
|
+
edgeGroups[commonPrefix] = [];
|
|
64
62
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
let edgeData = edge.data;
|
|
77
|
-
const words = edgeData.split(" ");
|
|
78
|
-
// Group words into chunks of wrapLabelNWords size
|
|
79
|
-
if (words.length > wrapLabelNWords) {
|
|
80
|
-
edgeData = words
|
|
81
|
-
.reduce((acc, word, i) => {
|
|
82
|
-
if (i % wrapLabelNWords === 0)
|
|
83
|
-
acc.push("");
|
|
84
|
-
acc[acc.length - 1] += ` ${word}`;
|
|
85
|
-
return acc;
|
|
86
|
-
}, [])
|
|
87
|
-
.join("<br>");
|
|
63
|
+
edgeGroups[commonPrefix].push(edge);
|
|
64
|
+
}
|
|
65
|
+
const seenSubgraphs = new Set();
|
|
66
|
+
function addSubgraph(edges, prefix) {
|
|
67
|
+
const selfLoop = edges.length === 1 && edges[0].source === edges[0].target;
|
|
68
|
+
if (prefix && !selfLoop) {
|
|
69
|
+
const subgraph = prefix.split(":").pop();
|
|
70
|
+
if (seenSubgraphs.has(subgraph)) {
|
|
71
|
+
throw new Error(`Found duplicate subgraph '${subgraph}' -- this likely means that ` +
|
|
72
|
+
"you're reusing a subgraph node with the same name. " +
|
|
73
|
+
"Please adjust your graph to have subgraph nodes with unique names.");
|
|
88
74
|
}
|
|
89
|
-
|
|
90
|
-
|
|
75
|
+
seenSubgraphs.add(subgraph);
|
|
76
|
+
mermaidGraph += `\tsubgraph ${subgraph}\n`;
|
|
77
|
+
}
|
|
78
|
+
for (const edge of edges) {
|
|
79
|
+
const { source, target, data, conditional } = edge;
|
|
80
|
+
let edgeLabel = "";
|
|
81
|
+
if (data !== undefined) {
|
|
82
|
+
let edgeData = data;
|
|
83
|
+
const words = edgeData.split(" ");
|
|
84
|
+
if (words.length > wrapLabelNWords) {
|
|
85
|
+
edgeData = Array.from({ length: Math.ceil(words.length / wrapLabelNWords) }, (_, i) => words
|
|
86
|
+
.slice(i * wrapLabelNWords, (i + 1) * wrapLabelNWords)
|
|
87
|
+
.join(" ")).join(" <br> ");
|
|
88
|
+
}
|
|
89
|
+
edgeLabel = conditional
|
|
90
|
+
? ` -. ${edgeData} .-> `
|
|
91
|
+
: ` -- ${edgeData} --> `;
|
|
91
92
|
}
|
|
92
93
|
else {
|
|
93
|
-
edgeLabel =
|
|
94
|
+
edgeLabel = conditional ? " -.-> " : " --> ";
|
|
94
95
|
}
|
|
96
|
+
mermaidGraph += `\t${_escapeNodeLabel(source)}${edgeLabel}${_escapeNodeLabel(target)};\n`;
|
|
95
97
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
else {
|
|
101
|
-
edgeLabel = ` --> `;
|
|
98
|
+
// Recursively add nested subgraphs
|
|
99
|
+
for (const nestedPrefix in edgeGroups) {
|
|
100
|
+
if (nestedPrefix.startsWith(`${prefix}:`) && nestedPrefix !== prefix) {
|
|
101
|
+
addSubgraph(edgeGroups[nestedPrefix], nestedPrefix);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
if (prefix && !selfLoop) {
|
|
105
|
+
mermaidGraph += "\tend\n";
|
|
106
|
+
}
|
|
105
107
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
// Start with the top-level edges (no common prefix)
|
|
109
|
+
addSubgraph(edgeGroups[""] ?? [], "");
|
|
110
|
+
// Add remaining subgraphs
|
|
111
|
+
for (const prefix in edgeGroups) {
|
|
112
|
+
if (!prefix.includes(":") && prefix !== "") {
|
|
113
|
+
addSubgraph(edgeGroups[prefix], prefix);
|
|
114
|
+
}
|
|
108
115
|
}
|
|
109
116
|
// Add custom styles for nodes
|
|
110
|
-
if (withStyles
|
|
111
|
-
mermaidGraph += _generateMermaidGraphStyles(nodeColors);
|
|
117
|
+
if (withStyles) {
|
|
118
|
+
mermaidGraph += _generateMermaidGraphStyles(nodeColors ?? {});
|
|
112
119
|
}
|
|
113
120
|
return mermaidGraph;
|
|
114
121
|
}
|