Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
189aea04c9 | ||
|
|
e830351691 |
@@ -729,19 +729,6 @@ mode: "wide"
|
|||||||
|
|
||||||
<Tab title="TypeScript">
|
<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">
|
<Update label="2026-03-09" description="v2.3.0">
|
||||||
|
|
||||||
**Breaking Changes:**
|
**Breaking Changes:**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "mem0ai",
|
"name": "mem0ai",
|
||||||
"version": "2.4.0",
|
"version": "2.3.0",
|
||||||
"description": "The Memory Layer For Your AI Apps",
|
"description": "The Memory Layer For Your AI Apps",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
"module": "./dist/index.mjs",
|
"module": "./dist/index.mjs",
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export class ConfigManager {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
baseURL: userConf?.baseURL || defaultConf.baseURL,
|
baseURL: userConf?.baseURL || defaultConf.baseURL,
|
||||||
|
url: userConf?.url,
|
||||||
apiKey:
|
apiKey:
|
||||||
userConf?.apiKey !== undefined
|
userConf?.apiKey !== undefined
|
||||||
? userConf.apiKey
|
? userConf.apiKey
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class OllamaEmbedder implements Embedder {
|
|||||||
|
|
||||||
constructor(config: EmbeddingConfig) {
|
constructor(config: EmbeddingConfig) {
|
||||||
this.ollama = new Ollama({
|
this.ollama = new Ollama({
|
||||||
host: config.url || "http://localhost:11434",
|
host: config.url || config.baseURL || "http://localhost:11434",
|
||||||
});
|
});
|
||||||
this.model = config.model || "nomic-embed-text:latest";
|
this.model = config.model || "nomic-embed-text:latest";
|
||||||
this.embeddingDims = config.embeddingDims || 768;
|
this.embeddingDims = config.embeddingDims || 768;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export class OllamaLLM implements LLM {
|
|||||||
|
|
||||||
constructor(config: LLMConfig) {
|
constructor(config: LLMConfig) {
|
||||||
this.ollama = new Ollama({
|
this.ollama = new Ollama({
|
||||||
host: config.config?.url || "http://localhost:11434",
|
host: config.url || config.baseURL || "http://localhost:11434",
|
||||||
});
|
});
|
||||||
this.model = config.model || "llama3.1:8b";
|
this.model = config.model || "llama3.1:8b";
|
||||||
this.ensureModelExists().catch((err) => {
|
this.ensureModelExists().catch((err) => {
|
||||||
|
|||||||
@@ -278,9 +278,5 @@ export function parseMessages(messages: string[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function removeCodeBlocks(text: string): string {
|
export function removeCodeBlocks(text: string): string {
|
||||||
// Extract content inside code fences instead of deleting it.
|
return text.replace(/```[^`]*```/g, "");
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,7 +210,11 @@ describe("backward compat: MemoryVectorStore", () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const store = new MemoryVectorStore({ dimension: 3, dbPath });
|
const store = new MemoryVectorStore({ dimension: 3, dbPath });
|
||||||
await store.insert([normalize([1, 0, 0])], ["id1"], [{ text: "hello" }]);
|
await store.insert(
|
||||||
|
[normalize([1, 0, 0])],
|
||||||
|
["id1"],
|
||||||
|
[{ text: "hello" }],
|
||||||
|
);
|
||||||
|
|
||||||
expect(fs.existsSync(dbPath)).toBe(true);
|
expect(fs.existsSync(dbPath)).toBe(true);
|
||||||
|
|
||||||
|
|||||||
@@ -200,9 +200,9 @@ describe("MemoryVectorStore – path handling", () => {
|
|||||||
expect(
|
expect(
|
||||||
fs.existsSync(path.join(fakeHome, ".mem0", "vector_store.db")),
|
fs.existsSync(path.join(fakeHome, ".mem0", "vector_store.db")),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(fs.existsSync(path.join(readOnlyCwd, "vector_store.db"))).toBe(
|
expect(
|
||||||
false,
|
fs.existsSync(path.join(readOnlyCwd, "vector_store.db")),
|
||||||
);
|
).toBe(false);
|
||||||
} finally {
|
} finally {
|
||||||
fs.chmodSync(readOnlyCwd, 0o755);
|
fs.chmodSync(readOnlyCwd, 0o755);
|
||||||
fs.rmSync(fakeHome, { recursive: true, force: true });
|
fs.rmSync(fakeHome, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export interface HistoryStoreConfig {
|
|||||||
export interface LLMConfig {
|
export interface LLMConfig {
|
||||||
provider?: string;
|
provider?: string;
|
||||||
baseURL?: string;
|
baseURL?: string;
|
||||||
|
url?: string;
|
||||||
config?: Record<string, any>;
|
config?: Record<string, any>;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
model?: string | any;
|
model?: string | any;
|
||||||
@@ -143,6 +144,7 @@ export const MemoryConfigSchema = z.object({
|
|||||||
model: z.union([z.string(), z.any()]).optional(),
|
model: z.union([z.string(), z.any()]).optional(),
|
||||||
modelProperties: z.record(z.string(), z.any()).optional(),
|
modelProperties: z.record(z.string(), z.any()).optional(),
|
||||||
baseURL: z.string().optional(),
|
baseURL: z.string().optional(),
|
||||||
|
url: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
historyDbPath: z.string().optional(),
|
historyDbPath: z.string().optional(),
|
||||||
|
|||||||
@@ -273,7 +273,10 @@ export class Qdrant implements VectorStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureCollection(name: string, size: number): Promise<void> {
|
private async ensureCollection(
|
||||||
|
name: string,
|
||||||
|
size: number,
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.client.createCollection(name, {
|
await this.client.createCollection(name, {
|
||||||
vectors: {
|
vectors: {
|
||||||
|
|||||||
@@ -85,4 +85,80 @@ describe("ConfigManager", () => {
|
|||||||
expect(config.vectorStore.config.dimension).toBe(768);
|
expect(config.vectorStore.config.dimension).toBe(768);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("mergeConfig - LLM url passthrough for Ollama", () => {
|
||||||
|
const baseEmbedder = {
|
||||||
|
provider: "openai",
|
||||||
|
config: { apiKey: "test-key" },
|
||||||
|
};
|
||||||
|
const baseVectorStore = {
|
||||||
|
provider: "memory",
|
||||||
|
config: { collectionName: "test" },
|
||||||
|
};
|
||||||
|
|
||||||
|
it("should preserve url in LLM config when provided", () => {
|
||||||
|
const config = ConfigManager.mergeConfig({
|
||||||
|
embedder: baseEmbedder,
|
||||||
|
vectorStore: baseVectorStore,
|
||||||
|
llm: {
|
||||||
|
provider: "ollama",
|
||||||
|
config: { model: "llama3.2:3b", url: "http://10.0.0.100:11434" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.llm.config.url).toBe("http://10.0.0.100:11434");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prefer baseURL over url when both are provided", () => {
|
||||||
|
const config = ConfigManager.mergeConfig({
|
||||||
|
embedder: baseEmbedder,
|
||||||
|
vectorStore: baseVectorStore,
|
||||||
|
llm: {
|
||||||
|
provider: "ollama",
|
||||||
|
config: {
|
||||||
|
model: "llama3.2:3b",
|
||||||
|
baseURL: "http://custom:11434",
|
||||||
|
url: "http://fallback:11434",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.llm.config.baseURL).toBe("http://custom:11434");
|
||||||
|
expect(config.llm.config.url).toBe("http://fallback:11434");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use default baseURL when no url or baseURL provided", () => {
|
||||||
|
const config = ConfigManager.mergeConfig({
|
||||||
|
embedder: baseEmbedder,
|
||||||
|
vectorStore: baseVectorStore,
|
||||||
|
llm: {
|
||||||
|
provider: "ollama",
|
||||||
|
config: { model: "llama3.2:3b" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.llm.config.url).toBeUndefined();
|
||||||
|
expect(config.llm.config.baseURL).toBe("https://api.openai.com/v1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve url in embedder config (existing behavior)", () => {
|
||||||
|
const config = ConfigManager.mergeConfig({
|
||||||
|
embedder: {
|
||||||
|
provider: "ollama",
|
||||||
|
config: {
|
||||||
|
model: "nomic-embed-text",
|
||||||
|
url: "http://10.0.0.100:11434",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vectorStore: baseVectorStore,
|
||||||
|
llm: {
|
||||||
|
provider: "ollama",
|
||||||
|
config: { model: "llama3.2:3b", url: "http://10.0.0.100:11434" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(config.embedder.config.url).toBe("http://10.0.0.100:11434");
|
||||||
|
expect(config.llm.config.url).toBe("http://10.0.0.100:11434");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,10 +13,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { MemoryGraph } from "../src/memory/graph_memory";
|
import { MemoryGraph } from "../src/memory/graph_memory";
|
||||||
import {
|
import { EXTRACT_RELATIONS_PROMPT, getDeleteMessages } from "../src/graphs/utils";
|
||||||
EXTRACT_RELATIONS_PROMPT,
|
|
||||||
getDeleteMessages,
|
|
||||||
} from "../src/graphs/utils";
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Mocks – we replace heavy dependencies so tests run without Neo4j / OpenAI
|
// Mocks – we replace heavy dependencies so tests run without Neo4j / OpenAI
|
||||||
@@ -103,10 +100,7 @@ describe("_retrieveNodesFromData", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mg = graph();
|
const mg = graph();
|
||||||
const result = await mg._retrieveNodesFromData(
|
const result = await mg._retrieveNodesFromData("Alice likes pizza", FILTERS);
|
||||||
"Alice likes pizza",
|
|
||||||
FILTERS,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual({ alice: "person", pizza: "food" });
|
expect(result).toEqual({ alice: "person", pizza: "food" });
|
||||||
});
|
});
|
||||||
@@ -172,9 +166,7 @@ describe("_retrieveNodesFromData", () => {
|
|||||||
toolCalls: [
|
toolCalls: [
|
||||||
{
|
{
|
||||||
name: "some_other_tool",
|
name: "some_other_tool",
|
||||||
arguments: JSON.stringify({
|
arguments: JSON.stringify({ entities: [{ entity: "X", entity_type: "Y" }] }),
|
||||||
entities: [{ entity: "X", entity_type: "Y" }],
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -281,7 +273,9 @@ describe("_establishNodesRelationsFromData", () => {
|
|||||||
|
|
||||||
it("throws on malformed JSON in tool call arguments (no try/catch in source)", async () => {
|
it("throws on malformed JSON in tool call arguments (no try/catch in source)", async () => {
|
||||||
mockGenerateResponse.mockResolvedValueOnce({
|
mockGenerateResponse.mockResolvedValueOnce({
|
||||||
toolCalls: [{ name: "establish_relationships", arguments: "<<BROKEN>>" }],
|
toolCalls: [
|
||||||
|
{ name: "establish_relationships", arguments: "<<BROKEN>>" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const mg = graph();
|
const mg = graph();
|
||||||
@@ -353,11 +347,7 @@ describe("_getDeleteEntitiesFromSearchOutput", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mg = graph();
|
const mg = graph();
|
||||||
const result = await mg._getDeleteEntitiesFromSearchOutput(
|
const result = await mg._getDeleteEntitiesFromSearchOutput(SEARCH_OUTPUT, "Alice hates pizza", FILTERS);
|
||||||
SEARCH_OUTPUT,
|
|
||||||
"Alice hates pizza",
|
|
||||||
FILTERS,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ source: "alice", relationship: "likes", destination: "pizza" },
|
{ source: "alice", relationship: "likes", destination: "pizza" },
|
||||||
@@ -368,11 +358,7 @@ describe("_getDeleteEntitiesFromSearchOutput", () => {
|
|||||||
mockGenerateResponse.mockResolvedValueOnce("string response");
|
mockGenerateResponse.mockResolvedValueOnce("string response");
|
||||||
|
|
||||||
const mg = graph();
|
const mg = graph();
|
||||||
const result = await mg._getDeleteEntitiesFromSearchOutput(
|
const result = await mg._getDeleteEntitiesFromSearchOutput(SEARCH_OUTPUT, "x", FILTERS);
|
||||||
SEARCH_OUTPUT,
|
|
||||||
"x",
|
|
||||||
FILTERS,
|
|
||||||
);
|
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -380,11 +366,7 @@ describe("_getDeleteEntitiesFromSearchOutput", () => {
|
|||||||
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
|
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
|
||||||
|
|
||||||
const mg = graph();
|
const mg = graph();
|
||||||
const result = await mg._getDeleteEntitiesFromSearchOutput(
|
const result = await mg._getDeleteEntitiesFromSearchOutput(SEARCH_OUTPUT, "x", FILTERS);
|
||||||
SEARCH_OUTPUT,
|
|
||||||
"x",
|
|
||||||
FILTERS,
|
|
||||||
);
|
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -399,11 +381,7 @@ describe("_getDeleteEntitiesFromSearchOutput", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mg = graph();
|
const mg = graph();
|
||||||
const result = await mg._getDeleteEntitiesFromSearchOutput(
|
const result = await mg._getDeleteEntitiesFromSearchOutput(SEARCH_OUTPUT, "x", FILTERS);
|
||||||
SEARCH_OUTPUT,
|
|
||||||
"x",
|
|
||||||
FILTERS,
|
|
||||||
);
|
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -412,29 +390,17 @@ describe("_getDeleteEntitiesFromSearchOutput", () => {
|
|||||||
toolCalls: [
|
toolCalls: [
|
||||||
{
|
{
|
||||||
name: "delete_graph_memory",
|
name: "delete_graph_memory",
|
||||||
arguments: JSON.stringify({
|
arguments: JSON.stringify({ source: "A", relationship: "r1", destination: "B" }),
|
||||||
source: "A",
|
|
||||||
relationship: "r1",
|
|
||||||
destination: "B",
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "delete_graph_memory",
|
name: "delete_graph_memory",
|
||||||
arguments: JSON.stringify({
|
arguments: JSON.stringify({ source: "C", relationship: "r2", destination: "D" }),
|
||||||
source: "C",
|
|
||||||
relationship: "r2",
|
|
||||||
destination: "D",
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const mg = graph();
|
const mg = graph();
|
||||||
const result = await mg._getDeleteEntitiesFromSearchOutput(
|
const result = await mg._getDeleteEntitiesFromSearchOutput(SEARCH_OUTPUT, "x", FILTERS);
|
||||||
SEARCH_OUTPUT,
|
|
||||||
"x",
|
|
||||||
FILTERS,
|
|
||||||
);
|
|
||||||
expect(result).toHaveLength(2);
|
expect(result).toHaveLength(2);
|
||||||
expect(result[0].source).toBe("a");
|
expect(result[0].source).toBe("a");
|
||||||
expect(result[1].source).toBe("c");
|
expect(result[1].source).toBe("c");
|
||||||
@@ -459,11 +425,7 @@ describe("_getDeleteEntitiesFromSearchOutput", () => {
|
|||||||
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
|
mockGenerateResponse.mockResolvedValueOnce({ toolCalls: [] });
|
||||||
|
|
||||||
const mg = graph();
|
const mg = graph();
|
||||||
const result = await mg._getDeleteEntitiesFromSearchOutput(
|
const result = await mg._getDeleteEntitiesFromSearchOutput([], "data", FILTERS);
|
||||||
[],
|
|
||||||
"data",
|
|
||||||
FILTERS,
|
|
||||||
);
|
|
||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -519,11 +481,7 @@ describe("_removeSpacesFromEntities (via _establishNodesRelationsFromData)", ()
|
|||||||
name: "establish_relationships",
|
name: "establish_relationships",
|
||||||
arguments: JSON.stringify({
|
arguments: JSON.stringify({
|
||||||
entities: [
|
entities: [
|
||||||
{
|
{ source: "New York", relationship: "Capital Of", destination: "United States" },
|
||||||
source: "New York",
|
|
||||||
relationship: "Capital Of",
|
|
||||||
destination: "United States",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -531,18 +489,10 @@ describe("_removeSpacesFromEntities (via _establishNodesRelationsFromData)", ()
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mg = graph();
|
const mg = graph();
|
||||||
const result = await mg._establishNodesRelationsFromData(
|
const result = await mg._establishNodesRelationsFromData("test", FILTERS, {});
|
||||||
"test",
|
|
||||||
FILTERS,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{ source: "new_york", relationship: "capital_of", destination: "united_states" },
|
||||||
source: "new_york",
|
|
||||||
relationship: "capital_of",
|
|
||||||
destination: "united_states",
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -31,8 +31,7 @@ describe("Graph prompts — JSON keyword requirement", () => {
|
|||||||
it("EXTRACT_RELATIONS_PROMPT produces a message containing 'json' once the suffix is appended", () => {
|
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."
|
// graph_memory.ts appends "\nPlease provide your response in JSON format."
|
||||||
const withSuffix =
|
const withSuffix =
|
||||||
EXTRACT_RELATIONS_PROMPT +
|
EXTRACT_RELATIONS_PROMPT + "\nPlease provide your response in JSON format.";
|
||||||
"\nPlease provide your response in JSON format.";
|
|
||||||
expect(withSuffix.toLowerCase()).toContain("json");
|
expect(withSuffix.toLowerCase()).toContain("json");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,21 +86,13 @@ describe("getDeleteMessages", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("handles special characters in userId (e.g. angle brackets, quotes)", () => {
|
it("handles special characters in userId (e.g. angle brackets, quotes)", () => {
|
||||||
const [system] = getDeleteMessages(
|
const [system] = getDeleteMessages("mem", "data", '<script>alert("xss")</script>');
|
||||||
"mem",
|
|
||||||
"data",
|
|
||||||
'<script>alert("xss")</script>',
|
|
||||||
);
|
|
||||||
expect(system).toContain('<script>alert("xss")</script>');
|
expect(system).toContain('<script>alert("xss")</script>');
|
||||||
expect(system).not.toContain("USER_ID");
|
expect(system).not.toContain("USER_ID");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles unicode input", () => {
|
it("handles unicode input", () => {
|
||||||
const [system, user] = getDeleteMessages(
|
const [system, user] = getDeleteMessages("日本語メモリ", "新しい情報", "ユーザー1");
|
||||||
"日本語メモリ",
|
|
||||||
"新しい情報",
|
|
||||||
"ユーザー1",
|
|
||||||
);
|
|
||||||
expect(system).toContain("ユーザー1");
|
expect(system).toContain("ユーザー1");
|
||||||
expect(user).toContain("日本語メモリ");
|
expect(user).toContain("日本語メモリ");
|
||||||
expect(user).toContain("新しい情報");
|
expect(user).toContain("新しい情報");
|
||||||
@@ -139,7 +130,7 @@ describe("formatEntities", () => {
|
|||||||
|
|
||||||
it("preserves special characters in entity fields", () => {
|
it("preserves special characters in entity fields", () => {
|
||||||
const result = formatEntities([
|
const result = formatEntities([
|
||||||
{ source: "O'Brien", relationship: 'said "hello"', destination: "café" },
|
{ source: "O'Brien", relationship: "said \"hello\"", destination: "café" },
|
||||||
]);
|
]);
|
||||||
expect(result).toContain("O'Brien");
|
expect(result).toContain("O'Brien");
|
||||||
expect(result).toContain('said "hello"');
|
expect(result).toContain('said "hello"');
|
||||||
|
|||||||
@@ -151,14 +151,10 @@ describeIfQdrant("Issue #4212/#4173: Qdrant dimension mismatch", () => {
|
|||||||
const id1 = uuidv4();
|
const id1 = uuidv4();
|
||||||
const id2 = uuidv4();
|
const id2 = uuidv4();
|
||||||
|
|
||||||
await store.insert(
|
await store.insert([vec1, vec2], [id1, id2], [
|
||||||
[vec1, vec2],
|
{ data: "hello", userId: "u1" },
|
||||||
[id1, id2],
|
{ data: "world", userId: "u1" },
|
||||||
[
|
]);
|
||||||
{ data: "hello", userId: "u1" },
|
|
||||||
{ data: "world", userId: "u1" },
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Search with 768-dim query — this USED TO fail with Bad Request
|
// Search with 768-dim query — this USED TO fail with Bad Request
|
||||||
const results = await store.search(vec1, 2, { userId: "u1" });
|
const results = await store.search(vec1, 2, { userId: "u1" });
|
||||||
@@ -207,11 +203,9 @@ describeIfQdrant("Issue #4212/#4173: Qdrant dimension mismatch", () => {
|
|||||||
return {
|
return {
|
||||||
EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) },
|
EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) },
|
||||||
VectorStoreFactory: {
|
VectorStoreFactory: {
|
||||||
create: jest
|
create: jest.fn().mockImplementation((_provider: string, config: any) => {
|
||||||
.fn()
|
return new QdrantStore(config);
|
||||||
.mockImplementation((_provider: string, config: any) => {
|
}),
|
||||||
return new QdrantStore(config);
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
LLMFactory: {
|
LLMFactory: {
|
||||||
create: jest.fn().mockReturnValue({
|
create: jest.fn().mockReturnValue({
|
||||||
@@ -286,11 +280,9 @@ describeIfQdrant("Issue #4212/#4173: Qdrant dimension mismatch", () => {
|
|||||||
return {
|
return {
|
||||||
EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) },
|
EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) },
|
||||||
VectorStoreFactory: {
|
VectorStoreFactory: {
|
||||||
create: jest
|
create: jest.fn().mockImplementation((_provider: string, config: any) => {
|
||||||
.fn()
|
return new QdrantStore(config);
|
||||||
.mockImplementation((_provider: string, config: any) => {
|
}),
|
||||||
return new QdrantStore(config);
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
LLMFactory: {
|
LLMFactory: {
|
||||||
create: jest.fn().mockReturnValue({
|
create: jest.fn().mockReturnValue({
|
||||||
@@ -358,11 +350,9 @@ describeIfQdrant("Issue #4212/#4173: Qdrant dimension mismatch", () => {
|
|||||||
return {
|
return {
|
||||||
EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) },
|
EmbedderFactory: { create: jest.fn().mockReturnValue(fakeEmbedder) },
|
||||||
VectorStoreFactory: {
|
VectorStoreFactory: {
|
||||||
create: jest
|
create: jest.fn().mockImplementation((_provider: string, config: any) => {
|
||||||
.fn()
|
return new QdrantStore(config);
|
||||||
.mockImplementation((_provider: string, config: any) => {
|
}),
|
||||||
return new QdrantStore(config);
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
LLMFactory: {
|
LLMFactory: {
|
||||||
create: jest.fn().mockReturnValue({
|
create: jest.fn().mockReturnValue({
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
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("```");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -76,14 +76,10 @@ describe("MemoryVectorStore – full backward compat", () => {
|
|||||||
vec2[1] = 1.0;
|
vec2[1] = 1.0;
|
||||||
|
|
||||||
// Insert
|
// Insert
|
||||||
await store.insert(
|
await store.insert([vec1, vec2], ["id-1", "id-2"], [
|
||||||
[vec1, vec2],
|
{ data: "alpha", userId: "u1" },
|
||||||
["id-1", "id-2"],
|
{ data: "beta", userId: "u1" },
|
||||||
[
|
]);
|
||||||
{ data: "alpha", userId: "u1" },
|
|
||||||
{ data: "beta", userId: "u1" },
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get
|
// Get
|
||||||
const item = await store.get("id-1");
|
const item = await store.get("id-1");
|
||||||
@@ -208,87 +204,66 @@ describe("MemoryVectorStore – full backward compat", () => {
|
|||||||
describe("Qdrant – backward compat with mocked client", () => {
|
describe("Qdrant – backward compat with mocked client", () => {
|
||||||
function createMockQdrantClient() {
|
function createMockQdrantClient() {
|
||||||
const collections = new Map<string, number>();
|
const collections = new Map<string, number>();
|
||||||
const points = new Map<
|
const points = new Map<string, { id: string; vector: number[]; payload: any }>();
|
||||||
string,
|
|
||||||
{ id: string; vector: number[]; payload: any }
|
|
||||||
>();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_collections: collections,
|
_collections: collections,
|
||||||
_points: points,
|
_points: points,
|
||||||
createCollection: jest
|
createCollection: jest.fn().mockImplementation(async (name: string, opts: any) => {
|
||||||
.fn()
|
if (collections.has(name)) {
|
||||||
.mockImplementation(async (name: string, opts: any) => {
|
const err: any = new Error("Collection already exists");
|
||||||
if (collections.has(name)) {
|
err.status = 409;
|
||||||
const err: any = new Error("Collection already exists");
|
throw err;
|
||||||
err.status = 409;
|
}
|
||||||
throw err;
|
collections.set(name, opts.vectors.size);
|
||||||
}
|
}),
|
||||||
collections.set(name, opts.vectors.size);
|
|
||||||
}),
|
|
||||||
getCollection: jest.fn().mockImplementation(async (name: string) => {
|
getCollection: jest.fn().mockImplementation(async (name: string) => {
|
||||||
if (!collections.has(name)) {
|
if (!collections.has(name)) {
|
||||||
const err: any = new Error("Not found");
|
const err: any = new Error("Not found");
|
||||||
err.status = 404;
|
err.status = 404;
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
return {
|
return { config: { params: { vectors: { size: collections.get(name) } } } };
|
||||||
config: { params: { vectors: { size: collections.get(name) } } },
|
|
||||||
};
|
|
||||||
}),
|
}),
|
||||||
getCollections: jest.fn().mockResolvedValue({
|
getCollections: jest.fn().mockResolvedValue({
|
||||||
collections: [],
|
collections: [],
|
||||||
}),
|
}),
|
||||||
upsert: jest
|
upsert: jest.fn().mockImplementation(async (collName: string, opts: any) => {
|
||||||
.fn()
|
for (const pt of opts.points) {
|
||||||
.mockImplementation(async (collName: string, opts: any) => {
|
points.set(`${collName}:${pt.id}`, { id: pt.id, vector: pt.vector, payload: pt.payload });
|
||||||
for (const pt of opts.points) {
|
}
|
||||||
points.set(`${collName}:${pt.id}`, {
|
}),
|
||||||
id: pt.id,
|
retrieve: jest.fn().mockImplementation(async (collName: string, opts: any) => {
|
||||||
vector: pt.vector,
|
const results = [];
|
||||||
payload: pt.payload,
|
for (const id of opts.ids) {
|
||||||
});
|
const pt = points.get(`${collName}:${id}`);
|
||||||
|
if (pt) results.push({ id: pt.id, payload: pt.payload });
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}),
|
||||||
|
search: jest.fn().mockImplementation(async (collName: string, opts: any) => {
|
||||||
|
const results: any[] = [];
|
||||||
|
points.forEach((pt, key) => {
|
||||||
|
if (key.startsWith(`${collName}:`)) {
|
||||||
|
results.push({ id: pt.id, payload: pt.payload, score: 0.9 });
|
||||||
}
|
}
|
||||||
}),
|
});
|
||||||
retrieve: jest
|
return results.slice(0, opts.limit);
|
||||||
.fn()
|
}),
|
||||||
.mockImplementation(async (collName: string, opts: any) => {
|
scroll: jest.fn().mockImplementation(async (collName: string, opts: any) => {
|
||||||
const results = [];
|
const results: any[] = [];
|
||||||
for (const id of opts.ids) {
|
points.forEach((pt, key) => {
|
||||||
const pt = points.get(`${collName}:${id}`);
|
if (key.startsWith(`${collName}:`)) {
|
||||||
if (pt) results.push({ id: pt.id, payload: pt.payload });
|
results.push({ id: pt.id, payload: pt.payload });
|
||||||
}
|
}
|
||||||
return results;
|
});
|
||||||
}),
|
return { points: results.slice(0, opts.limit) };
|
||||||
search: jest
|
}),
|
||||||
.fn()
|
delete: jest.fn().mockImplementation(async (collName: string, opts: any) => {
|
||||||
.mockImplementation(async (collName: string, opts: any) => {
|
for (const id of opts.points) {
|
||||||
const results: any[] = [];
|
points.delete(`${collName}:${id}`);
|
||||||
points.forEach((pt, key) => {
|
}
|
||||||
if (key.startsWith(`${collName}:`)) {
|
}),
|
||||||
results.push({ id: pt.id, payload: pt.payload, score: 0.9 });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return results.slice(0, opts.limit);
|
|
||||||
}),
|
|
||||||
scroll: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementation(async (collName: string, opts: any) => {
|
|
||||||
const results: any[] = [];
|
|
||||||
points.forEach((pt, key) => {
|
|
||||||
if (key.startsWith(`${collName}:`)) {
|
|
||||||
results.push({ id: pt.id, payload: pt.payload });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { points: results.slice(0, opts.limit) };
|
|
||||||
}),
|
|
||||||
delete: jest
|
|
||||||
.fn()
|
|
||||||
.mockImplementation(async (collName: string, opts: any) => {
|
|
||||||
for (const id of opts.points) {
|
|
||||||
points.delete(`${collName}:${id}`);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
deleteCollection: jest.fn().mockImplementation(async (name: string) => {
|
deleteCollection: jest.fn().mockImplementation(async (name: string) => {
|
||||||
collections.delete(name);
|
collections.delete(name);
|
||||||
}),
|
}),
|
||||||
@@ -349,10 +324,7 @@ describe("Qdrant – backward compat with mocked client", () => {
|
|||||||
|
|
||||||
// Insert
|
// Insert
|
||||||
await store.insert(
|
await store.insert(
|
||||||
[
|
[[1, 2, 3], [4, 5, 6]],
|
||||||
[1, 2, 3],
|
|
||||||
[4, 5, 6],
|
|
||||||
],
|
|
||||||
["id-1", "id-2"],
|
["id-1", "id-2"],
|
||||||
[{ data: "alpha" }, { data: "beta" }],
|
[{ data: "alpha" }, { data: "beta" }],
|
||||||
);
|
);
|
||||||
@@ -419,9 +391,9 @@ describe("Redis – backward compat with mocked client", () => {
|
|||||||
connect: jest.fn().mockResolvedValue(undefined),
|
connect: jest.fn().mockResolvedValue(undefined),
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
moduleList: jest
|
moduleList: jest.fn().mockResolvedValue([
|
||||||
.fn()
|
["name", "search", "ver", 20000],
|
||||||
.mockResolvedValue([["name", "search", "ver", 20000]]),
|
]),
|
||||||
ft: {
|
ft: {
|
||||||
dropIndex: jest.fn().mockResolvedValue(undefined),
|
dropIndex: jest.fn().mockResolvedValue(undefined),
|
||||||
create: jest.fn().mockResolvedValue(undefined),
|
create: jest.fn().mockResolvedValue(undefined),
|
||||||
@@ -627,17 +599,14 @@ describe("AzureAISearch – backward compat with mocked client", () => {
|
|||||||
createOrUpdateIndex: jest.fn().mockResolvedValue({}),
|
createOrUpdateIndex: jest.fn().mockResolvedValue({}),
|
||||||
deleteIndex: jest.fn().mockResolvedValue({}),
|
deleteIndex: jest.fn().mockResolvedValue({}),
|
||||||
})),
|
})),
|
||||||
AzureKeyCredential: jest
|
AzureKeyCredential: jest.fn().mockImplementation((key: string) => ({ key })),
|
||||||
.fn()
|
|
||||||
.mockImplementation((key: string) => ({ key })),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.doMock("@azure/identity", () => ({
|
jest.doMock("@azure/identity", () => ({
|
||||||
DefaultAzureCredential: jest.fn(),
|
DefaultAzureCredential: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
AzureAISearch =
|
AzureAISearch = require("../src/vector_stores/azure_ai_search").AzureAISearch;
|
||||||
require("../src/vector_stores/azure_ai_search").AzureAISearch;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -692,9 +661,7 @@ describe("Vectorize – backward compat with mocked client", () => {
|
|||||||
jest.doMock("cloudflare", () => {
|
jest.doMock("cloudflare", () => {
|
||||||
const mockIndexes = {
|
const mockIndexes = {
|
||||||
list: jest.fn().mockReturnValue({
|
list: jest.fn().mockReturnValue({
|
||||||
[Symbol.asyncIterator]: () => ({
|
[Symbol.asyncIterator]: () => ({ next: async () => ({ done: true }) }),
|
||||||
next: async () => ({ done: true }),
|
|
||||||
}),
|
|
||||||
}),
|
}),
|
||||||
create: jest.fn().mockResolvedValue({}),
|
create: jest.fn().mockResolvedValue({}),
|
||||||
delete: jest.fn().mockResolvedValue({}),
|
delete: jest.fn().mockResolvedValue({}),
|
||||||
@@ -805,14 +772,9 @@ describe("LangchainVectorStore – backward compat", () => {
|
|||||||
const { LangchainVectorStore } = require("../src/vector_stores/langchain");
|
const { LangchainVectorStore } = require("../src/vector_stores/langchain");
|
||||||
const mockLcStore = {
|
const mockLcStore = {
|
||||||
addVectors: jest.fn().mockResolvedValue(undefined),
|
addVectors: jest.fn().mockResolvedValue(undefined),
|
||||||
similaritySearchVectorWithScore: jest
|
similaritySearchVectorWithScore: jest.fn().mockResolvedValue([
|
||||||
.fn()
|
[{ metadata: { _mem0_id: "id-1", data: "test" }, pageContent: "" }, 0.95],
|
||||||
.mockResolvedValue([
|
]),
|
||||||
[
|
|
||||||
{ metadata: { _mem0_id: "id-1", data: "test" }, pageContent: "" },
|
|
||||||
0.95,
|
|
||||||
],
|
|
||||||
]),
|
|
||||||
};
|
};
|
||||||
const store = new LangchainVectorStore({
|
const store = new LangchainVectorStore({
|
||||||
client: mockLcStore,
|
client: mockLcStore,
|
||||||
@@ -859,9 +821,9 @@ describe("LangchainVectorStore – backward compat", () => {
|
|||||||
dimension: 4,
|
dimension: 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(store.insert([[1, 2, 3]], ["id-1"], [{}])).rejects.toThrow(
|
await expect(
|
||||||
"Vector dimension mismatch",
|
store.insert([[1, 2, 3]], ["id-1"], [{}]),
|
||||||
);
|
).rejects.toThrow("Vector dimension mismatch");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1014,27 +976,11 @@ describe("Memory class – backward compat with all providers", () => {
|
|||||||
]);
|
]);
|
||||||
mockVStore.get.mockResolvedValue({
|
mockVStore.get.mockResolvedValue({
|
||||||
id: "id-1",
|
id: "id-1",
|
||||||
payload: {
|
payload: { memory: "test", hash: "h", created_at: new Date().toISOString(), updated_at: new Date().toISOString() },
|
||||||
memory: "test",
|
|
||||||
hash: "h",
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
mockVStore.list.mockResolvedValue([
|
mockVStore.list.mockResolvedValue([[
|
||||||
[
|
{ id: "id-1", payload: { memory: "test", hash: "h", created_at: new Date().toISOString(), updated_at: new Date().toISOString() } },
|
||||||
{
|
], 1]);
|
||||||
id: "id-1",
|
|
||||||
payload: {
|
|
||||||
memory: "test",
|
|
||||||
hash: "h",
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
1,
|
|
||||||
]);
|
|
||||||
mockVectorStoreFactory.create.mockReturnValue(mockVStore);
|
mockVectorStoreFactory.create.mockReturnValue(mockVStore);
|
||||||
|
|
||||||
const mem = new MemoryClass({
|
const mem = new MemoryClass({
|
||||||
@@ -1109,9 +1055,7 @@ describe("Memory class – backward compat with all providers", () => {
|
|||||||
};
|
};
|
||||||
mockEmbedderFactory.create.mockReturnValue(failingEmbedder);
|
mockEmbedderFactory.create.mockReturnValue(failingEmbedder);
|
||||||
|
|
||||||
const consoleSpy = jest
|
const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {});
|
||||||
.spyOn(console, "error")
|
|
||||||
.mockImplementation(() => {});
|
|
||||||
|
|
||||||
const mem = new MemoryClass({
|
const mem = new MemoryClass({
|
||||||
embedder: { provider: "ollama", config: { model: "test" } },
|
embedder: { provider: "ollama", config: { model: "test" } },
|
||||||
@@ -1133,3 +1077,4 @@ describe("Memory class – backward compat with all providers", () => {
|
|||||||
consoleSpy.mockRestore();
|
consoleSpy.mockRestore();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ class MemoryGraph:
|
|||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.graph = Neo4jGraph(
|
self.graph = Neo4jGraph(
|
||||||
url=self.config.graph_store.config.url,
|
self.config.graph_store.config.url,
|
||||||
username=self.config.graph_store.config.username,
|
self.config.graph_store.config.username,
|
||||||
password=self.config.graph_store.config.password,
|
self.config.graph_store.config.password,
|
||||||
database=self.config.graph_store.config.database,
|
self.config.graph_store.config.database,
|
||||||
refresh_schema=False,
|
refresh_schema=False,
|
||||||
driver_config={"notifications_min_severity": "OFF"},
|
driver_config={"notifications_min_severity": "OFF"},
|
||||||
)
|
)
|
||||||
|
|||||||
+33
-34
@@ -24,7 +24,7 @@ from mem0.exceptions import ValidationError as Mem0ValidationError
|
|||||||
from mem0.memory.base import MemoryBase
|
from mem0.memory.base import MemoryBase
|
||||||
from mem0.memory.setup import mem0_dir, setup_config
|
from mem0.memory.setup import mem0_dir, setup_config
|
||||||
from mem0.memory.storage import SQLiteManager
|
from mem0.memory.storage import SQLiteManager
|
||||||
from mem0.memory.telemetry import MEM0_TELEMETRY, capture_event
|
from mem0.memory.telemetry import capture_event
|
||||||
from mem0.memory.utils import (
|
from mem0.memory.utils import (
|
||||||
extract_json,
|
extract_json,
|
||||||
get_fact_retrieval_messages,
|
get_fact_retrieval_messages,
|
||||||
@@ -37,8 +37,8 @@ from mem0.utils.factory import (
|
|||||||
EmbedderFactory,
|
EmbedderFactory,
|
||||||
GraphStoreFactory,
|
GraphStoreFactory,
|
||||||
LlmFactory,
|
LlmFactory,
|
||||||
RerankerFactory,
|
|
||||||
VectorStoreFactory,
|
VectorStoreFactory,
|
||||||
|
RerankerFactory,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Suppress SWIG deprecation warnings globally
|
# Suppress SWIG deprecation warnings globally
|
||||||
@@ -204,33 +204,32 @@ class Memory(MemoryBase):
|
|||||||
self.enable_graph = True
|
self.enable_graph = True
|
||||||
else:
|
else:
|
||||||
self.graph = None
|
self.graph = None
|
||||||
if MEM0_TELEMETRY:
|
# Create telemetry config manually to avoid deepcopy issues with thread locks
|
||||||
# Create telemetry config manually to avoid deepcopy issues with thread locks
|
telemetry_config_dict = {}
|
||||||
telemetry_config_dict = {}
|
if hasattr(self.config.vector_store.config, 'model_dump'):
|
||||||
if hasattr(self.config.vector_store.config, 'model_dump'):
|
# For pydantic models
|
||||||
# For pydantic models
|
telemetry_config_dict = self.config.vector_store.config.model_dump()
|
||||||
telemetry_config_dict = self.config.vector_store.config.model_dump()
|
else:
|
||||||
else:
|
# For other objects, manually copy common attributes
|
||||||
# For other objects, manually copy common attributes
|
for attr in ['host', 'port', 'path', 'api_key', 'index_name', 'dimension', 'metric']:
|
||||||
for attr in ['host', 'port', 'path', 'api_key', 'index_name', 'dimension', 'metric']:
|
if hasattr(self.config.vector_store.config, attr):
|
||||||
if hasattr(self.config.vector_store.config, attr):
|
telemetry_config_dict[attr] = getattr(self.config.vector_store.config, attr)
|
||||||
telemetry_config_dict[attr] = getattr(self.config.vector_store.config, attr)
|
|
||||||
|
|
||||||
# Override collection name for telemetry
|
# Override collection name for telemetry
|
||||||
telemetry_config_dict['collection_name'] = "mem0migrations"
|
telemetry_config_dict['collection_name'] = "mem0migrations"
|
||||||
|
|
||||||
# Set path for file-based vector stores
|
# Set path for file-based vector stores
|
||||||
telemetry_config = _safe_deepcopy_config(self.config.vector_store.config)
|
telemetry_config = _safe_deepcopy_config(self.config.vector_store.config)
|
||||||
if self.config.vector_store.provider in ["faiss", "qdrant"]:
|
if self.config.vector_store.provider in ["faiss", "qdrant"]:
|
||||||
provider_path = f"migrations_{self.config.vector_store.provider}"
|
provider_path = f"migrations_{self.config.vector_store.provider}"
|
||||||
telemetry_config_dict['path'] = os.path.join(mem0_dir, provider_path)
|
telemetry_config_dict['path'] = os.path.join(mem0_dir, provider_path)
|
||||||
os.makedirs(telemetry_config_dict['path'], exist_ok=True)
|
os.makedirs(telemetry_config_dict['path'], exist_ok=True)
|
||||||
|
|
||||||
# Create the config object using the same class as the original
|
# Create the config object using the same class as the original
|
||||||
telemetry_config = self.config.vector_store.config.__class__(**telemetry_config_dict)
|
telemetry_config = self.config.vector_store.config.__class__(**telemetry_config_dict)
|
||||||
self._telemetry_vector_store = VectorStoreFactory.create(
|
self._telemetry_vector_store = VectorStoreFactory.create(
|
||||||
self.config.vector_store.provider, telemetry_config
|
self.config.vector_store.provider, telemetry_config
|
||||||
)
|
)
|
||||||
capture_event("mem0.init", self, {"sync_type": "sync"})
|
capture_event("mem0.init", self, {"sync_type": "sync"})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1273,14 +1272,14 @@ class AsyncMemory(MemoryBase):
|
|||||||
else:
|
else:
|
||||||
self.graph = None
|
self.graph = None
|
||||||
|
|
||||||
if MEM0_TELEMETRY:
|
telemetry_config = _safe_deepcopy_config(self.config.vector_store.config)
|
||||||
telemetry_config = _safe_deepcopy_config(self.config.vector_store.config)
|
telemetry_config.collection_name = "mem0migrations"
|
||||||
telemetry_config.collection_name = "mem0migrations"
|
if self.config.vector_store.provider in ["faiss", "qdrant"]:
|
||||||
if self.config.vector_store.provider in ["faiss", "qdrant"]:
|
provider_path = f"migrations_{self.config.vector_store.provider}"
|
||||||
provider_path = f"migrations_{self.config.vector_store.provider}"
|
telemetry_config.path = os.path.join(mem0_dir, provider_path)
|
||||||
telemetry_config.path = os.path.join(mem0_dir, provider_path)
|
os.makedirs(telemetry_config.path, exist_ok=True)
|
||||||
os.makedirs(telemetry_config.path, exist_ok=True)
|
self._telemetry_vector_store = VectorStoreFactory.create(self.config.vector_store.provider, telemetry_config)
|
||||||
self._telemetry_vector_store = VectorStoreFactory.create(self.config.vector_store.provider, telemetry_config)
|
|
||||||
capture_event("mem0.init", self, {"sync_type": "async"})
|
capture_event("mem0.init", self, {"sync_type": "async"})
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
package-manager-strict-version=false
|
|
||||||
approve-builds=esbuild
|
|
||||||
+1
-1
@@ -138,7 +138,7 @@ class PlatformProvider implements Mem0Provider {
|
|||||||
|
|
||||||
private async _init(): Promise<void> {
|
private async _init(): Promise<void> {
|
||||||
const { default: MemoryClient } = await import("mem0ai");
|
const { default: MemoryClient } = await import("mem0ai");
|
||||||
const opts: { apiKey: string; org_id?: string; project_id?: string } = { apiKey: this.apiKey };
|
const opts: Record<string, string> = { apiKey: this.apiKey };
|
||||||
if (this.orgId) opts.org_id = this.orgId;
|
if (this.orgId) opts.org_id = this.orgId;
|
||||||
if (this.projectId) opts.project_id = this.projectId;
|
if (this.projectId) opts.project_id = this.projectId;
|
||||||
this.client = new MemoryClient(opts);
|
this.client = new MemoryClient(opts);
|
||||||
|
|||||||
Vendored
-30
@@ -1,30 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-18
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mem0/openclaw-mem0",
|
"name": "@mem0/openclaw-mem0",
|
||||||
"version": "0.3.3",
|
"version": "0.3.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
|
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
@@ -11,20 +11,7 @@
|
|||||||
"mem0",
|
"mem0",
|
||||||
"long-term-memory"
|
"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": {
|
"scripts": {
|
||||||
"build": "tsup",
|
|
||||||
"test": "vitest run"
|
"test": "vitest run"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -33,13 +20,10 @@
|
|||||||
},
|
},
|
||||||
"openclaw": {
|
"openclaw": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"./dist/index.js"
|
"./index.ts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.15.0",
|
|
||||||
"tsup": "^8.5.0",
|
|
||||||
"typescript": "^5.8.3",
|
|
||||||
"vitest": "^4.0.18"
|
"vitest": "^4.0.18"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
-4105
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
|||||||
approveBuilds: esbuild
|
|
||||||
|
|
||||||
onlyBuiltDependencies:
|
|
||||||
- better-sqlite3
|
|
||||||
- esbuild
|
|
||||||
- protobufjs
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { defineConfig } from "tsup";
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
entry: ["index.ts"],
|
|
||||||
format: ["esm"],
|
|
||||||
dts: true,
|
|
||||||
sourcemap: true,
|
|
||||||
clean: true,
|
|
||||||
});
|
|
||||||
+1
-1
@@ -20,7 +20,7 @@ dependencies = [
|
|||||||
"posthog>=3.5.0",
|
"posthog>=3.5.0",
|
||||||
"pytz>=2024.1",
|
"pytz>=2024.1",
|
||||||
"sqlalchemy>=2.0.31",
|
"sqlalchemy>=2.0.31",
|
||||||
"protobuf>=5.29.6,<7.0.0",
|
"protobuf>=5.29.0,<6.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@@ -296,43 +296,3 @@ def test_custom_prompts(memory_custom_instance):
|
|||||||
messages=[{"role": "user", "content": mock_get_update_memory_messages.return_value}],
|
messages=[{"role": "user", "content": mock_get_update_memory_messages.return_value}],
|
||||||
response_format={"type": "json_object"},
|
response_format={"type": "json_object"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_no_telemetry_vector_store_when_disabled():
|
|
||||||
"""VectorStoreFactory should only be called once (for user data) when telemetry is disabled."""
|
|
||||||
with (
|
|
||||||
patch("mem0.memory.main.MEM0_TELEMETRY", False),
|
|
||||||
patch("mem0.utils.factory.EmbedderFactory") as mock_embedder,
|
|
||||||
patch("mem0.memory.main.VectorStoreFactory") as mock_vector_store,
|
|
||||||
patch("mem0.utils.factory.LlmFactory") as mock_llm,
|
|
||||||
patch("mem0.memory.telemetry.capture_event"),
|
|
||||||
):
|
|
||||||
mock_embedder.create.return_value = Mock()
|
|
||||||
mock_vector_store.create.return_value = Mock()
|
|
||||||
mock_llm.create.return_value = Mock()
|
|
||||||
|
|
||||||
config = MemoryConfig(version="v1.1")
|
|
||||||
Memory(config)
|
|
||||||
|
|
||||||
# VectorStoreFactory.create should be called exactly once — for user data only, not telemetry
|
|
||||||
assert mock_vector_store.create.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_telemetry_vector_store_created_when_enabled():
|
|
||||||
"""VectorStoreFactory should be called twice (user data + telemetry) when telemetry is enabled."""
|
|
||||||
with (
|
|
||||||
patch("mem0.memory.main.MEM0_TELEMETRY", True),
|
|
||||||
patch("mem0.utils.factory.EmbedderFactory") as mock_embedder,
|
|
||||||
patch("mem0.memory.main.VectorStoreFactory") as mock_vector_store,
|
|
||||||
patch("mem0.utils.factory.LlmFactory") as mock_llm,
|
|
||||||
patch("mem0.memory.telemetry.capture_event"),
|
|
||||||
):
|
|
||||||
mock_embedder.create.return_value = Mock()
|
|
||||||
mock_vector_store.create.return_value = Mock()
|
|
||||||
mock_llm.create.return_value = Mock()
|
|
||||||
|
|
||||||
config = MemoryConfig(version="v1.1")
|
|
||||||
Memory(config)
|
|
||||||
|
|
||||||
# VectorStoreFactory.create should be called twice — user data + telemetry
|
|
||||||
assert mock_vector_store.create.call_count == 2
|
|
||||||
|
|||||||
Reference in New Issue
Block a user