@langchain/core 0.2.11 → 0.2.13-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/README.md +1 -1
- package/dist/language_models/tests/chat_models.test.js +17 -0
- package/dist/messages/base.d.ts +3 -1
- package/dist/messages/utils.cjs +19 -10
- package/dist/messages/utils.js +19 -10
- package/dist/prompts/chat.cjs +20 -16
- package/dist/prompts/chat.d.ts +0 -1
- package/dist/prompts/chat.js +20 -16
- package/dist/prompts/tests/chat.test.js +51 -1
- package/dist/runnables/graph.cjs +30 -8
- package/dist/runnables/graph.d.ts +14 -11
- package/dist/runnables/graph.js +30 -8
- package/dist/runnables/graph_mermaid.cjs +145 -0
- package/dist/runnables/graph_mermaid.d.ts +18 -0
- package/dist/runnables/graph_mermaid.js +140 -0
- package/dist/runnables/tests/runnable_graph.test.js +16 -0
- package/dist/runnables/types.d.ts +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Rather than having to write multiple implementations for all of those, LCEL allo
|
|
|
85
85
|
|
|
86
86
|
For more check out the [LCEL docs](https://js.langchain.com/v0.2/docs/concepts#langchain-expression-language).
|
|
87
87
|
|
|
88
|
-

|
|
89
89
|
|
|
90
90
|
## 📕 Releases & Versioning
|
|
91
91
|
|
|
@@ -3,6 +3,23 @@ import { test } from "@jest/globals";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
5
5
|
import { FakeChatModel, FakeListChatModel } from "../../utils/testing/index.js";
|
|
6
|
+
test("Test ChatModel accepts array shorthand for messages", async () => {
|
|
7
|
+
const model = new FakeChatModel({});
|
|
8
|
+
const response = await model.invoke([["human", "Hello there!"]]);
|
|
9
|
+
expect(response.content).toEqual("Hello there!");
|
|
10
|
+
});
|
|
11
|
+
test("Test ChatModel accepts object shorthand for messages", async () => {
|
|
12
|
+
const model = new FakeChatModel({});
|
|
13
|
+
const response = await model.invoke([
|
|
14
|
+
{
|
|
15
|
+
type: "human",
|
|
16
|
+
content: "Hello there!",
|
|
17
|
+
additional_kwargs: {},
|
|
18
|
+
example: true,
|
|
19
|
+
},
|
|
20
|
+
]);
|
|
21
|
+
expect(response.content).toEqual("Hello there!");
|
|
22
|
+
});
|
|
6
23
|
test("Test ChatModel uses callbacks", async () => {
|
|
7
24
|
const model = new FakeChatModel({});
|
|
8
25
|
let acc = "";
|
package/dist/messages/base.d.ts
CHANGED
|
@@ -140,7 +140,9 @@ export declare function _mergeLists(left?: any[], right?: any[]): any[] | undefi
|
|
|
140
140
|
export declare abstract class BaseMessageChunk extends BaseMessage {
|
|
141
141
|
abstract concat(chunk: BaseMessageChunk): BaseMessageChunk;
|
|
142
142
|
}
|
|
143
|
-
export type BaseMessageLike = BaseMessage |
|
|
143
|
+
export type BaseMessageLike = BaseMessage | ({
|
|
144
|
+
type: MessageType | "user" | "assistant" | "placeholder";
|
|
145
|
+
} & BaseMessageFields & Record<string, unknown>) | [
|
|
144
146
|
StringWithAutocomplete<MessageType | "user" | "assistant" | "placeholder">,
|
|
145
147
|
MessageContent
|
|
146
148
|
] | string;
|
package/dist/messages/utils.cjs
CHANGED
|
@@ -8,6 +8,21 @@ const function_js_1 = require("./function.cjs");
|
|
|
8
8
|
const human_js_1 = require("./human.cjs");
|
|
9
9
|
const system_js_1 = require("./system.cjs");
|
|
10
10
|
const tool_js_1 = require("./tool.cjs");
|
|
11
|
+
function _constructMessageFromParams(params) {
|
|
12
|
+
const { type, ...rest } = params;
|
|
13
|
+
if (type === "human" || type === "user") {
|
|
14
|
+
return new human_js_1.HumanMessage(rest);
|
|
15
|
+
}
|
|
16
|
+
else if (type === "ai" || type === "assistant") {
|
|
17
|
+
return new ai_js_1.AIMessage(rest);
|
|
18
|
+
}
|
|
19
|
+
else if (type === "system") {
|
|
20
|
+
return new system_js_1.SystemMessage(rest);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
throw new Error(`Unable to coerce message from array: only human, AI, or system message coercion is currently supported.`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
11
26
|
function coerceMessageLikeToMessage(messageLike) {
|
|
12
27
|
if (typeof messageLike === "string") {
|
|
13
28
|
return new human_js_1.HumanMessage(messageLike);
|
|
@@ -15,18 +30,12 @@ function coerceMessageLikeToMessage(messageLike) {
|
|
|
15
30
|
else if ((0, base_js_1.isBaseMessage)(messageLike)) {
|
|
16
31
|
return messageLike;
|
|
17
32
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
else if (type === "ai" || type === "assistant") {
|
|
23
|
-
return new ai_js_1.AIMessage({ content });
|
|
24
|
-
}
|
|
25
|
-
else if (type === "system") {
|
|
26
|
-
return new system_js_1.SystemMessage({ content });
|
|
33
|
+
if (Array.isArray(messageLike)) {
|
|
34
|
+
const [type, content] = messageLike;
|
|
35
|
+
return _constructMessageFromParams({ type, content });
|
|
27
36
|
}
|
|
28
37
|
else {
|
|
29
|
-
|
|
38
|
+
return _constructMessageFromParams(messageLike);
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
exports.coerceMessageLikeToMessage = coerceMessageLikeToMessage;
|
package/dist/messages/utils.js
CHANGED
|
@@ -5,6 +5,21 @@ import { FunctionMessage, FunctionMessageChunk, } from "./function.js";
|
|
|
5
5
|
import { HumanMessage, HumanMessageChunk } from "./human.js";
|
|
6
6
|
import { SystemMessage, SystemMessageChunk } from "./system.js";
|
|
7
7
|
import { ToolMessage } from "./tool.js";
|
|
8
|
+
function _constructMessageFromParams(params) {
|
|
9
|
+
const { type, ...rest } = params;
|
|
10
|
+
if (type === "human" || type === "user") {
|
|
11
|
+
return new HumanMessage(rest);
|
|
12
|
+
}
|
|
13
|
+
else if (type === "ai" || type === "assistant") {
|
|
14
|
+
return new AIMessage(rest);
|
|
15
|
+
}
|
|
16
|
+
else if (type === "system") {
|
|
17
|
+
return new SystemMessage(rest);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
throw new Error(`Unable to coerce message from array: only human, AI, or system message coercion is currently supported.`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
8
23
|
export function coerceMessageLikeToMessage(messageLike) {
|
|
9
24
|
if (typeof messageLike === "string") {
|
|
10
25
|
return new HumanMessage(messageLike);
|
|
@@ -12,18 +27,12 @@ export function coerceMessageLikeToMessage(messageLike) {
|
|
|
12
27
|
else if (isBaseMessage(messageLike)) {
|
|
13
28
|
return messageLike;
|
|
14
29
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
else if (type === "ai" || type === "assistant") {
|
|
20
|
-
return new AIMessage({ content });
|
|
21
|
-
}
|
|
22
|
-
else if (type === "system") {
|
|
23
|
-
return new SystemMessage({ content });
|
|
30
|
+
if (Array.isArray(messageLike)) {
|
|
31
|
+
const [type, content] = messageLike;
|
|
32
|
+
return _constructMessageFromParams({ type, content });
|
|
24
33
|
}
|
|
25
34
|
else {
|
|
26
|
-
|
|
35
|
+
return _constructMessageFromParams(messageLike);
|
|
27
36
|
}
|
|
28
37
|
}
|
|
29
38
|
/**
|
package/dist/prompts/chat.cjs
CHANGED
|
@@ -75,33 +75,37 @@ class MessagesPlaceholder extends BaseMessagePromptTemplate {
|
|
|
75
75
|
get inputVariables() {
|
|
76
76
|
return [this.variableName];
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
async formatMessages(values) {
|
|
79
|
+
const input = values[this.variableName];
|
|
79
80
|
if (this.optional && !input) {
|
|
80
|
-
return
|
|
81
|
+
return [];
|
|
81
82
|
}
|
|
82
83
|
else if (!input) {
|
|
83
|
-
const error = new Error(`
|
|
84
|
+
const error = new Error(`Field "${this.variableName}" in prompt uses a MessagesPlaceholder, which expects an array of BaseMessages as an input value. Received: undefined`);
|
|
84
85
|
error.name = "InputFormatError";
|
|
85
86
|
throw error;
|
|
86
87
|
}
|
|
87
|
-
let
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
let formattedMessages;
|
|
89
|
+
try {
|
|
90
|
+
if (Array.isArray(input)) {
|
|
91
|
+
formattedMessages = input.map(index_js_1.coerceMessageLikeToMessage);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
formattedMessages = [(0, index_js_1.coerceMessageLikeToMessage)(input)];
|
|
95
|
+
}
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
93
97
|
}
|
|
94
|
-
|
|
98
|
+
catch (e) {
|
|
95
99
|
const readableInput = typeof input === "string" ? input : JSON.stringify(input, null, 2);
|
|
96
|
-
const error = new Error(
|
|
100
|
+
const error = new Error([
|
|
101
|
+
`Field "${this.variableName}" in prompt uses a MessagesPlaceholder, which expects an array of BaseMessages or coerceable values as input.`,
|
|
102
|
+
`Received value: ${readableInput}`,
|
|
103
|
+
`Additional message: ${e.message}`,
|
|
104
|
+
].join("\n\n"));
|
|
97
105
|
error.name = "InputFormatError";
|
|
98
106
|
throw error;
|
|
99
107
|
}
|
|
100
|
-
return
|
|
101
|
-
}
|
|
102
|
-
async formatMessages(values) {
|
|
103
|
-
this.validateInputOrThrow(values[this.variableName], this.variableName);
|
|
104
|
-
return values[this.variableName] ?? [];
|
|
108
|
+
return formattedMessages;
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
exports.MessagesPlaceholder = MessagesPlaceholder;
|
package/dist/prompts/chat.d.ts
CHANGED
|
@@ -50,7 +50,6 @@ export declare class MessagesPlaceholder<RunInput extends InputValues = any> ext
|
|
|
50
50
|
constructor(variableName: Extract<keyof RunInput, string>);
|
|
51
51
|
constructor(fields: MessagesPlaceholderFields<Extract<keyof RunInput, string>>);
|
|
52
52
|
get inputVariables(): Extract<keyof RunInput, string>[];
|
|
53
|
-
validateInputOrThrow(input: Array<unknown> | undefined, variableName: Extract<keyof RunInput, string>): input is BaseMessage[];
|
|
54
53
|
formatMessages(values: TypedPromptInputValues<RunInput>): Promise<BaseMessage[]>;
|
|
55
54
|
}
|
|
56
55
|
/**
|
package/dist/prompts/chat.js
CHANGED
|
@@ -71,33 +71,37 @@ export class MessagesPlaceholder extends BaseMessagePromptTemplate {
|
|
|
71
71
|
get inputVariables() {
|
|
72
72
|
return [this.variableName];
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
async formatMessages(values) {
|
|
75
|
+
const input = values[this.variableName];
|
|
75
76
|
if (this.optional && !input) {
|
|
76
|
-
return
|
|
77
|
+
return [];
|
|
77
78
|
}
|
|
78
79
|
else if (!input) {
|
|
79
|
-
const error = new Error(`
|
|
80
|
+
const error = new Error(`Field "${this.variableName}" in prompt uses a MessagesPlaceholder, which expects an array of BaseMessages as an input value. Received: undefined`);
|
|
80
81
|
error.name = "InputFormatError";
|
|
81
82
|
throw error;
|
|
82
83
|
}
|
|
83
|
-
let
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
let formattedMessages;
|
|
85
|
+
try {
|
|
86
|
+
if (Array.isArray(input)) {
|
|
87
|
+
formattedMessages = input.map(coerceMessageLikeToMessage);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
formattedMessages = [coerceMessageLikeToMessage(input)];
|
|
91
|
+
}
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
93
|
}
|
|
90
|
-
|
|
94
|
+
catch (e) {
|
|
91
95
|
const readableInput = typeof input === "string" ? input : JSON.stringify(input, null, 2);
|
|
92
|
-
const error = new Error(
|
|
96
|
+
const error = new Error([
|
|
97
|
+
`Field "${this.variableName}" in prompt uses a MessagesPlaceholder, which expects an array of BaseMessages or coerceable values as input.`,
|
|
98
|
+
`Received value: ${readableInput}`,
|
|
99
|
+
`Additional message: ${e.message}`,
|
|
100
|
+
].join("\n\n"));
|
|
93
101
|
error.name = "InputFormatError";
|
|
94
102
|
throw error;
|
|
95
103
|
}
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
async formatMessages(values) {
|
|
99
|
-
this.validateInputOrThrow(values[this.variableName], this.variableName);
|
|
100
|
-
return values[this.variableName] ?? [];
|
|
104
|
+
return formattedMessages;
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
/**
|
|
@@ -258,7 +258,7 @@ test("Test MessagesPlaceholder not optional", async () => {
|
|
|
258
258
|
variableName: "foo",
|
|
259
259
|
});
|
|
260
260
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
261
|
-
await expect(prompt.formatMessages({})).rejects.toThrow('
|
|
261
|
+
await expect(prompt.formatMessages({})).rejects.toThrow('Field "foo" in prompt uses a MessagesPlaceholder, which expects an array of BaseMessages as an input value. Received: undefined');
|
|
262
262
|
});
|
|
263
263
|
test("Test MessagesPlaceholder shorthand in a chat prompt template should throw for invalid syntax", async () => {
|
|
264
264
|
expect(() => ChatPromptTemplate.fromMessages([["placeholder", "foo"]])).toThrow();
|
|
@@ -273,6 +273,56 @@ test("Test MessagesPlaceholder shorthand in a chat prompt template", async () =>
|
|
|
273
273
|
new AIMessage("how r u"),
|
|
274
274
|
]);
|
|
275
275
|
});
|
|
276
|
+
test("Test MessagesPlaceholder shorthand in a chat prompt template with object format", async () => {
|
|
277
|
+
const prompt = ChatPromptTemplate.fromMessages([["placeholder", "{foo}"]]);
|
|
278
|
+
const messages = await prompt.formatMessages({
|
|
279
|
+
foo: [
|
|
280
|
+
{
|
|
281
|
+
type: "system",
|
|
282
|
+
content: "some initial content",
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
type: "human",
|
|
286
|
+
content: [
|
|
287
|
+
{
|
|
288
|
+
text: "page: 1\ndescription: One Purchase Flow\ntimestamp: '2024-06-04T14:46:46.062Z'\ntype: navigate\nscreenshot_present: true\n",
|
|
289
|
+
type: "text",
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
text: "page: 3\ndescription: intent_str=buy,mode_str=redirect,screenName_str=order-completed,\ntimestamp: '2024-06-04T14:46:58.846Z'\ntype: Screen View\nscreenshot_present: false\n",
|
|
293
|
+
type: "text",
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
type: "assistant",
|
|
299
|
+
content: "some captivating response",
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
});
|
|
303
|
+
expect(messages).toEqual([
|
|
304
|
+
new SystemMessage("some initial content"),
|
|
305
|
+
new HumanMessage({
|
|
306
|
+
content: [
|
|
307
|
+
{
|
|
308
|
+
text: "page: 1\ndescription: One Purchase Flow\ntimestamp: '2024-06-04T14:46:46.062Z'\ntype: navigate\nscreenshot_present: true\n",
|
|
309
|
+
type: "text",
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
text: "page: 3\ndescription: intent_str=buy,mode_str=redirect,screenName_str=order-completed,\ntimestamp: '2024-06-04T14:46:58.846Z'\ntype: Screen View\nscreenshot_present: false\n",
|
|
313
|
+
type: "text",
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
}),
|
|
317
|
+
new AIMessage("some captivating response"),
|
|
318
|
+
]);
|
|
319
|
+
});
|
|
320
|
+
test("Test MessagesPlaceholder with invalid shorthand should throw", async () => {
|
|
321
|
+
const prompt = ChatPromptTemplate.fromMessages([["placeholder", "{foo}"]]);
|
|
322
|
+
await expect(() => prompt.formatMessages({
|
|
323
|
+
foo: [{ badFormatting: true }],
|
|
324
|
+
})).rejects.toThrow();
|
|
325
|
+
});
|
|
276
326
|
test("Test using partial", async () => {
|
|
277
327
|
const userPrompt = new PromptTemplate({
|
|
278
328
|
template: "{foo}{bar}",
|
package/dist/runnables/graph.cjs
CHANGED
|
@@ -4,6 +4,7 @@ exports.Graph = exports.nodeDataStr = void 0;
|
|
|
4
4
|
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
|
+
const graph_mermaid_js_1 = require("./graph_mermaid.cjs");
|
|
7
8
|
const MAX_DATA_DISPLAY_NAME_LENGTH = 42;
|
|
8
9
|
function nodeDataStr(node) {
|
|
9
10
|
if (!(0, uuid_1.validate)(node.id)) {
|
|
@@ -11,16 +12,12 @@ function nodeDataStr(node) {
|
|
|
11
12
|
}
|
|
12
13
|
else if ((0, utils_js_1.isRunnableInterface)(node.data)) {
|
|
13
14
|
try {
|
|
14
|
-
let data = node.data.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
data.split("\n").length > 1) {
|
|
18
|
-
data = node.data.getName();
|
|
19
|
-
}
|
|
20
|
-
else if (data.length > MAX_DATA_DISPLAY_NAME_LENGTH) {
|
|
15
|
+
let data = node.data.getName();
|
|
16
|
+
data = data.startsWith("Runnable") ? data.slice("Runnable".length) : data;
|
|
17
|
+
if (data.length > MAX_DATA_DISPLAY_NAME_LENGTH) {
|
|
21
18
|
data = `${data.substring(0, MAX_DATA_DISPLAY_NAME_LENGTH)}...`;
|
|
22
19
|
}
|
|
23
|
-
return data
|
|
20
|
+
return data;
|
|
24
21
|
}
|
|
25
22
|
catch (error) {
|
|
26
23
|
return node.data.getName();
|
|
@@ -160,5 +157,30 @@ class Graph {
|
|
|
160
157
|
}
|
|
161
158
|
}
|
|
162
159
|
}
|
|
160
|
+
drawMermaid(params) {
|
|
161
|
+
const { withStyles, curveStyle, nodeColors = { start: "#ffdfba", end: "#baffc9", other: "#fad7de" }, wrapLabelNWords, } = params ?? {};
|
|
162
|
+
const nodes = {};
|
|
163
|
+
for (const node of Object.values(this.nodes)) {
|
|
164
|
+
nodes[node.id] = nodeDataStr(node);
|
|
165
|
+
}
|
|
166
|
+
const firstNode = this.firstNode();
|
|
167
|
+
const firstNodeLabel = firstNode ? nodeDataStr(firstNode) : undefined;
|
|
168
|
+
const lastNode = this.lastNode();
|
|
169
|
+
const lastNodeLabel = lastNode ? nodeDataStr(lastNode) : undefined;
|
|
170
|
+
return (0, graph_mermaid_js_1.drawMermaid)(nodes, this.edges, {
|
|
171
|
+
firstNodeLabel,
|
|
172
|
+
lastNodeLabel,
|
|
173
|
+
withStyles,
|
|
174
|
+
curveStyle,
|
|
175
|
+
nodeColors,
|
|
176
|
+
wrapLabelNWords,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
async drawMermaidPng(params) {
|
|
180
|
+
const mermaidSyntax = this.drawMermaid(params);
|
|
181
|
+
return (0, graph_mermaid_js_1.drawMermaidPng)(mermaidSyntax, {
|
|
182
|
+
backgroundColor: params?.backgroundColor,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
163
185
|
}
|
|
164
186
|
exports.Graph = Graph;
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
import type { RunnableInterface, RunnableIOSchema } from "./types.js";
|
|
2
|
-
interface Edge {
|
|
3
|
-
source: string;
|
|
4
|
-
target: string;
|
|
5
|
-
data?: string;
|
|
6
|
-
}
|
|
7
|
-
interface Node {
|
|
8
|
-
id: string;
|
|
9
|
-
data: RunnableIOSchema | RunnableInterface;
|
|
10
|
-
}
|
|
1
|
+
import type { RunnableInterface, RunnableIOSchema, Node, Edge } from "./types.js";
|
|
11
2
|
export declare function nodeDataStr(node: Node): string;
|
|
12
3
|
export declare class Graph {
|
|
13
4
|
nodes: Record<string, Node>;
|
|
@@ -21,5 +12,17 @@ export declare class Graph {
|
|
|
21
12
|
extend(graph: Graph): void;
|
|
22
13
|
trimFirstNode(): void;
|
|
23
14
|
trimLastNode(): void;
|
|
15
|
+
drawMermaid(params?: {
|
|
16
|
+
withStyles?: boolean;
|
|
17
|
+
curveStyle?: string;
|
|
18
|
+
nodeColors?: Record<string, string>;
|
|
19
|
+
wrapLabelNWords?: number;
|
|
20
|
+
}): string;
|
|
21
|
+
drawMermaidPng(params?: {
|
|
22
|
+
withStyles?: boolean;
|
|
23
|
+
curveStyle?: string;
|
|
24
|
+
nodeColors?: Record<string, string>;
|
|
25
|
+
wrapLabelNWords?: number;
|
|
26
|
+
backgroundColor?: string;
|
|
27
|
+
}): Promise<Blob>;
|
|
24
28
|
}
|
|
25
|
-
export {};
|
package/dist/runnables/graph.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
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
|
+
import { drawMermaid, drawMermaidPng } from "./graph_mermaid.js";
|
|
4
5
|
const MAX_DATA_DISPLAY_NAME_LENGTH = 42;
|
|
5
6
|
export function nodeDataStr(node) {
|
|
6
7
|
if (!isUuid(node.id)) {
|
|
@@ -8,16 +9,12 @@ export function nodeDataStr(node) {
|
|
|
8
9
|
}
|
|
9
10
|
else if (isRunnableInterface(node.data)) {
|
|
10
11
|
try {
|
|
11
|
-
let data = node.data.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
data.split("\n").length > 1) {
|
|
15
|
-
data = node.data.getName();
|
|
16
|
-
}
|
|
17
|
-
else if (data.length > MAX_DATA_DISPLAY_NAME_LENGTH) {
|
|
12
|
+
let data = node.data.getName();
|
|
13
|
+
data = data.startsWith("Runnable") ? data.slice("Runnable".length) : data;
|
|
14
|
+
if (data.length > MAX_DATA_DISPLAY_NAME_LENGTH) {
|
|
18
15
|
data = `${data.substring(0, MAX_DATA_DISPLAY_NAME_LENGTH)}...`;
|
|
19
16
|
}
|
|
20
|
-
return data
|
|
17
|
+
return data;
|
|
21
18
|
}
|
|
22
19
|
catch (error) {
|
|
23
20
|
return node.data.getName();
|
|
@@ -156,4 +153,29 @@ export class Graph {
|
|
|
156
153
|
}
|
|
157
154
|
}
|
|
158
155
|
}
|
|
156
|
+
drawMermaid(params) {
|
|
157
|
+
const { withStyles, curveStyle, nodeColors = { start: "#ffdfba", end: "#baffc9", other: "#fad7de" }, wrapLabelNWords, } = params ?? {};
|
|
158
|
+
const nodes = {};
|
|
159
|
+
for (const node of Object.values(this.nodes)) {
|
|
160
|
+
nodes[node.id] = nodeDataStr(node);
|
|
161
|
+
}
|
|
162
|
+
const firstNode = this.firstNode();
|
|
163
|
+
const firstNodeLabel = firstNode ? nodeDataStr(firstNode) : undefined;
|
|
164
|
+
const lastNode = this.lastNode();
|
|
165
|
+
const lastNodeLabel = lastNode ? nodeDataStr(lastNode) : undefined;
|
|
166
|
+
return drawMermaid(nodes, this.edges, {
|
|
167
|
+
firstNodeLabel,
|
|
168
|
+
lastNodeLabel,
|
|
169
|
+
withStyles,
|
|
170
|
+
curveStyle,
|
|
171
|
+
nodeColors,
|
|
172
|
+
wrapLabelNWords,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async drawMermaidPng(params) {
|
|
176
|
+
const mermaidSyntax = this.drawMermaid(params);
|
|
177
|
+
return drawMermaidPng(mermaidSyntax, {
|
|
178
|
+
backgroundColor: params?.backgroundColor,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
159
181
|
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.drawMermaidPng = exports.drawMermaid = void 0;
|
|
4
|
+
function _escapeNodeLabel(nodeLabel) {
|
|
5
|
+
// Escapes the node label for Mermaid syntax.
|
|
6
|
+
return nodeLabel.replace(/[^a-zA-Z-_0-9]/g, "_");
|
|
7
|
+
}
|
|
8
|
+
// Adjusts Mermaid edge to map conditional nodes to pure nodes.
|
|
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
|
+
}
|
|
14
|
+
function _generateMermaidGraphStyles(nodeColors) {
|
|
15
|
+
let styles = "";
|
|
16
|
+
for (const [className, color] of Object.entries(nodeColors)) {
|
|
17
|
+
styles += `\tclassDef ${className}class fill:${color};\n`;
|
|
18
|
+
}
|
|
19
|
+
return styles;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Draws a Mermaid graph using the provided graph data
|
|
23
|
+
*/
|
|
24
|
+
function drawMermaid(nodes, edges, config) {
|
|
25
|
+
const { firstNodeLabel, lastNodeLabel, nodeColors, withStyles = true, curveStyle = "linear", wrapLabelNWords = 9, } = config ?? {};
|
|
26
|
+
// Initialize Mermaid graph configuration
|
|
27
|
+
let mermaidGraph = withStyles
|
|
28
|
+
? `%%{init: {'flowchart': {'curve': '${curveStyle}'}}}%%\ngraph TD;\n`
|
|
29
|
+
: "graph TD;\n";
|
|
30
|
+
if (withStyles) {
|
|
31
|
+
// Node formatting templates
|
|
32
|
+
const defaultClassLabel = "default";
|
|
33
|
+
const formatDict = {
|
|
34
|
+
[defaultClassLabel]: "{0}([{1}]):::otherclass",
|
|
35
|
+
};
|
|
36
|
+
if (firstNodeLabel !== undefined) {
|
|
37
|
+
formatDict[firstNodeLabel] = "{0}[{0}]:::startclass";
|
|
38
|
+
}
|
|
39
|
+
if (lastNodeLabel !== undefined) {
|
|
40
|
+
formatDict[lastNodeLabel] = "{0}[{0}]:::endclass";
|
|
41
|
+
}
|
|
42
|
+
// Add nodes to the graph
|
|
43
|
+
for (const node of Object.values(nodes)) {
|
|
44
|
+
const nodeLabel = formatDict[node] ?? formatDict[defaultClassLabel];
|
|
45
|
+
const escapedNodeLabel = _escapeNodeLabel(node);
|
|
46
|
+
const nodeParts = node.split(":");
|
|
47
|
+
const nodeSplit = nodeParts[nodeParts.length - 1];
|
|
48
|
+
mermaidGraph += `\t${nodeLabel
|
|
49
|
+
.replace(/\{0\}/g, escapedNodeLabel)
|
|
50
|
+
.replace(/\{1\}/g, nodeSplit)};\n`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
let subgraph = "";
|
|
54
|
+
// Add edges to the graph
|
|
55
|
+
for (const edge of edges) {
|
|
56
|
+
const sourcePrefix = edge.source.includes(":")
|
|
57
|
+
? edge.source.split(":")[0]
|
|
58
|
+
: undefined;
|
|
59
|
+
const targetPrefix = edge.target.includes(":")
|
|
60
|
+
? edge.target.split(":")[0]
|
|
61
|
+
: undefined;
|
|
62
|
+
// Exit subgraph if source or target is not in the same subgraph
|
|
63
|
+
if (subgraph !== "" &&
|
|
64
|
+
(subgraph !== sourcePrefix || subgraph !== targetPrefix)) {
|
|
65
|
+
mermaidGraph += "\tend\n";
|
|
66
|
+
subgraph = "";
|
|
67
|
+
}
|
|
68
|
+
// Enter subgraph if source and target are in the same subgraph
|
|
69
|
+
if (subgraph === "" &&
|
|
70
|
+
sourcePrefix !== undefined &&
|
|
71
|
+
sourcePrefix === targetPrefix) {
|
|
72
|
+
mermaidGraph = `\tsubgraph ${sourcePrefix}\n`;
|
|
73
|
+
subgraph = sourcePrefix;
|
|
74
|
+
}
|
|
75
|
+
const [source, target] = _adjustMermaidEdge(edge, nodes);
|
|
76
|
+
let edgeLabel = "";
|
|
77
|
+
// Add BR every wrapLabelNWords words
|
|
78
|
+
if (edge.data !== undefined) {
|
|
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>");
|
|
91
|
+
if (edge.conditional) {
|
|
92
|
+
edgeLabel = ` -. ${edgeData} .-> `;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
edgeLabel = ` -- ${edgeData} --> `;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
if (edge.conditional) {
|
|
101
|
+
edgeLabel = ` -.-> `;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
edgeLabel = ` --> `;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
mermaidGraph += `\t${_escapeNodeLabel(source)}${edgeLabel}${_escapeNodeLabel(target)};\n`;
|
|
108
|
+
}
|
|
109
|
+
if (subgraph !== "") {
|
|
110
|
+
mermaidGraph += "end\n";
|
|
111
|
+
}
|
|
112
|
+
// Add custom styles for nodes
|
|
113
|
+
if (withStyles && nodeColors !== undefined) {
|
|
114
|
+
mermaidGraph += _generateMermaidGraphStyles(nodeColors);
|
|
115
|
+
}
|
|
116
|
+
return mermaidGraph;
|
|
117
|
+
}
|
|
118
|
+
exports.drawMermaid = drawMermaid;
|
|
119
|
+
/**
|
|
120
|
+
* Renders Mermaid graph using the Mermaid.INK API.
|
|
121
|
+
*/
|
|
122
|
+
async function drawMermaidPng(mermaidSyntax, config) {
|
|
123
|
+
let { backgroundColor = "white" } = config ?? {};
|
|
124
|
+
// Use btoa for compatibility, assume ASCII
|
|
125
|
+
const mermaidSyntaxEncoded = btoa(mermaidSyntax);
|
|
126
|
+
// Check if the background color is a hexadecimal color code using regex
|
|
127
|
+
if (backgroundColor !== undefined) {
|
|
128
|
+
const hexColorPattern = /^#(?:[0-9a-fA-F]{3}){1,2}$/;
|
|
129
|
+
if (!hexColorPattern.test(backgroundColor)) {
|
|
130
|
+
backgroundColor = `!${backgroundColor}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const imageUrl = `https://mermaid.ink/img/${mermaidSyntaxEncoded}?bgColor=${backgroundColor}`;
|
|
134
|
+
const res = await fetch(imageUrl);
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
throw new Error([
|
|
137
|
+
`Failed to render the graph using the Mermaid.INK API.`,
|
|
138
|
+
`Status code: ${res.status}`,
|
|
139
|
+
`Status text: ${res.statusText}`,
|
|
140
|
+
].join("\n"));
|
|
141
|
+
}
|
|
142
|
+
const content = await res.blob();
|
|
143
|
+
return content;
|
|
144
|
+
}
|
|
145
|
+
exports.drawMermaidPng = drawMermaidPng;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Edge } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Draws a Mermaid graph using the provided graph data
|
|
4
|
+
*/
|
|
5
|
+
export declare function drawMermaid(nodes: Record<string, string>, edges: Edge[], config?: {
|
|
6
|
+
firstNodeLabel?: string;
|
|
7
|
+
lastNodeLabel?: string;
|
|
8
|
+
curveStyle?: string;
|
|
9
|
+
withStyles?: boolean;
|
|
10
|
+
nodeColors?: Record<string, string>;
|
|
11
|
+
wrapLabelNWords?: number;
|
|
12
|
+
}): string;
|
|
13
|
+
/**
|
|
14
|
+
* Renders Mermaid graph using the Mermaid.INK API.
|
|
15
|
+
*/
|
|
16
|
+
export declare function drawMermaidPng(mermaidSyntax: string, config?: {
|
|
17
|
+
backgroundColor?: string;
|
|
18
|
+
}): Promise<Blob>;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
function _escapeNodeLabel(nodeLabel) {
|
|
2
|
+
// Escapes the node label for Mermaid syntax.
|
|
3
|
+
return nodeLabel.replace(/[^a-zA-Z-_0-9]/g, "_");
|
|
4
|
+
}
|
|
5
|
+
// Adjusts Mermaid edge to map conditional nodes to pure nodes.
|
|
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
|
+
}
|
|
11
|
+
function _generateMermaidGraphStyles(nodeColors) {
|
|
12
|
+
let styles = "";
|
|
13
|
+
for (const [className, color] of Object.entries(nodeColors)) {
|
|
14
|
+
styles += `\tclassDef ${className}class fill:${color};\n`;
|
|
15
|
+
}
|
|
16
|
+
return styles;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Draws a Mermaid graph using the provided graph data
|
|
20
|
+
*/
|
|
21
|
+
export function drawMermaid(nodes, edges, config) {
|
|
22
|
+
const { firstNodeLabel, lastNodeLabel, nodeColors, withStyles = true, curveStyle = "linear", wrapLabelNWords = 9, } = config ?? {};
|
|
23
|
+
// Initialize Mermaid graph configuration
|
|
24
|
+
let mermaidGraph = withStyles
|
|
25
|
+
? `%%{init: {'flowchart': {'curve': '${curveStyle}'}}}%%\ngraph TD;\n`
|
|
26
|
+
: "graph TD;\n";
|
|
27
|
+
if (withStyles) {
|
|
28
|
+
// Node formatting templates
|
|
29
|
+
const defaultClassLabel = "default";
|
|
30
|
+
const formatDict = {
|
|
31
|
+
[defaultClassLabel]: "{0}([{1}]):::otherclass",
|
|
32
|
+
};
|
|
33
|
+
if (firstNodeLabel !== undefined) {
|
|
34
|
+
formatDict[firstNodeLabel] = "{0}[{0}]:::startclass";
|
|
35
|
+
}
|
|
36
|
+
if (lastNodeLabel !== undefined) {
|
|
37
|
+
formatDict[lastNodeLabel] = "{0}[{0}]:::endclass";
|
|
38
|
+
}
|
|
39
|
+
// Add nodes to the graph
|
|
40
|
+
for (const node of Object.values(nodes)) {
|
|
41
|
+
const nodeLabel = formatDict[node] ?? formatDict[defaultClassLabel];
|
|
42
|
+
const escapedNodeLabel = _escapeNodeLabel(node);
|
|
43
|
+
const nodeParts = node.split(":");
|
|
44
|
+
const nodeSplit = nodeParts[nodeParts.length - 1];
|
|
45
|
+
mermaidGraph += `\t${nodeLabel
|
|
46
|
+
.replace(/\{0\}/g, escapedNodeLabel)
|
|
47
|
+
.replace(/\{1\}/g, nodeSplit)};\n`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let subgraph = "";
|
|
51
|
+
// Add edges to the graph
|
|
52
|
+
for (const edge of edges) {
|
|
53
|
+
const sourcePrefix = edge.source.includes(":")
|
|
54
|
+
? edge.source.split(":")[0]
|
|
55
|
+
: undefined;
|
|
56
|
+
const targetPrefix = edge.target.includes(":")
|
|
57
|
+
? edge.target.split(":")[0]
|
|
58
|
+
: undefined;
|
|
59
|
+
// Exit subgraph if source or target is not in the same subgraph
|
|
60
|
+
if (subgraph !== "" &&
|
|
61
|
+
(subgraph !== sourcePrefix || subgraph !== targetPrefix)) {
|
|
62
|
+
mermaidGraph += "\tend\n";
|
|
63
|
+
subgraph = "";
|
|
64
|
+
}
|
|
65
|
+
// Enter subgraph if source and target are in the same subgraph
|
|
66
|
+
if (subgraph === "" &&
|
|
67
|
+
sourcePrefix !== undefined &&
|
|
68
|
+
sourcePrefix === targetPrefix) {
|
|
69
|
+
mermaidGraph = `\tsubgraph ${sourcePrefix}\n`;
|
|
70
|
+
subgraph = sourcePrefix;
|
|
71
|
+
}
|
|
72
|
+
const [source, target] = _adjustMermaidEdge(edge, nodes);
|
|
73
|
+
let edgeLabel = "";
|
|
74
|
+
// Add BR every wrapLabelNWords words
|
|
75
|
+
if (edge.data !== undefined) {
|
|
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>");
|
|
88
|
+
if (edge.conditional) {
|
|
89
|
+
edgeLabel = ` -. ${edgeData} .-> `;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
edgeLabel = ` -- ${edgeData} --> `;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
if (edge.conditional) {
|
|
98
|
+
edgeLabel = ` -.-> `;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
edgeLabel = ` --> `;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
mermaidGraph += `\t${_escapeNodeLabel(source)}${edgeLabel}${_escapeNodeLabel(target)};\n`;
|
|
105
|
+
}
|
|
106
|
+
if (subgraph !== "") {
|
|
107
|
+
mermaidGraph += "end\n";
|
|
108
|
+
}
|
|
109
|
+
// Add custom styles for nodes
|
|
110
|
+
if (withStyles && nodeColors !== undefined) {
|
|
111
|
+
mermaidGraph += _generateMermaidGraphStyles(nodeColors);
|
|
112
|
+
}
|
|
113
|
+
return mermaidGraph;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Renders Mermaid graph using the Mermaid.INK API.
|
|
117
|
+
*/
|
|
118
|
+
export async function drawMermaidPng(mermaidSyntax, config) {
|
|
119
|
+
let { backgroundColor = "white" } = config ?? {};
|
|
120
|
+
// Use btoa for compatibility, assume ASCII
|
|
121
|
+
const mermaidSyntaxEncoded = btoa(mermaidSyntax);
|
|
122
|
+
// Check if the background color is a hexadecimal color code using regex
|
|
123
|
+
if (backgroundColor !== undefined) {
|
|
124
|
+
const hexColorPattern = /^#(?:[0-9a-fA-F]{3}){1,2}$/;
|
|
125
|
+
if (!hexColorPattern.test(backgroundColor)) {
|
|
126
|
+
backgroundColor = `!${backgroundColor}`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const imageUrl = `https://mermaid.ink/img/${mermaidSyntaxEncoded}?bgColor=${backgroundColor}`;
|
|
130
|
+
const res = await fetch(imageUrl);
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
throw new Error([
|
|
133
|
+
`Failed to render the graph using the Mermaid.INK API.`,
|
|
134
|
+
`Status code: ${res.status}`,
|
|
135
|
+
`Status text: ${res.statusText}`,
|
|
136
|
+
].join("\n"));
|
|
137
|
+
}
|
|
138
|
+
const content = await res.blob();
|
|
139
|
+
return content;
|
|
140
|
+
}
|
|
@@ -81,4 +81,20 @@ test("Test graph sequence", async () => {
|
|
|
81
81
|
{ source: 2, target: 3 },
|
|
82
82
|
],
|
|
83
83
|
});
|
|
84
|
+
expect(graph.drawMermaid())
|
|
85
|
+
.toEqual(`%%{init: {'flowchart': {'curve': 'linear'}}}%%
|
|
86
|
+
graph TD;
|
|
87
|
+
\tPromptTemplateInput[PromptTemplateInput]:::startclass;
|
|
88
|
+
\tPromptTemplate([PromptTemplate]):::otherclass;
|
|
89
|
+
\tFakeLLM([FakeLLM]):::otherclass;
|
|
90
|
+
\tCommaSeparatedListOutputParser([CommaSeparatedListOutputParser]):::otherclass;
|
|
91
|
+
\tCommaSeparatedListOutputParserOutput[CommaSeparatedListOutputParserOutput]:::endclass;
|
|
92
|
+
\tPromptTemplateInput --> PromptTemplate;
|
|
93
|
+
\tPromptTemplate --> FakeLLM;
|
|
94
|
+
\tCommaSeparatedListOutputParser --> CommaSeparatedListOutputParserOutput;
|
|
95
|
+
\tFakeLLM --> CommaSeparatedListOutputParser;
|
|
96
|
+
\tclassDef startclass fill:#ffdfba;
|
|
97
|
+
\tclassDef endclass fill:#baffc9;
|
|
98
|
+
\tclassDef otherclass fill:#fad7de;
|
|
99
|
+
`);
|
|
84
100
|
});
|
|
@@ -31,3 +31,13 @@ export interface RunnableInterface<RunInput = any, RunOutput = any, CallOptions
|
|
|
31
31
|
transform(generator: AsyncGenerator<RunInput>, options: Partial<CallOptions>): AsyncGenerator<RunOutput>;
|
|
32
32
|
getName(suffix?: string): string;
|
|
33
33
|
}
|
|
34
|
+
export interface Edge {
|
|
35
|
+
source: string;
|
|
36
|
+
target: string;
|
|
37
|
+
data?: string;
|
|
38
|
+
conditional?: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface Node {
|
|
41
|
+
id: string;
|
|
42
|
+
data: RunnableIOSchema | RunnableInterface;
|
|
43
|
+
}
|