@langchain/langgraph-checkpoint
Version:
Library with base interfaces for LangGraph checkpoint savers.
366 lines • 14.3 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.MemoryStore = exports.InMemoryStore = void 0;
const base_js_1 = require("./base.cjs");
const utils_js_1 = require("./utils.cjs");
/**
* In-memory key-value store with optional vector search.
*
* A lightweight store implementation using JavaScript Maps. Supports basic
* key-value operations and vector search when configured with embeddings.
*
* @example
* ```typescript
* // Basic key-value storage
* const store = new InMemoryStore();
* await store.put(["users", "123"], "prefs", { theme: "dark" });
* const item = await store.get(["users", "123"], "prefs");
*
* // Vector search with embeddings
* import { OpenAIEmbeddings } from "@langchain/openai";
* const store = new InMemoryStore({
* index: {
* dims: 1536,
* embeddings: new OpenAIEmbeddings({ modelName: "text-embedding-3-small" }),
* }
* });
*
* // Store documents
* await store.put(["docs"], "doc1", { text: "Python tutorial" });
* await store.put(["docs"], "doc2", { text: "TypeScript guide" });
*
* // Search by similarity
* const results = await store.search(["docs"], { query: "python programming" });
* ```
*
* @warning This store keeps all data in memory. Data is lost when the process exits.
* For persistence, use a database-backed store.
*/
class InMemoryStore extends base_js_1.BaseStore {
constructor(options) {
super();
Object.defineProperty(this, "data", {
enumerable: true,
configurable: true,
writable: true,
value: new Map()
});
// Namespace -> Key -> Path/field -> Vector
Object.defineProperty(this, "vectors", {
enumerable: true,
configurable: true,
writable: true,
value: new Map()
});
Object.defineProperty(this, "_indexConfig", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
if (options?.index) {
this._indexConfig = {
...options.index,
__tokenizedFields: (options.index.fields ?? ["$"]).map((p) => [
p,
p === "$" ? [p] : (0, utils_js_1.tokenizePath)(p),
]),
};
}
}
async batch(operations) {
const results = [];
const putOps = new Map();
const searchOps = new Map();
// First pass - handle gets and prepare search/put operations
for (let i = 0; i < operations.length; i += 1) {
const op = operations[i];
if ("key" in op && "namespace" in op && !("value" in op)) {
// GetOperation
results.push(this.getOperation(op));
}
else if ("namespacePrefix" in op) {
// SearchOperation
const candidates = this.filterItems(op);
searchOps.set(i, [op, candidates]);
results.push(null);
}
else if ("value" in op) {
// PutOperation
const key = `${op.namespace.join(":")}:${op.key}`;
putOps.set(key, op);
results.push(null);
}
else if ("matchConditions" in op) {
// ListNamespacesOperation
results.push(this.listNamespacesOperation(op));
}
}
// Handle search operations with embeddings
if (searchOps.size > 0) {
if (this._indexConfig?.embeddings) {
const queries = new Set();
for (const [op] of searchOps.values()) {
if (op.query)
queries.add(op.query);
}
// Get embeddings for all queries
const queryEmbeddings = queries.size > 0
? await Promise.all(Array.from(queries).map((q) => this._indexConfig.embeddings.embedQuery(q)))
: [];
const queryVectors = Object.fromEntries(Array.from(queries).map((q, i) => [q, queryEmbeddings[i]]));
// Process each search operation
for (const [i, [op, candidates]] of searchOps.entries()) {
if (op.query && queryVectors[op.query]) {
const queryVector = queryVectors[op.query];
const scoredResults = this.scoreResults(candidates, queryVector, op.offset ?? 0, op.limit ?? 10);
results[i] = scoredResults;
}
else {
results[i] = this.paginateResults(candidates.map((item) => ({ ...item, score: undefined })), op.offset ?? 0, op.limit ?? 10);
}
}
}
else {
// No embeddings - just paginate the filtered results
for (const [i, [op, candidates]] of searchOps.entries()) {
results[i] = this.paginateResults(candidates.map((item) => ({ ...item, score: undefined })), op.offset ?? 0, op.limit ?? 10);
}
}
}
// Handle put operations with embeddings
if (putOps.size > 0 && this._indexConfig?.embeddings) {
const toEmbed = this.extractTexts(Array.from(putOps.values()));
if (Object.keys(toEmbed).length > 0) {
const embeddings = await this._indexConfig.embeddings.embedDocuments(Object.keys(toEmbed));
this.insertVectors(toEmbed, embeddings);
}
}
// Apply all put operations
for (const op of putOps.values()) {
this.putOperation(op);
}
return results;
}
getOperation(op) {
const namespaceKey = op.namespace.join(":");
const item = this.data.get(namespaceKey)?.get(op.key);
return item ?? null;
}
putOperation(op) {
const namespaceKey = op.namespace.join(":");
if (!this.data.has(namespaceKey)) {
this.data.set(namespaceKey, new Map());
}
const namespaceMap = this.data.get(namespaceKey);
if (op.value === null) {
namespaceMap.delete(op.key);
}
else {
const now = new Date();
if (namespaceMap.has(op.key)) {
const item = namespaceMap.get(op.key);
item.value = op.value;
item.updatedAt = now;
}
else {
namespaceMap.set(op.key, {
value: op.value,
key: op.key,
namespace: op.namespace,
createdAt: now,
updatedAt: now,
});
}
}
}
listNamespacesOperation(op) {
const allNamespaces = Array.from(this.data.keys()).map((ns) => ns.split(":"));
let namespaces = allNamespaces;
if (op.matchConditions && op.matchConditions.length > 0) {
namespaces = namespaces.filter((ns) => op.matchConditions.every((condition) => this.doesMatch(condition, ns)));
}
if (op.maxDepth !== undefined) {
namespaces = Array.from(new Set(namespaces.map((ns) => ns.slice(0, op.maxDepth).join(":")))).map((ns) => ns.split(":"));
}
namespaces.sort((a, b) => a.join(":").localeCompare(b.join(":")));
return namespaces.slice(op.offset ?? 0, (op.offset ?? 0) + (op.limit ?? namespaces.length));
}
doesMatch(matchCondition, key) {
const { matchType, path } = matchCondition;
if (matchType === "prefix") {
if (path.length > key.length)
return false;
return path.every((pElem, index) => {
const kElem = key[index];
return pElem === "*" || kElem === pElem;
});
}
else if (matchType === "suffix") {
if (path.length > key.length)
return false;
return path.every((pElem, index) => {
const kElem = key[key.length - path.length + index];
return pElem === "*" || kElem === pElem;
});
}
throw new Error(`Unsupported match type: ${matchType}`);
}
filterItems(op) {
const candidates = [];
for (const [namespace, items] of this.data.entries()) {
if (namespace.startsWith(op.namespacePrefix.join(":"))) {
candidates.push(...items.values());
}
}
let filteredCandidates = candidates;
if (op.filter) {
filteredCandidates = candidates.filter((item) => Object.entries(op.filter).every(([key, value]) => (0, utils_js_1.compareValues)(item.value[key], value)));
}
return filteredCandidates;
}
scoreResults(candidates, queryVector, offset = 0, limit = 10) {
const flatItems = [];
const flatVectors = [];
const scoreless = [];
for (const item of candidates) {
const vectors = this.getVectors(item);
if (vectors.length) {
for (const vector of vectors) {
flatItems.push(item);
flatVectors.push(vector);
}
}
else {
scoreless.push(item);
}
}
const scores = this.cosineSimilarity(queryVector, flatVectors);
const sortedResults = scores
.map((score, i) => [score, flatItems[i]])
.sort((a, b) => b[0] - a[0]);
const seen = new Set();
const kept = [];
for (const [score, item] of sortedResults) {
const key = `${item.namespace.join(":")}:${item.key}`;
if (seen.has(key))
continue;
const ix = seen.size;
if (ix >= offset + limit)
break;
if (ix < offset) {
seen.add(key);
continue;
}
seen.add(key);
kept.push([score, item]);
}
if (scoreless.length && kept.length < limit) {
for (const item of scoreless.slice(0, limit - kept.length)) {
const key = `${item.namespace.join(":")}:${item.key}`;
if (!seen.has(key)) {
seen.add(key);
kept.push([undefined, item]);
}
}
}
return kept.map(([score, item]) => ({
...item,
score,
}));
}
paginateResults(results, offset, limit) {
return results.slice(offset, offset + limit);
}
extractTexts(ops) {
if (!ops.length || !this._indexConfig) {
return {};
}
const toEmbed = {};
for (const op of ops) {
if (op.value !== null && op.index !== false) {
const paths = op.index === null || op.index === undefined
? this._indexConfig.__tokenizedFields ?? []
: op.index.map((ix) => [ix, (0, utils_js_1.tokenizePath)(ix)]);
for (const [path, field] of paths) {
const texts = (0, utils_js_1.getTextAtPath)(op.value, field);
if (texts.length) {
if (texts.length > 1) {
texts.forEach((text, i) => {
if (!toEmbed[text])
toEmbed[text] = [];
toEmbed[text].push([op.namespace, op.key, `${path}.${i}`]);
});
}
else {
if (!toEmbed[texts[0]])
toEmbed[texts[0]] = [];
toEmbed[texts[0]].push([op.namespace, op.key, path]);
}
}
}
}
}
return toEmbed;
}
insertVectors(texts, embeddings) {
for (const [text, metadata] of Object.entries(texts)) {
const embedding = embeddings.shift();
if (!embedding) {
throw new Error(`No embedding found for text: ${text}`);
}
for (const [namespace, key, field] of metadata) {
const namespaceKey = namespace.join(":");
if (!this.vectors.has(namespaceKey)) {
this.vectors.set(namespaceKey, new Map());
}
const namespaceMap = this.vectors.get(namespaceKey);
if (!namespaceMap.has(key)) {
namespaceMap.set(key, new Map());
}
const itemMap = namespaceMap.get(key);
itemMap.set(field, embedding);
}
}
}
getVectors(item) {
const namespaceKey = item.namespace.join(":");
const itemKey = item.key;
if (!this.vectors.has(namespaceKey)) {
return [];
}
const namespaceMap = this.vectors.get(namespaceKey);
if (!namespaceMap.has(itemKey)) {
return [];
}
const itemMap = namespaceMap.get(itemKey);
const vectors = Array.from(itemMap.values());
if (!vectors.length) {
return [];
}
return vectors;
}
cosineSimilarity(X, Y) {
if (!Y.length)
return [];
// Calculate dot products for all vectors at once
const dotProducts = Y.map((vector) => vector.reduce((acc, val, i) => acc + val * X[i], 0));
// Calculate magnitudes
const magnitude1 = Math.sqrt(X.reduce((acc, val) => acc + val * val, 0));
const magnitudes2 = Y.map((vector) => Math.sqrt(vector.reduce((acc, val) => acc + val * val, 0)));
// Calculate similarities
return dotProducts.map((dot, i) => {
const magnitude2 = magnitudes2[i];
return magnitude1 && magnitude2 ? dot / (magnitude1 * magnitude2) : 0;
});
}
get indexConfig() {
return this._indexConfig;
}
}
exports.InMemoryStore = InMemoryStore;
/** @deprecated Alias for InMemoryStore */
class MemoryStore extends InMemoryStore {
}
exports.MemoryStore = MemoryStore;
//# sourceMappingURL=memory.js.map
;