Compare commits

..
Author SHA1 Message Date
mintlify[bot]andGitHub 44dc74d78f docs: expand short frontmatter descriptions for SEO 2026-06-26 10:32:06 +00:00
KartikandGitHub d258b638ef docs: document FastEmbed embedder + missing org/project API endpoints (#5852) 2026-06-26 16:00:12 +05:30
Harsh Vardhan GuptaandGitHub bbbfcfea07 fix(deps): bump undici to >=6.27.0 (CVE-2026-12151) (#5861) 2026-06-26 15:32:09 +05:30
KartikandGitHub fbef369b91 docs(open-source): fix OSS feature docs for v3 SDK (#5847) 2026-06-26 14:14:33 +05:30
KartikandGitHub a7ed68e697 docs(integrations): fix retired model IDs, v3 response shapes & vercel deps (#5842) 2026-06-26 13:59:33 +05:30
KartikandGitHub 8a92cf0306 docs(integrations): sync agent-plugin hook tables with actual hooks.json (#5839) 2026-06-26 13:58:28 +05:30
0fbbb2f525 feat(memory): expose expiration controls in client docs (#5874)
Co-authored-by: Soumil Rathi <soumilrathi@gmail.com>
2026-06-25 17:14:33 -07:00
818c2981b7 feat(ts-sdk): add MiniMax LLM provider (#5858)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-25 17:01:50 +05:30
Rod BoevandGitHub 1f66aadfa3 fix(openclaw): normalize Windows skill-loader URLs before fileURLToPath (#5679) 2026-06-25 16:36:11 +05:30
Hrushikesh YadavandGitHub b91c745fbc fix: apply remove_code_blocks() to LangChain path in async _create_procedural_memory (#5711) 2026-06-25 16:34:28 +05:30
Muhammad FurqanandGitHub af70668308 fix(reranker): score HuggingFace reranker with sigmoid, not min-max (#5715) 2026-06-25 16:18:02 +05:30
890473f891 fix(core): validate and trim entity IDs in delete_all() (#5735)
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Kartik <kartik.labhshetwar@mem0.ai>
2026-06-25 16:14:30 +05:30
Hrushikesh YadavandGitHub d2ff83cf72 fix(redis): use .get() for hash/created_at in insert() to handle entity payloads (#5709) 2026-06-25 16:13:44 +05:30
Rod BoevandGitHub 0e02effaf7 fix(notices): derive scale counts for Redis and search backends (#5687) 2026-06-25 16:04:44 +05:30
9269a0ad6e feat(ts-sdk): add LiteLLM as LLM provider (#5830)
Co-authored-by: kartik-mem0 <kartik.labhshetwar@mem0.ai>
2026-06-25 15:52:46 +05:30
Hrushikesh YadavandGitHub 3d06006f36 fix(valkey): escape special chars in FT.SEARCH tag filter values (#5750) 2026-06-25 15:20:13 +05:30
76 changed files with 1590 additions and 700 deletions
+43 -130
View File
@@ -1,157 +1,72 @@
# Contributing to Mem0
# Contributing to mem0
First off, thank you for taking the time to contribute! 🎉 Mem0 is a
community-driven project and we welcome contributions of all kinds — bug fixes,
new features, documentation, examples, and integrations.
Let us make contribution easy, collaborative and fun.
Mem0 is a polyglot monorepo, and this guide covers contributing to both the
**Python SDK** and the **TypeScript SDK** (and the rest of the repository).
## Submit your Contribution through PR
## Before You Start
To make a contribution, follow these steps:
### 1. Open an Issue First
1. Fork and clone this repository
2. Do the changes on your fork with dedicated feature branch `feature/f1`
3. If you modified the code (new feature or bug-fix), please add tests for it
4. Include proper documentation / docstring and examples to run the feature
5. Ensure that all tests pass
6. Submit a pull request
**Always open an issue before opening a pull request.** This lets us discuss the
change, avoid duplicate effort, and agree on the approach before you invest time
in code.
For more details about pull requests, please read [GitHub's guides](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
- Search [existing issues](https://github.com/mem0ai/mem0/issues) first to see if
your bug or idea already exists.
- If it doesn't, open a
[bug report](https://github.com/mem0ai/mem0/issues/new?template=bug_report.yml) or
[feature request](https://github.com/mem0ai/mem0/issues/new?template=feature_request.yml).
- For anything beyond a trivial fix, wait for a maintainer to confirm the approach
before starting significant work.
Every pull request must link to an issue using `Closes #<issue-number>`.
### 📦 Development Environment
### 2. Sign the Contributor License Agreement (CLA)
**We cannot accept or merge any pull request until you have signed our Contributor
License Agreement (CLA).**
When you open your first PR, the CLA bot will automatically comment with a link to
sign. Signing takes less than a minute and only needs to be done once. Pull
requests from contributors who have not signed the CLA will be blocked from
merging.
## Repository Layout
The two most common contribution targets are the SDKs:
| Package | Path | Language | Package manager |
| --------------------- | ---------- | ------------ | --------------- |
| Python SDK (`mem0ai`) | `mem0/` | Python 3.9+ | `hatch` |
| TypeScript SDK (`mem0ai`) | `mem0-ts/` | TypeScript | `pnpm` |
Other packages include the CLIs (`cli/python/`, `cli/node/`), integrations
(`integrations/`), the self-hosted `server/`, `openmemory/`, and the docs site
(`docs/`). See [AGENTS.md](./AGENTS.md) for a full map of the repository.
## Development Workflow
1. **Fork** the repository and **clone** your fork.
2. Create a **feature branch** from `main` (e.g. `feature/my-new-feature` or
`fix/issue-1234`).
3. Make your changes — add **tests**, **documentation**, and **examples** as
appropriate.
4. Run **linting and tests** for every package you touched (see below).
5. Commit using [Conventional Commits](https://www.conventionalcommits.org/)
(e.g. `feat:`, `fix:`, `docs:`, `refactor:`, `test:`).
6. Push and open a **pull request** against `main`, linking the issue with
`Closes #<number>` and filling out the
[PR template](./.github/PULL_REQUEST_TEMPLATE.md).
### Contributing to the Python SDK (`mem0/`)
We use [`hatch`](https://hatch.pypa.io/latest/install/) to manage environments.
**Do not use `pip` or `conda` for dependency management.**
We use `hatch` for managing development environments. To set up:
```bash
# Activate a dev environment (3.9 / 3.10 / 3.11 / 3.12)
hatch shell dev_py_3_11
# Activate environment for specific Python version:
hatch shell dev_py_3_9 # Python 3.9
hatch shell dev_py_3_10 # Python 3.10
hatch shell dev_py_3_11 # Python 3.11
hatch shell dev_py_3_12 # Python 3.12
# Install pre-commit hooks (runs ruff + isort on commit)
pre-commit install
# Lint, format, and sort imports
make lint
make format
make sort
# Run the test suite (run `make install_all` first if deps are missing)
# The environment will automatically install all dev dependencies
# Run tests within the activated shell:
make test
```
- **Linter / formatter:** Ruff (line length **120**)
- **Import sorting:** isort (`profile = "black"`)
- **Tests:** pytest (in `tests/`)
### 📌 Pre-commit
See the full [Development guide](https://docs.mem0.ai/contributing/development) for
environment details.
### Contributing to the TypeScript SDK (`mem0-ts/`)
We use [`pnpm`](https://pnpm.io/) (v10+) for all TypeScript packages. **Do not use
`npm` or `yarn`.**
To ensure our standards, make sure to install pre-commit before starting to contribute.
```bash
cd mem0-ts
pnpm install
pnpm run build # tsup (CJS + ESM)
pnpm run test # jest (all tests)
pnpm run test:unit # unit tests with coverage
pre-commit install
```
- **Build:** tsup
- **Formatter:** Prettier
- **Tests:** jest
- Always run type checking after changes: `pnpm run typecheck` (or `tsc --noEmit`).
- Use ES module `import` syntax — never `require()`.
### 🧪 Testing
## Good Contribution Practices
We use `pytest` to test our code across multiple Python versions. You can run tests using:
- **Keep PRs small and focused.** One logical change per PR is easier to review and
merge.
- **Follow existing patterns.** Match the style, structure, and conventions of the
code around you. Don't introduce new frameworks or abstractions without
discussion.
- **Write tests** that would fail without your change — regression tests for bugs,
coverage for new features.
- **Update documentation** in `docs/` for any user-facing change. New `.mdx` pages
must be added to `docs/llms.txt` (run
`python scripts/check-llms-txt-coverage.py --write` to scaffold entries).
- **Add examples** when introducing new user-facing behavior.
- **Run linters and tests locally** before pushing — CI re-runs them on every PR
via the CI Gate.
- **Never commit secrets** — no `.env` files, API keys, or credentials.
- **Don't add core dependencies lightly.** New Python dependencies belong in an
optional group in `pyproject.toml`, not the core `dependencies` list.
- **Be responsive** to review feedback and keep your branch up to date with `main`.
```bash
# Run tests with default Python version
make test
## Pull Request Checklist
# Test specific Python versions:
make test-py-3.9 # Python 3.9 environment
make test-py-3.10 # Python 3.10 environment
make test-py-3.11 # Python 3.11 environment
make test-py-3.12 # Python 3.12 environment
Before requesting review, make sure:
# When using hatch shells, run tests with:
make test # After activating a shell with hatch shell test_XX
```
- [ ] An issue exists and is linked with `Closes #<number>`
- [ ] You have signed the CLA
- [ ] Your code follows the project's style guidelines (lint passes)
- [ ] You performed a self-review of your changes
- [ ] Tests are added/updated and pass locally
- [ ] Documentation is updated if needed
Make sure that all tests pass across all supported Python versions before submitting a pull request.
## Reporting Security Issues
We look forward to your pull requests and can't wait to see your contributions!
**Do not report security vulnerabilities through public issues or pull requests.**
Please follow our [Security Policy](./SECURITY.md) to report them privately.
### 🚀 Releasing
## Releasing
All packages are published automatically via GitHub Actions when a GitHub Release is created with the correct tag prefix.
All packages are published automatically via GitHub Actions when a GitHub Release
is created with the correct tag prefix.
### Tag Prefixes
#### Tag Prefixes
| Package | Registry | Tag Prefix | Example |
|---------|----------|------------|---------|
@@ -162,17 +77,15 @@ is created with the correct tag prefix.
| `@mem0/vercel-ai-provider` | npm | `vercel-ai-v*` | `vercel-ai-v2.0.6` |
| `@mem0/openclaw-mem0` | npm | `openclaw-v*` | `openclaw-v1.0.1` |
### How to Release
#### How to Release
1. Bump the version in `pyproject.toml` (Python) or `package.json` (Node)
2. Create a [GitHub Release](https://github.com/mem0ai/mem0/releases/new) with the matching tag prefix
3. The correct workflow will trigger automatically — verify in the [Actions tab](https://github.com/mem0ai/mem0/actions)
### Publishing Details
#### Publishing Details
- **PyPI packages** use OIDC trusted publishing via `pypa/gh-action-pypi-publish`
- **npm packages** use OIDC trusted publishing via npm CLI (>= 11.5.1) — no tokens or secrets required
- All workflows require `permissions: id-token: write` for OIDC authentication
- First publish of a new npm package must be done manually; OIDC works for subsequent versions
We look forward to your pull requests and can't wait to see your contributions!
-48
View File
@@ -1,48 +0,0 @@
# Security Policy
We take the security of Mem0 and our community seriously. Thank you for helping
keep Mem0 and its users safe by disclosing vulnerabilities responsibly.
## Reporting a Vulnerability
Please **do not** report security vulnerabilities through public GitHub issues,
pull requests, or discussions.
If you believe you have found a security vulnerability in Mem0, please report it
privately through one of the following channels:
1. **GitHub Private Vulnerability Reporting** — open a
[private security advisory](https://github.com/mem0ai/mem0/security/advisories/new)
directly on this repository.
2. **Email** the maintainers at **support@mem0.ai** with the subject line:
`SECURITY: Mem0 vulnerability report`
To help us triage and resolve the issue quickly, please include as much of the
following as you can:
- Affected component or package (e.g. Python SDK, TypeScript SDK, server, OpenMemory)
- Affected version, tag, or commit
- Clear, step-by-step reproduction instructions
- The security impact and a proof of concept, if available
- Any suggested fix or mitigation
## Response Process
- We will acknowledge receipt of your report within **72 hours**.
- We will work with you privately to confirm the issue and assess its impact.
- Once a fix or mitigation is ready, we will coordinate a disclosure timeline
with you and credit you for the discovery, unless you prefer to remain anonymous.
## Public Disclosure
Please avoid sharing technical details of the vulnerability publicly until the
maintainers have reviewed the issue and a fix or mitigation has been released. We
are committed to resolving valid reports promptly and keeping you informed
throughout the process.
## Supported Versions
We release security fixes against the latest published version of each package.
Whenever possible, please reproduce the issue on the most recent release before
reporting.
@@ -50,6 +50,7 @@ Provide conversation messages for Mem0 to extract memories from. At least one en
| `app_id` | string | No* | Associates the memory with an app. |
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
| `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. |
| `expiration_date` | string | Optional | Date in `YYYY-MM-DD` format. The memory is visible through this date and hidden by default after it passes. |
> \* At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required.
@@ -83,3 +84,11 @@ The request is queued for background processing. The response contains an `event
<Info>
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
</Info>
<Info>
Memories with `expiration_date` remain stored after they expire. Search and get-all hide them by default; pass `show_expired: true` to include them.
</Info>
<Info>
Python uses `expiration_date`; TypeScript uses `expirationDate`.
</Info>
@@ -6,6 +6,10 @@ openapi: post /v3/memories/
List memories scoped by filters with paginated results. Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400.
Expired memories are hidden by default. Pass `show_expired: true` to include memories whose `expiration_date` has passed.
Python uses `show_expired`; TypeScript uses `showExpired`.
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
- `in`: Matches any of the values specified
@@ -32,6 +36,7 @@ memories = client.get_all(
}
]
},
show_expired=False,
page=1,
page_size=50
)
@@ -46,12 +51,14 @@ memories = client.get_all(
{
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
"memory": "Alex is planning a trip to San Francisco from July 1st to July 10th",
"expiration_date": null,
"created_at": "2024-07-01T12:00:00Z",
"updated_at": "2024-07-01T12:00:00Z"
},
{
"id": "a2b8c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
"memory": "Alex prefers vegetarian restaurants",
"expiration_date": null,
"created_at": "2024-07-05T15:30:00Z",
"updated_at": "2024-07-05T15:30:00Z"
}
@@ -8,6 +8,10 @@ Relevance-ranked hybrid search across stored memories. V3 uses multi-signal retr
Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400. At least one entity ID is required.
Expired memories are hidden by default. Pass `show_expired: true` to include memories whose `expiration_date` has passed.
Python uses `show_expired`; TypeScript uses `showExpired`.
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
- `in`: Matches any of the values specified
- `gte`: Greater than or equal to
@@ -30,6 +34,7 @@ The `filters` object supports complex logical operations (AND, OR, NOT) and comp
```python Platform API Example
related_memories = client.search(
query="What are Alice's hobbies?",
show_expired=False,
filters={
"OR": [
{
@@ -54,6 +59,7 @@ related_memories = client.search(
"category": "hobbies"
},
"score": 0.82,
"expiration_date": null,
"created_at": "2024-07-26T10:29:36.630547-07:00",
"updated_at": null,
"categories": ["hobbies"]
+11 -2
View File
@@ -1,5 +1,14 @@
---
title: 'Update Memory'
description: "Update the content or metadata of a single memory by its unique ID using the PUT endpoint."
description: "Update the content, metadata, timestamp, or expiration date of a single memory by its unique ID using the PUT endpoint."
openapi: put /v1/memories/{memory_id}/
---
---
Use this endpoint to update mutable memory fields. To make a memory expire, set `expiration_date` to a `YYYY-MM-DD` date. To make it permanent again, send `expiration_date: null`.
```python
client.update("mem_123", expiration_date="2030-01-31")
client.update("mem_123", expiration_date=None)
```
TypeScript uses `expirationDate`.
@@ -0,0 +1,5 @@
---
title: "Remove Organization Member"
description: "Remove a member from a Mem0 organization by revoking their access and clearing their assigned role."
openapi: "delete /api/v1/orgs/organizations/{org_id}/members/"
---
@@ -0,0 +1,5 @@
---
title: "Update Organization Member"
description: "Update the role assigned to an existing member of a Mem0 organization to change their permissions."
openapi: "put /api/v1/orgs/organizations/{org_id}/members/"
---
@@ -0,0 +1,5 @@
---
title: "Remove Project Member"
description: "Remove a member from a Mem0 project to revoke their access and unassign their project-level role."
openapi: "delete /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/"
---
@@ -0,0 +1,5 @@
---
title: "Update Project Member"
description: "Update the role assigned to an existing member of a Mem0 project to change their project-level permissions."
openapi: "put /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/"
---
@@ -0,0 +1,5 @@
---
title: "Update Project"
description: "Update settings and configuration for an existing Mem0 project, including name and project-level options."
openapi: "patch /api/v1/orgs/organizations/{org_id}/projects/{project_id}/"
---
@@ -0,0 +1,50 @@
---
title: "FastEmbed"
description: "Configure FastEmbed as an embedding provider in Mem0 to generate embeddings locally using ONNX-based models without a GPU."
---
You can use FastEmbed to run embedding models locally in Mem0. FastEmbed is an ONNX-based embedding library that runs efficiently on CPU without requiring a GPU or an external API key.
### Installation
```bash
pip install fastembed
```
### Usage
<CodeGroup>
```python Python
import os
from mem0 import Memory
os.environ["OPENAI_API_KEY"] = "your_api_key" # For LLM
config = {
"embedder": {
"provider": "fastembed",
"config": {
"model": "thenlper/gte-large"
}
}
}
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 thriller movies but I love sci-fi movies."},
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
]
m.add(messages, user_id="john")
```
</CodeGroup>
### Config
Here are the parameters available for configuring FastEmbed embedder:
| Parameter | Description | Default Value |
| --- | --- | --- |
| `model` | The name of the FastEmbed model to use | `thenlper/gte-large` |
| `embedding_dims` | Dimensions of the embedding model (auto-derived from the model if not set) | `None` |
+31 -1
View File
@@ -4,9 +4,12 @@ description: "Use LiteLLM as an LLM provider in Mem0 to access over 100 language
---
[Litellm](https://litellm.vercel.app/docs/) is compatible with over 100 large language models (LLMs), all using a standardized input/output format. You can explore the [available models](https://litellm.vercel.app/docs/providers) to use with Litellm. Ensure you set the `API_KEY` for the model you choose to use.
In the TypeScript SDK, run LiteLLM as a [proxy server](https://docs.litellm.ai/docs/simple_proxy) (an OpenAI-compatible endpoint) and point Mem0 at it via `LITELLM_API_BASE` (defaults to `http://localhost:4000`).
## Usage
```python
<CodeGroup>
```python Python
import os
from mem0 import Memory
@@ -33,6 +36,33 @@ messages = [
m.add(messages, user_id="alice", metadata={"category": "movies"})
```
```typescript TypeScript
import { Memory } from 'mem0ai/oss';
// Point Mem0 at your LiteLLM proxy. apiKey defaults to "sk-anything"
// (the proxy handles real auth); baseURL defaults to http://localhost:4000.
const config = {
llm: {
provider: 'litellm',
config: {
apiKey: process.env.LITELLM_API_KEY || 'sk-anything',
baseURL: process.env.LITELLM_API_BASE || 'http://localhost:4000',
model: 'gpt-5-mini',
},
},
};
const memory = new Memory(config);
const 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 thriller movies but I love sci-fi movies."},
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
];
await memory.add(messages, { userId: 'alice', metadata: { category: 'movies' } });
```
</CodeGroup>
## Config
All available parameters for the `litellm` config are present in [Master List of All Params in Config](../config).
+45 -2
View File
@@ -7,7 +7,8 @@ To use MiniMax LLM models, you have to set the `MINIMAX_API_KEY` environment var
## Usage
```python
<CodeGroup>
```python Python
import os
from mem0 import Memory
@@ -36,9 +37,37 @@ messages = [
m.add(messages, user_id="alice", metadata={"category": "movies"})
```
```typescript TypeScript
import { Memory } from 'mem0ai/oss';
const config = {
llm: {
provider: 'minimax',
config: {
apiKey: process.env.MINIMAX_API_KEY || '',
model: 'MiniMax-M2.7',
temperature: 0.2,
maxTokens: 2000,
topP: 1.0,
},
},
};
const memory = new Memory(config);
const 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 thriller movies but I love sci-fi movies." },
{ role: "assistant", content: "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future." },
];
await memory.add(messages, { userId: 'alice', metadata: { category: 'movies' } });
```
</CodeGroup>
You can also configure the API base URL in the config:
```python
<CodeGroup>
```python Python
config = {
"llm": {
"provider": "minimax",
@@ -51,6 +80,20 @@ config = {
}
```
```typescript TypeScript
const config = {
llm: {
provider: 'minimax',
config: {
model: 'MiniMax-M2.7',
baseURL: 'https://your-custom-endpoint.com',
apiKey: 'your-api-key', // alternatively to using the environment variable
},
},
};
```
</CodeGroup>
## Config
All available parameters for the `minimax` config are present in [Master List of All Params in Config](../config).
-226
View File
@@ -1,226 +0,0 @@
---
title: LLM as Reranker
description: "Use any LLM as a flexible reranker in Mem0 with custom prompts and domain-specific scoring logic."
---
<Warning>
**This page has been superseded.** Please see [LLM Reranker](/components/rerankers/models/llm_reranker) for the complete and up-to-date documentation on using LLMs for reranking.
</Warning>
LLM-based reranker provides maximum flexibility by using any Large Language Model to score document relevance. This approach allows for custom prompts and domain-specific scoring logic.
## Supported LLM Providers
Any LLM provider supported by Mem0 can be used for reranking:
- **OpenAI**: GPT-4, GPT-3.5-turbo, etc.
- **Anthropic**: Claude models
- **Together**: Open-source models
- **Groq**: Fast inference
- **Ollama**: Local models
- And more...
## Configuration
```python Python
from mem0 import Memory
config = {
"vector_store": {
"provider": "chroma",
"config": {
"collection_name": "my_memories",
"path": "./chroma_db"
}
},
"llm": {
"provider": "openai",
"config": {
"model": "gpt-4o-mini"
}
},
"reranker": {
"provider": "llm",
"config": {
"model": "gpt-4o-mini",
"provider": "openai",
"api_key": "your-openai-api-key", # or set OPENAI_API_KEY
"top_k": 5,
"temperature": 0.0
}
}
}
memory = Memory.from_config(config)
```
## Custom Scoring Prompt
You can provide a custom prompt for relevance scoring:
```python Python
custom_prompt = """You are a relevance scoring assistant. Rate how well this document answers the query.
Query: "{query}"
Document: "{document}"
Score from 0.0 to 1.0 where:
- 1.0: Perfect match, directly answers the query
- 0.8-0.9: Highly relevant, good match
- 0.6-0.7: Moderately relevant, partial match
- 0.4-0.5: Slightly relevant, limited useful information
- 0.0-0.3: Not relevant or no useful information
Provide only a single numerical score between 0.0 and 1.0."""
config["reranker"]["config"]["scoring_prompt"] = custom_prompt
```
## Usage Example
```python Python
import os
from mem0 import Memory
# Set API key
os.environ["OPENAI_API_KEY"] = "your-api-key"
# Initialize memory with LLM reranker
config = {
"vector_store": {"provider": "chroma"},
"llm": {"provider": "openai", "config": {"model": "gpt-4o-mini"}},
"reranker": {
"provider": "llm",
"config": {
"model": "gpt-4o-mini",
"provider": "openai",
"temperature": 0.0
}
}
}
memory = Memory.from_config(config)
# Add memories
messages = [
{"role": "user", "content": "I'm learning Python programming"},
{"role": "user", "content": "I find object-oriented programming challenging"},
{"role": "user", "content": "I love hiking in national parks"}
]
memory.add(messages, user_id="david")
# Search with LLM reranking
results = memory.search("What programming topics is the user studying?", filters={"user_id": "david"})
for result in results['results']:
print(f"Memory: {result['memory']}")
print(f"Vector Score: {result['score']:.3f}")
print(f"Rerank Score: {result['rerank_score']:.3f}")
print()
```
```text Output
Memory: I'm learning Python programming
Vector Score: 0.856
Rerank Score: 0.920
Memory: I find object-oriented programming challenging
Vector Score: 0.782
Rerank Score: 0.850
```
## Domain-Specific Scoring
Create specialized scoring for your domain:
```python Python
medical_prompt = """You are a medical relevance expert. Score how relevant this medical record is to the clinical query.
Clinical Query: "{query}"
Medical Record: "{document}"
Consider:
- Clinical relevance and accuracy
- Patient safety implications
- Diagnostic value
- Treatment relevance
Score from 0.0 to 1.0. Provide only the numerical score."""
config = {
"reranker": {
"provider": "llm",
"config": {
"model": "gpt-4o-mini",
"provider": "openai",
"scoring_prompt": medical_prompt,
"temperature": 0.0
}
}
}
```
## Multiple LLM Providers
Use different LLM providers for reranking:
```python Python
# Using Anthropic Claude
anthropic_config = {
"reranker": {
"provider": "llm",
"config": {
"model": "claude-3-haiku-20240307",
"provider": "anthropic",
"temperature": 0.0
}
}
}
# Using local Ollama model
ollama_config = {
"reranker": {
"provider": "llm",
"config": {
"model": "llama2:7b",
"provider": "ollama",
"temperature": 0.0
}
}
}
```
## Configuration Parameters
| Parameter | Description | Type | Default |
|-----------|-------------|------|---------|
| `model` | LLM model to use for scoring | `str` | `"gpt-4o-mini"` |
| `provider` | LLM provider name | `str` | `"openai"` |
| `api_key` | API key for the LLM provider | `str` | `None` |
| `top_k` | Maximum documents to return | `int` | `None` |
| `temperature` | Temperature for LLM generation | `float` | `0.0` |
| `max_tokens` | Maximum tokens for LLM response | `int` | `100` |
| `scoring_prompt` | Custom prompt template | `str` | Default prompt |
## Advantages
- **Maximum Flexibility**: Custom prompts for any use case
- **Domain Expertise**: Leverage LLM knowledge for specialized domains
- **Interpretability**: Understand scoring through prompt engineering
- **Multi-criteria**: Score based on multiple relevance factors
## Considerations
- **Latency**: Higher latency than specialized rerankers
- **Cost**: LLM API costs per reranking operation
- **Consistency**: May have slight variations in scoring
- **Prompt Engineering**: Requires careful prompt design
## Best Practices
1. **Temperature**: Use 0.0 for consistent scoring
2. **Prompt Design**: Be specific about scoring criteria
3. **Token Efficiency**: Keep prompts concise to reduce costs
4. **Caching**: Cache results for repeated queries when possible
5. **Fallback**: Handle API errors gracefully
+17 -83
View File
@@ -1,65 +1,32 @@
---
title: Development
description: "Guide to contributing code to Mem0, covering the issue-first workflow, the CLA, environment setup for the Python and TypeScript SDKs, and code quality checks."
description: "Guide to contributing code to Mem0, covering the fork and clone workflow, PR submission, and code quality checks."
icon: "code"
---
# Development Contributions
We strive to make contributions **easy, collaborative, and enjoyable**. Mem0 is a
polyglot monorepo containing the **Python SDK** (`mem0/`), the **TypeScript SDK**
(`mem0-ts/`), CLIs, integrations, the self-hosted server, and the docs site.
Follow the steps below for a smooth contribution process.
We strive to make contributions **easy, collaborative, and enjoyable**. Follow the steps below to ensure a smooth contribution process.
<Note>
For the complete contributor checklist, see
[CONTRIBUTING.md](https://github.com/mem0ai/mem0/blob/main/CONTRIBUTING.md) in
the repository root.
</Note>
## Submitting Your Contribution through PR
## Before You Start
### 1. Open an Issue First
**Always open an issue before opening a pull request.** This lets us discuss the
change, avoid duplicate work, and agree on the approach before you write code.
- Search [existing issues](https://github.com/mem0ai/mem0/issues) first.
- If none match, open a
[bug report](https://github.com/mem0ai/mem0/issues/new?template=bug_report.yml)
or [feature request](https://github.com/mem0ai/mem0/issues/new?template=feature_request.yml).
- For anything beyond a trivial fix, wait for a maintainer to confirm the approach.
Every pull request must link to an issue using `Closes #<issue-number>`.
### 2. Sign the Contributor License Agreement (CLA)
**We cannot merge any pull request until you have signed our Contributor License
Agreement (CLA).** When you open your first PR, the CLA bot will comment with a
link to sign — it takes less than a minute and only needs to be done once.
## Submitting Your Contribution through a PR
To contribute, follow these steps:
1. **Fork & Clone** the repository: [Mem0 on GitHub](https://github.com/mem0ai/mem0)
2. **Create a Feature Branch**: Use a dedicated branch, e.g., `feature/my-new-feature`
3. **Implement Changes**: If adding a feature or fixing a bug, be sure to:
2. **Create a Feature Branch**: Use a dedicated branch for your changes, e.g., `feature/my-new-feature`
3. **Implement Changes**: If adding a feature or fixing a bug, ensure to:
- Write necessary **tests**
- Add **documentation, docstrings, and runnable examples**
4. **Code Quality Checks**:
- Run **linting** to catch style issues
- Ensure **all tests pass**
5. **Commit** using [Conventional Commits](https://www.conventionalcommits.org/)
(`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
6. **Submit a Pull Request** against `main`, linking the issue and filling out the
PR template.
5. **Submit a Pull Request**
For detailed guidance on pull requests, refer to [GitHub's documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
---
## Python SDK (`mem0/`)
### Dependency Management
## Dependency Management
We use `hatch` as our package manager. Install it by following the [official instructions](https://hatch.pypa.io/latest/install/).
@@ -77,9 +44,13 @@ hatch -e dev_py_3_11 shell # For dev_py_3_11 (differences are mentioned in pypr
make install_all
```
---
## Development Standards
### Pre-commit Hooks
Ensure `pre-commit` is installed before contributing (hooks run ruff + isort):
Ensure `pre-commit` is installed before contributing:
```bash
pre-commit install
@@ -87,7 +58,7 @@ pre-commit install
### Linting with `ruff`
Run the linter and fix any reported issues before submitting your PR (line length **120**):
Run the linter and fix any reported issues before submitting your PR:
```bash
make lint
@@ -95,11 +66,10 @@ make lint
### Code Formatting
To maintain a consistent code style, format your code and sort imports (isort, `profile = "black"`):
To maintain a consistent code style, format your code:
```bash
make format
make sort
```
### Testing with `pytest`
@@ -114,46 +84,10 @@ make test
---
## TypeScript SDK (`mem0-ts/`)
We use [`pnpm`](https://pnpm.io/) (v10+) for all TypeScript packages. **Do NOT use
`npm` or `yarn`.**
```bash
cd mem0-ts
pnpm install
pnpm run build # tsup (CJS + ESM)
pnpm run test # jest (all tests)
pnpm run test:unit # unit tests with coverage
```
### Standards
- **Build:** tsup
- **Formatter:** Prettier
- **Tests:** jest
- Always run type checking after changes: `pnpm run typecheck` (or `tsc --noEmit`)
- Use ES module `import` syntax — never `require()`
---
## Reporting Security Issues
**Do not report security vulnerabilities through public issues or pull requests.**
Please follow our [Security Policy](https://github.com/mem0ai/mem0/blob/main/SECURITY.md)
to report them privately.
---
## Release Process
Packages are published automatically via GitHub Actions when a GitHub Release is
created with the correct tag prefix (e.g. `v*` for the Python SDK, `ts-v*` for the
TypeScript SDK). See
[CONTRIBUTING.md](https://github.com/mem0ai/mem0/blob/main/CONTRIBUTING.md#releasing)
for the full tag-prefix table and publishing details.
Currently, releases are handled manually. We aim for frequent releases, typically when new features or bug fixes are introduced.
---
Thank you for contributing to Mem0!
Thank you for contributing to Mem0!
+11 -1
View File
@@ -273,7 +273,8 @@
"components/embedders/models/lmstudio",
"components/embedders/models/together",
"components/embedders/models/langchain",
"components/embedders/models/aws_bedrock"
"components/embedders/models/aws_bedrock",
"components/embedders/models/fastembed"
]
}
]
@@ -533,6 +534,8 @@
"api-reference/organization/get-org",
"api-reference/organization/get-org-members",
"api-reference/organization/add-org-member",
"api-reference/organization/update-org-member",
"api-reference/organization/remove-org-member",
"api-reference/organization/delete-org"
]
},
@@ -545,6 +548,9 @@
"api-reference/project/get-project",
"api-reference/project/get-project-members",
"api-reference/project/add-project-member",
"api-reference/project/update-project",
"api-reference/project/update-project-member",
"api-reference/project/remove-project-member",
"api-reference/project/delete-project"
]
},
@@ -624,6 +630,10 @@
]
},
"redirects": [
{
"source": "/components/rerankers/models/llm",
"destination": "/components/rerankers/models/llm_reranker"
},
{
"source": "/migration/breaking-changes",
"destination": "/"
+1 -1
View File
@@ -73,7 +73,7 @@ client = MemoryClient()
# Define the agent
agent = Agent(
name="Personal Agent",
model=OpenAIChat(id="gpt-4"),
model=OpenAIChat(id="gpt-5-mini"),
description="You are a helpful personal agent that helps me with day to day activities."
"You can process both text and images.",
markdown=True
+2 -2
View File
@@ -43,7 +43,7 @@ OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY')
memory_client = MemoryClient()
agent = ConversableAgent(
"chatbot",
llm_config={"config_list": [{"model": "gpt-4", "api_key": OPENAI_API_KEY}]},
llm_config={"config_list": [{"model": "gpt-5-mini", "api_key": OPENAI_API_KEY}]},
code_execution_config=False,
human_input_mode="NEVER",
)
@@ -99,7 +99,7 @@ For more complex scenarios, you can create multiple agents:
manager = ConversableAgent(
"manager",
system_message="You are a manager who helps in resolving complex customer issues.",
llm_config={"config_list": [{"model": "gpt-4", "api_key": OPENAI_API_KEY}]},
llm_config={"config_list": [{"model": "gpt-5-mini", "api_key": OPENAI_API_KEY}]},
human_input_mode="NEVER"
)
+2 -1
View File
@@ -138,9 +138,10 @@ When installed via the plugin marketplace, Mem0 hooks into Claude Code's lifecyc
| Hook | Event | What it does |
|------|-------|-------------|
| **Setup** | `Setup` | Installs the mem0 SDK and dependencies (runs on init and maintenance) |
| **Session start** | `SessionStart` | Loads prior memories and displays status banner |
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message; skips short prompts |
| **Pre-tool** | `PreToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tool calls |
| **Pre-tool (3 handlers)** | `PreToolUse` | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context |
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
| **Stop** | `Stop` | Stores a session summary when the session ends |
| **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted |
+1 -1
View File
@@ -123,7 +123,7 @@ When installed via the plugin marketplace, Mem0 hooks into Codex's lifecycle to
|------|-------|-------------|
| **Session start** | `SessionStart` | Loads prior memories and displays status banner |
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message |
| **Pre-tool** | `PreToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tool calls |
| **Pre-tool (3 handlers)** | `PreToolUse` | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context |
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
| **Stop** | `Stop` | Stores a session summary when the session ends |
| **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted |
+1 -1
View File
@@ -108,7 +108,7 @@ When installed via the Cursor Marketplace, Mem0 hooks into Cursor's lifecycle:
|------|-------|-------------|
| **Session start** | `sessionStart` | Loads prior memories and displays status banner |
| **User prompt** | `beforeSubmitPrompt` | Searches relevant memories before each message; skips short prompts |
| **Pre-tool (2 handlers)** | `preToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tool calls |
| **Pre-tool (3 handlers)** | `preToolUse` | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context |
| **Post-tool (2 handlers)** | `postToolUse` | Tracks stats, scans bash errors for related memories |
| **Stop** | `stop` | Stores a session summary when the session ends |
| **Pre-compact** | `preCompact` | Stores a summary before the context is compacted |
+22 -31
View File
@@ -98,20 +98,9 @@ add_result = add_tool.invoke(add_input)
```json Output
{
"results": [
{
"memory": "Name is Alex",
"event": "ADD"
},
{
"memory": "Is a vegetarian",
"event": "ADD"
},
{
"memory": "Is allergic to nuts",
"event": "ADD"
}
]
"message": "Memory processing has been queued for background execution",
"status": "PENDING",
"event_id": "3a1b2c3d-4e5f-6789-abcd-ef0123456789"
}
```
</CodeGroup>
@@ -173,23 +162,25 @@ result = search_tool.invoke(search_input)
```
```json Output
[
{
"id": "1a75e827-7eca-45ea-8c5c-cfd43299f061",
"memory": "Name is Alex",
"user_id": "alex",
"hash": "d0fccc8fa47f7a149ee95750c37bb0ca",
"metadata": {
"food": "vegan"
},
"categories": [
"personal_details"
],
"created_at": "2024-11-27T16:53:43.276872-08:00",
"updated_at": "2024-11-27T16:53:43.276885-08:00",
"score": 0.3810526501504994
}
]
{
"results": [
{
"id": "1a75e827-7eca-45ea-8c5c-cfd43299f061",
"memory": "Name is Alex",
"user_id": "alex",
"hash": "d0fccc8fa47f7a149ee95750c37bb0ca",
"metadata": {
"food": "vegan"
},
"categories": [
"personal_details"
],
"created_at": "2024-11-27T16:53:43.276872-08:00",
"updated_at": "2024-11-27T16:53:43.276885-08:00",
"score": 0.3810526501504994
}
]
}
```
</CodeGroup>
+1 -1
View File
@@ -41,7 +41,7 @@ load_dotenv()
# MEM0_API_KEY = 'your-mem0-key' # Replace with your actual Mem0 API key
# Initialize LangChain and Mem0
llm = ChatOpenAI(model="gpt-4")
llm = ChatOpenAI(model="gpt-5-mini")
mem0 = MemoryClient()
```
+1 -1
View File
@@ -121,7 +121,7 @@ async def websocket_endpoint(websocket: WebSocket):
# LLM for response generation
llm = OpenAILLMService(
api_key=os.getenv("OPENAI_API_KEY"),
model="gpt-3.5-turbo",
model="gpt-5-mini",
system_prompt="You are a helpful assistant that remembers past conversations."
)
+8 -5
View File
@@ -26,12 +26,12 @@ Install the SDK provider and AI SDK:
npm install @mem0/vercel-ai-provider ai@^6
```
### Peer Dependencies
### Dependencies
`@mem0/vercel-ai-provider` v3.0.0 requires:
- `ai` v6+ (`^6.0.199`)
- `@ai-sdk/provider` v3+ (`^3.0.10`)
- Provider packages at v3+: `@ai-sdk/openai@^3`, `@ai-sdk/anthropic@^3`, `@ai-sdk/google@^3`, `@ai-sdk/groq@^3`, `@ai-sdk/cohere@^3`
`@mem0/vercel-ai-provider` bundles `ai`, all `@ai-sdk/*` provider packages, and `@ai-sdk/provider` as regular dependencies — you do **not** need to install them separately. The install command above (`npm install @mem0/vercel-ai-provider ai@^6`) is sufficient.
The only true peer dependency is `zod` (optional):
- `zod` v3+ (`^3.0.0`) — required only if you use Zod schemas in tool definitions
## Getting Started
@@ -305,6 +305,8 @@ These options can be passed per-request when creating a model instance:
| `rerank` | `boolean` | Enable reranking of results |
| `page` | `number` | Page number for pagination |
| `page_size` | `number` | Results per page |
| `mem0ApiKey` | `string` | Mem0 API key; overrides the `MEM0_API_KEY` env var |
| `host` | `string` | Custom Mem0 API base URL for self-hosted deployments |
## Key Features
@@ -312,6 +314,7 @@ These options can be passed per-request when creating a model instance:
- `retrieveMemories()`: Retrieves memory context for prompts as a formatted system prompt string.
- `getMemories()`: Get memories from your profile in array format.
- `addMemories()`: Adds user memories to enhance contextual responses.
- `searchMemories()`: Searches memories and returns the raw results array (semantic search rather than the full retrieval pipeline).
## Migrating from v2.x
+7 -3
View File
@@ -367,6 +367,8 @@ All API Reference docs describe Mem0 Platform REST endpoints (requires API key).
- [Get Organization](https://docs.mem0.ai/api-reference/organization/get-org) [Platform]: Use when fetching one org.
- [Get Organization Members](https://docs.mem0.ai/api-reference/organization/get-org-members) [Platform]: Use when listing org members.
- [Add Organization Member](https://docs.mem0.ai/api-reference/organization/add-org-member) [Platform]: Use when inviting a member to an org.
- [Update Organization Member](https://docs.mem0.ai/api-reference/organization/update-org-member) [Platform]: Use when updating an org member's role.
- [Remove Organization Member](https://docs.mem0.ai/api-reference/organization/remove-org-member) [Platform]: Use when removing a member from an organization.
- [Delete Organization](https://docs.mem0.ai/api-reference/organization/delete-org) [Platform]: Use when removing an org.
### Projects
@@ -375,6 +377,9 @@ All API Reference docs describe Mem0 Platform REST endpoints (requires API key).
- [Get Project](https://docs.mem0.ai/api-reference/project/get-project) [Platform]: Use when fetching one project.
- [Get Project Members](https://docs.mem0.ai/api-reference/project/get-project-members) [Platform]: Use when listing project members.
- [Add Project Member](https://docs.mem0.ai/api-reference/project/add-project-member) [Platform]: Use when inviting a member to a project.
- [Update Project](https://docs.mem0.ai/api-reference/project/update-project) [Platform]: Use when updating project settings.
- [Update Project Member](https://docs.mem0.ai/api-reference/project/update-project-member) [Platform]: Use when updating a project member's role.
- [Remove Project Member](https://docs.mem0.ai/api-reference/project/remove-project-member) [Platform]: Use when removing a member from a project.
- [Delete Project](https://docs.mem0.ai/api-reference/project/delete-project) [Platform]: Use when removing a project.
### Webhooks
@@ -460,6 +465,7 @@ Everything below is OSS-only provider configuration. Skip this entire section wh
- [LM Studio Embeddings](https://docs.mem0.ai/components/embedders/models/lmstudio) [OSS]: Use when embeddings run through LM Studio.
- [Together Embeddings](https://docs.mem0.ai/components/embedders/models/together) [OSS]: Use when embeddings run on Together.
- [LangChain Embeddings](https://docs.mem0.ai/components/embedders/models/langchain) [OSS]: Use when embeddings are wrapped behind a LangChain adapter.
- [FastEmbed](https://docs.mem0.ai/components/embedders/models/fastembed) [OSS]: Use when embeddings run locally via FastEmbed (ONNX).
### Vector Databases [OSS]
- [Vector Database Overview](https://docs.mem0.ai/components/vectordbs/overview) [OSS]: Use when choosing a vector store.
@@ -498,7 +504,5 @@ Everything below is OSS-only provider configuration. Skip this entire section wh
- [Custom Reranker Prompts](https://docs.mem0.ai/components/rerankers/custom-prompts) [OSS]: Use when rewriting reranker prompts.
- [Cohere Reranker](https://docs.mem0.ai/components/rerankers/models/cohere) [OSS]: Use for Cohere Rerank.
- [Sentence Transformer Reranker](https://docs.mem0.ai/components/rerankers/models/sentence_transformer) [OSS]: Use for local cross-encoder rerankers.
- [Hugging Face Reranker](https://docs.mem0.ai/components/rerankers/models/huggingface) [OSS]: Use for HF-hosted reranker models.
- [LLM Reranker (prompt)](https://docs.mem0.ai/components/rerankers/models/llm) [OSS]: Use when the reranker is a prompted LLM (config guide).
- [LLM Reranker](https://docs.mem0.ai/components/rerankers/models/llm_reranker) [OSS]: Use when the reranker is a prompted LLM (implementation reference).
- [Hugging Face Reranker](https://docs.mem0.ai/components/rerankers/models/huggingface) [OSS]: Use for HF-hosted reranker models.- [LLM Reranker](https://docs.mem0.ai/components/rerankers/models/llm_reranker) [OSS]: Use when the reranker is a prompted LLM (implementation reference).
- [Zero Entropy Reranker](https://docs.mem0.ai/components/rerankers/models/zero_entropy) [OSS]: Use for the Zero Entropy reranker.
+4 -4
View File
@@ -62,7 +62,7 @@ These changes produce a **+20 point improvement on LoCoMo** (71.4 → 91.6) and
|---|---|---|---|
| Constructor | `MemoryClient(api_key, org_id, project_id)` | `MemoryClient(api_key)` | Remove `org_id`, `project_id` from constructor |
| Method options | `client.add(messages, **kwargs)` | `client.add(messages, options=AddMemoryOptions(...))` | Use typed option classes (or `**kwargs` still works) |
| Removed params | `api_version`, `output_format`, `async_mode`, `filter_memories`, `expiration_date`, `keyword_search`, `force_add_only`, `batch_size`, `immutable`, `includes`, `excludes`, `enable_graph`, `org_name`, `project_name` | — | Remove from all calls |
| Removed params | `api_version`, `output_format`, `async_mode`, `filter_memories`, `keyword_search`, `force_add_only`, `batch_size`, `immutable`, `includes`, `excludes`, `enable_graph`, `org_name`, `project_name` | — | Remove from all calls |
### TypeScript Client SDK
@@ -70,7 +70,7 @@ These changes produce a **+20 point improvement on LoCoMo** (71.4 → 91.6) and
|---|---|---|---|
| Constructor | `new MemoryClient({ apiKey, organizationId, projectId })` | `new MemoryClient({ apiKey })` | Remove `organizationId`, `projectId`, `organizationName`, `projectName` |
| All params | snake_case: `user_id`, `agent_id`, `top_k` | camelCase: `userId`, `agentId`, `topK` | Rename all params to camelCase |
| Removed params | `api_version`, `output_format`, `async_mode`, `enable_graph`, `org_id`, `project_id`, `org_name`, `project_name`, `filter_memories`, `batch_size`, `force_add_only`, `immutable`, `expiration_date`, `includes`, `excludes`, `keyword_search` | — | Remove from all calls |
| Removed params | `api_version`, `output_format`, `async_mode`, `enable_graph`, `org_id`, `project_id`, `org_name`, `project_name`, `filter_memories`, `batch_size`, `force_add_only`, `immutable`, `includes`, `excludes`, `keyword_search` | — | Remove from all calls |
| Output format enum | `OutputFormat.V1`, `OutputFormat.V1_1` | Removed | v1.1 is now always used |
| API version enum | `API_VERSION.V1`, `API_VERSION.V2` | Removed | Handled internally |
@@ -423,7 +423,7 @@ These parameters have been removed across all SDKs. Remove them from your code:
**All methods:** `api_version`, `output_format`, `async_mode`, `org_name`, `project_name`, `org_id`, `project_id`
**add():** `enable_graph`, `immutable`, `expiration_date`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`
**add():** `enable_graph`, `immutable`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`
**search():** `enable_graph`
@@ -437,7 +437,7 @@ These parameters have been removed across all SDKs. Remove them from your code:
**All methods:** `OutputFormat` enum, `API_VERSION` enum
**add():** `enable_graph` / `enableGraph`, `async_mode` / `asyncMode`, `output_format` / `outputFormat`, `immutable`, `expiration_date` / `expirationDate`, `filter_memories` / `filterMemories`, `batch_size` / `batchSize`, `force_add_only` / `forceAddOnly`, `includes`, `excludes`, `keyword_search` / `keywordSearch`
**add():** `enable_graph` / `enableGraph`, `async_mode` / `asyncMode`, `output_format` / `outputFormat`, `immutable`, `filter_memories` / `filterMemories`, `batch_size` / `batchSize`, `force_add_only` / `forceAddOnly`, `includes`, `excludes`, `keyword_search` / `keywordSearch`
**search():** `enable_graph` / `enableGraph`
+2 -2
View File
@@ -215,7 +215,7 @@ client.add(messages, user_id="alice")
# async_mode and output_format removed (async by default, v1.1 always)
```
**Removed parameters:** `org_id`, `project_id`, `api_version`, `output_format`, `async_mode`, `enable_graph`, `immutable`, `expiration_date`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`, `org_name`, `project_name`
**Removed parameters:** `org_id`, `project_id`, `api_version`, `output_format`, `async_mode`, `enable_graph`, `immutable`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`, `org_name`, `project_name`
### TypeScript Client SDK
@@ -242,7 +242,7 @@ await client.search("query", {
});
```
**Removed:** `OutputFormat` enum, `API_VERSION` enum, `organizationId`, `projectId`, `organizationName`, `projectName`, `enableGraph`, `asyncMode`, `outputFormat`, `immutable`, `expirationDate`, `filterMemories`, `batchSize`, `forceAddOnly`, `includes`, `excludes`, `keywordSearch`
**Removed:** `OutputFormat` enum, `API_VERSION` enum, `organizationId`, `projectId`, `organizationName`, `projectName`, `enableGraph`, `asyncMode`, `outputFormat`, `immutable`, `filterMemories`, `batchSize`, `forceAddOnly`, `includes`, `excludes`, `keywordSearch`
<Info>
For the full list of parameter changes across all SDKs, see the [OSS migration guide](/migration/oss-v2-to-v3#removed-parameters-reference).
+1 -1
View File
@@ -130,7 +130,7 @@ memory = Memory.from_config_file("config.yaml")
</Tabs>
<Info icon="check">
Run `memory.add(["Remember my favorite cafe in Tokyo."], user_id="alex")` and then `memory.search("favorite cafe", filters={"user_id": "alex"})`. You should see the Qdrant collection populate and the reranker mark the memory as a top hit.
Run `memory.add("Remember my favorite cafe in Tokyo.", user_id="alex")` and then `memory.search("favorite cafe", filters={"user_id": "alex"})`. You should see the Qdrant collection populate and the reranker mark the memory as a top hit.
</Info>
## Tune component settings
+1 -1
View File
@@ -18,7 +18,7 @@ icon: "bolt"
</Warning>
<Note>
Working in TypeScript? The Node SDK still uses synchronous calls—use `Memory` there and rely on Python’s `AsyncMemory` when you need awaited operations.
Working in TypeScript? The OSS `Memory` class in the Node SDK (`mem0ai/oss`) is also fully async — every method returns a `Promise` and must be `await`ed. Python’s `AsyncMemory` serves the same purpose within Python async frameworks like FastAPI. Both runtimes support awaited memory operations; choose the SDK that matches your language.
</Note>
## Feature anatomy
@@ -165,8 +165,7 @@ await memory.add("Yesterday, I ordered a laptop, the order id is 12345", { userI
{"memory": "Ordered a laptop", "event": "ADD"},
{"memory": "Order ID: 12345", "event": "ADD"},
{"memory": "Order placed yesterday", "event": "ADD"}
],
"relations": []
]
}
```
</CodeGroup>
@@ -188,8 +187,7 @@ await memory.add("I like going to hikes", { userId: "user123" });
```json Output
{
"results": [],
"relations": []
"results": []
}
```
</CodeGroup>
@@ -41,6 +41,14 @@ Multimodal support lets Mem0 extract facts from images alongside regular text. A
## Configure it
<Warning>
You must set `enable_vision: True` in your LLM config for image content to be processed. Without it, image turns are silently dropped and no vision memories are created. Example:
```python
config = {"llm": {"provider": "openai", "config": {"enable_vision": True, "vision_details": "auto"}}}
client = Memory.from_config(config)
```
</Warning>
### Add image messages from URLs
<CodeGroup>
@@ -66,7 +74,7 @@ client.add(messages, user_id="alice")
```
```ts TypeScript
import { Memory } from "mem0ai";
import { Memory } from "mem0ai/oss";
const client = new Memory();
@@ -123,7 +131,7 @@ client.add(messages, user_id="alice")
```ts TypeScript
import fs from "fs";
import { Memory } from "mem0ai";
import { Memory } from "mem0ai/oss";
function encodeImage(imagePath: string) {
const buffer = fs.readFileSync(imagePath);
@@ -226,7 +234,7 @@ client.add(messages, user_id="user123")
<CodeGroup>
```python Python
from mem0 import Memory
from mem0.exceptions import InvalidImageError, FileSizeError
from mem0.exceptions import ValidationError
client = Memory()
@@ -242,16 +250,14 @@ try:
client.add(messages, user_id="user123")
print("Image processed successfully")
except InvalidImageError:
print("Invalid image format or corrupted file")
except FileSizeError:
print("Image file too large")
except ValidationError as exc:
print(f"Image validation error: {exc}")
except Exception as exc:
print(f"Unexpected error: {exc}")
```
```ts TypeScript
import { Memory } from "mem0ai";
import { Memory } from "mem0ai/oss";
const client = new Memory();
@@ -124,7 +124,7 @@ config = {
"provider": "llm_reranker",
"config": {
"provider": "openai",
"model": "gpt-4o-mini",
"model": "gpt-5-mini",
"api_key": "your-openai-api-key",
"top_k": 5
}
@@ -150,7 +150,7 @@ config = {
"llm": {
"provider": "openai",
"config": {
"model": "gpt-4",
"model": "gpt-5-mini",
"api_key": "your-openai-api-key"
}
},
+23 -1
View File
@@ -1779,6 +1779,11 @@
"type": "object",
"description": "Entity and metadata filters. Must include at least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`).",
"additionalProperties": true
},
"show_expired": {
"type": "boolean",
"default": false,
"description": "When true, include memories whose `expiration_date` has passed. Expired memories are hidden by default."
}
}
},
@@ -1977,6 +1982,12 @@
"additionalProperties": true,
"description": "User-supplied metadata to attach to each extracted memory."
},
"expiration_date": {
"type": "string",
"format": "date",
"nullable": true,
"description": "Optional expiration date in YYYY-MM-DD format. After this date, memories are hidden from search and get-all unless `show_expired` is true."
},
"custom_instructions": {
"type": "string",
"description": "Project-level instructions that guide extraction for this call."
@@ -2094,6 +2105,11 @@
"description": "Entity and metadata filters. Must include at least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`). Supports `AND`, `OR`, `NOT`, and comparison operators (`in`, `gte`, `lte`, `gt`, `lt`, `contains`, `icontains`, `ne`).",
"additionalProperties": true
},
"show_expired": {
"type": "boolean",
"default": false,
"description": "When true, include memories whose `expiration_date` has passed. Expired memories are hidden by default."
},
"top_k": {
"type": "integer",
"minimum": 1,
@@ -2432,6 +2448,12 @@
"metadata": {
"type": "object",
"description": "Additional metadata associated with the memory"
},
"expiration_date": {
"type": "string",
"format": "date",
"nullable": true,
"description": "Expiration date in YYYY-MM-DD format, or null to clear the expiration date."
}
}
}
@@ -6256,4 +6278,4 @@
}
},
"x-original-swagger-version": "2.0"
}
}
+2 -1
View File
@@ -71,7 +71,8 @@
"picomatch@<2.3.2": "^2.3.2",
"@qdrant/js-client-rest": "^1.18.0",
"uuid@<11.1.1": ">=11.1.1",
"esbuild": ">=0.28.1"
"esbuild": ">=0.28.1",
"undici@<6.27.0": ">=6.27.0 <8.0.0"
}
}
}
+6 -5
View File
@@ -13,6 +13,7 @@ overrides:
'@qdrant/js-client-rest': ^1.18.0
uuid@<11.1.1: '>=11.1.1'
esbuild: '>=0.28.1'
undici@<6.27.0: '>=6.27.0 <8.0.0'
importers:
@@ -1996,9 +1997,9 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici@6.26.0:
resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==}
engines: {node: '>=18.17'}
undici@7.28.0:
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
engines: {node: '>=20.18.1'}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -2509,7 +2510,7 @@ snapshots:
dependencies:
'@qdrant/openapi-typescript-fetch': 1.2.6
typescript: 5.9.3
undici: 6.26.0
undici: 7.28.0
'@qdrant/openapi-typescript-fetch@1.2.6': {}
@@ -4063,7 +4064,7 @@ snapshots:
undici-types@6.21.0: {}
undici@6.26.0: {}
undici@7.28.0: {}
util-deprecate@1.0.2: {}
@@ -21,3 +21,4 @@ overrides:
"@qdrant/js-client-rest": "^1.18.0"
"uuid@<11.1.1": ">=11.1.1"
"esbuild": ">=0.28.1"
"undici@<6.27.0": ">=6.27.0 <8.0.0"
@@ -4,10 +4,12 @@
import { describe, it, expect } from "vitest";
import {
safePath,
normalizeModuleUrlToPath,
loadSkill,
loadTriagePrompt,
loadCompactTriagePrompt,
} from "./skill-loader.ts";
import { fileURLToPath, pathToFileURL } from "node:url";
// ---------------------------------------------------------------------------
// safePath — path containment
@@ -74,6 +76,31 @@ describe("loadSkill path traversal", () => {
});
});
describe("normalizeModuleUrlToPath", () => {
it("normalizes raw Windows paths before fileURLToPath conversion", () => {
const rawWindowsMetaUrl = "C:\\Users\\example\\openclaw\\index.ts";
const result = normalizeModuleUrlToPath(rawWindowsMetaUrl);
// Assert the decoded property directly rather than reconstructing via the function body
expect(typeof result).toBe("string");
expect(result).not.toContain("%5C");
});
it("leaves already-correct file URLs unchanged", () => {
const fileMetaUrl = "file:///C:/Users/example/openclaw/index.ts";
const expected = fileURLToPath(fileMetaUrl);
expect(normalizeModuleUrlToPath(fileMetaUrl)).toBe(expected);
});
it.skipIf(process.platform === "win32")(
"passes POSIX absolute paths through unchanged",
() => {
const posixPath = "/home/user/openclaw/index.ts";
expect(normalizeModuleUrlToPath(posixPath)).toBe(posixPath);
},
);
});
describe("loadCompactTriagePrompt", () => {
it("keeps the core triage instructions without inlining the full skill body", () => {
const prompt = loadCompactTriagePrompt();
+10 -2
View File
@@ -4,7 +4,7 @@
*/
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
import type { SkillsConfig, CategoryConfig } from "./types.ts";
import { readText, exists } from "./fs-safe.ts";
@@ -84,6 +84,14 @@ function parseSkillFile(content: string): ParsedSkill {
};
}
/** @internal — exported for testing only */
export function normalizeModuleUrlToPath(moduleUrl: string): string {
const normalizedUrl = moduleUrl.startsWith("file:")
? moduleUrl
: pathToFileURL(moduleUrl).toString();
return fileURLToPath(normalizedUrl);
}
// ============================================================================
// Skill Loader
// ============================================================================
@@ -96,7 +104,7 @@ function resolveSkillsDir(): string {
// Strategy 1: import.meta.url (works in native ESM)
try {
const metaDir = path.dirname(fileURLToPath(import.meta.url));
const metaDir = path.dirname(normalizeModuleUrlToPath(import.meta.url));
candidates.push(path.join(metaDir, "skills"));
candidates.push(path.join(metaDir, "..", "skills"));
} catch {
@@ -74,6 +74,7 @@
"form-data@<4.0.6": ">=4.0.6",
"uuid@<11.1.1": ">=11.1.1",
"esbuild": ">=0.28.1",
"undici@<6.27.0": ">=6.27.0 <8.0.0",
"undici@>=8.0.0 <8.5.0": ">=8.5.0"
}
}
+6 -5
View File
@@ -8,6 +8,7 @@ overrides:
form-data@<4.0.6: '>=4.0.6'
uuid@<11.1.1: '>=11.1.1'
esbuild: '>=0.28.1'
undici@<6.27.0: '>=6.27.0 <8.0.0'
undici@>=8.0.0 <8.5.0: '>=8.5.0'
importers:
@@ -2351,9 +2352,9 @@ packages:
undici-types@7.24.6:
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
undici@6.26.0:
resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==}
engines: {node: '>=18.17'}
undici@7.28.0:
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
engines: {node: '>=20.18.1'}
undici@8.5.0:
resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==}
@@ -3202,7 +3203,7 @@ snapshots:
dependencies:
'@qdrant/openapi-typescript-fetch': 1.2.6
typescript: 6.0.3
undici: 6.26.0
undici: 7.28.0
'@qdrant/openapi-typescript-fetch@1.2.6': {}
@@ -4894,7 +4895,7 @@ snapshots:
undici-types@7.24.6: {}
undici@6.26.0: {}
undici@7.28.0: {}
undici@8.5.0: {}
@@ -5,4 +5,5 @@ overrides:
"form-data@<4.0.6": ">=4.0.6"
"uuid@<11.1.1": ">=11.1.1"
"esbuild": ">=0.28.1"
"undici@<6.27.0": ">=6.27.0 <8.0.0"
"undici@>=8.0.0 <8.5.0": ">=8.5.0"
+2 -1
View File
@@ -156,7 +156,8 @@
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4",
"glob@>=10.2.0 <10.5.0": "^10.5.0",
"@modelcontextprotocol/sdk": "^1.25.4",
"esbuild": ">=0.28.1"
"esbuild": ">=0.28.1",
"undici@<6.27.0": ">=6.27.0 <8.0.0"
}
}
}
+6 -5
View File
@@ -23,6 +23,7 @@ overrides:
glob@>=10.2.0 <10.5.0: ^10.5.0
'@modelcontextprotocol/sdk': ^1.25.4
esbuild: '>=0.28.1'
undici@<6.27.0: '>=6.27.0 <8.0.0'
importers:
@@ -2925,9 +2926,9 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici@6.26.0:
resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==}
engines: {node: '>=18.17'}
undici@7.28.0:
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
engines: {node: '>=20.18.1'}
update-browserslist-db@1.2.3:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
@@ -3747,7 +3748,7 @@ snapshots:
dependencies:
'@qdrant/openapi-typescript-fetch': 1.2.6
typescript: 5.5.4
undici: 6.26.0
undici: 7.28.0
'@qdrant/openapi-typescript-fetch@1.2.6': {}
@@ -6113,7 +6114,7 @@ snapshots:
undici-types@6.21.0: {}
undici@6.26.0: {}
undici@7.28.0: {}
update-browserslist-db@1.2.3(browserslist@4.28.2):
dependencies:
+1
View File
@@ -24,3 +24,4 @@ overrides:
"glob@>=10.2.0 <10.5.0": "^10.5.0"
"@modelcontextprotocol/sdk": "^1.25.4"
"esbuild": ">=0.28.1"
"undici@<6.27.0": ">=6.27.0 <8.0.0"
+6 -2
View File
@@ -281,19 +281,22 @@ export default class MemoryClient {
text,
metadata,
timestamp,
expirationDate,
}: {
text?: string;
metadata?: Record<string, any>;
timestamp?: number | string;
expirationDate?: string | null;
},
): Promise<Array<Memory>> {
if (
text === undefined &&
metadata === undefined &&
timestamp === undefined
timestamp === undefined &&
expirationDate === undefined
) {
throw new Error(
"At least one of text, metadata, or timestamp must be provided for update.",
"At least one of text, metadata, timestamp, or expirationDate must be provided for update.",
);
}
@@ -302,6 +305,7 @@ export default class MemoryClient {
if (text !== undefined) payload.text = text;
if (metadata !== undefined) payload.metadata = metadata;
if (timestamp !== undefined) payload.timestamp = timestamp;
if (expirationDate !== undefined) payload.expiration_date = expirationDate;
const payloadKeys = Object.keys(payload);
this._captureEvent("update", [payloadKeys]);
+4
View File
@@ -13,6 +13,7 @@ export interface AddMemoryOptions extends EntityOptions {
customCategories?: custom_categories[];
customInstructions?: string;
timestamp?: number;
expirationDate?: string;
structuredDataSchema?: Record<string, any>;
}
@@ -25,6 +26,7 @@ export interface SearchMemoryOptions {
latestOnly?: boolean;
fields?: string[];
categories?: string[];
showExpired?: boolean;
}
export interface GetAllMemoryOptions {
@@ -35,6 +37,7 @@ export interface GetAllMemoryOptions {
endDate?: string;
latestOnly?: boolean;
categories?: string[];
showExpired?: boolean;
}
export interface DeleteAllMemoryOptions extends EntityOptions {}
@@ -119,6 +122,7 @@ export interface Memory {
memoryType?: string;
score?: number;
metadata?: any | null;
expirationDate?: string | null;
owner?: string | null;
agentId?: string | null;
appId?: string | null;
@@ -59,6 +59,21 @@ describe("MemoryClient - add()", () => {
expect(getFetchBody(call!).user_id).toBe("user_1");
});
test("serializes expirationDate as expiration_date", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/add/", { status: 200, body: [createMockMemory()] });
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.add([{ role: "user", content: "test" }], {
userId: "u1",
expirationDate: "2030-01-31",
});
const call = findFetchCall(mock, "/v3/memories/add/", "POST");
expect(getFetchBody(call!).expiration_date).toBe("2030-01-31");
});
test("throws an error when given an empty messages array", async () => {
setupMockFetch();
@@ -176,11 +191,26 @@ describe("MemoryClient - update()", () => {
expect(body.timestamp).toBe(1710600000);
});
test("sends expirationDate as expiration_date, including null", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v1/memories/mem_123/", {
status: 200,
body: createMockMemory(),
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.update("mem_123", { expirationDate: null });
const call = findFetchCall(mock, "/v1/memories/mem_123/", "PUT");
expect(getFetchBody(call!).expiration_date).toBeNull();
});
test("throws when no fields provided", async () => {
setupMockFetch();
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await expect(client.update("mem_123", {})).rejects.toThrow(
"At least one of text, metadata, or timestamp must be provided",
"At least one of text, metadata, timestamp, or expirationDate must be provided",
);
});
});
@@ -81,6 +81,24 @@ describe("MemoryClient - search()", () => {
expect(getFetchBody(call!).latest_only).toBe(true);
});
test("serializes showExpired as show_expired", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/search/", {
status: 200,
body: { results: [] },
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.search("test", {
filters: { user_id: "u1" },
showExpired: true,
});
const call = findFetchCall(mock, "/v3/memories/search/", "POST");
expect(getFetchBody(call!).show_expired).toBe(true);
});
test("passes complex OR filters through to the API body", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/search/", {
@@ -300,4 +318,22 @@ describe("MemoryClient - getAll() entity param rejection", () => {
const call = findFetchCall(mock, "/v3/memories/", "POST");
expect(getFetchBody(call!).latest_only).toBe(true);
});
test("serializes showExpired as show_expired", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v3/memories/", {
status: 200,
body: { results: [] },
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.getAll({
filters: { user_id: "u1" },
showExpired: true,
});
const call = findFetchCall(mock, "/v3/memories/", "POST");
expect(getFetchBody(call!).show_expired).toBe(true);
});
});
+1
View File
@@ -18,6 +18,7 @@ export * from "./llms/ollama";
export * from "./llms/lmstudio";
export * from "./llms/mistral";
export * from "./llms/langchain";
export * from "./llms/litellm";
export * from "./vector_stores/base";
export * from "./vector_stores/memory";
export * from "./vector_stores/qdrant";
+39
View File
@@ -0,0 +1,39 @@
import { OpenAILLM } from "./openai";
import { LLMConfig, Message } from "../types";
import { LLMResponse } from "./base";
export class LiteLLM extends OpenAILLM {
constructor(config: LLMConfig) {
super({
...config,
apiKey: config.apiKey || process.env.LITELLM_API_KEY || "sk-anything",
baseURL:
config.baseURL ||
process.env.LITELLM_API_BASE ||
"http://localhost:4000",
model: config.model || "gpt-5-mini",
});
}
async generateResponse(
messages: Message[],
responseFormat?: { type: string },
tools?: any[],
): Promise<string | LLMResponse> {
try {
return await super.generateResponse(messages, responseFormat, tools);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`LiteLLM failed: ${message}`);
}
}
async generateChat(messages: Message[]): Promise<LLMResponse> {
try {
return await super.generateChat(messages);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`LiteLLM failed: ${message}`);
}
}
}
+43
View File
@@ -0,0 +1,43 @@
import { OpenAILLM } from "./openai";
import { LLMConfig, Message } from "../types";
import { LLMResponse } from "./base";
export class MiniMaxLLM extends OpenAILLM {
constructor(config: LLMConfig) {
const apiKey = config.apiKey || process.env.MINIMAX_API_KEY;
if (!apiKey) {
throw new Error("MiniMax API key is required");
}
super({
...config,
apiKey,
baseURL:
config.baseURL ||
process.env.MINIMAX_API_BASE ||
"https://api.minimax.io/v1",
model: config.model || "MiniMax-M2.7",
});
}
async generateResponse(
messages: Message[],
responseFormat?: { type: string },
tools?: any[],
): Promise<string | LLMResponse> {
try {
return await super.generateResponse(messages, responseFormat, tools);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`MiniMax LLM failed: ${message}`);
}
}
async generateChat(messages: Message[]): Promise<LLMResponse> {
try {
return await super.generateChat(messages);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(`MiniMax LLM failed: ${message}`);
}
}
}
+3 -1
View File
@@ -1601,7 +1601,9 @@ export class Memory {
has_agent_id: !!config.agentId,
has_run_id: !!config.runId,
});
const { userId, agentId, runId } = config;
const userId = validateAndTrimEntityId(config.userId, "userId");
const agentId = validateAndTrimEntityId(config.agentId, "agentId");
const runId = validateAndTrimEntityId(config.runId, "runId");
// Convert camelCase entity params to snake_case for filters (matches storage and search/getAll)
const filters: SearchFilters = {};
+6
View File
@@ -22,6 +22,8 @@ import { RedisDB } from "../vector_stores/redis";
import { OllamaLLM } from "../llms/ollama";
import { LMStudioLLM } from "../llms/lmstudio";
import { DeepSeekLLM } from "../llms/deepseek";
import { LiteLLM } from "../llms/litellm";
import { MiniMaxLLM } from "../llms/minimax";
import { SupabaseDB } from "../vector_stores/supabase";
import { SQLiteManager } from "../storage/SQLiteManager";
import { MemoryHistoryManager } from "../storage/MemoryHistoryManager";
@@ -85,6 +87,10 @@ export class LLMFactory {
return new LangchainLLM(config);
case "deepseek":
return new DeepSeekLLM(config);
case "litellm":
return new LiteLLM(config);
case "minimax":
return new MiniMaxLLM(config);
default:
throw new Error(`Unsupported LLM provider: ${provider}`);
}
@@ -92,6 +92,16 @@ jest.mock("../src/llms/deepseek", () => ({
.fn()
.mockImplementation((config) => ({ type: "deepseek-llm", config })),
}));
jest.mock("../src/llms/litellm", () => ({
LiteLLM: jest
.fn()
.mockImplementation((config) => ({ type: "litellm-llm", config })),
}));
jest.mock("../src/llms/minimax", () => ({
MiniMaxLLM: jest
.fn()
.mockImplementation((config) => ({ type: "minimax-llm", config })),
}));
jest.mock("../src/vector_stores/qdrant", () => ({
Qdrant: jest
@@ -206,6 +216,8 @@ describe("LLMFactory", () => {
["langchain"],
["lmstudio"],
["deepseek"],
["litellm"],
["minimax"],
])("creates LLM for provider '%s'", (provider) => {
expect(() => LLMFactory.create(provider, dummyLLMConfig)).not.toThrow();
});
+131
View File
@@ -0,0 +1,131 @@
/// <reference types="jest" />
/**
* LiteLLM — unit tests (mocked OpenAI).
*/
import { LiteLLM } from "../src/llms/litellm";
const mockCreate = jest.fn();
jest.mock("openai", () => {
return jest.fn().mockImplementation(() => ({
chat: { completions: { create: mockCreate } },
}));
});
describe("LiteLLM (unit)", () => {
beforeEach(() => mockCreate.mockClear());
it("uses default baseURL when none is provided", () => {
const llm = new LiteLLM({});
expect(llm).toBeDefined();
});
it("generateResponse() returns a text response", async () => {
mockCreate.mockResolvedValueOnce({
choices: [
{
message: {
content: "Hello, world!",
role: "assistant",
tool_calls: null,
},
},
],
});
const llm = new LiteLLM({ baseURL: "http://localhost:4000" });
const result = await llm.generateResponse([
{ role: "user", content: "Hi" },
]);
expect(mockCreate).toHaveBeenCalledTimes(1);
expect(result).toBe("Hello, world!");
});
it("generateResponse() handles tool calls", async () => {
mockCreate.mockResolvedValueOnce({
choices: [
{
message: {
content: "",
role: "assistant",
tool_calls: [
{
function: {
name: "get_weather",
arguments: '{"city": "London"}',
},
},
],
},
},
],
});
const llm = new LiteLLM({});
const result = await llm.generateResponse(
[{ role: "user", content: "What is the weather?" }],
undefined,
[{ type: "function", function: { name: "get_weather" } }],
);
expect(result).toEqual({
content: "",
role: "assistant",
toolCalls: [{ name: "get_weather", arguments: '{"city": "London"}' }],
});
});
it("generateResponse() wraps API errors with a clear message", async () => {
mockCreate.mockRejectedValueOnce(new Error("Connection refused"));
const llm = new LiteLLM({});
await expect(
llm.generateResponse([{ role: "user", content: "Hi" }]),
).rejects.toThrow("LiteLLM failed: Connection refused");
});
it("generateChat() returns LLMResponse shape", async () => {
mockCreate.mockResolvedValueOnce({
choices: [
{
message: { content: "I can help with that.", role: "assistant" },
},
],
});
const llm = new LiteLLM({});
const result = await llm.generateChat([
{ role: "user", content: "Help me" },
]);
expect(result).toEqual({
content: "I can help with that.",
role: "assistant",
});
});
it("generateChat() wraps API errors with a clear message", async () => {
mockCreate.mockRejectedValueOnce(new Error("Timeout"));
const llm = new LiteLLM({});
await expect(
llm.generateChat([{ role: "user", content: "Hi" }]),
).rejects.toThrow("LiteLLM failed: Timeout");
});
it("respects LITELLM_API_BASE env var", () => {
const original = process.env.LITELLM_API_BASE;
process.env.LITELLM_API_BASE = "http://custom-proxy:8080";
try {
const llm = new LiteLLM({});
expect(llm).toBeDefined();
} finally {
if (original !== undefined) process.env.LITELLM_API_BASE = original;
else delete process.env.LITELLM_API_BASE;
}
});
});
@@ -314,4 +314,27 @@ describe("Memory Input Validation", () => {
expect(result.results).toBeDefined();
});
});
describe("deleteAll() entity ID validation", () => {
it("should throw error when userId is whitespace-only", async () => {
await expect(memory.deleteAll({ userId: " " })).rejects.toThrow(
"Invalid userId",
);
});
it("should throw error when userId contains internal whitespace", async () => {
await expect(memory.deleteAll({ userId: "user 123" })).rejects.toThrow(
"Invalid userId: cannot contain whitespace",
);
});
it("should trim userId before listing memories", async () => {
const listSpy = jest.spyOn(memory["vectorStore"], "list");
listSpy.mockResolvedValue([[], null]);
await memory.deleteAll({ userId: " alice " });
expect(listSpy).toHaveBeenCalledWith({ user_id: "alice" });
});
});
});
+166
View File
@@ -0,0 +1,166 @@
/// <reference types="jest" />
/**
* MiniMax LLM - unit tests (mocked OpenAI).
*/
let capturedConstructorArgs: any;
const mockCreate = jest.fn();
jest.mock("openai", () => {
return jest.fn().mockImplementation((args: any) => {
capturedConstructorArgs = args;
return {
chat: { completions: { create: mockCreate } },
};
});
});
import { MiniMaxLLM } from "../src/llms/minimax";
describe("MiniMaxLLM (unit)", () => {
beforeEach(() => {
capturedConstructorArgs = undefined;
mockCreate.mockClear();
delete process.env.MINIMAX_API_KEY;
delete process.env.MINIMAX_API_BASE;
});
it("throws when no API key is provided", () => {
expect(() => new MiniMaxLLM({})).toThrow("MiniMax API key is required");
});
it("uses MiniMax defaults with an explicit API key", () => {
new MiniMaxLLM({ apiKey: "test-key" });
expect(capturedConstructorArgs).toMatchObject({
apiKey: "test-key",
baseURL: "https://api.minimax.io/v1",
});
});
it("uses environment variables when config does not provide credentials", () => {
process.env.MINIMAX_API_KEY = "env-key";
process.env.MINIMAX_API_BASE = "https://example.minimax.test/v1";
new MiniMaxLLM({});
expect(capturedConstructorArgs).toMatchObject({
apiKey: "env-key",
baseURL: "https://example.minimax.test/v1",
});
});
it("config values take precedence over environment variables", () => {
process.env.MINIMAX_API_KEY = "env-key";
process.env.MINIMAX_API_BASE = "https://env.minimax.test/v1";
new MiniMaxLLM({
apiKey: "config-key",
baseURL: "https://config.minimax.test/v1",
model: "MiniMax-M1",
});
expect(capturedConstructorArgs).toMatchObject({
apiKey: "config-key",
baseURL: "https://config.minimax.test/v1",
});
});
it("generateResponse() returns a text response", async () => {
mockCreate.mockResolvedValueOnce({
choices: [
{
message: {
content: "Hello from MiniMax",
role: "assistant",
tool_calls: null,
},
},
],
});
const llm = new MiniMaxLLM({ apiKey: "test-key" });
const result = await llm.generateResponse([
{ role: "user", content: "Hi" },
]);
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({ model: "MiniMax-M2.7" }),
);
expect(result).toBe("Hello from MiniMax");
});
it("generateResponse() handles tool calls", async () => {
mockCreate.mockResolvedValueOnce({
choices: [
{
message: {
content: "",
role: "assistant",
tool_calls: [
{
function: {
name: "search_memory",
arguments: '{"query": "alice"}',
},
},
],
},
},
],
});
const llm = new MiniMaxLLM({ apiKey: "test-key" });
const result = await llm.generateResponse(
[{ role: "user", content: "Find Alice" }],
undefined,
[{ type: "function", function: { name: "search_memory" } }],
);
expect(result).toEqual({
content: "",
role: "assistant",
toolCalls: [{ name: "search_memory", arguments: '{"query": "alice"}' }],
});
});
it("generateResponse() wraps API errors with a clear message", async () => {
mockCreate.mockRejectedValueOnce(new Error("Connection refused"));
const llm = new MiniMaxLLM({ apiKey: "test-key" });
await expect(
llm.generateResponse([{ role: "user", content: "Hi" }]),
).rejects.toThrow("MiniMax LLM failed: Connection refused");
});
it("generateChat() returns LLMResponse shape", async () => {
mockCreate.mockResolvedValueOnce({
choices: [
{
message: { content: "I can help with that.", role: "assistant" },
},
],
});
const llm = new MiniMaxLLM({ apiKey: "test-key" });
const result = await llm.generateChat([
{ role: "user", content: "Help me" },
]);
expect(result).toEqual({
content: "I can help with that.",
role: "assistant",
});
});
it("generateChat() wraps API errors with a clear message", async () => {
mockCreate.mockRejectedValueOnce(new Error("Timeout"));
const llm = new MiniMaxLLM({ apiKey: "test-key" });
await expect(
llm.generateChat([{ role: "user", content: "Hi" }]),
).rejects.toThrow("MiniMax LLM failed: Timeout");
});
});
+14 -10
View File
@@ -344,24 +344,26 @@ class MemoryClient:
Args:
memory_id: The ID of the memory to update.
options: Typed options (UpdateMemoryOptions) with text, metadata,
and/or timestamp fields.
**kwargs: Alternatively pass text, metadata, timestamp as keyword args.
timestamp, and/or expiration_date fields.
**kwargs: Alternatively pass text, metadata, timestamp, or
expiration_date as keyword args.
Returns:
Dict[str, Any]: The response from the server.
Raises:
ValueError: If none of text, metadata, or timestamp are provided.
ValueError: If none of text, metadata, timestamp, or expiration_date are provided.
Example:
>>> client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
>>> client.update("mem_123", text="Updated text")
>>> client.update("mem_123", expiration_date=None)
"""
payload = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
payload = {k: v for k, v in payload.items() if v is not None}
payload = {k: v for k, v in payload.items() if v is not None or k == "expiration_date"}
if not payload:
raise ValueError("At least one of text, metadata, or timestamp must be provided for update.")
raise ValueError("At least one of text, metadata, timestamp, or expiration_date must be provided for update.")
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "sync"})
params = self._prepare_params()
@@ -1260,24 +1262,26 @@ class AsyncMemoryClient:
Args:
memory_id: The ID of the memory to update.
options: Typed options (UpdateMemoryOptions) with text, metadata,
and/or timestamp fields.
**kwargs: Alternatively pass text, metadata, timestamp as keyword args.
timestamp, and/or expiration_date fields.
**kwargs: Alternatively pass text, metadata, timestamp, or
expiration_date as keyword args.
Returns:
Dict[str, Any]: The response from the server.
Raises:
ValueError: If none of text, metadata, or timestamp are provided.
ValueError: If none of text, metadata, timestamp, or expiration_date are provided.
Example:
>>> await client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
>>> await client.update("mem_123", text="Updated text")
>>> await client.update("mem_123", expiration_date=None)
"""
payload = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
payload = {k: v for k, v in payload.items() if v is not None}
payload = {k: v for k, v in payload.items() if v is not None or k == "expiration_date"}
if not payload:
raise ValueError("At least one of text, metadata, or timestamp must be provided for update.")
raise ValueError("At least one of text, metadata, timestamp, or expiration_date must be provided for update.")
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "async"})
params = self._prepare_params()
+4
View File
@@ -29,6 +29,7 @@ class AddMemoryOptions(BaseModel):
)
custom_instructions: Optional[str] = Field(default=None, description="Custom instructions for fact extraction")
timestamp: Optional[int] = Field(default=None, description="Unix timestamp for the memory")
expiration_date: Optional[str] = Field(default=None, description="Expiration date in YYYY-MM-DD format")
structured_data_schema: Optional[Dict[str, Any]] = Field(
default=None, description="Schema for structured data extraction"
)
@@ -50,6 +51,7 @@ class SearchMemoryOptions(BaseModel):
threshold: Optional[float] = Field(default=None, description="Minimum similarity score threshold")
fields: Optional[List[str]] = Field(default=None, description="Fields to include in the response")
categories: Optional[List[str]] = Field(default=None, description="Categories to filter by")
show_expired: Optional[bool] = Field(default=None, description="Whether to include expired memories")
class GetAllMemoryOptions(BaseModel):
@@ -71,6 +73,7 @@ class GetAllMemoryOptions(BaseModel):
default=None, description="Filter memories created on or before this date (ISO 8601)"
)
categories: Optional[List[str]] = Field(default=None, description="Categories to filter by")
show_expired: Optional[bool] = Field(default=None, description="Whether to include expired memories")
class DeleteAllMemoryOptions(BaseModel):
@@ -91,6 +94,7 @@ class UpdateMemoryOptions(BaseModel):
text: Optional[str] = Field(default=None, description="New text content for the memory")
metadata: Optional[Dict[str, Any]] = Field(default=None, description="Updated metadata")
timestamp: Optional[Union[int, float, str]] = Field(default=None, description="Updated timestamp")
expiration_date: Optional[str] = Field(default=None, description="Expiration date in YYYY-MM-DD format, or None to clear")
class ProjectUpdateOptions(BaseModel):
+153 -25
View File
@@ -9,7 +9,7 @@ import time
import uuid
import warnings
from copy import deepcopy
from datetime import datetime, timezone
from datetime import date, datetime, timezone
from typing import Any, Dict, Optional
from pydantic import ValidationError
@@ -378,9 +378,37 @@ def _entity_collection_name(provider: str, collection_name: str) -> str:
return f"{collection_name}{separator}entities"
def _normalize_expiration_date(value: Any) -> Optional[str]:
if value is None:
return None
if isinstance(value, datetime):
return value.date().isoformat()
if isinstance(value, date):
return value.isoformat()
if isinstance(value, str):
try:
return date.fromisoformat(value).isoformat()
except ValueError as exc:
raise ValueError("expiration_date must be a valid date in YYYY-MM-DD format.") from exc
raise ValueError("expiration_date must be a date string in YYYY-MM-DD format.")
def _payload_is_expired(payload: Optional[Dict[str, Any]]) -> bool:
if not payload:
return False
expiration_date = payload.get("expiration_date")
if not expiration_date:
return False
try:
return date.fromisoformat(str(expiration_date)) < datetime.now(timezone.utc).date()
except ValueError:
return False
setup_config()
logger = logging.getLogger(__name__)
_UNSET = object()
_PROJECT_UPDATE_UNSUPPORTED_ERROR = "Project updates are not supported by the OSS Memory SDK."
@@ -694,6 +722,7 @@ class Memory(MemoryBase):
run_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
timestamp: Optional[Any] = None,
expiration_date: Optional[Any] = None,
infer: bool = True,
memory_type: Optional[str] = None,
prompt: Optional[str] = None,
@@ -712,6 +741,8 @@ class Memory(MemoryBase):
run_id (str, optional): ID of the run creating the memory. Defaults to None.
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
expiration_date (Any, optional): Date in YYYY-MM-DD format. Expired memories are hidden
from search and get_all unless show_expired is True.
infer (bool, optional): If True (default), an LLM is used to extract key facts from
'messages' and decide whether to add, update, or delete related memories.
If False, 'messages' are added as raw memories directly.
@@ -737,6 +768,7 @@ class Memory(MemoryBase):
if timestamp is not None:
raise ValueError(get_temporal_feature_error_message("sync", "add", "timestamp"))
normalized_expiration_date = _normalize_expiration_date(expiration_date)
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
processed_metadata, effective_filters = _build_filters_and_metadata(
user_id=user_id,
@@ -744,6 +776,8 @@ class Memory(MemoryBase):
run_id=run_id,
input_metadata=metadata,
)
if normalized_expiration_date is not None:
processed_metadata["expiration_date"] = normalized_expiration_date
if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
raise Mem0ValidationError(
@@ -1141,6 +1175,7 @@ class Memory(MemoryBase):
"actor_id",
"role",
"attributed_to",
"expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
@@ -1169,6 +1204,7 @@ class Memory(MemoryBase):
*,
filters: Optional[Dict[str, Any]] = None,
top_k: int = 20,
show_expired: bool = False,
**kwargs,
):
"""
@@ -1179,6 +1215,7 @@ class Memory(MemoryBase):
Must contain at least one of: user_id, agent_id, run_id.
Example: filters={"user_id": "u1", "agent_id": "a1"}
top_k (int, optional): The maximum number of memories to return. Defaults to 20.
show_expired (bool, optional): Include expired memories. Defaults to False.
Returns:
dict: A dictionary containing a list of memories under the "results" key.
@@ -1217,6 +1254,7 @@ class Memory(MemoryBase):
)
limit = top_k
fetch_limit = limit if show_expired else max(limit * 4, 60)
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
keys, encoded_ids = process_telemetry_filters(effective_filters)
@@ -1224,7 +1262,7 @@ class Memory(MemoryBase):
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"}
)
all_memories_result = self._get_all_from_vector_store(effective_filters, limit)
all_memories_result = self._get_all_from_vector_store(effective_filters, fetch_limit, show_expired, limit)
if scale_threshold_notice:
display_scale_threshold_notice(self, "sync", "get_all", *scale_threshold_notice)
@@ -1232,7 +1270,7 @@ class Memory(MemoryBase):
display_first_run_notice(self, "sync", "get_all")
return {"results": all_memories_result}
def _get_all_from_vector_store(self, filters, limit):
def _get_all_from_vector_store(self, filters, limit, show_expired=False, output_limit=None):
memories_result = self.vector_store.list(filters=filters, top_k=limit)
# Handle different vector store return formats by inspecting first element
@@ -1255,11 +1293,14 @@ class Memory(MemoryBase):
"actor_id",
"role",
"attributed_to",
"expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
formatted_memories = []
for mem in actual_memories:
if not show_expired and _payload_is_expired(mem.payload):
continue
memory_item_dict = MemoryItem(
id=mem.id,
memory=mem.payload.get("data", ""),
@@ -1277,6 +1318,8 @@ class Memory(MemoryBase):
memory_item_dict["metadata"] = additional_metadata
formatted_memories.append(memory_item_dict)
if output_limit is not None and len(formatted_memories) >= output_limit:
break
return formatted_memories
@@ -1290,6 +1333,7 @@ class Memory(MemoryBase):
rerank: bool = False,
explain: bool = False,
reference_date: Optional[Any] = None,
show_expired: bool = False,
**kwargs,
):
"""
@@ -1322,6 +1366,7 @@ class Memory(MemoryBase):
rerank (bool, optional): Whether to rerank results. Defaults to False.
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
show_expired (bool, optional): Include expired memories. Defaults to False.
Returns:
dict: A dictionary containing the search results under a "results" key.
@@ -1393,7 +1438,9 @@ class Memory(MemoryBase):
)
search_start = time.perf_counter()
original_memories = self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
original_memories = self._search_vector_store(
query, effective_filters, limit, threshold, explain=explain, show_expired=show_expired
)
search_elapsed_seconds = time.perf_counter() - search_start
# Apply reranking if enabled and reranker is available
@@ -1525,7 +1572,7 @@ class Memory(MemoryBase):
return True
return False
def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False):
def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False, show_expired=False):
# Guard against None threshold (backward compat)
if threshold is None:
threshold = 0.1
@@ -1566,11 +1613,14 @@ class Memory(MemoryBase):
# Step 7: Build candidate set from semantic results
candidates = []
for mem in semantic_results:
payload = mem.payload if hasattr(mem, 'payload') else {}
if not show_expired and _payload_is_expired(payload):
continue
mem_id = str(mem.id)
candidates.append({
"id": mem_id,
"score": mem.score,
"payload": mem.payload if hasattr(mem, 'payload') else {},
"payload": payload,
})
# Step 8: Score and rank
@@ -1591,6 +1641,7 @@ class Memory(MemoryBase):
"actor_id",
"role",
"attributed_to",
"expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
@@ -1708,14 +1759,21 @@ class Memory(MemoryBase):
return memory_boosts
def update(self, memory_id, data, metadata: Optional[Dict[str, Any]] = None):
def update(
self,
memory_id,
data: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
expiration_date: Any = _UNSET,
):
"""
Update a memory by ID.
Args:
memory_id (str): ID of the memory to update.
data (str): New content to update the memory with.
data (str, optional): New content to update the memory with.
metadata (dict, optional): Metadata to update with the memory. Defaults to None.
expiration_date (Any, optional): Date in YYYY-MM-DD format, or None to clear it.
Returns:
dict: Success message indicating the memory was updated.
@@ -1726,9 +1784,19 @@ class Memory(MemoryBase):
"""
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "sync"})
existing_embeddings = {data: self.embedding_model.embed(data, "update")}
if data is None and metadata is None and expiration_date is _UNSET:
raise ValueError("At least one of data, metadata, or expiration_date must be provided.")
self._update_memory(memory_id, data, existing_embeddings, metadata)
update_metadata = deepcopy(metadata) if metadata is not None else None
if expiration_date is not _UNSET:
update_metadata = update_metadata or {}
update_metadata["expiration_date"] = _normalize_expiration_date(expiration_date)
existing_embeddings = {}
if data is not None:
existing_embeddings[data] = self.embedding_model.embed(data, "update")
self._update_memory(memory_id, data, existing_embeddings, update_metadata)
display_first_run_notice(self, "sync", "update")
return {"message": "Memory updated successfully!"}
@@ -1762,6 +1830,10 @@ class Memory(MemoryBase):
agent_id (str, optional): ID of the agent to delete memories for. Defaults to None.
run_id (str, optional): ID of the run to delete memories for. Defaults to None.
"""
user_id = _validate_and_trim_entity_id(user_id, "user_id")
agent_id = _validate_and_trim_entity_id(agent_id, "agent_id")
run_id = _validate_and_trim_entity_id(run_id, "run_id")
filters: Dict[str, Any] = {}
if user_id:
filters["user_id"] = user_id
@@ -1891,6 +1963,11 @@ class Memory(MemoryBase):
raise ValueError(f"Memory with id {memory_id} not found. Please provide a valid 'memory_id'")
prev_value = existing_memory.payload.get("data")
if data is None:
data = prev_value
if not isinstance(data, str):
raise ValueError(f"Memory with id {memory_id} does not have text content to update")
text_changed = data != prev_value
new_metadata = deepcopy(existing_memory.payload)
if metadata is not None:
@@ -1932,8 +2009,9 @@ class Memory(MemoryBase):
# Entity-store cleanup: strip this memory's id from old-text entities,
# then re-extract entities from the new text and link them back.
session_filters = {k: new_metadata[k] for k in ("user_id", "agent_id", "run_id") if new_metadata.get(k)}
self._remove_memory_from_entity_store(memory_id, session_filters)
self._link_entities_for_memory(memory_id, data, session_filters)
if text_changed:
self._remove_memory_from_entity_store(memory_id, session_filters)
self._link_entities_for_memory(memory_id, data, session_filters)
return memory_id
@@ -2280,6 +2358,7 @@ class AsyncMemory(MemoryBase):
run_id: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
timestamp: Optional[Any] = None,
expiration_date: Optional[Any] = None,
infer: bool = True,
memory_type: Optional[str] = None,
prompt: Optional[str] = None,
@@ -2295,6 +2374,8 @@ class AsyncMemory(MemoryBase):
run_id (str, optional): ID of the run creating the memory. Defaults to None.
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
expiration_date (Any, optional): Date in YYYY-MM-DD format. Expired memories are hidden
from search and get_all unless show_expired is True.
infer (bool, optional): Whether to infer the memories. Defaults to True.
memory_type (str, optional): Type of memory to create. Defaults to None.
Pass "procedural_memory" to create procedural memories.
@@ -2306,10 +2387,13 @@ class AsyncMemory(MemoryBase):
if timestamp is not None:
raise ValueError(await get_temporal_feature_error_message_async("async", "add", "timestamp"))
normalized_expiration_date = _normalize_expiration_date(expiration_date)
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
processed_metadata, effective_filters = _build_filters_and_metadata(
user_id=user_id, agent_id=agent_id, run_id=run_id, input_metadata=metadata
)
if normalized_expiration_date is not None:
processed_metadata["expiration_date"] = normalized_expiration_date
if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
raise ValueError(
@@ -2712,6 +2796,7 @@ class AsyncMemory(MemoryBase):
"actor_id",
"role",
"attributed_to",
"expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
@@ -2740,6 +2825,7 @@ class AsyncMemory(MemoryBase):
*,
filters: Optional[Dict[str, Any]] = None,
top_k: int = 20,
show_expired: bool = False,
**kwargs,
):
"""
@@ -2750,6 +2836,7 @@ class AsyncMemory(MemoryBase):
Must contain at least one of: user_id, agent_id, run_id.
Example: filters={"user_id": "u1", "agent_id": "a1"}
top_k (int, optional): The maximum number of memories to return. Defaults to 20.
show_expired (bool, optional): Include expired memories. Defaults to False.
Returns:
dict: A dictionary containing a list of memories under the "results" key.
@@ -2788,6 +2875,7 @@ class AsyncMemory(MemoryBase):
)
limit = top_k
fetch_limit = limit if show_expired else max(limit * 4, 60)
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
keys, encoded_ids = process_telemetry_filters(effective_filters)
@@ -2795,7 +2883,7 @@ class AsyncMemory(MemoryBase):
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"}
)
all_memories_result = await self._get_all_from_vector_store(effective_filters, limit)
all_memories_result = await self._get_all_from_vector_store(effective_filters, fetch_limit, show_expired, limit)
if scale_threshold_notice:
await display_scale_threshold_notice_async(self, "async", "get_all", *scale_threshold_notice)
@@ -2803,7 +2891,7 @@ class AsyncMemory(MemoryBase):
await display_first_run_notice_async(self, "async", "get_all")
return {"results": all_memories_result}
async def _get_all_from_vector_store(self, filters, limit):
async def _get_all_from_vector_store(self, filters, limit, show_expired=False, output_limit=None):
memories_result = await asyncio.to_thread(self.vector_store.list, filters=filters, top_k=limit)
# Handle different vector store return formats by inspecting first element
@@ -2826,11 +2914,14 @@ class AsyncMemory(MemoryBase):
"actor_id",
"role",
"attributed_to",
"expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
formatted_memories = []
for mem in actual_memories:
if not show_expired and _payload_is_expired(mem.payload):
continue
memory_item_dict = MemoryItem(
id=mem.id,
memory=mem.payload.get("data", ""),
@@ -2848,6 +2939,8 @@ class AsyncMemory(MemoryBase):
memory_item_dict["metadata"] = additional_metadata
formatted_memories.append(memory_item_dict)
if output_limit is not None and len(formatted_memories) >= output_limit:
break
return formatted_memories
@@ -2861,6 +2954,7 @@ class AsyncMemory(MemoryBase):
rerank: bool = False,
explain: bool = False,
reference_date: Optional[Any] = None,
show_expired: bool = False,
**kwargs,
):
"""
@@ -2893,6 +2987,7 @@ class AsyncMemory(MemoryBase):
rerank (bool, optional): Whether to rerank results. Defaults to False.
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
show_expired (bool, optional): Include expired memories. Defaults to False.
Returns:
dict: A dictionary containing the search results under a "results" key.
@@ -2968,7 +3063,9 @@ class AsyncMemory(MemoryBase):
)
search_start = time.perf_counter()
original_memories = await self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
original_memories = await self._search_vector_store(
query, effective_filters, limit, threshold, explain=explain, show_expired=show_expired
)
search_elapsed_seconds = time.perf_counter() - search_start
# Apply reranking if enabled and reranker is available
@@ -3103,7 +3200,7 @@ class AsyncMemory(MemoryBase):
return True
return False
async def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False):
async def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False, show_expired=False):
if threshold is None:
threshold = 0.1
@@ -3143,11 +3240,14 @@ class AsyncMemory(MemoryBase):
# Step 7: Build candidate set from semantic results
candidates = []
for mem in semantic_results:
payload = mem.payload if hasattr(mem, 'payload') else {}
if not show_expired and _payload_is_expired(payload):
continue
mem_id = str(mem.id)
candidates.append({
"id": mem_id,
"score": mem.score,
"payload": mem.payload if hasattr(mem, 'payload') else {},
"payload": payload,
})
# Step 8: Score and rank
@@ -3168,6 +3268,7 @@ class AsyncMemory(MemoryBase):
"actor_id",
"role",
"attributed_to",
"expiration_date",
]
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
@@ -3276,14 +3377,21 @@ class AsyncMemory(MemoryBase):
return memory_boosts
async def update(self, memory_id, data, metadata: Optional[Dict[str, Any]] = None):
async def update(
self,
memory_id,
data: Optional[str] = None,
metadata: Optional[Dict[str, Any]] = None,
expiration_date: Any = _UNSET,
):
"""
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.
data (str, optional): New content to update the memory with.
metadata (dict, optional): Metadata to update with the memory. Defaults to None.
expiration_date (Any, optional): Date in YYYY-MM-DD format, or None to clear it.
Returns:
dict: Success message indicating the memory was updated.
@@ -3294,10 +3402,20 @@ class AsyncMemory(MemoryBase):
"""
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}
if data is None and metadata is None and expiration_date is _UNSET:
raise ValueError("At least one of data, metadata, or expiration_date must be provided.")
await self._update_memory(memory_id, data, existing_embeddings, metadata)
update_metadata = deepcopy(metadata) if metadata is not None else None
if expiration_date is not _UNSET:
update_metadata = update_metadata or {}
update_metadata["expiration_date"] = _normalize_expiration_date(expiration_date)
existing_embeddings = {}
if data is not None:
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update")
existing_embeddings[data] = embeddings
await self._update_memory(memory_id, data, existing_embeddings, update_metadata)
await display_first_run_notice_async(self, "async", "update")
return {"message": "Memory updated successfully!"}
@@ -3331,6 +3449,10 @@ class AsyncMemory(MemoryBase):
agent_id (str, optional): ID of the agent to delete memories for. Defaults to None.
run_id (str, optional): ID of the run to delete memories for. Defaults to None.
"""
user_id = _validate_and_trim_entity_id(user_id, "user_id")
agent_id = _validate_and_trim_entity_id(agent_id, "agent_id")
run_id = _validate_and_trim_entity_id(run_id, "run_id")
filters = {}
if user_id:
filters["user_id"] = user_id
@@ -3456,7 +3578,7 @@ class AsyncMemory(MemoryBase):
if llm is not None:
parsed_messages = convert_to_messages(parsed_messages)
response = await asyncio.to_thread(llm.invoke, input=parsed_messages)
procedural_memory = response.content
procedural_memory = remove_code_blocks(response.content)
else:
procedural_memory = await asyncio.to_thread(self.llm.generate_response, messages=parsed_messages)
procedural_memory = remove_code_blocks(procedural_memory)
@@ -3491,6 +3613,11 @@ class AsyncMemory(MemoryBase):
raise ValueError(f"Memory with id {memory_id} not found. Please provide a valid 'memory_id'")
prev_value = existing_memory.payload.get("data")
if data is None:
data = prev_value
if not isinstance(data, str):
raise ValueError(f"Memory with id {memory_id} does not have text content to update")
text_changed = data != prev_value
new_metadata = deepcopy(existing_memory.payload)
if metadata is not None:
@@ -3534,8 +3661,9 @@ class AsyncMemory(MemoryBase):
# Entity-store cleanup: strip this memory's id from old-text entities,
# then re-extract entities from the new text and link them back.
session_filters = {k: new_metadata[k] for k in ("user_id", "agent_id", "run_id") if new_metadata.get(k)}
await self._remove_memory_from_entity_store(memory_id, session_filters)
await self._link_entities_for_memory(memory_id, data, session_filters)
if text_changed:
await self._remove_memory_from_entity_store(memory_id, session_filters)
await self._link_entities_for_memory(memory_id, data, session_filters)
return memory_id
+26 -3
View File
@@ -1418,7 +1418,30 @@ def _get_provider_memory_count(memory_instance) -> Optional[int]:
try:
col_info = getattr(vector_store, "col_info", None)
if callable(col_info):
return _extract_count(col_info())
collection_name = getattr(vector_store, "collection_name", None)
if collection_name is None:
schema = getattr(vector_store, "schema", None)
if isinstance(schema, dict):
index = schema.get("index")
if isinstance(index, dict):
collection_name = index.get("name")
if collection_name is not None:
try:
info = col_info(collection_name)
except TypeError:
info = col_info()
else:
info = col_info()
value = _extract_count(info)
if value is not None:
return value
client = getattr(vector_store, "client", None)
client_count = (
getattr(client, "count", None) if client is not None and collection_name is not None else None
)
if callable(client_count):
return _extract_count(client_count(index=collection_name))
except Exception:
return None
@@ -1430,7 +1453,7 @@ def _extract_count(info: Any) -> Optional[int]:
return None
if isinstance(info, dict):
for key in ("count", "points_count", "vectors_count", "indexed_vectors_count"):
for key in ("count", "points_count", "vectors_count", "indexed_vectors_count", "num_docs"):
value = _coerce_nonnegative_int(info.get(key), None)
if value is not None:
return value
@@ -1445,7 +1468,7 @@ def _extract_count(info: Any) -> Optional[int]:
except Exception:
return None
for attr in ("count", "points_count", "vectors_count", "indexed_vectors_count"):
for attr in ("count", "points_count", "vectors_count", "indexed_vectors_count", "num_docs"):
value = _coerce_nonnegative_int(getattr(info, attr, None), None)
if value is not None:
return value
+21 -3
View File
@@ -59,6 +59,26 @@ class HuggingFaceReranker(BaseReranker):
self.model.to(self.device)
self.model.eval()
@staticmethod
def _normalize_scores(scores: List[float]) -> List[float]:
"""Map raw cross-encoder logits into the [0, 1] range via a sigmoid.
Cross-encoder rerankers (e.g. ``BAAI/bge-reranker-*``) emit unbounded
logits; the documented way to obtain an interpretable [0, 1] relevance
score is a per-document sigmoid (``1 / (1 + e^-x)``), which preserves the
ranking order.
This replaces the previous min-max scaling, which produced *set-relative*
scores: the lowest-ranked document was always forced to 0.0, and a single
document (or any set of tied scores) collapsed to 0.0 — wrongly reporting
a result as completely irrelevant. Sigmoid scores each document on its own
merit, so those cases are handled naturally.
"""
if not scores:
return []
arr = np.asarray(scores, dtype=float)
return (1.0 / (1.0 + np.exp(-arr))).tolist()
def rerank(self, query: str, documents: List[Dict[str, Any]], top_k: int = None) -> List[Dict[str, Any]]:
"""
Rerank documents using HuggingFace cross-encoder model.
@@ -118,9 +138,7 @@ class HuggingFaceReranker(BaseReranker):
# Normalize scores if requested
if self.config.normalize:
scores = np.array(scores)
scores = (scores - scores.min()) / (scores.max() - scores.min() + 1e-8)
scores = scores.tolist()
scores = self._normalize_scores(scores)
# Combine documents with scores
doc_score_pairs = list(zip(documents, scores))
+13 -7
View File
@@ -124,11 +124,13 @@ class RedisDB(VectorStoreBase):
data = []
for vector, payload, id in zip(vectors, payloads, ids):
# Start with required fields
created_at_str = payload.get("created_at")
created_at_ts = int(datetime.fromisoformat(created_at_str).timestamp()) if created_at_str else 0
entry = {
"memory_id": id,
"hash": payload["hash"],
"memory": payload["data"],
"created_at": int(datetime.fromisoformat(payload["created_at"]).timestamp()),
"hash": payload.get("hash", ""),
"memory": payload.get("data", ""),
"created_at": created_at_ts,
"embedding": np.array(vector, dtype=np.float32).tobytes(),
}
@@ -244,12 +246,16 @@ class RedisDB(VectorStoreBase):
self.index.drop_keys(f"{self.schema['index']['prefix']}:{vector_id}")
def update(self, vector_id=None, vector=None, payload=None):
created_at_str = payload.get("created_at")
created_at_ts = int(datetime.fromisoformat(created_at_str).timestamp()) if created_at_str else 0
updated_at_str = payload.get("updated_at")
updated_at_ts = int(datetime.fromisoformat(updated_at_str).timestamp()) if updated_at_str else 0
data = {
"memory_id": vector_id,
"hash": payload["hash"],
"memory": payload["data"],
"created_at": int(datetime.fromisoformat(payload["created_at"]).timestamp()),
"updated_at": int(datetime.fromisoformat(payload["updated_at"]).timestamp()),
"hash": payload.get("hash", ""),
"memory": payload.get("data", ""),
"created_at": created_at_ts,
"updated_at": updated_at_ts,
}
# Only update embedding if vector is provided
+19 -34
View File
@@ -41,7 +41,20 @@ class OutputData(BaseModel):
payload: Dict
_VALKEY_TAG_SPECIAL = set(r',.<>{}[]"\':;!@#$%^&*()-+=~| ')
class ValkeyDB(VectorStoreBase):
@staticmethod
def _escape_tag_value(value):
"""Escape special characters in a Valkey FT.SEARCH tag filter value.
Without escaping, characters like * (wildcard) or | (OR) alter query
semantics and can bypass tenant-isolation filters.
"""
s = str(value)
return "".join(f"\\{c}" if c in _VALKEY_TAG_SPECIAL else c for c in s)
def __init__(
self,
valkey_url: str,
@@ -329,8 +342,8 @@ class ValkeyDB(VectorStoreBase):
knn_part (str): The KNN part of the query.
filters (dict, optional): Filters to apply to the search. Each key-value pair
becomes a tag filter (@key:{value}). None values are ignored.
Values are used as-is (no validation) - wildcards, lists, etc. are
passed through literally to Valkey search. Multiple filters are
Values are escaped via _escape_tag_value() before interpolation
to prevent wildcard/operator injection. Multiple filters are
combined with AND logic (space-separated).
Returns:
@@ -345,8 +358,8 @@ class ValkeyDB(VectorStoreBase):
filter_parts = []
for key, value in filters.items():
if value is not None:
# Use the correct filter syntax for Valkey
filter_parts.append(f"@{key}:{{{value}}}")
escaped = self._escape_tag_value(value)
filter_parts.append(f"@{key}:{{{escaped}}}")
# No valid filter parts
if not filter_parts:
@@ -750,34 +763,6 @@ class ValkeyDB(VectorStoreBase):
logger.exception(f"Error resetting index {self.collection_name}: {e}")
raise
def _build_list_query(self, filters=None):
"""
Build a query for listing vectors.
Args:
filters (dict, optional): Filters to apply to the list. Each key-value pair
becomes a tag filter (@key:{value}). None values are ignored.
Values are used as-is (no validation) - wildcards, lists, etc. are
passed through literally to Valkey search.
Returns:
str: The query string. Returns "*" if no valid filters provided.
"""
# Default query
q = "*"
# Add filters if provided
if filters and any(value is not None for key, value in filters.items()):
filter_conditions = []
for key, value in filters.items():
if value is not None:
filter_conditions.append(f"@{key}:{{{value}}}")
if filter_conditions:
q = " ".join(filter_conditions)
return q
def list(self, filters: dict = None, top_k: int = None) -> list:
"""
List all recent created memories from the vector store.
@@ -785,8 +770,8 @@ class ValkeyDB(VectorStoreBase):
Args:
filters (dict, optional): Filters to apply to the list. Each key-value pair
becomes a tag filter (@key:{value}). None values are ignored.
Values are used as-is without validation - wildcards, special characters,
lists, etc. are passed through literally to Valkey search.
Values are escaped via _escape_tag_value() before interpolation
to prevent wildcard/operator injection.
Multiple filters are combined with AND logic.
top_k (int, optional): Maximum number of results to return. Defaults to 1000
if not specified.
+20 -5
View File
@@ -182,14 +182,16 @@ class MemoryCreate(BaseModel):
agent_id: Optional[str] = None
run_id: Optional[str] = None
metadata: Optional[Dict[str, Any]] = None
expiration_date: Optional[str] = Field(None, description="Expiration date in YYYY-MM-DD format.")
infer: Optional[bool] = Field(None, description="Whether to extract facts from messages. Defaults to True.")
memory_type: Optional[str] = Field(None, description="Type of memory to store (e.g. 'core').")
prompt: Optional[str] = Field(None, description="Custom prompt to use for fact extraction.")
class MemoryUpdate(BaseModel):
text: str = Field(..., description="New content to update the memory with.")
text: Optional[str] = Field(None, description="New content to update the memory with.")
metadata: Optional[Dict[str, Any]] = Field(None, description="Metadata to update.")
expiration_date: Optional[str] = Field(None, description="Expiration date in YYYY-MM-DD format, or null to clear.")
class SearchRequest(BaseModel):
@@ -201,6 +203,7 @@ class SearchRequest(BaseModel):
top_k: Optional[int] = Field(None, description="Maximum number of results to return.")
threshold: Optional[float] = Field(None, description="Minimum similarity score for results.")
explain: Optional[bool] = Field(None, description="Include score details for each search result.")
show_expired: Optional[bool] = Field(None, description="Include expired memories.")
class GenerateInstructionsRequest(BaseModel):
@@ -379,7 +382,7 @@ def add_memory(memory_create: MemoryCreate, _auth=Depends(verify_auth)):
ALL_MEMORIES_LIMIT = 1000
_RESERVED_PAYLOAD_KEYS = {"data", "user_id", "agent_id", "run_id", "hash", "created_at", "updated_at"}
_RESERVED_PAYLOAD_KEYS = {"data", "user_id", "agent_id", "run_id", "hash", "created_at", "updated_at", "expiration_date"}
def _serialize_memory(row: Any) -> Dict[str, Any]:
@@ -391,6 +394,7 @@ def _serialize_memory(row: Any) -> Dict[str, Any]:
"agent_id": payload.get("agent_id"),
"run_id": payload.get("run_id"),
"hash": payload.get("hash"),
"expiration_date": payload.get("expiration_date"),
"metadata": {k: v for k, v in payload.items() if k not in _RESERVED_PAYLOAD_KEYS},
"created_at": payload.get("created_at"),
"updated_at": payload.get("updated_at"),
@@ -410,6 +414,7 @@ def get_all_memories(
run_id: Optional[str] = None,
agent_id: Optional[str] = None,
top_k: Optional[int] = Query(None, ge=0, le=ALL_MEMORIES_LIMIT),
show_expired: bool = Query(False),
_auth=Depends(verify_auth),
):
"""Retrieve stored memories. Lists all memories when no identifier is provided (admin only)."""
@@ -418,6 +423,7 @@ def get_all_memories(
auth_type = getattr(request.state, "auth_type", "none")
if _auth is not None and _auth.role != "admin" and auth_type not in {"admin_api_key", "disabled"}:
raise HTTPException(status_code=403, detail="Admin role required to list all memories.")
# Admin all-memory listing is intentionally raw; scoped get_all below applies expiry visibility.
return _list_all_memories(limit=top_k if top_k is not None else ALL_MEMORIES_LIMIT)
filters = {
k: v for k, v in {"user_id": user_id, "run_id": run_id, "agent_id": agent_id}.items() if v is not None
@@ -425,6 +431,7 @@ def get_all_memories(
params = {"filters": filters}
if top_k is not None:
params["top_k"] = top_k
params["show_expired"] = show_expired
return get_memory_instance().get_all(**params)
except HTTPException:
raise
@@ -465,6 +472,8 @@ def search_memories(search_req: SearchRequest, _auth=Depends(verify_auth)):
params["threshold"] = search_req.threshold
if search_req.explain is not None:
params["explain"] = search_req.explain
if search_req.show_expired is not None:
params["show_expired"] = search_req.show_expired
return get_memory_instance().search(query=search_req.query, filters=filters, **params)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@@ -478,9 +487,15 @@ def search_memories(search_req: SearchRequest, _auth=Depends(verify_auth)):
def update_memory(memory_id: str, updated_memory: MemoryUpdate, _auth=Depends(verify_auth)):
"""Update an existing memory."""
try:
return get_memory_instance().update(
memory_id=memory_id, data=updated_memory.text, metadata=updated_memory.metadata
)
fields_set = getattr(updated_memory, "model_fields_set", getattr(updated_memory, "__fields_set__", set()))
params = {"memory_id": memory_id}
if "text" in fields_set:
params["data"] = updated_memory.text
if "metadata" in fields_set:
params["metadata"] = updated_memory.metadata
if "expiration_date" in fields_set:
params["expiration_date"] = updated_memory.expiration_date
return get_memory_instance().update(**params)
except (ValueError, Mem0ValidationError) as e:
raise _client_error(e)
except Exception:
+27
View File
@@ -167,6 +167,33 @@ class TestAsyncUpdate:
"test_id", "Updated memory", {"Updated memory": [0.1, 0.2, 0.3]}, {}
)
@pytest.mark.asyncio
async def test_async_update_can_change_expiration_date_without_changing_text(self, mock_async_memory, mocker):
mock_async_memory.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
mock_async_memory.vector_store.get = Mock(
return_value=Mock(
payload={
"data": "Existing memory",
"user_id": "test_user",
"created_at": "2026-01-01T00:00:00+00:00",
"expiration_date": "2026-12-31",
}
)
)
mock_async_memory.vector_store.update = Mock()
mock_async_memory.db.add_history = Mock()
mock_async_memory._remove_memory_from_entity_store = mocker.AsyncMock()
mock_async_memory._link_entities_for_memory = mocker.AsyncMock()
result = await mock_async_memory.update("test_id", expiration_date="2999-01-01")
assert result["message"] == "Memory updated successfully!"
payload = mock_async_memory.vector_store.update.call_args.kwargs["payload"]
assert payload["data"] == "Existing memory"
assert payload["expiration_date"] == "2999-01-01"
mock_async_memory._remove_memory_from_entity_store.assert_not_called()
mock_async_memory._link_entities_for_memory.assert_not_called()
@pytest.mark.asyncio
class TestAsyncAddToVectorStoreErrors:
+33 -1
View File
@@ -1403,6 +1403,30 @@ def test_scale_threshold_provider_count_helpers_are_safe():
def col_info(self):
return {"count": 2300}
class RedisInfoStore:
def __init__(self):
self.schema = {"index": {"name": "test_collection"}}
def count(self):
raise RuntimeError("count unavailable")
def col_info(self, name):
assert name == self.schema["index"]["name"]
return {"index_name": "test_collection", "num_docs": 2300}
class SearchMetadataStore:
def __init__(self):
self.collection_name = "test_collection"
self.client = MagicMock()
self.client.count.return_value = {"count": 2400}
def count(self):
raise RuntimeError("count unavailable")
def col_info(self, name):
assert name == self.collection_name
return {"test_collection": {"settings": {"index": {}}}}
memory = MagicMock()
memory.vector_store = CountStore()
assert notices._get_provider_memory_count(memory) == 2100
@@ -1410,7 +1434,15 @@ def test_scale_threshold_provider_count_helpers_are_safe():
memory.vector_store = FallbackStore()
assert notices._get_provider_memory_count(memory) == 2300
assert notices._extract_count({"points_count": 2400}) == 2400
memory.vector_store = RedisInfoStore()
assert notices._get_provider_memory_count(memory) == 2300
search_store = SearchMetadataStore()
memory.vector_store = search_store
assert notices._get_provider_memory_count(memory) == 2400
search_store.client.count.assert_called_once_with(index="test_collection")
assert notices._extract_count({"points_count": 2500}) == 2500
assert notices._extract_count(Info()) == 2200
assert notices._extract_count({"count": -1}) is None
@@ -0,0 +1,51 @@
"""Unit tests for HuggingFaceReranker score normalization.
These exercise the pure ``_normalize_scores`` helper directly, so they do not
require ``transformers`` / ``torch`` to be installed.
"""
import math
import pytest
from mem0.reranker.huggingface_reranker import HuggingFaceReranker
def _sigmoid(x):
return 1.0 / (1.0 + math.exp(-x))
class TestHuggingFaceNormalizeScores:
def test_logits_mapped_via_sigmoid(self):
scores = HuggingFaceReranker._normalize_scores([2.0, 8.0, 5.0])
assert scores == pytest.approx([_sigmoid(2.0), _sigmoid(8.0), _sigmoid(5.0)])
def test_output_bounded_between_zero_and_one(self):
for s in HuggingFaceReranker._normalize_scores([-12.0, -1.0, 0.0, 3.0, 15.0]):
assert 0.0 <= s <= 1.0
def test_sigmoid_preserves_ranking_order(self):
raw = [1.0, -4.0, 9.0, 2.5]
normalized = HuggingFaceReranker._normalize_scores(raw)
# argsort of raw and normalized must match — sigmoid is monotonic.
assert sorted(range(len(raw)), key=lambda i: raw[i]) == sorted(
range(len(normalized)), key=lambda i: normalized[i]
)
def test_single_score_not_collapsed_to_zero(self):
# Regression: a lone document used to normalize to ~0.0 under min-max.
# A positive logit must now yield a clearly-relevant score (> 0.5).
(score,) = HuggingFaceReranker._normalize_scores([4.2])
assert score == pytest.approx(_sigmoid(4.2))
assert score > 0.5
def test_tied_scores_not_collapsed_to_zero(self):
# Regression: tied candidates all collapsed to ~0.0 under min-max.
scores = HuggingFaceReranker._normalize_scores([3.0, 3.0, 3.0])
assert scores == pytest.approx([_sigmoid(3.0)] * 3)
def test_zero_logit_maps_to_half(self):
assert HuggingFaceReranker._normalize_scores([0.0]) == pytest.approx([0.5])
def test_empty_scores(self):
assert HuggingFaceReranker._normalize_scores([]) == []
+47
View File
@@ -51,6 +51,20 @@ class TestSearchEntityParamRejection:
json={"query": "test query", "filters": {"user_id": "u1"}},
)
def test_search_passes_show_expired(self, mock_memory_client):
"""search() should pass show_expired to the API."""
mock_response = MagicMock()
mock_response.json.return_value = {"results": []}
mock_response.raise_for_status.return_value = None
mock_memory_client.client.post.return_value = mock_response
mock_memory_client.search("test query", filters={"user_id": "u1"}, show_expired=True)
mock_memory_client.client.post.assert_called_once_with(
"/v3/memories/search/",
json={"query": "test query", "filters": {"user_id": "u1"}, "show_expired": True},
)
def test_search_rejects_user_id_kwarg(self, mock_memory_client):
"""search() should reject user_id as top-level kwarg."""
with pytest.raises(ValueError, match=r"user_id"):
@@ -100,6 +114,39 @@ class TestGetAllEntityParamRejection:
with pytest.raises(ValueError, match=r"run_id"):
mock_memory_client.get_all(run_id="r1")
def test_get_all_passes_show_expired(self, mock_memory_client):
"""get_all() should pass show_expired to the API."""
mock_response = MagicMock()
mock_response.json.return_value = {"results": []}
mock_response.raise_for_status.return_value = None
mock_memory_client.client.post.return_value = mock_response
mock_memory_client.get_all(filters={"user_id": "u1"}, show_expired=True)
mock_memory_client.client.post.assert_called_once_with(
"/v3/memories/",
json={"filters": {"user_id": "u1"}, "show_expired": True},
)
class TestUpdateExpirationDate:
"""Tests for update expiration_date payload handling."""
def test_update_preserves_null_expiration_date(self, mock_memory_client):
"""update() should send expiration_date=None so the API can clear it."""
mock_response = MagicMock()
mock_response.json.return_value = {"id": "mem_1", "expiration_date": None}
mock_response.raise_for_status.return_value = None
mock_memory_client.client.put.return_value = mock_response
mock_memory_client.update("mem_1", expiration_date=None)
mock_memory_client.client.put.assert_called_once_with(
"/v1/memories/mem_1/",
json={"expiration_date": None},
params={},
)
class TestFilterOperatorPassthrough:
"""Tests that AND/OR/NOT filter operators are passed through to the API."""
+129 -1
View File
@@ -65,6 +65,24 @@ def test_add(memory_instance):
)
def test_add_stores_expiration_date(memory_instance):
memory_instance._add_to_vector_store = Mock(return_value=[{"memory": "Test memory", "event": "ADD"}])
memory_instance.add(
messages=[{"role": "user", "content": "Test message"}],
user_id="test_user",
expiration_date="2999-01-01",
)
memory_instance._add_to_vector_store.assert_called_once_with(
[{"role": "user", "content": "Test message"}],
{"user_id": "test_user", "expiration_date": "2999-01-01"},
{"user_id": "test_user"},
True,
prompt=None,
)
def test_get(memory_instance):
mock_memory = Mock(
id="test_id",
@@ -117,6 +135,39 @@ def test_search(memory_instance):
)
def test_search_hides_expired_memories_by_default(memory_instance):
mock_memories = [
Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}, score=0.9),
Mock(id="2", payload={"data": "Active memory", "user_id": "test_user", "expiration_date": "2999-01-01"}, score=0.8),
]
memory_instance.vector_store.search = Mock(return_value=mock_memories)
memory_instance.vector_store.keyword_search = Mock(return_value=None)
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
with patch("mem0.memory.main.lemmatize_for_bm25", return_value="test query"), \
patch("mem0.memory.main.extract_entities", return_value=[]):
result = memory_instance.search("test query", filters={"user_id": "test_user"})
assert [memory["memory"] for memory in result["results"]] == ["Active memory"]
assert result["results"][0]["expiration_date"] == "2999-01-01"
def test_search_can_show_expired_memories(memory_instance):
mock_memories = [
Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}, score=0.9),
Mock(id="2", payload={"data": "Active memory", "user_id": "test_user"}, score=0.8),
]
memory_instance.vector_store.search = Mock(return_value=mock_memories)
memory_instance.vector_store.keyword_search = Mock(return_value=None)
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
with patch("mem0.memory.main.lemmatize_for_bm25", return_value="test query"), \
patch("mem0.memory.main.extract_entities", return_value=[]):
result = memory_instance.search("test query", filters={"user_id": "test_user"}, show_expired=True)
assert [memory["memory"] for memory in result["results"]] == ["Expired memory", "Active memory"]
def test_update(memory_instance):
memory_instance.embedding_model = Mock()
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
@@ -161,6 +212,42 @@ def test_update_with_empty_metadata(memory_instance):
)
@pytest.mark.parametrize(
("expiration_date", "expected_expiration_date"),
[
("2999-01-01", "2999-01-01"),
(None, None),
],
)
def test_update_can_change_expiration_date_without_changing_text(
memory_instance, expiration_date, expected_expiration_date
):
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
memory_instance.vector_store.get = Mock(
return_value=Mock(
payload={
"data": "Existing memory",
"user_id": "test_user",
"created_at": "2026-01-01T00:00:00+00:00",
"expiration_date": "2026-12-31",
}
)
)
memory_instance.vector_store.update = Mock()
memory_instance.db.add_history = Mock()
memory_instance._remove_memory_from_entity_store = Mock()
memory_instance._link_entities_for_memory = Mock()
result = memory_instance.update("test_id", expiration_date=expiration_date)
assert result["message"] == "Memory updated successfully!"
payload = memory_instance.vector_store.update.call_args.kwargs["payload"]
assert payload["data"] == "Existing memory"
assert payload["expiration_date"] == expected_expiration_date
memory_instance._remove_memory_from_entity_store.assert_not_called()
memory_instance._link_entities_for_memory.assert_not_called()
def test_delete(memory_instance):
memory_instance._delete_memory = Mock()
@@ -200,7 +287,30 @@ def test_get_all(memory_instance):
assert result["results"][0]["memory"] == "Memory 1"
assert result["results"][0]["user_id"] == "test_user"
memory_instance.vector_store.list.assert_called_once_with(filters={"user_id": "test_user"}, top_k=20)
def test_get_all_hides_expired_memories_by_default(memory_instance):
mock_memories = [
Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}),
Mock(id="2", payload={"data": "Active memory", "user_id": "test_user", "expiration_date": "2999-01-01"}),
]
memory_instance.vector_store.list = Mock(return_value=(mock_memories, None))
result = memory_instance.get_all(filters={"user_id": "test_user"})
assert [memory["memory"] for memory in result["results"]] == ["Active memory"]
assert result["results"][0]["expiration_date"] == "2999-01-01"
def test_get_all_can_show_expired_memories(memory_instance):
mock_memories = [
Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}),
Mock(id="2", payload={"data": "Active memory", "user_id": "test_user"}),
]
memory_instance.vector_store.list = Mock(return_value=(mock_memories, None))
result = memory_instance.get_all(filters={"user_id": "test_user"}, show_expired=True)
assert [memory["memory"] for memory in result["results"]] == ["Expired memory", "Active memory"]
def test_no_telemetry_vector_store_when_disabled():
@@ -286,6 +396,24 @@ class TestEntityIdValidation:
with pytest.raises(ValueError, match="Invalid user_id.*cannot contain whitespace"):
memory_instance.add("test message", user_id="user 123")
def test_delete_all_rejects_whitespace_only_user_id(self, memory_instance):
"""delete_all should reject whitespace-only user_id."""
with pytest.raises(ValueError, match="Invalid user_id.*cannot be empty"):
memory_instance.delete_all(user_id=" ")
def test_delete_all_rejects_internal_whitespace_user_id(self, memory_instance):
"""delete_all should reject user_id with internal whitespace."""
with pytest.raises(ValueError, match="Invalid user_id.*cannot contain whitespace"):
memory_instance.delete_all(user_id="user 123")
def test_delete_all_trims_user_id_before_list(self, memory_instance):
"""delete_all should trim leading/trailing whitespace on entity IDs."""
memory_instance.vector_store.list = Mock(return_value=([], None))
memory_instance.delete_all(user_id=" alice ")
memory_instance.vector_store.list.assert_called_once_with(filters={"user_id": "alice"})
class TestSearchParamValidation:
"""Tests for search parameter validation (threshold and top_k)."""
+33
View File
@@ -1496,3 +1496,36 @@ class TestAsyncDeleteAllEntityRace:
mock_entity_store.delete.assert_called_once_with(vector_id="entity-alice")
assert mock_vector_store.delete.call_count == 2
@pytest.mark.asyncio
@patch("mem0.memory.main.VectorStoreFactory")
@patch("mem0.memory.main.EmbedderFactory")
@patch("mem0.memory.main.LlmFactory")
async def test_async_procedural_memory_langchain_strips_code_blocks(mock_llm_factory, mock_emb, mock_vs):
"""Regression #5710: async LangChain path must call remove_code_blocks()."""
mock_vs.return_value = MagicMock()
mock_emb.return_value = MagicMock()
mock_emb.return_value.embed.return_value = [0.1] * 1536
mock_llm_factory.return_value = MagicMock()
from mem0.memory.main import AsyncMemory
config = MemoryConfig()
memory = AsyncMemory(config)
memory.vector_store = MagicMock()
memory.vector_store.insert = MagicMock()
mock_langchain_llm = MagicMock()
mock_response = MagicMock()
mock_response.content = '```json\n{"key": "value"}\n```'
mock_langchain_llm.invoke.return_value = mock_response
messages = [{"role": "user", "content": "test"}]
metadata = {"user_id": "test_user"}
await memory._create_procedural_memory(messages, metadata=metadata, llm=mock_langchain_llm)
insert_call = memory.vector_store.insert.call_args
stored_data = insert_call[1]["payloads"][0]["data"]
assert "```" not in stored_data
+13 -5
View File
@@ -564,12 +564,20 @@ class TestUpdateMemory:
resp = client.put("/memories/mem-1", json={"text": "Likes tennis"})
assert resp.status_code == 200
_, kwargs = mock_memory.update.call_args
assert kwargs["metadata"] is None
assert "metadata" not in kwargs
def test_missing_text_returns_422(self, client):
"""text is required — omitting it should fail validation."""
resp = client.put("/memories/mem-1", json={"metadata": {"k": "v"}})
assert resp.status_code == 422
def test_expiration_date_forwarded_without_text(self, client, mock_memory):
resp = client.put("/memories/mem-1", json={"expiration_date": "2999-01-01"})
assert resp.status_code == 200
_, kwargs = mock_memory.update.call_args
assert kwargs["expiration_date"] == "2999-01-01"
assert "data" not in kwargs
def test_null_expiration_date_forwarded_for_clear(self, client, mock_memory):
resp = client.put("/memories/mem-1", json={"expiration_date": None})
assert resp.status_code == 200
_, kwargs = mock_memory.update.call_args
assert kwargs["expiration_date"] is None
def test_dict_not_passed_as_data(self, client, mock_memory):
"""Regression test for #3933: the entire dict must NOT be passed as data."""
+48
View File
@@ -180,3 +180,51 @@ def test_get_returns_none_for_missing_id():
assert db.get("missing_id") is None
mock_index.fetch.assert_called_once_with("missing_id")
def test_insert_entity_payload_without_hash_and_created_at():
"""insert() must not crash on entity payloads that lack hash/created_at."""
db, mock_index = _make_redis_db()
entity_payload = {
"data": "OpenAI",
"entity_type": "organization",
"linked_memory_ids": ["mem-1"],
"user_id": "test_user",
}
db.insert(
vectors=[[0.1, 0.2, 0.3]],
payloads=[entity_payload],
ids=["entity-1"],
)
mock_index.load.assert_called_once()
data = mock_index.load.call_args[0][0]
assert data[0]["memory_id"] == "entity-1"
assert data[0]["memory"] == "OpenAI"
assert data[0]["hash"] == ""
assert data[0]["created_at"] == 0
def test_update_entity_payload_without_hash_and_timestamps():
"""update() must not crash on entity payloads that lack hash/created_at/updated_at."""
db, mock_index = _make_redis_db()
entity_payload = {
"data": "OpenAI",
"entity_type": "organization",
"linked_memory_ids": ["mem-1"],
"user_id": "test_user",
}
db.update(vector_id="entity-1", vector=[0.1, 0.2, 0.3], payload=entity_payload)
mock_index.load.assert_called_once()
call_kwargs = mock_index.load.call_args
data_dict = call_kwargs[1]["data"][0] if "data" in call_kwargs[1] else call_kwargs[0][0][0]
assert data_dict["memory_id"] == "entity-1"
assert data_dict["memory"] == "OpenAI"
assert data_dict["hash"] == ""
assert data_dict["created_at"] == 0
assert data_dict["updated_at"] == 0
+25 -20
View File
@@ -490,26 +490,6 @@ def test_reset(valkey_db, mock_valkey_client):
assert result is True
def test_build_list_query(valkey_db):
"""Test building a list query with and without filters."""
# Test without filters
query = valkey_db._build_list_query(None)
assert query == "*"
# Test with empty filters
query = valkey_db._build_list_query({})
assert query == "*"
# Test with filters
query = valkey_db._build_list_query({"user_id": "test_user"})
assert query == "@user_id:{test_user}"
# Test with multiple filters
query = valkey_db._build_list_query({"user_id": "test_user", "agent_id": "test_agent"})
assert "@user_id:{test_user}" in query
assert "@agent_id:{test_agent}" in query
def test_process_document_fields(valkey_db):
"""Test processing document fields from hash results."""
# Create a mock result with all fields
@@ -1061,3 +1041,28 @@ def test_build_index_schema_indexes_memory_as_text(valkey_db):
)
# And it must not be declared as TAG.
assert ["memory", "TAG"] != cmd[memory_idx : memory_idx + 2]
def test_escape_tag_value_wildcards(valkey_db):
"""Wildcard characters in filter values must be escaped to prevent query injection."""
assert "\\*" in valkey_db._escape_tag_value("*")
assert "\\|" in valkey_db._escape_tag_value("a|b")
def test_build_search_query_escapes_filter_values(valkey_db):
"""_build_search_query must escape special chars in filter values."""
knn_part = "[KNN 5 @embedding $vec_param AS vector_score]"
query = valkey_db._build_search_query(knn_part, {"user_id": "*"})
assert "\\*" in query
assert "@user_id:{\\*}" in query
def test_escape_tag_value_normal_strings(valkey_db):
"""Normal alphanumeric filter values must pass through unchanged."""
assert valkey_db._escape_tag_value("alice") == "alice"
assert valkey_db._escape_tag_value("user123") == "user123"
def test_escape_tag_value_hyphenated_user_id(valkey_db):
"""Hyphenated user IDs must have the hyphen escaped for exact-match."""
assert valkey_db._escape_tag_value("user-123") == r"user\-123"