diff --git a/docs/api-reference/memory/add-memories.mdx b/docs/api-reference/memory/add-memories.mdx index a44c7d384..146f1ea07 100644 --- a/docs/api-reference/memory/add-memories.mdx +++ b/docs/api-reference/memory/add-memories.mdx @@ -47,7 +47,6 @@ Provide at least one message or direct memory string. Most callers supply `messa | `messages` | array | No* | Conversation turns for Mem0 to infer memories from. Each object should include `role` and `content`. | | `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). | | `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. | -| `enable_graph` | boolean (default `false`) | Optional | Set to `true` to create entity nodes and relationships for knowledge graph. | > \* Provide at least one `messages` entry to describe what you are storing. For scoped memories, include `user_id`. You can also attach `agent_id`, `app_id`, `run_id`, `project_id`, or `org_id` to refine ownership. @@ -82,20 +81,3 @@ Successful requests return an array of events queued for processing. Each event ``` -## Graph relationships - -Add Memories can enrich the knowledge graph on write. Set `enable_graph: true` to create entity nodes and relationships for the stored memory. Use this when you want downstream `get_all` or search calls to traverse connected entities. - - -```json Graph-aware request -{ - "user_id": "alice", - "messages": [ - { "role": "user", "content": "I met with Dr. Lee at General Hospital." } - ], - "enable_graph": true -} -``` - - -The response follows the same format, and related entities become available in [Graph Memory](/platform/features/graph-memory) queries. diff --git a/docs/api-reference/organizations-projects.mdx b/docs/api-reference/organizations-projects.mdx index 22bd9350c..cbdf1f162 100644 --- a/docs/api-reference/organizations-projects.mdx +++ b/docs/api-reference/organizations-projects.mdx @@ -95,9 +95,6 @@ client.project.update( custom_instructions="..." ) -# Enable graph memory for the project -client.project.update(enable_graph=True) - # Use the input language for memory storage and retrieval client.project.update(multilingual=True) @@ -108,7 +105,6 @@ client.project.update( {"personal_info": "User personal information and preferences"}, {"work_context": "Professional context and work-related information"} ], - enable_graph=True, multilingual=True ) ``` @@ -173,7 +169,7 @@ async def manage_project(): # All methods support async/await project_info = await client.project.get() - await client.project.update(enable_graph=True) + await client.project.update(multilingual=True) members = await client.project.get_members() # To call the async function properly diff --git a/docs/cookbooks/integrations/aws-bedrock.mdx b/docs/cookbooks/integrations/aws-bedrock.mdx index e43ea9599..bf6a46b08 100644 --- a/docs/cookbooks/integrations/aws-bedrock.mdx +++ b/docs/cookbooks/integrations/aws-bedrock.mdx @@ -137,7 +137,7 @@ With Mem0 and AWS services like Bedrock, OpenSearch, and Neptune Analytics, you Explore graph-based memory storage with AWS Neptune Analytics. - + Learn how to leverage knowledge graphs for entity relationships. diff --git a/docs/core-concepts/memory-operations/add.mdx b/docs/core-concepts/memory-operations/add.mdx index 28aa3fe59..45adc300d 100644 --- a/docs/core-concepts/memory-operations/add.mdx +++ b/docs/core-concepts/memory-operations/add.mdx @@ -172,7 +172,6 @@ For full list of supported fields, required formats, and advanced options, see t | Capability | Mem0 Platform | Mem0 OSS | | --- | --- | --- | | Conflict resolution | Automatic with dashboard visibility | SDK handles merges locally; you control storage | -| Graph writes | Toggle per request (`enable_graph=True`) | Requires configuring a graph provider | | Rate limits | Managed quotas per workspace | Limited by your hardware and provider APIs | | Dashboard visibility | Yes — inspect memories visually | Inspect via CLI, logs, or custom UI | diff --git a/docs/core-concepts/memory-operations/delete.mdx b/docs/core-concepts/memory-operations/delete.mdx index 3b07b0ca9..bb9801eeb 100644 --- a/docs/core-concepts/memory-operations/delete.mdx +++ b/docs/core-concepts/memory-operations/delete.mdx @@ -120,7 +120,7 @@ import MemoryClient from 'mem0ai'; const client = new MemoryClient({ apiKey: "your-api-key" }); -client.deleteAll({ user_id: "alice" }) +client.deleteAll({ userId: "alice" }) .then(result => console.log(result)) .catch(error => console.error(error)); ``` @@ -162,12 +162,12 @@ import MemoryClient from 'mem0ai'; const client = new MemoryClient({ apiKey: "your-api-key" }); // Delete all memories across every user in the project -client.deleteAll({ user_id: "*" }) +client.deleteAll({ userId: "*" }) .then(result => console.log(result)) .catch(error => console.error(error)); // Full project wipe — all four filters must be explicitly set to "*" -client.deleteAll({ user_id: "*", agent_id: "*", app_id: "*", run_id: "*" }) +client.deleteAll({ userId: "*", agentId: "*", appId: "*", runId: "*" }) .then(result => console.log(result)) .catch(error => console.error(error)); ``` diff --git a/docs/docs.json b/docs/docs.json index 18bb24613..fcf12d777 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -78,7 +78,6 @@ "group": "Advanced Features", "icon": "bolt", "pages": [ - "platform/features/graph-memory", "platform/features/graph-threshold", "platform/features/advanced-retrieval", "platform/advanced-memory-operations", @@ -633,6 +632,10 @@ "source": "/open-source/features/custom-fact-extraction-prompt", "destination": "/open-source/features/custom-instructions" }, + { + "source": "/platform/features/graph-memory", + "destination": "/open-source/features/graph-memory" + }, { "source": "/changelog", "destination": "/changelog/highlights" @@ -987,7 +990,7 @@ }, { "source": "/features/graph-memory", - "destination": "/platform/features/graph-memory" + "destination": "/open-source/features/graph-memory" }, { "source": "/features/:slug", diff --git a/docs/open-source/features/graph-memory.mdx b/docs/open-source/features/graph-memory.mdx index a9ef43768..ed5a74f99 100644 --- a/docs/open-source/features/graph-memory.mdx +++ b/docs/open-source/features/graph-memory.mdx @@ -123,7 +123,6 @@ export NEO4J_PASSWORD="your-password" import { Memory } from "mem0ai/oss"; const config = { - enableGraph: true, graphStore: { provider: "neo4j", config: { @@ -196,7 +195,6 @@ memory = Memory.from_config(config_dict=config) import { Memory } from "mem0ai/oss"; const config = { - enableGraph: true, graphStore: { provider: "neo4j", config: { @@ -217,14 +215,6 @@ const memory = new Memory(config); ```python config["graph_store"]["config"]["threshold"] = 0.75 -``` - - - Disable graph writes or reads when you only want vector behaviour. - -```python -memory.add(messages, user_id="demo-user", enable_graph=False) -results = memory.search("marketing partners", user_id="demo-user", enable_graph=False) ``` @@ -257,9 +247,6 @@ Monitor graph growth, especially on free tiers, by periodically cleaning dormant Ensure the graph identifier matches the vector dimension used by your embedder and that the IAM role allows `neptune-graph:*DataViaQuery` actions. - - Catch the provider error and retry with `enable_graph=False` so vector-only search keeps serving responses while the graph backend recovers. - ## Decision Points @@ -280,7 +267,6 @@ Choose your backend and expand the matching panel for configuration details and import { Memory } from "mem0ai/oss"; const config = { - enableGraph: true, graphStore: { provider: "neo4j", config: { diff --git a/docs/open-source/features/multimodal-support.mdx b/docs/open-source/features/multimodal-support.mdx index 776e6a53d..a01cec4aa 100644 --- a/docs/open-source/features/multimodal-support.mdx +++ b/docs/open-source/features/multimodal-support.mdx @@ -81,7 +81,7 @@ const messages = [ } ]; -await client.add(messages, { user_id: "alice" }); +await client.add(messages, { userId: "alice" }); ``` @@ -148,7 +148,7 @@ const messages = [ } ]; -await client.add(messages, { user_id: "alice" }); +await client.add(messages, { userId: "alice" }); ``` @@ -264,7 +264,7 @@ try { } }]; - await client.add(messages, { user_id: "user123" }); + await client.add(messages, { userId: "user123" }); console.log("Image processed successfully"); } catch (error: any) { if (error.type === "invalid_image") { diff --git a/docs/open-source/node-quickstart.mdx b/docs/open-source/node-quickstart.mdx index e42fcf20b..2e3aa2740 100644 --- a/docs/open-source/node-quickstart.mdx +++ b/docs/open-source/node-quickstart.mdx @@ -209,14 +209,6 @@ Mem0 offers granular configuration across vector stores, LLMs, embedders, and hi | `topP` | Probability threshold | All | | `topK` | Token count to keep | All | | `openaiBaseUrl` | Base URL override | OpenAI | - - -| Parameter | Description | Default | -| --- | --- | --- | -| `provider` | Graph store provider (e.g., `"neo4j"`) | `"neo4j"` | -| `url` | Connection URL | `process.env.NEO4J_URL` | -| `username` | Username | `process.env.NEO4J_USERNAME` | -| `password` | Password | `process.env.NEO4J_PASSWORD` | | Parameter | Description | Default | diff --git a/docs/platform/advanced-memory-operations.mdx b/docs/platform/advanced-memory-operations.mdx index c8c3a14b6..36c47a78c 100644 --- a/docs/platform/advanced-memory-operations.mdx +++ b/docs/platform/advanced-memory-operations.mdx @@ -81,7 +81,6 @@ result = await memory.add( conversation, user_id="traveler-42", metadata={"trip": "japan-2025", "preferences": ["boutique", "no-shellfish"]}, - enable_graph=True, run_id="planning-call-1", ) ``` @@ -101,7 +100,6 @@ const conversation = [ const result = await memory.add(conversation, { userId: "traveler-42", metadata: { trip: "japan-2025", preferences: ["boutique", "no-shellfish"] }, - enableGraph: true, runId: "planning-call-1", }); ``` @@ -163,10 +161,6 @@ await memory.update(matches.results[0].id, { - - Need to pause graph writes on a per-request basis? Pass `enableGraph: false` (TypeScript) or `enable_graph=False` (Python) when latency matters more than relationship building. - - ## Clean up @@ -192,8 +186,6 @@ await memory.deleteAll({ userId: "traveler-42", runId: "planning-call-1" }); ## Quick recovery -- `Missing required key enableGraph`: update the SDK to `mem0ai>=0.4.0`. -- `Graph backend unavailable`: retry with `enableGraph=False` and inspect your graph provider status. - Empty results with filters: log `filters` values and confirm metadata keys match (case-sensitive). diff --git a/docs/platform/features/async-client.mdx b/docs/platform/features/async-client.mdx index 51c73a333..e08283598 100644 --- a/docs/platform/features/async-client.mdx +++ b/docs/platform/features/async-client.mdx @@ -50,7 +50,7 @@ const messages = [ {"role": "user", "content": "Alice loves playing badminton"}, {"role": "assistant", "content": "That's great! Alice is a fitness freak"}, ]; -await client.add(messages, { user_id: "alice" }); +await client.add(messages, { userId: "alice" }); ``` @@ -66,7 +66,7 @@ await client.search("What is Alice's favorite sport?", user_id="alice") ``` ```javascript JavaScript -await client.search("What is Alice's favorite sport?", { user_id: "alice" }); +await client.search("What is Alice's favorite sport?", { userId: "alice" }); ``` @@ -118,7 +118,7 @@ await client.delete_all(user_id="alice") ``` ```javascript JavaScript -await client.deleteAll({ user_id: "alice" }); +await client.deleteAll({ userId: "alice" }); ``` diff --git a/docs/platform/features/contextual-add.mdx b/docs/platform/features/contextual-add.mdx index ce3d9dbce..1344e1ca3 100644 --- a/docs/platform/features/contextual-add.mdx +++ b/docs/platform/features/contextual-add.mdx @@ -25,7 +25,7 @@ const messages = [ {"role": "assistant", "content": "Great! I'll remember your preference for Italian cuisine."} ]; -await client.add(messages, { user_id: "user123", version: "v2" }); +await client.add(messages, { userId: "user123", version: "v2" }); ``` @@ -65,14 +65,14 @@ const messages1 = [ {"role": "user", "content": "Hi, I'm Sarah from New York"}, {"role": "assistant", "content": "Hello Sarah! Nice to meet you."} ]; -await client.add(messages1, { user_id: "sarah", version: "v2" }); +await client.add(messages1, { userId: "sarah", version: "v2" }); // Later interaction - just send new messages const messages2 = [ {"role": "user", "content": "I'm planning a trip to Italy next month"}, {"role": "assistant", "content": "How exciting! Italy is beautiful this time of year."} ]; -await client.add(messages2, { user_id: "sarah", version: "v2" }); +await client.add(messages2, { userId: "sarah", version: "v2" }); // Mem0 automatically knows Sarah is from New York and can use this context ``` @@ -104,7 +104,7 @@ const messages = [ {"role": "assistant", "content": "I've noted your allergies for future reference."} ]; -await client.add(messages, { user_id: "user123", version: "v2" }); +await client.add(messages, { userId: "user123", version: "v2" }); // This allergy info will be available in ALL future interactions ``` @@ -143,21 +143,21 @@ const messages1 = [ {"role": "user", "content": "I want to plan a 5-day trip to Tokyo"}, {"role": "assistant", "content": "Perfect! Let's plan your Tokyo adventure."} ]; -await client.add(messages1, { user_id: "user123", run_id: "tokyo-trip-2024", version: "v2" }); +await client.add(messages1, { userId: "user123", runId: "tokyo-trip-2024", version: "v2" }); // Later in the same trip planning session const messages2 = [ {"role": "user", "content": "I prefer staying near Shibuya"}, {"role": "assistant", "content": "Great choice! Shibuya is very convenient."} ]; -await client.add(messages2, { user_id: "user123", run_id: "tokyo-trip-2024", version: "v2" }); +await client.add(messages2, { userId: "user123", runId: "tokyo-trip-2024", version: "v2" }); // Different session for work project (separate context) const workMessages = [ {"role": "user", "content": "Let's discuss the Q4 marketing strategy"}, {"role": "assistant", "content": "Sure! What are your main goals for Q4?"} ]; -await client.add(workMessages, { user_id: "user123", run_id: "q4-marketing", version: "v2" }); +await client.add(workMessages, { userId: "user123", runId: "q4-marketing", version: "v2" }); ``` diff --git a/docs/platform/features/custom-instructions.mdx b/docs/platform/features/custom-instructions.mdx index 6d6bae3c5..f406f6206 100644 --- a/docs/platform/features/custom-instructions.mdx +++ b/docs/platform/features/custom-instructions.mdx @@ -33,7 +33,7 @@ Extract only health and wellness information: Exclude: Personal identifiers, financial data `; -await client.project.update({ custom_instructions: prompt }); +await client.project.update({ customInstructions: prompt }); ``` @@ -60,11 +60,11 @@ print(response["custom_instructions"]) ```javascript JavaScript // Set instructions for your project -await client.project.update({ custom_instructions: "Your guidelines here..." }); +await client.project.update({ customInstructions: "Your guidelines here..." }); // Retrieve current instructions -const response = await client.project.get({ fields: ["custom_instructions"] }); -console.log(response.custom_instructions); +const response = await client.project.get({ fields: ["customInstructions"] }); +console.log(response.customInstructions); ``` @@ -145,7 +145,7 @@ Extract customer service information for better support: Exclude: Payment card numbers, passwords, personal identifiers. `; -await client.project.update({ custom_instructions: instructions }); +await client.project.update({ customInstructions: instructions }); ``` @@ -198,7 +198,7 @@ Extract learning-related information for personalized education: Exclude: Specific grades, personal identifiers, financial information. `; -await client.project.update({ custom_instructions: educationPrompt }); +await client.project.update({ customInstructions: educationPrompt }); ``` @@ -251,7 +251,7 @@ Extract financial planning information for advisory services: Exclude: Account numbers, SSNs, passwords, specific financial amounts. `; -await client.project.update({ custom_instructions: financePrompt }); +await client.project.update({ customInstructions: financePrompt }); ``` diff --git a/docs/platform/features/graph-memory.mdx b/docs/platform/features/graph-memory.mdx deleted file mode 100644 index 8bb947667..000000000 --- a/docs/platform/features/graph-memory.mdx +++ /dev/null @@ -1,333 +0,0 @@ ---- -title: Graph Memory -description: "Enable graph-based memory retrieval for more contextually relevant results" ---- - -## Overview - -Graph Memory enhances the memory pipeline by creating relationships between entities in your data. It builds a network of interconnected information for more contextually relevant search results. - -This feature allows your AI applications to understand connections between entities, providing richer context for responses. It's ideal for applications needing relationship tracking and nuanced information retrieval across related memories. - -## How Graph Memory Works - -The Graph Memory feature analyzes how each entity connects and relates to each other. When enabled: - -1. Mem0 automatically builds a graph representation of entities -2. Vector search returns the top semantic matches (with any reranker you configure) -3. Graph relations are returned alongside those results to provide additional context—they do not reorder the vector hits - -## Using Graph Memory - -To use Graph Memory, you need to enable it in your API calls by setting the `enable_graph=True` parameter. - -### Adding Memories with Graph Memory - -When adding new memories, enable Graph Memory to automatically build relationships with existing memories: - - - -```python Python -from mem0 import MemoryClient - -client = MemoryClient(api_key="your-api-key") - -messages = [ - {"role": "user", "content": "My name is Joseph"}, - {"role": "assistant", "content": "Hello Joseph, it's nice to meet you!"}, - {"role": "user", "content": "I'm from Seattle and I work as a software engineer"} -] - -# Enable graph memory when adding -client.add( - messages, - user_id="joseph", - enable_graph=True -) -``` - -```javascript JavaScript -import { MemoryClient } from "mem0"; - -const client = new MemoryClient({ apiKey: "your-api-key" }); - -const messages = [ - { role: "user", content: "My name is Joseph" }, - { role: "assistant", content: "Hello Joseph, it's nice to meet you!" }, - { role: "user", content: "I'm from Seattle and I work as a software engineer" } -]; - -// Enable graph memory when adding -await client.add({ - messages, - user_id: "joseph", - enable_graph: true -}); -``` - -```json Output -{ - "results": [ - { - "memory": "Name is Joseph", - "event": "ADD", - "id": "4a5a417a-fa10-43b5-8c53-a77c45e80438" - }, - { - "memory": "Is from Seattle", - "event": "ADD", - "id": "8d268d0f-5452-4714-b27d-ae46f676a49d" - }, - { - "memory": "Is a software engineer", - "event": "ADD", - "id": "5f0a184e-ddea-4fe6-9b92-692d6a901df8" - } - ] -} -``` - - -The graph memory would look like this: - - - Graph Memory Visualization showing relationships between entities - - -Graph Memory creates a network of relationships between entities, enabling more contextual retrieval - - - -Response for the graph memory's `add` operation will not be available directly in the response. As adding graph memories is an asynchronous operation due to heavy processing, you can use the `get_all()` endpoint to retrieve the memory with the graph metadata. - - - -### Searching with Graph Memory - -When searching memories, Graph Memory helps retrieve entities that are contextually important even if they're not direct semantic matches. - - - -```python Python -# Search with graph memory enabled -results = client.search( - "what is my name?", - user_id="joseph", - enable_graph=True -) - -print(results) -``` - -```javascript JavaScript -// Search with graph memory enabled -const results = await client.search({ - query: "what is my name?", - user_id: "joseph", - enable_graph: true -}); - -console.log(results); -``` - -```json Output -{ - "results": [ - { - "id": "4a5a417a-fa10-43b5-8c53-a77c45e80438", - "memory": "Name is Joseph", - "user_id": "joseph", - "metadata": null, - "categories": ["personal_details"], - "immutable": false, - "created_at": "2025-03-19T09:09:00.146390-07:00", - "updated_at": "2025-03-19T09:09:00.146404-07:00", - "score": 0.3621795393335552 - }, - { - "id": "8d268d0f-5452-4714-b27d-ae46f676a49d", - "memory": "Is from Seattle", - "user_id": "joseph", - "metadata": null, - "categories": ["personal_details"], - "immutable": false, - "created_at": "2025-03-19T09:09:00.170680-07:00", - "updated_at": "2025-03-19T09:09:00.170692-07:00", - "score": 0.31212713194651254 - } - ], - "relations": [ - { - "source": "joseph", - "source_type": "person", - "relationship": "name", - "target": "joseph", - "target_type": "person", - "score": 0.39 - } - ] -} -``` - - - - -`results` always reflects the vector search order (optionally reranked). Graph Memory augments that response by adding related entities in the `relations` array; it does not re-rank the vector results automatically. - - -### Retrieving All Memories with Graph Memory - -When retrieving all memories, Graph Memory provides additional relationship context: - - -`get_all()` now requires filters to be specified. - - - - -```python Python -# Get all memories with graph context -memories = client.get_all( - filters={"AND": [{"user_id": "joseph"}]}, - enable_graph=True -) - -print(memories) -``` - -```javascript JavaScript -// Get all memories with graph context -const memories = await client.getAll({ - filters: {"AND": [{"user_id": "joseph"}]}, - enable_graph: true -}); - -console.log(memories); -``` - -```json Output -{ - "results": [ - { - "id": "5f0a184e-ddea-4fe6-9b92-692d6a901df8", - "memory": "Is a software engineer", - "user_id": "joseph", - "metadata": null, - "categories": ["professional_details"], - "immutable": false, - "created_at": "2025-03-19T09:09:00.194116-07:00", - "updated_at": "2025-03-19T09:09:00.194128-07:00", - }, - { - "id": "8d268d0f-5452-4714-b27d-ae46f676a49d", - "memory": "Is from Seattle", - "user_id": "joseph", - "metadata": null, - "categories": ["personal_details"], - "immutable": false, - "created_at": "2025-03-19T09:09:00.170680-07:00", - "updated_at": "2025-03-19T09:09:00.170692-07:00", - }, - { - "id": "4a5a417a-fa10-43b5-8c53-a77c45e80438", - "memory": "Name is Joseph", - "user_id": "joseph", - "metadata": null, - "categories": ["personal_details"], - "immutable": false, - "created_at": "2025-03-19T09:09:00.146390-07:00", - "updated_at": "2025-03-19T09:09:00.146404-07:00", - } - ], - "relations": [ - { - "source": "joseph", - "source_type": "person", - "relationship": "name", - "target": "joseph", - "target_type": "person" - }, - { - "source": "joseph", - "source_type": "person", - "relationship": "city", - "target": "seattle", - "target_type": "city" - }, - { - "source": "joseph", - "source_type": "person", - "relationship": "job", - "target": "software engineer", - "target_type": "job" - } - ] -} -``` - - - -### Setting Graph Memory at Project Level - -Instead of passing `enable_graph=True` to every add call, you can enable it once at the project level: - - - -```python Python -from mem0 import MemoryClient - -client = MemoryClient(api_key="your-api-key") - -# Enable graph memory for all operations in this project -client.project.update(enable_graph=True) - -# Now all add operations will use graph memory by default -messages = [ - {"role": "user", "content": "My name is Joseph"}, - {"role": "assistant", "content": "Hello Joseph, it's nice to meet you!"}, - {"role": "user", "content": "I'm from Seattle and I work as a software engineer"} -] - -client.add( - messages, - user_id="joseph" -) -``` - -```javascript JavaScript -import { MemoryClient } from "mem0"; - -const client = new MemoryClient({ apiKey: "your-api-key" }); - -// Enable graph memory for all operations in this project -await client.project.update({ enable_graph: true }); - -// Now all add operations will use graph memory by default -const messages = [ - { role: "user", content: "My name is Joseph" }, - { role: "assistant", content: "Hello Joseph, it's nice to meet you!" }, - { role: "user", content: "I'm from Seattle and I work as a software engineer" } -]; - -await client.add({ - messages, - user_id: "joseph" -}); -``` - - - - -## Best Practices - -- Enable Graph Memory for applications where understanding context and relationships between memories is important. -- Graph Memory works best with a rich history of related conversations. -- Consider Graph Memory for long-running assistants that need to track evolving information. - -## Performance Considerations - -Graph Memory requires additional processing and may increase response times slightly for very large memory stores. However, for most use cases, the improved retrieval quality outweighs the minimal performance impact. - -If you have any questions, please feel free to reach out to us using one of the following methods: - - diff --git a/docs/platform/features/graph-threshold.mdx b/docs/platform/features/graph-threshold.mdx index 8bd841e66..5f9d5b546 100644 --- a/docs/platform/features/graph-threshold.mdx +++ b/docs/platform/features/graph-threshold.mdx @@ -206,5 +206,5 @@ config = {"graph_store": {"threshold": 0.7}} # Valid: 0.0 ≤ x ≤ 1.0 ## Related -- [Graph Memory](/platform/features/graph-memory) +- [Graph Memory](/open-source/features/graph-memory) - [Issue #3590](https://github.com/mem0ai/mem0/issues/3590) diff --git a/docs/platform/features/memory-export.mdx b/docs/platform/features/memory-export.mdx index 3d08a4cd5..a18773ea1 100644 --- a/docs/platform/features/memory-export.mdx +++ b/docs/platform/features/memory-export.mdx @@ -134,7 +134,7 @@ const filters = { const responseWithInstructions = await client.createMemoryExport({ schema: json_schema, filters: filters, - export_instructions: export_instructions + exportInstructions: export_instructions }); console.log(responseWithInstructions); @@ -176,10 +176,10 @@ print(response) ```javascript JavaScript // Retrieve using export ID -const memory_export_id = "550e8400-e29b-41d4-a716-446655440000"; +const memoryExportId = "550e8400-e29b-41d4-a716-446655440000"; const response = await client.getMemoryExport({ - memory_export_id: memory_export_id + memoryExportId: memoryExportId }); console.log(response); diff --git a/docs/platform/features/multimodal-support.mdx b/docs/platform/features/multimodal-support.mdx index b1d419ba1..24a3b2f4b 100644 --- a/docs/platform/features/multimodal-support.mdx +++ b/docs/platform/features/multimodal-support.mdx @@ -67,7 +67,7 @@ const messages = [ }, ] -await client.add(messages, { user_id: "alice" }) +await client.add(messages, { userId: "alice" }) ``` ```json Output @@ -166,7 +166,7 @@ const imageMessage = { } }; -await client.add([imageMessage], { user_id: "alice" }) +await client.add([imageMessage], { userId: "alice" }) ``` diff --git a/docs/platform/features/platform-overview.mdx b/docs/platform/features/platform-overview.mdx index 7376b7e20..8b947f9e8 100644 --- a/docs/platform/features/platform-overview.mdx +++ b/docs/platform/features/platform-overview.mdx @@ -20,7 +20,7 @@ Mem0 Platform features help managed deployments scale from basic filtering to gr Non-blocking add/search requests for agents. - + Relationship-aware recall across entities. console.log(response)) .catch(error => console.error(error)); ``` @@ -131,7 +131,7 @@ const january2023Timestamp = 1672531200; // Unix timestamp for 2023-01-01 00:00 const messages = [ {"role": "user", "content": "I'm travelling to SF"} ] -client.add(messages, { user_id: "user1", timestamp: january2023Timestamp }) +client.add(messages, { userId: "user1", timestamp: january2023Timestamp }) .then(response => console.log(response)) .catch(error => console.error(error)); ``` diff --git a/docs/platform/quickstart.mdx b/docs/platform/quickstart.mdx index f5737cdc4..fe1b8aa8f 100644 --- a/docs/platform/quickstart.mdx +++ b/docs/platform/quickstart.mdx @@ -68,7 +68,7 @@ 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" }); +await client.add(messages, { userId: "user123" }); ```` ```bash cURL diff --git a/mem0-ts/src/client/mem0.ts b/mem0-ts/src/client/mem0.ts index 51c658611..0200282b0 100644 --- a/mem0-ts/src/client/mem0.ts +++ b/mem0-ts/src/client/mem0.ts @@ -20,6 +20,7 @@ import { GetMemoryExportPayload, } from "./mem0.types"; import { captureClientEvent, generateHash } from "./telemetry"; +import { camelToSnake, camelToSnakeKeys, snakeToCamelKeys } from "./utils"; import { createExceptionFromResponse, MemoryError } from "../common/exceptions"; class APIError extends Error { @@ -123,7 +124,7 @@ export default class MemoryClient { throw createExceptionFromResponse(response.status, errorData); } const jsonResponse = await response.json(); - return jsonResponse; + return snakeToCamelKeys(jsonResponse); } _preparePayload( @@ -132,7 +133,7 @@ export default class MemoryClient { ): object { const payload: any = {}; payload.messages = messages; - return { ...payload, ...options }; + return camelToSnakeKeys({ ...payload, ...options }); } _prepareParams(options: Record): object { @@ -161,11 +162,11 @@ export default class MemoryClient { throw new APIError(response.message || "API Key is invalid"); } - const { org_id, project_id, user_email } = response; + const { orgId, projectId, userEmail } = response; - if (org_id) this.organizationId = org_id; - if (project_id) this.projectId = project_id; - if (user_email) this.telemetryId = user_email; + if (orgId) this.organizationId = orgId; + if (projectId) this.projectId = projectId; + if (userEmail) this.telemetryId = userEmail; } catch (error: any) { // Pass through structured exceptions and APIError if (error instanceof MemoryError || error instanceof APIError) { @@ -256,15 +257,15 @@ export default class MemoryClient { if (this.telemetryId === "") await this.ping(); const payloadKeys = Object.keys(options || {}); this._captureEvent("get_all", [payloadKeys]); - const { page, page_size, ...rest } = options ?? {}; + const { page, pageSize, ...rest } = options ?? {}; const body: Record = { output_format: "v1.1", - ...rest, + ...camelToSnakeKeys(rest), }; let url = `${this.host}/v2/memories/`; - if (page && page_size) { - url += `?page=${page}&page_size=${page_size}`; + if (page && pageSize) { + url += `?page=${page}&page_size=${pageSize}`; } const response = await this._fetchWithErrorHandling(url, { @@ -286,7 +287,7 @@ export default class MemoryClient { const payload: Record = { query, output_format: "v1.1", - ...(options ?? {}), + ...camelToSnakeKeys(options ?? {}), }; const response = await this._fetchWithErrorHandling( @@ -319,8 +320,9 @@ export default class MemoryClient { if (this.telemetryId === "") await this.ping(); const payloadKeys = Object.keys(options || {}); this._captureEvent("delete_all", [payloadKeys]); + const snakeOptions = camelToSnakeKeys(this._prepareParams(options)); // @ts-ignore - const params = new URLSearchParams(this._prepareParams(options)); + const params = new URLSearchParams(snakeOptions); const response = await this._fetchWithErrorHandling( `${this.host}/v1/memories/?${params}`, { @@ -345,14 +347,14 @@ export default class MemoryClient { async users(options?: { page?: number; - page_size?: number; + pageSize?: number; }): Promise { if (this.telemetryId === "") await this.ping(); this._captureEvent("users", []); let url = `${this.host}/v1/entities/`; const params: string[] = []; if (options?.page) params.push(`page=${options.page}`); - if (options?.page_size) params.push(`page_size=${options.page_size}`); + if (options?.pageSize) params.push(`page_size=${options.pageSize}`); if (params.length) url += `?${params.join("&")}`; const response = await this._fetchWithErrorHandling(url, { headers: this.headers, @@ -384,25 +386,25 @@ export default class MemoryClient { async deleteUsers( params: { - user_id?: string; - agent_id?: string; - app_id?: string; - run_id?: string; + userId?: string; + agentId?: string; + appId?: string; + runId?: string; } = {}, ): Promise<{ message: string }> { if (this.telemetryId === "") await this.ping(); let to_delete: Array<{ type: string; name: string }> = []; - const { user_id, agent_id, app_id, run_id } = params; + const { userId, agentId, appId, runId } = params; - if (user_id) { - to_delete = [{ type: "user", name: user_id }]; - } else if (agent_id) { - to_delete = [{ type: "agent", name: agent_id }]; - } else if (app_id) { - to_delete = [{ type: "app", name: app_id }]; - } else if (run_id) { - to_delete = [{ type: "run", name: run_id }]; + if (userId) { + to_delete = [{ type: "user", name: userId }]; + } else if (agentId) { + to_delete = [{ type: "agent", name: agentId }]; + } else if (appId) { + to_delete = [{ type: "app", name: appId }]; + } else if (runId) { + to_delete = [{ type: "run", name: runId }]; } else { const entities = await this.users(); to_delete = entities.results.map((entity) => ({ @@ -426,12 +428,12 @@ export default class MemoryClient { } this._captureEvent("delete_users", [ - { user_id, agent_id, app_id, run_id, sync_type: "sync" }, + { userId, agentId, appId, runId, sync_type: "sync" }, ]); return { message: - user_id || agent_id || app_id || run_id + userId || agentId || appId || runId ? "Entity deleted successfully." : "All users, agents, apps and runs deleted.", }; @@ -485,7 +487,7 @@ export default class MemoryClient { } const params = new URLSearchParams(); - fields?.forEach((field) => params.append("fields", field)); + fields?.forEach((field) => params.append("fields", camelToSnake(field))); const response = await this._fetchWithErrorHandling( `${this.host}/api/v1/orgs/organizations/${this.organizationId}/projects/${this.projectId}/?${params.toString()}`, @@ -512,7 +514,7 @@ export default class MemoryClient { { method: "PATCH", headers: this.headers, - body: JSON.stringify(prompts), + body: JSON.stringify(camelToSnakeKeys(prompts)), }, ); return response; @@ -596,7 +598,7 @@ export default class MemoryClient { { method: "POST", headers: this.headers, - body: JSON.stringify(data), + body: JSON.stringify(camelToSnakeKeys(data)), }, ); return response; @@ -617,7 +619,7 @@ export default class MemoryClient { { method: "POST", headers: this.headers, - body: JSON.stringify(data), + body: JSON.stringify(camelToSnakeKeys(data)), }, ); @@ -630,8 +632,8 @@ export default class MemoryClient { if (this.telemetryId === "") await this.ping(); this._captureEvent("get_memory_export", []); - if (!data.memory_export_id && !data.filters) { - throw new Error("Missing memory_export_id or filters"); + if (!data.memoryExportId && !data.filters) { + throw new Error("Missing memoryExportId or filters"); } const response = await this._fetchWithErrorHandling( @@ -639,7 +641,7 @@ export default class MemoryClient { { method: "POST", headers: this.headers, - body: JSON.stringify(data), + body: JSON.stringify(camelToSnakeKeys(data)), }, ); return response; diff --git a/mem0-ts/src/client/mem0.types.ts b/mem0-ts/src/client/mem0.types.ts index 342bab674..af1f67a72 100644 --- a/mem0-ts/src/client/mem0.types.ts +++ b/mem0-ts/src/client/mem0.types.ts @@ -1,41 +1,38 @@ // ─── Entity Options (for add/delete — top-level identity) ─── export interface EntityOptions { - user_id?: string; - agent_id?: string; - app_id?: string; - run_id?: string; + userId?: string; + agentId?: string; + appId?: string; + runId?: string; } // ─── Per-Method Options ───────────────────────────────────── export interface AddMemoryOptions extends EntityOptions { metadata?: Record; infer?: boolean; - custom_categories?: custom_categories[]; - custom_instructions?: string; + customCategories?: custom_categories[]; + customInstructions?: string; timestamp?: number; - structured_data_schema?: Record; - enable_graph?: boolean; + structuredDataSchema?: Record; } export interface SearchMemoryOptions { filters?: Record; metadata?: Record; - top_k?: number; + topK?: number; threshold?: number; rerank?: boolean; fields?: string[]; categories?: string[]; - enable_graph?: boolean; } export interface GetAllMemoryOptions { filters?: Record; page?: number; - page_size?: number; - start_date?: string; - end_date?: string; + pageSize?: number; + startDate?: string; + endDate?: string; categories?: string[]; - enable_graph?: boolean; } export interface DeleteAllMemoryOptions extends EntityOptions {} @@ -46,13 +43,12 @@ export interface ProjectOptions { } export interface PromptUpdatePayload { - custom_instructions?: string; - custom_categories?: custom_categories[]; - retrieval_criteria?: any[]; - enable_graph?: boolean; + customInstructions?: string; + customCategories?: custom_categories[]; + retrievalCriteria?: any[]; version?: string; - memory_depth?: string | null; - usecase_setting?: string | number; + memoryDepth?: string | null; + usecaseSetting?: string | number; multilingual?: boolean; [key: string]: any; } @@ -79,7 +75,7 @@ export interface Messages { export interface Message extends Messages {} -// ─── Response Types (reflect API shapes, unchanged) ───────── +// ─── Response Types (camelCase — converted from API snake_case) ───── export interface MemoryData { memory: string; } @@ -97,31 +93,31 @@ export interface Memory { event?: Event | string; data?: MemoryData | null; memory?: string; - user_id?: string; + userId?: string; hash?: string; categories?: Array; - created_at?: Date; - updated_at?: Date; - memory_type?: string; + createdAt?: Date; + updatedAt?: Date; + memoryType?: string; score?: number; metadata?: any | null; owner?: string | null; - agent_id?: string | null; - app_id?: string | null; - run_id?: string | null; + agentId?: string | null; + appId?: string | null; + runId?: string | null; } export interface MemoryHistory { id: string; - memory_id: string; + memoryId: string; input: Array; - old_memory: string | null; - new_memory: string | null; - user_id: string; + oldMemory: string | null; + newMemory: string | null; + userId: string; categories: Array; event: Event | string; - created_at: Date; - updated_at: Date; + createdAt: Date; + updatedAt: Date; } export interface MemoryUpdateBody { @@ -132,9 +128,9 @@ export interface MemoryUpdateBody { export interface User { id: string; name: string; - created_at: Date; - updated_at: Date; - total_memories: number; + createdAt: Date; + updatedAt: Date; + totalMemories: number; owner: string; type: string; } @@ -147,8 +143,8 @@ export interface AllUsers { } export interface ProjectResponse { - custom_instructions?: string; - custom_categories?: string[]; + customInstructions?: string; + customCategories?: string[]; [key: string]: any; } @@ -165,14 +161,14 @@ export enum WebhookEvent { } export interface Webhook { - webhook_id?: string; + webhookId?: string; name: string; url: string; project?: string; - created_at?: Date; - updated_at?: Date; - is_active?: boolean; - event_types?: WebhookEvent[]; + createdAt?: Date; + updatedAt?: Date; + isActive?: boolean; + eventTypes?: WebhookEvent[]; } export interface WebhookCreatePayload { @@ -190,18 +186,18 @@ export interface WebhookUpdatePayload { // ─── Feedback & Export Types ──────────────────────────────── export interface FeedbackPayload { - memory_id: string; + memoryId: string; feedback?: Feedback | null; - feedback_reason?: string | null; + feedbackReason?: string | null; } export interface CreateMemoryExportPayload { schema: Record; filters: Record; - export_instructions?: string; + exportInstructions?: string; } export interface GetMemoryExportPayload { filters?: Record; - memory_export_id?: string; + memoryExportId?: string; } diff --git a/mem0-ts/src/client/tests/integration/crud.test.ts b/mem0-ts/src/client/tests/integration/crud.test.ts index 2b6e2e771..134ba1c7e 100644 --- a/mem0-ts/src/client/tests/integration/crud.test.ts +++ b/mem0-ts/src/client/tests/integration/crud.test.ts @@ -51,7 +51,7 @@ describeIntegration("MemoryClient Integration — CRUD", () => { }, ]; - const result = await client.add(messages, { user_id: TEST_USER_ID }); + const result = await client.add(messages, { userId: TEST_USER_ID }); // API processes memories asynchronously — returns PENDING expect(Array.isArray(result)).toBe(true); @@ -60,7 +60,7 @@ describeIntegration("MemoryClient Integration — CRUD", () => { // Validate response shape for (const item of result) { expect(item).toHaveProperty("status"); - expect(item).toHaveProperty("event_id"); + expect(item).toHaveProperty("eventId"); } }); @@ -76,7 +76,7 @@ describeIntegration("MemoryClient Integration — CRUD", () => { }, ]; - const result = await client.add(messages, { user_id: TEST_USER_ID }); + const result = await client.add(messages, { userId: TEST_USER_ID }); expect(Array.isArray(result)).toBe(true); }); @@ -103,17 +103,17 @@ describeIntegration("MemoryClient Integration — CRUD", () => { expect(memory.id).toBe(memoryId); expect(typeof memory.memory).toBe("string"); expect(memory.memory!.length).toBeGreaterThan(0); - expect(typeof memory.user_id).toBe("string"); + expect(typeof memory.userId).toBe("string"); expect( memory.metadata === null || typeof memory.metadata === "object", ).toBe(true); expect( Array.isArray(memory.categories) || memory.categories === null, ).toBe(true); - expect(new Date(memory.created_at || "").toString()).not.toBe( + expect(new Date(memory.createdAt || "").toString()).not.toBe( "Invalid Date", ); - expect(new Date(memory.updated_at || "").toString()).not.toBe( + expect(new Date(memory.updatedAt || "").toString()).not.toBe( "Invalid Date", ); }); @@ -123,7 +123,7 @@ describeIntegration("MemoryClient Integration — CRUD", () => { describe("get all memories", () => { test("returns all memories for test user", async () => { const memories = await client.getAll({ - filters: { user_id: TEST_USER_ID }, + filters: { userId: TEST_USER_ID }, }); expect(Array.isArray(memories)).toBe(true); @@ -137,9 +137,9 @@ describeIntegration("MemoryClient Integration — CRUD", () => { test("returns paginated results with page and page_size", async () => { const page1 = await client.getAll({ - filters: { user_id: TEST_USER_ID }, + filters: { userId: TEST_USER_ID }, page: 1, - page_size: 1, + pageSize: 1, }); // Paginated response is an object with results array @@ -191,7 +191,7 @@ describeIntegration("MemoryClient Integration — CRUD", () => { }, ], { - user_id: TEST_USER_ID, + userId: TEST_USER_ID, metadata: { source: "integration-test", category: "preferences" }, }, ); @@ -202,7 +202,7 @@ describeIntegration("MemoryClient Integration — CRUD", () => { test("getAll for non-existent user returns empty array", async () => { const memories = await client.getAll({ - filters: { user_id: `nonexistent-user-${randomUUID()}` }, + filters: { userId: `nonexistent-user-${randomUUID()}` }, }); expect(Array.isArray(memories)).toBe(true); @@ -211,7 +211,7 @@ describeIntegration("MemoryClient Integration — CRUD", () => { test("deleteAll for non-existent user does not throw", async () => { const result = await client.deleteAll({ - user_id: `nonexistent-user-${randomUUID()}`, + userId: `nonexistent-user-${randomUUID()}`, }); expect(result).toBeDefined(); @@ -241,13 +241,13 @@ describeIntegration("MemoryClient Integration — CRUD", () => { // ─── Delete all + delete user ───────────────────────────── describe("cleanup operations", () => { test("deletes all memories for test user", async () => { - const result = await client.deleteAll({ user_id: TEST_USER_ID }); + const result = await client.deleteAll({ userId: TEST_USER_ID }); expect(result).toBeDefined(); expect(typeof result.message).toBe("string"); }); test("deletes the test user entity", async () => { - const result = await client.deleteUsers({ user_id: TEST_USER_ID }); + const result = await client.deleteUsers({ userId: TEST_USER_ID }); expect(result).toBeDefined(); expect(result.message).toBe("Entity deleted successfully."); }); diff --git a/mem0-ts/src/client/tests/integration/global-setup.ts b/mem0-ts/src/client/tests/integration/global-setup.ts index 215c61d8a..7adae5831 100644 --- a/mem0-ts/src/client/tests/integration/global-setup.ts +++ b/mem0-ts/src/client/tests/integration/global-setup.ts @@ -18,10 +18,10 @@ export default async function globalSetup() { // Full project wipe — all four filters set explicitly try { await client.deleteAll({ - user_id: "*", - agent_id: "*", - app_id: "*", - run_id: "*", + userId: "*", + agentId: "*", + appId: "*", + runId: "*", }); } catch { // ignore — may 404 if no data exists diff --git a/mem0-ts/src/client/tests/integration/global-teardown.ts b/mem0-ts/src/client/tests/integration/global-teardown.ts index 9e94af661..56815d556 100644 --- a/mem0-ts/src/client/tests/integration/global-teardown.ts +++ b/mem0-ts/src/client/tests/integration/global-teardown.ts @@ -17,10 +17,10 @@ export default async function globalTeardown() { try { await client.deleteAll({ - user_id: "*", - agent_id: "*", - app_id: "*", - run_id: "*", + userId: "*", + agentId: "*", + appId: "*", + runId: "*", }); } catch { // ignore diff --git a/mem0-ts/src/client/tests/integration/helpers.ts b/mem0-ts/src/client/tests/integration/helpers.ts index 3f0b8bfc1..c85baa189 100644 --- a/mem0-ts/src/client/tests/integration/helpers.ts +++ b/mem0-ts/src/client/tests/integration/helpers.ts @@ -64,7 +64,7 @@ export async function waitForMemories( ): Promise { for (let attempt = 1; attempt <= maxRetries; attempt++) { const memories = await withRetry(() => - client.getAll({ filters: { user_id: userId } }), + client.getAll({ filters: { userId } }), ); if (Array.isArray(memories) && memories.length >= minCount) { return memories; @@ -154,7 +154,7 @@ export async function seedTestMemories( "Nice to meet you! I'll remember that your favorite color is blue.", }, ], - { user_id: userId }, + { userId }, ), ); @@ -170,7 +170,7 @@ export async function seedTestMemories( content: "Got it, you're a software engineer at Acme Corp!", }, ], - { user_id: userId }, + { userId }, ), ); @@ -186,12 +186,12 @@ export async function cleanupTestUser( userId: string, ): Promise { try { - await client.deleteAll({ user_id: userId }); + await client.deleteAll({ userId }); } catch { // ignore } try { - await client.deleteUsers({ user_id: userId }); + await client.deleteUsers({ userId }); } catch { // ignore } @@ -209,10 +209,10 @@ export async function fullProjectCleanup(client: MemoryClient): Promise { // Delete all memories — all four filters set explicitly try { await client.deleteAll({ - user_id: "*", - agent_id: "*", - app_id: "*", - run_id: "*", + userId: "*", + agentId: "*", + appId: "*", + runId: "*", }); } catch { // ignore — may 404 if no data exists diff --git a/mem0-ts/src/client/tests/integration/management.test.ts b/mem0-ts/src/client/tests/integration/management.test.ts index dc79ff86e..a6b5c97b5 100644 --- a/mem0-ts/src/client/tests/integration/management.test.ts +++ b/mem0-ts/src/client/tests/integration/management.test.ts @@ -67,16 +67,16 @@ describeIntegration("MemoryClient Integration — Users & Project", () => { expect(project).toBeDefined(); expect(typeof project).toBe("object"); - expect("custom_instructions" in project).toBe(true); + expect("customInstructions" in project).toBe(true); - originalInstructions = project.custom_instructions; + originalInstructions = project.customInstructions; }); test("updates project custom_instructions via updateProject()", async () => { const testInstruction = `integration-test-${randomUUID().slice(0, 8)}`; const result = await client.updateProject({ - custom_instructions: testInstruction, + customInstructions: testInstruction, }); expect(result).toBeDefined(); @@ -85,11 +85,11 @@ describeIntegration("MemoryClient Integration — Users & Project", () => { const project = await client.getProject({ fields: ["custom_instructions"], }); - expect(project.custom_instructions).toBe(testInstruction); + expect(project.customInstructions).toBe(testInstruction); // Restore original await client.updateProject({ - custom_instructions: originalInstructions || "", + customInstructions: originalInstructions || "", }); }); }); @@ -120,26 +120,26 @@ describeIntegration("MemoryClient Integration — Users & Project", () => { eventTypes: [WebhookEvent.MEMORY_ADDED, WebhookEvent.MEMORY_UPDATED], }), ); - createdWebhookId = result.webhook_id!; - expect(result.webhook_id).toBeDefined(); + createdWebhookId = result.webhookId!; + expect(result.webhookId).toBeDefined(); }); test("createWebhook returns the correct name", async () => { const webhooks = await withRetry(() => client.getWebhooks()); - const wh = webhooks.find((w) => w.webhook_id === createdWebhookId); + const wh = webhooks.find((w) => w.webhookId === createdWebhookId); expect(wh!.name).toBe(hookName); }); test("createWebhook returns the correct url", async () => { const webhooks = await withRetry(() => client.getWebhooks()); - const wh = webhooks.find((w) => w.webhook_id === createdWebhookId); + const wh = webhooks.find((w) => w.webhookId === createdWebhookId); expect(wh!.url).toBe(hookUrl); }); test("createWebhook returns the correct event_types", async () => { const webhooks = await withRetry(() => client.getWebhooks()); - const wh = webhooks.find((w) => w.webhook_id === createdWebhookId); - expect(wh!.event_types?.sort()).toStrictEqual( + const wh = webhooks.find((w) => w.webhookId === createdWebhookId); + expect(wh!.eventTypes?.sort()).toStrictEqual( [WebhookEvent.MEMORY_ADDED, WebhookEvent.MEMORY_UPDATED].sort(), ); }); @@ -152,14 +152,14 @@ describeIntegration("MemoryClient Integration — Users & Project", () => { test("getWebhooks includes the created webhook", async () => { const webhooks = await withRetry(() => client.getWebhooks()); - const found = webhooks.find((w) => w.webhook_id === createdWebhookId); + const found = webhooks.find((w) => w.webhookId === createdWebhookId); expect(found).toBeDefined(); }); test("getWebhooks shows the webhook as active", async () => { const webhooks = await withRetry(() => client.getWebhooks()); - const found = webhooks.find((w) => w.webhook_id === createdWebhookId); - expect(found!.is_active).toBe(true); + const found = webhooks.find((w) => w.webhookId === createdWebhookId); + expect(found!.isActive).toBe(true); }); // ─── Update ──────────────────────────────────────────── @@ -177,14 +177,14 @@ describeIntegration("MemoryClient Integration — Users & Project", () => { test("updateWebhook persists the new name", async () => { const webhooks = await withRetry(() => client.getWebhooks()); - const updated = webhooks.find((w) => w.webhook_id === createdWebhookId); + const updated = webhooks.find((w) => w.webhookId === createdWebhookId); expect(updated!.name).toBe(updatedName); }); test("updateWebhook persists the new event_types", async () => { const webhooks = await withRetry(() => client.getWebhooks()); - const updated = webhooks.find((w) => w.webhook_id === createdWebhookId); - expect(updated!.event_types?.sort()).toStrictEqual( + const updated = webhooks.find((w) => w.webhookId === createdWebhookId); + expect(updated!.eventTypes?.sort()).toStrictEqual( [WebhookEvent.MEMORY_DELETED].sort(), ); }); @@ -199,7 +199,7 @@ describeIntegration("MemoryClient Integration — Users & Project", () => { test("deleteWebhook removes the webhook from the list", async () => { const webhooks = await withRetry(() => client.getWebhooks()); - const found = webhooks.find((w) => w.webhook_id === createdWebhookId); + const found = webhooks.find((w) => w.webhookId === createdWebhookId); expect(found).toBeUndefined(); createdWebhookId = ""; }); diff --git a/mem0-ts/src/client/tests/integration/search.test.ts b/mem0-ts/src/client/tests/integration/search.test.ts index 0591f2ffa..1f3e9cf89 100644 --- a/mem0-ts/src/client/tests/integration/search.test.ts +++ b/mem0-ts/src/client/tests/integration/search.test.ts @@ -43,7 +43,7 @@ describeIntegration("MemoryClient Integration — Search & History", () => { const results = await waitForSearchResults( client, "What is my favorite color?", - { filters: { user_id: TEST_USER_ID } }, + { filters: { userId: TEST_USER_ID } }, ); expect(Array.isArray(results)).toBe(true); @@ -64,7 +64,7 @@ describeIntegration("MemoryClient Integration — Search & History", () => { client, "What do you know about me?", { - filters: { OR: [{ user_id: TEST_USER_ID }] }, + filters: { OR: [{ userId: TEST_USER_ID }] }, }, ); @@ -89,15 +89,15 @@ describeIntegration("MemoryClient Integration — Search & History", () => { const entry = history[0]; expect(typeof entry.id).toBe("string"); - expect(typeof entry.memory_id).toBe("string"); + expect(typeof entry.memoryId).toBe("string"); expect(["ADD", "UPDATE", "DELETE", "NOOP"]).toContain(entry.event); - expect(new Date(entry.created_at).toString()).not.toBe("Invalid Date"); - expect(new Date(entry.updated_at).toString()).not.toBe("Invalid Date"); + expect(new Date(entry.createdAt).toString()).not.toBe("Invalid Date"); + expect(new Date(entry.updatedAt).toString()).not.toBe("Invalid Date"); expect( - entry.new_memory === null || typeof entry.new_memory === "string", + entry.newMemory === null || typeof entry.newMemory === "string", ).toBe(true); expect( - entry.old_memory === null || typeof entry.old_memory === "string", + entry.oldMemory === null || typeof entry.oldMemory === "string", ).toBe(true); const events = history.map((h) => h.event); @@ -109,7 +109,7 @@ describeIntegration("MemoryClient Integration — Search & History", () => { describe("edge cases", () => { test("search for non-existent user returns empty results", async () => { const results = await client.search("anything", { - filters: { user_id: `nonexistent-user-${randomUUID()}` }, + filters: { userId: `nonexistent-user-${randomUUID()}` }, }); expect(Array.isArray(results)).toBe(true); @@ -120,8 +120,8 @@ describeIntegration("MemoryClient Integration — Search & History", () => { const results = await client.search( "Tell me about integration test user", { - filters: { user_id: TEST_USER_ID }, - top_k: 1, + filters: { userId: TEST_USER_ID }, + topK: 1, }, ); diff --git a/mem0-ts/src/client/tests/memoryClient.crud.test.ts b/mem0-ts/src/client/tests/memoryClient.crud.test.ts index 7391f8994..9ce61c003 100644 --- a/mem0-ts/src/client/tests/memoryClient.crud.test.ts +++ b/mem0-ts/src/client/tests/memoryClient.crud.test.ts @@ -27,7 +27,7 @@ describe("MemoryClient - add()", () => { const mock = setupMockFetch(extra); const client = new MemoryClient({ apiKey: TEST_API_KEY }); - await client.add([{ role: "user", content: "Hello" }], { user_id: "u1" }); + await client.add([{ role: "user", content: "Hello" }], { userId: "u1" }); expect(findFetchCall(mock, "/v1/memories/", "POST")).toBeDefined(); }); @@ -39,7 +39,7 @@ describe("MemoryClient - add()", () => { const mock = setupMockFetch(extra); const client = new MemoryClient({ apiKey: TEST_API_KEY }); - await client.add(messages, { user_id: "u1" }); + await client.add(messages, { userId: "u1" }); const call = findFetchCall(mock, "/v1/memories/", "POST"); expect(getFetchBody(call!).messages).toEqual(messages); @@ -65,7 +65,7 @@ describe("MemoryClient - add()", () => { const mock = setupMockFetch(extra); const client = new MemoryClient({ apiKey: TEST_API_KEY }); - await client.add([], { user_id: "u1" }); + await client.add([], { userId: "u1" }); const call = findFetchCall(mock, "/v1/memories/", "POST"); expect(getFetchBody(call!).messages).toEqual([]); @@ -215,7 +215,7 @@ describe("MemoryClient - deleteAll()", () => { const mock = setupMockFetch(extra); const client = new MemoryClient({ apiKey: TEST_API_KEY }); - await client.deleteAll({ user_id: "u1" }); + await client.deleteAll({ userId: "u1" }); const call = mock.mock.calls.find( (c: [string, RequestInit]) => @@ -231,7 +231,7 @@ describe("MemoryClient - deleteAll()", () => { const mock = setupMockFetch(extra); const client = new MemoryClient({ apiKey: TEST_API_KEY }); - await client.deleteAll({ user_id: "user@email.com" }); + await client.deleteAll({ userId: "user@email.com" }); const call = mock.mock.calls.find( (c: [string, RequestInit]) => diff --git a/mem0-ts/src/client/tests/memoryClient.project.test.ts b/mem0-ts/src/client/tests/memoryClient.project.test.ts index 4e5c9014e..ed5dd6696 100644 --- a/mem0-ts/src/client/tests/memoryClient.project.test.ts +++ b/mem0-ts/src/client/tests/memoryClient.project.test.ts @@ -111,9 +111,9 @@ describe("MemoryClient - feedback()", () => { const client = new MemoryClient({ apiKey: TEST_API_KEY }); await client.feedback({ - memory_id: "mem_123", + memoryId: "mem_123", feedback: Feedback.POSITIVE, - feedback_reason: "Very helpful", + feedbackReason: "Very helpful", }); const call = findFetchCall(mock, "/v1/feedback/", "POST"); @@ -130,9 +130,9 @@ describe("MemoryClient - feedback()", () => { const client = new MemoryClient({ apiKey: TEST_API_KEY }); await client.feedback({ - memory_id: "mem_123", + memoryId: "mem_123", feedback: Feedback.POSITIVE, - feedback_reason: "Very helpful", + feedbackReason: "Very helpful", }); const call = findFetchCall(mock, "/v1/feedback/", "POST"); @@ -178,7 +178,7 @@ describe("MemoryClient - Memory Exports", () => { setupMockFetch(); const client = new MemoryClient({ apiKey: TEST_API_KEY }); await expect(client.getMemoryExport({} as never)).rejects.toThrow( - "Missing memory_export_id or filters", + "Missing memoryExportId or filters", ); }); @@ -191,7 +191,7 @@ describe("MemoryClient - Memory Exports", () => { const mock = setupMockFetch(extra); const client = new MemoryClient({ apiKey: TEST_API_KEY }); - await client.getMemoryExport({ memory_export_id: "exp_123" }); + await client.getMemoryExport({ memoryExportId: "exp_123" }); expect(findFetchCall(mock, "/v1/exports/get/", "POST")).toBeDefined(); }); diff --git a/mem0-ts/src/client/tests/memoryClient.search.test.ts b/mem0-ts/src/client/tests/memoryClient.search.test.ts index 6efbaf6b5..f1f5c7e5f 100644 --- a/mem0-ts/src/client/tests/memoryClient.search.test.ts +++ b/mem0-ts/src/client/tests/memoryClient.search.test.ts @@ -22,7 +22,7 @@ describe("MemoryClient - search()", () => { const client = new MemoryClient({ apiKey: TEST_API_KEY }); await client.search("What is my name?", { - filters: { user_id: "u1" }, + filters: { userId: "u1" }, }); expect(findFetchCall(mock, "/v2/memories/search/", "POST")).toBeDefined(); @@ -35,7 +35,7 @@ describe("MemoryClient - search()", () => { const client = new MemoryClient({ apiKey: TEST_API_KEY }); await client.search("What is my name?", { - filters: { user_id: "u1" }, + filters: { userId: "u1" }, }); const call = findFetchCall(mock, "/v2/memories/search/", "POST"); @@ -48,7 +48,7 @@ describe("MemoryClient - search()", () => { const mock = setupMockFetch(extra); const client = new MemoryClient({ apiKey: TEST_API_KEY }); - await client.search("test", { filters: { user_id: "u1" } }); + await client.search("test", { filters: { userId: "u1" } }); const call = findFetchCall(mock, "/v2/memories/search/", "POST"); expect(getFetchBody(call!).filters).toEqual({ user_id: "u1" }); @@ -88,7 +88,7 @@ describe("MemoryClient - search()", () => { const client = new MemoryClient({ apiKey: TEST_API_KEY }); const result: Memory[] = await client.search("nonexistent query", { - filters: { user_id: "u1" }, + filters: { userId: "u1" }, }); expect(result).toHaveLength(0); }); diff --git a/mem0-ts/src/client/utils.ts b/mem0-ts/src/client/utils.ts new file mode 100644 index 000000000..5580b0585 --- /dev/null +++ b/mem0-ts/src/client/utils.ts @@ -0,0 +1,49 @@ +/** + * Converts a camelCase string to snake_case. + */ +export function camelToSnake(str: string): string { + // Skip all-uppercase keys (e.g. OR, AND, NOT — logical operators) + if (str === str.toUpperCase()) return str; + return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); +} + +/** + * Converts a snake_case string to camelCase. + */ +function snakeToCamel(str: string): string { + return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); +} + +/** + * Recursively converts all keys of an object from camelCase to snake_case. + * Used for converting user-facing camelCase params to API snake_case payloads. + */ +export function camelToSnakeKeys(obj: any): any { + if (obj === null || obj === undefined || typeof obj !== "object") return obj; + if (Array.isArray(obj)) return obj.map(camelToSnakeKeys); + if (obj instanceof Date) return obj; + + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [ + camelToSnake(key), + camelToSnakeKeys(value), + ]), + ); +} + +/** + * Recursively converts all keys of an object from snake_case to camelCase. + * Used for converting API snake_case responses to user-facing camelCase. + */ +export function snakeToCamelKeys(obj: any): any { + if (obj === null || obj === undefined || typeof obj !== "object") return obj; + if (Array.isArray(obj)) return obj.map(snakeToCamelKeys); + if (obj instanceof Date) return obj; + + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [ + snakeToCamel(key), + snakeToCamelKeys(value), + ]), + ); +} diff --git a/mem0-ts/src/oss/src/config/defaults.ts b/mem0-ts/src/oss/src/config/defaults.ts index bbce65cc6..c42718139 100644 --- a/mem0-ts/src/oss/src/config/defaults.ts +++ b/mem0-ts/src/oss/src/config/defaults.ts @@ -26,15 +26,6 @@ export const DEFAULT_MEMORY_CONFIG: MemoryConfig = { modelProperties: undefined, }, }, - enableGraph: false, - graphStore: { - provider: "neo4j", - config: { - url: process.env.NEO4J_URL || "neo4j://localhost:7687", - username: process.env.NEO4J_USERNAME || "neo4j", - password: process.env.NEO4J_PASSWORD || "password", - }, - }, historyStore: { provider: "sqlite", config: { diff --git a/mem0-ts/src/oss/src/config/manager.ts b/mem0-ts/src/oss/src/config/manager.ts index 6235157ce..02afe6ddd 100644 --- a/mem0-ts/src/oss/src/config/manager.ts +++ b/mem0-ts/src/oss/src/config/manager.ts @@ -132,10 +132,9 @@ export class ConfigManager { userConfig.historyStore?.config?.historyDbPath || DEFAULT_MEMORY_CONFIG.historyStore?.config?.historyDbPath, customInstructions: userConfig.customInstructions, - graphStore: { - ...DEFAULT_MEMORY_CONFIG.graphStore, - ...userConfig.graphStore, - }, + graphStore: userConfig.graphStore + ? { ...userConfig.graphStore } + : undefined, historyStore: (() => { const defaultHistoryStore = DEFAULT_MEMORY_CONFIG.historyStore!; const historyProvider = @@ -158,7 +157,6 @@ export class ConfigManager { })(), disableHistory: userConfig.disableHistory || DEFAULT_MEMORY_CONFIG.disableHistory, - enableGraph: userConfig.enableGraph || DEFAULT_MEMORY_CONFIG.enableGraph, }; // Validate the merged config diff --git a/mem0-ts/src/oss/src/memory/index.ts b/mem0-ts/src/oss/src/memory/index.ts index d4c4e25d0..044585028 100644 --- a/mem0-ts/src/oss/src/memory/index.ts +++ b/mem0-ts/src/oss/src/memory/index.ts @@ -47,7 +47,6 @@ export class Memory { private collectionName: string | undefined; private apiVersion: string; private graphMemory?: MemoryGraph; - private enableGraph: boolean; telemetryId: string; private _initPromise: Promise; private _initError?: Error; @@ -79,11 +78,10 @@ export class Memory { this.collectionName = this.config.vectorStore.config.collectionName; this.apiVersion = this.config.version || "v1.0"; - this.enableGraph = this.config.enableGraph || false; this.telemetryId = "anonymous"; - // Initialize graph memory if configured - if (this.enableGraph && this.config.graphStore) { + // Initialize graph memory if graphStore is configured + if (this.config.graphStore) { this.graphMemory = new MemoryGraph(this.config); } @@ -159,7 +157,6 @@ export class Memory { api_version: this.apiVersion, client_type: "Memory", collection_name: this.collectionName, - enable_graph: this.enableGraph, }); } catch (error) {} } diff --git a/mem0-ts/src/oss/src/tests/sqlite-backward-compat.test.ts b/mem0-ts/src/oss/src/tests/sqlite-backward-compat.test.ts index 8e2eb7750..3f7991b3f 100644 --- a/mem0-ts/src/oss/src/tests/sqlite-backward-compat.test.ts +++ b/mem0-ts/src/oss/src/tests/sqlite-backward-compat.test.ts @@ -38,7 +38,7 @@ describe("backward compat: ConfigManager.mergeConfig", () => { expect(cfg.historyStore!.provider).toBe("sqlite"); expect(cfg.historyStore!.config.historyDbPath).toBe("memory.db"); expect(cfg.disableHistory).toBe(false); - expect(cfg.enableGraph).toBe(false); + expect(cfg.graphStore).toBeUndefined(); }); it("workaround: explicit historyStore still works (existing user pattern)", () => { @@ -106,7 +106,6 @@ describe("backward compat: ConfigManager.mergeConfig", () => { it("graphStore config passes through unchanged", () => { const cfg = ConfigManager.mergeConfig({ - enableGraph: true, graphStore: { provider: "neo4j", config: { @@ -116,7 +115,6 @@ describe("backward compat: ConfigManager.mergeConfig", () => { }, }, }); - expect(cfg.enableGraph).toBe(true); expect(cfg.graphStore!.config.url).toBe("neo4j://custom:7687"); }); diff --git a/mem0-ts/src/oss/src/types/index.ts b/mem0-ts/src/oss/src/types/index.ts index 0476ef515..5a5cb91b4 100644 --- a/mem0-ts/src/oss/src/types/index.ts +++ b/mem0-ts/src/oss/src/types/index.ts @@ -82,7 +82,6 @@ export interface MemoryConfig { historyDbPath?: string; customInstructions?: string; graphStore?: GraphStoreConfig; - enableGraph?: boolean; } export interface MemoryItem { @@ -149,7 +148,6 @@ export const MemoryConfigSchema = z.object({ }), historyDbPath: z.string().optional(), customInstructions: z.string().optional(), - enableGraph: z.boolean().optional(), graphStore: z .object({ provider: z.string(), diff --git a/mem0/client/main.py b/mem0/client/main.py index 6511e73ca..6cf74e412 100644 --- a/mem0/client/main.py +++ b/mem0/client/main.py @@ -665,7 +665,6 @@ class MemoryClient: custom_instructions: Optional[str] = None, custom_categories: Optional[List[str]] = None, retrieval_criteria: Optional[List[Dict[str, Any]]] = None, - enable_graph: Optional[bool] = None, memory_depth: Optional[str] = None, usecase_setting: Optional[str] = None, multilingual: Optional[bool] = None, @@ -677,7 +676,6 @@ class MemoryClient: custom_instructions: New instructions for the project. custom_categories: New categories for the project. retrieval_criteria: New retrieval criteria for the project. - enable_graph: Enable or disable the graph for the project. memory_depth: Memory depth for the project. usecase_setting: Usecase setting for the project. multilingual: Whether to use the input language for memory storage and retrieval. @@ -703,7 +701,6 @@ class MemoryClient: "custom_instructions": custom_instructions, "custom_categories": custom_categories, "retrieval_criteria": retrieval_criteria, - "enable_graph": enable_graph, "memory_depth": memory_depth, "usecase_setting": usecase_setting, "multilingual": multilingual, @@ -1549,7 +1546,6 @@ class AsyncMemoryClient: custom_instructions: Optional[str] = None, custom_categories: Optional[List[str]] = None, retrieval_criteria: Optional[List[Dict[str, Any]]] = None, - enable_graph: Optional[bool] = None, memory_depth: Optional[str] = None, usecase_setting: Optional[str] = None, multilingual: Optional[bool] = None, @@ -1561,7 +1557,6 @@ class AsyncMemoryClient: custom_instructions: New instructions for the project. custom_categories: New categories for the project. retrieval_criteria: New retrieval criteria for the project. - enable_graph: Enable or disable the graph for the project. memory_depth: Memory depth for the project. usecase_setting: Usecase setting for the project. multilingual: Whether to use the input language for memory storage and retrieval. @@ -1587,7 +1582,6 @@ class AsyncMemoryClient: "custom_instructions": custom_instructions, "custom_categories": custom_categories, "retrieval_criteria": retrieval_criteria, - "enable_graph": enable_graph, "memory_depth": memory_depth, "usecase_setting": usecase_setting, "multilingual": multilingual, diff --git a/mem0/client/project.py b/mem0/client/project.py index 92a021dec..fe466b7ef 100644 --- a/mem0/client/project.py +++ b/mem0/client/project.py @@ -7,6 +7,7 @@ from pydantic import BaseModel, ConfigDict, Field from mem0.client.utils import api_error_handler from mem0.memory.telemetry import capture_client_event + # Exception classes are referenced in docstrings only logger = logging.getLogger(__name__) @@ -177,7 +178,6 @@ class BaseProject(ABC): custom_instructions: Optional[str] = None, custom_categories: Optional[List[str]] = None, retrieval_criteria: Optional[List[Dict[str, Any]]] = None, - enable_graph: Optional[bool] = None, ) -> Dict[str, Any]: """ Update project settings. @@ -186,7 +186,6 @@ class BaseProject(ABC): custom_instructions: New instructions for the project custom_categories: New categories for the project retrieval_criteria: New retrieval criteria for the project - enable_graph: Enable or disable the graph for the project Returns: Dictionary containing the API response. @@ -398,7 +397,6 @@ class Project(BaseProject): custom_instructions: Optional[str] = None, custom_categories: Optional[List[str]] = None, retrieval_criteria: Optional[List[Dict[str, Any]]] = None, - enable_graph: Optional[bool] = None, multilingual: Optional[bool] = None, ) -> Dict[str, Any]: """ @@ -408,7 +406,6 @@ class Project(BaseProject): custom_instructions: New instructions for the project custom_categories: New categories for the project retrieval_criteria: New retrieval criteria for the project - enable_graph: Enable or disable the graph for the project multilingual: Whether to use the input language for memory storage and retrieval Returns: @@ -425,13 +422,12 @@ class Project(BaseProject): custom_instructions is None and custom_categories is None and retrieval_criteria is None - and enable_graph is None and multilingual is None ): raise ValueError( "At least one parameter must be provided for update: " "custom_instructions, custom_categories, retrieval_criteria, " - "enable_graph, multilingual" + "multilingual" ) payload = self._prepare_params( @@ -439,7 +435,6 @@ class Project(BaseProject): "custom_instructions": custom_instructions, "custom_categories": custom_categories, "retrieval_criteria": retrieval_criteria, - "enable_graph": enable_graph, "multilingual": multilingual, } ) @@ -455,7 +450,6 @@ class Project(BaseProject): "custom_instructions": custom_instructions, "custom_categories": custom_categories, "retrieval_criteria": retrieval_criteria, - "enable_graph": enable_graph, "multilingual": multilingual, "sync_type": "sync", }, @@ -720,7 +714,6 @@ class AsyncProject(BaseProject): custom_instructions: Optional[str] = None, custom_categories: Optional[List[str]] = None, retrieval_criteria: Optional[List[Dict[str, Any]]] = None, - enable_graph: Optional[bool] = None, multilingual: Optional[bool] = None, ) -> Dict[str, Any]: """ @@ -730,7 +723,6 @@ class AsyncProject(BaseProject): custom_instructions: New instructions for the project custom_categories: New categories for the project retrieval_criteria: New retrieval criteria for the project - enable_graph: Enable or disable the graph for the project multilingual: Whether to use the input language for memory storage and retrieval Returns: @@ -747,13 +739,12 @@ class AsyncProject(BaseProject): custom_instructions is None and custom_categories is None and retrieval_criteria is None - and enable_graph is None and multilingual is None ): raise ValueError( "At least one parameter must be provided for update: " "custom_instructions, custom_categories, retrieval_criteria, " - "enable_graph, multilingual" + "multilingual" ) payload = self._prepare_params( @@ -761,7 +752,6 @@ class AsyncProject(BaseProject): "custom_instructions": custom_instructions, "custom_categories": custom_categories, "retrieval_criteria": retrieval_criteria, - "enable_graph": enable_graph, "multilingual": multilingual, } ) @@ -777,7 +767,6 @@ class AsyncProject(BaseProject): "custom_instructions": custom_instructions, "custom_categories": custom_categories, "retrieval_criteria": retrieval_criteria, - "enable_graph": enable_graph, "multilingual": multilingual, "sync_type": "async", }, diff --git a/mem0/client/types.py b/mem0/client/types.py index e55b78713..4053d69de 100644 --- a/mem0/client/types.py +++ b/mem0/client/types.py @@ -31,7 +31,6 @@ class AddMemoryOptions(EntityOptions): structured_data_schema: Optional[Dict[str, Any]] = Field( default=None, description="Schema for structured data extraction" ) - enable_graph: Optional[bool] = Field(default=None, description="Whether to enable graph memory for this operation") class SearchMemoryOptions(BaseModel): @@ -50,7 +49,6 @@ class SearchMemoryOptions(BaseModel): threshold: Optional[float] = Field(default=None, description="Minimum similarity score threshold") fields: Optional[List[str]] = Field(default=None, description="Fields to include in the response") categories: Optional[List[str]] = Field(default=None, description="Categories to filter by") - enable_graph: Optional[bool] = Field(default=None, description="Whether to enable graph memory for this search") class GetAllMemoryOptions(BaseModel): @@ -72,7 +70,6 @@ class GetAllMemoryOptions(BaseModel): default=None, description="Filter memories created on or before this date (ISO 8601)" ) categories: Optional[List[str]] = Field(default=None, description="Categories to filter by") - enable_graph: Optional[bool] = Field(default=None, description="Whether to enable graph memory for retrieval") class DeleteAllMemoryOptions(EntityOptions): @@ -96,7 +93,6 @@ class ProjectUpdateOptions(BaseModel): custom_categories: Optional[List[Dict[str, Any]]] = Field( default=None, description="Custom categories for classification" ) - enable_graph: Optional[bool] = Field(default=None, description="Whether to enable graph memory") memory_depth: Optional[str] = Field(default=None, description="Memory depth configuration") usecase_setting: Optional[Any] = Field(default=None, description="Use case specific settings") multilingual: Optional[bool] = Field(default=None, description="Whether to enable multilingual support") diff --git a/mem0/memory/main.py b/mem0/memory/main.py index c18b093a0..24ecf50b4 100644 --- a/mem0/memory/main.py +++ b/mem0/memory/main.py @@ -281,12 +281,9 @@ class Memory(MemoryBase): config.reranker.config ) - self.enable_graph = False - if self.config.graph_store.config: provider = self.config.graph_store.provider self.graph = GraphStoreFactory.create(provider, self.config) - self.enable_graph = True else: self.graph = None if MEM0_TELEMETRY: @@ -477,7 +474,7 @@ class Memory(MemoryBase): vector_store_result = future1.result() graph_result = future2.result() - if self.enable_graph: + if self.graph: return { "results": vector_store_result, "relations": graph_result, @@ -726,7 +723,7 @@ class Memory(MemoryBase): def _add_to_graph(self, messages, filters): added_entities = [] - if self.enable_graph: + if self.graph: if filters.get("user_id") is None: filters["user_id"] = "user" @@ -821,7 +818,7 @@ class Memory(MemoryBase): with concurrent.futures.ThreadPoolExecutor() as executor: future_memories = executor.submit(self._get_all_from_vector_store, effective_filters, top_k) future_graph_entities = ( - executor.submit(self.graph.get_all, effective_filters, top_k) if self.enable_graph else None + executor.submit(self.graph.get_all, effective_filters, top_k) if self.graph else None ) concurrent.futures.wait( @@ -831,7 +828,7 @@ class Memory(MemoryBase): all_memories_result = future_memories.result() graph_entities_result = future_graph_entities.result() if future_graph_entities else None - if self.enable_graph: + if self.graph: return {"results": all_memories_result, "relations": graph_entities_result} return {"results": all_memories_result} @@ -960,7 +957,7 @@ class Memory(MemoryBase): with concurrent.futures.ThreadPoolExecutor() as executor: future_memories = executor.submit(self._search_vector_store, query, effective_filters, top_k, threshold) future_graph_entities = ( - executor.submit(self.graph.search, query, effective_filters, top_k) if self.enable_graph else None + executor.submit(self.graph.search, query, effective_filters, top_k) if self.graph else None ) concurrent.futures.wait( @@ -978,7 +975,7 @@ class Memory(MemoryBase): except Exception as e: logger.warning(f"Reranking failed, using original results: {e}") - if self.enable_graph: + if self.graph: return {"results": original_memories, "relations": graph_entities} return {"results": original_memories} @@ -1157,7 +1154,7 @@ class Memory(MemoryBase): raise ValueError(f"Memory with id {memory_id} not found") # Clean up graph entities before deleting from vector store - if self.enable_graph: + if self.graph: try: memory_text = existing_memory.payload.get("data", "") if memory_text: @@ -1205,7 +1202,7 @@ class Memory(MemoryBase): logger.info(f"Deleted {len(memories)} memories") - if self.enable_graph: + if self.graph: self.graph.delete_all(filters) return {"message": "Memories deleted successfully!"} @@ -1407,7 +1404,7 @@ class Memory(MemoryBase): ) capture_event("mem0.reset", self, {"sync_type": "sync"}) - if self.enable_graph: + if self.graph: try: self.graph.reset() except Exception: @@ -1442,12 +1439,9 @@ class AsyncMemory(MemoryBase): config.reranker.config ) - self.enable_graph = False - if self.config.graph_store.config: provider = self.config.graph_store.provider self.graph = GraphStoreFactory.create(provider, self.config) - self.enable_graph = True else: self.graph = None @@ -1590,7 +1584,7 @@ class AsyncMemory(MemoryBase): vector_store_result, graph_result = await asyncio.gather(vector_store_task, graph_task) - if self.enable_graph: + if self.graph: return { "results": vector_store_result, "relations": graph_result, @@ -1863,7 +1857,7 @@ class AsyncMemory(MemoryBase): async def _add_to_graph(self, messages, filters): added_entities = [] - if self.enable_graph: + if self.graph: if filters.get("user_id") is None: filters["user_id"] = "user" @@ -1961,7 +1955,7 @@ class AsyncMemory(MemoryBase): vector_store_task = asyncio.create_task(self._get_all_from_vector_store(effective_filters, top_k)) graph_task = None - if self.enable_graph: + if self.graph: graph_get_all = getattr(self.graph, "get_all", None) if callable(graph_get_all): if asyncio.iscoroutinefunction(graph_get_all): @@ -2104,7 +2098,7 @@ class AsyncMemory(MemoryBase): vector_store_task = asyncio.create_task(self._search_vector_store(query, effective_filters, top_k, threshold)) graph_task = None - if self.enable_graph: + if self.graph: if hasattr(self.graph.search, "__await__"): # Check if graph search is async graph_task = asyncio.create_task(self.graph.search(query, effective_filters, top_k)) else: @@ -2127,7 +2121,7 @@ class AsyncMemory(MemoryBase): except Exception as e: logger.warning(f"Reranking failed, using original results: {e}") - if self.enable_graph: + if self.graph: return {"results": original_memories, "relations": graph_entities} return {"results": original_memories} @@ -2309,7 +2303,7 @@ class AsyncMemory(MemoryBase): raise ValueError(f"Memory with id {memory_id} not found") # Clean up graph entities before deleting from vector store - if self.enable_graph: + if self.graph: try: memory_text = existing_memory.payload.get("data", "") if memory_text: @@ -2360,7 +2354,7 @@ class AsyncMemory(MemoryBase): logger.info(f"Deleted {len(memories[0])} memories") - if self.enable_graph: + if self.graph: await asyncio.to_thread(self.graph.delete_all, filters) return {"message": "Memories deleted successfully!"} @@ -2587,7 +2581,7 @@ class AsyncMemory(MemoryBase): ) capture_event("mem0.reset", self, {"sync_type": "async"}) - if self.enable_graph: + if self.graph: try: await asyncio.to_thread(self.graph.reset) except Exception: diff --git a/tests/test_graph_delete.py b/tests/test_graph_delete.py deleted file mode 100644 index a54d6d4f6..000000000 --- a/tests/test_graph_delete.py +++ /dev/null @@ -1,517 +0,0 @@ -"""Tests for graph cleanup on memory deletion (issue #3245).""" - -from unittest.mock import MagicMock, patch - -import pytest - -from mem0.configs.base import MemoryConfig - - -class MockVectorMemory: - def __init__(self, memory_id, payload, score=0.8): - self.id = memory_id - self.payload = payload - self.score = score - - -@patch("mem0.utils.factory.EmbedderFactory.create") -@patch("mem0.utils.factory.VectorStoreFactory.create") -@patch("mem0.utils.factory.LlmFactory.create") -@patch("mem0.memory.storage.SQLiteManager") -def test_delete_calls_graph_cleanup_when_graph_enabled( - mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory -): - """When graph is enabled, delete() should call graph.delete() with memory text and filters.""" - mock_embedder_factory.return_value = MagicMock() - mock_vector_store = MagicMock() - mock_vector_factory.return_value = mock_vector_store - mock_llm_factory.return_value = MagicMock() - mock_sqlite.return_value = MagicMock() - - from mem0.memory.main import Memory - - config = MemoryConfig() - memory = Memory(config) - - # Enable graph with a mock - memory.enable_graph = True - memory.graph = MagicMock() - - # Set up vector store to return a memory with graph-relevant data - mock_vector_store.get.return_value = MockVectorMemory( - "mem-1", - { - "data": "Alice likes Bob", - "user_id": "user-1", - "agent_id": "agent-1", - "hash": "abc", - }, - ) - - memory.delete("mem-1") - - # graph.delete should have been called with the memory text and filters - memory.graph.delete.assert_called_once_with( - "Alice likes Bob", {"user_id": "user-1", "agent_id": "agent-1"} - ) - - # _delete_memory should still have been called (vector store + history cleanup) - mock_vector_store.delete.assert_called_once_with(vector_id="mem-1") - - -@patch("mem0.utils.factory.EmbedderFactory.create") -@patch("mem0.utils.factory.VectorStoreFactory.create") -@patch("mem0.utils.factory.LlmFactory.create") -@patch("mem0.memory.storage.SQLiteManager") -def test_delete_skips_graph_when_not_enabled( - mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory -): - """When graph is not enabled, delete() should not attempt graph cleanup.""" - mock_embedder_factory.return_value = MagicMock() - mock_vector_store = MagicMock() - mock_vector_factory.return_value = mock_vector_store - mock_llm_factory.return_value = MagicMock() - mock_sqlite.return_value = MagicMock() - - from mem0.memory.main import Memory - - config = MemoryConfig() - memory = Memory(config) - - assert memory.enable_graph is False - - mock_vector_store.get.return_value = MockVectorMemory( - "mem-1", {"data": "Alice likes Bob", "user_id": "user-1", "hash": "abc"} - ) - - result = memory.delete("mem-1") - - assert result == {"message": "Memory deleted successfully!"} - mock_vector_store.delete.assert_called_once_with(vector_id="mem-1") - - -@patch("mem0.utils.factory.EmbedderFactory.create") -@patch("mem0.utils.factory.VectorStoreFactory.create") -@patch("mem0.utils.factory.LlmFactory.create") -@patch("mem0.memory.storage.SQLiteManager") -def test_delete_continues_if_graph_cleanup_fails( - mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory -): - """If graph cleanup raises an exception, delete() should still succeed.""" - mock_embedder_factory.return_value = MagicMock() - mock_vector_store = MagicMock() - mock_vector_factory.return_value = mock_vector_store - mock_llm_factory.return_value = MagicMock() - mock_sqlite.return_value = MagicMock() - - from mem0.memory.main import Memory - - config = MemoryConfig() - memory = Memory(config) - - memory.enable_graph = True - memory.graph = MagicMock() - memory.graph.delete.side_effect = RuntimeError("Neo4j connection lost") - - mock_vector_store.get.return_value = MockVectorMemory( - "mem-1", {"data": "Alice likes Bob", "user_id": "user-1", "hash": "abc"} - ) - - # Should not raise - result = memory.delete("mem-1") - assert result == {"message": "Memory deleted successfully!"} - - # Vector store deletion should still proceed - mock_vector_store.delete.assert_called_once_with(vector_id="mem-1") - - -@patch("mem0.utils.factory.EmbedderFactory.create") -@patch("mem0.utils.factory.VectorStoreFactory.create") -@patch("mem0.utils.factory.LlmFactory.create") -@patch("mem0.memory.storage.SQLiteManager") -def test_delete_skips_graph_when_no_user_id( - mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory -): - """Graph cleanup should be skipped if the memory has no user_id.""" - mock_embedder_factory.return_value = MagicMock() - mock_vector_store = MagicMock() - mock_vector_factory.return_value = mock_vector_store - mock_llm_factory.return_value = MagicMock() - mock_sqlite.return_value = MagicMock() - - from mem0.memory.main import Memory - - config = MemoryConfig() - memory = Memory(config) - - memory.enable_graph = True - memory.graph = MagicMock() - - # Memory with no user_id - mock_vector_store.get.return_value = MockVectorMemory( - "mem-1", {"data": "Some data", "hash": "abc"} - ) - - memory.delete("mem-1") - - # graph.delete should NOT have been called since there's no user_id - memory.graph.delete.assert_not_called() - mock_vector_store.delete.assert_called_once_with(vector_id="mem-1") - - -@patch("mem0.utils.factory.EmbedderFactory.create") -@patch("mem0.utils.factory.VectorStoreFactory.create") -@patch("mem0.utils.factory.LlmFactory.create") -@patch("mem0.memory.storage.SQLiteManager") -def test_delete_skips_graph_when_no_memory_text( - mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory -): - """Graph cleanup should be skipped if the memory has no text data.""" - mock_embedder_factory.return_value = MagicMock() - mock_vector_store = MagicMock() - mock_vector_factory.return_value = mock_vector_store - mock_llm_factory.return_value = MagicMock() - mock_sqlite.return_value = MagicMock() - - from mem0.memory.main import Memory - - config = MemoryConfig() - memory = Memory(config) - - memory.enable_graph = True - memory.graph = MagicMock() - - mock_vector_store.get.return_value = MockVectorMemory( - "mem-1", {"user_id": "user-1", "hash": "abc"} - ) - - memory.delete("mem-1") - - memory.graph.delete.assert_not_called() - mock_vector_store.delete.assert_called_once_with(vector_id="mem-1") - - -@patch("mem0.utils.factory.EmbedderFactory.create") -@patch("mem0.utils.factory.VectorStoreFactory.create") -@patch("mem0.utils.factory.LlmFactory.create") -@patch("mem0.memory.storage.SQLiteManager") -def test_delete_passes_all_filters_to_graph( - mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory -): - """Graph cleanup should include all available filters (user_id, agent_id, run_id).""" - mock_embedder_factory.return_value = MagicMock() - mock_vector_store = MagicMock() - mock_vector_factory.return_value = mock_vector_store - mock_llm_factory.return_value = MagicMock() - mock_sqlite.return_value = MagicMock() - - from mem0.memory.main import Memory - - config = MemoryConfig() - memory = Memory(config) - - memory.enable_graph = True - memory.graph = MagicMock() - - mock_vector_store.get.return_value = MockVectorMemory( - "mem-1", - { - "data": "Alice likes Bob", - "user_id": "user-1", - "agent_id": "agent-1", - "run_id": "run-1", - "hash": "abc", - }, - ) - - memory.delete("mem-1") - - memory.graph.delete.assert_called_once_with( - "Alice likes Bob", - {"user_id": "user-1", "agent_id": "agent-1", "run_id": "run-1"}, - ) - - -@pytest.mark.asyncio -@patch("mem0.utils.factory.EmbedderFactory.create") -@patch("mem0.utils.factory.VectorStoreFactory.create") -@patch("mem0.utils.factory.LlmFactory.create") -@patch("mem0.memory.storage.SQLiteManager") -async def test_async_delete_calls_graph_cleanup( - mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory -): - """Async delete() should also perform graph cleanup.""" - mock_embedder_factory.return_value = MagicMock() - mock_vector_store = MagicMock() - mock_vector_factory.return_value = mock_vector_store - mock_llm_factory.return_value = MagicMock() - mock_sqlite.return_value = MagicMock() - - from mem0.memory.main import AsyncMemory - - config = MemoryConfig() - memory = AsyncMemory(config) - - memory.enable_graph = True - memory.graph = MagicMock() - - mock_vector_store.get.return_value = MockVectorMemory( - "mem-1", - { - "data": "Alice likes Bob", - "user_id": "user-1", - "hash": "abc", - }, - ) - - result = await memory.delete("mem-1") - - assert result == {"message": "Memory deleted successfully!"} - memory.graph.delete.assert_called_once_with("Alice likes Bob", {"user_id": "user-1"}) - mock_vector_store.delete.assert_called_once_with(vector_id="mem-1") - - -@pytest.mark.asyncio -@patch("mem0.utils.factory.EmbedderFactory.create") -@patch("mem0.utils.factory.VectorStoreFactory.create") -@patch("mem0.utils.factory.LlmFactory.create") -@patch("mem0.memory.storage.SQLiteManager") -async def test_async_delete_continues_if_graph_cleanup_fails( - mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory -): - """Async delete() should continue even if graph cleanup fails.""" - mock_embedder_factory.return_value = MagicMock() - mock_vector_store = MagicMock() - mock_vector_factory.return_value = mock_vector_store - mock_llm_factory.return_value = MagicMock() - mock_sqlite.return_value = MagicMock() - - from mem0.memory.main import AsyncMemory - - config = MemoryConfig() - memory = AsyncMemory(config) - - memory.enable_graph = True - memory.graph = MagicMock() - memory.graph.delete.side_effect = RuntimeError("Graph error") - - mock_vector_store.get.return_value = MockVectorMemory( - "mem-1", {"data": "Alice likes Bob", "user_id": "user-1", "hash": "abc"} - ) - - result = await memory.delete("mem-1") - assert result == {"message": "Memory deleted successfully!"} - mock_vector_store.delete.assert_called_once_with(vector_id="mem-1") - - -@patch("mem0.utils.factory.EmbedderFactory.create") -@patch("mem0.utils.factory.VectorStoreFactory.create") -@patch("mem0.utils.factory.LlmFactory.create") -@patch("mem0.memory.storage.SQLiteManager") -def test_delete_raises_for_nonexistent_memory_with_graph_enabled( - mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory -): - """delete() should raise ValueError for non-existent memory even with graph enabled.""" - mock_embedder_factory.return_value = MagicMock() - mock_vector_store = MagicMock() - mock_vector_factory.return_value = mock_vector_store - mock_llm_factory.return_value = MagicMock() - mock_sqlite.return_value = MagicMock() - - from mem0.memory.main import Memory - - config = MemoryConfig() - memory = Memory(config) - - memory.enable_graph = True - memory.graph = MagicMock() - - mock_vector_store.get.return_value = None - - with pytest.raises(ValueError, match="Memory with id non-existent not found"): - memory.delete("non-existent") - - memory.graph.delete.assert_not_called() - mock_vector_store.delete.assert_not_called() - - -@pytest.mark.asyncio -@patch("mem0.utils.factory.EmbedderFactory.create") -@patch("mem0.utils.factory.VectorStoreFactory.create") -@patch("mem0.utils.factory.LlmFactory.create") -@patch("mem0.memory.storage.SQLiteManager") -async def test_async_delete_raises_for_nonexistent_memory_with_graph_enabled( - mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory -): - """Async delete() should raise ValueError for non-existent memory even with graph enabled.""" - mock_embedder_factory.return_value = MagicMock() - mock_vector_store = MagicMock() - mock_vector_store.get.return_value = None - mock_vector_factory.return_value = mock_vector_store - mock_llm_factory.return_value = MagicMock() - mock_sqlite.return_value = MagicMock() - - from mem0.memory.main import AsyncMemory - - config = MemoryConfig() - memory = AsyncMemory(config) - - memory.enable_graph = True - memory.graph = MagicMock() - - with pytest.raises(ValueError, match="Memory with id non-existent not found"): - await memory.delete("non-existent") - - memory.graph.delete.assert_not_called() - mock_vector_store.delete.assert_not_called() - - -@patch("mem0.utils.factory.EmbedderFactory.create") -@patch("mem0.utils.factory.VectorStoreFactory.create") -@patch("mem0.utils.factory.LlmFactory.create") -@patch("mem0.memory.storage.SQLiteManager") -def test_delete_all_does_not_trigger_per_memory_graph_cleanup( - mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory -): - """delete_all() should use graph.delete_all(), not per-memory graph.delete().""" - mock_embedder_factory.return_value = MagicMock() - mock_vector_store = MagicMock() - mock_vector_factory.return_value = mock_vector_store - mock_llm_factory.return_value = MagicMock() - mock_sqlite.return_value = MagicMock() - - from mem0.memory.main import Memory - - config = MemoryConfig() - memory = Memory(config) - - memory.enable_graph = True - memory.graph = MagicMock() - - mem1 = MockVectorMemory("mem-1", {"data": "Alice likes Bob", "user_id": "user-1"}) - mem2 = MockVectorMemory("mem-2", {"data": "Bob likes Charlie", "user_id": "user-1"}) - mock_vector_store.list.return_value = ([mem1, mem2], 2) - mock_vector_store.get.return_value = MockVectorMemory( - "mem-1", {"data": "Alice likes Bob", "user_id": "user-1"} - ) - - memory.delete_all(user_id="user-1") - - # graph.delete (per-memory) should NOT be called - memory.graph.delete.assert_not_called() - # graph.delete_all (bulk) SHOULD be called - memory.graph.delete_all.assert_called_once_with({"user_id": "user-1"}) - - -@patch("mem0.utils.factory.EmbedderFactory.create") -@patch("mem0.utils.factory.VectorStoreFactory.create") -@patch("mem0.utils.factory.LlmFactory.create") -@patch("mem0.memory.storage.SQLiteManager") -def test_internal_delete_memory_does_not_trigger_graph_cleanup( - mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory -): - """_delete_memory() should NOT call graph.delete() — only the public delete() does. - - This ensures that the DELETE branch inside _add_to_vector_store() (which calls - _delete_memory directly) does not interfere with the parallel graph pipeline - running in _add_to_graph(). - """ - mock_embedder_factory.return_value = MagicMock() - mock_vector_store = MagicMock() - mock_vector_factory.return_value = mock_vector_store - mock_llm_factory.return_value = MagicMock() - mock_sqlite.return_value = MagicMock() - - from mem0.memory.main import Memory - - config = MemoryConfig() - memory = Memory(config) - - memory.enable_graph = True - memory.graph = MagicMock() - - mock_vector_store.get.return_value = MockVectorMemory( - "mem-1", {"data": "Alice likes Bob", "user_id": "user-1", "hash": "abc"} - ) - - # Call _delete_memory directly (as _add_to_vector_store does for DELETE events) - memory._delete_memory("mem-1") - - # graph.delete should NOT have been called — graph cleanup is only in delete() - memory.graph.delete.assert_not_called() - # But vector store deletion should proceed - mock_vector_store.delete.assert_called_once_with(vector_id="mem-1") - - -def test_graph_memory_delete_calls_internal_methods(): - """Test that MemoryGraph.delete() calls the expected internal pipeline methods.""" - from unittest.mock import patch as _patch - - # We need to mock the Neo4j import - with _patch.dict("sys.modules", {"langchain_neo4j": MagicMock(), "rank_bm25": MagicMock()}): - from mem0.memory.graph_memory import MemoryGraph - - with _patch.object(MemoryGraph, "__init__", return_value=None): - graph = MemoryGraph.__new__(MemoryGraph) - - # Mock the internal methods - graph._retrieve_nodes_from_data = MagicMock( - return_value={"alice": "person", "bob": "person"} - ) - graph._establish_nodes_relations_from_data = MagicMock( - return_value=[ - {"source": "alice", "destination": "bob", "relationship": "likes"} - ] - ) - graph._delete_entities = MagicMock(return_value=[]) - - filters = {"user_id": "user-1"} - graph.delete("Alice likes Bob", filters) - - graph._retrieve_nodes_from_data.assert_called_once_with("Alice likes Bob", filters) - graph._establish_nodes_relations_from_data.assert_called_once_with( - "Alice likes Bob", filters, {"alice": "person", "bob": "person"} - ) - graph._delete_entities.assert_called_once_with( - [{"source": "alice", "destination": "bob", "relationship": "likes"}], - filters, - ) - - -def test_graph_memory_delete_skips_when_no_entities(): - """Test that MemoryGraph.delete() does nothing when no entities are extracted.""" - from unittest.mock import patch as _patch - - with _patch.dict("sys.modules", {"langchain_neo4j": MagicMock(), "rank_bm25": MagicMock()}): - from mem0.memory.graph_memory import MemoryGraph - - with _patch.object(MemoryGraph, "__init__", return_value=None): - graph = MemoryGraph.__new__(MemoryGraph) - - graph._retrieve_nodes_from_data = MagicMock(return_value={}) - graph._establish_nodes_relations_from_data = MagicMock() - graph._delete_entities = MagicMock() - - graph.delete("Some text", {"user_id": "user-1"}) - - graph._retrieve_nodes_from_data.assert_called_once() - graph._establish_nodes_relations_from_data.assert_not_called() - graph._delete_entities.assert_not_called() - - -def test_graph_memory_delete_handles_exception(): - """Test that MemoryGraph.delete() catches exceptions without raising.""" - from unittest.mock import patch as _patch - - with _patch.dict("sys.modules", {"langchain_neo4j": MagicMock(), "rank_bm25": MagicMock()}): - from mem0.memory.graph_memory import MemoryGraph - - with _patch.object(MemoryGraph, "__init__", return_value=None): - graph = MemoryGraph.__new__(MemoryGraph) - - graph._retrieve_nodes_from_data = MagicMock( - side_effect=RuntimeError("LLM error") - ) - - # Should not raise - graph.delete("Some text", {"user_id": "user-1"}) diff --git a/tests/test_graph_delete_e2e.py b/tests/test_graph_delete_e2e.py deleted file mode 100644 index 0169d2ce0..000000000 --- a/tests/test_graph_delete_e2e.py +++ /dev/null @@ -1,736 +0,0 @@ -""" -End-to-end tests for graph cleanup on memory deletion (issue #3245). - -Uses a real Kuzu embedded database to verify that graph entities are -correctly cleaned up when memories are deleted. LLM and embedding calls -are mocked to provide deterministic entity extraction. - -Tests are skipped automatically if kuzu is not installed. -""" - -import shutil -import tempfile -from unittest.mock import MagicMock, patch - -import pytest - -from mem0.configs.base import MemoryConfig - -try: - import kuzu # noqa: F401 - _kuzu_available = True -except ImportError: - _kuzu_available = False - -requires_kuzu = pytest.mark.skipif(not _kuzu_available, reason="kuzu is not installed") - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - - -def _node_count(kuzu_graph): - """Return total node count in the Kuzu graph.""" - result = kuzu_graph.execute("MATCH (n:Entity) RETURN count(n) AS cnt") - rows = list(result.rows_as_dict()) - return int(rows[0]["cnt"]) - - -def _edge_count(kuzu_graph): - """Return total edge count in the Kuzu graph.""" - result = kuzu_graph.execute("MATCH ()-[r:CONNECTED_TO]->() RETURN count(r) AS cnt") - rows = list(result.rows_as_dict()) - return int(rows[0]["cnt"]) - - -def _get_edges(kuzu_graph): - """Return all edges as list of (source, relationship, destination) tuples.""" - result = kuzu_graph.execute( - "MATCH (s:Entity)-[r:CONNECTED_TO]->(d:Entity) " - "RETURN s.name AS src, r.name AS rel, d.name AS dst" - ) - return [(row["src"], row["rel"], row["dst"]) for row in result.rows_as_dict()] - - -def _get_nodes(kuzu_graph): - """Return all node names.""" - result = kuzu_graph.execute("MATCH (n:Entity) RETURN n.name AS name, n.user_id AS uid") - return [(row["name"], row["uid"]) for row in result.rows_as_dict()] - - -# --------------------------------------------------------------------------- -# Fixtures -# --------------------------------------------------------------------------- - - -class MockVectorMemory: - """Mimics the object returned by vector_store.get().""" - - def __init__(self, memory_id, payload, score=0.8): - self.id = memory_id - self.payload = payload - self.score = score - - -@pytest.fixture -def kuzu_graph_memory(): - """ - Create a real Kuzu-backed MemoryGraph with mocked LLM and embedder. - Yields (graph_memory_instance, kuzu_connection) then cleans up. - """ - import os - - import kuzu - - tmpdir = tempfile.mkdtemp() - db_path = os.path.join(tmpdir, "test.kuzu") - db = kuzu.Database(db_path) - conn = kuzu.Connection(db) - - # We'll construct the MemoryGraph by bypassing __init__ and setting up manually - from mem0.memory.kuzu_memory import MemoryGraph - - mg = MemoryGraph.__new__(MemoryGraph) - - # Real Kuzu connection - mg.db = db - mg.graph = conn - mg.node_label = ":Entity" - mg.rel_label = ":CONNECTED_TO" - mg.kuzu_create_schema() - - # Deterministic embedding: use one-hot-style vectors per entity name - # to avoid accidental cosine similarity matches between different entities - embedding_dims = 64 - mg.embedding_dims = embedding_dims - - _embed_cache = {} - _embed_counter = [0] - - def deterministic_embed(text): - """Generate a deterministic, near-orthogonal embedding for each unique text.""" - text_lower = text.lower().strip() - if text_lower not in _embed_cache: - # Create a sparse vector — set a unique dimension to 1.0 - vec = [0.0] * embedding_dims - idx = _embed_counter[0] % embedding_dims - vec[idx] = 1.0 - # Add small noise to other dims so it's not exactly zero - import hashlib - - h = hashlib.sha256(text_lower.encode()).digest() - for i in range(embedding_dims): - vec[i] += float(h[i % len(h)]) / 25500.0 # tiny noise - norm = sum(v * v for v in vec) ** 0.5 - _embed_cache[text_lower] = [v / norm for v in vec] - _embed_counter[0] += 1 - return _embed_cache[text_lower] - - mock_embedder = MagicMock() - mock_embedder.embed.side_effect = deterministic_embed - mock_embedder.config.embedding_dims = embedding_dims - mg.embedding_model = mock_embedder - - # Mock LLM — configured per-test via mock_embedder - mg.llm = MagicMock() - mg.llm_provider = "openai" - mg.user_id = None - # High threshold so only identical entity names merge, not similar ones - mg.threshold = 0.99 - mg.config = MagicMock() - mg.config.graph_store.custom_prompt = None - - yield mg, conn - - # Cleanup - conn.close() - shutil.rmtree(tmpdir, ignore_errors=True) - - -def _setup_llm_for_entities(mg, entities, relations): - """ - Configure the mock LLM to return specific entities and relations. - - entities: list of {"entity": str, "entity_type": str} - relations: list of {"source": str, "destination": str, "relationship": str} - """ - - def generate_response(messages, tools): - # Detect which tool is being called based on tool definition names - tool_names = [] - for t in tools: - if isinstance(t, dict): - fn = t.get("function", t) - tool_names.append(fn.get("name", "")) - else: - tool_names.append(getattr(t, "name", str(t))) - - if any("extract_entities" in n for n in tool_names): - return { - "tool_calls": [ - { - "name": "extract_entities", - "arguments": {"entities": entities}, - } - ] - } - elif any("establish" in n or "relation" in n for n in tool_names): - return { - "tool_calls": [ - { - "name": "establish_nodes_relations", - "arguments": {"entities": relations}, - } - ] - } - elif any("delete" in n for n in tool_names): - # For _get_delete_entities_from_search_output during add() — return nothing to delete - return {"tool_calls": []} - return {"tool_calls": []} - - mg.llm.generate_response.side_effect = generate_response - - -# --------------------------------------------------------------------------- -# End-to-end tests -# --------------------------------------------------------------------------- - - -@requires_kuzu -class TestKuzuGraphDeleteE2E: - """End-to-end tests using a real Kuzu database.""" - - def test_add_creates_nodes_and_edges(self, kuzu_graph_memory): - """Baseline: verify add() actually creates graph data.""" - mg, conn = kuzu_graph_memory - - _setup_llm_for_entities( - mg, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Bob", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Bob", "relationship": "likes"}, - ], - ) - - filters = {"user_id": "test_user"} - mg.add("Alice likes Bob", filters) - - assert _node_count(conn) == 2 - assert _edge_count(conn) == 1 - edges = _get_edges(conn) - assert ("alice", "likes", "bob") in edges - - def test_delete_removes_edges_created_by_add(self, kuzu_graph_memory): - """Core test: delete() should remove the relationships that add() created.""" - mg, conn = kuzu_graph_memory - - _setup_llm_for_entities( - mg, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Bob", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Bob", "relationship": "likes"}, - ], - ) - - filters = {"user_id": "test_user"} - mg.add("Alice likes Bob", filters) - - assert _edge_count(conn) == 1 - - # Now delete using the same text — should remove the relationship - mg.delete("Alice likes Bob", filters) - - assert _edge_count(conn) == 0 - # Nodes remain (we don't delete nodes on single memory delete) - assert _node_count(conn) == 2 - - def test_delete_only_removes_matching_edges(self, kuzu_graph_memory): - """delete() should only remove edges matching the extracted relationships.""" - mg, conn = kuzu_graph_memory - - # First add: Alice likes Bob - _setup_llm_for_entities( - mg, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Bob", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Bob", "relationship": "likes"}, - ], - ) - filters = {"user_id": "test_user"} - mg.add("Alice likes Bob", filters) - - # Second add: Alice knows Charlie - _setup_llm_for_entities( - mg, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Charlie", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Charlie", "relationship": "knows"}, - ], - ) - mg.add("Alice knows Charlie", filters) - - assert _edge_count(conn) == 2 - - # Delete only the "Alice likes Bob" memory - _setup_llm_for_entities( - mg, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Bob", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Bob", "relationship": "likes"}, - ], - ) - mg.delete("Alice likes Bob", filters) - - assert _edge_count(conn) == 1 - edges = _get_edges(conn) - assert ("alice", "knows", "charlie") in edges - assert ("alice", "likes", "bob") not in edges - - def test_delete_with_different_user_id_does_not_affect_other_users(self, kuzu_graph_memory): - """delete() scoped to user_id should not touch another user's graph data.""" - mg, conn = kuzu_graph_memory - - _setup_llm_for_entities( - mg, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Bob", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Bob", "relationship": "likes"}, - ], - ) - - # Add for user1 - mg.add("Alice likes Bob", {"user_id": "user1"}) - # Add same data for user2 - mg.add("Alice likes Bob", {"user_id": "user2"}) - - assert _edge_count(conn) == 2 - - # Delete only user1's data - mg.delete("Alice likes Bob", {"user_id": "user1"}) - - assert _edge_count(conn) == 1 - # Remaining edge belongs to user2 - nodes = _get_nodes(conn) - user2_nodes = [n for n in nodes if n[1] == "user2"] - assert len(user2_nodes) == 2 - - def test_delete_nonexistent_relationship_is_safe(self, kuzu_graph_memory): - """delete() on data that doesn't exist in the graph should be a no-op.""" - mg, conn = kuzu_graph_memory - - _setup_llm_for_entities( - mg, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Bob", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Bob", "relationship": "hates"}, - ], - ) - - filters = {"user_id": "test_user"} - - # Nothing in the graph yet - assert _edge_count(conn) == 0 - assert _node_count(conn) == 0 - - # Should not raise - mg.delete("Alice hates Bob", filters) - - assert _edge_count(conn) == 0 - assert _node_count(conn) == 0 - - def test_delete_with_llm_failure_does_not_raise(self, kuzu_graph_memory): - """If LLM fails during entity extraction, delete() should not raise.""" - mg, conn = kuzu_graph_memory - - # Make LLM raise - mg.llm.generate_response.side_effect = RuntimeError("LLM service down") - - filters = {"user_id": "test_user"} - - # Should not raise - mg.delete("Alice likes Bob", filters) - - def test_delete_with_empty_entity_extraction(self, kuzu_graph_memory): - """If LLM returns no entities, delete() should be a no-op.""" - mg, conn = kuzu_graph_memory - - # Add real data - _setup_llm_for_entities( - mg, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Bob", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Bob", "relationship": "likes"}, - ], - ) - filters = {"user_id": "test_user"} - mg.add("Alice likes Bob", filters) - assert _edge_count(conn) == 1 - - # Now delete but LLM returns no entities - _setup_llm_for_entities(mg, entities=[], relations=[]) - mg.delete("some text", filters) - - # Data should still be there - assert _edge_count(conn) == 1 - - def test_delete_all_removes_everything_for_user(self, kuzu_graph_memory): - """delete_all() should remove all nodes/edges for a user (baseline behavior).""" - mg, conn = kuzu_graph_memory - - _setup_llm_for_entities( - mg, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Bob", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Bob", "relationship": "likes"}, - ], - ) - filters = {"user_id": "test_user"} - mg.add("Alice likes Bob", filters) - - _setup_llm_for_entities( - mg, - entities=[ - {"entity": "Bob", "entity_type": "person"}, - {"entity": "Charlie", "entity_type": "person"}, - ], - relations=[ - {"source": "Bob", "destination": "Charlie", "relationship": "knows"}, - ], - ) - mg.add("Bob knows Charlie", filters) - - assert _node_count(conn) >= 3 - assert _edge_count(conn) == 2 - - mg.delete_all(filters) - - assert _node_count(conn) == 0 - assert _edge_count(conn) == 0 - - def test_add_delete_add_cycle(self, kuzu_graph_memory): - """Verify that add → delete → re-add works correctly.""" - mg, conn = kuzu_graph_memory - - _setup_llm_for_entities( - mg, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Bob", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Bob", "relationship": "likes"}, - ], - ) - filters = {"user_id": "test_user"} - - # Add - mg.add("Alice likes Bob", filters) - assert _edge_count(conn) == 1 - - # Delete - mg.delete("Alice likes Bob", filters) - assert _edge_count(conn) == 0 - - # Re-add - mg.add("Alice likes Bob", filters) - assert _edge_count(conn) == 1 - edges = _get_edges(conn) - assert ("alice", "likes", "bob") in edges - - -@requires_kuzu -class TestMemoryDeleteWithGraphE2E: - """ - End-to-end tests for Memory.delete() with graph enabled. - - Uses a real Kuzu database for the graph store and mocks for - the vector store, LLM, and embedder. - """ - - @pytest.fixture - def memory_with_graph(self): - """Create a Memory instance with a real Kuzu graph backend.""" - import os - - import kuzu - - tmpdir = tempfile.mkdtemp() - - with ( - patch("mem0.utils.factory.EmbedderFactory.create") as mock_embedder_factory, - patch("mem0.utils.factory.VectorStoreFactory.create") as mock_vector_factory, - patch("mem0.utils.factory.LlmFactory.create") as mock_llm_factory, - patch("mem0.memory.storage.SQLiteManager") as mock_sqlite, - ): - _mem_embed_cache = {} - _mem_embed_counter = [0] - - def _mem_deterministic_embed(text, *args, **kwargs): - text_lower = text.lower().strip() - if text_lower not in _mem_embed_cache: - import hashlib - - vec = [0.0] * 64 - idx = _mem_embed_counter[0] % 64 - vec[idx] = 1.0 - h = hashlib.sha256(text_lower.encode()).digest() - for i in range(64): - vec[i] += float(h[i % len(h)]) / 25500.0 - norm = sum(v * v for v in vec) ** 0.5 - _mem_embed_cache[text_lower] = [v / norm for v in vec] - _mem_embed_counter[0] += 1 - return _mem_embed_cache[text_lower] - - mock_embedder = MagicMock() - mock_embedder.embed.side_effect = _mem_deterministic_embed - mock_embedder.config.embedding_dims = 64 - mock_embedder_factory.return_value = mock_embedder - - mock_vector_store = MagicMock() - mock_vector_factory.return_value = mock_vector_store - - mock_llm = MagicMock() - mock_llm_factory.return_value = mock_llm - - mock_sqlite.return_value = MagicMock() - - from mem0.memory.main import Memory - - config = MemoryConfig() - memory = Memory(config) - - # Now wire up a real Kuzu graph - db_path = os.path.join(tmpdir, "test.kuzu") - db = kuzu.Database(db_path) - conn = kuzu.Connection(db) - - from mem0.memory.kuzu_memory import MemoryGraph as KuzuMemoryGraph - - graph = KuzuMemoryGraph.__new__(KuzuMemoryGraph) - graph.db = db - graph.graph = conn - graph.node_label = ":Entity" - graph.rel_label = ":CONNECTED_TO" - graph.kuzu_create_schema() - graph.embedding_dims = 64 - graph.embedding_model = mock_embedder - graph.llm = mock_llm - graph.llm_provider = "openai" - graph.user_id = None - graph.threshold = 0.99 - graph.config = MagicMock() - graph.config.graph_store.custom_prompt = None - - memory.graph = graph - memory.enable_graph = True - - yield memory, mock_vector_store, mock_llm, conn - - conn.close() - shutil.rmtree(tmpdir, ignore_errors=True) - - def test_memory_delete_triggers_graph_cleanup(self, memory_with_graph): - """ - Full integration: Memory.delete() should clean up both vector store and graph. - """ - memory, mock_vs, mock_llm, conn = memory_with_graph - - # 1. Manually add entities to the graph (simulating what add() would do) - _setup_llm_for_memory_graph( - mock_llm, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Bob", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Bob", "relationship": "likes"}, - ], - ) - memory.graph.add("Alice likes Bob", {"user_id": "user-1"}) - assert _edge_count(conn) == 1 - - # 2. Set up mock vector store to return this memory - mock_vs.get.return_value = MockVectorMemory( - "mem-1", - {"data": "Alice likes Bob", "user_id": "user-1", "hash": "abc"}, - ) - - # 3. Delete the memory - result = memory.delete("mem-1") - - assert result == {"message": "Memory deleted successfully!"} - - # 4. Verify graph was cleaned up - assert _edge_count(conn) == 0 - - # 5. Verify vector store was also cleaned up - mock_vs.delete.assert_called_once_with(vector_id="mem-1") - - def test_memory_delete_with_graph_preserves_other_users_data(self, memory_with_graph): - """Deleting user1's memory should not affect user2's graph data.""" - memory, mock_vs, mock_llm, conn = memory_with_graph - - _setup_llm_for_memory_graph( - mock_llm, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Bob", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Bob", "relationship": "likes"}, - ], - ) - - # Add data for two users - memory.graph.add("Alice likes Bob", {"user_id": "user-1"}) - memory.graph.add("Alice likes Bob", {"user_id": "user-2"}) - assert _edge_count(conn) == 2 - - # Delete only user-1's memory - mock_vs.get.return_value = MockVectorMemory( - "mem-1", - {"data": "Alice likes Bob", "user_id": "user-1", "hash": "abc"}, - ) - memory.delete("mem-1") - - # user-2's data should be intact - assert _edge_count(conn) == 1 - nodes = _get_nodes(conn) - remaining_user_ids = set(uid for _, uid in nodes) - assert "user-2" in remaining_user_ids - - def test_memory_delete_graph_failure_still_deletes_vector(self, memory_with_graph): - """If graph cleanup fails, vector store deletion should still proceed.""" - memory, mock_vs, mock_llm, conn = memory_with_graph - - # Make LLM raise during entity extraction (graph cleanup will fail) - mock_llm.generate_response.side_effect = RuntimeError("LLM exploded") - - mock_vs.get.return_value = MockVectorMemory( - "mem-1", - {"data": "Alice likes Bob", "user_id": "user-1", "hash": "abc"}, - ) - - result = memory.delete("mem-1") - - assert result == {"message": "Memory deleted successfully!"} - mock_vs.delete.assert_called_once_with(vector_id="mem-1") - - def test_memory_delete_all_uses_bulk_not_per_memory(self, memory_with_graph): - """delete_all() should use delete_all() on graph, not per-memory delete().""" - memory, mock_vs, mock_llm, conn = memory_with_graph - - _setup_llm_for_memory_graph( - mock_llm, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Bob", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Bob", "relationship": "likes"}, - ], - ) - memory.graph.add("Alice likes Bob", {"user_id": "user-1"}) - assert _edge_count(conn) == 1 - - # Set up vector store to return memories for deletion - mem1 = MockVectorMemory("mem-1", {"data": "Alice likes Bob", "user_id": "user-1"}) - mock_vs.list.return_value = ([mem1], 1) - mock_vs.get.return_value = mem1 - - memory.delete_all(user_id="user-1") - - # After delete_all, graph should be empty (via graph.delete_all) - assert _edge_count(conn) == 0 - assert _node_count(conn) == 0 - - def test_memory_delete_nonexistent_raises_without_graph_side_effects(self, memory_with_graph): - """Deleting a non-existent memory should raise ValueError without touching graph.""" - memory, mock_vs, mock_llm, conn = memory_with_graph - - # Add some graph data that should NOT be affected - _setup_llm_for_memory_graph( - mock_llm, - entities=[ - {"entity": "Alice", "entity_type": "person"}, - {"entity": "Bob", "entity_type": "person"}, - ], - relations=[ - {"source": "Alice", "destination": "Bob", "relationship": "likes"}, - ], - ) - memory.graph.add("Alice likes Bob", {"user_id": "user-1"}) - assert _edge_count(conn) == 1 - - # Memory doesn't exist in vector store - mock_vs.get.return_value = None - - with pytest.raises(ValueError, match="Memory with id non-existent not found"): - memory.delete("non-existent") - - # Graph data should be untouched - assert _edge_count(conn) == 1 - - -def _setup_llm_for_memory_graph(mock_llm, entities, relations): - """Configure mock LLM for the Memory-level graph operations.""" - - def generate_response(messages, tools): - tool_names = [] - for t in tools: - if isinstance(t, dict): - fn = t.get("function", t) - tool_names.append(fn.get("name", "")) - else: - tool_names.append(getattr(t, "name", str(t))) - - if any("extract_entities" in n for n in tool_names): - return { - "tool_calls": [ - { - "name": "extract_entities", - "arguments": {"entities": entities}, - } - ] - } - elif any("establish" in n or "relation" in n for n in tool_names): - return { - "tool_calls": [ - { - "name": "establish_nodes_relations", - "arguments": {"entities": relations}, - } - ] - } - elif any("delete" in n for n in tool_names): - return {"tool_calls": []} - return {"tool_calls": []} - - mock_llm.generate_response.side_effect = generate_response diff --git a/tests/test_main.py b/tests/test_main.py index 2a12ce55e..850ae659f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -70,7 +70,8 @@ def memory_custom_instance(): @pytest.mark.parametrize("version, enable_graph", [("v1.0", False), ("v1.1", True)]) def test_add(memory_instance, version, enable_graph): memory_instance.config.version = version - memory_instance.enable_graph = enable_graph + if not enable_graph: + memory_instance.graph = None memory_instance._add_to_vector_store = Mock(return_value=[{"memory": "Test memory", "event": "ADD"}]) memory_instance._add_to_graph = Mock(return_value=[]) @@ -123,14 +124,16 @@ def test_get(memory_instance): @pytest.mark.parametrize("version, enable_graph", [("v1.0", False), ("v1.1", True)]) def test_search(memory_instance, version, enable_graph): memory_instance.config.version = version - memory_instance.enable_graph = enable_graph + if not enable_graph: + memory_instance.graph = None mock_memories = [ Mock(id="1", payload={"data": "Memory 1", "user_id": "test_user"}, score=0.9), Mock(id="2", payload={"data": "Memory 2", "user_id": "test_user"}, score=0.8), ] memory_instance.vector_store.search = Mock(return_value=mock_memories) memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3]) - memory_instance.graph.search = Mock(return_value=[{"relation": "test_relation"}]) + if memory_instance.graph: + memory_instance.graph.search = Mock(return_value=[{"relation": "test_relation"}]) result = memory_instance.search("test query", user_id="test_user") @@ -162,8 +165,6 @@ def test_search(memory_instance, version, enable_graph): if enable_graph: memory_instance.graph.search.assert_called_once_with("test query", {"user_id": "test_user"}, 100) - else: - memory_instance.graph.search.assert_not_called() def test_update(memory_instance): @@ -224,12 +225,14 @@ def test_delete(memory_instance): @pytest.mark.parametrize("version, enable_graph", [("v1.0", False), ("v1.1", True)]) def test_delete_all(memory_instance, version, enable_graph): memory_instance.config.version = version - memory_instance.enable_graph = enable_graph + if not enable_graph: + memory_instance.graph = None mock_memories = [Mock(id="1"), Mock(id="2")] memory_instance.vector_store.list = Mock(return_value=(mock_memories, None)) memory_instance.vector_store.reset = Mock() memory_instance._delete_memory = Mock() - memory_instance.graph.delete_all = Mock() + if memory_instance.graph: + memory_instance.graph.delete_all = Mock() result = memory_instance.delete_all(user_id="test_user") @@ -239,8 +242,6 @@ def test_delete_all(memory_instance, version, enable_graph): if enable_graph: memory_instance.graph.delete_all.assert_called_once_with({"user_id": "test_user"}) - else: - memory_instance.graph.delete_all.assert_not_called() assert result["message"] == "Memories deleted successfully!" @@ -262,12 +263,14 @@ def test_delete_all(memory_instance, version, enable_graph): ) def test_get_all(memory_instance, version, enable_graph, expected_result): memory_instance.config.version = version - memory_instance.enable_graph = enable_graph + if not enable_graph: + memory_instance.graph = None mock_memories = [Mock(id="1", payload={"data": "Memory 1", "user_id": "test_user"})] memory_instance.vector_store.list = Mock(return_value=(mock_memories, None)) - memory_instance.graph.get_all = Mock( - return_value=[{"source": "entity1", "relationship": "rel", "target": "entity2"}] - ) + if memory_instance.graph: + memory_instance.graph.get_all = Mock( + return_value=[{"source": "entity1", "relationship": "rel", "target": "entity2"}] + ) result = memory_instance.get_all(user_id="test_user") @@ -290,8 +293,6 @@ def test_get_all(memory_instance, version, enable_graph, expected_result): if enable_graph: memory_instance.graph.get_all.assert_called_once_with({"user_id": "test_user"}, 100) - else: - memory_instance.graph.get_all.assert_not_called() def test_custom_prompts(memory_custom_instance): diff --git a/tests/test_memory.py b/tests/test_memory.py index 0b0c6e251..adee7bba7 100644 --- a/tests/test_memory.py +++ b/tests/test_memory.py @@ -776,8 +776,7 @@ def test_reset_calls_graph_reset_when_graph_enabled(mock_sqlite, mock_llm_factor config = MemoryConfig() memory = Memory(config) - # Simulate graph being enabled - memory.enable_graph = True + # Simulate graph being enabled by setting graph instance mock_graph = MagicMock() memory.graph = mock_graph @@ -801,13 +800,13 @@ def test_reset_skips_graph_when_graph_disabled(mock_sqlite, mock_llm_factory, mo config = MemoryConfig() memory = Memory(config) - # Graph is disabled by default - memory.enable_graph = False + # Graph is disabled by default (graph is None) + memory.graph = None memory.reset() - # graph attribute may not even exist, but reset should not fail - assert not hasattr(memory, 'graph') or not memory.enable_graph + # graph should remain None after reset + assert memory.graph is None @patch('mem0.utils.factory.EmbedderFactory.create') @@ -825,7 +824,6 @@ def test_reset_continues_if_graph_reset_fails(mock_sqlite, mock_llm_factory, moc config = MemoryConfig() memory = Memory(config) - memory.enable_graph = True mock_graph = MagicMock() mock_graph.reset.side_effect = Exception("Neo4j connection failed") memory.graph = mock_graph