Compare commits

...

17 Commits

Author SHA1 Message Date
Himanshu-Sangshetti 339d1e35f8 docs(integrations): add Zapier and n8n docs; align n8n toolchain and codex
- Add docs/integrations/zapier.mdx and n8n.mdx, register both in docs.json
  (Developer Tools) and docs/llms.txt
- Align n8n CD toolchain (pnpm 9 / Node 20) with the checks workflow so the
  published artifact is built on the toolchain CI validated
- Add Mem0.node.json codex and copy it into dist via gulp for nodes-panel
  discoverability
- Align search default limit to 50 across Zapier and n8n
2026-07-20 12:23:56 +05:30
Himanshu-Sangshetti cf861ebf5a fix(integrations): address review feedback on Zapier and n8n
Zapier:
- Bound the poll budget under Zapier's step timeout and default Wait for
  Completion off; timeout message notes the add likely still succeeded
- Add offline unit tests (mocked z.request) and run them in CI
- Expose a Page input on Get Memories
- Encode memory_id in the delete path
- Update E2E to test the default no-wait path (add then search-retry)

n8n:
- Clarify the Infer description (controls extraction, not waiting)
- Add an App ID field and require at least one entity id before add
- Pass itemIndex into pollEvent errors
- Unwrap event results so wait-path output matches search and get many
2026-07-20 11:55:29 +05:30
Himanshu-Sangshetti 43901527a3 ci(integrations): fix n8n + Zapier checks (skip native builds, Node 22)
- n8n: add --ignore-scripts to pnpm install (checks + cd). n8n-workflow
  transitively pulls isolated-vm, whose native build fails on the CI
  runner and isn't needed for lint/build/publish (types + eslint only).
- Zapier: bump checks/cd to Node 22 — zapier-platform-cli@19 requires
  Node >= 22 (validate failed on Node 20).
2026-07-20 11:55:29 +05:30
Himanshu-Sangshetti 0ff023133c chore(integrations): address review — Zapier LICENSE, docs, tidy job names
- Add MIT LICENSE to zapier-mem0 (package.json declared MIT but shipped no
  file; every peer commits one) + keywords/author/homepage for parity.
- Document the n8n and Zapier checks workflows in the AGENTS.md CI table.
- Title-case the ci-gate job display names (n8n Node, Zapier App) to match
  the other integration jobs.
2026-07-20 11:55:29 +05:30
Himanshu-Sangshetti 98ae733f07 ci(integrations): add CI/CD workflows for n8n + Zapier packages
Match the repo's per-integration CI/CD convention (vercel-ai / pi-agent
pattern):

- n8n-nodes-mem0-checks.yml (lint + build + dist verify) and
  n8n-nodes-mem0-cd.yml (npm publish --provenance via OIDC, dispatched by
  the release router on tag n8n-nodes-mem0-v*).
- zapier-mem0-checks.yml (zapier validate) and zapier-mem0-cd.yml (manual
  zapier push; requires ZAPIER_DEPLOY_KEY). Zapier deploys to Zapier's
  platform, not npm, so it is not in the release router.
- Wire both packages into ci-gate.yml (path filters + call jobs + gate).
- Register the n8n tag prefix in release.yml and document it + the Zapier
  manual-deploy in AGENTS.md.
- Skip the Zapier E2E suite when MEM0_API_KEY is absent so CI is green
  offline (it still runs locally with a key).
2026-07-20 11:55:29 +05:30
Himanshu-Sangshetti 00aa647f78 chore(integrations): use pnpm, real Mem0 icon, fix Zapier field defaults
- Switch both packages from npm to pnpm (pnpm-lock.yaml) to match the
  repo convention (openclaw / pi-agent-plugin / vercel-ai-sdk); gitignore
  package-lock.json.
- Replace the placeholder n8n node icon with the real Mem0 mark (same
  asset used by integrations/mem0-plugin).
- Zapier: revert boolean/integer field defaults to strings — Zapier's
  schema requires string defaults (zapier validate now passes with 0
  errors). Runtime coercion (String(x)!=='false', Number()) is unchanged.
2026-07-20 11:55:29 +05:30
Himanshu-Sangshetti e66e97b725 fix(integrations): address code-review findings
n8n: throw on FAILED memory events (was silently returned), poll on any
non-terminal status (PENDING or RUNNING), use top_k (not limit) for
search, coerce search/getAll responses to arrays, guard invalid-JSON
metadata, reject empty updates, mark user_id required; lint clean.

Zapier: coerce boolean fields (String(x) !== 'false') + boolean defaults
so "Wait"/"Infer = No" are honored, throw on HTTP >= 400 (was silent
empty/fake-success), throw on FAILED events, use top_k, numeric coercion
with clamping, mark user_id required.
2026-07-20 11:55:29 +05:30
Himanshu-Sangshetti 9882eea7b8 feat(integrations): add Zapier integration for Mem0
Zapier Platform CLI app (zapier-mem0) with Add Memory / Delete Memory
creates and Search Memories / Get Memories searches against the Mem0 v3
API. Custom API-key auth (Authorization: Token), base-URL + auth
injected via beforeRequest middleware, non-2xx surfaced as errors.

Add Memory polls GET /v1/event/{id}/ for async extraction. Includes an
E2E test suite via createAppTester.
2026-07-20 11:55:29 +05:30
Himanshu-Sangshetti 4464dca825 feat(integrations): add n8n community node for Mem0
Programmatic n8n node (n8n-nodes-mem0) with Add/Search/Get Many/Get/
Update/Delete memory operations against the Mem0 v3 API, plus API-key
credentials and usableAsTool support for the AI Agent node.

