Add user_id in TS OSS SDK (#2514)

This commit is contained in:
Saket Aryan
2025-04-09 22:54:56 +05:30
committed by GitHub
parent f4d8647264
commit 309c8c18a6
14 changed files with 819 additions and 166 deletions

View File

@@ -90,6 +90,11 @@ mode: "wide"
<Tab title="TypeScript">
<Update label="2025-04-09" description="v2.1.15">
**Improvements:**
- **Client:** Added support for Mem0 to work with Chrome Extensions
</Update>
<Update label="2025-04-01" description="v2.1.14">
**New Features:**
- **Mastra Example:** Added Mastra example

View File

@@ -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",

187
mem0-ts/pnpm-lock.yaml generated
View File

@@ -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: {}

View File

@@ -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/";

View File

@@ -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 || "";
}

View File

@@ -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<MemoryConfig> = {}) {
// 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<string, any>): Memory {
@@ -106,6 +156,12 @@ export class Memory {
messages: string | Message[],
config: AddMemoryOptions,
): Promise<SearchResult> {
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<SearchResult> {
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<void> {
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<SearchResult> {
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 = {};

View File

@@ -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<string, any> = {},
) {
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 };

View File

@@ -0,0 +1,34 @@
export interface TelemetryClient {
captureEvent(
distinctId: string,
eventName: string,
properties?: Record<string, any>,
): Promise<void>;
shutdown(): Promise<void>;
}
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;
}

View File

@@ -23,4 +23,7 @@ export interface VectorStore {
filters?: SearchFilters,
limit?: number,
): Promise<[VectorStoreResult[], number]>;
getUserId(): Promise<string>;
setUserId(userId: string): Promise<void>;
initialize(): Promise<void>;
}

View File

@@ -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<void> {
@@ -201,4 +208,33 @@ export class MemoryVectorStore implements VectorStore {
return [results.slice(0, limit), results.length];
}
async getUserId(): Promise<string> {
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<void> {
await this.run(`DELETE FROM memory_migrations`);
await this.run(`INSERT INTO memory_migrations (user_id) VALUES (?)`, [
userId,
]);
}
async initialize(): Promise<void> {
await this.init();
}
}

View File

@@ -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<void> {
async initialize(): Promise<void> {
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<void> {
await this.client.end();
}
async getUserId(): Promise<string> {
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<void> {
await this.client.query("DELETE FROM memory_migrations");
await this.client.query(
"INSERT INTO memory_migrations (user_id) VALUES ($1)",
[userId],
);
}
}

View File

@@ -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<string, unknown> | { [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<void> {
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<string> {
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<void> {
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<void> {
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;
}
}
}

View File

@@ -187,57 +187,6 @@ export class RedisDB implements VectorStore {
});
}
private async initialize(): Promise<void> {
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<void> {
try {
// Drop existing index if it exists
@@ -290,6 +239,61 @@ export class RedisDB implements VectorStore {
}
}
async initialize(): Promise<void> {
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<void> {
await this.client.quit();
}
async getUserId(): Promise<string> {
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<void> {
try {
await this.client.set("memory_migrations:1", userId);
} catch (error) {
console.error("Error setting user ID:", error);
throw error;
}
}
}

View File

@@ -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<void> {
async initialize(): Promise<void> {
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<string> {
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<void> {
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);
}
}
}