diff --git a/docs/changelog/overview.mdx b/docs/changelog/overview.mdx index 5969a178..e6e0dbbd 100644 --- a/docs/changelog/overview.mdx +++ b/docs/changelog/overview.mdx @@ -90,6 +90,11 @@ mode: "wide" + +**Improvements:** +- **Client:** Added support for Mem0 to work with Chrome Extensions + + **New Features:** - **Mastra Example:** Added Mastra example diff --git a/mem0-ts/package.json b/mem0-ts/package.json index 2b01b782..d634ccde 100644 --- a/mem0-ts/package.json +++ b/mem0-ts/package.json @@ -1,6 +1,6 @@ { "name": "mem0ai", - "version": "2.1.13", + "version": "2.1.15", "description": "The Memory Layer For Your AI Apps", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -93,12 +93,14 @@ "dependencies": { "axios": "1.7.7", "openai": "4.28.0", + "redis": "^4.6.13", "uuid": "9.0.1", "zod": "3.22.4" }, "peerDependencies": { "@anthropic-ai/sdk": "0.18.0", "@qdrant/js-client-rest": "1.13.0", + "@google/genai": "^0.7.0", "@supabase/supabase-js": "^2.49.1", "@types/jest": "29.5.14", "@types/pg": "8.11.0", diff --git a/mem0-ts/pnpm-lock.yaml b/mem0-ts/pnpm-lock.yaml index 9bdb9b48..c8f1427e 100644 --- a/mem0-ts/pnpm-lock.yaml +++ b/mem0-ts/pnpm-lock.yaml @@ -10,6 +10,9 @@ importers: "@anthropic-ai/sdk": specifier: 0.18.0 version: 0.18.0(encoding@0.1.13) + "@google/genai": + specifier: ^0.7.0 + version: 0.7.0(encoding@0.1.13) "@qdrant/js-client-rest": specifier: 1.13.0 version: 1.13.0(typescript@5.5.4) @@ -615,6 +618,13 @@ packages: integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==, } + "@google/genai@0.7.0": + resolution: + { + integrity: sha512-r+Fwj/emnXZN5R+4JCxDXboY4AGTmTn7+Wnori5dgyJiStP0P82f9YYL0CVsCnDIumNY2i0UIcZ1zGZdtHJ34w==, + } + engines: { node: ">=18.0.0" } + "@isaacs/cliui@8.0.2": resolution: { @@ -1313,6 +1323,13 @@ packages: } engines: { node: ">= 6.0.0" } + agent-base@7.1.3: + resolution: + { + integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==, + } + engines: { node: ">= 14" } + agentkeepalive@4.6.0: resolution: { @@ -1484,6 +1501,12 @@ packages: integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, } + bignumber.js@9.2.0: + resolution: + { + integrity: sha512-JocpCSOixzy5XFJi2ub6IMmV/G9i8Lrm2lZvwBv9xPdglmZM0ufDVBbjbrfU/zuLvBfD7Bv2eYxz9i+OHTgkew==, + } + binary-extensions@2.3.0: resolution: { @@ -1543,6 +1566,12 @@ packages: integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==, } + buffer-equal-constant-time@1.0.1: + resolution: + { + integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==, + } + buffer-from@1.1.2: resolution: { @@ -1916,6 +1945,12 @@ packages: integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==, } + ecdsa-sig-formatter@1.0.11: + resolution: + { + integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==, + } + ejs@3.1.10: resolution: { @@ -2073,6 +2108,12 @@ packages: } engines: { node: ^14.15.0 || ^16.10.0 || >=18.0.0 } + extend@3.0.2: + resolution: + { + integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==, + } + fast-glob@3.3.3: resolution: { @@ -2222,6 +2263,20 @@ packages: engines: { node: ^12.13.0 || ^14.15.0 || >=16.0.0 } deprecated: This package is no longer supported. + gaxios@6.7.1: + resolution: + { + integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==, + } + engines: { node: ">=14" } + + gcp-metadata@6.1.1: + resolution: + { + integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==, + } + engines: { node: ">=14" } + generic-pool@3.9.0: resolution: { @@ -2305,6 +2360,20 @@ packages: } engines: { node: ">=4" } + google-auth-library@9.15.1: + resolution: + { + integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==, + } + engines: { node: ">=14" } + + google-logging-utils@0.0.2: + resolution: + { + integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==, + } + engines: { node: ">=14" } + gopd@1.2.0: resolution: { @@ -2324,6 +2393,13 @@ packages: integrity: sha512-Cdgjh4YoSBE2X4S9sxPGXaAy1dlN4bRtAaDZ3cnq+XsxhhN9WSBeHF64l7LWwuD5ntmw7YC5Vf4Ff1oHCg1LOg==, } + gtoken@7.1.0: + resolution: + { + integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==, + } + engines: { node: ">=14.0.0" } + has-flag@3.0.0: resolution: { @@ -2398,6 +2474,13 @@ packages: } engines: { node: ">= 6" } + https-proxy-agent@7.0.6: + resolution: + { + integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==, + } + engines: { node: ">= 14" } + human-signals@2.1.0: resolution: { @@ -2861,6 +2944,12 @@ packages: engines: { node: ">=6" } hasBin: true + json-bigint@1.0.0: + resolution: + { + integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==, + } + json-parse-even-better-errors@2.3.1: resolution: { @@ -2882,6 +2971,18 @@ packages: engines: { node: ">=6" } hasBin: true + jwa@2.0.0: + resolution: + { + integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==, + } + + jws@4.0.0: + resolution: + { + integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==, + } + kleur@3.0.3: resolution: { @@ -4913,6 +5014,16 @@ snapshots: "@gar/promisify@1.1.3": optional: true + "@google/genai@0.7.0(encoding@0.1.13)": + dependencies: + google-auth-library: 9.15.1(encoding@0.1.13) + ws: 8.18.1 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + "@isaacs/cliui@8.0.2": dependencies: string-width: 5.1.2 @@ -5406,6 +5517,8 @@ snapshots: - supports-color optional: true + agent-base@7.1.3: {} + agentkeepalive@4.6.0: dependencies: humanize-ms: 1.2.1 @@ -5527,6 +5640,8 @@ snapshots: base64-js@1.5.1: {} + bignumber.js@9.2.0: {} + binary-extensions@2.3.0: {} bindings@1.5.0: @@ -5567,6 +5682,8 @@ snapshots: dependencies: node-int64: 0.4.0 + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer-writer@2.0.0: {} @@ -5766,6 +5883,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ejs@3.1.10: dependencies: jake: 10.9.2 @@ -5872,6 +5993,8 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 + extend@3.0.2: {} + fast-glob@3.3.3: dependencies: "@nodelib/fs.stat": 2.0.5 @@ -5962,6 +6085,26 @@ snapshots: wide-align: 1.1.5 optional: true + gaxios@6.7.1(encoding@0.1.13): + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0(encoding@0.1.13) + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.1(encoding@0.1.13): + dependencies: + gaxios: 6.7.1(encoding@0.1.13) + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + generic-pool@3.9.0: {} gensync@1.0.0-beta.2: {} @@ -6016,6 +6159,20 @@ snapshots: globals@11.12.0: {} + google-auth-library@9.15.1(encoding@0.1.13): + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1(encoding@0.1.13) + gcp-metadata: 6.1.1(encoding@0.1.13) + gtoken: 7.1.0(encoding@0.1.13) + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + google-logging-utils@0.0.2: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -6034,6 +6191,14 @@ snapshots: transitivePeerDependencies: - encoding + gtoken@7.1.0(encoding@0.1.13): + dependencies: + gaxios: 6.7.1(encoding@0.1.13) + jws: 4.0.0 + transitivePeerDependencies: + - encoding + - supports-color + has-flag@3.0.0: {} has-flag@4.0.0: {} @@ -6077,6 +6242,13 @@ snapshots: - supports-color optional: true + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.3 + debug: 4.4.0(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} humanize-ms@1.2.1: @@ -6528,12 +6700,27 @@ snapshots: jsesc@3.1.0: {} + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.2.0 + json-parse-even-better-errors@2.3.1: {} json-parse-even-better-errors@3.0.2: {} json5@2.2.3: {} + jwa@2.0.0: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.0: + dependencies: + jwa: 2.0.0 + safe-buffer: 5.2.1 + kleur@3.0.3: {} kolorist@1.8.0: {} diff --git a/mem0-ts/src/client/telemetry.ts b/mem0-ts/src/client/telemetry.ts index e45aaadd..ab56cb8f 100644 --- a/mem0-ts/src/client/telemetry.ts +++ b/mem0-ts/src/client/telemetry.ts @@ -4,7 +4,10 @@ import type { TelemetryClient, TelemetryOptions } from "./telemetry.types"; let version = "2.1.12"; // Safely check for process.env in different environments -const MEM0_TELEMETRY = process?.env?.MEM0_TELEMETRY === "false" ? false : true; +let MEM0_TELEMETRY = true; +try { + MEM0_TELEMETRY = process?.env?.MEM0_TELEMETRY === "false" ? false : true; +} catch (error) {} const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"; const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"; diff --git a/mem0-ts/src/oss/src/llms/google.ts b/mem0-ts/src/oss/src/llms/google.ts index 735a2c38..685cef79 100644 --- a/mem0-ts/src/oss/src/llms/google.ts +++ b/mem0-ts/src/oss/src/llms/google.ts @@ -35,7 +35,9 @@ export class GoogleLLM implements LLM { // }, }); - const text = completion.text?.replace(/^```json\n/, "").replace(/\n```$/, ""); + const text = completion.text + ?.replace(/^```json\n/, "") + .replace(/\n```$/, ""); return text || ""; } diff --git a/mem0-ts/src/oss/src/memory/index.ts b/mem0-ts/src/oss/src/memory/index.ts index 8c930550..42e8e350 100644 --- a/mem0-ts/src/oss/src/memory/index.ts +++ b/mem0-ts/src/oss/src/memory/index.ts @@ -34,6 +34,8 @@ import { } from "./memory.types"; import { parse_vision_messages } from "../utils/memory"; import { HistoryManager } from "../storage/base"; +import { captureClientEvent } from "../utils/telemetry"; + export class Memory { private config: MemoryConfig; private customPrompt: string | undefined; @@ -45,6 +47,7 @@ export class Memory { private apiVersion: string; private graphMemory?: MemoryGraph; private enableGraph: boolean; + telemetryId: string; constructor(config: Partial = {}) { // Merge and validate config @@ -85,11 +88,58 @@ export class Memory { this.collectionName = this.config.vectorStore.config.collectionName; this.apiVersion = this.config.version || "v1.0"; this.enableGraph = this.config.enableGraph || false; + this.telemetryId = "anonymous"; // Initialize graph memory if configured if (this.enableGraph && this.config.graphStore) { this.graphMemory = new MemoryGraph(this.config); } + + // Initialize telemetry if vector store is initialized + this._initializeTelemetry(); + } + + private async _initializeTelemetry() { + try { + await this._getTelemetryId(); + + // Capture initialization event + await captureClientEvent("init", this, { + api_version: this.apiVersion, + client_type: "Memory", + collection_name: this.collectionName, + enable_graph: this.enableGraph, + }); + } catch (error) {} + } + + private async _getTelemetryId() { + try { + if ( + !this.telemetryId || + this.telemetryId === "anonymous" || + this.telemetryId === "anonymous-supabase" + ) { + this.telemetryId = await this.vectorStore.getUserId(); + } + return this.telemetryId; + } catch (error) { + this.telemetryId = "anonymous"; + return this.telemetryId; + } + } + + private async _captureEvent(methodName: string, additionalData = {}) { + try { + await this._getTelemetryId(); + await captureClientEvent(methodName, this, { + ...additionalData, + api_version: this.apiVersion, + collection_name: this.collectionName, + }); + } catch (error) { + console.error(`Failed to capture ${methodName} event:`, error); + } } static fromConfig(configDict: Record): Memory { @@ -106,6 +156,12 @@ export class Memory { messages: string | Message[], config: AddMemoryOptions, ): Promise { + await this._captureEvent("add", { + message_count: Array.isArray(messages) ? messages.length : 1, + has_metadata: !!config.metadata, + has_filters: !!config.filters, + infer: config.infer, + }); const { userId, agentId, @@ -341,6 +397,11 @@ export class Memory { query: string, config: SearchMemoryOptions, ): Promise { + await this._captureEvent("search", { + query_length: query.length, + limit: config.limit, + has_filters: !!config.filters, + }); const { userId, agentId, runId, limit = 100, filters = {} } = config; if (userId) filters.userId = userId; @@ -402,12 +463,14 @@ export class Memory { } async update(memoryId: string, data: string): Promise<{ message: string }> { + await this._captureEvent("update", { memory_id: memoryId }); const embedding = await this.embedder.embed(data); await this.updateMemory(memoryId, data, { [data]: embedding }); return { message: "Memory updated successfully!" }; } async delete(memoryId: string): Promise<{ message: string }> { + await this._captureEvent("delete", { memory_id: memoryId }); await this.deleteMemory(memoryId); return { message: "Memory deleted successfully!" }; } @@ -415,6 +478,11 @@ export class Memory { async deleteAll( config: DeleteAllMemoryOptions, ): Promise<{ message: string }> { + await this._captureEvent("delete_all", { + has_user_id: !!config.userId, + has_agent_id: !!config.agentId, + has_run_id: !!config.runId, + }); const { userId, agentId, runId } = config; const filters: SearchFilters = {}; @@ -441,6 +509,7 @@ export class Memory { } async reset(): Promise { + await this._captureEvent("reset"); await this.db.reset(); await this.vectorStore.deleteCol(); if (this.graphMemory) { @@ -453,6 +522,12 @@ export class Memory { } async getAll(config: GetAllMemoryOptions): Promise { + await this._captureEvent("get_all", { + limit: config.limit, + has_user_id: !!config.userId, + has_agent_id: !!config.agentId, + has_run_id: !!config.runId, + }); const { userId, agentId, runId, limit = 100 } = config; const filters: SearchFilters = {}; diff --git a/mem0-ts/src/oss/src/utils/telemetry.ts b/mem0-ts/src/oss/src/utils/telemetry.ts new file mode 100644 index 00000000..a478db02 --- /dev/null +++ b/mem0-ts/src/oss/src/utils/telemetry.ts @@ -0,0 +1,98 @@ +import type { + TelemetryClient, + TelemetryInstance, + TelemetryEventData, +} from "./telemetry.types"; + +let version = "2.1.15"; + +// Safely check for process.env in different environments +let MEM0_TELEMETRY = true; +try { + MEM0_TELEMETRY = process?.env?.MEM0_TELEMETRY === "false" ? false : true; +} catch (error) {} +const POSTHOG_API_KEY = "phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX"; +const POSTHOG_HOST = "https://us.i.posthog.com/i/v0/e/"; + +class UnifiedTelemetry implements TelemetryClient { + private apiKey: string; + private host: string; + + constructor(projectApiKey: string, host: string) { + this.apiKey = projectApiKey; + this.host = host; + } + + async captureEvent(distinctId: string, eventName: string, properties = {}) { + if (!MEM0_TELEMETRY) return; + + const eventProperties = { + client_version: version, + timestamp: new Date().toISOString(), + ...properties, + $process_person_profile: + distinctId === "anonymous" || distinctId === "anonymous-supabase" + ? false + : true, + $lib: "posthog-node", + }; + + const payload = { + api_key: this.apiKey, + distinct_id: distinctId, + event: eventName, + properties: eventProperties, + }; + + try { + const response = await fetch(this.host, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + + if (!response.ok) { + console.error("Telemetry event capture failed:", await response.text()); + } + } catch (error) { + console.error("Telemetry event capture failed:", error); + } + } + + async shutdown() { + // No shutdown needed for direct API calls + } +} + +const telemetry = new UnifiedTelemetry(POSTHOG_API_KEY, POSTHOG_HOST); + +async function captureClientEvent( + eventName: string, + instance: TelemetryInstance, + additionalData: Record = {}, +) { + if (!instance.telemetryId) { + console.warn("No telemetry ID found for instance"); + return; + } + + const eventData: TelemetryEventData = { + function: `${instance.constructor.name}`, + method: eventName, + api_host: instance.host, + timestamp: new Date().toISOString(), + client_version: version, + client_source: "nodejs", + ...additionalData, + }; + + await telemetry.captureEvent( + instance.telemetryId, + `mem0.${eventName}`, + eventData, + ); +} + +export { telemetry, captureClientEvent }; diff --git a/mem0-ts/src/oss/src/utils/telemetry.types.ts b/mem0-ts/src/oss/src/utils/telemetry.types.ts new file mode 100644 index 00000000..5b307d99 --- /dev/null +++ b/mem0-ts/src/oss/src/utils/telemetry.types.ts @@ -0,0 +1,34 @@ +export interface TelemetryClient { + captureEvent( + distinctId: string, + eventName: string, + properties?: Record, + ): Promise; + shutdown(): Promise; +} + +export interface TelemetryInstance { + telemetryId: string; + constructor: { + name: string; + }; + host?: string; + apiKey?: string; +} + +export interface TelemetryEventData { + function: string; + method: string; + api_host?: string; + timestamp?: string; + client_source: "browser" | "nodejs"; + client_version: string; + [key: string]: any; +} + +export interface TelemetryOptions { + enabled?: boolean; + apiKey?: string; + host?: string; + version?: string; +} diff --git a/mem0-ts/src/oss/src/vector_stores/base.ts b/mem0-ts/src/oss/src/vector_stores/base.ts index a5bdef75..cb6f24aa 100644 --- a/mem0-ts/src/oss/src/vector_stores/base.ts +++ b/mem0-ts/src/oss/src/vector_stores/base.ts @@ -23,4 +23,7 @@ export interface VectorStore { filters?: SearchFilters, limit?: number, ): Promise<[VectorStoreResult[], number]>; + getUserId(): Promise; + setUserId(userId: string): Promise; + initialize(): Promise; } diff --git a/mem0-ts/src/oss/src/vector_stores/memory.ts b/mem0-ts/src/oss/src/vector_stores/memory.ts index 38936a52..711fa320 100644 --- a/mem0-ts/src/oss/src/vector_stores/memory.ts +++ b/mem0-ts/src/oss/src/vector_stores/memory.ts @@ -32,6 +32,13 @@ export class MemoryVectorStore implements VectorStore { payload TEXT NOT NULL ) `); + + await this.run(` + CREATE TABLE IF NOT EXISTS memory_migrations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id TEXT NOT NULL UNIQUE + ) + `); } private async run(sql: string, params: any[] = []): Promise { @@ -201,4 +208,33 @@ export class MemoryVectorStore implements VectorStore { return [results.slice(0, limit), results.length]; } + + async getUserId(): Promise { + const row = await this.getOne( + `SELECT user_id FROM memory_migrations LIMIT 1`, + ); + if (row) { + return row.user_id; + } + + // Generate a random user_id if none exists + const randomUserId = + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); + await this.run(`INSERT INTO memory_migrations (user_id) VALUES (?)`, [ + randomUserId, + ]); + return randomUserId; + } + + async setUserId(userId: string): Promise { + await this.run(`DELETE FROM memory_migrations`); + await this.run(`INSERT INTO memory_migrations (user_id) VALUES (?)`, [ + userId, + ]); + } + + async initialize(): Promise { + await this.init(); + } } diff --git a/mem0-ts/src/oss/src/vector_stores/pgvector.ts b/mem0-ts/src/oss/src/vector_stores/pgvector.ts index fd000c44..4dd98893 100644 --- a/mem0-ts/src/oss/src/vector_stores/pgvector.ts +++ b/mem0-ts/src/oss/src/vector_stores/pgvector.ts @@ -19,12 +19,14 @@ export class PGVector implements VectorStore { private useDiskann: boolean; private useHnsw: boolean; private readonly dbName: string; + private config: PGVectorConfig; constructor(config: PGVectorConfig) { this.collectionName = config.collectionName; this.useDiskann = config.diskann || false; this.useHnsw = config.hnsw || false; this.dbName = config.dbname || "vector_store"; + this.config = config; this.client = new Client({ database: "postgres", // Initially connect to default postgres database @@ -33,14 +35,9 @@ export class PGVector implements VectorStore { host: config.host, port: config.port, }); - - this.initialize(config, config.embeddingModelDims); } - private async initialize( - config: PGVectorConfig, - embeddingModelDims: number, - ): Promise { + async initialize(): Promise { try { await this.client.connect(); @@ -56,20 +53,28 @@ export class PGVector implements VectorStore { // Connect to the target database this.client = new Client({ database: this.dbName, - user: config.user, - password: config.password, - host: config.host, - port: config.port, + user: this.config.user, + password: this.config.password, + host: this.config.host, + port: this.config.port, }); await this.client.connect(); // Create vector extension await this.client.query("CREATE EXTENSION IF NOT EXISTS vector"); + // Create memory_migrations table + await this.client.query(` + CREATE TABLE IF NOT EXISTS memory_migrations ( + id SERIAL PRIMARY KEY, + user_id TEXT NOT NULL UNIQUE + ) + `); + // Check if the collection exists const collections = await this.listCols(); if (!collections.includes(this.collectionName)) { - await this.createCol(embeddingModelDims); + await this.createCol(this.config.embeddingModelDims); } } catch (error) { console.error("Error during initialization:", error); @@ -296,4 +301,32 @@ export class PGVector implements VectorStore { async close(): Promise { await this.client.end(); } + + async getUserId(): Promise { + const result = await this.client.query( + "SELECT user_id FROM memory_migrations LIMIT 1", + ); + + if (result.rows.length > 0) { + return result.rows[0].user_id; + } + + // Generate a random user_id if none exists + const randomUserId = + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); + await this.client.query( + "INSERT INTO memory_migrations (user_id) VALUES ($1)", + [randomUserId], + ); + return randomUserId; + } + + async setUserId(userId: string): Promise { + await this.client.query("DELETE FROM memory_migrations"); + await this.client.query( + "INSERT INTO memory_migrations (user_id) VALUES ($1)", + [userId], + ); + } } diff --git a/mem0-ts/src/oss/src/vector_stores/qdrant.ts b/mem0-ts/src/oss/src/vector_stores/qdrant.ts index 5fcb9ff8..226cf9b2 100644 --- a/mem0-ts/src/oss/src/vector_stores/qdrant.ts +++ b/mem0-ts/src/oss/src/vector_stores/qdrant.ts @@ -13,33 +13,7 @@ interface QdrantConfig extends VectorStoreConfig { onDisk?: boolean; collectionName: string; embeddingModelDims: number; -} - -type DistanceType = "Cosine" | "Euclid" | "Dot"; - -interface QdrantPoint { - id: string | number; - vector: { name: string; vector: number[] }; - payload?: Record | { [key: string]: unknown } | null; - shard_key?: string; - version?: number; -} - -interface QdrantScoredPoint extends QdrantPoint { - score: number; - version: number; -} - -interface QdrantNamedVector { - name: string; - vector: number[]; -} - -interface QdrantSearchRequest { - vector: { name: string; vector: number[] }; - limit?: number; - offset?: number; - filter?: QdrantFilter; + dimension?: number; } interface QdrantFilter { @@ -54,27 +28,10 @@ interface QdrantCondition { range?: { gte?: number; gt?: number; lte?: number; lt?: number }; } -interface QdrantVectorParams { - size: number; - distance: "Cosine" | "Euclid" | "Dot" | "Manhattan"; - on_disk?: boolean; -} - -interface QdrantCollectionInfo { - config?: { - params?: { - vectors?: { - size: number; - distance: "Cosine" | "Euclid" | "Dot" | "Manhattan"; - on_disk?: boolean; - }; - }; - }; -} - export class Qdrant implements VectorStore { private client: QdrantClient; private readonly collectionName: string; + private dimension: number; constructor(config: QdrantConfig) { if (config.client) { @@ -107,61 +64,8 @@ export class Qdrant implements VectorStore { } this.collectionName = config.collectionName; - this.createCol(config.embeddingModelDims, config.onDisk || false); - } - - private async createCol( - vectorSize: number, - onDisk: boolean, - distance: DistanceType = "Cosine", - ): Promise { - try { - // Check if collection exists - const collections = await this.client.getCollections(); - const exists = collections.collections.some( - (col: { name: string }) => col.name === this.collectionName, - ); - - if (!exists) { - const vectorParams: QdrantVectorParams = { - size: vectorSize, - distance: distance as "Cosine" | "Euclid" | "Dot" | "Manhattan", - on_disk: onDisk, - }; - - try { - await this.client.createCollection(this.collectionName, { - vectors: vectorParams, - }); - } catch (error: any) { - // Handle case where collection was created between our check and create - if (error?.status === 409) { - // Collection already exists - verify it has the correct configuration - const collectionInfo = (await this.client.getCollection( - this.collectionName, - )) as QdrantCollectionInfo; - const vectorConfig = collectionInfo.config?.params?.vectors; - - if (!vectorConfig || vectorConfig.size !== vectorSize) { - throw new Error( - `Collection ${this.collectionName} exists but has wrong configuration. ` + - `Expected vector size: ${vectorSize}, got: ${vectorConfig?.size}`, - ); - } - // Collection exists with correct configuration - we can proceed - return; - } - throw error; - } - } - } catch (error) { - if (error instanceof Error) { - console.error("Error creating/verifying collection:", error.message); - } else { - console.error("Error creating/verifying collection:", error); - } - throw error; - } + this.dimension = config.dimension || 1536; // Default OpenAI dimension + this.initialize().catch(console.error); } private createFilter(filters?: SearchFilters): QdrantFilter | undefined { @@ -293,4 +197,158 @@ export class Qdrant implements VectorStore { return [results, response.points.length]; } + + private generateUUID(): string { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( + /[xy]/g, + function (c) { + const r = (Math.random() * 16) | 0; + const v = c === "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }, + ); + } + + async getUserId(): Promise { + try { + // First check if the collection exists + const collections = await this.client.getCollections(); + const userCollectionExists = collections.collections.some( + (col: { name: string }) => col.name === "memory_migrations", + ); + + if (!userCollectionExists) { + // Create the collection if it doesn't exist + await this.client.createCollection("memory_migrations", { + vectors: { + size: 1, + distance: "Cosine", + on_disk: false, + }, + }); + } + + // Now try to get the user ID + const result = await this.client.scroll("memory_migrations", { + limit: 1, + with_payload: true, + }); + + if (result.points.length > 0) { + return result.points[0].payload?.user_id as string; + } + + // Generate a random user_id if none exists + const randomUserId = + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); + + await this.client.upsert("memory_migrations", { + points: [ + { + id: this.generateUUID(), + vector: [0], + payload: { user_id: randomUserId }, + }, + ], + }); + + return randomUserId; + } catch (error) { + console.error("Error getting user ID:", error); + throw error; + } + } + + async setUserId(userId: string): Promise { + try { + // Get existing point ID + const result = await this.client.scroll("memory_migrations", { + limit: 1, + with_payload: true, + }); + + const pointId = + result.points.length > 0 ? result.points[0].id : this.generateUUID(); + + await this.client.upsert("memory_migrations", { + points: [ + { + id: pointId, + vector: [0], + payload: { user_id: userId }, + }, + ], + }); + } catch (error) { + console.error("Error setting user ID:", error); + throw error; + } + } + + async initialize(): Promise { + try { + // Create collection if it doesn't exist + const collections = await this.client.getCollections(); + const exists = collections.collections.some( + (c) => c.name === this.collectionName, + ); + + if (!exists) { + try { + await this.client.createCollection(this.collectionName, { + vectors: { + size: this.dimension, + distance: "Cosine", + }, + }); + } catch (error: any) { + // Handle case where collection was created between our check and create + if (error?.status === 409) { + // Collection already exists - verify it has the correct configuration + const collectionInfo = await this.client.getCollection( + this.collectionName, + ); + const vectorConfig = collectionInfo.config?.params?.vectors; + + if (!vectorConfig || vectorConfig.size !== this.dimension) { + throw new Error( + `Collection ${this.collectionName} exists but has wrong configuration. ` + + `Expected vector size: ${this.dimension}, got: ${vectorConfig?.size}`, + ); + } + // Collection exists with correct configuration - we can proceed + } else { + throw error; + } + } + } + + // Create memory_migrations collection if it doesn't exist + const userExists = collections.collections.some( + (c) => c.name === "memory_migrations", + ); + + if (!userExists) { + try { + await this.client.createCollection("memory_migrations", { + vectors: { + size: 1, // Minimal size since we only store user_id + distance: "Cosine", + }, + }); + } catch (error: any) { + // Handle case where collection was created between our check and create + if (error?.status === 409) { + // Collection already exists - we can proceed + } else { + throw error; + } + } + } + } catch (error) { + console.error("Error initializing Qdrant:", error); + throw error; + } + } } diff --git a/mem0-ts/src/oss/src/vector_stores/redis.ts b/mem0-ts/src/oss/src/vector_stores/redis.ts index 796199dc..7678b82f 100644 --- a/mem0-ts/src/oss/src/vector_stores/redis.ts +++ b/mem0-ts/src/oss/src/vector_stores/redis.ts @@ -187,57 +187,6 @@ export class RedisDB implements VectorStore { }); } - private async initialize(): Promise { - try { - await this.client.connect(); - console.log("Connected to Redis"); - - // Check if Redis Stack modules are loaded - const modulesResponse = - (await this.client.moduleList()) as unknown as any[]; - - // Parse module list to find search module - const hasSearch = modulesResponse.some((module: any[]) => { - const moduleMap = new Map(); - for (let i = 0; i < module.length; i += 2) { - moduleMap.set(module[i], module[i + 1]); - } - return moduleMap.get("name")?.toLowerCase() === "search"; - }); - - if (!hasSearch) { - throw new Error( - "RediSearch module is not loaded. Please ensure Redis Stack is properly installed and running.", - ); - } - - // Create index with retries - let retries = 0; - const maxRetries = 3; - while (retries < maxRetries) { - try { - await this.createIndex(); - console.log("Redis index created successfully"); - break; - } catch (error) { - console.error( - `Error creating index (attempt ${retries + 1}/${maxRetries}):`, - error, - ); - retries++; - if (retries === maxRetries) { - throw error; - } - // Wait before retrying - await new Promise((resolve) => setTimeout(resolve, 1000)); - } - } - } catch (error) { - console.error("Error during Redis initialization:", error); - throw error; - } - } - private async createIndex(): Promise { try { // Drop existing index if it exists @@ -290,6 +239,61 @@ export class RedisDB implements VectorStore { } } + async initialize(): Promise { + try { + await this.client.connect(); + console.log("Connected to Redis"); + + // Check if Redis Stack modules are loaded + const modulesResponse = + (await this.client.moduleList()) as unknown as any[]; + + // Parse module list to find search module + const hasSearch = modulesResponse.some((module: any[]) => { + const moduleMap = new Map(); + for (let i = 0; i < module.length; i += 2) { + moduleMap.set(module[i], module[i + 1]); + } + return moduleMap.get("name")?.toLowerCase() === "search"; + }); + + if (!hasSearch) { + throw new Error( + "RediSearch module is not loaded. Please ensure Redis Stack is properly installed and running.", + ); + } + + // Create index with retries + let retries = 0; + const maxRetries = 3; + while (retries < maxRetries) { + try { + await this.createIndex(); + console.log("Redis index created successfully"); + break; + } catch (error) { + console.error( + `Error creating index (attempt ${retries + 1}/${maxRetries}):`, + error, + ); + retries++; + if (retries === maxRetries) { + throw error; + } + // Wait before retrying + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } + } catch (error) { + if (error instanceof Error) { + console.error("Error initializing Redis:", error.message); + } else { + console.error("Error initializing Redis:", error); + } + throw error; + } + } + async insert( vectors: number[][], ids: string[], @@ -629,4 +633,35 @@ export class RedisDB implements VectorStore { async close(): Promise { await this.client.quit(); } + + async getUserId(): Promise { + try { + // Check if the user ID exists in Redis + const userId = await this.client.get("memory_migrations:1"); + if (userId) { + return userId; + } + + // Generate a random user_id if none exists + const randomUserId = + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); + + // Store the user ID + await this.client.set("memory_migrations:1", randomUserId); + return randomUserId; + } catch (error) { + console.error("Error getting user ID:", error); + throw error; + } + } + + async setUserId(userId: string): Promise { + try { + await this.client.set("memory_migrations:1", userId); + } catch (error) { + console.error("Error setting user ID:", error); + throw error; + } + } } diff --git a/mem0-ts/src/oss/src/vector_stores/supabase.ts b/mem0-ts/src/oss/src/vector_stores/supabase.ts index 0ce82b25..bf2db537 100644 --- a/mem0-ts/src/oss/src/vector_stores/supabase.ts +++ b/mem0-ts/src/oss/src/vector_stores/supabase.ts @@ -45,6 +45,12 @@ create table if not exists memories ( updated_at timestamp with time zone default timezone('utc', now()) ); +-- Create the memory migrations table +create table if not exists memory_migrations ( + user_id text primary key, + created_at timestamp with time zone default timezone('utc', now()) +); + -- Create the vector similarity search function create or replace function match_vectors( query_embedding vector(1536), @@ -93,7 +99,7 @@ export class SupabaseDB implements VectorStore { }); } - private async initialize(): Promise { + async initialize(): Promise { try { // Verify table exists and vector operations work by attempting a test insert const testVector = Array(1536).fill(0); @@ -133,6 +139,12 @@ create table if not exists memories ( updated_at timestamp with time zone default timezone('utc', now()) ); +-- Create the memory migrations table +create table if not exists memory_migrations ( + user_id text primary key, + created_at timestamp with time zone default timezone('utc', now()) +); + -- Create the vector similarity search function create or replace function match_vectors( query_embedding vector(1536), @@ -336,4 +348,74 @@ See the SQL migration instructions in the code comments.`, throw error; } } + + async getUserId(): Promise { + try { + // First check if the table exists + const { data: tableExists } = await this.client + .from("memory_migrations") + .select("user_id") + .limit(1); + + if (!tableExists || tableExists.length === 0) { + // Generate a random user_id + const randomUserId = + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); + + // Insert the new user_id + const { error: insertError } = await this.client + .from("memory_migrations") + .insert({ user_id: randomUserId }); + + if (insertError) throw insertError; + return randomUserId; + } + + // Get the first user_id + const { data, error } = await this.client + .from("memory_migrations") + .select("user_id") + .limit(1); + + if (error) throw error; + if (!data || data.length === 0) { + // Generate a random user_id if no data found + const randomUserId = + Math.random().toString(36).substring(2, 15) + + Math.random().toString(36).substring(2, 15); + + const { error: insertError } = await this.client + .from("memory_migrations") + .insert({ user_id: randomUserId }); + + if (insertError) throw insertError; + return randomUserId; + } + + return data[0].user_id; + } catch (error) { + console.error("Error getting user ID:", error); + return "anonymous-supabase"; + } + } + + async setUserId(userId: string): Promise { + try { + const { error: deleteError } = await this.client + .from("memory_migrations") + .delete() + .neq("user_id", ""); + + if (deleteError) throw deleteError; + + const { error: insertError } = await this.client + .from("memory_migrations") + .insert({ user_id: userId }); + + if (insertError) throw insertError; + } catch (error) { + console.error("Error setting user ID:", error); + } + } }