Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9897951783 |
@@ -12,7 +12,7 @@
|
||||
"name": "mem0",
|
||||
"source": "./integrations/mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search to Claude workflows.",
|
||||
"version": "0.2.13"
|
||||
"version": "0.2.12"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"name": "mem0",
|
||||
"source": "./integrations/mem0-plugin",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search.",
|
||||
"version": "0.2.13"
|
||||
"version": "0.2.12"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,10 +9,12 @@ body:
|
||||
label: Component
|
||||
description: Which part of mem0 is affected?
|
||||
options:
|
||||
- Python SDK
|
||||
- Core / Python SDK
|
||||
- TypeScript SDK
|
||||
- Vector Store
|
||||
- Plugin
|
||||
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
|
||||
- Graph Memory (Neo4j, Memgraph, etc.)
|
||||
- Ollama / Local Models
|
||||
- OpenClaw
|
||||
- REST API
|
||||
- Other
|
||||
validations:
|
||||
|
||||
@@ -9,11 +9,14 @@ body:
|
||||
label: Component
|
||||
description: Which part of mem0 does this relate to?
|
||||
options:
|
||||
- Python SDK
|
||||
- Core / Python SDK
|
||||
- TypeScript SDK
|
||||
- Vector Store
|
||||
- Plugin
|
||||
- Vector Store (Qdrant, PGVector, Redis, Chroma, etc.)
|
||||
- Graph Memory (Neo4j, Memgraph, etc.)
|
||||
- Ollama / Local Models
|
||||
- OpenClaw
|
||||
- REST API
|
||||
- Benchmarks / Evals
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
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']
|
||||
# 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"
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"openmemory": [
|
||||
"openmemory", "open memory", "localhost:8765", "localhost:3000",
|
||||
"openmemory ui", "openmemory/api", "openmemory/ui"
|
||||
],
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
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/**'
|
||||
|
||||
openmemory:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: 'openmemory/**'
|
||||
|
||||
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'
|
||||
@@ -1,44 +0,0 @@
|
||||
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 };
|
||||
@@ -1,108 +0,0 @@
|
||||
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: 3444,
|
||||
title: "Fix: Openmemory run.sh non-existent vector-store route",
|
||||
body: "### 🐛 Describe the bug\n\n# Vector_store not implemented\nThere is many references to ` ${NEXT_PUBLIC_API_URL}/api/v1/config/mem0/vector_store` in lines 280, 293, 306, 319, 332, 345, 358, and 371. \n```bash\ncurl -fsS -X PUT \"${NEXT_PUBLIC_API_URL}/api/v1/config/mem0/vector_store\" # Line 280 and for each vector store\n```\nBut the api route is not implemented in `api/app/routers/config.py`.\n# Suggested solution\nI would implement `vector_store` route or remove and use `update_configuration` for all config updates. Also Create class with all config keys for vector_store",
|
||||
expected: ["openmemory"],
|
||||
},
|
||||
{
|
||||
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.`);
|
||||
@@ -12,56 +12,28 @@ 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
|
||||
|
||||
- name: Infer component from text when the form was not used
|
||||
uses: actions/github-script@v7
|
||||
- uses: stefanbuck/github-issue-parser@v3
|
||||
id: feature-parser
|
||||
if: contains(github.event.issue.labels.*.name, 'enhancement')
|
||||
with:
|
||||
script: |
|
||||
const {
|
||||
componentLabels,
|
||||
inferComponentLabels,
|
||||
loadKeywords,
|
||||
} = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/infer-component-labels.js`);
|
||||
template-path: .github/ISSUE_TEMPLATE/feature_request.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,
|
||||
});
|
||||
- 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
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
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', 'openmemory', '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],
|
||||
});
|
||||
}
|
||||
@@ -462,7 +462,6 @@ 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`. |
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/cli",
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.10",
|
||||
"description": "The official CLI for mem0 — the memory layer for AI agents",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0-cli"
|
||||
version = "0.2.10"
|
||||
version = "0.2.9"
|
||||
description = "The official CLI for mem0 — the memory layer for AI agents"
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""mem0 CLI — the command-line interface for the mem0 memory layer."""
|
||||
|
||||
__version__ = "0.2.10"
|
||||
__version__ = "0.2.9"
|
||||
|
||||
@@ -79,7 +79,7 @@ new_project = client.project.create(
|
||||
|
||||
### Update Project Settings
|
||||
|
||||
Modify project configuration including custom instructions, categories, language preferences, and memory decay:
|
||||
Modify project configuration including custom instructions, categories, language preferences, retrieval criteria, and memory decay:
|
||||
|
||||
```python
|
||||
# Update project with custom categories
|
||||
@@ -98,6 +98,14 @@ client.project.update(
|
||||
# Use the input language for memory storage and retrieval
|
||||
client.project.update(multilingual=True)
|
||||
|
||||
# Set retrieval criteria to control which memories are surfaced in search
|
||||
client.project.update(
|
||||
retrieval_criteria=[
|
||||
{"name": "relevance", "description": "How directly relevant this memory is to the current topic or user query", "weight": 3},
|
||||
{"name": "access_frequency", "description": "How often this memory has been accessed or surfaced recently", "weight": 1}
|
||||
]
|
||||
)
|
||||
|
||||
# Enable Memory Decay (boosts recently-accessed memories at search time)
|
||||
client.project.update(decay=True)
|
||||
|
||||
@@ -112,6 +120,34 @@ client.project.update(
|
||||
)
|
||||
```
|
||||
|
||||
#### Set Retrieval Criteria
|
||||
|
||||
`retrieval_criteria` is a per-project list of dictionaries (`List[Dict]`) that shapes how memories are ranked and filtered during search. Each dictionary has three fields: `name` (identifier), `description` (interpreted by the LLM to score each memory), and `weight` (relative influence on the final score). Use this to focus retrieval on intent-aligned or signal-specific memories:
|
||||
|
||||
```python
|
||||
client.project.update(
|
||||
retrieval_criteria=[
|
||||
{
|
||||
"name": "joy",
|
||||
"description": "Measure the intensity of positive emotions such as happiness, excitement, or amusement expressed in the memory. A higher score reflects greater joy.",
|
||||
"weight": 3
|
||||
},
|
||||
{
|
||||
"name": "curiosity",
|
||||
"description": "Assess the extent to which the memory reflects inquisitiveness or interest in exploring new information. A higher score reflects stronger curiosity.",
|
||||
"weight": 2
|
||||
},
|
||||
{
|
||||
"name": "access_frequency",
|
||||
"description": "How often this memory has been accessed or surfaced recently.",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
Pass an empty list to clear all criteria and restore default retrieval behaviour.
|
||||
|
||||
#### Toggle Memory Decay
|
||||
|
||||
`decay` is a per-project boolean that turns on [Memory Decay](/platform/features/memory-decay): a search-time ranking bias that reinforces recently-accessed memories and gently dampens stale ones. The flag is `false` by default; set it via the same project-update endpoint:
|
||||
|
||||
@@ -4,22 +4,6 @@ description: "Major product launches, headline features, and milestones for Mem0
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
<Update label="2026-07-13" description="TypeScript provider expansion">
|
||||
|
||||
**TypeScript OSS SDK: 26 New Providers, Reranking, and Zero-Dependency Imports**
|
||||
|
||||
TypeScript SDK v3.1.0 is the largest provider release for the OSS SDK so far, closing most of the remaining gap with the Python SDK. Python SDK v2.0.12 ships alongside it with fixes and security patches.
|
||||
|
||||
- **17 new vector stores:** Pinecone, Weaviate, Milvus, Chroma, MongoDB, Elasticsearch, OpenSearch, Databricks, AWS Neptune Analytics, S3 Vectors, Azure MySQL, Google Vertex AI Vector Search, Turbopuffer, Upstash Vector, Valkey, Cassandra, and Baidu Mochow.
|
||||
- **5 new LLM providers:** AWS Bedrock, xAI Grok, Together, vLLM, and Sarvam.
|
||||
- **4 new embedding providers:** Vertex AI, HuggingFace, FastEmbed, and Together.
|
||||
- **Reranking in TypeScript:** Four rerankers (Cohere, ZeroEntropy, cross-encoder, and LLM-based) with per-search rerank via a `rerank` option on `search()`.
|
||||
- **Install only what you use:** Importing `mem0ai/oss` no longer pulls in any provider SDK. Provider packages are resolved lazily on first use, so an app that configures only OpenAI and Qdrant does not need the other provider SDKs installed.
|
||||
|
||||
See [SDK & Tools](/changelog/sdk) for version details and PR links.
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-27" description="SDK memory expiration">
|
||||
|
||||
**SDK Memory Expiration: Expiring Memories Across Python and TypeScript**
|
||||
|
||||
@@ -7,51 +7,6 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<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:**
|
||||
- **Memory (OSS):** Accept `text` in `Memory.update()` and `AsyncMemory.update()`. `data` still works but is now deprecated, so prefer `text` in new code ([#6044](https://github.com/mem0ai/mem0/pull/6044))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Core:** Coerce non-string entity IDs (`user_id`, `agent_id`, `run_id`) instead of crashing on `.strip()`, so passing an integer ID no longer raises `AttributeError` ([#6206](https://github.com/mem0ai/mem0/pull/6206))
|
||||
- **Core:** Stop requiring `langchain-core` for the default async procedural memory path. The optional dependency is now only imported when you pass a custom LangChain LLM, matching the sync behavior ([#6209](https://github.com/mem0ai/mem0/pull/6209))
|
||||
- **Client:** Encode dynamic URL path segments so IDs containing special characters no longer produce malformed requests ([#5963](https://github.com/mem0ai/mem0/pull/5963))
|
||||
- **LLMs:** Skip `temperature` and `top_p` for newer Anthropic models that reject sampling parameters. Detection is automatic per model family and version, and the new `enable_sampling_parameters` config flag overrides it ([#6211](https://github.com/mem0ai/mem0/pull/6211))
|
||||
- **Vector Stores:** Stop writing internal `OutputData` model fields as properties on Weaviate `update()` ([#6149](https://github.com/mem0ai/mem0/pull/6149))
|
||||
- **Vector Stores:** Improve wildcard search handling in Milvus ([#6187](https://github.com/mem0ai/mem0/pull/6187))
|
||||
- **Vector Stores:** Keep env-resolved Upstash Vector credentials after config validation. An env-var-only config previously passed validation and then failed to build ([#5811](https://github.com/mem0ai/mem0/pull/5811))
|
||||
- **Vector Stores:** Restore the previous payload when a Neptune Analytics vector upsert fails inside `update()`, so a partial write can no longer leave the payload and embedding out of sync ([#5824](https://github.com/mem0ai/mem0/pull/5824))
|
||||
|
||||
**Changes:**
|
||||
- **LLMs:** The Together default model is now `MiniMaxAI/MiniMax-M3` (was `mistralai/Mixtral-8x7B-Instruct-v0.1`) ([#6049](https://github.com/mem0ai/mem0/pull/6049))
|
||||
- **LLMs:** The xAI default model is now `grok-4.3` (was `grok-2-latest`) ([#6115](https://github.com/mem0ai/mem0/pull/6115))
|
||||
- **Embeddings:** The Together default embedding model is now `intfloat/multilingual-e5-large-instruct` at 1024 dimensions (was `togethercomputer/m2-bert-80M-8k-retrieval` at 768). If you use the Together embedder without pinning `model`, existing vectors were written at the old dimension: either re-embed them, or pin `model` and `embedding_dims` to the old values ([#5989](https://github.com/mem0ai/mem0/pull/5989))
|
||||
- **Rerankers:** The Cohere default rerank model is now `rerank-v3.5` (was `rerank-english-v3.0`) ([#6055](https://github.com/mem0ai/mem0/pull/6055))
|
||||
|
||||
**Security:**
|
||||
- **Vector Stores:** Fix SQL and Cypher injection vulnerabilities in the PGVector, Azure MySQL, and Neptune providers ([#4878](https://github.com/mem0ai/mem0/pull/4878))
|
||||
- **Vector Stores:** Validate Elasticsearch filter keys and values to prevent term query injection ([#5980](https://github.com/mem0ai/mem0/pull/5980))
|
||||
- **Dependencies:** Require `transformers>=5.3.0` to remediate GHSA-29pf-2h5f-8g72 (CVE-2026-4372) ([#6110](https://github.com/mem0ai/mem0/pull/6110))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-07-01" description="v2.0.11">
|
||||
|
||||
**Bug Fixes:**
|
||||
@@ -1145,49 +1100,6 @@ See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-
|
||||
|
||||
<Tab title="TypeScript">
|
||||
|
||||
<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.
|
||||
|
||||
**New Features:**
|
||||
- **Rerankers:** Add reranking to the OSS SDK with four providers (Cohere, ZeroEntropy, cross-encoder, and LLM-based), plus per-search rerank via a `rerank` option on `search()` ([#6055](https://github.com/mem0ai/mem0/pull/6055))
|
||||
- **Memory (OSS):** Accept `text` in `Memory.update()`. `data` still works but is now deprecated, so prefer `text` in new code ([#6044](https://github.com/mem0ai/mem0/pull/6044))
|
||||
- **Vector Stores:** Add Pinecone ([#5802](https://github.com/mem0ai/mem0/pull/5802)), Weaviate ([#5800](https://github.com/mem0ai/mem0/pull/5800)), Milvus ([#5889](https://github.com/mem0ai/mem0/pull/5889)), Chroma ([#6145](https://github.com/mem0ai/mem0/pull/6145)), MongoDB ([#5793](https://github.com/mem0ai/mem0/pull/5793)), Elasticsearch ([#5866](https://github.com/mem0ai/mem0/pull/5866)), and OpenSearch ([#5810](https://github.com/mem0ai/mem0/pull/5810))
|
||||
- **Vector Stores:** Add Databricks ([#5824](https://github.com/mem0ai/mem0/pull/5824)), AWS Neptune Analytics ([#5797](https://github.com/mem0ai/mem0/pull/5797)), S3 Vectors ([#5822](https://github.com/mem0ai/mem0/pull/5822)), Azure MySQL ([#5827](https://github.com/mem0ai/mem0/pull/5827)), and Google Vertex AI Vector Search ([#5791](https://github.com/mem0ai/mem0/pull/5791))
|
||||
- **Vector Stores:** Add Turbopuffer ([#5801](https://github.com/mem0ai/mem0/pull/5801)), Upstash Vector ([#5811](https://github.com/mem0ai/mem0/pull/5811)), Valkey ([#5826](https://github.com/mem0ai/mem0/pull/5826)), Cassandra ([#5823](https://github.com/mem0ai/mem0/pull/5823)), and Baidu Mochow ([#5790](https://github.com/mem0ai/mem0/pull/5790))
|
||||
- **LLMs:** Add AWS Bedrock ([#5890](https://github.com/mem0ai/mem0/pull/5890)), xAI Grok ([#6115](https://github.com/mem0ai/mem0/pull/6115)), Together ([#6049](https://github.com/mem0ai/mem0/pull/6049)), vLLM ([#5805](https://github.com/mem0ai/mem0/pull/5805)), and Sarvam ([#6130](https://github.com/mem0ai/mem0/pull/6130))
|
||||
- **Embeddings:** Add Vertex AI ([#5882](https://github.com/mem0ai/mem0/pull/5882)), HuggingFace ([#6027](https://github.com/mem0ai/mem0/pull/6027)), FastEmbed ([#5862](https://github.com/mem0ai/mem0/pull/5862)), and Together ([#5989](https://github.com/mem0ai/mem0/pull/5989))
|
||||
|
||||
**Improvements:**
|
||||
- **Packaging:** Lazy-load optional provider SDKs so importing `mem0ai/oss` never requires them. Provider packages are now resolved on first use, so an app that only configures OpenAI and Qdrant does not need the other provider SDKs installed ([#6280](https://github.com/mem0ai/mem0/pull/6280))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Memory (OSS):** Re-raise LLM extraction transport failures instead of returning `[]`, so a network error during extraction surfaces as an error rather than a silently empty result ([#6102](https://github.com/mem0ai/mem0/pull/6102))
|
||||
- **Vector Stores:** Prevent an unhandled promise rejection in the Supabase and Redis constructors ([#6111](https://github.com/mem0ai/mem0/pull/6111))
|
||||
- **Client:** Encode dynamic URL path segments so IDs containing special characters no longer produce malformed requests ([#5963](https://github.com/mem0ai/mem0/pull/5963))
|
||||
|
||||
**Security:**
|
||||
- **Dependencies:** Patch the `fast-xml-parser` and `tar` transitive CVEs ([#6160](https://github.com/mem0ai/mem0/pull/6160))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-07-01" description="v3.0.13">
|
||||
|
||||
**Bug Fixes:**
|
||||
@@ -1694,13 +1606,6 @@ See the [TypeScript SDK migration guide](https://docs.mem0.ai/migration/ts-v2-to
|
||||
|
||||
<Tab title="CLI">
|
||||
|
||||
<Update label="2026-07-13" description="Python v0.2.10 / Node v0.2.11">
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Platform backend:** Encode dynamic URL path segments so memory and entity IDs containing special characters no longer produce malformed requests (Python and Node [#5963](https://github.com/mem0ai/mem0/pull/5963))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-07-01" description="Python v0.2.9 / Node v0.2.10">
|
||||
|
||||
**Bug Fixes:**
|
||||
@@ -1859,15 +1764,6 @@ A full-featured command-line interface for Mem0, available in both Python and No
|
||||
<Tabs>
|
||||
<Tab title="Mem0 Plugin">
|
||||
|
||||
<Update label="2026-07-14" description="mem0-plugin v0.2.13">
|
||||
|
||||
**Fixes:**
|
||||
- **Assistant messages no longer stored as your own:** The session-summary hook (fires at the end of every assistant turn) and the post-compaction hook were sending the assistant's own message to Mem0 tagged `role: "user"`. Because Mem0 extracts *facts about the user* from each message and uses `role` to decide who spoke, the assistant's first-person prose was being saved as the human's stated preferences — "I recommend we drop Redis" became `User prefers dropping Redis entirely`. Both hooks now send `role: "assistant"`, so the same session is stored as `Assistant recommended...`. Affects Claude Code, Cursor, Codex, and Antigravity, which share these hooks.
|
||||
|
||||
Existing memories written by the previous versions are not rewritten. If your memories contain preferences you never expressed, delete them — the plugin will not recreate them.
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-30" description="mem0-plugin v0.2.12">
|
||||
|
||||
**New Features:**
|
||||
@@ -2118,13 +2014,6 @@ 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:**
|
||||
@@ -2198,15 +2087,6 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Antigravity">
|
||||
|
||||
<Update label="2026-07-14" description="Antigravity plugin v0.1.5">
|
||||
|
||||
**Fixes:**
|
||||
- **Assistant messages no longer stored as your own:** The session-summary hook (fires at the end of every assistant turn) and the post-compaction hook were sending the assistant's own message to Mem0 tagged `role: "user"`. Because Mem0 extracts *facts about the user* from each message and uses `role` to decide who spoke, the assistant's first-person prose was being saved as the human's stated preferences — "I recommend we drop Redis" became `User prefers dropping Redis entirely`. Both hooks now send `role: "assistant"`, so the same session is stored as `Assistant recommended...`.
|
||||
|
||||
Existing memories written by the previous versions are not rewritten. If your memories contain preferences you never expressed, delete them — the plugin will not recreate them.
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-06-30" description="Antigravity plugin v0.1.4">
|
||||
|
||||
**New Features:**
|
||||
|
||||
@@ -3,27 +3,11 @@ title: AWS Bedrock
|
||||
description: "Configure AWS Bedrock as an embedding provider in Mem0 with IAM credentials and boto3 authentication."
|
||||
---
|
||||
|
||||
To use AWS Bedrock embedding models, you need the appropriate AWS credentials and permissions. Python uses `boto3`, and TypeScript uses `@aws-sdk/client-bedrock-runtime`.
|
||||
|
||||
Both SDKs support the Amazon Titan and Cohere embedding model families.
|
||||
To use AWS Bedrock embedding models, you need to have the appropriate AWS credentials and permissions. The embeddings implementation relies on the `boto3` library.
|
||||
|
||||
### Setup
|
||||
|
||||
- Model access is automatic: Bedrock enables serverless foundation models on first invocation in AWS commercial regions, and the [Model access page has been retired](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html). Cohere models are served from AWS Marketplace, so an account's first invocation must come from a principal with the `aws-marketplace:Subscribe` permission; after that, any user in the account can invoke them. Browse the models available to you in the [Bedrock model catalog](https://console.aws.amazon.com/bedrock/).
|
||||
- Install the AWS client for your language:
|
||||
|
||||
<CodeGroup>
|
||||
```bash Python
|
||||
pip install boto3
|
||||
```
|
||||
|
||||
```bash TypeScript
|
||||
npm install @aws-sdk/client-bedrock-runtime
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
In TypeScript this package is an optional peer dependency, so it is only required when you actually use the Bedrock embedder.
|
||||
|
||||
- Ensure you have model access from the [AWS Bedrock Console](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess)
|
||||
- Authenticate the boto3 client using a method described in the [AWS documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html)
|
||||
- Set up environment variables for authentication:
|
||||
```bash
|
||||
export AWS_REGION=us-east-1
|
||||
@@ -31,8 +15,6 @@ Both SDKs support the Amazon Titan and Cohere embedding model families.
|
||||
export AWS_SECRET_ACCESS_KEY=your-secret-key
|
||||
```
|
||||
|
||||
Both SDKs fall back to the standard AWS credential chain (environment variables, shared config, SSO, or an instance role) when you do not pass credentials in the config, so you rarely need to hardcode keys. See the [boto3 credentials guide](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html) for the Python resolution order.
|
||||
|
||||
### Usage
|
||||
|
||||
<CodeGroup>
|
||||
@@ -66,46 +48,8 @@ messages = [
|
||||
]
|
||||
m.add(messages, user_id="alice")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
// Credentials are read from the AWS default chain (AWS_REGION,
|
||||
// AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, SSO, or an instance role).
|
||||
const memory = new Memory({
|
||||
embedder: {
|
||||
provider: "aws_bedrock",
|
||||
config: {
|
||||
model: "amazon.titan-embed-text-v2:0",
|
||||
awsRegion: "us-west-2",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const messages = [
|
||||
{ role: "user", content: "I'm planning to watch a movie tonight. Any recommendations?" },
|
||||
{ role: "assistant", content: "How about thriller movies? They can be quite engaging." },
|
||||
{ role: "user", content: "I'm not a big fan of thriller movies but I love sci-fi movies." },
|
||||
{ role: "assistant", content: "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future." },
|
||||
];
|
||||
await memory.add(messages, { userId: "alice" });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Choosing a model
|
||||
|
||||
| Model | Notes |
|
||||
| --- | --- |
|
||||
| `amazon.titan-embed-text-v1` | Default. Fixed 1536-dimension output. |
|
||||
| `amazon.titan-embed-text-v2:0` | Supports a configurable output size of 256, 512, or 1024. |
|
||||
| `cohere.embed-english-v3` | English text. Embeds up to 96 texts per request. |
|
||||
| `cohere.embed-multilingual-v3` | Multilingual text. Embeds up to 96 texts per request. |
|
||||
| `cohere.embed-v4:0` | Text. Embeds up to 96 texts per request. Supports a configurable output size of 256, 512, 1024, or 1536. TypeScript only. |
|
||||
|
||||
Custom output sizes are model specific. In Python, only Titan Text Embeddings V2 accepts one. In TypeScript, Titan Text Embeddings V2 and Cohere Embed v4 both do, and `embeddingDims` is ignored on Titan V1 and on Cohere v3, which have no such parameter. When you do set it, make sure your vector store dimension matches, otherwise inserts will fail.
|
||||
|
||||
Bedrock caps a Cohere embedding call at 96 texts. The TypeScript SDK splits larger batches into multiple requests for you, so a 200 text batch becomes 3 calls.
|
||||
|
||||
### Config
|
||||
|
||||
Here are the parameters available for configuring AWS Bedrock embedder:
|
||||
@@ -120,16 +64,4 @@ Here are the parameters available for configuring AWS Bedrock embedder:
|
||||
| `aws_secret_access_key` | AWS secret access key for authentication | `None` |
|
||||
| `aws_session_token` | AWS session token for temporary credentials | `None` |
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `model` | The name of the embedding model to use | `amazon.titan-embed-text-v1` |
|
||||
| `awsRegion` | AWS region for the Bedrock client. Falls back to the `AWS_REGION` environment variable | `us-west-2` |
|
||||
| `embeddingDims` | Output vector size. Titan Text Embeddings V2 (256, 512, or 1024) and Cohere Embed v4 (256, 512, 1024, or 1536) only | `undefined` |
|
||||
| `awsAccessKeyId` | AWS access key ID for authentication | `undefined` |
|
||||
| `awsSecretAccessKey` | AWS secret access key for authentication | `undefined` |
|
||||
| `awsSessionToken` | AWS session token for temporary credentials | `undefined` |
|
||||
|
||||
Omit the three credential fields to use the AWS default credential chain. If you do pass them, `awsAccessKeyId` and `awsSecretAccessKey` are both required.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -4,36 +4,11 @@ description: "Configure Google Cloud Vertex AI as an embedding provider in Mem0
|
||||
---
|
||||
### Vertex AI
|
||||
|
||||
Google Cloud's Vertex AI serves text embedding models such as `gemini-embedding-001`. Mem0 uses them through the provider's own SDK, which you install alongside Mem0.
|
||||
|
||||
### Installation
|
||||
|
||||
The Vertex AI client is an optional dependency, so install it yourself.
|
||||
|
||||
<CodeGroup>
|
||||
```bash Python
|
||||
pip install vertexai
|
||||
```
|
||||
|
||||
```bash TypeScript
|
||||
npm install @google-cloud/aiplatform
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Authentication
|
||||
|
||||
Both SDKs authenticate with [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials). Pick whichever fits your environment:
|
||||
|
||||
- **Local development:** run `gcloud auth application-default login`.
|
||||
- **Service account:** create a key in the [Google Cloud Console](https://console.cloud.google.com/) and point `GOOGLE_APPLICATION_CREDENTIALS` at the JSON file, or pass its path through the embedder config.
|
||||
- **Google Cloud runtimes** (Cloud Run, GKE, Compute Engine): the attached service account is picked up automatically.
|
||||
|
||||
The TypeScript SDK reads the project ID from `googleProjectId`, then the `GCP_PROJECT_ID`, `GOOGLE_CLOUD_PROJECT`, and `GCLOUD_PROJECT` environment variables, and finally from your credentials. Set it explicitly when your credentials cover more than one project.
|
||||
To use Google Cloud's Vertex AI for text embedding models, set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to point to the path of your service account's credentials JSON file. These credentials can be created in the [Google Cloud Console](https://console.cloud.google.com/).
|
||||
|
||||
### Usage
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
```python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
@@ -57,87 +32,28 @@ 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": "user", "content": "I’m not a big fan of thriller movies but I love sci-fi movies."},
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
|
||||
]
|
||||
m.add(messages, user_id="john")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const config = {
|
||||
embedder: {
|
||||
provider: "vertexai",
|
||||
config: {
|
||||
model: "gemini-embedding-001",
|
||||
// Optional. Falls back to GCP_PROJECT_ID / GOOGLE_CLOUD_PROJECT /
|
||||
// GCLOUD_PROJECT, then to the project on your credentials.
|
||||
googleProjectId: process.env.GCP_PROJECT_ID,
|
||||
location: "us-central1",
|
||||
// Optional. Path to a service account key file, or pass the JSON inline
|
||||
// via googleServiceAccountJson.
|
||||
vertexCredentialsJson: "/path/to/your/credentials.json",
|
||||
embeddingDims: 256,
|
||||
memoryAddEmbeddingType: "RETRIEVAL_DOCUMENT",
|
||||
memoryUpdateEmbeddingType: "RETRIEVAL_DOCUMENT",
|
||||
memorySearchEmbeddingType: "RETRIEVAL_QUERY",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const memory = new Memory(config);
|
||||
await memory.add("I love sci-fi movies but not thrillers", { userId: "john" });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Embedding types
|
||||
|
||||
Vertex AI embeds the same text differently depending on the task you declare. The embedding types can be one of the following:
|
||||
The embedding types can be one of the following:
|
||||
- SEMANTIC_SIMILARITY
|
||||
- CLASSIFICATION
|
||||
- CLUSTERING
|
||||
- RETRIEVAL_DOCUMENT, RETRIEVAL_QUERY, QUESTION_ANSWERING, FACT_VERIFICATION
|
||||
- CODE_RETRIEVAL_QUERY
|
||||
|
||||
Check out the [Vertex AI documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/task-types#supported_task_types) for more information.
|
||||
|
||||
<Note>
|
||||
These embedding types map to the add, update, and search memory actions in both the Python and TypeScript SDKs. Stored memories use the add or update type, and searches use the search type.
|
||||
</Note>
|
||||
|
||||
### Choosing a model
|
||||
|
||||
<Warning>
|
||||
`gemini-embedding-001` accepts **one input text per request**. When Mem0 embeds several texts at once, such as the memories extracted from a single conversation turn, it issues one request per text. The older `text-embedding-005` and `text-multilingual-embedding-002` models accept up to 250 texts per request, so they are faster and cheaper for large batches. See [Get text embeddings](https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-text-embeddings).
|
||||
</Warning>
|
||||
|
||||
- CODE_RETRIEVAL_QUERY
|
||||
Check out the [Vertex AI documentation](https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/task-types#supported_task_types) for more information.
|
||||
|
||||
### Config
|
||||
|
||||
Here are the parameters available for configuring the Vertex AI embedder:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
| Parameter | Description | Default Value |
|
||||
| -------------------------------- | ---------------------------------------------------------- | ---------------------- |
|
||||
| `model` | The name of the Vertex AI embedding model to use | `gemini-embedding-001` |
|
||||
| `vertex_credentials_json` | Path to the Google Cloud credentials JSON file | `None` |
|
||||
| `embedding_dims` | Dimensions of the embedding model | `256` |
|
||||
| `memory_add_embedding_type` | The embedding type to use for the add memory action | `RETRIEVAL_DOCUMENT` |
|
||||
| `memory_update_embedding_type` | The embedding type to use for the update memory action | `RETRIEVAL_DOCUMENT` |
|
||||
| `memory_search_embedding_type` | The embedding type to use for the search memory action | `RETRIEVAL_QUERY` |
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
| Parameter | Description | Default Value |
|
||||
| ----------------------------- | -------------------------------------------------------------------------- | ---------------------- |
|
||||
| `model` | The name of the Vertex AI embedding model to use | `gemini-embedding-001` |
|
||||
| `googleProjectId` | Google Cloud project ID (falls back to `GCP_PROJECT_ID` env var, then to your credentials) | Resolved from credentials |
|
||||
| `location` | Google Cloud region (falls back to `GCP_LOCATION` env var) | `us-central1` |
|
||||
| `vertexCredentialsJson` | Path to the Google Cloud credentials JSON file | `None` |
|
||||
| `googleServiceAccountJson` | Service account credentials as a JSON string or object | `None` |
|
||||
| `embeddingDims` | Dimensions of the embedding model | `256` |
|
||||
| `memoryAddEmbeddingType` | The embedding type to use for the add memory action | `RETRIEVAL_DOCUMENT` |
|
||||
| `memoryUpdateEmbeddingType` | The embedding type to use for the update memory action | `RETRIEVAL_DOCUMENT` |
|
||||
| `memorySearchEmbeddingType` | The embedding type to use for the search memory action | `RETRIEVAL_QUERY` |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
| Parameter | Description | Default Value |
|
||||
| ------------------------- | ------------------------------------------------ | -------------------- |
|
||||
| `model` | The name of the Vertex AI embedding model to use | `gemini-embedding-001` |
|
||||
| `vertex_credentials_json` | Path to the Google Cloud credentials JSON file | `None` |
|
||||
| `embedding_dims` | Dimensions of the embedding model | `256` |
|
||||
| `memory_add_embedding_type` | The type of embedding to use for the add memory action | `RETRIEVAL_DOCUMENT` |
|
||||
| `memory_update_embedding_type` | The type of embedding to use for the update memory action | `RETRIEVAL_DOCUMENT` |
|
||||
| `memory_search_embedding_type` | The type of embedding to use for the search memory action | `RETRIEVAL_QUERY` |
|
||||
|
||||
@@ -10,7 +10,7 @@ Mem0 offers support for various embedding models, allowing users to choose the o
|
||||
See the list of supported embedders below.
|
||||
|
||||
<Note>
|
||||
All embedders listed below are supported in the Python implementation. The TypeScript implementation supports: **OpenAI**, **Azure OpenAI**, **AWS Bedrock**, **FastEmbed**, **Google AI**, **Hugging Face**, **Langchain**, **LM Studio**, **Ollama**, **Together**, and **Vertex AI**.
|
||||
All embedders listed below are supported in the Python implementation. The TypeScript implementation supports: **OpenAI**, **Azure OpenAI**, **FastEmbed**, **Google AI**, **Langchain**, **LM Studio**, **Ollama**, and **Together**.
|
||||
</Note>
|
||||
|
||||
<CardGroup cols={4}>
|
||||
|
||||
@@ -5,18 +5,16 @@ description: "Configure AWS Bedrock as an LLM provider in Mem0 with IAM authenti
|
||||
|
||||
### Setup
|
||||
- Before using the AWS Bedrock LLM, make sure you have the appropriate model access from [Bedrock Console](https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess).
|
||||
- Model availability is per-region. `anthropic.claude-sonnet-4-20250514-v1:0` supports on-demand inference in `us-east-1` and `ap-southeast-4`; from any other region, use the cross-region inference profile ID `us.anthropic.claude-sonnet-4-20250514-v1:0` instead.
|
||||
- Install the AWS SDK for your language: `pip install boto3` (Python) or `npm install @aws-sdk/client-bedrock-runtime` (TypeScript).
|
||||
- Both SDKs fall back to the standard AWS credential chain (environment variables, `~/.aws/credentials`, or an attached IAM role), so exporting `AWS_REGION`, `AWS_ACCESS_KEY_ID`, and `AWS_SECRET_ACCESS_KEY` is the quickest way to get started. In TypeScript you can also pass credentials inline with `awsRegion`, `awsAccessKeyId`, `awsSecretAccessKey`, and `awsSessionToken`, as shown below.
|
||||
- You will also need to authenticate the `boto3` client by using a method in the [AWS documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/credentials.html#configuring-credentials)
|
||||
- You will have to export `AWS_REGION`, `AWS_ACCESS_KEY_ID`, and `AWS_SECRET_ACCESS_KEY` to set environment variables.
|
||||
|
||||
### Usage
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
```python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
os.environ['AWS_REGION'] = 'us-east-1'
|
||||
os.environ['AWS_REGION'] = 'us-west-2'
|
||||
os.environ["AWS_ACCESS_KEY_ID"] = "xx"
|
||||
os.environ["AWS_SECRET_ACCESS_KEY"] = "xx"
|
||||
|
||||
@@ -24,7 +22,7 @@ config = {
|
||||
"llm": {
|
||||
"provider": "aws_bedrock",
|
||||
"config": {
|
||||
"model": "anthropic.claude-sonnet-4-20250514-v1:0",
|
||||
"model": "anthropic.claude-3-5-haiku-20241022-v1:0",
|
||||
"temperature": 0.2,
|
||||
"max_tokens": 2000,
|
||||
}
|
||||
@@ -41,43 +39,6 @@ messages = [
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Memory } from 'mem0ai/oss';
|
||||
|
||||
const config = {
|
||||
llm: {
|
||||
provider: 'aws_bedrock',
|
||||
config: {
|
||||
model: 'anthropic.claude-sonnet-4-20250514-v1:0',
|
||||
temperature: 0.2,
|
||||
maxTokens: 2000,
|
||||
// Optional. Omit these to use the default AWS credential chain.
|
||||
awsRegion: process.env.AWS_REGION,
|
||||
awsAccessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||
awsSecretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const memory = new Memory(config);
|
||||
const messages = [
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
|
||||
{"role": "user", "content": "I’m not a big fan of thriller movies but I love sci-fi movies."},
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
|
||||
];
|
||||
await memory.add(messages, { userId: 'alice', metadata: { category: 'movies' } });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
`@aws-sdk/client-bedrock-runtime` is an optional peer dependency of `mem0ai`, so npm will not install it for you. The TypeScript provider loads it lazily and throws a clear error on the first request if the package is missing.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
The TypeScript provider calls the Bedrock [Converse API](https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference.html), a single uniform interface across the current Bedrock model families. Streaming and `InvokeModel`-only models are not supported yet.
|
||||
</Note>
|
||||
|
||||
### Config
|
||||
|
||||
All available parameters for the `aws_bedrock` config are present in [Master List of All Params in Config](../config).
|
||||
All available parameters for the `aws_bedrock` config are present in [Master List of All Params in Config](../config).
|
||||
@@ -16,7 +16,7 @@ For a comprehensive list of available parameters for llm configuration, please r
|
||||
See the list of supported LLMs below.
|
||||
|
||||
<Note>
|
||||
All LLMs are supported in Python. The following LLMs are also supported in TypeScript: **OpenAI**, **Anthropic**, **AWS Bedrock**, **Groq**, **Azure OpenAI**, **DeepSeek**, **Google AI**, **Langchain**, **LM Studio**, **Mistral AI**, and **Ollama**.
|
||||
All LLMs are supported in Python. The following LLMs are also supported in TypeScript: **OpenAI**, **Anthropic**, **Groq**, **Azure OpenAI**, **DeepSeek**, **Google AI**, **Langchain**, **LM Studio**, **Mistral AI**, and **Ollama**.
|
||||
</Note>
|
||||
|
||||
<CardGroup cols={4}>
|
||||
|
||||
@@ -26,7 +26,7 @@ All rerankers share these common configuration parameters:
|
||||
|
||||
| Parameter | Description | Type | Default |
|
||||
| -------------------- | -------------------------------------------- | ------ | ----------------------- |
|
||||
| `model` | Cohere rerank model | `str` | `"rerank-v3.5"` |
|
||||
| `model` | Cohere rerank model | `str` | `"rerank-english-v3.0"` |
|
||||
| `api_key` | Cohere API key | `str` | `None` |
|
||||
| `return_documents` | Whether to return document texts in response | `bool` | `False` |
|
||||
| `max_chunks_per_doc` | Maximum chunks per document | `int` | `None` |
|
||||
@@ -103,30 +103,3 @@ config = {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript SDK
|
||||
|
||||
The self-hosted [TypeScript SDK](/open-source/features/reranker-search#typescript-sdk) (`mem0ai/oss`) supports the same five providers. Config keys are camelCase (`apiKey`, `topK`, `maxLength`) and each provider's SDK is a peer dependency you install per reranker.
|
||||
|
||||
| Provider | Install | Default model | Key config fields |
|
||||
| --- | --- | --- | --- |
|
||||
| `cohere` | `pnpm add cohere-ai` | `rerank-v3.5` | `apiKey`, `model`, `topK` |
|
||||
| `zero_entropy` | `pnpm add zeroentropy` | `zerank-1` | `apiKey`, `model`, `topK` |
|
||||
| `sentence_transformer` | `pnpm add @huggingface/transformers` | `Xenova/ms-marco-MiniLM-L-6-v2` | `model`, `device`, `maxLength`, `normalize`, `topK` |
|
||||
| `huggingface` | `pnpm add @huggingface/transformers` | `Xenova/bge-reranker-base` | `model`, `device`, `maxLength`, `normalize`, `topK` |
|
||||
| `llm_reranker` | None (uses your LLM provider's own SDK) | `openai` / `gpt-4o-mini` | `provider`, `model`, `apiKey`, `llm` (nested override), `topK` |
|
||||
|
||||
```typescript
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const memory = new Memory({
|
||||
reranker: {
|
||||
provider: "zero_entropy",
|
||||
config: { apiKey: process.env.ZERO_ENTROPY_API_KEY, topK: 5 },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
The local cross-encoder providers (`sentence_transformer`, `huggingface`) run on [Transformers.js](https://huggingface.co/docs/transformers.js) and default to ONNX (`Xenova/*`) model mirrors, so Python default model strings must be swapped for their ONNX equivalents. `batchSize` and `showProgressBar` are accepted for parity with Python but are no-ops in the TypeScript runtime. See the [reranker feature guide](/open-source/features/reranker-search#typescript-sdk) for full examples.
|
||||
</Note>
|
||||
|
||||
@@ -9,9 +9,9 @@ Cohere provides enterprise-grade reranking models with excellent multilingual su
|
||||
|
||||
Cohere offers several reranking models:
|
||||
|
||||
- **`rerank-v3.5`** (default): Latest reranker, multilingual, best performance
|
||||
- **`rerank-english-v3.0`**: Previous generation, English only
|
||||
- **`rerank-multilingual-v3.0`**: Previous generation, multilingual
|
||||
- **`rerank-english-v3.0`**: Latest English reranker with best performance
|
||||
- **`rerank-multilingual-v3.0`**: Multilingual support for global applications
|
||||
- **`rerank-english-v2.0`**: Previous generation English reranker
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -41,7 +41,7 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "cohere",
|
||||
"config": {
|
||||
"model": "rerank-v3.5",
|
||||
"model": "rerank-english-v3.0",
|
||||
"api_key": "your-cohere-api-key", # or set COHERE_API_KEY
|
||||
"top_k": 5,
|
||||
"return_documents": False,
|
||||
@@ -53,34 +53,6 @@ config = {
|
||||
memory = Memory.from_config(config)
|
||||
```
|
||||
|
||||
## TypeScript (self-hosted)
|
||||
|
||||
The [TypeScript OSS SDK](/open-source/features/reranker-search#typescript-sdk) (`mem0ai/oss`) ships the Cohere reranker. Config keys are camelCase, it defaults to the `rerank-v3.5` model, and you opt in per search with `rerank: true`.
|
||||
|
||||
```bash
|
||||
pnpm add cohere-ai
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const memory = new Memory({
|
||||
reranker: {
|
||||
provider: "cohere",
|
||||
config: {
|
||||
apiKey: process.env.COHERE_API_KEY, // or set COHERE_API_KEY
|
||||
// model: "rerank-v3.5", // default
|
||||
topK: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const results = await memory.search("What is the user's profession?", {
|
||||
filters: { userId: "bob" },
|
||||
rerank: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Set your API key as an environment variable:
|
||||
@@ -105,7 +77,7 @@ config = {
|
||||
"rerank": {
|
||||
"provider": "cohere",
|
||||
"config": {
|
||||
"model": "rerank-v3.5",
|
||||
"model": "rerank-english-v3.0",
|
||||
"top_k": 3
|
||||
}
|
||||
}
|
||||
@@ -152,7 +124,7 @@ config = {
|
||||
|
||||
| Parameter | Description | Type | Default |
|
||||
| -------------------- | -------------------------------- | ------ | ----------------------- |
|
||||
| `model` | Cohere rerank model to use | `str` | `"rerank-v3.5"` |
|
||||
| `model` | Cohere rerank model to use | `str` | `"rerank-english-v3.0"` |
|
||||
| `api_key` | Cohere API key | `str` | `None` |
|
||||
| `top_k` | Maximum documents to return | `int` | `None` |
|
||||
| `return_documents` | Whether to return document texts | `bool` | `False` |
|
||||
@@ -167,7 +139,7 @@ config = {
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Model Selection**: `rerank-v3.5` handles English and multilingual workloads; pin an older `v3.0` model only if you need to reproduce prior results
|
||||
1. **Model Selection**: Use `rerank-english-v3.0` for English, `rerank-multilingual-v3.0` for other languages
|
||||
2. **Batch Processing**: Process multiple queries efficiently
|
||||
3. **Error Handling**: Implement retry logic for production systems
|
||||
4. **Monitoring**: Track reranking performance and costs
|
||||
|
||||
@@ -57,40 +57,6 @@ config = {
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript (self-hosted)
|
||||
|
||||
The [TypeScript OSS SDK](/open-source/features/reranker-search#typescript-sdk) (`mem0ai/oss`) runs this reranker locally with [Transformers.js](https://huggingface.co/docs/transformers.js), the same cross-encoder path as `sentence_transformer`, just a different default model. It executes ONNX weights, so the default is the ONNX mirror `Xenova/bge-reranker-base`. Point `model` at any ONNX-exported reranker on the Hub (a raw `BAAI/bge-reranker-*` PyTorch checkpoint will not load in this runtime).
|
||||
|
||||
```bash
|
||||
pnpm add @huggingface/transformers
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const memory = new Memory({
|
||||
reranker: {
|
||||
provider: "huggingface",
|
||||
config: {
|
||||
// model: "Xenova/bge-reranker-base", // default (ONNX)
|
||||
device: "cpu", // "cpu" | "wasm" | "webgpu"
|
||||
maxLength: 512, // max tokens per query-document pair
|
||||
normalize: true, // sigmoid-normalize logits to [0, 1] (default)
|
||||
topK: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const results = await memory.search("What are the user's interests?", {
|
||||
filters: { userId: "alice" },
|
||||
rerank: true,
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`batchSize` and `showProgressBar` are accepted for parity with the Python SDK but are no-ops in the TypeScript runtime. `trust_remote_code` and `model_kwargs` are Python-only.
|
||||
</Note>
|
||||
|
||||
## Popular Models
|
||||
|
||||
### BGE Rerankers (Recommended)
|
||||
|
||||
@@ -67,43 +67,6 @@ config = {
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript (self-hosted)
|
||||
|
||||
The [TypeScript OSS SDK](/open-source/features/reranker-search#typescript-sdk) (`mem0ai/oss`) ships the LLM reranker under the provider name `llm_reranker`. It does **not** reuse the Memory's main `llm` instance; it builds its own LLM from the reranker's own config, defaulting to `openai` / `gpt-4o-mini`. Set `provider`/`model`/`apiKey` directly on `config`, or nest a fully separate `config.llm: { provider, config }` (its `provider`/`config` take priority over the top-level fields, which only backfill values missing from the nested config).
|
||||
|
||||
```typescript
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const memory = new Memory({
|
||||
reranker: {
|
||||
provider: "llm_reranker",
|
||||
config: { apiKey: process.env.OPENAI_API_KEY },
|
||||
},
|
||||
});
|
||||
|
||||
const results = await memory.search("What movies do I like?", {
|
||||
filters: { userId: "alice" },
|
||||
rerank: true,
|
||||
});
|
||||
```
|
||||
|
||||
To rerank with a different LLM provider than the Memory's main `llm`, nest it under `config.llm`:
|
||||
|
||||
```typescript
|
||||
const memory = new Memory({
|
||||
llm: { provider: "openai", config: { apiKey: process.env.OPENAI_API_KEY } },
|
||||
reranker: {
|
||||
provider: "llm_reranker",
|
||||
config: {
|
||||
llm: {
|
||||
provider: "anthropic",
|
||||
config: { apiKey: process.env.ANTHROPIC_API_KEY },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Supported LLM Providers
|
||||
|
||||
### OpenAI
|
||||
|
||||
@@ -54,40 +54,6 @@ config = {
|
||||
memory = Memory.from_config(config)
|
||||
```
|
||||
|
||||
## TypeScript (self-hosted)
|
||||
|
||||
The [TypeScript OSS SDK](/open-source/features/reranker-search#typescript-sdk) (`mem0ai/oss`) runs this reranker locally with [Transformers.js](https://huggingface.co/docs/transformers.js). Because it executes ONNX weights, the default model is the ONNX mirror of the Python default: `Xenova/ms-marco-MiniLM-L-6-v2`. Point `model` at any ONNX-exported cross-encoder on the Hub (a raw `cross-encoder/...` PyTorch checkpoint will not load in this runtime).
|
||||
|
||||
```bash
|
||||
pnpm add @huggingface/transformers
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const memory = new Memory({
|
||||
reranker: {
|
||||
provider: "sentence_transformer",
|
||||
config: {
|
||||
// model: "Xenova/ms-marco-MiniLM-L-6-v2", // default (ONNX)
|
||||
device: "cpu", // "cpu" | "wasm" | "webgpu"
|
||||
maxLength: 512, // max tokens per query-document pair
|
||||
normalize: true, // sigmoid-normalize logits to [0, 1] (default)
|
||||
topK: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const results = await memory.search("What books does the user like?", {
|
||||
filters: { userId: "charlie" },
|
||||
rerank: true,
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`batchSize` and `showProgressBar` are accepted for parity with the Python SDK but are no-ops in the TypeScript runtime, because a search reranks a small candidate set in a single in-process forward pass. The model downloads once and is cached in-process.
|
||||
</Note>
|
||||
|
||||
## GPU Acceleration
|
||||
|
||||
For better performance, use GPU acceleration:
|
||||
|
||||
@@ -50,34 +50,6 @@ config = {
|
||||
memory = Memory.from_config(config)
|
||||
```
|
||||
|
||||
## TypeScript (self-hosted)
|
||||
|
||||
The [TypeScript OSS SDK](/open-source/features/reranker-search#typescript-sdk) (`mem0ai/oss`) ships the Zero Entropy reranker under the same provider name as Python, `zero_entropy`. It reads the key from config or `ZERO_ENTROPY_API_KEY` and defaults to the `zerank-1` model.
|
||||
|
||||
```bash
|
||||
pnpm add zeroentropy
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const memory = new Memory({
|
||||
reranker: {
|
||||
provider: "zero_entropy",
|
||||
config: {
|
||||
apiKey: process.env.ZERO_ENTROPY_API_KEY,
|
||||
// model: "zerank-1", // default (or "zerank-1-small")
|
||||
topK: 5,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const results = await memory.search("What Italian food does the user like?", {
|
||||
filters: { userId: "alice" },
|
||||
rerank: true,
|
||||
});
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Set your API key as an environment variable:
|
||||
|
||||
@@ -47,7 +47,7 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "cohere",
|
||||
"config": {
|
||||
"model": "rerank-v3.5",
|
||||
"model": "rerank-english-v3.0",
|
||||
"top_n": 10,
|
||||
"max_chunks_per_doc": 10, # Limit chunk processing
|
||||
"return_documents": False # Reduce response size
|
||||
@@ -280,7 +280,7 @@ config = {
|
||||
```python
|
||||
def benchmark_rerankers():
|
||||
configs = [
|
||||
{"provider": "cohere", "model": "rerank-v3.5"},
|
||||
{"provider": "cohere", "model": "rerank-english-v3.0"},
|
||||
{"provider": "sentence_transformer", "model": "cross-encoder/ms-marco-MiniLM-L-6-v2"},
|
||||
{"provider": "huggingface", "model": "BAAI/bge-reranker-base"}
|
||||
]
|
||||
|
||||
@@ -19,10 +19,6 @@ Reranking trades extra latency for better precision. Start once you have baselin
|
||||
<Card title="Zero Entropy" icon="/images/provider-icons/zeroentropy.svg" href="/components/rerankers/models/zero_entropy" />
|
||||
</CardGroup>
|
||||
|
||||
<Note>
|
||||
All five rerankers are available in both the Python and the [TypeScript](/open-source/features/reranker-search#typescript-sdk) self-hosted SDKs. Each provider page has a **TypeScript (self-hosted)** section with the camelCase config.
|
||||
</Note>
|
||||
|
||||
## Reranking Workflow
|
||||
|
||||
<CardGroup cols={3}>
|
||||
|
||||
@@ -5,22 +5,10 @@ description: "Use Baidu Mochow as an enterprise vector database in Mem0 for high
|
||||
|
||||
[Baidu VectorDB](https://cloud.baidu.com/doc/VDB/index.html) is an enterprise-level distributed vector database service developed by Baidu Intelligent Cloud. It is powered by Baidu's proprietary "Mochow" vector database kernel, providing high performance, availability, and security for vector search.
|
||||
|
||||
### Installation
|
||||
|
||||
<CodeGroup>
|
||||
```bash Python
|
||||
pip install pymochow
|
||||
```
|
||||
|
||||
```bash TypeScript
|
||||
npm install @mochow/mochow-sdk-node
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
### Usage
|
||||
|
||||
```python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
config = {
|
||||
@@ -48,63 +36,19 @@ messages = [
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const memory = new Memory({
|
||||
embedder: {
|
||||
provider: "openai",
|
||||
config: {
|
||||
apiKey: process.env.OPENAI_API_KEY || "",
|
||||
model: "text-embedding-3-small",
|
||||
embeddingDims: 1536,
|
||||
},
|
||||
},
|
||||
vectorStore: {
|
||||
provider: "baidu",
|
||||
config: {
|
||||
endpoint: process.env.BAIDU_ENDPOINT || "",
|
||||
account: process.env.BAIDU_ACCOUNT || "root",
|
||||
apiKey: process.env.BAIDU_API_KEY || "",
|
||||
databaseName: "mem0",
|
||||
tableName: "mem0_table",
|
||||
embeddingModelDims: 1536,
|
||||
metricType: "COSINE",
|
||||
},
|
||||
},
|
||||
llm: {
|
||||
provider: "openai",
|
||||
config: {
|
||||
apiKey: process.env.OPENAI_API_KEY || "",
|
||||
model: "gpt-5-mini",
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Config
|
||||
|
||||
Here are the parameters available for configuring Baidu VectorDB:
|
||||
|
||||
| Parameter | Description | Default Value |
|
||||
| ---------------------- | --------------------------------------------- | ------------- |
|
||||
| `endpoint` | Endpoint URL for your Baidu VectorDB instance | Required |
|
||||
| `account` | Baidu VectorDB account name | `root` |
|
||||
| `api_key` | API key for accessing Baidu VectorDB | Required |
|
||||
| `database_name` | Name of the database | `mem0` |
|
||||
| `table_name` | Name of the table | `mem0` |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` |
|
||||
| `metric_type` | Distance metric for similarity search | `L2` |
|
||||
| `client` | Prebuilt Mochow client (TypeScript SDK only) | `None` |
|
||||
|
||||
For the TypeScript OSS SDK, use the camelCase equivalents:
|
||||
|
||||
- `databaseName`
|
||||
- `tableName`
|
||||
- `embeddingModelDims`
|
||||
- `metricType`
|
||||
|
||||
For OSS TS usage, `endpoint`, `account`, `apiKey`, `databaseName`, `tableName`, and `embeddingModelDims` are required unless you inject a prebuilt client. `metricType` defaults to `L2`, matching the Python SDK.
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `endpoint` | Endpoint URL for your Baidu VectorDB instance | Required |
|
||||
| `account` | Baidu VectorDB account name | `root` |
|
||||
| `api_key` | API key for accessing Baidu VectorDB | Required |
|
||||
| `database_name` | Name of the database | `mem0` |
|
||||
| `table_name` | Name of the table | `mem0` |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` |
|
||||
| `metric_type` | Distance metric for similarity search | `L2` |
|
||||
|
||||
### Distance Metrics
|
||||
|
||||
@@ -122,5 +66,3 @@ The vector index is automatically configured with the following HNSW parameters:
|
||||
- `efconstruction`: 200 (size of the dynamic candidate list)
|
||||
- `auto_build`: true (automatically build index)
|
||||
- `auto_build_index_policy`: Incremental build with 10000 rows increment
|
||||
|
||||
The TypeScript provider also creates a BM25 inverted index over a `textLemmatized` column so `keywordSearch()` runs against a real full-text index. Mem0 lemmatizes the query before it reaches the vector store, so only the lemmatized form of each memory is indexed. If you point `tableName` at a table created before this index existed, `keywordSearch()` returns `null` and search falls back to vector similarity alone; recreate the table to enable it.
|
||||
|
||||
@@ -6,8 +6,7 @@ description: "Use Databricks Vector Search as a serverless vector store in Mem0
|
||||
|
||||
### Usage
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
```python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
@@ -37,44 +36,10 @@ messages = [
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
// Requires the Databricks SQL driver (peer dependency): pnpm add @databricks/sql
|
||||
import { Memory } from 'mem0ai/oss';
|
||||
|
||||
const config = {
|
||||
vectorStore: {
|
||||
provider: 'databricks',
|
||||
config: {
|
||||
workspaceUrl: 'https://your-workspace.databricks.com',
|
||||
// SQL warehouse HTTP path, used for index writes (required)
|
||||
httpPath: '/sql/1.0/warehouses/your-warehouse-id',
|
||||
accessToken: 'your-access-token',
|
||||
catalog: 'your_catalog',
|
||||
schema: 'your_schema',
|
||||
tableName: 'your_table',
|
||||
collectionName: 'your_index_name',
|
||||
embeddingModelDims: 1536,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const memory = new Memory(config);
|
||||
const messages = [
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
|
||||
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
|
||||
]
|
||||
await memory.add(messages, { userId: "alice", metadata: { category: "movies" } });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Config
|
||||
|
||||
Here are the parameters available for configuring Databricks Vector Search:
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `workspace_url` | The URL of your Databricks workspace | **Required** |
|
||||
@@ -95,32 +60,6 @@ Here are the parameters available for configuring Databricks Vector Search:
|
||||
| `pipeline_type` | Sync pipeline type: `TRIGGERED` or `CONTINUOUS` | `TRIGGERED` |
|
||||
| `warehouse_name` | Databricks SQL warehouse name (if using SQL warehouse) | `None` |
|
||||
| `query_type` | Query type: `ANN` or `HYBRID` | `ANN` |
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `workspaceUrl` | The URL of your Databricks workspace (or pass `host`) | **Required** |
|
||||
| `httpPath` | SQL warehouse HTTP path, used for index writes | **Required** |
|
||||
| `accessToken` | Personal Access Token for authentication | `None` |
|
||||
| `clientId` | Service principal client ID (alternative to `accessToken`) | `None` |
|
||||
| `clientSecret` | Service principal client secret (required with `clientId`) | `None` |
|
||||
| `endpointName` | Name of the Vector Search endpoint | `mem0_vector_search` |
|
||||
| `endpointType` | Type of endpoint (`STANDARD` or `STORAGE_OPTIMIZED`) | `STANDARD` |
|
||||
| `pipelineType` | Delta Sync pipeline type: `TRIGGERED` or `CONTINUOUS` | `TRIGGERED` |
|
||||
| `queryType` | Query type: `ANN` or `HYBRID` | `ANN` |
|
||||
| `catalog` | Unity Catalog catalog name | `main` |
|
||||
| `schema` | Unity Catalog schema name | `default` |
|
||||
| `collectionName` | Vector Search index name | `mem0` |
|
||||
| `tableName` | Source Delta table name | falls back to `collectionName` |
|
||||
| `embeddingModelDims` | Dimension of self-managed embeddings | `1536` |
|
||||
| `syncPollIntervalMs` | Poll interval while waiting for a `TRIGGERED` sync | `1000` |
|
||||
| `syncTimeoutMs` | Timeout while waiting for an index sync | `300000` |
|
||||
|
||||
<Note>
|
||||
The TypeScript provider uses `DELTA_SYNC` indexes with self-managed embeddings: pass vectors directly. `DIRECT_ACCESS` indexes, Databricks-computed embeddings (`embedding_model_endpoint_name`), and Azure AD auth are Python-only today. It writes to the index through a SQL warehouse, so `httpPath` is required, and `@databricks/sql` must be installed as a peer dependency.
|
||||
</Note>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Authentication
|
||||
|
||||
|
||||
@@ -2,37 +2,26 @@
|
||||
title: "Neptune Analytics"
|
||||
description: "Use AWS Neptune Analytics as a vector store in Mem0, combining graph analytics with vector search capabilities."
|
||||
---
|
||||
# Neptune Analytics Vector Store
|
||||
|
||||
[Neptune Analytics](https://docs.aws.amazon.com/neptune-analytics/latest/userguide/what-is-neptune-analytics.html) is a memory-optimized graph database engine for analytics. With Neptune Analytics, you can get insights and find trends by processing large amounts of graph data in seconds, including vector search.
|
||||
[Neptune Analytics](https://docs.aws.amazon.com/neptune-analytics/latest/userguide/what-is-neptune-analytics.html/) is a memory-optimized graph database engine for analytics. With Neptune Analytics, you can get insights and find trends by processing large amounts of graph data in seconds, including vector search.
|
||||
|
||||
### Installation
|
||||
|
||||
The Neptune Analytics provider needs the AWS Neptune Graph client. Install it alongside `mem0ai`:
|
||||
## Installation
|
||||
|
||||
<CodeGroup>
|
||||
```bash Python
|
||||
```bash
|
||||
pip install mem0ai[vector-stores]
|
||||
```
|
||||
|
||||
```bash TypeScript
|
||||
npm install @aws-sdk/client-neptune-graph
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Usage
|
||||
|
||||
Configure AWS credentials in your environment (environment variables, shared config file, an IAM role, or an instance profile). Both SDKs pick them up automatically through the standard AWS credential chain.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from mem0 import Memory
|
||||
## Usage
|
||||
|
||||
```python
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "neptune",
|
||||
"config": {
|
||||
"collection_name": "mem0",
|
||||
"endpoint": "neptune-graph://g-abc123xyz0",
|
||||
"endpoint": f"neptune-graph://my-graph-identifier",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -40,90 +29,18 @@ config = {
|
||||
m = Memory.from_config(config)
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about a thriller movie? They can be quite engaging."},
|
||||
{"role": "assistant", "content": "How about a 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"})
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Memory } from 'mem0ai/oss';
|
||||
## Parameters
|
||||
|
||||
const config = {
|
||||
vectorStore: {
|
||||
provider: 'neptune',
|
||||
config: {
|
||||
collectionName: 'mem0',
|
||||
graphIdentifier: 'g-abc123xyz0',
|
||||
// Any other key here (region, credentials, maxAttempts, ...) is
|
||||
// forwarded to the underlying NeptuneGraphClient constructor.
|
||||
region: 'us-east-1',
|
||||
},
|
||||
},
|
||||
};
|
||||
Let's see the available parameters for the `neptune` config:
|
||||
|
||||
const memory = new Memory(config);
|
||||
const messages = [
|
||||
{ role: "user", content: "I'm planning to watch a movie tonight. Any recommendations?" },
|
||||
{ role: "assistant", content: "How about a thriller movie? They can be quite engaging." },
|
||||
{ role: "user", content: "I'm not a big fan of thriller movies but I love sci-fi movies." },
|
||||
{ role: "assistant", content: "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future." },
|
||||
];
|
||||
await memory.add(messages, { userId: "alice", metadata: { category: "movies" } });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Config
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `collection_name` | The name of the collection to store the vectors | `mem0` |
|
||||
| `endpoint` | Connection URL for the Neptune Analytics service, must be `neptune-graph://<graph-id>` | Required |
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `collectionName` | The name of the collection to store the vectors | `memories` |
|
||||
| `graphIdentifier` | Graph ID, e.g. `g-abc123xyz0`. Takes priority over `endpoint`. | Required, unless `endpoint` supplies it |
|
||||
| `endpoint` | Either `neptune-graph://<graph-id>` (or a bare graph ID) to supply the graph ID, or an `https://` service endpoint to override the AWS endpoint. An `https://` value must be paired with `graphIdentifier`. | `undefined` |
|
||||
| `dimension` | Embedding vector dimension | Auto-detected from the embedder when omitted |
|
||||
| `client` | A pre-built `NeptuneGraphClient` to use instead of constructing one | `undefined` |
|
||||
| any other key | Forwarded as-is to the [`NeptuneGraphClient`](https://www.npmjs.com/package/@aws-sdk/client-neptune-graph) constructor, e.g. `region`, `credentials`, `maxAttempts` | N/A |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Both SDKs store vectors on graph nodes labeled `MEM0_VECTOR_<collection_name>`. Point them at the same
|
||||
graph with the same `collection_name` — the defaults differ, `mem0` in Python and `memories` in
|
||||
TypeScript — and `get()`, `list()`, and `delete()` interoperate across SDKs.
|
||||
|
||||
<Note>
|
||||
`search()` is not currently cross-SDK compatible. The TypeScript provider filters on Neptune's reserved
|
||||
`~label` metafield, while the Python provider filters on a synthetic `label` property that only Python's
|
||||
own `insert()` writes. Python's `search()` therefore cannot see nodes written by the TypeScript provider.
|
||||
</Note>
|
||||
|
||||
### IAM Permissions
|
||||
|
||||
Your AWS identity (user or role) needs a policy that allows the [`ExecuteQuery`](https://docs.aws.amazon.com/neptune-analytics/latest/apiref/API_ExecuteQuery.html) actions used for reads, writes, and deletes:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"neptune-graph:ReadDataViaQuery",
|
||||
"neptune-graph:WriteDataViaQuery",
|
||||
"neptune-graph:DeleteDataViaQuery"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For production, scope the resource ARN down to your specific graph.
|
||||
| `endpoint` | Connection URL for the Neptune Analytics service | `neptune-graph://my-graph-identifier` |
|
||||
|
||||
@@ -42,7 +42,7 @@ const config = {
|
||||
provider: 'qdrant',
|
||||
config: {
|
||||
collectionName: 'memories',
|
||||
dimension: 1536,
|
||||
embeddingModelDims: 1536,
|
||||
host: 'localhost',
|
||||
port: 6333,
|
||||
},
|
||||
@@ -83,7 +83,7 @@ Let's see the available parameters for the `qdrant` config:
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| `collectionName` | The name of the collection to store the vectors | `mem0` |
|
||||
| `dimension` | Dimensions of the embedding model | `1536` |
|
||||
| `embeddingModelDims` | Dimensions of the embedding model | `1536` |
|
||||
| `host` | The host where the Qdrant server is running | `None` |
|
||||
| `port` | The port where the Qdrant server is running | `None` |
|
||||
| `path` | Path for the Qdrant database | `/tmp/qdrant` |
|
||||
|
||||
@@ -10,7 +10,7 @@ Mem0 includes built-in support for various popular databases. Memory can utilize
|
||||
See the list of supported vector databases below.
|
||||
|
||||
<Note>
|
||||
The following vector databases are supported in the Python implementation. The TypeScript implementation currently supports Qdrant, Redis, PGVector, Supabase, LangChain, Azure AI Search, Vectorize, Amazon S3 Vectors, Milvus, Neptune Analytics, and an in-memory store.
|
||||
The following vector databases are supported in the Python implementation. The TypeScript implementation currently supports Qdrant, Redis, PGVector, Supabase, LangChain, Azure AI Search, Vectorize, Amazon S3 Vectors, Milvus, and an in-memory store.
|
||||
</Note>
|
||||
|
||||
<CardGroup cols={3}>
|
||||
@@ -32,7 +32,6 @@ See the list of supported vector databases below.
|
||||
<Card title="FAISS" icon="layer-group" href="/components/vectordbs/dbs/faiss"></Card>
|
||||
<Card title="LangChain" icon="/images/provider-icons/langchain-color.svg" href="/components/vectordbs/dbs/langchain"></Card>
|
||||
<Card title="Amazon S3 Vectors" icon="/images/provider-icons/aws-color.svg" href="/components/vectordbs/dbs/s3_vectors"></Card>
|
||||
<Card title="Neptune Analytics" icon="/images/provider-icons/aws-color.svg" href="/components/vectordbs/dbs/neptune_analytics"></Card>
|
||||
<Card title="Databricks" icon="/images/provider-icons/databricks.svg" href="/components/vectordbs/dbs/databricks"></Card>
|
||||
<Card title="Turbopuffer" icon="/images/provider-icons/turbopuffer.svg" href="/components/vectordbs/dbs/turbopuffer"></Card>
|
||||
</CardGroup>
|
||||
|
||||
@@ -588,9 +588,9 @@ mem0_client.project.update(
|
||||
Exclude: greetings, filler, casual chat
|
||||
""",
|
||||
custom_categories=[
|
||||
{"goals": "Training targets"},
|
||||
{"constraints": "Injuries and limitations"},
|
||||
{"preferences": "Training style"}
|
||||
{"name": "goals", "description": "Training targets"},
|
||||
{"name": "constraints", "description": "Injuries and limitations"},
|
||||
{"name": "preferences", "description": "Training style"}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
@@ -19,7 +19,7 @@ client = MemoryClient(api_key="your-api-key")
|
||||
```
|
||||
|
||||
<Note>
|
||||
Define custom categories at the **project level** with `client.project.update()` before adding memories. Categories apply to all future memories: Mem0 auto-assigns them based on content semantics. You can also pass `custom_categories` on a single `client.add()` call to override the project list for just those memories. See [Custom Categories](/platform/features/custom-categories).
|
||||
Define custom categories at the **project level** with `client.project.update()` before adding memories. Categories apply to all future memories: Mem0 auto-assigns them based on content semantics.
|
||||
</Note>
|
||||
|
||||
---
|
||||
@@ -96,10 +96,6 @@ Start with 3-5 clear categories that match how your team thinks. Too many catego
|
||||
|
||||
These categories are now available project-wide. Every memory can be tagged with one or more categories.
|
||||
|
||||
<Tip>
|
||||
Need a different vocabulary for one tenant or one kind of conversation? Pass `custom_categories=[...]` to `client.add()`. That list replaces the project list for the memories created by that call, and it does not change the project configuration.
|
||||
</Tip>
|
||||
|
||||
---
|
||||
|
||||
## Tagging Memories
|
||||
|
||||
@@ -15,7 +15,6 @@ Adding memory is how Mem0 captures useful details from a conversation so your ag
|
||||
- **Infer**: Controls whether Mem0 extracts structured memories (`infer=True`, default) or stores raw messages.
|
||||
- **Metadata**: Optional filters (e.g., `{"category": "movie_recommendations"}`) that improve retrieval later.
|
||||
- **User / Session identifiers**: `user_id`, `agent_id`, `app_id`, or `run_id` that scope the memory for future searches.
|
||||
- **expiration_date**: Optional `YYYY-MM-DD` date after which the memory is treated as expired. Use `expirationDate` in the JavaScript SDKs. Expired memories are hidden from `search` and `get_all` unless you pass `show_expired` (`showExpired` in JavaScript); fetching by ID still returns them.
|
||||
|
||||
## How does it work?
|
||||
|
||||
@@ -83,50 +82,6 @@ await client.add(messages, {
|
||||
Expect a `status: "PENDING"` response with an `event_id`. Poll `GET /v1/event/{event_id}/` to confirm completion.
|
||||
</Info>
|
||||
|
||||
### Automatic conversation context
|
||||
|
||||
On the Platform, you only send new messages. Mem0 automatically pulls the earlier messages that share the same identifiers (`user_id`, and `run_id` if you use one) and uses them as context when extracting memories, so you never need to resend conversation history.
|
||||
|
||||
This means a follow-up turn is understood against what came before it:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# First interaction
|
||||
client.add(
|
||||
[{"role": "user", "content": "My dog's name is Biscuit. He's a golden retriever."}],
|
||||
user_id="alice",
|
||||
)
|
||||
|
||||
# Later — send only the new turn, no history
|
||||
client.add(
|
||||
[{"role": "user", "content": "He turned 5 today, and I'm taking him to the vet on Friday."}],
|
||||
user_id="alice",
|
||||
)
|
||||
# Stored as: "User's dog Biscuit turned 5" — "He" is resolved against the earlier turn.
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
// First interaction
|
||||
await client.add(
|
||||
[{ role: "user", content: "My dog's name is Biscuit. He's a golden retriever." }],
|
||||
{ userId: "alice" },
|
||||
);
|
||||
|
||||
// Later — send only the new turn, no history
|
||||
await client.add(
|
||||
[{ role: "user", content: "He turned 5 today, and I'm taking him to the vet on Friday." }],
|
||||
{ userId: "alice" },
|
||||
);
|
||||
// Stored as: "User's dog Biscuit turned 5" — "He" is resolved against the earlier turn.
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Without that earlier turn, the same message can only be stored as "User's male pet turned 5", because there is nothing to resolve "He" against. Scope each conversation with a consistent `user_id` (plus `run_id` for a distinct session) and Mem0 handles the rest.
|
||||
|
||||
<Info>
|
||||
This is default behavior and needs no configuration. Earlier SDK versions gated it behind a `version="v2"` argument on `add`; that argument no longer exists and is ignored if sent.
|
||||
</Info>
|
||||
|
||||
## Add with Mem0 Open Source
|
||||
|
||||
<CodeGroup>
|
||||
@@ -150,9 +105,6 @@ result = m.add(messages, user_id="alice", metadata={"category": "movie_recommend
|
||||
|
||||
# Optionally store raw messages without inference
|
||||
result = m.add(messages, user_id="alice", metadata={"category": "movie_recommendations"}, infer=False)
|
||||
|
||||
# Optionally set an expiration date (YYYY-MM-DD)
|
||||
result = m.add(messages, user_id="alice", expiration_date="2030-01-31")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
@@ -171,12 +123,6 @@ const result = memory.add(messages, {
|
||||
userId: "alice",
|
||||
metadata: { category: "preferences" }
|
||||
});
|
||||
|
||||
// Optionally set an expiration date (YYYY-MM-DD)
|
||||
const expiring = memory.add(messages, {
|
||||
userId: "alice",
|
||||
expirationDate: "2030-01-31",
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
@@ -188,17 +188,12 @@ memory = Memory()
|
||||
memory.delete(memory_id="mem_123")
|
||||
memory.delete_all(user_id="alice")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const memory = new Memory();
|
||||
|
||||
await memory.delete("mem_123");
|
||||
await memory.deleteAll({ userId: "alice" });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
The OSS JavaScript SDK does not yet expose deletion helpers: use the REST API or Python SDK when self-hosting.
|
||||
</Note>
|
||||
|
||||
## Use cases recap
|
||||
|
||||
- Forget a user’s preferences at their request.
|
||||
|
||||
@@ -12,7 +12,7 @@ Mem0’s update operation lets you fix or enrich an existing memory without dele
|
||||
## Key terms
|
||||
|
||||
- **memory_id**: Unique identifier returned by `add` or `search` results.
|
||||
- **text**: New content that replaces the stored memory value. In the Python OSS SDK, `data` is a deprecated alias for `text`.
|
||||
- **text** / **data**: New content that replaces the stored memory value.
|
||||
- **metadata**: Optional key-value pairs you update alongside the text.
|
||||
- **timestamp**: Unix epoch (int/float) or ISO 8601 string to override the memory's timestamp.
|
||||
- **batch_update**: Platform API that edits multiple memories in a single request.
|
||||
@@ -110,47 +110,17 @@ from mem0 import Memory
|
||||
|
||||
memory = Memory()
|
||||
|
||||
# Replace the content
|
||||
memory.update(
|
||||
memory_id="mem_123",
|
||||
text="Alex now prefers decaf coffee",
|
||||
)
|
||||
|
||||
# Update content plus metadata and an expiration date (None clears it)
|
||||
memory.update(
|
||||
memory_id="mem_123",
|
||||
text="Alex now prefers decaf coffee",
|
||||
metadata={"category": "preferences"},
|
||||
expiration_date="2030-01-31",
|
||||
data="Alex now prefers decaf coffee",
|
||||
)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const memory = new Memory();
|
||||
|
||||
// Replace the content
|
||||
await memory.update("mem_123", { text: "Alex now prefers decaf coffee" });
|
||||
|
||||
// Update content plus metadata and an expiration date (null clears it)
|
||||
await memory.update("mem_123", {
|
||||
text: "Alex now prefers decaf coffee",
|
||||
metadata: { category: "preferences" },
|
||||
expirationDate: "2030-01-31",
|
||||
});
|
||||
|
||||
// Update metadata only, leaving the stored text untouched
|
||||
await memory.update("mem_123", { metadata: { category: "preferences" } });
|
||||
```
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
In both OSS SDKs the content is optional: pass only `metadata` and/or an expiration date to update those while keeping the existing content. At least one of the three must be provided, otherwise the call raises.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
`data` is a deprecated alias for `text` in both OSS SDKs (`data=` in Python, `{ data: ... }` in JavaScript). It still works but logs a warning; prefer `text`. In JavaScript, passing a bare string is shorthand for `{ text }`, so `update(memoryId, "new text")` also still works.
|
||||
OSS JavaScript SDK does not expose `update` yet: use the REST API or Python SDK when self-hosting.
|
||||
</Note>
|
||||
|
||||
## Tips
|
||||
@@ -168,7 +138,7 @@ await memory.update("mem_123", { metadata: { category: "preferences" } });
|
||||
|
||||
| Capability | Mem0 Platform | Mem0 OSS |
|
||||
| --- | --- | --- |
|
||||
| Update call | `client.update(memory_id, {...})` | `memory.update(memory_id, text=...)` |
|
||||
| Update call | `client.update(memory_id, {...})` | `memory.update(memory_id, data=...)` |
|
||||
| Batch updates | `client.batch_update` (up to 1000 memories) | Script your own loop or bulk job |
|
||||
| Dashboard visibility | Inspect updates in the UI | Inspect via logs or custom tooling |
|
||||
| Immutable handling | Returns descriptive error | Raises exception: delete and re-add |
|
||||
|
||||
+4
-12
@@ -84,6 +84,8 @@
|
||||
"pages": [
|
||||
"platform/features/advanced-retrieval",
|
||||
"platform/advanced-memory-operations",
|
||||
"platform/features/criteria-retrieval",
|
||||
"platform/features/contextual-add",
|
||||
"platform/features/custom-instructions",
|
||||
"platform/features/memory-decay"
|
||||
]
|
||||
@@ -94,8 +96,7 @@
|
||||
"pages": [
|
||||
"platform/features/direct-import",
|
||||
"platform/features/memory-export",
|
||||
"platform/features/timestamp",
|
||||
"platform/features/memory-expiration"
|
||||
"platform/features/timestamp"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -151,8 +152,7 @@
|
||||
"open-source/features/multimodal-support",
|
||||
"open-source/features/custom-instructions",
|
||||
"open-source/features/rest-api",
|
||||
"open-source/features/openai_compatibility",
|
||||
"platform/features/memory-expiration"
|
||||
"open-source/features/openai_compatibility"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -607,10 +607,6 @@
|
||||
]
|
||||
},
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/platform/features/contextual-add",
|
||||
"destination": "/core-concepts/memory-operations/add"
|
||||
},
|
||||
{
|
||||
"source": "/changelog/openclaw",
|
||||
"destination": "/changelog/sdk"
|
||||
@@ -1230,10 +1226,6 @@
|
||||
{
|
||||
"source": "/open-source/multimodal-support",
|
||||
"destination": "/open-source/features/multimodal-support"
|
||||
},
|
||||
{
|
||||
"source": "/platform/features/criteria-retrieval",
|
||||
"destination": "/platform/features/advanced-retrieval"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -65,9 +65,7 @@ The plugin uses the same shell scripts as Claude Code, Cursor, and Codex: hooks
|
||||
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message |
|
||||
| **Pre-tool** | `PreToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tools |
|
||||
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
|
||||
| **Stop** | `Stop` | Stores a session summary at the end of every assistant turn (not just at session end) |
|
||||
|
||||
What you type is stored as yours. What the agent produces — session summaries and compaction summaries — is stored as the assistant's, so its suggestions never become your stated preferences.
|
||||
| **Stop** | `Stop` | Stores a session summary when the session ends |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -44,14 +44,14 @@ Install the full plugin including MCP server, lifecycle hooks, and SDK skill.
|
||||
|
||||
1. Add the Mem0 marketplace:
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add mem0ai/mem0
|
||||
```
|
||||
/plugin marketplace add mem0ai/mem0
|
||||
```
|
||||
|
||||
2. Install the plugin:
|
||||
|
||||
```bash
|
||||
claude plugin install mem0@mem0-plugins
|
||||
```
|
||||
/plugin install mem0@mem0-plugins
|
||||
```
|
||||
|
||||
**Claude Cowork desktop app:** Open the Cowork tab, click **Customize** in the sidebar, click **Browse plugins**, and install Mem0.
|
||||
@@ -88,15 +88,6 @@ Add to your Claude Code MCP config (`.mcp.json`):
|
||||
}
|
||||
```
|
||||
|
||||
### Managing the Plugin
|
||||
|
||||
```bash
|
||||
claude plugin update mem0@mem0-plugins # update the plugin to the latest version (restart to apply)
|
||||
claude plugin marketplace update mem0-plugins # refresh the marketplace catalog
|
||||
claude plugin uninstall mem0@mem0-plugins # uninstall the plugin (keeps the marketplace)
|
||||
claude plugin marketplace remove mem0-plugins # unregister the marketplace entirely
|
||||
```
|
||||
|
||||
<Info icon="check">
|
||||
Start a new session and ask: *"List my mem0 entities"* or *"Search my memories for hello"*. If the `mem0` tools appear and respond, you're all set.
|
||||
</Info>
|
||||
@@ -152,11 +143,9 @@ When installed via the plugin marketplace, Mem0 hooks into Claude Code's lifecyc
|
||||
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message; skips short prompts |
|
||||
| **Pre-tool (3 handlers)** | `PreToolUse` | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context |
|
||||
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
|
||||
| **Stop** | `Stop` | Stores a session summary at the end of every assistant turn (not just at session end) |
|
||||
| **Stop** | `Stop` | Stores a session summary when the session ends |
|
||||
| **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted |
|
||||
|
||||
What you type is stored as yours. What Claude produces — session summaries and compaction summaries — is stored as the assistant's, so its suggestions never become your stated preferences.
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```text
|
||||
@@ -164,17 +153,16 @@ What you type is stored as yours. What Claude produces — session summaries and
|
||||
You: Let's refactor the auth module to use JWT tokens instead of sessions.
|
||||
|
||||
# Claude searches memories, finds nothing relevant, proceeds with the work.
|
||||
# Mem0 stores what you said as yours:
|
||||
# - Your preference: "Prefers TypeScript, uses ESLint"
|
||||
# ...and what Claude did as the assistant's, in the session summary:
|
||||
# After completing the task, Mem0 stores:
|
||||
# - Decision: "Migrated auth from sessions to JWT tokens"
|
||||
# - Files modified: auth/middleware.ts, auth/token.ts
|
||||
# - User preference: "Prefers TypeScript, uses ESLint"
|
||||
|
||||
# Session 2 (days later): Related work
|
||||
You: Add refresh token rotation to the auth system.
|
||||
|
||||
# Claude searches memories, retrieves the JWT migration context.
|
||||
# Knows the file structure, decisions made, and your stated preferences.
|
||||
# Knows the file structure, decisions made, and user preferences.
|
||||
# Continues seamlessly without re-explaining the codebase.
|
||||
```
|
||||
|
||||
|
||||
@@ -125,23 +125,20 @@ When installed via the plugin marketplace, Mem0 hooks into Codex's lifecycle to
|
||||
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message |
|
||||
| **Pre-tool (3 handlers)** | `PreToolUse` | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context |
|
||||
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
|
||||
| **Stop** | `Stop` | Stores a session summary at the end of every assistant turn (not just at session end) |
|
||||
| **Stop** | `Stop` | Stores a session summary when the session ends |
|
||||
| **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted |
|
||||
|
||||
What you type is stored as yours. What Codex produces — session summaries and compaction summaries — is stored as the assistant's, so its suggestions never become your stated preferences.
|
||||
|
||||
## Example Workflow
|
||||
|
||||
```text
|
||||
# Task 1: Setting up a new service
|
||||
You: Create a REST API for the notifications service using Express and TypeScript.
|
||||
|
||||
# Codex searches memories, finds your preferences from prior tasks.
|
||||
# Mem0 stores what you said as yours:
|
||||
# - Your preference: "Prefers explicit error types over generic catch-all"
|
||||
# ...and what Codex did as the assistant's, in the session summary:
|
||||
# Codex searches memories, finds user preferences from prior tasks.
|
||||
# After completing the task, Mem0 stores:
|
||||
# - Decision: "Notifications service uses Express + TypeScript + Zod validation"
|
||||
# - Convention: "All API routes follow /api/v1/{resource} pattern"
|
||||
# - Preference: "User prefers explicit error types over generic catch-all"
|
||||
|
||||
# Task 2 (days later): Extending the service
|
||||
You: Add WebSocket support for real-time notification delivery.
|
||||
|
||||
@@ -96,11 +96,10 @@ Once installed, the following tools are available in every Cursor session:
|
||||
You: The API endpoint /users is taking 3 seconds. Help me optimize it.
|
||||
|
||||
# Cursor agent searches memories, proceeds with investigation.
|
||||
# Mem0 stores what you said as yours:
|
||||
# - Your preference: "Prefers query-level fixes over caching"
|
||||
# ...and what the agent did as the assistant's:
|
||||
# After completing the task, Mem0 stores:
|
||||
# - Learning: "N+1 query in UserService.getAll(): fixed with eager loading"
|
||||
# - Decision: "Added database index on users.email column"
|
||||
# - Preference: "User prefers query-level fixes over caching"
|
||||
|
||||
# Session 2 (next week): Similar issue
|
||||
You: The /orders endpoint is also slow, same pattern as before.
|
||||
|
||||
+5
-5
@@ -61,7 +61,7 @@ client.get_all(user_id="alice")
|
||||
client.get(memory_id="<id>")
|
||||
|
||||
# Update
|
||||
client.update(memory_id="<id>", text="Alice loves mountain hiking")
|
||||
client.update(memory_id="<id>", data="Alice loves mountain hiking")
|
||||
|
||||
# Delete
|
||||
client.delete(memory_id="<id>")
|
||||
@@ -118,7 +118,7 @@ m.get_all(user_id="alice")
|
||||
m.get(memory_id="<id>")
|
||||
|
||||
# Update
|
||||
m.update(memory_id="<id>", text="Alice loves mountain hiking")
|
||||
m.update(memory_id="<id>", data="Alice loves mountain hiking")
|
||||
|
||||
# Delete
|
||||
m.delete(memory_id="<id>")
|
||||
@@ -204,7 +204,9 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
|
||||
|
||||
### Features - Advanced Retrieval
|
||||
- [Advanced Retrieval](https://docs.mem0.ai/platform/features/advanced-retrieval) [Platform]: Use when the user needs keyword search, reranking, or hybrid retrieval.
|
||||
- [Criteria-Based Retrieval](https://docs.mem0.ai/platform/features/criteria-retrieval) [Platform]: Use when targeting memories by custom criteria, not just semantic similarity.
|
||||
- [Temporal Reasoning](https://docs.mem0.ai/platform/features/temporal-reasoning) [Platform]: Use when time-aware searches like last week, upcoming, or right now need better result ordering.
|
||||
- [Contextual Add](https://docs.mem0.ai/platform/features/contextual-add) [Platform]: Use when `add()` should consider the surrounding conversation, not just the latest turn.
|
||||
- [Custom Instructions](https://docs.mem0.ai/platform/features/custom-instructions) [Platform]: Use when tailoring what Mem0 extracts and stores on Platform.
|
||||
- [Memory Decay](https://docs.mem0.ai/platform/features/memory-decay) [Platform]: Use when search results should boost recently-reinforced memories and dampen stale ones. Opt in per project; applies at search time and never filters candidates out.
|
||||
- [Advanced Memory Operations](https://docs.mem0.ai/platform/advanced-memory-operations) [Platform]: Use when basic CRUD is not enough - batch ops, complex filters, workflows.
|
||||
@@ -213,7 +215,6 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
|
||||
- [Direct Import](https://docs.mem0.ai/platform/features/direct-import) [Platform]: Use when seeding a Mem0 project from existing data.
|
||||
- [Memory Export](https://docs.mem0.ai/platform/features/memory-export) [Platform]: Use when exporting memories via a Pydantic schema.
|
||||
- [Timestamp Support](https://docs.mem0.ai/platform/features/timestamp) [Platform]: Use when temporal queries or time-based filtering matter.
|
||||
- [Memory Expiration](https://docs.mem0.ai/platform/features/memory-expiration) [Both]: Use when a memory should stop surfacing after a known date without being deleted, e.g. trial facts, seasonal preferences, or retention windows.
|
||||
|
||||
### Features - Integration & Ops
|
||||
- [Webhooks](https://docs.mem0.ai/platform/features/webhooks) [Platform]: Use when another system needs to react to memory changes in real time.
|
||||
@@ -500,6 +501,5 @@ Everything below is OSS-only provider configuration. Skip this entire section wh
|
||||
- [Custom Reranker Prompts](https://docs.mem0.ai/components/rerankers/custom-prompts) [OSS]: Use when rewriting reranker prompts.
|
||||
- [Cohere Reranker](https://docs.mem0.ai/components/rerankers/models/cohere) [OSS]: Use for Cohere Rerank.
|
||||
- [Sentence Transformer Reranker](https://docs.mem0.ai/components/rerankers/models/sentence_transformer) [OSS]: Use for local cross-encoder rerankers.
|
||||
- [Hugging Face Reranker](https://docs.mem0.ai/components/rerankers/models/huggingface) [OSS]: Use for HF-hosted reranker models.
|
||||
- [LLM Reranker](https://docs.mem0.ai/components/rerankers/models/llm_reranker) [OSS]: Use when the reranker is a prompted LLM (implementation reference).
|
||||
- [Hugging Face Reranker](https://docs.mem0.ai/components/rerankers/models/huggingface) [OSS]: Use for HF-hosted reranker models.- [LLM Reranker](https://docs.mem0.ai/components/rerankers/models/llm_reranker) [OSS]: Use when the reranker is a prompted LLM (implementation reference).
|
||||
- [Zero Entropy Reranker](https://docs.mem0.ai/components/rerankers/models/zero_entropy) [OSS]: Use for the Zero Entropy reranker.
|
||||
|
||||
@@ -339,22 +339,21 @@ The Platform introduces powerful capabilities not available in OSS:
|
||||
</Info>
|
||||
```python
|
||||
# Set custom categories for your project
|
||||
client.project.update(
|
||||
custom_categories=[
|
||||
{"customer_preferences": "Likes, dislikes, and product preferences"},
|
||||
{"product_feedback": "Feature requests and complaints about the product"},
|
||||
{"support_issues": "Problems reported and how they were resolved"}
|
||||
client.projects.update_categories(
|
||||
project_id="proj_123",
|
||||
categories=[
|
||||
"Customer Preferences",
|
||||
"Product Feedback",
|
||||
"Support Issues",
|
||||
"Feature Requests"
|
||||
]
|
||||
)
|
||||
|
||||
# Mem0 assigns these categories automatically as memories come in
|
||||
client.add("User wants dark mode in dashboard", user_id="alex")
|
||||
|
||||
# Or pass a different catalog for a single call
|
||||
# Memories will use these categories
|
||||
client.add(
|
||||
"User wants dark mode in dashboard",
|
||||
user_id="alex",
|
||||
custom_categories=[{"ui_requests": "Requests about interface and appearance"}]
|
||||
categories=["Customer Preferences"]
|
||||
)
|
||||
```
|
||||
</Accordion>
|
||||
|
||||
@@ -59,7 +59,7 @@ config = {
|
||||
},
|
||||
"reranker": {
|
||||
"provider": "cohere",
|
||||
"config": {"model": "rerank-v3.5"},
|
||||
"config": {"model": "rerank-english-v3.0"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ Change the `provider` string to switch backends. The most common options:
|
||||
|
||||
| Component | Python | TypeScript |
|
||||
| --- | --- | --- |
|
||||
| LLM | `openai`, `anthropic`, `gemini`, `groq`, `ollama`, `aws_bedrock`, `azure_openai`, `litellm` | `openai`, `anthropic`, `gemini`, `groq`, `ollama`, `aws_bedrock`, `azure_openai`, `mistral`, `deepseek` |
|
||||
| LLM | `openai`, `anthropic`, `gemini`, `groq`, `ollama`, `aws_bedrock`, `azure_openai`, `litellm` | `openai`, `anthropic`, `gemini`, `groq`, `ollama`, `azure_openai`, `mistral`, `deepseek` |
|
||||
| Embedder | `openai`, `gemini`, `azure_openai`, `ollama`, `huggingface`, `vertexai`, `aws_bedrock` | `openai`, `gemini`, `azure_openai`, `ollama` |
|
||||
| Vector store | `qdrant`, `pgvector`, `chroma`, `pinecone`, `redis`, `weaviate`, `milvus`, `elasticsearch` | `memory`, `qdrant`, `pgvector`, `redis`, `supabase`, `azure-ai-search`, `vectorize`, `milvus` |
|
||||
|
||||
@@ -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): 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.
|
||||
- `Cannot find module` (Node): import from the OSS entry point, `import { Memory } from "mem0ai/oss"`, not `"mem0ai"`.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
|
||||
@@ -36,7 +36,7 @@ icon: "bolt"
|
||||
| Search memories | `await memory.search(...)` | Returns dict with `results`, identical shape. |
|
||||
| List memories | `await memory.get_all(...)` | Filter by `user_id`, `agent_id`, `run_id`. |
|
||||
| Retrieve memory | `await memory.get(memory_id=...)` | Raises `ValueError` if ID is invalid. |
|
||||
| Update memory | `await memory.update(memory_id=..., text=...)` | Accepts partial updates. |
|
||||
| Update memory | `await memory.update(memory_id=..., data=...)` | Accepts partial updates. |
|
||||
| Delete memory | `await memory.delete(memory_id=...)` | Returns confirmation payload. |
|
||||
| Delete in bulk | `await memory.delete_all(...)` | Requires at least one scope filter. |
|
||||
| History | `await memory.history(memory_id=...)` | Fetches change log for auditing. |
|
||||
@@ -185,7 +185,7 @@ specific_memory = await memory.get(memory_id="memory-id-here")
|
||||
# Update a memory
|
||||
updated_memory = await memory.update(
|
||||
memory_id="memory-id-here",
|
||||
text="I'm travelling to Seattle"
|
||||
data="I'm travelling to Seattle"
|
||||
)
|
||||
|
||||
# Delete a memory
|
||||
|
||||
@@ -18,129 +18,7 @@ Reranker-enhanced search adds a second scoring pass after vector retrieval so Me
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
The `Configure it` and `See it in action` snippets below use the Python SDK. The self-hosted **TypeScript SDK** supports the Cohere, Zero Entropy, Sentence Transformer, Hugging Face, and LLM rerankers; see [TypeScript SDK](#typescript-sdk).
|
||||
</Note>
|
||||
|
||||
---
|
||||
|
||||
## TypeScript SDK
|
||||
|
||||
The self-hosted TypeScript SDK (`mem0ai/oss`) ships five rerankers: **Cohere**, **Zero Entropy**, **Sentence Transformer**, **Hugging Face**, and the **LLM reranker**. Configure one under `reranker`, then opt in per search with `rerank: true`. Keys are camelCase (`apiKey`, not `api_key`).
|
||||
|
||||
Provider SDKs are peer dependencies. Install the one your reranker needs:
|
||||
|
||||
```bash
|
||||
pnpm add cohere-ai # cohere
|
||||
pnpm add zeroentropy # zero_entropy
|
||||
pnpm add @huggingface/transformers # sentence_transformer, huggingface
|
||||
# llm_reranker defaults to openai (already a core dependency); install another
|
||||
# provider's SDK only if you nest a different one under config.llm
|
||||
```
|
||||
|
||||
### Hosted rerankers (Cohere, Zero Entropy)
|
||||
|
||||
Both call a hosted API and read their key from config or the provider's environment variable (`COHERE_API_KEY`, `ZERO_ENTROPY_API_KEY`).
|
||||
|
||||
```typescript
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
// Cohere reranker (defaults to the rerank-v3.5 model)
|
||||
const memory = new Memory({
|
||||
reranker: {
|
||||
provider: "cohere",
|
||||
config: { apiKey: process.env.COHERE_API_KEY },
|
||||
},
|
||||
});
|
||||
|
||||
const results = await memory.search("What are my food preferences?", {
|
||||
filters: { userId: "alice" },
|
||||
rerank: true,
|
||||
});
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Zero Entropy reranker (defaults to the zerank-1 model)
|
||||
const memory = new Memory({
|
||||
reranker: {
|
||||
provider: "zero_entropy",
|
||||
config: { apiKey: process.env.ZERO_ENTROPY_API_KEY },
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Local cross-encoders (Sentence Transformer, Hugging Face)
|
||||
|
||||
Both run a cross-encoder locally with [Transformers.js](https://huggingface.co/docs/transformers.js): no API key, no network at inference time. Because Transformers.js runs ONNX weights, the default models are the ONNX mirrors of the Python SDK's defaults (`sentence_transformer` → `Xenova/ms-marco-MiniLM-L-6-v2`, `huggingface` → `Xenova/bge-reranker-base`). Point `model` at any ONNX-exported cross-encoder on the Hub to override.
|
||||
|
||||
```typescript
|
||||
const memory = new Memory({
|
||||
reranker: {
|
||||
provider: "sentence_transformer", // or "huggingface"
|
||||
config: {
|
||||
// model: "Xenova/bge-reranker-base", // override the default
|
||||
device: "cpu", // Transformers.js device: "cpu" | "wasm" | "webgpu"
|
||||
maxLength: 512, // max tokens per query-document pair
|
||||
normalize: true, // sigmoid-normalize logits to [0, 1] (default)
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const results = await memory.search("What movies do I like?", {
|
||||
filters: { userId: "alice" },
|
||||
rerank: true,
|
||||
});
|
||||
```
|
||||
|
||||
<Note>
|
||||
`batchSize` and `showProgressBar` are accepted for config parity with the Python SDK but are no-ops in this runtime, because a memory search reranks a small candidate set in a single in-process forward pass. The model is downloaded once and cached in-process on first use.
|
||||
</Note>
|
||||
|
||||
### LLM reranker
|
||||
|
||||
To score with an LLM instead of a dedicated reranker, use the `llm_reranker` provider. It builds its own LLM from the reranker's config (defaulting to `openai` / `gpt-4o-mini`) rather than reusing the Memory's main `llm`:
|
||||
|
||||
```typescript
|
||||
const memory = new Memory({
|
||||
reranker: {
|
||||
provider: "llm_reranker",
|
||||
config: { apiKey: process.env.OPENAI_API_KEY },
|
||||
},
|
||||
});
|
||||
|
||||
const results = await memory.search("What movies do I like?", {
|
||||
filters: { userId: "alice" },
|
||||
rerank: true,
|
||||
});
|
||||
```
|
||||
|
||||
Nest a different provider under `config.llm` to override the default:
|
||||
|
||||
```typescript
|
||||
const memory = new Memory({
|
||||
reranker: {
|
||||
provider: "llm_reranker",
|
||||
config: {
|
||||
llm: {
|
||||
provider: "anthropic",
|
||||
config: { apiKey: process.env.ANTHROPIC_API_KEY },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Config reference
|
||||
|
||||
| Provider | Default model | Key config fields |
|
||||
| --- | --- | --- |
|
||||
| `cohere` | `rerank-v3.5` | `apiKey`, `model`, `topK` |
|
||||
| `zero_entropy` | `zerank-1` | `apiKey`, `model`, `topK` |
|
||||
| `sentence_transformer` | `Xenova/ms-marco-MiniLM-L-6-v2` | `model`, `device`, `maxLength`, `normalize`, `topK` |
|
||||
| `huggingface` | `Xenova/bge-reranker-base` | `model`, `device`, `maxLength`, `normalize`, `topK` |
|
||||
| `llm_reranker` | `openai` / `gpt-4o-mini` | `provider`, `model`, `apiKey`, `llm` (nested override), `topK` |
|
||||
|
||||
<Note>
|
||||
`rerank` is opt-in per search and a no-op when no `reranker` is configured. If the reranker call fails, Mem0 logs a warning and returns the original vector-ranked results.
|
||||
All configuration snippets translate directly to the TypeScript SDK: swap dictionaries for objects while keeping the same keys (`provider`, `config`, `rerank` flags).
|
||||
</Note>
|
||||
|
||||
---
|
||||
@@ -183,7 +61,7 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "cohere",
|
||||
"config": {
|
||||
"model": "rerank-v3.5",
|
||||
"model": "rerank-english-v3.0",
|
||||
"api_key": "your-cohere-api-key"
|
||||
}
|
||||
}
|
||||
@@ -208,7 +86,7 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "cohere",
|
||||
"config": {
|
||||
"model": "rerank-v3.5",
|
||||
"model": "rerank-english-v3.0",
|
||||
"api_key": "your-cohere-api-key",
|
||||
"top_k": 10,
|
||||
"return_documents": True
|
||||
@@ -286,7 +164,7 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "cohere",
|
||||
"config": {
|
||||
"model": "rerank-v3.5",
|
||||
"model": "rerank-english-v3.0",
|
||||
"api_key": "your-cohere-api-key",
|
||||
"top_k": 15,
|
||||
"return_documents": True
|
||||
@@ -460,7 +338,7 @@ config = {
|
||||
"reranker": {
|
||||
"provider": "cohere",
|
||||
"config": {
|
||||
"model": "rerank-v3.5",
|
||||
"model": "rerank-english-v3.0",
|
||||
"api_key": "your-cohere-api-key"
|
||||
}
|
||||
}
|
||||
|
||||
+16
-27
@@ -1232,7 +1232,7 @@
|
||||
"tags": [
|
||||
"memories"
|
||||
],
|
||||
"description": "Delete memories by filter. At least one filter is required. Previously, omitting all filters silently deleted everything; now it returns a validation error.",
|
||||
"description": "Delete memories by filter. At least one filter is required — previously omitting all filters silently deleted everything; now it returns a validation error.",
|
||||
"operationId": "memories_delete_all",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1315,15 +1315,15 @@
|
||||
"x-code-samples": [
|
||||
{
|
||||
"lang": "Python",
|
||||
"source": "# To use the Python SDK, install the package:\n# pip install mem0ai\n\nfrom mem0 import MemoryClient\nclient = MemoryClient(api_key=\"your_api_key\")\n\n# Delete all memories for a specific user\nclient.delete_all(user_id=\"<user_id>\")\n\n# Delete all memories for every user in the project (wildcard)\nclient.delete_all(user_id=\"*\")\n\n# Full project wipe: all four filters must be explicitly set to \"*\"\nclient.delete_all(user_id=\"*\", agent_id=\"*\", app_id=\"*\", run_id=\"*\")\n\n# NOTE: Calling delete_all() with no filters raises a validation error.\n# At least one filter is required to prevent accidental data loss."
|
||||
"source": "# To use the Python SDK, install the package:\n# pip install mem0ai\n\nfrom mem0 import MemoryClient\nclient = MemoryClient(api_key=\"your_api_key\")\n\n# Delete all memories for a specific user\nclient.delete_all(user_id=\"<user_id>\")\n\n# Delete all memories for every user in the project (wildcard)\nclient.delete_all(user_id=\"*\")\n\n# Full project wipe — all four filters must be explicitly set to \"*\"\nclient.delete_all(user_id=\"*\", agent_id=\"*\", app_id=\"*\", run_id=\"*\")\n\n# NOTE: Calling delete_all() with no filters raises a validation error.\n# At least one filter is required to prevent accidental data loss."
|
||||
},
|
||||
{
|
||||
"lang": "JavaScript",
|
||||
"source": "// To use the JavaScript SDK, install the package:\n// npm i mem0ai\n\nimport MemoryClient from 'mem0ai';\nconst client = new MemoryClient({ apiKey: \"your-api-key\" });\n\n// Delete all memories for a specific user\nclient.deleteAll({ user_id: \"<user_id>\" })\n .then(result => console.log(result))\n .catch(error => console.error(error));\n\n// Delete all memories for every user in the project (wildcard)\nclient.deleteAll({ user_id: \"*\" })\n .then(result => console.log(result))\n .catch(error => console.error(error));\n\n// Full project wipe: all four filters must be explicitly set to \"*\"\nclient.deleteAll({ user_id: \"*\", agent_id: \"*\", app_id: \"*\", run_id: \"*\" })\n .then(result => console.log(result))\n .catch(error => console.error(error));"
|
||||
"source": "// To use the JavaScript SDK, install the package:\n// npm i mem0ai\n\nimport MemoryClient from 'mem0ai';\nconst client = new MemoryClient({ apiKey: \"your-api-key\" });\n\n// Delete all memories for a specific user\nclient.deleteAll({ user_id: \"<user_id>\" })\n .then(result => console.log(result))\n .catch(error => console.error(error));\n\n// Delete all memories for every user in the project (wildcard)\nclient.deleteAll({ user_id: \"*\" })\n .then(result => console.log(result))\n .catch(error => console.error(error));\n\n// Full project wipe — all four filters must be explicitly set to \"*\"\nclient.deleteAll({ user_id: \"*\", agent_id: \"*\", app_id: \"*\", run_id: \"*\" })\n .then(result => console.log(result))\n .catch(error => console.error(error));"
|
||||
},
|
||||
{
|
||||
"lang": "cURL",
|
||||
"source": "# Delete memories for a specific user\ncurl --request DELETE \\\n --url 'https://api.mem0.ai/v1/memories/?user_id=<user_id>' \\\n --header 'Authorization: Token <api-key>'\n\n# Delete memories for all users (wildcard)\ncurl --request DELETE \\\n --url 'https://api.mem0.ai/v1/memories/?user_id=*' \\\n --header 'Authorization: Token <api-key>'\n\n# Full project wipe: all four filters must be set to *\ncurl --request DELETE \\\n --url 'https://api.mem0.ai/v1/memories/?user_id=*&agent_id=*&app_id=*&run_id=*' \\\n --header 'Authorization: Token <api-key>'"
|
||||
"source": "# Delete memories for a specific user\ncurl --request DELETE \\\n --url 'https://api.mem0.ai/v1/memories/?user_id=<user_id>' \\\n --header 'Authorization: Token <api-key>'\n\n# Delete memories for all users (wildcard)\ncurl --request DELETE \\\n --url 'https://api.mem0.ai/v1/memories/?user_id=*' \\\n --header 'Authorization: Token <api-key>'\n\n# Full project wipe — all four filters must be set to *\ncurl --request DELETE \\\n --url 'https://api.mem0.ai/v1/memories/?user_id=*&agent_id=*&app_id=*&run_id=*' \\\n --header 'Authorization: Token <api-key>'"
|
||||
},
|
||||
{
|
||||
"lang": "Go",
|
||||
@@ -1740,7 +1740,7 @@
|
||||
"memories"
|
||||
],
|
||||
"summary": "Get all memories (V3, paginated)",
|
||||
"description": "List memories scoped by filters, paginated. Entity IDs **must** be passed inside the `filters` object. Top-level `user_id` / `agent_id` / `run_id` are rejected with 400. `filters` supports the same operator set as V2 search (`AND`, `OR`, `NOT`, `in`, `gte`, `lte`, etc.). Response is a paginated envelope; pass `page` and `page_size` as query parameters to step through results.",
|
||||
"description": "List memories scoped by filters, paginated. Entity IDs **must** be passed inside the `filters` object — top-level `user_id` / `agent_id` / `run_id` are rejected with 400. `filters` supports the same operator set as V2 search (`AND`, `OR`, `NOT`, `in`, `gte`, `lte`, etc.). Response is a paginated envelope; pass `page` and `page_size` as query parameters to step through results.",
|
||||
"operationId": "memories_list_v3",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1896,10 +1896,10 @@
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Validation error, e.g. empty `filters` or no positively-scoped entity ID."
|
||||
"description": "Validation error — e.g. empty `filters` or no positively-scoped entity ID."
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized: missing or invalid API key."
|
||||
"description": "Unauthorized — missing or invalid API key."
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
@@ -1992,17 +1992,6 @@
|
||||
"type": "string",
|
||||
"description": "Project-level instructions that guide extraction for this call."
|
||||
},
|
||||
"custom_categories": {
|
||||
"type": "array",
|
||||
"description": "Category catalog for this call. Replaces the project-level list rather than merging with it. Omit to fall back to the project list, then the default catalog.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Maps a category name to the description the classifier matches against."
|
||||
}
|
||||
},
|
||||
"infer": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@@ -2018,7 +2007,7 @@
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "Got it, I'll update your location."
|
||||
"content": "Got it — I'll update your location."
|
||||
}
|
||||
],
|
||||
"user_id": "alice"
|
||||
@@ -2060,10 +2049,10 @@
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Validation error, e.g. missing `messages` or no entity ID supplied."
|
||||
"description": "Validation error — e.g. missing `messages` or no entity ID supplied."
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized: missing or invalid API key."
|
||||
"description": "Unauthorized — missing or invalid API key."
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
@@ -2074,15 +2063,15 @@
|
||||
"x-codeSamples": [
|
||||
{
|
||||
"lang": "cURL",
|
||||
"source": "curl -X POST https://api.mem0.ai/v3/memories/add/ \\\n -H \"Authorization: Token <api-key>\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"messages\": [\n {\"role\": \"user\", \"content\": \"I just moved to San Francisco from New York.\"},\n {\"role\": \"assistant\", \"content\": \"Got it, I\\u0027ll update your location.\"}\n ],\n \"user_id\": \"alice\"\n }'"
|
||||
"source": "curl -X POST https://api.mem0.ai/v3/memories/add/ \\\n -H \"Authorization: Token <api-key>\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"messages\": [\n {\"role\": \"user\", \"content\": \"I just moved to San Francisco from New York.\"},\n {\"role\": \"assistant\", \"content\": \"Got it — I\\u0027ll update your location.\"}\n ],\n \"user_id\": \"alice\"\n }'"
|
||||
},
|
||||
{
|
||||
"lang": "Python",
|
||||
"source": "from mem0 import MemoryClient\n\nclient = MemoryClient(api_key=\"your-api-key\")\n\nresult = client.add(\n messages=[\n {\"role\": \"user\", \"content\": \"I just moved to San Francisco from New York.\"},\n {\"role\": \"assistant\", \"content\": \"Got it, I'll update your location.\"}\n ],\n user_id=\"alice\",\n)\nprint(result)"
|
||||
"source": "from mem0 import MemoryClient\n\nclient = MemoryClient(api_key=\"your-api-key\")\n\nresult = client.add(\n messages=[\n {\"role\": \"user\", \"content\": \"I just moved to San Francisco from New York.\"},\n {\"role\": \"assistant\", \"content\": \"Got it — I'll update your location.\"}\n ],\n user_id=\"alice\",\n)\nprint(result)"
|
||||
},
|
||||
{
|
||||
"lang": "JavaScript",
|
||||
"source": "import MemoryClient from \"mem0ai\";\n\nconst client = new MemoryClient({ apiKey: \"your-api-key\" });\n\nconst result = await client.add(\n [\n { role: \"user\", content: \"I just moved to San Francisco from New York.\" },\n { role: \"assistant\", content: \"Got it, I'll update your location.\" },\n ],\n { userId: \"alice\" }\n);\nconsole.log(result);"
|
||||
"source": "import MemoryClient from \"mem0ai\";\n\nconst client = new MemoryClient({ apiKey: \"your-api-key\" });\n\nconst result = await client.add(\n [\n { role: \"user\", content: \"I just moved to San Francisco from New York.\" },\n { role: \"assistant\", content: \"Got it — I'll update your location.\" },\n ],\n { userId: \"alice\" }\n);\nconsole.log(result);"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2093,7 +2082,7 @@
|
||||
"memories"
|
||||
],
|
||||
"summary": "Search memories (V3)",
|
||||
"description": "Relevance-ranked search across stored memories. V3 uses hybrid retrieval and can also apply temporal reasoning for time-aware queries. Entity IDs **must** be passed inside the `filters` object. Top-level `user_id` / `agent_id` / `run_id` are rejected with 400. At least one entity ID is required.",
|
||||
"description": "Relevance-ranked search across stored memories. V3 uses hybrid retrieval and can also apply temporal reasoning for time-aware queries. Entity IDs **must** be passed inside the `filters` object — top-level `user_id` / `agent_id` / `run_id` are rejected with 400. At least one entity ID is required.",
|
||||
"operationId": "memories_search_v3",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
@@ -2247,10 +2236,10 @@
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Validation error, e.g. empty `query`, missing `filters`, or no positively-scoped entity ID."
|
||||
"description": "Validation error — e.g. empty `query`, missing `filters`, or no positively-scoped entity ID."
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized: missing or invalid API key."
|
||||
"description": "Unauthorized — missing or invalid API key."
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
|
||||
@@ -129,7 +129,7 @@ matches = await memory.search(
|
||||
```python
|
||||
await memory.update(
|
||||
memory_id=matches["results"][0]["id"],
|
||||
text="Morgan avoids shellfish and prefers boutique hotels in central Tokyo.",
|
||||
data="Morgan avoids shellfish and prefers boutique hotels in central Tokyo.",
|
||||
)
|
||||
```
|
||||
</Step>
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
---
|
||||
title: Contextual Memory Creation
|
||||
description: "Add messages with automatic context management - no manual history tracking required"
|
||||
---
|
||||
|
||||
## What is Contextual Memory Creation?
|
||||
|
||||
Contextual memory creation automatically manages message history, allowing you to focus on building AI experiences without manually tracking interactions. Simply send new messages, and Mem0 handles the context automatically.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Just send new messages - Mem0 handles the context
|
||||
messages = [
|
||||
{"role": "user", "content": "I love Italian food, especially pasta"},
|
||||
{"role": "assistant", "content": "Great! I'll remember your preference for Italian cuisine."}
|
||||
]
|
||||
|
||||
client.add(messages, user_id="user123")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
// Just send new messages - Mem0 handles the context
|
||||
const messages = [
|
||||
{"role": "user", "content": "I love Italian food, especially pasta"},
|
||||
{"role": "assistant", "content": "Great! I'll remember your preference for Italian cuisine."}
|
||||
];
|
||||
|
||||
await client.add(messages, { userId: "user123" });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Why Use Contextual Memory Creation?
|
||||
|
||||
- **Simple**: Send only new messages, no manual history tracking
|
||||
- **Efficient**: Smaller payloads and faster processing
|
||||
- **Automatic**: Context management handled by Mem0
|
||||
- **Reliable**: No risk of missing interaction history
|
||||
- **Scalable**: Works seamlessly as your application grows
|
||||
|
||||
## How It Works
|
||||
|
||||
### Basic Usage
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# First interaction
|
||||
messages1 = [
|
||||
{"role": "user", "content": "Hi, I'm Sarah from New York"},
|
||||
{"role": "assistant", "content": "Hello Sarah! Nice to meet you."}
|
||||
]
|
||||
client.add(messages1, user_id="sarah")
|
||||
|
||||
# Later interaction - just send new messages
|
||||
messages2 = [
|
||||
{"role": "user", "content": "I'm planning a trip to Italy next month"},
|
||||
{"role": "assistant", "content": "How exciting! Italy is beautiful this time of year."}
|
||||
]
|
||||
client.add(messages2, user_id="sarah")
|
||||
# Mem0 automatically knows Sarah is from New York and can use this context
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
// First interaction
|
||||
const messages1 = [
|
||||
{"role": "user", "content": "Hi, I'm Sarah from New York"},
|
||||
{"role": "assistant", "content": "Hello Sarah! Nice to meet you."}
|
||||
];
|
||||
await client.add(messages1, { userId: "sarah" });
|
||||
|
||||
// Later interaction - just send new messages
|
||||
const messages2 = [
|
||||
{"role": "user", "content": "I'm planning a trip to Italy next month"},
|
||||
{"role": "assistant", "content": "How exciting! Italy is beautiful this time of year."}
|
||||
];
|
||||
await client.add(messages2, { userId: "sarah" });
|
||||
// Mem0 automatically knows Sarah is from New York and can use this context
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Organization Strategies
|
||||
|
||||
Choose the right approach based on your application's needs:
|
||||
|
||||
### User-Level Memories (`user_id` only)
|
||||
|
||||
**Best for:** Personal preferences, profile information, long-term user data
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Persistent user memories across all interactions
|
||||
messages = [
|
||||
{"role": "user", "content": "I'm allergic to nuts and dairy"},
|
||||
{"role": "assistant", "content": "I've noted your allergies for future reference."}
|
||||
]
|
||||
|
||||
client.add(messages, user_id="user123")
|
||||
# This allergy info will be available in ALL future interactions
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
// Persistent user memories across all interactions
|
||||
const messages = [
|
||||
{"role": "user", "content": "I'm allergic to nuts and dairy"},
|
||||
{"role": "assistant", "content": "I've noted your allergies for future reference."}
|
||||
];
|
||||
|
||||
await client.add(messages, { userId: "user123" });
|
||||
// This allergy info will be available in ALL future interactions
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
### Session-Specific Memories (`user_id` + `run_id`)
|
||||
|
||||
**Best for:** Task-specific context, separate interaction threads, project-based sessions
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Trip planning session
|
||||
messages1 = [
|
||||
{"role": "user", "content": "I want to plan a 5-day trip to Tokyo"},
|
||||
{"role": "assistant", "content": "Perfect! Let's plan your Tokyo adventure."}
|
||||
]
|
||||
client.add(messages1, user_id="user123", run_id="tokyo-trip-2024")
|
||||
|
||||
# Later in the same trip planning session
|
||||
messages2 = [
|
||||
{"role": "user", "content": "I prefer staying near Shibuya"},
|
||||
{"role": "assistant", "content": "Great choice! Shibuya is very convenient."}
|
||||
]
|
||||
client.add(messages2, user_id="user123", run_id="tokyo-trip-2024")
|
||||
|
||||
# Different session for work project (separate context)
|
||||
work_messages = [
|
||||
{"role": "user", "content": "Let's discuss the Q4 marketing strategy"},
|
||||
{"role": "assistant", "content": "Sure! What are your main goals for Q4?"}
|
||||
]
|
||||
client.add(work_messages, user_id="user123", run_id="q4-marketing")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
// Trip planning session
|
||||
const messages1 = [
|
||||
{"role": "user", "content": "I want to plan a 5-day trip to Tokyo"},
|
||||
{"role": "assistant", "content": "Perfect! Let's plan your Tokyo adventure."}
|
||||
];
|
||||
await client.add(messages1, { userId: "user123", runId: "tokyo-trip-2024" });
|
||||
|
||||
// Later in the same trip planning session
|
||||
const messages2 = [
|
||||
{"role": "user", "content": "I prefer staying near Shibuya"},
|
||||
{"role": "assistant", "content": "Great choice! Shibuya is very convenient."}
|
||||
];
|
||||
await client.add(messages2, { userId: "user123", runId: "tokyo-trip-2024" });
|
||||
|
||||
// Different session for work project (separate context)
|
||||
const workMessages = [
|
||||
{"role": "user", "content": "Let's discuss the Q4 marketing strategy"},
|
||||
{"role": "assistant", "content": "Sure! What are your main goals for Q4?"}
|
||||
];
|
||||
await client.add(workMessages, { userId: "user123", runId: "q4-marketing" });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## Real-World Use Cases
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Customer Support">
|
||||
```python Python
|
||||
# Support ticket context - keeps interaction focused
|
||||
messages = [
|
||||
{"role": "user", "content": "My subscription isn't working"},
|
||||
{"role": "assistant", "content": "I can help with that. What specific issue are you experiencing?"},
|
||||
{"role": "user", "content": "I can't access premium features even though I paid"}
|
||||
]
|
||||
|
||||
# Each support ticket gets its own run_id
|
||||
client.add(messages,
|
||||
user_id="customer123",
|
||||
run_id="ticket-2024-001"
|
||||
)
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Personal AI Assistant">
|
||||
```python Python
|
||||
# Personal preferences (persistent across all interactions)
|
||||
preference_messages = [
|
||||
{"role": "user", "content": "I prefer morning workouts and vegetarian meals"},
|
||||
{"role": "assistant", "content": "Got it! I'll keep your fitness and dietary preferences in mind."}
|
||||
]
|
||||
|
||||
client.add(preference_messages, user_id="user456")
|
||||
|
||||
# Daily planning session (session-specific)
|
||||
planning_messages = [
|
||||
{"role": "user", "content": "Help me plan tomorrow's schedule"},
|
||||
{"role": "assistant", "content": "Of course! I'll consider your morning workout preference."}
|
||||
]
|
||||
|
||||
client.add(planning_messages,
|
||||
user_id="user456",
|
||||
run_id="daily-plan-2024-01-15"
|
||||
)
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="Educational Platform">
|
||||
```python Python
|
||||
# Student profile (persistent)
|
||||
profile_messages = [
|
||||
{"role": "user", "content": "I'm studying computer science and struggle with math"},
|
||||
{"role": "assistant", "content": "I'll tailor explanations to help with math concepts."}
|
||||
]
|
||||
|
||||
client.add(profile_messages, user_id="student789")
|
||||
|
||||
# Specific lesson session
|
||||
lesson_messages = [
|
||||
{"role": "user", "content": "Can you explain algorithms?"},
|
||||
{"role": "assistant", "content": "Sure! I'll explain algorithms with math-friendly examples."}
|
||||
]
|
||||
|
||||
client.add(lesson_messages,
|
||||
user_id="student789",
|
||||
run_id="algorithms-lesson-1"
|
||||
)
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Best Practices
|
||||
|
||||
### ✅ Do
|
||||
- **Organize by context scope**: Use `user_id` only for persistent data, add `run_id` for session-specific context
|
||||
- **Keep messages focused** on the current interaction
|
||||
- **Test with real interaction flows** to ensure context works as expected
|
||||
|
||||
### ❌ Don't
|
||||
- Send duplicate messages or interaction history
|
||||
- Skip identifiers like `user_id` or `run_id` that scope the memory
|
||||
- Mix contextual and non-contextual approaches in the same application
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| **Context not working** | Ensure each call uses the same `user_id` / `run_id` combo; version is automatic |
|
||||
| **Wrong context retrieved** | Check if you need separate `run_id` values for different interaction topics |
|
||||
| **Missing interaction history** | Verify all messages in the interaction thread use the same `user_id` and `run_id` |
|
||||
| **Too much irrelevant context** | Use more specific `run_id` values to separate different interaction types |
|
||||
|
||||
|
||||
<Snippet file="get-help.mdx" />
|
||||
@@ -0,0 +1,201 @@
|
||||
---
|
||||
title: Criteria Retrieval
|
||||
description: "Rank and retrieve memories based on custom-defined criteria like emotional tone, intent, and behavioral signals."
|
||||
---
|
||||
|
||||
Mem0's Criteria Retrieval feature allows you to retrieve memories based on your defined criteria. It goes beyond generic semantic relevance and ranks memories based on what matters to your application: emotional tone, intent, behavioral signals, or other custom traits.
|
||||
|
||||
Instead of just searching for "how similar a memory is to this query," you can define what relevance truly means for your project. For example:
|
||||
|
||||
- Prioritize joyful memories when building a wellness assistant
|
||||
- Downrank negative memories in a productivity-focused agent
|
||||
- Highlight curiosity in a tutoring agent
|
||||
|
||||
You define criteria: custom attributes like "joy", "negativity", "confidence", or "urgency", and assign weights to control how they influence scoring. When you search, Mem0 uses these to re-rank semantically relevant memories, favoring those that better match your intent.
|
||||
|
||||
This gives you nuanced, intent-aware memory search that adapts to your use case.
|
||||
|
||||
|
||||
|
||||
## When to Use Criteria Retrieval
|
||||
|
||||
Use Criteria Retrieval if:
|
||||
|
||||
- You’re building an agent that should react to **emotions** or **behavioral signals**
|
||||
- You want to guide memory selection based on **context**, not just content
|
||||
- You have domain-specific signals like "risk", "positivity", "confidence", etc. that shape recall
|
||||
|
||||
|
||||
|
||||
## Setting Up Criteria Retrieval
|
||||
|
||||
Let’s walk through how to configure and use Criteria Retrieval step by step.
|
||||
|
||||
### Initialize the Client
|
||||
|
||||
Before defining any criteria, make sure to initialize the `MemoryClient` with your credentials and project ID:
|
||||
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key="your_mem0_api_key")
|
||||
```
|
||||
|
||||
### Define Your Criteria
|
||||
|
||||
Each criterion includes:
|
||||
- A `name` (used in scoring)
|
||||
- A `description` (interpreted by the LLM)
|
||||
- A `weight` (how much it influences the final score)
|
||||
|
||||
```python
|
||||
retrieval_criteria = [
|
||||
{
|
||||
"name": "joy",
|
||||
"description": "Measure the intensity of positive emotions such as happiness, excitement, or amusement expressed in the sentence. A higher score reflects greater joy.",
|
||||
"weight": 3
|
||||
},
|
||||
{
|
||||
"name": "curiosity",
|
||||
"description": "Assess the extent to which the sentence reflects inquisitiveness, interest in exploring new information, or asking questions. A higher score reflects stronger curiosity.",
|
||||
"weight": 2
|
||||
},
|
||||
{
|
||||
"name": "emotion",
|
||||
"description": "Evaluate the presence and depth of sadness or negative emotional tone, including expressions of disappointment, frustration, or sorrow. A higher score reflects greater sadness.",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Apply Criteria to Your Project
|
||||
|
||||
Once defined, register the criteria to your project:
|
||||
|
||||
```python
|
||||
client.project.update(retrieval_criteria=retrieval_criteria)
|
||||
```
|
||||
|
||||
Criteria apply project-wide. Once set, they affect all searches automatically.
|
||||
|
||||
|
||||
## Example Walkthrough
|
||||
|
||||
After setting up your criteria, you can use them to filter and retrieve memories. Here's an example:
|
||||
|
||||
### Add Memories
|
||||
|
||||
```python
|
||||
messages = [
|
||||
{"role": "user", "content": "What a beautiful sunny day! I feel so refreshed and ready to take on anything!"},
|
||||
{"role": "user", "content": "I've always wondered how storms form, what triggers them in the atmosphere?"},
|
||||
{"role": "user", "content": "It's been raining for days, and it just makes everything feel heavier."},
|
||||
{"role": "user", "content": "Finally I get time to draw something today, after a long time!! I am super happy today."}
|
||||
]
|
||||
|
||||
client.add(messages, user_id="alice")
|
||||
```
|
||||
|
||||
### Run Standard vs. Criteria-Based Search
|
||||
|
||||
```python
|
||||
# Search with criteria enabled
|
||||
filters = {"user_id": "alice"}
|
||||
results_with_criteria = client.search(
|
||||
query="Why I am feeling happy today?",
|
||||
filters=filters
|
||||
)
|
||||
|
||||
# To disable criteria for a specific search
|
||||
results_without_criteria = client.search(
|
||||
query="Why I am feeling happy today?",
|
||||
filters=filters,
|
||||
use_criteria=False # Disable criteria-based scoring
|
||||
)
|
||||
```
|
||||
|
||||
### Compare Results
|
||||
|
||||
### Search Results (with Criteria)
|
||||
```text
|
||||
[
|
||||
{"memory": "User feels refreshed and ready to take on anything on a beautiful sunny day", "score": 0.666, ...},
|
||||
{"memory": "User finally has time to draw something after a long time", "score": 0.616, ...},
|
||||
{"memory": "User is happy today", "score": 0.500, ...},
|
||||
{"memory": "User is curious about how storms form and what triggers them in the atmosphere.", "score": 0.400, ...},
|
||||
{"memory": "It has been raining for days, making everything feel heavier.", "score": 0.116, ...}
|
||||
]
|
||||
```
|
||||
|
||||
### Search Results (without Criteria)
|
||||
```text
|
||||
[
|
||||
{"memory": "User is happy today", "score": 0.607, ...},
|
||||
{"memory": "User feels refreshed and ready to take on anything on a beautiful sunny day", "score": 0.512, ...},
|
||||
{"memory": "It has been raining for days, making everything feel heavier.", "score": 0.4617, ...},
|
||||
{"memory": "User is curious about how storms form and what triggers them in the atmosphere.", "score": 0.340, ...},
|
||||
{"memory": "User finally has time to draw something after a long time", "score": 0.336, ...},
|
||||
]
|
||||
```
|
||||
|
||||
## Search Results Comparison
|
||||
|
||||
1. **Memory Ordering**: With criteria, memories with high joy scores (like feeling refreshed and drawing) are ranked higher. Without criteria, the most relevant memory ("User is happy today") comes first.
|
||||
2. **Score Distribution**: With criteria, scores are more spread out (0.116 to 0.666) and reflect the criteria weights. Without criteria, scores are more clustered (0.336 to 0.607) and based purely on relevance.
|
||||
3. **Trait Sensitivity**: "Rainy day" content is penalized due to negative tone, while "Storm curiosity" is recognized and scored accordingly.
|
||||
|
||||
|
||||
|
||||
## Key Differences vs. Standard Search
|
||||
|
||||
| Aspect | Standard Search | Criteria Retrieval |
|
||||
|-------------------------|--------------------------------------|-------------------------------------------------|
|
||||
| Ranking Logic | Semantic similarity only | Semantic + LLM-based criteria scoring |
|
||||
| Control Over Relevance | None | Fully customizable with weighted criteria |
|
||||
| Memory Reordering | Static based on similarity | Dynamically re-ranked by intent alignment |
|
||||
| Emotional Sensitivity | No tone or trait awareness | Incorporates emotion, tone, or custom behaviors |
|
||||
| Activation | Default (no criteria defined) | Enabled when criteria are defined in project |
|
||||
|
||||
<Note>
|
||||
If no criteria are defined for a project, search behaves normally based on semantic similarity only.
|
||||
</Note>
|
||||
|
||||
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Choose 3-5 criteria that reflect your application's intent
|
||||
- Make descriptions clear and distinct; these are interpreted by an LLM
|
||||
- Use stronger weights to amplify the impact of important traits
|
||||
- Avoid redundant or ambiguous criteria (e.g., "positivity" and "joy")
|
||||
- Always handle empty result sets in your application logic
|
||||
|
||||
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Criteria Definition**: Define custom criteria with a name, description, and weight. These describe what matters in a memory (e.g., joy, urgency, empathy).
|
||||
2. **Project Configuration**: Register these criteria using `project.update()`. They apply at the project level and automatically influence all searches.
|
||||
3. **Memory Retrieval**: When you perform a search, Mem0 first retrieves relevant memories based on the query.
|
||||
4. **Weighted Scoring**: Each retrieved memory is evaluated and scored against your defined criteria and weights.
|
||||
|
||||
This lets you prioritize memories that align with your agent's goals and not just those that look similar to the query.
|
||||
|
||||
<Note>
|
||||
Criteria retrieval is automatically enabled when criteria are defined in your project. Use `use_criteria=False` in search to temporarily disable it for a specific query. `use_criteria` is a server-side parameter passed through to the Platform API: it is not a typed option in the SDK's `SearchMemoryOptions` interface, but the server accepts and processes it when included in the request body.
|
||||
</Note>
|
||||
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
- Define what "relevant" means using criteria
|
||||
- Apply them per project via `project.update()`
|
||||
- Criteria-aware search activates automatically when criteria are configured
|
||||
- Build agents that reason not just with relevance, but **contextual importance**
|
||||
|
||||
---
|
||||
|
||||
Need help designing or tuning your criteria?
|
||||
|
||||
<Snippet file="get-help.mdx" />
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Custom Categories
|
||||
description: "Replace default memory tags with custom category labels that match your product terminology, set once per project or per individual add call."
|
||||
description: "Replace default memory tags with custom category labels that match your product terminology at the project level."
|
||||
---
|
||||
|
||||
# Custom Categories
|
||||
@@ -14,7 +14,9 @@ Mem0 automatically tags every memory, but the default labels (travel, sports, mu
|
||||
- You’re moving from the open-source version and want the same labels here.
|
||||
</Info>
|
||||
|
||||
You can set the list once for the whole project, or pass a different list on an individual `add` call.
|
||||
<Warning>
|
||||
Per-request overrides (`custom_categories=...` on `client.add`) are not supported on the managed API yet. Set categories at the project level, then ingest memories as usual.
|
||||
</Warning>
|
||||
|
||||
## Configure access
|
||||
|
||||
@@ -25,20 +27,7 @@ You can set the list once for the whole project, or pass a different list on an
|
||||
|
||||
- **Default list**: Each project starts with 15 broad categories like `travel`, `sports`, and `music`.
|
||||
- **Project override**: When you call `project.update(custom_categories=[...])`, that list replaces the defaults for future memories.
|
||||
- **Per-call override**: When you pass `custom_categories=[...]` to `client.add(...)`, that list is used for the memories extracted from that call.
|
||||
- **Automatic tags**: As new memories come in, Mem0 picks the closest matches from the active list and saves them in the `categories` field.
|
||||
|
||||
### Which list wins
|
||||
|
||||
Mem0 resolves the category catalog for each `add` call in this order, and stops at the first one it finds:
|
||||
|
||||
1. `custom_categories` passed on the `add` call
|
||||
2. `custom_categories` set on the project
|
||||
3. The built-in default catalog
|
||||
|
||||
A per-call list **fully replaces** the project list for that call. The two are not merged, so a memory added with a per-call list can only be tagged with categories from that list.
|
||||
|
||||
Categories are applied at ingestion time. Changing the project list, or passing a new per-call list, does not re-tag memories that already exist.
|
||||
- **Automatic tags**: As new memories come in, Mem0 picks the closest matches from your list and saves them in the `categories` field.
|
||||
|
||||
<Note>
|
||||
Default catalog: `personal_details`, `family`, `professional_details`, `sports`, `travel`, `food`, `music`, `health`, `technology`, `hobbies`, `fashion`, `entertainment`, `milestones`, `user_preferences`, `misc`.
|
||||
@@ -95,82 +84,6 @@ print(categories)
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
`get` echoes back the shape you set. `update` also accepts a plain list of names, such as `["billing", "support"]`, in which case `get` returns that same list of names. Descriptions are optional here, and the classifier uses them to disambiguate when it has them.
|
||||
|
||||
<Warning>
|
||||
`add` is stricter than `update`. Every entry in a per-call `custom_categories` list must be an object mapping a name to a description. Passing bare names to `add` fails with `400 Expected a dictionary of items but got type "str"`.
|
||||
</Warning>
|
||||
|
||||
### 3. Override categories on a single add call
|
||||
|
||||
Pass `custom_categories` directly to `add` when one call needs a different catalog than the project default. The memories created by that call are tagged from the list you pass, and the project list is left untouched.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
health_messages = [
|
||||
{"role": "user", "content": "My doctor bumped my metformin to 1000mg and I see her again on the 14th."},
|
||||
{"role": "assistant", "content": "Noted the new dosage and the follow-up appointment."},
|
||||
]
|
||||
|
||||
health_categories = [
|
||||
{"symptoms": "Reported physical or mental symptoms"},
|
||||
{"medications": "Prescriptions, dosages, and adherence"},
|
||||
{"appointments": "Scheduled visits and follow-ups"},
|
||||
]
|
||||
|
||||
client.add(
|
||||
health_messages,
|
||||
user_id="alice",
|
||||
custom_categories=health_categories,
|
||||
)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
const healthMessages = [
|
||||
{ role: "user", content: "My doctor bumped my metformin to 1000mg and I see her again on the 14th." },
|
||||
{ role: "assistant", content: "Noted the new dosage and the follow-up appointment." },
|
||||
];
|
||||
|
||||
const healthCategories = [
|
||||
{ symptoms: "Reported physical or mental symptoms" },
|
||||
{ medications: "Prescriptions, dosages, and adherence" },
|
||||
{ appointments: "Scheduled visits and follow-ups" },
|
||||
];
|
||||
|
||||
await client.add(healthMessages, {
|
||||
userId: "alice",
|
||||
customCategories: healthCategories,
|
||||
});
|
||||
```
|
||||
|
||||
```text Resulting categories
|
||||
["medications", "appointments"]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The memory is tagged from `health_categories` alone. The project catalog is not consulted for this call, and it is not modified.
|
||||
|
||||
#### Per-user categories inside one project
|
||||
|
||||
The main reason to reach for a per-call list is to give different users, tenants, or entities their own vocabulary without splitting them across projects. Keep one project, and pass the list that fits the entity you are writing for.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
patient_categories = [
|
||||
{"symptoms": "Reported physical or mental symptoms"},
|
||||
{"medications": "Prescriptions, dosages, and adherence"},
|
||||
]
|
||||
|
||||
clinician_categories = [
|
||||
{"caseload": "Patients under this clinician's care"},
|
||||
{"availability": "Shift patterns and on-call windows"},
|
||||
]
|
||||
|
||||
client.add("My metformin is now 1000mg.", user_id="alice", custom_categories=patient_categories)
|
||||
client.add("I'm on call Tuesdays and Thursdays.", user_id="dr-reyes", custom_categories=clinician_categories)
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## See it in action
|
||||
|
||||
### Add a memory (uses the project catalog automatically)
|
||||
@@ -191,57 +104,46 @@ client.add(messages, user_id="alice")
|
||||
|
||||
### Retrieve memories and inspect categories
|
||||
|
||||
`get_all` returns a paginated object. The memories are under `results`, and each one carries its own `categories` list.
|
||||
|
||||
<CodeGroup>
|
||||
```python Code
|
||||
response = client.get_all(filters={"user_id": "alice"})
|
||||
|
||||
for memory in response["results"]:
|
||||
print(memory["memory"], memory["categories"])
|
||||
memories = client.get_all(filters={"user_id": "alice"})
|
||||
```
|
||||
|
||||
```text Output
|
||||
User introduced herself as Alice and expressed a desire for help organizing her daily schedule. ['lifestyle_management_concerns', 'seeking_structure', 'personal_information']
|
||||
User feels overwhelmed trying to balance work responsibilities, regular exercise, and a social life, indicating difficulty managing time across these areas. ['lifestyle_management_concerns']
|
||||
User's goals include becoming more productive at work, maintaining a consistent workout routine, and preserving enough energy for friends and hobbies. ['lifestyle_management_concerns', 'seeking_structure']
|
||||
```json Output
|
||||
["lifestyle_management_concerns", "seeking_structure"]
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Extraction is model driven, so the exact wording and the number of memories vary between runs. The categories are drawn from the active list.
|
||||
|
||||
<Info>
|
||||
**Sample memory payload**
|
||||
```json
|
||||
{
|
||||
"id": "638008c4-***",
|
||||
"memory": "User is seeking to balance work responsibilities with regular workout sessions and requests a personalized schedule to manage both.",
|
||||
"id": "33d2***",
|
||||
"memory": "Trying to balance work and workouts",
|
||||
"user_id": "alice",
|
||||
"metadata": null,
|
||||
"categories": ["lifestyle_management_concerns", "seeking_structure"],
|
||||
"created_at": "2026-07-10T06:13:12-07:00",
|
||||
"updated_at": "2026-07-10T06:13:20-07:00",
|
||||
"categories": ["wellness"], // ← matches the custom category we set
|
||||
"created_at": "2025-11-01T02:13:32.828364-07:00",
|
||||
"updated_at": "2025-11-01T02:13:32.830896-07:00",
|
||||
"expiration_date": null,
|
||||
"structured_attributes": {
|
||||
"year": 2026,
|
||||
"month": 7,
|
||||
"day": 10,
|
||||
"hour": 13,
|
||||
"day": 1,
|
||||
"hour": 9,
|
||||
"year": 2025,
|
||||
"month": 11,
|
||||
"minute": 13,
|
||||
"day_of_week": "friday",
|
||||
"week_of_year": 28,
|
||||
"day_of_year": 191,
|
||||
"quarter": 3,
|
||||
"is_weekend": false
|
||||
"quarter": 4,
|
||||
"is_weekend": true,
|
||||
"day_of_week": "saturday",
|
||||
"day_of_year": 305,
|
||||
"week_of_year": 44
|
||||
}
|
||||
}
|
||||
```
|
||||
</Info>
|
||||
|
||||
Categorization runs asynchronously, a moment after the memory itself is written. A memory fetched immediately after `add` may not show up in `get_all` yet, or can come back with `categories: null` and pick up its tags a moment later. Poll until `categories` is populated rather than reading once.
|
||||
|
||||
<Note>
|
||||
Need ad-hoc labels for a single call? Pass `custom_categories` on that `add` call. Use `metadata` instead when the label is a fixed value you already know, rather than something the classifier should infer.
|
||||
Need ad-hoc labels for a single call? Store them in `metadata` until per-request overrides become available.
|
||||
</Note>
|
||||
|
||||
## Default categories (fallback)
|
||||
@@ -306,13 +208,11 @@ client.project.get(["custom_categories"])
|
||||
|
||||
```json Output
|
||||
{
|
||||
"custom_categories": null
|
||||
"custom_categories": None
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
A project that has never set a list returns `null`. One you have reset with `project.update(custom_categories=[])` returns `[]`. Both mean the default catalog is active.
|
||||
|
||||
## Verify the feature is working
|
||||
|
||||
- `client.project.get(["custom_categories"])` returns the category list you set.
|
||||
@@ -323,8 +223,7 @@ A project that has never set a list returns `null`. One you have reset with `pro
|
||||
|
||||
- Keep category descriptions concise but specific; the classifier uses them to disambiguate.
|
||||
- Review memories with empty `categories` to see where you might extend or rename your list.
|
||||
- Set the catalog your app uses most often at the project level, and reserve per-call lists for the calls that genuinely need a different vocabulary.
|
||||
- If a per-call list should also keep the project categories, include them in the list you pass. Passing a list replaces, it does not extend.
|
||||
- Stick with project-level overrides until per-request support is released; mixing approaches causes confusion.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Advanced Memory Operations" icon="wand-magic-sparkles" href="/platform/advanced-memory-operations">
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
---
|
||||
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."
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
**Expiration hides a memory, it does not delete it.** The record stays in storage untouched. `search()` and `get_all()` skip it, fetching it by ID still returns it, and clearing the date brings it straight back.
|
||||
|
||||
## How it works
|
||||
|
||||
- **Format**: a plain `YYYY-MM-DD` date. No time component, no timezone offset.
|
||||
- **Evaluated in UTC**, never against the caller's local timezone.
|
||||
- **Inclusive of the date itself**: a memory set to expire on `2030-01-31` stays visible all through `2030-01-31` UTC and disappears on `2030-02-01`.
|
||||
- **Only list-shaped reads filter**: `search()` and `get_all()` (`getAll()` in TypeScript) hide expired memories. <Link href="/api-reference/memory/get-memory">`get(memory_id)`</Link> always returns the memory, so there is no `show_expired` parameter on that path.
|
||||
- **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
|
||||
|
||||
Set it when you add the memory:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
# Mem0 Platform
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key="your-api-key")
|
||||
messages = [{"role": "user", "content": "My Pro trial ends soon."}]
|
||||
|
||||
client.add(messages, user_id="alice", expiration_date="2030-01-31")
|
||||
|
||||
# Mem0 OSS
|
||||
from mem0 import Memory
|
||||
|
||||
memory = Memory()
|
||||
memory.add(messages, user_id="alice", expiration_date="2030-01-31")
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
// Mem0 Platform
|
||||
import { MemoryClient } from "mem0ai";
|
||||
|
||||
const client = new MemoryClient({ apiKey: "your-api-key" });
|
||||
const messages = [{ role: "user", content: "My Pro trial ends soon." }];
|
||||
|
||||
await client.add(messages, { userId: "alice", expirationDate: "2030-01-31" });
|
||||
|
||||
// Mem0 OSS
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const memory = new Memory();
|
||||
await memory.add(messages, { userId: "alice", expirationDate: "2030-01-31" });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Or attach it to a memory that already exists, using `update()`:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
client.update("mem_123", expiration_date="2030-01-31") # Platform
|
||||
memory.update("mem_123", expiration_date="2030-01-31") # OSS
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
await client.update("mem_123", { expirationDate: "2030-01-31" }); // Platform
|
||||
await memory.update("mem_123", { expirationDate: "2030-01-31" }); // OSS
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
Full field lists live in the <Link href="/api-reference/memory/add-memories">Add Memories</Link> and <Link href="/api-reference/memory/update-memory">Update Memory</Link> references.
|
||||
|
||||
## Read expired memories back
|
||||
|
||||
Pass `show_expired` (`showExpired` in TypeScript) to include them. It defaults to `false` on every client.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
client.get_all(filters={"user_id": "alice"}, show_expired=True)
|
||||
client.search("What plan is Alice on?", filters={"user_id": "alice"}, show_expired=True)
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
await client.getAll({ filters: { user_id: "alice" }, showExpired: true });
|
||||
await client.search("What plan is Alice on?", {
|
||||
filters: { user_id: "alice" },
|
||||
showExpired: true,
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The same parameter and spelling work on the OSS `Memory` class. See <Link href="/api-reference/memory/search-memories">Search Memories</Link> and <Link href="/api-reference/memory/get-memories">Get Memories</Link>.
|
||||
|
||||
<Note>
|
||||
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
|
||||
|
||||
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".
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
client.update("mem_123", expiration_date=None) # Platform
|
||||
memory.update("mem_123", expiration_date=None) # OSS
|
||||
```
|
||||
|
||||
```javascript JavaScript
|
||||
await client.update("mem_123", { expirationDate: null }); // Platform
|
||||
await memory.update("mem_123", { expirationDate: null }); // OSS
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
`update()` needs at least one of `text`, `metadata`, or `expiration_date`, and raises if you pass none of them. Clearing the date satisfies that on its own: the memory's content and metadata are left alone.
|
||||
</Note>
|
||||
|
||||
## What each client accepts
|
||||
|
||||
| Client | Accepted input | Notes |
|
||||
| --- | --- | --- |
|
||||
| Python (Platform and OSS) | `str` in `YYYY-MM-DD` form, or a `date` / `datetime` object | Normalized to `YYYY-MM-DD` before storage. |
|
||||
| TypeScript (Platform and OSS) | `string` in `YYYY-MM-DD` form only | Stricter than `new Date()`: rejects `12/31/2099`, `2099-12-31T23:00:00`, and non-days like `2099-02-30` or `2100-02-29`. |
|
||||
| Self-hosted REST server | `string` in `YYYY-MM-DD` form | Same normalization as OSS Python underneath. |
|
||||
| CLI (`mem0 add --expires`) | `string` in `YYYY-MM-DD` form | Must be strictly in the future, checked against the local system date. The SDKs have no such restriction. Platform only: the CLI has no OSS backend. |
|
||||
|
||||
## Reading the field back
|
||||
|
||||
Most clients return expiration as a top-level field on the memory: `expiration_date` in Python (Platform and OSS) and in the REST API, `expirationDate` in the Platform TypeScript SDK.
|
||||
|
||||
<Info>
|
||||
The OSS TypeScript SDK is the one exception. There, expiration round-trips under **`result.metadata.expiration_date`**, not `result.expirationDate`, on both `get()` and `getAll()`.
|
||||
</Info>
|
||||
|
||||
## Expiration, decay, and delete
|
||||
|
||||
These three get conflated. They solve different problems:
|
||||
|
||||
| | Memory Expiration | Memory Decay | Delete |
|
||||
| --- | --- | --- | --- |
|
||||
| What it does | Hides a memory once a date you set passes | Re-ranks results by how recently a memory was used | Removes a memory permanently |
|
||||
| Data still stored? | Yes | Yes | No |
|
||||
| Filters results? | Yes, after the date | Never, it only reorders scores | Yes, permanently |
|
||||
| Reversible? | Yes, clear or push back the date | Yes, toggle `decay` off | No |
|
||||
| Set where | Per memory, by you | Per project, opt-in | Per call |
|
||||
| Available in | Platform and OSS | Platform only | Platform and OSS |
|
||||
|
||||
Reach for <Link href="/platform/features/memory-decay">Memory Decay</Link> when old memories should rank lower but stay searchable, expiration when a memory should stop appearing after a specific known date, and <Link href="/core-concepts/memory-operations/delete">Delete</Link> when it should be gone for good.
|
||||
|
||||
## Common patterns
|
||||
|
||||
**Trial and subscription facts.** "Alice is on the Pro trial" is true until the trial ends. Set `expiration_date` to that end date when you write the fact. If she upgrades, clear the date and the memory becomes permanent. If she doesn't, it stops surfacing the next day on its own.
|
||||
|
||||
**Seasonal preferences.** "Alex wants gift ideas for the holidays" matters in December and is noise in July. A short-lived expiration date keeps it from competing with evergreen preferences in every search.
|
||||
|
||||
**Retention windows.** Data-retention policies usually want a soft window before a hard delete: keep a ticket's memories searchable for 90 days, stop surfacing them, purge them later on a schedule. Expiration is the soft step, and a scheduled <Link href="/core-concepts/memory-operations/delete">delete</Link> is the permanent one.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card
|
||||
title="Memory Decay"
|
||||
description="Rank stale memories lower instead of hiding them outright."
|
||||
icon="chart-line"
|
||||
href="/platform/features/memory-decay"
|
||||
/>
|
||||
<Card
|
||||
title="Delete Memories"
|
||||
description="Remove memories permanently instead of hiding them."
|
||||
icon="trash"
|
||||
href="/core-concepts/memory-operations/delete"
|
||||
/>
|
||||
</CardGroup>
|
||||
|
||||
<Snippet file="get-help.mdx" />
|
||||
@@ -60,6 +60,7 @@ Mem0 offers two powerful ways to add memory to your AI applications. Choose base
|
||||
| **Multimodal support** | ✅ | ✅ |
|
||||
| **Custom categories** | ✅ | Limited |
|
||||
| **Advanced retrieval** | ✅ | ✅ |
|
||||
| **Criteria retrieval** | ✅ | ❌ |
|
||||
| **Temporal reasoning** | ✅ (v3) | ❌ |
|
||||
| **Memory decay** | ✅ (v3) | ❌ |
|
||||
| **Graph memory** | ✅ Built-in | ✅ External graph store |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.2.13",
|
||||
"version": "0.2.12",
|
||||
"description": "Persistent memory for Claude Code. Remembers decisions, patterns, and preferences across sessions.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.2.13",
|
||||
"version": "0.2.12",
|
||||
"description": "Persistent memory for Codex. Remembers decisions, patterns, and preferences across sessions.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.2.13",
|
||||
"version": "0.2.12",
|
||||
"description": "Mem0 memory layer for AI applications. Add persistent memory, personalization, and semantic search using the Mem0 Platform MCP server.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
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,7 +25,6 @@ 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;
|
||||
@@ -259,7 +258,7 @@ function extractUserText(input: any, output: any): string {
|
||||
const Mem0Plugin: Plugin = async (ctx) => {
|
||||
const {$, client} = ctx;
|
||||
|
||||
const apiKey = resolveApiKey();
|
||||
const apiKey = process.env.MEM0_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/opencode-plugin",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.1",
|
||||
"type": "module",
|
||||
"description": "Mem0 persistent memory plugin for OpenCode — add, search, and manage memories across sessions",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"name": "mem0",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.4",
|
||||
"description": "Persistent semantic memory for Antigravity agents. Cross-session, user-level recall via the Mem0 Platform MCP server. 16 slash commands, lifecycle hooks for auto-capture and metadata enforcement.",
|
||||
"author": { "name": "Mem0", "email": "support@mem0.ai" },
|
||||
"publisher": "mem0ai",
|
||||
|
||||
@@ -104,11 +104,8 @@ def store_summary(api_key: str, summary: str, user_id: str, session_id: str, pro
|
||||
}
|
||||
if branch:
|
||||
metadata["branch"] = branch
|
||||
# The compact summary is model-authored prose, in the first person and with no
|
||||
# framing to mark it as such. Under role="user" mem0 reads "I recommend X" as
|
||||
# the human saying it and stores "User recommends X".
|
||||
body = {
|
||||
"messages": [{"role": "assistant", "content": summary}],
|
||||
"messages": [{"role": "user", "content": summary}],
|
||||
"user_id": user_id,
|
||||
"app_id": project_id,
|
||||
"metadata": metadata,
|
||||
|
||||
@@ -175,12 +175,8 @@ def store_summary(
|
||||
if files:
|
||||
metadata["files_touched"] = files[:20]
|
||||
|
||||
# summary_prompt wraps the assistant's own last message. Mem0 extracts "facts
|
||||
# about the user" from each message and role is the only signal telling it who
|
||||
# spoke, so role="user" here turns Claude's opinions into the human's stated
|
||||
# preferences ("User prefers dropping Redis...").
|
||||
body = {
|
||||
"messages": [{"role": "assistant", "content": summary_prompt}],
|
||||
"messages": [{"role": "user", "content": summary_prompt}],
|
||||
"user_id": user_id,
|
||||
"app_id": project_id,
|
||||
"run_id": session_id,
|
||||
|
||||
@@ -8,6 +8,7 @@ Additional platform capabilities beyond core CRUD operations.
|
||||
- [Entity Linking](#entity-linking)
|
||||
- [Custom Categories](#custom-categories)
|
||||
- [Custom Instructions](#custom-instructions)
|
||||
- [Criteria Retrieval](#criteria-retrieval)
|
||||
- [Feedback Mechanism](#feedback-mechanism)
|
||||
- [Memory Export](#memory-export)
|
||||
- [Group Chat](#group-chat)
|
||||
@@ -101,29 +102,9 @@ await client.updateProject({ customCategories: newCategories });
|
||||
categories = client.project.get(fields=["custom_categories"])
|
||||
```
|
||||
|
||||
**Override categories for a single add call:**
|
||||
```python
|
||||
client.add(messages, user_id="alice", custom_categories=per_call_categories)
|
||||
```
|
||||
### Key Constraint
|
||||
|
||||
```javascript
|
||||
await client.add(messages, { userId: "alice", customCategories: perCallCategories });
|
||||
```
|
||||
|
||||
### Resolution Order
|
||||
|
||||
1. `custom_categories` passed on the `add` call
|
||||
2. `custom_categories` set on the project
|
||||
3. Built-in default catalog
|
||||
|
||||
### Key Constraints
|
||||
|
||||
- A per-call list **fully replaces** the project list for that call. The lists are not merged.
|
||||
- Categories are applied at ingestion time. Changing the list later does not re-tag existing memories.
|
||||
|
||||
### Main Use Case
|
||||
|
||||
Per-call lists give different users or entities their own vocabulary inside a single project, without splitting them across projects.
|
||||
Per-request overrides (`custom_categories=...` on `client.add`) are **not supported** on the managed API. Only project-level configuration works. Workaround: store ad-hoc labels in `metadata` field.
|
||||
|
||||
---
|
||||
|
||||
@@ -164,6 +145,44 @@ await client.updateProject({ customInstructions: "Your guidelines here..." });
|
||||
|
||||
---
|
||||
|
||||
## Criteria Retrieval
|
||||
|
||||
Custom attribute-based memory ranking using LLM-evaluated criteria with weights. Goes beyond semantic similarity to prioritize memories based on domain-specific signals.
|
||||
|
||||
### Configuration
|
||||
|
||||
```python
|
||||
# Define criteria at project level
|
||||
retrieval_criteria = [
|
||||
{"name": "joy", "description": "Positive emotions like happiness and excitement", "weight": 3},
|
||||
{"name": "curiosity", "description": "Inquisitiveness and desire to learn", "weight": 2},
|
||||
{"name": "urgency", "description": "Time-sensitive or high-priority items", "weight": 4},
|
||||
]
|
||||
client.project.update(retrieval_criteria=retrieval_criteria)
|
||||
```
|
||||
|
||||
```typescript
|
||||
await client.updateProject({
|
||||
retrievalCriteria: [
|
||||
{ name: 'joy', description: 'Positive emotions', weight: 3 },
|
||||
{ name: 'urgency', description: 'Time-sensitive items', weight: 4 },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
Once configured, `client.search()` automatically applies criteria ranking:
|
||||
|
||||
```python
|
||||
# Criteria-weighted results returned automatically
|
||||
results = client.search("Why am I feeling happy?", filters={"user_id": "alice"})
|
||||
```
|
||||
|
||||
**Best for:** Wellness assistants, tutoring platforms, productivity tools — any app needing intent-aware retrieval.
|
||||
|
||||
---
|
||||
|
||||
## Feedback Mechanism
|
||||
|
||||
Provide feedback on extracted memories to improve system quality over time.
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
"""Regression tests: assistant-authored text must never be posted as role="user".
|
||||
|
||||
The Stop hook (capture_session_summary) and the post-compact hook
|
||||
(capture_compact_summary) both ship *model-authored* prose to
|
||||
POST /v3/memories/add/. Mem0's fact extractor renders each message as
|
||||
"{role}: {content}" and is instructed to extract "facts and preferences about
|
||||
the user" — so role is the only signal separating what the human said from what
|
||||
Claude said.
|
||||
|
||||
Posting Claude's own words under role="user" made the extractor read Claude's
|
||||
first-person prose ("I recommend pgvector", "I found the bug in auth.py") as the
|
||||
*human's* statements and store them under their user_id. The Stop hook fires on
|
||||
every assistant turn, so this corrupted memory on nearly every message.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class _FakeResp:
|
||||
status = 200
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_):
|
||||
return False
|
||||
|
||||
|
||||
def _capture(monkeypatch, module):
|
||||
"""Patch urlopen so store_summary posts nowhere; capture the request body."""
|
||||
captured: dict = {}
|
||||
|
||||
def fake_urlopen(req, timeout=0):
|
||||
captured["body"] = json.loads(req.data.decode("utf-8"))
|
||||
return _FakeResp()
|
||||
|
||||
monkeypatch.setattr(module.urllib.request, "urlopen", fake_urlopen)
|
||||
return captured
|
||||
|
||||
|
||||
# Claude's own voice — first-person prose that must never be attributed to the human.
|
||||
ASSISTANT_PROSE = (
|
||||
"I traced the root cause to auth.py and I recommend we switch to pgvector "
|
||||
"for the vector store. I'll refactor the session handler next."
|
||||
)
|
||||
|
||||
|
||||
def test_session_summary_posts_assistant_prose_as_assistant(monkeypatch):
|
||||
"""Stop hook: the last assistant message must be tagged role="assistant"."""
|
||||
import capture_session_summary as css
|
||||
|
||||
captured = _capture(monkeypatch, css)
|
||||
|
||||
css.store_summary(
|
||||
api_key="test-key",
|
||||
summary_prompt=css.build_summary_prompt(ASSISTANT_PROSE, []),
|
||||
user_id="u1",
|
||||
session_id="s1",
|
||||
project_id="p1",
|
||||
branch="main",
|
||||
files=[],
|
||||
)
|
||||
|
||||
messages = captured["body"]["messages"]
|
||||
for msg in messages:
|
||||
if ASSISTANT_PROSE in msg["content"]:
|
||||
assert msg["role"] == "assistant", (
|
||||
"Claude's own words were posted as role='user' — mem0 will extract "
|
||||
"them as facts about the human. Got role=%r" % msg["role"]
|
||||
)
|
||||
break
|
||||
else:
|
||||
raise AssertionError("assistant prose never made it into the payload")
|
||||
|
||||
|
||||
def test_compact_summary_posts_assistant_prose_as_assistant(monkeypatch):
|
||||
"""Post-compact hook: the compact summary is model-authored, not user-authored."""
|
||||
import capture_compact_summary as ccs
|
||||
|
||||
captured = _capture(monkeypatch, ccs)
|
||||
|
||||
ccs.store_summary(
|
||||
api_key="test-key",
|
||||
summary=ASSISTANT_PROSE,
|
||||
user_id="u1",
|
||||
session_id="s1",
|
||||
project_id="p1",
|
||||
branch="main",
|
||||
)
|
||||
|
||||
messages = captured["body"]["messages"]
|
||||
for msg in messages:
|
||||
if ASSISTANT_PROSE in msg["content"]:
|
||||
assert msg["role"] == "assistant", (
|
||||
"Compact summary (written by Claude) was posted as role='user'. Got role=%r" % msg["role"]
|
||||
)
|
||||
break
|
||||
else:
|
||||
raise AssertionError("assistant prose never made it into the payload")
|
||||
|
||||
|
||||
def test_no_user_role_message_carries_assistant_prose(monkeypatch):
|
||||
"""Belt and braces: no user-role message may contain the assistant's words."""
|
||||
import capture_session_summary as css
|
||||
|
||||
captured = _capture(monkeypatch, css)
|
||||
|
||||
css.store_summary(
|
||||
api_key="test-key",
|
||||
summary_prompt=css.build_summary_prompt(ASSISTANT_PROSE, ["auth.py"]),
|
||||
user_id="u1",
|
||||
session_id="s1",
|
||||
project_id="p1",
|
||||
branch="main",
|
||||
files=["auth.py"],
|
||||
)
|
||||
|
||||
for msg in captured["body"]["messages"]:
|
||||
if msg["role"] == "user":
|
||||
assert ASSISTANT_PROSE not in msg["content"], (
|
||||
"A user-role message carries Claude's prose — this is the misattribution bug."
|
||||
)
|
||||
|
||||
|
||||
def test_auto_capture_preserves_real_roles():
|
||||
"""auto_capture is the reference: it must pass roles through untouched."""
|
||||
import auto_capture
|
||||
|
||||
lines = [
|
||||
json.dumps({"type": "user", "message": {"role": "user", "content": "why is the build failing on main?"}}),
|
||||
json.dumps(
|
||||
{
|
||||
"type": "assistant",
|
||||
"message": {"role": "assistant", "content": [{"type": "text", "text": ASSISTANT_PROSE}]},
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
messages = auto_capture.extract_recent_exchanges(lines)
|
||||
|
||||
assert [m["role"] for m in messages] == ["user", "assistant"]
|
||||
assert ASSISTANT_PROSE in messages[1]["content"]
|
||||
+4
-110
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0ai",
|
||||
"version": "3.1.1",
|
||||
"version": "3.0.13",
|
||||
"description": "The Memory Layer For Your AI Apps",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
@@ -98,8 +98,7 @@
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "^8.3.0",
|
||||
"typescript": "5.5.4",
|
||||
"iovalkey": "^0.3.3",
|
||||
"@mochow/mochow-sdk-node": "^2.1.5"
|
||||
"iovalkey": "^0.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.16.0",
|
||||
@@ -109,16 +108,12 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@anthropic-ai/sdk": "^0.40.1",
|
||||
"@aws-sdk/client-neptune-graph": ">=3.0.0 <3.968.0",
|
||||
"@aws-sdk/client-s3vectors": "3.967.0",
|
||||
"@mochow/mochow-sdk-node": "^2.1.5",
|
||||
"@azure/identity": "^4.0.0",
|
||||
"@azure/search-documents": "^12.0.0",
|
||||
"@cloudflare/workers-types": "^4.20250504.0",
|
||||
"@databricks/sql": "^1.16.0",
|
||||
"@google-cloud/aiplatform": "^6.8.0",
|
||||
"@google/genai": "^1.40.0",
|
||||
"@huggingface/transformers": "^3.0.0 || ^4.0.0",
|
||||
"@langchain/core": "^1.1.47",
|
||||
"@mistralai/mistralai": "^1.5.2",
|
||||
"@opensearch-project/opensearch": "^3.5.1",
|
||||
@@ -133,7 +128,6 @@
|
||||
"cassandra-driver": "4.8.0",
|
||||
"chromadb": "^3.5.0",
|
||||
"cloudflare": "^4.2.0",
|
||||
"cohere-ai": "^7.17.0 || ^8.0.0",
|
||||
"fastembed": "^2.1.0",
|
||||
"groq-sdk": "0.3.0",
|
||||
"mongodb": "^7.0.0",
|
||||
@@ -145,113 +139,15 @@
|
||||
"iovalkey": "^0.3.3",
|
||||
"compromise": "^14.0.0",
|
||||
"natural": "^8.0.1",
|
||||
"zeroentropy": "^0.1.0-alpha.10",
|
||||
"mysql2": "^3.0.0",
|
||||
"@zilliz/milvus2-sdk-node": "^2.4.0 || ^3.0.0",
|
||||
"@aws-sdk/client-bedrock-runtime": ">=3.0.0 <3.968.0"
|
||||
"@zilliz/milvus2-sdk-node": "^2.4.0 || ^3.0.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
|
||||
},
|
||||
"@zilliz/milvus2-sdk-node": {
|
||||
"optional": true
|
||||
},
|
||||
"@mochow/mochow-sdk-node": {
|
||||
"optional": true
|
||||
},
|
||||
"@aws-sdk/client-bedrock-runtime": {
|
||||
"optional": true
|
||||
},
|
||||
"@databricks/sql": {
|
||||
"optional": true
|
||||
},
|
||||
"@aws-sdk/client-neptune-graph": {
|
||||
"optional": true
|
||||
},
|
||||
"chromadb": {
|
||||
"optional": true
|
||||
},
|
||||
"mongodb": {
|
||||
"optional": true
|
||||
},
|
||||
"weaviate-client": {
|
||||
"optional": true
|
||||
},
|
||||
"cassandra-driver": {
|
||||
"optional": true
|
||||
},
|
||||
"@pinecone-database/pinecone": {
|
||||
"optional": true
|
||||
},
|
||||
"@aws-sdk/client-s3vectors": {
|
||||
"optional": true
|
||||
},
|
||||
"@turbopuffer/turbopuffer": {
|
||||
"optional": true
|
||||
},
|
||||
"@upstash/vector": {
|
||||
"optional": true
|
||||
},
|
||||
"@elastic/elasticsearch": {
|
||||
"optional": true
|
||||
},
|
||||
"@opensearch-project/opensearch": {
|
||||
"optional": true
|
||||
},
|
||||
"cohere-ai": {
|
||||
"optional": true
|
||||
},
|
||||
"zeroentropy": {
|
||||
"optional": true
|
||||
},
|
||||
"fastembed": {
|
||||
"optional": true
|
||||
},
|
||||
"@google-cloud/aiplatform": {
|
||||
"optional": true
|
||||
},
|
||||
"@huggingface/transformers": {
|
||||
"optional": true
|
||||
},
|
||||
"iovalkey": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
@@ -288,9 +184,7 @@
|
||||
"tar@<=7.5.15": "^7.5.19",
|
||||
"@modelcontextprotocol/sdk": "^1.25.4",
|
||||
"esbuild": ">=0.28.1",
|
||||
"undici@<6.27.0": ">=6.27.0 <8.0.0",
|
||||
"@aws-sdk/client-bedrock-runtime": "3.967.0",
|
||||
"@aws-sdk/client-neptune-graph": "3.966.0"
|
||||
"undici@<6.27.0": ">=6.27.0 <8.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+216
-1821
File diff suppressed because it is too large
Load Diff
@@ -59,6 +59,7 @@ export interface ProjectOptions {
|
||||
export interface PromptUpdatePayload {
|
||||
customInstructions?: string;
|
||||
customCategories?: custom_categories[];
|
||||
retrievalCriteria?: any[];
|
||||
version?: string;
|
||||
memoryDepth?: string | null;
|
||||
usecaseSetting?: string | number;
|
||||
|
||||
@@ -110,42 +110,14 @@ You only need to provide API keys - all other settings are optional.
|
||||
### Methods
|
||||
|
||||
- `add(messages: string | Message[], userId?: string, ...): Promise<SearchResult>`
|
||||
- Options include `metadata`, `infer`, and `expirationDate` (a `YYYY-MM-DD` date after which the memory is treated as expired).
|
||||
- `search(query: string, userId?: string, ...): Promise<SearchResult>`
|
||||
- Expired memories are omitted unless you pass `showExpired: true`.
|
||||
- `get(memoryId: string): Promise<MemoryItem | null>`
|
||||
- Fetching by ID returns the memory even if it has expired.
|
||||
- `getAll(options): Promise<SearchResult>`
|
||||
- Expired memories are omitted unless you pass `showExpired: true`.
|
||||
- `update(memoryId: string, config: string | UpdateMemoryOptions): Promise<{ message: string }>`
|
||||
- `UpdateMemoryOptions` is `{ text?, data?, metadata?, expirationDate? }`. At least one must be
|
||||
provided; omitted fields are left untouched, and `expirationDate: null` clears an existing
|
||||
expiry. `data` is a deprecated alias for `text`.
|
||||
- Passing a bare string is shorthand for `{ text }`, so `update(memoryId, "new text")` still works.
|
||||
- `update(memoryId: string, data: string): Promise<{ message: string }>`
|
||||
- `delete(memoryId: string): Promise<{ message: string }>`
|
||||
- `deleteAll(userId?: string, ...): Promise<{ message: string }>`
|
||||
- `history(memoryId: string): Promise<any[]>`
|
||||
- `reset(): Promise<void>`
|
||||
|
||||
```typescript
|
||||
// Replace the content
|
||||
await memory.update(memoryId, { text: "Alex now prefers decaf coffee" });
|
||||
|
||||
// Update metadata only, leaving the stored text untouched
|
||||
await memory.update(memoryId, { metadata: { category: "preferences" } });
|
||||
|
||||
// Expire the memory on a given day, or clear an existing expiry
|
||||
await memory.update(memoryId, { expirationDate: "2030-01-31" });
|
||||
await memory.update(memoryId, { expirationDate: null });
|
||||
|
||||
// Include expired memories in reads
|
||||
await memory.getAll({ filters: { user_id: "alice" }, showExpired: true });
|
||||
await memory.search("coffee", {
|
||||
filters: { user_id: "alice" },
|
||||
showExpired: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Try the Example
|
||||
|
||||
We provide a comprehensive example in `examples/basic.ts` that demonstrates all the features including:
|
||||
|
||||
@@ -73,9 +73,10 @@ async function runTests(memory: Memory) {
|
||||
}
|
||||
|
||||
// Updating this memory
|
||||
const result4 = await memory.update(result1.results[0].id, {
|
||||
text: "I love India, it is my favorite country.",
|
||||
});
|
||||
const result4 = await memory.update(
|
||||
result1.results[0].id,
|
||||
"I love India, it is my favorite country.",
|
||||
);
|
||||
console.log("Updated memory:", result4);
|
||||
|
||||
// Get all memories
|
||||
|
||||
@@ -55,9 +55,10 @@ export async function runTests(memory: Memory) {
|
||||
}
|
||||
|
||||
// Updating this memory
|
||||
const result4 = await memory.update(result1.results[0].id, {
|
||||
text: "I love India, it is my favorite country.",
|
||||
});
|
||||
const result4 = await memory.update(
|
||||
result1.results[0].id,
|
||||
"I love India, it is my favorite country.",
|
||||
);
|
||||
console.log("Updated memory:", result4);
|
||||
|
||||
// Get all memories
|
||||
|
||||
@@ -40,10 +40,6 @@ export class ConfigManager {
|
||||
| undefined);
|
||||
|
||||
return {
|
||||
// Spread first so provider-specific keys (e.g. the Vertex AI
|
||||
// project/location/credentials) survive the merge, while the
|
||||
// normalized values below still win.
|
||||
...userConf,
|
||||
apiKey:
|
||||
userConf?.apiKey !== undefined
|
||||
? userConf.apiKey
|
||||
@@ -60,14 +56,9 @@ export class ConfigManager {
|
||||
})(),
|
||||
},
|
||||
vectorStore: {
|
||||
// Every factory already matches the provider case-insensitively, so a capitalized
|
||||
// name constructs the right store -- but the `provider === "memory"` comparisons that
|
||||
// pick per-provider entity-store settings do not. Normalize once, here, so those
|
||||
// comparisons cannot silently miss.
|
||||
provider: (
|
||||
provider:
|
||||
userConfig.vectorStore?.provider ||
|
||||
DEFAULT_MEMORY_CONFIG.vectorStore.provider
|
||||
).toLowerCase(),
|
||||
DEFAULT_MEMORY_CONFIG.vectorStore.provider,
|
||||
config: (() => {
|
||||
const defaultConf = DEFAULT_MEMORY_CONFIG.vectorStore.config;
|
||||
const userConf = userConfig.vectorStore?.config;
|
||||
@@ -142,11 +133,6 @@ export class ConfigManager {
|
||||
userConf?.maxTokens ?? (llmRaw?.max_tokens as number | undefined);
|
||||
|
||||
return {
|
||||
// Spread user-provided config first so any additional fields
|
||||
// (e.g. future aws_bedrock options) pass through without a
|
||||
// manager.ts edit, matching the vectorStore.config pattern above
|
||||
// and making the schema's .passthrough() on llm.config meaningful.
|
||||
...userConf,
|
||||
baseURL: llmBaseURL,
|
||||
url: userConf?.url,
|
||||
apiKey:
|
||||
@@ -161,20 +147,6 @@ export class ConfigManager {
|
||||
temperature,
|
||||
topP,
|
||||
maxTokens,
|
||||
// Pass through AWS Bedrock fields so the aws_bedrock provider works
|
||||
// through the standard Memory config path (snake_case tolerated).
|
||||
awsRegion:
|
||||
userConf?.awsRegion ?? (llmRaw?.aws_region as string | undefined),
|
||||
awsAccessKeyId:
|
||||
userConf?.awsAccessKeyId ??
|
||||
(llmRaw?.aws_access_key_id as string | undefined),
|
||||
awsSecretAccessKey:
|
||||
userConf?.awsSecretAccessKey ??
|
||||
(llmRaw?.aws_secret_access_key as string | undefined),
|
||||
awsSessionToken:
|
||||
userConf?.awsSessionToken ??
|
||||
(llmRaw?.aws_session_token as string | undefined),
|
||||
client: userConf?.client,
|
||||
};
|
||||
})(),
|
||||
},
|
||||
@@ -205,7 +177,6 @@ export class ConfigManager {
|
||||
})(),
|
||||
disableHistory:
|
||||
userConfig.disableHistory || DEFAULT_MEMORY_CONFIG.disableHistory,
|
||||
reranker: userConfig.reranker,
|
||||
};
|
||||
|
||||
// Validate the merged config
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
import { Embedder } from "./base";
|
||||
import { EmbeddingConfig } from "../types";
|
||||
|
||||
const DEFAULT_MODEL = "amazon.titan-embed-text-v1";
|
||||
const DEFAULT_REGION = "us-west-2";
|
||||
|
||||
// Cohere's Bedrock embed API rejects an InvokeModel call carrying more than 96
|
||||
// texts, so `embedBatch` chunks at that boundary.
|
||||
const COHERE_MAX_BATCH = 96;
|
||||
|
||||
// Titan has no server-side batch endpoint -- one InvokeModel call per text --
|
||||
// so without a cap a large embedBatch() would fan out one request per text.
|
||||
// Bounds concurrency the same way COHERE_MAX_BATCH bounds the Cohere path.
|
||||
const TITAN_MAX_CONCURRENCY = 4;
|
||||
|
||||
// Cohere wants to know whether a text is being embedded for storage or for a
|
||||
// retrieval query; embedding a search query in document mode silently
|
||||
// degrades retrieval. Titan ignores this and has no equivalent parameter.
|
||||
const COHERE_INPUT_TYPES: Record<"add" | "update" | "search", string> = {
|
||||
add: "search_document",
|
||||
update: "search_document",
|
||||
search: "search_query",
|
||||
};
|
||||
|
||||
type BedrockRuntimeModule = typeof import("@aws-sdk/client-bedrock-runtime");
|
||||
|
||||
interface BedrockCredentials {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
sessionToken?: string;
|
||||
}
|
||||
|
||||
interface BedrockEmbeddingResponse {
|
||||
// Titan returns a single vector. Cohere v3 returns a flat array of vectors;
|
||||
// Cohere v4, when `embedding_types` is requested, nests it as `{ float }`.
|
||||
embedding?: number[];
|
||||
embeddings?: number[][] | { float?: number[][] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs `fn` over `items` with at most `limit` calls in flight at once,
|
||||
* returning results in input order regardless of completion order.
|
||||
*/
|
||||
async function mapWithConcurrencyLimit<T, R>(
|
||||
items: T[],
|
||||
limit: number,
|
||||
fn: (item: T) => Promise<R>,
|
||||
): Promise<R[]> {
|
||||
const results: R[] = new Array(items.length);
|
||||
let next = 0;
|
||||
|
||||
async function worker(): Promise<void> {
|
||||
while (next < items.length) {
|
||||
const index = next++;
|
||||
results[index] = await fn(items[index]);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.min(limit, items.length) }, worker),
|
||||
);
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* AWS Bedrock embedder, mirroring `mem0/embeddings/aws_bedrock.py`.
|
||||
*
|
||||
* Supports the Amazon Titan and Cohere embedding model families. The
|
||||
* `@aws-sdk/client-bedrock-runtime` dependency is lazily imported so the
|
||||
* package stays optional: importing this module never forces the SDK to be
|
||||
* installed until a Bedrock embedder actually embeds something.
|
||||
*/
|
||||
export class AWSBedrockEmbedder implements Embedder {
|
||||
private readonly model: string;
|
||||
private readonly region: string;
|
||||
private readonly embeddingDims?: number;
|
||||
private readonly credentials?: BedrockCredentials;
|
||||
private clientPromise?: Promise<{
|
||||
sdk: BedrockRuntimeModule;
|
||||
client: { send: (command: any) => Promise<{ body?: Uint8Array }> };
|
||||
}>;
|
||||
|
||||
constructor(config: EmbeddingConfig) {
|
||||
this.model = config.model || DEFAULT_MODEL;
|
||||
this.region = config.awsRegion || process.env.AWS_REGION || DEFAULT_REGION;
|
||||
this.embeddingDims = config.embeddingDims;
|
||||
|
||||
const hasKeyPair = Boolean(
|
||||
config.awsAccessKeyId && config.awsSecretAccessKey,
|
||||
);
|
||||
const hasAnyCredential = Boolean(
|
||||
config.awsAccessKeyId ||
|
||||
config.awsSecretAccessKey ||
|
||||
config.awsSessionToken,
|
||||
);
|
||||
|
||||
// Partially configured credentials would silently fall back to the default
|
||||
// chain, embedding under an identity the caller never chose.
|
||||
if (hasAnyCredential && !hasKeyPair) {
|
||||
throw new Error(
|
||||
"AWS Bedrock requires both awsAccessKeyId and awsSecretAccessKey when any explicit credential is configured. " +
|
||||
"Omit all credential fields to use the AWS default credential chain.",
|
||||
);
|
||||
}
|
||||
|
||||
// Leaving `credentials` unset lets the AWS SDK resolve them from its
|
||||
// default chain: environment, shared config, SSO, or the instance role.
|
||||
if (hasKeyPair) {
|
||||
this.credentials = {
|
||||
accessKeyId: config.awsAccessKeyId!,
|
||||
secretAccessKey: config.awsSecretAccessKey!,
|
||||
...(config.awsSessionToken && { sessionToken: config.awsSessionToken }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async loadSdk(): Promise<BedrockRuntimeModule> {
|
||||
try {
|
||||
return await import("@aws-sdk/client-bedrock-runtime");
|
||||
} catch (error) {
|
||||
// Only a genuine module-resolution failure gets the friendly install
|
||||
// hint. Node's native ESM loader raises ERR_MODULE_NOT_FOUND; Jest's
|
||||
// and bundlers' CJS-style resolvers raise MODULE_NOT_FOUND. Anything
|
||||
// else (e.g. the package is installed but throws while loading, such
|
||||
// as on a Node version older than the SDK's own engines requirement)
|
||||
// rethrows unchanged instead of being misreported as "not installed".
|
||||
const code = (error as { code?: string } | undefined)?.code;
|
||||
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
|
||||
throw Object.assign(
|
||||
new Error(
|
||||
"The '@aws-sdk/client-bedrock-runtime' package is required to use the AWS Bedrock embedder. " +
|
||||
"Install it with: npm install @aws-sdk/client-bedrock-runtime",
|
||||
),
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async createClient(): Promise<{
|
||||
sdk: BedrockRuntimeModule;
|
||||
client: { send: (command: any) => Promise<{ body?: Uint8Array }> };
|
||||
}> {
|
||||
const sdk = await this.loadSdk();
|
||||
return {
|
||||
sdk,
|
||||
client: new sdk.BedrockRuntimeClient({
|
||||
region: this.region,
|
||||
...(this.credentials && { credentials: this.credentials }),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private getClient() {
|
||||
// Memoized so concurrent embed() calls share one client instead of each
|
||||
// racing to build their own. Cleared on rejection so a transient failure
|
||||
// (e.g. a network blip while resolving credentials) doesn't permanently
|
||||
// disable Bedrock for the rest of this embedder's lifetime.
|
||||
if (!this.clientPromise) {
|
||||
this.clientPromise = this.createClient().catch((err) => {
|
||||
this.clientPromise = undefined;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return this.clientPromise;
|
||||
}
|
||||
|
||||
private isCohereModel(): boolean {
|
||||
return this.model.startsWith("cohere.");
|
||||
}
|
||||
|
||||
private isCohereV4Model(): boolean {
|
||||
return this.model.includes("embed-v4");
|
||||
}
|
||||
|
||||
private buildRequestBody(
|
||||
texts: string[],
|
||||
memoryAction?: "add" | "update" | "search",
|
||||
): Record<string, unknown> {
|
||||
if (this.isCohereModel()) {
|
||||
const body: Record<string, unknown> = {
|
||||
texts,
|
||||
input_type: memoryAction
|
||||
? COHERE_INPUT_TYPES[memoryAction]
|
||||
: "search_document",
|
||||
};
|
||||
|
||||
// Only Embed v4 understands embedding_types / output_dimension; v3
|
||||
// rejects unknown fields, so they're guarded to the v4 model family.
|
||||
if (this.isCohereV4Model()) {
|
||||
body.embedding_types = ["float"];
|
||||
if (this.embeddingDims !== undefined) {
|
||||
body.output_dimension = this.embeddingDims;
|
||||
}
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
// Titan accepts one text per call. Only Titan Text Embeddings V2 supports
|
||||
// a caller-chosen output size (256/512/1024), so the field is guarded the
|
||||
// same way the Python provider guards it.
|
||||
return {
|
||||
inputText: texts[0],
|
||||
...(this.embeddingDims !== undefined &&
|
||||
this.model.includes("titan-embed-text-v2") && {
|
||||
dimensions: this.embeddingDims,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private async invoke(
|
||||
texts: string[],
|
||||
memoryAction?: "add" | "update" | "search",
|
||||
): Promise<number[][]> {
|
||||
const { sdk, client } = await this.getClient();
|
||||
|
||||
let payload: BedrockEmbeddingResponse;
|
||||
try {
|
||||
const response = await client.send(
|
||||
new sdk.InvokeModelCommand({
|
||||
modelId: this.model,
|
||||
contentType: "application/json",
|
||||
accept: "application/json",
|
||||
body: new TextEncoder().encode(
|
||||
JSON.stringify(this.buildRequestBody(texts, memoryAction)),
|
||||
),
|
||||
}),
|
||||
);
|
||||
payload = JSON.parse(new TextDecoder().decode(response.body));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(
|
||||
`Error getting embedding from AWS Bedrock model ${this.model}: ${message}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Validated outside the try so this message is not re-wrapped by the catch.
|
||||
// Cohere v3 replies with a flat `embeddings` array; v4 (when
|
||||
// embedding_types is requested) nests it under `.float`.
|
||||
const embeddings = this.isCohereModel()
|
||||
? Array.isArray(payload.embeddings)
|
||||
? payload.embeddings
|
||||
: payload.embeddings?.float
|
||||
: payload.embedding && [payload.embedding];
|
||||
|
||||
// `[]` is truthy, so a lone zero-length vector must be checked for
|
||||
// explicitly -- otherwise it passes the length check and hands the
|
||||
// caller an empty embedding instead of an error.
|
||||
if (
|
||||
!embeddings ||
|
||||
embeddings.length !== texts.length ||
|
||||
embeddings.some((embedding) => embedding.length === 0)
|
||||
) {
|
||||
throw new Error(
|
||||
`AWS Bedrock model ${this.model} returned no embedding for one or more inputs`,
|
||||
);
|
||||
}
|
||||
return embeddings;
|
||||
}
|
||||
|
||||
async embed(
|
||||
text: string,
|
||||
memoryAction?: "add" | "update" | "search",
|
||||
): Promise<number[]> {
|
||||
return (await this.invoke([text], memoryAction))[0];
|
||||
}
|
||||
|
||||
async embedBatch(
|
||||
texts: string[],
|
||||
memoryAction?: "add" | "update" | "search",
|
||||
): Promise<number[][]> {
|
||||
if (texts.length === 0) return [];
|
||||
|
||||
if (!this.isCohereModel()) {
|
||||
return mapWithConcurrencyLimit(texts, TITAN_MAX_CONCURRENCY, (text) =>
|
||||
this.embed(text, memoryAction),
|
||||
);
|
||||
}
|
||||
|
||||
const embeddings: number[][] = [];
|
||||
for (let i = 0; i < texts.length; i += COHERE_MAX_BATCH) {
|
||||
embeddings.push(
|
||||
...(await this.invoke(
|
||||
texts.slice(i, i + COHERE_MAX_BATCH),
|
||||
memoryAction,
|
||||
)),
|
||||
);
|
||||
}
|
||||
return embeddings;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,4 @@
|
||||
export interface Embedder {
|
||||
embed(
|
||||
text: string,
|
||||
memoryAction?: "add" | "update" | "search",
|
||||
): Promise<number[]>;
|
||||
embedBatch(
|
||||
texts: string[],
|
||||
memoryAction?: "add" | "update" | "search",
|
||||
): Promise<number[][]>;
|
||||
embed(text: string): Promise<number[]>;
|
||||
embedBatch(texts: string[]): Promise<number[][]>;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
import type { FlagEmbedding } from "fastembed";
|
||||
import { EmbeddingModel, 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
|
||||
// be rejected synchronously in the constructor — with a clear message instead
|
||||
// of a `FlagEmbedding.init()` download error — without eagerly importing the
|
||||
// optional 'fastembed' package just to read its enum. Keep in sync if
|
||||
// fastembed adds a model.
|
||||
const SUPPORTED_MODELS = [
|
||||
"fast-all-MiniLM-L6-v2",
|
||||
"fast-bge-base-en",
|
||||
"fast-bge-base-en-v1.5",
|
||||
"fast-bge-small-en",
|
||||
"fast-bge-small-en-v1.5",
|
||||
"fast-bge-small-zh-v1.5",
|
||||
"fast-multilingual-e5-large",
|
||||
] as const;
|
||||
type FastEmbedModel = (typeof SUPPORTED_MODELS)[number];
|
||||
const DEFAULT_MODEL: FastEmbedModel = "fast-bge-small-en-v1.5";
|
||||
const DEFAULT_MODEL = EmbeddingModel.BGESmallENV15;
|
||||
type FastEmbedModel = Exclude<EmbeddingModel, EmbeddingModel.CUSTOM>;
|
||||
|
||||
// FastEmbed only ships a fixed set of ONNX models. Keep the list handy so we can
|
||||
// reject unknown model names up front with a clear message instead of letting
|
||||
// FlagEmbedding.init fail later with an opaque download error.
|
||||
const SUPPORTED_MODELS = Object.values(EmbeddingModel).filter(
|
||||
(model) => model !== EmbeddingModel.CUSTOM,
|
||||
) as FastEmbedModel[];
|
||||
|
||||
export class FastEmbedEmbedder implements Embedder {
|
||||
private readonly modelName: FastEmbedModel;
|
||||
private modelName: FastEmbedModel;
|
||||
private embeddingModel?: Promise<FlagEmbedding>;
|
||||
|
||||
constructor(config: EmbeddingConfig) {
|
||||
@@ -41,7 +32,9 @@ export class FastEmbedEmbedder implements Embedder {
|
||||
|
||||
private getEmbeddingModel(): Promise<FlagEmbedding> {
|
||||
if (!this.embeddingModel) {
|
||||
this.embeddingModel = this.initEmbeddingModel().catch((error) => {
|
||||
this.embeddingModel = FlagEmbedding.init({
|
||||
model: this.modelName,
|
||||
}).catch((error) => {
|
||||
this.embeddingModel = undefined;
|
||||
throw error;
|
||||
});
|
||||
@@ -50,20 +43,6 @@ export class FastEmbedEmbedder implements Embedder {
|
||||
return this.embeddingModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily import the optional `fastembed` peer and initialize the model, so
|
||||
* consumers that never touch FastEmbed don't need it installed.
|
||||
*/
|
||||
private async initEmbeddingModel(): Promise<FlagEmbedding> {
|
||||
const sdk = await loadPeer(
|
||||
"fastembed",
|
||||
"FastEmbed embedder",
|
||||
() => import("fastembed"),
|
||||
);
|
||||
|
||||
return sdk.FlagEmbedding.init({ model: this.modelName });
|
||||
}
|
||||
|
||||
private normalizeInput(text: string): string {
|
||||
return text.replace(/\n/g, " ");
|
||||
}
|
||||
|
||||
@@ -1,32 +1,21 @@
|
||||
import type { GoogleGenAI } from "@google/genai";
|
||||
import { 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.apiKey = config.apiKey || process.env.GOOGLE_API_KEY;
|
||||
this.google = new GoogleGenAI({
|
||||
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,
|
||||
@@ -38,7 +27,6 @@ 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 type { Embeddings } from "@langchain/core/embeddings";
|
||||
import { Embeddings } from "@langchain/core/embeddings";
|
||||
import { Embedder } from "./base";
|
||||
import { EmbeddingConfig } from "../types";
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { Ollama } from "ollama";
|
||||
import { 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.host = config.url || config.baseURL || "http://localhost:11434";
|
||||
this.ollama = new Ollama({
|
||||
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,18 +21,7 @@ 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) {
|
||||
@@ -65,7 +54,6 @@ 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,251 +0,0 @@
|
||||
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<
|
||||
ConstructorParameters<AIPlatform["PredictionServiceClient"]>[0]
|
||||
>;
|
||||
|
||||
interface EmbeddingResponse {
|
||||
embeddings: {
|
||||
values: number[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Vertex AI caps how many input texts one `predict()` call may carry, and the
|
||||
* cap depends on the model family. `gemini-embedding-*` accepts exactly one
|
||||
* text per request; the older `text-embedding-*` / `text-multilingual-*`
|
||||
* models accept up to 250.
|
||||
* https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-text-embeddings
|
||||
*/
|
||||
function maxInstancesPerRequest(model: string): number {
|
||||
return model.startsWith("gemini-embedding") ? 1 : 250;
|
||||
}
|
||||
|
||||
function isValidEmbedding(value: unknown): value is EmbeddingResponse {
|
||||
if (typeof value !== "object" || value === null) return false;
|
||||
const obj = value as Record<string, unknown>;
|
||||
if (typeof obj.embeddings !== "object" || obj.embeddings === null)
|
||||
return false;
|
||||
const embeddings = obj.embeddings as Record<string, unknown>;
|
||||
const values = embeddings.values;
|
||||
return (
|
||||
Array.isArray(values) &&
|
||||
values.every((v) => typeof v === "number" && Number.isFinite(v))
|
||||
);
|
||||
}
|
||||
|
||||
export class VertexAIEmbedder implements Embedder {
|
||||
private client: PredictionServiceClient | undefined;
|
||||
private helpers: AIPlatform["helpers"] | undefined;
|
||||
private initPromise: Promise<void> | undefined;
|
||||
private clientOptions: ClientOptions;
|
||||
private model: string;
|
||||
private embeddingDims: number;
|
||||
private location: string;
|
||||
private projectId: string;
|
||||
private embeddingTypes: {
|
||||
add: string;
|
||||
update: string;
|
||||
search: string;
|
||||
};
|
||||
|
||||
constructor(config: VertexAIConfig) {
|
||||
this.model = config.model || "gemini-embedding-001";
|
||||
this.embeddingDims = config.embeddingDims || 256;
|
||||
this.location =
|
||||
config.location || process.env.GCP_LOCATION || "us-central1";
|
||||
|
||||
// Left empty when unset: initClient() resolves it from Application Default
|
||||
// Credentials or the service account key file, the way the Python SDK does.
|
||||
this.projectId =
|
||||
config.googleProjectId ||
|
||||
process.env.GCP_PROJECT_ID ||
|
||||
process.env.GOOGLE_CLOUD_PROJECT ||
|
||||
process.env.GCLOUD_PROJECT ||
|
||||
"";
|
||||
|
||||
this.embeddingTypes = {
|
||||
add: config.memoryAddEmbeddingType || "RETRIEVAL_DOCUMENT",
|
||||
update: config.memoryUpdateEmbeddingType || "RETRIEVAL_DOCUMENT",
|
||||
search: config.memorySearchEmbeddingType || "RETRIEVAL_QUERY",
|
||||
};
|
||||
|
||||
const endpoint = `${this.location}-aiplatform.googleapis.com`;
|
||||
this.clientOptions = { apiEndpoint: endpoint };
|
||||
|
||||
if (config.vertexCredentialsJson) {
|
||||
this.clientOptions.keyFilename = config.vertexCredentialsJson;
|
||||
} else if (config.googleServiceAccountJson) {
|
||||
try {
|
||||
this.clientOptions.credentials =
|
||||
typeof config.googleServiceAccountJson === "string"
|
||||
? JSON.parse(config.googleServiceAccountJson)
|
||||
: config.googleServiceAccountJson;
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
"Failed to parse googleServiceAccountJson: " + (err as Error).message,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async initClient(): Promise<void> {
|
||||
// Memoized so concurrent embed() calls share one client instead of each
|
||||
// racing to build (and leak) their own gRPC channel.
|
||||
if (!this.initPromise) {
|
||||
this.initPromise = this.createClient().catch((err) => {
|
||||
this.initPromise = undefined;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
await this.initPromise;
|
||||
}
|
||||
|
||||
private async createClient(): Promise<void> {
|
||||
const aiplatform: AIPlatform = await loadPeer(
|
||||
"@google-cloud/aiplatform",
|
||||
"Vertex AI embedding provider",
|
||||
() => import("@google-cloud/aiplatform"),
|
||||
);
|
||||
|
||||
const client = new aiplatform.PredictionServiceClient(this.clientOptions);
|
||||
|
||||
if (!this.projectId) {
|
||||
try {
|
||||
this.projectId = await client.getProjectId();
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
"Vertex AI could not determine a Google Cloud project ID. Set googleProjectId in config, " +
|
||||
"one of the GCP_PROJECT_ID / GOOGLE_CLOUD_PROJECT / GCLOUD_PROJECT env vars, or configure " +
|
||||
"Application Default Credentials: " +
|
||||
(err as Error).message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.client = client;
|
||||
this.helpers = aiplatform.helpers;
|
||||
}
|
||||
|
||||
private endpoint(): string {
|
||||
return `projects/${this.projectId}/locations/${this.location}/publishers/google/models/${this.model}`;
|
||||
}
|
||||
|
||||
private formatInstance(text: string, taskType: string) {
|
||||
// task_type must live on the instance (snake_case), not in `parameters`.
|
||||
// Vertex silently ignores an unknown `parameters.taskType`, which would
|
||||
// fall back to the model's default task type. This mirrors the Python SDK's
|
||||
// TextEmbeddingInput(text=..., task_type=...).
|
||||
return {
|
||||
content: text,
|
||||
task_type: taskType,
|
||||
};
|
||||
}
|
||||
|
||||
async embed(
|
||||
text: string,
|
||||
memoryAction?: "add" | "update" | "search",
|
||||
): Promise<number[]> {
|
||||
await this.initClient();
|
||||
if (!this.client || !this.helpers) {
|
||||
throw new Error("Client not initialized");
|
||||
}
|
||||
|
||||
let embeddingType = "SEMANTIC_SIMILARITY";
|
||||
if (memoryAction !== undefined) {
|
||||
if (!(memoryAction in this.embeddingTypes)) {
|
||||
throw new Error(`Invalid memory action: ${memoryAction}`);
|
||||
}
|
||||
embeddingType = this.embeddingTypes[memoryAction];
|
||||
}
|
||||
|
||||
const instance = this.formatInstance(text, embeddingType);
|
||||
const parameters = {
|
||||
outputDimensionality: this.embeddingDims,
|
||||
};
|
||||
|
||||
const [response] = await this.client.predict({
|
||||
endpoint: this.endpoint(),
|
||||
instances: [this.helpers.toValue(instance) as any],
|
||||
parameters: this.helpers.toValue(parameters) as any,
|
||||
});
|
||||
|
||||
if (!response.predictions || response.predictions.length === 0) {
|
||||
throw new Error("No predictions returned from Vertex AI");
|
||||
}
|
||||
|
||||
const decoded = this.helpers.fromValue(response.predictions[0] as any);
|
||||
if (!isValidEmbedding(decoded)) {
|
||||
throw new Error("Failed to extract embedding values from response");
|
||||
}
|
||||
|
||||
return decoded.embeddings.values;
|
||||
}
|
||||
|
||||
async embedBatch(
|
||||
texts: string[],
|
||||
memoryAction: "add" | "update" | "search" = "add",
|
||||
): Promise<number[][]> {
|
||||
if (!texts || texts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await this.initClient();
|
||||
if (!this.client || !this.helpers) {
|
||||
throw new Error("Client not initialized");
|
||||
}
|
||||
|
||||
if (!(memoryAction in this.embeddingTypes)) {
|
||||
throw new Error(`Invalid memory action: ${memoryAction}`);
|
||||
}
|
||||
const embeddingType = this.embeddingTypes[memoryAction];
|
||||
|
||||
const allEmbeddings: number[][] = [];
|
||||
const batchSize = maxInstancesPerRequest(this.model);
|
||||
|
||||
for (let i = 0; i < texts.length; i += batchSize) {
|
||||
const chunk = texts.slice(i, i + batchSize);
|
||||
const instances = chunk.map(
|
||||
(text) =>
|
||||
this.helpers!.toValue(
|
||||
this.formatInstance(text, embeddingType),
|
||||
) as any,
|
||||
);
|
||||
const parameters = {
|
||||
outputDimensionality: this.embeddingDims,
|
||||
};
|
||||
|
||||
const [response] = await this.client.predict({
|
||||
endpoint: this.endpoint(),
|
||||
instances,
|
||||
parameters: this.helpers.toValue(parameters) as any,
|
||||
});
|
||||
|
||||
if (!response.predictions || response.predictions.length === 0) {
|
||||
throw new Error("No predictions returned from Vertex AI batch request");
|
||||
}
|
||||
|
||||
for (const prediction of response.predictions) {
|
||||
const decoded = this.helpers.fromValue(prediction as any);
|
||||
if (!isValidEmbedding(decoded)) {
|
||||
throw new Error(
|
||||
"Failed to extract embedding values from batch response",
|
||||
);
|
||||
}
|
||||
allEmbeddings.push(decoded.embeddings.values);
|
||||
}
|
||||
}
|
||||
|
||||
if (allEmbeddings.length !== texts.length) {
|
||||
throw new Error(
|
||||
`Vertex AI embedBatch() returned ${allEmbeddings.length} embeddings for ${texts.length} texts using model '${this.model}'`,
|
||||
);
|
||||
}
|
||||
|
||||
return allEmbeddings;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ export * from "./memory";
|
||||
export * from "./memory/memory.types";
|
||||
export * from "./types";
|
||||
export * from "./embeddings/base";
|
||||
export * from "./embeddings/aws_bedrock";
|
||||
export * from "./embeddings/huggingface";
|
||||
export * from "./embeddings/openai";
|
||||
export * from "./embeddings/ollama";
|
||||
@@ -11,7 +10,6 @@ export * from "./embeddings/together";
|
||||
export * from "./embeddings/google";
|
||||
export * from "./embeddings/azure";
|
||||
export * from "./embeddings/langchain";
|
||||
export * from "./embeddings/vertexai";
|
||||
export * from "./embeddings/fastembed";
|
||||
export * from "./llms/base";
|
||||
export * from "./llms/openai";
|
||||
@@ -25,10 +23,8 @@ export * from "./llms/mistral";
|
||||
export * from "./llms/langchain";
|
||||
export * from "./llms/litellm";
|
||||
export * from "./llms/vllm";
|
||||
export * from "./llms/aws_bedrock";
|
||||
export * from "./vector_stores/base";
|
||||
export * from "./vector_stores/memory";
|
||||
export * from "./vector_stores/baidu";
|
||||
export * from "./vector_stores/qdrant";
|
||||
export * from "./vector_stores/redis";
|
||||
export * from "./vector_stores/valkey";
|
||||
@@ -37,8 +33,6 @@ export * from "./vector_stores/langchain";
|
||||
export * from "./vector_stores/vectorize";
|
||||
export * from "./vector_stores/azure_ai_search";
|
||||
export * from "./vector_stores/pgvector";
|
||||
export * from "./vector_stores/databricks";
|
||||
export * from "./vector_stores/neptune_analytics";
|
||||
export * from "./vector_stores/elasticsearch";
|
||||
export * from "./vector_stores/upstash_vector";
|
||||
export * from "./vector_stores/azure_mysql";
|
||||
@@ -51,9 +45,4 @@ export * from "./vector_stores/milvus";
|
||||
export * from "./vector_stores/mongodb";
|
||||
export * from "./vector_stores/opensearch";
|
||||
export * from "./vector_stores/weaviate";
|
||||
export * from "./rerankers/base";
|
||||
export * from "./rerankers/cohere";
|
||||
export * from "./rerankers/llm";
|
||||
export * from "./rerankers/zeroentropy";
|
||||
export * from "./rerankers/cross_encoder";
|
||||
export * from "./utils/factory";
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import type Anthropic from "@anthropic-ai/sdk";
|
||||
import 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 readonly clientArgs: { apiKey: string; baseURL?: string };
|
||||
private client: Anthropic;
|
||||
private model: string;
|
||||
private maxTokens: number;
|
||||
private temperature?: number;
|
||||
@@ -22,7 +20,7 @@ export class AnthropicLLM implements LLM {
|
||||
if (config.baseURL) {
|
||||
clientArgs.baseURL = config.baseURL;
|
||||
}
|
||||
this.clientArgs = clientArgs;
|
||||
this.client = new Anthropic(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).
|
||||
@@ -31,22 +29,11 @@ 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");
|
||||
|
||||
@@ -1,294 +0,0 @@
|
||||
import { LLM, LLMResponse } from "./base";
|
||||
import { LLMConfig, Message } from "../types";
|
||||
|
||||
/**
|
||||
* Providers recognised in Bedrock model identifiers, mirroring the Python
|
||||
* provider's `PROVIDERS` list (`mem0/llms/aws_bedrock.py`).
|
||||
*/
|
||||
const PROVIDERS = [
|
||||
"ai21",
|
||||
"amazon",
|
||||
"anthropic",
|
||||
"cohere",
|
||||
"meta",
|
||||
"mistral",
|
||||
"stability",
|
||||
"writer",
|
||||
"deepseek",
|
||||
"gpt-oss",
|
||||
"perplexity",
|
||||
"snowflake",
|
||||
"titan",
|
||||
"command",
|
||||
"j2",
|
||||
"llama",
|
||||
"minimax",
|
||||
];
|
||||
|
||||
/**
|
||||
* Extract the model-family provider from a Bedrock model id
|
||||
* (e.g. `anthropic.claude-3-sonnet-...` -> `anthropic`).
|
||||
*/
|
||||
export function extractProvider(model: string): string {
|
||||
for (const provider of PROVIDERS) {
|
||||
const re = new RegExp(
|
||||
`\\b${provider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`,
|
||||
);
|
||||
if (re.test(model)) return provider;
|
||||
}
|
||||
throw new Error(`Unknown provider in model: ${model}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* AWS Bedrock fields (awsRegion / awsAccessKeyId / awsSecretAccessKey /
|
||||
* awsSessionToken / client) now live on the shared `LLMConfig`, so the
|
||||
* provider is configurable through the standard typed `Memory` config path.
|
||||
*/
|
||||
type AWSBedrockConfig = LLMConfig;
|
||||
|
||||
/**
|
||||
* AWS Bedrock LLM provider for the TypeScript OSS SDK.
|
||||
*
|
||||
* Mirrors `mem0/llms/aws_bedrock.py`. Uses the Bedrock **Converse API**
|
||||
* (`ConverseCommand`), which provides a uniform message/tool interface across
|
||||
* the Anthropic / Amazon (Nova) / Meta / Mistral / Cohere model families, so a
|
||||
* single code path serves them all (the Python provider keeps per-family
|
||||
* `invoke_model` branches for legacy reasons; Converse supersedes them).
|
||||
*
|
||||
* The `@aws-sdk/client-bedrock-runtime` dependency is loaded on first use via
|
||||
* dynamic `import()` so the package stays optional. Credentials resolve via the
|
||||
* standard AWS chain unless provided explicitly in config.
|
||||
*/
|
||||
interface BedrockSDK {
|
||||
BedrockRuntimeClient: new (config: Record<string, any>) => any;
|
||||
ConverseCommand: new (input: Record<string, any>) => any;
|
||||
}
|
||||
|
||||
export class AWSBedrockLLM implements LLM {
|
||||
private model: string;
|
||||
private provider: string;
|
||||
private temperature: number;
|
||||
private maxTokens: number;
|
||||
private topP?: number;
|
||||
private clientConfig: Record<string, any>;
|
||||
private clientOverride?: any;
|
||||
private sdkPromise?: Promise<BedrockSDK>;
|
||||
private clientPromise?: Promise<any>;
|
||||
|
||||
constructor(config: AWSBedrockConfig = {}) {
|
||||
this.model =
|
||||
(typeof config.model === "string" && config.model) ||
|
||||
"anthropic.claude-3-5-sonnet-20240620-v1:0";
|
||||
this.provider = extractProvider(this.model);
|
||||
this.temperature = config.temperature ?? 0.1;
|
||||
this.maxTokens = config.maxTokens ?? 2000;
|
||||
this.topP = config.topP;
|
||||
|
||||
const region =
|
||||
config.awsRegion ||
|
||||
process.env.AWS_REGION ||
|
||||
process.env.AWS_DEFAULT_REGION;
|
||||
const clientConfig: Record<string, any> = {};
|
||||
if (region) clientConfig.region = region;
|
||||
if (config.awsAccessKeyId && config.awsSecretAccessKey) {
|
||||
clientConfig.credentials = {
|
||||
accessKeyId: config.awsAccessKeyId,
|
||||
secretAccessKey: config.awsSecretAccessKey,
|
||||
...(config.awsSessionToken && {
|
||||
sessionToken: config.awsSessionToken,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
this.clientConfig = clientConfig;
|
||||
this.clientOverride = config.client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the optional AWS SDK on first use.
|
||||
*
|
||||
* This MUST be a dynamic `import()`, never `require()`: tsup/esbuild rewrite
|
||||
* `require()` in the published ESM bundle (`dist/oss/index.mjs`) into a
|
||||
* `__require` shim that throws `Dynamic require of "..." is not supported`,
|
||||
* so every ESM consumer would hit a dead provider even with the SDK installed.
|
||||
*/
|
||||
private async getSDK(): Promise<BedrockSDK> {
|
||||
if (!this.sdkPromise) {
|
||||
this.sdkPromise = import("@aws-sdk/client-bedrock-runtime").then(
|
||||
(sdk) => sdk as unknown as BedrockSDK,
|
||||
(err) => {
|
||||
// Let a later call retry rather than caching the rejection forever.
|
||||
this.sdkPromise = undefined;
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(
|
||||
"The '@aws-sdk/client-bedrock-runtime' package is required to use the AWS Bedrock LLM provider. " +
|
||||
`Install it with: npm install @aws-sdk/client-bedrock-runtime (original error: ${detail})`,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return this.sdkPromise;
|
||||
}
|
||||
|
||||
/** Memoized Bedrock client; an injected `config.client` short-circuits the SDK. */
|
||||
private async getClient(): Promise<any> {
|
||||
if (this.clientOverride) return this.clientOverride;
|
||||
if (!this.clientPromise) {
|
||||
this.clientPromise = this.getSDK().then(
|
||||
({ BedrockRuntimeClient }) =>
|
||||
new BedrockRuntimeClient(this.clientConfig),
|
||||
);
|
||||
}
|
||||
return this.clientPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split messages into a top-level `system` block (Converse passes system
|
||||
* prompts separately) and role-tagged content blocks for everything else.
|
||||
*/
|
||||
private formatMessages(messages: Message[]): {
|
||||
system?: { text: string }[];
|
||||
converseMessages: { role: string; content: { text: string }[] }[];
|
||||
} {
|
||||
const systemParts: string[] = [];
|
||||
const converseMessages: { role: string; content: { text: string }[] }[] =
|
||||
[];
|
||||
|
||||
for (const msg of messages) {
|
||||
const role = msg.role;
|
||||
const content =
|
||||
typeof msg.content === "string"
|
||||
? msg.content
|
||||
: JSON.stringify(msg.content);
|
||||
if (role === "system") {
|
||||
systemParts.push(content);
|
||||
} else {
|
||||
converseMessages.push({
|
||||
role: role === "assistant" ? "assistant" : "user",
|
||||
content: [{ text: content }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (converseMessages.length === 0) {
|
||||
converseMessages.push({ role: "user", content: [{ text: "" }] });
|
||||
}
|
||||
|
||||
return {
|
||||
system: systemParts.length
|
||||
? [{ text: systemParts.join("\n") }]
|
||||
: undefined,
|
||||
converseMessages,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the Converse `inferenceConfig`. Anthropic and MiniMax reasoning
|
||||
* models reject requests carrying both `temperature` and `topP`, so `topP`
|
||||
* is omitted for those families (mirrors the Python `_build_inference_config`).
|
||||
*/
|
||||
private buildInferenceConfig(): Record<string, any> {
|
||||
const inferenceConfig: Record<string, any> = {
|
||||
maxTokens: this.maxTokens,
|
||||
temperature: this.temperature,
|
||||
};
|
||||
if (
|
||||
this.topP != null &&
|
||||
!["anthropic", "minimax"].includes(this.provider)
|
||||
) {
|
||||
inferenceConfig.topP = this.topP;
|
||||
}
|
||||
return inferenceConfig;
|
||||
}
|
||||
|
||||
/** Convert OpenAI-style tools to the Converse `toolConfig` shape. */
|
||||
private convertToolsToConverse(tools: any[]): any | undefined {
|
||||
if (!tools || tools.length === 0) return undefined;
|
||||
const converseTools = tools
|
||||
.filter((t) => t?.type === "function" && t.function)
|
||||
.map((t) => ({
|
||||
toolSpec: {
|
||||
name: t.function.name,
|
||||
description: t.function.description || "",
|
||||
inputSchema: { json: t.function.parameters || {} },
|
||||
},
|
||||
}));
|
||||
return converseTools.length ? { tools: converseTools } : undefined;
|
||||
}
|
||||
|
||||
private async converse(messages: Message[], tools?: any[]): Promise<any> {
|
||||
const { system, converseMessages } = this.formatMessages(messages);
|
||||
const input: Record<string, any> = {
|
||||
modelId: this.model,
|
||||
messages: converseMessages,
|
||||
inferenceConfig: this.buildInferenceConfig(),
|
||||
};
|
||||
if (system) input.system = system;
|
||||
const toolConfig = tools ? this.convertToolsToConverse(tools) : undefined;
|
||||
if (toolConfig) input.toolConfig = toolConfig;
|
||||
|
||||
const [{ ConverseCommand }, client] = await Promise.all([
|
||||
this.getSDK(),
|
||||
this.getClient(),
|
||||
]);
|
||||
return client.send(new ConverseCommand(input));
|
||||
}
|
||||
|
||||
/** Pull the first text block out of a Converse response. */
|
||||
private parseText(response: any): string {
|
||||
const content = response?.output?.message?.content || [];
|
||||
for (const block of content) {
|
||||
if (block && typeof block.text === "string") return block.text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** Collect any toolUse blocks out of a Converse response. */
|
||||
private parseToolCalls(response: any): { name: string; arguments: string }[] {
|
||||
const content = response?.output?.message?.content || [];
|
||||
const calls: { name: string; arguments: string }[] = [];
|
||||
for (const block of content) {
|
||||
if (block?.toolUse) {
|
||||
calls.push({
|
||||
name: block.toolUse.name,
|
||||
arguments: JSON.stringify(block.toolUse.input ?? {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
return calls;
|
||||
}
|
||||
|
||||
async generateResponse(
|
||||
messages: Message[],
|
||||
_responseFormat?: { type: string },
|
||||
tools?: any[],
|
||||
): Promise<string | LLMResponse> {
|
||||
try {
|
||||
const response = await this.converse(messages, tools);
|
||||
if (tools && tools.length) {
|
||||
const toolCalls = this.parseToolCalls(response);
|
||||
if (toolCalls.length) {
|
||||
return {
|
||||
content: this.parseText(response),
|
||||
role: "assistant",
|
||||
toolCalls,
|
||||
};
|
||||
}
|
||||
}
|
||||
return this.parseText(response);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`AWS Bedrock LLM failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async generateChat(messages: Message[]): Promise<LLMResponse> {
|
||||
try {
|
||||
const response = await this.converse(messages);
|
||||
return { content: this.parseText(response), role: "assistant" };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`AWS Bedrock LLM failed: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,16 @@
|
||||
import type { GoogleGenAI } from "@google/genai";
|
||||
import { 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.apiKey = config.apiKey;
|
||||
this.google = new GoogleGenAI({ 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: [
|
||||
@@ -42,7 +30,6 @@ 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
|
||||
@@ -85,7 +72,6 @@ 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,37 +1,24 @@
|
||||
import type { Groq } from "groq-sdk";
|
||||
import { 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.apiKey = apiKey;
|
||||
this.client = new Groq({ 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) => ({
|
||||
@@ -48,7 +35,6 @@ 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,16 +1,17 @@
|
||||
import type { BaseLanguageModel } from "@langchain/core/language_models/base";
|
||||
import type { BaseMessage } from "@langchain/core/messages";
|
||||
import { BaseLanguageModel } from "@langchain/core/language_models/base";
|
||||
import {
|
||||
AIMessage,
|
||||
HumanMessage,
|
||||
SystemMessage,
|
||||
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 = async (
|
||||
messages: Message[],
|
||||
): Promise<BaseMessage[]> => {
|
||||
const { AIMessage, HumanMessage, SystemMessage } =
|
||||
await import("@langchain/core/messages");
|
||||
const convertToLangchainMessages = (messages: Message[]): BaseMessage[] => {
|
||||
return messages.map((msg) => {
|
||||
const content =
|
||||
typeof msg.content === "string"
|
||||
@@ -61,7 +62,7 @@ export class LangchainLLM implements LLM {
|
||||
response_format?: { type: string },
|
||||
tools?: any[],
|
||||
): Promise<string | LLMResponse> {
|
||||
const langchainMessages = await convertToLangchainMessages(messages);
|
||||
const langchainMessages = convertToLangchainMessages(messages);
|
||||
let runnable: any = this.llmInstance;
|
||||
const invokeOptions: Record<string, any> = {};
|
||||
let isStructuredOutput = false;
|
||||
@@ -169,7 +170,7 @@ export class LangchainLLM implements LLM {
|
||||
}
|
||||
|
||||
async generateChat(messages: Message[]): Promise<LLMResponse> {
|
||||
const langchainMessages = await convertToLangchainMessages(messages);
|
||||
const langchainMessages = convertToLangchainMessages(messages);
|
||||
try {
|
||||
const response = await this.llmInstance.invoke(langchainMessages);
|
||||
if (response && typeof response.content === "string") {
|
||||
|
||||
@@ -1,31 +1,21 @@
|
||||
import type { Mistral } from "@mistralai/mistralai";
|
||||
import { 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.apiKey = config.apiKey;
|
||||
this.client = new Mistral({
|
||||
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") {
|
||||
@@ -51,7 +41,6 @@ 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) => ({
|
||||
@@ -93,7 +82,6 @@ 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,36 +1,29 @@
|
||||
import type { Ollama } from "ollama";
|
||||
import { 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.host = config.url || config.baseURL || "http://localhost:11434";
|
||||
this.ollama = new Ollama({
|
||||
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) {
|
||||
@@ -70,7 +63,6 @@ export class OllamaLLM implements LLM {
|
||||
}
|
||||
|
||||
async generateChat(messages: Message[]): Promise<LLMResponse> {
|
||||
await this.ensureClient();
|
||||
try {
|
||||
await this.ensureModelExists();
|
||||
} catch (err) {
|
||||
@@ -101,7 +93,6 @@ 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}...`);
|
||||
|
||||
@@ -7,14 +7,12 @@ import {
|
||||
Message,
|
||||
SearchFilters,
|
||||
SearchResult,
|
||||
VectorStoreConfig,
|
||||
} from "../types";
|
||||
import {
|
||||
EmbedderFactory,
|
||||
LLMFactory,
|
||||
VectorStoreFactory,
|
||||
HistoryManagerFactory,
|
||||
RerankerFactory,
|
||||
} from "../utils/factory";
|
||||
import {
|
||||
FactRetrievalSchema,
|
||||
@@ -30,7 +28,6 @@ import {
|
||||
import { DummyHistoryManager } from "../storage/DummyHistoryManager";
|
||||
import { Embedder } from "../embeddings/base";
|
||||
import { LLM } from "../llms/base";
|
||||
import { Reranker } from "../rerankers/base";
|
||||
import { VectorStore } from "../vector_stores/base";
|
||||
import { ConfigManager } from "../config/manager";
|
||||
|
||||
@@ -39,7 +36,6 @@ import {
|
||||
SearchMemoryOptions,
|
||||
DeleteAllMemoryOptions,
|
||||
GetAllMemoryOptions,
|
||||
UpdateMemoryOptions,
|
||||
UpdateProjectOptions,
|
||||
} from "./memory.types";
|
||||
import { parse_vision_messages } from "../utils/memory";
|
||||
@@ -76,22 +72,9 @@ import {
|
||||
ScoredResult,
|
||||
} from "../utils/scoring";
|
||||
import { getDefaultVectorStoreDbPath } from "../utils/sqlite";
|
||||
import { logger } from "../utils/logger";
|
||||
import { normalizeExpirationDate, payloadIsExpired } from "../utils/expiration";
|
||||
import { getOrCreateMem0UserId } from "../../../client/config";
|
||||
|
||||
export class LLMError extends Error {
|
||||
readonly cause?: unknown;
|
||||
|
||||
constructor(message: string, options: { cause?: unknown } = {}) {
|
||||
super(message);
|
||||
this.name = "LLMError";
|
||||
this.cause = options.cause;
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
// Entity params that must be passed via filters check both snake_case and camelCase
|
||||
// Entity params that must be passed via filters - check both snake_case and camelCase
|
||||
const ENTITY_PARAMS = [
|
||||
"user_id",
|
||||
"agent_id",
|
||||
@@ -101,10 +84,6 @@ 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
|
||||
@@ -126,19 +105,18 @@ 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/null
|
||||
* @returns The trimmed entity ID, or undefined if input is undefined
|
||||
* @throws Error if entity ID is invalid
|
||||
*/
|
||||
function validateAndTrimEntityId(
|
||||
value: string | number | undefined | null,
|
||||
value: string | undefined,
|
||||
name: string,
|
||||
): string | undefined {
|
||||
if (value == null) return undefined;
|
||||
const trimmed = String(value).trim();
|
||||
if (value === undefined) return undefined;
|
||||
const trimmed = value.trim();
|
||||
if (trimmed === "") {
|
||||
throw new Error(
|
||||
`Invalid ${name}: cannot be empty or whitespace-only. Provide a valid identifier.`,
|
||||
@@ -183,7 +161,6 @@ export class Memory {
|
||||
private embedder: Embedder;
|
||||
private vectorStore!: VectorStore;
|
||||
private llm: LLM;
|
||||
private reranker: Reranker | null = null;
|
||||
private db: HistoryManager;
|
||||
private collectionName: string | undefined;
|
||||
private apiVersion: string;
|
||||
@@ -208,12 +185,6 @@ export class Memory {
|
||||
this.config.llm.provider,
|
||||
this.config.llm.config,
|
||||
);
|
||||
if (this.config.reranker) {
|
||||
this.reranker = RerankerFactory.create(
|
||||
this.config.reranker.provider,
|
||||
this.config.reranker.config,
|
||||
);
|
||||
}
|
||||
if (this.config.disableHistory) {
|
||||
this.db = new DummyHistoryManager();
|
||||
} else {
|
||||
@@ -292,24 +263,18 @@ export class Memory {
|
||||
|
||||
private async getEntityStore(): Promise<VectorStore> {
|
||||
if (!this._entityStore) {
|
||||
const entityProvider = this.config.vectorStore.provider;
|
||||
const entityCollectionName = `${this.collectionName}_entities`;
|
||||
const entityConfig: VectorStoreConfig = {
|
||||
const entityConfig = {
|
||||
...this.config.vectorStore.config,
|
||||
collectionName: entityCollectionName,
|
||||
};
|
||||
// For file-based stores (memory/SQLite), always use a separate DB for entities
|
||||
if (entityProvider === "memory") {
|
||||
if (this.config.vectorStore.provider === "memory") {
|
||||
const basePath = entityConfig.dbPath || getDefaultVectorStoreDbPath();
|
||||
entityConfig.dbPath = basePath.replace(/\.db$/, "_entities.db");
|
||||
}
|
||||
if (entityProvider === "databricks") {
|
||||
entityConfig.tableName = entityConfig.tableName
|
||||
? `${entityConfig.tableName}_entities`
|
||||
: entityCollectionName;
|
||||
}
|
||||
this._entityStore = VectorStoreFactory.create(
|
||||
entityProvider,
|
||||
this.config.vectorStore.provider,
|
||||
entityConfig,
|
||||
);
|
||||
await this._entityStore.initialize();
|
||||
@@ -431,7 +396,7 @@ export class Memory {
|
||||
}
|
||||
let vec: number[];
|
||||
try {
|
||||
vec = await this.embedder.embed(entityText, "update");
|
||||
vec = await this.embedder.embed(entityText);
|
||||
} catch (e) {
|
||||
console.debug(`Entity re-embed failed for '${entityText}': ${e}`);
|
||||
continue;
|
||||
@@ -475,7 +440,7 @@ export class Memory {
|
||||
try {
|
||||
let entityVec: number[];
|
||||
try {
|
||||
entityVec = await this.embedder.embed(entity.text, "add");
|
||||
entityVec = await this.embedder.embed(entity.text);
|
||||
} catch (e) {
|
||||
console.debug(`Entity embed failed for '${entity.text}': ${e}`);
|
||||
continue;
|
||||
@@ -748,11 +713,6 @@ export class Memory {
|
||||
if (agentId) filters.agent_id = metadata.agent_id = agentId;
|
||||
if (runId) filters.run_id = metadata.run_id = runId;
|
||||
|
||||
// Normalize expiration date into the stored metadata (round-trips via get()).
|
||||
if (config.expirationDate != null) {
|
||||
metadata.expiration_date = normalizeExpirationDate(config.expirationDate);
|
||||
}
|
||||
|
||||
if (!filters.user_id && !filters.agent_id && !filters.run_id) {
|
||||
throw new Error(
|
||||
"One of the filters: userId, agentId or runId is required!",
|
||||
@@ -850,7 +810,7 @@ export class Memory {
|
||||
.join("\n");
|
||||
|
||||
// Phase 1: Existing memory retrieval
|
||||
const queryEmbedding = await this.embedder.embed(parsedMessages, "search");
|
||||
const queryEmbedding = await this.embedder.embed(parsedMessages);
|
||||
const existingResults = await this.vectorStore.search(
|
||||
queryEmbedding,
|
||||
10,
|
||||
@@ -894,7 +854,7 @@ export class Memory {
|
||||
)) as string;
|
||||
} catch (e) {
|
||||
console.error("LLM extraction failed:", e);
|
||||
throw new LLMError(`LLM extraction failed: ${e}`, { cause: e });
|
||||
return [];
|
||||
}
|
||||
|
||||
// Parse response
|
||||
@@ -944,7 +904,7 @@ export class Memory {
|
||||
.filter((t) => t.length > 0);
|
||||
let embedMap: Record<string, number[]> = {};
|
||||
try {
|
||||
const memEmbeddingsList = await this.embedder.embedBatch(memTexts, "add");
|
||||
const memEmbeddingsList = await this.embedder.embedBatch(memTexts);
|
||||
for (let i = 0; i < memTexts.length; i++) {
|
||||
embedMap[memTexts[i]] = memEmbeddingsList[i];
|
||||
}
|
||||
@@ -952,7 +912,7 @@ export class Memory {
|
||||
// Fallback: embed individually
|
||||
for (const text of memTexts) {
|
||||
try {
|
||||
embedMap[text] = await this.embedder.embed(text, "add");
|
||||
embedMap[text] = await this.embedder.embed(text);
|
||||
} catch (e) {
|
||||
console.warn(`Failed to embed memory text: ${e}`);
|
||||
}
|
||||
@@ -1130,13 +1090,13 @@ export class Memory {
|
||||
// 7b: Single batch embed for all unique entities
|
||||
let entityEmbeddings: (number[] | null)[];
|
||||
try {
|
||||
entityEmbeddings = await this.embedder.embedBatch(entityTexts, "add");
|
||||
entityEmbeddings = await this.embedder.embedBatch(entityTexts);
|
||||
} catch {
|
||||
// Fallback: embed individually
|
||||
entityEmbeddings = [];
|
||||
for (const t of entityTexts) {
|
||||
try {
|
||||
entityEmbeddings.push(await this.embedder.embed(t, "add"));
|
||||
entityEmbeddings.push(await this.embedder.embed(t));
|
||||
} catch {
|
||||
entityEmbeddings.push(null);
|
||||
}
|
||||
@@ -1347,12 +1307,7 @@ export class Memory {
|
||||
: {};
|
||||
|
||||
await this._ensureInitialized();
|
||||
const {
|
||||
topK = 20,
|
||||
threshold = 0.1,
|
||||
explain = false,
|
||||
showExpired = false,
|
||||
} = config;
|
||||
const { topK = 20, threshold = 0.1, explain = false } = config;
|
||||
|
||||
await this._captureEvent("search", {
|
||||
query_length: query.length,
|
||||
@@ -1400,7 +1355,7 @@ export class Memory {
|
||||
const queryEntities = extractEntities(query);
|
||||
|
||||
// Step 2: Embed query
|
||||
const queryEmbedding = await this.embedder.embed(query, "search");
|
||||
const queryEmbedding = await this.embedder.embed(query);
|
||||
|
||||
// Step 3: Semantic search (over-fetch for scoring pool)
|
||||
const internalLimit = Math.max(topK * 4, 60);
|
||||
@@ -1465,10 +1420,7 @@ export class Memory {
|
||||
entitySearchFilters[k] = effectiveFilters[k];
|
||||
}
|
||||
const entityTexts = deduped.map((e) => e.text);
|
||||
const embeddings = await this.embedder.embedBatch(
|
||||
entityTexts,
|
||||
"search",
|
||||
);
|
||||
const embeddings = await this.embedder.embedBatch(entityTexts);
|
||||
|
||||
if (embeddings.length !== entityTexts.length) {
|
||||
console.warn(
|
||||
@@ -1523,13 +1475,11 @@ export class Memory {
|
||||
}
|
||||
|
||||
// Step 7: Build candidate set from semantic results
|
||||
const candidates = semanticResults
|
||||
.filter((mem) => showExpired || !payloadIsExpired(mem.payload))
|
||||
.map((mem) => ({
|
||||
id: String(mem.id),
|
||||
score: mem.score ?? 0,
|
||||
payload: mem.payload || {},
|
||||
}));
|
||||
const candidates = semanticResults.map((mem) => ({
|
||||
id: String(mem.id),
|
||||
score: mem.score ?? 0,
|
||||
payload: mem.payload || {},
|
||||
}));
|
||||
|
||||
// Step 8: Score and rank
|
||||
const scoredResults = scoreAndRank(
|
||||
@@ -1576,30 +1526,8 @@ export class Memory {
|
||||
};
|
||||
});
|
||||
|
||||
// Step 10: Optionally re-rank with the configured reranker. Opt-in per
|
||||
// search via `rerank: true`; a no-op when no reranker is configured.
|
||||
const invokeReranker = Boolean(
|
||||
config.rerank && this.reranker && results.length > 0,
|
||||
);
|
||||
let finalResults = results;
|
||||
if (invokeReranker) {
|
||||
try {
|
||||
const ranked = await this.reranker!.rerank(
|
||||
query,
|
||||
results.map((r) => r.memory),
|
||||
topK,
|
||||
);
|
||||
finalResults = ranked.map((r) => ({
|
||||
...results[r.index],
|
||||
rerankScore: r.rerankScore,
|
||||
}));
|
||||
} catch (e) {
|
||||
console.warn(`Reranking failed, using original results: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
results: finalResults,
|
||||
results,
|
||||
};
|
||||
const searchElapsedMs = Date.now() - searchStartMs;
|
||||
if (temporalUsageNotice) {
|
||||
@@ -1635,49 +1563,11 @@ export class Memory {
|
||||
return result;
|
||||
}
|
||||
|
||||
async update(
|
||||
memoryId: string,
|
||||
config: string | UpdateMemoryOptions,
|
||||
): Promise<{ message: string }> {
|
||||
async update(memoryId: string, data: string): Promise<{ message: string }> {
|
||||
await this._ensureInitialized();
|
||||
await this._captureEvent("update", { memory_id: memoryId });
|
||||
|
||||
const options: UpdateMemoryOptions =
|
||||
typeof config === "string" ? { text: config } : config;
|
||||
|
||||
const { data, metadata, expirationDate } = options;
|
||||
let text = options.text;
|
||||
|
||||
if (data != null) {
|
||||
logger.warn(
|
||||
"The `data` option of update() is deprecated and will be removed in " +
|
||||
"the next major release. Use `text` instead.",
|
||||
);
|
||||
if (text == null) {
|
||||
text = data;
|
||||
}
|
||||
}
|
||||
|
||||
if (text == null && metadata == null && expirationDate === undefined) {
|
||||
throw new Error(
|
||||
"At least one of text, metadata, or expirationDate must be provided.",
|
||||
);
|
||||
}
|
||||
|
||||
const updateMetadata: Record<string, any> = { ...metadata };
|
||||
if (expirationDate !== undefined) {
|
||||
updateMetadata.expiration_date =
|
||||
expirationDate === null
|
||||
? null
|
||||
: normalizeExpirationDate(expirationDate);
|
||||
}
|
||||
|
||||
const existingEmbeddings: Record<string, number[]> = {};
|
||||
if (text != null) {
|
||||
existingEmbeddings[text] = await this.embedder.embed(text, "update");
|
||||
}
|
||||
|
||||
await this.updateMemory(memoryId, text, existingEmbeddings, updateMetadata);
|
||||
const embedding = await this.embedder.embed(data);
|
||||
await this.updateMemory(memoryId, data, { [data]: embedding });
|
||||
const result = { message: "Memory updated successfully!" };
|
||||
await this._displayFirstRunNotice("update");
|
||||
return result;
|
||||
@@ -1759,7 +1649,7 @@ export class Memory {
|
||||
await this.db.reset();
|
||||
|
||||
// Check provider before attempting deleteCol
|
||||
if (this.config.vectorStore.provider !== "langchain") {
|
||||
if (this.config.vectorStore.provider.toLowerCase() !== "langchain") {
|
||||
try {
|
||||
await this.vectorStore.deleteCol();
|
||||
} catch (e) {
|
||||
@@ -1814,7 +1704,7 @@ export class Memory {
|
||||
|
||||
await this._ensureInitialized();
|
||||
|
||||
const { topK = 20, showExpired = false } = config;
|
||||
const { topK = 20 } = config;
|
||||
|
||||
// Validate and trim entity IDs in filters. Drop keys that resolve to
|
||||
// undefined so downstream vector stores don't receive
|
||||
@@ -1843,13 +1733,7 @@ export class Memory {
|
||||
);
|
||||
}
|
||||
|
||||
// Over-fetch so expired memories dropped below still leave topK survivors.
|
||||
const fetchLimit = showExpired ? topK : Math.max(topK * 4, 60);
|
||||
const [memories] = await this.vectorStore.list(filters, fetchLimit);
|
||||
|
||||
const visibleMemories = showExpired
|
||||
? memories
|
||||
: memories.filter((mem) => !payloadIsExpired(mem.payload));
|
||||
const [memories] = await this.vectorStore.list(filters, topK);
|
||||
|
||||
const excludedKeys = new Set([
|
||||
"user_id",
|
||||
@@ -1862,7 +1746,7 @@ export class Memory {
|
||||
"textLemmatized",
|
||||
"attributedTo",
|
||||
]);
|
||||
const results = visibleMemories.slice(0, topK).map((mem) => ({
|
||||
const results = memories.map((mem) => ({
|
||||
id: mem.id,
|
||||
memory: mem.payload.data,
|
||||
hash: mem.payload.hash,
|
||||
@@ -1899,7 +1783,7 @@ export class Memory {
|
||||
): Promise<string> {
|
||||
const memoryId = uuidv4();
|
||||
const embedding =
|
||||
existingEmbeddings[data] || (await this.embedder.embed(data, "add"));
|
||||
existingEmbeddings[data] || (await this.embedder.embed(data));
|
||||
|
||||
const memoryMetadata = {
|
||||
...metadata,
|
||||
@@ -1923,7 +1807,7 @@ export class Memory {
|
||||
|
||||
private async updateMemory(
|
||||
memoryId: string,
|
||||
data: string | undefined,
|
||||
data: string,
|
||||
existingEmbeddings: Record<string, number[]>,
|
||||
metadata: Record<string, any> = {},
|
||||
): Promise<string> {
|
||||
@@ -1933,30 +1817,15 @@ export class Memory {
|
||||
}
|
||||
|
||||
const prevValue = existingMemory.payload.data;
|
||||
// Metadata-only update: fall back to the stored text so we can re-index it.
|
||||
const newData = data ?? prevValue;
|
||||
if (typeof newData !== "string") {
|
||||
throw new Error(
|
||||
`Memory with ID ${memoryId} does not have text content to update`,
|
||||
);
|
||||
}
|
||||
const textChanged = newData !== prevValue;
|
||||
|
||||
const embedding =
|
||||
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)),
|
||||
);
|
||||
existingEmbeddings[data] || (await this.embedder.embed(data));
|
||||
|
||||
const newMetadata = {
|
||||
...existingMemory.payload,
|
||||
...sanitizedMetadata,
|
||||
data: newData,
|
||||
hash: createHash("md5").update(newData).digest("hex"),
|
||||
textLemmatized: lemmatizeForBm25(newData),
|
||||
...metadata,
|
||||
data,
|
||||
hash: createHash("md5").update(data).digest("hex"),
|
||||
textLemmatized: lemmatizeForBm25(data),
|
||||
createdAt: existingMemory.payload.createdAt,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
@@ -1965,22 +1834,20 @@ export class Memory {
|
||||
await this.db.addHistory(
|
||||
memoryId,
|
||||
prevValue,
|
||||
newData,
|
||||
data,
|
||||
"UPDATE",
|
||||
newMetadata.createdAt,
|
||||
newMetadata.updatedAt,
|
||||
);
|
||||
|
||||
// Entity-store cleanup only when the text changed: strip this memory's id
|
||||
// from old-text entities, then re-extract from the new text and link back.
|
||||
if (textChanged) {
|
||||
try {
|
||||
const sessionFilters = this._sessionFiltersFromPayload(newMetadata);
|
||||
await this._removeMemoryFromEntityStore(memoryId, sessionFilters);
|
||||
await this._linkEntitiesForMemory(memoryId, newData, sessionFilters);
|
||||
} catch (e) {
|
||||
console.warn(`Entity store cleanup/link failed during update: ${e}`);
|
||||
}
|
||||
// Entity-store cleanup: strip this memory's id from old-text entities,
|
||||
// then re-extract entities from the new text and link them back.
|
||||
try {
|
||||
const sessionFilters = this._sessionFiltersFromPayload(newMetadata);
|
||||
await this._removeMemoryFromEntityStore(memoryId, sessionFilters);
|
||||
await this._linkEntitiesForMemory(memoryId, data, sessionFilters);
|
||||
} catch (e) {
|
||||
console.warn(`Entity store cleanup/link failed during update: ${e}`);
|
||||
}
|
||||
|
||||
return memoryId;
|
||||
|
||||
@@ -12,22 +12,6 @@ export interface AddMemoryOptions extends Entity {
|
||||
filters?: SearchFilters;
|
||||
infer?: boolean;
|
||||
timestamp?: number | string | Date | null;
|
||||
/** Date (YYYY-MM-DD) after which the memory is considered expired. */
|
||||
expirationDate?: string | null;
|
||||
}
|
||||
|
||||
export interface UpdateMemoryOptions {
|
||||
/** New content to update the memory with. */
|
||||
text?: string;
|
||||
/**
|
||||
* New content to update the memory with.
|
||||
* @deprecated Use `text` instead. Will be removed in the next major release.
|
||||
*/
|
||||
data?: string;
|
||||
/** Metadata merged into the memory's existing metadata. */
|
||||
metadata?: Record<string, any>;
|
||||
/** Date (YYYY-MM-DD) after which the memory expires, or `null` to clear it. */
|
||||
expirationDate?: string | null;
|
||||
}
|
||||
|
||||
export interface SearchMemoryOptions {
|
||||
@@ -36,20 +20,11 @@ export interface SearchMemoryOptions {
|
||||
threshold?: number;
|
||||
explain?: boolean;
|
||||
referenceDate?: number | string | Date | null;
|
||||
/**
|
||||
* Re-rank the results with the configured reranker before returning. No-op
|
||||
* when no `reranker` is configured on the Memory.
|
||||
*/
|
||||
rerank?: boolean;
|
||||
/** Include expired memories in the results. Defaults to false. */
|
||||
showExpired?: boolean;
|
||||
}
|
||||
|
||||
export interface GetAllMemoryOptions {
|
||||
topK?: number;
|
||||
filters?: SearchFilters;
|
||||
/** Include expired memories in the results. Defaults to false. */
|
||||
showExpired?: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteAllMemoryOptions extends Entity {}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
export interface RerankResult {
|
||||
/** Index into the input `documents` array. */
|
||||
index: number;
|
||||
/** Relevance of the document to the query, 0..1, higher = more relevant. */
|
||||
rerankScore: number;
|
||||
}
|
||||
|
||||
export interface Reranker {
|
||||
/**
|
||||
* Rank `documents` by relevance to `query`.
|
||||
*
|
||||
* Returns results sorted by descending relevance. When `topK` is given, at
|
||||
* most that many results are returned. Each result's `index` points back into
|
||||
* the input `documents` array so callers can recover the original item.
|
||||
*/
|
||||
rerank(
|
||||
query: string,
|
||||
documents: string[],
|
||||
topK?: number,
|
||||
): Promise<RerankResult[]>;
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
const mockRerank = jest.fn();
|
||||
|
||||
jest.mock("cohere-ai", () => ({
|
||||
CohereClient: jest.fn().mockImplementation(() => ({
|
||||
rerank: mockRerank,
|
||||
})),
|
||||
}));
|
||||
|
||||
import { CohereClient } from "cohere-ai";
|
||||
import { CohereReranker } from "./cohere";
|
||||
|
||||
describe("CohereReranker", () => {
|
||||
beforeEach(() => {
|
||||
mockRerank.mockReset();
|
||||
(CohereClient as unknown as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
it("throws when no API key is provided or configured", () => {
|
||||
const originalEnv = process.env.COHERE_API_KEY;
|
||||
delete process.env.COHERE_API_KEY;
|
||||
|
||||
expect(() => new CohereReranker({})).toThrow(/Cohere API key is required/);
|
||||
|
||||
if (originalEnv !== undefined) process.env.COHERE_API_KEY = originalEnv;
|
||||
});
|
||||
|
||||
it("sends the query, documents, topN, and default model to Cohere", async () => {
|
||||
mockRerank.mockResolvedValue({ results: [] });
|
||||
const reranker = new CohereReranker({ apiKey: "key" });
|
||||
|
||||
await reranker.rerank("capital of US?", ["a", "b", "c"], 2);
|
||||
|
||||
expect(mockRerank).toHaveBeenCalledWith({
|
||||
model: "rerank-v3.5",
|
||||
query: "capital of US?",
|
||||
documents: ["a", "b", "c"],
|
||||
topN: 2,
|
||||
returnDocuments: false,
|
||||
maxChunksPerDoc: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("defaults topN to documents.length when neither the call nor config sets a top_k", async () => {
|
||||
mockRerank.mockResolvedValue({ results: [] });
|
||||
const reranker = new CohereReranker({ apiKey: "key" });
|
||||
|
||||
await reranker.rerank("q", ["a", "b", "c"]);
|
||||
|
||||
expect(mockRerank).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ topN: 3 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards returnDocuments and maxChunksPerDoc from config", async () => {
|
||||
mockRerank.mockResolvedValue({ results: [] });
|
||||
const reranker = new CohereReranker({
|
||||
apiKey: "key",
|
||||
returnDocuments: true,
|
||||
maxChunksPerDoc: 5,
|
||||
});
|
||||
|
||||
await reranker.rerank("q", ["a"]);
|
||||
|
||||
expect(mockRerank).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ returnDocuments: true, maxChunksPerDoc: 5 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns Cohere's ranked results as {index, rerankScore}", async () => {
|
||||
mockRerank.mockResolvedValue({
|
||||
results: [
|
||||
{ index: 2, relevanceScore: 0.9 },
|
||||
{ index: 0, relevanceScore: 0.31 },
|
||||
],
|
||||
});
|
||||
const reranker = new CohereReranker({ apiKey: "key" });
|
||||
|
||||
const results = await reranker.rerank("q", ["x", "y", "z"]);
|
||||
|
||||
expect(results).toEqual([
|
||||
{ index: 2, rerankScore: 0.9 },
|
||||
{ index: 0, rerankScore: 0.31 },
|
||||
]);
|
||||
});
|
||||
|
||||
it("uses a custom model when provided", async () => {
|
||||
mockRerank.mockResolvedValue({ results: [] });
|
||||
const reranker = new CohereReranker({
|
||||
apiKey: "key",
|
||||
model: "rerank-v4.0-pro",
|
||||
});
|
||||
|
||||
await reranker.rerank("q", ["a"]);
|
||||
|
||||
expect(mockRerank).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ model: "rerank-v4.0-pro" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns an empty array without calling Cohere when there are no documents", async () => {
|
||||
const reranker = new CohereReranker({ apiKey: "key" });
|
||||
|
||||
const results = await reranker.rerank("q", []);
|
||||
|
||||
expect(results).toEqual([]);
|
||||
expect(mockRerank).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to the original order with rerankScore 0.0 when the Cohere API call fails", async () => {
|
||||
mockRerank.mockRejectedValue(new Error("cohere is down"));
|
||||
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const reranker = new CohereReranker({ apiKey: "key" });
|
||||
|
||||
const results = await reranker.rerank("q", ["a", "b", "c"]);
|
||||
|
||||
expect(results).toEqual([
|
||||
{ index: 0, rerankScore: 0.0 },
|
||||
{ index: 1, rerankScore: 0.0 },
|
||||
{ index: 2, rerankScore: 0.0 },
|
||||
]);
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("slices the fallback results by topK when the Cohere API call fails", async () => {
|
||||
mockRerank.mockRejectedValue(new Error("cohere is down"));
|
||||
jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const reranker = new CohereReranker({ apiKey: "key", topK: 2 });
|
||||
|
||||
const results = await reranker.rerank("q", ["a", "b", "c"]);
|
||||
|
||||
expect(results).toEqual([
|
||||
{ index: 0, rerankScore: 0.0 },
|
||||
{ index: 1, rerankScore: 0.0 },
|
||||
]);
|
||||
|
||||
(console.warn as jest.Mock).mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
import { RerankerConfig } from "../types";
|
||||
import { Reranker, RerankResult } from "./base";
|
||||
import { loadPeer } from "../utils/load_peer";
|
||||
|
||||
const DEFAULT_MODEL = "rerank-v3.5";
|
||||
|
||||
export class CohereReranker implements Reranker {
|
||||
private clientInstance?: any;
|
||||
private clientPromise?: Promise<any>;
|
||||
private readonly apiKey: string;
|
||||
private model: string;
|
||||
private topK?: number;
|
||||
private returnDocuments: boolean;
|
||||
private maxChunksPerDoc?: number;
|
||||
|
||||
constructor(config: RerankerConfig) {
|
||||
const apiKey = config.apiKey || process.env.COHERE_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
"Cohere API key is required. Set COHERE_API_KEY environment variable or pass apiKey in config.",
|
||||
);
|
||||
}
|
||||
this.apiKey = apiKey;
|
||||
this.model = config.model || DEFAULT_MODEL;
|
||||
this.topK = config.topK;
|
||||
this.returnDocuments = config.returnDocuments ?? false;
|
||||
this.maxChunksPerDoc = config.maxChunksPerDoc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily construct (or reuse) the Cohere client, importing the optional
|
||||
* `cohere-ai` peer only when the reranker is first used so consumers that
|
||||
* never touch Cohere don't need it installed.
|
||||
*/
|
||||
private async getClient(): Promise<any> {
|
||||
if (this.clientInstance) return this.clientInstance;
|
||||
if (!this.clientPromise) {
|
||||
this.clientPromise = this.createClient();
|
||||
}
|
||||
this.clientInstance = await this.clientPromise;
|
||||
return this.clientInstance;
|
||||
}
|
||||
|
||||
private async createClient(): Promise<any> {
|
||||
const sdk = await loadPeer(
|
||||
"cohere-ai",
|
||||
"Cohere reranker",
|
||||
() => import("cohere-ai"),
|
||||
);
|
||||
return new sdk.CohereClient({ token: this.apiKey });
|
||||
}
|
||||
|
||||
async rerank(
|
||||
query: string,
|
||||
documents: string[],
|
||||
topK?: number,
|
||||
): Promise<RerankResult[]> {
|
||||
if (documents.length === 0) return [];
|
||||
|
||||
try {
|
||||
const client = await this.getClient();
|
||||
const response = await client.rerank({
|
||||
model: this.model,
|
||||
query,
|
||||
documents,
|
||||
topN: topK || this.topK || documents.length,
|
||||
returnDocuments: this.returnDocuments,
|
||||
maxChunksPerDoc: this.maxChunksPerDoc,
|
||||
});
|
||||
|
||||
return response.results.map((result: any) => ({
|
||||
index: result.index,
|
||||
rerankScore: result.relevanceScore,
|
||||
}));
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Cohere reranking failed, falling back to original order: ${e}`,
|
||||
);
|
||||
const scored = documents.map((_, index) => ({
|
||||
index,
|
||||
rerankScore: 0.0,
|
||||
}));
|
||||
const finalTopK = topK || this.topK;
|
||||
return finalTopK ? scored.slice(0, finalTopK) : scored;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
const mockModelFromPretrained = jest.fn();
|
||||
const mockTokenizerFromPretrained = jest.fn();
|
||||
|
||||
jest.mock("@huggingface/transformers", () => ({
|
||||
AutoModelForSequenceClassification: {
|
||||
from_pretrained: mockModelFromPretrained,
|
||||
},
|
||||
AutoTokenizer: { from_pretrained: mockTokenizerFromPretrained },
|
||||
}));
|
||||
|
||||
import { CrossEncoderReranker } from "./cross_encoder";
|
||||
|
||||
const sigmoid = (x: number) => 1 / (1 + Math.exp(-x));
|
||||
|
||||
/** Wire the mocked tokenizer + model so the model returns `logits` for a call. */
|
||||
function setupModel(logits: number[][]) {
|
||||
const tokenizer = jest.fn().mockReturnValue({ input_ids: [] });
|
||||
mockTokenizerFromPretrained.mockResolvedValue(tokenizer);
|
||||
const model = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ logits: { tolist: () => logits } });
|
||||
mockModelFromPretrained.mockResolvedValue(model);
|
||||
return { tokenizer, model };
|
||||
}
|
||||
|
||||
describe("CrossEncoderReranker", () => {
|
||||
beforeEach(() => {
|
||||
mockModelFromPretrained.mockReset();
|
||||
mockTokenizerFromPretrained.mockReset();
|
||||
});
|
||||
|
||||
it("scores each document and returns them sorted by relevance, sigmoid-normalized to [0,1]", async () => {
|
||||
setupModel([[0.0], [2.0], [-1.0]]);
|
||||
const reranker = new CrossEncoderReranker({}, "default-model");
|
||||
|
||||
const results = await reranker.rerank("q", ["a", "b", "c"]);
|
||||
|
||||
// sigmoid: b(2.0)=0.88 > a(0.0)=0.5 > c(-1.0)=0.27
|
||||
expect(results.map((r) => r.index)).toEqual([1, 0, 2]);
|
||||
expect(results[0].rerankScore).toBeCloseTo(sigmoid(2.0), 5);
|
||||
expect(results[1].rerankScore).toBeCloseTo(sigmoid(0.0), 5);
|
||||
expect(results[2].rerankScore).toBeCloseTo(sigmoid(-1.0), 5);
|
||||
});
|
||||
|
||||
it("pairs the query with each document via text_pair when tokenizing", async () => {
|
||||
const { tokenizer } = setupModel([[0.1], [0.2]]);
|
||||
const reranker = new CrossEncoderReranker(
|
||||
{ maxLength: 128 },
|
||||
"default-model",
|
||||
);
|
||||
|
||||
await reranker.rerank("what is x", ["doc one", "doc two"]);
|
||||
|
||||
expect(tokenizer).toHaveBeenCalledWith(
|
||||
["what is x", "what is x"],
|
||||
expect.objectContaining({
|
||||
text_pair: ["doc one", "doc two"],
|
||||
padding: true,
|
||||
truncation: true,
|
||||
max_length: 128,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("applies the topK limit", async () => {
|
||||
setupModel([[0.0], [2.0], [-1.0]]);
|
||||
const reranker = new CrossEncoderReranker({}, "default-model");
|
||||
|
||||
const results = await reranker.rerank("q", ["a", "b", "c"], 2);
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.map((r) => r.index)).toEqual([1, 0]);
|
||||
});
|
||||
|
||||
it("falls back to config.topK when the rerank() call omits one", async () => {
|
||||
setupModel([[0.0], [2.0], [-1.0]]);
|
||||
const reranker = new CrossEncoderReranker({ topK: 1 }, "default-model");
|
||||
|
||||
const results = await reranker.rerank("q", ["a", "b", "c"]);
|
||||
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results.map((r) => r.index)).toEqual([1]);
|
||||
});
|
||||
|
||||
it("returns [] without loading the model when there are no documents", async () => {
|
||||
const reranker = new CrossEncoderReranker({}, "default-model");
|
||||
|
||||
const results = await reranker.rerank("q", []);
|
||||
|
||||
expect(results).toEqual([]);
|
||||
expect(mockModelFromPretrained).not.toHaveBeenCalled();
|
||||
expect(mockTokenizerFromPretrained).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns raw logits as scores when normalize is false", async () => {
|
||||
setupModel([[2.0], [0.0]]);
|
||||
const reranker = new CrossEncoderReranker(
|
||||
{ normalize: false },
|
||||
"default-model",
|
||||
);
|
||||
|
||||
const results = await reranker.rerank("q", ["a", "b"]);
|
||||
|
||||
expect(results.map((r) => r.index)).toEqual([0, 1]);
|
||||
expect(results[0].rerankScore).toBe(2.0);
|
||||
expect(results[1].rerankScore).toBe(0.0);
|
||||
});
|
||||
|
||||
it("loads the model and tokenizer only once across multiple rerank calls", async () => {
|
||||
setupModel([[0.5]]);
|
||||
const reranker = new CrossEncoderReranker({}, "default-model");
|
||||
|
||||
await reranker.rerank("q", ["a"]);
|
||||
await reranker.rerank("q2", ["b"]);
|
||||
|
||||
expect(mockModelFromPretrained).toHaveBeenCalledTimes(1);
|
||||
expect(mockTokenizerFromPretrained).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("loads the default model, or the configured model when provided", async () => {
|
||||
setupModel([[0.5]]);
|
||||
|
||||
await new CrossEncoderReranker({}, "the-default").rerank("q", ["a"]);
|
||||
expect(mockModelFromPretrained).toHaveBeenCalledWith(
|
||||
"the-default",
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
mockModelFromPretrained.mockClear();
|
||||
setupModel([[0.5]]);
|
||||
await new CrossEncoderReranker(
|
||||
{ model: "custom/model" },
|
||||
"the-default",
|
||||
).rerank("q", ["a"]);
|
||||
expect(mockModelFromPretrained).toHaveBeenCalledWith(
|
||||
"custom/model",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("applies a default maxLength (as the huggingface provider passes 512) when config omits one", async () => {
|
||||
const { tokenizer } = setupModel([[0.5]]);
|
||||
const reranker = new CrossEncoderReranker({}, "default-model", 512);
|
||||
|
||||
await reranker.rerank("q", ["a"]);
|
||||
|
||||
expect(tokenizer).toHaveBeenCalledWith(
|
||||
["q"],
|
||||
expect.objectContaining({ max_length: 512 }),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to the original order with rerankScore 0.0 when the model fails to load", async () => {
|
||||
mockModelFromPretrained.mockResolvedValue(jest.fn());
|
||||
mockTokenizerFromPretrained.mockRejectedValue(
|
||||
new Error("model download failed"),
|
||||
);
|
||||
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const reranker = new CrossEncoderReranker({}, "default-model");
|
||||
|
||||
const results = await reranker.rerank("q", ["a", "b", "c"]);
|
||||
|
||||
expect(results).toEqual([
|
||||
{ index: 0, rerankScore: 0.0 },
|
||||
{ index: 1, rerankScore: 0.0 },
|
||||
{ index: 2, rerankScore: 0.0 },
|
||||
]);
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("falls back to the original order with rerankScore 0.0, sliced by topK, when scoring fails", async () => {
|
||||
const tokenizer = jest.fn().mockReturnValue({ input_ids: [] });
|
||||
mockTokenizerFromPretrained.mockResolvedValue(tokenizer);
|
||||
mockModelFromPretrained.mockResolvedValue(
|
||||
jest.fn().mockRejectedValue(new Error("forward pass failed")),
|
||||
);
|
||||
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const reranker = new CrossEncoderReranker({}, "default-model");
|
||||
|
||||
const results = await reranker.rerank("q", ["a", "b"], 1);
|
||||
|
||||
expect(results).toEqual([{ index: 0, rerankScore: 0.0 }]);
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -1,98 +0,0 @@
|
||||
import { RerankerConfig } from "../types";
|
||||
import { Reranker, RerankResult } from "./base";
|
||||
|
||||
const sigmoid = (x: number) => 1 / (1 + Math.exp(-x));
|
||||
|
||||
export class CrossEncoderReranker implements Reranker {
|
||||
private modelId: string;
|
||||
private device?: string;
|
||||
private maxLength?: number;
|
||||
private normalize: boolean;
|
||||
private topK?: number;
|
||||
// ponytail: batchSize/showProgressBar are accepted for config parity with the
|
||||
// Python SDK but are no-ops here — a memory search reranks a small candidate
|
||||
// set in a single forward pass. Chunk by batchSize if that ever grows.
|
||||
private loaded?: Promise<{ model: any; tokenizer: any }>;
|
||||
|
||||
constructor(
|
||||
config: RerankerConfig,
|
||||
defaultModel: string,
|
||||
defaultMaxLength?: number,
|
||||
) {
|
||||
this.modelId = config.model || defaultModel;
|
||||
this.device = config.device;
|
||||
this.maxLength = config.maxLength ?? defaultMaxLength;
|
||||
this.normalize = config.normalize ?? true;
|
||||
this.topK = config.topK;
|
||||
}
|
||||
|
||||
private load() {
|
||||
if (!this.loaded) {
|
||||
this.loaded = (async () => {
|
||||
// Lazy-load Transformers.js (and its onnxruntime native binding) only
|
||||
// when a rerank actually runs. A static import would pull onnxruntime
|
||||
// into every `new Memory()`, colliding on Linux with fastembed's
|
||||
// separate onnxruntime version — see the merge with the FastEmbed
|
||||
// embedder. Deferring it keeps memory construction free of ONNX.
|
||||
const { AutoModelForSequenceClassification, AutoTokenizer } =
|
||||
await import("@huggingface/transformers");
|
||||
const options: any = {};
|
||||
if (this.device) options.device = this.device;
|
||||
const model = await AutoModelForSequenceClassification.from_pretrained(
|
||||
this.modelId,
|
||||
options,
|
||||
);
|
||||
const tokenizer = await AutoTokenizer.from_pretrained(this.modelId);
|
||||
return { model, tokenizer };
|
||||
})();
|
||||
}
|
||||
return this.loaded;
|
||||
}
|
||||
|
||||
async rerank(
|
||||
query: string,
|
||||
documents: string[],
|
||||
topK?: number,
|
||||
): Promise<RerankResult[]> {
|
||||
if (documents.length === 0) return [];
|
||||
|
||||
try {
|
||||
const { model, tokenizer } = await this.load();
|
||||
|
||||
const inputs = tokenizer(
|
||||
documents.map(() => query),
|
||||
{
|
||||
text_pair: documents,
|
||||
padding: true,
|
||||
truncation: true,
|
||||
...(this.maxLength ? { max_length: this.maxLength } : {}),
|
||||
},
|
||||
);
|
||||
|
||||
const { logits } = await model(inputs);
|
||||
const rows: unknown[] = logits.tolist();
|
||||
|
||||
const scored = rows.map((row, index) => {
|
||||
const logit = Array.isArray(row) ? (row[0] as number) : (row as number);
|
||||
return {
|
||||
index,
|
||||
rerankScore: this.normalize ? sigmoid(logit) : logit,
|
||||
};
|
||||
});
|
||||
|
||||
scored.sort((a, b) => b.rerankScore - a.rerankScore);
|
||||
const finalTopK = topK || this.topK;
|
||||
return finalTopK ? scored.slice(0, finalTopK) : scored;
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`Cross-encoder reranking failed, falling back to original order: ${e}`,
|
||||
);
|
||||
const scored = documents.map((_, index) => ({
|
||||
index,
|
||||
rerankScore: 0.0,
|
||||
}));
|
||||
const finalTopK = topK || this.topK;
|
||||
return finalTopK ? scored.slice(0, finalTopK) : scored;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
import { LLM } from "../llms/base";
|
||||
import { LLMReranker } from "./llm";
|
||||
|
||||
// Duplicated rather than imported from ./llm.ts so this test catches drift.
|
||||
const EXPECTED_SYSTEM_PROMPT = `You are a relevance scoring assistant. Given a query and a document, score how relevant the document is to the query.
|
||||
|
||||
Score the relevance on a scale from 0.0 to 1.0, where:
|
||||
- 1.0 = Perfectly relevant and directly answers the query
|
||||
- 0.8-0.9 = Highly relevant with good information
|
||||
- 0.6-0.7 = Moderately relevant with some useful information
|
||||
- 0.4-0.5 = Slightly relevant with limited useful information
|
||||
- 0.0-0.3 = Not relevant or no useful information
|
||||
|
||||
Respond with only a single numerical score between 0.0 and 1.0. Do not include any explanation or additional text.`;
|
||||
|
||||
/**
|
||||
* Fake LLM that scores a document by looking up the document text inside the
|
||||
* prompt. Test document tokens must be distinct and must not be substrings of
|
||||
* the prompt boilerplate (e.g. avoid "a"/"b"), or the lookup resolves the
|
||||
* wrong doc.
|
||||
*/
|
||||
function makeLLM(scoreByDoc: Record<string, string>): LLM {
|
||||
return {
|
||||
generateResponse: async (
|
||||
messages: Array<{ role: string; content: string }>,
|
||||
) => {
|
||||
const prompt = messages.map((m) => m.content).join("\n");
|
||||
const doc = Object.keys(scoreByDoc).find((d) => prompt.includes(d));
|
||||
return doc ? scoreByDoc[doc] : "no number here";
|
||||
},
|
||||
generateChat: async () => ({ content: "", role: "assistant" }),
|
||||
};
|
||||
}
|
||||
|
||||
describe("LLMReranker", () => {
|
||||
it("sorts documents by descending relevance score", async () => {
|
||||
const llm = makeLLM({ cats: "0.2", dogs: "0.9", fish: "0.5" });
|
||||
const reranker = new LLMReranker({}, llm);
|
||||
|
||||
const results = await reranker.rerank("pets", ["cats", "dogs", "fish"]);
|
||||
|
||||
expect(results.map((r) => r.index)).toEqual([1, 2, 0]);
|
||||
expect(results.map((r) => r.rerankScore)).toEqual([0.9, 0.5, 0.2]);
|
||||
});
|
||||
|
||||
it("clamps scores to the [0, 1] range", async () => {
|
||||
const llm = makeLLM({ zebra: "1.5", walrus: "-0.3" });
|
||||
const reranker = new LLMReranker({}, llm);
|
||||
|
||||
const results = await reranker.rerank("q", ["zebra", "walrus"]);
|
||||
|
||||
const byIndex = new Map(results.map((r) => [r.index, r.rerankScore]));
|
||||
expect(byIndex.get(0)).toBe(1); // "zebra" 1.5 -> clamped to 1
|
||||
expect(byIndex.get(1)).toBe(0); // "walrus" -0.3 -> clamped to 0
|
||||
});
|
||||
|
||||
it("truncates results to topK", async () => {
|
||||
const llm = makeLLM({ alpha: "0.1", bravo: "0.8", charlie: "0.5" });
|
||||
const reranker = new LLMReranker({}, llm);
|
||||
|
||||
const results = await reranker.rerank(
|
||||
"q",
|
||||
["alpha", "bravo", "charlie"],
|
||||
2,
|
||||
);
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
expect(results.map((r) => r.index)).toEqual([1, 2]); // bravo(0.8), charlie(0.5)
|
||||
});
|
||||
|
||||
it("falls back to config.topK when the rerank() call omits one", async () => {
|
||||
const llm = makeLLM({ alpha: "0.1", bravo: "0.8", charlie: "0.5" });
|
||||
const reranker = new LLMReranker({ topK: 1 }, llm);
|
||||
|
||||
const results = await reranker.rerank("q", ["alpha", "bravo", "charlie"]);
|
||||
|
||||
expect(results).toHaveLength(1);
|
||||
expect(results[0].index).toBe(1); // bravo(0.8)
|
||||
});
|
||||
|
||||
it("falls back to a neutral score of 0.5 (not 0) when the LLM output has no number", async () => {
|
||||
const llm = makeLLM({ junk: "I cannot rate this" });
|
||||
const reranker = new LLMReranker({}, llm);
|
||||
|
||||
const results = await reranker.rerank("q", ["junk"]);
|
||||
|
||||
expect(results[0].rerankScore).toBe(0.5);
|
||||
});
|
||||
|
||||
it("prefers a decimal match over an integer match when extracting the score", async () => {
|
||||
const llm = makeLLM({ item: "The score is 0.73 out of 1" });
|
||||
const reranker = new LLMReranker({}, llm);
|
||||
|
||||
const results = await reranker.rerank("q", ["item"]);
|
||||
|
||||
expect(results[0].rerankScore).toBe(0.73);
|
||||
});
|
||||
|
||||
it("falls back to an integer match when no decimal is present", async () => {
|
||||
const llm = makeLLM({ item: "I'd say this is a solid 1" });
|
||||
const reranker = new LLMReranker({}, llm);
|
||||
|
||||
const results = await reranker.rerank("q", ["item"]);
|
||||
|
||||
expect(results[0].rerankScore).toBe(1);
|
||||
});
|
||||
|
||||
it("assigns a neutral 0.5 score (not 0.0) when a per-document LLM call fails, and still returns that document", async () => {
|
||||
const llm: LLM = {
|
||||
generateResponse: jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce("0.9") // scores "good"
|
||||
.mockRejectedValueOnce(new Error("rate limited")), // scores "bad"
|
||||
generateChat: async () => ({ content: "", role: "assistant" }),
|
||||
};
|
||||
const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
|
||||
const reranker = new LLMReranker({}, llm);
|
||||
|
||||
const results = await reranker.rerank("q", ["good", "bad"]);
|
||||
|
||||
expect(results).toHaveLength(2);
|
||||
const byIndex = new Map(results.map((r) => [r.index, r.rerankScore]));
|
||||
expect(byIndex.get(0)).toBe(0.9);
|
||||
expect(byIndex.get(1)).toBe(0.5);
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("sends the exact system prompt and a separate user message with the query and document", async () => {
|
||||
const generateResponse = jest.fn().mockResolvedValue("0.5");
|
||||
const llm: LLM = {
|
||||
generateResponse,
|
||||
generateChat: async () => ({ content: "", role: "assistant" }),
|
||||
};
|
||||
const reranker = new LLMReranker({}, llm);
|
||||
|
||||
await reranker.rerank("what is the capital?", [
|
||||
"Paris is the capital of France.",
|
||||
]);
|
||||
|
||||
expect(generateResponse).toHaveBeenCalledWith([
|
||||
{ role: "system", content: EXPECTED_SYSTEM_PROMPT },
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
"Query: what is the capital?\n\nDocument: Paris is the capital of France.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("truncates the query and document to 4000 characters before sending", async () => {
|
||||
const generateResponse = jest.fn().mockResolvedValue("0.5");
|
||||
const llm: LLM = {
|
||||
generateResponse,
|
||||
generateChat: async () => ({ content: "", role: "assistant" }),
|
||||
};
|
||||
const reranker = new LLMReranker({}, llm);
|
||||
const longQuery = "q".repeat(5000);
|
||||
const longDoc = "d".repeat(5000);
|
||||
|
||||
await reranker.rerank(longQuery, [longDoc]);
|
||||
|
||||
const userMessage = generateResponse.mock.calls[0][0][1];
|
||||
const sentQuery = userMessage.content.match(/^Query: (q+)/)[1];
|
||||
const sentDoc = userMessage.content.match(/Document: (d+)/)[1];
|
||||
expect(sentQuery).toHaveLength(4000);
|
||||
expect(sentDoc).toHaveLength(4000);
|
||||
});
|
||||
|
||||
it("throws when no LLM is provided", () => {
|
||||
expect(() => new LLMReranker({}, undefined as unknown as LLM)).toThrow();
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user