Add uses POST /v3/memories/add/ and polls GET /v1/event/{id}/ until the
async extraction resolves. No runtime dependencies (n8n verification
requirement); calls the REST API directly. MIT licensed per-package.
2026-07-20 11:55:29 +05:30
JainamShah-22 9383e9a255 fix(server): scope auth DB sessions to prevent connection-pool exhaustion (#6237) 2026-07-20 00:07:50 +05:30
Jupiter ddaa655edf fix(llms): honor OPENAI_BASE_URL in structured provider (#6322) 2026-07-17 20:45:58 +05:30
youneshima 739534c0a3 docs: use CLI commands for Claude Code plugin install steps (#6341) 2026-07-16 11:00:50 +05:30
Kartik 633b035342 feat(ts-sdk): add AWS Bedrock embedding provider (#6185) 2026-07-15 14:27:07 +05:30
Kartik ccbe5861a1 docs: remove criteria retrieval docs for non-existent feature (#6282) 2026-07-14 20:06:05 +05:30
Kartik 50c3cf44f1 refactor(sdk): remove dead retrieval_criteria parameter (#6313) 2026-07-14 20:03:53 +05:30
Kartik d6d2588ef5 fix(mem0-plugin): store assistant-authored summaries with role="assistant" (#6316) 2026-07-14 20:03:36 +05:30
Kartik 6c1741e3a4 docs: fold contextual-add into the Add concept page and redirect (#6286) 2026-07-14 20:02:47 +05:30
77 changed files with 8951 additions and 662 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
"name": "mem0",
"source": "./integrations/mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
"version": "0.2.12"
"version": "0.2.13"
}
]
}
+1 -1
View File
@@ -12,7 +12,7 @@
"name": "mem0",
"source": "./integrations/mem0-plugin",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
"version": "0.2.12"
"version": "0.2.13"
}
]
}
+26
View File
@@ -40,6 +40,8 @@ jobs:
openclaw: ${{ steps.filter.outputs.openclaw }}
opencode_plugin: ${{ steps.filter.outputs.opencode_plugin }}
pi_agent_plugin: ${{ steps.filter.outputs.pi_agent_plugin }}
n8n_nodes_mem0: ${{ steps.filter.outputs.n8n_nodes_mem0 }}
zapier_mem0: ${{ steps.filter.outputs.zapier_mem0 }}
docs_llms_txt: ${{ steps.filter.outputs.docs_llms_txt }}
steps:
- uses: dorny/paths-filter@v3
@@ -79,6 +81,14 @@ jobs:
- 'integrations/pi-agent-plugin/**'
- '.github/workflows/pi-agent-plugin-checks.yml'
- '.github/workflows/ci-gate.yml'
n8n_nodes_mem0:
- 'integrations/n8n-nodes-mem0/**'
- '.github/workflows/n8n-nodes-mem0-checks.yml'
- '.github/workflows/ci-gate.yml'
zapier_mem0:
- 'integrations/zapier-mem0/**'
- '.github/workflows/zapier-mem0-checks.yml'
- '.github/workflows/ci-gate.yml'
docs_llms_txt:
- 'docs/**/*.mdx'
- 'docs/llms.txt'
@@ -136,6 +146,20 @@ jobs:
uses: ./.github/workflows/pi-agent-plugin-checks.yml
secrets: inherit
n8n-nodes-mem0:
name: n8n Node
needs: changes
if: needs.changes.outputs.n8n_nodes_mem0 == 'true'
uses: ./.github/workflows/n8n-nodes-mem0-checks.yml
secrets: inherit
zapier-mem0:
name: Zapier App
needs: changes
if: needs.changes.outputs.zapier_mem0 == 'true'
uses: ./.github/workflows/zapier-mem0-checks.yml
secrets: inherit
docs-llms-txt:
name: docs llms.txt
needs: changes
@@ -154,6 +178,8 @@ jobs:
- openclaw
- opencode-plugin
- pi-agent-plugin
- n8n-nodes-mem0
- zapier-mem0
- docs-llms-txt
if: always()
runs-on: ubuntu-latest
+60
View File
@@ -0,0 +1,60 @@
name: Publish n8n-nodes-mem0 📦 to npm
# Dispatched by release.yml (Release Router) when a release tagged
# n8n-nodes-mem0-v* is published. Can also be dispatched manually to
# re-publish a tag.
on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to build and publish (e.g. n8n-nodes-mem0-v0.1.0)'
required: true
type: string
prerelease:
description: 'Publish under the version preid dist-tag instead of latest'
required: false
type: boolean
default: false
jobs:
build-n-publish:
name: Build and publish n8n-nodes-mem0 📦 to npm
if: startsWith(inputs.tag, 'n8n-nodes-mem0-v')
runs-on: ubuntu-latest
permissions:
id-token: write
defaults:
run:
working-directory: integrations/n8n-nodes-mem0
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: integrations/n8n-nodes-mem0/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Build
run: pnpm run build
- name: Publish to npm
run: |
if [ "${{ inputs.prerelease }}" = "true" ]; then
PREID=$(node -p "require('./package.json').version.split('-')[1].split('.')[0]")
npx npm@latest publish --provenance --access public --tag "$PREID"
else
npx npm@latest publish --provenance --access public
fi
@@ -0,0 +1,65 @@
name: n8n-nodes-mem0 checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/n8n-nodes-mem0/**'
- '.github/workflows/n8n-nodes-mem0-checks.yml'
workflow_call:
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/n8n-nodes-mem0/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/n8n-nodes-mem0 && pnpm install --frozen-lockfile --ignore-scripts
- name: Lint
run: cd integrations/n8n-nodes-mem0 && pnpm run lint
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
cache-dependency-path: integrations/n8n-nodes-mem0/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/n8n-nodes-mem0 && pnpm install --frozen-lockfile --ignore-scripts
- name: Build
run: cd integrations/n8n-nodes-mem0 && pnpm run build
- name: Verify dist output exists
run: |
test -f integrations/n8n-nodes-mem0/dist/nodes/Mem0/Mem0.node.js || (echo "Build output missing: dist/nodes/Mem0/Mem0.node.js" && exit 1)
test -f integrations/n8n-nodes-mem0/dist/credentials/Mem0Api.credentials.js || (echo "Build output missing: dist/credentials/Mem0Api.credentials.js" && exit 1)
test -f integrations/n8n-nodes-mem0/dist/nodes/Mem0/mem0.svg || (echo "Build output missing: dist/nodes/Mem0/mem0.svg" && exit 1)
+1
View File
@@ -45,6 +45,7 @@ jobs:
openclaw-v*) workflow="openclaw-cd.yml" ;;
opencode-v*) workflow="opencode-plugin-cd.yml" ;;
pi-agent-v*) workflow="pi-agent-plugin-cd.yml" ;;
n8n-nodes-mem0-v*) workflow="n8n-nodes-mem0-cd.yml" ;;
v*) workflow="cd.yml" ;;
*)
echo "::error::Release tag '$TAG' does not match any known package prefix — nothing will be published. See the tag prefix table in AGENTS.md."
+39
View File
@@ -0,0 +1,39 @@
name: Deploy zapier-mem0 to Zapier
# Zapier apps deploy to Zapier's own platform (not npm), so this is NOT wired
# into the npm release router (release.yml). It is manual workflow_dispatch
# only and requires the ZAPIER_DEPLOY_KEY repo secret.
#
# gh workflow run zapier-mem0-cd.yml --ref main
on:
workflow_dispatch:
jobs:
push:
name: Push zapier-mem0 to Zapier
runs-on: ubuntu-latest
defaults:
run:
working-directory: integrations/zapier-mem0
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
cache-dependency-path: integrations/zapier-mem0/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Push to Zapier
env:
ZAPIER_DEPLOY_KEY: ${{ secrets.ZAPIER_DEPLOY_KEY }}
run: npx zapier-platform-cli@19 push
+44
View File
@@ -0,0 +1,44 @@
name: zapier-mem0 checks
# On PRs this is invoked by ci-gate.yml (the single required check);
# push-to-main and manual runs remain standalone.
#
# CI runs `zapier validate` (offline schema + style checks) plus the offline
# jest unit suite (test/unit.test.js — mocked z.request, no network). The
# end-to-end jest suite is skipped here because it hits the live Mem0 API — it
# runs locally with MEM0_API_KEY set (see the package README).
on:
workflow_dispatch:
push:
branches: [main]
paths:
- 'integrations/zapier-mem0/**'
- '.github/workflows/zapier-mem0-checks.yml'
workflow_call:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
cache-dependency-path: integrations/zapier-mem0/pnpm-lock.yaml
- name: Install dependencies
run: cd integrations/zapier-mem0 && pnpm install --frozen-lockfile
- name: Validate Zapier app definition
run: cd integrations/zapier-mem0 && npx zapier-platform-cli@19 validate
- name: Run offline unit tests
run: cd integrations/zapier-mem0 && pnpm test:unit
+4
View File
@@ -431,6 +431,8 @@ PR testing is orchestrated by a single entry point: **`ci-gate.yml` (CI Gate)**
| OpenClaw | `openclaw-checks.yml` | Push to main (on `integrations/openclaw/`), manual | tsc + vitest (with Codecov) + tsup build on Node 20, 22 |
| OpenCode Plugin | `opencode-plugin-checks.yml` | Push to main (on `integrations/mem0-plugin/.opencode-plugin/`), manual | Bun: tsc type-check + build + dist artifact check |
| Pi Agent Plugin | `pi-agent-plugin-checks.yml` | Push to main (on `integrations/pi-agent-plugin/`), manual | tsc + vitest + tsup build (dist artifact check) on Node 20, 22 |
| n8n Node | `n8n-nodes-mem0-checks.yml` | Push to main (on `integrations/n8n-nodes-mem0/`), manual | ESLint (n8n-nodes-base) + tsc build (dist artifact check) on Node 20 |
| Zapier App | `zapier-mem0-checks.yml` | Push to main (on `integrations/zapier-mem0/`), manual | `zapier validate` (schema + style checks) on Node 20 |
| docs llms.txt | `docs-llms-txt-check.yml` | Manual | `docs/llms.txt` coverage check |
When adding a new package CI workflow: give it `workflow_call` (plus `push`/`workflow_dispatch` as needed, but no `pull_request` trigger), then register it in `ci-gate.yml` — a path filter under the `changes` job, a call job, and an entry in the gate job's `needs` list.
@@ -450,11 +452,13 @@ Publishing is routed through a single entry point: **`release.yml` (Release Rout
| OpenClaw | `openclaw-cd.yml` | `openclaw-v*` | npm (`@mem0/openclaw-mem0`) |
| OpenCode Plugin | `opencode-plugin-cd.yml` | `opencode-v*` | npm (`@mem0/opencode-plugin`) |
| Pi Agent Plugin | `pi-agent-plugin-cd.yml` | `pi-agent-v*` | npm (`@mem0/pi-agent-plugin`) |
| n8n Node | `n8n-nodes-mem0-cd.yml` | `n8n-nodes-mem0-v*` | npm (`n8n-nodes-mem0`) |
- Package CD workflows are `workflow_dispatch`-only (inputs: `tag`, `prerelease`); they check out and build the given tag. Registry trusted-publisher settings stay pinned to each package's own workflow filename.
- All publishing uses **OIDC trusted publishing** — no tokens or secrets required.
- First publish of a new npm package must be done manually; OIDC works for subsequent versions.
- To re-publish a release (e.g. after a registry settings fix), do **not** delete/recreate the GitHub release — manually dispatch the package workflow instead: `gh workflow run <package>-cd.yml --ref refs/tags/<tag> -f tag=<tag>`.
- The **Zapier app** (`integrations/zapier-mem0`) deploys to Zapier's own platform, not npm, so it is **not** in the release router. Deploy it manually: `gh workflow run zapier-mem0-cd.yml --ref main` (requires the `ZAPIER_DEPLOY_KEY` secret).
- When adding a new package: add its CD workflow (`workflow_dispatch` with `tag`/`prerelease` inputs), then register its tag prefix in the `case` block in `release.yml`. Keep the bare `v*` arm last.
### Utility Workflows
+1 -37
View File
@@ -79,7 +79,7 @@ new_project = client.project.create(
### Update Project Settings
Modify project configuration including custom instructions, categories, language preferences, retrieval criteria, and memory decay:
Modify project configuration including custom instructions, categories, language preferences, and memory decay:
```python
# Update project with custom categories
@@ -98,14 +98,6 @@ client.project.update(
# Use the input language for memory storage and retrieval
client.project.update(multilingual=True)
# Set retrieval criteria to control which memories are surfaced in search
client.project.update(
retrieval_criteria=[
{"name": "relevance", "description": "How directly relevant this memory is to the current topic or user query", "weight": 3},
{"name": "access_frequency", "description": "How often this memory has been accessed or surfaced recently", "weight": 1}
]
)
# Enable Memory Decay (boosts recently-accessed memories at search time)
client.project.update(decay=True)
@@ -120,34 +112,6 @@ client.project.update(
)
```
#### Set Retrieval Criteria
`retrieval_criteria` is a per-project list of dictionaries (`List[Dict]`) that shapes how memories are ranked and filtered during search. Each dictionary has three fields: `name` (identifier), `description` (interpreted by the LLM to score each memory), and `weight` (relative influence on the final score). Use this to focus retrieval on intent-aligned or signal-specific memories:
```python
client.project.update(
retrieval_criteria=[
{
"name": "joy",
"description": "Measure the intensity of positive emotions such as happiness, excitement, or amusement expressed in the memory. A higher score reflects greater joy.",
"weight": 3
},
{
"name": "curiosity",
"description": "Assess the extent to which the memory reflects inquisitiveness or interest in exploring new information. A higher score reflects stronger curiosity.",
"weight": 2
},
{
"name": "access_frequency",
"description": "How often this memory has been accessed or surfaced recently.",
"weight": 1
}
]
)
```
Pass an empty list to clear all criteria and restore default retrieval behaviour.
#### Toggle Memory Decay
`decay` is a per-project boolean that turns on [Memory Decay](/platform/features/memory-decay): a search-time ranking bias that reinforces recently-accessed memories and gently dampens stale ones. The flag is `false` by default; set it via the same project-update endpoint:
+18
View File
@@ -1825,6 +1825,15 @@ A full-featured command-line interface for Mem0, available in both Python and No
<Tabs>
<Tab title="Mem0 Plugin">
<Update label="2026-07-14" description="mem0-plugin v0.2.13">
**Fixes:**
- **Assistant messages no longer stored as your own:** The session-summary hook (fires at the end of every assistant turn) and the post-compaction hook were sending the assistant's own message to Mem0 tagged `role: "user"`. Because Mem0 extracts *facts about the user* from each message and uses `role` to decide who spoke, the assistant's first-person prose was being saved as the human's stated preferences — "I recommend we drop Redis" became `User prefers dropping Redis entirely`. Both hooks now send `role: "assistant"`, so the same session is stored as `Assistant recommended...`. Affects Claude Code, Cursor, Codex, and Antigravity, which share these hooks.
Existing memories written by the previous versions are not rewritten. If your memories contain preferences you never expressed, delete them — the plugin will not recreate them.
</Update>
<Update label="2026-06-30" description="mem0-plugin v0.2.12">
**New Features:**
@@ -2148,6 +2157,15 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
<Tab title="Antigravity">
<Update label="2026-07-14" description="Antigravity plugin v0.1.5">
**Fixes:**
- **Assistant messages no longer stored as your own:** The session-summary hook (fires at the end of every assistant turn) and the post-compaction hook were sending the assistant's own message to Mem0 tagged `role: "user"`. Because Mem0 extracts *facts about the user* from each message and uses `role` to decide who spoke, the assistant's first-person prose was being saved as the human's stated preferences — "I recommend we drop Redis" became `User prefers dropping Redis entirely`. Both hooks now send `role: "assistant"`, so the same session is stored as `Assistant recommended...`.
Existing memories written by the previous versions are not rewritten. If your memories contain preferences you never expressed, delete them — the plugin will not recreate them.
</Update>
<Update label="2026-06-30" description="Antigravity plugin v0.1.4">
**New Features:**
@@ -3,11 +3,27 @@ title: AWS Bedrock
description: "Configure AWS Bedrock as an embedding provider in Mem0 with IAM credentials and boto3 authentication."
---
To use AWS Bedrock embedding models, you need to have the appropriate AWS credentials and permissions. The embeddings implementation relies on the `boto3` library.
To use AWS Bedrock embedding models, you need the appropriate AWS credentials and permissions. Python uses `boto3`, and TypeScript uses `@aws-sdk/client-bedrock-runtime`.
Both SDKs support the Amazon Titan and Cohere embedding model families.
### Setup
- Ensure you have model access from the [AWS Bedrock Console](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess)
- Authenticate the boto3 client using a method described in the [AWS documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html)
- Model access is automatic: Bedrock enables serverless foundation models on first invocation in AWS commercial regions, and the [Model access page has been retired](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html). Cohere models are served from AWS Marketplace, so an account's first invocation must come from a principal with the `aws-marketplace:Subscribe` permission; after that, any user in the account can invoke them. Browse the models available to you in the [Bedrock model catalog](https://console.aws.amazon.com/bedrock/).
- Install the AWS client for your language:
<CodeGroup>
```bash Python
pip install boto3
```
```bash TypeScript
npm install @aws-sdk/client-bedrock-runtime
```
</CodeGroup>
In TypeScript this package is an optional peer dependency, so it is only required when you actually use the Bedrock embedder.
- Set up environment variables for authentication:
```bash
export AWS_REGION=us-east-1
@@ -15,6 +31,8 @@ To use AWS Bedrock embedding models, you need to have the appropriate AWS creden
export AWS_SECRET_ACCESS_KEY=your-secret-key
```
Both SDKs fall back to the standard AWS credential chain (environment variables, shared config, SSO, or an instance role) when you do not pass credentials in the config, so you rarely need to hardcode keys. See the [boto3 credentials guide](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html) for the Python resolution order.
### Usage
<CodeGroup>
@@ -48,8 +66,46 @@ messages = [
]
m.add(messages, user_id="alice")
```
```typescript TypeScript
import { Memory } from "mem0ai/oss";
// Credentials are read from the AWS default chain (AWS_REGION,
// AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, SSO, or an instance role).
const memory = new Memory({
embedder: {
provider: "aws_bedrock",
config: {
model: "amazon.titan-embed-text-v2:0",
awsRegion: "us-west-2",
},
},
});
const messages = [
{ role: "user", content: "I'm planning to watch a movie tonight. Any recommendations?" },
{ role: "assistant", content: "How about thriller movies? They can be quite engaging." },
{ role: "user", content: "I'm not a big fan of thriller movies but I love sci-fi movies." },
{ role: "assistant", content: "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future." },
];
await memory.add(messages, { userId: "alice" });
```
</CodeGroup>
### Choosing a model
| Model | Notes |
| --- | --- |
| `amazon.titan-embed-text-v1` | Default. Fixed 1536-dimension output. |
| `amazon.titan-embed-text-v2:0` | Supports a configurable output size of 256, 512, or 1024. |
| `cohere.embed-english-v3` | English text. Embeds up to 96 texts per request. |
| `cohere.embed-multilingual-v3` | Multilingual text. Embeds up to 96 texts per request. |
| `cohere.embed-v4:0` | Text. Embeds up to 96 texts per request. Supports a configurable output size of 256, 512, 1024, or 1536. TypeScript only. |
Custom output sizes are model specific. In Python, only Titan Text Embeddings V2 accepts one. In TypeScript, Titan Text Embeddings V2 and Cohere Embed v4 both do, and `embeddingDims` is ignored on Titan V1 and on Cohere v3, which have no such parameter. When you do set it, make sure your vector store dimension matches, otherwise inserts will fail.
Bedrock caps a Cohere embedding call at 96 texts. The TypeScript SDK splits larger batches into multiple requests for you, so a 200 text batch becomes 3 calls.
### Config
Here are the parameters available for configuring AWS Bedrock embedder:
@@ -64,4 +120,16 @@ Here are the parameters available for configuring AWS Bedrock embedder:
| `aws_secret_access_key` | AWS secret access key for authentication | `None` |
| `aws_session_token` | AWS session token for temporary credentials | `None` |
</Tab>
<Tab title="TypeScript">
| Parameter | Description | Default Value |
| --- | --- | --- |
| `model` | The name of the embedding model to use | `amazon.titan-embed-text-v1` |
| `awsRegion` | AWS region for the Bedrock client. Falls back to the `AWS_REGION` environment variable | `us-west-2` |
| `embeddingDims` | Output vector size. Titan Text Embeddings V2 (256, 512, or 1024) and Cohere Embed v4 (256, 512, 1024, or 1536) only | `undefined` |
| `awsAccessKeyId` | AWS access key ID for authentication | `undefined` |
| `awsSecretAccessKey` | AWS secret access key for authentication | `undefined` |
| `awsSessionToken` | AWS session token for temporary credentials | `undefined` |
Omit the three credential fields to use the AWS default credential chain. If you do pass them, `awsAccessKeyId` and `awsSecretAccessKey` are both required.
</Tab>
</Tabs>
+1 -1
View File
@@ -10,7 +10,7 @@ Mem0 offers support for various embedding models, allowing users to choose the o
See the list of supported embedders below.
<Note>
All embedders listed below are supported in the Python implementation. The TypeScript implementation supports: **OpenAI**, **Azure OpenAI**, **FastEmbed**, **Google AI**, **Langchain**, **LM Studio**, **Ollama**, and **Together**.
All embedders listed below are supported in the Python implementation. The TypeScript implementation supports: **OpenAI**, **Azure OpenAI**, **AWS Bedrock**, **FastEmbed**, **Google AI**, **Hugging Face**, **Langchain**, **LM Studio**, **Ollama**, **Together**, and **Vertex AI**.
</Note>
<CardGroup cols={4}>
@@ -83,6 +83,50 @@ await client.add(messages, {
Expect a `status: "PENDING"` response with an `event_id`. Poll `GET /v1/event/{event_id}/` to confirm completion.
</Info>
### Automatic conversation context
On the Platform, you only send new messages. Mem0 automatically pulls the earlier messages that share the same identifiers (`user_id`, and `run_id` if you use one) and uses them as context when extracting memories, so you never need to resend conversation history.
This means a follow-up turn is understood against what came before it:
<CodeGroup>
```python Python
# First interaction
client.add(
[{"role": "user", "content": "My dog's name is Biscuit. He's a golden retriever."}],
user_id="alice",
)
# Later — send only the new turn, no history
client.add(
[{"role": "user", "content": "He turned 5 today, and I'm taking him to the vet on Friday."}],
user_id="alice",
)
# Stored as: "User's dog Biscuit turned 5" — "He" is resolved against the earlier turn.
```
```javascript JavaScript
// First interaction
await client.add(
[{ role: "user", content: "My dog's name is Biscuit. He's a golden retriever." }],
{ userId: "alice" },
);
// Later — send only the new turn, no history
await client.add(
[{ role: "user", content: "He turned 5 today, and I'm taking him to the vet on Friday." }],
{ userId: "alice" },
);
// Stored as: "User's dog Biscuit turned 5" — "He" is resolved against the earlier turn.
```
</CodeGroup>
Without that earlier turn, the same message can only be stored as "User's male pet turned 5", because there is nothing to resolve "He" against. Scope each conversation with a consistent `user_id` (plus `run_id` for a distinct session) and Mem0 handles the rest.
<Info>
This is default behavior and needs no configuration. Earlier SDK versions gated it behind a `version="v2"` argument on `add`; that argument no longer exists and is ignored if sent.
</Info>
## Add with Mem0 Open Source
<CodeGroup>
+10 -2
View File
@@ -84,8 +84,6 @@
"pages": [
"platform/features/advanced-retrieval",
"platform/advanced-memory-operations",
"platform/features/criteria-retrieval",
"platform/features/contextual-add",
"platform/features/custom-instructions",
"platform/features/memory-decay"
]
@@ -350,6 +348,8 @@
"pages": [
"integrations/dify",
"integrations/flowise",
"integrations/n8n",
"integrations/zapier",
"integrations/langchain-tools",
"integrations/agentops",
"integrations/respan",
@@ -609,6 +609,10 @@
]
},
"redirects": [
{
"source": "/platform/features/contextual-add",
"destination": "/core-concepts/memory-operations/add"
},
{
"source": "/changelog/openclaw",
"destination": "/changelog/sdk"
@@ -1228,6 +1232,10 @@
{
"source": "/open-source/multimodal-support",
"destination": "/open-source/features/multimodal-support"
},
{
"source": "/platform/features/criteria-retrieval",
"destination": "/platform/features/advanced-retrieval"
}
]
}
+3 -1
View File
@@ -65,7 +65,9 @@ The plugin uses the same shell scripts as Claude Code, Cursor, and Codex: hooks
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message |
| **Pre-tool** | `PreToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tools |
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
| **Stop** | `Stop` | Stores a session summary when the session ends |
| **Stop** | `Stop` | Stores a session summary at the end of every assistant turn (not just at session end) |
What you type is stored as yours. What the agent produces — session summaries and compaction summaries — is stored as the assistant's, so its suggestions never become your stated preferences.
## Troubleshooting
+20 -8
View File
@@ -44,14 +44,14 @@ Install the full plugin including MCP server, lifecycle hooks, and SDK skill.
1. Add the Mem0 marketplace:
```
/plugin marketplace add mem0ai/mem0
```bash
claude plugin marketplace add mem0ai/mem0
```
2. Install the plugin:
```
/plugin install mem0@mem0-plugins
```bash
claude plugin install mem0@mem0-plugins
```
**Claude Cowork desktop app:** Open the Cowork tab, click **Customize** in the sidebar, click **Browse plugins**, and install Mem0.
@@ -88,6 +88,15 @@ Add to your Claude Code MCP config (`.mcp.json`):
}
```
### Managing the Plugin
```bash
claude plugin update mem0@mem0-plugins # update the plugin to the latest version (restart to apply)
claude plugin marketplace update mem0-plugins # refresh the marketplace catalog
claude plugin uninstall mem0@mem0-plugins # uninstall the plugin (keeps the marketplace)
claude plugin marketplace remove mem0-plugins # unregister the marketplace entirely
```
<Info icon="check">
Start a new session and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
</Info>
@@ -143,9 +152,11 @@ When installed via the plugin marketplace, Mem0 hooks into Claude Code's lifecyc
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message; skips short prompts |
| **Pre-tool (3 handlers)** | `PreToolUse` | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context |
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
| **Stop** | `Stop` | Stores a session summary when the session ends |
| **Stop** | `Stop` | Stores a session summary at the end of every assistant turn (not just at session end) |
| **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted |
What you type is stored as yours. What Claude produces — session summaries and compaction summaries — is stored as the assistant's, so its suggestions never become your stated preferences.
## Example Workflow
```text
@@ -153,16 +164,17 @@ When installed via the plugin marketplace, Mem0 hooks into Claude Code's lifecyc
You: Let's refactor the auth module to use JWT tokens instead of sessions.
# Claude searches memories, finds nothing relevant, proceeds with the work.
# After completing the task, Mem0 stores:
# Mem0 stores what you said as yours:
# - Your preference: "Prefers TypeScript, uses ESLint"
# ...and what Claude did as the assistant's, in the session summary:
# - Decision: "Migrated auth from sessions to JWT tokens"
# - Files modified: auth/middleware.ts, auth/token.ts
# - User preference: "Prefers TypeScript, uses ESLint"
# Session 2 (days later): Related work
You: Add refresh token rotation to the auth system.
# Claude searches memories, retrieves the JWT migration context.
# Knows the file structure, decisions made, and user preferences.
# Knows the file structure, decisions made, and your stated preferences.
# Continues seamlessly without re-explaining the codebase.
```
+7 -4
View File
@@ -125,20 +125,23 @@ When installed via the plugin marketplace, Mem0 hooks into Codex's lifecycle to
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message |
| **Pre-tool (3 handlers)** | `PreToolUse` | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context |
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
| **Stop** | `Stop` | Stores a session summary when the session ends |
| **Stop** | `Stop` | Stores a session summary at the end of every assistant turn (not just at session end) |
| **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted |
What you type is stored as yours. What Codex produces — session summaries and compaction summaries — is stored as the assistant's, so its suggestions never become your stated preferences.
## Example Workflow
```text
# Task 1: Setting up a new service
You: Create a REST API for the notifications service using Express and TypeScript.
# Codex searches memories, finds user preferences from prior tasks.
# After completing the task, Mem0 stores:
# Codex searches memories, finds your preferences from prior tasks.
# Mem0 stores what you said as yours:
# - Your preference: "Prefers explicit error types over generic catch-all"
# ...and what Codex did as the assistant's, in the session summary:
# - Decision: "Notifications service uses Express + TypeScript + Zod validation"
# - Convention: "All API routes follow /api/v1/{resource} pattern"
# - Preference: "User prefers explicit error types over generic catch-all"
# Task 2 (days later): Extending the service
You: Add WebSocket support for real-time notification delivery.
+3 -2
View File
@@ -96,10 +96,11 @@ Once installed, the following tools are available in every Cursor session:
You: The API endpoint /users is taking 3 seconds. Help me optimize it.
# Cursor agent searches memories, proceeds with investigation.
# After completing the task, Mem0 stores:
# Mem0 stores what you said as yours:
# - Your preference: "Prefers query-level fixes over caching"
# ...and what the agent did as the assistant's:
# - Learning: "N+1 query in UserService.getAll(): fixed with eager loading"
# - Decision: "Added database index on users.email column"
# - Preference: "User prefers query-level fixes over caching"
# Session 2 (next week): Similar issue
You: The /orders endpoint is also slow, same pattern as before.
+72
View File
@@ -0,0 +1,72 @@
---
title: n8n
description: "Add long-term memory to n8n workflows and AI Agents with the Mem0 community node, no code required."
---
The [`n8n-nodes-mem0`](https://www.npmjs.com/package/n8n-nodes-mem0) community node brings [Mem0](https://mem0.ai) memory to [n8n](https://n8n.io). Add, search, and manage long-term memories inside any workflow, and use it as a tool for the n8n AI Agent.
## Overview
The node wraps the hosted Mem0 REST API and supports six operations on the **Memory** resource:
| Operation | Endpoint |
| --- | --- |
| **Add** | `POST /v3/memories/add/` |
| **Search** | `POST /v3/memories/search/` |
| **Get** | `GET /v1/memories/{id}/` |
| **Get Many** | `POST /v3/memories/` |
| **Update** | `PUT /v1/memories/{id}/` |
| **Delete** | `DELETE /v1/memories/{id}/` |
It is marked `usableAsTool`, so the n8n AI Agent (Tools Agent) can call it directly to remember and recall information.
## Installation
Install it like any n8n community node:
1. In n8n, go to **Settings → Community Nodes → Install**.
2. Enter `n8n-nodes-mem0` and confirm.
The node then appears in the nodes panel under the AI category.
## Authentication
Create a **Mem0 API** credential in n8n:
- **API Key**: from the <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-n8n" rel="nofollow">Mem0 API Key dashboard</a>. Sent as `Authorization: Token <key>`.
- **Base URL**: defaults to `https://api.mem0.ai`.
## Operations
### Add
Extracts and stores memories from one or more messages. Provide at least one entity id (**User ID**, **Agent ID**, **Run ID**, or **App ID** in Additional Fields); the node validates this before calling the API. Additional Fields also expose **Metadata (JSON)** and **Infer** (run LLM extraction, or store verbatim).
Extraction is asynchronous. **Wait for Completion** (on by default) polls the event until it finishes and returns the resulting memories; turn it off to return immediately with the event ID.
### Search
Semantic search over stored memories. Provide a **Query**, a **User ID** (required, the API needs an entity filter), and an optional **Limit**.
### Get Many
Lists memories for a **User ID** with **Page** and **Page Size** controls.
### Get / Update / Delete
Operate on a single memory by **Memory ID**. Update accepts new **Text** and/or **Metadata (JSON)**.
## Choosing a `userId`
The `userId` is a stable string you choose to identify whose memories these are. It is not looked up in the dashboard. Common choices are your app's internal user ID, an email, or a UUID. Use the same value across Add, Search, and Get Many so recall works.
<CardGroup cols={2}>
<Card title="Zapier Integration" icon="bolt" href="/integrations/zapier">
Add memory to Zaps across thousands of apps
</Card>
<Card title="Flowise Integration" icon="blocks" href="/integrations/flowise">
Add memory to Flowise chatflows
</Card>
</CardGroup>
<Snippet file="star-on-github.mdx" />
+61
View File
@@ -0,0 +1,61 @@
---
title: Zapier
description: "Add, search, and manage Mem0 memories from any Zap using the Mem0 Zapier app, no code required."
---
The [Mem0](https://mem0.ai) Zapier app lets you add, search, list, and delete long-term memories from any [Zapier](https://zapier.com) workflow. Wire "remember" and "recall" into thousands of apps without writing code.
## Overview
The app wraps the hosted Mem0 REST API and exposes four actions:
| Type | Action | Endpoint |
| --- | --- | --- |
| Create | **Add Memory** | `POST /v3/memories/add/` |
| Create | **Delete Memory** | `DELETE /v1/memories/{id}/` |
| Search | **Search Memories** | `POST /v3/memories/search/` |
| Search | **Get Memories** | `POST /v3/memories/` |
## Authentication
The app uses API key authentication. Grab a key from the <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-zapier" rel="nofollow">Mem0 API Key dashboard</a> and paste it when connecting your Mem0 account in Zapier. The key is sent as `Authorization: Token <key>` on every request.
## Actions
### Add Memory
Extracts and stores memories from a message. Fields:
- **Content** (required): the message text to extract memories from.
- **Role**: `user`, `assistant`, or `system`.
- **User ID / Agent ID / Run ID**: entity the memory belongs to.
- **Metadata (JSON)**: optional structured metadata.
- **Infer**: run LLM extraction (default) or store the message verbatim.
- **Wait for Completion**: off by default. Extraction is asynchronous, so the action returns immediately with an event ID. Turn this on to poll until extraction finishes and return the resulting memories. Extraction can take longer than a single Zapier step is allowed to run, so a timeout here does not mean the add failed; it usually still completes on the server.
### Search Memories
Semantic search over stored memories. Provide a **Query**, a **User ID** (required, the API needs an entity filter), and an optional **Limit**.
### Get Memories
Lists stored memories for a user. Provide a **User ID** (required), a **Limit** (page size), and a **Page** (1-based) to page through larger result sets.
### Delete Memory
Deletes a single memory by its **Memory ID**.
## Choosing a `userId`
The `user_id` is a stable string you choose to identify whose memories these are. It is not looked up in the dashboard. Common choices are your app's internal user ID, an email, or a UUID. Use the same value across Add, Search, and Get so recall works.
<CardGroup cols={2}>
<Card title="n8n Integration" icon="diagram-project" href="/integrations/n8n">
Build workflows with the Mem0 n8n community node
</Card>
<Card title="Flowise Integration" icon="blocks" href="/integrations/flowise">
Add memory to Flowise chatflows
</Card>
</CardGroup>
<Snippet file="star-on-github.mdx" />
+2 -2
View File
@@ -204,9 +204,7 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
### Features - Advanced Retrieval
- [Advanced Retrieval](https://docs.mem0.ai/platform/features/advanced-retrieval) [Platform]: Use when the user needs keyword search, reranking, or hybrid retrieval.
- [Criteria-Based Retrieval](https://docs.mem0.ai/platform/features/criteria-retrieval) [Platform]: Use when targeting memories by custom criteria, not just semantic similarity.
- [Temporal Reasoning](https://docs.mem0.ai/platform/features/temporal-reasoning) [Platform]: Use when time-aware searches like last week, upcoming, or right now need better result ordering.
- [Contextual Add](https://docs.mem0.ai/platform/features/contextual-add) [Platform]: Use when `add()` should consider the surrounding conversation, not just the latest turn.
- [Custom Instructions](https://docs.mem0.ai/platform/features/custom-instructions) [Platform]: Use when tailoring what Mem0 extracts and stores on Platform.
- [Memory Decay](https://docs.mem0.ai/platform/features/memory-decay) [Platform]: Use when search results should boost recently-reinforced memories and dampen stale ones. Opt in per project; applies at search time and never filters candidates out.
- [Advanced Memory Operations](https://docs.mem0.ai/platform/advanced-memory-operations) [Platform]: Use when basic CRUD is not enough - batch ops, complex filters, workflows.
@@ -283,6 +281,8 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
### Developer Tools
- [Dify](https://docs.mem0.ai/integrations/dify) [Both]: Use when the user is on Dify LLMOps.
- [Flowise](https://docs.mem0.ai/integrations/flowise) [Both]: Use when the user is on Flowise no-code.
- [n8n](https://docs.mem0.ai/integrations/n8n) [Both]: Use when the user builds workflows or AI agents in n8n.
- [Zapier](https://docs.mem0.ai/integrations/zapier) [Both]: Use when the user automates workflows with Zapier.
- [AgentOps](https://docs.mem0.ai/integrations/agentops) [Both]: Use when tracking agent observability with memory metadata.
- [Respan](https://docs.mem0.ai/integrations/respan) [Both]: Use when monitoring Mem0 with Respan (formerly Keywords AI) LLM observability.
- [Raycast](https://docs.mem0.ai/integrations/raycast) [Both]: Use when the user wants quick memory access via Raycast.
-251
View File
@@ -1,251 +0,0 @@
---
title: Contextual Memory Creation
description: "Add messages with automatic context management - no manual history tracking required"
---
## What is Contextual Memory Creation?
Contextual memory creation automatically manages message history, allowing you to focus on building AI experiences without manually tracking interactions. Simply send new messages, and Mem0 handles the context automatically.
<CodeGroup>
```python Python
# Just send new messages - Mem0 handles the context
messages = [
{"role": "user", "content": "I love Italian food, especially pasta"},
{"role": "assistant", "content": "Great! I'll remember your preference for Italian cuisine."}
]
client.add(messages, user_id="user123")
```
```javascript JavaScript
// Just send new messages - Mem0 handles the context
const messages = [
{"role": "user", "content": "I love Italian food, especially pasta"},
{"role": "assistant", "content": "Great! I'll remember your preference for Italian cuisine."}
];
await client.add(messages, { userId: "user123" });
```
</CodeGroup>
## Why Use Contextual Memory Creation?
- **Simple**: Send only new messages, no manual history tracking
- **Efficient**: Smaller payloads and faster processing
- **Automatic**: Context management handled by Mem0
- **Reliable**: No risk of missing interaction history
- **Scalable**: Works seamlessly as your application grows
## How It Works
### Basic Usage
<CodeGroup>
```python Python
# First interaction
messages1 = [
{"role": "user", "content": "Hi, I'm Sarah from New York"},
{"role": "assistant", "content": "Hello Sarah! Nice to meet you."}
]
client.add(messages1, user_id="sarah")
# Later interaction - just send new messages
messages2 = [
{"role": "user", "content": "I'm planning a trip to Italy next month"},
{"role": "assistant", "content": "How exciting! Italy is beautiful this time of year."}
]
client.add(messages2, user_id="sarah")
# Mem0 automatically knows Sarah is from New York and can use this context
```
```javascript JavaScript
// First interaction
const messages1 = [
{"role": "user", "content": "Hi, I'm Sarah from New York"},
{"role": "assistant", "content": "Hello Sarah! Nice to meet you."}
];
await client.add(messages1, { userId: "sarah" });
// Later interaction - just send new messages
const messages2 = [
{"role": "user", "content": "I'm planning a trip to Italy next month"},
{"role": "assistant", "content": "How exciting! Italy is beautiful this time of year."}
];
await client.add(messages2, { userId: "sarah" });
// Mem0 automatically knows Sarah is from New York and can use this context
```
</CodeGroup>
## Organization Strategies
Choose the right approach based on your application's needs:
### User-Level Memories (`user_id` only)
**Best for:** Personal preferences, profile information, long-term user data
<CodeGroup>
```python Python
# Persistent user memories across all interactions
messages = [
{"role": "user", "content": "I'm allergic to nuts and dairy"},
{"role": "assistant", "content": "I've noted your allergies for future reference."}
]
client.add(messages, user_id="user123")
# This allergy info will be available in ALL future interactions
```
```javascript JavaScript
// Persistent user memories across all interactions
const messages = [
{"role": "user", "content": "I'm allergic to nuts and dairy"},
{"role": "assistant", "content": "I've noted your allergies for future reference."}
];
await client.add(messages, { userId: "user123" });
// This allergy info will be available in ALL future interactions
```
</CodeGroup>
### Session-Specific Memories (`user_id` + `run_id`)
**Best for:** Task-specific context, separate interaction threads, project-based sessions
<CodeGroup>
```python Python
# Trip planning session
messages1 = [
{"role": "user", "content": "I want to plan a 5-day trip to Tokyo"},
{"role": "assistant", "content": "Perfect! Let's plan your Tokyo adventure."}
]
client.add(messages1, user_id="user123", run_id="tokyo-trip-2024")
# Later in the same trip planning session
messages2 = [
{"role": "user", "content": "I prefer staying near Shibuya"},
{"role": "assistant", "content": "Great choice! Shibuya is very convenient."}
]
client.add(messages2, user_id="user123", run_id="tokyo-trip-2024")
# Different session for work project (separate context)
work_messages = [
{"role": "user", "content": "Let's discuss the Q4 marketing strategy"},
{"role": "assistant", "content": "Sure! What are your main goals for Q4?"}
]
client.add(work_messages, user_id="user123", run_id="q4-marketing")
```
```javascript JavaScript
// Trip planning session
const messages1 = [
{"role": "user", "content": "I want to plan a 5-day trip to Tokyo"},
{"role": "assistant", "content": "Perfect! Let's plan your Tokyo adventure."}
];
await client.add(messages1, { userId: "user123", runId: "tokyo-trip-2024" });
// Later in the same trip planning session
const messages2 = [
{"role": "user", "content": "I prefer staying near Shibuya"},
{"role": "assistant", "content": "Great choice! Shibuya is very convenient."}
];
await client.add(messages2, { userId: "user123", runId: "tokyo-trip-2024" });
// Different session for work project (separate context)
const workMessages = [
{"role": "user", "content": "Let's discuss the Q4 marketing strategy"},
{"role": "assistant", "content": "Sure! What are your main goals for Q4?"}
];
await client.add(workMessages, { userId: "user123", runId: "q4-marketing" });
```
</CodeGroup>
## Real-World Use Cases
<Tabs>
<Tab title="Customer Support">
```python Python
# Support ticket context - keeps interaction focused
messages = [
{"role": "user", "content": "My subscription isn't working"},
{"role": "assistant", "content": "I can help with that. What specific issue are you experiencing?"},
{"role": "user", "content": "I can't access premium features even though I paid"}
]
# Each support ticket gets its own run_id
client.add(messages,
user_id="customer123",
run_id="ticket-2024-001"
)
```
</Tab>
<Tab title="Personal AI Assistant">
```python Python
# Personal preferences (persistent across all interactions)
preference_messages = [
{"role": "user", "content": "I prefer morning workouts and vegetarian meals"},
{"role": "assistant", "content": "Got it! I'll keep your fitness and dietary preferences in mind."}
]
client.add(preference_messages, user_id="user456")
# Daily planning session (session-specific)
planning_messages = [
{"role": "user", "content": "Help me plan tomorrow's schedule"},
{"role": "assistant", "content": "Of course! I'll consider your morning workout preference."}
]
client.add(planning_messages,
user_id="user456",
run_id="daily-plan-2024-01-15"
)
```
</Tab>
<Tab title="Educational Platform">
```python Python
# Student profile (persistent)
profile_messages = [
{"role": "user", "content": "I'm studying computer science and struggle with math"},
{"role": "assistant", "content": "I'll tailor explanations to help with math concepts."}
]
client.add(profile_messages, user_id="student789")
# Specific lesson session
lesson_messages = [
{"role": "user", "content": "Can you explain algorithms?"},
{"role": "assistant", "content": "Sure! I'll explain algorithms with math-friendly examples."}
]
client.add(lesson_messages,
user_id="student789",
run_id="algorithms-lesson-1"
)
```
</Tab>
</Tabs>
## Best Practices
### ✅ Do
- **Organize by context scope**: Use `user_id` only for persistent data, add `run_id` for session-specific context
- **Keep messages focused** on the current interaction
- **Test with real interaction flows** to ensure context works as expected
### ❌ Don't
- Send duplicate messages or interaction history
- Skip identifiers like `user_id` or `run_id` that scope the memory
- Mix contextual and non-contextual approaches in the same application
## Troubleshooting
| Issue | Solution |
|-------|----------|
| **Context not working** | Ensure each call uses the same `user_id` / `run_id` combo; version is automatic |
| **Wrong context retrieved** | Check if you need separate `run_id` values for different interaction topics |
| **Missing interaction history** | Verify all messages in the interaction thread use the same `user_id` and `run_id` |
| **Too much irrelevant context** | Use more specific `run_id` values to separate different interaction types |
<Snippet file="get-help.mdx" />
@@ -1,201 +0,0 @@
---
title: Criteria Retrieval
description: "Rank and retrieve memories based on custom-defined criteria like emotional tone, intent, and behavioral signals."
---
Mem0's Criteria Retrieval feature allows you to retrieve memories based on your defined criteria. It goes beyond generic semantic relevance and ranks memories based on what matters to your application: emotional tone, intent, behavioral signals, or other custom traits.
Instead of just searching for "how similar a memory is to this query," you can define what relevance truly means for your project. For example:
- Prioritize joyful memories when building a wellness assistant
- Downrank negative memories in a productivity-focused agent
- Highlight curiosity in a tutoring agent
You define criteria: custom attributes like "joy", "negativity", "confidence", or "urgency", and assign weights to control how they influence scoring. When you search, Mem0 uses these to re-rank semantically relevant memories, favoring those that better match your intent.
This gives you nuanced, intent-aware memory search that adapts to your use case.
## When to Use Criteria Retrieval
Use Criteria Retrieval if:
- You’re building an agent that should react to **emotions** or **behavioral signals**
- You want to guide memory selection based on **context**, not just content
- You have domain-specific signals like "risk", "positivity", "confidence", etc. that shape recall
## Setting Up Criteria Retrieval
Let’s walk through how to configure and use Criteria Retrieval step by step.
### Initialize the Client
Before defining any criteria, make sure to initialize the `MemoryClient` with your credentials and project ID:
```python
from mem0 import MemoryClient
client = MemoryClient(api_key="your_mem0_api_key")
```
### Define Your Criteria
Each criterion includes:
- A `name` (used in scoring)
- A `description` (interpreted by the LLM)
- A `weight` (how much it influences the final score)
```python
retrieval_criteria = [
{
"name": "joy",
"description": "Measure the intensity of positive emotions such as happiness, excitement, or amusement expressed in the sentence. A higher score reflects greater joy.",
"weight": 3
},
{
"name": "curiosity",
"description": "Assess the extent to which the sentence reflects inquisitiveness, interest in exploring new information, or asking questions. A higher score reflects stronger curiosity.",
"weight": 2
},
{
"name": "emotion",
"description": "Evaluate the presence and depth of sadness or negative emotional tone, including expressions of disappointment, frustration, or sorrow. A higher score reflects greater sadness.",
"weight": 1
}
]
```
### Apply Criteria to Your Project
Once defined, register the criteria to your project:
```python
client.project.update(retrieval_criteria=retrieval_criteria)
```
Criteria apply project-wide. Once set, they affect all searches automatically.
## Example Walkthrough
After setting up your criteria, you can use them to filter and retrieve memories. Here's an example:
### Add Memories
```python
messages = [
{"role": "user", "content": "What a beautiful sunny day! I feel so refreshed and ready to take on anything!"},
{"role": "user", "content": "I've always wondered how storms form, what triggers them in the atmosphere?"},
{"role": "user", "content": "It's been raining for days, and it just makes everything feel heavier."},
{"role": "user", "content": "Finally I get time to draw something today, after a long time!! I am super happy today."}
]
client.add(messages, user_id="alice")
```
### Run Standard vs. Criteria-Based Search
```python
# Search with criteria enabled
filters = {"user_id": "alice"}
results_with_criteria = client.search(
query="Why I am feeling happy today?",
filters=filters
)
# To disable criteria for a specific search
results_without_criteria = client.search(
query="Why I am feeling happy today?",
filters=filters,
use_criteria=False # Disable criteria-based scoring
)
```
### Compare Results
### Search Results (with Criteria)
```text
[
{"memory": "User feels refreshed and ready to take on anything on a beautiful sunny day", "score": 0.666, ...},
{"memory": "User finally has time to draw something after a long time", "score": 0.616, ...},
{"memory": "User is happy today", "score": 0.500, ...},
{"memory": "User is curious about how storms form and what triggers them in the atmosphere.", "score": 0.400, ...},
{"memory": "It has been raining for days, making everything feel heavier.", "score": 0.116, ...}
]
```
### Search Results (without Criteria)
```text
[
{"memory": "User is happy today", "score": 0.607, ...},
{"memory": "User feels refreshed and ready to take on anything on a beautiful sunny day", "score": 0.512, ...},
{"memory": "It has been raining for days, making everything feel heavier.", "score": 0.4617, ...},
{"memory": "User is curious about how storms form and what triggers them in the atmosphere.", "score": 0.340, ...},
{"memory": "User finally has time to draw something after a long time", "score": 0.336, ...},
]
```
## Search Results Comparison
1. **Memory Ordering**: With criteria, memories with high joy scores (like feeling refreshed and drawing) are ranked higher. Without criteria, the most relevant memory ("User is happy today") comes first.
2. **Score Distribution**: With criteria, scores are more spread out (0.116 to 0.666) and reflect the criteria weights. Without criteria, scores are more clustered (0.336 to 0.607) and based purely on relevance.
3. **Trait Sensitivity**: "Rainy day" content is penalized due to negative tone, while "Storm curiosity" is recognized and scored accordingly.
## Key Differences vs. Standard Search
| Aspect | Standard Search | Criteria Retrieval |
|-------------------------|--------------------------------------|-------------------------------------------------|
| Ranking Logic | Semantic similarity only | Semantic + LLM-based criteria scoring |
| Control Over Relevance | None | Fully customizable with weighted criteria |
| Memory Reordering | Static based on similarity | Dynamically re-ranked by intent alignment |
| Emotional Sensitivity | No tone or trait awareness | Incorporates emotion, tone, or custom behaviors |
| Activation | Default (no criteria defined) | Enabled when criteria are defined in project |
<Note>
If no criteria are defined for a project, search behaves normally based on semantic similarity only.
</Note>
## Best Practices
- Choose 3-5 criteria that reflect your application's intent
- Make descriptions clear and distinct; these are interpreted by an LLM
- Use stronger weights to amplify the impact of important traits
- Avoid redundant or ambiguous criteria (e.g., "positivity" and "joy")
- Always handle empty result sets in your application logic
## How It Works
1. **Criteria Definition**: Define custom criteria with a name, description, and weight. These describe what matters in a memory (e.g., joy, urgency, empathy).
2. **Project Configuration**: Register these criteria using `project.update()`. They apply at the project level and automatically influence all searches.
3. **Memory Retrieval**: When you perform a search, Mem0 first retrieves relevant memories based on the query.
4. **Weighted Scoring**: Each retrieved memory is evaluated and scored against your defined criteria and weights.
This lets you prioritize memories that align with your agent's goals and not just those that look similar to the query.
<Note>
Criteria retrieval is automatically enabled when criteria are defined in your project. Use `use_criteria=False` in search to temporarily disable it for a specific query. `use_criteria` is a server-side parameter passed through to the Platform API: it is not a typed option in the SDK's `SearchMemoryOptions` interface, but the server accepts and processes it when included in the request body.
</Note>
## Summary
- Define what "relevant" means using criteria
- Apply them per project via `project.update()`
- Criteria-aware search activates automatically when criteria are configured
- Build agents that reason not just with relevance, but **contextual importance**
---
Need help designing or tuning your criteria?
<Snippet file="get-help.mdx" />
-1
View File
@@ -60,7 +60,6 @@ Mem0 offers two powerful ways to add memory to your AI applications. Choose base
| **Multimodal support** | ✅ | ✅ |
| **Custom categories** | ✅ | Limited |
| **Advanced retrieval** | ✅ | ✅ |
| **Criteria retrieval** | ✅ | ❌ |
| **Temporal reasoning** | ✅ (v3) | ❌ |
| **Memory decay** | ✅ (v3) | ❌ |
| **Graph memory** | ✅ Built-in | ✅ External graph store |
@@ -1,6 +1,6 @@
{
"name": "mem0",
"version": "0.2.12",
"version": "0.2.13",
"description": "Persistent memory for Claude Code. Remembers decisions, patterns, and preferences across sessions.",
"author": {
"name": "Mem0",
@@ -1,6 +1,6 @@
{
"name": "mem0",
"version": "0.2.12",
"version": "0.2.13",
"description": "Persistent memory for Codex. Remembers decisions, patterns, and preferences across sessions.",
"author": {
"name": "Mem0",
@@ -1,6 +1,6 @@
{
"name": "mem0",
"version": "0.2.12",
"version": "0.2.13",
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.",
"author": {
"name": "Mem0",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "mem0",
"name": "mem0",
"version": "0.1.4",
"version": "0.1.5",
"description": "Persistent semantic memory for Antigravity agents. Cross-session, user-level recall via the Mem0 Platform MCP server. 16 slash commands, lifecycle hooks for auto-capture and metadata enforcement.",
"author": { "name": "Mem0", "email": "support@mem0.ai" },
"publisher": "mem0ai",
@@ -104,8 +104,11 @@ def store_summary(api_key: str, summary: str, user_id: str, session_id: str, pro
}
if branch:
metadata["branch"] = branch
# The compact summary is model-authored prose, in the first person and with no
# framing to mark it as such. Under role="user" mem0 reads "I recommend X" as
# the human saying it and stores "User recommends X".
body = {
"messages": [{"role": "user", "content": summary}],
"messages": [{"role": "assistant", "content": summary}],
"user_id": user_id,
"app_id": project_id,
"metadata": metadata,
@@ -175,8 +175,12 @@ def store_summary(
if files:
metadata["files_touched"] = files[:20]
# summary_prompt wraps the assistant's own last message. Mem0 extracts "facts
# about the user" from each message and role is the only signal telling it who
# spoke, so role="user" here turns Claude's opinions into the human's stated
# preferences ("User prefers dropping Redis...").
body = {
"messages": [{"role": "user", "content": summary_prompt}],
"messages": [{"role": "assistant", "content": summary_prompt}],
"user_id": user_id,
"app_id": project_id,
"run_id": session_id,
@@ -8,7 +8,6 @@ Additional platform capabilities beyond core CRUD operations.
- [Entity Linking](#entity-linking)
- [Custom Categories](#custom-categories)
- [Custom Instructions](#custom-instructions)
- [Criteria Retrieval](#criteria-retrieval)
- [Feedback Mechanism](#feedback-mechanism)
- [Memory Export](#memory-export)
- [Group Chat](#group-chat)
@@ -165,44 +164,6 @@ await client.updateProject({ customInstructions: "Your guidelines here..." });
---
## Criteria Retrieval
Custom attribute-based memory ranking using LLM-evaluated criteria with weights. Goes beyond semantic similarity to prioritize memories based on domain-specific signals.
### Configuration
```python
# Define criteria at project level
retrieval_criteria = [
{"name": "joy", "description": "Positive emotions like happiness and excitement", "weight": 3},
{"name": "curiosity", "description": "Inquisitiveness and desire to learn", "weight": 2},
{"name": "urgency", "description": "Time-sensitive or high-priority items", "weight": 4},
]
client.project.update(retrieval_criteria=retrieval_criteria)
```
```typescript
await client.updateProject({
retrievalCriteria: [
{ name: 'joy', description: 'Positive emotions', weight: 3 },
{ name: 'urgency', description: 'Time-sensitive items', weight: 4 },
],
});
```
### Usage
Once configured, `client.search()` automatically applies criteria ranking:
```python
# Criteria-weighted results returned automatically
results = client.search("Why am I feeling happy?", filters={"user_id": "alice"})
```
**Best for:** Wellness assistants, tutoring platforms, productivity tools — any app needing intent-aware retrieval.
---
## Feedback Mechanism
Provide feedback on extracted memories to improve system quality over time.
@@ -0,0 +1,144 @@
"""Regression tests: assistant-authored text must never be posted as role="user".
The Stop hook (capture_session_summary) and the post-compact hook
(capture_compact_summary) both ship *model-authored* prose to
POST /v3/memories/add/. Mem0's fact extractor renders each message as
"{role}: {content}" and is instructed to extract "facts and preferences about
the user" — so role is the only signal separating what the human said from what
Claude said.
Posting Claude's own words under role="user" made the extractor read Claude's
first-person prose ("I recommend pgvector", "I found the bug in auth.py") as the
*human's* statements and store them under their user_id. The Stop hook fires on
every assistant turn, so this corrupted memory on nearly every message.
"""
from __future__ import annotations
import json
class _FakeResp:
status = 200
def __enter__(self):
return self
def __exit__(self, *_):
return False
def _capture(monkeypatch, module):
"""Patch urlopen so store_summary posts nowhere; capture the request body."""
captured: dict = {}
def fake_urlopen(req, timeout=0):
captured["body"] = json.loads(req.data.decode("utf-8"))
return _FakeResp()
monkeypatch.setattr(module.urllib.request, "urlopen", fake_urlopen)
return captured
# Claude's own voice — first-person prose that must never be attributed to the human.
ASSISTANT_PROSE = (
"I traced the root cause to auth.py and I recommend we switch to pgvector "
"for the vector store. I'll refactor the session handler next."
)
def test_session_summary_posts_assistant_prose_as_assistant(monkeypatch):
"""Stop hook: the last assistant message must be tagged role="assistant"."""
import capture_session_summary as css
captured = _capture(monkeypatch, css)
css.store_summary(
api_key="test-key",
summary_prompt=css.build_summary_prompt(ASSISTANT_PROSE, []),
user_id="u1",
session_id="s1",
project_id="p1",
branch="main",
files=[],
)
messages = captured["body"]["messages"]
for msg in messages:
if ASSISTANT_PROSE in msg["content"]:
assert msg["role"] == "assistant", (
"Claude's own words were posted as role='user' — mem0 will extract "
"them as facts about the human. Got role=%r" % msg["role"]
)
break
else:
raise AssertionError("assistant prose never made it into the payload")
def test_compact_summary_posts_assistant_prose_as_assistant(monkeypatch):
"""Post-compact hook: the compact summary is model-authored, not user-authored."""
import capture_compact_summary as ccs
captured = _capture(monkeypatch, ccs)
ccs.store_summary(
api_key="test-key",
summary=ASSISTANT_PROSE,
user_id="u1",
session_id="s1",
project_id="p1",
branch="main",
)
messages = captured["body"]["messages"]
for msg in messages:
if ASSISTANT_PROSE in msg["content"]:
assert msg["role"] == "assistant", (
"Compact summary (written by Claude) was posted as role='user'. Got role=%r" % msg["role"]
)
break
else:
raise AssertionError("assistant prose never made it into the payload")
def test_no_user_role_message_carries_assistant_prose(monkeypatch):
"""Belt and braces: no user-role message may contain the assistant's words."""
import capture_session_summary as css
captured = _capture(monkeypatch, css)
css.store_summary(
api_key="test-key",
summary_prompt=css.build_summary_prompt(ASSISTANT_PROSE, ["auth.py"]),
user_id="u1",
session_id="s1",
project_id="p1",
branch="main",
files=["auth.py"],
)
for msg in captured["body"]["messages"]:
if msg["role"] == "user":
assert ASSISTANT_PROSE not in msg["content"], (
"A user-role message carries Claude's prose — this is the misattribution bug."
)
def test_auto_capture_preserves_real_roles():
"""auto_capture is the reference: it must pass roles through untouched."""
import auto_capture
lines = [
json.dumps({"type": "user", "message": {"role": "user", "content": "why is the build failing on main?"}}),
json.dumps(
{
"type": "assistant",
"message": {"role": "assistant", "content": [{"type": "text", "text": ASSISTANT_PROSE}]},
}
),
]
messages = auto_capture.extract_recent_exchanges(lines)
assert [m["role"] for m in messages] == ["user", "assistant"]
assert ASSISTANT_PROSE in messages[1]["content"]
+31
View File
@@ -0,0 +1,31 @@
module.exports = {
root: true,
env: { browser: true, es6: true, node: true },
parser: '@typescript-eslint/parser',
parserOptions: { sourceType: 'module', extraFileExtensions: ['.json'] },
ignorePatterns: ['.eslintrc.js', '**/*.js', '**/node_modules/**', '**/dist/**'],
overrides: [
{
files: ['package.json'],
plugins: ['eslint-plugin-n8n-nodes-base'],
extends: ['plugin:n8n-nodes-base/community'],
rules: { 'n8n-nodes-base/community-package-json-name-still-default': 'off' },
},
{
files: ['./credentials/**/*.ts'],
plugins: ['eslint-plugin-n8n-nodes-base'],
extends: ['plugin:n8n-nodes-base/credentials'],
rules: {
// This rule only applies to nodes in n8n's main repository (where
// documentationUrl is an internal docs slug). Community nodes use a
// full external URL, so it is disabled here.
'n8n-nodes-base/cred-class-field-documentation-url-miscased': 'off',
},
},
{
files: ['./nodes/**/*.ts'],
plugins: ['eslint-plugin-n8n-nodes-base'],
extends: ['plugin:n8n-nodes-base/nodes'],
},
],
};
+8
View File
@@ -0,0 +1,8 @@
node_modules/
dist/
package-lock.json
*.tsbuildinfo
.env
coverage/
*.log
.DS_Store
@@ -0,0 +1,9 @@
module.exports = {
semi: true,
trailingComma: 'all',
bracketSpacing: true,
useTabs: true,
tabWidth: 2,
printWidth: 100,
singleQuote: true,
};
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Mem0
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+54
View File
@@ -0,0 +1,54 @@
# n8n-nodes-mem0
This is an n8n community node that lets you use [Mem0](https://mem0.ai) — the memory layer for AI agents — in your n8n workflows.
Mem0 gives your agents long-term memory: add memories from conversations, then search and recall them across sessions.
[n8n](https://n8n.io) is a [fair-code licensed](https://docs.n8n.io/reference/license/) workflow automation platform.
[Installation](#installation) · [Operations](#operations) · [Credentials](#credentials) · [Usage](#usage) · [Resources](#resources)
## Installation
Follow the [community nodes installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) and install `n8n-nodes-mem0`.
## Operations
The **Memory** resource supports:
| Operation | Description | Endpoint |
| --- | --- | --- |
| **Add** | Extract and store memories from messages | `POST /v3/memories/add/` |
| **Search** | Semantic search over stored memories | `POST /v3/memories/search/` |
| **Get Many** | List memories for a user (paginated) | `POST /v3/memories/` |
| **Get** | Retrieve a single memory by ID | `GET /v1/memories/{id}/` |
| **Update** | Update a memory's text or metadata | `PUT /v1/memories/{id}/` |
| **Delete** | Delete a single memory by ID | `DELETE /v1/memories/{id}/` |
### Add & asynchronous extraction
By default, **Add** runs LLM-based extraction asynchronously — the API returns an event ID and the node polls until extraction finishes, then returns the resulting memories. Disable **Wait for Completion** to return immediately with the event ID, or set **Infer = false** (under Additional Fields) to store messages verbatim and return synchronously.
## Credentials
You need a Mem0 API key. Create one at [app.mem0.ai](https://app.mem0.ai) → Settings → API Keys. The key is sent as `Authorization: Token <key>`.
## Usage
This node is also **usable as a tool** by n8n's AI Agent node — attach it so an agent can "remember" and "recall" autonomously.
A typical loop:
1. **Search** memory before answering, filtered by `User ID`.
2. **Add** durable facts after a meaningful exchange.
Memory writes are asynchronous by default; allow a moment after an Add before searching for the same content.
## Resources
- [Mem0 documentation](https://docs.mem0.ai)
- [n8n community nodes documentation](https://docs.n8n.io/integrations/community-nodes/)
## License
[MIT](./LICENSE)
@@ -0,0 +1,53 @@
import {
IAuthenticateGeneric,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class Mem0Api implements ICredentialType {
name = 'mem0Api';
displayName = 'Mem0 API';
documentationUrl = 'https://docs.mem0.ai/platform/quickstart';
properties: INodeProperties[] = [
{
displayName: 'API Key',
name: 'apiKey',
type: 'string',
typeOptions: { password: true },
default: '',
required: true,
description: 'Your Mem0 API key (starts with "m0-"). Create one at app.mem0.ai → Settings → API Keys.',
},
{
displayName: 'Base URL',
name: 'baseUrl',
type: 'string',
default: 'https://api.mem0.ai',
description: 'Mem0 API base URL. Override only for self-hosted or non-default deployments.',
},
];
// Injects "Authorization: Token <apiKey>" on every request, matching the
// scheme used by Mem0's official SDKs (Authorization: Token m0-...).
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=Token {{$credentials.apiKey}}',
},
},
};
// Cheap authenticated GET; validates the key when the user clicks "Test".
test: ICredentialTestRequest = {
request: {
baseURL: '={{$credentials.baseUrl}}',
url: '/v1/ping/',
method: 'GET',
},
};
}
+16
View File
@@ -0,0 +1,16 @@
const path = require('path');
const { task, src, dest } = require('gulp');
task('build:icons', copyIcons);
function copyIcons() {
// Copy icons and the codex (*.node.json) next to the compiled nodes; tsc emits
// only .js, so these static assets need copying for n8n to pick them up.
const nodeSource = path.resolve('nodes', '**', '*.{png,svg,json}');
const nodeDestination = path.resolve('dist', 'nodes');
src(nodeSource).pipe(dest(nodeDestination));
const credSource = path.resolve('credentials', '**', '*.{png,svg}');
const credDestination = path.resolve('dist', 'credentials');
return src(credSource, { allowEmpty: true }).pipe(dest(credDestination));
}
+3
View File
@@ -0,0 +1,3 @@
// n8n loads nodes and credentials via the "n8n" key in package.json.
// This entry point is intentionally empty.
module.exports = {};
@@ -0,0 +1,21 @@
{
"node": "n8n-nodes-mem0.mem0",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["AI"],
"subcategories": {
"AI": ["Memory"]
},
"resources": {
"primaryDocumentation": [
{
"url": "https://docs.mem0.ai/integrations/n8n"
}
],
"credentialDocumentation": [
{
"url": "https://docs.mem0.ai/integrations/n8n"
}
]
}
}
@@ -0,0 +1,463 @@
import {
IExecuteFunctions,
IDataObject,
IHttpRequestMethods,
IHttpRequestOptions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
JsonObject,
NodeApiError,
NodeOperationError,
sleep,
} from 'n8n-workflow';
// Poll settings for asynchronous (infer=true) memory addition.
const POLL_INTERVAL_MS = 1500;
const MAX_POLL_ATTEMPTS = 40; // ~60s ceiling
export class Mem0 implements INodeType {
description: INodeTypeDescription = {
displayName: 'Mem0',
name: 'mem0',
icon: 'file:mem0.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Add, search, and manage long-term memories with Mem0',
defaults: {
name: 'Mem0',
},
// Makes the node available to the AI Agent (Tools Agent) node.
usableAsTool: true,
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'mem0Api',
required: true,
},
],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [{ name: 'Memory', value: 'memory' }],
default: 'memory',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: { show: { resource: ['memory'] } },
options: [
{
name: 'Add',
value: 'add',
action: 'Add a memory',
description: 'Extract and store memories from messages',
},
{
name: 'Delete',
value: 'delete',
action: 'Delete a memory',
description: 'Delete a single memory by ID',
},
{
name: 'Get',
value: 'get',
action: 'Get a memory',
description: 'Retrieve a single memory by ID',
},
{
name: 'Get Many',
value: 'getAll',
action: 'Get many memories',
description: 'List stored memories for an entity',
},
{
name: 'Search',
value: 'search',
action: 'Search memories',
description: 'Semantic search over stored memories',
},
{
name: 'Update',
value: 'update',
action: 'Update a memory',
description: 'Update the text or metadata of a memory',
},
],
default: 'add',
},
// ---- Add ---------------------------------------------------------
{
displayName: 'Messages',
name: 'messages',
placeholder: 'Add Message',
type: 'fixedCollection',
typeOptions: { multipleValues: true },
displayOptions: { show: { resource: ['memory'], operation: ['add'] } },
default: {},
description: 'The conversation messages to extract memories from',
options: [
{
name: 'message',
displayName: 'Message',
values: [
{
displayName: 'Role',
name: 'role',
type: 'options',
options: [
{ name: 'User', value: 'user' },
{ name: 'Assistant', value: 'assistant' },
{ name: 'System', value: 'system' },
],
default: 'user',
},
{
displayName: 'Content',
name: 'content',
type: 'string',
typeOptions: { rows: 2 },
default: '',
},
],
},
],
},
{
displayName: 'User ID',
name: 'userId',
type: 'string',
default: '',
displayOptions: { show: { resource: ['memory'], operation: ['add'] } },
description: 'Associate the memories with this user',
},
{
displayName: 'Wait for Completion',
name: 'waitForCompletion',
type: 'boolean',
default: true,
displayOptions: { show: { resource: ['memory'], operation: ['add'] } },
description:
'Whether to poll until memory extraction finishes and return the resulting memories. Turn off to return immediately with the event ID.',
},
{
displayName: 'Additional Fields',
name: 'addFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: { show: { resource: ['memory'], operation: ['add'] } },
options: [
{
displayName: 'Agent ID',
name: 'agent_id',
type: 'string',
default: '',
},
{
displayName: 'App ID',
name: 'app_id',
type: 'string',
default: '',
},
{
displayName: 'Infer',
name: 'infer',
type: 'boolean',
default: true,
description:
'Whether to run LLM extraction over the messages. Turn off to store them verbatim. ' +
'This controls extraction only — use "Wait for Completion" to control whether the node waits.',
},
{
displayName: 'Metadata (JSON)',
name: 'metadata',
type: 'json',
default: '',
},
{
displayName: 'Run ID',
name: 'run_id',
type: 'string',
default: '',
},
],
},
// ---- Search ------------------------------------------------------
{
displayName: 'Query',
name: 'query',
type: 'string',
default: '',
required: true,
displayOptions: { show: { resource: ['memory'], operation: ['search'] } },
description: 'What to recall from memory',
},
{
displayName: 'User ID',
name: 'userId',
type: 'string',
default: '',
required: true,
displayOptions: { show: { resource: ['memory'], operation: ['search'] } },
description: 'Restrict the search to this user (required — the API needs an entity filter)',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
typeOptions: { minValue: 1 },
default: 50,
displayOptions: { show: { resource: ['memory'], operation: ['search'] } },
description: 'Max number of results to return',
},
// ---- Get Many ----------------------------------------------------
{
displayName: 'User ID',
name: 'userId',
type: 'string',
default: '',
required: true,
displayOptions: { show: { resource: ['memory'], operation: ['getAll'] } },
description: 'Restrict the listing to this user (required — the API needs an entity filter)',
},
{
displayName: 'Page',
name: 'page',
type: 'number',
typeOptions: { minValue: 1 },
default: 1,
displayOptions: { show: { resource: ['memory'], operation: ['getAll'] } },
},
{
displayName: 'Page Size',
name: 'pageSize',
type: 'number',
typeOptions: { minValue: 1 },
default: 50,
displayOptions: { show: { resource: ['memory'], operation: ['getAll'] } },
},
// ---- Get / Update / Delete (by ID) -------------------------------
{
displayName: 'Memory ID',
name: 'memoryId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: { resource: ['memory'], operation: ['get', 'update', 'delete'] },
},
},
{
displayName: 'Text',
name: 'text',
type: 'string',
default: '',
displayOptions: { show: { resource: ['memory'], operation: ['update'] } },
description: 'The new memory text',
},
{
displayName: 'Metadata (JSON)',
name: 'metadata',
type: 'json',
default: '',
displayOptions: { show: { resource: ['memory'], operation: ['update'] } },
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const baseUrl = ((await this.getCredentials('mem0Api')).baseUrl as string) || 'https://api.mem0.ai';
const request = async (
method: IHttpRequestMethods,
url: string,
body?: IDataObject,
qs?: IDataObject,
): Promise<IDataObject> => {
const options: IHttpRequestOptions = {
method,
url: `${baseUrl}${url}`,
json: true,
...(body ? { body } : {}),
...(qs ? { qs } : {}),
};
return (await this.helpers.httpRequestWithAuthentication.call(
this,
'mem0Api',
options,
)) as IDataObject;
};
for (let i = 0; i < items.length; i++) {
try {
const operation = this.getNodeParameter('operation', i) as string;
let responseData: IDataObject | IDataObject[] = {};
if (operation === 'add') {
const messagesUi = this.getNodeParameter('messages.message', i, []) as IDataObject[];
if (!messagesUi.length) {
throw new NodeOperationError(this.getNode(), 'At least one message is required', {
itemIndex: i,
});
}
const addFields = this.getNodeParameter('addFields', i, {}) as IDataObject;
const body: IDataObject = {
messages: messagesUi.map((m) => ({ role: m.role, content: m.content })),
infer: addFields.infer !== undefined ? addFields.infer : true,
};
const userId = this.getNodeParameter('userId', i, '') as string;
if (userId) body.user_id = userId;
if (addFields.agent_id) body.agent_id = addFields.agent_id;
if (addFields.app_id) body.app_id = addFields.app_id;
if (addFields.run_id) body.run_id = addFields.run_id;
if (addFields.metadata) {
try {
body.metadata =
typeof addFields.metadata === 'string'
? JSON.parse(addFields.metadata as string)
: addFields.metadata;
} catch {
throw new NodeOperationError(this.getNode(), 'Invalid JSON in "Metadata" field', {
itemIndex: i,
});
}
}
// API requires at least one entity id — fail clearly instead of a raw 4xx.
if (!body.user_id && !body.agent_id && !body.run_id && !body.app_id) {
throw new NodeOperationError(
this.getNode(),
'Add requires at least one of User ID, Agent ID, Run ID, or App ID',
{ itemIndex: i },
);
}
const addResp = await request('POST', '/v3/memories/add/', body);
const waitForCompletion = this.getNodeParameter('waitForCompletion', i, true) as boolean;
const addStatus = addResp.status as string | undefined;
const isTerminal = addStatus === 'SUCCEEDED' || addStatus === 'FAILED';
// Add returns {event_id, status:PENDING|RUNNING}; poll until terminal when asked to wait.
if (waitForCompletion && addResp.event_id && !isTerminal) {
responseData = await pollEvent(request, addResp.event_id as string, this, i);
} else if (addStatus === 'FAILED') {
throw new NodeOperationError(
this.getNode(),
`Mem0 memory add failed: ${(addResp.message as string) || 'unknown error'}`,
{ itemIndex: i },
);
} else {
// If the response is already terminal, unwrap results; otherwise return as-is.
responseData = Array.isArray(addResp.results)
? (addResp.results as IDataObject[])
: addResp;
}
} else if (operation === 'search') {
const body: IDataObject = {
query: this.getNodeParameter('query', i) as string,
output_format: 'v1.1',
top_k: this.getNodeParameter('limit', i, 50) as number,
};
const userId = this.getNodeParameter('userId', i, '') as string;
if (userId) body.filters = { user_id: userId };
const resp = await request('POST', '/v3/memories/search/', body);
responseData = Array.isArray(resp.results) ? (resp.results as IDataObject[]) : [];
} else if (operation === 'getAll') {
const userId = this.getNodeParameter('userId', i, '') as string;
const page = this.getNodeParameter('page', i, 1) as number;
const pageSize = this.getNodeParameter('pageSize', i, 50) as number;
const body: IDataObject = {};
if (userId) body.filters = { user_id: userId };
const resp = await request('POST', '/v3/memories/', body, { page, page_size: pageSize });
responseData = Array.isArray(resp.results) ? (resp.results as IDataObject[]) : [];
} else if (operation === 'get') {
const memoryId = this.getNodeParameter('memoryId', i) as string;
responseData = await request('GET', `/v1/memories/${memoryId}/`);
} else if (operation === 'update') {
const memoryId = this.getNodeParameter('memoryId', i) as string;
const body: IDataObject = {};
const text = this.getNodeParameter('text', i, '') as string;
const metadata = this.getNodeParameter('metadata', i, '') as string;
if (text) body.text = text;
if (metadata) {
try {
body.metadata = typeof metadata === 'string' ? JSON.parse(metadata) : metadata;
} catch {
throw new NodeOperationError(this.getNode(), 'Invalid JSON in "Metadata" field', {
itemIndex: i,
});
}
}
if (Object.keys(body).length === 0) {
throw new NodeOperationError(this.getNode(), 'Provide text or metadata to update', {
itemIndex: i,
});
}
responseData = await request('PUT', `/v1/memories/${memoryId}/`, body);
} else if (operation === 'delete') {
const memoryId = this.getNodeParameter('memoryId', i) as string;
responseData = await request('DELETE', `/v1/memories/${memoryId}/`);
}
const arr = Array.isArray(responseData) ? responseData : [responseData];
for (const entry of arr) {
returnData.push({ json: entry, pairedItem: { item: i } });
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ json: { error: (error as Error).message }, pairedItem: { item: i } });
continue;
}
if (error instanceof NodeApiError || error instanceof NodeOperationError) throw error;
throw new NodeApiError(this.getNode(), error as JsonObject);
}
}
return [returnData];
}
}
// Polls GET /v1/event/{id}/ until the memory-addition event resolves.
async function pollEvent(
request: (m: IHttpRequestMethods, u: string) => Promise<IDataObject>,
eventId: string,
ctx: IExecuteFunctions,
itemIndex: number,
): Promise<IDataObject | IDataObject[]> {
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
const event = await request('GET', `/v1/event/${eventId}/`);
const status = event.status as string;
if (status === 'SUCCEEDED') {
// Match the shape of search/getAll (a clean array); fall back to the envelope.
return Array.isArray(event.results) ? (event.results as IDataObject[]) : event;
}
if (status === 'FAILED') {
const reason = (event.error as string) || (event.message as string) || 'unknown error';
throw new NodeOperationError(
ctx.getNode(),
`Mem0 memory event ${eventId} failed: ${reason}`,
{ itemIndex },
);
}
await sleep(POLL_INTERVAL_MS);
}
throw new NodeOperationError(
ctx.getNode(),
`Timed out waiting for memory event ${eventId} to complete`,
{ itemIndex },
);
}
@@ -0,0 +1,19 @@
<svg width="307" height="307" viewBox="0 0 307 307" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M162.496 25.3505C165.003 25.3505 167.453 24.6071 169.538 23.2144C171.622 21.8216 173.247 19.8419 174.206 17.5258C175.165 15.2097 175.416 12.6612 174.927 10.2024C174.438 7.74365 173.231 5.48516 171.458 3.71249C169.686 1.93983 167.427 0.73263 164.968 0.243552C162.51 -0.245525 159.961 0.00550576 157.645 0.964866C155.329 1.92423 153.349 3.54885 151.956 5.63328C150.564 7.71772 149.82 10.1683 149.82 12.6753C149.818 14.3404 150.145 15.9895 150.781 17.5283C151.417 19.0671 152.351 20.4653 153.528 21.6427C154.706 22.8201 156.104 23.7537 157.643 24.39C159.181 25.0262 160.83 25.3526 162.496 25.3505Z" fill="white"/>
<path d="M69.3342 56.559C71.1066 54.7862 72.3135 52.5277 72.8024 50.069C73.2913 47.6103 73.0401 45.0619 72.0807 42.7459C71.1213 40.43 69.4967 38.4505 67.4123 37.0579C65.3279 35.6652 62.8774 34.9219 60.3706 34.9219C57.8637 34.9219 55.4132 35.6652 53.3288 37.0579C51.2444 38.4505 49.6198 40.43 48.6604 42.7459C47.701 45.0619 47.4498 47.6103 47.9387 50.069C48.4276 52.5277 49.6345 54.7862 51.4069 56.559C52.5839 57.7363 53.9813 58.6701 55.5193 59.3073C57.0573 59.9444 58.7058 60.2724 60.3706 60.2724C62.0353 60.2724 63.6838 59.9444 65.2218 59.3073C66.7598 58.6701 68.1572 57.7363 69.3342 56.559Z" fill="white"/>
<path d="M25.3505 144.504C25.3505 141.997 24.6071 139.547 23.2143 137.462C21.8216 135.378 19.842 133.753 17.5259 132.794C15.2098 131.835 12.6612 131.584 10.2024 132.073C7.74368 132.562 5.48513 133.769 3.71247 135.542C1.9398 137.314 0.732655 139.573 0.243578 142.032C-0.2455 144.49 0.00543354 147.039 0.964793 149.355C1.92415 151.671 3.54877 153.651 5.63321 155.044C7.71764 156.436 10.1683 157.18 12.6752 157.18C16.0369 157.18 19.261 155.844 21.638 153.467C24.0151 151.09 25.3505 147.866 25.3505 144.504Z" fill="white"/>
<path d="M56.5589 237.749C54.7862 235.976 52.5277 234.769 50.069 234.28C47.6103 233.792 45.0619 234.043 42.7459 235.002C40.43 235.962 38.4505 237.586 37.0579 239.671C35.6652 241.755 34.9219 244.206 34.9219 246.712C34.9219 249.219 35.6652 251.67 37.0579 253.754C38.4505 255.838 40.43 257.463 42.7459 258.423C45.0619 259.382 47.6103 259.633 50.069 259.144C52.5277 258.655 54.7862 257.448 56.5589 255.676C57.7362 254.499 58.6701 253.102 59.3073 251.564C59.9444 250.026 60.2724 248.377 60.2724 246.712C60.2724 245.048 59.9444 243.399 59.3073 241.861C58.6701 240.323 57.7362 238.926 56.5589 237.749Z" fill="white"/>
<path d="M144.488 281.648C141.981 281.648 139.53 282.392 137.446 283.785C135.361 285.177 133.737 287.157 132.777 289.473C131.818 291.789 131.567 294.338 132.056 296.797C132.545 299.255 133.752 301.514 135.525 303.286C137.298 305.059 139.556 306.266 142.015 306.755C144.474 307.244 147.022 306.993 149.338 306.034C151.655 305.075 153.634 303.45 155.027 301.366C156.42 299.281 157.163 296.831 157.163 294.324C157.159 290.963 155.822 287.742 153.446 285.366C151.07 282.989 147.848 281.653 144.488 281.648Z" fill="white"/>
<path d="M237.751 250.487C235.978 252.26 234.771 254.518 234.282 256.977C233.794 259.435 234.045 261.984 235.004 264.3C235.964 266.616 237.588 268.595 239.673 269.988C241.757 271.381 244.207 272.124 246.714 272.124C249.221 272.124 251.672 271.381 253.756 269.988C255.84 268.595 257.465 266.616 258.424 264.3C259.384 261.984 259.635 259.435 259.146 256.977C258.657 254.518 257.45 252.26 255.678 250.487C254.501 249.31 253.104 248.376 251.566 247.739C250.028 247.101 248.379 246.773 246.714 246.773C245.05 246.773 243.401 247.101 241.863 247.739C240.325 248.376 238.928 249.31 237.751 250.487Z" fill="white"/>
<path d="M281.648 162.512C281.648 165.019 282.392 167.469 283.785 169.554C285.177 171.638 287.157 173.263 289.473 174.222C291.789 175.181 294.338 175.432 296.797 174.943C299.255 174.454 301.514 173.247 303.286 171.474C305.059 169.702 306.266 167.443 306.755 164.984C307.244 162.526 306.993 159.977 306.034 157.661C305.075 155.345 303.45 153.365 301.366 151.973C299.281 150.58 296.831 149.836 294.324 149.836C290.962 149.836 287.738 151.172 285.361 153.549C282.984 155.926 281.648 159.15 281.648 162.512Z" fill="white"/>
<path d="M250.471 69.3303C252.244 71.1027 254.503 72.3097 256.961 72.7985C259.42 73.2874 261.968 73.0363 264.284 72.0768C266.6 71.1174 268.58 69.4928 269.972 67.4084C271.365 65.324 272.108 62.8735 272.108 60.3667C272.108 57.8599 271.365 55.4093 269.972 53.3249C268.58 51.2406 266.6 49.616 264.284 48.6565C261.968 47.6971 259.42 47.4459 256.961 47.9348C254.503 48.4236 252.244 49.6306 250.471 51.403C249.294 52.58 248.36 53.9775 247.723 55.5155C247.086 57.0535 246.758 58.7019 246.758 60.3667C246.758 62.0314 247.086 63.6799 247.723 65.2179C248.36 66.7559 249.294 68.1533 250.471 69.3303Z" fill="white"/>
<path d="M184.782 60.8054C180.168 63.4713 178.3 69.0427 177.63 74.3267C177.047 78.9358 175.033 83.2457 171.87 86.6488C168.707 90.052 164.556 92.3766 160.002 93.2951C155.448 94.2136 150.721 93.6796 146.487 91.7683C142.252 89.857 138.724 86.6649 136.401 82.642C134.077 78.6192 133.075 73.9684 133.535 69.3455C133.995 64.7226 135.895 60.3607 138.966 56.8748C142.037 53.389 146.125 50.9549 150.653 49.9159C155.181 48.8768 159.921 49.2852 164.204 51.0834C169.121 53.1428 174.884 54.2762 179.514 51.6422C184.143 49.0082 185.995 43.4049 186.665 38.1208C187.245 33.5107 189.257 29.1987 192.418 25.7931C195.579 22.3876 199.73 20.0603 204.284 19.1394C208.838 18.2185 213.567 18.7505 217.803 20.6605C222.039 22.5704 225.568 25.7618 227.893 29.7846C230.218 33.8075 231.222 38.4587 230.763 43.0824C230.303 47.7062 228.404 52.0691 225.333 55.5559C222.262 59.0426 218.173 61.4773 213.645 62.5165C209.116 63.5557 204.375 63.147 200.091 61.3481C195.174 59.3048 189.411 58.1554 184.782 60.8054Z" fill="white"/>
<path d="M110.073 65.8178C108.7 70.9742 111.318 76.2422 114.575 80.4567C117.417 84.1261 119.036 88.595 119.204 93.2335C119.372 97.872 118.08 102.446 115.51 106.311C112.941 110.177 109.223 113.138 104.881 114.778C100.538 116.419 95.7912 116.655 91.3077 115.454C86.8242 114.253 82.8306 111.675 79.8898 108.084C76.9489 104.493 75.2091 100.07 74.9155 95.4379C74.6219 90.8057 75.7894 86.1981 78.2533 82.2645C80.7173 78.331 84.3534 75.2698 88.6494 73.5124C93.5822 71.485 98.4991 68.2444 99.8241 63.0881C101.149 57.9317 98.579 52.6637 95.3224 48.4493C92.4827 44.7781 90.8665 40.3083 90.7018 35.6699C90.537 31.0315 91.8319 26.4583 94.4039 22.5949C96.976 18.7314 100.695 15.7724 105.038 14.1349C109.381 12.4974 114.128 12.2639 118.611 13.4673C123.094 14.6708 127.085 17.2505 130.024 20.8429C132.963 24.4354 134.7 28.8594 134.991 33.4916C135.283 38.1237 134.113 42.7305 131.647 46.6627C129.182 50.5948 125.544 53.6541 121.248 55.4095C116.363 57.4209 111.462 60.6775 110.073 65.8178Z" fill="white"/>
<path d="M60.7892 122.218C63.4552 126.831 69.0425 128.699 74.3265 129.37C78.9361 129.955 83.2455 131.973 86.6471 135.138C90.0487 138.304 92.3707 142.457 93.2857 147.013C94.2006 151.569 93.6625 156.296 91.747 160.53C89.8314 164.763 86.6353 168.288 82.6093 170.608C78.5833 172.928 73.9305 173.926 69.3073 173.46C64.6841 172.995 60.3236 171.09 56.841 168.014C53.3583 164.938 50.9292 160.846 49.8962 156.316C48.8631 151.785 49.2783 147.045 51.0832 142.763C53.1426 137.846 54.2759 132.083 51.6419 127.454C49.0079 122.824 43.4046 120.973 38.1047 120.302C33.4951 119.717 29.1856 117.699 25.7841 114.533C22.3825 111.368 20.0604 107.214 19.1454 102.659C18.2304 98.1032 18.7687 93.3752 20.6842 89.1418C22.5997 84.9084 25.7959 81.3832 29.8219 79.0632C33.8479 76.7433 38.5006 75.7457 43.1238 76.2113C47.7471 76.6768 52.1075 78.582 55.5902 81.658C59.0728 84.7341 61.502 88.8258 62.535 93.3561C63.568 97.8865 63.1528 102.627 61.3479 106.908C59.2886 111.825 58.1552 117.588 60.7892 122.218Z" fill="white"/>
<path d="M65.8204 196.93C70.9767 198.303 76.2287 195.685 80.4592 192.428C84.1286 189.586 88.5975 187.967 93.236 187.799C97.8745 187.631 102.449 188.923 106.314 191.493C110.179 194.062 113.141 197.78 114.781 202.122C116.421 206.464 116.657 211.212 115.457 215.695C114.256 220.179 111.678 224.172 108.087 227.113C104.496 230.054 100.073 231.794 95.4404 232.087C90.8082 232.381 86.2006 231.214 82.2671 228.75C78.3335 226.286 75.2723 222.649 73.5149 218.353C71.4875 213.421 68.231 208.504 63.0906 207.179C57.9503 205.854 52.6662 208.424 48.4518 211.681C44.7804 214.528 40.308 216.151 35.6652 216.32C31.0224 216.49 26.4435 215.199 22.5738 212.628C18.7042 210.057 15.7391 206.336 14.0968 201.99C12.4544 197.644 12.2176 192.892 13.4197 188.404C14.6218 183.917 17.2021 179.919 20.7969 176.976C24.3917 174.033 28.8195 172.293 33.4562 172C38.0929 171.708 42.7045 172.878 46.6407 175.345C50.577 177.813 53.6394 181.454 55.3961 185.755C57.4235 190.656 60.6641 195.541 65.8204 196.93Z" fill="white"/>
<path d="M122.205 246.21C126.818 243.544 128.686 237.956 129.373 232.672C129.96 228.068 131.978 223.763 135.142 220.366C138.306 216.969 142.456 214.651 147.008 213.738C151.559 212.825 156.283 213.364 160.512 215.278C164.741 217.192 168.263 220.385 170.58 224.408C172.898 228.43 173.895 233.078 173.43 237.697C172.966 242.316 171.064 246.673 167.991 250.153C164.919 253.633 160.832 256.061 156.306 257.095C151.781 258.129 147.045 257.717 142.766 255.916C137.833 253.856 132.07 252.723 127.457 255.357C122.843 257.991 120.96 263.594 120.289 268.894C119.7 273.498 117.681 277.8 114.517 281.196C111.353 284.591 107.204 286.908 102.653 287.821C98.1027 288.733 93.3808 288.194 89.1525 286.281C84.9243 284.367 81.4031 281.175 79.085 277.154C76.767 273.134 75.7689 268.487 76.2316 263.869C76.6942 259.251 78.5942 254.895 81.6638 251.414C84.7334 247.933 88.8179 245.503 93.3417 244.466C97.8655 243.429 102.601 243.838 106.88 245.635C111.828 247.694 117.591 248.876 122.205 246.21Z" fill="white"/>
<path d="M196.915 241.18C198.304 236.024 195.686 230.756 192.414 226.542C189.567 222.87 187.944 218.398 187.774 213.755C187.604 209.112 188.896 204.533 191.467 200.664C194.038 196.794 197.759 193.829 202.104 192.187C206.45 190.544 211.202 190.307 215.69 191.509C220.178 192.712 224.175 195.292 227.118 198.887C230.061 202.481 231.802 206.909 232.094 211.546C232.387 216.183 231.217 220.794 228.749 224.731C226.281 228.667 222.64 231.729 218.339 233.486C213.406 235.513 208.505 238.77 207.164 243.91C205.823 249.051 208.393 254.335 211.666 258.549C214.513 262.22 216.136 266.693 216.306 271.335C216.476 275.978 215.184 280.557 212.613 284.427C210.042 288.297 206.321 291.262 201.975 292.904C197.629 294.546 192.877 294.783 188.39 293.581C183.902 292.379 179.905 289.799 176.962 286.204C174.019 282.609 172.278 278.181 171.985 273.545C171.693 268.908 172.863 264.296 175.331 260.36C177.799 256.424 181.44 253.361 185.741 251.605C190.658 249.577 195.543 246.337 196.915 241.18Z" fill="white"/>
<path d="M246.195 184.797C243.529 180.184 237.957 178.316 232.673 177.629C228.069 177.045 223.764 175.03 220.365 171.869C216.967 168.708 214.646 164.56 213.729 160.01C212.813 155.46 213.348 150.737 215.258 146.507C217.168 142.277 220.357 138.753 224.376 136.431C228.395 134.11 233.041 133.108 237.66 133.567C242.279 134.026 246.637 135.923 250.12 138.991C253.604 142.058 256.037 146.141 257.077 150.664C258.117 155.188 257.711 159.923 255.917 164.204C253.857 169.137 252.724 174.9 255.342 179.513C257.96 184.127 263.595 186.01 268.879 186.681C273.484 187.267 277.789 189.283 281.186 192.446C284.584 195.608 286.904 199.757 287.819 204.308C288.733 208.859 288.197 213.582 286.285 217.811C284.372 222.041 281.181 225.564 277.16 227.884C273.14 230.203 268.492 231.203 263.874 230.741C259.255 230.279 254.897 228.379 251.416 225.31C247.934 222.24 245.503 218.155 244.466 213.63C243.429 209.106 243.838 204.37 245.636 200.09C247.695 195.173 248.861 189.411 246.195 184.797Z" fill="white"/>
<path d="M241.18 110.07C236.024 108.697 230.756 111.315 226.542 114.588C222.87 117.435 218.398 119.058 213.755 119.228C209.112 119.398 204.533 118.106 200.664 115.535C196.794 112.964 193.829 109.243 192.187 104.897C190.544 100.551 190.307 95.7994 191.509 91.3117C192.712 86.824 195.292 82.8268 198.887 79.8837C202.481 76.9405 206.909 75.2 211.546 74.9073C216.183 74.6147 220.794 75.7849 224.731 78.2527C228.667 80.7206 231.729 84.3617 233.486 88.6627C235.513 93.5955 238.754 98.4964 243.91 99.8374C249.066 101.178 254.335 98.6082 258.549 95.3356C262.22 92.4959 266.69 90.8798 271.328 90.7151C275.967 90.5503 280.54 91.8452 284.403 94.4172C288.267 96.9892 291.226 100.709 292.863 105.052C294.501 109.394 294.734 114.142 293.531 118.624C292.327 123.107 289.748 127.099 286.155 130.037C282.563 132.976 278.139 134.714 273.507 135.005C268.875 135.296 264.268 134.126 260.336 131.661C256.403 129.195 253.344 125.557 251.589 121.261C249.577 116.36 246.321 111.459 241.18 110.07Z" fill="white"/>
<path d="M153.491 191.533C174.501 191.533 191.533 174.501 191.533 153.491C191.533 132.482 174.501 115.45 153.491 115.45C132.481 115.45 115.449 132.482 115.449 153.491C115.449 174.501 132.481 191.533 153.491 191.533Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

