Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8004b93db | |||
| 4e414e4015 | |||
| 16455789d4 | |||
| 3ac4e047de | |||
| a2ffca3266 | |||
| 3a9fcdbec2 | |||
| 515f87b6bd | |||
| 5edc0cc99f | |||
| 7fff26f374 | |||
| 1aecfadf45 | |||
| 7e06aeeada | |||
| 2a59c9fd99 | |||
| 669ed184e4 | |||
| 3c2683c1b5 | |||
| f06e2d744d | |||
| f9e30304d7 | |||
| 7e3b727528 | |||
| 13c7f84eec | |||
| 924ac00c52 | |||
| 2a36960f4c | |||
| 2e0f91e70d | |||
| d1b4b304c7 | |||
| 2868bfe749 | |||
| 5431badfd4 | |||
| bda5b726bd | |||
| 16bcc91716 | |||
| ba63ea4528 | |||
| 5332741961 | |||
| d8a6960b4a | |||
| 316dc67a0a | |||
| 65156d5176 | |||
| c5e8216362 | |||
| ecedbc11d9 | |||
| 9aadfa3221 | |||
| cc45561abd | |||
| 7cebaba0a2 | |||
| 5dabf24809 | |||
| ec326f0f92 | |||
| eb780f4880 | |||
| c39d5ada4d | |||
| 06c25eb00b | |||
| 7a09663156 | |||
| 267bcf2931 | |||
| bf9a5703b1 | |||
| 884e740b53 | |||
| 824032a81d | |||
| abdb07c204 | |||
| 7b26df728d |
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"owner": {
|
||||
"name": "Mem0",
|
||||
"email": "support@mem0.ai"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Official Mem0 plugins for Claude"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": "./mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"owner": {
|
||||
"name": "Mem0",
|
||||
"email": "support@mem0.ai"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "Official Mem0 plugins for Cursor"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": "./mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,41 +1,55 @@
|
||||
name: 🐛 Bug Report
|
||||
description: Create a report to help us reproduce and fix the bug
|
||||
name: Bug Report
|
||||
description: Report a bug in mem0
|
||||
labels: ["bug"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
#### Before submitting a bug, please make sure the issue hasn't been already addressed by searching through [the existing and past issues](https://github.com/embedchain/embedchain/issues?q=is%3Aissue+sort%3Acreated-desc+).
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: 🐛 Describe the bug
|
||||
description: |
|
||||
Please provide a clear and concise description of what the bug is.
|
||||
- type: dropdown
|
||||
id: component
|
||||
attributes:
|
||||
label: Component
|
||||
description: Which part of mem0 is affected?
|
||||
options:
|
||||
- Core / Python SDK
|
||||
- TypeScript SDK
|
||||
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
|
||||
- Graph Memory (Neo4j, Memgraph, etc.)
|
||||
- Ollama / Local Models
|
||||
- OpenClaw
|
||||
- REST API
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
If relevant, add a minimal example so that we can reproduce the error by running the code. It is very important for the snippet to be as succinct (minimal) as possible, so please take time to trim down any irrelevant code to help us debug efficiently. We are going to copy-paste your code and we expect to get the same result as you did: avoid any external data, and include the relevant imports, etc. For example:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
value: |
|
||||
### Summary
|
||||
|
||||
```python
|
||||
# All necessary imports at the beginning
|
||||
import embedchain as ec
|
||||
# Your code goes here
|
||||
A clear summary of the bug.
|
||||
|
||||
### Steps to Reproduce
|
||||
|
||||
```
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
Please also paste or describe the results you observe instead of the expected results. If you observe an error, please paste the error message including the **full** traceback of the exception. It may be relevant to wrap error messages in ```` ```triple quotes blocks``` ````.
|
||||
placeholder: |
|
||||
A clear and concise description of what the bug is.
|
||||
m = Memory()
|
||||
# Your code here...
|
||||
```
|
||||
|
||||
```python
|
||||
Sample code to reproduce the problem
|
||||
```
|
||||
### Expected Behavior
|
||||
|
||||
```
|
||||
The error message you got, with the full traceback.
|
||||
````
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Thanks for contributing 🎉!
|
||||
What you expected to happen.
|
||||
|
||||
### Actual Behavior
|
||||
|
||||
What actually happened. Paste the full error traceback if applicable.
|
||||
|
||||
### Environment
|
||||
|
||||
- mem0 version:
|
||||
- Python/Node version:
|
||||
- OS:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: 1-on-1 Session
|
||||
url: https://cal.com/taranjeetio/ec
|
||||
about: Speak directly with Taranjeet, the founder, to discuss issues, share feedback, or explore improvements for Embedchain
|
||||
- name: Discord
|
||||
- name: Discord Community
|
||||
url: https://discord.gg/6PzXDgEjG5
|
||||
about: General community discussions
|
||||
about: Ask questions and discuss with the community
|
||||
- name: Documentation
|
||||
url: https://docs.mem0.ai
|
||||
about: Read the official mem0 documentation
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
name: Documentation
|
||||
description: Report an issue related to the Embedchain docs.
|
||||
title: "DOC: <Please write a comprehensive title after the 'DOC: ' prefix>"
|
||||
name: Documentation Issue
|
||||
description: Report an issue or suggest an improvement to the mem0 docs
|
||||
labels: ["documentation"]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: "Issue with current documentation:"
|
||||
description: >
|
||||
Please make sure to leave a reference to the document/code you're
|
||||
referring to.
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
value: |
|
||||
### Page
|
||||
|
||||
Link to the docs page: https://docs.mem0.ai/...
|
||||
|
||||
### What's Wrong or Missing
|
||||
|
||||
Describe what's incorrect, unclear, or missing.
|
||||
|
||||
### Suggested Fix
|
||||
|
||||
How should the docs be improved?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -1,23 +1,41 @@
|
||||
name: 🚀 Feature request
|
||||
description: Submit a proposal/request for a new Embedchain feature
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement for mem0
|
||||
labels: ["enhancement"]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature-request
|
||||
attributes:
|
||||
label: 🚀 The feature
|
||||
description: >
|
||||
A clear and concise description of the feature proposal
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Motivation, pitch
|
||||
description: >
|
||||
Please outline the motivation for the proposal. Is your feature request related to a specific problem? e.g., *"I'm working on X and would like Y to be possible"*. If this is related to another GitHub issue, please link here too.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: >
|
||||
Thanks for contributing 🎉!
|
||||
- type: dropdown
|
||||
id: component
|
||||
attributes:
|
||||
label: Component
|
||||
description: Which part of mem0 does this relate to?
|
||||
options:
|
||||
- Core / Python SDK
|
||||
- TypeScript SDK
|
||||
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
|
||||
- Graph Memory (Neo4j, Memgraph, etc.)
|
||||
- Ollama / Local Models
|
||||
- OpenClaw
|
||||
- REST API
|
||||
- Benchmarks / Evals
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
value: |
|
||||
### Use Case
|
||||
|
||||
What problem are you trying to solve?
|
||||
|
||||
### Proposed Solution
|
||||
|
||||
How should this work? Include API examples or pseudocode if helpful.
|
||||
|
||||
### Alternatives Considered
|
||||
|
||||
Any workarounds you've tried or other approaches considered.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -1,41 +1,38 @@
|
||||
## Linked Issue
|
||||
|
||||
Closes #<!-- issue number -->
|
||||
|
||||
## Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.
|
||||
<!-- What does this PR do? Why is it needed? -->
|
||||
|
||||
Fixes # (issue)
|
||||
## Type of Change
|
||||
|
||||
## Type of change
|
||||
|
||||
Please delete options that are not relevant.
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Refactor (does not change functionality, e.g. code style improvements, linting)
|
||||
- [ ] Bug fix (non-breaking change that fixes an issue)
|
||||
- [ ] New feature (non-breaking change that adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
- [ ] Refactor (no functional changes)
|
||||
- [ ] Documentation update
|
||||
|
||||
## How Has This Been Tested?
|
||||
## Breaking Changes
|
||||
|
||||
Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration
|
||||
<!-- If this is a breaking change, describe what breaks and the migration path. Delete this section if not applicable. -->
|
||||
|
||||
Please delete options that are not relevant.
|
||||
N/A
|
||||
|
||||
- [ ] Unit Test
|
||||
- [ ] Test Script (please provide)
|
||||
## Test Coverage
|
||||
|
||||
## Checklist:
|
||||
- [ ] I added/updated unit tests
|
||||
- [ ] I added/updated integration tests
|
||||
- [ ] I tested manually (describe below)
|
||||
- [ ] No tests needed (explain why)
|
||||
|
||||
- [ ] My code follows the style guidelines of this project
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
- [ ] Any dependent changes have been merged and published in downstream modules
|
||||
- [ ] I have checked my code and corrected any misspellings
|
||||
<!-- Describe how you tested this, or link to CI results. -->
|
||||
|
||||
## Maintainer Checklist
|
||||
## Checklist
|
||||
|
||||
- [ ] closes #xxxx (Replace xxxx with the GitHub issue number)
|
||||
- [ ] Made sure Checks passed
|
||||
- [ ] My code follows the project's style guidelines
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have added tests that prove my fix/feature works
|
||||
- [ ] New and existing tests pass locally
|
||||
- [ ] I have updated documentation if needed
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Maps dropdown selections to GitHub labels
|
||||
# Used by the advanced-issue-labeler GitHub Action
|
||||
|
||||
component:
|
||||
- label: "sdk-python"
|
||||
matcher: "Core / Python SDK"
|
||||
- label: "sdk-typescript"
|
||||
matcher: "TypeScript SDK"
|
||||
- label: "vector-store"
|
||||
matcher: "Vector Store"
|
||||
- label: "graph-memory"
|
||||
matcher: "Graph Memory"
|
||||
- label: "ollama"
|
||||
matcher: "Ollama"
|
||||
- label: "openclaw"
|
||||
matcher: "OpenClaw"
|
||||
- label: "rest-api"
|
||||
matcher: "REST API"
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Auto-label issues
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: stefanbuck/github-issue-parser@v3
|
||||
id: issue-parser
|
||||
with:
|
||||
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
|
||||
|
||||
- uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
|
||||
with:
|
||||
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
|
||||
section: component
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
config-path: .github/advanced-issue-labeler.yml
|
||||
|
||||
- uses: stefanbuck/github-issue-parser@v3
|
||||
id: feature-parser
|
||||
if: contains(github.event.issue.labels.*.name, 'enhancement')
|
||||
with:
|
||||
template-path: .github/ISSUE_TEMPLATE/feature_request.yml
|
||||
|
||||
- uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
|
||||
if: contains(github.event.issue.labels.*.name, 'enhancement')
|
||||
with:
|
||||
issue-form: ${{ steps.feature-parser.outputs.jsonString }}
|
||||
section: component
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
config-path: .github/advanced-issue-labeler.yml
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Close stale issues
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
# Issue settings
|
||||
days-before-issue-stale: 90
|
||||
days-before-issue-close: 14
|
||||
stale-issue-label: 'stale'
|
||||
stale-issue-message: >
|
||||
This issue has been automatically marked as stale because it has not
|
||||
had any activity in 90 days. It will be closed in 14 days if no
|
||||
further activity occurs. If this is still relevant, please leave a
|
||||
comment or remove the `stale` label.
|
||||
close-issue-message: >
|
||||
This issue has been closed due to inactivity. If this is still
|
||||
relevant, feel free to reopen it or create a new issue.
|
||||
|
||||
# PR settings — mark stale but never auto-close
|
||||
days-before-pr-stale: 90
|
||||
days-before-pr-close: -1
|
||||
stale-pr-label: 'stale'
|
||||
stale-pr-message: >
|
||||
This pull request has been automatically marked as stale because it
|
||||
has not had any activity in 90 days. Please update your branch and
|
||||
address any review comments, or it may be closed in the future.
|
||||
|
||||
# Exempt these labels from stale processing
|
||||
exempt-issue-labels: 'P0-critical,P1-high,good first issue,security'
|
||||
exempt-pr-labels: 'P0-critical,P1-high'
|
||||
|
||||
# Remove stale label when there is new activity
|
||||
remove-stale-when-updated: true
|
||||
|
||||
# Process up to 100 issues per run to stay within API limits
|
||||
operations-per-run: 100
|
||||
@@ -15,8 +15,6 @@
|
||||
<a href="https://mem0.dev/DiG">Join Discord</a>
|
||||
·
|
||||
<a href="https://mem0.dev/demo">Demo</a>
|
||||
·
|
||||
<a href="https://mem0.dev/openmemory">OpenMemory</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
|
||||
+59
-9
@@ -8,20 +8,56 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<Update label="2026-03-26" description="v1.0.8">
|
||||
|
||||
**New Features & Updates:**
|
||||
- **Vector Stores:** Integrated Turbopuffer as a vector database provider ([#4428](https://github.com/mem0ai/mem0/pull/4428))
|
||||
- **LLMs:** Added MiniMax LLM provider ([#4431](https://github.com/mem0ai/mem0/pull/4431))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Core:** Fixed merging of multiple filter operators for the same key ([#4559](https://github.com/mem0ai/mem0/pull/4559))
|
||||
- **Core:** Prevented in-place mutation of metadata in `_create_memory` ([#4529](https://github.com/mem0ai/mem0/pull/4529))
|
||||
- **Core:** Preserved custom metadata when updating memory ([#4495](https://github.com/mem0ai/mem0/pull/4495))
|
||||
- **Core:** Handled chatty LLM responses in JSON parsing ([#4525](https://github.com/mem0ai/mem0/pull/4525))
|
||||
- **Core:** Prevented double embedding in `mem0.add` ([#3996](https://github.com/mem0ai/mem0/pull/3996))
|
||||
- **Core:** Raised `ValueError` when deleting nonexistent memory ([#4455](https://github.com/mem0ai/mem0/pull/4455))
|
||||
- **Core:** Cleaned up graph store data on `Memory.delete()` ([#4505](https://github.com/mem0ai/mem0/pull/4505))
|
||||
- **Vector Stores:** Prevented SQL injection in Databricks vector store ([#4558](https://github.com/mem0ai/mem0/pull/4558))
|
||||
- **Vector Stores:** Upgraded MongoDB vector store from deprecated `knnVector` to GA `vectorSearch` ([#3995](https://github.com/mem0ai/mem0/pull/3995))
|
||||
- **Vector Stores:** Prevented embedding corruption in Valkey and Redis when vector is `None` ([#4362](https://github.com/mem0ai/mem0/pull/4362))
|
||||
- **Vector Stores:** Accepted default `/tmp/chroma` path in `ChromaDbConfig` validator ([#4179](https://github.com/mem0ai/mem0/pull/4179))
|
||||
- **Vector Stores:** Wrapped vector and payload in lists for `Langchain.update` ([#4446](https://github.com/mem0ai/mem0/pull/4446))
|
||||
- **Graph:** Soft-delete graph relationships instead of hard `DELETE` ([#4188](https://github.com/mem0ai/mem0/pull/4188))
|
||||
- **Graph:** Sanitized hyphens in Neo4j Cypher relationship names ([#4154](https://github.com/mem0ai/mem0/pull/4154))
|
||||
- **Graph:** Used root LLM config as fallback for graph store instead of hardcoded OpenAI default ([#4466](https://github.com/mem0ai/mem0/pull/4466))
|
||||
- **Qdrant:** Fixed `do not remove local path on init` ([#4475](https://github.com/mem0ai/mem0/pull/4475))
|
||||
- **Qdrant:** Implemented enhanced metadata filtering operators ([#4127](https://github.com/mem0ai/mem0/pull/4127))
|
||||
- **Embeddings:** Fixed OpenAI embedding dimensions ([#4481](https://github.com/mem0ai/mem0/pull/4481))
|
||||
- **LLMs:** Omitted `topP` for Anthropic Converse in Bedrock; used `AWSBedrockConfig` in `LlmFactory` ([#4469](https://github.com/mem0ai/mem0/pull/4469))
|
||||
- **LLMs:** Avoided sending both `temperature` and `top_p` to Anthropic API ([#4471](https://github.com/mem0ai/mem0/pull/4471))
|
||||
- **LLMs:** Handled `None` content and empty candidates in `GeminiLLM` parsing ([#4462](https://github.com/mem0ai/mem0/pull/4462))
|
||||
- **LLMs:** Added missing `_parse_response` to `AzureOpenAIStructuredLLM` ([#4434](https://github.com/mem0ai/mem0/pull/4434))
|
||||
- **History:** Added timestamps for `DELETE` operations in history ([#4492](https://github.com/mem0ai/mem0/pull/4492))
|
||||
|
||||
**Improvements:**
|
||||
- **Vector Stores:** Added vector validation to OpenSearchDB to ensure non-null, non-empty, and correct-dimension vectors ([#4472](https://github.com/mem0ai/mem0/pull/4472))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-03-19" description="v1.0.7">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Core:** Fixed control characters in LLM JSON responses causing parse failures (#4420)
|
||||
- **Core:** Replaced hardcoded US/Pacific timezone references with `timezone.utc` (#4404)
|
||||
- **Core:** Preserved `http_auth` in `_safe_deepcopy_config` for OpenSearch (#4418)
|
||||
- **Core:** Normalized malformed LLM fact output before embedding (#4224)
|
||||
- **Embeddings:** Pass `encoding_format='float'` in OpenAI embeddings for proxy compatibility (#4058)
|
||||
- **LLMs:** Fixed Ollama to pass tools to `client.chat` and parse `tool_calls` from response (#4176)
|
||||
- **Reranker:** Support nested LLM config in `LLMReranker` for non-OpenAI providers (#4405)
|
||||
- **Vector Stores:** Cast `vector_distance` to float in Redis search (#4377)
|
||||
- **Core:** Fixed control characters in LLM JSON responses causing parse failures ([#4420](https://github.com/mem0ai/mem0/pull/4420))
|
||||
- **Core:** Replaced hardcoded US/Pacific timezone references with `timezone.utc` ([#4404](https://github.com/mem0ai/mem0/pull/4404))
|
||||
- **Core:** Preserved `http_auth` in `_safe_deepcopy_config` for OpenSearch ([#4418](https://github.com/mem0ai/mem0/pull/4418))
|
||||
- **Core:** Normalized malformed LLM fact output before embedding ([#4224](https://github.com/mem0ai/mem0/pull/4224))
|
||||
- **Embeddings:** Pass `encoding_format='float'` in OpenAI embeddings for proxy compatibility ([#4058](https://github.com/mem0ai/mem0/pull/4058))
|
||||
- **LLMs:** Fixed Ollama to pass tools to `client.chat` and parse `tool_calls` from response ([#4176](https://github.com/mem0ai/mem0/pull/4176))
|
||||
- **Reranker:** Support nested LLM config in `LLMReranker` for non-OpenAI providers ([#4405](https://github.com/mem0ai/mem0/pull/4405))
|
||||
- **Vector Stores:** Cast `vector_distance` to float in Redis search ([#4377](https://github.com/mem0ai/mem0/pull/4377))
|
||||
|
||||
**Improvements:**
|
||||
- **Embeddings:** Improved Ollama embedder with model name normalization and error handling (#4403)
|
||||
- **Embeddings:** Improved Ollama embedder with model name normalization and error handling ([#4403](https://github.com/mem0ai/mem0/pull/4403))
|
||||
|
||||
</Update>
|
||||
|
||||
@@ -763,6 +799,20 @@ mode: "wide"
|
||||
|
||||
<Tab title="TypeScript">
|
||||
|
||||
<Update label="2026-03-26" description="v2.4.3">
|
||||
|
||||
**New Features & Updates:**
|
||||
- **OSS:** Added pgvector support to NodeJS OSS `VectorStoreFactory` ([#3997](https://github.com/mem0ai/mem0/pull/3997))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **OSS:** Made pgvector `pg` import compatible with ESM ([#4544](https://github.com/mem0ai/mem0/pull/4544))
|
||||
- **OSS:** Registered pgvector in `VectorStoreFactory` ([#4502](https://github.com/mem0ai/mem0/pull/4502))
|
||||
- **OSS:** Used root LLM config as fallback for graph store instead of hardcoded OpenAI default ([#4466](https://github.com/mem0ai/mem0/pull/4466))
|
||||
- **OSS:** Fixed `toCamelCase` in Redis `get` method for the payload ([#3172](https://github.com/mem0ai/mem0/pull/3172))
|
||||
- **Client:** Fixed Zod Schema incompatibility with OpenAI Structured Outputs API ([#3462](https://github.com/mem0ai/mem0/pull/3462))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-03-19" description="v2.4.2">
|
||||
|
||||
**Bug Fixes:**
|
||||
|
||||
@@ -7,58 +7,51 @@ When using LLM rerankers, you can customize the prompts used for ranking to bett
|
||||
|
||||
## Default Prompt
|
||||
|
||||
The default LLM reranker prompt is designed to be general-purpose:
|
||||
The default LLM reranker prompt scores each memory individually on a 0.0-1.0 scale:
|
||||
|
||||
```
|
||||
Given a query and a list of memory entries, rank the memory entries based on their relevance to the query.
|
||||
Rate each memory on a scale of 1-10 where 10 is most relevant.
|
||||
You are a relevance scoring assistant. Given a query and a document, you need to score how relevant the document is to the query.
|
||||
|
||||
Query: {query}
|
||||
Score the relevance on a scale from 0.0 to 1.0, where:
|
||||
- 1.0 = Perfectly relevant and directly answers the query
|
||||
- 0.8-0.9 = Highly relevant with good information
|
||||
- 0.6-0.7 = Moderately relevant with some useful information
|
||||
- 0.4-0.5 = Slightly relevant with limited useful information
|
||||
- 0.0-0.3 = Not relevant or no useful information
|
||||
|
||||
Memory entries:
|
||||
{memories}
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
|
||||
Provide your ranking as a JSON array with scores for each memory.
|
||||
Provide only a single numerical score between 0.0 and 1.0. Do not include any explanation or additional text.
|
||||
```
|
||||
|
||||
## Custom Prompt Configuration
|
||||
|
||||
You can provide a custom prompt template when configuring the LLM reranker:
|
||||
You can provide a custom prompt template using the `scoring_prompt` parameter:
|
||||
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
custom_prompt = """
|
||||
You are an expert at ranking memories for a personal AI assistant.
|
||||
Given a user query and a list of memory entries, rank each memory based on:
|
||||
1. Direct relevance to the query
|
||||
2. Temporal relevance (recent memories may be more important)
|
||||
3. Emotional significance
|
||||
4. Actionability
|
||||
You are an expert at evaluating memories for a personal AI assistant.
|
||||
Given a user query and a memory entry, score how relevant the memory is.
|
||||
Consider direct relevance, temporal relevance, and actionability.
|
||||
|
||||
Query: {query}
|
||||
User Context: {user_context}
|
||||
Query: "{query}"
|
||||
Memory: "{document}"
|
||||
|
||||
Memory entries:
|
||||
{memories}
|
||||
|
||||
Rate each memory from 1-10 and provide reasoning.
|
||||
Return as JSON: {{"rankings": [{{"index": 0, "score": 8, "reason": "..."}}]}}
|
||||
Provide only a single numerical score between 0.0 and 1.0.
|
||||
"""
|
||||
|
||||
config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4.1-nano-2025-04-14",
|
||||
"api_key": "your-openai-key"
|
||||
}
|
||||
},
|
||||
"custom_prompt": custom_prompt,
|
||||
"top_n": 5
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-openai-key",
|
||||
"scoring_prompt": custom_prompt,
|
||||
"top_k": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,12 +63,14 @@ memory = Memory.from_config(config)
|
||||
|
||||
Your custom prompt can use the following variables:
|
||||
|
||||
| Variable | Description |
|
||||
| ---------------- | ------------------------------------- |
|
||||
| `{query}` | The search query |
|
||||
| `{memories}` | The list of memory entries to rank |
|
||||
| `{user_id}` | The user ID (if available) |
|
||||
| `{user_context}` | Additional user context (if provided) |
|
||||
| Variable | Description |
|
||||
| ------------ | ----------------------------- |
|
||||
| `{query}` | The search query |
|
||||
| `{document}` | The memory entry being scored |
|
||||
|
||||
<Note>
|
||||
Both `{query}` and `{document}` are required in your custom prompt. The LLM reranker scores each memory individually against the query, so the prompt is called once per candidate memory.
|
||||
</Note>
|
||||
|
||||
## Domain-Specific Examples
|
||||
|
||||
@@ -89,13 +84,10 @@ Prioritize memories that:
|
||||
- Show previous resolution patterns
|
||||
- Indicate customer preferences or constraints
|
||||
|
||||
Query: {query}
|
||||
Customer Context: Previous interactions with this customer
|
||||
Query: "{query}"
|
||||
Memory: "{document}"
|
||||
|
||||
Memories:
|
||||
{memories}
|
||||
|
||||
Rank each memory 1-10 based on support relevance.
|
||||
Score relevance from 0.0 to 1.0.
|
||||
"""
|
||||
```
|
||||
|
||||
@@ -103,19 +95,16 @@ Rank each memory 1-10 based on support relevance.
|
||||
|
||||
```python
|
||||
educational_prompt = """
|
||||
Rank these learning memories for a student query.
|
||||
Score this learning memory for relevance to a student query.
|
||||
Consider:
|
||||
- Prerequisite knowledge requirements
|
||||
- Learning progression and difficulty
|
||||
- Relevance to current learning objectives
|
||||
|
||||
Student Query: {query}
|
||||
Learning Context: {user_context}
|
||||
Student Query: "{query}"
|
||||
Memory: "{document}"
|
||||
|
||||
Available memories:
|
||||
{memories}
|
||||
|
||||
Score each memory for educational value (1-10).
|
||||
Score educational relevance from 0.0 to 1.0.
|
||||
"""
|
||||
```
|
||||
|
||||
@@ -123,73 +112,64 @@ Score each memory for educational value (1-10).
|
||||
|
||||
```python
|
||||
personal_assistant_prompt = """
|
||||
Rank personal memories for relevance to the user's query.
|
||||
Score this personal memory for relevance to the user's query.
|
||||
Consider:
|
||||
- Recent vs. historical importance
|
||||
- Personal preferences and habits
|
||||
- Contextual relationships between memories
|
||||
- Contextual relationships
|
||||
|
||||
Query: {query}
|
||||
Personal context: {user_context}
|
||||
Query: "{query}"
|
||||
Memory: "{document}"
|
||||
|
||||
Memories to rank:
|
||||
{memories}
|
||||
|
||||
Provide relevance scores (1-10) with brief explanations.
|
||||
Provide relevance score from 0.0 to 1.0.
|
||||
"""
|
||||
```
|
||||
|
||||
## Advanced Prompt Techniques
|
||||
|
||||
### Multi-Criteria Ranking
|
||||
### Multi-Criteria Scoring
|
||||
|
||||
```python
|
||||
multi_criteria_prompt = """
|
||||
Evaluate memories using multiple criteria:
|
||||
Evaluate this memory using multiple criteria:
|
||||
|
||||
1. RELEVANCE (40%): How directly related to the query
|
||||
2. RECENCY (20%): How recent the memory is
|
||||
2. RECENCY (20%): How recent the memory appears to be
|
||||
3. IMPORTANCE (25%): Personal or business significance
|
||||
4. ACTIONABILITY (15%): How useful for next steps
|
||||
|
||||
Query: {query}
|
||||
Context: {user_context}
|
||||
Query: "{query}"
|
||||
Memory: "{document}"
|
||||
|
||||
Memories:
|
||||
{memories}
|
||||
|
||||
For each memory, provide:
|
||||
- Overall score (1-10)
|
||||
- Breakdown by criteria
|
||||
- Final ranking recommendation
|
||||
|
||||
Format: JSON with detailed scoring
|
||||
Compute a weighted score from 0.0 to 1.0 based on these criteria.
|
||||
Provide only the final numerical score.
|
||||
"""
|
||||
```
|
||||
|
||||
### Contextual Ranking
|
||||
### Chain-of-Thought Scoring
|
||||
|
||||
```python
|
||||
contextual_prompt = """
|
||||
Consider the following context when ranking memories:
|
||||
- Current user situation: {user_context}
|
||||
- Time of day: {current_time}
|
||||
- Recent activities: {recent_activities}
|
||||
reasoning_prompt = """
|
||||
Evaluate this memory's relevance step by step:
|
||||
|
||||
Query: {query}
|
||||
1. What is the main intent of the query?
|
||||
2. What key information does the memory contain?
|
||||
3. How directly does the memory address the query?
|
||||
|
||||
Rank these memories considering both direct relevance and contextual appropriateness:
|
||||
{memories}
|
||||
Based on this analysis, provide a single relevance score from 0.0 to 1.0.
|
||||
|
||||
Provide contextually-aware relevance scores (1-10).
|
||||
Query: "{query}"
|
||||
Memory: "{document}"
|
||||
|
||||
Score:
|
||||
"""
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Be Specific**: Clearly define what makes a memory relevant for your use case
|
||||
2. **Use Examples**: Include examples in your prompt for better model understanding
|
||||
3. **Structure Output**: Specify the exact JSON format you want returned
|
||||
2. **Use 0.0-1.0 Scale**: The score extractor expects values between 0.0 and 1.0
|
||||
3. **Request Only the Score**: Ask for just the numerical score to improve extraction reliability
|
||||
4. **Test Iteratively**: Refine your prompt based on actual ranking performance
|
||||
5. **Consider Token Limits**: Keep prompts concise while being comprehensive
|
||||
|
||||
@@ -206,7 +186,7 @@ prompts = [
|
||||
]
|
||||
|
||||
for i, prompt in enumerate(prompts):
|
||||
config["reranker"]["config"]["custom_prompt"] = prompt
|
||||
config["reranker"]["config"]["scoring_prompt"] = prompt
|
||||
memory = Memory.from_config(config)
|
||||
|
||||
results = memory.search("test query", user_id="test_user")
|
||||
@@ -216,6 +196,6 @@ for i, prompt in enumerate(prompts):
|
||||
## Common Issues
|
||||
|
||||
- **Too Long**: Keep prompts under token limits for your chosen LLM
|
||||
- **Too Vague**: Be specific about ranking criteria
|
||||
- **Inconsistent Format**: Ensure JSON output format is clearly specified
|
||||
- **Missing Context**: Include relevant variables for your use case
|
||||
- **Too Vague**: Be specific about scoring criteria
|
||||
- **Wrong Scale**: Use 0.0-1.0 scale to match the default score extractor
|
||||
- **Extra Output**: Ask for only the numeric score — extra text can confuse score extraction
|
||||
|
||||
@@ -18,13 +18,9 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-openai-api-key"
|
||||
}
|
||||
}
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-openai-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,11 +32,14 @@ m = Memory.from_config(config)
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `llm` | dict | Required | LLM configuration object |
|
||||
| `top_k` | int | 10 | Number of results to rerank |
|
||||
| `provider` | str | `"openai"` | LLM provider (openai, anthropic, etc.) |
|
||||
| `model` | str | `"gpt-4o-mini"` | LLM model to use for reranking |
|
||||
| `api_key` | str | None | API key for the LLM provider |
|
||||
| `top_k` | int | None | Number of top documents to return after reranking |
|
||||
| `temperature` | float | 0.0 | LLM temperature for consistency |
|
||||
| `custom_prompt` | str | None | Custom reranking prompt |
|
||||
| `score_range` | tuple | (0, 10) | Score range for relevance |
|
||||
| `max_tokens` | int | 100 | Maximum tokens for LLM response |
|
||||
| `scoring_prompt` | str | None | Custom prompt template for scoring documents |
|
||||
| `llm` | dict | None | Optional nested LLM config for provider-specific fields (e.g., `ollama_base_url`, `azure_endpoint`). Overrides top-level `provider`/`model`/`api_key` when provided. |
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
@@ -49,20 +48,19 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"llm": {
|
||||
"provider": "anthropic",
|
||||
"config": {
|
||||
"model": "claude-3-sonnet-20240229",
|
||||
"api_key": "your-anthropic-api-key"
|
||||
}
|
||||
},
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"api_key": "your-anthropic-api-key",
|
||||
"top_k": 15,
|
||||
"temperature": 0.0,
|
||||
"score_range": (1, 5),
|
||||
"custom_prompt": """
|
||||
Rate the relevance of each memory to the query on a scale of 1-5.
|
||||
"scoring_prompt": """
|
||||
Rate the relevance of each memory to the query on a scale of 0.0-1.0.
|
||||
Consider semantic similarity, context, and practical utility.
|
||||
Only provide the numeric score.
|
||||
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
Score:
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -78,14 +76,10 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-openai-api-key",
|
||||
"temperature": 0.0
|
||||
}
|
||||
}
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-openai-api-key",
|
||||
"temperature": 0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,13 +92,9 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"llm": {
|
||||
"provider": "anthropic",
|
||||
"config": {
|
||||
"model": "claude-3-sonnet-20240229",
|
||||
"api_key": "your-anthropic-api-key"
|
||||
}
|
||||
}
|
||||
"provider": "anthropic",
|
||||
"model": "claude-sonnet-4-20250514",
|
||||
"api_key": "your-anthropic-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,10 +107,12 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "ollama",
|
||||
"model": "llama3.2",
|
||||
"llm": {
|
||||
"provider": "ollama",
|
||||
"config": {
|
||||
"model": "llama2",
|
||||
"model": "llama3.2",
|
||||
"ollama_base_url": "http://localhost:11434"
|
||||
}
|
||||
}
|
||||
@@ -129,6 +121,10 @@ config = {
|
||||
}
|
||||
```
|
||||
|
||||
<Note>
|
||||
For providers like Ollama that need extra fields (e.g., `ollama_base_url`), use the optional nested `llm` key to pass provider-specific configuration. The nested `llm` config overrides top-level `provider`/`model`/`api_key` when provided.
|
||||
</Note>
|
||||
|
||||
### Azure OpenAI
|
||||
|
||||
```python
|
||||
@@ -136,13 +132,16 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "azure_openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-azure-api-key",
|
||||
"llm": {
|
||||
"provider": "azure_openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-azure-api-key",
|
||||
"azure_endpoint": "https://your-resource.openai.azure.com/",
|
||||
"azure_deployment": "gpt-4-deployment"
|
||||
"azure_deployment": "gpt-4o-mini-deployment"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,16 +153,22 @@ config = {
|
||||
|
||||
### Default Prompt Behavior
|
||||
|
||||
The default prompt asks the LLM to score relevance on a 0-10 scale:
|
||||
The default prompt asks the LLM to score relevance on a 0.0-1.0 scale:
|
||||
|
||||
```
|
||||
Given a query and a memory, rate how relevant the memory is to answering the query.
|
||||
Score from 0 (completely irrelevant) to 10 (perfectly relevant).
|
||||
Only provide the numeric score.
|
||||
You are a relevance scoring assistant. Given a query and a document, you need to score how relevant the document is to the query.
|
||||
|
||||
Query: {query}
|
||||
Memory: {memory}
|
||||
Score:
|
||||
Score the relevance on a scale from 0.0 to 1.0, where:
|
||||
- 1.0 = Perfectly relevant and directly answers the query
|
||||
- 0.8-0.9 = Highly relevant with good information
|
||||
- 0.6-0.7 = Moderately relevant with some useful information
|
||||
- 0.4-0.5 = Slightly relevant with limited useful information
|
||||
- 0.0-0.3 = Not relevant or no useful information
|
||||
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
|
||||
Provide only a single numerical score between 0.0 and 1.0. Do not include any explanation or additional text.
|
||||
```
|
||||
|
||||
### Custom Prompt Examples
|
||||
@@ -174,14 +179,14 @@ Score:
|
||||
custom_prompt = """
|
||||
You are a medical information specialist. Rate how relevant each memory is for answering the medical query.
|
||||
Consider clinical accuracy, specificity, and practical applicability.
|
||||
Rate from 1-10 where:
|
||||
- 1-3: Irrelevant or potentially harmful
|
||||
- 4-6: Somewhat relevant but incomplete
|
||||
- 7-8: Relevant and helpful
|
||||
- 9-10: Highly relevant and clinically useful
|
||||
Rate from 0.0 to 1.0 where:
|
||||
- 0.0-0.3: Irrelevant or potentially harmful
|
||||
- 0.4-0.6: Somewhat relevant but incomplete
|
||||
- 0.7-0.8: Relevant and helpful
|
||||
- 0.9-1.0: Highly relevant and clinically useful
|
||||
|
||||
Query: {query}
|
||||
Memory: {memory}
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
Score:
|
||||
"""
|
||||
|
||||
@@ -189,14 +194,10 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-api-key"
|
||||
}
|
||||
},
|
||||
"custom_prompt": custom_prompt
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-api-key",
|
||||
"scoring_prompt": custom_prompt
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,15 +214,15 @@ Consider:
|
||||
- Recency and accuracy
|
||||
- Practical usefulness
|
||||
|
||||
Rate 1-5:
|
||||
1 = Not relevant
|
||||
2 = Slightly relevant
|
||||
3 = Moderately relevant
|
||||
4 = Very relevant
|
||||
5 = Perfectly answers the question
|
||||
Rate 0.0-1.0:
|
||||
0.0 = Not relevant
|
||||
0.25 = Slightly relevant
|
||||
0.5 = Moderately relevant
|
||||
0.75 = Very relevant
|
||||
1.0 = Perfectly answers the question
|
||||
|
||||
Query: {query}
|
||||
Memory: {memory}
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
Score:
|
||||
"""
|
||||
```
|
||||
@@ -239,13 +240,17 @@ Consider:
|
||||
- Factual accuracy
|
||||
- Conversation flow
|
||||
|
||||
Rate 0-10:
|
||||
Query: {query}
|
||||
Memory: {memory}
|
||||
Rate 0.0-1.0:
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
Score:
|
||||
"""
|
||||
```
|
||||
|
||||
<Note>
|
||||
Custom prompts must include `{query}` and `{document}` placeholders. The LLM response should contain a numerical score which is automatically extracted.
|
||||
</Note>
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
@@ -295,9 +300,9 @@ results = safe_llm_rerank_search("What are my preferences?", "alice")
|
||||
|
||||
| Model Type | Speed | Quality | Cost | Best For |
|
||||
|------------|-------|---------|------|----------|
|
||||
| GPT-3.5 Turbo | Fast | Good | Low | High-volume applications |
|
||||
| GPT-4 | Medium | Excellent | Medium | Quality-critical applications |
|
||||
| Claude 3 Sonnet | Medium | Excellent | Medium | Balanced performance |
|
||||
| GPT-4o mini | Fast | Good | Low | High-volume applications |
|
||||
| GPT-4o | Medium | Excellent | Medium | Quality-critical applications |
|
||||
| Claude Sonnet | Medium | Excellent | Medium | Balanced performance |
|
||||
| Ollama Local | Variable | Good | Free | Privacy-sensitive applications |
|
||||
|
||||
### Optimization Strategies
|
||||
@@ -308,14 +313,10 @@ fast_config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-3.5-turbo",
|
||||
"api_key": "your-api-key"
|
||||
}
|
||||
},
|
||||
"top_k": 5, # Limit candidates
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-api-key",
|
||||
"top_k": 5,
|
||||
"temperature": 0.0
|
||||
}
|
||||
}
|
||||
@@ -326,13 +327,9 @@ quality_config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-api-key"
|
||||
}
|
||||
},
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o",
|
||||
"api_key": "your-api-key",
|
||||
"top_k": 15,
|
||||
"temperature": 0.0
|
||||
}
|
||||
@@ -353,10 +350,10 @@ Evaluate this memory's relevance using multi-step reasoning:
|
||||
3. How directly does the memory address the query?
|
||||
4. What additional context might be needed?
|
||||
|
||||
Based on this analysis, rate relevance 1-10:
|
||||
Based on this analysis, rate relevance 0.0-1.0:
|
||||
|
||||
Query: {query}
|
||||
Memory: {memory}
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
|
||||
Analysis:
|
||||
Step 1 (Intent):
|
||||
@@ -367,42 +364,6 @@ Final Score:
|
||||
"""
|
||||
```
|
||||
|
||||
### Comparative Ranking
|
||||
|
||||
```python
|
||||
comparative_prompt = """
|
||||
You will see a query and multiple memories. Rank them in order of relevance.
|
||||
Consider which memories best answer the question and would be most helpful.
|
||||
|
||||
Query: {query}
|
||||
|
||||
Memories to rank:
|
||||
{memories}
|
||||
|
||||
Provide scores 1-10 for each memory, considering their relative usefulness.
|
||||
"""
|
||||
```
|
||||
|
||||
### Emotional Intelligence
|
||||
|
||||
```python
|
||||
emotional_prompt = """
|
||||
Consider both factual relevance and emotional appropriateness.
|
||||
Rate how suitable this memory is for responding to the user's query.
|
||||
|
||||
Factors to consider:
|
||||
- Factual accuracy and relevance
|
||||
- Emotional tone and sensitivity
|
||||
- User's likely emotional state
|
||||
- Appropriateness of response
|
||||
|
||||
Query: {query}
|
||||
Memory: {memory}
|
||||
Emotional Context: {context}
|
||||
Score (1-10):
|
||||
"""
|
||||
```
|
||||
|
||||
## Error Handling and Fallbacks
|
||||
|
||||
```python
|
||||
@@ -433,14 +394,22 @@ class RobustLLMReranker:
|
||||
primary_config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {"llm": {"provider": "openai", "config": {"model": "gpt-4"}}}
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o",
|
||||
"api_key": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fallback_config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {"llm": {"provider": "openai", "config": {"model": "gpt-3.5-turbo"}}}
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,4 +455,4 @@ results = reranker.search("What are my preferences?", "alice")
|
||||
<Card title="Performance Optimization" icon="bolt" href="/components/rerankers/optimization">
|
||||
Optimize LLM reranker performance
|
||||
</Card>
|
||||
</CardGroup>
|
||||
</CardGroup>
|
||||
|
||||
@@ -17,8 +17,10 @@ config = {
|
||||
"workspace_url": "https://your-workspace.databricks.com",
|
||||
"access_token": "your-access-token",
|
||||
"endpoint_name": "your-vector-search-endpoint",
|
||||
"index_name": "catalog.schema.index_name",
|
||||
"source_table_name": "catalog.schema.source_table",
|
||||
"catalog": "your_catalog",
|
||||
"schema": "your_schema",
|
||||
"table_name": "your_table",
|
||||
"collection_name": "your_index_name",
|
||||
"embedding_dimension": 1536
|
||||
}
|
||||
}
|
||||
@@ -42,17 +44,22 @@ Here are the parameters available for configuring Databricks Vector Search:
|
||||
| --- | --- | --- |
|
||||
| `workspace_url` | The URL of your Databricks workspace | **Required** |
|
||||
| `access_token` | Personal Access Token for authentication | `None` |
|
||||
| `service_principal_client_id` | Service principal client ID (alternative to access_token) | `None` |
|
||||
| `service_principal_client_secret` | Service principal client secret (required with client_id) | `None` |
|
||||
| `client_id` | Service principal client ID (alternative to access_token) | `None` |
|
||||
| `client_secret` | Service principal client secret (required with client_id) | `None` |
|
||||
| `azure_client_id` | Azure AD application client ID (for Azure Databricks) | `None` |
|
||||
| `azure_client_secret` | Azure AD application client secret (for Azure Databricks) | `None` |
|
||||
| `endpoint_name` | Name of the Vector Search endpoint | **Required** |
|
||||
| `index_name` | Name of the vector index (Unity Catalog format: catalog.schema.index) | **Required** |
|
||||
| `source_table_name` | Name of the source Delta table (Unity Catalog format: catalog.schema.table) | **Required** |
|
||||
| `embedding_dimension` | Dimension of self-managed embeddings | `1536` |
|
||||
| `embedding_source_column` | Column name for text when using Databricks-computed embeddings | `None` |
|
||||
| `catalog` | Unity Catalog catalog name | **Required** |
|
||||
| `schema` | Unity Catalog schema name | **Required** |
|
||||
| `table_name` | Source Delta table name | **Required** |
|
||||
| `collection_name` | Vector search index name | `mem0` |
|
||||
| `index_type` | Index type: `DELTA_SYNC` or `DIRECT_ACCESS` | `DELTA_SYNC` |
|
||||
| `embedding_model_endpoint_name` | Databricks serving endpoint for embeddings | `None` |
|
||||
| `embedding_vector_column` | Column name for self-managed embedding vectors | `embedding` |
|
||||
| `embedding_dimension` | Dimension of self-managed embeddings | `1536` |
|
||||
| `endpoint_type` | Type of endpoint (`STANDARD` or `STORAGE_OPTIMIZED`) | `STANDARD` |
|
||||
| `sync_computed_embeddings` | Whether to sync computed embeddings automatically | `True` |
|
||||
| `pipeline_type` | Sync pipeline type: `TRIGGERED` or `CONTINUOUS` | `TRIGGERED` |
|
||||
| `warehouse_name` | Databricks SQL warehouse name (if using SQL warehouse) | `None` |
|
||||
| `query_type` | Query type: `ANN` or `HYBRID` | `ANN` |
|
||||
|
||||
### Authentication
|
||||
|
||||
@@ -65,11 +72,13 @@ config = {
|
||||
"provider": "databricks",
|
||||
"config": {
|
||||
"workspace_url": "https://your-workspace.databricks.com",
|
||||
"service_principal_client_id": "your-service-principal-id",
|
||||
"service_principal_client_secret": "your-service-principal-secret",
|
||||
"client_id": "your-service-principal-id",
|
||||
"client_secret": "your-service-principal-secret",
|
||||
"endpoint_name": "your-endpoint",
|
||||
"index_name": "catalog.schema.index_name",
|
||||
"source_table_name": "catalog.schema.source_table"
|
||||
"catalog": "your_catalog",
|
||||
"schema": "your_schema",
|
||||
"table_name": "your_table",
|
||||
"collection_name": "your_index_name",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,8 +93,10 @@ config = {
|
||||
"workspace_url": "https://your-workspace.databricks.com",
|
||||
"access_token": "your-personal-access-token",
|
||||
"endpoint_name": "your-endpoint",
|
||||
"index_name": "catalog.schema.index_name",
|
||||
"source_table_name": "catalog.schema.source_table"
|
||||
"catalog": "your_catalog",
|
||||
"schema": "your_schema",
|
||||
"table_name": "your_table",
|
||||
"collection_name": "your_index_name",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +114,6 @@ config = {
|
||||
"config": {
|
||||
# ... authentication config ...
|
||||
"embedding_dimension": 768, # Match your embedding model
|
||||
"embedding_vector_column": "embedding"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -118,7 +128,6 @@ config = {
|
||||
"provider": "databricks",
|
||||
"config": {
|
||||
# ... authentication config ...
|
||||
"embedding_source_column": "text",
|
||||
"embedding_model_endpoint_name": "e5-small-v2"
|
||||
}
|
||||
}
|
||||
@@ -127,8 +136,8 @@ config = {
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Delta Sync Index**: This implementation uses Delta Sync Index, which automatically syncs with your source Delta table. Direct vector insertion/deletion/update operations will log warnings as they're not supported with Delta Sync.
|
||||
- **Unity Catalog**: Both the source table and index must be in Unity Catalog format (`catalog.schema.table_name`).
|
||||
- **Index Types**: This implementation supports both `DELTA_SYNC` (auto-syncs with source Delta table) and `DIRECT_ACCESS` (manage vectors directly) index types.
|
||||
- **Unity Catalog**: The source table and index are created under the specified `catalog.schema` namespace.
|
||||
- **Endpoint Auto-Creation**: If the specified endpoint doesn't exist, it will be created automatically.
|
||||
- **Index Auto-Creation**: If the specified index doesn't exist, it will be created automatically with the provided configuration.
|
||||
- **Filter Support**: Supports filtering by metadata fields, with different syntax for STANDARD vs STORAGE_OPTIMIZED endpoints.
|
||||
|
||||
@@ -48,7 +48,7 @@ const config = {
|
||||
password: '123',
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
dbname: 'vector_store', // Optional, defaults to 'postgres'
|
||||
dbname: 'vector_store', // Optional; TypeScript OSS defaults to `vector_store` when omitted
|
||||
diskann: false, // Optional, requires pgvectorscale extension
|
||||
hnsw: false, // Optional, for HNSW indexing
|
||||
},
|
||||
@@ -85,6 +85,8 @@ Here are the parameters available for configuring pgvector:
|
||||
| `connection_string` | PostgreSQL connection string (overrides individual connection parameters) | `None` |
|
||||
| `connection_pool` | psycopg2 connection pool object (overrides connection string and individual parameters) | `None` |
|
||||
|
||||
**Note (TypeScript OSS):** If you omit `dbname`, the TypeScript client uses the database name `vector_store`. Python defaults to `postgres` for `dbname`, as in the table above.
|
||||
|
||||
**Note**: The connection parameters have the following priority:
|
||||
1. `connection_pool` (highest priority)
|
||||
2. `connection_string`
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: "Turbopuffer"
|
||||
description: "Use Turbopuffer as a serverless vector database in Mem0 for low-latency search at scale with native metadata filtering."
|
||||
---
|
||||
[Turbopuffer](https://turbopuffer.com) is a serverless vector database optimized for low-latency search at scale. It offers cost-effective vector storage with native metadata filtering.
|
||||
|
||||
### Usage
|
||||
|
||||
```python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "sk-xx"
|
||||
os.environ["TURBOPUFFER_API_KEY"] = "tpuf_xxxxxxxxxxxx"
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "turbopuffer",
|
||||
"config": {
|
||||
"collection_name": "movie_preferences",
|
||||
"embedding_model_dims": 1536,
|
||||
"region": "gcp-us-central1",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m = Memory.from_config(config)
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"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."},
|
||||
{"role": "assistant", "content": "Got it! I'll suggest sci-fi movies instead."}
|
||||
]
|
||||
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
|
||||
# Search memories
|
||||
results = m.search(query="sci-fi recommendations", user_id="alice")
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
Here are the parameters available for configuring Turbopuffer:
|
||||
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `collection_name` | Name of the namespace/collection | `mem0` |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model (must match your chosen embedding model) | `1536` |
|
||||
| `api_key` | Turbopuffer API key | Environment variable: `TURBOPUFFER_API_KEY` |
|
||||
| `region` | Turbopuffer region | `gcp-us-central1` |
|
||||
| `distance_metric` | Distance metric for vector similarity (`cosine_distance` or `euclidean_squared`) | `cosine_distance` |
|
||||
| `batch_size` | Batch size for bulk operations | `100` |
|
||||
| `extra_params` | Additional parameters for the Turbopuffer client | `None` |
|
||||
|
||||
### Regions
|
||||
|
||||
| Region | Location |
|
||||
| --- | --- |
|
||||
| `gcp-us-central1` | Iowa, USA (Default) |
|
||||
| `aws-us-west-2` | Oregon, USA |
|
||||
|
||||
### Config Example
|
||||
|
||||
```python
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "turbopuffer",
|
||||
"config": {
|
||||
"collection_name": "my_memories",
|
||||
"embedding_model_dims": 1536,
|
||||
"api_key": "tpuf_xxxxxxxxxxxx",
|
||||
"region": "aws-us-west-2",
|
||||
"distance_metric": "cosine_distance",
|
||||
"batch_size": 200,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -33,6 +33,7 @@ See the list of supported vector databases below.
|
||||
<Card title="LangChain" href="/components/vectordbs/dbs/langchain"></Card>
|
||||
<Card title="Amazon S3 Vectors" href="/components/vectordbs/dbs/s3_vectors"></Card>
|
||||
<Card title="Databricks" href="/components/vectordbs/dbs/databricks"></Card>
|
||||
<Card title="Turbopuffer" href="/components/vectordbs/dbs/turbopuffer"></Card>
|
||||
</CardGroup>
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -3,7 +3,7 @@ title: "Gemini 3 with Mem0 MCP"
|
||||
description: "Create snappy, smart, memory-aware agents by pairing Gemini 3 with Mem0 MCP server."
|
||||
---
|
||||
|
||||
Gemini 3, when paired with mem0-mcp-server, works in synergy to create snappy, smart, memory-aware agents.
|
||||
Gemini 3, when paired with Mem0's cloud MCP server, works in synergy to create snappy, smart, memory-aware agents.
|
||||
|
||||
<Callout type="info" icon="sparkles" color="#8B5CF6">
|
||||
This is the primary example of MCP integration - the same patterns work with Claude Desktop, Cursor, or any MCP-compatible client.
|
||||
@@ -27,10 +27,22 @@ The Mem0 MCP server provides these tools to Gemini:
|
||||
|
||||
## Setup
|
||||
|
||||
### Configure Mem0 MCP
|
||||
|
||||
Add Mem0 MCP to your MCP client:
|
||||
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "claude,claude code,cursor,windsurf,vscode,opencode"
|
||||
```
|
||||
|
||||
### Install dependencies
|
||||
|
||||
```bash
|
||||
pip install pydantic-ai nest-asyncio python-dotenv uv google-genai
|
||||
pip install pydantic-ai nest-asyncio python-dotenv google-genai
|
||||
```
|
||||
|
||||
### Environment Setup
|
||||
@@ -40,7 +52,6 @@ Create a file named `.env`:
|
||||
```bash
|
||||
MEM0_API_KEY=m0-xxxxxxxxxxxxxxxxx
|
||||
GEMINI_API_KEY=your-gemini-api-key-here
|
||||
MEM0_DEFAULT_USER_ID=demo-user
|
||||
```
|
||||
|
||||
<Note>
|
||||
@@ -60,7 +71,7 @@ import asyncio
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from pydantic_ai import Agent
|
||||
from pydantic_ai.mcp import MCPServerStdio
|
||||
from pydantic_ai.mcp import MCPServerHTTP
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
@@ -75,11 +86,9 @@ class MemoryAgent:
|
||||
|
||||
def _setup(self):
|
||||
"""Initialize the agent with MCP tools"""
|
||||
# Create MCP server directly
|
||||
self.server = MCPServerStdio(
|
||||
command="uvx",
|
||||
args=["mem0-mcp-server"],
|
||||
env=os.environ
|
||||
# Connect to Mem0's cloud MCP server
|
||||
self.server = MCPServerHTTP(
|
||||
url="https://mcp.mem0.ai/mcp"
|
||||
)
|
||||
|
||||
# Create agent with Gemini and memory tools
|
||||
|
||||
+16
-15
@@ -40,6 +40,7 @@
|
||||
"icon": "rocket",
|
||||
"pages": [
|
||||
"platform/overview",
|
||||
"vibecoding",
|
||||
"platform/mem0-mcp",
|
||||
"platform/platform-vs-oss",
|
||||
"platform/quickstart"
|
||||
@@ -142,6 +143,7 @@
|
||||
"icon": "rocket",
|
||||
"pages": [
|
||||
"open-source/overview",
|
||||
"vibecoding",
|
||||
"open-source/python-quickstart",
|
||||
"open-source/node-quickstart"
|
||||
]
|
||||
@@ -231,7 +233,8 @@
|
||||
"components/vectordbs/dbs/cassandra",
|
||||
"components/vectordbs/dbs/s3_vectors",
|
||||
"components/vectordbs/dbs/databricks",
|
||||
"components/vectordbs/dbs/neptune_analytics"
|
||||
"components/vectordbs/dbs/neptune_analytics",
|
||||
"components/vectordbs/dbs/turbopuffer"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -293,20 +296,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "OpenMemory",
|
||||
"groups": [
|
||||
{
|
||||
"group": "Overview & Quickstart",
|
||||
"icon": "square-terminal",
|
||||
"pages": [
|
||||
"openmemory/overview",
|
||||
"openmemory/quickstart",
|
||||
"openmemory/integrations"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tab": "Cookbooks",
|
||||
"groups": [
|
||||
@@ -1052,6 +1041,18 @@
|
||||
{
|
||||
"source": "/cookbooks/customer-support-agent",
|
||||
"destination": "/cookbooks/operations/support-inbox"
|
||||
},
|
||||
{
|
||||
"source": "/openmemory/overview",
|
||||
"destination": "/introduction"
|
||||
},
|
||||
{
|
||||
"source": "/openmemory/quickstart",
|
||||
"destination": "/introduction"
|
||||
},
|
||||
{
|
||||
"source": "/openmemory/integrations",
|
||||
"destination": "/introduction"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 282 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 426 KiB |
+1
-26
@@ -31,7 +31,7 @@ mode: "custom"
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="grid gap-6 sm:grid-cols-2">
|
||||
<a
|
||||
href="/platform/overview"
|
||||
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
|
||||
@@ -84,31 +84,6 @@ mode: "custom"
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/openmemory/overview"
|
||||
className="group flex h-full flex-col overflow-hidden rounded-2xl border border-gray-200 dark:border-zinc-800/40 bg-white dark:bg-zinc-900/40 transition hover:border-primary/60 hover:bg-gray-50 dark:hover:bg-zinc-900"
|
||||
>
|
||||
<img
|
||||
className="block dark:hidden aspect-[4/3] w-full object-cover"
|
||||
src="/images/docs thumbnails/light/mem0_openmemory.png"
|
||||
alt="OpenMemory thumbnail"
|
||||
style={{pointerEvents: "none"}}
|
||||
/>
|
||||
<img
|
||||
className="hidden dark:block aspect-[4/3] w-full object-cover"
|
||||
src="/images/docs thumbnails/dark/mem0_openmemory.png"
|
||||
alt="OpenMemory thumbnail"
|
||||
style={{pointerEvents: "none"}}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col gap-3 px-5 pb-6 pt-5 text-left">
|
||||
<h3 className="text-base font-semibold text-gray-900 dark:text-zinc-100 group-hover:text-primary">
|
||||
OpenMemory
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-zinc-400">
|
||||
Workspace-based memory for teams collaborating across agents and projects.
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -213,4 +213,3 @@ Key differentiators:
|
||||
- [FAQs](https://docs.mem0.ai/platform/faqs): Frequently asked questions about Mem0's Platform capabilities and implementation details
|
||||
- [Changelog](https://docs.mem0.ai/changelog): Detailed product updates and version history for tracking new features and improvements
|
||||
- [Contributing Guide](https://docs.mem0.ai/contributing/development): Guidelines for contributing to Mem0's open-source development
|
||||
- [OpenMemory](https://docs.mem0.ai/openmemory/overview): Open-source memory infrastructure for research and experimentation
|
||||
|
||||
@@ -123,13 +123,9 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-openai-api-key"
|
||||
}
|
||||
},
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"api_key": "your-openai-api-key",
|
||||
"top_k": 5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ The Mem0 REST API server exposes every OSS memory operation over HTTP. Run it al
|
||||
- You plan to explore or debug endpoints through the built-in OpenAPI page at `/docs`.
|
||||
</Info>
|
||||
|
||||
<Warning>
|
||||
**OSS vs Platform API paths:** The self-hosted OSS server does **not** use the `/v1/` prefix. For example, the endpoint is `POST /memories`, not `POST /v1/memories/`. The [API Reference](/api-reference) documents the hosted platform at `api.mem0.ai` which uses `/v1/` paths — those do not apply to the OSS server.
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
Enable API key authentication (see below) and HTTPS before exposing the server to anything beyond your internal network.
|
||||
</Warning>
|
||||
@@ -147,13 +151,18 @@ curl -X POST http://localhost:8000/memories \
|
||||
</Info>
|
||||
|
||||
```bash
|
||||
curl "http://localhost:8000/memories/search?user_id=alice&query=vegetable"
|
||||
curl -X POST http://localhost:8000/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"query": "vegetable",
|
||||
"user_id": "alice"
|
||||
}'
|
||||
```
|
||||
|
||||
### Explore with OpenAPI docs
|
||||
|
||||
1. Navigate to `http://localhost:8000/docs`.
|
||||
2. Pick an endpoint (e.g., `POST /memories/search`).
|
||||
1. Navigate to `http://localhost:8000/docs`.
|
||||
2. Pick an endpoint (e.g., `POST /search`).
|
||||
3. Fill in parameters and click **Execute** to try requests in-browser.
|
||||
|
||||
<Tip>
|
||||
@@ -162,6 +171,25 @@ curl "http://localhost:8000/memories/search?user_id=alice&query=vegetable"
|
||||
|
||||
---
|
||||
|
||||
## Endpoint reference
|
||||
|
||||
The OSS REST server exposes the following endpoints. None use the `/v1/` prefix.
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| `POST` | `/configure` | Set memory configuration |
|
||||
| `POST` | `/memories` | Create memories |
|
||||
| `GET` | `/memories` | Get all memories (filter by `user_id`, `agent_id`, or `run_id`) |
|
||||
| `GET` | `/memories/{memory_id}` | Get a specific memory |
|
||||
| `PUT` | `/memories/{memory_id}` | Update a memory |
|
||||
| `DELETE` | `/memories/{memory_id}` | Delete a specific memory |
|
||||
| `DELETE` | `/memories` | Delete all memories for an identifier |
|
||||
| `GET` | `/memories/{memory_id}/history` | Get memory history |
|
||||
| `POST` | `/search` | Search memories |
|
||||
| `POST` | `/reset` | Reset all memories |
|
||||
|
||||
---
|
||||
|
||||
## Verify the feature is working
|
||||
|
||||
- Hit the root route and `/docs` to confirm the server is reachable.
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
"title": "Mem0 API Docs",
|
||||
"description": "mem0.ai API Docs",
|
||||
"contact": {
|
||||
"email": "deshraj@mem0.ai"
|
||||
"email": "support@mem0.ai"
|
||||
},
|
||||
"license": {
|
||||
"name": "Apache 2.0"
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
title: MCP Client Integration Guide
|
||||
description: "Connect MCP-compatible clients to a locally running OpenMemory server for seamless memory integration."
|
||||
icon: "plug"
|
||||
iconType: "solid"
|
||||
---
|
||||
|
||||
## Connecting an MCP Client
|
||||
|
||||
Once your OpenMemory server is running locally, you can connect any compatible MCP client to your personal memory stream. This enables a seamless memory layer integration for AI tools and agents.
|
||||
|
||||
Ensure the following environment variables are correctly set in your configuration files:
|
||||
|
||||
**In `/ui/.env`:**
|
||||
```env
|
||||
NEXT_PUBLIC_API_URL=http://localhost:8765
|
||||
NEXT_PUBLIC_USER_ID=<user-id>
|
||||
```
|
||||
|
||||
**In `/api/.env`:**
|
||||
```env
|
||||
OPENAI_API_KEY=sk-xxx
|
||||
USER=<user-id>
|
||||
```
|
||||
|
||||
These values define where your MCP server is running and which user's memory is accessed.
|
||||
|
||||
### MCP Client Setup
|
||||
|
||||
Use the following one-step command to configure OpenMemory Local MCP to a client. The general command format is as follows:
|
||||
|
||||
```bash
|
||||
npx @openmemory/install local http://localhost:8765/mcp/<client-name>/sse/<user-id> --client <client-name>
|
||||
```
|
||||
|
||||
Replace `<client-name>` with the desired client name and `<user-id>` with the value specified in your environment variables.
|
||||
|
||||
### Example Commands for Supported Clients
|
||||
|
||||
| Client | Command |
|
||||
|-------------|---------|
|
||||
| Claude | `npx install-mcp http://localhost:8765/mcp/claude/sse/<user-id> --client claude` |
|
||||
| Cursor | `npx install-mcp http://localhost:8765/mcp/cursor/sse/<user-id> --client cursor` |
|
||||
| Cline | `npx install-mcp http://localhost:8765/mcp/cline/sse/<user-id> --client cline` |
|
||||
| RooCline | `npx install-mcp http://localhost:8765/mcp/roocline/sse/<user-id> --client roocline` |
|
||||
| Windsurf | `npx install-mcp http://localhost:8765/mcp/windsurf/sse/<user-id> --client windsurf` |
|
||||
| Witsy | `npx install-mcp http://localhost:8765/mcp/witsy/sse/<user-id> --client witsy` |
|
||||
| Enconvo | `npx install-mcp http://localhost:8765/mcp/enconvo/sse/<user-id> --client enconvo` |
|
||||
| Augment | `npx install-mcp http://localhost:8765/mcp/augment/sse/<user-id> --client augment` |
|
||||
|
||||
### What This Does
|
||||
|
||||
Running one of the above commands registers the specified MCP client and connects it to your OpenMemory server. This enables the client to stream and store contextual memory for the provided user ID.
|
||||
|
||||
The connection status and memory activity can be monitored via the OpenMemory UI at [http://localhost:3000](http://localhost:3000).
|
||||
@@ -1,124 +0,0 @@
|
||||
---
|
||||
title: Overview
|
||||
description: "Overview of OpenMemory, a local and hosted memory infrastructure powered by Mem0 with MCP server support."
|
||||
icon: "info"
|
||||
iconType: "solid"
|
||||
---
|
||||
|
||||
## 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, etc.)
|
||||
- Same standard memory operations: `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 across 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" />
|
||||
|
||||
## What is the OpenMemory MCP Server
|
||||
|
||||
The OpenMemory MCP Server is a private, local-first memory server that creates a shared, persistent memory layer for your MCP-compatible tools. It runs entirely on your machine, enabling seamless context handoff across tools. Whether you're switching between development, planning, or debugging environments, your AI assistants can access relevant memory without needing repeated instructions.
|
||||
|
||||
The OpenMemory MCP Server ensures all memory stays local, structured, and under your control with no cloud sync or external storage.
|
||||
|
||||
## OpenMemory Easy Setup
|
||||
|
||||
### Prerequisites
|
||||
- Docker
|
||||
- OpenAI API Key
|
||||
|
||||
You can quickly run OpenMemory by running the following command:
|
||||
|
||||
```bash
|
||||
curl -sL https://raw.githubusercontent.com/mem0ai/mem0/main/openmemory/run.sh | bash
|
||||
```
|
||||
|
||||
You should set the `OPENAI_API_KEY` as a global environment variable:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_api_key
|
||||
```
|
||||
|
||||
You can also set the `OPENAI_API_KEY` as a parameter to the script:
|
||||
|
||||
```bash
|
||||
curl -sL https://raw.githubusercontent.com/mem0ai/mem0/main/openmemory/run.sh | OPENAI_API_KEY=your_api_key bash
|
||||
```
|
||||
|
||||
This will start the OpenMemory server and the OpenMemory UI. Deleting the container will lead to the deletion of the memory store. We suggest you follow the instructions [here](/openmemory/quickstart#setting-up-openmemory) to set up OpenMemory on your local machine with a more persistent memory store.
|
||||
|
||||
## How the OpenMemory MCP Server Works
|
||||
|
||||
Built around the Model Context Protocol (MCP), the OpenMemory MCP Server exposes a standardized set of memory tools:
|
||||
- `add_memories`: Store new memory objects
|
||||
- `search_memory`: Retrieve relevant memories
|
||||
- `list_memories`: View all stored memory
|
||||
- `delete_all_memories`: Clear memory entirely
|
||||
|
||||
Any MCP-compatible tool can connect to the server and use these APIs to persist and access memory.
|
||||
|
||||
## What It Enables
|
||||
|
||||
### Cross-Client Memory Access
|
||||
Store context in Cursor and retrieve it later in Claude or Windsurf without repeating yourself.
|
||||
|
||||
### Fully Local Memory Store
|
||||
All memory is stored on your machine. Nothing goes to the cloud. You maintain full ownership and control.
|
||||
|
||||
### Unified Memory UI
|
||||
The built-in OpenMemory dashboard provides a central view of everything stored. Add, browse, delete, and control memory access to clients directly from the dashboard.
|
||||
|
||||
## Supported Clients
|
||||
|
||||
The OpenMemory MCP Server is compatible with any client that supports the Model Context Protocol. This includes:
|
||||
- Cursor
|
||||
- Claude Desktop
|
||||
- Windsurf
|
||||
- Cline
|
||||
- And more
|
||||
|
||||
As more AI systems adopt MCP, your private memory becomes more valuable.
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### Scenario 1: Cross-Tool Project Flow
|
||||
Define technical requirements of a project in Claude Desktop. Build in Cursor. Debug issues in Windsurf - all with shared context passed through OpenMemory.
|
||||
|
||||
### Scenario 2: Preferences That Persist
|
||||
Set your preferred code style or tone in one tool. When you switch to another MCP client, it can access those same preferences without redefining them.
|
||||
|
||||
### Scenario 3: Project Knowledge
|
||||
Save important project details once, then access them from any compatible AI tool - no more repetitive explanations.
|
||||
|
||||
## Conclusion
|
||||
|
||||
The OpenMemory MCP Server brings memory to MCP-compatible tools without giving up control or privacy. It solves a foundational limitation in modern LLM workflows: the loss of context across tools, sessions, and environments.
|
||||
|
||||
By standardizing memory operations and keeping all data local, it reduces token overhead, improves performance, and unlocks more intelligent interactions across the growing ecosystem of AI assistants.
|
||||
|
||||
This is just the beginning. The MCP server is the first core layer in the OpenMemory platform, a broader effort to make memory portable, private, and interoperable across AI systems.
|
||||
|
||||
## Getting Started Today
|
||||
|
||||
- Repository: [GitHub](https://github.com/mem0ai/mem0/tree/main/openmemory)
|
||||
- Join our community: [Discord](https://discord.gg/6PzXDgEjG5)
|
||||
|
||||
With OpenMemory, your AI memories stay private, portable, and under your control, exactly where they belong.
|
||||
|
||||
OpenMemory: Your memories, your control.
|
||||
|
||||
## Contributing
|
||||
|
||||
OpenMemory is open source and we welcome contributions. Please see the [CONTRIBUTING.md](https://github.com/mem0ai/mem0/blob/main/openmemory/CONTRIBUTING.md) file for more information.
|
||||
@@ -1,192 +0,0 @@
|
||||
---
|
||||
title: Quickstart
|
||||
description: "Get started with hosted or self-hosted OpenMemory MCP, including API key setup and client installation."
|
||||
icon: "terminal"
|
||||
iconType: "solid"
|
||||
---
|
||||
|
||||
## 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, etc.)
|
||||
- Same standard memory operations: `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:**
|
||||
```bash
|
||||
npx @openmemory/install --client claude --env OPENMEMORY_API_KEY=your-key
|
||||
```
|
||||
|
||||
**For Cursor:**
|
||||
```bash
|
||||
npx @openmemory/install --client cursor --env OPENMEMORY_API_KEY=your-key
|
||||
```
|
||||
|
||||
**For Windsurf:**
|
||||
```bash
|
||||
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
|
||||
- Docker
|
||||
- OpenAI API Key
|
||||
|
||||
You can quickly run OpenMemory by running the following command:
|
||||
|
||||
```bash
|
||||
curl -sL https://raw.githubusercontent.com/mem0ai/mem0/main/openmemory/run.sh | bash
|
||||
```
|
||||
|
||||
You should set the `OPENAI_API_KEY` as a global environment variable:
|
||||
|
||||
```bash
|
||||
export OPENAI_API_KEY=your_api_key
|
||||
```
|
||||
|
||||
You can also set the `OPENAI_API_KEY` as a parameter to the script:
|
||||
|
||||
```bash
|
||||
curl -sL https://raw.githubusercontent.com/mem0ai/mem0/main/openmemory/run.sh | OPENAI_API_KEY=your_api_key bash
|
||||
```
|
||||
|
||||
This will start the OpenMemory server and the OpenMemory UI. Deleting the container will lead to the deletion of the memory store. We suggest you follow the instructions below to set up OpenMemory on your local machine with a more persistent memory store.
|
||||
|
||||
## Setting Up OpenMemory
|
||||
|
||||
Getting started with OpenMemory is straightforward and takes just a few minutes to set up on your local machine. Follow these steps:
|
||||
|
||||
### 1. Clone the Repository
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/mem0ai/mem0.git
|
||||
cd mem0/openmemory
|
||||
```
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
#### LLM Configuration (optional)
|
||||
|
||||
By default, OpenMemory uses OpenAI (`gpt-4o-mini`) for the LLM and embedder. You can configure a different provider by adding these variables to `/api/.env`:
|
||||
|
||||
| Variable | Description | Default |
|
||||
|---|---|---|
|
||||
| `LLM_PROVIDER` | LLM provider (`openai`, `ollama`, `anthropic`, `groq`, `together`, `deepseek`, etc.) | `openai` |
|
||||
| `LLM_MODEL` | Model name for the LLM provider | `gpt-4o-mini` (OpenAI) / `llama3.1:latest` (Ollama) |
|
||||
| `LLM_API_KEY` | API key for the LLM provider | `OPENAI_API_KEY` env var |
|
||||
| `LLM_BASE_URL` | Custom base URL for the LLM API | Provider default |
|
||||
| `OLLAMA_BASE_URL` | Ollama-specific base URL (takes precedence over `LLM_BASE_URL` for Ollama) | `http://localhost:11434` |
|
||||
| `EMBEDDER_PROVIDER` | Embedder provider (defaults to `ollama` when LLM is Ollama, otherwise `openai`) | `openai` |
|
||||
| `EMBEDDER_MODEL` | Model name for the embedder | `text-embedding-3-small` (OpenAI) / `nomic-embed-text` (Ollama) |
|
||||
| `EMBEDDER_API_KEY` | API key for the embedder provider | `OPENAI_API_KEY` env var |
|
||||
| `EMBEDDER_BASE_URL` | Custom base URL for the embedder API | Provider default |
|
||||
|
||||
**Example: Using Ollama (fully local)**
|
||||
```bash
|
||||
LLM_PROVIDER=ollama
|
||||
LLM_MODEL=llama3.1:latest
|
||||
EMBEDDER_PROVIDER=ollama
|
||||
EMBEDDER_MODEL=nomic-embed-text
|
||||
OLLAMA_BASE_URL=http://localhost:11434
|
||||
```
|
||||
|
||||
**Example: Using Anthropic**
|
||||
```bash
|
||||
LLM_PROVIDER=anthropic
|
||||
LLM_MODEL=claude-sonnet-4-20250514
|
||||
LLM_API_KEY=sk-ant-xxx
|
||||
```
|
||||
|
||||
#### 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
|
||||
npx @openmemory/install local "http://localhost:8765/mcp/cursor/sse/username" --client cursor
|
||||
```
|
||||
|
||||
The OpenMemory dashboard will be available at http://localhost:3000. From here, you can view and manage your memories and check connection status with your MCP clients.
|
||||
|
||||
Once set up, OpenMemory runs locally on your machine, ensuring all your AI memories remain private and secure while being accessible across any compatible MCP client.
|
||||
|
||||
## Getting Started Today
|
||||
|
||||
GitHub Repository: https://github.com/mem0ai/mem0/tree/main/openmemory
|
||||
@@ -9,11 +9,48 @@ description: "Connect any AI client to Mem0 using Model Context Protocol for uni
|
||||
|
||||
When building AI applications, memory management often requires manual integration. MCP eliminates this complexity by:
|
||||
|
||||
- **Universal compatibility**: Works with any MCP-compatible client (Claude Desktop, Cursor, custom agents)
|
||||
- **Universal compatibility**: Works with any MCP-compatible client (Claude, Claude Code, Cursor, Windsurf, VS Code, OpenCode)
|
||||
- **Agent autonomy**: AI agents decide when to save, search, or update memories
|
||||
- **Zero infrastructure**: No servers to maintain - Mem0 handles everything
|
||||
- **Zero infrastructure**: No servers to maintain - Mem0's cloud MCP handles everything
|
||||
- **Standardized protocol**: One integration works across all your AI tools
|
||||
|
||||
## Setup
|
||||
|
||||
Add Mem0 MCP to all supported clients with a single command:
|
||||
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "claude,claude code,cursor,windsurf,vscode,opencode"
|
||||
```
|
||||
|
||||
Or configure a specific client:
|
||||
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "cursor"
|
||||
```
|
||||
|
||||
For manual configuration, add this to your MCP client config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0-mcp": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.mem0.ai/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For detailed per-client instructions, see the [Mem0 MCP Quickstart](/platform/mem0-mcp).
|
||||
|
||||
## Available tools
|
||||
|
||||
The MCP server exposes 9 memory tools to your AI client:
|
||||
@@ -30,139 +67,12 @@ The MCP server exposes 9 memory tools to your AI client:
|
||||
| `get_memory` | Retrieve single memory by ID |
|
||||
| `list_entities` | View stored entities |
|
||||
|
||||
## Deployment options
|
||||
## How it works
|
||||
|
||||
Choose the deployment method that fits your workflow:
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Python package (recommended)">
|
||||
Install and run locally with uvx:
|
||||
|
||||
```bash
|
||||
uv pip install mem0-mcp-server
|
||||
```
|
||||
|
||||
Configure your client:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"command": "uvx",
|
||||
"args": ["mem0-mcp-server"],
|
||||
"env": {
|
||||
"MEM0_API_KEY": "m0-...",
|
||||
"MEM0_DEFAULT_USER_ID": "your-handle"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Docker container">
|
||||
Containerized deployment with HTTP endpoint:
|
||||
|
||||
```bash
|
||||
docker build -t mem0-mcp-server https://github.com/mem0ai/mem0-mcp.git
|
||||
docker run --rm -d -e MEM0_API_KEY="m0-..." -p 8080:8081 mem0-mcp-server
|
||||
```
|
||||
|
||||
Configure for HTTP:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0-docker": {
|
||||
"command": "curl",
|
||||
"args": ["-X", "POST", "http://localhost:8080/mcp", "--data-binary", "@"],
|
||||
"env": {
|
||||
"MEM0_API_KEY": "m0-..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Smithery">
|
||||
One-click setup with managed service:
|
||||
|
||||
Visit [smithery.ai/server/@mem0ai/mem0-memory-mcp](https://smithery.ai/server/@mem0ai/mem0-memory-mcp) and:
|
||||
|
||||
1. Select your AI client (Cursor, Claude Desktop, etc.)
|
||||
2. Configure your Mem0 API key
|
||||
3. Set your default user ID
|
||||
4. Enable graph memory (optional)
|
||||
5. Copy the generated configuration
|
||||
|
||||
Your client connects automatically - no installation required.
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Configuration
|
||||
|
||||
### Required environment variables
|
||||
```bash
|
||||
MEM0_API_KEY="m0-..." # Your Mem0 API key
|
||||
MEM0_DEFAULT_USER_ID="your-handle" # Default user ID
|
||||
```
|
||||
|
||||
### Optional variables
|
||||
```bash
|
||||
MEM0_ENABLE_GRAPH_DEFAULT="true" # Enable graph memories
|
||||
MEM0_MCP_AGENT_MODEL="gpt-4o-mini" # LLM for bundled examples
|
||||
```
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Test your setup with the Python agent">
|
||||
The included Pydantic AI agent provides an interactive REPL to test memory operations:
|
||||
|
||||
```bash
|
||||
# Install the package
|
||||
pip install mem0-mcp-server
|
||||
|
||||
# Set your API keys
|
||||
export MEM0_API_KEY="m0-..."
|
||||
export OPENAI_API_KEY="sk-openai-..."
|
||||
|
||||
# Clone and test with the agent
|
||||
git clone https://github.com/mem0ai/mem0-mcp.git
|
||||
cd mem0-mcp-server
|
||||
python example/pydantic_ai_repl.py
|
||||
```
|
||||
|
||||
**Testing different server configurations:**
|
||||
|
||||
- **Local server** (default): `python example/pydantic_ai_repl.py`
|
||||
|
||||
- **Docker container**:
|
||||
```bash
|
||||
export MEM0_MCP_CONFIG_PATH=example/docker-config.json
|
||||
export MEM0_MCP_CONFIG_SERVER=mem0-docker
|
||||
python example/pydantic_ai_repl.py
|
||||
```
|
||||
|
||||
- **Smithery remote**:
|
||||
```bash
|
||||
export MEM0_MCP_CONFIG_PATH=example/config-smithery.json
|
||||
export MEM0_MCP_CONFIG_SERVER=mem0-memory-mcp
|
||||
python example/pydantic_ai_repl.py
|
||||
```
|
||||
|
||||
Try these test prompts:
|
||||
- "Remember that I love tiramisu"
|
||||
- "Search for my food preferences"
|
||||
- "Update my project: the mobile app is now 80% complete"
|
||||
- "Show me all memories about project Phoenix"
|
||||
- "Delete memories from 2023"
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## How the testing works
|
||||
|
||||
1. **Configuration loads** - Reads from `example/config.json` by default
|
||||
2. **Server starts** - Launches or connects to the Mem0 MCP server
|
||||
3. **Agent connects** - Pydantic AI agent (Mem0Guide) attaches to the server
|
||||
4. **Interactive REPL** - You get a chat interface to test all memory operations
|
||||
1. **Configure the MCP server** - Add Mem0 MCP to your AI client using the setup command above
|
||||
2. **Agent connects** - Your AI client connects to Mem0's cloud MCP server over HTTP
|
||||
3. **Autonomous memory** - The agent decides when to store/retrieve memories as part of its reasoning
|
||||
4. **No manual API calls** - The agent manages memory automatically through MCP tools
|
||||
|
||||
## Example interactions
|
||||
|
||||
@@ -225,14 +135,11 @@ The Mem0 MCP server enables powerful memory capabilities for your AI application
|
||||
|
||||
## Best practices
|
||||
|
||||
- **Start simple**: Use the Python package for development
|
||||
- **Use the cloud MCP**: The hosted MCP server at `https://mcp.mem0.ai/mcp` handles infrastructure for you
|
||||
- **Use wildcards**: `user_id: "*"` to search across all users
|
||||
- **Test locally**: Use the bundled Python agent to verify setup
|
||||
- **Monitor usage**: Track memory operations in the dashboard
|
||||
- **Document patterns**: Share successful prompt patterns with your team
|
||||
|
||||
{/* DEBUG: verify CTA targets */}
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="Memory Filters"
|
||||
@@ -246,4 +153,4 @@ The Mem0 MCP server enables powerful memory capabilities for your AI application
|
||||
icon="book-open"
|
||||
href="/cookbooks/frameworks/gemini-3-with-mem0-mcp"
|
||||
/>
|
||||
</CardGroup>
|
||||
</CardGroup>
|
||||
|
||||
+108
-141
@@ -2,28 +2,34 @@
|
||||
title: "Mem0 MCP"
|
||||
description: "Connect any AI client to Mem0 using Model Context Protocol in minutes"
|
||||
icon: "puzzle-piece"
|
||||
estimatedTime: "~5 minutes"
|
||||
estimatedTime: "~2 minutes"
|
||||
---
|
||||
|
||||
<Info>
|
||||
**Prerequisites**
|
||||
- Mem0 Platform account ([Sign up here](https://app.mem0.ai))
|
||||
- API key ([Get one from dashboard](https://app.mem0.ai/settings/api-keys))
|
||||
- Python 3.10+, Docker, or Node.js 14+
|
||||
- An MCP-compatible client (Claude Desktop, Cursor, or custom agent)
|
||||
- Node.js 14+ (for npx)
|
||||
- An MCP-compatible client (Claude, Claude Code, Cursor, Windsurf, VS Code, OpenCode)
|
||||
</Info>
|
||||
|
||||
## What is Mem0 MCP?
|
||||
|
||||
Mem0 MCP Server exposes Mem0's memory capabilities as MCP tools, letting AI agents decide when to save, search, or update information.
|
||||
Mem0 MCP Server exposes Mem0's memory capabilities as MCP tools, letting AI agents decide when to save, search, or update information. The cloud-hosted MCP server requires no local installation — just connect and start using memory.
|
||||
|
||||
## Deployment Options
|
||||
## Quick Setup
|
||||
|
||||
Choose from three deployment methods:
|
||||
Add Mem0 MCP to your preferred clients with a single command:
|
||||
|
||||
1. **Python Package (Recommended)** - Install locally with `uvx` for instant setup
|
||||
2. **Docker Container** - Isolated deployment with HTTP endpoint
|
||||
3. **Smithery** - Remote hosted service for managed deployments
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "claude,claude code,cursor,windsurf,vscode,opencode"
|
||||
```
|
||||
|
||||
This automatically configures Mem0 MCP for all supported clients at once.
|
||||
|
||||
## Available Tools
|
||||
|
||||
@@ -43,54 +49,105 @@ The MCP server exposes these memory tools to your AI client:
|
||||
|
||||
---
|
||||
|
||||
## Quickstart with Python (UVX)
|
||||
## Client-Specific Setup
|
||||
|
||||
<Steps>
|
||||
<Step title="Install the MCP Server">
|
||||
```bash
|
||||
uv pip install mem0-mcp-server
|
||||
```
|
||||
</Step>
|
||||
You can also configure individual clients:
|
||||
|
||||
<Step title="Configure your MCP client">
|
||||
Add this to your MCP client (e.g., Claude Desktop):
|
||||
<AccordionGroup>
|
||||
<Accordion title="Claude Desktop">
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "claude"
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"command": "uvx",
|
||||
"args": ["mem0-mcp-server"],
|
||||
"env": {
|
||||
"MEM0_API_KEY": "m0-...",
|
||||
"MEM0_DEFAULT_USER_ID": "your-handle"
|
||||
Or manually add to your Claude Desktop configuration (`claude_desktop_config.json`):
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0-mcp": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.mem0.ai/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
Set your environment variables:
|
||||
<Accordion title="Claude Code">
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "claude code"
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
```bash
|
||||
export MEM0_API_KEY="m0-..."
|
||||
export MEM0_DEFAULT_USER_ID="your-handle"
|
||||
```
|
||||
</Step>
|
||||
<Accordion title="Cursor">
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "cursor"
|
||||
```
|
||||
|
||||
<Step title="Test with the Python agent">
|
||||
```bash
|
||||
# Clone the mem0-mcp repository
|
||||
git clone https://github.com/mem0ai/mem0-mcp.git
|
||||
cd mem0-mcp
|
||||
Or go to Cursor → Settings → MCP and add:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0-mcp": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.mem0.ai/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
# Set your API keys
|
||||
export MEM0_API_KEY="m0-..."
|
||||
export OPENAI_API_KEY="sk-openai-..."
|
||||
<Accordion title="Windsurf">
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "windsurf"
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
# Run the interactive agent
|
||||
python example/pydantic_ai_repl.py
|
||||
```
|
||||
<Accordion title="VS Code">
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "vscode"
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="OpenCode">
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "opencode"
|
||||
```
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
---
|
||||
|
||||
## Verify Your Setup
|
||||
|
||||
Once configured, your AI client can:
|
||||
- Automatically save information with `add_memory`
|
||||
- Search memories with `search_memories`
|
||||
- Update memories with `update_memory`
|
||||
- Delete memories with `delete_memory`
|
||||
|
||||
**Sample Interactions:**
|
||||
|
||||
@@ -104,107 +161,18 @@ Agent: Based on your memories, you love tiramisu.
|
||||
User: Update my project: the mobile app is now 80% complete
|
||||
Agent: Updated your project status successfully.
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Verify the setup">
|
||||
Your AI client can now:
|
||||
- Automatically save information with `add_memory`
|
||||
- Search memories with `search_memories`
|
||||
- Update memories with `update_memory`
|
||||
- Delete memories with `delete_memory`
|
||||
|
||||
<Info icon="check">
|
||||
If you get "Connection failed", ensure your API key is valid and the server is running.
|
||||
If you get "Connection failed", ensure you have a valid API key from [Mem0 Dashboard](https://app.mem0.ai/settings/api-keys).
|
||||
</Info>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
## Quickstart with Docker
|
||||
|
||||
<Steps>
|
||||
<Step title="Build the Docker image">
|
||||
```bash
|
||||
docker build -t mem0-mcp-server https://github.com/mem0ai/mem0-mcp.git
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Run the container">
|
||||
```bash
|
||||
docker run --rm -d \
|
||||
--name mem0-mcp \
|
||||
-e MEM0_API_KEY="m0-..." \
|
||||
-p 8080:8081 \
|
||||
mem0-mcp-server
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Configure your client for HTTP">
|
||||
For clients that connect via HTTP (instead of stdio):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0-docker": {
|
||||
"command": "curl",
|
||||
"args": ["-X", "POST", "http://localhost:8080/mcp", "--data-binary", "@-"],
|
||||
"env": {
|
||||
"MEM0_API_KEY": "m0-..."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
|
||||
<Step title="Verify the setup">
|
||||
```bash
|
||||
# Check container logs
|
||||
docker logs mem0-mcp
|
||||
|
||||
# Test HTTP endpoint
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
<Info icon="check">
|
||||
The container should start successfully and respond to HTTP requests. If port 8080 is occupied, change it with `-p 8081:8081`.
|
||||
</Info>
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
## Quickstart with Smithery (Hosted)
|
||||
|
||||
For the simplest integration, use Smithery's hosted Mem0 MCP server - no installation required.
|
||||
|
||||
**Example: One-click setup in Cursor**
|
||||
|
||||
1. Visit [smithery.ai/server/@mem0ai/mem0-memory-mcp](https://smithery.ai/server/@mem0ai/mem0-memory-mcp) and select Cursor as your client
|
||||
|
||||

|
||||
|
||||
2. Open Cursor → Settings → MCP
|
||||
3. Click `mem0-mcp` → Initiate authorization
|
||||
4. Configure Smithery with your environment:
|
||||
- `MEM0_API_KEY`: Your Mem0 API key
|
||||
- `MEM0_DEFAULT_USER_ID`: Your user ID
|
||||
- `MEM0_ENABLE_GRAPH_DEFAULT`: Optional, set to `true` for graph memories
|
||||
5. Return to Cursor settings and wait for tools to load
|
||||
6. Start chatting with Cursor and begin storing preferences
|
||||
|
||||
**For other clients:**
|
||||
Visit [smithery.ai/server/@mem0ai/mem0-memory-mcp](https://smithery.ai/server/@mem0ai/mem0-memory-mcp) to connect any MCP-compatible client with your Mem0 credentials.
|
||||
|
||||
---
|
||||
|
||||
## Quick Recovery
|
||||
|
||||
- **"uvx command not found"** → Install with `pip install uv` or use `pip install mem0-mcp-server` instead. Make sure your Python environment has `uv` installed (or system-wide).
|
||||
- **"Connection refused"** → Check that the server is running and the correct port is configured
|
||||
- **"Connection refused"** → Check your internet connection and ensure the MCP client is correctly configured
|
||||
- **"Invalid API key"** → Get a new key from [Mem0 Dashboard](https://app.mem0.ai/settings/api-keys)
|
||||
- **"Permission denied"** → Ensure Docker has access to bind ports (try with `sudo` on Linux)
|
||||
- **"npx command not found"** → Install Node.js from [nodejs.org](https://nodejs.org)
|
||||
|
||||
---
|
||||
|
||||
@@ -227,6 +195,5 @@ Visit [smithery.ai/server/@mem0ai/mem0-memory-mcp](https://smithery.ai/server/@m
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **[Mem0 MCP Repository](https://github.com/mem0ai/mem0-mcp)** - Source code and examples
|
||||
- **[Platform Quickstart](/platform/quickstart)** - Direct API integration guide
|
||||
- **[MCP Specification](https://modelcontextprotocol.io)** - Learn about MCP protocol
|
||||
- **[MCP Specification](https://modelcontextprotocol.io)** - Learn about MCP protocol
|
||||
|
||||
Vendored
+2
-2
@@ -166,13 +166,13 @@ Call out the most common mistake or edge case for this layer.
|
||||
title="[Related cookbook / deep dive]"
|
||||
description="[Why this pairs well with the current guide]"
|
||||
icon="arrow-right"
|
||||
href="/[related-link]"
|
||||
href="#related-link"
|
||||
/>
|
||||
<Card
|
||||
title="[Next cookbook in journey]"
|
||||
description="[Set expectation for the next step]"
|
||||
icon="rocket"
|
||||
href="/[next-link]"
|
||||
href="#next-link"
|
||||
/>
|
||||
</CardGroup>
|
||||
```
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Integration Guide Template
|
||||
description: "Pattern for pairing Mem0 with third-party tools."
|
||||
description: "A reusable template for writing integration guides that pair Mem0 with third-party tools and services."
|
||||
icon: "plug"
|
||||
---
|
||||
|
||||
|
||||
+2
-2
@@ -145,13 +145,13 @@ npm install mem0ai@[version]
|
||||
title="[Deep dive reference]"
|
||||
description="[Why this reference matters post-migration]"
|
||||
icon="book"
|
||||
href="/[reference-link]"
|
||||
href="#reference-link"
|
||||
/>
|
||||
<Card
|
||||
title="[Applied example or next step]"
|
||||
description="[What readers can build now]"
|
||||
icon="rocket"
|
||||
href="/[example-link]"
|
||||
href="#example-link"
|
||||
/>
|
||||
</CardGroup>
|
||||
```
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: "Vibecoding with Mem0"
|
||||
sidebarTitle: "Vibecoding"
|
||||
description: "Agent skills, starter prompts, and setup for building with Mem0 using AI coding tools."
|
||||
icon: "wand-magic-sparkles"
|
||||
---
|
||||
|
||||
These docs are designed to be easily consumable by LLMs. Each page has a button that lets you copy the page as Markdown or paste directly into ChatGPT, Claude, or any AI coding tool.
|
||||
|
||||
We follow the llms.txt standard:
|
||||
|
||||
- [llms.txt](https://docs.mem0.ai/llms.txt)
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Get an API Key" icon="key" href="https://app.mem0.ai">
|
||||
Sign up for Mem0 Platform and start building
|
||||
</Card>
|
||||
<Card title="Quickstart" icon="rocket" href="/platform/quickstart">
|
||||
Store your first memory in under 5 minutes
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
## Agent Skills
|
||||
|
||||
Teach your coding assistant how to build with Mem0:
|
||||
|
||||
```bash
|
||||
npx skills add https://github.com/mem0ai/mem0 --skill mem0
|
||||
```
|
||||
|
||||
Works with Claude Code, Cursor, Windsurf, and any assistant that supports skills. Once installed, your assistant understands Mem0's full API, framework integrations, and common patterns.
|
||||
|
||||
## MCP Server Setup
|
||||
|
||||
Connect Claude, Claude Code, Cursor, Windsurf, VS Code, OpenCode, or any MCP-compatible client to Mem0.
|
||||
|
||||
Get your API key from [app.mem0.ai](https://app.mem0.ai), then add Mem0 MCP with a single command:
|
||||
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "claude,claude code,cursor,windsurf,vscode,opencode"
|
||||
```
|
||||
|
||||
For per-client setup and advanced options, see [Mem0 MCP Setup](/platform/mem0-mcp).
|
||||
|
||||
## Universal Starter Prompt
|
||||
|
||||
Copy this into any AI tool to start building with Mem0:
|
||||
|
||||
```text
|
||||
I want to start building with Mem0 — a self-improving memory layer for LLM
|
||||
applications that gives agents persistent context across sessions.
|
||||
|
||||
## Mem0 Resources
|
||||
|
||||
**Documentation:**
|
||||
- Main docs: https://docs.mem0.ai
|
||||
- Platform Quickstart: https://docs.mem0.ai/platform/quickstart
|
||||
- OSS Python Quickstart: https://docs.mem0.ai/open-source/python-quickstart
|
||||
- OSS Node.js Quickstart: https://docs.mem0.ai/open-source/node-quickstart
|
||||
- API Reference: https://docs.mem0.ai/api-reference
|
||||
- Full LLM-friendly docs: https://docs.mem0.ai/llms.txt
|
||||
|
||||
**Code & Examples:**
|
||||
- Core repo: https://github.com/mem0ai/mem0
|
||||
- Python SDK: pip install mem0ai
|
||||
- TypeScript SDK: npm install mem0ai
|
||||
- Cookbooks: https://docs.mem0.ai/cookbooks/overview
|
||||
|
||||
**What Mem0 Does:**
|
||||
Mem0 is a memory layer for AI apps — managed (Mem0 Platform) or self-hosted
|
||||
(Open Source). It stores, retrieves, and manages user memories so agents
|
||||
remember preferences, learn from interactions, and personalize over time.
|
||||
Sub-50ms retrieval. Dual storage: vector embeddings + graph databases.
|
||||
|
||||
**Architecture Overview:**
|
||||
- Memory is scoped by user_id, agent_id, or run_id
|
||||
- Core operations: add, search, update, delete
|
||||
- Memory types: factual (preferences, facts), episodic (past interactions),
|
||||
semantic (concept relationships), working (session state)
|
||||
- Integration pattern: retrieve relevant memories → generate response → store
|
||||
new memories
|
||||
|
||||
**Quick Usage (Python Platform):**
|
||||
from mem0 import MemoryClient
|
||||
client = MemoryClient(api_key="m0-xxx")
|
||||
client.add("I prefer dark mode and use VS Code.", user_id="user1")
|
||||
results = client.search("What editor do they use?", user_id="user1")
|
||||
|
||||
**Quick Usage (JavaScript Platform):**
|
||||
import MemoryClient from 'mem0ai';
|
||||
const client = new MemoryClient({ apiKey: 'm0-xxx' });
|
||||
await client.add([{ role: "user", content: "I prefer dark mode." }], { user_id: "user1" });
|
||||
const results = await client.search("What editor?", { user_id: "user1" });
|
||||
|
||||
**Quick Usage (Python Open Source):**
|
||||
from mem0 import Memory
|
||||
m = Memory()
|
||||
m.add("I prefer dark mode and use VS Code.", user_id="user1")
|
||||
results = m.search("What editor do they use?", user_id="user1")
|
||||
|
||||
Help me integrate Mem0 into my project. Start by asking what I'm building,
|
||||
what language/framework I'm using, and whether I want managed or self-hosted.
|
||||
```
|
||||
|
||||
## Go Deeper
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Platform Quickstart" icon="cloud" href="/platform/quickstart">
|
||||
Get started with the managed API
|
||||
</Card>
|
||||
<Card title="Open Source" icon="code-branch" href="/open-source/overview">
|
||||
Self-host with full control
|
||||
</Card>
|
||||
<Card title="Cookbooks" icon="book" href="/cookbooks/overview">
|
||||
Production-ready tutorials and examples
|
||||
</Card>
|
||||
<Card title="API Reference" icon="code" href="/api-reference">
|
||||
Explore every REST endpoint
|
||||
</Card>
|
||||
</CardGroup>
|
||||
Executable
+419
@@ -0,0 +1,419 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# install-mem0-plugin.sh
|
||||
#
|
||||
# Installs and configures the @mem0/openclaw-mem0 plugin for an existing
|
||||
# NemoClaw sandbox. Assumes NemoClaw is already installed and onboarded.
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x install-mem0-plugin.sh && ./install-mem0-plugin.sh
|
||||
#
|
||||
# Requirements:
|
||||
# - NemoClaw installed and onboarded (sandbox in Ready state)
|
||||
# - Mem0 API key (from app.mem0.ai)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colors and formatting ────────────────────────────────────────────────────
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||
success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
step() { echo -e "\n${BOLD}${CYAN}── $* ──${NC}\n"; }
|
||||
ask() { echo -en "${BOLD}$*${NC}"; }
|
||||
|
||||
die() {
|
||||
error "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── Defaults ─────────────────────────────────────────────────────────────────
|
||||
|
||||
MEM0_USER_ID="${MEM0_USER_ID:-default}"
|
||||
PLUGIN_PKG="@mem0/openclaw-mem0"
|
||||
CONTAINER_NAME="nemoclaw-dev"
|
||||
|
||||
# ── Detect platform ──────────────────────────────────────────────────────────
|
||||
|
||||
OS_TYPE="$(uname -s)"
|
||||
IS_MACOS=false
|
||||
IS_LINUX=false
|
||||
|
||||
case "$OS_TYPE" in
|
||||
Darwin) IS_MACOS=true ;;
|
||||
Linux) IS_LINUX=true ;;
|
||||
*) die "Unsupported OS: $OS_TYPE" ;;
|
||||
esac
|
||||
|
||||
# ── Helper functions ─────────────────────────────────────────────────────────
|
||||
|
||||
check_command() {
|
||||
command -v "$1" &>/dev/null
|
||||
}
|
||||
|
||||
ensure_nvm() {
|
||||
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
||||
if [[ -s "$NVM_DIR/nvm.sh" ]]; then
|
||||
source "$NVM_DIR/nvm.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_path() {
|
||||
for p in "$HOME/.local/bin" "$HOME/.nvm/versions/node/"*/bin; do
|
||||
if [[ -d "$p" ]] && [[ ":$PATH:" != *":$p:"* ]]; then
|
||||
export PATH="$p:$PATH"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Wrapper to run a command natively (Linux) or in the container (macOS)
|
||||
run_cmd() {
|
||||
if $IS_MACOS; then
|
||||
docker exec "$CONTAINER_NAME" bash -c "export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh && $*"
|
||||
else
|
||||
eval "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Banner ───────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}${CYAN}║ Mem0 Plugin Installer for NemoClaw ║${NC}"
|
||||
echo -e "${BOLD}${CYAN}║ Long-term memory for your OpenClaw agent ║${NC}"
|
||||
echo -e "${BOLD}${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
if $IS_MACOS; then
|
||||
info "Platform: macOS (using Docker container '$CONTAINER_NAME')"
|
||||
else
|
||||
info "Platform: Linux"
|
||||
fi
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# PRE-CHECK: Verify NemoClaw is installed and sandbox is ready
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
step "Pre-check: Verifying NemoClaw setup"
|
||||
|
||||
if $IS_LINUX; then
|
||||
ensure_nvm
|
||||
ensure_path
|
||||
fi
|
||||
|
||||
# Verify nemoclaw/openshell are available
|
||||
if $IS_MACOS; then
|
||||
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||
die "Docker container '$CONTAINER_NAME' is not running. Start it with: docker start $CONTAINER_NAME"
|
||||
fi
|
||||
if ! run_cmd "command -v nemoclaw" &>/dev/null; then
|
||||
die "NemoClaw is not installed in container '$CONTAINER_NAME'. Run the full setup script first."
|
||||
fi
|
||||
else
|
||||
if ! check_command openshell; then
|
||||
die "openshell not found. Is NemoClaw installed and onboarded? Try: source ~/.bashrc"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Detect sandbox
|
||||
SANDBOX_NAME=$(run_cmd "openshell sandbox list 2>/dev/null" | awk 'NR>1 && $1!="" {print $1; exit}' || true)
|
||||
|
||||
if [[ -z "$SANDBOX_NAME" ]]; then
|
||||
die "No sandbox found. Run 'nemoclaw onboard' first."
|
||||
fi
|
||||
|
||||
# Verify sandbox is ready
|
||||
SANDBOX_PHASE=$(run_cmd "openshell sandbox get '$SANDBOX_NAME' 2>/dev/null" | grep -i "phase" | awk '{print $NF}' || true)
|
||||
if [[ "$SANDBOX_PHASE" != "Ready" ]]; then
|
||||
warn "Sandbox '$SANDBOX_NAME' is not in Ready state (current: ${SANDBOX_PHASE:-unknown})."
|
||||
warn "Waiting up to 2 minutes..."
|
||||
WAIT_OK=false
|
||||
for i in $(seq 1 24); do
|
||||
SANDBOX_PHASE=$(run_cmd "openshell sandbox get '$SANDBOX_NAME' 2>/dev/null" | grep -i "phase" | awk '{print $NF}' || true)
|
||||
if [[ "$SANDBOX_PHASE" == "Ready" ]]; then
|
||||
WAIT_OK=true
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
if ! $WAIT_OK; then
|
||||
die "Sandbox '$SANDBOX_NAME' did not become Ready. Run: openshell sandbox list"
|
||||
fi
|
||||
fi
|
||||
|
||||
success "NemoClaw installed"
|
||||
success "Sandbox '$SANDBOX_NAME' is ready"
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# STEP 1: Install Mem0 Plugin
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
step "Step 1: Installing Mem0 plugin ($PLUGIN_PKG)"
|
||||
|
||||
# Helper: run a command inside the sandbox non-interactively via piped stdin
|
||||
sandbox_exec() {
|
||||
local cmd="$1"
|
||||
if $IS_MACOS; then
|
||||
printf '%s\nexit\n' "$cmd" | docker exec -i "$CONTAINER_NAME" bash -c \
|
||||
"export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh && openshell sandbox connect '$SANDBOX_NAME'" 2>&1
|
||||
else
|
||||
printf '%s\nexit\n' "$cmd" | openshell sandbox connect "$SANDBOX_NAME" 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if plugin is already installed
|
||||
PLUGIN_EXISTS=false
|
||||
if run_cmd "openshell sandbox download '$SANDBOX_NAME' /sandbox/.openclaw/extensions/openclaw-mem0/package.json /tmp/_mem0_plugin_check.json" &>/dev/null; then
|
||||
if [[ -f /tmp/_mem0_plugin_check.json ]] || run_cmd "test -f /tmp/_mem0_plugin_check.json" &>/dev/null; then
|
||||
PLUGIN_EXISTS=true
|
||||
fi
|
||||
fi
|
||||
rm -f /tmp/_mem0_plugin_check.json 2>/dev/null || true
|
||||
run_cmd "rm -f /tmp/_mem0_plugin_check.json" 2>/dev/null || true
|
||||
|
||||
if $PLUGIN_EXISTS; then
|
||||
success "Mem0 plugin already installed in sandbox"
|
||||
else
|
||||
info "Downloading $PLUGIN_PKG..."
|
||||
|
||||
# Download and build outside the sandbox (on host or in container)
|
||||
run_cmd "cd /tmp && rm -rf openclaw-mem0-full mem0-openclaw-mem0-*.tgz openclaw-mem0-full.tgz"
|
||||
|
||||
if ! run_cmd "cd /tmp && npm pack '$PLUGIN_PKG' 2>/dev/null"; then
|
||||
die "Failed to download $PLUGIN_PKG from npm. Check your internet connection."
|
||||
fi
|
||||
|
||||
success "Downloaded plugin"
|
||||
|
||||
info "Installing plugin dependencies..."
|
||||
run_cmd "mkdir -p /tmp/openclaw-mem0-full && cd /tmp/openclaw-mem0-full && tar xzf /tmp/mem0-openclaw-mem0-*.tgz --strip-components=1 && npm install --omit=dev 2>&1 | tail -3"
|
||||
|
||||
success "Dependencies installed"
|
||||
|
||||
info "Uploading plugin to sandbox..."
|
||||
run_cmd "cd /tmp && tar czf openclaw-mem0-full.tgz -C openclaw-mem0-full ."
|
||||
|
||||
if ! run_cmd "openshell sandbox upload '$SANDBOX_NAME' /tmp/openclaw-mem0-full.tgz /sandbox/openclaw-mem0-full.tgz 2>&1"; then
|
||||
die "Failed to upload plugin to sandbox. Check: openshell sandbox list"
|
||||
fi
|
||||
|
||||
success "Plugin uploaded"
|
||||
|
||||
info "Extracting plugin inside sandbox..."
|
||||
sandbox_exec "mkdir -p ~/.openclaw/extensions/openclaw-mem0 && tar xzf /sandbox/openclaw-mem0-full.tgz/openclaw-mem0-full.tgz -C ~/.openclaw/extensions/openclaw-mem0 2>/dev/null || tar xzf /sandbox/openclaw-mem0-full.tgz -C ~/.openclaw/extensions/openclaw-mem0 2>/dev/null && echo EXTRACT_OK" >/dev/null 2>&1 || true
|
||||
|
||||
# Verify
|
||||
VERIFY_OK=false
|
||||
if run_cmd "openshell sandbox download '$SANDBOX_NAME' /sandbox/.openclaw/extensions/openclaw-mem0/package.json /tmp/_mem0_verify.json" &>/dev/null; then
|
||||
VERIFY_OK=true
|
||||
fi
|
||||
rm -f /tmp/_mem0_verify.json 2>/dev/null || true
|
||||
run_cmd "rm -f /tmp/_mem0_verify.json" 2>/dev/null || true
|
||||
if $VERIFY_OK; then
|
||||
success "Plugin extracted inside sandbox"
|
||||
else
|
||||
warn "Could not verify plugin extraction. You may need to extract manually."
|
||||
if $IS_MACOS; then
|
||||
echo " docker exec -it $CONTAINER_NAME bash"
|
||||
echo " export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh"
|
||||
fi
|
||||
echo " nemoclaw $SANDBOX_NAME connect"
|
||||
echo " mkdir -p ~/.openclaw/extensions/openclaw-mem0"
|
||||
echo " tar xzf /sandbox/openclaw-mem0-full.tgz/openclaw-mem0-full.tgz -C ~/.openclaw/extensions/openclaw-mem0"
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
run_cmd "rm -rf /tmp/openclaw-mem0-full /tmp/mem0-openclaw-mem0-*.tgz /tmp/openclaw-mem0-full.tgz" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# STEP 2: Update Network Policy
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
step "Step 2: Updating network policy to allow api.mem0.ai and telemetry"
|
||||
|
||||
# Find baseline policy
|
||||
BASELINE_POLICY=$(run_cmd "find / -path '*/nemoclaw-blueprint/policies/openclaw-sandbox.yaml' 2>/dev/null | head -1" || true)
|
||||
|
||||
if [[ -z "$BASELINE_POLICY" ]]; then
|
||||
die "Cannot find NemoClaw baseline policy file. Is NemoClaw installed?"
|
||||
fi
|
||||
|
||||
info "Baseline policy: $BASELINE_POLICY"
|
||||
|
||||
# Check if mem0_api already exists
|
||||
HAS_MEM0=$(run_cmd "grep -c mem0_api '$BASELINE_POLICY' 2>/dev/null" || echo "0")
|
||||
|
||||
if [[ "$HAS_MEM0" != "0" ]]; then
|
||||
success "mem0_api already in baseline policy"
|
||||
else
|
||||
info "Adding api.mem0.ai to network policy..."
|
||||
fi
|
||||
|
||||
# Create custom policy with mem0_api + telemetry
|
||||
run_cmd "node -e \"
|
||||
const fs = require('fs');
|
||||
let c = fs.readFileSync('$BASELINE_POLICY', 'utf8');
|
||||
const mem0Block = '\\n mem0_api:\\n name: mem0_api\\n endpoints:\\n - host: api.mem0.ai\\n port: 443\\n access: full\\n binaries:\\n - { path: /usr/local/bin/node }\\n - { path: /usr/local/bin/openclaw }\\n';
|
||||
const telemetryBlock = '\\n mem0_telemetry:\\n name: mem0_telemetry\\n endpoints:\\n - host: us.i.posthog.com\\n port: 443\\n access: full\\n binaries:\\n - { path: /usr/local/bin/node }\\n - { path: /usr/local/bin/openclaw }\\n';
|
||||
if (!c.includes('mem0_api')) {
|
||||
if (c.includes('# ── Messaging')) {
|
||||
c = c.replace(' # ── Messaging', mem0Block + '\\n # ── Messaging');
|
||||
} else {
|
||||
c += mem0Block;
|
||||
}
|
||||
}
|
||||
if (!c.includes('mem0_telemetry')) {
|
||||
if (c.includes('mem0_api:')) {
|
||||
c = c.replace(' mem0_api:', telemetryBlock + '\\n mem0_api:');
|
||||
} else {
|
||||
c += telemetryBlock;
|
||||
}
|
||||
}
|
||||
fs.writeFileSync('/tmp/nemoclaw-mem0-policy.yaml', c);
|
||||
console.log('ok');
|
||||
\""
|
||||
|
||||
success "Custom policy file created"
|
||||
|
||||
# Apply the policy
|
||||
info "Applying network policy..."
|
||||
if ! run_cmd "openshell policy set '$SANDBOX_NAME' --policy /tmp/nemoclaw-mem0-policy.yaml --wait 2>&1"; then
|
||||
error "Failed to apply network policy."
|
||||
echo ""
|
||||
echo " If you see 'sandbox not found', re-run: nemoclaw onboard"
|
||||
echo " Then re-run this script."
|
||||
echo ""
|
||||
die "Network policy update failed."
|
||||
fi
|
||||
|
||||
success "Network policy applied — api.mem0.ai and telemetry allowed"
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# STEP 3: Configure Plugin
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
step "Step 3: Configuring Mem0 plugin"
|
||||
|
||||
echo ""
|
||||
echo -e " ${DIM}Get your Mem0 API key from: https://app.mem0.ai${NC}"
|
||||
echo -e " ${DIM}The key starts with 'm0-'${NC}"
|
||||
echo ""
|
||||
ask "Enter your Mem0 API key: "
|
||||
read -r MEM0_API_KEY
|
||||
|
||||
if [[ -z "$MEM0_API_KEY" ]]; then
|
||||
die "Mem0 API key is required."
|
||||
fi
|
||||
|
||||
if [[ ! "$MEM0_API_KEY" =~ ^m0- ]]; then
|
||||
warn "Key doesn't start with 'm0-'. Make sure this is correct."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${DIM}The user ID scopes all memories. Pick any unique identifier.${NC}"
|
||||
echo -e " ${DIM}Examples: alice, user_123, your-email@example.com${NC}"
|
||||
echo ""
|
||||
ask "Enter user ID [$MEM0_USER_ID]: "
|
||||
read -r custom_user_id
|
||||
MEM0_USER_ID="${custom_user_id:-$MEM0_USER_ID}"
|
||||
|
||||
info "Configuring plugin inside sandbox..."
|
||||
|
||||
CONFIG_SCRIPT="openclaw config set plugins.slots.memory openclaw-mem0 2>&1 | tail -1 && \
|
||||
openclaw config set plugins.entries.openclaw-mem0.enabled true 2>&1 | tail -1 && \
|
||||
openclaw config set plugins.entries.openclaw-mem0.config.apiKey '$MEM0_API_KEY' 2>&1 | tail -1 && \
|
||||
openclaw config set plugins.entries.openclaw-mem0.config.userId '$MEM0_USER_ID' 2>&1 | tail -1 && \
|
||||
echo SETUP_DONE"
|
||||
|
||||
CONFIG_OUTPUT=$(sandbox_exec "$CONFIG_SCRIPT" || true)
|
||||
|
||||
if echo "$CONFIG_OUTPUT" | grep -q "SETUP_DONE"; then
|
||||
success "Plugin configured (mode: platform, user: $MEM0_USER_ID)"
|
||||
else
|
||||
warn "Could not verify config. You may need to configure manually:"
|
||||
echo ""
|
||||
if $IS_MACOS; then
|
||||
echo " docker exec -it $CONTAINER_NAME bash"
|
||||
echo " export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh"
|
||||
fi
|
||||
echo " nemoclaw $SANDBOX_NAME connect"
|
||||
echo " openclaw config set plugins.slots.memory openclaw-mem0"
|
||||
echo " openclaw config set plugins.entries.openclaw-mem0.enabled true"
|
||||
echo " openclaw config set plugins.entries.openclaw-mem0.config.apiKey \"$MEM0_API_KEY\""
|
||||
echo " openclaw config set plugins.entries.openclaw-mem0.config.userId \"$MEM0_USER_ID\""
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# Done
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
step "Verification"
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}${GREEN}║ Setup Complete! ║${NC}"
|
||||
echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Sandbox:${NC} $SANDBOX_NAME"
|
||||
echo -e " ${BOLD}Plugin:${NC} @mem0/openclaw-mem0 (platform mode)"
|
||||
echo -e " ${BOLD}User ID:${NC} $MEM0_USER_ID"
|
||||
if $IS_MACOS; then
|
||||
echo -e " ${BOLD}Container:${NC} $CONTAINER_NAME"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${BOLD}${CYAN}Next steps:${NC}"
|
||||
echo ""
|
||||
|
||||
if $IS_MACOS; then
|
||||
echo -e " 1. Open a shell in the container:"
|
||||
echo ""
|
||||
echo -e " ${DIM}docker exec -it $CONTAINER_NAME bash${NC}"
|
||||
echo -e " ${DIM}export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh${NC}"
|
||||
echo ""
|
||||
echo -e " 2. Connect to the sandbox and start the gateway:"
|
||||
echo ""
|
||||
echo -e " ${DIM}nemoclaw $SANDBOX_NAME connect${NC}"
|
||||
echo -e " ${DIM}nemoclaw-start${NC}"
|
||||
else
|
||||
echo -e " 1. Connect to the sandbox and start the gateway:"
|
||||
echo ""
|
||||
echo -e " ${DIM}source ~/.bashrc${NC}"
|
||||
echo -e " ${DIM}nemoclaw $SANDBOX_NAME connect${NC}"
|
||||
echo -e " ${DIM}nemoclaw-start${NC}"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " Then verify the plugin loaded (look for 'openclaw-mem0: registered'):"
|
||||
echo ""
|
||||
echo -e " ${DIM}openclaw plugins list${NC}"
|
||||
echo ""
|
||||
echo -e " Test auto-capture (storing memories):"
|
||||
echo ""
|
||||
echo -e " ${DIM}openclaw agent --agent main --local -m \"My name is Alice\" --session-id test1${NC}"
|
||||
echo ""
|
||||
echo -e " Test auto-recall (new session, memories should appear):"
|
||||
echo ""
|
||||
echo -e " ${DIM}openclaw agent --agent main --local -m \"What do you know about me?\" --session-id test2${NC}"
|
||||
echo ""
|
||||
echo -e " Or use the interactive TUI:"
|
||||
echo ""
|
||||
echo -e " ${DIM}openclaw tui${NC}"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Note:${NC} You may see 'Telemetry event capture failed' errors."
|
||||
echo -e " These are harmless and do not affect memory functionality."
|
||||
echo ""
|
||||
echo -e " ${BOLD}Documentation:${NC} https://docs.mem0.ai"
|
||||
echo -e " ${BOLD}Plugin source:${NC} https://www.npmjs.com/package/@mem0/openclaw-mem0"
|
||||
echo ""
|
||||
@@ -0,0 +1,272 @@
|
||||
# Mem0 Plugin for NemoClaw — Quickstart
|
||||
|
||||
Add persistent long-term memory to your [NemoClaw](https://docs.nvidia.com/nemoclaw/latest/get-started/quickstart.html) OpenClaw agent using the `@mem0/openclaw-mem0` plugin.
|
||||
|
||||
> **Note:** This plugin requires **Mem0 Platform mode** (i.e., a Mem0 API key from [app.mem0.ai](https://app.mem0.ai)). Open-source mode is not supported in NemoClaw sandboxes because the sandbox proxy blocks `/v1/embeddings` requests required by the open-source backend. See [Known Limitations](#known-limitations) for details.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Resource | Recommended | Minimum |
|
||||
|----------|-------------|---------|
|
||||
| CPU | 4+ vCPU | 2 vCPU |
|
||||
| RAM | 16 GB | 8 GB |
|
||||
| Disk | 40 GB free | 20 GB free |
|
||||
|
||||
**Accounts required:**
|
||||
|
||||
- **NVIDIA** — sign up at [build.nvidia.com](https://build.nvidia.com), generate an API key at [build.nvidia.com/settings/api-keys](https://build.nvidia.com/settings/api-keys) (starts with `nvapi-`)
|
||||
- **Mem0** — sign up at [app.mem0.ai](https://app.mem0.ai), generate an API key from the dashboard (starts with `m0-`)
|
||||
|
||||
**Supported platforms:** Ubuntu 22.04+, macOS (via Docker), Windows (WSL 2 + Docker)
|
||||
|
||||
---
|
||||
|
||||
## Choose Your Path
|
||||
|
||||
### Option A: Full Setup (NemoClaw + Mem0 Plugin)
|
||||
|
||||
Use this if you **don't have NemoClaw installed yet**. The script handles everything: Docker, Node.js, NemoClaw installation, onboarding, Mem0 plugin installation, network policy, and configuration.
|
||||
|
||||
```bash
|
||||
# Download
|
||||
curl -fsSL https://raw.githubusercontent.com/mem0ai/mem0/main/examples/nemoclaw/setup-mem0-nemoclaw.sh -o setup-mem0-nemoclaw.sh
|
||||
|
||||
# Run
|
||||
chmod +x setup-mem0-nemoclaw.sh
|
||||
./setup-mem0-nemoclaw.sh
|
||||
```
|
||||
|
||||
The script runs through 7 phases:
|
||||
|
||||
| Phase | What it does | User input |
|
||||
|-------|-------------|------------|
|
||||
| 1 | Prerequisites (Docker, RAM, disk) | None (automatic) |
|
||||
| 2 | Install NemoClaw | None (automatic) |
|
||||
| 3 | NemoClaw onboarding (sandbox + k3s) | Sandbox name, NVIDIA API key |
|
||||
| 4 | Install Mem0 plugin into sandbox | None (automatic) |
|
||||
| 5 | Update network policy for `api.mem0.ai` | None (automatic) |
|
||||
| 6 | Configure plugin | Mem0 API key, user ID |
|
||||
| 7 | Verification | None |
|
||||
|
||||
### Option B: Plugin Only (NemoClaw Already Installed)
|
||||
|
||||
Use this if you **already have NemoClaw installed and onboarded** with a sandbox in `Ready` state.
|
||||
|
||||
```bash
|
||||
# Download
|
||||
curl -fsSL https://raw.githubusercontent.com/mem0ai/mem0/main/examples/nemoclaw/install-mem0-plugin.sh -o install-mem0-plugin.sh
|
||||
|
||||
# Run
|
||||
chmod +x install-mem0-plugin.sh
|
||||
./install-mem0-plugin.sh
|
||||
```
|
||||
|
||||
The script auto-detects your sandbox and runs 3 steps:
|
||||
|
||||
| Step | What it does | User input |
|
||||
|------|-------------|------------|
|
||||
| 1 | Install Mem0 plugin into sandbox | None (automatic) |
|
||||
| 2 | Update network policy for `api.mem0.ai` | None (automatic) |
|
||||
| 3 | Configure plugin | Mem0 API key, user ID |
|
||||
|
||||
---
|
||||
|
||||
## Verify the Installation
|
||||
|
||||
After either script completes, connect to the sandbox and start the gateway:
|
||||
|
||||
```bash
|
||||
source ~/.bashrc
|
||||
nemoclaw <sandbox-name> connect
|
||||
nemoclaw-start
|
||||
```
|
||||
|
||||
Look for this line in the startup output:
|
||||
|
||||
```
|
||||
openclaw-mem0: registered (mode: platform, user: your-user-id, graph: false, autoRecall: true, autoCapture: true)
|
||||
```
|
||||
|
||||
You can also verify with:
|
||||
|
||||
```bash
|
||||
openclaw plugins list
|
||||
```
|
||||
|
||||
The `Memory (Mem0)` plugin should show status **loaded**.
|
||||
|
||||
## Test the Integration
|
||||
|
||||
All test commands run **inside the sandbox**.
|
||||
|
||||
### Test 1: Auto-capture (storing memories)
|
||||
|
||||
```bash
|
||||
openclaw agent --agent main --local \
|
||||
-m "My name is Alice and I work on distributed systems" \
|
||||
--session-id test1
|
||||
```
|
||||
|
||||
Look for: `openclaw-mem0: auto-captured 1 memories`
|
||||
|
||||
### Test 2: Auto-recall (retrieving memories across sessions)
|
||||
|
||||
Start a **new session** (different `--session-id`):
|
||||
|
||||
```bash
|
||||
openclaw agent --agent main --local \
|
||||
-m "What do you know about me?" \
|
||||
--session-id test2
|
||||
```
|
||||
|
||||
Look for: `openclaw-mem0: injecting 1 memories into context (1 long-term, 0 session)`
|
||||
|
||||
The agent should respond with information from the previous session ("Alice", "distributed systems").
|
||||
|
||||
### Test 3: Interactive TUI
|
||||
|
||||
```bash
|
||||
openclaw tui
|
||||
```
|
||||
|
||||
Send messages and the plugin will automatically capture and recall memories in the background.
|
||||
|
||||
---
|
||||
|
||||
## Plugin Configuration Reference
|
||||
|
||||
All options are set inside the sandbox via:
|
||||
|
||||
```bash
|
||||
openclaw config set plugins.entries.openclaw-mem0.config.<key> <value>
|
||||
```
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Backend mode |
|
||||
| `apiKey` | string | — | Mem0 API key (starts with `m0-`) |
|
||||
| `userId` | string | `"default"` | Unique identifier for the user |
|
||||
| `autoRecall` | boolean | `true` | Inject memories before each agent turn |
|
||||
| `autoCapture` | boolean | `true` | Store facts after each agent turn |
|
||||
| `topK` | number | `5` | Max memories per recall |
|
||||
| `searchThreshold` | number | `0.3` | Min similarity score (0-1) |
|
||||
| `orgId` | string | — | Mem0 organization ID |
|
||||
| `projectId` | string | — | Mem0 project ID |
|
||||
| `enableGraph` | boolean | `false` | Enable entity graph for relationships |
|
||||
| `customInstructions` | string | — | Rules for what Mem0 should store/exclude |
|
||||
|
||||
## Agent Memory Tools
|
||||
|
||||
Once the plugin is active, the agent can use these tools during conversations:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `memory_search` | Search memories by natural language |
|
||||
| `memory_list` | List all stored memories for a user |
|
||||
| `memory_store` | Explicitly save a fact |
|
||||
| `memory_get` | Retrieve a memory by ID |
|
||||
| `memory_forget` | Delete by ID or by query |
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Run these inside the sandbox:
|
||||
|
||||
```bash
|
||||
# Search all memories (long-term + session)
|
||||
openclaw mem0 search "what languages does the user know"
|
||||
|
||||
# Search only long-term memories
|
||||
openclaw mem0 search "user preferences" --scope long-term
|
||||
|
||||
# Search only session memories
|
||||
openclaw mem0 search "current task" --scope session
|
||||
|
||||
# Memory stats
|
||||
openclaw mem0 stats
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For detailed troubleshooting steps, see the [troubleshooting guide](troubleshooting-guide.pdf) included in this directory.
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### `npm tar TAR_ENTRY_ERROR ENOENT` during NemoClaw installation
|
||||
|
||||
This is a known npm bug where concurrent tar extraction races cause `ENOENT` errors on deeply nested packages. The `setup-mem0-nemoclaw.sh` script works around this by cloning the NemoClaw repo and installing with `--maxsockets=1` to serialize downloads. If you installed NemoClaw manually and hit this error:
|
||||
|
||||
```bash
|
||||
git clone --depth 1 https://github.com/NVIDIA/NemoClaw.git ~/.nemoclaw-src
|
||||
cd ~/.nemoclaw-src
|
||||
npm install --maxsockets=1
|
||||
npm link
|
||||
```
|
||||
|
||||
#### `sandbox not found` during onboarding step 7
|
||||
|
||||
The gateway restarted during onboarding and lost the sandbox state. Re-run `nemoclaw onboard`. When prompted that the sandbox already exists, choose `y` to recreate it.
|
||||
|
||||
#### `npm error 403 Forbidden` when installing plugin inside sandbox
|
||||
|
||||
The OpenShell gateway's TLS proxy blocks scoped npm packages (the `%2f` in the URL). Use the manual installation method (Method 2 in the scripts) which downloads outside the sandbox and uploads the tarball.
|
||||
|
||||
#### `capture failed: Connection error` or `recall failed: Connection error`
|
||||
|
||||
The network policy is not applied or missing the `mem0_api` entry. Re-run the plugin install script or manually apply the policy:
|
||||
|
||||
```bash
|
||||
openshell policy set <sandbox-name> --policy /tmp/nemoclaw-mem0-policy.yaml --wait
|
||||
```
|
||||
|
||||
#### `Telemetry event capture failed: TypeError: fetch failed`
|
||||
|
||||
This is harmless. The Mem0 SDK's telemetry endpoint (`us.i.posthog.com`) is blocked by the sandbox proxy. It does not affect memory functionality.
|
||||
|
||||
#### Plugin shows `disabled` in `openclaw plugins list`
|
||||
|
||||
The memory slot is not set to `openclaw-mem0`. Inside the sandbox, run:
|
||||
|
||||
```bash
|
||||
openclaw config set plugins.slots.memory openclaw-mem0
|
||||
```
|
||||
|
||||
#### `K8s namespace not ready` on Ubuntu 24.04
|
||||
|
||||
Ubuntu 24.04 defaults to cgroup v2 which causes k3s (used by NemoClaw) to fail. Apply the cgroup fix:
|
||||
|
||||
```bash
|
||||
sudo python3 -c "
|
||||
import json, os
|
||||
p = '/etc/docker/daemon.json'
|
||||
c = json.load(open(p)) if os.path.exists(p) else {}
|
||||
c['default-cgroupns-mode'] = 'host'
|
||||
json.dump(c, open(p, 'w'), indent=2)
|
||||
"
|
||||
sudo systemctl restart docker
|
||||
```
|
||||
|
||||
Then re-run `nemoclaw onboard`.
|
||||
|
||||
### Known Limitations
|
||||
|
||||
**Open-source mode is not supported in NemoClaw sandboxes.** The Mem0 open-source mode requires calling `/v1/embeddings` on an external LLM provider. NemoClaw's sandbox proxy intercepts all OpenAI-compatible API requests but only allows `/v1/chat/completions` through. Use **platform mode** instead.
|
||||
|
||||
---
|
||||
|
||||
## Files in This Directory
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `setup-mem0-nemoclaw.sh` | Full setup script (NemoClaw + Mem0 plugin) |
|
||||
| `install-mem0-plugin.sh` | Plugin-only install script (NemoClaw already set up) |
|
||||
| `troubleshooting-guide.pdf` | Detailed setup and troubleshooting guide |
|
||||
| `quickstart.md` | This file |
|
||||
|
||||
## Links
|
||||
|
||||
- [Mem0 Documentation](https://docs.mem0.ai)
|
||||
- [NemoClaw Documentation](https://docs.nvidia.com/nemoclaw/latest/get-started/quickstart.html)
|
||||
- [`@mem0/openclaw-mem0` on npm](https://www.npmjs.com/package/@mem0/openclaw-mem0)
|
||||
- [Mem0 Dashboard](https://app.mem0.ai)
|
||||
Executable
+864
@@ -0,0 +1,864 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# setup-mem0-nemoclaw.sh
|
||||
#
|
||||
# One-script setup for NemoClaw + Mem0 OpenClaw plugin.
|
||||
# Supports Ubuntu servers (native) and macOS (via Docker container).
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://raw.githubusercontent.com/mem0ai/mem0/main/scripts/setup-mem0-nemoclaw.sh | bash
|
||||
# # or
|
||||
# chmod +x setup-mem0-nemoclaw.sh && ./setup-mem0-nemoclaw.sh
|
||||
#
|
||||
# Requirements:
|
||||
# - Ubuntu 22.04+ OR macOS with Docker Desktop OR Windows with WSL 2 + Docker Desktop
|
||||
# - 8 GB RAM minimum (16 GB recommended)
|
||||
# - 40 GB free disk (20 GB minimum)
|
||||
# - NVIDIA API key (from build.nvidia.com)
|
||||
# - Mem0 API key (from app.mem0.ai)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colors and formatting ────────────────────────────────────────────────────
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
DIM='\033[2m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||
success() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
step() { echo -e "\n${BOLD}${CYAN}── $* ──${NC}\n"; }
|
||||
ask() { echo -en "${BOLD}$*${NC}"; }
|
||||
|
||||
die() {
|
||||
error "$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── Defaults ─────────────────────────────────────────────────────────────────
|
||||
|
||||
SANDBOX_NAME="${SANDBOX_NAME:-my-assistant}"
|
||||
MEM0_USER_ID="${MEM0_USER_ID:-default}"
|
||||
MIN_RAM_MB=6000
|
||||
MIN_DISK_MB=15000
|
||||
PLUGIN_PKG="@mem0/openclaw-mem0"
|
||||
CONTAINER_NAME="nemoclaw-dev"
|
||||
|
||||
# ── Detect platform ─────────────────────────────────────────────────────────
|
||||
|
||||
OS_TYPE="$(uname -s)"
|
||||
IS_MACOS=false
|
||||
IS_LINUX=false
|
||||
IS_WSL=false
|
||||
|
||||
case "$OS_TYPE" in
|
||||
Darwin) IS_MACOS=true ;;
|
||||
Linux)
|
||||
IS_LINUX=true
|
||||
# Detect WSL (Windows Subsystem for Linux)
|
||||
if grep -qi "microsoft\|wsl" /proc/version 2>/dev/null; then
|
||||
IS_WSL=true
|
||||
fi
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
error "This script cannot run in Git Bash, MSYS2, or Cygwin."
|
||||
echo ""
|
||||
echo " NemoClaw requires a full Linux environment. On Windows, use WSL 2:"
|
||||
echo ""
|
||||
echo " 1. Open PowerShell as Administrator and run:"
|
||||
echo " wsl --install -d Ubuntu-24.04"
|
||||
echo ""
|
||||
echo " 2. Restart your computer when prompted"
|
||||
echo ""
|
||||
echo " 3. Open 'Ubuntu' from the Start menu (this opens a WSL 2 shell)"
|
||||
echo ""
|
||||
echo " 4. Install Docker Desktop for Windows:"
|
||||
echo " https://www.docker.com/products/docker-desktop/"
|
||||
echo " Enable 'Use the WSL 2 based engine' in Docker Desktop settings"
|
||||
echo " Enable 'Ubuntu-24.04' under Resources → WSL Integration"
|
||||
echo ""
|
||||
echo " 5. In the Ubuntu WSL 2 shell, re-run this script:"
|
||||
echo " curl -fsSL https://raw.githubusercontent.com/mem0ai/mem0/main/scripts/setup-mem0-nemoclaw.sh | bash"
|
||||
echo ""
|
||||
die "Please use WSL 2 instead."
|
||||
;;
|
||||
*)
|
||||
die "Unsupported OS: $OS_TYPE. This script supports Linux (Ubuntu), macOS, and Windows (WSL 2)."
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Helper functions ─────────────────────────────────────────────────────────
|
||||
|
||||
check_command() {
|
||||
command -v "$1" &>/dev/null
|
||||
}
|
||||
|
||||
get_ram_mb() {
|
||||
if $IS_MACOS; then
|
||||
sysctl -n hw.memsize 2>/dev/null | awk '{printf "%.0f", $1/1024/1024}' || echo "0"
|
||||
else
|
||||
free -m 2>/dev/null | awk '/^Mem:/ {print $2}' || echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
get_disk_mb() {
|
||||
if $IS_MACOS; then
|
||||
df -m / 2>/dev/null | awk 'NR==2 {print $4}' || echo "0"
|
||||
else
|
||||
df -m / 2>/dev/null | awk 'NR==2 {print $4}' || echo "0"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_nvm() {
|
||||
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
||||
if [[ -s "$NVM_DIR/nvm.sh" ]]; then
|
||||
source "$NVM_DIR/nvm.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_path() {
|
||||
for p in "$HOME/.local/bin" "$HOME/.nvm/versions/node/"*/bin; do
|
||||
if [[ -d "$p" ]] && [[ ":$PATH:" != *":$p:"* ]]; then
|
||||
export PATH="$p:$PATH"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
wait_for_sandbox() {
|
||||
local name="$1"
|
||||
local timeout=120
|
||||
local elapsed=0
|
||||
while (( elapsed < timeout )); do
|
||||
local phase
|
||||
phase=$($RUN_CMD openshell sandbox get "$name" 2>/dev/null | grep -i "phase" | awk '{print $NF}' || true)
|
||||
if [[ "$phase" == "Ready" ]]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 5
|
||||
elapsed=$((elapsed + 5))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ── macOS: run command inside the container ──────────────────────────────────
|
||||
# On macOS, NemoClaw runs inside a Docker container. All nemoclaw/openshell
|
||||
# commands must be exec'd into the container. On Linux, they run natively.
|
||||
|
||||
setup_run_cmd() {
|
||||
if $IS_MACOS; then
|
||||
RUN_CMD="docker exec $CONTAINER_NAME bash -c 'export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh && "
|
||||
RUN_CMD_SUFFIX="'"
|
||||
RUN_CMD_IT="docker exec -it $CONTAINER_NAME bash -c 'export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh && "
|
||||
RUN_CMD_IT_SUFFIX="'"
|
||||
else
|
||||
RUN_CMD=""
|
||||
RUN_CMD_SUFFIX=""
|
||||
RUN_CMD_IT=""
|
||||
RUN_CMD_IT_SUFFIX=""
|
||||
fi
|
||||
}
|
||||
|
||||
# Wrapper to run a command natively (Linux) or in the container (macOS)
|
||||
run_cmd() {
|
||||
if $IS_MACOS; then
|
||||
docker exec "$CONTAINER_NAME" bash -c "export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh && $*"
|
||||
else
|
||||
eval "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
run_cmd_it() {
|
||||
if $IS_MACOS; then
|
||||
docker exec -it "$CONTAINER_NAME" bash -c "export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh && $*"
|
||||
else
|
||||
eval "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Banner ───────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}${CYAN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}${CYAN}║ Mem0 Plugin Setup for NemoClaw ║${NC}"
|
||||
echo -e "${BOLD}${CYAN}║ Long-term memory for your OpenClaw agent ║${NC}"
|
||||
echo -e "${BOLD}${CYAN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
if $IS_MACOS; then
|
||||
info "Platform: macOS (will use Docker container)"
|
||||
elif $IS_WSL; then
|
||||
info "Platform: Windows (WSL 2)"
|
||||
else
|
||||
info "Platform: Linux"
|
||||
fi
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE 1: Prerequisites
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
step "Phase 1: Checking prerequisites"
|
||||
|
||||
# ── OS check ─────────────────────────────────────────────────────────────────
|
||||
|
||||
if $IS_LINUX; then
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
source /etc/os-release
|
||||
if [[ "$ID" != "ubuntu" ]]; then
|
||||
warn "Detected OS: $PRETTY_NAME (not Ubuntu). This script is tested on Ubuntu 22.04+."
|
||||
ask "Continue anyway? [y/N]: "
|
||||
read -r cont
|
||||
[[ "$cont" =~ ^[Yy]$ ]] || exit 0
|
||||
else
|
||||
success "OS: $PRETTY_NAME"
|
||||
fi
|
||||
else
|
||||
warn "Cannot detect Linux distribution. Proceeding anyway."
|
||||
fi
|
||||
elif $IS_MACOS; then
|
||||
MACOS_VERSION=$(sw_vers -productVersion 2>/dev/null || echo "unknown")
|
||||
success "OS: macOS $MACOS_VERSION"
|
||||
fi
|
||||
|
||||
# ── RAM check ────────────────────────────────────────────────────────────────
|
||||
|
||||
RAM_MB=$(get_ram_mb)
|
||||
if (( RAM_MB < MIN_RAM_MB )); then
|
||||
error "Insufficient RAM: ${RAM_MB} MB available, ${MIN_RAM_MB} MB required."
|
||||
echo ""
|
||||
echo " NemoClaw needs at least 8 GB RAM (16 GB recommended)."
|
||||
if $IS_WSL; then
|
||||
echo ""
|
||||
echo " WSL 2 may have limited memory. Increase it:"
|
||||
echo " 1. Create/edit %USERPROFILE%\\.wslconfig in Windows"
|
||||
echo " 2. Add:"
|
||||
echo " [wsl2]"
|
||||
echo " memory=8GB"
|
||||
echo " 3. Restart WSL: wsl --shutdown (from PowerShell)"
|
||||
elif $IS_LINUX; then
|
||||
echo ""
|
||||
echo " If running on AWS EC2:"
|
||||
echo " 1. Stop the instance"
|
||||
echo " 2. Change instance type to t3.large (8 GB) or t3.xlarge (16 GB)"
|
||||
echo " 3. Start the instance and re-run this script"
|
||||
fi
|
||||
echo ""
|
||||
die "Aborting due to insufficient RAM."
|
||||
else
|
||||
success "RAM: ${RAM_MB} MB available"
|
||||
fi
|
||||
|
||||
# ── Disk check ───────────────────────────────────────────────────────────────
|
||||
|
||||
DISK_MB=$(get_disk_mb)
|
||||
if (( DISK_MB < MIN_DISK_MB )); then
|
||||
error "Insufficient disk space: ${DISK_MB} MB available, ${MIN_DISK_MB} MB required."
|
||||
echo ""
|
||||
echo " NemoClaw needs at least 20 GB free disk (40 GB recommended)."
|
||||
echo ""
|
||||
echo " Quick fix:"
|
||||
echo " docker system prune -a -f # Remove unused Docker data"
|
||||
if $IS_LINUX; then
|
||||
echo ""
|
||||
echo " If running on AWS EC2, expand the EBS volume:"
|
||||
echo " 1. Go to AWS Console → EC2 → Volumes"
|
||||
echo " 2. Select the volume, Actions → Modify Volume → increase size"
|
||||
echo " 3. Then run:"
|
||||
echo " sudo growpart /dev/xvda 1"
|
||||
echo " sudo resize2fs /dev/xvda1"
|
||||
fi
|
||||
echo ""
|
||||
die "Aborting due to insufficient disk space."
|
||||
else
|
||||
success "Disk: ${DISK_MB} MB available"
|
||||
fi
|
||||
|
||||
# ── Docker check ─────────────────────────────────────────────────────────────
|
||||
|
||||
if ! check_command docker; then
|
||||
if $IS_MACOS; then
|
||||
error "Docker not found."
|
||||
echo ""
|
||||
echo " Install Docker Desktop for macOS:"
|
||||
echo " https://www.docker.com/products/docker-desktop/"
|
||||
echo ""
|
||||
echo " After installing, start Docker Desktop and re-run this script."
|
||||
echo ""
|
||||
die "Docker Desktop is required on macOS."
|
||||
elif $IS_WSL; then
|
||||
error "Docker not found inside WSL."
|
||||
echo ""
|
||||
echo " Docker Desktop must be installed on Windows with WSL 2 integration enabled:"
|
||||
echo ""
|
||||
echo " 1. Install Docker Desktop for Windows:"
|
||||
echo " https://www.docker.com/products/docker-desktop/"
|
||||
echo ""
|
||||
echo " 2. Open Docker Desktop → Settings → General:"
|
||||
echo " ✓ Enable 'Use the WSL 2 based engine'"
|
||||
echo ""
|
||||
echo " 3. Open Docker Desktop → Settings → Resources → WSL Integration:"
|
||||
echo " ✓ Enable integration with your Ubuntu distribution"
|
||||
echo ""
|
||||
echo " 4. Click 'Apply & restart', then re-run this script in WSL."
|
||||
echo ""
|
||||
die "Docker Desktop WSL 2 integration is required."
|
||||
else
|
||||
info "Docker not found. Installing Docker..."
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq docker.io >/dev/null 2>&1
|
||||
sudo systemctl enable --now docker
|
||||
sudo usermod -aG docker "$USER"
|
||||
success "Docker installed"
|
||||
fi
|
||||
fi
|
||||
|
||||
if $IS_LINUX; then
|
||||
# Ensure Docker daemon is running
|
||||
if ! sudo docker info &>/dev/null; then
|
||||
info "Starting Docker daemon..."
|
||||
sudo systemctl start docker
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# Handle Docker group permissions
|
||||
if ! docker info &>/dev/null; then
|
||||
if id -nG "$USER" | grep -qw docker || grep -q "^docker:.*\b${USER}\b" /etc/group; then
|
||||
info "Activating docker group for current session..."
|
||||
exec sg docker -c "$0 $*"
|
||||
else
|
||||
info "Adding $USER to docker group..."
|
||||
sudo usermod -aG docker "$USER"
|
||||
info "Activating docker group for current session..."
|
||||
exec sg docker -c "$0 $*"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# macOS: just verify Docker is responding
|
||||
if ! docker info &>/dev/null; then
|
||||
error "Docker is not running."
|
||||
echo ""
|
||||
echo " Start Docker Desktop and wait for it to be ready, then re-run this script."
|
||||
echo ""
|
||||
die "Docker Desktop is not running."
|
||||
fi
|
||||
fi
|
||||
|
||||
success "Docker: running ($(docker --version | awk '{print $3}' | tr -d ','))"
|
||||
|
||||
# ── Linux-only: cgroup v2 fix for Ubuntu 24.04 ──────────────────────────────
|
||||
|
||||
if $IS_LINUX && [[ "${VERSION_ID:-}" == "24.04" ]]; then
|
||||
DAEMON_JSON="/etc/docker/daemon.json"
|
||||
NEEDS_CGROUP_FIX=false
|
||||
|
||||
if [[ ! -f "$DAEMON_JSON" ]]; then
|
||||
NEEDS_CGROUP_FIX=true
|
||||
elif ! grep -q '"default-cgroupns-mode"' "$DAEMON_JSON" 2>/dev/null; then
|
||||
NEEDS_CGROUP_FIX=true
|
||||
fi
|
||||
|
||||
if $NEEDS_CGROUP_FIX; then
|
||||
warn "Ubuntu 24.04 detected — applying cgroup v2 fix for Docker."
|
||||
warn "This prevents 'K8s namespace not ready' errors during onboarding."
|
||||
sudo python3 -c "
|
||||
import json, os
|
||||
p = '$DAEMON_JSON'
|
||||
c = json.load(open(p)) if os.path.exists(p) else {}
|
||||
c['default-cgroupns-mode'] = 'host'
|
||||
json.dump(c, open(p, 'w'), indent=2)
|
||||
"
|
||||
sudo systemctl restart docker
|
||||
success "Docker cgroup v2 fix applied"
|
||||
else
|
||||
success "Docker cgroup v2: already configured"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Linux-only: Swap check ──────────────────────────────────────────────────
|
||||
|
||||
if $IS_LINUX; then
|
||||
SWAP_MB=$(free -m 2>/dev/null | awk '/^Swap:/ {print $2}' || echo "0")
|
||||
if (( RAM_MB < 12000 && SWAP_MB < 2000 )); then
|
||||
warn "Low RAM (${RAM_MB} MB) and low swap. Adding 4 GB swap to prevent OOM kills."
|
||||
if [[ ! -f /swapfile ]]; then
|
||||
sudo fallocate -l 4G /swapfile
|
||||
sudo chmod 600 /swapfile
|
||||
sudo mkswap /swapfile >/dev/null
|
||||
sudo swapon /swapfile
|
||||
success "4 GB swap enabled"
|
||||
else
|
||||
success "Swap file already exists"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── macOS-only: Create Docker container ──────────────────────────────────────
|
||||
|
||||
if $IS_MACOS; then
|
||||
step "Phase 1b: Setting up NemoClaw Docker container"
|
||||
|
||||
if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||
success "Container '$CONTAINER_NAME' is already running"
|
||||
elif docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
|
||||
info "Starting existing container '$CONTAINER_NAME'..."
|
||||
docker start "$CONTAINER_NAME"
|
||||
success "Container started"
|
||||
else
|
||||
info "Creating Ubuntu container '$CONTAINER_NAME' for NemoClaw..."
|
||||
echo -e " ${DIM}This container runs NemoClaw with --network host and Docker socket access.${NC}"
|
||||
|
||||
docker run -d \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--privileged \
|
||||
--network host \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
ubuntu:24.04 sleep infinity
|
||||
|
||||
success "Container '$CONTAINER_NAME' created"
|
||||
|
||||
info "Installing dependencies inside container..."
|
||||
docker exec "$CONTAINER_NAME" bash -c "apt-get update -qq && apt-get install -y -qq curl git docker.io >/dev/null 2>&1"
|
||||
success "Dependencies installed"
|
||||
fi
|
||||
|
||||
# Check if NemoClaw is installed in the container
|
||||
NEMO_IN_CONTAINER=$(docker exec "$CONTAINER_NAME" bash -c "export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh 2>/dev/null && command -v nemoclaw" 2>/dev/null || true)
|
||||
if [[ -z "$NEMO_IN_CONTAINER" ]]; then
|
||||
info "Installing NemoClaw inside container (this takes a few minutes)..."
|
||||
# The NVIDIA installer triggers npm tar race conditions. Workaround: clone
|
||||
# and install with --maxsockets=1 to serialize downloads.
|
||||
docker exec -it "$CONTAINER_NAME" bash -c "
|
||||
export NVM_DIR=/root/.nvm &&
|
||||
if [ ! -s \"\$NVM_DIR/nvm.sh\" ]; then
|
||||
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash &&
|
||||
source \"\$NVM_DIR/nvm.sh\" &&
|
||||
nvm install 22
|
||||
else
|
||||
source \"\$NVM_DIR/nvm.sh\"
|
||||
fi &&
|
||||
git clone --depth 1 https://github.com/NVIDIA/NemoClaw.git /root/.nemoclaw-src &&
|
||||
cd /root/.nemoclaw-src &&
|
||||
npm install --maxsockets=1 &&
|
||||
npm link
|
||||
"
|
||||
success "NemoClaw installed in container"
|
||||
else
|
||||
success "NemoClaw already installed in container"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE 2: Install NemoClaw (Linux-only, macOS handled above)
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
if $IS_LINUX; then
|
||||
step "Phase 2: Installing NemoClaw"
|
||||
|
||||
ensure_nvm
|
||||
ensure_path
|
||||
|
||||
if check_command nemoclaw; then
|
||||
success "NemoClaw already installed: $(nemoclaw --version 2>/dev/null || echo 'found')"
|
||||
else
|
||||
info "Installing NemoClaw (this takes a few minutes)..."
|
||||
|
||||
# Install Node.js via nvm if not available
|
||||
if ! check_command node; then
|
||||
info "Node.js not found — installing via nvm..."
|
||||
export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
|
||||
curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
|
||||
source "$NVM_DIR/nvm.sh"
|
||||
nvm install 22
|
||||
ensure_nvm
|
||||
ensure_path
|
||||
fi
|
||||
|
||||
# The NVIDIA installer (curl | bash) triggers npm tar race conditions
|
||||
# causing ENOENT errors on deeply nested packages. Workaround: clone the
|
||||
# repo and install with --maxsockets=1 to serialize downloads.
|
||||
NEMOCLAW_DIR="$HOME/.nemoclaw-src"
|
||||
rm -rf "$NEMOCLAW_DIR"
|
||||
info "Cloning NemoClaw from GitHub..."
|
||||
git clone --depth 1 https://github.com/NVIDIA/NemoClaw.git "$NEMOCLAW_DIR"
|
||||
cd "$NEMOCLAW_DIR"
|
||||
info "Installing dependencies (serialized to avoid tar race)..."
|
||||
npm install --maxsockets=1
|
||||
npm link
|
||||
cd - >/dev/null
|
||||
|
||||
ensure_nvm
|
||||
ensure_path
|
||||
source "$HOME/.bashrc" 2>/dev/null || true
|
||||
hash -r 2>/dev/null || true
|
||||
|
||||
if ! check_command nemoclaw; then
|
||||
# npm link may place the binary outside the current PATH; find and add it
|
||||
NEMOCLAW_BIN=$(find "$HOME/.nvm" -name nemoclaw \( -type f -o -type l \) -path "*/bin/*" 2>/dev/null | head -1)
|
||||
if [[ -n "$NEMOCLAW_BIN" ]]; then
|
||||
export PATH="$(dirname "$NEMOCLAW_BIN"):$PATH"
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! check_command nemoclaw; then
|
||||
die "NemoClaw installation failed. Try: source ~/.bashrc && nemoclaw --help"
|
||||
fi
|
||||
|
||||
success "NemoClaw installed"
|
||||
fi
|
||||
|
||||
ensure_path
|
||||
if ! check_command openshell; then
|
||||
warn "openshell not on PATH yet — it will be installed during onboarding."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE 3: NemoClaw Onboarding
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
step "Phase 3: NemoClaw Onboarding"
|
||||
|
||||
# Check if a sandbox already exists
|
||||
EXISTING_SANDBOX=""
|
||||
if $IS_MACOS; then
|
||||
EXISTING_SANDBOX=$(run_cmd "openshell sandbox list 2>/dev/null" | awk 'NR>1 && $1!="" {print $1; exit}' || true)
|
||||
else
|
||||
ensure_path
|
||||
if check_command openshell; then
|
||||
EXISTING_SANDBOX=$(openshell sandbox list 2>/dev/null | awk 'NR>1 && $1!="" {print $1; exit}' || true)
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$EXISTING_SANDBOX" ]]; then
|
||||
success "Sandbox already exists: $EXISTING_SANDBOX"
|
||||
SANDBOX_NAME="$EXISTING_SANDBOX"
|
||||
ask "Use existing sandbox '$SANDBOX_NAME'? [Y/n]: "
|
||||
read -r use_existing
|
||||
if [[ "$use_existing" =~ ^[Nn]$ ]]; then
|
||||
ask "Enter sandbox name [my-assistant]: "
|
||||
read -r custom_name
|
||||
SANDBOX_NAME="${custom_name:-my-assistant}"
|
||||
info "Running NemoClaw onboarding..."
|
||||
echo -e " ${DIM}You'll need your NVIDIA API key (nvapi-...) from build.nvidia.com${NC}"
|
||||
echo ""
|
||||
run_cmd_it "nemoclaw onboard"
|
||||
if $IS_LINUX; then ensure_path; fi
|
||||
fi
|
||||
else
|
||||
info "No existing sandbox found. Running NemoClaw onboarding..."
|
||||
echo ""
|
||||
echo -e " ${DIM}The onboarding wizard will guide you through 7 steps:${NC}"
|
||||
echo -e " ${DIM} 1. Preflight checks (automatic)${NC}"
|
||||
echo -e " ${DIM} 2. Start gateway (automatic, takes 1-2 min)${NC}"
|
||||
echo -e " ${DIM} 3. Sandbox name — enter a name (e.g. my-assistant)${NC}"
|
||||
echo -e " ${DIM} 4. NVIDIA API key — paste your nvapi-... key${NC}"
|
||||
echo -e " ${DIM} 5. Inference provider (automatic)${NC}"
|
||||
echo -e " ${DIM} 6. OpenClaw setup (automatic)${NC}"
|
||||
echo -e " ${DIM} 7. Policy presets — type Y to apply pypi and npm${NC}"
|
||||
echo ""
|
||||
ask "Press Enter to start onboarding..."
|
||||
read -r
|
||||
|
||||
run_cmd_it "nemoclaw onboard"
|
||||
|
||||
if $IS_LINUX; then
|
||||
ensure_nvm
|
||||
ensure_path
|
||||
fi
|
||||
|
||||
# Detect sandbox name
|
||||
SANDBOX_NAME=$(run_cmd "openshell sandbox list 2>/dev/null" | awk 'NR>1 && $1!="" {print $1; exit}' || echo "$SANDBOX_NAME")
|
||||
fi
|
||||
|
||||
# Verify sandbox exists
|
||||
SANDBOX_PHASE=$(run_cmd "openshell sandbox get '$SANDBOX_NAME' 2>/dev/null" | grep -i "phase" | awk '{print $NF}' || true)
|
||||
if [[ "$SANDBOX_PHASE" != "Ready" ]]; then
|
||||
warn "Sandbox '$SANDBOX_NAME' is not in Ready state (current: ${SANDBOX_PHASE:-unknown})."
|
||||
warn "Waiting up to 2 minutes..."
|
||||
WAIT_OK=false
|
||||
for i in $(seq 1 24); do
|
||||
SANDBOX_PHASE=$(run_cmd "openshell sandbox get '$SANDBOX_NAME' 2>/dev/null" | grep -i "phase" | awk '{print $NF}' || true)
|
||||
if [[ "$SANDBOX_PHASE" == "Ready" ]]; then
|
||||
WAIT_OK=true
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
if ! $WAIT_OK; then
|
||||
die "Sandbox '$SANDBOX_NAME' did not become Ready. Run: openshell sandbox list"
|
||||
fi
|
||||
fi
|
||||
|
||||
success "Sandbox '$SANDBOX_NAME' is ready"
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE 4: Install Mem0 Plugin
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
step "Phase 4: Installing Mem0 plugin ($PLUGIN_PKG)"
|
||||
|
||||
# Helper: run a command inside the sandbox non-interactively via piped stdin
|
||||
sandbox_exec() {
|
||||
local cmd="$1"
|
||||
if $IS_MACOS; then
|
||||
printf '%s\nexit\n' "$cmd" | docker exec -i "$CONTAINER_NAME" bash -c \
|
||||
"export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh && openshell sandbox connect '$SANDBOX_NAME'" 2>&1
|
||||
else
|
||||
printf '%s\nexit\n' "$cmd" | openshell sandbox connect "$SANDBOX_NAME" 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if plugin is already installed by trying to download a known file from the sandbox.
|
||||
# We avoid sandbox_exec for the check because piped stdin to openshell sandbox connect
|
||||
# produces shell prompt noise that causes false positives with grep.
|
||||
PLUGIN_EXISTS=false
|
||||
if run_cmd "openshell sandbox download '$SANDBOX_NAME' /sandbox/.openclaw/extensions/openclaw-mem0/package.json /tmp/_mem0_plugin_check.json" &>/dev/null; then
|
||||
if [[ -f /tmp/_mem0_plugin_check.json ]] || run_cmd "test -f /tmp/_mem0_plugin_check.json" &>/dev/null; then
|
||||
PLUGIN_EXISTS=true
|
||||
fi
|
||||
fi
|
||||
rm -f /tmp/_mem0_plugin_check.json 2>/dev/null || true
|
||||
run_cmd "rm -f /tmp/_mem0_plugin_check.json" 2>/dev/null || true
|
||||
|
||||
if $PLUGIN_EXISTS; then
|
||||
success "Mem0 plugin already installed in sandbox"
|
||||
else
|
||||
info "Downloading $PLUGIN_PKG..."
|
||||
|
||||
# Download and build outside the sandbox (on host or in container)
|
||||
run_cmd "cd /tmp && rm -rf openclaw-mem0-full mem0-openclaw-mem0-*.tgz openclaw-mem0-full.tgz"
|
||||
|
||||
if ! run_cmd "cd /tmp && npm pack '$PLUGIN_PKG' 2>/dev/null"; then
|
||||
die "Failed to download $PLUGIN_PKG from npm. Check your internet connection."
|
||||
fi
|
||||
|
||||
success "Downloaded plugin"
|
||||
|
||||
info "Installing plugin dependencies..."
|
||||
run_cmd "mkdir -p /tmp/openclaw-mem0-full && cd /tmp/openclaw-mem0-full && tar xzf /tmp/mem0-openclaw-mem0-*.tgz --strip-components=1 && npm install --omit=dev 2>&1 | tail -3"
|
||||
|
||||
success "Dependencies installed"
|
||||
|
||||
info "Uploading plugin to sandbox..."
|
||||
run_cmd "cd /tmp && tar czf openclaw-mem0-full.tgz -C openclaw-mem0-full ."
|
||||
|
||||
if ! run_cmd "openshell sandbox upload '$SANDBOX_NAME' /tmp/openclaw-mem0-full.tgz /sandbox/openclaw-mem0-full.tgz 2>&1"; then
|
||||
die "Failed to upload plugin to sandbox. Check: openshell sandbox list"
|
||||
fi
|
||||
|
||||
success "Plugin uploaded"
|
||||
|
||||
info "Extracting plugin inside sandbox..."
|
||||
sandbox_exec "mkdir -p ~/.openclaw/extensions/openclaw-mem0 && tar xzf /sandbox/openclaw-mem0-full.tgz/openclaw-mem0-full.tgz -C ~/.openclaw/extensions/openclaw-mem0 2>/dev/null || tar xzf /sandbox/openclaw-mem0-full.tgz -C ~/.openclaw/extensions/openclaw-mem0 2>/dev/null && echo EXTRACT_OK" >/dev/null 2>&1 || true
|
||||
|
||||
# Verify
|
||||
VERIFY_OK=false
|
||||
if run_cmd "openshell sandbox download '$SANDBOX_NAME' /sandbox/.openclaw/extensions/openclaw-mem0/package.json /tmp/_mem0_verify.json" &>/dev/null; then
|
||||
VERIFY_OK=true
|
||||
fi
|
||||
rm -f /tmp/_mem0_verify.json 2>/dev/null || true
|
||||
run_cmd "rm -f /tmp/_mem0_verify.json" 2>/dev/null || true
|
||||
if $VERIFY_OK; then
|
||||
success "Plugin extracted inside sandbox"
|
||||
else
|
||||
warn "Could not verify plugin extraction. You may need to extract manually."
|
||||
if $IS_MACOS; then
|
||||
echo " docker exec -it $CONTAINER_NAME bash"
|
||||
echo " export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh"
|
||||
fi
|
||||
echo " nemoclaw $SANDBOX_NAME connect"
|
||||
echo " mkdir -p ~/.openclaw/extensions/openclaw-mem0"
|
||||
echo " tar xzf /sandbox/openclaw-mem0-full.tgz/openclaw-mem0-full.tgz -C ~/.openclaw/extensions/openclaw-mem0"
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
run_cmd "rm -rf /tmp/openclaw-mem0-full /tmp/mem0-openclaw-mem0-*.tgz /tmp/openclaw-mem0-full.tgz" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE 5: Update Network Policy
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
step "Phase 5: Updating network policy to allow api.mem0.ai and telemetry"
|
||||
|
||||
# Find baseline policy
|
||||
BASELINE_POLICY=$(run_cmd "find / -path '*/nemoclaw-blueprint/policies/openclaw-sandbox.yaml' 2>/dev/null | head -1" || true)
|
||||
|
||||
if [[ -z "$BASELINE_POLICY" ]]; then
|
||||
die "Cannot find NemoClaw baseline policy file. Is NemoClaw installed?"
|
||||
fi
|
||||
|
||||
info "Baseline policy: $BASELINE_POLICY"
|
||||
|
||||
# Check if mem0_api already exists
|
||||
HAS_MEM0=$(run_cmd "grep -c mem0_api '$BASELINE_POLICY' 2>/dev/null" || echo "0")
|
||||
|
||||
if [[ "$HAS_MEM0" != "0" ]]; then
|
||||
success "mem0_api already in baseline policy"
|
||||
else
|
||||
info "Adding api.mem0.ai to network policy..."
|
||||
fi
|
||||
|
||||
# Create custom policy with mem0_api + telemetry — use node for reliable cross-platform YAML editing
|
||||
run_cmd "node -e \"
|
||||
const fs = require('fs');
|
||||
let c = fs.readFileSync('$BASELINE_POLICY', 'utf8');
|
||||
const mem0Block = '\\n mem0_api:\\n name: mem0_api\\n endpoints:\\n - host: api.mem0.ai\\n port: 443\\n access: full\\n binaries:\\n - { path: /usr/local/bin/node }\\n - { path: /usr/local/bin/openclaw }\\n';
|
||||
const telemetryBlock = '\\n mem0_telemetry:\\n name: mem0_telemetry\\n endpoints:\\n - host: us.i.posthog.com\\n port: 443\\n access: full\\n binaries:\\n - { path: /usr/local/bin/node }\\n - { path: /usr/local/bin/openclaw }\\n';
|
||||
if (!c.includes('mem0_api')) {
|
||||
if (c.includes('# ── Messaging')) {
|
||||
c = c.replace(' # ── Messaging', mem0Block + '\\n # ── Messaging');
|
||||
} else {
|
||||
c += mem0Block;
|
||||
}
|
||||
}
|
||||
if (!c.includes('mem0_telemetry')) {
|
||||
if (c.includes('mem0_api:')) {
|
||||
c = c.replace(' mem0_api:', telemetryBlock + '\\n mem0_api:');
|
||||
} else {
|
||||
c += telemetryBlock;
|
||||
}
|
||||
}
|
||||
fs.writeFileSync('/tmp/nemoclaw-mem0-policy.yaml', c);
|
||||
console.log('ok');
|
||||
\""
|
||||
|
||||
success "Custom policy file created"
|
||||
|
||||
# Apply the policy
|
||||
info "Applying network policy..."
|
||||
if ! run_cmd "openshell policy set '$SANDBOX_NAME' --policy /tmp/nemoclaw-mem0-policy.yaml --wait 2>&1"; then
|
||||
error "Failed to apply network policy."
|
||||
echo ""
|
||||
echo " If you see 'sandbox not found', re-run: nemoclaw onboard"
|
||||
echo " Then re-run this script."
|
||||
echo ""
|
||||
die "Network policy update failed."
|
||||
fi
|
||||
|
||||
success "Network policy applied — api.mem0.ai and telemetry allowed"
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE 6: Configure Plugin
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
step "Phase 6: Configuring Mem0 plugin"
|
||||
|
||||
echo ""
|
||||
echo -e " ${DIM}Get your Mem0 API key from: https://app.mem0.ai${NC}"
|
||||
echo -e " ${DIM}The key starts with 'm0-'${NC}"
|
||||
echo ""
|
||||
ask "Enter your Mem0 API key: "
|
||||
read -r MEM0_API_KEY
|
||||
|
||||
if [[ -z "$MEM0_API_KEY" ]]; then
|
||||
die "Mem0 API key is required."
|
||||
fi
|
||||
|
||||
if [[ ! "$MEM0_API_KEY" =~ ^m0- ]]; then
|
||||
warn "Key doesn't start with 'm0-'. Make sure this is correct."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e " ${DIM}The user ID scopes all memories. Pick any unique identifier.${NC}"
|
||||
echo -e " ${DIM}Examples: alice, user_123, your-email@example.com${NC}"
|
||||
echo ""
|
||||
ask "Enter user ID [$MEM0_USER_ID]: "
|
||||
read -r custom_user_id
|
||||
MEM0_USER_ID="${custom_user_id:-$MEM0_USER_ID}"
|
||||
|
||||
info "Configuring plugin inside sandbox..."
|
||||
|
||||
CONFIG_SCRIPT="openclaw config set plugins.slots.memory openclaw-mem0 2>&1 | tail -1 && \
|
||||
openclaw config set plugins.entries.openclaw-mem0.enabled true 2>&1 | tail -1 && \
|
||||
openclaw config set plugins.entries.openclaw-mem0.config.apiKey '$MEM0_API_KEY' 2>&1 | tail -1 && \
|
||||
openclaw config set plugins.entries.openclaw-mem0.config.userId '$MEM0_USER_ID' 2>&1 | tail -1 && \
|
||||
echo SETUP_DONE"
|
||||
|
||||
CONFIG_OUTPUT=$(sandbox_exec "$CONFIG_SCRIPT" || true)
|
||||
|
||||
if echo "$CONFIG_OUTPUT" | grep -q "SETUP_DONE"; then
|
||||
success "Plugin configured (mode: platform, user: $MEM0_USER_ID)"
|
||||
else
|
||||
warn "Could not verify config. You may need to configure manually:"
|
||||
echo ""
|
||||
if $IS_MACOS; then
|
||||
echo " docker exec -it $CONTAINER_NAME bash"
|
||||
echo " export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh"
|
||||
fi
|
||||
echo " nemoclaw $SANDBOX_NAME connect"
|
||||
echo " openclaw config set plugins.slots.memory openclaw-mem0"
|
||||
echo " openclaw config set plugins.entries.openclaw-mem0.enabled true"
|
||||
echo " openclaw config set plugins.entries.openclaw-mem0.config.apiKey \"$MEM0_API_KEY\""
|
||||
echo " openclaw config set plugins.entries.openclaw-mem0.config.userId \"$MEM0_USER_ID\""
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# PHASE 7: Verify & Test
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
step "Phase 7: Verification"
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BOLD}${GREEN}║ Setup Complete! ║${NC}"
|
||||
echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Sandbox:${NC} $SANDBOX_NAME"
|
||||
echo -e " ${BOLD}Plugin:${NC} @mem0/openclaw-mem0 (platform mode)"
|
||||
echo -e " ${BOLD}User ID:${NC} $MEM0_USER_ID"
|
||||
if $IS_MACOS; then
|
||||
echo -e " ${BOLD}Container:${NC} $CONTAINER_NAME"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " ${BOLD}${CYAN}Next steps:${NC}"
|
||||
echo ""
|
||||
|
||||
if $IS_MACOS; then
|
||||
echo -e " 1. Open a shell in the container:"
|
||||
echo ""
|
||||
echo -e " ${DIM}docker exec -it $CONTAINER_NAME bash${NC}"
|
||||
echo -e " ${DIM}export NVM_DIR=/root/.nvm && source /root/.nvm/nvm.sh${NC}"
|
||||
echo ""
|
||||
echo -e " 2. Connect to the sandbox and start the gateway:"
|
||||
echo ""
|
||||
echo -e " ${DIM}nemoclaw $SANDBOX_NAME connect${NC}"
|
||||
echo -e " ${DIM}nemoclaw-start${NC}"
|
||||
else
|
||||
echo -e " 1. Connect to the sandbox and start the gateway:"
|
||||
echo ""
|
||||
echo -e " ${DIM}source ~/.bashrc${NC}"
|
||||
echo -e " ${DIM}nemoclaw $SANDBOX_NAME connect${NC}"
|
||||
echo -e " ${DIM}nemoclaw-start${NC}"
|
||||
fi
|
||||
echo ""
|
||||
echo -e " Then verify the plugin loaded (look for 'openclaw-mem0: registered'):"
|
||||
echo ""
|
||||
echo -e " ${DIM}openclaw plugins list${NC}"
|
||||
echo ""
|
||||
echo -e " Test auto-capture (storing memories):"
|
||||
echo ""
|
||||
echo -e " ${DIM}openclaw agent --agent main --local -m \"My name is Alice\" --session-id test1${NC}"
|
||||
echo ""
|
||||
echo -e " Test auto-recall (new session, memories should appear):"
|
||||
echo ""
|
||||
echo -e " ${DIM}openclaw agent --agent main --local -m \"What do you know about me?\" --session-id test2${NC}"
|
||||
echo ""
|
||||
echo -e " Or use the interactive TUI:"
|
||||
echo ""
|
||||
echo -e " ${DIM}openclaw tui${NC}"
|
||||
echo ""
|
||||
echo -e " ${YELLOW}Note:${NC} You may see 'Telemetry event capture failed' errors."
|
||||
echo -e " These are harmless and do not affect memory functionality."
|
||||
echo ""
|
||||
echo -e " ${BOLD}Documentation:${NC} https://docs.mem0.ai"
|
||||
echo -e " ${BOLD}Plugin source:${NC} https://www.npmjs.com/package/@mem0/openclaw-mem0"
|
||||
echo ""
|
||||
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.1.0",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows using the Mem0 Platform MCP server.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
"email": "support@mem0.ai"
|
||||
},
|
||||
"homepage": "https://mem0.ai",
|
||||
"repository": "https://github.com/mem0ai/mem0",
|
||||
"logo": "logo.svg",
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${env:MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.1.0",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
"email": "support@mem0.ai"
|
||||
},
|
||||
"homepage": "https://mem0.ai",
|
||||
"repository": "https://github.com/mem0ai/mem0",
|
||||
"logo": "logo.svg",
|
||||
"license": "Apache-2.0",
|
||||
"keywords": ["mem0", "memory", "mcp", "personalization", "semantic-search"],
|
||||
"skills": "./skills/",
|
||||
"hooks": "./hooks/cursor-hooks.json",
|
||||
"mcpServers": ".cursor-mcp.json"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
# Mem0 Plugin for Claude Code, Claude Cowork & Cursor
|
||||
|
||||
Add persistent memory to your AI workflows. Store, retrieve, and manage memories across sessions using the Mem0 Platform. Works with **Claude Code** (CLI), **Claude Cowork** (desktop app), and **Cursor**.
|
||||
|
||||
## Step 1: Set your API key
|
||||
|
||||
> **You must complete this step before installing the plugin.**
|
||||
|
||||
1. Sign up at [app.mem0.ai](https://app.mem0.ai) if you haven't already
|
||||
2. Go to [app.mem0.ai/dashboard/api-keys](https://app.mem0.ai/dashboard/api-keys)
|
||||
3. Click **Create API Key** and copy the key (starts with `m0-`)
|
||||
4. Add it to your shell profile:
|
||||
|
||||
```bash
|
||||
# For zsh (default on macOS)
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
|
||||
# For bash
|
||||
echo 'export MEM0_API_KEY="m0-your-api-key"' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
5. Confirm it's set:
|
||||
|
||||
```bash
|
||||
echo $MEM0_API_KEY
|
||||
# Should print: m0-your-api-key
|
||||
```
|
||||
|
||||
## Step 2: Install the plugin
|
||||
|
||||
Choose one of the options below. All require `MEM0_API_KEY` to be set first (see above).
|
||||
|
||||
### Claude Code (CLI) / Claude Cowork (Desktop)
|
||||
|
||||
Claude Code and Claude Cowork share the same plugin system.
|
||||
|
||||
**CLI:**
|
||||
|
||||
```
|
||||
/plugin marketplace add mem0ai/mem0
|
||||
/plugin install mem0@mem0-plugins
|
||||
```
|
||||
|
||||
**Cowork desktop app:** Open the Cowork tab, click **Customize** in the sidebar, click **Browse plugins**, and install Mem0.
|
||||
|
||||
This installs the full plugin including the MCP server, lifecycle hooks (automatic memory capture), and the Mem0 SDK skill.
|
||||
|
||||
### Cursor
|
||||
|
||||
> **Already have `mem0` configured as an MCP server?** Remove the existing entry from your Cursor MCP settings before installing to avoid duplicate tools.
|
||||
|
||||
**Option A — One-click deeplink** (installs MCP server only):
|
||||
|
||||
[Install Mem0 MCP in Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=mem0&config=eyJtY3BTZXJ2ZXJzIjp7Im1lbTAiOnsidXJsIjoiaHR0cHM6Ly9tY3AubWVtMC5haS9tY3AvIiwiaGVhZGVycyI6eyJBdXRob3JpemF0aW9uIjoiVG9rZW4gJHtlbnY6TUVNMF9BUElfS0VZfSJ9fX19)
|
||||
|
||||
**Option B — Manual configuration** (MCP server only):
|
||||
|
||||
Add the following to your `.cursor/mcp.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${env:MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Option C — Cursor Marketplace** (full plugin with hooks and skills):
|
||||
|
||||
Install from the [Cursor Marketplace](https://cursor.com/marketplace) for the complete experience including lifecycle hooks and the Mem0 SDK skill.
|
||||
|
||||
## Verify it works
|
||||
|
||||
After installing, confirm the MCP server is connected:
|
||||
|
||||
1. Start a new session (or restart your current one)
|
||||
2. Ask: *"List my mem0 entities"* or *"Search my memories for hello"*
|
||||
3. If the `mem0` tools appear and respond, you're all set
|
||||
|
||||
## What's included
|
||||
|
||||
| Component | Claude Code / Cowork | Cursor (Marketplace) | Cursor (Deeplink/Manual) |
|
||||
|-----------|:--------------------:|:--------------------:|:------------------------:|
|
||||
| MCP Server | Yes | Yes | Yes |
|
||||
| Lifecycle Hooks | Yes | Yes | No |
|
||||
| Mem0 SDK Skill | Yes | Yes | No |
|
||||
|
||||
- **MCP Server** — Connects to the Mem0 remote MCP server (`mcp.mem0.ai`), providing tools to add, search, update, and delete memories. No local dependencies required.
|
||||
- **Lifecycle Hooks** — Automatic memory capture at key points: session start, context compaction, task completion, and session end.
|
||||
- **Mem0 SDK Skill** — Guides the AI on how to integrate the Mem0 SDK (Python & TypeScript) into your applications.
|
||||
|
||||
## MCP Tools
|
||||
|
||||
Once installed, the following tools are available:
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `add_memory` | Save text or conversation history for a user/agent |
|
||||
| `search_memories` | Semantic search across memories with filters |
|
||||
| `get_memories` | List memories with filters and pagination |
|
||||
| `get_memory` | Retrieve a specific memory by ID |
|
||||
| `update_memory` | Overwrite a memory's text by ID |
|
||||
| `delete_memory` | Delete a single memory by ID |
|
||||
| `delete_all_memories` | Bulk delete all memories in scope |
|
||||
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
|
||||
| `list_entities` | List users/agents/apps/runs stored in Mem0 |
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"hooks": {
|
||||
"sessionStart": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_session_start.sh",
|
||||
"matcher": "startup|resume|compact"
|
||||
}
|
||||
],
|
||||
"preToolUse": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/block_memory_write.sh",
|
||||
"matcher": "Write|Edit"
|
||||
}
|
||||
],
|
||||
"preCompact": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_pre_compact.sh"
|
||||
},
|
||||
{
|
||||
"command": "python3 ${CURSOR_PLUGIN_ROOT}/scripts/on_pre_compact.py",
|
||||
"timeout": 30
|
||||
}
|
||||
],
|
||||
"stop": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_stop.sh",
|
||||
"timeout": 10
|
||||
}
|
||||
],
|
||||
"beforeSubmitPrompt": [
|
||||
{
|
||||
"command": "${CURSOR_PLUGIN_ROOT}/scripts/on_user_prompt.sh",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"description": "Mem0 memory capture hooks — automatic memory extraction at key lifecycle points",
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume|compact",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_session_start.sh",
|
||||
"statusMessage": "Loading mem0 context..."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "Write|Edit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/block_memory_write.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PreCompact": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_pre_compact.sh",
|
||||
"statusMessage": "Preparing pre-compaction summary..."
|
||||
},
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/scripts/on_pre_compact.py",
|
||||
"statusMessage": "Saving session state to mem0...",
|
||||
"timeout": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_stop.sh",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_user_prompt.sh",
|
||||
"statusMessage": "Searching mem0 memories...",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"TaskCompleted": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/on_task_completed.sh",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<svg width="307" height="307" viewBox="0 0 307 307" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M162.496 25.3505C165.003 25.3505 167.453 24.6071 169.538 23.2144C171.622 21.8216 173.247 19.8419 174.206 17.5258C175.165 15.2097 175.416 12.6612 174.927 10.2024C174.438 7.74365 173.231 5.48516 171.458 3.71249C169.686 1.93983 167.427 0.73263 164.968 0.243552C162.51 -0.245525 159.961 0.00550576 157.645 0.964866C155.329 1.92423 153.349 3.54885 151.956 5.63328C150.564 7.71772 149.82 10.1683 149.82 12.6753C149.818 14.3404 150.145 15.9895 150.781 17.5283C151.417 19.0671 152.351 20.4653 153.528 21.6427C154.706 22.8201 156.104 23.7537 157.643 24.39C159.181 25.0262 160.83 25.3526 162.496 25.3505Z" fill="white"/>
|
||||
<path d="M69.3342 56.559C71.1066 54.7862 72.3135 52.5277 72.8024 50.069C73.2913 47.6103 73.0401 45.0619 72.0807 42.7459C71.1213 40.43 69.4967 38.4505 67.4123 37.0579C65.3279 35.6652 62.8774 34.9219 60.3706 34.9219C57.8637 34.9219 55.4132 35.6652 53.3288 37.0579C51.2444 38.4505 49.6198 40.43 48.6604 42.7459C47.701 45.0619 47.4498 47.6103 47.9387 50.069C48.4276 52.5277 49.6345 54.7862 51.4069 56.559C52.5839 57.7363 53.9813 58.6701 55.5193 59.3073C57.0573 59.9444 58.7058 60.2724 60.3706 60.2724C62.0353 60.2724 63.6838 59.9444 65.2218 59.3073C66.7598 58.6701 68.1572 57.7363 69.3342 56.559Z" fill="white"/>
|
||||
<path d="M25.3505 144.504C25.3505 141.997 24.6071 139.547 23.2143 137.462C21.8216 135.378 19.842 133.753 17.5259 132.794C15.2098 131.835 12.6612 131.584 10.2024 132.073C7.74368 132.562 5.48513 133.769 3.71247 135.542C1.9398 137.314 0.732655 139.573 0.243578 142.032C-0.2455 144.49 0.00543354 147.039 0.964793 149.355C1.92415 151.671 3.54877 153.651 5.63321 155.044C7.71764 156.436 10.1683 157.18 12.6752 157.18C16.0369 157.18 19.261 155.844 21.638 153.467C24.0151 151.09 25.3505 147.866 25.3505 144.504Z" fill="white"/>
|
||||
<path d="M56.5589 237.749C54.7862 235.976 52.5277 234.769 50.069 234.28C47.6103 233.792 45.0619 234.043 42.7459 235.002C40.43 235.962 38.4505 237.586 37.0579 239.671C35.6652 241.755 34.9219 244.206 34.9219 246.712C34.9219 249.219 35.6652 251.67 37.0579 253.754C38.4505 255.838 40.43 257.463 42.7459 258.423C45.0619 259.382 47.6103 259.633 50.069 259.144C52.5277 258.655 54.7862 257.448 56.5589 255.676C57.7362 254.499 58.6701 253.102 59.3073 251.564C59.9444 250.026 60.2724 248.377 60.2724 246.712C60.2724 245.048 59.9444 243.399 59.3073 241.861C58.6701 240.323 57.7362 238.926 56.5589 237.749Z" fill="white"/>
|
||||
<path d="M144.488 281.648C141.981 281.648 139.53 282.392 137.446 283.785C135.361 285.177 133.737 287.157 132.777 289.473C131.818 291.789 131.567 294.338 132.056 296.797C132.545 299.255 133.752 301.514 135.525 303.286C137.298 305.059 139.556 306.266 142.015 306.755C144.474 307.244 147.022 306.993 149.338 306.034C151.655 305.075 153.634 303.45 155.027 301.366C156.42 299.281 157.163 296.831 157.163 294.324C157.159 290.963 155.822 287.742 153.446 285.366C151.07 282.989 147.848 281.653 144.488 281.648Z" fill="white"/>
|
||||
<path d="M237.751 250.487C235.978 252.26 234.771 254.518 234.282 256.977C233.794 259.435 234.045 261.984 235.004 264.3C235.964 266.616 237.588 268.595 239.673 269.988C241.757 271.381 244.207 272.124 246.714 272.124C249.221 272.124 251.672 271.381 253.756 269.988C255.84 268.595 257.465 266.616 258.424 264.3C259.384 261.984 259.635 259.435 259.146 256.977C258.657 254.518 257.45 252.26 255.678 250.487C254.501 249.31 253.104 248.376 251.566 247.739C250.028 247.101 248.379 246.773 246.714 246.773C245.05 246.773 243.401 247.101 241.863 247.739C240.325 248.376 238.928 249.31 237.751 250.487Z" fill="white"/>
|
||||
<path d="M281.648 162.512C281.648 165.019 282.392 167.469 283.785 169.554C285.177 171.638 287.157 173.263 289.473 174.222C291.789 175.181 294.338 175.432 296.797 174.943C299.255 174.454 301.514 173.247 303.286 171.474C305.059 169.702 306.266 167.443 306.755 164.984C307.244 162.526 306.993 159.977 306.034 157.661C305.075 155.345 303.45 153.365 301.366 151.973C299.281 150.58 296.831 149.836 294.324 149.836C290.962 149.836 287.738 151.172 285.361 153.549C282.984 155.926 281.648 159.15 281.648 162.512Z" fill="white"/>
|
||||
<path d="M250.471 69.3303C252.244 71.1027 254.503 72.3097 256.961 72.7985C259.42 73.2874 261.968 73.0363 264.284 72.0768C266.6 71.1174 268.58 69.4928 269.972 67.4084C271.365 65.324 272.108 62.8735 272.108 60.3667C272.108 57.8599 271.365 55.4093 269.972 53.3249C268.58 51.2406 266.6 49.616 264.284 48.6565C261.968 47.6971 259.42 47.4459 256.961 47.9348C254.503 48.4236 252.244 49.6306 250.471 51.403C249.294 52.58 248.36 53.9775 247.723 55.5155C247.086 57.0535 246.758 58.7019 246.758 60.3667C246.758 62.0314 247.086 63.6799 247.723 65.2179C248.36 66.7559 249.294 68.1533 250.471 69.3303Z" fill="white"/>
|
||||
<path d="M184.782 60.8054C180.168 63.4713 178.3 69.0427 177.63 74.3267C177.047 78.9358 175.033 83.2457 171.87 86.6488C168.707 90.052 164.556 92.3766 160.002 93.2951C155.448 94.2136 150.721 93.6796 146.487 91.7683C142.252 89.857 138.724 86.6649 136.401 82.642C134.077 78.6192 133.075 73.9684 133.535 69.3455C133.995 64.7226 135.895 60.3607 138.966 56.8748C142.037 53.389 146.125 50.9549 150.653 49.9159C155.181 48.8768 159.921 49.2852 164.204 51.0834C169.121 53.1428 174.884 54.2762 179.514 51.6422C184.143 49.0082 185.995 43.4049 186.665 38.1208C187.245 33.5107 189.257 29.1987 192.418 25.7931C195.579 22.3876 199.73 20.0603 204.284 19.1394C208.838 18.2185 213.567 18.7505 217.803 20.6605C222.039 22.5704 225.568 25.7618 227.893 29.7846C230.218 33.8075 231.222 38.4587 230.763 43.0824C230.303 47.7062 228.404 52.0691 225.333 55.5559C222.262 59.0426 218.173 61.4773 213.645 62.5165C209.116 63.5557 204.375 63.147 200.091 61.3481C195.174 59.3048 189.411 58.1554 184.782 60.8054Z" fill="white"/>
|
||||
<path d="M110.073 65.8178C108.7 70.9742 111.318 76.2422 114.575 80.4567C117.417 84.1261 119.036 88.595 119.204 93.2335C119.372 97.872 118.08 102.446 115.51 106.311C112.941 110.177 109.223 113.138 104.881 114.778C100.538 116.419 95.7912 116.655 91.3077 115.454C86.8242 114.253 82.8306 111.675 79.8898 108.084C76.9489 104.493 75.2091 100.07 74.9155 95.4379C74.6219 90.8057 75.7894 86.1981 78.2533 82.2645C80.7173 78.331 84.3534 75.2698 88.6494 73.5124C93.5822 71.485 98.4991 68.2444 99.8241 63.0881C101.149 57.9317 98.579 52.6637 95.3224 48.4493C92.4827 44.7781 90.8665 40.3083 90.7018 35.6699C90.537 31.0315 91.8319 26.4583 94.4039 22.5949C96.976 18.7314 100.695 15.7724 105.038 14.1349C109.381 12.4974 114.128 12.2639 118.611 13.4673C123.094 14.6708 127.085 17.2505 130.024 20.8429C132.963 24.4354 134.7 28.8594 134.991 33.4916C135.283 38.1237 134.113 42.7305 131.647 46.6627C129.182 50.5948 125.544 53.6541 121.248 55.4095C116.363 57.4209 111.462 60.6775 110.073 65.8178Z" fill="white"/>
|
||||
<path d="M60.7892 122.218C63.4552 126.831 69.0425 128.699 74.3265 129.37C78.9361 129.955 83.2455 131.973 86.6471 135.138C90.0487 138.304 92.3707 142.457 93.2857 147.013C94.2006 151.569 93.6625 156.296 91.747 160.53C89.8314 164.763 86.6353 168.288 82.6093 170.608C78.5833 172.928 73.9305 173.926 69.3073 173.46C64.6841 172.995 60.3236 171.09 56.841 168.014C53.3583 164.938 50.9292 160.846 49.8962 156.316C48.8631 151.785 49.2783 147.045 51.0832 142.763C53.1426 137.846 54.2759 132.083 51.6419 127.454C49.0079 122.824 43.4046 120.973 38.1047 120.302C33.4951 119.717 29.1856 117.699 25.7841 114.533C22.3825 111.368 20.0604 107.214 19.1454 102.659C18.2304 98.1032 18.7687 93.3752 20.6842 89.1418C22.5997 84.9084 25.7959 81.3832 29.8219 79.0632C33.8479 76.7433 38.5006 75.7457 43.1238 76.2113C47.7471 76.6768 52.1075 78.582 55.5902 81.658C59.0728 84.7341 61.502 88.8258 62.535 93.3561C63.568 97.8865 63.1528 102.627 61.3479 106.908C59.2886 111.825 58.1552 117.588 60.7892 122.218Z" fill="white"/>
|
||||
<path d="M65.8204 196.93C70.9767 198.303 76.2287 195.685 80.4592 192.428C84.1286 189.586 88.5975 187.967 93.236 187.799C97.8745 187.631 102.449 188.923 106.314 191.493C110.179 194.062 113.141 197.78 114.781 202.122C116.421 206.464 116.657 211.212 115.457 215.695C114.256 220.179 111.678 224.172 108.087 227.113C104.496 230.054 100.073 231.794 95.4404 232.087C90.8082 232.381 86.2006 231.214 82.2671 228.75C78.3335 226.286 75.2723 222.649 73.5149 218.353C71.4875 213.421 68.231 208.504 63.0906 207.179C57.9503 205.854 52.6662 208.424 48.4518 211.681C44.7804 214.528 40.308 216.151 35.6652 216.32C31.0224 216.49 26.4435 215.199 22.5738 212.628C18.7042 210.057 15.7391 206.336 14.0968 201.99C12.4544 197.644 12.2176 192.892 13.4197 188.404C14.6218 183.917 17.2021 179.919 20.7969 176.976C24.3917 174.033 28.8195 172.293 33.4562 172C38.0929 171.708 42.7045 172.878 46.6407 175.345C50.577 177.813 53.6394 181.454 55.3961 185.755C57.4235 190.656 60.6641 195.541 65.8204 196.93Z" fill="white"/>
|
||||
<path d="M122.205 246.21C126.818 243.544 128.686 237.956 129.373 232.672C129.96 228.068 131.978 223.763 135.142 220.366C138.306 216.969 142.456 214.651 147.008 213.738C151.559 212.825 156.283 213.364 160.512 215.278C164.741 217.192 168.263 220.385 170.58 224.408C172.898 228.43 173.895 233.078 173.43 237.697C172.966 242.316 171.064 246.673 167.991 250.153C164.919 253.633 160.832 256.061 156.306 257.095C151.781 258.129 147.045 257.717 142.766 255.916C137.833 253.856 132.07 252.723 127.457 255.357C122.843 257.991 120.96 263.594 120.289 268.894C119.7 273.498 117.681 277.8 114.517 281.196C111.353 284.591 107.204 286.908 102.653 287.821C98.1027 288.733 93.3808 288.194 89.1525 286.281C84.9243 284.367 81.4031 281.175 79.085 277.154C76.767 273.134 75.7689 268.487 76.2316 263.869C76.6942 259.251 78.5942 254.895 81.6638 251.414C84.7334 247.933 88.8179 245.503 93.3417 244.466C97.8655 243.429 102.601 243.838 106.88 245.635C111.828 247.694 117.591 248.876 122.205 246.21Z" fill="white"/>
|
||||
<path d="M196.915 241.18C198.304 236.024 195.686 230.756 192.414 226.542C189.567 222.87 187.944 218.398 187.774 213.755C187.604 209.112 188.896 204.533 191.467 200.664C194.038 196.794 197.759 193.829 202.104 192.187C206.45 190.544 211.202 190.307 215.69 191.509C220.178 192.712 224.175 195.292 227.118 198.887C230.061 202.481 231.802 206.909 232.094 211.546C232.387 216.183 231.217 220.794 228.749 224.731C226.281 228.667 222.64 231.729 218.339 233.486C213.406 235.513 208.505 238.77 207.164 243.91C205.823 249.051 208.393 254.335 211.666 258.549C214.513 262.22 216.136 266.693 216.306 271.335C216.476 275.978 215.184 280.557 212.613 284.427C210.042 288.297 206.321 291.262 201.975 292.904C197.629 294.546 192.877 294.783 188.39 293.581C183.902 292.379 179.905 289.799 176.962 286.204C174.019 282.609 172.278 278.181 171.985 273.545C171.693 268.908 172.863 264.296 175.331 260.36C177.799 256.424 181.44 253.361 185.741 251.605C190.658 249.577 195.543 246.337 196.915 241.18Z" fill="white"/>
|
||||
<path d="M246.195 184.797C243.529 180.184 237.957 178.316 232.673 177.629C228.069 177.045 223.764 175.03 220.365 171.869C216.967 168.708 214.646 164.56 213.729 160.01C212.813 155.46 213.348 150.737 215.258 146.507C217.168 142.277 220.357 138.753 224.376 136.431C228.395 134.11 233.041 133.108 237.66 133.567C242.279 134.026 246.637 135.923 250.12 138.991C253.604 142.058 256.037 146.141 257.077 150.664C258.117 155.188 257.711 159.923 255.917 164.204C253.857 169.137 252.724 174.9 255.342 179.513C257.96 184.127 263.595 186.01 268.879 186.681C273.484 187.267 277.789 189.283 281.186 192.446C284.584 195.608 286.904 199.757 287.819 204.308C288.733 208.859 288.197 213.582 286.285 217.811C284.372 222.041 281.181 225.564 277.16 227.884C273.14 230.203 268.492 231.203 263.874 230.741C259.255 230.279 254.897 228.379 251.416 225.31C247.934 222.24 245.503 218.155 244.466 213.63C243.429 209.106 243.838 204.37 245.636 200.09C247.695 195.173 248.861 189.411 246.195 184.797Z" fill="white"/>
|
||||
<path d="M241.18 110.07C236.024 108.697 230.756 111.315 226.542 114.588C222.87 117.435 218.398 119.058 213.755 119.228C209.112 119.398 204.533 118.106 200.664 115.535C196.794 112.964 193.829 109.243 192.187 104.897C190.544 100.551 190.307 95.7994 191.509 91.3117C192.712 86.824 195.292 82.8268 198.887 79.8837C202.481 76.9405 206.909 75.2 211.546 74.9073C216.183 74.6147 220.794 75.7849 224.731 78.2527C228.667 80.7206 231.729 84.3617 233.486 88.6627C235.513 93.5955 238.754 98.4964 243.91 99.8374C249.066 101.178 254.335 98.6082 258.549 95.3356C262.22 92.4959 266.69 90.8798 271.328 90.7151C275.967 90.5503 280.54 91.8452 284.403 94.4172C288.267 96.9892 291.226 100.709 292.863 105.052C294.501 109.394 294.734 114.142 293.531 118.624C292.327 123.107 289.748 127.099 286.155 130.037C282.563 132.976 278.139 134.714 273.507 135.005C268.875 135.296 264.268 134.126 260.336 131.661C256.403 129.195 253.344 125.557 251.589 121.261C249.577 116.36 246.321 111.459 241.18 110.07Z" fill="white"/>
|
||||
<path d="M153.491 191.533C174.501 191.533 191.533 174.501 191.533 153.491C191.533 132.482 174.501 115.45 153.491 115.45C132.481 115.45 115.449 132.482 115.449 153.491C115.449 174.501 132.481 191.533 153.491 191.533Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: PreToolUse (matcher: Write|Edit)
|
||||
#
|
||||
# Blocks writes to MEMORY.md and auto-memory files, redirecting Claude
|
||||
# to use the mem0 MCP add_memory tool instead.
|
||||
#
|
||||
# Input: JSON on stdin with tool_name, tool_input
|
||||
# Output: stderr message (exit 2 = block)
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = allow the tool call
|
||||
# 2 = block the tool call (stderr is shown to Claude as feedback)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // ""' 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$FILE_PATH" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$FILE_PATH" in
|
||||
*/MEMORY.md|*/memory/*.md|*/.claude/*/memory/*)
|
||||
echo "BLOCKED: Do not write to $FILE_PATH. Use the mem0 MCP \`add_memory\` tool instead to persist memories. This project uses mem0 for all memory storage." >&2
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
Executable
+239
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture session state via the Mem0 REST API.
|
||||
|
||||
Safety net for PreCompact and Stop hooks — reads the transcript JSONL,
|
||||
extracts structured session state, and stores it in Mem0 directly.
|
||||
|
||||
Used by:
|
||||
- PreCompact hook: Tags with "pre-compaction" (context about to be lost)
|
||||
- Stop hook: Tags with "session-end" (session ending, Claude can't respond)
|
||||
|
||||
Input: JSON on stdin with transcript_path, session_id, cwd
|
||||
Output: stderr logs only (exit 0 always — must not block)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
log = logging.getLogger("mem0-capture")
|
||||
log.setLevel(logging.DEBUG)
|
||||
_handler = logging.StreamHandler(sys.stderr)
|
||||
_handler.setFormatter(logging.Formatter("[mem0-capture] %(message)s"))
|
||||
log.addHandler(_handler)
|
||||
|
||||
API_URL = "https://api.mem0.ai"
|
||||
MAX_TAIL_LINES = 500
|
||||
MAX_USER_MESSAGES = 30
|
||||
MAX_BASH_COMMANDS = 20
|
||||
MAX_ASSISTANT_TEXT = 10000
|
||||
|
||||
|
||||
def tail_lines(filepath: str, n: int) -> list[str]:
|
||||
"""Read last n lines of a file efficiently."""
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
f.seek(0, 2)
|
||||
file_size = f.tell()
|
||||
if file_size == 0:
|
||||
return []
|
||||
chunk_size = min(file_size, n * 4096)
|
||||
f.seek(max(0, file_size - chunk_size))
|
||||
data = f.read().decode("utf-8", errors="replace")
|
||||
return data.splitlines()[-n:]
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
|
||||
def parse_transcript(lines: list[str]) -> dict:
|
||||
"""Parse transcript JSONL lines and extract session state."""
|
||||
user_messages: list[str] = []
|
||||
files_modified: set[str] = set()
|
||||
bash_commands: list[str] = []
|
||||
last_assistant_text = ""
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
entry_type = entry.get("type")
|
||||
if entry_type not in ("user", "assistant"):
|
||||
continue
|
||||
if entry.get("isSidechain"):
|
||||
continue
|
||||
|
||||
message = entry.get("message", {})
|
||||
content_blocks = message.get("content", [])
|
||||
|
||||
if entry_type == "user":
|
||||
parts = []
|
||||
if isinstance(content_blocks, str):
|
||||
parts.append(content_blocks)
|
||||
elif isinstance(content_blocks, list):
|
||||
for block in content_blocks:
|
||||
if isinstance(block, str):
|
||||
parts.append(block)
|
||||
elif isinstance(block, dict) and block.get("type") == "text":
|
||||
parts.append(block.get("text", ""))
|
||||
text = "\n".join(parts).strip()
|
||||
if text and len(text) > 10 and not text.startswith("<"):
|
||||
user_messages.append(text)
|
||||
|
||||
elif entry_type == "assistant":
|
||||
for block in content_blocks:
|
||||
if not isinstance(block, dict):
|
||||
continue
|
||||
if block.get("type") == "text":
|
||||
text = block.get("text", "").strip()
|
||||
if text:
|
||||
last_assistant_text = text
|
||||
if block.get("type") == "tool_use":
|
||||
tool_name = block.get("name", "")
|
||||
tool_input = block.get("input", {})
|
||||
if tool_name in ("Write", "Edit"):
|
||||
fp = tool_input.get("file_path", "")
|
||||
if fp:
|
||||
files_modified.add(fp)
|
||||
elif tool_name == "Bash":
|
||||
cmd = tool_input.get("command", "")
|
||||
if cmd:
|
||||
bash_commands.append(cmd)
|
||||
|
||||
return {
|
||||
"user_messages": user_messages[-MAX_USER_MESSAGES:],
|
||||
"files_modified": sorted(files_modified),
|
||||
"bash_commands": bash_commands[-MAX_BASH_COMMANDS:],
|
||||
"last_assistant_text": last_assistant_text[:MAX_ASSISTANT_TEXT],
|
||||
}
|
||||
|
||||
|
||||
def build_content(state: dict, source: str) -> str:
|
||||
"""Build structured markdown from parsed state."""
|
||||
parts = [f"## Session State ({source})\n"]
|
||||
|
||||
if state["user_messages"]:
|
||||
parts.append("### What the user was working on")
|
||||
for msg in state["user_messages"]:
|
||||
truncated = msg[:5000] + "..." if len(msg) > 5000 else msg
|
||||
parts.append(f"- {truncated}")
|
||||
parts.append("")
|
||||
|
||||
if state["files_modified"]:
|
||||
parts.append("### Files modified this session")
|
||||
for fp in state["files_modified"]:
|
||||
parts.append(f"- `{fp}`")
|
||||
parts.append("")
|
||||
|
||||
if state["bash_commands"]:
|
||||
parts.append("### Recent commands")
|
||||
for cmd in state["bash_commands"]:
|
||||
truncated = cmd[:1000] + "..." if len(cmd) > 1000 else cmd
|
||||
parts.append(f"- `{truncated}`")
|
||||
parts.append("")
|
||||
|
||||
if state["last_assistant_text"]:
|
||||
parts.append("### Last context")
|
||||
parts.append(state["last_assistant_text"])
|
||||
parts.append("")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def store_memory(api_key: str, content: str, user_id: str, source: str) -> bool:
|
||||
"""Store session state as a memory via the Mem0 REST API."""
|
||||
body = {
|
||||
"messages": [
|
||||
{"role": "user", "content": content}
|
||||
],
|
||||
"user_id": user_id,
|
||||
"metadata": {
|
||||
"type": "session_state",
|
||||
"source": source,
|
||||
},
|
||||
}
|
||||
|
||||
data = json.dumps(body).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{API_URL}/v1/memories/",
|
||||
data=data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Token {api_key}",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
if resp.status in (200, 201):
|
||||
log.info("Session state stored successfully")
|
||||
return True
|
||||
log.warning("API returned status %d", resp.status)
|
||||
return False
|
||||
except urllib.error.URLError as e:
|
||||
log.warning("API call failed: %s", e)
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
source = "pre-compaction"
|
||||
for arg in sys.argv[1:]:
|
||||
if arg.startswith("--source="):
|
||||
source = arg.split("=", 1)[1]
|
||||
|
||||
api_key = os.environ.get("MEM0_API_KEY", "")
|
||||
if not api_key:
|
||||
log.debug("MEM0_API_KEY not set, skipping capture")
|
||||
return
|
||||
|
||||
try:
|
||||
hook_input = json.loads(sys.stdin.read())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
log.debug("No valid JSON on stdin")
|
||||
return
|
||||
|
||||
transcript_path = hook_input.get("transcript_path", "")
|
||||
if not transcript_path:
|
||||
log.debug("No transcript_path provided")
|
||||
return
|
||||
|
||||
user_id = os.environ.get("MEM0_USER_ID", os.environ.get("USER", "default"))
|
||||
|
||||
lines = tail_lines(transcript_path, MAX_TAIL_LINES)
|
||||
if not lines:
|
||||
log.debug("Transcript empty or unreadable: %s", transcript_path)
|
||||
return
|
||||
|
||||
state = parse_transcript(lines)
|
||||
if not state["user_messages"] and not state["files_modified"]:
|
||||
log.debug("No meaningful session state to capture")
|
||||
return
|
||||
|
||||
content = build_content(state, source)
|
||||
|
||||
log.info(
|
||||
"Capturing session state: %d user msgs, %d files, %d commands",
|
||||
len(state["user_messages"]),
|
||||
len(state["files_modified"]),
|
||||
len(state["bash_commands"]),
|
||||
)
|
||||
|
||||
store_memory(api_key, content, user_id, source)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as e:
|
||||
log.error("Unexpected error: %s", e)
|
||||
sys.exit(0)
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: PreCompact
|
||||
#
|
||||
# Fires BEFORE context compaction. This is the last chance to capture
|
||||
# the full context before it gets compressed.
|
||||
#
|
||||
# Output: Text instructions injected into Claude's context.
|
||||
# Claude still has the full conversation and can write an accurate summary.
|
||||
# A companion Python script (on_pre_compact.py) also runs to capture
|
||||
# transcript state directly via the Mem0 REST API as a safety net.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cat <<'EOF'
|
||||
## CRITICAL: Pre-Compaction Session Summary
|
||||
|
||||
Context compaction is about to happen. You are about to lose most of your conversation history. You MUST store a comprehensive session summary NOW using the mem0 `add_memory` tool.
|
||||
|
||||
### Step 1: Store session summary
|
||||
|
||||
Call `add_memory` with a thorough summary covering ALL of the following:
|
||||
|
||||
```
|
||||
## Session Summary (Pre-Compaction)
|
||||
|
||||
### User's Goal
|
||||
[What the user originally asked for and their intent]
|
||||
|
||||
### What Was Accomplished
|
||||
[Numbered list of tasks completed, features built, bugs fixed]
|
||||
|
||||
### Key Decisions Made
|
||||
[Architectural choices, design decisions, trade-offs discussed]
|
||||
|
||||
### Files Created or Modified
|
||||
[List of important file paths with what changed in each]
|
||||
|
||||
### Current State
|
||||
[What is in progress RIGHT NOW — the task you were in the middle of]
|
||||
[Any pending items, blockers, or next steps]
|
||||
|
||||
### Important Context
|
||||
[User preferences observed, coding patterns, anything that would help
|
||||
the post-compaction agent continue without asking redundant questions]
|
||||
```
|
||||
|
||||
Include metadata: `{"type": "session_state", "source": "pre-compaction"}`
|
||||
|
||||
### Step 2: Store any unstored learnings
|
||||
|
||||
If there are learnings from this session that you haven't stored yet, store them as separate memories:
|
||||
- Failed approaches -> metadata `{"type": "anti_pattern"}`
|
||||
- Successful strategies -> metadata `{"type": "task_learning"}`
|
||||
- Architecture decisions -> metadata `{"type": "decision"}`
|
||||
|
||||
### Step 3: Acknowledge
|
||||
|
||||
After storing, briefly tell the user that session state has been saved and you're ready for compaction.
|
||||
|
||||
Do this NOW. Do not skip any section. The quality of this summary directly determines whether you can continue the user's task after compaction.
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: SessionStart (matcher: startup|resume|compact)
|
||||
#
|
||||
# Bootstraps mem0 context at the start of every session.
|
||||
# Output becomes part of Claude's context so it calls mem0 MCP tools.
|
||||
#
|
||||
# Input: JSON on stdin with session_id, source, transcript_path, model, cwd
|
||||
# Output: Text injected into Claude's context (exit 0)
|
||||
|
||||
# Intentionally omit -e so the script always outputs a bootstrap prompt
|
||||
# even if jq is missing or stdin is malformed.
|
||||
set -uo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"' 2>/dev/null || echo "startup")
|
||||
|
||||
if [ "$SOURCE" = "startup" ]; then
|
||||
cat <<'EOF'
|
||||
## Mem0 Session Bootstrap
|
||||
|
||||
You have access to persistent memory via the mem0 MCP tools. Before doing anything else:
|
||||
|
||||
1. Call `search_memories` with a query related to the current project or user request to load relevant context.
|
||||
2. Review the returned memories to understand what has been learned in prior sessions.
|
||||
3. If appropriate, call `get_memories` to browse all stored memories for this user.
|
||||
|
||||
IMPORTANT: Do NOT skip this step. Always bootstrap context first.
|
||||
EOF
|
||||
|
||||
elif [ "$SOURCE" = "resume" ]; then
|
||||
cat <<'EOF'
|
||||
## Mem0 Session Resumed
|
||||
|
||||
This is a resumed session. Your prior context is already loaded. Before continuing:
|
||||
|
||||
1. Call `search_memories` with a query related to the current task to refresh relevant memories.
|
||||
2. If significant time has passed, search for recent project-wide updates.
|
||||
|
||||
Continue where you left off.
|
||||
EOF
|
||||
|
||||
elif [ "$SOURCE" = "compact" ]; then
|
||||
cat <<'EOF'
|
||||
## Mem0 Post-Compaction Recovery
|
||||
|
||||
Context was just compacted. You may have lost important session context.
|
||||
|
||||
1. Call `search_memories` with queries related to what you were working on to reload relevant knowledge.
|
||||
2. Check for any session state memories that were saved before compaction.
|
||||
3. Continue working based on the recovered context.
|
||||
EOF
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: Stop
|
||||
#
|
||||
# Fires when Claude finishes responding.
|
||||
# Reminds Claude to store any unsaved learnings, then spawns a background
|
||||
# process to capture transcript state via the Mem0 REST API directly.
|
||||
#
|
||||
# Input: JSON on stdin with stop_hook_active, transcript_path, cwd
|
||||
# Output: Text that becomes Claude's context (exit 0), or nothing
|
||||
#
|
||||
# IMPORTANT: Check stop_hook_active to avoid infinite loops.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
INPUT=$(cat)
|
||||
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
|
||||
|
||||
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
cat <<'EOF'
|
||||
Before finishing, check if there are important learnings from this interaction that should be persisted using the mem0 `add_memory` tool:
|
||||
|
||||
1. Were any significant decisions made? -> Store with metadata `{"type": "decision"}`
|
||||
2. Were any new patterns or strategies discovered? -> Store with metadata `{"type": "task_learning"}`
|
||||
3. Did any approach fail? -> Store with metadata `{"type": "anti_pattern"}`
|
||||
4. Did you learn anything about the user's preferences? -> Store with metadata `{"type": "user_preference"}`
|
||||
5. Were there environment/setup discoveries? -> Store with metadata `{"type": "environmental"}`
|
||||
|
||||
Memories can be as detailed as needed — include full context, reasoning, code snippets, file paths, and examples. Longer, searchable memories are more valuable than vague one-liners.
|
||||
|
||||
If nothing notable happened in this interaction, it's fine to skip. Only store genuinely useful learnings.
|
||||
EOF
|
||||
|
||||
# Capture transcript state in the background via Mem0 REST API
|
||||
echo "$INPUT" | python3 "$SCRIPT_DIR/on_pre_compact.py" --source=session-end 2>/dev/null &
|
||||
|
||||
exit 0
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: TaskCompleted
|
||||
#
|
||||
# Fires when a task is marked as completed. Reminds Claude to extract
|
||||
# and store learnings via the mem0 MCP tools.
|
||||
#
|
||||
# Input: JSON on stdin with task_id, task_subject, task_description
|
||||
# Output: Text that becomes feedback to the model (exit 0)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject // "unknown task"' 2>/dev/null || echo "unknown task")
|
||||
|
||||
cat <<EOF
|
||||
Task completed: "$TASK_SUBJECT"
|
||||
|
||||
Extract key learnings from this completed task and store them using the mem0 \`add_memory\` tool:
|
||||
|
||||
1. What strategy worked well? -> Store with metadata \`{"type": "task_learning"}\`
|
||||
2. Were there failed approaches before finding the solution? -> Store with metadata \`{"type": "anti_pattern"}\`
|
||||
3. Were there architectural decisions? -> Store with metadata \`{"type": "decision"}\`
|
||||
4. Any new conventions or patterns established? -> Store with metadata \`{"type": "convention"}\`
|
||||
|
||||
Memories can be as detailed as needed — include full context, reasoning, code snippets, and examples.
|
||||
Only store genuinely useful learnings — skip if the task was trivial.
|
||||
EOF
|
||||
|
||||
exit 0
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: UserPromptSubmit
|
||||
#
|
||||
# Fires on every user message. Searches mem0 for relevant memories
|
||||
# and injects them into Claude's context before processing.
|
||||
#
|
||||
# Input: JSON on stdin with prompt, session_id, cwd, transcript_path
|
||||
# Output: Matching memories as context text (exit 0)
|
||||
#
|
||||
# Skips search for very short prompts (< 20 chars) and when
|
||||
# MEM0_API_KEY is not set. Uses a 3s timeout to minimize latency.
|
||||
|
||||
# Intentionally omit -e so the script always exits 0 even if
|
||||
# curl or jq fail — must never block the user's prompt.
|
||||
set -uo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""' 2>/dev/null || echo "")
|
||||
|
||||
# Skip trivial prompts — not worth a network call
|
||||
if [ ${#PROMPT} -lt 20 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
API_KEY="${MEM0_API_KEY:-}"
|
||||
if [ -z "$API_KEY" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
USER_ID="${MEM0_USER_ID:-${USER:-default}}"
|
||||
|
||||
# Build request body safely via jq to avoid injection
|
||||
BODY=$(jq -n --arg query "$PROMPT" --arg user_id "$USER_ID" \
|
||||
'{query: $query, filters: {user_id: $user_id}, top_k: 5}')
|
||||
|
||||
# Search mem0 for memories relevant to this prompt
|
||||
RESPONSE=$(curl -s --max-time 3 \
|
||||
-X POST "https://api.mem0.ai/v2/memories/search/" \
|
||||
-H "Authorization: Token $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY" \
|
||||
2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$RESPONSE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract memories from response (API returns a flat array)
|
||||
MEMORIES=$(echo "$RESPONSE" | jq -r '
|
||||
if type == "array" then . else .results // [] end |
|
||||
if length == 0 then empty else
|
||||
"## Relevant memories from mem0\n\n" +
|
||||
(map(select(.memory != null) | "- " + .memory) | join("\n"))
|
||||
end
|
||||
' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$MEMORIES" ]; then
|
||||
echo "$MEMORIES"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,189 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but not
|
||||
limited to compiled object code, generated documentation, and
|
||||
conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to the Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by the Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding any notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2024 Mem0.ai
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,73 @@
|
||||
# Mem0 Skill for Claude
|
||||
|
||||
Add persistent memory to any AI application in minutes using [Mem0 Platform](https://app.mem0.ai).
|
||||
|
||||
## What This Skill Does
|
||||
|
||||
When installed, Claude can:
|
||||
|
||||
- **Set up Mem0** in your Python or TypeScript project
|
||||
- **Integrate memory** into your existing AI app (LangChain, CrewAI, Vercel AI, OpenAI Agents, LangGraph, LlamaIndex, etc.)
|
||||
- **Generate working code** using real API references and tested patterns
|
||||
- **Search live docs** on demand for the latest Mem0 documentation
|
||||
|
||||
## Installation
|
||||
|
||||
This skill is included automatically when you install the Mem0 plugin:
|
||||
|
||||
```
|
||||
/plugin marketplace add mem0ai/mem0
|
||||
/plugin install mem0@mem0-plugins
|
||||
```
|
||||
|
||||
See the [plugin README](../../README.md) for full setup instructions.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- A Mem0 Platform API key ([Get one here](https://app.mem0.ai/dashboard/api-keys))
|
||||
- Python 3.10+ or Node.js 18+
|
||||
- Set the environment variable:
|
||||
|
||||
```bash
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
After installing, just ask Claude:
|
||||
|
||||
- "Set up mem0 in my project"
|
||||
- "Add memory to my chatbot"
|
||||
- "Help me search user memories with filters"
|
||||
- "Integrate mem0 with my LangChain app"
|
||||
- "Add graph memory to track entity relationships"
|
||||
|
||||
## What's Inside
|
||||
|
||||
```text
|
||||
skills/mem0/
|
||||
├── SKILL.md # Skill definition and instructions
|
||||
├── README.md # This file
|
||||
├── LICENSE # Apache-2.0
|
||||
├── scripts/
|
||||
│ └── mem0_doc_search.py # Search live Mem0 docs on demand
|
||||
└── references/ # Documentation (loaded on demand)
|
||||
├── quickstart.md # Full quickstart (Python, TS, cURL)
|
||||
├── sdk-guide.md # All SDK methods (Python + TypeScript)
|
||||
├── api-reference.md # REST endpoints, filters, memory object
|
||||
├── architecture.md # Processing pipeline, lifecycle, scoping, performance
|
||||
├── features.md # Retrieval, graph, categories, MCP, webhooks, multimodal
|
||||
├── integration-patterns.md # LangChain, CrewAI, Vercel AI, LangGraph, LlamaIndex, etc.
|
||||
└── use-cases.md # 7 real-world patterns with Python + TypeScript code
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- [Mem0 Platform Dashboard](https://app.mem0.ai)
|
||||
- [Mem0 Documentation](https://docs.mem0.ai)
|
||||
- [Mem0 GitHub](https://github.com/mem0ai/mem0)
|
||||
- [API Reference](https://docs.mem0.ai/api-reference)
|
||||
|
||||
## License
|
||||
|
||||
Apache-2.0
|
||||
@@ -0,0 +1,156 @@
|
||||
---
|
||||
name: mem0
|
||||
description: >
|
||||
Integrate Mem0 Platform into AI applications for persistent memory, personalization, and semantic search.
|
||||
Use this skill when the user mentions "mem0", "memory layer", "remember user preferences",
|
||||
"persistent context", "personalization", or needs to add long-term memory to chatbots, agents,
|
||||
or AI apps. Covers Python and TypeScript SDKs, framework integrations (LangChain, CrewAI,
|
||||
Vercel AI SDK, OpenAI Agents SDK, Pipecat), and the full Platform API. Use even when the user
|
||||
doesn't explicitly say "mem0" but describes needing conversation memory, user context retention,
|
||||
or knowledge retrieval across sessions.
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: mem0ai
|
||||
version: "0.1.0"
|
||||
category: ai-memory
|
||||
tags: "memory, personalization, ai, python, typescript, vector-search"
|
||||
compatibility: Requires Python 3.10+ or Node.js 18+, pip install mem0ai or npm install mem0ai, MEM0_API_KEY env var, and internet access to api.mem0.ai
|
||||
---
|
||||
|
||||
# Mem0 Platform Integration
|
||||
|
||||
Mem0 is a managed memory layer for AI applications. It stores, retrieves, and manages user memories via API — no infrastructure to deploy.
|
||||
|
||||
## Step 1: Install and authenticate
|
||||
|
||||
**Python:**
|
||||
```bash
|
||||
pip install mem0ai
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
```
|
||||
|
||||
**TypeScript/JavaScript:**
|
||||
```bash
|
||||
npm install mem0ai
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
```
|
||||
|
||||
Get an API key at: https://app.mem0.ai/dashboard/api-keys
|
||||
|
||||
## Step 2: Initialize the client
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
client = MemoryClient(api_key="m0-xxx")
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
import MemoryClient from 'mem0ai';
|
||||
const client = new MemoryClient({ apiKey: 'm0-xxx' });
|
||||
```
|
||||
|
||||
For async Python, use `AsyncMemoryClient`.
|
||||
|
||||
## Step 3: Core operations
|
||||
|
||||
Every Mem0 integration follows the same pattern: **retrieve → generate → store**.
|
||||
|
||||
### Add memories
|
||||
```python
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
|
||||
{"role": "assistant", "content": "Got it! I'll remember that."}
|
||||
]
|
||||
client.add(messages, user_id="alice")
|
||||
```
|
||||
|
||||
### Search memories
|
||||
```python
|
||||
results = client.search("dietary preferences", user_id="alice")
|
||||
for mem in results.get("results", []):
|
||||
print(mem["memory"])
|
||||
```
|
||||
|
||||
### Get all memories
|
||||
```python
|
||||
all_memories = client.get_all(user_id="alice")
|
||||
```
|
||||
|
||||
### Update a memory
|
||||
```python
|
||||
client.update("memory-uuid", text="Updated: vegetarian, nut allergy, prefers organic")
|
||||
```
|
||||
|
||||
### Delete a memory
|
||||
```python
|
||||
client.delete("memory-uuid")
|
||||
client.delete_all(user_id="alice") # delete all for a user
|
||||
```
|
||||
|
||||
## Common integration pattern
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
from openai import OpenAI
|
||||
|
||||
mem0 = MemoryClient()
|
||||
openai = OpenAI()
|
||||
|
||||
def chat(user_input: str, user_id: str) -> str:
|
||||
# 1. Retrieve relevant memories
|
||||
memories = mem0.search(user_input, user_id=user_id)
|
||||
context = "\n".join([m["memory"] for m in memories.get("results", [])])
|
||||
|
||||
# 2. Generate response with memory context
|
||||
response = openai.chat.completions.create(
|
||||
model="gpt-4.1-nano-2025-04-14",
|
||||
messages=[
|
||||
{"role": "system", "content": f"User context:\n{context}"},
|
||||
{"role": "user", "content": user_input},
|
||||
]
|
||||
)
|
||||
reply = response.choices[0].message.content
|
||||
|
||||
# 3. Store interaction for future context
|
||||
mem0.add(
|
||||
[{"role": "user", "content": user_input}, {"role": "assistant", "content": reply}],
|
||||
user_id=user_id
|
||||
)
|
||||
return reply
|
||||
```
|
||||
|
||||
## Common edge cases
|
||||
|
||||
- **Search returns empty:** Memories process asynchronously. Wait 2-3s after `add()` before searching. Also verify `user_id` matches exactly (case-sensitive).
|
||||
- **AND filter with user_id + agent_id returns empty:** Entities are stored separately. Use `OR` instead, or query separately.
|
||||
- **Duplicate memories:** Don't mix `infer=True` (default) and `infer=False` for the same data. Stick to one mode.
|
||||
- **Wrong import:** Always use `from mem0 import MemoryClient` (or `AsyncMemoryClient` for async). Do not use `from mem0 import Memory`.
|
||||
- **Immutable memories:** Cannot be updated or deleted once created. Use `client.history(memory_id)` to track changes over time.
|
||||
|
||||
## Live documentation search
|
||||
|
||||
For the latest docs beyond what's in the references, use the doc search tool:
|
||||
|
||||
```bash
|
||||
python ${CLAUDE_SKILL_DIR}/scripts/mem0_doc_search.py --query "topic"
|
||||
python ${CLAUDE_SKILL_DIR}/scripts/mem0_doc_search.py --page "/platform/features/graph-memory"
|
||||
python ${CLAUDE_SKILL_DIR}/scripts/mem0_doc_search.py --index
|
||||
```
|
||||
|
||||
No API key needed — searches docs.mem0.ai directly.
|
||||
|
||||
## References
|
||||
|
||||
Load these on demand for deeper detail:
|
||||
|
||||
| Topic | File |
|
||||
|-------|------|
|
||||
| Quickstart (Python, TS, cURL) | [references/quickstart.md](references/quickstart.md) |
|
||||
| SDK guide (all methods, both languages) | [references/sdk-guide.md](references/sdk-guide.md) |
|
||||
| API reference (endpoints, filters, object schema) | [references/api-reference.md](references/api-reference.md) |
|
||||
| Architecture (pipeline, lifecycle, scoping, performance) | [references/architecture.md](references/architecture.md) |
|
||||
| Platform features (retrieval, graph, categories, MCP, etc.) | [references/features.md](references/features.md) |
|
||||
| Framework integrations (LangChain, CrewAI, Vercel AI, etc.) | [references/integration-patterns.md](references/integration-patterns.md) |
|
||||
| Use cases & examples (real-world patterns with code) | [references/use-cases.md](references/use-cases.md) |
|
||||
@@ -0,0 +1,140 @@
|
||||
# Mem0 Platform API Reference
|
||||
|
||||
REST API endpoints for the Mem0 Platform. Base URL: `https://api.mem0.ai`
|
||||
|
||||
All endpoints require: `Authorization: Token <MEM0_API_KEY>`
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Operation | Method | URL |
|
||||
|-----------|--------|-----|
|
||||
| Add Memories | `POST` | `/v1/memories/` |
|
||||
| Search Memories | `POST` | `/v2/memories/search/` |
|
||||
| Get All Memories | `POST` | `/v2/memories/` |
|
||||
| Get Single Memory | `GET` | `/v1/memories/{memory_id}/` |
|
||||
| Update Memory | `PUT` | `/v1/memories/{memory_id}/` |
|
||||
| Delete Memory | `DELETE` | `/v1/memories/{memory_id}/` |
|
||||
|
||||
## Memory Object Structure
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | string (UUID) | Unique memory identifier |
|
||||
| `memory` | string | Text content of the memory |
|
||||
| `user_id` | string | Associated user |
|
||||
| `agent_id` | string (nullable) | Agent identifier |
|
||||
| `app_id` | string (nullable) | Application identifier |
|
||||
| `run_id` | string (nullable) | Run/session identifier |
|
||||
| `metadata` | object | Custom key-value pairs |
|
||||
| `categories` | array of strings | Auto-assigned category tags |
|
||||
| `immutable` | boolean | If true, prevents modification |
|
||||
| `expiration_date` | datetime (nullable) | Auto-expiry date |
|
||||
| `hash` | string | Content hash |
|
||||
| `created_at` | datetime | Creation timestamp |
|
||||
| `updated_at` | datetime | Last modification timestamp |
|
||||
|
||||
Search results additionally include `score` (relevance metric).
|
||||
|
||||
## Scoping Identifiers
|
||||
|
||||
Memories can be scoped to different levels:
|
||||
|
||||
| Scope | Parameter | Use Case |
|
||||
|-------|-----------|----------|
|
||||
| User | `user_id` | Per-user memory isolation |
|
||||
| Agent | `agent_id` | Per-agent memory partitioning |
|
||||
| Application | `app_id` | Cross-agent app-level memory |
|
||||
| Run/Session | `run_id` | Session-scoped temporary memory |
|
||||
|
||||
**Critical:** Combining `user_id` and `agent_id` in a single AND filter yields empty results. Entities are stored separately. Use `OR` logic or separate queries.
|
||||
|
||||
## Processing Model
|
||||
|
||||
- Memories are processed **asynchronously by default** (`async_mode=true`)
|
||||
- Add responses return queued events (`ADD`, `UPDATE`, `DELETE`) for tracking
|
||||
- Set `async_mode=false` for synchronous processing when needed
|
||||
- Graph metadata is processed asynchronously -- use `get_all()` for complete graph data
|
||||
|
||||
## Filter System
|
||||
|
||||
Filters use nested JSON with a logical operator at the root:
|
||||
|
||||
```json
|
||||
{
|
||||
"AND": [
|
||||
{"user_id": "alice"},
|
||||
{"categories": {"contains": "finance"}},
|
||||
{"created_at": {"gte": "2024-01-01"}}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Root must be `AND`, `OR`, or `NOT`. Simple shorthand `{"user_id": "alice"}` also works.
|
||||
|
||||
### Supported Operators
|
||||
|
||||
| Operator | Description |
|
||||
|----------|-------------|
|
||||
| `eq` | Equal to (default) |
|
||||
| `ne` | Not equal to |
|
||||
| `in` | Matches any value in array |
|
||||
| `gt`, `gte` | Greater than / greater than or equal |
|
||||
| `lt`, `lte` | Less than / less than or equal |
|
||||
| `contains` | Case-sensitive containment |
|
||||
| `icontains` | Case-insensitive containment |
|
||||
| `*` | Wildcard -- matches any non-null value |
|
||||
|
||||
### Filterable Fields
|
||||
|
||||
| Field | Valid Operators |
|
||||
|-------|-----------------|
|
||||
| `user_id`, `agent_id`, `app_id`, `run_id` | `eq`, `ne`, `in`, `*` |
|
||||
| `created_at`, `updated_at`, `timestamp` | `gt`, `gte`, `lt`, `lte`, `eq`, `ne` |
|
||||
| `categories` | `eq`, `ne`, `in`, `contains` |
|
||||
| `metadata` | `eq`, `ne`, `contains` (top-level keys only) |
|
||||
| `keywords` | `contains`, `icontains` |
|
||||
| `memory_ids` | `in` |
|
||||
|
||||
### Filter Constraints
|
||||
|
||||
1. **Entity scope partitioning:** `user_id` AND `agent_id` in one `AND` block yields empty results.
|
||||
2. **Metadata limitations:** Only top-level keys. Only `eq`, `contains`, `ne`. No `in` or `gt`.
|
||||
3. **Operator syntax:** Use `gte`, `lt`, `ne`. SQL-style (`>=`, `!=`) rejected.
|
||||
4. **Entity filter required for get-all:** At least one of `user_id`, `agent_id`, `app_id`, or `run_id`.
|
||||
5. **Wildcard excludes null:** `*` matches only non-null values.
|
||||
6. **Date format:** ISO 8601 (`YYYY-MM-DDTHH:MM:SSZ`). Timezone-naive defaults to UTC.
|
||||
|
||||
## Response Formats
|
||||
|
||||
### Add Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "mem_01JF8ZS4Y0R0SPM13R5R6H32CJ",
|
||||
"event": "ADD",
|
||||
"data": { "memory": "The user moved to Austin in 2025." }
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Event types: `ADD`, `UPDATE`, `DELETE`. A single add can trigger multiple events.
|
||||
|
||||
### Search Response
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "ea925981-...",
|
||||
"memory": "Is a vegetarian and allergic to nuts.",
|
||||
"user_id": "user123",
|
||||
"categories": ["food", "health"],
|
||||
"score": 0.89,
|
||||
"created_at": "2024-07-26T10:29:36.630547-07:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
With `enable_graph=true`, includes additional `relations` array with entity relationships.
|
||||
@@ -0,0 +1,386 @@
|
||||
# Mem0 Platform Architecture
|
||||
|
||||
How Mem0 processes, stores, and retrieves memories under the hood.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Core Concept](#core-concept)
|
||||
- [Memory Processing Pipeline](#memory-processing-pipeline)
|
||||
- [Retrieval Pipeline](#retrieval-pipeline)
|
||||
- [Memory Lifecycle](#memory-lifecycle)
|
||||
- [Memory Object Structure](#memory-object-structure)
|
||||
- [Scoping & Multi-Tenancy](#scoping--multi-tenancy)
|
||||
- [Memory Layers](#memory-layers)
|
||||
- [Performance Characteristics](#performance-characteristics)
|
||||
|
||||
---
|
||||
|
||||
## Core Concept
|
||||
|
||||
Mem0 is a managed memory layer that sits between your AI application and users. Every integration follows the same 3-step loop:
|
||||
|
||||
```
|
||||
User Input → Retrieve relevant memories → Enrich LLM prompt → Generate response → Store new memories
|
||||
```
|
||||
|
||||
Mem0 handles the complexity of extraction, deduplication, conflict resolution, and semantic retrieval so your application only needs to call `search()` and `add()`.
|
||||
|
||||
**Dual storage architecture:**
|
||||
- **Vector store**: Embeddings for semantic similarity search
|
||||
- **Graph store** (optional): Entity nodes and relationship edges for structured knowledge
|
||||
|
||||
---
|
||||
|
||||
## Memory Processing Pipeline
|
||||
|
||||
### What happens when you call `client.add()`
|
||||
|
||||
```
|
||||
Messages In
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 1. EXTRACTION │ LLM analyzes messages, extracts key facts
|
||||
│ (infer=True) │ If infer=False, stores raw text as-is
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 2. CONFLICT │ Checks existing memories for duplicates
|
||||
│ RESOLUTION │ Latest truth wins (newer overrides older)
|
||||
│ │ Only runs when infer=True
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 3. STORAGE │ Generates embeddings → vector store
|
||||
│ │ Optional: entity extraction → graph store
|
||||
│ │ Indexes metadata, categories, timestamps
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
Memory Object
|
||||
(id, memory, categories, structured_attributes)
|
||||
```
|
||||
|
||||
### Processing modes
|
||||
|
||||
**Async (default, `async_mode=True`):**
|
||||
- API returns immediately: `{"status": "PENDING", "event_id": "..."}`
|
||||
- Processing happens in background
|
||||
- Use webhooks for completion notifications
|
||||
- Best for: high-throughput, non-blocking workflows
|
||||
|
||||
**Sync (`async_mode=False`):**
|
||||
- API waits for full processing
|
||||
- Returns complete memory object with `id`, `event`, `memory`
|
||||
- Best for: real-time access immediately after add
|
||||
|
||||
### Extraction modes
|
||||
|
||||
**Inferred (`infer=True`, default):**
|
||||
- LLM extracts structured facts from conversation
|
||||
- Conflict resolution deduplicates and resolves contradictions
|
||||
- Best for: natural conversation → memory
|
||||
|
||||
**Raw (`infer=False`):**
|
||||
- Stores text exactly as provided, no LLM processing
|
||||
- Skips conflict resolution — same fact can be stored twice
|
||||
- Only `user` role messages are stored; `assistant` messages ignored
|
||||
- Best for: bulk imports, pre-structured data, migrations
|
||||
|
||||
**Warning:** Don't mix `infer=True` and `infer=False` for the same data — the same fact will be stored twice.
|
||||
|
||||
---
|
||||
|
||||
## Retrieval Pipeline
|
||||
|
||||
### What happens when you call `client.search()`
|
||||
|
||||
```
|
||||
Query In
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 1. QUERY EMBEDDING │ Convert query to vector representation
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 2. VECTOR SEARCH │ Cosine similarity across stored embeddings
|
||||
│ │ Scoped by filters (user_id, agent_id, etc.)
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼ (optional enhancements)
|
||||
┌─────────────────────┐
|
||||
│ 3a. KEYWORD SEARCH │ Expands results with specific terms (+10ms)
|
||||
│ 3b. RERANKING │ Deep semantic reordering (+150-200ms)
|
||||
│ 3c. FILTER MEMORIES │ Precision filtering, removes low-relevance (+200-300ms)
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼ (if enable_graph=True)
|
||||
┌─────────────────────┐
|
||||
│ 4. GRAPH LOOKUP │ Finds entity relationships
|
||||
│ │ Appends relations WITHOUT reranking vector results
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
Results + Relations
|
||||
```
|
||||
|
||||
### Retrieval enhancement combinations
|
||||
|
||||
| Configuration | Latency | Best for |
|
||||
|--------------|---------|----------|
|
||||
| Base search only | ~100ms | Simple lookups |
|
||||
| `keyword_search=True` | ~110ms | Entity-heavy queries, broad coverage |
|
||||
| `rerank=True` | ~250-300ms | User-facing results, top-N precision |
|
||||
| `keyword_search=True` + `rerank=True` | ~310ms | Balanced (recommended for most apps) |
|
||||
| `rerank=True` + `filter_memories=True` | ~400-500ms | Safety-critical, production systems |
|
||||
|
||||
### Implicit null scoping
|
||||
|
||||
When you search with `user_id="alice"` only, Mem0 returns memories where `agent_id`, `app_id`, and `run_id` are all null. This prevents cross-scope leakage by default.
|
||||
|
||||
To include memories with non-null fields, use explicit filters:
|
||||
```python
|
||||
# Gets memories for alice regardless of agent/app/run
|
||||
filters={"OR": [{"user_id": "alice"}]}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Lifecycle
|
||||
|
||||
```
|
||||
CREATE ──→ ACTIVE ──→ UPDATE ──→ ACTIVE
|
||||
│ │ │
|
||||
│ ▼ ▼
|
||||
│ EXPIRED EXPIRED
|
||||
│ (still stored, (still stored,
|
||||
│ not retrieved) not retrieved)
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
DELETE DELETE DELETE
|
||||
(permanent)
|
||||
```
|
||||
|
||||
### Creation
|
||||
- Triggered by `client.add(messages, user_id="...")`
|
||||
- Messages processed through extraction → conflict resolution → storage
|
||||
- Gets unique UUID, `created_at` timestamp
|
||||
- Optional: custom `timestamp`, `expiration_date`, `metadata`, `immutable`
|
||||
|
||||
### Updates
|
||||
- `client.update(memory_id, text="...")` replaces text and reindexes
|
||||
- `client.batch_update([...])` for up to 1000 memories at once
|
||||
- Immutable memories (`immutable=True`) cannot be updated — must delete and re-add
|
||||
|
||||
### Deduplication
|
||||
- Automatic during `add()` with `infer=True`
|
||||
- Conflict resolution merges duplicate facts
|
||||
- Latest truth wins when contradictions detected
|
||||
- Prevents memory bloat from repeated information
|
||||
|
||||
### Expiration
|
||||
- Optional `expiration_date` parameter (ISO 8601 or `YYYY-MM-DD`)
|
||||
- After expiration: memory NOT returned in searches but remains in storage
|
||||
- Useful for time-sensitive info (events, temporary preferences, session state)
|
||||
|
||||
### Deletion
|
||||
- Single: `client.delete(memory_id)` — permanent, no recovery
|
||||
- Batch: `client.batch_delete([memory_ids])` — up to 1000
|
||||
- Bulk: `client.delete_all(user_id="alice")` — all memories for entity
|
||||
- `delete_all()` without filters raises error to prevent accidental data loss
|
||||
|
||||
### History tracking
|
||||
- `client.history(memory_id)` returns version timeline
|
||||
- Shows all changes: `{previous_value, new_value, action, timestamps}`
|
||||
- Useful for audit trails and debugging
|
||||
|
||||
---
|
||||
|
||||
## Memory Object Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid-string",
|
||||
"memory": "Extracted memory text",
|
||||
"user_id": "user-identifier",
|
||||
"agent_id": null,
|
||||
"app_id": null,
|
||||
"run_id": null,
|
||||
"metadata": { "source": "chat", "priority": "high" },
|
||||
"categories": ["health", "preferences"],
|
||||
"created_at": "2025-03-12T12:34:56Z",
|
||||
"updated_at": "2025-03-12T12:34:56Z",
|
||||
"expiration_date": null,
|
||||
"immutable": false,
|
||||
"structured_attributes": {
|
||||
"day": 12, "month": 3, "year": 2025,
|
||||
"hour": 12, "minute": 34,
|
||||
"day_of_week": "wednesday",
|
||||
"is_weekend": false,
|
||||
"quarter": 1, "week_of_year": 11
|
||||
},
|
||||
"score": 0.85
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `id` | UUID | Unique identifier, used for update/delete |
|
||||
| `memory` | string | Extracted or stored text content |
|
||||
| `user_id` | string | Primary entity scope |
|
||||
| `agent_id` | string | Agent scope |
|
||||
| `app_id` | string | Application scope |
|
||||
| `run_id` | string | Session/run scope |
|
||||
| `metadata` | object | Custom key-value pairs for filtering |
|
||||
| `categories` | array | Auto-assigned or custom category tags |
|
||||
| `created_at` | datetime | Creation timestamp |
|
||||
| `updated_at` | datetime | Last modification timestamp |
|
||||
| `expiration_date` | datetime | Auto-expiry date (stops retrieval, data persists) |
|
||||
| `immutable` | boolean | If true, prevents modification |
|
||||
| `structured_attributes` | object | Temporal breakdown for time-based queries |
|
||||
| `score` | float | Semantic similarity (search results only, 0-1) |
|
||||
|
||||
---
|
||||
|
||||
## Scoping & Multi-Tenancy
|
||||
|
||||
Mem0 separates memories across four dimensions to prevent data mixing:
|
||||
|
||||
| Dimension | Field | Purpose | Example |
|
||||
|-----------|-------|---------|---------|
|
||||
| User | `user_id` | Persistent persona or account | `"customer_6412"` |
|
||||
| Agent | `agent_id` | Distinct agent or tool | `"meal_planner"` |
|
||||
| App | `app_id` | Product surface or deployment | `"ios_retail_app"` |
|
||||
| Session | `run_id` | Short-lived flow or thread | `"ticket-9241"` |
|
||||
|
||||
### Storage model
|
||||
|
||||
Each entity combination creates separate records. A memory with `user_id="alice"` is stored separately from one with `user_id="alice"` + `agent_id="bot"`.
|
||||
|
||||
### Critical: cross-entity queries
|
||||
|
||||
```python
|
||||
# This returns NOTHING — user and agent memories are stored separately
|
||||
filters={"AND": [{"user_id": "alice"}, {"agent_id": "bot"}]}
|
||||
|
||||
# Use OR to query multiple scopes
|
||||
filters={"OR": [{"user_id": "alice"}, {"agent_id": "bot"}]}
|
||||
|
||||
# Use wildcard to include any non-null value
|
||||
filters={"AND": [{"user_id": "*"}]} # All users (excludes null)
|
||||
```
|
||||
|
||||
### Recommended scoping patterns
|
||||
|
||||
```python
|
||||
# User-level: persistent preferences
|
||||
client.add(messages, user_id="alice")
|
||||
|
||||
# Session-level: temporary context
|
||||
client.add(messages, user_id="alice", run_id="session_123")
|
||||
# Clean up when done: client.delete_all(run_id="session_123")
|
||||
|
||||
# Agent-level: agent-specific knowledge
|
||||
client.add(messages, agent_id="support_bot", app_id="helpdesk")
|
||||
|
||||
# Multi-tenant: full isolation
|
||||
client.add(messages, user_id="alice", agent_id="bot", app_id="acme_corp", run_id="ticket_42")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Layers
|
||||
|
||||
Mem0 supports three layers of memory, from shortest to longest lived:
|
||||
|
||||
### Conversation memory
|
||||
- In-flight messages within a single turn
|
||||
- Tool calls, chain-of-thought reasoning
|
||||
- **Lifetime:** Single response — lost after turn finishes
|
||||
- **Managed by:** Your application, not Mem0
|
||||
|
||||
### Session memory
|
||||
- Short-lived facts for current task or channel
|
||||
- Multi-step flows (onboarding, debugging, support tickets)
|
||||
- **Lifetime:** Minutes to hours
|
||||
- **Managed by:** Mem0 via `run_id` parameter
|
||||
- Clean up with `client.delete_all(run_id="session_id")`
|
||||
|
||||
### User memory
|
||||
- Long-lived knowledge tied to a person or account
|
||||
- Personal preferences, account state, compliance details
|
||||
- **Lifetime:** Weeks to forever
|
||||
- **Managed by:** Mem0 via `user_id` parameter
|
||||
- Persists across all sessions and interactions
|
||||
|
||||
### How layering works in practice
|
||||
|
||||
```python
|
||||
def chat(user_input: str, user_id: str, session_id: str) -> str:
|
||||
# 1. Retrieve user memories (long-term preferences)
|
||||
user_mems = mem0.search(user_input, user_id=user_id)
|
||||
|
||||
# 2. Retrieve session memories (current task context)
|
||||
session_mems = mem0.search(user_input, filters={
|
||||
"AND": [{"user_id": user_id}, {"run_id": session_id}]
|
||||
})
|
||||
|
||||
# 3. Combine both layers for LLM context
|
||||
context = format_memories(user_mems) + format_memories(session_mems)
|
||||
|
||||
# 4. Generate response
|
||||
response = llm.generate(context=context, input=user_input)
|
||||
|
||||
# 5. Store in session scope (temporary) + user scope (persistent)
|
||||
messages = [{"role": "user", "content": user_input}, {"role": "assistant", "content": response}]
|
||||
mem0.add(messages, user_id=user_id, run_id=session_id)
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Latency
|
||||
|
||||
| Operation | Typical Latency |
|
||||
|-----------|----------------|
|
||||
| Base vector search | ~100ms |
|
||||
| + keyword_search | +10ms |
|
||||
| + reranking | +150-200ms |
|
||||
| + filter_memories | +200-300ms |
|
||||
| Add (async, default) | < 50ms response, background processing |
|
||||
| Add (sync) | 500ms-2s depending on extraction complexity |
|
||||
| Graph operations | Slight overhead for large stores |
|
||||
|
||||
### Processing
|
||||
|
||||
- **Async mode (default):** Returns immediately, processes in background
|
||||
- **Sync mode:** Waits for full extraction + storage pipeline
|
||||
- **Batch operations:** Up to 1000 memories per batch_update/batch_delete
|
||||
- **Webhooks:** Real-time notifications when async processing completes
|
||||
|
||||
### Scoping strategy for performance
|
||||
|
||||
- Use `user_id` for all user-facing queries (most common, fastest)
|
||||
- Add `run_id` for session isolation (narrows search space)
|
||||
- Avoid wildcard `"*"` filters on large datasets (scans all non-null records)
|
||||
- Use `top_k` to limit result count when you only need a few memories
|
||||
|
||||
---
|
||||
|
||||
## Comparison with Alternatives
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| **Raw vector DB** | Fast, full control | No extraction, no dedup, no conflict resolution |
|
||||
| **In-memory chat history** | Zero latency | Lost on restart, no cross-session, grows unbounded |
|
||||
| **RAG over documents** | Good for static knowledge | No personalization, no memory updates |
|
||||
| **Mem0 Platform** | Managed extraction + dedup + graph + scoping | External dependency, async processing delay |
|
||||
|
||||
Mem0 combines the best of vector search (semantic retrieval) with automatic extraction (LLM-powered), conflict resolution (deduplication), and structured scoping (multi-tenancy) — in a single managed API.
|
||||
@@ -0,0 +1,496 @@
|
||||
# Platform Features -- Mem0 Platform
|
||||
|
||||
Additional platform capabilities beyond core CRUD operations.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Advanced Retrieval](#advanced-retrieval)
|
||||
- [Graph Memory](#graph-memory)
|
||||
- [Custom Categories](#custom-categories)
|
||||
- [Custom Instructions](#custom-instructions)
|
||||
- [Criteria Retrieval](#criteria-retrieval)
|
||||
- [Feedback Mechanism](#feedback-mechanism)
|
||||
- [Memory Export](#memory-export)
|
||||
- [Group Chat](#group-chat)
|
||||
- [MCP Integration](#mcp-integration)
|
||||
- [Webhooks](#webhooks)
|
||||
- [Multimodal Support](#multimodal-support)
|
||||
|
||||
## Advanced Retrieval
|
||||
|
||||
Three enhancement options for tuning search precision, recall, and latency.
|
||||
|
||||
### Keyword Search (`keyword_search=True`)
|
||||
|
||||
Expands results to include memories with specific terms, names, and technical keywords.
|
||||
|
||||
- Latency: +10ms
|
||||
- Recall: Significantly increased
|
||||
- Best for: entity-heavy queries, comprehensive coverage
|
||||
|
||||
### Reranking (`rerank=True`)
|
||||
|
||||
Deep semantic reordering of results — most relevant first.
|
||||
|
||||
- Latency: +150-200ms
|
||||
- Accuracy: Significantly improved
|
||||
- Best for: user-facing results, top-N precision
|
||||
|
||||
### Filter Memories (`filter_memories=True`)
|
||||
|
||||
Precision filtering — removes low-relevance results entirely.
|
||||
|
||||
- Latency: +200-300ms
|
||||
- Precision: Maximized
|
||||
- Best for: safety-critical applications, production systems
|
||||
|
||||
### Recommended Combinations
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
# Fast & broad
|
||||
results = client.search(query, keyword_search=True, user_id="user123")
|
||||
|
||||
# Balanced (recommended for most apps)
|
||||
results = client.search(query, keyword_search=True, rerank=True, user_id="user123")
|
||||
|
||||
# High precision (critical apps)
|
||||
results = client.search(query, rerank=True, filter_memories=True, user_id="user123")
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
const results = await client.search(query, {
|
||||
user_id: 'user123',
|
||||
keyword_search: true,
|
||||
rerank: true,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Graph Memory
|
||||
|
||||
Entity-level knowledge graph that creates relationships between memories.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Extraction**: LLM analyzes conversation and identifies entities and relationships
|
||||
2. **Storage**: Embeddings go to vector store; entity nodes and edges go to graph store
|
||||
3. **Retrieval**: Vector search returns semantic matches; graph relations are appended to results
|
||||
|
||||
Graph relations **augment** vector results without reordering them. Vector similarity always determines hit sequence.
|
||||
|
||||
### Enabling Graph Memory
|
||||
|
||||
**Per request:**
|
||||
```python
|
||||
client.add(messages, user_id="alice", enable_graph=True)
|
||||
client.search("query", user_id="alice", enable_graph=True)
|
||||
client.get_all(filters={"AND": [{"user_id": "alice"}]}, enable_graph=True)
|
||||
```
|
||||
|
||||
**Project-level (default for all operations):**
|
||||
```python
|
||||
client.project.update(enable_graph=True)
|
||||
```
|
||||
|
||||
```javascript
|
||||
await client.updateProject({ enable_graph: true });
|
||||
```
|
||||
|
||||
### Relation Structure
|
||||
|
||||
Each relation in the response contains:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `source` | string | Source entity name |
|
||||
| `source_type` | string | Source entity type (e.g., "Person") |
|
||||
| `relationship` | string | Relationship label (e.g., "lives_in") |
|
||||
| `target` | string | Target entity name |
|
||||
| `target_type` | string | Target entity type (e.g., "City") |
|
||||
| `score` | number | Confidence score |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"relations": [
|
||||
{
|
||||
"source": "Joseph",
|
||||
"source_type": "Person",
|
||||
"relationship": "lives_in",
|
||||
"target": "Seattle",
|
||||
"target_type": "City",
|
||||
"score": 0.92
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Technical Notes
|
||||
|
||||
- Graph Memory adds processing time; see docs for current plan availability
|
||||
- Works optimally with rich conversation histories containing entity relationships
|
||||
- Best suited for long-running assistants tracking evolving information
|
||||
- Graph writes and reads toggle independently per request
|
||||
- Multi-agent context supported via `user_id`, `agent_id`, `run_id` scoping
|
||||
- Add operations are asynchronous; graph metadata may not be immediately available
|
||||
|
||||
---
|
||||
|
||||
## Custom Categories
|
||||
|
||||
Replace Mem0's default 15 labels with domain-specific categories. The system automatically tags memories to the closest matching category.
|
||||
|
||||
### Default Categories (15)
|
||||
|
||||
`personal_details`, `family`, `professional_details`, `sports`, `travel`, `food`, `music`, `health`, `technology`, `hobbies`, `fashion`, `entertainment`, `milestones`, `user_preferences`, `misc`
|
||||
|
||||
### Configuration
|
||||
|
||||
**Set project-level categories:**
|
||||
```python
|
||||
new_categories = [
|
||||
{"lifestyle_management": "Tracks daily routines, habits, wellness activities"},
|
||||
{"seeking_structure": "Documents goals around creating routines and systems"},
|
||||
{"personal_information": "Basic information about the user"}
|
||||
]
|
||||
client.project.update(custom_categories=new_categories)
|
||||
```
|
||||
|
||||
```javascript
|
||||
await client.updateProject({ custom_categories: new_categories });
|
||||
```
|
||||
|
||||
**Retrieve active categories:**
|
||||
```python
|
||||
categories = client.project.get(fields=["custom_categories"])
|
||||
```
|
||||
|
||||
### Key Constraint
|
||||
|
||||
Per-request overrides (`custom_categories=...` on `client.add`) are **not supported** on the managed API. Only project-level configuration works. Workaround: store ad-hoc labels in `metadata` field.
|
||||
|
||||
---
|
||||
|
||||
## Custom Instructions
|
||||
|
||||
Natural language filters that control what information Mem0 extracts when creating memories.
|
||||
|
||||
### Set Instructions
|
||||
|
||||
```python
|
||||
client.project.update(custom_instructions="Your guidelines here...")
|
||||
```
|
||||
|
||||
```javascript
|
||||
await client.updateProject({ custom_instructions: "Your guidelines here..." });
|
||||
```
|
||||
|
||||
### Template Structure
|
||||
|
||||
1. **Task Description** -- brief extraction overview
|
||||
2. **Information Categories** -- numbered sections with specific details to capture
|
||||
3. **Processing Guidelines** -- quality and handling rules
|
||||
4. **Exclusion List** -- sensitive/irrelevant data to filter out
|
||||
|
||||
### Domain Examples
|
||||
|
||||
**E-commerce:** Capture product issues, preferences, service experience; exclude payment data.
|
||||
|
||||
**Education:** Extract learning progress, student preferences, performance patterns; exclude specific grades.
|
||||
|
||||
**Finance:** Track financial goals, life events, investment interests; exclude account numbers and SSNs.
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Start simply, test with sample messages, iterate based on results
|
||||
- Avoid overly lengthy instructions
|
||||
- Be specific about what to include AND exclude
|
||||
|
||||
---
|
||||
|
||||
## Criteria Retrieval
|
||||
|
||||
Custom attribute-based memory ranking using LLM-evaluated criteria with weights. Goes beyond semantic similarity to prioritize memories based on domain-specific signals.
|
||||
|
||||
### Configuration
|
||||
|
||||
```python
|
||||
# Define criteria at project level
|
||||
retrieval_criteria = [
|
||||
{"name": "joy", "description": "Positive emotions like happiness and excitement", "weight": 3},
|
||||
{"name": "curiosity", "description": "Inquisitiveness and desire to learn", "weight": 2},
|
||||
{"name": "urgency", "description": "Time-sensitive or high-priority items", "weight": 4},
|
||||
]
|
||||
client.project.update(retrieval_criteria=retrieval_criteria)
|
||||
```
|
||||
|
||||
```typescript
|
||||
await client.updateProject({
|
||||
retrieval_criteria: [
|
||||
{ name: 'joy', description: 'Positive emotions', weight: 3 },
|
||||
{ name: 'urgency', description: 'Time-sensitive items', weight: 4 },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
Once configured, `client.search()` automatically applies criteria ranking:
|
||||
|
||||
```python
|
||||
# Criteria-weighted results returned automatically
|
||||
results = client.search("Why am I feeling happy?", filters={"user_id": "alice"})
|
||||
```
|
||||
|
||||
**Best for:** Wellness assistants, tutoring platforms, productivity tools — any app needing intent-aware retrieval.
|
||||
|
||||
---
|
||||
|
||||
## Feedback Mechanism
|
||||
|
||||
Provide feedback on extracted memories to improve system quality over time.
|
||||
|
||||
### Feedback Types
|
||||
|
||||
| Type | Meaning |
|
||||
|------|---------|
|
||||
| `POSITIVE` | Memory is useful and accurate |
|
||||
| `NEGATIVE` | Memory is not useful |
|
||||
| `VERY_NEGATIVE` | Memory is harmful or completely wrong |
|
||||
| `None` | Clear existing feedback |
|
||||
|
||||
### Usage
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
client.feedback(
|
||||
memory_id="mem-123",
|
||||
feedback="POSITIVE",
|
||||
feedback_reason="Accurately captured dietary preference"
|
||||
)
|
||||
|
||||
# Bulk feedback
|
||||
for item in feedback_data:
|
||||
client.feedback(**item)
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
await client.feedback('mem-123', {
|
||||
feedback: 'POSITIVE',
|
||||
feedback_reason: 'Accurately captured dietary preference',
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Export
|
||||
|
||||
Create structured exports of memories using customizable schemas with filters.
|
||||
|
||||
### Usage
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
# Define export schema
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"preferences": {"type": "array", "items": {"type": "string"}},
|
||||
"health_info": {"type": "string"},
|
||||
}
|
||||
}
|
||||
|
||||
# Create export
|
||||
response = client.create_memory_export(
|
||||
schema=json.dumps(schema),
|
||||
filters={"user_id": "alice"},
|
||||
export_instructions="Create comprehensive profile based on all memories"
|
||||
)
|
||||
|
||||
# Retrieve export (may take a moment to process)
|
||||
result = client.get_memory_export(memory_export_id=response["id"])
|
||||
```
|
||||
|
||||
**Best for:** Data analytics, user profile generation, compliance audits, CRM sync.
|
||||
|
||||
---
|
||||
|
||||
## Group Chat
|
||||
|
||||
Process multi-participant conversations and automatically attribute memories to individual speakers.
|
||||
|
||||
### Usage
|
||||
|
||||
```python
|
||||
messages = [
|
||||
{"role": "user", "name": "Alice", "content": "I think we should use React for the frontend"},
|
||||
{"role": "user", "name": "Bob", "content": "I prefer Vue.js, it's simpler for our use case"},
|
||||
{"role": "assistant", "content": "Both are great choices. Let me note your preferences."},
|
||||
]
|
||||
|
||||
# Mem0 automatically attributes memories to each speaker
|
||||
response = client.add(messages, run_id="team_meeting_1")
|
||||
|
||||
# Retrieve Alice's memories from that session
|
||||
alice_mems = client.get_all(
|
||||
filters={"AND": [{"user_id": "alice"}, {"run_id": "team_meeting_1"}]}
|
||||
)
|
||||
```
|
||||
|
||||
Use the `name` field in messages to identify speakers. Mem0 maps names to entity scopes automatically.
|
||||
|
||||
---
|
||||
|
||||
## MCP Integration
|
||||
|
||||
Model Context Protocol integration enables AI clients (Claude Desktop, Cursor, custom agents) to manage Mem0 memory autonomously.
|
||||
|
||||
### Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"command": "uvx",
|
||||
"args": ["mem0-mcp-server"],
|
||||
"env": {
|
||||
"MEM0_API_KEY": "m0-your-api-key",
|
||||
"MEM0_DEFAULT_USER_ID": "your-user-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available MCP Tools
|
||||
|
||||
The MCP server exposes 9 memory tools that AI agents can use autonomously:
|
||||
- Add, search, get, update, delete memories
|
||||
- Get history, list users, delete users
|
||||
- Search Mem0 documentation
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Configure the MCP server in your AI client
|
||||
2. The agent autonomously decides when to store/retrieve memories
|
||||
3. No manual API calls needed — the agent manages memory as part of its reasoning
|
||||
|
||||
**Best for:** Universal AI client integration — one protocol works everywhere.
|
||||
|
||||
---
|
||||
|
||||
## Webhooks
|
||||
|
||||
Real-time event notifications for memory operations.
|
||||
|
||||
### Supported Events
|
||||
|
||||
| Event | Trigger |
|
||||
|-------|---------|
|
||||
| `memory_add` | Memory created |
|
||||
| `memory_update` | Memory modified |
|
||||
| `memory_delete` | Memory removed |
|
||||
| `memory_categorize` | Memory tagged |
|
||||
|
||||
### Create Webhook
|
||||
|
||||
Note: `project_id` here refers to the Mem0 dashboard project scope for webhooks — not the deprecated client init parameter.
|
||||
|
||||
```python
|
||||
webhook = client.create_webhook(
|
||||
url="https://your-app.com/webhook",
|
||||
name="Memory Logger",
|
||||
project_id="proj_123",
|
||||
event_types=["memory_add", "memory_categorize"]
|
||||
)
|
||||
```
|
||||
|
||||
### Manage Webhooks
|
||||
|
||||
```python
|
||||
# Retrieve
|
||||
webhooks = client.get_webhooks(project_id="proj_123")
|
||||
|
||||
# Update
|
||||
client.update_webhook(
|
||||
name="Updated Logger",
|
||||
url="https://your-app.com/new-webhook",
|
||||
event_types=["memory_update", "memory_add"],
|
||||
webhook_id="wh_123"
|
||||
)
|
||||
|
||||
# Delete
|
||||
client.delete_webhook(webhook_id="wh_123")
|
||||
```
|
||||
|
||||
### Payload Structure
|
||||
|
||||
Memory events contain: ID, data object with memory content, event type (`ADD`/`UPDATE`/`DELETE`).
|
||||
Categorization events contain: memory ID, event type (`CATEGORIZE`), assigned category labels.
|
||||
|
||||
---
|
||||
|
||||
## Multimodal Support
|
||||
|
||||
Mem0 can process images and documents alongside text.
|
||||
|
||||
### Supported Media Types
|
||||
|
||||
- Images: JPG, PNG
|
||||
- Documents: MDX, TXT, PDF
|
||||
|
||||
### Image via URL
|
||||
|
||||
```python
|
||||
image_message = {
|
||||
"role": "user",
|
||||
"content": {
|
||||
"type": "image_url",
|
||||
"image_url": {"url": "https://example.com/image.jpg"}
|
||||
}
|
||||
}
|
||||
client.add([image_message], user_id="alice")
|
||||
```
|
||||
|
||||
### Image via Base64
|
||||
|
||||
```python
|
||||
import base64
|
||||
with open("photo.jpg", "rb") as f:
|
||||
base64_image = base64.b64encode(f.read()).decode("utf-8")
|
||||
|
||||
image_message = {
|
||||
"role": "user",
|
||||
"content": {
|
||||
"type": "image_url",
|
||||
"image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}
|
||||
}
|
||||
}
|
||||
client.add([image_message], user_id="alice")
|
||||
```
|
||||
|
||||
### Document (MDX/TXT)
|
||||
|
||||
```python
|
||||
doc_message = {
|
||||
"role": "user",
|
||||
"content": {"type": "mdx_url", "mdx_url": {"url": document_url}}
|
||||
}
|
||||
client.add([doc_message], user_id="alice")
|
||||
```
|
||||
|
||||
### PDF Document
|
||||
|
||||
```python
|
||||
pdf_message = {
|
||||
"role": "user",
|
||||
"content": {"type": "pdf_url", "pdf_url": {"url": pdf_url}}
|
||||
}
|
||||
client.add([pdf_message], user_id="alice")
|
||||
```
|
||||
@@ -0,0 +1,444 @@
|
||||
# Mem0 Integration Patterns
|
||||
|
||||
Working code examples for integrating Mem0 Platform with popular AI frameworks.
|
||||
All examples use `MemoryClient` (Platform API key).
|
||||
|
||||
Code examples are sourced from official Mem0 integration docs at docs.mem0.ai, simplified for quick reference.
|
||||
|
||||
---
|
||||
|
||||
## Common Pattern
|
||||
|
||||
Every integration follows the same 3-step loop:
|
||||
|
||||
1. **Retrieve** -- search relevant memories before generating a response
|
||||
2. **Generate** -- include memories as context in the LLM prompt
|
||||
3. **Store** -- save the interaction back to Mem0 for future use
|
||||
|
||||
---
|
||||
|
||||
## LangChain
|
||||
|
||||
Source: [docs.mem0.ai/integrations/langchain](https://docs.mem0.ai/integrations/langchain)
|
||||
|
||||
```python
|
||||
from langchain_openai import ChatOpenAI
|
||||
from langchain_core.messages import SystemMessage, HumanMessage
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from mem0 import MemoryClient
|
||||
|
||||
llm = ChatOpenAI(model="gpt-4.1-nano-2025-04-14")
|
||||
mem0 = MemoryClient()
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages([
|
||||
SystemMessage(content="You are a helpful travel agent AI. Use the provided context to personalize your responses."),
|
||||
MessagesPlaceholder(variable_name="context"),
|
||||
HumanMessage(content="{input}")
|
||||
])
|
||||
|
||||
def retrieve_context(query: str, user_id: str):
|
||||
"""Retrieve relevant memories from Mem0"""
|
||||
memories = mem0.search(query, user_id=user_id)
|
||||
memory_list = memories['results']
|
||||
serialized = ' '.join([m["memory"] for m in memory_list])
|
||||
return [
|
||||
{"role": "system", "content": f"Relevant information: {serialized}"},
|
||||
{"role": "user", "content": query}
|
||||
]
|
||||
|
||||
def chat_turn(user_input: str, user_id: str) -> str:
|
||||
# 1. Retrieve
|
||||
context = retrieve_context(user_input, user_id)
|
||||
# 2. Generate
|
||||
chain = prompt | llm
|
||||
response = chain.invoke({"context": context, "input": user_input})
|
||||
# 3. Store
|
||||
mem0.add(
|
||||
[{"role": "user", "content": user_input}, {"role": "assistant", "content": response.content}],
|
||||
user_id=user_id
|
||||
)
|
||||
return response.content
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CrewAI
|
||||
|
||||
Source: [docs.mem0.ai/integrations/crewai](https://docs.mem0.ai/integrations/crewai)
|
||||
|
||||
CrewAI has native Mem0 integration via `memory_config`:
|
||||
|
||||
```python
|
||||
from crewai import Agent, Task, Crew, Process
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient()
|
||||
|
||||
# Store user preferences first
|
||||
messages = [
|
||||
{"role": "user", "content": "I am more of a beach person than a mountain person."},
|
||||
{"role": "assistant", "content": "Noted! I'll recommend beach destinations."},
|
||||
{"role": "user", "content": "I like Airbnb more than hotels."},
|
||||
]
|
||||
client.add(messages, user_id="crew_user_1")
|
||||
|
||||
# Create agent
|
||||
travel_agent = Agent(
|
||||
role="Personalized Travel Planner",
|
||||
goal="Plan personalized travel itineraries",
|
||||
backstory="You are a seasoned travel planner.",
|
||||
memory=True,
|
||||
)
|
||||
|
||||
# Create task
|
||||
task = Task(
|
||||
description="Find places to live, eat, and visit in San Francisco.",
|
||||
expected_output="A detailed list of places to live, eat, and visit.",
|
||||
agent=travel_agent,
|
||||
)
|
||||
|
||||
# Setup crew with Mem0 memory
|
||||
crew = Crew(
|
||||
agents=[travel_agent],
|
||||
tasks=[task],
|
||||
process=Process.sequential,
|
||||
memory=True,
|
||||
memory_config={
|
||||
"provider": "mem0",
|
||||
"config": {"user_id": "crew_user_1"},
|
||||
}
|
||||
)
|
||||
|
||||
result = crew.kickoff()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vercel AI SDK
|
||||
|
||||
Source: [docs.mem0.ai/integrations/vercel-ai-sdk](https://docs.mem0.ai/integrations/vercel-ai-sdk)
|
||||
|
||||
Install: `npm install @mem0/vercel-ai-provider`
|
||||
|
||||
### Basic Text Generation with Memory
|
||||
|
||||
```typescript
|
||||
import { generateText } from "ai";
|
||||
import { createMem0 } from "@mem0/vercel-ai-provider";
|
||||
|
||||
const mem0 = createMem0({
|
||||
provider: "openai",
|
||||
mem0ApiKey: "m0-xxx",
|
||||
apiKey: "openai-api-key",
|
||||
});
|
||||
|
||||
const { text } = await generateText({
|
||||
model: mem0("gpt-4-turbo", { user_id: "borat" }),
|
||||
prompt: "Suggest me a good car to buy!",
|
||||
});
|
||||
```
|
||||
|
||||
### Streaming with Memory
|
||||
|
||||
```typescript
|
||||
import { streamText } from "ai";
|
||||
import { createMem0 } from "@mem0/vercel-ai-provider";
|
||||
|
||||
const mem0 = createMem0();
|
||||
|
||||
const { textStream } = streamText({
|
||||
model: mem0("gpt-4-turbo", { user_id: "borat" }),
|
||||
prompt: "Suggest me a good car to buy!",
|
||||
});
|
||||
|
||||
for await (const textPart of textStream) {
|
||||
process.stdout.write(textPart);
|
||||
}
|
||||
```
|
||||
|
||||
### Using Memory Utilities Standalone
|
||||
|
||||
```typescript
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { generateText } from "ai";
|
||||
import { retrieveMemories, addMemories } from "@mem0/vercel-ai-provider";
|
||||
|
||||
// Retrieve memories and inject into any provider
|
||||
const prompt = "Suggest me a good car to buy.";
|
||||
const memories = await retrieveMemories(prompt, { user_id: "borat", mem0ApiKey: "m0-xxx" });
|
||||
|
||||
const { text } = await generateText({
|
||||
model: openai("gpt-4-turbo"),
|
||||
prompt: prompt,
|
||||
system: memories,
|
||||
});
|
||||
|
||||
// Store new memories
|
||||
await addMemories(
|
||||
[{ role: "user", content: [{ type: "text", text: "I love red cars." }] }],
|
||||
{ user_id: "borat", mem0ApiKey: "m0-xxx" }
|
||||
);
|
||||
```
|
||||
|
||||
### Supported Providers
|
||||
|
||||
`openai`, `anthropic`, `google`, `groq`
|
||||
|
||||
---
|
||||
|
||||
## OpenAI Agents SDK
|
||||
|
||||
Source: [docs.mem0.ai/integrations/openai-agents-sdk](https://docs.mem0.ai/integrations/openai-agents-sdk)
|
||||
|
||||
```python
|
||||
from agents import Agent, Runner, function_tool
|
||||
from mem0 import MemoryClient
|
||||
|
||||
mem0 = MemoryClient()
|
||||
|
||||
@function_tool
|
||||
def search_memory(query: str, user_id: str) -> str:
|
||||
"""Search through past conversations and memories"""
|
||||
memories = mem0.search(query, user_id=user_id, top_k=3)
|
||||
if memories and memories.get('results'):
|
||||
return "\n".join([f"- {mem['memory']}" for mem in memories['results']])
|
||||
return "No relevant memories found."
|
||||
|
||||
@function_tool
|
||||
def save_memory(content: str, user_id: str) -> str:
|
||||
"""Save important information to memory"""
|
||||
mem0.add([{"role": "user", "content": content}], user_id=user_id)
|
||||
return "Information saved to memory."
|
||||
|
||||
agent = Agent(
|
||||
name="Personal Assistant",
|
||||
instructions="""You are a helpful personal assistant with memory capabilities.
|
||||
Use search_memory to recall past conversations.
|
||||
Use save_memory to store important information.""",
|
||||
tools=[search_memory, save_memory],
|
||||
model="gpt-4.1-nano-2025-04-14"
|
||||
)
|
||||
|
||||
result = Runner.run_sync(agent, "I love Italian food and I'm planning a trip to Rome next month")
|
||||
print(result.final_output)
|
||||
```
|
||||
|
||||
### Multi-Agent with Handoffs
|
||||
|
||||
```python
|
||||
from agents import Agent, Runner, function_tool
|
||||
|
||||
travel_agent = Agent(
|
||||
name="Travel Planner",
|
||||
instructions="You are a travel planning specialist. Use search_memory and save_memory tools.",
|
||||
tools=[search_memory, save_memory],
|
||||
model="gpt-4.1-nano-2025-04-14"
|
||||
)
|
||||
|
||||
health_agent = Agent(
|
||||
name="Health Advisor",
|
||||
instructions="You are a health and wellness advisor. Use search_memory and save_memory tools.",
|
||||
tools=[search_memory, save_memory],
|
||||
model="gpt-4.1-nano-2025-04-14"
|
||||
)
|
||||
|
||||
triage_agent = Agent(
|
||||
name="Personal Assistant",
|
||||
instructions="""Route travel questions to Travel Planner, health questions to Health Advisor.""",
|
||||
handoffs=[travel_agent, health_agent],
|
||||
model="gpt-4.1-nano-2025-04-14"
|
||||
)
|
||||
|
||||
result = Runner.run_sync(triage_agent, "Plan a healthy meal for my Italy trip")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pipecat (Voice / Real-Time)
|
||||
|
||||
Source: [docs.mem0.ai/integrations/pipecat](https://docs.mem0.ai/integrations/pipecat)
|
||||
|
||||
```python
|
||||
from pipecat.services.mem0 import Mem0MemoryService
|
||||
|
||||
memory = Mem0MemoryService(
|
||||
api_key=os.getenv("MEM0_API_KEY"),
|
||||
user_id="alice",
|
||||
agent_id="voice_bot",
|
||||
params={
|
||||
"search_limit": 10,
|
||||
"search_threshold": 0.1,
|
||||
"system_prompt": "Here are your past memories:",
|
||||
"add_as_system_message": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Use in pipeline
|
||||
pipeline = Pipeline([
|
||||
transport.input(),
|
||||
stt,
|
||||
user_context,
|
||||
memory, # Memory enhances context automatically
|
||||
llm,
|
||||
transport.output(),
|
||||
assistant_context
|
||||
])
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## LangGraph
|
||||
|
||||
Source: [docs.mem0.ai/integrations/langgraph](https://docs.mem0.ai/integrations/langgraph)
|
||||
|
||||
State-based agent workflows with memory persistence. Best for complex conversation flows with branching logic.
|
||||
|
||||
```python
|
||||
from typing import Annotated, TypedDict, List
|
||||
from langgraph.graph import StateGraph, START
|
||||
from langgraph.graph.message import add_messages
|
||||
from langchain_openai import ChatOpenAI
|
||||
from mem0 import MemoryClient
|
||||
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
|
||||
|
||||
llm = ChatOpenAI(model="gpt-4")
|
||||
mem0 = MemoryClient()
|
||||
|
||||
class State(TypedDict):
|
||||
messages: Annotated[List[HumanMessage | AIMessage], add_messages]
|
||||
mem0_user_id: str
|
||||
|
||||
def chatbot(state: State):
|
||||
messages = state["messages"]
|
||||
user_id = state["mem0_user_id"]
|
||||
|
||||
# Retrieve relevant memories
|
||||
memories = mem0.search(messages[-1].content, user_id=user_id)
|
||||
context = "Relevant context:\n"
|
||||
for memory in memories["results"]:
|
||||
context += f"- {memory['memory']}\n"
|
||||
|
||||
system_message = SystemMessage(content=f"""You are a helpful support assistant.
|
||||
{context}""")
|
||||
|
||||
response = llm.invoke([system_message] + messages)
|
||||
|
||||
# Store the interaction
|
||||
mem0.add(
|
||||
[{"role": "user", "content": messages[-1].content},
|
||||
{"role": "assistant", "content": response.content}],
|
||||
user_id=user_id
|
||||
)
|
||||
return {"messages": [response]}
|
||||
|
||||
graph = StateGraph(State)
|
||||
graph.add_node("chatbot", chatbot)
|
||||
graph.add_edge(START, "chatbot")
|
||||
app = graph.compile()
|
||||
|
||||
# Usage
|
||||
result = app.invoke({
|
||||
"messages": [HumanMessage(content="I need help with my order")],
|
||||
"mem0_user_id": "customer_123"
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LlamaIndex
|
||||
|
||||
Source: [docs.mem0.ai/integrations/llama-index](https://docs.mem0.ai/integrations/llama-index)
|
||||
|
||||
Install: `pip install llama-index-core llama-index-memory-mem0`
|
||||
|
||||
LlamaIndex has native Mem0 support via `Mem0Memory`. Works with ReAct and FunctionCalling agents.
|
||||
|
||||
```python
|
||||
from llama_index.memory.mem0 import Mem0Memory
|
||||
|
||||
context = {"user_id": "alice", "agent_id": "llama_agent_1"}
|
||||
memory = Mem0Memory.from_client(
|
||||
context=context,
|
||||
search_msg_limit=4, # messages from chat history used for retrieval (default: 5)
|
||||
)
|
||||
|
||||
# Use with LlamaIndex agent
|
||||
from llama_index.core.agent import FunctionCallingAgent
|
||||
from llama_index.llms.openai import OpenAI
|
||||
|
||||
llm = OpenAI(model="gpt-4")
|
||||
agent = FunctionCallingAgent.from_tools(
|
||||
tools=[],
|
||||
llm=llm,
|
||||
memory=memory,
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
response = agent.chat("I prefer vegetarian restaurants")
|
||||
# Memory automatically stores and retrieves context
|
||||
response = agent.chat("What kind of food do I like?")
|
||||
# Agent retrieves the vegetarian preference from Mem0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AutoGen
|
||||
|
||||
Source: [docs.mem0.ai/integrations/autogen](https://docs.mem0.ai/integrations/autogen)
|
||||
|
||||
Install: `pip install autogen mem0ai`
|
||||
|
||||
Multi-agent conversational systems with memory persistence.
|
||||
|
||||
```python
|
||||
from autogen import ConversableAgent
|
||||
from mem0 import MemoryClient
|
||||
|
||||
memory_client = MemoryClient()
|
||||
USER_ID = "alice"
|
||||
|
||||
agent = ConversableAgent(
|
||||
"chatbot",
|
||||
llm_config={"config_list": [{"model": "gpt-4", "api_key": os.environ["OPENAI_API_KEY"]}]},
|
||||
code_execution_config=False,
|
||||
human_input_mode="NEVER",
|
||||
)
|
||||
|
||||
def get_context_aware_response(question: str) -> str:
|
||||
# Retrieve memories for context
|
||||
relevant_memories = memory_client.search(question, user_id=USER_ID)
|
||||
context = "\n".join([m["memory"] for m in relevant_memories.get("results", [])])
|
||||
|
||||
prompt = f"""Answer considering previous interactions:
|
||||
Previous context: {context}
|
||||
Question: {question}"""
|
||||
|
||||
reply = agent.generate_reply(messages=[{"content": prompt, "role": "user"}])
|
||||
|
||||
# Store the new interaction
|
||||
memory_client.add(
|
||||
[{"role": "user", "content": question}, {"role": "assistant", "content": reply}],
|
||||
user_id=USER_ID
|
||||
)
|
||||
return reply
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## All Supported Frameworks
|
||||
|
||||
Beyond the examples above, Mem0 integrates with:
|
||||
|
||||
| Framework | Type | Install |
|
||||
|-----------|------|---------|
|
||||
| [Mastra](https://docs.mem0.ai/integrations/mastra) | TS agent framework | `npm install @mastra/mem0` |
|
||||
| [ElevenLabs](https://docs.mem0.ai/integrations/elevenlabs) | Voice AI | `pip install elevenlabs mem0ai` |
|
||||
| [LiveKit](https://docs.mem0.ai/integrations/livekit) | Real-time voice/video | `pip install livekit-agents mem0ai` |
|
||||
| [Camel AI](https://docs.mem0.ai/integrations/camel-ai) | Multi-agent framework | `pip install camel-ai[all] mem0ai` |
|
||||
| [AWS Bedrock](https://docs.mem0.ai/integrations/aws-bedrock) | Cloud LLM provider | `pip install boto3 mem0ai` |
|
||||
| [Dify](https://docs.mem0.ai/integrations/dify) | Low-code AI platform | Plugin-based |
|
||||
| [Google AI ADK](https://docs.mem0.ai/integrations/google-ai-adk) | Google agent framework | `pip install google-adk mem0ai` |
|
||||
|
||||
For the general Python pattern (no framework), see the "Common integration pattern" in [SKILL.md](../SKILL.md).
|
||||
@@ -0,0 +1,119 @@
|
||||
# Mem0 Platform Quickstart
|
||||
|
||||
Get running with Mem0 in 2 minutes. No infrastructure to deploy -- just an API key.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Python 3.10+ or Node.js 18+
|
||||
- A Mem0 Platform API key ([Get one here](https://app.mem0.ai/dashboard/api-keys))
|
||||
|
||||
## Python Setup
|
||||
|
||||
```bash
|
||||
pip install mem0ai
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
```
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key="your-api-key")
|
||||
|
||||
# Add a memory
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
|
||||
{"role": "assistant", "content": "Got it! I'll remember your dietary preferences."}
|
||||
]
|
||||
client.add(messages, user_id="user123")
|
||||
|
||||
# Search memories
|
||||
results = client.search("What are my dietary restrictions?", user_id="user123")
|
||||
print(results)
|
||||
```
|
||||
|
||||
### Async Client
|
||||
|
||||
```python
|
||||
from mem0 import AsyncMemoryClient
|
||||
|
||||
client = AsyncMemoryClient(api_key="your-api-key")
|
||||
|
||||
await client.add(messages, user_id="user123")
|
||||
results = await client.search("query", user_id="user123")
|
||||
```
|
||||
|
||||
## TypeScript / JavaScript Setup
|
||||
|
||||
```bash
|
||||
npm install mem0ai
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
```
|
||||
|
||||
```javascript
|
||||
import MemoryClient from 'mem0ai';
|
||||
|
||||
const client = new MemoryClient({ apiKey: 'your-api-key' });
|
||||
|
||||
// Add a memory
|
||||
const messages = [
|
||||
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
|
||||
{"role": "assistant", "content": "Got it! I'll remember your dietary preferences."}
|
||||
];
|
||||
await client.add(messages, { user_id: "user123" });
|
||||
|
||||
// Search memories
|
||||
const results = await client.search("What are my dietary restrictions?", {
|
||||
user_id: "user123"
|
||||
});
|
||||
console.log(results);
|
||||
```
|
||||
|
||||
## cURL
|
||||
|
||||
```bash
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
|
||||
# Add memory
|
||||
curl -X POST https://api.mem0.ai/v1/memories/ \
|
||||
-H "Authorization: Token $MEM0_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [
|
||||
{"role": "user", "content": "I am a vegetarian and allergic to nuts."},
|
||||
{"role": "assistant", "content": "Got it! I will remember your dietary preferences."}
|
||||
],
|
||||
"user_id": "user123"
|
||||
}'
|
||||
|
||||
# Search memories
|
||||
curl -X POST https://api.mem0.ai/v2/memories/search/ \
|
||||
-H "Authorization: Token $MEM0_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"query": "What are my dietary restrictions?",
|
||||
"filters": {"user_id": "user123"}
|
||||
}'
|
||||
```
|
||||
|
||||
## Sample Response
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "14e1b28a-2014-40ad-ac42-69c9ef42193d",
|
||||
"memory": "Allergic to nuts",
|
||||
"user_id": "user123",
|
||||
"categories": ["health"],
|
||||
"created_at": "2025-10-22T04:40:22.864647-07:00",
|
||||
"score": 0.30
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [SDK Guide](sdk-guide.md) -- all methods for Python and TypeScript
|
||||
- [API Reference](api-reference.md) -- REST endpoints and memory object structure
|
||||
- [Integration Patterns](integration-patterns.md) -- LangChain, CrewAI, Vercel AI, etc.
|
||||
@@ -0,0 +1,308 @@
|
||||
# Mem0 SDK Guide
|
||||
|
||||
Complete SDK reference for Python and TypeScript. All methods use `MemoryClient` (Platform API).
|
||||
|
||||
## Initialization
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
client = MemoryClient(api_key="m0-your-api-key")
|
||||
```
|
||||
|
||||
**Python (Async):**
|
||||
```python
|
||||
from mem0 import AsyncMemoryClient
|
||||
client = AsyncMemoryClient(api_key="m0-your-api-key")
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
import MemoryClient from 'mem0ai';
|
||||
const client = new MemoryClient({ apiKey: 'm0-your-api-key' });
|
||||
```
|
||||
|
||||
Constructor accepts `apiKey` (required) and `host` (optional, default: `https://api.mem0.ai`).
|
||||
|
||||
---
|
||||
|
||||
## add() -- Store Memories
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
|
||||
{"role": "assistant", "content": "Got it! I'll remember that."}
|
||||
]
|
||||
client.add(messages, user_id="alice")
|
||||
|
||||
# With metadata
|
||||
client.add(messages, user_id="alice", metadata={"source": "onboarding"})
|
||||
|
||||
# With graph memory
|
||||
client.add(messages, user_id="alice", enable_graph=True)
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
await client.add(messages, { user_id: "alice" });
|
||||
await client.add(messages, { user_id: "alice", metadata: { source: "onboarding" } });
|
||||
await client.add(messages, { user_id: "alice", enable_graph: true });
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `messages` | array | `[{"role": "user", "content": "..."}]` |
|
||||
| `user_id` | string | User identifier (recommended) |
|
||||
| `agent_id` | string | Agent identifier |
|
||||
| `run_id` | string | Session identifier |
|
||||
| `metadata` | object | Custom key-value pairs |
|
||||
| `enable_graph` | boolean | Activate knowledge graph |
|
||||
| `infer` | boolean | If `false`, store raw text without inference (default: `true`) |
|
||||
| `immutable` | boolean | Prevents modification after creation |
|
||||
| `expiration_date` | string | Auto-expiry date (`YYYY-MM-DD`) |
|
||||
| `includes` | string | Preference filters for inclusion |
|
||||
| `excludes` | string | Preference filters for exclusion |
|
||||
| `async_mode` | boolean | Async processing (default: `true`). Set `false` to wait |
|
||||
|
||||
### Advanced Add Options
|
||||
|
||||
```python
|
||||
# Immutable -- cannot be modified or overwritten
|
||||
client.add(messages, user_id="alice", immutable=True)
|
||||
|
||||
# Expiring memory
|
||||
client.add(messages, user_id="alice", expiration_date="2025-12-31")
|
||||
|
||||
# Selective extraction
|
||||
client.add(messages, user_id="alice", includes="dietary preferences", excludes="payment info")
|
||||
|
||||
# Agent + session scoping
|
||||
client.add(messages, user_id="alice", agent_id="nutrition-agent", run_id="session-456")
|
||||
|
||||
# Synchronous processing (wait for completion)
|
||||
client.add(messages, user_id="alice", async_mode=False)
|
||||
|
||||
# Raw text -- skip LLM inference
|
||||
client.add(
|
||||
[{"role": "user", "content": "User prefers dark mode."}],
|
||||
user_id="alice",
|
||||
infer=False,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## search() -- Find Memories
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
results = client.search("dietary preferences?", user_id="alice")
|
||||
|
||||
# With filters and reranking
|
||||
results = client.search(
|
||||
query="work experience",
|
||||
filters={"AND": [{"user_id": "alice"}, {"categories": {"contains": "professional_details"}}]},
|
||||
top_k=5,
|
||||
rerank=True,
|
||||
threshold=0.5
|
||||
)
|
||||
|
||||
# With graph relations
|
||||
results = client.search("colleagues", user_id="alice", enable_graph=True)
|
||||
|
||||
# Keyword search
|
||||
results = client.search("vegetarian", user_id="alice", keyword_search=True)
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
const results = await client.search("dietary preferences", { user_id: "alice" });
|
||||
const results = await client.search("work experience", {
|
||||
filters: { AND: [{ user_id: "alice" }, { categories: { contains: "professional_details" } }] },
|
||||
top_k: 5,
|
||||
rerank: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `query` | string | Natural language search query |
|
||||
| `user_id` | string | Filter by user |
|
||||
| `filters` | object | V2 filter object (AND/OR operators) |
|
||||
| `top_k` | number | Number of results (default: 10) |
|
||||
| `rerank` | boolean | Enable reranking for better relevance |
|
||||
| `threshold` | number | Minimum similarity score (default: 0.3) |
|
||||
| `keyword_search` | boolean | Use keyword-based search |
|
||||
| `enable_graph` | boolean | Include graph relations |
|
||||
|
||||
### Common Filter Patterns
|
||||
|
||||
```python
|
||||
# Single user (shorthand)
|
||||
client.search("query", user_id="alice")
|
||||
|
||||
# OR across agents
|
||||
filters={"OR": [{"user_id": "alice"}, {"agent_id": {"in": ["travel-agent", "sports-agent"]}}]}
|
||||
|
||||
# Category filtering (partial match)
|
||||
filters={"AND": [{"user_id": "alice"}, {"categories": {"contains": "finance"}}]}
|
||||
|
||||
# Category filtering (exact match)
|
||||
filters={"AND": [{"user_id": "alice"}, {"categories": {"in": ["personal_information"]}}]}
|
||||
|
||||
# Wildcard (match any non-null run)
|
||||
filters={"AND": [{"user_id": "alice"}, {"run_id": "*"}]}
|
||||
|
||||
# Date range
|
||||
filters={"AND": [
|
||||
{"user_id": "alice"},
|
||||
{"created_at": {"gte": "2024-01-01T00:00:00Z"}},
|
||||
{"created_at": {"lt": "2024-02-01T00:00:00Z"}}
|
||||
]}
|
||||
|
||||
# Exclude categories with NOT
|
||||
filters={"AND": [{"user_id": "user_123"}, {"NOT": {"categories": {"in": ["spam", "test"]}}}]}
|
||||
|
||||
# Multi-dimensional query
|
||||
filters={"AND": [
|
||||
{"user_id": "user_123"},
|
||||
{"keywords": {"icontains": "invoice"}},
|
||||
{"categories": {"in": ["finance"]}},
|
||||
{"created_at": {"gte": "2024-01-01T00:00:00Z"}}
|
||||
]}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## get() / getAll() -- Retrieve Memories
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
# Single memory by ID
|
||||
memory = client.get(memory_id="ea925981-...")
|
||||
|
||||
# All memories for a user
|
||||
memories = client.get_all(filters={"AND": [{"user_id": "alice"}]})
|
||||
|
||||
# With date range
|
||||
memories = client.get_all(
|
||||
filters={"AND": [
|
||||
{"user_id": "alex"},
|
||||
{"created_at": {"gte": "2024-07-01", "lte": "2024-07-31"}}
|
||||
]}
|
||||
)
|
||||
|
||||
# With graph data
|
||||
memories = client.get_all(filters={"AND": [{"user_id": "alice"}]}, enable_graph=True)
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
const memory = await client.get("ea925981-...");
|
||||
const memories = await client.getAll({ filters: { AND: [{ user_id: "alice" }] } });
|
||||
```
|
||||
|
||||
**Note:** `get_all` requires at least one of `user_id`, `agent_id`, `app_id`, or `run_id` in filters.
|
||||
|
||||
---
|
||||
|
||||
## update() -- Modify Memories
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
client.update(memory_id="ea925981-...", text="Updated: vegan since 2024")
|
||||
client.update(memory_id="ea925981-...", text="Updated", metadata={"verified": True})
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
await client.update("ea925981-...", { text: "Updated: vegan since 2024" });
|
||||
```
|
||||
|
||||
Cannot update immutable memories.
|
||||
|
||||
---
|
||||
|
||||
## delete() / deleteAll() -- Remove Memories
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
client.delete(memory_id="ea925981-...")
|
||||
client.delete_all(user_id="alice") # Irreversible bulk delete
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
await client.delete("ea925981-...");
|
||||
await client.deleteAll({ user_id: "alice" });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## history() -- Track Changes
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
history = client.history(memory_id="ea925981-...")
|
||||
# Returns: [{previous_value, new_value, action, timestamps}]
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
const history = await client.history("ea925981-...");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Batch Operations (TypeScript)
|
||||
|
||||
```typescript
|
||||
// Batch update
|
||||
await client.batchUpdate([
|
||||
{ memoryId: "uuid-1", text: "Updated text" },
|
||||
{ memoryId: "uuid-2", text: "Another updated text" },
|
||||
]);
|
||||
|
||||
// Batch delete
|
||||
await client.batchDelete(["uuid-1", "uuid-2", "uuid-3"]);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Methods
|
||||
|
||||
```python
|
||||
# List all users/agents/sessions with memories
|
||||
users = client.users()
|
||||
|
||||
# Delete a user/agent entity
|
||||
client.delete_users(user_id="alice")
|
||||
|
||||
# Submit feedback on a memory
|
||||
client.feedback(memory_id="...", feedback="POSITIVE", feedback_reason="Accurate extraction")
|
||||
|
||||
# Export memories
|
||||
export = client.create_memory_export(filters={"AND": [{"user_id": "alice"}]})
|
||||
data = client.get_memory_export(memory_export_id=export["id"])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Entity cross-filtering fails silently** -- `AND` with `user_id` + `agent_id` returns empty. Use `OR`.
|
||||
2. **SQL operators rejected** -- use `gte`, `lt`, etc. Not `>=`, `<`.
|
||||
3. **Metadata filtering is limited** -- only top-level keys with `eq`, `contains`, `ne`.
|
||||
4. **Wildcard `*` excludes null** -- only matches non-null values.
|
||||
5. **Default threshold is 0.3** -- increase for stricter matching.
|
||||
6. **Async processing** -- memories process asynchronously. Wait 2-3s after `add()` before searching.
|
||||
7. **Immutable memories** -- cannot be updated or deleted once created.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
Python uses `snake_case` (`user_id`, `memory_id`, `get_all`). TypeScript uses `camelCase` for methods (`getAll`, `deleteAll`, `batchUpdate`) but `snake_case` for API parameters (`user_id`, `agent_id`).
|
||||
@@ -0,0 +1,720 @@
|
||||
# Mem0 Use Cases & Examples
|
||||
|
||||
Real-world implementation patterns for Mem0 Platform. Each use case includes complete, runnable code in both Python and TypeScript.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Personalized AI Companion](#1-personalized-ai-companion)
|
||||
- [Customer Support with Categories](#2-customer-support-with-categories)
|
||||
- [Healthcare Coach](#3-healthcare-coach)
|
||||
- [Content Creation Workflow](#4-content-creation-workflow)
|
||||
- [Multi-Agent / Multi-Tenant](#5-multi-agent--multi-tenant)
|
||||
- [Personalized Search](#6-personalized-search)
|
||||
- [Email Intelligence](#7-email-intelligence)
|
||||
- [Common Patterns Across Use Cases](#common-patterns-across-use-cases)
|
||||
|
||||
---
|
||||
|
||||
## 1. Personalized AI Companion
|
||||
|
||||
A fitness coach that remembers goals, preferences, and progress across sessions. Mem0 persists context across app restarts — no session state needed.
|
||||
|
||||
### Implementation (Python)
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
from openai import OpenAI
|
||||
|
||||
mem0 = MemoryClient()
|
||||
openai_client = OpenAI()
|
||||
|
||||
def chat(user_input: str, user_id: str) -> str:
|
||||
# 1. Retrieve relevant memories
|
||||
memories = mem0.search(user_input, user_id=user_id)
|
||||
context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
|
||||
|
||||
# 2. Generate response with memory context
|
||||
system_prompt = f"""You are Ray, a personal fitness coach.
|
||||
Use these known facts about the user to personalize your response:
|
||||
{context if context else 'No prior context yet.'}"""
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4.1-nano-2025-04-14",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_input},
|
||||
]
|
||||
)
|
||||
reply = response.choices[0].message.content
|
||||
|
||||
# 3. Store interaction for future context
|
||||
mem0.add(
|
||||
[{"role": "user", "content": user_input}, {"role": "assistant", "content": reply}],
|
||||
user_id=user_id
|
||||
)
|
||||
return reply
|
||||
|
||||
# Usage
|
||||
chat("I want to run a marathon in under 4 hours", user_id="max")
|
||||
# Next day, app restarted:
|
||||
chat("What should I focus on today?", user_id="max")
|
||||
# Ray remembers the sub-4 marathon goal
|
||||
```
|
||||
|
||||
### Implementation (TypeScript)
|
||||
|
||||
```typescript
|
||||
import MemoryClient from 'mem0ai';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
|
||||
const openai = new OpenAI();
|
||||
|
||||
async function chat(userInput: string, userId: string): Promise<string> {
|
||||
// 1. Retrieve relevant memories
|
||||
const memories = await mem0.search(userInput, { user_id: userId });
|
||||
const context = memories.results
|
||||
?.map((m: any) => `- ${m.memory}`)
|
||||
.join('\n') || 'No prior context yet.';
|
||||
|
||||
// 2. Generate response with memory context
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-4.1-nano-2025-04-14',
|
||||
messages: [
|
||||
{ role: 'system', content: `You are Ray, a personal fitness coach.\nUser context:\n${context}` },
|
||||
{ role: 'user', content: userInput },
|
||||
],
|
||||
});
|
||||
const reply = response.choices[0].message.content!;
|
||||
|
||||
// 3. Store interaction
|
||||
await mem0.add(
|
||||
[{ role: 'user', content: userInput }, { role: 'assistant', content: reply }],
|
||||
{ user_id: userId }
|
||||
);
|
||||
return reply;
|
||||
}
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- Context persists across app restarts — no session management needed
|
||||
- Memories are automatically deduplicated and updated
|
||||
- Works with any LLM provider (OpenAI, Anthropic, etc.)
|
||||
|
||||
**Best for:** Fitness coaches, tutors, therapists — any assistant that needs to remember goals across sessions.
|
||||
|
||||
---
|
||||
|
||||
## 2. Customer Support with Categories
|
||||
|
||||
Auto-categorize support data so teams retrieve the right facts fast. Uses custom categories for structured retrieval.
|
||||
|
||||
### Implementation (Python)
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient()
|
||||
|
||||
# 1. Define categories at the project level (one-time setup)
|
||||
custom_categories = [
|
||||
{"support_tickets": "Customer issues and resolutions"},
|
||||
{"account_info": "Account details and preferences"},
|
||||
{"billing": "Payment history and billing questions"},
|
||||
{"product_feedback": "Feature requests and feedback"},
|
||||
]
|
||||
client.project.update(custom_categories=custom_categories)
|
||||
|
||||
# 2. Store interactions — auto-classified into categories
|
||||
def log_support_interaction(user_id: str, message: str, priority: str = "normal"):
|
||||
client.add(
|
||||
[{"role": "user", "content": message}],
|
||||
user_id=user_id,
|
||||
metadata={"priority": priority, "source": "support_chat"}
|
||||
)
|
||||
|
||||
# 3. Retrieve by category
|
||||
def get_billing_issues(user_id: str):
|
||||
return client.get_all(
|
||||
filters={
|
||||
"AND": [
|
||||
{"user_id": user_id},
|
||||
{"categories": {"in": ["billing"]}}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
def search_support_history(user_id: str, query: str):
|
||||
return client.search(
|
||||
query,
|
||||
filters={
|
||||
"AND": [
|
||||
{"user_id": user_id},
|
||||
{"categories": {"contains": "support_tickets"}}
|
||||
]
|
||||
},
|
||||
top_k=5
|
||||
)
|
||||
|
||||
# Usage
|
||||
log_support_interaction("maria", "I was charged twice for last month's subscription", priority="high")
|
||||
log_support_interaction("maria", "The dashboard is loading slowly on mobile")
|
||||
billing = get_billing_issues("maria") # Returns only billing-related memories
|
||||
```
|
||||
|
||||
### Implementation (TypeScript)
|
||||
|
||||
```typescript
|
||||
import MemoryClient from 'mem0ai';
|
||||
|
||||
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
|
||||
|
||||
// Setup categories (one-time)
|
||||
await client.updateProject({
|
||||
custom_categories: [
|
||||
{ support_tickets: 'Customer issues and resolutions' },
|
||||
{ billing: 'Payment history and billing questions' },
|
||||
{ product_feedback: 'Feature requests and feedback' },
|
||||
],
|
||||
});
|
||||
|
||||
async function logInteraction(userId: string, message: string, priority = 'normal') {
|
||||
await client.add(
|
||||
[{ role: 'user', content: message }],
|
||||
{ user_id: userId, metadata: { priority, source: 'support_chat' } }
|
||||
);
|
||||
}
|
||||
|
||||
async function getBillingIssues(userId: string) {
|
||||
return client.getAll({
|
||||
filters: { AND: [{ user_id: userId }, { categories: { in: ['billing'] } }] },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- Automatic categorization — no manual tagging
|
||||
- Filter by category for structured retrieval
|
||||
- Metadata (`priority`, `source`) enables multi-dimensional queries
|
||||
|
||||
**Best for:** Help desks, SaaS support, e-commerce — structured retrieval by category eliminates manual scanning.
|
||||
|
||||
---
|
||||
|
||||
## 3. Healthcare Coach
|
||||
|
||||
Guide patients with an assistant that remembers medical history. Uses high `threshold` for confident retrieval in safety-critical contexts.
|
||||
|
||||
### Implementation (Python)
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
from openai import OpenAI
|
||||
|
||||
mem0 = MemoryClient()
|
||||
openai_client = OpenAI()
|
||||
|
||||
def save_patient_info(user_id: str, information: str):
|
||||
mem0.add(
|
||||
[{"role": "user", "content": information}],
|
||||
user_id=user_id,
|
||||
run_id="healthcare_session",
|
||||
metadata={"type": "patient_information"}
|
||||
)
|
||||
|
||||
def consult(user_id: str, question: str) -> str:
|
||||
# High threshold for medical accuracy
|
||||
memories = mem0.search(question, user_id=user_id, top_k=5, threshold=0.7)
|
||||
context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4.1-nano-2025-04-14",
|
||||
messages=[
|
||||
{"role": "system", "content": f"You are a health coach. Patient context:\n{context}"},
|
||||
{"role": "user", "content": question},
|
||||
]
|
||||
)
|
||||
reply = response.choices[0].message.content
|
||||
|
||||
# Store the interaction
|
||||
mem0.add(
|
||||
[{"role": "user", "content": question}, {"role": "assistant", "content": reply}],
|
||||
user_id=user_id,
|
||||
run_id="healthcare_session",
|
||||
)
|
||||
return reply
|
||||
|
||||
# Usage
|
||||
save_patient_info("alex", "I'm allergic to penicillin and take metformin for type 2 diabetes")
|
||||
consult("alex", "Can I take amoxicillin for my sore throat?")
|
||||
# Remembers penicillin allergy — amoxicillin is a penicillin-type antibiotic
|
||||
```
|
||||
|
||||
### Implementation (TypeScript)
|
||||
|
||||
```typescript
|
||||
import MemoryClient from 'mem0ai';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
|
||||
const openai = new OpenAI();
|
||||
|
||||
async function savePatientInfo(userId: string, info: string) {
|
||||
await mem0.add(
|
||||
[{ role: 'user', content: info }],
|
||||
{ user_id: userId, run_id: 'healthcare_session', metadata: { type: 'patient_information' } }
|
||||
);
|
||||
}
|
||||
|
||||
async function consult(userId: string, question: string): Promise<string> {
|
||||
const memories = await mem0.search(question, {
|
||||
user_id: userId,
|
||||
top_k: 5,
|
||||
threshold: 0.7,
|
||||
});
|
||||
const context = memories.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-4.1-nano-2025-04-14',
|
||||
messages: [
|
||||
{ role: 'system', content: `You are a health coach. Patient context:\n${context}` },
|
||||
{ role: 'user', content: question },
|
||||
],
|
||||
});
|
||||
const reply = response.choices[0].message.content!;
|
||||
|
||||
await mem0.add(
|
||||
[{ role: 'user', content: question }, { role: 'assistant', content: reply }],
|
||||
{ user_id: userId, run_id: 'healthcare_session' }
|
||||
);
|
||||
return reply;
|
||||
}
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- High threshold (0.7) ensures only confident matches for safety-critical retrieval
|
||||
- Session scoping via `run_id` groups related health interactions
|
||||
- Metadata tagging separates patient info from conversation history
|
||||
|
||||
**Best for:** Telehealth, wellness apps, patient management — persistent health context across visits.
|
||||
|
||||
---
|
||||
|
||||
## 4. Content Creation Workflow
|
||||
|
||||
Store voice guidelines once and apply them across every draft. Uses `run_id` and `metadata` to scope writing preferences per session.
|
||||
|
||||
### Implementation (Python)
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
from openai import OpenAI
|
||||
|
||||
mem0 = MemoryClient()
|
||||
openai_client = OpenAI()
|
||||
|
||||
def store_writing_preferences(user_id: str, preferences: str):
|
||||
mem0.add(
|
||||
[{"role": "user", "content": preferences}],
|
||||
user_id=user_id,
|
||||
run_id="editing_session",
|
||||
metadata={"type": "preferences", "category": "writing_style"}
|
||||
)
|
||||
|
||||
def draft_content(user_id: str, topic: str) -> str:
|
||||
# Retrieve writing preferences
|
||||
prefs = mem0.search(
|
||||
"writing style preferences",
|
||||
filters={"AND": [{"user_id": user_id}, {"run_id": "editing_session"}]}
|
||||
)
|
||||
style_context = "\n".join([f"- {m['memory']}" for m in prefs.get("results", [])])
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4.1-nano-2025-04-14",
|
||||
messages=[
|
||||
{"role": "system", "content": f"Write content matching these style preferences:\n{style_context}"},
|
||||
{"role": "user", "content": f"Write a blog post about: {topic}"},
|
||||
]
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
|
||||
# Usage
|
||||
store_writing_preferences("writer_01", "I prefer short sentences. Active voice. No jargon. Use analogies.")
|
||||
draft_content("writer_01", "Why AI memory matters for chatbots")
|
||||
# Drafts content matching the stored voice guidelines
|
||||
```
|
||||
|
||||
### Implementation (TypeScript)
|
||||
|
||||
```typescript
|
||||
import MemoryClient from 'mem0ai';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
|
||||
const openai = new OpenAI();
|
||||
|
||||
async function storePreferences(userId: string, preferences: string) {
|
||||
await mem0.add(
|
||||
[{ role: 'user', content: preferences }],
|
||||
{ user_id: userId, run_id: 'editing_session', metadata: { type: 'preferences' } }
|
||||
);
|
||||
}
|
||||
|
||||
async function draftContent(userId: string, topic: string): Promise<string> {
|
||||
const prefs = await mem0.search('writing style preferences', {
|
||||
filters: { AND: [{ user_id: userId }, { run_id: 'editing_session' }] },
|
||||
});
|
||||
const styleContext = prefs.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-4.1-nano-2025-04-14',
|
||||
messages: [
|
||||
{ role: 'system', content: `Write content matching these preferences:\n${styleContext}` },
|
||||
{ role: 'user', content: `Write a blog post about: ${topic}` },
|
||||
],
|
||||
});
|
||||
return response.choices[0].message.content!;
|
||||
}
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- Voice consistency across all content without repeating guidelines
|
||||
- Scoped sessions let you maintain different style profiles
|
||||
- Preferences update automatically as you refine them
|
||||
|
||||
**Best for:** Marketing teams, technical writers, agencies — consistent voice across all content.
|
||||
|
||||
---
|
||||
|
||||
## 5. Multi-Agent / Multi-Tenant
|
||||
|
||||
Keep memories separate using `user_id`, `agent_id`, `app_id`, and `run_id` scoping. Critical for multi-agent workflows and multi-tenant apps.
|
||||
|
||||
### Implementation (Python)
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient()
|
||||
|
||||
# Store memories scoped to user + agent + session
|
||||
def store_scoped_memory(messages: list, user_id: str, agent_id: str, run_id: str, app_id: str):
|
||||
client.add(
|
||||
messages,
|
||||
user_id=user_id,
|
||||
agent_id=agent_id,
|
||||
run_id=run_id,
|
||||
app_id=app_id
|
||||
)
|
||||
|
||||
# Query within a specific scope
|
||||
def search_user_session(query: str, user_id: str, app_id: str, run_id: str):
|
||||
"""Search memories for a specific user within a specific session."""
|
||||
return client.search(
|
||||
query,
|
||||
filters={
|
||||
"AND": [
|
||||
{"user_id": user_id},
|
||||
{"app_id": app_id},
|
||||
{"run_id": run_id}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
def search_agent_knowledge(query: str, agent_id: str, app_id: str):
|
||||
"""Search all memories an agent has across all users."""
|
||||
return client.search(
|
||||
query,
|
||||
filters={
|
||||
"AND": [
|
||||
{"agent_id": agent_id},
|
||||
{"app_id": app_id}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
# Usage: Travel concierge app with multiple agents
|
||||
store_scoped_memory(
|
||||
[{"role": "user", "content": "I'm vegetarian and prefer window seats"}],
|
||||
user_id="traveler_cam",
|
||||
agent_id="travel_planner",
|
||||
run_id="tokyo-2025",
|
||||
app_id="concierge_app"
|
||||
)
|
||||
|
||||
# User-scoped query: "What does Cam prefer?"
|
||||
user_mems = search_user_session("dietary restrictions?", "traveler_cam", "concierge_app", "tokyo-2025")
|
||||
|
||||
# Agent-scoped query: "What do all travelers prefer?" (across users)
|
||||
agent_mems = search_agent_knowledge("common dietary restrictions?", "travel_planner", "concierge_app")
|
||||
```
|
||||
|
||||
### Implementation (TypeScript)
|
||||
|
||||
```typescript
|
||||
import MemoryClient from 'mem0ai';
|
||||
|
||||
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
|
||||
|
||||
async function storeScopedMemory(
|
||||
messages: Array<{ role: string; content: string }>,
|
||||
userId: string, agentId: string, runId: string, appId: string
|
||||
) {
|
||||
await client.add(messages, {
|
||||
user_id: userId,
|
||||
agent_id: agentId,
|
||||
run_id: runId,
|
||||
app_id: appId,
|
||||
});
|
||||
}
|
||||
|
||||
async function searchUserSession(query: string, userId: string, appId: string, runId: string) {
|
||||
return client.search(query, {
|
||||
filters: { AND: [{ user_id: userId }, { app_id: appId }, { run_id: runId }] },
|
||||
});
|
||||
}
|
||||
|
||||
async function searchAgentKnowledge(query: string, agentId: string, appId: string) {
|
||||
return client.search(query, {
|
||||
filters: { AND: [{ agent_id: agentId }, { app_id: appId }] },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- Full isolation between users, agents, sessions, and apps
|
||||
- Query at any scope level — user, agent, session, or app-wide
|
||||
- No memory leakage between tenants
|
||||
|
||||
**Best for:** Multi-agent workflows, multi-tenant SaaS — proper isolation at every level.
|
||||
|
||||
---
|
||||
|
||||
## 6. Personalized Search
|
||||
|
||||
Blend real-time search results with personal context. Uses `custom_instructions` to infer preferences from queries.
|
||||
|
||||
### Implementation (Python)
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
from openai import OpenAI
|
||||
|
||||
mem0 = MemoryClient()
|
||||
openai_client = OpenAI()
|
||||
|
||||
# One-time setup: configure Mem0 to infer from queries
|
||||
mem0.project.update(
|
||||
custom_instructions="""Infer user preferences and facts from their search queries.
|
||||
Extract dietary preferences, location, interests, and purchase history."""
|
||||
)
|
||||
|
||||
def personalized_search(user_id: str, query: str, search_results: list) -> str:
|
||||
# Get user context from memory
|
||||
memories = mem0.search(query, user_id=user_id, top_k=5)
|
||||
user_context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4.1-nano-2025-04-14",
|
||||
messages=[
|
||||
{"role": "system", "content": f"Personalize search results using user context:\n{user_context}"},
|
||||
{"role": "user", "content": f"Query: {query}\n\nSearch results:\n{search_results}"},
|
||||
]
|
||||
)
|
||||
reply = response.choices[0].message.content
|
||||
|
||||
# Store the query to learn preferences over time
|
||||
mem0.add(
|
||||
[{"role": "user", "content": query}],
|
||||
user_id=user_id
|
||||
)
|
||||
return reply
|
||||
|
||||
# Usage
|
||||
personalized_search("user_42", "best restaurants nearby", ["Restaurant A", "Restaurant B"])
|
||||
# Over time, Mem0 learns: "user prefers vegetarian, lives in Austin"
|
||||
# Future searches are automatically personalized
|
||||
```
|
||||
|
||||
### Implementation (TypeScript)
|
||||
|
||||
```typescript
|
||||
import MemoryClient from 'mem0ai';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
|
||||
const openai = new OpenAI();
|
||||
|
||||
async function personalizedSearch(userId: string, query: string, searchResults: string[]): Promise<string> {
|
||||
const memories = await mem0.search(query, { user_id: userId, top_k: 5 });
|
||||
const context = memories.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-4.1-nano-2025-04-14',
|
||||
messages: [
|
||||
{ role: 'system', content: `Personalize results using user context:\n${context}` },
|
||||
{ role: 'user', content: `Query: ${query}\nResults: ${searchResults.join(', ')}` },
|
||||
],
|
||||
});
|
||||
const reply = response.choices[0].message.content!;
|
||||
|
||||
await mem0.add([{ role: 'user', content: query }], { user_id: userId });
|
||||
return reply;
|
||||
}
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- Learns preferences from queries automatically via `custom_instructions`
|
||||
- Personalizes any search provider (Tavily, Google, Bing)
|
||||
- Zero manual preference setup — improves over time
|
||||
|
||||
**Best for:** Personalized search engines, recommendation systems — search results tailored to individual users.
|
||||
|
||||
---
|
||||
|
||||
## 7. Email Intelligence
|
||||
|
||||
Capture, categorize, and recall inbox threads using persistent memories with rich metadata.
|
||||
|
||||
### Implementation (Python)
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient()
|
||||
|
||||
def store_email(user_id: str, sender: str, subject: str, body: str, date: str):
|
||||
client.add(
|
||||
[{"role": "user", "content": f"Email from {sender}: {subject}\n\n{body}"}],
|
||||
user_id=user_id,
|
||||
metadata={"email_type": "incoming", "sender": sender, "subject": subject, "date": date}
|
||||
)
|
||||
|
||||
def search_emails(user_id: str, query: str):
|
||||
return client.search(
|
||||
query,
|
||||
filters={"AND": [{"user_id": user_id}, {"categories": {"contains": "email"}}]},
|
||||
top_k=10
|
||||
)
|
||||
|
||||
def get_emails_from_sender(user_id: str, sender: str):
|
||||
return client.get_all(
|
||||
filters={
|
||||
"AND": [
|
||||
{"user_id": user_id},
|
||||
{"metadata": {"contains": sender}}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
# Usage
|
||||
store_email("alice", "bob@acme.com", "Q3 Budget Review", "Attached is the Q3 budget...", "2025-01-15")
|
||||
store_email("alice", "carol@acme.com", "Sprint Planning", "Here are the priorities...", "2025-01-16")
|
||||
|
||||
results = search_emails("alice", "budget discussions")
|
||||
sender_emails = get_emails_from_sender("alice", "bob@acme.com")
|
||||
```
|
||||
|
||||
### Implementation (TypeScript)
|
||||
|
||||
```typescript
|
||||
import MemoryClient from 'mem0ai';
|
||||
|
||||
const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
|
||||
|
||||
async function storeEmail(userId: string, sender: string, subject: string, body: string, date: string) {
|
||||
await client.add(
|
||||
[{ role: 'user', content: `Email from ${sender}: ${subject}\n\n${body}` }],
|
||||
{ user_id: userId, metadata: { email_type: 'incoming', sender, subject, date } }
|
||||
);
|
||||
}
|
||||
|
||||
async function searchEmails(userId: string, query: string) {
|
||||
return client.search(query, {
|
||||
filters: { AND: [{ user_id: userId }, { categories: { contains: 'email' } }] },
|
||||
top_k: 10,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
|
||||
- Rich metadata enables multi-dimensional queries (sender, date, subject)
|
||||
- Category filtering separates emails from other memory types
|
||||
- Semantic search across all email content
|
||||
|
||||
**Best for:** Inbox management, email automation — searchable email memories with metadata filtering.
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns Across Use Cases
|
||||
|
||||
### Pattern 1: Retrieve → Generate → Store
|
||||
|
||||
Every use case follows the same 3-step loop:
|
||||
|
||||
```python
|
||||
# 1. Retrieve relevant context
|
||||
memories = mem0.search(user_input, user_id=user_id)
|
||||
context = "\n".join([m["memory"] for m in memories.get("results", [])])
|
||||
|
||||
# 2. Generate with context
|
||||
response = llm.generate(system_prompt=f"Context:\n{context}", user_input=user_input)
|
||||
|
||||
# 3. Store the interaction
|
||||
mem0.add(
|
||||
[{"role": "user", "content": user_input}, {"role": "assistant", "content": response}],
|
||||
user_id=user_id
|
||||
)
|
||||
```
|
||||
|
||||
### Pattern 2: Scope with Entity Identifiers
|
||||
|
||||
Use `user_id`, `agent_id`, `app_id`, and `run_id` to isolate memories:
|
||||
|
||||
```python
|
||||
# User-level: personal preferences
|
||||
client.add(messages, user_id="alice")
|
||||
|
||||
# Session-level: conversation within one session
|
||||
client.add(messages, user_id="alice", run_id="session_123")
|
||||
|
||||
# Agent-level: agent-specific knowledge
|
||||
client.add(messages, agent_id="support_bot", app_id="helpdesk")
|
||||
```
|
||||
|
||||
### Pattern 3: Rich Metadata for Filtering
|
||||
|
||||
Attach structured metadata for multi-dimensional queries:
|
||||
|
||||
```python
|
||||
# Store with metadata
|
||||
client.add(messages, user_id="alice", metadata={"priority": "high", "source": "phone_call"})
|
||||
|
||||
# Filter by category + metadata
|
||||
client.search("billing issues", filters={
|
||||
"AND": [{"user_id": "alice"}, {"categories": {"contains": "billing"}}]
|
||||
})
|
||||
```
|
||||
|
||||
### Pattern 4: Custom Instructions for Domain-Specific Extraction
|
||||
|
||||
Control what Mem0 extracts from conversations:
|
||||
|
||||
```python
|
||||
client.project.update(
|
||||
custom_instructions="Extract medical conditions, medications, and allergies. Exclude billing info."
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## More Examples
|
||||
|
||||
For 30+ cookbooks with complete working code: [docs.mem0.ai/cookbooks](https://docs.mem0.ai/cookbooks)
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Mem0 Documentation Search Agent (Mintlify-based)
|
||||
On-demand search tool for querying Mem0 documentation without storing content locally.
|
||||
|
||||
This tool leverages Mintlify's documentation structure to perform just-in-time
|
||||
retrieval of technical information from docs.mem0.ai.
|
||||
|
||||
Usage:
|
||||
python mem0_doc_search.py --query "how to add graph memory"
|
||||
python mem0_doc_search.py --query "filter syntax for categories"
|
||||
python mem0_doc_search.py --page "/platform/features/graph-memory"
|
||||
python mem0_doc_search.py --index
|
||||
python mem0_doc_search.py --query "webhook events" --section platform
|
||||
|
||||
Purpose:
|
||||
- Avoid bloating local context with full documentation
|
||||
- Enable just-in-time retrieval of technical details
|
||||
- Query specific documentation pages on demand
|
||||
- Search across the full Mem0 documentation site
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
DOCS_BASE = "https://docs.mem0.ai"
|
||||
SEARCH_ENDPOINT = f"{DOCS_BASE}/api/search"
|
||||
LLMS_INDEX = f"{DOCS_BASE}/llms.txt"
|
||||
|
||||
# Known documentation sections for targeted retrieval
|
||||
SECTION_MAP = {
|
||||
"platform": [
|
||||
"/platform/overview",
|
||||
"/platform/quickstart",
|
||||
"/platform/features",
|
||||
"/platform/features/graph-memory",
|
||||
"/platform/features/selective-memory",
|
||||
"/platform/features/custom-categories",
|
||||
"/platform/features/v2-memory-filters",
|
||||
"/platform/features/async-client",
|
||||
"/platform/features/webhooks",
|
||||
"/platform/features/multimodal-support",
|
||||
],
|
||||
"api": [
|
||||
"/api-reference/memory/add-memories",
|
||||
"/api-reference/memory/v2-search-memories",
|
||||
"/api-reference/memory/v2-get-memories",
|
||||
"/api-reference/memory/get-memory",
|
||||
"/api-reference/memory/update-memory",
|
||||
"/api-reference/memory/delete-memory",
|
||||
],
|
||||
"open-source": [
|
||||
"/open-source/overview",
|
||||
"/open-source/python-quickstart",
|
||||
"/open-source/node-quickstart",
|
||||
"/open-source/features",
|
||||
"/open-source/features/graph-memory",
|
||||
"/open-source/features/rest-api",
|
||||
"/open-source/configure-components",
|
||||
],
|
||||
"openmemory": [
|
||||
"/openmemory/overview",
|
||||
"/openmemory/quickstart",
|
||||
],
|
||||
"sdks": [
|
||||
"/sdks/python",
|
||||
"/sdks/js",
|
||||
],
|
||||
"integrations": [
|
||||
"/integrations",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def fetch_url(url: str) -> str:
|
||||
"""Fetch content from a URL."""
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mem0DocSearchAgent/1.0"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
return resp.read().decode("utf-8")
|
||||
except urllib.error.HTTPError as e:
|
||||
return f"HTTP Error {e.code}: {e.reason}"
|
||||
except urllib.error.URLError as e:
|
||||
return f"URL Error: {e.reason}"
|
||||
|
||||
|
||||
def search_docs(query: str, section: str | None = None) -> dict:
|
||||
"""
|
||||
Search Mem0 documentation using Mintlify's search API.
|
||||
Falls back to the llms.txt index for keyword matching if the API is unavailable.
|
||||
"""
|
||||
# Try Mintlify search API first
|
||||
params = urllib.parse.urlencode({"query": query})
|
||||
search_url = f"{SEARCH_ENDPOINT}?{params}"
|
||||
|
||||
try:
|
||||
result = fetch_url(search_url)
|
||||
data = json.loads(result)
|
||||
if isinstance(data, dict) and data.get("results"):
|
||||
results = data["results"]
|
||||
if section and section in SECTION_MAP:
|
||||
section_paths = SECTION_MAP[section]
|
||||
results = [r for r in results if any(r.get("url", "").startswith(p) for p in section_paths)]
|
||||
return {"source": "mintlify_search", "results": results}
|
||||
except (json.JSONDecodeError, Exception):
|
||||
pass
|
||||
|
||||
# Fallback: search llms.txt index for matching URLs
|
||||
index_content = fetch_url(LLMS_INDEX)
|
||||
query_lower = query.lower()
|
||||
matching_urls = []
|
||||
|
||||
for line in index_content.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
if query_lower in line.lower():
|
||||
matching_urls.append(line)
|
||||
|
||||
if section and section in SECTION_MAP:
|
||||
section_paths = SECTION_MAP[section]
|
||||
matching_urls = [u for u in matching_urls if any(p in u for p in section_paths)]
|
||||
|
||||
return {
|
||||
"source": "llms_txt_index",
|
||||
"query": query,
|
||||
"matching_urls": matching_urls[:20],
|
||||
"suggestion": "Fetch specific URLs for detailed content",
|
||||
}
|
||||
|
||||
|
||||
def fetch_page(page_path: str) -> dict:
|
||||
"""Fetch a specific documentation page."""
|
||||
url = f"{DOCS_BASE}{page_path}" if page_path.startswith("/") else page_path
|
||||
content = fetch_url(url)
|
||||
return {"url": url, "content": content[:10000], "truncated": len(content) > 10000}
|
||||
|
||||
|
||||
def get_index() -> dict:
|
||||
"""Fetch the full documentation index from llms.txt."""
|
||||
content = fetch_url(LLMS_INDEX)
|
||||
urls = [line.strip() for line in content.splitlines() if line.strip() and not line.startswith("#")]
|
||||
return {"total_pages": len(urls), "urls": urls, "sections": list(SECTION_MAP.keys())}
|
||||
|
||||
|
||||
def list_section(section: str) -> dict:
|
||||
"""List all known pages in a documentation section."""
|
||||
if section not in SECTION_MAP:
|
||||
return {"error": f"Unknown section: {section}", "available": list(SECTION_MAP.keys())}
|
||||
return {
|
||||
"section": section,
|
||||
"pages": [f"{DOCS_BASE}{p}" for p in SECTION_MAP[section]],
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Search Mem0 documentation on demand")
|
||||
parser.add_argument("--query", help="Search query for documentation")
|
||||
parser.add_argument("--page", help="Fetch a specific page path (e.g., /platform/features/graph-memory)")
|
||||
parser.add_argument("--index", action="store_true", help="Show full documentation index")
|
||||
parser.add_argument("--section", help="Filter by section or list section pages")
|
||||
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.index:
|
||||
result = get_index()
|
||||
elif args.section and not args.query:
|
||||
result = list_section(args.section)
|
||||
elif args.page:
|
||||
result = fetch_page(args.page)
|
||||
elif args.query:
|
||||
result = search_docs(args.query, section=args.section)
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2))
|
||||
else:
|
||||
if isinstance(result, dict):
|
||||
if "results" in result:
|
||||
print(f"Source: {result.get('source', 'unknown')}")
|
||||
for r in result["results"]:
|
||||
print(f" - {r.get('title', 'N/A')}: {r.get('url', 'N/A')}")
|
||||
if r.get("description"):
|
||||
print(f" {r['description'][:200]}")
|
||||
elif "matching_urls" in result:
|
||||
print(f"Source: {result['source']}")
|
||||
print(f"Query: {result['query']}")
|
||||
for url in result["matching_urls"]:
|
||||
print(f" - {url}")
|
||||
if result.get("suggestion"):
|
||||
print(f"\n{result['suggestion']}")
|
||||
elif "urls" in result:
|
||||
print(f"Total documentation pages: {result['total_pages']}")
|
||||
print(f"Sections: {', '.join(result['sections'])}")
|
||||
for url in result["urls"][:30]:
|
||||
print(f" - {url}")
|
||||
if result["total_pages"] > 30:
|
||||
print(f" ... and {result['total_pages'] - 30} more")
|
||||
elif "pages" in result:
|
||||
print(f"Section: {result['section']}")
|
||||
for page in result["pages"]:
|
||||
print(f" - {page}")
|
||||
elif "content" in result:
|
||||
print(f"URL: {result['url']}")
|
||||
if result.get("truncated"):
|
||||
print("[Content truncated to 10000 chars]")
|
||||
print(result["content"])
|
||||
elif "error" in result:
|
||||
print(f"Error: {result['error']}")
|
||||
if result.get("available"):
|
||||
print(f"Available sections: {', '.join(result['available'])}")
|
||||
else:
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -2,6 +2,7 @@
|
||||
module.exports = {
|
||||
...require("./jest.config"),
|
||||
testMatch: ["**/integration/**/*.test.ts"],
|
||||
globalSetup: "<rootDir>/src/client/tests/integration/global-setup.ts",
|
||||
globalTeardown: "<rootDir>/src/client/tests/integration/global-teardown.ts",
|
||||
// Run integration tests serially to avoid rate limiting and race conditions
|
||||
maxWorkers: 1,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0ai",
|
||||
"version": "2.4.2",
|
||||
"version": "2.4.3",
|
||||
"description": "The Memory Layer For Your AI Apps",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -34,12 +34,6 @@ export const DEFAULT_MEMORY_CONFIG: MemoryConfig = {
|
||||
username: process.env.NEO4J_USERNAME || "neo4j",
|
||||
password: process.env.NEO4J_PASSWORD || "password",
|
||||
},
|
||||
llm: {
|
||||
provider: "openai",
|
||||
config: {
|
||||
model: "gpt-4-turbo-preview",
|
||||
},
|
||||
},
|
||||
},
|
||||
historyStore: {
|
||||
provider: "sqlite",
|
||||
|
||||
@@ -80,18 +80,18 @@ export class MemoryGraph {
|
||||
);
|
||||
|
||||
this.llmProvider = "openai";
|
||||
let llmConfig = this.config.llm.config;
|
||||
|
||||
if (this.config.llm?.provider) {
|
||||
this.llmProvider = this.config.llm.provider;
|
||||
}
|
||||
if (this.config.graphStore?.llm?.provider) {
|
||||
this.llmProvider = this.config.graphStore.llm.provider;
|
||||
llmConfig = this.config.graphStore.llm.config ?? llmConfig;
|
||||
}
|
||||
|
||||
this.llm = LLMFactory.create(this.llmProvider, this.config.llm.config);
|
||||
this.structuredLlm = LLMFactory.create(
|
||||
this.llmProvider,
|
||||
this.config.llm.config,
|
||||
);
|
||||
this.llm = LLMFactory.create(this.llmProvider, llmConfig);
|
||||
this.structuredLlm = LLMFactory.create(this.llmProvider, llmConfig);
|
||||
this.threshold = 0.7;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ export const MemoryUpdateSchema = z.object({
|
||||
old_memory: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.describe(
|
||||
"The previous content of the memory item if the event was UPDATE.",
|
||||
),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Client, Pool } from "pg";
|
||||
import type { Client as ClientType } from "pg";
|
||||
import pkg from "pg";
|
||||
const { Client } = pkg;
|
||||
import { VectorStore } from "./base";
|
||||
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
||||
|
||||
@@ -14,7 +16,7 @@ interface PGVectorConfig extends VectorStoreConfig {
|
||||
}
|
||||
|
||||
export class PGVector implements VectorStore {
|
||||
private client: Client;
|
||||
private client: ClientType;
|
||||
private collectionName: string;
|
||||
private useDiskann: boolean;
|
||||
private useHnsw: boolean;
|
||||
@@ -35,6 +37,7 @@ export class PGVector implements VectorStore {
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
});
|
||||
this.initialize().catch(console.error);
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
|
||||
@@ -349,6 +349,78 @@ describe("ConfigManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Graph store LLM config propagation (issue #3425)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
describe("mergeConfig - graph store LLM config (issue #3425)", () => {
|
||||
const baseEmbedder = {
|
||||
provider: "openai",
|
||||
config: { apiKey: "test-key" },
|
||||
};
|
||||
const baseVectorStore = {
|
||||
provider: "memory",
|
||||
config: { collectionName: "test" },
|
||||
};
|
||||
const graphStoreNeo4j = {
|
||||
provider: "neo4j",
|
||||
config: {
|
||||
url: "neo4j://localhost:7687",
|
||||
username: "neo4j",
|
||||
password: "password",
|
||||
},
|
||||
};
|
||||
|
||||
it("should NOT have a default graphStore.llm — root llm should be the fallback", () => {
|
||||
const config = ConfigManager.mergeConfig({
|
||||
embedder: baseEmbedder,
|
||||
vectorStore: baseVectorStore,
|
||||
llm: {
|
||||
provider: "anthropic",
|
||||
config: { model: "claude-sonnet-4-20250514" },
|
||||
},
|
||||
graphStore: graphStoreNeo4j,
|
||||
});
|
||||
|
||||
// graphStore should NOT have its own llm after merge
|
||||
expect(config.graphStore?.llm).toBeUndefined();
|
||||
// root llm should be anthropic
|
||||
expect(config.llm.provider).toBe("anthropic");
|
||||
expect(config.llm.config.model).toBe("claude-sonnet-4-20250514");
|
||||
});
|
||||
|
||||
it("should preserve explicit graphStore.llm when user provides it", () => {
|
||||
const config = ConfigManager.mergeConfig({
|
||||
embedder: baseEmbedder,
|
||||
vectorStore: baseVectorStore,
|
||||
llm: {
|
||||
provider: "anthropic",
|
||||
config: { model: "claude-sonnet-4-20250514" },
|
||||
},
|
||||
graphStore: {
|
||||
...graphStoreNeo4j,
|
||||
llm: { provider: "openai", config: { model: "gpt-4o" } },
|
||||
},
|
||||
});
|
||||
|
||||
// graphStore should have its own llm
|
||||
expect(config.graphStore?.llm?.provider).toBe("openai");
|
||||
expect(config.graphStore?.llm?.config).toEqual({ model: "gpt-4o" });
|
||||
// root llm should still be anthropic
|
||||
expect(config.llm.provider).toBe("anthropic");
|
||||
});
|
||||
|
||||
it("should not have graphStore.llm when user does not provide one", () => {
|
||||
const config = ConfigManager.mergeConfig({
|
||||
embedder: baseEmbedder,
|
||||
vectorStore: baseVectorStore,
|
||||
llm: { provider: "openai", config: { model: "gpt-4o" } },
|
||||
});
|
||||
|
||||
// Default graphStore should not have llm
|
||||
expect(config.graphStore?.llm).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Memory class – LM Studio end-to-end flow (mocked factories)
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -118,6 +118,11 @@ jest.mock("../src/vector_stores/azure_ai_search", () => ({
|
||||
.fn()
|
||||
.mockImplementation((config) => ({ type: "azure-ai-search", config })),
|
||||
}));
|
||||
jest.mock("../src/vector_stores/pgvector", () => ({
|
||||
PGVector: jest
|
||||
.fn()
|
||||
.mockImplementation((config) => ({ type: "pgvector", config })),
|
||||
}));
|
||||
jest.mock("../src/storage/SupabaseHistoryManager", () => ({
|
||||
SupabaseHistoryManager: jest
|
||||
.fn()
|
||||
@@ -236,6 +241,7 @@ describe("VectorStoreFactory", () => {
|
||||
["langchain"],
|
||||
["vectorize"],
|
||||
["azure-ai-search"],
|
||||
["pgvector"],
|
||||
])("creates vector store for provider '%s'", (provider) => {
|
||||
expect(() =>
|
||||
VectorStoreFactory.create(provider, dummyVSConfig),
|
||||
|
||||
@@ -511,6 +511,137 @@ describe("Prompt construction — all json_object sites include 'json'", () => {
|
||||
// 5. Edge cases – malformed entity fields in _removeSpacesFromEntities
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// 5a. LLM config propagation — graph store uses correct provider & config
|
||||
// Regression test for https://github.com/mem0ai/mem0/issues/3425
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
describe("LLM config propagation to graph store (issue #3425)", () => {
|
||||
const { LLMFactory } = require("../src/utils/factory");
|
||||
|
||||
beforeEach(() => {
|
||||
(LLMFactory.create as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
it("uses root llm config when no graphStore.llm is provided", () => {
|
||||
const config = {
|
||||
graphStore: {
|
||||
config: {
|
||||
url: "bolt://localhost:7687",
|
||||
username: "neo4j",
|
||||
password: "test",
|
||||
},
|
||||
},
|
||||
embedder: { provider: "openai", config: {} },
|
||||
llm: {
|
||||
provider: "anthropic",
|
||||
config: { model: "claude-sonnet-4-20250514", apiKey: "sk-ant-test" },
|
||||
},
|
||||
} as any;
|
||||
|
||||
new MemoryGraph(config);
|
||||
|
||||
expect(LLMFactory.create).toHaveBeenCalledWith("anthropic", {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
apiKey: "sk-ant-test",
|
||||
});
|
||||
// Both llm and structuredLlm should use the same config
|
||||
expect(LLMFactory.create).toHaveBeenCalledTimes(2);
|
||||
expect(LLMFactory.create).toHaveBeenNthCalledWith(1, "anthropic", {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
apiKey: "sk-ant-test",
|
||||
});
|
||||
expect(LLMFactory.create).toHaveBeenNthCalledWith(2, "anthropic", {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
apiKey: "sk-ant-test",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses graphStore.llm config when provided, overriding root llm", () => {
|
||||
const config = {
|
||||
graphStore: {
|
||||
config: {
|
||||
url: "bolt://localhost:7687",
|
||||
username: "neo4j",
|
||||
password: "test",
|
||||
},
|
||||
llm: {
|
||||
provider: "openai",
|
||||
config: { model: "gpt-4o", apiKey: "sk-openai-test" },
|
||||
},
|
||||
},
|
||||
embedder: { provider: "openai", config: {} },
|
||||
llm: {
|
||||
provider: "anthropic",
|
||||
config: { model: "claude-sonnet-4-20250514", apiKey: "sk-ant-test" },
|
||||
},
|
||||
} as any;
|
||||
|
||||
new MemoryGraph(config);
|
||||
|
||||
// Should use graphStore.llm, NOT root llm
|
||||
expect(LLMFactory.create).toHaveBeenNthCalledWith(1, "openai", {
|
||||
model: "gpt-4o",
|
||||
apiKey: "sk-openai-test",
|
||||
});
|
||||
expect(LLMFactory.create).toHaveBeenNthCalledWith(2, "openai", {
|
||||
model: "gpt-4o",
|
||||
apiKey: "sk-openai-test",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to root llm config when graphStore.llm.config is undefined", () => {
|
||||
// Note: in practice, Zod schema requires config when graphStore.llm is
|
||||
// present. This tests the defensive fallback in MemoryGraph itself.
|
||||
const config = {
|
||||
graphStore: {
|
||||
config: {
|
||||
url: "bolt://localhost:7687",
|
||||
username: "neo4j",
|
||||
password: "test",
|
||||
},
|
||||
llm: {
|
||||
provider: "openai",
|
||||
// config explicitly undefined
|
||||
config: undefined,
|
||||
},
|
||||
},
|
||||
embedder: { provider: "openai", config: {} },
|
||||
llm: {
|
||||
provider: "anthropic",
|
||||
config: { model: "claude-sonnet-4-20250514" },
|
||||
},
|
||||
} as any;
|
||||
|
||||
new MemoryGraph(config);
|
||||
|
||||
// Provider from graphStore.llm, but config falls back to root llm.config
|
||||
expect(LLMFactory.create).toHaveBeenNthCalledWith(1, "openai", {
|
||||
model: "claude-sonnet-4-20250514",
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults to openai when neither root nor graphStore llm provider is set", () => {
|
||||
const config = {
|
||||
graphStore: {
|
||||
config: {
|
||||
url: "bolt://localhost:7687",
|
||||
username: "neo4j",
|
||||
password: "test",
|
||||
},
|
||||
},
|
||||
embedder: { provider: "openai", config: {} },
|
||||
llm: { config: { model: "gpt-4" } },
|
||||
} as any;
|
||||
|
||||
new MemoryGraph(config);
|
||||
|
||||
expect(LLMFactory.create).toHaveBeenNthCalledWith(1, "openai", {
|
||||
model: "gpt-4",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("_removeSpacesFromEntities (via _establishNodesRelationsFromData)", () => {
|
||||
it("normalises spaces and case in entity source/relationship/destination", async () => {
|
||||
mockGenerateResponse.mockResolvedValueOnce({
|
||||
|
||||
@@ -16,7 +16,7 @@ class AnthropicConfig(BaseLlmConfig):
|
||||
temperature: float = 0.1,
|
||||
api_key: Optional[str] = None,
|
||||
max_tokens: int = 2000,
|
||||
top_p: float = 0.1,
|
||||
top_p: Optional[float] = None,
|
||||
top_k: int = 1,
|
||||
enable_vision: bool = False,
|
||||
vision_details: Optional[str] = "auto",
|
||||
@@ -32,7 +32,7 @@ class AnthropicConfig(BaseLlmConfig):
|
||||
temperature: Controls randomness, defaults to 0.1
|
||||
api_key: Anthropic API key, defaults to None
|
||||
max_tokens: Maximum tokens to generate, defaults to 2000
|
||||
top_p: Nucleus sampling parameter, defaults to 0.1
|
||||
top_p: Nucleus sampling parameter, defaults to None (omitted to avoid conflict with temperature)
|
||||
top_k: Top-k sampling parameter, defaults to 1
|
||||
enable_vision: Enable vision capabilities, defaults to False
|
||||
vision_details: Vision detail level, defaults to "auto"
|
||||
|
||||
@@ -16,7 +16,7 @@ class AWSBedrockConfig(BaseLlmConfig):
|
||||
model: Optional[str] = None,
|
||||
temperature: float = 0.1,
|
||||
max_tokens: int = 2000,
|
||||
top_p: float = 0.9,
|
||||
top_p: Optional[float] = None,
|
||||
top_k: int = 1,
|
||||
aws_access_key_id: Optional[str] = None,
|
||||
aws_secret_access_key: Optional[str] = None,
|
||||
@@ -33,7 +33,8 @@ class AWSBedrockConfig(BaseLlmConfig):
|
||||
model: Bedrock model identifier (e.g., "amazon.nova-3-mini-20241119-v1:0")
|
||||
temperature: Controls randomness (0.0 to 2.0)
|
||||
max_tokens: Maximum tokens to generate
|
||||
top_p: Nucleus sampling parameter (0.0 to 1.0)
|
||||
top_p: Nucleus sampling (0.0–1.0). Default None omits topP on Converse
|
||||
(required for Anthropic, which rejects temperature and topP together).
|
||||
top_k: Top-k sampling parameter (1 to 40)
|
||||
aws_access_key_id: AWS access key (optional, uses env vars if not provided)
|
||||
aws_secret_access_key: AWS secret key (optional, uses env vars if not provided)
|
||||
@@ -75,13 +76,16 @@ class AWSBedrockConfig(BaseLlmConfig):
|
||||
|
||||
def get_model_config(self) -> Dict[str, Any]:
|
||||
"""Get model-specific configuration parameters."""
|
||||
base_config = {
|
||||
base_config: Dict[str, Any] = {
|
||||
"temperature": self.temperature,
|
||||
"max_tokens": self.max_tokens,
|
||||
"top_p": self.top_p,
|
||||
"top_k": self.top_k,
|
||||
}
|
||||
|
||||
# Only include top_p when explicitly set by the user.
|
||||
if self.top_p is not None:
|
||||
base_config["top_p"] = self.top_p
|
||||
|
||||
# Add custom model kwargs
|
||||
base_config.update(self.model_kwargs)
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ class ChromaDbConfig(BaseModel):
|
||||
values.pop("path", None)
|
||||
return values
|
||||
|
||||
# Check if local/server configuration is provided (excluding default tmp path for cloud config)
|
||||
local_config = bool(path and path != "/tmp/chroma") or bool(host and port)
|
||||
# Check if local/server configuration is provided
|
||||
local_config = bool(path) or bool(host and port)
|
||||
|
||||
if not cloud_config and not local_config:
|
||||
raise ValueError("Either ChromaDB Cloud configuration (api_key, tenant) or local configuration (path or host/port) must be provided.")
|
||||
|
||||
@@ -16,7 +16,7 @@ class QdrantConfig(BaseModel):
|
||||
path: Optional[str] = Field("/tmp/qdrant", description="Path for local Qdrant database")
|
||||
url: Optional[str] = Field(None, description="Full URL for Qdrant server")
|
||||
api_key: Optional[str] = Field(None, description="API key for Qdrant server")
|
||||
on_disk: Optional[bool] = Field(False, description="Enables persistent storage")
|
||||
on_disk: Optional[bool] = Field(False,description="Enables persistent storage. Vectors are kept on disk (True) or in memory (False). Does not delete the local database path.")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import os
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
|
||||
class TurbopufferConfig(BaseModel):
|
||||
collection_name: str = Field("mem0", description="Name of the namespace/collection")
|
||||
embedding_model_dims: int = Field(1536, description="Dimensions of the embedding model")
|
||||
api_key: Optional[str] = Field(None, description="API key for Turbopuffer")
|
||||
region: str = Field("gcp-us-central1", description="Turbopuffer region (e.g., 'gcp-us-central1', 'aws-us-west-2')")
|
||||
distance_metric: str = Field(
|
||||
"cosine_distance",
|
||||
description="Distance metric for vector similarity ('cosine_distance' or 'euclidean_squared')",
|
||||
)
|
||||
batch_size: int = Field(100, description="Batch size for bulk operations")
|
||||
extra_params: Optional[Dict[str, Any]] = Field(
|
||||
None,
|
||||
description="Additional parameters for Turbopuffer client",
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def check_api_key(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
api_key = values.get("api_key")
|
||||
if not api_key and "TURBOPUFFER_API_KEY" not in os.environ:
|
||||
raise ValueError(
|
||||
"Either 'api_key' must be provided or TURBOPUFFER_API_KEY environment variable must be set."
|
||||
)
|
||||
return values
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def validate_extra_fields(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
||||
allowed_fields = set(cls.model_fields.keys())
|
||||
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 input only the following fields: {', '.join(allowed_fields)}"
|
||||
)
|
||||
return values
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
+11
-10
@@ -13,6 +13,9 @@ class OpenAIEmbedding(EmbeddingBase):
|
||||
super().__init__(config)
|
||||
|
||||
self.config.model = self.config.model or "text-embedding-3-small"
|
||||
# Only pass `dimensions` to the API when the user set embedding_dims; non-matryoshka
|
||||
# OpenAI-compatible backends (vLLM, Voyage, etc.) reject the parameter
|
||||
self._pass_dimensions_to_api = self.config.embedding_dims is not None
|
||||
self.config.embedding_dims = self.config.embedding_dims or 1536
|
||||
|
||||
api_key = self.config.api_key or os.getenv("OPENAI_API_KEY")
|
||||
@@ -42,13 +45,11 @@ class OpenAIEmbedding(EmbeddingBase):
|
||||
list: The embedding vector.
|
||||
"""
|
||||
text = text.replace("\n", " ")
|
||||
return (
|
||||
self.client.embeddings.create(
|
||||
input=[text],
|
||||
model=self.config.model,
|
||||
dimensions=self.config.embedding_dims,
|
||||
encoding_format="float",
|
||||
)
|
||||
.data[0]
|
||||
.embedding
|
||||
)
|
||||
kwargs = {
|
||||
"input": [text],
|
||||
"model": self.config.model,
|
||||
"encoding_format": "float",
|
||||
}
|
||||
if self._pass_dimensions_to_api:
|
||||
kwargs["dimensions"] = self.config.embedding_dims
|
||||
return self.client.embeddings.create(**kwargs).data[0].embedding
|
||||
|
||||
@@ -409,6 +409,28 @@ class NeptuneBase(ABC):
|
||||
"""
|
||||
pass
|
||||
|
||||
def delete(self, data, filters):
|
||||
"""
|
||||
Delete graph entities associated with the given memory text.
|
||||
|
||||
Extracts entities and relationships from the memory text using the same
|
||||
pipeline as add(), then deletes the matching relationships in the graph.
|
||||
|
||||
Args:
|
||||
data (str): The memory text whose graph entities should be removed.
|
||||
filters (dict): Scope filters (user_id, agent_id, run_id).
|
||||
"""
|
||||
try:
|
||||
entity_type_map = self._retrieve_nodes_from_data(data, filters)
|
||||
if not entity_type_map:
|
||||
logger.debug("No entities found in memory text, skipping graph cleanup")
|
||||
return
|
||||
to_be_deleted = self._establish_nodes_relations_from_data(data, filters, entity_type_map)
|
||||
if to_be_deleted:
|
||||
self._delete_entities(to_be_deleted, filters["user_id"])
|
||||
except Exception as e:
|
||||
logger.error(f"Error during graph cleanup for memory delete: {e}")
|
||||
|
||||
def delete_all(self, filters):
|
||||
cypher, params = self._delete_all_cypher(filters)
|
||||
self.graph.query(cypher, params=params)
|
||||
|
||||
@@ -40,6 +40,31 @@ class AnthropicLLM(LLMBase):
|
||||
api_key = self.config.api_key or os.getenv("ANTHROPIC_API_KEY")
|
||||
self.client = anthropic.Anthropic(api_key=api_key)
|
||||
|
||||
def _get_common_params(self, **kwargs) -> Dict:
|
||||
"""Get common parameters, avoiding sending both temperature and top_p together.
|
||||
|
||||
Anthropic rejects requests that include both temperature and top_p.
|
||||
When both are set, we keep temperature and drop top_p.
|
||||
"""
|
||||
params = {}
|
||||
|
||||
if self.config.max_tokens is not None:
|
||||
params["max_tokens"] = self.config.max_tokens
|
||||
|
||||
has_temperature = self.config.temperature is not None
|
||||
has_top_p = self.config.top_p is not None
|
||||
|
||||
if has_temperature and has_top_p:
|
||||
# Anthropic forbids both; prefer temperature
|
||||
params["temperature"] = self.config.temperature
|
||||
elif has_temperature:
|
||||
params["temperature"] = self.config.temperature
|
||||
elif has_top_p:
|
||||
params["top_p"] = self.config.top_p
|
||||
|
||||
params.update(kwargs)
|
||||
return params
|
||||
|
||||
def generate_response(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
|
||||
+43
-41
@@ -228,6 +228,12 @@ class AWSBedrockLLM(LLMBase):
|
||||
|
||||
return "\n\nHuman: " + "".join(formatted_messages) + "\n\nAssistant:"
|
||||
|
||||
def _merge_optional_top_p(self, target: Dict[str, Any], *, key: str = "top_p") -> None:
|
||||
"""Add nucleus sampling to ``target`` only when ``model_config`` has ``top_p`` set."""
|
||||
top_p = self.model_config.get("top_p")
|
||||
if top_p is not None:
|
||||
target[key] = top_p
|
||||
|
||||
def _prepare_input(self, prompt: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Prepare input for the current provider's model.
|
||||
@@ -268,44 +274,38 @@ class AWSBedrockLLM(LLMBase):
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"max_tokens": self.model_config.get("max_tokens", 5000),
|
||||
"temperature": self.model_config.get("temperature", 0.1),
|
||||
"top_p": self.model_config.get("top_p", 0.9),
|
||||
}
|
||||
self._merge_optional_top_p(input_body)
|
||||
else:
|
||||
# Legacy Amazon models
|
||||
input_body = {
|
||||
"inputText": prompt,
|
||||
"textGenerationConfig": {
|
||||
"maxTokenCount": self.model_config.get("max_tokens", 5000),
|
||||
"topP": self.model_config.get("top_p", 0.9),
|
||||
"temperature": self.model_config.get("temperature", 0.1),
|
||||
},
|
||||
}
|
||||
# Remove None values
|
||||
input_body["textGenerationConfig"] = {
|
||||
k: v for k, v in input_body["textGenerationConfig"].items() if v is not None
|
||||
text_gen_config: Dict[str, Any] = {
|
||||
"maxTokenCount": self.model_config.get("max_tokens", 5000),
|
||||
"temperature": self.model_config.get("temperature", 0.1),
|
||||
}
|
||||
self._merge_optional_top_p(text_gen_config, key="topP")
|
||||
input_body = {"inputText": prompt, "textGenerationConfig": text_gen_config}
|
||||
elif self.provider == "anthropic":
|
||||
input_body = {
|
||||
"messages": [{"role": "user", "content": [{"type": "text", "text": prompt}]}],
|
||||
"max_tokens": self.model_config.get("max_tokens", 2000),
|
||||
"temperature": self.model_config.get("temperature", 0.1),
|
||||
"top_p": self.model_config.get("top_p", 0.9),
|
||||
"anthropic_version": "bedrock-2023-05-31",
|
||||
}
|
||||
self._merge_optional_top_p(input_body)
|
||||
elif self.provider == "meta":
|
||||
input_body = {
|
||||
"prompt": prompt,
|
||||
"max_gen_len": self.model_config.get("max_tokens", 5000),
|
||||
"temperature": self.model_config.get("temperature", 0.1),
|
||||
"top_p": self.model_config.get("top_p", 0.9),
|
||||
}
|
||||
self._merge_optional_top_p(input_body)
|
||||
elif self.provider == "mistral":
|
||||
input_body = {
|
||||
"prompt": prompt,
|
||||
"max_tokens": self.model_config.get("max_tokens", 5000),
|
||||
"temperature": self.model_config.get("temperature", 0.1),
|
||||
"top_p": self.model_config.get("top_p", 0.9),
|
||||
}
|
||||
self._merge_optional_top_p(input_body)
|
||||
else:
|
||||
# Generic case - add all model config parameters
|
||||
input_body.update(self.model_config)
|
||||
@@ -479,6 +479,29 @@ class AWSBedrockLLM(LLMBase):
|
||||
|
||||
return converse_tools
|
||||
|
||||
def _default_max_tokens_for_converse(self) -> int:
|
||||
"""Default maxTokens if ``max_tokens`` is missing (Nova: 5000, else 2000)."""
|
||||
model_id = (self.config.model or "").lower()
|
||||
if self.provider == "amazon" and "nova" in model_id:
|
||||
return 5000
|
||||
return 2000
|
||||
|
||||
def _build_inference_config(self) -> Dict[str, Any]:
|
||||
"""Build Converse ``inferenceConfig``. Anthropic allows only one of temperature or topP; we keep temperature and omit topP."""
|
||||
inference_config: Dict[str, Any] = {
|
||||
"maxTokens": self.model_config.get("max_tokens", self._default_max_tokens_for_converse()),
|
||||
"temperature": self.model_config.get("temperature", 0.1),
|
||||
}
|
||||
|
||||
top_p = self.model_config.get("top_p")
|
||||
if top_p is not None:
|
||||
if self.provider == "anthropic":
|
||||
logger.debug("Omitting topP for Anthropic Converse (using temperature); top_p=%s", top_p)
|
||||
else:
|
||||
inference_config["topP"] = top_p
|
||||
|
||||
return inference_config
|
||||
|
||||
def _generate_with_tools(self, messages: List[Dict[str, str]], tools: List[Dict], stream: bool = False) -> Dict[str, Any]:
|
||||
"""Generate response with tool calling support using correct message format."""
|
||||
# Format messages for tool-enabled models
|
||||
@@ -501,11 +524,7 @@ class AWSBedrockLLM(LLMBase):
|
||||
converse_params = {
|
||||
"modelId": self.config.model,
|
||||
"messages": formatted_messages,
|
||||
"inferenceConfig": {
|
||||
"maxTokens": self.model_config.get("max_tokens", 2000),
|
||||
"temperature": self.model_config.get("temperature", 0.1),
|
||||
"topP": self.model_config.get("top_p", 0.9),
|
||||
}
|
||||
"inferenceConfig": self._build_inference_config(),
|
||||
}
|
||||
|
||||
# Add system message if present (for Anthropic)
|
||||
@@ -531,11 +550,7 @@ class AWSBedrockLLM(LLMBase):
|
||||
converse_params = {
|
||||
"modelId": self.config.model,
|
||||
"messages": formatted_messages,
|
||||
"inferenceConfig": {
|
||||
"maxTokens": self.model_config.get("max_tokens", 2000),
|
||||
"temperature": self.model_config.get("temperature", 0.1),
|
||||
"topP": self.model_config.get("top_p", 0.9),
|
||||
}
|
||||
"inferenceConfig": self._build_inference_config(),
|
||||
}
|
||||
|
||||
# Add system message if present
|
||||
@@ -554,26 +569,13 @@ class AWSBedrockLLM(LLMBase):
|
||||
return str(response)
|
||||
|
||||
elif self.provider == "amazon" and "nova" in self.config.model.lower():
|
||||
# Nova models use converse API even without tools
|
||||
# Nova models use the Converse API even without tools
|
||||
formatted_messages = self._format_messages_amazon(messages)
|
||||
input_body = {
|
||||
"messages": formatted_messages,
|
||||
"max_tokens": self.model_config.get("max_tokens", 5000),
|
||||
"temperature": self.model_config.get("temperature", 0.1),
|
||||
"top_p": self.model_config.get("top_p", 0.9),
|
||||
}
|
||||
|
||||
# Use converse API for Nova models
|
||||
response = self.client.converse(
|
||||
modelId=self.config.model,
|
||||
messages=input_body["messages"],
|
||||
inferenceConfig={
|
||||
"maxTokens": input_body["max_tokens"],
|
||||
"temperature": input_body["temperature"],
|
||||
"topP": input_body["top_p"],
|
||||
}
|
||||
messages=formatted_messages,
|
||||
inferenceConfig=self._build_inference_config(),
|
||||
)
|
||||
|
||||
return self._parse_response(response)
|
||||
else:
|
||||
# For other providers and legacy Amazon models (like Titan)
|
||||
|
||||
+11
-8
@@ -32,22 +32,25 @@ class GeminiLLM(LLMBase):
|
||||
Returns:
|
||||
str or dict: The processed response.
|
||||
"""
|
||||
# Get parts safely — content can be None when Gemini blocks the response
|
||||
candidate = response.candidates[0] if response.candidates else None
|
||||
parts = candidate.content.parts if candidate and candidate.content else None
|
||||
|
||||
if tools:
|
||||
processed_response = {
|
||||
"content": None,
|
||||
"tool_calls": [],
|
||||
}
|
||||
|
||||
# Extract content from the first candidate
|
||||
if response.candidates and response.candidates[0].content.parts:
|
||||
for part in response.candidates[0].content.parts:
|
||||
if parts:
|
||||
# Extract content from the first candidate
|
||||
for part in parts:
|
||||
if hasattr(part, "text") and part.text:
|
||||
processed_response["content"] = part.text
|
||||
break
|
||||
|
||||
# Extract function calls
|
||||
if response.candidates and response.candidates[0].content.parts:
|
||||
for part in response.candidates[0].content.parts:
|
||||
# Extract function calls
|
||||
for part in parts:
|
||||
if hasattr(part, "function_call") and part.function_call:
|
||||
fn = part.function_call
|
||||
processed_response["tool_calls"].append(
|
||||
@@ -59,8 +62,8 @@ class GeminiLLM(LLMBase):
|
||||
|
||||
return processed_response
|
||||
else:
|
||||
if response.candidates and response.candidates[0].content.parts:
|
||||
for part in response.candidates[0].content.parts:
|
||||
if parts:
|
||||
for part in parts:
|
||||
if hasattr(part, "text") and part.text:
|
||||
return part.text
|
||||
return ""
|
||||
|
||||
@@ -246,6 +246,28 @@ class MemoryGraph:
|
||||
logger.info(f"Returned {len(search_results)} search results")
|
||||
return search_results
|
||||
|
||||
def delete(self, data, filters):
|
||||
"""
|
||||
Delete graph entities associated with the given memory text.
|
||||
|
||||
Extracts entities and relationships from the memory text using the same
|
||||
pipeline as add(), then deletes the matching relationships in the graph.
|
||||
|
||||
Args:
|
||||
data (str): The memory text whose graph entities should be removed.
|
||||
filters (dict): Scope filters (user_id, agent_id, run_id).
|
||||
"""
|
||||
try:
|
||||
entity_type_map = self._retrieve_nodes_from_data(data, filters)
|
||||
if not entity_type_map:
|
||||
logger.debug("No entities found in memory text, skipping graph cleanup")
|
||||
return
|
||||
to_be_deleted = self._establish_nodes_relations_from_data(data, filters, entity_type_map)
|
||||
if to_be_deleted:
|
||||
self._delete_entities(to_be_deleted, filters)
|
||||
except Exception as e:
|
||||
logger.error(f"Error during graph cleanup for memory delete: {e}")
|
||||
|
||||
def delete_all(self, filters):
|
||||
"""Delete all nodes and relationships for a user or specific agent."""
|
||||
where_parts = ["n.user_id = %s"]
|
||||
|
||||
+68
-18
@@ -129,6 +129,28 @@ class MemoryGraph:
|
||||
|
||||
return search_results
|
||||
|
||||
def delete(self, data, filters):
|
||||
"""
|
||||
Delete graph entities associated with the given memory text.
|
||||
|
||||
Extracts entities and relationships from the memory text using the same
|
||||
pipeline as add(), then soft-deletes the matching relationships in the graph.
|
||||
|
||||
Args:
|
||||
data (str): The memory text whose graph entities should be removed.
|
||||
filters (dict): Scope filters (user_id, agent_id, run_id).
|
||||
"""
|
||||
try:
|
||||
entity_type_map = self._retrieve_nodes_from_data(data, filters)
|
||||
if not entity_type_map:
|
||||
logger.debug("No entities found in memory text, skipping graph cleanup")
|
||||
return
|
||||
to_be_deleted = self._establish_nodes_relations_from_data(data, filters, entity_type_map)
|
||||
if to_be_deleted:
|
||||
self._delete_entities(to_be_deleted, filters)
|
||||
except Exception as e:
|
||||
logger.error(f"Error during graph cleanup for memory delete: {e}")
|
||||
|
||||
def delete_all(self, filters):
|
||||
# Build node properties for filtering
|
||||
node_props = ["user_id: $user_id"]
|
||||
@@ -174,6 +196,7 @@ class MemoryGraph:
|
||||
|
||||
query = f"""
|
||||
MATCH (n {self.node_label} {{{node_props_str}}})-[r]->(m {self.node_label} {{{node_props_str}}})
|
||||
WHERE r.valid IS NULL OR r.valid = true
|
||||
RETURN n.name AS source, type(r) AS relationship, m.name AS target
|
||||
LIMIT $limit
|
||||
"""
|
||||
@@ -291,10 +314,12 @@ class MemoryGraph:
|
||||
CALL {{
|
||||
WITH n
|
||||
MATCH (n)-[r]->(m {self.node_label} {{{node_props_str}}})
|
||||
WHERE r.valid IS NULL OR r.valid = true
|
||||
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
|
||||
WITH n
|
||||
MATCH (n)<-[r]-(m {self.node_label} {{{node_props_str}}})
|
||||
WHERE r.valid IS NULL OR r.valid = true
|
||||
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
|
||||
@@ -392,13 +417,15 @@ class MemoryGraph:
|
||||
source_props_str = ", ".join(source_props)
|
||||
dest_props_str = ", ".join(dest_props)
|
||||
|
||||
# Delete the specific relationship between nodes
|
||||
# Soft-delete: mark relationship as invalid instead of removing it,
|
||||
# enabling temporal reasoning over historical graph state.
|
||||
# See: https://github.com/mem0ai/mem0/issues/4187
|
||||
cypher = f"""
|
||||
MATCH (n {self.node_label} {{{source_props_str}}})
|
||||
-[r:{relationship}]->
|
||||
(m {self.node_label} {{{dest_props_str}}})
|
||||
|
||||
DELETE r
|
||||
WHERE r.valid IS NULL OR r.valid = true
|
||||
SET r.valid = false, r.invalidated_at = datetime()
|
||||
RETURN
|
||||
n.name AS source,
|
||||
m.name AS target,
|
||||
@@ -464,11 +491,16 @@ class MemoryGraph:
|
||||
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 CREATE SET
|
||||
r.created_at = timestamp(),
|
||||
r.updated_at = timestamp(),
|
||||
r.mentions = 1,
|
||||
r.valid = true
|
||||
ON MATCH SET
|
||||
r.mentions = coalesce(r.mentions, 0) + 1
|
||||
r.mentions = coalesce(r.mentions, 0) + 1,
|
||||
r.valid = true,
|
||||
r.updated_at = timestamp(),
|
||||
r.invalidated_at = null
|
||||
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
|
||||
"""
|
||||
|
||||
@@ -508,11 +540,16 @@ class MemoryGraph:
|
||||
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 CREATE SET
|
||||
r.created_at = timestamp(),
|
||||
r.updated_at = timestamp(),
|
||||
r.mentions = 1,
|
||||
r.valid = true
|
||||
ON MATCH SET
|
||||
r.mentions = coalesce(r.mentions, 0) + 1
|
||||
r.mentions = coalesce(r.mentions, 0) + 1,
|
||||
r.valid = true,
|
||||
r.updated_at = timestamp(),
|
||||
r.invalidated_at = null
|
||||
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
|
||||
"""
|
||||
|
||||
@@ -537,11 +574,16 @@ class MemoryGraph:
|
||||
WHERE elementId(destination) = $destination_id
|
||||
SET destination.mentions = coalesce(destination.mentions, 0) + 1
|
||||
MERGE (source)-[r:{relationship}]->(destination)
|
||||
ON CREATE SET
|
||||
ON CREATE SET
|
||||
r.created_at = timestamp(),
|
||||
r.updated_at = timestamp(),
|
||||
r.mentions = 1
|
||||
ON MATCH SET r.mentions = coalesce(r.mentions, 0) + 1
|
||||
r.mentions = 1,
|
||||
r.valid = true
|
||||
ON MATCH SET
|
||||
r.mentions = coalesce(r.mentions, 0) + 1,
|
||||
r.valid = true,
|
||||
r.updated_at = timestamp(),
|
||||
r.invalidated_at = null
|
||||
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
|
||||
"""
|
||||
|
||||
@@ -585,10 +627,18 @@ class MemoryGraph:
|
||||
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
|
||||
MERGE (source)-[r:{relationship}]->(destination)
|
||||
ON CREATE SET
|
||||
r.created_at = timestamp(),
|
||||
r.updated_at = timestamp(),
|
||||
r.mentions = 1,
|
||||
r.valid = true
|
||||
ON MATCH SET
|
||||
r.mentions = coalesce(r.mentions, 0) + 1,
|
||||
r.valid = true,
|
||||
r.updated_at = timestamp(),
|
||||
r.invalidated_at = null
|
||||
RETURN source.name AS source, type(r) AS relationship, destination.name AS target
|
||||
"""
|
||||
|
||||
params = {
|
||||
|
||||
@@ -149,6 +149,28 @@ class MemoryGraph:
|
||||
|
||||
return search_results
|
||||
|
||||
def delete(self, data, filters):
|
||||
"""
|
||||
Delete graph entities associated with the given memory text.
|
||||
|
||||
Extracts entities and relationships from the memory text using the same
|
||||
pipeline as add(), then deletes the matching relationships in the graph.
|
||||
|
||||
Args:
|
||||
data (str): The memory text whose graph entities should be removed.
|
||||
filters (dict): Scope filters (user_id, agent_id, run_id).
|
||||
"""
|
||||
try:
|
||||
entity_type_map = self._retrieve_nodes_from_data(data, filters)
|
||||
if not entity_type_map:
|
||||
logger.debug("No entities found in memory text, skipping graph cleanup")
|
||||
return
|
||||
to_be_deleted = self._establish_nodes_relations_from_data(data, filters, entity_type_map)
|
||||
if to_be_deleted:
|
||||
self._delete_entities(to_be_deleted, filters)
|
||||
except Exception as e:
|
||||
logger.error(f"Error during graph cleanup for memory delete: {e}")
|
||||
|
||||
def delete_all(self, filters):
|
||||
# Build node properties for filtering
|
||||
node_props = ["user_id: $user_id"]
|
||||
|
||||
+157
-58
@@ -9,7 +9,7 @@ import uuid
|
||||
import warnings
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
@@ -479,7 +479,8 @@ class Memory(MemoryBase):
|
||||
|
||||
msg_content = message_dict["content"]
|
||||
msg_embeddings = self.embedding_model.embed(msg_content, "add")
|
||||
mem_id = self._create_memory(msg_content, msg_embeddings, per_msg_meta)
|
||||
# Pass embeddings as a dict so _create_memory can reuse the cached embedding
|
||||
mem_id = self._create_memory(msg_content, {msg_content: msg_embeddings}, per_msg_meta)
|
||||
|
||||
returned_memories.append(
|
||||
{
|
||||
@@ -515,15 +516,15 @@ class Memory(MemoryBase):
|
||||
)
|
||||
|
||||
try:
|
||||
response = remove_code_blocks(response)
|
||||
if not response.strip():
|
||||
cleaned_response = remove_code_blocks(response)
|
||||
if not cleaned_response.strip():
|
||||
new_retrieved_facts = []
|
||||
else:
|
||||
try:
|
||||
# First try direct JSON parsing
|
||||
new_retrieved_facts = json.loads(response, strict=False)["facts"]
|
||||
new_retrieved_facts = json.loads(cleaned_response, strict=False)["facts"]
|
||||
except json.JSONDecodeError:
|
||||
# Try extracting JSON from response using built-in function
|
||||
# Try extracting JSON from response (handles chatty LLM output)
|
||||
extracted_json = extract_json(response)
|
||||
new_retrieved_facts = json.loads(extracted_json, strict=False)["facts"]
|
||||
new_retrieved_facts = normalize_facts(new_retrieved_facts)
|
||||
@@ -588,8 +589,11 @@ class Memory(MemoryBase):
|
||||
logger.warning("Empty response from LLM, no memories to extract")
|
||||
new_memories_with_actions = {}
|
||||
else:
|
||||
response = remove_code_blocks(response)
|
||||
new_memories_with_actions = json.loads(response, strict=False)
|
||||
try:
|
||||
new_memories_with_actions = json.loads(remove_code_blocks(response), strict=False)
|
||||
except json.JSONDecodeError:
|
||||
extracted_json = extract_json(response)
|
||||
new_memories_with_actions = json.loads(extracted_json, strict=False)
|
||||
except Exception as e:
|
||||
logger.error(f"Invalid JSON response: {e}")
|
||||
new_memories_with_actions = {}
|
||||
@@ -608,6 +612,9 @@ class Memory(MemoryBase):
|
||||
|
||||
event_type = resp.get("event")
|
||||
if event_type == "ADD":
|
||||
# Ensure action_text has an embedding cached to avoid redundant API calls
|
||||
if action_text not in new_message_embeddings:
|
||||
new_message_embeddings[action_text] = self.embedding_model.embed(action_text, "add")
|
||||
memory_id = self._create_memory(
|
||||
data=action_text,
|
||||
existing_embeddings=new_message_embeddings,
|
||||
@@ -615,6 +622,9 @@ class Memory(MemoryBase):
|
||||
)
|
||||
returned_memories.append({"id": memory_id, "memory": action_text, "event": event_type})
|
||||
elif event_type == "UPDATE":
|
||||
# Ensure action_text has an embedding cached to avoid redundant API calls
|
||||
if action_text not in new_message_embeddings:
|
||||
new_message_embeddings[action_text] = self.embedding_model.embed(action_text, "update")
|
||||
self._update_memory(
|
||||
memory_id=temp_uuid_mapping[resp.get("id")],
|
||||
data=action_text,
|
||||
@@ -967,11 +977,11 @@ class Memory(MemoryBase):
|
||||
}
|
||||
|
||||
if operator in operator_map:
|
||||
result[key] = {operator_map[operator]: value}
|
||||
result.setdefault(key, {})[operator_map[operator]] = value
|
||||
else:
|
||||
raise ValueError(f"Unsupported metadata filter operator: {operator}")
|
||||
return result
|
||||
|
||||
|
||||
for key, value in metadata_filters.items():
|
||||
if key == "AND":
|
||||
# Logical AND: combine multiple conditions
|
||||
@@ -1071,13 +1081,15 @@ class Memory(MemoryBase):
|
||||
|
||||
return original_memories
|
||||
|
||||
def update(self, memory_id, data):
|
||||
def update(self, memory_id, data, metadata: Optional[Dict[str, Any]] = None):
|
||||
"""
|
||||
Update a memory by ID.
|
||||
|
||||
Args:
|
||||
memory_id (str): ID of the memory to update.
|
||||
data (str): New content to update the memory with.
|
||||
metadata (dict, optional): Additional metadata to update. Existing metadata fields
|
||||
not specified here will be preserved. Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict: Success message indicating the memory was updated.
|
||||
@@ -1085,12 +1097,14 @@ class Memory(MemoryBase):
|
||||
Example:
|
||||
>>> m.update(memory_id="mem_123", data="Likes to play tennis on weekends")
|
||||
{'message': 'Memory updated successfully!'}
|
||||
>>> m.update(memory_id="mem_123", data="Likes tennis", metadata={"category": "sports"})
|
||||
{'message': 'Memory updated successfully!'}
|
||||
"""
|
||||
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "sync"})
|
||||
|
||||
existing_embeddings = {data: self.embedding_model.embed(data, "update")}
|
||||
|
||||
self._update_memory(memory_id, data, existing_embeddings)
|
||||
self._update_memory(memory_id, data, existing_embeddings, metadata)
|
||||
return {"message": "Memory updated successfully!"}
|
||||
|
||||
def delete(self, memory_id):
|
||||
@@ -1101,7 +1115,27 @@ class Memory(MemoryBase):
|
||||
memory_id (str): ID of the memory to delete.
|
||||
"""
|
||||
capture_event("mem0.delete", self, {"memory_id": memory_id, "sync_type": "sync"})
|
||||
self._delete_memory(memory_id)
|
||||
|
||||
existing_memory = self.vector_store.get(vector_id=memory_id)
|
||||
if existing_memory is None:
|
||||
raise ValueError(f"Memory with id {memory_id} not found")
|
||||
|
||||
# Clean up graph entities before deleting from vector store
|
||||
if self.enable_graph:
|
||||
try:
|
||||
memory_text = existing_memory.payload.get("data", "")
|
||||
if memory_text:
|
||||
filters = {}
|
||||
for key in ("user_id", "agent_id", "run_id"):
|
||||
val = existing_memory.payload.get(key)
|
||||
if val:
|
||||
filters[key] = val
|
||||
if filters.get("user_id"):
|
||||
self.graph.delete(memory_text, filters)
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up graph for memory {memory_id}: {e}")
|
||||
|
||||
self._delete_memory(memory_id, existing_memory)
|
||||
return {"message": "Memory deleted successfully!"}
|
||||
|
||||
def delete_all(self, user_id: Optional[str] = None, agent_id: Optional[str] = None, run_id: Optional[str] = None):
|
||||
@@ -1153,31 +1187,34 @@ class Memory(MemoryBase):
|
||||
capture_event("mem0.history", self, {"memory_id": memory_id, "sync_type": "sync"})
|
||||
return self.db.get_history(memory_id)
|
||||
|
||||
def _create_memory(self, data, existing_embeddings, metadata=None):
|
||||
def _create_memory(self, data: str, existing_embeddings: Union[Dict[str, List[float]], List[float]], metadata=None):
|
||||
logger.debug(f"Creating memory with {data=}")
|
||||
if data in existing_embeddings:
|
||||
# existing_embeddings may be a dict (preferred) or a precomputed vector
|
||||
if isinstance(existing_embeddings, dict) and data in existing_embeddings:
|
||||
embeddings = existing_embeddings[data]
|
||||
elif not isinstance(existing_embeddings, dict):
|
||||
embeddings = existing_embeddings
|
||||
else:
|
||||
embeddings = self.embedding_model.embed(data, memory_action="add")
|
||||
memory_id = str(uuid.uuid4())
|
||||
metadata = metadata or {}
|
||||
metadata["data"] = data
|
||||
metadata["hash"] = hashlib.md5(data.encode()).hexdigest()
|
||||
metadata["created_at"] = datetime.now(timezone.utc).isoformat()
|
||||
new_metadata = deepcopy(metadata) if metadata is not None else {}
|
||||
new_metadata["data"] = data
|
||||
new_metadata["hash"] = hashlib.md5(data.encode()).hexdigest()
|
||||
new_metadata["created_at"] = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
self.vector_store.insert(
|
||||
vectors=[embeddings],
|
||||
ids=[memory_id],
|
||||
payloads=[metadata],
|
||||
payloads=[new_metadata],
|
||||
)
|
||||
self.db.add_history(
|
||||
memory_id,
|
||||
None,
|
||||
data,
|
||||
"ADD",
|
||||
created_at=metadata.get("created_at"),
|
||||
actor_id=metadata.get("actor_id"),
|
||||
role=metadata.get("role"),
|
||||
created_at=new_metadata.get("created_at"),
|
||||
actor_id=new_metadata.get("actor_id"),
|
||||
role=new_metadata.get("role"),
|
||||
)
|
||||
return memory_id
|
||||
|
||||
@@ -1211,16 +1248,17 @@ class Memory(MemoryBase):
|
||||
if metadata is None:
|
||||
raise ValueError("Metadata cannot be done for procedural memory.")
|
||||
|
||||
metadata["memory_type"] = MemoryType.PROCEDURAL.value
|
||||
new_metadata = deepcopy(metadata)
|
||||
new_metadata["memory_type"] = MemoryType.PROCEDURAL.value
|
||||
embeddings = self.embedding_model.embed(procedural_memory, memory_action="add")
|
||||
memory_id = self._create_memory(procedural_memory, {procedural_memory: embeddings}, metadata=metadata)
|
||||
memory_id = self._create_memory(procedural_memory, {procedural_memory: embeddings}, metadata=new_metadata)
|
||||
capture_event("mem0._create_procedural_memory", self, {"memory_id": memory_id, "sync_type": "sync"})
|
||||
|
||||
result = {"results": [{"id": memory_id, "memory": procedural_memory, "event": "ADD"}]}
|
||||
|
||||
return result
|
||||
|
||||
def _update_memory(self, memory_id, data, existing_embeddings, metadata=None):
|
||||
def _update_memory(self, memory_id, data: str, existing_embeddings: Union[Dict[str, List[float]], List[float]], metadata=None):
|
||||
logger.info(f"Updating memory with {data=}")
|
||||
|
||||
try:
|
||||
@@ -1253,8 +1291,10 @@ class Memory(MemoryBase):
|
||||
if "role" not in new_metadata and "role" in existing_memory.payload:
|
||||
new_metadata["role"] = existing_memory.payload["role"]
|
||||
|
||||
if data in existing_embeddings:
|
||||
if isinstance(existing_embeddings, dict) and data in existing_embeddings:
|
||||
embeddings = existing_embeddings[data]
|
||||
elif not isinstance(existing_embeddings, dict):
|
||||
embeddings = existing_embeddings
|
||||
else:
|
||||
embeddings = self.embedding_model.embed(data, "update")
|
||||
|
||||
@@ -1277,18 +1317,26 @@ class Memory(MemoryBase):
|
||||
)
|
||||
return memory_id
|
||||
|
||||
def _delete_memory(self, memory_id):
|
||||
def _delete_memory(self, memory_id, existing_memory=None):
|
||||
logger.info(f"Deleting memory with {memory_id=}")
|
||||
existing_memory = self.vector_store.get(vector_id=memory_id)
|
||||
if existing_memory is None:
|
||||
raise ValueError(f"Memory with id {memory_id} not found")
|
||||
existing_memory = self.vector_store.get(vector_id=memory_id)
|
||||
if existing_memory is None:
|
||||
raise ValueError(f"Memory with id {memory_id} not found")
|
||||
prev_value = existing_memory.payload.get("data", "")
|
||||
|
||||
# Preserve original created_at and record deletion time
|
||||
created_at = _normalize_iso_timestamp_to_utc(existing_memory.payload.get("created_at"))
|
||||
updated_at = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
self.vector_store.delete(vector_id=memory_id)
|
||||
self.db.add_history(
|
||||
memory_id,
|
||||
prev_value,
|
||||
None,
|
||||
"DELETE",
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
actor_id=existing_memory.payload.get("actor_id"),
|
||||
role=existing_memory.payload.get("role"),
|
||||
is_deleted=1,
|
||||
@@ -1523,7 +1571,8 @@ class AsyncMemory(MemoryBase):
|
||||
|
||||
msg_content = message_dict["content"]
|
||||
msg_embeddings = await asyncio.to_thread(self.embedding_model.embed, msg_content, "add")
|
||||
mem_id = await self._create_memory(msg_content, msg_embeddings, per_msg_meta)
|
||||
# Pass embeddings as a dict so _create_memory can reuse the cached embedding
|
||||
mem_id = await self._create_memory(msg_content, {msg_content: msg_embeddings}, per_msg_meta)
|
||||
|
||||
returned_memories.append(
|
||||
{
|
||||
@@ -1555,15 +1604,15 @@ class AsyncMemory(MemoryBase):
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
try:
|
||||
response = remove_code_blocks(response)
|
||||
if not response.strip():
|
||||
cleaned_response = remove_code_blocks(response)
|
||||
if not cleaned_response.strip():
|
||||
new_retrieved_facts = []
|
||||
else:
|
||||
try:
|
||||
# First try direct JSON parsing
|
||||
new_retrieved_facts = json.loads(response, strict=False)["facts"]
|
||||
new_retrieved_facts = json.loads(cleaned_response, strict=False)["facts"]
|
||||
except json.JSONDecodeError:
|
||||
# Try extracting JSON from response using built-in function
|
||||
# Try extracting JSON from response (handles chatty LLM output)
|
||||
extracted_json = extract_json(response)
|
||||
new_retrieved_facts = json.loads(extracted_json, strict=False)["facts"]
|
||||
new_retrieved_facts = normalize_facts(new_retrieved_facts)
|
||||
@@ -1631,8 +1680,11 @@ class AsyncMemory(MemoryBase):
|
||||
logger.warning("Empty response from LLM, no memories to extract")
|
||||
new_memories_with_actions = {}
|
||||
else:
|
||||
response = remove_code_blocks(response)
|
||||
new_memories_with_actions = json.loads(response, strict=False)
|
||||
try:
|
||||
new_memories_with_actions = json.loads(remove_code_blocks(response), strict=False)
|
||||
except json.JSONDecodeError:
|
||||
extracted_json = extract_json(response)
|
||||
new_memories_with_actions = json.loads(extracted_json, strict=False)
|
||||
except Exception as e:
|
||||
logger.error(f"Invalid JSON response: {e}")
|
||||
new_memories_with_actions = {}
|
||||
@@ -1651,6 +1703,11 @@ class AsyncMemory(MemoryBase):
|
||||
event_type = resp.get("event")
|
||||
|
||||
if event_type == "ADD":
|
||||
# Ensure action_text has an embedding cached to avoid redundant API calls
|
||||
if action_text not in new_message_embeddings:
|
||||
new_message_embeddings[action_text] = await asyncio.to_thread(
|
||||
self.embedding_model.embed, action_text, "add"
|
||||
)
|
||||
task = asyncio.create_task(
|
||||
self._create_memory(
|
||||
data=action_text,
|
||||
@@ -1660,6 +1717,11 @@ class AsyncMemory(MemoryBase):
|
||||
)
|
||||
memory_tasks.append((task, resp, "ADD", None))
|
||||
elif event_type == "UPDATE":
|
||||
# Ensure action_text has an embedding cached to avoid redundant API calls
|
||||
if action_text not in new_message_embeddings:
|
||||
new_message_embeddings[action_text] = await asyncio.to_thread(
|
||||
self.embedding_model.embed, action_text, "update"
|
||||
)
|
||||
task = asyncio.create_task(
|
||||
self._update_memory(
|
||||
memory_id=temp_uuid_mapping[resp["id"]],
|
||||
@@ -2037,7 +2099,7 @@ class AsyncMemory(MemoryBase):
|
||||
}
|
||||
|
||||
if operator in operator_map:
|
||||
result[key] = {operator_map[operator]: value}
|
||||
result.setdefault(key, {})[operator_map[operator]] = value
|
||||
else:
|
||||
raise ValueError(f"Unsupported metadata filter operator: {operator}")
|
||||
return result
|
||||
@@ -2143,13 +2205,15 @@ class AsyncMemory(MemoryBase):
|
||||
|
||||
return original_memories
|
||||
|
||||
async def update(self, memory_id, data):
|
||||
async def update(self, memory_id, data, metadata: Optional[Dict[str, Any]] = None):
|
||||
"""
|
||||
Update a memory by ID asynchronously.
|
||||
|
||||
Args:
|
||||
memory_id (str): ID of the memory to update.
|
||||
data (str): New content to update the memory with.
|
||||
metadata (dict, optional): Additional metadata to update. Existing metadata fields
|
||||
not specified here will be preserved. Defaults to None.
|
||||
|
||||
Returns:
|
||||
dict: Success message indicating the memory was updated.
|
||||
@@ -2157,13 +2221,15 @@ class AsyncMemory(MemoryBase):
|
||||
Example:
|
||||
>>> await m.update(memory_id="mem_123", data="Likes to play tennis on weekends")
|
||||
{'message': 'Memory updated successfully!'}
|
||||
>>> await m.update(memory_id="mem_123", data="Likes tennis", metadata={"category": "sports"})
|
||||
{'message': 'Memory updated successfully!'}
|
||||
"""
|
||||
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "async"})
|
||||
|
||||
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update")
|
||||
existing_embeddings = {data: embeddings}
|
||||
|
||||
await self._update_memory(memory_id, data, existing_embeddings)
|
||||
await self._update_memory(memory_id, data, existing_embeddings, metadata)
|
||||
return {"message": "Memory updated successfully!"}
|
||||
|
||||
async def delete(self, memory_id):
|
||||
@@ -2174,7 +2240,27 @@ class AsyncMemory(MemoryBase):
|
||||
memory_id (str): ID of the memory to delete.
|
||||
"""
|
||||
capture_event("mem0.delete", self, {"memory_id": memory_id, "sync_type": "async"})
|
||||
await self._delete_memory(memory_id)
|
||||
|
||||
existing_memory = await asyncio.to_thread(self.vector_store.get, vector_id=memory_id)
|
||||
if existing_memory is None:
|
||||
raise ValueError(f"Memory with id {memory_id} not found")
|
||||
|
||||
# Clean up graph entities before deleting from vector store
|
||||
if self.enable_graph:
|
||||
try:
|
||||
memory_text = existing_memory.payload.get("data", "")
|
||||
if memory_text:
|
||||
filters = {}
|
||||
for key in ("user_id", "agent_id", "run_id"):
|
||||
val = existing_memory.payload.get(key)
|
||||
if val:
|
||||
filters[key] = val
|
||||
if filters.get("user_id"):
|
||||
await asyncio.to_thread(self.graph.delete, memory_text, filters)
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up graph for memory {memory_id}: {e}")
|
||||
|
||||
await self._delete_memory(memory_id, existing_memory)
|
||||
return {"message": "Memory deleted successfully!"}
|
||||
|
||||
async def delete_all(self, user_id=None, agent_id=None, run_id=None):
|
||||
@@ -2229,24 +2315,27 @@ class AsyncMemory(MemoryBase):
|
||||
capture_event("mem0.history", self, {"memory_id": memory_id, "sync_type": "async"})
|
||||
return await asyncio.to_thread(self.db.get_history, memory_id)
|
||||
|
||||
async def _create_memory(self, data, existing_embeddings, metadata=None):
|
||||
async def _create_memory(self, data: str, existing_embeddings: Union[Dict[str, List[float]], List[float]], metadata=None):
|
||||
logger.debug(f"Creating memory with {data=}")
|
||||
if data in existing_embeddings:
|
||||
# existing_embeddings may be a dict (preferred) or a precomputed vector
|
||||
if isinstance(existing_embeddings, dict) and data in existing_embeddings:
|
||||
embeddings = existing_embeddings[data]
|
||||
elif not isinstance(existing_embeddings, dict):
|
||||
embeddings = existing_embeddings
|
||||
else:
|
||||
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, memory_action="add")
|
||||
|
||||
memory_id = str(uuid.uuid4())
|
||||
metadata = metadata or {}
|
||||
metadata["data"] = data
|
||||
metadata["hash"] = hashlib.md5(data.encode()).hexdigest()
|
||||
metadata["created_at"] = datetime.now(timezone.utc).isoformat()
|
||||
new_metadata = deepcopy(metadata) if metadata is not None else {}
|
||||
new_metadata["data"] = data
|
||||
new_metadata["hash"] = hashlib.md5(data.encode()).hexdigest()
|
||||
new_metadata["created_at"] = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
await asyncio.to_thread(
|
||||
self.vector_store.insert,
|
||||
vectors=[embeddings],
|
||||
ids=[memory_id],
|
||||
payloads=[metadata],
|
||||
payloads=[new_metadata],
|
||||
)
|
||||
|
||||
await asyncio.to_thread(
|
||||
@@ -2255,9 +2344,9 @@ class AsyncMemory(MemoryBase):
|
||||
None,
|
||||
data,
|
||||
"ADD",
|
||||
created_at=metadata.get("created_at"),
|
||||
actor_id=metadata.get("actor_id"),
|
||||
role=metadata.get("role"),
|
||||
created_at=new_metadata.get("created_at"),
|
||||
actor_id=new_metadata.get("actor_id"),
|
||||
role=new_metadata.get("role"),
|
||||
)
|
||||
|
||||
return memory_id
|
||||
@@ -2306,16 +2395,17 @@ class AsyncMemory(MemoryBase):
|
||||
if metadata is None:
|
||||
raise ValueError("Metadata cannot be done for procedural memory.")
|
||||
|
||||
metadata["memory_type"] = MemoryType.PROCEDURAL.value
|
||||
new_metadata = deepcopy(metadata)
|
||||
new_metadata["memory_type"] = MemoryType.PROCEDURAL.value
|
||||
embeddings = await asyncio.to_thread(self.embedding_model.embed, procedural_memory, memory_action="add")
|
||||
memory_id = await self._create_memory(procedural_memory, {procedural_memory: embeddings}, metadata=metadata)
|
||||
memory_id = await self._create_memory(procedural_memory, {procedural_memory: embeddings}, metadata=new_metadata)
|
||||
capture_event("mem0._create_procedural_memory", self, {"memory_id": memory_id, "sync_type": "async"})
|
||||
|
||||
result = {"results": [{"id": memory_id, "memory": procedural_memory, "event": "ADD"}]}
|
||||
|
||||
return result
|
||||
|
||||
async def _update_memory(self, memory_id, data, existing_embeddings, metadata=None):
|
||||
async def _update_memory(self, memory_id, data: str, existing_embeddings: Union[Dict[str, List[float]], List[float]], metadata=None):
|
||||
logger.info(f"Updating memory with {data=}")
|
||||
|
||||
try:
|
||||
@@ -2349,8 +2439,10 @@ class AsyncMemory(MemoryBase):
|
||||
if "role" not in new_metadata and "role" in existing_memory.payload:
|
||||
new_metadata["role"] = existing_memory.payload["role"]
|
||||
|
||||
if data in existing_embeddings:
|
||||
if isinstance(existing_embeddings, dict) and data in existing_embeddings:
|
||||
embeddings = existing_embeddings[data]
|
||||
elif not isinstance(existing_embeddings, dict):
|
||||
embeddings = existing_embeddings
|
||||
else:
|
||||
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update")
|
||||
|
||||
@@ -2375,13 +2467,18 @@ class AsyncMemory(MemoryBase):
|
||||
)
|
||||
return memory_id
|
||||
|
||||
async def _delete_memory(self, memory_id):
|
||||
async def _delete_memory(self, memory_id, existing_memory=None):
|
||||
logger.info(f"Deleting memory with {memory_id=}")
|
||||
existing_memory = await asyncio.to_thread(self.vector_store.get, vector_id=memory_id)
|
||||
if existing_memory is None:
|
||||
raise ValueError(f"Memory with id {memory_id} not found")
|
||||
existing_memory = await asyncio.to_thread(self.vector_store.get, vector_id=memory_id)
|
||||
if existing_memory is None:
|
||||
raise ValueError(f"Memory with id {memory_id} not found")
|
||||
prev_value = existing_memory.payload.get("data", "")
|
||||
|
||||
# Preserve original created_at and record deletion time
|
||||
created_at = _normalize_iso_timestamp_to_utc(existing_memory.payload.get("created_at"))
|
||||
updated_at = datetime.now(timezone.utc).isoformat()
|
||||
|
||||
await asyncio.to_thread(self.vector_store.delete, vector_id=memory_id)
|
||||
await asyncio.to_thread(
|
||||
self.db.add_history,
|
||||
@@ -2389,6 +2486,8 @@ class AsyncMemory(MemoryBase):
|
||||
prev_value,
|
||||
None,
|
||||
"DELETE",
|
||||
created_at=created_at,
|
||||
updated_at=updated_at,
|
||||
actor_id=existing_memory.payload.get("actor_id"),
|
||||
role=existing_memory.payload.get("role"),
|
||||
is_deleted=1,
|
||||
|
||||
@@ -134,6 +134,28 @@ class MemoryGraph:
|
||||
|
||||
return search_results
|
||||
|
||||
def delete(self, data, filters):
|
||||
"""
|
||||
Delete graph entities associated with the given memory text.
|
||||
|
||||
Extracts entities and relationships from the memory text using the same
|
||||
pipeline as add(), then deletes the matching relationships in the graph.
|
||||
|
||||
Args:
|
||||
data (str): The memory text whose graph entities should be removed.
|
||||
filters (dict): Scope filters (user_id, agent_id).
|
||||
"""
|
||||
try:
|
||||
entity_type_map = self._retrieve_nodes_from_data(data, filters)
|
||||
if not entity_type_map:
|
||||
logger.debug("No entities found in memory text, skipping graph cleanup")
|
||||
return
|
||||
to_be_deleted = self._establish_nodes_relations_from_data(data, filters, entity_type_map)
|
||||
if to_be_deleted:
|
||||
self._delete_entities(to_be_deleted, filters)
|
||||
except Exception as e:
|
||||
logger.error(f"Error during graph cleanup for memory delete: {e}")
|
||||
|
||||
def delete_all(self, filters):
|
||||
"""Delete all nodes and relationships for a user or specific agent."""
|
||||
if filters.get("agent_id"):
|
||||
|
||||
@@ -124,14 +124,20 @@ def remove_code_blocks(content: str) -> str:
|
||||
def extract_json(text):
|
||||
"""
|
||||
Extracts JSON content from a string, removing enclosing triple backticks and optional 'json' tag if present.
|
||||
If no code block is found, returns the text as-is.
|
||||
If no code block is found, attempts to locate JSON by finding the first '{' and last '}'.
|
||||
If that also fails, returns the text as-is.
|
||||
"""
|
||||
text = text.strip()
|
||||
match = re.search(r"```(?:json)?\s*(.*?)\s*```", text, re.DOTALL)
|
||||
if match:
|
||||
json_str = match.group(1)
|
||||
else:
|
||||
json_str = text # assume it's raw JSON
|
||||
start_idx = text.find("{")
|
||||
end_idx = text.rfind("}")
|
||||
if start_idx != -1 and end_idx != -1 and end_idx > start_idx:
|
||||
json_str = text[start_idx : end_idx + 1]
|
||||
else:
|
||||
json_str = text
|
||||
return json_str
|
||||
|
||||
|
||||
@@ -249,6 +255,7 @@ def sanitize_relationship_for_cypher(relationship) -> str:
|
||||
"}": "_rbrace_",
|
||||
"<": "_langle_",
|
||||
">": "_rangle_",
|
||||
"-": "_",
|
||||
}
|
||||
|
||||
# Apply replacements and clean up
|
||||
|
||||
@@ -3,6 +3,7 @@ from typing import Dict, Optional, Union
|
||||
|
||||
from mem0.configs.embeddings.base import BaseEmbedderConfig
|
||||
from mem0.configs.llms.anthropic import AnthropicConfig
|
||||
from mem0.configs.llms.aws_bedrock import AWSBedrockConfig
|
||||
from mem0.configs.llms.azure import AzureOpenAIConfig
|
||||
from mem0.configs.llms.base import BaseLlmConfig
|
||||
from mem0.configs.llms.deepseek import DeepSeekConfig
|
||||
@@ -38,7 +39,7 @@ class LlmFactory:
|
||||
"openai": ("mem0.llms.openai.OpenAILLM", OpenAIConfig),
|
||||
"groq": ("mem0.llms.groq.GroqLLM", BaseLlmConfig),
|
||||
"together": ("mem0.llms.together.TogetherLLM", BaseLlmConfig),
|
||||
"aws_bedrock": ("mem0.llms.aws_bedrock.AWSBedrockLLM", BaseLlmConfig),
|
||||
"aws_bedrock": ("mem0.llms.aws_bedrock.AWSBedrockLLM", AWSBedrockConfig),
|
||||
"litellm": ("mem0.llms.litellm.LiteLLM", BaseLlmConfig),
|
||||
"azure_openai": ("mem0.llms.azure_openai.AzureOpenAILLM", AzureOpenAIConfig),
|
||||
"openai_structured": ("mem0.llms.openai_structured.OpenAIStructuredLLM", OpenAIConfig),
|
||||
@@ -188,6 +189,7 @@ class VectorStoreFactory:
|
||||
"baidu": "mem0.vector_stores.baidu.BaiduDB",
|
||||
"cassandra": "mem0.vector_stores.cassandra.CassandraDB",
|
||||
"neptune": "mem0.vector_stores.neptune_analytics.NeptuneAnalyticsVector",
|
||||
"turbopuffer": "mem0.vector_stores.turbopuffer.TurbopufferDB",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -34,6 +34,7 @@ class VectorStoreConfig(BaseModel):
|
||||
"faiss": "FAISSConfig",
|
||||
"langchain": "LangchainConfig",
|
||||
"s3_vectors": "S3VectorsConfig",
|
||||
"turbopuffer": "TurbopufferConfig",
|
||||
}
|
||||
|
||||
@model_validator(mode="after")
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, date
|
||||
from databricks.sdk.service.catalog import ColumnInfo, ColumnTypeName, TableType, DataSourceFormat
|
||||
from databricks.sdk.service.catalog import TableConstraint, PrimaryKeyConstraint
|
||||
from databricks.sdk import WorkspaceClient
|
||||
from databricks.sdk.service.sql import StatementParameterListItem
|
||||
from databricks.sdk.service.vectorsearch import (
|
||||
VectorIndexType,
|
||||
DeltaSyncVectorIndexSpecRequest,
|
||||
@@ -28,6 +30,9 @@ class MemoryResult(BaseModel):
|
||||
|
||||
excluded_keys = {"user_id", "agent_id", "run_id", "hash", "data", "created_at", "updated_at"}
|
||||
|
||||
# Pattern for valid SQL identifiers to prevent column name injection
|
||||
_VALID_SQL_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
||||
|
||||
|
||||
class Databricks(VectorStoreBase):
|
||||
def __init__(
|
||||
@@ -65,7 +70,7 @@ class Databricks(VectorStoreBase):
|
||||
catalog (str): Unity Catalog catalog name.
|
||||
schema (str): Unity Catalog schema name.
|
||||
table_name (str): Source Delta table name.
|
||||
index_name (str, optional): Vector search index name (default: "mem0").
|
||||
collection_name (str, optional): Vector search index name (default: "mem0").
|
||||
index_type (str, optional): Index type, either "DELTA_SYNC" or "DIRECT_ACCESS" (default: "DELTA_SYNC").
|
||||
embedding_model_endpoint_name (str, optional): Embedding model endpoint for Databricks-computed embeddings.
|
||||
embedding_dimension (int, optional): Vector embedding dimensions (default: 1536).
|
||||
@@ -85,7 +90,7 @@ class Databricks(VectorStoreBase):
|
||||
self.fully_qualified_index_name = f"{self.catalog}.{self.schema}.{self.index_name}"
|
||||
|
||||
# Configuration
|
||||
self.index_type = index_type
|
||||
self.index_type = VectorIndexType(index_type) if isinstance(index_type, str) else index_type
|
||||
self.embedding_model_endpoint_name = embedding_model_endpoint_name
|
||||
self.embedding_dimension = embedding_dimension
|
||||
self.endpoint_type = endpoint_type
|
||||
@@ -261,11 +266,11 @@ class Databricks(VectorStoreBase):
|
||||
)
|
||||
logger.info(f"Successfully created source table '{self.fully_qualified_table_name}'")
|
||||
self.client.table_constraints.create(
|
||||
full_name_arg="logistics_dev.ai.dev_memory",
|
||||
full_name_arg=self.fully_qualified_table_name,
|
||||
constraint=TableConstraint(
|
||||
primary_key_constraint=PrimaryKeyConstraint(
|
||||
name="pk_dev_memory", # Name of the primary key constraint
|
||||
child_columns=["memory_id"], # Columns that make up the primary key
|
||||
name=f"pk_{self.table_name}",
|
||||
child_columns=["memory_id"],
|
||||
)
|
||||
),
|
||||
)
|
||||
@@ -388,28 +393,46 @@ class Databricks(VectorStoreBase):
|
||||
# Determine the number of items to process
|
||||
num_items = len(payloads) if payloads else len(vectors) if vectors else 0
|
||||
|
||||
params = []
|
||||
value_tuples = []
|
||||
for i in range(num_items):
|
||||
values = []
|
||||
placeholders = []
|
||||
for col in self.columns:
|
||||
param_name = f"{col.name}_{i}"
|
||||
if col.name == "memory_id":
|
||||
val = ids[i] if ids and i < len(ids) else str(uuid.uuid4())
|
||||
elif col.name == "embedding":
|
||||
val = vectors[i] if vectors and i < len(vectors) else []
|
||||
# Vectors are numeric arrays — ARRAY type not supported by StatementParameterListItem,
|
||||
# so we inline using _format_sql_value (values are floats from the embedding model).
|
||||
placeholders.append(self._format_sql_value(val))
|
||||
continue
|
||||
elif col.name == "memory":
|
||||
val = payloads[i].get("data") if payloads and i < len(payloads) else None
|
||||
else:
|
||||
val = payloads[i].get(col.name) if payloads and i < len(payloads) else None
|
||||
values.append(val)
|
||||
formatted = [self._format_sql_value(v) for v in values]
|
||||
value_tuples.append(f"({', '.join(formatted)})")
|
||||
|
||||
if val is None:
|
||||
placeholders.append("NULL")
|
||||
else:
|
||||
placeholders.append(f":{param_name}")
|
||||
if isinstance(val, dict):
|
||||
val = json.dumps(val)
|
||||
# Use explicit type for TIMESTAMP columns so Databricks doesn't
|
||||
# rely on implicit STRING→TIMESTAMP casting.
|
||||
param_type = "TIMESTAMP" if col.type_name == ColumnTypeName.TIMESTAMP else None
|
||||
params.append(StatementParameterListItem(name=param_name, value=str(val), type=param_type))
|
||||
value_tuples.append(f"({', '.join(placeholders)})")
|
||||
|
||||
insert_sql = f"INSERT INTO {self.fully_qualified_table_name} ({', '.join(self.column_names)}) VALUES {', '.join(value_tuples)}"
|
||||
|
||||
# Execute the insert
|
||||
try:
|
||||
response = self.client.statement_execution.execute_statement(
|
||||
statement=insert_sql, warehouse_id=self.warehouse_id, wait_timeout="30s"
|
||||
statement=insert_sql,
|
||||
warehouse_id=self.warehouse_id,
|
||||
wait_timeout="30s",
|
||||
parameters=params,
|
||||
)
|
||||
if response.status.state.value == "SUCCEEDED":
|
||||
logger.info(
|
||||
@@ -439,29 +462,29 @@ class Databricks(VectorStoreBase):
|
||||
try:
|
||||
filters_json = json.dumps(filters) if filters else None
|
||||
|
||||
# Choose query type
|
||||
if self.index_type == VectorIndexType.DELTA_SYNC and query:
|
||||
# Text-based search
|
||||
sdk_results = self.client.vector_search_indexes.query_index(
|
||||
index_name=self.fully_qualified_index_name,
|
||||
columns=self.column_names,
|
||||
query_text=query,
|
||||
num_results=limit,
|
||||
query_type=self.query_type,
|
||||
filters_json=filters_json,
|
||||
)
|
||||
elif self.index_type == VectorIndexType.DIRECT_ACCESS and vectors:
|
||||
# Vector-based search
|
||||
sdk_results = self.client.vector_search_indexes.query_index(
|
||||
index_name=self.fully_qualified_index_name,
|
||||
columns=self.column_names,
|
||||
query_vector=vectors,
|
||||
num_results=limit,
|
||||
query_type=self.query_type,
|
||||
filters_json=filters_json,
|
||||
)
|
||||
# Choose query mode per Databricks SDK contract:
|
||||
# - query_text: for Delta Sync Index with model endpoint
|
||||
# - query_vector: for Direct Access Index and Delta Sync Index with self-managed vectors
|
||||
query_kwargs = {
|
||||
"index_name": self.fully_qualified_index_name,
|
||||
"columns": self.column_names,
|
||||
"num_results": limit,
|
||||
"query_type": self.query_type,
|
||||
"filters_json": filters_json,
|
||||
}
|
||||
uses_model_endpoint = (
|
||||
self.index_type == VectorIndexType.DELTA_SYNC and self.embedding_model_endpoint_name
|
||||
)
|
||||
if uses_model_endpoint:
|
||||
if not query:
|
||||
raise ValueError("Query text is required for Delta Sync Index with model endpoint.")
|
||||
query_kwargs["query_text"] = query
|
||||
elif vectors:
|
||||
query_kwargs["query_vector"] = vectors
|
||||
else:
|
||||
raise ValueError("Must provide query text for DELTA_SYNC or vectors for DIRECT_ACCESS.")
|
||||
raise ValueError("Must provide vectors for search.")
|
||||
|
||||
sdk_results = self.client.vector_search_indexes.query_index(**query_kwargs)
|
||||
|
||||
# Parse results
|
||||
result_data = sdk_results.result if hasattr(sdk_results, "result") else sdk_results
|
||||
@@ -494,10 +517,13 @@ class Databricks(VectorStoreBase):
|
||||
try:
|
||||
logger.info(f"Deleting vector with ID {vector_id} from Delta table {self.fully_qualified_table_name}")
|
||||
|
||||
delete_sql = f"DELETE FROM {self.fully_qualified_table_name} WHERE memory_id = '{vector_id}'"
|
||||
delete_sql = f"DELETE FROM {self.fully_qualified_table_name} WHERE memory_id = :vector_id"
|
||||
|
||||
response = self.client.statement_execution.execute_statement(
|
||||
statement=delete_sql, warehouse_id=self.warehouse_id, wait_timeout="30s"
|
||||
statement=delete_sql,
|
||||
warehouse_id=self.warehouse_id,
|
||||
wait_timeout="30s",
|
||||
parameters=[StatementParameterListItem(name="vector_id", value=str(vector_id))],
|
||||
)
|
||||
|
||||
if response.status.state.value == "SUCCEEDED":
|
||||
@@ -519,8 +545,8 @@ class Databricks(VectorStoreBase):
|
||||
payload (dict, optional): New payload data.
|
||||
"""
|
||||
|
||||
update_sql = f"UPDATE {self.fully_qualified_table_name} SET "
|
||||
set_clauses = []
|
||||
params = []
|
||||
if not vector_id:
|
||||
logger.error("vector_id is required for update operation")
|
||||
return
|
||||
@@ -528,25 +554,38 @@ class Databricks(VectorStoreBase):
|
||||
if not isinstance(vector, list):
|
||||
logger.error("vector must be a list of float values")
|
||||
return
|
||||
set_clauses.append(f"embedding = {vector}")
|
||||
# Vectors are numeric arrays — safe to inline since StatementParameterListItem
|
||||
# doesn't support ARRAY types, and values are validated as list of floats above.
|
||||
# Use array() SQL syntax, not Python list repr which is invalid Databricks SQL.
|
||||
set_clauses.append(f"embedding = {self._format_sql_value(vector)}")
|
||||
if payload:
|
||||
if not isinstance(payload, dict):
|
||||
logger.error("payload must be a dictionary")
|
||||
return
|
||||
for key, value in payload.items():
|
||||
if key not in excluded_keys:
|
||||
set_clauses.append(f"{key} = '{value}'")
|
||||
if not _VALID_SQL_IDENTIFIER.match(key):
|
||||
logger.warning(f"Skipping invalid column name in payload: {key!r}")
|
||||
continue
|
||||
param_name = f"payload_{key}"
|
||||
set_clauses.append(f"{key} = :{param_name}")
|
||||
params.append(StatementParameterListItem(name=param_name, value=str(value)))
|
||||
|
||||
if not set_clauses:
|
||||
logger.error("No fields to update")
|
||||
return
|
||||
update_sql = f"UPDATE {self.fully_qualified_table_name} SET "
|
||||
update_sql += ", ".join(set_clauses)
|
||||
update_sql += f" WHERE memory_id = '{vector_id}'"
|
||||
update_sql += " WHERE memory_id = :vector_id"
|
||||
params.append(StatementParameterListItem(name="vector_id", value=str(vector_id)))
|
||||
try:
|
||||
logger.info(f"Updating vector with ID {vector_id} in Delta table {self.fully_qualified_table_name}")
|
||||
|
||||
response = self.client.statement_execution.execute_statement(
|
||||
statement=update_sql, warehouse_id=self.warehouse_id, wait_timeout="30s"
|
||||
statement=update_sql,
|
||||
warehouse_id=self.warehouse_id,
|
||||
wait_timeout="30s",
|
||||
parameters=params,
|
||||
)
|
||||
|
||||
if response.status.state.value == "SUCCEEDED":
|
||||
@@ -572,14 +611,23 @@ class Databricks(VectorStoreBase):
|
||||
filters = {"memory_id": vector_id}
|
||||
filters_json = json.dumps(filters)
|
||||
|
||||
results = self.client.vector_search_indexes.query_index(
|
||||
index_name=self.fully_qualified_index_name,
|
||||
columns=self.column_names,
|
||||
query_text=" ", # Empty query, rely on filters
|
||||
num_results=1,
|
||||
query_type=self.query_type,
|
||||
filters_json=filters_json,
|
||||
# Use query_text for Delta Sync with model endpoint, query_vector otherwise
|
||||
query_kwargs = {
|
||||
"index_name": self.fully_qualified_index_name,
|
||||
"columns": self.column_names,
|
||||
"num_results": 1,
|
||||
"query_type": self.query_type,
|
||||
"filters_json": filters_json,
|
||||
}
|
||||
uses_model_endpoint = (
|
||||
self.index_type == VectorIndexType.DELTA_SYNC and self.embedding_model_endpoint_name
|
||||
)
|
||||
if uses_model_endpoint:
|
||||
query_kwargs["query_text"] = " "
|
||||
else:
|
||||
query_kwargs["query_vector"] = [0.0] * self.embedding_dimension
|
||||
|
||||
results = self.client.vector_search_indexes.query_index(**query_kwargs)
|
||||
|
||||
# Process results
|
||||
result_data = results.result if hasattr(results, "result") else results
|
||||
@@ -589,7 +637,7 @@ class Databricks(VectorStoreBase):
|
||||
raise KeyError(f"Vector with ID {vector_id} not found")
|
||||
|
||||
result = data_array[0]
|
||||
columns = columns = [col.name for col in results.manifest.columns] if results.manifest and results.manifest.columns else []
|
||||
columns = [col.name for col in results.manifest.columns] if results.manifest and results.manifest.columns else []
|
||||
row_data = dict(zip(columns, result))
|
||||
|
||||
# Build payload following the standard schema
|
||||
@@ -686,14 +734,23 @@ class Databricks(VectorStoreBase):
|
||||
filters_json = json.dumps(filters) if filters else None
|
||||
num_results = limit or 100
|
||||
columns = self.column_names
|
||||
sdk_results = self.client.vector_search_indexes.query_index(
|
||||
index_name=self.fully_qualified_index_name,
|
||||
columns=columns,
|
||||
query_text=" ",
|
||||
num_results=num_results,
|
||||
query_type=self.query_type,
|
||||
filters_json=filters_json,
|
||||
# Use query_text for Delta Sync with model endpoint, query_vector otherwise
|
||||
query_kwargs = {
|
||||
"index_name": self.fully_qualified_index_name,
|
||||
"columns": columns,
|
||||
"num_results": num_results,
|
||||
"query_type": self.query_type,
|
||||
"filters_json": filters_json,
|
||||
}
|
||||
uses_model_endpoint = (
|
||||
self.index_type == VectorIndexType.DELTA_SYNC and self.embedding_model_endpoint_name
|
||||
)
|
||||
if uses_model_endpoint:
|
||||
query_kwargs["query_text"] = " "
|
||||
else:
|
||||
query_kwargs["query_vector"] = [0.0] * self.embedding_dimension
|
||||
|
||||
sdk_results = self.client.vector_search_indexes.query_index(**query_kwargs)
|
||||
result_data = sdk_results.result if hasattr(sdk_results, "result") else sdk_results
|
||||
data_array = result_data.data_array if hasattr(result_data, "data_array") else []
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class OutputData(BaseModel):
|
||||
|
||||
|
||||
class MongoDB(VectorStoreBase):
|
||||
VECTOR_TYPE = "knnVector"
|
||||
VECTOR_TYPE = "vector"
|
||||
SIMILARITY_METRIC = "cosine"
|
||||
|
||||
def __init__(self, db_name: str, collection_name: str, embedding_model_dims: int, mongo_uri: str):
|
||||
@@ -69,17 +69,16 @@ class MongoDB(VectorStoreBase):
|
||||
else:
|
||||
search_index_model = SearchIndexModel(
|
||||
name=self.index_name,
|
||||
type="vectorSearch",
|
||||
definition={
|
||||
"mappings": {
|
||||
"dynamic": False,
|
||||
"fields": {
|
||||
"embedding": {
|
||||
"type": self.VECTOR_TYPE,
|
||||
"dimensions": self.embedding_model_dims,
|
||||
"similarity": self.SIMILARITY_METRIC,
|
||||
}
|
||||
},
|
||||
}
|
||||
"fields": [
|
||||
{
|
||||
"type": self.VECTOR_TYPE,
|
||||
"path": "embedding",
|
||||
"numDimensions": self.embedding_model_dims,
|
||||
"similarity": self.SIMILARITY_METRIC,
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
collection.create_search_index(search_index_model)
|
||||
@@ -141,7 +140,7 @@ class MongoDB(VectorStoreBase):
|
||||
"$vectorSearch": {
|
||||
"index": self.index_name,
|
||||
"limit": limit,
|
||||
"numCandidates": limit,
|
||||
"numCandidates": min(limit * 20, 10000),
|
||||
"queryVector": vectors,
|
||||
"path": "embedding",
|
||||
}
|
||||
@@ -198,7 +197,8 @@ class MongoDB(VectorStoreBase):
|
||||
if vector is not None:
|
||||
update_fields["embedding"] = vector
|
||||
if payload is not None:
|
||||
update_fields["payload"] = payload
|
||||
for key, value in payload.items():
|
||||
update_fields[f"payload.{key}"] = value
|
||||
|
||||
if update_fields:
|
||||
try:
|
||||
|
||||
@@ -113,6 +113,25 @@ class OpenSearchDB(VectorStoreBase):
|
||||
if payloads is None:
|
||||
payloads = [{} for _ in range(len(vectors))]
|
||||
|
||||
for idx, vec in enumerate(vectors):
|
||||
if vec is None:
|
||||
raise ValueError(
|
||||
f"Vector at index {idx} is null. "
|
||||
f"This usually means the embedding model failed to generate an embedding. "
|
||||
f"Check that your embedding model is configured correctly and returning valid vectors."
|
||||
)
|
||||
if len(vec) == 0:
|
||||
raise ValueError(
|
||||
f"Vector at index {idx} is empty. "
|
||||
f"Expected a vector of dimension {self.embedding_model_dims}, got an empty vector."
|
||||
)
|
||||
if len(vec) != self.embedding_model_dims:
|
||||
raise ValueError(
|
||||
f"Vector at index {idx} has dimension {len(vec)}, "
|
||||
f"but the index '{self.collection_name}' expects dimension {self.embedding_model_dims}. "
|
||||
f"Ensure your embedding model's output dimensions match the vector store configuration."
|
||||
)
|
||||
|
||||
results = []
|
||||
for i, (vec, id_) in enumerate(zip(vectors, ids)):
|
||||
body = {
|
||||
@@ -124,14 +143,16 @@ class OpenSearchDB(VectorStoreBase):
|
||||
self.client.index(index=self.collection_name, body=body)
|
||||
# Force refresh to make documents immediately searchable for tests
|
||||
self.client.indices.refresh(index=self.collection_name)
|
||||
|
||||
results.append(OutputData(
|
||||
id=id_,
|
||||
score=1.0, # No score for inserts
|
||||
payload=payloads[i]
|
||||
))
|
||||
|
||||
results.append(
|
||||
OutputData(
|
||||
id=id_,
|
||||
score=1.0, # No score for inserts
|
||||
payload=payloads[i],
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error inserting vector {id_}: {e}")
|
||||
logger.error(f"Error inserting vector {id_}: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
return results
|
||||
@@ -179,7 +200,7 @@ class OpenSearchDB(VectorStoreBase):
|
||||
]
|
||||
return results
|
||||
except Exception as e:
|
||||
logger.error(f"Error during search: {e}")
|
||||
logger.error(f"Error during search: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
def delete(self, vector_id: str) -> None:
|
||||
@@ -200,6 +221,15 @@ class OpenSearchDB(VectorStoreBase):
|
||||
|
||||
def update(self, vector_id: str, vector: Optional[List[float]] = None, payload: Optional[Dict] = None) -> None:
|
||||
"""Update a vector and its payload using the custom 'id' field."""
|
||||
if vector is not None:
|
||||
if len(vector) == 0:
|
||||
raise ValueError("Cannot update with an empty vector.")
|
||||
if len(vector) != self.embedding_model_dims:
|
||||
raise ValueError(
|
||||
f"Update vector has dimension {len(vector)}, "
|
||||
f"but the index '{self.collection_name}' expects dimension {self.embedding_model_dims}. "
|
||||
f"Ensure your embedding model's output dimensions match the vector store configuration."
|
||||
)
|
||||
|
||||
# First, find the document by custom ID
|
||||
search_query = {"query": {"term": {"id": vector_id}}}
|
||||
@@ -222,8 +252,9 @@ class OpenSearchDB(VectorStoreBase):
|
||||
if doc:
|
||||
try:
|
||||
response = self.client.update(index=self.collection_name, id=opensearch_id, body={"doc": doc})
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating vector {vector_id}: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def get(self, vector_id: str) -> Optional[OutputData]:
|
||||
"""Retrieve a vector by ID."""
|
||||
@@ -238,7 +269,7 @@ class OpenSearchDB(VectorStoreBase):
|
||||
|
||||
return OutputData(id=hits[0]["_source"].get("id"), score=1.0, payload=hits[0]["_source"].get("payload", {}))
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector {vector_id}: {str(e)}")
|
||||
logger.error(f"Error retrieving vector {vector_id}: {str(e)}", exc_info=True)
|
||||
return None
|
||||
|
||||
def list_cols(self) -> List[str]:
|
||||
@@ -281,9 +312,8 @@ class OpenSearchDB(VectorStoreBase):
|
||||
]
|
||||
return [results] # VectorStore expects tuple/list format
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing vectors: {e}")
|
||||
logger.error(f"Error listing vectors: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
def reset(self):
|
||||
"""Reset the index by deleting and recreating it."""
|
||||
|
||||
+136
-14
@@ -1,12 +1,14 @@
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
from typing import Optional
|
||||
|
||||
from qdrant_client import QdrantClient
|
||||
from qdrant_client.models import (
|
||||
Distance,
|
||||
FieldCondition,
|
||||
Filter,
|
||||
MatchAny,
|
||||
MatchExcept,
|
||||
MatchText,
|
||||
MatchValue,
|
||||
PointIdsList,
|
||||
PointStruct,
|
||||
@@ -44,7 +46,8 @@ class Qdrant(VectorStoreBase):
|
||||
path (str, optional): Path for local Qdrant database. Defaults to None.
|
||||
url (str, optional): Full URL for Qdrant server. Defaults to None.
|
||||
api_key (str, optional): API key for Qdrant server. Defaults to None.
|
||||
on_disk (bool, optional): Enables persistent storage. Defaults to False.
|
||||
on_disk (bool, optional): Enables persistent storage. Vectors are stored on disk (True) or in memory (False).
|
||||
Does not delete the local database path. Defaults to False.
|
||||
"""
|
||||
if client:
|
||||
self.client = client
|
||||
@@ -62,9 +65,6 @@ class Qdrant(VectorStoreBase):
|
||||
if not params:
|
||||
params["path"] = path
|
||||
self.is_local = True
|
||||
if not on_disk:
|
||||
if os.path.exists(path) and os.path.isdir(path):
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
self.is_local = False
|
||||
|
||||
@@ -138,26 +138,148 @@ class Qdrant(VectorStoreBase):
|
||||
]
|
||||
self.client.upsert(collection_name=self.collection_name, points=points)
|
||||
|
||||
def _create_filter(self, filters: dict) -> Filter:
|
||||
def _build_field_condition(self, key: str, value) -> Optional[FieldCondition]:
|
||||
"""
|
||||
Build a single FieldCondition from a key-value filter pair.
|
||||
|
||||
Supports the enhanced filter syntax documented at
|
||||
https://docs.mem0.ai/open-source/features/metadata-filtering
|
||||
|
||||
Args:
|
||||
key (str): The payload field name.
|
||||
value: A scalar for simple equality, or a dict with one operator key.
|
||||
|
||||
Returns:
|
||||
Optional[FieldCondition]: The Qdrant field condition, or None if the
|
||||
value is the wildcard '*' (match any / field exists — skip filter).
|
||||
"""
|
||||
if not isinstance(value, dict):
|
||||
if value == "*":
|
||||
# Wildcard: match any value. Qdrant has no direct "field exists"
|
||||
# condition via FieldCondition, so we skip this filter (match all).
|
||||
return None
|
||||
if isinstance(value, list):
|
||||
# List shorthand: {"field": ["a", "b"]} treated as in-operator.
|
||||
return FieldCondition(key=key, match=MatchAny(any=value))
|
||||
# Simple equality: {"field": "value"}
|
||||
return FieldCondition(key=key, match=MatchValue(value=value))
|
||||
|
||||
ops = set(value.keys())
|
||||
range_ops = {"gt", "gte", "lt", "lte"}
|
||||
non_range_ops = ops - range_ops
|
||||
|
||||
if ops & range_ops:
|
||||
if non_range_ops:
|
||||
raise ValueError(
|
||||
f"Cannot mix range operators ({ops & range_ops}) with "
|
||||
f"non-range operators ({non_range_ops}) for field '{key}'. "
|
||||
f"Use AND to combine them as separate conditions."
|
||||
)
|
||||
range_kwargs = {op: value[op] for op in range_ops if op in value}
|
||||
return FieldCondition(key=key, range=Range(**range_kwargs))
|
||||
elif "eq" in value:
|
||||
return FieldCondition(key=key, match=MatchValue(value=value["eq"]))
|
||||
elif "ne" in value:
|
||||
return FieldCondition(key=key, match=MatchExcept(**{"except": [value["ne"]]}))
|
||||
elif "in" in value:
|
||||
return FieldCondition(key=key, match=MatchAny(any=value["in"]))
|
||||
elif "nin" in value:
|
||||
return FieldCondition(key=key, match=MatchExcept(**{"except": value["nin"]}))
|
||||
elif "contains" in value or "icontains" in value:
|
||||
# MatchText: with a full-text index, tokenized matching (all words must appear).
|
||||
# Without a full-text index, exact substring match.
|
||||
op = "icontains" if "icontains" in value else "contains"
|
||||
text = value[op]
|
||||
if op == "icontains":
|
||||
logger.debug(
|
||||
"icontains on field '%s': Qdrant MatchText case sensitivity depends on "
|
||||
"full-text index configuration. Without a full-text index this behaves "
|
||||
"as a case-sensitive substring match (same as 'contains').",
|
||||
key,
|
||||
)
|
||||
return FieldCondition(key=key, match=MatchText(text=text))
|
||||
else:
|
||||
supported = {"eq", "ne", "gt", "gte", "lt", "lte", "in", "nin", "contains", "icontains"}
|
||||
raise ValueError(
|
||||
f"Unsupported filter operator(s) for field '{key}': {ops}. "
|
||||
f"Supported operators: {supported}"
|
||||
)
|
||||
|
||||
def _create_filter(self, filters: dict) -> Optional[Filter]:
|
||||
"""
|
||||
Create a Filter object from the provided filters.
|
||||
|
||||
Supports the enhanced filter syntax with comparison operators (eq, ne,
|
||||
gt, gte, lt, lte), list operators (in, nin), string operators (contains,
|
||||
icontains), and logical operators (AND, OR, NOT).
|
||||
|
||||
Args:
|
||||
filters (dict): Filters to apply.
|
||||
|
||||
Returns:
|
||||
Filter: The created Filter object.
|
||||
Filter: The created Filter object, or None if filters is empty.
|
||||
"""
|
||||
if not filters:
|
||||
return None
|
||||
|
||||
conditions = []
|
||||
|
||||
# Normalize $or/$not/$and → OR/NOT/AND and deduplicate.
|
||||
# Memory._process_metadata_filters() renames OR→$or and NOT→$not,
|
||||
# but effective_filters retains the original OR/NOT keys from
|
||||
# deepcopy(input_filters). Without dedup the same sub-conditions
|
||||
# would be evaluated twice.
|
||||
key_map = {"$or": "OR", "$not": "NOT", "$and": "AND"}
|
||||
normalized = {}
|
||||
for key, value in filters.items():
|
||||
if isinstance(value, dict) and "gte" in value and "lte" in value:
|
||||
conditions.append(FieldCondition(key=key, range=Range(gte=value["gte"], lte=value["lte"])))
|
||||
norm_key = key_map.get(key, key)
|
||||
if norm_key not in normalized:
|
||||
normalized[norm_key] = value
|
||||
|
||||
must = []
|
||||
should = []
|
||||
must_not = []
|
||||
|
||||
for key, value in normalized.items():
|
||||
if key in ("AND", "OR", "NOT"):
|
||||
if not isinstance(value, list):
|
||||
raise ValueError(
|
||||
f"{key} filter value must be a list of filter dicts, "
|
||||
f"got {type(value).__name__}"
|
||||
)
|
||||
for i, item in enumerate(value):
|
||||
if not isinstance(item, dict):
|
||||
raise ValueError(
|
||||
f"{key} filter list item at index {i} must be a dict, "
|
||||
f"got {type(item).__name__}: {item!r}"
|
||||
)
|
||||
|
||||
if key == "AND":
|
||||
for sub in value:
|
||||
built = self._create_filter(sub)
|
||||
if built:
|
||||
must.append(built)
|
||||
elif key == "OR":
|
||||
for sub in value:
|
||||
built = self._create_filter(sub)
|
||||
if built:
|
||||
should.append(built)
|
||||
elif key == "NOT":
|
||||
for sub in value:
|
||||
built = self._create_filter(sub)
|
||||
if built:
|
||||
must_not.append(built)
|
||||
else:
|
||||
conditions.append(FieldCondition(key=key, match=MatchValue(value=value)))
|
||||
return Filter(must=conditions) if conditions else None
|
||||
condition = self._build_field_condition(key, value)
|
||||
if condition is not None:
|
||||
must.append(condition)
|
||||
|
||||
if not any([must, should, must_not]):
|
||||
return None
|
||||
|
||||
return Filter(
|
||||
must=must or None,
|
||||
should=should or None,
|
||||
must_not=must_not or None,
|
||||
)
|
||||
|
||||
def search(self, query: str, vectors: list, limit: int = 5, filters: dict = None) -> list:
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
try:
|
||||
from turbopuffer import Turbopuffer as TurbopufferClient
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Turbopuffer requires extra dependencies. Install with `pip install turbopuffer`"
|
||||
) from None
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mem0.vector_stores.base import VectorStoreBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OutputData(BaseModel):
|
||||
id: Optional[str]
|
||||
score: Optional[float]
|
||||
payload: Optional[Dict]
|
||||
|
||||
|
||||
class TurbopufferDB(VectorStoreBase):
|
||||
def __init__(
|
||||
self,
|
||||
collection_name: str,
|
||||
embedding_model_dims: int,
|
||||
api_key: Optional[str] = None,
|
||||
region: str = "gcp-us-central1",
|
||||
distance_metric: str = "cosine_distance",
|
||||
batch_size: int = 100,
|
||||
extra_params: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""
|
||||
Initialize the Turbopuffer vector store.
|
||||
|
||||
Args:
|
||||
collection_name (str): Name of the namespace/collection.
|
||||
embedding_model_dims (int): Dimensions of the embedding model.
|
||||
api_key (str, optional): API key for Turbopuffer. Defaults to None.
|
||||
region (str, optional): Turbopuffer region. Defaults to "gcp-us-central1".
|
||||
distance_metric (str, optional): Distance metric for vector similarity.
|
||||
Options: "cosine_distance" or "euclidean_squared". Defaults to "cosine_distance".
|
||||
batch_size (int, optional): Batch size for operations. Defaults to 100.
|
||||
extra_params (Dict, optional): Additional parameters for Turbopuffer client. Defaults to None.
|
||||
"""
|
||||
api_key = api_key or os.environ.get("TURBOPUFFER_API_KEY")
|
||||
if not api_key:
|
||||
raise ValueError(
|
||||
"Turbopuffer API key must be provided either as a parameter or via TURBOPUFFER_API_KEY environment variable"
|
||||
)
|
||||
|
||||
params = extra_params or {}
|
||||
params["region"] = region
|
||||
|
||||
self.client = TurbopufferClient(api_key=api_key, **params)
|
||||
self.collection_name = collection_name
|
||||
self.embedding_model_dims = embedding_model_dims
|
||||
self.distance_metric = distance_metric
|
||||
self.batch_size = batch_size
|
||||
|
||||
self.namespace = self.client.namespace(self.collection_name)
|
||||
|
||||
def create_col(self, name=None, vector_size=None, distance=None):
|
||||
"""
|
||||
Create a new namespace in Turbopuffer.
|
||||
Namespaces are created implicitly on first upsert, so this is a no-op.
|
||||
"""
|
||||
pass
|
||||
|
||||
def insert(
|
||||
self,
|
||||
vectors: List[List[float]],
|
||||
payloads: Optional[List[Dict]] = None,
|
||||
ids: Optional[List[Union[str, int]]] = None,
|
||||
):
|
||||
"""
|
||||
Insert vectors into the namespace.
|
||||
|
||||
Args:
|
||||
vectors (list): List of vectors to insert.
|
||||
payloads (list, optional): List of payloads corresponding to vectors. Defaults to None.
|
||||
ids (list, optional): List of IDs corresponding to vectors. Defaults to None.
|
||||
"""
|
||||
logger.info(f"Inserting {len(vectors)} vectors into namespace {self.collection_name}")
|
||||
|
||||
if ids is None:
|
||||
ids = [str(i) for i in range(len(vectors))]
|
||||
|
||||
for i in range(0, len(vectors), self.batch_size):
|
||||
batch_end = i + self.batch_size
|
||||
rows = []
|
||||
for j in range(i, min(batch_end, len(vectors))):
|
||||
row = {}
|
||||
if payloads and payloads[j]:
|
||||
row.update(payloads[j])
|
||||
row["id"] = str(ids[j])
|
||||
row["vector"] = vectors[j]
|
||||
rows.append(row)
|
||||
|
||||
self.namespace.write(
|
||||
upsert_rows=rows,
|
||||
distance_metric=self.distance_metric,
|
||||
)
|
||||
|
||||
def _parse_output(self, rows) -> List[OutputData]:
|
||||
"""
|
||||
Parse the output data from Turbopuffer query results.
|
||||
|
||||
Args:
|
||||
rows: List of Row objects from Turbopuffer query.
|
||||
|
||||
Returns:
|
||||
List[OutputData]: Parsed output data.
|
||||
"""
|
||||
results = []
|
||||
for row in rows:
|
||||
row_dict = row.model_dump()
|
||||
row_id = str(row_dict.pop("id"))
|
||||
dist = row_dict.pop("$dist", None)
|
||||
row_dict.pop("vector", None)
|
||||
|
||||
score = 1 - dist if dist is not None else None
|
||||
|
||||
results.append(OutputData(
|
||||
id=row_id,
|
||||
score=score,
|
||||
payload=row_dict,
|
||||
))
|
||||
return results
|
||||
|
||||
def _convert_filters(self, filters: Optional[Dict]):
|
||||
"""
|
||||
Convert mem0 filters to Turbopuffer filter format.
|
||||
|
||||
Turbopuffer filters use tuple format: ("And", (("field", "Op", value), ...))
|
||||
"""
|
||||
if not filters:
|
||||
return None
|
||||
|
||||
conditions = []
|
||||
for key, value in filters.items():
|
||||
if isinstance(value, dict):
|
||||
if "gte" in value:
|
||||
conditions.append((key, "Gte", value["gte"]))
|
||||
if "lte" in value:
|
||||
conditions.append((key, "Lte", value["lte"]))
|
||||
else:
|
||||
conditions.append((key, "Eq", value))
|
||||
|
||||
if not conditions:
|
||||
return None
|
||||
if len(conditions) == 1:
|
||||
return conditions[0]
|
||||
return ("And", tuple(conditions))
|
||||
|
||||
def search(
|
||||
self, query: str, vectors: List[float], limit: int = 5, filters: Optional[Dict] = None
|
||||
) -> List[OutputData]:
|
||||
"""
|
||||
Search for similar vectors.
|
||||
|
||||
Args:
|
||||
query (str): Query text (unused in vector search, kept for interface consistency).
|
||||
vectors (list): Query vector to search with.
|
||||
limit (int, optional): Number of results to return. Defaults to 5.
|
||||
filters (dict, optional): Filters to apply to the search. Defaults to None.
|
||||
|
||||
Returns:
|
||||
list: Search results.
|
||||
"""
|
||||
query_params = {
|
||||
"rank_by": ("vector", "ANN", vectors),
|
||||
"top_k": limit,
|
||||
"include_attributes": True,
|
||||
}
|
||||
|
||||
tpuf_filters = self._convert_filters(filters)
|
||||
if tpuf_filters is not None:
|
||||
query_params["filters"] = tpuf_filters
|
||||
|
||||
response = self.namespace.query(**query_params)
|
||||
return self._parse_output(response.rows or [])
|
||||
|
||||
def delete(self, vector_id: Union[str, int]):
|
||||
"""
|
||||
Delete a vector by ID.
|
||||
|
||||
Args:
|
||||
vector_id (Union[str, int]): ID of the vector to delete.
|
||||
"""
|
||||
self.namespace.write(deletes=[str(vector_id)])
|
||||
|
||||
def update(
|
||||
self,
|
||||
vector_id: Union[str, int],
|
||||
vector: Optional[List[float]] = None,
|
||||
payload: Optional[Dict] = None,
|
||||
):
|
||||
"""
|
||||
Update a vector and its payload.
|
||||
|
||||
Args:
|
||||
vector_id (Union[str, int]): ID of the vector to update.
|
||||
vector (list, optional): Updated vector. Defaults to None.
|
||||
payload (dict, optional): Updated payload. Defaults to None.
|
||||
"""
|
||||
if vector is not None:
|
||||
row = {}
|
||||
if payload:
|
||||
row.update(payload)
|
||||
row["id"] = str(vector_id)
|
||||
row["vector"] = vector
|
||||
self.namespace.write(
|
||||
upsert_rows=[row],
|
||||
distance_metric=self.distance_metric,
|
||||
)
|
||||
elif payload is not None:
|
||||
row = dict(payload)
|
||||
row["id"] = str(vector_id)
|
||||
self.namespace.write(patch_rows=[row])
|
||||
|
||||
def get(self, vector_id: Union[str, int]) -> Optional[OutputData]:
|
||||
"""
|
||||
Retrieve a vector by ID.
|
||||
|
||||
Args:
|
||||
vector_id (Union[str, int]): ID of the vector to retrieve.
|
||||
|
||||
Returns:
|
||||
OutputData: Retrieved vector data, or None if not found.
|
||||
"""
|
||||
try:
|
||||
response = self.namespace.query(
|
||||
top_k=1,
|
||||
rank_by=("vector", "ANN", [0.0] * self.embedding_model_dims),
|
||||
filters=("id", "Eq", str(vector_id)),
|
||||
include_attributes=True,
|
||||
)
|
||||
rows = response.rows or []
|
||||
if rows:
|
||||
return self._parse_output(rows)[0]
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error retrieving vector {vector_id}: {e}")
|
||||
return None
|
||||
|
||||
def list_cols(self) -> list:
|
||||
"""
|
||||
List all namespaces.
|
||||
|
||||
Returns:
|
||||
list: List of namespace summaries.
|
||||
"""
|
||||
try:
|
||||
result = []
|
||||
for ns in self.client.namespaces():
|
||||
result.append(ns)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing namespaces: {e}")
|
||||
return []
|
||||
|
||||
def delete_col(self):
|
||||
"""Delete the entire namespace."""
|
||||
try:
|
||||
self.namespace.delete_all()
|
||||
logger.info(f"Namespace {self.collection_name} deleted successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting namespace {self.collection_name}: {e}")
|
||||
|
||||
def col_info(self) -> Dict:
|
||||
"""
|
||||
Get information about the namespace.
|
||||
|
||||
Returns:
|
||||
dict: Namespace metadata.
|
||||
"""
|
||||
try:
|
||||
metadata = self.namespace.metadata()
|
||||
return {
|
||||
"name": self.collection_name,
|
||||
"approx_row_count": metadata.approx_row_count,
|
||||
"approx_logical_bytes": metadata.approx_logical_bytes,
|
||||
"created_at": str(metadata.created_at),
|
||||
"updated_at": str(metadata.updated_at),
|
||||
}
|
||||
except Exception:
|
||||
return {"name": self.collection_name}
|
||||
|
||||
def list(self, filters: Optional[Dict] = None, limit: int = 100) -> list:
|
||||
"""
|
||||
List vectors in the namespace with optional filtering.
|
||||
|
||||
Args:
|
||||
filters (dict, optional): Filters to apply. Defaults to None.
|
||||
limit (int, optional): Number of vectors to return. Defaults to 100.
|
||||
|
||||
Returns:
|
||||
list: Wrapped list of OutputData objects ([[results]]).
|
||||
"""
|
||||
query_params = {
|
||||
"rank_by": ("vector", "ANN", [0.0] * self.embedding_model_dims),
|
||||
"top_k": limit,
|
||||
"include_attributes": True,
|
||||
}
|
||||
|
||||
tpuf_filters = self._convert_filters(filters)
|
||||
if tpuf_filters is not None:
|
||||
query_params["filters"] = tpuf_filters
|
||||
|
||||
try:
|
||||
response = self.namespace.query(**query_params)
|
||||
results = self._parse_output(response.rows or [])
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing vectors: {e}")
|
||||
results = []
|
||||
return [results]
|
||||
|
||||
def count(self) -> int:
|
||||
"""
|
||||
Get approximate count of vectors in the namespace.
|
||||
|
||||
Returns:
|
||||
int: Approximate number of vectors.
|
||||
"""
|
||||
try:
|
||||
metadata = self.namespace.metadata()
|
||||
return metadata.approx_row_count
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def reset(self):
|
||||
"""Reset the namespace by deleting all vectors."""
|
||||
self.delete_col()
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
All notable changes to the `@mem0/openclaw-mem0` plugin will be documented in this file.
|
||||
|
||||
## [0.4.1] - 2026-03-26
|
||||
|
||||
### Added
|
||||
- **Improved extraction quality**: Enhanced noise filtering, deduplication, and better extraction instructions for higher-quality memory capture (#4302)
|
||||
|
||||
### Fixed
|
||||
- **Credential detection in extraction**: Improved detection of credentials, API keys, and secrets in extraction instructions to prevent them from being stored as memories (#4552)
|
||||
- **Standalone timestamp extraction**: Prevented extraction of standalone timestamps as memories when no meaningful content accompanies them (#4550)
|
||||
|
||||
## [0.4.0] - 2026-03-16
|
||||
|
||||
### Added
|
||||
|
||||
+4
-1
@@ -111,12 +111,15 @@ LANGUAGE:
|
||||
- If the user speaks Spanish, store the memory in Spanish; do not translate
|
||||
|
||||
Exclude (NEVER store):
|
||||
- Passwords, API keys, tokens, secrets, or any credentials — even if shared in conversation. Instead store: "Tavily API key was configured and saved to .env (as of 2026-02-20)"
|
||||
- Passwords, API keys, tokens, secrets, or any credentials — even when embedded in configuration blocks, setup logs, or tool output. This includes strings starting with sk-, m0-, ak_, ghp_, bot tokens (digits followed by colon and alphanumeric string), bearer tokens, webhook URLs containing tokens, pairing codes, and any long alphanumeric strings that appear in config/env contexts. Never include the actual secret value in a memory. Instead, record that the credential was configured:
|
||||
WRONG: "User's API key is sk-abc123..." or "Bot token is 12345:AABcd..."
|
||||
RIGHT: "API key was configured for the service (as of YYYY-MM-DD)" or "Telegram bot token was set up"
|
||||
- One-time commands or instructions ("stop the script", "continue where you left off")
|
||||
- Acknowledgments or emotional reactions ("ok", "sounds good", "you're right", "sir")
|
||||
- Transient UI/navigation states ("user is in the admin panel", "relay is attached")
|
||||
- Ephemeral process status ("download at 50%", "daemon not running", "still syncing")
|
||||
- Cron heartbeat outputs, NO_REPLY responses, compaction flush directives
|
||||
- The current date/time as a standalone fact — timestamps are conversation context, not durable knowledge. "User indicates current time is 3:25 PM" is NEVER worth storing. However, DO use timestamps to anchor other facts: "User installed Ollama on 2026-03-21" is correct.
|
||||
- System routing metadata (message IDs, sender IDs, channel routing info)
|
||||
- Generic small talk with no informational content
|
||||
- Raw code snippets (capture the intent/decision, not the code itself)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user