@langchain/core 0.3.78 → 0.3.80
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/load/index.cjs +145 -58
- package/dist/load/index.d.ts +146 -2
- package/dist/load/index.js +145 -58
- package/dist/load/serializable.cjs +12 -1
- package/dist/load/serializable.d.ts +14 -0
- package/dist/load/serializable.js +12 -1
- package/dist/load/validation.cjs +222 -0
- package/dist/load/validation.d.ts +99 -0
- package/dist/load/validation.js +212 -0
- package/dist/messages/ai.cjs +4 -2
- package/dist/messages/ai.js +4 -2
- package/package.json +1 -1
package/dist/load/index.cjs
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Load LangChain objects from JSON strings or objects.
|
|
4
|
+
*
|
|
5
|
+
* ## How it works
|
|
6
|
+
*
|
|
7
|
+
* Each `Serializable` LangChain object has a unique identifier (its "class path"),
|
|
8
|
+
* which is a list of strings representing the module path and class name. For example:
|
|
9
|
+
*
|
|
10
|
+
* - `AIMessage` -> `["langchain_core", "messages", "ai", "AIMessage"]`
|
|
11
|
+
* - `ChatPromptTemplate` -> `["langchain_core", "prompts", "chat", "ChatPromptTemplate"]`
|
|
12
|
+
*
|
|
13
|
+
* When deserializing, the class path is validated against supported namespaces.
|
|
14
|
+
*
|
|
15
|
+
* ## Security model
|
|
16
|
+
*
|
|
17
|
+
* The `secretsFromEnv` parameter controls whether secrets can be loaded from environment
|
|
18
|
+
* variables:
|
|
19
|
+
*
|
|
20
|
+
* - `false` (default): Secrets must be provided in `secretsMap`. If a secret is not
|
|
21
|
+
* found, `null` is returned instead of loading from environment variables.
|
|
22
|
+
* - `true`: If a secret is not found in `secretsMap`, it will be loaded from
|
|
23
|
+
* environment variables. Use this only in trusted environments.
|
|
24
|
+
*
|
|
25
|
+
* ### Injection protection (escape-based)
|
|
26
|
+
*
|
|
27
|
+
* During serialization, plain objects that contain an `'lc'` key are escaped by wrapping
|
|
28
|
+
* them: `{"__lc_escaped__": {...}}`. During deserialization, escaped objects are unwrapped
|
|
29
|
+
* and returned as plain objects, NOT instantiated as LC objects.
|
|
30
|
+
*
|
|
31
|
+
* This is an allowlist approach: only objects explicitly produced by
|
|
32
|
+
* `Serializable.toJSON()` (which are NOT escaped) are treated as LC objects;
|
|
33
|
+
* everything else is user data.
|
|
34
|
+
*
|
|
35
|
+
* @module
|
|
36
|
+
*/
|
|
2
37
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
38
|
if (k2 === undefined) k2 = k;
|
|
4
39
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -39,6 +74,12 @@ const import_constants_js_1 = require("./import_constants.cjs");
|
|
|
39
74
|
const coreImportMap = __importStar(require("./import_map.cjs"));
|
|
40
75
|
const map_keys_js_1 = require("./map_keys.cjs");
|
|
41
76
|
const env_js_1 = require("../utils/env.cjs");
|
|
77
|
+
const validation_js_1 = require("./validation.cjs");
|
|
78
|
+
/**
|
|
79
|
+
* Default maximum recursion depth for deserialization.
|
|
80
|
+
* This provides protection against DoS attacks via deeply nested structures.
|
|
81
|
+
*/
|
|
82
|
+
const DEFAULT_MAX_DEPTH = 50;
|
|
42
83
|
function combineAliasesAndInvert(constructor) {
|
|
43
84
|
const aliases = {};
|
|
44
85
|
for (
|
|
@@ -51,53 +92,74 @@ function combineAliasesAndInvert(constructor) {
|
|
|
51
92
|
return acc;
|
|
52
93
|
}, {});
|
|
53
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Recursively revive a value, handling escape markers and LC objects.
|
|
97
|
+
*
|
|
98
|
+
* This function handles:
|
|
99
|
+
* 1. Escaped dicts - unwrapped and returned as plain objects
|
|
100
|
+
* 2. LC secret objects - resolved from secretsMap or env
|
|
101
|
+
* 3. LC constructor objects - instantiated
|
|
102
|
+
* 4. Regular objects/arrays - recursed into
|
|
103
|
+
*/
|
|
54
104
|
async function reviver(value) {
|
|
55
|
-
const { optionalImportsMap
|
|
105
|
+
const { optionalImportsMap, optionalImportEntrypoints, importMap, secretsMap, secretsFromEnv, path, depth, maxDepth, } = this;
|
|
56
106
|
const pathStr = path.join(".");
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
value
|
|
65
|
-
|
|
107
|
+
// Check recursion depth to prevent DoS via deeply nested structures
|
|
108
|
+
if (depth > maxDepth) {
|
|
109
|
+
throw new Error(`Maximum recursion depth (${maxDepth}) exceeded during deserialization. ` +
|
|
110
|
+
`This may indicate a malicious payload or you may need to increase maxDepth.`);
|
|
111
|
+
}
|
|
112
|
+
// If not an object, return as-is
|
|
113
|
+
if (typeof value !== "object" || value == null) {
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
// Handle arrays - recurse into elements
|
|
117
|
+
if (Array.isArray(value)) {
|
|
118
|
+
return Promise.all(value.map((v, i) => reviver.call({ ...this, path: [...path, `${i}`], depth: depth + 1 }, v)));
|
|
119
|
+
}
|
|
120
|
+
// It's an object - check for escape marker FIRST
|
|
121
|
+
const record = value;
|
|
122
|
+
if ((0, validation_js_1.isEscapedObject)(record)) {
|
|
123
|
+
// This is an escaped user object - unwrap and return as-is (no LC processing)
|
|
124
|
+
return (0, validation_js_1.unescapeValue)(record);
|
|
125
|
+
}
|
|
126
|
+
// Check for LC secret object
|
|
127
|
+
if ("lc" in record &&
|
|
128
|
+
"type" in record &&
|
|
129
|
+
"id" in record &&
|
|
130
|
+
record.lc === 1 &&
|
|
131
|
+
record.type === "secret") {
|
|
132
|
+
const serialized = record;
|
|
66
133
|
const [key] = serialized.id;
|
|
67
134
|
if (key in secretsMap) {
|
|
68
135
|
return secretsMap[key];
|
|
69
136
|
}
|
|
70
|
-
else {
|
|
137
|
+
else if (secretsFromEnv) {
|
|
71
138
|
const secretValueInEnv = (0, env_js_1.getEnvironmentVariable)(key);
|
|
72
139
|
if (secretValueInEnv) {
|
|
73
140
|
return secretValueInEnv;
|
|
74
141
|
}
|
|
75
|
-
else {
|
|
76
|
-
throw new Error(`Missing key "${key}" for ${pathStr} in load(secretsMap={})`);
|
|
77
|
-
}
|
|
78
142
|
}
|
|
143
|
+
throw new Error(`Missing secret "${key}" at ${pathStr}`);
|
|
79
144
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
value.type === "not_implemented") {
|
|
88
|
-
const serialized = value;
|
|
145
|
+
// Check for LC not_implemented object
|
|
146
|
+
if ("lc" in record &&
|
|
147
|
+
"type" in record &&
|
|
148
|
+
"id" in record &&
|
|
149
|
+
record.lc === 1 &&
|
|
150
|
+
record.type === "not_implemented") {
|
|
151
|
+
const serialized = record;
|
|
89
152
|
const str = JSON.stringify(serialized);
|
|
90
153
|
throw new Error(`Trying to load an object that doesn't implement serialization: ${pathStr} -> ${str}`);
|
|
91
154
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
|
|
98
|
-
"
|
|
99
|
-
|
|
100
|
-
const serialized = value;
|
|
155
|
+
// Check for LC constructor object
|
|
156
|
+
if ("lc" in record &&
|
|
157
|
+
"type" in record &&
|
|
158
|
+
"id" in record &&
|
|
159
|
+
"kwargs" in record &&
|
|
160
|
+
record.lc === 1 &&
|
|
161
|
+
record.type === "constructor") {
|
|
162
|
+
const serialized = record;
|
|
101
163
|
const str = JSON.stringify(serialized);
|
|
102
164
|
const [name, ...namespaceReverse] = serialized.id.slice().reverse();
|
|
103
165
|
const namespace = namespaceReverse.reverse();
|
|
@@ -164,35 +226,60 @@ async function reviver(value) {
|
|
|
164
226
|
throw new Error(`Invalid identifer: ${pathStr} -> ${str}`);
|
|
165
227
|
}
|
|
166
228
|
// Recurse on the arguments, which may be serialized objects themselves
|
|
167
|
-
const kwargs = await reviver.call({ ...this, path: [...path, "kwargs"] }, serialized.kwargs);
|
|
229
|
+
const kwargs = await reviver.call({ ...this, path: [...path, "kwargs"], depth: depth + 1 }, serialized.kwargs);
|
|
168
230
|
// Construct the object
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return instance;
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
throw new Error(`Invalid type: ${pathStr} -> ${str}`);
|
|
180
|
-
}
|
|
231
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
232
|
+
const instance = new builder((0, map_keys_js_1.mapKeys)(kwargs, map_keys_js_1.keyFromJson, combineAliasesAndInvert(builder)));
|
|
233
|
+
// Minification in severless/edge runtimes will mange the
|
|
234
|
+
// name of classes presented in traces. As the names in import map
|
|
235
|
+
// are present as-is even with minification, use these names instead
|
|
236
|
+
Object.defineProperty(instance.constructor, "name", { value: name });
|
|
237
|
+
return instance;
|
|
181
238
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, value]) => [
|
|
188
|
-
key,
|
|
189
|
-
await reviver.call({ ...this, path: [...path, key] }, value),
|
|
190
|
-
])));
|
|
191
|
-
}
|
|
239
|
+
// Regular object - recurse into values
|
|
240
|
+
const result = {};
|
|
241
|
+
for (const [key, val] of Object.entries(record)) {
|
|
242
|
+
result[key] = await reviver.call({ ...this, path: [...path, key], depth: depth + 1 }, val);
|
|
192
243
|
}
|
|
193
|
-
return
|
|
244
|
+
return result;
|
|
194
245
|
}
|
|
195
|
-
|
|
246
|
+
/**
|
|
247
|
+
* Load a LangChain object from a JSON string.
|
|
248
|
+
*
|
|
249
|
+
* @param text - The JSON string to parse and load.
|
|
250
|
+
* @param options - Options for loading.
|
|
251
|
+
* @returns The loaded LangChain object.
|
|
252
|
+
*
|
|
253
|
+
* @example
|
|
254
|
+
* ```typescript
|
|
255
|
+
* import { load } from "@langchain/core/load";
|
|
256
|
+
* import { AIMessage } from "@langchain/core/messages";
|
|
257
|
+
*
|
|
258
|
+
* // Basic usage - secrets must be provided explicitly
|
|
259
|
+
* const msg = await load<AIMessage>(jsonString);
|
|
260
|
+
*
|
|
261
|
+
* // With secrets from a map
|
|
262
|
+
* const msg = await load<AIMessage>(jsonString, {
|
|
263
|
+
* secretsMap: { OPENAI_API_KEY: "sk-..." }
|
|
264
|
+
* });
|
|
265
|
+
*
|
|
266
|
+
* // Allow loading secrets from environment (use with caution)
|
|
267
|
+
* const msg = await load<AIMessage>(jsonString, {
|
|
268
|
+
* secretsFromEnv: true
|
|
269
|
+
* });
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
async function load(text, options) {
|
|
196
273
|
const json = JSON.parse(text);
|
|
197
|
-
|
|
274
|
+
const context = {
|
|
275
|
+
optionalImportsMap: options?.optionalImportsMap ?? {},
|
|
276
|
+
optionalImportEntrypoints: options?.optionalImportEntrypoints ?? [],
|
|
277
|
+
secretsMap: options?.secretsMap ?? {},
|
|
278
|
+
secretsFromEnv: options?.secretsFromEnv ?? false,
|
|
279
|
+
importMap: options?.importMap ?? {},
|
|
280
|
+
path: ["$"],
|
|
281
|
+
depth: 0,
|
|
282
|
+
maxDepth: options?.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
283
|
+
};
|
|
284
|
+
return reviver.call(context, json);
|
|
198
285
|
}
|
package/dist/load/index.d.ts
CHANGED
|
@@ -1,7 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load LangChain objects from JSON strings or objects.
|
|
3
|
+
*
|
|
4
|
+
* ## How it works
|
|
5
|
+
*
|
|
6
|
+
* Each `Serializable` LangChain object has a unique identifier (its "class path"),
|
|
7
|
+
* which is a list of strings representing the module path and class name. For example:
|
|
8
|
+
*
|
|
9
|
+
* - `AIMessage` -> `["langchain_core", "messages", "ai", "AIMessage"]`
|
|
10
|
+
* - `ChatPromptTemplate` -> `["langchain_core", "prompts", "chat", "ChatPromptTemplate"]`
|
|
11
|
+
*
|
|
12
|
+
* When deserializing, the class path is validated against supported namespaces.
|
|
13
|
+
*
|
|
14
|
+
* ## Security model
|
|
15
|
+
*
|
|
16
|
+
* The `secretsFromEnv` parameter controls whether secrets can be loaded from environment
|
|
17
|
+
* variables:
|
|
18
|
+
*
|
|
19
|
+
* - `false` (default): Secrets must be provided in `secretsMap`. If a secret is not
|
|
20
|
+
* found, `null` is returned instead of loading from environment variables.
|
|
21
|
+
* - `true`: If a secret is not found in `secretsMap`, it will be loaded from
|
|
22
|
+
* environment variables. Use this only in trusted environments.
|
|
23
|
+
*
|
|
24
|
+
* ### Injection protection (escape-based)
|
|
25
|
+
*
|
|
26
|
+
* During serialization, plain objects that contain an `'lc'` key are escaped by wrapping
|
|
27
|
+
* them: `{"__lc_escaped__": {...}}`. During deserialization, escaped objects are unwrapped
|
|
28
|
+
* and returned as plain objects, NOT instantiated as LC objects.
|
|
29
|
+
*
|
|
30
|
+
* This is an allowlist approach: only objects explicitly produced by
|
|
31
|
+
* `Serializable.toJSON()` (which are NOT escaped) are treated as LC objects;
|
|
32
|
+
* everything else is user data.
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
1
36
|
import type { OptionalImportMap, SecretMap } from "./import_type.js";
|
|
2
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Options for loading serialized LangChain objects.
|
|
39
|
+
*
|
|
40
|
+
* @remarks
|
|
41
|
+
* **Security considerations:**
|
|
42
|
+
*
|
|
43
|
+
* Deserialization can instantiate arbitrary classes from the allowed namespaces.
|
|
44
|
+
* When loading untrusted data, be aware that:
|
|
45
|
+
*
|
|
46
|
+
* 1. **`secretsFromEnv`**: Defaults to `false`. Setting to `true` allows the
|
|
47
|
+
* deserializer to read environment variables, which could leak secrets if
|
|
48
|
+
* the serialized data contains malicious secret references.
|
|
49
|
+
*
|
|
50
|
+
* 2. **`importMap` / `optionalImportsMap`**: These allow extending which classes
|
|
51
|
+
* can be instantiated. Never populate these from user input. Only include
|
|
52
|
+
* modules you explicitly trust.
|
|
53
|
+
*
|
|
54
|
+
* 3. **Class instantiation**: Allowed classes will have their constructors called
|
|
55
|
+
* with the deserialized kwargs. If a class performs side effects in its
|
|
56
|
+
* constructor (network calls, file I/O, etc.), those will execute.
|
|
57
|
+
*/
|
|
58
|
+
export interface LoadOptions {
|
|
59
|
+
/**
|
|
60
|
+
* A map of secrets to load. Keys are secret identifiers, values are the secret values.
|
|
61
|
+
*
|
|
62
|
+
* If a secret is not found in this map and `secretsFromEnv` is `false`, an error is
|
|
63
|
+
* thrown. If `secretsFromEnv` is `true`, the secret will be loaded from environment
|
|
64
|
+
* variables (if not found there either, an error is thrown).
|
|
65
|
+
*/
|
|
3
66
|
secretsMap?: SecretMap;
|
|
67
|
+
/**
|
|
68
|
+
* Whether to load secrets from environment variables when not found in `secretsMap`.
|
|
69
|
+
*
|
|
70
|
+
* @default false
|
|
71
|
+
*
|
|
72
|
+
* @remarks
|
|
73
|
+
* **Security warning:** Setting this to `true` allows the deserializer to read
|
|
74
|
+
* environment variables, which could be a security risk if the serialized data
|
|
75
|
+
* is not trusted. Only set this to `true` when deserializing data from trusted
|
|
76
|
+
* sources (e.g., your own database, not user input).
|
|
77
|
+
*/
|
|
78
|
+
secretsFromEnv?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* A map of optional imports. Keys are namespace paths (e.g., "langchain_community/llms"),
|
|
81
|
+
* values are the imported modules.
|
|
82
|
+
*
|
|
83
|
+
* @remarks
|
|
84
|
+
* **Security warning:** This extends which classes can be instantiated during
|
|
85
|
+
* deserialization. Never populate this map with values derived from user input.
|
|
86
|
+
* Only include modules that you explicitly trust and have reviewed.
|
|
87
|
+
*
|
|
88
|
+
* Classes in these modules can be instantiated with attacker-controlled kwargs
|
|
89
|
+
* if the serialized data is untrusted.
|
|
90
|
+
*/
|
|
4
91
|
optionalImportsMap?: OptionalImportMap;
|
|
92
|
+
/**
|
|
93
|
+
* Additional optional import entrypoints to allow beyond the defaults.
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* **Security warning:** This extends which namespace paths are considered valid
|
|
97
|
+
* for deserialization. Never populate this array with values derived from user
|
|
98
|
+
* input. Each entrypoint you add expands the attack surface for deserialization.
|
|
99
|
+
*/
|
|
5
100
|
optionalImportEntrypoints?: string[];
|
|
101
|
+
/**
|
|
102
|
+
* Additional import map for the "langchain" namespace.
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* **Security warning:** This extends which classes can be instantiated during
|
|
106
|
+
* deserialization. Never populate this map with values derived from user input.
|
|
107
|
+
* Only include modules that you explicitly trust and have reviewed.
|
|
108
|
+
*
|
|
109
|
+
* Any class exposed through this map can be instantiated with attacker-controlled
|
|
110
|
+
* kwargs if the serialized data is untrusted.
|
|
111
|
+
*/
|
|
6
112
|
importMap?: Record<string, unknown>;
|
|
7
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Maximum recursion depth allowed during deserialization.
|
|
115
|
+
*
|
|
116
|
+
* @default 50
|
|
117
|
+
*
|
|
118
|
+
* @remarks
|
|
119
|
+
* This limit protects against denial-of-service attacks using deeply nested
|
|
120
|
+
* JSON structures that could cause stack overflow. If your legitimate data
|
|
121
|
+
* requires deeper nesting, you can increase this limit.
|
|
122
|
+
*/
|
|
123
|
+
maxDepth?: number;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Load a LangChain object from a JSON string.
|
|
127
|
+
*
|
|
128
|
+
* @param text - The JSON string to parse and load.
|
|
129
|
+
* @param options - Options for loading.
|
|
130
|
+
* @returns The loaded LangChain object.
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* import { load } from "@langchain/core/load";
|
|
135
|
+
* import { AIMessage } from "@langchain/core/messages";
|
|
136
|
+
*
|
|
137
|
+
* // Basic usage - secrets must be provided explicitly
|
|
138
|
+
* const msg = await load<AIMessage>(jsonString);
|
|
139
|
+
*
|
|
140
|
+
* // With secrets from a map
|
|
141
|
+
* const msg = await load<AIMessage>(jsonString, {
|
|
142
|
+
* secretsMap: { OPENAI_API_KEY: "sk-..." }
|
|
143
|
+
* });
|
|
144
|
+
*
|
|
145
|
+
* // Allow loading secrets from environment (use with caution)
|
|
146
|
+
* const msg = await load<AIMessage>(jsonString, {
|
|
147
|
+
* secretsFromEnv: true
|
|
148
|
+
* });
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export declare function load<T>(text: string, options?: LoadOptions): Promise<T>;
|
package/dist/load/index.js
CHANGED
|
@@ -1,8 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load LangChain objects from JSON strings or objects.
|
|
3
|
+
*
|
|
4
|
+
* ## How it works
|
|
5
|
+
*
|
|
6
|
+
* Each `Serializable` LangChain object has a unique identifier (its "class path"),
|
|
7
|
+
* which is a list of strings representing the module path and class name. For example:
|
|
8
|
+
*
|
|
9
|
+
* - `AIMessage` -> `["langchain_core", "messages", "ai", "AIMessage"]`
|
|
10
|
+
* - `ChatPromptTemplate` -> `["langchain_core", "prompts", "chat", "ChatPromptTemplate"]`
|
|
11
|
+
*
|
|
12
|
+
* When deserializing, the class path is validated against supported namespaces.
|
|
13
|
+
*
|
|
14
|
+
* ## Security model
|
|
15
|
+
*
|
|
16
|
+
* The `secretsFromEnv` parameter controls whether secrets can be loaded from environment
|
|
17
|
+
* variables:
|
|
18
|
+
*
|
|
19
|
+
* - `false` (default): Secrets must be provided in `secretsMap`. If a secret is not
|
|
20
|
+
* found, `null` is returned instead of loading from environment variables.
|
|
21
|
+
* - `true`: If a secret is not found in `secretsMap`, it will be loaded from
|
|
22
|
+
* environment variables. Use this only in trusted environments.
|
|
23
|
+
*
|
|
24
|
+
* ### Injection protection (escape-based)
|
|
25
|
+
*
|
|
26
|
+
* During serialization, plain objects that contain an `'lc'` key are escaped by wrapping
|
|
27
|
+
* them: `{"__lc_escaped__": {...}}`. During deserialization, escaped objects are unwrapped
|
|
28
|
+
* and returned as plain objects, NOT instantiated as LC objects.
|
|
29
|
+
*
|
|
30
|
+
* This is an allowlist approach: only objects explicitly produced by
|
|
31
|
+
* `Serializable.toJSON()` (which are NOT escaped) are treated as LC objects;
|
|
32
|
+
* everything else is user data.
|
|
33
|
+
*
|
|
34
|
+
* @module
|
|
35
|
+
*/
|
|
1
36
|
import { get_lc_unique_name, } from "./serializable.js";
|
|
2
37
|
import { optionalImportEntrypoints as defaultOptionalImportEntrypoints } from "./import_constants.js";
|
|
3
38
|
import * as coreImportMap from "./import_map.js";
|
|
4
39
|
import { keyFromJson, mapKeys } from "./map_keys.js";
|
|
5
40
|
import { getEnvironmentVariable } from "../utils/env.js";
|
|
41
|
+
import { isEscapedObject, unescapeValue } from "./validation.js";
|
|
42
|
+
/**
|
|
43
|
+
* Default maximum recursion depth for deserialization.
|
|
44
|
+
* This provides protection against DoS attacks via deeply nested structures.
|
|
45
|
+
*/
|
|
46
|
+
const DEFAULT_MAX_DEPTH = 50;
|
|
6
47
|
function combineAliasesAndInvert(constructor) {
|
|
7
48
|
const aliases = {};
|
|
8
49
|
for (
|
|
@@ -15,53 +56,74 @@ function combineAliasesAndInvert(constructor) {
|
|
|
15
56
|
return acc;
|
|
16
57
|
}, {});
|
|
17
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Recursively revive a value, handling escape markers and LC objects.
|
|
61
|
+
*
|
|
62
|
+
* This function handles:
|
|
63
|
+
* 1. Escaped dicts - unwrapped and returned as plain objects
|
|
64
|
+
* 2. LC secret objects - resolved from secretsMap or env
|
|
65
|
+
* 3. LC constructor objects - instantiated
|
|
66
|
+
* 4. Regular objects/arrays - recursed into
|
|
67
|
+
*/
|
|
18
68
|
async function reviver(value) {
|
|
19
|
-
const { optionalImportsMap
|
|
69
|
+
const { optionalImportsMap, optionalImportEntrypoints, importMap, secretsMap, secretsFromEnv, path, depth, maxDepth, } = this;
|
|
20
70
|
const pathStr = path.join(".");
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
value
|
|
29
|
-
|
|
71
|
+
// Check recursion depth to prevent DoS via deeply nested structures
|
|
72
|
+
if (depth > maxDepth) {
|
|
73
|
+
throw new Error(`Maximum recursion depth (${maxDepth}) exceeded during deserialization. ` +
|
|
74
|
+
`This may indicate a malicious payload or you may need to increase maxDepth.`);
|
|
75
|
+
}
|
|
76
|
+
// If not an object, return as-is
|
|
77
|
+
if (typeof value !== "object" || value == null) {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
// Handle arrays - recurse into elements
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
return Promise.all(value.map((v, i) => reviver.call({ ...this, path: [...path, `${i}`], depth: depth + 1 }, v)));
|
|
83
|
+
}
|
|
84
|
+
// It's an object - check for escape marker FIRST
|
|
85
|
+
const record = value;
|
|
86
|
+
if (isEscapedObject(record)) {
|
|
87
|
+
// This is an escaped user object - unwrap and return as-is (no LC processing)
|
|
88
|
+
return unescapeValue(record);
|
|
89
|
+
}
|
|
90
|
+
// Check for LC secret object
|
|
91
|
+
if ("lc" in record &&
|
|
92
|
+
"type" in record &&
|
|
93
|
+
"id" in record &&
|
|
94
|
+
record.lc === 1 &&
|
|
95
|
+
record.type === "secret") {
|
|
96
|
+
const serialized = record;
|
|
30
97
|
const [key] = serialized.id;
|
|
31
98
|
if (key in secretsMap) {
|
|
32
99
|
return secretsMap[key];
|
|
33
100
|
}
|
|
34
|
-
else {
|
|
101
|
+
else if (secretsFromEnv) {
|
|
35
102
|
const secretValueInEnv = getEnvironmentVariable(key);
|
|
36
103
|
if (secretValueInEnv) {
|
|
37
104
|
return secretValueInEnv;
|
|
38
105
|
}
|
|
39
|
-
else {
|
|
40
|
-
throw new Error(`Missing key "${key}" for ${pathStr} in load(secretsMap={})`);
|
|
41
|
-
}
|
|
42
106
|
}
|
|
107
|
+
throw new Error(`Missing secret "${key}" at ${pathStr}`);
|
|
43
108
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
value.type === "not_implemented") {
|
|
52
|
-
const serialized = value;
|
|
109
|
+
// Check for LC not_implemented object
|
|
110
|
+
if ("lc" in record &&
|
|
111
|
+
"type" in record &&
|
|
112
|
+
"id" in record &&
|
|
113
|
+
record.lc === 1 &&
|
|
114
|
+
record.type === "not_implemented") {
|
|
115
|
+
const serialized = record;
|
|
53
116
|
const str = JSON.stringify(serialized);
|
|
54
117
|
throw new Error(`Trying to load an object that doesn't implement serialization: ${pathStr} -> ${str}`);
|
|
55
118
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
const serialized = value;
|
|
119
|
+
// Check for LC constructor object
|
|
120
|
+
if ("lc" in record &&
|
|
121
|
+
"type" in record &&
|
|
122
|
+
"id" in record &&
|
|
123
|
+
"kwargs" in record &&
|
|
124
|
+
record.lc === 1 &&
|
|
125
|
+
record.type === "constructor") {
|
|
126
|
+
const serialized = record;
|
|
65
127
|
const str = JSON.stringify(serialized);
|
|
66
128
|
const [name, ...namespaceReverse] = serialized.id.slice().reverse();
|
|
67
129
|
const namespace = namespaceReverse.reverse();
|
|
@@ -128,35 +190,60 @@ async function reviver(value) {
|
|
|
128
190
|
throw new Error(`Invalid identifer: ${pathStr} -> ${str}`);
|
|
129
191
|
}
|
|
130
192
|
// Recurse on the arguments, which may be serialized objects themselves
|
|
131
|
-
const kwargs = await reviver.call({ ...this, path: [...path, "kwargs"] }, serialized.kwargs);
|
|
193
|
+
const kwargs = await reviver.call({ ...this, path: [...path, "kwargs"], depth: depth + 1 }, serialized.kwargs);
|
|
132
194
|
// Construct the object
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return instance;
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
throw new Error(`Invalid type: ${pathStr} -> ${str}`);
|
|
144
|
-
}
|
|
195
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
196
|
+
const instance = new builder(mapKeys(kwargs, keyFromJson, combineAliasesAndInvert(builder)));
|
|
197
|
+
// Minification in severless/edge runtimes will mange the
|
|
198
|
+
// name of classes presented in traces. As the names in import map
|
|
199
|
+
// are present as-is even with minification, use these names instead
|
|
200
|
+
Object.defineProperty(instance.constructor, "name", { value: name });
|
|
201
|
+
return instance;
|
|
145
202
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, value]) => [
|
|
152
|
-
key,
|
|
153
|
-
await reviver.call({ ...this, path: [...path, key] }, value),
|
|
154
|
-
])));
|
|
155
|
-
}
|
|
203
|
+
// Regular object - recurse into values
|
|
204
|
+
const result = {};
|
|
205
|
+
for (const [key, val] of Object.entries(record)) {
|
|
206
|
+
result[key] = await reviver.call({ ...this, path: [...path, key], depth: depth + 1 }, val);
|
|
156
207
|
}
|
|
157
|
-
return
|
|
208
|
+
return result;
|
|
158
209
|
}
|
|
159
|
-
|
|
210
|
+
/**
|
|
211
|
+
* Load a LangChain object from a JSON string.
|
|
212
|
+
*
|
|
213
|
+
* @param text - The JSON string to parse and load.
|
|
214
|
+
* @param options - Options for loading.
|
|
215
|
+
* @returns The loaded LangChain object.
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* ```typescript
|
|
219
|
+
* import { load } from "@langchain/core/load";
|
|
220
|
+
* import { AIMessage } from "@langchain/core/messages";
|
|
221
|
+
*
|
|
222
|
+
* // Basic usage - secrets must be provided explicitly
|
|
223
|
+
* const msg = await load<AIMessage>(jsonString);
|
|
224
|
+
*
|
|
225
|
+
* // With secrets from a map
|
|
226
|
+
* const msg = await load<AIMessage>(jsonString, {
|
|
227
|
+
* secretsMap: { OPENAI_API_KEY: "sk-..." }
|
|
228
|
+
* });
|
|
229
|
+
*
|
|
230
|
+
* // Allow loading secrets from environment (use with caution)
|
|
231
|
+
* const msg = await load<AIMessage>(jsonString, {
|
|
232
|
+
* secretsFromEnv: true
|
|
233
|
+
* });
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
export async function load(text, options) {
|
|
160
237
|
const json = JSON.parse(text);
|
|
161
|
-
|
|
238
|
+
const context = {
|
|
239
|
+
optionalImportsMap: options?.optionalImportsMap ?? {},
|
|
240
|
+
optionalImportEntrypoints: options?.optionalImportEntrypoints ?? [],
|
|
241
|
+
secretsMap: options?.secretsMap ?? {},
|
|
242
|
+
secretsFromEnv: options?.secretsFromEnv ?? false,
|
|
243
|
+
importMap: options?.importMap ?? {},
|
|
244
|
+
path: ["$"],
|
|
245
|
+
depth: 0,
|
|
246
|
+
maxDepth: options?.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
247
|
+
};
|
|
248
|
+
return reviver.call(context, json);
|
|
162
249
|
}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Serializable = void 0;
|
|
4
4
|
exports.get_lc_unique_name = get_lc_unique_name;
|
|
5
5
|
const map_keys_js_1 = require("./map_keys.cjs");
|
|
6
|
+
const validation_js_1 = require("./validation.cjs");
|
|
6
7
|
function shallowCopy(obj) {
|
|
7
8
|
return Array.isArray(obj) ? [...obj] : { ...obj };
|
|
8
9
|
}
|
|
@@ -174,11 +175,21 @@ class Serializable {
|
|
|
174
175
|
write[last] = write[last] || read[last];
|
|
175
176
|
}
|
|
176
177
|
});
|
|
178
|
+
const escapedKwargs = {};
|
|
179
|
+
for (const [key, value] of Object.entries(kwargs)) {
|
|
180
|
+
escapedKwargs[key] = (0, validation_js_1.escapeIfNeeded)(value);
|
|
181
|
+
}
|
|
182
|
+
// Now add secret markers - these are added AFTER escaping so they won't be escaped
|
|
183
|
+
const kwargsWithSecrets = Object.keys(secrets).length
|
|
184
|
+
? replaceSecrets(escapedKwargs, secrets)
|
|
185
|
+
: escapedKwargs;
|
|
186
|
+
// Finally transform keys to JSON format
|
|
187
|
+
const processedKwargs = (0, map_keys_js_1.mapKeys)(kwargsWithSecrets, map_keys_js_1.keyToJson, aliases);
|
|
177
188
|
return {
|
|
178
189
|
lc: 1,
|
|
179
190
|
type: "constructor",
|
|
180
191
|
id: this.lc_id,
|
|
181
|
-
kwargs:
|
|
192
|
+
kwargs: processedKwargs,
|
|
182
193
|
};
|
|
183
194
|
}
|
|
184
195
|
toJSONNotImplemented() {
|
|
@@ -19,6 +19,20 @@ export type Serialized = SerializedConstructor | SerializedSecret | SerializedNo
|
|
|
19
19
|
* Should not be subclassed, subclass lc_name above instead.
|
|
20
20
|
*/
|
|
21
21
|
export declare function get_lc_unique_name(serializableClass: typeof Serializable): string;
|
|
22
|
+
/**
|
|
23
|
+
* Interface for objects that can be serialized.
|
|
24
|
+
* This is a duck-typed interface to avoid circular imports.
|
|
25
|
+
*/
|
|
26
|
+
export interface SerializableLike {
|
|
27
|
+
lc_serializable: boolean;
|
|
28
|
+
lc_secrets?: Record<string, string>;
|
|
29
|
+
toJSON(): {
|
|
30
|
+
lc: number;
|
|
31
|
+
type: string;
|
|
32
|
+
id: string[];
|
|
33
|
+
kwargs?: Record<string, unknown>;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
22
36
|
export interface SerializableInterface {
|
|
23
37
|
get lc_id(): string[];
|
|
24
38
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { keyToJson, mapKeys } from "./map_keys.js";
|
|
2
|
+
import { escapeIfNeeded } from "./validation.js";
|
|
2
3
|
function shallowCopy(obj) {
|
|
3
4
|
return Array.isArray(obj) ? [...obj] : { ...obj };
|
|
4
5
|
}
|
|
@@ -170,11 +171,21 @@ export class Serializable {
|
|
|
170
171
|
write[last] = write[last] || read[last];
|
|
171
172
|
}
|
|
172
173
|
});
|
|
174
|
+
const escapedKwargs = {};
|
|
175
|
+
for (const [key, value] of Object.entries(kwargs)) {
|
|
176
|
+
escapedKwargs[key] = escapeIfNeeded(value);
|
|
177
|
+
}
|
|
178
|
+
// Now add secret markers - these are added AFTER escaping so they won't be escaped
|
|
179
|
+
const kwargsWithSecrets = Object.keys(secrets).length
|
|
180
|
+
? replaceSecrets(escapedKwargs, secrets)
|
|
181
|
+
: escapedKwargs;
|
|
182
|
+
// Finally transform keys to JSON format
|
|
183
|
+
const processedKwargs = mapKeys(kwargsWithSecrets, keyToJson, aliases);
|
|
173
184
|
return {
|
|
174
185
|
lc: 1,
|
|
175
186
|
type: "constructor",
|
|
176
187
|
id: this.lc_id,
|
|
177
|
-
kwargs:
|
|
188
|
+
kwargs: processedKwargs,
|
|
178
189
|
};
|
|
179
190
|
}
|
|
180
191
|
toJSONNotImplemented() {
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LC_ESCAPED_KEY = void 0;
|
|
4
|
+
exports.needsEscaping = needsEscaping;
|
|
5
|
+
exports.escapeObject = escapeObject;
|
|
6
|
+
exports.isEscapedObject = isEscapedObject;
|
|
7
|
+
exports.serializeValue = serializeValue;
|
|
8
|
+
exports.serializeLcObject = serializeLcObject;
|
|
9
|
+
exports.escapeIfNeeded = escapeIfNeeded;
|
|
10
|
+
exports.unescapeValue = unescapeValue;
|
|
11
|
+
/**
|
|
12
|
+
* Sentinel key used to mark escaped user objects during serialization.
|
|
13
|
+
*
|
|
14
|
+
* When a plain object contains 'lc' key (which could be confused with LC objects),
|
|
15
|
+
* we wrap it as `{"__lc_escaped__": {...original...}}`.
|
|
16
|
+
*/
|
|
17
|
+
exports.LC_ESCAPED_KEY = "__lc_escaped__";
|
|
18
|
+
/**
|
|
19
|
+
* Check if an object needs escaping to prevent confusion with LC objects.
|
|
20
|
+
*
|
|
21
|
+
* An object needs escaping if:
|
|
22
|
+
* 1. It has an `'lc'` key (could be confused with LC serialization format)
|
|
23
|
+
* 2. It has only the escape key (would be mistaken for an escaped object)
|
|
24
|
+
*/
|
|
25
|
+
function needsEscaping(obj) {
|
|
26
|
+
return ("lc" in obj || (Object.keys(obj).length === 1 && exports.LC_ESCAPED_KEY in obj));
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Wrap an object in the escape marker.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* {"key": "value"} // becomes {"__lc_escaped__": {"key": "value"}}
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
function escapeObject(obj) {
|
|
37
|
+
return { [exports.LC_ESCAPED_KEY]: obj };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Check if an object is an escaped user object.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* {"__lc_escaped__": {...}} // is an escaped object
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
function isEscapedObject(obj) {
|
|
48
|
+
return Object.keys(obj).length === 1 && exports.LC_ESCAPED_KEY in obj;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check if an object looks like a Serializable instance (duck typing).
|
|
52
|
+
*/
|
|
53
|
+
function isSerializableLike(obj) {
|
|
54
|
+
return (obj !== null &&
|
|
55
|
+
typeof obj === "object" &&
|
|
56
|
+
"lc_serializable" in obj &&
|
|
57
|
+
typeof obj.toJSON === "function");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create a "not_implemented" serialization result for objects that cannot be serialized.
|
|
61
|
+
*/
|
|
62
|
+
function createNotImplemented(obj) {
|
|
63
|
+
let id;
|
|
64
|
+
if (obj !== null && typeof obj === "object") {
|
|
65
|
+
if ("lc_id" in obj && Array.isArray(obj.lc_id)) {
|
|
66
|
+
id = obj.lc_id;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
id = [obj.constructor?.name ?? "Object"];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
id = [typeof obj];
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
lc: 1,
|
|
77
|
+
type: "not_implemented",
|
|
78
|
+
id,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Serialize a value with escaping of user objects.
|
|
83
|
+
*
|
|
84
|
+
* Called recursively on kwarg values to escape any plain objects that could be
|
|
85
|
+
* confused with LC objects.
|
|
86
|
+
*
|
|
87
|
+
* @param obj - The value to serialize.
|
|
88
|
+
* @returns The serialized value with user objects escaped as needed.
|
|
89
|
+
*/
|
|
90
|
+
function serializeValue(obj) {
|
|
91
|
+
if (isSerializableLike(obj)) {
|
|
92
|
+
// This is an LC object - serialize it properly (not escaped)
|
|
93
|
+
return serializeLcObject(obj);
|
|
94
|
+
}
|
|
95
|
+
if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
|
|
96
|
+
const record = obj;
|
|
97
|
+
// Check if object needs escaping BEFORE recursing into values.
|
|
98
|
+
// If it needs escaping, wrap it as-is - the contents are user data that
|
|
99
|
+
// will be returned as-is during deserialization (no instantiation).
|
|
100
|
+
// This prevents re-escaping of already-escaped nested content.
|
|
101
|
+
if (needsEscaping(record)) {
|
|
102
|
+
return escapeObject(record);
|
|
103
|
+
}
|
|
104
|
+
// Safe object (no 'lc' key) - recurse into values
|
|
105
|
+
const result = {};
|
|
106
|
+
for (const [key, value] of Object.entries(record)) {
|
|
107
|
+
result[key] = serializeValue(value);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(obj)) {
|
|
112
|
+
return obj.map((item) => serializeValue(item));
|
|
113
|
+
}
|
|
114
|
+
if (typeof obj === "string" ||
|
|
115
|
+
typeof obj === "number" ||
|
|
116
|
+
typeof obj === "boolean" ||
|
|
117
|
+
obj === null) {
|
|
118
|
+
return obj;
|
|
119
|
+
}
|
|
120
|
+
// Non-JSON-serializable object (Date, custom objects, etc.)
|
|
121
|
+
return createNotImplemented(obj);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Serialize a `Serializable` object with escaping of user data in kwargs.
|
|
125
|
+
*
|
|
126
|
+
* @param obj - The `Serializable` object to serialize.
|
|
127
|
+
* @returns The serialized object with user data in kwargs escaped as needed.
|
|
128
|
+
*
|
|
129
|
+
* @remarks
|
|
130
|
+
* Kwargs values are processed with `serializeValue` to escape user data (like
|
|
131
|
+
* metadata) that contains `'lc'` keys. Secret fields (from `lc_secrets`) are
|
|
132
|
+
* skipped because `toJSON()` replaces their values with secret markers.
|
|
133
|
+
*/
|
|
134
|
+
function serializeLcObject(obj) {
|
|
135
|
+
// Secret fields are handled by toJSON() - it replaces values with secret markers
|
|
136
|
+
const secretFields = new Set(Object.keys(obj.lc_secrets ?? {}));
|
|
137
|
+
const serialized = { ...obj.toJSON() };
|
|
138
|
+
// Process kwargs to escape user data that could be confused with LC objects
|
|
139
|
+
// Skip secret fields - toJSON() already converted them to secret markers
|
|
140
|
+
if (serialized.type === "constructor" && serialized.kwargs) {
|
|
141
|
+
const newKwargs = {};
|
|
142
|
+
for (const [key, value] of Object.entries(serialized.kwargs)) {
|
|
143
|
+
if (secretFields.has(key)) {
|
|
144
|
+
newKwargs[key] = value;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
newKwargs[key] = serializeValue(value);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
serialized.kwargs = newKwargs;
|
|
151
|
+
}
|
|
152
|
+
return serialized;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Escape a value if it needs escaping (contains `lc` key).
|
|
156
|
+
*
|
|
157
|
+
* This is a simpler version of `serializeValue` that doesn't handle Serializable
|
|
158
|
+
* objects - it's meant to be called on kwargs values that have already been
|
|
159
|
+
* processed by `toJSON()`.
|
|
160
|
+
*
|
|
161
|
+
* @param value - The value to potentially escape.
|
|
162
|
+
* @returns The value with any `lc`-containing objects wrapped in escape markers.
|
|
163
|
+
*/
|
|
164
|
+
function escapeIfNeeded(value) {
|
|
165
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
166
|
+
// Preserve Serializable objects - they have their own toJSON() that will be
|
|
167
|
+
// called by JSON.stringify. We don't want to convert them to plain objects.
|
|
168
|
+
if (isSerializableLike(value)) {
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
const record = value;
|
|
172
|
+
// Check if object needs escaping BEFORE recursing into values.
|
|
173
|
+
// If it needs escaping, wrap it as-is - the contents are user data that
|
|
174
|
+
// will be returned as-is during deserialization (no instantiation).
|
|
175
|
+
if (needsEscaping(record)) {
|
|
176
|
+
return escapeObject(record);
|
|
177
|
+
}
|
|
178
|
+
// Safe object (no 'lc' key) - recurse into values
|
|
179
|
+
const result = {};
|
|
180
|
+
for (const [key, val] of Object.entries(record)) {
|
|
181
|
+
result[key] = escapeIfNeeded(val);
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
if (Array.isArray(value)) {
|
|
186
|
+
return value.map((item) => escapeIfNeeded(item));
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Unescape a value, processing escape markers in object values and arrays.
|
|
192
|
+
*
|
|
193
|
+
* When an escaped object is encountered (`{"__lc_escaped__": ...}`), it's
|
|
194
|
+
* unwrapped and the contents are returned AS-IS (no further processing).
|
|
195
|
+
* The contents represent user data that should not be modified.
|
|
196
|
+
*
|
|
197
|
+
* For regular objects and arrays, we recurse to find any nested escape markers.
|
|
198
|
+
*
|
|
199
|
+
* @param obj - The value to unescape.
|
|
200
|
+
* @returns The unescaped value.
|
|
201
|
+
*/
|
|
202
|
+
function unescapeValue(obj) {
|
|
203
|
+
if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
|
|
204
|
+
const record = obj;
|
|
205
|
+
if (isEscapedObject(record)) {
|
|
206
|
+
// Unwrap and return the user data as-is (no further unescaping).
|
|
207
|
+
// The contents are user data that may contain more escape keys,
|
|
208
|
+
// but those are part of the user's actual data.
|
|
209
|
+
return record[exports.LC_ESCAPED_KEY];
|
|
210
|
+
}
|
|
211
|
+
// Regular object - recurse into values to find nested escape markers
|
|
212
|
+
const result = {};
|
|
213
|
+
for (const [key, value] of Object.entries(record)) {
|
|
214
|
+
result[key] = unescapeValue(value);
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
if (Array.isArray(obj)) {
|
|
219
|
+
return obj.map((item) => unescapeValue(item));
|
|
220
|
+
}
|
|
221
|
+
return obj;
|
|
222
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel key used to mark escaped user objects during serialization.
|
|
3
|
+
*
|
|
4
|
+
* When a plain object contains 'lc' key (which could be confused with LC objects),
|
|
5
|
+
* we wrap it as `{"__lc_escaped__": {...original...}}`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LC_ESCAPED_KEY = "__lc_escaped__";
|
|
8
|
+
/**
|
|
9
|
+
* Check if an object needs escaping to prevent confusion with LC objects.
|
|
10
|
+
*
|
|
11
|
+
* An object needs escaping if:
|
|
12
|
+
* 1. It has an `'lc'` key (could be confused with LC serialization format)
|
|
13
|
+
* 2. It has only the escape key (would be mistaken for an escaped object)
|
|
14
|
+
*/
|
|
15
|
+
export declare function needsEscaping(obj: Record<string, unknown>): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Wrap an object in the escape marker.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* {"key": "value"} // becomes {"__lc_escaped__": {"key": "value"}}
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function escapeObject(obj: Record<string, unknown>): Record<string, unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* Check if an object is an escaped user object.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* {"__lc_escaped__": {...}} // is an escaped object
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function isEscapedObject(obj: Record<string, unknown>): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Interface for objects that can be serialized.
|
|
36
|
+
* This is a duck-typed interface to avoid circular imports.
|
|
37
|
+
*/
|
|
38
|
+
interface SerializableLike {
|
|
39
|
+
lc_serializable: boolean;
|
|
40
|
+
lc_secrets?: Record<string, string>;
|
|
41
|
+
toJSON(): {
|
|
42
|
+
lc: number;
|
|
43
|
+
type: string;
|
|
44
|
+
id: string[];
|
|
45
|
+
kwargs?: Record<string, unknown>;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Serialize a value with escaping of user objects.
|
|
50
|
+
*
|
|
51
|
+
* Called recursively on kwarg values to escape any plain objects that could be
|
|
52
|
+
* confused with LC objects.
|
|
53
|
+
*
|
|
54
|
+
* @param obj - The value to serialize.
|
|
55
|
+
* @returns The serialized value with user objects escaped as needed.
|
|
56
|
+
*/
|
|
57
|
+
export declare function serializeValue(obj: unknown): unknown;
|
|
58
|
+
/**
|
|
59
|
+
* Serialize a `Serializable` object with escaping of user data in kwargs.
|
|
60
|
+
*
|
|
61
|
+
* @param obj - The `Serializable` object to serialize.
|
|
62
|
+
* @returns The serialized object with user data in kwargs escaped as needed.
|
|
63
|
+
*
|
|
64
|
+
* @remarks
|
|
65
|
+
* Kwargs values are processed with `serializeValue` to escape user data (like
|
|
66
|
+
* metadata) that contains `'lc'` keys. Secret fields (from `lc_secrets`) are
|
|
67
|
+
* skipped because `toJSON()` replaces their values with secret markers.
|
|
68
|
+
*/
|
|
69
|
+
export declare function serializeLcObject(obj: SerializableLike): {
|
|
70
|
+
lc: number;
|
|
71
|
+
type: string;
|
|
72
|
+
id: string[];
|
|
73
|
+
kwargs?: Record<string, unknown>;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Escape a value if it needs escaping (contains `lc` key).
|
|
77
|
+
*
|
|
78
|
+
* This is a simpler version of `serializeValue` that doesn't handle Serializable
|
|
79
|
+
* objects - it's meant to be called on kwargs values that have already been
|
|
80
|
+
* processed by `toJSON()`.
|
|
81
|
+
*
|
|
82
|
+
* @param value - The value to potentially escape.
|
|
83
|
+
* @returns The value with any `lc`-containing objects wrapped in escape markers.
|
|
84
|
+
*/
|
|
85
|
+
export declare function escapeIfNeeded(value: unknown): unknown;
|
|
86
|
+
/**
|
|
87
|
+
* Unescape a value, processing escape markers in object values and arrays.
|
|
88
|
+
*
|
|
89
|
+
* When an escaped object is encountered (`{"__lc_escaped__": ...}`), it's
|
|
90
|
+
* unwrapped and the contents are returned AS-IS (no further processing).
|
|
91
|
+
* The contents represent user data that should not be modified.
|
|
92
|
+
*
|
|
93
|
+
* For regular objects and arrays, we recurse to find any nested escape markers.
|
|
94
|
+
*
|
|
95
|
+
* @param obj - The value to unescape.
|
|
96
|
+
* @returns The unescaped value.
|
|
97
|
+
*/
|
|
98
|
+
export declare function unescapeValue(obj: unknown): unknown;
|
|
99
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel key used to mark escaped user objects during serialization.
|
|
3
|
+
*
|
|
4
|
+
* When a plain object contains 'lc' key (which could be confused with LC objects),
|
|
5
|
+
* we wrap it as `{"__lc_escaped__": {...original...}}`.
|
|
6
|
+
*/
|
|
7
|
+
export const LC_ESCAPED_KEY = "__lc_escaped__";
|
|
8
|
+
/**
|
|
9
|
+
* Check if an object needs escaping to prevent confusion with LC objects.
|
|
10
|
+
*
|
|
11
|
+
* An object needs escaping if:
|
|
12
|
+
* 1. It has an `'lc'` key (could be confused with LC serialization format)
|
|
13
|
+
* 2. It has only the escape key (would be mistaken for an escaped object)
|
|
14
|
+
*/
|
|
15
|
+
export function needsEscaping(obj) {
|
|
16
|
+
return ("lc" in obj || (Object.keys(obj).length === 1 && LC_ESCAPED_KEY in obj));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Wrap an object in the escape marker.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* {"key": "value"} // becomes {"__lc_escaped__": {"key": "value"}}
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function escapeObject(obj) {
|
|
27
|
+
return { [LC_ESCAPED_KEY]: obj };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Check if an object is an escaped user object.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* {"__lc_escaped__": {...}} // is an escaped object
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export function isEscapedObject(obj) {
|
|
38
|
+
return Object.keys(obj).length === 1 && LC_ESCAPED_KEY in obj;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Check if an object looks like a Serializable instance (duck typing).
|
|
42
|
+
*/
|
|
43
|
+
function isSerializableLike(obj) {
|
|
44
|
+
return (obj !== null &&
|
|
45
|
+
typeof obj === "object" &&
|
|
46
|
+
"lc_serializable" in obj &&
|
|
47
|
+
typeof obj.toJSON === "function");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Create a "not_implemented" serialization result for objects that cannot be serialized.
|
|
51
|
+
*/
|
|
52
|
+
function createNotImplemented(obj) {
|
|
53
|
+
let id;
|
|
54
|
+
if (obj !== null && typeof obj === "object") {
|
|
55
|
+
if ("lc_id" in obj && Array.isArray(obj.lc_id)) {
|
|
56
|
+
id = obj.lc_id;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
id = [obj.constructor?.name ?? "Object"];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
id = [typeof obj];
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
lc: 1,
|
|
67
|
+
type: "not_implemented",
|
|
68
|
+
id,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Serialize a value with escaping of user objects.
|
|
73
|
+
*
|
|
74
|
+
* Called recursively on kwarg values to escape any plain objects that could be
|
|
75
|
+
* confused with LC objects.
|
|
76
|
+
*
|
|
77
|
+
* @param obj - The value to serialize.
|
|
78
|
+
* @returns The serialized value with user objects escaped as needed.
|
|
79
|
+
*/
|
|
80
|
+
export function serializeValue(obj) {
|
|
81
|
+
if (isSerializableLike(obj)) {
|
|
82
|
+
// This is an LC object - serialize it properly (not escaped)
|
|
83
|
+
return serializeLcObject(obj);
|
|
84
|
+
}
|
|
85
|
+
if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
|
|
86
|
+
const record = obj;
|
|
87
|
+
// Check if object needs escaping BEFORE recursing into values.
|
|
88
|
+
// If it needs escaping, wrap it as-is - the contents are user data that
|
|
89
|
+
// will be returned as-is during deserialization (no instantiation).
|
|
90
|
+
// This prevents re-escaping of already-escaped nested content.
|
|
91
|
+
if (needsEscaping(record)) {
|
|
92
|
+
return escapeObject(record);
|
|
93
|
+
}
|
|
94
|
+
// Safe object (no 'lc' key) - recurse into values
|
|
95
|
+
const result = {};
|
|
96
|
+
for (const [key, value] of Object.entries(record)) {
|
|
97
|
+
result[key] = serializeValue(value);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(obj)) {
|
|
102
|
+
return obj.map((item) => serializeValue(item));
|
|
103
|
+
}
|
|
104
|
+
if (typeof obj === "string" ||
|
|
105
|
+
typeof obj === "number" ||
|
|
106
|
+
typeof obj === "boolean" ||
|
|
107
|
+
obj === null) {
|
|
108
|
+
return obj;
|
|
109
|
+
}
|
|
110
|
+
// Non-JSON-serializable object (Date, custom objects, etc.)
|
|
111
|
+
return createNotImplemented(obj);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Serialize a `Serializable` object with escaping of user data in kwargs.
|
|
115
|
+
*
|
|
116
|
+
* @param obj - The `Serializable` object to serialize.
|
|
117
|
+
* @returns The serialized object with user data in kwargs escaped as needed.
|
|
118
|
+
*
|
|
119
|
+
* @remarks
|
|
120
|
+
* Kwargs values are processed with `serializeValue` to escape user data (like
|
|
121
|
+
* metadata) that contains `'lc'` keys. Secret fields (from `lc_secrets`) are
|
|
122
|
+
* skipped because `toJSON()` replaces their values with secret markers.
|
|
123
|
+
*/
|
|
124
|
+
export function serializeLcObject(obj) {
|
|
125
|
+
// Secret fields are handled by toJSON() - it replaces values with secret markers
|
|
126
|
+
const secretFields = new Set(Object.keys(obj.lc_secrets ?? {}));
|
|
127
|
+
const serialized = { ...obj.toJSON() };
|
|
128
|
+
// Process kwargs to escape user data that could be confused with LC objects
|
|
129
|
+
// Skip secret fields - toJSON() already converted them to secret markers
|
|
130
|
+
if (serialized.type === "constructor" && serialized.kwargs) {
|
|
131
|
+
const newKwargs = {};
|
|
132
|
+
for (const [key, value] of Object.entries(serialized.kwargs)) {
|
|
133
|
+
if (secretFields.has(key)) {
|
|
134
|
+
newKwargs[key] = value;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
newKwargs[key] = serializeValue(value);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
serialized.kwargs = newKwargs;
|
|
141
|
+
}
|
|
142
|
+
return serialized;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Escape a value if it needs escaping (contains `lc` key).
|
|
146
|
+
*
|
|
147
|
+
* This is a simpler version of `serializeValue` that doesn't handle Serializable
|
|
148
|
+
* objects - it's meant to be called on kwargs values that have already been
|
|
149
|
+
* processed by `toJSON()`.
|
|
150
|
+
*
|
|
151
|
+
* @param value - The value to potentially escape.
|
|
152
|
+
* @returns The value with any `lc`-containing objects wrapped in escape markers.
|
|
153
|
+
*/
|
|
154
|
+
export function escapeIfNeeded(value) {
|
|
155
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
156
|
+
// Preserve Serializable objects - they have their own toJSON() that will be
|
|
157
|
+
// called by JSON.stringify. We don't want to convert them to plain objects.
|
|
158
|
+
if (isSerializableLike(value)) {
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
const record = value;
|
|
162
|
+
// Check if object needs escaping BEFORE recursing into values.
|
|
163
|
+
// If it needs escaping, wrap it as-is - the contents are user data that
|
|
164
|
+
// will be returned as-is during deserialization (no instantiation).
|
|
165
|
+
if (needsEscaping(record)) {
|
|
166
|
+
return escapeObject(record);
|
|
167
|
+
}
|
|
168
|
+
// Safe object (no 'lc' key) - recurse into values
|
|
169
|
+
const result = {};
|
|
170
|
+
for (const [key, val] of Object.entries(record)) {
|
|
171
|
+
result[key] = escapeIfNeeded(val);
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
return value.map((item) => escapeIfNeeded(item));
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Unescape a value, processing escape markers in object values and arrays.
|
|
182
|
+
*
|
|
183
|
+
* When an escaped object is encountered (`{"__lc_escaped__": ...}`), it's
|
|
184
|
+
* unwrapped and the contents are returned AS-IS (no further processing).
|
|
185
|
+
* The contents represent user data that should not be modified.
|
|
186
|
+
*
|
|
187
|
+
* For regular objects and arrays, we recurse to find any nested escape markers.
|
|
188
|
+
*
|
|
189
|
+
* @param obj - The value to unescape.
|
|
190
|
+
* @returns The unescaped value.
|
|
191
|
+
*/
|
|
192
|
+
export function unescapeValue(obj) {
|
|
193
|
+
if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
|
|
194
|
+
const record = obj;
|
|
195
|
+
if (isEscapedObject(record)) {
|
|
196
|
+
// Unwrap and return the user data as-is (no further unescaping).
|
|
197
|
+
// The contents are user data that may contain more escape keys,
|
|
198
|
+
// but those are part of the user's actual data.
|
|
199
|
+
return record[LC_ESCAPED_KEY];
|
|
200
|
+
}
|
|
201
|
+
// Regular object - recurse into values to find nested escape markers
|
|
202
|
+
const result = {};
|
|
203
|
+
for (const [key, value] of Object.entries(record)) {
|
|
204
|
+
result[key] = unescapeValue(value);
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
if (Array.isArray(obj)) {
|
|
209
|
+
return obj.map((item) => unescapeValue(item));
|
|
210
|
+
}
|
|
211
|
+
return obj;
|
|
212
|
+
}
|
package/dist/messages/ai.cjs
CHANGED
|
@@ -131,7 +131,8 @@ class AIMessageChunk extends base_js_1.BaseMessageChunk {
|
|
|
131
131
|
tool_call_chunks: [],
|
|
132
132
|
};
|
|
133
133
|
}
|
|
134
|
-
else if (fields.tool_call_chunks === undefined
|
|
134
|
+
else if (fields.tool_call_chunks === undefined ||
|
|
135
|
+
fields.tool_call_chunks.length === 0) {
|
|
135
136
|
initParams = {
|
|
136
137
|
...fields,
|
|
137
138
|
tool_calls: fields.tool_calls ?? [],
|
|
@@ -143,7 +144,8 @@ class AIMessageChunk extends base_js_1.BaseMessageChunk {
|
|
|
143
144
|
};
|
|
144
145
|
}
|
|
145
146
|
else {
|
|
146
|
-
const
|
|
147
|
+
const toolCallChunks = fields.tool_call_chunks ?? [];
|
|
148
|
+
const groupedToolCallChunks = toolCallChunks.reduce((acc, chunk) => {
|
|
147
149
|
const matchedChunkIndex = acc.findIndex(([match]) => {
|
|
148
150
|
// If chunk has an id and index, match if both are present
|
|
149
151
|
if ("id" in chunk &&
|
package/dist/messages/ai.js
CHANGED
|
@@ -125,7 +125,8 @@ export class AIMessageChunk extends BaseMessageChunk {
|
|
|
125
125
|
tool_call_chunks: [],
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
-
else if (fields.tool_call_chunks === undefined
|
|
128
|
+
else if (fields.tool_call_chunks === undefined ||
|
|
129
|
+
fields.tool_call_chunks.length === 0) {
|
|
129
130
|
initParams = {
|
|
130
131
|
...fields,
|
|
131
132
|
tool_calls: fields.tool_calls ?? [],
|
|
@@ -137,7 +138,8 @@ export class AIMessageChunk extends BaseMessageChunk {
|
|
|
137
138
|
};
|
|
138
139
|
}
|
|
139
140
|
else {
|
|
140
|
-
const
|
|
141
|
+
const toolCallChunks = fields.tool_call_chunks ?? [];
|
|
142
|
+
const groupedToolCallChunks = toolCallChunks.reduce((acc, chunk) => {
|
|
141
143
|
const matchedChunkIndex = acc.findIndex(([match]) => {
|
|
142
144
|
// If chunk has an id and index, match if both are present
|
|
143
145
|
if ("id" in chunk &&
|