fix(sdk): removing the enable graph flag and switching from snake case to camel case for client ts sdk (#4776)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kartik
2026-04-12 02:40:54 +05:30
committed by GitHub
co-authored by Claude Opus 4.6
parent e44b46ef2e
commit 9d6b79a14e
45 changed files with 292 additions and 1927 deletions
@@ -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
```
</CodeGroup>
## 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.
<CodeGroup>
```json Graph-aware request
{
"user_id": "alice",
"messages": [
{ "role": "user", "content": "I met with Dr. Lee at General Hospital." }
],
"enable_graph": true
}
```
</CodeGroup>
The response follows the same format, and related entities become available in [Graph Memory](/platform/features/graph-memory) queries.
@@ -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
+1 -1
View File
@@ -137,7 +137,7 @@ With Mem0 and AWS services like Bedrock, OpenSearch, and Neptune Analytics, you
<Card title="Neptune Analytics with Mem0" icon="database" href="/cookbooks/integrations/neptune-analytics">
Explore graph-based memory storage with AWS Neptune Analytics.
</Card>
<Card title="Graph Memory Features" icon="sitemap" href="/platform/features/graph-memory">
<Card title="Graph Memory Features" icon="sitemap" href="/open-source/features/graph-memory">
Learn how to leverage knowledge graphs for entity relationships.
</Card>
</CardGroup>
@@ -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 |
@@ -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));
```
+5 -2
View File
@@ -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",
@@ -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
```
</Accordion>
<Accordion title="Toggle graph writes per request">
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)
```
</Accordion>
<Accordion title="Organize multi-agent graphs">
@@ -257,9 +247,6 @@ Monitor graph growth, especially on free tiers, by periodically cleaning dormant
<Accordion title="Neptune Analytics rejects requests">
Ensure the graph identifier matches the vector dimension used by your embedder and that the IAM role allows `neptune-graph:*DataViaQuery` actions.
</Accordion>
<Accordion title="Graph store outage fallback">
Catch the provider error and retry with `enable_graph=False` so vector-only search keeps serving responses while the graph backend recovers.
</Accordion>
</AccordionGroup>
## 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: {
@@ -81,7 +81,7 @@ const messages = [
}
];
await client.add(messages, { user_id: "alice" });
await client.add(messages, { userId: "alice" });
```
</CodeGroup>
@@ -148,7 +148,7 @@ const messages = [
}
];
await client.add(messages, { user_id: "alice" });
await client.add(messages, { userId: "alice" });
```
</CodeGroup>
@@ -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") {
-8
View File
@@ -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 |
</Accordion>
<Accordion title="Graph store">
| 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` |
</Accordion>
<Accordion title="Embedder">
| Parameter | Description | Default |
@@ -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, {
</Tab>
</Tabs>
<Tip>
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.
</Tip>
## Clean up
<Tabs>
@@ -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).
<Warning>
+3 -3
View File
@@ -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" });
```
</CodeGroup>
@@ -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" });
```
</CodeGroup>
@@ -118,7 +118,7 @@ await client.delete_all(user_id="alice")
```
```javascript JavaScript
await client.deleteAll({ user_id: "alice" });
await client.deleteAll({ userId: "alice" });
```
</CodeGroup>
+7 -7
View File
@@ -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" });
```
</CodeGroup>
@@ -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
```
</CodeGroup>
@@ -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
```
</CodeGroup>
@@ -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" });
```
</CodeGroup>
@@ -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 });
```
</CodeGroup>
@@ -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);
```
</CodeGroup>
@@ -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 });
```
</CodeGroup>
</Tab>
@@ -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 });
```
</CodeGroup>
</Tab>
@@ -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 });
```
</CodeGroup>
</Tab>
-333
View File
@@ -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:
<CodeGroup>
```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"
}
]
}
```
</CodeGroup>
The graph memory would look like this:
<Frame>
<img src="/images/graph-platform.png" alt="Graph Memory Visualization showing relationships between entities" />
</Frame>
<Caption>Graph Memory creates a network of relationships between entities, enabling more contextual retrieval</Caption>
<Note>
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.
</Note>
### Searching with Graph Memory
When searching memories, Graph Memory helps retrieve entities that are contextually important even if they're not direct semantic matches.
<CodeGroup>
```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
}
]
}
```
</CodeGroup>
<Note>
`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.
</Note>
### Retrieving All Memories with Graph Memory
When retrieving all memories, Graph Memory provides additional relationship context:
<Callout type="warning" title="Filters Required">
`get_all()` now requires filters to be specified.
</Callout>
<CodeGroup>
```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"
}
]
}
```
</CodeGroup>
### 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:
<CodeGroup>
```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"
});
```
</CodeGroup>
## 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:
<Snippet file="get-help.mdx" />
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -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);
@@ -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" })
```
</CodeGroup>
+1 -1
View File
@@ -20,7 +20,7 @@ Mem0 Platform features help managed deployments scale from basic filtering to gr
<Card title="Go Real-Time with Async" icon="bolt" href="/platform/features/async-client">
Non-blocking add/search requests for agents.
</Card>
<Card title="Unlock Graph Memory" icon="circle-nodes" href="/platform/features/graph-memory">
<Card title="Unlock Graph Memory" icon="circle-nodes" href="/open-source/features/graph-memory">
Relationship-aware recall across entities.
</Card>
<Card
+2 -2
View File
@@ -76,7 +76,7 @@ const unixTimestamp = Math.floor(fiveDaysAgo.getTime() / 1000);
const messages = [
{"role": "user", "content": "I'm travelling to SF"}
]
client.add(messages, { user_id: "user1", timestamp: unixTimestamp })
client.add(messages, { userId: "user1", timestamp: unixTimestamp })
.then(response => 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));
```
+1 -1
View File
@@ -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
+38 -36
View File
@@ -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<string, any>): 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<string, any> = {
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<string, any> = {
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<AllUsers> {
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;
+44 -48
View File
@@ -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<string, any>;
infer?: boolean;
custom_categories?: custom_categories[];
custom_instructions?: string;
customCategories?: custom_categories[];
customInstructions?: string;
timestamp?: number;
structured_data_schema?: Record<string, any>;
enable_graph?: boolean;
structuredDataSchema?: Record<string, any>;
}
export interface SearchMemoryOptions {
filters?: Record<string, any>;
metadata?: Record<string, any>;
top_k?: number;
topK?: number;
threshold?: number;
rerank?: boolean;
fields?: string[];
categories?: string[];
enable_graph?: boolean;
}
export interface GetAllMemoryOptions {
filters?: Record<string, any>;
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<string>;
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<Messages>;
old_memory: string | null;
new_memory: string | null;
user_id: string;
oldMemory: string | null;
newMemory: string | null;
userId: string;
categories: Array<string>;
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<string, any>;
filters: Record<string, any>;
export_instructions?: string;
exportInstructions?: string;
}
export interface GetMemoryExportPayload {
filters?: Record<string, any>;
memory_export_id?: string;
memoryExportId?: string;
}
@@ -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.");
});
@@ -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
@@ -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
@@ -64,7 +64,7 @@ export async function waitForMemories(
): Promise<Memory[]> {
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<void> {
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<void> {
// 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
@@ -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 = "";
});
@@ -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,
},
);
@@ -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]) =>
@@ -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();
});
@@ -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);
});
+49
View File
@@ -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),
]),
);
}
-9
View File
@@ -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: {
+3 -5
View File
@@ -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
+2 -5
View File
@@ -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<void>;
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) {}
}
@@ -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");
});
-2
View File
@@ -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(),
-6
View File
@@ -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,
+3 -14
View File
@@ -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",
},
-4
View File
@@ -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")
+17 -23
View File
@@ -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:
-517
View File
@@ -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"})
-736
View File
@@ -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
+16 -15
View File
@@ -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):
+5 -7
View File
@@ -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