Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84a1318f71 | ||
|
|
29601e54f4 | ||
|
|
a8e2a49209 | ||
|
|
5b379d9840 | ||
|
|
2a61fe1a32 | ||
|
|
1ac3aa7256 | ||
|
|
aa939d00d4 | ||
|
|
e168d48e04 | ||
|
|
84ace27f27 | ||
|
|
a7711c9c38 | ||
|
|
7495ab67ad | ||
|
|
a618939f00 | ||
|
|
ea2ee07586 | ||
|
|
540d23d610 | ||
|
|
3274390f82 | ||
|
|
b357a5a1b0 | ||
|
|
d653b63fac | ||
|
|
cc4671579f | ||
|
|
01afdde3e7 | ||
|
|
d6d89c987b | ||
|
|
c2150e8f1a | ||
|
|
19c7bb84a2 | ||
|
|
e6281ab724 | ||
|
|
a71d7bdbe3 | ||
|
|
56ec7d20f1 | ||
|
|
7417f87252 | ||
|
|
41a9701d7f | ||
|
|
ca2abca2b8 | ||
|
|
0e582adc6c | ||
|
|
9caffeaa7b | ||
|
|
a58e0586ad | ||
|
|
a9cb4bb644 | ||
|
|
7bf84b8d38 | ||
|
|
5e7adc4d12 | ||
|
|
14393b5962 | ||
|
|
f590c9596a | ||
|
|
dd5f7e39a8 | ||
|
|
70ab76a053 | ||
|
|
2af3a72f73 | ||
|
|
8005e18aec | ||
|
|
39551145b8 | ||
|
|
c2bc28e589 | ||
|
|
fec2fe6a2c | ||
|
|
d0c23a5950 | ||
|
|
b05cce581b | ||
|
|
8a57967c8a | ||
|
|
756b0b1b6d | ||
|
|
726bcc80b2 |
@@ -9,12 +9,10 @@ body:
|
||||
label: Component
|
||||
description: Which part of mem0 is affected?
|
||||
options:
|
||||
- Core / Python SDK
|
||||
- Python SDK
|
||||
- TypeScript SDK
|
||||
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
|
||||
- Graph Memory (Neo4j, Memgraph, etc.)
|
||||
- Ollama / Local Models
|
||||
- OpenClaw
|
||||
- Vector Store
|
||||
- Plugin
|
||||
- REST API
|
||||
- Other
|
||||
validations:
|
||||
|
||||
@@ -9,14 +9,11 @@ body:
|
||||
label: Component
|
||||
description: Which part of mem0 does this relate to?
|
||||
options:
|
||||
- Core / Python SDK
|
||||
- Python SDK
|
||||
- TypeScript SDK
|
||||
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
|
||||
- Graph Memory (Neo4j, Memgraph, etc.)
|
||||
- Ollama / Local Models
|
||||
- OpenClaw
|
||||
- Vector Store
|
||||
- Plugin
|
||||
- REST API
|
||||
- Benchmarks / Evals
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
# Maps dropdown selections to GitHub labels
|
||||
# Used by the advanced-issue-labeler GitHub Action
|
||||
|
||||
component:
|
||||
- label: "sdk-python"
|
||||
matcher: "Core / Python SDK"
|
||||
- label: "sdk-typescript"
|
||||
matcher: "TypeScript SDK"
|
||||
- label: "vector-store"
|
||||
matcher: "Vector Store"
|
||||
- label: "graph-memory"
|
||||
matcher: "Graph Memory"
|
||||
- label: "ollama"
|
||||
matcher: "Ollama"
|
||||
- label: "openclaw"
|
||||
matcher: "OpenClaw"
|
||||
- label: "rest-api"
|
||||
matcher: "REST API"
|
||||
policy:
|
||||
- section:
|
||||
- id: ['component']
|
||||
block-list: ['Other']
|
||||
label:
|
||||
- name: 'sdk-python'
|
||||
keys: ['Python SDK']
|
||||
- name: 'sdk-typescript'
|
||||
keys: ['TypeScript SDK']
|
||||
- name: 'vector-store'
|
||||
keys: ['Vector Store']
|
||||
- name: 'plugin'
|
||||
keys: ['Plugin']
|
||||
- name: 'rest-api'
|
||||
keys: ['REST API']
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"language": {
|
||||
"sdk-python": [
|
||||
"python", "pip install", "pypi", "pyproject", "requirements.txt",
|
||||
"from mem0", "import mem0", "traceback", "pydantic", "asyncmemory",
|
||||
"poetry", "virtualenv", "venv", "conda", "pytest", "async def"
|
||||
],
|
||||
"sdk-typescript": [
|
||||
"typescript", "javascript", "pnpm", "yarn", "node.js", "nodejs",
|
||||
"mem0-ts", "mem0ai/oss", "tsconfig", "await import",
|
||||
"=> {", "undefined is not"
|
||||
]
|
||||
},
|
||||
"area": {
|
||||
"plugin": [
|
||||
"openclaw", "openclaw-mem0", "openclaw.json", "openclaw plugin",
|
||||
"claude code", "opencode", "pi agent", "mem0-plugin",
|
||||
"cursor plugin", "codex plugin", "editor plugin"
|
||||
],
|
||||
"cli": ["mem0-cli", "@mem0/cli", "npx mem0", "command line"],
|
||||
"vector-store": [
|
||||
"pgvector", "pinecone", "chroma", "chromadb", "weaviate",
|
||||
"milvus", "faiss", "vector store", "vectorstore",
|
||||
"elasticsearch", "supabase", "azure ai search",
|
||||
"s3 vectors", "mongodb"
|
||||
],
|
||||
"integrations": [
|
||||
"vercel ai", "vercel-ai-sdk", "@mem0/vercel-ai-provider",
|
||||
"llamaindex", "crewai", "autogen", "langgraph"
|
||||
],
|
||||
"rest-api": [
|
||||
"rest api", "fastapi", "docker-compose", "/v1/memories",
|
||||
"localhost:8000", "localhost:8888", "curl -x", "http endpoint"
|
||||
],
|
||||
"documentation": [
|
||||
"docs.mem0.ai", "documentation", "typo", "readme", "docstring", "broken link",
|
||||
"issue on docs", "docs:", "link to the docs page", "issue with current documentation"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
sdk-python:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'mem0/**'
|
||||
- 'tests/**'
|
||||
- 'cli/python/**'
|
||||
- 'pyproject.toml'
|
||||
- 'poetry.lock'
|
||||
|
||||
sdk-typescript:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'mem0-ts/**'
|
||||
- 'cli/node/**'
|
||||
|
||||
vector-store:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'mem0/vector_stores/**'
|
||||
- 'mem0-ts/src/oss/src/vector_stores/**'
|
||||
|
||||
rest-api:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'server/**'
|
||||
|
||||
integrations:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'integrations/**'
|
||||
|
||||
plugin:
|
||||
- changed-files:
|
||||
- all-globs-to-any-file:
|
||||
- 'integrations/**'
|
||||
- '!integrations/vercel-ai-sdk/**'
|
||||
- any-glob-to-any-file:
|
||||
- 'skills/**'
|
||||
- '.agents/**'
|
||||
- '.claude-plugin/**'
|
||||
- '.codex-plugin/**'
|
||||
- '.cursor-plugin/**'
|
||||
- 'marketplace.json'
|
||||
|
||||
cli:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'cli/**'
|
||||
|
||||
documentation:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- 'docs/**'
|
||||
- 'examples/**'
|
||||
- '*.md'
|
||||
|
||||
ci:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- '.github/**'
|
||||
- 'scripts/**'
|
||||
- '.pre-commit-config.yaml'
|
||||
@@ -0,0 +1,44 @@
|
||||
const fs = require('fs');
|
||||
|
||||
function componentLabels(keywords) {
|
||||
return Object.values(keywords).flatMap(Object.keys);
|
||||
}
|
||||
|
||||
function toMatcher(term) {
|
||||
const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const prefix = /^[a-z0-9]/i.test(term) ? '\\b' : '';
|
||||
return new RegExp(prefix + escaped, 'i');
|
||||
}
|
||||
|
||||
function scoreGroup(text, group) {
|
||||
let winner = null;
|
||||
let best = 0;
|
||||
for (const [label, terms] of Object.entries(group)) {
|
||||
const score = terms.reduce((n, term) => n + (toMatcher(term).test(text) ? 1 : 0), 0);
|
||||
if (score > best) {
|
||||
winner = label;
|
||||
best = score;
|
||||
}
|
||||
}
|
||||
return winner;
|
||||
}
|
||||
|
||||
const UMBRELLA = { plugin: 'integrations' };
|
||||
|
||||
function inferComponentLabels(text, keywords) {
|
||||
if (!text) return [];
|
||||
const labels = [scoreGroup(text, keywords.language), scoreGroup(text, keywords.area)].filter(
|
||||
Boolean,
|
||||
);
|
||||
for (const label of labels.slice()) {
|
||||
const parent = UMBRELLA[label];
|
||||
if (parent && !labels.includes(parent)) labels.push(parent);
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
function loadKeywords(file) {
|
||||
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
}
|
||||
|
||||
module.exports = { componentLabels, inferComponentLabels, loadKeywords };
|
||||
@@ -0,0 +1,102 @@
|
||||
const assert = require('assert');
|
||||
const path = require('path');
|
||||
const { inferComponentLabels, loadKeywords } = require('./infer-component-labels.js');
|
||||
|
||||
const keywords = loadKeywords(path.join(__dirname, '..', 'component-keywords.json'));
|
||||
|
||||
const cases = [
|
||||
{
|
||||
number: 6210,
|
||||
title: "but(anthropic): sampling parameters returns 400 error for new model",
|
||||
body: "### Component\n\nCore / Python SDK\n\n### Description\n\n### Summary\n\nWhen using Anthropic latest models such as `claude-opus-4-7`, `claude-opus-4-8`, or `claude-sonnet-5`, Mem0 still sends sampling parameters like `temperature` / `top_p`. These models do not support those parameters, causing Anthropic API requests to fail.\n\nSee https://platform.claude.com/docs/en/about-claude/models/migration-guide\n\n### Steps to Reproduce\n\n```python\n from mem0 import Memory\n\n m = Memory.from_config({\n \"llm\": {\n \"provider\": \"anthropic\",\n \"config\": {\n \"model\": \"claude-opus-4-8\",\n \"api_key\": \"your-anthropic-api-key\"\n },\n },\n ...\n })\n```\n\n### Expected Behavior\n\nMem0 should detect Anthropic models that do not support sampling parameters and omit temperature and top_p from the request.\n\nFor models that still support sampling parameters, such as claude-opus-4-6, claude-sonnet-4-6, and claude-haiku-4-5, Mem0 should continue sending supported sampling parameters till they're deprecated.\n\n### Actual Behavior\n\nMem0 includes temperature by default for Anthropic requests. With newer Anthropic models that do not support sampling parameters, the API request fails because unsupported parameters are sent.\n\n### Environment\n\n - mem0 version: 2.0.11\n - Python/Node version: Python 3.11\n - OS: macOS\n",
|
||||
expected: ["sdk-python"],
|
||||
},
|
||||
{
|
||||
number: 5770,
|
||||
title: "feat(ts-sdk): add FastEmbed embedding provider",
|
||||
body: "## Summary\n\nThe Python SDK supports **FastEmbed** as an embedding provider, but the TypeScript OSS SDK (`mem0ai/oss`) does not. Add it to bring the TS SDK to parity.\n\n| | |\n|---|---|\n| Python reference | `mem0/embeddings/fastembed.py` |\n| Registered in (Python) | `mem0/utils/factory.py` (EmbedderFactory) |\n| Target file (TypeScript) | `mem0-ts/src/oss/src/embeddings/fastembed.ts` |\n| Suggested implementation | Use the `fastembed` npm package (ONNX local embeddings). |\n\n## Requirements\n\n- [ ] Implement `FastEmbedEmbedder` in `mem0-ts/src/oss/src/embeddings/fastembed.ts`, extending `Embedder` (`mem0-ts/src/oss/src/embeddings/base.ts`) and mirroring the Python provider's behavior (embed / embedBatch).\n- [ ] Register the `\"fastembed\"` provider in `mem0-ts/src/oss/src/utils/factory.ts` (EmbedderFactory).\n- [ ] Add config typing in `mem0-ts/src/oss/src/types/`.\n- [ ] Add a unit test under `mem0-ts/src/oss/src/tests/`.\n- [ ] Add `fastembed` to `mem0-ts/package.json` (optional/peer dependency, lazy-imported like other providers).\n- [ ] Update docs under `docs/` if this provider is user-facing.\n\n## Reference pattern\n\nMirror an existing TS provider: `embeddings/openai.ts`.\n\n## Notes\n\n`fastembed` (v2.x) is the JS port of Qdrant's FastEmbed — local/offline embeddings. Mirror the default model in `mem0/embeddings/fastembed.py`.\n\n---\n_Part of the TypeScript ↔ Python SDK provider-parity effort. One provider per issue (atomic)._\n",
|
||||
expected: ["sdk-typescript"],
|
||||
},
|
||||
{
|
||||
number: 3940,
|
||||
title: "Milvus database will return distance not similarity score",
|
||||
body: "### 🐛 Describe the bug\n\nMilvus database will return distance not similarity score\n\n## in milvus.py\n\ndef _parse_output(self, data: list):\n \"\"\"\n Parse the output data.\n\n Args:\n data (Dict): Output data.\n\n Returns:\n List[OutputData]: Parsed output data.\n \"\"\"\n memory = []\n\n for value in data:\n uid, score, metadata = (\n value.get(\"id\"),\n value.get(\"distance\"), # here\n value.get(\"entity\", {}).get(\"metadata\"),\n )\n\n memory_obj = OutputData(id=uid, score=score, payload=metadata)\n memory.append(memory_obj)\n\n return memory\n",
|
||||
expected: ["vector-store"],
|
||||
},
|
||||
{
|
||||
number: 5290,
|
||||
title: "Recall search failed: Bad Request Using OpenAI Embedding Model",
|
||||
body: "### Component\n\nOpenClaw\n\n### Description\n\n### Summary\nuse openclaw.json config:\n\n```json\n...\n\"embedder\": {\n \"provider\": \"openai\",\n \"config\": {\n \"model\": \"bge-base-zh-v1.5\",\n \"embedding_dims\": 1024,\n \"embeddingDims\": 1024,\n \"url\": \"https://xxxxxxxxx/v1\",\n \"apiKey\": \"xxxxxxxxxxxx\"\n }\n },\n\"vectorStore\": {\n \"provider\": \"qdrant\",\n \"config\": {\n \"url\": \"http://qdrant:6333\",\n \"apiKey\": \"${QDRANT_API_KEY}\",\n \"collectionName\": \"mem0\",\n \"embeddingModelDims\": 1024\n }\n }\n```\n```\n\nopenclaw log info is:\n\n```\n23:14:20 Api key is used with unsecure connection.\n23:14:21 [mem0] Recall search failed: Bad Request\n23:14:21 [plugins] openclaw-mem0: skills-mode recall (strategy=smart) injecting 0 memories (~20 tokens)\n23:14:22 [ws] ⇄ res ✓ sessions.list 256ms conn=d1eb9bc4…17da id=201b8113…c9dc\n23:14:22 [ws] ⇄ res ✓ sessions.list 264ms conn=d1eb9bc4…17da id=4939f962…2f16\n23:14:34 [ws] ⇄ res ✓ sessions.list 250ms conn=d1eb9bc4…17da id=f7ad503f…baa6\n23:15:12 [mem0] **Recall search failed: Bad Request**\n23:15:12 [plugins] openclaw-mem0: skills-mode recall (strategy=smart) injecting 0 memories (~20 tokens)\n23:15:12 [ws] ⇄ res ✓ sessions.list 288ms conn=d1eb9bc4…17da id=9e20bb86…371e\n23:15:13 [ws] ⇄ res ✓ sessions.list 268ms conn=d1eb9bc4…17da id=3b49a2ad…7ada\n23:15:20 [ws] ⇄ res ✓ sessions.list 235ms conn=d1eb9bc4…17da id=a192da30…069f\n```\n\n### Actual Behavior\n\nembedding model response ok,response message has 1024 vectors,but the vectors are submitted to vector-db:qdrant with all zero vectors,and vectors has only 256 size.\n\n```http\nPOST /collections/mem0/points/search HTTP/1.1\nhost: qdrant:6333\nconnection: keep-alive\nuser-agent: qdrant-js/1.13.0\napi-key: xxxxxxxxxxxxxxxxxxxxxxxxxxxx\nContent-Type: application/json\nAccept: application/json\naccept-language: *\nsec-fetch-mode: cors\naccept-encoding: gzip, deflate\ncontent-length: 651\n\n{\"vector\":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],\"limit\":120,\"offset\":0,\"filter\":{\"must\":[{\"key\":\"user_id\",\"match\":{\"value\":\"agent\"}}]},\"with_payload\":true,\"with_vector\":false}\n\n**HTTP/1.1 400 Bad Request**\ntransfer-encoding: chunked\ncontent-type: application/json\nvary: accept-encoding, Origin, Access-Control-Request-Method, Access-Control-Request-Headers\ncontent-encoding: gzip\n\n```\n\n### Expected Behavior\n\nembedding model response ok by tcpdump, response message has 1024 vectors,and this vectors are submitted to vector-db:qdrant with the same vectors,and vectors has also 1024 size.\n\n\n### Environment\n\n- openclaw-mem0 version: 1.0.11\n- qdrant: 1.13.6\n",
|
||||
expected: ["plugin", "integrations"],
|
||||
},
|
||||
{
|
||||
number: 3696,
|
||||
title: "Cannot set expiration_date for memory in REST API server (Docker Compose)",
|
||||
body: "### 🐛 Describe the bug\n\nI'm using docker compose to deploy a REST API server. When adding memory, I'm unable to set the expiration_date. Is this feature not supported?",
|
||||
expected: ["rest-api"],
|
||||
},
|
||||
{
|
||||
number: 6252,
|
||||
title: "cursor: on_file_read_cursor.sh ignores auto_search / MEM0_AUTO_SEARCH",
|
||||
body: "### Component\n\nCursor / mem0-plugin\n\n### Description\n\n`on_file_read_cursor.sh` never checks `MEM0_AUTO_SEARCH`. In Claude Code, #6065/#6071 added a guard on `on_file_read.sh`, but the Cursor PreToolUse variant still always calls `file_context.py` (and thus Platform search) once `MEM0_API_KEY` is set.\n\n### Expected\n\nWhen `auto_search: false` / `MEM0_AUTO_SEARCH=false`, `on_file_read_cursor.sh` should exit 0 without searching.\n\n### Actual\n\nTimeline search still runs.\n\n### Related\n\n#6065, #6071, #6250\n",
|
||||
expected: ["plugin", "integrations"],
|
||||
},
|
||||
{
|
||||
number: 6032,
|
||||
title: "docs: fix typos and punctuation errors across docs",
|
||||
body: "### Description\n\n### Page\nMultiple pages — see list below.\n\n### What's Wrong or Missing\n1. https://docs.mem0.ai/components/llms/overview — \"a llm\" should be \"an LLM\"\n2. https://docs.mem0.ai/components/vectordbs/dbs/azure — 2 comma splices + \"setup\" used as a verb (should be \"set up\")\n3. https://docs.mem0.ai/components/embedders/models/azure_openai — \"from the Azure.\" is an incomplete sentence\n4. https://docs.mem0.ai/components/llms/models/azure_openai — same incomplete \"from the Azure\" phrasing\n5. https://docs.mem0.ai/cookbooks/companions/voice-companion-openai — \"an important information\" (uncountable noun)\n6. https://docs.mem0.ai/cookbooks/essentials/exporting-memories — comma splice\n7. https://docs.mem0.ai/cookbooks/integrations/tavily-search — \"usecase\" should be \"use case\"\n8. https://docs.mem0.ai/cookbooks/overview — broken parallelism in bullet list\n9. README.md — \"Github App\" should be \"GitHub App\"\n10. https://docs.mem0.ai/platform/overview — table cell not capitalized like other rows\n\n### Suggested Fix\nApply the corrections listed above for each page. I will submit a PR soon addressing all of the issues mentioned.",
|
||||
expected: ["documentation"],
|
||||
},
|
||||
];
|
||||
|
||||
const cliRegressionCase = {
|
||||
number: 3144,
|
||||
title: "Bug Report: Memory Score Does Not Match Expected Relevance in Local Search",
|
||||
body: "### 🐛 Describe the bug\n\n#### Description\n\nWhen using the locally deployed `mem0` server, the returned memory `score` from the `search` interface does not align with the expected semantic relevance. In particular, irrelevant or less relevant memories sometimes receive higher scores than directly related ones.\n\n#### Reproduction Steps\n\n```python\nmem0 = mem0_client(mode=\"local\")\nprint(\"Mem0 client initialized successfully.\")\n\nprint(\"Adding memories...\")\nresult = mem0.add(messages=[\n {\"role\": \"user\", \"content\": \"I like drinking coffee in the morning\"},\n {\"role\": \"user\", \"content\": \"I enjoy reading books at night\"}\n], user_id=\"alice\")\nprint(\"Memory added:\", result)\n\nprint(\"Searching memories...\")\nsearch_result = mem0.search(query=\"coffee\", user_id=\"alice\", top_k=2)\nprint(\"Search results:\", search_result)\n```\n\n#### Actual Output\n\n```json\n{\n \"results\": [\n {\n \"id\": \"5099b5be-c673-4f09-99de-a196f43b6476\",\n \"memory\": \"Likes drinking coffee in the morning\",\n \"score\": 0.5115111920687857\n },\n {\n \"id\": \"08df5c51-c52b-4c45-a5b6-b3f864ea149a\",\n \"memory\": \"Enjoys reading books at night\",\n \"score\": 0.7755568273863331\n }\n ],\n \"relations\": [\n {\"source\": \"coffee\", \"relationship\": \"consumed_in\", \"destination\": \"morning\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"likes\", \"destination\": \"coffee\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"likes_drinking\", \"destination\": \"coffee\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"in_time\", \"destination\": \"morning\"},\n {\"source\": \"user_id:_alice\", \"relationship\": \"drinks_in\", \"destination\": \"morning\"}\n ]\n}\n```\n\n#### Expected Behavior\n\nThe memory `\"Likes drinking coffee in the morning\"` should have a **higher score** than `\"Enjoys reading books at night\"` when querying for `\"coffee\"`, since it is directly semantically related.",
|
||||
};
|
||||
|
||||
let failures = 0;
|
||||
|
||||
function run(name, fn) {
|
||||
try {
|
||||
fn();
|
||||
console.log(`PASS ${name}`);
|
||||
} catch (err) {
|
||||
failures++;
|
||||
console.error(`FAIL ${name}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const { number, title, body, expected } of cases) {
|
||||
const text = `${title}
|
||||
|
||||
${body}`;
|
||||
run(`#${number}`, () => {
|
||||
assert.deepStrictEqual(inferComponentLabels(text, keywords), expected);
|
||||
});
|
||||
}
|
||||
|
||||
run('#3144 cliKeywordPrefixSubstringRegression', () => {
|
||||
const text = `${cliRegressionCase.title}
|
||||
|
||||
${cliRegressionCase.body}`;
|
||||
const inferred = inferComponentLabels(text, keywords);
|
||||
assert.ok(!inferred.includes('cli'), `expected 'cli' absent (body contains 'Mem0 client', a substring of the removed 'mem0 cli' term), got ${JSON.stringify(inferred)}`);
|
||||
});
|
||||
|
||||
run('noKeywordMatchReturnsEmptyArray', () => {
|
||||
const text = 'The weather today is sunny and I went for a walk in the park with my dog.';
|
||||
assert.deepStrictEqual(inferComponentLabels(text, keywords), []);
|
||||
});
|
||||
|
||||
run('emptyStringReturnsEmptyArray', () => {
|
||||
assert.deepStrictEqual(inferComponentLabels('', keywords), []);
|
||||
});
|
||||
|
||||
if (failures > 0) {
|
||||
console.error(`
|
||||
${failures} test(s) failed.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`
|
||||
All ${cases.length + 3} tests passed.`);
|
||||
@@ -84,7 +84,6 @@ jobs:
|
||||
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'
|
||||
@@ -151,8 +150,6 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
run: |
|
||||
pip install --upgrade pip
|
||||
pip install -e ".[test,graph,vector_stores,llms,extras]"
|
||||
pip install ruff
|
||||
pip install ruff==0.16.0
|
||||
- name: Run Linting
|
||||
if: needs.check_changes.outputs.mem0_changed == 'true'
|
||||
run: make lint
|
||||
|
||||
@@ -12,28 +12,56 @@ jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: stefanbuck/github-issue-parser@v3
|
||||
id: issue-parser
|
||||
continue-on-error: true
|
||||
with:
|
||||
template-path: .github/ISSUE_TEMPLATE/bug_report.yml
|
||||
|
||||
- uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
issue-form: ${{ steps.issue-parser.outputs.jsonString }}
|
||||
section: component
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
config-path: .github/advanced-issue-labeler.yml
|
||||
|
||||
- uses: stefanbuck/github-issue-parser@v3
|
||||
id: feature-parser
|
||||
if: contains(github.event.issue.labels.*.name, 'enhancement')
|
||||
- name: Infer component from text when the form was not used
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
template-path: .github/ISSUE_TEMPLATE/feature_request.yml
|
||||
script: |
|
||||
const {
|
||||
componentLabels,
|
||||
inferComponentLabels,
|
||||
loadKeywords,
|
||||
} = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/infer-component-labels.js`);
|
||||
|
||||
- uses: redhat-plumbers-in-action/advanced-issue-labeler@v3
|
||||
if: contains(github.event.issue.labels.*.name, 'enhancement')
|
||||
with:
|
||||
issue-form: ${{ steps.feature-parser.outputs.jsonString }}
|
||||
section: component
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
config-path: .github/advanced-issue-labeler.yml
|
||||
const { data: issue } = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
|
||||
const keywords = loadKeywords(`${process.env.GITHUB_WORKSPACE}/.github/component-keywords.json`);
|
||||
const known = componentLabels(keywords);
|
||||
const existing = issue.labels.map((label) => label.name || label);
|
||||
if (existing.some((name) => known.includes(name))) {
|
||||
core.info(`Component label already present: ${existing.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = inferComponentLabels(`${issue.title}\n\n${issue.body || ''}`, keywords);
|
||||
|
||||
if (labels.length === 0) {
|
||||
core.info('No component could be inferred from the issue text');
|
||||
return;
|
||||
}
|
||||
|
||||
core.info(`Inferred: ${labels.join(', ')}`);
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels,
|
||||
});
|
||||
|
||||
@@ -35,6 +35,29 @@ jobs:
|
||||
- name: Lint
|
||||
run: cd integrations/n8n-nodes-mem0 && pnpm run lint
|
||||
|
||||
test:
|
||||
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: Run tests
|
||||
run: cd integrations/n8n-nodes-mem0 && pnpm test
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
name: PR Labeler
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
|
||||
concurrency:
|
||||
group: pr-labeler-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: read
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Propagate labels from linked issues
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const allowed = new Set([
|
||||
'sdk-python', 'sdk-typescript', 'vector-store', 'plugin',
|
||||
'rest-api', 'documentation', 'ci', 'cli', 'integrations',
|
||||
]);
|
||||
const umbrella = { plugin: 'integrations' };
|
||||
const { repository } = await github.graphql(
|
||||
`query ($owner: String!, $repo: String!, $number: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $number) {
|
||||
closingIssuesReferences(first: 20) {
|
||||
nodes { labels(first: 50) { nodes { name } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{ owner: context.repo.owner, repo: context.repo.repo, number: context.issue.number },
|
||||
);
|
||||
|
||||
const labels = new Set();
|
||||
for (const issue of repository.pullRequest.closingIssuesReferences.nodes) {
|
||||
for (const label of issue.labels.nodes) {
|
||||
if (allowed.has(label.name)) labels.add(label.name);
|
||||
}
|
||||
}
|
||||
|
||||
for (const label of [...labels]) {
|
||||
if (umbrella[label]) labels.add(umbrella[label]);
|
||||
}
|
||||
|
||||
if (labels.size > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: [...labels],
|
||||
});
|
||||
}
|
||||
@@ -33,6 +33,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build TypeScript
|
||||
run: pnpm build
|
||||
|
||||
- name: Push to Zapier
|
||||
env:
|
||||
ZAPIER_DEPLOY_KEY: ${{ secrets.ZAPIER_DEPLOY_KEY }}
|
||||
|
||||
@@ -3,10 +3,10 @@ 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).
|
||||
# CI compiles the TypeScript app, runs `zapier validate` (offline schema + style
|
||||
# checks) against the build, plus the offline jest unit suite (test/unit.test.ts —
|
||||
# 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 README).
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
@@ -37,6 +37,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: cd integrations/zapier-mem0 && pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build TypeScript
|
||||
run: cd integrations/zapier-mem0 && pnpm build
|
||||
|
||||
- name: Validate Zapier app definition
|
||||
run: cd integrations/zapier-mem0 && npx zapier-platform-cli@19 validate
|
||||
|
||||
|
||||
@@ -27,8 +27,9 @@ This is a **polyglot monorepo** containing Python and TypeScript packages, CLIs,
|
||||
| `integrations/openclaw/` | `@mem0/openclaw-mem0` — OpenClaw plugin for Claude Code / AI editors |
|
||||
| `integrations/pi-agent-plugin/` | `@mem0/pi-agent-plugin` — Pi Agent plugin |
|
||||
| `integrations/vercel-ai-sdk/` | `@mem0/vercel-ai-provider` — Vercel AI SDK memory provider |
|
||||
| `integrations/n8n-nodes-mem0/` | `@mem0/n8n-nodes-mem0` — n8n community node; add / search / get / update / delete memories |
|
||||
| `integrations/zapier-mem0/` | `@mem0/zapier` — Zapier Platform CLI app (deploys to Zapier, not npm); add / search / get / delete memories |
|
||||
| `server/` | FastAPI REST server for self-hosted Mem0 (Docker: FastAPI + PostgreSQL/pgvector + Neo4j) |
|
||||
| `openmemory/` | Self-hosted memory platform — `api/` (FastAPI + Alembic + MCP server) and `ui/` (Next.js 15 + React 19) |
|
||||
| `skills/` | Claude Code skill definitions. Reference skills (SDK knowledge, always-on): `mem0/`, `mem0-cli/`, `mem0-vercel-ai-sdk/`. Pipeline skills (run on demand): `mem0-integrate/`, `mem0-test-integration/`, `mem0-oss-to-platform/` |
|
||||
| `docs/` | Documentation site (Mintlify) |
|
||||
| `tests/` | Python SDK tests (pytest) |
|
||||
@@ -62,7 +63,7 @@ integrations/openclaw/ ──▶ mem0ai (npm)
|
||||
- **Node.js**: v18+ (v20 or v22 recommended)
|
||||
- **pnpm**: v10+ (`npm install -g pnpm@10`) — used for all TypeScript packages
|
||||
- **Hatch**: Python build/environment tool (`pip install hatch`)
|
||||
- **Docker**: Required for `server/` and `openmemory/` development
|
||||
- **Docker**: Required for `server/` development
|
||||
|
||||
### Initial Setup
|
||||
|
||||
@@ -214,28 +215,6 @@ docker-compose up # starts all 3 services
|
||||
- **Services:** PostgreSQL with pgvector, Neo4j 5.x with APOC plugin
|
||||
- **Hot reload:** Dev Dockerfile mounts `server/` and `mem0/` for live changes
|
||||
|
||||
### OpenMemory (`openmemory/`)
|
||||
|
||||
```bash
|
||||
# Full stack via Docker Compose
|
||||
cd openmemory
|
||||
docker-compose up
|
||||
# Qdrant: localhost:6333
|
||||
# API (MCP): localhost:8765
|
||||
# UI: localhost:3000
|
||||
|
||||
# Individual development
|
||||
cd openmemory/api && uvicorn main:app --reload # FastAPI backend
|
||||
cd openmemory/ui && npm run dev # Next.js frontend
|
||||
|
||||
# Tests
|
||||
cd openmemory/api && pytest tests/ # API tests (e.g., test_mcp_server.py)
|
||||
```
|
||||
|
||||
- **API:** FastAPI + Alembic (DB migrations) + MCP server (Model Context Protocol)
|
||||
- **UI:** Next.js 15, React 19, Radix UI, Redux Toolkit, TailwindCSS, Recharts
|
||||
- **Vector store:** Qdrant
|
||||
|
||||
### Documentation (`docs/`)
|
||||
|
||||
```bash
|
||||
@@ -331,7 +310,6 @@ python -m benchmarks.beam.run --project-name my-test --backend cloud --mem0-api-
|
||||
- Root SDK: line length **120**
|
||||
- Python CLI: line length **100** with extended rule set (UP, B, SIM, RUF)
|
||||
- **isort** with `profile = "black"` for import sorting.
|
||||
- Ruff excludes `openmemory/` from root config.
|
||||
|
||||
### TypeScript Conventions
|
||||
|
||||
@@ -382,7 +360,6 @@ Optional layer on top of vector memory for relationship-aware retrieval. Configu
|
||||
Model Context Protocol support in multiple places:
|
||||
|
||||
- **Remote:** MCP server at `mcp.mem0.ai`
|
||||
- **Local:** MCP server in `openmemory/api/` (FastAPI-based)
|
||||
- **Plugin:** MCP tools in `integrations/mem0-plugin/` — 9 tools: `add_memory`, `search_memories`, `get_memories`, `get_memory`, `update_memory`, `delete_memory`, `delete_all_memories`, `delete_entities`, `list_entities`
|
||||
|
||||
### Plugin & Skills System
|
||||
@@ -432,7 +409,7 @@ PR testing is orchestrated by a single entry point: **`ci-gate.yml` (CI Gate)**
|
||||
| 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 |
|
||||
| Zapier App | `zapier-mem0-checks.yml` | Push to main (on `integrations/zapier-mem0/`), manual | build (tsc) + `zapier validate` + offline unit tests on Node 22 |
|
||||
| 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.
|
||||
@@ -452,7 +429,7 @@ 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`) |
|
||||
| n8n Node | `n8n-nodes-mem0-cd.yml` | `n8n-nodes-mem0-v*` | npm (`@mem0/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.
|
||||
@@ -466,6 +443,7 @@ Publishing is routed through a single entry point: **`release.yml` (Release Rout
|
||||
| Workflow | File | Purpose |
|
||||
|----------|------|---------|
|
||||
| Issue Labeler | `issue-labeler.yml` | Automatic issue labeling |
|
||||
| PR Labeler | `pr-labeler.yml` | Path-based PR labeling plus propagating labels from linked issues |
|
||||
| Stale Bot | `stale.yml` | Marks stale issues and PRs |
|
||||
| llms.txt Check | `docs-llms-txt-check.yml` | Blocks PRs touching `docs/**/*.mdx` when `docs/llms.txt` is out of sync. Fix locally with `python scripts/check-llms-txt-coverage.py --write`. |
|
||||
|
||||
@@ -588,7 +566,7 @@ N/A
|
||||
|
||||
- Follow existing code patterns — don't introduce new frameworks or abstractions without discussion.
|
||||
- Version bumps go in `pyproject.toml` (Python) or `package.json` (TypeScript).
|
||||
- For `server/` and `openmemory/` work, use Docker Compose for local development.
|
||||
- For `server/` work, use Docker Compose for local development.
|
||||
- Do NOT use `pip` or `conda` for dependency management — use `hatch` (see `docs/contributing/development.mdx`).
|
||||
|
||||
### Contributing Guides
|
||||
@@ -611,5 +589,4 @@ N/A
|
||||
- Use npm or yarn in TypeScript packages — this repo uses pnpm exclusively.
|
||||
- Use `require()` for imports in TypeScript — use ES module `import` syntax.
|
||||
- Mix up linter configs: root Python SDK uses line-length 120, Python CLI uses 100, Node CLI uses Biome (not ESLint/Ruff).
|
||||
- Modify `openmemory/` database migrations without understanding the Alembic migration chain.
|
||||
- Change public APIs without updating documentation in `docs/`.
|
||||
|
||||
@@ -45,7 +45,7 @@ The two most common contribution targets are the SDKs:
|
||||
| TypeScript SDK (`mem0ai`) | `mem0-ts/` | TypeScript | `pnpm` |
|
||||
|
||||
Other packages include the CLIs (`cli/python/`, `cli/node/`), integrations
|
||||
(`integrations/`), the self-hosted `server/`, `openmemory/`, and the docs site
|
||||
(`integrations/`), the self-hosted `server/`, and the docs site
|
||||
(`docs/`). See [AGENTS.md](./AGENTS.md) for a full map of the repository.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
@@ -11,7 +11,7 @@ install:
|
||||
hatch env create
|
||||
|
||||
install_all:
|
||||
pip install ruff==0.6.9 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
|
||||
pip install ruff==0.16.0 groq together boto3 litellm ollama chromadb weaviate weaviate-client sentence_transformers vertexai \
|
||||
google-generativeai elasticsearch opensearch-py vecs "pinecone<7.0.0" pinecone-text faiss-cpu langchain-community \
|
||||
upstash-vector azure-search-documents langchain-memgraph langchain-neo4j langchain-aws rank-bm25 pymochow pymongo psycopg kuzu databricks-sdk valkey
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ privately through one of the following channels:
|
||||
To help us triage and resolve the issue quickly, please include as much of the
|
||||
following as you can:
|
||||
|
||||
- Affected component or package (e.g. Python SDK, TypeScript SDK, server, OpenMemory)
|
||||
- Affected component or package (e.g. Python SDK, TypeScript SDK, server, CLI)
|
||||
- Affected version, tag, or commit
|
||||
- Clear, step-by-step reproduction instructions
|
||||
- The security impact and a proof of concept, if available
|
||||
|
||||
@@ -7,6 +7,35 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<Update label="2026-07-25" description="v2.0.14">
|
||||
|
||||
**New Features:**
|
||||
- **Vector Stores:** Add an Oracle AI Vector Search provider (`oracledb`) with connection pooling, `HNSW`/`IVF` indexes, JSON metadata filtering, and six selectable distance metrics ([#5358](https://github.com/mem0ai/mem0/pull/5358))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Vector Stores:** Translate a `"*"` filter value in OpenSearch into an `exists` query for every key, not just identity keys. It was previously ignored or matched literally against the string `"*"`, so a wildcard filter returned nothing ([#6522](https://github.com/mem0ai/mem0/pull/6522))
|
||||
- **Vector Stores:** Re-raise errors from OpenSearch `search()` instead of returning `[]`, so a transport, auth, or index misconfiguration surfaces instead of looking like zero matches. `keyword_search()` still degrades on failure, since it is a best-effort BM25 signal ([#6519](https://github.com/mem0ai/mem0/pull/6519))
|
||||
- **Vector Stores:** Guard the `text` field in Milvus `update()` behind the `_has_bm25_schema` check, matching `insert()`, so updating a memory in a collection without the BM25 `text`/`sparse` schema no longer fails ([#5705](https://github.com/mem0ai/mem0/pull/5705))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-07-22" description="v2.0.13">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Vector Stores:** Fix `reset()` silently leaving stale vectors behind on local (on-disk) Qdrant when the old collection directory could not be removed, for example an open file handle on Windows or NFS ([#6412](https://github.com/mem0ai/mem0/pull/6412))
|
||||
- **Core:** Stop `update()` metadata from overwriting or injecting `user_id`, `agent_id`, `run_id`, or `actor_id`. These identity fields are immutable after creation, so passing them in `metadata` can no longer move a memory into a different tenant's scope ([#6278](https://github.com/mem0ai/mem0/pull/6278))
|
||||
- **Vector Stores:** Scope Pinecone `delete_col()`/`reset()` to the configured namespace instead of deleting the whole index, so resetting a namespaced Pinecone store no longer wipes out the other namespaces sharing that index ([#6287](https://github.com/mem0ai/mem0/pull/6287))
|
||||
- **Vector Stores:** Convert Baidu Mochow's raw L2 distance into a similarity score in `search()` (`1 / (1 + distance)`), so closer matches rank higher instead of lower, matching the Milvus provider and the rest of the `VectorStoreBase` contract ([#6435](https://github.com/mem0ai/mem0/pull/6435))
|
||||
- **LLMs:** Read `OPENAI_BASE_URL` (was `OPENAI_API_BASE`) in `OpenAIStructuredLLM`, matching the official OpenAI SDK's environment variable and the rest of the OpenAI-compatible providers ([#6322](https://github.com/mem0ai/mem0/pull/6322))
|
||||
|
||||
**Improvements:**
|
||||
- **LLMs:** Remove a dead, no-op `api_key` attribute check from `LLMBase.__init__` ([#6460](https://github.com/mem0ai/mem0/pull/6460))
|
||||
|
||||
**Changes:**
|
||||
- **Client:** Remove the `retrieval_criteria` parameter from `MemoryClient.update_project()`/`AsyncMemoryClient.update_project()` and `Project.update()`/`AsyncProject.update()`. It was accepted and forwarded but never affected retrieval, so removing it is not a behavior change ([#6313](https://github.com/mem0ai/mem0/pull/6313))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-07-13" description="v2.0.12">
|
||||
|
||||
**New Features:**
|
||||
@@ -1128,6 +1157,35 @@ See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-
|
||||
|
||||
<Tab title="TypeScript">
|
||||
|
||||
<Update label="2026-07-25" description="v3.1.2">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Vector Stores:** Apply every operator in a Cassandra compound field filter (e.g. `{ age: { gte: 10, lte: 20 } }`) instead of stopping after the first, so the remaining bounds are no longer silently ignored ([#6511](https://github.com/mem0ai/mem0/pull/6511))
|
||||
- **Vector Stores:** Stop the Chroma where-clause translator from dropping filter conditions. Same-field ranges (`gte` + `lte`), multi-field conditions inside `$or`, and negated `contains`/`icontains` under `$not` each collapsed to a single clause or vanished, widening the search instead of narrowing it ([#6521](https://github.com/mem0ai/mem0/pull/6521))
|
||||
- **Vector Stores:** Skip `"*"` wildcard filter values in Milvus instead of matching them literally, so a filter like `{ user_id: "*" }` no longer returns zero memories ([#6508](https://github.com/mem0ai/mem0/pull/6508))
|
||||
- **Vector Stores:** Read `textLemmatized` for BM25 keyword search on Milvus, OpenSearch, and MongoDB, matching the field the memory layer actually writes, so hybrid search on those backends no longer loses the keyword signal ([#6497](https://github.com/mem0ai/mem0/pull/6497))
|
||||
- **LLMs:** Forward `responseFormat` to Gemini's `responseMimeType` in `generateResponse()`, so requesting `json_object` returns JSON instead of free-form text ([#6468](https://github.com/mem0ai/mem0/pull/6468))
|
||||
- **LLMs:** Find the Anthropic text block by type instead of indexing `content[0]`, so a thinking-enabled model whose `thinking` block comes first no longer throws `Unexpected response type from Anthropic API` ([#6506](https://github.com/mem0ai/mem0/pull/6506))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-07-22" description="v3.1.1">
|
||||
|
||||
**New Features:**
|
||||
- **Embeddings:** Add an AWS Bedrock embedding provider ([#6185](https://github.com/mem0ai/mem0/pull/6185))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Packaging:** Finish the lazy-loading work started in v3.1.0. The remaining LLMs (Anthropic, Google, Groq, LangChain, Mistral, Ollama), embedders (Google, LangChain, Ollama, Vertex AI), vector stores (Azure AI Search, Azure MySQL, Baidu, LangChain, Qdrant, Redis, Supabase, Valkey, Vectorize), and the Supabase history store still imported their SDKs at module load, so importing `mem0ai/oss` required every provider package to be installed ([#6389](https://github.com/mem0ai/mem0/pull/6389))
|
||||
- **Vector Stores:** Convert Baidu Mochow's raw L2 distance into a similarity score in `search()` (`1 / (1 + distance)`), so closer matches rank higher instead of lower. A row the backend returns without a score is now left `undefined` instead of being treated as the closest match ([#6485](https://github.com/mem0ai/mem0/pull/6485))
|
||||
- **Memory (OSS):** Coerce non-string entity IDs (e.g. a numeric `user_id`) to strings instead of crashing on `.trim()` ([#6263](https://github.com/mem0ai/mem0/pull/6263))
|
||||
- **Memory (OSS):** Stop `update()` metadata from overwriting or injecting `user_id`, `agent_id`, `run_id`, or `actor_id` (in either snake_case or camelCase). These identity fields are immutable after creation, so passing them in `metadata` can no longer move a memory into a different tenant's scope ([#6343](https://github.com/mem0ai/mem0/pull/6343))
|
||||
- **Vector Stores:** Scope Pinecone `deleteCol()`/`reset()` to the configured namespace instead of deleting the whole index, so resetting a namespaced Pinecone store no longer wipes out the other namespaces sharing that index ([#6287](https://github.com/mem0ai/mem0/pull/6287))
|
||||
|
||||
**Changes:**
|
||||
- **Client:** Remove the unused `retrievalCriteria` field from `PromptUpdatePayload`. It was accepted and forwarded but never affected retrieval, so removing it is not a behavior change ([#6313](https://github.com/mem0ai/mem0/pull/6313))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-07-13" description="v3.1.0">
|
||||
|
||||
The largest provider release for the TypeScript OSS SDK so far: 17 new vector stores, 5 new LLM providers, 4 new embedders, and reranking support. Importing `mem0ai/oss` no longer pulls in any provider SDK, so you only install what you actually configure.
|
||||
@@ -2084,6 +2142,13 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="OpenCode">
|
||||
|
||||
<Update label="2026-07-22" description="OpenCode plugin v0.2.2">
|
||||
|
||||
**Fixes:**
|
||||
- **Shell-profile API key recovery:** When `MEM0_API_KEY` isn't set in the process environment, the plugin now falls back to reading it from `.zshrc`, `.bashrc`, `.zprofile`, `.bash_profile`, or `.profile`, fixing startup failures on clients (e.g. Desktop) that launch without shell-exported environment variables.
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-30" description="OpenCode plugin v0.2.1">
|
||||
|
||||
**Improvements:**
|
||||
|
||||
@@ -7,7 +7,7 @@ description: "Reference for vector database configuration options in Mem0, inclu
|
||||
|
||||
The `config` is defined as an object with two main keys:
|
||||
- `vector_store`: Specifies the vector database provider and its configuration
|
||||
- `provider`: The name of the vector database (e.g., "chroma", "pgvector", "qdrant", "milvus", "upstash_vector", "azure_ai_search", "vertex_ai_vector_search", "valkey")
|
||||
- `provider`: The name of the vector database (e.g., "chroma", "pgvector", "qdrant", "milvus", "upstash_vector", "azure_ai_search", "vertex_ai_vector_search", "valkey", "oracledb")
|
||||
- `config`: A nested dictionary containing provider-specific settings
|
||||
|
||||
|
||||
@@ -95,6 +95,11 @@ Here's a comprehensive list of all parameters that can be used across different
|
||||
| `connection_string` | PostgreSQL connection string (for Supabase/PGVector) |
|
||||
| `index_method` | Vector index method (for Supabase) |
|
||||
| `index_measure` | Distance measure for similarity search (for Supabase) |
|
||||
| `connection_params` | Connection settings for Oracle AI Vector Search |
|
||||
| `use_connection_pool` | Create an Oracle connection pool from `connection_params` |
|
||||
| `distance_metric` | Distance metric for Oracle vector indexing and search |
|
||||
| `index_type` | Oracle vector index type: `HNSW` or `IVF` |
|
||||
| `index_parameters` | Oracle vector-index parameters for the selected index type |
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
| Parameter | Description |
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
title: "Oracle AI Vector Search"
|
||||
description: "Use Oracle Database AI Vector Search as a vector store in Mem0 for semantic and relational queries."
|
||||
---
|
||||
|
||||
[Oracle AI Vector Search](https://www.oracle.com/database/ai-vector-search/) stores embeddings in an Oracle table using the native `VECTOR` data type, so you can combine semantic search over unstructured data with relational queries over business data in a single database.
|
||||
|
||||
### Requirements
|
||||
|
||||
- Oracle Database 23.4 or later, with a user that can create tables and vector indexes
|
||||
- The `python-oracledb` driver. In thick mode, Oracle Client 23.4 or later is also required.
|
||||
|
||||
```bash
|
||||
pip install oracledb
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "sk-xx"
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "oracledb",
|
||||
"config": {
|
||||
"collection_name": "mem0",
|
||||
"embedding_model_dims": 1536,
|
||||
"connection_params": {
|
||||
"user": "mem0_user",
|
||||
"password": "your-password",
|
||||
"dsn": "localhost:1521/FREEPDB1",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m = Memory.from_config(config)
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
|
||||
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
|
||||
]
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
To reuse a connection or pool you already manage, pass it as `client` instead of `connection_params`:
|
||||
|
||||
```python
|
||||
import oracledb
|
||||
|
||||
pool = oracledb.create_pool(user="mem0_user", password="your-password", dsn="localhost:1521/FREEPDB1")
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "oracledb",
|
||||
"config": {"client": pool},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
Here are the parameters available for configuring Oracle AI Vector Search:
|
||||
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `connection_params` | Connection settings passed to `python-oracledb`, such as `user`, `password` and `dsn`. See the [connection handling guide](https://python-oracledb.readthedocs.io/en/latest/user_guide/connection_handling.html). | `None` |
|
||||
| `use_connection_pool` | Create a connection pool from `connection_params` instead of a single connection | `True` |
|
||||
| `client` | An existing `oracledb.Connection` or `oracledb.ConnectionPool` to use instead of building one from `connection_params` | `None` |
|
||||
| `collection_name` | Name of the Oracle table that stores vectors and payloads | `mem0` |
|
||||
| `embedding_model_dims` | Dimension of your embedding vectors, must be greater than 0 | `1536` |
|
||||
| `distance_metric` | Distance function used for indexing and search: `COSINE`, `EUCLIDEAN`, `EUCLIDEAN_SQUARED`, `DOT`, `HAMMING` or `MANHATTAN` | `COSINE` |
|
||||
| `do_create_index` | Whether to create a vector index on the collection | `True` |
|
||||
| `index_type` | Vector index type: `HNSW` or `IVF` | `HNSW` |
|
||||
| `index_name` | Name of the vector index | `<collection_name>_VEC_IDX` |
|
||||
| `index_parameters` | Index tuning parameters. For `HNSW`: `neighbors`, `efconstruction`. For `IVF`: `neighbor partitions`, `samples_per_partition`, `min_vectors_per_partition`. | `None` |
|
||||
| `index_accuracy` | Target index accuracy from 1 to 100, applied as `WITH TARGET ACCURACY <n>` | `None` |
|
||||
|
||||
<Note>
|
||||
When you pass a pre-built `client`, Mem0 uses it as-is and ignores `connection_params` and `use_connection_pool`. Mem0 does not close a client it did not create.
|
||||
</Note>
|
||||
|
||||
### Vector indexes
|
||||
|
||||
Set the index type with `index_type` and tune it with `index_parameters`:
|
||||
|
||||
```python
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "oracledb",
|
||||
"config": {
|
||||
"connection_params": {"user": "mem0_user", "password": "your-password", "dsn": "localhost:1521/FREEPDB1"},
|
||||
"index_type": "HNSW",
|
||||
"index_parameters": {"neighbors": 32, "efconstruction": 200},
|
||||
"index_accuracy": 95,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For the full list of supported options, see the Oracle [`CREATE VECTOR INDEX`](https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlrf/create-vector-index.html) reference.
|
||||
|
||||
### Search scores
|
||||
|
||||
Oracle returns a distance from `VECTOR_DISTANCE`, which Mem0 converts to a `score` where higher means more similar. `COSINE` and the other non-negative metrics produce scores in the range `[0, 1]`. `DOT` returns the inner product, which can fall outside that range.
|
||||
|
||||
### Metadata filters
|
||||
|
||||
Filters run against the JSON `payload` column and support:
|
||||
|
||||
| Filter type | Examples |
|
||||
| --- | --- |
|
||||
| Scalar equality | `{"user_id": "alice"}` |
|
||||
| Field existence | `{"agent_id": "*"}` |
|
||||
| Comparison | `{"score": {"gte": 0.5}}`, also `eq`, `ne`, `gt`, `lt`, `lte` |
|
||||
| Membership | `{"category": {"in": ["movies", "books"]}}`, also `nin` |
|
||||
| String matching | `{"title": {"contains": "sci-fi"}}`, also `icontains` for case-insensitive |
|
||||
| Logical groups | `{"AND": [...]}`, `{"OR": [...]}`, `{"NOT": [...]}` |
|
||||
|
||||
Multiple fields at the top level are combined with `AND`:
|
||||
|
||||
```python
|
||||
m.search(
|
||||
"movie recommendations",
|
||||
user_id="alice",
|
||||
filters={"category": {"in": ["movies", "books"]}, "rating": {"gte": 4}},
|
||||
)
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Overview
|
||||
description: "Overview of all supported vector databases in Mem0, including Qdrant, Chroma, PGVector, Pinecone, and more."
|
||||
description: "Overview of all supported vector databases in Mem0, including Qdrant, Chroma, PGVector, Pinecone, Oracle, and more."
|
||||
---
|
||||
|
||||
Mem0 includes built-in support for various popular databases. Memory can utilize the database provided by the user, ensuring efficient use for specific needs.
|
||||
@@ -21,6 +21,7 @@ See the list of supported vector databases below.
|
||||
<Card title="Milvus" icon="/images/provider-icons/milvus.svg" href="/components/vectordbs/dbs/milvus"></Card>
|
||||
<Card title="Pinecone" icon="/images/provider-icons/pinecone.svg" href="/components/vectordbs/dbs/pinecone"></Card>
|
||||
<Card title="MongoDB" icon="/images/provider-icons/mongodb.svg" href="/components/vectordbs/dbs/mongodb"></Card>
|
||||
<Card title="Oracle AI Vector Search" icon="/images/provider-icons/oracle.svg" href="/components/vectordbs/dbs/oracledb"></Card>
|
||||
<Card title="Azure" icon="/images/provider-icons/azure-color.svg" href="/components/vectordbs/dbs/azure"></Card>
|
||||
<Card title="Redis" icon="/images/provider-icons/redis.svg" href="/components/vectordbs/dbs/redis"></Card>
|
||||
<Card title="Valkey" icon="/images/provider-icons/valkey.svg" href="/components/vectordbs/dbs/valkey"></Card>
|
||||
|
||||
@@ -207,6 +207,7 @@
|
||||
"components/vectordbs/dbs/milvus",
|
||||
"components/vectordbs/dbs/pinecone",
|
||||
"components/vectordbs/dbs/mongodb",
|
||||
"components/vectordbs/dbs/oracledb",
|
||||
"components/vectordbs/dbs/azure",
|
||||
"components/vectordbs/dbs/azure_mysql",
|
||||
"components/vectordbs/dbs/redis",
|
||||
@@ -635,7 +636,7 @@
|
||||
},
|
||||
{
|
||||
"source": "/platform/features/expiration-date",
|
||||
"destination": "/"
|
||||
"destination": "/platform/features/memory-expiration"
|
||||
},
|
||||
{
|
||||
"source": "/cookbooks/essentials/memory-expiration-short-and-long-term",
|
||||
@@ -1236,6 +1237,10 @@
|
||||
{
|
||||
"source": "/platform/features/criteria-retrieval",
|
||||
"destination": "/platform/features/advanced-retrieval"
|
||||
},
|
||||
{
|
||||
"source": "/integrations/keywords",
|
||||
"destination": "/integrations/respan"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 66 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="#8F74E0" role="img" viewBox="0 0 93.9 59.4" xmlns="http://www.w3.org/2000/svg"><title>Oracle</title><path d="M30.5,59.4H65c16.4-0.4,29.3-14.1,28.9-30.4C93.5,13.1,80.7,0.4,65,0H30.5C14.1-0.4,0.4,12.5,0,28.9s12.5,30,28.9,30.4C29.4,59.4,29.9,59.4,30.5,59.4 M64.2,48.9h-33c-10.6-0.3-18.9-9.2-18.6-19.8C13,19,21.1,10.8,31.2,10.5h33c10.6-0.3,19.5,8,19.8,18.6c0.3,10.6-8,19.5-18.6,19.8C65,48.9,64.6,48.9,64.2,48.9"/></svg>
|
||||
|
After Width: | Height: | Size: 427 B |
@@ -3,62 +3,147 @@ 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.
|
||||
Your n8n workflows start from zero on every run. The [`@mem0/n8n-nodes-mem0`](https://www.npmjs.com/package/@mem0/n8n-nodes-mem0) community node fixes that: store durable facts as memories, recall them in any later run, and hand the node to an [n8n AI Agent](https://docs.n8n.io/advanced-ai/) as a tool so it can remember and recall on its own.
|
||||
|
||||
## Overview
|
||||
|
||||
The node wraps the hosted Mem0 REST API and supports six operations on the **Memory** resource:
|
||||
1. Install the node from n8n's community nodes panel.
|
||||
2. Connect your Mem0 API key once as a credential.
|
||||
3. Drop a **Mem0** node into any workflow to add, search, or manage memories.
|
||||
4. Optionally attach it to an **AI Agent** node, where it becomes a tool the agent calls itself.
|
||||
|
||||
| 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}/` |
|
||||
## Prerequisites
|
||||
|
||||
It is marked `usableAsTool`, so the n8n AI Agent (Tools Agent) can call it directly to remember and recall information.
|
||||
1. A Mem0 API key from the <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-n8n" rel="nofollow">API Keys dashboard</a> (sign up at <a href="https://app.mem0.ai?utm_source=oss&utm_medium=integration-n8n" rel="nofollow">app.mem0.ai</a> if you do not have an account).
|
||||
2. A **self-hosted** n8n instance. Installing community nodes from npm is a self-hosted feature; n8n Cloud only offers nodes that n8n has verified.
|
||||
3. Owner access to that instance, since only instance owners can install community nodes.
|
||||
|
||||
## Installation
|
||||
|
||||
Install it like any n8n community node:
|
||||
<Steps>
|
||||
<Step title="Open the community nodes panel">
|
||||
In n8n, go to **Settings → Community Nodes** and select **Install**.
|
||||
</Step>
|
||||
<Step title="Install the package">
|
||||
Enter `@mem0/n8n-nodes-mem0`, tick the risk acknowledgement, and select **Install**.
|
||||
</Step>
|
||||
<Step title="Create the credential">
|
||||
Add a new **Mem0 API** credential and paste your API key. Leave **Base URL** at `https://api.mem0.ai` unless you run Mem0 somewhere else.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
1. In n8n, go to **Settings → Community Nodes → Install**.
|
||||
2. Enter `n8n-nodes-mem0` and confirm.
|
||||
<Info>
|
||||
**Verify the install:** search the nodes panel for `Mem0`. The node should appear with a **Memory** resource offering Add, Search, Get, Get Many, Update, and Delete.
|
||||
</Info>
|
||||
|
||||
The node then appears in the nodes panel under the AI category.
|
||||
## Quickstart
|
||||
|
||||
## Authentication
|
||||
A two-node workflow that writes a memory and reads it back:
|
||||
|
||||
Create a **Mem0 API** credential in n8n:
|
||||
```text
|
||||
Manual Trigger → Mem0 (Add) → Mem0 (Search)
|
||||
```
|
||||
|
||||
- **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`.
|
||||
<Steps>
|
||||
<Step title="Add a memory">
|
||||
Add a **Mem0** node, keep **Operation: Add**, set **User ID** to `alice`, and add one message with **Role** `user` and **Content**:
|
||||
|
||||
`I am vegetarian and I never eat mushrooms.`
|
||||
</Step>
|
||||
<Step title="Search for it">
|
||||
Add a second **Mem0** node with **Operation: Search**, **User ID** `alice`, and **Query** `what does the user eat?`.
|
||||
</Step>
|
||||
<Step title="Run it">
|
||||
Select **Test workflow**. The Search node returns the extracted dietary memory.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Note>
|
||||
Extraction is asynchronous. The Add node's **Wait for Completion** option is on by default, so it polls until extraction finishes before the next node runs. If you turn it off, allow a few seconds before searching for what you just wrote.
|
||||
</Note>
|
||||
|
||||
## Use it as an AI Agent tool
|
||||
|
||||
The node is marked `usableAsTool`, so an n8n **AI Agent** (Tools Agent) can call it without any wiring on your side:
|
||||
|
||||
```text
|
||||
Chat Trigger → AI Agent ──tool──▶ Mem0 (Search)
|
||||
──tool──▶ Mem0 (Add)
|
||||
```
|
||||
|
||||
Attach one Mem0 node set to **Search** and one set to **Add**. The agent searches memory before answering and writes back durable facts after a meaningful exchange. Keep **User ID** the same on both.
|
||||
|
||||
## Operations
|
||||
|
||||
The node wraps the hosted Mem0 REST API and supports six operations on the **Memory** resource:
|
||||
|
||||
| Operation | What it does | 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 stored memories (one page, or **Return All**) | `POST /v3/memories/` |
|
||||
| **Get** | Fetch a single memory by ID | `GET /v1/memories/{id}/` |
|
||||
| **Update** | Change a memory's text or metadata | `PUT /v1/memories/{id}/` |
|
||||
| **Delete** | Delete a single memory by ID | `DELETE /v1/memories/{id}/` |
|
||||
|
||||
### 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).
|
||||
Extracts and stores memories from one or more messages. Supply at least one entity id (**User ID**, or **Agent ID** / **App ID** / **Run ID** under Additional Fields); the node checks this before calling the API.
|
||||
|
||||
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.
|
||||
**Additional Fields:**
|
||||
|
||||
| Field | Purpose |
|
||||
| --- | --- |
|
||||
| **Agent ID** | Scopes the memory to an agent |
|
||||
| **App ID** | Scopes the memory to an app or project |
|
||||
| **Run ID** | Scopes the memory to a single session or run |
|
||||
| **Metadata (JSON)** | Arbitrary JSON attached to each extracted memory |
|
||||
| **Infer** | On by default. Turn off to store messages verbatim instead of running LLM extraction |
|
||||
| **Custom Instructions** | Free-text guidance steering what the extractor keeps or ignores, for this call |
|
||||
| **Custom Categories** | JSON array of `{category: description}` objects, replacing the project-level catalog for this call |
|
||||
| **Includes** | Only extract memories matching this description |
|
||||
| **Excludes** | Skip memories matching this description |
|
||||
|
||||
**Includes** and **Excludes** narrow what extraction keeps. Sending *"I am vegetarian and I never eat mushrooms. I drive a blue Toyota Corolla and my parking spot is B12"* stores three memories by default; with `Includes: "only record food and diet preferences"` it stores just the dietary one.
|
||||
|
||||
### Search
|
||||
|
||||
Semantic search over stored memories. Provide a **Query**, a **User ID** (required, the API needs an entity filter), and an optional **Limit**.
|
||||
Semantic search over stored memories. Takes a **Query**, at least one entity id, and an optional **Limit**.
|
||||
|
||||
### Get Many
|
||||
|
||||
Lists memories for a **User ID** with **Page** and **Page Size** controls.
|
||||
Lists stored memories for the entity ids you supply. Turn on **Return All** to page through everything automatically, or leave it off to fetch a single **Page**. **Page Size** applies either way.
|
||||
|
||||
### Get / Update / Delete
|
||||
### Get, Update, Delete
|
||||
|
||||
Operate on a single memory by **Memory ID**. Update accepts new **Text** and/or **Metadata (JSON)**.
|
||||
Operate on one memory by **Memory ID**. Update accepts new **Text** and/or **Metadata (JSON)**.
|
||||
|
||||
## Choosing a `userId`
|
||||
## Entity filters on Search and Get Many
|
||||
|
||||
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.
|
||||
Both operations take **User ID**, **Agent ID**, **App ID**, and **Run ID**. At least one is required, since the API rejects a query with no entity scope, and the node fails with a clear message before making the call if all four are empty.
|
||||
|
||||
Supply several and they combine with **OR**, so the result is the union of those scopes:
|
||||
|
||||
```json
|
||||
{ "OR": [{ "user_id": "alice" }, { "agent_id": "support-bot" }] }
|
||||
```
|
||||
|
||||
<Warning>
|
||||
This is deliberate, not a shortcut. Mem0 indexes each entity separately, so an `AND` across `user_id` and `agent_id` matches nothing even when a memory was written with both. To narrow rather than widen, run one operation per entity id.
|
||||
</Warning>
|
||||
|
||||
## Choosing a User ID
|
||||
|
||||
The **User ID** is a stable string you pick to identify whose memories these are. It is not looked up in the dashboard, so any consistent value works: your app's internal user ID, an email, or a UUID. Use the same value across Add, Search, and Get Many or recall returns nothing.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **The node does not appear in the panel**: community nodes install on self-hosted n8n only, and only instance owners can install them. On n8n Cloud, this node is not yet available.
|
||||
- **`401 Unauthorized`**: the API key is wrong or was revoked. Regenerate it in the <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-n8n" rel="nofollow">API Keys dashboard</a> and update the credential.
|
||||
- **"Provide at least one of User ID, Agent ID, App ID, or Run ID"**: every Add, Search, and Get Many needs an entity scope. Fill in at least one.
|
||||
- **Search returns nothing right after an Add**: extraction is asynchronous. Leave **Wait for Completion** on, or add a short Wait node before searching.
|
||||
- **Searching two entity ids returns more than expected**: multiple ids are combined with OR by design. Run one operation per id to narrow.
|
||||
- **"Timed out waiting for memory event"**: the add was accepted and is likely still finishing on the server. A timeout here does not mean it failed.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Zapier Integration" icon="bolt" href="/integrations/zapier">
|
||||
|
||||
@@ -3,51 +3,133 @@ 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.
|
||||
Zaps fire and forget. The [Mem0](https://mem0.ai) app gives them memory: store durable facts from a form submission, a support ticket, or a chat message, then recall them later from any of [Zapier's](https://zapier.com) thousands of apps. No code, no server.
|
||||
|
||||
## Overview
|
||||
|
||||
The app wraps the hosted Mem0 REST API and exposes four actions:
|
||||
1. Connect your Mem0 API key once as a Zapier connection.
|
||||
2. Use **Add Memory** to store what a Zap learns.
|
||||
3. Use **Search Memories** or **Get Memories** to pull that context back into a later step.
|
||||
4. Use **Delete Memory** to remove one by ID.
|
||||
|
||||
| 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/` |
|
||||
## Prerequisites
|
||||
|
||||
## Authentication
|
||||
1. A Mem0 API key from the <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-zapier" rel="nofollow">API Keys dashboard</a> (sign up at <a href="https://app.mem0.ai?utm_source=oss&utm_medium=integration-zapier" rel="nofollow">app.mem0.ai</a> if you do not have an account).
|
||||
2. A Zapier account on any plan.
|
||||
|
||||
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.
|
||||
<Note>
|
||||
The Mem0 app is not yet listed in Zapier's public App Directory, so you need an invite link to add it to a Zap. Email [support@mem0.ai](mailto:support@mem0.ai) to request one.
|
||||
</Note>
|
||||
|
||||
## Setup
|
||||
|
||||
<Steps>
|
||||
<Step title="Add a Mem0 step">
|
||||
In the Zap editor, search for **Mem0** and pick an action such as **Add Memory**.
|
||||
</Step>
|
||||
<Step title="Connect your account">
|
||||
Select **Sign in**, paste your **Mem0 API Key** (it starts with `m0-`), and leave **Base URL** at `https://api.mem0.ai` unless you run Mem0 somewhere else.
|
||||
</Step>
|
||||
<Step title="Confirm the connection">
|
||||
Zapier validates the key against Mem0 the moment you save it. A connection labelled **Mem0** means the key works.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Info>
|
||||
The key is a password field, so Zapier masks it in the editor. It is sent to Mem0 as `Authorization: Token <key>`.
|
||||
</Info>
|
||||
|
||||
## Quickstart
|
||||
|
||||
Remember what a user tells you:
|
||||
|
||||
```text
|
||||
Trigger (form, chat, ticket) → Mem0: Add Memory
|
||||
```
|
||||
|
||||
Set **Content** to the message text and **User ID** to a stable identifier for that person, such as their email.
|
||||
|
||||
Then recall it in a later Zap:
|
||||
|
||||
```text
|
||||
Trigger (new message) → Mem0: Search Memories → Send reply
|
||||
```
|
||||
|
||||
Set **Query** to the incoming message and **User ID** to the same value. The matched memories become available to every step after it.
|
||||
|
||||
<Note>
|
||||
Extraction is asynchronous. **Add Memory** returns immediately with an event ID by default, so a Search fired a second later may not see the new memory yet. See [Waiting for extraction](#waiting-for-extraction).
|
||||
</Note>
|
||||
|
||||
## Actions
|
||||
|
||||
| Type | Action | What it does | Endpoint |
|
||||
| --- | --- | --- | --- |
|
||||
| Create | **Add Memory** | Extract and store memories from a message | `POST /v3/memories/add/` |
|
||||
| Search | **Search Memories** | Semantic search over stored memories | `POST /v3/memories/search/` |
|
||||
| Search | **Get Memories** | List stored memories, one page at a time | `POST /v3/memories/` |
|
||||
| Create | **Delete Memory** | Delete a single memory by ID | `DELETE /v1/memories/{id}/` |
|
||||
|
||||
### Add Memory
|
||||
|
||||
Extracts and stores memories from a message. Fields:
|
||||
| Field | Required | Purpose |
|
||||
| --- | --- | --- |
|
||||
| **Content** | Yes | The message text to extract memories from |
|
||||
| **Role** | | `User` (default), `Assistant`, or `System` |
|
||||
| **User ID** | | Scopes the memory to a person |
|
||||
| **Agent ID** | | Scopes the memory to an agent |
|
||||
| **Run ID** | | Scopes the memory to a single session or run |
|
||||
| **Metadata (JSON)** | | Arbitrary JSON attached to each extracted memory |
|
||||
| **Custom Instructions** | | Free-text guidance steering what the extractor keeps or ignores, for this call |
|
||||
| **Custom Categories (JSON)** | | JSON array of `{category: description}` objects, replacing the project-level catalog for this call |
|
||||
| **Includes** | | Only extract memories matching this description |
|
||||
| **Excludes** | | Skip memories matching this description |
|
||||
| **Infer** | | On by default. Turn off to store the message verbatim instead of running LLM extraction |
|
||||
| **Wait for Completion** | | Off by default. Turn on to poll until extraction finishes and return the resulting memories |
|
||||
|
||||
- **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.
|
||||
**Includes** and **Excludes** narrow what extraction keeps. Sending *"I am vegetarian and I never eat mushrooms. I drive a blue Toyota Corolla and my parking spot is B12"* stores three memories by default; with `Includes: "only record food and diet preferences"` it stores just the dietary one.
|
||||
|
||||
#### Waiting for extraction
|
||||
|
||||
Extraction runs asynchronously, so **Add Memory** returns an event ID and moves on unless you turn on **Wait for Completion**. When you do, the step polls for up to 60 seconds and returns the extracted memories instead.
|
||||
|
||||
<Warning>
|
||||
Extraction can take longer than Zapier allows a single step to run, which is why waiting is opt-in. If the step times out, the add was still accepted and typically completes on Mem0's side, so do not retry it blindly.
|
||||
</Warning>
|
||||
|
||||
### Search Memories
|
||||
|
||||
Semantic search over stored memories. Provide a **Query**, a **User ID** (required, the API needs an entity filter), and an optional **Limit**.
|
||||
| Field | Required | Purpose |
|
||||
| --- | --- | --- |
|
||||
| **Query** | Yes | Natural-language search text |
|
||||
| **User ID** | Yes | Whose memories to search. The API needs an entity filter |
|
||||
| **Limit** | | Maximum results, default `50` |
|
||||
|
||||
### 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.
|
||||
| Field | Required | Purpose |
|
||||
| --- | --- | --- |
|
||||
| **User ID** | Yes | Whose memories to list |
|
||||
| **Limit** | | Memories per page, default `50` |
|
||||
| **Page** | | Which page to return, 1-based, default `1` |
|
||||
|
||||
Returns one page per run. Raise **Page** to walk through larger result sets.
|
||||
|
||||
### Delete Memory
|
||||
|
||||
Deletes a single memory by its **Memory ID**.
|
||||
Takes a **Memory ID** and deletes that memory. Pair it with **Search Memories** or **Get Memories** to get the ID first.
|
||||
|
||||
## Choosing a `userId`
|
||||
## Choosing a User ID
|
||||
|
||||
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.
|
||||
The **User ID** is a stable string you pick to identify whose memories these are. It is not looked up in the dashboard, so any consistent value works: your app's internal user ID, an email, or a UUID. Use the same value on Add, Search, and Get Memories or recall returns nothing.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Mem0 does not appear in the Zap editor**: the app is not yet in the public App Directory. Email [support@mem0.ai](mailto:support@mem0.ai) for an invite link.
|
||||
- **The connection fails when you paste the key**: check that it starts with `m0-` and has not been revoked in the <a href="https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=integration-zapier" rel="nofollow">API Keys dashboard</a>.
|
||||
- **Search returns nothing right after an Add**: extraction is asynchronous. Turn on **Wait for Completion**, or put a Zapier **Delay** step before the Search.
|
||||
- **"Metadata must be valid JSON" or "Custom Categories must be valid JSON"**: those fields take raw JSON. Check for smart quotes and trailing commas.
|
||||
- **The Add step times out**: the memory was still accepted and is likely finishing server-side. Confirm with **Get Memories** before re-running.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="n8n Integration" icon="diagram-project" href="/integrations/n8n">
|
||||
|
||||
@@ -418,7 +418,6 @@ Editor-specific setup docs (already listed above under `## Integrations > AI Cod
|
||||
### MCP Endpoints
|
||||
|
||||
- Hosted MCP server: `https://mcp.mem0.ai` - requires Platform API key. See `platform/mem0-mcp`.
|
||||
- Self-hosted MCP server: ships with `openmemory/api/` (FastAPI) - runs against your own Qdrant + LLM stack.
|
||||
|
||||
## Community & Support
|
||||
|
||||
@@ -474,6 +473,7 @@ Everything below is OSS-only provider configuration. Skip this entire section wh
|
||||
- [Milvus](https://docs.mem0.ai/components/vectordbs/dbs/milvus) [OSS]: Use for large-scale Milvus deployments.
|
||||
- [Pinecone](https://docs.mem0.ai/components/vectordbs/dbs/pinecone) [OSS]: Use when the user is on Pinecone managed.
|
||||
- [MongoDB](https://docs.mem0.ai/components/vectordbs/dbs/mongodb) [OSS]: Use when Mongo Atlas Vector Search is the backing store.
|
||||
- [Oracle AI Vector Search](https://docs.mem0.ai/components/vectordbs/dbs/oracledb) [OSS]: Use when Oracle Database AI Vector Search is the backing store.
|
||||
- [Azure AI Search](https://docs.mem0.ai/components/vectordbs/dbs/azure) [OSS]: Use when the user is on Azure AI Search.
|
||||
- [Azure MySQL](https://docs.mem0.ai/components/vectordbs/dbs/azure_mysql) [OSS]: Use when vector search runs on Azure Database for MySQL.
|
||||
- [Redis](https://docs.mem0.ai/components/vectordbs/dbs/redis) [OSS]: Use when Redis Stack is the backing store.
|
||||
|
||||
@@ -148,7 +148,7 @@ See the full catalog in <Link href="/components/llms/overview">Components</Link>
|
||||
- Qdrant connection errors: confirm port `6333` is exposed and the API key (if set) matches.
|
||||
- Empty search results: verify the embedder model name. A mismatch causes dimension errors.
|
||||
- `Unknown reranker` (Python): upgrade the SDK with `pip install --upgrade mem0ai` to load the latest provider registry.
|
||||
- `Cannot find module` (Node): import from the OSS entry point, `import { Memory } from "mem0ai/oss"`, not `"mem0ai"`.
|
||||
- `Cannot find module` (Node): two common causes. First, import from the OSS entry point, `import { Memory } from "mem0ai/oss"`, not `"mem0ai"`. Second, provider SDKs are optional peer dependencies loaded on demand, so install the one for the provider you configured (for example `npm install @qdrant/js-client-rest` for Qdrant). Installing `mem0ai` alone only pulls in the providers used by default; you do not need SDKs for providers you never select.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
title: Memory Expiration
|
||||
description: "Give a memory a shelf life: set an expiration date and it stops surfacing in search once that date passes, without deleting the record."
|
||||
title: "Memory Expiration in Mem0"
|
||||
sidebarTitle: "Memory Expiration"
|
||||
description: "Set an expiration date on a Mem0 memory and it stops surfacing in search once that date passes. Nothing is deleted. Works on Platform and Open Source."
|
||||
---
|
||||
|
||||
# Memory Expiration
|
||||
## Why Use Memory Expiration?
|
||||
|
||||
Some facts are only true for a while. A trial plan ends, a seasonal preference goes stale, a support ticket ages past its retention window. Set an `expiration_date` on a memory and Mem0 stops surfacing it once that date passes, so you don't need a cleanup job hunting for rows to delete.
|
||||
|
||||
@@ -18,7 +19,7 @@ Some facts are only true for a while. A trial plan ends, a seasonal preference g
|
||||
- **No expiration date means never expires.** That is the default for every memory.
|
||||
- **Malformed dates fail open**: a stored value Mem0 can't parse is treated as *not* expired. A bad date never makes a memory silently vanish.
|
||||
|
||||
## Set an expiration date
|
||||
## How do you set an expiration date on a memory?
|
||||
|
||||
Set it when you add the memory:
|
||||
|
||||
@@ -97,7 +98,7 @@ The same parameter and spelling work on the OSS `Memory` class. See <Link href="
|
||||
Expired memories are dropped *before* your `top_k` is applied, so Mem0 widens the internal candidate pool first and short result sets are rare. They are not impossible: if nearly every memory in a scope has expired, a call can still return fewer than `top_k` results. Pass `show_expired: true` to get the full set back.
|
||||
</Note>
|
||||
|
||||
## Clear an expiration date
|
||||
## How do you clear or remove an expiration date?
|
||||
|
||||
Pass an explicit `None` (Python) or `null` (TypeScript) to make the memory permanent again. The SDKs deliberately preserve that null instead of treating it as "argument not supplied".
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import {afterEach, describe, expect, test} from "bun:test";
|
||||
import {mkdtempSync, mkdirSync, writeFileSync} from "fs";
|
||||
import {tmpdir} from "os";
|
||||
import {join} from "path";
|
||||
import {parseApiKeyLine, resolveApiKey} from "./api-key";
|
||||
import Mem0Plugin from "./opencode-mem0";
|
||||
|
||||
const originalKey = process.env.MEM0_API_KEY;
|
||||
const originalHome = process.env.HOME;
|
||||
const originalUserProfile = process.env.USERPROFILE;
|
||||
const originalTelemetry = process.env.MEM0_TELEMETRY;
|
||||
const originalFetch = globalThis.fetch;
|
||||
const testCleanups: Array<() => Promise<void> | void> = [];
|
||||
|
||||
afterEach(async () => {
|
||||
while (testCleanups.length > 0) {
|
||||
await testCleanups.pop()?.();
|
||||
}
|
||||
if (originalKey === undefined) delete process.env.MEM0_API_KEY;
|
||||
else process.env.MEM0_API_KEY = originalKey;
|
||||
if (originalHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = originalHome;
|
||||
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
||||
else process.env.USERPROFILE = originalUserProfile;
|
||||
if (originalTelemetry === undefined) delete process.env.MEM0_TELEMETRY;
|
||||
else process.env.MEM0_TELEMETRY = originalTelemetry;
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
function home(): string {
|
||||
return mkdtempSync(join(tmpdir(), "mem0-api-key-"));
|
||||
}
|
||||
|
||||
function pluginContext(logs: unknown[]) {
|
||||
return {
|
||||
client: {app: {log: async (entry: unknown) => logs.push(entry)}},
|
||||
$: () => ({quiet: async () => ({stdout: ""})}),
|
||||
} as any;
|
||||
}
|
||||
|
||||
function stubFetch(): void {
|
||||
globalThis.fetch = (async (input) => {
|
||||
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
||||
const body =
|
||||
url.includes("/v1/ping/")
|
||||
? {status: "ok", userEmail: "plugin-test@mem0.dev"}
|
||||
: url.includes("/v1/projects/")
|
||||
? {customCategories: []}
|
||||
: {};
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: {"content-type": "application/json"},
|
||||
});
|
||||
}) as typeof fetch;
|
||||
}
|
||||
|
||||
function captureDeferredPluginCleanup(): () => Promise<void> {
|
||||
const baseline = new Set(process.listeners("beforeExit"));
|
||||
return async () => {
|
||||
await Promise.resolve();
|
||||
for (const listener of process.listeners("beforeExit")) {
|
||||
if (!baseline.has(listener)) {
|
||||
process.off("beforeExit", listener as (...args: any[]) => void);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
describe("parseApiKeyLine", () => {
|
||||
test("accepts literal export and assignment forms", () => {
|
||||
expect(parseApiKeyLine('export MEM0_API_KEY="m0-quoted" # comment')).toBe("m0-quoted");
|
||||
expect(parseApiKeyLine("MEM0_API_KEY=m0-literal")).toBe("m0-literal");
|
||||
});
|
||||
|
||||
test("rejects unrelated, empty, and executable-looking values", () => {
|
||||
expect(parseApiKeyLine("OTHER_KEY=value")).toBeUndefined();
|
||||
expect(parseApiKeyLine("MEM0_API_KEY=")).toBeUndefined();
|
||||
expect(parseApiKeyLine("MEM0_API_KEY=$MEM0_API_KEY")).toBeUndefined();
|
||||
expect(parseApiKeyLine("MEM0_API_KEY=$(cat secret)")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveApiKey", () => {
|
||||
test("explicit environment value wins over profiles", () => {
|
||||
const dir = home();
|
||||
writeFileSync(join(dir, ".zshrc"), "MEM0_API_KEY=profile\n");
|
||||
expect(resolveApiKey({MEM0_API_KEY: " explicit "}, dir)).toBe("explicit");
|
||||
});
|
||||
|
||||
test("uses the first valid allowlisted profile", () => {
|
||||
const dir = home();
|
||||
writeFileSync(join(dir, ".zshrc"), "MEM0_API_KEY=$UNSET\n");
|
||||
writeFileSync(join(dir, ".bashrc"), "export MEM0_API_KEY='m0-from-bashrc'\n");
|
||||
writeFileSync(join(dir, ".profile"), "MEM0_API_KEY=late\n");
|
||||
expect(resolveApiKey({}, dir)).toBe("m0-from-bashrc");
|
||||
});
|
||||
|
||||
test("continues after an unreadable or absent earlier profile", () => {
|
||||
const dir = home();
|
||||
mkdirSync(join(dir, ".zshrc"));
|
||||
writeFileSync(join(dir, ".profile"), "MEM0_API_KEY=m0-later\n");
|
||||
expect(resolveApiKey({}, dir)).toBe("m0-later");
|
||||
});
|
||||
|
||||
test("ignores unsupported files and invalid assignments", () => {
|
||||
const dir = home();
|
||||
writeFileSync(join(dir, ".env"), "MEM0_API_KEY=unsupported\n");
|
||||
writeFileSync(join(dir, ".zshrc"), "MEM0_API_KEY= # empty\nMEM0_API_KEY=$(unsafe)\n");
|
||||
expect(resolveApiKey({}, dir)).toBe("");
|
||||
});
|
||||
|
||||
test("keeps the missing-key guard when no source yields a key", async () => {
|
||||
const dir = home();
|
||||
writeFileSync(join(dir, ".zshrc"), "MEM0_API_KEY= # empty\nMEM0_API_KEY=$UNSET\n");
|
||||
delete process.env.MEM0_API_KEY;
|
||||
process.env.HOME = dir;
|
||||
process.env.USERPROFILE = dir;
|
||||
process.env.MEM0_TELEMETRY = "false";
|
||||
stubFetch();
|
||||
testCleanups.push(captureDeferredPluginCleanup());
|
||||
|
||||
const logs: unknown[] = [];
|
||||
const plugin = await Mem0Plugin(pluginContext(logs));
|
||||
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0]).toMatchObject({
|
||||
body: {
|
||||
level: "error",
|
||||
message: "MEM0_API_KEY environment variable not set. Get one at https://app.mem0.ai/dashboard/api-keys",
|
||||
},
|
||||
});
|
||||
expect(plugin).toEqual({});
|
||||
});
|
||||
|
||||
test("recovers the issue's shell-profile startup path", async () => {
|
||||
const dir = home();
|
||||
// Problem 2 in issue #6003: Desktop has no process key, but .zshrc does.
|
||||
writeFileSync(join(dir, ".zshrc"), 'export MEM0_API_KEY="m0-from-profile"\n');
|
||||
delete process.env.MEM0_API_KEY;
|
||||
process.env.HOME = dir;
|
||||
process.env.USERPROFILE = dir;
|
||||
process.env.MEM0_TELEMETRY = "false";
|
||||
stubFetch();
|
||||
testCleanups.push(captureDeferredPluginCleanup());
|
||||
|
||||
const logs: unknown[] = [];
|
||||
const plugin = await Mem0Plugin(pluginContext(logs));
|
||||
|
||||
expect(logs).toHaveLength(0);
|
||||
expect(Object.keys(plugin)).toContain("chat.message");
|
||||
expect(plugin).toHaveProperty("tool");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import {readFileSync} from "fs";
|
||||
import {homedir} from "os";
|
||||
import {join} from "path";
|
||||
|
||||
const PROFILE_FILES = [".zshrc", ".bashrc", ".zprofile", ".bash_profile", ".profile"];
|
||||
|
||||
export function parseApiKeyLine(line: string): string | undefined {
|
||||
const match = line.match(/^\s*(?:export\s+)?MEM0_API_KEY=(.*)$/);
|
||||
if (!match) return undefined;
|
||||
|
||||
const value = match[1].replace(/#.*$/, "").trim().replace(/^("|')(.*)\1$/, "$2").trim();
|
||||
return value && !value.startsWith("$") ? value : undefined;
|
||||
}
|
||||
|
||||
export function resolveApiKey(env: NodeJS.ProcessEnv = process.env, homeDir = homedir()): string {
|
||||
const explicit = env.MEM0_API_KEY?.trim();
|
||||
if (explicit) return explicit;
|
||||
|
||||
for (const profile of PROFILE_FILES) {
|
||||
try {
|
||||
for (const line of readFileSync(join(homeDir, profile), "utf8").split(/\r?\n/)) {
|
||||
const key = parseApiKeyLine(line);
|
||||
if (key) return key;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
} from "./dream";
|
||||
import {asScope, scopeSearchFilters, scopeWriteParams, resolveDefaultScope, SCOPE_GUIDANCE, type Scope} from "./scope";
|
||||
import {parseProjectFromRemote} from "./project";
|
||||
import {resolveApiKey} from "./api-key";
|
||||
|
||||
async function getUserId(): Promise<string> {
|
||||
if (process.env.MEM0_USER_ID) return process.env.MEM0_USER_ID;
|
||||
@@ -258,7 +259,7 @@ function extractUserText(input: any, output: any): string {
|
||||
const Mem0Plugin: Plugin = async (ctx) => {
|
||||
const {$, client} = ctx;
|
||||
|
||||
const apiKey = process.env.MEM0_API_KEY;
|
||||
const apiKey = resolveApiKey();
|
||||
|
||||
if (!apiKey) {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/opencode-plugin",
|
||||
"version": "0.2.1",
|
||||
"version": "0.2.2",
|
||||
"type": "module",
|
||||
"description": "Mem0 persistent memory plugin for OpenCode — add, search, and manage memories across sessions",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -62,10 +62,6 @@ SECTION_MAP = {
|
||||
"/open-source/features/rest-api",
|
||||
"/open-source/configure-components",
|
||||
],
|
||||
"openmemory": [
|
||||
"/openmemory/overview",
|
||||
"/openmemory/quickstart",
|
||||
],
|
||||
"sdks": [
|
||||
"/sdks/python",
|
||||
"/sdks/js",
|
||||
|
||||
@@ -9,7 +9,10 @@ module.exports = {
|
||||
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' },
|
||||
rules: {
|
||||
'n8n-nodes-base/community-package-json-name-still-default': 'off',
|
||||
'n8n-nodes-base/community-package-json-license-not-default': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['./credentials/**/*.ts'],
|
||||
@@ -26,6 +29,14 @@ module.exports = {
|
||||
files: ['./nodes/**/*.ts'],
|
||||
plugins: ['eslint-plugin-n8n-nodes-base'],
|
||||
extends: ['plugin:n8n-nodes-base/nodes'],
|
||||
rules: {
|
||||
// Superseded by the verification scanner's `@n8n/community-nodes`
|
||||
// node-connection-type-literal rule, which requires
|
||||
// NodeConnectionTypes.Main instead of the 'main' string literal.
|
||||
// n8n's own scanner disables these two, so we match it.
|
||||
'n8n-nodes-base/node-class-description-inputs-wrong-regular-node': 'off',
|
||||
'n8n-nodes-base/node-class-description-outputs-wrong': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,21 +1,201 @@
|
||||
MIT License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Copyright (c) 2026 Mem0
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
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:
|
||||
1. Definitions.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
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.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [2026] [Taranjeet Singh]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# n8n-nodes-mem0
|
||||
# @mem0/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.
|
||||
|
||||
@@ -10,7 +10,7 @@ Mem0 gives your agents long-term memory: add memories from conversations, then s
|
||||
|
||||
## Installation
|
||||
|
||||
Follow the [community nodes installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) and install `n8n-nodes-mem0`.
|
||||
Follow the [community nodes installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) and install `@mem0/n8n-nodes-mem0`.
|
||||
|
||||
## Operations
|
||||
|
||||
@@ -20,14 +20,27 @@ The **Memory** resource supports:
|
||||
| --- | --- | --- |
|
||||
| **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 Many** | List stored memories (single page, or **Return All**) | `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.
|
||||
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.
|
||||
|
||||
Two independent controls:
|
||||
|
||||
- **Wait for Completion** (on by default) decides whether the node polls. Turn it off to return immediately with the event ID.
|
||||
- **Infer** (on by default, under Additional Fields) decides whether the API runs LLM extraction at all. Turn it off to store the messages verbatim.
|
||||
|
||||
**Custom Instructions**, **Custom Categories**, **Includes**, and **Excludes** (also under Additional Fields) steer what extraction keeps for that call. **Agent ID**, **App ID**, and **Run ID** live there too, and scope the memory alongside (or instead of) **User ID**.
|
||||
|
||||
### Entity filters on Search & Get Many
|
||||
|
||||
Both take **User ID**, **Agent ID**, **App ID**, and **Run ID**, and at least one is required — the API rejects a query with no entity scope, and the node fails with a clear message before calling it.
|
||||
|
||||
Supplying several combines them with **OR**, giving the union of those scopes. Mem0 indexes each entity separately, so an `AND` across `user_id` and `agent_id` matches nothing even for a memory written with both. To narrow instead of widen, run one operation per entity id.
|
||||
|
||||
## Credentials
|
||||
|
||||
@@ -39,11 +52,15 @@ This node is also **usable as a tool** by n8n's AI Agent node — attach it so a
|
||||
|
||||
A typical loop:
|
||||
|
||||
1. **Search** memory before answering, filtered by `User ID`.
|
||||
1. **Search** memory before answering, filtered by `User ID` (or `Agent ID` / `App ID` / `Run 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.
|
||||
|
||||
## Telemetry
|
||||
|
||||
This node sends no third-party telemetry. Its API requests are tagged with `source: "N8N"` so Mem0 can see aggregate usage of the integration. No separate analytics service is contacted and nothing else is collected.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Mem0 documentation](https://docs.mem0.ai)
|
||||
@@ -51,4 +68,4 @@ Memory writes are asynchronous by default; allow a moment after an Add before se
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
[Apache-2.0](./LICENSE)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
IAuthenticateGeneric,
|
||||
Icon,
|
||||
ICredentialTestRequest,
|
||||
ICredentialType,
|
||||
INodeProperties,
|
||||
@@ -10,6 +11,8 @@ export class Mem0Api implements ICredentialType {
|
||||
|
||||
displayName = 'Mem0 API';
|
||||
|
||||
icon: Icon = 'file:mem0.svg';
|
||||
|
||||
documentationUrl = 'https://docs.mem0.ai/platform/quickstart';
|
||||
|
||||
properties: INodeProperties[] = [
|
||||
|
||||
@@ -1,19 +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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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="#9C58FA"/>
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,10 @@
|
||||
/** Jest config lives here (not in package.json) so the published package.json
|
||||
* stays minimal for the n8n verification scanner. This file is dev-only; it is
|
||||
* not shipped (see the `files` field in package.json). */
|
||||
module.exports = {
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/test/**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': ['ts-jest', {}],
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"node": "n8n-nodes-mem0.mem0",
|
||||
"node": "@mem0/n8n-nodes-mem0.mem0",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["AI"],
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
INodeTypeDescription,
|
||||
JsonObject,
|
||||
NodeApiError,
|
||||
NodeConnectionTypes,
|
||||
NodeOperationError,
|
||||
sleep,
|
||||
} from 'n8n-workflow';
|
||||
@@ -30,8 +31,8 @@ export class Mem0 implements INodeType {
|
||||
},
|
||||
// Makes the node available to the AI Agent (Tools Agent) node.
|
||||
usableAsTool: true,
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
credentials: [
|
||||
{
|
||||
name: 'mem0Api',
|
||||
@@ -168,6 +169,37 @@ export class Mem0 implements INodeType {
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Categories',
|
||||
name: 'custom_categories',
|
||||
type: 'json',
|
||||
default: '',
|
||||
description:
|
||||
'Optional taxonomy for categorising extracted memories, as a JSON array of {category: description} objects',
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Instructions',
|
||||
name: 'custom_instructions',
|
||||
type: 'string',
|
||||
typeOptions: { rows: 3 },
|
||||
default: '',
|
||||
description:
|
||||
'Optional instructions that steer what the extractor keeps or ignores',
|
||||
},
|
||||
{
|
||||
displayName: 'Excludes',
|
||||
name: 'excludes',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Optional: skip memories matching this description',
|
||||
},
|
||||
{
|
||||
displayName: 'Includes',
|
||||
name: 'includes',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Optional: only extract memories matching this description',
|
||||
},
|
||||
{
|
||||
displayName: 'Infer',
|
||||
name: 'infer',
|
||||
@@ -207,9 +239,33 @@ export class Mem0 implements INodeType {
|
||||
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)',
|
||||
description:
|
||||
'Restrict the search to this user. Supply at least one of User ID, Agent ID, App ID, or Run ID.',
|
||||
},
|
||||
{
|
||||
displayName: 'Agent ID',
|
||||
name: 'agentId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: { show: { resource: ['memory'], operation: ['search'] } },
|
||||
description: 'Restrict the search to memories scoped to this agent',
|
||||
},
|
||||
{
|
||||
displayName: 'App ID',
|
||||
name: 'appId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: { show: { resource: ['memory'], operation: ['search'] } },
|
||||
description: 'Restrict the search to memories scoped to this app or project',
|
||||
},
|
||||
{
|
||||
displayName: 'Run ID',
|
||||
name: 'runId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: { show: { resource: ['memory'], operation: ['search'] } },
|
||||
description: 'Restrict the search to memories scoped to this session or run',
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
@@ -227,9 +283,41 @@ export class Mem0 implements INodeType {
|
||||
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)',
|
||||
description:
|
||||
'Restrict the listing to this user. Supply at least one of User ID, Agent ID, App ID, or Run ID.',
|
||||
},
|
||||
{
|
||||
displayName: 'Agent ID',
|
||||
name: 'agentId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: { show: { resource: ['memory'], operation: ['getAll'] } },
|
||||
description: 'Restrict the listing to memories scoped to this agent',
|
||||
},
|
||||
{
|
||||
displayName: 'App ID',
|
||||
name: 'appId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: { show: { resource: ['memory'], operation: ['getAll'] } },
|
||||
description: 'Restrict the listing to memories scoped to this app or project',
|
||||
},
|
||||
{
|
||||
displayName: 'Run ID',
|
||||
name: 'runId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
displayOptions: { show: { resource: ['memory'], operation: ['getAll'] } },
|
||||
description: 'Restrict the listing to memories scoped to this session or run',
|
||||
},
|
||||
{
|
||||
displayName: 'Return All',
|
||||
name: 'returnAll',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: { show: { resource: ['memory'], operation: ['getAll'] } },
|
||||
description: 'Whether to return all results or only up to a given limit',
|
||||
},
|
||||
{
|
||||
displayName: 'Page',
|
||||
@@ -237,7 +325,9 @@ export class Mem0 implements INodeType {
|
||||
type: 'number',
|
||||
typeOptions: { minValue: 1 },
|
||||
default: 1,
|
||||
displayOptions: { show: { resource: ['memory'], operation: ['getAll'] } },
|
||||
displayOptions: {
|
||||
show: { resource: ['memory'], operation: ['getAll'], returnAll: [false] },
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Page Size',
|
||||
@@ -280,7 +370,8 @@ export class Mem0 implements INodeType {
|
||||
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 credentials = await this.getCredentials('mem0Api');
|
||||
const baseUrl = (credentials.baseUrl as string) || 'https://api.mem0.ai';
|
||||
|
||||
const request = async (
|
||||
method: IHttpRequestMethods,
|
||||
@@ -293,7 +384,8 @@ export class Mem0 implements INodeType {
|
||||
url: `${baseUrl}${url}`,
|
||||
json: true,
|
||||
...(body ? { body } : {}),
|
||||
...(qs ? { qs } : {}),
|
||||
// First-party usage attribution: the backend reads `source` (same as OpenClaw).
|
||||
qs: { source: 'N8N', ...(qs ?? {}) },
|
||||
};
|
||||
return (await this.helpers.httpRequestWithAuthentication.call(
|
||||
this,
|
||||
@@ -337,6 +429,28 @@ export class Mem0 implements INodeType {
|
||||
}
|
||||
}
|
||||
|
||||
// Custom extraction controls (optional): steer what the API extracts.
|
||||
if (addFields.custom_instructions) {
|
||||
body.custom_instructions = addFields.custom_instructions;
|
||||
}
|
||||
if (addFields.custom_categories) {
|
||||
try {
|
||||
body.custom_categories =
|
||||
typeof addFields.custom_categories === 'string'
|
||||
? JSON.parse(addFields.custom_categories as string)
|
||||
: addFields.custom_categories;
|
||||
} catch {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
'Invalid JSON in "Custom Categories" field',
|
||||
{ itemIndex: i },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (addFields.includes) body.includes = addFields.includes;
|
||||
if (addFields.excludes) body.excludes = addFields.excludes;
|
||||
|
||||
// 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(
|
||||
@@ -372,21 +486,51 @@ export class Mem0 implements INodeType {
|
||||
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 };
|
||||
body.filters = buildEntityFilters(
|
||||
{
|
||||
user_id: this.getNodeParameter('userId', i, '') as string,
|
||||
agent_id: this.getNodeParameter('agentId', i, '') as string,
|
||||
app_id: this.getNodeParameter('appId', i, '') as string,
|
||||
run_id: this.getNodeParameter('runId', i, '') as string,
|
||||
},
|
||||
this,
|
||||
i,
|
||||
);
|
||||
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 returnAll = this.getNodeParameter('returnAll', i, false) as boolean;
|
||||
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[]) : [];
|
||||
const body: IDataObject = {
|
||||
filters: buildEntityFilters(
|
||||
{
|
||||
user_id: this.getNodeParameter('userId', i, '') as string,
|
||||
agent_id: this.getNodeParameter('agentId', i, '') as string,
|
||||
app_id: this.getNodeParameter('appId', i, '') as string,
|
||||
run_id: this.getNodeParameter('runId', i, '') as string,
|
||||
},
|
||||
this,
|
||||
i,
|
||||
),
|
||||
};
|
||||
if (returnAll) {
|
||||
// Page through until a short/empty page or no `next` (hard-capped for safety).
|
||||
const all: IDataObject[] = [];
|
||||
for (let page = 1; page <= 10000; page++) {
|
||||
const resp = await request('POST', '/v3/memories/', body, { page, page_size: pageSize });
|
||||
const results = Array.isArray(resp.results) ? (resp.results as IDataObject[]) : [];
|
||||
all.push(...results);
|
||||
if (results.length < pageSize || !resp.next) break;
|
||||
}
|
||||
responseData = all;
|
||||
} else {
|
||||
const page = this.getNodeParameter('page', i, 1) as number;
|
||||
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}/`);
|
||||
responseData = await request('GET', `/v1/memories/${encodeURIComponent(memoryId)}/`);
|
||||
} else if (operation === 'update') {
|
||||
const memoryId = this.getNodeParameter('memoryId', i) as string;
|
||||
const body: IDataObject = {};
|
||||
@@ -407,10 +551,10 @@ export class Mem0 implements INodeType {
|
||||
itemIndex: i,
|
||||
});
|
||||
}
|
||||
responseData = await request('PUT', `/v1/memories/${memoryId}/`, body);
|
||||
responseData = await request('PUT', `/v1/memories/${encodeURIComponent(memoryId)}/`, body);
|
||||
} else if (operation === 'delete') {
|
||||
const memoryId = this.getNodeParameter('memoryId', i) as string;
|
||||
responseData = await request('DELETE', `/v1/memories/${memoryId}/`);
|
||||
responseData = await request('DELETE', `/v1/memories/${encodeURIComponent(memoryId)}/`);
|
||||
}
|
||||
|
||||
const arr = Array.isArray(responseData) ? responseData : [responseData];
|
||||
@@ -422,8 +566,7 @@ export class Mem0 implements INodeType {
|
||||
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);
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject, { itemIndex: i });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,6 +574,26 @@ export class Mem0 implements INodeType {
|
||||
}
|
||||
}
|
||||
|
||||
function buildEntityFilters(
|
||||
ids: Record<string, string>,
|
||||
ctx: IExecuteFunctions,
|
||||
itemIndex: number,
|
||||
): IDataObject {
|
||||
const clauses: IDataObject[] = Object.entries(ids)
|
||||
.filter(([, value]) => value)
|
||||
.map(([key, value]) => ({ [key]: value }));
|
||||
|
||||
if (clauses.length === 0) {
|
||||
throw new NodeOperationError(
|
||||
ctx.getNode(),
|
||||
'Provide at least one of User ID, Agent ID, App ID, or Run ID',
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
return clauses.length === 1 ? clauses[0] : { OR: clauses };
|
||||
}
|
||||
|
||||
// Polls GET /v1/event/{id}/ until the memory-addition event resolves.
|
||||
async function pollEvent(
|
||||
request: (m: IHttpRequestMethods, u: string) => Promise<IDataObject>,
|
||||
@@ -439,7 +602,7 @@ async function pollEvent(
|
||||
itemIndex: number,
|
||||
): Promise<IDataObject | IDataObject[]> {
|
||||
for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
|
||||
const event = await request('GET', `/v1/event/${eventId}/`);
|
||||
const event = await request('GET', `/v1/event/${encodeURIComponent(eventId)}/`);
|
||||
const status = event.status as string;
|
||||
if (status === 'SUCCEEDED') {
|
||||
// Match the shape of search/getAll (a clean array); fall back to the envelope.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "n8n-nodes-mem0",
|
||||
"name": "@mem0/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": [
|
||||
@@ -10,7 +10,7 @@
|
||||
"agents",
|
||||
"llm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://mem0.ai",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
@@ -25,12 +25,16 @@
|
||||
"node": ">=20.15"
|
||||
},
|
||||
"main": "index.js",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"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",
|
||||
"test": "jest",
|
||||
"prepublishOnly": "npm run build && npm run lint"
|
||||
},
|
||||
"files": [
|
||||
@@ -46,16 +50,30 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"jest": "^29.7.0",
|
||||
"eslint-plugin-n8n-nodes-base": "^1.16.3",
|
||||
"gulp": "^5.0.0",
|
||||
"n8n-workflow": "*",
|
||||
"prettier": "^3.3.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"n8n-workflow": "*"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"form-data@<4.0.6": ">=4.0.6",
|
||||
"uuid@<11.1.1": ">=11.1.1 <12.0.0",
|
||||
"lodash@<=4.17.23": ">=4.18.0",
|
||||
"brace-expansion@<1.1.16": ">=1.1.16 <2.0.0",
|
||||
"brace-expansion@>=2.0.0 <2.1.2": ">=2.1.2 <3.0.0",
|
||||
"brace-expansion@>=5.0.0 <5.0.8": ">=5.0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
// Offline unit tests: they stub IExecuteFunctions and mock the HTTP layer, so
|
||||
// they run with no network. They cover the review-flagged paths: the entity-id
|
||||
// guard, JSON-parse errors, the poll-timeout loop, plus source attribution and
|
||||
// the Return All pagination.
|
||||
|
||||
// Make the poll loop instant (pollEvent sleeps between attempts).
|
||||
jest.mock('n8n-workflow', () => {
|
||||
const actual = jest.requireActual('n8n-workflow');
|
||||
return { ...actual, sleep: jest.fn().mockResolvedValue(undefined) };
|
||||
});
|
||||
|
||||
import { Mem0 } from '../nodes/Mem0/Mem0.node';
|
||||
|
||||
type HttpImpl = (options: any) => Promise<any>;
|
||||
|
||||
function makeCtx(
|
||||
operation: string,
|
||||
params: Record<string, any>,
|
||||
http: HttpImpl,
|
||||
opts: { continueOnFail?: boolean } = {},
|
||||
): any {
|
||||
const requests: any[] = [];
|
||||
const node = { name: 'Mem0', type: '@mem0/n8n-nodes-mem0.mem0', typeVersion: 1 };
|
||||
const ctx: any = {
|
||||
getInputData: () => [{ json: {} }],
|
||||
getCredentials: async () => ({ apiKey: 'm0-test', baseUrl: 'https://api.mem0.ai' }),
|
||||
getNodeParameter: (name: string, _i: number, dflt?: any) =>
|
||||
name === 'operation' ? operation : name in params ? params[name] : dflt,
|
||||
getNode: () => node,
|
||||
continueOnFail: () => opts.continueOnFail ?? false,
|
||||
helpers: {
|
||||
httpRequestWithAuthentication: jest.fn(async function (_cred: string, options: any) {
|
||||
requests.push(options);
|
||||
return http(options);
|
||||
}),
|
||||
},
|
||||
};
|
||||
ctx.requests = requests;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
const run = (ctx: any) => Mem0.prototype.execute.call(ctx);
|
||||
|
||||
describe('Mem0 node (offline)', () => {
|
||||
it('reports a clear error when Add has no entity id', async () => {
|
||||
const ctx = makeCtx(
|
||||
'add',
|
||||
{ 'messages.message': [{ role: 'user', content: 'hi' }], addFields: {}, userId: '' },
|
||||
async () => ({}),
|
||||
{ continueOnFail: true },
|
||||
);
|
||||
const out: any = await run(ctx);
|
||||
expect(out[0][0].json.error).toMatch(/at least one of User ID/i);
|
||||
});
|
||||
|
||||
it('forwards app id, includes and excludes on Add', async () => {
|
||||
const ctx = makeCtx(
|
||||
'add',
|
||||
{
|
||||
'messages.message': [{ role: 'user', content: 'hi' }],
|
||||
addFields: {
|
||||
app_id: 'p1',
|
||||
includes: 'only food preferences',
|
||||
excludes: 'nothing about vehicles',
|
||||
},
|
||||
userId: 'u1',
|
||||
},
|
||||
async () => ({}),
|
||||
);
|
||||
await run(ctx);
|
||||
expect(ctx.requests[0].body).toMatchObject({
|
||||
app_id: 'p1',
|
||||
includes: 'only food preferences',
|
||||
excludes: 'nothing about vehicles',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts an app id alone as the entity scope on Add', async () => {
|
||||
const ctx = makeCtx(
|
||||
'add',
|
||||
{
|
||||
'messages.message': [{ role: 'user', content: 'hi' }],
|
||||
addFields: { app_id: 'p1' },
|
||||
userId: '',
|
||||
},
|
||||
async () => ({}),
|
||||
{ continueOnFail: true },
|
||||
);
|
||||
const out: any = await run(ctx);
|
||||
expect(out[0][0].json.error).toBeUndefined();
|
||||
expect(ctx.requests[0].body.app_id).toBe('p1');
|
||||
});
|
||||
|
||||
it('reports a clear error on invalid JSON in Custom Categories', async () => {
|
||||
const ctx = makeCtx(
|
||||
'add',
|
||||
{
|
||||
'messages.message': [{ role: 'user', content: 'hi' }],
|
||||
addFields: { custom_categories: '{bad' },
|
||||
userId: 'u1',
|
||||
},
|
||||
async () => ({}),
|
||||
{ continueOnFail: true },
|
||||
);
|
||||
const out: any = await run(ctx);
|
||||
expect(out[0][0].json.error).toMatch(/Invalid JSON/i);
|
||||
});
|
||||
|
||||
it('times out when the add event never resolves', async () => {
|
||||
const ctx = makeCtx(
|
||||
'add',
|
||||
{
|
||||
'messages.message': [{ role: 'user', content: 'hi' }],
|
||||
addFields: {},
|
||||
userId: 'u1',
|
||||
waitForCompletion: true,
|
||||
},
|
||||
async (options) => {
|
||||
if (options.url.includes('/v3/memories/add/')) return { event_id: 'e1', status: 'PENDING' };
|
||||
if (options.url.includes('/v1/event/')) return { status: 'PENDING' }; // never terminal
|
||||
return {};
|
||||
},
|
||||
{ continueOnFail: true },
|
||||
);
|
||||
const out: any = await run(ctx);
|
||||
expect(out[0][0].json.error).toMatch(/Timed out waiting for memory event/i);
|
||||
});
|
||||
|
||||
it('tags every request with source=N8N for first-party attribution', async () => {
|
||||
const ctx = makeCtx('search', { query: 'x', userId: 'u1', limit: 5 }, async () => ({ results: [] }));
|
||||
await run(ctx);
|
||||
expect(ctx.requests[0].qs.source).toBe('N8N');
|
||||
});
|
||||
|
||||
it('sends a single entity id as a flat filter', async () => {
|
||||
const ctx = makeCtx('search', { query: 'x', userId: 'u1' }, async () => ({ results: [] }));
|
||||
await run(ctx);
|
||||
expect(ctx.requests[0].body.filters).toEqual({ user_id: 'u1' });
|
||||
});
|
||||
|
||||
it('combines entity ids with OR, never AND (entities are stored separately, so AND matches nothing)', async () => {
|
||||
const ctx = makeCtx(
|
||||
'search',
|
||||
{ query: 'x', userId: 'u1', agentId: 'a1', appId: 'p1', runId: 'r1' },
|
||||
async () => ({ results: [] }),
|
||||
);
|
||||
await run(ctx);
|
||||
expect(ctx.requests[0].body.filters).toEqual({
|
||||
OR: [{ user_id: 'u1' }, { agent_id: 'a1' }, { app_id: 'p1' }, { run_id: 'r1' }],
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['search', 'getAll'])('filters %s by app id alone', async (op) => {
|
||||
const ctx = makeCtx(op, { query: 'x', appId: 'p1' }, async () => ({ results: [] }));
|
||||
await run(ctx);
|
||||
expect(ctx.requests[0].body.filters).toEqual({ app_id: 'p1' });
|
||||
});
|
||||
|
||||
it('filters Get Many by agent id alone', async () => {
|
||||
const ctx = makeCtx('getAll', { agentId: 'a1' }, async () => ({ results: [] }));
|
||||
await run(ctx);
|
||||
expect(ctx.requests[0].body.filters).toEqual({ agent_id: 'a1' });
|
||||
});
|
||||
|
||||
it.each(['search', 'getAll'])('reports a clear error when %s has no entity id', async (op) => {
|
||||
const ctx = makeCtx(op, { query: 'x' }, async () => ({ results: [] }), {
|
||||
continueOnFail: true,
|
||||
});
|
||||
const out: any = await run(ctx);
|
||||
expect(out[0][0].json.error).toMatch(/at least one of User ID/i);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['get', 'GET'],
|
||||
['delete', 'DELETE'],
|
||||
])('escapes the memory id in the %s url', async (op, method) => {
|
||||
const ctx = makeCtx(op, { memoryId: '../v1/entities' }, async () => ({}));
|
||||
await run(ctx);
|
||||
expect(ctx.requests[0].method).toBe(method);
|
||||
expect(ctx.requests[0].url).toBe('https://api.mem0.ai/v1/memories/..%2Fv1%2Fentities/');
|
||||
});
|
||||
|
||||
it('escapes the event id when polling', async () => {
|
||||
const ctx = makeCtx(
|
||||
'add',
|
||||
{
|
||||
'messages.message': [{ role: 'user', content: 'hi' }],
|
||||
addFields: {},
|
||||
userId: 'u1',
|
||||
waitForCompletion: true,
|
||||
},
|
||||
async (options) => {
|
||||
if (options.url.includes('/v3/memories/add/')) return { event_id: 'a b/c' };
|
||||
return { status: 'SUCCEEDED', results: [] };
|
||||
},
|
||||
);
|
||||
await run(ctx);
|
||||
expect(ctx.requests[1].url).toBe('https://api.mem0.ai/v1/event/a%20b%2Fc/');
|
||||
});
|
||||
|
||||
it('Return All pages through until a short page', async () => {
|
||||
let call = 0;
|
||||
const ctx = makeCtx('getAll', { userId: 'u1', returnAll: true, pageSize: 2 }, async () => {
|
||||
call++;
|
||||
if (call === 1) return { results: [{ id: 'a' }, { id: 'b' }], next: 'page2' };
|
||||
return { results: [{ id: 'c' }], next: null }; // short page -> stop
|
||||
});
|
||||
const out: any = await run(ctx);
|
||||
expect(out[0].map((d: any) => d.json.id)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
});
|
||||
@@ -14,9 +14,10 @@
|
||||
"strictNullChecks": true,
|
||||
"preserveConstEnums": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"resolveJsonModule": true,
|
||||
"incremental": true,
|
||||
"declaration": true,
|
||||
"incremental": false,
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "./dist/"
|
||||
|
||||
@@ -3,6 +3,7 @@ package-lock.json
|
||||
.env
|
||||
.zapierapprc
|
||||
build/
|
||||
dist/
|
||||
coverage/
|
||||
*.log
|
||||
.DS_Store
|
||||
|
||||
@@ -1,21 +1,201 @@
|
||||
MIT License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Copyright (c) 2026 Mem0
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
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:
|
||||
1. Definitions.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
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.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [2023] [Taranjeet Singh]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -23,12 +23,18 @@ Custom (API key) auth. Provide a Mem0 API key from [app.mem0.ai](https://app.mem
|
||||
|
||||
## Development
|
||||
|
||||
Written in TypeScript; the app compiles to `dist/` (Zapier runs the compiled JS).
|
||||
|
||||
```bash
|
||||
npm install
|
||||
MEM0_API_KEY=m0-... npm test # runs the E2E suite against api.mem0.ai
|
||||
pnpm install
|
||||
pnpm build # compile src/ → dist/
|
||||
pnpm test:unit # offline unit tests (mocked, no network)
|
||||
MEM0_API_KEY=m0-... pnpm test # unit + live E2E against api.mem0.ai
|
||||
```
|
||||
|
||||
To deploy (maintainers): `zapier login && zapier push`.
|
||||
Anonymous usage telemetry is sent to Mem0; opt out with `MEM0_TELEMETRY=false`.
|
||||
|
||||
To deploy (maintainers): `pnpm build && zapier push`.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
'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: {},
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "zapier-mem0",
|
||||
"name": "@mem0/zapier",
|
||||
"version": "0.1.0",
|
||||
"description": "Zapier integration for Mem0 — the memory layer for AI agents.",
|
||||
"keywords": [
|
||||
@@ -19,21 +19,51 @@
|
||||
"url": "https://github.com/mem0ai/mem0",
|
||||
"directory": "integrations/zapier-mem0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"test": "jest --testTimeout 120000",
|
||||
"test:unit": "jest test/unit.test.js"
|
||||
"build": "tsc",
|
||||
"test": "jest --testTimeout 180000",
|
||||
"test:unit": "jest test/unit.test.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"npm": ">=5.6.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"testMatch": ["**/test/**/*.test.ts"],
|
||||
"setupFiles": ["<rootDir>/test/setup.ts"],
|
||||
"transform": {
|
||||
"^.+\\.tsx?$": ["ts-jest", {}]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"zapier-platform-core": "19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jest": "^29.7.0"
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.9.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.6.3"
|
||||
},
|
||||
"private": true
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"form-data@<4.0.6": ">=4.0.6",
|
||||
"uuid@<11.1.1": ">=11.1.1",
|
||||
"esbuild": ">=0.28.1",
|
||||
"undici@<6.27.0": ">=6.27.0 <8.0.0",
|
||||
"undici@>=8.0.0 <8.5.0": ">=8.5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,13 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
form-data@<4.0.6: '>=4.0.6'
|
||||
uuid@<11.1.1: '>=11.1.1'
|
||||
esbuild: '>=0.28.1'
|
||||
undici@<6.27.0: '>=6.27.0 <8.0.0'
|
||||
undici@>=8.0.0 <8.5.0: '>=8.5.0'
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
@@ -12,9 +19,21 @@ importers:
|
||||
specifier: 19.0.0
|
||||
version: 19.0.0
|
||||
devDependencies:
|
||||
'@types/jest':
|
||||
specifier: ^29.5.14
|
||||
version: 29.5.14
|
||||
'@types/node':
|
||||
specifier: ^22.9.0
|
||||
version: 22.20.1
|
||||
jest:
|
||||
specifier: ^29.7.0
|
||||
version: 29.7.0(@types/node@26.1.0)
|
||||
version: 29.7.0(@types/node@22.20.1)
|
||||
ts-jest:
|
||||
specifier: ^29.2.5
|
||||
version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.1))(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: ^5.6.3
|
||||
version: 5.9.3
|
||||
|
||||
packages:
|
||||
|
||||
@@ -310,11 +329,14 @@ packages:
|
||||
'@types/istanbul-reports@3.0.4':
|
||||
resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
|
||||
|
||||
'@types/jest@29.5.14':
|
||||
resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==}
|
||||
|
||||
'@types/node@20.19.43':
|
||||
resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==}
|
||||
|
||||
'@types/node@26.1.0':
|
||||
resolution: {integrity: sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==}
|
||||
'@types/node@22.20.1':
|
||||
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
|
||||
|
||||
'@types/stack-utils@2.0.3':
|
||||
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
|
||||
@@ -403,6 +425,10 @@ packages:
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
bs-logger@0.2.6:
|
||||
resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
bser@2.1.1:
|
||||
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
|
||||
|
||||
@@ -625,8 +651,8 @@ packages:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
form-data@4.0.5:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
form-data@4.0.6:
|
||||
resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
fs.realpath@1.0.0:
|
||||
@@ -675,6 +701,11 @@ packages:
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
handlebars@4.7.9:
|
||||
resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==}
|
||||
engines: {node: '>=0.4.7'}
|
||||
hasBin: true
|
||||
|
||||
has-flag@4.0.0:
|
||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -962,6 +993,9 @@ packages:
|
||||
lodash.isplainobject@4.0.6:
|
||||
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||
|
||||
lodash.memoize@4.1.2:
|
||||
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
|
||||
|
||||
lodash@4.18.1:
|
||||
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||
|
||||
@@ -972,6 +1006,9 @@ packages:
|
||||
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
make-error@1.3.6:
|
||||
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
|
||||
|
||||
makeerror@1.0.12:
|
||||
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
|
||||
|
||||
@@ -1012,12 +1049,18 @@ packages:
|
||||
minimatch@3.1.5:
|
||||
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
neo-async@2.6.2:
|
||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||
|
||||
node-abort-controller@3.1.1:
|
||||
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
|
||||
|
||||
@@ -1275,6 +1318,33 @@ packages:
|
||||
ts-algebra@2.0.0:
|
||||
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
|
||||
|
||||
ts-jest@29.4.12:
|
||||
resolution: {integrity: sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@babel/core': '>=7.0.0-beta.0 <8'
|
||||
'@jest/transform': ^29.0.0 || ^30.0.0
|
||||
'@jest/types': ^29.0.0 || ^30.0.0
|
||||
babel-jest: ^29.0.0 || ^30.0.0
|
||||
esbuild: '*'
|
||||
jest: ^29.0.0 || ^30.0.0
|
||||
jest-util: ^29.0.0 || ^30.0.0
|
||||
typescript: '>=4.3 <7'
|
||||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
'@jest/transform':
|
||||
optional: true
|
||||
'@jest/types':
|
||||
optional: true
|
||||
babel-jest:
|
||||
optional: true
|
||||
esbuild:
|
||||
optional: true
|
||||
jest-util:
|
||||
optional: true
|
||||
|
||||
type-detect@4.0.8:
|
||||
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -1283,16 +1353,27 @@ packages:
|
||||
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
type-fest@4.41.0:
|
||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
uglify-js@3.19.3:
|
||||
resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==}
|
||||
engines: {node: '>=0.8.0'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
undici-types@8.3.0:
|
||||
resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==}
|
||||
|
||||
update-browserslist-db@1.2.3:
|
||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||
hasBin: true
|
||||
@@ -1327,6 +1408,9 @@ packages:
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
wordwrap@1.0.0:
|
||||
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1575,7 +1659,7 @@ snapshots:
|
||||
'@jest/console@29.7.0':
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
chalk: 4.1.2
|
||||
jest-message-util: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
@@ -1588,14 +1672,14 @@ snapshots:
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-changed-files: 29.7.0
|
||||
jest-config: 29.7.0(@types/node@26.1.0)
|
||||
jest-config: 29.7.0(@types/node@22.20.1)
|
||||
jest-haste-map: 29.7.0
|
||||
jest-message-util: 29.7.0
|
||||
jest-regex-util: 29.6.3
|
||||
@@ -1620,7 +1704,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jest/fake-timers': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
jest-mock: 29.7.0
|
||||
|
||||
'@jest/expect-utils@29.7.0':
|
||||
@@ -1638,7 +1722,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@sinonjs/fake-timers': 10.3.0
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
jest-message-util: 29.7.0
|
||||
jest-mock: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
@@ -1660,7 +1744,7 @@ snapshots:
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
chalk: 4.1.2
|
||||
collect-v8-coverage: 1.0.3
|
||||
exit: 0.1.2
|
||||
@@ -1730,7 +1814,7 @@ snapshots:
|
||||
'@jest/schemas': 29.6.3
|
||||
'@types/istanbul-lib-coverage': 2.0.6
|
||||
'@types/istanbul-reports': 3.0.4
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
'@types/yargs': 17.0.35
|
||||
chalk: 4.1.2
|
||||
|
||||
@@ -1786,7 +1870,7 @@ snapshots:
|
||||
|
||||
'@types/graceful-fs@4.1.9':
|
||||
dependencies:
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
|
||||
'@types/istanbul-lib-coverage@2.0.6': {}
|
||||
|
||||
@@ -1798,14 +1882,19 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/istanbul-lib-report': 3.0.3
|
||||
|
||||
'@types/jest@29.5.14':
|
||||
dependencies:
|
||||
expect: 29.7.0
|
||||
pretty-format: 29.7.0
|
||||
|
||||
'@types/node@20.19.43':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
optional: true
|
||||
|
||||
'@types/node@26.1.0':
|
||||
'@types/node@22.20.1':
|
||||
dependencies:
|
||||
undici-types: 8.3.0
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/stack-utils@2.0.3': {}
|
||||
|
||||
@@ -1923,6 +2012,10 @@ snapshots:
|
||||
node-releases: 2.0.50
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.4)
|
||||
|
||||
bs-logger@0.2.6:
|
||||
dependencies:
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
|
||||
bser@2.1.1:
|
||||
dependencies:
|
||||
node-int64: 0.4.0
|
||||
@@ -2009,13 +2102,13 @@ snapshots:
|
||||
ripemd160: 2.0.3
|
||||
sha.js: 2.4.12
|
||||
|
||||
create-jest@29.7.0(@types/node@26.1.0):
|
||||
create-jest@29.7.0(@types/node@22.20.1):
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
exit: 0.1.2
|
||||
graceful-fs: 4.2.11
|
||||
jest-config: 29.7.0(@types/node@26.1.0)
|
||||
jest-config: 29.7.0(@types/node@22.20.1)
|
||||
jest-util: 29.7.0
|
||||
prompts: 2.4.2
|
||||
transitivePeerDependencies:
|
||||
@@ -2137,7 +2230,7 @@ snapshots:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
form-data@4.0.5:
|
||||
form-data@4.0.6:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
@@ -2191,6 +2284,15 @@ snapshots:
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
handlebars@4.7.9:
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
neo-async: 2.6.2
|
||||
source-map: 0.6.1
|
||||
wordwrap: 1.0.0
|
||||
optionalDependencies:
|
||||
uglify-js: 3.19.3
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
||||
has-property-descriptors@1.0.2:
|
||||
@@ -2311,7 +2413,7 @@ snapshots:
|
||||
'@jest/expect': 29.7.0
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
chalk: 4.1.2
|
||||
co: 4.6.0
|
||||
dedent: 1.7.2
|
||||
@@ -2331,16 +2433,16 @@ snapshots:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
|
||||
jest-cli@29.7.0(@types/node@26.1.0):
|
||||
jest-cli@29.7.0(@types/node@22.20.1):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
chalk: 4.1.2
|
||||
create-jest: 29.7.0(@types/node@26.1.0)
|
||||
create-jest: 29.7.0(@types/node@22.20.1)
|
||||
exit: 0.1.2
|
||||
import-local: 3.2.0
|
||||
jest-config: 29.7.0(@types/node@26.1.0)
|
||||
jest-config: 29.7.0(@types/node@22.20.1)
|
||||
jest-util: 29.7.0
|
||||
jest-validate: 29.7.0
|
||||
yargs: 17.7.3
|
||||
@@ -2350,7 +2452,7 @@ snapshots:
|
||||
- supports-color
|
||||
- ts-node
|
||||
|
||||
jest-config@29.7.0(@types/node@26.1.0):
|
||||
jest-config@29.7.0(@types/node@22.20.1):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@jest/test-sequencer': 29.7.0
|
||||
@@ -2375,7 +2477,7 @@ snapshots:
|
||||
slash: 3.0.0
|
||||
strip-json-comments: 3.1.1
|
||||
optionalDependencies:
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
transitivePeerDependencies:
|
||||
- babel-plugin-macros
|
||||
- supports-color
|
||||
@@ -2404,7 +2506,7 @@ snapshots:
|
||||
'@jest/environment': 29.7.0
|
||||
'@jest/fake-timers': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
jest-mock: 29.7.0
|
||||
jest-util: 29.7.0
|
||||
|
||||
@@ -2414,7 +2516,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/graceful-fs': 4.1.9
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
anymatch: 3.1.3
|
||||
fb-watchman: 2.0.2
|
||||
graceful-fs: 4.2.11
|
||||
@@ -2453,7 +2555,7 @@ snapshots:
|
||||
jest-mock@29.7.0:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
jest-util: 29.7.0
|
||||
|
||||
jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
|
||||
@@ -2488,7 +2590,7 @@ snapshots:
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
chalk: 4.1.2
|
||||
emittery: 0.13.1
|
||||
graceful-fs: 4.2.11
|
||||
@@ -2516,7 +2618,7 @@ snapshots:
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
chalk: 4.1.2
|
||||
cjs-module-lexer: 1.4.3
|
||||
collect-v8-coverage: 1.0.3
|
||||
@@ -2562,7 +2664,7 @@ snapshots:
|
||||
jest-util@29.7.0:
|
||||
dependencies:
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
chalk: 4.1.2
|
||||
ci-info: 3.9.0
|
||||
graceful-fs: 4.2.11
|
||||
@@ -2581,7 +2683,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jest/test-result': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
ansi-escapes: 4.3.2
|
||||
chalk: 4.1.2
|
||||
emittery: 0.13.1
|
||||
@@ -2590,17 +2692,17 @@ snapshots:
|
||||
|
||||
jest-worker@29.7.0:
|
||||
dependencies:
|
||||
'@types/node': 26.1.0
|
||||
'@types/node': 22.20.1
|
||||
jest-util: 29.7.0
|
||||
merge-stream: 2.0.0
|
||||
supports-color: 8.1.1
|
||||
|
||||
jest@29.7.0(@types/node@26.1.0):
|
||||
jest@29.7.0(@types/node@22.20.1):
|
||||
dependencies:
|
||||
'@jest/core': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
import-local: 3.2.0
|
||||
jest-cli: 29.7.0(@types/node@26.1.0)
|
||||
jest-cli: 29.7.0(@types/node@22.20.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- babel-plugin-macros
|
||||
@@ -2641,6 +2743,8 @@ snapshots:
|
||||
|
||||
lodash.isplainobject@4.0.6: {}
|
||||
|
||||
lodash.memoize@4.1.2: {}
|
||||
|
||||
lodash@4.18.1: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
@@ -2651,6 +2755,8 @@ snapshots:
|
||||
dependencies:
|
||||
semver: 7.8.5
|
||||
|
||||
make-error@1.3.6: {}
|
||||
|
||||
makeerror@1.0.12:
|
||||
dependencies:
|
||||
tmpl: 1.0.5
|
||||
@@ -2688,10 +2794,14 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.15
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
node-abort-controller@3.1.1: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
@@ -2915,20 +3025,44 @@ snapshots:
|
||||
|
||||
ts-algebra@2.0.0: {}
|
||||
|
||||
ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.20.1))(typescript@5.9.3):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
handlebars: 4.7.9
|
||||
jest: 29.7.0(@types/node@22.20.1)
|
||||
json5: 2.2.3
|
||||
lodash.memoize: 4.1.2
|
||||
make-error: 1.3.6
|
||||
semver: 7.8.5
|
||||
type-fest: 4.41.0
|
||||
typescript: 5.9.3
|
||||
yargs-parser: 21.1.1
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.29.7)
|
||||
jest-util: 29.7.0
|
||||
|
||||
type-detect@4.0.8: {}
|
||||
|
||||
type-fest@0.21.3: {}
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
typed-array-buffer@1.0.3:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
es-errors: 1.3.0
|
||||
is-typed-array: 1.1.15
|
||||
|
||||
undici-types@6.21.0:
|
||||
typescript@5.9.3: {}
|
||||
|
||||
uglify-js@3.19.3:
|
||||
optional: true
|
||||
|
||||
undici-types@8.3.0: {}
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.4):
|
||||
dependencies:
|
||||
@@ -2971,6 +3105,8 @@ snapshots:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
wordwrap@1.0.0: {}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -3008,7 +3144,7 @@ snapshots:
|
||||
content-disposition: 0.5.4
|
||||
dotenv: 17.2.1
|
||||
fernet: 0.3.3
|
||||
form-data: 4.0.5
|
||||
form-data: 4.0.6
|
||||
json-schema-to-ts: 3.1.1
|
||||
lodash: 4.18.1
|
||||
mime-types: 3.0.1
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
packages:
|
||||
- '.'
|
||||
|
||||
overrides:
|
||||
"form-data@<4.0.6": ">=4.0.6"
|
||||
"uuid@<11.1.1": ">=11.1.1"
|
||||
"esbuild": ">=0.28.1"
|
||||
"undici@<6.27.0": ">=6.27.0 <8.0.0"
|
||||
"undici@>=8.0.0 <8.5.0": ">=8.5.0"
|
||||
@@ -1,18 +1,18 @@
|
||||
'use strict';
|
||||
import type { ZObject, Bundle } from './types';
|
||||
|
||||
// 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' });
|
||||
const test = (z: ZObject, _bundle: Bundle) => z.request({ url: '/v1/ping/', method: 'GET' });
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
type: 'custom',
|
||||
test,
|
||||
fields: [
|
||||
{
|
||||
key: 'apiKey',
|
||||
label: 'Mem0 API Key',
|
||||
type: 'string',
|
||||
// `password` so Zapier masks the key in the connection UI (it is a secret).
|
||||
type: 'password',
|
||||
required: true,
|
||||
helpText:
|
||||
'Your Mem0 API key (starts with `m0-`). Create one at [app.mem0.ai](https://app.mem0.ai) → Settings → API Keys.',
|
||||
@@ -1,19 +1,21 @@
|
||||
'use strict';
|
||||
import type { ZObject, Bundle, AddResponse, EventResponse } from '../types';
|
||||
import { captureEvent } from '../telemetry';
|
||||
|
||||
const POLL_INTERVAL_MS = 1500;
|
||||
// Bounded so the poll budget stays under Zapier's per-step execution timeout.
|
||||
const MAX_POLL_ATTEMPTS = 12;
|
||||
// Bounded to a 60s poll budget, under Zapier's per-step execution timeout.
|
||||
const MAX_POLL_ATTEMPTS = 40;
|
||||
|
||||
// Polls GET /v1/event/{id}/ until the async memory-addition event resolves.
|
||||
const pollEvent = async (z, eventId) => {
|
||||
const pollEvent = async (z: ZObject, eventId: string): Promise<EventResponse> => {
|
||||
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;
|
||||
const data = res.data as EventResponse;
|
||||
const status = data && data.status;
|
||||
if (status === 'SUCCEEDED') {
|
||||
return res.data;
|
||||
return data;
|
||||
}
|
||||
if (status === 'FAILED') {
|
||||
const reason = (res.data && (res.data.error || res.data.message)) || 'unknown error';
|
||||
const reason = (data && (data.error || 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));
|
||||
@@ -26,14 +28,14 @@ const pollEvent = async (z, eventId) => {
|
||||
);
|
||||
};
|
||||
|
||||
const perform = async (z, bundle) => {
|
||||
const perform = async (z: ZObject, bundle: Bundle): Promise<AddResponse | EventResponse> => {
|
||||
// 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 = {
|
||||
const body: Record<string, unknown> = {
|
||||
messages: [{ role: bundle.inputData.role || 'user', content: bundle.inputData.content }],
|
||||
infer,
|
||||
};
|
||||
@@ -46,10 +48,28 @@ const perform = async (z, bundle) => {
|
||||
typeof bundle.inputData.metadata === 'string'
|
||||
? JSON.parse(bundle.inputData.metadata)
|
||||
: bundle.inputData.metadata;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
throw new z.errors.Error('Metadata must be valid JSON.', 'InvalidInput', 400);
|
||||
}
|
||||
}
|
||||
// Custom extraction controls (optional): steer what the API extracts.
|
||||
if (bundle.inputData.custom_instructions) {
|
||||
body.custom_instructions = bundle.inputData.custom_instructions;
|
||||
}
|
||||
if (bundle.inputData.custom_categories) {
|
||||
try {
|
||||
body.custom_categories =
|
||||
typeof bundle.inputData.custom_categories === 'string'
|
||||
? JSON.parse(bundle.inputData.custom_categories)
|
||||
: bundle.inputData.custom_categories;
|
||||
} catch {
|
||||
throw new z.errors.Error('Custom Categories must be valid JSON.', 'InvalidInput', 400);
|
||||
}
|
||||
}
|
||||
if (bundle.inputData.includes) body.includes = bundle.inputData.includes;
|
||||
if (bundle.inputData.excludes) body.excludes = bundle.inputData.excludes;
|
||||
|
||||
captureEvent('zapier.add_memory', bundle.authData?.apiKey, { infer, wait });
|
||||
|
||||
const response = await z.request({
|
||||
url: '/v3/memories/add/',
|
||||
@@ -57,7 +77,9 @@ const perform = async (z, bundle) => {
|
||||
body,
|
||||
});
|
||||
|
||||
const data = response.data;
|
||||
// Default to an empty object so an empty/no-content 2xx body can't crash the
|
||||
// `.status` / `.event_id` reads below.
|
||||
const data = (response.data ?? {}) as AddResponse;
|
||||
|
||||
// Add returns {event_id, status:PENDING|RUNNING}; poll only when opted in.
|
||||
if (wait && data.event_id && data.status !== 'SUCCEEDED' && data.status !== 'FAILED') {
|
||||
@@ -70,7 +92,7 @@ const perform = async (z, bundle) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
key: 'add_memory',
|
||||
noun: 'Memory',
|
||||
display: {
|
||||
@@ -97,6 +119,31 @@ module.exports = {
|
||||
{ key: 'agent_id', label: 'Agent ID', type: 'string' },
|
||||
{ key: 'run_id', label: 'Run ID', type: 'string' },
|
||||
{ key: 'metadata', label: 'Metadata (JSON)', type: 'string' },
|
||||
{
|
||||
key: 'custom_instructions',
|
||||
label: 'Custom Instructions',
|
||||
type: 'text',
|
||||
helpText: 'Optional instructions that steer what the extractor keeps or ignores.',
|
||||
},
|
||||
{
|
||||
key: 'custom_categories',
|
||||
label: 'Custom Categories (JSON)',
|
||||
type: 'string',
|
||||
helpText:
|
||||
'Optional taxonomy for categorising memories, as a JSON array of {category: description} objects.',
|
||||
},
|
||||
{
|
||||
key: 'includes',
|
||||
label: 'Includes',
|
||||
type: 'text',
|
||||
helpText: 'Optional: only extract memories matching this description.',
|
||||
},
|
||||
{
|
||||
key: 'excludes',
|
||||
label: 'Excludes',
|
||||
type: 'text',
|
||||
helpText: 'Optional: skip memories matching this description.',
|
||||
},
|
||||
{
|
||||
key: 'infer',
|
||||
label: 'Infer',
|
||||
@@ -115,6 +162,7 @@ module.exports = {
|
||||
'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: [] },
|
||||
// Default (no-wait) returns the accepted event; the wait path returns the resolved event.
|
||||
sample: { event_id: '00000000-0000-0000-0000-000000000000', status: 'PENDING' },
|
||||
},
|
||||
};
|
||||
@@ -1,15 +1,15 @@
|
||||
'use strict';
|
||||
import type { ZObject, Bundle } from '../types';
|
||||
|
||||
const perform = async (z, bundle) => {
|
||||
const perform = async (z: ZObject, bundle: 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)}/`,
|
||||
url: `/v1/memories/${encodeURIComponent(String(bundle.inputData.memory_id))}/`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
return response.data || { message: 'Deleted', memory_id: bundle.inputData.memory_id };
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
key: 'delete_memory',
|
||||
noun: 'Memory',
|
||||
display: {
|
||||
@@ -18,9 +18,7 @@ module.exports = {
|
||||
},
|
||||
operation: {
|
||||
perform,
|
||||
inputFields: [
|
||||
{ key: 'memory_id', label: 'Memory ID', type: 'string', required: true },
|
||||
],
|
||||
inputFields: [{ key: 'memory_id', label: 'Memory ID', type: 'string', required: true }],
|
||||
sample: { message: 'Memory deleted successfully' },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
import { version as platformVersion } from 'zapier-platform-core';
|
||||
import authentication from './authentication';
|
||||
import { includeApiKey, handleBadResponses } from './middleware';
|
||||
import addMemory from './creates/add_memory';
|
||||
import deleteMemory from './creates/delete_memory';
|
||||
import searchMemories from './searches/search_memories';
|
||||
import getMemories from './searches/get_memories';
|
||||
import pkg from '../package.json';
|
||||
|
||||
const app = {
|
||||
version: pkg.version,
|
||||
platformVersion,
|
||||
|
||||
authentication,
|
||||
|
||||
beforeRequest: [includeApiKey],
|
||||
afterResponse: [handleBadResponses],
|
||||
|
||||
creates: {
|
||||
[addMemory.key]: addMemory,
|
||||
[deleteMemory.key]: deleteMemory,
|
||||
},
|
||||
|
||||
searches: {
|
||||
[searchMemories.key]: searchMemories,
|
||||
[getMemories.key]: getMemories,
|
||||
},
|
||||
|
||||
resources: {},
|
||||
triggers: {},
|
||||
};
|
||||
|
||||
export = app;
|
||||
@@ -1,7 +1,11 @@
|
||||
'use strict';
|
||||
import type { ZObject, Bundle, MutableRequest, ZResponse } from './types';
|
||||
|
||||
// Prepend the configured base URL and inject the auth header on every request.
|
||||
const includeApiKey = (request, z, bundle) => {
|
||||
export const includeApiKey = (
|
||||
request: MutableRequest,
|
||||
_z: ZObject,
|
||||
bundle: Bundle,
|
||||
): MutableRequest => {
|
||||
if (bundle.authData && bundle.authData.apiKey) {
|
||||
request.headers = request.headers || {};
|
||||
request.headers.Authorization = `Token ${bundle.authData.apiKey}`;
|
||||
@@ -17,7 +21,11 @@ const includeApiKey = (request, z, bundle) => {
|
||||
// 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) => {
|
||||
export const handleBadResponses = (
|
||||
response: ZResponse,
|
||||
z: ZObject,
|
||||
_bundle: Bundle,
|
||||
): ZResponse => {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
throw new z.errors.Error(
|
||||
'The Mem0 API key you supplied is invalid or lacks access.',
|
||||
@@ -26,8 +34,12 @@ const handleBadResponses = (response, z, _bundle) => {
|
||||
);
|
||||
}
|
||||
if (response.status >= 400) {
|
||||
const data = response.data || {};
|
||||
const detail = data.detail || data.error || data.message || response.content || 'unknown error';
|
||||
const data = (response.data as Record<string, unknown>) || {};
|
||||
const raw =
|
||||
data.detail || data.error || data.message || response.content || 'unknown error';
|
||||
// DRF sometimes returns detail as an object/array; stringify so the
|
||||
// thrown message never renders as "[object Object]".
|
||||
const detail = typeof raw === 'string' ? raw : JSON.stringify(raw);
|
||||
throw new z.errors.Error(
|
||||
`Mem0 API request failed (HTTP ${response.status}): ${detail}`,
|
||||
'Mem0ApiError',
|
||||
@@ -36,5 +48,3 @@ const handleBadResponses = (response, z, _bundle) => {
|
||||
}
|
||||
return response;
|
||||
};
|
||||
|
||||
module.exports = { includeApiKey, handleBadResponses };
|
||||
@@ -1,9 +1,12 @@
|
||||
'use strict';
|
||||
import type { ZObject, Bundle, Memory } from '../types';
|
||||
import { captureEvent } from '../telemetry';
|
||||
|
||||
const perform = async (z, bundle) => {
|
||||
const body = {};
|
||||
const perform = async (z: ZObject, bundle: Bundle): Promise<Memory[]> => {
|
||||
const body: Record<string, unknown> = {};
|
||||
if (bundle.inputData.user_id) body.filters = { user_id: bundle.inputData.user_id };
|
||||
|
||||
captureEvent('zapier.get_memories', bundle.authData?.apiKey);
|
||||
|
||||
const response = await z.request({
|
||||
url: '/v3/memories/',
|
||||
method: 'POST',
|
||||
@@ -13,11 +16,12 @@ const perform = async (z, bundle) => {
|
||||
},
|
||||
body,
|
||||
});
|
||||
const data = response.data;
|
||||
return Array.isArray(data) ? data : data.results || [];
|
||||
// Guard against a null/empty body; always return an array.
|
||||
const data = response.data as Memory[] | { results?: Memory[] } | null;
|
||||
return Array.isArray(data) ? data : data?.results ?? [];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
key: 'get_memories',
|
||||
noun: 'Memory',
|
||||
display: {
|
||||
@@ -1,24 +1,27 @@
|
||||
'use strict';
|
||||
import type { ZObject, Bundle, Memory } from '../types';
|
||||
import { captureEvent } from '../telemetry';
|
||||
|
||||
const perform = async (z, bundle) => {
|
||||
const body = {
|
||||
const perform = async (z: ZObject, bundle: Bundle): Promise<Memory[]> => {
|
||||
const body: Record<string, unknown> = {
|
||||
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 };
|
||||
|
||||
captureEvent('zapier.search_memories', bundle.authData?.apiKey);
|
||||
|
||||
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 || [];
|
||||
// Searches must return an array; guard against a null/empty body.
|
||||
const data = response.data as Memory[] | { results?: Memory[] } | null;
|
||||
return Array.isArray(data) ? data : data?.results ?? [];
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
key: 'search_memories',
|
||||
noun: 'Memory',
|
||||
display: {
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
// Anonymous usage telemetry via PostHog, mirroring the Mem0 CLI / plugins.
|
||||
// Fire-and-forget: never awaited, never throws, so it can neither slow down
|
||||
// nor break an action. Opt out with MEM0_TELEMETRY=false.
|
||||
const POSTHOG_API_KEY = 'phc_hgJkUVJFYtmaJqrvf6CYN67TIQ8yhXAkWzUn9AMU4yX';
|
||||
const POSTHOG_HOST = 'https://us.i.posthog.com/i/v0/e/';
|
||||
|
||||
export function captureEvent(
|
||||
event: string,
|
||||
apiKey: string | undefined,
|
||||
properties: Record<string, unknown> = {},
|
||||
): void {
|
||||
if (process.env.MEM0_TELEMETRY === 'false') return;
|
||||
try {
|
||||
// Hash the API key so events are attributable to one account without
|
||||
// ever transmitting the key itself.
|
||||
const distinctId = apiKey ? createHash('md5').update(apiKey).digest('hex') : 'zapier-anon';
|
||||
const payload = {
|
||||
api_key: POSTHOG_API_KEY,
|
||||
event,
|
||||
distinct_id: distinctId,
|
||||
properties: {
|
||||
source: 'ZAPIER',
|
||||
$process_person_profile: false,
|
||||
...properties,
|
||||
},
|
||||
};
|
||||
void fetch(POSTHOG_HOST, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}).catch(() => {
|
||||
/* swallow — telemetry must never surface to the user */
|
||||
});
|
||||
} catch {
|
||||
/* swallow — telemetry must never break the action */
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { ZObject, Bundle } from 'zapier-platform-core';
|
||||
|
||||
export type { ZObject, Bundle };
|
||||
|
||||
/** Mutable outgoing request, as seen by the beforeRequest middleware. */
|
||||
export interface MutableRequest {
|
||||
url?: string;
|
||||
headers?: Record<string, string>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** Response as seen by the afterResponse middleware. */
|
||||
export interface ZResponse {
|
||||
status: number;
|
||||
data?: Record<string, unknown> | unknown[] | null;
|
||||
content?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** Async add response from POST /v3/memories/add/. */
|
||||
export interface AddResponse {
|
||||
event_id?: string;
|
||||
status?: string;
|
||||
error?: string;
|
||||
message?: string;
|
||||
results?: unknown[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** GET /v1/event/{id}/ response. */
|
||||
export interface EventResponse {
|
||||
status?: string;
|
||||
error?: string;
|
||||
message?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
/** A stored memory object returned by search / list. */
|
||||
export interface Memory {
|
||||
id?: string;
|
||||
memory?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
'use strict';
|
||||
import * as zapier from 'zapier-platform-core';
|
||||
import App from '../src';
|
||||
|
||||
/* global describe, it, expect, beforeAll */
|
||||
|
||||
const zapier = require('zapier-platform-core');
|
||||
const App = require('../index');
|
||||
|
||||
const appTester = zapier.createAppTester(App);
|
||||
const appTester = zapier.createAppTester(App as any);
|
||||
|
||||
const authData = {
|
||||
apiKey: process.env.MEM0_API_KEY,
|
||||
@@ -16,8 +12,12 @@ 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;
|
||||
const until = async <T>(
|
||||
fn: () => Promise<T>,
|
||||
done: (r: T) => boolean,
|
||||
{ attempts = 30, delayMs = 2000 } = {},
|
||||
): Promise<T> => {
|
||||
let last: T = undefined as unknown as T;
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
last = await fn();
|
||||
if (done(last)) return last;
|
||||
@@ -31,15 +31,14 @@ const until = async (fn, done, { attempts = 30, delayMs = 2000 } = {}) => {
|
||||
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 });
|
||||
const res: any = await appTester((App as any).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, {
|
||||
const added: any = await appTester((App as any).creates.add_memory.operation.perform, {
|
||||
authData,
|
||||
inputData: {
|
||||
content: 'I love hiking in the Alps and my favorite food is sushi',
|
||||
@@ -49,19 +48,21 @@ describeE2E('Mem0 Zapier integration (E2E)', () => {
|
||||
expect(added.event_id).toBeDefined();
|
||||
|
||||
// Extraction is async; retry search until the memory is indexed.
|
||||
const found = await until(
|
||||
// Budget generously — live extraction can occasionally exceed a minute.
|
||||
const found: any[] = await until(
|
||||
() =>
|
||||
appTester(App.searches.search_memories.operation.perform, {
|
||||
appTester((App as any).searches.search_memories.operation.perform, {
|
||||
authData,
|
||||
inputData: { query: 'outdoor activities', user_id: userId, limit: 5 },
|
||||
}),
|
||||
(r) => Array.isArray(r) && r.length > 0,
|
||||
(r: any[]) => Array.isArray(r) && r.length > 0,
|
||||
{ attempts: 60, delayMs: 2000 },
|
||||
);
|
||||
expect(Array.isArray(found)).toBe(true);
|
||||
expect(found.length).toBeGreaterThan(0);
|
||||
|
||||
// Get all
|
||||
const all = await appTester(App.searches.get_memories.operation.perform, {
|
||||
const all: any[] = await appTester((App as any).searches.get_memories.operation.perform, {
|
||||
authData,
|
||||
inputData: { user_id: userId },
|
||||
});
|
||||
@@ -70,23 +71,23 @@ describeE2E('Mem0 Zapier integration (E2E)', () => {
|
||||
|
||||
// Cleanup: delete every memory we created
|
||||
for (const mem of all) {
|
||||
await appTester(App.creates.delete_memory.operation.perform, {
|
||||
await appTester((App as any).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 },
|
||||
});
|
||||
const afterDelete: any[] = await appTester(
|
||||
(App as any).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, {
|
||||
appTester((App as any).searches.search_memories.operation.perform, {
|
||||
authData,
|
||||
inputData: { query: 'anything' },
|
||||
}),
|
||||
@@ -0,0 +1,3 @@
|
||||
// Disable telemetry during tests so runs never fire real PostHog events
|
||||
// (and so the fire-and-forget fetch cannot leave an open handle after tests).
|
||||
process.env.MEM0_TELEMETRY = 'false';
|
||||
@@ -1,124 +0,0 @@
|
||||
'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');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,183 @@
|
||||
// Offline unit tests: they mock `z.request`, so they run unconditionally in CI
|
||||
// (unlike the live E2E suite in mem0.test.ts, gated on MEM0_API_KEY). They cover
|
||||
// what `zapier validate` can't: boolean coercion, URL join, metadata, array shapes.
|
||||
|
||||
import addMemory from '../src/creates/add_memory';
|
||||
import deleteMemory from '../src/creates/delete_memory';
|
||||
import searchMemories from '../src/searches/search_memories';
|
||||
import getMemories from '../src/searches/get_memories';
|
||||
import { includeApiKey } from '../src/middleware';
|
||||
|
||||
// Minimal `z` stub: hands back queued responses and records every request.
|
||||
const makeZ = (responses: any[] = []): any => {
|
||||
const queue = [...responses];
|
||||
const requests: any[] = [];
|
||||
return {
|
||||
requests,
|
||||
request: async (opts: any) => {
|
||||
requests.push(opts);
|
||||
const next = queue.shift();
|
||||
return next !== undefined ? next : { data: {} };
|
||||
},
|
||||
errors: {
|
||||
Error: class Mem0Error extends Error {
|
||||
name: string;
|
||||
status?: number;
|
||||
constructor(message: string, name?: string, status?: number) {
|
||||
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' },
|
||||
} as any);
|
||||
// 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 as any).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' },
|
||||
} as any);
|
||||
expect(z.requests).toHaveLength(2);
|
||||
expect(z.requests[1].url).toBe('/v1/event/e1/');
|
||||
expect((res as any).status).toBe('SUCCEEDED');
|
||||
});
|
||||
|
||||
it('keeps polling past the old 12-attempt budget when the API is slow', async () => {
|
||||
jest.useFakeTimers();
|
||||
const pendingPolls = Array.from({ length: 20 }, () => ({ data: { status: 'PENDING' } }));
|
||||
const z = makeZ([
|
||||
{ data: { event_id: 'e1', status: 'PENDING' } },
|
||||
...pendingPolls,
|
||||
{ data: { status: 'SUCCEEDED', results: [{ id: 'm1' }] } },
|
||||
]);
|
||||
const resultPromise = addMemory.operation.perform(z, {
|
||||
inputData: { content: 'hi', user_id: 'u1', waitForCompletion: 'true' },
|
||||
} as any);
|
||||
for (let i = 0; i < pendingPolls.length; i++) {
|
||||
await jest.advanceTimersByTimeAsync(1500);
|
||||
}
|
||||
const res = await resultPromise;
|
||||
expect((res as any).status).toBe('SUCCEEDED');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
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' },
|
||||
} as any),
|
||||
).rejects.toThrow('Metadata must be valid JSON.');
|
||||
});
|
||||
|
||||
it('forwards custom_instructions and parses custom_categories JSON', async () => {
|
||||
const z = makeZ([{ data: { event_id: 'e1', status: 'PENDING' } }]);
|
||||
await addMemory.operation.perform(z, {
|
||||
inputData: {
|
||||
content: 'hi',
|
||||
user_id: 'u1',
|
||||
custom_instructions: 'keep durable facts',
|
||||
custom_categories: '[{"work":"job related"}]',
|
||||
},
|
||||
} as any);
|
||||
expect(z.requests[0].body.custom_instructions).toBe('keep durable facts');
|
||||
expect(z.requests[0].body.custom_categories).toEqual([{ work: 'job related' }]);
|
||||
});
|
||||
|
||||
it('throws a clear error on invalid Custom Categories JSON', async () => {
|
||||
const z = makeZ();
|
||||
await expect(
|
||||
addMemory.operation.perform(z, {
|
||||
inputData: { content: 'hi', user_id: 'u1', custom_categories: '{bad' },
|
||||
} as any),
|
||||
).rejects.toThrow('Custom Categories must be valid JSON.');
|
||||
});
|
||||
|
||||
it('forwards includes and excludes when provided', async () => {
|
||||
const z = makeZ([{ data: { event_id: 'e1', status: 'PENDING' } }]);
|
||||
await addMemory.operation.perform(z, {
|
||||
inputData: { content: 'hi', user_id: 'u1', includes: 'work facts', excludes: 'small talk' },
|
||||
} as any);
|
||||
expect(z.requests[0].body.includes).toBe('work facts');
|
||||
expect(z.requests[0].body.excludes).toBe('small talk');
|
||||
});
|
||||
});
|
||||
|
||||
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' },
|
||||
} as any);
|
||||
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' } } as any);
|
||||
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' },
|
||||
} as any);
|
||||
expect(z.requests[0].params).toEqual({ page: 2, page_size: 10 });
|
||||
});
|
||||
|
||||
it('search and get_memories return [] on a null/empty body (no crash)', async () => {
|
||||
const zSearch = makeZ([{ data: null }]);
|
||||
const found = await searchMemories.operation.perform(zSearch, {
|
||||
inputData: { query: 'x', user_id: 'u1' },
|
||||
} as any);
|
||||
expect(found).toEqual([]);
|
||||
|
||||
const zGet = makeZ([{ data: null }]);
|
||||
const all = await getMemories.operation.perform(zGet, { inputData: { user_id: 'u1' } } as any);
|
||||
expect(all).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
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' } } as any);
|
||||
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 as any, {
|
||||
authData: { apiKey: 'k', baseUrl: 'https://api.mem0.ai/' },
|
||||
} as any);
|
||||
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 as any, {
|
||||
authData: { apiKey: 'k' },
|
||||
} as any);
|
||||
expect(req.url).toBe('https://other.example/x');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": false,
|
||||
"sourceMap": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist", "test"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0ai",
|
||||
"version": "3.1.0",
|
||||
"version": "3.1.2",
|
||||
"description": "The Memory Layer For Your AI Apps",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -151,6 +151,42 @@
|
||||
"@aws-sdk/client-bedrock-runtime": ">=3.0.0 <3.968.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@qdrant/js-client-rest": {
|
||||
"optional": true
|
||||
},
|
||||
"redis": {
|
||||
"optional": true
|
||||
},
|
||||
"@supabase/supabase-js": {
|
||||
"optional": true
|
||||
},
|
||||
"cloudflare": {
|
||||
"optional": true
|
||||
},
|
||||
"@azure/search-documents": {
|
||||
"optional": true
|
||||
},
|
||||
"@azure/identity": {
|
||||
"optional": true
|
||||
},
|
||||
"@langchain/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@anthropic-ai/sdk": {
|
||||
"optional": true
|
||||
},
|
||||
"@google/genai": {
|
||||
"optional": true
|
||||
},
|
||||
"groq-sdk": {
|
||||
"optional": true
|
||||
},
|
||||
"@mistralai/mistralai": {
|
||||
"optional": true
|
||||
},
|
||||
"ollama": {
|
||||
"optional": true
|
||||
},
|
||||
"mysql2": {
|
||||
"optional": true
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FlagEmbedding } from "fastembed";
|
||||
import { Embedder } from "./base";
|
||||
import { EmbeddingConfig } from "../types";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
// FastEmbed only ships a fixed set of ONNX models (fastembed's `EmbeddingModel`
|
||||
// enum, minus CUSTOM). Mirrored here as literals so an invalid model name can
|
||||
@@ -54,14 +55,11 @@ export class FastEmbedEmbedder implements Embedder {
|
||||
* consumers that never touch FastEmbed don't need it installed.
|
||||
*/
|
||||
private async initEmbeddingModel(): Promise<FlagEmbedding> {
|
||||
let sdk: any;
|
||||
try {
|
||||
sdk = await import("fastembed");
|
||||
} catch {
|
||||
throw new Error(
|
||||
"The 'fastembed' package is required to use the FastEmbed embedder. Install it with: npm install fastembed",
|
||||
);
|
||||
}
|
||||
const sdk = await loadPeer(
|
||||
"fastembed",
|
||||
"FastEmbed embedder",
|
||||
() => import("fastembed"),
|
||||
);
|
||||
|
||||
return sdk.FlagEmbedding.init({ model: this.modelName });
|
||||
}
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import type { GoogleGenAI } from "@google/genai";
|
||||
import { Embedder } from "./base";
|
||||
import { EmbeddingConfig } from "../types";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
export class GoogleEmbedder implements Embedder {
|
||||
private google: GoogleGenAI;
|
||||
private google!: GoogleGenAI;
|
||||
private model: string;
|
||||
private embeddingDims: number | undefined;
|
||||
private readonly apiKey: string | undefined;
|
||||
|
||||
constructor(config: EmbeddingConfig) {
|
||||
this.google = new GoogleGenAI({
|
||||
apiKey: config.apiKey || process.env.GOOGLE_API_KEY,
|
||||
});
|
||||
this.apiKey = config.apiKey || process.env.GOOGLE_API_KEY;
|
||||
this.model = config.model || "gemini-embedding-001";
|
||||
this.embeddingDims = config.embeddingDims;
|
||||
}
|
||||
|
||||
private async ensureClient(): Promise<void> {
|
||||
if (this.google) return;
|
||||
const sdk = await loadPeer(
|
||||
"@google/genai",
|
||||
"Google embedder",
|
||||
() => import("@google/genai"),
|
||||
);
|
||||
this.google = new sdk.GoogleGenAI({ apiKey: this.apiKey });
|
||||
}
|
||||
|
||||
async embed(text: string): Promise<number[]> {
|
||||
await this.ensureClient();
|
||||
const response = await this.google.models.embedContent({
|
||||
model: this.model,
|
||||
contents: text,
|
||||
@@ -27,6 +38,7 @@ export class GoogleEmbedder implements Embedder {
|
||||
}
|
||||
|
||||
async embedBatch(texts: string[]): Promise<number[][]> {
|
||||
await this.ensureClient();
|
||||
const response = await this.google.models.embedContent({
|
||||
model: this.model,
|
||||
contents: texts,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Embeddings } from "@langchain/core/embeddings";
|
||||
import type { Embeddings } from "@langchain/core/embeddings";
|
||||
import { Embedder } from "./base";
|
||||
import { EmbeddingConfig } from "../types";
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { Ollama } from "ollama";
|
||||
import type { Ollama } from "ollama";
|
||||
import { Embedder } from "./base";
|
||||
import { EmbeddingConfig } from "../types";
|
||||
import { logger } from "../utils/logger";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
export class OllamaEmbedder implements Embedder {
|
||||
private ollama: Ollama;
|
||||
private ollama!: Ollama;
|
||||
private model: string;
|
||||
private embeddingDims?: number;
|
||||
private readonly host: string;
|
||||
// Using this variable to avoid calling the Ollama server multiple times
|
||||
private initialized: boolean = false;
|
||||
|
||||
constructor(config: EmbeddingConfig) {
|
||||
this.ollama = new Ollama({
|
||||
host: config.url || config.baseURL || "http://localhost:11434",
|
||||
});
|
||||
this.host = config.url || config.baseURL || "http://localhost:11434";
|
||||
this.model = config.model || "nomic-embed-text:latest";
|
||||
this.embeddingDims = config.embeddingDims || 768;
|
||||
this.ensureModelExists().catch((err) => {
|
||||
@@ -21,7 +21,18 @@ export class OllamaEmbedder implements Embedder {
|
||||
});
|
||||
}
|
||||
|
||||
private async ensureClient(): Promise<void> {
|
||||
if (this.ollama) return;
|
||||
const sdk = await loadPeer(
|
||||
"ollama",
|
||||
"Ollama embedder",
|
||||
() => import("ollama"),
|
||||
);
|
||||
this.ollama = new sdk.Ollama({ host: this.host });
|
||||
}
|
||||
|
||||
async embed(text: string): Promise<number[]> {
|
||||
await this.ensureClient();
|
||||
try {
|
||||
await this.ensureModelExists();
|
||||
} catch (err) {
|
||||
@@ -54,6 +65,7 @@ export class OllamaEmbedder implements Embedder {
|
||||
if (this.initialized) {
|
||||
return true;
|
||||
}
|
||||
await this.ensureClient();
|
||||
const local_models = await this.ollama.list();
|
||||
const target = OllamaEmbedder.normalizeModelName(this.model);
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PredictionServiceClient } from "@google-cloud/aiplatform";
|
||||
import { Embedder } from "./base";
|
||||
import { VertexAIConfig } from "../types";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
type AIPlatform = typeof import("@google-cloud/aiplatform");
|
||||
type ClientOptions = NonNullable<
|
||||
@@ -105,15 +106,11 @@ export class VertexAIEmbedder implements Embedder {
|
||||
}
|
||||
|
||||
private async createClient(): Promise<void> {
|
||||
let aiplatform: AIPlatform;
|
||||
try {
|
||||
aiplatform = await import("@google-cloud/aiplatform");
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
"Failed to import '@google-cloud/aiplatform'. Please install it to use the Vertex AI embedding provider: " +
|
||||
(err as Error).message,
|
||||
);
|
||||
}
|
||||
const aiplatform: AIPlatform = await loadPeer(
|
||||
"@google-cloud/aiplatform",
|
||||
"Vertex AI embedding provider",
|
||||
() => import("@google-cloud/aiplatform"),
|
||||
);
|
||||
|
||||
const client = new aiplatform.PredictionServiceClient(this.clientOptions);
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import type Anthropic from "@anthropic-ai/sdk";
|
||||
import { LLM, LLMResponse } from "./base";
|
||||
import { LLMConfig, Message } from "../types";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
export class AnthropicLLM implements LLM {
|
||||
private client: Anthropic;
|
||||
private client!: Anthropic;
|
||||
private readonly clientArgs: { apiKey: string; baseURL?: string };
|
||||
private model: string;
|
||||
private maxTokens: number;
|
||||
private temperature?: number;
|
||||
@@ -20,7 +22,7 @@ export class AnthropicLLM implements LLM {
|
||||
if (config.baseURL) {
|
||||
clientArgs.baseURL = config.baseURL;
|
||||
}
|
||||
this.client = new Anthropic(clientArgs);
|
||||
this.clientArgs = clientArgs;
|
||||
this.model = config.model || "claude-sonnet-4-6";
|
||||
// Defaults mirror the Python provider's AnthropicConfig
|
||||
// (max_tokens=2000, temperature=0.1, top_p omitted).
|
||||
@@ -29,11 +31,22 @@ export class AnthropicLLM implements LLM {
|
||||
this.topP = config.topP;
|
||||
}
|
||||
|
||||
private async ensureClient(): Promise<void> {
|
||||
if (this.client) return;
|
||||
const sdk = await loadPeer(
|
||||
"@anthropic-ai/sdk",
|
||||
"Anthropic LLM",
|
||||
() => import("@anthropic-ai/sdk"),
|
||||
);
|
||||
this.client = new sdk.default(this.clientArgs);
|
||||
}
|
||||
|
||||
async generateResponse(
|
||||
messages: Message[],
|
||||
responseFormat?: { type: string },
|
||||
tools?: any[],
|
||||
): Promise<string | LLMResponse> {
|
||||
await this.ensureClient();
|
||||
// Extract system message if present
|
||||
const systemMessage = messages.find((msg) => msg.role === "system");
|
||||
const otherMessages = messages.filter((msg) => msg.role !== "system");
|
||||
@@ -87,12 +100,16 @@ export class AnthropicLLM implements LLM {
|
||||
return { content, role: "assistant", toolCalls };
|
||||
}
|
||||
|
||||
const firstBlock = response.content[0];
|
||||
if (firstBlock.type === "text") {
|
||||
return firstBlock.text;
|
||||
} else {
|
||||
throw new Error("Unexpected response type from Anthropic API");
|
||||
// Thinking-enabled responses put a thinking block before the text block,
|
||||
// and a response can carry no text block at all, so find the text block
|
||||
// like the tools branch above instead of indexing content[0]. Mirrors the
|
||||
// Python provider (#6481).
|
||||
for (const block of response.content) {
|
||||
if (block.type === "text") {
|
||||
return block.text;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async generateChat(messages: Message[]): Promise<LLMResponse> {
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
import { GoogleGenAI } from "@google/genai";
|
||||
import type { GoogleGenAI } from "@google/genai";
|
||||
import { LLM, LLMResponse } from "./base";
|
||||
import { LLMConfig, Message } from "../types";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
export class GoogleLLM implements LLM {
|
||||
private google: GoogleGenAI;
|
||||
private google!: GoogleGenAI;
|
||||
private model: string;
|
||||
private readonly apiKey: string | undefined;
|
||||
|
||||
constructor(config: LLMConfig) {
|
||||
this.google = new GoogleGenAI({ apiKey: config.apiKey });
|
||||
this.apiKey = config.apiKey;
|
||||
this.model = config.model || "gemini-2.0-flash";
|
||||
}
|
||||
|
||||
private async ensureClient(): Promise<void> {
|
||||
if (this.google) return;
|
||||
const sdk = await loadPeer(
|
||||
"@google/genai",
|
||||
"Google LLM",
|
||||
() => import("@google/genai"),
|
||||
);
|
||||
this.google = new sdk.GoogleGenAI({ apiKey: this.apiKey });
|
||||
}
|
||||
|
||||
private formatContents(messages: Message[]) {
|
||||
return messages.map((msg) => ({
|
||||
parts: [
|
||||
@@ -30,6 +42,7 @@ export class GoogleLLM implements LLM {
|
||||
responseFormat?: { type: string },
|
||||
tools?: any[],
|
||||
): Promise<string | LLMResponse> {
|
||||
await this.ensureClient();
|
||||
const contents = this.formatContents(messages);
|
||||
|
||||
// Build config with tools if provided
|
||||
@@ -46,6 +59,15 @@ export class GoogleLLM implements LLM {
|
||||
];
|
||||
}
|
||||
|
||||
// Honor a requested JSON response format (parity with the Python SDK's
|
||||
// mem0/llms/gemini.py). Gemini's structured output is opt-in via
|
||||
// responseMimeType — without it the model is never told to emit JSON, so
|
||||
// callers passing json_object silently get free-form text and depend on a
|
||||
// fragile markdown-fence strip downstream.
|
||||
if (responseFormat?.type === "json_object") {
|
||||
config.responseMimeType = "application/json";
|
||||
}
|
||||
|
||||
const completion = await this.google.models.generateContent({
|
||||
contents,
|
||||
model: this.model,
|
||||
@@ -72,6 +94,7 @@ export class GoogleLLM implements LLM {
|
||||
}
|
||||
|
||||
async generateChat(messages: Message[]): Promise<LLMResponse> {
|
||||
await this.ensureClient();
|
||||
const completion = await this.google.models.generateContent({
|
||||
contents: this.formatContents(messages),
|
||||
model: this.model,
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
import { Groq } from "groq-sdk";
|
||||
import type { Groq } from "groq-sdk";
|
||||
import { LLM, LLMResponse } from "./base";
|
||||
import { LLMConfig, Message } from "../types";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
export class GroqLLM implements LLM {
|
||||
private client: Groq;
|
||||
private client!: Groq;
|
||||
private model: string;
|
||||
private readonly apiKey: string;
|
||||
|
||||
constructor(config: LLMConfig) {
|
||||
const apiKey = config.apiKey || process.env.GROQ_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("Groq API key is required");
|
||||
}
|
||||
this.client = new Groq({ apiKey });
|
||||
this.apiKey = apiKey;
|
||||
this.model = config.model || "llama3-70b-8192";
|
||||
}
|
||||
|
||||
private async ensureClient(): Promise<void> {
|
||||
if (this.client) return;
|
||||
const sdk = await loadPeer(
|
||||
"groq-sdk",
|
||||
"Groq LLM",
|
||||
() => import("groq-sdk"),
|
||||
);
|
||||
this.client = new sdk.Groq({ apiKey: this.apiKey });
|
||||
}
|
||||
|
||||
async generateResponse(
|
||||
messages: Message[],
|
||||
responseFormat?: { type: string },
|
||||
): Promise<string> {
|
||||
await this.ensureClient();
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
messages: messages.map((msg) => ({
|
||||
@@ -35,6 +48,7 @@ export class GroqLLM implements LLM {
|
||||
}
|
||||
|
||||
async generateChat(messages: Message[]): Promise<LLMResponse> {
|
||||
await this.ensureClient();
|
||||
const response = await this.client.chat.completions.create({
|
||||
model: this.model,
|
||||
messages: messages.map((msg) => ({
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { BaseLanguageModel } from "@langchain/core/language_models/base";
|
||||
import {
|
||||
AIMessage,
|
||||
HumanMessage,
|
||||
SystemMessage,
|
||||
BaseMessage,
|
||||
} from "@langchain/core/messages";
|
||||
import type { BaseLanguageModel } from "@langchain/core/language_models/base";
|
||||
import type { BaseMessage } from "@langchain/core/messages";
|
||||
import { z } from "zod";
|
||||
import { LLM, LLMResponse } from "./base";
|
||||
import { LLMConfig, Message } from "../types/index";
|
||||
// Import the schemas directly into LangchainLLM
|
||||
import { FactRetrievalSchema, MemoryUpdateSchema } from "../prompts";
|
||||
|
||||
const convertToLangchainMessages = (messages: Message[]): BaseMessage[] => {
|
||||
const convertToLangchainMessages = async (
|
||||
messages: Message[],
|
||||
): Promise<BaseMessage[]> => {
|
||||
const { AIMessage, HumanMessage, SystemMessage } =
|
||||
await import("@langchain/core/messages");
|
||||
return messages.map((msg) => {
|
||||
const content =
|
||||
typeof msg.content === "string"
|
||||
@@ -62,7 +61,7 @@ export class LangchainLLM implements LLM {
|
||||
response_format?: { type: string },
|
||||
tools?: any[],
|
||||
): Promise<string | LLMResponse> {
|
||||
const langchainMessages = convertToLangchainMessages(messages);
|
||||
const langchainMessages = await convertToLangchainMessages(messages);
|
||||
let runnable: any = this.llmInstance;
|
||||
const invokeOptions: Record<string, any> = {};
|
||||
let isStructuredOutput = false;
|
||||
@@ -170,7 +169,7 @@ export class LangchainLLM implements LLM {
|
||||
}
|
||||
|
||||
async generateChat(messages: Message[]): Promise<LLMResponse> {
|
||||
const langchainMessages = convertToLangchainMessages(messages);
|
||||
const langchainMessages = await convertToLangchainMessages(messages);
|
||||
try {
|
||||
const response = await this.llmInstance.invoke(langchainMessages);
|
||||
if (response && typeof response.content === "string") {
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import { Mistral } from "@mistralai/mistralai";
|
||||
import type { Mistral } from "@mistralai/mistralai";
|
||||
import { LLM, LLMResponse } from "./base";
|
||||
import { LLMConfig, Message } from "../types";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
export class MistralLLM implements LLM {
|
||||
private client: Mistral;
|
||||
private client!: Mistral;
|
||||
private model: string;
|
||||
private readonly apiKey: string;
|
||||
|
||||
constructor(config: LLMConfig) {
|
||||
if (!config.apiKey) {
|
||||
throw new Error("Mistral API key is required");
|
||||
}
|
||||
this.client = new Mistral({
|
||||
apiKey: config.apiKey,
|
||||
});
|
||||
this.apiKey = config.apiKey;
|
||||
this.model = config.model || "mistral-tiny-latest";
|
||||
}
|
||||
|
||||
private async ensureClient(): Promise<void> {
|
||||
if (this.client) return;
|
||||
const sdk = await loadPeer(
|
||||
"@mistralai/mistralai",
|
||||
"Mistral LLM",
|
||||
() => import("@mistralai/mistralai"),
|
||||
);
|
||||
this.client = new sdk.Mistral({ apiKey: this.apiKey });
|
||||
}
|
||||
|
||||
// Helper function to convert content to string
|
||||
private contentToString(content: any): string {
|
||||
if (typeof content === "string") {
|
||||
@@ -41,6 +51,7 @@ export class MistralLLM implements LLM {
|
||||
responseFormat?: { type: string },
|
||||
tools?: any[],
|
||||
): Promise<string | LLMResponse> {
|
||||
await this.ensureClient();
|
||||
const response = await this.client.chat.complete({
|
||||
model: this.model,
|
||||
messages: messages.map((msg) => ({
|
||||
@@ -82,6 +93,7 @@ export class MistralLLM implements LLM {
|
||||
}
|
||||
|
||||
async generateChat(messages: Message[]): Promise<LLMResponse> {
|
||||
await this.ensureClient();
|
||||
const formattedMessages = messages.map((msg) => ({
|
||||
role: msg.role as "system" | "user" | "assistant",
|
||||
content:
|
||||
|
||||
@@ -1,29 +1,36 @@
|
||||
import { Ollama } from "ollama";
|
||||
import type { Ollama } from "ollama";
|
||||
import { LLM, LLMResponse } from "./base";
|
||||
import { LLMConfig, Message } from "../types";
|
||||
import { logger } from "../utils/logger";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
export class OllamaLLM implements LLM {
|
||||
private ollama: Ollama;
|
||||
private ollama!: Ollama;
|
||||
private model: string;
|
||||
private readonly host: string;
|
||||
// Using this variable to avoid calling the Ollama server multiple times
|
||||
private initialized: boolean = false;
|
||||
|
||||
constructor(config: LLMConfig) {
|
||||
this.ollama = new Ollama({
|
||||
host: config.url || config.baseURL || "http://localhost:11434",
|
||||
});
|
||||
this.host = config.url || config.baseURL || "http://localhost:11434";
|
||||
this.model = config.model || "llama3.1:8b";
|
||||
this.ensureModelExists().catch((err) => {
|
||||
logger.error(`Error ensuring model exists: ${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
private async ensureClient(): Promise<void> {
|
||||
if (this.ollama) return;
|
||||
const sdk = await loadPeer("ollama", "Ollama LLM", () => import("ollama"));
|
||||
this.ollama = new sdk.Ollama({ host: this.host });
|
||||
}
|
||||
|
||||
async generateResponse(
|
||||
messages: Message[],
|
||||
responseFormat?: { type: string },
|
||||
tools?: any[],
|
||||
): Promise<string | LLMResponse> {
|
||||
await this.ensureClient();
|
||||
try {
|
||||
await this.ensureModelExists();
|
||||
} catch (err) {
|
||||
@@ -63,6 +70,7 @@ export class OllamaLLM implements LLM {
|
||||
}
|
||||
|
||||
async generateChat(messages: Message[]): Promise<LLMResponse> {
|
||||
await this.ensureClient();
|
||||
try {
|
||||
await this.ensureModelExists();
|
||||
} catch (err) {
|
||||
@@ -93,6 +101,7 @@ export class OllamaLLM implements LLM {
|
||||
if (this.initialized) {
|
||||
return true;
|
||||
}
|
||||
await this.ensureClient();
|
||||
const local_models = await this.ollama.list();
|
||||
if (!local_models.models.find((m: any) => m.name === this.model)) {
|
||||
logger.info(`Pulling model ${this.model}...`);
|
||||
|
||||
@@ -101,6 +101,10 @@ const ENTITY_PARAMS = [
|
||||
"runId",
|
||||
];
|
||||
|
||||
// Identity keys stripped from update() metadata: ENTITY_PARAMS covers user_id/agent_id/run_id
|
||||
// in both casings (the default store promotes camelCase on read); actor_id has no camelCase alias.
|
||||
const IDENTITY_KEYS = [...ENTITY_PARAMS, "actor_id"];
|
||||
|
||||
/**
|
||||
* Validates that no top-level entity parameters are passed in config.
|
||||
* @throws Error if entity params are found at top level
|
||||
@@ -122,18 +126,19 @@ function rejectTopLevelEntityParams(
|
||||
|
||||
/**
|
||||
* Validates and normalizes an entity ID.
|
||||
* - Coerces non-string ids (e.g. numeric database keys) to string
|
||||
* - Trims leading/trailing whitespace
|
||||
* - Rejects empty or whitespace-only strings
|
||||
* - Rejects strings containing internal whitespace
|
||||
* @returns The trimmed entity ID, or undefined if input is undefined
|
||||
* @returns The trimmed entity ID, or undefined if input is undefined/null
|
||||
* @throws Error if entity ID is invalid
|
||||
*/
|
||||
function validateAndTrimEntityId(
|
||||
value: string | undefined,
|
||||
value: string | number | undefined | null,
|
||||
name: string,
|
||||
): string | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const trimmed = value.trim();
|
||||
if (value == null) return undefined;
|
||||
const trimmed = String(value).trim();
|
||||
if (trimmed === "") {
|
||||
throw new Error(
|
||||
`Invalid ${name}: cannot be empty or whitespace-only. Provide a valid identifier.`,
|
||||
@@ -1941,9 +1946,14 @@ export class Memory {
|
||||
existingEmbeddings[newData] ||
|
||||
(await this.embedder.embed(newData, "update"));
|
||||
|
||||
// Caller metadata must not overwrite or inject an identity scope (#6342 / #6367).
|
||||
const sanitizedMetadata = Object.fromEntries(
|
||||
Object.entries(metadata).filter(([k]) => !IDENTITY_KEYS.includes(k)),
|
||||
);
|
||||
|
||||
const newMetadata = {
|
||||
...existingMemory.payload,
|
||||
...metadata,
|
||||
...sanitizedMetadata,
|
||||
data: newData,
|
||||
hash: createHash("md5").update(newData).digest("hex"),
|
||||
textLemmatized: lemmatizeForBm25(newData),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RerankerConfig } from "../types";
|
||||
import { Reranker, RerankResult } from "./base";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
const DEFAULT_MODEL = "rerank-v3.5";
|
||||
|
||||
@@ -41,14 +42,11 @@ export class CohereReranker implements Reranker {
|
||||
}
|
||||
|
||||
private async createClient(): Promise<any> {
|
||||
let sdk: any;
|
||||
try {
|
||||
sdk = await import("cohere-ai");
|
||||
} catch {
|
||||
throw new Error(
|
||||
"The 'cohere-ai' package is required to use the Cohere reranker. Install it with: npm install cohere-ai",
|
||||
);
|
||||
}
|
||||
const sdk = await loadPeer(
|
||||
"cohere-ai",
|
||||
"Cohere reranker",
|
||||
() => import("cohere-ai"),
|
||||
);
|
||||
return new sdk.CohereClient({ token: this.apiKey });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { RerankerConfig } from "../types";
|
||||
import { Reranker, RerankResult } from "./base";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
const DEFAULT_MODEL = "zerank-1";
|
||||
|
||||
@@ -37,14 +38,11 @@ export class ZeroEntropyReranker implements Reranker {
|
||||
}
|
||||
|
||||
private async createClient(): Promise<any> {
|
||||
let sdk: any;
|
||||
try {
|
||||
sdk = await import("zeroentropy");
|
||||
} catch {
|
||||
throw new Error(
|
||||
"The 'zeroentropy' package is required to use the ZeroEntropy reranker. Install it with: npm install zeroentropy",
|
||||
);
|
||||
}
|
||||
const sdk = await loadPeer(
|
||||
"zeroentropy",
|
||||
"ZeroEntropy reranker",
|
||||
() => import("zeroentropy"),
|
||||
);
|
||||
return new sdk.ZeroEntropy({ apiKey: this.apiKey });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createClient, SupabaseClient } from "@supabase/supabase-js";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { HistoryManager } from "./base";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
interface HistoryEntry {
|
||||
id: string;
|
||||
@@ -20,16 +21,32 @@ interface SupabaseHistoryConfig {
|
||||
}
|
||||
|
||||
export class SupabaseHistoryManager implements HistoryManager {
|
||||
private supabase: SupabaseClient;
|
||||
// ponytail: benign double-construct race — two concurrent first-calls may each
|
||||
// build a client; createClient opens no connection, so last-write-wins is fine.
|
||||
private supabase!: SupabaseClient;
|
||||
private readonly supabaseUrl: string;
|
||||
private readonly supabaseKey: string;
|
||||
private readonly tableName: string;
|
||||
|
||||
constructor(config: SupabaseHistoryConfig) {
|
||||
this.tableName = config.tableName || "memory_history";
|
||||
this.supabase = createClient(config.supabaseUrl, config.supabaseKey);
|
||||
this.supabaseUrl = config.supabaseUrl;
|
||||
this.supabaseKey = config.supabaseKey;
|
||||
this.initializeSupabase().catch(console.error);
|
||||
}
|
||||
|
||||
private async ensureClient(): Promise<void> {
|
||||
if (this.supabase) return;
|
||||
const sdk = await loadPeer(
|
||||
"@supabase/supabase-js",
|
||||
"Supabase history manager",
|
||||
() => import("@supabase/supabase-js"),
|
||||
);
|
||||
this.supabase = sdk.createClient(this.supabaseUrl, this.supabaseKey);
|
||||
}
|
||||
|
||||
private async initializeSupabase(): Promise<void> {
|
||||
await this.ensureClient();
|
||||
// Check if table exists
|
||||
const { error } = await this.supabase
|
||||
.from(this.tableName)
|
||||
@@ -65,6 +82,7 @@ create table ${this.tableName} (
|
||||
updatedAt?: string,
|
||||
isDeleted: number = 0,
|
||||
): Promise<void> {
|
||||
await this.ensureClient();
|
||||
const historyEntry: HistoryEntry = {
|
||||
id: uuidv4(),
|
||||
memory_id: memoryId,
|
||||
@@ -87,6 +105,7 @@ create table ${this.tableName} (
|
||||
}
|
||||
|
||||
async getHistory(memoryId: string): Promise<any[]> {
|
||||
await this.ensureClient();
|
||||
const { data, error } = await this.supabase
|
||||
.from(this.tableName)
|
||||
.select("*")
|
||||
@@ -103,6 +122,7 @@ create table ${this.tableName} (
|
||||
}
|
||||
|
||||
async reset(): Promise<void> {
|
||||
await this.ensureClient();
|
||||
const { error } = await this.supabase
|
||||
.from(this.tableName)
|
||||
.delete()
|
||||
|
||||
@@ -440,4 +440,52 @@ describe("generateWhereClause", () => {
|
||||
ChromaDB.generateWhereClause({ $not: [{ age: { gt: 18 } }] }),
|
||||
).toEqual({ age: { $lte: 18 } });
|
||||
});
|
||||
|
||||
// Regression tests for the three where-clause translation bugs fixed in
|
||||
// Python by #6452 and tracked for the TS SDK in #6513. ChromaDB allows
|
||||
// exactly one operator or field per dict level.
|
||||
it("keeps both bounds of a same-field range as $and-combined clauses", () => {
|
||||
expect(ChromaDB.generateWhereClause({ age: { gte: 18, lte: 65 } })).toEqual(
|
||||
{ $and: [{ age: { $gte: 18 } }, { age: { $lte: 65 } }] },
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps same-field ranges inside $or branches", () => {
|
||||
expect(
|
||||
ChromaDB.generateWhereClause({
|
||||
$or: [{ age: { gte: 18, lte: 65 } }, { vip: true }],
|
||||
}),
|
||||
).toEqual({
|
||||
$or: [
|
||||
{ $and: [{ age: { $gte: 18 } }, { age: { $lte: 65 } }] },
|
||||
{ vip: { $eq: true } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("wraps multi-field conditions inside $or in $and", () => {
|
||||
expect(
|
||||
ChromaDB.generateWhereClause({
|
||||
$or: [{ age: { gte: 18 }, vip: true }, { city: "sh" }],
|
||||
}),
|
||||
).toEqual({
|
||||
$or: [
|
||||
{ $and: [{ age: { $gte: 18 } }, { vip: { $eq: true } }] },
|
||||
{ city: { $eq: "sh" } },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("negates $not contains/icontains instead of dropping the clause", () => {
|
||||
expect(
|
||||
ChromaDB.generateWhereClause({
|
||||
$not: [{ title: { contains: "draft" } }],
|
||||
}),
|
||||
).toEqual({ title: { $ne: "draft" } });
|
||||
expect(
|
||||
ChromaDB.generateWhereClause({
|
||||
$not: [{ title: { icontains: "draft" } }],
|
||||
}),
|
||||
).toEqual({ title: { $ne: "draft" } });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -267,6 +267,36 @@ describe("Milvus vector store (TS OSS SDK)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("skips a wildcard '*' filter value and keeps the rest", async () => {
|
||||
const client = new FakeMilvusClient({ existing: ["mem0"] });
|
||||
client.searchResponse = { results: [] };
|
||||
const store = makeStore(client, { metricType: "COSINE" });
|
||||
await store.initialize();
|
||||
|
||||
// "*" means match-any: it must be dropped, not emitted as `== "*"` (which
|
||||
// matches nothing), leaving only the real agent_id clause.
|
||||
await store.search([0.1, 0.2, 0.3], 5, {
|
||||
user_id: "*",
|
||||
agent_id: "a1",
|
||||
});
|
||||
|
||||
const searchCall = client.calls.find((c) => c.method === "search")!;
|
||||
expect(searchCall.args.filter).toBe('(metadata["agent_id"] == "a1")');
|
||||
});
|
||||
|
||||
it("omits the filter entirely when every value is a wildcard", async () => {
|
||||
const client = new FakeMilvusClient({ existing: ["mem0"] });
|
||||
const store = makeStore(client);
|
||||
await store.initialize();
|
||||
|
||||
await store.list({ user_id: "*" });
|
||||
|
||||
// All clauses dropped, so list() falls back to its match-all "" filter
|
||||
// rather than a literal `(metadata["user_id"] == "*")` that matches nothing.
|
||||
const queryCall = client.calls.filter((c) => c.method === "query").pop()!;
|
||||
expect(queryCall.args.filter).toBe("");
|
||||
});
|
||||
|
||||
it("normalises L2 distances into a 0..1 similarity score", async () => {
|
||||
const client = new FakeMilvusClient({ existing: ["mem0"] });
|
||||
client.searchResponse = {
|
||||
@@ -432,12 +462,18 @@ describe("Milvus vector store (TS OSS SDK)", () => {
|
||||
["a"],
|
||||
[{ data: "hello world", text_lemmatized: "hello world lemma" }],
|
||||
);
|
||||
await store.insert(
|
||||
[[0.2, 0.3, 0.4]],
|
||||
["c"],
|
||||
[{ data: "hello world", textLemmatized: "hello world camel" }],
|
||||
);
|
||||
// Falls back to raw data when there is no lemmatized text.
|
||||
await store.insert([[0.4, 0.5, 0.6]], ["b"], [{ data: "just data" }]);
|
||||
|
||||
const insertCalls = client.calls.filter((c) => c.method === "insert");
|
||||
expect(insertCalls[0].args.data[0].text).toBe("hello world lemma");
|
||||
expect(insertCalls[1].args.data[0].text).toBe("just data");
|
||||
expect(insertCalls[1].args.data[0].text).toBe("hello world camel");
|
||||
expect(insertCalls[2].args.data[0].text).toBe("just data");
|
||||
});
|
||||
|
||||
it("writes the BM25 text field on update for a BM25 collection", async () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ const mockFindOne = jest.fn();
|
||||
const mockUpdateOne = jest.fn();
|
||||
const mockListSearchIndexes = jest.fn();
|
||||
const mockCreateSearchIndex = jest.fn();
|
||||
const mockDropSearchIndex = jest.fn();
|
||||
const mockDrop = jest.fn();
|
||||
const mockToArray = jest.fn();
|
||||
const mockLimit = jest.fn().mockReturnThis();
|
||||
@@ -26,6 +27,7 @@ const mockCollection = {
|
||||
updateOne: mockUpdateOne,
|
||||
listSearchIndexes: mockListSearchIndexes,
|
||||
createSearchIndex: mockCreateSearchIndex,
|
||||
dropSearchIndex: mockDropSearchIndex,
|
||||
drop: mockDrop,
|
||||
find: mockFind,
|
||||
aggregate: mockAggregate,
|
||||
@@ -74,6 +76,25 @@ describe("MongoDB Vector Store", () => {
|
||||
await store.close();
|
||||
});
|
||||
|
||||
const expectedTextSearchIndexDefinition = {
|
||||
name: "test_col_text_search_index",
|
||||
definition: {
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
fields: {
|
||||
payload: {
|
||||
type: "document",
|
||||
fields: {
|
||||
data: { type: "string" },
|
||||
textLemmatized: { type: "string" },
|
||||
text_lemmatized: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it("should initialize client and check/create collection and indexes", async () => {
|
||||
await store.initialize();
|
||||
|
||||
@@ -84,6 +105,83 @@ describe("MongoDB Vector Store", () => {
|
||||
});
|
||||
expect(mockCollection.deleteOne).toHaveBeenCalledWith({ _id: 0 });
|
||||
expect(mockCreateSearchIndex).toHaveBeenCalledTimes(2);
|
||||
expect(mockCreateSearchIndex).toHaveBeenCalledWith(
|
||||
expectedTextSearchIndexDefinition,
|
||||
);
|
||||
});
|
||||
|
||||
it("should drop and recreate a stale text search index on upgrade", async () => {
|
||||
mockListCollections.mockReturnValue({
|
||||
toArray: jest.fn().mockResolvedValue([{ name: "test_col" }]),
|
||||
});
|
||||
mockListSearchIndexes.mockReturnValue({
|
||||
toArray: jest.fn().mockResolvedValue([
|
||||
{ name: "test_col_vector_index" },
|
||||
{
|
||||
name: "test_col_text_search_index",
|
||||
definition: {
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
fields: {
|
||||
payload: {
|
||||
type: "document",
|
||||
fields: {
|
||||
data: { type: "string" },
|
||||
text_lemmatized: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
await store.initialize();
|
||||
|
||||
expect(mockDropSearchIndex).toHaveBeenCalledWith(
|
||||
"test_col_text_search_index",
|
||||
);
|
||||
expect(mockCreateSearchIndex).toHaveBeenCalledTimes(1);
|
||||
expect(mockCreateSearchIndex).toHaveBeenCalledWith(
|
||||
expectedTextSearchIndexDefinition,
|
||||
);
|
||||
});
|
||||
|
||||
it("should not recreate a text search index that already has textLemmatized", async () => {
|
||||
mockListCollections.mockReturnValue({
|
||||
toArray: jest.fn().mockResolvedValue([{ name: "test_col" }]),
|
||||
});
|
||||
mockListSearchIndexes.mockReturnValue({
|
||||
toArray: jest.fn().mockResolvedValue([
|
||||
{ name: "test_col_vector_index" },
|
||||
{
|
||||
name: "test_col_text_search_index",
|
||||
latestDefinition: expectedTextSearchIndexDefinition.definition,
|
||||
},
|
||||
]),
|
||||
});
|
||||
|
||||
await store.initialize();
|
||||
|
||||
expect(mockDropSearchIndex).not.toHaveBeenCalled();
|
||||
expect(mockCreateSearchIndex).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should map payload.textLemmatized in the text search index", async () => {
|
||||
await store.initialize();
|
||||
|
||||
const textIndexCall = mockCreateSearchIndex.mock.calls.find(
|
||||
([arg]: any[]) => arg.name === "test_col_text_search_index",
|
||||
);
|
||||
expect(textIndexCall).toBeDefined();
|
||||
expect(textIndexCall![0].definition.mappings.fields.payload.fields).toEqual(
|
||||
{
|
||||
data: { type: "string" },
|
||||
text_lemmatized: { type: "string" },
|
||||
textLemmatized: { type: "string" },
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("should insert documents correctly", async () => {
|
||||
@@ -197,7 +295,11 @@ describe("MongoDB Vector Store", () => {
|
||||
index: "test_col_text_search_index",
|
||||
text: {
|
||||
query: "test",
|
||||
path: ["payload.data", "payload.text_lemmatized"],
|
||||
path: [
|
||||
"payload.data",
|
||||
"payload.text_lemmatized",
|
||||
"payload.textLemmatized",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -12,8 +12,9 @@ const upsert = jest.fn().mockResolvedValue(undefined);
|
||||
const query = jest.fn().mockResolvedValue({ matches: [] });
|
||||
const fetch = jest.fn().mockResolvedValue({ records: {} });
|
||||
const deleteOne = jest.fn().mockResolvedValue(undefined);
|
||||
const deleteAll = jest.fn().mockResolvedValue(undefined);
|
||||
|
||||
const nsHandle = { upsert, query, fetch, deleteOne };
|
||||
const nsHandle = { upsert, query, fetch, deleteOne, deleteAll };
|
||||
const namespace = jest.fn().mockReturnValue(nsHandle);
|
||||
|
||||
const describeIndexStats = jest
|
||||
@@ -47,6 +48,7 @@ const __mocks__ = {
|
||||
query,
|
||||
fetch,
|
||||
deleteOne,
|
||||
deleteAll,
|
||||
namespace,
|
||||
describeIndexStats,
|
||||
index,
|
||||
@@ -94,6 +96,7 @@ beforeEach(() => {
|
||||
__mocks__.query.mockResolvedValue({ matches: [] });
|
||||
__mocks__.fetch.mockResolvedValue({ records: {} });
|
||||
__mocks__.deleteOne.mockResolvedValue(undefined);
|
||||
__mocks__.deleteAll.mockResolvedValue(undefined);
|
||||
__mocks__.describeIndexStats.mockResolvedValue({
|
||||
totalRecordCount: 0,
|
||||
namespaces: {},
|
||||
@@ -104,6 +107,7 @@ beforeEach(() => {
|
||||
query: __mocks__.query,
|
||||
fetch: __mocks__.fetch,
|
||||
deleteOne: __mocks__.deleteOne,
|
||||
deleteAll: __mocks__.deleteAll,
|
||||
};
|
||||
__mocks__.namespace.mockReturnValue(nsHandle);
|
||||
__mocks__.index.mockReturnValue({
|
||||
@@ -424,6 +428,14 @@ describe("deleteCol", () => {
|
||||
await db.initialize();
|
||||
expect(__mocks__.createIndex).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("clears only the namespace and never drops the shared index", async () => {
|
||||
const db = await initDb({ namespace: "tenant-a" });
|
||||
await db.deleteCol();
|
||||
expect(__mocks__.namespace).toHaveBeenCalledWith("tenant-a");
|
||||
expect(__mocks__.deleteAll).toHaveBeenCalled();
|
||||
expect(__mocks__.deleteIndex).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("list", () => {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export async function loadPeer(
|
||||
pkg: string,
|
||||
label: string,
|
||||
load: () => Promise<any>,
|
||||
): Promise<any> {
|
||||
try {
|
||||
return await load();
|
||||
} catch {
|
||||
throw new Error(
|
||||
`The '${pkg}' package is required to use the ${label}. Install it with: npm install ${pkg}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import {
|
||||
import type {
|
||||
SearchClient,
|
||||
SearchIndexClient,
|
||||
AzureKeyCredential,
|
||||
SearchIndex,
|
||||
SearchField,
|
||||
SearchFieldDataType,
|
||||
@@ -13,9 +12,9 @@ import {
|
||||
BinaryQuantizationCompression,
|
||||
VectorizedQuery,
|
||||
} from "@azure/search-documents";
|
||||
import { DefaultAzureCredential } from "@azure/identity";
|
||||
import { VectorStore } from "./base";
|
||||
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
/**
|
||||
* Configuration interface for Azure AI Search vector store
|
||||
@@ -71,8 +70,8 @@ interface AzureAISearchConfig extends VectorStoreConfig {
|
||||
* Supports vector search with hybrid search, compression, and filtering
|
||||
*/
|
||||
export class AzureAISearch implements VectorStore {
|
||||
private searchClient: SearchClient<any>;
|
||||
private indexClient: SearchIndexClient;
|
||||
private searchClient!: SearchClient<any>;
|
||||
private indexClient!: SearchIndexClient;
|
||||
private readonly serviceName: string;
|
||||
private readonly indexName: string;
|
||||
private readonly embeddingModelDims: number;
|
||||
@@ -93,25 +92,42 @@ export class AzureAISearch implements VectorStore {
|
||||
this.vectorFilterMode = config.vectorFilterMode || "preFilter";
|
||||
this.apiKey = config.apiKey;
|
||||
|
||||
// Initialize the index
|
||||
this.initialize().catch(console.error);
|
||||
}
|
||||
|
||||
private async ensureClient(): Promise<void> {
|
||||
if (this.searchClient) return;
|
||||
const searchSdk = await loadPeer(
|
||||
"@azure/search-documents",
|
||||
"Azure AI Search vector store",
|
||||
() => import("@azure/search-documents"),
|
||||
);
|
||||
|
||||
const serviceEndpoint = `https://${this.serviceName}.search.windows.net`;
|
||||
|
||||
// Determine authentication: API key or DefaultAzureCredential
|
||||
const credential =
|
||||
this.apiKey && this.apiKey !== "" && this.apiKey !== "your-api-key"
|
||||
? new AzureKeyCredential(this.apiKey)
|
||||
: new DefaultAzureCredential();
|
||||
let credential: any;
|
||||
if (this.apiKey && this.apiKey !== "" && this.apiKey !== "your-api-key") {
|
||||
credential = new searchSdk.AzureKeyCredential(this.apiKey);
|
||||
} else {
|
||||
const identitySdk = await loadPeer(
|
||||
"@azure/identity",
|
||||
"Azure AI Search without an apiKey",
|
||||
() => import("@azure/identity"),
|
||||
);
|
||||
credential = new identitySdk.DefaultAzureCredential();
|
||||
}
|
||||
|
||||
// Initialize clients
|
||||
this.searchClient = new SearchClient(
|
||||
this.searchClient = new searchSdk.SearchClient(
|
||||
serviceEndpoint,
|
||||
this.indexName,
|
||||
credential,
|
||||
);
|
||||
|
||||
this.indexClient = new SearchIndexClient(serviceEndpoint, credential);
|
||||
|
||||
// Initialize the index
|
||||
this.initialize().catch(console.error);
|
||||
this.indexClient = new searchSdk.SearchIndexClient(
|
||||
serviceEndpoint,
|
||||
credential,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,6 +141,7 @@ export class AzureAISearch implements VectorStore {
|
||||
}
|
||||
|
||||
private async _doInitialize(): Promise<void> {
|
||||
await this.ensureClient();
|
||||
try {
|
||||
const collections = await this.listCols();
|
||||
if (!collections.includes(this.indexName)) {
|
||||
@@ -262,6 +279,7 @@ export class AzureAISearch implements VectorStore {
|
||||
ids: string[],
|
||||
payloads: Record<string, any>[],
|
||||
): Promise<void> {
|
||||
await this.initialize();
|
||||
console.log(
|
||||
`Inserting ${vectors.length} vectors into index ${this.indexName}`,
|
||||
);
|
||||
@@ -334,6 +352,7 @@ export class AzureAISearch implements VectorStore {
|
||||
topK: number = 5,
|
||||
filters?: SearchFilters,
|
||||
): Promise<VectorStoreResult[] | null> {
|
||||
await this.initialize();
|
||||
try {
|
||||
const filterExpression = filters
|
||||
? this.buildFilterExpression(filters)
|
||||
@@ -373,6 +392,7 @@ export class AzureAISearch implements VectorStore {
|
||||
topK: number = 5,
|
||||
filters?: SearchFilters,
|
||||
): Promise<VectorStoreResult[]> {
|
||||
await this.initialize();
|
||||
const filterExpression = filters
|
||||
? this.buildFilterExpression(filters)
|
||||
: undefined;
|
||||
@@ -429,6 +449,7 @@ export class AzureAISearch implements VectorStore {
|
||||
* Delete a vector by ID
|
||||
*/
|
||||
async delete(vectorId: string): Promise<void> {
|
||||
await this.initialize();
|
||||
const response = await this.searchClient.deleteDocuments([
|
||||
{ id: vectorId },
|
||||
]);
|
||||
@@ -454,6 +475,7 @@ export class AzureAISearch implements VectorStore {
|
||||
vector: number[],
|
||||
payload: Record<string, any>,
|
||||
): Promise<void> {
|
||||
await this.initialize();
|
||||
const document: Record<string, any> = { id: vectorId };
|
||||
|
||||
if (vector) {
|
||||
@@ -486,6 +508,7 @@ export class AzureAISearch implements VectorStore {
|
||||
* Retrieve a vector by ID
|
||||
*/
|
||||
async get(vectorId: string): Promise<VectorStoreResult | null> {
|
||||
await this.initialize();
|
||||
try {
|
||||
const result = await this.searchClient.getDocument(vectorId);
|
||||
const payloadStr = result.payload as string;
|
||||
@@ -521,6 +544,7 @@ export class AzureAISearch implements VectorStore {
|
||||
* Delete the index
|
||||
*/
|
||||
async deleteCol(): Promise<void> {
|
||||
await this.initialize();
|
||||
await this.indexClient.deleteIndex(this.indexName);
|
||||
}
|
||||
|
||||
@@ -542,6 +566,7 @@ export class AzureAISearch implements VectorStore {
|
||||
filters?: SearchFilters,
|
||||
topK: number = 100,
|
||||
): Promise<[VectorStoreResult[], number]> {
|
||||
await this.initialize();
|
||||
const filterExpression = filters
|
||||
? this.buildFilterExpression(filters)
|
||||
: undefined;
|
||||
@@ -586,6 +611,7 @@ export class AzureAISearch implements VectorStore {
|
||||
* Required by VectorStore interface
|
||||
*/
|
||||
async getUserId(): Promise<string> {
|
||||
await this.initialize();
|
||||
try {
|
||||
// Check if memory_migrations index exists
|
||||
const collections = await this.listCols();
|
||||
@@ -648,6 +674,7 @@ export class AzureAISearch implements VectorStore {
|
||||
* Required by VectorStore interface
|
||||
*/
|
||||
async setUserId(userId: string): Promise<void> {
|
||||
await this.initialize();
|
||||
try {
|
||||
// Get existing point ID or generate new one
|
||||
const searchResults = await this.searchClient.search("*", {
|
||||
@@ -677,6 +704,7 @@ export class AzureAISearch implements VectorStore {
|
||||
* Reset the index by deleting and recreating it
|
||||
*/
|
||||
async reset(): Promise<void> {
|
||||
await this.initialize();
|
||||
console.log(`Resetting index ${this.indexName}...`);
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Pool, RowDataPacket } from "mysql2/promise";
|
||||
import { VectorStore } from "./base";
|
||||
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
const SAFE_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,127}$/;
|
||||
|
||||
@@ -94,14 +95,11 @@ export class AzureMySQLDB implements VectorStore {
|
||||
|
||||
// Loaded dynamically: mysql2 is an optional peer dependency, so a static value import
|
||||
// would break `import { Memory } from "mem0ai/oss"` for everyone else.
|
||||
let createPool: typeof import("mysql2/promise").createPool;
|
||||
try {
|
||||
({ createPool } = await import("mysql2/promise"));
|
||||
} catch {
|
||||
throw new Error(
|
||||
"The Azure MySQL vector store requires the 'mysql2' package. Install it with: npm install mysql2",
|
||||
);
|
||||
}
|
||||
const { createPool }: typeof import("mysql2/promise") = await loadPeer(
|
||||
"mysql2",
|
||||
"Azure MySQL vector store",
|
||||
() => import("mysql2/promise"),
|
||||
);
|
||||
|
||||
this.pool = createPool({
|
||||
host: this.config.host,
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
} from "@mochow/mochow-sdk-node";
|
||||
import { VectorStore } from "./base";
|
||||
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
type MochowSdk = typeof import("@mochow/mochow-sdk-node");
|
||||
|
||||
@@ -156,14 +157,11 @@ export class BaiduDB implements VectorStore {
|
||||
// value import would break `import { Memory } from "mem0ai/oss"` for everyone else.
|
||||
private async loadSdk(): Promise<MochowSdk> {
|
||||
if (!this.sdk) {
|
||||
let module: MochowSdk & { default?: MochowSdk };
|
||||
try {
|
||||
module = await import("@mochow/mochow-sdk-node");
|
||||
} catch {
|
||||
throw new Error(
|
||||
"The Baidu vector store requires the '@mochow/mochow-sdk-node' package. Install it with: npm install @mochow/mochow-sdk-node",
|
||||
);
|
||||
}
|
||||
const module: MochowSdk & { default?: MochowSdk } = await loadPeer(
|
||||
"@mochow/mochow-sdk-node",
|
||||
"Baidu vector store",
|
||||
() => import("@mochow/mochow-sdk-node"),
|
||||
);
|
||||
this.sdk = module.default ?? module;
|
||||
}
|
||||
return this.sdk;
|
||||
@@ -455,7 +453,13 @@ export class BaiduDB implements VectorStore {
|
||||
return (response.rows ?? []).map((result) => ({
|
||||
id: String(result.row.id),
|
||||
payload: resultPayload(result.row),
|
||||
score: result.score,
|
||||
// L2 is a distance (lower = closer); the VectorStore contract wants higher = better.
|
||||
score:
|
||||
this.metricType === "L2"
|
||||
? result.score != null
|
||||
? 1 / (1 + result.score)
|
||||
: undefined
|
||||
: result.score,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { VectorStore } from "./base";
|
||||
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
const MIGRATION_ROW_ID = "mem0-user";
|
||||
const SAFE_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
||||
@@ -377,14 +378,11 @@ export class CassandraDB implements VectorStore {
|
||||
// Loaded dynamically: cassandra-driver is an optional peer dependency, so a static
|
||||
// value import would break `import { Memory } from "mem0ai/oss"` for everyone else.
|
||||
private async loadDriver(): Promise<any> {
|
||||
let sdk: any;
|
||||
try {
|
||||
sdk = await import("cassandra-driver");
|
||||
} catch {
|
||||
throw new Error(
|
||||
"The 'cassandra-driver' package is required to use the Cassandra vector store. Install it with: npm install cassandra-driver",
|
||||
);
|
||||
}
|
||||
const sdk = await loadPeer(
|
||||
"cassandra-driver",
|
||||
"Cassandra vector store",
|
||||
() => import("cassandra-driver"),
|
||||
);
|
||||
return sdk.default ?? sdk;
|
||||
}
|
||||
|
||||
@@ -457,44 +455,64 @@ export class CassandraDB implements VectorStore {
|
||||
return value.includes(payloadValue);
|
||||
}
|
||||
|
||||
// Every operator present in a compound condition must hold (AND), so check
|
||||
// them all instead of returning on the first match. Returning early meant a
|
||||
// range like { gte: 10, lte: 20 } only applied `gte`. Mirrors the databricks
|
||||
// store's matcher.
|
||||
let sawOperator = false;
|
||||
|
||||
if ("eq" in value) {
|
||||
return payloadValue === value.eq;
|
||||
sawOperator = true;
|
||||
if (payloadValue !== value.eq) return false;
|
||||
}
|
||||
if ("ne" in value) {
|
||||
return payloadValue !== value.ne;
|
||||
sawOperator = true;
|
||||
if (payloadValue === value.ne) return false;
|
||||
}
|
||||
if ("gt" in value) {
|
||||
return payloadValue > value.gt;
|
||||
sawOperator = true;
|
||||
if (!(payloadValue > value.gt)) return false;
|
||||
}
|
||||
if ("gte" in value) {
|
||||
return payloadValue >= value.gte;
|
||||
sawOperator = true;
|
||||
if (!(payloadValue >= value.gte)) return false;
|
||||
}
|
||||
if ("lt" in value) {
|
||||
return payloadValue < value.lt;
|
||||
sawOperator = true;
|
||||
if (!(payloadValue < value.lt)) return false;
|
||||
}
|
||||
if ("lte" in value) {
|
||||
return payloadValue <= value.lte;
|
||||
sawOperator = true;
|
||||
if (!(payloadValue <= value.lte)) return false;
|
||||
}
|
||||
if ("in" in value) {
|
||||
return Array.isArray(value.in) && value.in.includes(payloadValue);
|
||||
sawOperator = true;
|
||||
if (!Array.isArray(value.in) || !value.in.includes(payloadValue))
|
||||
return false;
|
||||
}
|
||||
if ("nin" in value) {
|
||||
return !Array.isArray(value.nin) || !value.nin.includes(payloadValue);
|
||||
sawOperator = true;
|
||||
if (Array.isArray(value.nin) && value.nin.includes(payloadValue))
|
||||
return false;
|
||||
}
|
||||
if ("contains" in value) {
|
||||
return (
|
||||
typeof payloadValue === "string" &&
|
||||
payloadValue.includes(value.contains)
|
||||
);
|
||||
sawOperator = true;
|
||||
if (
|
||||
typeof payloadValue !== "string" ||
|
||||
!payloadValue.includes(value.contains)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
if ("icontains" in value) {
|
||||
return (
|
||||
typeof payloadValue === "string" &&
|
||||
payloadValue.toLowerCase().includes(value.icontains.toLowerCase())
|
||||
);
|
||||
sawOperator = true;
|
||||
if (
|
||||
typeof payloadValue !== "string" ||
|
||||
!payloadValue.toLowerCase().includes(value.icontains.toLowerCase())
|
||||
)
|
||||
return false;
|
||||
}
|
||||
|
||||
return payloadValue === value;
|
||||
return sawOperator ? true : payloadValue === value;
|
||||
}
|
||||
|
||||
private filterVector(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ChromaClient, CloudClient } from "chromadb";
|
||||
import { VectorStore } from "./base";
|
||||
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
interface ChromaConfig extends VectorStoreConfig {
|
||||
/** Pre-configured ChromaDB client instance. */
|
||||
@@ -65,14 +66,11 @@ export class ChromaDB implements VectorStore {
|
||||
return config.client;
|
||||
}
|
||||
|
||||
let sdk: any;
|
||||
try {
|
||||
sdk = await import("chromadb");
|
||||
} catch {
|
||||
throw new Error(
|
||||
"The 'chromadb' package is required to use the Chroma vector store. Install it with: npm install chromadb",
|
||||
);
|
||||
}
|
||||
const sdk = await loadPeer(
|
||||
"chromadb",
|
||||
"Chroma vector store",
|
||||
() => import("chromadb"),
|
||||
);
|
||||
|
||||
if (config.apiKey && config.tenant) {
|
||||
return new sdk.CloudClient({
|
||||
@@ -275,14 +273,14 @@ export class ChromaDB implements VectorStore {
|
||||
private static convertCondition(
|
||||
key: string,
|
||||
value: any,
|
||||
): Record<string, any> | null {
|
||||
): Array<Record<string, any>> {
|
||||
// Wildcard - ChromaDB has no direct wildcard, so skip this filter.
|
||||
if (value === "*") {
|
||||
return null;
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return { [key]: { $in: value } };
|
||||
return [{ [key]: { $in: value } }];
|
||||
}
|
||||
|
||||
if (value !== null && typeof value === "object") {
|
||||
@@ -296,19 +294,31 @@ export class ChromaDB implements VectorStore {
|
||||
in: "$in",
|
||||
nin: "$nin",
|
||||
};
|
||||
const condition: Record<string, any> = {};
|
||||
for (const [op, val] of Object.entries(value)) {
|
||||
if (op in opMap) {
|
||||
condition[key] = { [opMap[op]]: val };
|
||||
} else {
|
||||
// contains/icontains and unknown operators fall back to equality.
|
||||
condition[key] = { $eq: val };
|
||||
}
|
||||
}
|
||||
return condition;
|
||||
// ChromaDB allows exactly one operator per field expression, so each
|
||||
// operator becomes its own clause (combined with $and by the caller).
|
||||
// Previously each operator overwrote the last, silently dropping range
|
||||
// bounds. contains/icontains and unknown operators fall back to
|
||||
// equality.
|
||||
return Object.entries(value).map(([op, val]) => ({
|
||||
[key]: { [opMap[op] ?? "$eq"]: val },
|
||||
}));
|
||||
}
|
||||
|
||||
return { [key]: { $eq: value } };
|
||||
return [{ [key]: { $eq: value } }];
|
||||
}
|
||||
|
||||
/** Combine clauses under a logical operator, unwrapping singletons. */
|
||||
private static combineClauses(
|
||||
clauses: Array<Record<string, any>>,
|
||||
operator: "$and" | "$or",
|
||||
): Record<string, any> | null {
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (clauses.length === 1) {
|
||||
return clauses[0];
|
||||
}
|
||||
return { [operator]: clauses };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -339,66 +349,55 @@ export class ChromaDB implements VectorStore {
|
||||
if (key === "$or" || key === "OR") {
|
||||
const orConditions: any[] = [];
|
||||
for (const condition of value as any[]) {
|
||||
const built: Record<string, any> = {};
|
||||
const subClauses: Array<Record<string, any>> = [];
|
||||
for (const [subKey, subValue] of Object.entries(condition)) {
|
||||
const converted = ChromaDB.convertCondition(subKey, subValue);
|
||||
if (converted) Object.assign(built, converted);
|
||||
subClauses.push(...ChromaDB.convertCondition(subKey, subValue));
|
||||
}
|
||||
if (Object.keys(built).length > 0) orConditions.push(built);
|
||||
}
|
||||
if (orConditions.length > 1) {
|
||||
processed.push({ $or: orConditions });
|
||||
} else if (orConditions.length === 1) {
|
||||
processed.push(orConditions[0]);
|
||||
// Multi-field conditions must be wrapped in $and — ChromaDB rejects
|
||||
// flat objects with more than one field per level.
|
||||
const combined = ChromaDB.combineClauses(subClauses, "$and");
|
||||
if (combined) orConditions.push(combined);
|
||||
}
|
||||
const combinedOr = ChromaDB.combineClauses(orConditions, "$or");
|
||||
if (combinedOr) processed.push(combinedOr);
|
||||
} else if (key === "$not" || key === "NOT") {
|
||||
// De Morgan: NOT(a AND b) is (NOT a) OR (NOT b), so the negated fields
|
||||
// within one condition are combined with $or, and separate conditions
|
||||
// are combined with $and. This mirrors the Python SDK's ChromaDB port.
|
||||
const negatedPerGroup: any[] = [];
|
||||
for (const condition of value as any[]) {
|
||||
const negatedFields: any[] = [];
|
||||
const negatedFields: Array<Record<string, any>> = [];
|
||||
for (const [subKey, subValue] of Object.entries(condition)) {
|
||||
if (subValue !== null && typeof subValue === "object") {
|
||||
for (const [op, val] of Object.entries(subValue as any)) {
|
||||
const neg = negateOp[op];
|
||||
if (neg) {
|
||||
const converted = ChromaDB.convertCondition(subKey, {
|
||||
[neg]: val,
|
||||
});
|
||||
if (converted) negatedFields.push(converted);
|
||||
}
|
||||
// Unknown operators mirror the positive-path equality
|
||||
// fallback as inequality (previously they were silently
|
||||
// dropped, which could erase the entire NOT clause).
|
||||
const neg = negateOp[op] ?? "ne";
|
||||
negatedFields.push(
|
||||
...ChromaDB.convertCondition(subKey, { [neg]: val }),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const converted = ChromaDB.convertCondition(subKey, {
|
||||
ne: subValue,
|
||||
});
|
||||
if (converted) negatedFields.push(converted);
|
||||
negatedFields.push(
|
||||
...ChromaDB.convertCondition(subKey, { ne: subValue }),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (negatedFields.length > 1) {
|
||||
negatedPerGroup.push({ $or: negatedFields });
|
||||
} else if (negatedFields.length === 1) {
|
||||
negatedPerGroup.push(negatedFields[0]);
|
||||
}
|
||||
}
|
||||
if (negatedPerGroup.length > 1) {
|
||||
processed.push({ $and: negatedPerGroup });
|
||||
} else if (negatedPerGroup.length === 1) {
|
||||
processed.push(negatedPerGroup[0]);
|
||||
const combined = ChromaDB.combineClauses(negatedFields, "$or");
|
||||
if (combined) negatedPerGroup.push(combined);
|
||||
}
|
||||
const combinedNot = ChromaDB.combineClauses(negatedPerGroup, "$and");
|
||||
if (combinedNot) processed.push(combinedNot);
|
||||
} else {
|
||||
const converted = ChromaDB.convertCondition(key, value);
|
||||
if (converted) processed.push(converted);
|
||||
const combined = ChromaDB.combineClauses(
|
||||
ChromaDB.convertCondition(key, value),
|
||||
"$and",
|
||||
);
|
||||
if (combined) processed.push(combined);
|
||||
}
|
||||
}
|
||||
|
||||
if (processed.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (processed.length === 1) {
|
||||
return processed[0];
|
||||
}
|
||||
return { $and: processed };
|
||||
return ChromaDB.combineClauses(processed, "$and") ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Client } from "@elastic/elasticsearch";
|
||||
import { VectorStore } from "./base";
|
||||
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
interface ElasticsearchConfig extends VectorStoreConfig {
|
||||
/** Pre-configured Elasticsearch client instance (typed as `any` to keep the
|
||||
@@ -100,14 +101,11 @@ export class ElasticsearchDB implements VectorStore {
|
||||
params.headers = config.headers;
|
||||
}
|
||||
|
||||
let sdk: any;
|
||||
try {
|
||||
sdk = await import("@elastic/elasticsearch");
|
||||
} catch {
|
||||
throw new Error(
|
||||
"The '@elastic/elasticsearch' package is required to use the Elasticsearch vector store. Install it with: npm install @elastic/elasticsearch",
|
||||
);
|
||||
}
|
||||
const sdk = await loadPeer(
|
||||
"@elastic/elasticsearch",
|
||||
"Elasticsearch vector store",
|
||||
() => import("@elastic/elasticsearch"),
|
||||
);
|
||||
|
||||
this.client = new sdk.Client(params);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { VectorStore as LangchainVectorStoreInterface } from "@langchain/core/vectorstores";
|
||||
import { Document } from "@langchain/core/documents";
|
||||
import type { VectorStore as LangchainVectorStoreInterface } from "@langchain/core/vectorstores";
|
||||
import { VectorStore } from "./base"; // mem0's VectorStore interface
|
||||
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
|
||||
|
||||
@@ -77,6 +76,7 @@ export class LangchainVectorStore implements VectorStore {
|
||||
}
|
||||
|
||||
// Convert payloads to Langchain Document metadata format
|
||||
const { Document } = await import("@langchain/core/documents");
|
||||
const documents = payloads.map((payload, i) => {
|
||||
// Provide empty pageContent, store mem0 id and other data in metadata
|
||||
return new Document({
|
||||
|
||||
@@ -235,6 +235,13 @@ export class Milvus implements VectorStore {
|
||||
if (!Milvus.SAFE_FILTER_KEY.test(key)) {
|
||||
throw new Error(`Invalid filter key: ${JSON.stringify(key)}`);
|
||||
}
|
||||
if (value === "*") {
|
||||
// Wildcard - match any value. Milvus has no direct wildcard, so skip
|
||||
// the clause rather than emitting a literal `== "*"` that matches
|
||||
// nothing. Mirrors the Python provider (#6187) and the chroma/pinecone
|
||||
// stores.
|
||||
continue;
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
// Escape backslashes before quotes so a value can't break out of the
|
||||
// string literal (order matters, exactly as in the Python provider).
|
||||
@@ -252,13 +259,13 @@ export class Milvus implements VectorStore {
|
||||
}
|
||||
|
||||
/**
|
||||
* Text fed to the BM25 sparse index for a payload. Prefers the lemmatized
|
||||
* text, falls back to the raw memory `data`, and truncates to the VarChar
|
||||
* limit (mirrors the Python provider).
|
||||
* Text fed to the BM25 sparse index for a payload. Prefers `textLemmatized`,
|
||||
* then `text_lemmatized`, then raw `data`; truncates to the VarChar limit.
|
||||
*/
|
||||
private bm25Text(payload?: Record<string, any>): string {
|
||||
if (!payload) return "";
|
||||
const raw = payload.text_lemmatized || payload.data || "";
|
||||
const raw =
|
||||
payload.textLemmatized || payload.text_lemmatized || payload.data || "";
|
||||
return String(raw).slice(0, 65535);
|
||||
}
|
||||
|
||||
|
||||