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 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 ## 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. 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 | | `customPrompt` | Custom prompt for memory processing | None |
</Accordion> </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"> <Accordion title="Complete Configuration Example">
```typescript ```typescript
const config = { const config = {
@@ -377,7 +435,15 @@ const config = {
model: 'gpt-4-turbo-preview', 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.", customPrompt: "I'm a virtual assistant. I'm here to help you with your queries.",
} }
``` ```

View File

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

View File

@@ -1,6 +1,7 @@
import { MemoryConfig } from "../types"; import { MemoryConfig } from "../types";
export const DEFAULT_MEMORY_CONFIG: MemoryConfig = { export const DEFAULT_MEMORY_CONFIG: MemoryConfig = {
disableHistory: false,
version: "v1.1", version: "v1.1",
embedder: { embedder: {
provider: "openai", 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, ...DEFAULT_MEMORY_CONFIG.graphStore,
...userConfig.graphStore, ...userConfig.graphStore,
}, },
historyStore: {
...DEFAULT_MEMORY_CONFIG.historyStore,
...userConfig.historyStore,
},
disableHistory:
userConfig.disableHistory || DEFAULT_MEMORY_CONFIG.disableHistory,
enableGraph: userConfig.enableGraph || DEFAULT_MEMORY_CONFIG.enableGraph, enableGraph: userConfig.enableGraph || DEFAULT_MEMORY_CONFIG.enableGraph,
}; };

View File

@@ -12,6 +12,7 @@ import {
EmbedderFactory, EmbedderFactory,
LLMFactory, LLMFactory,
VectorStoreFactory, VectorStoreFactory,
HistoryManagerFactory,
} from "../utils/factory"; } from "../utils/factory";
import { import {
getFactRetrievalMessages, getFactRetrievalMessages,
@@ -19,7 +20,7 @@ import {
parseMessages, parseMessages,
removeCodeBlocks, removeCodeBlocks,
} from "../prompts"; } from "../prompts";
import { SQLiteManager } from "../storage"; import { DummyHistoryManager } from "../storage/DummyHistoryManager";
import { Embedder } from "../embeddings/base"; import { Embedder } from "../embeddings/base";
import { LLM } from "../llms/base"; import { LLM } from "../llms/base";
import { VectorStore } from "../vector_stores/base"; import { VectorStore } from "../vector_stores/base";
@@ -32,14 +33,14 @@ import {
GetAllMemoryOptions, GetAllMemoryOptions,
} from "./memory.types"; } from "./memory.types";
import { parse_vision_messages } from "../utils/memory"; import { parse_vision_messages } from "../utils/memory";
import { HistoryManager } from "../storage/base";
export class Memory { export class Memory {
private config: MemoryConfig; private config: MemoryConfig;
private customPrompt: string | undefined; private customPrompt: string | undefined;
private embedder: Embedder; private embedder: Embedder;
private vectorStore: VectorStore; private vectorStore: VectorStore;
private llm: LLM; private llm: LLM;
private db: SQLiteManager; private db: HistoryManager;
private collectionName: string; private collectionName: string;
private apiVersion: string; private apiVersion: string;
private graphMemory?: MemoryGraph; private graphMemory?: MemoryGraph;
@@ -62,7 +63,25 @@ export class Memory {
this.config.llm.provider, this.config.llm.provider,
this.config.llm.config, 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.collectionName = this.config.vectorStore.config.collectionName;
this.apiVersion = this.config.version || "v1.0"; this.apiVersion = this.config.version || "v1.0";
this.enableGraph = this.config.enableGraph || false; 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 sqlite3 from "sqlite3";
import { promisify } from "util"; import { HistoryManager } from "./base";
export class SQLiteManager { export class SQLiteManager implements HistoryManager {
private db: sqlite3.Database; private db: sqlite3.Database;
constructor(dbPath: string) { 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 "./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; [key: string]: any;
} }
export interface HistoryStoreConfig {
provider: string;
config: {
historyDbPath?: string;
supabaseUrl?: string;
supabaseKey?: string;
tableName?: string;
};
}
export interface LLMConfig { export interface LLMConfig {
provider?: string; provider?: string;
config?: Record<string, any>; config?: Record<string, any>;
@@ -58,6 +68,8 @@ export interface MemoryConfig {
provider: string; provider: string;
config: LLMConfig; config: LLMConfig;
}; };
historyStore?: HistoryStoreConfig;
disableHistory?: boolean;
historyDbPath?: string; historyDbPath?: string;
customPrompt?: string; customPrompt?: string;
graphStore?: GraphStoreConfig; graphStore?: GraphStoreConfig;
@@ -137,4 +149,11 @@ export const MemoryConfigSchema = z.object({
customPrompt: z.string().optional(), customPrompt: z.string().optional(),
}) })
.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 { AnthropicLLM } from "../llms/anthropic";
import { GroqLLM } from "../llms/groq"; import { GroqLLM } from "../llms/groq";
import { MemoryVectorStore } from "../vector_stores/memory"; 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 { Embedder } from "../embeddings/base";
import { LLM } from "../llms/base"; import { LLM } from "../llms/base";
import { VectorStore } from "../vector_stores/base"; import { VectorStore } from "../vector_stores/base";
@@ -13,6 +18,10 @@ import { Qdrant } from "../vector_stores/qdrant";
import { RedisDB } from "../vector_stores/redis"; import { RedisDB } from "../vector_stores/redis";
import { OllamaLLM } from "../llms/ollama"; import { OllamaLLM } from "../llms/ollama";
import { SupabaseDB } from "../vector_stores/supabase"; 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 { export class EmbedderFactory {
static create(provider: string, config: EmbeddingConfig): Embedder { 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 { 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);
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 const { error: testError } = await this.client
.from(this.tableName) .from(this.tableName)
.insert({ .insert({
@@ -113,6 +118,50 @@ export class SupabaseDB implements VectorStore {
1. The vector extension is enabled 1. The vector extension is enabled
2. The table "${this.tableName}" exists with correct schema 2. The table "${this.tableName}" exists with correct schema
3. The match_vectors function is created 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.`, See the SQL migration instructions in the code comments.`,
); );
} }