Compare commits

...

15 Commits

Author SHA1 Message Date
kartik-mem0 da2ffddf7e chore(docs): adding skills.sh installation command in the readme. 2026-03-16 13:53:32 +05:30
Saket Aryan 06ee1b588c fix(openclaw): point plugin extension entry to built output for npm compatibility (#4340) 2026-03-15 04:41:11 +05:30
Saket Aryan dc6122ec3d chore(openclaw): add tsup build pipeline with ESM output and type declarations (#4335) 2026-03-15 00:41:07 +05:30
Saket Aryan df79a43925 chroe(ts-sdk): fix lints (#4334) 2026-03-14 23:39:23 +05:30
Utkarsh a6242710df chore(ts-sdk): bump mem0ai version to 2.4.0 (#4332)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 23:34:56 +05:30
d 🔹 4e1e4c0c5a fix(ts): extract content from code blocks instead of deleting it (#4317)
Co-authored-by: d 🔹 <258577966+voidborne-d@users.noreply.github.com>
2026-03-14 23:34:43 +05:30
Kartik fa5c85f9f6 chore: bump protobuf dependency to 5.29.6 and extend upper bound to 7.0.0 (#4326) 2026-03-14 10:58:58 -07:00
Muhammed Ajmal M 7c29eb2645 fix: incorrect database param (#3913) 2026-03-14 01:54:20 -07:00
Anisha Mahuli 6f079c313f fix OpenAI embedder baseurl (#4275) 2026-03-13 01:38:59 -07:00
Giulio Leone e95090e116 fix: add missing 'json' keyword to graph memory prompts (fixes #4248) (#4249) 2026-03-13 01:38:24 -07:00
Utkarsh 861cbb7289 fix(openclaw): use absolute URL for architecture image in README (#4311) 2026-03-12 11:06:51 -07:00
Kartik 54aa760720 feat(skills): add Mem0 Platform Claude Code skill (#4309) 2026-03-12 10:00:39 -07:00
Utkarsh 59c3b050bd fix(oss): auto-detect embedding dimension to fix Qdrant mismatch with non-OpenAI embedders (#4297)
Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 21:45:09 +05:30
Utkarsh 5b3acf416b fix(ts-sdk): resolve SQLite db paths correctly in OSS mode (#4307) 2026-03-12 09:13:44 -07:00
Kartik 63f587c922 fix(docs): use filters param for search in LiveKit integration (#4300) 2026-03-12 18:05:02 +05:30
52 changed files with 11882 additions and 123 deletions
+13
View File
@@ -729,6 +729,19 @@ mode: "wide"
<Tab title="TypeScript">
<Update label="2026-03-14" description="v2.4.0">
**Bug Fixes:**
- **OSS Storage:** Fixed `SQLITE_CANTOPEN` errors when running as a LaunchAgent, systemd service, or in containers where `process.cwd()` is read-only (e.g. `/`). Default `vector_store.db` location changed from `process.cwd()/vector_store.db` to `~/.mem0/vector_store.db`.
- **OSS Storage:** Fixed `historyDbPath` config being silently ignored — config merging always overwrote it with defaults. Top-level `historyDbPath` is now correctly propagated into `historyStore.config` with proper precedence.
- **OSS Storage:** Added `ensureSQLiteDirectory()` — parent directories for SQLite database files are now auto-created before opening, preventing `SQLITE_CANTOPEN` when using nested paths.
**Improvements:**
- **Migration:** Added deprecation warning when an existing `vector_store.db` is found at the old `process.cwd()` location, guiding users to move it or set `vectorStore.config.dbPath` explicitly.
- **Config:** Limited default SQLite config spreading to only SQLite history providers, preventing config leaking into Supabase or other providers.
</Update>
<Update label="2026-03-09" description="v2.3.0">
**Breaking Changes:**
+1 -1
View File
@@ -114,7 +114,7 @@ class MemoryEnabledAgent(Agent):
logger.info("About to await mem0_client.search for RAG context")
search_results = await mem0_client.search(
new_message.text_content,
user_id=RAG_USER_ID,
filters={"user_id": RAG_USER_ID},
)
logger.info(f"mem0_client.search returned: {search_results}")
if search_results and search_results.get('results', []):
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0ai",
"version": "2.3.0",
"version": "2.4.0",
"description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
+38 -9
View File
@@ -26,6 +26,7 @@ export class ConfigManager {
? userConf.apiKey
: defaultConf.apiKey,
model: finalModel,
baseURL: userConf?.baseURL,
url: userConf?.url,
embeddingDims: userConf?.embeddingDims,
modelProperties:
@@ -43,13 +44,23 @@ export class ConfigManager {
const defaultConf = DEFAULT_MEMORY_CONFIG.vectorStore.config;
const userConf = userConfig.vectorStore?.config;
// Resolve the vector store dimension. If the user explicitly
// provided one, use it. Otherwise leave it undefined so that
// Memory._autoInitialize() can auto-detect it by running a
// probe embedding at startup — this makes *any* embedder work
// out of the box without the user needing to know or set the
// dimension manually.
const explicitDimension =
userConf?.dimension ||
userConfig.embedder?.config?.embeddingDims ||
undefined;
// Prioritize user-provided client instance
if (userConf?.client && typeof userConf.client === "object") {
return {
client: userConf.client,
// Include other fields from userConf if necessary, or omit defaults
collectionName: userConf.collectionName, // Can be undefined
dimension: userConf.dimension || defaultConf.dimension, // Merge dimension
collectionName: userConf.collectionName,
dimension: explicitDimension,
...userConf, // Include any other passthrough fields from user
};
} else {
@@ -57,7 +68,7 @@ export class ConfigManager {
return {
collectionName:
userConf?.collectionName || defaultConf.collectionName,
dimension: userConf?.dimension || defaultConf.dimension,
dimension: explicitDimension,
// Ensure client is not carried over from defaults if not provided by user
client: undefined,
// Include other passthrough fields from userConf even if no client
@@ -95,16 +106,34 @@ export class ConfigManager {
})(),
},
historyDbPath:
userConfig.historyDbPath || DEFAULT_MEMORY_CONFIG.historyDbPath,
userConfig.historyDbPath ||
userConfig.historyStore?.config?.historyDbPath ||
DEFAULT_MEMORY_CONFIG.historyStore?.config?.historyDbPath,
customPrompt: userConfig.customPrompt,
graphStore: {
...DEFAULT_MEMORY_CONFIG.graphStore,
...userConfig.graphStore,
},
historyStore: {
...DEFAULT_MEMORY_CONFIG.historyStore,
...userConfig.historyStore,
},
historyStore: (() => {
const defaultHistoryStore = DEFAULT_MEMORY_CONFIG.historyStore!;
const historyProvider =
userConfig.historyStore?.provider || defaultHistoryStore.provider;
const isSqlite = historyProvider.toLowerCase() === "sqlite";
// Precedence: explicit historyStore.config > top-level historyDbPath > default
return {
...defaultHistoryStore,
...userConfig.historyStore,
provider: historyProvider,
config: {
...(isSqlite ? defaultHistoryStore.config : {}),
...(isSqlite && userConfig.historyDbPath
? { historyDbPath: userConfig.historyDbPath }
: {}),
...userConfig.historyStore?.config,
},
};
})(),
disableHistory:
userConfig.disableHistory || DEFAULT_MEMORY_CONFIG.disableHistory,
enableGraph: userConfig.enableGraph || DEFAULT_MEMORY_CONFIG.enableGraph,
+1 -1
View File
@@ -28,7 +28,7 @@ export class GoogleEmbedder implements Embedder {
const response = await this.google.models.embedContent({
model: this.model,
contents: texts,
config: { outputDimensionality: 768 },
config: { outputDimensionality: this.embeddingDims },
});
return response.embeddings!.map((item) => item.values!);
}
+4 -1
View File
@@ -8,7 +8,10 @@ export class OpenAIEmbedder implements Embedder {
private embeddingDims?: number;
constructor(config: EmbeddingConfig) {
this.openai = new OpenAI({ apiKey: config.apiKey });
this.openai = new OpenAI({
apiKey: config.apiKey,
baseURL: config.baseURL || config.url,
});
this.model = config.model || "text-embedding-3-small";
this.embeddingDims = config.embeddingDims || 1536;
}
+2
View File
@@ -88,6 +88,8 @@ Memory Format:
source -- relationship -- destination
Provide a list of deletion instructions, each specifying the relationship to be deleted.
Respond in JSON format.
`;
export function getDeleteMessages(
+1 -1
View File
@@ -212,7 +212,7 @@ export class MemoryGraph {
[
{
role: "system",
content: `You are a smart assistant who understands entities and their types in a given text. If user message contains self reference such as 'I', 'me', 'my' etc. then use ${filters["userId"]} as the source entity. Extract all the entities from the text. ***DO NOT*** answer the question itself if the given text is a question.`,
content: `You are a smart assistant who understands entities and their types in a given text. If user message contains self reference such as 'I', 'me', 'my' etc. then use ${filters["userId"]} as the source entity. Extract all the entities from the text. ***DO NOT*** answer the question itself if the given text is a question. Respond in JSON format.`,
},
{ role: "user", content: data },
],
+91 -31
View File
@@ -41,7 +41,7 @@ export class Memory {
private config: MemoryConfig;
private customPrompt: string | undefined;
private embedder: Embedder;
private vectorStore: VectorStore;
private vectorStore!: VectorStore;
private llm: LLM;
private db: HistoryManager;
private collectionName: string | undefined;
@@ -49,6 +49,8 @@ export class Memory {
private graphMemory?: MemoryGraph;
private enableGraph: boolean;
telemetryId: string;
private _initPromise: Promise<void>;
private _initError?: Error;
constructor(config: Partial<MemoryConfig> = {}) {
// Merge and validate config
@@ -59,10 +61,9 @@ export class Memory {
this.config.embedder.provider,
this.config.embedder.config,
);
this.vectorStore = VectorStoreFactory.create(
this.config.vectorStore.provider,
this.config.vectorStore.config,
);
// Vector store creation is deferred to _autoInitialize() so that
// the embedding dimension can be auto-detected first when not
// explicitly configured.
this.llm = LLMFactory.create(
this.config.llm.provider,
this.config.llm.config,
@@ -70,20 +71,10 @@ export class 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.db = HistoryManagerFactory.create(
this.config.historyStore!.provider,
this.config.historyStore!,
);
}
this.collectionName = this.config.vectorStore.config.collectionName;
@@ -96,8 +87,67 @@ export class Memory {
this.graphMemory = new MemoryGraph(this.config);
}
// Initialize telemetry if vector store is initialized
this._initializeTelemetry();
// Auto-detect embedding dimension (if needed), create vector store,
// and initialize it. All public methods await this before proceeding.
this._initPromise = this._autoInitialize().catch((error) => {
this._initError =
error instanceof Error ? error : new Error(String(error));
console.error(this._initError);
});
}
/**
* If no explicit dimension was provided, runs a probe embedding to
* detect it. Then creates and initializes the vector store.
*/
private async _autoInitialize(): Promise<void> {
if (!this.config.vectorStore.config.dimension) {
try {
const probe = await this.embedder.embed("dimension probe");
this.config.vectorStore.config.dimension = probe.length;
} catch (error: any) {
throw new Error(
`Failed to auto-detect embedding dimension from provider '${this.config.embedder.provider}': ${error.message}. ` +
`Please set 'dimension' in vectorStore.config or 'embeddingDims' in embedder.config explicitly.`,
);
}
}
this.vectorStore = VectorStoreFactory.create(
this.config.vectorStore.provider,
this.config.vectorStore.config,
);
// The vector store constructor may fire initialize() asynchronously
// (e.g. Qdrant). Explicitly await it here to guarantee the backing
// store (collections, tables, etc.) is ready before any public method
// attempts to read or write.
await this.vectorStore.initialize();
await this._initializeTelemetry();
}
/**
* Ensures that auto-initialization (dimension detection + vector store
* creation) has completed before any public method proceeds.
* If a previous init attempt failed, retries automatically.
*/
private async _ensureInitialized(): Promise<void> {
await this._initPromise;
if (this._initError) {
// Clear failed state and retry — the embedder or vector store
// may have been transiently unavailable at startup.
this._initError = undefined;
this._initPromise = this._autoInitialize().catch((error) => {
this._initError =
error instanceof Error ? error : new Error(String(error));
console.error(this._initError);
});
await this._initPromise;
if (this._initError) {
throw this._initError;
}
}
}
private async _initializeTelemetry() {
@@ -157,6 +207,7 @@ export class Memory {
messages: string | Message[],
config: AddMemoryOptions,
): Promise<SearchResult> {
await this._ensureInitialized();
await this._captureEvent("add", {
message_count: Array.isArray(messages) ? messages.length : 1,
has_metadata: !!config.metadata,
@@ -382,6 +433,7 @@ export class Memory {
}
async get(memoryId: string): Promise<MemoryItem | null> {
await this._ensureInitialized();
const memory = await this.vectorStore.get(memoryId);
if (!memory) return null;
@@ -423,6 +475,7 @@ export class Memory {
query: string,
config: SearchMemoryOptions,
): Promise<SearchResult> {
await this._ensureInitialized();
await this._captureEvent("search", {
query_length: query.length,
limit: config.limit,
@@ -489,6 +542,7 @@ export class Memory {
}
async update(memoryId: string, data: string): Promise<{ message: string }> {
await this._ensureInitialized();
await this._captureEvent("update", { memory_id: memoryId });
const embedding = await this.embedder.embed(data);
await this.updateMemory(memoryId, data, { [data]: embedding });
@@ -496,6 +550,7 @@ export class Memory {
}
async delete(memoryId: string): Promise<{ message: string }> {
await this._ensureInitialized();
await this._captureEvent("delete", { memory_id: memoryId });
await this.deleteMemory(memoryId);
return { message: "Memory deleted successfully!" };
@@ -504,6 +559,7 @@ export class Memory {
async deleteAll(
config: DeleteAllMemoryOptions,
): Promise<{ message: string }> {
await this._ensureInitialized();
await this._captureEvent("delete_all", {
has_user_id: !!config.userId,
has_agent_id: !!config.agentId,
@@ -531,10 +587,12 @@ export class Memory {
}
async history(memoryId: string): Promise<any[]> {
await this._ensureInitialized();
return this.db.getHistory(memoryId);
}
async reset(): Promise<void> {
await this._ensureInitialized();
await this._captureEvent("reset");
await this.db.reset();
@@ -559,28 +617,30 @@ export class Memory {
await this.graphMemory.deleteAll({ userId: "default" }); // Assuming this is okay, or needs similar check?
}
// Re-initialize factories/clients based on the original config
// Re-initialize factories/clients based on the original config.
// Dimension is already set in this.config from the initial probe,
// so _autoInitialize will skip the probe and just re-create the store.
this.embedder = EmbedderFactory.create(
this.config.embedder.provider,
this.config.embedder.config,
);
// Re-create vector store instance - crucial for Langchain to reset wrapper state if needed
this.vectorStore = VectorStoreFactory.create(
this.config.vectorStore.provider,
this.config.vectorStore.config, // This will pass the original client instance back
);
this.llm = LLMFactory.create(
this.config.llm.provider,
this.config.llm.config,
);
// Re-init DB if needed (though db.reset() likely handles its state)
// Re-init Graph if needed
// Re-initialize telemetry
this._initializeTelemetry();
// Re-create vector store via _autoInitialize (which handles dimension + creation)
this._initError = undefined;
this._initPromise = this._autoInitialize().catch((error) => {
this._initError =
error instanceof Error ? error : new Error(String(error));
console.error(this._initError);
});
await this._initPromise;
}
async getAll(config: GetAllMemoryOptions): Promise<SearchResult> {
await this._ensureInitialized();
await this._captureEvent("get_all", {
limit: config.limit,
has_user_id: !!config.userId,
+5 -1
View File
@@ -278,5 +278,9 @@ export function parseMessages(messages: string[]): string {
}
export function removeCodeBlocks(text: string): string {
return text.replace(/```[^`]*```/g, "");
// Extract content inside code fences instead of deleting it.
// The old regex /```[^`]*```/g replaced the entire block (including
// its content) with an empty string, so when an LLM returned JSON
// wrapped in ```json ... ``` the actual payload was discarded.
return text.replace(/```(?:\w+)?\n?([\s\S]*?)```/g, "$1").trim();
}
@@ -1,5 +1,6 @@
import Database from "better-sqlite3";
import { HistoryManager } from "./base";
import { ensureSQLiteDirectory } from "../utils/sqlite";
export class SQLiteManager implements HistoryManager {
private db: Database.Database;
@@ -7,6 +8,7 @@ export class SQLiteManager implements HistoryManager {
private stmtSelect!: Database.Statement;
constructor(dbPath: string) {
ensureSQLiteDirectory(dbPath);
this.db = new Database(dbPath);
this.init();
}
@@ -0,0 +1,396 @@
/**
* Backward-compatibility tests for SQLite path handling changes.
*
* These tests verify that every documented and common usage pattern
* from before the fix continues to work identically after the change.
*/
import fs from "fs";
import os from "os";
import path from "path";
import { ConfigManager } from "../config/manager";
import { SQLiteManager } from "../storage/SQLiteManager";
import { MemoryVectorStore } from "../vector_stores/memory";
import {
ensureSQLiteDirectory,
getDefaultVectorStoreDbPath,
} from "../utils/sqlite";
function normalize(vector: number[]): number[] {
const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
return vector.map((value) => value / norm);
}
// ---------------------------------------------------------------------------
// 1. Config merging – existing patterns must keep working
// ---------------------------------------------------------------------------
describe("backward compat: ConfigManager.mergeConfig", () => {
it("empty config returns all expected defaults", () => {
const cfg = ConfigManager.mergeConfig({});
expect(cfg.version).toBe("v1.1");
expect(cfg.embedder.provider).toBe("openai");
expect(cfg.vectorStore.provider).toBe("memory");
expect(cfg.vectorStore.config.collectionName).toBe("memories");
expect(cfg.vectorStore.config.dimension).toBe(1536);
expect(cfg.llm.provider).toBe("openai");
expect(cfg.historyStore).toBeDefined();
expect(cfg.historyStore!.provider).toBe("sqlite");
expect(cfg.historyStore!.config.historyDbPath).toBe("memory.db");
expect(cfg.disableHistory).toBe(false);
expect(cfg.enableGraph).toBe(false);
});
it("workaround: explicit historyStore still works (existing user pattern)", () => {
// This is the documented workaround from all three issues
const cfg = ConfigManager.mergeConfig({
historyStore: {
provider: "sqlite",
config: { historyDbPath: "/tmp/workaround.db" },
},
});
expect(cfg.historyStore!.provider).toBe("sqlite");
expect(cfg.historyStore!.config.historyDbPath).toBe("/tmp/workaround.db");
});
it("disableHistory: true still works", () => {
const cfg = ConfigManager.mergeConfig({ disableHistory: true });
expect(cfg.disableHistory).toBe(true);
});
it("supabase historyStore config is preserved", () => {
const cfg = ConfigManager.mergeConfig({
historyStore: {
provider: "supabase",
config: {
supabaseUrl: "https://abc.supabase.co",
supabaseKey: "secret-key",
tableName: "custom_history",
},
},
});
expect(cfg.historyStore!.provider).toBe("supabase");
expect(cfg.historyStore!.config.supabaseUrl).toBe(
"https://abc.supabase.co",
);
expect(cfg.historyStore!.config.supabaseKey).toBe("secret-key");
expect(cfg.historyStore!.config.tableName).toBe("custom_history");
});
it("custom embedder, llm, vectorStore configs pass through unchanged", () => {
const cfg = ConfigManager.mergeConfig({
embedder: {
provider: "ollama",
config: { model: "nomic-embed-text", url: "http://localhost:11434" },
},
llm: {
provider: "ollama",
config: { model: "llama3.1:8b" },
},
vectorStore: {
provider: "qdrant",
config: {
collectionName: "test",
dimension: 768,
},
},
});
expect(cfg.embedder.provider).toBe("ollama");
expect(cfg.embedder.config.model).toBe("nomic-embed-text");
expect(cfg.llm.provider).toBe("ollama");
expect(cfg.llm.config.model).toBe("llama3.1:8b");
expect(cfg.vectorStore.provider).toBe("qdrant");
expect(cfg.vectorStore.config.collectionName).toBe("test");
expect(cfg.vectorStore.config.dimension).toBe(768);
});
it("graphStore config passes through unchanged", () => {
const cfg = ConfigManager.mergeConfig({
enableGraph: true,
graphStore: {
provider: "neo4j",
config: {
url: "neo4j://custom:7687",
username: "admin",
password: "pass",
},
},
});
expect(cfg.enableGraph).toBe(true);
expect(cfg.graphStore!.config.url).toBe("neo4j://custom:7687");
});
it("customPrompt passes through unchanged", () => {
const cfg = ConfigManager.mergeConfig({
customPrompt: "You are a helpful assistant",
});
expect(cfg.customPrompt).toBe("You are a helpful assistant");
});
it("version override passes through unchanged", () => {
const cfg = ConfigManager.mergeConfig({ version: "v1.0" });
expect(cfg.version).toBe("v1.0");
});
});
// ---------------------------------------------------------------------------
// 2. SQLiteManager – existing behavior preserved
// ---------------------------------------------------------------------------
describe("backward compat: SQLiteManager", () => {
it("relative path still works (resolves from CWD)", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-"));
const originalCwd = process.cwd();
try {
process.chdir(tempDir);
const manager = new SQLiteManager("memory.db");
await manager.addHistory("m1", null, "value", "ADD");
const history = await manager.getHistory("m1");
expect(history).toHaveLength(1);
expect(fs.existsSync(path.join(tempDir, "memory.db"))).toBe(true);
manager.close();
} finally {
process.chdir(originalCwd);
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("absolute path still works", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-"));
const dbPath = path.join(tempDir, "history.db");
try {
const manager = new SQLiteManager(dbPath);
await manager.addHistory("m1", null, "value", "ADD");
expect(fs.existsSync(dbPath)).toBe(true);
manager.close();
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it(":memory: still works", async () => {
const manager = new SQLiteManager(":memory:");
await manager.addHistory("m1", null, "value", "ADD");
const history = await manager.getHistory("m1");
expect(history).toHaveLength(1);
manager.close();
});
it("reset clears history and allows re-use", async () => {
const manager = new SQLiteManager(":memory:");
await manager.addHistory("m1", null, "val", "ADD");
await manager.reset();
const history = await manager.getHistory("m1");
expect(history).toHaveLength(0);
await manager.addHistory("m2", null, "new-val", "ADD");
const history2 = await manager.getHistory("m2");
expect(history2).toHaveLength(1);
manager.close();
});
});
// ---------------------------------------------------------------------------
// 3. MemoryVectorStore – existing API preserved
// ---------------------------------------------------------------------------
describe("backward compat: MemoryVectorStore", () => {
const originalCwd = process.cwd();
afterEach(() => {
process.chdir(originalCwd);
jest.restoreAllMocks();
});
it("explicit dbPath still works (the existing config.dbPath feature)", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-"));
const dbPath = path.join(tempDir, "my_vectors.db");
try {
const store = new MemoryVectorStore({ dimension: 3, dbPath });
await store.insert([normalize([1, 0, 0])], ["id1"], [{ text: "hello" }]);
expect(fs.existsSync(dbPath)).toBe(true);
const result = await store.get("id1");
expect(result).not.toBeNull();
expect(result!.payload.text).toBe("hello");
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("insert, search, get, update, delete, list all work", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-"));
const dbPath = path.join(tempDir, "test.db");
try {
const store = new MemoryVectorStore({ dimension: 3, dbPath });
const v1 = normalize([1, 0, 0]);
const v2 = normalize([0, 1, 0]);
// insert
await store.insert([v1, v2], ["a", "b"], [{ t: "a" }, { t: "b" }]);
// get
const a = await store.get("a");
expect(a!.payload.t).toBe("a");
// search
const results = await store.search(v1, 2);
expect(results).toHaveLength(2);
expect(results[0].id).toBe("a"); // closest to v1
// update
await store.update("a", v2, { t: "updated" });
const updated = await store.get("a");
expect(updated!.payload.t).toBe("updated");
// list
const [listed, count] = await store.list();
expect(count).toBe(2);
expect(listed).toHaveLength(2);
// delete
await store.delete("a");
const deleted = await store.get("a");
expect(deleted).toBeNull();
// deleteCol
await store.deleteCol();
const [afterDrop] = await store.list();
expect(afterDrop).toHaveLength(0);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("dimension mismatch on insert still throws", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-"));
const dbPath = path.join(tempDir, "test.db");
try {
const store = new MemoryVectorStore({ dimension: 3, dbPath });
await expect(
store.insert([[1, 0]], ["id1"], [{ t: "x" }]),
).rejects.toThrow("Vector dimension mismatch");
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("dimension mismatch on search still throws", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-"));
const dbPath = path.join(tempDir, "test.db");
try {
const store = new MemoryVectorStore({ dimension: 3, dbPath });
await expect(store.search([1, 0], 1)).rejects.toThrow(
"Query dimension mismatch",
);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("default dimension is 1536 when not specified", () => {
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-"));
try {
jest.spyOn(os, "homedir").mockReturnValue(fakeHome);
const store = new MemoryVectorStore({});
// Verify by trying to insert a 1536-dim vector
const vec = new Array(1536).fill(0);
vec[0] = 1;
expect(store.insert([vec], ["id1"], [{ t: "x" }])).resolves.not.toThrow();
} finally {
fs.rmSync(fakeHome, { recursive: true, force: true });
}
});
it("search with filters still works", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-compat-vs-"));
const dbPath = path.join(tempDir, "test.db");
try {
const store = new MemoryVectorStore({ dimension: 3, dbPath });
await store.insert(
[normalize([1, 0, 0]), normalize([0, 1, 0])],
["a", "b"],
[
{ text: "hello", userId: "user1" },
{ text: "world", userId: "user2" },
],
);
const results = await store.search(normalize([1, 0, 0]), 10, {
userId: "user2",
});
expect(results).toHaveLength(1);
expect(results[0].id).toBe("b");
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
});
// ---------------------------------------------------------------------------
// 4. VectorStoreConfig type – dbPath is optional, existing configs work
// ---------------------------------------------------------------------------
describe("backward compat: VectorStoreConfig type", () => {
it("config without dbPath still works (no required field breakage)", () => {
const cfg = ConfigManager.mergeConfig({
vectorStore: {
provider: "memory",
config: { collectionName: "test", dimension: 512 },
},
});
expect(cfg.vectorStore.config.dbPath).toBeUndefined();
expect(cfg.vectorStore.config.collectionName).toBe("test");
expect(cfg.vectorStore.config.dimension).toBe(512);
});
it("config with client instance passes through unchanged", () => {
const fakeClient = { connect: () => {} };
const cfg = ConfigManager.mergeConfig({
vectorStore: {
provider: "qdrant",
config: { client: fakeClient, dimension: 768 },
},
});
expect(cfg.vectorStore.config.client).toBe(fakeClient);
expect(cfg.vectorStore.config.dimension).toBe(768);
});
});
// ---------------------------------------------------------------------------
// 5. ensureSQLiteDirectory – does not break existing paths
// ---------------------------------------------------------------------------
describe("backward compat: ensureSQLiteDirectory", () => {
it("no-ops for already existing directory", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-existing-"));
try {
// Should not throw even though directory already exists
expect(() =>
ensureSQLiteDirectory(path.join(tempDir, "test.db")),
).not.toThrow();
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("handles path with trailing slash gracefully", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-trailing-"));
try {
// path.dirname of "dir/sub/" is "dir/sub", mkdirSync should handle it
expect(() =>
ensureSQLiteDirectory(path.join(tempDir, "sub", "test.db")),
).not.toThrow();
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
});
@@ -0,0 +1,290 @@
import fs from "fs";
import os from "os";
import path from "path";
import { ConfigManager } from "../config/manager";
import { SQLiteManager } from "../storage/SQLiteManager";
import { MemoryVectorStore } from "../vector_stores/memory";
import {
ensureSQLiteDirectory,
getDefaultVectorStoreDbPath,
} from "../utils/sqlite";
function normalize(vector: number[]): number[] {
const norm = Math.sqrt(vector.reduce((sum, value) => sum + value * value, 0));
return vector.map((value) => value / norm);
}
// ---------------------------------------------------------------------------
// Config merging – historyDbPath
// ---------------------------------------------------------------------------
describe("ConfigManager.mergeConfig – historyDbPath handling", () => {
it("propagates top-level historyDbPath into historyStore.config", () => {
const cfg = ConfigManager.mergeConfig({
historyDbPath: "/tmp/custom/history.db",
});
expect(cfg.historyDbPath).toBe("/tmp/custom/history.db");
expect(cfg.historyStore?.provider).toBe("sqlite");
expect(cfg.historyStore?.config.historyDbPath).toBe(
"/tmp/custom/history.db",
);
});
it("explicit historyStore.config.historyDbPath takes precedence over top-level", () => {
const cfg = ConfigManager.mergeConfig({
historyDbPath: "/tmp/shorthand.db",
historyStore: {
provider: "sqlite",
config: { historyDbPath: "/tmp/explicit.db" },
},
});
expect(cfg.historyStore?.config.historyDbPath).toBe("/tmp/explicit.db");
});
it("preserves default memory.db when nothing is provided", () => {
const cfg = ConfigManager.mergeConfig({});
expect(cfg.historyStore?.provider).toBe("sqlite");
expect(cfg.historyStore?.config.historyDbPath).toBe("memory.db");
});
it("respects only historyStore.config when top-level is absent", () => {
const cfg = ConfigManager.mergeConfig({
historyStore: {
provider: "sqlite",
config: { historyDbPath: "/tmp/nested-only.db" },
},
});
expect(cfg.historyStore?.config.historyDbPath).toBe("/tmp/nested-only.db");
});
it("does not leak historyDbPath into non-sqlite providers", () => {
const cfg = ConfigManager.mergeConfig({
historyDbPath: "/tmp/should-not-apply.db",
historyStore: {
provider: "supabase",
config: {
supabaseUrl: "https://x.supabase.co",
supabaseKey: "key",
},
},
});
expect(cfg.historyStore?.provider).toBe("supabase");
expect(cfg.historyStore?.config.historyDbPath).toBeUndefined();
});
it("disableHistory does not prevent historyStore config from merging", () => {
const cfg = ConfigManager.mergeConfig({
disableHistory: true,
historyDbPath: "/tmp/disabled.db",
});
expect(cfg.disableHistory).toBe(true);
expect(cfg.historyStore?.config.historyDbPath).toBe("/tmp/disabled.db");
});
});
// ---------------------------------------------------------------------------
// SQLiteManager – directory creation & DB operations
// ---------------------------------------------------------------------------
describe("SQLiteManager – directory auto-creation", () => {
it("creates nested parent directories and writes to the DB", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-sqlite-"));
const dbPath = path.join(tempDir, "a", "b", "c", "history.db");
let manager: SQLiteManager | undefined;
try {
manager = new SQLiteManager(dbPath);
await manager.addHistory("mem-1", null, "test value", "ADD");
const history = await manager.getHistory("mem-1");
expect(fs.existsSync(dbPath)).toBe(true);
expect(history).toHaveLength(1);
expect(history[0].new_value).toBe("test value");
} finally {
manager?.close();
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("end-to-end: mergeConfig + SQLiteManager at configured path", async () => {
const tempDir = fs.mkdtempSync(
path.join(os.tmpdir(), "mem0-history-path-"),
);
const historyDbPath = path.join(tempDir, "nested", "history.db");
let manager: SQLiteManager | undefined;
try {
const mergedConfig = ConfigManager.mergeConfig({ historyDbPath });
manager = new SQLiteManager(
mergedConfig.historyStore!.config.historyDbPath!,
);
await manager.addHistory("memory-1", null, "remember me", "ADD");
expect(fs.existsSync(historyDbPath)).toBe(true);
} finally {
manager?.close();
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("works with :memory: without attempting directory creation", () => {
const manager = new SQLiteManager(":memory:");
expect(manager).toBeDefined();
manager.close();
});
});
// ---------------------------------------------------------------------------
// MemoryVectorStore – path handling
// ---------------------------------------------------------------------------
describe("MemoryVectorStore – path handling", () => {
const originalCwd = process.cwd();
afterEach(() => {
process.chdir(originalCwd);
jest.restoreAllMocks();
});
it("uses ~/.mem0/vector_store.db by default", () => {
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-"));
try {
jest.spyOn(os, "homedir").mockReturnValue(fakeHome);
new MemoryVectorStore({ dimension: 4 });
expect(
fs.existsSync(path.join(fakeHome, ".mem0", "vector_store.db")),
).toBe(true);
} finally {
fs.rmSync(fakeHome, { recursive: true, force: true });
}
});
it("respects explicit dbPath config", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-vs-"));
const dbPath = path.join(tempDir, "custom", "vectors.db");
try {
const store = new MemoryVectorStore({ dimension: 4, dbPath });
await store.insert(
[normalize([1, 0, 0, 0])],
["v1"],
[{ text: "hello" }],
);
expect(fs.existsSync(dbPath)).toBe(true);
const results = await store.search(normalize([1, 0, 0, 0]), 1);
expect(results).toHaveLength(1);
expect(results[0].payload.text).toBe("hello");
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("works when CWD is read-only", async () => {
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-"));
const readOnlyCwd = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-ro-"));
try {
fs.chmodSync(readOnlyCwd, 0o555);
jest.spyOn(os, "homedir").mockReturnValue(fakeHome);
process.chdir(readOnlyCwd);
const store = new MemoryVectorStore({ dimension: 4 });
await store.insert(
[normalize([0, 1, 0, 0])],
["v2"],
[{ text: "works" }],
);
expect(
fs.existsSync(path.join(fakeHome, ".mem0", "vector_store.db")),
).toBe(true);
expect(fs.existsSync(path.join(readOnlyCwd, "vector_store.db"))).toBe(
false,
);
} finally {
fs.chmodSync(readOnlyCwd, 0o755);
fs.rmSync(fakeHome, { recursive: true, force: true });
fs.rmSync(readOnlyCwd, { recursive: true, force: true });
}
});
it("emits migration warning when old CWD-based vector_store.db exists", () => {
const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-home-"));
const tempCwd = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-cwd-"));
try {
fs.writeFileSync(path.join(tempCwd, "vector_store.db"), "");
jest.spyOn(os, "homedir").mockReturnValue(fakeHome);
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
process.chdir(tempCwd);
new MemoryVectorStore({ dimension: 4 });
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining("Default vector_store.db location changed"),
);
} finally {
fs.rmSync(fakeHome, { recursive: true, force: true });
fs.rmSync(tempCwd, { recursive: true, force: true });
}
});
it("does NOT emit migration warning when dbPath is explicitly set", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-vs-"));
const tempCwd = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-cwd-"));
try {
fs.writeFileSync(path.join(tempCwd, "vector_store.db"), "");
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
process.chdir(tempCwd);
new MemoryVectorStore({
dimension: 4,
dbPath: path.join(tempDir, "explicit.db"),
});
expect(warnSpy).not.toHaveBeenCalled();
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
fs.rmSync(tempCwd, { recursive: true, force: true });
}
});
});
// ---------------------------------------------------------------------------
// Utils
// ---------------------------------------------------------------------------
describe("ensureSQLiteDirectory", () => {
it("creates nested directories", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-ensure-"));
const target = path.join(tempDir, "x", "y", "z", "test.db");
try {
ensureSQLiteDirectory(target);
expect(fs.existsSync(path.join(tempDir, "x", "y", "z"))).toBe(true);
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
it("skips :memory:", () => {
expect(() => ensureSQLiteDirectory(":memory:")).not.toThrow();
});
it("skips file: URIs", () => {
expect(() => ensureSQLiteDirectory("file::memory:")).not.toThrow();
});
it("skips empty string", () => {
expect(() => ensureSQLiteDirectory("")).not.toThrow();
});
});
describe("getDefaultVectorStoreDbPath", () => {
it("returns path under homedir/.mem0", () => {
const result = getDefaultVectorStoreDbPath();
expect(result).toBe(path.join(os.homedir(), ".mem0", "vector_store.db"));
});
});
+3
View File
@@ -15,6 +15,7 @@ export interface Message {
export interface EmbeddingConfig {
apiKey?: string;
model?: string | any;
baseURL?: string;
url?: string;
embeddingDims?: number;
modelProperties?: Record<string, any>;
@@ -23,6 +24,7 @@ export interface EmbeddingConfig {
export interface VectorStoreConfig {
collectionName?: string;
dimension?: number;
dbPath?: string;
client?: any;
instance?: any;
[key: string]: any;
@@ -129,6 +131,7 @@ export const MemoryConfigSchema = z.object({
.object({
collectionName: z.string().optional(),
dimension: z.number().optional(),
dbPath: z.string().optional(),
client: z.any().optional(),
})
.passthrough(),
+15
View File
@@ -0,0 +1,15 @@
import fs from "fs";
import os from "os";
import path from "path";
export function getDefaultVectorStoreDbPath(): string {
return path.join(os.homedir(), ".mem0", "vector_store.db");
}
export function ensureSQLiteDirectory(dbPath: string): void {
if (!dbPath || dbPath === ":memory:" || dbPath.startsWith("file:")) {
return;
}
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
}
@@ -81,6 +81,7 @@ export class AzureAISearch implements VectorStore {
private readonly hybridSearch: boolean;
private readonly vectorFilterMode: string;
private readonly apiKey: string | undefined;
private _initPromise?: Promise<void>;
constructor(config: AzureAISearchConfig) {
this.serviceName = config.serviceName;
@@ -117,6 +118,13 @@ export class AzureAISearch implements VectorStore {
* Initialize the Azure AI Search index if it doesn't exist
*/
async initialize(): Promise<void> {
if (!this._initPromise) {
this._initPromise = this._doInitialize();
}
return this._initPromise;
}
private async _doInitialize(): Promise<void> {
try {
const collections = await this.listCols();
if (!collections.includes(this.indexName)) {
+17 -3
View File
@@ -1,7 +1,12 @@
import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
import Database from "better-sqlite3";
import fs from "fs";
import path from "path";
import {
ensureSQLiteDirectory,
getDefaultVectorStoreDbPath,
} from "../utils/sqlite";
interface MemoryVector {
id: string;
@@ -16,10 +21,19 @@ export class MemoryVectorStore implements VectorStore {
constructor(config: VectorStoreConfig) {
this.dimension = config.dimension || 1536; // Default OpenAI dimension
this.dbPath = path.join(process.cwd(), "vector_store.db");
if (config.dbPath) {
this.dbPath = config.dbPath;
this.dbPath = config.dbPath || getDefaultVectorStoreDbPath();
if (!config.dbPath) {
const oldDefault = path.join(process.cwd(), "vector_store.db");
if (fs.existsSync(oldDefault) && oldDefault !== this.dbPath) {
console.warn(
`[mem0] Default vector_store.db location changed from ${oldDefault} to ${this.dbPath}. ` +
`Move your existing file or set vectorStore.config.dbPath explicitly.`,
);
}
}
ensureSQLiteDirectory(this.dbPath);
this.db = new Database(this.dbPath);
this.init();
}
+44 -65
View File
@@ -32,6 +32,7 @@ export class Qdrant implements VectorStore {
private client: QdrantClient;
private readonly collectionName: string;
private dimension: number;
private _initPromise?: Promise<void>;
constructor(config: QdrantConfig) {
if (config.client) {
@@ -211,22 +212,8 @@ export class Qdrant implements VectorStore {
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,
},
});
}
// Ensure collection exists (idempotent — handles race conditions)
await this.ensureCollection("memory_migrations", 1);
// Now try to get the user ID
const result = await this.client.scroll("memory_migrations", {
@@ -286,66 +273,58 @@ export class Qdrant implements VectorStore {
}
}
async initialize(): Promise<void> {
private async ensureCollection(name: string, size: number): 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,
);
await this.client.createCollection(name, {
vectors: {
size,
distance: "Cosine",
},
});
} catch (error: any) {
if (error?.status === 409) {
// Collection already exists — verify configuration for the main collection
if (name === this.collectionName) {
try {
const collectionInfo = await this.client.getCollection(name);
const vectorConfig = collectionInfo.config?.params?.vectors;
if (!vectorConfig || vectorConfig.size !== this.dimension) {
if (vectorConfig && vectorConfig.size !== size) {
throw new Error(
`Collection ${this.collectionName} exists but has wrong configuration. ` +
`Expected vector size: ${this.dimension}, got: ${vectorConfig?.size}`,
`Collection ${name} exists but has wrong vector size. ` +
`Expected: ${size}, got: ${vectorConfig.size}`,
);
}
// Collection exists with correct configuration - we can proceed
} else {
throw error;
} catch (verifyError: any) {
// Re-throw dimension mismatch errors
if (verifyError?.message?.includes("wrong vector size")) {
throw verifyError;
}
// Transient errors (e.g. 500 while collection is being committed)
// are non-fatal — the collection exists per the 409.
console.warn(
`Collection '${name}' exists (409) but dimension verification failed: ${verifyError?.message || verifyError}. Proceeding anyway.`,
);
}
}
// Otherwise collection exists and is fine — proceed
} else {
throw error;
}
}
}
// Create memory_migrations collection if it doesn't exist
const userExists = collections.collections.some(
(c) => c.name === "memory_migrations",
);
async initialize(): Promise<void> {
if (!this._initPromise) {
this._initPromise = this._doInitialize();
}
return this._initPromise;
}
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;
}
}
}
private async _doInitialize(): Promise<void> {
try {
await this.ensureCollection(this.collectionName, this.dimension);
await this.ensureCollection("memory_migrations", 1);
} catch (error) {
console.error("Error initializing Qdrant:", error);
throw error;
@@ -139,6 +139,7 @@ export class RedisDB implements VectorStore {
private readonly indexName: string;
private readonly indexPrefix: string;
private readonly schema: RedisSchema;
private _initPromise?: Promise<void>;
constructor(config: RedisConfig) {
this.indexName = config.collectionName;
@@ -240,6 +241,13 @@ export class RedisDB implements VectorStore {
}
async initialize(): Promise<void> {
if (!this._initPromise) {
this._initPromise = this._doInitialize();
}
return this._initPromise;
}
private async _doInitialize(): Promise<void> {
try {
await this.client.connect();
console.log("Connected to Redis");
@@ -86,6 +86,7 @@ export class SupabaseDB implements VectorStore {
private readonly tableName: string;
private readonly embeddingColumnName: string;
private readonly metadataColumnName: string;
private _initPromise?: Promise<void>;
constructor(config: SupabaseConfig) {
this.client = createClient(config.supabaseUrl, config.supabaseKey);
@@ -100,6 +101,13 @@ export class SupabaseDB implements VectorStore {
}
async initialize(): Promise<void> {
if (!this._initPromise) {
this._initPromise = this._doInitialize();
}
return this._initPromise;
}
private async _doInitialize(): Promise<void> {
try {
// Verify table exists and vector operations work by attempting a test insert
const testVector = Array(1536).fill(0);
@@ -20,6 +20,7 @@ export class VectorizeDB implements VectorStore {
private dimensions: number;
private indexName: string;
private accountId: string;
private _initPromise?: Promise<void>;
constructor(config: VectorizeConfig) {
this.client = new Cloudflare({ apiToken: config.apiKey });
@@ -343,6 +344,13 @@ export class VectorizeDB implements VectorStore {
}
async initialize(): Promise<void> {
if (!this._initPromise) {
this._initPromise = this._doInitialize();
}
return this._initPromise;
}
private async _doInitialize(): Promise<void> {
try {
// Check if the index already exists
let indexFound = false;
@@ -0,0 +1,88 @@
/// <reference types="jest" />
import { ConfigManager } from "../src/config/manager";
describe("ConfigManager", () => {
describe("mergeConfig - dimension handling", () => {
const baseLlm = {
provider: "openai",
config: { apiKey: "test-key" },
};
it("should leave dimension undefined when no explicit dimension or embeddingDims provided", () => {
const config = ConfigManager.mergeConfig({
embedder: { provider: "openai", config: { apiKey: "test-key" } },
vectorStore: { provider: "memory", config: { collectionName: "test" } },
llm: baseLlm,
});
// Dimension should be undefined so Memory._autoInitialize() will
// auto-detect it via a probe embedding at runtime.
expect(config.vectorStore.config.dimension).toBeUndefined();
});
it("should use embeddingDims from embedder config when provided", () => {
const config = ConfigManager.mergeConfig({
embedder: {
provider: "ollama",
config: { model: "nomic-embed-text", embeddingDims: 768 },
},
vectorStore: { provider: "qdrant", config: { collectionName: "test" } },
llm: baseLlm,
});
expect(config.vectorStore.config.dimension).toBe(768);
});
it("should prefer explicit vector store dimension over embedder dims", () => {
const config = ConfigManager.mergeConfig({
embedder: {
provider: "ollama",
config: { model: "nomic-embed-text", embeddingDims: 768 },
},
vectorStore: {
provider: "qdrant",
config: { collectionName: "test", dimension: 1024 },
},
llm: baseLlm,
});
expect(config.vectorStore.config.dimension).toBe(1024);
});
it("should leave dimension undefined when using a custom client without explicit dims", () => {
const mockClient = { someMethod: () => {} };
const config = ConfigManager.mergeConfig({
embedder: {
provider: "ollama",
config: { model: "nomic-embed-text" },
},
vectorStore: {
provider: "qdrant",
config: { collectionName: "test", client: mockClient },
},
llm: baseLlm,
});
// No embeddingDims and no explicit dimension → should be undefined
// for auto-detection at runtime.
expect(config.vectorStore.config.dimension).toBeUndefined();
});
it("should use embeddingDims when using a custom client", () => {
const mockClient = { someMethod: () => {} };
const config = ConfigManager.mergeConfig({
embedder: {
provider: "ollama",
config: { model: "nomic-embed-text", embeddingDims: 768 },
},
vectorStore: {
provider: "qdrant",
config: { collectionName: "test", client: mockClient },
},
llm: baseLlm,
});
expect(config.vectorStore.config.dimension).toBe(768);
});
});
});
@@ -0,0 +1,519 @@
/// <reference types="jest" />
/**
* Tests for embedding dimension auto-detection.
*
* Covers:
* - ConfigManager: dimension resolution logic
* - Memory class: probe-based auto-detection, lazy init gate, backward compat
* - MemoryVectorStore: backward compat with explicit dimensions
* - Explicit error messages on probe failure
*/
import { ConfigManager } from "../src/config/manager";
import { MemoryVectorStore } from "../src/vector_stores/memory";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
jest.setTimeout(15000);
// ───────────────────────────────────────────────────────────────────────────
// 1. ConfigManager – dimension resolution
// ───────────────────────────────────────────────────────────────────────────
describe("ConfigManager – dimension resolution", () => {
const baseLlm = { provider: "openai", config: { apiKey: "k" } };
it("leaves dimension undefined when nothing explicit is set", () => {
const cfg = ConfigManager.mergeConfig({
embedder: { provider: "openai", config: { apiKey: "k" } },
vectorStore: { provider: "memory", config: { collectionName: "t" } },
llm: baseLlm,
});
expect(cfg.vectorStore.config.dimension).toBeUndefined();
});
it("uses embeddingDims from embedder config", () => {
const cfg = ConfigManager.mergeConfig({
embedder: {
provider: "ollama",
config: { model: "nomic-embed-text", embeddingDims: 768 },
},
vectorStore: { provider: "qdrant", config: { collectionName: "t" } },
llm: baseLlm,
});
expect(cfg.vectorStore.config.dimension).toBe(768);
});
it("prefers explicit vectorStore.dimension over embeddingDims", () => {
const cfg = ConfigManager.mergeConfig({
embedder: {
provider: "ollama",
config: { model: "nomic-embed-text", embeddingDims: 768 },
},
vectorStore: {
provider: "qdrant",
config: { collectionName: "t", dimension: 1024 },
},
llm: baseLlm,
});
expect(cfg.vectorStore.config.dimension).toBe(1024);
});
it("leaves dimension undefined for custom client without explicit dims", () => {
const cfg = ConfigManager.mergeConfig({
embedder: { provider: "ollama", config: { model: "nomic-embed-text" } },
vectorStore: {
provider: "qdrant",
config: { collectionName: "t", client: {} },
},
llm: baseLlm,
});
expect(cfg.vectorStore.config.dimension).toBeUndefined();
});
it("uses embeddingDims with a custom client", () => {
const cfg = ConfigManager.mergeConfig({
embedder: {
provider: "ollama",
config: { model: "nomic-embed-text", embeddingDims: 768 },
},
vectorStore: {
provider: "qdrant",
config: { collectionName: "t", client: {} },
},
llm: baseLlm,
});
expect(cfg.vectorStore.config.dimension).toBe(768);
});
it("preserves all other vectorStore config fields", () => {
const cfg = ConfigManager.mergeConfig({
embedder: { provider: "openai", config: { apiKey: "k" } },
vectorStore: {
provider: "qdrant",
config: {
collectionName: "my-coll",
host: "my-host",
port: 6333,
apiKey: "qdrant-key",
},
},
llm: baseLlm,
});
expect(cfg.vectorStore.config.collectionName).toBe("my-coll");
expect(cfg.vectorStore.config.host).toBe("my-host");
expect(cfg.vectorStore.config.port).toBe(6333);
expect(cfg.vectorStore.config.apiKey).toBe("qdrant-key");
});
it("leaves dimension undefined with empty config", () => {
const cfg = ConfigManager.mergeConfig({
embedder: { provider: "openai", config: {} },
vectorStore: { provider: "memory", config: {} },
llm: baseLlm,
});
expect(cfg.vectorStore.config.dimension).toBeUndefined();
});
});
// ───────────────────────────────────────────────────────────────────────────
// 2. MemoryVectorStore – backward compat with explicit dimensions
// ───────────────────────────────────────────────────────────────────────────
describe("MemoryVectorStore – backward compat", () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "mem0-test-"));
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it("defaults to dimension 1536 when not specified", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dbPath: path.join(tmpDir, "vs.db"),
});
const vector = new Array(1536).fill(0.1);
await store.insert([vector], ["id-1"], [{ data: "hello" }]);
const result = await store.get("id-1");
expect(result).not.toBeNull();
});
it("explicit dimension=1536 still works", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dimension: 1536,
dbPath: path.join(tmpDir, "vs.db"),
});
const vector = new Array(1536).fill(0.1);
await store.insert([vector], ["id-1"], [{ data: "hello" }]);
const result = await store.get("id-1");
expect(result).not.toBeNull();
});
it("explicit dimension rejects mismatched vectors", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dimension: 1536,
dbPath: path.join(tmpDir, "vs.db"),
});
const wrongVector = new Array(768).fill(0.1);
await expect(
store.insert([wrongVector], ["id-1"], [{ data: "hello" }]),
).rejects.toThrow("Vector dimension mismatch");
});
it("search validates dimension", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dimension: 4,
dbPath: path.join(tmpDir, "vs.db"),
});
await expect(store.search([1, 2, 3], 1)).rejects.toThrow(
"Query dimension mismatch",
);
});
it("custom dimension=768 works end-to-end", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dimension: 768,
dbPath: path.join(tmpDir, "vs.db"),
});
await store.insert(
[
[1, ...new Array(767).fill(0)],
[0, 1, ...new Array(766).fill(0)],
],
["a", "b"],
[{ data: "alpha" }, { data: "beta" }],
);
const results = await store.search([1, ...new Array(767).fill(0)], 2);
expect(results.length).toBe(2);
expect(results[0].id).toBe("a");
});
it("getUserId and setUserId still work", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dbPath: path.join(tmpDir, "vs.db"),
});
const userId = await store.getUserId();
expect(typeof userId).toBe("string");
expect(userId.length).toBeGreaterThan(0);
await store.setUserId("custom-user");
const newUserId = await store.getUserId();
expect(newUserId).toBe("custom-user");
});
it("initialize() is idempotent", async () => {
const store = new MemoryVectorStore({
collectionName: "test",
dbPath: path.join(tmpDir, "vs.db"),
});
await store.initialize();
await store.initialize();
await store.initialize();
});
});
// ───────────────────────────────────────────────────────────────────────────
// 3. Memory class – auto-init with probe, lazy gate, backward compat
// ───────────────────────────────────────────────────────────────────────────
describe("Memory – auto-initialization", () => {
let mockEmbedderFactory: any;
let mockVectorStoreFactory: any;
let mockLlmFactory: any;
let mockHistoryFactory: any;
let MemoryClass: any;
function createMockEmbedder(dims: number) {
return {
embed: jest.fn().mockResolvedValue(new Array(dims).fill(0)),
embedBatch: jest.fn().mockResolvedValue([new Array(dims).fill(0)]),
};
}
function createMockVectorStore() {
return {
insert: jest.fn().mockResolvedValue(undefined),
search: jest.fn().mockResolvedValue([]),
get: jest.fn().mockResolvedValue(null),
update: jest.fn().mockResolvedValue(undefined),
delete: jest.fn().mockResolvedValue(undefined),
deleteCol: jest.fn().mockResolvedValue(undefined),
list: jest.fn().mockResolvedValue([[], 0]),
getUserId: jest.fn().mockResolvedValue("test-user-id"),
setUserId: jest.fn().mockResolvedValue(undefined),
initialize: jest.fn().mockResolvedValue(undefined),
};
}
beforeEach(() => {
jest.resetModules();
const mockEmbedder = createMockEmbedder(768);
const mockVStore = createMockVectorStore();
mockEmbedderFactory = { create: jest.fn().mockReturnValue(mockEmbedder) };
mockVectorStoreFactory = { create: jest.fn().mockReturnValue(mockVStore) };
mockLlmFactory = {
create: jest.fn().mockReturnValue({
generateResponse: jest.fn().mockResolvedValue('{"facts":[]}'),
}),
};
mockHistoryFactory = {
create: jest.fn().mockReturnValue({
addHistory: jest.fn().mockResolvedValue(undefined),
getHistory: jest.fn().mockResolvedValue([]),
reset: jest.fn().mockResolvedValue(undefined),
}),
};
jest.doMock("../src/utils/factory", () => ({
EmbedderFactory: mockEmbedderFactory,
VectorStoreFactory: mockVectorStoreFactory,
LLMFactory: mockLlmFactory,
HistoryManagerFactory: mockHistoryFactory,
}));
jest.doMock("../src/utils/telemetry", () => ({
captureClientEvent: jest.fn().mockResolvedValue(undefined),
}));
MemoryClass = require("../src/memory").Memory;
});
afterEach(() => {
jest.restoreAllMocks();
jest.resetModules();
});
it("probes embedder to detect dimension when none set", async () => {
const mockEmbedder = createMockEmbedder(768);
const mockVStore = createMockVectorStore();
mockEmbedderFactory.create.mockReturnValue(mockEmbedder);
mockVectorStoreFactory.create.mockReturnValue(mockVStore);
const mem = new MemoryClass({
embedder: { provider: "ollama", config: { model: "nomic-embed-text" } },
vectorStore: { provider: "qdrant", config: { collectionName: "test" } },
llm: { provider: "openai", config: { apiKey: "k" } },
disableHistory: true,
});
await mem.getAll({ userId: "u1" });
// Should have called embed("dimension probe") to detect dimension
expect(mockEmbedder.embed).toHaveBeenCalledWith("dimension probe");
// VectorStoreFactory should have been called with detected dimension
const vsCreateCall = mockVectorStoreFactory.create.mock.calls[0];
expect(vsCreateCall[1].dimension).toBe(768);
});
it("skips probe when explicit dimension provided", async () => {
const mockEmbedder = createMockEmbedder(1536);
const mockVStore = createMockVectorStore();
mockEmbedderFactory.create.mockReturnValue(mockEmbedder);
mockVectorStoreFactory.create.mockReturnValue(mockVStore);
const mem = new MemoryClass({
embedder: { provider: "openai", config: { apiKey: "k" } },
vectorStore: {
provider: "memory",
config: { collectionName: "test", dimension: 1536 },
},
llm: { provider: "openai", config: { apiKey: "k" } },
disableHistory: true,
});
await mem.getAll({ userId: "u1" });
// embed should NOT have been called for probing
expect(mockEmbedder.embed).not.toHaveBeenCalledWith("dimension probe");
// VectorStoreFactory gets the explicit dimension
const vsCreateCall = mockVectorStoreFactory.create.mock.calls[0];
expect(vsCreateCall[1].dimension).toBe(1536);
});
it("skips probe when embeddingDims provided", async () => {
const mockEmbedder = createMockEmbedder(768);
const mockVStore = createMockVectorStore();
mockEmbedderFactory.create.mockReturnValue(mockEmbedder);
mockVectorStoreFactory.create.mockReturnValue(mockVStore);
const mem = new MemoryClass({
embedder: {
provider: "ollama",
config: { model: "nomic-embed-text", embeddingDims: 768 },
},
vectorStore: { provider: "qdrant", config: { collectionName: "test" } },
llm: { provider: "openai", config: { apiKey: "k" } },
disableHistory: true,
});
await mem.getAll({ userId: "u1" });
// ConfigManager resolves dimension from embeddingDims → no probe needed
expect(mockEmbedder.embed).not.toHaveBeenCalledWith("dimension probe");
});
it("all public methods wait for initialization", async () => {
let resolveProbe: () => void;
let probeCallCount = 0;
const mockEmbedder = {
embed: jest.fn().mockImplementation(() => {
probeCallCount++;
if (probeCallCount === 1) {
// First call is the dimension probe — hang until manually resolved
return new Promise<number[]>((resolve) => {
resolveProbe = () => resolve(new Array(768).fill(0));
});
}
// Subsequent calls (from search, etc.) resolve immediately
return Promise.resolve(new Array(768).fill(0));
}),
embedBatch: jest.fn(),
};
const mockVStore = createMockVectorStore();
mockEmbedderFactory.create.mockReturnValue(mockEmbedder);
mockVectorStoreFactory.create.mockReturnValue(mockVStore);
const mem = new MemoryClass({
embedder: { provider: "ollama", config: { model: "test" } },
vectorStore: { provider: "qdrant", config: { collectionName: "t" } },
llm: { provider: "openai", config: { apiKey: "k" } },
disableHistory: true,
});
let getAllDone = false;
let searchDone = false;
let getDone = false;
const getAllP = mem.getAll({ userId: "u" }).then(() => (getAllDone = true));
const searchP = mem
.search("q", { userId: "u" })
.then(() => (searchDone = true));
const getP = mem.get("id").then(() => (getDone = true));
await new Promise((r) => setTimeout(r, 50));
expect(getAllDone).toBe(false);
expect(searchDone).toBe(false);
expect(getDone).toBe(false);
// Resolve the probe — init completes — methods unblock
resolveProbe!();
await Promise.all([getAllP, searchP, getP]);
expect(getAllDone).toBe(true);
expect(searchDone).toBe(true);
expect(getDone).toBe(true);
});
it("reset re-creates vector store with correct dimension", async () => {
const mockEmbedder = createMockEmbedder(768);
const mockVStore = createMockVectorStore();
mockEmbedderFactory.create.mockReturnValue(mockEmbedder);
mockVectorStoreFactory.create.mockReturnValue(mockVStore);
const mem = new MemoryClass({
embedder: { provider: "ollama", config: { model: "nomic-embed-text" } },
vectorStore: { provider: "qdrant", config: { collectionName: "test" } },
llm: { provider: "openai", config: { apiKey: "k" } },
disableHistory: true,
});
await mem.getAll({ userId: "u1" });
expect(mockVectorStoreFactory.create).toHaveBeenCalledTimes(1);
// Reset should re-create vector store
const mockVStore2 = createMockVectorStore();
mockVectorStoreFactory.create.mockReturnValue(mockVStore2);
await mem.reset();
expect(mockVectorStoreFactory.create).toHaveBeenCalledTimes(2);
// Second creation should still have dimension=768 (cached from first probe)
const secondCall = mockVectorStoreFactory.create.mock.calls[1];
expect(secondCall[1].dimension).toBe(768);
});
it("backward compat: full explicit config works without probe", async () => {
const mockEmbedder = createMockEmbedder(1536);
const mockVStore = createMockVectorStore();
mockEmbedderFactory.create.mockReturnValue(mockEmbedder);
mockVectorStoreFactory.create.mockReturnValue(mockVStore);
const mem = new MemoryClass({
version: "v1.1",
embedder: {
provider: "openai",
config: { apiKey: "sk-fake", model: "text-embedding-3-small" },
},
vectorStore: {
provider: "memory",
config: { collectionName: "test-memories", dimension: 1536 },
},
llm: {
provider: "openai",
config: { apiKey: "sk-fake", model: "gpt-4-turbo-preview" },
},
historyDbPath: ":memory:",
disableHistory: true,
});
await mem.getAll({ userId: "u1" });
expect(mockEmbedder.embed).not.toHaveBeenCalledWith("dimension probe");
});
it("throws explicit error when probe fails", async () => {
const mockEmbedder = {
embed: jest.fn().mockRejectedValue(new Error("Connection refused")),
embedBatch: jest.fn(),
};
mockEmbedderFactory.create.mockReturnValue(mockEmbedder);
// Suppress console.error for this test
const consoleSpy = jest
.spyOn(console, "error")
.mockImplementation(() => {});
const mem = new MemoryClass({
embedder: { provider: "ollama", config: { model: "nomic-embed-text" } },
vectorStore: { provider: "qdrant", config: { collectionName: "test" } },
llm: { provider: "openai", config: { apiKey: "k" } },
disableHistory: true,
});
// getAll should reject with the init error
await expect(mem.getAll({ userId: "u1" })).rejects.toThrow(
"auto-detect embedding dimension",
);
// Verify the error was logged and contains helpful information
const errorCall = consoleSpy.mock.calls.find(
(call) =>
call[0] instanceof Error &&
call[0].message.includes("auto-detect embedding dimension"),
);
expect(errorCall).toBeDefined();
const errorMsg = (errorCall![0] as Error).message;
expect(errorMsg).toContain("ollama");
expect(errorMsg).toContain("Connection refused");
expect(errorMsg).toContain("dimension");
expect(errorMsg).toContain("embeddingDims");
consoleSpy.mockRestore();
});
});
@@ -0,0 +1,548 @@
/**
* Regression tests for graph_memory.ts response parsing (issue #4248).
*
* Exercises the three json_object call sites in MemoryGraph with a mocked LLM:
* 1. _retrieveNodesFromData → entity extraction
* 2. _establishNodesRelationsFromData → relation extraction
* 3. _getDeleteEntitiesFromSearchOutput → deletion identification
*
* Covers: malformed LLM responses, missing fields, bad JSON in toolCalls,
* string-only responses, empty tool calls, and prompt construction.
*
* See: https://github.com/mem0ai/mem0/issues/4248
*/
import { MemoryGraph } from "../src/memory/graph_memory";
import {
EXTRACT_RELATIONS_PROMPT,
getDeleteMessages,
} from "../src/graphs/utils";
// ---------------------------------------------------------------------------
// Mocks – we replace heavy dependencies so tests run without Neo4j / OpenAI
// ---------------------------------------------------------------------------
// Mock neo4j-driver: provides a fake Driver with a no-op session
jest.mock("neo4j-driver", () => ({
__esModule: true,
default: {
driver: jest.fn(() => ({
session: () => ({
run: jest.fn().mockResolvedValue({ records: [] }),
close: jest.fn(),
}),
})),
auth: { basic: jest.fn() },
},
}));
// Mock factory so constructor doesn't try to instantiate real LLMs / embedders
const mockGenerateResponse = jest.fn();
const mockGenerateChat = jest.fn();
const mockEmbed = jest.fn().mockResolvedValue([0.1, 0.2, 0.3]);
jest.mock("../src/utils/factory", () => ({
LLMFactory: {
create: jest.fn(() => ({
generateResponse: mockGenerateResponse,
generateChat: mockGenerateChat,
})),
},
EmbedderFactory: {
create: jest.fn(() => ({
embed: mockEmbed,
})),
},
}));
// Minimal config that satisfies the MemoryGraph constructor
function makeConfig(overrides: Record<string, any> = {}) {
return {
graphStore: {
config: {
url: "bolt://localhost:7687",
username: "neo4j",
password: "test",
},
...overrides,
},
embedder: { provider: "openai", config: {} },
llm: { provider: "openai", config: {} },
} as any;
}
// Helper to access private methods via `any` cast
function graph(overrides: Record<string, any> = {}): any {
return new MemoryGraph(makeConfig(overrides));
}
const FILTERS = { userId: "test-user" };
beforeEach(() => {
jest.clearAllMocks();
});
// ═══════════════════════════════════════════════════════════════════════════
// 1. _retrieveNodesFromData – entity extraction
// ═══════════════════════════════════════════════════════════════════════════
describe("_retrieveNodesFromData", () => {
it("parses a well-formed extract_entities tool call", async () => {
mockGenerateResponse.mockResolvedValueOnce({
toolCalls: [
{
name: "extract_entities",
arguments: JSON.stringify({
entities: [
{ entity: "Alice", entity_type: "person" },
{ entity: "Pizza", entity_type: "food" },
],
}),
},
],
});
const mg = graph();
const result = await mg._retrieveNodesFromData(
"Alice likes pizza",
FILTERS,
);
expect(result).toEqual({ alice: "person", pizza: "food" });
});
it("returns empty map when LLM returns a plain string", async () => {
mockGenerateResponse.mockResolvedValueOnce("I am a string, not an object");
const mg = graph();
const result = await mg._retrieveNodesFromData("anything", FILTERS);
expect(result).toEqual({});
});
it("returns empty map when toolCalls is undefined", async () => {
mockGenerateResponse.mockResolvedValueOnce({});
const mg = graph();
const result = await mg._retrieveNodesFromData("anything", FILTERS);
expect(result).toEqual({});
});
it("returns empty map when toolCalls is an empty array", async () => {
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
const mg = graph();
const result = await mg._retrieveNodesFromData("anything", FILTERS);
expect(result).toEqual({});
});
it("handles malformed JSON in tool call arguments gracefully", async () => {
mockGenerateResponse.mockResolvedValueOnce({
toolCalls: [
{ name: "extract_entities", arguments: "NOT VALID JSON {{{" },
],
});
const mg = graph();
// Should not throw — the catch block in the source logs the error
const result = await mg._retrieveNodesFromData("anything", FILTERS);
expect(result).toEqual({});
});
it("handles missing entities array in arguments", async () => {
mockGenerateResponse.mockResolvedValueOnce({
toolCalls: [
{
name: "extract_entities",
arguments: JSON.stringify({ wrong_key: [] }),
},
],
});
const mg = graph();
// args.entities is undefined → for..of on undefined throws → caught
const result = await mg._retrieveNodesFromData("anything", FILTERS);
expect(result).toEqual({});
});
it("skips tool calls with unrelated names", async () => {
mockGenerateResponse.mockResolvedValueOnce({
toolCalls: [
{
name: "some_other_tool",
arguments: JSON.stringify({
entities: [{ entity: "X", entity_type: "Y" }],
}),
},
],
});
const mg = graph();
const result = await mg._retrieveNodesFromData("anything", FILTERS);
expect(result).toEqual({});
});
it("normalises entity names to lowercase with underscores", async () => {
mockGenerateResponse.mockResolvedValueOnce({
toolCalls: [
{
name: "extract_entities",
arguments: JSON.stringify({
entities: [{ entity: "New York City", entity_type: "City Name" }],
}),
},
],
});
const mg = graph();
const result = await mg._retrieveNodesFromData("anything", FILTERS);
expect(result).toEqual({ new_york_city: "city_name" });
});
it("passes json_object response format and the correct system prompt", async () => {
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
const mg = graph();
await mg._retrieveNodesFromData("test data", FILTERS);
const [messages, responseFormat] = mockGenerateResponse.mock.calls[0];
expect(responseFormat).toEqual({ type: "json_object" });
const systemMsg = messages[0].content as string;
expect(systemMsg.toLowerCase()).toContain("json");
expect(systemMsg).toContain("test-user");
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 2. _establishNodesRelationsFromData – relation extraction
// ═══════════════════════════════════════════════════════════════════════════
describe("_establishNodesRelationsFromData", () => {
it("parses a well-formed establish_relationships tool call", async () => {
mockGenerateResponse.mockResolvedValueOnce({
toolCalls: [
{
name: "establish_relationships",
arguments: JSON.stringify({
entities: [
{ source: "Alice", relationship: "likes", destination: "Pizza" },
],
}),
},
],
});
const mg = graph();
const result = await mg._establishNodesRelationsFromData(
"Alice likes pizza",
FILTERS,
{ alice: "person", pizza: "food" },
);
expect(result).toEqual([
{ source: "alice", relationship: "likes", destination: "pizza" },
]);
});
it("returns empty array when LLM returns a string", async () => {
mockGenerateResponse.mockResolvedValueOnce("just a string");
const mg = graph();
const result = await mg._establishNodesRelationsFromData("x", FILTERS, {});
expect(result).toEqual([]);
});
it("returns empty array when toolCalls is empty", async () => {
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
const mg = graph();
const result = await mg._establishNodesRelationsFromData("x", FILTERS, {});
expect(result).toEqual([]);
});
it("returns empty array when entities key is missing from arguments", async () => {
mockGenerateResponse.mockResolvedValueOnce({
toolCalls: [
{
name: "establish_relationships",
arguments: JSON.stringify({ not_entities: [] }),
},
],
});
const mg = graph();
const result = await mg._establishNodesRelationsFromData("x", FILTERS, {});
// args.entities is undefined → falls back to []
expect(result).toEqual([]);
});
it("throws on malformed JSON in tool call arguments (no try/catch in source)", async () => {
mockGenerateResponse.mockResolvedValueOnce({
toolCalls: [{ name: "establish_relationships", arguments: "<<BROKEN>>" }],
});
const mg = graph();
// _establishNodesRelationsFromData does JSON.parse without try/catch
await expect(
mg._establishNodesRelationsFromData("x", FILTERS, {}),
).rejects.toThrow();
});
it("appends JSON format suffix to system prompt (no custom prompt)", async () => {
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
const mg = graph();
await mg._establishNodesRelationsFromData("data", FILTERS, { a: "b" });
const [messages, responseFormat] = mockGenerateResponse.mock.calls[0];
expect(responseFormat).toEqual({ type: "json_object" });
const systemContent = messages[0].content as string;
expect(systemContent.toLowerCase()).toContain("json");
expect(systemContent).toContain("test-user");
expect(systemContent).not.toContain("USER_ID");
// CUSTOM_PROMPT placeholder stays when no custom prompt is configured
// (only replaced when config.graphStore.customPrompt is set)
});
it("appends JSON format suffix and custom prompt when configured", async () => {
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
const mg = graph({ customPrompt: "Focus on food relationships only." });
await mg._establishNodesRelationsFromData("data", FILTERS, {});
const [messages] = mockGenerateResponse.mock.calls[0];
const systemContent = messages[0].content as string;
expect(systemContent.toLowerCase()).toContain("json");
expect(systemContent).toContain("Focus on food relationships only.");
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 3. _getDeleteEntitiesFromSearchOutput – deletion identification
// ═══════════════════════════════════════════════════════════════════════════
describe("_getDeleteEntitiesFromSearchOutput", () => {
const SEARCH_OUTPUT = [
{
source: "alice",
source_id: "1",
relationship: "likes",
relation_id: "r1",
destination: "pizza",
destination_id: "2",
similarity: 0.95,
},
];
it("parses a well-formed delete_graph_memory tool call", async () => {
mockGenerateResponse.mockResolvedValueOnce({
toolCalls: [
{
name: "delete_graph_memory",
arguments: JSON.stringify({
source: "Alice",
relationship: "likes",
destination: "Pizza",
}),
},
],
});
const mg = graph();
const result = await mg._getDeleteEntitiesFromSearchOutput(
SEARCH_OUTPUT,
"Alice hates pizza",
FILTERS,
);
expect(result).toEqual([
{ source: "alice", relationship: "likes", destination: "pizza" },
]);
});
it("returns empty array when LLM returns a string", async () => {
mockGenerateResponse.mockResolvedValueOnce("string response");
const mg = graph();
const result = await mg._getDeleteEntitiesFromSearchOutput(
SEARCH_OUTPUT,
"x",
FILTERS,
);
expect(result).toEqual([]);
});
it("returns empty array when no tool calls are present", async () => {
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
const mg = graph();
const result = await mg._getDeleteEntitiesFromSearchOutput(
SEARCH_OUTPUT,
"x",
FILTERS,
);
expect(result).toEqual([]);
});
it("skips non-delete_graph_memory tool calls", async () => {
mockGenerateResponse.mockResolvedValueOnce({
toolCalls: [
{
name: "noop",
arguments: JSON.stringify({}),
},
],
});
const mg = graph();
const result = await mg._getDeleteEntitiesFromSearchOutput(
SEARCH_OUTPUT,
"x",
FILTERS,
);
expect(result).toEqual([]);
});
it("collects multiple delete tool calls", async () => {
mockGenerateResponse.mockResolvedValueOnce({
toolCalls: [
{
name: "delete_graph_memory",
arguments: JSON.stringify({
source: "A",
relationship: "r1",
destination: "B",
}),
},
{
name: "delete_graph_memory",
arguments: JSON.stringify({
source: "C",
relationship: "r2",
destination: "D",
}),
},
],
});
const mg = graph();
const result = await mg._getDeleteEntitiesFromSearchOutput(
SEARCH_OUTPUT,
"x",
FILTERS,
);
expect(result).toHaveLength(2);
expect(result[0].source).toBe("a");
expect(result[1].source).toBe("c");
});
it("passes json_object format and includes 'json' in system prompt", async () => {
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
const mg = graph();
await mg._getDeleteEntitiesFromSearchOutput(SEARCH_OUTPUT, "data", FILTERS);
const [messages, responseFormat] = mockGenerateResponse.mock.calls[0];
expect(responseFormat).toEqual({ type: "json_object" });
const systemContent = messages[0].content as string;
expect(systemContent.toLowerCase()).toContain("json");
expect(systemContent).toContain("test-user");
expect(systemContent).not.toContain("USER_ID");
});
it("handles empty searchOutput array", async () => {
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
const mg = graph();
const result = await mg._getDeleteEntitiesFromSearchOutput(
[],
"data",
FILTERS,
);
expect(result).toEqual([]);
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 4. Prompt construction — JSON keyword present in every json_object site
// ═══════════════════════════════════════════════════════════════════════════
describe("Prompt construction — all json_object sites include 'json'", () => {
it("_retrieveNodesFromData system message includes 'json' for any userId", async () => {
for (const userId of ["", "user-1", "special<>chars", "ユーザー"]) {
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
const mg = graph();
await mg._retrieveNodesFromData("test", { userId });
const systemMsg = mockGenerateResponse.mock.calls.at(-1)![0][0].content;
expect(systemMsg.toLowerCase()).toContain("json");
}
});
it("_establishNodesRelationsFromData system message includes 'json' for any userId", async () => {
for (const userId of ["", "user-1", "special<>chars"]) {
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
const mg = graph();
await mg._establishNodesRelationsFromData("test", { userId }, {});
const systemMsg = mockGenerateResponse.mock.calls.at(-1)![0][0].content;
expect(systemMsg.toLowerCase()).toContain("json");
}
});
it("_getDeleteEntitiesFromSearchOutput system message includes 'json' for any userId", async () => {
for (const userId of ["", "user-1", "special<>chars"]) {
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
const mg = graph();
await mg._getDeleteEntitiesFromSearchOutput([], "test", { userId });
const systemMsg = mockGenerateResponse.mock.calls.at(-1)![0][0].content;
expect(systemMsg.toLowerCase()).toContain("json");
}
});
});
// ═══════════════════════════════════════════════════════════════════════════
// 5. Edge cases – malformed entity fields in _removeSpacesFromEntities
// ═══════════════════════════════════════════════════════════════════════════
describe("_removeSpacesFromEntities (via _establishNodesRelationsFromData)", () => {
it("normalises spaces and case in entity source/relationship/destination", async () => {
mockGenerateResponse.mockResolvedValueOnce({
toolCalls: [
{
name: "establish_relationships",
arguments: JSON.stringify({
entities: [
{
source: "New York",
relationship: "Capital Of",
destination: "United States",
},
],
}),
},
],
});
const mg = graph();
const result = await mg._establishNodesRelationsFromData(
"test",
FILTERS,
{},
);
expect(result).toEqual([
{
source: "new_york",
relationship: "capital_of",
destination: "united_states",
},
]);
});
});
+177
View File
@@ -0,0 +1,177 @@
import {
DELETE_RELATIONS_SYSTEM_PROMPT,
EXTRACT_RELATIONS_PROMPT,
UPDATE_GRAPH_PROMPT,
getDeleteMessages,
formatEntities,
} from "../src/graphs/utils";
/**
* Regression tests for graph prompts (issue #4248).
*
* When response_format: { type: "json_object" } is used, OpenAI requires
* the word "json" (case-insensitive) to appear in at least one message.
* Missing it produces a 400 error.
*
* Three call sites use json_object today:
* 1. _getDeleteEntitiesFromSearchOutput → DELETE_RELATIONS_SYSTEM_PROMPT
* 2. _retrieveNodesFromData → inline prompt (graph_memory.ts)
* 3. _getRelatedEntities → EXTRACT_RELATIONS_PROMPT + suffix
*
* See: https://github.com/mem0ai/mem0/issues/4248
*/
// ─── JSON keyword presence ────────────────────────────────────────────────────
describe("Graph prompts — JSON keyword requirement", () => {
it("DELETE_RELATIONS_SYSTEM_PROMPT contains 'json'", () => {
expect(DELETE_RELATIONS_SYSTEM_PROMPT.toLowerCase()).toContain("json");
});
it("EXTRACT_RELATIONS_PROMPT produces a message containing 'json' once the suffix is appended", () => {
// graph_memory.ts appends "\nPlease provide your response in JSON format."
const withSuffix =
EXTRACT_RELATIONS_PROMPT +
"\nPlease provide your response in JSON format.";
expect(withSuffix.toLowerCase()).toContain("json");
});
it("getDeleteMessages system message contains 'json' after USER_ID substitution", () => {
const [systemContent] = getDeleteMessages(
"alice -- loves -- pizza",
"Alice now hates pizza",
"user-42",
);
expect(systemContent.toLowerCase()).toContain("json");
});
it("entity extraction inline prompt contains 'json' (simulated from graph_memory.ts)", () => {
// Mirrors the template in _retrieveNodesFromData()
const userId = "user-1";
const prompt = `You are a smart assistant who understands entities and their types in a given text. If user message contains self reference such as 'I', 'me', 'my' etc. then use ${userId} as the source entity. Extract all the entities from the text. ***DO NOT*** answer the question itself if the given text is a question. Respond in JSON format.`;
expect(prompt.toLowerCase()).toContain("json");
});
});
// ─── getDeleteMessages ────────────────────────────────────────────────────────
describe("getDeleteMessages", () => {
it("replaces USER_ID with the provided userId in the system prompt", () => {
const [system] = getDeleteMessages("mem", "data", "alice-123");
expect(system).toContain("alice-123");
expect(system).not.toContain("USER_ID");
});
it("includes existing memories and new data in the user prompt", () => {
const existing = "bob -- knows -- carol";
const newData = "Bob no longer knows Carol";
const [, user] = getDeleteMessages(existing, newData, "u1");
expect(user).toContain(existing);
expect(user).toContain(newData);
});
it("returns a 2-tuple [system, user]", () => {
const result = getDeleteMessages("a", "b", "c");
expect(result).toHaveLength(2);
expect(typeof result[0]).toBe("string");
expect(typeof result[1]).toBe("string");
});
// — Malformed / edge-case inputs —
it("handles empty strings without throwing", () => {
expect(() => getDeleteMessages("", "", "")).not.toThrow();
const [system, user] = getDeleteMessages("", "", "");
expect(system.toLowerCase()).toContain("json");
expect(typeof user).toBe("string");
});
it("handles special characters in userId (e.g. angle brackets, quotes)", () => {
const [system] = getDeleteMessages(
"mem",
"data",
'<script>alert("xss")</script>',
);
expect(system).toContain('<script>alert("xss")</script>');
expect(system).not.toContain("USER_ID");
});
it("handles unicode input", () => {
const [system, user] = getDeleteMessages(
"日本語メモリ",
"新しい情報",
"ユーザー1",
);
expect(system).toContain("ユーザー1");
expect(user).toContain("日本語メモリ");
expect(user).toContain("新しい情報");
});
it("handles very long input strings", () => {
const longStr = "x".repeat(100_000);
expect(() => getDeleteMessages(longStr, longStr, "u")).not.toThrow();
const [system] = getDeleteMessages(longStr, longStr, "u");
expect(system.toLowerCase()).toContain("json");
});
});
// ─── formatEntities ───────────────────────────────────────────────────────────
describe("formatEntities", () => {
it("formats a single entity triplet", () => {
const result = formatEntities([
{ source: "Alice", relationship: "knows", destination: "Bob" },
]);
expect(result).toBe("Alice -- knows -- Bob");
});
it("joins multiple entities with newlines", () => {
const result = formatEntities([
{ source: "A", relationship: "r1", destination: "B" },
{ source: "C", relationship: "r2", destination: "D" },
]);
expect(result).toBe("A -- r1 -- B\nC -- r2 -- D");
});
it("returns empty string for empty array", () => {
expect(formatEntities([])).toBe("");
});
it("preserves special characters in entity fields", () => {
const result = formatEntities([
{ source: "O'Brien", relationship: 'said "hello"', destination: "café" },
]);
expect(result).toContain("O'Brien");
expect(result).toContain('said "hello"');
expect(result).toContain("café");
});
});
// ─── Prompt structural invariants ─────────────────────────────────────────────
describe("Prompt structural invariants", () => {
it("DELETE_RELATIONS_SYSTEM_PROMPT contains USER_ID placeholder", () => {
expect(DELETE_RELATIONS_SYSTEM_PROMPT).toContain("USER_ID");
});
it("EXTRACT_RELATIONS_PROMPT contains USER_ID placeholder", () => {
expect(EXTRACT_RELATIONS_PROMPT).toContain("USER_ID");
});
it("EXTRACT_RELATIONS_PROMPT contains CUSTOM_PROMPT placeholder", () => {
expect(EXTRACT_RELATIONS_PROMPT).toContain("CUSTOM_PROMPT");
});
it("UPDATE_GRAPH_PROMPT contains memory template placeholders", () => {
expect(UPDATE_GRAPH_PROMPT).toContain("{existing_memories}");
expect(UPDATE_GRAPH_PROMPT).toContain("{new_memories}");
});
it("DELETE_RELATIONS_SYSTEM_PROMPT is non-empty and reasonably sized", () => {
expect(DELETE_RELATIONS_SYSTEM_PROMPT.length).toBeGreaterThan(100);
});
it("EXTRACT_RELATIONS_PROMPT is non-empty and reasonably sized", () => {
expect(EXTRACT_RELATIONS_PROMPT.length).toBeGreaterThan(100);
});
});
+531
View File
@@ -0,0 +1,531 @@
/// <reference types="jest" />
/**
* End-to-end tests for Qdrant dimension mismatch fix.
*
* Requires a running Qdrant instance at localhost:6333 (v1.13.x).
* These tests replicate the exact scenarios from issues #4212, #4173, #4056.
*
* Skipped automatically when Qdrant is not available.
*
* Run: npx jest --config jest.config.js src/oss/tests/qdrant-e2e.test.ts --forceExit
*/
import { QdrantClient } from "@qdrant/js-client-rest";
import { Qdrant } from "../src/vector_stores/qdrant";
import { v4 as uuidv4 } from "uuid";
jest.setTimeout(30000);
const QDRANT_HOST = "localhost";
const QDRANT_PORT = 6333;
// Check if Qdrant is reachable synchronously at load time using
// a sync check via child_process so describe.skip works correctly.
function isQdrantAvailable(): boolean {
try {
const { execSync } = require("child_process");
execSync(
`node -e "const s=require('net').createConnection({host:'${QDRANT_HOST}',port:${QDRANT_PORT}});s.on('connect',()=>{s.destroy();process.exit(0)});s.on('error',()=>process.exit(1));s.setTimeout(2000,()=>process.exit(1))"`,
{ timeout: 3000, stdio: "ignore" },
);
return true;
} catch {
return false;
}
}
const qdrantAvailable = isQdrantAvailable();
if (!qdrantAvailable) {
console.warn("Qdrant not available at localhost:6333 — skipping e2e tests");
}
let qdrantClient: QdrantClient;
beforeAll(async () => {
if (!qdrantAvailable) return;
qdrantClient = new QdrantClient({ host: QDRANT_HOST, port: QDRANT_PORT });
const collections = await qdrantClient.getCollections();
expect(collections).toBeDefined();
});
// Helper: delete a collection if it exists
async function deleteCollectionIfExists(name: string) {
try {
await qdrantClient.deleteCollection(name);
} catch {
// Collection doesn't exist — fine
}
}
// Helper: create a fake embedder that produces vectors of a given dimension
function createFakeEmbedder(dims: number) {
return {
embed: jest.fn().mockImplementation(async (_text: string) => {
const vec = new Array(dims).fill(0);
for (let i = 0; i < _text.length && i < dims; i++) {
vec[i] = _text.charCodeAt(i) / 255;
}
return vec;
}),
embedBatch: jest.fn().mockImplementation(async (texts: string[]) => {
return Promise.all(
texts.map(async (t) => {
const vec = new Array(dims).fill(0);
for (let i = 0; i < t.length && i < dims; i++) {
vec[i] = t.charCodeAt(i) / 255;
}
return vec;
}),
);
}),
};
}
// Conditionally skip tests when Qdrant is unavailable
const describeIfQdrant = qdrantAvailable ? describe : describe.skip;
afterAll(async () => {
await deleteCollectionIfExists("e2e_test_768");
await deleteCollectionIfExists("e2e_test_1536");
await deleteCollectionIfExists("e2e_test_race");
await deleteCollectionIfExists("e2e_test_race2");
await deleteCollectionIfExists("e2e_test_noexplicit");
await deleteCollectionIfExists("e2e_test_explicit");
await deleteCollectionIfExists("e2e_test_embdims");
await deleteCollectionIfExists("e2e_test_autodetect");
await deleteCollectionIfExists("memory_migrations");
});
// ───────────────────────────────────────────────────────────────────────────
// 1. Reproduce #4212 / #4173: dimension mismatch with 768-dim embedder
// ───────────────────────────────────────────────────────────────────────────
describeIfQdrant("Issue #4212/#4173: Qdrant dimension mismatch", () => {
it("BEFORE FIX scenario: 768-dim vector into 1536-dim collection → Bad Request", async () => {
const collectionName = "e2e_test_1536";
await deleteCollectionIfExists(collectionName);
await qdrantClient.createCollection(collectionName, {
vectors: { size: 1536, distance: "Cosine" },
});
// Insert a 768-dim vector — this is what nomic-embed-text produces
const vector768 = new Array(768).fill(0.1);
try {
await qdrantClient.upsert(collectionName, {
points: [
{ id: "test-1", vector: vector768, payload: { data: "hello" } },
],
});
fail("Expected Qdrant to reject 768-dim vector into 1536-dim collection");
} catch (error: any) {
// This is the exact "Bad Request" error users were hitting
expect(error.status).toBe(400);
}
await deleteCollectionIfExists(collectionName);
});
it("AFTER FIX: Qdrant store with dimension=768 works end-to-end", async () => {
const collectionName = "e2e_test_768";
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
// Create Qdrant store with correct dimension (what our auto-detect provides)
const store = new Qdrant({
host: QDRANT_HOST,
port: QDRANT_PORT,
collectionName,
embeddingModelDims: 768,
dimension: 768,
});
await store.initialize();
// Verify collection was created with 768 dims
const info = await qdrantClient.getCollection(collectionName);
expect(info.config?.params?.vectors?.size).toBe(768);
// Insert 768-dim vectors (what nomic-embed-text produces)
const vec1 = new Array(768).fill(0);
vec1[0] = 1.0;
const vec2 = new Array(768).fill(0);
vec2[1] = 1.0;
const id1 = uuidv4();
const id2 = uuidv4();
await store.insert(
[vec1, vec2],
[id1, id2],
[
{ data: "hello", userId: "u1" },
{ data: "world", userId: "u1" },
],
);
// Search with 768-dim query — this USED TO fail with Bad Request
const results = await store.search(vec1, 2, { userId: "u1" });
expect(results.length).toBe(2);
expect(results[0].id).toBe(id1); // Most similar to itself
expect(results[0].score).toBeGreaterThan(0.9);
// Get by ID
const item = await store.get(id1);
expect(item).not.toBeNull();
expect(item!.payload.data).toBe("hello");
// Update with 768-dim vector
const vec3 = new Array(768).fill(0);
vec3[2] = 1.0;
await store.update(id1, vec3, { data: "updated", userId: "u1" });
const updated = await store.get(id1);
expect(updated!.payload.data).toBe("updated");
// Delete
await store.delete(id2);
const deleted = await store.get(id2);
expect(deleted).toBeNull();
// List
const [listed, count] = await store.list({ userId: "u1" });
expect(count).toBe(1);
expect(listed[0].payload.data).toBe("updated");
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
});
it("AFTER FIX: Memory auto-detects 768 dims via probe (full integration)", async () => {
const collectionName = "e2e_test_autodetect";
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
const fakeEmbedder = createFakeEmbedder(768);
// Mock only the non-Qdrant factories to avoid Google SDK import crash
jest.resetModules();
jest.doMock("../src/utils/factory", () => {
// Import Qdrant directly (avoids loading Google embedder via factory)
const { Qdrant: QdrantStore } = require("../src/vector_stores/qdrant");
return {
EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) },
VectorStoreFactory: {
create: jest
.fn()
.mockImplementation((_provider: string, config: any) => {
return new QdrantStore(config);
}),
},
LLMFactory: {
create: jest.fn().mockReturnValue({
generateResponse: jest.fn().mockResolvedValue('{"facts":[]}'),
}),
},
HistoryManagerFactory: {
create: jest.fn().mockReturnValue({
addHistory: jest.fn().mockResolvedValue(undefined),
getHistory: jest.fn().mockResolvedValue([]),
reset: jest.fn().mockResolvedValue(undefined),
}),
},
};
});
jest.doMock("../src/utils/telemetry", () => ({
captureClientEvent: jest.fn().mockResolvedValue(undefined),
}));
const { Memory } = require("../src/memory");
// This is the EXACT config from issue #4212 — NO dimension specified
const mem = new Memory({
embedder: {
provider: "ollama",
config: { model: "nomic-embed-text" },
},
vectorStore: {
provider: "qdrant",
config: {
host: QDRANT_HOST,
port: QDRANT_PORT,
collectionName,
},
},
llm: { provider: "openai", config: { apiKey: "fake" } },
disableHistory: true,
});
// This triggers init — probe should detect 768 dims
await mem.getAll({ userId: "test-user" });
// Verify the probe was called
expect(fakeEmbedder.embed).toHaveBeenCalledWith("dimension probe");
// Verify Qdrant collection was created with auto-detected 768 dims
const collectionInfo = await qdrantClient.getCollection(collectionName);
expect(collectionInfo.config?.params?.vectors?.size).toBe(768);
// Search should work (this used to throw Bad Request)
const searchResult = await mem.search("hello world", {
userId: "test-user",
});
expect(searchResult).toBeDefined();
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
jest.resetModules();
});
it("AFTER FIX: explicit dimension=768 skips probe (backward compat)", async () => {
const collectionName = "e2e_test_explicit";
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
const fakeEmbedder = createFakeEmbedder(768);
jest.resetModules();
jest.doMock("../src/utils/factory", () => {
const { Qdrant: QdrantStore } = require("../src/vector_stores/qdrant");
return {
EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) },
VectorStoreFactory: {
create: jest
.fn()
.mockImplementation((_provider: string, config: any) => {
return new QdrantStore(config);
}),
},
LLMFactory: {
create: jest.fn().mockReturnValue({
generateResponse: jest.fn().mockResolvedValue('{"facts":[]}'),
}),
},
HistoryManagerFactory: {
create: jest.fn().mockReturnValue({
addHistory: jest.fn().mockResolvedValue(undefined),
getHistory: jest.fn().mockResolvedValue([]),
reset: jest.fn().mockResolvedValue(undefined),
}),
},
};
});
jest.doMock("../src/utils/telemetry", () => ({
captureClientEvent: jest.fn().mockResolvedValue(undefined),
}));
const { Memory } = require("../src/memory");
// Workaround config from #4212 — explicit dimension
const mem = new Memory({
embedder: {
provider: "ollama",
config: { model: "nomic-embed-text" },
},
vectorStore: {
provider: "qdrant",
config: {
host: QDRANT_HOST,
port: QDRANT_PORT,
collectionName,
dimension: 768,
},
},
llm: { provider: "openai", config: { apiKey: "fake" } },
disableHistory: true,
});
await mem.getAll({ userId: "test-user" });
// Probe should NOT have been called
expect(fakeEmbedder.embed).not.toHaveBeenCalledWith("dimension probe");
const collectionInfo = await qdrantClient.getCollection(collectionName);
expect(collectionInfo.config?.params?.vectors?.size).toBe(768);
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
jest.resetModules();
});
it("AFTER FIX: embeddingDims in embedder config skips probe", async () => {
const collectionName = "e2e_test_embdims";
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
const fakeEmbedder = createFakeEmbedder(768);
jest.resetModules();
jest.doMock("../src/utils/factory", () => {
const { Qdrant: QdrantStore } = require("../src/vector_stores/qdrant");
return {
EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) },
VectorStoreFactory: {
create: jest
.fn()
.mockImplementation((_provider: string, config: any) => {
return new QdrantStore(config);
}),
},
LLMFactory: {
create: jest.fn().mockReturnValue({
generateResponse: jest.fn().mockResolvedValue('{"facts":[]}'),
}),
},
HistoryManagerFactory: {
create: jest.fn().mockReturnValue({
addHistory: jest.fn().mockResolvedValue(undefined),
getHistory: jest.fn().mockResolvedValue([]),
reset: jest.fn().mockResolvedValue(undefined),
}),
},
};
});
jest.doMock("../src/utils/telemetry", () => ({
captureClientEvent: jest.fn().mockResolvedValue(undefined),
}));
const { Memory } = require("../src/memory");
const mem = new Memory({
embedder: {
provider: "ollama",
config: { model: "nomic-embed-text", embeddingDims: 768 },
},
vectorStore: {
provider: "qdrant",
config: {
host: QDRANT_HOST,
port: QDRANT_PORT,
collectionName,
},
},
llm: { provider: "openai", config: { apiKey: "fake" } },
disableHistory: true,
});
await mem.getAll({ userId: "test-user" });
// Probe should NOT have been called — dimension inferred from embeddingDims
expect(fakeEmbedder.embed).not.toHaveBeenCalledWith("dimension probe");
const collectionInfo = await qdrantClient.getCollection(collectionName);
expect(collectionInfo.config?.params?.vectors?.size).toBe(768);
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
jest.resetModules();
});
});
// ───────────────────────────────────────────────────────────────────────────
// 2. Reproduce #4056 issue 1: Collection creation race condition
// ───────────────────────────────────────────────────────────────────────────
describeIfQdrant("Issue #4056: Qdrant race condition", () => {
it("concurrent ensureCollection calls don't crash (no 409 error leak)", async () => {
const collectionName = "e2e_test_race";
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
// Create 5 Qdrant instances concurrently — this simulates the race
// that caused "Collection memory_migrations already exists!" in #4056
const instances = Array.from(
{ length: 5 },
() =>
new Qdrant({
host: QDRANT_HOST,
port: QDRANT_PORT,
collectionName,
embeddingModelDims: 768,
dimension: 768,
}),
);
// All should initialize without throwing 409 Conflict
await Promise.all(instances.map((inst) => inst.initialize()));
// Verify collection exists with correct dimension
const info = await qdrantClient.getCollection(collectionName);
expect(info.config?.params?.vectors?.size).toBe(768);
// memory_migrations should also exist (created by initialize)
const migrInfo = await qdrantClient.getCollection("memory_migrations");
expect(migrInfo.config?.params?.vectors?.size).toBe(1);
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
});
it("getUserId works after concurrent initialization", async () => {
const collectionName = "e2e_test_race2";
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
const instance = new Qdrant({
host: QDRANT_HOST,
port: QDRANT_PORT,
collectionName,
embeddingModelDims: 768,
dimension: 768,
});
await instance.initialize();
// getUserId should work without 409 crash
const userId = await instance.getUserId();
expect(typeof userId).toBe("string");
expect(userId.length).toBeGreaterThan(0);
// setUserId + getUserId roundtrip
await instance.setUserId("custom-e2e-user");
const updated = await instance.getUserId();
expect(updated).toBe("custom-e2e-user");
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
});
});
// ───────────────────────────────────────────────────────────────────────────
// 3. Reproduce #4056 issue 2: memory_migrations dimension isolation
// ───────────────────────────────────────────────────────────────────────────
describeIfQdrant("Issue #4056: memory_migrations dimension isolation", () => {
it("memory_migrations uses dim=1 independently of main collection dim=768", async () => {
const collectionName = "e2e_test_noexplicit";
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
const instance = new Qdrant({
host: QDRANT_HOST,
port: QDRANT_PORT,
collectionName,
embeddingModelDims: 768,
dimension: 768,
});
await instance.initialize();
// Allow Qdrant a moment to fully commit collections
await new Promise((r) => setTimeout(r, 500));
// Main collection should be 768
const mainInfo = await qdrantClient.getCollection(collectionName);
expect(mainInfo.config?.params?.vectors?.size).toBe(768);
// memory_migrations should be 1 (NOT 768!)
// This was the bug in #4056 issue 2 — telemetry used wrong dimension
const migrationsInfo =
await qdrantClient.getCollection("memory_migrations");
expect(migrationsInfo.config?.params?.vectors?.size).toBe(1);
// getUserId should work — vector dim=1 in memory_migrations
const userId = await instance.getUserId();
expect(typeof userId).toBe("string");
// setUserId should also work
await instance.setUserId("custom-test-user");
const newUserId = await instance.getUserId();
expect(newUserId).toBe("custom-test-user");
await deleteCollectionIfExists(collectionName);
await deleteCollectionIfExists("memory_migrations");
});
});
+427
View File
@@ -0,0 +1,427 @@
/// <reference types="jest" />
/**
* End-to-end tests for Redis vector store with init guard fix.
*
* Requires a running Redis Stack instance at localhost:6379.
* Skipped automatically when Redis is not available.
*
* Run: npx jest --config jest.config.js src/oss/tests/redis-e2e.test.ts --forceExit
*/
import { createClient } from "redis";
import { RedisDB } from "../src/vector_stores/redis";
import { v4 as uuidv4 } from "uuid";
jest.setTimeout(30000);
const REDIS_HOST = "localhost";
const REDIS_PORT = 6379;
const REDIS_URL = `redis://${REDIS_HOST}:${REDIS_PORT}`;
const COLLECTION_NAME = "e2e_redis_test";
// Check if Redis is reachable synchronously at load time
function isRedisAvailable(): boolean {
try {
const { execSync } = require("child_process");
execSync(
`node -e "const s=require('net').createConnection({host:'${REDIS_HOST}',port:${REDIS_PORT}});s.on('connect',()=>{s.destroy();process.exit(0)});s.on('error',()=>process.exit(1));s.setTimeout(2000,()=>process.exit(1))"`,
{ timeout: 3000, stdio: "ignore" },
);
return true;
} catch {
return false;
}
}
const redisAvailable = isRedisAvailable();
if (!redisAvailable) {
console.warn("Redis not available at localhost:6379 — skipping e2e tests");
}
// Standalone client for cleanup
let cleanupClient: ReturnType<typeof createClient>;
async function cleanupRedis() {
if (!redisAvailable) return;
try {
// Drop the index if it exists
await cleanupClient.ft.dropIndex(COLLECTION_NAME);
} catch {
// Index doesn't exist — fine
}
// Delete all keys with our prefix
const keys = await cleanupClient.keys(`mem0:${COLLECTION_NAME}:*`);
if (keys.length > 0) {
await cleanupClient.del(keys);
}
// Clean up memory_migrations key
await cleanupClient.del("memory_migrations:1");
}
beforeAll(async () => {
if (!redisAvailable) return;
cleanupClient = createClient({ url: REDIS_URL });
await cleanupClient.connect();
// Verify Redis Stack is running with search module
const modules = (await cleanupClient.moduleList()) as unknown as any[];
const hasSearch = modules.some((mod: any[]) => {
const moduleMap = new Map();
for (let i = 0; i < mod.length; i += 2) {
moduleMap.set(mod[i], mod[i + 1]);
}
return moduleMap.get("name")?.toLowerCase() === "search";
});
expect(hasSearch).toBe(true);
});
afterAll(async () => {
if (!redisAvailable) return;
await cleanupRedis();
await cleanupClient.quit();
});
// Conditionally skip tests when Redis is unavailable
const describeIfRedis = redisAvailable ? describe : describe.skip;
// ───────────────────────────────────────────────────────────────────────────
// 1. Basic initialization and idempotent init guard
// ───────────────────────────────────────────────────────────────────────────
describeIfRedis("Redis: initialization", () => {
afterEach(async () => {
await cleanupRedis();
});
it("initializes successfully and creates index", async () => {
const store = new RedisDB({
redisUrl: REDIS_URL,
collectionName: COLLECTION_NAME,
embeddingModelDims: 128,
});
await store.initialize();
// Verify the index was created by querying index info
const info = await cleanupClient.ft.info(COLLECTION_NAME);
expect(info).toBeDefined();
expect(info.indexName).toBe(COLLECTION_NAME);
await store.close();
});
it("idempotent initialize() — multiple calls don't crash", async () => {
const store = new RedisDB({
redisUrl: REDIS_URL,
collectionName: COLLECTION_NAME,
embeddingModelDims: 128,
});
// Call initialize multiple times concurrently
await Promise.all([
store.initialize(),
store.initialize(),
store.initialize(),
]);
// Should still work fine
const info = await cleanupClient.ft.info(COLLECTION_NAME);
expect(info).toBeDefined();
await store.close();
});
});
// ───────────────────────────────────────────────────────────────────────────
// 2. Full CRUD operations
// ───────────────────────────────────────────────────────────────────────────
describeIfRedis("Redis: CRUD operations", () => {
let store: RedisDB;
beforeEach(async () => {
await cleanupRedis();
store = new RedisDB({
redisUrl: REDIS_URL,
collectionName: COLLECTION_NAME,
embeddingModelDims: 4, // Small dims for testing
});
await store.initialize();
});
afterEach(async () => {
await store.close();
await cleanupRedis();
});
it("insert and search vectors", async () => {
const id1 = uuidv4();
const id2 = uuidv4();
const vec1 = [1.0, 0.0, 0.0, 0.0];
const vec2 = [0.0, 1.0, 0.0, 0.0];
await store.insert(
[vec1, vec2],
[id1, id2],
[
{
data: "hello world",
hash: "h1",
userId: "user1",
createdAt: new Date().toISOString(),
},
{
data: "goodbye world",
hash: "h2",
userId: "user1",
createdAt: new Date().toISOString(),
},
],
);
// Search — vec1 should be most similar to itself
const results = await store.search(vec1, 2, { userId: "user1" });
expect(results.length).toBe(2);
// The first result should be closest to the query
expect(results[0].id).toBe(id1);
expect(results[0].score).toBeDefined();
expect(results[0].payload).toBeDefined();
});
it("get vector by ID", async () => {
const id = uuidv4();
const vec = [0.5, 0.5, 0.0, 0.0];
await store.insert(
[vec],
[id],
[
{
data: "test memory",
hash: "h-test",
userId: "user1",
createdAt: new Date().toISOString(),
},
],
);
const result = await store.get(id);
expect(result).not.toBeNull();
expect(result!.id).toBe(id);
expect(result!.payload.data).toBe("test memory");
expect(result!.payload.hash).toBe("h-test");
});
it("get non-existent vector returns null", async () => {
const result = await store.get("non-existent-id");
expect(result).toBeNull();
});
it("update vector and payload", async () => {
const id = uuidv4();
const vec = [1.0, 0.0, 0.0, 0.0];
await store.insert(
[vec],
[id],
[
{
data: "original",
hash: "h-orig",
userId: "user1",
createdAt: new Date().toISOString(),
},
],
);
// Update with new vector and payload
const newVec = [0.0, 0.0, 1.0, 0.0];
await store.update(id, newVec, {
data: "updated memory",
hash: "h-updated",
userId: "user1",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
const result = await store.get(id);
expect(result).not.toBeNull();
expect(result!.payload.data).toBe("updated memory");
expect(result!.payload.hash).toBe("h-updated");
});
it("delete vector", async () => {
const id = uuidv4();
const vec = [0.0, 0.0, 0.0, 1.0];
await store.insert(
[vec],
[id],
[
{
data: "to be deleted",
hash: "h-del",
userId: "user1",
createdAt: new Date().toISOString(),
},
],
);
// Verify it exists
const before = await store.get(id);
expect(before).not.toBeNull();
// Delete
await store.delete(id);
// Verify it's gone
const after = await store.get(id);
expect(after).toBeNull();
});
it("list vectors with filters", async () => {
const id1 = uuidv4();
const id2 = uuidv4();
const id3 = uuidv4();
await store.insert(
[
[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
],
[id1, id2, id3],
[
{
data: "mem1",
hash: "h1",
userId: "usera",
createdAt: new Date().toISOString(),
},
{
data: "mem2",
hash: "h2",
userId: "usera",
createdAt: new Date().toISOString(),
},
{
data: "mem3",
hash: "h3",
userId: "userb",
createdAt: new Date().toISOString(),
},
],
);
// List all
const [all, allCount] = await store.list();
expect(allCount).toBe(3);
expect(all.length).toBe(3);
// List with filter
const [filtered, filteredCount] = await store.list({
userId: "usera",
});
expect(filteredCount).toBe(2);
expect(filtered.length).toBe(2);
});
});
// ───────────────────────────────────────────────────────────────────────────
// 3. getUserId / setUserId
// ───────────────────────────────────────────────────────────────────────────
describeIfRedis("Redis: getUserId / setUserId", () => {
let store: RedisDB;
beforeEach(async () => {
await cleanupRedis();
store = new RedisDB({
redisUrl: REDIS_URL,
collectionName: COLLECTION_NAME,
embeddingModelDims: 4,
});
await store.initialize();
});
afterEach(async () => {
await store.close();
await cleanupRedis();
});
it("getUserId generates random ID if none exists", async () => {
const userId = await store.getUserId();
expect(typeof userId).toBe("string");
expect(userId.length).toBeGreaterThan(0);
});
it("setUserId + getUserId roundtrip", async () => {
await store.setUserId("custom-redis-user");
const retrieved = await store.getUserId();
expect(retrieved).toBe("custom-redis-user");
});
it("getUserId returns same value on subsequent calls", async () => {
const first = await store.getUserId();
const second = await store.getUserId();
expect(first).toBe(second);
});
});
// ───────────────────────────────────────────────────────────────────────────
// 4. Dimension handling (our fix ensures correct dims from Memory)
// ───────────────────────────────────────────────────────────────────────────
describeIfRedis("Redis: dimension handling", () => {
afterEach(async () => {
await cleanupRedis();
});
it("creates index with correct dimensions from config", async () => {
const store = new RedisDB({
redisUrl: REDIS_URL,
collectionName: COLLECTION_NAME,
embeddingModelDims: 768,
});
await store.initialize();
// Verify the index has the right dimension in its schema
const info = await cleanupClient.ft.info(COLLECTION_NAME);
// Check that the vector field has DIM=768
const attributes = info.attributes as any[];
const vectorAttr = attributes.find(
(a: any) => a.identifier === "embedding" || a.attribute === "embedding",
);
expect(vectorAttr).toBeDefined();
await store.close();
});
it("insert with matching dimension succeeds", async () => {
const dims = 128;
const store = new RedisDB({
redisUrl: REDIS_URL,
collectionName: COLLECTION_NAME,
embeddingModelDims: dims,
});
await store.initialize();
const id = uuidv4();
const vec = new Array(dims).fill(0.1);
await store.insert(
[vec],
[id],
[
{
data: "test",
hash: "h1",
createdAt: new Date().toISOString(),
},
],
);
const result = await store.get(id);
expect(result).not.toBeNull();
expect(result!.id).toBe(id);
await store.close();
});
});
@@ -0,0 +1,30 @@
import { removeCodeBlocks } from "../src/prompts";
describe("removeCodeBlocks", () => {
it("extracts JSON from ```json code fence", () => {
const input = '```json\n{"facts": ["hello"]}\n```';
expect(removeCodeBlocks(input)).toBe('{"facts": ["hello"]}');
});
it("extracts content from bare ``` code fence", () => {
const input = '```\n{"key": "value"}\n```';
expect(removeCodeBlocks(input)).toBe('{"key": "value"}');
});
it("returns plain text unchanged", () => {
const input = '{"facts": ["hello"]}';
expect(removeCodeBlocks(input)).toBe('{"facts": ["hello"]}');
});
it("handles multiple code blocks", () => {
const input = '```json\n{"a":1}\n```\nsome text\n```json\n{"b":2}\n```';
expect(removeCodeBlocks(input)).toBe('{"a":1}\n\nsome text\n{"b":2}');
});
it("handles Claude-style response with surrounding text", () => {
const input =
'Here is the JSON:\n```json\n{"facts": ["user likes TypeScript"]}\n```';
expect(removeCodeBlocks(input)).toContain('"facts"');
expect(removeCodeBlocks(input)).not.toContain("```");
});
});
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -30,10 +30,10 @@ class MemoryGraph:
def __init__(self, config):
self.config = config
self.graph = Neo4jGraph(
self.config.graph_store.config.url,
self.config.graph_store.config.username,
self.config.graph_store.config.password,
self.config.graph_store.config.database,
url=self.config.graph_store.config.url,
username=self.config.graph_store.config.username,
password=self.config.graph_store.config.password,
database=self.config.graph_store.config.database,
refresh_schema=False,
driver_config={"notifications_min_severity": "OFF"},
)
+2
View File
@@ -0,0 +1,2 @@
package-manager-strict-version=false
approve-builds=esbuild
+5
View File
@@ -2,6 +2,11 @@
All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in this file.
## [0.3.1] - 2026-03-12
### Fixed
- **README image on npmjs.com**: Changed architecture diagram from relative path to absolute GitHub URL so it renders correctly on the npm registry
## [0.3.0] - 2026-03-10
### Fixed
+1 -1
View File
@@ -7,7 +7,7 @@ Your agent forgets everything between sessions. This plugin fixes that. It watch
## How it works
<p align="center">
<img src="../docs/images/openclaw-architecture.png" alt="Architecture" width="800" />
<img src="https://raw.githubusercontent.com/mem0ai/mem0/main/docs/images/openclaw-architecture.png" alt="Architecture" width="800" />
</p>
**Auto-Recall** — Before the agent responds, the plugin searches Mem0 for memories that match the current message and injects them into context.
+1 -1
View File
@@ -138,7 +138,7 @@ class PlatformProvider implements Mem0Provider {
private async _init(): Promise<void> {
const { default: MemoryClient } = await import("mem0ai");
const opts: Record<string, string> = { apiKey: this.apiKey };
const opts: { apiKey: string; org_id?: string; project_id?: string } = { apiKey: this.apiKey };
if (this.orgId) opts.org_id = this.orgId;
if (this.projectId) opts.project_id = this.projectId;
this.client = new MemoryClient(opts);
+30
View File
@@ -0,0 +1,30 @@
declare module "openclaw/plugin-sdk" {
export interface OpenClawPluginApi {
pluginConfig: Record<string, unknown>;
logger: {
info(msg: string): void;
warn(msg: string): void;
error(msg: string): void;
debug(msg: string): void;
};
resolvePath(p: string): string;
registerTool(
definition: Record<string, unknown>,
metadata?: Record<string, unknown>,
): void;
on(
event: string,
handler: (event: any, ctx: any) => any,
): void;
registerCli(
handler: (context: { program: any }) => void,
options?: Record<string, unknown>,
): void;
registerService(service: {
id: string;
start: () => void;
stop: () => void;
}): void;
[key: string]: unknown;
}
}
+18 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/openclaw-mem0",
"version": "0.3.0",
"version": "0.3.3",
"type": "module",
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
"license": "Apache-2.0",
@@ -11,7 +11,20 @@
"mem0",
"long-term-memory"
],
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist",
"openclaw.plugin.json"
],
"scripts": {
"build": "tsup",
"test": "vitest run"
},
"dependencies": {
@@ -20,10 +33,13 @@
},
"openclaw": {
"extensions": [
"./index.ts"
"./dist/index.js"
]
},
"devDependencies": {
"@types/node": "^22.15.0",
"tsup": "^8.5.0",
"typescript": "^5.8.3",
"vitest": "^4.0.18"
}
}
+4105
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
approveBuilds: esbuild
onlyBuiltDependencies:
- better-sqlite3
- esbuild
- protobufjs
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": ".",
"strict": false,
"noImplicitAny": false,
"types": ["node"],
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true
},
"include": ["index.ts", "openclaw-plugin-sdk.d.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
+9
View File
@@ -0,0 +1,9 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["index.ts"],
format: ["esm"],
dts: true,
sourcemap: true,
clean: true,
});
+1 -1
View File
@@ -20,7 +20,7 @@ dependencies = [
"posthog>=3.5.0",
"pytz>=2024.1",
"sqlalchemy>=2.0.31",
"protobuf>=5.29.0,<6.0.0",
"protobuf>=5.29.6,<7.0.0",
]
[project.optional-dependencies]
+189
View File
@@ -0,0 +1,189 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but not
limited to compiled object code, generated documentation, and
conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work.
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2024 Mem0.ai
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+85
View File
@@ -0,0 +1,85 @@
# Mem0 Skill for Claude
Add persistent memory to any AI application in minutes using [Mem0 Platform](https://app.mem0.ai).
## What This Skill Does
When installed, Claude can:
- **Set up Mem0** in your Python or TypeScript project
- **Integrate memory** into your existing AI app (LangChain, CrewAI, Vercel AI, OpenAI Agents, LangGraph, LlamaIndex, etc.)
- **Generate working code** using real API references and tested patterns
- **Search live docs** on demand for the latest Mem0 documentation
## Installation
### CLI (Claude Code, OpenCode, OpenClaw, or any tool that supports skills)
```bash
npx skills add https://github.com/mem0ai/mem0 --skill mem0
```
### Claude.ai
1. Download this `skills/mem0` folder as a ZIP
2. Go to **Settings > Capabilities > Skills**
3. Click **Upload skill** and select the ZIP
### Claude API (Skills API)
```bash
curl -X POST https://api.anthropic.com/v1/skills \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "mem0", "source": "https://github.com/mem0ai/mem0/tree/main/skills/mem0"}'
```
### Prerequisites
- A Mem0 Platform API key ([Get one here](https://app.mem0.ai/dashboard/api-keys))
- Python 3.10+ or Node.js 18+
- Set the environment variable:
```bash
export MEM0_API_KEY="m0-your-api-key"
```
## Quick Start
After installing, just ask Claude:
- "Set up mem0 in my project"
- "Add memory to my chatbot"
- "Help me search user memories with filters"
- "Integrate mem0 with my LangChain app"
- "Add graph memory to track entity relationships"
## What's Inside
```text
skills/mem0/
├── SKILL.md # Skill definition and instructions
├── README.md # This file
├── LICENSE # Apache-2.0
├── scripts/
│ └── mem0_doc_search.py # Search live Mem0 docs on demand
└── references/ # Documentation (loaded on demand)
├── quickstart.md # Full quickstart (Python, TS, cURL)
├── sdk-guide.md # All SDK methods (Python + TypeScript)
├── api-reference.md # REST endpoints, filters, memory object
├── architecture.md # Processing pipeline, lifecycle, scoping, performance
├── features.md # Retrieval, graph, categories, MCP, webhooks, multimodal
├── integration-patterns.md # LangChain, CrewAI, Vercel AI, LangGraph, LlamaIndex, etc.
└── use-cases.md # 7 real-world patterns with Python + TypeScript code
```
## Links
- [Mem0 Platform Dashboard](https://app.mem0.ai)
- [Mem0 Documentation](https://docs.mem0.ai)
- [Mem0 GitHub](https://github.com/mem0ai/mem0)
- [API Reference](https://docs.mem0.ai/api-reference)
## License
Apache-2.0
+156
View File
@@ -0,0 +1,156 @@
---
name: mem0
description: >
Integrate Mem0 Platform into AI applications for persistent memory, personalization, and semantic search.
Use this skill when the user mentions "mem0", "memory layer", "remember user preferences",
"persistent context", "personalization", or needs to add long-term memory to chatbots, agents,
or AI apps. Covers Python and TypeScript SDKs, framework integrations (LangChain, CrewAI,
Vercel AI SDK, OpenAI Agents SDK, Pipecat), and the full Platform API. Use even when the user
doesn't explicitly say "mem0" but describes needing conversation memory, user context retention,
or knowledge retrieval across sessions.
license: Apache-2.0
metadata:
author: mem0ai
version: "1.0.0"
category: ai-memory
tags: "memory, personalization, ai, python, typescript, vector-search"
compatibility: Requires Python 3.10+ or Node.js 18+, pip install mem0ai or npm install mem0ai, MEM0_API_KEY env var, and internet access to api.mem0.ai
---
# Mem0 Platform Integration
Mem0 is a managed memory layer for AI applications. It stores, retrieves, and manages user memories via API — no infrastructure to deploy.
## Step 1: Install and authenticate
**Python:**
```bash
pip install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
**TypeScript/JavaScript:**
```bash
npm install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
Get an API key at: https://app.mem0.ai/dashboard/api-keys
## Step 2: Initialize the client
**Python:**
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="m0-xxx")
```
**TypeScript:**
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: 'm0-xxx' });
```
For async Python, use `AsyncMemoryClient`.
## Step 3: Core operations
Every Mem0 integration follows the same pattern: **retrieve → generate → store**.
### Add memories
```python
messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember that."}
]
client.add(messages, user_id="alice")
```
### Search memories
```python
results = client.search("dietary preferences", user_id="alice")
for mem in results.get("results", []):
print(mem["memory"])
```
### Get all memories
```python
all_memories = client.get_all(user_id="alice")
```
### Update a memory
```python
client.update("memory-uuid", text="Updated: vegetarian, nut allergy, prefers organic")
```
### Delete a memory
```python
client.delete("memory-uuid")
client.delete_all(user_id="alice") # delete all for a user
```
## Common integration pattern
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai = OpenAI()
def chat(user_input: str, user_id: str) -> str:
# 1. Retrieve relevant memories
memories = mem0.search(user_input, user_id=user_id)
context = "\n".join([m["memory"] for m in memories.get("results", [])])
# 2. Generate response with memory context
response = openai.chat.completions.create(
model="gpt-4.1-nano-2025-04-14",
messages=[
{"role": "system", "content": f"User context:\n{context}"},
{"role": "user", "content": user_input},
]
)
reply = response.choices[0].message.content
# 3. Store interaction for future context
mem0.add(
[{"role": "user", "content": user_input}, {"role": "assistant", "content": reply}],
user_id=user_id
)
return reply
```
## Common edge cases
- **Search returns empty:** Memories process asynchronously. Wait 2-3s after `add()` before searching. Also verify `user_id` matches exactly (case-sensitive).
- **AND filter with user_id + agent_id returns empty:** Entities are stored separately. Use `OR` instead, or query separately.
- **Duplicate memories:** Don't mix `infer=True` (default) and `infer=False` for the same data. Stick to one mode.
- **Wrong import:** Always use `from mem0 import MemoryClient` (or `AsyncMemoryClient` for async). Do not use `from mem0 import Memory`.
- **Immutable memories:** Cannot be updated or deleted once created. Use `client.history(memory_id)` to track changes over time.
## Live documentation search
For the latest docs beyond what's in the references, use the doc search tool:
```bash
python scripts/mem0_doc_search.py --query "topic"
python scripts/mem0_doc_search.py --page "/platform/features/graph-memory"
python scripts/mem0_doc_search.py --index
```
No API key needed — searches docs.mem0.ai directly.
## References
Load these on demand for deeper detail:
| Topic | File |
|-------|------|
| Quickstart (Python, TS, cURL) | [references/quickstart.md](references/quickstart.md) |
| SDK guide (all methods, both languages) | [references/sdk-guide.md](references/sdk-guide.md) |
| API reference (endpoints, filters, object schema) | [references/api-reference.md](references/api-reference.md) |
| Architecture (pipeline, lifecycle, scoping, performance) | [references/architecture.md](references/architecture.md) |
| Platform features (retrieval, graph, categories, MCP, etc.) | [references/features.md](references/features.md) |
| Framework integrations (LangChain, CrewAI, Vercel AI, etc.) | [references/integration-patterns.md](references/integration-patterns.md) |
| Use cases & examples (real-world patterns with code) | [references/use-cases.md](references/use-cases.md) |
+140
View File
@@ -0,0 +1,140 @@
# Mem0 Platform API Reference
REST API endpoints for the Mem0 Platform. Base URL: `https://api.mem0.ai`
All endpoints require: `Authorization: Token <MEM0_API_KEY>`
## Endpoints
| Operation | Method | URL |
|-----------|--------|-----|
| Add Memories | `POST` | `/v1/memories/` |
| Search Memories | `POST` | `/v2/memories/search/` |
| Get All Memories | `POST` | `/v2/memories/` |
| Get Single Memory | `GET` | `/v1/memories/{memory_id}/` |
| Update Memory | `PUT` | `/v1/memories/{memory_id}/` |
| Delete Memory | `DELETE` | `/v1/memories/{memory_id}/` |
## Memory Object Structure
| Field | Type | Description |
|-------|------|-------------|
| `id` | string (UUID) | Unique memory identifier |
| `memory` | string | Text content of the memory |
| `user_id` | string | Associated user |
| `agent_id` | string (nullable) | Agent identifier |
| `app_id` | string (nullable) | Application identifier |
| `run_id` | string (nullable) | Run/session identifier |
| `metadata` | object | Custom key-value pairs |
| `categories` | array of strings | Auto-assigned category tags |
| `immutable` | boolean | If true, prevents modification |
| `expiration_date` | datetime (nullable) | Auto-expiry date |
| `hash` | string | Content hash |
| `created_at` | datetime | Creation timestamp |
| `updated_at` | datetime | Last modification timestamp |
Search results additionally include `score` (relevance metric).
## Scoping Identifiers
Memories can be scoped to different levels:
| Scope | Parameter | Use Case |
|-------|-----------|----------|
| User | `user_id` | Per-user memory isolation |
| Agent | `agent_id` | Per-agent memory partitioning |
| Application | `app_id` | Cross-agent app-level memory |
| Run/Session | `run_id` | Session-scoped temporary memory |
**Critical:** Combining `user_id` and `agent_id` in a single AND filter yields empty results. Entities are stored separately. Use `OR` logic or separate queries.
## Processing Model
- Memories are processed **asynchronously by default** (`async_mode=true`)
- Add responses return queued events (`ADD`, `UPDATE`, `DELETE`) for tracking
- Set `async_mode=false` for synchronous processing when needed
- Graph metadata is processed asynchronously -- use `get_all()` for complete graph data
## Filter System
Filters use nested JSON with a logical operator at the root:
```json
{
"AND": [
{"user_id": "alice"},
{"categories": {"contains": "finance"}},
{"created_at": {"gte": "2024-01-01"}}
]
}
```
Root must be `AND`, `OR`, or `NOT`. Simple shorthand `{"user_id": "alice"}` also works.
### Supported Operators
| Operator | Description |
|----------|-------------|
| `eq` | Equal to (default) |
| `ne` | Not equal to |
| `in` | Matches any value in array |
| `gt`, `gte` | Greater than / greater than or equal |
| `lt`, `lte` | Less than / less than or equal |
| `contains` | Case-sensitive containment |
| `icontains` | Case-insensitive containment |
| `*` | Wildcard -- matches any non-null value |
### Filterable Fields
| Field | Valid Operators |
|-------|-----------------|
| `user_id`, `agent_id`, `app_id`, `run_id` | `eq`, `ne`, `in`, `*` |
| `created_at`, `updated_at`, `timestamp` | `gt`, `gte`, `lt`, `lte`, `eq`, `ne` |
| `categories` | `eq`, `ne`, `in`, `contains` |
| `metadata` | `eq`, `ne`, `contains` (top-level keys only) |
| `keywords` | `contains`, `icontains` |
| `memory_ids` | `in` |
### Filter Constraints
1. **Entity scope partitioning:** `user_id` AND `agent_id` in one `AND` block yields empty results.
2. **Metadata limitations:** Only top-level keys. Only `eq`, `contains`, `ne`. No `in` or `gt`.
3. **Operator syntax:** Use `gte`, `lt`, `ne`. SQL-style (`>=`, `!=`) rejected.
4. **Entity filter required for get-all:** At least one of `user_id`, `agent_id`, `app_id`, or `run_id`.
5. **Wildcard excludes null:** `*` matches only non-null values.
6. **Date format:** ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). Timezone-naive defaults to UTC.
## Response Formats
### Add Response
```json
[
{
"id": "mem_01JF8ZS4Y0R0SPM13R5R6H32CJ",
"event": "ADD",
"data": { "memory": "The user moved to Austin in 2025." }
}
]
```
Event types: `ADD`, `UPDATE`, `DELETE`. A single add can trigger multiple events.
### Search Response
```json
{
"results": [
{
"id": "ea925981-...",
"memory": "Is a vegetarian and allergic to nuts.",
"user_id": "user123",
"categories": ["food", "health"],
"score": 0.89,
"created_at": "2024-07-26T10:29:36.630547-07:00"
}
]
}
```
With `enable_graph=true`, includes additional `relations` array with entity relationships.
+386
View File
@@ -0,0 +1,386 @@
# Mem0 Platform Architecture
How Mem0 processes, stores, and retrieves memories under the hood.
## Table of Contents
- [Core Concept](#core-concept)
- [Memory Processing Pipeline](#memory-processing-pipeline)
- [Retrieval Pipeline](#retrieval-pipeline)
- [Memory Lifecycle](#memory-lifecycle)
- [Memory Object Structure](#memory-object-structure)
- [Scoping & Multi-Tenancy](#scoping--multi-tenancy)
- [Memory Layers](#memory-layers)
- [Performance Characteristics](#performance-characteristics)
---
## Core Concept
Mem0 is a managed memory layer that sits between your AI application and users. Every integration follows the same 3-step loop:
```
User Input → Retrieve relevant memories → Enrich LLM prompt → Generate response → Store new memories
```
Mem0 handles the complexity of extraction, deduplication, conflict resolution, and semantic retrieval so your application only needs to call `search()` and `add()`.
**Dual storage architecture:**
- **Vector store**: Embeddings for semantic similarity search
- **Graph store** (optional): Entity nodes and relationship edges for structured knowledge
---
## Memory Processing Pipeline
### What happens when you call `client.add()`
```
Messages In
│
▼
┌─────────────────────┐
│ 1. EXTRACTION │ LLM analyzes messages, extracts key facts
│ (infer=True) │ If infer=False, stores raw text as-is
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 2. CONFLICT │ Checks existing memories for duplicates
│ RESOLUTION │ Latest truth wins (newer overrides older)
│ │ Only runs when infer=True
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 3. STORAGE │ Generates embeddings → vector store
│ │ Optional: entity extraction → graph store
│ │ Indexes metadata, categories, timestamps
└─────────┬───────────┘
│
▼
Memory Object
(id, memory, categories, structured_attributes)
```
### Processing modes
**Async (default, `async_mode=True`):**
- API returns immediately: `{"status": "PENDING", "event_id": "..."}`
- Processing happens in background
- Use webhooks for completion notifications
- Best for: high-throughput, non-blocking workflows
**Sync (`async_mode=False`):**
- API waits for full processing
- Returns complete memory object with `id`, `event`, `memory`
- Best for: real-time access immediately after add
### Extraction modes
**Inferred (`infer=True`, default):**
- LLM extracts structured facts from conversation
- Conflict resolution deduplicates and resolves contradictions
- Best for: natural conversation → memory
**Raw (`infer=False`):**
- Stores text exactly as provided, no LLM processing
- Skips conflict resolution — same fact can be stored twice
- Only `user` role messages are stored; `assistant` messages ignored
- Best for: bulk imports, pre-structured data, migrations
**Warning:** Don't mix `infer=True` and `infer=False` for the same data — the same fact will be stored twice.
---
## Retrieval Pipeline
### What happens when you call `client.search()`
```
Query In
│
▼
┌─────────────────────┐
│ 1. QUERY EMBEDDING │ Convert query to vector representation
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ 2. VECTOR SEARCH │ Cosine similarity across stored embeddings
│ │ Scoped by filters (user_id, agent_id, etc.)
└─────────┬───────────┘
│
▼ (optional enhancements)
┌─────────────────────┐
│ 3a. KEYWORD SEARCH │ Expands results with specific terms (+10ms)
│ 3b. RERANKING │ Deep semantic reordering (+150-200ms)
│ 3c. FILTER MEMORIES │ Precision filtering, removes low-relevance (+200-300ms)
└─────────┬───────────┘
│
▼ (if enable_graph=True)
┌─────────────────────┐
│ 4. GRAPH LOOKUP │ Finds entity relationships
│ │ Appends relations WITHOUT reranking vector results
└─────────┬───────────┘
│
▼
Results + Relations
```
### Retrieval enhancement combinations
| Configuration | Latency | Best for |
|--------------|---------|----------|
| Base search only | ~100ms | Simple lookups |
| `keyword_search=True` | ~110ms | Entity-heavy queries, broad coverage |
| `rerank=True` | ~250-300ms | User-facing results, top-N precision |
| `keyword_search=True` + `rerank=True` | ~310ms | Balanced (recommended for most apps) |
| `rerank=True` + `filter_memories=True` | ~400-500ms | Safety-critical, production systems |
### Implicit null scoping
When you search with `user_id="alice"` only, Mem0 returns memories where `agent_id`, `app_id`, and `run_id` are all null. This prevents cross-scope leakage by default.
To include memories with non-null fields, use explicit filters:
```python
# Gets memories for alice regardless of agent/app/run
filters={"OR": [{"user_id": "alice"}]}
```
---
## Memory Lifecycle
```
CREATE ──→ ACTIVE ──→ UPDATE ──→ ACTIVE
│ │ │
│ ▼ ▼
│ EXPIRED EXPIRED
│ (still stored, (still stored,
│ not retrieved) not retrieved)
│ │ │
▼ ▼ ▼
DELETE DELETE DELETE
(permanent)
```
### Creation
- Triggered by `client.add(messages, user_id="...")`
- Messages processed through extraction → conflict resolution → storage
- Gets unique UUID, `created_at` timestamp
- Optional: custom `timestamp`, `expiration_date`, `metadata`, `immutable`
### Updates
- `client.update(memory_id, text="...")` replaces text and reindexes
- `client.batch_update([...])` for up to 1000 memories at once
- Immutable memories (`immutable=True`) cannot be updated — must delete and re-add
### Deduplication
- Automatic during `add()` with `infer=True`
- Conflict resolution merges duplicate facts
- Latest truth wins when contradictions detected
- Prevents memory bloat from repeated information
### Expiration
- Optional `expiration_date` parameter (ISO 8601 or `YYYY-MM-DD`)
- After expiration: memory NOT returned in searches but remains in storage
- Useful for time-sensitive info (events, temporary preferences, session state)
### Deletion
- Single: `client.delete(memory_id)` — permanent, no recovery
- Batch: `client.batch_delete([memory_ids])` — up to 1000
- Bulk: `client.delete_all(user_id="alice")` — all memories for entity
- `delete_all()` without filters raises error to prevent accidental data loss
### History tracking
- `client.history(memory_id)` returns version timeline
- Shows all changes: `{previous_value, new_value, action, timestamps}`
- Useful for audit trails and debugging
---
## Memory Object Structure
```json
{
"id": "uuid-string",
"memory": "Extracted memory text",
"user_id": "user-identifier",
"agent_id": null,
"app_id": null,
"run_id": null,
"metadata": { "source": "chat", "priority": "high" },
"categories": ["health", "preferences"],
"created_at": "2025-03-12T12:34:56Z",
"updated_at": "2025-03-12T12:34:56Z",
"expiration_date": null,
"immutable": false,
"structured_attributes": {
"day": 12, "month": 3, "year": 2025,
"hour": 12, "minute": 34,
"day_of_week": "wednesday",
"is_weekend": false,
"quarter": 1, "week_of_year": 11
},
"score": 0.85
}
```
| Field | Type | Description |
|-------|------|-------------|
| `id` | UUID | Unique identifier, used for update/delete |
| `memory` | string | Extracted or stored text content |
| `user_id` | string | Primary entity scope |
| `agent_id` | string | Agent scope |
| `app_id` | string | Application scope |
| `run_id` | string | Session/run scope |
| `metadata` | object | Custom key-value pairs for filtering |
| `categories` | array | Auto-assigned or custom category tags |
| `created_at` | datetime | Creation timestamp |
| `updated_at` | datetime | Last modification timestamp |
| `expiration_date` | datetime | Auto-expiry date (stops retrieval, data persists) |
| `immutable` | boolean | If true, prevents modification |
| `structured_attributes` | object | Temporal breakdown for time-based queries |
| `score` | float | Semantic similarity (search results only, 0-1) |
---
## Scoping & Multi-Tenancy
Mem0 separates memories across four dimensions to prevent data mixing:
| Dimension | Field | Purpose | Example |
|-----------|-------|---------|---------|
| User | `user_id` | Persistent persona or account | `"customer_6412"` |
| Agent | `agent_id` | Distinct agent or tool | `"meal_planner"` |
| App | `app_id` | Product surface or deployment | `"ios_retail_app"` |
| Session | `run_id` | Short-lived flow or thread | `"ticket-9241"` |
### Storage model
Each entity combination creates separate records. A memory with `user_id="alice"` is stored separately from one with `user_id="alice"` + `agent_id="bot"`.
### Critical: cross-entity queries
```python
# This returns NOTHING — user and agent memories are stored separately
filters={"AND": [{"user_id": "alice"}, {"agent_id": "bot"}]}
# Use OR to query multiple scopes
filters={"OR": [{"user_id": "alice"}, {"agent_id": "bot"}]}
# Use wildcard to include any non-null value
filters={"AND": [{"user_id": "*"}]} # All users (excludes null)
```
### Recommended scoping patterns
```python
# User-level: persistent preferences
client.add(messages, user_id="alice")
# Session-level: temporary context
client.add(messages, user_id="alice", run_id="session_123")
# Clean up when done: client.delete_all(run_id="session_123")
# Agent-level: agent-specific knowledge
client.add(messages, agent_id="support_bot", app_id="helpdesk")
# Multi-tenant: full isolation
client.add(messages, user_id="alice", agent_id="bot", app_id="acme_corp", run_id="ticket_42")
```
---
## Memory Layers
Mem0 supports three layers of memory, from shortest to longest lived:
### Conversation memory
- In-flight messages within a single turn
- Tool calls, chain-of-thought reasoning
- **Lifetime:** Single response — lost after turn finishes
- **Managed by:** Your application, not Mem0
### Session memory
- Short-lived facts for current task or channel
- Multi-step flows (onboarding, debugging, support tickets)
- **Lifetime:** Minutes to hours
- **Managed by:** Mem0 via `run_id` parameter
- Clean up with `client.delete_all(run_id="session_id")`
### User memory
- Long-lived knowledge tied to a person or account
- Personal preferences, account state, compliance details
- **Lifetime:** Weeks to forever
- **Managed by:** Mem0 via `user_id` parameter
- Persists across all sessions and interactions
### How layering works in practice
```python
def chat(user_input: str, user_id: str, session_id: str) -> str:
# 1. Retrieve user memories (long-term preferences)
user_mems = mem0.search(user_input, user_id=user_id)
# 2. Retrieve session memories (current task context)
session_mems = mem0.search(user_input, filters={
"AND": [{"user_id": user_id}, {"run_id": session_id}]
})
# 3. Combine both layers for LLM context
context = format_memories(user_mems) + format_memories(session_mems)
# 4. Generate response
response = llm.generate(context=context, input=user_input)
# 5. Store in session scope (temporary) + user scope (persistent)
messages = [{"role": "user", "content": user_input}, {"role": "assistant", "content": response}]
mem0.add(messages, user_id=user_id, run_id=session_id)
return response
```
---
## Performance Characteristics
### Latency
| Operation | Typical Latency |
|-----------|----------------|
| Base vector search | ~100ms |
| + keyword_search | +10ms |
| + reranking | +150-200ms |
| + filter_memories | +200-300ms |
| Add (async, default) | < 50ms response, background processing |
| Add (sync) | 500ms-2s depending on extraction complexity |
| Graph operations | Slight overhead for large stores |
### Processing
- **Async mode (default):** Returns immediately, processes in background
- **Sync mode:** Waits for full extraction + storage pipeline
- **Batch operations:** Up to 1000 memories per batch_update/batch_delete
- **Webhooks:** Real-time notifications when async processing completes
### Scoping strategy for performance
- Use `user_id` for all user-facing queries (most common, fastest)
- Add `run_id` for session isolation (narrows search space)
- Avoid wildcard `"*"` filters on large datasets (scans all non-null records)
- Use `top_k` to limit result count when you only need a few memories
---
## Comparison with Alternatives
| Approach | Pros | Cons |
|----------|------|------|
| **Raw vector DB** | Fast, full control | No extraction, no dedup, no conflict resolution |
| **In-memory chat history** | Zero latency | Lost on restart, no cross-session, grows unbounded |
| **RAG over documents** | Good for static knowledge | No personalization, no memory updates |
| **Mem0 Platform** | Managed extraction + dedup + graph + scoping | External dependency, async processing delay |
Mem0 combines the best of vector search (semantic retrieval) with automatic extraction (LLM-powered), conflict resolution (deduplication), and structured scoping (multi-tenancy) — in a single managed API.
+496
View File
@@ -0,0 +1,496 @@
# Platform Features -- Mem0 Platform
Additional platform capabilities beyond core CRUD operations.
## Table of Contents
- [Advanced Retrieval](#advanced-retrieval)
- [Graph Memory](#graph-memory)
- [Custom Categories](#custom-categories)
- [Custom Instructions](#custom-instructions)
- [Criteria Retrieval](#criteria-retrieval)
- [Feedback Mechanism](#feedback-mechanism)
- [Memory Export](#memory-export)
- [Group Chat](#group-chat)
- [MCP Integration](#mcp-integration)
- [Webhooks](#webhooks)
- [Multimodal Support](#multimodal-support)
## Advanced Retrieval
Three enhancement options for tuning search precision, recall, and latency.
### Keyword Search (`keyword_search=True`)
Expands results to include memories with specific terms, names, and technical keywords.
- Latency: +10ms
- Recall: Significantly increased
- Best for: entity-heavy queries, comprehensive coverage
### Reranking (`rerank=True`)
Deep semantic reordering of results — most relevant first.
- Latency: +150-200ms
- Accuracy: Significantly improved
- Best for: user-facing results, top-N precision
### Filter Memories (`filter_memories=True`)
Precision filtering — removes low-relevance results entirely.
- Latency: +200-300ms
- Precision: Maximized
- Best for: safety-critical applications, production systems
### Recommended Combinations
**Python:**
```python
# Fast & broad
results = client.search(query, keyword_search=True, user_id="user123")
# Balanced (recommended for most apps)
results = client.search(query, keyword_search=True, rerank=True, user_id="user123")
# High precision (critical apps)
results = client.search(query, rerank=True, filter_memories=True, user_id="user123")
```
**TypeScript:**
```typescript
const results = await client.search(query, {
user_id: 'user123',
keyword_search: true,
rerank: true,
});
```
---
## Graph Memory
Entity-level knowledge graph that creates relationships between memories.
### How It Works
1. **Extraction**: LLM analyzes conversation and identifies entities and relationships
2. **Storage**: Embeddings go to vector store; entity nodes and edges go to graph store
3. **Retrieval**: Vector search returns semantic matches; graph relations are appended to results
Graph relations **augment** vector results without reordering them. Vector similarity always determines hit sequence.
### Enabling Graph Memory
**Per request:**
```python
client.add(messages, user_id="alice", enable_graph=True)
client.search("query", user_id="alice", enable_graph=True)
client.get_all(filters={"AND": [{"user_id": "alice"}]}, enable_graph=True)
```
**Project-level (default for all operations):**
```python
client.project.update(enable_graph=True)
```
```javascript
await client.updateProject({ enable_graph: true });
```
### Relation Structure
Each relation in the response contains:
| Field | Type | Description |
|-------|------|-------------|
| `source` | string | Source entity name |
| `source_type` | string | Source entity type (e.g., "Person") |
| `relationship` | string | Relationship label (e.g., "lives_in") |
| `target` | string | Target entity name |
| `target_type` | string | Target entity type (e.g., "City") |
| `score` | number | Confidence score |
**Example:**
```json
{
"relations": [
{
"source": "Joseph",
"source_type": "Person",
"relationship": "lives_in",
"target": "Seattle",
"target_type": "City",
"score": 0.92
}
]
}
```
### Technical Notes
- Graph Memory adds processing time; see docs for current plan availability
- Works optimally with rich conversation histories containing entity relationships
- Best suited for long-running assistants tracking evolving information
- Graph writes and reads toggle independently per request
- Multi-agent context supported via `user_id`, `agent_id`, `run_id` scoping
- Add operations are asynchronous; graph metadata may not be immediately available
---
## Custom Categories
Replace Mem0's default 15 labels with domain-specific categories. The system automatically tags memories to the closest matching category.
### Default Categories (15)
`personal_details`, `family`, `professional_details`, `sports`, `travel`, `food`, `music`, `health`, `technology`, `hobbies`, `fashion`, `entertainment`, `milestones`, `user_preferences`, `misc`
### Configuration
**Set project-level categories:**
```python
new_categories = [
{"lifestyle_management": "Tracks daily routines, habits, wellness activities"},
{"seeking_structure": "Documents goals around creating routines and systems"},
{"personal_information": "Basic information about the user"}
]
client.project.update(custom_categories=new_categories)
```
```javascript
await client.updateProject({ custom_categories: new_categories });
```
**Retrieve active categories:**
```python
categories = client.project.get(fields=["custom_categories"])
```
### Key Constraint
Per-request overrides (`custom_categories=...` on `client.add`) are **not supported** on the managed API. Only project-level configuration works. Workaround: store ad-hoc labels in `metadata` field.
---
## Custom Instructions
Natural language filters that control what information Mem0 extracts when creating memories.
### Set Instructions
```python
client.project.update(custom_instructions="Your guidelines here...")
```
```javascript
await client.updateProject({ custom_instructions: "Your guidelines here..." });
```
### Template Structure
1. **Task Description** -- brief extraction overview
2. **Information Categories** -- numbered sections with specific details to capture
3. **Processing Guidelines** -- quality and handling rules
4. **Exclusion List** -- sensitive/irrelevant data to filter out
### Domain Examples
**E-commerce:** Capture product issues, preferences, service experience; exclude payment data.
**Education:** Extract learning progress, student preferences, performance patterns; exclude specific grades.
**Finance:** Track financial goals, life events, investment interests; exclude account numbers and SSNs.
### Best Practices
- Start simply, test with sample messages, iterate based on results
- Avoid overly lengthy instructions
- Be specific about what to include AND exclude
---
## Criteria Retrieval
Custom attribute-based memory ranking using LLM-evaluated criteria with weights. Goes beyond semantic similarity to prioritize memories based on domain-specific signals.
### Configuration
```python
# Define criteria at project level
retrieval_criteria = [
{"name": "joy", "description": "Positive emotions like happiness and excitement", "weight": 3},
{"name": "curiosity", "description": "Inquisitiveness and desire to learn", "weight": 2},
{"name": "urgency", "description": "Time-sensitive or high-priority items", "weight": 4},
]
client.project.update(retrieval_criteria=retrieval_criteria)
```
```typescript
await client.updateProject({
retrieval_criteria: [
{ name: 'joy', description: 'Positive emotions', weight: 3 },
{ name: 'urgency', description: 'Time-sensitive items', weight: 4 },
],
});
```
### Usage
Once configured, `client.search()` automatically applies criteria ranking:
```python
# Criteria-weighted results returned automatically
results = client.search("Why am I feeling happy?", filters={"user_id": "alice"})
```
**Best for:** Wellness assistants, tutoring platforms, productivity tools — any app needing intent-aware retrieval.
---
## Feedback Mechanism
Provide feedback on extracted memories to improve system quality over time.
### Feedback Types
| Type | Meaning |
|------|---------|
| `POSITIVE` | Memory is useful and accurate |
| `NEGATIVE` | Memory is not useful |
| `VERY_NEGATIVE` | Memory is harmful or completely wrong |
| `None` | Clear existing feedback |
### Usage
**Python:**
```python
client.feedback(
memory_id="mem-123",
feedback="POSITIVE",
feedback_reason="Accurately captured dietary preference"
)
# Bulk feedback
for item in feedback_data:
client.feedback(**item)
```
**TypeScript:**
```typescript
await client.feedback('mem-123', {
feedback: 'POSITIVE',
feedback_reason: 'Accurately captured dietary preference',
});
```
---
## Memory Export
Create structured exports of memories using customizable schemas with filters.
### Usage
```python
import json
# Define export schema
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"preferences": {"type": "array", "items": {"type": "string"}},
"health_info": {"type": "string"},
}
}
# Create export
response = client.create_memory_export(
schema=json.dumps(schema),
filters={"user_id": "alice"},
export_instructions="Create comprehensive profile based on all memories"
)
# Retrieve export (may take a moment to process)
result = client.get_memory_export(memory_export_id=response["id"])
```
**Best for:** Data analytics, user profile generation, compliance audits, CRM sync.
---
## Group Chat
Process multi-participant conversations and automatically attribute memories to individual speakers.
### Usage
```python
messages = [
{"role": "user", "name": "Alice", "content": "I think we should use React for the frontend"},
{"role": "user", "name": "Bob", "content": "I prefer Vue.js, it's simpler for our use case"},
{"role": "assistant", "content": "Both are great choices. Let me note your preferences."},
]
# Mem0 automatically attributes memories to each speaker
response = client.add(messages, run_id="team_meeting_1")
# Retrieve Alice's memories from that session
alice_mems = client.get_all(
filters={"AND": [{"user_id": "alice"}, {"run_id": "team_meeting_1"}]}
)
```
Use the `name` field in messages to identify speakers. Mem0 maps names to entity scopes automatically.
---
## MCP Integration
Model Context Protocol integration enables AI clients (Claude Desktop, Cursor, custom agents) to manage Mem0 memory autonomously.
### Configuration
```json
{
"mcpServers": {
"mem0": {
"command": "uvx",
"args": ["mem0-mcp-server"],
"env": {
"MEM0_API_KEY": "m0-your-api-key",
"MEM0_DEFAULT_USER_ID": "your-user-id"
}
}
}
}
```
### Available MCP Tools
The MCP server exposes 9 memory tools that AI agents can use autonomously:
- Add, search, get, update, delete memories
- Get history, list users, delete users
- Search Mem0 documentation
### How It Works
1. Configure the MCP server in your AI client
2. The agent autonomously decides when to store/retrieve memories
3. No manual API calls needed — the agent manages memory as part of its reasoning
**Best for:** Universal AI client integration — one protocol works everywhere.
---
## Webhooks
Real-time event notifications for memory operations.
### Supported Events
| Event | Trigger |
|-------|---------|
| `memory_add` | Memory created |
| `memory_update` | Memory modified |
| `memory_delete` | Memory removed |
| `memory_categorize` | Memory tagged |
### Create Webhook
Note: `project_id` here refers to the Mem0 dashboard project scope for webhooks — not the deprecated client init parameter.
```python
webhook = client.create_webhook(
url="https://your-app.com/webhook",
name="Memory Logger",
project_id="proj_123",
event_types=["memory_add", "memory_categorize"]
)
```
### Manage Webhooks
```python
# Retrieve
webhooks = client.get_webhooks(project_id="proj_123")
# Update
client.update_webhook(
name="Updated Logger",
url="https://your-app.com/new-webhook",
event_types=["memory_update", "memory_add"],
webhook_id="wh_123"
)
# Delete
client.delete_webhook(webhook_id="wh_123")
```
### Payload Structure
Memory events contain: ID, data object with memory content, event type (`ADD`/`UPDATE`/`DELETE`).
Categorization events contain: memory ID, event type (`CATEGORIZE`), assigned category labels.
---
## Multimodal Support
Mem0 can process images and documents alongside text.
### Supported Media Types
- Images: JPG, PNG
- Documents: MDX, TXT, PDF
### Image via URL
```python
image_message = {
"role": "user",
"content": {
"type": "image_url",
"image_url": {"url": "https://example.com/image.jpg"}
}
}
client.add([image_message], user_id="alice")
```
### Image via Base64
```python
import base64
with open("photo.jpg", "rb") as f:
base64_image = base64.b64encode(f.read()).decode("utf-8")
image_message = {
"role": "user",
"content": {
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}
}
}
client.add([image_message], user_id="alice")
```
### Document (MDX/TXT)
```python
doc_message = {
"role": "user",
"content": {"type": "mdx_url", "mdx_url": {"url": document_url}}
}
client.add([doc_message], user_id="alice")
```
### PDF Document
```python
pdf_message = {
"role": "user",
"content": {"type": "pdf_url", "pdf_url": {"url": pdf_url}}
}
client.add([pdf_message], user_id="alice")
```
@@ -0,0 +1,444 @@
# Mem0 Integration Patterns
Working code examples for integrating Mem0 Platform with popular AI frameworks.
All examples use `MemoryClient` (Platform API key).
Code examples are sourced from official Mem0 integration docs at docs.mem0.ai, simplified for quick reference.
---
## Common Pattern
Every integration follows the same 3-step loop:
1. **Retrieve** -- search relevant memories before generating a response
2. **Generate** -- include memories as context in the LLM prompt
3. **Store** -- save the interaction back to Mem0 for future use
---
## LangChain
Source: [docs.mem0.ai/integrations/langchain](https://docs.mem0.ai/integrations/langchain)
```python
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from mem0 import MemoryClient
llm = ChatOpenAI(model="gpt-4.1-nano-2025-04-14")
mem0 = MemoryClient()
prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="You are a helpful travel agent AI. Use the provided context to personalize your responses."),
MessagesPlaceholder(variable_name="context"),
HumanMessage(content="{input}")
])
def retrieve_context(query: str, user_id: str):
"""Retrieve relevant memories from Mem0"""
memories = mem0.search(query, user_id=user_id)
memory_list = memories['results']
serialized = ' '.join([m["memory"] for m in memory_list])
return [
{"role": "system", "content": f"Relevant information: {serialized}"},
{"role": "user", "content": query}
]
def chat_turn(user_input: str, user_id: str) -> str:
# 1. Retrieve
context = retrieve_context(user_input, user_id)
# 2. Generate
chain = prompt | llm
response = chain.invoke({"context": context, "input": user_input})
# 3. Store
mem0.add(
[{"role": "user", "content": user_input}, {"role": "assistant", "content": response.content}],
user_id=user_id
)
return response.content
```
---
## CrewAI
Source: [docs.mem0.ai/integrations/crewai](https://docs.mem0.ai/integrations/crewai)
CrewAI has native Mem0 integration via `memory_config`:
```python
from crewai import Agent, Task, Crew, Process
from mem0 import MemoryClient
client = MemoryClient()
# Store user preferences first
messages = [
{"role": "user", "content": "I am more of a beach person than a mountain person."},
{"role": "assistant", "content": "Noted! I'll recommend beach destinations."},
{"role": "user", "content": "I like Airbnb more than hotels."},
]
client.add(messages, user_id="crew_user_1")
# Create agent
travel_agent = Agent(
role="Personalized Travel Planner",
goal="Plan personalized travel itineraries",
backstory="You are a seasoned travel planner.",
memory=True,
)
# Create task
task = Task(
description="Find places to live, eat, and visit in San Francisco.",
expected_output="A detailed list of places to live, eat, and visit.",
agent=travel_agent,
)
# Setup crew with Mem0 memory
crew = Crew(
agents=[travel_agent],
tasks=[task],
process=Process.sequential,
memory=True,
memory_config={
"provider": "mem0",
"config": {"user_id": "crew_user_1"},
}
)
result = crew.kickoff()
```
---
## Vercel AI SDK
Source: [docs.mem0.ai/integrations/vercel-ai-sdk](https://docs.mem0.ai/integrations/vercel-ai-sdk)
Install: `npm install @mem0/vercel-ai-provider`
### Basic Text Generation with Memory
```typescript
import { generateText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0({
provider: "openai",
mem0ApiKey: "m0-xxx",
apiKey: "openai-api-key",
});
const { text } = await generateText({
model: mem0("gpt-4-turbo", { user_id: "borat" }),
prompt: "Suggest me a good car to buy!",
});
```
### Streaming with Memory
```typescript
import { streamText } from "ai";
import { createMem0 } from "@mem0/vercel-ai-provider";
const mem0 = createMem0();
const { textStream } = streamText({
model: mem0("gpt-4-turbo", { user_id: "borat" }),
prompt: "Suggest me a good car to buy!",
});
for await (const textPart of textStream) {
process.stdout.write(textPart);
}
```
### Using Memory Utilities Standalone
```typescript
import { openai } from "@ai-sdk/openai";
import { generateText } from "ai";
import { retrieveMemories, addMemories } from "@mem0/vercel-ai-provider";
// Retrieve memories and inject into any provider
const prompt = "Suggest me a good car to buy.";
const memories = await retrieveMemories(prompt, { user_id: "borat", mem0ApiKey: "m0-xxx" });
const { text } = await generateText({
model: openai("gpt-4-turbo"),
prompt: prompt,
system: memories,
});
// Store new memories
await addMemories(
[{ role: "user", content: [{ type: "text", text: "I love red cars." }] }],
{ user_id: "borat", mem0ApiKey: "m0-xxx" }
);
```
### Supported Providers
`openai`, `anthropic`, `google`, `groq`
---
## OpenAI Agents SDK
Source: [docs.mem0.ai/integrations/openai-agents-sdk](https://docs.mem0.ai/integrations/openai-agents-sdk)
```python
from agents import Agent, Runner, function_tool
from mem0 import MemoryClient
mem0 = MemoryClient()
@function_tool
def search_memory(query: str, user_id: str) -> str:
"""Search through past conversations and memories"""
memories = mem0.search(query, user_id=user_id, top_k=3)
if memories and memories.get('results'):
return "\n".join([f"- {mem['memory']}" for mem in memories['results']])
return "No relevant memories found."
@function_tool
def save_memory(content: str, user_id: str) -> str:
"""Save important information to memory"""
mem0.add([{"role": "user", "content": content}], user_id=user_id)
return "Information saved to memory."
agent = Agent(
name="Personal Assistant",
instructions="""You are a helpful personal assistant with memory capabilities.
Use search_memory to recall past conversations.
Use save_memory to store important information.""",
tools=[search_memory, save_memory],
model="gpt-4.1-nano-2025-04-14"
)
result = Runner.run_sync(agent, "I love Italian food and I'm planning a trip to Rome next month")
print(result.final_output)
```
### Multi-Agent with Handoffs
```python
from agents import Agent, Runner, function_tool
travel_agent = Agent(
name="Travel Planner",
instructions="You are a travel planning specialist. Use search_memory and save_memory tools.",
tools=[search_memory, save_memory],
model="gpt-4.1-nano-2025-04-14"
)
health_agent = Agent(
name="Health Advisor",
instructions="You are a health and wellness advisor. Use search_memory and save_memory tools.",
tools=[search_memory, save_memory],
model="gpt-4.1-nano-2025-04-14"
)
triage_agent = Agent(
name="Personal Assistant",
instructions="""Route travel questions to Travel Planner, health questions to Health Advisor.""",
handoffs=[travel_agent, health_agent],
model="gpt-4.1-nano-2025-04-14"
)
result = Runner.run_sync(triage_agent, "Plan a healthy meal for my Italy trip")
```
---
## Pipecat (Voice / Real-Time)
Source: [docs.mem0.ai/integrations/pipecat](https://docs.mem0.ai/integrations/pipecat)
```python
from pipecat.services.mem0 import Mem0MemoryService
memory = Mem0MemoryService(
api_key=os.getenv("MEM0_API_KEY"),
user_id="alice",
agent_id="voice_bot",
params={
"search_limit": 10,
"search_threshold": 0.1,
"system_prompt": "Here are your past memories:",
"add_as_system_message": True,
}
)
# Use in pipeline
pipeline = Pipeline([
transport.input(),
stt,
user_context,
memory, # Memory enhances context automatically
llm,
transport.output(),
assistant_context
])
```
---
## LangGraph
Source: [docs.mem0.ai/integrations/langgraph](https://docs.mem0.ai/integrations/langgraph)
State-based agent workflows with memory persistence. Best for complex conversation flows with branching logic.
```python
from typing import Annotated, TypedDict, List
from langgraph.graph import StateGraph, START
from langgraph.graph.message import add_messages
from langchain_openai import ChatOpenAI
from mem0 import MemoryClient
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
llm = ChatOpenAI(model="gpt-4")
mem0 = MemoryClient()
class State(TypedDict):
messages: Annotated[List[HumanMessage | AIMessage], add_messages]
mem0_user_id: str
def chatbot(state: State):
messages = state["messages"]
user_id = state["mem0_user_id"]
# Retrieve relevant memories
memories = mem0.search(messages[-1].content, user_id=user_id)
context = "Relevant context:\n"
for memory in memories["results"]:
context += f"- {memory['memory']}\n"
system_message = SystemMessage(content=f"""You are a helpful support assistant.
{context}""")
response = llm.invoke([system_message] + messages)
# Store the interaction
mem0.add(
[{"role": "user", "content": messages[-1].content},
{"role": "assistant", "content": response.content}],
user_id=user_id
)
return {"messages": [response]}
graph = StateGraph(State)
graph.add_node("chatbot", chatbot)
graph.add_edge(START, "chatbot")
app = graph.compile()
# Usage
result = app.invoke({
"messages": [HumanMessage(content="I need help with my order")],
"mem0_user_id": "customer_123"
})
```
---
## LlamaIndex
Source: [docs.mem0.ai/integrations/llama-index](https://docs.mem0.ai/integrations/llama-index)
Install: `pip install llama-index-core llama-index-memory-mem0`
LlamaIndex has native Mem0 support via `Mem0Memory`. Works with ReAct and FunctionCalling agents.
```python
from llama_index.memory.mem0 import Mem0Memory
context = {"user_id": "alice", "agent_id": "llama_agent_1"}
memory = Mem0Memory.from_client(
context=context,
search_msg_limit=4, # messages from chat history used for retrieval (default: 5)
)
# Use with LlamaIndex agent
from llama_index.core.agent import FunctionCallingAgent
from llama_index.llms.openai import OpenAI
llm = OpenAI(model="gpt-4")
agent = FunctionCallingAgent.from_tools(
tools=[],
llm=llm,
memory=memory,
verbose=True,
)
response = agent.chat("I prefer vegetarian restaurants")
# Memory automatically stores and retrieves context
response = agent.chat("What kind of food do I like?")
# Agent retrieves the vegetarian preference from Mem0
```
---
## AutoGen
Source: [docs.mem0.ai/integrations/autogen](https://docs.mem0.ai/integrations/autogen)
Install: `pip install autogen mem0ai`
Multi-agent conversational systems with memory persistence.
```python
from autogen import ConversableAgent
from mem0 import MemoryClient
memory_client = MemoryClient()
USER_ID = "alice"
agent = ConversableAgent(
"chatbot",
llm_config={"config_list": [{"model": "gpt-4", "api_key": os.environ["OPENAI_API_KEY"]}]},
code_execution_config=False,
human_input_mode="NEVER",
)
def get_context_aware_response(question: str) -> str:
# Retrieve memories for context
relevant_memories = memory_client.search(question, user_id=USER_ID)
context = "\n".join([m["memory"] for m in relevant_memories.get("results", [])])
prompt = f"""Answer considering previous interactions:
Previous context: {context}
Question: {question}"""
reply = agent.generate_reply(messages=[{"content": prompt, "role": "user"}])
# Store the new interaction
memory_client.add(
[{"role": "user", "content": question}, {"role": "assistant", "content": reply}],
user_id=USER_ID
)
return reply
```
---
## All Supported Frameworks
Beyond the examples above, Mem0 integrates with:
| Framework | Type | Install |
|-----------|------|---------|
| [Mastra](https://docs.mem0.ai/integrations/mastra) | TS agent framework | `npm install @mastra/mem0` |
| [ElevenLabs](https://docs.mem0.ai/integrations/elevenlabs) | Voice AI | `pip install elevenlabs mem0ai` |
| [LiveKit](https://docs.mem0.ai/integrations/livekit) | Real-time voice/video | `pip install livekit-agents mem0ai` |
| [Camel AI](https://docs.mem0.ai/integrations/camel-ai) | Multi-agent framework | `pip install camel-ai[all] mem0ai` |
| [AWS Bedrock](https://docs.mem0.ai/integrations/aws-bedrock) | Cloud LLM provider | `pip install boto3 mem0ai` |
| [Dify](https://docs.mem0.ai/integrations/dify) | Low-code AI platform | Plugin-based |
| [Google AI ADK](https://docs.mem0.ai/integrations/google-ai-adk) | Google agent framework | `pip install google-adk mem0ai` |
For the general Python pattern (no framework), see the "Common integration pattern" in [SKILL.md](../SKILL.md).
+119
View File
@@ -0,0 +1,119 @@
# Mem0 Platform Quickstart
Get running with Mem0 in 2 minutes. No infrastructure to deploy -- just an API key.
## Prerequisites
- Python 3.10+ or Node.js 18+
- A Mem0 Platform API key ([Get one here](https://app.mem0.ai/dashboard/api-keys))
## Python Setup
```bash
pip install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="your-api-key")
# Add a memory
messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember your dietary preferences."}
]
client.add(messages, user_id="user123")
# Search memories
results = client.search("What are my dietary restrictions?", user_id="user123")
print(results)
```
### Async Client
```python
from mem0 import AsyncMemoryClient
client = AsyncMemoryClient(api_key="your-api-key")
await client.add(messages, user_id="user123")
results = await client.search("query", user_id="user123")
```
## TypeScript / JavaScript Setup
```bash
npm install mem0ai
export MEM0_API_KEY="m0-your-api-key"
```
```javascript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: 'your-api-key' });
// Add a memory
const messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember your dietary preferences."}
];
await client.add(messages, { user_id: "user123" });
// Search memories
const results = await client.search("What are my dietary restrictions?", {
user_id: "user123"
});
console.log(results);
```
## cURL
```bash
export MEM0_API_KEY="m0-your-api-key"
# Add memory
curl -X POST https://api.mem0.ai/v1/memories/ \
-H "Authorization: Token $MEM0_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{"role": "user", "content": "I am a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I will remember your dietary preferences."}
],
"user_id": "user123"
}'
# Search memories
curl -X POST https://api.mem0.ai/v2/memories/search/ \
-H "Authorization: Token $MEM0_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "What are my dietary restrictions?",
"filters": {"user_id": "user123"}
}'
```
## Sample Response
```json
{
"results": [
{
"id": "14e1b28a-2014-40ad-ac42-69c9ef42193d",
"memory": "Allergic to nuts",
"user_id": "user123",
"categories": ["health"],
"created_at": "2025-10-22T04:40:22.864647-07:00",
"score": 0.30
}
]
}
```
## Next Steps
- [SDK Guide](sdk-guide.md) -- all methods for Python and TypeScript
- [API Reference](api-reference.md) -- REST endpoints and memory object structure
- [Integration Patterns](integration-patterns.md) -- LangChain, CrewAI, Vercel AI, etc.
+308
View File
@@ -0,0 +1,308 @@
# Mem0 SDK Guide
Complete SDK reference for Python and TypeScript. All methods use `MemoryClient` (Platform API).
## Initialization
**Python:**
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="m0-your-api-key")
```
**Python (Async):**
```python
from mem0 import AsyncMemoryClient
client = AsyncMemoryClient(api_key="m0-your-api-key")
```
**TypeScript:**
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: 'm0-your-api-key' });
```
Constructor accepts `apiKey` (required) and `host` (optional, default: `https://api.mem0.ai`).
---
## add() -- Store Memories
**Python:**
```python
messages = [
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
{"role": "assistant", "content": "Got it! I'll remember that."}
]
client.add(messages, user_id="alice")
# With metadata
client.add(messages, user_id="alice", metadata={"source": "onboarding"})
# With graph memory
client.add(messages, user_id="alice", enable_graph=True)
```
**TypeScript:**
```typescript
await client.add(messages, { user_id: "alice" });
await client.add(messages, { user_id: "alice", metadata: { source: "onboarding" } });
await client.add(messages, { user_id: "alice", enable_graph: true });
```
### Parameters
| Name | Type | Description |
|------|------|-------------|
| `messages` | array | `[{"role": "user", "content": "..."}]` |
| `user_id` | string | User identifier (recommended) |
| `agent_id` | string | Agent identifier |
| `run_id` | string | Session identifier |
| `metadata` | object | Custom key-value pairs |
| `enable_graph` | boolean | Activate knowledge graph |
| `infer` | boolean | If `false`, store raw text without inference (default: `true`) |
| `immutable` | boolean | Prevents modification after creation |
| `expiration_date` | string | Auto-expiry date (`YYYY-MM-DD`) |
| `includes` | string | Preference filters for inclusion |
| `excludes` | string | Preference filters for exclusion |
| `async_mode` | boolean | Async processing (default: `true`). Set `false` to wait |
### Advanced Add Options
```python
# Immutable -- cannot be modified or overwritten
client.add(messages, user_id="alice", immutable=True)
# Expiring memory
client.add(messages, user_id="alice", expiration_date="2025-12-31")
# Selective extraction
client.add(messages, user_id="alice", includes="dietary preferences", excludes="payment info")
# Agent + session scoping
client.add(messages, user_id="alice", agent_id="nutrition-agent", run_id="session-456")
# Synchronous processing (wait for completion)
client.add(messages, user_id="alice", async_mode=False)
# Raw text -- skip LLM inference
client.add(
[{"role": "user", "content": "User prefers dark mode."}],
user_id="alice",
infer=False,
)
```
---
## search() -- Find Memories
**Python:**
```python
results = client.search("dietary preferences?", user_id="alice")
# With filters and reranking
results = client.search(
query="work experience",
filters={"AND": [{"user_id": "alice"}, {"categories": {"contains": "professional_details"}}]},
top_k=5,
rerank=True,
threshold=0.5
)
# With graph relations
results = client.search("colleagues", user_id="alice", enable_graph=True)
# Keyword search
results = client.search("vegetarian", user_id="alice", keyword_search=True)
```
**TypeScript:**
```typescript
const results = await client.search("dietary preferences", { user_id: "alice" });
const results = await client.search("work experience", {
filters: { AND: [{ user_id: "alice" }, { categories: { contains: "professional_details" } }] },
top_k: 5,
rerank: true,
});
```
### Parameters
| Name | Type | Description |
|------|------|-------------|
| `query` | string | Natural language search query |
| `user_id` | string | Filter by user |
| `filters` | object | V2 filter object (AND/OR operators) |
| `top_k` | number | Number of results (default: 10) |
| `rerank` | boolean | Enable reranking for better relevance |
| `threshold` | number | Minimum similarity score (default: 0.3) |
| `keyword_search` | boolean | Use keyword-based search |
| `enable_graph` | boolean | Include graph relations |
### Common Filter Patterns
```python
# Single user (shorthand)
client.search("query", user_id="alice")
# OR across agents
filters={"OR": [{"user_id": "alice"}, {"agent_id": {"in": ["travel-agent", "sports-agent"]}}]}
# Category filtering (partial match)
filters={"AND": [{"user_id": "alice"}, {"categories": {"contains": "finance"}}]}
# Category filtering (exact match)
filters={"AND": [{"user_id": "alice"}, {"categories": {"in": ["personal_information"]}}]}
# Wildcard (match any non-null run)
filters={"AND": [{"user_id": "alice"}, {"run_id": "*"}]}
# Date range
filters={"AND": [
{"user_id": "alice"},
{"created_at": {"gte": "2024-01-01T00:00:00Z"}},
{"created_at": {"lt": "2024-02-01T00:00:00Z"}}
]}
# Exclude categories with NOT
filters={"AND": [{"user_id": "user_123"}, {"NOT": {"categories": {"in": ["spam", "test"]}}}]}
# Multi-dimensional query
filters={"AND": [
{"user_id": "user_123"},
{"keywords": {"icontains": "invoice"}},
{"categories": {"in": ["finance"]}},
{"created_at": {"gte": "2024-01-01T00:00:00Z"}}
]}
```
---
## get() / getAll() -- Retrieve Memories
**Python:**
```python
# Single memory by ID
memory = client.get(memory_id="ea925981-...")
# All memories for a user
memories = client.get_all(filters={"AND": [{"user_id": "alice"}]})
# With date range
memories = client.get_all(
filters={"AND": [
{"user_id": "alex"},
{"created_at": {"gte": "2024-07-01", "lte": "2024-07-31"}}
]}
)
# With graph data
memories = client.get_all(filters={"AND": [{"user_id": "alice"}]}, enable_graph=True)
```
**TypeScript:**
```typescript
const memory = await client.get("ea925981-...");
const memories = await client.getAll({ filters: { AND: [{ user_id: "alice" }] } });
```
**Note:** `get_all` requires at least one of `user_id`, `agent_id`, `app_id`, or `run_id` in filters.
---
## update() -- Modify Memories
**Python:**
```python
client.update(memory_id="ea925981-...", text="Updated: vegan since 2024")
client.update(memory_id="ea925981-...", text="Updated", metadata={"verified": True})
```
**TypeScript:**
```typescript
await client.update("ea925981-...", { text: "Updated: vegan since 2024" });
```
Cannot update immutable memories.
---
## delete() / deleteAll() -- Remove Memories
**Python:**
```python
client.delete(memory_id="ea925981-...")
client.delete_all(user_id="alice") # Irreversible bulk delete
```
**TypeScript:**
```typescript
await client.delete("ea925981-...");
await client.deleteAll({ user_id: "alice" });
```
---
## history() -- Track Changes
**Python:**
```python
history = client.history(memory_id="ea925981-...")
# Returns: [{previous_value, new_value, action, timestamps}]
```
**TypeScript:**
```typescript
const history = await client.history("ea925981-...");
```
---
## Batch Operations (TypeScript)
```typescript
// Batch update
await client.batchUpdate([
{ memoryId: "uuid-1", text: "Updated text" },
{ memoryId: "uuid-2", text: "Another updated text" },
]);
// Batch delete
await client.batchDelete(["uuid-1", "uuid-2", "uuid-3"]);
```
---
## Additional Methods
```python
# List all users/agents/sessions with memories
users = client.users()
# Delete a user/agent entity
client.delete_users(user_id="alice")
# Submit feedback on a memory
client.feedback(memory_id="...", feedback="POSITIVE", feedback_reason="Accurate extraction")
# Export memories
export = client.create_memory_export(filters={"AND": [{"user_id": "alice"}]})
data = client.get_memory_export(memory_export_id=export["id"])
```
---
## Common Pitfalls
1. **Entity cross-filtering fails silently** -- `AND` with `user_id` + `agent_id` returns empty. Use `OR`.
2. **SQL operators rejected** -- use `gte`, `lt`, etc. Not `>=`, `<`.
3. **Metadata filtering is limited** -- only top-level keys with `eq`, `contains`, `ne`.
4. **Wildcard `*` excludes null** -- only matches non-null values.
5. **Default threshold is 0.3** -- increase for stricter matching.
6. **Async processing** -- memories process asynchronously. Wait 2-3s after `add()` before searching.
7. **Immutable memories** -- cannot be updated or deleted once created.
## Naming Conventions
Python uses `snake_case` (`user_id`, `memory_id`, `get_all`). TypeScript uses `camelCase` for methods (`getAll`, `deleteAll`, `batchUpdate`) but `snake_case` for API parameters (`user_id`, `agent_id`).
+720
View File
@@ -0,0 +1,720 @@
# Mem0 Use Cases & Examples
Real-world implementation patterns for Mem0 Platform. Each use case includes complete, runnable code in both Python and TypeScript.
## Table of Contents
- [Personalized AI Companion](#1-personalized-ai-companion)
- [Customer Support with Categories](#2-customer-support-with-categories)
- [Healthcare Coach](#3-healthcare-coach)
- [Content Creation Workflow](#4-content-creation-workflow)
- [Multi-Agent / Multi-Tenant](#5-multi-agent--multi-tenant)
- [Personalized Search](#6-personalized-search)
- [Email Intelligence](#7-email-intelligence)
- [Common Patterns Across Use Cases](#common-patterns-across-use-cases)
---
## 1. Personalized AI Companion
A fitness coach that remembers goals, preferences, and progress across sessions. Mem0 persists context across app restarts — no session state needed.
### Implementation (Python)
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai_client = OpenAI()
def chat(user_input: str, user_id: str) -> str:
# 1. Retrieve relevant memories
memories = mem0.search(user_input, user_id=user_id)
context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
# 2. Generate response with memory context
system_prompt = f"""You are Ray, a personal fitness coach.
Use these known facts about the user to personalize your response:
{context if context else 'No prior context yet.'}"""
response = openai_client.chat.completions.create(
model="gpt-4.1-nano-2025-04-14",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_input},
]
)
reply = response.choices[0].message.content
# 3. Store interaction for future context
mem0.add(
[{"role": "user", "content": user_input}, {"role": "assistant", "content": reply}],
user_id=user_id
)
return reply
# Usage
chat("I want to run a marathon in under 4 hours", user_id="max")
# Next day, app restarted:
chat("What should I focus on today?", user_id="max")
# Ray remembers the sub-4 marathon goal
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
import OpenAI from 'openai';
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
const openai = new OpenAI();
async function chat(userInput: string, userId: string): Promise<string> {
// 1. Retrieve relevant memories
const memories = await mem0.search(userInput, { user_id: userId });
const context = memories.results
?.map((m: any) => `- ${m.memory}`)
.join('\n') || 'No prior context yet.';
// 2. Generate response with memory context
const response = await openai.chat.completions.create({
model: 'gpt-4.1-nano-2025-04-14',
messages: [
{ role: 'system', content: `You are Ray, a personal fitness coach.\nUser context:\n${context}` },
{ role: 'user', content: userInput },
],
});
const reply = response.choices[0].message.content!;
// 3. Store interaction
await mem0.add(
[{ role: 'user', content: userInput }, { role: 'assistant', content: reply }],
{ user_id: userId }
);
return reply;
}
```
### Key Benefits
- Context persists across app restarts — no session management needed
- Memories are automatically deduplicated and updated
- Works with any LLM provider (OpenAI, Anthropic, etc.)
**Best for:** Fitness coaches, tutors, therapists — any assistant that needs to remember goals across sessions.
---
## 2. Customer Support with Categories
Auto-categorize support data so teams retrieve the right facts fast. Uses custom categories for structured retrieval.
### Implementation (Python)
```python
from mem0 import MemoryClient
client = MemoryClient()
# 1. Define categories at the project level (one-time setup)
custom_categories = [
{"support_tickets": "Customer issues and resolutions"},
{"account_info": "Account details and preferences"},
{"billing": "Payment history and billing questions"},
{"product_feedback": "Feature requests and feedback"},
]
client.project.update(custom_categories=custom_categories)
# 2. Store interactions — auto-classified into categories
def log_support_interaction(user_id: str, message: str, priority: str = "normal"):
client.add(
[{"role": "user", "content": message}],
user_id=user_id,
metadata={"priority": priority, "source": "support_chat"}
)
# 3. Retrieve by category
def get_billing_issues(user_id: str):
return client.get_all(
filters={
"AND": [
{"user_id": user_id},
{"categories": {"in": ["billing"]}}
]
}
)
def search_support_history(user_id: str, query: str):
return client.search(
query,
filters={
"AND": [
{"user_id": user_id},
{"categories": {"contains": "support_tickets"}}
]
},
top_k=5
)
# Usage
log_support_interaction("maria", "I was charged twice for last month's subscription", priority="high")
log_support_interaction("maria", "The dashboard is loading slowly on mobile")
billing = get_billing_issues("maria") # Returns only billing-related memories
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
// Setup categories (one-time)
await client.updateProject({
custom_categories: [
{ support_tickets: 'Customer issues and resolutions' },
{ billing: 'Payment history and billing questions' },
{ product_feedback: 'Feature requests and feedback' },
],
});
async function logInteraction(userId: string, message: string, priority = 'normal') {
await client.add(
[{ role: 'user', content: message }],
{ user_id: userId, metadata: { priority, source: 'support_chat' } }
);
}
async function getBillingIssues(userId: string) {
return client.getAll({
filters: { AND: [{ user_id: userId }, { categories: { in: ['billing'] } }] },
});
}
```
### Key Benefits
- Automatic categorization — no manual tagging
- Filter by category for structured retrieval
- Metadata (`priority`, `source`) enables multi-dimensional queries
**Best for:** Help desks, SaaS support, e-commerce — structured retrieval by category eliminates manual scanning.
---
## 3. Healthcare Coach
Guide patients with an assistant that remembers medical history. Uses high `threshold` for confident retrieval in safety-critical contexts.
### Implementation (Python)
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai_client = OpenAI()
def save_patient_info(user_id: str, information: str):
mem0.add(
[{"role": "user", "content": information}],
user_id=user_id,
run_id="healthcare_session",
metadata={"type": "patient_information"}
)
def consult(user_id: str, question: str) -> str:
# High threshold for medical accuracy
memories = mem0.search(question, user_id=user_id, top_k=5, threshold=0.7)
context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
response = openai_client.chat.completions.create(
model="gpt-4.1-nano-2025-04-14",
messages=[
{"role": "system", "content": f"You are a health coach. Patient context:\n{context}"},
{"role": "user", "content": question},
]
)
reply = response.choices[0].message.content
# Store the interaction
mem0.add(
[{"role": "user", "content": question}, {"role": "assistant", "content": reply}],
user_id=user_id,
run_id="healthcare_session",
)
return reply
# Usage
save_patient_info("alex", "I'm allergic to penicillin and take metformin for type 2 diabetes")
consult("alex", "Can I take amoxicillin for my sore throat?")
# Remembers penicillin allergy — amoxicillin is a penicillin-type antibiotic
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
import OpenAI from 'openai';
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
const openai = new OpenAI();
async function savePatientInfo(userId: string, info: string) {
await mem0.add(
[{ role: 'user', content: info }],
{ user_id: userId, run_id: 'healthcare_session', metadata: { type: 'patient_information' } }
);
}
async function consult(userId: string, question: string): Promise<string> {
const memories = await mem0.search(question, {
user_id: userId,
top_k: 5,
threshold: 0.7,
});
const context = memories.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
const response = await openai.chat.completions.create({
model: 'gpt-4.1-nano-2025-04-14',
messages: [
{ role: 'system', content: `You are a health coach. Patient context:\n${context}` },
{ role: 'user', content: question },
],
});
const reply = response.choices[0].message.content!;
await mem0.add(
[{ role: 'user', content: question }, { role: 'assistant', content: reply }],
{ user_id: userId, run_id: 'healthcare_session' }
);
return reply;
}
```
### Key Benefits
- High threshold (0.7) ensures only confident matches for safety-critical retrieval
- Session scoping via `run_id` groups related health interactions
- Metadata tagging separates patient info from conversation history
**Best for:** Telehealth, wellness apps, patient management — persistent health context across visits.
---
## 4. Content Creation Workflow
Store voice guidelines once and apply them across every draft. Uses `run_id` and `metadata` to scope writing preferences per session.
### Implementation (Python)
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai_client = OpenAI()
def store_writing_preferences(user_id: str, preferences: str):
mem0.add(
[{"role": "user", "content": preferences}],
user_id=user_id,
run_id="editing_session",
metadata={"type": "preferences", "category": "writing_style"}
)
def draft_content(user_id: str, topic: str) -> str:
# Retrieve writing preferences
prefs = mem0.search(
"writing style preferences",
filters={"AND": [{"user_id": user_id}, {"run_id": "editing_session"}]}
)
style_context = "\n".join([f"- {m['memory']}" for m in prefs.get("results", [])])
response = openai_client.chat.completions.create(
model="gpt-4.1-nano-2025-04-14",
messages=[
{"role": "system", "content": f"Write content matching these style preferences:\n{style_context}"},
{"role": "user", "content": f"Write a blog post about: {topic}"},
]
)
return response.choices[0].message.content
# Usage
store_writing_preferences("writer_01", "I prefer short sentences. Active voice. No jargon. Use analogies.")
draft_content("writer_01", "Why AI memory matters for chatbots")
# Drafts content matching the stored voice guidelines
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
import OpenAI from 'openai';
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
const openai = new OpenAI();
async function storePreferences(userId: string, preferences: string) {
await mem0.add(
[{ role: 'user', content: preferences }],
{ user_id: userId, run_id: 'editing_session', metadata: { type: 'preferences' } }
);
}
async function draftContent(userId: string, topic: string): Promise<string> {
const prefs = await mem0.search('writing style preferences', {
filters: { AND: [{ user_id: userId }, { run_id: 'editing_session' }] },
});
const styleContext = prefs.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
const response = await openai.chat.completions.create({
model: 'gpt-4.1-nano-2025-04-14',
messages: [
{ role: 'system', content: `Write content matching these preferences:\n${styleContext}` },
{ role: 'user', content: `Write a blog post about: ${topic}` },
],
});
return response.choices[0].message.content!;
}
```
### Key Benefits
- Voice consistency across all content without repeating guidelines
- Scoped sessions let you maintain different style profiles
- Preferences update automatically as you refine them
**Best for:** Marketing teams, technical writers, agencies — consistent voice across all content.
---
## 5. Multi-Agent / Multi-Tenant
Keep memories separate using `user_id`, `agent_id`, `app_id`, and `run_id` scoping. Critical for multi-agent workflows and multi-tenant apps.
### Implementation (Python)
```python
from mem0 import MemoryClient
client = MemoryClient()
# Store memories scoped to user + agent + session
def store_scoped_memory(messages: list, user_id: str, agent_id: str, run_id: str, app_id: str):
client.add(
messages,
user_id=user_id,
agent_id=agent_id,
run_id=run_id,
app_id=app_id
)
# Query within a specific scope
def search_user_session(query: str, user_id: str, app_id: str, run_id: str):
"""Search memories for a specific user within a specific session."""
return client.search(
query,
filters={
"AND": [
{"user_id": user_id},
{"app_id": app_id},
{"run_id": run_id}
]
}
)
def search_agent_knowledge(query: str, agent_id: str, app_id: str):
"""Search all memories an agent has across all users."""
return client.search(
query,
filters={
"AND": [
{"agent_id": agent_id},
{"app_id": app_id}
]
}
)
# Usage: Travel concierge app with multiple agents
store_scoped_memory(
[{"role": "user", "content": "I'm vegetarian and prefer window seats"}],
user_id="traveler_cam",
agent_id="travel_planner",
run_id="tokyo-2025",
app_id="concierge_app"
)
# User-scoped query: "What does Cam prefer?"
user_mems = search_user_session("dietary restrictions?", "traveler_cam", "concierge_app", "tokyo-2025")
# Agent-scoped query: "What do all travelers prefer?" (across users)
agent_mems = search_agent_knowledge("common dietary restrictions?", "travel_planner", "concierge_app")
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
async function storeScopedMemory(
messages: Array<{ role: string; content: string }>,
userId: string, agentId: string, runId: string, appId: string
) {
await client.add(messages, {
user_id: userId,
agent_id: agentId,
run_id: runId,
app_id: appId,
});
}
async function searchUserSession(query: string, userId: string, appId: string, runId: string) {
return client.search(query, {
filters: { AND: [{ user_id: userId }, { app_id: appId }, { run_id: runId }] },
});
}
async function searchAgentKnowledge(query: string, agentId: string, appId: string) {
return client.search(query, {
filters: { AND: [{ agent_id: agentId }, { app_id: appId }] },
});
}
```
### Key Benefits
- Full isolation between users, agents, sessions, and apps
- Query at any scope level — user, agent, session, or app-wide
- No memory leakage between tenants
**Best for:** Multi-agent workflows, multi-tenant SaaS — proper isolation at every level.
---
## 6. Personalized Search
Blend real-time search results with personal context. Uses `custom_instructions` to infer preferences from queries.
### Implementation (Python)
```python
from mem0 import MemoryClient
from openai import OpenAI
mem0 = MemoryClient()
openai_client = OpenAI()
# One-time setup: configure Mem0 to infer from queries
mem0.project.update(
custom_instructions="""Infer user preferences and facts from their search queries.
Extract dietary preferences, location, interests, and purchase history."""
)
def personalized_search(user_id: str, query: str, search_results: list) -> str:
# Get user context from memory
memories = mem0.search(query, user_id=user_id, top_k=5)
user_context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
response = openai_client.chat.completions.create(
model="gpt-4.1-nano-2025-04-14",
messages=[
{"role": "system", "content": f"Personalize search results using user context:\n{user_context}"},
{"role": "user", "content": f"Query: {query}\n\nSearch results:\n{search_results}"},
]
)
reply = response.choices[0].message.content
# Store the query to learn preferences over time
mem0.add(
[{"role": "user", "content": query}],
user_id=user_id
)
return reply
# Usage
personalized_search("user_42", "best restaurants nearby", ["Restaurant A", "Restaurant B"])
# Over time, Mem0 learns: "user prefers vegetarian, lives in Austin"
# Future searches are automatically personalized
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
import OpenAI from 'openai';
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
const openai = new OpenAI();
async function personalizedSearch(userId: string, query: string, searchResults: string[]): Promise<string> {
const memories = await mem0.search(query, { user_id: userId, top_k: 5 });
const context = memories.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
const response = await openai.chat.completions.create({
model: 'gpt-4.1-nano-2025-04-14',
messages: [
{ role: 'system', content: `Personalize results using user context:\n${context}` },
{ role: 'user', content: `Query: ${query}\nResults: ${searchResults.join(', ')}` },
],
});
const reply = response.choices[0].message.content!;
await mem0.add([{ role: 'user', content: query }], { user_id: userId });
return reply;
}
```
### Key Benefits
- Learns preferences from queries automatically via `custom_instructions`
- Personalizes any search provider (Tavily, Google, Bing)
- Zero manual preference setup — improves over time
**Best for:** Personalized search engines, recommendation systems — search results tailored to individual users.
---
## 7. Email Intelligence
Capture, categorize, and recall inbox threads using persistent memories with rich metadata.
### Implementation (Python)
```python
from mem0 import MemoryClient
client = MemoryClient()
def store_email(user_id: str, sender: str, subject: str, body: str, date: str):
client.add(
[{"role": "user", "content": f"Email from {sender}: {subject}\n\n{body}"}],
user_id=user_id,
metadata={"email_type": "incoming", "sender": sender, "subject": subject, "date": date}
)
def search_emails(user_id: str, query: str):
return client.search(
query,
filters={"AND": [{"user_id": user_id}, {"categories": {"contains": "email"}}]},
top_k=10
)
def get_emails_from_sender(user_id: str, sender: str):
return client.get_all(
filters={
"AND": [
{"user_id": user_id},
{"metadata": {"contains": sender}}
]
}
)
# Usage
store_email("alice", "bob@acme.com", "Q3 Budget Review", "Attached is the Q3 budget...", "2025-01-15")
store_email("alice", "carol@acme.com", "Sprint Planning", "Here are the priorities...", "2025-01-16")
results = search_emails("alice", "budget discussions")
sender_emails = get_emails_from_sender("alice", "bob@acme.com")
```
### Implementation (TypeScript)
```typescript
import MemoryClient from 'mem0ai';
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
async function storeEmail(userId: string, sender: string, subject: string, body: string, date: string) {
await client.add(
[{ role: 'user', content: `Email from ${sender}: ${subject}\n\n${body}` }],
{ user_id: userId, metadata: { email_type: 'incoming', sender, subject, date } }
);
}
async function searchEmails(userId: string, query: string) {
return client.search(query, {
filters: { AND: [{ user_id: userId }, { categories: { contains: 'email' } }] },
top_k: 10,
});
}
```
### Key Benefits
- Rich metadata enables multi-dimensional queries (sender, date, subject)
- Category filtering separates emails from other memory types
- Semantic search across all email content
**Best for:** Inbox management, email automation — searchable email memories with metadata filtering.
---
## Common Patterns Across Use Cases
### Pattern 1: Retrieve → Generate → Store
Every use case follows the same 3-step loop:
```python
# 1. Retrieve relevant context
memories = mem0.search(user_input, user_id=user_id)
context = "\n".join([m["memory"] for m in memories.get("results", [])])
# 2. Generate with context
response = llm.generate(system_prompt=f"Context:\n{context}", user_input=user_input)
# 3. Store the interaction
mem0.add(
[{"role": "user", "content": user_input}, {"role": "assistant", "content": response}],
user_id=user_id
)
```
### Pattern 2: Scope with Entity Identifiers
Use `user_id`, `agent_id`, `app_id`, and `run_id` to isolate memories:
```python
# User-level: personal preferences
client.add(messages, user_id="alice")
# Session-level: conversation within one session
client.add(messages, user_id="alice", run_id="session_123")
# Agent-level: agent-specific knowledge
client.add(messages, agent_id="support_bot", app_id="helpdesk")
```
### Pattern 3: Rich Metadata for Filtering
Attach structured metadata for multi-dimensional queries:
```python
# Store with metadata
client.add(messages, user_id="alice", metadata={"priority": "high", "source": "phone_call"})
# Filter by category + metadata
client.search("billing issues", filters={
"AND": [{"user_id": "alice"}, {"categories": {"contains": "billing"}}]
})
```
### Pattern 4: Custom Instructions for Domain-Specific Extraction
Control what Mem0 extracts from conversations:
```python
client.project.update(
custom_instructions="Extract medical conditions, medications, and allergies. Exclude billing info."
)
```
---
## More Examples
For 30+ cookbooks with complete working code: [docs.mem0.ai/cookbooks](https://docs.mem0.ai/cookbooks)
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""
Mem0 Documentation Search Agent (Mintlify-based)
On-demand search tool for querying Mem0 documentation without storing content locally.
This tool leverages Mintlify's documentation structure to perform just-in-time
retrieval of technical information from docs.mem0.ai.
Usage:
python mem0_doc_search.py --query "how to add graph memory"
python mem0_doc_search.py --query "filter syntax for categories"
python mem0_doc_search.py --page "/platform/features/graph-memory"
python mem0_doc_search.py --index
python mem0_doc_search.py --query "webhook events" --section platform
Purpose:
- Avoid bloating local context with full documentation
- Enable just-in-time retrieval of technical details
- Query specific documentation pages on demand
- Search across the full Mem0 documentation site
"""
import argparse
import json
import sys
import urllib.error
import urllib.parse
import urllib.request
DOCS_BASE = "https://docs.mem0.ai"
SEARCH_ENDPOINT = f"{DOCS_BASE}/api/search"
LLMS_INDEX = f"{DOCS_BASE}/llms.txt"
# Known documentation sections for targeted retrieval
SECTION_MAP = {
"platform": [
"/platform/overview",
"/platform/quickstart",
"/platform/features",
"/platform/features/graph-memory",
"/platform/features/selective-memory",
"/platform/features/custom-categories",
"/platform/features/v2-memory-filters",
"/platform/features/async-client",
"/platform/features/webhooks",
"/platform/features/multimodal",
],
"api": [
"/api-reference/memory/add-memories",
"/api-reference/memory/v2-search-memories",
"/api-reference/memory/v2-get-memories",
"/api-reference/memory/get-memory",
"/api-reference/memory/update-memory",
"/api-reference/memory/delete-memory",
],
"open-source": [
"/open-source/overview",
"/open-source/python-quickstart",
"/open-source/node-quickstart",
"/open-source/features",
"/open-source/features/graph-memory",
"/open-source/features/rest-api",
"/open-source/configure-components",
],
"openmemory": [
"/openmemory/overview",
"/openmemory/quickstart",
],
"sdks": [
"/sdks/python",
"/sdks/js",
],
"integrations": [
"/integrations",
],
}
def fetch_url(url: str) -> str:
"""Fetch content from a URL."""
req = urllib.request.Request(url, headers={"User-Agent": "Mem0DocSearchAgent/1.0"})
try:
with urllib.request.urlopen(req, timeout=15) as resp:
return resp.read().decode("utf-8")
except urllib.error.HTTPError as e:
return f"HTTP Error {e.code}: {e.reason}"
except urllib.error.URLError as e:
return f"URL Error: {e.reason}"
def search_docs(query: str, section: str | None = None) -> dict:
"""
Search Mem0 documentation using Mintlify's search API.
Falls back to the llms.txt index for keyword matching if the API is unavailable.
"""
# Try Mintlify search API first
params = urllib.parse.urlencode({"query": query})
search_url = f"{SEARCH_ENDPOINT}?{params}"
try:
result = fetch_url(search_url)
data = json.loads(result)
if isinstance(data, dict) and data.get("results"):
results = data["results"]
if section and section in SECTION_MAP:
section_paths = SECTION_MAP[section]
results = [r for r in results if any(r.get("url", "").startswith(p) for p in section_paths)]
return {"source": "mintlify_search", "results": results}
except (json.JSONDecodeError, Exception):
pass
# Fallback: search llms.txt index for matching URLs
index_content = fetch_url(LLMS_INDEX)
query_lower = query.lower()
matching_urls = []
for line in index_content.splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if query_lower in line.lower():
matching_urls.append(line)
if section and section in SECTION_MAP:
section_paths = SECTION_MAP[section]
matching_urls = [u for u in matching_urls if any(p in u for p in section_paths)]
return {
"source": "llms_txt_index",
"query": query,
"matching_urls": matching_urls[:20],
"suggestion": "Fetch specific URLs for detailed content",
}
def fetch_page(page_path: str) -> dict:
"""Fetch a specific documentation page."""
url = f"{DOCS_BASE}{page_path}" if page_path.startswith("/") else page_path
content = fetch_url(url)
return {"url": url, "content": content[:10000], "truncated": len(content) > 10000}
def get_index() -> dict:
"""Fetch the full documentation index from llms.txt."""
content = fetch_url(LLMS_INDEX)
urls = [line.strip() for line in content.splitlines() if line.strip() and not line.startswith("#")]
return {"total_pages": len(urls), "urls": urls, "sections": list(SECTION_MAP.keys())}
def list_section(section: str) -> dict:
"""List all known pages in a documentation section."""
if section not in SECTION_MAP:
return {"error": f"Unknown section: {section}", "available": list(SECTION_MAP.keys())}
return {
"section": section,
"pages": [f"{DOCS_BASE}{p}" for p in SECTION_MAP[section]],
}
def main():
parser = argparse.ArgumentParser(description="Search Mem0 documentation on demand")
parser.add_argument("--query", help="Search query for documentation")
parser.add_argument("--page", help="Fetch a specific page path (e.g., /platform/features/graph-memory)")
parser.add_argument("--index", action="store_true", help="Show full documentation index")
parser.add_argument("--section", help="Filter by section or list section pages")
parser.add_argument("--json", action="store_true", help="Output as JSON")
args = parser.parse_args()
if args.index:
result = get_index()
elif args.section and not args.query:
result = list_section(args.section)
elif args.page:
result = fetch_page(args.page)
elif args.query:
result = search_docs(args.query, section=args.section)
else:
parser.print_help()
sys.exit(1)
if args.json:
print(json.dumps(result, indent=2))
else:
if isinstance(result, dict):
if "results" in result:
print(f"Source: {result.get('source', 'unknown')}")
for r in result["results"]:
print(f" - {r.get('title', 'N/A')}: {r.get('url', 'N/A')}")
if r.get("description"):
print(f" {r['description'][:200]}")
elif "matching_urls" in result:
print(f"Source: {result['source']}")
print(f"Query: {result['query']}")
for url in result["matching_urls"]:
print(f" - {url}")
if result.get("suggestion"):
print(f"\n{result['suggestion']}")
elif "urls" in result:
print(f"Total documentation pages: {result['total_pages']}")
print(f"Sections: {', '.join(result['sections'])}")
for url in result["urls"][:30]:
print(f" - {url}")
if result["total_pages"] > 30:
print(f" ... and {result['total_pages'] - 30} more")
elif "pages" in result:
print(f"Section: {result['section']}")
for page in result["pages"]:
print(f" - {page}")
elif "content" in result:
print(f"URL: {result['url']}")
if result.get("truncated"):
print("[Content truncated to 10000 chars]")
print(result["content"])
elif "error" in result:
print(f"Error: {result['error']}")
if result.get("available"):
print(f"Available sections: {', '.join(result['available'])}")
else:
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()