Add Supabase History DB to run Mem0 OSS on Serverless (#2429)
This commit is contained in:
@@ -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.",
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
27
mem0-ts/src/oss/src/storage/DummyHistoryManager.ts
Normal file
27
mem0-ts/src/oss/src/storage/DummyHistoryManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
58
mem0-ts/src/oss/src/storage/MemoryHistoryManager.ts
Normal file
58
mem0-ts/src/oss/src/storage/MemoryHistoryManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
121
mem0-ts/src/oss/src/storage/SupabaseHistoryManager.ts
Normal file
121
mem0-ts/src/oss/src/storage/SupabaseHistoryManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
14
mem0-ts/src/oss/src/storage/base.ts
Normal file
14
mem0-ts/src/oss/src/storage/base.ts
Normal 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;
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
export * from "./SQLiteManager";
|
||||
export * from "./DummyHistoryManager";
|
||||
export * from "./SupabaseHistoryManager";
|
||||
export * from "./MemoryHistoryManager";
|
||||
export * from "./base";
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user