+61
View File
@@ -0,0 +1,61 @@
{
"name": "n8n-nodes-mem0",
"version": "0.1.0",
"description": "n8n community node for Mem0 — the memory layer for AI agents. Add, search, get, update, and delete long-term memories.",
"keywords": [
"n8n-community-node-package",
"mem0",
"memory",
"ai",
"agents",
"llm"
],
"license": "MIT",
"homepage": "https://mem0.ai",
"author": {
"name": "Mem0",
"email": "founders@mem0.ai"
},
"repository": {
"type": "git",
"url": "https://github.com/mem0ai/mem0",
"directory": "integrations/n8n-nodes-mem0"
},
"engines": {
"node": ">=20.15"
},
"main": "index.js",
"scripts": {
"build": "npx rimraf dist && tsc && gulp build:icons",
"dev": "tsc --watch",
"format": "prettier nodes credentials --write",
"lint": "eslint nodes credentials package.json",
"lintfix": "eslint nodes credentials package.json --fix",
"prepublishOnly": "npm run build && npm run lint"
},
"files": [
"dist"
],
"n8n": {
"n8nNodesApiVersion": 1,
"credentials": [
"dist/credentials/Mem0Api.credentials.js"
],
"nodes": [
"dist/nodes/Mem0/Mem0.node.js"
]
},
"devDependencies": {
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^8.57.0",
"eslint-plugin-n8n-nodes-base": "^1.16.3",
"gulp": "^5.0.0",
"n8n-workflow": "*",
"prettier": "^3.3.0",
"rimraf": "^5.0.0",
"typescript": "^5.5.0"
},
"peerDependencies": {
"n8n-workflow": "*"
}
}
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"strict": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "es2019",
"lib": ["es2019", "es2020", "es2022.error"],
"removeComments": true,
"useUnknownInCatchVariables": false,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"preserveConstEnums": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"incremental": true,
"declaration": true,
"sourceMap": true,
"skipLibCheck": true,
"outDir": "./dist/"
},
"include": ["credentials/**/*", "nodes/**/*"],
"exclude": ["node_modules", "dist"]
}
+8
View File
@@ -0,0 +1,8 @@
node_modules/
package-lock.json
.env
.zapierapprc
build/
coverage/
*.log
.DS_Store
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Mem0
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+35
View File
@@ -0,0 +1,35 @@
# Zapier integration for Mem0
A [Zapier](https://zapier.com) integration for [Mem0](https://mem0.ai) — the memory layer for AI agents. Add, search, list, and delete long-term memories from any Zap.
Built with the [Zapier Platform CLI](https://docs.zapier.com/platform/quickstart/cli-tutorial).
## Actions
| Type | Name | Endpoint |
| --- | --- | --- |
| Create | **Add Memory** | `POST /v3/memories/add/` |
| Create | **Delete Memory** | `DELETE /v1/memories/{id}/` |
| Search | **Search Memories** | `POST /v3/memories/search/` |
| Search | **Get Memories** | `POST /v3/memories/` |
**Add Memory** runs LLM extraction asynchronously and returns immediately with an event ID by default. Turn on **Wait for Completion** to have the action poll until extraction finishes and return the resulting memories — note that extraction can take longer than Zapier allows a single step to run, and a timeout there does **not** mean the add failed (it typically still completes server-side). Set **Infer = false** to store the message verbatim instead of extracting.
**Get Memories** returns one page at a time; use the **Page** and **Limit** fields to page through larger result sets.
## Authentication
Custom (API key) auth. Provide a Mem0 API key from [app.mem0.ai](https://app.mem0.ai) → Settings → API Keys. It is sent as `Authorization: Token <key>`.
## Development
```bash
npm install
MEM0_API_KEY=m0-... npm test # runs the E2E suite against api.mem0.ai
```
To deploy (maintainers): `zapier login && zapier push`.
## License
MIT
@@ -0,0 +1,31 @@
'use strict';
// Custom (API key) authentication for Mem0.
// The key is sent as "Authorization: Token <apiKey>" (matches Mem0's SDKs).
const test = (z, _bundle) =>
z.request({ url: '/v1/ping/', method: 'GET' });
module.exports = {
type: 'custom',
test,
fields: [
{
key: 'apiKey',
label: 'Mem0 API Key',
type: 'string',
required: true,
helpText:
'Your Mem0 API key (starts with `m0-`). Create one at [app.mem0.ai](https://app.mem0.ai) → Settings → API Keys.',
},
{
key: 'baseUrl',
label: 'Base URL',
type: 'string',
required: false,
default: 'https://api.mem0.ai',
helpText: 'Override only for self-hosted or non-default deployments.',
},
],
// Shown on the connection label in the Zap editor.
connectionLabel: 'Mem0',
};
@@ -0,0 +1,120 @@
'use strict';
const POLL_INTERVAL_MS = 1500;
// Bounded so the poll budget stays under Zapier's per-step execution timeout.
const MAX_POLL_ATTEMPTS = 12;
// Polls GET /v1/event/{id}/ until the async memory-addition event resolves.
const pollEvent = async (z, eventId) => {
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
const res = await z.request({ url: `/v1/event/${eventId}/`, method: 'GET' });
const status = res.data && res.data.status;
if (status === 'SUCCEEDED') {
return res.data;
}
if (status === 'FAILED') {
const reason = (res.data && (res.data.error || res.data.message)) || 'unknown error';
throw new z.errors.Error(`Mem0 memory event ${eventId} failed: ${reason}`, 'Mem0EventFailed', 400);
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
}
throw new z.errors.Error(
`Timed out waiting for memory event ${eventId}. The add was accepted and is ` +
`likely still completing on the server — a timeout here does not mean it failed.`,
'Mem0Timeout',
408,
);
};
const perform = async (z, bundle) => {
// Zapier boolean fields can arrive as the strings 'true'/'false'; coerce
// explicitly so "Infer = No" / "Wait = No" are honored.
const infer = String(bundle.inputData.infer) !== 'false';
// Waiting is opt-in (the poll path can exceed Zapier's step timeout).
const wait = String(bundle.inputData.waitForCompletion) === 'true';
const body = {
messages: [{ role: bundle.inputData.role || 'user', content: bundle.inputData.content }],
infer,
};
if (bundle.inputData.user_id) body.user_id = bundle.inputData.user_id;
if (bundle.inputData.agent_id) body.agent_id = bundle.inputData.agent_id;
if (bundle.inputData.run_id) body.run_id = bundle.inputData.run_id;
if (bundle.inputData.metadata) {
try {
body.metadata =
typeof bundle.inputData.metadata === 'string'
? JSON.parse(bundle.inputData.metadata)
: bundle.inputData.metadata;
} catch (e) {
throw new z.errors.Error('Metadata must be valid JSON.', 'InvalidInput', 400);
}
}
const response = await z.request({
url: '/v3/memories/add/',
method: 'POST',
body,
});
const data = response.data;
// Add returns {event_id, status:PENDING|RUNNING}; poll only when opted in.
if (wait && data.event_id && data.status !== 'SUCCEEDED' && data.status !== 'FAILED') {
return pollEvent(z, data.event_id);
}
if (data.status === 'FAILED') {
const reason = data.error || data.message || 'unknown error';
throw new z.errors.Error(`Mem0 memory add failed: ${reason}`, 'Mem0EventFailed', 400);
}
return data;
};
module.exports = {
key: 'add_memory',
noun: 'Memory',
display: {
label: 'Add Memory',
description: 'Extract and store memories from a message.',
},
operation: {
perform,
inputFields: [
{
key: 'content',
label: 'Content',
type: 'text',
required: true,
helpText: 'The message content to extract memories from.',
},
{
key: 'role',
label: 'Role',
choices: { user: 'User', assistant: 'Assistant', system: 'System' },
default: 'user',
},
{ key: 'user_id', label: 'User ID', type: 'string' },
{ key: 'agent_id', label: 'Agent ID', type: 'string' },
{ key: 'run_id', label: 'Run ID', type: 'string' },
{ key: 'metadata', label: 'Metadata (JSON)', type: 'string' },
{
key: 'infer',
label: 'Infer',
type: 'boolean',
default: 'true',
helpText: 'Run LLM extraction over the message. Turn off to store it verbatim.',
},
{
key: 'waitForCompletion',
label: 'Wait for Completion',
type: 'boolean',
default: 'false',
helpText:
'Poll until extraction finishes and return the resulting memories. ' +
'Leave off (default) to return immediately with an event ID — extraction can take ' +
'longer than Zapier allows this step to run, and a timeout does not mean the add failed.',
},
],
sample: { status: 'SUCCEEDED', event_id: '00000000-0000-0000-0000-000000000000', results: [] },
},
};
@@ -0,0 +1,26 @@
'use strict';
const perform = async (z, bundle) => {
// Trailing slash required (Django APPEND_SLASH); id encoded so a stray slash can't mistarget the path.
const response = await z.request({
url: `/v1/memories/${encodeURIComponent(bundle.inputData.memory_id)}/`,
method: 'DELETE',
});
return response.data || { message: 'Deleted', memory_id: bundle.inputData.memory_id };
};
module.exports = {
key: 'delete_memory',
noun: 'Memory',
display: {
label: 'Delete Memory',
description: 'Delete a single memory by its ID.',
},
operation: {
perform,
inputFields: [
{ key: 'memory_id', label: 'Memory ID', type: 'string', required: true },
],
sample: { message: 'Memory deleted successfully' },
},
};
+35
View File
@@ -0,0 +1,35 @@
'use strict';
const authentication = require('./authentication');
const { includeApiKey, handleBadResponses } = require('./middleware');
const addMemory = require('./creates/add_memory');
const deleteMemory = require('./creates/delete_memory');
const searchMemories = require('./searches/search_memories');
const getMemories = require('./searches/get_memories');
const { version } = require('./package.json');
const platformVersion = require('zapier-platform-core').version;
module.exports = {
version,
platformVersion,
authentication,
beforeRequest: [includeApiKey],
afterResponse: [handleBadResponses],
creates: {
[addMemory.key]: addMemory,
[deleteMemory.key]: deleteMemory,
},
searches: {
[searchMemories.key]: searchMemories,
[getMemories.key]: getMemories,
},
resources: {},
triggers: {},
};
+40
View File
@@ -0,0 +1,40 @@
'use strict';
// Prepend the configured base URL and inject the auth header on every request.
const includeApiKey = (request, z, bundle) => {
if (bundle.authData && bundle.authData.apiKey) {
request.headers = request.headers || {};
request.headers.Authorization = `Token ${bundle.authData.apiKey}`;
}
// Resolve relative URLs against the configured base URL.
if (request.url && request.url.startsWith('/')) {
const base = (bundle.authData && bundle.authData.baseUrl) || 'https://api.mem0.ai';
request.url = `${base.replace(/\/$/, '')}${request.url}`;
}
return request;
};
// Surface HTTP failures as errors. z.request does NOT throw on non-2xx by
// default, so without this a 4xx/5xx would flow downstream as a fake success
// (empty search results / error body returned as a created memory).
const handleBadResponses = (response, z, _bundle) => {
if (response.status === 401 || response.status === 403) {
throw new z.errors.Error(
'The Mem0 API key you supplied is invalid or lacks access.',
'AuthenticationError',
response.status,
);
}
if (response.status >= 400) {
const data = response.data || {};
const detail = data.detail || data.error || data.message || response.content || 'unknown error';
throw new z.errors.Error(
`Mem0 API request failed (HTTP ${response.status}): ${detail}`,
'Mem0ApiError',
response.status,
);
}
return response;
};
module.exports = { includeApiKey, handleBadResponses };
+39
View File
@@ -0,0 +1,39 @@
{
"name": "zapier-mem0",
"version": "0.1.0",
"description": "Zapier integration for Mem0 — the memory layer for AI agents.",
"keywords": [
"zapier",
"mem0",
"memory",
"ai",
"agents"
],
"homepage": "https://mem0.ai",
"author": {
"name": "Mem0",
"email": "founders@mem0.ai"
},
"repository": {
"type": "git",
"url": "https://github.com/mem0ai/mem0",
"directory": "integrations/zapier-mem0"
},
"license": "MIT",
"main": "index.js",
"scripts": {
"test": "jest --testTimeout 120000",
"test:unit": "jest test/unit.test.js"
},
"engines": {
"node": ">=18",
"npm": ">=5.6.0"
},
"dependencies": {
"zapier-platform-core": "19.0.0"
},
"devDependencies": {
"jest": "^29.7.0"
},
"private": true
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,48 @@
'use strict';
const perform = async (z, bundle) => {
const body = {};
if (bundle.inputData.user_id) body.filters = { user_id: bundle.inputData.user_id };
const response = await z.request({
url: '/v3/memories/',
method: 'POST',
params: {
page: Math.max(1, Math.floor(Number(bundle.inputData.page) || 1)),
page_size: Math.max(1, Math.floor(Number(bundle.inputData.limit) || 50)),
},
body,
});
const data = response.data;
return Array.isArray(data) ? data : data.results || [];
};
module.exports = {
key: 'get_memories',
noun: 'Memory',
display: {
label: 'Get Memories',
description: 'List stored memories for a user.',
},
operation: {
perform,
inputFields: [
{ key: 'user_id', label: 'User ID', type: 'string', required: true },
{
key: 'limit',
label: 'Limit',
type: 'integer',
default: '50',
helpText: 'Max memories per page. Use Page to page through larger result sets.',
},
{
key: 'page',
label: 'Page',
type: 'integer',
default: '1',
helpText: 'Which page of results to return (1-based).',
},
],
sample: { id: '00000000-0000-0000-0000-000000000000', memory: 'User loves hiking' },
},
};
@@ -0,0 +1,37 @@
'use strict';
const perform = async (z, bundle) => {
const body = {
query: bundle.inputData.query,
output_format: 'v1.1',
top_k: Math.max(1, Math.floor(Number(bundle.inputData.limit) || 50)),
};
if (bundle.inputData.user_id) body.filters = { user_id: bundle.inputData.user_id };
const response = await z.request({
url: '/v3/memories/search/',
method: 'POST',
body,
});
// Searches must return an array.
const data = response.data;
return Array.isArray(data) ? data : data.results || [];
};
module.exports = {
key: 'search_memories',
noun: 'Memory',
display: {
label: 'Search Memories',
description: 'Semantic search over stored memories.',
},
operation: {
perform,
inputFields: [
{ key: 'query', label: 'Query', type: 'string', required: true },
{ key: 'user_id', label: 'User ID', type: 'string', required: true },
{ key: 'limit', label: 'Limit', type: 'integer', default: '50' },
],
sample: { id: '00000000-0000-0000-0000-000000000000', memory: 'User loves hiking' },
},
};
@@ -0,0 +1,95 @@
'use strict';
/* global describe, it, expect, beforeAll */
const zapier = require('zapier-platform-core');
const App = require('../index');
const appTester = zapier.createAppTester(App);
const authData = {
apiKey: process.env.MEM0_API_KEY,
baseUrl: process.env.MEM0_BASE_URL || 'https://api.mem0.ai',
};
const userId = `zapier-e2e-${Date.now()}`;
// Retry an async op until `done` is satisfied or attempts run out. Extraction is
// async, so the default Add returns before the memory is searchable.
const until = async (fn, done, { attempts = 30, delayMs = 2000 } = {}) => {
let last;
for (let i = 0; i < attempts; i++) {
last = await fn();
if (done(last)) return last;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
return last;
};
// The E2E suite hits the live Mem0 API, so it only runs when MEM0_API_KEY is
// set (locally / with a secret). In CI without a key it is skipped, not failed.
const describeE2E = authData.apiKey ? describe : describe.skip;
describeE2E('Mem0 Zapier integration (E2E)', () => {
it('authentication.test succeeds', async () => {
const res = await appTester(App.authentication.test, { authData });
expect(res.status).toBe(200);
});
it('adds, searches, lists, and deletes a memory', async () => {
// Add via the default path: returns immediately with an event id.
const added = await appTester(App.creates.add_memory.operation.perform, {
authData,
inputData: {
content: 'I love hiking in the Alps and my favorite food is sushi',
user_id: userId,
},
});
expect(added.event_id).toBeDefined();
// Extraction is async; retry search until the memory is indexed.
const found = await until(
() =>
appTester(App.searches.search_memories.operation.perform, {
authData,
inputData: { query: 'outdoor activities', user_id: userId, limit: 5 },
}),
(r) => Array.isArray(r) && r.length > 0,
);
expect(Array.isArray(found)).toBe(true);
expect(found.length).toBeGreaterThan(0);
// Get all
const all = await appTester(App.searches.get_memories.operation.perform, {
authData,
inputData: { user_id: userId },
});
expect(Array.isArray(all)).toBe(true);
expect(all.length).toBeGreaterThan(0);
// Cleanup: delete every memory we created
for (const mem of all) {
await appTester(App.creates.delete_memory.operation.perform, {
authData,
inputData: { memory_id: mem.id },
});
}
const afterDelete = await appTester(App.searches.get_memories.operation.perform, {
authData,
inputData: { user_id: userId },
});
expect(afterDelete.length).toBe(0);
});
it('surfaces API errors instead of returning an empty array (search needs a filter)', async () => {
// filters is required by the API; omitting it must throw, not return [].
await expect(
appTester(App.searches.search_memories.operation.perform, {
authData,
inputData: { query: 'anything' },
}),
).rejects.toThrow();
});
});
+124
View File
@@ -0,0 +1,124 @@
'use strict';
/* global describe, it, expect */
// Offline unit tests: they mock `z.request`, so they run unconditionally in CI
// (unlike the live E2E suite in mem0.test.js, gated on MEM0_API_KEY). They cover
// what `zapier validate` can't: boolean coercion, URL join, metadata, array shapes.
const addMemory = require('../creates/add_memory');
const deleteMemory = require('../creates/delete_memory');
const searchMemories = require('../searches/search_memories');
const getMemories = require('../searches/get_memories');
const { includeApiKey } = require('../middleware');
// Minimal `z` stub: hands back queued responses and records every request.
const makeZ = (responses = []) => {
const queue = [...responses];
const requests = [];
return {
requests,
request: async (opts) => {
requests.push(opts);
const next = queue.shift();
return next !== undefined ? next : { data: {} };
},
errors: {
Error: class Mem0Error extends Error {
constructor(message, name, status) {
super(message);
this.name = name || 'Error';
this.status = status;
}
},
},
};
};
describe('add_memory (offline)', () => {
it('coerces infer="false" to a boolean and does not poll by default', async () => {
const z = makeZ([{ data: { event_id: 'e1', status: 'PENDING' } }]);
const res = await addMemory.operation.perform(z, {
inputData: { content: 'hi', user_id: 'u1', infer: 'false' },
});
// waitForCompletion defaults off -> a single request (the add), no poll.
expect(z.requests).toHaveLength(1);
expect(z.requests[0].body.infer).toBe(false);
expect(res.status).toBe('PENDING');
});
it('polls the event only when waitForCompletion="true"', async () => {
const z = makeZ([
{ data: { event_id: 'e1', status: 'PENDING' } },
{ data: { status: 'SUCCEEDED', results: [{ id: 'm1' }] } },
]);
const res = await addMemory.operation.perform(z, {
inputData: { content: 'hi', user_id: 'u1', waitForCompletion: 'true' },
});
expect(z.requests).toHaveLength(2);
expect(z.requests[1].url).toBe('/v1/event/e1/');
expect(res.status).toBe('SUCCEEDED');
});
it('throws a clear error on invalid JSON metadata', async () => {
const z = makeZ();
await expect(
addMemory.operation.perform(z, {
inputData: { content: 'hi', user_id: 'u1', metadata: '{not json' },
}),
).rejects.toThrow('Metadata must be valid JSON.');
});
});
describe('search / get array-shape enforcement (offline)', () => {
it('search unwraps an object {results:[...]} into an array', async () => {
const z = makeZ([{ data: { results: [{ id: 'm1' }] } }]);
const res = await searchMemories.operation.perform(z, {
inputData: { query: 'x', user_id: 'u1' },
});
expect(Array.isArray(res)).toBe(true);
expect(res).toHaveLength(1);
});
it('get_memories returns [] when the API returns neither array nor results', async () => {
const z = makeZ([{ data: {} }]);
const res = await getMemories.operation.perform(z, { inputData: { user_id: 'u1' } });
expect(Array.isArray(res)).toBe(true);
expect(res).toHaveLength(0);
});
it('get_memories forwards page and page_size as numbers', async () => {
const z = makeZ([{ data: { results: [] } }]);
await getMemories.operation.perform(z, {
inputData: { user_id: 'u1', page: '2', limit: '10' },
});
expect(z.requests[0].params).toEqual({ page: 2, page_size: 10 });
});
});
describe('delete_memory (offline)', () => {
it('encodes the memory id in the URL path', async () => {
const z = makeZ([{ data: {} }]);
await deleteMemory.operation.perform(z, { inputData: { memory_id: 'a/b c' } });
expect(z.requests[0].url).toBe('/v1/memories/a%2Fb%20c/');
});
});
describe('includeApiKey middleware (offline)', () => {
it('prepends the base URL and injects the auth header', () => {
const req = includeApiKey(
{ url: '/v3/memories/' },
null,
{ authData: { apiKey: 'k', baseUrl: 'https://api.mem0.ai/' } },
);
expect(req.url).toBe('https://api.mem0.ai/v3/memories/');
expect(req.headers.Authorization).toBe('Token k');
});
it('leaves absolute URLs untouched', () => {
const req = includeApiKey({ url: 'https://other.example/x' }, null, {
authData: { apiKey: 'k' },
});
expect(req.url).toBe('https://other.example/x');
});
});
-1
View File
@@ -59,7 +59,6 @@ export interface ProjectOptions {
export interface PromptUpdatePayload {
customInstructions?: string;
customCategories?: custom_categories[];
retrievalCriteria?: any[];
version?: string;
memoryDepth?: string | null;
usecaseSetting?: string | number;
@@ -0,0 +1,292 @@
import { Embedder } from "./base";
import { EmbeddingConfig } from "../types";
const DEFAULT_MODEL = "amazon.titan-embed-text-v1";
const DEFAULT_REGION = "us-west-2";
// Cohere's Bedrock embed API rejects an InvokeModel call carrying more than 96
// texts, so `embedBatch` chunks at that boundary.
const COHERE_MAX_BATCH = 96;
// Titan has no server-side batch endpoint -- one InvokeModel call per text --
// so without a cap a large embedBatch() would fan out one request per text.
// Bounds concurrency the same way COHERE_MAX_BATCH bounds the Cohere path.
const TITAN_MAX_CONCURRENCY = 4;
// Cohere wants to know whether a text is being embedded for storage or for a
// retrieval query; embedding a search query in document mode silently
// degrades retrieval. Titan ignores this and has no equivalent parameter.
const COHERE_INPUT_TYPES: Record<"add" | "update" | "search", string> = {
add: "search_document",
update: "search_document",
search: "search_query",
};
type BedrockRuntimeModule = typeof import("@aws-sdk/client-bedrock-runtime");
interface BedrockCredentials {
accessKeyId: string;
secretAccessKey: string;
sessionToken?: string;
}
interface BedrockEmbeddingResponse {
// Titan returns a single vector. Cohere v3 returns a flat array of vectors;
// Cohere v4, when `embedding_types` is requested, nests it as `{ float }`.
embedding?: number[];
embeddings?: number[][] | { float?: number[][] };
}
/**
* Runs `fn` over `items` with at most `limit` calls in flight at once,
* returning results in input order regardless of completion order.
*/
async function mapWithConcurrencyLimit<T, R>(
items: T[],
limit: number,
fn: (item: T) => Promise<R>,
): Promise<R[]> {
const results: R[] = new Array(items.length);
let next = 0;
async function worker(): Promise<void> {
while (next < items.length) {
const index = next++;
results[index] = await fn(items[index]);
}
}
await Promise.all(
Array.from({ length: Math.min(limit, items.length) }, worker),
);
return results;
}
/**
* AWS Bedrock embedder, mirroring `mem0/embeddings/aws_bedrock.py`.
*
* Supports the Amazon Titan and Cohere embedding model families. The
* `@aws-sdk/client-bedrock-runtime` dependency is lazily imported so the
* package stays optional: importing this module never forces the SDK to be
* installed until a Bedrock embedder actually embeds something.
*/
export class AWSBedrockEmbedder implements Embedder {
private readonly model: string;
private readonly region: string;
private readonly embeddingDims?: number;
private readonly credentials?: BedrockCredentials;
private clientPromise?: Promise<{
sdk: BedrockRuntimeModule;
client: { send: (command: any) => Promise<{ body?: Uint8Array }> };
}>;
constructor(config: EmbeddingConfig) {
this.model = config.model || DEFAULT_MODEL;
this.region = config.awsRegion || process.env.AWS_REGION || DEFAULT_REGION;
this.embeddingDims = config.embeddingDims;
const hasKeyPair = Boolean(
config.awsAccessKeyId && config.awsSecretAccessKey,
);
const hasAnyCredential = Boolean(
config.awsAccessKeyId ||
config.awsSecretAccessKey ||
config.awsSessionToken,
);
// Partially configured credentials would silently fall back to the default
// chain, embedding under an identity the caller never chose.
if (hasAnyCredential && !hasKeyPair) {
throw new Error(
"AWS Bedrock requires both awsAccessKeyId and awsSecretAccessKey when any explicit credential is configured. " +
"Omit all credential fields to use the AWS default credential chain.",
);
}
// Leaving `credentials` unset lets the AWS SDK resolve them from its
// default chain: environment, shared config, SSO, or the instance role.
if (hasKeyPair) {
this.credentials = {
accessKeyId: config.awsAccessKeyId!,
secretAccessKey: config.awsSecretAccessKey!,
...(config.awsSessionToken && { sessionToken: config.awsSessionToken }),
};
}
}
private async loadSdk(): Promise<BedrockRuntimeModule> {
try {
return await import("@aws-sdk/client-bedrock-runtime");
} catch (error) {
// Only a genuine module-resolution failure gets the friendly install
// hint. Node's native ESM loader raises ERR_MODULE_NOT_FOUND; Jest's
// and bundlers' CJS-style resolvers raise MODULE_NOT_FOUND. Anything
// else (e.g. the package is installed but throws while loading, such
// as on a Node version older than the SDK's own engines requirement)
// rethrows unchanged instead of being misreported as "not installed".
const code = (error as { code?: string } | undefined)?.code;
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
throw Object.assign(
new Error(
"The '@aws-sdk/client-bedrock-runtime' package is required to use the AWS Bedrock embedder. " +
"Install it with: npm install @aws-sdk/client-bedrock-runtime",
),
{ cause: error },
);
}
throw error;
}
}
private async createClient(): Promise<{
sdk: BedrockRuntimeModule;
client: { send: (command: any) => Promise<{ body?: Uint8Array }> };
}> {
const sdk = await this.loadSdk();
return {
sdk,
client: new sdk.BedrockRuntimeClient({
region: this.region,
...(this.credentials && { credentials: this.credentials }),
}),
};
}
private getClient() {
// Memoized so concurrent embed() calls share one client instead of each
// racing to build their own. Cleared on rejection so a transient failure
// (e.g. a network blip while resolving credentials) doesn't permanently
// disable Bedrock for the rest of this embedder's lifetime.
if (!this.clientPromise) {
this.clientPromise = this.createClient().catch((err) => {
this.clientPromise = undefined;
throw err;
});
}
return this.clientPromise;
}
private isCohereModel(): boolean {
return this.model.startsWith("cohere.");
}
private isCohereV4Model(): boolean {
return this.model.includes("embed-v4");
}
private buildRequestBody(
texts: string[],
memoryAction?: "add" | "update" | "search",
): Record<string, unknown> {
if (this.isCohereModel()) {
const body: Record<string, unknown> = {
texts,
input_type: memoryAction
? COHERE_INPUT_TYPES[memoryAction]
: "search_document",
};
// Only Embed v4 understands embedding_types / output_dimension; v3
// rejects unknown fields, so they're guarded to the v4 model family.
if (this.isCohereV4Model()) {
body.embedding_types = ["float"];
if (this.embeddingDims !== undefined) {
body.output_dimension = this.embeddingDims;
}
}
return body;
}
// Titan accepts one text per call. Only Titan Text Embeddings V2 supports
// a caller-chosen output size (256/512/1024), so the field is guarded the
// same way the Python provider guards it.
return {
inputText: texts[0],
...(this.embeddingDims !== undefined &&
this.model.includes("titan-embed-text-v2") && {
dimensions: this.embeddingDims,
}),
};
}
private async invoke(
texts: string[],
memoryAction?: "add" | "update" | "search",
): Promise<number[][]> {
const { sdk, client } = await this.getClient();
let payload: BedrockEmbeddingResponse;
try {
const response = await client.send(
new sdk.InvokeModelCommand({
modelId: this.model,
contentType: "application/json",
accept: "application/json",
body: new TextEncoder().encode(
JSON.stringify(this.buildRequestBody(texts, memoryAction)),
),
}),
);
payload = JSON.parse(new TextDecoder().decode(response.body));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(
`Error getting embedding from AWS Bedrock model ${this.model}: ${message}`,
);
}
// Validated outside the try so this message is not re-wrapped by the catch.
// Cohere v3 replies with a flat `embeddings` array; v4 (when
// embedding_types is requested) nests it under `.float`.
const embeddings = this.isCohereModel()
? Array.isArray(payload.embeddings)
? payload.embeddings
: payload.embeddings?.float
: payload.embedding && [payload.embedding];
// `[]` is truthy, so a lone zero-length vector must be checked for
// explicitly -- otherwise it passes the length check and hands the
// caller an empty embedding instead of an error.
if (
!embeddings ||
embeddings.length !== texts.length ||
embeddings.some((embedding) => embedding.length === 0)
) {
throw new Error(
`AWS Bedrock model ${this.model} returned no embedding for one or more inputs`,
);
}
return embeddings;
}
async embed(
text: string,
memoryAction?: "add" | "update" | "search",
): Promise<number[]> {
return (await this.invoke([text], memoryAction))[0];
}
async embedBatch(
texts: string[],
memoryAction?: "add" | "update" | "search",
): Promise<number[][]> {
if (texts.length === 0) return [];
if (!this.isCohereModel()) {
return mapWithConcurrencyLimit(texts, TITAN_MAX_CONCURRENCY, (text) =>
this.embed(text, memoryAction),
);
}
const embeddings: number[][] = [];
for (let i = 0; i < texts.length; i += COHERE_MAX_BATCH) {
embeddings.push(
...(await this.invoke(
texts.slice(i, i + COHERE_MAX_BATCH),
memoryAction,
)),
);
}
return embeddings;
}
}
+1
View File
@@ -2,6 +2,7 @@ export * from "./memory";
export * from "./memory/memory.types";
export * from "./types";
export * from "./embeddings/base";
export * from "./embeddings/aws_bedrock";
export * from "./embeddings/huggingface";
export * from "./embeddings/openai";
export * from "./embeddings/ollama";
+9
View File
@@ -21,6 +21,11 @@ export interface EmbeddingConfig {
modelProperties?: Record<string, any>;
// HuggingFace TEI / OpenAI-compatible inference endpoint base URL.
huggingfaceBaseUrl?: string;
// AWS Bedrock. Omit the credential fields to use the AWS default chain.
awsRegion?: string;
awsAccessKeyId?: string;
awsSecretAccessKey?: string;
awsSessionToken?: string;
}
export interface VertexAIConfig extends EmbeddingConfig {
@@ -198,6 +203,10 @@ export const MemoryConfigSchema = z.object({
memoryAddEmbeddingType: z.string().optional(),
memoryUpdateEmbeddingType: z.string().optional(),
memorySearchEmbeddingType: z.string().optional(),
awsRegion: z.string().optional(),
awsAccessKeyId: z.string().optional(),
awsSecretAccessKey: z.string().optional(),
awsSessionToken: z.string().optional(),
}),
}),
vectorStore: z.object({
+3
View File
@@ -1,4 +1,5 @@
import { OpenAIEmbedder } from "../embeddings/openai";
import { AWSBedrockEmbedder } from "../embeddings/aws_bedrock";
import { OllamaEmbedder } from "../embeddings/ollama";
import { LMStudioEmbedder } from "../embeddings/lmstudio";
import { TogetherEmbedder } from "../embeddings/together";
@@ -76,6 +77,8 @@ export class EmbedderFactory {
switch (provider.toLowerCase()) {
case "openai":
return new OpenAIEmbedder(config);
case "aws_bedrock":
return new AWSBedrockEmbedder(config);
case "ollama":
return new OllamaEmbedder(config);
case "lmstudio":
@@ -0,0 +1,517 @@
import type { InvokeModelCommand } from "@aws-sdk/client-bedrock-runtime";
import { AWSBedrockEmbedder } from "../src/embeddings/aws_bedrock";
import type { Embedder } from "../src/embeddings/base";
import { EmbedderFactory } from "../src/utils/factory";
/**
* Only the network boundary is faked: `BedrockRuntimeClient.send` never leaves
* the process. `InvokeModelCommand` stays the real class from the AWS SDK, so
* every assertion below runs against the exact payload Bedrock would receive.
*/
const mockSend = jest.fn();
const mockClientConfigs: any[] = [];
const mockClientConstructor = jest
.fn()
.mockImplementation((config: unknown) => {
mockClientConfigs.push(config);
return { send: mockSend };
});
jest.mock("@aws-sdk/client-bedrock-runtime", () => {
const actual = jest.requireActual("@aws-sdk/client-bedrock-runtime");
return {
...actual,
BedrockRuntimeClient: mockClientConstructor,
};
});
const encode = (payload: unknown) => ({
body: new TextEncoder().encode(JSON.stringify(payload)),
});
const titanReply = (embedding: number[]) =>
encode({ embedding, inputTextTokenCount: embedding.length });
const cohereReply = (embeddings: number[][]) =>
encode({ embeddings, id: "req-1", response_type: "embeddings_floats" });
const commandAt = (index: number): InvokeModelCommand =>
mockSend.mock.calls[index][0];
const requestBodyAt = (index: number) =>
JSON.parse(new TextDecoder().decode(commandAt(index).input.body));
describe("AWSBedrockEmbedder", () => {
const savedEnv = { ...process.env };
beforeEach(() => {
jest.clearAllMocks();
mockClientConfigs.length = 0;
delete process.env.AWS_REGION;
});
afterAll(() => {
process.env = savedEnv;
});
describe("Titan models", () => {
it("sends inputText and returns the embedding vector", async () => {
mockSend.mockResolvedValueOnce(titanReply([0.1, 0.2, 0.3]));
const embedder = new AWSBedrockEmbedder({});
const embedding = await embedder.embed("hello world");
expect(embedding).toEqual([0.1, 0.2, 0.3]);
expect(mockSend).toHaveBeenCalledTimes(1);
expect(commandAt(0).input.modelId).toBe("amazon.titan-embed-text-v1");
expect(commandAt(0).input.contentType).toBe("application/json");
expect(commandAt(0).input.accept).toBe("application/json");
expect(requestBodyAt(0)).toEqual({ inputText: "hello world" });
});
it("forwards dimensions to Titan V2 when embeddingDims is set", async () => {
mockSend.mockResolvedValueOnce(titanReply([0.1, 0.2]));
const embedder = new AWSBedrockEmbedder({
model: "amazon.titan-embed-text-v2:0",
embeddingDims: 512,
});
await embedder.embed("hello");
expect(requestBodyAt(0)).toEqual({ inputText: "hello", dimensions: 512 });
});
it("omits dimensions on Titan V1, which rejects the field", async () => {
mockSend.mockResolvedValueOnce(titanReply([0.1, 0.2]));
const embedder = new AWSBedrockEmbedder({
model: "amazon.titan-embed-text-v1",
embeddingDims: 512,
});
await embedder.embed("hello");
expect(requestBodyAt(0)).toEqual({ inputText: "hello" });
});
// F6: the old guard was `model.includes("v2")`, which would also match
// any future/other Titan model whose id merely contains "v2" somewhere
// (e.g. an image model), wrongly sending `dimensions` to a model that may
// reject it. Only Titan Text Embeddings V2 should get the field.
it("does not forward dimensions to a non-Titan-V2 model whose name merely contains v2", async () => {
mockSend.mockResolvedValueOnce(titanReply([0.1, 0.2]));
const embedder = new AWSBedrockEmbedder({
model: "amazon.titan-embed-image-v2:0",
embeddingDims: 512,
});
await embedder.embed("hello");
expect(requestBodyAt(0)).toEqual({ inputText: "hello" });
});
it("embedBatch issues one request per text and preserves order", async () => {
mockSend
.mockResolvedValueOnce(titanReply([1, 1]))
.mockResolvedValueOnce(titanReply([2, 2]));
const embedder = new AWSBedrockEmbedder({});
const embeddings = await embedder.embedBatch(["first", "second"]);
expect(embeddings).toEqual([
[1, 1],
[2, 2],
]);
expect(mockSend).toHaveBeenCalledTimes(2);
expect(requestBodyAt(0)).toEqual({ inputText: "first" });
expect(requestBodyAt(1)).toEqual({ inputText: "second" });
});
});
describe("Cohere models", () => {
it("sends texts with a search_document input type", async () => {
mockSend.mockResolvedValueOnce(cohereReply([[0.4, 0.5]]));
const embedder = new AWSBedrockEmbedder({
model: "cohere.embed-english-v3",
});
const embedding = await embedder.embed("hello");
expect(embedding).toEqual([0.4, 0.5]);
expect(requestBodyAt(0)).toEqual({
texts: ["hello"],
input_type: "search_document",
});
});
it("embedBatch sends every text in a single request", async () => {
mockSend.mockResolvedValueOnce(cohereReply([[1], [2], [3]]));
const embedder = new AWSBedrockEmbedder({
model: "cohere.embed-multilingual-v3",
});
const embeddings = await embedder.embedBatch(["a", "b", "c"]);
expect(embeddings).toEqual([[1], [2], [3]]);
expect(mockSend).toHaveBeenCalledTimes(1);
expect(requestBodyAt(0).texts).toEqual(["a", "b", "c"]);
});
it("embedBatch splits requests at Cohere's 96 text limit", async () => {
const texts = Array.from({ length: 100 }, (_, i) => `text-${i}`);
mockSend
.mockResolvedValueOnce(
cohereReply(texts.slice(0, 96).map((_, i) => [i])),
)
.mockResolvedValueOnce(cohereReply(texts.slice(96).map((_, i) => [i])));
const embedder = new AWSBedrockEmbedder({
model: "cohere.embed-english-v3",
});
const embeddings = await embedder.embedBatch(texts);
expect(embeddings).toHaveLength(100);
expect(mockSend).toHaveBeenCalledTimes(2);
expect(requestBodyAt(0).texts).toHaveLength(96);
expect(requestBodyAt(1).texts).toEqual([
"text-96",
"text-97",
"text-98",
"text-99",
]);
});
});
describe("Cohere Embed v4", () => {
// F5: only Embed v4 understands embedding_types / output_dimension, and
// (when embedding_types is requested) replies with a nested
// `{ embeddings: { float: [...] } }` shape instead of v3's flat array.
it("requests embedding_types and output_dimension for v4 models", async () => {
mockSend.mockResolvedValueOnce(
encode({ embeddings: { float: [[0.1, 0.2, 0.3]] } }),
);
const embedder = new AWSBedrockEmbedder({
model: "cohere.embed-v4:0",
embeddingDims: 512,
});
const embedding = await embedder.embed("hello");
expect(embedding).toEqual([0.1, 0.2, 0.3]);
expect(requestBodyAt(0)).toEqual({
texts: ["hello"],
input_type: "search_document",
embedding_types: ["float"],
output_dimension: 512,
});
});
it("omits output_dimension for v4 when embeddingDims is unset", async () => {
mockSend.mockResolvedValueOnce(
encode({ embeddings: { float: [[0.1, 0.2]] } }),
);
const embedder = new AWSBedrockEmbedder({ model: "cohere.embed-v4:0" });
await embedder.embed("hello");
expect(requestBodyAt(0)).toEqual({
texts: ["hello"],
input_type: "search_document",
embedding_types: ["float"],
});
});
it("parses the nested embeddings.float response shape", async () => {
mockSend.mockResolvedValueOnce(
encode({
embeddings: {
float: [
[1, 2],
[3, 4],
],
},
}),
);
const embedder = new AWSBedrockEmbedder({ model: "cohere.embed-v4:0" });
const embeddings = await embedder.embedBatch(["a", "b"]);
expect(embeddings).toEqual([
[1, 2],
[3, 4],
]);
});
});
describe("client configuration", () => {
it("defaults to the us-west-2 region", async () => {
mockSend.mockResolvedValueOnce(titanReply([1]));
await new AWSBedrockEmbedder({}).embed("hello");
expect(mockClientConfigs[0].region).toBe("us-west-2");
});
it("prefers awsRegion over the AWS_REGION environment variable", async () => {
process.env.AWS_REGION = "eu-central-1";
mockSend.mockResolvedValueOnce(titanReply([1]));
await new AWSBedrockEmbedder({ awsRegion: "ap-south-1" }).embed("hello");
expect(mockClientConfigs[0].region).toBe("ap-south-1");
});
it("falls back to the AWS_REGION environment variable", async () => {
process.env.AWS_REGION = "eu-central-1";
mockSend.mockResolvedValueOnce(titanReply([1]));
await new AWSBedrockEmbedder({}).embed("hello");
expect(mockClientConfigs[0].region).toBe("eu-central-1");
});
it("passes explicitly configured credentials to the client", async () => {
mockSend.mockResolvedValueOnce(titanReply([1]));
await new AWSBedrockEmbedder({
awsAccessKeyId: "AKIA_TEST",
awsSecretAccessKey: "secret",
awsSessionToken: "token",
}).embed("hello");
expect(mockClientConfigs[0].credentials).toEqual({
accessKeyId: "AKIA_TEST",
secretAccessKey: "secret",
sessionToken: "token",
});
});
it("leaves credentials unset so the AWS default credential chain applies", async () => {
mockSend.mockResolvedValueOnce(titanReply([1]));
await new AWSBedrockEmbedder({}).embed("hello");
expect(mockClientConfigs[0].credentials).toBeUndefined();
});
it("rejects a half-configured credential pair", () => {
expect(
() => new AWSBedrockEmbedder({ awsAccessKeyId: "AKIA_TEST" }),
).toThrow(/awsAccessKeyId and awsSecretAccessKey/);
expect(
() => new AWSBedrockEmbedder({ awsSecretAccessKey: "secret" }),
).toThrow(/awsAccessKeyId and awsSecretAccessKey/);
});
// Silently ignoring a lone session token would fall back to the ambient
// credential chain, embedding under an identity the caller never chose.
it("rejects a session token supplied without the key pair", () => {
expect(
() => new AWSBedrockEmbedder({ awsSessionToken: "token" }),
).toThrow(/awsAccessKeyId and awsSecretAccessKey/);
});
});
describe("provider registration", () => {
it("is constructed by EmbedderFactory for the aws_bedrock provider", () => {
const embedder = EmbedderFactory.create("aws_bedrock", {
model: "amazon.titan-embed-text-v2:0",
});
expect(embedder).toBeInstanceOf(AWSBedrockEmbedder);
});
});
describe("error handling", () => {
it("wraps Bedrock failures with the model id", async () => {
mockSend.mockRejectedValueOnce(new Error("AccessDeniedException"));
const embedder = new AWSBedrockEmbedder({});
await expect(embedder.embed("hello")).rejects.toThrow(
"Error getting embedding from AWS Bedrock model amazon.titan-embed-text-v1: AccessDeniedException",
);
});
it("fails when the response carries no embedding", async () => {
mockSend.mockResolvedValueOnce(encode({ inputTextTokenCount: 3 }));
const embedder = new AWSBedrockEmbedder({});
await expect(embedder.embed("hello")).rejects.toThrow(
/returned no embedding/,
);
});
// F7: `[]` is truthy, so `payload.embedding && [payload.embedding]` used to
// turn `{"embedding": []}` into `[[]]` -- length 1, which satisfied the
// length check for a single-input call and handed the caller an empty vector.
it("rejects a zero-length Titan embedding as no embedding", async () => {
mockSend.mockResolvedValueOnce(encode({ embedding: [] }));
const embedder = new AWSBedrockEmbedder({});
await expect(embedder.embed("hello")).rejects.toThrow(
/returned no embedding/,
);
});
it("returns an empty array for an empty batch without calling Bedrock", async () => {
const embedder = new AWSBedrockEmbedder({});
await expect(embedder.embedBatch([])).resolves.toEqual([]);
expect(mockSend).not.toHaveBeenCalled();
});
});
describe("dynamic SDK import", () => {
// These tests replace the module registered for
// @aws-sdk/client-bedrock-runtime for a single resolution. Restore the
// working mock afterward so every other test in this file keeps getting
// the mocked client instead of hitting module resolution for real.
afterEach(() => {
jest.resetModules();
jest.doMock("@aws-sdk/client-bedrock-runtime", () => {
const actual = jest.requireActual("@aws-sdk/client-bedrock-runtime");
return { ...actual, BedrockRuntimeClient: mockClientConstructor };
});
});
// F1: loadSdk()'s catch used to rewrite *every* import failure into the
// "package is required" hint, even when the package is installed but
// failed to load for an unrelated reason. That discarded the real error.
it("propagates a non-resolution import error unchanged", async () => {
jest.resetModules();
jest.doMock("@aws-sdk/client-bedrock-runtime", () => {
const err: any = new Error("boom: unrelated crash while loading");
err.code = "ERR_SOMETHING_ELSE";
throw err;
});
const embedder = new AWSBedrockEmbedder({});
await expect(embedder.embed("hello")).rejects.toThrow(
"boom: unrelated crash while loading",
);
});
// F1: a genuine resolution failure should still get the friendly install
// hint, with the original error preserved as `cause` for debugging.
it("gives an install hint for a genuine module-not-found error, preserving the cause", async () => {
jest.resetModules();
jest.doMock("@aws-sdk/client-bedrock-runtime", () => {
const err: any = new Error(
"Cannot find module '@aws-sdk/client-bedrock-runtime'",
);
err.code = "MODULE_NOT_FOUND";
throw err;
});
const embedder = new AWSBedrockEmbedder({});
await expect(embedder.embed("hello")).rejects.toThrow(
/npm install @aws-sdk\/client-bedrock-runtime/,
);
await expect(embedder.embed("hello")).rejects.toMatchObject({
cause: expect.objectContaining({ code: "MODULE_NOT_FOUND" }),
});
});
});
describe("client promise retry", () => {
// F2: getClient() used to memoize the client promise before it resolved,
// so a rejected construction (e.g. a transient credentials failure) was
// cached forever -- every later embed() call on that instance would
// reject immediately without ever retrying.
it("retries client construction after a failure instead of caching the rejection", async () => {
mockClientConstructor.mockImplementationOnce(() => {
throw new Error("credentials not ready");
});
mockSend.mockResolvedValueOnce(titanReply([1, 2, 3]));
const embedder = new AWSBedrockEmbedder({});
await expect(embedder.embed("hello")).rejects.toThrow(
"credentials not ready",
);
await expect(embedder.embed("hello")).resolves.toEqual([1, 2, 3]);
});
});
describe("memoryAction -> Cohere input_type", () => {
// F3: buildRequestBody() used to hardcode `input_type: "search_document"`
// regardless of the caller's action, so `Memory.search()` (which calls
// `embed(query, "search")`) embedded the query in document mode.
//
// Typed as `Embedder` (not `AWSBedrockEmbedder`) because that is how
// memory/index.ts actually calls it: the interface already declares an
// optional `memoryAction` second parameter, so a narrower concrete
// `embed(text: string)` satisfies it structurally and tsc stays silent --
// the bug is a silent behavioral one, not a compile error.
it("sends search_query for a search action", async () => {
mockSend.mockResolvedValueOnce(cohereReply([[0.1]]));
const embedder: Embedder = new AWSBedrockEmbedder({
model: "cohere.embed-english-v3",
});
await embedder.embed("query text", "search");
expect(requestBodyAt(0)).toEqual({
texts: ["query text"],
input_type: "search_query",
});
});
it("sends search_document for add and update actions", async () => {
mockSend
.mockResolvedValueOnce(cohereReply([[0.1]]))
.mockResolvedValueOnce(cohereReply([[0.2]]));
const embedder: Embedder = new AWSBedrockEmbedder({
model: "cohere.embed-english-v3",
});
await embedder.embed("doc one", "add");
await embedder.embed("doc two", "update");
expect(requestBodyAt(0).input_type).toBe("search_document");
expect(requestBodyAt(1).input_type).toBe("search_document");
});
// Titan has no input_type concept; buildRequestBody() must not add one
// even when a memoryAction is explicitly passed through.
it("Titan ignores memoryAction and never sends input_type", async () => {
mockSend.mockResolvedValueOnce(titanReply([1, 2]));
const embedder: Embedder = new AWSBedrockEmbedder({});
await embedder.embed("hello", "search");
expect(requestBodyAt(0)).toEqual({ inputText: "hello" });
});
});
describe("Titan embedBatch concurrency", () => {
afterEach(() => {
// This test sets a persistent mockImplementation (not a *Once), so
// clear it explicitly -- jest.clearAllMocks() in the top beforeEach
// clears call data but not implementations.
mockSend.mockReset();
});
// F4: embedBatch() used to Promise.all-fan-out one InvokeModel call per
// text with no cap, so a large batch could open hundreds of concurrent
// requests at once. TITAN_MAX_CONCURRENCY bounds this to a small pool
// while still preserving output order.
it("never runs more than TITAN_MAX_CONCURRENCY Titan requests at once, and preserves order", async () => {
let active = 0;
let peak = 0;
mockSend.mockImplementation(async (command: InvokeModelCommand) => {
active++;
peak = Math.max(peak, active);
const body = JSON.parse(
new TextDecoder().decode(command.input.body as Uint8Array),
);
await new Promise((resolve) => setTimeout(resolve, 10));
active--;
const index = Number(body.inputText.split("-")[1]);
return titanReply([index]);
});
const embedder = new AWSBedrockEmbedder({});
const texts = Array.from({ length: 10 }, (_, i) => `text-${i}`);
const embeddings = await embedder.embedBatch(texts);
expect(peak).toBeGreaterThan(1);
expect(peak).toBeLessThanOrEqual(4);
expect(embeddings).toEqual(texts.map((_, i) => [i]));
expect(mockSend).toHaveBeenCalledTimes(10);
});
});
});
+9 -9
View File
@@ -20,7 +20,13 @@ from mem0.client.types import (
from mem0.client.utils import api_error_handler
# Exception classes are referenced in docstrings only
from mem0.memory.setup import get_user_id, is_aliased, mark_aliased, read_anon_ids, setup_config
from mem0.memory.setup import (
get_user_id,
is_aliased,
mark_aliased,
read_anon_ids,
setup_config,
)
from mem0.memory.telemetry import capture_client_event, client_telemetry
logger = logging.getLogger(__name__)
@@ -725,7 +731,6 @@ class MemoryClient:
options: Optional[ProjectUpdateOptions] = None,
custom_instructions: Optional[str] = None,
custom_categories: Optional[List[str]] = None,
retrieval_criteria: Optional[List[Dict[str, Any]]] = None,
memory_depth: Optional[str] = None,
usecase_setting: Optional[str] = None,
multilingual: Optional[bool] = None,
@@ -736,7 +741,6 @@ class MemoryClient:
options: Typed options for the update operation (ProjectUpdateOptions).
custom_instructions: New instructions for the project.
custom_categories: New categories for the project.
retrieval_criteria: New retrieval criteria for the project.
memory_depth: Memory depth for the project.
usecase_setting: Usecase setting for the project.
multilingual: Whether to use the input language for memory storage and retrieval.
@@ -761,7 +765,6 @@ class MemoryClient:
for k, v in {
"custom_instructions": custom_instructions,
"custom_categories": custom_categories,
"retrieval_criteria": retrieval_criteria,
"memory_depth": memory_depth,
"usecase_setting": usecase_setting,
"multilingual": multilingual,
@@ -773,7 +776,7 @@ class MemoryClient:
if not kwargs:
raise ValueError(
"Currently we only support updating custom_instructions or "
"custom_categories or retrieval_criteria, so you must "
"custom_categories, so you must "
"provide at least one of them"
)
@@ -1630,7 +1633,6 @@ class AsyncMemoryClient:
options: Optional[ProjectUpdateOptions] = None,
custom_instructions: Optional[str] = None,
custom_categories: Optional[List[str]] = None,
retrieval_criteria: Optional[List[Dict[str, Any]]] = None,
memory_depth: Optional[str] = None,
usecase_setting: Optional[str] = None,
multilingual: Optional[bool] = None,
@@ -1641,7 +1643,6 @@ class AsyncMemoryClient:
options: Typed options for the update operation (ProjectUpdateOptions).
custom_instructions: New instructions for the project.
custom_categories: New categories for the project.
retrieval_criteria: New retrieval criteria for the project.
memory_depth: Memory depth for the project.
usecase_setting: Usecase setting for the project.
multilingual: Whether to use the input language for memory storage and retrieval.
@@ -1666,7 +1667,6 @@ class AsyncMemoryClient:
for k, v in {
"custom_instructions": custom_instructions,
"custom_categories": custom_categories,
"retrieval_criteria": retrieval_criteria,
"memory_depth": memory_depth,
"usecase_setting": usecase_setting,
"multilingual": multilingual,
@@ -1678,7 +1678,7 @@ class AsyncMemoryClient:
if not kwargs:
raise ValueError(
"Currently we only support updating custom_instructions or "
"custom_categories or retrieval_criteria, so you must "
"custom_categories, so you must "
"provide at least one of them"
)
+4 -28
View File
@@ -177,7 +177,6 @@ class BaseProject(ABC):
self,
custom_instructions: Optional[str] = None,
custom_categories: Optional[List[str]] = None,
retrieval_criteria: Optional[List[Dict[str, Any]]] = None,
) -> Dict[str, Any]:
"""
Update project settings.
@@ -185,7 +184,6 @@ class BaseProject(ABC):
Args:
custom_instructions: New instructions for the project
custom_categories: New categories for the project
retrieval_criteria: New retrieval criteria for the project
Returns:
Dictionary containing the API response.
@@ -396,7 +394,6 @@ class Project(BaseProject):
self,
custom_instructions: Optional[str] = None,
custom_categories: Optional[List[str]] = None,
retrieval_criteria: Optional[List[Dict[str, Any]]] = None,
multilingual: Optional[bool] = None,
decay: Optional[bool] = None,
) -> Dict[str, Any]:
@@ -406,7 +403,6 @@ class Project(BaseProject):
Args:
custom_instructions: New instructions for the project
custom_categories: New categories for the project
retrieval_criteria: New retrieval criteria for the project
multilingual: Whether to use the input language for memory storage and retrieval
decay: Toggle Memory Decay for this project. When True, search-time
ranking boosts recently-used memories and gently dampens stale ones; when
@@ -422,24 +418,16 @@ class Project(BaseProject):
NetworkError: If network connectivity issues occur.
ValueError: If org_id or project_id are not set.
"""
if (
custom_instructions is None
and custom_categories is None
and retrieval_criteria is None
and multilingual is None
and decay is None
):
if custom_instructions is None and custom_categories is None and multilingual is None and decay is None:
raise ValueError(
"At least one parameter must be provided for update: "
"custom_instructions, custom_categories, retrieval_criteria, "
"multilingual, decay"
"custom_instructions, custom_categories, multilingual, decay"
)
payload = self._prepare_params(
{
"custom_instructions": custom_instructions,
"custom_categories": custom_categories,
"retrieval_criteria": retrieval_criteria,
"multilingual": multilingual,
"decay": decay,
}
@@ -455,7 +443,6 @@ class Project(BaseProject):
{
"custom_instructions": custom_instructions,
"custom_categories": custom_categories,
"retrieval_criteria": retrieval_criteria,
"multilingual": multilingual,
"decay": decay,
"sync_type": "sync",
@@ -720,7 +707,6 @@ class AsyncProject(BaseProject):
self,
custom_instructions: Optional[str] = None,
custom_categories: Optional[List[str]] = None,
retrieval_criteria: Optional[List[Dict[str, Any]]] = None,
multilingual: Optional[bool] = None,
decay: Optional[bool] = None,
) -> Dict[str, Any]:
@@ -730,7 +716,6 @@ class AsyncProject(BaseProject):
Args:
custom_instructions: New instructions for the project
custom_categories: New categories for the project
retrieval_criteria: New retrieval criteria for the project
multilingual: Whether to use the input language for memory storage and retrieval
decay: Toggle Memory Decay for this project. When True, search-time
ranking boosts recently-used memories and gently dampens stale ones; when
@@ -746,24 +731,16 @@ class AsyncProject(BaseProject):
NetworkError: If network connectivity issues occur.
ValueError: If org_id or project_id are not set.
"""
if (
custom_instructions is None
and custom_categories is None
and retrieval_criteria is None
and multilingual is None
and decay is None
):
if custom_instructions is None and custom_categories is None and multilingual is None and decay is None:
raise ValueError(
"At least one parameter must be provided for update: "
"custom_instructions, custom_categories, retrieval_criteria, "
"multilingual, decay"
"custom_instructions, custom_categories, multilingual, decay"
)
payload = self._prepare_params(
{
"custom_instructions": custom_instructions,
"custom_categories": custom_categories,
"retrieval_criteria": retrieval_criteria,
"multilingual": multilingual,
"decay": decay,
}
@@ -779,7 +756,6 @@ class AsyncProject(BaseProject):
{
"custom_instructions": custom_instructions,
"custom_categories": custom_categories,
"retrieval_criteria": retrieval_criteria,
"multilingual": multilingual,
"decay": decay,
"sync_type": "async",
-1
View File
@@ -107,4 +107,3 @@ class ProjectUpdateOptions(BaseModel):
memory_depth: Optional[str] = Field(default=None, description="Memory depth configuration")
usecase_setting: Optional[Any] = Field(default=None, description="Use case specific settings")
multilingual: Optional[bool] = Field(default=None, description="Whether to enable multilingual support")
retrieval_criteria: Optional[List[Any]] = Field(default=None, description="Criteria for memory retrieval")
+1 -1
View File
@@ -15,7 +15,7 @@ class OpenAIStructuredLLM(LLMBase):
self.config.model = "gpt-5-mini"
api_key = self.config.api_key or os.getenv("OPENAI_API_KEY")
base_url = self.config.openai_base_url or os.getenv("OPENAI_API_BASE") or "https://api.openai.com/v1"
base_url = self.config.openai_base_url or os.getenv("OPENAI_BASE_URL") or "https://api.openai.com/v1"
self.client = OpenAI(api_key=api_key, base_url=base_url)
def generate_response(
-2
View File
@@ -424,7 +424,6 @@ class _OSSProject:
self,
custom_instructions: Optional[str] = None,
custom_categories: Optional[list] = None,
retrieval_criteria: Optional[list] = None,
multilingual: Optional[bool] = None,
decay: Optional[bool] = None,
):
@@ -438,7 +437,6 @@ class _AsyncOSSProject:
self,
custom_instructions: Optional[str] = None,
custom_categories: Optional[list] = None,
retrieval_criteria: Optional[list] = None,
multilingual: Optional[bool] = None,
decay: Optional[bool] = None,
):
+14 -9
View File
@@ -3,7 +3,7 @@ import secrets
import uuid
from datetime import datetime, timedelta, timezone
from db import get_db
from db import SessionLocal
from fastapi import Depends, HTTPException, Request
from fastapi.security import APIKeyHeader, HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
@@ -145,19 +145,24 @@ async def verify_auth(
request: Request,
credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
x_api_key: str | None = Depends(api_key_header),
db: Session = Depends(get_db),
) -> User | None:
"""Authenticate via JWT, X-API-Key, or legacy ADMIN_API_KEY. Returns User or None."""
"""Authenticate via JWT, X-API-Key, or legacy ADMIN_API_KEY. Returns User or None.
A short-lived session is opened only on the branches that query the DB, so no
pooled connection is held for the lifetime of the (possibly long-running) request.
"""
if credentials is not None:
_mark_auth_type(request, "bearer")
return _resolve_user_from_jwt(credentials.credentials, db)
with SessionLocal() as db:
return _resolve_user_from_jwt(credentials.credentials, db)
if x_api_key is not None:
if ADMIN_API_KEY and secrets.compare_digest(x_api_key, ADMIN_API_KEY):
_mark_auth_type(request, "admin_api_key")
return None
_mark_auth_type(request, "api_key")
return _resolve_user_from_api_key(x_api_key, db)
with SessionLocal() as db:
return _resolve_user_from_api_key(x_api_key, db)
if AUTH_DISABLED:
_mark_auth_type(request, "disabled")
@@ -173,12 +178,12 @@ async def verify_auth(
async def require_auth(
request: Request,
user: User | None = Depends(verify_auth),
db: Session = Depends(get_db),
) -> User:
"""Like verify_auth but guarantees a non-None User. Use for endpoints that require auth."""
if user is None:
if getattr(request.state, "auth_type", "none") in {"admin_api_key", "disabled"}:
default_user = _get_default_user(db)
with SessionLocal() as db:
default_user = _get_default_user(db)
if default_user is not None:
return default_user
raise HTTPException(status_code=401, detail="Authentication required.")
@@ -193,7 +198,6 @@ _BOOTSTRAP_ADMIN = User(
async def require_admin(
request: Request,
user: User | None = Depends(verify_auth),
db: Session = Depends(get_db),
) -> User:
"""Like require_auth but also enforces admin role.
@@ -203,7 +207,8 @@ async def require_admin(
auth_type = getattr(request.state, "auth_type", "none")
if user is None:
if auth_type in {"admin_api_key", "disabled"}:
default_user = _get_default_user(db)
with SessionLocal() as db:
default_user = _get_default_user(db)
if default_user is not None:
if default_user.role != "admin":
raise HTTPException(status_code=403, detail="Admin role required.")
+17 -9
View File
@@ -175,18 +175,23 @@ def update_me(
user: User = Depends(require_auth),
db: Session = Depends(get_db),
):
if body.name is not None and body.name.strip():
user.name = body.name.strip()
# require_auth resolves the user in its own short-lived session, so `user` is
# detached from this request's `db`. Load a session-managed copy to mutate.
db_user = db.get(User, user.id)
if db_user is None:
raise HTTPException(status_code=404, detail="User not found.")
if body.email is not None and body.email != user.email:
collision = db.scalar(select(User).where(User.email == body.email, User.id != user.id))
if body.name is not None and body.name.strip():
db_user.name = body.name.strip()
if body.email is not None and body.email != db_user.email:
collision = db.scalar(select(User).where(User.email == body.email, User.id != db_user.id))
if collision is not None:
raise HTTPException(status_code=409, detail="Email is already in use.")
user.email = body.email
db_user.email = body.email
db.commit()
db.refresh(user)
return user
return db_user
@router.post("/change-password", response_model=MessageResponse)
@@ -195,12 +200,15 @@ def change_password(
user: User = Depends(require_auth),
db: Session = Depends(get_db),
):
if not verify_password(body.current_password, user.password_hash):
# require_auth resolves the user in its own short-lived session, so `user` is
# detached from this request's `db`. Load a session-managed copy to mutate.
db_user = db.get(User, user.id)
if db_user is None or not verify_password(body.current_password, db_user.password_hash):
raise HTTPException(status_code=401, detail="Current password is incorrect.")
_require_password_length(body.new_password)
user.password_hash = hash_password(body.new_password)
db_user.password_hash = hash_password(body.new_password)
db.commit()
return MessageResponse(message="Password updated.")
-39
View File
@@ -8,7 +8,6 @@ Additional platform capabilities beyond core CRUD operations.
- [Entity Linking](#entity-linking)
- [Custom Categories](#custom-categories)
- [Custom Instructions](#custom-instructions)
- [Criteria Retrieval](#criteria-retrieval)
- [Feedback Mechanism](#feedback-mechanism)
- [Memory Export](#memory-export)
- [Group Chat](#group-chat)
@@ -165,44 +164,6 @@ await client.updateProject({ customInstructions: "Your guidelines here..." });
---
## Criteria Retrieval
Custom attribute-based memory ranking using LLM-evaluated criteria with weights. Goes beyond semantic similarity to prioritize memories based on domain-specific signals.
### Configuration
```python
# Define criteria at project level
retrieval_criteria = [
{"name": "joy", "description": "Positive emotions like happiness and excitement", "weight": 3},
{"name": "curiosity", "description": "Inquisitiveness and desire to learn", "weight": 2},
{"name": "urgency", "description": "Time-sensitive or high-priority items", "weight": 4},
]
client.project.update(retrieval_criteria=retrieval_criteria)
```
```typescript
await client.updateProject({
retrievalCriteria: [
{ name: 'joy', description: 'Positive emotions', weight: 3 },
{ name: 'urgency', description: 'Time-sensitive items', weight: 4 },
],
});
```
### Usage
Once configured, `client.search()` automatically applies criteria ranking:
```python
# Criteria-weighted results returned automatically
results = client.search("Why am I feeling happy?", filters={"user_id": "alice"})
```
**Best for:** Wellness assistants, tutoring platforms, productivity tools — any app needing intent-aware retrieval.
---
## Feedback Mechanism
Provide feedback on extracted memories to improve system quality over time.
+11
View File
@@ -49,3 +49,14 @@ def test_regular_model_sends_sampling_params(mock_openai_client):
assert "max_tokens" in call_kwargs # standard sampling params still forwarded
assert "top_p" in call_kwargs
assert call_kwargs["model"] == "gpt-4o"
def test_uses_openai_base_url_environment_variable(monkeypatch):
base_url = "https://gateway.example/v1"
monkeypatch.setenv("OPENAI_API_BASE", "https://legacy.example/v1")
monkeypatch.setenv("OPENAI_BASE_URL", base_url)
with patch("mem0.llms.openai_structured.OpenAI") as mock_openai:
OpenAIStructuredLLM(OpenAIConfig(api_key="test-api-key"))
mock_openai.assert_called_once_with(api_key="test-api-key", base_url=base_url)
+3 -3
View File
@@ -2,9 +2,9 @@
parameter-passthrough surface.
Verifies the kwarg → JSON payload mapping for every supported field
(``custom_instructions``, ``custom_categories``, ``retrieval_criteria``,
``multilingual``, ``decay``), the ValueError when no field is
provided, and the URL/method shape. The HTTP layer is mocked.
(``custom_instructions``, ``custom_categories``, ``multilingual``,
``decay``), the ValueError when no field is provided, and the
URL/method shape. The HTTP layer is mocked.
"""
from unittest.mock import MagicMock, patch