fix(ts-sdk): lazy-load optional provider SDKs in mem0ai/oss (#6389)

This commit is contained in:
Kartik
2026-07-20 15:53:35 +05:30
committed by GitHub
parent 726bcc80b2
commit 756b0b1b6d
38 changed files with 536 additions and 274 deletions
+1 -1
View File
@@ -148,7 +148,7 @@ See the full catalog in <Link href="/components/llms/overview">Components</Link>
- Qdrant connection errors: confirm port `6333` is exposed and the API key (if set) matches. - Qdrant connection errors: confirm port `6333` is exposed and the API key (if set) matches.
- Empty search results: verify the embedder model name. A mismatch causes dimension errors. - Empty search results: verify the embedder model name. A mismatch causes dimension errors.
- `Unknown reranker` (Python): upgrade the SDK with `pip install --upgrade mem0ai` to load the latest provider registry. - `Unknown reranker` (Python): upgrade the SDK with `pip install --upgrade mem0ai` to load the latest provider registry.
- `Cannot find module` (Node): import from the OSS entry point, `import { Memory } from "mem0ai/oss"`, not `"mem0ai"`. - `Cannot find module` (Node): two common causes. First, import from the OSS entry point, `import { Memory } from "mem0ai/oss"`, not `"mem0ai"`. Second, provider SDKs are optional peer dependencies loaded on demand, so install the one for the provider you configured (for example `npm install @qdrant/js-client-rest` for Qdrant). Installing `mem0ai` alone only pulls in the providers used by default; you do not need SDKs for providers you never select.
<CardGroup cols={2}> <CardGroup cols={2}>
<Card <Card
+36
View File
@@ -151,6 +151,42 @@
"@aws-sdk/client-bedrock-runtime": ">=3.0.0 <3.968.0" "@aws-sdk/client-bedrock-runtime": ">=3.0.0 <3.968.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@qdrant/js-client-rest": {
"optional": true
},
"redis": {
"optional": true
},
"@supabase/supabase-js": {
"optional": true
},
"cloudflare": {
"optional": true
},
"@azure/search-documents": {
"optional": true
},
"@azure/identity": {
"optional": true
},
"@langchain/core": {
"optional": true
},
"@anthropic-ai/sdk": {
"optional": true
},
"@google/genai": {
"optional": true
},
"groq-sdk": {
"optional": true
},
"@mistralai/mistralai": {
"optional": true
},
"ollama": {
"optional": true
},
"mysql2": { "mysql2": {
"optional": true "optional": true
}, },
+6 -8
View File
@@ -1,6 +1,7 @@
import type { FlagEmbedding } from "fastembed"; import type { FlagEmbedding } from "fastembed";
import { Embedder } from "./base"; import { Embedder } from "./base";
import { EmbeddingConfig } from "../types"; import { EmbeddingConfig } from "../types";
import { loadPeer } from "../utils/load_peer";
// FastEmbed only ships a fixed set of ONNX models (fastembed's `EmbeddingModel` // FastEmbed only ships a fixed set of ONNX models (fastembed's `EmbeddingModel`
// enum, minus CUSTOM). Mirrored here as literals so an invalid model name can // enum, minus CUSTOM). Mirrored here as literals so an invalid model name can
@@ -54,14 +55,11 @@ export class FastEmbedEmbedder implements Embedder {
* consumers that never touch FastEmbed don't need it installed. * consumers that never touch FastEmbed don't need it installed.
*/ */
private async initEmbeddingModel(): Promise<FlagEmbedding> { private async initEmbeddingModel(): Promise<FlagEmbedding> {
let sdk: any; const sdk = await loadPeer(
try { "fastembed",
sdk = await import("fastembed"); "FastEmbed embedder",
} catch { () => import("fastembed"),
throw new Error( );
"The 'fastembed' package is required to use the FastEmbed embedder. Install it with: npm install fastembed",
);
}
return sdk.FlagEmbedding.init({ model: this.modelName }); return sdk.FlagEmbedding.init({ model: this.modelName });
} }
+17 -5
View File
@@ -1,21 +1,32 @@
import { GoogleGenAI } from "@google/genai"; import type { GoogleGenAI } from "@google/genai";
import { Embedder } from "./base"; import { Embedder } from "./base";
import { EmbeddingConfig } from "../types"; import { EmbeddingConfig } from "../types";
import { loadPeer } from "../utils/load_peer";
export class GoogleEmbedder implements Embedder { export class GoogleEmbedder implements Embedder {
private google: GoogleGenAI; private google!: GoogleGenAI;
private model: string; private model: string;
private embeddingDims: number | undefined; private embeddingDims: number | undefined;
private readonly apiKey: string | undefined;
constructor(config: EmbeddingConfig) { constructor(config: EmbeddingConfig) {
this.google = new GoogleGenAI({ this.apiKey = config.apiKey || process.env.GOOGLE_API_KEY;
apiKey: config.apiKey || process.env.GOOGLE_API_KEY,
});
this.model = config.model || "gemini-embedding-001"; this.model = config.model || "gemini-embedding-001";
this.embeddingDims = config.embeddingDims; this.embeddingDims = config.embeddingDims;
} }
private async ensureClient(): Promise<void> {
if (this.google) return;
const sdk = await loadPeer(
"@google/genai",
"Google embedder",
() => import("@google/genai"),
);
this.google = new sdk.GoogleGenAI({ apiKey: this.apiKey });
}
async embed(text: string): Promise<number[]> { async embed(text: string): Promise<number[]> {
await this.ensureClient();
const response = await this.google.models.embedContent({ const response = await this.google.models.embedContent({
model: this.model, model: this.model,
contents: text, contents: text,
@@ -27,6 +38,7 @@ export class GoogleEmbedder implements Embedder {
} }
async embedBatch(texts: string[]): Promise<number[][]> { async embedBatch(texts: string[]): Promise<number[][]> {
await this.ensureClient();
const response = await this.google.models.embedContent({ const response = await this.google.models.embedContent({
model: this.model, model: this.model,
contents: texts, contents: texts,
+1 -1
View File
@@ -1,4 +1,4 @@
import { Embeddings } from "@langchain/core/embeddings"; import type { Embeddings } from "@langchain/core/embeddings";
import { Embedder } from "./base"; import { Embedder } from "./base";
import { EmbeddingConfig } from "../types"; import { EmbeddingConfig } from "../types";
+17 -5
View File
@@ -1,19 +1,19 @@
import { Ollama } from "ollama"; import type { Ollama } from "ollama";
import { Embedder } from "./base"; import { Embedder } from "./base";
import { EmbeddingConfig } from "../types"; import { EmbeddingConfig } from "../types";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { loadPeer } from "../utils/load_peer";
export class OllamaEmbedder implements Embedder { export class OllamaEmbedder implements Embedder {
private ollama: Ollama; private ollama!: Ollama;
private model: string; private model: string;
private embeddingDims?: number; private embeddingDims?: number;
private readonly host: string;
// Using this variable to avoid calling the Ollama server multiple times // Using this variable to avoid calling the Ollama server multiple times
private initialized: boolean = false; private initialized: boolean = false;
constructor(config: EmbeddingConfig) { constructor(config: EmbeddingConfig) {
this.ollama = new Ollama({ this.host = config.url || config.baseURL || "http://localhost:11434";
host: config.url || config.baseURL || "http://localhost:11434",
});
this.model = config.model || "nomic-embed-text:latest"; this.model = config.model || "nomic-embed-text:latest";
this.embeddingDims = config.embeddingDims || 768; this.embeddingDims = config.embeddingDims || 768;
this.ensureModelExists().catch((err) => { this.ensureModelExists().catch((err) => {
@@ -21,7 +21,18 @@ export class OllamaEmbedder implements Embedder {
}); });
} }
private async ensureClient(): Promise<void> {
if (this.ollama) return;
const sdk = await loadPeer(
"ollama",
"Ollama embedder",
() => import("ollama"),
);
this.ollama = new sdk.Ollama({ host: this.host });
}
async embed(text: string): Promise<number[]> { async embed(text: string): Promise<number[]> {
await this.ensureClient();
try { try {
await this.ensureModelExists(); await this.ensureModelExists();
} catch (err) { } catch (err) {
@@ -54,6 +65,7 @@ export class OllamaEmbedder implements Embedder {
if (this.initialized) { if (this.initialized) {
return true; return true;
} }
await this.ensureClient();
const local_models = await this.ollama.list(); const local_models = await this.ollama.list();
const target = OllamaEmbedder.normalizeModelName(this.model); const target = OllamaEmbedder.normalizeModelName(this.model);
if ( if (
+6 -9
View File
@@ -1,6 +1,7 @@
import type { PredictionServiceClient } from "@google-cloud/aiplatform"; import type { PredictionServiceClient } from "@google-cloud/aiplatform";
import { Embedder } from "./base"; import { Embedder } from "./base";
import { VertexAIConfig } from "../types"; import { VertexAIConfig } from "../types";
import { loadPeer } from "../utils/load_peer";
type AIPlatform = typeof import("@google-cloud/aiplatform"); type AIPlatform = typeof import("@google-cloud/aiplatform");
type ClientOptions = NonNullable< type ClientOptions = NonNullable<
@@ -105,15 +106,11 @@ export class VertexAIEmbedder implements Embedder {
} }
private async createClient(): Promise<void> { private async createClient(): Promise<void> {
let aiplatform: AIPlatform; const aiplatform: AIPlatform = await loadPeer(
try { "@google-cloud/aiplatform",
aiplatform = await import("@google-cloud/aiplatform"); "Vertex AI embedding provider",
} catch (err) { () => import("@google-cloud/aiplatform"),
throw new Error( );
"Failed to import '@google-cloud/aiplatform'. Please install it to use the Vertex AI embedding provider: " +
(err as Error).message,
);
}
const client = new aiplatform.PredictionServiceClient(this.clientOptions); const client = new aiplatform.PredictionServiceClient(this.clientOptions);
+16 -3
View File
@@ -1,9 +1,11 @@
import Anthropic from "@anthropic-ai/sdk"; import type Anthropic from "@anthropic-ai/sdk";
import { LLM, LLMResponse } from "./base"; import { LLM, LLMResponse } from "./base";
import { LLMConfig, Message } from "../types"; import { LLMConfig, Message } from "../types";
import { loadPeer } from "../utils/load_peer";
export class AnthropicLLM implements LLM { export class AnthropicLLM implements LLM {
private client: Anthropic; private client!: Anthropic;
private readonly clientArgs: { apiKey: string; baseURL?: string };
private model: string; private model: string;
private maxTokens: number; private maxTokens: number;
private temperature?: number; private temperature?: number;
@@ -20,7 +22,7 @@ export class AnthropicLLM implements LLM {
if (config.baseURL) { if (config.baseURL) {
clientArgs.baseURL = config.baseURL; clientArgs.baseURL = config.baseURL;
} }
this.client = new Anthropic(clientArgs); this.clientArgs = clientArgs;
this.model = config.model || "claude-sonnet-4-6"; this.model = config.model || "claude-sonnet-4-6";
// Defaults mirror the Python provider's AnthropicConfig // Defaults mirror the Python provider's AnthropicConfig
// (max_tokens=2000, temperature=0.1, top_p omitted). // (max_tokens=2000, temperature=0.1, top_p omitted).
@@ -29,11 +31,22 @@ export class AnthropicLLM implements LLM {
this.topP = config.topP; this.topP = config.topP;
} }
private async ensureClient(): Promise<void> {
if (this.client) return;
const sdk = await loadPeer(
"@anthropic-ai/sdk",
"Anthropic LLM",
() => import("@anthropic-ai/sdk"),
);
this.client = new sdk.default(this.clientArgs);
}
async generateResponse( async generateResponse(
messages: Message[], messages: Message[],
responseFormat?: { type: string }, responseFormat?: { type: string },
tools?: any[], tools?: any[],
): Promise<string | LLMResponse> { ): Promise<string | LLMResponse> {
await this.ensureClient();
// Extract system message if present // Extract system message if present
const systemMessage = messages.find((msg) => msg.role === "system"); const systemMessage = messages.find((msg) => msg.role === "system");
const otherMessages = messages.filter((msg) => msg.role !== "system"); const otherMessages = messages.filter((msg) => msg.role !== "system");
+17 -3
View File
@@ -1,16 +1,28 @@
import { GoogleGenAI } from "@google/genai"; import type { GoogleGenAI } from "@google/genai";
import { LLM, LLMResponse } from "./base"; import { LLM, LLMResponse } from "./base";
import { LLMConfig, Message } from "../types"; import { LLMConfig, Message } from "../types";
import { loadPeer } from "../utils/load_peer";
export class GoogleLLM implements LLM { export class GoogleLLM implements LLM {
private google: GoogleGenAI; private google!: GoogleGenAI;
private model: string; private model: string;
private readonly apiKey: string | undefined;
constructor(config: LLMConfig) { constructor(config: LLMConfig) {
this.google = new GoogleGenAI({ apiKey: config.apiKey }); this.apiKey = config.apiKey;
this.model = config.model || "gemini-2.0-flash"; this.model = config.model || "gemini-2.0-flash";
} }
private async ensureClient(): Promise<void> {
if (this.google) return;
const sdk = await loadPeer(
"@google/genai",
"Google LLM",
() => import("@google/genai"),
);
this.google = new sdk.GoogleGenAI({ apiKey: this.apiKey });
}
private formatContents(messages: Message[]) { private formatContents(messages: Message[]) {
return messages.map((msg) => ({ return messages.map((msg) => ({
parts: [ parts: [
@@ -30,6 +42,7 @@ export class GoogleLLM implements LLM {
responseFormat?: { type: string }, responseFormat?: { type: string },
tools?: any[], tools?: any[],
): Promise<string | LLMResponse> { ): Promise<string | LLMResponse> {
await this.ensureClient();
const contents = this.formatContents(messages); const contents = this.formatContents(messages);
// Build config with tools if provided // Build config with tools if provided
@@ -72,6 +85,7 @@ export class GoogleLLM implements LLM {
} }
async generateChat(messages: Message[]): Promise<LLMResponse> { async generateChat(messages: Message[]): Promise<LLMResponse> {
await this.ensureClient();
const completion = await this.google.models.generateContent({ const completion = await this.google.models.generateContent({
contents: this.formatContents(messages), contents: this.formatContents(messages),
model: this.model, model: this.model,
+17 -3
View File
@@ -1,24 +1,37 @@
import { Groq } from "groq-sdk"; import type { Groq } from "groq-sdk";
import { LLM, LLMResponse } from "./base"; import { LLM, LLMResponse } from "./base";
import { LLMConfig, Message } from "../types"; import { LLMConfig, Message } from "../types";
import { loadPeer } from "../utils/load_peer";
export class GroqLLM implements LLM { export class GroqLLM implements LLM {
private client: Groq; private client!: Groq;
private model: string; private model: string;
private readonly apiKey: string;
constructor(config: LLMConfig) { constructor(config: LLMConfig) {
const apiKey = config.apiKey || process.env.GROQ_API_KEY; const apiKey = config.apiKey || process.env.GROQ_API_KEY;
if (!apiKey) { if (!apiKey) {
throw new Error("Groq API key is required"); throw new Error("Groq API key is required");
} }
this.client = new Groq({ apiKey }); this.apiKey = apiKey;
this.model = config.model || "llama3-70b-8192"; this.model = config.model || "llama3-70b-8192";
} }
private async ensureClient(): Promise<void> {
if (this.client) return;
const sdk = await loadPeer(
"groq-sdk",
"Groq LLM",
() => import("groq-sdk"),
);
this.client = new sdk.Groq({ apiKey: this.apiKey });
}
async generateResponse( async generateResponse(
messages: Message[], messages: Message[],
responseFormat?: { type: string }, responseFormat?: { type: string },
): Promise<string> { ): Promise<string> {
await this.ensureClient();
const response = await this.client.chat.completions.create({ const response = await this.client.chat.completions.create({
model: this.model, model: this.model,
messages: messages.map((msg) => ({ messages: messages.map((msg) => ({
@@ -35,6 +48,7 @@ export class GroqLLM implements LLM {
} }
async generateChat(messages: Message[]): Promise<LLMResponse> { async generateChat(messages: Message[]): Promise<LLMResponse> {
await this.ensureClient();
const response = await this.client.chat.completions.create({ const response = await this.client.chat.completions.create({
model: this.model, model: this.model,
messages: messages.map((msg) => ({ messages: messages.map((msg) => ({
+9 -10
View File
@@ -1,17 +1,16 @@
import { BaseLanguageModel } from "@langchain/core/language_models/base"; import type { BaseLanguageModel } from "@langchain/core/language_models/base";
import { import type { BaseMessage } from "@langchain/core/messages";
AIMessage,
HumanMessage,
SystemMessage,
BaseMessage,
} from "@langchain/core/messages";
import { z } from "zod"; import { z } from "zod";
import { LLM, LLMResponse } from "./base"; import { LLM, LLMResponse } from "./base";
import { LLMConfig, Message } from "../types/index"; import { LLMConfig, Message } from "../types/index";
// Import the schemas directly into LangchainLLM // Import the schemas directly into LangchainLLM
import { FactRetrievalSchema, MemoryUpdateSchema } from "../prompts"; import { FactRetrievalSchema, MemoryUpdateSchema } from "../prompts";
const convertToLangchainMessages = (messages: Message[]): BaseMessage[] => { const convertToLangchainMessages = async (
messages: Message[],
): Promise<BaseMessage[]> => {
const { AIMessage, HumanMessage, SystemMessage } =
await import("@langchain/core/messages");
return messages.map((msg) => { return messages.map((msg) => {
const content = const content =
typeof msg.content === "string" typeof msg.content === "string"
@@ -62,7 +61,7 @@ export class LangchainLLM implements LLM {
response_format?: { type: string }, response_format?: { type: string },
tools?: any[], tools?: any[],
): Promise<string | LLMResponse> { ): Promise<string | LLMResponse> {
const langchainMessages = convertToLangchainMessages(messages); const langchainMessages = await convertToLangchainMessages(messages);
let runnable: any = this.llmInstance; let runnable: any = this.llmInstance;
const invokeOptions: Record<string, any> = {}; const invokeOptions: Record<string, any> = {};
let isStructuredOutput = false; let isStructuredOutput = false;
@@ -170,7 +169,7 @@ export class LangchainLLM implements LLM {
} }
async generateChat(messages: Message[]): Promise<LLMResponse> { async generateChat(messages: Message[]): Promise<LLMResponse> {
const langchainMessages = convertToLangchainMessages(messages); const langchainMessages = await convertToLangchainMessages(messages);
try { try {
const response = await this.llmInstance.invoke(langchainMessages); const response = await this.llmInstance.invoke(langchainMessages);
if (response && typeof response.content === "string") { if (response && typeof response.content === "string") {
+17 -5
View File
@@ -1,21 +1,31 @@
import { Mistral } from "@mistralai/mistralai"; import type { Mistral } from "@mistralai/mistralai";
import { LLM, LLMResponse } from "./base"; import { LLM, LLMResponse } from "./base";
import { LLMConfig, Message } from "../types"; import { LLMConfig, Message } from "../types";
import { loadPeer } from "../utils/load_peer";
export class MistralLLM implements LLM { export class MistralLLM implements LLM {
private client: Mistral; private client!: Mistral;
private model: string; private model: string;
private readonly apiKey: string;
constructor(config: LLMConfig) { constructor(config: LLMConfig) {
if (!config.apiKey) { if (!config.apiKey) {
throw new Error("Mistral API key is required"); throw new Error("Mistral API key is required");
} }
this.client = new Mistral({ this.apiKey = config.apiKey;
apiKey: config.apiKey,
});
this.model = config.model || "mistral-tiny-latest"; this.model = config.model || "mistral-tiny-latest";
} }
private async ensureClient(): Promise<void> {
if (this.client) return;
const sdk = await loadPeer(
"@mistralai/mistralai",
"Mistral LLM",
() => import("@mistralai/mistralai"),
);
this.client = new sdk.Mistral({ apiKey: this.apiKey });
}
// Helper function to convert content to string // Helper function to convert content to string
private contentToString(content: any): string { private contentToString(content: any): string {
if (typeof content === "string") { if (typeof content === "string") {
@@ -41,6 +51,7 @@ export class MistralLLM implements LLM {
responseFormat?: { type: string }, responseFormat?: { type: string },
tools?: any[], tools?: any[],
): Promise<string | LLMResponse> { ): Promise<string | LLMResponse> {
await this.ensureClient();
const response = await this.client.chat.complete({ const response = await this.client.chat.complete({
model: this.model, model: this.model,
messages: messages.map((msg) => ({ messages: messages.map((msg) => ({
@@ -82,6 +93,7 @@ export class MistralLLM implements LLM {
} }
async generateChat(messages: Message[]): Promise<LLMResponse> { async generateChat(messages: Message[]): Promise<LLMResponse> {
await this.ensureClient();
const formattedMessages = messages.map((msg) => ({ const formattedMessages = messages.map((msg) => ({
role: msg.role as "system" | "user" | "assistant", role: msg.role as "system" | "user" | "assistant",
content: content:
+14 -5
View File
@@ -1,29 +1,36 @@
import { Ollama } from "ollama"; import type { Ollama } from "ollama";
import { LLM, LLMResponse } from "./base"; import { LLM, LLMResponse } from "./base";
import { LLMConfig, Message } from "../types"; import { LLMConfig, Message } from "../types";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { loadPeer } from "../utils/load_peer";
export class OllamaLLM implements LLM { export class OllamaLLM implements LLM {
private ollama: Ollama; private ollama!: Ollama;
private model: string; private model: string;
private readonly host: string;
// Using this variable to avoid calling the Ollama server multiple times // Using this variable to avoid calling the Ollama server multiple times
private initialized: boolean = false; private initialized: boolean = false;
constructor(config: LLMConfig) { constructor(config: LLMConfig) {
this.ollama = new Ollama({ this.host = config.url || config.baseURL || "http://localhost:11434";
host: config.url || config.baseURL || "http://localhost:11434",
});
this.model = config.model || "llama3.1:8b"; this.model = config.model || "llama3.1:8b";
this.ensureModelExists().catch((err) => { this.ensureModelExists().catch((err) => {
logger.error(`Error ensuring model exists: ${err}`); logger.error(`Error ensuring model exists: ${err}`);
}); });
} }
private async ensureClient(): Promise<void> {
if (this.ollama) return;
const sdk = await loadPeer("ollama", "Ollama LLM", () => import("ollama"));
this.ollama = new sdk.Ollama({ host: this.host });
}
async generateResponse( async generateResponse(
messages: Message[], messages: Message[],
responseFormat?: { type: string }, responseFormat?: { type: string },
tools?: any[], tools?: any[],
): Promise<string | LLMResponse> { ): Promise<string | LLMResponse> {
await this.ensureClient();
try { try {
await this.ensureModelExists(); await this.ensureModelExists();
} catch (err) { } catch (err) {
@@ -63,6 +70,7 @@ export class OllamaLLM implements LLM {
} }
async generateChat(messages: Message[]): Promise<LLMResponse> { async generateChat(messages: Message[]): Promise<LLMResponse> {
await this.ensureClient();
try { try {
await this.ensureModelExists(); await this.ensureModelExists();
} catch (err) { } catch (err) {
@@ -93,6 +101,7 @@ export class OllamaLLM implements LLM {
if (this.initialized) { if (this.initialized) {
return true; return true;
} }
await this.ensureClient();
const local_models = await this.ollama.list(); const local_models = await this.ollama.list();
if (!local_models.models.find((m: any) => m.name === this.model)) { if (!local_models.models.find((m: any) => m.name === this.model)) {
logger.info(`Pulling model ${this.model}...`); logger.info(`Pulling model ${this.model}...`);
+6 -8
View File
@@ -1,5 +1,6 @@
import { RerankerConfig } from "../types"; import { RerankerConfig } from "../types";
import { Reranker, RerankResult } from "./base"; import { Reranker, RerankResult } from "./base";
import { loadPeer } from "../utils/load_peer";
const DEFAULT_MODEL = "rerank-v3.5"; const DEFAULT_MODEL = "rerank-v3.5";
@@ -41,14 +42,11 @@ export class CohereReranker implements Reranker {
} }
private async createClient(): Promise<any> { private async createClient(): Promise<any> {
let sdk: any; const sdk = await loadPeer(
try { "cohere-ai",
sdk = await import("cohere-ai"); "Cohere reranker",
} catch { () => import("cohere-ai"),
throw new Error( );
"The 'cohere-ai' package is required to use the Cohere reranker. Install it with: npm install cohere-ai",
);
}
return new sdk.CohereClient({ token: this.apiKey }); return new sdk.CohereClient({ token: this.apiKey });
} }
+6 -8
View File
@@ -1,5 +1,6 @@
import { RerankerConfig } from "../types"; import { RerankerConfig } from "../types";
import { Reranker, RerankResult } from "./base"; import { Reranker, RerankResult } from "./base";
import { loadPeer } from "../utils/load_peer";
const DEFAULT_MODEL = "zerank-1"; const DEFAULT_MODEL = "zerank-1";
@@ -37,14 +38,11 @@ export class ZeroEntropyReranker implements Reranker {
} }
private async createClient(): Promise<any> { private async createClient(): Promise<any> {
let sdk: any; const sdk = await loadPeer(
try { "zeroentropy",
sdk = await import("zeroentropy"); "ZeroEntropy reranker",
} catch { () => import("zeroentropy"),
throw new Error( );
"The 'zeroentropy' package is required to use the ZeroEntropy reranker. Install it with: npm install zeroentropy",
);
}
return new sdk.ZeroEntropy({ apiKey: this.apiKey }); return new sdk.ZeroEntropy({ apiKey: this.apiKey });
} }
@@ -1,6 +1,7 @@
import { createClient, SupabaseClient } from "@supabase/supabase-js"; import type { SupabaseClient } from "@supabase/supabase-js";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { HistoryManager } from "./base"; import { HistoryManager } from "./base";
import { loadPeer } from "../utils/load_peer";
interface HistoryEntry { interface HistoryEntry {
id: string; id: string;
@@ -20,16 +21,32 @@ interface SupabaseHistoryConfig {
} }
export class SupabaseHistoryManager implements HistoryManager { export class SupabaseHistoryManager implements HistoryManager {
private supabase: SupabaseClient; // ponytail: benign double-construct race — two concurrent first-calls may each
// build a client; createClient opens no connection, so last-write-wins is fine.
private supabase!: SupabaseClient;
private readonly supabaseUrl: string;
private readonly supabaseKey: string;
private readonly tableName: string; private readonly tableName: string;
constructor(config: SupabaseHistoryConfig) { constructor(config: SupabaseHistoryConfig) {
this.tableName = config.tableName || "memory_history"; this.tableName = config.tableName || "memory_history";
this.supabase = createClient(config.supabaseUrl, config.supabaseKey); this.supabaseUrl = config.supabaseUrl;
this.supabaseKey = config.supabaseKey;
this.initializeSupabase().catch(console.error); this.initializeSupabase().catch(console.error);
} }
private async ensureClient(): Promise<void> {
if (this.supabase) return;
const sdk = await loadPeer(
"@supabase/supabase-js",
"Supabase history manager",
() => import("@supabase/supabase-js"),
);
this.supabase = sdk.createClient(this.supabaseUrl, this.supabaseKey);
}
private async initializeSupabase(): Promise<void> { private async initializeSupabase(): Promise<void> {
await this.ensureClient();
// Check if table exists // Check if table exists
const { error } = await this.supabase const { error } = await this.supabase
.from(this.tableName) .from(this.tableName)
@@ -65,6 +82,7 @@ create table ${this.tableName} (
updatedAt?: string, updatedAt?: string,
isDeleted: number = 0, isDeleted: number = 0,
): Promise<void> { ): Promise<void> {
await this.ensureClient();
const historyEntry: HistoryEntry = { const historyEntry: HistoryEntry = {
id: uuidv4(), id: uuidv4(),
memory_id: memoryId, memory_id: memoryId,
@@ -87,6 +105,7 @@ create table ${this.tableName} (
} }
async getHistory(memoryId: string): Promise<any[]> { async getHistory(memoryId: string): Promise<any[]> {
await this.ensureClient();
const { data, error } = await this.supabase const { data, error } = await this.supabase
.from(this.tableName) .from(this.tableName)
.select("*") .select("*")
@@ -103,6 +122,7 @@ create table ${this.tableName} (
} }
async reset(): Promise<void> { async reset(): Promise<void> {
await this.ensureClient();
const { error } = await this.supabase const { error } = await this.supabase
.from(this.tableName) .from(this.tableName)
.delete() .delete()
+13
View File
@@ -0,0 +1,13 @@
export async function loadPeer(
pkg: string,
label: string,
load: () => Promise<any>,
): Promise<any> {
try {
return await load();
} catch {
throw new Error(
`The '${pkg}' package is required to use the ${label}. Install it with: npm install ${pkg}`,
);
}
}
@@ -1,7 +1,6 @@
import { import type {
SearchClient, SearchClient,
SearchIndexClient, SearchIndexClient,
AzureKeyCredential,
SearchIndex, SearchIndex,
SearchField, SearchField,
SearchFieldDataType, SearchFieldDataType,
@@ -13,9 +12,9 @@ import {
BinaryQuantizationCompression, BinaryQuantizationCompression,
VectorizedQuery, VectorizedQuery,
} from "@azure/search-documents"; } from "@azure/search-documents";
import { DefaultAzureCredential } from "@azure/identity";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
/** /**
* Configuration interface for Azure AI Search vector store * Configuration interface for Azure AI Search vector store
@@ -71,8 +70,8 @@ interface AzureAISearchConfig extends VectorStoreConfig {
* Supports vector search with hybrid search, compression, and filtering * Supports vector search with hybrid search, compression, and filtering
*/ */
export class AzureAISearch implements VectorStore { export class AzureAISearch implements VectorStore {
private searchClient: SearchClient<any>; private searchClient!: SearchClient<any>;
private indexClient: SearchIndexClient; private indexClient!: SearchIndexClient;
private readonly serviceName: string; private readonly serviceName: string;
private readonly indexName: string; private readonly indexName: string;
private readonly embeddingModelDims: number; private readonly embeddingModelDims: number;
@@ -93,25 +92,42 @@ export class AzureAISearch implements VectorStore {
this.vectorFilterMode = config.vectorFilterMode || "preFilter"; this.vectorFilterMode = config.vectorFilterMode || "preFilter";
this.apiKey = config.apiKey; this.apiKey = config.apiKey;
// Initialize the index
this.initialize().catch(console.error);
}
private async ensureClient(): Promise<void> {
if (this.searchClient) return;
const searchSdk = await loadPeer(
"@azure/search-documents",
"Azure AI Search vector store",
() => import("@azure/search-documents"),
);
const serviceEndpoint = `https://${this.serviceName}.search.windows.net`; const serviceEndpoint = `https://${this.serviceName}.search.windows.net`;
// Determine authentication: API key or DefaultAzureCredential // Determine authentication: API key or DefaultAzureCredential
const credential = let credential: any;
this.apiKey && this.apiKey !== "" && this.apiKey !== "your-api-key" if (this.apiKey && this.apiKey !== "" && this.apiKey !== "your-api-key") {
? new AzureKeyCredential(this.apiKey) credential = new searchSdk.AzureKeyCredential(this.apiKey);
: new DefaultAzureCredential(); } else {
const identitySdk = await loadPeer(
"@azure/identity",
"Azure AI Search without an apiKey",
() => import("@azure/identity"),
);
credential = new identitySdk.DefaultAzureCredential();
}
// Initialize clients this.searchClient = new searchSdk.SearchClient(
this.searchClient = new SearchClient(
serviceEndpoint, serviceEndpoint,
this.indexName, this.indexName,
credential, credential,
); );
this.indexClient = new searchSdk.SearchIndexClient(
this.indexClient = new SearchIndexClient(serviceEndpoint, credential); serviceEndpoint,
credential,
// Initialize the index );
this.initialize().catch(console.error);
} }
/** /**
@@ -125,6 +141,7 @@ export class AzureAISearch implements VectorStore {
} }
private async _doInitialize(): Promise<void> { private async _doInitialize(): Promise<void> {
await this.ensureClient();
try { try {
const collections = await this.listCols(); const collections = await this.listCols();
if (!collections.includes(this.indexName)) { if (!collections.includes(this.indexName)) {
@@ -262,6 +279,7 @@ export class AzureAISearch implements VectorStore {
ids: string[], ids: string[],
payloads: Record<string, any>[], payloads: Record<string, any>[],
): Promise<void> { ): Promise<void> {
await this.initialize();
console.log( console.log(
`Inserting ${vectors.length} vectors into index ${this.indexName}`, `Inserting ${vectors.length} vectors into index ${this.indexName}`,
); );
@@ -334,6 +352,7 @@ export class AzureAISearch implements VectorStore {
topK: number = 5, topK: number = 5,
filters?: SearchFilters, filters?: SearchFilters,
): Promise<VectorStoreResult[] | null> { ): Promise<VectorStoreResult[] | null> {
await this.initialize();
try { try {
const filterExpression = filters const filterExpression = filters
? this.buildFilterExpression(filters) ? this.buildFilterExpression(filters)
@@ -373,6 +392,7 @@ export class AzureAISearch implements VectorStore {
topK: number = 5, topK: number = 5,
filters?: SearchFilters, filters?: SearchFilters,
): Promise<VectorStoreResult[]> { ): Promise<VectorStoreResult[]> {
await this.initialize();
const filterExpression = filters const filterExpression = filters
? this.buildFilterExpression(filters) ? this.buildFilterExpression(filters)
: undefined; : undefined;
@@ -429,6 +449,7 @@ export class AzureAISearch implements VectorStore {
* Delete a vector by ID * Delete a vector by ID
*/ */
async delete(vectorId: string): Promise<void> { async delete(vectorId: string): Promise<void> {
await this.initialize();
const response = await this.searchClient.deleteDocuments([ const response = await this.searchClient.deleteDocuments([
{ id: vectorId }, { id: vectorId },
]); ]);
@@ -454,6 +475,7 @@ export class AzureAISearch implements VectorStore {
vector: number[], vector: number[],
payload: Record<string, any>, payload: Record<string, any>,
): Promise<void> { ): Promise<void> {
await this.initialize();
const document: Record<string, any> = { id: vectorId }; const document: Record<string, any> = { id: vectorId };
if (vector) { if (vector) {
@@ -486,6 +508,7 @@ export class AzureAISearch implements VectorStore {
* Retrieve a vector by ID * Retrieve a vector by ID
*/ */
async get(vectorId: string): Promise<VectorStoreResult | null> { async get(vectorId: string): Promise<VectorStoreResult | null> {
await this.initialize();
try { try {
const result = await this.searchClient.getDocument(vectorId); const result = await this.searchClient.getDocument(vectorId);
const payloadStr = result.payload as string; const payloadStr = result.payload as string;
@@ -521,6 +544,7 @@ export class AzureAISearch implements VectorStore {
* Delete the index * Delete the index
*/ */
async deleteCol(): Promise<void> { async deleteCol(): Promise<void> {
await this.initialize();
await this.indexClient.deleteIndex(this.indexName); await this.indexClient.deleteIndex(this.indexName);
} }
@@ -542,6 +566,7 @@ export class AzureAISearch implements VectorStore {
filters?: SearchFilters, filters?: SearchFilters,
topK: number = 100, topK: number = 100,
): Promise<[VectorStoreResult[], number]> { ): Promise<[VectorStoreResult[], number]> {
await this.initialize();
const filterExpression = filters const filterExpression = filters
? this.buildFilterExpression(filters) ? this.buildFilterExpression(filters)
: undefined; : undefined;
@@ -586,6 +611,7 @@ export class AzureAISearch implements VectorStore {
* Required by VectorStore interface * Required by VectorStore interface
*/ */
async getUserId(): Promise<string> { async getUserId(): Promise<string> {
await this.initialize();
try { try {
// Check if memory_migrations index exists // Check if memory_migrations index exists
const collections = await this.listCols(); const collections = await this.listCols();
@@ -648,6 +674,7 @@ export class AzureAISearch implements VectorStore {
* Required by VectorStore interface * Required by VectorStore interface
*/ */
async setUserId(userId: string): Promise<void> { async setUserId(userId: string): Promise<void> {
await this.initialize();
try { try {
// Get existing point ID or generate new one // Get existing point ID or generate new one
const searchResults = await this.searchClient.search("*", { const searchResults = await this.searchClient.search("*", {
@@ -677,6 +704,7 @@ export class AzureAISearch implements VectorStore {
* Reset the index by deleting and recreating it * Reset the index by deleting and recreating it
*/ */
async reset(): Promise<void> { async reset(): Promise<void> {
await this.initialize();
console.log(`Resetting index ${this.indexName}...`); console.log(`Resetting index ${this.indexName}...`);
try { try {
@@ -1,6 +1,7 @@
import type { Pool, RowDataPacket } from "mysql2/promise"; import type { Pool, RowDataPacket } from "mysql2/promise";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,127}$/; const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,127}$/;
@@ -94,14 +95,11 @@ export class AzureMySQLDB implements VectorStore {
// Loaded dynamically: mysql2 is an optional peer dependency, so a static value import // Loaded dynamically: mysql2 is an optional peer dependency, so a static value import
// would break `import { Memory } from "mem0ai/oss"` for everyone else. // would break `import { Memory } from "mem0ai/oss"` for everyone else.
let createPool: typeof import("mysql2/promise").createPool; const { createPool }: typeof import("mysql2/promise") = await loadPeer(
try { "mysql2",
({ createPool } = await import("mysql2/promise")); "Azure MySQL vector store",
} catch { () => import("mysql2/promise"),
throw new Error( );
"The Azure MySQL vector store requires the 'mysql2' package. Install it with: npm install mysql2",
);
}
this.pool = createPool({ this.pool = createPool({
host: this.config.host, host: this.config.host,
+6 -8
View File
@@ -12,6 +12,7 @@ import type {
} from "@mochow/mochow-sdk-node"; } from "@mochow/mochow-sdk-node";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
type MochowSdk = typeof import("@mochow/mochow-sdk-node"); type MochowSdk = typeof import("@mochow/mochow-sdk-node");
@@ -156,14 +157,11 @@ export class BaiduDB implements VectorStore {
// value import would break `import { Memory } from "mem0ai/oss"` for everyone else. // value import would break `import { Memory } from "mem0ai/oss"` for everyone else.
private async loadSdk(): Promise<MochowSdk> { private async loadSdk(): Promise<MochowSdk> {
if (!this.sdk) { if (!this.sdk) {
let module: MochowSdk & { default?: MochowSdk }; const module: MochowSdk & { default?: MochowSdk } = await loadPeer(
try { "@mochow/mochow-sdk-node",
module = await import("@mochow/mochow-sdk-node"); "Baidu vector store",
} catch { () => import("@mochow/mochow-sdk-node"),
throw new Error( );
"The Baidu vector store requires the '@mochow/mochow-sdk-node' package. Install it with: npm install @mochow/mochow-sdk-node",
);
}
this.sdk = module.default ?? module; this.sdk = module.default ?? module;
} }
return this.sdk; return this.sdk;
@@ -1,5 +1,6 @@
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
const MIGRATION_ROW_ID = "mem0-user"; const MIGRATION_ROW_ID = "mem0-user";
const SAFE_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/; const SAFE_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
@@ -377,14 +378,11 @@ export class CassandraDB implements VectorStore {
// Loaded dynamically: cassandra-driver is an optional peer dependency, so a static // Loaded dynamically: cassandra-driver is an optional peer dependency, so a static
// value import would break `import { Memory } from "mem0ai/oss"` for everyone else. // value import would break `import { Memory } from "mem0ai/oss"` for everyone else.
private async loadDriver(): Promise<any> { private async loadDriver(): Promise<any> {
let sdk: any; const sdk = await loadPeer(
try { "cassandra-driver",
sdk = await import("cassandra-driver"); "Cassandra vector store",
} catch { () => import("cassandra-driver"),
throw new Error( );
"The 'cassandra-driver' package is required to use the Cassandra vector store. Install it with: npm install cassandra-driver",
);
}
return sdk.default ?? sdk; return sdk.default ?? sdk;
} }
+6 -8
View File
@@ -1,6 +1,7 @@
import type { ChromaClient, CloudClient } from "chromadb"; import type { ChromaClient, CloudClient } from "chromadb";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
interface ChromaConfig extends VectorStoreConfig { interface ChromaConfig extends VectorStoreConfig {
/** Pre-configured ChromaDB client instance. */ /** Pre-configured ChromaDB client instance. */
@@ -65,14 +66,11 @@ export class ChromaDB implements VectorStore {
return config.client; return config.client;
} }
let sdk: any; const sdk = await loadPeer(
try { "chromadb",
sdk = await import("chromadb"); "Chroma vector store",
} catch { () => import("chromadb"),
throw new Error( );
"The 'chromadb' package is required to use the Chroma vector store. Install it with: npm install chromadb",
);
}
if (config.apiKey && config.tenant) { if (config.apiKey && config.tenant) {
return new sdk.CloudClient({ return new sdk.CloudClient({
@@ -1,6 +1,7 @@
import type { Client } from "@elastic/elasticsearch"; import type { Client } from "@elastic/elasticsearch";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
interface ElasticsearchConfig extends VectorStoreConfig { interface ElasticsearchConfig extends VectorStoreConfig {
/** Pre-configured Elasticsearch client instance (typed as `any` to keep the /** Pre-configured Elasticsearch client instance (typed as `any` to keep the
@@ -100,14 +101,11 @@ export class ElasticsearchDB implements VectorStore {
params.headers = config.headers; params.headers = config.headers;
} }
let sdk: any; const sdk = await loadPeer(
try { "@elastic/elasticsearch",
sdk = await import("@elastic/elasticsearch"); "Elasticsearch vector store",
} catch { () => import("@elastic/elasticsearch"),
throw new Error( );
"The '@elastic/elasticsearch' package is required to use the Elasticsearch vector store. Install it with: npm install @elastic/elasticsearch",
);
}
this.client = new sdk.Client(params); this.client = new sdk.Client(params);
} }
@@ -1,5 +1,4 @@
import { VectorStore as LangchainVectorStoreInterface } from "@langchain/core/vectorstores"; import type { VectorStore as LangchainVectorStoreInterface } from "@langchain/core/vectorstores";
import { Document } from "@langchain/core/documents";
import { VectorStore } from "./base"; // mem0's VectorStore interface import { VectorStore } from "./base"; // mem0's VectorStore interface
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
@@ -77,6 +76,7 @@ export class LangchainVectorStore implements VectorStore {
} }
// Convert payloads to Langchain Document metadata format // Convert payloads to Langchain Document metadata format
const { Document } = await import("@langchain/core/documents");
const documents = payloads.map((payload, i) => { const documents = payloads.map((payload, i) => {
// Provide empty pageContent, store mem0 id and other data in metadata // Provide empty pageContent, store mem0 id and other data in metadata
return new Document({ return new Document({
+6 -8
View File
@@ -1,6 +1,7 @@
import type { MongoClient, Collection, Db } from "mongodb"; import type { MongoClient, Collection, Db } from "mongodb";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
export interface MongoDBConfig extends VectorStoreConfig { export interface MongoDBConfig extends VectorStoreConfig {
url?: string; url?: string;
@@ -42,14 +43,11 @@ export class MongoDB implements VectorStore {
if (config.client) { if (config.client) {
this.client = config.client; this.client = config.client;
} else { } else {
let sdk: any; const sdk = await loadPeer(
try { "mongodb",
sdk = await import("mongodb"); "MongoDB vector store",
} catch { () => import("mongodb"),
throw new Error( );
"The 'mongodb' package is required to use the MongoDB vector store. Install it with: npm install mongodb",
);
}
const url = config.url || "mongodb://localhost:27017"; const url = config.url || "mongodb://localhost:27017";
this.client = new sdk.MongoClient(url, { appName: "Mem0" }); this.client = new sdk.MongoClient(url, { appName: "Mem0" });
} }
@@ -1,6 +1,7 @@
import type { Client } from "@opensearch-project/opensearch"; import type { Client } from "@opensearch-project/opensearch";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
type OpenSearchAuth = type OpenSearchAuth =
| { | {
@@ -98,14 +99,11 @@ export class OpenSearchDB implements VectorStore {
? { username: config.user, password: config.password } ? { username: config.user, password: config.password }
: undefined); : undefined);
let sdk: any; const sdk = await loadPeer(
try { "@opensearch-project/opensearch",
sdk = await import("@opensearch-project/opensearch"); "OpenSearch vector store",
} catch { () => import("@opensearch-project/opensearch"),
throw new Error( );
"The '@opensearch-project/opensearch' package is required to use the OpenSearch vector store. Install it with: npm install @opensearch-project/opensearch",
);
}
this.client = new sdk.Client({ this.client = new sdk.Client({
node: `${useSSL ? "https" : "http"}://${host}:${port}`, node: `${useSSL ? "https" : "http"}://${host}:${port}`,
@@ -1,6 +1,7 @@
import type { Pinecone, Index } from "@pinecone-database/pinecone"; import type { Pinecone, Index } from "@pinecone-database/pinecone";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
const MIGRATIONS_NAMESPACE = "__mem0_migrations__"; const MIGRATIONS_NAMESPACE = "__mem0_migrations__";
const MIGRATIONS_RECORD_ID = "mem0-user-id"; const MIGRATIONS_RECORD_ID = "mem0-user-id";
@@ -82,14 +83,11 @@ export class PineconeDB implements VectorStore {
this.client = config.client; this.client = config.client;
} else { } else {
const apiKey = config.apiKey || process.env.PINECONE_API_KEY; const apiKey = config.apiKey || process.env.PINECONE_API_KEY;
let sdk: any; const sdk = await loadPeer(
try { "@pinecone-database/pinecone",
sdk = await import("@pinecone-database/pinecone"); "Pinecone vector store",
} catch { () => import("@pinecone-database/pinecone"),
throw new Error( );
"The '@pinecone-database/pinecone' package is required to use the Pinecone vector store. Install it with: npm install @pinecone-database/pinecone",
);
}
this.client = new sdk.Pinecone({ apiKey }); this.client = new sdk.Pinecone({ apiKey });
} }
} }
+60 -38
View File
@@ -1,6 +1,7 @@
import { QdrantClient } from "@qdrant/js-client-rest"; import type { QdrantClient } from "@qdrant/js-client-rest";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
import * as fs from "fs"; import * as fs from "fs";
interface QdrantConfig extends VectorStoreConfig { interface QdrantConfig extends VectorStoreConfig {
@@ -55,53 +56,64 @@ const KEY_MAP: Record<string, string> = {
}; };
export class Qdrant implements VectorStore { export class Qdrant implements VectorStore {
private client: QdrantClient; private client!: QdrantClient;
private readonly config: QdrantConfig;
private readonly collectionName: string; private readonly collectionName: string;
private dimension: number; private dimension: number;
private _initPromise?: Promise<void>; private _initPromise?: Promise<void>;
constructor(config: QdrantConfig) { constructor(config: QdrantConfig) {
if (config.client) { this.config = config;
this.client = config.client;
} else {
const params: Record<string, any> = {};
if (config.apiKey) {
params.apiKey = config.apiKey;
}
if (config.url) {
params.url = config.url;
// Workaround for qdrant/qdrant-js#59: explicitly pass port to avoid "Illegal host" error
try {
const parsedUrl = new URL(config.url);
params.port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 6333;
} catch (_) {
params.port = 6333;
}
}
if (config.host && config.port) {
params.host = config.host;
params.port = config.port;
}
if (!Object.keys(params).length) {
params.path = config.path;
if (!config.onDisk && config.path) {
if (
fs.existsSync(config.path) &&
fs.statSync(config.path).isDirectory()
) {
fs.rmSync(config.path, { recursive: true });
}
}
}
this.client = new QdrantClient(params);
}
this.collectionName = config.collectionName; this.collectionName = config.collectionName;
this.dimension = config.dimension || 1536; // Default OpenAI dimension this.dimension = config.dimension || 1536; // Default OpenAI dimension
this.initialize().catch(console.error); this.initialize().catch(console.error);
} }
private async ensureClient(): Promise<void> {
if (this.client) return;
const config = this.config;
if (config.client) {
this.client = config.client;
return;
}
const params: Record<string, any> = {};
if (config.apiKey) {
params.apiKey = config.apiKey;
}
if (config.url) {
params.url = config.url;
// Workaround for qdrant/qdrant-js#59: explicitly pass port to avoid "Illegal host" error
try {
const parsedUrl = new URL(config.url);
params.port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 6333;
} catch (_) {
params.port = 6333;
}
}
if (config.host && config.port) {
params.host = config.host;
params.port = config.port;
}
if (!Object.keys(params).length) {
params.path = config.path;
if (!config.onDisk && config.path) {
if (
fs.existsSync(config.path) &&
fs.statSync(config.path).isDirectory()
) {
fs.rmSync(config.path, { recursive: true });
}
}
}
const sdk = await loadPeer(
"@qdrant/js-client-rest",
"Qdrant vector store",
() => import("@qdrant/js-client-rest"),
);
this.client = new sdk.QdrantClient(params);
}
/** /**
* Build a single field condition from a key-value filter pair. * Build a single field condition from a key-value filter pair.
* Supports enhanced filter syntax with comparison operators. * Supports enhanced filter syntax with comparison operators.
@@ -270,6 +282,7 @@ export class Qdrant implements VectorStore {
ids: string[], ids: string[],
payloads: Record<string, any>[], payloads: Record<string, any>[],
): Promise<void> { ): Promise<void> {
await this.initialize();
const points = vectors.map((vector, idx) => ({ const points = vectors.map((vector, idx) => ({
id: ids[idx], id: ids[idx],
vector: vector, vector: vector,
@@ -290,6 +303,7 @@ export class Qdrant implements VectorStore {
topK: number = 5, topK: number = 5,
filters?: SearchFilters, filters?: SearchFilters,
): Promise<VectorStoreResult[]> { ): Promise<VectorStoreResult[]> {
await this.initialize();
const queryFilter = this.createFilter(filters); const queryFilter = this.createFilter(filters);
const results = await this.client.search(this.collectionName, { const results = await this.client.search(this.collectionName, {
vector: query, vector: query,
@@ -305,6 +319,7 @@ export class Qdrant implements VectorStore {
} }
async get(vectorId: string): Promise<VectorStoreResult | null> { async get(vectorId: string): Promise<VectorStoreResult | null> {
await this.initialize();
const results = await this.client.retrieve(this.collectionName, { const results = await this.client.retrieve(this.collectionName, {
ids: [vectorId], ids: [vectorId],
with_payload: true, with_payload: true,
@@ -323,6 +338,7 @@ export class Qdrant implements VectorStore {
vector: number[], vector: number[],
payload: Record<string, any>, payload: Record<string, any>,
): Promise<void> { ): Promise<void> {
await this.initialize();
const point = { const point = {
id: vectorId, id: vectorId,
vector: vector, vector: vector,
@@ -335,12 +351,14 @@ export class Qdrant implements VectorStore {
} }
async delete(vectorId: string): Promise<void> { async delete(vectorId: string): Promise<void> {
await this.initialize();
await this.client.delete(this.collectionName, { await this.client.delete(this.collectionName, {
points: [vectorId], points: [vectorId],
}); });
} }
async deleteCol(): Promise<void> { async deleteCol(): Promise<void> {
await this.initialize();
await this.client.deleteCollection(this.collectionName); await this.client.deleteCollection(this.collectionName);
} }
@@ -348,6 +366,7 @@ export class Qdrant implements VectorStore {
filters?: SearchFilters, filters?: SearchFilters,
topK: number = 100, topK: number = 100,
): Promise<[VectorStoreResult[], number]> { ): Promise<[VectorStoreResult[], number]> {
await this.initialize();
const scrollRequest = { const scrollRequest = {
limit: topK, limit: topK,
filter: this.createFilter(filters), filter: this.createFilter(filters),
@@ -380,6 +399,7 @@ export class Qdrant implements VectorStore {
} }
async getUserId(): Promise<string> { async getUserId(): Promise<string> {
await this.initialize();
try { try {
// Ensure collection exists (idempotent — handles race conditions) // Ensure collection exists (idempotent — handles race conditions)
await this.ensureCollection("memory_migrations", 1); await this.ensureCollection("memory_migrations", 1);
@@ -417,6 +437,7 @@ export class Qdrant implements VectorStore {
} }
async setUserId(userId: string): Promise<void> { async setUserId(userId: string): Promise<void> {
await this.initialize();
try { try {
// Get existing point ID // Get existing point ID
const result = await this.client.scroll("memory_migrations", { const result = await this.client.scroll("memory_migrations", {
@@ -496,6 +517,7 @@ export class Qdrant implements VectorStore {
private async _doInitialize(): Promise<void> { private async _doInitialize(): Promise<void> {
try { try {
await this.ensureClient();
await this.ensureCollection(this.collectionName, this.dimension); await this.ensureCollection(this.collectionName, this.dimension);
await this.ensureCollection("memory_migrations", 1); await this.ensureCollection("memory_migrations", 1);
} catch (error) { } catch (error) {
+36 -12
View File
@@ -1,4 +1,3 @@
import { createClient } from "redis";
import type { import type {
RedisClientType, RedisClientType,
RedisDefaultModules, RedisDefaultModules,
@@ -8,6 +7,7 @@ import type {
} from "redis"; } from "redis";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
/** /**
* Escape RediSearch TAG filter special characters. Any punctuation in the * Escape RediSearch TAG filter special characters. Any punctuation in the
@@ -146,15 +146,21 @@ function toCamelCase(obj: Record<string, any>): Record<string, any> {
} }
export class RedisDB implements VectorStore { export class RedisDB implements VectorStore {
private client: RedisClientType< private client!: RedisClientType<
RedisDefaultModules & RedisModules & RedisFunctions & RedisScripts RedisDefaultModules & RedisModules & RedisFunctions & RedisScripts
>; >;
private readonly redisUrl: string;
private readonly username?: string;
private readonly password?: string;
private readonly indexName: string; private readonly indexName: string;
private readonly indexPrefix: string; private readonly indexPrefix: string;
private readonly schema: RedisSchema; private readonly schema: RedisSchema;
private _initPromise?: Promise<void>; private _initPromise?: Promise<void>;
constructor(config: RedisConfig) { constructor(config: RedisConfig) {
this.redisUrl = config.redisUrl;
this.username = config.username;
this.password = config.password;
this.indexName = config.collectionName; this.indexName = config.collectionName;
this.indexPrefix = `mem0:${config.collectionName}`; this.indexPrefix = `mem0:${config.collectionName}`;
@@ -177,12 +183,24 @@ export class RedisDB implements VectorStore {
}), }),
}; };
this.client = createClient({ this.initialize().catch((err) => {
url: config.redisUrl, console.error("Failed to initialize Redis:", err);
username: config.username, });
password: config.password, }
private async ensureClient(): Promise<void> {
if (this.client) return;
const sdk = await loadPeer(
"redis",
"Redis vector store",
() => import("redis"),
);
this.client = sdk.createClient({
url: this.redisUrl,
username: this.username,
password: this.password,
socket: { socket: {
reconnectStrategy: (retries) => { reconnectStrategy: (retries: number) => {
if (retries > 10) { if (retries > 10) {
console.error("Max reconnection attempts reached"); console.error("Max reconnection attempts reached");
return new Error("Max reconnection attempts reached"); return new Error("Max reconnection attempts reached");
@@ -194,10 +212,6 @@ export class RedisDB implements VectorStore {
this.client.on("error", (err) => console.error("Redis Client Error:", err)); this.client.on("error", (err) => console.error("Redis Client Error:", err));
this.client.on("connect", () => console.log("Redis Client Connected")); this.client.on("connect", () => console.log("Redis Client Connected"));
this.initialize().catch((err) => {
console.error("Failed to initialize Redis:", err);
});
} }
private async createIndex(): Promise<void> { private async createIndex(): Promise<void> {
@@ -260,6 +274,7 @@ export class RedisDB implements VectorStore {
} }
private async _doInitialize(): Promise<void> { private async _doInitialize(): Promise<void> {
await this.ensureClient();
try { try {
await this.client.connect(); await this.client.connect();
console.log("Connected to Redis"); console.log("Connected to Redis");
@@ -331,6 +346,7 @@ export class RedisDB implements VectorStore {
ids: string[], ids: string[],
payloads: Record<string, any>[], payloads: Record<string, any>[],
): Promise<void> { ): Promise<void> {
await this.initialize();
const data = vectors.map((vector, idx) => { const data = vectors.map((vector, idx) => {
const payload = toSnakeCase(payloads[idx]); const payload = toSnakeCase(payloads[idx]);
const id = ids[idx]; const id = ids[idx];
@@ -389,6 +405,7 @@ export class RedisDB implements VectorStore {
topK: number = 5, topK: number = 5,
filters?: SearchFilters, filters?: SearchFilters,
): Promise<VectorStoreResult[]> { ): Promise<VectorStoreResult[]> {
await this.initialize();
const snakeFilters = filters ? toSnakeCase(filters) : undefined; const snakeFilters = filters ? toSnakeCase(filters) : undefined;
const filterExpr = snakeFilters const filterExpr = snakeFilters
? Object.entries(snakeFilters) ? Object.entries(snakeFilters)
@@ -456,6 +473,7 @@ export class RedisDB implements VectorStore {
} }
async get(vectorId: string): Promise<VectorStoreResult | null> { async get(vectorId: string): Promise<VectorStoreResult | null> {
await this.initialize();
try { try {
// Check if the memory exists first // Check if the memory exists first
const exists = await this.client.exists( const exists = await this.client.exists(
@@ -562,6 +580,7 @@ export class RedisDB implements VectorStore {
vector: number[], vector: number[],
payload: Record<string, any>, payload: Record<string, any>,
): Promise<void> { ): Promise<void> {
await this.initialize();
const snakePayload = toSnakeCase(payload); const snakePayload = toSnakeCase(payload);
const createdAt = snakePayload.created_at const createdAt = snakePayload.created_at
? new Date(snakePayload.created_at).getTime() ? new Date(snakePayload.created_at).getTime()
@@ -601,6 +620,7 @@ export class RedisDB implements VectorStore {
} }
async delete(vectorId: string): Promise<void> { async delete(vectorId: string): Promise<void> {
await this.initialize();
try { try {
// Check if memory exists first // Check if memory exists first
const key = `${this.indexPrefix}:${vectorId}`; const key = `${this.indexPrefix}:${vectorId}`;
@@ -626,6 +646,7 @@ export class RedisDB implements VectorStore {
} }
async deleteCol(): Promise<void> { async deleteCol(): Promise<void> {
await this.initialize();
await this.client.ft.dropIndex(this.indexName); await this.client.ft.dropIndex(this.indexName);
} }
@@ -633,6 +654,7 @@ export class RedisDB implements VectorStore {
filters?: SearchFilters, filters?: SearchFilters,
topK: number = 100, topK: number = 100,
): Promise<[VectorStoreResult[], number]> { ): Promise<[VectorStoreResult[], number]> {
await this.initialize();
const snakeFilters = filters ? toSnakeCase(filters) : undefined; const snakeFilters = filters ? toSnakeCase(filters) : undefined;
const filterExpr = snakeFilters const filterExpr = snakeFilters
? Object.entries(snakeFilters) ? Object.entries(snakeFilters)
@@ -676,10 +698,11 @@ export class RedisDB implements VectorStore {
} }
async close(): Promise<void> { async close(): Promise<void> {
await this.client.quit(); if (this.client) await this.client.quit();
} }
async getUserId(): Promise<string> { async getUserId(): Promise<string> {
await this.initialize();
try { try {
// Check if the user ID exists in Redis // Check if the user ID exists in Redis
const userId = await this.client.get("memory_migrations:1"); const userId = await this.client.get("memory_migrations:1");
@@ -702,6 +725,7 @@ export class RedisDB implements VectorStore {
} }
async setUserId(userId: string): Promise<void> { async setUserId(userId: string): Promise<void> {
await this.initialize();
try { try {
await this.client.set("memory_migrations:1", userId); await this.client.set("memory_migrations:1", userId);
} catch (error) { } catch (error) {
+27 -3
View File
@@ -1,6 +1,7 @@
import { createClient, SupabaseClient } from "@supabase/supabase-js"; import type { SupabaseClient } from "@supabase/supabase-js";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
interface VectorData { interface VectorData {
id: string; id: string;
@@ -82,14 +83,17 @@ $$;
*/ */
export class SupabaseDB implements VectorStore { export class SupabaseDB implements VectorStore {
private client: SupabaseClient; private client!: SupabaseClient;
private readonly supabaseUrl: string;
private readonly supabaseKey: string;
private readonly tableName: string; private readonly tableName: string;
private readonly embeddingColumnName: string; private readonly embeddingColumnName: string;
private readonly metadataColumnName: string; private readonly metadataColumnName: string;
private _initPromise?: Promise<void>; private _initPromise?: Promise<void>;
constructor(config: SupabaseConfig) { constructor(config: SupabaseConfig) {
this.client = createClient(config.supabaseUrl, config.supabaseKey); this.supabaseUrl = config.supabaseUrl;
this.supabaseKey = config.supabaseKey;
this.tableName = config.tableName; this.tableName = config.tableName;
this.embeddingColumnName = config.embeddingColumnName || "embedding"; this.embeddingColumnName = config.embeddingColumnName || "embedding";
this.metadataColumnName = config.metadataColumnName || "metadata"; this.metadataColumnName = config.metadataColumnName || "metadata";
@@ -99,6 +103,16 @@ export class SupabaseDB implements VectorStore {
}); });
} }
private async ensureClient(): Promise<void> {
if (this.client) return;
const sdk = await loadPeer(
"@supabase/supabase-js",
"Supabase vector store",
() => import("@supabase/supabase-js"),
);
this.client = sdk.createClient(this.supabaseUrl, this.supabaseKey);
}
async initialize(): Promise<void> { async initialize(): Promise<void> {
if (!this._initPromise) { if (!this._initPromise) {
this._initPromise = this._doInitialize(); this._initPromise = this._doInitialize();
@@ -107,6 +121,7 @@ export class SupabaseDB implements VectorStore {
} }
private async _doInitialize(): Promise<void> { private async _doInitialize(): Promise<void> {
await this.ensureClient();
try { try {
// Verify table exists and vector operations work by attempting a test insert // Verify table exists and vector operations work by attempting a test insert
const testVector = Array(1536).fill(0); const testVector = Array(1536).fill(0);
@@ -209,6 +224,7 @@ See the SQL migration instructions in the code comments.`,
ids: string[], ids: string[],
payloads: Record<string, any>[], payloads: Record<string, any>[],
): Promise<void> { ): Promise<void> {
await this.initialize();
try { try {
const data = vectors.map((vector, idx) => ({ const data = vectors.map((vector, idx) => ({
id: ids[idx], id: ids[idx],
@@ -237,6 +253,7 @@ See the SQL migration instructions in the code comments.`,
topK: number = 5, topK: number = 5,
filters?: SearchFilters, filters?: SearchFilters,
): Promise<VectorStoreResult[]> { ): Promise<VectorStoreResult[]> {
await this.initialize();
try { try {
const rpcQuery: VectorQueryParams = { const rpcQuery: VectorQueryParams = {
query_embedding: query, query_embedding: query,
@@ -265,6 +282,7 @@ See the SQL migration instructions in the code comments.`,
} }
async get(vectorId: string): Promise<VectorStoreResult | null> { async get(vectorId: string): Promise<VectorStoreResult | null> {
await this.initialize();
try { try {
const { data, error } = await this.client const { data, error } = await this.client
.from(this.tableName) .from(this.tableName)
@@ -290,6 +308,7 @@ See the SQL migration instructions in the code comments.`,
vector: number[], vector: number[],
payload: Record<string, any>, payload: Record<string, any>,
): Promise<void> { ): Promise<void> {
await this.initialize();
try { try {
const { error } = await this.client const { error } = await this.client
.from(this.tableName) .from(this.tableName)
@@ -310,6 +329,7 @@ See the SQL migration instructions in the code comments.`,
} }
async delete(vectorId: string): Promise<void> { async delete(vectorId: string): Promise<void> {
await this.initialize();
try { try {
const { error } = await this.client const { error } = await this.client
.from(this.tableName) .from(this.tableName)
@@ -324,6 +344,7 @@ See the SQL migration instructions in the code comments.`,
} }
async deleteCol(): Promise<void> { async deleteCol(): Promise<void> {
await this.initialize();
try { try {
const { error } = await this.client const { error } = await this.client
.from(this.tableName) .from(this.tableName)
@@ -341,6 +362,7 @@ See the SQL migration instructions in the code comments.`,
filters?: SearchFilters, filters?: SearchFilters,
topK: number = 100, topK: number = 100,
): Promise<[VectorStoreResult[], number]> { ): Promise<[VectorStoreResult[], number]> {
await this.initialize();
try { try {
let query = this.client let query = this.client
.from(this.tableName) .from(this.tableName)
@@ -370,6 +392,7 @@ See the SQL migration instructions in the code comments.`,
} }
async getUserId(): Promise<string> { async getUserId(): Promise<string> {
await this.initialize();
try { try {
// First check if the table exists // First check if the table exists
const { data: tableExists } = await this.client const { data: tableExists } = await this.client
@@ -421,6 +444,7 @@ See the SQL migration instructions in the code comments.`,
} }
async setUserId(userId: string): Promise<void> { async setUserId(userId: string): Promise<void> {
await this.initialize();
try { try {
const { error: deleteError } = await this.client const { error: deleteError } = await this.client
.from("memory_migrations") .from("memory_migrations")
@@ -1,5 +1,6 @@
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
interface TurbopufferConfig extends VectorStoreConfig { interface TurbopufferConfig extends VectorStoreConfig {
apiKey?: string; apiKey?: string;
@@ -48,14 +49,11 @@ export class TurbopufferDB implements VectorStore {
} }
private async createClient(): Promise<any> { private async createClient(): Promise<any> {
let sdk: any; const sdk = await loadPeer(
try { "@turbopuffer/turbopuffer",
sdk = await import("@turbopuffer/turbopuffer"); "Turbopuffer vector store",
} catch { () => import("@turbopuffer/turbopuffer"),
throw new Error( );
"The '@turbopuffer/turbopuffer' package is required to use the Turbopuffer vector store. Install it with: npm install @turbopuffer/turbopuffer",
);
}
// @turbopuffer/turbopuffer ships `Turbopuffer` as both the default export // @turbopuffer/turbopuffer ships `Turbopuffer` as both the default export
// and a named export pointing at the same class. Use `.default` since // and a named export pointing at the same class. Use `.default` since
@@ -1,6 +1,7 @@
import type { Index, QueryResult, Vector } from "@upstash/vector"; import type { Index, QueryResult, Vector } from "@upstash/vector";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
interface UpstashVectorConfig extends VectorStoreConfig { interface UpstashVectorConfig extends VectorStoreConfig {
collectionName: string; collectionName: string;
@@ -42,14 +43,11 @@ export class UpstashVector implements VectorStore {
if (config.client) { if (config.client) {
this.client = config.client; this.client = config.client;
} else { } else {
let sdk: any; const sdk = await loadPeer(
try { "@upstash/vector",
sdk = await import("@upstash/vector"); "Upstash Vector store",
} catch { () => import("@upstash/vector"),
throw new Error( );
"The '@upstash/vector' package is required to use the Upstash Vector store. Install it with: npm install @upstash/vector",
);
}
this.client = new sdk.Index({ this.client = new sdk.Index({
url: config.url, url: config.url,
token: config.token, token: config.token,
+3 -8
View File
@@ -1,6 +1,7 @@
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreResult } from "../types";
import { ValkeyConfig } from "../types/valkey"; import { ValkeyConfig } from "../types/valkey";
import { loadPeer } from "../utils/load_peer";
interface ValkeyClient { interface ValkeyClient {
call: (...args: (string | number | Buffer)[]) => Promise<unknown>; call: (...args: (string | number | Buffer)[]) => Promise<unknown>;
@@ -142,14 +143,8 @@ function formatTimestamp(timestamp: number, timezone: string = "UTC"): string {
return `${yyyy}-${MM}-${dd}T${HH}:${mm}:${ss}${sign}${offHH}:${offMM}`; return `${yyyy}-${MM}-${dd}T${HH}:${mm}:${ss}${sign}${offHH}:${offMM}`;
} }
async function loadIovalkey(): Promise<typeof import("iovalkey")> { function loadIovalkey(): Promise<typeof import("iovalkey")> {
try { return loadPeer("iovalkey", "Valkey vector store", () => import("iovalkey"));
return await import("iovalkey");
} catch {
throw new Error(
"iovalkey is required for the Valkey vector store. Install it with: npm install iovalkey",
);
}
} }
export class ValkeyDB implements VectorStore { export class ValkeyDB implements VectorStore {
+24 -2
View File
@@ -1,7 +1,8 @@
import Cloudflare from "cloudflare"; import type Cloudflare from "cloudflare";
import type { Vectorize, VectorizeVector } from "@cloudflare/workers-types"; import type { Vectorize, VectorizeVector } from "@cloudflare/workers-types";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
interface VectorizeConfig extends VectorStoreConfig { interface VectorizeConfig extends VectorStoreConfig {
apiKey?: string; apiKey?: string;
@@ -17,24 +18,36 @@ interface CloudflareVector {
export class VectorizeDB implements VectorStore { export class VectorizeDB implements VectorStore {
private client: Cloudflare | null = null; private client: Cloudflare | null = null;
private apiKey?: string;
private dimensions: number; private dimensions: number;
private indexName: string; private indexName: string;
private accountId: string; private accountId: string;
private _initPromise?: Promise<void>; private _initPromise?: Promise<void>;
constructor(config: VectorizeConfig) { constructor(config: VectorizeConfig) {
this.client = new Cloudflare({ apiToken: config.apiKey }); this.apiKey = config.apiKey;
this.dimensions = config.dimension || 1536; this.dimensions = config.dimension || 1536;
this.indexName = config.indexName; this.indexName = config.indexName;
this.accountId = config.accountId; this.accountId = config.accountId;
this.initialize().catch(console.error); this.initialize().catch(console.error);
} }
private async ensureClient(): Promise<void> {
if (this.client) return;
const sdk = await loadPeer(
"cloudflare",
"Vectorize vector store",
() => import("cloudflare"),
);
this.client = new sdk.default({ apiToken: this.apiKey });
}
async insert( async insert(
vectors: number[][], vectors: number[][],
ids: string[], ids: string[],
payloads: Record<string, any>[], payloads: Record<string, any>[],
): Promise<void> { ): Promise<void> {
await this.initialize();
try { try {
const vectorObjects: CloudflareVector[] = vectors.map( const vectorObjects: CloudflareVector[] = vectors.map(
(vector, index) => ({ (vector, index) => ({
@@ -83,6 +96,7 @@ export class VectorizeDB implements VectorStore {
topK: number = 5, topK: number = 5,
filters?: SearchFilters, filters?: SearchFilters,
): Promise<VectorStoreResult[]> { ): Promise<VectorStoreResult[]> {
await this.initialize();
try { try {
const result = await this.client?.vectorize.indexes.query( const result = await this.client?.vectorize.indexes.query(
this.indexName, this.indexName,
@@ -111,6 +125,7 @@ export class VectorizeDB implements VectorStore {
} }
async get(vectorId: string): Promise<VectorStoreResult | null> { async get(vectorId: string): Promise<VectorStoreResult | null> {
await this.initialize();
try { try {
const result = (await this.client?.vectorize.indexes.getByIds( const result = (await this.client?.vectorize.indexes.getByIds(
this.indexName, this.indexName,
@@ -139,6 +154,7 @@ export class VectorizeDB implements VectorStore {
vector: number[], vector: number[],
payload: Record<string, any>, payload: Record<string, any>,
): Promise<void> { ): Promise<void> {
await this.initialize();
try { try {
const data: VectorizeVector = { const data: VectorizeVector = {
id: vectorId, id: vectorId,
@@ -173,6 +189,7 @@ export class VectorizeDB implements VectorStore {
} }
async delete(vectorId: string): Promise<void> { async delete(vectorId: string): Promise<void> {
await this.initialize();
try { try {
await this.client?.vectorize.indexes.deleteByIds(this.indexName, { await this.client?.vectorize.indexes.deleteByIds(this.indexName, {
account_id: this.accountId, account_id: this.accountId,
@@ -187,6 +204,7 @@ export class VectorizeDB implements VectorStore {
} }
async deleteCol(): Promise<void> { async deleteCol(): Promise<void> {
await this.initialize();
try { try {
await this.client?.vectorize.indexes.delete(this.indexName, { await this.client?.vectorize.indexes.delete(this.indexName, {
account_id: this.accountId, account_id: this.accountId,
@@ -203,6 +221,7 @@ export class VectorizeDB implements VectorStore {
filters?: SearchFilters, filters?: SearchFilters,
topK: number = 20, topK: number = 20,
): Promise<[VectorStoreResult[], number]> { ): Promise<[VectorStoreResult[], number]> {
await this.initialize();
try { try {
const result = await this.client?.vectorize.indexes.query( const result = await this.client?.vectorize.indexes.query(
this.indexName, this.indexName,
@@ -243,6 +262,7 @@ export class VectorizeDB implements VectorStore {
} }
async getUserId(): Promise<string> { async getUserId(): Promise<string> {
await this.initialize();
try { try {
let found = false; let found = false;
for await (const index of this.client!.vectorize.indexes.list({ for await (const index of this.client!.vectorize.indexes.list({
@@ -309,6 +329,7 @@ export class VectorizeDB implements VectorStore {
} }
async setUserId(userId: string): Promise<void> { async setUserId(userId: string): Promise<void> {
await this.initialize();
try { try {
// Get existing point ID // Get existing point ID
const result: any = await this.client?.vectorize.indexes.query( const result: any = await this.client?.vectorize.indexes.query(
@@ -355,6 +376,7 @@ export class VectorizeDB implements VectorStore {
} }
private async _doInitialize(): Promise<void> { private async _doInitialize(): Promise<void> {
await this.ensureClient();
try { try {
// Check if the index already exists // Check if the index already exists
let indexFound = false; let indexFound = false;
@@ -2,6 +2,7 @@ import type { WeaviateClient } from "weaviate-client";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import { VectorStore } from "./base"; import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types"; import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import { loadPeer } from "../utils/load_peer";
interface WeaviateConfig extends VectorStoreConfig { interface WeaviateConfig extends VectorStoreConfig {
/** Pre-configured Weaviate client instance (typed as `any` to keep the /** Pre-configured Weaviate client instance (typed as `any` to keep the
@@ -50,14 +51,11 @@ export class WeaviateDB implements VectorStore {
private async ensureClient(): Promise<void> { private async ensureClient(): Promise<void> {
if (this._client) return; if (this._client) return;
let sdk: any; const sdk = await loadPeer(
try { "weaviate-client",
sdk = await import("weaviate-client"); "Weaviate vector store",
} catch { () => import("weaviate-client"),
throw new Error( );
"The 'weaviate-client' package is required to use the Weaviate vector store. Install it with: npm install weaviate-client",
);
}
this._sdk = sdk; this._sdk = sdk;
const { client, clusterUrl, apiKey, additionalHeaders } = this._config; const { client, clusterUrl, apiKey, additionalHeaders } = this._config;
+13 -4
View File
@@ -23,11 +23,16 @@ describe("AnthropicLLM (unit)", () => {
// Regression #5665: a configured baseURL must reach the Anthropic client so // Regression #5665: a configured baseURL must reach the Anthropic client so
// proxy/gateway users are not silently bypassed (TS parity with #5626). // proxy/gateway users are not silently bypassed (TS parity with #5626).
it("forwards baseURL to the Anthropic client when set", () => { // The client is constructed lazily on first use, so drive generateResponse.
new AnthropicLLM({ it("forwards baseURL to the Anthropic client when set", async () => {
mockCreate.mockResolvedValueOnce({
content: [{ type: "text", text: "ok" }],
});
const llm = new AnthropicLLM({
apiKey: "test-key", apiKey: "test-key",
baseURL: "https://proxy.example/v1", baseURL: "https://proxy.example/v1",
}); });
await llm.generateResponse([{ role: "user", content: "Hi" }]);
expect(mockConstructor).toHaveBeenCalledTimes(1); expect(mockConstructor).toHaveBeenCalledTimes(1);
const ctorArgs = mockConstructor.mock.calls[0][0]; const ctorArgs = mockConstructor.mock.calls[0][0];
@@ -37,8 +42,12 @@ describe("AnthropicLLM (unit)", () => {
// When no baseURL is configured the client must not receive a baseURL key // When no baseURL is configured the client must not receive a baseURL key
// (so the SDK default endpoint is used). // (so the SDK default endpoint is used).
it("does NOT set baseURL when none is configured", () => { it("does NOT set baseURL when none is configured", async () => {
new AnthropicLLM({ apiKey: "test-key" }); mockCreate.mockResolvedValueOnce({
content: [{ type: "text", text: "ok" }],
});
const llm = new AnthropicLLM({ apiKey: "test-key" });
await llm.generateResponse([{ role: "user", content: "Hi" }]);
expect(mockConstructor).toHaveBeenCalledTimes(1); expect(mockConstructor).toHaveBeenCalledTimes(1);
const ctorArgs = mockConstructor.mock.calls[0][0]; const ctorArgs = mockConstructor.mock.calls[0][0];
+10 -1
View File
@@ -22,7 +22,16 @@ function sourceFiles(dir: string, acc: string[] = []): string[] {
for (const entry of readdirSync(dir)) { for (const entry of readdirSync(dir)) {
const full = join(dir, entry); const full = join(dir, entry);
if (statSync(full).isDirectory()) { if (statSync(full).isDirectory()) {
if (entry !== "tests" && entry !== "__tests__") sourceFiles(full, acc); // examples/ are dev scripts and community/ is the separate @mem0/community
// package — neither ships in files:["dist"] nor is reachable via the mem0ai/oss
// barrel, so a static import there cannot crash a Memory() consumer.
if (
entry !== "tests" &&
entry !== "__tests__" &&
entry !== "examples" &&
entry !== "community"
)
sourceFiles(full, acc);
} else if (entry.endsWith(".ts") && !entry.endsWith(".test.ts")) { } else if (entry.endsWith(".ts") && !entry.endsWith(".test.ts")) {
acc.push(full); acc.push(full);
} }
+29 -23
View File
@@ -40,14 +40,15 @@ beforeEach(() => {
}); });
describe("Qdrant URL port extraction (qdrant/qdrant-js#59 workaround)", () => { describe("Qdrant URL port extraction (qdrant/qdrant-js#59 workaround)", () => {
it("extracts port from HTTPS URL with explicit port", () => { it("extracts port from HTTPS URL with explicit port", async () => {
new Qdrant({ const store = new Qdrant({
url: "https://my-cluster.us-west-1-0.aws.cloud.qdrant.io:6333", url: "https://my-cluster.us-west-1-0.aws.cloud.qdrant.io:6333",
apiKey: "test-key", apiKey: "test-key",
collectionName: "test", collectionName: "test",
embeddingModelDims: 768, embeddingModelDims: 768,
dimension: 768, dimension: 768,
}); });
await store.initialize();
expect(capturedParams).toBeDefined(); expect(capturedParams).toBeDefined();
expect(capturedParams!.url).toBe( expect(capturedParams!.url).toBe(
@@ -57,47 +58,50 @@ describe("Qdrant URL port extraction (qdrant/qdrant-js#59 workaround)", () => {
expect(capturedParams!.apiKey).toBe("test-key"); expect(capturedParams!.apiKey).toBe("test-key");
}); });
it("extracts port from HTTP URL with explicit port", () => { it("extracts port from HTTP URL with explicit port", async () => {
new Qdrant({ const store = new Qdrant({
url: "http://localhost:6333", url: "http://localhost:6333",
collectionName: "test", collectionName: "test",
embeddingModelDims: 768, embeddingModelDims: 768,
dimension: 768, dimension: 768,
}); });
await store.initialize();
expect(capturedParams).toBeDefined(); expect(capturedParams).toBeDefined();
expect(capturedParams!.url).toBe("http://localhost:6333"); expect(capturedParams!.url).toBe("http://localhost:6333");
expect(capturedParams!.port).toBe(6333); expect(capturedParams!.port).toBe(6333);
}); });
it("defaults to port 6333 when HTTPS URL has no explicit port", () => { it("defaults to port 6333 when HTTPS URL has no explicit port", async () => {
new Qdrant({ const store = new Qdrant({
url: "https://my-cluster.cloud.qdrant.io", url: "https://my-cluster.cloud.qdrant.io",
apiKey: "test-key", apiKey: "test-key",
collectionName: "test", collectionName: "test",
embeddingModelDims: 768, embeddingModelDims: 768,
dimension: 768, dimension: 768,
}); });
await store.initialize();
expect(capturedParams).toBeDefined(); expect(capturedParams).toBeDefined();
expect(capturedParams!.url).toBe("https://my-cluster.cloud.qdrant.io"); expect(capturedParams!.url).toBe("https://my-cluster.cloud.qdrant.io");
expect(capturedParams!.port).toBe(6333); expect(capturedParams!.port).toBe(6333);
}); });
it("defaults to port 6333 when HTTP URL has no explicit port", () => { it("defaults to port 6333 when HTTP URL has no explicit port", async () => {
new Qdrant({ const store = new Qdrant({
url: "http://localhost", url: "http://localhost",
collectionName: "test", collectionName: "test",
embeddingModelDims: 768, embeddingModelDims: 768,
dimension: 768, dimension: 768,
}); });
await store.initialize();
expect(capturedParams).toBeDefined(); expect(capturedParams).toBeDefined();
expect(capturedParams!.port).toBe(6333); expect(capturedParams!.port).toBe(6333);
}); });
it("host+port config overrides URL-extracted port", () => { it("host+port config overrides URL-extracted port", async () => {
new Qdrant({ const store = new Qdrant({
url: "https://my-cluster.cloud.qdrant.io:6333", url: "https://my-cluster.cloud.qdrant.io:6333",
host: "custom-host", host: "custom-host",
port: 9999, port: 9999,
@@ -106,28 +110,28 @@ describe("Qdrant URL port extraction (qdrant/qdrant-js#59 workaround)", () => {
embeddingModelDims: 768, embeddingModelDims: 768,
dimension: 768, dimension: 768,
}); });
await store.initialize();
expect(capturedParams).toBeDefined(); expect(capturedParams).toBeDefined();
expect(capturedParams!.host).toBe("custom-host"); expect(capturedParams!.host).toBe("custom-host");
expect(capturedParams!.port).toBe(9999); expect(capturedParams!.port).toBe(9999);
}); });
it("handles invalid URL gracefully without crashing", () => { it("handles invalid URL gracefully without crashing", async () => {
expect(() => { const store = new Qdrant({
new Qdrant({ url: "not-a-valid-url",
url: "not-a-valid-url", collectionName: "test",
collectionName: "test", embeddingModelDims: 768,
embeddingModelDims: 768, dimension: 768,
dimension: 768, });
}); await expect(store.initialize()).resolves.not.toThrow();
}).not.toThrow();
expect(capturedParams).toBeDefined(); expect(capturedParams).toBeDefined();
expect(capturedParams!.url).toBe("not-a-valid-url"); expect(capturedParams!.url).toBe("not-a-valid-url");
expect(capturedParams!.port).toBe(6333); expect(capturedParams!.port).toBe(6333);
}); });
it("does not pass port when using pre-configured client", () => { it("does not pass port when using pre-configured client", async () => {
const mockClient: any = { const mockClient: any = {
createCollection: jest.fn().mockResolvedValue(undefined), createCollection: jest.fn().mockResolvedValue(undefined),
getCollection: jest.fn().mockResolvedValue({ getCollection: jest.fn().mockResolvedValue({
@@ -141,12 +145,13 @@ describe("Qdrant URL port extraction (qdrant/qdrant-js#59 workaround)", () => {
deleteCollection: jest.fn().mockResolvedValue(undefined), deleteCollection: jest.fn().mockResolvedValue(undefined),
}; };
new Qdrant({ const store = new Qdrant({
client: mockClient, client: mockClient,
collectionName: "test", collectionName: "test",
embeddingModelDims: 768, embeddingModelDims: 768,
dimension: 768, dimension: 768,
}); });
await store.initialize();
// QdrantClient constructor should NOT have been called // QdrantClient constructor should NOT have been called
expect( expect(
@@ -154,14 +159,15 @@ describe("Qdrant URL port extraction (qdrant/qdrant-js#59 workaround)", () => {
).not.toHaveBeenCalled(); ).not.toHaveBeenCalled();
}); });
it("defaults to 6333 when HTTPS URL uses default port 443", () => { it("defaults to 6333 when HTTPS URL uses default port 443", async () => {
new Qdrant({ const store = new Qdrant({
url: "https://my-cluster.cloud.qdrant.io:443", url: "https://my-cluster.cloud.qdrant.io:443",
apiKey: "test-key", apiKey: "test-key",
collectionName: "test", collectionName: "test",
embeddingModelDims: 768, embeddingModelDims: 768,
dimension: 768, dimension: 768,
}); });
await store.initialize();
// 443 is default for HTTPS, so URL.port returns empty string — we default to 6333 // 443 is default for HTTPS, so URL.port returns empty string — we default to 6333
expect(capturedParams).toBeDefined(); expect(capturedParams).toBeDefined();