Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d3486ca56 | ||
|
|
ebb9bb2b15 | ||
|
|
594b4e65d6 | ||
|
|
1b95c99db4 | ||
|
|
b66cf0f272 | ||
|
|
72dca1cdf5 | ||
|
|
ece7ff6b84 | ||
|
|
30ce028a71 | ||
|
|
bd9d27ff50 | ||
|
|
08b746c9be | ||
|
|
693e709389 | ||
|
|
553e275112 | ||
|
|
43dde3b186 | ||
|
|
cca7551192 | ||
|
|
5be2630f5b |
@@ -12,7 +12,7 @@
|
||||
"name": "mem0",
|
||||
"source": "./mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
|
||||
"version": "0.1.0"
|
||||
"version": "0.1.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"name": "mem0",
|
||||
"source": "./mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
|
||||
"version": "0.1.0"
|
||||
"version": "0.1.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://mem0.ai/research"><strong>📄 Building Production-Ready AI Agents with Scalable Long-Term Memory →</strong></a>
|
||||
<a href="https://mem0.ai/research"><strong>📄 Benchmarking Mem0's token-efficient memory algorithm →</strong></a>
|
||||
</p>
|
||||
|
||||
## New Memory Algorithm (April 2026)
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
---
|
||||
title: 'Add Memories'
|
||||
description: "Add facts, messages, or metadata to a user memory store with support for async processing and event tracking."
|
||||
openapi: post /v1/memories/
|
||||
title: Add Memories
|
||||
description: "Add facts, messages, or metadata to a user memory store with async processing and event tracking via the V3 additive pipeline."
|
||||
openapi: post /v3/memories/add/
|
||||
---
|
||||
|
||||
Add new facts, messages, or metadata to a user’s memory store. The Add Memories endpoint accepts either raw text or conversational turns and commits them asynchronously so the memory is ready for later search, retrieval, and graph queries.
|
||||
Extract and store memories from a conversation using the V3 additive pipeline. The endpoint uses single-pass ADD-only extraction — one LLM call, no UPDATE/DELETE. Memories accumulate over time; nothing is overwritten.
|
||||
|
||||
## Endpoint
|
||||
|
||||
- **Method**: `POST`
|
||||
- **URL**: `/v1/memories/`
|
||||
- **URL**: `/v3/memories/add/`
|
||||
- **Content-Type**: `application/json`
|
||||
|
||||
Memories are processed asynchronously by default. The response contains queued events you can track while the platform finalizes enrichment.
|
||||
Processing is asynchronous. The response returns an `event_id` you can poll via `GET /v1/event/{event_id}/`.
|
||||
|
||||
## Required headers
|
||||
|
||||
@@ -23,7 +23,7 @@ Memories are processed asynchronously by default. The response contains queued e
|
||||
|
||||
## Request body
|
||||
|
||||
Provide at least one message or direct memory string. Most callers supply `messages` so Mem0 can infer structured memories as part of ingestion.
|
||||
Provide conversation messages for Mem0 to extract memories from. At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required so the memory is scoped to a session. Entity IDs are accepted at the top level.
|
||||
|
||||
<CodeGroup>
|
||||
```json Basic request
|
||||
@@ -43,12 +43,15 @@ Provide at least one message or direct memory string. Most callers supply `messa
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `user_id` | string | No* | Associates the memory with a user. Provide when you want the memory scoped to a specific identity. |
|
||||
| `messages` | array | No* | Conversation turns for Mem0 to infer memories from. Each object should include `role` and `content`. |
|
||||
| `messages` | array | Yes | Conversation turns for Mem0 to extract memories from. Each object should include `role` and `content`. |
|
||||
| `user_id` | string | No* | Associates the memory with a user. |
|
||||
| `agent_id` | string | No* | Associates the memory with an agent. |
|
||||
| `run_id` | string | No* | Associates the memory with a run. |
|
||||
| `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. |
|
||||
|
||||
> \* Provide at least one `messages` entry to describe what you are storing. For scoped memories, include `user_id`. You can also attach `agent_id`, `app_id`, `run_id`, `project_id`, or `org_id` to refine ownership.
|
||||
> \* At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required.
|
||||
|
||||
<Tip>
|
||||
Need more details? See [all request parameters](#body-messages) below for complete field descriptions, types, and constraints.
|
||||
@@ -56,19 +59,15 @@ Provide at least one message or direct memory string. Most callers supply `messa
|
||||
|
||||
## Response
|
||||
|
||||
Successful requests return an array of events queued for processing. Each event includes the generated memory text and an identifier you can persist for auditing.
|
||||
The request is queued for background processing. The response contains an `event_id` for tracking status.
|
||||
|
||||
<CodeGroup>
|
||||
```json 200 response
|
||||
[
|
||||
{
|
||||
"id": "mem_01JF8ZS4Y0R0SPM13R5R6H32CJ",
|
||||
"event": "ADD",
|
||||
"data": {
|
||||
"memory": "The user moved to Austin in 2025."
|
||||
}
|
||||
}
|
||||
]
|
||||
{
|
||||
"message": "Memory processing has been queued for background execution",
|
||||
"status": "PENDING",
|
||||
"event_id": "evt-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
```json 400 response
|
||||
@@ -81,3 +80,7 @@ Successful requests return an array of events queued for processing. Each event
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Info>
|
||||
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
|
||||
</Info>
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
---
|
||||
title: "Get Memories"
|
||||
description: "Retrieve memories with advanced filtering using logical operators like AND, OR, NOT, and comparison queries."
|
||||
openapi: post /v2/memories/
|
||||
description: "Retrieve memories with paginated results and advanced filtering using logical operators like AND, OR, NOT, and comparison queries."
|
||||
openapi: post /v3/memories/
|
||||
---
|
||||
|
||||
The v2 get memories API is powerful and flexible, allowing for more precise memory listing without the need for a search query. It supports complex logical operations (AND, OR, NOT) and comparison operators for advanced filtering capabilities. The comparison operators include:
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -15,6 +17,8 @@ The v2 get memories API is powerful and flexible, allowing for more precise memo
|
||||
- `icontains`: Case-insensitive containment check
|
||||
- `*`: Wildcard character that matches everything
|
||||
|
||||
Pass `page` and `page_size` as query parameters to paginate through results.
|
||||
|
||||
<CodeGroup>
|
||||
```python Code
|
||||
memories = client.get_all(
|
||||
@@ -27,12 +31,17 @@ memories = client.get_all(
|
||||
"created_at": {"gte": "2024-07-01", "lte": "2024-07-31"}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
page=1,
|
||||
page_size=50
|
||||
)
|
||||
```
|
||||
|
||||
```python Output
|
||||
{
|
||||
"count": 2,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
|
||||
@@ -46,10 +55,13 @@ memories = client.get_all(
|
||||
"created_at": "2024-07-05T15:30:00Z",
|
||||
"updated_at": "2024-07-05T15:30:00Z"
|
||||
}
|
||||
],
|
||||
"total": 2
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
<Info>
|
||||
The response is a paginated envelope with `count`, `next`, `previous`, and `results`. Use `page` and `page_size` query params to step through results.
|
||||
</Info>
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
---
|
||||
title: 'Search Memories'
|
||||
description: "Search memories with semantic queries and advanced filtering using logical and comparison operators."
|
||||
openapi: post /v2/memories/search/
|
||||
description: "Search memories with hybrid retrieval (semantic + BM25 + entity matching) and advanced filtering using logical and comparison operators."
|
||||
openapi: post /v3/memories/search/
|
||||
---
|
||||
|
||||
The v2 search API is powerful and flexible, allowing for more precise memory retrieval. It supports complex logical operations (AND, OR, NOT) and comparison operators for advanced filtering capabilities. The comparison operators include:
|
||||
Relevance-ranked hybrid search across stored memories. V3 uses multi-signal retrieval — semantic, BM25 keyword, and entity matching scored in parallel and fused. The returned `score` is a combined `[0, 1]` value.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
- `lte`: Less than or equal to
|
||||
@@ -14,6 +18,14 @@ The v2 search API is powerful and flexible, allowing for more precise memory ret
|
||||
- `icontains`: Case-insensitive containment check
|
||||
- `*`: Wildcard character that matches everything
|
||||
|
||||
### 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) |
|
||||
|
||||
<CodeGroup>
|
||||
```python Platform API Example
|
||||
related_memories = client.search(
|
||||
@@ -33,20 +45,19 @@ related_memories = client.search(
|
||||
|
||||
```json Output
|
||||
{
|
||||
"memories": [
|
||||
"results": [
|
||||
{
|
||||
"id": "ea925981-272f-40dd-b576-be64e4871429",
|
||||
"memory": "Likes to play cricket and plays cricket on weekends.",
|
||||
"metadata": {
|
||||
"category": "hobbies"
|
||||
},
|
||||
"score": 0.32116443111457704,
|
||||
"score": 0.82,
|
||||
"created_at": "2024-07-26T10:29:36.630547-07:00",
|
||||
"updated_at": null,
|
||||
"user_id": "alice",
|
||||
"agent_id": "sports-agent"
|
||||
"categories": ["hobbies"]
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -4,6 +4,36 @@ description: "Release notes for the OpenClaw plugin and agent harness."
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Update label="2026-04-29" description="v1.0.11">
|
||||
|
||||
**New Features:**
|
||||
- **Skills-mode auto-setup:** `enableSkillsConfig()` now runs automatically after onboarding — enables triage, recall (with reranking + keyword search), and dream consolidation with `tools.profile = "full"` and disables the built-in session-memory hook to avoid conflicts
|
||||
- **Memory runtime capability:** Plugin now exposes `runtime.getMemorySearchManager()` and `resolveMemoryBackendConfig()` on the registered memory capability, enabling OpenClaw gateway to query memory status and backend config directly
|
||||
- **Dimension-aware collections:** OSS wizard detects embedder dimension changes and creates a new collection (`mem0_<dims>d`) automatically, with a warning about old memories being inaccessible under the new embedder
|
||||
- **Tool documentation in skills:** Both `memory-triage` and `memory-dream` SKILL.md files now include full tool reference sections listing all available tools with parameters
|
||||
|
||||
**Improvements:**
|
||||
- **Auto-capture and auto-recall default to enabled:** `autoCapture` and `autoRecall` now default to `true` (was `false`). Manifest descriptions updated accordingly. Ignored in skills mode
|
||||
- **`memory_update` over delete+add:** Skills now prefer `memory_update` for in-place edits — atomic and preserves edit history. Consolidation pattern updated: update best memory, delete redundant ones
|
||||
- **Search threshold lowered:** Default `searchThreshold` reduced from `0.5` to `0.1` for broader recall. Removed hardcoded `0.6` recall-specific override — all searches now use the configured threshold
|
||||
- **Embedder dimension propagation:** Vector store config auto-resolves dimensions from embedder config when not explicitly set. Syncs `dimension` and `embeddingModelDims` fields for Qdrant/PGVector compatibility
|
||||
- **Config file write safety:** `writeFullConfig()` now re-reads and deep-merges the `plugins` section before writing, preserving `installs` and `slots` written by the OpenClaw gateway
|
||||
- **Additional embedder models:** Added `mxbai-embed-large` (1024), `all-minilm` (384), and `snowflake-arctic-embed` (1024) to known embedder dimensions
|
||||
|
||||
**Security:**
|
||||
- Bumped `protobufjs` to `>=7.5.5` via pnpm overrides (GHSA-xq3m-2v4x-88gg) ([#5012](https://github.com/mem0ai/mem0/pull/5012))
|
||||
|
||||
**Fixes:**
|
||||
- Moved `bootstrapTelemetryFlag()` and removed `ensureInstallRecord()` from module-level side effects — both now run inside `register()` to avoid crashes when loaded outside OpenClaw gateway
|
||||
- Fixed OSS history DB path resolution: absolute paths no longer passed through `resolvePath()`, preventing double-prefix bugs
|
||||
- Manifest `providerAuthEnvVars` replaced with spec-compliant `setup.providers` format using `id` + `envVars`
|
||||
|
||||
**Dependencies:**
|
||||
- Bumped `mem0ai` from `3.0.1` to `3.0.2`
|
||||
- Bumped `pluginApi` and `minGatewayVersion` compat to `>=2026.4.24`
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-23" description="v1.0.10">
|
||||
|
||||
**Security:**
|
||||
|
||||
@@ -7,6 +7,23 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<Update label="2026-04-25" description="v2.0.1">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Client:** Map `user_id`, `agent_id`, `run_id` entity params to filters in `GET /memories` ([#4960](https://github.com/mem0ai/mem0/pull/4960))
|
||||
- **Memory:** Honor `prompt` param in vector store extraction pipeline ([#4914](https://github.com/mem0ai/mem0/pull/4914))
|
||||
- **Memory:** Add missing `text_lemmatized` field in `AsyncMemory._create_memory` ([#4886](https://github.com/mem0ai/mem0/pull/4886))
|
||||
- **Memory:** Merge same-key operator dicts in AND metadata filters ([#4853](https://github.com/mem0ai/mem0/pull/4853))
|
||||
- **LLMs:** Narrow `_is_reasoning_model` check to not match `gpt-5.x` variants ([#4746](https://github.com/mem0ai/mem0/pull/4746))
|
||||
- **Vector Stores:** Add `ca_certs` config option for Elasticsearch vector store ([#3993](https://github.com/mem0ai/mem0/pull/3993))
|
||||
- **Vector Stores:** Add `agent_id` and `run_id` to Elasticsearch/OpenSearch default mappings ([#4906](https://github.com/mem0ai/mem0/pull/4906))
|
||||
- **Embeddings:** Set FastEmbed `embedding_dims` from model metadata at init ([#4711](https://github.com/mem0ai/mem0/pull/4711))
|
||||
|
||||
**Security:**
|
||||
- Bump vulnerable dependencies to patched versions ([#4835](https://github.com/mem0ai/mem0/pull/4835))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-14" description="v2.0.0">
|
||||
|
||||
**Major Release** — Python SDK with V3 memory pipeline, ADD-only extraction, and cleaned-up API surface.
|
||||
@@ -893,6 +910,17 @@ See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to-
|
||||
</Tab>
|
||||
|
||||
<Tab title="TypeScript">
|
||||
<Update label="2026-04-25" description="v3.0.2">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **LLMs:** Forward `timeout` config to OpenAI client in JS OSS LLM providers ([#4770](https://github.com/mem0ai/mem0/pull/4770))
|
||||
|
||||
**Improvements:**
|
||||
- **Telemetry:** Harden TS telemetry version injection and require changelog entry on version bump ([#4900](https://github.com/mem0ai/mem0/pull/4900))
|
||||
- **Docs:** Update memory tool list, CLI usage, and config file reading logic ([#4861](https://github.com/mem0ai/mem0/pull/4861))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-04-20" description="v3.0.1">
|
||||
|
||||
**Bug Fixes:**
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 293 KiB After Width: | Height: | Size: 139 KiB |
+81
-68
@@ -30,91 +30,100 @@ export MEM0_API_KEY="m0-your-api-key"
|
||||
|
||||
## Installation
|
||||
|
||||
### Option A — Repo Marketplace (Recommended for Teams)
|
||||
### Option A — Direct MCP (Recommended)
|
||||
|
||||
Add a `.agents/plugins/marketplace.json` to your repository root:
|
||||
The fastest way to connect Codex to Mem0 — no downloads, no marketplace. Codex reads MCP servers from `~/.codex/config.toml` as TOML. Add:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"interface": {
|
||||
"displayName": "Mem0 Plugins"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "./plugins/mem0"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
```toml
|
||||
[mcp_servers.mem0]
|
||||
url = "https://mcp.mem0.ai/mcp"
|
||||
bearer_token_env_var = "MEM0_API_KEY"
|
||||
```
|
||||
|
||||
Then in Codex, browse the repo's plugin directory and install Mem0.
|
||||
Make sure `MEM0_API_KEY` is exported in the shell you launch Codex from, then restart Codex.
|
||||
|
||||
### Option B — Personal Marketplace
|
||||
<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>
|
||||
|
||||
Add to `~/.agents/plugins/marketplace.json`:
|
||||
### Option B — Sideload the Plugin (Advanced)
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"interface": {
|
||||
"displayName": "Mem0 Plugins"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "/path/to/mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
For the full plugin experience — MCP server **plus** the Mem0 SDK skill, memory protocol skill, and opt-in lifecycle hooks — sideload the plugin from a local clone. The Mem0 repo already ships a marketplace manifest at [`.agents/plugins/marketplace.json`](https://github.com/mem0ai/mem0/blob/main/.agents/plugins/marketplace.json), so there's no JSON to author by hand. This follows the Codex [build-plugins](https://developers.openai.com/codex/plugins/build) local-testing workflow.
|
||||
|
||||
<Info>
|
||||
Don't combine Option B with Option A. The plugin manifest declares its MCP server via [`.codex-mcp.json`](https://github.com/mem0ai/mem0/blob/main/mem0-plugin/.codex-mcp.json), so Codex auto-registers the `mem0` MCP server when the plugin loads. Adding the same `[mcp_servers.mem0]` block to `~/.codex/config.toml` will create a duplicate registration.
|
||||
</Info>
|
||||
|
||||
**Step 1.** Clone the Mem0 repository anywhere on disk:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mem0ai/mem0.git ~/codex-plugins/mem0-source
|
||||
```
|
||||
|
||||
### Option C — Manual MCP Configuration
|
||||
**Step 2.** Register the bundled marketplace with Codex's CLI:
|
||||
|
||||
Add to your Codex MCP config:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```bash
|
||||
codex plugin marketplace add ~/codex-plugins/mem0-source
|
||||
```
|
||||
|
||||
This points Codex at the repo's `.agents/plugins/marketplace.json`. The bundled file uses `path: "./mem0-plugin"`, which Codex resolves relative to the clone root.
|
||||
|
||||
<Info>
|
||||
**Why we recommend this over hand-authoring `~/.agents/plugins/marketplace.json`:** Codex requires `source.path` in any marketplace manifest to be **relative** (starting with `./`) and **inside the marketplace root**. The repo's bundled manifest already satisfies this — the marketplace root is the clone directory, and `mem0-plugin/` lives inside it. With a personal `~/.agents/plugins/marketplace.json`, the root is `~/` and the clone has to live under `~/` too. The CLI form sidesteps that constraint.
|
||||
</Info>
|
||||
|
||||
**Step 3.** Restart Codex, run `/plugins`, browse the `Mem0 Plugins` marketplace, and install **Mem0**.
|
||||
|
||||
**Step 4 (optional) — enable lifecycle hooks.** Codex doesn't auto-wire hooks from plugin manifests; it only reads them from `~/.codex/hooks.json` (or `<repo>/.codex/hooks.json`). Run the bundled installer once to merge the Mem0 entries into your global hooks file:
|
||||
|
||||
```bash
|
||||
python3 ~/codex-plugins/mem0-source/mem0-plugin/scripts/install_codex_hooks.py
|
||||
```
|
||||
|
||||
Then enable the hooks feature flag in `~/.codex/config.toml`:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
codex_hooks = true
|
||||
```
|
||||
|
||||
Restart Codex. The installer registers three hooks pointing at scripts inside your clone:
|
||||
|
||||
| Event | Behavior |
|
||||
|-------|----------|
|
||||
| `SessionStart` | Loads prior memories as bootstrap context |
|
||||
| `UserPromptSubmit` | Injects relevant memories before each prompt |
|
||||
| `Stop` | Reminds the agent to persist learnings at turn end |
|
||||
|
||||
Re-running the installer is idempotent. To remove the hooks: `python3 ~/codex-plugins/mem0-source/mem0-plugin/scripts/install_codex_hooks.py --uninstall`.
|
||||
|
||||
<Warning>
|
||||
The hooks file stores absolute paths into your clone (e.g. `~/codex-plugins/mem0-source/mem0-plugin/scripts/...`). If you move or delete the clone, the hooks will break silently — re-run the installer from the new location, or run `--uninstall` first.
|
||||
</Warning>
|
||||
|
||||
### Managing the Plugin
|
||||
|
||||
Codex provides CLI commands for managing marketplaces after install:
|
||||
|
||||
```bash
|
||||
codex plugin marketplace upgrade # pull latest plugin versions
|
||||
codex plugin marketplace remove mem0-plugins # unregister the marketplace
|
||||
```
|
||||
|
||||
To pull updates to the plugin source itself, `git pull` inside your clone (`~/codex-plugins/mem0-source`) and then run `codex plugin marketplace upgrade` to refresh Codex's plugin cache. Plugins are cached at `~/.codex/plugins/cache/<marketplace>/<plugin>/<version>/`.
|
||||
|
||||
<Info icon="check">
|
||||
Start a new Codex task and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
|
||||
After either option, start a new Codex task and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
|
||||
</Info>
|
||||
|
||||
## What's Included
|
||||
|
||||
| Component | Plugin Install | MCP Only |
|
||||
|-----------|:--------------:|:--------:|
|
||||
| Component | Sideloaded Plugin | Direct MCP |
|
||||
|-----------|:-----------------:|:----------:|
|
||||
| MCP Server (9 memory tools) | Yes | Yes |
|
||||
| Memory Protocol Skill | Yes | No |
|
||||
| Mem0 SDK Skill | Yes | No |
|
||||
| Lifecycle Hooks (opt-in) | Yes | No |
|
||||
|
||||
## Available MCP Tools
|
||||
|
||||
@@ -134,7 +143,7 @@ Once installed, the following tools are available in every Codex session:
|
||||
|
||||
## Memory Protocol Skill
|
||||
|
||||
Codex uses a skill-based approach instead of lifecycle hooks. When installed via the plugin marketplace, the memory protocol skill instructs the agent to:
|
||||
When the plugin is sideloaded, the memory protocol skill instructs the agent to:
|
||||
|
||||
### On Every New Task
|
||||
1. Call `search_memories` with a query related to the current task to load relevant context
|
||||
@@ -199,8 +208,12 @@ You: Add WebSocket support for real-time notification delivery.
|
||||
|
||||
- **"Connection failed"** — Verify `MEM0_API_KEY` is set in your shell: `echo $MEM0_API_KEY`
|
||||
- **No tools appearing** — Restart your Codex session after plugin installation
|
||||
- **Plugin not found** — Ensure `.agents/plugins/marketplace.json` is at the repository root and `source.path` points to the correct plugin directory
|
||||
- **Skills not loading** — Verify the `skills` field in `plugin.json` points to a valid directory containing `SKILL.md` files
|
||||
- **Duplicate `mem0` MCP server / "tool collision" errors** — You combined Option A (Direct MCP) with Option B (sideload). The sideloaded plugin auto-registers `mem0` from `.codex-mcp.json`, so remove the `[mcp_servers.mem0]` block from `~/.codex/config.toml`.
|
||||
- **`plugin/read failed in TUI`** — Codex can't find the plugin directory the marketplace points at. If you used `codex plugin marketplace add <path>`, confirm the path is your clone root and that `<clone>/.agents/plugins/marketplace.json` exists. If you hand-authored `~/.agents/plugins/marketplace.json`, `source.path` must be relative (start with `./`), inside the marketplace root (`~/` for personal installs), and end in `mem0-plugin` — e.g. `"./codex-plugins/mem0-source/mem0-plugin"`.
|
||||
- **Plugin not found in `/plugins`** — Run `codex plugin marketplace add ~/path/to/clone` again, or confirm the marketplace was registered with `codex plugin marketplace remove mem0-plugins` then re-add.
|
||||
- **Skills not loading** — Verify the `skills` field in `plugin.json` points to a valid directory containing `SKILL.md` files.
|
||||
- **Hooks not firing** — Confirm `codex_hooks = true` is in `~/.codex/config.toml` under `[features]`, and that `~/.codex/hooks.json` contains the Mem0 entries (re-run the installer if not). Restart Codex after enabling the flag.
|
||||
- **Hooks broke after moving the clone** — The installer bakes absolute paths into `~/.codex/hooks.json` pointing at scripts inside your clone. If you moved or renamed the clone directory, run `python3 <new-clone>/mem0-plugin/scripts/install_codex_hooks.py` from the new location — the installer is idempotent and replaces the old entries.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Mem0 MCP Setup" icon="puzzle-piece" href="/platform/mem0-mcp">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: OpenClaw
|
||||
description: "Add long-term memory to OpenClaw agents using the Mem0 plugin with auto-recall and auto-capture support."
|
||||
description: "Add long-term memory to OpenClaw agents using the Mem0 plugin with skills-based memory extraction and recall."
|
||||
---
|
||||
|
||||
Add long-term memory to [OpenClaw](https://github.com/openclaw/openclaw) agents with the `@mem0/openclaw-mem0` plugin. Your agent forgets everything between sessions — this plugin fixes that by automatically watching conversations, extracting what matters, and bringing it back when relevant.
|
||||
@@ -12,11 +12,12 @@ Add long-term memory to [OpenClaw](https://github.com/openclaw/openclaw) agents
|
||||
</Frame>
|
||||
|
||||
The plugin provides:
|
||||
1. **Auto-Recall** — Before the agent responds, memories matching the current message are injected into context
|
||||
2. **Auto-Capture** — After the agent responds, the exchange is sent to Mem0 which decides what's worth keeping
|
||||
3. **Agent Tools** — Eight tools for explicit memory operations during conversations
|
||||
1. **Triage** — The agent extracts durable facts from conversations using a structured protocol with importance gates and domain overlays
|
||||
2. **Recall** — Before each turn, relevant memories are retrieved with reranking and injected into context
|
||||
3. **Dream** — Periodic memory consolidation: merges duplicates, resolves conflicts, prunes stale entries
|
||||
4. **Agent Tools** — Eight tools for explicit memory operations during conversations
|
||||
|
||||
Both auto-recall and auto-capture are opt-in (`autoRecall: true`, `autoCapture: true` in config). Once enabled, they run silently with no manual intervention required.
|
||||
Skills mode, `autoRecall`, and `autoCapture` are all enabled by default during `openclaw mem0 init`.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -24,12 +25,12 @@ Check your OpenClaw version:
|
||||
|
||||
```bash
|
||||
openclaw --version
|
||||
# OpenClaw 2026.4.15 (041266a)
|
||||
# OpenClaw 2026.4.25 (aa36ee6)
|
||||
```
|
||||
|
||||
| OpenClaw Version | Plugin Support |
|
||||
|------------------|----------------|
|
||||
| `>= 2026.4.15` | Fully supported |
|
||||
| `>= 2026.4.25` | Fully supported |
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -100,9 +101,9 @@ You no longer need manual config editing to get started. Everything happens insi
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
That's it. No API key, no config file editing, no environment variables. The plugin is now active and auto-capture and auto-recall are running on every turn.
|
||||
That's it. No API key, no config file editing, no environment variables. The plugin is now active with skills-based memory (triage, recall, and dream) running automatically.
|
||||
|
||||
<Note>The chat flow uses the same underlying config as manual setup — it writes `apiKey` and `userId` into `openclaw.json` for you. You can still open the file to inspect or override values afterward.</Note>
|
||||
<Note>The chat flow uses the same underlying config as manual setup — it writes `apiKey`, `userId`, and `skills` config into `openclaw.json` for you. You can still open the file to inspect or override values afterward.</Note>
|
||||
|
||||
#### Option 2: Manual Config
|
||||
|
||||
@@ -131,7 +132,19 @@ That's it. No API key, no config file editing, no environment variables. The plu
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"apiKey": "${MEM0_API_KEY}",
|
||||
"userId": "alice" // any unique identifier you choose for this user
|
||||
"userId": "alice", // any unique identifier you choose for this user
|
||||
"skills": {
|
||||
"triage": { "enabled": true },
|
||||
"recall": {
|
||||
"enabled": true,
|
||||
"tokenBudget": 1500,
|
||||
"rerank": true,
|
||||
"keywordSearch": true,
|
||||
"identityAlwaysInclude": true
|
||||
},
|
||||
"dream": { "enabled": true },
|
||||
"domain": "companion"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,8 +341,8 @@ openclaw mem0 status --json
|
||||
|-----|------|---------|-------------|
|
||||
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Which backend to use |
|
||||
| `userId` | `string` | OS username | Scope memories per user |
|
||||
| `autoRecall` | `boolean` | `false` | Inject memories before each turn (opt-in) |
|
||||
| `autoCapture` | `boolean` | `false` | Store facts after each turn (opt-in) |
|
||||
| `autoRecall` | `boolean` | `true` | Inject memories before each turn. Ignored when `skills` is configured. |
|
||||
| `autoCapture` | `boolean` | `true` | Store facts after each turn. Ignored when `skills` is configured. |
|
||||
| `topK` | `number` | `5` | Max memories per recall |
|
||||
| `searchThreshold` | `number` | `0.3` | Min similarity (0–1) |
|
||||
|
||||
@@ -426,9 +439,11 @@ If `openclaw plugins update` fails:
|
||||
| **Platform** | Conversations sent to `api.mem0.ai` for extraction and storage | Mem0 cloud |
|
||||
| **Open-source** | Embeddings generated via configured provider (default: OpenAI API). Vectors stored locally. | `~/.mem0/vector_store.db` (SQLite) |
|
||||
|
||||
### Enabling Auto-Capture and Auto-Recall
|
||||
### Auto-Capture and Auto-Recall
|
||||
|
||||
Auto-capture and auto-recall are disabled by default (opt-in). To enable either or both:
|
||||
Auto-capture and auto-recall are **enabled by default**. When skills mode is configured (the default after `openclaw mem0 init`), these are ignored in favor of the skills-based triage/recall/dream protocol.
|
||||
|
||||
To disable either:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -436,8 +451,8 @@ Auto-capture and auto-recall are disabled by default (opt-in). To enable either
|
||||
"entries": {
|
||||
"openclaw-mem0": {
|
||||
"config": {
|
||||
"autoCapture": true, // send conversations to Mem0 for extraction
|
||||
"autoRecall": true // inject relevant memories into context
|
||||
"autoCapture": false, // disable automatic fact extraction
|
||||
"autoRecall": false // disable automatic memory injection
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -445,7 +460,7 @@ Auto-capture and auto-recall are disabled by default (opt-in). To enable either
|
||||
}
|
||||
```
|
||||
|
||||
Without these enabled, the agent can still use memory tools (`memory_add`, `memory_search`, etc.) explicitly — only the automatic background behavior is off.
|
||||
The agent can always use memory tools (`memory_add`, `memory_search`, etc.) explicitly regardless of these settings.
|
||||
|
||||
### Credential Protection
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ For detailed per-client instructions, see the [Mem0 MCP Quickstart](/platform/me
|
||||
|
||||
## Available tools
|
||||
|
||||
The MCP server exposes 9 memory tools to your AI client:
|
||||
The MCP server exposes 11 memory tools to your AI client:
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
@@ -66,6 +66,8 @@ The MCP server exposes 9 memory tools to your AI client:
|
||||
| `delete_entities` | Remove user/agent/app entities |
|
||||
| `get_memory` | Retrieve single memory by ID |
|
||||
| `list_entities` | View stored entities |
|
||||
| `list_events` | List memory operation events with filters and pagination |
|
||||
| `get_event_status` | Check the status of an async memory operation by `event_id` |
|
||||
|
||||
## How it works
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ estimatedTime: "~2 minutes"
|
||||
- Mem0 Platform account (<a href="https://app.mem0.ai?utm_source=oss&utm_medium=platform-mem0-mcp" rel="nofollow">Sign up here</a>)
|
||||
- API key (<a href="https://app.mem0.ai/settings/api-keys?utm_source=oss&utm_medium=platform-mem0-mcp" rel="nofollow">Get one from dashboard</a>)
|
||||
- Node.js 14+ (for npx)
|
||||
- An MCP-compatible client (Claude, Claude Code, Cursor, Windsurf, VS Code, OpenCode)
|
||||
- An MCP-compatible client (Claude, Claude Code, Codex, Cursor, Windsurf, VS Code, OpenCode)
|
||||
</Info>
|
||||
|
||||
## What is Mem0 MCP?
|
||||
@@ -46,6 +46,8 @@ The MCP server exposes these memory tools to your AI client:
|
||||
| `delete_all_memories` | Bulk delete all memories in scope |
|
||||
| `delete_entities` | Delete a user/agent/app/run entity and its memories |
|
||||
| `list_entities` | Enumerate users/agents/apps/runs stored in Mem0 |
|
||||
| `list_events` | List memory operation events with filters and pagination |
|
||||
| `get_event_status` | Check the status of an async memory operation by `event_id` |
|
||||
|
||||
---
|
||||
|
||||
@@ -86,6 +88,33 @@ You can also configure individual clients:
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Codex">
|
||||
**Direct MCP (fastest, MCP only).** Codex reads MCP servers from `~/.codex/config.toml` as TOML (not JSON). Add:
|
||||
|
||||
```toml
|
||||
[mcp_servers.mem0]
|
||||
url = "https://mcp.mem0.ai/mcp"
|
||||
bearer_token_env_var = "MEM0_API_KEY"
|
||||
```
|
||||
|
||||
Export `MEM0_API_KEY` in the shell you launch Codex from, then restart Codex. `codex mcp add` only supports stdio servers, so HTTP servers must be added via `config.toml` directly — or via the **Plugins → Connect to a custom MCP → Streamable HTTP** UI in the Codex app.
|
||||
|
||||
<Note>
|
||||
Codex uses the server name `mem0` (not `mem0-mcp` like the other clients on this page) so it matches the name the bundled plugin registers if you ever sideload it later.
|
||||
</Note>
|
||||
|
||||
**Sideloaded plugin (full experience).** If you want the memory protocol skill, Mem0 SDK skill, and opt-in lifecycle hooks alongside the MCP server, sideload the plugin from a clone of `mem0ai/mem0`. The repo ships a marketplace manifest at `.agents/plugins/marketplace.json`, so you can register it with one CLI call:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mem0ai/mem0.git ~/codex-plugins/mem0-source
|
||||
codex plugin marketplace add ~/codex-plugins/mem0-source
|
||||
```
|
||||
|
||||
Then run `codex` and `/plugins`, browse the **Mem0 Plugins** marketplace, and install **Mem0**. Don't combine this with the Direct MCP setup above — the sideloaded plugin auto-registers `mem0` via `.codex-mcp.json`, so a manual `[mcp_servers.mem0]` block would create a duplicate.
|
||||
|
||||
See the [Codex integration guide](/integrations/codex) for full details, lifecycle-hook setup, and management commands (`codex plugin marketplace upgrade` / `remove`).
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Cursor">
|
||||
```bash
|
||||
npx mcp-add \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows using the Mem0 Platform MCP server.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"url": "https://mcp.mem0.ai/mcp",
|
||||
"bearer_token_env_var": "MEM0_API_KEY"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Codex workflows using the Mem0 Platform MCP server.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
+51
-51
@@ -49,62 +49,62 @@ This installs the full plugin including the MCP server, lifecycle hooks (automat
|
||||
|
||||
### Codex
|
||||
|
||||
**Option A — Repo marketplace** (recommended for teams):
|
||||
**Option A — Direct MCP** (fastest, MCP only):
|
||||
|
||||
Add the plugin marketplace to your repo root (already included in this repository):
|
||||
Codex reads MCP servers from `~/.codex/config.toml` as TOML. Add:
|
||||
|
||||
```
|
||||
.agents/plugins/marketplace.json
|
||||
```toml
|
||||
[mcp_servers.mem0]
|
||||
url = "https://mcp.mem0.ai/mcp"
|
||||
bearer_token_env_var = "MEM0_API_KEY"
|
||||
```
|
||||
|
||||
Then in Codex, browse the repo's plugin directory and install Mem0.
|
||||
Export `MEM0_API_KEY` in your shell and restart Codex. `codex mcp add` only supports stdio servers, so HTTP servers like Mem0's must be added via `config.toml` directly (or via the **Plugins → Connect to a custom MCP → Streamable HTTP** UI in the Codex app).
|
||||
|
||||
**Option B — Personal marketplace**:
|
||||
**Option B — Sideload the plugin** (full experience: MCP + skills + opt-in hooks):
|
||||
|
||||
Add to `~/.agents/plugins/marketplace.json`:
|
||||
Clone the repo and register the bundled marketplace with one CLI call:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "mem0-plugins",
|
||||
"interface": {
|
||||
"displayName": "Mem0 Plugins"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "mem0",
|
||||
"source": {
|
||||
"source": "local",
|
||||
"path": "/path/to/mem0/mem0-plugin"
|
||||
},
|
||||
"policy": {
|
||||
"installation": "AVAILABLE",
|
||||
"authentication": "ON_INSTALL"
|
||||
},
|
||||
"category": "Productivity"
|
||||
}
|
||||
]
|
||||
}
|
||||
```bash
|
||||
git clone https://github.com/mem0ai/mem0.git ~/codex-plugins/mem0-source
|
||||
codex plugin marketplace add ~/codex-plugins/mem0-source
|
||||
```
|
||||
|
||||
**Option C — Manual MCP configuration**:
|
||||
This points Codex at the repo's `.agents/plugins/marketplace.json`, which references `mem0-plugin/` as the local source. Restart Codex, run `/plugins`, and install **Mem0** from the **Mem0 Plugins** marketplace.
|
||||
|
||||
Add to your Codex MCP config:
|
||||
> **Don't combine with Option A.** The plugin manifest auto-registers `mem0` as an MCP server via `mem0-plugin/.codex-mcp.json` — adding a manual `[mcp_servers.mem0]` block would duplicate the registration.
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.mem0.ai/mcp/",
|
||||
"headers": {
|
||||
"Authorization": "Token ${MEM0_API_KEY}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
**Optional — enable lifecycle hooks.** Codex doesn't auto-wire hooks from plugin manifests; it only reads `~/.codex/hooks.json` (or `<repo>/.codex/hooks.json`) ([docs](https://developers.openai.com/codex/hooks)). Run the bundled installer once to merge Mem0's entries:
|
||||
|
||||
```bash
|
||||
python3 ~/codex-plugins/mem0-source/mem0-plugin/scripts/install_codex_hooks.py
|
||||
```
|
||||
|
||||
This installs the MCP server and the Mem0 SDK skill. Codex uses the skill-based memory protocol instead of lifecycle hooks.
|
||||
This merges three entries into `~/.codex/hooks.json` with absolute paths pointing into your clone:
|
||||
|
||||
| Event | What it does |
|
||||
|-------|--------------|
|
||||
| `SessionStart` | Loads prior memories as bootstrap context |
|
||||
| `UserPromptSubmit` | Injects relevant memories into the prompt |
|
||||
| `Stop` | Reminds the agent to persist learnings at turn end |
|
||||
|
||||
Re-running the installer is idempotent (replaces the Mem0 entries rather than duplicating) and preserves any other hooks you have. To remove: `python3 .../install_codex_hooks.py --uninstall`. If you move or delete the clone directory, re-run the installer from the new location — the hooks file stores absolute paths.
|
||||
|
||||
Codex hooks also require the `codex_hooks` feature flag in `~/.codex/config.toml`:
|
||||
|
||||
```toml
|
||||
[features]
|
||||
codex_hooks = true
|
||||
```
|
||||
|
||||
The installer prints a reminder if the flag isn't set. Restart Codex after editing the config.
|
||||
|
||||
**Managing the plugin:**
|
||||
|
||||
```bash
|
||||
codex plugin marketplace upgrade # pull latest plugin versions
|
||||
codex plugin marketplace remove mem0-plugins # unregister the marketplace
|
||||
```
|
||||
|
||||
### Cursor
|
||||
|
||||
@@ -145,17 +145,17 @@ After installing, confirm the MCP server is connected:
|
||||
|
||||
## What's included
|
||||
|
||||
| Component | Claude Code / Cowork | Cursor (Marketplace) | Cursor (Deeplink/Manual) | Codex |
|
||||
|-----------|:--------------------:|:--------------------:|:------------------------:|:-----:|
|
||||
| MCP Server | Yes | Yes | Yes | Yes |
|
||||
| Lifecycle Hooks | Yes | Yes | No | No |
|
||||
| Mem0 SDK Skill | Yes | Yes | No | Yes |
|
||||
| Memory Protocol Skill | No | No | No | Yes |
|
||||
| Component | Claude Code / Cowork | Cursor (Marketplace) | Cursor (Deeplink/Manual) | Codex (Sideload) | Codex (Direct MCP) |
|
||||
|-----------|:--------------------:|:--------------------:|:------------------------:|:----------------:|:------------------:|
|
||||
| MCP Server | Yes | Yes | Yes | Yes | Yes |
|
||||
| Lifecycle Hooks | Yes | Yes | No | Opt-in | No |
|
||||
| Mem0 SDK Skill | Yes | Yes | No | Yes | No |
|
||||
| Memory Protocol Skill | No | No | No | Yes | No |
|
||||
|
||||
- **MCP Server** — Connects to the Mem0 remote MCP server (`mcp.mem0.ai`), providing tools to add, search, update, and delete memories. No local dependencies required.
|
||||
- **Lifecycle Hooks** — Automatic memory capture at key points: session start, context compaction, task completion, and session end. (Claude Code/Cursor only)
|
||||
- **Lifecycle Hooks** — Automatic memory capture at key points. Claude Code and Cursor wire hooks up natively when the plugin is installed (session start, context compaction, task completion, session end). Codex hooks are opt-in via a one-time installer (`scripts/install_codex_hooks.py`) that writes entries into `~/.codex/hooks.json` for `SessionStart`, `UserPromptSubmit`, and `Stop`.
|
||||
- **Mem0 SDK Skill** — Guides the AI on how to integrate the Mem0 SDK (Python & TypeScript) into your applications.
|
||||
- **Memory Protocol Skill** — Codex-specific skill that instructs the agent to retrieve relevant memories at task start, store learnings on completion, and capture session state before context loss. Replaces lifecycle hooks on platforms that don't support them.
|
||||
- **Memory Protocol Skill** — Codex-specific skill that instructs the agent to retrieve relevant memories at task start, store learnings on completion, and capture session state before context loss. Complements the lifecycle hooks on Codex.
|
||||
|
||||
## MCP Tools
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"matcher": "startup|resume",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CODEX_PLUGIN_ROOT}/scripts/on_session_start.sh",
|
||||
"statusMessage": "Loading mem0 context..."
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CODEX_PLUGIN_ROOT}/scripts/on_user_prompt.sh",
|
||||
"statusMessage": "Searching mem0 memories...",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "${CODEX_PLUGIN_ROOT}/scripts/on_stop_codex.sh",
|
||||
"timeout": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+149
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install Mem0 lifecycle hooks into ~/.codex/hooks.json.
|
||||
|
||||
Codex discovers hooks only at ~/.codex/hooks.json or <repo>/.codex/hooks.json,
|
||||
and has no plugin-host mechanism for auto-wiring hooks from an installed
|
||||
plugin. This installer reads the template at hooks/codex-hooks.json, rewrites
|
||||
the ${CODEX_PLUGIN_ROOT} placeholder to the absolute install path of this
|
||||
plugin, then merges the entries into ~/.codex/hooks.json.
|
||||
|
||||
Re-running is idempotent: existing Mem0 entries (identified by the plugin
|
||||
directory name in the command string) are removed before fresh entries are
|
||||
added, so upgrades don't leave duplicates.
|
||||
|
||||
Usage:
|
||||
python3 install_codex_hooks.py # install or update
|
||||
python3 install_codex_hooks.py --uninstall # remove Mem0 entries
|
||||
|
||||
After installing, Codex requires the hooks feature flag in ~/.codex/config.toml:
|
||||
|
||||
[features]
|
||||
codex_hooks = true
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
PLUGIN_ROOT = SCRIPT_DIR.parent
|
||||
|
||||
CODEX_DIR = Path.home() / ".codex"
|
||||
HOOKS_FILE = CODEX_DIR / "hooks.json"
|
||||
CONFIG_FILE = CODEX_DIR / "config.toml"
|
||||
|
||||
TEMPLATE_FILE = PLUGIN_ROOT / "hooks" / "codex-hooks.json"
|
||||
|
||||
# Substring we look for when identifying entries this installer owns.
|
||||
# Matches the plugin directory name, which stays stable across install paths.
|
||||
OWNER_MARKER = "mem0-plugin"
|
||||
|
||||
|
||||
def load_template() -> dict:
|
||||
raw = TEMPLATE_FILE.read_text()
|
||||
raw = raw.replace("${CODEX_PLUGIN_ROOT}", str(PLUGIN_ROOT))
|
||||
return json.loads(raw)
|
||||
|
||||
|
||||
def load_existing() -> dict:
|
||||
if not HOOKS_FILE.exists():
|
||||
return {"hooks": {}}
|
||||
try:
|
||||
return json.loads(HOOKS_FILE.read_text())
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print(f"error: failed to read {HOOKS_FILE}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def is_owned_entry(entry: dict) -> bool:
|
||||
for hook in entry.get("hooks", []):
|
||||
if OWNER_MARKER in hook.get("command", ""):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def strip_owned_entries(config: dict) -> dict:
|
||||
hooks = config.get("hooks", {}) or {}
|
||||
for event in list(hooks.keys()):
|
||||
hooks[event] = [e for e in hooks[event] if not is_owned_entry(e)]
|
||||
if not hooks[event]:
|
||||
del hooks[event]
|
||||
config["hooks"] = hooks
|
||||
return config
|
||||
|
||||
|
||||
def merge_template(config: dict, template: dict) -> dict:
|
||||
hooks = config.setdefault("hooks", {})
|
||||
for event, entries in template.get("hooks", {}).items():
|
||||
hooks.setdefault(event, []).extend(entries)
|
||||
return config
|
||||
|
||||
|
||||
def write_config(config: dict) -> None:
|
||||
CODEX_DIR.mkdir(parents=True, exist_ok=True)
|
||||
HOOKS_FILE.write_text(json.dumps(config, indent=2) + "\n")
|
||||
|
||||
|
||||
def feature_flag_enabled() -> bool:
|
||||
if not CONFIG_FILE.exists():
|
||||
return False
|
||||
content = CONFIG_FILE.read_text()
|
||||
for line in content.splitlines():
|
||||
stripped = line.split("#", 1)[0].strip().replace(" ", "")
|
||||
if stripped == "codex_hooks=true":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def print_feature_flag_hint() -> None:
|
||||
print()
|
||||
print("Codex hooks feature flag is not enabled.")
|
||||
print(f"Add this to {CONFIG_FILE}:")
|
||||
print()
|
||||
print(" [features]")
|
||||
print(" codex_hooks = true")
|
||||
print()
|
||||
print("Then restart Codex.")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Install or remove Mem0 Codex hooks.")
|
||||
parser.add_argument(
|
||||
"--uninstall",
|
||||
action="store_true",
|
||||
help="Remove Mem0 entries from ~/.codex/hooks.json and exit.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
config = load_existing()
|
||||
|
||||
if args.uninstall:
|
||||
config = strip_owned_entries(config)
|
||||
write_config(config)
|
||||
print(f"Removed Mem0 hooks from {HOOKS_FILE}")
|
||||
return 0
|
||||
|
||||
if not TEMPLATE_FILE.exists():
|
||||
print(f"error: template not found at {TEMPLATE_FILE}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
template = load_template()
|
||||
config = strip_owned_entries(config)
|
||||
config = merge_template(config, template)
|
||||
write_config(config)
|
||||
|
||||
print(f"Installed Mem0 hooks into {HOOKS_FILE}")
|
||||
print(f"Plugin path: {PLUGIN_ROOT}")
|
||||
print("Events: SessionStart, UserPromptSubmit, Stop")
|
||||
|
||||
if not feature_flag_enabled():
|
||||
print_feature_flag_hint()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
# Hook: Stop (Codex)
|
||||
#
|
||||
# Fires when Codex finishes a turn. Reminds the agent to persist any
|
||||
# important learnings via the mem0 MCP tools before the turn closes.
|
||||
#
|
||||
# Input: JSON on stdin with session_id, turn_id, stop_hook_active,
|
||||
# last_assistant_message, transcript_path, cwd,
|
||||
# hook_event_name, model
|
||||
# Output: JSON on stdout (Codex rejects plain text on Stop).
|
||||
# - stop_hook_active=true -> {"continue": true} (let the turn end)
|
||||
# - stop_hook_active=false -> {"decision":"block","reason":"..."}
|
||||
# (continue the turn with the reminder as context)
|
||||
#
|
||||
# We must respect stop_hook_active or we'd loop forever: every "block"
|
||||
# reopens the turn, which triggers Stop again when the agent settles.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
INPUT=$(cat)
|
||||
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
|
||||
|
||||
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
|
||||
printf '{"continue":true}\n'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
REASON=$(cat <<'EOF'
|
||||
Before finishing, check if there are important learnings from this interaction that should be persisted using the mem0 `add_memory` tool:
|
||||
|
||||
1. Were any significant decisions made? -> Store with metadata `{"type": "decision"}`
|
||||
2. Were any new patterns or strategies discovered? -> Store with metadata `{"type": "task_learning"}`
|
||||
3. Did any approach fail? -> Store with metadata `{"type": "anti_pattern"}`
|
||||
4. Did you learn anything about the user's preferences? -> Store with metadata `{"type": "user_preference"}`
|
||||
5. Were there environment/setup discoveries? -> Store with metadata `{"type": "environmental"}`
|
||||
|
||||
Memories can be as detailed as needed — include full context, reasoning, code snippets, file paths, and examples. Longer, searchable memories are more valuable than vague one-liners.
|
||||
|
||||
If nothing notable happened in this interaction, it's fine to skip. Only store genuinely useful learnings.
|
||||
EOF
|
||||
)
|
||||
|
||||
jq -cn --arg reason "$REASON" '{decision:"block", reason:$reason}'
|
||||
exit 0
|
||||
@@ -1,25 +1,34 @@
|
||||
---
|
||||
name: mem0
|
||||
description: >
|
||||
Integrate Mem0 Platform into AI applications for persistent memory, personalization, and semantic search.
|
||||
Use this skill when the user mentions "mem0", "memory layer", "remember user preferences",
|
||||
"persistent context", "personalization", or needs to add long-term memory to chatbots, agents,
|
||||
or AI apps. Covers Python and TypeScript SDKs, framework integrations (LangChain, CrewAI,
|
||||
Vercel AI SDK, OpenAI Agents SDK, Pipecat), and the full Platform API. Use even when the user
|
||||
doesn't explicitly say "mem0" but describes needing conversation memory, user context retention,
|
||||
or knowledge retrieval across sessions.
|
||||
Mem0 Platform SDK for adding persistent memory to AI applications.
|
||||
TRIGGER when: user mentions "mem0", "MemoryClient", "memory layer",
|
||||
"remember user preferences", "persistent context", "personalization",
|
||||
or needs to add long-term memory to chatbots, agents, or AI apps.
|
||||
Covers Python SDK (mem0ai), TypeScript SDK (mem0ai), and framework integrations
|
||||
(LangChain, CrewAI, OpenAI Agents SDK, Pipecat, LlamaIndex, AutoGen, LangGraph).
|
||||
Also covers the open-source self-hosted Memory class.
|
||||
This is the DEFAULT mem0 skill for ambiguous queries.
|
||||
DO NOT TRIGGER when: user asks about CLI commands, terminal usage, or shell
|
||||
scripts (use mem0-cli), or Vercel AI SDK / @mem0/vercel-ai-provider / createMem0
|
||||
(use mem0-vercel-ai-sdk).
|
||||
license: Apache-2.0
|
||||
metadata:
|
||||
author: mem0ai
|
||||
version: "0.1.0"
|
||||
version: "0.1.1"
|
||||
category: ai-memory
|
||||
tags: "memory, personalization, ai, python, typescript, vector-search"
|
||||
compatibility: Requires Python 3.10+ or Node.js 18+, pip install mem0ai or npm install mem0ai, MEM0_API_KEY env var, and internet access to api.mem0.ai
|
||||
compatibility: Requires Python 3.10+ or Node.js 18+, pip install mem0ai or npm install mem0ai, MEM0_API_KEY env var (Platform), and internet access to api.mem0.ai. Uses Mem0 v3 API.
|
||||
---
|
||||
|
||||
# Mem0 Platform Integration
|
||||
|
||||
Mem0 is a managed memory layer for AI applications. It stores, retrieves, and manages user memories via API — no infrastructure to deploy.
|
||||
> **Skill Graph:** This skill is part of the Mem0 skill graph:
|
||||
> - **mem0** (this skill) -- Platform Client SDK + OSS (Python + TypeScript)
|
||||
> - **[mem0-cli](https://github.com/mem0ai/mem0/tree/main/skills/mem0-cli)** -- Command-line interface
|
||||
> - **[mem0-vercel-ai-sdk](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk)** -- Vercel AI SDK provider
|
||||
|
||||
Mem0 is a managed memory layer for AI applications. It stores, retrieves, and manages user memories via API — no infrastructure to deploy. For self-hosted usage, see the OSS section in the client references below.
|
||||
|
||||
## Step 1: Install and authenticate
|
||||
|
||||
@@ -68,14 +77,14 @@ client.add(messages, user_id="alice")
|
||||
|
||||
### Search memories
|
||||
```python
|
||||
results = client.search("dietary preferences", user_id="alice")
|
||||
results = client.search("dietary preferences", filters={"user_id": "alice"})
|
||||
for mem in results.get("results", []):
|
||||
print(mem["memory"])
|
||||
```
|
||||
|
||||
### Get all memories
|
||||
```python
|
||||
all_memories = client.get_all(user_id="alice")
|
||||
all_memories = client.get_all(filters={"user_id": "alice"})
|
||||
```
|
||||
|
||||
### Update a memory
|
||||
@@ -100,12 +109,12 @@ openai = OpenAI()
|
||||
|
||||
def chat(user_input: str, user_id: str) -> str:
|
||||
# 1. Retrieve relevant memories
|
||||
memories = mem0.search(user_input, user_id=user_id)
|
||||
memories = mem0.search(user_input, filters={"user_id": user_id})
|
||||
context = "\n".join([m["memory"] for m in memories.get("results", [])])
|
||||
|
||||
# 2. Generate response with memory context
|
||||
response = openai.chat.completions.create(
|
||||
model="gpt-4.1-nano-2025-04-14",
|
||||
model="gpt-5-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": f"User context:\n{context}"},
|
||||
{"role": "user", "content": user_input},
|
||||
@@ -123,11 +132,20 @@ def chat(user_input: str, user_id: str) -> str:
|
||||
|
||||
## Common edge cases
|
||||
|
||||
- **Search returns empty:** Memories process asynchronously. Wait 2-3s after `add()` before searching. Also verify `user_id` matches exactly (case-sensitive).
|
||||
- **Search returns empty:** Memories process asynchronously. Wait 2-3s after `add()` before searching. Also verify `user_id` matches exactly (case-sensitive) and use `filters={"user_id": "..."}` syntax.
|
||||
- **AND filter with user_id + agent_id returns empty:** Entities are stored separately. Use `OR` instead, or query separately.
|
||||
- **Duplicate memories:** Don't mix `infer=True` (default) and `infer=False` for the same data. Stick to one mode.
|
||||
- **Wrong import:** Always use `from mem0 import MemoryClient` (or `AsyncMemoryClient` for async). Do not use `from mem0 import Memory`.
|
||||
- **Immutable memories:** Cannot be updated or deleted once created. Use `client.history(memory_id)` to track changes over time.
|
||||
- **v3 defaults:** `top_k=20`, `threshold=0.1`, `rerank=False`. Adjust as needed for your use case.
|
||||
|
||||
## v2 Compatibility
|
||||
|
||||
If you're using SDK v2.x, note these differences:
|
||||
- **Entity IDs:** Pass `user_id` as top-level kwarg to `search()` instead of inside `filters`
|
||||
- **Defaults:** `top_k=100`, no threshold, `rerank=True`
|
||||
- **Graph memory:** Available via `enable_graph=True`
|
||||
|
||||
See the [migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for details.
|
||||
|
||||
## Live documentation search
|
||||
|
||||
@@ -141,7 +159,17 @@ python ${CLAUDE_SKILL_DIR}/scripts/mem0_doc_search.py --index
|
||||
|
||||
No API key needed — searches docs.mem0.ai directly.
|
||||
|
||||
## References
|
||||
## Client SDK References
|
||||
|
||||
Language-specific deep references (Platform + OSS):
|
||||
|
||||
| Language | File |
|
||||
|----------|------|
|
||||
| Python (MemoryClient + AsyncMemoryClient + Memory OSS) | [client/python.md](client/python.md) |
|
||||
| TypeScript/Node.js (MemoryClient + Memory OSS) | [client/node.md](client/node.md) |
|
||||
| Python vs TypeScript differences | [client/differences.md](client/differences.md) |
|
||||
|
||||
## Platform References
|
||||
|
||||
Load these on demand for deeper detail:
|
||||
|
||||
@@ -152,5 +180,12 @@ Load these on demand for deeper detail:
|
||||
| API reference (endpoints, filters, object schema) | [references/api-reference.md](references/api-reference.md) |
|
||||
| Architecture (pipeline, lifecycle, scoping, performance) | [references/architecture.md](references/architecture.md) |
|
||||
| Platform features (retrieval, graph, categories, MCP, etc.) | [references/features.md](references/features.md) |
|
||||
| Framework integrations (LangChain, CrewAI, Vercel AI, etc.) | [references/integration-patterns.md](references/integration-patterns.md) |
|
||||
| Framework integrations (LangChain, CrewAI, OpenAI Agents, etc.) | [references/integration-patterns.md](references/integration-patterns.md) |
|
||||
| Use cases & examples (real-world patterns with code) | [references/use-cases.md](references/use-cases.md) |
|
||||
|
||||
## Related Mem0 Skills
|
||||
|
||||
| Skill | When to use | Link |
|
||||
|-------|-------------|------|
|
||||
| mem0-cli | Terminal commands, scripting, CI/CD, agent tool loops | [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-cli) |
|
||||
| mem0-vercel-ai-sdk | Vercel AI SDK provider with automatic memory | [GitHub](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk) |
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
# Python vs TypeScript SDK Differences
|
||||
|
||||
Quick-reference cheatsheet for developers working across both Mem0 SDKs.
|
||||
|
||||
## Constructor
|
||||
|
||||
| Aspect | Python | TypeScript |
|
||||
|--------|--------|------------|
|
||||
| Import (Platform) | `from mem0 import MemoryClient` | `import MemoryClient from 'mem0ai'` |
|
||||
| Import (OSS) | `from mem0 import Memory` | `import { Memory } from 'mem0ai/oss'` |
|
||||
| Constructor | `MemoryClient(api_key="m0-xxx")` | `new MemoryClient({ apiKey: 'm0-xxx' })` |
|
||||
| Required param | `api_key` (positional or kwarg) | `apiKey` (in options object) |
|
||||
|
||||
Both read from `MEM0_API_KEY` env var if no key provided.
|
||||
|
||||
## Method Naming
|
||||
|
||||
| Operation | Python | TypeScript |
|
||||
|-----------|--------|------------|
|
||||
| Add | `add()` | `add()` |
|
||||
| Search | `search()` | `search()` |
|
||||
| Get | `get()` | `get()` |
|
||||
| Get all | `get_all()` | `getAll()` |
|
||||
| Update | `update()` | `update()` |
|
||||
| Delete | `delete()` | `delete()` |
|
||||
| Delete all | `delete_all()` | `deleteAll()` |
|
||||
| History | `history()` | `history()` |
|
||||
| Batch update | `batch_update()` | `batchUpdate()` |
|
||||
| Batch delete | `batch_delete()` | `batchDelete()` |
|
||||
| List users | `users()` | `users()` |
|
||||
| Delete users | `delete_users()` | `deleteUsers()` |
|
||||
| Get project | `project.get()` | `getProject()` |
|
||||
| Update project | `project.update()` | `updateProject()` |
|
||||
| Create webhook | `create_webhook()` | `createWebhook()` |
|
||||
| Get webhooks | `get_webhooks()` | `getWebhooks()` |
|
||||
| Update webhook | `update_webhook()` | `updateWebhook()` |
|
||||
| Delete webhook | `delete_webhook()` | `deleteWebhook()` |
|
||||
| Create export | `create_memory_export()` | `createMemoryExport()` |
|
||||
| Get export | `get_memory_export()` | `getMemoryExport()` |
|
||||
| Feedback | `feedback()` | `feedback()` |
|
||||
|
||||
**Rule:** Python uses `snake_case`, TypeScript uses `camelCase` for method names.
|
||||
|
||||
## Parameter Passing
|
||||
|
||||
```python
|
||||
# Python: kwargs
|
||||
client.add(messages, user_id="alice", metadata={"source": "chat"})
|
||||
client.search("query", filters={"user_id": "alice"}, top_k=5, rerank=True)
|
||||
```
|
||||
|
||||
```typescript
|
||||
// TypeScript: options object with camelCase for top-level params, snake_case for filter keys
|
||||
await client.add(messages, { userId: 'alice', metadata: { source: 'chat' } });
|
||||
await client.search('query', { filters: { user_id: 'alice' }, topK: 5, rerank: true });
|
||||
```
|
||||
|
||||
**v3:** Python uses `snake_case` everywhere. TypeScript uses `camelCase` for top-level params (`userId`, `topK`) but `snake_case` for filter keys (`user_id`, `agent_id`).
|
||||
|
||||
## Architectural Differences
|
||||
|
||||
| Aspect | Python | TypeScript |
|
||||
|--------|--------|------------|
|
||||
| HTTP library | httpx | axios |
|
||||
| Default timeout | 300s | 60s |
|
||||
| Sync support | Yes (`MemoryClient`) | No (all async) |
|
||||
| Async support | Yes (`AsyncMemoryClient`) | All methods are async |
|
||||
| Project management | `client.project.*` (separate class) | `client.getProject()` / `client.updateProject()` |
|
||||
| Context manager | `async with AsyncMemoryClient()` | Not supported |
|
||||
|
||||
## Platform Features: Python-only
|
||||
|
||||
These methods exist in Python but not TypeScript:
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `get_summary(filters)` | Get summary of memories |
|
||||
| `reset()` | Delete ALL data (users + memories) |
|
||||
| `project.create(name)` | Create a new project |
|
||||
| `project.delete()` | Delete current project |
|
||||
| `project.get_members()` | List project members |
|
||||
| `project.add_member(email, role)` | Add member to project |
|
||||
| `project.update_member(email, role)` | Change member role |
|
||||
| `project.remove_member(email)` | Remove member |
|
||||
|
||||
## Platform Features: TypeScript-only
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `deleteUser(data)` | Convenience method for single entity deletion |
|
||||
| `ping()` | Health check endpoint |
|
||||
|
||||
## OSS Config Naming
|
||||
|
||||
| Python config key | TypeScript config key |
|
||||
|-------------------|----------------------|
|
||||
| `vector_store` | `vectorStore` |
|
||||
| `history_db_path` | `historyDbPath` |
|
||||
| `custom_instructions` | `customInstructions` |
|
||||
|
||||
## OSS Scope Parameter Naming
|
||||
|
||||
| Python | TypeScript |
|
||||
|--------|------------|
|
||||
| `user_id="alice"` | `userId: 'alice'` |
|
||||
| `agent_id="bot"` | `agentId: 'bot'` |
|
||||
| `run_id="session"` | `runId: 'session'` |
|
||||
|
||||
## Entity ID Passing (v3)
|
||||
|
||||
| Method | Python | TypeScript |
|
||||
|--------|--------|------------|
|
||||
| add() | Top-level: `user_id="alice"` | Top-level: `{ userId: 'alice' }` |
|
||||
| search() | In filters: `filters={"user_id": "alice"}` | In filters: `{ filters: { user_id: 'alice' } }` |
|
||||
| get_all() | In filters: `filters={"user_id": "alice"}` | In filters: `{ filters: { user_id: 'alice' } }` |
|
||||
|
||||
## Common Gotcha
|
||||
|
||||
When searching/filtering, both Python and TypeScript use `snake_case` for filter keys. TypeScript only uses `camelCase` for top-level method parameters:
|
||||
|
||||
```python
|
||||
# Python - snake_case in filters
|
||||
results = client.search("query", filters={"user_id": "alice"})
|
||||
```
|
||||
|
||||
```typescript
|
||||
// TypeScript - snake_case in filters, camelCase for top-level params
|
||||
const results = await client.search('query', { filters: { user_id: 'alice' }, topK: 20 });
|
||||
```
|
||||
@@ -0,0 +1,418 @@
|
||||
# Mem0 Node.js / TypeScript SDK Reference
|
||||
|
||||
Complete reference for the `mem0ai` npm package. Covers both the Platform client (managed API) and the Open Source self-hosted variant.
|
||||
|
||||
---
|
||||
|
||||
## Platform Client
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install mem0ai
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
```
|
||||
|
||||
### MemoryClient
|
||||
|
||||
```typescript
|
||||
import MemoryClient from 'mem0ai';
|
||||
|
||||
const client = new MemoryClient({ apiKey: 'm0-xxx' });
|
||||
```
|
||||
|
||||
**Constructor:** `new MemoryClient({ apiKey })`. If `apiKey` is not provided, reads from `MEM0_API_KEY` environment variable.
|
||||
|
||||
- HTTP library: `axios`
|
||||
- Timeout: 60 seconds
|
||||
- Base URL: `https://api.mem0.ai`
|
||||
- All methods are async (return `Promise`)
|
||||
|
||||
---
|
||||
|
||||
### Memory Methods
|
||||
|
||||
#### add(messages, options?)
|
||||
|
||||
Store new memories from messages.
|
||||
|
||||
```typescript
|
||||
const messages = [
|
||||
{ role: 'user', content: "I'm a vegetarian and allergic to nuts." },
|
||||
{ role: 'assistant', content: "Got it! I'll remember that." },
|
||||
];
|
||||
await client.add(messages, { userId: 'alice' });
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `messages` | `Message[]` | Array of `{role, content}` objects |
|
||||
| `options.userId` | string | User identifier |
|
||||
| `options.agentId` | string | Agent identifier |
|
||||
| `options.appId` | string | Application identifier |
|
||||
| `options.runId` | string | Session identifier |
|
||||
| `options.metadata` | object | Custom key-value pairs |
|
||||
| `options.infer` | boolean | If false, store raw text (default: true) |
|
||||
|
||||
**Returns:** `Promise<any>` -- list of events
|
||||
|
||||
#### search(query, options?)
|
||||
|
||||
Search memories by semantic similarity.
|
||||
|
||||
```typescript
|
||||
const results = await client.search('dietary preferences', { filters: { user_id: 'alice' }, topK: 20 });
|
||||
for (const mem of results.results) {
|
||||
console.log(mem.memory, mem.score);
|
||||
}
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `query` | string | Natural language search query |
|
||||
| `options.filters` | object | Filter object with entity IDs (`user_id`, `agent_id`, etc.) and/or `AND`/`OR`/`NOT` conditions |
|
||||
| `options.topK` | number | Number of results (default: 20) |
|
||||
| `options.rerank` | boolean | Enable semantic reranking (default: false) |
|
||||
| `options.threshold` | number | Minimum similarity (default: 0.1) |
|
||||
|
||||
**Returns:** `Promise<SearchResult>` -- `{results: [{id, memory, score, ...}]}`
|
||||
|
||||
#### get(memoryId)
|
||||
|
||||
```typescript
|
||||
const memory = await client.get('ea925981-...');
|
||||
```
|
||||
|
||||
#### getAll(options?)
|
||||
|
||||
Retrieve all memories. Requires at least one entity identifier in filters.
|
||||
|
||||
```typescript
|
||||
const memories = await client.getAll({ filters: { user_id: 'alice' } });
|
||||
// With filters
|
||||
const filtered = await client.getAll({
|
||||
filters: { AND: [{ user_id: 'alice' }, { categories: { contains: 'health' } }] },
|
||||
});
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `options.filters` | object | Filter object with entity IDs (`user_id`, `agent_id`, etc.) and/or `AND`/`OR`/`NOT` conditions |
|
||||
| `options.page` | number | Page number |
|
||||
| `options.pageSize` | number | Results per page |
|
||||
|
||||
#### update(memoryId, data)
|
||||
|
||||
```typescript
|
||||
await client.update('ea925981-...', { text: 'Updated: vegan since 2024' });
|
||||
await client.update('ea925981-...', { text: 'Updated', metadata: { verified: true } });
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `memoryId` | string | Memory ID |
|
||||
| `data.text` | string | New content |
|
||||
| `data.metadata` | object | New metadata |
|
||||
| `data.timestamp` | string | New timestamp |
|
||||
|
||||
#### delete(memoryId)
|
||||
|
||||
```typescript
|
||||
await client.delete('ea925981-...');
|
||||
```
|
||||
|
||||
#### deleteAll(options?)
|
||||
|
||||
```typescript
|
||||
await client.deleteAll({ userId: 'alice' });
|
||||
```
|
||||
|
||||
#### history(memoryId)
|
||||
|
||||
```typescript
|
||||
const history = await client.history('ea925981-...');
|
||||
// Returns: [{previousValue, newValue, action, timestamps}]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Batch Methods
|
||||
|
||||
#### batchUpdate(memories)
|
||||
|
||||
```typescript
|
||||
await client.batchUpdate([
|
||||
{ memoryId: 'uuid-1', text: 'Updated text' },
|
||||
{ memoryId: 'uuid-2', text: 'Another update' },
|
||||
]);
|
||||
```
|
||||
|
||||
#### batchDelete(memories)
|
||||
|
||||
```typescript
|
||||
await client.batchDelete(['uuid-1', 'uuid-2', 'uuid-3']);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### User/Entity Management
|
||||
|
||||
#### users()
|
||||
|
||||
```typescript
|
||||
const users = await client.users();
|
||||
// Returns: {results: [{type: "user", name: "alice"}, ...]}
|
||||
```
|
||||
|
||||
#### deleteUser(data) / deleteUsers(data)
|
||||
|
||||
```typescript
|
||||
await client.deleteUser({ userId: 'alice' }); // Single entity
|
||||
await client.deleteUsers({ agentId: 'bot-1' }); // Flexible
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Project Management
|
||||
|
||||
```typescript
|
||||
// Get project config
|
||||
const config = await client.getProject({ fields: ['customCategories'] });
|
||||
|
||||
// Update project settings
|
||||
await client.updateProject({
|
||||
customInstructions: 'Extract dietary preferences and health info',
|
||||
customCategories: [{ health: 'Medical and dietary info' }],
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Webhooks
|
||||
|
||||
```typescript
|
||||
// List
|
||||
const webhooks = await client.getWebhooks({ projectId: 'proj_123' });
|
||||
|
||||
// Create
|
||||
const webhook = await client.createWebhook({
|
||||
url: 'https://your-app.com/webhook',
|
||||
name: 'Memory Logger',
|
||||
projectId: 'proj_123',
|
||||
eventTypes: ['memory_add', 'memory_update'],
|
||||
});
|
||||
|
||||
// Update
|
||||
await client.updateWebhook({
|
||||
webhookId: 'wh_123',
|
||||
name: 'Updated Logger',
|
||||
url: 'https://new-url.com',
|
||||
});
|
||||
|
||||
// Delete
|
||||
await client.deleteWebhook({ webhookId: 'wh_123' });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Feedback
|
||||
|
||||
```typescript
|
||||
await client.feedback({
|
||||
memoryId: 'mem-123',
|
||||
feedback: 'POSITIVE',
|
||||
feedbackReason: 'Accurately captured preference',
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Export
|
||||
|
||||
```typescript
|
||||
const exportReq = await client.createMemoryExport({
|
||||
schema: JSON.stringify({ type: 'object', properties: { name: { type: 'string' } } }),
|
||||
filters: { user_id: 'alice' },
|
||||
});
|
||||
|
||||
const result = await client.getMemoryExport({ memoryExportId: exportReq.id });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### TypeScript Types
|
||||
|
||||
Key interfaces from `mem0.types.ts`:
|
||||
|
||||
```typescript
|
||||
interface Message { role: string; content: string; }
|
||||
interface Memory { id: string; memory: string; userId: string; categories: string[]; score?: number; /* ... */ }
|
||||
interface MemoryOptions { userId?: string; agentId?: string; appId?: string; runId?: string; metadata?: object; /* ... */ }
|
||||
interface SearchOptions { filters?: object; topK?: number; rerank?: boolean; threshold?: number; /* ... */ }
|
||||
interface MemoryHistory { id: string; memoryId: string; previousValue: string; newValue: string; action: string; /* ... */ }
|
||||
interface FeedbackPayload { memoryId: string; feedback: string; feedbackReason?: string; }
|
||||
interface WebhookCreatePayload { url: string; name: string; projectId: string; eventTypes: string[]; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Open Source / Self-Hosted
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
npm install mem0ai
|
||||
```
|
||||
|
||||
### Memory Class
|
||||
|
||||
```typescript
|
||||
import { Memory } from 'mem0ai/oss';
|
||||
|
||||
const m = new Memory(); // Uses default config
|
||||
```
|
||||
|
||||
**Import:** `from 'mem0ai/oss'` (NOT the default export -- that is `MemoryClient` for Platform)
|
||||
|
||||
### Configuration
|
||||
|
||||
```typescript
|
||||
const config = {
|
||||
llm: {
|
||||
provider: 'openai', // openai, groq, anthropic, google, ollama, lmstudio, mistral, azure
|
||||
config: {
|
||||
model: 'gpt-5-mini',
|
||||
apiKey: 'sk-xxx',
|
||||
},
|
||||
},
|
||||
embedder: {
|
||||
provider: 'openai', // openai, ollama, lmstudio, google, azure, langchain, anthropic
|
||||
config: {
|
||||
model: 'text-embedding-3-small',
|
||||
apiKey: 'sk-xxx',
|
||||
},
|
||||
},
|
||||
vectorStore: {
|
||||
provider: 'qdrant', // memory, qdrant, redis, supabase, langchain, azure_ai_search, pgvector
|
||||
config: {
|
||||
collectionName: 'my_memories',
|
||||
host: 'localhost',
|
||||
port: 6333,
|
||||
},
|
||||
},
|
||||
historyDbPath: 'history.db',
|
||||
customInstructions: '...',
|
||||
disableHistory: false,
|
||||
};
|
||||
|
||||
const m = new Memory(config);
|
||||
// Or from dict with validation:
|
||||
const m2 = Memory.fromConfig(config);
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
All methods are async (return `Promise`):
|
||||
|
||||
#### add(messages, config)
|
||||
|
||||
```typescript
|
||||
await m.add('I prefer dark mode', { userId: 'alice' });
|
||||
await m.add([
|
||||
{ role: 'user', content: 'I like hiking' },
|
||||
{ role: 'assistant', content: 'Great outdoor activity!' },
|
||||
], { userId: 'alice' });
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `messages` | `string \| Message[]` | Content to store |
|
||||
| `config.userId` | string | User identifier (at least one scope required) |
|
||||
| `config.agentId` | string | Agent identifier |
|
||||
| `config.runId` | string | Session identifier |
|
||||
| `config.metadata` | object | Custom key-value pairs |
|
||||
| `config.filters` | object | Additional filters |
|
||||
| `config.infer` | boolean | LLM inference (default: true) |
|
||||
|
||||
**Returns:** `Promise<{results: [...], relations?: [...]}>`
|
||||
|
||||
#### search(query, config)
|
||||
|
||||
```typescript
|
||||
const results = await m.search('dietary preferences', { filters: { user_id: 'alice' }, topK: 5 });
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `query` | string | Search query |
|
||||
| `config.filters` | object | Filter object with entity IDs (`user_id`, `agent_id`, `run_id`, etc.) |
|
||||
| `config.topK` | number | Max results (default: 20) |
|
||||
|
||||
#### get(memoryId) / getAll(config) / update(memoryId, data) / delete(memoryId) / deleteAll(config) / history(memoryId)
|
||||
|
||||
Same interface patterns. Note: OSS `update` takes a string for data, not an object.
|
||||
|
||||
```typescript
|
||||
await m.update('mem-id', 'new content');
|
||||
```
|
||||
|
||||
#### reset()
|
||||
|
||||
Clear the entire vector store and history.
|
||||
|
||||
```typescript
|
||||
await m.reset();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Differences: Platform vs OSS
|
||||
|
||||
| Aspect | Platform (`MemoryClient`) | OSS (`Memory`) |
|
||||
|--------|--------------------------|----------------|
|
||||
| **Import** | `import MemoryClient from 'mem0ai'` | `import { Memory } from 'mem0ai/oss'` |
|
||||
| **Auth** | API key required (`MEM0_API_KEY`) | No API key -- config-based |
|
||||
| **Execution** | API calls to `api.mem0.ai` | Local execution |
|
||||
| **Infrastructure** | Fully managed | Self-managed vector DB, embedder, LLM |
|
||||
| **Param style** | Top-level: `camelCase` (`userId`, `topK`), filter keys: `snake_case` (`user_id`) | Top-level: `camelCase` (`userId`, `topK`), filter keys: `snake_case` (`user_id`) |
|
||||
| **Batch ops** | `batchUpdate`, `batchDelete` | Not available |
|
||||
| **Webhooks** | Full CRUD | Not available |
|
||||
| **Export** | `createMemoryExport` | Not available |
|
||||
| **Feedback** | `feedback()` | Not available |
|
||||
| **Project mgmt** | `getProject`, `updateProject` | Not available |
|
||||
| **User listing** | `users()`, `deleteUser()` | Not available |
|
||||
| **History** | Platform-managed | SQLite (configurable) |
|
||||
|
||||
---
|
||||
|
||||
## v2 Compatibility
|
||||
|
||||
If you're using SDK v2.x:
|
||||
|
||||
**Naming Changes:**
|
||||
- Top-level params now use camelCase: `topK`, `rerank` (not `top_k`)
|
||||
- Filter keys use snake_case: `user_id`, `agent_id`
|
||||
- OSS: `limit` renamed to `topK`
|
||||
|
||||
**API Changes:**
|
||||
```typescript
|
||||
// v2 - top-level entity IDs, snake_case
|
||||
await client.search("query", { user_id: "alice", top_k: 20 });
|
||||
|
||||
// v3 - filters object with snake_case keys, camelCase top-level params
|
||||
await client.search("query", { filters: { user_id: "alice" }, topK: 20 });
|
||||
```
|
||||
|
||||
**Default Changes:**
|
||||
| Param | v2 | v3 |
|
||||
|-------|----|----|
|
||||
| `topK` | 100 | 20 |
|
||||
| `threshold` | none | 0.1 |
|
||||
| `rerank` | true | false |
|
||||
|
||||
**Removed:**
|
||||
- `OutputFormat` and `API_VERSION` enums
|
||||
- `organizationId`, `projectId` from constructor
|
||||
- `enableGraph`, `asyncMode`, `outputFormat`, `immutable`, `expirationDate`, `filterMemories`, `batchSize`, `forceAddOnly`, `includes`, `excludes`, `keywordSearch`
|
||||
|
||||
See the [v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for details.
|
||||
@@ -0,0 +1,487 @@
|
||||
# Mem0 Python SDK Reference
|
||||
|
||||
Complete reference for the `mem0ai` Python package. Covers both the Platform client (managed API) and the Open Source self-hosted variant.
|
||||
|
||||
---
|
||||
|
||||
## Platform Client
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
pip install mem0ai
|
||||
export MEM0_API_KEY="m0-your-api-key"
|
||||
```
|
||||
|
||||
### MemoryClient (Synchronous)
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key="m0-xxx")
|
||||
```
|
||||
|
||||
**Constructor:** `MemoryClient(api_key=None)`. If `api_key` is not provided, reads from `MEM0_API_KEY` environment variable. Raises `ValueError` if no key found.
|
||||
|
||||
- HTTP library: `httpx`
|
||||
- Timeout: 300 seconds
|
||||
- Base URL: `https://api.mem0.ai`
|
||||
|
||||
### AsyncMemoryClient (Asynchronous)
|
||||
|
||||
```python
|
||||
from mem0 import AsyncMemoryClient
|
||||
|
||||
client = AsyncMemoryClient(api_key="m0-xxx")
|
||||
|
||||
# Or use as context manager
|
||||
async with AsyncMemoryClient(api_key="m0-xxx") as client:
|
||||
results = await client.search("query", filters={"user_id": "alice"})
|
||||
```
|
||||
|
||||
Same methods as `MemoryClient`, all `async`/`await`. Supports async context manager.
|
||||
|
||||
---
|
||||
|
||||
### Memory Methods
|
||||
|
||||
#### add(messages, **kwargs)
|
||||
|
||||
Store new memories from messages.
|
||||
|
||||
```python
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
|
||||
{"role": "assistant", "content": "Got it! I'll remember that."}
|
||||
]
|
||||
client.add(messages, user_id="alice")
|
||||
```
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `messages` | str \| dict \| list[dict] | required | Message content. Strings auto-convert to user messages |
|
||||
| `user_id` | str | None | User identifier |
|
||||
| `agent_id` | str | None | Agent identifier |
|
||||
| `app_id` | str | None | Application identifier |
|
||||
| `run_id` | str | None | Session/run identifier |
|
||||
| `metadata` | dict | None | Custom key-value pairs |
|
||||
| `infer` | bool | True | If False, store raw text without LLM inference |
|
||||
| `custom_categories` | list | None | Override project categories |
|
||||
| `custom_instructions` | str | None | Override extraction instructions |
|
||||
| `timestamp` | int \| float \| str | None | Custom timestamp (Unix epoch or ISO 8601) |
|
||||
|
||||
**Returns:** `dict` -- list of events: `[{"id": "...", "event": "ADD", "data": {"memory": "..."}}]`
|
||||
|
||||
#### search(query, **kwargs)
|
||||
|
||||
Search memories by semantic similarity.
|
||||
|
||||
```python
|
||||
results = client.search("dietary preferences", filters={"user_id": "alice"})
|
||||
for mem in results.get("results", []):
|
||||
print(mem["memory"], mem["score"])
|
||||
```
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `query` | str | required | Natural language search query |
|
||||
| `filters` | dict | None | Filter object with entity IDs and/or `AND`/`OR`/`NOT` conditions (e.g., `{"user_id": "alice"}`) |
|
||||
| `top_k` | int | 10 | Number of results |
|
||||
| `rerank` | bool | False | Enable deep semantic reranking (+150-200ms) |
|
||||
| `threshold` | float | 0.1 | Minimum similarity score |
|
||||
| `fields` | list | None | Specific fields to return |
|
||||
| `categories` | list | None | Filter by category |
|
||||
|
||||
**Returns:** `dict` -- `{"results": [{id, memory, user_id, categories, score, created_at, ...}]}`
|
||||
|
||||
#### get(memory_id)
|
||||
|
||||
Retrieve a single memory by ID.
|
||||
|
||||
```python
|
||||
memory = client.get(memory_id="ea925981-...")
|
||||
```
|
||||
|
||||
**Returns:** `dict` -- full memory object
|
||||
|
||||
#### get_all(**kwargs)
|
||||
|
||||
Retrieve all memories with optional filtering. Requires at least one entity identifier.
|
||||
|
||||
```python
|
||||
memories = client.get_all(filters={"user_id": "alice"})
|
||||
# With compound filters
|
||||
memories = client.get_all(filters={"AND": [{"user_id": "alice"}, {"categories": {"contains": "health"}}]})
|
||||
```
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `filters` | dict | None | Filter object with entity IDs and/or `AND`/`OR`/`NOT` conditions |
|
||||
| `top_k` | int | None | Limit results |
|
||||
| `page` | int | None | Page number |
|
||||
| `page_size` | int | None | Results per page |
|
||||
|
||||
**Returns:** `dict` -- `{"results": [...]}`
|
||||
|
||||
#### update(memory_id, text=None, metadata=None, timestamp=None)
|
||||
|
||||
Update a memory's content, metadata, or timestamp. At least one parameter required.
|
||||
|
||||
```python
|
||||
client.update("ea925981-...", text="Updated: vegan since 2024")
|
||||
client.update("ea925981-...", metadata={"verified": True})
|
||||
```
|
||||
|
||||
**Returns:** `dict` -- updated memory
|
||||
|
||||
#### delete(memory_id)
|
||||
|
||||
Permanently delete a single memory.
|
||||
|
||||
```python
|
||||
client.delete("ea925981-...")
|
||||
```
|
||||
|
||||
#### delete_all(**kwargs)
|
||||
|
||||
Delete all memories matching filters. Irreversible.
|
||||
|
||||
```python
|
||||
client.delete_all(user_id="alice")
|
||||
```
|
||||
|
||||
#### history(memory_id)
|
||||
|
||||
Get the change history of a memory.
|
||||
|
||||
```python
|
||||
history = client.history("ea925981-...")
|
||||
# Returns: [{previous_value, new_value, action, timestamps}]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Batch Methods
|
||||
|
||||
#### batch_update(memories)
|
||||
|
||||
Update up to 1000 memories in a single request.
|
||||
|
||||
```python
|
||||
client.batch_update([
|
||||
{"memory_id": "uuid-1", "text": "Updated text"},
|
||||
{"memory_id": "uuid-2", "text": "Another update", "metadata": {"verified": True}},
|
||||
])
|
||||
```
|
||||
|
||||
#### batch_delete(memories)
|
||||
|
||||
Delete up to 1000 memories in a single request.
|
||||
|
||||
```python
|
||||
client.batch_delete([
|
||||
{"memory_id": "uuid-1"},
|
||||
{"memory_id": "uuid-2"},
|
||||
])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### User/Entity Management
|
||||
|
||||
#### users()
|
||||
|
||||
List all users, agents, and sessions that have memories.
|
||||
|
||||
```python
|
||||
users = client.users()
|
||||
# Returns: {"results": [{"type": "user", "name": "alice"}, ...]}
|
||||
```
|
||||
|
||||
#### delete_users(user_id=None, agent_id=None, app_id=None, run_id=None)
|
||||
|
||||
Delete a specific entity and all its memories.
|
||||
|
||||
```python
|
||||
client.delete_users(user_id="alice")
|
||||
```
|
||||
|
||||
#### reset()
|
||||
|
||||
Delete ALL users, agents, sessions, and memories. Complete data reset.
|
||||
|
||||
```python
|
||||
client.reset()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Export & Summary
|
||||
|
||||
#### create_memory_export(schema, **kwargs)
|
||||
|
||||
Create a structured export of memories.
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
schema = json.dumps({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"preferences": {"type": "array", "items": {"type": "string"}},
|
||||
}
|
||||
})
|
||||
export = client.create_memory_export(schema=schema, user_id="alice")
|
||||
```
|
||||
|
||||
#### get_memory_export(**kwargs)
|
||||
|
||||
Retrieve a previously created export.
|
||||
|
||||
```python
|
||||
result = client.get_memory_export(memory_export_id=export["id"])
|
||||
```
|
||||
|
||||
#### get_summary(filters=None)
|
||||
|
||||
Get a summary of memories.
|
||||
|
||||
```python
|
||||
summary = client.get_summary(filters={"user_id": "alice"})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Feedback
|
||||
|
||||
#### feedback(memory_id, feedback=None, feedback_reason=None)
|
||||
|
||||
Provide quality feedback on a memory.
|
||||
|
||||
```python
|
||||
client.feedback(
|
||||
memory_id="mem-123",
|
||||
feedback="POSITIVE", # POSITIVE | NEGATIVE | VERY_NEGATIVE | None (clear)
|
||||
feedback_reason="Accurately captured preference"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Webhooks
|
||||
|
||||
```python
|
||||
# List
|
||||
webhooks = client.get_webhooks(project_id="proj_123")
|
||||
|
||||
# Create
|
||||
webhook = client.create_webhook(
|
||||
url="https://your-app.com/webhook",
|
||||
name="Memory Logger",
|
||||
project_id="proj_123",
|
||||
event_types=["memory_add", "memory_update"]
|
||||
)
|
||||
|
||||
# Update
|
||||
client.update_webhook(webhook_id=123, name="Updated", url="https://new-url.com")
|
||||
|
||||
# Delete
|
||||
client.delete_webhook(webhook_id=123)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Project Management
|
||||
|
||||
Access via `client.project.*`:
|
||||
|
||||
```python
|
||||
# Get project config
|
||||
config = client.project.get(fields=["custom_categories", "custom_instructions"])
|
||||
|
||||
# Update project settings
|
||||
client.project.update(
|
||||
custom_instructions="Extract dietary preferences and health info",
|
||||
custom_categories=[{"health": "Medical and dietary info"}],
|
||||
multilingual=True,
|
||||
)
|
||||
|
||||
# Create/delete project
|
||||
client.project.create(name="My Project", description="...")
|
||||
client.project.delete()
|
||||
|
||||
# Member management
|
||||
members = client.project.get_members()
|
||||
client.project.add_member(email="user@example.com", role="READER") # READER or OWNER
|
||||
client.project.update_member(email="user@example.com", role="OWNER")
|
||||
client.project.remove_member(email="user@example.com")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Open Source / Self-Hosted
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
pip install mem0ai
|
||||
```
|
||||
|
||||
### Memory Class
|
||||
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
m = Memory() # Uses default config (OpenAI embedder + in-memory vector store)
|
||||
```
|
||||
|
||||
**Import:** `from mem0 import Memory` (NOT `MemoryClient` -- that is the Platform client)
|
||||
|
||||
### Configuration
|
||||
|
||||
```python
|
||||
config = {
|
||||
"llm": {
|
||||
"provider": "openai", # openai, groq, azure, ollama, lmstudio, google, anthropic, mistral
|
||||
"config": {
|
||||
"model": "gpt-5-mini",
|
||||
"api_key": "sk-xxx",
|
||||
}
|
||||
},
|
||||
"embedder": {
|
||||
"provider": "openai", # openai, ollama, azure, lmstudio, google, huggingface
|
||||
"config": {
|
||||
"model": "text-embedding-3-small",
|
||||
"api_key": "sk-xxx",
|
||||
}
|
||||
},
|
||||
"vector_store": {
|
||||
"provider": "qdrant", # faiss, qdrant, pgvector, redis, supabase, azure_ai_search, memory
|
||||
"config": {
|
||||
"collection_name": "my_memories",
|
||||
"host": "localhost",
|
||||
"port": 6333,
|
||||
}
|
||||
},
|
||||
"history_db_path": "history.db", # SQLite path for change history
|
||||
"custom_instructions": "...", # Custom LLM prompt for extraction
|
||||
}
|
||||
|
||||
m = Memory.from_config(config)
|
||||
```
|
||||
|
||||
### Context Manager
|
||||
|
||||
```python
|
||||
with Memory(config) as m:
|
||||
m.add("I prefer dark mode", user_id="alice")
|
||||
results = m.search("preferences", filters={"user_id": "alice"})
|
||||
# SQLite connections released automatically
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
All methods mirror the Platform client but run locally:
|
||||
|
||||
#### add(messages, *, user_id, agent_id, run_id, metadata, infer=True)
|
||||
|
||||
```python
|
||||
m.add("I'm a vegetarian", user_id="alice")
|
||||
m.add([
|
||||
{"role": "user", "content": "I like hiking"},
|
||||
{"role": "assistant", "content": "Great outdoor activity!"}
|
||||
], user_id="alice")
|
||||
```
|
||||
|
||||
At least one of `user_id`, `agent_id`, `run_id` required.
|
||||
|
||||
**Returns:** `{"results": [...], "relations": [...]}`
|
||||
|
||||
#### search(query, *, filters=None, top_k=20, threshold=0.1, rerank=False)
|
||||
|
||||
```python
|
||||
results = m.search("dietary preferences", filters={"user_id": "alice"}, top_k=5)
|
||||
```
|
||||
|
||||
Entity IDs (`user_id`, `agent_id`, `run_id`) must be passed inside the `filters` dict.
|
||||
|
||||
Supports filter operators: `eq`, `ne`, `in`, `nin`, `gt`, `gte`, `lt`, `lte`, `contains`, `not_contains`.
|
||||
|
||||
#### get(memory_id) / get_all(**kwargs) / update(memory_id, data, metadata=None) / delete(memory_id) / delete_all(**kwargs) / history(memory_id)
|
||||
|
||||
Same interface as Platform client.
|
||||
|
||||
#### reset()
|
||||
|
||||
Clear the entire vector store collection and history database. Recreates the vector store.
|
||||
|
||||
```python
|
||||
m.reset()
|
||||
```
|
||||
|
||||
#### close()
|
||||
|
||||
Release SQLite connections. Called automatically when using context manager.
|
||||
|
||||
### AsyncMemory
|
||||
|
||||
```python
|
||||
from mem0 import AsyncMemory
|
||||
|
||||
m = AsyncMemory(config)
|
||||
await m.add("text", user_id="alice")
|
||||
results = await m.search("query", filters={"user_id": "alice"})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Differences: Platform vs OSS
|
||||
|
||||
| Aspect | Platform (`MemoryClient`) | OSS (`Memory`) |
|
||||
|--------|--------------------------|----------------|
|
||||
| **Import** | `from mem0 import MemoryClient` | `from mem0 import Memory` |
|
||||
| **Auth** | API key required (`MEM0_API_KEY`) | No API key -- config-based |
|
||||
| **Execution** | API calls to `api.mem0.ai` | Local execution |
|
||||
| **Infrastructure** | Fully managed | Self-managed vector DB, embedder, LLM |
|
||||
| **Entity filtering** | `filters={"user_id": "..."}` | `filters={"user_id": "..."}` |
|
||||
| **Batch ops** | `batch_update`, `batch_delete` | Not available |
|
||||
| **Webhooks** | Full CRUD | Not available |
|
||||
| **Export** | `create_memory_export`, `get_memory_export` | Not available |
|
||||
| **Feedback** | `feedback()` | Not available |
|
||||
| **Project mgmt** | `client.project.*` | Not available |
|
||||
| **User listing** | `users()`, `delete_users()` | Not available |
|
||||
| **Custom prompts** | Via project settings | Direct config (`custom_instructions`) |
|
||||
| **History** | Platform-managed | SQLite (configurable) |
|
||||
| **Async** | `AsyncMemoryClient` | `AsyncMemory` |
|
||||
|
||||
---
|
||||
|
||||
## v2 Compatibility
|
||||
|
||||
If you're using SDK v2.x or the v2 API:
|
||||
|
||||
**API Changes:**
|
||||
- **Entity IDs in search/get_all:** Pass `user_id`, `agent_id` as top-level kwargs instead of inside `filters`
|
||||
```python
|
||||
# v2
|
||||
results = client.search("query", user_id="alice")
|
||||
# v3
|
||||
results = client.search("query", filters={"user_id": "alice"})
|
||||
```
|
||||
- **add() returns:** v2 returns ADD, UPDATE, DELETE events; v3 returns ADD only
|
||||
|
||||
**Default Changes:**
|
||||
| Param | v2 | v3 |
|
||||
|-------|----|----|
|
||||
| `top_k` | 100 | 20 |
|
||||
| `threshold` | None | 0.1 |
|
||||
| `rerank` | True | False |
|
||||
|
||||
**Removed Parameters:**
|
||||
- Constructor: `org_id`, `project_id`
|
||||
- add(): `async_mode`, `output_format`, `enable_graph`, `immutable`, `expiration_date`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`
|
||||
- search()/get_all(): `enable_graph`
|
||||
- Config: `enable_graph`, `graph_store`, `custom_fact_extraction_prompt` (renamed to `custom_instructions`)
|
||||
|
||||
See the [v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for full details.
|
||||
@@ -8,13 +8,15 @@ All endpoints require: `Authorization: Token <MEM0_API_KEY>`
|
||||
|
||||
| Operation | Method | URL |
|
||||
|-----------|--------|-----|
|
||||
| Add Memories | `POST` | `/v1/memories/` |
|
||||
| Search Memories | `POST` | `/v2/memories/search/` |
|
||||
| Get All Memories | `POST` | `/v2/memories/` |
|
||||
| Add Memories | `POST` | `/v3/memories/add/` |
|
||||
| Search Memories | `POST` | `/v3/memories/search/` |
|
||||
| Get All Memories | `POST` | `/v3/memories/` |
|
||||
| Get Single Memory | `GET` | `/v1/memories/{memory_id}/` |
|
||||
| Update Memory | `PUT` | `/v1/memories/{memory_id}/` |
|
||||
| Delete Memory | `DELETE` | `/v1/memories/{memory_id}/` |
|
||||
|
||||
Note: v1/v2 endpoints still work (backward compatible).
|
||||
|
||||
## Memory Object Structure
|
||||
|
||||
| Field | Type | Description |
|
||||
@@ -27,8 +29,6 @@ All endpoints require: `Authorization: Token <MEM0_API_KEY>`
|
||||
| `run_id` | string (nullable) | Run/session identifier |
|
||||
| `metadata` | object | Custom key-value pairs |
|
||||
| `categories` | array of strings | Auto-assigned category tags |
|
||||
| `immutable` | boolean | If true, prevents modification |
|
||||
| `expiration_date` | datetime (nullable) | Auto-expiry date |
|
||||
| `hash` | string | Content hash |
|
||||
| `created_at` | datetime | Creation timestamp |
|
||||
| `updated_at` | datetime | Last modification timestamp |
|
||||
@@ -50,10 +50,9 @@ Memories can be scoped to different levels:
|
||||
|
||||
## Processing Model
|
||||
|
||||
- Memories are processed **asynchronously by default** (`async_mode=true`)
|
||||
- Add responses return queued events (`ADD`, `UPDATE`, `DELETE`) for tracking
|
||||
- Set `async_mode=false` for synchronous processing when needed
|
||||
- Graph metadata is processed asynchronously -- use `get_all()` for complete graph data
|
||||
- Memories are processed **asynchronously** (v3 default)
|
||||
- Add responses return queued `ADD` events only (v3 is ADD-only, no UPDATE/DELETE)
|
||||
- Poll status via `GET /v1/event/{event_id}/`
|
||||
|
||||
## Filter System
|
||||
|
||||
@@ -106,19 +105,17 @@ Root must be `AND`, `OR`, or `NOT`. Simple shorthand `{"user_id": "alice"}` also
|
||||
|
||||
## Response Formats
|
||||
|
||||
### Add Response
|
||||
### Add Response (v3)
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "mem_01JF8ZS4Y0R0SPM13R5R6H32CJ",
|
||||
"event": "ADD",
|
||||
"data": { "memory": "The user moved to Austin in 2025." }
|
||||
}
|
||||
]
|
||||
{
|
||||
"message": "Memory processing has been queued for background execution",
|
||||
"status": "PENDING",
|
||||
"event_id": "evt-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
Event types: `ADD`, `UPDATE`, `DELETE`. A single add can trigger multiple events.
|
||||
v3 is ADD-only. No UPDATE or DELETE events.
|
||||
|
||||
### Search Response
|
||||
|
||||
@@ -137,4 +134,17 @@ Event types: `ADD`, `UPDATE`, `DELETE`. A single add can trigger multiple events
|
||||
}
|
||||
```
|
||||
|
||||
With `enable_graph=true`, includes additional `relations` array with entity relationships.
|
||||
In v3, `score` is a combined multi-signal relevance score.
|
||||
|
||||
### Get All Response (v3)
|
||||
|
||||
```json
|
||||
{
|
||||
"count": 123,
|
||||
"next": "https://api.mem0.ai/v3/memories/?page=2&page_size=50",
|
||||
"previous": null,
|
||||
"results": [...]
|
||||
}
|
||||
```
|
||||
|
||||
v3 returns paginated envelope. Use `page` and `page_size` query params.
|
||||
|
||||
@@ -25,9 +25,9 @@ User Input → Retrieve relevant memories → Enrich LLM prompt → Generate res
|
||||
|
||||
Mem0 handles the complexity of extraction, deduplication, conflict resolution, and semantic retrieval so your application only needs to call `search()` and `add()`.
|
||||
|
||||
**Dual storage architecture:**
|
||||
**Storage architecture:**
|
||||
- **Vector store**: Embeddings for semantic similarity search
|
||||
- **Graph store** (optional): Entity nodes and relationship edges for structured knowledge
|
||||
- **Entity store**: Automatic entity linking for relationship-aware retrieval
|
||||
|
||||
---
|
||||
|
||||
@@ -40,41 +40,32 @@ Messages In
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 1. EXTRACTION │ LLM analyzes messages, extracts key facts
|
||||
│ 1. EXTRACTION │ Single LLM call extracts all distinct new facts
|
||||
│ (infer=True) │ If infer=False, stores raw text as-is
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 2. CONFLICT │ Checks existing memories for duplicates
|
||||
│ RESOLUTION │ Latest truth wins (newer overrides older)
|
||||
│ │ Only runs when infer=True
|
||||
│ 2. DEDUPLICATION │ Hash-based dedup (MD5 prevents exact duplicates)
|
||||
│ │ No UPDATE/DELETE - v3 is ADD-only
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 3. STORAGE │ Generates embeddings → vector store
|
||||
│ │ Optional: entity extraction → graph store
|
||||
│ │ Indexes metadata, categories, timestamps
|
||||
│ 3. STORAGE │ Batch embed → vector store
|
||||
│ │ Entity extraction → entity store
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
Memory Object
|
||||
(id, memory, categories, structured_attributes)
|
||||
```
|
||||
|
||||
### Processing modes
|
||||
### Processing (v3)
|
||||
|
||||
**Async (default, `async_mode=True`):**
|
||||
- API returns immediately: `{"status": "PENDING", "event_id": "..."}`
|
||||
- Processing happens in background
|
||||
v3 processes memories asynchronously by default:
|
||||
- API returns immediately: `{"status": "PENDING", "event_id": "evt-..."}`
|
||||
- Poll status via `GET /v1/event/{event_id}/`
|
||||
- Use webhooks for completion notifications
|
||||
- Best for: high-throughput, non-blocking workflows
|
||||
|
||||
**Sync (`async_mode=False`):**
|
||||
- API waits for full processing
|
||||
- Returns complete memory object with `id`, `event`, `memory`
|
||||
- Best for: real-time access immediately after add
|
||||
|
||||
### Extraction modes
|
||||
|
||||
@@ -93,7 +84,7 @@ Messages In
|
||||
|
||||
---
|
||||
|
||||
## Retrieval Pipeline
|
||||
## Retrieval Pipeline (v3)
|
||||
|
||||
### What happens when you call `client.search()`
|
||||
|
||||
@@ -102,45 +93,37 @@ Query In
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 1. QUERY EMBEDDING │ Convert query to vector representation
|
||||
│ 1. PREPROCESSING │ Lemmatize keywords, extract entities
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 2. VECTOR SEARCH │ Cosine similarity across stored embeddings
|
||||
│ │ Scoped by filters (user_id, agent_id, etc.)
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼ (optional enhancements)
|
||||
┌─────────────────────┐
|
||||
│ 3a. KEYWORD SEARCH │ Expands results with specific terms (+10ms)
|
||||
│ 3b. RERANKING │ Deep semantic reordering (+150-200ms)
|
||||
│ 3c. FILTER MEMORIES │ Precision filtering, removes low-relevance (+200-300ms)
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼ (if enable_graph=True)
|
||||
┌─────────────────────┐
|
||||
│ 4. GRAPH LOOKUP │ Finds entity relationships
|
||||
│ │ Appends relations WITHOUT reranking vector results
|
||||
│ 2. PARALLEL SCORING │ Semantic search (vector similarity)
|
||||
│ │ BM25 keyword search (term matching)
|
||||
│ │ Entity matching (entity graph boost)
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
Results + Relations
|
||||
┌─────────────────────┐
|
||||
│ 3. SCORE FUSION │ Combine signals into single score
|
||||
│ │ Optional: rerank=True for deep reordering
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
▼
|
||||
Results (combined score per memory)
|
||||
```
|
||||
|
||||
### Retrieval enhancement combinations
|
||||
### v3 Search Defaults
|
||||
|
||||
| Configuration | Latency | Best for |
|
||||
|--------------|---------|----------|
|
||||
| Base search only | ~100ms | Simple lookups |
|
||||
| `keyword_search=True` | ~110ms | Entity-heavy queries, broad coverage |
|
||||
| `rerank=True` | ~250-300ms | User-facing results, top-N precision |
|
||||
| `keyword_search=True` + `rerank=True` | ~310ms | Balanced (recommended for most apps) |
|
||||
| `rerank=True` + `filter_memories=True` | ~400-500ms | Safety-critical, production systems |
|
||||
| Parameter | Default | Notes |
|
||||
|-----------|---------|-------|
|
||||
| `top_k` | 20 | Was 100 in v2 |
|
||||
| `threshold` | 0.1 | Was None in v2 |
|
||||
| `rerank` | False | Was True in v2 |
|
||||
|
||||
### Implicit null scoping
|
||||
|
||||
When you search with `user_id="alice"` only, Mem0 returns memories where `agent_id`, `app_id`, and `run_id` are all null. This prevents cross-scope leakage by default.
|
||||
When you search with `filters={"user_id": "alice"}` only, Mem0 returns memories where `agent_id`, `app_id`, and `run_id` are all null. This prevents cross-scope leakage by default.
|
||||
|
||||
To include memories with non-null fields, use explicit filters:
|
||||
```python
|
||||
@@ -150,53 +133,23 @@ filters={"OR": [{"user_id": "alice"}]}
|
||||
|
||||
---
|
||||
|
||||
## Memory Lifecycle
|
||||
## Memory Lifecycle (v3)
|
||||
|
||||
```
|
||||
CREATE ──→ ACTIVE ──→ UPDATE ──→ ACTIVE
|
||||
│ │ │
|
||||
│ ▼ ▼
|
||||
│ EXPIRED EXPIRED
|
||||
│ (still stored, (still stored,
|
||||
│ not retrieved) not retrieved)
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
DELETE DELETE DELETE
|
||||
(permanent)
|
||||
```
|
||||
v3 uses ADD-only extraction. Memories accumulate over time rather than being consolidated.
|
||||
|
||||
### Creation
|
||||
- Triggered by `client.add(messages, user_id="...")`
|
||||
- Messages processed through extraction → conflict resolution → storage
|
||||
- Gets unique UUID, `created_at` timestamp
|
||||
- Optional: custom `timestamp`, `expiration_date`, `metadata`, `immutable`
|
||||
- `client.add(messages, user_id="...")`
|
||||
- Single-pass extraction → deduplication → storage
|
||||
- Returns `{"event_id": "...", "status": "PENDING"}`
|
||||
|
||||
### Updates
|
||||
- `client.update(memory_id, text="...")` replaces text and reindexes
|
||||
- `client.batch_update([...])` for up to 1000 memories at once
|
||||
- Immutable memories (`immutable=True`) cannot be updated — must delete and re-add
|
||||
|
||||
### Deduplication
|
||||
- Automatic during `add()` with `infer=True`
|
||||
- Conflict resolution merges duplicate facts
|
||||
- Latest truth wins when contradictions detected
|
||||
- Prevents memory bloat from repeated information
|
||||
|
||||
### Expiration
|
||||
- Optional `expiration_date` parameter (ISO 8601 or `YYYY-MM-DD`)
|
||||
- After expiration: memory NOT returned in searches but remains in storage
|
||||
- Useful for time-sensitive info (events, temporary preferences, session state)
|
||||
- `client.update(memory_id, text="...")` replaces text
|
||||
- Batch: `client.batch_update([...])`
|
||||
|
||||
### Deletion
|
||||
- Single: `client.delete(memory_id)` — permanent, no recovery
|
||||
- Batch: `client.batch_delete([memory_ids])` — up to 1000
|
||||
- Bulk: `client.delete_all(user_id="alice")` — all memories for entity
|
||||
- `delete_all()` without filters raises error to prevent accidental data loss
|
||||
|
||||
### History tracking
|
||||
- `client.history(memory_id)` returns version timeline
|
||||
- Shows all changes: `{previous_value, new_value, action, timestamps}`
|
||||
- Useful for audit trails and debugging
|
||||
- Single: `client.delete(memory_id)`
|
||||
- Batch: `client.batch_delete([...])`
|
||||
- Bulk: `client.delete_all(filters={"user_id": "alice"})`
|
||||
|
||||
---
|
||||
|
||||
@@ -214,8 +167,6 @@ DELETE DELETE DELETE
|
||||
"categories": ["health", "preferences"],
|
||||
"created_at": "2025-03-12T12:34:56Z",
|
||||
"updated_at": "2025-03-12T12:34:56Z",
|
||||
"expiration_date": null,
|
||||
"immutable": false,
|
||||
"structured_attributes": {
|
||||
"day": 12, "month": 3, "year": 2025,
|
||||
"hour": 12, "minute": 34,
|
||||
@@ -239,8 +190,6 @@ DELETE DELETE DELETE
|
||||
| `categories` | array | Auto-assigned or custom category tags |
|
||||
| `created_at` | datetime | Creation timestamp |
|
||||
| `updated_at` | datetime | Last modification timestamp |
|
||||
| `expiration_date` | datetime | Auto-expiry date (stops retrieval, data persists) |
|
||||
| `immutable` | boolean | If true, prevents modification |
|
||||
| `structured_attributes` | object | Temporal breakdown for time-based queries |
|
||||
| `score` | float | Semantic similarity (search results only, 0-1) |
|
||||
|
||||
@@ -322,7 +271,7 @@ Mem0 supports three layers of memory, from shortest to longest lived:
|
||||
```python
|
||||
def chat(user_input: str, user_id: str, session_id: str) -> str:
|
||||
# 1. Retrieve user memories (long-term preferences)
|
||||
user_mems = mem0.search(user_input, user_id=user_id)
|
||||
user_mems = mem0.search(user_input, filters={"user_id": user_id})
|
||||
|
||||
# 2. Retrieve session memories (current task context)
|
||||
session_mems = mem0.search(user_input, filters={
|
||||
@@ -350,18 +299,13 @@ def chat(user_input: str, user_id: str, session_id: str) -> str:
|
||||
|
||||
| Operation | Typical Latency |
|
||||
|-----------|----------------|
|
||||
| Base vector search | ~100ms |
|
||||
| + keyword_search | +10ms |
|
||||
| Hybrid search (v3 default) | ~100-150ms |
|
||||
| + reranking | +150-200ms |
|
||||
| + filter_memories | +200-300ms |
|
||||
| Add (async, default) | < 50ms response, background processing |
|
||||
| Add (sync) | 500ms-2s depending on extraction complexity |
|
||||
| Graph operations | Slight overhead for large stores |
|
||||
| Add (async) | < 50ms response |
|
||||
|
||||
### Processing
|
||||
|
||||
- **Async mode (default):** Returns immediately, processes in background
|
||||
- **Sync mode:** Waits for full extraction + storage pipeline
|
||||
- **Async (default):** Returns immediately, processes in background
|
||||
- **Batch operations:** Up to 1000 memories per batch_update/batch_delete
|
||||
- **Webhooks:** Real-time notifications when async processing completes
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Additional platform capabilities beyond core CRUD operations.
|
||||
## Table of Contents
|
||||
|
||||
- [Advanced Retrieval](#advanced-retrieval)
|
||||
- [Graph Memory](#graph-memory)
|
||||
- [Entity Linking](#entity-linking)
|
||||
- [Custom Categories](#custom-categories)
|
||||
- [Custom Instructions](#custom-instructions)
|
||||
- [Criteria Retrieval](#criteria-retrieval)
|
||||
@@ -18,124 +18,58 @@ Additional platform capabilities beyond core CRUD operations.
|
||||
|
||||
## Advanced Retrieval
|
||||
|
||||
Three enhancement options for tuning search precision, recall, and latency.
|
||||
### Hybrid Search (v3 Default)
|
||||
|
||||
### Keyword Search (`keyword_search=True`)
|
||||
v3 uses multi-signal hybrid search combining:
|
||||
- **Semantic search** (vector similarity)
|
||||
- **BM25 keyword search** (normalized term matching)
|
||||
- **Entity matching** (entity graph boost)
|
||||
|
||||
Expands results to include memories with specific terms, names, and technical keywords.
|
||||
|
||||
- Latency: +10ms
|
||||
- Recall: Significantly increased
|
||||
- Best for: entity-heavy queries, comprehensive coverage
|
||||
This is automatic — no configuration needed.
|
||||
|
||||
### Reranking (`rerank=True`)
|
||||
|
||||
Deep semantic reordering of results — most relevant first.
|
||||
|
||||
- Latency: +150-200ms
|
||||
- Accuracy: Significantly improved
|
||||
- Default: `False` (was `True` in v2)
|
||||
- Best for: user-facing results, top-N precision
|
||||
|
||||
### Filter Memories (`filter_memories=True`)
|
||||
|
||||
Precision filtering — removes low-relevance results entirely.
|
||||
|
||||
- Latency: +200-300ms
|
||||
- Precision: Maximized
|
||||
- Best for: safety-critical applications, production systems
|
||||
|
||||
### Recommended Combinations
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
# Fast & broad
|
||||
results = client.search(query, keyword_search=True, user_id="user123")
|
||||
|
||||
# Balanced (recommended for most apps)
|
||||
results = client.search(query, keyword_search=True, rerank=True, user_id="user123")
|
||||
|
||||
# High precision (critical apps)
|
||||
results = client.search(query, rerank=True, filter_memories=True, user_id="user123")
|
||||
results = client.search(query, filters={"user_id": "user123"}, rerank=True)
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
const results = await client.search(query, {
|
||||
user_id: 'user123',
|
||||
keyword_search: true,
|
||||
filters: { user_id: 'user123' },
|
||||
rerank: true,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Graph Memory
|
||||
## Entity Linking
|
||||
|
||||
Entity-level knowledge graph that creates relationships between memories.
|
||||
v3 replaces graph memory with built-in entity linking. Entities (proper nouns, quoted text, compound noun phrases) are automatically extracted and linked across memories.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Extraction**: LLM analyzes conversation and identifies entities and relationships
|
||||
2. **Storage**: Embeddings go to vector store; entity nodes and edges go to graph store
|
||||
3. **Retrieval**: Vector search returns semantic matches; graph relations are appended to results
|
||||
1. **Extraction**: During `add()`, entities are automatically extracted from memory text
|
||||
2. **Storage**: Entities are stored in a parallel collection (`{collection}_entities`)
|
||||
3. **Retrieval**: During `search()`, query entities are matched and used to boost relevant memories
|
||||
|
||||
Graph relations **augment** vector results without reordering them. Vector similarity always determines hit sequence.
|
||||
Entity linking is automatic — no configuration required. The boost is folded into the combined `score` on each result.
|
||||
|
||||
### Enabling Graph Memory
|
||||
### v2 Migration Note
|
||||
|
||||
**Per request:**
|
||||
```python
|
||||
client.add(messages, user_id="alice", enable_graph=True)
|
||||
client.search("query", user_id="alice", enable_graph=True)
|
||||
client.get_all(filters={"AND": [{"user_id": "alice"}]}, enable_graph=True)
|
||||
```
|
||||
If you were using `enable_graph=True` in v2:
|
||||
- Remove `enable_graph` from all API calls
|
||||
- Remove `graph_store` from OSS configuration
|
||||
- Entity relationships are now consumed through retrieval ranking, not exposed as a separate `relations` array
|
||||
|
||||
**Project-level (default for all operations):**
|
||||
```python
|
||||
client.project.update(enable_graph=True)
|
||||
```
|
||||
|
||||
```javascript
|
||||
await client.updateProject({ enable_graph: true });
|
||||
```
|
||||
|
||||
### Relation Structure
|
||||
|
||||
Each relation in the response contains:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `source` | string | Source entity name |
|
||||
| `source_type` | string | Source entity type (e.g., "Person") |
|
||||
| `relationship` | string | Relationship label (e.g., "lives_in") |
|
||||
| `target` | string | Target entity name |
|
||||
| `target_type` | string | Target entity type (e.g., "City") |
|
||||
| `score` | number | Confidence score |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"relations": [
|
||||
{
|
||||
"source": "Joseph",
|
||||
"source_type": "Person",
|
||||
"relationship": "lives_in",
|
||||
"target": "Seattle",
|
||||
"target_type": "City",
|
||||
"score": 0.92
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Technical Notes
|
||||
|
||||
- Graph Memory adds processing time; see docs for current plan availability
|
||||
- Works optimally with rich conversation histories containing entity relationships
|
||||
- Best suited for long-running assistants tracking evolving information
|
||||
- Graph writes and reads toggle independently per request
|
||||
- Multi-agent context supported via `user_id`, `agent_id`, `run_id` scoping
|
||||
- Add operations are asynchronous; graph metadata may not be immediately available
|
||||
See the [v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) for details.
|
||||
|
||||
---
|
||||
|
||||
@@ -160,7 +94,7 @@ client.project.update(custom_categories=new_categories)
|
||||
```
|
||||
|
||||
```javascript
|
||||
await client.updateProject({ custom_categories: new_categories });
|
||||
await client.updateProject({ customCategories: newCategories });
|
||||
```
|
||||
|
||||
**Retrieve active categories:**
|
||||
@@ -185,7 +119,7 @@ client.project.update(custom_instructions="Your guidelines here...")
|
||||
```
|
||||
|
||||
```javascript
|
||||
await client.updateProject({ custom_instructions: "Your guidelines here..." });
|
||||
await client.updateProject({ customInstructions: "Your guidelines here..." });
|
||||
```
|
||||
|
||||
### Template Structure
|
||||
@@ -229,7 +163,7 @@ client.project.update(retrieval_criteria=retrieval_criteria)
|
||||
|
||||
```typescript
|
||||
await client.updateProject({
|
||||
retrieval_criteria: [
|
||||
retrievalCriteria: [
|
||||
{ name: 'joy', description: 'Positive emotions', weight: 3 },
|
||||
{ name: 'urgency', description: 'Time-sensitive items', weight: 4 },
|
||||
],
|
||||
@@ -281,7 +215,7 @@ for item in feedback_data:
|
||||
```typescript
|
||||
await client.feedback('mem-123', {
|
||||
feedback: 'POSITIVE',
|
||||
feedback_reason: 'Accurately captured dietary preference',
|
||||
feedbackReason: 'Accurately captured dietary preference',
|
||||
});
|
||||
```
|
||||
|
||||
@@ -349,23 +283,18 @@ Use the `name` field in messages to identify speakers. Mem0 maps names to entity
|
||||
|
||||
## MCP Integration
|
||||
|
||||
Model Context Protocol integration enables AI clients (Claude Desktop, Cursor, custom agents) to manage Mem0 memory autonomously.
|
||||
Model Context Protocol integration enables AI clients (Claude, Claude Code, Cursor, Windsurf, VS Code, OpenCode) to manage Mem0 memory autonomously.
|
||||
|
||||
### Configuration
|
||||
### Setup
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": {
|
||||
"command": "uvx",
|
||||
"args": ["mem0-mcp-server"],
|
||||
"env": {
|
||||
"MEM0_API_KEY": "m0-your-api-key",
|
||||
"MEM0_DEFAULT_USER_ID": "your-user-id"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Add Mem0 MCP to your clients with a single command:
|
||||
|
||||
```bash
|
||||
npx mcp-add \
|
||||
--name mem0-mcp \
|
||||
--type http \
|
||||
--url "https://mcp.mem0.ai/mcp" \
|
||||
--clients "claude,claude code,cursor,windsurf,vscode,opencode"
|
||||
```
|
||||
|
||||
### Available MCP Tools
|
||||
@@ -377,7 +306,7 @@ The MCP server exposes 9 memory tools that AI agents can use autonomously:
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Configure the MCP server in your AI client
|
||||
1. Add Mem0 MCP to your AI client using the setup command above
|
||||
2. The agent autonomously decides when to store/retrieve memories
|
||||
3. No manual API calls needed — the agent manages memory as part of its reasoning
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from langchain_core.messages import SystemMessage, HumanMessage
|
||||
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
||||
from mem0 import MemoryClient
|
||||
|
||||
llm = ChatOpenAI(model="gpt-4.1-nano-2025-04-14")
|
||||
llm = ChatOpenAI(model="gpt-5-mini")
|
||||
mem0 = MemoryClient()
|
||||
|
||||
prompt = ChatPromptTemplate.from_messages([
|
||||
@@ -38,7 +38,7 @@ prompt = ChatPromptTemplate.from_messages([
|
||||
|
||||
def retrieve_context(query: str, user_id: str):
|
||||
"""Retrieve relevant memories from Mem0"""
|
||||
memories = mem0.search(query, user_id=user_id)
|
||||
memories = mem0.search(query, filters={"user_id": user_id})
|
||||
memory_list = memories['results']
|
||||
serialized = ' '.join([m["memory"] for m in memory_list])
|
||||
return [
|
||||
@@ -116,73 +116,24 @@ result = crew.kickoff()
|
||||
|
||||
## Vercel AI SDK
|
||||
|
||||
Source: [docs.mem0.ai/integrations/vercel-ai-sdk](https://docs.mem0.ai/integrations/vercel-ai-sdk)
|
||||
> **Dedicated skill available.** For comprehensive Vercel AI SDK documentation, see the [mem0-vercel-ai-sdk skill](https://github.com/mem0ai/mem0/tree/main/skills/mem0-vercel-ai-sdk).
|
||||
|
||||
Install: `npm install @mem0/vercel-ai-provider`
|
||||
|
||||
### Basic Text Generation with Memory
|
||||
Quick example (wrapped model with automatic memory):
|
||||
|
||||
```typescript
|
||||
import { generateText } from "ai";
|
||||
import { createMem0 } from "@mem0/vercel-ai-provider";
|
||||
|
||||
const mem0 = createMem0({
|
||||
provider: "openai",
|
||||
mem0ApiKey: "m0-xxx",
|
||||
apiKey: "openai-api-key",
|
||||
});
|
||||
|
||||
const { text } = await generateText({
|
||||
model: mem0("gpt-4-turbo", { user_id: "borat" }),
|
||||
prompt: "Suggest me a good car to buy!",
|
||||
});
|
||||
```
|
||||
|
||||
### Streaming with Memory
|
||||
|
||||
```typescript
|
||||
import { streamText } from "ai";
|
||||
import { createMem0 } from "@mem0/vercel-ai-provider";
|
||||
|
||||
const mem0 = createMem0();
|
||||
|
||||
const { textStream } = streamText({
|
||||
model: mem0("gpt-4-turbo", { user_id: "borat" }),
|
||||
const { text } = await generateText({
|
||||
model: mem0("gpt-5-mini", { user_id: "borat" }),
|
||||
prompt: "Suggest me a good car to buy!",
|
||||
});
|
||||
|
||||
for await (const textPart of textStream) {
|
||||
process.stdout.write(textPart);
|
||||
}
|
||||
```
|
||||
|
||||
### Using Memory Utilities Standalone
|
||||
|
||||
```typescript
|
||||
import { openai } from "@ai-sdk/openai";
|
||||
import { generateText } from "ai";
|
||||
import { retrieveMemories, addMemories } from "@mem0/vercel-ai-provider";
|
||||
|
||||
// Retrieve memories and inject into any provider
|
||||
const prompt = "Suggest me a good car to buy.";
|
||||
const memories = await retrieveMemories(prompt, { user_id: "borat", mem0ApiKey: "m0-xxx" });
|
||||
|
||||
const { text } = await generateText({
|
||||
model: openai("gpt-4-turbo"),
|
||||
prompt: prompt,
|
||||
system: memories,
|
||||
});
|
||||
|
||||
// Store new memories
|
||||
await addMemories(
|
||||
[{ role: "user", content: [{ type: "text", text: "I love red cars." }] }],
|
||||
{ user_id: "borat", mem0ApiKey: "m0-xxx" }
|
||||
);
|
||||
```
|
||||
|
||||
### Supported Providers
|
||||
|
||||
`openai`, `anthropic`, `google`, `groq`
|
||||
Supported providers: `openai`, `anthropic`, `google`, `groq`, `cohere`
|
||||
|
||||
---
|
||||
|
||||
@@ -199,7 +150,7 @@ mem0 = MemoryClient()
|
||||
@function_tool
|
||||
def search_memory(query: str, user_id: str) -> str:
|
||||
"""Search through past conversations and memories"""
|
||||
memories = mem0.search(query, user_id=user_id, top_k=3)
|
||||
memories = mem0.search(query, filters={"user_id": user_id}, top_k=3)
|
||||
if memories and memories.get('results'):
|
||||
return "\n".join([f"- {mem['memory']}" for mem in memories['results']])
|
||||
return "No relevant memories found."
|
||||
@@ -216,7 +167,7 @@ agent = Agent(
|
||||
Use search_memory to recall past conversations.
|
||||
Use save_memory to store important information.""",
|
||||
tools=[search_memory, save_memory],
|
||||
model="gpt-4.1-nano-2025-04-14"
|
||||
model="gpt-5-mini"
|
||||
)
|
||||
|
||||
result = Runner.run_sync(agent, "I love Italian food and I'm planning a trip to Rome next month")
|
||||
@@ -232,21 +183,21 @@ travel_agent = Agent(
|
||||
name="Travel Planner",
|
||||
instructions="You are a travel planning specialist. Use search_memory and save_memory tools.",
|
||||
tools=[search_memory, save_memory],
|
||||
model="gpt-4.1-nano-2025-04-14"
|
||||
model="gpt-5-mini"
|
||||
)
|
||||
|
||||
health_agent = Agent(
|
||||
name="Health Advisor",
|
||||
instructions="You are a health and wellness advisor. Use search_memory and save_memory tools.",
|
||||
tools=[search_memory, save_memory],
|
||||
model="gpt-4.1-nano-2025-04-14"
|
||||
model="gpt-5-mini"
|
||||
)
|
||||
|
||||
triage_agent = Agent(
|
||||
name="Personal Assistant",
|
||||
instructions="""Route travel questions to Travel Planner, health questions to Health Advisor.""",
|
||||
handoffs=[travel_agent, health_agent],
|
||||
model="gpt-4.1-nano-2025-04-14"
|
||||
model="gpt-5-mini"
|
||||
)
|
||||
|
||||
result = Runner.run_sync(triage_agent, "Plan a healthy meal for my Italy trip")
|
||||
@@ -303,7 +254,7 @@ from langchain_openai import ChatOpenAI
|
||||
from mem0 import MemoryClient
|
||||
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
|
||||
|
||||
llm = ChatOpenAI(model="gpt-4")
|
||||
llm = ChatOpenAI(model="gpt-5-mini")
|
||||
mem0 = MemoryClient()
|
||||
|
||||
class State(TypedDict):
|
||||
@@ -315,7 +266,7 @@ def chatbot(state: State):
|
||||
user_id = state["mem0_user_id"]
|
||||
|
||||
# Retrieve relevant memories
|
||||
memories = mem0.search(messages[-1].content, user_id=user_id)
|
||||
memories = mem0.search(messages[-1].content, filters={"user_id": user_id})
|
||||
context = "Relevant context:\n"
|
||||
for memory in memories["results"]:
|
||||
context += f"- {memory['memory']}\n"
|
||||
@@ -368,7 +319,7 @@ memory = Mem0Memory.from_client(
|
||||
from llama_index.core.agent import FunctionCallingAgent
|
||||
from llama_index.llms.openai import OpenAI
|
||||
|
||||
llm = OpenAI(model="gpt-4")
|
||||
llm = OpenAI(model="gpt-5-mini")
|
||||
agent = FunctionCallingAgent.from_tools(
|
||||
tools=[],
|
||||
llm=llm,
|
||||
@@ -401,14 +352,14 @@ USER_ID = "alice"
|
||||
|
||||
agent = ConversableAgent(
|
||||
"chatbot",
|
||||
llm_config={"config_list": [{"model": "gpt-4", "api_key": os.environ["OPENAI_API_KEY"]}]},
|
||||
llm_config={"config_list": [{"model": "gpt-5-mini", "api_key": os.environ["OPENAI_API_KEY"]}]},
|
||||
code_execution_config=False,
|
||||
human_input_mode="NEVER",
|
||||
)
|
||||
|
||||
def get_context_aware_response(question: str) -> str:
|
||||
# Retrieve memories for context
|
||||
relevant_memories = memory_client.search(question, user_id=USER_ID)
|
||||
relevant_memories = memory_client.search(question, filters={"user_id": USER_ID})
|
||||
context = "\n".join([m["memory"] for m in relevant_memories.get("results", [])])
|
||||
|
||||
prompt = f"""Answer considering previous interactions:
|
||||
|
||||
@@ -27,7 +27,7 @@ messages = [
|
||||
client.add(messages, user_id="user123")
|
||||
|
||||
# Search memories
|
||||
results = client.search("What are my dietary restrictions?", user_id="user123")
|
||||
results = client.search("What are my dietary restrictions?", filters={"user_id": "user123"})
|
||||
print(results)
|
||||
```
|
||||
|
||||
@@ -39,7 +39,7 @@ from mem0 import AsyncMemoryClient
|
||||
client = AsyncMemoryClient(api_key="your-api-key")
|
||||
|
||||
await client.add(messages, user_id="user123")
|
||||
results = await client.search("query", user_id="user123")
|
||||
results = await client.search("query", filters={"user_id": "user123"})
|
||||
```
|
||||
|
||||
## TypeScript / JavaScript Setup
|
||||
@@ -59,11 +59,11 @@ const messages = [
|
||||
{"role": "user", "content": "I'm a vegetarian and allergic to nuts."},
|
||||
{"role": "assistant", "content": "Got it! I'll remember your dietary preferences."}
|
||||
];
|
||||
await client.add(messages, { user_id: "user123" });
|
||||
await client.add(messages, { userId: "user123" });
|
||||
|
||||
// Search memories
|
||||
const results = await client.search("What are my dietary restrictions?", {
|
||||
user_id: "user123"
|
||||
filters: { user_id: "user123" }
|
||||
});
|
||||
console.log(results);
|
||||
```
|
||||
@@ -86,7 +86,7 @@ curl -X POST https://api.mem0.ai/v1/memories/ \
|
||||
}'
|
||||
|
||||
# Search memories
|
||||
curl -X POST https://api.mem0.ai/v2/memories/search/ \
|
||||
curl -X POST https://api.mem0.ai/v3/memories/search/ \
|
||||
-H "Authorization: Token $MEM0_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
Complete SDK reference for Python and TypeScript. All methods use `MemoryClient` (Platform API).
|
||||
|
||||
> **For language-specific deep references (including OSS):** See [client/python.md](../client/python.md) and [client/node.md](../client/node.md). For Python vs TypeScript differences: [client/differences.md](../client/differences.md).
|
||||
|
||||
## Initialization
|
||||
|
||||
**Python:**
|
||||
@@ -38,16 +40,12 @@ client.add(messages, user_id="alice")
|
||||
|
||||
# With metadata
|
||||
client.add(messages, user_id="alice", metadata={"source": "onboarding"})
|
||||
|
||||
# With graph memory
|
||||
client.add(messages, user_id="alice", enable_graph=True)
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
await client.add(messages, { user_id: "alice" });
|
||||
await client.add(messages, { user_id: "alice", metadata: { source: "onboarding" } });
|
||||
await client.add(messages, { user_id: "alice", enable_graph: true });
|
||||
await client.add(messages, { userId: "alice" });
|
||||
await client.add(messages, { userId: "alice", metadata: { source: "onboarding" } });
|
||||
```
|
||||
|
||||
### Parameters
|
||||
@@ -59,32 +57,14 @@ await client.add(messages, { user_id: "alice", enable_graph: true });
|
||||
| `agent_id` | string | Agent identifier |
|
||||
| `run_id` | string | Session identifier |
|
||||
| `metadata` | object | Custom key-value pairs |
|
||||
| `enable_graph` | boolean | Activate knowledge graph |
|
||||
| `infer` | boolean | If `false`, store raw text without inference (default: `true`) |
|
||||
| `immutable` | boolean | Prevents modification after creation |
|
||||
| `expiration_date` | string | Auto-expiry date (`YYYY-MM-DD`) |
|
||||
| `includes` | string | Preference filters for inclusion |
|
||||
| `excludes` | string | Preference filters for exclusion |
|
||||
| `async_mode` | boolean | Async processing (default: `true`). Set `false` to wait |
|
||||
|
||||
### Advanced Add Options
|
||||
|
||||
```python
|
||||
# Immutable -- cannot be modified or overwritten
|
||||
client.add(messages, user_id="alice", immutable=True)
|
||||
|
||||
# Expiring memory
|
||||
client.add(messages, user_id="alice", expiration_date="2025-12-31")
|
||||
|
||||
# Selective extraction
|
||||
client.add(messages, user_id="alice", includes="dietary preferences", excludes="payment info")
|
||||
|
||||
# Agent + session scoping
|
||||
client.add(messages, user_id="alice", agent_id="nutrition-agent", run_id="session-456")
|
||||
|
||||
# Synchronous processing (wait for completion)
|
||||
client.add(messages, user_id="alice", async_mode=False)
|
||||
|
||||
# Raw text -- skip LLM inference
|
||||
client.add(
|
||||
[{"role": "user", "content": "User prefers dark mode."}],
|
||||
@@ -99,7 +79,7 @@ client.add(
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
results = client.search("dietary preferences?", user_id="alice")
|
||||
results = client.search("dietary preferences?", filters={"user_id": "alice"})
|
||||
|
||||
# With filters and reranking
|
||||
results = client.search(
|
||||
@@ -109,20 +89,14 @@ results = client.search(
|
||||
rerank=True,
|
||||
threshold=0.5
|
||||
)
|
||||
|
||||
# With graph relations
|
||||
results = client.search("colleagues", user_id="alice", enable_graph=True)
|
||||
|
||||
# Keyword search
|
||||
results = client.search("vegetarian", user_id="alice", keyword_search=True)
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
const results = await client.search("dietary preferences", { user_id: "alice" });
|
||||
const results = await client.search("dietary preferences", { filters: { user_id: "alice" } });
|
||||
const results = await client.search("work experience", {
|
||||
filters: { AND: [{ user_id: "alice" }, { categories: { contains: "professional_details" } }] },
|
||||
top_k: 5,
|
||||
topK: 5,
|
||||
rerank: true,
|
||||
});
|
||||
```
|
||||
@@ -132,19 +106,17 @@ const results = await client.search("work experience", {
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `query` | string | Natural language search query |
|
||||
| `user_id` | string | Filter by user |
|
||||
| `filters` | object | V2 filter object (AND/OR operators) |
|
||||
| `top_k` | number | Number of results (default: 10) |
|
||||
| `rerank` | boolean | Enable reranking for better relevance |
|
||||
| `threshold` | number | Minimum similarity score (default: 0.3) |
|
||||
| `keyword_search` | boolean | Use keyword-based search |
|
||||
| `enable_graph` | boolean | Include graph relations |
|
||||
| `filters` | object | Filter object (AND/OR operators). Use `{"user_id": "..."}` to filter by user |
|
||||
| `top_k` | number | Number of results (default: 10 for Platform) |
|
||||
| `rerank` | boolean | Enable reranking for better relevance (default: `false`) |
|
||||
| `threshold` | number | Minimum similarity score (default: 0.1) |
|
||||
|
||||
### Common Filter Patterns
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
# Single user (shorthand)
|
||||
client.search("query", user_id="alice")
|
||||
# Single user filter
|
||||
filters={"user_id": "alice"}
|
||||
|
||||
# OR across agents
|
||||
filters={"OR": [{"user_id": "alice"}, {"agent_id": {"in": ["travel-agent", "sports-agent"]}}]}
|
||||
@@ -177,6 +149,21 @@ filters={"AND": [
|
||||
]}
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
// Single user filter
|
||||
filters: { user_id: "alice" }
|
||||
|
||||
// OR across agents
|
||||
filters: { OR: [{ user_id: "alice" }, { agent_id: { in: ["travel-agent", "sports-agent"] } }] }
|
||||
|
||||
// Category filtering (partial match)
|
||||
filters: { AND: [{ user_id: "alice" }, { categories: { contains: "finance" } }] }
|
||||
|
||||
// Category filtering (exact match)
|
||||
filters: { AND: [{ user_id: "alice" }, { categories: { in: ["personal_information"] } }] }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## get() / getAll() -- Retrieve Memories
|
||||
@@ -187,7 +174,7 @@ filters={"AND": [
|
||||
memory = client.get(memory_id="ea925981-...")
|
||||
|
||||
# All memories for a user
|
||||
memories = client.get_all(filters={"AND": [{"user_id": "alice"}]})
|
||||
memories = client.get_all(filters={"user_id": "alice"})
|
||||
|
||||
# With date range
|
||||
memories = client.get_all(
|
||||
@@ -196,15 +183,12 @@ memories = client.get_all(
|
||||
{"created_at": {"gte": "2024-07-01", "lte": "2024-07-31"}}
|
||||
]}
|
||||
)
|
||||
|
||||
# With graph data
|
||||
memories = client.get_all(filters={"AND": [{"user_id": "alice"}]}, enable_graph=True)
|
||||
```
|
||||
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
const memory = await client.get("ea925981-...");
|
||||
const memories = await client.getAll({ filters: { AND: [{ user_id: "alice" }] } });
|
||||
const memories = await client.getAll({ filters: { user_id: "alice" } });
|
||||
```
|
||||
|
||||
**Note:** `get_all` requires at least one of `user_id`, `agent_id`, `app_id`, or `run_id` in filters.
|
||||
@@ -224,8 +208,6 @@ client.update(memory_id="ea925981-...", text="Updated", metadata={"verified": Tr
|
||||
await client.update("ea925981-...", { text: "Updated: vegan since 2024" });
|
||||
```
|
||||
|
||||
Cannot update immutable memories.
|
||||
|
||||
---
|
||||
|
||||
## delete() / deleteAll() -- Remove Memories
|
||||
@@ -239,7 +221,7 @@ client.delete_all(user_id="alice") # Irreversible bulk delete
|
||||
**TypeScript:**
|
||||
```typescript
|
||||
await client.delete("ea925981-...");
|
||||
await client.deleteAll({ user_id: "alice" });
|
||||
await client.deleteAll({ userId: "alice" });
|
||||
```
|
||||
|
||||
---
|
||||
@@ -299,10 +281,73 @@ data = client.get_memory_export(memory_export_id=export["id"])
|
||||
2. **SQL operators rejected** -- use `gte`, `lt`, etc. Not `>=`, `<`.
|
||||
3. **Metadata filtering is limited** -- only top-level keys with `eq`, `contains`, `ne`.
|
||||
4. **Wildcard `*` excludes null** -- only matches non-null values.
|
||||
5. **Default threshold is 0.3** -- increase for stricter matching.
|
||||
5. **Default threshold is 0.1** -- increase for stricter matching.
|
||||
6. **Async processing** -- memories process asynchronously. Wait 2-3s after `add()` before searching.
|
||||
7. **Immutable memories** -- cannot be updated or deleted once created.
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
Python uses `snake_case` (`user_id`, `memory_id`, `get_all`). TypeScript uses `camelCase` for methods (`getAll`, `deleteAll`, `batchUpdate`) but `snake_case` for API parameters (`user_id`, `agent_id`).
|
||||
Python uses `snake_case` everywhere (`user_id`, `memory_id`, `get_all`). TypeScript uses `camelCase` for methods (`getAll`, `deleteAll`, `batchUpdate`) and top-level parameters (`userId`, `topK`, `pageSize`), but filter keys use `snake_case` (`user_id`, `agent_id`).
|
||||
|
||||
---
|
||||
|
||||
## v2 to v3 Migration
|
||||
|
||||
### Breaking Changes in v3
|
||||
|
||||
**1. Entity IDs in search() and getAll()**
|
||||
|
||||
v3 requires entity IDs (`user_id`, `agent_id`, `run_id`) inside `filters` instead of as top-level parameters:
|
||||
|
||||
```python
|
||||
# v2 (deprecated)
|
||||
client.search("query", user_id="alice")
|
||||
client.get_all(user_id="alice")
|
||||
|
||||
# v3
|
||||
client.search("query", filters={"user_id": "alice"})
|
||||
client.get_all(filters={"user_id": "alice"})
|
||||
```
|
||||
|
||||
```typescript
|
||||
// v2 (deprecated)
|
||||
await client.search("query", { user_id: "alice" });
|
||||
await client.getAll({ user_id: "alice" });
|
||||
|
||||
// v3
|
||||
await client.search("query", { filters: { user_id: "alice" } });
|
||||
await client.getAll({ filters: { user_id: "alice" } });
|
||||
```
|
||||
|
||||
**2. TypeScript Parameter Naming**
|
||||
|
||||
v3 TypeScript uses camelCase for all parameters:
|
||||
|
||||
| v2 | v3 |
|
||||
|----|-----|
|
||||
| `user_id` | `userId` |
|
||||
| `agent_id` | `agentId` |
|
||||
| `run_id` | `runId` |
|
||||
| `top_k` | `topK` |
|
||||
| `page_size` | `pageSize` |
|
||||
|
||||
**3. Default Values Changed**
|
||||
|
||||
| Parameter | v2 Default | v3 Default |
|
||||
|-----------|------------|------------|
|
||||
| `threshold` | 0.3 | 0.1 |
|
||||
| `rerank` | (not specified) | `false` |
|
||||
|
||||
**4. Removed Parameters**
|
||||
|
||||
The following parameters are no longer supported:
|
||||
|
||||
| Parameter | Status |
|
||||
|-----------|--------|
|
||||
| `enable_graph` | Removed from add/search/getAll |
|
||||
| `keyword_search` | Removed from search |
|
||||
| `filter_memories` | Removed |
|
||||
| `immutable` | Removed from add |
|
||||
| `expiration_date` | Removed from add |
|
||||
| `includes` | Removed from add |
|
||||
| `excludes` | Removed from add |
|
||||
| `async_mode` | Removed from add |
|
||||
|
||||
@@ -39,7 +39,7 @@ Use these known facts about the user to personalize your response:
|
||||
{context if context else 'No prior context yet.'}"""
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4.1-nano-2025-04-14",
|
||||
model="gpt-5-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_input},
|
||||
@@ -72,14 +72,14 @@ const openai = new OpenAI();
|
||||
|
||||
async function chat(userInput: string, userId: string): Promise<string> {
|
||||
// 1. Retrieve relevant memories
|
||||
const memories = await mem0.search(userInput, { user_id: userId });
|
||||
const memories = await mem0.search(userInput, { filters: { user_id: userId } });
|
||||
const context = memories.results
|
||||
?.map((m: any) => `- ${m.memory}`)
|
||||
.join('\n') || 'No prior context yet.';
|
||||
|
||||
// 2. Generate response with memory context
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-4.1-nano-2025-04-14',
|
||||
model: 'gpt-5-mini',
|
||||
messages: [
|
||||
{ role: 'system', content: `You are Ray, a personal fitness coach.\nUser context:\n${context}` },
|
||||
{ role: 'user', content: userInput },
|
||||
@@ -90,7 +90,7 @@ async function chat(userInput: string, userId: string): Promise<string> {
|
||||
// 3. Store interaction
|
||||
await mem0.add(
|
||||
[{ role: 'user', content: userInput }, { role: 'assistant', content: reply }],
|
||||
{ user_id: userId }
|
||||
{ userId: userId }
|
||||
);
|
||||
return reply;
|
||||
}
|
||||
@@ -182,7 +182,7 @@ await client.updateProject({
|
||||
async function logInteraction(userId: string, message: string, priority = 'normal') {
|
||||
await client.add(
|
||||
[{ role: 'user', content: message }],
|
||||
{ user_id: userId, metadata: { priority, source: 'support_chat' } }
|
||||
{ userId: userId, metadata: { priority, source: 'support_chat' } }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ def consult(user_id: str, question: str) -> str:
|
||||
context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4.1-nano-2025-04-14",
|
||||
model="gpt-5-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": f"You are a health coach. Patient context:\n{context}"},
|
||||
{"role": "user", "content": question},
|
||||
@@ -264,20 +264,20 @@ const openai = new OpenAI();
|
||||
async function savePatientInfo(userId: string, info: string) {
|
||||
await mem0.add(
|
||||
[{ role: 'user', content: info }],
|
||||
{ user_id: userId, run_id: 'healthcare_session', metadata: { type: 'patient_information' } }
|
||||
{ userId: userId, runId: 'healthcare_session', metadata: { type: 'patient_information' } }
|
||||
);
|
||||
}
|
||||
|
||||
async function consult(userId: string, question: string): Promise<string> {
|
||||
const memories = await mem0.search(question, {
|
||||
user_id: userId,
|
||||
top_k: 5,
|
||||
filters: { user_id: userId },
|
||||
topK: 5,
|
||||
threshold: 0.7,
|
||||
});
|
||||
const context = memories.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-4.1-nano-2025-04-14',
|
||||
model: 'gpt-5-mini',
|
||||
messages: [
|
||||
{ role: 'system', content: `You are a health coach. Patient context:\n${context}` },
|
||||
{ role: 'user', content: question },
|
||||
@@ -287,7 +287,7 @@ async function consult(userId: string, question: string): Promise<string> {
|
||||
|
||||
await mem0.add(
|
||||
[{ role: 'user', content: question }, { role: 'assistant', content: reply }],
|
||||
{ user_id: userId, run_id: 'healthcare_session' }
|
||||
{ userId: userId, runId: 'healthcare_session' }
|
||||
);
|
||||
return reply;
|
||||
}
|
||||
@@ -333,7 +333,7 @@ def draft_content(user_id: str, topic: str) -> str:
|
||||
style_context = "\n".join([f"- {m['memory']}" for m in prefs.get("results", [])])
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4.1-nano-2025-04-14",
|
||||
model="gpt-5-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": f"Write content matching these style preferences:\n{style_context}"},
|
||||
{"role": "user", "content": f"Write a blog post about: {topic}"},
|
||||
@@ -359,7 +359,7 @@ const openai = new OpenAI();
|
||||
async function storePreferences(userId: string, preferences: string) {
|
||||
await mem0.add(
|
||||
[{ role: 'user', content: preferences }],
|
||||
{ user_id: userId, run_id: 'editing_session', metadata: { type: 'preferences' } }
|
||||
{ userId: userId, runId: 'editing_session', metadata: { type: 'preferences' } }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ async function draftContent(userId: string, topic: string): Promise<string> {
|
||||
const styleContext = prefs.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-4.1-nano-2025-04-14',
|
||||
model: 'gpt-5-mini',
|
||||
messages: [
|
||||
{ role: 'system', content: `Write content matching these preferences:\n${styleContext}` },
|
||||
{ role: 'user', content: `Write a blog post about: ${topic}` },
|
||||
@@ -465,10 +465,10 @@ async function storeScopedMemory(
|
||||
userId: string, agentId: string, runId: string, appId: string
|
||||
) {
|
||||
await client.add(messages, {
|
||||
user_id: userId,
|
||||
agent_id: agentId,
|
||||
run_id: runId,
|
||||
app_id: appId,
|
||||
userId: userId,
|
||||
agentId: agentId,
|
||||
runId: runId,
|
||||
appId: appId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -520,7 +520,7 @@ def personalized_search(user_id: str, query: str, search_results: list) -> str:
|
||||
user_context = "\n".join([f"- {m['memory']}" for m in memories.get("results", [])])
|
||||
|
||||
response = openai_client.chat.completions.create(
|
||||
model="gpt-4.1-nano-2025-04-14",
|
||||
model="gpt-5-mini",
|
||||
messages=[
|
||||
{"role": "system", "content": f"Personalize search results using user context:\n{user_context}"},
|
||||
{"role": "user", "content": f"Query: {query}\n\nSearch results:\n{search_results}"},
|
||||
@@ -551,11 +551,11 @@ const mem0 = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
|
||||
const openai = new OpenAI();
|
||||
|
||||
async function personalizedSearch(userId: string, query: string, searchResults: string[]): Promise<string> {
|
||||
const memories = await mem0.search(query, { user_id: userId, top_k: 5 });
|
||||
const memories = await mem0.search(query, { filters: { user_id: userId }, topK: 5 });
|
||||
const context = memories.results?.map((m: any) => `- ${m.memory}`).join('\n') || '';
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gpt-4.1-nano-2025-04-14',
|
||||
model: 'gpt-5-mini',
|
||||
messages: [
|
||||
{ role: 'system', content: `Personalize results using user context:\n${context}` },
|
||||
{ role: 'user', content: `Query: ${query}\nResults: ${searchResults.join(', ')}` },
|
||||
@@ -563,7 +563,7 @@ async function personalizedSearch(userId: string, query: string, searchResults:
|
||||
});
|
||||
const reply = response.choices[0].message.content!;
|
||||
|
||||
await mem0.add([{ role: 'user', content: query }], { user_id: userId });
|
||||
await mem0.add([{ role: 'user', content: query }], { userId: userId });
|
||||
return reply;
|
||||
}
|
||||
```
|
||||
@@ -631,14 +631,14 @@ const client = new MemoryClient({ apiKey: process.env.MEM0_API_KEY! });
|
||||
async function storeEmail(userId: string, sender: string, subject: string, body: string, date: string) {
|
||||
await client.add(
|
||||
[{ role: 'user', content: `Email from ${sender}: ${subject}\n\n${body}` }],
|
||||
{ user_id: userId, metadata: { email_type: 'incoming', sender, subject, date } }
|
||||
{ userId: userId, metadata: { email_type: 'incoming', sender, subject, date } }
|
||||
);
|
||||
}
|
||||
|
||||
async function searchEmails(userId: string, query: string) {
|
||||
return client.search(query, {
|
||||
filters: { AND: [{ user_id: userId }, { categories: { contains: 'email' } }] },
|
||||
top_k: 10,
|
||||
topK: 10,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0ai",
|
||||
"version": "3.0.1",
|
||||
"version": "3.0.2",
|
||||
"description": "The Memory Layer For Your AI Apps",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
import type { Client as ClientType } from "pg";
|
||||
import pkg from "pg";
|
||||
const { Client } = pkg;
|
||||
const { Client, escapeIdentifier } = pkg;
|
||||
import { VectorStore } from "./base";
|
||||
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
||||
|
||||
const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,127}$/;
|
||||
|
||||
function validateIdentifier(
|
||||
name: string,
|
||||
label: string = "identifier",
|
||||
): string {
|
||||
if (!SAFE_IDENTIFIER_RE.test(name)) {
|
||||
throw new Error(
|
||||
`Invalid ${label} '${name}': only letters, digits, and underscores are allowed, ` +
|
||||
`must start with a letter or underscore, and be at most 128 characters.`,
|
||||
);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
function escapeFilterKey(key: string): string {
|
||||
if (!SAFE_IDENTIFIER_RE.test(key)) {
|
||||
throw new Error(
|
||||
`Invalid filter key '${key}': only letters, digits, and underscores are allowed.`,
|
||||
);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
interface PGVectorConfig extends VectorStoreConfig {
|
||||
dbname?: string;
|
||||
user: string;
|
||||
@@ -25,10 +49,13 @@ export class PGVector implements VectorStore {
|
||||
private _initPromise?: Promise<void>;
|
||||
|
||||
constructor(config: PGVectorConfig) {
|
||||
this.collectionName = config.collectionName || "memories";
|
||||
this.collectionName = validateIdentifier(
|
||||
config.collectionName || "memories",
|
||||
"collectionName",
|
||||
);
|
||||
this.useDiskann = config.diskann || false;
|
||||
this.useHnsw = config.hnsw || false;
|
||||
this.dbName = config.dbname || "vector_store";
|
||||
this.dbName = validateIdentifier(config.dbname || "vector_store", "dbname");
|
||||
this.config = config;
|
||||
|
||||
this.client = new Client({
|
||||
@@ -41,6 +68,10 @@ export class PGVector implements VectorStore {
|
||||
this.initialize().catch(console.error);
|
||||
}
|
||||
|
||||
private col(): string {
|
||||
return escapeIdentifier(this.collectionName);
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
if (!this._initPromise) {
|
||||
this._initPromise = this._doInitialize();
|
||||
@@ -102,31 +133,28 @@ export class PGVector implements VectorStore {
|
||||
}
|
||||
|
||||
private async createDatabase(dbName: string): Promise<void> {
|
||||
// Create database (cannot be parameterized)
|
||||
await this.client.query(`CREATE DATABASE ${dbName}`);
|
||||
await this.client.query(`CREATE DATABASE ${escapeIdentifier(dbName)}`);
|
||||
}
|
||||
|
||||
private async createCol(embeddingModelDims: number): Promise<void> {
|
||||
// Create the table
|
||||
const dims = Math.floor(embeddingModelDims);
|
||||
await this.client.query(`
|
||||
CREATE TABLE IF NOT EXISTS ${this.collectionName} (
|
||||
CREATE TABLE IF NOT EXISTS ${this.col()} (
|
||||
id UUID PRIMARY KEY,
|
||||
vector vector(${embeddingModelDims}),
|
||||
vector vector(${dims}),
|
||||
payload JSONB
|
||||
);
|
||||
`);
|
||||
|
||||
// Create indexes based on configuration
|
||||
if (this.useDiskann && embeddingModelDims < 2000) {
|
||||
try {
|
||||
// Check if vectorscale extension is available
|
||||
const result = await this.client.query(
|
||||
"SELECT * FROM pg_extension WHERE extname = 'vectorscale'",
|
||||
);
|
||||
if (result.rows.length > 0) {
|
||||
await this.client.query(`
|
||||
CREATE INDEX IF NOT EXISTS ${this.collectionName}_diskann_idx
|
||||
ON ${this.collectionName}
|
||||
CREATE INDEX IF NOT EXISTS ${escapeIdentifier(this.collectionName + "_diskann_idx")}
|
||||
ON ${this.col()}
|
||||
USING diskann (vector);
|
||||
`);
|
||||
}
|
||||
@@ -136,8 +164,8 @@ export class PGVector implements VectorStore {
|
||||
} else if (this.useHnsw) {
|
||||
try {
|
||||
await this.client.query(`
|
||||
CREATE INDEX IF NOT EXISTS ${this.collectionName}_hnsw_idx
|
||||
ON ${this.collectionName}
|
||||
CREATE INDEX IF NOT EXISTS ${escapeIdentifier(this.collectionName + "_hnsw_idx")}
|
||||
ON ${this.col()}
|
||||
USING hnsw (vector vector_cosine_ops);
|
||||
`);
|
||||
} catch (error) {
|
||||
@@ -153,16 +181,15 @@ export class PGVector implements VectorStore {
|
||||
): Promise<void> {
|
||||
const values = vectors.map((vector, i) => ({
|
||||
id: ids[i],
|
||||
vector: `[${vector.join(",")}]`, // Format vector as string with square brackets
|
||||
vector: `[${vector.join(",")}]`,
|
||||
payload: payloads[i],
|
||||
}));
|
||||
|
||||
const query = `
|
||||
INSERT INTO ${this.collectionName} (id, vector, payload)
|
||||
INSERT INTO ${this.col()} (id, vector, payload)
|
||||
VALUES ($1, $2::vector, $3::jsonb)
|
||||
`;
|
||||
|
||||
// Execute inserts in parallel using Promise.all
|
||||
await Promise.all(
|
||||
values.map((value) =>
|
||||
this.client.query(query, [value.id, value.vector, value.payload]),
|
||||
@@ -182,7 +209,8 @@ export class PGVector implements VectorStore {
|
||||
|
||||
if (filters) {
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
filterConditions.push(`payload->>'${key}' = $${filterIndex}`);
|
||||
const safeKey = escapeFilterKey(key);
|
||||
filterConditions.push(`payload->>'${safeKey}' = $${filterIndex}`);
|
||||
filterValues.push(value);
|
||||
filterIndex++;
|
||||
}
|
||||
@@ -195,7 +223,7 @@ export class PGVector implements VectorStore {
|
||||
|
||||
const searchQuery = `
|
||||
SELECT id, ts_rank_cd(to_tsvector('simple', payload->>'textLemmatized'), plainto_tsquery('simple', $1)) AS score, payload
|
||||
FROM ${this.collectionName}
|
||||
FROM ${this.col()}
|
||||
WHERE to_tsvector('simple', payload->>'textLemmatized') @@ plainto_tsquery('simple', $1)
|
||||
${filterClause}
|
||||
ORDER BY score DESC
|
||||
@@ -221,13 +249,14 @@ export class PGVector implements VectorStore {
|
||||
filters?: SearchFilters,
|
||||
): Promise<VectorStoreResult[]> {
|
||||
const filterConditions: string[] = [];
|
||||
const queryVector = `[${query.join(",")}]`; // Format query vector as string with square brackets
|
||||
const queryVector = `[${query.join(",")}]`;
|
||||
const filterValues: any[] = [queryVector, topK];
|
||||
let filterIndex = 3;
|
||||
|
||||
if (filters) {
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
filterConditions.push(`payload->>'${key}' = $${filterIndex}`);
|
||||
const safeKey = escapeFilterKey(key);
|
||||
filterConditions.push(`payload->>'${safeKey}' = $${filterIndex}`);
|
||||
filterValues.push(value);
|
||||
filterIndex++;
|
||||
}
|
||||
@@ -240,7 +269,7 @@ export class PGVector implements VectorStore {
|
||||
|
||||
const searchQuery = `
|
||||
SELECT id, vector <=> $1::vector AS distance, payload
|
||||
FROM ${this.collectionName}
|
||||
FROM ${this.col()}
|
||||
${filterClause}
|
||||
ORDER BY distance
|
||||
LIMIT $2
|
||||
@@ -251,13 +280,13 @@ export class PGVector implements VectorStore {
|
||||
return result.rows.map((row) => ({
|
||||
id: row.id,
|
||||
payload: row.payload,
|
||||
score: row.distance,
|
||||
score: Math.max(0, Math.min(1, 1 - Number(row.distance))),
|
||||
}));
|
||||
}
|
||||
|
||||
async get(vectorId: string): Promise<VectorStoreResult | null> {
|
||||
const result = await this.client.query(
|
||||
`SELECT id, payload FROM ${this.collectionName} WHERE id = $1`,
|
||||
`SELECT id, payload FROM ${this.col()} WHERE id = $1`,
|
||||
[vectorId],
|
||||
);
|
||||
|
||||
@@ -274,10 +303,10 @@ export class PGVector implements VectorStore {
|
||||
vector: number[],
|
||||
payload: Record<string, any>,
|
||||
): Promise<void> {
|
||||
const vectorStr = `[${vector.join(",")}]`; // Format vector as string with square brackets
|
||||
const vectorStr = `[${vector.join(",")}]`;
|
||||
await this.client.query(
|
||||
`
|
||||
UPDATE ${this.collectionName}
|
||||
UPDATE ${this.col()}
|
||||
SET vector = $1::vector, payload = $2::jsonb
|
||||
WHERE id = $3
|
||||
`,
|
||||
@@ -286,14 +315,13 @@ export class PGVector implements VectorStore {
|
||||
}
|
||||
|
||||
async delete(vectorId: string): Promise<void> {
|
||||
await this.client.query(
|
||||
`DELETE FROM ${this.collectionName} WHERE id = $1`,
|
||||
[vectorId],
|
||||
);
|
||||
await this.client.query(`DELETE FROM ${this.col()} WHERE id = $1`, [
|
||||
vectorId,
|
||||
]);
|
||||
}
|
||||
|
||||
async deleteCol(): Promise<void> {
|
||||
await this.client.query(`DROP TABLE IF EXISTS ${this.collectionName}`);
|
||||
await this.client.query(`DROP TABLE IF EXISTS ${this.col()}`);
|
||||
}
|
||||
|
||||
private async listCols(): Promise<string[]> {
|
||||
@@ -315,7 +343,8 @@ export class PGVector implements VectorStore {
|
||||
|
||||
if (filters) {
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
filterConditions.push(`payload->>'${key}' = $${paramIndex}`);
|
||||
const safeKey = escapeFilterKey(key);
|
||||
filterConditions.push(`payload->>'${safeKey}' = $${paramIndex}`);
|
||||
filterValues.push(value);
|
||||
paramIndex++;
|
||||
}
|
||||
@@ -328,14 +357,14 @@ export class PGVector implements VectorStore {
|
||||
|
||||
const listQuery = `
|
||||
SELECT id, payload
|
||||
FROM ${this.collectionName}
|
||||
FROM ${this.col()}
|
||||
${filterClause}
|
||||
LIMIT $${paramIndex}
|
||||
`;
|
||||
|
||||
const countQuery = `
|
||||
SELECT COUNT(*)
|
||||
FROM ${this.collectionName}
|
||||
FROM ${this.col()}
|
||||
${filterClause}
|
||||
`;
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
/// <reference types="jest" />
|
||||
|
||||
const searchRows = [
|
||||
{
|
||||
id: "a",
|
||||
payload: { data: "exactly x-axis" },
|
||||
distance: "0",
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
payload: { data: "close to x-axis" },
|
||||
distance: "0.006116251198662548",
|
||||
},
|
||||
{
|
||||
id: "c",
|
||||
payload: { data: "y-axis" },
|
||||
distance: "1",
|
||||
},
|
||||
{
|
||||
id: "d",
|
||||
payload: { data: "opposite x-axis" },
|
||||
distance: "2",
|
||||
},
|
||||
];
|
||||
|
||||
function mockPgQuery(sql: string) {
|
||||
if (sql.includes("SELECT 1 FROM pg_database")) {
|
||||
return { rows: [{ "?column?": 1 }] };
|
||||
}
|
||||
|
||||
if (sql.includes("FROM information_schema.tables")) {
|
||||
return { rows: [{ table_name: "memories" }] };
|
||||
}
|
||||
|
||||
if (sql.includes("vector <=> $1::vector AS distance")) {
|
||||
return { rows: searchRows };
|
||||
}
|
||||
|
||||
return { rows: [] };
|
||||
}
|
||||
|
||||
jest.mock("pg", () => {
|
||||
const clients: any[] = [];
|
||||
|
||||
const Client = jest.fn().mockImplementation((config: any) => {
|
||||
const client = {
|
||||
config,
|
||||
connect: jest.fn().mockResolvedValue(undefined),
|
||||
end: jest.fn().mockResolvedValue(undefined),
|
||||
query: jest
|
||||
.fn()
|
||||
.mockImplementation(async (sql: string) => mockPgQuery(sql)),
|
||||
};
|
||||
|
||||
clients.push(client);
|
||||
return client;
|
||||
});
|
||||
|
||||
const escapeIdentifier = (str: string) => `"${str.replace(/"/g, '""')}"`;
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
default: { Client, escapeIdentifier },
|
||||
Client,
|
||||
escapeIdentifier,
|
||||
__mock: { Client, clients },
|
||||
};
|
||||
});
|
||||
|
||||
import { PGVector } from "../src/vector_stores/pgvector";
|
||||
|
||||
describe("PGVector - search()", () => {
|
||||
beforeEach(() => {
|
||||
const pg = require("pg");
|
||||
pg.__mock.Client.mockClear();
|
||||
pg.__mock.clients.length = 0;
|
||||
});
|
||||
|
||||
test("returns similarity score (1 - distance) clamped to [0, 1]", async () => {
|
||||
const store = new PGVector({
|
||||
collectionName: "memories",
|
||||
user: "postgres",
|
||||
password: "postgres",
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
embeddingModelDims: 3,
|
||||
dimension: 3,
|
||||
} as any);
|
||||
|
||||
await store.initialize();
|
||||
|
||||
const results = await store.search([1, 0, 0], 4);
|
||||
|
||||
expect(results).toEqual([
|
||||
{
|
||||
id: "a",
|
||||
payload: { data: "exactly x-axis" },
|
||||
score: 1,
|
||||
},
|
||||
{
|
||||
id: "b",
|
||||
payload: { data: "close to x-axis" },
|
||||
score: 0.9938837488013375,
|
||||
},
|
||||
{
|
||||
id: "c",
|
||||
payload: { data: "y-axis" },
|
||||
score: 0,
|
||||
},
|
||||
{
|
||||
id: "d",
|
||||
payload: { data: "opposite x-axis" },
|
||||
score: 0,
|
||||
},
|
||||
]);
|
||||
|
||||
const pg = require("pg");
|
||||
expect(pg.__mock.Client).toHaveBeenCalledTimes(2);
|
||||
|
||||
const activeClient = pg.__mock.clients[1];
|
||||
expect(activeClient.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining("vector <=> $1::vector AS distance"),
|
||||
["[1,0,0]", 4],
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -14,6 +14,7 @@ class ElasticsearchConfig(BaseModel):
|
||||
api_key: Optional[str] = Field(None, description="API key for authentication")
|
||||
embedding_model_dims: int = Field(1536, description="Dimension of the embedding vector")
|
||||
verify_certs: bool = Field(True, description="Verify SSL certificates")
|
||||
ca_certs: Optional[str] = Field(None, description="Path to CA bundle for SSL certificate verification")
|
||||
use_ssl: bool = Field(True, description="Use SSL for connection")
|
||||
auto_create_index: bool = Field(True, description="Automatically create index during initialization")
|
||||
custom_search_query: Optional[Callable[[List[float], int, Optional[Dict]], Dict]] = Field(
|
||||
|
||||
+7
-5
@@ -656,10 +656,10 @@ class Memory(MemoryBase):
|
||||
else:
|
||||
messages = parse_vision_messages(messages)
|
||||
|
||||
vector_store_result = self._add_to_vector_store(messages, processed_metadata, effective_filters, infer)
|
||||
vector_store_result = self._add_to_vector_store(messages, processed_metadata, effective_filters, infer, prompt=prompt)
|
||||
return {"results": vector_store_result}
|
||||
|
||||
def _add_to_vector_store(self, messages, metadata, filters, infer):
|
||||
def _add_to_vector_store(self, messages, metadata, filters, infer, prompt=None):
|
||||
if not infer:
|
||||
returned_memories = []
|
||||
for message_dict in messages:
|
||||
@@ -726,7 +726,7 @@ class Memory(MemoryBase):
|
||||
if is_agent_scoped:
|
||||
system_prompt += AGENT_CONTEXT_SUFFIX
|
||||
|
||||
custom_instr = self.custom_instructions
|
||||
custom_instr = prompt or self.custom_instructions
|
||||
|
||||
user_prompt = generate_additive_extraction_prompt(
|
||||
existing_memories=existing_memories,
|
||||
@@ -2064,7 +2064,7 @@ class AsyncMemory(MemoryBase):
|
||||
else:
|
||||
messages = parse_vision_messages(messages)
|
||||
|
||||
vector_store_result = await self._add_to_vector_store(messages, processed_metadata, effective_filters, infer)
|
||||
vector_store_result = await self._add_to_vector_store(messages, processed_metadata, effective_filters, infer, prompt=prompt)
|
||||
return {"results": vector_store_result}
|
||||
|
||||
async def _add_to_vector_store(
|
||||
@@ -2073,6 +2073,7 @@ class AsyncMemory(MemoryBase):
|
||||
metadata: dict,
|
||||
effective_filters: dict,
|
||||
infer: bool,
|
||||
prompt: Optional[str] = None,
|
||||
):
|
||||
if not infer:
|
||||
returned_memories = []
|
||||
@@ -2141,7 +2142,7 @@ class AsyncMemory(MemoryBase):
|
||||
if is_agent_scoped:
|
||||
system_prompt += AGENT_CONTEXT_SUFFIX
|
||||
|
||||
custom_instr = self.custom_instructions
|
||||
custom_instr = prompt or self.custom_instructions
|
||||
|
||||
user_prompt = generate_additive_extraction_prompt(
|
||||
existing_memories=existing_memories,
|
||||
@@ -3008,6 +3009,7 @@ class AsyncMemory(MemoryBase):
|
||||
if "created_at" not in new_metadata:
|
||||
new_metadata["created_at"] = datetime.now(timezone.utc).isoformat()
|
||||
new_metadata["updated_at"] = new_metadata["created_at"]
|
||||
new_metadata["text_lemmatized"] = lemmatize_for_bm25(data)
|
||||
|
||||
await asyncio.to_thread(
|
||||
self.vector_store.insert,
|
||||
|
||||
@@ -58,24 +58,35 @@ class LLMReranker(BaseReranker):
|
||||
# Initialize LLM using the factory
|
||||
self.llm = LlmFactory.create(llm_provider, llm_config)
|
||||
|
||||
# Default scoring prompt
|
||||
self.scoring_prompt = getattr(self.config, 'scoring_prompt', None) or self._get_default_prompt()
|
||||
|
||||
def _get_default_prompt(self) -> str:
|
||||
"""Get the default scoring prompt template."""
|
||||
return """You are a relevance scoring assistant. Given a query and a document, you need to score how relevant the document is to the query.
|
||||
# Honor custom scoring_prompt from config if provided
|
||||
custom_prompt = getattr(self.config, 'scoring_prompt', None)
|
||||
if custom_prompt:
|
||||
import warnings
|
||||
warnings.warn(
|
||||
"LLMRerankerConfig.scoring_prompt is deprecated and will be removed in a future version. "
|
||||
"The prompt is now used as the system message.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
self._system_prompt = custom_prompt
|
||||
else:
|
||||
self._system_prompt = self._SYSTEM_PROMPT
|
||||
|
||||
Score the relevance on a scale from 0.0 to 1.0, where:
|
||||
- 1.0 = Perfectly relevant and directly answers the query
|
||||
- 0.8-0.9 = Highly relevant with good information
|
||||
- 0.6-0.7 = Moderately relevant with some useful information
|
||||
- 0.4-0.5 = Slightly relevant with limited useful information
|
||||
- 0.0-0.3 = Not relevant or no useful information
|
||||
_SYSTEM_PROMPT = (
|
||||
"You are a relevance scoring assistant. "
|
||||
"Given a query and a document, score how relevant the document is to the query.\n\n"
|
||||
"Score the relevance on a scale from 0.0 to 1.0, where:\n"
|
||||
"- 1.0 = Perfectly relevant and directly answers the query\n"
|
||||
"- 0.8-0.9 = Highly relevant with good information\n"
|
||||
"- 0.6-0.7 = Moderately relevant with some useful information\n"
|
||||
"- 0.4-0.5 = Slightly relevant with limited useful information\n"
|
||||
"- 0.0-0.3 = Not relevant or no useful information\n\n"
|
||||
"Respond with only a single numerical score between 0.0 and 1.0. "
|
||||
"Do not include any explanation or additional text."
|
||||
)
|
||||
|
||||
Query: "{query}"
|
||||
Document: "{document}"
|
||||
|
||||
Provide only a single numerical score between 0.0 and 1.0. Do not include any explanation or additional text."""
|
||||
# Maximum character length for query and document inputs to prevent prompt flooding.
|
||||
_MAX_INPUT_LEN = 4000
|
||||
|
||||
def _extract_score(self, response_text: str) -> float:
|
||||
"""Extract numerical score from LLM response."""
|
||||
@@ -119,12 +130,17 @@ Provide only a single numerical score between 0.0 and 1.0. Do not include any ex
|
||||
doc_text = str(doc)
|
||||
|
||||
try:
|
||||
# Generate scoring prompt
|
||||
prompt = self.scoring_prompt.format(query=query, document=doc_text)
|
||||
|
||||
# Get LLM response
|
||||
# Truncate inputs to prevent prompt flooding, then send as separate
|
||||
# system/user messages so instructions cannot be overridden by user data.
|
||||
safe_query = query[: self._MAX_INPUT_LEN]
|
||||
safe_doc = doc_text[: self._MAX_INPUT_LEN]
|
||||
user_message = f"Query: {safe_query}\n\nDocument: {safe_doc}"
|
||||
|
||||
response = self.llm.generate_response(
|
||||
messages=[{"role": "user", "content": prompt}]
|
||||
messages=[
|
||||
{"role": "system", "content": self._system_prompt},
|
||||
{"role": "user", "content": user_message},
|
||||
]
|
||||
)
|
||||
|
||||
# Extract score from response
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@@ -25,6 +26,17 @@ from mem0.vector_stores.base import VectorStoreBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SAFE_IDENTIFIER_RE = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]{0,127}$')
|
||||
|
||||
|
||||
def _validate_identifier(name: str, label: str = "identifier") -> str:
|
||||
if not _SAFE_IDENTIFIER_RE.match(name):
|
||||
raise ValueError(
|
||||
f"Invalid {label} '{name}': only letters, digits, and underscores are allowed, "
|
||||
"must start with a letter or underscore, and be at most 128 characters."
|
||||
)
|
||||
return name
|
||||
|
||||
|
||||
class OutputData(BaseModel):
|
||||
id: Optional[str]
|
||||
@@ -72,7 +84,7 @@ class AzureMySQL(VectorStoreBase):
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.database = database
|
||||
self.collection_name = collection_name
|
||||
self.collection_name = _validate_identifier(collection_name, "collection_name")
|
||||
self.embedding_model_dims = embedding_model_dims
|
||||
self.use_azure_credential = use_azure_credential
|
||||
self.ssl_ca = ssl_ca
|
||||
@@ -174,7 +186,7 @@ class AzureMySQL(VectorStoreBase):
|
||||
vector_size (int, optional): Vector dimension (uses self.embedding_model_dims if not provided)
|
||||
distance (str): Distance metric (cosine, euclidean, dot_product)
|
||||
"""
|
||||
table_name = name or self.collection_name
|
||||
table_name = _validate_identifier(name, "table_name") if name else self.collection_name
|
||||
dims = vector_size or self.embedding_model_dims
|
||||
|
||||
with self._get_cursor(commit=True) as cur:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@@ -19,6 +20,17 @@ from mem0.vector_stores.base import VectorStoreBase
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SAFE_IDENTIFIER_RE = re.compile(r'^[a-zA-Z_][a-zA-Z0-9_]{0,127}$')
|
||||
|
||||
|
||||
def _validate_identifier(name: str, label: str = "identifier") -> str:
|
||||
if not _SAFE_IDENTIFIER_RE.match(name):
|
||||
raise ValueError(
|
||||
f"Invalid {label} '{name}': only letters, digits, and underscores are allowed, "
|
||||
"must start with a letter or underscore, and be at most 128 characters."
|
||||
)
|
||||
return name
|
||||
|
||||
|
||||
class OutputData(BaseModel):
|
||||
id: Optional[str]
|
||||
@@ -59,8 +71,8 @@ class CassandraDB(VectorStoreBase):
|
||||
self.port = port
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.keyspace = keyspace
|
||||
self.collection_name = collection_name
|
||||
self.keyspace = _validate_identifier(keyspace, "keyspace")
|
||||
self.collection_name = _validate_identifier(collection_name, "collection_name")
|
||||
self.embedding_model_dims = embedding_model_dims
|
||||
self.secure_connect_bundle = secure_connect_bundle
|
||||
self.protocol_version = protocol_version
|
||||
@@ -156,7 +168,7 @@ class CassandraDB(VectorStoreBase):
|
||||
vector_size (int, optional): Vector dimension (uses self.embedding_model_dims if not provided)
|
||||
distance (str): Distance metric (cosine, euclidean, dot_product)
|
||||
"""
|
||||
table_name = name or self.collection_name
|
||||
table_name = _validate_identifier(name, "table_name") if name else self.collection_name
|
||||
dims = vector_size or self.embedding_model_dims
|
||||
|
||||
try:
|
||||
@@ -375,12 +387,10 @@ class CassandraDB(VectorStoreBase):
|
||||
List[str]: List of collection names
|
||||
"""
|
||||
try:
|
||||
query = f"""
|
||||
SELECT table_name
|
||||
FROM system_schema.tables
|
||||
WHERE keyspace_name = '{self.keyspace}'
|
||||
"""
|
||||
rows = self.session.execute(query)
|
||||
prepared = self.session.prepare(
|
||||
"SELECT table_name FROM system_schema.tables WHERE keyspace_name = ?"
|
||||
)
|
||||
rows = self.session.execute(prepared, (self.keyspace,))
|
||||
return [row.table_name for row in rows]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list collections: {e}")
|
||||
|
||||
@@ -31,6 +31,7 @@ class ElasticsearchDB(VectorStoreBase):
|
||||
cloud_id=config.cloud_id,
|
||||
api_key=config.api_key,
|
||||
verify_certs=config.verify_certs,
|
||||
ca_certs=config.ca_certs,
|
||||
headers= config.headers or {},
|
||||
)
|
||||
else:
|
||||
@@ -38,6 +39,7 @@ class ElasticsearchDB(VectorStoreBase):
|
||||
hosts=[f"{config.host}" if config.port is None else f"{config.host}:{config.port}"],
|
||||
basic_auth=(config.user, config.password) if (config.user and config.password) else None,
|
||||
verify_certs=config.verify_certs,
|
||||
ca_certs=config.ca_certs,
|
||||
headers= config.headers or {},
|
||||
)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from pydantic import BaseModel
|
||||
|
||||
# Try to import psycopg (psycopg3) first, then fall back to psycopg2
|
||||
try:
|
||||
from psycopg import sql
|
||||
from psycopg.types.json import Json
|
||||
from psycopg_pool import ConnectionPool
|
||||
PSYCOPG_VERSION = 3
|
||||
@@ -14,6 +15,7 @@ try:
|
||||
logger.info("Using psycopg (psycopg3) with ConnectionPool for PostgreSQL connections")
|
||||
except ImportError:
|
||||
try:
|
||||
from psycopg2 import sql
|
||||
from psycopg2.extras import Json, execute_values
|
||||
from psycopg2.pool import ThreadedConnectionPool as ConnectionPool
|
||||
PSYCOPG_VERSION = 2
|
||||
@@ -144,6 +146,10 @@ class PGVector(VectorStoreBase):
|
||||
cur.close()
|
||||
self.connection_pool.putconn(conn)
|
||||
|
||||
def _col(self) -> "sql.Identifier":
|
||||
"""Return a safely-quoted SQL identifier for the collection table."""
|
||||
return sql.Identifier(self.collection_name)
|
||||
|
||||
def create_col(self) -> None:
|
||||
"""
|
||||
Create a new collection (table in PostgreSQL).
|
||||
@@ -152,39 +158,45 @@ class PGVector(VectorStoreBase):
|
||||
with self._get_cursor(commit=True) as cur:
|
||||
cur.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
cur.execute(
|
||||
f"""
|
||||
CREATE TABLE IF NOT EXISTS {self.collection_name} (
|
||||
sql.SQL("""
|
||||
CREATE TABLE IF NOT EXISTS {} (
|
||||
id UUID PRIMARY KEY,
|
||||
vector vector({self.embedding_model_dims}),
|
||||
vector vector({}),
|
||||
payload JSONB
|
||||
);
|
||||
"""
|
||||
""").format(self._col(), sql.Literal(self.embedding_model_dims))
|
||||
)
|
||||
if self.use_diskann and self.embedding_model_dims < 2000:
|
||||
cur.execute("SELECT * FROM pg_extension WHERE extname = 'vectorscale'")
|
||||
if cur.fetchone():
|
||||
# Create DiskANN index if extension is installed for faster search
|
||||
cur.execute(
|
||||
f"""
|
||||
CREATE INDEX IF NOT EXISTS {self.collection_name}_diskann_idx
|
||||
ON {self.collection_name}
|
||||
sql.SQL("""
|
||||
CREATE INDEX IF NOT EXISTS {} ON {}
|
||||
USING diskann (vector);
|
||||
"""
|
||||
""").format(
|
||||
sql.Identifier(f"{self.collection_name}_diskann_idx"),
|
||||
self._col(),
|
||||
)
|
||||
)
|
||||
elif self.use_hnsw:
|
||||
cur.execute(
|
||||
f"""
|
||||
CREATE INDEX IF NOT EXISTS {self.collection_name}_hnsw_idx
|
||||
ON {self.collection_name}
|
||||
sql.SQL("""
|
||||
CREATE INDEX IF NOT EXISTS {} ON {}
|
||||
USING hnsw (vector vector_cosine_ops)
|
||||
"""
|
||||
""").format(
|
||||
sql.Identifier(f"{self.collection_name}_hnsw_idx"),
|
||||
self._col(),
|
||||
)
|
||||
)
|
||||
cur.execute(
|
||||
f"""
|
||||
CREATE INDEX IF NOT EXISTS {self.collection_name}_text_lemmatized_idx
|
||||
ON {self.collection_name}
|
||||
sql.SQL("""
|
||||
CREATE INDEX IF NOT EXISTS {} ON {}
|
||||
USING gin(to_tsvector('simple', payload->>'text_lemmatized'));
|
||||
"""
|
||||
""").format(
|
||||
sql.Identifier(f"{self.collection_name}_text_lemmatized_idx"),
|
||||
self._col(),
|
||||
)
|
||||
)
|
||||
|
||||
def insert(self, vectors: list[list[float]], payloads=None, ids=None) -> None:
|
||||
@@ -195,14 +207,14 @@ class PGVector(VectorStoreBase):
|
||||
if PSYCOPG_VERSION == 3:
|
||||
with self._get_cursor(commit=True) as cur:
|
||||
cur.executemany(
|
||||
f"INSERT INTO {self.collection_name} (id, vector, payload) VALUES (%s, %s, %s)",
|
||||
sql.SQL("INSERT INTO {} (id, vector, payload) VALUES (%s, %s, %s)").format(self._col()),
|
||||
data,
|
||||
)
|
||||
else:
|
||||
with self._get_cursor(commit=True) as cur:
|
||||
execute_values(
|
||||
cur,
|
||||
f"INSERT INTO {self.collection_name} (id, vector, payload) VALUES %s",
|
||||
sql.SQL("INSERT INTO {} (id, vector, payload) VALUES %s").format(self._col()),
|
||||
data,
|
||||
)
|
||||
|
||||
@@ -233,17 +245,17 @@ class PGVector(VectorStoreBase):
|
||||
filter_conditions.append("payload->>%s = %s")
|
||||
filter_params.extend([k, str(v)])
|
||||
|
||||
filter_clause = "WHERE " + " AND ".join(filter_conditions) if filter_conditions else ""
|
||||
filter_clause = sql.SQL("WHERE " + " AND ".join(filter_conditions)) if filter_conditions else sql.SQL("")
|
||||
|
||||
with self._get_cursor() as cur:
|
||||
cur.execute(
|
||||
f"""
|
||||
sql.SQL("""
|
||||
SELECT id, vector <=> %s::vector AS distance, payload
|
||||
FROM {self.collection_name}
|
||||
{filter_clause}
|
||||
FROM {}
|
||||
{}
|
||||
ORDER BY distance
|
||||
LIMIT %s
|
||||
""",
|
||||
""").format(self._col(), filter_clause),
|
||||
(vectors, *filter_params, top_k),
|
||||
)
|
||||
|
||||
@@ -270,21 +282,19 @@ class PGVector(VectorStoreBase):
|
||||
filter_conditions.append("payload->>%s = %s")
|
||||
filter_params.extend([k, str(v)])
|
||||
|
||||
filter_clause = ""
|
||||
if filter_conditions:
|
||||
filter_clause = "AND " + " AND ".join(filter_conditions)
|
||||
filter_clause = sql.SQL("AND " + " AND ".join(filter_conditions)) if filter_conditions else sql.SQL("")
|
||||
|
||||
try:
|
||||
with self._get_cursor() as cur:
|
||||
cur.execute(
|
||||
f"""
|
||||
sql.SQL("""
|
||||
SELECT id, ts_rank_cd(to_tsvector('simple', payload->>'text_lemmatized'), plainto_tsquery('simple', %s)) AS score, payload
|
||||
FROM {self.collection_name}
|
||||
FROM {}
|
||||
WHERE to_tsvector('simple', payload->>'text_lemmatized') @@ plainto_tsquery('simple', %s)
|
||||
{filter_clause}
|
||||
{}
|
||||
ORDER BY score DESC
|
||||
LIMIT %s
|
||||
""",
|
||||
""").format(self._col(), filter_clause),
|
||||
(query, query, *filter_params, top_k),
|
||||
)
|
||||
|
||||
@@ -302,7 +312,7 @@ class PGVector(VectorStoreBase):
|
||||
vector_id (str): ID of the vector to delete.
|
||||
"""
|
||||
with self._get_cursor(commit=True) as cur:
|
||||
cur.execute(f"DELETE FROM {self.collection_name} WHERE id = %s", (vector_id,))
|
||||
cur.execute(sql.SQL("DELETE FROM {} WHERE id = %s").format(self._col()), (vector_id,))
|
||||
|
||||
def update(
|
||||
self,
|
||||
@@ -321,7 +331,7 @@ class PGVector(VectorStoreBase):
|
||||
with self._get_cursor(commit=True) as cur:
|
||||
if vector:
|
||||
cur.execute(
|
||||
f"UPDATE {self.collection_name} SET vector = %s WHERE id = %s",
|
||||
sql.SQL("UPDATE {} SET vector = %s WHERE id = %s").format(self._col()),
|
||||
(vector, vector_id),
|
||||
)
|
||||
if payload:
|
||||
@@ -329,13 +339,13 @@ class PGVector(VectorStoreBase):
|
||||
if PSYCOPG_VERSION == 3:
|
||||
# psycopg3 uses psycopg.types.json.Json
|
||||
cur.execute(
|
||||
f"UPDATE {self.collection_name} SET payload = %s WHERE id = %s",
|
||||
sql.SQL("UPDATE {} SET payload = %s WHERE id = %s").format(self._col()),
|
||||
(Json(payload), vector_id),
|
||||
)
|
||||
else:
|
||||
# psycopg2 uses psycopg2.extras.Json
|
||||
cur.execute(
|
||||
f"UPDATE {self.collection_name} SET payload = %s WHERE id = %s",
|
||||
sql.SQL("UPDATE {} SET payload = %s WHERE id = %s").format(self._col()),
|
||||
(Json(payload), vector_id),
|
||||
)
|
||||
|
||||
@@ -352,7 +362,7 @@ class PGVector(VectorStoreBase):
|
||||
"""
|
||||
with self._get_cursor() as cur:
|
||||
cur.execute(
|
||||
f"SELECT id, vector, payload FROM {self.collection_name} WHERE id = %s",
|
||||
sql.SQL("SELECT id, vector, payload FROM {} WHERE id = %s").format(self._col()),
|
||||
(vector_id,),
|
||||
)
|
||||
result = cur.fetchone()
|
||||
@@ -374,7 +384,7 @@ class PGVector(VectorStoreBase):
|
||||
def delete_col(self) -> None:
|
||||
"""Delete a collection."""
|
||||
with self._get_cursor(commit=True) as cur:
|
||||
cur.execute(f"DROP TABLE IF EXISTS {self.collection_name}")
|
||||
cur.execute(sql.SQL("DROP TABLE IF EXISTS {}").format(self._col()))
|
||||
|
||||
def col_info(self) -> dict[str, Any]:
|
||||
"""
|
||||
@@ -385,14 +395,14 @@ class PGVector(VectorStoreBase):
|
||||
"""
|
||||
with self._get_cursor() as cur:
|
||||
cur.execute(
|
||||
f"""
|
||||
sql.SQL("""
|
||||
SELECT
|
||||
table_name,
|
||||
(SELECT COUNT(*) FROM {self.collection_name}) as row_count,
|
||||
(SELECT pg_size_pretty(pg_total_relation_size('{self.collection_name}'))) as total_size
|
||||
(SELECT COUNT(*) FROM {}) as row_count,
|
||||
(SELECT pg_size_pretty(pg_total_relation_size({}::regclass))) as total_size
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public' AND table_name = %s
|
||||
""",
|
||||
""").format(self._col(), sql.Literal(self.collection_name)),
|
||||
(self.collection_name,),
|
||||
)
|
||||
result = cur.fetchone()
|
||||
@@ -421,17 +431,18 @@ class PGVector(VectorStoreBase):
|
||||
filter_conditions.append("payload->>%s = %s")
|
||||
filter_params.extend([k, str(v)])
|
||||
|
||||
filter_clause = "WHERE " + " AND ".join(filter_conditions) if filter_conditions else ""
|
||||
|
||||
query = f"""
|
||||
SELECT id, vector, payload
|
||||
FROM {self.collection_name}
|
||||
{filter_clause}
|
||||
LIMIT %s
|
||||
"""
|
||||
filter_clause = sql.SQL("WHERE " + " AND ".join(filter_conditions)) if filter_conditions else sql.SQL("")
|
||||
|
||||
with self._get_cursor() as cur:
|
||||
cur.execute(query, (*filter_params, top_k))
|
||||
cur.execute(
|
||||
sql.SQL("""
|
||||
SELECT id, vector, payload
|
||||
FROM {}
|
||||
{}
|
||||
LIMIT %s
|
||||
""").format(self._col(), filter_clause),
|
||||
(*filter_params, top_k),
|
||||
)
|
||||
results = cur.fetchall()
|
||||
return [[OutputData(id=str(r[0]), score=None, payload=r[2]) for r in results]]
|
||||
|
||||
|
||||
+59
-15
@@ -2,7 +2,9 @@
|
||||
|
||||
Long-term memory for [OpenClaw](https://github.com/openclaw/openclaw) agents, powered by [Mem0](https://mem0.ai).
|
||||
|
||||
Your agent forgets everything between sessions. This plugin fixes that — it stores conversations, extracts what matters, and brings it back when relevant. Enable `autoRecall` and `autoCapture` in config to run this automatically, or use agent tools for explicit control.
|
||||
Your agent forgets everything between sessions. This plugin fixes that — it stores conversations, extracts what matters, and brings it back when relevant.
|
||||
|
||||
By default, the plugin runs in **skills mode**: the agent controls what to remember (triage), how to recall (recall), and periodic cleanup (dream). Skills mode, `autoRecall`, and `autoCapture` are all enabled by default during `openclaw mem0 init`.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -10,12 +12,12 @@ Check your OpenClaw version:
|
||||
|
||||
```bash
|
||||
openclaw --version
|
||||
# OpenClaw 2026.4.15 (041266a)
|
||||
# OpenClaw 2026.4.25 (aa36ee6)
|
||||
```
|
||||
|
||||
| OpenClaw Version | Plugin Support |
|
||||
|------------------|----------------|
|
||||
| `>= 2026.4.15` | Fully supported |
|
||||
| `>= 2026.4.25` | Fully supported |
|
||||
|
||||
## Quick Start
|
||||
|
||||
@@ -50,7 +52,19 @@ openclaw --version
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"apiKey": "${MEM0_API_KEY}",
|
||||
"userId": "alice"
|
||||
"userId": "alice",
|
||||
"skills": {
|
||||
"triage": { "enabled": true },
|
||||
"recall": {
|
||||
"enabled": true,
|
||||
"tokenBudget": 1500,
|
||||
"rerank": true,
|
||||
"keywordSearch": true,
|
||||
"identityAlwaysInclude": true
|
||||
},
|
||||
"dream": { "enabled": true },
|
||||
"domain": "companion"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,11 +196,24 @@ All `oss` fields are optional. See the [Mem0 OSS docs](https://docs.mem0.ai/open
|
||||
<img src="https://raw.githubusercontent.com/mem0ai/mem0/main/docs/images/openclaw-architecture.png" alt="Architecture" width="800" />
|
||||
</p>
|
||||
|
||||
**Auto-Recall** (`autoRecall: true`) — Before the agent responds, the plugin searches Mem0 for relevant memories and injects them into context.
|
||||
### Skills Mode (Default)
|
||||
|
||||
**Auto-Capture** (`autoCapture: true`) — After the agent responds, the conversation is filtered through a noise-removal pipeline and sent to Mem0. New facts get stored, stale ones updated, duplicates merged.
|
||||
Enabled automatically during `openclaw mem0 init`. The agent controls memory through three skills:
|
||||
|
||||
Both are opt-in. Once enabled, they run silently — no prompting, no manual calls required. Without them, the agent can still use memory tools (`memory_add`, `memory_search`, etc.) explicitly.
|
||||
- **Triage** — Extracts durable facts from conversations using a structured protocol. Categories, importance gates, and domain overlays control what gets stored.
|
||||
- **Recall** — Before each turn, rewrites the user message into search queries, retrieves relevant memories with reranking, and injects them into context.
|
||||
- **Dream** — Periodic memory consolidation: merges duplicates, resolves conflicts, and prunes stale entries.
|
||||
|
||||
When skills mode is active, the skills handle memory operations. `autoRecall` and `autoCapture` remain `true` by default alongside skills mode. The built-in `session-memory` hook is disabled to avoid conflicts.
|
||||
|
||||
### Auto-Recall & Auto-Capture
|
||||
|
||||
When skills mode is not configured, the plugin uses `autoRecall` and `autoCapture` (both enabled by default):
|
||||
|
||||
- **Auto-Recall** — Before the agent responds, the plugin searches Mem0 for relevant memories and injects them into context.
|
||||
- **Auto-Capture** — After the agent responds, the conversation is filtered through a noise-removal pipeline and sent to Mem0. New facts get stored, stale ones updated, duplicates merged.
|
||||
|
||||
Set `autoRecall: false` or `autoCapture: false` to disable individually. The agent can also use memory tools (`memory_add`, `memory_search`, etc.) explicitly regardless of these settings.
|
||||
|
||||
### Memory Scopes
|
||||
|
||||
@@ -260,10 +287,25 @@ openclaw mem0 help --json # discover all comma
|
||||
| --- | ---- | ------- | ----------- |
|
||||
| `mode` | `"platform"` \| `"open-source"` | `"platform"` | Backend mode |
|
||||
| `userId` | `string` | OS username | User identifier. All memories scoped to this value. |
|
||||
| `autoRecall` | `boolean` | `false` | Inject relevant memories before each turn |
|
||||
| `autoCapture` | `boolean` | `false` | Extract and store facts after each turn |
|
||||
| `autoRecall` | `boolean` | `true` | Inject relevant memories before each turn. Ignored when `skills` is set. |
|
||||
| `autoCapture` | `boolean` | `true` | Extract and store facts after each turn. Ignored when `skills` is set. |
|
||||
| `topK` | `number` | `5` | Max memories returned per recall |
|
||||
| `searchThreshold` | `number` | `0.3` | Minimum similarity score (0-1) |
|
||||
| `searchThreshold` | `number` | `0.1` | Minimum similarity score (0-1) |
|
||||
|
||||
### Skills Mode (Recommended)
|
||||
|
||||
Enabled by default during `openclaw mem0 init`. `autoRecall` and `autoCapture` are also `true` by default and work alongside skills mode.
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
| --- | ---- | ------- | ----------- |
|
||||
| `skills.triage.enabled` | `boolean` | `true` | Enable fact extraction from conversations |
|
||||
| `skills.recall.enabled` | `boolean` | `true` | Enable memory recall before each turn |
|
||||
| `skills.recall.tokenBudget` | `number` | `1500` | Max tokens for injected memories |
|
||||
| `skills.recall.rerank` | `boolean` | `true` | Rerank search results for relevance |
|
||||
| `skills.recall.keywordSearch` | `boolean` | `true` | Augment with keyword-based search |
|
||||
| `skills.recall.identityAlwaysInclude` | `boolean` | `true` | Always include identity memories |
|
||||
| `skills.dream.enabled` | `boolean` | `true` | Enable periodic memory consolidation |
|
||||
| `skills.domain` | `string` | `"companion"` | Domain overlay for triage rules |
|
||||
|
||||
### Platform Mode
|
||||
|
||||
@@ -306,13 +348,15 @@ To avoid plaintext credentials:
|
||||
- Use env var references: `"apiKey": "${MEM0_API_KEY}"`
|
||||
- Use SecretRef: `"apiKey": {"source": "env", "provider": "default", "id": "MEM0_API_KEY"}`
|
||||
|
||||
### Auto-Capture & Auto-Recall
|
||||
### Memory Processing
|
||||
|
||||
Both are **disabled by default** (`false`). When enabled:
|
||||
- `autoCapture`: sends conversation content to your configured backend (cloud or local) after each agent turn
|
||||
- `autoRecall`: queries your memory store before each agent turn and injects results into agent context
|
||||
In **skills mode** (default after `openclaw mem0 init`), the agent uses structured protocols (triage, recall, dream) to decide what to store and recall. The built-in `session-memory` hook is disabled to avoid conflicts.
|
||||
|
||||
Do not enable `autoCapture` in platform mode if your conversations contain sensitive data you do not want stored on Mem0 cloud.
|
||||
Without skills, `autoCapture` and `autoRecall` are both enabled by default:
|
||||
- `autoCapture`: sends conversation content to your configured backend after each agent turn
|
||||
- `autoRecall`: queries your memory store before each agent turn and injects results into context
|
||||
|
||||
In platform mode, conversation content is sent to `api.mem0.ai` for processing. Do not use with sensitive data you do not want stored on Mem0 cloud.
|
||||
|
||||
### Persistence Locations
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
readPluginAuth,
|
||||
writePluginAuth,
|
||||
writePluginConfigField,
|
||||
enableSkillsConfig,
|
||||
OPENCLAW_CONFIG_FILE,
|
||||
} from "./config-file.ts";
|
||||
import { jsonOut, jsonErr, redactSecrets } from "./json-helpers.ts";
|
||||
@@ -50,6 +51,7 @@ import {
|
||||
LLM_PROVIDERS, EMBEDDER_PROVIDERS, VECTOR_PROVIDERS,
|
||||
buildOssLlmConfig, buildOssEmbedderConfig, buildOssVectorConfig,
|
||||
validateOssFlags, checkQdrantConnectivity, checkOllamaConnectivity, checkPgConnectivity,
|
||||
collectionNameForDims,
|
||||
} from "./oss-wizard.ts";
|
||||
|
||||
// ============================================================================
|
||||
@@ -213,10 +215,11 @@ function saveLoginConfig(
|
||||
const userId = resolveUserId(userIdFlag, existingAuth.userId);
|
||||
|
||||
writePluginAuth({ apiKey, userId, mode: "platform", ...(userEmail && { userEmail }) });
|
||||
enableSkillsConfig(userId);
|
||||
|
||||
if (!silent) {
|
||||
console.log(` Configuration saved to ${OPENCLAW_CONFIG_FILE}`);
|
||||
console.log(` Mode: platform`);
|
||||
console.log(` Mode: platform (skills enabled)`);
|
||||
console.log(` User ID: ${userId}`);
|
||||
}
|
||||
}
|
||||
@@ -226,10 +229,11 @@ function saveOssConfig(userIdFlag?: string, silent?: boolean): void {
|
||||
const userId = resolveUserId(userIdFlag, existingAuth.userId);
|
||||
|
||||
writePluginAuth({ apiKey: "", userId, mode: "open-source" });
|
||||
enableSkillsConfig(userId);
|
||||
|
||||
if (!silent) {
|
||||
console.log(` Configuration saved to ${OPENCLAW_CONFIG_FILE}`);
|
||||
console.log(` Mode: open-source`);
|
||||
console.log(` Mode: open-source (skills enabled)`);
|
||||
console.log(` User ID: ${userId}`);
|
||||
}
|
||||
}
|
||||
@@ -350,6 +354,17 @@ async function runOssWizardInteractive(
|
||||
}
|
||||
|
||||
const vecCfg = buildOssVectorConfig(vecDef.id, vecInput as any);
|
||||
|
||||
// Warn if switching embedder dimensions — old collection will have wrong vector size
|
||||
const existingVecCfg = existingAuth as any;
|
||||
const oldDims = existingVecCfg?.oss?.vectorStore?.config?.dimension as number | undefined;
|
||||
if (oldDims && dims && oldDims !== dims) {
|
||||
console.log(`\n ⚠ Dimension change detected: ${oldDims} → ${dims}`);
|
||||
console.log(` Old collection had ${oldDims}-dim vectors. New embedder produces ${dims}-dim vectors.`);
|
||||
console.log(` A new collection "${collectionNameForDims(dims)}" will be created.`);
|
||||
console.log(` Old memories in the previous collection will NOT be accessible with the new embedder.\n`);
|
||||
}
|
||||
|
||||
writePluginConfigField(["oss", "vectorStore"], vecCfg);
|
||||
|
||||
// === Step 4: User ID ===
|
||||
@@ -370,6 +385,8 @@ async function runOssWizardInteractive(
|
||||
console.log(` LLM: ${llmDef.id} (${llmCfg.config.model})`);
|
||||
console.log(` Embedder: ${embDef.id} (${embCfg.config.model})`);
|
||||
console.log(` Vector: ${vecDef.id} (${vecDef.id === "qdrant" ? vecCfg.config.url : vecCfg.config.host})`);
|
||||
console.log(` Dims: ${dims ?? "unknown"}`);
|
||||
console.log(` Collection:${dims ? " " + collectionNameForDims(dims) : " (default)"}`);
|
||||
console.log(` User ID: ${userIdValue}`);
|
||||
console.log("");
|
||||
console.log(" Run: openclaw gateway restart");
|
||||
@@ -539,6 +556,15 @@ export function registerCliCommands(
|
||||
}
|
||||
}
|
||||
|
||||
// Warn on dimension change
|
||||
const prevAuth = readPluginAuth() as any;
|
||||
const prevDims = prevAuth?.oss?.vectorStore?.config?.dimension as number | undefined;
|
||||
const newDims = dims;
|
||||
let dimWarning: string | undefined;
|
||||
if (prevDims && newDims && prevDims !== newDims) {
|
||||
dimWarning = `Dimension change: ${prevDims} → ${newDims}. New collection "${collectionNameForDims(newDims)}" will be used. Old memories not accessible with new embedder.`;
|
||||
}
|
||||
|
||||
writePluginConfigField(["oss", "llm"], llmCfg);
|
||||
writePluginConfigField(["oss", "embedder"], { provider: embCfg.provider, config: embCfg.config });
|
||||
writePluginConfigField(["oss", "vectorStore"], vecCfg);
|
||||
@@ -550,9 +576,10 @@ export function registerCliCommands(
|
||||
mode: "open-source",
|
||||
config: {
|
||||
llm: { provider: llmCfg.provider, model: llmCfg.config.model },
|
||||
embedder: { provider: embCfg.provider, model: embCfg.config.model },
|
||||
vectorStore: { provider: vecCfg.provider, ...(vecId === "qdrant" ? { url: vecCfg.config.url } : { host: vecCfg.config.host }) },
|
||||
embedder: { provider: embCfg.provider, model: embCfg.config.model, dims: newDims },
|
||||
vectorStore: { provider: vecCfg.provider, ...(vecId === "qdrant" ? { url: vecCfg.config.url } : { host: vecCfg.config.host }), collectionName: newDims ? collectionNameForDims(newDims) : undefined },
|
||||
},
|
||||
...(dimWarning && { warning: dimWarning }),
|
||||
userId: resolveUserId(opts.userId, existingAuth.userId),
|
||||
message: "Open-source mode configured. Restart the gateway: openclaw gateway restart",
|
||||
};
|
||||
@@ -889,7 +916,7 @@ export function registerCliCommands(
|
||||
runId?: string,
|
||||
): SearchOptions => {
|
||||
const base = buildSearchOptions(userIdOverride, lim, runId);
|
||||
base.threshold = 0.3;
|
||||
base.threshold = 0.1;
|
||||
return base;
|
||||
};
|
||||
|
||||
|
||||
+78
-78
@@ -19,7 +19,6 @@ export const OPENCLAW_CONFIG_FILE = join(OPENCLAW_CONFIG_DIR, "openclaw.json");
|
||||
export const DEFAULT_BASE_URL = "https://api.mem0.ai";
|
||||
|
||||
const PLUGIN_ID = "openclaw-mem0";
|
||||
const NPM_PACKAGE = "@mem0/openclaw-mem0";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@@ -76,11 +75,44 @@ function readFullConfig(): Record<string, unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Write the full ~/.openclaw/openclaw.json (preserves all non-plugin config) */
|
||||
/**
|
||||
* Write the full ~/.openclaw/openclaw.json.
|
||||
*
|
||||
* Re-reads the file immediately before writing and deep-merges the
|
||||
* `plugins` section so that fields written by other processes (e.g.
|
||||
* OpenClaw gateway adding `installs`, `slots`) are not clobbered.
|
||||
*/
|
||||
function writeFullConfig(config: Record<string, unknown>): void {
|
||||
if (!exists(OPENCLAW_CONFIG_DIR)) {
|
||||
mkdirp(OPENCLAW_CONFIG_DIR, 0o700);
|
||||
}
|
||||
|
||||
if (exists(OPENCLAW_CONFIG_FILE)) {
|
||||
try {
|
||||
const diskText = readText(OPENCLAW_CONFIG_FILE);
|
||||
if (diskText.trim()) {
|
||||
const disk = JSON.parse(diskText) as Record<string, unknown>;
|
||||
const diskPlugins = disk.plugins as Record<string, unknown> | undefined;
|
||||
const ourPlugins = config.plugins as Record<string, unknown> | undefined;
|
||||
if (diskPlugins && ourPlugins) {
|
||||
const OPENCLAW_MANAGED = ["installs", "slots"];
|
||||
for (const key of OPENCLAW_MANAGED) {
|
||||
if (key in diskPlugins) {
|
||||
ourPlugins[key] = diskPlugins[key];
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(diskPlugins)) {
|
||||
if (!(key in ourPlugins)) {
|
||||
ourPlugins[key] = diskPlugins[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// disk unreadable — write our version as-is
|
||||
}
|
||||
}
|
||||
|
||||
writeText(
|
||||
OPENCLAW_CONFIG_FILE,
|
||||
JSON.stringify(config, null, 2),
|
||||
@@ -122,82 +154,6 @@ export function writePluginAuth(auth: PluginAuthConfig): void {
|
||||
writeFullConfig(full);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the plugin has a valid install record and is in plugins.allow.
|
||||
*
|
||||
* OpenClaw's `plugins update` command requires a `plugins.installs.<id>`
|
||||
* record with `source: "npm"` and `spec` to know how to update. Without
|
||||
* this, `openclaw plugins update` prints "No install record" and skips.
|
||||
*
|
||||
* Similarly, if `plugins.allow` exists as an array, the plugin ID must
|
||||
* be in it or OpenClaw treats the plugin as untrusted.
|
||||
*
|
||||
* This is safe to call multiple times — it only writes missing fields.
|
||||
*/
|
||||
export function ensureInstallRecord(): void {
|
||||
try {
|
||||
const full = readFullConfig() as any;
|
||||
|
||||
const entry = full?.plugins?.entries?.[PLUGIN_ID];
|
||||
const record = full?.plugins?.installs?.[PLUGIN_ID];
|
||||
const allow = full?.plugins?.allow;
|
||||
const specPinned = record?.spec && /\d+\.\d+\.\d+/.test(record.spec);
|
||||
if (
|
||||
entry?.enabled === true &&
|
||||
record?.source &&
|
||||
record?.spec &&
|
||||
!specPinned &&
|
||||
Array.isArray(allow) &&
|
||||
allow.includes(PLUGIN_ID)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensurePluginStructure(full);
|
||||
|
||||
let changed = false;
|
||||
|
||||
// Ensure install record exists for `openclaw plugins update` support
|
||||
if (!full.plugins.installs) full.plugins.installs = {};
|
||||
if (!full.plugins.installs[PLUGIN_ID]) {
|
||||
full.plugins.installs[PLUGIN_ID] = {
|
||||
source: "npm",
|
||||
spec: `${NPM_PACKAGE}@latest`,
|
||||
resolvedName: NPM_PACKAGE,
|
||||
installedAt: new Date().toISOString(),
|
||||
};
|
||||
changed = true;
|
||||
} else {
|
||||
const record = full.plugins.installs[PLUGIN_ID];
|
||||
if (!record.source) {
|
||||
record.source = "npm";
|
||||
changed = true;
|
||||
}
|
||||
if (!record.spec || /\d+\.\d+\.\d+/.test(record.spec)) {
|
||||
record.spec = record.source === "clawhub"
|
||||
? `clawhub:${NPM_PACKAGE}`
|
||||
: `${NPM_PACKAGE}@latest`;
|
||||
changed = true;
|
||||
}
|
||||
if (!record.resolvedName) {
|
||||
record.resolvedName = NPM_PACKAGE;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(full.plugins.allow)) {
|
||||
full.plugins.allow = [PLUGIN_ID];
|
||||
changed = true;
|
||||
} else if (!full.plugins.allow.includes(PLUGIN_ID)) {
|
||||
full.plugins.allow.push(PLUGIN_ID);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) writeFullConfig(full);
|
||||
} catch {
|
||||
// Best-effort — don't break plugin loading if config is unreadable
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensure the nested plugin entry structure exists in the config object. */
|
||||
function ensurePluginStructure(full: any): void {
|
||||
@@ -231,6 +187,50 @@ export function writePluginConfigField(
|
||||
writeFullConfig(full);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default skills configuration — matches configure.py output.
|
||||
* Enables triage, recall (with reranking), and dream consolidation.
|
||||
*/
|
||||
const DEFAULT_SKILLS_CONFIG = {
|
||||
triage: { enabled: true },
|
||||
recall: {
|
||||
enabled: true,
|
||||
tokenBudget: 1500,
|
||||
rerank: true,
|
||||
keywordSearch: true,
|
||||
identityAlwaysInclude: true,
|
||||
},
|
||||
dream: { enabled: true },
|
||||
domain: "companion",
|
||||
};
|
||||
|
||||
/**
|
||||
* Enable skills-mode config after onboarding.
|
||||
*
|
||||
* Sets skills config on the plugin entry, tools.profile = "full",
|
||||
* and disables the built-in session-memory hook to avoid conflicts.
|
||||
* Preserves any existing skills config if already set.
|
||||
*/
|
||||
export function enableSkillsConfig(userId: string): void {
|
||||
const full = readFullConfig() as any;
|
||||
ensurePluginStructure(full);
|
||||
|
||||
const cfg = full.plugins.entries[PLUGIN_ID].config;
|
||||
if (!cfg.skills) {
|
||||
cfg.skills = { ...DEFAULT_SKILLS_CONFIG };
|
||||
}
|
||||
|
||||
if (!full.tools) full.tools = {};
|
||||
full.tools.profile = "full";
|
||||
|
||||
if (!full.hooks) full.hooks = {};
|
||||
if (!full.hooks.internal) full.hooks.internal = {};
|
||||
if (!full.hooks.internal.entries) full.hooks.internal.entries = {};
|
||||
full.hooks.internal.entries["session-memory"] = { enabled: false };
|
||||
|
||||
writeFullConfig(full);
|
||||
}
|
||||
|
||||
/** Get the configured base URL from openclaw.json or default */
|
||||
export function getBaseUrl(): string {
|
||||
const auth = readPluginAuth();
|
||||
|
||||
@@ -56,8 +56,15 @@ export const KNOWN_EMBEDDER_DIMS: Record<string, number> = {
|
||||
"text-embedding-3-large": 3072,
|
||||
"text-embedding-ada-002": 1536,
|
||||
"nomic-embed-text": 768,
|
||||
"mxbai-embed-large": 1024,
|
||||
"all-minilm": 384,
|
||||
"snowflake-arctic-embed": 1024,
|
||||
};
|
||||
|
||||
export function collectionNameForDims(dims: number): string {
|
||||
return `mem0_${dims}d`;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Config builders
|
||||
// ============================================================================
|
||||
@@ -105,7 +112,8 @@ export function buildOssEmbedderConfig(
|
||||
config.url = input.url || def.defaultUrl;
|
||||
}
|
||||
|
||||
const dims = KNOWN_EMBEDDER_DIMS[model] ?? undefined;
|
||||
const dims = KNOWN_EMBEDDER_DIMS[model] ?? def.defaultDims;
|
||||
if (dims) config.embeddingDims = dims;
|
||||
return { provider: providerId, config, dims };
|
||||
}
|
||||
|
||||
@@ -138,7 +146,11 @@ export function buildOssVectorConfig(
|
||||
config.dbname = input.dbname || "postgres";
|
||||
}
|
||||
|
||||
if (input.dims) config.dimension = input.dims;
|
||||
if (input.dims) {
|
||||
config.dimension = input.dims;
|
||||
config.embeddingModelDims = input.dims;
|
||||
config.collectionName = collectionNameForDims(input.dims);
|
||||
}
|
||||
return { provider: providerId, config };
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -231,8 +231,8 @@ export const mem0ConfigSchema = {
|
||||
return "default";
|
||||
}
|
||||
})(),
|
||||
autoCapture: cfg.autoCapture === true,
|
||||
autoRecall: cfg.autoRecall === true,
|
||||
autoCapture: cfg.autoCapture !== false,
|
||||
autoRecall: cfg.autoRecall !== false,
|
||||
// v3.0.0: customPrompt renamed to customInstructions (backwards-compat: accept either)
|
||||
customInstructions:
|
||||
typeof cfg.customInstructions === "string"
|
||||
@@ -247,7 +247,7 @@ export const mem0ConfigSchema = {
|
||||
? (cfg.customCategories as Record<string, string>)
|
||||
: DEFAULT_CUSTOM_CATEGORIES,
|
||||
searchThreshold:
|
||||
typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.5,
|
||||
typeof cfg.searchThreshold === "number" ? cfg.searchThreshold : 0.1,
|
||||
topK: typeof cfg.topK === "number" ? cfg.topK : 5,
|
||||
needsSetup,
|
||||
oss: ossConfig,
|
||||
|
||||
@@ -561,3 +561,52 @@ What is the deployment plan?`,
|
||||
expect(result).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auto-recall threshold filtering
|
||||
// The recall hook in index.ts filters search results using cfg.searchThreshold.
|
||||
// These tests verify the threshold is honored and no hardcoded floor overrides it.
|
||||
// ---------------------------------------------------------------------------
|
||||
describe("auto-recall threshold respects cfg.searchThreshold", () => {
|
||||
const typicalV3Results = [
|
||||
{ id: "1", score: 0.553, memory: "User prefers dark mode" },
|
||||
{ id: "2", score: 0.496, memory: "User works on mem0 project" },
|
||||
{ id: "3", score: 0.471, memory: "User likes TypeScript" },
|
||||
{ id: "4", score: 0.45, memory: "User's timezone is PST" },
|
||||
{ id: "5", score: 0.42, memory: "User uses VS Code" },
|
||||
{ id: "6", score: 0.35, memory: "User mentioned family trip" },
|
||||
];
|
||||
|
||||
function applyThresholdFilter(
|
||||
results: typeof typicalV3Results,
|
||||
searchThreshold: number,
|
||||
) {
|
||||
return results.filter((r) => (r.score ?? 0) >= searchThreshold);
|
||||
}
|
||||
|
||||
it("default 0.5 threshold returns results scoring >= 0.5", () => {
|
||||
const filtered = applyThresholdFilter(typicalV3Results, 0.5);
|
||||
expect(filtered).toHaveLength(1);
|
||||
expect(filtered[0].id).toBe("1");
|
||||
});
|
||||
|
||||
it("threshold 0.4 returns results scoring >= 0.4", () => {
|
||||
const filtered = applyThresholdFilter(typicalV3Results, 0.4);
|
||||
expect(filtered).toHaveLength(5);
|
||||
});
|
||||
|
||||
it("threshold 0.3 returns all results", () => {
|
||||
const filtered = applyThresholdFilter(typicalV3Results, 0.3);
|
||||
expect(filtered).toHaveLength(6);
|
||||
});
|
||||
|
||||
it("threshold 0.6 correctly filters everything below", () => {
|
||||
const filtered = applyThresholdFilter(typicalV3Results, 0.6);
|
||||
expect(filtered).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("threshold 0 returns all results", () => {
|
||||
const filtered = applyThresholdFilter(typicalV3Results, 0);
|
||||
expect(filtered).toHaveLength(6);
|
||||
});
|
||||
});
|
||||
|
||||
+55
-11
@@ -54,15 +54,12 @@ import {
|
||||
import { PlatformBackend } from "./backend/platform.ts";
|
||||
import type { Backend } from "./backend/base.ts";
|
||||
import { registerCliCommands } from "./cli/commands.ts";
|
||||
import { readPluginAuth, ensureInstallRecord } from "./cli/config-file.ts";
|
||||
import { readPluginAuth } from "./cli/config-file.ts";
|
||||
import { registerAllTools } from "./tools/index.ts";
|
||||
import type { ToolDeps } from "./tools/index.ts";
|
||||
import { captureEvent } from "./telemetry.ts";
|
||||
import { bootstrapTelemetryFlag } from "./fs-safe.ts";
|
||||
|
||||
bootstrapTelemetryFlag();
|
||||
ensureInstallRecord();
|
||||
|
||||
// ============================================================================
|
||||
// Re-exports (for tests and external consumers)
|
||||
// ============================================================================
|
||||
@@ -100,6 +97,8 @@ const memoryPlugin = definePluginEntry({
|
||||
description: "Mem0 memory backend — Mem0 platform or self-hosted open-source",
|
||||
|
||||
register(api: OpenClawPluginApi) {
|
||||
bootstrapTelemetryFlag();
|
||||
|
||||
// Read auth from openclaw.json plugin config (picks up post-startup login).
|
||||
// This is the single source of truth — set via `openclaw mem0 login`.
|
||||
const pluginAuth = readPluginAuth();
|
||||
@@ -207,8 +206,57 @@ const memoryPlugin = definePluginEntry({
|
||||
},
|
||||
effectiveUserId: _effectiveUserId,
|
||||
}),
|
||||
runtime: {
|
||||
async getMemorySearchManager(_params: any) {
|
||||
try {
|
||||
const userId = _effectiveUserId();
|
||||
let memoryCount = 0;
|
||||
try {
|
||||
const memories = await provider.getAll({
|
||||
user_id: userId,
|
||||
page_size: 1,
|
||||
source: "OPENCLAW",
|
||||
});
|
||||
memoryCount = Array.isArray(memories) ? memories.length : 0;
|
||||
} catch {
|
||||
// Non-fatal: status still works without count
|
||||
}
|
||||
return {
|
||||
manager: {
|
||||
status() {
|
||||
return {
|
||||
backend: cfg.mode,
|
||||
files: 0,
|
||||
chunks: memoryCount,
|
||||
dirty: false,
|
||||
workspaceDir: pluginStateDir ?? "",
|
||||
userId,
|
||||
};
|
||||
},
|
||||
async probeEmbeddingAvailability() {
|
||||
return { ok: true };
|
||||
},
|
||||
async close() {},
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
manager: null,
|
||||
error: `mem0 ${cfg.mode} backend unavailable: ${String(err)}`,
|
||||
};
|
||||
}
|
||||
},
|
||||
resolveMemoryBackendConfig(_params: any) {
|
||||
return {
|
||||
backend: cfg.mode,
|
||||
baseUrl: cfg.baseUrl ?? "https://api.mem0.ai",
|
||||
userId: cfg.userId,
|
||||
};
|
||||
},
|
||||
async closeAllMemorySearchManagers() {},
|
||||
},
|
||||
});
|
||||
api.logger.debug("openclaw-mem0: publicArtifacts capability registered");
|
||||
api.logger.debug("openclaw-mem0: memory capability + runtime registered");
|
||||
}
|
||||
|
||||
// Helper: build add options
|
||||
@@ -681,12 +729,8 @@ function registerHooks(
|
||||
),
|
||||
);
|
||||
|
||||
// Client-side threshold filter for auto-recall — use a stricter
|
||||
// threshold (0.6) than explicit tool searches (0.5) to avoid
|
||||
// injecting irrelevant memories into agent context
|
||||
const recallThreshold = Math.max(cfg.searchThreshold, 0.6);
|
||||
longTermResults = longTermResults.filter(
|
||||
(r) => (r.score ?? 0) >= recallThreshold,
|
||||
(r) => (r.score ?? 0) >= cfg.searchThreshold,
|
||||
);
|
||||
|
||||
// Dynamic thresholding: drop memories scoring less than 50% of
|
||||
@@ -709,7 +753,7 @@ function registerHooks(
|
||||
undefined,
|
||||
recallSessionKey,
|
||||
);
|
||||
broadOpts.threshold = 0.5;
|
||||
broadOpts.threshold = cfg.searchThreshold;
|
||||
const broadResults = await provider.search(
|
||||
"recent decisions, preferences, active projects, and configuration",
|
||||
broadOpts,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "openclaw-mem0",
|
||||
"name": "Memory (Mem0)",
|
||||
"description": "Mem0 memory backend for OpenClaw — platform (mem0.ai cloud) or self-hosted open-source. Auto-recall and auto-capture are opt-in (disabled by default). Supports OpenAI, Anthropic, Ollama (fully local), Qdrant, and PGVector providers.",
|
||||
"version": "1.0.10",
|
||||
"version": "1.0.11",
|
||||
"kind": "memory",
|
||||
"skills": ["skills"],
|
||||
"commandAliases": [
|
||||
@@ -17,9 +17,17 @@
|
||||
"memory_update", "memory_delete", "memory_event_list", "memory_event_status"
|
||||
]
|
||||
},
|
||||
"providerAuthEnvVars": {
|
||||
"mem0": ["MEM0_API_KEY"],
|
||||
"openclaw-mem0-oss": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"]
|
||||
"setup": {
|
||||
"providers": [
|
||||
{
|
||||
"id": "mem0",
|
||||
"envVars": ["MEM0_API_KEY"]
|
||||
},
|
||||
{
|
||||
"id": "openclaw-mem0-oss",
|
||||
"envVars": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"providerAuthChoices": [
|
||||
{
|
||||
@@ -171,13 +179,13 @@
|
||||
},
|
||||
"autoCapture": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Opt-in. When true, extracts durable facts after each agent turn. Disabled by default."
|
||||
"default": true,
|
||||
"description": "When true, extracts durable facts after each agent turn. Enabled by default. Ignored in skills mode."
|
||||
},
|
||||
"autoRecall": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Opt-in. When true, injects relevant memories before each agent turn. Disabled by default."
|
||||
"default": true,
|
||||
"description": "When true, injects relevant memories before each agent turn. Enabled by default. Ignored in skills mode."
|
||||
},
|
||||
"customInstructions": {
|
||||
"type": "string"
|
||||
|
||||
+11
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/openclaw-mem0",
|
||||
"version": "1.0.10",
|
||||
"version": "1.0.11",
|
||||
"type": "module",
|
||||
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
|
||||
"license": "Apache-2.0",
|
||||
@@ -35,19 +35,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "0.34.47",
|
||||
"mem0ai": "3.0.1"
|
||||
"mem0ai": "3.0.2"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./dist/index.js"
|
||||
],
|
||||
"compat": {
|
||||
"pluginApi": ">=2026.3.28",
|
||||
"minGatewayVersion": ">=2026.3.28"
|
||||
"pluginApi": ">=2026.4.24",
|
||||
"minGatewayVersion": ">=2026.4.24"
|
||||
},
|
||||
"build": {
|
||||
"openclawVersion": "2026.4.1",
|
||||
"pluginSdkVersion": "2026.4.1"
|
||||
"openclawVersion": "2026.4.24",
|
||||
"pluginSdkVersion": "2026.4.24"
|
||||
},
|
||||
"install": {
|
||||
"npmSpec": "@mem0/openclaw-mem0"
|
||||
@@ -59,5 +59,10 @@
|
||||
"tsup": "^8.5.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^4.0.18"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"protobufjs@<7.5.5": "^7.5.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+25
-22
@@ -4,6 +4,9 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
protobufjs@<7.5.5: ^7.5.5
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -12,8 +15,8 @@ importers:
|
||||
specifier: 0.34.47
|
||||
version: 0.34.47
|
||||
mem0ai:
|
||||
specifier: 3.0.1
|
||||
version: 3.0.1(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(compromise@14.15.0)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.20.0)(redis@5.12.1)(ws@8.19.0)
|
||||
specifier: 3.0.2
|
||||
version: 3.0.2(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(compromise@14.15.0)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.20.0)(redis@5.12.1)(ws@8.19.0)
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22.15.0
|
||||
@@ -363,8 +366,8 @@ packages:
|
||||
'@protobufjs/base64@1.1.2':
|
||||
resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
|
||||
|
||||
'@protobufjs/codegen@2.0.4':
|
||||
resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==}
|
||||
'@protobufjs/codegen@2.0.5':
|
||||
resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==}
|
||||
|
||||
'@protobufjs/eventemitter@1.1.0':
|
||||
resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==}
|
||||
@@ -375,8 +378,8 @@ packages:
|
||||
'@protobufjs/float@1.0.2':
|
||||
resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
|
||||
|
||||
'@protobufjs/inquire@1.1.0':
|
||||
resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==}
|
||||
'@protobufjs/inquire@1.1.1':
|
||||
resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==}
|
||||
|
||||
'@protobufjs/path@1.1.2':
|
||||
resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
|
||||
@@ -384,8 +387,8 @@ packages:
|
||||
'@protobufjs/pool@1.1.0':
|
||||
resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
|
||||
|
||||
'@protobufjs/utf8@1.1.0':
|
||||
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
|
||||
'@protobufjs/utf8@1.1.1':
|
||||
resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==}
|
||||
|
||||
'@qdrant/js-client-rest@1.13.0':
|
||||
resolution: {integrity: sha512-bewMtnXlGvhhnfXsp0sLoLXOGvnrCM15z9lNlG0Snp021OedNAnRtKkerjk5vkOcbQWUmJHXYCuxDfcT93aSkA==}
|
||||
@@ -1505,8 +1508,8 @@ packages:
|
||||
md5@2.3.0:
|
||||
resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==}
|
||||
|
||||
mem0ai@3.0.1:
|
||||
resolution: {integrity: sha512-6phM544/3NRcCg7n5DBNRc9uUEMqGTe7sksULM2KM/FTihH27yTl5nPms8cnNpSKM5/ZKJ9jnkpzwGlvYYbtBQ==}
|
||||
mem0ai@3.0.2:
|
||||
resolution: {integrity: sha512-smB9q27jrJu2D5WZje65+zMVptdS/WsqALxd2kjiZhEpThd/qkS8T3bumatcTu6Zb+LsT28FRpeWjXGTJjyQXQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
'@anthropic-ai/sdk': ^0.40.1
|
||||
@@ -1845,8 +1848,8 @@ packages:
|
||||
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
protobufjs@7.5.4:
|
||||
resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==}
|
||||
protobufjs@7.5.6:
|
||||
resolution: {integrity: sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
@@ -2536,7 +2539,7 @@ snapshots:
|
||||
dependencies:
|
||||
google-auth-library: 10.6.1
|
||||
p-retry: 4.6.2
|
||||
protobufjs: 7.5.4
|
||||
protobufjs: 7.5.6
|
||||
ws: 8.19.0
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
@@ -2634,24 +2637,24 @@ snapshots:
|
||||
|
||||
'@protobufjs/base64@1.1.2': {}
|
||||
|
||||
'@protobufjs/codegen@2.0.4': {}
|
||||
'@protobufjs/codegen@2.0.5': {}
|
||||
|
||||
'@protobufjs/eventemitter@1.1.0': {}
|
||||
|
||||
'@protobufjs/fetch@1.1.0':
|
||||
dependencies:
|
||||
'@protobufjs/aspromise': 1.1.2
|
||||
'@protobufjs/inquire': 1.1.0
|
||||
'@protobufjs/inquire': 1.1.1
|
||||
|
||||
'@protobufjs/float@1.0.2': {}
|
||||
|
||||
'@protobufjs/inquire@1.1.0': {}
|
||||
'@protobufjs/inquire@1.1.1': {}
|
||||
|
||||
'@protobufjs/path@1.1.2': {}
|
||||
|
||||
'@protobufjs/pool@1.1.0': {}
|
||||
|
||||
'@protobufjs/utf8@1.1.0': {}
|
||||
'@protobufjs/utf8@1.1.1': {}
|
||||
|
||||
'@qdrant/js-client-rest@1.13.0(typescript@5.9.3)':
|
||||
dependencies:
|
||||
@@ -3687,7 +3690,7 @@ snapshots:
|
||||
crypt: 0.0.2
|
||||
is-buffer: 1.1.6
|
||||
|
||||
mem0ai@3.0.1(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(compromise@14.15.0)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.20.0)(redis@5.12.1)(ws@8.19.0):
|
||||
mem0ai@3.0.2(@anthropic-ai/sdk@0.40.1)(@azure/identity@4.13.0)(@azure/search-documents@12.2.0)(@cloudflare/workers-types@4.20260313.1)(@google/genai@1.45.0)(@langchain/core@0.3.80(openai@4.104.0(ws@8.19.0)(zod@3.25.76)))(@mistralai/mistralai@1.15.1)(@qdrant/js-client-rest@1.13.0(typescript@5.9.3))(@supabase/supabase-js@2.99.1)(@types/jest@29.5.14)(@types/pg@8.11.0)(better-sqlite3@12.8.0)(cloudflare@4.5.0)(compromise@14.15.0)(groq-sdk@0.3.0)(natural@8.1.1)(ollama@0.5.18)(pg@8.20.0)(redis@5.12.1)(ws@8.19.0):
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk': 0.40.1
|
||||
'@azure/identity': 4.13.0
|
||||
@@ -4020,18 +4023,18 @@ snapshots:
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 18.3.1
|
||||
|
||||
protobufjs@7.5.4:
|
||||
protobufjs@7.5.6:
|
||||
dependencies:
|
||||
'@protobufjs/aspromise': 1.1.2
|
||||
'@protobufjs/base64': 1.1.2
|
||||
'@protobufjs/codegen': 2.0.4
|
||||
'@protobufjs/codegen': 2.0.5
|
||||
'@protobufjs/eventemitter': 1.1.0
|
||||
'@protobufjs/fetch': 1.1.0
|
||||
'@protobufjs/float': 1.0.2
|
||||
'@protobufjs/inquire': 1.1.0
|
||||
'@protobufjs/inquire': 1.1.1
|
||||
'@protobufjs/path': 1.1.2
|
||||
'@protobufjs/pool': 1.1.0
|
||||
'@protobufjs/utf8': 1.1.0
|
||||
'@protobufjs/utf8': 1.1.1
|
||||
'@types/node': 22.19.15
|
||||
long: 5.3.2
|
||||
|
||||
|
||||
+21
-5
@@ -280,13 +280,29 @@ class OSSProvider implements Mem0Provider {
|
||||
config.llm = defaultLlm;
|
||||
}
|
||||
|
||||
if (this.ossConfig?.vectorStore)
|
||||
config.vectorStore = { ...this.ossConfig.vectorStore };
|
||||
if (this.ossConfig?.vectorStore) {
|
||||
const vs = { ...this.ossConfig.vectorStore } as Record<string, unknown>;
|
||||
const vsCfg = (vs.config ?? {}) as Record<string, unknown>;
|
||||
// Resolve dims from embedder config if vector store doesn't have them
|
||||
const embedderDims = (config.embedder as any)?.config?.embeddingDims;
|
||||
if (!vsCfg.dimension && embedderDims) {
|
||||
vsCfg.dimension = embedderDims;
|
||||
}
|
||||
// Sync both dimension fields — Qdrant reads dimension, PGVector reads embeddingModelDims
|
||||
if (vsCfg.dimension && !vsCfg.embeddingModelDims) {
|
||||
vsCfg.embeddingModelDims = vsCfg.dimension;
|
||||
} else if (vsCfg.embeddingModelDims && !vsCfg.dimension) {
|
||||
vsCfg.dimension = vsCfg.embeddingModelDims;
|
||||
}
|
||||
vs.config = vsCfg;
|
||||
config.vectorStore = vs;
|
||||
}
|
||||
|
||||
if (this.ossConfig?.historyDbPath) {
|
||||
const dbPath = this.resolvePath
|
||||
? this.resolvePath(this.ossConfig.historyDbPath)
|
||||
: this.ossConfig.historyDbPath;
|
||||
const raw = this.ossConfig.historyDbPath;
|
||||
const isAbsolute = raw.startsWith("/") || /^[A-Za-z]:[/\\]/.test(raw);
|
||||
const dbPath =
|
||||
isAbsolute || !this.resolvePath ? raw : this.resolvePath(raw);
|
||||
config.historyDbPath = dbPath;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,49 @@ metadata:
|
||||
|
||||
You are performing a memory consolidation pass. Your goal is to review all stored memories for this user and improve their overall quality. Think of this as compressing raw observations into clean, durable knowledge.
|
||||
|
||||
## Available Tools
|
||||
|
||||
### memory_search
|
||||
Semantic search across stored memories.
|
||||
- `query` (required): search query
|
||||
- `limit`: max results
|
||||
- `userId`, `agentId`: scope overrides
|
||||
- `scope`: `"all"` (default), `"session"`, or `"long-term"`
|
||||
- `categories`: filter by category array
|
||||
|
||||
### memory_add
|
||||
Store new facts in long-term memory.
|
||||
- `facts` (required): array of facts — ALL must share the same category
|
||||
- `category`: `"identity"`, `"preference"`, `"decision"`, `"rule"`, `"project"`, `"configuration"`, `"technical"`, `"relationship"`
|
||||
- `importance`: 0.0–1.0
|
||||
|
||||
### memory_get
|
||||
Retrieve a single memory by ID.
|
||||
- `memoryId` (required): the memory ID
|
||||
|
||||
### memory_list
|
||||
List all stored memories for a user or agent.
|
||||
- `userId`, `agentId`: scope overrides
|
||||
- `scope`: `"all"` (default), `"session"`, or `"long-term"`
|
||||
|
||||
### memory_update
|
||||
Update an existing memory's text in place. Atomic and preserves edit history.
|
||||
- `memoryId` (required): the memory ID to update
|
||||
- `text` (required): the new text (replaces old)
|
||||
|
||||
### memory_delete
|
||||
Delete memories by ID, query, or bulk.
|
||||
- `memoryId`: specific memory ID to delete
|
||||
- `all`: delete ALL memories (requires `confirm: true`)
|
||||
- `userId`, `agentId`: scope overrides
|
||||
|
||||
### memory_event_list
|
||||
List recent background processing events (platform mode only).
|
||||
|
||||
### memory_event_status
|
||||
Get status of a specific background event.
|
||||
- `event_id` (required): the event ID to check
|
||||
|
||||
Follow these four phases in order. Do not skip phases.
|
||||
|
||||
## Phase 1: Orient
|
||||
|
||||
@@ -18,6 +18,56 @@ Your primary role is to extract relevant pieces of information from the conversa
|
||||
|
||||
**The core question**: "Would a new agent — with no prior context — benefit from knowing this?" If no → do nothing. Most turns produce zero memory operations. That is correct and expected.
|
||||
|
||||
## Available Tools
|
||||
|
||||
### memory_search
|
||||
Semantic search across stored memories.
|
||||
- `query` (required): search query
|
||||
- `limit`: max results (default: configured topK)
|
||||
- `userId`, `agentId`: scope overrides
|
||||
- `scope`: `"all"` (default), `"session"`, or `"long-term"`
|
||||
- `categories`: filter by category array
|
||||
- `filters`: advanced filter object
|
||||
|
||||
### memory_add
|
||||
Store new facts in long-term memory.
|
||||
- `facts` (required): array of facts to store — ALL must share the same category
|
||||
- `text`: alternative single-fact string
|
||||
- `category`: `"identity"`, `"preference"`, `"decision"`, `"rule"`, `"project"`, `"configuration"`, `"technical"`, `"relationship"`
|
||||
- `importance`: 0.0–1.0 (omit for category default)
|
||||
- `userId`, `agentId`: scope overrides
|
||||
- `metadata`: additional key-value metadata
|
||||
- `longTerm`: true (default) for persistent, false for session-scoped
|
||||
|
||||
### memory_get
|
||||
Retrieve a single memory by ID.
|
||||
- `memoryId` (required): the memory ID
|
||||
|
||||
### memory_list
|
||||
List all stored memories for a user or agent.
|
||||
- `userId`, `agentId`: scope overrides
|
||||
- `scope`: `"all"` (default), `"session"`, or `"long-term"`
|
||||
|
||||
### memory_update
|
||||
Update an existing memory's text in place. Atomic and preserves edit history.
|
||||
- `memoryId` (required): the memory ID to update
|
||||
- `text` (required): the new text (replaces old)
|
||||
|
||||
### memory_delete
|
||||
Delete memories by ID, query, or bulk.
|
||||
- `memoryId`: specific memory ID to delete
|
||||
- `query`: search query to find and delete matching memories
|
||||
- `all`: delete ALL memories (requires `confirm: true`)
|
||||
- `confirm`: safety gate for bulk operations
|
||||
- `userId`, `agentId`: scope overrides
|
||||
|
||||
### memory_event_list
|
||||
List recent background processing events (platform mode only).
|
||||
|
||||
### memory_event_status
|
||||
Get status of a specific background event.
|
||||
- `event_id` (required): the event ID to check
|
||||
|
||||
## Decision Gate
|
||||
|
||||
Every candidate fact must pass ALL four gates:
|
||||
@@ -28,7 +78,7 @@ Every candidate fact must pass ALL four gates:
|
||||
|
||||
**Gate 2 — NOVELTY**: Check your recalled memories below — is this already known?
|
||||
- Already known and unchanged → SKIP
|
||||
- Known but materially changed → UPDATE (find old → forget → store new)
|
||||
- Known but materially changed → UPDATE (find old → update in place)
|
||||
- Genuinely new → proceed
|
||||
- **Material difference test**: Only UPDATE if new information adds real context, details, or changes meaning. Cosmetic differences (synonyms, rephrasing, punctuation) are NOT updates. "Loves daily walks" vs "enjoys daily walks" = no material change = SKIP.
|
||||
|
||||
@@ -196,8 +246,9 @@ Categories: `identity`, `configuration`, `rule`, `preference`, `decision`, `tech
|
||||
|
||||
When a recalled memory needs updating (fact changed, status changed, new detail added):
|
||||
1. `memory_search` to find the existing memory
|
||||
2. `memory_delete` on the old memory's ID
|
||||
3. `memory_add` with the corrected/expanded fact
|
||||
2. `memory_update` on the memory's ID with the corrected/expanded text
|
||||
|
||||
`memory_update` is preferred over delete+add because it is **atomic and preserves edit history**.
|
||||
|
||||
**Choose the MORE COMPLETE version.** When both old and new have unique context, COMBINE them into a unified memory using the user's stated words.
|
||||
|
||||
@@ -206,10 +257,10 @@ When a recalled memory needs updating (fact changed, status changed, new detail
|
||||
- "User likes Python" → "User enjoys Python" = NOT material = SKIP
|
||||
- When both have unique context, combine: Old "Trip to Paris in September with Jack" + New "User can't wait to visit Eiffel Tower" → "Trip to Paris in September 2025 with friend Jack, user says they can't wait to visit the Eiffel Tower and try authentic French pastries"
|
||||
|
||||
**Consolidation**: When a rich new fact encompasses multiple existing memories, update one to the comprehensive version and forget the others.
|
||||
**Consolidation**: When a rich new fact encompasses multiple existing memories, `memory_update` the best one to the comprehensive version and `memory_delete` the rest.
|
||||
- Old: "User has a dog" + "Dog's name is Poppy" + "User walks dog daily"
|
||||
- New: "User has a dog named Poppy and says taking him for walks is the best part of their day"
|
||||
- Action: forget all three old memories, store one consolidated memory
|
||||
- Action: `memory_update` the best version with consolidated text, `memory_delete` the redundant ones
|
||||
|
||||
**Temporary vs permanent changes**: A temporary constraint (e.g., injury pausing a hobby) does NOT contradict the underlying preference. Store the constraint as a new memory; don't delete the preference.
|
||||
- Old: "User enjoys hiking on weekends"
|
||||
@@ -267,8 +318,7 @@ User: "Never use Docker for local dev, it ate 40GB of disk last time and my Mac
|
||||
Recalled: ["As of 2026-03-15, user is planning trip to Paris in September with friend Jack"]
|
||||
User: "Can't wait for the Paris trip, definitely want to hit the Eiffel Tower and try authentic French pastries"
|
||||
→ memory_search("Paris trip planning")
|
||||
→ memory_delete(memoryId: "mem-id-of-old")
|
||||
→ memory_add(facts: ["As of 2026-03-30, user is planning trip to Paris in September 2025 with friend Jack, says they can't wait to visit the Eiffel Tower and try authentic French pastries"], category: "project")
|
||||
→ memory_update(memoryId: "mem-id-of-old", text: "As of 2026-03-30, user is planning trip to Paris in September 2025 with friend Jack, says they can't wait to visit the Eiffel Tower and try authentic French pastries")
|
||||
```
|
||||
|
||||
### Example 6: Outcome over intent
|
||||
@@ -327,8 +377,8 @@ Agent: "Hello! How can I help?"
|
||||
Recalled: ["User has a dog", "Dog's name is Poppy", "User walks dog daily"]
|
||||
User: "Poppy learned fetch! Our walks are even better now, honestly it's the best part of my day"
|
||||
→ memory_search("dog Poppy walks") → find all three old memory IDs
|
||||
→ memory_delete(memoryId: "id-1"), memory_delete(memoryId: "id-2"), memory_delete(memoryId: "id-3")
|
||||
→ memory_add(facts: ["User has a dog named Poppy and says taking him for walks is the best part of their day. Poppy recently learned fetch, making walks more enjoyable."], category: "preference")
|
||||
→ memory_update(memoryId: "id-1", text: "User has a dog named Poppy and says taking him for walks is the best part of their day. Poppy recently learned fetch, making walks more enjoyable.")
|
||||
→ memory_delete(memoryId: "id-2"), memory_delete(memoryId: "id-3")
|
||||
```
|
||||
|
||||
### Example 12: NOOP — generic greeting, nothing to store
|
||||
|
||||
@@ -443,7 +443,7 @@ describe("OSSProvider — _buildConfig branch coverage", () => {
|
||||
config: expect.objectContaining({ model: "gpt-4", apiKey: "sk-l" }),
|
||||
});
|
||||
expect(capturedConfig!.vectorStore).toEqual({ provider: "qdrant", config: { host: "localhost", port: 6333 } });
|
||||
expect(capturedConfig!.historyDbPath).toBe("/resolved/tmp/history.db");
|
||||
expect(capturedConfig!.historyDbPath).toBe("/tmp/history.db");
|
||||
expect(capturedConfig!.disableHistory).toBe(true);
|
||||
});
|
||||
|
||||
@@ -732,4 +732,58 @@ describe("OSSProvider — customInstructions passthrough", () => {
|
||||
expect(capturedConfig).toBeDefined();
|
||||
expect(capturedConfig!.customInstructions).toBe("Extract only user preferences.");
|
||||
});
|
||||
|
||||
it("preserves absolute Unix historyDbPath without resolvePath mangling", async () => {
|
||||
const { createProvider } = await import("./index.ts");
|
||||
const cfg = mem0ConfigSchema.parse({
|
||||
mode: "open-source",
|
||||
oss: {
|
||||
historyDbPath: "/home/user/.myapp/history.db",
|
||||
disableHistory: true,
|
||||
},
|
||||
});
|
||||
const api = { resolvePath: (p: string) => `/stateDir/${p}` } as any;
|
||||
const provider = createProvider(cfg, api);
|
||||
|
||||
await provider.search("test", { user_id: "u1" });
|
||||
|
||||
expect(capturedConfig).toBeDefined();
|
||||
expect(capturedConfig!.historyDbPath).toBe("/home/user/.myapp/history.db");
|
||||
});
|
||||
|
||||
it("preserves absolute Windows historyDbPath without resolvePath mangling", async () => {
|
||||
const { createProvider } = await import("./index.ts");
|
||||
const cfg = mem0ConfigSchema.parse({
|
||||
mode: "open-source",
|
||||
oss: {
|
||||
historyDbPath: "C:\\Users\\me\\history.db",
|
||||
disableHistory: true,
|
||||
},
|
||||
});
|
||||
const api = { resolvePath: (p: string) => `/stateDir/${p}` } as any;
|
||||
const provider = createProvider(cfg, api);
|
||||
|
||||
await provider.search("test", { user_id: "u1" });
|
||||
|
||||
expect(capturedConfig).toBeDefined();
|
||||
expect(capturedConfig!.historyDbPath).toBe("C:\\Users\\me\\history.db");
|
||||
});
|
||||
|
||||
it("still resolves relative historyDbPath via resolvePath", async () => {
|
||||
const { createProvider } = await import("./index.ts");
|
||||
const cfg = mem0ConfigSchema.parse({
|
||||
mode: "open-source",
|
||||
oss: {
|
||||
historyDbPath: "data/history.db",
|
||||
disableHistory: true,
|
||||
},
|
||||
});
|
||||
const api = { resolvePath: (p: string) => `/resolved/${p}` } as any;
|
||||
const provider = createProvider(cfg, api);
|
||||
|
||||
await provider.search("test", { user_id: "u1" });
|
||||
|
||||
expect(capturedConfig).toBeDefined();
|
||||
expect(capturedConfig!.historyDbPath).toBe("/resolved/data/history.db");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ vi.mock("../cli/config-file.ts", () => ({
|
||||
readPluginAuth: vi.fn().mockReturnValue({}),
|
||||
writePluginAuth: vi.fn(),
|
||||
writePluginConfigField: vi.fn(),
|
||||
enableSkillsConfig: vi.fn(),
|
||||
getBaseUrl: vi.fn().mockReturnValue("https://api.mem0.ai"),
|
||||
OPENCLAW_CONFIG_FILE: "/mock/.openclaw/openclaw.json",
|
||||
}));
|
||||
@@ -42,6 +43,7 @@ import {
|
||||
readPluginAuth,
|
||||
writePluginAuth,
|
||||
writePluginConfigField,
|
||||
enableSkillsConfig,
|
||||
getBaseUrl,
|
||||
} from "../cli/config-file.ts";
|
||||
import { loadDreamPrompt } from "../skill-loader.ts";
|
||||
@@ -178,7 +180,7 @@ function createMockCfg() {
|
||||
topK: 5,
|
||||
autoCapture: true,
|
||||
autoRecall: true,
|
||||
searchThreshold: 0.5,
|
||||
searchThreshold: 0.1,
|
||||
customInstructions: "",
|
||||
customCategories: {},
|
||||
skills: {},
|
||||
|
||||
@@ -44,14 +44,14 @@ describe("mem0ConfigSchema.parse() — defaults", () => {
|
||||
expect(cfg.userId.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("autoCapture defaults to false", () => {
|
||||
it("autoCapture defaults to true", () => {
|
||||
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
|
||||
expect(cfg.autoCapture).toBe(false);
|
||||
expect(cfg.autoCapture).toBe(true);
|
||||
});
|
||||
|
||||
it("autoRecall defaults to false", () => {
|
||||
it("autoRecall defaults to true", () => {
|
||||
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
|
||||
expect(cfg.autoRecall).toBe(false);
|
||||
expect(cfg.autoRecall).toBe(true);
|
||||
});
|
||||
|
||||
it("topK defaults to 5", () => {
|
||||
@@ -59,9 +59,9 @@ describe("mem0ConfigSchema.parse() — defaults", () => {
|
||||
expect(cfg.topK).toBe(5);
|
||||
});
|
||||
|
||||
it("searchThreshold defaults to 0.5", () => {
|
||||
it("searchThreshold defaults to 0.1", () => {
|
||||
const cfg = mem0ConfigSchema.parse({ apiKey: "test-key" });
|
||||
expect(cfg.searchThreshold).toBe(0.5);
|
||||
expect(cfg.searchThreshold).toBe(0.1);
|
||||
});
|
||||
|
||||
it("customInstructions defaults to DEFAULT_CUSTOM_INSTRUCTIONS", () => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
buildOssLlmConfig,
|
||||
buildOssEmbedderConfig,
|
||||
buildOssVectorConfig,
|
||||
collectionNameForDims,
|
||||
validateOssFlags,
|
||||
checkQdrantConnectivity,
|
||||
checkOllamaConnectivity,
|
||||
@@ -90,9 +91,9 @@ describe("buildOssEmbedderConfig", () => {
|
||||
expect(result.dims).toBe(768);
|
||||
});
|
||||
|
||||
it("returns unknown dims for custom model", () => {
|
||||
it("falls back to provider default dims for custom model", () => {
|
||||
const result = buildOssEmbedderConfig("ollama", { model: "custom-embed" });
|
||||
expect(result.dims).toBeUndefined();
|
||||
expect(result.dims).toBe(768);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -147,6 +148,49 @@ describe("buildOssVectorConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectionNameForDims", () => {
|
||||
it("generates dimension-based collection name", () => {
|
||||
expect(collectionNameForDims(1536)).toBe("mem0_1536d");
|
||||
expect(collectionNameForDims(768)).toBe("mem0_768d");
|
||||
expect(collectionNameForDims(384)).toBe("mem0_384d");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildOssVectorConfig dimension safety", () => {
|
||||
it("sets both dimension and embeddingModelDims when dims provided", () => {
|
||||
const result = buildOssVectorConfig("qdrant", { dims: 768 });
|
||||
expect(result.config.dimension).toBe(768);
|
||||
expect(result.config.embeddingModelDims).toBe(768);
|
||||
});
|
||||
|
||||
it("sets collectionName based on dims", () => {
|
||||
const result = buildOssVectorConfig("qdrant", { dims: 768 });
|
||||
expect(result.config.collectionName).toBe("mem0_768d");
|
||||
});
|
||||
|
||||
it("uses different collection names for different dims", () => {
|
||||
const r1 = buildOssVectorConfig("qdrant", { dims: 1536 });
|
||||
const r2 = buildOssVectorConfig("qdrant", { dims: 768 });
|
||||
expect(r1.config.collectionName).not.toBe(r2.config.collectionName);
|
||||
});
|
||||
|
||||
it("omits dimension fields when dims not provided", () => {
|
||||
const result = buildOssVectorConfig("qdrant", {});
|
||||
expect(result.config.dimension).toBeUndefined();
|
||||
expect(result.config.embeddingModelDims).toBeUndefined();
|
||||
expect(result.config.collectionName).toBeUndefined();
|
||||
});
|
||||
|
||||
it("works with pgvector too", () => {
|
||||
const result = buildOssVectorConfig("pgvector", {
|
||||
host: "localhost", port: "5432", user: "me", password: "pw", dbname: "test", dims: 768,
|
||||
});
|
||||
expect(result.config.dimension).toBe(768);
|
||||
expect(result.config.embeddingModelDims).toBe(768);
|
||||
expect(result.config.collectionName).toBe("mem0_768d");
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkQdrantConnectivity", () => {
|
||||
it("returns error for unreachable host", async () => {
|
||||
const result = await checkQdrantConnectivity("http://localhost:19999");
|
||||
|
||||
@@ -34,7 +34,7 @@ function createMockToolDeps(overrides = {}): ToolDeps {
|
||||
topK: 5,
|
||||
autoCapture: true,
|
||||
autoRecall: true,
|
||||
searchThreshold: 0.5,
|
||||
searchThreshold: 0.1,
|
||||
customInstructions: "test",
|
||||
customCategories: {},
|
||||
} as any,
|
||||
|
||||
@@ -8,7 +8,8 @@ export default defineConfig({
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
clean: true,
|
||||
external: [/^node:/, /^openclaw\//, "fs", "os", "path", "url", "readline", "module"],
|
||||
external: [/^node:/, /^openclaw\//, "fs", "os", "path", "url", "readline", "module",
|
||||
"mem0ai", /^mem0ai\//, "better-sqlite3", "@sinclair/typebox"],
|
||||
define: {
|
||||
__OPENCLAW_PLUGIN_VERSION__: JSON.stringify(pkg.version),
|
||||
},
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# OpenMemory
|
||||
|
||||
> **⚠️ Sunsetting Notice:** OpenMemory is being sunset. For local self-hosted memory with a dashboard, please use the [Mem0 self-hosted server](https://docs.mem0.ai/open-source/overview) instead. Get started with `cd server && make bootstrap`. See the [self-hosted docs](https://docs.mem0.ai/open-source/setup) for configuration details.
|
||||
|
||||
OpenMemory is your personal memory layer for LLMs - private, portable, and open-source. Your memories live locally, giving you complete control over your data. Build AI applications with personalized memories while keeping your data secure.
|
||||
|
||||

|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0ai"
|
||||
version = "2.0.0"
|
||||
version = "2.0.1"
|
||||
description = "Long-term memory for AI Agents"
|
||||
authors = [
|
||||
{ name = "Mem0", email = "support@mem0.ai" }
|
||||
|
||||
+2
-2
@@ -395,10 +395,10 @@ def get_all_memories(
|
||||
try:
|
||||
if not any([user_id, run_id, agent_id]):
|
||||
return _list_all_memories()
|
||||
params = {
|
||||
filters = {
|
||||
k: v for k, v in {"user_id": user_id, "run_id": run_id, "agent_id": agent_id}.items() if v is not None
|
||||
}
|
||||
return get_memory_instance().get_all(**params)
|
||||
return get_memory_instance().get_all(filters=filters)
|
||||
except Exception:
|
||||
raise upstream_error()
|
||||
|
||||
|
||||
@@ -78,6 +78,43 @@ class TestAddToVectorStoreErrors:
|
||||
assert result == [] # Should return empty list when no memories processed
|
||||
|
||||
|
||||
class TestPromptOverridesCustomInstructions:
|
||||
@pytest.fixture
|
||||
def mock_memory(self, mocker):
|
||||
mock_llm, _ = _setup_mocks(mocker)
|
||||
mock_llm.return_value.generate_response.return_value = '{"memory": []}'
|
||||
|
||||
memory = Memory()
|
||||
memory.custom_instructions = "config-level instructions"
|
||||
memory.db.get_last_messages = MagicMock(return_value=[])
|
||||
memory.db.save_messages = MagicMock()
|
||||
return memory
|
||||
|
||||
def test_prompt_overrides_custom_instructions(self, mock_memory):
|
||||
mock_memory._add_to_vector_store(
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
metadata={},
|
||||
filters={},
|
||||
infer=True,
|
||||
prompt="per-call override",
|
||||
)
|
||||
|
||||
user_prompt = mock_memory.llm.generate_response.call_args[1]["messages"][1]["content"]
|
||||
assert "per-call override" in user_prompt
|
||||
assert "config-level instructions" not in user_prompt
|
||||
|
||||
def test_falls_back_to_custom_instructions_when_no_prompt(self, mock_memory):
|
||||
mock_memory._add_to_vector_store(
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
metadata={},
|
||||
filters={},
|
||||
infer=True,
|
||||
)
|
||||
|
||||
user_prompt = mock_memory.llm.generate_response.call_args[1]["messages"][1]["content"]
|
||||
assert "config-level instructions" in user_prompt
|
||||
|
||||
|
||||
class TestAsyncUpdate:
|
||||
@pytest.fixture
|
||||
def mock_async_memory(self, mocker):
|
||||
|
||||
@@ -79,8 +79,8 @@ class TestRerank:
|
||||
reranker = LLMReranker({"provider": "openai"})
|
||||
reranker.rerank("query", [{"text": "some text"}])
|
||||
|
||||
prompt_sent = mock_llm_instance.generate_response.call_args[1]["messages"][0]["content"]
|
||||
assert "some text" in prompt_sent
|
||||
user_msg = mock_llm_instance.generate_response.call_args[1]["messages"][1]["content"]
|
||||
assert "some text" in user_msg
|
||||
|
||||
def test_content_field_extraction(self, mock_llm):
|
||||
_, mock_llm_instance = mock_llm
|
||||
@@ -89,8 +89,8 @@ class TestRerank:
|
||||
reranker = LLMReranker({"provider": "openai"})
|
||||
reranker.rerank("query", [{"content": "some content"}])
|
||||
|
||||
prompt_sent = mock_llm_instance.generate_response.call_args[1]["messages"][0]["content"]
|
||||
assert "some content" in prompt_sent
|
||||
user_msg = mock_llm_instance.generate_response.call_args[1]["messages"][1]["content"]
|
||||
assert "some content" in user_msg
|
||||
|
||||
def test_fallback_score_on_llm_error(self, mock_llm):
|
||||
_, mock_llm_instance = mock_llm
|
||||
@@ -106,12 +106,14 @@ class TestRerank:
|
||||
_, mock_llm_instance = mock_llm
|
||||
mock_llm_instance.generate_response.return_value = "0.7"
|
||||
|
||||
custom_prompt = "Rate this: query={query} doc={document}"
|
||||
custom_prompt = "Rate relevance on a scale of 0.0 to 1.0."
|
||||
reranker = LLMReranker({"provider": "openai", "scoring_prompt": custom_prompt})
|
||||
reranker.rerank("my query", [{"memory": "my doc"}])
|
||||
|
||||
prompt_sent = mock_llm_instance.generate_response.call_args[1]["messages"][0]["content"]
|
||||
assert prompt_sent == "Rate this: query=my query doc=my doc"
|
||||
messages = mock_llm_instance.generate_response.call_args[1]["messages"]
|
||||
assert messages[0]["content"] == custom_prompt
|
||||
assert "my query" in messages[1]["content"]
|
||||
assert "my doc" in messages[1]["content"]
|
||||
|
||||
def test_original_doc_not_mutated(self, mock_llm):
|
||||
_, mock_llm_instance = mock_llm
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ def test_add(memory_instance):
|
||||
assert result["results"] == [{"memory": "Test memory", "event": "ADD"}]
|
||||
|
||||
memory_instance._add_to_vector_store.assert_called_once_with(
|
||||
[{"role": "user", "content": "Test message"}], {"user_id": "test_user"}, {"user_id": "test_user"}, True
|
||||
[{"role": "user", "content": "Test message"}], {"user_id": "test_user"}, {"user_id": "test_user"}, True, prompt=None
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -854,3 +854,84 @@ def test_get_all_rejects_user_id_kwarg(mock_sqlite, mock_llm_factory, mock_vecto
|
||||
|
||||
with pytest.raises(ValueError, match=r"user_id.*filters"):
|
||||
memory.get_all(user_id="u1")
|
||||
|
||||
|
||||
# ─── Regression: AsyncMemory._create_memory must store text_lemmatized ─────────
|
||||
@patch('mem0.utils.factory.EmbedderFactory.create')
|
||||
@patch('mem0.utils.factory.VectorStoreFactory.create')
|
||||
@patch('mem0.utils.factory.LlmFactory.create')
|
||||
@patch('mem0.memory.storage.SQLiteManager')
|
||||
def test_sync_create_memory_stores_text_lemmatized(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
|
||||
"""Sync Memory._create_memory must include text_lemmatized in payload for BM25 keyword search."""
|
||||
embedder = MagicMock()
|
||||
embedder.embed.return_value = [0.1, 0.2, 0.3]
|
||||
mock_embedder_factory.return_value = embedder
|
||||
|
||||
mock_vector_store = MagicMock()
|
||||
mock_vector_store.insert.return_value = None
|
||||
telemetry_vector_store = MagicMock()
|
||||
mock_vector_factory.side_effect = [mock_vector_store, telemetry_vector_store]
|
||||
|
||||
mock_llm_factory.return_value = MagicMock()
|
||||
mock_sqlite.return_value = MagicMock()
|
||||
|
||||
from mem0.memory.main import Memory as MemoryClass
|
||||
memory = MemoryClass(MemoryConfig())
|
||||
|
||||
data = "I love hiking in the mountains"
|
||||
embeddings = {data: [0.1, 0.2, 0.3]}
|
||||
metadata = {"user_id": "test_user"}
|
||||
|
||||
memory._create_memory(data, embeddings, metadata)
|
||||
|
||||
# Check that text_lemmatized was stored in the payload
|
||||
insert_call = mock_vector_store.insert.call_args
|
||||
payload = insert_call.kwargs.get("payloads") or insert_call[1].get("payloads")
|
||||
assert payload is not None and len(payload) == 1
|
||||
assert "text_lemmatized" in payload[0], "Sync _create_memory must store text_lemmatized for BM25"
|
||||
assert payload[0]["text_lemmatized"] != "", "text_lemmatized must not be empty"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch('mem0.utils.factory.EmbedderFactory.create')
|
||||
@patch('mem0.utils.factory.VectorStoreFactory.create')
|
||||
@patch('mem0.utils.factory.LlmFactory.create')
|
||||
@patch('mem0.memory.storage.SQLiteManager')
|
||||
async def test_async_create_memory_stores_text_lemmatized(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
|
||||
"""
|
||||
Regression test: AsyncMemory._create_memory must include text_lemmatized
|
||||
in the vector store payload.
|
||||
|
||||
Without text_lemmatized, memories created via AsyncMemory with infer=False
|
||||
are invisible to BM25 keyword search, silently degrading search recall for
|
||||
all async users.
|
||||
"""
|
||||
embedder = MagicMock()
|
||||
embedder.embed.return_value = [0.1, 0.2, 0.3]
|
||||
mock_embedder_factory.return_value = embedder
|
||||
|
||||
mock_vector_store = MagicMock()
|
||||
mock_vector_store.insert.return_value = None
|
||||
mock_vector_factory.return_value = mock_vector_store
|
||||
|
||||
mock_llm_factory.return_value = MagicMock()
|
||||
mock_sqlite.return_value = MagicMock()
|
||||
|
||||
from mem0.memory.main import AsyncMemory
|
||||
memory = AsyncMemory(MemoryConfig())
|
||||
|
||||
data = "I love hiking in the mountains"
|
||||
embeddings = {data: [0.1, 0.2, 0.3]}
|
||||
metadata = {"user_id": "test_user"}
|
||||
|
||||
await memory._create_memory(data, embeddings, metadata)
|
||||
|
||||
# Check that text_lemmatized was stored in the payload
|
||||
insert_call = mock_vector_store.insert.call_args
|
||||
payload = insert_call.kwargs.get("payloads") or insert_call[1].get("payloads")
|
||||
assert payload is not None and len(payload) == 1
|
||||
assert "text_lemmatized" in payload[0], (
|
||||
"AsyncMemory._create_memory must store text_lemmatized for BM25 keyword search"
|
||||
)
|
||||
assert payload[0]["text_lemmatized"] != "", "text_lemmatized must not be empty"
|
||||
|
||||
|
||||
@@ -561,3 +561,30 @@ class TestUpdateOpenAPISchema:
|
||||
schema = client.get("/openapi.json").json()
|
||||
update_props = schema["components"]["schemas"]["MemoryUpdate"]["properties"]
|
||||
assert "metadata" in update_props
|
||||
|
||||
# ===========================================================================
|
||||
# GetMemories: Entity parameters to filters mapping (fix for #4955)
|
||||
# ===========================================================================
|
||||
|
||||
class TestGetMemories:
|
||||
"""Verify that GET /memories correctly maps entity parameters to the filters dict."""
|
||||
|
||||
def test_get_memories_entity_filters_routing(self, client, mock_memory):
|
||||
"""
|
||||
Issue #4955: Test that the GET /memories route correctly handles
|
||||
top-level entity parameters by mapping them to the filters dictionary
|
||||
instead of passing them as direct kwargs to get_all()
|
||||
"""
|
||||
# Send a request with a valid top-level entity parameter
|
||||
response = client.get("/memories?user_id=test_routing_user")
|
||||
|
||||
# 1. Verify the endpoint doesn't crash with a 500 error
|
||||
assert response.status_code == 200
|
||||
|
||||
# 2. Verify the response is structured correctly
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
|
||||
# 3. Verify the core logic: the param was mapped to the filters dict!
|
||||
_, kwargs = mock_memory.get_all.call_args
|
||||
assert kwargs["filters"] == {"user_id": "test_routing_user"}
|
||||
|
||||
@@ -127,10 +127,10 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Verify vector extension and table creation
|
||||
self.mock_cursor.execute.assert_any_call("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
table_creation_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "CREATE TABLE IF NOT EXISTS test_collection" in str(call)]
|
||||
table_creation_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "CREATE TABLE IF NOT EXISTS" in str(call) and "test_collection" in str(call)]
|
||||
self.assertTrue(len(table_creation_calls) > 0)
|
||||
|
||||
|
||||
# Verify pgvector instance properties
|
||||
self.assertEqual(pgvector.collection_name, "test_collection")
|
||||
self.assertEqual(pgvector.embedding_model_dims, 3)
|
||||
@@ -179,8 +179,8 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Verify vector extension and table creation
|
||||
self.mock_cursor.execute.assert_any_call("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
table_creation_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "CREATE TABLE IF NOT EXISTS test_collection" in str(call)]
|
||||
table_creation_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "CREATE TABLE IF NOT EXISTS" in str(call) and "test_collection" in str(call)]
|
||||
self.assertTrue(len(table_creation_calls) > 0)
|
||||
|
||||
# Verify pgvector instance properties
|
||||
@@ -233,7 +233,7 @@ class TestPGVector(unittest.TestCase):
|
||||
# Verify vector extension and table creation
|
||||
self.mock_cursor.execute.assert_any_call("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
table_creation_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "CREATE TABLE IF NOT EXISTS test_collection" in str(call)]
|
||||
if "CREATE TABLE IF NOT EXISTS" in str(call) and "test_collection" in str(call)]
|
||||
self.assertTrue(len(table_creation_calls) > 0)
|
||||
|
||||
# Verify pgvector instance properties
|
||||
@@ -277,7 +277,7 @@ class TestPGVector(unittest.TestCase):
|
||||
# Verify vector extension and table creation
|
||||
self.mock_cursor.execute.assert_any_call("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
table_creation_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "CREATE TABLE IF NOT EXISTS test_collection" in str(call)]
|
||||
if "CREATE TABLE IF NOT EXISTS" in str(call) and "test_collection" in str(call)]
|
||||
self.assertTrue(len(table_creation_calls) > 0)
|
||||
|
||||
# Verify pgvector instance properties
|
||||
@@ -319,7 +319,7 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Verify insert query was executed (psycopg3 uses executemany)
|
||||
insert_calls = [call for call in self.mock_cursor.executemany.call_args_list
|
||||
if "INSERT INTO test_collection" in str(call)]
|
||||
if "INSERT INTO" in str(call) and "test_collection" in str(call)]
|
||||
self.assertTrue(len(insert_calls) > 0)
|
||||
|
||||
# Verify data format
|
||||
@@ -392,7 +392,9 @@ class TestPGVector(unittest.TestCase):
|
||||
mock_execute_values.assert_called_once()
|
||||
call_args = mock_execute_values.call_args
|
||||
|
||||
self.assertIn("INSERT INTO test_collection", call_args[0][1])
|
||||
mock_psycopg2.sql.SQL.assert_any_call(
|
||||
"INSERT INTO {} (id, vector, payload) VALUES %s"
|
||||
)
|
||||
|
||||
# The data argument should be a list of tuples, one per vector
|
||||
data_arg = call_args[0][2]
|
||||
@@ -400,6 +402,9 @@ class TestPGVector(unittest.TestCase):
|
||||
self.assertEqual(data_arg[0][0], self.test_ids[0])
|
||||
self.assertEqual(data_arg[1][0], self.test_ids[1])
|
||||
|
||||
# Restore the module after the sys.modules patch reverts
|
||||
importlib.reload(sys.modules['mem0.vector_stores.pgvector'])
|
||||
|
||||
@patch('mem0.vector_stores.pgvector.PSYCOPG_VERSION', 3)
|
||||
@patch('mem0.vector_stores.pgvector.ConnectionPool')
|
||||
@patch.object(PGVector, '_get_cursor')
|
||||
@@ -534,7 +539,7 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Verify delete query was executed
|
||||
delete_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "DELETE FROM test_collection" in str(call)]
|
||||
if "DELETE FROM" in str(call) and "test_collection" in str(call)]
|
||||
self.assertTrue(len(delete_calls) > 0)
|
||||
|
||||
@patch('mem0.vector_stores.pgvector.PSYCOPG_VERSION', 2)
|
||||
@@ -573,7 +578,7 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Verify delete query was executed
|
||||
delete_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "DELETE FROM test_collection" in str(call)]
|
||||
if "DELETE FROM" in str(call) and "test_collection" in str(call)]
|
||||
self.assertTrue(len(delete_calls) > 0)
|
||||
|
||||
@patch('mem0.vector_stores.pgvector.PSYCOPG_VERSION', 3)
|
||||
@@ -615,7 +620,7 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Verify update queries were executed
|
||||
update_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "UPDATE test_collection" in str(call)]
|
||||
if "UPDATE" in str(call) and "test_collection" in str(call)]
|
||||
self.assertTrue(len(update_calls) > 0)
|
||||
|
||||
@patch('mem0.vector_stores.pgvector.PSYCOPG_VERSION', 2)
|
||||
@@ -657,7 +662,7 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Verify update queries were executed
|
||||
update_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "UPDATE test_collection" in str(call)]
|
||||
if "UPDATE" in str(call) and "test_collection" in str(call)]
|
||||
self.assertTrue(len(update_calls) > 0)
|
||||
|
||||
@patch('mem0.vector_stores.pgvector.PSYCOPG_VERSION', 3)
|
||||
@@ -867,7 +872,7 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Verify delete_col query was executed
|
||||
delete_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "DROP TABLE IF EXISTS test_collection" in str(call)]
|
||||
if "DROP TABLE IF EXISTS" in str(call) and "test_collection" in str(call)]
|
||||
self.assertTrue(len(delete_calls) > 0)
|
||||
|
||||
@patch('mem0.vector_stores.pgvector.PSYCOPG_VERSION', 2)
|
||||
@@ -906,7 +911,7 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Verify delete_col query was executed
|
||||
delete_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "DROP TABLE IF EXISTS test_collection" in str(call)]
|
||||
if "DROP TABLE IF EXISTS" in str(call) and "test_collection" in str(call)]
|
||||
self.assertTrue(len(delete_calls) > 0)
|
||||
|
||||
@patch('mem0.vector_stores.pgvector.PSYCOPG_VERSION', 3)
|
||||
@@ -1802,7 +1807,7 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Verify the update query was executed
|
||||
update_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "UPDATE test_collection SET payload" in str(call)]
|
||||
if "UPDATE" in str(call) and "test_collection" in str(call) and "SET payload" in str(call)]
|
||||
self.assertTrue(len(update_calls) > 0)
|
||||
|
||||
@patch('mem0.vector_stores.pgvector.PSYCOPG_VERSION', 2)
|
||||
@@ -1843,7 +1848,7 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Verify the update query was executed
|
||||
update_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "UPDATE test_collection SET payload" in str(call)]
|
||||
if "UPDATE" in str(call) and "test_collection" in str(call) and "SET payload" in str(call)]
|
||||
self.assertTrue(len(update_calls) > 0)
|
||||
|
||||
@patch('mem0.vector_stores.pgvector.PSYCOPG_VERSION', 2)
|
||||
@@ -1861,7 +1866,7 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Only raise exception on the delete operation, not during setup
|
||||
def execute_side_effect(*args, **kwargs):
|
||||
if args and "DELETE FROM" in str(args[0]):
|
||||
if args and ("DELETE FROM" in str(args[0]) or "DELETE" in repr(args[0])):
|
||||
raise Exception("Database error")
|
||||
return MagicMock()
|
||||
mock_cursor.execute.side_effect = execute_side_effect
|
||||
@@ -2003,9 +2008,9 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Verify only vector update query was executed (not payload)
|
||||
vector_update_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "UPDATE test_collection SET vector" in str(call) and "payload" not in str(call)]
|
||||
if "UPDATE" in str(call) and "test_collection" in str(call) and "SET vector" in str(call) and "payload" not in str(call)]
|
||||
payload_update_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "UPDATE test_collection SET payload" in str(call)]
|
||||
if "UPDATE" in str(call) and "test_collection" in str(call) and "SET payload" in str(call)]
|
||||
|
||||
self.assertTrue(len(vector_update_calls) > 0)
|
||||
self.assertEqual(len(payload_update_calls), 0)
|
||||
@@ -2045,9 +2050,9 @@ class TestPGVector(unittest.TestCase):
|
||||
|
||||
# Verify both vector and payload update queries were executed
|
||||
vector_update_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "UPDATE test_collection SET vector" in str(call)]
|
||||
if "UPDATE" in str(call) and "test_collection" in str(call) and "SET vector" in str(call)]
|
||||
payload_update_calls = [call for call in self.mock_cursor.execute.call_args_list
|
||||
if "UPDATE test_collection SET payload" in str(call)]
|
||||
if "UPDATE" in str(call) and "test_collection" in str(call) and "SET payload" in str(call)]
|
||||
|
||||
self.assertTrue(len(vector_update_calls) > 0)
|
||||
self.assertTrue(len(payload_update_calls) > 0)
|
||||
|
||||
Reference in New Issue
Block a user