[Mem0] Integrate Graph Memory (#1718)
Co-authored-by: Deshraj Yadav <deshrajdry@gmail.com>
@@ -23,7 +23,7 @@
|
||||
|
||||
# Introduction
|
||||
|
||||
[Mem0](https://mem0.ai)(pronounced "mem-zero") enhances AI assistants and agents with an intelligent memory layer, enabling personalized AI interactions. Mem0 remembers user preferences, adapts to individual needs, and continuously improves over time, making it ideal for customer support chatbots, AI assistants, and autonomous systems.
|
||||
[Mem0](https://mem0.ai) (pronounced "mem-zero") enhances AI assistants and agents with an intelligent memory layer, enabling personalized AI interactions. Mem0 remembers user preferences, adapts to individual needs, and continuously improves over time, making it ideal for customer support chatbots, AI assistants, and autonomous systems.
|
||||
|
||||
### Core Features
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<Card title="Discord" icon="discord" href="https://mem0.ai/discord" color="#7289DA">
|
||||
Join our community
|
||||
</Card>
|
||||
<Card title="GitHub" icon="github" href="https://github.com/mem0ai/mem0">
|
||||
Star us on GitHub
|
||||
<Card title="GitHub" icon="github" href="https://github.com/mem0ai/mem0/discussions/new?category=q-a">
|
||||
Ask questions on GitHub
|
||||
</Card>
|
||||
<Card title="Support" icon="calendar" href="mailto:taranjeet@mem0.ai">
|
||||
<Card title="Support" icon="calendar" href="https://cal.com/taranjeetio/meet">
|
||||
Talk to founders
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
BIN
docs/images/graph_memory/graph_example1.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/images/graph_memory/graph_example2.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/images/graph_memory/graph_example3.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
docs/images/graph_memory/graph_example4.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
docs/images/graph_memory/graph_example5.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
docs/images/graph_memory/graph_example6.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
docs/images/graph_memory/graph_example7.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
docs/images/graph_memory/graph_example8.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
docs/images/graph_memory/graph_example9.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
@@ -117,7 +117,9 @@
|
||||
{
|
||||
"group": "Features",
|
||||
"pages": ["features/openai_compatibility"]
|
||||
}
|
||||
},
|
||||
"open-source/graph-memory"
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
248
docs/open-source/graph-memory.mdx
Normal file
@@ -0,0 +1,248 @@
|
||||
---
|
||||
title: Graph Memory
|
||||
description: 'Enhance your memory system with graph-based knowledge representation and retrieval'
|
||||
---
|
||||
|
||||
Mem0 now supports **Graph Memory**.
|
||||
With Graph Memory, users can now create and utilize complex relationships between pieces of information, allowing for more nuanced and context-aware responses.
|
||||
This integration enables users to leverage the strengths of both vector-based and graph-based approaches, resulting in more accurate and comprehensive information retrieval and generation.
|
||||
|
||||
|
||||
> **Note:** The Graph Memory implementation is not standalone. You will be adding/retrieving memories to the vector store and the graph store simultaneously.
|
||||
|
||||
|
||||
|
||||
## Initialize Graph Memory
|
||||
|
||||
To initialize Graph Memory you'll need to set up your configuration with graph store providers.
|
||||
Currently, we support Neo4j as a graph store provider. You can setup [Neo4j](https://neo4j.com/) locally or use the hosted [Neo4j AuraDB](https://neo4j.com/product/auradb/).
|
||||
Moreover, you also need to set the version to `v1.1` (*prior versions are not supported*).
|
||||
Here's how you can do it:
|
||||
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
config = {
|
||||
"graph_store": {
|
||||
"provider": "neo4j",
|
||||
"config": {
|
||||
"url": "neo4j+s://xxx",
|
||||
"username": "neo4j",
|
||||
"password": "xxx"
|
||||
}
|
||||
},
|
||||
"version": "v1.1"
|
||||
}
|
||||
|
||||
m = Memory.from_config(config_dict=config)
|
||||
```
|
||||
|
||||
## Graph Operations
|
||||
The Mem0's graph supports the following operations:
|
||||
|
||||
### Add Memories
|
||||
|
||||
<CodeGroup>
|
||||
```python Code
|
||||
m.add("I like pizza", user_id="alice")
|
||||
```
|
||||
|
||||
```json Output
|
||||
{'message': 'ok'}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
### Get all memories
|
||||
|
||||
<CodeGroup>
|
||||
```python Code
|
||||
m.get_all(user_id="alice")
|
||||
```
|
||||
|
||||
```json Output
|
||||
{
|
||||
'memories': [
|
||||
{
|
||||
'id': 'de69f426-0350-4101-9d0e-5055e34976a5',
|
||||
'memory': 'Likes pizza',
|
||||
'hash': '92128989705eef03ce31c462e198b47d',
|
||||
'metadata': None,
|
||||
'created_at': '2024-08-20T14:09:27.588719-07:00',
|
||||
'updated_at': None,
|
||||
'user_id': 'alice'
|
||||
}
|
||||
],
|
||||
'entities': [
|
||||
{
|
||||
'source': 'alice',
|
||||
'relationship': 'likes',
|
||||
'target': 'pizza'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Search Memories
|
||||
|
||||
<CodeGroup>
|
||||
```python Code
|
||||
m.search("tell me my name.", user_id="alice")
|
||||
```
|
||||
|
||||
```json Output
|
||||
{
|
||||
'memories': [
|
||||
{
|
||||
'id': 'de69f426-0350-4101-9d0e-5055e34976a5',
|
||||
'memory': 'Likes pizza',
|
||||
'hash': '92128989705eef03ce31c462e198b47d',
|
||||
'metadata': None,
|
||||
'created_at': '2024-08-20T14:09:27.588719-07:00',
|
||||
'updated_at': None,
|
||||
'user_id': 'alice'
|
||||
}
|
||||
],
|
||||
'entities': [
|
||||
{
|
||||
'source': 'alice',
|
||||
'relationship': 'likes',
|
||||
'target': 'pizza'
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
### Delete all Memories
|
||||
```python
|
||||
m.delete_all(user_id="alice")
|
||||
```
|
||||
|
||||
|
||||
# Example Usage
|
||||
Here's an example of how to use Mem0's graph operations:
|
||||
|
||||
1. First, we'll add some memories for a user named Alice.
|
||||
2. Then, we'll visualize how the graph evolves as we add more memories.
|
||||
3. You'll see how entities and relationships are automatically extracted and connected in the graph.
|
||||
|
||||
### Add Memories
|
||||
|
||||
Below are the steps to add memories and visualize the graph:
|
||||
|
||||
<Steps>
|
||||
<Step title="Add memory 'I like going to hikes'">
|
||||
|
||||
```python
|
||||
m.add("I like going to hikes", user_id="alice123")
|
||||
```
|
||||

|
||||
|
||||
</Step>
|
||||
<Step title="Add memory 'I love to play badminton'">
|
||||
|
||||
|
||||
```python
|
||||
m.add("I love to play badminton", user_id="alice123")
|
||||
```
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add memory 'I hate playing badminton'">
|
||||
|
||||
```python
|
||||
m.add("I hate playing badminton", user_id="alice123")
|
||||
```
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add memory 'My friend name is john and john has a dog named tommy'">
|
||||
|
||||
```python
|
||||
m.add("My friend name is john and john has a dog named tommy", user_id="alice123")
|
||||
```
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add memory 'My name is Alice'">
|
||||
|
||||
```python
|
||||
m.add("My name is Alice", user_id="alice123")
|
||||
```
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add memory 'John loves to hike and Harry loves to hike as well'">
|
||||
|
||||
```python
|
||||
m.add("John loves to hike and Harry loves to hike as well", user_id="alice123")
|
||||
```
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Step title="Add memory 'My friend peter is the spiderman'">
|
||||
|
||||
```python
|
||||
m.add("My friend peter is the spiderman", user_id="alice123")
|
||||
```
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
|
||||
### Search Memories
|
||||
|
||||
<CodeGroup>
|
||||
```python Code
|
||||
m.search("What is my name?", user_id="alice123")
|
||||
```
|
||||
|
||||
```json Output
|
||||
{
|
||||
'memories': [...],
|
||||
'entities': [
|
||||
{'source': 'alice123', 'relation': 'dislikes_playing','destination': 'badminton'},
|
||||
{'source': 'alice123', 'relation': 'friend', 'destination': 'peter'},
|
||||
{'source': 'alice123', 'relation': 'friend', 'destination': 'john'},
|
||||
{'source': 'alice123', 'relation': 'has_name', 'destination': 'alice'},
|
||||
{'source': 'alice123', 'relation': 'likes', 'destination': 'hiking'}
|
||||
]
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Below graph visualization shows what nodes and relationships are fetched from the graph for the provided query.
|
||||
|
||||

|
||||
|
||||
<CodeGroup>
|
||||
```python Code
|
||||
m.search("Who is spiderman?", user_id="alice123")
|
||||
```
|
||||
|
||||
```json Output
|
||||
{
|
||||
'memories': [...],
|
||||
'entities': [
|
||||
{'source': 'peter', 'relation': 'identity','destination': 'spiderman'}
|
||||
]
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||

|
||||
|
||||
If you want to use a managed version of Mem0, please check out [Mem0](https://app.mem0.ai). If you have any questions, please feel free to reach out to us using one of the following methods:
|
||||
|
||||
<Snippet file="get-help.mdx" />
|
||||
@@ -55,6 +55,28 @@ config = {
|
||||
m = Memory.from_config(config)
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="Advanced (Graph Memory)">
|
||||
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
config = {
|
||||
"graph_store": {
|
||||
"provider": "neo4j",
|
||||
"config": {
|
||||
"url": "neo4j+s://---",
|
||||
"username": "neo4j",
|
||||
"password": "---"
|
||||
}
|
||||
},
|
||||
"version": "v1.1"
|
||||
}
|
||||
|
||||
m = Memory.from_config(config_dict=config)
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -94,6 +116,9 @@ all_memories = m.get_all()
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
<br />
|
||||
|
||||
<CodeGroup>
|
||||
```python Code
|
||||
# Get a single memory by ID
|
||||
@@ -184,9 +209,10 @@ history = m.history(memory_id="m1")
|
||||
### Delete Memory
|
||||
|
||||
```python
|
||||
m.delete(memory_id="m1") # Delete a memory
|
||||
|
||||
m.delete_all(user_id="alice") # Delete all memories
|
||||
# Delete a memory by id
|
||||
m.delete(memory_id="m1")
|
||||
# Delete all memories for a user
|
||||
m.delete_all(user_id="alice")
|
||||
```
|
||||
|
||||
### Reset Memory
|
||||
@@ -222,7 +248,7 @@ messages = [
|
||||
"content": "I love indian food but I cannot eat pizza since allergic to cheese."
|
||||
},
|
||||
]
|
||||
user_id = "deshraj"
|
||||
user_id = "alice"
|
||||
chat_completion = client.chat.completions.create(messages=messages, model="gpt-4o-mini", user_id=user_id)
|
||||
# Memory saved after this will look like: "Loves Indian food. Allergic to cheese and cannot eat pizza."
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from mem0.memory.setup import mem0_dir
|
||||
from mem0.vector_stores.configs import VectorStoreConfig
|
||||
from mem0.llms.configs import LlmConfig
|
||||
from mem0.embeddings.configs import EmbedderConfig
|
||||
from mem0.graphs.configs import GraphStoreConfig
|
||||
|
||||
|
||||
class MemoryItem(BaseModel):
|
||||
@@ -46,3 +47,12 @@ class MemoryConfig(BaseModel):
|
||||
description="Path to the history database",
|
||||
default=os.path.join(mem0_dir, "history.db"),
|
||||
)
|
||||
graph_store: GraphStoreConfig = Field(
|
||||
description="Configuration for the graph",
|
||||
default_factory=GraphStoreConfig,
|
||||
)
|
||||
version: str = Field(
|
||||
description="The version of the API",
|
||||
default="v1.0",
|
||||
)
|
||||
|
||||
40
mem0/graphs/configs.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
|
||||
class Neo4jConfig(BaseModel):
|
||||
url: Optional[str] = Field(None, description="Host address for the graph database")
|
||||
username: Optional[str] = Field(None, description="Username for the graph database")
|
||||
password: Optional[str] = Field(None, description="Password for the graph database")
|
||||
|
||||
@model_validator(mode="before")
|
||||
def check_host_port_or_path(cls, values):
|
||||
url, username, password = (
|
||||
values.get("url"),
|
||||
values.get("username"),
|
||||
values.get("password"),
|
||||
)
|
||||
if not url and not username and not password:
|
||||
raise ValueError(
|
||||
"Please provide 'url', 'username' and 'password'."
|
||||
)
|
||||
return values
|
||||
|
||||
|
||||
class GraphStoreConfig(BaseModel):
|
||||
provider: str = Field(
|
||||
description="Provider of the data store (e.g., 'neo4j')",
|
||||
default="neo4j"
|
||||
)
|
||||
config: Neo4jConfig = Field(
|
||||
description="Configuration for the specific data store",
|
||||
default=None
|
||||
)
|
||||
|
||||
@field_validator("config")
|
||||
def validate_config(cls, v, values):
|
||||
provider = values.data.get("provider")
|
||||
if provider == "neo4j":
|
||||
return Neo4jConfig(**v.model_dump())
|
||||
else:
|
||||
raise ValueError(f"Unsupported graph store provider: {provider}")
|
||||
80
mem0/graphs/tools.py
Normal file
@@ -0,0 +1,80 @@
|
||||
|
||||
UPDATE_MEMORY_TOOL_GRAPH = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "update_graph_memory",
|
||||
"description": "Update the relationship key of an existing graph memory based on new information. This function should be called when there's a need to modify an existing relationship in the knowledge graph. The update should only be performed if the new information is more recent, more accurate, or provides additional context compared to the existing information. The source and destination nodes of the relationship must remain the same as in the existing graph memory; only the relationship itself can be updated.",
|
||||
"strict": True,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "The identifier of the source node in the relationship to be updated. This should match an existing node in the graph."
|
||||
},
|
||||
"destination": {
|
||||
"type": "string",
|
||||
"description": "The identifier of the destination node in the relationship to be updated. This should match an existing node in the graph."
|
||||
},
|
||||
"relationship": {
|
||||
"type": "string",
|
||||
"description": "The new or updated relationship between the source and destination nodes. This should be a concise, clear description of how the two nodes are connected."
|
||||
}
|
||||
},
|
||||
"required": ["source", "destination", "relationship"],
|
||||
"additionalProperties": False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ADD_MEMORY_TOOL_GRAPH = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "add_graph_memory",
|
||||
"description": "Add a new graph memory to the knowledge graph. This function creates a new relationship between two nodes, potentially creating new nodes if they don't exist.",
|
||||
"strict": True,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "The identifier of the source node in the new relationship. This can be an existing node or a new node to be created."
|
||||
},
|
||||
"destination": {
|
||||
"type": "string",
|
||||
"description": "The identifier of the destination node in the new relationship. This can be an existing node or a new node to be created."
|
||||
},
|
||||
"relationship": {
|
||||
"type": "string",
|
||||
"description": "The type of relationship between the source and destination nodes. This should be a concise, clear description of how the two nodes are connected."
|
||||
},
|
||||
"source_type": {
|
||||
"type": "string",
|
||||
"description": "The type or category of the source node. This helps in classifying and organizing nodes in the graph."
|
||||
},
|
||||
"destination_type": {
|
||||
"type": "string",
|
||||
"description": "The type or category of the destination node. This helps in classifying and organizing nodes in the graph."
|
||||
}
|
||||
},
|
||||
"required": ["source", "destination", "relationship", "source_type", "destination_type"],
|
||||
"additionalProperties": False
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NOOP_TOOL = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "noop",
|
||||
"description": "No operation should be performed to the graph entities. This function is called when the system determines that no changes or additions are necessary based on the current input or context. It serves as a placeholder action when no other actions are required, ensuring that the system can explicitly acknowledge situations where no modifications to the graph are needed.",
|
||||
"strict": True,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
"additionalProperties": False
|
||||
}
|
||||
}
|
||||
}
|
||||
107
mem0/graphs/utils.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from mem0.llms.openai import OpenAILLM
|
||||
|
||||
UPDATE_GRAPH_PROMPT = """
|
||||
You are an AI expert specializing in graph memory management and optimization. Your task is to analyze existing graph memories alongside new information, and update the relationships in the memory list to ensure the most accurate, current, and coherent representation of knowledge.
|
||||
|
||||
Input:
|
||||
1. Existing Graph Memories: A list of current graph memories, each containing source, target, and relationship information.
|
||||
2. New Graph Memory: Fresh information to be integrated into the existing graph structure.
|
||||
|
||||
Guidelines:
|
||||
1. Identification: Use the source and target as primary identifiers when matching existing memories with new information.
|
||||
2. Conflict Resolution:
|
||||
- If new information contradicts an existing memory:
|
||||
a) For matching source and target but differing content, update the relationship of the existing memory.
|
||||
b) If the new memory provides more recent or accurate information, update the existing memory accordingly.
|
||||
3. Comprehensive Review: Thoroughly examine each existing graph memory against the new information, updating relationships as necessary. Multiple updates may be required.
|
||||
4. Consistency: Maintain a uniform and clear style across all memories. Each entry should be concise yet comprehensive.
|
||||
5. Semantic Coherence: Ensure that updates maintain or improve the overall semantic structure of the graph.
|
||||
6. Temporal Awareness: If timestamps are available, consider the recency of information when making updates.
|
||||
7. Relationship Refinement: Look for opportunities to refine relationship descriptions for greater precision or clarity.
|
||||
8. Redundancy Elimination: Identify and merge any redundant or highly similar relationships that may result from the update.
|
||||
|
||||
Task Details:
|
||||
- Existing Graph Memories:
|
||||
{existing_memories}
|
||||
|
||||
- New Graph Memory: {memory}
|
||||
|
||||
Output:
|
||||
Provide a list of update instructions, each specifying the source, target, and the new relationship to be set. Only include memories that require updates.
|
||||
"""
|
||||
|
||||
EXTRACT_ENTITIES_PROMPT = """
|
||||
|
||||
You are an advanced algorithm designed to extract structured information from text to construct knowledge graphs. Your goal is to capture comprehensive information while maintaining accuracy. Follow these key principles:
|
||||
|
||||
1. Extract only explicitly stated information from the text.
|
||||
2. Identify nodes (entities/concepts), their types, and relationships.
|
||||
3. Use "USER_ID" as the source node for any self-references (I, me, my, etc.) in user messages.
|
||||
|
||||
Nodes and Types:
|
||||
- Aim for simplicity and clarity in node representation.
|
||||
- Use basic, general types for node labels (e.g. "person" instead of "mathematician").
|
||||
|
||||
Relationships:
|
||||
- Use consistent, general, and timeless relationship types.
|
||||
- Example: Prefer "PROFESSOR" over "BECAME_PROFESSOR".
|
||||
|
||||
Entity Consistency:
|
||||
- Use the most complete identifier for entities mentioned multiple times.
|
||||
- Example: Always use "John Doe" instead of variations like "Joe" or pronouns.
|
||||
|
||||
Strive for a coherent, easily understandable knowledge graph by maintaining consistency in entity references and relationship types.
|
||||
|
||||
Adhere strictly to these guidelines to ensure high-quality knowledge graph extraction."""
|
||||
|
||||
|
||||
|
||||
def get_update_memory_prompt(existing_memories, memory, template):
|
||||
return template.format(existing_memories=existing_memories, memory=memory)
|
||||
|
||||
def get_update_memory_messages(existing_memories, memory):
|
||||
return [
|
||||
{
|
||||
"role": "user",
|
||||
"content": get_update_memory_prompt(existing_memories, memory, UPDATE_GRAPH_PROMPT),
|
||||
},
|
||||
]
|
||||
|
||||
def get_search_results(entities, query):
|
||||
|
||||
search_graph_prompt = f"""
|
||||
You are an expert at searching through graph entity memories.
|
||||
When provided with existing graph entities and a query, your task is to search through the provided graph entities to find the most relevant information from the graph entities related to the query.
|
||||
The output should be from the graph entities only.
|
||||
|
||||
Here are the details of the task:
|
||||
- Existing Graph Entities (source -> relationship -> target):
|
||||
{entities}
|
||||
|
||||
- Query: {query}
|
||||
|
||||
The output should be from the graph entities only.
|
||||
The output should be in the following JSON format:
|
||||
{{
|
||||
"search_results": [
|
||||
{{
|
||||
"source_node": "source_node",
|
||||
"relationship": "relationship",
|
||||
"target_node": "target_node"
|
||||
}}
|
||||
]
|
||||
}}
|
||||
"""
|
||||
|
||||
messages = [
|
||||
{
|
||||
"role": "user",
|
||||
"content": search_graph_prompt,
|
||||
}
|
||||
]
|
||||
|
||||
llm = OpenAILLM()
|
||||
|
||||
results = llm.generate_response(messages=messages, response_format={"type": "json_object"})
|
||||
|
||||
return results
|
||||
@@ -4,9 +4,8 @@ import uuid
|
||||
import pytz
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict
|
||||
|
||||
import warnings
|
||||
from pydantic import ValidationError
|
||||
|
||||
from mem0.llms.utils.tools import (
|
||||
ADD_MEMORY_TOOL,
|
||||
DELETE_MEMORY_TOOL,
|
||||
@@ -37,6 +36,14 @@ class Memory(MemoryBase):
|
||||
self.llm = LlmFactory.create(self.config.llm.provider, self.config.llm.config)
|
||||
self.db = SQLiteManager(self.config.history_db_path)
|
||||
self.collection_name = self.config.vector_store.config.collection_name
|
||||
self.version = self.config.version
|
||||
|
||||
self.enable_graph = False
|
||||
|
||||
if self.version == "v1.1" and self.config.graph_store.config:
|
||||
from mem0.memory.main_graph import MemoryGraph
|
||||
self.graph = MemoryGraph(self.config)
|
||||
self.enable_graph = True
|
||||
|
||||
capture_event("mem0.init", self)
|
||||
|
||||
@@ -164,6 +171,14 @@ class Memory(MemoryBase):
|
||||
{"memory_id": function_result, "function_name": function_name},
|
||||
)
|
||||
capture_event("mem0.add", self)
|
||||
|
||||
if self.version == "v1.1" and self.enable_graph:
|
||||
if user_id:
|
||||
self.graph.user_id = user_id
|
||||
else:
|
||||
self.graph.user_id = "USER"
|
||||
added_entities = self.graph.add(data)
|
||||
|
||||
return {"message": "ok"}
|
||||
|
||||
def get(self, memory_id):
|
||||
@@ -234,16 +249,8 @@ class Memory(MemoryBase):
|
||||
capture_event("mem0.get_all", self, {"filters": len(filters), "limit": limit})
|
||||
memories = self.vector_store.list(filters=filters, limit=limit)
|
||||
|
||||
excluded_keys = {
|
||||
"user_id",
|
||||
"agent_id",
|
||||
"run_id",
|
||||
"hash",
|
||||
"data",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
}
|
||||
return [
|
||||
excluded_keys = {"user_id", "agent_id", "run_id", "hash", "data", "created_at", "updated_at"}
|
||||
all_memories = [
|
||||
{
|
||||
**MemoryItem(
|
||||
id=mem.id,
|
||||
@@ -272,6 +279,23 @@ class Memory(MemoryBase):
|
||||
for mem in memories[0]
|
||||
]
|
||||
|
||||
if self.version == "v1.1":
|
||||
if self.enable_graph:
|
||||
graph_entities = self.graph.get_all()
|
||||
return {"memories": all_memories, "entities": graph_entities}
|
||||
else:
|
||||
return {"memories" : all_memories}
|
||||
else:
|
||||
warnings.warn(
|
||||
"The current get_all API output format is deprecated. "
|
||||
"To use the latest format, set `api_version='v1.1'`. "
|
||||
"The current format will be removed in mem0ai 1.1.0 and later versions.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return all_memories
|
||||
|
||||
|
||||
def search(
|
||||
self, query, user_id=None, agent_id=None, run_id=None, limit=100, filters=None
|
||||
):
|
||||
@@ -302,7 +326,7 @@ class Memory(MemoryBase):
|
||||
"One of the filters: user_id, agent_id or run_id is required!"
|
||||
)
|
||||
|
||||
capture_event("mem0.search", self, {"filters": len(filters), "limit": limit})
|
||||
capture_event("mem0.search", self, {"filters": len(filters), "limit": limit, "version": self.version})
|
||||
embeddings = self.embedding_model.embed(query)
|
||||
memories = self.vector_store.search(
|
||||
query=embeddings, limit=limit, filters=filters
|
||||
@@ -318,7 +342,7 @@ class Memory(MemoryBase):
|
||||
"updated_at",
|
||||
}
|
||||
|
||||
return [
|
||||
original_memories = [
|
||||
{
|
||||
**MemoryItem(
|
||||
id=mem.id,
|
||||
@@ -348,6 +372,22 @@ class Memory(MemoryBase):
|
||||
for mem in memories
|
||||
]
|
||||
|
||||
if self.version == "v1.1":
|
||||
if self.enable_graph:
|
||||
graph_entities = self.graph.search(query)
|
||||
return {"memories": original_memories, "entities": graph_entities}
|
||||
else:
|
||||
return {"memories" : original_memories}
|
||||
else:
|
||||
warnings.warn(
|
||||
"The current get_all API output format is deprecated. "
|
||||
"To use the latest format, set `api_version='v1.1'`. "
|
||||
"The current format will be removed in mem0ai 1.1.0 and later versions.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2
|
||||
)
|
||||
return original_memories
|
||||
|
||||
def update(self, memory_id, data):
|
||||
"""
|
||||
Update a memory by ID.
|
||||
@@ -400,7 +440,11 @@ class Memory(MemoryBase):
|
||||
memories = self.vector_store.list(filters=filters)[0]
|
||||
for memory in memories:
|
||||
self._delete_memory_tool(memory.id)
|
||||
return {"message": "Memories deleted successfully!"}
|
||||
|
||||
if self.version == "v1.1" and self.enable_graph:
|
||||
self.graph.delete_all()
|
||||
|
||||
return {'message': 'Memories deleted successfully!'}
|
||||
|
||||
def history(self, memory_id):
|
||||
"""
|
||||
|
||||
284
mem0/memory/main_graph.py
Normal file
@@ -0,0 +1,284 @@
|
||||
from langchain_community.graphs import Neo4jGraph
|
||||
from pydantic import BaseModel, Field
|
||||
import json
|
||||
from openai import OpenAI
|
||||
|
||||
from mem0.embeddings.openai import OpenAIEmbedding
|
||||
from mem0.llms.openai import OpenAILLM
|
||||
from mem0.graphs.utils import get_update_memory_messages, EXTRACT_ENTITIES_PROMPT
|
||||
from mem0.graphs.tools import UPDATE_MEMORY_TOOL_GRAPH, ADD_MEMORY_TOOL_GRAPH, NOOP_TOOL
|
||||
|
||||
client = OpenAI()
|
||||
|
||||
class GraphData(BaseModel):
|
||||
source: str = Field(..., description="The source node of the relationship")
|
||||
target: str = Field(..., description="The target node of the relationship")
|
||||
relationship: str = Field(..., description="The type of the relationship")
|
||||
|
||||
class Entities(BaseModel):
|
||||
source_node: str
|
||||
source_type: str
|
||||
relation: str
|
||||
destination_node: str
|
||||
destination_type: str
|
||||
|
||||
class ADDQuery(BaseModel):
|
||||
entities: list[Entities]
|
||||
|
||||
class SEARCHQuery(BaseModel):
|
||||
nodes: list[str]
|
||||
relations: list[str]
|
||||
|
||||
def get_embedding(text):
|
||||
response = client.embeddings.create(
|
||||
model="text-embedding-3-small",
|
||||
input=text
|
||||
)
|
||||
return response.data[0].embedding
|
||||
|
||||
class MemoryGraph:
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.graph = Neo4jGraph(self.config.graph_store.config.url, self.config.graph_store.config.username, self.config.graph_store.config.password)
|
||||
|
||||
self.llm = OpenAILLM()
|
||||
self.embedding_model = OpenAIEmbedding()
|
||||
self.user_id = None
|
||||
self.threshold = 0.7
|
||||
self.model_name = "gpt-4o-2024-08-06"
|
||||
|
||||
def add(self, data):
|
||||
"""
|
||||
Adds data to the graph.
|
||||
|
||||
Args:
|
||||
data (str): The data to add to the graph.
|
||||
stored_memories (list): A list of stored memories.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the entities added to the graph.
|
||||
"""
|
||||
|
||||
# retrieve the search results
|
||||
search_output = self._search(data)
|
||||
|
||||
extracted_entities = client.beta.chat.completions.parse(
|
||||
model=self.model_name,
|
||||
messages=[
|
||||
{"role": "system", "content": EXTRACT_ENTITIES_PROMPT.replace("USER_ID", self.user_id)},
|
||||
{"role": "user", "content": data},
|
||||
],
|
||||
response_format=ADDQuery,
|
||||
temperature=0,
|
||||
).choices[0].message.parsed.entities
|
||||
|
||||
update_memory_prompt = get_update_memory_messages(search_output, extracted_entities)
|
||||
tools = [UPDATE_MEMORY_TOOL_GRAPH, ADD_MEMORY_TOOL_GRAPH, NOOP_TOOL]
|
||||
|
||||
memory_updates = client.beta.chat.completions.parse(
|
||||
model=self.model_name,
|
||||
messages=update_memory_prompt,
|
||||
tools=tools,
|
||||
temperature=0,
|
||||
).choices[0].message.tool_calls
|
||||
|
||||
to_be_added = []
|
||||
for item in memory_updates:
|
||||
function_name = item.function.name
|
||||
arguments = json.loads(item.function.arguments)
|
||||
if function_name == "add_graph_memory":
|
||||
to_be_added.append(arguments)
|
||||
elif function_name == "update_graph_memory":
|
||||
self._update_relationship(arguments['source'], arguments['destination'], arguments['relationship'])
|
||||
elif function_name == "update_name":
|
||||
self._update_name(arguments['name'])
|
||||
elif function_name == "noop":
|
||||
continue
|
||||
|
||||
new_relationships_response = []
|
||||
for item in to_be_added:
|
||||
source = item['source'].lower().replace(" ", "_")
|
||||
source_type = item['source_type'].lower().replace(" ", "_")
|
||||
relation = item['relationship'].lower().replace(" ", "_")
|
||||
destination = item['destination'].lower().replace(" ", "_")
|
||||
destination_type = item['destination_type'].lower().replace(" ", "_")
|
||||
|
||||
# Create embeddings
|
||||
source_embedding = get_embedding(source)
|
||||
dest_embedding = get_embedding(destination)
|
||||
|
||||
# Updated Cypher query to include node types and embeddings
|
||||
cypher = f"""
|
||||
MERGE (n:{source_type} {{name: $source_name}})
|
||||
ON CREATE SET n.created = timestamp(), n.embedding = $source_embedding
|
||||
ON MATCH SET n.embedding = $source_embedding
|
||||
MERGE (m:{destination_type} {{name: $dest_name}})
|
||||
ON CREATE SET m.created = timestamp(), m.embedding = $dest_embedding
|
||||
ON MATCH SET m.embedding = $dest_embedding
|
||||
MERGE (n)-[rel:{relation}]->(m)
|
||||
ON CREATE SET rel.created = timestamp()
|
||||
RETURN n, rel, m
|
||||
"""
|
||||
|
||||
params = {
|
||||
"source_name": source,
|
||||
"dest_name": destination,
|
||||
"source_embedding": source_embedding,
|
||||
"dest_embedding": dest_embedding
|
||||
}
|
||||
|
||||
result = self.graph.query(cypher, params=params)
|
||||
|
||||
|
||||
|
||||
def _search(self, query):
|
||||
search_results = client.beta.chat.completions.parse(
|
||||
model="gpt-4o-2024-08-06",
|
||||
messages=[
|
||||
{"role": "system", "content": f"You are a smart assistant who understands the entities, their types, and relations in a given text. If user message contains self reference such as 'I', 'me', 'my' etc. then use {self.user_id} as the source node. Extract the entities."},
|
||||
{"role": "user", "content": query},
|
||||
],
|
||||
response_format=SEARCHQuery,
|
||||
).choices[0].message
|
||||
|
||||
node_list = search_results.parsed.nodes
|
||||
relation_list = search_results.parsed.relations
|
||||
|
||||
node_list = [node.lower().replace(" ", "_") for node in node_list]
|
||||
relation_list = [relation.lower().replace(" ", "_") for relation in relation_list]
|
||||
|
||||
result_relations = []
|
||||
|
||||
for node in node_list:
|
||||
n_embedding = get_embedding(node)
|
||||
|
||||
cypher_query = """
|
||||
MATCH (n)
|
||||
WHERE n.embedding IS NOT NULL
|
||||
WITH n,
|
||||
round(reduce(dot = 0.0, i IN range(0, size(n.embedding)-1) | dot + n.embedding[i] * $n_embedding[i]) /
|
||||
(sqrt(reduce(l2 = 0.0, i IN range(0, size(n.embedding)-1) | l2 + n.embedding[i] * n.embedding[i])) *
|
||||
sqrt(reduce(l2 = 0.0, i IN range(0, size($n_embedding)-1) | l2 + $n_embedding[i] * $n_embedding[i]))), 4) AS similarity
|
||||
WHERE similarity >= $threshold
|
||||
MATCH (n)-[r]->(m)
|
||||
RETURN n.name AS source, elementId(n) AS source_id, type(r) AS relation, elementId(r) AS relation_id, m.name AS destination, elementId(m) AS destination_id, similarity
|
||||
UNION
|
||||
MATCH (n)
|
||||
WHERE n.embedding IS NOT NULL
|
||||
WITH n,
|
||||
round(reduce(dot = 0.0, i IN range(0, size(n.embedding)-1) | dot + n.embedding[i] * $n_embedding[i]) /
|
||||
(sqrt(reduce(l2 = 0.0, i IN range(0, size(n.embedding)-1) | l2 + n.embedding[i] * n.embedding[i])) *
|
||||
sqrt(reduce(l2 = 0.0, i IN range(0, size($n_embedding)-1) | l2 + $n_embedding[i] * $n_embedding[i]))), 4) AS similarity
|
||||
WHERE similarity >= $threshold
|
||||
MATCH (m)-[r]->(n)
|
||||
RETURN m.name AS source, elementId(m) AS source_id, type(r) AS relation, elementId(r) AS relation_id, n.name AS destination, elementId(n) AS destination_id, similarity
|
||||
ORDER BY similarity DESC
|
||||
"""
|
||||
params = {"n_embedding": n_embedding, "threshold": self.threshold}
|
||||
ans = self.graph.query(cypher_query, params=params)
|
||||
result_relations.extend(ans)
|
||||
|
||||
return result_relations
|
||||
|
||||
|
||||
def search(self, query):
|
||||
"""
|
||||
Search for memories and related graph data.
|
||||
|
||||
Args:
|
||||
query (str): Query to search for.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing:
|
||||
- "contexts": List of search results from the base data store.
|
||||
- "entities": List of related graph data based on the query.
|
||||
"""
|
||||
|
||||
search_output = self._search(query)
|
||||
search_results = []
|
||||
for item in search_output:
|
||||
search_results.append({
|
||||
"source": item['source'],
|
||||
"relation": item['relation'],
|
||||
"destination": item['destination']
|
||||
})
|
||||
|
||||
return search_results
|
||||
|
||||
|
||||
def delete_all(self):
|
||||
cypher = """
|
||||
MATCH (n)
|
||||
DETACH DELETE n
|
||||
"""
|
||||
self.graph.query(cypher)
|
||||
|
||||
|
||||
def get_all(self):
|
||||
"""
|
||||
Retrieves all nodes and relationships from the graph database based on optional filtering criteria.
|
||||
|
||||
Args:
|
||||
all_memories (list): A list of dictionaries, each containing:
|
||||
Returns:
|
||||
list: A list of dictionaries, each containing:
|
||||
- 'contexts': The base data store response for each memory.
|
||||
- 'entities': A list of strings representing the nodes and relationships
|
||||
"""
|
||||
|
||||
# return all nodes and relationships
|
||||
query = """
|
||||
MATCH (n)-[r]->(m)
|
||||
RETURN n.name AS source, type(r) AS relationship, m.name AS target
|
||||
"""
|
||||
results = self.graph.query(query)
|
||||
|
||||
final_results = []
|
||||
for result in results:
|
||||
final_results.append({
|
||||
"source": result['source'],
|
||||
"relationship": result['relationship'],
|
||||
"target": result['target']
|
||||
})
|
||||
|
||||
return final_results
|
||||
|
||||
|
||||
def _update_relationship(self, source, target, relationship):
|
||||
"""
|
||||
Update or create a relationship between two nodes in the graph.
|
||||
|
||||
Args:
|
||||
source (str): The name of the source node.
|
||||
target (str): The name of the target node.
|
||||
relationship (str): The type of the relationship.
|
||||
|
||||
Raises:
|
||||
Exception: If the operation fails.
|
||||
"""
|
||||
relationship = relationship.lower().replace(" ", "_")
|
||||
|
||||
# Check if nodes exist and create them if they don't
|
||||
check_and_create_query = """
|
||||
MERGE (n1 {name: $source})
|
||||
MERGE (n2 {name: $target})
|
||||
"""
|
||||
self.graph.query(check_and_create_query, params={"source": source, "target": target})
|
||||
|
||||
# Delete any existing relationship between the nodes
|
||||
delete_query = """
|
||||
MATCH (n1 {name: $source})-[r]->(n2 {name: $target})
|
||||
DELETE r
|
||||
"""
|
||||
self.graph.query(delete_query, params={"source": source, "target": target})
|
||||
|
||||
# Create the new relationship
|
||||
create_query = f"""
|
||||
MATCH (n1 {{name: $source}}), (n2 {{name: $target}})
|
||||
CREATE (n1)-[r:{relationship}]->(n2)
|
||||
RETURN n1, r, n2
|
||||
"""
|
||||
result = self.graph.query(create_query, params={"source": source, "target": target})
|
||||
|
||||
if not result:
|
||||
raise Exception(f"Failed to update or create relationship between {source} and {target}")
|
||||
@@ -53,7 +53,7 @@ def capture_event(event_name, memory_instance, additional_data=None):
|
||||
"vector_store": f"{memory_instance.vector_store.__class__.__module__}.{memory_instance.vector_store.__class__.__name__}",
|
||||
"llm": f"{memory_instance.llm.__class__.__module__}.{memory_instance.llm.__class__.__name__}",
|
||||
"embedding_model": f"{memory_instance.embedding_model.__class__.__module__}.{memory_instance.embedding_model.__class__.__name__}",
|
||||
"function": f"{memory_instance.__class__.__module__}.{memory_instance.__class__.__name__}",
|
||||
"function": f"{memory_instance.__class__.__module__}.{memory_instance.__class__.__name__}.{memory_instance.version}",
|
||||
}
|
||||
if additional_data:
|
||||
event_data.update(additional_data)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "mem0ai"
|
||||
version = "0.0.21"
|
||||
version = "0.1.0"
|
||||
description = "Long-term memory for AI Agents"
|
||||
authors = ["Mem0 <founders@mem0.ai>"]
|
||||
exclude = [
|
||||
|
||||