Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5b04e70fa | |||
| 7e7682a06d | |||
| f38608fb50 | |||
| fb11cdffbb | |||
| ee600705c2 | |||
| f4ccef5157 | |||
| 08da741a31 | |||
| dc857e43ca | |||
| e4efdd2e29 | |||
| a87c9ce367 | |||
| d258b638ef | |||
| bbbfcfea07 | |||
| fbef369b91 | |||
| a7ed68e697 | |||
| 8a92cf0306 | |||
| 0fbbb2f525 | |||
| 818c2981b7 | |||
| 1f66aadfa3 | |||
| b91c745fbc | |||
| af70668308 | |||
| 890473f891 | |||
| d2ff83cf72 | |||
| 0e02effaf7 | |||
| 9269a0ad6e | |||
| 3d06006f36 | |||
| 7e056281e5 | |||
| b5789d4afe | |||
| 6bb1d328ad | |||
| ac296f7534 | |||
| ac8f862ff7 | |||
| b33fa5427c | |||
| 5d573dd2ae | |||
| 98dbf90864 |
@@ -12,7 +12,7 @@
|
||||
"name": "mem0",
|
||||
"source": "./integrations/mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
|
||||
"version": "0.2.10"
|
||||
"version": "0.2.11"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"name": "mem0",
|
||||
"source": "./integrations/mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
|
||||
"version": "0.2.10"
|
||||
"version": "0.2.11"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+134
-47
@@ -1,72 +1,157 @@
|
||||
# Contributing to mem0
|
||||
# Contributing to Mem0
|
||||
|
||||
Let us make contribution easy, collaborative and fun.
|
||||
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.
|
||||
|
||||
## Submit your Contribution through PR
|
||||
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).
|
||||
|
||||
To make a contribution, follow these steps:
|
||||
## Before You Start
|
||||
|
||||
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
|
||||
### 1. Open an Issue First
|
||||
|
||||
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).
|
||||
**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.
|
||||
|
||||
- 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.
|
||||
|
||||
### 📦 Development Environment
|
||||
Every pull request must link to an issue using `Closes #<issue-number>`.
|
||||
|
||||
We use `hatch` for managing development environments. To set up:
|
||||
### 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.**
|
||||
|
||||
```bash
|
||||
# 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
|
||||
# Activate a dev environment (3.9 / 3.10 / 3.11 / 3.12)
|
||||
hatch shell dev_py_3_11
|
||||
|
||||
# The environment will automatically install all dev dependencies
|
||||
# Run tests within the activated shell:
|
||||
make test
|
||||
```
|
||||
|
||||
### 📌 Pre-commit
|
||||
|
||||
To ensure our standards, make sure to install pre-commit before starting to contribute.
|
||||
|
||||
```bash
|
||||
# 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)
|
||||
make test
|
||||
```
|
||||
|
||||
### 🧪 Testing
|
||||
- **Linter / formatter:** Ruff (line length **120**)
|
||||
- **Import sorting:** isort (`profile = "black"`)
|
||||
- **Tests:** pytest (in `tests/`)
|
||||
|
||||
We use `pytest` to test our code across multiple Python versions. You can run tests using:
|
||||
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`.**
|
||||
|
||||
```bash
|
||||
# Run tests with default Python version
|
||||
make test
|
||||
cd mem0-ts
|
||||
pnpm install
|
||||
|
||||
# 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
|
||||
|
||||
# When using hatch shells, run tests with:
|
||||
make test # After activating a shell with hatch shell test_XX
|
||||
pnpm run build # tsup (CJS + ESM)
|
||||
pnpm run test # jest (all tests)
|
||||
pnpm run test:unit # unit tests with coverage
|
||||
```
|
||||
|
||||
Make sure that all tests pass across all supported Python versions before submitting a pull request.
|
||||
- **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()`.
|
||||
|
||||
We look forward to your pull requests and can't wait to see your contributions!
|
||||
## Good Contribution Practices
|
||||
|
||||
### 🚀 Releasing
|
||||
- **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`.
|
||||
|
||||
All packages are published automatically via GitHub Actions when a GitHub Release is created with the correct tag prefix.
|
||||
## Pull Request Checklist
|
||||
|
||||
#### Tag Prefixes
|
||||
Before requesting review, make sure:
|
||||
|
||||
- [ ] 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
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Do not report security vulnerabilities through public issues or pull requests.**
|
||||
Please follow our [Security Policy](./SECURITY.md) to report them privately.
|
||||
|
||||
## Releasing
|
||||
|
||||
All packages are published automatically via GitHub Actions when a GitHub Release
|
||||
is created with the correct tag prefix.
|
||||
|
||||
### Tag Prefixes
|
||||
|
||||
| Package | Registry | Tag Prefix | Example |
|
||||
|---------|----------|------------|---------|
|
||||
@@ -77,15 +162,17 @@ All packages are published automatically via GitHub Actions when a GitHub Releas
|
||||
| `@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
@@ -0,0 +1,48 @@
|
||||
# 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>
|
||||
|
||||
@@ -4,4 +4,4 @@ description: "Submit an export job to create a structured memory export using a
|
||||
openapi: post /v1/exports/
|
||||
---
|
||||
|
||||
Submit a job to create a structured export of memories using a customizable Pydantic schema. This process may take some time to complete, especially if you're exporting a large number of memories. You can tailor the export by applying various filters (e.g., `user_id`, `agent_id`, `run_id`, or `session_id`) and by modifying the Pydantic schema to ensure the final data matches your exact needs.
|
||||
Submit a job to create a structured export of memories using a customizable Pydantic schema. This process may take some time to complete, especially if you're exporting a large number of memories. You can tailor the export by applying various filters (e.g., `user_id`, `agent_id`, `app_id`, or `run_id`) and by modifying the Pydantic schema to ensure the final data matches your exact needs.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ description: "Retrieve the latest structured memory export after submitting an e
|
||||
openapi: post /v1/exports/get
|
||||
---
|
||||
|
||||
Retrieve the latest structured memory export after submitting an export job. You can filter the export by `user_id`, `run_id`, `session_id`, or `app_id` to get the most recent export matching your filters.
|
||||
Retrieve the latest structured memory export after submitting an export job. You can filter the export by `user_id`, `agent_id`, `app_id`, `run_id`, `created_at`, or `updated_at` to get the most recent export matching your filters.
|
||||
@@ -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
|
||||
@@ -20,16 +24,17 @@ The `filters` object supports complex logical operations (AND, OR, NOT) and comp
|
||||
|
||||
### Search parameter defaults
|
||||
|
||||
| Parameter | V1/V2 | V3 |
|
||||
| --- | --- | --- |
|
||||
| `top_k` | Supported (default 10) | Supported (1-1000, default 10) |
|
||||
| `threshold` | No default | Default `0.1` (pass `0.0` to disable) |
|
||||
| `rerank` | Default `true` | Default `false` (pass `true` to enable) |
|
||||
| Parameter | Default |
|
||||
| --- | --- |
|
||||
| `top_k` | `10` (range 1–1000) |
|
||||
| `threshold` | `0.1` (pass `0.0` to disable) |
|
||||
| `rerank` | `false` (pass `true` to enable) |
|
||||
|
||||
<CodeGroup>
|
||||
```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"]
|
||||
|
||||
@@ -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 an organization to revoke their access to its projects and resources."
|
||||
openapi: "delete /api/v1/orgs/organizations/{org_id}/members/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Update Organization Member"
|
||||
description: "Update an existing member's role within an organization to change their permissions and access level."
|
||||
openapi: "put /api/v1/orgs/organizations/{org_id}/members/"
|
||||
---
|
||||
@@ -14,7 +14,7 @@ Organizations and projects are **optional** features. You can use Mem0 without t
|
||||
|
||||
## Key Capabilities
|
||||
|
||||
- **Multi-org/project Support**: Specify organization and project when initializing the Mem0 client to attribute API usage appropriately
|
||||
- **Multi-org/project Support**: Organization and project are resolved automatically from your API key via `/v1/ping/` — no org or project params are accepted by `MemoryClient.__init__`. Use a project-specific API key to target a particular project.
|
||||
- **Member Management**: Control access to data through organization and project membership
|
||||
- **Access Control**: Only members can access memories and data within their organization/project scope
|
||||
- **Team Isolation**: Maintain data separation between different teams and projects for secure collaboration
|
||||
@@ -79,7 +79,7 @@ new_project = client.project.create(
|
||||
|
||||
### Update Project Settings
|
||||
|
||||
Modify project configuration including custom instructions, categories, and language preferences:
|
||||
Modify project configuration including custom instructions, categories, language preferences, retrieval criteria, and memory decay:
|
||||
|
||||
```python
|
||||
# Update project with custom categories
|
||||
@@ -98,6 +98,17 @@ client.project.update(
|
||||
# Use the input language for memory storage and retrieval
|
||||
client.project.update(multilingual=True)
|
||||
|
||||
# Set retrieval criteria to control which memories are surfaced in search
|
||||
client.project.update(
|
||||
retrieval_criteria=[
|
||||
{"name": "relevance", "description": "How directly relevant this memory is to the current topic or user query", "weight": 3},
|
||||
{"name": "access_frequency", "description": "How often this memory has been accessed or surfaced recently", "weight": 1}
|
||||
]
|
||||
)
|
||||
|
||||
# Enable Memory Decay (boosts recently-accessed memories at search time)
|
||||
client.project.update(decay=True)
|
||||
|
||||
# Update multiple settings at once
|
||||
client.project.update(
|
||||
custom_instructions="...",
|
||||
@@ -109,6 +120,34 @@ client.project.update(
|
||||
)
|
||||
```
|
||||
|
||||
#### Set Retrieval Criteria
|
||||
|
||||
`retrieval_criteria` is a per-project list of dictionaries (`List[Dict]`) that shapes how memories are ranked and filtered during search. Each dictionary has three fields: `name` (identifier), `description` (interpreted by the LLM to score each memory), and `weight` (relative influence on the final score). Use this to focus retrieval on intent-aligned or signal-specific memories:
|
||||
|
||||
```python
|
||||
client.project.update(
|
||||
retrieval_criteria=[
|
||||
{
|
||||
"name": "joy",
|
||||
"description": "Measure the intensity of positive emotions such as happiness, excitement, or amusement expressed in the memory. A higher score reflects greater joy.",
|
||||
"weight": 3
|
||||
},
|
||||
{
|
||||
"name": "curiosity",
|
||||
"description": "Assess the extent to which the memory reflects inquisitiveness or interest in exploring new information. A higher score reflects stronger curiosity.",
|
||||
"weight": 2
|
||||
},
|
||||
{
|
||||
"name": "access_frequency",
|
||||
"description": "How often this memory has been accessed or surfaced recently.",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
Pass an empty list to clear all criteria and restore default retrieval behaviour.
|
||||
|
||||
#### Toggle Memory Decay
|
||||
|
||||
`decay` is a per-project boolean that turns on [Memory Decay](/platform/features/memory-decay) — a search-time ranking bias that reinforces recently-accessed memories and gently dampens stale ones. The flag is `false` by default; set it via the same project-update endpoint:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Remove Project Member"
|
||||
description: "Remove a member from a project to revoke their access to its memories, configuration, and resources."
|
||||
openapi: "delete /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Update Project Member"
|
||||
description: "Update an existing member's role within a project to change their permissions and access level."
|
||||
openapi: "put /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Update Project"
|
||||
description: "Update a project's settings, including name, custom instructions, and other configuration options."
|
||||
openapi: "patch /api/v1/orgs/organizations/{org_id}/projects/{project_id}/"
|
||||
---
|
||||
@@ -113,7 +113,7 @@ Launched a unified Mem0 plugin across three major AI development environments
|
||||
|
||||
Major expansion of the provider ecosystem:
|
||||
|
||||
- **Apache AGE** — New graph store support, bringing the total to 4 graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE)
|
||||
- **Apache AGE** — New graph store support, bringing the total to 4 graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE). **Note:** All external graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE) were subsequently removed in v2.0.0 (2026-04-14). Graph memory is now built-in entity linking with no external graph store required; see the [v2.0.0 entry above](#mem0-sdk-v2-0-0-v3-0-0).
|
||||
- **Turbopuffer** — New vector database provider for Python SDK
|
||||
- **MiniMax** — New LLM provider with dedicated AWS Bedrock support
|
||||
- **pgvector for Node.js** — PostgreSQL vector support added to the TypeScript OSS SDK
|
||||
|
||||
+86
-2
@@ -7,6 +7,65 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<Update label="2026-06-24" description="v2.0.9">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Memory (OSS):** Improve entity extraction precision by avoiding sentence-start common noun noise, preserving useful topic phrases, and exact-deduplicating entity links before semantic matching ([#5829](https://github.com/mem0ai/mem0/pull/5829))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-24" description="v2.0.8">
|
||||
|
||||
**New Features:**
|
||||
- **Embeddings:** Add native `embed_batch` to five embedders — LM Studio, Together, HuggingFace, Vertex AI, and Google GenAI — for batched embedding requests ([#5609](https://github.com/mem0ai/mem0/pull/5609))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Core:** Guard against malformed `image_url` entries in `parse_vision_messages` to prevent crashes ([#5631](https://github.com/mem0ai/mem0/pull/5631))
|
||||
- **Core:** Return `attributed_to` from `get()`, `get_all()`, and `search()` ([#5629](https://github.com/mem0ai/mem0/pull/5629))
|
||||
- **Core:** Fix `reset()` only dropping the history table and leaving stale messages behind ([#5541](https://github.com/mem0ai/mem0/pull/5541))
|
||||
- **Core:** Guard against an entity `embed_batch` count mismatch in the v3 add pipeline ([#5604](https://github.com/mem0ai/mem0/pull/5604))
|
||||
- **Core:** Fix an async `delete_all` race condition that corrupted the entity store's `linked_memory_ids` ([#5553](https://github.com/mem0ai/mem0/pull/5553))
|
||||
- **LLMs:** Skip the JSON `response_format` for Groq compound models that reject it ([#5513](https://github.com/mem0ai/mem0/pull/5513))
|
||||
- **LLMs:** Preserve reasoning fields during base-to-provider config conversion ([#5638](https://github.com/mem0ai/mem0/pull/5638))
|
||||
- **LLMs:** Pass the configured `anthropic_base_url` to the Anthropic client ([#5626](https://github.com/mem0ai/mem0/pull/5626))
|
||||
- **LLMs:** Stop the Azure provider from mutating and corrupting caller messages during content rewrite ([#5731](https://github.com/mem0ai/mem0/pull/5731))
|
||||
- **LLMs & Embeddings:** Repair HTTP proxy support for `httpx>=0.28` and preserve `proxies` in `LlmFactory` ([#5447](https://github.com/mem0ai/mem0/pull/5447))
|
||||
- **Embeddings:** Forward `embedding_dims` to Titan V2 in the AWS Bedrock embedder ([#5671](https://github.com/mem0ai/mem0/pull/5671))
|
||||
- **Rerankers:** Log reranking failures instead of swallowing them silently ([#5717](https://github.com/mem0ai/mem0/pull/5717))
|
||||
- **Rerankers:** Clamp out-of-range LLM scores instead of mis-parsing them ([#5635](https://github.com/mem0ai/mem0/pull/5635))
|
||||
- **Rerankers:** Export all five rerankers from the package root ([#5636](https://github.com/mem0ai/mem0/pull/5636))
|
||||
- **Vector Stores:** Point the FastEmbed-missing warning at `mem0ai[extras]` ([#5622](https://github.com/mem0ai/mem0/pull/5622))
|
||||
- **Vector Stores:** Preserve empty Azure AI Search update values ([#5524](https://github.com/mem0ai/mem0/pull/5524))
|
||||
- **Vector Stores:** Add an `auto_refresh` option for OpenSearch Serverless compatibility ([#3893](https://github.com/mem0ai/mem0/pull/3893))
|
||||
- **Vector Stores:** Wrap a scalar `vector_id` in a list for Chroma `delete()` ([#5703](https://github.com/mem0ai/mem0/pull/5703))
|
||||
- **Vector Stores:** Wrap Chroma `update()` ids, embeddings, and metadatas in lists ([#5757](https://github.com/mem0ai/mem0/pull/5757))
|
||||
- **Vector Stores:** Wrap a scalar `vector_id` in a list for Milvus `delete()` ([#5704](https://github.com/mem0ai/mem0/pull/5704))
|
||||
- **Vector Stores:** Map all comparison operators in the Pinecone `_create_filter()` ([#5707](https://github.com/mem0ai/mem0/pull/5707))
|
||||
- **Vector Stores:** Return `None` instead of `{}` from Chroma `_generate_where_clause` for empty filters ([#5713](https://github.com/mem0ai/mem0/pull/5713))
|
||||
- **Vector Stores:** Return `[[]]` from the OpenSearch `list()` error path to honor the `list()` contract ([#5727](https://github.com/mem0ai/mem0/pull/5727))
|
||||
- **Vector Stores:** Return `[[]]` from the Pinecone `list()` error path instead of a dict ([#5706](https://github.com/mem0ai/mem0/pull/5706))
|
||||
- **Vector Stores:** Return `[[]]` for an uninitialized FAISS index to honor the `list()` contract ([#5725](https://github.com/mem0ai/mem0/pull/5725))
|
||||
- **Vector Stores:** Wrap the MongoDB `list()` return in an outer list to match the interface contract ([#5729](https://github.com/mem0ai/mem0/pull/5729))
|
||||
- **Vector Stores:** Deep-copy Redis `DEFAULT_FIELDS` so instances keep distinct dims ([#5633](https://github.com/mem0ai/mem0/pull/5633))
|
||||
- **Vector Stores:** Pass the required `vectors` arg in Vertex AI `list()` and similarity search ([#5627](https://github.com/mem0ai/mem0/pull/5627))
|
||||
- **Vector Stores:** Return `None` from Redis `get()` for missing IDs ([#5625](https://github.com/mem0ai/mem0/pull/5625))
|
||||
- **Vector Stores:** Drop a stray `print` in Weaviate `list_cols` ([#5637](https://github.com/mem0ai/mem0/pull/5637))
|
||||
- **Graph:** Keep distinct entities that share a substring prefix ([#5630](https://github.com/mem0ai/mem0/pull/5630))
|
||||
- **Client:** Check the HTTP status before parsing the ping response in `_validate_api_key` ([#5639](https://github.com/mem0ai/mem0/pull/5639))
|
||||
- **Server:** Fetch filtered dashboard memories beyond the default page ([#5753](https://github.com/mem0ai/mem0/pull/5753))
|
||||
- **Server:** Return 404/400 instead of 502 for not-found and invalid input ([#5634](https://github.com/mem0ai/mem0/pull/5634))
|
||||
- **Server:** Return 404 instead of 500 for a malformed API key id on revoke ([#5640](https://github.com/mem0ai/mem0/pull/5640))
|
||||
- **Server:** Use `127.0.0.1` in the dashboard healthcheck to avoid IPv6 localhost resolution ([#5612](https://github.com/mem0ai/mem0/pull/5612))
|
||||
|
||||
**Improvements:**
|
||||
- **Vector Stores:** Batch BM25 sparse encoding in Qdrant insert ([#5592](https://github.com/mem0ai/mem0/pull/5592))
|
||||
|
||||
**Security:**
|
||||
- **Vector Stores:** Sanitize Milvus and Baidu filter values to prevent expression injection ([#5746](https://github.com/mem0ai/mem0/pull/5746))
|
||||
- **Vector Stores:** Reject dict filter values in MongoDB to prevent NoSQL operator injection ([#5748](https://github.com/mem0ai/mem0/pull/5748))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-17" description="v2.0.7">
|
||||
|
||||
**New Features:**
|
||||
@@ -62,7 +121,7 @@ mode: "wide"
|
||||
|
||||
**New Features:**
|
||||
- **Memory:** Warn at init time when hybrid/BM25 search silently degrades to semantic-only because the configured vector store does not implement `keyword_search`. Affected stores: Chroma, FAISS, Cassandra, LangChain, Neptune Analytics, S3 Vectors, Supabase, TurboPuffer, Valkey ([#5444](https://github.com/mem0ai/mem0/pull/5444))
|
||||
- **Memory:** Add opt-in `explain=True` parameter to `Memory.search()` and `AsyncMemory.search()`. When enabled, each result includes a `score_breakdown` dict with `semantic`, `keyword` (normalized BM25), `entity_boost`, and `temporal_boost` signals so callers can understand and tune retrieval ranking ([#5102](https://github.com/mem0ai/mem0/pull/5102))
|
||||
- **Memory:** Add opt-in `explain=True` parameter to `Memory.search()` and `AsyncMemory.search()`. When enabled, each result includes a `score_details` dict with `semantic_score`, `bm25_score`, `entity_boost`, `raw_score`, `max_possible_score`, `final_score`, and `threshold` so callers can understand and tune retrieval ranking ([#5102](https://github.com/mem0ai/mem0/pull/5102))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Vector Stores:** Normalize similarity scores to `[0, 1]` (higher = better) consistently across all backends. 11 adapters previously returned raw distance metrics (lower = better) — FAISS, Chroma, Milvus, Redis, Cassandra, PGVector, S3 Vectors, Supabase, Valkey, Azure MySQL, and Vertex AI Vector Search — causing incorrect ranking in multi-store setups ([#5391](https://github.com/mem0ai/mem0/pull/5391))
|
||||
@@ -170,7 +229,7 @@ mode: "wide"
|
||||
**Improvements:**
|
||||
- **Telemetry:** Sample OSS hot-path events at 10% via PostHog `before_send` hook to reduce event volume ([#4771](https://github.com/mem0ai/mem0/pull/4771))
|
||||
|
||||
See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to-v2) and [Platform migration guide](https://docs.mem0.ai/migration/platform-v2-to-v3) for upgrade instructions.
|
||||
See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) and [Platform migration guide](https://docs.mem0.ai/migration/platform-v2-to-v3) for upgrade instructions.
|
||||
|
||||
</Update>
|
||||
|
||||
@@ -1011,6 +1070,31 @@ See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to-
|
||||
|
||||
<Tab title="TypeScript">
|
||||
|
||||
<Update label="2026-06-24" description="v3.0.11">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Memory (OSS):** Align entity extraction with Python by reducing generic entity noise, preserving useful topic phrases, and exact-deduplicating entity links before semantic matching ([#5829](https://github.com/mem0ai/mem0/pull/5829))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-24" description="v3.0.10">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Memory (OSS):** Guard against malformed `image_url` entries in `parseVisionMessages` to prevent crashes ([#5631](https://github.com/mem0ai/mem0/pull/5631))
|
||||
- **Memory (OSS):** Return `attributedTo` from `get()`, `search()`, and `getAll()` ([#5675](https://github.com/mem0ai/mem0/pull/5675))
|
||||
- **Memory (OSS):** Preserve message roles in the extraction input so assistant facts aren't attributed to the user ([#5643](https://github.com/mem0ai/mem0/pull/5643))
|
||||
- **Memory (OSS):** Reject empty or blank messages in `Memory.add()` to prevent hallucinated memories ([#5545](https://github.com/mem0ai/mem0/pull/5545))
|
||||
- **Memory (OSS):** Check `message.role` instead of `content` when detecting system messages ([#3921](https://github.com/mem0ai/mem0/pull/3921))
|
||||
- **LLMs:** Honor the configured `baseURL` in `AnthropicLLM` ([#5740](https://github.com/mem0ai/mem0/pull/5740))
|
||||
- **Client:** Preserve `customCategories` names through key conversion ([#5741](https://github.com/mem0ai/mem0/pull/5741))
|
||||
- **Client:** Prevent hallucinated memories on an empty messages payload ([#5613](https://github.com/mem0ai/mem0/pull/5613))
|
||||
- **Client:** Preserve user metadata keys across the case-conversion round-trip ([#5515](https://github.com/mem0ai/mem0/pull/5515))
|
||||
|
||||
**Security:**
|
||||
- **Dependencies:** Upgrade `form-data` to `>=4.0.6` across pnpm workspaces to remediate CVE-2026-12143 ([#5618](https://github.com/mem0ai/mem0/pull/5618))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-17" description="v3.0.9">
|
||||
|
||||
**Bug Fixes:**
|
||||
|
||||
@@ -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` |
|
||||
@@ -7,7 +7,8 @@ To use DeepSeek LLM models, you have to set the `DEEPSEEK_API_KEY` environment v
|
||||
|
||||
## Usage
|
||||
|
||||
```python
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
@@ -36,6 +37,32 @@ messages = [
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Memory } from 'mem0ai/oss';
|
||||
|
||||
const config = {
|
||||
llm: {
|
||||
provider: 'deepseek',
|
||||
config: {
|
||||
apiKey: process.env.DEEPSEEK_API_KEY || '',
|
||||
model: 'deepseek-chat',
|
||||
temperature: 0.2,
|
||||
maxTokens: 2000,
|
||||
top_p: 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
|
||||
|
||||
@@ -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).
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
@@ -46,7 +46,7 @@ Here are the parameters available for configuring Baidu VectorDB:
|
||||
| `account` | Baidu VectorDB account name | `root` |
|
||||
| `api_key` | API key for accessing Baidu VectorDB | Required |
|
||||
| `database_name` | Name of the database | `mem0` |
|
||||
| `table_name` | Name of the table | `mem0_table` |
|
||||
| `table_name` | Name of the table | `mem0` |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` |
|
||||
| `metric_type` | Distance metric for similarity search | `L2` |
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ Here are the parameters available for configuring Elasticsearch:
|
||||
| `api_key` | API key for authentication | `None` |
|
||||
| `user` | Username for basic authentication | `None` |
|
||||
| `password` | Password for basic authentication | `None` |
|
||||
| `use_ssl` | Whether to use SSL for the connection | `True` |
|
||||
| `ca_certs` | Path to CA bundle for SSL certificate verification | `None` |
|
||||
| `verify_certs` | Whether to verify SSL certificates | `True` |
|
||||
| `auto_create_index` | Whether to automatically create the index | `True` |
|
||||
| `custom_search_query` | Function returning a custom search query | `None` |
|
||||
|
||||
@@ -55,6 +55,7 @@ Here are the parameters available for configuring FAISS:
|
||||
| `path` | Path to store FAISS index and metadata | `/tmp/faiss/<collection_name>` |
|
||||
| `distance_strategy` | Distance metric strategy to use (options: 'euclidean', 'inner_product', 'cosine') | `euclidean` |
|
||||
| `normalize_L2` | Whether to normalize L2 vectors (only applicable for euclidean distance) | `False` |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` |
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
|
||||
@@ -47,12 +47,12 @@ m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Memory } from "mem0ai";
|
||||
import { Memory } from "mem0ai/oss";
|
||||
import { OpenAIEmbeddings } from "@langchain/openai";
|
||||
import { MemoryVectorStore as LangchainMemoryStore } from "langchain/vectorstores/memory";
|
||||
import { MemoryVectorStore } from "langchain/vectorstores/memory";
|
||||
|
||||
const embeddings = new OpenAIEmbeddings();
|
||||
const vectorStore = new LangchainVectorStore(embeddings);
|
||||
const vectorStore = new MemoryVectorStore(embeddings);
|
||||
|
||||
const config = {
|
||||
"vector_store": {
|
||||
|
||||
@@ -42,8 +42,8 @@ Here are the parameters available for configuring MongoDB:
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| db_name | Name of the MongoDB database | `"mem0_db"` |
|
||||
| collection_name | Name of the MongoDB collection | `"mem0_collection"` |
|
||||
| collection_name | Name of the MongoDB collection | `"mem0"` |
|
||||
| embedding_model_dims | Dimensions of the embedding vectors | `1536` |
|
||||
| mongo_uri | The MongoDB URI connection string | `mongodb://username:password@localhost:27017` |
|
||||
| mongo_uri | The MongoDB URI connection string | `mongodb://localhost:27017` |
|
||||
|
||||
> **Note**: If `mongo_uri` is not provided, it will default to `mongodb://username:password@localhost:27017`.
|
||||
> **Note**: If `mongo_uri` is not provided, it will default to `mongodb://localhost:27017`.
|
||||
|
||||
@@ -53,17 +53,14 @@ print(results)
|
||||
import "dotenv/config";
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const databaseUrl = new URL(process.env.DATABASE_URL!);
|
||||
|
||||
const m = new Memory({
|
||||
vectorStore: {
|
||||
provider: "pgvector",
|
||||
config: {
|
||||
user: decodeURIComponent(databaseUrl.username),
|
||||
password: decodeURIComponent(databaseUrl.password),
|
||||
host: databaseUrl.hostname,
|
||||
port: Number(databaseUrl.port || 5432),
|
||||
dbname: databaseUrl.pathname.slice(1) || "neondb",
|
||||
connectionString: process.env.DATABASE_URL!,
|
||||
ssl: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
collectionName: "memories",
|
||||
dimension: 1536,
|
||||
embeddingModelDims: 1536,
|
||||
@@ -90,6 +87,7 @@ const results = await m.search("What movies should I recommend?", {
|
||||
|
||||
console.log(results);
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
## SQL Migration
|
||||
@@ -116,20 +114,19 @@ DATABASE_URL=postgresql://user:password@ep-example.us-east-2.aws.neon.tech/neond
|
||||
| `sslmode` | PostgreSQL SSL mode. Use `require` for Neon. | Driver default |
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
The current Mem0 TypeScript `pgvector` adapter takes individual Postgres fields,
|
||||
so parse `DATABASE_URL` before creating `Memory`.
|
||||
Use the Neon `DATABASE_URL` directly with `connectionString`. Set `ssl` if your runtime needs an explicit TLS config object.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| -------------------- | ---------------------------------------------- | -------------- |
|
||||
| `connectionString` | Neon Postgres connection string. | Required |
|
||||
| `ssl` | Optional TLS settings passed directly to `pg`. | Driver default |
|
||||
| `collectionName` | Name for the vector collection. | `memories` |
|
||||
| `dimension` | Vector dimension for Mem0 config. | Auto-detected |
|
||||
| `embeddingModelDims` | Embedding model dimensions for table creation. | Required |
|
||||
| `hnsw` | Enables HNSW indexing. | `false` |
|
||||
|
||||
**TLS note:** `ssl: true` is sufficient for most Neon connections since Neon uses valid certificates. Use `ssl: { rejectUnauthorized: false }` only when connecting through Neon's connection pooler on certain edge runtimes (e.g. Cloudflare Workers) that require it, or when your environment does not trust the Neon CA chain.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
| --- | --- | --- |
|
||||
| `user` | Database user. | Required |
|
||||
| `password` | Database password. | Required |
|
||||
| `host` | Database host. | Required |
|
||||
| `port` | Database port. | `5432` |
|
||||
| `dbname` | Database name. | `vector_store` |
|
||||
| `collectionName` | Name for the vector collection. | `memories` |
|
||||
| `dimension` | Vector dimension for Mem0 config. | Auto-detected |
|
||||
| `embeddingModelDims` | Embedding model dimensions for table creation. | Required |
|
||||
| `hnsw` | Enables HNSW indexing. | `false` |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ description: "Use AWS Neptune Analytics as a vector store in Mem0, combining gra
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install mem0ai[vector_stores]
|
||||
pip install mem0ai[vector-stores]
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: "pgvector"
|
||||
description: "Use pgvector as a vector store in Mem0 for PostgreSQL-based vector similarity search with open-source simplicity."
|
||||
---
|
||||
|
||||
[pgvector](https://github.com/pgvector/pgvector) is an open-source vector similarity search extension for Postgres. After connecting to Postgres, run `CREATE EXTENSION IF NOT EXISTS vector;` to create the vector extension.
|
||||
|
||||
### Usage
|
||||
@@ -21,7 +22,7 @@ config = {
|
||||
"password": "123",
|
||||
"host": "127.0.0.1",
|
||||
"port": "5432",
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,25 +31,22 @@ 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."}
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."},
|
||||
]
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Memory } from 'mem0ai/oss';
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const config = {
|
||||
vectorStore: {
|
||||
provider: 'pgvector',
|
||||
provider: "pgvector",
|
||||
config: {
|
||||
collectionName: 'memories',
|
||||
collectionName: "memories",
|
||||
embeddingModelDims: 1536,
|
||||
user: 'test',
|
||||
password: '123',
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
dbname: 'vector_store', // Optional; TypeScript OSS defaults to `vector_store` when omitted
|
||||
connectionString: "postgresql://test:123@localhost:5432/vector_store",
|
||||
diskann: false, // Optional, requires pgvectorscale extension
|
||||
hnsw: false, // Optional, for HNSW indexing
|
||||
},
|
||||
@@ -57,37 +55,44 @@ const config = {
|
||||
|
||||
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."}
|
||||
]
|
||||
{ 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
|
||||
|
||||
Here are the parameters available for configuring pgvector:
|
||||
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `dbname` | The name of the database | `postgres` |
|
||||
| `collection_name` | The name of the collection | `mem0` |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` |
|
||||
| `user` | User name to connect to the database | `None` |
|
||||
| `password` | Password to connect to the database | `None` |
|
||||
| `host` | The host where the Postgres server is running | `None` |
|
||||
| `port` | The port where the Postgres server is running | `None` |
|
||||
| `diskann` | Whether to use diskann for vector similarity search (requires pgvectorscale) | `True` |
|
||||
| `hnsw` | Whether to use hnsw for vector similarity search | `False` |
|
||||
| `sslmode` | SSL mode for PostgreSQL connection (e.g., 'require', 'prefer', 'disable') | `None` |
|
||||
| `connection_string` | PostgreSQL connection string (overrides individual connection parameters) | `None` |
|
||||
| `connection_pool` | psycopg2 connection pool object (overrides connection string and individual parameters) | `None` |
|
||||
| Parameter | SDK | Description | Default Value |
|
||||
| -------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
||||
| `connectionString` | TypeScript OSS | PostgreSQL connection string for direct connections. When set, Mem0 connects to the target database directly and skips the bootstrap `postgres` database flow. | `None` |
|
||||
| `ssl` | TypeScript OSS | SSL option passed directly to `pg`, either `true` or an SSL config object, for both `connectionString` and split-field connections. | `None` |
|
||||
| `dbname` | TypeScript OSS | Split-field database name. This is only used when `connectionString` is absent. | `vector_store` |
|
||||
| `collectionName` | TypeScript OSS | Collection name. | `memories` |
|
||||
| `embeddingModelDims` | TypeScript OSS | Dimensions of the embedding model. | Required |
|
||||
| `user` | TypeScript OSS + Python | Database user for split-field connections. | `None` |
|
||||
| `password` | TypeScript OSS + Python | Database password for split-field connections. | `None` |
|
||||
| `host` | TypeScript OSS + Python | Database host for split-field connections. | `None` |
|
||||
| `port` | TypeScript OSS + Python | Database port for split-field connections. | `None` |
|
||||
| `diskann` | TypeScript OSS + Python | Whether to use DiskANN for vector similarity search, requires pgvectorscale. | `False` |
|
||||
| `hnsw` | TypeScript OSS + Python | Whether to use HNSW for vector similarity search. | TypeScript OSS: `False`, Python: `True` |
|
||||
| `connection_string` | Python only | PostgreSQL connection string, overrides individual connection parameters. | `None` |
|
||||
| `sslmode` | Python only | SSL mode for PostgreSQL connections, such as `require`, `prefer`, or `disable`. | `None` |
|
||||
| `connection_pool` | Python only | psycopg connection pool object, overrides connection string and individual connection parameters. | `None` |
|
||||
|
||||
**Note (TypeScript OSS):** If you omit `dbname`, the TypeScript client uses the database name `vector_store`. Python defaults to `postgres` for `dbname`, as in the table above.
|
||||
**TypeScript OSS:** Use `connectionString` plus optional `ssl` for managed Postgres setups. If you omit `connectionString`, Mem0 falls back to split fields and uses `dbname`, `user`, `password`, `host`, `port`, and optional `ssl`.
|
||||
|
||||
**Python:** The Python SDK uses snake_case keys such as `connection_string`, `sslmode`, `collection_name`, and `embedding_model_dims`.
|
||||
|
||||
**Python connection priority**:
|
||||
|
||||
**Note**: The connection parameters have the following priority:
|
||||
1. `connection_pool` (highest priority)
|
||||
2. `connection_string`
|
||||
3. Individual connection parameters (`user`, `password`, `host`, `port`, `sslmode`)
|
||||
3. Individual connection parameters (`user`, `password`, `host`, `port`, `sslmode`)
|
||||
|
||||
@@ -18,7 +18,9 @@ os.environ["UPSTASH_VECTOR_REST_TOKEN"] = "..."
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "upstash_vector",
|
||||
"enable_embeddings": True,
|
||||
"config": {
|
||||
"enable_embeddings": True,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ description: "Use Valkey as an open-source vector store in Mem0 for high-perform
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install mem0ai[vector_stores]
|
||||
pip install mem0ai[vector-stores]
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -51,7 +51,7 @@ Here are the parameters available for configuring Valkey:
|
||||
| `hnsw_ef_construction` | Size of dynamic candidate list for HNSW | `200` |
|
||||
| `hnsw_ef_runtime` | Size of dynamic candidate list for search | `10` |
|
||||
| `cluster_mode` | Enable cluster mode for Valkey cluster (CME) deployments | `false` |
|
||||
| `distance_metric` | Distance metric for vector similarity | `cosine` |
|
||||
| `timezone` | Timezone for timestamp handling | `UTC` |
|
||||
|
||||
## Cluster Mode
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ config = {
|
||||
"deployment_index_id": "YOUR_DEPLOYMENT_INDEX_ID", # Required: Deployment-specific ID
|
||||
"project_id": "YOUR_PROJECT_ID", # Required: Google Cloud project ID
|
||||
"project_number": "YOUR_PROJECT_NUMBER", # Required: Google Cloud project number
|
||||
"region": "YOUR_REGION", # Optional: Defaults to GOOGLE_CLOUD_REGION
|
||||
"region": "YOUR_REGION", # Required: Google Cloud region
|
||||
"credentials_path": "path/to/credentials.json", # Optional: Defaults to GOOGLE_APPLICATION_CREDENTIALS
|
||||
"vector_search_api_endpoint": "YOUR_API_ENDPOINT" # Required for get operations
|
||||
}
|
||||
@@ -45,5 +45,6 @@ m.add("Your text here", user_id="user", metadata={"category": "example"})
|
||||
| `project_id` | Google Cloud project ID | Yes |
|
||||
| `project_number` | Google Cloud project number | Yes |
|
||||
| `vector_search_api_endpoint` | Vector search API endpoint | Yes (for get operations) |
|
||||
| `region` | Google Cloud region | No (defaults to GOOGLE_CLOUD_REGION) |
|
||||
| `region` | Google Cloud region | Yes |
|
||||
| `credentials_path` | Path to service account credentials | No (defaults to GOOGLE_APPLICATION_CREDENTIALS) |
|
||||
| `service_account_json` | Service account credentials as a dictionary (alternative to `credentials_path`) | `None` |
|
||||
|
||||
@@ -7,7 +7,7 @@ description: "Use Weaviate as an open-source vector search engine in Mem0 for st
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
pip install weaviate weaviate-client
|
||||
pip install weaviate-client
|
||||
```
|
||||
|
||||
### Usage
|
||||
@@ -48,4 +48,5 @@ Here are the parameters available for configuring Weaviate:
|
||||
| `collection_name` | The name of the collection to store the vectors | `mem0` |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` |
|
||||
| `cluster_url` | URL for the Weaviate server | `None` |
|
||||
| `auth_client_secret` | API key for Weaviate authentication | `None` |
|
||||
| `auth_client_secret` | API key for Weaviate authentication | `None` |
|
||||
| `additional_headers` | Additional headers to include in requests (`Dict[str, str]`) | `None` |
|
||||
@@ -10,7 +10,7 @@ Mem0 includes built-in support for various popular databases. Memory can utilize
|
||||
See the list of supported vector databases below.
|
||||
|
||||
<Note>
|
||||
The following vector databases are supported in the Python implementation. The TypeScript implementation currently only supports Qdrant, Redis, Valkey, Vectorize and in-memory vector database.
|
||||
The following vector databases are supported in the Python implementation. The TypeScript implementation currently supports Qdrant, Redis, PGVector, Supabase, LangChain, Azure AI Search, Vectorize, and an in-memory store.
|
||||
</Note>
|
||||
|
||||
<CardGroup cols={3}>
|
||||
|
||||
@@ -1,32 +1,65 @@
|
||||
---
|
||||
title: Development
|
||||
description: "Guide to contributing code to Mem0, covering the fork and clone workflow, PR submission, and code quality checks."
|
||||
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."
|
||||
icon: "code"
|
||||
---
|
||||
|
||||
# Development Contributions
|
||||
|
||||
We strive to make contributions **easy, collaborative, and enjoyable**. Follow the steps below to ensure a smooth contribution process.
|
||||
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.
|
||||
|
||||
## Submitting Your Contribution through PR
|
||||
<Note>
|
||||
For the complete contributor checklist, see
|
||||
[CONTRIBUTING.md](https://github.com/mem0ai/mem0/blob/main/CONTRIBUTING.md) in
|
||||
the repository root.
|
||||
</Note>
|
||||
|
||||
To contribute, follow these steps:
|
||||
## 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
|
||||
|
||||
1. **Fork & Clone** the repository: [Mem0 on GitHub](https://github.com/mem0ai/mem0)
|
||||
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:
|
||||
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:
|
||||
- Write necessary **tests**
|
||||
- Add **documentation, docstrings, and runnable examples**
|
||||
4. **Code Quality Checks**:
|
||||
- Run **linting** to catch style issues
|
||||
- Ensure **all tests pass**
|
||||
5. **Submit a Pull Request**
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
## Dependency Management
|
||||
## Python SDK (`mem0/`)
|
||||
|
||||
### Dependency Management
|
||||
|
||||
We use `hatch` as our package manager. Install it by following the [official instructions](https://hatch.pypa.io/latest/install/).
|
||||
|
||||
@@ -44,13 +77,9 @@ 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:
|
||||
Ensure `pre-commit` is installed before contributing (hooks run ruff + isort):
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
@@ -58,7 +87,7 @@ pre-commit install
|
||||
|
||||
### Linting with `ruff`
|
||||
|
||||
Run the linter and fix any reported issues before submitting your PR:
|
||||
Run the linter and fix any reported issues before submitting your PR (line length **120**):
|
||||
|
||||
```bash
|
||||
make lint
|
||||
@@ -66,10 +95,11 @@ make lint
|
||||
|
||||
### Code Formatting
|
||||
|
||||
To maintain a consistent code style, format your code:
|
||||
To maintain a consistent code style, format your code and sort imports (isort, `profile = "black"`):
|
||||
|
||||
```bash
|
||||
make format
|
||||
make sort
|
||||
```
|
||||
|
||||
### Testing with `pytest`
|
||||
@@ -84,10 +114,46 @@ make test
|
||||
|
||||
---
|
||||
|
||||
## Release Process
|
||||
## TypeScript SDK (`mem0-ts/`)
|
||||
|
||||
Currently, releases are handled manually. We aim for frequent releases, typically when new features or bug fixes are introduced.
|
||||
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()`
|
||||
|
||||
---
|
||||
|
||||
Thank you for contributing to Mem0!
|
||||
## 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.
|
||||
|
||||
---
|
||||
|
||||
Thank you for contributing to Mem0!
|
||||
|
||||
@@ -345,7 +345,7 @@ When evaluating memory systems, keep these considerations in mind:
|
||||
<Card title="Research" icon="flask" href="https://mem0.ai/research">
|
||||
Published research papers and technical reports
|
||||
</Card>
|
||||
<Card title="Blog Post" icon="newspaper" href="https://mem0.ai/blog/new-algorithm">
|
||||
<Card title="Blog Post" icon="newspaper" href="https://mem0.ai/blog/the-token-efficient-memory-algorithm-now-has-temporal-reasoning">
|
||||
Detailed writeup of the new algorithm design and results
|
||||
</Card>
|
||||
<Card title="Platform Migration" icon="arrow-right" href="/migration/platform-v2-to-v3">
|
||||
|
||||
+16
-3
@@ -123,8 +123,7 @@
|
||||
"icon": "arrow-right",
|
||||
"pages": [
|
||||
"migration/platform-v2-to-v3",
|
||||
"migration/oss-to-platform",
|
||||
"migration/api-changes"
|
||||
"migration/oss-to-platform"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -273,7 +272,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 +533,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 +547,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 +629,10 @@
|
||||
]
|
||||
},
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/components/rerankers/models/llm",
|
||||
"destination": "/components/rerankers/models/llm_reranker"
|
||||
},
|
||||
{
|
||||
"source": "/migration/breaking-changes",
|
||||
"destination": "/"
|
||||
@@ -632,6 +641,10 @@
|
||||
"source": "/migration/v0-to-v1",
|
||||
"destination": "/"
|
||||
},
|
||||
{
|
||||
"source": "/migration/api-changes",
|
||||
"destination": "/migration/oss-v2-to-v3"
|
||||
},
|
||||
{
|
||||
"source": "/platform/features/expiration-date",
|
||||
"destination": "/"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -65,6 +65,7 @@ The plugin uses the same shell scripts as Claude Code, Cursor, and Codex — hoo
|
||||
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message |
|
||||
| **Pre-tool** | `PreToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tools |
|
||||
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
|
||||
| **Stop** | `Stop` | Stores a session summary when the session ends |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ Add the Mem0 MCP server directly with a single command:
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--url "https://mcp.mem0.ai/mcp/" \
|
||||
--clients "claude code"
|
||||
```
|
||||
|
||||
@@ -138,11 +138,13 @@ 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 |
|
||||
| **Pre-compact** | `PreCompact` | Stores a session summary before context compaction |
|
||||
| **Stop** | `Stop` | Stores a session summary when the session ends |
|
||||
| **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted |
|
||||
|
||||
## Example Workflow
|
||||
|
||||
|
||||
+24
-10
@@ -41,7 +41,17 @@ Install the full plugin including MCP server, lifecycle hooks, and SDK skill.
|
||||
codex plugin marketplace add mem0ai/mem0
|
||||
```
|
||||
|
||||
2. Restart Codex, open the Plugin Directory, browse the **Mem0 Plugins** marketplace, and install **Mem0**.
|
||||
2. Install the plugin:
|
||||
|
||||
```bash
|
||||
codex plugin add mem0@mem0-plugins
|
||||
```
|
||||
|
||||
Or, in the app: restart Codex, open the Plugin Directory, browse the **Mem0 Plugins** marketplace, and install **Mem0**.
|
||||
|
||||
<Note>
|
||||
Step 1 is required for the app UI. Mem0 isn't in OpenAI's curated directory yet, so **without `codex plugin marketplace add`, Mem0 won't appear in the Codex app's Plugin Directory** — searching for it returns nothing. Adding the marketplace surfaces it (under **Created by you**) and makes it installable.
|
||||
</Note>
|
||||
|
||||
<Info>
|
||||
Do not combine with Option B. The plugin manifest auto-registers the `mem0` MCP server, so adding both will create a duplicate registration.
|
||||
@@ -49,27 +59,30 @@ Install the full plugin including MCP server, lifecycle hooks, and SDK skill.
|
||||
|
||||
### Option B — Direct MCP
|
||||
|
||||
The fastest way to connect Codex to Mem0 — no plugin, no marketplace. Add to `~/.codex/config.toml`:
|
||||
The fastest way to connect Codex to Mem0 — no plugin, no marketplace. Add the MCP server with a single command:
|
||||
|
||||
```bash
|
||||
codex mcp add mem0 --url https://mcp.mem0.ai/mcp/ --bearer-token-env-var MEM0_API_KEY
|
||||
```
|
||||
|
||||
Or add it manually to `~/.codex/config.toml`:
|
||||
|
||||
```toml
|
||||
[mcp_servers.mem0]
|
||||
url = "https://mcp.mem0.ai/mcp"
|
||||
url = "https://mcp.mem0.ai/mcp/"
|
||||
bearer_token_env_var = "MEM0_API_KEY"
|
||||
```
|
||||
|
||||
Make sure `MEM0_API_KEY` is exported in the shell you launch Codex from, then restart Codex.
|
||||
|
||||
<Info>
|
||||
Codex's `codex mcp add` CLI only supports stdio MCP servers. Because Mem0's MCP is HTTP/streamable, you configure it by editing `config.toml` directly (or via the **Plugins → Connect to a custom MCP → Streamable HTTP** UI in the Codex app).
|
||||
</Info>
|
||||
|
||||
This gives you the MCP tools but not the lifecycle hooks or SDK skill.
|
||||
|
||||
### Managing the Plugin
|
||||
|
||||
```bash
|
||||
codex plugin marketplace upgrade # pull latest plugin versions
|
||||
codex plugin marketplace remove mem0-plugins # unregister the marketplace
|
||||
codex plugin remove mem0@mem0-plugins # uninstall the plugin (keeps the marketplace)
|
||||
codex plugin marketplace remove mem0-plugins # unregister the marketplace entirely
|
||||
```
|
||||
|
||||
To update, run `codex plugin marketplace upgrade` to pull the latest from the Mem0 repo.
|
||||
@@ -110,9 +123,10 @@ 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 |
|
||||
| **Pre-compact** | `PreCompact` | Stores a session summary before context compaction |
|
||||
| **Stop** | `Stop` | Stores a session summary when the session ends |
|
||||
| **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted |
|
||||
|
||||
## Example Workflow
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ The fastest way to get started. Click the link below to install the Mem0 MCP ser
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--url "https://mcp.mem0.ai/mcp/" \
|
||||
--clients "cursor"
|
||||
```
|
||||
|
||||
@@ -108,9 +108,10 @@ 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 |
|
||||
| **Pre-compact** | `preCompact` | Stores a session summary before context compaction |
|
||||
| **Stop** | `stop` | Stores a session summary when the session ends |
|
||||
| **Pre-compact** | `preCompact` | Stores a summary before the context is compacted |
|
||||
|
||||
## Example Workflow
|
||||
|
||||
|
||||
@@ -100,12 +100,12 @@ This section:
|
||||
Initialize both the ElevenLabs and Mem0 clients:
|
||||
|
||||
```python
|
||||
# Initialize ElevenLabs client
|
||||
client = ElevenLabs(api_key=API_KEY)
|
||||
# Initialize ElevenLabs client
|
||||
client = ElevenLabs(api_key=API_KEY)
|
||||
|
||||
# Initialize memory client and tools
|
||||
client_tools = ClientTools()
|
||||
mem0_client = AsyncMemoryClient()
|
||||
# Initialize memory client and tools
|
||||
client_tools = ClientTools()
|
||||
mem0_client = AsyncMemoryClient()
|
||||
```
|
||||
|
||||
Here we:
|
||||
@@ -118,36 +118,36 @@ Here we:
|
||||
Define the two key memory functions that will be registered as tools:
|
||||
|
||||
```python
|
||||
# Define memory-related functions for the agent
|
||||
async def add_memories(parameters):
|
||||
"""Add a message to the memory store"""
|
||||
message = parameters.get("message")
|
||||
await mem0_client.add(
|
||||
messages=message,
|
||||
user_id=USER_ID
|
||||
)
|
||||
return "Memory added successfully"
|
||||
# Define memory-related functions for the agent
|
||||
async def add_memories(parameters):
|
||||
"""Add a message to the memory store"""
|
||||
message = parameters.get("message")
|
||||
await mem0_client.add(
|
||||
messages=message,
|
||||
user_id=USER_ID
|
||||
)
|
||||
return "Memory added successfully"
|
||||
|
||||
async def retrieve_memories(parameters):
|
||||
"""Retrieve relevant memories based on the input message"""
|
||||
message = parameters.get("message")
|
||||
async def retrieve_memories(parameters):
|
||||
"""Retrieve relevant memories based on the input message"""
|
||||
message = parameters.get("message")
|
||||
|
||||
# For Platform API, user_id goes in filters
|
||||
filters = {"user_id": USER_ID}
|
||||
# For Platform API, user_id goes in filters
|
||||
filters = {"user_id": USER_ID}
|
||||
|
||||
# Search for relevant memories using the message as a query
|
||||
results = await mem0_client.search(
|
||||
query=message,
|
||||
filters=filters
|
||||
)
|
||||
# Search for relevant memories using the message as a query
|
||||
results = await mem0_client.search(
|
||||
query=message,
|
||||
filters=filters
|
||||
)
|
||||
|
||||
# Extract and join the memory texts
|
||||
memories = ' '.join([result["memory"] for result in results.get('results', [])])
|
||||
print("[ Memories ]", memories)
|
||||
# Extract and join the memory texts
|
||||
memories = ' '.join([result["memory"] for result in results.get('results', [])])
|
||||
print("[ Memories ]", memories)
|
||||
|
||||
if memories:
|
||||
return memories
|
||||
return "No memories found"
|
||||
if memories:
|
||||
return memories
|
||||
return "No memories found"
|
||||
```
|
||||
|
||||
These functions:
|
||||
@@ -171,9 +171,9 @@ These functions:
|
||||
Register the memory functions with the ElevenLabs ClientTools system:
|
||||
|
||||
```python
|
||||
# Register the memory functions as tools for the agent
|
||||
client_tools.register("addMemories", add_memories, is_async=True)
|
||||
client_tools.register("retrieveMemories", retrieve_memories, is_async=True)
|
||||
# Register the memory functions as tools for the agent
|
||||
client_tools.register("addMemories", add_memories, is_async=True)
|
||||
client_tools.register("retrieveMemories", retrieve_memories, is_async=True)
|
||||
```
|
||||
|
||||
This allows the ElevenLabs agent to:
|
||||
@@ -186,19 +186,19 @@ This allows the ElevenLabs agent to:
|
||||
Configure the conversation with ElevenLabs:
|
||||
|
||||
```python
|
||||
# Initialize the conversation
|
||||
conversation = Conversation(
|
||||
client,
|
||||
AGENT_ID,
|
||||
# Assume auth is required when API_KEY is set
|
||||
requires_auth=bool(API_KEY),
|
||||
audio_interface=DefaultAudioInterface(),
|
||||
client_tools=client_tools,
|
||||
callback_agent_response=lambda response: print(f"Agent: {response}"),
|
||||
callback_agent_response_correction=lambda original, corrected: print(f"Agent: {original} -> {corrected}"),
|
||||
callback_user_transcript=lambda transcript: print(f"User: {transcript}"),
|
||||
# callback_latency_measurement=lambda latency: print(f"Latency: {latency}ms"),
|
||||
)
|
||||
# Initialize the conversation
|
||||
conversation = Conversation(
|
||||
client,
|
||||
AGENT_ID,
|
||||
# Assume auth is required when API_KEY is set
|
||||
requires_auth=bool(API_KEY),
|
||||
audio_interface=DefaultAudioInterface(),
|
||||
client_tools=client_tools,
|
||||
callback_agent_response=lambda response: print(f"Agent: {response}"),
|
||||
callback_agent_response_correction=lambda original, corrected: print(f"Agent: {original} -> {corrected}"),
|
||||
callback_user_transcript=lambda transcript: print(f"User: {transcript}"),
|
||||
# callback_latency_measurement=lambda latency: print(f"Latency: {latency}ms"),
|
||||
)
|
||||
```
|
||||
|
||||
This sets up the conversation with:
|
||||
@@ -217,16 +217,16 @@ This sets up the conversation with:
|
||||
Start and manage the conversation:
|
||||
|
||||
```python
|
||||
# Start the conversation
|
||||
print(f"Starting conversation with user_id: {USER_ID}")
|
||||
conversation.start_session()
|
||||
# Start the conversation
|
||||
print(f"Starting conversation with user_id: {USER_ID}")
|
||||
conversation.start_session()
|
||||
|
||||
# Handle Ctrl+C to gracefully end the session
|
||||
signal.signal(signal.SIGINT, lambda sig, frame: conversation.end_session())
|
||||
# Handle Ctrl+C to gracefully end the session
|
||||
signal.signal(signal.SIGINT, lambda sig, frame: conversation.end_session())
|
||||
|
||||
# Wait for the conversation to end and get the conversation ID
|
||||
conversation_id = conversation.wait_for_session_end()
|
||||
print(f"Conversation ID: {conversation_id}")
|
||||
# Wait for the conversation to end and get the conversation ID
|
||||
conversation_id = conversation.wait_for_session_end()
|
||||
print(f"Conversation ID: {conversation_id}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -445,4 +445,3 @@ By integrating ElevenLabs Conversational AI with Mem0, you can create voice agen
|
||||
Create voice-first AI applications
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
```
|
||||
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
|
||||
@@ -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
-4
@@ -228,7 +228,6 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
|
||||
- [OSS to Platform Migration](https://docs.mem0.ai/migration/oss-to-platform) [Both]: Use when moving from self-hosted to managed.
|
||||
- [OSS v2 to v3 Migration](https://docs.mem0.ai/migration/oss-v2-to-v3) [OSS]: Use when upgrading a self-hosted deployment across major versions.
|
||||
- [Platform v2 to v3 Migration](https://docs.mem0.ai/migration/platform-v2-to-v3) [Platform]: Use when upgrading a Platform integration across major versions.
|
||||
- [API Changes](https://docs.mem0.ai/migration/api-changes) [Both]: Use when the upgrade involves API surface changes.
|
||||
- [Server pgvector Image Upgrade](https://docs.mem0.ai/migration/server-pgvector-upgrade) [OSS]: Use when upgrading the self-hosted server Docker image from ankane/pgvector to pgvector/pgvector.
|
||||
- [Changelog](https://docs.mem0.ai/changelog/highlights) [Both]: Use when the user asks what shipped recently.
|
||||
|
||||
@@ -367,6 +366,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 +376,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 +464,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 +503,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.
|
||||
|
||||
@@ -1,566 +0,0 @@
|
||||
---
|
||||
title: API Reference Changes
|
||||
description: "Comprehensive reference of all API changes between Mem0 v0.x and v1.0.0 Beta, organized by component and method."
|
||||
icon: "code"
|
||||
iconType: "solid"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This page documents all API changes between Mem0 v0.x and v1.0.0 Beta, organized by component and method.
|
||||
|
||||
## Memory Class Changes
|
||||
|
||||
### Constructor
|
||||
|
||||
#### v0.x
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
# Basic initialization
|
||||
m = Memory()
|
||||
|
||||
# With configuration
|
||||
config = {
|
||||
"version": "v1.0", # Supported in v0.x
|
||||
"vector_store": {...}
|
||||
}
|
||||
m = Memory.from_config(config)
|
||||
```
|
||||
|
||||
#### v1.0.0
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
# Basic initialization (same)
|
||||
m = Memory()
|
||||
|
||||
# With configuration
|
||||
config = {
|
||||
"version": "v1.1", # v1.1+ only
|
||||
"vector_store": {...},
|
||||
# New optional features
|
||||
"reranker": {
|
||||
"provider": "cohere",
|
||||
"config": {...}
|
||||
}
|
||||
}
|
||||
m = Memory.from_config(config)
|
||||
```
|
||||
|
||||
### add() Method
|
||||
|
||||
#### v0.x Signature
|
||||
```python
|
||||
def add(
|
||||
self,
|
||||
messages,
|
||||
user_id: str = None,
|
||||
agent_id: str = None,
|
||||
run_id: str = None,
|
||||
metadata: dict = None,
|
||||
filters: dict = None,
|
||||
output_format: str = None, # ❌ REMOVED
|
||||
version: str = None # ❌ REMOVED
|
||||
) -> Union[List[dict], dict]
|
||||
```
|
||||
|
||||
#### v1.0.0 Signature
|
||||
```python
|
||||
def add(
|
||||
self,
|
||||
messages,
|
||||
user_id: str = None,
|
||||
agent_id: str = None,
|
||||
run_id: str = None,
|
||||
metadata: dict = None,
|
||||
filters: dict = None,
|
||||
infer: bool = True # ✅ NEW: Control memory inference
|
||||
) -> dict # Always returns dict with "results" key
|
||||
```
|
||||
|
||||
#### Changes Summary
|
||||
|
||||
| Parameter | v0.x | v1.0.0 | Change |
|
||||
|-----------|------|-----------|---------|
|
||||
| `messages` | ✅ | ✅ | Unchanged |
|
||||
| `user_id` | ✅ | ✅ | Unchanged |
|
||||
| `agent_id` | ✅ | ✅ | Unchanged |
|
||||
| `run_id` | ✅ | ✅ | Unchanged |
|
||||
| `metadata` | ✅ | ✅ | Unchanged |
|
||||
| `filters` | ✅ | ✅ | Unchanged |
|
||||
| `output_format` | ✅ | ❌ | **REMOVED** |
|
||||
| `version` | ✅ | ❌ | **REMOVED** |
|
||||
| `infer` | ❌ | ✅ | **NEW** |
|
||||
|
||||
#### Response Format Changes
|
||||
|
||||
**v0.x Response (variable format):**
|
||||
```python
|
||||
# With output_format="v1.0"
|
||||
[
|
||||
{
|
||||
"id": "mem_123",
|
||||
"memory": "User loves pizza",
|
||||
"event": "ADD"
|
||||
}
|
||||
]
|
||||
|
||||
# With output_format="v1.1"
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "mem_123",
|
||||
"memory": "User loves pizza",
|
||||
"event": "ADD"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**v1.0.0 Response (standardized):**
|
||||
```python
|
||||
# Always returns this format
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "mem_123",
|
||||
"memory": "User loves pizza",
|
||||
"metadata": {...},
|
||||
"event": "ADD"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### search() Method
|
||||
|
||||
#### v0.x Signature
|
||||
```python
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
user_id: str = None,
|
||||
agent_id: str = None,
|
||||
run_id: str = None,
|
||||
limit: int = 100,
|
||||
filters: dict = None, # Basic key-value only
|
||||
output_format: str = None, # ❌ REMOVED
|
||||
version: str = None # ❌ REMOVED
|
||||
) -> Union[List[dict], dict]
|
||||
```
|
||||
|
||||
#### v1.0.0 Signature
|
||||
```python
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
user_id: str = None,
|
||||
agent_id: str = None,
|
||||
run_id: str = None,
|
||||
limit: int = 100,
|
||||
filters: dict = None, # ✅ ENHANCED: Advanced operators
|
||||
rerank: bool = True # ✅ NEW: Reranking support
|
||||
) -> dict # Always returns dict with "results" key
|
||||
```
|
||||
|
||||
#### Enhanced Filtering
|
||||
|
||||
**v0.x Filters (basic):**
|
||||
```python
|
||||
# Simple key-value filtering only
|
||||
filters = {
|
||||
"category": "food",
|
||||
"user_id": "alice"
|
||||
}
|
||||
```
|
||||
|
||||
**v1.0.0 Filters (enhanced):**
|
||||
```python
|
||||
# Advanced filtering with operators
|
||||
filters = {
|
||||
"AND": [
|
||||
{"category": "food"},
|
||||
{"score": {"gte": 0.8}},
|
||||
{
|
||||
"OR": [
|
||||
{"priority": "high"},
|
||||
{"urgent": True}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Comparison operators
|
||||
filters = {
|
||||
"score": {"gt": 0.5}, # Greater than
|
||||
"priority": {"gte": 5}, # Greater than or equal
|
||||
"rating": {"lt": 3}, # Less than
|
||||
"confidence": {"lte": 0.9}, # Less than or equal
|
||||
"status": {"eq": "active"}, # Equal
|
||||
"archived": {"ne": True}, # Not equal
|
||||
"tags": {"in": ["work", "personal"]}, # In list
|
||||
"category": {"nin": ["spam", "deleted"]} # Not in list
|
||||
}
|
||||
```
|
||||
|
||||
### get_all() Method
|
||||
|
||||
#### v0.x Signature
|
||||
```python
|
||||
def get_all(
|
||||
self,
|
||||
user_id: str = None,
|
||||
agent_id: str = None,
|
||||
run_id: str = None,
|
||||
filters: dict = None,
|
||||
output_format: str = None, # ❌ REMOVED
|
||||
version: str = None # ❌ REMOVED
|
||||
) -> Union[List[dict], dict]
|
||||
```
|
||||
|
||||
#### v1.0.0 Signature
|
||||
```python
|
||||
def get_all(
|
||||
self,
|
||||
user_id: str = None,
|
||||
agent_id: str = None,
|
||||
run_id: str = None,
|
||||
filters: dict = None # ✅ ENHANCED: Advanced operators
|
||||
) -> dict # Always returns dict with "results" key
|
||||
```
|
||||
|
||||
### update() Method
|
||||
|
||||
#### No Breaking Changes
|
||||
```python
|
||||
# Same signature in both versions
|
||||
def update(
|
||||
self,
|
||||
memory_id: str,
|
||||
data: str
|
||||
) -> dict
|
||||
```
|
||||
|
||||
### delete() Method
|
||||
|
||||
#### No Breaking Changes
|
||||
```python
|
||||
# Same signature in both versions
|
||||
def delete(
|
||||
self,
|
||||
memory_id: str
|
||||
) -> dict
|
||||
```
|
||||
|
||||
### delete_all() Method
|
||||
|
||||
#### Breaking Change — Empty filter no longer silently deletes everything
|
||||
|
||||
**Before:** calling `delete_all()` with no filters silently deleted **all memories in the project**.
|
||||
|
||||
**After:**
|
||||
- No filters → raises a validation error (prevents accidental full-project wipe).
|
||||
- Concrete ID (e.g. `user_id="alice"`) → deletes memories for that entity (unchanged).
|
||||
- `"*"` for a filter → deletes all memories for that entity type across the project (new).
|
||||
- All four filters set to `"*"` → explicit full project wipe (new, requires opt-in on every parameter).
|
||||
|
||||
This change replaces the silent full-project delete (triggered by an empty or missing filter) with a validation error, and introduces `"*"` wildcards as the intentional path for bulk deletion.
|
||||
|
||||
```python
|
||||
# v0.x — no filter silently wiped all project memories
|
||||
m.delete_all() # DANGER: deleted everything
|
||||
m.delete_all(user_id="alice") # deleted alice's memories
|
||||
|
||||
# v1.x — no filter now raises an error; use "*" for intentional bulk deletes
|
||||
m.delete_all() # ERROR: at least one filter required
|
||||
m.delete_all(user_id="alice") # unchanged
|
||||
m.delete_all(user_id="*") # NEW — delete all users' memories
|
||||
m.delete_all(user_id="*", agent_id="*", app_id="*", run_id="*") # NEW — full project wipe
|
||||
```
|
||||
|
||||
## Platform Client (MemoryClient) Changes
|
||||
|
||||
### async_mode Default Changed
|
||||
|
||||
#### v0.x
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key="your-key")
|
||||
|
||||
# async_mode had to be explicitly set or had different default
|
||||
result = client.add("content", user_id="alice", async_mode=True)
|
||||
```
|
||||
|
||||
#### v1.0.0
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key="your-key")
|
||||
|
||||
# async_mode defaults to True now (better performance)
|
||||
result = client.add("content", user_id="alice") # Uses async_mode=True by default
|
||||
|
||||
# Can still override if needed
|
||||
result = client.add("content", user_id="alice", async_mode=False)
|
||||
```
|
||||
|
||||
## Configuration Changes
|
||||
|
||||
### Memory Configuration
|
||||
|
||||
#### v0.x Config Options
|
||||
```python
|
||||
config = {
|
||||
"vector_store": {...},
|
||||
"llm": {...},
|
||||
"embedder": {...},
|
||||
"graph_store": {...},
|
||||
"version": "v1.0", # ❌ v1.0 no longer supported
|
||||
"history_db_path": "...",
|
||||
"custom_instructions": "..."
|
||||
}
|
||||
```
|
||||
|
||||
#### v1.0.0 Config Options
|
||||
```python
|
||||
config = {
|
||||
"vector_store": {...},
|
||||
"llm": {...},
|
||||
"embedder": {...},
|
||||
"graph_store": {...},
|
||||
"reranker": { # ✅ NEW: Reranker support
|
||||
"provider": "cohere",
|
||||
"config": {...}
|
||||
},
|
||||
"version": "v1.1", # ✅ v1.1+ only
|
||||
"history_db_path": "...",
|
||||
"custom_instructions": "...",
|
||||
"custom_update_memory_prompt": "..." # ✅ NEW: Custom update prompt
|
||||
}
|
||||
```
|
||||
|
||||
### New Configuration Options
|
||||
|
||||
#### Reranker Configuration
|
||||
```python
|
||||
# Cohere reranker
|
||||
"reranker": {
|
||||
"provider": "cohere",
|
||||
"config": {
|
||||
"model": "rerank-english-v3.0",
|
||||
"api_key": "your-api-key",
|
||||
"top_k": 10
|
||||
}
|
||||
}
|
||||
|
||||
# Sentence Transformer reranker
|
||||
"reranker": {
|
||||
"provider": "sentence_transformer",
|
||||
"config": {
|
||||
"model": "cross-encoder/ms-marco-MiniLM-L-6-v2",
|
||||
"device": "cuda"
|
||||
}
|
||||
}
|
||||
|
||||
# Hugging Face reranker
|
||||
"reranker": {
|
||||
"provider": "huggingface",
|
||||
"config": {
|
||||
"model": "BAAI/bge-reranker-base",
|
||||
"device": "cuda"
|
||||
}
|
||||
}
|
||||
|
||||
# LLM-based reranker
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Changes
|
||||
|
||||
### New Error Types
|
||||
|
||||
#### v0.x Errors
|
||||
```python
|
||||
# Generic exceptions
|
||||
try:
|
||||
result = m.add("content", user_id="alice", version="v1.0")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
```
|
||||
|
||||
#### v1.0.0 Errors
|
||||
```python
|
||||
# More specific error handling
|
||||
try:
|
||||
result = m.add("content", user_id="alice")
|
||||
except ValueError as e:
|
||||
if "v1.0 API format is no longer supported" in str(e):
|
||||
# Handle version compatibility error
|
||||
pass
|
||||
elif "Invalid filter operator" in str(e):
|
||||
# Handle filter syntax error
|
||||
pass
|
||||
except TypeError as e:
|
||||
# Handle parameter errors
|
||||
pass
|
||||
except Exception as e:
|
||||
# Handle unexpected errors
|
||||
pass
|
||||
```
|
||||
|
||||
### Validation Changes
|
||||
|
||||
#### Stricter Parameter Validation
|
||||
|
||||
**v0.x (Lenient):**
|
||||
```python
|
||||
# Unknown parameters might be ignored
|
||||
result = m.add("content", user_id="alice", unknown_param="value")
|
||||
```
|
||||
|
||||
**v1.0.0 (Strict):**
|
||||
```python
|
||||
# Unknown parameters raise TypeError
|
||||
try:
|
||||
result = m.add("content", user_id="alice", unknown_param="value")
|
||||
except TypeError as e:
|
||||
print(f"Invalid parameter: {e}")
|
||||
```
|
||||
|
||||
## Response Schema Changes
|
||||
|
||||
### Memory Object Schema
|
||||
|
||||
#### v0.x Schema
|
||||
```python
|
||||
{
|
||||
"id": "mem_123",
|
||||
"memory": "User loves pizza",
|
||||
"user_id": "alice",
|
||||
"metadata": {...},
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
"score": 0.95 # In search results
|
||||
}
|
||||
```
|
||||
|
||||
#### v1.0.0 Schema (Enhanced)
|
||||
```python
|
||||
{
|
||||
"id": "mem_123",
|
||||
"memory": "User loves pizza",
|
||||
"user_id": "alice",
|
||||
"agent_id": "assistant", # ✅ More context
|
||||
"run_id": "session_001", # ✅ More context
|
||||
"metadata": {...},
|
||||
"categories": ["food"], # ✅ NEW: Auto-categorization
|
||||
"immutable": false, # ✅ NEW: Immutability flag
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
"score": 0.95, # In search results
|
||||
"rerank_score": 0.98 # ✅ NEW: If reranking used
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Code Examples
|
||||
|
||||
### Simple Migration
|
||||
|
||||
#### Before (v0.x)
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
m = Memory()
|
||||
|
||||
# Add with deprecated parameters
|
||||
result = m.add(
|
||||
"I love pizza",
|
||||
user_id="alice",
|
||||
output_format="v1.1",
|
||||
version="v1.0"
|
||||
)
|
||||
|
||||
# Handle variable response format
|
||||
if isinstance(result, list):
|
||||
memories = result
|
||||
else:
|
||||
memories = result.get("results", [])
|
||||
|
||||
for memory in memories:
|
||||
print(memory["memory"])
|
||||
```
|
||||
|
||||
#### After (v1.0.0 )
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
m = Memory()
|
||||
|
||||
# Add without deprecated parameters
|
||||
result = m.add(
|
||||
"I love pizza",
|
||||
user_id="alice"
|
||||
)
|
||||
|
||||
# Always dict format with "results" key
|
||||
for memory in result["results"]:
|
||||
print(memory["memory"])
|
||||
```
|
||||
|
||||
### Advanced Migration
|
||||
|
||||
#### Before (v0.x)
|
||||
```python
|
||||
# Basic filtering
|
||||
results = m.search(
|
||||
"food preferences",
|
||||
user_id="alice",
|
||||
filters={"category": "food"},
|
||||
output_format="v1.1"
|
||||
)
|
||||
```
|
||||
|
||||
#### After (v1.0.0 )
|
||||
```python
|
||||
# Enhanced filtering with reranking
|
||||
results = m.search(
|
||||
"food preferences",
|
||||
user_id="alice",
|
||||
filters={
|
||||
"AND": [
|
||||
{"category": "food"},
|
||||
{"score": {"gte": 0.8}}
|
||||
]
|
||||
},
|
||||
rerank=True
|
||||
)
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Component | v0.x | v1.0.0 | Status |
|
||||
|-----------|------|-----------|---------|
|
||||
| `add()` method | Variable response | Standardized response | ⚠️ Breaking |
|
||||
| `search()` method | Basic filtering | Enhanced filtering + reranking | ⚠️ Breaking |
|
||||
| `get_all()` method | Variable response | Standardized response | ⚠️ Breaking |
|
||||
| Response format | Variable | Always `{"results": [...]}` | ⚠️ Breaking |
|
||||
| Reranking | ❌ Not available | ✅ Full support | ✅ New feature |
|
||||
| Advanced filtering | ❌ Basic only | ✅ Full operators | ✅ Enhancement |
|
||||
| Error handling | Generic | Specific error types | ✅ Improvement |
|
||||
|
||||
<Info>
|
||||
Use this reference to systematically update your codebase. Test each change thoroughly before deploying to production.
|
||||
</Info>
|
||||
@@ -120,13 +120,14 @@ client = MemoryClient(api_key="m0-...")
|
||||
|
||||
| Method | Open Source | Platform |
|
||||
| ------ | ----------- | -------- |
|
||||
| `search()` | `m.search(query, user_id="alex")` | `client.search(query, filters={"user_id": "alex"})` |
|
||||
| `get_all()` | `m.get_all(user_id="alex")` | `client.get_all(filters={"user_id": "alex"})` |
|
||||
| `search()` | `m.search(query, filters={"user_id": "alex"})` | `client.search(query, filters={"user_id": "alex"})` |
|
||||
| `get_all()` | `m.get_all(filters={"user_id": "alex"})` | `client.get_all(filters={"user_id": "alex"})` |
|
||||
| `add()` | `m.add(memory, user_id="alex")` | `client.add(memory, user_id="alex")` |
|
||||
| `update()` | `m.update(memory_id, data="Updated content")` | `client.update(memory_id, text="Updated content")` |
|
||||
| `delete()` | `m.delete(memory_id)` | `client.delete(memory_id)` |
|
||||
| `delete_all()` | `m.delete_all(user_id="alex")` | `client.delete_all(user_id="alex")` |
|
||||
|
||||
Note: `add()` and `delete()` methods remain unchanged. The `update()` method is not available in Platform - use delete + add pattern instead.
|
||||
Note: `add()` and `delete()` methods remain unchanged. The `update()` method is available in Platform via `client.update(memory_id, text="Updated content")`.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Search Memories">
|
||||
@@ -158,18 +159,15 @@ Note: `add()` and `delete()` methods remain unchanged. The `update()` method is
|
||||
<CodeGroup>
|
||||
```python Open Source (Old)
|
||||
# Get all memories for a user
|
||||
memories = m.get_all(user_id="alex", top_k=10)
|
||||
|
||||
# Get memories with pagination
|
||||
memories = m.get_all(user_id="alex", top_k=5, offset=10)
|
||||
memories = m.get_all(filters={"user_id": "alex"}, top_k=10)
|
||||
```
|
||||
|
||||
```python Platform (New)
|
||||
# Get all memories for a user
|
||||
memories = client.get_all(filters={"user_id": "alex"}, top_k=10)
|
||||
|
||||
# Get memories with pagination
|
||||
memories = client.get_all(filters={"user_id": "alex"}, top_k=5, offset=10)
|
||||
# Get memories with pagination (Platform supports page/page_size)
|
||||
memories = client.get_all(filters={"user_id": "alex"}, page=2, page_size=10)
|
||||
```
|
||||
</CodeGroup>
|
||||
</Accordion>
|
||||
@@ -218,16 +216,18 @@ Note: `add()` and `delete()` methods remain unchanged. The `update()` method is
|
||||
<CodeGroup>
|
||||
```python Open Source (Old)
|
||||
# Update memory content
|
||||
m.update(memory_id="mem_123", new_memory="Updated content")
|
||||
m.update(memory_id="mem_123", data="Updated content")
|
||||
```
|
||||
|
||||
```python Platform (New)
|
||||
# Update memory (not available in Platform)
|
||||
# Use delete + add pattern instead
|
||||
client.delete(memory_id="mem_123")
|
||||
client.add("Updated content", user_id="alex")
|
||||
# Update memory content
|
||||
client.update(memory_id="mem_123", text="Updated content")
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
The parameter name differs between SDKs: OSS `Memory.update()` takes `data=`, while the Platform `MemoryClient.update()` (Python and JS/TS) takes `text=`. When migrating, rename this keyword argument.
|
||||
</Note>
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -392,7 +392,7 @@ The Platform introduces powerful capabilities not available in OSS:
|
||||
| **Add Method** | `m.add(memory, user_id="x")` | `client.add(memory, user_id="x")` | No change |
|
||||
| **Delete Method** | `m.delete(memory_id)` | `client.delete(memory_id)` | No change |
|
||||
| **Delete All** | `m.delete_all(user_id="x")` | `client.delete_all(user_id="x")` | No change |
|
||||
| **Update Method** | `m.update(memory_id, new_memory)` | Use delete + add pattern | Replace with delete then add |
|
||||
| **Update Method** | `m.update(memory_id, data="Updated content")` | `client.update(memory_id, text="Updated content")` | Rename `data=` kwarg to `text=` |
|
||||
| **Config** | Local vector store + LLM config | Managed cloud infrastructure | Remove local config setup |
|
||||
|
||||
## Rollback plan
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
|
||||
+43
-8
@@ -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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4861,8 +4883,7 @@
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"memory_id",
|
||||
"text"
|
||||
"memory_id"
|
||||
],
|
||||
"properties": {
|
||||
"memory_id": {
|
||||
@@ -4873,6 +4894,11 @@
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "The new text content for the memory"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Updated metadata to associate with the memory."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4948,18 +4974,27 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_ids": {
|
||||
"memories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The unique identifier of the memory to delete."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"memory_id"
|
||||
]
|
||||
},
|
||||
"maxItems": 1000,
|
||||
"description": "Array of memory IDs to delete."
|
||||
"description": "Array of memory objects to delete."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"memory_ids"
|
||||
"memories"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6256,4 +6291,4 @@
|
||||
}
|
||||
},
|
||||
"x-original-swagger-version": "2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ results_without_criteria = client.search(
|
||||
### Compare Results
|
||||
|
||||
### Search Results (with Criteria)
|
||||
```python
|
||||
```text
|
||||
[
|
||||
{"memory": "User feels refreshed and ready to take on anything on a beautiful sunny day", "score": 0.666, ...},
|
||||
{"memory": "User finally has time to draw something after a long time", "score": 0.616, ...},
|
||||
@@ -128,7 +128,7 @@ results_without_criteria = client.search(
|
||||
```
|
||||
|
||||
### Search Results (without Criteria)
|
||||
```python
|
||||
```text
|
||||
[
|
||||
{"memory": "User is happy today", "score": 0.607, ...},
|
||||
{"memory": "User feels refreshed and ready to take on anything on a beautiful sunny day", "score": 0.512, ...},
|
||||
|
||||
@@ -190,7 +190,7 @@ messages = [
|
||||
client.add(messages, user_id='alice')
|
||||
```
|
||||
|
||||
```python Memories with categories
|
||||
```text Memories with categories
|
||||
# Following categories will be created for the memories added
|
||||
Sometimes draws and sketches in free time (hobbies)
|
||||
Is quite athletic (sports)
|
||||
|
||||
@@ -108,10 +108,10 @@ print(response)
|
||||
|
||||
```javascript JavaScript
|
||||
// Basic Export request
|
||||
const filters = {"user_id": "alice"};
|
||||
const basicFilters = {"user_id": "alice"};
|
||||
const response = await client.createMemoryExport({
|
||||
schema: json_schema,
|
||||
filters: filters
|
||||
filters: basicFilters
|
||||
});
|
||||
|
||||
// Export with custom instructions and additional filters
|
||||
@@ -124,16 +124,16 @@ const export_instructions = `
|
||||
`;
|
||||
|
||||
// For create operation, using only user_id filter as requested
|
||||
const filters = {
|
||||
const exportFilters = {
|
||||
"AND": [
|
||||
{"user_id": "alex"},
|
||||
{"created_at": {"gte": "2024-01-01"}}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const responseWithInstructions = await client.createMemoryExport({
|
||||
schema: json_schema,
|
||||
filters: filters,
|
||||
filters: exportFilters,
|
||||
exportInstructions: export_instructions
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.2.10",
|
||||
"version": "0.2.11",
|
||||
"description": "Persistent memory for Claude Code. Remembers decisions, patterns, and preferences across sessions.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.2.10",
|
||||
"version": "0.2.11",
|
||||
"description": "Persistent memory for Codex. Remembers decisions, patterns, and preferences across sessions.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.2.10",
|
||||
"version": "0.2.11",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
|
||||
All notable changes to the Mem0 plugin will be documented in this file.
|
||||
|
||||
## 0.2.11 — Session-summary metadata fix + rerank auto-injected context by default
|
||||
|
||||
> Versions: Claude Code / Cursor / Codex `0.2.11`; Antigravity `0.1.3`. All four editors share `scripts/`, so the fix below applies to every editor.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`files_touched` was double-JSON-encoded in session summaries (`scripts/capture_session_summary.py`):** the Stop-hook summary set `metadata["files_touched"] = json.dumps(files[:20])` — a pre-serialized JSON string — and then serialized the whole request body again with `json.dumps(body)`. The stored memory therefore carried an escaped string blob (`"[\"mem0/memory/main.py\", \"src/client/index.ts\"]"`) instead of a real array, so file paths surfaced as backslash- and slash-heavy escaped text when those memories were returned by `search_memories`/`get_memories` and shown in Claude Code, Cursor, Codex, and Antigravity. The fix stores the list directly (`metadata["files_touched"] = files[:20]`) so the body is encoded exactly once. New `tests/test_capture_session_summary.py` asserts the posted body contains a JSON array and no escaped-string artifact.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Auto-injected memory context is now reranked by default (`scripts/_search.py`, `scripts/file_context.py`, `scripts/on_bash_output.sh`, `scripts/on_user_prompt.sh`):** the REST search endpoint does not rerank when `rerank` is omitted, so hook-injected context (file-context, bash-error lookup, session-resume prefetch) was ordered by raw vector similarity and the single most relevant memory could fall outside the injected `top_k` window. A new `should_rerank()` helper turns reranking on for every auto-injection path; the extra ~150–200 ms stays within the hook's curl budget. Opt out with `MEM0_RERANK=0` (also accepts `false`/`no`/`off`). (#5690)
|
||||
|
||||
## 0.2.10 — Accurate per-editor telemetry attribution
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"name": "mem0",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"description": "Persistent semantic memory for Antigravity agents. Cross-session, user-level recall via the Mem0 Platform MCP server. 16 slash commands, lifecycle hooks for auto-capture and metadata enforcement.",
|
||||
"author": { "name": "Mem0", "email": "support@mem0.ai" },
|
||||
"publisher": "mem0ai",
|
||||
|
||||
@@ -173,7 +173,7 @@ def store_summary(
|
||||
if branch:
|
||||
metadata["branch"] = branch
|
||||
if files:
|
||||
metadata["files_touched"] = json.dumps(files[:20])
|
||||
metadata["files_touched"] = files[:20]
|
||||
|
||||
body = {
|
||||
"messages": [{"role": "user", "content": summary_prompt}],
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Regression tests for capture_session_summary.py request body construction.
|
||||
|
||||
Guards against the double-JSON-encoding bug where ``files_touched`` was stored
|
||||
as a pre-serialized JSON string and then encoded a second time with the rest of
|
||||
the request body — surfacing as escaped, slash-heavy blobs in the memories shown
|
||||
inside Claude Code / Cursor / Codex / Antigravity (all four editors share this
|
||||
script).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class _FakeResp:
|
||||
status = 200
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_):
|
||||
return False
|
||||
|
||||
|
||||
def _capture_request_body(monkeypatch):
|
||||
"""Patch urlopen so store_summary posts nowhere; capture the request body."""
|
||||
import capture_session_summary as css
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
def fake_urlopen(req, timeout=0):
|
||||
captured["raw"] = req.data.decode("utf-8")
|
||||
captured["body"] = json.loads(captured["raw"])
|
||||
return _FakeResp()
|
||||
|
||||
monkeypatch.setattr(css.urllib.request, "urlopen", fake_urlopen)
|
||||
return captured, css
|
||||
|
||||
|
||||
def test_files_touched_is_json_array_not_double_encoded(monkeypatch):
|
||||
"""files_touched must be a real JSON array, encoded exactly once."""
|
||||
captured, css = _capture_request_body(monkeypatch)
|
||||
files = ["mem0/memory/main.py", "src/client/index.ts"]
|
||||
|
||||
css.store_summary(
|
||||
api_key="test-key",
|
||||
summary_prompt="did some work",
|
||||
user_id="u1",
|
||||
session_id="s1",
|
||||
project_id="p1",
|
||||
branch="main",
|
||||
files=files,
|
||||
)
|
||||
|
||||
files_touched = captured["body"]["metadata"]["files_touched"]
|
||||
assert isinstance(files_touched, list), (
|
||||
"files_touched must be a JSON array, not a double-encoded string; "
|
||||
f"got {type(files_touched).__name__}: {files_touched!r}"
|
||||
)
|
||||
assert files_touched == files
|
||||
# The file paths must not appear as an escaped JSON string inside the body.
|
||||
assert '\\"' not in captured["raw"]
|
||||
|
||||
|
||||
def test_files_touched_omitted_when_no_files(monkeypatch):
|
||||
"""No files touched -> no files_touched key (unchanged behaviour)."""
|
||||
captured, css = _capture_request_body(monkeypatch)
|
||||
|
||||
css.store_summary(
|
||||
api_key="test-key",
|
||||
summary_prompt="did some work",
|
||||
user_id="u1",
|
||||
session_id="s1",
|
||||
project_id="p1",
|
||||
branch="main",
|
||||
files=[],
|
||||
)
|
||||
|
||||
assert "files_touched" not in captured["body"]["metadata"]
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+6
-5
@@ -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();
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0ai",
|
||||
"version": "3.0.9",
|
||||
"version": "3.0.11",
|
||||
"description": "The Memory Layer For Your AI Apps",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+6
-5
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,6 +296,44 @@ export class Memory {
|
||||
return filters;
|
||||
}
|
||||
|
||||
private _normalizeEntityText(value: string): string {
|
||||
return value.trim().toLowerCase().replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
private async _existingEntitiesByText(
|
||||
entityStore: VectorStore,
|
||||
filters: Record<string, any>,
|
||||
): Promise<Map<string, { id: string; payload: Record<string, any> }>> {
|
||||
const rowsByText = new Map<
|
||||
string,
|
||||
{ id: string; payload: Record<string, any> }
|
||||
>();
|
||||
let rows: Array<{ id: string; payload: Record<string, any> }> = [];
|
||||
try {
|
||||
const listed = await entityStore.list(filters, 10000);
|
||||
rows = (
|
||||
Array.isArray(listed) && Array.isArray(listed[0])
|
||||
? listed[0]
|
||||
: (listed as any)
|
||||
) as Array<{ id: string; payload: Record<string, any> }>;
|
||||
} catch (e) {
|
||||
console.debug(
|
||||
`Exact entity lookup failed, falling back to semantic dedup: ${e}`,
|
||||
);
|
||||
return rowsByText;
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
const text = row.payload?.data;
|
||||
if (typeof text !== "string") continue;
|
||||
const key = this._normalizeEntityText(text);
|
||||
if (key && !rowsByText.has(key)) {
|
||||
rowsByText.set(key, row);
|
||||
}
|
||||
}
|
||||
return rowsByText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove `memoryId` from every entity record scoped to `filters`.
|
||||
* If an entity's `linkedMemoryIds` becomes empty after removal, the
|
||||
@@ -393,6 +431,10 @@ export class Memory {
|
||||
if (entities.length === 0) return;
|
||||
|
||||
const entityStore = await this.getEntityStore();
|
||||
const exactMatches = await this._existingEntitiesByText(
|
||||
entityStore,
|
||||
filters,
|
||||
);
|
||||
|
||||
for (const entity of entities) {
|
||||
try {
|
||||
@@ -409,12 +451,21 @@ export class Memory {
|
||||
score?: number;
|
||||
payload: Record<string, any>;
|
||||
}> = [];
|
||||
try {
|
||||
matches = await entityStore.search(entityVec, 1, filters);
|
||||
} catch {}
|
||||
const exactMatch = exactMatches.get(
|
||||
this._normalizeEntityText(entity.text),
|
||||
);
|
||||
if (!exactMatch) {
|
||||
try {
|
||||
matches = await entityStore.search(entityVec, 1, filters);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (matches.length > 0 && (matches[0].score ?? 0) >= 0.95) {
|
||||
const match = matches[0];
|
||||
const semanticMatch =
|
||||
matches.length > 0 && (matches[0].score ?? 0) >= 0.95
|
||||
? matches[0]
|
||||
: undefined;
|
||||
const match = exactMatch ?? semanticMatch;
|
||||
if (match) {
|
||||
const payload = match.payload || {};
|
||||
const linked = new Set<string>(
|
||||
Array.isArray(payload.linkedMemoryIds)
|
||||
@@ -1062,6 +1113,10 @@ export class Memory {
|
||||
|
||||
if (valid.length > 0) {
|
||||
const entityStore = await this.getEntityStore();
|
||||
const exactMatches = await this._existingEntitiesByText(
|
||||
entityStore,
|
||||
filters,
|
||||
);
|
||||
|
||||
// 7c: Search for existing entities one by one (no batch search)
|
||||
const toInsertVectors: number[][] = [];
|
||||
@@ -1077,13 +1132,20 @@ export class Memory {
|
||||
score?: number;
|
||||
payload: Record<string, any>;
|
||||
}> = [];
|
||||
try {
|
||||
matches = await entityStore.search(entityVec, 1, filters);
|
||||
} catch {}
|
||||
const exactMatch = exactMatches.get(key);
|
||||
if (!exactMatch) {
|
||||
try {
|
||||
matches = await entityStore.search(entityVec, 1, filters);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (matches.length > 0 && (matches[0].score ?? 0) >= 0.95) {
|
||||
const semanticMatch =
|
||||
matches.length > 0 && (matches[0].score ?? 0) >= 0.95
|
||||
? matches[0]
|
||||
: undefined;
|
||||
const match = exactMatch ?? semanticMatch;
|
||||
if (match) {
|
||||
// Update existing entity
|
||||
const match = matches[0];
|
||||
const payload = match.payload || {};
|
||||
const linked = new Set<string>(payload.linkedMemoryIds ?? []);
|
||||
for (const mid of memoryIds) linked.add(mid);
|
||||
@@ -1539,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 = {};
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
* Extracts four types of entities from text:
|
||||
* - PROPER: Capitalized multi-word sequences (person names, places, brands)
|
||||
* - QUOTED: Text in single or double quotes (titles, specific terms)
|
||||
* - COMPOUND: Multi-word noun phrases with specific modifiers (e.g., "machine learning")
|
||||
* - NOUN: Single nouns from circumstantial compound patterns
|
||||
* - TOPIC: Multi-word noun/topic phrases with specific modifiers
|
||||
* - IDENTIFIER: Dotted technical identifiers such as person.properties.email
|
||||
*
|
||||
* Uses the `compromise` npm package for NLP-based extraction when available.
|
||||
* Falls back to regex-only extraction if `compromise` is not installed.
|
||||
@@ -196,6 +196,25 @@ const NON_SPECIFIC_ADJ: Set<string> = new Set([
|
||||
"final",
|
||||
"initial",
|
||||
"side",
|
||||
"top",
|
||||
]);
|
||||
|
||||
/** Leading words that frame a topic but are not part of the topic itself. */
|
||||
const TOPIC_PREFIX_WORDS: Set<string> = new Set([
|
||||
"a",
|
||||
"an",
|
||||
"the",
|
||||
"my",
|
||||
"your",
|
||||
"our",
|
||||
"their",
|
||||
"his",
|
||||
"her",
|
||||
"its",
|
||||
"this",
|
||||
"that",
|
||||
"these",
|
||||
"those",
|
||||
]);
|
||||
|
||||
/** Generic tail words to strip from compound entities. */
|
||||
@@ -267,6 +286,25 @@ const GENERIC_CAPS: Set<string> = new Set([
|
||||
"disadvantages",
|
||||
]);
|
||||
|
||||
/** Generic role/title words that should not become single-token entities. */
|
||||
const GENERIC_SINGLE_ENTITY_TERMS: Set<string> = new Set([
|
||||
"user",
|
||||
"assistant",
|
||||
"agent",
|
||||
"customer",
|
||||
"client",
|
||||
"person",
|
||||
"people",
|
||||
"human",
|
||||
"memory",
|
||||
"message",
|
||||
"conversation",
|
||||
"chat",
|
||||
"session",
|
||||
"system",
|
||||
"top",
|
||||
]);
|
||||
|
||||
/** Markdown/formatting markers to skip during extraction. */
|
||||
const FORMATTING_MARKERS: Set<string> = new Set([
|
||||
"*",
|
||||
@@ -287,7 +325,7 @@ const FORMATTING_MARKERS: Set<string> = new Set([
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ExtractedEntity {
|
||||
type: "PROPER" | "QUOTED" | "COMPOUND" | "NOUN";
|
||||
type: "PROPER" | "QUOTED" | "TOPIC" | "IDENTIFIER";
|
||||
text: string;
|
||||
}
|
||||
|
||||
@@ -338,32 +376,96 @@ function stripGenericEnding(words: string[]): string[] {
|
||||
return words;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a token position is at the start of a sentence.
|
||||
* Simple heuristic: index 0, or preceded by sentence-ending punctuation
|
||||
* or formatting markers.
|
||||
*/
|
||||
function isSentenceStart(
|
||||
tokens: string[],
|
||||
idx: number,
|
||||
rawText: string,
|
||||
): boolean {
|
||||
if (idx === 0) {
|
||||
return true;
|
||||
function stripTopicPrefix(words: string[]): string[] {
|
||||
let start = 0;
|
||||
while (
|
||||
start < words.length &&
|
||||
TOPIC_PREFIX_WORDS.has(words[start].toLowerCase())
|
||||
) {
|
||||
start++;
|
||||
}
|
||||
const prev = tokens[idx - 1];
|
||||
if (/[.!?:]$/.test(prev)) {
|
||||
return words.slice(start);
|
||||
}
|
||||
|
||||
function cleanToken(token: string): string {
|
||||
return token.replace(/^[^\w.]+|[^\w.]+$/g, "");
|
||||
}
|
||||
|
||||
function tokenize(text: string): string[] {
|
||||
return (
|
||||
text.match(
|
||||
/[A-Za-z_][\w-]*(?:\.[A-Za-z_][\w-]*)*|\d[\d,]*(?:\.\d+)?|[,:;.!?&]/g,
|
||||
) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
function isCapitalized(token: string): boolean {
|
||||
return /^[A-Z]/.test(token) && /[A-Za-z]/.test(token);
|
||||
}
|
||||
|
||||
function hasInternalCapOrDigit(token: string): boolean {
|
||||
return (
|
||||
/\d/.test(token) ||
|
||||
/[A-Z]/.test(token.slice(1)) ||
|
||||
/^[A-Z]{2,}$/.test(token)
|
||||
);
|
||||
}
|
||||
|
||||
function isBadSingleNameToken(token: string): boolean {
|
||||
const lower = token.toLowerCase();
|
||||
return GENERIC_SINGLE_ENTITY_TERMS.has(lower) || GENERIC_CAPS.has(lower);
|
||||
}
|
||||
|
||||
function looksLikeMetricCount(token: string): boolean {
|
||||
return /^\d[\d,]*(?:\.\d+)?$/.test(token);
|
||||
}
|
||||
|
||||
function isMetricListContext(tokens: string[], idx: number): boolean {
|
||||
const prev = idx > 0 ? tokens[idx - 1] : "";
|
||||
const next = idx + 1 < tokens.length ? tokens[idx + 1] : "";
|
||||
return [":", ",", ";"].includes(prev) || [",", ";"].includes(next);
|
||||
}
|
||||
|
||||
function isSentenceStart(tokens: string[], idx: number): boolean {
|
||||
if (idx === 0) return true;
|
||||
return (
|
||||
[".", "!", "?", ":"].includes(tokens[idx - 1]) ||
|
||||
FORMATTING_MARKERS.has(tokens[idx - 1])
|
||||
);
|
||||
}
|
||||
|
||||
function isListItemNameToken(tokens: string[], idx: number): boolean {
|
||||
const token = cleanToken(tokens[idx]);
|
||||
if (!isCapitalized(token) || isBadSingleNameToken(token)) return false;
|
||||
const next = idx + 1 < tokens.length ? cleanToken(tokens[idx + 1]) : "";
|
||||
if (!looksLikeMetricCount(next)) return false;
|
||||
return (
|
||||
isMetricListContext(tokens, idx) || isMetricListContext(tokens, idx + 1)
|
||||
);
|
||||
}
|
||||
|
||||
function isNameToken(tokens: string[], idx: number): boolean {
|
||||
const token = cleanToken(tokens[idx]);
|
||||
if (!token || !isCapitalized(token) || isBadSingleNameToken(token))
|
||||
return false;
|
||||
if (hasInternalCapOrDigit(token) || isListItemNameToken(tokens, idx))
|
||||
return true;
|
||||
}
|
||||
if (FORMATTING_MARKERS.has(prev)) {
|
||||
return true;
|
||||
}
|
||||
// Check for newline before this token in the raw text
|
||||
const tokenStart = rawText.indexOf(tokens[idx]);
|
||||
if (tokenStart > 0 && rawText.charAt(tokenStart - 1) === "\n") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return !isSentenceStart(tokens, idx);
|
||||
}
|
||||
|
||||
function cleanEntityText(text: string): string {
|
||||
return text
|
||||
.replace(/^\*+\s*|\s*\*+$/g, "")
|
||||
.replace(/\s*:+$/g, "")
|
||||
.replace(/^\d+\s*\.\s*/, "")
|
||||
.replace(/\s+\d[\d,]*(?:\.\d+)?$/g, "")
|
||||
.replace(/[.,;!?]+$/, "")
|
||||
.trim()
|
||||
.replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function isCoordinatedNameTopic(text: string): boolean {
|
||||
return /\b[A-Z][\w-]+\s+and\s+[A-Z][\w-]+\b/.test(text);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -397,86 +499,79 @@ function extractQuoted(text: string): ExtractedEntity[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract proper noun sequences using capitalization heuristics.
|
||||
* Finds sequences of capitalized words that are not at sentence starts.
|
||||
* Extract dotted technical identifiers such as person.properties.email.
|
||||
*/
|
||||
function extractIdentifiers(text: string): ExtractedEntity[] {
|
||||
const entities: ExtractedEntity[] = [];
|
||||
const identifierRe = /\b[A-Za-z_][\w-]*(?:\.[A-Za-z_][\w-]*)+\b/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = identifierRe.exec(text)) !== null) {
|
||||
entities.push({ type: "IDENTIFIER", text: match[0] });
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract proper names using capitalization and list-context heuristics.
|
||||
*/
|
||||
function extractProper(text: string): ExtractedEntity[] {
|
||||
const entities: ExtractedEntity[] = [];
|
||||
// Tokenize on whitespace, preserving order
|
||||
const tokens = text.split(/\s+/).filter(Boolean);
|
||||
const functionWords = new Set([
|
||||
"'s",
|
||||
"of",
|
||||
"the",
|
||||
"in",
|
||||
"and",
|
||||
"for",
|
||||
"at",
|
||||
"is",
|
||||
]);
|
||||
const tokens = tokenize(text);
|
||||
const innerConnectors = new Set(["of", "the", "in", "for", "at"]);
|
||||
|
||||
let i = 0;
|
||||
while (i < tokens.length) {
|
||||
const tok = tokens[i];
|
||||
// Skip formatting markers
|
||||
if (FORMATTING_MARKERS.has(tok)) {
|
||||
const token = cleanToken(tokens[i]);
|
||||
const next = i + 1 < tokens.length ? tokens[i + 1] : "";
|
||||
const afterNext = i + 2 < tokens.length ? cleanToken(tokens[i + 2]) : "";
|
||||
if (
|
||||
token &&
|
||||
next === "&" &&
|
||||
afterNext &&
|
||||
isCapitalized(token) &&
|
||||
isCapitalized(afterNext) &&
|
||||
!isBadSingleNameToken(token) &&
|
||||
!isBadSingleNameToken(afterNext)
|
||||
) {
|
||||
entities.push({
|
||||
type: "PROPER",
|
||||
text: cleanEntityText(`${token} & ${afterNext}`),
|
||||
});
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isNameToken(tokens, i)) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const isLabel = i + 1 < tokens.length && tokens[i + 1] === ":";
|
||||
const isCap =
|
||||
tok.length > 0 &&
|
||||
tok.charAt(0) === tok.charAt(0).toUpperCase() &&
|
||||
/[A-Z]/.test(tok.charAt(0));
|
||||
|
||||
if (isCap && !isLabel) {
|
||||
const seq: Array<{ token: string; idx: number }> = [
|
||||
{ token: tok, idx: i },
|
||||
];
|
||||
let j = i + 1;
|
||||
while (j < tokens.length) {
|
||||
const t = tokens[j];
|
||||
const tIsCap =
|
||||
t.length > 0 &&
|
||||
t.charAt(0) === t.charAt(0).toUpperCase() &&
|
||||
/[A-Z]/.test(t.charAt(0));
|
||||
if (tIsCap || functionWords.has(t.toLowerCase())) {
|
||||
seq.push({ token: t, idx: j });
|
||||
j++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
const span = [cleanToken(tokens[i])];
|
||||
let j = i + 1;
|
||||
while (j < tokens.length) {
|
||||
const current = cleanToken(tokens[j]);
|
||||
if (isNameToken(tokens, j)) {
|
||||
span.push(current);
|
||||
j++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip trailing function words
|
||||
while (
|
||||
seq.length > 0 &&
|
||||
functionWords.has(seq[seq.length - 1].token.toLowerCase())
|
||||
if (
|
||||
innerConnectors.has(current.toLowerCase()) &&
|
||||
j + 1 < tokens.length &&
|
||||
isNameToken(tokens, j + 1)
|
||||
) {
|
||||
seq.pop();
|
||||
span.push(current, cleanToken(tokens[j + 1]));
|
||||
j += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seq.length > 0) {
|
||||
// Check for at least one mid-sentence capitalized word
|
||||
const hasMidCap = seq.some(({ token, idx: tokenIdx }) => {
|
||||
const isCapWord =
|
||||
/[A-Z]/.test(token.charAt(0)) &&
|
||||
!functionWords.has(token.toLowerCase());
|
||||
return isCapWord && !isSentenceStart(tokens, tokenIdx, text);
|
||||
});
|
||||
|
||||
if (hasMidCap) {
|
||||
const phrase = seq.map((s) => s.token).join(" ");
|
||||
if (phrase.length > 2) {
|
||||
entities.push({ type: "PROPER", text: phrase });
|
||||
}
|
||||
}
|
||||
}
|
||||
i = j;
|
||||
} else {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
|
||||
const phrase = cleanEntityText(span.join(" "));
|
||||
if (phrase.length > 2) {
|
||||
entities.push({ type: "PROPER", text: phrase });
|
||||
}
|
||||
i = Math.max(j, i + 1);
|
||||
}
|
||||
|
||||
return entities;
|
||||
@@ -484,7 +579,7 @@ function extractProper(text: string): ExtractedEntity[] {
|
||||
|
||||
/**
|
||||
* Extract compound noun phrases using the `compromise` NLP library.
|
||||
* Returns COMPOUND and NOUN entities derived from noun chunks.
|
||||
* Returns TOPIC entities derived from noun chunks.
|
||||
*/
|
||||
function extractCompoundsWithNlp(text: string): ExtractedEntity[] {
|
||||
if (!nlp) {
|
||||
@@ -524,12 +619,12 @@ function extractCompoundsWithNlp(text: string): ExtractedEntity[] {
|
||||
const filtered = words.filter(
|
||||
(w) => !NON_SPECIFIC_ADJ.has(w.toLowerCase()),
|
||||
);
|
||||
const cleaned = stripGenericEnding(filtered);
|
||||
const cleaned = stripGenericEnding(stripTopicPrefix(filtered));
|
||||
|
||||
if (cleaned.length >= 2) {
|
||||
const phrase = cleaned.join(" ");
|
||||
const phrase = cleanEntityText(cleaned.join(" "));
|
||||
if (phrase.length > 3) {
|
||||
entities.push({ type: "COMPOUND", text: phrase });
|
||||
entities.push({ type: "TOPIC", text: phrase });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,7 +642,7 @@ function extractCompoundsRegex(text: string): ExtractedEntity[] {
|
||||
// Multi-word sequences with at least one non-trivial word
|
||||
// Match sequences like "machine learning", "New York", "data science"
|
||||
const compoundRe =
|
||||
/\b([A-Z][a-z]+(?:\s+(?:of|and|the|for|in)\s+)?[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\b/g;
|
||||
/\b([A-Z][a-z]+(?:\s+(?:of|the|for|in)\s+)?[A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)\b/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = compoundRe.exec(text)) !== null) {
|
||||
const phrase = match[1].trim();
|
||||
@@ -558,9 +653,12 @@ function extractCompoundsRegex(text: string): ExtractedEntity[] {
|
||||
const filtered = words.filter(
|
||||
(w) => !NON_SPECIFIC_ADJ.has(w.toLowerCase()),
|
||||
);
|
||||
const cleaned = stripGenericEnding(filtered);
|
||||
const cleaned = stripGenericEnding(stripTopicPrefix(filtered));
|
||||
if (cleaned.length >= 2) {
|
||||
entities.push({ type: "COMPOUND", text: cleaned.join(" ") });
|
||||
entities.push({
|
||||
type: "TOPIC",
|
||||
text: cleanEntityText(cleaned.join(" ")),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -590,9 +688,12 @@ function extractCompoundsRegex(text: string): ExtractedEntity[] {
|
||||
const filtered = words.filter(
|
||||
(w) => !NON_SPECIFIC_ADJ.has(w.toLowerCase()),
|
||||
);
|
||||
const cleaned = stripGenericEnding(filtered);
|
||||
const cleaned = stripGenericEnding(stripTopicPrefix(filtered));
|
||||
if (cleaned.length >= 2) {
|
||||
entities.push({ type: "COMPOUND", text: cleaned.join(" ") });
|
||||
entities.push({
|
||||
type: "TOPIC",
|
||||
text: cleanEntityText(cleaned.join(" ")),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -614,9 +715,9 @@ function extractCompoundsRegex(text: string): ExtractedEntity[] {
|
||||
*
|
||||
* Entity types (in priority order for deduplication):
|
||||
* PROPER - Capitalized multi-word sequences not at sentence start
|
||||
* COMPOUND - Multi-word noun phrases with specific modifiers
|
||||
* IDENTIFIER - Dotted technical identifiers
|
||||
* QUOTED - Text in single or double quotes (min 3 chars)
|
||||
* NOUN - Single nouns from circumstantial patterns
|
||||
* TOPIC - Multi-word noun/topic phrases with specific modifiers
|
||||
*
|
||||
* @param text - Input text to extract entities from.
|
||||
* @returns Deduplicated list of extracted entities.
|
||||
@@ -630,7 +731,10 @@ export function extractEntities(text: string): ExtractedEntity[] {
|
||||
// 2. PROPER entities (capitalization heuristics)
|
||||
raw.push(...extractProper(text));
|
||||
|
||||
// 3. COMPOUND entities (NLP or regex fallback)
|
||||
// 3. IDENTIFIER entities
|
||||
raw.push(...extractIdentifiers(text));
|
||||
|
||||
// 4. TOPIC entities (NLP or regex fallback)
|
||||
if (nlp) {
|
||||
raw.push(...extractCompoundsWithNlp(text));
|
||||
} else {
|
||||
@@ -654,19 +758,17 @@ export function extractEntities(text: string): ExtractedEntity[] {
|
||||
const cleaned: ExtractedEntity[] = [];
|
||||
for (const entity of deduped) {
|
||||
let txt = entity.text.trim();
|
||||
// Strip leading/trailing asterisks
|
||||
txt = txt.replace(/^\*+\s*|\s*\*+$/g, "");
|
||||
// Strip trailing colons
|
||||
txt = txt.replace(/\s*:+$/, "");
|
||||
// Strip leading numbered list markers
|
||||
txt = txt.replace(/^\d+\s*\.\s*/, "");
|
||||
// Strip trailing sentence punctuation (".", ",", ";", "!", "?") — otherwise
|
||||
// "Paris." and "Paris" produce different embeddings and break entity dedup.
|
||||
txt = txt.replace(/[.,;!?]+$/, "").trim();
|
||||
txt = cleanEntityText(txt);
|
||||
|
||||
if (!txt || txt.length <= 2 || hasArtifacts(txt)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
entity.type === "TOPIC" &&
|
||||
(/^\d/.test(txt) || isCoordinatedNameTopic(txt))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter generic single-word PROPER nouns
|
||||
if (
|
||||
@@ -680,12 +782,12 @@ export function extractEntities(text: string): ExtractedEntity[] {
|
||||
cleaned.push({ type: entity.type, text: txt });
|
||||
}
|
||||
|
||||
// Keep best type per entity (PROPER > COMPOUND > QUOTED > NOUN)
|
||||
// Keep best type per entity (PROPER > IDENTIFIER > QUOTED > TOPIC)
|
||||
const typePriority: Record<string, number> = {
|
||||
PROPER: 0,
|
||||
COMPOUND: 1,
|
||||
IDENTIFIER: 1,
|
||||
QUOTED: 2,
|
||||
NOUN: 3,
|
||||
TOPIC: 3,
|
||||
};
|
||||
const best = new Map<string, ExtractedEntity>();
|
||||
for (const entity of cleaned) {
|
||||
@@ -700,14 +802,17 @@ export function extractEntities(text: string): ExtractedEntity[] {
|
||||
}
|
||||
const bestEntities = Array.from(best.values());
|
||||
|
||||
// Remove entities that are substrings of longer entities
|
||||
const allLower = bestEntities.map((e) => e.text.toLowerCase());
|
||||
// Remove entities that are token substrings of longer entities.
|
||||
return bestEntities.filter(
|
||||
(entity) =>
|
||||
!allLower.some(
|
||||
!bestEntities.some(
|
||||
(other) =>
|
||||
entity.text.toLowerCase() !== other &&
|
||||
other.includes(entity.text.toLowerCase()),
|
||||
entity.text.toLowerCase() !== other.text.toLowerCase() &&
|
||||
(typePriority[entity.type] ?? 99) >=
|
||||
(typePriority[other.type] ?? 99) &&
|
||||
new RegExp(
|
||||
`(^|\\s)${entity.text.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`,
|
||||
).test(other.text.toLowerCase()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Client as ClientType } from "pg";
|
||||
import type { Client as ClientType, ClientConfig } from "pg";
|
||||
import pkg from "pg";
|
||||
const { Client, escapeIdentifier } = pkg;
|
||||
import { VectorStore } from "./base";
|
||||
@@ -157,41 +157,90 @@ export function buildFilterConditions(
|
||||
|
||||
interface PGVectorConfig extends VectorStoreConfig {
|
||||
dbname?: string;
|
||||
user: string;
|
||||
password: string;
|
||||
host: string;
|
||||
port: number;
|
||||
user?: string;
|
||||
password?: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
connectionString?: string;
|
||||
ssl?: ClientConfig["ssl"];
|
||||
embeddingModelDims: number;
|
||||
diskann?: boolean;
|
||||
hnsw?: boolean;
|
||||
}
|
||||
|
||||
function getConnectionString(config: PGVectorConfig): string | undefined {
|
||||
return config.connectionString?.trim() || undefined;
|
||||
}
|
||||
|
||||
function validateConnectionConfig(config: PGVectorConfig): void {
|
||||
if (getConnectionString(config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const missingFields = ["user", "password", "host", "port"].filter((field) => {
|
||||
const v = config[field as keyof PGVectorConfig];
|
||||
return v === undefined || v === null || v === "";
|
||||
});
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
throw new Error(
|
||||
`PGVector requires either connectionString or ${missingFields.join(", ")}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function buildClientConfig(
|
||||
config: PGVectorConfig,
|
||||
database?: string,
|
||||
): ClientConfig {
|
||||
const connectionString = getConnectionString(config);
|
||||
if (connectionString) {
|
||||
return {
|
||||
connectionString,
|
||||
...(config.ssl !== undefined ? { ssl: config.ssl } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
database,
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
...(config.ssl !== undefined ? { ssl: config.ssl } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export class PGVector implements VectorStore {
|
||||
private client: ClientType;
|
||||
private collectionName: string;
|
||||
private useDiskann: boolean;
|
||||
private useHnsw: boolean;
|
||||
private readonly dbName: string;
|
||||
private readonly useDirectConnection: boolean;
|
||||
private config: PGVectorConfig;
|
||||
private _initPromise?: Promise<void>;
|
||||
|
||||
constructor(config: PGVectorConfig) {
|
||||
validateConnectionConfig(config);
|
||||
this.collectionName = validateIdentifier(
|
||||
config.collectionName || "memories",
|
||||
"collectionName",
|
||||
);
|
||||
this.useDiskann = config.diskann || false;
|
||||
this.useHnsw = config.hnsw || false;
|
||||
this.dbName = validateIdentifier(config.dbname || "vector_store", "dbname");
|
||||
this.useDirectConnection = !!getConnectionString(config);
|
||||
this.dbName = this.useDirectConnection
|
||||
? ""
|
||||
: validateIdentifier(config.dbname || "vector_store", "dbname");
|
||||
this.config = config;
|
||||
|
||||
this.client = new Client({
|
||||
database: "postgres", // Initially connect to default postgres database
|
||||
user: config.user,
|
||||
password: config.password,
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
});
|
||||
this.client = new Client(
|
||||
buildClientConfig(
|
||||
config,
|
||||
this.useDirectConnection ? undefined : "postgres",
|
||||
),
|
||||
);
|
||||
this.initialize().catch(console.error);
|
||||
}
|
||||
|
||||
@@ -210,29 +259,20 @@ export class PGVector implements VectorStore {
|
||||
try {
|
||||
await this.client.connect();
|
||||
|
||||
// Check if database exists
|
||||
const dbExists = await this.checkDatabaseExists(this.dbName);
|
||||
if (!dbExists) {
|
||||
await this.createDatabase(this.dbName);
|
||||
if (!this.useDirectConnection) {
|
||||
const dbExists = await this.checkDatabaseExists(this.dbName);
|
||||
if (!dbExists) {
|
||||
await this.createDatabase(this.dbName);
|
||||
}
|
||||
|
||||
await this.client.end();
|
||||
|
||||
this.client = new Client(buildClientConfig(this.config, this.dbName));
|
||||
await this.client.connect();
|
||||
}
|
||||
|
||||
// Disconnect from postgres database
|
||||
await this.client.end();
|
||||
|
||||
// Connect to the target database
|
||||
this.client = new Client({
|
||||
database: this.dbName,
|
||||
user: this.config.user,
|
||||
password: this.config.password,
|
||||
host: this.config.host,
|
||||
port: this.config.port,
|
||||
});
|
||||
await this.client.connect();
|
||||
|
||||
// Create vector extension
|
||||
await this.client.query("CREATE EXTENSION IF NOT EXISTS vector");
|
||||
|
||||
// Create memory_migrations table
|
||||
await this.client.query(`
|
||||
CREATE TABLE IF NOT EXISTS memory_migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@@ -240,7 +280,6 @@ export class PGVector implements VectorStore {
|
||||
)
|
||||
`);
|
||||
|
||||
// Check if the collection exists
|
||||
const collections = await this.listCols();
|
||||
if (!collections.includes(this.collectionName)) {
|
||||
await this.createCol(this.config.embeddingModelDims);
|
||||
|
||||
@@ -337,11 +337,14 @@ export class RedisDB implements VectorStore {
|
||||
const id = ids[idx];
|
||||
|
||||
// Create entry with required fields
|
||||
const createdAt = payload.created_at
|
||||
? new Date(payload.created_at).getTime()
|
||||
: 0;
|
||||
const entry: Record<string, any> = {
|
||||
memory_id: id,
|
||||
hash: payload.hash,
|
||||
memory: payload.data,
|
||||
created_at: new Date(payload.created_at).getTime(),
|
||||
hash: payload.hash ?? "",
|
||||
memory: payload.data ?? "",
|
||||
created_at: createdAt,
|
||||
embedding: new Float32Array(vector).buffer,
|
||||
};
|
||||
|
||||
@@ -561,12 +564,18 @@ export class RedisDB implements VectorStore {
|
||||
payload: Record<string, any>,
|
||||
): Promise<void> {
|
||||
const snakePayload = toSnakeCase(payload);
|
||||
const createdAt = snakePayload.created_at
|
||||
? new Date(snakePayload.created_at).getTime()
|
||||
: 0;
|
||||
const updatedAt = snakePayload.updated_at
|
||||
? new Date(snakePayload.updated_at).getTime()
|
||||
: 0;
|
||||
const entry: Record<string, any> = {
|
||||
memory_id: vectorId,
|
||||
hash: snakePayload.hash,
|
||||
memory: snakePayload.data,
|
||||
created_at: new Date(snakePayload.created_at).getTime(),
|
||||
updated_at: new Date(snakePayload.updated_at).getTime(),
|
||||
hash: snakePayload.hash ?? "",
|
||||
memory: snakePayload.data ?? "",
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt,
|
||||
embedding: Buffer.from(new Float32Array(vector).buffer),
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { extractEntities } from "../src/utils/entity_extraction";
|
||||
|
||||
describe("extractEntities", () => {
|
||||
it("handles product lists, coordinated names, and identifiers", () => {
|
||||
const text =
|
||||
"User reported top inbound integration pages: OpenClaw 25,443, " +
|
||||
"Claude Code 8,916, Codex 2,573, Dify 656. " +
|
||||
"User compared Cartesia and Deepgram. " +
|
||||
"The email field for Mem0 lives at person.properties.email. " +
|
||||
"The qwen endpoint uses person.properties.email. " +
|
||||
"Johnson & Johnson was mentioned. " +
|
||||
"Glasses around my window. " +
|
||||
"On 2026-05-27 there were 90 days of stats.";
|
||||
|
||||
const entityTexts = new Set(
|
||||
extractEntities(text).map((entity) => entity.text),
|
||||
);
|
||||
const normalized = new Set(
|
||||
[...entityTexts].map((entityText) => entityText.toLowerCase()),
|
||||
);
|
||||
|
||||
for (const expected of [
|
||||
"OpenClaw",
|
||||
"Claude Code",
|
||||
"Codex",
|
||||
"Dify",
|
||||
"Cartesia",
|
||||
"Deepgram",
|
||||
"Mem0",
|
||||
]) {
|
||||
expect(entityTexts.has(expected)).toBe(true);
|
||||
}
|
||||
expect(entityTexts.has("person.properties.email")).toBe(true);
|
||||
expect(entityTexts.has("qwen endpoint")).toBe(true);
|
||||
expect(entityTexts.has("Johnson & Johnson")).toBe(true);
|
||||
expect(entityTexts.has("Johnson")).toBe(false);
|
||||
expect(normalized.has("top")).toBe(false);
|
||||
expect(normalized.has("glasses")).toBe(false);
|
||||
expect(entityTexts.has("Cartesia and Deepgram")).toBe(false);
|
||||
expect(entityTexts.has("Claude Code 8,916")).toBe(false);
|
||||
for (const rejected of ["8,916", "2,573", "656", "2026-05-27", "90"]) {
|
||||
expect(entityTexts.has(rejected)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
/// <reference types="jest" />
|
||||
|
||||
jest.mock("pg", () => {
|
||||
const Client = jest.fn().mockImplementation(() => ({
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user