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:
co-authored by
Claude Opus 4.6
parent
e44b46ef2e
commit
9d6b79a14e
@@ -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
|
||||
|
||||
@@ -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
@@ -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") {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
```
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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"})
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user