Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44dc74d78f | ||
|
|
d258b638ef | ||
|
|
bbbfcfea07 | ||
|
|
fbef369b91 | ||
|
|
a7ed68e697 | ||
|
|
8a92cf0306 | ||
|
|
0fbbb2f525 | ||
|
|
818c2981b7 | ||
|
|
1f66aadfa3 | ||
|
|
b91c745fbc | ||
|
|
af70668308 | ||
|
|
890473f891 | ||
|
|
d2ff83cf72 | ||
|
|
0e02effaf7 | ||
|
|
9269a0ad6e | ||
|
|
3d06006f36 | ||
|
|
6bb1d328ad |
@@ -50,6 +50,7 @@ Provide conversation messages for Mem0 to extract memories from. At least one en
|
|||||||
| `app_id` | string | No* | Associates the memory with an app. |
|
| `app_id` | string | No* | Associates the memory with an app. |
|
||||||
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
|
| `metadata` | object | Optional | Custom key/value metadata (e.g., `{"topic": "preferences"}`). |
|
||||||
| `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. |
|
| `infer` | boolean (default `true`) | Optional | Set to `false` to skip inference and store the provided text as-is. |
|
||||||
|
| `expiration_date` | string | Optional | Date in `YYYY-MM-DD` format. The memory is visible through this date and hidden by default after it passes. |
|
||||||
|
|
||||||
> \* At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required.
|
> \* At least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`) is required.
|
||||||
|
|
||||||
@@ -83,3 +84,11 @@ The request is queued for background processing. The response contains an `event
|
|||||||
<Info>
|
<Info>
|
||||||
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
|
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Memories with `expiration_date` remain stored after they expire. Search and get-all hide them by default; pass `show_expired: true` to include them.
|
||||||
|
</Info>
|
||||||
|
|
||||||
|
<Info>
|
||||||
|
Python uses `expiration_date`; TypeScript uses `expirationDate`.
|
||||||
|
</Info>
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ openapi: post /v3/memories/
|
|||||||
|
|
||||||
List memories scoped by filters with paginated results. Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400.
|
List memories scoped by filters with paginated results. Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400.
|
||||||
|
|
||||||
|
Expired memories are hidden by default. Pass `show_expired: true` to include memories whose `expiration_date` has passed.
|
||||||
|
|
||||||
|
Python uses `show_expired`; TypeScript uses `showExpired`.
|
||||||
|
|
||||||
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
|
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
|
||||||
|
|
||||||
- `in`: Matches any of the values specified
|
- `in`: Matches any of the values specified
|
||||||
@@ -32,6 +36,7 @@ memories = client.get_all(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
show_expired=False,
|
||||||
page=1,
|
page=1,
|
||||||
page_size=50
|
page_size=50
|
||||||
)
|
)
|
||||||
@@ -46,12 +51,14 @@ memories = client.get_all(
|
|||||||
{
|
{
|
||||||
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
|
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
|
||||||
"memory": "Alex is planning a trip to San Francisco from July 1st to July 10th",
|
"memory": "Alex is planning a trip to San Francisco from July 1st to July 10th",
|
||||||
|
"expiration_date": null,
|
||||||
"created_at": "2024-07-01T12:00:00Z",
|
"created_at": "2024-07-01T12:00:00Z",
|
||||||
"updated_at": "2024-07-01T12:00:00Z"
|
"updated_at": "2024-07-01T12:00:00Z"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a2b8c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
|
"id": "a2b8c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
|
||||||
"memory": "Alex prefers vegetarian restaurants",
|
"memory": "Alex prefers vegetarian restaurants",
|
||||||
|
"expiration_date": null,
|
||||||
"created_at": "2024-07-05T15:30:00Z",
|
"created_at": "2024-07-05T15:30:00Z",
|
||||||
"updated_at": "2024-07-05T15:30:00Z"
|
"updated_at": "2024-07-05T15:30:00Z"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ Relevance-ranked hybrid search across stored memories. V3 uses multi-signal retr
|
|||||||
|
|
||||||
Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400. At least one entity ID is required.
|
Entity IDs (`user_id`, `agent_id`, `app_id`, `run_id`) **must** be passed inside the `filters` object — top-level entity IDs are rejected with 400. At least one entity ID is required.
|
||||||
|
|
||||||
|
Expired memories are hidden by default. Pass `show_expired: true` to include memories whose `expiration_date` has passed.
|
||||||
|
|
||||||
|
Python uses `show_expired`; TypeScript uses `showExpired`.
|
||||||
|
|
||||||
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
|
The `filters` object supports complex logical operations (AND, OR, NOT) and comparison operators:
|
||||||
- `in`: Matches any of the values specified
|
- `in`: Matches any of the values specified
|
||||||
- `gte`: Greater than or equal to
|
- `gte`: Greater than or equal to
|
||||||
@@ -30,6 +34,7 @@ The `filters` object supports complex logical operations (AND, OR, NOT) and comp
|
|||||||
```python Platform API Example
|
```python Platform API Example
|
||||||
related_memories = client.search(
|
related_memories = client.search(
|
||||||
query="What are Alice's hobbies?",
|
query="What are Alice's hobbies?",
|
||||||
|
show_expired=False,
|
||||||
filters={
|
filters={
|
||||||
"OR": [
|
"OR": [
|
||||||
{
|
{
|
||||||
@@ -54,6 +59,7 @@ related_memories = client.search(
|
|||||||
"category": "hobbies"
|
"category": "hobbies"
|
||||||
},
|
},
|
||||||
"score": 0.82,
|
"score": 0.82,
|
||||||
|
"expiration_date": null,
|
||||||
"created_at": "2024-07-26T10:29:36.630547-07:00",
|
"created_at": "2024-07-26T10:29:36.630547-07:00",
|
||||||
"updated_at": null,
|
"updated_at": null,
|
||||||
"categories": ["hobbies"]
|
"categories": ["hobbies"]
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
---
|
---
|
||||||
title: 'Update Memory'
|
title: 'Update Memory'
|
||||||
description: "Update the content or metadata of a single memory by its unique ID using the PUT endpoint."
|
description: "Update the content, metadata, timestamp, or expiration date of a single memory by its unique ID using the PUT endpoint."
|
||||||
openapi: put /v1/memories/{memory_id}/
|
openapi: put /v1/memories/{memory_id}/
|
||||||
---
|
---
|
||||||
|
|
||||||
|
Use this endpoint to update mutable memory fields. To make a memory expire, set `expiration_date` to a `YYYY-MM-DD` date. To make it permanent again, send `expiration_date: null`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
client.update("mem_123", expiration_date="2030-01-31")
|
||||||
|
client.update("mem_123", expiration_date=None)
|
||||||
|
```
|
||||||
|
|
||||||
|
TypeScript uses `expirationDate`.
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: "Remove Organization Member"
|
||||||
|
description: "Remove a member from a Mem0 organization by revoking their access and clearing their assigned role."
|
||||||
|
openapi: "delete /api/v1/orgs/organizations/{org_id}/members/"
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: "Update Organization Member"
|
||||||
|
description: "Update the role assigned to an existing member of a Mem0 organization to change their permissions."
|
||||||
|
openapi: "put /api/v1/orgs/organizations/{org_id}/members/"
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: "Remove Project Member"
|
||||||
|
description: "Remove a member from a Mem0 project to revoke their access and unassign their project-level role."
|
||||||
|
openapi: "delete /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/"
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: "Update Project Member"
|
||||||
|
description: "Update the role assigned to an existing member of a Mem0 project to change their project-level permissions."
|
||||||
|
openapi: "put /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/"
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: "Update Project"
|
||||||
|
description: "Update settings and configuration for an existing Mem0 project, including name and project-level options."
|
||||||
|
openapi: "patch /api/v1/orgs/organizations/{org_id}/projects/{project_id}/"
|
||||||
|
---
|
||||||
@@ -113,7 +113,7 @@ Launched a unified Mem0 plugin across three major AI development environments
|
|||||||
|
|
||||||
Major expansion of the provider ecosystem:
|
Major expansion of the provider ecosystem:
|
||||||
|
|
||||||
- **Apache AGE** — New graph store support, bringing the total to 4 graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE). **Note:** All external graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE) were subsequently removed in v2.0.0 (2026-04-14). Graph memory is now built-in entity linking with no external graph store required; see the [v2.0.0 entry above](#mem0-sdk-v2-0-0-v3-0-0).
|
- **Apache AGE** — New graph store support, bringing the total to 4 graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE)
|
||||||
- **Turbopuffer** — New vector database provider for Python SDK
|
- **Turbopuffer** — New vector database provider for Python SDK
|
||||||
- **MiniMax** — New LLM provider with dedicated AWS Bedrock support
|
- **MiniMax** — New LLM provider with dedicated AWS Bedrock support
|
||||||
- **pgvector for Node.js** — PostgreSQL vector support added to the TypeScript OSS SDK
|
- **pgvector for Node.js** — PostgreSQL vector support added to the TypeScript OSS SDK
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ mode: "wide"
|
|||||||
|
|
||||||
**New Features:**
|
**New Features:**
|
||||||
- **Memory:** Warn at init time when hybrid/BM25 search silently degrades to semantic-only because the configured vector store does not implement `keyword_search`. Affected stores: Chroma, FAISS, Cassandra, LangChain, Neptune Analytics, S3 Vectors, Supabase, TurboPuffer, Valkey ([#5444](https://github.com/mem0ai/mem0/pull/5444))
|
- **Memory:** Warn at init time when hybrid/BM25 search silently degrades to semantic-only because the configured vector store does not implement `keyword_search`. Affected stores: Chroma, FAISS, Cassandra, LangChain, Neptune Analytics, S3 Vectors, Supabase, TurboPuffer, Valkey ([#5444](https://github.com/mem0ai/mem0/pull/5444))
|
||||||
- **Memory:** Add opt-in `explain=True` parameter to `Memory.search()` and `AsyncMemory.search()`. When enabled, each result includes a `score_details` dict with `semantic_score`, `bm25_score`, `entity_boost`, `raw_score`, `max_possible_score`, `final_score`, and `threshold` so callers can understand and tune retrieval ranking ([#5102](https://github.com/mem0ai/mem0/pull/5102))
|
- **Memory:** Add opt-in `explain=True` parameter to `Memory.search()` and `AsyncMemory.search()`. When enabled, each result includes a `score_breakdown` dict with `semantic`, `keyword` (normalized BM25), `entity_boost`, and `temporal_boost` signals so callers can understand and tune retrieval ranking ([#5102](https://github.com/mem0ai/mem0/pull/5102))
|
||||||
|
|
||||||
**Bug Fixes:**
|
**Bug Fixes:**
|
||||||
- **Vector Stores:** Normalize similarity scores to `[0, 1]` (higher = better) consistently across all backends. 11 adapters previously returned raw distance metrics (lower = better) — FAISS, Chroma, Milvus, Redis, Cassandra, PGVector, S3 Vectors, Supabase, Valkey, Azure MySQL, and Vertex AI Vector Search — causing incorrect ranking in multi-store setups ([#5391](https://github.com/mem0ai/mem0/pull/5391))
|
- **Vector Stores:** Normalize similarity scores to `[0, 1]` (higher = better) consistently across all backends. 11 adapters previously returned raw distance metrics (lower = better) — FAISS, Chroma, Milvus, Redis, Cassandra, PGVector, S3 Vectors, Supabase, Valkey, Azure MySQL, and Vertex AI Vector Search — causing incorrect ranking in multi-store setups ([#5391](https://github.com/mem0ai/mem0/pull/5391))
|
||||||
@@ -229,7 +229,7 @@ mode: "wide"
|
|||||||
**Improvements:**
|
**Improvements:**
|
||||||
- **Telemetry:** Sample OSS hot-path events at 10% via PostHog `before_send` hook to reduce event volume ([#4771](https://github.com/mem0ai/mem0/pull/4771))
|
- **Telemetry:** Sample OSS hot-path events at 10% via PostHog `before_send` hook to reduce event volume ([#4771](https://github.com/mem0ai/mem0/pull/4771))
|
||||||
|
|
||||||
See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-v3) and [Platform migration guide](https://docs.mem0.ai/migration/platform-v2-to-v3) for upgrade instructions.
|
See the [OSS v1 to v2 migration guide](https://docs.mem0.ai/migration/oss-v1-to-v2) and [Platform migration guide](https://docs.mem0.ai/migration/platform-v2-to-v3) for upgrade instructions.
|
||||||
|
|
||||||
</Update>
|
</Update>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
title: "FastEmbed"
|
||||||
|
description: "Configure FastEmbed as an embedding provider in Mem0 to generate embeddings locally using ONNX-based models without a GPU."
|
||||||
|
---
|
||||||
|
|
||||||
|
You can use FastEmbed to run embedding models locally in Mem0. FastEmbed is an ONNX-based embedding library that runs efficiently on CPU without requiring a GPU or an external API key.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install fastembed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
<CodeGroup>
|
||||||
|
```python Python
|
||||||
|
import os
|
||||||
|
from mem0 import Memory
|
||||||
|
|
||||||
|
os.environ["OPENAI_API_KEY"] = "your_api_key" # For LLM
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"embedder": {
|
||||||
|
"provider": "fastembed",
|
||||||
|
"config": {
|
||||||
|
"model": "thenlper/gte-large"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m = Memory.from_config(config)
|
||||||
|
messages = [
|
||||||
|
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||||
|
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
|
||||||
|
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
|
||||||
|
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
|
||||||
|
]
|
||||||
|
m.add(messages, user_id="john")
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
|
### Config
|
||||||
|
|
||||||
|
Here are the parameters available for configuring FastEmbed embedder:
|
||||||
|
|
||||||
|
| Parameter | Description | Default Value |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `model` | The name of the FastEmbed model to use | `thenlper/gte-large` |
|
||||||
|
| `embedding_dims` | Dimensions of the embedding model (auto-derived from the model if not set) | `None` |
|
||||||
@@ -4,9 +4,12 @@ description: "Use LiteLLM as an LLM provider in Mem0 to access over 100 language
|
|||||||
---
|
---
|
||||||
[Litellm](https://litellm.vercel.app/docs/) is compatible with over 100 large language models (LLMs), all using a standardized input/output format. You can explore the [available models](https://litellm.vercel.app/docs/providers) to use with Litellm. Ensure you set the `API_KEY` for the model you choose to use.
|
[Litellm](https://litellm.vercel.app/docs/) is compatible with over 100 large language models (LLMs), all using a standardized input/output format. You can explore the [available models](https://litellm.vercel.app/docs/providers) to use with Litellm. Ensure you set the `API_KEY` for the model you choose to use.
|
||||||
|
|
||||||
|
In the TypeScript SDK, run LiteLLM as a [proxy server](https://docs.litellm.ai/docs/simple_proxy) (an OpenAI-compatible endpoint) and point Mem0 at it via `LITELLM_API_BASE` (defaults to `http://localhost:4000`).
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```python
|
<CodeGroup>
|
||||||
|
```python Python
|
||||||
import os
|
import os
|
||||||
from mem0 import Memory
|
from mem0 import Memory
|
||||||
|
|
||||||
@@ -33,6 +36,33 @@ messages = [
|
|||||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```typescript TypeScript
|
||||||
|
import { Memory } from 'mem0ai/oss';
|
||||||
|
|
||||||
|
// Point Mem0 at your LiteLLM proxy. apiKey defaults to "sk-anything"
|
||||||
|
// (the proxy handles real auth); baseURL defaults to http://localhost:4000.
|
||||||
|
const config = {
|
||||||
|
llm: {
|
||||||
|
provider: 'litellm',
|
||||||
|
config: {
|
||||||
|
apiKey: process.env.LITELLM_API_KEY || 'sk-anything',
|
||||||
|
baseURL: process.env.LITELLM_API_BASE || 'http://localhost:4000',
|
||||||
|
model: 'gpt-5-mini',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const memory = new Memory(config);
|
||||||
|
const messages = [
|
||||||
|
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||||
|
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
|
||||||
|
{"role": "user", "content": "I’m not a big fan of thriller movies but I love sci-fi movies."},
|
||||||
|
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."}
|
||||||
|
];
|
||||||
|
await memory.add(messages, { userId: 'alice', metadata: { category: 'movies' } });
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
All available parameters for the `litellm` config are present in [Master List of All Params in Config](../config).
|
All available parameters for the `litellm` config are present in [Master List of All Params in Config](../config).
|
||||||
@@ -7,7 +7,8 @@ To use MiniMax LLM models, you have to set the `MINIMAX_API_KEY` environment var
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```python
|
<CodeGroup>
|
||||||
|
```python Python
|
||||||
import os
|
import os
|
||||||
from mem0 import Memory
|
from mem0 import Memory
|
||||||
|
|
||||||
@@ -36,9 +37,37 @@ messages = [
|
|||||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```typescript TypeScript
|
||||||
|
import { Memory } from 'mem0ai/oss';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
llm: {
|
||||||
|
provider: 'minimax',
|
||||||
|
config: {
|
||||||
|
apiKey: process.env.MINIMAX_API_KEY || '',
|
||||||
|
model: 'MiniMax-M2.7',
|
||||||
|
temperature: 0.2,
|
||||||
|
maxTokens: 2000,
|
||||||
|
topP: 1.0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const memory = new Memory(config);
|
||||||
|
const messages = [
|
||||||
|
{ role: "user", content: "I'm planning to watch a movie tonight. Any recommendations?" },
|
||||||
|
{ role: "assistant", content: "How about thriller movies? They can be quite engaging." },
|
||||||
|
{ role: "user", content: "I'm not a big fan of thriller movies but I love sci-fi movies." },
|
||||||
|
{ role: "assistant", content: "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future." },
|
||||||
|
];
|
||||||
|
await memory.add(messages, { userId: 'alice', metadata: { category: 'movies' } });
|
||||||
|
```
|
||||||
|
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
You can also configure the API base URL in the config:
|
You can also configure the API base URL in the config:
|
||||||
|
|
||||||
```python
|
<CodeGroup>
|
||||||
|
```python Python
|
||||||
config = {
|
config = {
|
||||||
"llm": {
|
"llm": {
|
||||||
"provider": "minimax",
|
"provider": "minimax",
|
||||||
@@ -51,6 +80,20 @@ config = {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```typescript TypeScript
|
||||||
|
const config = {
|
||||||
|
llm: {
|
||||||
|
provider: 'minimax',
|
||||||
|
config: {
|
||||||
|
model: 'MiniMax-M2.7',
|
||||||
|
baseURL: 'https://your-custom-endpoint.com',
|
||||||
|
apiKey: 'your-api-key', // alternatively to using the environment variable
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
```
|
||||||
|
</CodeGroup>
|
||||||
|
|
||||||
## Config
|
## Config
|
||||||
|
|
||||||
All available parameters for the `minimax` config are present in [Master List of All Params in Config](../config).
|
All available parameters for the `minimax` config are present in [Master List of All Params in Config](../config).
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
---
|
|
||||||
title: LLM as Reranker
|
|
||||||
description: "Use any LLM as a flexible reranker in Mem0 with custom prompts and domain-specific scoring logic."
|
|
||||||
---
|
|
||||||
|
|
||||||
<Warning>
|
|
||||||
**This page has been superseded.** Please see [LLM Reranker](/components/rerankers/models/llm_reranker) for the complete and up-to-date documentation on using LLMs for reranking.
|
|
||||||
</Warning>
|
|
||||||
|
|
||||||
LLM-based reranker provides maximum flexibility by using any Large Language Model to score document relevance. This approach allows for custom prompts and domain-specific scoring logic.
|
|
||||||
|
|
||||||
## Supported LLM Providers
|
|
||||||
|
|
||||||
Any LLM provider supported by Mem0 can be used for reranking:
|
|
||||||
|
|
||||||
- **OpenAI**: GPT-4, GPT-3.5-turbo, etc.
|
|
||||||
- **Anthropic**: Claude models
|
|
||||||
- **Together**: Open-source models
|
|
||||||
- **Groq**: Fast inference
|
|
||||||
- **Ollama**: Local models
|
|
||||||
- And more...
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
```python Python
|
|
||||||
from mem0 import Memory
|
|
||||||
|
|
||||||
config = {
|
|
||||||
"vector_store": {
|
|
||||||
"provider": "chroma",
|
|
||||||
"config": {
|
|
||||||
"collection_name": "my_memories",
|
|
||||||
"path": "./chroma_db"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"llm": {
|
|
||||||
"provider": "openai",
|
|
||||||
"config": {
|
|
||||||
"model": "gpt-4o-mini"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reranker": {
|
|
||||||
"provider": "llm",
|
|
||||||
"config": {
|
|
||||||
"model": "gpt-4o-mini",
|
|
||||||
"provider": "openai",
|
|
||||||
"api_key": "your-openai-api-key", # or set OPENAI_API_KEY
|
|
||||||
"top_k": 5,
|
|
||||||
"temperature": 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
memory = Memory.from_config(config)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Custom Scoring Prompt
|
|
||||||
|
|
||||||
You can provide a custom prompt for relevance scoring:
|
|
||||||
|
|
||||||
```python Python
|
|
||||||
custom_prompt = """You are a relevance scoring assistant. Rate how well this document answers the query.
|
|
||||||
|
|
||||||
Query: "{query}"
|
|
||||||
Document: "{document}"
|
|
||||||
|
|
||||||
Score from 0.0 to 1.0 where:
|
|
||||||
- 1.0: Perfect match, directly answers the query
|
|
||||||
- 0.8-0.9: Highly relevant, good match
|
|
||||||
- 0.6-0.7: Moderately relevant, partial match
|
|
||||||
- 0.4-0.5: Slightly relevant, limited useful information
|
|
||||||
- 0.0-0.3: Not relevant or no useful information
|
|
||||||
|
|
||||||
Provide only a single numerical score between 0.0 and 1.0."""
|
|
||||||
|
|
||||||
config["reranker"]["config"]["scoring_prompt"] = custom_prompt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage Example
|
|
||||||
|
|
||||||
```python Python
|
|
||||||
import os
|
|
||||||
from mem0 import Memory
|
|
||||||
|
|
||||||
# Set API key
|
|
||||||
os.environ["OPENAI_API_KEY"] = "your-api-key"
|
|
||||||
|
|
||||||
# Initialize memory with LLM reranker
|
|
||||||
config = {
|
|
||||||
"vector_store": {"provider": "chroma"},
|
|
||||||
"llm": {"provider": "openai", "config": {"model": "gpt-4o-mini"}},
|
|
||||||
"reranker": {
|
|
||||||
"provider": "llm",
|
|
||||||
"config": {
|
|
||||||
"model": "gpt-4o-mini",
|
|
||||||
"provider": "openai",
|
|
||||||
"temperature": 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
memory = Memory.from_config(config)
|
|
||||||
|
|
||||||
# Add memories
|
|
||||||
messages = [
|
|
||||||
{"role": "user", "content": "I'm learning Python programming"},
|
|
||||||
{"role": "user", "content": "I find object-oriented programming challenging"},
|
|
||||||
{"role": "user", "content": "I love hiking in national parks"}
|
|
||||||
]
|
|
||||||
|
|
||||||
memory.add(messages, user_id="david")
|
|
||||||
|
|
||||||
# Search with LLM reranking
|
|
||||||
results = memory.search("What programming topics is the user studying?", filters={"user_id": "david"})
|
|
||||||
|
|
||||||
for result in results['results']:
|
|
||||||
print(f"Memory: {result['memory']}")
|
|
||||||
print(f"Vector Score: {result['score']:.3f}")
|
|
||||||
print(f"Rerank Score: {result['rerank_score']:.3f}")
|
|
||||||
print()
|
|
||||||
```
|
|
||||||
|
|
||||||
```text Output
|
|
||||||
Memory: I'm learning Python programming
|
|
||||||
Vector Score: 0.856
|
|
||||||
Rerank Score: 0.920
|
|
||||||
|
|
||||||
Memory: I find object-oriented programming challenging
|
|
||||||
Vector Score: 0.782
|
|
||||||
Rerank Score: 0.850
|
|
||||||
```
|
|
||||||
|
|
||||||
## Domain-Specific Scoring
|
|
||||||
|
|
||||||
Create specialized scoring for your domain:
|
|
||||||
|
|
||||||
```python Python
|
|
||||||
medical_prompt = """You are a medical relevance expert. Score how relevant this medical record is to the clinical query.
|
|
||||||
|
|
||||||
Clinical Query: "{query}"
|
|
||||||
Medical Record: "{document}"
|
|
||||||
|
|
||||||
Consider:
|
|
||||||
- Clinical relevance and accuracy
|
|
||||||
- Patient safety implications
|
|
||||||
- Diagnostic value
|
|
||||||
- Treatment relevance
|
|
||||||
|
|
||||||
Score from 0.0 to 1.0. Provide only the numerical score."""
|
|
||||||
|
|
||||||
config = {
|
|
||||||
"reranker": {
|
|
||||||
"provider": "llm",
|
|
||||||
"config": {
|
|
||||||
"model": "gpt-4o-mini",
|
|
||||||
"provider": "openai",
|
|
||||||
"scoring_prompt": medical_prompt,
|
|
||||||
"temperature": 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Multiple LLM Providers
|
|
||||||
|
|
||||||
Use different LLM providers for reranking:
|
|
||||||
|
|
||||||
```python Python
|
|
||||||
# Using Anthropic Claude
|
|
||||||
anthropic_config = {
|
|
||||||
"reranker": {
|
|
||||||
"provider": "llm",
|
|
||||||
"config": {
|
|
||||||
"model": "claude-3-haiku-20240307",
|
|
||||||
"provider": "anthropic",
|
|
||||||
"temperature": 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Using local Ollama model
|
|
||||||
ollama_config = {
|
|
||||||
"reranker": {
|
|
||||||
"provider": "llm",
|
|
||||||
"config": {
|
|
||||||
"model": "llama2:7b",
|
|
||||||
"provider": "ollama",
|
|
||||||
"temperature": 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration Parameters
|
|
||||||
|
|
||||||
| Parameter | Description | Type | Default |
|
|
||||||
|-----------|-------------|------|---------|
|
|
||||||
| `model` | LLM model to use for scoring | `str` | `"gpt-4o-mini"` |
|
|
||||||
| `provider` | LLM provider name | `str` | `"openai"` |
|
|
||||||
| `api_key` | API key for the LLM provider | `str` | `None` |
|
|
||||||
| `top_k` | Maximum documents to return | `int` | `None` |
|
|
||||||
| `temperature` | Temperature for LLM generation | `float` | `0.0` |
|
|
||||||
| `max_tokens` | Maximum tokens for LLM response | `int` | `100` |
|
|
||||||
| `scoring_prompt` | Custom prompt template | `str` | Default prompt |
|
|
||||||
|
|
||||||
## Advantages
|
|
||||||
|
|
||||||
- **Maximum Flexibility**: Custom prompts for any use case
|
|
||||||
- **Domain Expertise**: Leverage LLM knowledge for specialized domains
|
|
||||||
- **Interpretability**: Understand scoring through prompt engineering
|
|
||||||
- **Multi-criteria**: Score based on multiple relevance factors
|
|
||||||
|
|
||||||
## Considerations
|
|
||||||
|
|
||||||
- **Latency**: Higher latency than specialized rerankers
|
|
||||||
- **Cost**: LLM API costs per reranking operation
|
|
||||||
- **Consistency**: May have slight variations in scoring
|
|
||||||
- **Prompt Engineering**: Requires careful prompt design
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Temperature**: Use 0.0 for consistent scoring
|
|
||||||
2. **Prompt Design**: Be specific about scoring criteria
|
|
||||||
3. **Token Efficiency**: Keep prompts concise to reduce costs
|
|
||||||
4. **Caching**: Cache results for repeated queries when possible
|
|
||||||
5. **Fallback**: Handle API errors gracefully
|
|
||||||
@@ -21,49 +21,47 @@ from mem0 import Memory
|
|||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"vector_store": {
|
"vector_store": {
|
||||||
"provider": "pgvector",
|
"provider": "pgvector",
|
||||||
"config": {
|
"config": {
|
||||||
"connection_string": os.environ["DATABASE_URL"],
|
"connection_string": os.environ["DATABASE_URL"],
|
||||||
"collection_name": "memories",
|
"collection_name": "memories",
|
||||||
"embedding_model_dims": 1536,
|
"embedding_model_dims": 1536,
|
||||||
"hnsw": True,
|
"hnsw": True,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
m = Memory.from_config(config)
|
m = Memory.from_config(config)
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
{"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": "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."},
|
{"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"})
|
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||||
|
|
||||||
results = m.search(
|
results = m.search(
|
||||||
"What movies should I recommend?",
|
"What movies should I recommend?",
|
||||||
filters={"user_id": "alice"},
|
filters={"user_id": "alice"},
|
||||||
)
|
)
|
||||||
|
|
||||||
print(results)
|
print(results)
|
||||||
```
|
|
||||||
|
````
|
||||||
|
|
||||||
```typescript TypeScript
|
```typescript TypeScript
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import { Memory } from "mem0ai/oss";
|
import { Memory } from "mem0ai/oss";
|
||||||
|
|
||||||
const databaseUrl = new URL(process.env.DATABASE_URL!);
|
|
||||||
|
|
||||||
const m = new Memory({
|
const m = new Memory({
|
||||||
vectorStore: {
|
vectorStore: {
|
||||||
provider: "pgvector",
|
provider: "pgvector",
|
||||||
config: {
|
config: {
|
||||||
user: decodeURIComponent(databaseUrl.username),
|
connectionString: process.env.DATABASE_URL!,
|
||||||
password: decodeURIComponent(databaseUrl.password),
|
ssl: {
|
||||||
host: databaseUrl.hostname,
|
rejectUnauthorized: false,
|
||||||
port: Number(databaseUrl.port || 5432),
|
},
|
||||||
dbname: databaseUrl.pathname.slice(1) || "neondb",
|
|
||||||
collectionName: "memories",
|
collectionName: "memories",
|
||||||
dimension: 1536,
|
dimension: 1536,
|
||||||
embeddingModelDims: 1536,
|
embeddingModelDims: 1536,
|
||||||
@@ -89,7 +87,8 @@ const results = await m.search("What movies should I recommend?", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log(results);
|
console.log(results);
|
||||||
```
|
````
|
||||||
|
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
## SQL Migration
|
## SQL Migration
|
||||||
@@ -116,20 +115,19 @@ DATABASE_URL=postgresql://user:password@ep-example.us-east-2.aws.neon.tech/neond
|
|||||||
| `sslmode` | PostgreSQL SSL mode. Use `require` for Neon. | Driver default |
|
| `sslmode` | PostgreSQL SSL mode. Use `require` for Neon. | Driver default |
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab title="TypeScript">
|
<Tab title="TypeScript">
|
||||||
The current Mem0 TypeScript `pgvector` adapter takes individual Postgres fields,
|
Use the Neon `DATABASE_URL` directly with `connectionString`. Set `ssl` if your runtime needs an explicit TLS config object.
|
||||||
so parse `DATABASE_URL` before creating `Memory`.
|
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
| -------------------- | ---------------------------------------------- | -------------- |
|
||||||
|
| `connectionString` | Neon Postgres connection string. | Required |
|
||||||
|
| `ssl` | Optional TLS settings passed directly to `pg`. | Driver default |
|
||||||
|
| `collectionName` | Name for the vector collection. | `memories` |
|
||||||
|
| `dimension` | Vector dimension for Mem0 config. | Auto-detected |
|
||||||
|
| `embeddingModelDims` | Embedding model dimensions for table creation. | Required |
|
||||||
|
| `hnsw` | Enables HNSW indexing. | `false` |
|
||||||
|
|
||||||
|
**TLS note:** `ssl: true` is sufficient for most Neon connections since Neon uses valid certificates. Use `ssl: { rejectUnauthorized: false }` only when connecting through Neon's connection pooler on certain edge runtimes (e.g. Cloudflare Workers) that require it, or when your environment does not trust the Neon CA chain.
|
||||||
|
|
||||||
| Parameter | Description | Default |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `user` | Database user. | Required |
|
|
||||||
| `password` | Database password. | Required |
|
|
||||||
| `host` | Database host. | Required |
|
|
||||||
| `port` | Database port. | `5432` |
|
|
||||||
| `dbname` | Database name. | `vector_store` |
|
|
||||||
| `collectionName` | Name for the vector collection. | `memories` |
|
|
||||||
| `dimension` | Vector dimension for Mem0 config. | Auto-detected |
|
|
||||||
| `embeddingModelDims` | Embedding model dimensions for table creation. | Required |
|
|
||||||
| `hnsw` | Enables HNSW indexing. | `false` |
|
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
title: "pgvector"
|
title: "pgvector"
|
||||||
description: "Use pgvector as a vector store in Mem0 for PostgreSQL-based vector similarity search with open-source simplicity."
|
description: "Use pgvector as a vector store in Mem0 for PostgreSQL-based vector similarity search with open-source simplicity."
|
||||||
---
|
---
|
||||||
|
|
||||||
[pgvector](https://github.com/pgvector/pgvector) is an open-source vector similarity search extension for Postgres. After connecting to Postgres, run `CREATE EXTENSION IF NOT EXISTS vector;` to create the vector extension.
|
[pgvector](https://github.com/pgvector/pgvector) is an open-source vector similarity search extension for Postgres. After connecting to Postgres, run `CREATE EXTENSION IF NOT EXISTS vector;` to create the vector extension.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
@@ -14,41 +15,38 @@ from mem0 import Memory
|
|||||||
os.environ["OPENAI_API_KEY"] = "sk-xx"
|
os.environ["OPENAI_API_KEY"] = "sk-xx"
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"vector_store": {
|
"vector_store": {
|
||||||
"provider": "pgvector",
|
"provider": "pgvector",
|
||||||
"config": {
|
"config": {
|
||||||
"user": "test",
|
"user": "test",
|
||||||
"password": "123",
|
"password": "123",
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"port": "5432",
|
"port": "5432",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m = Memory.from_config(config)
|
m = Memory.from_config(config)
|
||||||
messages = [
|
messages = [
|
||||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
{"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": "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."}
|
{"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"})
|
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||||
```
|
|
||||||
|
````
|
||||||
|
|
||||||
```typescript TypeScript
|
```typescript TypeScript
|
||||||
import { Memory } from 'mem0ai/oss';
|
import { Memory } from "mem0ai/oss";
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
vectorStore: {
|
vectorStore: {
|
||||||
provider: 'pgvector',
|
provider: "pgvector",
|
||||||
config: {
|
config: {
|
||||||
collectionName: 'memories',
|
collectionName: "memories",
|
||||||
embeddingModelDims: 1536,
|
embeddingModelDims: 1536,
|
||||||
user: 'test',
|
connectionString: "postgresql://test:123@localhost:5432/vector_store",
|
||||||
password: '123',
|
|
||||||
host: '127.0.0.1',
|
|
||||||
port: 5432,
|
|
||||||
dbname: 'vector_store', // Optional; TypeScript OSS defaults to `vector_store` when omitted
|
|
||||||
diskann: false, // Optional, requires pgvectorscale extension
|
diskann: false, // Optional, requires pgvectorscale extension
|
||||||
hnsw: false, // Optional, for HNSW indexing
|
hnsw: false, // Optional, for HNSW indexing
|
||||||
},
|
},
|
||||||
@@ -57,37 +55,44 @@ const config = {
|
|||||||
|
|
||||||
const memory = new Memory(config);
|
const memory = new Memory(config);
|
||||||
const messages = [
|
const messages = [
|
||||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
{ 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: "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."}
|
{ 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" } });
|
await memory.add(messages, { userId: "alice", metadata: { category: "movies" } });
|
||||||
```
|
````
|
||||||
|
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
### Config
|
### Config
|
||||||
|
|
||||||
Here are the parameters available for configuring pgvector:
|
Here are the parameters available for configuring pgvector:
|
||||||
|
|
||||||
| Parameter | Description | Default Value |
|
| Parameter | SDK | Description | Default Value |
|
||||||
| --- | --- | --- |
|
| -------------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
||||||
| `dbname` | The name of the database | `postgres` |
|
| `connectionString` | TypeScript OSS | PostgreSQL connection string for direct connections. When set, Mem0 connects to the target database directly and skips the bootstrap `postgres` database flow. | `None` |
|
||||||
| `collection_name` | The name of the collection | `mem0` |
|
| `ssl` | TypeScript OSS | SSL option passed directly to `pg`, either `true` or an SSL config object, for both `connectionString` and split-field connections. | `None` |
|
||||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` |
|
| `dbname` | TypeScript OSS | Split-field database name. This is only used when `connectionString` is absent. | `vector_store` |
|
||||||
| `user` | User name to connect to the database | `None` |
|
| `collectionName` | TypeScript OSS | Collection name. | `memories` |
|
||||||
| `password` | Password to connect to the database | `None` |
|
| `embeddingModelDims` | TypeScript OSS | Dimensions of the embedding model. | Required |
|
||||||
| `host` | The host where the Postgres server is running | `None` |
|
| `user` | TypeScript OSS + Python | Database user for split-field connections. | `None` |
|
||||||
| `port` | The port where the Postgres server is running | `None` |
|
| `password` | TypeScript OSS + Python | Database password for split-field connections. | `None` |
|
||||||
| `diskann` | Whether to use diskann for vector similarity search (requires pgvectorscale) | `True` |
|
| `host` | TypeScript OSS + Python | Database host for split-field connections. | `None` |
|
||||||
| `hnsw` | Whether to use hnsw for vector similarity search | `False` |
|
| `port` | TypeScript OSS + Python | Database port for split-field connections. | `None` |
|
||||||
| `sslmode` | SSL mode for PostgreSQL connection (e.g., 'require', 'prefer', 'disable') | `None` |
|
| `diskann` | TypeScript OSS + Python | Whether to use DiskANN for vector similarity search, requires pgvectorscale. | `False` |
|
||||||
| `connection_string` | PostgreSQL connection string (overrides individual connection parameters) | `None` |
|
| `hnsw` | TypeScript OSS + Python | Whether to use HNSW for vector similarity search. | TypeScript OSS: `False`, Python: `True` |
|
||||||
| `connection_pool` | psycopg2 connection pool object (overrides connection string and individual parameters) | `None` |
|
| `connection_string` | Python only | PostgreSQL connection string, overrides individual connection parameters. | `None` |
|
||||||
|
| `sslmode` | Python only | SSL mode for PostgreSQL connections, such as `require`, `prefer`, or `disable`. | `None` |
|
||||||
|
| `connection_pool` | Python only | psycopg connection pool object, overrides connection string and individual connection parameters. | `None` |
|
||||||
|
|
||||||
**Note (TypeScript OSS):** If you omit `dbname`, the TypeScript client uses the database name `vector_store`. Python defaults to `postgres` for `dbname`, as in the table above.
|
**TypeScript OSS:** Use `connectionString` plus optional `ssl` for managed Postgres setups. If you omit `connectionString`, Mem0 falls back to split fields and uses `dbname`, `user`, `password`, `host`, `port`, and optional `ssl`.
|
||||||
|
|
||||||
|
**Python:** The Python SDK uses snake_case keys such as `connection_string`, `sslmode`, `collection_name`, and `embedding_model_dims`.
|
||||||
|
|
||||||
|
**Python connection priority**:
|
||||||
|
|
||||||
**Note**: The connection parameters have the following priority:
|
|
||||||
1. `connection_pool` (highest priority)
|
1. `connection_pool` (highest priority)
|
||||||
2. `connection_string`
|
2. `connection_string`
|
||||||
3. Individual connection parameters (`user`, `password`, `host`, `port`, `sslmode`)
|
3. Individual connection parameters (`user`, `password`, `host`, `port`, `sslmode`)
|
||||||
+11
-1
@@ -273,7 +273,8 @@
|
|||||||
"components/embedders/models/lmstudio",
|
"components/embedders/models/lmstudio",
|
||||||
"components/embedders/models/together",
|
"components/embedders/models/together",
|
||||||
"components/embedders/models/langchain",
|
"components/embedders/models/langchain",
|
||||||
"components/embedders/models/aws_bedrock"
|
"components/embedders/models/aws_bedrock",
|
||||||
|
"components/embedders/models/fastembed"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -533,6 +534,8 @@
|
|||||||
"api-reference/organization/get-org",
|
"api-reference/organization/get-org",
|
||||||
"api-reference/organization/get-org-members",
|
"api-reference/organization/get-org-members",
|
||||||
"api-reference/organization/add-org-member",
|
"api-reference/organization/add-org-member",
|
||||||
|
"api-reference/organization/update-org-member",
|
||||||
|
"api-reference/organization/remove-org-member",
|
||||||
"api-reference/organization/delete-org"
|
"api-reference/organization/delete-org"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -545,6 +548,9 @@
|
|||||||
"api-reference/project/get-project",
|
"api-reference/project/get-project",
|
||||||
"api-reference/project/get-project-members",
|
"api-reference/project/get-project-members",
|
||||||
"api-reference/project/add-project-member",
|
"api-reference/project/add-project-member",
|
||||||
|
"api-reference/project/update-project",
|
||||||
|
"api-reference/project/update-project-member",
|
||||||
|
"api-reference/project/remove-project-member",
|
||||||
"api-reference/project/delete-project"
|
"api-reference/project/delete-project"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -624,6 +630,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"redirects": [
|
"redirects": [
|
||||||
|
{
|
||||||
|
"source": "/components/rerankers/models/llm",
|
||||||
|
"destination": "/components/rerankers/models/llm_reranker"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"source": "/migration/breaking-changes",
|
"source": "/migration/breaking-changes",
|
||||||
"destination": "/"
|
"destination": "/"
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ client = MemoryClient()
|
|||||||
# Define the agent
|
# Define the agent
|
||||||
agent = Agent(
|
agent = Agent(
|
||||||
name="Personal Agent",
|
name="Personal Agent",
|
||||||
model=OpenAIChat(id="gpt-4"),
|
model=OpenAIChat(id="gpt-5-mini"),
|
||||||
description="You are a helpful personal agent that helps me with day to day activities."
|
description="You are a helpful personal agent that helps me with day to day activities."
|
||||||
"You can process both text and images.",
|
"You can process both text and images.",
|
||||||
markdown=True
|
markdown=True
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY')
|
|||||||
memory_client = MemoryClient()
|
memory_client = MemoryClient()
|
||||||
agent = ConversableAgent(
|
agent = ConversableAgent(
|
||||||
"chatbot",
|
"chatbot",
|
||||||
llm_config={"config_list": [{"model": "gpt-4", "api_key": OPENAI_API_KEY}]},
|
llm_config={"config_list": [{"model": "gpt-5-mini", "api_key": OPENAI_API_KEY}]},
|
||||||
code_execution_config=False,
|
code_execution_config=False,
|
||||||
human_input_mode="NEVER",
|
human_input_mode="NEVER",
|
||||||
)
|
)
|
||||||
@@ -99,7 +99,7 @@ For more complex scenarios, you can create multiple agents:
|
|||||||
manager = ConversableAgent(
|
manager = ConversableAgent(
|
||||||
"manager",
|
"manager",
|
||||||
system_message="You are a manager who helps in resolving complex customer issues.",
|
system_message="You are a manager who helps in resolving complex customer issues.",
|
||||||
llm_config={"config_list": [{"model": "gpt-4", "api_key": OPENAI_API_KEY}]},
|
llm_config={"config_list": [{"model": "gpt-5-mini", "api_key": OPENAI_API_KEY}]},
|
||||||
human_input_mode="NEVER"
|
human_input_mode="NEVER"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -138,9 +138,10 @@ When installed via the plugin marketplace, Mem0 hooks into Claude Code's lifecyc
|
|||||||
|
|
||||||
| Hook | Event | What it does |
|
| Hook | Event | What it does |
|
||||||
|------|-------|-------------|
|
|------|-------|-------------|
|
||||||
|
| **Setup** | `Setup` | Installs the mem0 SDK and dependencies (runs on init and maintenance) |
|
||||||
| **Session start** | `SessionStart` | Loads prior memories and displays status banner |
|
| **Session start** | `SessionStart` | Loads prior memories and displays status banner |
|
||||||
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message; skips short prompts |
|
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message; skips short prompts |
|
||||||
| **Pre-tool** | `PreToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tool calls |
|
| **Pre-tool (3 handlers)** | `PreToolUse` | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context |
|
||||||
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
|
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
|
||||||
| **Stop** | `Stop` | Stores a session summary when the session ends |
|
| **Stop** | `Stop` | Stores a session summary when the session ends |
|
||||||
| **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted |
|
| **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted |
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ When installed via the plugin marketplace, Mem0 hooks into Codex's lifecycle to
|
|||||||
|------|-------|-------------|
|
|------|-------|-------------|
|
||||||
| **Session start** | `SessionStart` | Loads prior memories and displays status banner |
|
| **Session start** | `SessionStart` | Loads prior memories and displays status banner |
|
||||||
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message |
|
| **User prompt** | `UserPromptSubmit` | Searches relevant memories before each message |
|
||||||
| **Pre-tool** | `PreToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tool calls |
|
| **Pre-tool (3 handlers)** | `PreToolUse` | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context |
|
||||||
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
|
| **Post-tool** | `PostToolUse` | Tracks stats, scans bash errors for related memories |
|
||||||
| **Stop** | `Stop` | Stores a session summary when the session ends |
|
| **Stop** | `Stop` | Stores a session summary when the session ends |
|
||||||
| **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted |
|
| **Pre-compact** | `PreCompact` | Stores a summary before the context is compacted |
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ When installed via the Cursor Marketplace, Mem0 hooks into Cursor's lifecycle:
|
|||||||
|------|-------|-------------|
|
|------|-------|-------------|
|
||||||
| **Session start** | `sessionStart` | Loads prior memories and displays status banner |
|
| **Session start** | `sessionStart` | Loads prior memories and displays status banner |
|
||||||
| **User prompt** | `beforeSubmitPrompt` | Searches relevant memories before each message; skips short prompts |
|
| **User prompt** | `beforeSubmitPrompt` | Searches relevant memories before each message; skips short prompts |
|
||||||
| **Pre-tool (2 handlers)** | `preToolUse` | Blocks MEMORY.md writes, enforces `user_id`/`app_id` on mem0 tool calls |
|
| **Pre-tool (3 handlers)** | `preToolUse` | Blocks MEMORY.md writes; enforces `user_id`/`app_id` on mem0 tool calls; scans files being read for relevant memory context |
|
||||||
| **Post-tool (2 handlers)** | `postToolUse` | Tracks stats, scans bash errors for related memories |
|
| **Post-tool (2 handlers)** | `postToolUse` | Tracks stats, scans bash errors for related memories |
|
||||||
| **Stop** | `stop` | Stores a session summary when the session ends |
|
| **Stop** | `stop` | Stores a session summary when the session ends |
|
||||||
| **Pre-compact** | `preCompact` | Stores a summary before the context is compacted |
|
| **Pre-compact** | `preCompact` | Stores a summary before the context is compacted |
|
||||||
|
|||||||
@@ -98,20 +98,9 @@ add_result = add_tool.invoke(add_input)
|
|||||||
|
|
||||||
```json Output
|
```json Output
|
||||||
{
|
{
|
||||||
"results": [
|
"message": "Memory processing has been queued for background execution",
|
||||||
{
|
"status": "PENDING",
|
||||||
"memory": "Name is Alex",
|
"event_id": "3a1b2c3d-4e5f-6789-abcd-ef0123456789"
|
||||||
"event": "ADD"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"memory": "Is a vegetarian",
|
|
||||||
"event": "ADD"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"memory": "Is allergic to nuts",
|
|
||||||
"event": "ADD"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
@@ -173,23 +162,25 @@ result = search_tool.invoke(search_input)
|
|||||||
```
|
```
|
||||||
|
|
||||||
```json Output
|
```json Output
|
||||||
[
|
{
|
||||||
{
|
"results": [
|
||||||
"id": "1a75e827-7eca-45ea-8c5c-cfd43299f061",
|
{
|
||||||
"memory": "Name is Alex",
|
"id": "1a75e827-7eca-45ea-8c5c-cfd43299f061",
|
||||||
"user_id": "alex",
|
"memory": "Name is Alex",
|
||||||
"hash": "d0fccc8fa47f7a149ee95750c37bb0ca",
|
"user_id": "alex",
|
||||||
"metadata": {
|
"hash": "d0fccc8fa47f7a149ee95750c37bb0ca",
|
||||||
"food": "vegan"
|
"metadata": {
|
||||||
},
|
"food": "vegan"
|
||||||
"categories": [
|
},
|
||||||
"personal_details"
|
"categories": [
|
||||||
],
|
"personal_details"
|
||||||
"created_at": "2024-11-27T16:53:43.276872-08:00",
|
],
|
||||||
"updated_at": "2024-11-27T16:53:43.276885-08:00",
|
"created_at": "2024-11-27T16:53:43.276872-08:00",
|
||||||
"score": 0.3810526501504994
|
"updated_at": "2024-11-27T16:53:43.276885-08:00",
|
||||||
}
|
"score": 0.3810526501504994
|
||||||
]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ load_dotenv()
|
|||||||
# MEM0_API_KEY = 'your-mem0-key' # Replace with your actual Mem0 API key
|
# MEM0_API_KEY = 'your-mem0-key' # Replace with your actual Mem0 API key
|
||||||
|
|
||||||
# Initialize LangChain and Mem0
|
# Initialize LangChain and Mem0
|
||||||
llm = ChatOpenAI(model="gpt-4")
|
llm = ChatOpenAI(model="gpt-5-mini")
|
||||||
mem0 = MemoryClient()
|
mem0 = MemoryClient()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ async def websocket_endpoint(websocket: WebSocket):
|
|||||||
# LLM for response generation
|
# LLM for response generation
|
||||||
llm = OpenAILLMService(
|
llm = OpenAILLMService(
|
||||||
api_key=os.getenv("OPENAI_API_KEY"),
|
api_key=os.getenv("OPENAI_API_KEY"),
|
||||||
model="gpt-3.5-turbo",
|
model="gpt-5-mini",
|
||||||
system_prompt="You are a helpful assistant that remembers past conversations."
|
system_prompt="You are a helpful assistant that remembers past conversations."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ Install the SDK provider and AI SDK:
|
|||||||
npm install @mem0/vercel-ai-provider ai@^6
|
npm install @mem0/vercel-ai-provider ai@^6
|
||||||
```
|
```
|
||||||
|
|
||||||
### Peer Dependencies
|
### Dependencies
|
||||||
|
|
||||||
`@mem0/vercel-ai-provider` v3.0.0 requires:
|
`@mem0/vercel-ai-provider` bundles `ai`, all `@ai-sdk/*` provider packages, and `@ai-sdk/provider` as regular dependencies — you do **not** need to install them separately. The install command above (`npm install @mem0/vercel-ai-provider ai@^6`) is sufficient.
|
||||||
- `ai` v6+ (`^6.0.199`)
|
|
||||||
- `@ai-sdk/provider` v3+ (`^3.0.10`)
|
The only true peer dependency is `zod` (optional):
|
||||||
- Provider packages at v3+: `@ai-sdk/openai@^3`, `@ai-sdk/anthropic@^3`, `@ai-sdk/google@^3`, `@ai-sdk/groq@^3`, `@ai-sdk/cohere@^3`
|
- `zod` v3+ (`^3.0.0`) — required only if you use Zod schemas in tool definitions
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@@ -305,6 +305,8 @@ These options can be passed per-request when creating a model instance:
|
|||||||
| `rerank` | `boolean` | Enable reranking of results |
|
| `rerank` | `boolean` | Enable reranking of results |
|
||||||
| `page` | `number` | Page number for pagination |
|
| `page` | `number` | Page number for pagination |
|
||||||
| `page_size` | `number` | Results per page |
|
| `page_size` | `number` | Results per page |
|
||||||
|
| `mem0ApiKey` | `string` | Mem0 API key; overrides the `MEM0_API_KEY` env var |
|
||||||
|
| `host` | `string` | Custom Mem0 API base URL for self-hosted deployments |
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
@@ -312,6 +314,7 @@ These options can be passed per-request when creating a model instance:
|
|||||||
- `retrieveMemories()`: Retrieves memory context for prompts as a formatted system prompt string.
|
- `retrieveMemories()`: Retrieves memory context for prompts as a formatted system prompt string.
|
||||||
- `getMemories()`: Get memories from your profile in array format.
|
- `getMemories()`: Get memories from your profile in array format.
|
||||||
- `addMemories()`: Adds user memories to enhance contextual responses.
|
- `addMemories()`: Adds user memories to enhance contextual responses.
|
||||||
|
- `searchMemories()`: Searches memories and returns the raw results array (semantic search rather than the full retrieval pipeline).
|
||||||
|
|
||||||
## Migrating from v2.x
|
## Migrating from v2.x
|
||||||
|
|
||||||
|
|||||||
+7
-3
@@ -367,6 +367,8 @@ All API Reference docs describe Mem0 Platform REST endpoints (requires API key).
|
|||||||
- [Get Organization](https://docs.mem0.ai/api-reference/organization/get-org) [Platform]: Use when fetching one org.
|
- [Get Organization](https://docs.mem0.ai/api-reference/organization/get-org) [Platform]: Use when fetching one org.
|
||||||
- [Get Organization Members](https://docs.mem0.ai/api-reference/organization/get-org-members) [Platform]: Use when listing org members.
|
- [Get Organization Members](https://docs.mem0.ai/api-reference/organization/get-org-members) [Platform]: Use when listing org members.
|
||||||
- [Add Organization Member](https://docs.mem0.ai/api-reference/organization/add-org-member) [Platform]: Use when inviting a member to an org.
|
- [Add Organization Member](https://docs.mem0.ai/api-reference/organization/add-org-member) [Platform]: Use when inviting a member to an org.
|
||||||
|
- [Update Organization Member](https://docs.mem0.ai/api-reference/organization/update-org-member) [Platform]: Use when updating an org member's role.
|
||||||
|
- [Remove Organization Member](https://docs.mem0.ai/api-reference/organization/remove-org-member) [Platform]: Use when removing a member from an organization.
|
||||||
- [Delete Organization](https://docs.mem0.ai/api-reference/organization/delete-org) [Platform]: Use when removing an org.
|
- [Delete Organization](https://docs.mem0.ai/api-reference/organization/delete-org) [Platform]: Use when removing an org.
|
||||||
|
|
||||||
### Projects
|
### Projects
|
||||||
@@ -375,6 +377,9 @@ All API Reference docs describe Mem0 Platform REST endpoints (requires API key).
|
|||||||
- [Get Project](https://docs.mem0.ai/api-reference/project/get-project) [Platform]: Use when fetching one project.
|
- [Get Project](https://docs.mem0.ai/api-reference/project/get-project) [Platform]: Use when fetching one project.
|
||||||
- [Get Project Members](https://docs.mem0.ai/api-reference/project/get-project-members) [Platform]: Use when listing project members.
|
- [Get Project Members](https://docs.mem0.ai/api-reference/project/get-project-members) [Platform]: Use when listing project members.
|
||||||
- [Add Project Member](https://docs.mem0.ai/api-reference/project/add-project-member) [Platform]: Use when inviting a member to a project.
|
- [Add Project Member](https://docs.mem0.ai/api-reference/project/add-project-member) [Platform]: Use when inviting a member to a project.
|
||||||
|
- [Update Project](https://docs.mem0.ai/api-reference/project/update-project) [Platform]: Use when updating project settings.
|
||||||
|
- [Update Project Member](https://docs.mem0.ai/api-reference/project/update-project-member) [Platform]: Use when updating a project member's role.
|
||||||
|
- [Remove Project Member](https://docs.mem0.ai/api-reference/project/remove-project-member) [Platform]: Use when removing a member from a project.
|
||||||
- [Delete Project](https://docs.mem0.ai/api-reference/project/delete-project) [Platform]: Use when removing a project.
|
- [Delete Project](https://docs.mem0.ai/api-reference/project/delete-project) [Platform]: Use when removing a project.
|
||||||
|
|
||||||
### Webhooks
|
### Webhooks
|
||||||
@@ -460,6 +465,7 @@ Everything below is OSS-only provider configuration. Skip this entire section wh
|
|||||||
- [LM Studio Embeddings](https://docs.mem0.ai/components/embedders/models/lmstudio) [OSS]: Use when embeddings run through LM Studio.
|
- [LM Studio Embeddings](https://docs.mem0.ai/components/embedders/models/lmstudio) [OSS]: Use when embeddings run through LM Studio.
|
||||||
- [Together Embeddings](https://docs.mem0.ai/components/embedders/models/together) [OSS]: Use when embeddings run on Together.
|
- [Together Embeddings](https://docs.mem0.ai/components/embedders/models/together) [OSS]: Use when embeddings run on Together.
|
||||||
- [LangChain Embeddings](https://docs.mem0.ai/components/embedders/models/langchain) [OSS]: Use when embeddings are wrapped behind a LangChain adapter.
|
- [LangChain Embeddings](https://docs.mem0.ai/components/embedders/models/langchain) [OSS]: Use when embeddings are wrapped behind a LangChain adapter.
|
||||||
|
- [FastEmbed](https://docs.mem0.ai/components/embedders/models/fastembed) [OSS]: Use when embeddings run locally via FastEmbed (ONNX).
|
||||||
|
|
||||||
### Vector Databases [OSS]
|
### Vector Databases [OSS]
|
||||||
- [Vector Database Overview](https://docs.mem0.ai/components/vectordbs/overview) [OSS]: Use when choosing a vector store.
|
- [Vector Database Overview](https://docs.mem0.ai/components/vectordbs/overview) [OSS]: Use when choosing a vector store.
|
||||||
@@ -498,7 +504,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.
|
- [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.
|
- [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.
|
- [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.
|
- [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).
|
||||||
- [LLM Reranker (prompt)](https://docs.mem0.ai/components/rerankers/models/llm) [OSS]: Use when the reranker is a prompted LLM (config guide).
|
|
||||||
- [LLM Reranker](https://docs.mem0.ai/components/rerankers/models/llm_reranker) [OSS]: Use when the reranker is a prompted LLM (implementation reference).
|
|
||||||
- [Zero Entropy Reranker](https://docs.mem0.ai/components/rerankers/models/zero_entropy) [OSS]: Use for the Zero Entropy reranker.
|
- [Zero Entropy Reranker](https://docs.mem0.ai/components/rerankers/models/zero_entropy) [OSS]: Use for the Zero Entropy reranker.
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ These changes produce a **+20 point improvement on LoCoMo** (71.4 → 91.6) and
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Constructor | `MemoryClient(api_key, org_id, project_id)` | `MemoryClient(api_key)` | Remove `org_id`, `project_id` from constructor |
|
| Constructor | `MemoryClient(api_key, org_id, project_id)` | `MemoryClient(api_key)` | Remove `org_id`, `project_id` from constructor |
|
||||||
| Method options | `client.add(messages, **kwargs)` | `client.add(messages, options=AddMemoryOptions(...))` | Use typed option classes (or `**kwargs` still works) |
|
| Method options | `client.add(messages, **kwargs)` | `client.add(messages, options=AddMemoryOptions(...))` | Use typed option classes (or `**kwargs` still works) |
|
||||||
| Removed params | `api_version`, `output_format`, `async_mode`, `filter_memories`, `expiration_date`, `keyword_search`, `force_add_only`, `batch_size`, `immutable`, `includes`, `excludes`, `enable_graph`, `org_name`, `project_name` | — | Remove from all calls |
|
| Removed params | `api_version`, `output_format`, `async_mode`, `filter_memories`, `keyword_search`, `force_add_only`, `batch_size`, `immutable`, `includes`, `excludes`, `enable_graph`, `org_name`, `project_name` | — | Remove from all calls |
|
||||||
|
|
||||||
### TypeScript Client SDK
|
### TypeScript Client SDK
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ These changes produce a **+20 point improvement on LoCoMo** (71.4 → 91.6) and
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Constructor | `new MemoryClient({ apiKey, organizationId, projectId })` | `new MemoryClient({ apiKey })` | Remove `organizationId`, `projectId`, `organizationName`, `projectName` |
|
| Constructor | `new MemoryClient({ apiKey, organizationId, projectId })` | `new MemoryClient({ apiKey })` | Remove `organizationId`, `projectId`, `organizationName`, `projectName` |
|
||||||
| All params | snake_case: `user_id`, `agent_id`, `top_k` | camelCase: `userId`, `agentId`, `topK` | Rename all params to camelCase |
|
| All params | snake_case: `user_id`, `agent_id`, `top_k` | camelCase: `userId`, `agentId`, `topK` | Rename all params to camelCase |
|
||||||
| Removed params | `api_version`, `output_format`, `async_mode`, `enable_graph`, `org_id`, `project_id`, `org_name`, `project_name`, `filter_memories`, `batch_size`, `force_add_only`, `immutable`, `expiration_date`, `includes`, `excludes`, `keyword_search` | — | Remove from all calls |
|
| Removed params | `api_version`, `output_format`, `async_mode`, `enable_graph`, `org_id`, `project_id`, `org_name`, `project_name`, `filter_memories`, `batch_size`, `force_add_only`, `immutable`, `includes`, `excludes`, `keyword_search` | — | Remove from all calls |
|
||||||
| Output format enum | `OutputFormat.V1`, `OutputFormat.V1_1` | Removed | v1.1 is now always used |
|
| Output format enum | `OutputFormat.V1`, `OutputFormat.V1_1` | Removed | v1.1 is now always used |
|
||||||
| API version enum | `API_VERSION.V1`, `API_VERSION.V2` | Removed | Handled internally |
|
| API version enum | `API_VERSION.V1`, `API_VERSION.V2` | Removed | Handled internally |
|
||||||
|
|
||||||
@@ -423,7 +423,7 @@ These parameters have been removed across all SDKs. Remove them from your code:
|
|||||||
|
|
||||||
**All methods:** `api_version`, `output_format`, `async_mode`, `org_name`, `project_name`, `org_id`, `project_id`
|
**All methods:** `api_version`, `output_format`, `async_mode`, `org_name`, `project_name`, `org_id`, `project_id`
|
||||||
|
|
||||||
**add():** `enable_graph`, `immutable`, `expiration_date`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`
|
**add():** `enable_graph`, `immutable`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`
|
||||||
|
|
||||||
**search():** `enable_graph`
|
**search():** `enable_graph`
|
||||||
|
|
||||||
@@ -437,7 +437,7 @@ These parameters have been removed across all SDKs. Remove them from your code:
|
|||||||
|
|
||||||
**All methods:** `OutputFormat` enum, `API_VERSION` enum
|
**All methods:** `OutputFormat` enum, `API_VERSION` enum
|
||||||
|
|
||||||
**add():** `enable_graph` / `enableGraph`, `async_mode` / `asyncMode`, `output_format` / `outputFormat`, `immutable`, `expiration_date` / `expirationDate`, `filter_memories` / `filterMemories`, `batch_size` / `batchSize`, `force_add_only` / `forceAddOnly`, `includes`, `excludes`, `keyword_search` / `keywordSearch`
|
**add():** `enable_graph` / `enableGraph`, `async_mode` / `asyncMode`, `output_format` / `outputFormat`, `immutable`, `filter_memories` / `filterMemories`, `batch_size` / `batchSize`, `force_add_only` / `forceAddOnly`, `includes`, `excludes`, `keyword_search` / `keywordSearch`
|
||||||
|
|
||||||
**search():** `enable_graph` / `enableGraph`
|
**search():** `enable_graph` / `enableGraph`
|
||||||
|
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ client.add(messages, user_id="alice")
|
|||||||
# async_mode and output_format removed (async by default, v1.1 always)
|
# async_mode and output_format removed (async by default, v1.1 always)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Removed parameters:** `org_id`, `project_id`, `api_version`, `output_format`, `async_mode`, `enable_graph`, `immutable`, `expiration_date`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`, `org_name`, `project_name`
|
**Removed parameters:** `org_id`, `project_id`, `api_version`, `output_format`, `async_mode`, `enable_graph`, `immutable`, `filter_memories`, `batch_size`, `force_add_only`, `includes`, `excludes`, `keyword_search`, `org_name`, `project_name`
|
||||||
|
|
||||||
### TypeScript Client SDK
|
### TypeScript Client SDK
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ await client.search("query", {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
**Removed:** `OutputFormat` enum, `API_VERSION` enum, `organizationId`, `projectId`, `organizationName`, `projectName`, `enableGraph`, `asyncMode`, `outputFormat`, `immutable`, `expirationDate`, `filterMemories`, `batchSize`, `forceAddOnly`, `includes`, `excludes`, `keywordSearch`
|
**Removed:** `OutputFormat` enum, `API_VERSION` enum, `organizationId`, `projectId`, `organizationName`, `projectName`, `enableGraph`, `asyncMode`, `outputFormat`, `immutable`, `filterMemories`, `batchSize`, `forceAddOnly`, `includes`, `excludes`, `keywordSearch`
|
||||||
|
|
||||||
<Info>
|
<Info>
|
||||||
For the full list of parameter changes across all SDKs, see the [OSS migration guide](/migration/oss-v2-to-v3#removed-parameters-reference).
|
For the full list of parameter changes across all SDKs, see the [OSS migration guide](/migration/oss-v2-to-v3#removed-parameters-reference).
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ memory = Memory.from_config_file("config.yaml")
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Info icon="check">
|
<Info icon="check">
|
||||||
Run `memory.add(["Remember my favorite cafe in Tokyo."], user_id="alex")` and then `memory.search("favorite cafe", filters={"user_id": "alex"})`. You should see the Qdrant collection populate and the reranker mark the memory as a top hit.
|
Run `memory.add("Remember my favorite cafe in Tokyo.", user_id="alex")` and then `memory.search("favorite cafe", filters={"user_id": "alex"})`. You should see the Qdrant collection populate and the reranker mark the memory as a top hit.
|
||||||
</Info>
|
</Info>
|
||||||
|
|
||||||
## Tune component settings
|
## Tune component settings
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ icon: "bolt"
|
|||||||
</Warning>
|
</Warning>
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Working in TypeScript? The Node SDK still uses synchronous calls—use `Memory` there and rely on Python’s `AsyncMemory` when you need awaited operations.
|
Working in TypeScript? The OSS `Memory` class in the Node SDK (`mem0ai/oss`) is also fully async — every method returns a `Promise` and must be `await`ed. Python’s `AsyncMemory` serves the same purpose within Python async frameworks like FastAPI. Both runtimes support awaited memory operations; choose the SDK that matches your language.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
## Feature anatomy
|
## Feature anatomy
|
||||||
|
|||||||
@@ -165,8 +165,7 @@ await memory.add("Yesterday, I ordered a laptop, the order id is 12345", { userI
|
|||||||
{"memory": "Ordered a laptop", "event": "ADD"},
|
{"memory": "Ordered a laptop", "event": "ADD"},
|
||||||
{"memory": "Order ID: 12345", "event": "ADD"},
|
{"memory": "Order ID: 12345", "event": "ADD"},
|
||||||
{"memory": "Order placed yesterday", "event": "ADD"}
|
{"memory": "Order placed yesterday", "event": "ADD"}
|
||||||
],
|
]
|
||||||
"relations": []
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
@@ -188,8 +187,7 @@ await memory.add("I like going to hikes", { userId: "user123" });
|
|||||||
|
|
||||||
```json Output
|
```json Output
|
||||||
{
|
{
|
||||||
"results": [],
|
"results": []
|
||||||
"relations": []
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
</CodeGroup>
|
</CodeGroup>
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ Multimodal support lets Mem0 extract facts from images alongside regular text. A
|
|||||||
|
|
||||||
## Configure it
|
## Configure it
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
You must set `enable_vision: True` in your LLM config for image content to be processed. Without it, image turns are silently dropped and no vision memories are created. Example:
|
||||||
|
```python
|
||||||
|
config = {"llm": {"provider": "openai", "config": {"enable_vision": True, "vision_details": "auto"}}}
|
||||||
|
client = Memory.from_config(config)
|
||||||
|
```
|
||||||
|
</Warning>
|
||||||
|
|
||||||
### Add image messages from URLs
|
### Add image messages from URLs
|
||||||
|
|
||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
@@ -66,7 +74,7 @@ client.add(messages, user_id="alice")
|
|||||||
```
|
```
|
||||||
|
|
||||||
```ts TypeScript
|
```ts TypeScript
|
||||||
import { Memory } from "mem0ai";
|
import { Memory } from "mem0ai/oss";
|
||||||
|
|
||||||
const client = new Memory();
|
const client = new Memory();
|
||||||
|
|
||||||
@@ -123,7 +131,7 @@ client.add(messages, user_id="alice")
|
|||||||
|
|
||||||
```ts TypeScript
|
```ts TypeScript
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { Memory } from "mem0ai";
|
import { Memory } from "mem0ai/oss";
|
||||||
|
|
||||||
function encodeImage(imagePath: string) {
|
function encodeImage(imagePath: string) {
|
||||||
const buffer = fs.readFileSync(imagePath);
|
const buffer = fs.readFileSync(imagePath);
|
||||||
@@ -226,7 +234,7 @@ client.add(messages, user_id="user123")
|
|||||||
<CodeGroup>
|
<CodeGroup>
|
||||||
```python Python
|
```python Python
|
||||||
from mem0 import Memory
|
from mem0 import Memory
|
||||||
from mem0.exceptions import InvalidImageError, FileSizeError
|
from mem0.exceptions import ValidationError
|
||||||
|
|
||||||
client = Memory()
|
client = Memory()
|
||||||
|
|
||||||
@@ -242,16 +250,14 @@ try:
|
|||||||
client.add(messages, user_id="user123")
|
client.add(messages, user_id="user123")
|
||||||
print("Image processed successfully")
|
print("Image processed successfully")
|
||||||
|
|
||||||
except InvalidImageError:
|
except ValidationError as exc:
|
||||||
print("Invalid image format or corrupted file")
|
print(f"Image validation error: {exc}")
|
||||||
except FileSizeError:
|
|
||||||
print("Image file too large")
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"Unexpected error: {exc}")
|
print(f"Unexpected error: {exc}")
|
||||||
```
|
```
|
||||||
|
|
||||||
```ts TypeScript
|
```ts TypeScript
|
||||||
import { Memory } from "mem0ai";
|
import { Memory } from "mem0ai/oss";
|
||||||
|
|
||||||
const client = new Memory();
|
const client = new Memory();
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ config = {
|
|||||||
"provider": "llm_reranker",
|
"provider": "llm_reranker",
|
||||||
"config": {
|
"config": {
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"model": "gpt-4o-mini",
|
"model": "gpt-5-mini",
|
||||||
"api_key": "your-openai-api-key",
|
"api_key": "your-openai-api-key",
|
||||||
"top_k": 5
|
"top_k": 5
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ config = {
|
|||||||
"llm": {
|
"llm": {
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"config": {
|
"config": {
|
||||||
"model": "gpt-4",
|
"model": "gpt-5-mini",
|
||||||
"api_key": "your-openai-api-key"
|
"api_key": "your-openai-api-key"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1779,6 +1779,11 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Entity and metadata filters. Must include at least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`).",
|
"description": "Entity and metadata filters. Must include at least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`).",
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"show_expired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "When true, include memories whose `expiration_date` has passed. Expired memories are hidden by default."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1977,6 +1982,12 @@
|
|||||||
"additionalProperties": true,
|
"additionalProperties": true,
|
||||||
"description": "User-supplied metadata to attach to each extracted memory."
|
"description": "User-supplied metadata to attach to each extracted memory."
|
||||||
},
|
},
|
||||||
|
"expiration_date": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Optional expiration date in YYYY-MM-DD format. After this date, memories are hidden from search and get-all unless `show_expired` is true."
|
||||||
|
},
|
||||||
"custom_instructions": {
|
"custom_instructions": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Project-level instructions that guide extraction for this call."
|
"description": "Project-level instructions that guide extraction for this call."
|
||||||
@@ -2094,6 +2105,11 @@
|
|||||||
"description": "Entity and metadata filters. Must include at least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`). Supports `AND`, `OR`, `NOT`, and comparison operators (`in`, `gte`, `lte`, `gt`, `lt`, `contains`, `icontains`, `ne`).",
|
"description": "Entity and metadata filters. Must include at least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`). Supports `AND`, `OR`, `NOT`, and comparison operators (`in`, `gte`, `lte`, `gt`, `lt`, `contains`, `icontains`, `ne`).",
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
},
|
},
|
||||||
|
"show_expired": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "When true, include memories whose `expiration_date` has passed. Expired memories are hidden by default."
|
||||||
|
},
|
||||||
"top_k": {
|
"top_k": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 1,
|
"minimum": 1,
|
||||||
@@ -2432,6 +2448,12 @@
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Additional metadata associated with the memory"
|
"description": "Additional metadata associated with the memory"
|
||||||
|
},
|
||||||
|
"expiration_date": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Expiration date in YYYY-MM-DD format, or null to clear the expiration date."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,8 @@
|
|||||||
"picomatch@<2.3.2": "^2.3.2",
|
"picomatch@<2.3.2": "^2.3.2",
|
||||||
"@qdrant/js-client-rest": "^1.18.0",
|
"@qdrant/js-client-rest": "^1.18.0",
|
||||||
"uuid@<11.1.1": ">=11.1.1",
|
"uuid@<11.1.1": ">=11.1.1",
|
||||||
"esbuild": ">=0.28.1"
|
"esbuild": ">=0.28.1",
|
||||||
|
"undici@<6.27.0": ">=6.27.0 <8.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6
-5
@@ -13,6 +13,7 @@ overrides:
|
|||||||
'@qdrant/js-client-rest': ^1.18.0
|
'@qdrant/js-client-rest': ^1.18.0
|
||||||
uuid@<11.1.1: '>=11.1.1'
|
uuid@<11.1.1: '>=11.1.1'
|
||||||
esbuild: '>=0.28.1'
|
esbuild: '>=0.28.1'
|
||||||
|
undici@<6.27.0: '>=6.27.0 <8.0.0'
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
@@ -1996,9 +1997,9 @@ packages:
|
|||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
undici@6.26.0:
|
undici@7.28.0:
|
||||||
resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==}
|
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
|
||||||
engines: {node: '>=18.17'}
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
util-deprecate@1.0.2:
|
util-deprecate@1.0.2:
|
||||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||||
@@ -2509,7 +2510,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@qdrant/openapi-typescript-fetch': 1.2.6
|
'@qdrant/openapi-typescript-fetch': 1.2.6
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
undici: 6.26.0
|
undici: 7.28.0
|
||||||
|
|
||||||
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
||||||
|
|
||||||
@@ -4063,7 +4064,7 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
undici@6.26.0: {}
|
undici@7.28.0: {}
|
||||||
|
|
||||||
util-deprecate@1.0.2: {}
|
util-deprecate@1.0.2: {}
|
||||||
|
|
||||||
|
|||||||
@@ -21,3 +21,4 @@ overrides:
|
|||||||
"@qdrant/js-client-rest": "^1.18.0"
|
"@qdrant/js-client-rest": "^1.18.0"
|
||||||
"uuid@<11.1.1": ">=11.1.1"
|
"uuid@<11.1.1": ">=11.1.1"
|
||||||
"esbuild": ">=0.28.1"
|
"esbuild": ">=0.28.1"
|
||||||
|
"undici@<6.27.0": ">=6.27.0 <8.0.0"
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import {
|
import {
|
||||||
safePath,
|
safePath,
|
||||||
|
normalizeModuleUrlToPath,
|
||||||
loadSkill,
|
loadSkill,
|
||||||
loadTriagePrompt,
|
loadTriagePrompt,
|
||||||
loadCompactTriagePrompt,
|
loadCompactTriagePrompt,
|
||||||
} from "./skill-loader.ts";
|
} from "./skill-loader.ts";
|
||||||
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// safePath — path containment
|
// safePath — path containment
|
||||||
@@ -74,6 +76,31 @@ describe("loadSkill path traversal", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("normalizeModuleUrlToPath", () => {
|
||||||
|
it("normalizes raw Windows paths before fileURLToPath conversion", () => {
|
||||||
|
const rawWindowsMetaUrl = "C:\\Users\\example\\openclaw\\index.ts";
|
||||||
|
const result = normalizeModuleUrlToPath(rawWindowsMetaUrl);
|
||||||
|
// Assert the decoded property directly rather than reconstructing via the function body
|
||||||
|
expect(typeof result).toBe("string");
|
||||||
|
expect(result).not.toContain("%5C");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves already-correct file URLs unchanged", () => {
|
||||||
|
const fileMetaUrl = "file:///C:/Users/example/openclaw/index.ts";
|
||||||
|
const expected = fileURLToPath(fileMetaUrl);
|
||||||
|
|
||||||
|
expect(normalizeModuleUrlToPath(fileMetaUrl)).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skipIf(process.platform === "win32")(
|
||||||
|
"passes POSIX absolute paths through unchanged",
|
||||||
|
() => {
|
||||||
|
const posixPath = "/home/user/openclaw/index.ts";
|
||||||
|
expect(normalizeModuleUrlToPath(posixPath)).toBe(posixPath);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
describe("loadCompactTriagePrompt", () => {
|
describe("loadCompactTriagePrompt", () => {
|
||||||
it("keeps the core triage instructions without inlining the full skill body", () => {
|
it("keeps the core triage instructions without inlining the full skill body", () => {
|
||||||
const prompt = loadCompactTriagePrompt();
|
const prompt = loadCompactTriagePrompt();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||||
import type { SkillsConfig, CategoryConfig } from "./types.ts";
|
import type { SkillsConfig, CategoryConfig } from "./types.ts";
|
||||||
import { readText, exists } from "./fs-safe.ts";
|
import { readText, exists } from "./fs-safe.ts";
|
||||||
|
|
||||||
@@ -84,6 +84,14 @@ function parseSkillFile(content: string): ParsedSkill {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal — exported for testing only */
|
||||||
|
export function normalizeModuleUrlToPath(moduleUrl: string): string {
|
||||||
|
const normalizedUrl = moduleUrl.startsWith("file:")
|
||||||
|
? moduleUrl
|
||||||
|
: pathToFileURL(moduleUrl).toString();
|
||||||
|
return fileURLToPath(normalizedUrl);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Skill Loader
|
// Skill Loader
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -96,7 +104,7 @@ function resolveSkillsDir(): string {
|
|||||||
|
|
||||||
// Strategy 1: import.meta.url (works in native ESM)
|
// Strategy 1: import.meta.url (works in native ESM)
|
||||||
try {
|
try {
|
||||||
const metaDir = path.dirname(fileURLToPath(import.meta.url));
|
const metaDir = path.dirname(normalizeModuleUrlToPath(import.meta.url));
|
||||||
candidates.push(path.join(metaDir, "skills"));
|
candidates.push(path.join(metaDir, "skills"));
|
||||||
candidates.push(path.join(metaDir, "..", "skills"));
|
candidates.push(path.join(metaDir, "..", "skills"));
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
"form-data@<4.0.6": ">=4.0.6",
|
"form-data@<4.0.6": ">=4.0.6",
|
||||||
"uuid@<11.1.1": ">=11.1.1",
|
"uuid@<11.1.1": ">=11.1.1",
|
||||||
"esbuild": ">=0.28.1",
|
"esbuild": ">=0.28.1",
|
||||||
|
"undici@<6.27.0": ">=6.27.0 <8.0.0",
|
||||||
"undici@>=8.0.0 <8.5.0": ">=8.5.0"
|
"undici@>=8.0.0 <8.5.0": ">=8.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-5
@@ -8,6 +8,7 @@ overrides:
|
|||||||
form-data@<4.0.6: '>=4.0.6'
|
form-data@<4.0.6: '>=4.0.6'
|
||||||
uuid@<11.1.1: '>=11.1.1'
|
uuid@<11.1.1: '>=11.1.1'
|
||||||
esbuild: '>=0.28.1'
|
esbuild: '>=0.28.1'
|
||||||
|
undici@<6.27.0: '>=6.27.0 <8.0.0'
|
||||||
undici@>=8.0.0 <8.5.0: '>=8.5.0'
|
undici@>=8.0.0 <8.5.0: '>=8.5.0'
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
@@ -2351,9 +2352,9 @@ packages:
|
|||||||
undici-types@7.24.6:
|
undici-types@7.24.6:
|
||||||
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
|
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
|
||||||
|
|
||||||
undici@6.26.0:
|
undici@7.28.0:
|
||||||
resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==}
|
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
|
||||||
engines: {node: '>=18.17'}
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
undici@8.5.0:
|
undici@8.5.0:
|
||||||
resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==}
|
resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==}
|
||||||
@@ -3202,7 +3203,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@qdrant/openapi-typescript-fetch': 1.2.6
|
'@qdrant/openapi-typescript-fetch': 1.2.6
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
undici: 6.26.0
|
undici: 7.28.0
|
||||||
|
|
||||||
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
||||||
|
|
||||||
@@ -4894,7 +4895,7 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@7.24.6: {}
|
undici-types@7.24.6: {}
|
||||||
|
|
||||||
undici@6.26.0: {}
|
undici@7.28.0: {}
|
||||||
|
|
||||||
undici@8.5.0: {}
|
undici@8.5.0: {}
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ overrides:
|
|||||||
"form-data@<4.0.6": ">=4.0.6"
|
"form-data@<4.0.6": ">=4.0.6"
|
||||||
"uuid@<11.1.1": ">=11.1.1"
|
"uuid@<11.1.1": ">=11.1.1"
|
||||||
"esbuild": ">=0.28.1"
|
"esbuild": ">=0.28.1"
|
||||||
|
"undici@<6.27.0": ">=6.27.0 <8.0.0"
|
||||||
"undici@>=8.0.0 <8.5.0": ">=8.5.0"
|
"undici@>=8.0.0 <8.5.0": ">=8.5.0"
|
||||||
|
|||||||
@@ -156,7 +156,8 @@
|
|||||||
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4",
|
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4",
|
||||||
"glob@>=10.2.0 <10.5.0": "^10.5.0",
|
"glob@>=10.2.0 <10.5.0": "^10.5.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.25.4",
|
"@modelcontextprotocol/sdk": "^1.25.4",
|
||||||
"esbuild": ">=0.28.1"
|
"esbuild": ">=0.28.1",
|
||||||
|
"undici@<6.27.0": ">=6.27.0 <8.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+6
-5
@@ -23,6 +23,7 @@ overrides:
|
|||||||
glob@>=10.2.0 <10.5.0: ^10.5.0
|
glob@>=10.2.0 <10.5.0: ^10.5.0
|
||||||
'@modelcontextprotocol/sdk': ^1.25.4
|
'@modelcontextprotocol/sdk': ^1.25.4
|
||||||
esbuild: '>=0.28.1'
|
esbuild: '>=0.28.1'
|
||||||
|
undici@<6.27.0: '>=6.27.0 <8.0.0'
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
@@ -2925,9 +2926,9 @@ packages:
|
|||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
undici@6.26.0:
|
undici@7.28.0:
|
||||||
resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==}
|
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
|
||||||
engines: {node: '>=18.17'}
|
engines: {node: '>=20.18.1'}
|
||||||
|
|
||||||
update-browserslist-db@1.2.3:
|
update-browserslist-db@1.2.3:
|
||||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||||
@@ -3747,7 +3748,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@qdrant/openapi-typescript-fetch': 1.2.6
|
'@qdrant/openapi-typescript-fetch': 1.2.6
|
||||||
typescript: 5.5.4
|
typescript: 5.5.4
|
||||||
undici: 6.26.0
|
undici: 7.28.0
|
||||||
|
|
||||||
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
||||||
|
|
||||||
@@ -6113,7 +6114,7 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
undici@6.26.0: {}
|
undici@7.28.0: {}
|
||||||
|
|
||||||
update-browserslist-db@1.2.3(browserslist@4.28.2):
|
update-browserslist-db@1.2.3(browserslist@4.28.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -24,3 +24,4 @@ overrides:
|
|||||||
"glob@>=10.2.0 <10.5.0": "^10.5.0"
|
"glob@>=10.2.0 <10.5.0": "^10.5.0"
|
||||||
"@modelcontextprotocol/sdk": "^1.25.4"
|
"@modelcontextprotocol/sdk": "^1.25.4"
|
||||||
"esbuild": ">=0.28.1"
|
"esbuild": ">=0.28.1"
|
||||||
|
"undici@<6.27.0": ">=6.27.0 <8.0.0"
|
||||||
|
|||||||
@@ -281,19 +281,22 @@ export default class MemoryClient {
|
|||||||
text,
|
text,
|
||||||
metadata,
|
metadata,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
expirationDate,
|
||||||
}: {
|
}: {
|
||||||
text?: string;
|
text?: string;
|
||||||
metadata?: Record<string, any>;
|
metadata?: Record<string, any>;
|
||||||
timestamp?: number | string;
|
timestamp?: number | string;
|
||||||
|
expirationDate?: string | null;
|
||||||
},
|
},
|
||||||
): Promise<Array<Memory>> {
|
): Promise<Array<Memory>> {
|
||||||
if (
|
if (
|
||||||
text === undefined &&
|
text === undefined &&
|
||||||
metadata === undefined &&
|
metadata === undefined &&
|
||||||
timestamp === undefined
|
timestamp === undefined &&
|
||||||
|
expirationDate === undefined
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"At least one of text, metadata, or timestamp must be provided for update.",
|
"At least one of text, metadata, timestamp, or expirationDate must be provided for update.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,6 +305,7 @@ export default class MemoryClient {
|
|||||||
if (text !== undefined) payload.text = text;
|
if (text !== undefined) payload.text = text;
|
||||||
if (metadata !== undefined) payload.metadata = metadata;
|
if (metadata !== undefined) payload.metadata = metadata;
|
||||||
if (timestamp !== undefined) payload.timestamp = timestamp;
|
if (timestamp !== undefined) payload.timestamp = timestamp;
|
||||||
|
if (expirationDate !== undefined) payload.expiration_date = expirationDate;
|
||||||
|
|
||||||
const payloadKeys = Object.keys(payload);
|
const payloadKeys = Object.keys(payload);
|
||||||
this._captureEvent("update", [payloadKeys]);
|
this._captureEvent("update", [payloadKeys]);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export interface AddMemoryOptions extends EntityOptions {
|
|||||||
customCategories?: custom_categories[];
|
customCategories?: custom_categories[];
|
||||||
customInstructions?: string;
|
customInstructions?: string;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
|
expirationDate?: string;
|
||||||
structuredDataSchema?: Record<string, any>;
|
structuredDataSchema?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ export interface SearchMemoryOptions {
|
|||||||
latestOnly?: boolean;
|
latestOnly?: boolean;
|
||||||
fields?: string[];
|
fields?: string[];
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
|
showExpired?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetAllMemoryOptions {
|
export interface GetAllMemoryOptions {
|
||||||
@@ -35,6 +37,7 @@ export interface GetAllMemoryOptions {
|
|||||||
endDate?: string;
|
endDate?: string;
|
||||||
latestOnly?: boolean;
|
latestOnly?: boolean;
|
||||||
categories?: string[];
|
categories?: string[];
|
||||||
|
showExpired?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteAllMemoryOptions extends EntityOptions {}
|
export interface DeleteAllMemoryOptions extends EntityOptions {}
|
||||||
@@ -119,6 +122,7 @@ export interface Memory {
|
|||||||
memoryType?: string;
|
memoryType?: string;
|
||||||
score?: number;
|
score?: number;
|
||||||
metadata?: any | null;
|
metadata?: any | null;
|
||||||
|
expirationDate?: string | null;
|
||||||
owner?: string | null;
|
owner?: string | null;
|
||||||
agentId?: string | null;
|
agentId?: string | null;
|
||||||
appId?: string | null;
|
appId?: string | null;
|
||||||
|
|||||||
@@ -59,6 +59,21 @@ describe("MemoryClient - add()", () => {
|
|||||||
expect(getFetchBody(call!).user_id).toBe("user_1");
|
expect(getFetchBody(call!).user_id).toBe("user_1");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("serializes expirationDate as expiration_date", async () => {
|
||||||
|
const extra = new Map<string, { status: number; body: unknown }>();
|
||||||
|
extra.set("/v3/memories/add/", { status: 200, body: [createMockMemory()] });
|
||||||
|
const mock = setupMockFetch(extra);
|
||||||
|
|
||||||
|
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||||
|
await client.add([{ role: "user", content: "test" }], {
|
||||||
|
userId: "u1",
|
||||||
|
expirationDate: "2030-01-31",
|
||||||
|
});
|
||||||
|
|
||||||
|
const call = findFetchCall(mock, "/v3/memories/add/", "POST");
|
||||||
|
expect(getFetchBody(call!).expiration_date).toBe("2030-01-31");
|
||||||
|
});
|
||||||
|
|
||||||
test("throws an error when given an empty messages array", async () => {
|
test("throws an error when given an empty messages array", async () => {
|
||||||
setupMockFetch();
|
setupMockFetch();
|
||||||
|
|
||||||
@@ -176,11 +191,26 @@ describe("MemoryClient - update()", () => {
|
|||||||
expect(body.timestamp).toBe(1710600000);
|
expect(body.timestamp).toBe(1710600000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("sends expirationDate as expiration_date, including null", async () => {
|
||||||
|
const extra = new Map<string, { status: number; body: unknown }>();
|
||||||
|
extra.set("/v1/memories/mem_123/", {
|
||||||
|
status: 200,
|
||||||
|
body: createMockMemory(),
|
||||||
|
});
|
||||||
|
const mock = setupMockFetch(extra);
|
||||||
|
|
||||||
|
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||||
|
await client.update("mem_123", { expirationDate: null });
|
||||||
|
|
||||||
|
const call = findFetchCall(mock, "/v1/memories/mem_123/", "PUT");
|
||||||
|
expect(getFetchBody(call!).expiration_date).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
test("throws when no fields provided", async () => {
|
test("throws when no fields provided", async () => {
|
||||||
setupMockFetch();
|
setupMockFetch();
|
||||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||||
await expect(client.update("mem_123", {})).rejects.toThrow(
|
await expect(client.update("mem_123", {})).rejects.toThrow(
|
||||||
"At least one of text, metadata, or timestamp must be provided",
|
"At least one of text, metadata, timestamp, or expirationDate must be provided",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -81,6 +81,24 @@ describe("MemoryClient - search()", () => {
|
|||||||
expect(getFetchBody(call!).latest_only).toBe(true);
|
expect(getFetchBody(call!).latest_only).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("serializes showExpired as show_expired", async () => {
|
||||||
|
const extra = new Map<string, { status: number; body: unknown }>();
|
||||||
|
extra.set("/v3/memories/search/", {
|
||||||
|
status: 200,
|
||||||
|
body: { results: [] },
|
||||||
|
});
|
||||||
|
const mock = setupMockFetch(extra);
|
||||||
|
|
||||||
|
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||||
|
await client.search("test", {
|
||||||
|
filters: { user_id: "u1" },
|
||||||
|
showExpired: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const call = findFetchCall(mock, "/v3/memories/search/", "POST");
|
||||||
|
expect(getFetchBody(call!).show_expired).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
test("passes complex OR filters through to the API body", async () => {
|
test("passes complex OR filters through to the API body", async () => {
|
||||||
const extra = new Map<string, { status: number; body: unknown }>();
|
const extra = new Map<string, { status: number; body: unknown }>();
|
||||||
extra.set("/v3/memories/search/", {
|
extra.set("/v3/memories/search/", {
|
||||||
@@ -300,4 +318,22 @@ describe("MemoryClient - getAll() entity param rejection", () => {
|
|||||||
const call = findFetchCall(mock, "/v3/memories/", "POST");
|
const call = findFetchCall(mock, "/v3/memories/", "POST");
|
||||||
expect(getFetchBody(call!).latest_only).toBe(true);
|
expect(getFetchBody(call!).latest_only).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("serializes showExpired as show_expired", async () => {
|
||||||
|
const extra = new Map<string, { status: number; body: unknown }>();
|
||||||
|
extra.set("/v3/memories/", {
|
||||||
|
status: 200,
|
||||||
|
body: { results: [] },
|
||||||
|
});
|
||||||
|
const mock = setupMockFetch(extra);
|
||||||
|
|
||||||
|
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||||
|
await client.getAll({
|
||||||
|
filters: { user_id: "u1" },
|
||||||
|
showExpired: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const call = findFetchCall(mock, "/v3/memories/", "POST");
|
||||||
|
expect(getFetchBody(call!).show_expired).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export * from "./llms/ollama";
|
|||||||
export * from "./llms/lmstudio";
|
export * from "./llms/lmstudio";
|
||||||
export * from "./llms/mistral";
|
export * from "./llms/mistral";
|
||||||
export * from "./llms/langchain";
|
export * from "./llms/langchain";
|
||||||
|
export * from "./llms/litellm";
|
||||||
export * from "./vector_stores/base";
|
export * from "./vector_stores/base";
|
||||||
export * from "./vector_stores/memory";
|
export * from "./vector_stores/memory";
|
||||||
export * from "./vector_stores/qdrant";
|
export * from "./vector_stores/qdrant";
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { OpenAILLM } from "./openai";
|
||||||
|
import { LLMConfig, Message } from "../types";
|
||||||
|
import { LLMResponse } from "./base";
|
||||||
|
|
||||||
|
export class LiteLLM extends OpenAILLM {
|
||||||
|
constructor(config: LLMConfig) {
|
||||||
|
super({
|
||||||
|
...config,
|
||||||
|
apiKey: config.apiKey || process.env.LITELLM_API_KEY || "sk-anything",
|
||||||
|
baseURL:
|
||||||
|
config.baseURL ||
|
||||||
|
process.env.LITELLM_API_BASE ||
|
||||||
|
"http://localhost:4000",
|
||||||
|
model: config.model || "gpt-5-mini",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateResponse(
|
||||||
|
messages: Message[],
|
||||||
|
responseFormat?: { type: string },
|
||||||
|
tools?: any[],
|
||||||
|
): Promise<string | LLMResponse> {
|
||||||
|
try {
|
||||||
|
return await super.generateResponse(messages, responseFormat, tools);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new Error(`LiteLLM failed: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateChat(messages: Message[]): Promise<LLMResponse> {
|
||||||
|
try {
|
||||||
|
return await super.generateChat(messages);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new Error(`LiteLLM failed: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { OpenAILLM } from "./openai";
|
||||||
|
import { LLMConfig, Message } from "../types";
|
||||||
|
import { LLMResponse } from "./base";
|
||||||
|
|
||||||
|
export class MiniMaxLLM extends OpenAILLM {
|
||||||
|
constructor(config: LLMConfig) {
|
||||||
|
const apiKey = config.apiKey || process.env.MINIMAX_API_KEY;
|
||||||
|
if (!apiKey) {
|
||||||
|
throw new Error("MiniMax API key is required");
|
||||||
|
}
|
||||||
|
super({
|
||||||
|
...config,
|
||||||
|
apiKey,
|
||||||
|
baseURL:
|
||||||
|
config.baseURL ||
|
||||||
|
process.env.MINIMAX_API_BASE ||
|
||||||
|
"https://api.minimax.io/v1",
|
||||||
|
model: config.model || "MiniMax-M2.7",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateResponse(
|
||||||
|
messages: Message[],
|
||||||
|
responseFormat?: { type: string },
|
||||||
|
tools?: any[],
|
||||||
|
): Promise<string | LLMResponse> {
|
||||||
|
try {
|
||||||
|
return await super.generateResponse(messages, responseFormat, tools);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new Error(`MiniMax LLM failed: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateChat(messages: Message[]): Promise<LLMResponse> {
|
||||||
|
try {
|
||||||
|
return await super.generateChat(messages);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
throw new Error(`MiniMax LLM failed: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1601,7 +1601,9 @@ export class Memory {
|
|||||||
has_agent_id: !!config.agentId,
|
has_agent_id: !!config.agentId,
|
||||||
has_run_id: !!config.runId,
|
has_run_id: !!config.runId,
|
||||||
});
|
});
|
||||||
const { userId, agentId, runId } = config;
|
const userId = validateAndTrimEntityId(config.userId, "userId");
|
||||||
|
const agentId = validateAndTrimEntityId(config.agentId, "agentId");
|
||||||
|
const runId = validateAndTrimEntityId(config.runId, "runId");
|
||||||
|
|
||||||
// Convert camelCase entity params to snake_case for filters (matches storage and search/getAll)
|
// Convert camelCase entity params to snake_case for filters (matches storage and search/getAll)
|
||||||
const filters: SearchFilters = {};
|
const filters: SearchFilters = {};
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import { RedisDB } from "../vector_stores/redis";
|
|||||||
import { OllamaLLM } from "../llms/ollama";
|
import { OllamaLLM } from "../llms/ollama";
|
||||||
import { LMStudioLLM } from "../llms/lmstudio";
|
import { LMStudioLLM } from "../llms/lmstudio";
|
||||||
import { DeepSeekLLM } from "../llms/deepseek";
|
import { DeepSeekLLM } from "../llms/deepseek";
|
||||||
|
import { LiteLLM } from "../llms/litellm";
|
||||||
|
import { MiniMaxLLM } from "../llms/minimax";
|
||||||
import { SupabaseDB } from "../vector_stores/supabase";
|
import { SupabaseDB } from "../vector_stores/supabase";
|
||||||
import { SQLiteManager } from "../storage/SQLiteManager";
|
import { SQLiteManager } from "../storage/SQLiteManager";
|
||||||
import { MemoryHistoryManager } from "../storage/MemoryHistoryManager";
|
import { MemoryHistoryManager } from "../storage/MemoryHistoryManager";
|
||||||
@@ -85,6 +87,10 @@ export class LLMFactory {
|
|||||||
return new LangchainLLM(config);
|
return new LangchainLLM(config);
|
||||||
case "deepseek":
|
case "deepseek":
|
||||||
return new DeepSeekLLM(config);
|
return new DeepSeekLLM(config);
|
||||||
|
case "litellm":
|
||||||
|
return new LiteLLM(config);
|
||||||
|
case "minimax":
|
||||||
|
return new MiniMaxLLM(config);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported LLM provider: ${provider}`);
|
throw new Error(`Unsupported LLM provider: ${provider}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Client as ClientType } from "pg";
|
import type { Client as ClientType, ClientConfig } from "pg";
|
||||||
import pkg from "pg";
|
import pkg from "pg";
|
||||||
const { Client, escapeIdentifier } = pkg;
|
const { Client, escapeIdentifier } = pkg;
|
||||||
import { VectorStore } from "./base";
|
import { VectorStore } from "./base";
|
||||||
@@ -157,41 +157,90 @@ export function buildFilterConditions(
|
|||||||
|
|
||||||
interface PGVectorConfig extends VectorStoreConfig {
|
interface PGVectorConfig extends VectorStoreConfig {
|
||||||
dbname?: string;
|
dbname?: string;
|
||||||
user: string;
|
user?: string;
|
||||||
password: string;
|
password?: string;
|
||||||
host: string;
|
host?: string;
|
||||||
port: number;
|
port?: number;
|
||||||
|
connectionString?: string;
|
||||||
|
ssl?: ClientConfig["ssl"];
|
||||||
embeddingModelDims: number;
|
embeddingModelDims: number;
|
||||||
diskann?: boolean;
|
diskann?: boolean;
|
||||||
hnsw?: boolean;
|
hnsw?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getConnectionString(config: PGVectorConfig): string | undefined {
|
||||||
|
return config.connectionString?.trim() || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateConnectionConfig(config: PGVectorConfig): void {
|
||||||
|
if (getConnectionString(config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingFields = ["user", "password", "host", "port"].filter((field) => {
|
||||||
|
const v = config[field as keyof PGVectorConfig];
|
||||||
|
return v === undefined || v === null || v === "";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (missingFields.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`PGVector requires either connectionString or ${missingFields.join(", ")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildClientConfig(
|
||||||
|
config: PGVectorConfig,
|
||||||
|
database?: string,
|
||||||
|
): ClientConfig {
|
||||||
|
const connectionString = getConnectionString(config);
|
||||||
|
if (connectionString) {
|
||||||
|
return {
|
||||||
|
connectionString,
|
||||||
|
...(config.ssl !== undefined ? { ssl: config.ssl } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
database,
|
||||||
|
user: config.user,
|
||||||
|
password: config.password,
|
||||||
|
host: config.host,
|
||||||
|
port: config.port,
|
||||||
|
...(config.ssl !== undefined ? { ssl: config.ssl } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class PGVector implements VectorStore {
|
export class PGVector implements VectorStore {
|
||||||
private client: ClientType;
|
private client: ClientType;
|
||||||
private collectionName: string;
|
private collectionName: string;
|
||||||
private useDiskann: boolean;
|
private useDiskann: boolean;
|
||||||
private useHnsw: boolean;
|
private useHnsw: boolean;
|
||||||
private readonly dbName: string;
|
private readonly dbName: string;
|
||||||
|
private readonly useDirectConnection: boolean;
|
||||||
private config: PGVectorConfig;
|
private config: PGVectorConfig;
|
||||||
private _initPromise?: Promise<void>;
|
private _initPromise?: Promise<void>;
|
||||||
|
|
||||||
constructor(config: PGVectorConfig) {
|
constructor(config: PGVectorConfig) {
|
||||||
|
validateConnectionConfig(config);
|
||||||
this.collectionName = validateIdentifier(
|
this.collectionName = validateIdentifier(
|
||||||
config.collectionName || "memories",
|
config.collectionName || "memories",
|
||||||
"collectionName",
|
"collectionName",
|
||||||
);
|
);
|
||||||
this.useDiskann = config.diskann || false;
|
this.useDiskann = config.diskann || false;
|
||||||
this.useHnsw = config.hnsw || false;
|
this.useHnsw = config.hnsw || false;
|
||||||
this.dbName = validateIdentifier(config.dbname || "vector_store", "dbname");
|
this.useDirectConnection = !!getConnectionString(config);
|
||||||
|
this.dbName = this.useDirectConnection
|
||||||
|
? ""
|
||||||
|
: validateIdentifier(config.dbname || "vector_store", "dbname");
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
this.client = new Client({
|
this.client = new Client(
|
||||||
database: "postgres", // Initially connect to default postgres database
|
buildClientConfig(
|
||||||
user: config.user,
|
config,
|
||||||
password: config.password,
|
this.useDirectConnection ? undefined : "postgres",
|
||||||
host: config.host,
|
),
|
||||||
port: config.port,
|
);
|
||||||
});
|
|
||||||
this.initialize().catch(console.error);
|
this.initialize().catch(console.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,29 +259,20 @@ export class PGVector implements VectorStore {
|
|||||||
try {
|
try {
|
||||||
await this.client.connect();
|
await this.client.connect();
|
||||||
|
|
||||||
// Check if database exists
|
if (!this.useDirectConnection) {
|
||||||
const dbExists = await this.checkDatabaseExists(this.dbName);
|
const dbExists = await this.checkDatabaseExists(this.dbName);
|
||||||
if (!dbExists) {
|
if (!dbExists) {
|
||||||
await this.createDatabase(this.dbName);
|
await this.createDatabase(this.dbName);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.client.end();
|
||||||
|
|
||||||
|
this.client = new Client(buildClientConfig(this.config, this.dbName));
|
||||||
|
await this.client.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect from postgres database
|
|
||||||
await this.client.end();
|
|
||||||
|
|
||||||
// Connect to the target database
|
|
||||||
this.client = new Client({
|
|
||||||
database: this.dbName,
|
|
||||||
user: this.config.user,
|
|
||||||
password: this.config.password,
|
|
||||||
host: this.config.host,
|
|
||||||
port: this.config.port,
|
|
||||||
});
|
|
||||||
await this.client.connect();
|
|
||||||
|
|
||||||
// Create vector extension
|
|
||||||
await this.client.query("CREATE EXTENSION IF NOT EXISTS vector");
|
await this.client.query("CREATE EXTENSION IF NOT EXISTS vector");
|
||||||
|
|
||||||
// Create memory_migrations table
|
|
||||||
await this.client.query(`
|
await this.client.query(`
|
||||||
CREATE TABLE IF NOT EXISTS memory_migrations (
|
CREATE TABLE IF NOT EXISTS memory_migrations (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
@@ -240,7 +280,6 @@ export class PGVector implements VectorStore {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Check if the collection exists
|
|
||||||
const collections = await this.listCols();
|
const collections = await this.listCols();
|
||||||
if (!collections.includes(this.collectionName)) {
|
if (!collections.includes(this.collectionName)) {
|
||||||
await this.createCol(this.config.embeddingModelDims);
|
await this.createCol(this.config.embeddingModelDims);
|
||||||
|
|||||||
@@ -92,6 +92,16 @@ jest.mock("../src/llms/deepseek", () => ({
|
|||||||
.fn()
|
.fn()
|
||||||
.mockImplementation((config) => ({ type: "deepseek-llm", config })),
|
.mockImplementation((config) => ({ type: "deepseek-llm", config })),
|
||||||
}));
|
}));
|
||||||
|
jest.mock("../src/llms/litellm", () => ({
|
||||||
|
LiteLLM: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((config) => ({ type: "litellm-llm", config })),
|
||||||
|
}));
|
||||||
|
jest.mock("../src/llms/minimax", () => ({
|
||||||
|
MiniMaxLLM: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((config) => ({ type: "minimax-llm", config })),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock("../src/vector_stores/qdrant", () => ({
|
jest.mock("../src/vector_stores/qdrant", () => ({
|
||||||
Qdrant: jest
|
Qdrant: jest
|
||||||
@@ -206,6 +216,8 @@ describe("LLMFactory", () => {
|
|||||||
["langchain"],
|
["langchain"],
|
||||||
["lmstudio"],
|
["lmstudio"],
|
||||||
["deepseek"],
|
["deepseek"],
|
||||||
|
["litellm"],
|
||||||
|
["minimax"],
|
||||||
])("creates LLM for provider '%s'", (provider) => {
|
])("creates LLM for provider '%s'", (provider) => {
|
||||||
expect(() => LLMFactory.create(provider, dummyLLMConfig)).not.toThrow();
|
expect(() => LLMFactory.create(provider, dummyLLMConfig)).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/// <reference types="jest" />
|
||||||
|
/**
|
||||||
|
* LiteLLM — unit tests (mocked OpenAI).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LiteLLM } from "../src/llms/litellm";
|
||||||
|
|
||||||
|
const mockCreate = jest.fn();
|
||||||
|
|
||||||
|
jest.mock("openai", () => {
|
||||||
|
return jest.fn().mockImplementation(() => ({
|
||||||
|
chat: { completions: { create: mockCreate } },
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("LiteLLM (unit)", () => {
|
||||||
|
beforeEach(() => mockCreate.mockClear());
|
||||||
|
|
||||||
|
it("uses default baseURL when none is provided", () => {
|
||||||
|
const llm = new LiteLLM({});
|
||||||
|
expect(llm).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generateResponse() returns a text response", async () => {
|
||||||
|
mockCreate.mockResolvedValueOnce({
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
content: "Hello, world!",
|
||||||
|
role: "assistant",
|
||||||
|
tool_calls: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const llm = new LiteLLM({ baseURL: "http://localhost:4000" });
|
||||||
|
const result = await llm.generateResponse([
|
||||||
|
{ role: "user", content: "Hi" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(mockCreate).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result).toBe("Hello, world!");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generateResponse() handles tool calls", async () => {
|
||||||
|
mockCreate.mockResolvedValueOnce({
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
content: "",
|
||||||
|
role: "assistant",
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
function: {
|
||||||
|
name: "get_weather",
|
||||||
|
arguments: '{"city": "London"}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const llm = new LiteLLM({});
|
||||||
|
const result = await llm.generateResponse(
|
||||||
|
[{ role: "user", content: "What is the weather?" }],
|
||||||
|
undefined,
|
||||||
|
[{ type: "function", function: { name: "get_weather" } }],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: "",
|
||||||
|
role: "assistant",
|
||||||
|
toolCalls: [{ name: "get_weather", arguments: '{"city": "London"}' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generateResponse() wraps API errors with a clear message", async () => {
|
||||||
|
mockCreate.mockRejectedValueOnce(new Error("Connection refused"));
|
||||||
|
|
||||||
|
const llm = new LiteLLM({});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
llm.generateResponse([{ role: "user", content: "Hi" }]),
|
||||||
|
).rejects.toThrow("LiteLLM failed: Connection refused");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generateChat() returns LLMResponse shape", async () => {
|
||||||
|
mockCreate.mockResolvedValueOnce({
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: { content: "I can help with that.", role: "assistant" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const llm = new LiteLLM({});
|
||||||
|
const result = await llm.generateChat([
|
||||||
|
{ role: "user", content: "Help me" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: "I can help with that.",
|
||||||
|
role: "assistant",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generateChat() wraps API errors with a clear message", async () => {
|
||||||
|
mockCreate.mockRejectedValueOnce(new Error("Timeout"));
|
||||||
|
|
||||||
|
const llm = new LiteLLM({});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
llm.generateChat([{ role: "user", content: "Hi" }]),
|
||||||
|
).rejects.toThrow("LiteLLM failed: Timeout");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects LITELLM_API_BASE env var", () => {
|
||||||
|
const original = process.env.LITELLM_API_BASE;
|
||||||
|
process.env.LITELLM_API_BASE = "http://custom-proxy:8080";
|
||||||
|
try {
|
||||||
|
const llm = new LiteLLM({});
|
||||||
|
expect(llm).toBeDefined();
|
||||||
|
} finally {
|
||||||
|
if (original !== undefined) process.env.LITELLM_API_BASE = original;
|
||||||
|
else delete process.env.LITELLM_API_BASE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -314,4 +314,27 @@ describe("Memory Input Validation", () => {
|
|||||||
expect(result.results).toBeDefined();
|
expect(result.results).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("deleteAll() entity ID validation", () => {
|
||||||
|
it("should throw error when userId is whitespace-only", async () => {
|
||||||
|
await expect(memory.deleteAll({ userId: " " })).rejects.toThrow(
|
||||||
|
"Invalid userId",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when userId contains internal whitespace", async () => {
|
||||||
|
await expect(memory.deleteAll({ userId: "user 123" })).rejects.toThrow(
|
||||||
|
"Invalid userId: cannot contain whitespace",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trim userId before listing memories", async () => {
|
||||||
|
const listSpy = jest.spyOn(memory["vectorStore"], "list");
|
||||||
|
listSpy.mockResolvedValue([[], null]);
|
||||||
|
|
||||||
|
await memory.deleteAll({ userId: " alice " });
|
||||||
|
|
||||||
|
expect(listSpy).toHaveBeenCalledWith({ user_id: "alice" });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
/// <reference types="jest" />
|
||||||
|
/**
|
||||||
|
* MiniMax LLM - unit tests (mocked OpenAI).
|
||||||
|
*/
|
||||||
|
|
||||||
|
let capturedConstructorArgs: any;
|
||||||
|
const mockCreate = jest.fn();
|
||||||
|
|
||||||
|
jest.mock("openai", () => {
|
||||||
|
return jest.fn().mockImplementation((args: any) => {
|
||||||
|
capturedConstructorArgs = args;
|
||||||
|
return {
|
||||||
|
chat: { completions: { create: mockCreate } },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
import { MiniMaxLLM } from "../src/llms/minimax";
|
||||||
|
|
||||||
|
describe("MiniMaxLLM (unit)", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
capturedConstructorArgs = undefined;
|
||||||
|
mockCreate.mockClear();
|
||||||
|
delete process.env.MINIMAX_API_KEY;
|
||||||
|
delete process.env.MINIMAX_API_BASE;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws when no API key is provided", () => {
|
||||||
|
expect(() => new MiniMaxLLM({})).toThrow("MiniMax API key is required");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses MiniMax defaults with an explicit API key", () => {
|
||||||
|
new MiniMaxLLM({ apiKey: "test-key" });
|
||||||
|
|
||||||
|
expect(capturedConstructorArgs).toMatchObject({
|
||||||
|
apiKey: "test-key",
|
||||||
|
baseURL: "https://api.minimax.io/v1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses environment variables when config does not provide credentials", () => {
|
||||||
|
process.env.MINIMAX_API_KEY = "env-key";
|
||||||
|
process.env.MINIMAX_API_BASE = "https://example.minimax.test/v1";
|
||||||
|
|
||||||
|
new MiniMaxLLM({});
|
||||||
|
|
||||||
|
expect(capturedConstructorArgs).toMatchObject({
|
||||||
|
apiKey: "env-key",
|
||||||
|
baseURL: "https://example.minimax.test/v1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("config values take precedence over environment variables", () => {
|
||||||
|
process.env.MINIMAX_API_KEY = "env-key";
|
||||||
|
process.env.MINIMAX_API_BASE = "https://env.minimax.test/v1";
|
||||||
|
|
||||||
|
new MiniMaxLLM({
|
||||||
|
apiKey: "config-key",
|
||||||
|
baseURL: "https://config.minimax.test/v1",
|
||||||
|
model: "MiniMax-M1",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(capturedConstructorArgs).toMatchObject({
|
||||||
|
apiKey: "config-key",
|
||||||
|
baseURL: "https://config.minimax.test/v1",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generateResponse() returns a text response", async () => {
|
||||||
|
mockCreate.mockResolvedValueOnce({
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
content: "Hello from MiniMax",
|
||||||
|
role: "assistant",
|
||||||
|
tool_calls: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const llm = new MiniMaxLLM({ apiKey: "test-key" });
|
||||||
|
const result = await llm.generateResponse([
|
||||||
|
{ role: "user", content: "Hi" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(mockCreate).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ model: "MiniMax-M2.7" }),
|
||||||
|
);
|
||||||
|
expect(result).toBe("Hello from MiniMax");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generateResponse() handles tool calls", async () => {
|
||||||
|
mockCreate.mockResolvedValueOnce({
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: {
|
||||||
|
content: "",
|
||||||
|
role: "assistant",
|
||||||
|
tool_calls: [
|
||||||
|
{
|
||||||
|
function: {
|
||||||
|
name: "search_memory",
|
||||||
|
arguments: '{"query": "alice"}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const llm = new MiniMaxLLM({ apiKey: "test-key" });
|
||||||
|
const result = await llm.generateResponse(
|
||||||
|
[{ role: "user", content: "Find Alice" }],
|
||||||
|
undefined,
|
||||||
|
[{ type: "function", function: { name: "search_memory" } }],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: "",
|
||||||
|
role: "assistant",
|
||||||
|
toolCalls: [{ name: "search_memory", arguments: '{"query": "alice"}' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generateResponse() wraps API errors with a clear message", async () => {
|
||||||
|
mockCreate.mockRejectedValueOnce(new Error("Connection refused"));
|
||||||
|
|
||||||
|
const llm = new MiniMaxLLM({ apiKey: "test-key" });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
llm.generateResponse([{ role: "user", content: "Hi" }]),
|
||||||
|
).rejects.toThrow("MiniMax LLM failed: Connection refused");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generateChat() returns LLMResponse shape", async () => {
|
||||||
|
mockCreate.mockResolvedValueOnce({
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
message: { content: "I can help with that.", role: "assistant" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const llm = new MiniMaxLLM({ apiKey: "test-key" });
|
||||||
|
const result = await llm.generateChat([
|
||||||
|
{ role: "user", content: "Help me" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
content: "I can help with that.",
|
||||||
|
role: "assistant",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generateChat() wraps API errors with a clear message", async () => {
|
||||||
|
mockCreate.mockRejectedValueOnce(new Error("Timeout"));
|
||||||
|
|
||||||
|
const llm = new MiniMaxLLM({ apiKey: "test-key" });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
llm.generateChat([{ role: "user", content: "Hi" }]),
|
||||||
|
).rejects.toThrow("MiniMax LLM failed: Timeout");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
/// <reference types="jest" />
|
|
||||||
|
|
||||||
jest.mock("pg", () => {
|
jest.mock("pg", () => {
|
||||||
const Client = jest.fn().mockImplementation(() => ({
|
const Client = jest.fn().mockImplementation(() => ({
|
||||||
connect: jest.fn().mockResolvedValue(undefined),
|
connect: jest.fn().mockResolvedValue(undefined),
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/// <reference types="jest" />
|
|
||||||
|
|
||||||
const searchRows = [
|
const searchRows = [
|
||||||
{
|
{
|
||||||
id: "a",
|
id: "a",
|
||||||
@@ -23,9 +21,13 @@ const searchRows = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const mockState = {
|
||||||
|
databaseExists: true,
|
||||||
|
};
|
||||||
|
|
||||||
function mockPgQuery(sql: string) {
|
function mockPgQuery(sql: string) {
|
||||||
if (sql.includes("SELECT 1 FROM pg_database")) {
|
if (sql.includes("SELECT 1 FROM pg_database")) {
|
||||||
return { rows: [{ "?column?": 1 }] };
|
return { rows: mockState.databaseExists ? [{ "?column?": 1 }] : [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sql.includes("FROM information_schema.tables")) {
|
if (sql.includes("FROM information_schema.tables")) {
|
||||||
@@ -69,13 +71,129 @@ jest.mock("pg", () => {
|
|||||||
|
|
||||||
import { PGVector } from "../src/vector_stores/pgvector";
|
import { PGVector } from "../src/vector_stores/pgvector";
|
||||||
|
|
||||||
describe("PGVector - search()", () => {
|
function getClientQueries(client: { query: jest.Mock }) {
|
||||||
|
return client.query.mock.calls.map(([sql]) => sql as string);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("PGVector", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const pg = require("pg");
|
const pg = require("pg");
|
||||||
|
mockState.databaseExists = true;
|
||||||
pg.__mock.Client.mockClear();
|
pg.__mock.Client.mockClear();
|
||||||
pg.__mock.clients.length = 0;
|
pg.__mock.clients.length = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("uses one direct client for connectionString mode and skips bootstrap database creation", async () => {
|
||||||
|
mockState.databaseExists = false;
|
||||||
|
|
||||||
|
const ssl = { rejectUnauthorized: false };
|
||||||
|
const store = new PGVector({
|
||||||
|
collectionName: "memories",
|
||||||
|
connectionString:
|
||||||
|
"postgresql://postgres:postgres@db.example.com:5432/neondb",
|
||||||
|
ssl,
|
||||||
|
embeddingModelDims: 3,
|
||||||
|
dimension: 3,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
await store.initialize();
|
||||||
|
|
||||||
|
const pg = require("pg");
|
||||||
|
expect(pg.__mock.Client).toHaveBeenCalledTimes(1);
|
||||||
|
expect(pg.__mock.Client).toHaveBeenCalledWith({
|
||||||
|
connectionString:
|
||||||
|
"postgresql://postgres:postgres@db.example.com:5432/neondb",
|
||||||
|
ssl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const directClient = pg.__mock.clients[0];
|
||||||
|
const queries = getClientQueries(directClient);
|
||||||
|
|
||||||
|
expect(queries).not.toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.stringContaining("SELECT 1 FROM pg_database"),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(queries).not.toEqual(
|
||||||
|
expect.arrayContaining([expect.stringContaining("CREATE DATABASE")]),
|
||||||
|
);
|
||||||
|
expect(queries).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
"CREATE EXTENSION IF NOT EXISTS vector",
|
||||||
|
expect.stringContaining("FROM information_schema.tables"),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps the split-field bootstrap flow when connectionString is absent", async () => {
|
||||||
|
mockState.databaseExists = false;
|
||||||
|
const ssl = { rejectUnauthorized: false };
|
||||||
|
|
||||||
|
const store = new PGVector({
|
||||||
|
collectionName: "memories",
|
||||||
|
user: "postgres",
|
||||||
|
password: "postgres",
|
||||||
|
host: "localhost",
|
||||||
|
port: 5432,
|
||||||
|
dbname: "vector_store",
|
||||||
|
ssl,
|
||||||
|
embeddingModelDims: 3,
|
||||||
|
dimension: 3,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
await store.initialize();
|
||||||
|
|
||||||
|
const pg = require("pg");
|
||||||
|
expect(pg.__mock.Client).toHaveBeenCalledTimes(2);
|
||||||
|
expect(pg.__mock.Client).toHaveBeenNthCalledWith(1, {
|
||||||
|
database: "postgres",
|
||||||
|
user: "postgres",
|
||||||
|
password: "postgres",
|
||||||
|
host: "localhost",
|
||||||
|
port: 5432,
|
||||||
|
ssl,
|
||||||
|
});
|
||||||
|
expect(pg.__mock.Client).toHaveBeenNthCalledWith(2, {
|
||||||
|
database: "vector_store",
|
||||||
|
user: "postgres",
|
||||||
|
password: "postgres",
|
||||||
|
host: "localhost",
|
||||||
|
port: 5432,
|
||||||
|
ssl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const bootstrapClient = pg.__mock.clients[0];
|
||||||
|
const activeClient = pg.__mock.clients[1];
|
||||||
|
const bootstrapQueries = getClientQueries(bootstrapClient);
|
||||||
|
|
||||||
|
expect(bootstrapQueries).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
"SELECT 1 FROM pg_database WHERE datname = $1",
|
||||||
|
'CREATE DATABASE "vector_store"',
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(bootstrapClient.end).toHaveBeenCalledTimes(1);
|
||||||
|
expect(getClientQueries(activeClient)).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
"CREATE EXTENSION IF NOT EXISTS vector",
|
||||||
|
expect.stringContaining("FROM information_schema.tables"),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("throws when connectionString is absent and split-field params are missing", () => {
|
||||||
|
expect(
|
||||||
|
() =>
|
||||||
|
new PGVector({
|
||||||
|
collectionName: "memories",
|
||||||
|
embeddingModelDims: 3,
|
||||||
|
dimension: 3,
|
||||||
|
} as any),
|
||||||
|
).toThrow(
|
||||||
|
"PGVector requires either connectionString or user, password, host, port",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("returns similarity score (1 - distance) clamped to [0, 1]", async () => {
|
test("returns similarity score (1 - distance) clamped to [0, 1]", async () => {
|
||||||
const store = new PGVector({
|
const store = new PGVector({
|
||||||
collectionName: "memories",
|
collectionName: "memories",
|
||||||
|
|||||||
+14
-10
@@ -344,24 +344,26 @@ class MemoryClient:
|
|||||||
Args:
|
Args:
|
||||||
memory_id: The ID of the memory to update.
|
memory_id: The ID of the memory to update.
|
||||||
options: Typed options (UpdateMemoryOptions) with text, metadata,
|
options: Typed options (UpdateMemoryOptions) with text, metadata,
|
||||||
and/or timestamp fields.
|
timestamp, and/or expiration_date fields.
|
||||||
**kwargs: Alternatively pass text, metadata, timestamp as keyword args.
|
**kwargs: Alternatively pass text, metadata, timestamp, or
|
||||||
|
expiration_date as keyword args.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: The response from the server.
|
Dict[str, Any]: The response from the server.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If none of text, metadata, or timestamp are provided.
|
ValueError: If none of text, metadata, timestamp, or expiration_date are provided.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
|
>>> client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
|
||||||
>>> client.update("mem_123", text="Updated text")
|
>>> client.update("mem_123", text="Updated text")
|
||||||
|
>>> client.update("mem_123", expiration_date=None)
|
||||||
"""
|
"""
|
||||||
payload = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
|
payload = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
|
||||||
payload = {k: v for k, v in payload.items() if v is not None}
|
payload = {k: v for k, v in payload.items() if v is not None or k == "expiration_date"}
|
||||||
|
|
||||||
if not payload:
|
if not payload:
|
||||||
raise ValueError("At least one of text, metadata, or timestamp must be provided for update.")
|
raise ValueError("At least one of text, metadata, timestamp, or expiration_date must be provided for update.")
|
||||||
|
|
||||||
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "sync"})
|
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "sync"})
|
||||||
params = self._prepare_params()
|
params = self._prepare_params()
|
||||||
@@ -1260,24 +1262,26 @@ class AsyncMemoryClient:
|
|||||||
Args:
|
Args:
|
||||||
memory_id: The ID of the memory to update.
|
memory_id: The ID of the memory to update.
|
||||||
options: Typed options (UpdateMemoryOptions) with text, metadata,
|
options: Typed options (UpdateMemoryOptions) with text, metadata,
|
||||||
and/or timestamp fields.
|
timestamp, and/or expiration_date fields.
|
||||||
**kwargs: Alternatively pass text, metadata, timestamp as keyword args.
|
**kwargs: Alternatively pass text, metadata, timestamp, or
|
||||||
|
expiration_date as keyword args.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict[str, Any]: The response from the server.
|
Dict[str, Any]: The response from the server.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If none of text, metadata, or timestamp are provided.
|
ValueError: If none of text, metadata, timestamp, or expiration_date are provided.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
>>> await client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
|
>>> await client.update("mem_123", UpdateMemoryOptions(text="Updated text"))
|
||||||
>>> await client.update("mem_123", text="Updated text")
|
>>> await client.update("mem_123", text="Updated text")
|
||||||
|
>>> await client.update("mem_123", expiration_date=None)
|
||||||
"""
|
"""
|
||||||
payload = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
|
payload = {**(options.model_dump(exclude_unset=True) if options else {}), **kwargs}
|
||||||
payload = {k: v for k, v in payload.items() if v is not None}
|
payload = {k: v for k, v in payload.items() if v is not None or k == "expiration_date"}
|
||||||
|
|
||||||
if not payload:
|
if not payload:
|
||||||
raise ValueError("At least one of text, metadata, or timestamp must be provided for update.")
|
raise ValueError("At least one of text, metadata, timestamp, or expiration_date must be provided for update.")
|
||||||
|
|
||||||
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "async"})
|
capture_client_event("client.update", self, {"memory_id": memory_id, "sync_type": "async"})
|
||||||
params = self._prepare_params()
|
params = self._prepare_params()
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class AddMemoryOptions(BaseModel):
|
|||||||
)
|
)
|
||||||
custom_instructions: Optional[str] = Field(default=None, description="Custom instructions for fact extraction")
|
custom_instructions: Optional[str] = Field(default=None, description="Custom instructions for fact extraction")
|
||||||
timestamp: Optional[int] = Field(default=None, description="Unix timestamp for the memory")
|
timestamp: Optional[int] = Field(default=None, description="Unix timestamp for the memory")
|
||||||
|
expiration_date: Optional[str] = Field(default=None, description="Expiration date in YYYY-MM-DD format")
|
||||||
structured_data_schema: Optional[Dict[str, Any]] = Field(
|
structured_data_schema: Optional[Dict[str, Any]] = Field(
|
||||||
default=None, description="Schema for structured data extraction"
|
default=None, description="Schema for structured data extraction"
|
||||||
)
|
)
|
||||||
@@ -50,6 +51,7 @@ class SearchMemoryOptions(BaseModel):
|
|||||||
threshold: Optional[float] = Field(default=None, description="Minimum similarity score threshold")
|
threshold: Optional[float] = Field(default=None, description="Minimum similarity score threshold")
|
||||||
fields: Optional[List[str]] = Field(default=None, description="Fields to include in the response")
|
fields: Optional[List[str]] = Field(default=None, description="Fields to include in the response")
|
||||||
categories: Optional[List[str]] = Field(default=None, description="Categories to filter by")
|
categories: Optional[List[str]] = Field(default=None, description="Categories to filter by")
|
||||||
|
show_expired: Optional[bool] = Field(default=None, description="Whether to include expired memories")
|
||||||
|
|
||||||
|
|
||||||
class GetAllMemoryOptions(BaseModel):
|
class GetAllMemoryOptions(BaseModel):
|
||||||
@@ -71,6 +73,7 @@ class GetAllMemoryOptions(BaseModel):
|
|||||||
default=None, description="Filter memories created on or before this date (ISO 8601)"
|
default=None, description="Filter memories created on or before this date (ISO 8601)"
|
||||||
)
|
)
|
||||||
categories: Optional[List[str]] = Field(default=None, description="Categories to filter by")
|
categories: Optional[List[str]] = Field(default=None, description="Categories to filter by")
|
||||||
|
show_expired: Optional[bool] = Field(default=None, description="Whether to include expired memories")
|
||||||
|
|
||||||
|
|
||||||
class DeleteAllMemoryOptions(BaseModel):
|
class DeleteAllMemoryOptions(BaseModel):
|
||||||
@@ -91,6 +94,7 @@ class UpdateMemoryOptions(BaseModel):
|
|||||||
text: Optional[str] = Field(default=None, description="New text content for the memory")
|
text: Optional[str] = Field(default=None, description="New text content for the memory")
|
||||||
metadata: Optional[Dict[str, Any]] = Field(default=None, description="Updated metadata")
|
metadata: Optional[Dict[str, Any]] = Field(default=None, description="Updated metadata")
|
||||||
timestamp: Optional[Union[int, float, str]] = Field(default=None, description="Updated timestamp")
|
timestamp: Optional[Union[int, float, str]] = Field(default=None, description="Updated timestamp")
|
||||||
|
expiration_date: Optional[str] = Field(default=None, description="Expiration date in YYYY-MM-DD format, or None to clear")
|
||||||
|
|
||||||
|
|
||||||
class ProjectUpdateOptions(BaseModel):
|
class ProjectUpdateOptions(BaseModel):
|
||||||
|
|||||||
+153
-25
@@ -9,7 +9,7 @@ import time
|
|||||||
import uuid
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import datetime, timezone
|
from datetime import date, datetime, timezone
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
@@ -378,9 +378,37 @@ def _entity_collection_name(provider: str, collection_name: str) -> str:
|
|||||||
return f"{collection_name}{separator}entities"
|
return f"{collection_name}{separator}entities"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_expiration_date(value: Any) -> Optional[str]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value.date().isoformat()
|
||||||
|
if isinstance(value, date):
|
||||||
|
return value.isoformat()
|
||||||
|
if isinstance(value, str):
|
||||||
|
try:
|
||||||
|
return date.fromisoformat(value).isoformat()
|
||||||
|
except ValueError as exc:
|
||||||
|
raise ValueError("expiration_date must be a valid date in YYYY-MM-DD format.") from exc
|
||||||
|
raise ValueError("expiration_date must be a date string in YYYY-MM-DD format.")
|
||||||
|
|
||||||
|
|
||||||
|
def _payload_is_expired(payload: Optional[Dict[str, Any]]) -> bool:
|
||||||
|
if not payload:
|
||||||
|
return False
|
||||||
|
expiration_date = payload.get("expiration_date")
|
||||||
|
if not expiration_date:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return date.fromisoformat(str(expiration_date)) < datetime.now(timezone.utc).date()
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
setup_config()
|
setup_config()
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_UNSET = object()
|
||||||
_PROJECT_UPDATE_UNSUPPORTED_ERROR = "Project updates are not supported by the OSS Memory SDK."
|
_PROJECT_UPDATE_UNSUPPORTED_ERROR = "Project updates are not supported by the OSS Memory SDK."
|
||||||
|
|
||||||
|
|
||||||
@@ -694,6 +722,7 @@ class Memory(MemoryBase):
|
|||||||
run_id: Optional[str] = None,
|
run_id: Optional[str] = None,
|
||||||
metadata: Optional[Dict[str, Any]] = None,
|
metadata: Optional[Dict[str, Any]] = None,
|
||||||
timestamp: Optional[Any] = None,
|
timestamp: Optional[Any] = None,
|
||||||
|
expiration_date: Optional[Any] = None,
|
||||||
infer: bool = True,
|
infer: bool = True,
|
||||||
memory_type: Optional[str] = None,
|
memory_type: Optional[str] = None,
|
||||||
prompt: Optional[str] = None,
|
prompt: Optional[str] = None,
|
||||||
@@ -712,6 +741,8 @@ class Memory(MemoryBase):
|
|||||||
run_id (str, optional): ID of the run creating the memory. Defaults to None.
|
run_id (str, optional): ID of the run creating the memory. Defaults to None.
|
||||||
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
|
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
|
||||||
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
|
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
|
||||||
|
expiration_date (Any, optional): Date in YYYY-MM-DD format. Expired memories are hidden
|
||||||
|
from search and get_all unless show_expired is True.
|
||||||
infer (bool, optional): If True (default), an LLM is used to extract key facts from
|
infer (bool, optional): If True (default), an LLM is used to extract key facts from
|
||||||
'messages' and decide whether to add, update, or delete related memories.
|
'messages' and decide whether to add, update, or delete related memories.
|
||||||
If False, 'messages' are added as raw memories directly.
|
If False, 'messages' are added as raw memories directly.
|
||||||
@@ -737,6 +768,7 @@ class Memory(MemoryBase):
|
|||||||
if timestamp is not None:
|
if timestamp is not None:
|
||||||
raise ValueError(get_temporal_feature_error_message("sync", "add", "timestamp"))
|
raise ValueError(get_temporal_feature_error_message("sync", "add", "timestamp"))
|
||||||
|
|
||||||
|
normalized_expiration_date = _normalize_expiration_date(expiration_date)
|
||||||
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
|
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
|
||||||
processed_metadata, effective_filters = _build_filters_and_metadata(
|
processed_metadata, effective_filters = _build_filters_and_metadata(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@@ -744,6 +776,8 @@ class Memory(MemoryBase):
|
|||||||
run_id=run_id,
|
run_id=run_id,
|
||||||
input_metadata=metadata,
|
input_metadata=metadata,
|
||||||
)
|
)
|
||||||
|
if normalized_expiration_date is not None:
|
||||||
|
processed_metadata["expiration_date"] = normalized_expiration_date
|
||||||
|
|
||||||
if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
|
if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
|
||||||
raise Mem0ValidationError(
|
raise Mem0ValidationError(
|
||||||
@@ -1141,6 +1175,7 @@ class Memory(MemoryBase):
|
|||||||
"actor_id",
|
"actor_id",
|
||||||
"role",
|
"role",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
|
"expiration_date",
|
||||||
]
|
]
|
||||||
|
|
||||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
||||||
@@ -1169,6 +1204,7 @@ class Memory(MemoryBase):
|
|||||||
*,
|
*,
|
||||||
filters: Optional[Dict[str, Any]] = None,
|
filters: Optional[Dict[str, Any]] = None,
|
||||||
top_k: int = 20,
|
top_k: int = 20,
|
||||||
|
show_expired: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -1179,6 +1215,7 @@ class Memory(MemoryBase):
|
|||||||
Must contain at least one of: user_id, agent_id, run_id.
|
Must contain at least one of: user_id, agent_id, run_id.
|
||||||
Example: filters={"user_id": "u1", "agent_id": "a1"}
|
Example: filters={"user_id": "u1", "agent_id": "a1"}
|
||||||
top_k (int, optional): The maximum number of memories to return. Defaults to 20.
|
top_k (int, optional): The maximum number of memories to return. Defaults to 20.
|
||||||
|
show_expired (bool, optional): Include expired memories. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary containing a list of memories under the "results" key.
|
dict: A dictionary containing a list of memories under the "results" key.
|
||||||
@@ -1217,6 +1254,7 @@ class Memory(MemoryBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
limit = top_k
|
limit = top_k
|
||||||
|
fetch_limit = limit if show_expired else max(limit * 4, 60)
|
||||||
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
|
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
|
||||||
|
|
||||||
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
||||||
@@ -1224,7 +1262,7 @@ class Memory(MemoryBase):
|
|||||||
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"}
|
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "sync"}
|
||||||
)
|
)
|
||||||
|
|
||||||
all_memories_result = self._get_all_from_vector_store(effective_filters, limit)
|
all_memories_result = self._get_all_from_vector_store(effective_filters, fetch_limit, show_expired, limit)
|
||||||
|
|
||||||
if scale_threshold_notice:
|
if scale_threshold_notice:
|
||||||
display_scale_threshold_notice(self, "sync", "get_all", *scale_threshold_notice)
|
display_scale_threshold_notice(self, "sync", "get_all", *scale_threshold_notice)
|
||||||
@@ -1232,7 +1270,7 @@ class Memory(MemoryBase):
|
|||||||
display_first_run_notice(self, "sync", "get_all")
|
display_first_run_notice(self, "sync", "get_all")
|
||||||
return {"results": all_memories_result}
|
return {"results": all_memories_result}
|
||||||
|
|
||||||
def _get_all_from_vector_store(self, filters, limit):
|
def _get_all_from_vector_store(self, filters, limit, show_expired=False, output_limit=None):
|
||||||
memories_result = self.vector_store.list(filters=filters, top_k=limit)
|
memories_result = self.vector_store.list(filters=filters, top_k=limit)
|
||||||
|
|
||||||
# Handle different vector store return formats by inspecting first element
|
# Handle different vector store return formats by inspecting first element
|
||||||
@@ -1255,11 +1293,14 @@ class Memory(MemoryBase):
|
|||||||
"actor_id",
|
"actor_id",
|
||||||
"role",
|
"role",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
|
"expiration_date",
|
||||||
]
|
]
|
||||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
||||||
|
|
||||||
formatted_memories = []
|
formatted_memories = []
|
||||||
for mem in actual_memories:
|
for mem in actual_memories:
|
||||||
|
if not show_expired and _payload_is_expired(mem.payload):
|
||||||
|
continue
|
||||||
memory_item_dict = MemoryItem(
|
memory_item_dict = MemoryItem(
|
||||||
id=mem.id,
|
id=mem.id,
|
||||||
memory=mem.payload.get("data", ""),
|
memory=mem.payload.get("data", ""),
|
||||||
@@ -1277,6 +1318,8 @@ class Memory(MemoryBase):
|
|||||||
memory_item_dict["metadata"] = additional_metadata
|
memory_item_dict["metadata"] = additional_metadata
|
||||||
|
|
||||||
formatted_memories.append(memory_item_dict)
|
formatted_memories.append(memory_item_dict)
|
||||||
|
if output_limit is not None and len(formatted_memories) >= output_limit:
|
||||||
|
break
|
||||||
|
|
||||||
return formatted_memories
|
return formatted_memories
|
||||||
|
|
||||||
@@ -1290,6 +1333,7 @@ class Memory(MemoryBase):
|
|||||||
rerank: bool = False,
|
rerank: bool = False,
|
||||||
explain: bool = False,
|
explain: bool = False,
|
||||||
reference_date: Optional[Any] = None,
|
reference_date: Optional[Any] = None,
|
||||||
|
show_expired: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -1322,6 +1366,7 @@ class Memory(MemoryBase):
|
|||||||
rerank (bool, optional): Whether to rerank results. Defaults to False.
|
rerank (bool, optional): Whether to rerank results. Defaults to False.
|
||||||
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
|
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
|
||||||
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
|
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
|
||||||
|
show_expired (bool, optional): Include expired memories. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary containing the search results under a "results" key.
|
dict: A dictionary containing the search results under a "results" key.
|
||||||
@@ -1393,7 +1438,9 @@ class Memory(MemoryBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
search_start = time.perf_counter()
|
search_start = time.perf_counter()
|
||||||
original_memories = self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
|
original_memories = self._search_vector_store(
|
||||||
|
query, effective_filters, limit, threshold, explain=explain, show_expired=show_expired
|
||||||
|
)
|
||||||
search_elapsed_seconds = time.perf_counter() - search_start
|
search_elapsed_seconds = time.perf_counter() - search_start
|
||||||
|
|
||||||
# Apply reranking if enabled and reranker is available
|
# Apply reranking if enabled and reranker is available
|
||||||
@@ -1525,7 +1572,7 @@ class Memory(MemoryBase):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False):
|
def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False, show_expired=False):
|
||||||
# Guard against None threshold (backward compat)
|
# Guard against None threshold (backward compat)
|
||||||
if threshold is None:
|
if threshold is None:
|
||||||
threshold = 0.1
|
threshold = 0.1
|
||||||
@@ -1566,11 +1613,14 @@ class Memory(MemoryBase):
|
|||||||
# Step 7: Build candidate set from semantic results
|
# Step 7: Build candidate set from semantic results
|
||||||
candidates = []
|
candidates = []
|
||||||
for mem in semantic_results:
|
for mem in semantic_results:
|
||||||
|
payload = mem.payload if hasattr(mem, 'payload') else {}
|
||||||
|
if not show_expired and _payload_is_expired(payload):
|
||||||
|
continue
|
||||||
mem_id = str(mem.id)
|
mem_id = str(mem.id)
|
||||||
candidates.append({
|
candidates.append({
|
||||||
"id": mem_id,
|
"id": mem_id,
|
||||||
"score": mem.score,
|
"score": mem.score,
|
||||||
"payload": mem.payload if hasattr(mem, 'payload') else {},
|
"payload": payload,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Step 8: Score and rank
|
# Step 8: Score and rank
|
||||||
@@ -1591,6 +1641,7 @@ class Memory(MemoryBase):
|
|||||||
"actor_id",
|
"actor_id",
|
||||||
"role",
|
"role",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
|
"expiration_date",
|
||||||
]
|
]
|
||||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
||||||
|
|
||||||
@@ -1708,14 +1759,21 @@ class Memory(MemoryBase):
|
|||||||
|
|
||||||
return memory_boosts
|
return memory_boosts
|
||||||
|
|
||||||
def update(self, memory_id, data, metadata: Optional[Dict[str, Any]] = None):
|
def update(
|
||||||
|
self,
|
||||||
|
memory_id,
|
||||||
|
data: Optional[str] = None,
|
||||||
|
metadata: Optional[Dict[str, Any]] = None,
|
||||||
|
expiration_date: Any = _UNSET,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Update a memory by ID.
|
Update a memory by ID.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
memory_id (str): ID of the memory to update.
|
memory_id (str): ID of the memory to update.
|
||||||
data (str): New content to update the memory with.
|
data (str, optional): New content to update the memory with.
|
||||||
metadata (dict, optional): Metadata to update with the memory. Defaults to None.
|
metadata (dict, optional): Metadata to update with the memory. Defaults to None.
|
||||||
|
expiration_date (Any, optional): Date in YYYY-MM-DD format, or None to clear it.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Success message indicating the memory was updated.
|
dict: Success message indicating the memory was updated.
|
||||||
@@ -1726,9 +1784,19 @@ class Memory(MemoryBase):
|
|||||||
"""
|
"""
|
||||||
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "sync"})
|
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "sync"})
|
||||||
|
|
||||||
existing_embeddings = {data: self.embedding_model.embed(data, "update")}
|
if data is None and metadata is None and expiration_date is _UNSET:
|
||||||
|
raise ValueError("At least one of data, metadata, or expiration_date must be provided.")
|
||||||
|
|
||||||
self._update_memory(memory_id, data, existing_embeddings, metadata)
|
update_metadata = deepcopy(metadata) if metadata is not None else None
|
||||||
|
if expiration_date is not _UNSET:
|
||||||
|
update_metadata = update_metadata or {}
|
||||||
|
update_metadata["expiration_date"] = _normalize_expiration_date(expiration_date)
|
||||||
|
|
||||||
|
existing_embeddings = {}
|
||||||
|
if data is not None:
|
||||||
|
existing_embeddings[data] = self.embedding_model.embed(data, "update")
|
||||||
|
|
||||||
|
self._update_memory(memory_id, data, existing_embeddings, update_metadata)
|
||||||
display_first_run_notice(self, "sync", "update")
|
display_first_run_notice(self, "sync", "update")
|
||||||
return {"message": "Memory updated successfully!"}
|
return {"message": "Memory updated successfully!"}
|
||||||
|
|
||||||
@@ -1762,6 +1830,10 @@ class Memory(MemoryBase):
|
|||||||
agent_id (str, optional): ID of the agent to delete memories for. Defaults to None.
|
agent_id (str, optional): ID of the agent to delete memories for. Defaults to None.
|
||||||
run_id (str, optional): ID of the run to delete memories for. Defaults to None.
|
run_id (str, optional): ID of the run to delete memories for. Defaults to None.
|
||||||
"""
|
"""
|
||||||
|
user_id = _validate_and_trim_entity_id(user_id, "user_id")
|
||||||
|
agent_id = _validate_and_trim_entity_id(agent_id, "agent_id")
|
||||||
|
run_id = _validate_and_trim_entity_id(run_id, "run_id")
|
||||||
|
|
||||||
filters: Dict[str, Any] = {}
|
filters: Dict[str, Any] = {}
|
||||||
if user_id:
|
if user_id:
|
||||||
filters["user_id"] = user_id
|
filters["user_id"] = user_id
|
||||||
@@ -1891,6 +1963,11 @@ class Memory(MemoryBase):
|
|||||||
raise ValueError(f"Memory with id {memory_id} not found. Please provide a valid 'memory_id'")
|
raise ValueError(f"Memory with id {memory_id} not found. Please provide a valid 'memory_id'")
|
||||||
|
|
||||||
prev_value = existing_memory.payload.get("data")
|
prev_value = existing_memory.payload.get("data")
|
||||||
|
if data is None:
|
||||||
|
data = prev_value
|
||||||
|
if not isinstance(data, str):
|
||||||
|
raise ValueError(f"Memory with id {memory_id} does not have text content to update")
|
||||||
|
text_changed = data != prev_value
|
||||||
|
|
||||||
new_metadata = deepcopy(existing_memory.payload)
|
new_metadata = deepcopy(existing_memory.payload)
|
||||||
if metadata is not None:
|
if metadata is not None:
|
||||||
@@ -1932,8 +2009,9 @@ class Memory(MemoryBase):
|
|||||||
# Entity-store cleanup: strip this memory's id from old-text entities,
|
# Entity-store cleanup: strip this memory's id from old-text entities,
|
||||||
# then re-extract entities from the new text and link them back.
|
# then re-extract entities from the new text and link them back.
|
||||||
session_filters = {k: new_metadata[k] for k in ("user_id", "agent_id", "run_id") if new_metadata.get(k)}
|
session_filters = {k: new_metadata[k] for k in ("user_id", "agent_id", "run_id") if new_metadata.get(k)}
|
||||||
self._remove_memory_from_entity_store(memory_id, session_filters)
|
if text_changed:
|
||||||
self._link_entities_for_memory(memory_id, data, session_filters)
|
self._remove_memory_from_entity_store(memory_id, session_filters)
|
||||||
|
self._link_entities_for_memory(memory_id, data, session_filters)
|
||||||
|
|
||||||
return memory_id
|
return memory_id
|
||||||
|
|
||||||
@@ -2280,6 +2358,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
run_id: Optional[str] = None,
|
run_id: Optional[str] = None,
|
||||||
metadata: Optional[Dict[str, Any]] = None,
|
metadata: Optional[Dict[str, Any]] = None,
|
||||||
timestamp: Optional[Any] = None,
|
timestamp: Optional[Any] = None,
|
||||||
|
expiration_date: Optional[Any] = None,
|
||||||
infer: bool = True,
|
infer: bool = True,
|
||||||
memory_type: Optional[str] = None,
|
memory_type: Optional[str] = None,
|
||||||
prompt: Optional[str] = None,
|
prompt: Optional[str] = None,
|
||||||
@@ -2295,6 +2374,8 @@ class AsyncMemory(MemoryBase):
|
|||||||
run_id (str, optional): ID of the run creating the memory. Defaults to None.
|
run_id (str, optional): ID of the run creating the memory. Defaults to None.
|
||||||
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
|
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
|
||||||
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
|
timestamp (Any, optional): Platform-only temporal parameter. Not supported in OSS.
|
||||||
|
expiration_date (Any, optional): Date in YYYY-MM-DD format. Expired memories are hidden
|
||||||
|
from search and get_all unless show_expired is True.
|
||||||
infer (bool, optional): Whether to infer the memories. Defaults to True.
|
infer (bool, optional): Whether to infer the memories. Defaults to True.
|
||||||
memory_type (str, optional): Type of memory to create. Defaults to None.
|
memory_type (str, optional): Type of memory to create. Defaults to None.
|
||||||
Pass "procedural_memory" to create procedural memories.
|
Pass "procedural_memory" to create procedural memories.
|
||||||
@@ -2306,10 +2387,13 @@ class AsyncMemory(MemoryBase):
|
|||||||
if timestamp is not None:
|
if timestamp is not None:
|
||||||
raise ValueError(await get_temporal_feature_error_message_async("async", "add", "timestamp"))
|
raise ValueError(await get_temporal_feature_error_message_async("async", "add", "timestamp"))
|
||||||
|
|
||||||
|
normalized_expiration_date = _normalize_expiration_date(expiration_date)
|
||||||
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
|
temporal_usage_notice = detect_temporal_usage_from_metadata(metadata)
|
||||||
processed_metadata, effective_filters = _build_filters_and_metadata(
|
processed_metadata, effective_filters = _build_filters_and_metadata(
|
||||||
user_id=user_id, agent_id=agent_id, run_id=run_id, input_metadata=metadata
|
user_id=user_id, agent_id=agent_id, run_id=run_id, input_metadata=metadata
|
||||||
)
|
)
|
||||||
|
if normalized_expiration_date is not None:
|
||||||
|
processed_metadata["expiration_date"] = normalized_expiration_date
|
||||||
|
|
||||||
if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
|
if memory_type is not None and memory_type != MemoryType.PROCEDURAL.value:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@@ -2712,6 +2796,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
"actor_id",
|
"actor_id",
|
||||||
"role",
|
"role",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
|
"expiration_date",
|
||||||
]
|
]
|
||||||
|
|
||||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
||||||
@@ -2740,6 +2825,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
*,
|
*,
|
||||||
filters: Optional[Dict[str, Any]] = None,
|
filters: Optional[Dict[str, Any]] = None,
|
||||||
top_k: int = 20,
|
top_k: int = 20,
|
||||||
|
show_expired: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -2750,6 +2836,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
Must contain at least one of: user_id, agent_id, run_id.
|
Must contain at least one of: user_id, agent_id, run_id.
|
||||||
Example: filters={"user_id": "u1", "agent_id": "a1"}
|
Example: filters={"user_id": "u1", "agent_id": "a1"}
|
||||||
top_k (int, optional): The maximum number of memories to return. Defaults to 20.
|
top_k (int, optional): The maximum number of memories to return. Defaults to 20.
|
||||||
|
show_expired (bool, optional): Include expired memories. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary containing a list of memories under the "results" key.
|
dict: A dictionary containing a list of memories under the "results" key.
|
||||||
@@ -2788,6 +2875,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
limit = top_k
|
limit = top_k
|
||||||
|
fetch_limit = limit if show_expired else max(limit * 4, 60)
|
||||||
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
|
scale_threshold_notice = detect_scale_threshold_from_top_k(top_k)
|
||||||
|
|
||||||
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
keys, encoded_ids = process_telemetry_filters(effective_filters)
|
||||||
@@ -2795,7 +2883,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"}
|
"mem0.get_all", self, {"limit": limit, "keys": keys, "encoded_ids": encoded_ids, "sync_type": "async"}
|
||||||
)
|
)
|
||||||
|
|
||||||
all_memories_result = await self._get_all_from_vector_store(effective_filters, limit)
|
all_memories_result = await self._get_all_from_vector_store(effective_filters, fetch_limit, show_expired, limit)
|
||||||
|
|
||||||
if scale_threshold_notice:
|
if scale_threshold_notice:
|
||||||
await display_scale_threshold_notice_async(self, "async", "get_all", *scale_threshold_notice)
|
await display_scale_threshold_notice_async(self, "async", "get_all", *scale_threshold_notice)
|
||||||
@@ -2803,7 +2891,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
await display_first_run_notice_async(self, "async", "get_all")
|
await display_first_run_notice_async(self, "async", "get_all")
|
||||||
return {"results": all_memories_result}
|
return {"results": all_memories_result}
|
||||||
|
|
||||||
async def _get_all_from_vector_store(self, filters, limit):
|
async def _get_all_from_vector_store(self, filters, limit, show_expired=False, output_limit=None):
|
||||||
memories_result = await asyncio.to_thread(self.vector_store.list, filters=filters, top_k=limit)
|
memories_result = await asyncio.to_thread(self.vector_store.list, filters=filters, top_k=limit)
|
||||||
|
|
||||||
# Handle different vector store return formats by inspecting first element
|
# Handle different vector store return formats by inspecting first element
|
||||||
@@ -2826,11 +2914,14 @@ class AsyncMemory(MemoryBase):
|
|||||||
"actor_id",
|
"actor_id",
|
||||||
"role",
|
"role",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
|
"expiration_date",
|
||||||
]
|
]
|
||||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
||||||
|
|
||||||
formatted_memories = []
|
formatted_memories = []
|
||||||
for mem in actual_memories:
|
for mem in actual_memories:
|
||||||
|
if not show_expired and _payload_is_expired(mem.payload):
|
||||||
|
continue
|
||||||
memory_item_dict = MemoryItem(
|
memory_item_dict = MemoryItem(
|
||||||
id=mem.id,
|
id=mem.id,
|
||||||
memory=mem.payload.get("data", ""),
|
memory=mem.payload.get("data", ""),
|
||||||
@@ -2848,6 +2939,8 @@ class AsyncMemory(MemoryBase):
|
|||||||
memory_item_dict["metadata"] = additional_metadata
|
memory_item_dict["metadata"] = additional_metadata
|
||||||
|
|
||||||
formatted_memories.append(memory_item_dict)
|
formatted_memories.append(memory_item_dict)
|
||||||
|
if output_limit is not None and len(formatted_memories) >= output_limit:
|
||||||
|
break
|
||||||
|
|
||||||
return formatted_memories
|
return formatted_memories
|
||||||
|
|
||||||
@@ -2861,6 +2954,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
rerank: bool = False,
|
rerank: bool = False,
|
||||||
explain: bool = False,
|
explain: bool = False,
|
||||||
reference_date: Optional[Any] = None,
|
reference_date: Optional[Any] = None,
|
||||||
|
show_expired: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
@@ -2893,6 +2987,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
rerank (bool, optional): Whether to rerank results. Defaults to False.
|
rerank (bool, optional): Whether to rerank results. Defaults to False.
|
||||||
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
|
explain (bool, optional): Whether to include score_details for each result. Defaults to False.
|
||||||
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
|
reference_date (Any, optional): Platform-only temporal parameter. Not supported in OSS.
|
||||||
|
show_expired (bool, optional): Include expired memories. Defaults to False.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A dictionary containing the search results under a "results" key.
|
dict: A dictionary containing the search results under a "results" key.
|
||||||
@@ -2968,7 +3063,9 @@ class AsyncMemory(MemoryBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
search_start = time.perf_counter()
|
search_start = time.perf_counter()
|
||||||
original_memories = await self._search_vector_store(query, effective_filters, limit, threshold, explain=explain)
|
original_memories = await self._search_vector_store(
|
||||||
|
query, effective_filters, limit, threshold, explain=explain, show_expired=show_expired
|
||||||
|
)
|
||||||
search_elapsed_seconds = time.perf_counter() - search_start
|
search_elapsed_seconds = time.perf_counter() - search_start
|
||||||
|
|
||||||
# Apply reranking if enabled and reranker is available
|
# Apply reranking if enabled and reranker is available
|
||||||
@@ -3103,7 +3200,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False):
|
async def _search_vector_store(self, query, filters, limit, threshold=0.1, explain=False, show_expired=False):
|
||||||
if threshold is None:
|
if threshold is None:
|
||||||
threshold = 0.1
|
threshold = 0.1
|
||||||
|
|
||||||
@@ -3143,11 +3240,14 @@ class AsyncMemory(MemoryBase):
|
|||||||
# Step 7: Build candidate set from semantic results
|
# Step 7: Build candidate set from semantic results
|
||||||
candidates = []
|
candidates = []
|
||||||
for mem in semantic_results:
|
for mem in semantic_results:
|
||||||
|
payload = mem.payload if hasattr(mem, 'payload') else {}
|
||||||
|
if not show_expired and _payload_is_expired(payload):
|
||||||
|
continue
|
||||||
mem_id = str(mem.id)
|
mem_id = str(mem.id)
|
||||||
candidates.append({
|
candidates.append({
|
||||||
"id": mem_id,
|
"id": mem_id,
|
||||||
"score": mem.score,
|
"score": mem.score,
|
||||||
"payload": mem.payload if hasattr(mem, 'payload') else {},
|
"payload": payload,
|
||||||
})
|
})
|
||||||
|
|
||||||
# Step 8: Score and rank
|
# Step 8: Score and rank
|
||||||
@@ -3168,6 +3268,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
"actor_id",
|
"actor_id",
|
||||||
"role",
|
"role",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
|
"expiration_date",
|
||||||
]
|
]
|
||||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
||||||
|
|
||||||
@@ -3276,14 +3377,21 @@ class AsyncMemory(MemoryBase):
|
|||||||
|
|
||||||
return memory_boosts
|
return memory_boosts
|
||||||
|
|
||||||
async def update(self, memory_id, data, metadata: Optional[Dict[str, Any]] = None):
|
async def update(
|
||||||
|
self,
|
||||||
|
memory_id,
|
||||||
|
data: Optional[str] = None,
|
||||||
|
metadata: Optional[Dict[str, Any]] = None,
|
||||||
|
expiration_date: Any = _UNSET,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Update a memory by ID asynchronously.
|
Update a memory by ID asynchronously.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
memory_id (str): ID of the memory to update.
|
memory_id (str): ID of the memory to update.
|
||||||
data (str): New content to update the memory with.
|
data (str, optional): New content to update the memory with.
|
||||||
metadata (dict, optional): Metadata to update with the memory. Defaults to None.
|
metadata (dict, optional): Metadata to update with the memory. Defaults to None.
|
||||||
|
expiration_date (Any, optional): Date in YYYY-MM-DD format, or None to clear it.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Success message indicating the memory was updated.
|
dict: Success message indicating the memory was updated.
|
||||||
@@ -3294,10 +3402,20 @@ class AsyncMemory(MemoryBase):
|
|||||||
"""
|
"""
|
||||||
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "async"})
|
capture_event("mem0.update", self, {"memory_id": memory_id, "sync_type": "async"})
|
||||||
|
|
||||||
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update")
|
if data is None and metadata is None and expiration_date is _UNSET:
|
||||||
existing_embeddings = {data: embeddings}
|
raise ValueError("At least one of data, metadata, or expiration_date must be provided.")
|
||||||
|
|
||||||
await self._update_memory(memory_id, data, existing_embeddings, metadata)
|
update_metadata = deepcopy(metadata) if metadata is not None else None
|
||||||
|
if expiration_date is not _UNSET:
|
||||||
|
update_metadata = update_metadata or {}
|
||||||
|
update_metadata["expiration_date"] = _normalize_expiration_date(expiration_date)
|
||||||
|
|
||||||
|
existing_embeddings = {}
|
||||||
|
if data is not None:
|
||||||
|
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update")
|
||||||
|
existing_embeddings[data] = embeddings
|
||||||
|
|
||||||
|
await self._update_memory(memory_id, data, existing_embeddings, update_metadata)
|
||||||
await display_first_run_notice_async(self, "async", "update")
|
await display_first_run_notice_async(self, "async", "update")
|
||||||
return {"message": "Memory updated successfully!"}
|
return {"message": "Memory updated successfully!"}
|
||||||
|
|
||||||
@@ -3331,6 +3449,10 @@ class AsyncMemory(MemoryBase):
|
|||||||
agent_id (str, optional): ID of the agent to delete memories for. Defaults to None.
|
agent_id (str, optional): ID of the agent to delete memories for. Defaults to None.
|
||||||
run_id (str, optional): ID of the run to delete memories for. Defaults to None.
|
run_id (str, optional): ID of the run to delete memories for. Defaults to None.
|
||||||
"""
|
"""
|
||||||
|
user_id = _validate_and_trim_entity_id(user_id, "user_id")
|
||||||
|
agent_id = _validate_and_trim_entity_id(agent_id, "agent_id")
|
||||||
|
run_id = _validate_and_trim_entity_id(run_id, "run_id")
|
||||||
|
|
||||||
filters = {}
|
filters = {}
|
||||||
if user_id:
|
if user_id:
|
||||||
filters["user_id"] = user_id
|
filters["user_id"] = user_id
|
||||||
@@ -3456,7 +3578,7 @@ class AsyncMemory(MemoryBase):
|
|||||||
if llm is not None:
|
if llm is not None:
|
||||||
parsed_messages = convert_to_messages(parsed_messages)
|
parsed_messages = convert_to_messages(parsed_messages)
|
||||||
response = await asyncio.to_thread(llm.invoke, input=parsed_messages)
|
response = await asyncio.to_thread(llm.invoke, input=parsed_messages)
|
||||||
procedural_memory = response.content
|
procedural_memory = remove_code_blocks(response.content)
|
||||||
else:
|
else:
|
||||||
procedural_memory = await asyncio.to_thread(self.llm.generate_response, messages=parsed_messages)
|
procedural_memory = await asyncio.to_thread(self.llm.generate_response, messages=parsed_messages)
|
||||||
procedural_memory = remove_code_blocks(procedural_memory)
|
procedural_memory = remove_code_blocks(procedural_memory)
|
||||||
@@ -3491,6 +3613,11 @@ class AsyncMemory(MemoryBase):
|
|||||||
raise ValueError(f"Memory with id {memory_id} not found. Please provide a valid 'memory_id'")
|
raise ValueError(f"Memory with id {memory_id} not found. Please provide a valid 'memory_id'")
|
||||||
|
|
||||||
prev_value = existing_memory.payload.get("data")
|
prev_value = existing_memory.payload.get("data")
|
||||||
|
if data is None:
|
||||||
|
data = prev_value
|
||||||
|
if not isinstance(data, str):
|
||||||
|
raise ValueError(f"Memory with id {memory_id} does not have text content to update")
|
||||||
|
text_changed = data != prev_value
|
||||||
|
|
||||||
new_metadata = deepcopy(existing_memory.payload)
|
new_metadata = deepcopy(existing_memory.payload)
|
||||||
if metadata is not None:
|
if metadata is not None:
|
||||||
@@ -3534,8 +3661,9 @@ class AsyncMemory(MemoryBase):
|
|||||||
# Entity-store cleanup: strip this memory's id from old-text entities,
|
# Entity-store cleanup: strip this memory's id from old-text entities,
|
||||||
# then re-extract entities from the new text and link them back.
|
# then re-extract entities from the new text and link them back.
|
||||||
session_filters = {k: new_metadata[k] for k in ("user_id", "agent_id", "run_id") if new_metadata.get(k)}
|
session_filters = {k: new_metadata[k] for k in ("user_id", "agent_id", "run_id") if new_metadata.get(k)}
|
||||||
await self._remove_memory_from_entity_store(memory_id, session_filters)
|
if text_changed:
|
||||||
await self._link_entities_for_memory(memory_id, data, session_filters)
|
await self._remove_memory_from_entity_store(memory_id, session_filters)
|
||||||
|
await self._link_entities_for_memory(memory_id, data, session_filters)
|
||||||
|
|
||||||
return memory_id
|
return memory_id
|
||||||
|
|
||||||
|
|||||||
+26
-3
@@ -1418,7 +1418,30 @@ def _get_provider_memory_count(memory_instance) -> Optional[int]:
|
|||||||
try:
|
try:
|
||||||
col_info = getattr(vector_store, "col_info", None)
|
col_info = getattr(vector_store, "col_info", None)
|
||||||
if callable(col_info):
|
if callable(col_info):
|
||||||
return _extract_count(col_info())
|
collection_name = getattr(vector_store, "collection_name", None)
|
||||||
|
if collection_name is None:
|
||||||
|
schema = getattr(vector_store, "schema", None)
|
||||||
|
if isinstance(schema, dict):
|
||||||
|
index = schema.get("index")
|
||||||
|
if isinstance(index, dict):
|
||||||
|
collection_name = index.get("name")
|
||||||
|
if collection_name is not None:
|
||||||
|
try:
|
||||||
|
info = col_info(collection_name)
|
||||||
|
except TypeError:
|
||||||
|
info = col_info()
|
||||||
|
else:
|
||||||
|
info = col_info()
|
||||||
|
value = _extract_count(info)
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
client = getattr(vector_store, "client", None)
|
||||||
|
client_count = (
|
||||||
|
getattr(client, "count", None) if client is not None and collection_name is not None else None
|
||||||
|
)
|
||||||
|
if callable(client_count):
|
||||||
|
return _extract_count(client_count(index=collection_name))
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -1430,7 +1453,7 @@ def _extract_count(info: Any) -> Optional[int]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if isinstance(info, dict):
|
if isinstance(info, dict):
|
||||||
for key in ("count", "points_count", "vectors_count", "indexed_vectors_count"):
|
for key in ("count", "points_count", "vectors_count", "indexed_vectors_count", "num_docs"):
|
||||||
value = _coerce_nonnegative_int(info.get(key), None)
|
value = _coerce_nonnegative_int(info.get(key), None)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
return value
|
return value
|
||||||
@@ -1445,7 +1468,7 @@ def _extract_count(info: Any) -> Optional[int]:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
for attr in ("count", "points_count", "vectors_count", "indexed_vectors_count"):
|
for attr in ("count", "points_count", "vectors_count", "indexed_vectors_count", "num_docs"):
|
||||||
value = _coerce_nonnegative_int(getattr(info, attr, None), None)
|
value = _coerce_nonnegative_int(getattr(info, attr, None), None)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
return value
|
return value
|
||||||
|
|||||||
@@ -59,6 +59,26 @@ class HuggingFaceReranker(BaseReranker):
|
|||||||
self.model.to(self.device)
|
self.model.to(self.device)
|
||||||
self.model.eval()
|
self.model.eval()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_scores(scores: List[float]) -> List[float]:
|
||||||
|
"""Map raw cross-encoder logits into the [0, 1] range via a sigmoid.
|
||||||
|
|
||||||
|
Cross-encoder rerankers (e.g. ``BAAI/bge-reranker-*``) emit unbounded
|
||||||
|
logits; the documented way to obtain an interpretable [0, 1] relevance
|
||||||
|
score is a per-document sigmoid (``1 / (1 + e^-x)``), which preserves the
|
||||||
|
ranking order.
|
||||||
|
|
||||||
|
This replaces the previous min-max scaling, which produced *set-relative*
|
||||||
|
scores: the lowest-ranked document was always forced to 0.0, and a single
|
||||||
|
document (or any set of tied scores) collapsed to 0.0 — wrongly reporting
|
||||||
|
a result as completely irrelevant. Sigmoid scores each document on its own
|
||||||
|
merit, so those cases are handled naturally.
|
||||||
|
"""
|
||||||
|
if not scores:
|
||||||
|
return []
|
||||||
|
arr = np.asarray(scores, dtype=float)
|
||||||
|
return (1.0 / (1.0 + np.exp(-arr))).tolist()
|
||||||
|
|
||||||
def rerank(self, query: str, documents: List[Dict[str, Any]], top_k: int = None) -> List[Dict[str, Any]]:
|
def rerank(self, query: str, documents: List[Dict[str, Any]], top_k: int = None) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Rerank documents using HuggingFace cross-encoder model.
|
Rerank documents using HuggingFace cross-encoder model.
|
||||||
@@ -118,9 +138,7 @@ class HuggingFaceReranker(BaseReranker):
|
|||||||
|
|
||||||
# Normalize scores if requested
|
# Normalize scores if requested
|
||||||
if self.config.normalize:
|
if self.config.normalize:
|
||||||
scores = np.array(scores)
|
scores = self._normalize_scores(scores)
|
||||||
scores = (scores - scores.min()) / (scores.max() - scores.min() + 1e-8)
|
|
||||||
scores = scores.tolist()
|
|
||||||
|
|
||||||
# Combine documents with scores
|
# Combine documents with scores
|
||||||
doc_score_pairs = list(zip(documents, scores))
|
doc_score_pairs = list(zip(documents, scores))
|
||||||
|
|||||||
@@ -124,11 +124,13 @@ class RedisDB(VectorStoreBase):
|
|||||||
data = []
|
data = []
|
||||||
for vector, payload, id in zip(vectors, payloads, ids):
|
for vector, payload, id in zip(vectors, payloads, ids):
|
||||||
# Start with required fields
|
# Start with required fields
|
||||||
|
created_at_str = payload.get("created_at")
|
||||||
|
created_at_ts = int(datetime.fromisoformat(created_at_str).timestamp()) if created_at_str else 0
|
||||||
entry = {
|
entry = {
|
||||||
"memory_id": id,
|
"memory_id": id,
|
||||||
"hash": payload["hash"],
|
"hash": payload.get("hash", ""),
|
||||||
"memory": payload["data"],
|
"memory": payload.get("data", ""),
|
||||||
"created_at": int(datetime.fromisoformat(payload["created_at"]).timestamp()),
|
"created_at": created_at_ts,
|
||||||
"embedding": np.array(vector, dtype=np.float32).tobytes(),
|
"embedding": np.array(vector, dtype=np.float32).tobytes(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,12 +246,16 @@ class RedisDB(VectorStoreBase):
|
|||||||
self.index.drop_keys(f"{self.schema['index']['prefix']}:{vector_id}")
|
self.index.drop_keys(f"{self.schema['index']['prefix']}:{vector_id}")
|
||||||
|
|
||||||
def update(self, vector_id=None, vector=None, payload=None):
|
def update(self, vector_id=None, vector=None, payload=None):
|
||||||
|
created_at_str = payload.get("created_at")
|
||||||
|
created_at_ts = int(datetime.fromisoformat(created_at_str).timestamp()) if created_at_str else 0
|
||||||
|
updated_at_str = payload.get("updated_at")
|
||||||
|
updated_at_ts = int(datetime.fromisoformat(updated_at_str).timestamp()) if updated_at_str else 0
|
||||||
data = {
|
data = {
|
||||||
"memory_id": vector_id,
|
"memory_id": vector_id,
|
||||||
"hash": payload["hash"],
|
"hash": payload.get("hash", ""),
|
||||||
"memory": payload["data"],
|
"memory": payload.get("data", ""),
|
||||||
"created_at": int(datetime.fromisoformat(payload["created_at"]).timestamp()),
|
"created_at": created_at_ts,
|
||||||
"updated_at": int(datetime.fromisoformat(payload["updated_at"]).timestamp()),
|
"updated_at": updated_at_ts,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only update embedding if vector is provided
|
# Only update embedding if vector is provided
|
||||||
|
|||||||
@@ -41,7 +41,20 @@ class OutputData(BaseModel):
|
|||||||
payload: Dict
|
payload: Dict
|
||||||
|
|
||||||
|
|
||||||
|
_VALKEY_TAG_SPECIAL = set(r',.<>{}[]"\':;!@#$%^&*()-+=~| ')
|
||||||
|
|
||||||
|
|
||||||
class ValkeyDB(VectorStoreBase):
|
class ValkeyDB(VectorStoreBase):
|
||||||
|
@staticmethod
|
||||||
|
def _escape_tag_value(value):
|
||||||
|
"""Escape special characters in a Valkey FT.SEARCH tag filter value.
|
||||||
|
|
||||||
|
Without escaping, characters like * (wildcard) or | (OR) alter query
|
||||||
|
semantics and can bypass tenant-isolation filters.
|
||||||
|
"""
|
||||||
|
s = str(value)
|
||||||
|
return "".join(f"\\{c}" if c in _VALKEY_TAG_SPECIAL else c for c in s)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
valkey_url: str,
|
valkey_url: str,
|
||||||
@@ -329,8 +342,8 @@ class ValkeyDB(VectorStoreBase):
|
|||||||
knn_part (str): The KNN part of the query.
|
knn_part (str): The KNN part of the query.
|
||||||
filters (dict, optional): Filters to apply to the search. Each key-value pair
|
filters (dict, optional): Filters to apply to the search. Each key-value pair
|
||||||
becomes a tag filter (@key:{value}). None values are ignored.
|
becomes a tag filter (@key:{value}). None values are ignored.
|
||||||
Values are used as-is (no validation) - wildcards, lists, etc. are
|
Values are escaped via _escape_tag_value() before interpolation
|
||||||
passed through literally to Valkey search. Multiple filters are
|
to prevent wildcard/operator injection. Multiple filters are
|
||||||
combined with AND logic (space-separated).
|
combined with AND logic (space-separated).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -345,8 +358,8 @@ class ValkeyDB(VectorStoreBase):
|
|||||||
filter_parts = []
|
filter_parts = []
|
||||||
for key, value in filters.items():
|
for key, value in filters.items():
|
||||||
if value is not None:
|
if value is not None:
|
||||||
# Use the correct filter syntax for Valkey
|
escaped = self._escape_tag_value(value)
|
||||||
filter_parts.append(f"@{key}:{{{value}}}")
|
filter_parts.append(f"@{key}:{{{escaped}}}")
|
||||||
|
|
||||||
# No valid filter parts
|
# No valid filter parts
|
||||||
if not filter_parts:
|
if not filter_parts:
|
||||||
@@ -750,34 +763,6 @@ class ValkeyDB(VectorStoreBase):
|
|||||||
logger.exception(f"Error resetting index {self.collection_name}: {e}")
|
logger.exception(f"Error resetting index {self.collection_name}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _build_list_query(self, filters=None):
|
|
||||||
"""
|
|
||||||
Build a query for listing vectors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filters (dict, optional): Filters to apply to the list. Each key-value pair
|
|
||||||
becomes a tag filter (@key:{value}). None values are ignored.
|
|
||||||
Values are used as-is (no validation) - wildcards, lists, etc. are
|
|
||||||
passed through literally to Valkey search.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The query string. Returns "*" if no valid filters provided.
|
|
||||||
"""
|
|
||||||
# Default query
|
|
||||||
q = "*"
|
|
||||||
|
|
||||||
# Add filters if provided
|
|
||||||
if filters and any(value is not None for key, value in filters.items()):
|
|
||||||
filter_conditions = []
|
|
||||||
for key, value in filters.items():
|
|
||||||
if value is not None:
|
|
||||||
filter_conditions.append(f"@{key}:{{{value}}}")
|
|
||||||
|
|
||||||
if filter_conditions:
|
|
||||||
q = " ".join(filter_conditions)
|
|
||||||
|
|
||||||
return q
|
|
||||||
|
|
||||||
def list(self, filters: dict = None, top_k: int = None) -> list:
|
def list(self, filters: dict = None, top_k: int = None) -> list:
|
||||||
"""
|
"""
|
||||||
List all recent created memories from the vector store.
|
List all recent created memories from the vector store.
|
||||||
@@ -785,8 +770,8 @@ class ValkeyDB(VectorStoreBase):
|
|||||||
Args:
|
Args:
|
||||||
filters (dict, optional): Filters to apply to the list. Each key-value pair
|
filters (dict, optional): Filters to apply to the list. Each key-value pair
|
||||||
becomes a tag filter (@key:{value}). None values are ignored.
|
becomes a tag filter (@key:{value}). None values are ignored.
|
||||||
Values are used as-is without validation - wildcards, special characters,
|
Values are escaped via _escape_tag_value() before interpolation
|
||||||
lists, etc. are passed through literally to Valkey search.
|
to prevent wildcard/operator injection.
|
||||||
Multiple filters are combined with AND logic.
|
Multiple filters are combined with AND logic.
|
||||||
top_k (int, optional): Maximum number of results to return. Defaults to 1000
|
top_k (int, optional): Maximum number of results to return. Defaults to 1000
|
||||||
if not specified.
|
if not specified.
|
||||||
|
|||||||
+20
-5
@@ -182,14 +182,16 @@ class MemoryCreate(BaseModel):
|
|||||||
agent_id: Optional[str] = None
|
agent_id: Optional[str] = None
|
||||||
run_id: Optional[str] = None
|
run_id: Optional[str] = None
|
||||||
metadata: Optional[Dict[str, Any]] = None
|
metadata: Optional[Dict[str, Any]] = None
|
||||||
|
expiration_date: Optional[str] = Field(None, description="Expiration date in YYYY-MM-DD format.")
|
||||||
infer: Optional[bool] = Field(None, description="Whether to extract facts from messages. Defaults to True.")
|
infer: Optional[bool] = Field(None, description="Whether to extract facts from messages. Defaults to True.")
|
||||||
memory_type: Optional[str] = Field(None, description="Type of memory to store (e.g. 'core').")
|
memory_type: Optional[str] = Field(None, description="Type of memory to store (e.g. 'core').")
|
||||||
prompt: Optional[str] = Field(None, description="Custom prompt to use for fact extraction.")
|
prompt: Optional[str] = Field(None, description="Custom prompt to use for fact extraction.")
|
||||||
|
|
||||||
|
|
||||||
class MemoryUpdate(BaseModel):
|
class MemoryUpdate(BaseModel):
|
||||||
text: str = Field(..., description="New content to update the memory with.")
|
text: Optional[str] = Field(None, description="New content to update the memory with.")
|
||||||
metadata: Optional[Dict[str, Any]] = Field(None, description="Metadata to update.")
|
metadata: Optional[Dict[str, Any]] = Field(None, description="Metadata to update.")
|
||||||
|
expiration_date: Optional[str] = Field(None, description="Expiration date in YYYY-MM-DD format, or null to clear.")
|
||||||
|
|
||||||
|
|
||||||
class SearchRequest(BaseModel):
|
class SearchRequest(BaseModel):
|
||||||
@@ -201,6 +203,7 @@ class SearchRequest(BaseModel):
|
|||||||
top_k: Optional[int] = Field(None, description="Maximum number of results to return.")
|
top_k: Optional[int] = Field(None, description="Maximum number of results to return.")
|
||||||
threshold: Optional[float] = Field(None, description="Minimum similarity score for results.")
|
threshold: Optional[float] = Field(None, description="Minimum similarity score for results.")
|
||||||
explain: Optional[bool] = Field(None, description="Include score details for each search result.")
|
explain: Optional[bool] = Field(None, description="Include score details for each search result.")
|
||||||
|
show_expired: Optional[bool] = Field(None, description="Include expired memories.")
|
||||||
|
|
||||||
|
|
||||||
class GenerateInstructionsRequest(BaseModel):
|
class GenerateInstructionsRequest(BaseModel):
|
||||||
@@ -379,7 +382,7 @@ def add_memory(memory_create: MemoryCreate, _auth=Depends(verify_auth)):
|
|||||||
|
|
||||||
|
|
||||||
ALL_MEMORIES_LIMIT = 1000
|
ALL_MEMORIES_LIMIT = 1000
|
||||||
_RESERVED_PAYLOAD_KEYS = {"data", "user_id", "agent_id", "run_id", "hash", "created_at", "updated_at"}
|
_RESERVED_PAYLOAD_KEYS = {"data", "user_id", "agent_id", "run_id", "hash", "created_at", "updated_at", "expiration_date"}
|
||||||
|
|
||||||
|
|
||||||
def _serialize_memory(row: Any) -> Dict[str, Any]:
|
def _serialize_memory(row: Any) -> Dict[str, Any]:
|
||||||
@@ -391,6 +394,7 @@ def _serialize_memory(row: Any) -> Dict[str, Any]:
|
|||||||
"agent_id": payload.get("agent_id"),
|
"agent_id": payload.get("agent_id"),
|
||||||
"run_id": payload.get("run_id"),
|
"run_id": payload.get("run_id"),
|
||||||
"hash": payload.get("hash"),
|
"hash": payload.get("hash"),
|
||||||
|
"expiration_date": payload.get("expiration_date"),
|
||||||
"metadata": {k: v for k, v in payload.items() if k not in _RESERVED_PAYLOAD_KEYS},
|
"metadata": {k: v for k, v in payload.items() if k not in _RESERVED_PAYLOAD_KEYS},
|
||||||
"created_at": payload.get("created_at"),
|
"created_at": payload.get("created_at"),
|
||||||
"updated_at": payload.get("updated_at"),
|
"updated_at": payload.get("updated_at"),
|
||||||
@@ -410,6 +414,7 @@ def get_all_memories(
|
|||||||
run_id: Optional[str] = None,
|
run_id: Optional[str] = None,
|
||||||
agent_id: Optional[str] = None,
|
agent_id: Optional[str] = None,
|
||||||
top_k: Optional[int] = Query(None, ge=0, le=ALL_MEMORIES_LIMIT),
|
top_k: Optional[int] = Query(None, ge=0, le=ALL_MEMORIES_LIMIT),
|
||||||
|
show_expired: bool = Query(False),
|
||||||
_auth=Depends(verify_auth),
|
_auth=Depends(verify_auth),
|
||||||
):
|
):
|
||||||
"""Retrieve stored memories. Lists all memories when no identifier is provided (admin only)."""
|
"""Retrieve stored memories. Lists all memories when no identifier is provided (admin only)."""
|
||||||
@@ -418,6 +423,7 @@ def get_all_memories(
|
|||||||
auth_type = getattr(request.state, "auth_type", "none")
|
auth_type = getattr(request.state, "auth_type", "none")
|
||||||
if _auth is not None and _auth.role != "admin" and auth_type not in {"admin_api_key", "disabled"}:
|
if _auth is not None and _auth.role != "admin" and auth_type not in {"admin_api_key", "disabled"}:
|
||||||
raise HTTPException(status_code=403, detail="Admin role required to list all memories.")
|
raise HTTPException(status_code=403, detail="Admin role required to list all memories.")
|
||||||
|
# Admin all-memory listing is intentionally raw; scoped get_all below applies expiry visibility.
|
||||||
return _list_all_memories(limit=top_k if top_k is not None else ALL_MEMORIES_LIMIT)
|
return _list_all_memories(limit=top_k if top_k is not None else ALL_MEMORIES_LIMIT)
|
||||||
filters = {
|
filters = {
|
||||||
k: v for k, v in {"user_id": user_id, "run_id": run_id, "agent_id": agent_id}.items() if v is not None
|
k: v for k, v in {"user_id": user_id, "run_id": run_id, "agent_id": agent_id}.items() if v is not None
|
||||||
@@ -425,6 +431,7 @@ def get_all_memories(
|
|||||||
params = {"filters": filters}
|
params = {"filters": filters}
|
||||||
if top_k is not None:
|
if top_k is not None:
|
||||||
params["top_k"] = top_k
|
params["top_k"] = top_k
|
||||||
|
params["show_expired"] = show_expired
|
||||||
return get_memory_instance().get_all(**params)
|
return get_memory_instance().get_all(**params)
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
@@ -465,6 +472,8 @@ def search_memories(search_req: SearchRequest, _auth=Depends(verify_auth)):
|
|||||||
params["threshold"] = search_req.threshold
|
params["threshold"] = search_req.threshold
|
||||||
if search_req.explain is not None:
|
if search_req.explain is not None:
|
||||||
params["explain"] = search_req.explain
|
params["explain"] = search_req.explain
|
||||||
|
if search_req.show_expired is not None:
|
||||||
|
params["show_expired"] = search_req.show_expired
|
||||||
return get_memory_instance().search(query=search_req.query, filters=filters, **params)
|
return get_memory_instance().search(query=search_req.query, filters=filters, **params)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(e))
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
@@ -478,9 +487,15 @@ def search_memories(search_req: SearchRequest, _auth=Depends(verify_auth)):
|
|||||||
def update_memory(memory_id: str, updated_memory: MemoryUpdate, _auth=Depends(verify_auth)):
|
def update_memory(memory_id: str, updated_memory: MemoryUpdate, _auth=Depends(verify_auth)):
|
||||||
"""Update an existing memory."""
|
"""Update an existing memory."""
|
||||||
try:
|
try:
|
||||||
return get_memory_instance().update(
|
fields_set = getattr(updated_memory, "model_fields_set", getattr(updated_memory, "__fields_set__", set()))
|
||||||
memory_id=memory_id, data=updated_memory.text, metadata=updated_memory.metadata
|
params = {"memory_id": memory_id}
|
||||||
)
|
if "text" in fields_set:
|
||||||
|
params["data"] = updated_memory.text
|
||||||
|
if "metadata" in fields_set:
|
||||||
|
params["metadata"] = updated_memory.metadata
|
||||||
|
if "expiration_date" in fields_set:
|
||||||
|
params["expiration_date"] = updated_memory.expiration_date
|
||||||
|
return get_memory_instance().update(**params)
|
||||||
except (ValueError, Mem0ValidationError) as e:
|
except (ValueError, Mem0ValidationError) as e:
|
||||||
raise _client_error(e)
|
raise _client_error(e)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -167,6 +167,33 @@ class TestAsyncUpdate:
|
|||||||
"test_id", "Updated memory", {"Updated memory": [0.1, 0.2, 0.3]}, {}
|
"test_id", "Updated memory", {"Updated memory": [0.1, 0.2, 0.3]}, {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_async_update_can_change_expiration_date_without_changing_text(self, mock_async_memory, mocker):
|
||||||
|
mock_async_memory.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
|
||||||
|
mock_async_memory.vector_store.get = Mock(
|
||||||
|
return_value=Mock(
|
||||||
|
payload={
|
||||||
|
"data": "Existing memory",
|
||||||
|
"user_id": "test_user",
|
||||||
|
"created_at": "2026-01-01T00:00:00+00:00",
|
||||||
|
"expiration_date": "2026-12-31",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
mock_async_memory.vector_store.update = Mock()
|
||||||
|
mock_async_memory.db.add_history = Mock()
|
||||||
|
mock_async_memory._remove_memory_from_entity_store = mocker.AsyncMock()
|
||||||
|
mock_async_memory._link_entities_for_memory = mocker.AsyncMock()
|
||||||
|
|
||||||
|
result = await mock_async_memory.update("test_id", expiration_date="2999-01-01")
|
||||||
|
|
||||||
|
assert result["message"] == "Memory updated successfully!"
|
||||||
|
payload = mock_async_memory.vector_store.update.call_args.kwargs["payload"]
|
||||||
|
assert payload["data"] == "Existing memory"
|
||||||
|
assert payload["expiration_date"] == "2999-01-01"
|
||||||
|
mock_async_memory._remove_memory_from_entity_store.assert_not_called()
|
||||||
|
mock_async_memory._link_entities_for_memory.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
class TestAsyncAddToVectorStoreErrors:
|
class TestAsyncAddToVectorStoreErrors:
|
||||||
|
|||||||
@@ -1403,6 +1403,30 @@ def test_scale_threshold_provider_count_helpers_are_safe():
|
|||||||
def col_info(self):
|
def col_info(self):
|
||||||
return {"count": 2300}
|
return {"count": 2300}
|
||||||
|
|
||||||
|
class RedisInfoStore:
|
||||||
|
def __init__(self):
|
||||||
|
self.schema = {"index": {"name": "test_collection"}}
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
raise RuntimeError("count unavailable")
|
||||||
|
|
||||||
|
def col_info(self, name):
|
||||||
|
assert name == self.schema["index"]["name"]
|
||||||
|
return {"index_name": "test_collection", "num_docs": 2300}
|
||||||
|
|
||||||
|
class SearchMetadataStore:
|
||||||
|
def __init__(self):
|
||||||
|
self.collection_name = "test_collection"
|
||||||
|
self.client = MagicMock()
|
||||||
|
self.client.count.return_value = {"count": 2400}
|
||||||
|
|
||||||
|
def count(self):
|
||||||
|
raise RuntimeError("count unavailable")
|
||||||
|
|
||||||
|
def col_info(self, name):
|
||||||
|
assert name == self.collection_name
|
||||||
|
return {"test_collection": {"settings": {"index": {}}}}
|
||||||
|
|
||||||
memory = MagicMock()
|
memory = MagicMock()
|
||||||
memory.vector_store = CountStore()
|
memory.vector_store = CountStore()
|
||||||
assert notices._get_provider_memory_count(memory) == 2100
|
assert notices._get_provider_memory_count(memory) == 2100
|
||||||
@@ -1410,7 +1434,15 @@ def test_scale_threshold_provider_count_helpers_are_safe():
|
|||||||
memory.vector_store = FallbackStore()
|
memory.vector_store = FallbackStore()
|
||||||
assert notices._get_provider_memory_count(memory) == 2300
|
assert notices._get_provider_memory_count(memory) == 2300
|
||||||
|
|
||||||
assert notices._extract_count({"points_count": 2400}) == 2400
|
memory.vector_store = RedisInfoStore()
|
||||||
|
assert notices._get_provider_memory_count(memory) == 2300
|
||||||
|
|
||||||
|
search_store = SearchMetadataStore()
|
||||||
|
memory.vector_store = search_store
|
||||||
|
assert notices._get_provider_memory_count(memory) == 2400
|
||||||
|
search_store.client.count.assert_called_once_with(index="test_collection")
|
||||||
|
|
||||||
|
assert notices._extract_count({"points_count": 2500}) == 2500
|
||||||
assert notices._extract_count(Info()) == 2200
|
assert notices._extract_count(Info()) == 2200
|
||||||
assert notices._extract_count({"count": -1}) is None
|
assert notices._extract_count({"count": -1}) is None
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""Unit tests for HuggingFaceReranker score normalization.
|
||||||
|
|
||||||
|
These exercise the pure ``_normalize_scores`` helper directly, so they do not
|
||||||
|
require ``transformers`` / ``torch`` to be installed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from mem0.reranker.huggingface_reranker import HuggingFaceReranker
|
||||||
|
|
||||||
|
|
||||||
|
def _sigmoid(x):
|
||||||
|
return 1.0 / (1.0 + math.exp(-x))
|
||||||
|
|
||||||
|
|
||||||
|
class TestHuggingFaceNormalizeScores:
|
||||||
|
def test_logits_mapped_via_sigmoid(self):
|
||||||
|
scores = HuggingFaceReranker._normalize_scores([2.0, 8.0, 5.0])
|
||||||
|
assert scores == pytest.approx([_sigmoid(2.0), _sigmoid(8.0), _sigmoid(5.0)])
|
||||||
|
|
||||||
|
def test_output_bounded_between_zero_and_one(self):
|
||||||
|
for s in HuggingFaceReranker._normalize_scores([-12.0, -1.0, 0.0, 3.0, 15.0]):
|
||||||
|
assert 0.0 <= s <= 1.0
|
||||||
|
|
||||||
|
def test_sigmoid_preserves_ranking_order(self):
|
||||||
|
raw = [1.0, -4.0, 9.0, 2.5]
|
||||||
|
normalized = HuggingFaceReranker._normalize_scores(raw)
|
||||||
|
# argsort of raw and normalized must match — sigmoid is monotonic.
|
||||||
|
assert sorted(range(len(raw)), key=lambda i: raw[i]) == sorted(
|
||||||
|
range(len(normalized)), key=lambda i: normalized[i]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_single_score_not_collapsed_to_zero(self):
|
||||||
|
# Regression: a lone document used to normalize to ~0.0 under min-max.
|
||||||
|
# A positive logit must now yield a clearly-relevant score (> 0.5).
|
||||||
|
(score,) = HuggingFaceReranker._normalize_scores([4.2])
|
||||||
|
assert score == pytest.approx(_sigmoid(4.2))
|
||||||
|
assert score > 0.5
|
||||||
|
|
||||||
|
def test_tied_scores_not_collapsed_to_zero(self):
|
||||||
|
# Regression: tied candidates all collapsed to ~0.0 under min-max.
|
||||||
|
scores = HuggingFaceReranker._normalize_scores([3.0, 3.0, 3.0])
|
||||||
|
assert scores == pytest.approx([_sigmoid(3.0)] * 3)
|
||||||
|
|
||||||
|
def test_zero_logit_maps_to_half(self):
|
||||||
|
assert HuggingFaceReranker._normalize_scores([0.0]) == pytest.approx([0.5])
|
||||||
|
|
||||||
|
def test_empty_scores(self):
|
||||||
|
assert HuggingFaceReranker._normalize_scores([]) == []
|
||||||
@@ -51,6 +51,20 @@ class TestSearchEntityParamRejection:
|
|||||||
json={"query": "test query", "filters": {"user_id": "u1"}},
|
json={"query": "test query", "filters": {"user_id": "u1"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_search_passes_show_expired(self, mock_memory_client):
|
||||||
|
"""search() should pass show_expired to the API."""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.json.return_value = {"results": []}
|
||||||
|
mock_response.raise_for_status.return_value = None
|
||||||
|
mock_memory_client.client.post.return_value = mock_response
|
||||||
|
|
||||||
|
mock_memory_client.search("test query", filters={"user_id": "u1"}, show_expired=True)
|
||||||
|
|
||||||
|
mock_memory_client.client.post.assert_called_once_with(
|
||||||
|
"/v3/memories/search/",
|
||||||
|
json={"query": "test query", "filters": {"user_id": "u1"}, "show_expired": True},
|
||||||
|
)
|
||||||
|
|
||||||
def test_search_rejects_user_id_kwarg(self, mock_memory_client):
|
def test_search_rejects_user_id_kwarg(self, mock_memory_client):
|
||||||
"""search() should reject user_id as top-level kwarg."""
|
"""search() should reject user_id as top-level kwarg."""
|
||||||
with pytest.raises(ValueError, match=r"user_id"):
|
with pytest.raises(ValueError, match=r"user_id"):
|
||||||
@@ -100,6 +114,39 @@ class TestGetAllEntityParamRejection:
|
|||||||
with pytest.raises(ValueError, match=r"run_id"):
|
with pytest.raises(ValueError, match=r"run_id"):
|
||||||
mock_memory_client.get_all(run_id="r1")
|
mock_memory_client.get_all(run_id="r1")
|
||||||
|
|
||||||
|
def test_get_all_passes_show_expired(self, mock_memory_client):
|
||||||
|
"""get_all() should pass show_expired to the API."""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.json.return_value = {"results": []}
|
||||||
|
mock_response.raise_for_status.return_value = None
|
||||||
|
mock_memory_client.client.post.return_value = mock_response
|
||||||
|
|
||||||
|
mock_memory_client.get_all(filters={"user_id": "u1"}, show_expired=True)
|
||||||
|
|
||||||
|
mock_memory_client.client.post.assert_called_once_with(
|
||||||
|
"/v3/memories/",
|
||||||
|
json={"filters": {"user_id": "u1"}, "show_expired": True},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestUpdateExpirationDate:
|
||||||
|
"""Tests for update expiration_date payload handling."""
|
||||||
|
|
||||||
|
def test_update_preserves_null_expiration_date(self, mock_memory_client):
|
||||||
|
"""update() should send expiration_date=None so the API can clear it."""
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.json.return_value = {"id": "mem_1", "expiration_date": None}
|
||||||
|
mock_response.raise_for_status.return_value = None
|
||||||
|
mock_memory_client.client.put.return_value = mock_response
|
||||||
|
|
||||||
|
mock_memory_client.update("mem_1", expiration_date=None)
|
||||||
|
|
||||||
|
mock_memory_client.client.put.assert_called_once_with(
|
||||||
|
"/v1/memories/mem_1/",
|
||||||
|
json={"expiration_date": None},
|
||||||
|
params={},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestFilterOperatorPassthrough:
|
class TestFilterOperatorPassthrough:
|
||||||
"""Tests that AND/OR/NOT filter operators are passed through to the API."""
|
"""Tests that AND/OR/NOT filter operators are passed through to the API."""
|
||||||
|
|||||||
+129
-1
@@ -65,6 +65,24 @@ def test_add(memory_instance):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_stores_expiration_date(memory_instance):
|
||||||
|
memory_instance._add_to_vector_store = Mock(return_value=[{"memory": "Test memory", "event": "ADD"}])
|
||||||
|
|
||||||
|
memory_instance.add(
|
||||||
|
messages=[{"role": "user", "content": "Test message"}],
|
||||||
|
user_id="test_user",
|
||||||
|
expiration_date="2999-01-01",
|
||||||
|
)
|
||||||
|
|
||||||
|
memory_instance._add_to_vector_store.assert_called_once_with(
|
||||||
|
[{"role": "user", "content": "Test message"}],
|
||||||
|
{"user_id": "test_user", "expiration_date": "2999-01-01"},
|
||||||
|
{"user_id": "test_user"},
|
||||||
|
True,
|
||||||
|
prompt=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get(memory_instance):
|
def test_get(memory_instance):
|
||||||
mock_memory = Mock(
|
mock_memory = Mock(
|
||||||
id="test_id",
|
id="test_id",
|
||||||
@@ -117,6 +135,39 @@ def test_search(memory_instance):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_hides_expired_memories_by_default(memory_instance):
|
||||||
|
mock_memories = [
|
||||||
|
Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}, score=0.9),
|
||||||
|
Mock(id="2", payload={"data": "Active memory", "user_id": "test_user", "expiration_date": "2999-01-01"}, score=0.8),
|
||||||
|
]
|
||||||
|
memory_instance.vector_store.search = Mock(return_value=mock_memories)
|
||||||
|
memory_instance.vector_store.keyword_search = Mock(return_value=None)
|
||||||
|
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
|
||||||
|
|
||||||
|
with patch("mem0.memory.main.lemmatize_for_bm25", return_value="test query"), \
|
||||||
|
patch("mem0.memory.main.extract_entities", return_value=[]):
|
||||||
|
result = memory_instance.search("test query", filters={"user_id": "test_user"})
|
||||||
|
|
||||||
|
assert [memory["memory"] for memory in result["results"]] == ["Active memory"]
|
||||||
|
assert result["results"][0]["expiration_date"] == "2999-01-01"
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_can_show_expired_memories(memory_instance):
|
||||||
|
mock_memories = [
|
||||||
|
Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}, score=0.9),
|
||||||
|
Mock(id="2", payload={"data": "Active memory", "user_id": "test_user"}, score=0.8),
|
||||||
|
]
|
||||||
|
memory_instance.vector_store.search = Mock(return_value=mock_memories)
|
||||||
|
memory_instance.vector_store.keyword_search = Mock(return_value=None)
|
||||||
|
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
|
||||||
|
|
||||||
|
with patch("mem0.memory.main.lemmatize_for_bm25", return_value="test query"), \
|
||||||
|
patch("mem0.memory.main.extract_entities", return_value=[]):
|
||||||
|
result = memory_instance.search("test query", filters={"user_id": "test_user"}, show_expired=True)
|
||||||
|
|
||||||
|
assert [memory["memory"] for memory in result["results"]] == ["Expired memory", "Active memory"]
|
||||||
|
|
||||||
|
|
||||||
def test_update(memory_instance):
|
def test_update(memory_instance):
|
||||||
memory_instance.embedding_model = Mock()
|
memory_instance.embedding_model = Mock()
|
||||||
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
|
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
|
||||||
@@ -161,6 +212,42 @@ def test_update_with_empty_metadata(memory_instance):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("expiration_date", "expected_expiration_date"),
|
||||||
|
[
|
||||||
|
("2999-01-01", "2999-01-01"),
|
||||||
|
(None, None),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_update_can_change_expiration_date_without_changing_text(
|
||||||
|
memory_instance, expiration_date, expected_expiration_date
|
||||||
|
):
|
||||||
|
memory_instance.embedding_model.embed = Mock(return_value=[0.1, 0.2, 0.3])
|
||||||
|
memory_instance.vector_store.get = Mock(
|
||||||
|
return_value=Mock(
|
||||||
|
payload={
|
||||||
|
"data": "Existing memory",
|
||||||
|
"user_id": "test_user",
|
||||||
|
"created_at": "2026-01-01T00:00:00+00:00",
|
||||||
|
"expiration_date": "2026-12-31",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
memory_instance.vector_store.update = Mock()
|
||||||
|
memory_instance.db.add_history = Mock()
|
||||||
|
memory_instance._remove_memory_from_entity_store = Mock()
|
||||||
|
memory_instance._link_entities_for_memory = Mock()
|
||||||
|
|
||||||
|
result = memory_instance.update("test_id", expiration_date=expiration_date)
|
||||||
|
|
||||||
|
assert result["message"] == "Memory updated successfully!"
|
||||||
|
payload = memory_instance.vector_store.update.call_args.kwargs["payload"]
|
||||||
|
assert payload["data"] == "Existing memory"
|
||||||
|
assert payload["expiration_date"] == expected_expiration_date
|
||||||
|
memory_instance._remove_memory_from_entity_store.assert_not_called()
|
||||||
|
memory_instance._link_entities_for_memory.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_delete(memory_instance):
|
def test_delete(memory_instance):
|
||||||
memory_instance._delete_memory = Mock()
|
memory_instance._delete_memory = Mock()
|
||||||
|
|
||||||
@@ -200,7 +287,30 @@ def test_get_all(memory_instance):
|
|||||||
assert result["results"][0]["memory"] == "Memory 1"
|
assert result["results"][0]["memory"] == "Memory 1"
|
||||||
assert result["results"][0]["user_id"] == "test_user"
|
assert result["results"][0]["user_id"] == "test_user"
|
||||||
|
|
||||||
memory_instance.vector_store.list.assert_called_once_with(filters={"user_id": "test_user"}, top_k=20)
|
|
||||||
|
def test_get_all_hides_expired_memories_by_default(memory_instance):
|
||||||
|
mock_memories = [
|
||||||
|
Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}),
|
||||||
|
Mock(id="2", payload={"data": "Active memory", "user_id": "test_user", "expiration_date": "2999-01-01"}),
|
||||||
|
]
|
||||||
|
memory_instance.vector_store.list = Mock(return_value=(mock_memories, None))
|
||||||
|
|
||||||
|
result = memory_instance.get_all(filters={"user_id": "test_user"})
|
||||||
|
|
||||||
|
assert [memory["memory"] for memory in result["results"]] == ["Active memory"]
|
||||||
|
assert result["results"][0]["expiration_date"] == "2999-01-01"
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_all_can_show_expired_memories(memory_instance):
|
||||||
|
mock_memories = [
|
||||||
|
Mock(id="1", payload={"data": "Expired memory", "user_id": "test_user", "expiration_date": "2000-01-01"}),
|
||||||
|
Mock(id="2", payload={"data": "Active memory", "user_id": "test_user"}),
|
||||||
|
]
|
||||||
|
memory_instance.vector_store.list = Mock(return_value=(mock_memories, None))
|
||||||
|
|
||||||
|
result = memory_instance.get_all(filters={"user_id": "test_user"}, show_expired=True)
|
||||||
|
|
||||||
|
assert [memory["memory"] for memory in result["results"]] == ["Expired memory", "Active memory"]
|
||||||
|
|
||||||
|
|
||||||
def test_no_telemetry_vector_store_when_disabled():
|
def test_no_telemetry_vector_store_when_disabled():
|
||||||
@@ -286,6 +396,24 @@ class TestEntityIdValidation:
|
|||||||
with pytest.raises(ValueError, match="Invalid user_id.*cannot contain whitespace"):
|
with pytest.raises(ValueError, match="Invalid user_id.*cannot contain whitespace"):
|
||||||
memory_instance.add("test message", user_id="user 123")
|
memory_instance.add("test message", user_id="user 123")
|
||||||
|
|
||||||
|
def test_delete_all_rejects_whitespace_only_user_id(self, memory_instance):
|
||||||
|
"""delete_all should reject whitespace-only user_id."""
|
||||||
|
with pytest.raises(ValueError, match="Invalid user_id.*cannot be empty"):
|
||||||
|
memory_instance.delete_all(user_id=" ")
|
||||||
|
|
||||||
|
def test_delete_all_rejects_internal_whitespace_user_id(self, memory_instance):
|
||||||
|
"""delete_all should reject user_id with internal whitespace."""
|
||||||
|
with pytest.raises(ValueError, match="Invalid user_id.*cannot contain whitespace"):
|
||||||
|
memory_instance.delete_all(user_id="user 123")
|
||||||
|
|
||||||
|
def test_delete_all_trims_user_id_before_list(self, memory_instance):
|
||||||
|
"""delete_all should trim leading/trailing whitespace on entity IDs."""
|
||||||
|
memory_instance.vector_store.list = Mock(return_value=([], None))
|
||||||
|
|
||||||
|
memory_instance.delete_all(user_id=" alice ")
|
||||||
|
|
||||||
|
memory_instance.vector_store.list.assert_called_once_with(filters={"user_id": "alice"})
|
||||||
|
|
||||||
|
|
||||||
class TestSearchParamValidation:
|
class TestSearchParamValidation:
|
||||||
"""Tests for search parameter validation (threshold and top_k)."""
|
"""Tests for search parameter validation (threshold and top_k)."""
|
||||||
|
|||||||
@@ -1496,3 +1496,36 @@ class TestAsyncDeleteAllEntityRace:
|
|||||||
mock_entity_store.delete.assert_called_once_with(vector_id="entity-alice")
|
mock_entity_store.delete.assert_called_once_with(vector_id="entity-alice")
|
||||||
|
|
||||||
assert mock_vector_store.delete.call_count == 2
|
assert mock_vector_store.delete.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@patch("mem0.memory.main.VectorStoreFactory")
|
||||||
|
@patch("mem0.memory.main.EmbedderFactory")
|
||||||
|
@patch("mem0.memory.main.LlmFactory")
|
||||||
|
async def test_async_procedural_memory_langchain_strips_code_blocks(mock_llm_factory, mock_emb, mock_vs):
|
||||||
|
"""Regression #5710: async LangChain path must call remove_code_blocks()."""
|
||||||
|
mock_vs.return_value = MagicMock()
|
||||||
|
mock_emb.return_value = MagicMock()
|
||||||
|
mock_emb.return_value.embed.return_value = [0.1] * 1536
|
||||||
|
mock_llm_factory.return_value = MagicMock()
|
||||||
|
|
||||||
|
from mem0.memory.main import AsyncMemory
|
||||||
|
|
||||||
|
config = MemoryConfig()
|
||||||
|
memory = AsyncMemory(config)
|
||||||
|
memory.vector_store = MagicMock()
|
||||||
|
memory.vector_store.insert = MagicMock()
|
||||||
|
|
||||||
|
mock_langchain_llm = MagicMock()
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.content = '```json\n{"key": "value"}\n```'
|
||||||
|
mock_langchain_llm.invoke.return_value = mock_response
|
||||||
|
|
||||||
|
messages = [{"role": "user", "content": "test"}]
|
||||||
|
metadata = {"user_id": "test_user"}
|
||||||
|
|
||||||
|
await memory._create_procedural_memory(messages, metadata=metadata, llm=mock_langchain_llm)
|
||||||
|
|
||||||
|
insert_call = memory.vector_store.insert.call_args
|
||||||
|
stored_data = insert_call[1]["payloads"][0]["data"]
|
||||||
|
assert "```" not in stored_data
|
||||||
|
|||||||
@@ -564,12 +564,20 @@ class TestUpdateMemory:
|
|||||||
resp = client.put("/memories/mem-1", json={"text": "Likes tennis"})
|
resp = client.put("/memories/mem-1", json={"text": "Likes tennis"})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
_, kwargs = mock_memory.update.call_args
|
_, kwargs = mock_memory.update.call_args
|
||||||
assert kwargs["metadata"] is None
|
assert "metadata" not in kwargs
|
||||||
|
|
||||||
def test_missing_text_returns_422(self, client):
|
def test_expiration_date_forwarded_without_text(self, client, mock_memory):
|
||||||
"""text is required — omitting it should fail validation."""
|
resp = client.put("/memories/mem-1", json={"expiration_date": "2999-01-01"})
|
||||||
resp = client.put("/memories/mem-1", json={"metadata": {"k": "v"}})
|
assert resp.status_code == 200
|
||||||
assert resp.status_code == 422
|
_, kwargs = mock_memory.update.call_args
|
||||||
|
assert kwargs["expiration_date"] == "2999-01-01"
|
||||||
|
assert "data" not in kwargs
|
||||||
|
|
||||||
|
def test_null_expiration_date_forwarded_for_clear(self, client, mock_memory):
|
||||||
|
resp = client.put("/memories/mem-1", json={"expiration_date": None})
|
||||||
|
assert resp.status_code == 200
|
||||||
|
_, kwargs = mock_memory.update.call_args
|
||||||
|
assert kwargs["expiration_date"] is None
|
||||||
|
|
||||||
def test_dict_not_passed_as_data(self, client, mock_memory):
|
def test_dict_not_passed_as_data(self, client, mock_memory):
|
||||||
"""Regression test for #3933: the entire dict must NOT be passed as data."""
|
"""Regression test for #3933: the entire dict must NOT be passed as data."""
|
||||||
|
|||||||
@@ -180,3 +180,51 @@ def test_get_returns_none_for_missing_id():
|
|||||||
|
|
||||||
assert db.get("missing_id") is None
|
assert db.get("missing_id") is None
|
||||||
mock_index.fetch.assert_called_once_with("missing_id")
|
mock_index.fetch.assert_called_once_with("missing_id")
|
||||||
|
|
||||||
|
|
||||||
|
def test_insert_entity_payload_without_hash_and_created_at():
|
||||||
|
"""insert() must not crash on entity payloads that lack hash/created_at."""
|
||||||
|
db, mock_index = _make_redis_db()
|
||||||
|
|
||||||
|
entity_payload = {
|
||||||
|
"data": "OpenAI",
|
||||||
|
"entity_type": "organization",
|
||||||
|
"linked_memory_ids": ["mem-1"],
|
||||||
|
"user_id": "test_user",
|
||||||
|
}
|
||||||
|
|
||||||
|
db.insert(
|
||||||
|
vectors=[[0.1, 0.2, 0.3]],
|
||||||
|
payloads=[entity_payload],
|
||||||
|
ids=["entity-1"],
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_index.load.assert_called_once()
|
||||||
|
data = mock_index.load.call_args[0][0]
|
||||||
|
assert data[0]["memory_id"] == "entity-1"
|
||||||
|
assert data[0]["memory"] == "OpenAI"
|
||||||
|
assert data[0]["hash"] == ""
|
||||||
|
assert data[0]["created_at"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_entity_payload_without_hash_and_timestamps():
|
||||||
|
"""update() must not crash on entity payloads that lack hash/created_at/updated_at."""
|
||||||
|
db, mock_index = _make_redis_db()
|
||||||
|
|
||||||
|
entity_payload = {
|
||||||
|
"data": "OpenAI",
|
||||||
|
"entity_type": "organization",
|
||||||
|
"linked_memory_ids": ["mem-1"],
|
||||||
|
"user_id": "test_user",
|
||||||
|
}
|
||||||
|
|
||||||
|
db.update(vector_id="entity-1", vector=[0.1, 0.2, 0.3], payload=entity_payload)
|
||||||
|
|
||||||
|
mock_index.load.assert_called_once()
|
||||||
|
call_kwargs = mock_index.load.call_args
|
||||||
|
data_dict = call_kwargs[1]["data"][0] if "data" in call_kwargs[1] else call_kwargs[0][0][0]
|
||||||
|
assert data_dict["memory_id"] == "entity-1"
|
||||||
|
assert data_dict["memory"] == "OpenAI"
|
||||||
|
assert data_dict["hash"] == ""
|
||||||
|
assert data_dict["created_at"] == 0
|
||||||
|
assert data_dict["updated_at"] == 0
|
||||||
|
|||||||
@@ -490,26 +490,6 @@ def test_reset(valkey_db, mock_valkey_client):
|
|||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
def test_build_list_query(valkey_db):
|
|
||||||
"""Test building a list query with and without filters."""
|
|
||||||
# Test without filters
|
|
||||||
query = valkey_db._build_list_query(None)
|
|
||||||
assert query == "*"
|
|
||||||
|
|
||||||
# Test with empty filters
|
|
||||||
query = valkey_db._build_list_query({})
|
|
||||||
assert query == "*"
|
|
||||||
|
|
||||||
# Test with filters
|
|
||||||
query = valkey_db._build_list_query({"user_id": "test_user"})
|
|
||||||
assert query == "@user_id:{test_user}"
|
|
||||||
|
|
||||||
# Test with multiple filters
|
|
||||||
query = valkey_db._build_list_query({"user_id": "test_user", "agent_id": "test_agent"})
|
|
||||||
assert "@user_id:{test_user}" in query
|
|
||||||
assert "@agent_id:{test_agent}" in query
|
|
||||||
|
|
||||||
|
|
||||||
def test_process_document_fields(valkey_db):
|
def test_process_document_fields(valkey_db):
|
||||||
"""Test processing document fields from hash results."""
|
"""Test processing document fields from hash results."""
|
||||||
# Create a mock result with all fields
|
# Create a mock result with all fields
|
||||||
@@ -1061,3 +1041,28 @@ def test_build_index_schema_indexes_memory_as_text(valkey_db):
|
|||||||
)
|
)
|
||||||
# And it must not be declared as TAG.
|
# And it must not be declared as TAG.
|
||||||
assert ["memory", "TAG"] != cmd[memory_idx : memory_idx + 2]
|
assert ["memory", "TAG"] != cmd[memory_idx : memory_idx + 2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_escape_tag_value_wildcards(valkey_db):
|
||||||
|
"""Wildcard characters in filter values must be escaped to prevent query injection."""
|
||||||
|
assert "\\*" in valkey_db._escape_tag_value("*")
|
||||||
|
assert "\\|" in valkey_db._escape_tag_value("a|b")
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_search_query_escapes_filter_values(valkey_db):
|
||||||
|
"""_build_search_query must escape special chars in filter values."""
|
||||||
|
knn_part = "[KNN 5 @embedding $vec_param AS vector_score]"
|
||||||
|
query = valkey_db._build_search_query(knn_part, {"user_id": "*"})
|
||||||
|
assert "\\*" in query
|
||||||
|
assert "@user_id:{\\*}" in query
|
||||||
|
|
||||||
|
|
||||||
|
def test_escape_tag_value_normal_strings(valkey_db):
|
||||||
|
"""Normal alphanumeric filter values must pass through unchanged."""
|
||||||
|
assert valkey_db._escape_tag_value("alice") == "alice"
|
||||||
|
assert valkey_db._escape_tag_value("user123") == "user123"
|
||||||
|
|
||||||
|
|
||||||
|
def test_escape_tag_value_hyphenated_user_id(valkey_db):
|
||||||
|
"""Hyphenated user IDs must have the hyphen escaped for exact-match."""
|
||||||
|
assert valkey_db._escape_tag_value("user-123") == r"user\-123"
|
||||||
|
|||||||
Reference in New Issue
Block a user