Compare commits

..
28 Commits
Author SHA1 Message Date
Prateek ChhikaraandGitHub cdee6a4ff0 Enhance update method to support metadata (#2976) 2025-06-18 10:07:18 -07:00
Dev KhantandGitHub 9eb4e77c75 Fix pinecone for async memory (#2975) 2025-06-18 01:37:45 +05:30
Akshat JainandGitHub c700d790db Fix Build CI Failure (#2973) 2025-06-17 09:39:19 -07:00
Antaripa SahaandGitHub a90b572389 Memory agent powered by voice (Cartesia + Agno) (#2970) 2025-06-17 18:54:53 +05:30
i-sunandGitHub 62c330e5b3 feat(LM Studio): Add response_format param for LM Studio to config (#2502) 2025-06-17 17:55:18 +05:30
Akshat JainandGitHub c70dc7614b Fix: Add Google Genai library support (#2941) 2025-06-17 17:47:09 +05:30
Fenil FalduandGitHub e0003247c3 feat: add AgentOps integration (#2898) 2025-06-17 11:38:39 +05:30
Saket AryanandGitHub 888ee766c5 TS SDK - filter memories param (#2971) 2025-06-17 10:54:35 +05:30
Saket AryanandGitHub c7e91171a0 Added Param output_format in AI SDK (#2960) 2025-06-15 06:42:56 +05:30
Dev KhantandGitHub 18c870ec79 version bump -> 0.1.108 (#2958) 2025-06-14 21:58:12 +05:30
Dev KhantandGitHub 3e5f68ee90 Add logger in Opensearch (#2957) 2025-06-14 21:55:22 +05:30
a0cd4065d9 +MongoDB Vector Support (#2367)
Co-authored-by: Divya Gupta <divya.gupta@mongodb.com>
2025-06-14 17:57:06 +05:30
John LockwoodandGitHub 7c0c4a03c4 Feat/add python version test envs (#2774) 2025-06-14 17:43:16 +05:30
John LockwoodandGitHub a8ace18607 Fix/pin pinecone issue #2772 (#2773) 2025-06-14 17:38:32 +05:30
Dev KhantandGitHub df43f904d1 deploy minor version -> 0.1.107rc2 (#2953) 2025-06-13 12:09:54 +05:30
Prateek ChhikaraandGitHub a5a07d711b Updates in client to support summary (#2951) 2025-06-13 12:04:38 +05:30
Dev KhantandGitHub a40268dd51 Fix: Migration in storage and version bump -> -0.1.107 (#2943) 2025-06-11 21:48:43 +05:30
c59752c6d6 Update Categorisation Flow (#2922)
Co-authored-by: Deshraj Yadav <deshraj@gatech.edu>
2025-06-11 21:24:15 +05:30
Saket AryanandGitHub aa334fb569 Updated Docs for OMM Hosted Version (#2945) 2025-06-11 08:19:03 -07:00
Antaripa SahaandGitHub 40a5e87022 Livekit Docs Update (#2933) 2025-06-09 10:20:41 -07:00
Akshat JainandGitHub 4dec9ace88 Update support for unique user IDs (#2921) 2025-06-07 21:20:40 +05:30
Dev KhantandGitHub e1dc27276b Formatting and version bump -> 0.1.107 (#2927) 2025-06-07 12:27:22 +05:30
Saket AryanandGitHub 9a12ea7b3c Version Bump/Formatting (#2923) 2025-06-06 21:49:03 +05:30
Mrinank BhowmickandGitHub e10a509645 Added cloudflare vector-store (#2607) 2025-06-06 21:35:40 +05:30
Prateek ChhikaraandGitHub fe3f10adb8 Add Wildcard Character Support Documentation for v2 Memory APIs (#2919) 2025-06-06 12:23:04 +05:30
Akshat JainandGitHub 53c91fb107 Doc : Update Readme Docs for OpenMemory environment setup (#2913) 2025-06-05 21:52:04 +05:30
Prateek ChhikaraandGitHub ecc596b11f fix error of wrong exception (#2911) 2025-06-04 16:56:52 -07:00
Prateek ChhikaraandGitHub be37fca1bb Added threshold to search (#2899) 2025-06-03 02:58:21 -07:00
62 changed files with 3640 additions and 752 deletions
+3 -1
View File
@@ -52,6 +52,8 @@ jobs:
with:
path: .venv
key: venv-mem0-${{ runner.os }}-${{ hashFiles('**/pyproject.toml') }}
- name: Install GEOS Libraries
run: sudo apt-get update && sudo apt-get install -y libgeos-dev
- name: Install dependencies
run: |
make install_all
@@ -102,4 +104,4 @@ jobs:
with:
file: coverage.xml
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+21 -15
View File
@@ -16,18 +16,19 @@ To make a contribution, follow these steps:
For more details about pull requests, please read [GitHub's guides](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
### 📦 Package manager
### 📦 Development Environment
We use `poetry` as our package manager. You can install poetry by following the instructions [here](https://python-poetry.org/docs/#installation).
Please DO NOT use pip or conda to install the dependencies. Instead, use poetry:
We use `hatch` for managing development environments. To set up:
```bash
make install_all
# Activate environment for specific Python version:
hatch shell dev_py_3_9 # Python 3.9
hatch shell dev_py_3_10 # Python 3.10
hatch shell dev_py_3_11 # Python 3.11
#activate
poetry shell
# The environment will automatically install all dev dependencies
# Run tests within the activated shell:
make test
```
### 📌 Pre-commit
@@ -40,16 +41,21 @@ pre-commit install
### 🧪 Testing
We use `pytest` to test our code. You can run the tests by running the following command:
We use `pytest` to test our code across multiple Python versions. You can run tests using:
```bash
poetry run pytest tests
# or
# Run tests with default Python version
make test
# Test specific Python versions:
make test-py-3.9 # Python 3.9 environment
make test-py-3.10 # Python 3.10 environment
make test-py-3.11 # Python 3.11 environment
# When using hatch shells, run tests with:
make test # After activating a shell with hatch shell test_XX
```
Several packages have been removed from Poetry to make the package lighter. Therefore, it is recommended to run `make install_all` to install the remaining packages and ensure all tests pass. Make sure that all tests pass before submitting a pull request.
Make sure that all tests pass across all supported Python versions before submitting a pull request.
We look forward to your pull requests and can't wait to see your contributions!
We look forward to your pull requests and can't wait to see your contributions!
+10 -1
View File
@@ -12,7 +12,7 @@ install:
install_all:
pip install ruff==0.6.9 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
google-generativeai elasticsearch opensearch-py vecs pinecone pinecone-text faiss-cpu langchain-community \
google-generativeai elasticsearch opensearch-py vecs "pinecone<7.0.0" pinecone-text faiss-cpu langchain-community \
upstash-vector azure-search-documents langchain-memgraph langchain-neo4j rank-bm25
# Format code with ruff
@@ -41,3 +41,12 @@ clean:
test:
hatch run test
test-py-3.9:
hatch run dev_py_3_9:test
test-py-3.10:
hatch run dev_py_3_10:test
test-py-3.11:
hatch run dev_py_3_11:test
@@ -9,6 +9,9 @@ The v2 get memories API is powerful and flexible, allowing for more precise memo
- `lte`: Less than or equal to
- `gt`: Greater than
- `lt`: Less than
- `ne`: Not equal to
- `icontains`: Case-insensitive containment check
- `*`: Wildcard character that matches everything
<CodeGroup>
```python Code
@@ -41,3 +44,22 @@ memories = m.get_all(
]
```
</CodeGroup>
<CodeGroup>
```python Wildcard Example
# Using wildcard to get all memories for a specific user across all run_ids
memories = m.get_all(
filters={
"AND": [
{
"user_id": "alex"
},
{
"run_id": "*"
}
]
},
version="v2"
)
```
</CodeGroup>
@@ -11,6 +11,7 @@ The v2 search API is powerful and flexible, allowing for more precise memory ret
- `lt`: Less than
- `ne`: Not equal to
- `icontains`: Case-insensitive containment check
- `*`: Wildcard character that matches everything
<CodeGroup>
```python Code
@@ -49,3 +50,23 @@ The v2 search API is powerful and flexible, allowing for more precise memory ret
}
```
</CodeGroup>
<CodeGroup>
```python Wildcard Example
# Using wildcard to match all run_ids for a specific user
all_memories = m.search(
query="What are Alice's hobbies?",
version="v2",
filters={
"AND": [
{
"user_id": "alice"
},
{
"run_id": "*"
}
]
},
)
```
</CodeGroup>
+18
View File
@@ -288,6 +288,19 @@ mode: "wide"
<Tab title="TypeScript">
<Update label="2025-06-17" description="v2.1.31">
**Improvement :**
- **Client:** Added param `filter_memories`.
</Update>
<Update label="2025-06-06" description="v2.1.30">
**New Features:**
- **OSS:** Added Cloudflare support
**Improvements:**
- **OSS:** Fixed baseURL param in LLM Config.
</Update>
<Update label="2025-05-30" description="v2.1.29">
**Improvements:**
- **Client:** Added Async Mode Param for `add` method.
@@ -684,6 +697,11 @@ mode: "wide"
<Tab title="Vercel AI SDK">
<Update label="2025-06-15" description="v1.0.6">
**New Features:**
- **Vercel AI SDK:** Added param `filter_memories`.
</Update>
<Update label="2025-05-23" description="v1.0.5">
**New Features:**
- **Vercel AI SDK:** Added support for Google provider.
+15 -7
View File
@@ -4,7 +4,11 @@ title: Gemini
<Snippet file="paper-release.mdx" />
To use Gemini model, you have to set the `GEMINI_API_KEY` environment variable. You can obtain the Gemini API key from the [Google AI Studio](https://aistudio.google.com/app/apikey)
To use the Gemini model, set the `GEMINI_API_KEY` environment variable. You can obtain the Gemini API key from [Google AI Studio](https://aistudio.google.com/app/apikey).
> **Note:** As of the latest release, Mem0 uses the new `google.genai` SDK instead of the deprecated `google.generativeai`. All message formatting and model interaction now use the updated `types` module from `google.genai`.
> **Note:** Some Gemini models are being deprecated and will retire soon. It is recommended to migrate to the latest stable models like `"gemini-2.0-flash-001"` or `"gemini-2.0-flash-lite-001"` to ensure ongoing support and improvements.
## Usage
@@ -12,28 +16,32 @@ To use Gemini model, you have to set the `GEMINI_API_KEY` environment variable.
import os
from mem0 import Memory
os.environ["OPENAI_API_KEY"] = "your-api-key" # used for embedding model
os.environ["GEMINI_API_KEY"] = "your-api-key"
os.environ["OPENAI_API_KEY"] = "your-openai-api-key" # Used for embedding model
os.environ["GEMINI_API_KEY"] = "your-gemini-api-key"
config = {
"llm": {
"provider": "gemini",
"config": {
"model": "gemini-1.5-flash-latest",
"model": "gemini-2.0-flash-001",
"temperature": 0.2,
"max_tokens": 2000,
"top_p": 1.0
}
}
}
m = Memory.from_config(config)
messages = [
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
{"role": "assistant", "content": "How about a thriller movies? They can be quite engaging."},
{"role": "user", "content": "I’m not a big fan of thriller movies but I love sci-fi movies."},
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
{"role": "user", "content": "I’m not a big fan of thrillers, but I love sci-fi movies."},
{"role": "assistant", "content": "Got it! I'll avoid thrillers and suggest sci-fi movies instead."}
]
m.add(messages, user_id="alice", metadata={"category": "movies"})
```
## Config
+1
View File
@@ -23,6 +23,7 @@ config = {
"temperature": 0.2,
"max_tokens": 2000,
"lmstudio_base_url": "http://localhost:1234/v1", # default LM Studio API URL
"lmstudio_response_format": {"type": "json_schema", "json_schema": {"type": "object", "schema": {}}},
}
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ To view all supported llms, visit the [Supported LLMs](./models).
<Card title="Gemini" href="/components/llms/models/gemini" />
<Card title="DeepSeek" href="/components/llms/models/deepseek" />
<Card title="xAI" href="/components/llms/models/xAI" />
<Card title="XAI" href="/components/llms/models/sarvam" />
<Card title="Sarvam AI" href="/components/llms/models/sarvam" />
<Card title="LM Studio" href="/components/llms/models/lmstudio" />
<Card title="Langchain" href="/components/llms/models/langchain" />
</CardGroup>
+49
View File
@@ -0,0 +1,49 @@
# MongoDB
[MongoDB](https://www.mongodb.com/) is a versatile document database that supports vector search capabilities, allowing for efficient high-dimensional similarity searches over large datasets with robust scalability and performance.
## Usage
```python
import os
from mem0 import Memory
os.environ["OPENAI_API_KEY"] = "sk-xx"
config = {
"vector_store": {
"provider": "mongodb",
"config": {
"db_name": "mem0-db",
"collection_name": "mem0-collection",
"user": "my-user",
"password": "my-password",
}
}
}
m = Memory.from_config(config)
messages = [
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
{"role": "assistant", "content": "How about a thriller movies? They can be quite engaging."},
{"role": "user", "content": "I’m not a big fan of thriller movies but I love sci-fi movies."},
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
]
m.add(messages, user_id="alice", metadata={"category": "movies"})
```
## Config
Here are the parameters available for configuring MongoDB:
| Parameter | Description | Default Value |
| --- | --- | --- |
| db_name | Name of the MongoDB database | `"mem0_db"` |
| collection_name | Name of the MongoDB collection | `"mem0_collection"` |
| embedding_model_dims | Dimensions of the embedding vectors | `1536` |
| user | MongoDB user for authentication | `None` |
| password | Password for the MongoDB user | `None` |
| host | MongoDB host | `"localhost"` |
| port | MongoDB port | `27017` |
> **Note**: `user` and `password` must either be provided together or omitted together.
@@ -0,0 +1,45 @@
[Cloudflare Vectorize](https://developers.cloudflare.com/vectorize/) is a vector database offering from Cloudflare, allowing you to build AI-powered applications with vector embeddings.
### Usage
<CodeGroup>
```typescript TypeScript
import { Memory } from 'mem0ai/oss';
const config = {
vectorStore: {
provider: 'vectorize',
config: {
indexName: 'my-memory-index',
accountId: 'your-cloudflare-account-id',
apiKey: 'your-cloudflare-api-key',
dimension: 1536, // Optional: defaults to 1536
},
},
};
const memory = new Memory(config);
const messages = [
{"role": "user", "content": "I'm looking for a good book to read."},
{"role": "assistant", "content": "Sure, what genre are you interested in?"},
{"role": "user", "content": "I enjoy fantasy novels with strong world-building."},
{"role": "assistant", "content": "Great! I'll keep that in mind for future recommendations."}
]
await memory.add(messages, { userId: "bob", metadata: { interest: "books" } });
```
</CodeGroup>
### Config
Let's see the available parameters for the `vectorize` config:
<Tabs>
<Tab title="TypeScript">
| Parameter | Description | Default Value |
| --- | --- | --- |
| `indexName` | The name of the Vectorize index | `None` (Required) |
| `accountId` | Your Cloudflare account ID | `None` (Required) |
| `apiKey` | Your Cloudflare API token | `None` (Required) |
| `dimension` | Dimensions of the embedding model | `1536` |
</Tab>
</Tabs>
+2 -1
View File
@@ -13,7 +13,7 @@ Mem0 includes built-in support for various popular databases. Memory can utilize
See the list of supported vector databases below.
<Note>
The following vector databases are supported in the Python implementation. The TypeScript implementation currently only supports Qdrant, Redis and in-memory vector database.
The following vector databases are supported in the Python implementation. The TypeScript implementation currently only supports Qdrant, Redis,Vectorize and in-memory vector database.
</Note>
<CardGroup cols={3}>
@@ -23,6 +23,7 @@ See the list of supported vector databases below.
<Card title="Upstash Vector" href="/components/vectordbs/dbs/upstash-vector"></Card>
<Card title="Milvus" href="/components/vectordbs/dbs/milvus"></Card>
<Card title="Pinecone" href="/components/vectordbs/dbs/pinecone"></Card>
<Card title="MongoDB" href="/components/vectordbs/dbs/mongodb"></Card>
<Card title="Azure" href="/components/vectordbs/dbs/azure"></Card>
<Card title="Redis" href="/components/vectordbs/dbs/redis"></Card>
<Card title="Elasticsearch" href="/components/vectordbs/dbs/elasticsearch"></Card>
+2
View File
@@ -137,6 +137,7 @@
"components/vectordbs/dbs/pgvector",
"components/vectordbs/dbs/milvus",
"components/vectordbs/dbs/pinecone",
"components/vectordbs/dbs/mongodb",
"components/vectordbs/dbs/azure",
"components/vectordbs/dbs/redis",
"components/vectordbs/dbs/elasticsearch",
@@ -235,6 +236,7 @@
"icon": "plug",
"pages": [
"integrations",
"integrations/agentops",
"integrations/vercel-ai-sdk",
"integrations/flowise",
"integrations/crewai",
+18
View File
@@ -18,6 +18,23 @@ Here are the available integrations for Mem0:
## Integrations
<CardGroup cols={2}>
<Card
title="AgentOps"
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
width="25"
height="26"
viewBox="0 0 30 36"
fill="none"
>
<path d="M10.4659 6.47277C10.45 6.37428 10.4381 6.27986 10.4303 6.18101L10.4285 6.16388C10.4212 6.09482 10.414 6.02566 10.4106 5.95626L1.18538 21.8752C0.505422 23.0493 0.323356 24.4208 0.675227 25.7289C0.849119 26.3869 1.14971 26.9859 1.55323 27.5098C1.95675 28.0338 2.46282 28.4751 3.05175 28.8143C3.83464 29.2675 4.70856 29.5 5.59028 29.5C6.03318 29.5 6.4798 29.4408 6.91899 29.3226C8.23581 28.972 9.3349 28.1326 10.0152 26.9545L15.9268 16.749V16.7449L16.5001 15.7637L17.6431 13.7936L16.5001 11.8234L15.9309 10.8381L15.9268 10.8341L13.7836 7.13406C13.6651 6.933 13.5741 6.72418 13.5109 6.51165C13.2817 5.80223 13.3292 5.04172 13.6097 4.37599L13.8115 4.02535C14.3532 3.09155 15.31 2.53987 16.3184 2.47692C16.3738 2.46915 16.4251 2.46915 16.4804 2.46915C16.5421 2.46915 16.6038 2.47257 16.6654 2.47599L16.6822 2.47692C17.6906 2.53987 18.6474 3.09155 19.1892 4.02535L21.2216 7.52838L21.8146 8.55289L21.8421 8.60399L30.1024 22.8601C30.5174 23.5814 30.6281 24.4167 30.4148 25.2205C30.1975 26.0244 29.6832 26.6942 28.9598 27.1081C28.2364 27.5258 27.3977 27.6361 26.5911 27.4195C25.7844 27.2066 25.1123 26.6905 24.6968 25.9696L18.2119 14.7788L17.069 16.7449L22.9847 26.9545C23.6646 28.1326 24.7641 28.972 26.0809 29.3226C26.5197 29.4408 26.9626 29.5 27.4096 29.5C28.2914 29.5 29.1612 29.2675 29.9482 28.8143C31.1264 28.1367 31.9728 27.0411 32.3247 25.7289C32.6766 24.4208 32.4949 23.0493 31.8145 21.8752L21.1261 3.43034C20.7029 2.51617 20.0033 1.72011 19.0621 1.18027C18.5281 0.877027 17.9708 0.675975 17.3975 0.581189C17.3027 0.565268 17.2076 0.549717 17.1129 0.537868C17.0099 0.52602 16.9074 0.518244 16.8045 0.510469C16.6027 0.498621 16.3972 0.494548 16.1914 0.510469C16.0885 0.518244 15.9859 0.52639 15.883 0.537868C15.795 0.54887 15.7067 0.563384 15.6187 0.577852L15.5984 0.581189C15.0291 0.675605 14.4673 0.876657 13.9375 1.18027C12.9885 1.72789 12.2766 2.53579 11.8537 3.46181C11.7742 3.63473 11.707 3.81282 11.6471 3.99314C11.6361 4.02668 11.6269 4.06051 11.6177 4.09435C11.612 4.11503 11.6064 4.13579 11.6003 4.15642C11.5624 4.28601 11.5275 4.41634 11.4996 4.54853C11.4885 4.60231 11.4794 4.65668 11.4703 4.71111L11.4666 4.73329C11.4443 4.86399 11.4264 4.99543 11.4145 5.12762C11.4093 5.18686 11.4045 5.24573 11.4012 5.30534C11.3934 5.44567 11.3923 5.58637 11.3963 5.72744C11.3969 5.74403 11.3962 5.76062 11.3956 5.7772C11.3949 5.79616 11.3942 5.81512 11.3952 5.83407C11.3952 5.86184 11.3952 5.88924 11.3993 5.92071C11.3998 5.9291 11.4006 5.93736 11.4014 5.94564C11.402 5.95125 11.4026 5.95687 11.403 5.96255C11.4045 5.98181 11.4064 6.00106 11.4082 6.02031C11.4097 6.03577 11.4109 6.05122 11.4122 6.06674C11.4142 6.09134 11.4163 6.11621 11.419 6.14139L11.4428 6.32282C11.4506 6.38983 11.4625 6.46092 11.4744 6.52757C11.5063 6.68863 11.5468 6.84896 11.5936 7.0078C11.5944 7.0102 11.5949 7.0127 11.5955 7.0152C11.5958 7.01662 11.5961 7.01804 11.5965 7.01944C11.5967 7.02051 11.597 7.02157 11.5974 7.02261C11.6483 7.19293 11.7081 7.36177 11.7787 7.52838C11.8619 7.72943 11.9607 7.92641 12.0715 8.11932L12.3245 8.5566V8.56067L12.4984 8.85614L12.7199 9.24232H12.7239L12.728 9.25417L14.7802 12.7927V12.7968L14.7883 12.805V12.809L15.3576 13.7943L14.7883 14.7796L8.30344 25.9703C7.88431 26.6912 7.21216 27.2077 6.40921 27.4202C6.14019 27.4913 5.86338 27.5306 5.59474 27.5306C5.053 27.5306 4.51906 27.3888 4.04085 27.1089C3.31705 26.6953 2.79909 26.0251 2.58581 25.2213C2.36845 24.4174 2.47917 23.5821 2.89829 22.8609L11.1585 8.60473L11.186 8.56141V8.55734C11.1266 8.45478 11.0753 8.35629 11.024 8.25409C11.0105 8.22496 10.9969 8.19611 10.9834 8.16739C10.9458 8.08735 10.9086 8.00836 10.8739 7.92715C10.8718 7.92504 10.8708 7.92194 10.8698 7.91887C10.8688 7.91602 10.8679 7.91319 10.8661 7.91123V7.90346C10.8423 7.8483 10.8186 7.79311 10.7989 7.73795C10.7476 7.60799 10.7041 7.47803 10.6644 7.3477C10.6012 7.15479 10.5536 6.96152 10.518 6.76861C10.4942 6.67012 10.4786 6.5757 10.4667 6.47684C10.4667 6.47684 10.47 6.47684 10.4659 6.47277Z" fill="currentColor"></path>
</svg>
}
href="/integrations/agentops"
>
Monitor and analyze Mem0 operations with comprehensive AI agent analytics and LLM observability.
</Card>
<Card
title="LangChain"
icon={
@@ -305,6 +322,7 @@ Here are the available integrations for Mem0:
>
Build autonomous agents with memory using Agno framework.
</Card>
<Card
title="Keywords AI"
icon={
+172
View File
@@ -0,0 +1,172 @@
---
title: AgentOps
---
<Snippet file="paper-release.mdx" />
Integrate [**Mem0**](https://github.com/mem0ai/mem0) with [AgentOps](https://agentops.ai), a comprehensive monitoring and analytics platform for AI agents. This integration enables automatic tracking and analysis of memory operations, providing insights into agent performance and memory usage patterns.
## Overview
1. Automatic monitoring of Mem0 operations and performance metrics
2. Real-time tracking of memory add, search, and retrieval operations
3. Analytics dashboard with memory usage patterns and insights
4. Error tracking and debugging capabilities for memory operations
## Prerequisites
Before setting up Mem0 with AgentOps, ensure you have:
1. Installed the required packages:
```bash
pip install mem0ai agentops
```
2. Valid API keys:
- [AgentOps API Key](https://app.agentops.ai/dashboard/api-keys)
- OpenAI API Key (for LLM operations)
- [Mem0 API Key](https://app.mem0.ai/dashboard/api-keys) (optional, for cloud operations)
## Basic Integration Example
The following example demonstrates how to integrate Mem0 with AgentOps monitoring for comprehensive memory operation tracking:
```python
#Import the required libraries for local memory management with Mem0
from mem0 import Memory, AsyncMemory
import os
import asyncio
import logging
from dotenv import load_dotenv
import agentops
#Set up environment variables for API keys
os.environ["AGENTOPS_API_KEY"] = os.getenv("AGENTOPS_API_KEY")
os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY")
#Set up the configuration for local memory storage and define sample user data.
local_config = {
"llm": {
"provider": "openai",
"config": {
"model": "gpt-4o-mini",
"temperature": 0.1,
"max_tokens": 2000,
},
}
}
user_id = "alice_demo"
agent_id = "assistant_demo"
run_id = "session_001"
sample_messages = [
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
{"role": "assistant", "content": "How about a thriller? They can be quite engaging."},
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
{
"role": "assistant",
"content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future.",
},
]
sample_preferences = [
"I prefer dark roast coffee over light roast",
"I exercise every morning at 6 AM",
"I'm vegetarian and avoid all meat products",
"I love reading science fiction novels",
"I work in software engineering",
]
#This function demonstrates sequential memory operations using the synchronous Memory class
def demonstrate_sync_memory(local_config, sample_messages, sample_preferences, user_id):
"""
Demonstrate synchronous Memory class operations.
"""
agentops.start_trace("mem0_memory_example", tags=["mem0_memory_example"])
try:
memory = Memory.from_config(local_config)
result = memory.add(
sample_messages, user_id=user_id, metadata={"category": "movie_preferences", "session": "demo"}
)
for i, preference in enumerate(sample_preferences):
result = memory.add(preference, user_id=user_id, metadata={"type": "preference", "index": i})
search_queries = [
"What movies does the user like?",
"What are the user's food preferences?",
"When does the user exercise?",
]
for query in search_queries:
results = memory.search(query, user_id=user_id)
if results and "results" in results:
for j, result in enumerate(results):
print(f"Result {j+1}: {result.get('memory', 'N/A')}")
else:
print("No results found")
all_memories = memory.get_all(user_id=user_id)
if all_memories and "results" in all_memories:
print(f"Total memories: {len(all_memories['results'])}")
delete_all_result = memory.delete_all(user_id=user_id)
print(f"Delete all result: {delete_all_result}")
agentops.end_trace(end_state="success")
except Exception as e:
agentops.end_trace(end_state="error")
# Execute sync demonstrations
demonstrate_sync_memory(local_config, sample_messages, sample_preferences, user_id)
```
For detailed information on this integration, refer to the official [Agentops Mem0 integration documentation](https://docs.agentops.ai/v2/integrations/mem0).
## Key Features
### 1. Automatic Operation Tracking
AgentOps automatically monitors all Mem0 operations:
- **Memory Operations**: Track add, search, get_all, delete operations and much more
- **Performance Metrics**: Monitor response times and success rates
- **Error Tracking**: Capture and analyze operation failures
### 2. Real-time Analytics Dashboard
Access comprehensive analytics through the AgentOps dashboard:
- **Usage Patterns**: Visualize memory usage trends over time
- **User Behavior**: Analyze how different users interact with memory
- **Performance Insights**: Identify bottlenecks and optimization opportunities
### 3. Session Management
Organize your monitoring with structured sessions:
- **Session Tracking**: Group related operations into logical sessions
- **Success/Failure Rates**: Track session outcomes for reliability monitoring
- **Custom Metadata**: Add context to sessions for better analysis
## Best Practices
1. **Initialize Early**: Always initialize AgentOps before importing Mem0 classes
2. **Session Management**: Use meaningful session names and end sessions appropriately
3. **Error Handling**: Wrap operations in try-catch blocks and report failures
4. **Tagging**: Use tags to organize different types of memory operations
5. **Environment Separation**: Use different projects or tags for dev/staging/prod
## Help & Resources
- [AgentOps Documentation](https://docs.agentops.ai/)
- [AgentOps Dashboard](https://app.agentops.ai/)
- [Mem0 Platform](https://app.mem0.ai/)
<Snippet file="get-help.mdx" />
+147 -117
View File
@@ -12,8 +12,7 @@ Before you begin, make sure you have:
1. Installed Livekit Agents SDK with voice dependencies of silero and deepgram:
```bash
pip install livekit \
livekit-agents \
pip install livekit-agents[voice] \
livekit-plugins-silero \
livekit-plugins-deepgram \
livekit-plugins-openai
@@ -38,7 +37,7 @@ OPENAI_API_KEY=your_openai_api_key
## Code Breakdown
Let's break down the key components of this implementation:
Let's break down the key components of this implementation using LiveKit Agents:
### 1. Setting Up Dependencies and Environment
@@ -51,17 +50,19 @@ from typing import List, Dict, Any, Annotated
import aiohttp
from dotenv import load_dotenv
from livekit.agents import (
Agent,
AgentSession,
AutoSubscribe,
JobContext,
JobProcess,
WorkerOptions,
cli,
llm,
metrics,
function_tool,
RunContext,
cli,
WorkerOptions,
ModelSettings,
)
from livekit import rtc, api
from livekit.agents.pipeline import VoicePipelineAgent
from livekit.plugins import deepgram, openai, silero
from livekit.plugins.turn_detector.multilingual import MultilingualModel
from mem0 import AsyncMemoryClient
# Load environment variables
@@ -88,36 +89,45 @@ This section handles:
### 2. Memory Enrichment Function
```python
async def _enrich_with_memory(agent: VoicePipelineAgent, chat_ctx: llm.ChatContext):
"""Add memories and Augment chat context with relevant memories"""
async def _enrich_with_memory(chat_ctx: llm.ChatContext):
"""Add memories and augment chat context with relevant memories"""
if not chat_ctx.messages:
return
# Store user message in Mem0
# Get the latest user message
user_msg = chat_ctx.messages[-1]
if user_msg.role != "user":
return
user_content = user_msg.text_content()
if not user_content:
return
# Store user message in Mem0
await mem0.add(
[{"role": "user", "content": user_msg.content}],
[{"role": "user", "content": user_content}],
user_id=USER_ID
)
# Search for relevant memories
results = await mem0.search(
user_msg.content,
user_content,
user_id=USER_ID,
)
# Augment context with retrieved memories
if results:
memories = ' '.join([result["memory"] for result in results])
logger.info(f"Enriching with memory: {memories}")
rag_msg = llm.ChatMessage.create(
# Add memory context as a assistant message
memory_msg = llm.ChatMessage.create(
text=f"Relevant Memory: {memories}\n",
role="assistant",
)
# Modify chat context with retrieved memories
chat_ctx.messages[-1] = rag_msg
chat_ctx.messages[-1] = memory_msg
chat_ctx.messages.append(user_msg)
```
@@ -130,62 +140,45 @@ This function:
### 3. Prewarm and Entrypoint Functions
```python
def prewarm_process(proc: JobProcess):
# Preload silero VAD in memory to speed up session start
def prewarm_process(proc):
"""Preload components to speed up session start"""
proc.userdata["vad"] = silero.VAD.load()
async def entrypoint(ctx: JobContext):
"""Main entrypoint for the memory-enabled voice agent"""
# Connect to LiveKit room
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
# Wait for participant
participant = await ctx.wait_for_participant()
# Initialize Mem0 client
mem0 = AsyncMemoryClient()
# Define initial system context
initial_ctx = llm.ChatContext().append(
role="system",
text=(
"""
You are a helpful voice assistant.
You are a travel guide named George and will help the user to plan a travel trip of their dreams.
You should help the user plan for various adventures like work retreats, family vacations or solo backpacking trips.
You should be careful to not suggest anything that would be dangerous, illegal or inappropriate.
You can remember past interactions and use them to inform your answers.
Use semantic memory retrieval to provide contextually relevant responses.
"""
),
)
# Create VoicePipelineAgent with memory capabilities
agent = VoicePipelineAgent(
chat_ctx=initial_ctx,
vad=silero.VAD.load(),
# Create agent session with modern 1.0 architecture
session = AgentSession(
stt=deepgram.STT(),
llm=openai.LLM(model="gpt-4o-mini"),
tts=openai.TTS(),
before_llm_cb=_enrich_with_memory,
vad=silero.VAD.load(),
turn_detection=MultilingualModel(),
)
# Start agent and initial greeting
agent.start(ctx.room, participant)
await agent.say(
"Hello! I'm George. Can I help you plan an upcoming trip? ",
allow_interruptions=True
# Create memory-enabled agent
agent = MemoryEnabledAgent()
# Start the session
await session.start(
room=ctx.room,
agent=agent,
)
# Run the application
if __name__ == "__main__":
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint, prewarm_fnc=prewarm_process))
# Initial greeting
await session.generate_reply(
instructions="Greet the user warmly as George the travel guide and ask how you can help them plan their next adventure."
)
```
The entrypoint function:
- Connects to LiveKit room
- Initializes Mem0 memory client
- Sets up initial system context
- Creates a VoicePipelineAgent with memory enrichment
- Create agent session using `AgentSession` orchestrator with memory enrichment
- Uses modern turn detection with `MultilingualModel()`
- Starts the agent with an initial greeting
## Create a Memory-Enabled Voice Agent
@@ -196,22 +189,22 @@ Now that we've explained each component, here's the complete implementation that
import asyncio
import logging
import os
from typing import List, Dict, Any, Annotated
from typing import AsyncIterable, Any
import aiohttp
from dotenv import load_dotenv
from livekit.agents import (
AutoSubscribe,
Agent,
AgentSession,
JobContext,
JobProcess,
WorkerOptions,
cli,
llm,
metrics,
function_tool,
RunContext,
cli,
WorkerOptions,
ModelSettings,
)
from livekit import rtc, api
from livekit.agents.pipeline import VoicePipelineAgent
from livekit.plugins import deepgram, openai, silero
from livekit.plugins.turn_detector.multilingual import MultilingualModel
from mem0 import AsyncMemoryClient
# Load environment variables
@@ -227,92 +220,129 @@ USER_ID = "voice_user"
# Initialize Mem0 memory client
mem0 = AsyncMemoryClient()
def prewarm_process(proc: JobProcess):
# Preload silero VAD in memory to speed up session start
proc.userdata["vad"] = silero.VAD.load()
class MemoryEnabledAgent(Agent):
"""Travel guide agent with Mem0 memory integration"""
async def entrypoint(ctx: JobContext):
# Connect to LiveKit room
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
# Wait for participant
participant = await ctx.wait_for_participant()
async def _enrich_with_memory(agent: VoicePipelineAgent, chat_ctx: llm.ChatContext):
"""Add memories and Augment chat context with relevant memories"""
def __init__(self):
super().__init__(
instructions="""
You are a helpful voice assistant.
You are a travel guide named George and will help the user to plan a travel trip of their dreams.
You should help the user plan for various adventures like work retreats, family vacations or solo backpacking trips.
You should be careful to not suggest anything that would be dangerous, illegal or inappropriate.
You can remember past interactions and use them to inform your answers.
Use semantic memory retrieval to provide contextually relevant responses.
"""
)
async def llm_node(
self,
chat_ctx: llm.ChatContext,
tools: list[llm.FunctionTool],
model_settings: ModelSettings,
) -> AsyncIterable[llm.ChatChunk]:
"""Override LLM node to add memory enrichment before inference"""
# Enrich context with memory before LLM inference
await self._enrich_with_memory(chat_ctx)
# Call default LLM node with enriched context
async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
yield chunk
async def _enrich_with_memory(self, chat_ctx: llm.ChatContext):
"""Add memories and augment chat context with relevant memories"""
if not chat_ctx.messages:
return
# Store user message in Mem0
# Get the latest user message
user_msg = chat_ctx.messages[-1]
if user_msg.role != "user":
return
user_content = user_msg.text_content()
if not user_content:
return
# Store user message in Mem0
await mem0.add(
[{"role": "user", "content": user_msg.content}],
[{"role": "user", "content": user_content}],
user_id=USER_ID
)
# Search for relevant memories
results = await mem0.search(
user_msg.content,
user_content,
user_id=USER_ID,
)
# Augment context with retrieved memories
if results:
memories = ' '.join([result["memory"] for result in results])
logger.info(f"Enriching with memory: {memories}")
rag_msg = llm.ChatMessage.create(
# Add memory context as a assistant message
memory_msg = llm.ChatMessage.create(
text=f"Relevant Memory: {memories}\n",
role="assistant",
)
# Modify chat context with retrieved memories
chat_ctx.messages[-1] = rag_msg
chat_ctx.messages[-1] = memory_msg
chat_ctx.messages.append(user_msg)
# Define initial system context
initial_ctx = llm.ChatContext().append(
role="system",
text=(
"""
You are a helpful voice assistant.
You are a travel guide named George and will help the user to plan a travel trip of their dreams.
You should help the user plan for various adventures like work retreats, family vacations or solo backpacking trips.
You should be careful to not suggest anything that would be dangerous, illegal or inappropriate.
You can remember past interactions and use them to inform your answers.
Use semantic memory retrieval to provide contextually relevant responses.
"""
),
)
def prewarm_process(proc):
"""Preload components to speed up session start"""
proc.userdata["vad"] = silero.VAD.load()
# Create VoicePipelineAgent with memory capabilities
agent = VoicePipelineAgent(
chat_ctx=initial_ctx,
vad=silero.VAD.load(),
async def entrypoint(ctx: JobContext):
"""Main entrypoint for the memory-enabled voice agent"""
# Connect to LiveKit room
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
# Initialize Mem0 client
mem0 = AsyncMemoryClient()
# Create agent session with modern 1.0 architecture
session = AgentSession(
stt=deepgram.STT(),
llm=openai.LLM(model="gpt-4o-mini"),
tts=openai.TTS(),
before_llm_cb=_enrich_with_memory,
vad=silero.VAD.load(),
turn_detection=MultilingualModel(),
)
# Start agent and initial greeting
agent.start(ctx.room, participant)
await agent.say(
"Hello! I'm George. Can I help you plan an upcoming trip? ",
# Create memory-enabled agent
agent = MemoryEnabledAgent()
# Start the session
await session.start(
room=ctx.room,
agent=agent,
)
# Initial greeting
await session.generate_reply(
instructions="Greet the user warmly as George the travel guide and ask how you can help them plan their next adventure.",
allow_interruptions=True
)
# Run the application
if __name__ == "__main__":
cli.run_app(WorkerOptions(entrypoint_fnc=entrypoint, prewarm_fnc=prewarm_process))
cli.run_app(WorkerOptions(
entrypoint_fnc=entrypoint,
prewarm_fnc=prewarm_process
))
```
## Key Features of This Implementation
1. **Semantic Memory Retrieval**: Uses Mem0 to store and retrieve contextually relevant memories
2. **Voice Interaction**: Leverages LiveKit for voice communication
2. **Voice Interaction**: Leverages LiveKit for voice communication with proper turn detection
3. **Intelligent Context Management**: Augments conversations with past interactions
4. **Travel Planning Specialization**: Focused on creating a helpful travel guide assistant
5. **Function Tools**: Modern tool definition for enhanced capabilities
## Running the Example
@@ -325,13 +355,13 @@ To run this example:
```sh
python mem0-livekit-voice-agent.py start
```
5. After the script starts, you can interact with the voice agent using [Livekit's Agent Platform](https://agents-playground.livekit.io/) and Connect to the agent inorder to start conversations.
5. After the script starts, you can interact with the voice agent using [Livekit's Agent Platform](https://agents-playground.livekit.io/) and connect to the agent inorder to start conversations.
## Best Practices for Voice Agents with Memory
1. **Context Preservation**: Store enough context with each memory for effective retrieval
2. **Privacy Considerations**: Implement secure memory management
3. **Relevant Memory Filtering**: Use semantic search to retrieve only the most pertinent memories
3. **Relevant Memory Filtering**: Use semantic search to retrieve only the most relevant memories
4. **Error Handling**: Implement robust error handling for memory operations
## Debugging Function Tools
+74 -4
View File
@@ -238,16 +238,24 @@ The Mem0's graph supports the following operations:
### Add Memories
<Note>
If you are using Mem0 with Graph Memory, it is recommended to pass `user_id`. Use `userId` in NodeSDK.
Mem0 with Graph Memory supports both `user_id` and `agent_id` parameters. You can use either or both to organize your memories. Use `userId` and `agentId` in NodeSDK.
</Note>
<CodeGroup>
```python Python
# Using only user_id
m.add("I like pizza", user_id="alice")
# Using both user_id and agent_id
m.add("I like pizza", user_id="alice", agent_id="food-assistant")
```
```typescript TypeScript
// Using only userId
memory.add("I like pizza", { userId: "alice" });
// Using both userId and agentId
memory.add("I like pizza", { userId: "alice", agentId: "food-assistant" });
```
```json Output
@@ -260,11 +268,19 @@ memory.add("I like pizza", { userId: "alice" });
<CodeGroup>
```python Python
# Get all memories for a user
m.get_all(user_id="alice")
# Get all memories for a specific agent belonging to a user
m.get_all(user_id="alice", agent_id="food-assistant")
```
```typescript TypeScript
// Get all memories for a user
memory.getAll({ userId: "alice" });
// Get all memories for a specific agent belonging to a user
memory.getAll({ userId: "alice", agentId: "food-assistant" });
```
```json Output
@@ -277,7 +293,8 @@ memory.getAll({ userId: "alice" });
'metadata': None,
'created_at': '2024-08-20T14:09:27.588719-07:00',
'updated_at': None,
'user_id': 'alice'
'user_id': 'alice',
'agent_id': 'food-assistant'
}
],
'entities': [
@@ -295,11 +312,19 @@ memory.getAll({ userId: "alice" });
<CodeGroup>
```python Python
# Search memories for a user
m.search("tell me my name.", user_id="alice")
# Search memories for a specific agent belonging to a user
m.search("tell me my name.", user_id="alice", agent_id="food-assistant")
```
```typescript TypeScript
// Search memories for a user
memory.search("tell me my name.", { userId: "alice" });
// Search memories for a specific agent belonging to a user
memory.search("tell me my name.", { userId: "alice", agentId: "food-assistant" });
```
```json Output
@@ -312,7 +337,8 @@ memory.search("tell me my name.", { userId: "alice" });
'metadata': None,
'created_at': '2024-08-20T14:09:27.588719-07:00',
'updated_at': None,
'user_id': 'alice'
'user_id': 'alice',
'agent_id': 'food-assistant'
}
],
'entities': [
@@ -331,11 +357,19 @@ memory.search("tell me my name.", { userId: "alice" });
<CodeGroup>
```python Python
# Delete all memories for a user
m.delete_all(user_id="alice")
# Delete all memories for a specific agent belonging to a user
m.delete_all(user_id="alice", agent_id="food-assistant")
```
```typescript TypeScript
// Delete all memories for a user
memory.deleteAll({ userId: "alice" });
// Delete all memories for a specific agent belonging to a user
memory.deleteAll({ userId: "alice", agentId: "food-assistant" });
```
</CodeGroup>
@@ -516,6 +550,42 @@ memory.search("Who is spiderman?", { userId: "alice123" });
> **Note:** The Graph Memory implementation is not standalone. You will be adding/retrieving memories to the vector store and the graph store simultaneously.
## Using Multiple Agents with Graph Memory
When working with multiple agents, you can use the `agent_id` parameter to organize memories by both user and agent. This allows you to:
1. Create agent-specific knowledge graphs
2. Share common knowledge between agents
3. Isolate sensitive or specialized information to specific agents
### Example: Multi-Agent Setup
<CodeGroup>
```python Python
# Add memories for different agents
m.add("I prefer Italian cuisine", user_id="bob", agent_id="food-assistant")
m.add("I'm allergic to peanuts", user_id="bob", agent_id="health-assistant")
m.add("I live in Seattle", user_id="bob") # Shared across all agents
# Search within specific agent context
food_preferences = m.search("What food do I like?", user_id="bob", agent_id="food-assistant")
health_info = m.search("What are my allergies?", user_id="bob", agent_id="health-assistant")
location = m.search("Where do I live?", user_id="bob") # Searches across all agents
```
```typescript TypeScript
// Add memories for different agents
memory.add("I prefer Italian cuisine", { userId: "bob", agentId: "food-assistant" });
memory.add("I'm allergic to peanuts", { userId: "bob", agentId: "health-assistant" });
memory.add("I live in Seattle", { userId: "bob" }); // Shared across all agents
// Search within specific agent context
const foodPreferences = memory.search("What food do I like?", { userId: "bob", agentId: "food-assistant" });
const healthInfo = memory.search("What are my allergies?", { userId: "bob", agentId: "health-assistant" });
const location = memory.search("Where do I live?", { userId: "bob" }); // Searches across all agents
```
</CodeGroup>
If you want to use a managed version of Mem0, please check out [Mem0](https://mem0.dev/pd). If you have any questions, please feel free to reach out to us using one of the following methods:
<Snippet file="get-help.mdx" />
<Snippet file="get-help.mdx" />
+18
View File
@@ -6,6 +6,24 @@ iconType: "solid"
<Snippet file="paper-release.mdx" />
## 🚀 Hosted OpenMemory MCP Now Available!
#### Sign Up Now - [app.openmemory.dev](https://app.openmemory.dev)
Everything you love about OpenMemory MCP but with zero setup.
✅ Works with all MCP-compatible tools (Claude Desktop, Cursor...)
✅ Same standard memory ops: `add_memories`, `search_memory`, etc
✅ One-click provisioning, no Docker required
✅ Powered by Mem0
Add shared, persistent, low-friction memory to your MCP-compatible clients in seconds.
### 🌟 Get Started Now
**Sign up and get your access key at [app.openmemory.dev](https://app.openmemory.dev)**
Example installation: `npx @openmemory/install --client claude --env OPENMEMORY_API_KEY=your-key`
OpenMemory is a local memory infrastructure powered by Mem0 that lets you carry your memory accross any AI app. It provides a unified memory layer that stays with you, enabling agents and assistants to remember what matters across applications.
<img src="https://github.com/user-attachments/assets/3c701757-ad82-4afa-bfbe-e049c2b4320b" alt="OpenMemory UI" />
+103 -10
View File
@@ -6,6 +6,46 @@ iconType: "solid"
<Snippet file="paper-release.mdx" />
## 🚀 Hosted OpenMemory MCP Now Available!
#### Sign Up Now - [app.openmemory.dev](https://app.openmemory.dev)
Everything you love about OpenMemory MCP but with zero setup.
✅ Works with all MCP-compatible tools (Claude Desktop, Cursor...)
✅ Same standard memory ops: `add_memories`, `search_memory`, etc
✅ One-click provisioning, no Docker required
✅ Powered by Mem0
Add shared, persistent, low-friction memory to your MCP-compatible clients in seconds.
### 🌟 Get Started Now
**Sign up and get your access key at [app.openmemory.dev](https://app.openmemory.dev)**
Example installation: `npx @openmemory/install --client claude --env OPENMEMORY_API_KEY=your-key`
## Getting Started with Hosted OpenMemory
The fastest way to get started is with our hosted version - no setup required:
### 1. Get your API key
Visit [app.openmemory.dev](https://app.openmemory.dev) to sign up and get your `OPENMEMORY_API_KEY`.
### 2. Install and connect to your preferred client
Example commands (replace `your-key` with your actual API key):
For Claude Desktop: `npx @openmemory/install --client claude --env OPENMEMORY_API_KEY=your-key`
For Cursor: `npx @openmemory/install --client cursor --env OPENMEMORY_API_KEY=your-key`
For Windsurf: `npx @openmemory/install --client windsurf --env OPENMEMORY_API_KEY=your-key`
That's it! Your AI client now has persistent memory across sessions.
## Local Setup (Self-Hosted)
Prefer to run OpenMemory locally? Follow the instructions below for a self-hosted setup.
## OpenMemory Easy Setup
### Prerequisites
@@ -38,22 +78,75 @@ We suggest you follow the instructions below to set up OpenMemory on your local
Getting started with OpenMemory is straight forward and takes just a few minutes to set up on your local machine. Follow these steps:
### Getting started
First clone the repository and then follow the instructions:
### 1. First clone the repository and then follow the instructions:
```bash
# Clone the repository
git clone https://github.com/mem0ai/mem0.git
cd mem0/openmemory
# Create the backend .env file with your OpenAI key
make env
# Build the Docker images
make build
# Start all services (API server, vector database, and MCP server components)
make up
```
### 2. Set Up Environment Variables
Before running the project, you need to configure environment variables for both the API and the UI.
You can do this in one of the following ways:
- **Manually**:
Create a `.env` file in each of the following directories:
- `/api/.env`
- `/ui/.env`
- **Using `.env.example` files**:
Copy and rename the example files:
```bash
cp api/.env.example api/.env
cp ui/.env.example ui/.env
```
- **Using Makefile** (if supported):
Run:
```bash
make env
```
- #### Example `/api/.env`
``` bash
OPENAI_API_KEY=sk-xxx
USER=<user-id> # The User Id you want to associate the memories with
```
- #### Example `/ui/.env`
```bash
NEXT_PUBLIC_API_URL=http://localhost:8765
NEXT_PUBLIC_USER_ID=<user-id> # Same as the user id for environment variable in api
```
### 3. Build and Run the Project
You can run the project using the following two commands:
```bash
make build # builds the mcp server and ui
make up # runs openmemory mcp server and ui
```
After running these commands, you will have:
- OpenMemory MCP server running at: http://localhost:8765 (API documentation available at http://localhost:8765/docs)
- OpenMemory UI running at: http://localhost:3000
#### UI not working on http://localhost:3000?
If the UI does not start properly on http://localhost:3000, try running it manually:
```bash
cd ui
pnpm install
pnpm dev
```
You can configure the MCP client using the following command (replace username with your username):
```bash
+198 -8
View File
@@ -458,7 +458,7 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \
#### Search using custom filters
Our advanced search allows you to set custom search filters. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and text. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains). For more details, see [V2 Search Memories](/api-reference/memory/v2-search-memories).
Our advanced search allows you to set custom search filters. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and text. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains, *). The wildcard character (*) matches everything for a specific field. For more details, see [V2 Search Memories](/api-reference/memory/v2-search-memories).
Here you need to define `version` as `v2` in the search method.
@@ -765,6 +765,81 @@ curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \
</CodeGroup>
Example 5: Search using wildcard filters
<CodeGroup>
```python Python
query = "What do you know about me?"
filters = {
"AND": [
{
"user_id": "alex"
},
{
"run_id": "*" # Matches all run_ids
}
]
}
client.search(query, version="v2", filters=filters)
```
```javascript JavaScript
const query = "What do you know about me?";
const filters = {
"AND": [
{
"user_id": "alex"
},
{
"run_id": "*" // Matches all run_ids
}
]
};
client.search(query, { version: "v2", filters })
.then(results => console.log(results))
.catch(error => console.error(error));
```
```bash cURL
curl -X POST "https://api.mem0.ai/v1/memories/search/?version=v2" \
-H "Authorization: Token your-api-key" \
-H "Content-Type: application/json" \
-d '{
"query": "What do you know about me?",
"filters": {
"AND": [
{
"user_id": "alex"
},
{
"run_id": "*"
}
]
}
}'
```
```json Output
{
"results": [
{
"id": "7f165f7e-b411-4afe-b7e5-35789b72c4a5",
"memory": "Name: Alex. Vegetarian. Allergic to nuts.",
"user_id": "alex",
"run_id": "session-1",
"metadata": null,
"categories": ["food_preferences"],
"immutable": false,
"expiration_date": null,
"created_at": "2024-07-20T01:30:36.275141-07:00",
"updated_at": "2024-07-20T01:30:36.275172-07:00"
}
]
}
```
</CodeGroup>
### 4.3 Get All Users
@@ -1138,7 +1213,7 @@ curl -X GET "https://api.mem0.ai/v1/memories/?user_id=alex&keywords=to play&page
#### Get all memories using custom filters
Our advanced retrieval allows you to set custom filters when fetching memories. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and keywords. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains). For more details, see [v2 Get Memories](/api-reference/memory/v2-get-memories).
Our advanced retrieval allows you to set custom filters when fetching memories. You can filter by user_id, agent_id, app_id, run_id, created_at, updated_at, categories, and keywords. The filters support logical operators (AND, OR) and comparison operators (in, gte, lte, gt, lt, ne, contains, icontains, *). The wildcard character (*) matches everything for a specific field. For more details, see [v2 Get Memories](/api-reference/memory/v2-get-memories).
Here you need to define `version` as `v2` in the get_all method.
@@ -1497,6 +1572,119 @@ curl -X GET "https://api.mem0.ai/v1/memories/?version=v2&page=1&page_size=50" \
</CodeGroup>
Example 4: Get all memories using wildcard filters
<CodeGroup>
```python Python
filters = {
"AND": [
{
"user_id": "alex"
},
{
"run_id": "*" # Matches all run_ids
}
]
}
# Default (No Pagination)
client.get_all(version="v2", filters=filters)
# Pagination (You can also use the page and page_size parameters)
client.get_all(version="v2", filters=filters, page=1, page_size=50)
```
```javascript JavaScript
const filters = {
"AND": [
{
"user_id": "alex"
},
{
"run_id": "*" // Matches all run_ids
}
]
};
// Default (No Pagination)
client.getAll({ version: "v2", filters })
.then(memories => console.log(memories))
.catch(error => console.error(error));
// Pagination (You can also use the page and page_size parameters)
client.getAll({ version: "v2", filters, page: 1, page_size: 50 })
.then(memories => console.log(memories))
.catch(error => console.error(error));
```
```bash cURL
# Default (No Pagination)
curl -X GET "https://api.mem0.ai/v1/memories/?version=v2" \
-H "Authorization: Token your-api-key" \
-H "Content-Type: application/json" \
-d '{
"filters": {
"AND": [
{"user_id":"alex"},
{"run_id": "*"}
]
}
}'
# Pagination (You can also use the page and page_size parameters)
curl -X GET "https://api.mem0.ai/v1/memories/?version=v2&page=1&page_size=50" \
-H "Authorization: Token your-api-key" \
-H "Content-Type: application/json" \
-d '{
"filters": {
"AND": [
{"user_id":"alex"},
{"run_id": "*"}
]
}
}'
```
```json Output (Default)
[
{
"id": "f38b689d-6b24-45b7-bced-17fbb4d8bac7",
"memory": "Name: Alex. Vegetarian. Allergic to nuts.",
"user_id": "alex",
"run_id": "session-1",
"metadata": null,
"categories": ["food_preferences"],
"immutable": false,
"expiration_date": null,
"created_at": "2024-07-20T01:30:36.275141-07:00",
"updated_at": "2024-07-20T01:30:36.275172-07:00"
}
]
```
```json Output (Paginated)
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": "f38b689d-6b24-45b7-bced-17fbb4d8bac7",
"memory": "Name: Alex. Vegetarian. Allergic to nuts.",
"user_id": "alex",
"run_id": "session-1",
"metadata": null,
"categories": ["food_preferences"],
"immutable": false,
"expiration_date": null,
"created_at": "2024-07-20T01:30:36.275141-07:00",
"updated_at": "2024-07-20T01:30:36.275172-07:00"
}
]
}
```
</CodeGroup>
### 4.5 Memory History
@@ -1592,24 +1780,26 @@ curl -X GET "https://api.mem0.ai/v1/memories/<memory-id-here>/history/" \
### 4.6 Update Memory
Update a memory with new data.
Update a memory with new data. You can update the memory's text, metadata, or both.
<CodeGroup>
```python Python
message = "I recently tried chicken and I loved it. I'm thinking of trying more non-vegetarian dishes.."
client.update(memory_id, message)
client.update(
memory_id="<memory-id-here>",
text="I am now a vegetarian.",
metadata={"diet": "vegetarian"}
)
```
```javascript JavaScript
const message = "I recently tried chicken and I loved it. I'm thinking of trying more non-vegetarian dishes..";
client.update("memory-id-here", message)
client.update("memory-id-here", { text: "I am now a vegetarian.", metadata: { diet: "vegetarian" } })
.then(result => console.log(result))
.catch(error => console.error(error));
```
```bash cURL
curl -X PUT "https://api.mem0.ai/v1/memories/memory-id-here" \
curl -X PUT "https://api.mem0.ai/v1/memories/<memory-id-here>" \
-H "Authorization: Token your-api-key" \
-H "Content-Type: application/json" \
-d '{
+2 -2
View File
@@ -2552,7 +2552,7 @@ azure = ["adlfs (>=2024.2.0)"]
clip = ["open-clip", "pillow", "torch"]
dev = ["pre-commit", "ruff"]
docs = ["mkdocs", "mkdocs-jupyter", "mkdocs-material", "mkdocstrings[python]"]
embeddings = ["awscli (>=1.29.57)", "boto3 (>=1.28.57)", "botocore (>=1.31.57)", "cohere", "google-generativeai", "huggingface-hub", "instructorembedding", "open-clip-torch", "openai (>=1.6.1)", "pillow", "sentence-transformers", "torch"]
embeddings = ["awscli (>=1.29.57)", "boto3 (>=1.28.57)", "botocore (>=1.31.57)", "cohere", "google-generativeai", "huggingface-hub", "instructorembedding", "open-clip-torch", "openai (>=1.6.1)", "pillow", "sentence-transformers", "torch", "google-genai"]
tests = ["aiohttp", "boto3", "duckdb", "pandas (>=1.4)", "polars (>=0.19)", "pytest", "pytest-asyncio", "pytest-mock", "pytz", "tantivy"]
[[package]]
@@ -7129,7 +7129,7 @@ cffi = ["cffi (>=1.11)"]
aws = ["langchain-aws"]
elasticsearch = ["elasticsearch"]
gmail = ["google-api-core", "google-api-python-client", "google-auth", "google-auth-httplib2", "google-auth-oauthlib", "requests"]
google = ["google-generativeai"]
google = ["google-generativeai", "google-genai"]
googledrive = ["google-api-python-client", "google-auth-httplib2", "google-auth-oauthlib"]
lancedb = ["lancedb"]
llama2 = ["replicate"]
@@ -0,0 +1,124 @@
"""Simple Voice Agent with Memory: Personal Food Assistant.
A food assistant that remembers your dietary preferences and speaks recommendations
Powered by Agno + Cartesia + Mem0
export MEM0_API_KEY=your_mem0_api_key
export OPENAI_API_KEY=your_openai_api_key
export CARTESIA_API_KEY=your_cartesia_api_key
"""
from textwrap import dedent
from agno.agent import Agent
from agno.models.openai import OpenAIChat
from agno.tools.cartesia import CartesiaTools
from agno.utils.audio import write_audio_to_file
from mem0 import MemoryClient
memory_client = MemoryClient()
USER_ID = "food_user_01"
# Agent instructions
agent_instructions = dedent(
"""Follow these steps SEQUENTIALLY to provide personalized food recommendations with voice:
1. Analyze the user's food request and identify what type of recommendation they need.
2. Consider their dietary preferences, restrictions, and cooking habits from memory context.
3. Generate a personalized food recommendation based on their stored preferences.
4. Analyze the appropriate tone for the response (helpful, enthusiastic, cautious for allergies).
5. Call `list_voices` to retrieve available voices.
6. Select a voice that matches the helpful, friendly tone.
7. Call `text_to_speech` to generate the final audio recommendation.
"""
)
# Simple agent that remembers food preferences
food_agent = Agent(
name="Personal Food Assistant",
description="Provides personalized food recommendations with memory and generates voice responses using Cartesia TTS tools.",
instructions=agent_instructions,
model=OpenAIChat(id="gpt-4o"),
tools=[CartesiaTools(voice_localize_enabled=True)],
show_tool_calls=True,
)
def get_food_recommendation(user_query: str, user_id):
"""Get food recommendation with memory context"""
# Search memory for relevant food preferences
memories_result = memory_client.search(
query=user_query,
user_id=user_id,
limit=5
)
# Add memory context to the message
memories = [f"- {result['memory']}" for result in memories_result]
memory_context = "Memories about user that might be relevant:\n" + "\n".join(memories)
# Combine memory context with user request
full_request = f"""
{memory_context}
User: {user_query}
Answer the user query based on provided context and create a voice note.
"""
# Generate response with voice (same pattern as translator)
food_agent.print_response(full_request)
response = food_agent.run_response
# Save audio file
if response.audio:
import time
timestamp = int(time.time())
filename = f"food_recommendation_{timestamp}.mp3"
write_audio_to_file(
response.audio[0].base64_audio,
filename=filename,
)
print(f"Audio saved as {filename}")
return response.content
def initialize_food_memory(user_id):
"""Initialize memory with food preferences"""
messages = [
{
"role": "user",
"content": "Hi, I'm Sarah. I'm vegetarian and lactose intolerant. I love spicy food, especially Thai and Indian cuisine.",
},
{
"role": "assistant",
"content": "Hello Sarah! I've noted that you're vegetarian, lactose intolerant, and love spicy Thai and Indian food.",
},
{
"role": "user",
"content": "I prefer quick breakfasts since I'm always rushing, but I like cooking elaborate dinners. I also meal prep on Sundays.",
},
{
"role": "assistant",
"content": "Got it! Quick breakfasts, elaborate dinners, and Sunday meal prep. I'll remember this for future recommendations.",
},
{
"role": "user",
"content": "I'm trying to eat more protein. I like quinoa, lentils, chickpeas, and tofu. I hate mushrooms though.",
},
{
"role": "assistant",
"content": "Perfect! I'll focus on protein-rich options like quinoa, lentils, chickpeas, and tofu, and avoid mushrooms.",
},
]
memory_client.add(messages, user_id=user_id)
print("Food preferences stored in memory")
# Initialize the memory for the user once in order for the agent to learn the user preference
initialize_food_memory(user_id=USER_ID)
print(get_food_recommendation("Which type of restaurants should I go tonight for dinner and cuisines preferred?", user_id=USER_ID))
# OUTPUT: 🎵 Audio saved as food_recommendation_1750162610.mp3
# For dinner tonight, considering your love for healthy spic optionsy, you could try a nice Thai, Indian, or Mexican restaurant.
# You might find dishes with quinoa, chickpeas, tofu, and fresh herbs delightful. Enjoy your dinner!
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mem0ai",
"version": "2.1.29",
"version": "2.1.31",
"description": "The Memory Layer For Your AI Apps",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
@@ -107,6 +107,8 @@
"@types/sqlite3": "3.1.11",
"groq-sdk": "0.3.0",
"@langchain/core": "^0.3.44",
"cloudflare": "^4.2.0",
"@cloudflare/workers-types": "^4.20250504.0",
"neo4j-driver": "^5.28.1",
"ollama": "^0.5.14",
"pg": "8.11.3",
+32
View File
@@ -10,6 +10,9 @@ importers:
"@anthropic-ai/sdk":
specifier: ^0.40.1
version: 0.40.1(encoding@0.1.13)
"@cloudflare/workers-types":
specifier: ^4.20250504.0
version: 4.20250606.0
"@google/genai":
specifier: ^1.2.0
version: 1.2.0(@modelcontextprotocol/sdk@1.12.1)(encoding@0.1.13)
@@ -37,6 +40,9 @@ importers:
axios:
specifier: 1.7.7
version: 1.7.7
cloudflare:
specifier: ^4.2.0
version: 4.3.0(encoding@0.1.13)
groq-sdk:
specifier: 0.3.0
version: 0.3.0(encoding@0.1.13)
@@ -385,6 +391,12 @@ packages:
integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==,
}
"@cloudflare/workers-types@4.20250606.0":
resolution:
{
integrity: sha512-9T/Y/Mxe57UVzqgfjJKheiMplnStj/3CmCHlgoZNLU8JW2waRbXvpY3EEeliiYAJfeHZTjeAaKO2pCabxAoyCw==,
}
"@cspotcode/source-map-support@0.8.1":
resolution:
{
@@ -1811,6 +1823,12 @@ packages:
}
engines: { node: ">=12" }
cloudflare@4.3.0:
resolution:
{
integrity: sha512-C+4Jhsl/OY4V5sykRB1yJxComDld5BkKW1xd3s0MDJ1yYamT2sFAoC2FEUQg5zipyxMaaGU4N7hZ6il+gfJxZg==,
}
cluster-key-slot@1.1.2:
resolution:
{
@@ -5465,6 +5483,8 @@ snapshots:
"@cfworker/json-schema@4.1.1": {}
"@cloudflare/workers-types@4.20250606.0": {}
"@cspotcode/source-map-support@0.8.1":
dependencies:
"@jridgewell/trace-mapping": 0.3.9
@@ -6397,6 +6417,18 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
cloudflare@4.3.0(encoding@0.1.13):
dependencies:
"@types/node": 18.19.76
"@types/node-fetch": 2.6.12
abort-controller: 3.0.0
agentkeepalive: 4.6.0
form-data-encoder: 1.7.2
formdata-node: 4.4.1
node-fetch: 2.7.0(encoding@0.1.13)
transitivePeerDependencies:
- encoding
cluster-key-slot@1.1.2: {}
co@4.6.0: {}
+1
View File
@@ -24,6 +24,7 @@ export interface MemoryOptions {
timestamp?: number;
output_format?: string | OutputFormat;
async_mode?: boolean;
filter_memories?: boolean;
}
export interface ProjectOptions {
+2
View File
@@ -21,6 +21,7 @@
"@types/redis": "^4.0.10",
"@types/sqlite3": "^3.1.11",
"@types/uuid": "^9.0.8",
"cloudflare": "^4.2.0",
"dotenv": "^16.4.4",
"groq-sdk": "^0.3.0",
"openai": "^4.28.0",
@@ -31,6 +32,7 @@
"zod": "^3.22.4"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250504.0",
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"rimraf": "^5.0.5",
+1
View File
@@ -22,4 +22,5 @@ export * from "./vector_stores/qdrant";
export * from "./vector_stores/redis";
export * from "./vector_stores/supabase";
export * from "./vector_stores/langchain";
export * from "./vector_stores/vectorize";
export * from "./utils/factory";
+2
View File
@@ -117,6 +117,7 @@ export const MemoryConfigSchema = z.object({
modelProperties: z.record(z.string(), z.any()).optional(),
apiKey: z.string().optional(),
model: z.union([z.string(), z.any()]).optional(),
baseURL: z.string().optional(),
}),
}),
vectorStore: z.object({
@@ -135,6 +136,7 @@ export const MemoryConfigSchema = z.object({
apiKey: z.string().optional(),
model: z.union([z.string(), z.any()]).optional(),
modelProperties: z.record(z.string(), z.any()).optional(),
baseURL: z.string().optional(),
}),
}),
historyDbPath: z.string().optional(),
+3
View File
@@ -16,6 +16,7 @@ import { Embedder } from "../embeddings/base";
import { LLM } from "../llms/base";
import { VectorStore } from "../vector_stores/base";
import { Qdrant } from "../vector_stores/qdrant";
import { VectorizeDB } from "../vector_stores/vectorize";
import { RedisDB } from "../vector_stores/redis";
import { OllamaLLM } from "../llms/ollama";
import { SupabaseDB } from "../vector_stores/supabase";
@@ -90,6 +91,8 @@ export class VectorStoreFactory {
return new SupabaseDB(config as any);
case "langchain":
return new LangchainVectorStore(config as any);
case "vectorize":
return new VectorizeDB(config as any);
default:
throw new Error(`Unsupported vector store provider: ${provider}`);
}
@@ -0,0 +1,436 @@
import Cloudflare from "cloudflare";
import type { Vectorize, VectorizeVector } from "@cloudflare/workers-types";
import { VectorStore } from "./base";
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
interface VectorizeConfig extends VectorStoreConfig {
apiKey?: string;
indexName: string;
accountId: string;
}
interface CloudflareVector {
id: string;
values: number[];
metadata?: Record<string, any>;
}
export class VectorizeDB implements VectorStore {
private client: Cloudflare | null = null;
private dimensions: number;
private indexName: string;
private accountId: string;
constructor(config: VectorizeConfig) {
this.client = new Cloudflare({ apiToken: config.apiKey });
this.dimensions = config.dimension || 1536;
this.indexName = config.indexName;
this.accountId = config.accountId;
this.initialize().catch(console.error);
}
async insert(
vectors: number[][],
ids: string[],
payloads: Record<string, any>[],
): Promise<void> {
try {
const vectorObjects: CloudflareVector[] = vectors.map(
(vector, index) => ({
id: ids[index],
values: vector,
metadata: payloads[index] || {},
}),
);
const ndjsonPayload = vectorObjects
.map((v) => JSON.stringify(v))
.join("\n");
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/vectorize/v2/indexes/${this.indexName}/insert`,
{
method: "POST",
headers: {
"Content-Type": "application/x-ndjson",
Authorization: `Bearer ${this.client?.apiToken}`,
},
body: ndjsonPayload,
},
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Failed to insert vectors: ${response.status} ${errorText}`,
);
}
} catch (error) {
console.error("Error inserting vectors:", error);
throw new Error(
`Failed to insert vectors: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async search(
query: number[],
limit: number = 5,
filters?: SearchFilters,
): Promise<VectorStoreResult[]> {
try {
const result = await this.client?.vectorize.indexes.query(
this.indexName,
{
account_id: this.accountId,
vector: query,
filter: filters,
returnMetadata: "all",
topK: limit,
},
);
return (
(result?.matches?.map((match) => ({
id: match.id,
payload: match.metadata,
score: match.score,
})) as VectorStoreResult[]) || []
); // Return empty array if result or matches is null/undefined
} catch (error) {
console.error("Error searching vectors:", error);
throw new Error(
`Failed to search vectors: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async get(vectorId: string): Promise<VectorStoreResult | null> {
try {
const result = (await this.client?.vectorize.indexes.getByIds(
this.indexName,
{
account_id: this.accountId,
ids: [vectorId],
},
)) as any;
if (!result?.length) return null;
return {
id: vectorId,
payload: result[0].metadata,
};
} catch (error) {
console.error("Error getting vector:", error);
throw new Error(
`Failed to get vector: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async update(
vectorId: string,
vector: number[],
payload: Record<string, any>,
): Promise<void> {
try {
const data: VectorizeVector = {
id: vectorId,
values: vector,
metadata: payload,
};
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/vectorize/v2/indexes/${this.indexName}/upsert`,
{
method: "POST",
headers: {
"Content-Type": "application/x-ndjson",
Authorization: `Bearer ${this.client?.apiToken}`,
},
body: JSON.stringify(data) + "\n", // ndjson format
},
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`Failed to update vector: ${response.status} ${errorText}`,
);
}
} catch (error) {
console.error("Error updating vector:", error);
throw new Error(
`Failed to update vector: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async delete(vectorId: string): Promise<void> {
try {
await this.client?.vectorize.indexes.deleteByIds(this.indexName, {
account_id: this.accountId,
ids: [vectorId],
});
} catch (error) {
console.error("Error deleting vector:", error);
throw new Error(
`Failed to delete vector: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async deleteCol(): Promise<void> {
try {
await this.client?.vectorize.indexes.delete(this.indexName, {
account_id: this.accountId,
});
} catch (error) {
console.error("Error deleting collection:", error);
throw new Error(
`Failed to delete collection: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async list(
filters?: SearchFilters,
limit: number = 20,
): Promise<[VectorStoreResult[], number]> {
try {
const result = await this.client?.vectorize.indexes.query(
this.indexName,
{
account_id: this.accountId,
vector: Array(this.dimensions).fill(0), // Dummy vector for listing
filter: filters,
topK: limit,
returnMetadata: "all",
},
);
const matches =
(result?.matches?.map((match) => ({
id: match.id,
payload: match.metadata,
score: match.score,
})) as VectorStoreResult[]) || [];
return [matches, matches.length];
} catch (error) {
console.error("Error listing vectors:", error);
throw new Error(
`Failed to list vectors: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
private generateUUID(): string {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
/[xy]/g,
function (c) {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
},
);
}
async getUserId(): Promise<string> {
try {
let found = false;
for await (const index of this.client!.vectorize.indexes.list({
account_id: this.accountId,
})) {
if (index.name === "memory_migrations") {
found = true;
}
}
if (!found) {
await this.client?.vectorize.indexes.create({
account_id: this.accountId,
name: "memory_migrations",
config: {
dimensions: 1,
metric: "cosine",
},
});
}
// Now try to get the userId
const result: any = await this.client?.vectorize.indexes.query(
"memory_migrations",
{
account_id: this.accountId,
vector: [0],
topK: 1,
returnMetadata: "all",
},
);
if (result.matches.length > 0) {
return result.matches[0].metadata.userId as string;
}
// Generate a random userId if none exists
const randomUserId =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
const data: VectorizeVector = {
id: this.generateUUID(),
values: [0],
metadata: { userId: randomUserId },
};
await fetch(
`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/vectorize/v2/indexes/memory_migrations/upsert`,
{
method: "POST",
headers: {
"Content-Type": "application/x-ndjson",
Authorization: `Bearer ${this.client?.apiToken}`,
},
body: JSON.stringify(data) + "\n", // ndjson format
},
);
return randomUserId;
} catch (error) {
console.error("Error getting user ID:", error);
throw new Error(
`Failed to get user ID: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async setUserId(userId: string): Promise<void> {
try {
// Get existing point ID
const result: any = await this.client?.vectorize.indexes.query(
"memory_migrations",
{
account_id: this.accountId,
vector: [0],
topK: 1,
returnMetadata: "all",
},
);
const pointId =
result.matches.length > 0 ? result.matches[0].id : this.generateUUID();
const data: VectorizeVector = {
id: pointId,
values: [0],
metadata: { userId },
};
await fetch(
`https://api.cloudflare.com/client/v4/accounts/${this.accountId}/vectorize/v2/indexes/memory_migrations/upsert`,
{
method: "POST",
headers: {
"Content-Type": "application/x-ndjson",
Authorization: `Bearer ${this.client?.apiToken}`,
},
body: JSON.stringify(data) + "\n", // ndjson format
},
);
} catch (error) {
console.error("Error setting user ID:", error);
throw new Error(
`Failed to set user ID: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
async initialize(): Promise<void> {
try {
// Check if the index already exists
let indexFound = false;
for await (const idx of this.client!.vectorize.indexes.list({
account_id: this.accountId,
})) {
if (idx.name === this.indexName) {
indexFound = true;
break;
}
}
// If the index doesn't exist, create it
if (!indexFound) {
try {
await this.client?.vectorize.indexes.create({
account_id: this.accountId,
name: this.indexName,
config: {
dimensions: this.dimensions,
metric: "cosine",
},
});
const properties = ["userId", "agentId", "runId"];
for (const propertyName of properties) {
await this.client?.vectorize.indexes.metadataIndex.create(
this.indexName,
{
account_id: this.accountId,
indexType: "string",
propertyName,
},
);
}
} catch (err: any) {
throw new Error(err);
}
}
// check for metadata index
const metadataIndexes =
await this.client?.vectorize.indexes.metadataIndex.list(
this.indexName,
{
account_id: this.accountId,
},
);
const existingMetadataIndexes = new Set<string>();
for (const metadataIndex of metadataIndexes?.metadataIndexes || []) {
existingMetadataIndexes.add(metadataIndex.propertyName!);
}
const properties = ["userId", "agentId", "runId"];
for (const propertyName of properties) {
if (!existingMetadataIndexes.has(propertyName)) {
await this.client?.vectorize.indexes.metadataIndex.create(
this.indexName,
{
account_id: this.accountId,
indexType: "string",
propertyName,
},
);
}
}
// Create memory_migrations collection if it doesn't exist
let found = false;
for await (const index of this.client!.vectorize.indexes.list({
account_id: this.accountId,
})) {
if (index.name === "memory_migrations") {
found = true;
break;
}
}
if (!found) {
await this.client?.vectorize.indexes.create({
account_id: this.accountId,
name: "memory_migrations",
config: {
dimensions: 1,
metric: "cosine",
},
});
}
} catch (err: any) {
throw new Error(err);
}
}
}
+521 -104
View File
File diff suppressed because it is too large Load Diff
+4
View File
@@ -43,6 +43,7 @@ class BaseLlmConfig(ABC):
sarvam_base_url: Optional[str] = "https://api.sarvam.ai/v1",
# LM Studio specific
lmstudio_base_url: Optional[str] = "http://localhost:1234/v1",
lmstudio_response_format: dict = None,
# AWS Bedrock specific
aws_access_key_id: Optional[str] = None,
aws_secret_access_key: Optional[str] = None,
@@ -95,6 +96,8 @@ class BaseLlmConfig(ABC):
:type sarvam_base_url: Optional[str], optional
:param lmstudio_base_url: LM Studio base URL to be use, defaults to "http://localhost:1234/v1"
:type lmstudio_base_url: Optional[str], optional
:param lmstudio_response_format: LM Studio response format to be use, defaults to None
:type lmstudio_response_format: Optional[Dict], optional
"""
self.model = model
@@ -134,6 +137,7 @@ class BaseLlmConfig(ABC):
# LM Studio specific
self.lmstudio_base_url = lmstudio_base_url
self.lmstudio_response_format = lmstudio_response_format
# AWS Bedrock specific
self.aws_access_key_id = aws_access_key_id
+42
View File
@@ -0,0 +1,42 @@
from typing import Any, Dict, Optional, Callable, List
from pydantic import BaseModel, Field, root_validator
class MongoVectorConfig(BaseModel):
"""Configuration for MongoDB vector database."""
db_name: str = Field("mem0_db", description="Name of the MongoDB database")
collection_name: str = Field("mem0", description="Name of the MongoDB collection")
embedding_model_dims: Optional[int] = Field(1536, description="Dimensions of the embedding vectors")
user: Optional[str] = Field(None, description="MongoDB user for authentication")
password: Optional[str] = Field(None, description="Password for the MongoDB user")
host: Optional[str] = Field("localhost", description="MongoDB host. Default is 'localhost'")
port: Optional[int] = Field(27017, description="MongoDB port. Default is 27017")
@root_validator(pre=True)
def check_auth_and_connection(cls, values):
user = values.get("user")
password = values.get("password")
if (user is None) != (password is None):
raise ValueError("Both 'user' and 'password' must be provided together or omitted together.")
host = values.get("host")
port = values.get("port")
if host is None:
raise ValueError("The 'host' must be provided.")
if port is None:
raise ValueError("The 'port' must be provided.")
return values
@root_validator(pre=True)
def validate_extra_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
allowed_fields = set(cls.__fields__)
input_fields = set(values.keys())
extra_fields = input_fields - allowed_fields
if extra_fields:
raise ValueError(
f"Extra fields not allowed: {', '.join(extra_fields)}. "
f"Please provide only the following fields: {', '.join(allowed_fields)}."
)
return values
+59 -39
View File
@@ -2,9 +2,9 @@ import os
from typing import Dict, List, Optional
try:
import google.generativeai as genai
from google.generativeai import GenerativeModel, protos
from google.generativeai.types import content_types
from google import genai
from google.genai import types
except ImportError:
raise ImportError(
"The 'google-generativeai' library is required. Please install it using 'pip install google-generativeai'."
@@ -22,66 +22,71 @@ class GeminiLLM(LLMBase):
self.config.model = "gemini-1.5-flash-latest"
api_key = self.config.api_key or os.getenv("GEMINI_API_KEY")
genai.configure(api_key=api_key)
self.client = GenerativeModel(model_name=self.config.model)
self.client_gemini = genai.Client(
api_key=api_key,
)
def _parse_response(self, response, tools):
"""
Process the response based on whether tools are used or not.
Args:
response: The raw response from API.
response: The raw response from the API.
tools: The list of tools provided in the request.
Returns:
str or dict: The processed response.
"""
candidate = response.candidates[0]
content = candidate.content.parts[0].text if candidate.content.parts else None
if tools:
processed_response = {
"content": (content if (content := response.candidates[0].content.parts[0].text) else None),
"content": content,
"tool_calls": [],
}
for part in response.candidates[0].content.parts:
if fn := part.function_call:
if isinstance(fn, protos.FunctionCall):
fn_call = type(fn).to_dict(fn)
processed_response["tool_calls"].append({"name": fn_call["name"], "arguments": fn_call["args"]})
continue
processed_response["tool_calls"].append({"name": fn.name, "arguments": fn.args})
for part in candidate.content.parts:
fn = getattr(part, "function_call", None)
if fn:
processed_response["tool_calls"].append({
"name": fn.name,
"arguments": fn.args,
})
return processed_response
else:
return response.candidates[0].content.parts[0].text
def _reformat_messages(self, messages: List[Dict[str, str]]):
return content
def _reformat_messages(self, messages: List[Dict[str, str]]) -> List[types.Content]:
"""
Reformat messages for Gemini.
Reformat messages for Gemini using google.genai.types.
Args:
messages: The list of messages provided in the request.
Returns:
list: The list of messages in the required format.
list: A list of types.Content objects with proper role and parts.
"""
new_messages = []
for message in messages:
if message["role"] == "system":
content = "THIS IS A SYSTEM PROMPT. YOU MUST OBEY THIS: " + message["content"]
else:
content = message["content"]
new_messages.append(
{
"parts": content,
"role": "model" if message["role"] == "model" else "user",
}
types.Content(
role="model" if message["role"] == "model" else "user",
parts=[types.Part(text=content)]
)
)
return new_messages
def _reformat_tools(self, tools: Optional[List[Dict]]):
"""
Reformat tools for Gemini.
@@ -126,6 +131,7 @@ class GeminiLLM(LLMBase):
tools: Optional[List[Dict]] = None,
tool_choice: str = "auto",
):
"""
Generate a response based on the given messages using Gemini.
@@ -149,23 +155,37 @@ class GeminiLLM(LLMBase):
params["response_mime_type"] = "application/json"
if "schema" in response_format:
params["response_schema"] = response_format["schema"]
tool_config = None
if tool_choice:
tool_config = content_types.to_tool_config(
{
"function_calling_config": {
"mode": tool_choice,
"allowed_function_names": (
[tool["function"]["name"] for tool in tools] if tool_choice == "any" else None
),
}
}
tool_config = types.ToolConfig(
function_calling_config=types.FunctionCallingConfig(
mode=tool_choice.upper(), # Assuming 'any' should become 'ANY', etc.
allowed_function_names=[
tool["function"]["name"] for tool in tools
] if tool_choice == "any" else None
)
)
response = self.client.generate_content(
contents=self._reformat_messages(messages),
tools=self._reformat_tools(tools),
generation_config=genai.GenerationConfig(**params),
tool_config=tool_config,
)
print(f"Tool config: {tool_config}")
print(f"Params: {params}" )
print(f"Messages: {messages}")
print(f"Tools: {tools}")
print(f"Reformatted messages: {self._reformat_messages(messages)}")
print(f"Reformatted tools: {self._reformat_tools(tools)}")
response = self.client_gemini.models.generate_content(
model=self.config.model,
contents=self._reformat_messages(messages),
config=types.GenerateContentConfig(
temperature= self.config.temperature,
max_output_tokens= self.config.max_tokens,
top_p= self.config.top_p,
tools=self._reformat_tools(tools),
tool_config=tool_config,
),
)
print(f"Response test: {response}")
return self._parse_response(response, tools)
+2
View File
@@ -46,6 +46,8 @@ class LMStudioLLM(LLMBase):
}
if response_format:
params["response_format"] = response_format
if self.config.lmstudio_response_format is not None:
params["response_format"] = self.config.lmstudio_response_format
response = self.client.chat.completions.create(**params)
return response.choices[0].message.content
+8 -21
View File
@@ -18,22 +18,15 @@ class SarvamLLM(LLMBase):
if not self.api_key:
raise ValueError(
"Sarvam API key is required. Set SARVAM_API_KEY environment variable "
"or provide api_key in config."
"Sarvam API key is required. Set SARVAM_API_KEY environment variable " "or provide api_key in config."
)
# Set base URL - use config value or environment or default
self.base_url = (
getattr(self.config, 'sarvam_base_url', None) or
os.getenv("SARVAM_API_BASE") or
"https://api.sarvam.ai/v1"
getattr(self.config, "sarvam_base_url", None) or os.getenv("SARVAM_API_BASE") or "https://api.sarvam.ai/v1"
)
def generate_response(
self,
messages: List[Dict[str, str]],
response_format=None
) -> str:
def generate_response(self, messages: List[Dict[str, str]], response_format=None) -> str:
"""
Generate a response based on the given messages using Sarvam-M.
@@ -47,10 +40,7 @@ class SarvamLLM(LLMBase):
"""
url = f"{self.base_url}/chat/completions"
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
headers = {"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}
# Prepare the request payload
params = {
@@ -74,10 +64,7 @@ class SarvamLLM(LLMBase):
params["model"] = self.config.model.get("name", "sarvam-m")
# Add Sarvam-specific parameters
sarvam_specific_params = [
'reasoning_effort', 'frequency_penalty', 'presence_penalty',
'seed', 'stop', 'n'
]
sarvam_specific_params = ["reasoning_effort", "frequency_penalty", "presence_penalty", "seed", "stop", "n"]
for param in sarvam_specific_params:
if param in self.config.model:
@@ -89,12 +76,12 @@ class SarvamLLM(LLMBase):
result = response.json()
if 'choices' in result and len(result['choices']) > 0:
return result['choices'][0]['message']['content']
if "choices" in result and len(result["choices"]) > 0:
return result["choices"][0]["message"]["content"]
else:
raise ValueError("No response choices found in Sarvam API response")
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Sarvam API request failed: {e}")
except KeyError as e:
raise ValueError(f"Unexpected response format from Sarvam API: {e}")
raise ValueError(f"Unexpected response format from Sarvam API: {e}")
+226 -138
View File
@@ -80,8 +80,8 @@ class MemoryGraph:
# TODO: Batch queries with APOC plugin
# TODO: Add more filter support
deleted_entities = self._delete_entities(to_be_deleted, filters["user_id"])
added_entities = self._add_entities(to_be_added, filters["user_id"], entity_type_map)
deleted_entities = self._delete_entities(to_be_deleted, filters)
added_entities = self._add_entities(to_be_added, filters, entity_type_map)
return {"deleted_entities": deleted_entities, "added_entities": added_entities}
@@ -122,32 +122,35 @@ class MemoryGraph:
return search_results
def delete_all(self, filters):
cypher = f"""
MATCH (n {self.node_label} {{user_id: $user_id}})
DETACH DELETE n
"""
params = {"user_id": filters["user_id"]}
if filters.get("agent_id"):
cypher = f"""
MATCH (n {self.node_label} {{user_id: $user_id, agent_id: $agent_id}})
DETACH DELETE n
"""
params = {"user_id": filters["user_id"], "agent_id": filters["agent_id"]}
else:
cypher = f"""
MATCH (n {self.node_label} {{user_id: $user_id}})
DETACH DELETE n
"""
params = {"user_id": filters["user_id"]}
self.graph.query(cypher, params=params)
def get_all(self, filters, limit=100):
"""
Retrieves all nodes and relationships from the graph database based on optional filtering criteria.
Args:
filters (dict): A dictionary containing filters to be applied during the retrieval.
limit (int): The maximum number of nodes and relationships to retrieve. Defaults to 100.
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
def get_all(self, filters, limit=100):
agent_filter = ""
params = {"user_id": filters["user_id"], "limit": limit}
if filters.get("agent_id"):
agent_filter = "AND n.agent_id = $agent_id AND m.agent_id = $agent_id"
params["agent_id"] = filters["agent_id"]
query = f"""
MATCH (n {self.node_label} {{user_id: $user_id}})-[r]->(m {self.node_label} {{user_id: $user_id}})
WHERE 1=1 {agent_filter}
RETURN n.name AS source, type(r) AS relationship, m.name AS target
LIMIT $limit
"""
results = self.graph.query(query, params={"user_id": filters["user_id"], "limit": limit})
results = self.graph.query(query, params=params)
final_results = []
for result in results:
@@ -163,6 +166,7 @@ class MemoryGraph:
return final_results
def _retrieve_nodes_from_data(self, data, filters):
"""Extracts all the entities mentioned in the query."""
_tools = [EXTRACT_ENTITIES_TOOL]
@@ -197,23 +201,27 @@ class MemoryGraph:
return entity_type_map
def _establish_nodes_relations_from_data(self, data, filters, entity_type_map):
"""Eshtablish relations among the extracted nodes."""
"""Establish relations among the extracted nodes."""
# Compose user identification string for prompt
user_identity = f"user_id: {filters['user_id']}"
if filters.get("agent_id"):
user_identity += f", agent_id: {filters['agent_id']}"
if self.config.graph_store.custom_prompt:
system_content = EXTRACT_RELATIONS_PROMPT.replace("USER_ID", user_identity)
# Add the custom prompt line if configured
system_content = system_content.replace(
"CUSTOM_PROMPT", f"4. {self.config.graph_store.custom_prompt}"
)
messages = [
{
"role": "system",
"content": EXTRACT_RELATIONS_PROMPT.replace("USER_ID", filters["user_id"]).replace(
"CUSTOM_PROMPT", f"4. {self.config.graph_store.custom_prompt}"
),
},
{"role": "system", "content": system_content},
{"role": "user", "content": data},
]
else:
system_content = EXTRACT_RELATIONS_PROMPT.replace("USER_ID", user_identity)
messages = [
{
"role": "system",
"content": EXTRACT_RELATIONS_PROMPT.replace("USER_ID", filters["user_id"]),
},
{"role": "system", "content": system_content},
{"role": "user", "content": f"List of entities: {list(entity_type_map.keys())}. \n\nText: {data}"},
]
@@ -227,8 +235,8 @@ class MemoryGraph:
)
entities = []
if extracted_entities["tool_calls"]:
entities = extracted_entities["tool_calls"][0]["arguments"]["entities"]
if extracted_entities.get("tool_calls"):
entities = extracted_entities["tool_calls"][0].get("arguments", {}).get("entities", [])
entities = self._remove_spaces_from_entities(entities)
logger.debug(f"Extracted entities: {entities}")
@@ -237,32 +245,43 @@ class MemoryGraph:
def _search_graph_db(self, node_list, filters, limit=100):
"""Search similar nodes among and their respective incoming and outgoing relations."""
result_relations = []
agent_filter = ""
if filters.get("agent_id"):
agent_filter = "AND n.agent_id = $agent_id AND m.agent_id = $agent_id"
for node in node_list:
n_embedding = self.embedding_model.embed(node)
cypher_query = f"""
MATCH (n {self.node_label})
WHERE n.embedding IS NOT NULL AND n.user_id = $user_id
{agent_filter}
WITH n, round(2 * vector.similarity.cosine(n.embedding, $n_embedding) - 1, 4) AS similarity // denormalize for backward compatibility
WHERE similarity >= $threshold
CALL (n) {{
MATCH (n)-[r]->(m)
CALL {{
MATCH (n)-[r]->(m)
WHERE m.user_id = $user_id {agent_filter.replace("n.", "m.")}
RETURN n.name AS source, elementId(n) AS source_id, type(r) AS relationship, elementId(r) AS relation_id, m.name AS destination, elementId(m) AS destination_id
UNION
MATCH (m)-[r]->(n)
MATCH (m)-[r]->(n)
WHERE m.user_id = $user_id {agent_filter.replace("n.", "m.")}
RETURN m.name AS source, elementId(m) AS source_id, type(r) AS relationship, elementId(r) AS relation_id, n.name AS destination, elementId(n) AS destination_id
}}
WITH distinct source, source_id, relationship, relation_id, destination, destination_id, similarity //deduplicate
WITH distinct source, source_id, relationship, relation_id, destination, destination_id, similarity
RETURN source, source_id, relationship, relation_id, destination, destination_id, similarity
ORDER BY similarity DESC
LIMIT $limit
"""
params = {
"n_embedding": n_embedding,
"threshold": self.threshold,
"user_id": filters["user_id"],
"limit": limit,
}
if filters.get("agent_id"):
params["agent_id"] = filters["agent_id"]
ans = self.graph.query(cypher_query, params=params)
result_relations.extend(ans)
@@ -271,7 +290,13 @@ class MemoryGraph:
def _get_delete_entities_from_search_output(self, search_output, data, filters):
"""Get the entities to be deleted from the search output."""
search_output_string = format_entities(search_output)
system_prompt, user_prompt = get_delete_messages(search_output_string, data, filters["user_id"])
# Compose user identification string for prompt
user_identity = f"user_id: {filters['user_id']}"
if filters.get("agent_id"):
user_identity += f", agent_id: {filters['agent_id']}"
system_prompt, user_prompt = get_delete_messages(search_output_string, data, user_identity)
_tools = [DELETE_MEMORY_TOOL_GRAPH]
if self.llm_provider in ["azure_openai_structured", "openai_structured"]:
@@ -288,44 +313,59 @@ class MemoryGraph:
)
to_be_deleted = []
for item in memory_updates["tool_calls"]:
if item["name"] == "delete_graph_memory":
to_be_deleted.append(item["arguments"])
# in case if it is not in the correct format
for item in memory_updates.get("tool_calls", []):
if item.get("name") == "delete_graph_memory":
to_be_deleted.append(item.get("arguments"))
# Clean entities formatting
to_be_deleted = self._remove_spaces_from_entities(to_be_deleted)
logger.debug(f"Deleted relationships: {to_be_deleted}")
return to_be_deleted
def _delete_entities(self, to_be_deleted, user_id):
def _delete_entities(self, to_be_deleted, filters):
"""Delete the entities from the graph."""
user_id = filters["user_id"]
agent_id = filters.get("agent_id", None)
results = []
for item in to_be_deleted:
source = item["source"]
destination = item["destination"]
relationship = item["relationship"]
# Build the agent filter for the query
agent_filter = ""
params = {
"source_name": source,
"dest_name": destination,
"user_id": user_id,
}
if agent_id:
agent_filter = "AND n.agent_id = $agent_id AND m.agent_id = $agent_id"
params["agent_id"] = agent_id
# Delete the specific relationship between nodes
cypher = f"""
MATCH (n {self.node_label} {{name: $source_name, user_id: $user_id}})
-[r:{relationship}]->
(m {self.node_label} {{name: $dest_name, user_id: $user_id}})
WHERE 1=1 {agent_filter}
DELETE r
RETURN
n.name AS source,
m.name AS target,
type(r) AS relationship
"""
params = {
"source_name": source,
"dest_name": destination,
"user_id": user_id,
}
result = self.graph.query(cypher, params=params)
results.append(result)
return results
def _add_entities(self, to_be_added, user_id, entity_type_map):
def _add_entities(self, to_be_added, filters, entity_type_map):
"""Add the new entities to the graph. Merge the nodes if they already exist."""
user_id = filters["user_id"]
agent_id = filters.get("agent_id", None)
results = []
for item in to_be_added:
# entities
@@ -346,65 +386,80 @@ class MemoryGraph:
dest_embedding = self.embedding_model.embed(destination)
# search for the nodes with the closest embeddings
source_node_search_result = self._search_source_node(source_embedding, user_id, threshold=0.9)
destination_node_search_result = self._search_destination_node(dest_embedding, user_id, threshold=0.9)
source_node_search_result = self._search_source_node(source_embedding, filters, threshold=0.9)
destination_node_search_result = self._search_destination_node(dest_embedding, filters, threshold=0.9)
# TODO: Create a cypher query and common params for all the cases
if not destination_node_search_result and source_node_search_result:
cypher = f"""
MATCH (source)
WHERE elementId(source) = $source_id
SET source.mentions = coalesce(source.mentions, 0) + 1
WITH source
MERGE (destination {destination_label} {{name: $destination_name, user_id: $user_id}})
ON CREATE SET
destination.created = timestamp(),
destination.mentions = 1
{destination_extra_set}
ON MATCH SET
destination.mentions = coalesce(destination.mentions, 0) + 1
WITH source, destination
CALL db.create.setNodeVectorProperty(destination, 'embedding', $destination_embedding)
WITH source, destination
MERGE (source)-[r:{relationship}]->(destination)
ON CREATE SET
r.created = timestamp(),
r.mentions = 1
ON MATCH SET
r.mentions = coalesce(r.mentions, 0) + 1
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
"""
# Build destination MERGE properties
merge_props = ["name: $destination_name", "user_id: $user_id"]
if agent_id:
merge_props.append("agent_id: $agent_id")
merge_props_str = ", ".join(merge_props)
cypher = f"""
MATCH (source)
WHERE elementId(source) = $source_id
SET source.mentions = coalesce(source.mentions, 0) + 1
WITH source
MERGE (destination {destination_label} {{{merge_props_str}}})
ON CREATE SET
destination.created = timestamp(),
destination.mentions = 1
{destination_extra_set}
ON MATCH SET
destination.mentions = coalesce(destination.mentions, 0) + 1
WITH source, destination
CALL db.create.setNodeVectorProperty(destination, 'embedding', $destination_embedding)
WITH source, destination
MERGE (source)-[r:{relationship}]->(destination)
ON CREATE SET
r.created = timestamp(),
r.mentions = 1
ON MATCH SET
r.mentions = coalesce(r.mentions, 0) + 1
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
"""
params = {
"source_id": source_node_search_result[0]["elementId(source_candidate)"],
"destination_name": destination,
"destination_embedding": dest_embedding,
"user_id": user_id,
}
if agent_id:
params["agent_id"] = agent_id
elif destination_node_search_result and not source_node_search_result:
# Build source MERGE properties
merge_props = ["name: $source_name", "user_id: $user_id"]
if agent_id:
merge_props.append("agent_id: $agent_id")
merge_props_str = ", ".join(merge_props)
cypher = f"""
MATCH (destination)
WHERE elementId(destination) = $destination_id
SET destination.mentions = coalesce(destination.mentions, 0) + 1
WITH destination
MERGE (source {source_label} {{name: $source_name, user_id: $user_id}})
ON CREATE SET
source.created = timestamp(),
source.mentions = 1
{source_extra_set}
ON MATCH SET
source.mentions = coalesce(source.mentions, 0) + 1
WITH source, destination
CALL db.create.setNodeVectorProperty(source, 'embedding', $source_embedding)
WITH source, destination
MERGE (source)-[r:{relationship}]->(destination)
ON CREATE SET
r.created = timestamp(),
r.mentions = 1
ON MATCH SET
r.mentions = coalesce(r.mentions, 0) + 1
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
"""
MATCH (destination)
WHERE elementId(destination) = $destination_id
SET destination.mentions = coalesce(destination.mentions, 0) + 1
WITH destination
MERGE (source {source_label} {{{merge_props_str}}})
ON CREATE SET
source.created = timestamp(),
source.mentions = 1
{source_extra_set}
ON MATCH SET
source.mentions = coalesce(source.mentions, 0) + 1
WITH source, destination
CALL db.create.setNodeVectorProperty(source, 'embedding', $source_embedding)
WITH source, destination
MERGE (source)-[r:{relationship}]->(destination)
ON CREATE SET
r.created = timestamp(),
r.mentions = 1
ON MATCH SET
r.mentions = coalesce(r.mentions, 0) + 1
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
"""
params = {
"destination_id": destination_node_search_result[0]["elementId(destination_candidate)"],
@@ -412,53 +467,68 @@ class MemoryGraph:
"source_embedding": source_embedding,
"user_id": user_id,
}
if agent_id:
params["agent_id"] = agent_id
elif source_node_search_result and destination_node_search_result:
cypher = f"""
MATCH (source)
WHERE elementId(source) = $source_id
SET source.mentions = coalesce(source.mentions, 0) + 1
WITH source
MATCH (destination)
WHERE elementId(destination) = $destination_id
SET destination.mentions = coalesce(destination.mentions) + 1
MERGE (source)-[r:{relationship}]->(destination)
ON CREATE SET
r.created_at = timestamp(),
r.updated_at = timestamp(),
r.mentions = 1
ON MATCH SET r.mentions = coalesce(r.mentions, 0) + 1
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
"""
MATCH (source)
WHERE elementId(source) = $source_id
SET source.mentions = coalesce(source.mentions, 0) + 1
WITH source
MATCH (destination)
WHERE elementId(destination) = $destination_id
SET destination.mentions = coalesce(destination.mentions, 0) + 1
MERGE (source)-[r:{relationship}]->(destination)
ON CREATE SET
r.created_at = timestamp(),
r.updated_at = timestamp(),
r.mentions = 1
ON MATCH SET r.mentions = coalesce(r.mentions, 0) + 1
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
"""
params = {
"source_id": source_node_search_result[0]["elementId(source_candidate)"],
"destination_id": destination_node_search_result[0]["elementId(destination_candidate)"],
"user_id": user_id,
}
if agent_id:
params["agent_id"] = agent_id
else:
# Build dynamic MERGE props for both source and destination
source_props = ["name: $source_name", "user_id: $user_id"]
dest_props = ["name: $dest_name", "user_id: $user_id"]
if agent_id:
source_props.append("agent_id: $agent_id")
dest_props.append("agent_id: $agent_id")
source_props_str = ", ".join(source_props)
dest_props_str = ", ".join(dest_props)
cypher = f"""
MERGE (source {source_label} {{name: $source_name, user_id: $user_id}})
ON CREATE SET source.created = timestamp(),
source.mentions = 1
{source_extra_set}
ON MATCH SET source.mentions = coalesce(source.mentions, 0) + 1
WITH source
CALL db.create.setNodeVectorProperty(source, 'embedding', $source_embedding)
WITH source
MERGE (destination {destination_label} {{name: $dest_name, user_id: $user_id}})
ON CREATE SET destination.created = timestamp(),
destination.mentions = 1
{destination_extra_set}
ON MATCH SET destination.mentions = coalesce(destination.mentions, 0) + 1
WITH source, destination
CALL db.create.setNodeVectorProperty(destination, 'embedding', $source_embedding)
WITH source, destination
MERGE (source)-[rel:{relationship}]->(destination)
ON CREATE SET rel.created = timestamp(), rel.mentions = 1
ON MATCH SET rel.mentions = coalesce(rel.mentions, 0) + 1
RETURN source.name AS source, type(rel) AS relationship, destination.name AS target
"""
MERGE (source {source_label} {{{source_props_str}}})
ON CREATE SET source.created = timestamp(),
source.mentions = 1
{source_extra_set}
ON MATCH SET source.mentions = coalesce(source.mentions, 0) + 1
WITH source
CALL db.create.setNodeVectorProperty(source, 'embedding', $source_embedding)
WITH source
MERGE (destination {destination_label} {{{dest_props_str}}})
ON CREATE SET destination.created = timestamp(),
destination.mentions = 1
{destination_extra_set}
ON MATCH SET destination.mentions = coalesce(destination.mentions, 0) + 1
WITH source, destination
CALL db.create.setNodeVectorProperty(destination, 'embedding', $dest_embedding)
WITH source, destination
MERGE (source)-[rel:{relationship}]->(destination)
ON CREATE SET rel.created = timestamp(), rel.mentions = 1
ON MATCH SET rel.mentions = coalesce(rel.mentions, 0) + 1
RETURN source.name AS source, type(rel) AS relationship, destination.name AS target
"""
params = {
"source_name": source,
"dest_name": destination,
@@ -466,6 +536,8 @@ class MemoryGraph:
"dest_embedding": dest_embedding,
"user_id": user_id,
}
if agent_id:
params["agent_id"] = agent_id
result = self.graph.query(cypher, params=params)
results.append(result)
return results
@@ -477,11 +549,16 @@ class MemoryGraph:
item["destination"] = item["destination"].lower().replace(" ", "_")
return entity_list
def _search_source_node(self, source_embedding, user_id, threshold=0.9):
def _search_source_node(self, source_embedding, filters, threshold=0.9):
agent_filter = ""
if filters.get("agent_id"):
agent_filter = "AND source_candidate.agent_id = $agent_id"
cypher = f"""
MATCH (source_candidate {self.node_label})
WHERE source_candidate.embedding IS NOT NULL
AND source_candidate.user_id = $user_id
{agent_filter}
WITH source_candidate,
round(2 * vector.similarity.cosine(source_candidate.embedding, $source_embedding) - 1, 4) AS source_similarity // denormalize for backward compatibility
@@ -496,18 +573,26 @@ class MemoryGraph:
params = {
"source_embedding": source_embedding,
"user_id": user_id,
"user_id": filters["user_id"],
"threshold": threshold,
}
if filters.get("agent_id"):
params["agent_id"] = filters["agent_id"]
result = self.graph.query(cypher, params=params)
return result
def _search_destination_node(self, destination_embedding, user_id, threshold=0.9):
def _search_destination_node(self, destination_embedding, filters, threshold=0.9):
agent_filter = ""
if filters.get("agent_id"):
agent_filter = "AND destination_candidate.agent_id = $agent_id"
cypher = f"""
MATCH (destination_candidate {self.node_label})
WHERE destination_candidate.embedding IS NOT NULL
AND destination_candidate.user_id = $user_id
{agent_filter}
WITH destination_candidate,
round(2 * vector.similarity.cosine(destination_candidate.embedding, $destination_embedding) - 1, 4) AS destination_similarity // denormalize for backward compatibility
@@ -520,11 +605,14 @@ class MemoryGraph:
RETURN elementId(destination_candidate)
"""
params = {
"destination_embedding": destination_embedding,
"user_id": user_id,
"user_id": filters["user_id"],
"threshold": threshold,
}
if filters.get("agent_id"):
params["agent_id"] = filters["agent_id"]
result = self.graph.query(cypher, params=params)
return result
+122 -92
View File
@@ -28,6 +28,7 @@ from mem0.memory.utils import (
get_fact_retrieval_messages,
parse_messages,
parse_vision_messages,
process_telemetry_filters,
remove_code_blocks,
)
from mem0.utils.factory import EmbedderFactory, LlmFactory, VectorStoreFactory
@@ -45,13 +46,13 @@ def _build_filters_and_metadata(
"""
Constructs metadata for storage and filters for querying based on session and actor identifiers.
This helper ties every memory/query to exactly one session id (`user_id`, `agent_id`, or `run_id`) and optionally narrows queries to a specific `actor_id`. It returns two dicts:
This helper supports multiple session identifiers (`user_id`, `agent_id`, and/or `run_id`)
for flexible session scoping and optionally narrows queries to a specific `actor_id`. It returns two dicts:
1. `base_metadata_template`: Used as a template for metadata when storing new memories.
It includes the primary session identifier(s) and any `input_metadata`.
2. `effective_query_filters`: Used for querying existing memories. It includes the
primary session identifier(s), any `input_filters`, and a resolved actor
It includes all provided session identifier(s) and any `input_metadata`.
2. `effective_query_filters`: Used for querying existing memories. It includes all
provided session identifier(s), any `input_filters`, and a resolved actor
identifier for targeted filtering if specified by any actor-related inputs.
Actor filtering precedence: explicit `actor_id` arg → `filters["actor_id"]`
@@ -59,11 +60,9 @@ def _build_filters_and_metadata(
as the actor for storage is typically derived from message content at a later stage.
Args:
user_id (Optional[str]): User identifier, primarily for Classic Mode session scoping.
agent_id (Optional[str]): Agent identifier, for Classic Mode session scoping or
as auxiliary information in Group Mode.
run_id (Optional[str]): Run identifier, for Classic Mode session scoping or
as auxiliary information in Group Mode.
user_id (Optional[str]): User identifier, for session scoping.
agent_id (Optional[str]): Agent identifier, for session scoping.
run_id (Optional[str]): Run identifier, for session scoping.
actor_id (Optional[str]): Explicit actor identifier, used as a potential source for
actor-specific filtering. See actor resolution precedence in the main description.
input_metadata (Optional[Dict[str, Any]]): Base dictionary to be augmented with
@@ -74,28 +73,34 @@ def _build_filters_and_metadata(
Returns:
tuple[Dict[str, Any], Dict[str, Any]]: A tuple containing:
- base_metadata_template (Dict[str, Any]): Metadata template for storing memories,
scoped to the determined session.
scoped to the provided session(s).
- effective_query_filters (Dict[str, Any]): Filters for querying memories,
scoped to the determined session and potentially a resolved actor.
scoped to the provided session(s) and potentially a resolved actor.
"""
base_metadata_template = deepcopy(input_metadata) if input_metadata else {}
effective_query_filters = deepcopy(input_filters) if input_filters else {}
# ---------- resolve session id (mandatory) ----------
session_key, session_val = None, None
# ---------- add all provided session ids ----------
session_ids_provided = []
if user_id:
session_key, session_val = "user_id", user_id
elif agent_id:
session_key, session_val = "agent_id", agent_id
elif run_id:
session_key, session_val = "run_id", run_id
base_metadata_template["user_id"] = user_id
effective_query_filters["user_id"] = user_id
session_ids_provided.append("user_id")
if session_key is None:
raise ValueError("One of 'user_id', 'agent_id', or 'run_id' must be provided.")
if agent_id:
base_metadata_template["agent_id"] = agent_id
effective_query_filters["agent_id"] = agent_id
session_ids_provided.append("agent_id")
base_metadata_template[session_key] = session_val
effective_query_filters[session_key] = session_val
if run_id:
base_metadata_template["run_id"] = run_id
effective_query_filters["run_id"] = run_id
session_ids_provided.append("run_id")
if not session_ids_provided:
raise ValueError("At least one of 'user_id', 'agent_id', or 'run_id' must be provided.")
# ---------- optional actor filter ----------
resolved_actor_id = actor_id or effective_query_filters.get("actor_id")
@@ -333,10 +338,9 @@ class Memory(MemoryBase):
except Exception as e:
logging.error(f"Error in new_retrieved_facts: {e}")
new_retrieved_facts = []
if not new_retrieved_facts:
logger.debug("No new facts retrieved from input. Skipping memory update LLM call.")
return []
retrieved_old_memory = []
new_message_embeddings = {}
@@ -364,24 +368,27 @@ class Memory(MemoryBase):
temp_uuid_mapping[str(idx)] = item["id"]
retrieved_old_memory[idx]["id"] = str(idx)
function_calling_prompt = get_update_memory_messages(
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
)
try:
response: str = self.llm.generate_response(
messages=[{"role": "user", "content": function_calling_prompt}],
response_format={"type": "json_object"},
if new_retrieved_facts:
function_calling_prompt = get_update_memory_messages(
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
)
except Exception as e:
logging.error(f"Error in new memory actions response: {e}")
response = ""
try:
response = remove_code_blocks(response)
new_memories_with_actions = json.loads(response)
except Exception as e:
logging.error(f"Invalid JSON response: {e}")
try:
response: str = self.llm.generate_response(
messages=[{"role": "user", "content": function_calling_prompt}],
response_format={"type": "json_object"},
)
except Exception as e:
logging.error(f"Error in new memory actions response: {e}")
response = ""
try:
response = remove_code_blocks(response)
new_memories_with_actions = json.loads(response)
except Exception as e:
logging.error(f"Invalid JSON response: {e}")
new_memories_with_actions = {}
else:
new_memories_with_actions = {}
returned_memories = []
@@ -433,10 +440,11 @@ class Memory(MemoryBase):
except Exception as e:
logging.error(f"Error iterating new_memories_with_actions: {e}")
keys, encoded_ids = process_telemetry_filters(filters)
capture_event(
"mem0.add",
self,
{"version": self.api_version, "keys": list(filters.keys()), "sync_type": "sync"},
{"version": self.api_version, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"},
)
return returned_memories
@@ -529,8 +537,9 @@ class Memory(MemoryBase):
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
raise ValueError("At least one of 'user_id', 'agent_id', or 'run_id' must be specified.")
keys, encoded_ids = process_telemetry_filters(effective_filters)
capture_event(
"mem0.get_all", self, {"limit": limit, "keys": list(effective_filters.keys()), "sync_type": "sync"}
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"}
)
with concurrent.futures.ThreadPoolExecutor() as executor:
@@ -564,7 +573,9 @@ class Memory(MemoryBase):
def _get_all_from_vector_store(self, filters, limit):
memories_result = self.vector_store.list(filters=filters, limit=limit)
actual_memories = (
memories_result[0] if isinstance(memories_result, (tuple, list)) and len(memories_result) > 0 else memories_result
memories_result[0]
if isinstance(memories_result, (tuple, list)) and len(memories_result) > 0
else memories_result
)
promoted_payload_keys = [
@@ -607,6 +618,7 @@ class Memory(MemoryBase):
run_id: Optional[str] = None,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
threshold: Optional[float] = None,
):
"""
Searches for memories based on a query
@@ -617,6 +629,7 @@ class Memory(MemoryBase):
run_id (str, optional): ID of the run to search for. Defaults to None.
limit (int, optional): Limit the number of results. Defaults to 100.
filters (dict, optional): Filters to apply to the search. Defaults to None..
threshold (float, optional): Minimum score for a memory to be included in the results. Defaults to None.
Returns:
dict: A dictionary containing the search results, typically under a "results" key,
@@ -630,14 +643,22 @@ class Memory(MemoryBase):
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
raise ValueError("At least one of 'user_id', 'agent_id', or 'run_id' must be specified.")
keys, encoded_ids = process_telemetry_filters(effective_filters)
capture_event(
"mem0.search",
self,
{"limit": limit, "version": self.api_version, "keys": list(effective_filters.keys()), "sync_type": "sync"},
{
"limit": limit,
"version": self.api_version,
"keys": keys,
"encoded_ids": encoded_ids,
"sync_type": "sync",
"threshold": threshold,
},
)
with concurrent.futures.ThreadPoolExecutor() as executor:
future_memories = executor.submit(self._search_vector_store, query, effective_filters, limit)
future_memories = executor.submit(self._search_vector_store, query, effective_filters, limit, threshold)
future_graph_entities = (
executor.submit(self.graph.search, query, effective_filters, limit) if self.enable_graph else None
)
@@ -664,7 +685,7 @@ class Memory(MemoryBase):
else:
return {"results": original_memories}
def _search_vector_store(self, query, filters, limit):
def _search_vector_store(self, query, filters, limit, threshold: Optional[float] = None):
embeddings = self.embedding_model.embed(query, "search")
memories = self.vector_store.search(query=query, vectors=embeddings, limit=limit, filters=filters)
@@ -697,7 +718,8 @@ class Memory(MemoryBase):
if additional_metadata:
memory_item_dict["metadata"] = additional_metadata
original_memories.append(memory_item_dict)
if threshold is None or mem.score >= threshold:
original_memories.append(memory_item_dict)
return original_memories
@@ -752,7 +774,8 @@ class Memory(MemoryBase):
"At least one filter is required to delete all memories. If you want to delete all memories, use the `reset()` method."
)
capture_event("mem0.delete_all", self, {"keys": list(filters.keys()), "sync_type": "sync"})
keys, encoded_ids = process_telemetry_filters(filters)
capture_event("mem0.delete_all", self, {"keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"})
memories = self.vector_store.list(filters=filters)[0]
for memory in memories:
self._delete_memory(memory.id)
@@ -1086,7 +1109,7 @@ class AsyncMemory(MemoryBase):
self,
messages: list,
metadata: dict,
filters: dict,
effective_filters: dict,
infer: bool,
):
if not infer:
@@ -1141,13 +1164,11 @@ class AsyncMemory(MemoryBase):
response = remove_code_blocks(response)
new_retrieved_facts = json.loads(response)["facts"]
except Exception as e:
new_retrieved_facts = []
if not new_retrieved_facts:
logger.info("No new facts retrieved from input. Skipping memory update LLM call.")
return []
logging.error(f"Error in new_retrieved_facts: {e}")
new_retrieved_facts = []
if not new_retrieved_facts:
logger.debug("No new facts retrieved from input. Skipping memory update LLM call.")
retrieved_old_memory = []
new_message_embeddings = {}
@@ -1160,7 +1181,7 @@ class AsyncMemory(MemoryBase):
query=new_mem_content,
vectors=embeddings,
limit=5,
filters=filters, # 'filters' is query_filters_for_inference
filters=effective_filters, # 'filters' is query_filters_for_inference
)
return [{"id": mem.id, "text": mem.payload["data"]} for mem in existing_mems]
@@ -1179,34 +1200,25 @@ class AsyncMemory(MemoryBase):
temp_uuid_mapping[str(idx)] = item["id"]
retrieved_old_memory[idx]["id"] = str(idx)
function_calling_prompt = get_update_memory_messages(
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
)
try:
response = await asyncio.to_thread(
self.llm.generate_response,
messages=[{"role": "user", "content": function_calling_prompt}],
response_format={"type": "json_object"},
if new_retrieved_facts:
function_calling_prompt = get_update_memory_messages(
retrieved_old_memory, new_retrieved_facts, self.config.custom_update_memory_prompt
)
except Exception as e:
response = ""
logging.error(f"Error in new memory actions response: {e}")
response = ""
try:
response = remove_code_blocks(response)
new_memories_with_actions = json.loads(response)
except Exception as e:
new_memories_with_actions = {}
if not new_memories_with_actions:
logger.info("No new facts retrieved from input (async). Skipping memory update LLM call.")
return []
logging.error(f"Invalid JSON response: {e}")
new_memories_with_actions = {}
try:
response = await asyncio.to_thread(
self.llm.generate_response,
messages=[{"role": "user", "content": function_calling_prompt}],
response_format={"type": "json_object"},
)
except Exception as e:
logging.error(f"Error in new memory actions response: {e}")
response = ""
try:
response = remove_code_blocks(response)
new_memories_with_actions = json.loads(response)
except Exception as e:
logging.error(f"Invalid JSON response: {e}")
new_memories_with_actions = {}
returned_memories = []
try:
@@ -1267,8 +1279,11 @@ class AsyncMemory(MemoryBase):
except Exception as e:
logging.error(f"Error in memory processing loop (async): {e}")
keys, encoded_ids = process_telemetry_filters(effective_filters)
capture_event(
"mem0.add", self, {"version": self.api_version, "keys": list(filters.keys()), "sync_type": "async"}
"mem0.add",
self,
{"version": self.api_version, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"},
)
return returned_memories
@@ -1364,8 +1379,9 @@ class AsyncMemory(MemoryBase):
"at least one of 'user_id', 'agent_id', or 'run_id' must be specified for get_all."
)
keys, encoded_ids = process_telemetry_filters(effective_filters)
capture_event(
"mem0.get_all", self, {"limit": limit, "keys": list(effective_filters.keys()), "sync_type": "async"}
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"}
)
with concurrent.futures.ThreadPoolExecutor() as executor:
@@ -1399,7 +1415,9 @@ class AsyncMemory(MemoryBase):
async def _get_all_from_vector_store(self, filters, limit):
memories_result = await asyncio.to_thread(self.vector_store.list, filters=filters, limit=limit)
actual_memories = (
memories_result[0] if isinstance(memories_result, tuple) and len(memories_result) > 0 else memories_result
memories_result[0]
if isinstance(memories_result, (tuple, list)) and len(memories_result) > 0
else memories_result
)
promoted_payload_keys = [
@@ -1442,6 +1460,7 @@ class AsyncMemory(MemoryBase):
run_id: Optional[str] = None,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
threshold: Optional[float] = None,
):
"""
Searches for memories based on a query
@@ -1451,7 +1470,8 @@ class AsyncMemory(MemoryBase):
agent_id (str, optional): ID of the agent to search for. Defaults to None.
run_id (str, optional): ID of the run to search for. Defaults to None.
limit (int, optional): Limit the number of results. Defaults to 100.
filters (dict, optional): Filters to apply to the search. Defaults to None..
filters (dict, optional): Filters to apply to the search. Defaults to None.
threshold (float, optional): Minimum score for a memory to be included in the results. Defaults to None.
Returns:
dict: A dictionary containing the search results, typically under a "results" key,
@@ -1466,13 +1486,21 @@ class AsyncMemory(MemoryBase):
if not any(key in effective_filters for key in ("user_id", "agent_id", "run_id")):
raise ValueError("at least one of 'user_id', 'agent_id', or 'run_id' must be specified ")
keys, encoded_ids = process_telemetry_filters(effective_filters)
capture_event(
"mem0.search",
self,
{"limit": limit, "version": self.api_version, "keys": list(effective_filters.keys()), "sync_type": "async"},
{
"limit": limit,
"version": self.api_version,
"keys": keys,
"encoded_ids": encoded_ids,
"sync_type": "async",
"threshold": threshold,
},
)
vector_store_task = asyncio.create_task(self._search_vector_store(query, effective_filters, limit))
vector_store_task = asyncio.create_task(self._search_vector_store(query, effective_filters, limit, threshold))
graph_task = None
if self.enable_graph:
@@ -1502,7 +1530,7 @@ class AsyncMemory(MemoryBase):
else:
return {"results": original_memories}
async def _search_vector_store(self, query, filters, limit):
async def _search_vector_store(self, query, filters, limit, threshold: Optional[float] = None):
embeddings = await asyncio.to_thread(self.embedding_model.embed, query, "search")
memories = await asyncio.to_thread(
self.vector_store.search, query=query, vectors=embeddings, limit=limit, filters=filters
@@ -1537,7 +1565,8 @@ class AsyncMemory(MemoryBase):
if additional_metadata:
memory_item_dict["metadata"] = additional_metadata
original_memories.append(memory_item_dict)
if threshold is None or mem.score >= threshold:
original_memories.append(memory_item_dict)
return original_memories
@@ -1593,7 +1622,8 @@ class AsyncMemory(MemoryBase):
"At least one filter is required to delete all memories. If you want to delete all memories, use the `reset()` method."
)
capture_event("mem0.delete_all", self, {"keys": list(filters.keys()), "sync_type": "async"})
keys, encoded_ids = process_telemetry_filters(filters)
capture_event("mem0.delete_all", self, {"keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"})
memories = await asyncio.to_thread(self.vector_store.list, filters=filters)
delete_tasks = []
+211 -86
View File
@@ -118,11 +118,19 @@ class MemoryGraph:
return search_results
def delete_all(self, filters):
cypher = """
MATCH (n {user_id: $user_id})
DETACH DELETE n
"""
params = {"user_id": filters["user_id"]}
"""Delete all nodes and relationships for a user or specific agent."""
if filters.get("agent_id"):
cypher = """
MATCH (n:Entity {user_id: $user_id, agent_id: $agent_id})
DETACH DELETE n
"""
params = {"user_id": filters["user_id"], "agent_id": filters["agent_id"]}
else:
cypher = """
MATCH (n:Entity {user_id: $user_id})
DETACH DELETE n
"""
params = {"user_id": filters["user_id"]}
self.graph.query(cypher, params=params)
def get_all(self, filters, limit=100):
@@ -131,20 +139,31 @@ class MemoryGraph:
Args:
filters (dict): A dictionary containing filters to be applied during the retrieval.
Supports 'user_id' (required) and 'agent_id' (optional).
limit (int): The maximum number of nodes and relationships to retrieve. Defaults to 100.
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
- 'source': The source node name.
- 'relationship': The relationship type.
- 'target': The target node name.
"""
# return all nodes and relationships
query = """
MATCH (n:Entity {user_id: $user_id})-[r]->(m:Entity {user_id: $user_id})
RETURN n.name AS source, type(r) AS relationship, m.name AS target
LIMIT $limit
"""
results = self.graph.query(query, params={"user_id": filters["user_id"], "limit": limit})
# Build query based on whether agent_id is provided
if filters.get("agent_id"):
query = """
MATCH (n:Entity {user_id: $user_id, agent_id: $agent_id})-[r]->(m:Entity {user_id: $user_id, agent_id: $agent_id})
RETURN n.name AS source, type(r) AS relationship, m.name AS target
LIMIT $limit
"""
params = {"user_id": filters["user_id"], "agent_id": filters["agent_id"], "limit": limit}
else:
query = """
MATCH (n:Entity {user_id: $user_id})-[r]->(m:Entity {user_id: $user_id})
RETURN n.name AS source, type(r) AS relationship, m.name AS target
LIMIT $limit
"""
params = {"user_id": filters["user_id"], "limit": limit}
results = self.graph.query(query, params=params)
final_results = []
for result in results:
@@ -241,33 +260,65 @@ class MemoryGraph:
for node in node_list:
n_embedding = self.embedding_model.embed(node)
cypher_query = """
MATCH (n:Entity {user_id: $user_id})-[r]->(m:Entity)
WHERE n.embedding IS NOT NULL
WITH collect(n) AS nodes1, collect(m) AS nodes2, r
CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2)
YIELD node1, node2, similarity
WITH node1, node2, similarity, r
WHERE similarity >= $threshold
RETURN node1.user_id AS source, id(node1) AS source_id, type(r) AS relationship, id(r) AS relation_id, node2.user_id AS destination, id(node2) AS destination_id, similarity
UNION
MATCH (n:Entity {user_id: $user_id})<-[r]-(m:Entity)
WHERE n.embedding IS NOT NULL
WITH collect(n) AS nodes1, collect(m) AS nodes2, r
CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2)
YIELD node1, node2, similarity
WITH node1, node2, similarity, r
WHERE similarity >= $threshold
RETURN node2.name AS source, id(node2) AS source_id, type(r) AS relationship, id(r) AS relation_id, node1.name AS destination, id(node1) AS destination_id, similarity
ORDER BY similarity DESC
LIMIT $limit;
"""
params = {
"n_embedding": n_embedding,
"threshold": self.threshold,
"user_id": filters["user_id"],
"limit": limit,
}
# Build query based on whether agent_id is provided
if filters.get("agent_id"):
cypher_query = """
MATCH (n:Entity {user_id: $user_id, agent_id: $agent_id})-[r]->(m:Entity)
WHERE n.embedding IS NOT NULL
WITH collect(n) AS nodes1, collect(m) AS nodes2, r
CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2)
YIELD node1, node2, similarity
WITH node1, node2, similarity, r
WHERE similarity >= $threshold
RETURN node1.name AS source, id(node1) AS source_id, type(r) AS relationship, id(r) AS relation_id, node2.name AS destination, id(node2) AS destination_id, similarity
UNION
MATCH (n:Entity {user_id: $user_id, agent_id: $agent_id})<-[r]-(m:Entity)
WHERE n.embedding IS NOT NULL
WITH collect(n) AS nodes1, collect(m) AS nodes2, r
CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2)
YIELD node1, node2, similarity
WITH node1, node2, similarity, r
WHERE similarity >= $threshold
RETURN node2.name AS source, id(node2) AS source_id, type(r) AS relationship, id(r) AS relation_id, node1.name AS destination, id(node1) AS destination_id, similarity
ORDER BY similarity DESC
LIMIT $limit;
"""
params = {
"n_embedding": n_embedding,
"threshold": self.threshold,
"user_id": filters["user_id"],
"agent_id": filters["agent_id"],
"limit": limit,
}
else:
cypher_query = """
MATCH (n:Entity {user_id: $user_id})-[r]->(m:Entity)
WHERE n.embedding IS NOT NULL
WITH collect(n) AS nodes1, collect(m) AS nodes2, r
CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2)
YIELD node1, node2, similarity
WITH node1, node2, similarity, r
WHERE similarity >= $threshold
RETURN node1.name AS source, id(node1) AS source_id, type(r) AS relationship, id(r) AS relation_id, node2.name AS destination, id(node2) AS destination_id, similarity
UNION
MATCH (n:Entity {user_id: $user_id})<-[r]-(m:Entity)
WHERE n.embedding IS NOT NULL
WITH collect(n) AS nodes1, collect(m) AS nodes2, r
CALL node_similarity.cosine_pairwise("embedding", nodes1, nodes2)
YIELD node1, node2, similarity
WITH node1, node2, similarity, r
WHERE similarity >= $threshold
RETURN node2.name AS source, id(node2) AS source_id, type(r) AS relationship, id(r) AS relation_id, node1.name AS destination, id(node1) AS destination_id, similarity
ORDER BY similarity DESC
LIMIT $limit;
"""
params = {
"n_embedding": n_embedding,
"threshold": self.threshold,
"user_id": filters["user_id"],
"limit": limit,
}
ans = self.graph.query(cypher_query, params=params)
result_relations.extend(ans)
@@ -300,38 +351,54 @@ class MemoryGraph:
logger.debug(f"Deleted relationships: {to_be_deleted}")
return to_be_deleted
def _delete_entities(self, to_be_deleted, user_id):
def _delete_entities(self, to_be_deleted, filters):
"""Delete the entities from the graph."""
user_id = filters["user_id"]
agent_id = filters.get("agent_id", None)
results = []
for item in to_be_deleted:
source = item["source"]
destination = item["destination"]
relationship = item["relationship"]
# Build the agent filter for the query
agent_filter = ""
params = {
"source_name": source,
"dest_name": destination,
"user_id": user_id,
}
if agent_id:
agent_filter = "AND n.agent_id = $agent_id AND m.agent_id = $agent_id"
params["agent_id"] = agent_id
# Delete the specific relationship between nodes
cypher = f"""
MATCH (n:Entity {{name: $source_name, user_id: $user_id}})
-[r:{relationship}]->
(m {{name: $dest_name, user_id: $user_id}})
(m:Entity {{name: $dest_name, user_id: $user_id}})
WHERE 1=1 {agent_filter}
DELETE r
RETURN
n.name AS source,
m.name AS target,
type(r) AS relationship
"""
params = {
"source_name": source,
"dest_name": destination,
"user_id": user_id,
}
result = self.graph.query(cypher, params=params)
results.append(result)
return results
# added Entity label to all nodes for vector search to work
def _add_entities(self, to_be_added, user_id, entity_type_map):
def _add_entities(self, to_be_added, filters, entity_type_map):
"""Add the new entities to the graph. Merge the nodes if they already exist."""
user_id = filters["user_id"]
agent_id = filters.get("agent_id", None)
results = []
for item in to_be_added:
# entities
source = item["source"]
@@ -346,18 +413,21 @@ class MemoryGraph:
source_embedding = self.embedding_model.embed(source)
dest_embedding = self.embedding_model.embed(destination)
# search for the nodes with the closest embeddings; this is basically
# comparison of one embedding to all embeddings in a graph -> vector
# search with cosine similarity metric
source_node_search_result = self._search_source_node(source_embedding, user_id, threshold=0.9)
destination_node_search_result = self._search_destination_node(dest_embedding, user_id, threshold=0.9)
# search for the nodes with the closest embeddings
source_node_search_result = self._search_source_node(source_embedding, filters, threshold=0.9)
destination_node_search_result = self._search_destination_node(dest_embedding, filters, threshold=0.9)
# Prepare agent_id for node creation
agent_id_clause = ""
if agent_id:
agent_id_clause = ", agent_id: $agent_id"
# TODO: Create a cypher query and common params for all the cases
if not destination_node_search_result and source_node_search_result:
cypher = f"""
MATCH (source:Entity)
WHERE id(source) = $source_id
MERGE (destination:{destination_type}:Entity {{name: $destination_name, user_id: $user_id}})
MERGE (destination:{destination_type}:Entity {{name: $destination_name, user_id: $user_id{agent_id_clause}}})
ON CREATE SET
destination.created = timestamp(),
destination.embedding = $destination_embedding,
@@ -374,11 +444,14 @@ class MemoryGraph:
"destination_embedding": dest_embedding,
"user_id": user_id,
}
if agent_id:
params["agent_id"] = agent_id
elif destination_node_search_result and not source_node_search_result:
cypher = f"""
MATCH (destination:Entity)
WHERE id(destination) = $destination_id
MERGE (source:{source_type}:Entity {{name: $source_name, user_id: $user_id}})
MERGE (source:{source_type}:Entity {{name: $source_name, user_id: $user_id{agent_id_clause}}})
ON CREATE SET
source.created = timestamp(),
source.embedding = $source_embedding,
@@ -395,6 +468,9 @@ class MemoryGraph:
"source_embedding": source_embedding,
"user_id": user_id,
}
if agent_id:
params["agent_id"] = agent_id
elif source_node_search_result and destination_node_search_result:
cypher = f"""
MATCH (source:Entity)
@@ -412,12 +488,15 @@ class MemoryGraph:
"destination_id": destination_node_search_result[0]["id(destination_candidate)"],
"user_id": user_id,
}
if agent_id:
params["agent_id"] = agent_id
else:
cypher = f"""
MERGE (n:{source_type}:Entity {{name: $source_name, user_id: $user_id}})
MERGE (n:{source_type}:Entity {{name: $source_name, user_id: $user_id{agent_id_clause}}})
ON CREATE SET n.created = timestamp(), n.embedding = $source_embedding, n:Entity
ON MATCH SET n.embedding = $source_embedding
MERGE (m:{destination_type}:Entity {{name: $dest_name, user_id: $user_id}})
MERGE (m:{destination_type}:Entity {{name: $dest_name, user_id: $user_id{agent_id_clause}}})
ON CREATE SET m.created = timestamp(), m.embedding = $dest_embedding, m:Entity
ON MATCH SET m.embedding = $dest_embedding
MERGE (n)-[rel:{relationship}]->(m)
@@ -431,6 +510,9 @@ class MemoryGraph:
"dest_embedding": dest_embedding,
"user_id": user_id,
}
if agent_id:
params["agent_id"] = agent_id
result = self.graph.query(cypher, params=params)
results.append(result)
return results
@@ -442,37 +524,80 @@ class MemoryGraph:
item["destination"] = item["destination"].lower().replace(" ", "_")
return entity_list
def _search_source_node(self, source_embedding, user_id, threshold=0.9):
cypher = """
CALL vector_search.search("memzero", 1, $source_embedding)
YIELD distance, node, similarity
WITH node AS source_candidate, similarity
WHERE source_candidate.user_id = $user_id AND similarity >= $threshold
RETURN id(source_candidate);
"""
params = {
"source_embedding": source_embedding,
"user_id": user_id,
"threshold": threshold,
}
def _search_source_node(self, source_embedding, filters, threshold=0.9):
"""Search for source nodes with similar embeddings."""
user_id = filters["user_id"]
agent_id = filters.get("agent_id", None)
if agent_id:
cypher = """
CALL vector_search.search("memzero", 1, $source_embedding)
YIELD distance, node, similarity
WITH node AS source_candidate, similarity
WHERE source_candidate.user_id = $user_id
AND source_candidate.agent_id = $agent_id
AND similarity >= $threshold
RETURN id(source_candidate);
"""
params = {
"source_embedding": source_embedding,
"user_id": user_id,
"agent_id": agent_id,
"threshold": threshold,
}
else:
cypher = """
CALL vector_search.search("memzero", 1, $source_embedding)
YIELD distance, node, similarity
WITH node AS source_candidate, similarity
WHERE source_candidate.user_id = $user_id
AND similarity >= $threshold
RETURN id(source_candidate);
"""
params = {
"source_embedding": source_embedding,
"user_id": user_id,
"threshold": threshold,
}
result = self.graph.query(cypher, params=params)
return result
def _search_destination_node(self, destination_embedding, user_id, threshold=0.9):
cypher = """
CALL vector_search.search("memzero", 1, $destination_embedding)
YIELD distance, node, similarity
WITH node AS destination_candidate, similarity
WHERE node.user_id = $user_id AND similarity >= $threshold
RETURN id(destination_candidate);
"""
params = {
"destination_embedding": destination_embedding,
"user_id": user_id,
"threshold": threshold,
}
def _search_destination_node(self, destination_embedding, filters, threshold=0.9):
"""Search for destination nodes with similar embeddings."""
user_id = filters["user_id"]
agent_id = filters.get("agent_id", None)
if agent_id:
cypher = """
CALL vector_search.search("memzero", 1, $destination_embedding)
YIELD distance, node, similarity
WITH node AS destination_candidate, similarity
WHERE node.user_id = $user_id
AND node.agent_id = $agent_id
AND similarity >= $threshold
RETURN id(destination_candidate);
"""
params = {
"destination_embedding": destination_embedding,
"user_id": user_id,
"agent_id": agent_id,
"threshold": threshold,
}
else:
cypher = """
CALL vector_search.search("memzero", 1, $destination_embedding)
YIELD distance, node, similarity
WITH node AS destination_candidate, similarity
WHERE node.user_id = $user_id
AND similarity >= $threshold
RETURN id(destination_candidate);
"""
params = {
"destination_embedding": destination_embedding,
"user_id": user_id,
"threshold": threshold,
}
result = self.graph.query(cypher, params=params)
return result
+129 -67
View File
@@ -21,59 +21,107 @@ class SQLiteManager:
rename it, create the new schema, copy the intersecting data, then
drop the old table.
"""
with self._lock, self.connection:
cur = self.connection.cursor()
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='history'")
if cur.fetchone() is None:
return # nothing to migrate
with self._lock:
try:
# Start a transaction
self.connection.execute("BEGIN")
cur = self.connection.cursor()
cur.execute("PRAGMA table_info(history)")
old_cols = {row[1] for row in cur.fetchall()}
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='history'")
if cur.fetchone() is None:
self.connection.execute("COMMIT")
return # nothing to migrate
expected_cols = {
"id",
"memory_id",
"old_memory",
"new_memory",
"event",
"created_at",
"updated_at",
"is_deleted",
"actor_id",
"role",
}
cur.execute("PRAGMA table_info(history)")
old_cols = {row[1] for row in cur.fetchall()}
if old_cols == expected_cols:
return
expected_cols = {
"id",
"memory_id",
"old_memory",
"new_memory",
"event",
"created_at",
"updated_at",
"is_deleted",
"actor_id",
"role",
}
logger.info("Migrating history table to new schema (no convo columns).")
cur.execute("ALTER TABLE history RENAME TO history_old")
if old_cols == expected_cols:
self.connection.execute("COMMIT")
return
self._create_history_table()
logger.info("Migrating history table to new schema (no convo columns).")
intersecting = list(expected_cols & old_cols)
cols_csv = ", ".join(intersecting)
cur.execute(f"INSERT INTO history ({cols_csv}) SELECT {cols_csv} FROM history_old")
cur.execute("DROP TABLE history_old")
# Clean up any existing history_old table from previous failed migration
cur.execute("DROP TABLE IF EXISTS history_old")
# Rename the current history table
cur.execute("ALTER TABLE history RENAME TO history_old")
# Create the new history table with updated schema
cur.execute(
"""
CREATE TABLE history (
id TEXT PRIMARY KEY,
memory_id TEXT,
old_memory TEXT,
new_memory TEXT,
event TEXT,
created_at DATETIME,
updated_at DATETIME,
is_deleted INTEGER,
actor_id TEXT,
role TEXT
)
"""
)
# Copy data from old table to new table
intersecting = list(expected_cols & old_cols)
if intersecting:
cols_csv = ", ".join(intersecting)
cur.execute(f"INSERT INTO history ({cols_csv}) SELECT {cols_csv} FROM history_old")
# Drop the old table
cur.execute("DROP TABLE history_old")
# Commit the transaction
self.connection.execute("COMMIT")
logger.info("History table migration completed successfully.")
except Exception as e:
# Rollback the transaction on any error
self.connection.execute("ROLLBACK")
logger.error(f"History table migration failed: {e}")
raise
def _create_history_table(self) -> None:
with self._lock, self.connection:
self.connection.execute(
with self._lock:
try:
self.connection.execute("BEGIN")
self.connection.execute(
"""
CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY,
memory_id TEXT,
old_memory TEXT,
new_memory TEXT,
event TEXT,
created_at DATETIME,
updated_at DATETIME,
is_deleted INTEGER,
actor_id TEXT,
role TEXT
)
"""
CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY,
memory_id TEXT,
old_memory TEXT,
new_memory TEXT,
event TEXT,
created_at DATETIME,
updated_at DATETIME,
is_deleted INTEGER,
actor_id TEXT,
role TEXT
)
"""
)
self.connection.execute("COMMIT")
except Exception as e:
self.connection.execute("ROLLBACK")
logger.error(f"Failed to create history table: {e}")
raise
def add_history(
self,
@@ -88,28 +136,35 @@ class SQLiteManager:
actor_id: Optional[str] = None,
role: Optional[str] = None,
) -> None:
with self._lock, self.connection:
self.connection.execute(
"""
INSERT INTO history (
id, memory_id, old_memory, new_memory, event,
created_at, updated_at, is_deleted, actor_id, role
with self._lock:
try:
self.connection.execute("BEGIN")
self.connection.execute(
"""
INSERT INTO history (
id, memory_id, old_memory, new_memory, event,
created_at, updated_at, is_deleted, actor_id, role
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
str(uuid.uuid4()),
memory_id,
old_memory,
new_memory,
event,
created_at,
updated_at,
is_deleted,
actor_id,
role,
),
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
str(uuid.uuid4()),
memory_id,
old_memory,
new_memory,
event,
created_at,
updated_at,
is_deleted,
actor_id,
role,
),
)
self.connection.execute("COMMIT")
except Exception as e:
self.connection.execute("ROLLBACK")
logger.error(f"Failed to add history record: {e}")
raise
def get_history(self, memory_id: str) -> List[Dict[str, Any]]:
with self._lock:
@@ -143,9 +198,16 @@ class SQLiteManager:
def reset(self) -> None:
"""Drop and recreate the history table."""
with self._lock, self.connection:
self.connection.execute("DROP TABLE IF EXISTS history")
self._create_history_table()
with self._lock:
try:
self.connection.execute("BEGIN")
self.connection.execute("DROP TABLE IF EXISTS history")
self.connection.execute("COMMIT")
self._create_history_table()
except Exception as e:
self.connection.execute("ROLLBACK")
logger.error(f"Failed to reset history table: {e}")
raise
def close(self) -> None:
if self.connection:
+19
View File
@@ -1,4 +1,5 @@
import re
import hashlib
from mem0.configs.prompts import FACT_RETRIEVAL_PROMPT
@@ -98,3 +99,21 @@ def parse_vision_messages(messages, llm=None, vision_details="auto"):
returned_messages.append(msg)
return returned_messages
def process_telemetry_filters(filters):
"""
Process the telemetry filters
"""
if filters is None:
return {}
encoded_ids = {}
if "user_id" in filters:
encoded_ids["user_id"] = hashlib.md5(filters["user_id"].encode()).hexdigest()
if "agent_id" in filters:
encoded_ids["agent_id"] = hashlib.md5(filters["agent_id"].encode()).hexdigest()
if "run_id" in filters:
encoded_ids["run_id"] = hashlib.md5(filters["run_id"].encode()).hexdigest()
return list(filters.keys()), encoded_ids
+1
View File
@@ -79,6 +79,7 @@ class VectorStoreFactory:
"upstash_vector": "mem0.vector_stores.upstash_vector.UpstashVector",
"azure_ai_search": "mem0.vector_stores.azure_ai_search.AzureAISearch",
"pinecone": "mem0.vector_stores.pinecone.PineconeDB",
"mongodb": "mem0.vector_stores.mongodb.MongoDB",
"redis": "mem0.vector_stores.redis.RedisDB",
"elasticsearch": "mem0.vector_stores.elasticsearch.ElasticsearchDB",
"vertex_ai_vector_search": "mem0.vector_stores.vertex_ai_vector_search.GoogleMatchingEngine",
+1
View File
@@ -15,6 +15,7 @@ class VectorStoreConfig(BaseModel):
"chroma": "ChromaDbConfig",
"pgvector": "PGVectorConfig",
"pinecone": "PineconeConfig",
"mongodb": "MongoDBConfig",
"milvus": "MilvusDBConfig",
"upstash_vector": "UpstashVectorConfig",
"azure_ai_search": "AzureAISearchConfig",
+299
View File
@@ -0,0 +1,299 @@
import logging
from typing import List, Optional, Dict, Any, Callable
from pydantic import BaseModel
try:
from pymongo import MongoClient
from pymongo.operations import SearchIndexModel
from pymongo.errors import PyMongoError
except ImportError:
raise ImportError("The 'pymongo' library is required. Please install it using 'pip install pymongo'.")
from mem0.vector_stores.base import VectorStoreBase
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
class OutputData(BaseModel):
id: Optional[str]
score: Optional[float]
payload: Optional[dict]
class MongoVector(VectorStoreBase):
VECTOR_TYPE = "knnVector"
SIMILARITY_METRIC = "cosine"
def __init__(
self,
db_name: str,
collection_name: str,
embedding_model_dims: int,
mongo_uri: str
):
"""
Initialize the MongoDB vector store with vector search capabilities.
Args:
db_name (str): Database name
collection_name (str): Collection name
embedding_model_dims (int): Dimension of the embedding vector
mongo_uri (str): MongoDB connection URI
"""
self.collection_name = collection_name
self.embedding_model_dims = embedding_model_dims
self.db_name = db_name
self.client = MongoClient(
mongo_uri
)
self.db = self.client[db_name]
self.collection = self.create_col()
def create_col(self):
"""Create new collection with vector search index."""
try:
database = self.client[self.db_name]
collection_names = database.list_collection_names()
if self.collection_name not in collection_names:
logger.info(f"Collection '{self.collection_name}' does not exist. Creating it now.")
collection = database[self.collection_name]
# Insert and remove a placeholder document to create the collection
collection.insert_one({"_id": 0, "placeholder": True})
collection.delete_one({"_id": 0})
logger.info(f"Collection '{self.collection_name}' created successfully.")
else:
collection = database[self.collection_name]
self.index_name = f"{self.collection_name}_vector_index"
found_indexes = list(collection.list_search_indexes(name=self.index_name))
if found_indexes:
logger.info(f"Search index '{self.index_name}' already exists in collection '{self.collection_name}'.")
else:
search_index_model = SearchIndexModel(
name=self.index_name,
definition={
"mappings": {
"dynamic": False,
"fields": {
"embedding": {
"type": self.VECTOR_TYPE,
"dimensions": self.embedding_model_dims,
"similarity": self.SIMILARITY_METRIC,
}
},
}
},
)
collection.create_search_index(search_index_model)
logger.info(
f"Search index '{self.index_name}' created successfully for collection '{self.collection_name}'."
)
return collection
except PyMongoError as e:
logger.error(f"Error creating collection and search index: {e}")
return None
def insert(
self, vectors: List[List[float]], payloads: Optional[List[Dict]] = None, ids: Optional[List[str]] = None
) -> None:
"""
Insert vectors into the collection.
Args:
vectors (List[List[float]]): List of vectors to insert.
payloads (List[Dict], optional): List of payloads corresponding to vectors.
ids (List[str], optional): List of IDs corresponding to vectors.
"""
logger.info(f"Inserting {len(vectors)} vectors into collection '{self.collection_name}'.")
data = []
for vector, payload, _id in zip(vectors, payloads or [{}] * len(vectors), ids or [None] * len(vectors)):
document = {"_id": _id, "embedding": vector, "payload": payload}
data.append(document)
try:
self.collection.insert_many(data)
logger.info(f"Inserted {len(data)} documents into '{self.collection_name}'.")
except PyMongoError as e:
logger.error(f"Error inserting data: {e}")
def search(self, query: str, query_vector: List[float], limit=5, filters: Optional[Dict] = None) -> List[OutputData]:
"""
Search for similar vectors using the vector search index.
Args:
query (str): Query string
query_vector (List[float]): Query vector.
limit (int, optional): Number of results to return. Defaults to 5.
filters (Dict, optional): Filters to apply to the search.
Returns:
List[OutputData]: Search results.
"""
found_indexes = list(self.collection.list_search_indexes(name=self.index_name))
if not found_indexes:
logger.error(f"Index '{self.index_name}' does not exist.")
return []
results = []
try:
collection = self.client[self.db_name][self.collection_name]
pipeline = [
{
"$vectorSearch": {
"index": self.index_name,
"limit": limit,
"numCandidates": limit,
"queryVector": query_vector,
"path": "embedding",
}
},
{"$set": {"score": {"$meta": "vectorSearchScore"}}},
{"$project": {"embedding": 0}},
]
results = list(collection.aggregate(pipeline))
logger.info(f"Vector search completed. Found {len(results)} documents.")
except Exception as e:
logger.error(f"Error during vector search for query {query}: {e}")
return []
output = [OutputData(id=str(doc["_id"]), score=doc.get("score"), payload=doc.get("payload")) for doc in results]
return output
def delete(self, vector_id: str) -> None:
"""
Delete a vector by ID.
Args:
vector_id (str): ID of the vector to delete.
"""
try:
result = self.collection.delete_one({"_id": vector_id})
if result.deleted_count > 0:
logger.info(f"Deleted document with ID '{vector_id}'.")
else:
logger.warning(f"No document found with ID '{vector_id}' to delete.")
except PyMongoError as e:
logger.error(f"Error deleting document: {e}")
def update(self, vector_id: str, vector: Optional[List[float]] = None, payload: Optional[Dict] = None) -> None:
"""
Update a vector and its payload.
Args:
vector_id (str): ID of the vector to update.
vector (List[float], optional): Updated vector.
payload (Dict, optional): Updated payload.
"""
update_fields = {}
if vector is not None:
update_fields["embedding"] = vector
if payload is not None:
update_fields["payload"] = payload
if update_fields:
try:
result = self.collection.update_one({"_id": vector_id}, {"$set": update_fields})
if result.matched_count > 0:
logger.info(f"Updated document with ID '{vector_id}'.")
else:
logger.warning(f"No document found with ID '{vector_id}' to update.")
except PyMongoError as e:
logger.error(f"Error updating document: {e}")
def get(self, vector_id: str) -> Optional[OutputData]:
"""
Retrieve a vector by ID.
Args:
vector_id (str): ID of the vector to retrieve.
Returns:
Optional[OutputData]: Retrieved vector or None if not found.
"""
try:
doc = self.collection.find_one({"_id": vector_id})
if doc:
logger.info(f"Retrieved document with ID '{vector_id}'.")
return OutputData(id=str(doc["_id"]), score=None, payload=doc.get("payload"))
else:
logger.warning(f"Document with ID '{vector_id}' not found.")
return None
except PyMongoError as e:
logger.error(f"Error retrieving document: {e}")
return None
def list_cols(self) -> List[str]:
"""
List all collections in the database.
Returns:
List[str]: List of collection names.
"""
try:
collections = self.db.list_collection_names()
logger.info(f"Listing collections in database '{self.db_name}': {collections}")
return collections
except PyMongoError as e:
logger.error(f"Error listing collections: {e}")
return []
def delete_col(self) -> None:
"""Delete the collection."""
try:
self.collection.drop()
logger.info(f"Deleted collection '{self.collection_name}'.")
except PyMongoError as e:
logger.error(f"Error deleting collection: {e}")
def col_info(self) -> Dict[str, Any]:
"""
Get information about the collection.
Returns:
Dict[str, Any]: Collection information.
"""
try:
stats = self.db.command("collstats", self.collection_name)
info = {"name": self.collection_name, "count": stats.get("count"), "size": stats.get("size")}
logger.info(f"Collection info: {info}")
return info
except PyMongoError as e:
logger.error(f"Error getting collection info: {e}")
return {}
def list(self, filters: Optional[Dict] = None, limit: int = 100) -> List[OutputData]:
"""
List vectors in the collection.
Args:
filters (Dict, optional): Filters to apply to the list.
limit (int, optional): Number of vectors to return.
Returns:
List[OutputData]: List of vectors.
"""
try:
query = filters or {}
cursor = self.collection.find(query).limit(limit)
results = [OutputData(id=str(doc["_id"]), score=None, payload=doc.get("payload")) for doc in cursor]
logger.info(f"Retrieved {len(results)} documents from collection '{self.collection_name}'.")
return results
except PyMongoError as e:
logger.error(f"Error listing documents: {e}")
return []
def reset(self):
"""Reset the index by deleting and recreating it."""
logger.warning(f"Resetting index {self.collection_name}...")
self.delete_col()
self.collection = self.create_col(self.collection_name)
def __del__(self) -> None:
"""Close the database connection when the object is deleted."""
if hasattr(self, "client"):
self.client.close()
logger.info("MongoClient connection closed.")
+3 -3
View File
@@ -83,19 +83,19 @@ class OpenSearchDB(VectorStoreBase):
},
}
logger.warning(f"Creating index {name}, it might take 1-2 minutes...")
if not self.client.indices.exists(index=name):
self.client.indices.create(index=name, body=index_settings)
logger.info(f"Created index {name}")
# Wait for index to be ready
max_retries = 60 # 60 seconds timeout
max_retries = 180 # 3 minutes timeout
retry_count = 0
while retry_count < max_retries:
try:
# Check if index is ready by attempting a simple search
self.client.search(index=name, body={"query": {"match_all": {}}})
logger.info(f"Index {name} is ready")
time.sleep(1)
logger.info(f"Index {name} is ready")
return
except Exception:
retry_count += 1
+1 -2
View File
@@ -5,8 +5,7 @@ from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel
try:
from pinecone import Pinecone, PodSpec, ServerlessSpec
from pinecone.data.dataclasses.vector import Vector
from pinecone import Pinecone, PodSpec, ServerlessSpec, Vector
except ImportError:
raise ImportError(
"Pinecone requires extra dependencies. Install with `pip install pinecone pinecone-text`"
+50
View File
@@ -37,6 +37,45 @@ curl -sL https://raw.githubusercontent.com/mem0ai/mem0/main/openmemory/run.sh |
## Quickstart
### 1. Set Up Environment Variables
Before running the project, you need to configure environment variables for both the API and the UI.
You can do this in one of the following ways:
- **Manually**:
Create a `.env` file in each of the following directories:
- `/api/.env`
- `/ui/.env`
- **Using `.env.example` files**:
Copy and rename the example files:
```bash
cp api/.env.example api/.env
cp ui/.env.example ui/.env
```
- **Using Makefile** (if supported):
Run:
```bash
make env
```
- #### Example `/api/.env`
```env
OPENAI_API_KEY=sk-xxx
USER=<user-id> # The User Id you want to associate the memories with
```
- #### Example `/ui/.env`
```env
NEXT_PUBLIC_API_URL=http://localhost:8765
NEXT_PUBLIC_USER_ID=<user-id> # Same as the user id for environment variable in api
```
### 2. Build and Run the Project
You can run the project using the following two commands:
```bash
make build # builds the mcp server and ui
@@ -47,6 +86,17 @@ After running these commands, you will have:
- OpenMemory MCP server running at: http://localhost:8765 (API documentation available at http://localhost:8765/docs)
- OpenMemory UI running at: http://localhost:3000
#### UI not working on `localhost:3000`?
If the UI does not start properly on [http://localhost:3000](http://localhost:3000), try running it manually:
```bash
cd ui
pnpm install
pnpm dev
```
## Project Structure
- `api/` - Backend APIs + MCP server
+2 -1
View File
@@ -1 +1,2 @@
OPENAI_API_KEY=sk-xxx
OPENAI_API_KEY=sk-xxx
USER=user
@@ -0,0 +1,36 @@
"""remove_global_unique_constraint_on_app_name_add_composite_unique
Revision ID: afd00efbd06b
Revises: add_config_table
Create Date: 2025-06-04 01:59:41.637440
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'afd00efbd06b'
down_revision: Union[str, None] = 'add_config_table'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_apps_name', table_name='apps')
op.create_index(op.f('ix_apps_name'), 'apps', ['name'], unique=False)
op.create_index('idx_app_owner_name', 'apps', ['owner_id', 'name'], unique=True)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('idx_app_owner_name', table_name='apps')
op.drop_index(op.f('ix_apps_name'), table_name='apps')
op.create_index('ix_apps_name', 'apps', ['name'], unique=True)
# ### end Alembic commands ###
+6 -1
View File
@@ -1,6 +1,7 @@
import enum
import uuid
import datetime
import sqlalchemy as sa
from sqlalchemy import (
Column, String, Boolean, ForeignKey, Enum, Table,
DateTime, JSON, Integer, UUID, Index, event
@@ -43,7 +44,7 @@ class App(Base):
__tablename__ = "apps"
id = Column(UUID, primary_key=True, default=lambda: uuid.uuid4())
owner_id = Column(UUID, ForeignKey("users.id"), nullable=False, index=True)
name = Column(String, unique=True, nullable=False, index=True)
name = Column(String, nullable=False, index=True)
description = Column(String)
metadata_ = Column('metadata', JSON, default=dict)
is_active = Column(Boolean, default=True, index=True)
@@ -55,6 +56,10 @@ class App(Base):
owner = relationship("User", back_populates="apps")
memories = relationship("Memory", back_populates="app")
__table_args__ = (
sa.UniqueConstraint('owner_id', 'name', name='idx_app_owner_name'),
)
class Config(Base):
__tablename__ = "configs"
+2 -1
View File
@@ -212,7 +212,8 @@ async def create_memory(
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Get or create app
app_obj = db.query(App).filter(App.name == request.app).first()
app_obj = db.query(App).filter(App.name == request.app,
App.owner_id == user.id).first()
if not app_obj:
app_obj = App(name=request.app, owner_id=user.id)
db.add(app_obj)
+23 -16
View File
@@ -1,15 +1,13 @@
import json
import logging
from openai import OpenAI
from typing import List
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import BaseModel
from tenacity import retry, stop_after_attempt, wait_exponential
from app.utils.prompts import MEMORY_CATEGORIZATION_PROMPT
load_dotenv()
openai_client = OpenAI()
@@ -19,19 +17,28 @@ class MemoryCategories(BaseModel):
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=15))
def get_categories_for_memory(memory: str) -> List[str]:
"""Get categories for a memory."""
try:
response = openai_client.responses.parse(
messages = [
{"role": "system", "content": MEMORY_CATEGORIZATION_PROMPT},
{"role": "user", "content": memory}
]
# Let OpenAI handle the pydantic parsing directly
completion = openai_client.chat.completions.with_response_format(
response_format=MemoryCategories
).create(
model="gpt-4o-mini",
instructions=MEMORY_CATEGORIZATION_PROMPT,
input=memory,
temperature=0,
text_format=MemoryCategories,
messages=messages,
temperature=0
)
response_json =json.loads(response.output[0].content[0].text)
categories = response_json['categories']
categories = [cat.strip().lower() for cat in categories]
# TODO: Validate categories later may be
return categories
parsed: MemoryCategories = completion.choices[0].message.parsed
return [cat.strip().lower() for cat in parsed.categories]
except Exception as e:
raise e
logging.error(f"[ERROR] Failed to get categories: {e}")
try:
logging.debug(f"[DEBUG] Raw response: {completion.choices[0].message.content}")
except Exception as debug_e:
logging.debug(f"[DEBUG] Could not extract raw response: {debug_e}")
raise
+2 -2
View File
@@ -10,7 +10,7 @@ interface ProfileState {
}
const initialState: ProfileState = {
userId: process.env.NEXT_PUBLIC_USER_ID || 'deshraj',
userId: process.env.NEXT_PUBLIC_USER_ID || 'user',
totalMemories: 0,
totalApps: 0,
status: 'idle',
@@ -36,7 +36,7 @@ const profileSlice = createSlice({
resetProfileState: (state) => {
state.status = 'idle';
state.error = null;
state.userId = process.env.NEXT_PUBLIC_USER_ID || 'deshraj';
state.userId = process.env.NEXT_PUBLIC_USER_ID || 'user';
},
setTotalMemories: (state, action: PayloadAction<number>) => {
state.totalMemories = action.payload;
Generated
+1 -1
View File
@@ -2200,4 +2200,4 @@ graph = ["langchain-neo4j", "neo4j", "rank-bm25"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.9,<4.0"
content-hash = "07f2aee9c596c2d2470df085b92551b7b7e3c19cabe61ae5bee7505395601417"
content-hash = "07f2aee9c596c2d2470df085b92551b7b7e3c19cabe61ae5bee7505395601417"
+59 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "mem0ai"
version = "0.1.104"
version = "0.1.109"
description = "Long-term memory for AI Agents"
authors = [
{ name = "Mem0", email = "founders@mem0.ai" }
@@ -26,6 +26,34 @@ graph = [
"neo4j>=5.23.1",
"rank-bm25>=0.2.2",
]
vector_stores = [
"vecs>=0.4.0",
"chromadb>=0.4.24",
"weaviate-client>=4.4.0",
"pinecone<7.0.0",
"pinecone-text>=0.1.1",
"faiss-cpu>=1.7.4",
"upstash-vector>=0.1.0",
"azure-search-documents>=11.4.0b8",
]
llms = [
"groq>=0.3.0",
"together>=0.2.10",
"litellm>=0.1.0",
"ollama>=0.1.0",
"vertexai>=0.1.0",
"google-generativeai>=0.3.0",
"google-genai>=1.0.0",
]
extras = [
"boto3>=1.34.0",
"langchain-community>=0.0.0",
"sentence-transformers>=2.2.2",
"elasticsearch>=8.0.0",
"opensearch-py>=2.0.0",
"langchain-memgraph>=0.1.0",
]
test = [
"pytest>=8.2.2",
"pytest-mock>=3.14.0",
@@ -53,6 +81,36 @@ only-include = ["mem0"]
[tool.hatch.build.targets.wheel.shared-data]
"README.md" = "README.md"
[tool.hatch.envs.dev_py_3_9]
python = "3.9"
features = [
"test",
"graph",
"vector_stores",
"llms",
"extras",
]
[tool.hatch.envs.dev_py_3_10]
python = "3.10"
features = [
"test",
"graph",
"vector_stores",
"llms",
"extras",
]
[tool.hatch.envs.dev_py_3_11]
python = "3.11"
features = [
"test",
"graph",
"vector_stores",
"llms",
"extras",
]
[tool.hatch.envs.default.scripts]
format = [
"ruff format",
+27
View File
@@ -42,3 +42,30 @@ def test_generate_response_without_tools(mock_lm_studio_client):
)
assert response == "I'm doing well, thank you for asking!"
def test_generate_response_specifying_response_format(mock_lm_studio_client):
config = BaseLlmConfig(
model="lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
temperature=0.7,
max_tokens=100,
top_p=1.0,
lmstudio_response_format={"type": "json_schema"}, # Specifying the response format in config
)
llm = LMStudioLLM(config)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello, how are you?"},
]
response = llm.generate_response(messages)
mock_lm_studio_client.chat.completions.create.assert_called_once_with(
model="lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
messages=messages,
temperature=0.7,
max_tokens=100,
top_p=1.0,
response_format={"type": "json_schema"},
)
assert response == "I'm doing well, thank you for asking!"
+14 -5
View File
@@ -40,10 +40,12 @@ class TestAddToVectorStoreErrors:
return memory
def test_empty_llm_response_fact_extraction(self, mock_memory, caplog):
def test_empty_llm_response_fact_extraction(self, mocker, mock_memory, caplog):
"""Test empty response from LLM during fact extraction"""
# Setup
mock_memory.llm.generate_response.return_value = ""
mock_capture_event = mocker.MagicMock()
mocker.patch("mem0.memory.main.capture_event", mock_capture_event)
# Execute
with caplog.at_level(logging.ERROR):
@@ -52,9 +54,10 @@ class TestAddToVectorStoreErrors:
)
# Verify
assert mock_memory.llm.generate_response.call_count == 2
assert mock_memory.llm.generate_response.call_count == 1
assert result == [] # Should return empty list when no memories processed
assert "Error in new_retrieved_facts" in caplog.text
assert mock_capture_event.call_count == 1
def test_empty_llm_response_memory_actions(self, mock_memory, caplog):
"""Test empty response from LLM during memory actions"""
@@ -94,25 +97,31 @@ class TestAsyncAddToVectorStoreErrors:
"""Test empty response in AsyncMemory._add_to_vector_store"""
mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock())
mock_async_memory.llm.generate_response.return_value = ""
mock_capture_event = mocker.MagicMock()
mocker.patch("mem0.memory.main.capture_event", mock_capture_event)
with caplog.at_level(logging.ERROR):
result = await mock_async_memory._add_to_vector_store(
messages=[{"role": "user", "content": "test"}], metadata={}, filters={}, infer=True
messages=[{"role": "user", "content": "test"}], metadata={}, effective_filters={}, infer=True
)
assert mock_async_memory.llm.generate_response.call_count == 1
assert result == []
assert "Error in new_retrieved_facts" in caplog.text
assert mock_capture_event.call_count == 1
@pytest.mark.asyncio
async def test_async_empty_llm_response_memory_actions(self, mock_async_memory, caplog, mocker):
"""Test empty response in AsyncMemory._add_to_vector_store"""
mocker.patch("mem0.utils.factory.EmbedderFactory.create", return_value=MagicMock())
mock_async_memory.llm.generate_response.side_effect = ['{"facts": ["test fact"]}', ""]
mock_capture_event = mocker.MagicMock()
mocker.patch("mem0.memory.main.capture_event", mock_capture_event)
with caplog.at_level(logging.ERROR):
result = await mock_async_memory._add_to_vector_store(
messages=[{"role": "user", "content": "test"}], metadata={}, filters={}, infer=True
messages=[{"role": "user", "content": "test"}], metadata={}, effective_filters={}, infer=True
)
assert result == []
assert "Invalid JSON response" in caplog.text
assert mock_capture_event.call_count == 1
+9 -3
View File
@@ -19,13 +19,14 @@ def mock_openai():
def memory_instance():
with (
patch("mem0.utils.factory.EmbedderFactory") as mock_embedder,
patch("mem0.utils.factory.VectorStoreFactory") as mock_vector_store,
patch("mem0.memory.main.VectorStoreFactory") as mock_vector_store,
patch("mem0.utils.factory.LlmFactory") as mock_llm,
patch("mem0.memory.telemetry.capture_event"),
patch("mem0.memory.graph_memory.MemoryGraph"),
):
mock_embedder.create.return_value = Mock()
mock_vector_store.create.return_value = Mock()
mock_vector_store.create.return_value.search.return_value = []
mock_llm.create.return_value = Mock()
config = MemoryConfig(version="v1.1")
@@ -37,13 +38,14 @@ def memory_instance():
def memory_custom_instance():
with (
patch("mem0.utils.factory.EmbedderFactory") as mock_embedder,
patch("mem0.utils.factory.VectorStoreFactory") as mock_vector_store,
patch("mem0.memory.main.VectorStoreFactory") as mock_vector_store,
patch("mem0.utils.factory.LlmFactory") as mock_llm,
patch("mem0.memory.telemetry.capture_event"),
patch("mem0.memory.graph_memory.MemoryGraph"),
):
mock_embedder.create.return_value = Mock()
mock_vector_store.create.return_value = Mock()
mock_vector_store.create.return_value.search.return_value = []
mock_llm.create.return_value = Mock()
config = MemoryConfig(
@@ -250,7 +252,11 @@ def test_get_all(memory_instance, version, enable_graph, expected_result):
def test_custom_prompts(memory_custom_instance):
messages = [{"role": "user", "content": "Test message"}]
from mem0.embeddings.mock import MockEmbeddings
memory_custom_instance.llm.generate_response = Mock()
memory_custom_instance.llm.generate_response.return_value = '{"facts": ["fact1", "fact2"]}'
memory_custom_instance.embedding_model = MockEmbeddings()
with patch("mem0.memory.main.parse_messages", return_value="Test message") as mock_parse_messages:
with patch(
@@ -273,7 +279,7 @@ def test_custom_prompts(memory_custom_instance):
## custom update memory prompt
##
mock_get_update_memory_messages.assert_called_once_with(
[], [], memory_custom_instance.config.custom_update_memory_prompt
[], ["fact1", "fact2"], memory_custom_instance.config.custom_update_memory_prompt
)
memory_custom_instance.llm.generate_response.assert_any_call(
+176
View File
@@ -0,0 +1,176 @@
import time
import pytest
from unittest.mock import MagicMock, patch
from mem0.vector_stores.mongodb import MongoVector
from pymongo.operations import SearchIndexModel
@pytest.fixture
@patch("mem0.vector_stores.mongodb.MongoClient")
def mongo_vector_fixture(mock_mongo_client):
mock_client = mock_mongo_client.return_value
mock_db = mock_client["test_db"]
mock_collection = mock_db["test_collection"]
mock_collection.list_search_indexes.return_value = []
mock_collection.aggregate.return_value = []
mock_collection.find_one.return_value = None
mock_collection.find.return_value = []
mock_db.list_collection_names.return_value = []
mongo_vector = MongoVector(
db_name="test_db",
collection_name="test_collection",
embedding_model_dims=1536,
user="username",
password="password",
)
return mongo_vector, mock_collection, mock_db
def test_initalize_create_col(mongo_vector_fixture):
mongo_vector, mock_collection, mock_db = mongo_vector_fixture
assert mongo_vector.collection_name == "test_collection"
assert mongo_vector.embedding_model_dims == 1536
assert mongo_vector.db_name == "test_db"
# Verify create_col being called
mock_db.list_collection_names.assert_called_once()
mock_collection.insert_one.assert_called_once_with({"_id": 0, "placeholder": True})
mock_collection.delete_one.assert_called_once_with({"_id": 0})
assert mongo_vector.index_name == "test_collection_vector_index"
mock_collection.list_search_indexes.assert_called_once_with(name="test_collection_vector_index")
mock_collection.create_search_index.assert_called_once()
args, _ = mock_collection.create_search_index.call_args
search_index_model = args[0].document
assert search_index_model == {
"name": "test_collection_vector_index",
"definition": {
"mappings": {
"dynamic": False,
"fields": {
"embedding": {
"type": "knnVector",
"d": 1536,
"similarity": "cosine",
}
}
}
}
}
assert mongo_vector.collection == mock_collection
def test_insert(mongo_vector_fixture):
mongo_vector, mock_collection, _ = mongo_vector_fixture
vectors = [[0.1] * 1536, [0.2] * 1536]
payloads = [{"name": "vector1"}, {"name": "vector2"}]
ids = ["id1", "id2"]
mongo_vector.insert(vectors, payloads, ids)
expected_records=[
({"_id": ids[0], "embedding": vectors[0], "payload": payloads[0]}),
({"_id": ids[1], "embedding": vectors[1], "payload": payloads[1]})
]
mock_collection.insert_many.assert_called_once_with(expected_records)
def test_search(mongo_vector_fixture):
mongo_vector, mock_collection, _ = mongo_vector_fixture
query_vector = [0.1] * 1536
mock_collection.aggregate.return_value = [
{"_id": "id1", "score": 0.9, "payload": {"key": "value1"}},
{"_id": "id2", "score": 0.8, "payload": {"key": "value2"}},
]
mock_collection.list_search_indexes.return_value = ["test_collection_vector_index"]
results = mongo_vector.search("query_str", query_vector, limit=2)
mock_collection.list_search_indexes.assert_called_with(name="test_collection_vector_index")
mock_collection.aggregate.assert_called_once_with([
{
"$vectorSearch": {
"index": "test_collection_vector_index",
"limit": 2,
"numCandidates": 2,
"queryVector": query_vector,
"path": "embedding",
},
},
{"$set": {"score": {"$meta": "vectorSearchScore"}}},
{"$project": {"embedding": 0}},
])
assert len(results) == 2
assert results[0].id == "id1"
assert results[0].score == 0.9
assert results[1].id == "id2"
assert results[1].score == 0.8
def test_delete(mongo_vector_fixture):
mongo_vector, mock_collection, _ = mongo_vector_fixture
mock_delete_result = MagicMock()
mock_delete_result.deleted_count = 1
mock_collection.delete_one.return_value = mock_delete_result
mongo_vector.delete("id1")
mock_collection.delete_one.assert_called_with({"_id": "id1"})
def test_update(mongo_vector_fixture):
mongo_vector, mock_collection, _ = mongo_vector_fixture
mock_update_result = MagicMock()
mock_update_result.matched_count = 1
mock_collection.update_one.return_value = mock_update_result
idValue = "id1"
vectorValue = [0.2] * 1536
payloadValue = {"key": "updated"}
mongo_vector.update(idValue, vector=vectorValue, payload=payloadValue)
mock_collection.update_one.assert_called_once_with(
{"_id": idValue},
{"$set": {"embedding": vectorValue, "payload": payloadValue}},
)
def test_get(mongo_vector_fixture):
mongo_vector, mock_collection, _ = mongo_vector_fixture
mock_collection.find_one.return_value = {"_id": "id1", "payload": {"key": "value1"}}
result = mongo_vector.get("id1")
assert result is not None
assert result.id == "id1"
assert result.payload == {"key": "value1"}
def test_list_cols(mongo_vector_fixture):
mongo_vector, _, mock_db = mongo_vector_fixture
mock_db.list_collection_names.return_value = ["col1", "col2"]
collections = mongo_vector.list_cols()
assert collections == ["col1", "col2"]
def test_delete_col(mongo_vector_fixture):
mongo_vector, mock_collection, _ = mongo_vector_fixture
mongo_vector.delete_col()
mock_collection.drop.assert_called_once()
def test_col_info(mongo_vector_fixture):
mongo_vector, _, mock_db = mongo_vector_fixture
mock_db.command.return_value = {"count": 10, "size": 1024}
info = mongo_vector.col_info()
mock_db.command.assert_called_once_with("collstats", "test_collection")
assert info["name"] == "test_collection"
assert info["count"] == 10
assert info["size"] == 1024
def test_list(mongo_vector_fixture):
mongo_vector, mock_collection, _ = mongo_vector_fixture
mock_cursor = MagicMock()
mock_cursor.limit.return_value = [
{"_id": "id1", "payload": {"key": "value1"}},
{"_id": "id2", "payload": {"key": "value2"}},
]
mock_collection.find.return_value = mock_cursor
query_filters = {"_id": {"$in": ["id1", "id2"]}}
results = mongo_vector.list(filters=query_filters, limit=2)
mock_collection.find.assert_called_once_with(query_filters)
mock_cursor.limit.assert_called_once_with(2)
assert len(results) == 2
assert results[0].id == "id1"
assert results[0].payload == {"key": "value1"}
assert results[1].id == "id2"
assert results[1].payload == {"key": "value2"}
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@mem0/vercel-ai-provider",
"version": "1.0.4",
"version": "1.0.6",
"description": "Vercel AI Provider for providing memory to LLMs",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
@@ -28,6 +28,7 @@
"dependencies": {
"@ai-sdk/anthropic": "1.1.12",
"@ai-sdk/cohere": "1.1.12",
"@ai-sdk/google": "1.2.18",
"@ai-sdk/groq": "1.1.11",
"@ai-sdk/openai": "1.1.15",
"@ai-sdk/provider": "1.0.9",
+36
View File
@@ -14,6 +14,9 @@ importers:
'@ai-sdk/cohere':
specifier: 1.1.12
version: 1.1.12(zod@3.24.2)
'@ai-sdk/google':
specifier: 1.2.18
version: 1.2.18(zod@3.24.2)
'@ai-sdk/groq':
specifier: 1.1.11
version: 1.1.11(zod@3.24.2)
@@ -84,6 +87,12 @@ packages:
peerDependencies:
zod: ^3.0.0
'@ai-sdk/google@1.2.18':
resolution: {integrity: sha512-8B70+i+uB12Ae6Sn6B9Oc6W0W/XorGgc88Nx0pyUrcxFOdytHBaAVhTPqYsO3LLClfjYN8pQ9GMxd5cpGEnUcA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
'@ai-sdk/groq@1.1.11':
resolution: {integrity: sha512-Y5WUyWuxkQarl4AVGeIMbNSp4/XiwW/mxp9SKeagfDhflVnQHd2ggISVD6HiOBQhznusITjWYYC66DJeBn0v6A==}
engines: {node: '>=18'}
@@ -105,10 +114,20 @@ packages:
zod:
optional: true
'@ai-sdk/provider-utils@2.2.8':
resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.23.8
'@ai-sdk/provider@1.0.9':
resolution: {integrity: sha512-jie6ZJT2ZR0uVOVCDc9R2xCX5I/Dum/wEK28lx21PJx6ZnFAN9EzD2WsPhcDWfCgGx3OAZZ0GyM3CEobXpa9LA==}
engines: {node: '>=18'}
'@ai-sdk/provider@1.1.3':
resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
engines: {node: '>=18'}
'@ai-sdk/react@1.1.18':
resolution: {integrity: sha512-2wlWug6NVAc8zh3pgqtvwPkSNTdA6Q4x9CmrNXCeHcXfJkJ+MuHFQz/I7Wb7mLRajf0DAxsFLIhHyBCEuTkDNw==}
engines: {node: '>=18'}
@@ -2751,6 +2770,12 @@ snapshots:
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.2)
zod: 3.24.2
'@ai-sdk/google@1.2.18(zod@3.24.2)':
dependencies:
'@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.8(zod@3.24.2)
zod: 3.24.2
'@ai-sdk/groq@1.1.11(zod@3.24.2)':
dependencies:
'@ai-sdk/provider': 1.0.9
@@ -2772,10 +2797,21 @@ snapshots:
optionalDependencies:
zod: 3.24.2
'@ai-sdk/provider-utils@2.2.8(zod@3.24.2)':
dependencies:
'@ai-sdk/provider': 1.1.3
nanoid: 3.3.11
secure-json-parse: 2.7.0
zod: 3.24.2
'@ai-sdk/provider@1.0.9':
dependencies:
json-schema: 0.4.0
'@ai-sdk/provider@1.1.3':
dependencies:
json-schema: 0.4.0
'@ai-sdk/react@1.1.18(react@19.1.0)(zod@3.24.2)':
dependencies:
'@ai-sdk/provider-utils': 2.1.10(zod@3.24.2)
+1
View File
@@ -28,6 +28,7 @@ export interface Mem0ConfigSettings {
rerank?: boolean;
enable_graph?: boolean;
output_format?: string;
filter_memories?: boolean;
}
export interface Mem0ChatConfig extends Mem0ConfigSettings, Mem0ProviderSettings {}