@langchain/core 0.3.3 → 0.3.5-rc.0
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/language_models/chat_models.cjs +26 -1
- package/dist/language_models/chat_models.d.ts +2 -2
- package/dist/language_models/chat_models.js +27 -2
- package/dist/prompts/prompt.d.ts +1 -1
- package/dist/prompts/template.cjs +13 -8
- package/dist/prompts/template.js +13 -8
- package/dist/runnables/graph.cjs +67 -30
- package/dist/runnables/graph.d.ts +10 -1
- package/dist/runnables/graph.js +67 -30
- 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
|
@@ -88,6 +88,8 @@ class BaseChatModel extends base_js_1.BaseLanguageModel {
|
|
|
88
88
|
};
|
|
89
89
|
const runManagers = await callbackManager_?.handleChatModelStart(this.toJSON(), [messages], runnableConfig.runId, undefined, extra, undefined, undefined, runnableConfig.runName);
|
|
90
90
|
let generationChunk;
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
let llmOutput;
|
|
91
93
|
try {
|
|
92
94
|
for await (const chunk of this._streamResponseChunks(messages, callOptions, runManagers?.[0])) {
|
|
93
95
|
if (chunk.message.id == null) {
|
|
@@ -106,6 +108,16 @@ class BaseChatModel extends base_js_1.BaseLanguageModel {
|
|
|
106
108
|
else {
|
|
107
109
|
generationChunk = generationChunk.concat(chunk);
|
|
108
110
|
}
|
|
111
|
+
if ((0, index_js_1.isAIMessageChunk)(chunk.message) &&
|
|
112
|
+
chunk.message.usage_metadata !== undefined) {
|
|
113
|
+
llmOutput = {
|
|
114
|
+
tokenUsage: {
|
|
115
|
+
promptTokens: chunk.message.usage_metadata.input_tokens,
|
|
116
|
+
completionTokens: chunk.message.usage_metadata.output_tokens,
|
|
117
|
+
totalTokens: chunk.message.usage_metadata.total_tokens,
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
109
121
|
}
|
|
110
122
|
}
|
|
111
123
|
catch (err) {
|
|
@@ -115,6 +127,7 @@ class BaseChatModel extends base_js_1.BaseLanguageModel {
|
|
|
115
127
|
await Promise.all((runManagers ?? []).map((runManager) => runManager?.handleLLMEnd({
|
|
116
128
|
// TODO: Remove cast after figuring out inheritance
|
|
117
129
|
generations: [[generationChunk]],
|
|
130
|
+
llmOutput,
|
|
118
131
|
})));
|
|
119
132
|
}
|
|
120
133
|
}
|
|
@@ -154,6 +167,8 @@ class BaseChatModel extends base_js_1.BaseLanguageModel {
|
|
|
154
167
|
try {
|
|
155
168
|
const stream = await this._streamResponseChunks(baseMessages[0], parsedOptions, runManagers?.[0]);
|
|
156
169
|
let aggregated;
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
171
|
+
let llmOutput;
|
|
157
172
|
for await (const chunk of stream) {
|
|
158
173
|
if (chunk.message.id == null) {
|
|
159
174
|
const runId = runManagers?.at(0)?.runId;
|
|
@@ -166,6 +181,16 @@ class BaseChatModel extends base_js_1.BaseLanguageModel {
|
|
|
166
181
|
else {
|
|
167
182
|
aggregated = (0, stream_js_1.concat)(aggregated, chunk);
|
|
168
183
|
}
|
|
184
|
+
if ((0, index_js_1.isAIMessageChunk)(chunk.message) &&
|
|
185
|
+
chunk.message.usage_metadata !== undefined) {
|
|
186
|
+
llmOutput = {
|
|
187
|
+
tokenUsage: {
|
|
188
|
+
promptTokens: chunk.message.usage_metadata.input_tokens,
|
|
189
|
+
completionTokens: chunk.message.usage_metadata.output_tokens,
|
|
190
|
+
totalTokens: chunk.message.usage_metadata.total_tokens,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
169
194
|
}
|
|
170
195
|
if (aggregated === undefined) {
|
|
171
196
|
throw new Error("Received empty response from chat model call.");
|
|
@@ -173,7 +198,7 @@ class BaseChatModel extends base_js_1.BaseLanguageModel {
|
|
|
173
198
|
generations.push([aggregated]);
|
|
174
199
|
await runManagers?.[0].handleLLMEnd({
|
|
175
200
|
generations,
|
|
176
|
-
llmOutput
|
|
201
|
+
llmOutput,
|
|
177
202
|
});
|
|
178
203
|
}
|
|
179
204
|
catch (e) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { type BaseMessage, BaseMessageChunk, type BaseMessageLike } from "../messages/index.js";
|
|
2
|
+
import { type BaseMessage, BaseMessageChunk, type BaseMessageLike, AIMessageChunk } from "../messages/index.js";
|
|
3
3
|
import type { BasePromptValueInterface } from "../prompt_values.js";
|
|
4
4
|
import { LLMResult, ChatGenerationChunk, type ChatResult, type Generation } from "../outputs.js";
|
|
5
5
|
import { BaseLanguageModel, type StructuredOutputMethodOptions, type ToolDefinition, type BaseLanguageModelCallOptions, type BaseLanguageModelInput, type BaseLanguageModelParams } from "./base.js";
|
|
@@ -66,7 +66,7 @@ export type BindToolsInput = StructuredToolInterface | Record<string, any> | Too
|
|
|
66
66
|
* Base class for chat models. It extends the BaseLanguageModel class and
|
|
67
67
|
* provides methods for generating chat based on input messages.
|
|
68
68
|
*/
|
|
69
|
-
export declare abstract class BaseChatModel<CallOptions extends BaseChatModelCallOptions = BaseChatModelCallOptions, OutputMessageType extends BaseMessageChunk =
|
|
69
|
+
export declare abstract class BaseChatModel<CallOptions extends BaseChatModelCallOptions = BaseChatModelCallOptions, OutputMessageType extends BaseMessageChunk = AIMessageChunk> extends BaseLanguageModel<OutputMessageType, CallOptions> {
|
|
70
70
|
ParsedCallOptions: Omit<CallOptions, Exclude<keyof RunnableConfig, "signal" | "timeout" | "maxConcurrency">>;
|
|
71
71
|
lc_namespace: string[];
|
|
72
72
|
constructor(fields: BaseChatModelParams);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
2
|
-
import { AIMessage, HumanMessage, coerceMessageLikeToMessage, } from "../messages/index.js";
|
|
2
|
+
import { AIMessage, HumanMessage, coerceMessageLikeToMessage, isAIMessageChunk, } from "../messages/index.js";
|
|
3
3
|
import { RUN_KEY, } from "../outputs.js";
|
|
4
4
|
import { BaseLanguageModel, } from "./base.js";
|
|
5
5
|
import { CallbackManager, } from "../callbacks/manager.js";
|
|
@@ -84,6 +84,8 @@ export class BaseChatModel extends BaseLanguageModel {
|
|
|
84
84
|
};
|
|
85
85
|
const runManagers = await callbackManager_?.handleChatModelStart(this.toJSON(), [messages], runnableConfig.runId, undefined, extra, undefined, undefined, runnableConfig.runName);
|
|
86
86
|
let generationChunk;
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
+
let llmOutput;
|
|
87
89
|
try {
|
|
88
90
|
for await (const chunk of this._streamResponseChunks(messages, callOptions, runManagers?.[0])) {
|
|
89
91
|
if (chunk.message.id == null) {
|
|
@@ -102,6 +104,16 @@ export class BaseChatModel extends BaseLanguageModel {
|
|
|
102
104
|
else {
|
|
103
105
|
generationChunk = generationChunk.concat(chunk);
|
|
104
106
|
}
|
|
107
|
+
if (isAIMessageChunk(chunk.message) &&
|
|
108
|
+
chunk.message.usage_metadata !== undefined) {
|
|
109
|
+
llmOutput = {
|
|
110
|
+
tokenUsage: {
|
|
111
|
+
promptTokens: chunk.message.usage_metadata.input_tokens,
|
|
112
|
+
completionTokens: chunk.message.usage_metadata.output_tokens,
|
|
113
|
+
totalTokens: chunk.message.usage_metadata.total_tokens,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
105
117
|
}
|
|
106
118
|
}
|
|
107
119
|
catch (err) {
|
|
@@ -111,6 +123,7 @@ export class BaseChatModel extends BaseLanguageModel {
|
|
|
111
123
|
await Promise.all((runManagers ?? []).map((runManager) => runManager?.handleLLMEnd({
|
|
112
124
|
// TODO: Remove cast after figuring out inheritance
|
|
113
125
|
generations: [[generationChunk]],
|
|
126
|
+
llmOutput,
|
|
114
127
|
})));
|
|
115
128
|
}
|
|
116
129
|
}
|
|
@@ -150,6 +163,8 @@ export class BaseChatModel extends BaseLanguageModel {
|
|
|
150
163
|
try {
|
|
151
164
|
const stream = await this._streamResponseChunks(baseMessages[0], parsedOptions, runManagers?.[0]);
|
|
152
165
|
let aggregated;
|
|
166
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
167
|
+
let llmOutput;
|
|
153
168
|
for await (const chunk of stream) {
|
|
154
169
|
if (chunk.message.id == null) {
|
|
155
170
|
const runId = runManagers?.at(0)?.runId;
|
|
@@ -162,6 +177,16 @@ export class BaseChatModel extends BaseLanguageModel {
|
|
|
162
177
|
else {
|
|
163
178
|
aggregated = concat(aggregated, chunk);
|
|
164
179
|
}
|
|
180
|
+
if (isAIMessageChunk(chunk.message) &&
|
|
181
|
+
chunk.message.usage_metadata !== undefined) {
|
|
182
|
+
llmOutput = {
|
|
183
|
+
tokenUsage: {
|
|
184
|
+
promptTokens: chunk.message.usage_metadata.input_tokens,
|
|
185
|
+
completionTokens: chunk.message.usage_metadata.output_tokens,
|
|
186
|
+
totalTokens: chunk.message.usage_metadata.total_tokens,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
165
190
|
}
|
|
166
191
|
if (aggregated === undefined) {
|
|
167
192
|
throw new Error("Received empty response from chat model call.");
|
|
@@ -169,7 +194,7 @@ export class BaseChatModel extends BaseLanguageModel {
|
|
|
169
194
|
generations.push([aggregated]);
|
|
170
195
|
await runManagers?.[0].handleLLMEnd({
|
|
171
196
|
generations,
|
|
172
|
-
llmOutput
|
|
197
|
+
llmOutput,
|
|
173
198
|
});
|
|
174
199
|
}
|
|
175
200
|
catch (e) {
|
package/dist/prompts/prompt.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ type NonAlphanumeric = " " | "\t" | "\n" | "\r" | '"' | "'" | "{" | "[" | "(" |
|
|
|
38
38
|
*/
|
|
39
39
|
type ExtractTemplateParamsRecursive<T extends string, Result extends string[] = []> = T extends `${string}{${infer Param}}${infer Rest}` ? Param extends `${NonAlphanumeric}${string}` ? ExtractTemplateParamsRecursive<Rest, Result> : ExtractTemplateParamsRecursive<Rest, [...Result, Param]> : Result;
|
|
40
40
|
export type ParamsFromFString<T extends string> = {
|
|
41
|
-
[Key in ExtractTemplateParamsRecursive<T>[number] | (string & Record<never, never>)]:
|
|
41
|
+
[Key in ExtractTemplateParamsRecursive<T>[number] | (string & Record<never, never>)]: any;
|
|
42
42
|
};
|
|
43
43
|
export type ExtractedFStringParams<T extends string, RunInput extends InputValues = Symbol> = RunInput extends Symbol ? ParamsFromFString<T> : RunInput;
|
|
44
44
|
/**
|
|
@@ -86,15 +86,20 @@ const parseMustache = (template) => {
|
|
|
86
86
|
return mustacheTemplateToNodes(parsed);
|
|
87
87
|
};
|
|
88
88
|
exports.parseMustache = parseMustache;
|
|
89
|
-
const interpolateFString = (template, values) =>
|
|
90
|
-
|
|
91
|
-
if (node.
|
|
92
|
-
|
|
89
|
+
const interpolateFString = (template, values) => {
|
|
90
|
+
return (0, exports.parseFString)(template).reduce((res, node) => {
|
|
91
|
+
if (node.type === "variable") {
|
|
92
|
+
if (node.name in values) {
|
|
93
|
+
const stringValue = typeof values[node.name] === "string"
|
|
94
|
+
? values[node.name]
|
|
95
|
+
: JSON.stringify(values[node.name]);
|
|
96
|
+
return res + stringValue;
|
|
97
|
+
}
|
|
98
|
+
throw new Error(`(f-string) Missing value for input ${node.name}`);
|
|
93
99
|
}
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
}, "");
|
|
100
|
+
return res + node.text;
|
|
101
|
+
}, "");
|
|
102
|
+
};
|
|
98
103
|
exports.interpolateFString = interpolateFString;
|
|
99
104
|
const interpolateMustache = (template, values) => {
|
|
100
105
|
configureMustache();
|
package/dist/prompts/template.js
CHANGED
|
@@ -78,15 +78,20 @@ export const parseMustache = (template) => {
|
|
|
78
78
|
const parsed = mustache.parse(template);
|
|
79
79
|
return mustacheTemplateToNodes(parsed);
|
|
80
80
|
};
|
|
81
|
-
export const interpolateFString = (template, values) =>
|
|
82
|
-
|
|
83
|
-
if (node.
|
|
84
|
-
|
|
81
|
+
export const interpolateFString = (template, values) => {
|
|
82
|
+
return parseFString(template).reduce((res, node) => {
|
|
83
|
+
if (node.type === "variable") {
|
|
84
|
+
if (node.name in values) {
|
|
85
|
+
const stringValue = typeof values[node.name] === "string"
|
|
86
|
+
? values[node.name]
|
|
87
|
+
: JSON.stringify(values[node.name]);
|
|
88
|
+
return res + stringValue;
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`(f-string) Missing value for input ${node.name}`);
|
|
85
91
|
}
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
}, "");
|
|
92
|
+
return res + node.text;
|
|
93
|
+
}, "");
|
|
94
|
+
};
|
|
90
95
|
export const interpolateMustache = (template, values) => {
|
|
91
96
|
configureMustache();
|
|
92
97
|
return mustache.render(template, values);
|
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
|
}
|
|
@@ -188,19 +195,49 @@ class Graph {
|
|
|
188
195
|
}
|
|
189
196
|
}
|
|
190
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* Return a new graph with all nodes re-identified,
|
|
200
|
+
* using their unique, readable names where possible.
|
|
201
|
+
*/
|
|
202
|
+
reid() {
|
|
203
|
+
const nodeLabels = Object.fromEntries(Object.values(this.nodes).map((node) => [node.id, node.name]));
|
|
204
|
+
const nodeLabelCounts = new Map();
|
|
205
|
+
Object.values(nodeLabels).forEach((label) => {
|
|
206
|
+
nodeLabelCounts.set(label, (nodeLabelCounts.get(label) || 0) + 1);
|
|
207
|
+
});
|
|
208
|
+
const getNodeId = (nodeId) => {
|
|
209
|
+
const label = nodeLabels[nodeId];
|
|
210
|
+
if ((0, uuid_1.validate)(nodeId) && nodeLabelCounts.get(label) === 1) {
|
|
211
|
+
return label;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
return nodeId;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
return new Graph({
|
|
218
|
+
nodes: Object.fromEntries(Object.entries(this.nodes).map(([id, node]) => [
|
|
219
|
+
getNodeId(id),
|
|
220
|
+
{ ...node, id: getNodeId(id) },
|
|
221
|
+
])),
|
|
222
|
+
edges: this.edges.map((edge) => ({
|
|
223
|
+
...edge,
|
|
224
|
+
source: getNodeId(edge.source),
|
|
225
|
+
target: getNodeId(edge.target),
|
|
226
|
+
})),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
191
229
|
drawMermaid(params) {
|
|
192
|
-
const { withStyles, curveStyle, nodeColors = {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const lastNode =
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
lastNodeLabel,
|
|
230
|
+
const { withStyles, curveStyle, nodeColors = {
|
|
231
|
+
default: "fill:#f2f0ff,line-height:1.2",
|
|
232
|
+
first: "fill-opacity:0",
|
|
233
|
+
last: "fill:#bfb6fc",
|
|
234
|
+
}, wrapLabelNWords, } = params ?? {};
|
|
235
|
+
const graph = this.reid();
|
|
236
|
+
const firstNode = graph.firstNode();
|
|
237
|
+
const lastNode = graph.lastNode();
|
|
238
|
+
return (0, graph_mermaid_js_1.drawMermaid)(graph.nodes, graph.edges, {
|
|
239
|
+
firstNode: firstNode?.id,
|
|
240
|
+
lastNode: lastNode?.id,
|
|
204
241
|
withStyles,
|
|
205
242
|
curveStyle,
|
|
206
243
|
nodeColors,
|
|
@@ -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
|
}
|
|
@@ -185,19 +192,49 @@ export class Graph {
|
|
|
185
192
|
}
|
|
186
193
|
}
|
|
187
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Return a new graph with all nodes re-identified,
|
|
197
|
+
* using their unique, readable names where possible.
|
|
198
|
+
*/
|
|
199
|
+
reid() {
|
|
200
|
+
const nodeLabels = Object.fromEntries(Object.values(this.nodes).map((node) => [node.id, node.name]));
|
|
201
|
+
const nodeLabelCounts = new Map();
|
|
202
|
+
Object.values(nodeLabels).forEach((label) => {
|
|
203
|
+
nodeLabelCounts.set(label, (nodeLabelCounts.get(label) || 0) + 1);
|
|
204
|
+
});
|
|
205
|
+
const getNodeId = (nodeId) => {
|
|
206
|
+
const label = nodeLabels[nodeId];
|
|
207
|
+
if (isUuid(nodeId) && nodeLabelCounts.get(label) === 1) {
|
|
208
|
+
return label;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
return nodeId;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
return new Graph({
|
|
215
|
+
nodes: Object.fromEntries(Object.entries(this.nodes).map(([id, node]) => [
|
|
216
|
+
getNodeId(id),
|
|
217
|
+
{ ...node, id: getNodeId(id) },
|
|
218
|
+
])),
|
|
219
|
+
edges: this.edges.map((edge) => ({
|
|
220
|
+
...edge,
|
|
221
|
+
source: getNodeId(edge.source),
|
|
222
|
+
target: getNodeId(edge.target),
|
|
223
|
+
})),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
188
226
|
drawMermaid(params) {
|
|
189
|
-
const { withStyles, curveStyle, nodeColors = {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
const lastNode =
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
lastNodeLabel,
|
|
227
|
+
const { withStyles, curveStyle, nodeColors = {
|
|
228
|
+
default: "fill:#f2f0ff,line-height:1.2",
|
|
229
|
+
first: "fill-opacity:0",
|
|
230
|
+
last: "fill:#bfb6fc",
|
|
231
|
+
}, wrapLabelNWords, } = params ?? {};
|
|
232
|
+
const graph = this.reid();
|
|
233
|
+
const firstNode = graph.firstNode();
|
|
234
|
+
const lastNode = graph.lastNode();
|
|
235
|
+
return drawMermaid(graph.nodes, graph.edges, {
|
|
236
|
+
firstNode: firstNode?.id,
|
|
237
|
+
lastNode: lastNode?.id,
|
|
201
238
|
withStyles,
|
|
202
239
|
curveStyle,
|
|
203
240
|
nodeColors,
|
|
@@ -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 (node.metadata) {
|
|
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 = String(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 (node.metadata) {
|
|
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 = String(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
|
}
|