@langchain/langgraph-checkpoint 0.0.11 → 0.0.13
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/base.cjs +2 -0
- package/dist/base.d.ts +1 -1
- package/dist/base.js +3 -1
- package/dist/memory.cjs +1 -4
- package/dist/memory.js +1 -4
- package/dist/serde/types.cjs +3 -1
- package/dist/serde/types.d.ts +2 -0
- package/dist/serde/types.js +2 -0
- package/dist/store/base.cjs +144 -58
- package/dist/store/base.d.ts +262 -54
- package/dist/store/base.js +141 -57
- package/dist/store/memory.cjs +273 -28
- package/dist/store/memory.d.ts +48 -3
- package/dist/store/memory.js +273 -28
- package/dist/store/utils.cjs +261 -0
- package/dist/store/utils.d.ts +26 -0
- package/dist/store/utils.js +254 -0
- package/dist/types.d.ts +2 -1
- package/package.json +2 -2
package/dist/store/memory.js
CHANGED
|
@@ -1,54 +1,148 @@
|
|
|
1
1
|
import { BaseStore, } from "./base.js";
|
|
2
|
+
import { tokenizePath, compareValues, getTextAtPath } from "./utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* In-memory key-value store with optional vector search.
|
|
5
|
+
*
|
|
6
|
+
* A lightweight store implementation using JavaScript Maps. Supports basic
|
|
7
|
+
* key-value operations and vector search when configured with embeddings.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // Basic key-value storage
|
|
12
|
+
* const store = new InMemoryStore();
|
|
13
|
+
* await store.put(["users", "123"], "prefs", { theme: "dark" });
|
|
14
|
+
* const item = await store.get(["users", "123"], "prefs");
|
|
15
|
+
*
|
|
16
|
+
* // Vector search with embeddings
|
|
17
|
+
* import { OpenAIEmbeddings } from "@langchain/openai";
|
|
18
|
+
* const store = new InMemoryStore({
|
|
19
|
+
* index: {
|
|
20
|
+
* dims: 1536,
|
|
21
|
+
* embeddings: new OpenAIEmbeddings({ modelName: "text-embedding-3-small" }),
|
|
22
|
+
* }
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // Store documents
|
|
26
|
+
* await store.put(["docs"], "doc1", { text: "Python tutorial" });
|
|
27
|
+
* await store.put(["docs"], "doc2", { text: "TypeScript guide" });
|
|
28
|
+
*
|
|
29
|
+
* // Search by similarity
|
|
30
|
+
* const results = await store.search(["docs"], { query: "python programming" });
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* @warning This store keeps all data in memory. Data is lost when the process exits.
|
|
34
|
+
* For persistence, use a database-backed store.
|
|
35
|
+
*/
|
|
2
36
|
export class InMemoryStore extends BaseStore {
|
|
3
|
-
constructor() {
|
|
4
|
-
super(
|
|
37
|
+
constructor(options) {
|
|
38
|
+
super();
|
|
5
39
|
Object.defineProperty(this, "data", {
|
|
6
40
|
enumerable: true,
|
|
7
41
|
configurable: true,
|
|
8
42
|
writable: true,
|
|
9
43
|
value: new Map()
|
|
10
44
|
});
|
|
45
|
+
// Namespace -> Key -> Path/field -> Vector
|
|
46
|
+
Object.defineProperty(this, "vectors", {
|
|
47
|
+
enumerable: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
writable: true,
|
|
50
|
+
value: new Map()
|
|
51
|
+
});
|
|
52
|
+
Object.defineProperty(this, "_indexConfig", {
|
|
53
|
+
enumerable: true,
|
|
54
|
+
configurable: true,
|
|
55
|
+
writable: true,
|
|
56
|
+
value: void 0
|
|
57
|
+
});
|
|
58
|
+
if (options?.index) {
|
|
59
|
+
this._indexConfig = {
|
|
60
|
+
...options.index,
|
|
61
|
+
__tokenizedFields: (options.index.fields ?? ["$"]).map((p) => [
|
|
62
|
+
p,
|
|
63
|
+
p === "$" ? [p] : tokenizePath(p),
|
|
64
|
+
]),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
11
67
|
}
|
|
12
68
|
async batch(operations) {
|
|
13
69
|
const results = [];
|
|
14
|
-
|
|
70
|
+
const putOps = new Map();
|
|
71
|
+
const searchOps = new Map();
|
|
72
|
+
// First pass - handle gets and prepare search/put operations
|
|
73
|
+
for (let i = 0; i < operations.length; i += 1) {
|
|
74
|
+
const op = operations[i];
|
|
15
75
|
if ("key" in op && "namespace" in op && !("value" in op)) {
|
|
16
76
|
// GetOperation
|
|
17
77
|
results.push(this.getOperation(op));
|
|
18
78
|
}
|
|
19
79
|
else if ("namespacePrefix" in op) {
|
|
20
80
|
// SearchOperation
|
|
21
|
-
|
|
81
|
+
const candidates = this.filterItems(op);
|
|
82
|
+
searchOps.set(i, [op, candidates]);
|
|
83
|
+
results.push(null);
|
|
22
84
|
}
|
|
23
85
|
else if ("value" in op) {
|
|
24
86
|
// PutOperation
|
|
25
|
-
|
|
87
|
+
const key = `${op.namespace.join(":")}:${op.key}`;
|
|
88
|
+
putOps.set(key, op);
|
|
89
|
+
results.push(null);
|
|
26
90
|
}
|
|
27
91
|
else if ("matchConditions" in op) {
|
|
28
92
|
// ListNamespacesOperation
|
|
29
93
|
results.push(this.listNamespacesOperation(op));
|
|
30
94
|
}
|
|
31
95
|
}
|
|
32
|
-
|
|
96
|
+
// Handle search operations with embeddings
|
|
97
|
+
if (searchOps.size > 0) {
|
|
98
|
+
if (this._indexConfig?.embeddings) {
|
|
99
|
+
const queries = new Set();
|
|
100
|
+
for (const [op] of searchOps.values()) {
|
|
101
|
+
if (op.query)
|
|
102
|
+
queries.add(op.query);
|
|
103
|
+
}
|
|
104
|
+
// Get embeddings for all queries
|
|
105
|
+
const queryEmbeddings = queries.size > 0
|
|
106
|
+
? await Promise.all(Array.from(queries).map((q) => this._indexConfig.embeddings.embedQuery(q)))
|
|
107
|
+
: [];
|
|
108
|
+
const queryVectors = Object.fromEntries(Array.from(queries).map((q, i) => [q, queryEmbeddings[i]]));
|
|
109
|
+
// Process each search operation
|
|
110
|
+
for (const [i, [op, candidates]] of searchOps.entries()) {
|
|
111
|
+
if (op.query && queryVectors[op.query]) {
|
|
112
|
+
const queryVector = queryVectors[op.query];
|
|
113
|
+
const scoredResults = this.scoreResults(candidates, queryVector, op.offset ?? 0, op.limit ?? 10);
|
|
114
|
+
results[i] = scoredResults;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
results[i] = this.paginateResults(candidates.map((item) => ({ ...item, score: undefined })), op.offset ?? 0, op.limit ?? 10);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// No embeddings - just paginate the filtered results
|
|
123
|
+
for (const [i, [op, candidates]] of searchOps.entries()) {
|
|
124
|
+
results[i] = this.paginateResults(candidates.map((item) => ({ ...item, score: undefined })), op.offset ?? 0, op.limit ?? 10);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Handle put operations with embeddings
|
|
129
|
+
if (putOps.size > 0 && this._indexConfig?.embeddings) {
|
|
130
|
+
const toEmbed = this.extractTexts(Array.from(putOps.values()));
|
|
131
|
+
if (Object.keys(toEmbed).length > 0) {
|
|
132
|
+
const embeddings = await this._indexConfig.embeddings.embedDocuments(Object.keys(toEmbed));
|
|
133
|
+
this.insertVectors(toEmbed, embeddings);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Apply all put operations
|
|
137
|
+
for (const op of putOps.values()) {
|
|
138
|
+
this.putOperation(op);
|
|
139
|
+
}
|
|
140
|
+
return results;
|
|
33
141
|
}
|
|
34
142
|
getOperation(op) {
|
|
35
143
|
const namespaceKey = op.namespace.join(":");
|
|
36
144
|
const item = this.data.get(namespaceKey)?.get(op.key);
|
|
37
|
-
return item
|
|
38
|
-
}
|
|
39
|
-
searchOperation(op) {
|
|
40
|
-
const candidates = [];
|
|
41
|
-
for (const [namespace, items] of this.data.entries()) {
|
|
42
|
-
if (namespace.startsWith(op.namespacePrefix.join(":"))) {
|
|
43
|
-
candidates.push(...items.values());
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
let filteredCandidates = candidates;
|
|
47
|
-
if (op.filter) {
|
|
48
|
-
filteredCandidates = candidates.filter((item) => Object.entries(op.filter).every(([key, value]) => item.value[key] === value));
|
|
49
|
-
}
|
|
50
|
-
const searchResults = filteredCandidates.slice(op.offset || 0, (op.offset || 0) + (op.limit || 10));
|
|
51
|
-
return searchResults;
|
|
145
|
+
return item ?? null;
|
|
52
146
|
}
|
|
53
147
|
putOperation(op) {
|
|
54
148
|
const namespaceKey = op.namespace.join(":");
|
|
@@ -87,28 +181,179 @@ export class InMemoryStore extends BaseStore {
|
|
|
87
181
|
namespaces = Array.from(new Set(namespaces.map((ns) => ns.slice(0, op.maxDepth).join(":")))).map((ns) => ns.split(":"));
|
|
88
182
|
}
|
|
89
183
|
namespaces.sort((a, b) => a.join(":").localeCompare(b.join(":")));
|
|
90
|
-
|
|
91
|
-
return paginatedNamespaces;
|
|
184
|
+
return namespaces.slice(op.offset ?? 0, (op.offset ?? 0) + (op.limit ?? namespaces.length));
|
|
92
185
|
}
|
|
93
186
|
doesMatch(matchCondition, key) {
|
|
94
187
|
const { matchType, path } = matchCondition;
|
|
95
|
-
if (key.length < path.length) {
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
188
|
if (matchType === "prefix") {
|
|
189
|
+
if (path.length > key.length)
|
|
190
|
+
return false;
|
|
99
191
|
return path.every((pElem, index) => {
|
|
100
192
|
const kElem = key[index];
|
|
101
193
|
return pElem === "*" || kElem === pElem;
|
|
102
194
|
});
|
|
103
195
|
}
|
|
104
196
|
else if (matchType === "suffix") {
|
|
105
|
-
|
|
106
|
-
|
|
197
|
+
if (path.length > key.length)
|
|
198
|
+
return false;
|
|
199
|
+
return path.every((pElem, index) => {
|
|
200
|
+
const kElem = key[key.length - path.length + index];
|
|
107
201
|
return pElem === "*" || kElem === pElem;
|
|
108
202
|
});
|
|
109
203
|
}
|
|
110
204
|
throw new Error(`Unsupported match type: ${matchType}`);
|
|
111
205
|
}
|
|
206
|
+
filterItems(op) {
|
|
207
|
+
const candidates = [];
|
|
208
|
+
for (const [namespace, items] of this.data.entries()) {
|
|
209
|
+
if (namespace.startsWith(op.namespacePrefix.join(":"))) {
|
|
210
|
+
candidates.push(...items.values());
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
let filteredCandidates = candidates;
|
|
214
|
+
if (op.filter) {
|
|
215
|
+
filteredCandidates = candidates.filter((item) => Object.entries(op.filter).every(([key, value]) => compareValues(item.value[key], value)));
|
|
216
|
+
}
|
|
217
|
+
return filteredCandidates;
|
|
218
|
+
}
|
|
219
|
+
scoreResults(candidates, queryVector, offset = 0, limit = 10) {
|
|
220
|
+
const flatItems = [];
|
|
221
|
+
const flatVectors = [];
|
|
222
|
+
const scoreless = [];
|
|
223
|
+
for (const item of candidates) {
|
|
224
|
+
const vectors = this.getVectors(item);
|
|
225
|
+
if (vectors.length) {
|
|
226
|
+
for (const vector of vectors) {
|
|
227
|
+
flatItems.push(item);
|
|
228
|
+
flatVectors.push(vector);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
scoreless.push(item);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const scores = this.cosineSimilarity(queryVector, flatVectors);
|
|
236
|
+
const sortedResults = scores
|
|
237
|
+
.map((score, i) => [score, flatItems[i]])
|
|
238
|
+
.sort((a, b) => b[0] - a[0]);
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
const kept = [];
|
|
241
|
+
for (const [score, item] of sortedResults) {
|
|
242
|
+
const key = `${item.namespace.join(":")}:${item.key}`;
|
|
243
|
+
if (seen.has(key))
|
|
244
|
+
continue;
|
|
245
|
+
const ix = seen.size;
|
|
246
|
+
if (ix >= offset + limit)
|
|
247
|
+
break;
|
|
248
|
+
if (ix < offset) {
|
|
249
|
+
seen.add(key);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
seen.add(key);
|
|
253
|
+
kept.push([score, item]);
|
|
254
|
+
}
|
|
255
|
+
if (scoreless.length && kept.length < limit) {
|
|
256
|
+
for (const item of scoreless.slice(0, limit - kept.length)) {
|
|
257
|
+
const key = `${item.namespace.join(":")}:${item.key}`;
|
|
258
|
+
if (!seen.has(key)) {
|
|
259
|
+
seen.add(key);
|
|
260
|
+
kept.push([undefined, item]);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return kept.map(([score, item]) => ({
|
|
265
|
+
...item,
|
|
266
|
+
score,
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
paginateResults(results, offset, limit) {
|
|
270
|
+
return results.slice(offset, offset + limit);
|
|
271
|
+
}
|
|
272
|
+
extractTexts(ops) {
|
|
273
|
+
if (!ops.length || !this._indexConfig) {
|
|
274
|
+
return {};
|
|
275
|
+
}
|
|
276
|
+
const toEmbed = {};
|
|
277
|
+
for (const op of ops) {
|
|
278
|
+
if (op.value !== null && op.index !== false) {
|
|
279
|
+
const paths = op.index === null || op.index === undefined
|
|
280
|
+
? this._indexConfig.__tokenizedFields ?? []
|
|
281
|
+
: op.index.map((ix) => [ix, tokenizePath(ix)]);
|
|
282
|
+
for (const [path, field] of paths) {
|
|
283
|
+
const texts = getTextAtPath(op.value, field);
|
|
284
|
+
if (texts.length) {
|
|
285
|
+
if (texts.length > 1) {
|
|
286
|
+
texts.forEach((text, i) => {
|
|
287
|
+
if (!toEmbed[text])
|
|
288
|
+
toEmbed[text] = [];
|
|
289
|
+
toEmbed[text].push([op.namespace, op.key, `${path}.${i}`]);
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
if (!toEmbed[texts[0]])
|
|
294
|
+
toEmbed[texts[0]] = [];
|
|
295
|
+
toEmbed[texts[0]].push([op.namespace, op.key, path]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return toEmbed;
|
|
302
|
+
}
|
|
303
|
+
insertVectors(texts, embeddings) {
|
|
304
|
+
for (const [text, metadata] of Object.entries(texts)) {
|
|
305
|
+
const embedding = embeddings.shift();
|
|
306
|
+
if (!embedding) {
|
|
307
|
+
throw new Error(`No embedding found for text: ${text}`);
|
|
308
|
+
}
|
|
309
|
+
for (const [namespace, key, field] of metadata) {
|
|
310
|
+
const namespaceKey = namespace.join(":");
|
|
311
|
+
if (!this.vectors.has(namespaceKey)) {
|
|
312
|
+
this.vectors.set(namespaceKey, new Map());
|
|
313
|
+
}
|
|
314
|
+
const namespaceMap = this.vectors.get(namespaceKey);
|
|
315
|
+
if (!namespaceMap.has(key)) {
|
|
316
|
+
namespaceMap.set(key, new Map());
|
|
317
|
+
}
|
|
318
|
+
const itemMap = namespaceMap.get(key);
|
|
319
|
+
itemMap.set(field, embedding);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
getVectors(item) {
|
|
324
|
+
const namespaceKey = item.namespace.join(":");
|
|
325
|
+
const itemKey = item.key;
|
|
326
|
+
if (!this.vectors.has(namespaceKey)) {
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
const namespaceMap = this.vectors.get(namespaceKey);
|
|
330
|
+
if (!namespaceMap.has(itemKey)) {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
const itemMap = namespaceMap.get(itemKey);
|
|
334
|
+
const vectors = Array.from(itemMap.values());
|
|
335
|
+
if (!vectors.length) {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
return vectors;
|
|
339
|
+
}
|
|
340
|
+
cosineSimilarity(X, Y) {
|
|
341
|
+
if (!Y.length)
|
|
342
|
+
return [];
|
|
343
|
+
// Calculate dot products for all vectors at once
|
|
344
|
+
const dotProducts = Y.map((vector) => vector.reduce((acc, val, i) => acc + val * X[i], 0));
|
|
345
|
+
// Calculate magnitudes
|
|
346
|
+
const magnitude1 = Math.sqrt(X.reduce((acc, val) => acc + val * val, 0));
|
|
347
|
+
const magnitudes2 = Y.map((vector) => Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0)));
|
|
348
|
+
// Calculate similarities
|
|
349
|
+
return dotProducts.map((dot, i) => {
|
|
350
|
+
const magnitude2 = magnitudes2[i];
|
|
351
|
+
return magnitude1 && magnitude2 ? dot / (magnitude1 * magnitude2) : 0;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
get indexConfig() {
|
|
355
|
+
return this._indexConfig;
|
|
356
|
+
}
|
|
112
357
|
}
|
|
113
358
|
/** @deprecated Alias for InMemoryStore */
|
|
114
359
|
export class MemoryStore extends InMemoryStore {
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cosineSimilarity = exports.getTextAtPath = exports.compareValues = exports.tokenizePath = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Tokenize a JSON path into parts.
|
|
6
|
+
* @example
|
|
7
|
+
* tokenizePath("metadata.title") // -> ["metadata", "title"]
|
|
8
|
+
* tokenizePath("chapters[*].content") // -> ["chapters[*]", "content"]
|
|
9
|
+
*/
|
|
10
|
+
function tokenizePath(path) {
|
|
11
|
+
if (!path) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const tokens = [];
|
|
15
|
+
let current = [];
|
|
16
|
+
let i = 0;
|
|
17
|
+
while (i < path.length) {
|
|
18
|
+
const char = path[i];
|
|
19
|
+
if (char === "[") {
|
|
20
|
+
// Handle array index
|
|
21
|
+
if (current.length) {
|
|
22
|
+
tokens.push(current.join(""));
|
|
23
|
+
current = [];
|
|
24
|
+
}
|
|
25
|
+
let bracketCount = 1;
|
|
26
|
+
const indexChars = ["["];
|
|
27
|
+
i += 1;
|
|
28
|
+
while (i < path.length && bracketCount > 0) {
|
|
29
|
+
if (path[i] === "[") {
|
|
30
|
+
bracketCount += 1;
|
|
31
|
+
}
|
|
32
|
+
else if (path[i] === "]") {
|
|
33
|
+
bracketCount -= 1;
|
|
34
|
+
}
|
|
35
|
+
indexChars.push(path[i]);
|
|
36
|
+
i += 1;
|
|
37
|
+
}
|
|
38
|
+
tokens.push(indexChars.join(""));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
else if (char === "{") {
|
|
42
|
+
// Handle multi-field selection
|
|
43
|
+
if (current.length) {
|
|
44
|
+
tokens.push(current.join(""));
|
|
45
|
+
current = [];
|
|
46
|
+
}
|
|
47
|
+
let braceCount = 1;
|
|
48
|
+
const fieldChars = ["{"];
|
|
49
|
+
i += 1;
|
|
50
|
+
while (i < path.length && braceCount > 0) {
|
|
51
|
+
if (path[i] === "{") {
|
|
52
|
+
braceCount += 1;
|
|
53
|
+
}
|
|
54
|
+
else if (path[i] === "}") {
|
|
55
|
+
braceCount -= 1;
|
|
56
|
+
}
|
|
57
|
+
fieldChars.push(path[i]);
|
|
58
|
+
i += 1;
|
|
59
|
+
}
|
|
60
|
+
tokens.push(fieldChars.join(""));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
else if (char === ".") {
|
|
64
|
+
// Handle regular field
|
|
65
|
+
if (current.length) {
|
|
66
|
+
tokens.push(current.join(""));
|
|
67
|
+
current = [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
current.push(char);
|
|
72
|
+
}
|
|
73
|
+
i += 1;
|
|
74
|
+
}
|
|
75
|
+
if (current.length) {
|
|
76
|
+
tokens.push(current.join(""));
|
|
77
|
+
}
|
|
78
|
+
return tokens;
|
|
79
|
+
}
|
|
80
|
+
exports.tokenizePath = tokenizePath;
|
|
81
|
+
/**
|
|
82
|
+
* Type guard to check if an object is a FilterOperators
|
|
83
|
+
*/
|
|
84
|
+
function isFilterOperators(obj) {
|
|
85
|
+
return (typeof obj === "object" &&
|
|
86
|
+
obj !== null &&
|
|
87
|
+
Object.keys(obj).every((key) => key === "$eq" ||
|
|
88
|
+
key === "$ne" ||
|
|
89
|
+
key === "$gt" ||
|
|
90
|
+
key === "$gte" ||
|
|
91
|
+
key === "$lt" ||
|
|
92
|
+
key === "$lte" ||
|
|
93
|
+
key === "$in" ||
|
|
94
|
+
key === "$nin"));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Compare values for filtering, supporting operator-based comparisons.
|
|
98
|
+
*/
|
|
99
|
+
function compareValues(itemValue, filterValue) {
|
|
100
|
+
if (isFilterOperators(filterValue)) {
|
|
101
|
+
const operators = Object.keys(filterValue).filter((k) => k.startsWith("$"));
|
|
102
|
+
return operators.every((op) => {
|
|
103
|
+
const value = filterValue[op];
|
|
104
|
+
switch (op) {
|
|
105
|
+
case "$eq":
|
|
106
|
+
return itemValue === value;
|
|
107
|
+
case "$ne":
|
|
108
|
+
return itemValue !== value;
|
|
109
|
+
case "$gt":
|
|
110
|
+
return Number(itemValue) > Number(value);
|
|
111
|
+
case "$gte":
|
|
112
|
+
return Number(itemValue) >= Number(value);
|
|
113
|
+
case "$lt":
|
|
114
|
+
return Number(itemValue) < Number(value);
|
|
115
|
+
case "$lte":
|
|
116
|
+
return Number(itemValue) <= Number(value);
|
|
117
|
+
case "$in":
|
|
118
|
+
return Array.isArray(value) ? value.includes(itemValue) : false;
|
|
119
|
+
case "$nin":
|
|
120
|
+
return Array.isArray(value) ? !value.includes(itemValue) : true;
|
|
121
|
+
default:
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// If no operators, do a direct comparison
|
|
127
|
+
return itemValue === filterValue;
|
|
128
|
+
}
|
|
129
|
+
exports.compareValues = compareValues;
|
|
130
|
+
/**
|
|
131
|
+
* Extract text from a value at a specific JSON path.
|
|
132
|
+
*
|
|
133
|
+
* Supports:
|
|
134
|
+
* - Simple paths: "field1.field2"
|
|
135
|
+
* - Array indexing: "[0]", "[*]", "[-1]"
|
|
136
|
+
* - Wildcards: "*"
|
|
137
|
+
* - Multi-field selection: "{field1,field2}"
|
|
138
|
+
* - Nested paths in multi-field: "{field1,nested.field2}"
|
|
139
|
+
*/
|
|
140
|
+
function getTextAtPath(obj, path) {
|
|
141
|
+
if (!path || path === "$") {
|
|
142
|
+
return [JSON.stringify(obj, null, 2)];
|
|
143
|
+
}
|
|
144
|
+
const tokens = Array.isArray(path) ? path : tokenizePath(path);
|
|
145
|
+
function extractFromObj(obj, tokens, pos) {
|
|
146
|
+
if (pos >= tokens.length) {
|
|
147
|
+
if (typeof obj === "string" ||
|
|
148
|
+
typeof obj === "number" ||
|
|
149
|
+
typeof obj === "boolean") {
|
|
150
|
+
return [String(obj)];
|
|
151
|
+
}
|
|
152
|
+
if (obj === null || obj === undefined) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
if (Array.isArray(obj) || typeof obj === "object") {
|
|
156
|
+
return [JSON.stringify(obj, null, 2)];
|
|
157
|
+
}
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
const token = tokens[pos];
|
|
161
|
+
const results = [];
|
|
162
|
+
if (pos === 0 && token === "$") {
|
|
163
|
+
results.push(JSON.stringify(obj, null, 2));
|
|
164
|
+
}
|
|
165
|
+
if (token.startsWith("[") && token.endsWith("]")) {
|
|
166
|
+
if (!Array.isArray(obj))
|
|
167
|
+
return [];
|
|
168
|
+
const index = token.slice(1, -1);
|
|
169
|
+
if (index === "*") {
|
|
170
|
+
for (const item of obj) {
|
|
171
|
+
results.push(...extractFromObj(item, tokens, pos + 1));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
try {
|
|
176
|
+
let idx = parseInt(index, 10);
|
|
177
|
+
if (idx < 0) {
|
|
178
|
+
idx = obj.length + idx;
|
|
179
|
+
}
|
|
180
|
+
if (idx >= 0 && idx < obj.length) {
|
|
181
|
+
results.push(...extractFromObj(obj[idx], tokens, pos + 1));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (token.startsWith("{") && token.endsWith("}")) {
|
|
190
|
+
if (typeof obj !== "object" || obj === null)
|
|
191
|
+
return [];
|
|
192
|
+
const fields = token
|
|
193
|
+
.slice(1, -1)
|
|
194
|
+
.split(",")
|
|
195
|
+
.map((f) => f.trim());
|
|
196
|
+
for (const field of fields) {
|
|
197
|
+
const nestedTokens = tokenizePath(field);
|
|
198
|
+
if (nestedTokens.length) {
|
|
199
|
+
let currentObj = obj;
|
|
200
|
+
for (const nestedToken of nestedTokens) {
|
|
201
|
+
if (currentObj &&
|
|
202
|
+
typeof currentObj === "object" &&
|
|
203
|
+
nestedToken in currentObj) {
|
|
204
|
+
currentObj = currentObj[nestedToken];
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
currentObj = undefined;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (currentObj !== undefined) {
|
|
212
|
+
if (typeof currentObj === "string" ||
|
|
213
|
+
typeof currentObj === "number" ||
|
|
214
|
+
typeof currentObj === "boolean") {
|
|
215
|
+
results.push(String(currentObj));
|
|
216
|
+
}
|
|
217
|
+
else if (Array.isArray(currentObj) ||
|
|
218
|
+
typeof currentObj === "object") {
|
|
219
|
+
results.push(JSON.stringify(currentObj, null, 2));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else if (token === "*") {
|
|
226
|
+
if (Array.isArray(obj)) {
|
|
227
|
+
for (const item of obj) {
|
|
228
|
+
results.push(...extractFromObj(item, tokens, pos + 1));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if (typeof obj === "object" && obj !== null) {
|
|
232
|
+
for (const value of Object.values(obj)) {
|
|
233
|
+
results.push(...extractFromObj(value, tokens, pos + 1));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
if (typeof obj === "object" && obj !== null && token in obj) {
|
|
239
|
+
results.push(...extractFromObj(obj[token], tokens, pos + 1));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return results;
|
|
243
|
+
}
|
|
244
|
+
return extractFromObj(obj, tokens, 0);
|
|
245
|
+
}
|
|
246
|
+
exports.getTextAtPath = getTextAtPath;
|
|
247
|
+
/**
|
|
248
|
+
* Calculate cosine similarity between two vectors.
|
|
249
|
+
*/
|
|
250
|
+
function cosineSimilarity(vector1, vector2) {
|
|
251
|
+
if (vector1.length !== vector2.length) {
|
|
252
|
+
throw new Error("Vectors must have the same length");
|
|
253
|
+
}
|
|
254
|
+
const dotProduct = vector1.reduce((acc, val, i) => acc + val * vector2[i], 0);
|
|
255
|
+
const magnitude1 = Math.sqrt(vector1.reduce((acc, val) => acc + val * val, 0));
|
|
256
|
+
const magnitude2 = Math.sqrt(vector2.reduce((acc, val) => acc + val * val, 0));
|
|
257
|
+
if (magnitude1 === 0 || magnitude2 === 0)
|
|
258
|
+
return 0;
|
|
259
|
+
return dotProduct / (magnitude1 * magnitude2);
|
|
260
|
+
}
|
|
261
|
+
exports.cosineSimilarity = cosineSimilarity;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenize a JSON path into parts.
|
|
3
|
+
* @example
|
|
4
|
+
* tokenizePath("metadata.title") // -> ["metadata", "title"]
|
|
5
|
+
* tokenizePath("chapters[*].content") // -> ["chapters[*]", "content"]
|
|
6
|
+
*/
|
|
7
|
+
export declare function tokenizePath(path: string): string[];
|
|
8
|
+
/**
|
|
9
|
+
* Compare values for filtering, supporting operator-based comparisons.
|
|
10
|
+
*/
|
|
11
|
+
export declare function compareValues(itemValue: unknown, filterValue: unknown): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Extract text from a value at a specific JSON path.
|
|
14
|
+
*
|
|
15
|
+
* Supports:
|
|
16
|
+
* - Simple paths: "field1.field2"
|
|
17
|
+
* - Array indexing: "[0]", "[*]", "[-1]"
|
|
18
|
+
* - Wildcards: "*"
|
|
19
|
+
* - Multi-field selection: "{field1,field2}"
|
|
20
|
+
* - Nested paths in multi-field: "{field1,nested.field2}"
|
|
21
|
+
*/
|
|
22
|
+
export declare function getTextAtPath(obj: unknown, path: string[] | string): string[];
|
|
23
|
+
/**
|
|
24
|
+
* Calculate cosine similarity between two vectors.
|
|
25
|
+
*/
|
|
26
|
+
export declare function cosineSimilarity(vector1: number[], vector2: number[]): number;
|