Add Supabase History DB to run Mem0 OSS on Serverless (#2429)

This commit is contained in:
Saket Aryan
2025-03-25 04:44:29 +05:30
committed by GitHub
parent 953a5a4a2d
commit 1ae2747ff8
14 changed files with 427 additions and 10 deletions

View File

@@ -301,6 +301,56 @@ await memory.deleteAll({ userId: "alice" });
await memory.reset(); // Reset all memories
```
### History Store
Mem0 TypeScript SDK support history stores to run on a serverless environment:
We recommend using `Supabase` as a history store for serverless environments or disable history store to run on a serverless environment.
<CodeGroup>
```typescript Supabase
import { Memory } from 'mem0ai/oss';
const memory = new Memory({
historyStore: {
provider: 'supabase',
config: {
supabaseUrl: process.env.SUPABASE_URL || '',
supabaseKey: process.env.SUPABASE_KEY || '',
tableName: 'memory_history',
},
},
});
```
```typescript Disable History
import { Memory } from 'mem0ai/oss';
const memory = new Memory({
disableHistory: true,
});
```
</CodeGroup>
Mem0 uses SQLite as a default history store.
#### Create Memory History Table in Supabase
You may need to create a memory history table in Supabase to store the history of memories. Use the following SQL command in `SQL Editor` on the Supabase project dashboard to create a memory history table:
```sql
create table memory_history (
id text primary key,
memory_id text not null,
previous_value text,
new_value text,
action text not null,
created_at timestamp with time zone default timezone('utc', now()),
updated_at timestamp with time zone,
is_deleted integer default 0
);
```
## Configuration Parameters
Mem0 offers extensive configuration options to customize its behavior according to your needs. These configurations span across different components like vector stores, language models, embedders, and graph stores.
@@ -352,6 +402,14 @@ Mem0 offers extensive configuration options to customize its behavior according
| `customPrompt` | Custom prompt for memory processing | None |
</Accordion>
<Accordion title="History Table Configuration">
| Parameter | Description | Default |
|------------------|--------------------------------------|----------------------------|
| `provider` | History store provider | "sqlite" |
| `config` | History store configuration | None (Defaults to SQLite) |
| `disableHistory` | Disable history store | false |
</Accordion>
<Accordion title="Complete Configuration Example">
```typescript
const config = {
@@ -377,7 +435,15 @@ const config = {
model: 'gpt-4-turbo-preview',
},
},
historyDbPath: 'memory.db',
historyStore: {
provider: 'supabase',
config: {
supabaseUrl: process.env.SUPABASE_URL || '',
supabaseKey: process.env.SUPABASE_KEY || '',
tableName: 'memories',
},
},
disableHistory: false, // This is false by default
customPrompt: "I'm a virtual assistant. I'm here to help you with your queries.",
}
```

View File

@@ -1,6 +1,6 @@
{
"name": "mem0ai",
"version": "2.1.9",
"version": "2.1.10",
"description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js",
"module": "./dist/index.mjs",

View File

@@ -1,6 +1,7 @@
import { MemoryConfig } from "../types";
export const DEFAULT_MEMORY_CONFIG: MemoryConfig = {
disableHistory: false,
version: "v1.1",
embedder: {
provider: "openai",
@@ -38,5 +39,10 @@ export const DEFAULT_MEMORY_CONFIG: MemoryConfig = {
},
},
},
historyDbPath: "memory.db",
historyStore: {
provider: "sqlite",
config: {
historyDbPath: "memory.db",
},
},
};

View File

@@ -51,6 +51,12 @@ export class ConfigManager {
...DEFAULT_MEMORY_CONFIG.graphStore,
...userConfig.graphStore,
},
historyStore: {
...DEFAULT_MEMORY_CONFIG.historyStore,
...userConfig.historyStore,
},
disableHistory:
userConfig.disableHistory || DEFAULT_MEMORY_CONFIG.disableHistory,
enableGraph: userConfig.enableGraph || DEFAULT_MEMORY_CONFIG.enableGraph,
};

View File

@@ -12,6 +12,7 @@ import {
EmbedderFactory,
LLMFactory,
VectorStoreFactory,
HistoryManagerFactory,
} from "../utils/factory";
import {
getFactRetrievalMessages,
@@ -19,7 +20,7 @@ import {
parseMessages,
removeCodeBlocks,
} from "../prompts";
import { SQLiteManager } from "../storage";
import { DummyHistoryManager } from "../storage/DummyHistoryManager";
import { Embedder } from "../embeddings/base";
import { LLM } from "../llms/base";
import { VectorStore } from "../vector_stores/base";
@@ -32,14 +33,14 @@ import {
GetAllMemoryOptions,
} from "./memory.types";
import { parse_vision_messages } from "../utils/memory";
import { HistoryManager } from "../storage/base";
export class Memory {
private config: MemoryConfig;
private customPrompt: string | undefined;
private embedder: Embedder;
private vectorStore: VectorStore;
private llm: LLM;
private db: SQLiteManager;
private db: HistoryManager;
private collectionName: string;
private apiVersion: string;
private graphMemory?: MemoryGraph;
@@ -62,7 +63,25 @@ export class Memory {
this.config.llm.provider,
this.config.llm.config,
);
this.db = new SQLiteManager(this.config.historyDbPath || ":memory:");
if (this.config.disableHistory) {
this.db = new DummyHistoryManager();
} else {
const defaultConfig = {
provider: "sqlite",
config: {
historyDbPath: this.config.historyDbPath || ":memory:",
},
};
this.db =
this.config.historyStore && !this.config.disableHistory
? HistoryManagerFactory.create(
this.config.historyStore.provider,
this.config.historyStore,
)
: HistoryManagerFactory.create("sqlite", defaultConfig);
}
this.collectionName = this.config.vectorStore.config.collectionName;
this.apiVersion = this.config.version || "v1.0";
this.enableGraph = this.config.enableGraph || false;

View File

@@ -0,0 +1,27 @@
export class DummyHistoryManager {
constructor() {}
async addHistory(
memoryId: string,
previousValue: string | null,
newValue: string | null,
action: string,
createdAt?: string,
updatedAt?: string,
isDeleted: number = 0,
): Promise<void> {
return;
}
async getHistory(memoryId: string): Promise<any[]> {
return [];
}
async reset(): Promise<void> {
return;
}
close(): void {
return;
}
}

View File

@@ -0,0 +1,58 @@
import { v4 as uuidv4 } from "uuid";
import { HistoryManager } from "./base";
interface HistoryEntry {
id: string;
memory_id: string;
previous_value: string | null;
new_value: string | null;
action: string;
created_at: string;
updated_at: string | null;
is_deleted: number;
}
export class MemoryHistoryManager implements HistoryManager {
private memoryStore: Map<string, HistoryEntry> = new Map();
async addHistory(
memoryId: string,
previousValue: string | null,
newValue: string | null,
action: string,
createdAt?: string,
updatedAt?: string,
isDeleted: number = 0,
): Promise<void> {
const historyEntry: HistoryEntry = {
id: uuidv4(),
memory_id: memoryId,
previous_value: previousValue,
new_value: newValue,
action: action,
created_at: createdAt || new Date().toISOString(),
updated_at: updatedAt || null,
is_deleted: isDeleted,
};
this.memoryStore.set(historyEntry.id, historyEntry);
}
async getHistory(memoryId: string): Promise<any[]> {
return Array.from(this.memoryStore.values())
.filter((entry) => entry.memory_id === memoryId)
.sort(
(a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
)
.slice(0, 100);
}
async reset(): Promise<void> {
this.memoryStore.clear();
}
close(): void {
// No need to close anything for in-memory storage
return;
}
}

View File

@@ -1,7 +1,7 @@
import sqlite3 from "sqlite3";
import { promisify } from "util";
import { HistoryManager } from "./base";
export class SQLiteManager {
export class SQLiteManager implements HistoryManager {
private db: sqlite3.Database;
constructor(dbPath: string) {

View File

@@ -0,0 +1,121 @@
import { createClient, SupabaseClient } from "@supabase/supabase-js";
import { v4 as uuidv4 } from "uuid";
import { HistoryManager } from "./base";
interface HistoryEntry {
id: string;
memory_id: string;
previous_value: string | null;
new_value: string | null;
action: string;
created_at: string;
updated_at: string | null;
is_deleted: number;
}
interface SupabaseHistoryConfig {
supabaseUrl: string;
supabaseKey: string;
tableName?: string;
}
export class SupabaseHistoryManager implements HistoryManager {
private supabase: SupabaseClient;
private readonly tableName: string;
constructor(config: SupabaseHistoryConfig) {
this.tableName = config.tableName || "memory_history";
this.supabase = createClient(config.supabaseUrl, config.supabaseKey);
this.initializeSupabase().catch(console.error);
}
private async initializeSupabase(): Promise<void> {
// Check if table exists
const { error } = await this.supabase
.from(this.tableName)
.select("id")
.limit(1);
if (error) {
console.error(
"Error: Table does not exist. Please run this SQL in your Supabase SQL Editor:",
);
console.error(`
create table ${this.tableName} (
id text primary key,
memory_id text not null,
previous_value text,
new_value text,
action text not null,
created_at timestamp with time zone default timezone('utc', now()),
updated_at timestamp with time zone,
is_deleted integer default 0
);
`);
throw error;
}
}
async addHistory(
memoryId: string,
previousValue: string | null,
newValue: string | null,
action: string,
createdAt?: string,
updatedAt?: string,
isDeleted: number = 0,
): Promise<void> {
const historyEntry: HistoryEntry = {
id: uuidv4(),
memory_id: memoryId,
previous_value: previousValue,
new_value: newValue,
action: action,
created_at: createdAt || new Date().toISOString(),
updated_at: updatedAt || null,
is_deleted: isDeleted,
};
const { error } = await this.supabase
.from(this.tableName)
.insert(historyEntry);
if (error) {
console.error("Error adding history to Supabase:", error);
throw error;
}
}
async getHistory(memoryId: string): Promise<any[]> {
const { data, error } = await this.supabase
.from(this.tableName)
.select("*")
.eq("memory_id", memoryId)
.order("created_at", { ascending: false })
.limit(100);
if (error) {
console.error("Error getting history from Supabase:", error);
throw error;
}
return data || [];
}
async reset(): Promise<void> {
const { error } = await this.supabase
.from(this.tableName)
.delete()
.neq("id", "");
if (error) {
console.error("Error resetting Supabase history:", error);
throw error;
}
}
close(): void {
// No need to close anything as connections are handled by the client
return;
}
}

View File

@@ -0,0 +1,14 @@
export interface HistoryManager {
addHistory(
memoryId: string,
previousValue: string | null,
newValue: string | null,
action: string,
createdAt?: string,
updatedAt?: string,
isDeleted?: number,
): Promise<void>;
getHistory(memoryId: string): Promise<any[]>;
reset(): Promise<void>;
close(): void;
}

View File

@@ -1 +1,5 @@
export * from "./SQLiteManager";
export * from "./DummyHistoryManager";
export * from "./SupabaseHistoryManager";
export * from "./MemoryHistoryManager";
export * from "./base";

View File

@@ -24,6 +24,16 @@ export interface VectorStoreConfig {
[key: string]: any;
}
export interface HistoryStoreConfig {
provider: string;
config: {
historyDbPath?: string;
supabaseUrl?: string;
supabaseKey?: string;
tableName?: string;
};
}
export interface LLMConfig {
provider?: string;
config?: Record<string, any>;
@@ -58,6 +68,8 @@ export interface MemoryConfig {
provider: string;
config: LLMConfig;
};
historyStore?: HistoryStoreConfig;
disableHistory?: boolean;
historyDbPath?: string;
customPrompt?: string;
graphStore?: GraphStoreConfig;
@@ -137,4 +149,11 @@ export const MemoryConfigSchema = z.object({
customPrompt: z.string().optional(),
})
.optional(),
historyStore: z
.object({
provider: z.string(),
config: z.record(z.string(), z.any()),
})
.optional(),
disableHistory: z.boolean().optional(),
});

View File

@@ -5,7 +5,12 @@ import { OpenAIStructuredLLM } from "../llms/openai_structured";
import { AnthropicLLM } from "../llms/anthropic";
import { GroqLLM } from "../llms/groq";
import { MemoryVectorStore } from "../vector_stores/memory";
import { EmbeddingConfig, LLMConfig, VectorStoreConfig } from "../types";
import {
EmbeddingConfig,
HistoryStoreConfig,
LLMConfig,
VectorStoreConfig,
} from "../types";
import { Embedder } from "../embeddings/base";
import { LLM } from "../llms/base";
import { VectorStore } from "../vector_stores/base";
@@ -13,6 +18,10 @@ import { Qdrant } from "../vector_stores/qdrant";
import { RedisDB } from "../vector_stores/redis";
import { OllamaLLM } from "../llms/ollama";
import { SupabaseDB } from "../vector_stores/supabase";
import { SQLiteManager } from "../storage/SQLiteManager";
import { MemoryHistoryManager } from "../storage/MemoryHistoryManager";
import { SupabaseHistoryManager } from "../storage/SupabaseHistoryManager";
import { HistoryManager } from "../storage/base";
export class EmbedderFactory {
static create(provider: string, config: EmbeddingConfig): Embedder {
@@ -62,3 +71,22 @@ export class VectorStoreFactory {
}
}
}
export class HistoryManagerFactory {
static create(provider: string, config: HistoryStoreConfig): HistoryManager {
switch (provider.toLowerCase()) {
case "sqlite":
return new SQLiteManager(config.config.historyDbPath || ":memory:");
case "supabase":
return new SupabaseHistoryManager({
supabaseUrl: config.config.supabaseUrl || "",
supabaseKey: config.config.supabaseKey || "",
tableName: config.config.tableName || "memory_history",
});
case "memory":
return new MemoryHistoryManager();
default:
throw new Error(`Unsupported history store provider: ${provider}`);
}
}
}

View File

@@ -97,6 +97,11 @@ export class SupabaseDB implements VectorStore {
try {
// Verify table exists and vector operations work by attempting a test insert
const testVector = Array(1536).fill(0);
try {
await this.client.from(this.tableName).delete().eq("id", "test_vector");
} catch (error) {
console.warn("No test vector to delete, safe to ignore.");
}
const { error: testError } = await this.client
.from(this.tableName)
.insert({
@@ -113,6 +118,50 @@ export class SupabaseDB implements VectorStore {
1. The vector extension is enabled
2. The table "${this.tableName}" exists with correct schema
3. The match_vectors function is created
RUN THE FOLLOWING SQL IN YOUR SUPABASE SQL EDITOR:
-- Enable the vector extension
create extension if not exists vector;
-- Create the memories table
create table if not exists memories (
id text primary key,
embedding vector(1536),
metadata jsonb,
created_at timestamp with time zone default timezone('utc', now()),
updated_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),
match_count int,
filter jsonb default '{}'::jsonb
)
returns table (
id text,
similarity float,
metadata jsonb
)
language plpgsql
as $$
begin
return query
select
id,
similarity,
metadata
from memories
where case
when filter::text = '{}'::text then true
else metadata @> filter
end
order by embedding <=> query_embedding
limit match_count;
end;
$$;
See the SQL migration instructions in the code comments.`,
);
}