Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5b04e70fa | |||
| 7e7682a06d | |||
| f38608fb50 | |||
| fb11cdffbb | |||
| ee600705c2 | |||
| f4ccef5157 | |||
| 08da741a31 | |||
| dc857e43ca | |||
| e4efdd2e29 | |||
| a87c9ce367 | |||
| d258b638ef | |||
| bbbfcfea07 | |||
| fbef369b91 | |||
| a7ed68e697 | |||
| 8a92cf0306 | |||
| 0fbbb2f525 | |||
| 818c2981b7 | |||
| 1f66aadfa3 | |||
| b91c745fbc | |||
| af70668308 | |||
| 890473f891 | |||
| d2ff83cf72 | |||
| 0e02effaf7 | |||
| 9269a0ad6e | |||
| 3d06006f36 | |||
| 7e056281e5 | |||
| b5789d4afe |
+134
-47
@@ -1,72 +1,157 @@
|
||||
# Contributing to mem0
|
||||
# Contributing to Mem0
|
||||
|
||||
Let us make contribution easy, collaborative and fun.
|
||||
First off, thank you for taking the time to contribute! 🎉 Mem0 is a
|
||||
community-driven project and we welcome contributions of all kinds — bug fixes,
|
||||
new features, documentation, examples, and integrations.
|
||||
|
||||
## Submit your Contribution through PR
|
||||
Mem0 is a polyglot monorepo, and this guide covers contributing to both the
|
||||
**Python SDK** and the **TypeScript SDK** (and the rest of the repository).
|
||||
|
||||
To make a contribution, follow these steps:
|
||||
## Before You Start
|
||||
|
||||
1. Fork and clone this repository
|
||||
2. Do the changes on your fork with dedicated feature branch `feature/f1`
|
||||
3. If you modified the code (new feature or bug-fix), please add tests for it
|
||||
4. Include proper documentation / docstring and examples to run the feature
|
||||
5. Ensure that all tests pass
|
||||
6. Submit a pull request
|
||||
### 1. Open an Issue First
|
||||
|
||||
For more details about pull requests, please read [GitHub's guides](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
|
||||
**Always open an issue before opening a pull request.** This lets us discuss the
|
||||
change, avoid duplicate effort, and agree on the approach before you invest time
|
||||
in code.
|
||||
|
||||
- Search [existing issues](https://github.com/mem0ai/mem0/issues) first to see if
|
||||
your bug or idea already exists.
|
||||
- If it doesn't, open a
|
||||
[bug report](https://github.com/mem0ai/mem0/issues/new?template=bug_report.yml) or
|
||||
[feature request](https://github.com/mem0ai/mem0/issues/new?template=feature_request.yml).
|
||||
- For anything beyond a trivial fix, wait for a maintainer to confirm the approach
|
||||
before starting significant work.
|
||||
|
||||
### 📦 Development Environment
|
||||
Every pull request must link to an issue using `Closes #<issue-number>`.
|
||||
|
||||
We use `hatch` for managing development environments. To set up:
|
||||
### 2. Sign the Contributor License Agreement (CLA)
|
||||
|
||||
**We cannot accept or merge any pull request until you have signed our Contributor
|
||||
License Agreement (CLA).**
|
||||
|
||||
When you open your first PR, the CLA bot will automatically comment with a link to
|
||||
sign. Signing takes less than a minute and only needs to be done once. Pull
|
||||
requests from contributors who have not signed the CLA will be blocked from
|
||||
merging.
|
||||
|
||||
## Repository Layout
|
||||
|
||||
The two most common contribution targets are the SDKs:
|
||||
|
||||
| Package | Path | Language | Package manager |
|
||||
| --------------------- | ---------- | ------------ | --------------- |
|
||||
| Python SDK (`mem0ai`) | `mem0/` | Python 3.9+ | `hatch` |
|
||||
| TypeScript SDK (`mem0ai`) | `mem0-ts/` | TypeScript | `pnpm` |
|
||||
|
||||
Other packages include the CLIs (`cli/python/`, `cli/node/`), integrations
|
||||
(`integrations/`), the self-hosted `server/`, `openmemory/`, and the docs site
|
||||
(`docs/`). See [AGENTS.md](./AGENTS.md) for a full map of the repository.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Fork** the repository and **clone** your fork.
|
||||
2. Create a **feature branch** from `main` (e.g. `feature/my-new-feature` or
|
||||
`fix/issue-1234`).
|
||||
3. Make your changes — add **tests**, **documentation**, and **examples** as
|
||||
appropriate.
|
||||
4. Run **linting and tests** for every package you touched (see below).
|
||||
5. Commit using [Conventional Commits](https://www.conventionalcommits.org/)
|
||||
(e.g. `feat:`, `fix:`, `docs:`, `refactor:`, `test:`).
|
||||
6. Push and open a **pull request** against `main`, linking the issue with
|
||||
`Closes #<number>` and filling out the
|
||||
[PR template](./.github/PULL_REQUEST_TEMPLATE.md).
|
||||
|
||||
### Contributing to the Python SDK (`mem0/`)
|
||||
|
||||
We use [`hatch`](https://hatch.pypa.io/latest/install/) to manage environments.
|
||||
**Do not use `pip` or `conda` for dependency management.**
|
||||
|
||||
```bash
|
||||
# Activate environment for specific Python version:
|
||||
hatch shell dev_py_3_9 # Python 3.9
|
||||
hatch shell dev_py_3_10 # Python 3.10
|
||||
hatch shell dev_py_3_11 # Python 3.11
|
||||
hatch shell dev_py_3_12 # Python 3.12
|
||||
# Activate a dev environment (3.9 / 3.10 / 3.11 / 3.12)
|
||||
hatch shell dev_py_3_11
|
||||
|
||||
# The environment will automatically install all dev dependencies
|
||||
# Run tests within the activated shell:
|
||||
make test
|
||||
```
|
||||
|
||||
### 📌 Pre-commit
|
||||
|
||||
To ensure our standards, make sure to install pre-commit before starting to contribute.
|
||||
|
||||
```bash
|
||||
# Install pre-commit hooks (runs ruff + isort on commit)
|
||||
pre-commit install
|
||||
|
||||
# Lint, format, and sort imports
|
||||
make lint
|
||||
make format
|
||||
make sort
|
||||
|
||||
# Run the test suite (run `make install_all` first if deps are missing)
|
||||
make test
|
||||
```
|
||||
|
||||
### 🧪 Testing
|
||||
- **Linter / formatter:** Ruff (line length **120**)
|
||||
- **Import sorting:** isort (`profile = "black"`)
|
||||
- **Tests:** pytest (in `tests/`)
|
||||
|
||||
We use `pytest` to test our code across multiple Python versions. You can run tests using:
|
||||
See the full [Development guide](https://docs.mem0.ai/contributing/development) for
|
||||
environment details.
|
||||
|
||||
### Contributing to the TypeScript SDK (`mem0-ts/`)
|
||||
|
||||
We use [`pnpm`](https://pnpm.io/) (v10+) for all TypeScript packages. **Do not use
|
||||
`npm` or `yarn`.**
|
||||
|
||||
```bash
|
||||
# Run tests with default Python version
|
||||
make test
|
||||
cd mem0-ts
|
||||
pnpm install
|
||||
|
||||
# Test specific Python versions:
|
||||
make test-py-3.9 # Python 3.9 environment
|
||||
make test-py-3.10 # Python 3.10 environment
|
||||
make test-py-3.11 # Python 3.11 environment
|
||||
make test-py-3.12 # Python 3.12 environment
|
||||
|
||||
# When using hatch shells, run tests with:
|
||||
make test # After activating a shell with hatch shell test_XX
|
||||
pnpm run build # tsup (CJS + ESM)
|
||||
pnpm run test # jest (all tests)
|
||||
pnpm run test:unit # unit tests with coverage
|
||||
```
|
||||
|
||||
Make sure that all tests pass across all supported Python versions before submitting a pull request.
|
||||
- **Build:** tsup
|
||||
- **Formatter:** Prettier
|
||||
- **Tests:** jest
|
||||
- Always run type checking after changes: `pnpm run typecheck` (or `tsc --noEmit`).
|
||||
- Use ES module `import` syntax — never `require()`.
|
||||
|
||||
We look forward to your pull requests and can't wait to see your contributions!
|
||||
## Good Contribution Practices
|
||||
|
||||
### 🚀 Releasing
|
||||
- **Keep PRs small and focused.** One logical change per PR is easier to review and
|
||||
merge.
|
||||
- **Follow existing patterns.** Match the style, structure, and conventions of the
|
||||
code around you. Don't introduce new frameworks or abstractions without
|
||||
discussion.
|
||||
- **Write tests** that would fail without your change — regression tests for bugs,
|
||||
coverage for new features.
|
||||
- **Update documentation** in `docs/` for any user-facing change. New `.mdx` pages
|
||||
must be added to `docs/llms.txt` (run
|
||||
`python scripts/check-llms-txt-coverage.py --write` to scaffold entries).
|
||||
- **Add examples** when introducing new user-facing behavior.
|
||||
- **Run linters and tests locally** before pushing — CI re-runs them on every PR
|
||||
via the CI Gate.
|
||||
- **Never commit secrets** — no `.env` files, API keys, or credentials.
|
||||
- **Don't add core dependencies lightly.** New Python dependencies belong in an
|
||||
optional group in `pyproject.toml`, not the core `dependencies` list.
|
||||
- **Be responsive** to review feedback and keep your branch up to date with `main`.
|
||||
|
||||
All packages are published automatically via GitHub Actions when a GitHub Release is created with the correct tag prefix.
|
||||
## Pull Request Checklist
|
||||
|
||||
#### Tag Prefixes
|
||||
Before requesting review, make sure:
|
||||
|
||||
- [ ] An issue exists and is linked with `Closes #<number>`
|
||||
- [ ] You have signed the CLA
|
||||
- [ ] Your code follows the project's style guidelines (lint passes)
|
||||
- [ ] You performed a self-review of your changes
|
||||
- [ ] Tests are added/updated and pass locally
|
||||
- [ ] Documentation is updated if needed
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Do not report security vulnerabilities through public issues or pull requests.**
|
||||
Please follow our [Security Policy](./SECURITY.md) to report them privately.
|
||||
|
||||
## Releasing
|
||||
|
||||
All packages are published automatically via GitHub Actions when a GitHub Release
|
||||
is created with the correct tag prefix.
|
||||
|
||||
### Tag Prefixes
|
||||
|
||||
| Package | Registry | Tag Prefix | Example |
|
||||
|---------|----------|------------|---------|
|
||||
@@ -77,15 +162,17 @@ All packages are published automatically via GitHub Actions when a GitHub Releas
|
||||
| `@mem0/vercel-ai-provider` | npm | `vercel-ai-v*` | `vercel-ai-v2.0.6` |
|
||||
| `@mem0/openclaw-mem0` | npm | `openclaw-v*` | `openclaw-v1.0.1` |
|
||||
|
||||
#### How to Release
|
||||
### How to Release
|
||||
|
||||
1. Bump the version in `pyproject.toml` (Python) or `package.json` (Node)
|
||||
2. Create a [GitHub Release](https://github.com/mem0ai/mem0/releases/new) with the matching tag prefix
|
||||
3. The correct workflow will trigger automatically — verify in the [Actions tab](https://github.com/mem0ai/mem0/actions)
|
||||
|
||||
#### Publishing Details
|
||||
### Publishing Details
|
||||
|
||||
- **PyPI packages** use OIDC trusted publishing via `pypa/gh-action-pypi-publish`
|
||||
- **npm packages** use OIDC trusted publishing via npm CLI (>= 11.5.1) — no tokens or secrets required
|
||||
- All workflows require `permissions: id-token: write` for OIDC authentication
|
||||
- First publish of a new npm package must be done manually; OIDC works for subsequent versions
|
||||
|
||||
We look forward to your pull requests and can't wait to see your contributions!
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# Security Policy
|
||||
|
||||
We take the security of Mem0 and our community seriously. Thank you for helping
|
||||
keep Mem0 and its users safe by disclosing vulnerabilities responsibly.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please **do not** report security vulnerabilities through public GitHub issues,
|
||||
pull requests, or discussions.
|
||||
|
||||
If you believe you have found a security vulnerability in Mem0, please report it
|
||||
privately through one of the following channels:
|
||||
|
||||
1. **GitHub Private Vulnerability Reporting** — open a
|
||||
[private security advisory](https://github.com/mem0ai/mem0/security/advisories/new)
|
||||
directly on this repository.
|
||||
2. **Email** the maintainers at **support@mem0.ai** with the subject line:
|
||||
|
||||
`SECURITY: Mem0 vulnerability report`
|
||||
|
||||
To help us triage and resolve the issue quickly, please include as much of the
|
||||
following as you can:
|
||||
|
||||
- Affected component or package (e.g. Python SDK, TypeScript SDK, server, OpenMemory)
|
||||
- Affected version, tag, or commit
|
||||
- Clear, step-by-step reproduction instructions
|
||||
- The security impact and a proof of concept, if available
|
||||
- Any suggested fix or mitigation
|
||||
|
||||
## Response Process
|
||||
|
||||
- We will acknowledge receipt of your report within **72 hours**.
|
||||
- We will work with you privately to confirm the issue and assess its impact.
|
||||
- Once a fix or mitigation is ready, we will coordinate a disclosure timeline
|
||||
with you and credit you for the discovery, unless you prefer to remain anonymous.
|
||||
|
||||
## Public Disclosure
|
||||
|
||||
Please avoid sharing technical details of the vulnerability publicly until the
|
||||
maintainers have reviewed the issue and a fix or mitigation has been released. We
|
||||
are committed to resolving valid reports promptly and keeping you informed
|
||||
throughout the process.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We release security fixes against the latest published version of each package.
|
||||
Whenever possible, please reproduce the issue on the most recent release before
|
||||
reporting.
|
||||
@@ -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. |
|
||||
| `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. |
|
||||
| `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.
|
||||
|
||||
@@ -83,3 +84,11 @@ The request is queued for background processing. The response contains an `event
|
||||
<Info>
|
||||
Poll the event status via `GET /v1/event/{event_id}/`. Status will be `SUCCEEDED` or `FAILED` once processing completes.
|
||||
</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>
|
||||
|
||||
@@ -4,4 +4,4 @@ description: "Submit an export job to create a structured memory export using a
|
||||
openapi: post /v1/exports/
|
||||
---
|
||||
|
||||
Submit a job to create a structured export of memories using a customizable Pydantic schema. This process may take some time to complete, especially if you're exporting a large number of memories. You can tailor the export by applying various filters (e.g., `user_id`, `agent_id`, `run_id`, or `session_id`) and by modifying the Pydantic schema to ensure the final data matches your exact needs.
|
||||
Submit a job to create a structured export of memories using a customizable Pydantic schema. This process may take some time to complete, especially if you're exporting a large number of memories. You can tailor the export by applying various filters (e.g., `user_id`, `agent_id`, `app_id`, or `run_id`) and by modifying the Pydantic schema to ensure the final data matches your exact needs.
|
||||
|
||||
@@ -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.
|
||||
|
||||
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:
|
||||
|
||||
- `in`: Matches any of the values specified
|
||||
@@ -32,6 +36,7 @@ memories = client.get_all(
|
||||
}
|
||||
]
|
||||
},
|
||||
show_expired=False,
|
||||
page=1,
|
||||
page_size=50
|
||||
)
|
||||
@@ -46,12 +51,14 @@ memories = client.get_all(
|
||||
{
|
||||
"id": "f4cbdb08-7062-4f3e-8eb2-9f5c80dfe64c",
|
||||
"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",
|
||||
"updated_at": "2024-07-01T12:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "a2b8c3d4-5e6f-7g8h-9i0j-1k2l3m4n5o6p",
|
||||
"memory": "Alex prefers vegetarian restaurants",
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-05T15:30:00Z",
|
||||
"updated_at": "2024-07-05T15:30:00Z"
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ description: "Retrieve the latest structured memory export after submitting an e
|
||||
openapi: post /v1/exports/get
|
||||
---
|
||||
|
||||
Retrieve the latest structured memory export after submitting an export job. You can filter the export by `user_id`, `run_id`, `session_id`, or `app_id` to get the most recent export matching your filters.
|
||||
Retrieve the latest structured memory export after submitting an export job. You can filter the export by `user_id`, `agent_id`, `app_id`, `run_id`, `created_at`, or `updated_at` to get the most recent export matching your filters.
|
||||
@@ -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.
|
||||
|
||||
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:
|
||||
- `in`: Matches any of the values specified
|
||||
- `gte`: Greater than or equal to
|
||||
@@ -20,16 +24,17 @@ The `filters` object supports complex logical operations (AND, OR, NOT) and comp
|
||||
|
||||
### Search parameter defaults
|
||||
|
||||
| Parameter | V1/V2 | V3 |
|
||||
| --- | --- | --- |
|
||||
| `top_k` | Supported (default 10) | Supported (1-1000, default 10) |
|
||||
| `threshold` | No default | Default `0.1` (pass `0.0` to disable) |
|
||||
| `rerank` | Default `true` | Default `false` (pass `true` to enable) |
|
||||
| Parameter | Default |
|
||||
| --- | --- |
|
||||
| `top_k` | `10` (range 1–1000) |
|
||||
| `threshold` | `0.1` (pass `0.0` to disable) |
|
||||
| `rerank` | `false` (pass `true` to enable) |
|
||||
|
||||
<CodeGroup>
|
||||
```python Platform API Example
|
||||
related_memories = client.search(
|
||||
query="What are Alice's hobbies?",
|
||||
show_expired=False,
|
||||
filters={
|
||||
"OR": [
|
||||
{
|
||||
@@ -54,6 +59,7 @@ related_memories = client.search(
|
||||
"category": "hobbies"
|
||||
},
|
||||
"score": 0.82,
|
||||
"expiration_date": null,
|
||||
"created_at": "2024-07-26T10:29:36.630547-07:00",
|
||||
"updated_at": null,
|
||||
"categories": ["hobbies"]
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
---
|
||||
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}/
|
||||
---
|
||||
---
|
||||
|
||||
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 an organization to revoke their access to its projects and resources."
|
||||
openapi: "delete /api/v1/orgs/organizations/{org_id}/members/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Update Organization Member"
|
||||
description: "Update an existing member's role within an organization to change their permissions and access level."
|
||||
openapi: "put /api/v1/orgs/organizations/{org_id}/members/"
|
||||
---
|
||||
@@ -14,7 +14,7 @@ Organizations and projects are **optional** features. You can use Mem0 without t
|
||||
|
||||
## Key Capabilities
|
||||
|
||||
- **Multi-org/project Support**: Specify organization and project when initializing the Mem0 client to attribute API usage appropriately
|
||||
- **Multi-org/project Support**: Organization and project are resolved automatically from your API key via `/v1/ping/` — no org or project params are accepted by `MemoryClient.__init__`. Use a project-specific API key to target a particular project.
|
||||
- **Member Management**: Control access to data through organization and project membership
|
||||
- **Access Control**: Only members can access memories and data within their organization/project scope
|
||||
- **Team Isolation**: Maintain data separation between different teams and projects for secure collaboration
|
||||
@@ -79,7 +79,7 @@ new_project = client.project.create(
|
||||
|
||||
### Update Project Settings
|
||||
|
||||
Modify project configuration including custom instructions, categories, and language preferences:
|
||||
Modify project configuration including custom instructions, categories, language preferences, retrieval criteria, and memory decay:
|
||||
|
||||
```python
|
||||
# Update project with custom categories
|
||||
@@ -98,6 +98,17 @@ client.project.update(
|
||||
# Use the input language for memory storage and retrieval
|
||||
client.project.update(multilingual=True)
|
||||
|
||||
# Set retrieval criteria to control which memories are surfaced in search
|
||||
client.project.update(
|
||||
retrieval_criteria=[
|
||||
{"name": "relevance", "description": "How directly relevant this memory is to the current topic or user query", "weight": 3},
|
||||
{"name": "access_frequency", "description": "How often this memory has been accessed or surfaced recently", "weight": 1}
|
||||
]
|
||||
)
|
||||
|
||||
# Enable Memory Decay (boosts recently-accessed memories at search time)
|
||||
client.project.update(decay=True)
|
||||
|
||||
# Update multiple settings at once
|
||||
client.project.update(
|
||||
custom_instructions="...",
|
||||
@@ -109,6 +120,34 @@ client.project.update(
|
||||
)
|
||||
```
|
||||
|
||||
#### Set Retrieval Criteria
|
||||
|
||||
`retrieval_criteria` is a per-project list of dictionaries (`List[Dict]`) that shapes how memories are ranked and filtered during search. Each dictionary has three fields: `name` (identifier), `description` (interpreted by the LLM to score each memory), and `weight` (relative influence on the final score). Use this to focus retrieval on intent-aligned or signal-specific memories:
|
||||
|
||||
```python
|
||||
client.project.update(
|
||||
retrieval_criteria=[
|
||||
{
|
||||
"name": "joy",
|
||||
"description": "Measure the intensity of positive emotions such as happiness, excitement, or amusement expressed in the memory. A higher score reflects greater joy.",
|
||||
"weight": 3
|
||||
},
|
||||
{
|
||||
"name": "curiosity",
|
||||
"description": "Assess the extent to which the memory reflects inquisitiveness or interest in exploring new information. A higher score reflects stronger curiosity.",
|
||||
"weight": 2
|
||||
},
|
||||
{
|
||||
"name": "access_frequency",
|
||||
"description": "How often this memory has been accessed or surfaced recently.",
|
||||
"weight": 1
|
||||
}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
Pass an empty list to clear all criteria and restore default retrieval behaviour.
|
||||
|
||||
#### Toggle Memory Decay
|
||||
|
||||
`decay` is a per-project boolean that turns on [Memory Decay](/platform/features/memory-decay) — a search-time ranking bias that reinforces recently-accessed memories and gently dampens stale ones. The flag is `false` by default; set it via the same project-update endpoint:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Remove Project Member"
|
||||
description: "Remove a member from a project to revoke their access to its memories, configuration, and resources."
|
||||
openapi: "delete /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Update Project Member"
|
||||
description: "Update an existing member's role within a project to change their permissions and access level."
|
||||
openapi: "put /api/v1/orgs/organizations/{org_id}/projects/{project_id}/members/"
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: "Update Project"
|
||||
description: "Update a project's settings, including name, custom instructions, and other configuration 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:
|
||||
|
||||
- **Apache AGE** — New graph store support, bringing the total to 4 graph store backends (Neo4j, Memgraph, Kuzu, Apache AGE)
|
||||
- **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).
|
||||
- **Turbopuffer** — New vector database provider for Python SDK
|
||||
- **MiniMax** — New LLM provider with dedicated AWS Bedrock support
|
||||
- **pgvector for Node.js** — PostgreSQL vector support added to the TypeScript OSS SDK
|
||||
|
||||
@@ -121,7 +121,7 @@ mode: "wide"
|
||||
|
||||
**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:** 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))
|
||||
- **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))
|
||||
|
||||
**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))
|
||||
@@ -229,7 +229,7 @@ mode: "wide"
|
||||
**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))
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
</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.
|
||||
|
||||
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
|
||||
|
||||
```python
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
@@ -33,6 +36,33 @@ messages = [
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
```python
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import os
|
||||
from mem0 import Memory
|
||||
|
||||
@@ -36,9 +37,37 @@ messages = [
|
||||
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:
|
||||
|
||||
```python
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
config = {
|
||||
"llm": {
|
||||
"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
|
||||
|
||||
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
|
||||
@@ -46,7 +46,7 @@ Here are the parameters available for configuring Baidu VectorDB:
|
||||
| `account` | Baidu VectorDB account name | `root` |
|
||||
| `api_key` | API key for accessing Baidu VectorDB | Required |
|
||||
| `database_name` | Name of the database | `mem0` |
|
||||
| `table_name` | Name of the table | `mem0_table` |
|
||||
| `table_name` | Name of the table | `mem0` |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` |
|
||||
| `metric_type` | Distance metric for similarity search | `L2` |
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ Here are the parameters available for configuring Elasticsearch:
|
||||
| `api_key` | API key for authentication | `None` |
|
||||
| `user` | Username for basic authentication | `None` |
|
||||
| `password` | Password for basic authentication | `None` |
|
||||
| `use_ssl` | Whether to use SSL for the connection | `True` |
|
||||
| `ca_certs` | Path to CA bundle for SSL certificate verification | `None` |
|
||||
| `verify_certs` | Whether to verify SSL certificates | `True` |
|
||||
| `auto_create_index` | Whether to automatically create the index | `True` |
|
||||
| `custom_search_query` | Function returning a custom search query | `None` |
|
||||
|
||||
@@ -55,6 +55,7 @@ Here are the parameters available for configuring FAISS:
|
||||
| `path` | Path to store FAISS index and metadata | `/tmp/faiss/<collection_name>` |
|
||||
| `distance_strategy` | Distance metric strategy to use (options: 'euclidean', 'inner_product', 'cosine') | `euclidean` |
|
||||
| `normalize_L2` | Whether to normalize L2 vectors (only applicable for euclidean distance) | `False` |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` |
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
|
||||
@@ -47,12 +47,12 @@ m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Memory } from "mem0ai";
|
||||
import { Memory } from "mem0ai/oss";
|
||||
import { OpenAIEmbeddings } from "@langchain/openai";
|
||||
import { MemoryVectorStore as LangchainMemoryStore } from "langchain/vectorstores/memory";
|
||||
import { MemoryVectorStore } from "langchain/vectorstores/memory";
|
||||
|
||||
const embeddings = new OpenAIEmbeddings();
|
||||
const vectorStore = new LangchainVectorStore(embeddings);
|
||||
const vectorStore = new MemoryVectorStore(embeddings);
|
||||
|
||||
const config = {
|
||||
"vector_store": {
|
||||
|
||||
@@ -42,8 +42,8 @@ Here are the parameters available for configuring MongoDB:
|
||||
| Parameter | Description | Default Value |
|
||||
| --- | --- | --- |
|
||||
| db_name | Name of the MongoDB database | `"mem0_db"` |
|
||||
| collection_name | Name of the MongoDB collection | `"mem0_collection"` |
|
||||
| collection_name | Name of the MongoDB collection | `"mem0"` |
|
||||
| embedding_model_dims | Dimensions of the embedding vectors | `1536` |
|
||||
| mongo_uri | The MongoDB URI connection string | `mongodb://username:password@localhost:27017` |
|
||||
| mongo_uri | The MongoDB URI connection string | `mongodb://localhost:27017` |
|
||||
|
||||
> **Note**: If `mongo_uri` is not provided, it will default to `mongodb://username:password@localhost:27017`.
|
||||
> **Note**: If `mongo_uri` is not provided, it will default to `mongodb://localhost:27017`.
|
||||
|
||||
@@ -21,34 +21,33 @@ from mem0 import Memory
|
||||
load_dotenv()
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "pgvector",
|
||||
"config": {
|
||||
"connection_string": os.environ["DATABASE_URL"],
|
||||
"collection_name": "memories",
|
||||
"embedding_model_dims": 1536,
|
||||
"hnsw": True,
|
||||
},
|
||||
},
|
||||
"vector_store": {
|
||||
"provider": "pgvector",
|
||||
"config": {
|
||||
"connection_string": os.environ["DATABASE_URL"],
|
||||
"collection_name": "memories",
|
||||
"embedding_model_dims": 1536,
|
||||
"hnsw": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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."},
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
|
||||
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."},
|
||||
]
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
|
||||
results = m.search(
|
||||
"What movies should I recommend?",
|
||||
filters={"user_id": "alice"},
|
||||
"What movies should I recommend?",
|
||||
filters={"user_id": "alice"},
|
||||
)
|
||||
|
||||
print(results)
|
||||
|
||||
````
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import "dotenv/config";
|
||||
@@ -87,7 +86,7 @@ const results = await m.search("What movies should I recommend?", {
|
||||
});
|
||||
|
||||
console.log(results);
|
||||
````
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ description: "Use AWS Neptune Analytics as a vector store in Mem0, combining gra
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install mem0ai[vector_stores]
|
||||
pip install mem0ai[vector-stores]
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -15,27 +15,27 @@ from mem0 import Memory
|
||||
os.environ["OPENAI_API_KEY"] = "sk-xx"
|
||||
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "pgvector",
|
||||
"config": {
|
||||
"user": "test",
|
||||
"password": "123",
|
||||
"host": "127.0.0.1",
|
||||
"port": "5432",
|
||||
},
|
||||
}
|
||||
"vector_store": {
|
||||
"provider": "pgvector",
|
||||
"config": {
|
||||
"user": "test",
|
||||
"password": "123",
|
||||
"host": "127.0.0.1",
|
||||
"port": "5432",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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."},
|
||||
{"role": "user", "content": "I'm planning to watch a movie tonight. Any recommendations?"},
|
||||
{"role": "assistant", "content": "How about thriller movies? They can be quite engaging."},
|
||||
{"role": "user", "content": "I'm not a big fan of thriller movies but I love sci-fi movies."},
|
||||
{"role": "assistant", "content": "Got it! I'll avoid thriller recommendations and suggest sci-fi movies in the future."},
|
||||
]
|
||||
m.add(messages, user_id="alice", metadata={"category": "movies"})
|
||||
|
||||
````
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import { Memory } from "mem0ai/oss";
|
||||
@@ -62,7 +62,7 @@ const messages = [
|
||||
];
|
||||
|
||||
await memory.add(messages, { userId: "alice", metadata: { category: "movies" } });
|
||||
````
|
||||
```
|
||||
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
@@ -18,7 +18,9 @@ os.environ["UPSTASH_VECTOR_REST_TOKEN"] = "..."
|
||||
config = {
|
||||
"vector_store": {
|
||||
"provider": "upstash_vector",
|
||||
"enable_embeddings": True,
|
||||
"config": {
|
||||
"enable_embeddings": True,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ description: "Use Valkey as an open-source vector store in Mem0 for high-perform
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install mem0ai[vector_stores]
|
||||
pip install mem0ai[vector-stores]
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -51,7 +51,7 @@ Here are the parameters available for configuring Valkey:
|
||||
| `hnsw_ef_construction` | Size of dynamic candidate list for HNSW | `200` |
|
||||
| `hnsw_ef_runtime` | Size of dynamic candidate list for search | `10` |
|
||||
| `cluster_mode` | Enable cluster mode for Valkey cluster (CME) deployments | `false` |
|
||||
| `distance_metric` | Distance metric for vector similarity | `cosine` |
|
||||
| `timezone` | Timezone for timestamp handling | `UTC` |
|
||||
|
||||
## Cluster Mode
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ config = {
|
||||
"deployment_index_id": "YOUR_DEPLOYMENT_INDEX_ID", # Required: Deployment-specific ID
|
||||
"project_id": "YOUR_PROJECT_ID", # Required: Google Cloud project ID
|
||||
"project_number": "YOUR_PROJECT_NUMBER", # Required: Google Cloud project number
|
||||
"region": "YOUR_REGION", # Optional: Defaults to GOOGLE_CLOUD_REGION
|
||||
"region": "YOUR_REGION", # Required: Google Cloud region
|
||||
"credentials_path": "path/to/credentials.json", # Optional: Defaults to GOOGLE_APPLICATION_CREDENTIALS
|
||||
"vector_search_api_endpoint": "YOUR_API_ENDPOINT" # Required for get operations
|
||||
}
|
||||
@@ -45,5 +45,6 @@ m.add("Your text here", user_id="user", metadata={"category": "example"})
|
||||
| `project_id` | Google Cloud project ID | Yes |
|
||||
| `project_number` | Google Cloud project number | Yes |
|
||||
| `vector_search_api_endpoint` | Vector search API endpoint | Yes (for get operations) |
|
||||
| `region` | Google Cloud region | No (defaults to GOOGLE_CLOUD_REGION) |
|
||||
| `region` | Google Cloud region | Yes |
|
||||
| `credentials_path` | Path to service account credentials | No (defaults to GOOGLE_APPLICATION_CREDENTIALS) |
|
||||
| `service_account_json` | Service account credentials as a dictionary (alternative to `credentials_path`) | `None` |
|
||||
|
||||
@@ -7,7 +7,7 @@ description: "Use Weaviate as an open-source vector search engine in Mem0 for st
|
||||
|
||||
### Installation
|
||||
```bash
|
||||
pip install weaviate weaviate-client
|
||||
pip install weaviate-client
|
||||
```
|
||||
|
||||
### Usage
|
||||
@@ -48,4 +48,5 @@ Here are the parameters available for configuring Weaviate:
|
||||
| `collection_name` | The name of the collection to store the vectors | `mem0` |
|
||||
| `embedding_model_dims` | Dimensions of the embedding model | `1536` |
|
||||
| `cluster_url` | URL for the Weaviate server | `None` |
|
||||
| `auth_client_secret` | API key for Weaviate authentication | `None` |
|
||||
| `auth_client_secret` | API key for Weaviate authentication | `None` |
|
||||
| `additional_headers` | Additional headers to include in requests (`Dict[str, str]`) | `None` |
|
||||
@@ -10,7 +10,7 @@ Mem0 includes built-in support for various popular databases. Memory can utilize
|
||||
See the list of supported vector databases below.
|
||||
|
||||
<Note>
|
||||
The following vector databases are supported in the Python implementation. The TypeScript implementation currently only supports Qdrant, Redis, Valkey, Vectorize and in-memory vector database.
|
||||
The following vector databases are supported in the Python implementation. The TypeScript implementation currently supports Qdrant, Redis, PGVector, Supabase, LangChain, Azure AI Search, Vectorize, and an in-memory store.
|
||||
</Note>
|
||||
|
||||
<CardGroup cols={3}>
|
||||
|
||||
@@ -1,32 +1,65 @@
|
||||
---
|
||||
title: Development
|
||||
description: "Guide to contributing code to Mem0, covering the fork and clone workflow, PR submission, and code quality checks."
|
||||
description: "Guide to contributing code to Mem0, covering the issue-first workflow, the CLA, environment setup for the Python and TypeScript SDKs, and code quality checks."
|
||||
icon: "code"
|
||||
---
|
||||
|
||||
# Development Contributions
|
||||
|
||||
We strive to make contributions **easy, collaborative, and enjoyable**. Follow the steps below to ensure a smooth contribution process.
|
||||
We strive to make contributions **easy, collaborative, and enjoyable**. Mem0 is a
|
||||
polyglot monorepo containing the **Python SDK** (`mem0/`), the **TypeScript SDK**
|
||||
(`mem0-ts/`), CLIs, integrations, the self-hosted server, and the docs site.
|
||||
Follow the steps below for a smooth contribution process.
|
||||
|
||||
## Submitting Your Contribution through PR
|
||||
<Note>
|
||||
For the complete contributor checklist, see
|
||||
[CONTRIBUTING.md](https://github.com/mem0ai/mem0/blob/main/CONTRIBUTING.md) in
|
||||
the repository root.
|
||||
</Note>
|
||||
|
||||
To contribute, follow these steps:
|
||||
## Before You Start
|
||||
|
||||
### 1. Open an Issue First
|
||||
|
||||
**Always open an issue before opening a pull request.** This lets us discuss the
|
||||
change, avoid duplicate work, and agree on the approach before you write code.
|
||||
|
||||
- Search [existing issues](https://github.com/mem0ai/mem0/issues) first.
|
||||
- If none match, open a
|
||||
[bug report](https://github.com/mem0ai/mem0/issues/new?template=bug_report.yml)
|
||||
or [feature request](https://github.com/mem0ai/mem0/issues/new?template=feature_request.yml).
|
||||
- For anything beyond a trivial fix, wait for a maintainer to confirm the approach.
|
||||
|
||||
Every pull request must link to an issue using `Closes #<issue-number>`.
|
||||
|
||||
### 2. Sign the Contributor License Agreement (CLA)
|
||||
|
||||
**We cannot merge any pull request until you have signed our Contributor License
|
||||
Agreement (CLA).** When you open your first PR, the CLA bot will comment with a
|
||||
link to sign — it takes less than a minute and only needs to be done once.
|
||||
|
||||
## Submitting Your Contribution through a PR
|
||||
|
||||
1. **Fork & Clone** the repository: [Mem0 on GitHub](https://github.com/mem0ai/mem0)
|
||||
2. **Create a Feature Branch**: Use a dedicated branch for your changes, e.g., `feature/my-new-feature`
|
||||
3. **Implement Changes**: If adding a feature or fixing a bug, ensure to:
|
||||
2. **Create a Feature Branch**: Use a dedicated branch, e.g., `feature/my-new-feature`
|
||||
3. **Implement Changes**: If adding a feature or fixing a bug, be sure to:
|
||||
- Write necessary **tests**
|
||||
- Add **documentation, docstrings, and runnable examples**
|
||||
4. **Code Quality Checks**:
|
||||
- Run **linting** to catch style issues
|
||||
- Ensure **all tests pass**
|
||||
5. **Submit a Pull Request**
|
||||
5. **Commit** using [Conventional Commits](https://www.conventionalcommits.org/)
|
||||
(`feat:`, `fix:`, `docs:`, `refactor:`, `test:`)
|
||||
6. **Submit a Pull Request** against `main`, linking the issue and filling out the
|
||||
PR template.
|
||||
|
||||
For detailed guidance on pull requests, refer to [GitHub's documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request).
|
||||
|
||||
---
|
||||
|
||||
## Dependency Management
|
||||
## Python SDK (`mem0/`)
|
||||
|
||||
### Dependency Management
|
||||
|
||||
We use `hatch` as our package manager. Install it by following the [official instructions](https://hatch.pypa.io/latest/install/).
|
||||
|
||||
@@ -44,13 +77,9 @@ hatch -e dev_py_3_11 shell # For dev_py_3_11 (differences are mentioned in pypr
|
||||
make install_all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Standards
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
||||
Ensure `pre-commit` is installed before contributing:
|
||||
Ensure `pre-commit` is installed before contributing (hooks run ruff + isort):
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
@@ -58,7 +87,7 @@ pre-commit install
|
||||
|
||||
### Linting with `ruff`
|
||||
|
||||
Run the linter and fix any reported issues before submitting your PR:
|
||||
Run the linter and fix any reported issues before submitting your PR (line length **120**):
|
||||
|
||||
```bash
|
||||
make lint
|
||||
@@ -66,10 +95,11 @@ make lint
|
||||
|
||||
### Code Formatting
|
||||
|
||||
To maintain a consistent code style, format your code:
|
||||
To maintain a consistent code style, format your code and sort imports (isort, `profile = "black"`):
|
||||
|
||||
```bash
|
||||
make format
|
||||
make sort
|
||||
```
|
||||
|
||||
### Testing with `pytest`
|
||||
@@ -84,10 +114,46 @@ make test
|
||||
|
||||
---
|
||||
|
||||
## Release Process
|
||||
## TypeScript SDK (`mem0-ts/`)
|
||||
|
||||
Currently, releases are handled manually. We aim for frequent releases, typically when new features or bug fixes are introduced.
|
||||
We use [`pnpm`](https://pnpm.io/) (v10+) for all TypeScript packages. **Do NOT use
|
||||
`npm` or `yarn`.**
|
||||
|
||||
```bash
|
||||
cd mem0-ts
|
||||
pnpm install
|
||||
|
||||
pnpm run build # tsup (CJS + ESM)
|
||||
pnpm run test # jest (all tests)
|
||||
pnpm run test:unit # unit tests with coverage
|
||||
```
|
||||
|
||||
### Standards
|
||||
|
||||
- **Build:** tsup
|
||||
- **Formatter:** Prettier
|
||||
- **Tests:** jest
|
||||
- Always run type checking after changes: `pnpm run typecheck` (or `tsc --noEmit`)
|
||||
- Use ES module `import` syntax — never `require()`
|
||||
|
||||
---
|
||||
|
||||
Thank you for contributing to Mem0!
|
||||
## Reporting Security Issues
|
||||
|
||||
**Do not report security vulnerabilities through public issues or pull requests.**
|
||||
Please follow our [Security Policy](https://github.com/mem0ai/mem0/blob/main/SECURITY.md)
|
||||
to report them privately.
|
||||
|
||||
---
|
||||
|
||||
## Release Process
|
||||
|
||||
Packages are published automatically via GitHub Actions when a GitHub Release is
|
||||
created with the correct tag prefix (e.g. `v*` for the Python SDK, `ts-v*` for the
|
||||
TypeScript SDK). See
|
||||
[CONTRIBUTING.md](https://github.com/mem0ai/mem0/blob/main/CONTRIBUTING.md#releasing)
|
||||
for the full tag-prefix table and publishing details.
|
||||
|
||||
---
|
||||
|
||||
Thank you for contributing to Mem0!
|
||||
|
||||
+16
-3
@@ -123,8 +123,7 @@
|
||||
"icon": "arrow-right",
|
||||
"pages": [
|
||||
"migration/platform-v2-to-v3",
|
||||
"migration/oss-to-platform",
|
||||
"migration/api-changes"
|
||||
"migration/oss-to-platform"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -273,7 +272,8 @@
|
||||
"components/embedders/models/lmstudio",
|
||||
"components/embedders/models/together",
|
||||
"components/embedders/models/langchain",
|
||||
"components/embedders/models/aws_bedrock"
|
||||
"components/embedders/models/aws_bedrock",
|
||||
"components/embedders/models/fastembed"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -533,6 +533,8 @@
|
||||
"api-reference/organization/get-org",
|
||||
"api-reference/organization/get-org-members",
|
||||
"api-reference/organization/add-org-member",
|
||||
"api-reference/organization/update-org-member",
|
||||
"api-reference/organization/remove-org-member",
|
||||
"api-reference/organization/delete-org"
|
||||
]
|
||||
},
|
||||
@@ -545,6 +547,9 @@
|
||||
"api-reference/project/get-project",
|
||||
"api-reference/project/get-project-members",
|
||||
"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"
|
||||
]
|
||||
},
|
||||
@@ -624,6 +629,10 @@
|
||||
]
|
||||
},
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/components/rerankers/models/llm",
|
||||
"destination": "/components/rerankers/models/llm_reranker"
|
||||
},
|
||||
{
|
||||
"source": "/migration/breaking-changes",
|
||||
"destination": "/"
|
||||
@@ -632,6 +641,10 @@
|
||||
"source": "/migration/v0-to-v1",
|
||||
"destination": "/"
|
||||
},
|
||||
{
|
||||
"source": "/migration/api-changes",
|
||||
"destination": "/migration/oss-v2-to-v3"
|
||||
},
|
||||
{
|
||||
"source": "/platform/features/expiration-date",
|
||||
"destination": "/"
|
||||
|
||||
@@ -73,7 +73,7 @@ client = MemoryClient()
|
||||
# Define the agent
|
||||
agent = 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."
|
||||
"You can process both text and images.",
|
||||
markdown=True
|
||||
|
||||
@@ -43,7 +43,7 @@ OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY')
|
||||
memory_client = MemoryClient()
|
||||
agent = ConversableAgent(
|
||||
"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,
|
||||
human_input_mode="NEVER",
|
||||
)
|
||||
@@ -99,7 +99,7 @@ For more complex scenarios, you can create multiple agents:
|
||||
manager = ConversableAgent(
|
||||
"manager",
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
@@ -138,9 +138,10 @@ When installed via the plugin marketplace, Mem0 hooks into Claude Code's lifecyc
|
||||
|
||||
| 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 |
|
||||
| **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 |
|
||||
| **Stop** | `Stop` | Stores a session summary when the session ends |
|
||||
| **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 |
|
||||
| **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 |
|
||||
| **Stop** | `Stop` | Stores a session summary when the session ends |
|
||||
| **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 |
|
||||
| **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 |
|
||||
| **Stop** | `stop` | Stores a session summary when the session ends |
|
||||
| **Pre-compact** | `preCompact` | Stores a summary before the context is compacted |
|
||||
|
||||
@@ -98,20 +98,9 @@ add_result = add_tool.invoke(add_input)
|
||||
|
||||
```json Output
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"memory": "Name is Alex",
|
||||
"event": "ADD"
|
||||
},
|
||||
{
|
||||
"memory": "Is a vegetarian",
|
||||
"event": "ADD"
|
||||
},
|
||||
{
|
||||
"memory": "Is allergic to nuts",
|
||||
"event": "ADD"
|
||||
}
|
||||
]
|
||||
"message": "Memory processing has been queued for background execution",
|
||||
"status": "PENDING",
|
||||
"event_id": "3a1b2c3d-4e5f-6789-abcd-ef0123456789"
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -173,23 +162,25 @@ result = search_tool.invoke(search_input)
|
||||
```
|
||||
|
||||
```json Output
|
||||
[
|
||||
{
|
||||
"id": "1a75e827-7eca-45ea-8c5c-cfd43299f061",
|
||||
"memory": "Name is Alex",
|
||||
"user_id": "alex",
|
||||
"hash": "d0fccc8fa47f7a149ee95750c37bb0ca",
|
||||
"metadata": {
|
||||
"food": "vegan"
|
||||
},
|
||||
"categories": [
|
||||
"personal_details"
|
||||
],
|
||||
"created_at": "2024-11-27T16:53:43.276872-08:00",
|
||||
"updated_at": "2024-11-27T16:53:43.276885-08:00",
|
||||
"score": 0.3810526501504994
|
||||
}
|
||||
]
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "1a75e827-7eca-45ea-8c5c-cfd43299f061",
|
||||
"memory": "Name is Alex",
|
||||
"user_id": "alex",
|
||||
"hash": "d0fccc8fa47f7a149ee95750c37bb0ca",
|
||||
"metadata": {
|
||||
"food": "vegan"
|
||||
},
|
||||
"categories": [
|
||||
"personal_details"
|
||||
],
|
||||
"created_at": "2024-11-27T16:53:43.276872-08:00",
|
||||
"updated_at": "2024-11-27T16:53:43.276885-08:00",
|
||||
"score": 0.3810526501504994
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ load_dotenv()
|
||||
# MEM0_API_KEY = 'your-mem0-key' # Replace with your actual Mem0 API key
|
||||
|
||||
# Initialize LangChain and Mem0
|
||||
llm = ChatOpenAI(model="gpt-4")
|
||||
llm = ChatOpenAI(model="gpt-5-mini")
|
||||
mem0 = MemoryClient()
|
||||
```
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ async def websocket_endpoint(websocket: WebSocket):
|
||||
# LLM for response generation
|
||||
llm = OpenAILLMService(
|
||||
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."
|
||||
)
|
||||
|
||||
|
||||
@@ -26,12 +26,12 @@ Install the SDK provider and AI SDK:
|
||||
npm install @mem0/vercel-ai-provider ai@^6
|
||||
```
|
||||
|
||||
### Peer Dependencies
|
||||
### Dependencies
|
||||
|
||||
`@mem0/vercel-ai-provider` v3.0.0 requires:
|
||||
- `ai` v6+ (`^6.0.199`)
|
||||
- `@ai-sdk/provider` v3+ (`^3.0.10`)
|
||||
- Provider packages at v3+: `@ai-sdk/openai@^3`, `@ai-sdk/anthropic@^3`, `@ai-sdk/google@^3`, `@ai-sdk/groq@^3`, `@ai-sdk/cohere@^3`
|
||||
`@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.
|
||||
|
||||
The only true peer dependency is `zod` (optional):
|
||||
- `zod` v3+ (`^3.0.0`) — required only if you use Zod schemas in tool definitions
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -305,6 +305,8 @@ These options can be passed per-request when creating a model instance:
|
||||
| `rerank` | `boolean` | Enable reranking of results |
|
||||
| `page` | `number` | Page number for pagination |
|
||||
| `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
|
||||
|
||||
@@ -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.
|
||||
- `getMemories()`: Get memories from your profile in array format.
|
||||
- `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
|
||||
|
||||
|
||||
+7
-4
@@ -228,7 +228,6 @@ If the user is on a pre-current major (Python < 2, TS < 3, or Platform `output_f
|
||||
- [OSS to Platform Migration](https://docs.mem0.ai/migration/oss-to-platform) [Both]: Use when moving from self-hosted to managed.
|
||||
- [OSS v2 to v3 Migration](https://docs.mem0.ai/migration/oss-v2-to-v3) [OSS]: Use when upgrading a self-hosted deployment across major versions.
|
||||
- [Platform v2 to v3 Migration](https://docs.mem0.ai/migration/platform-v2-to-v3) [Platform]: Use when upgrading a Platform integration across major versions.
|
||||
- [API Changes](https://docs.mem0.ai/migration/api-changes) [Both]: Use when the upgrade involves API surface changes.
|
||||
- [Server pgvector Image Upgrade](https://docs.mem0.ai/migration/server-pgvector-upgrade) [OSS]: Use when upgrading the self-hosted server Docker image from ankane/pgvector to pgvector/pgvector.
|
||||
- [Changelog](https://docs.mem0.ai/changelog/highlights) [Both]: Use when the user asks what shipped recently.
|
||||
|
||||
@@ -367,6 +366,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 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.
|
||||
- [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.
|
||||
|
||||
### Projects
|
||||
@@ -375,6 +376,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 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.
|
||||
- [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.
|
||||
|
||||
### Webhooks
|
||||
@@ -460,6 +464,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.
|
||||
- [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.
|
||||
- [FastEmbed](https://docs.mem0.ai/components/embedders/models/fastembed) [OSS]: Use when embeddings run locally via FastEmbed (ONNX).
|
||||
|
||||
### Vector Databases [OSS]
|
||||
- [Vector Database Overview](https://docs.mem0.ai/components/vectordbs/overview) [OSS]: Use when choosing a vector store.
|
||||
@@ -498,7 +503,5 @@ Everything below is OSS-only provider configuration. Skip this entire section wh
|
||||
- [Custom Reranker Prompts](https://docs.mem0.ai/components/rerankers/custom-prompts) [OSS]: Use when rewriting reranker prompts.
|
||||
- [Cohere Reranker](https://docs.mem0.ai/components/rerankers/models/cohere) [OSS]: Use for Cohere Rerank.
|
||||
- [Sentence Transformer Reranker](https://docs.mem0.ai/components/rerankers/models/sentence_transformer) [OSS]: Use for local cross-encoder rerankers.
|
||||
- [Hugging Face Reranker](https://docs.mem0.ai/components/rerankers/models/huggingface) [OSS]: Use for HF-hosted reranker models.
|
||||
- [LLM Reranker (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).
|
||||
- [Hugging Face Reranker](https://docs.mem0.ai/components/rerankers/models/huggingface) [OSS]: Use for HF-hosted reranker models.- [LLM Reranker](https://docs.mem0.ai/components/rerankers/models/llm_reranker) [OSS]: Use when the reranker is a prompted LLM (implementation reference).
|
||||
- [Zero Entropy Reranker](https://docs.mem0.ai/components/rerankers/models/zero_entropy) [OSS]: Use for the Zero Entropy reranker.
|
||||
|
||||
@@ -1,574 +0,0 @@
|
||||
---
|
||||
title: API Reference Changes
|
||||
description: "Comprehensive reference of all API changes between Mem0 v0.x and v1.0.0 Beta, organized by component and method."
|
||||
icon: "code"
|
||||
iconType: "solid"
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This page documents all API changes between Mem0 v0.x and v1.0.0 Beta, organized by component and method.
|
||||
|
||||
## Memory Class Changes
|
||||
|
||||
### Constructor
|
||||
|
||||
#### v0.x
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
# Basic initialization
|
||||
m = Memory()
|
||||
|
||||
# With configuration
|
||||
config = {
|
||||
"version": "v1.0", # Supported in v0.x
|
||||
"vector_store": {...}
|
||||
}
|
||||
m = Memory.from_config(config)
|
||||
```
|
||||
|
||||
#### v1.0.0
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
# Basic initialization (same)
|
||||
m = Memory()
|
||||
|
||||
# With configuration
|
||||
config = {
|
||||
"version": "v1.1", # v1.1+ only
|
||||
"vector_store": {...},
|
||||
# New optional features
|
||||
"reranker": {
|
||||
"provider": "cohere",
|
||||
"config": {...}
|
||||
}
|
||||
}
|
||||
m = Memory.from_config(config)
|
||||
```
|
||||
|
||||
### add() Method
|
||||
|
||||
#### v0.x Signature
|
||||
```python
|
||||
def add(
|
||||
self,
|
||||
messages,
|
||||
user_id: str = None,
|
||||
agent_id: str = None,
|
||||
run_id: str = None,
|
||||
metadata: dict = None,
|
||||
filters: dict = None,
|
||||
output_format: str = None, # ❌ REMOVED
|
||||
version: str = None # ❌ REMOVED
|
||||
) -> Union[List[dict], dict]:
|
||||
...
|
||||
```
|
||||
|
||||
#### v1.0.0 Signature
|
||||
```python
|
||||
def add(
|
||||
self,
|
||||
messages,
|
||||
user_id: str = None,
|
||||
agent_id: str = None,
|
||||
run_id: str = None,
|
||||
metadata: dict = None,
|
||||
filters: dict = None,
|
||||
infer: bool = True # ✅ NEW: Control memory inference
|
||||
) -> dict: # Always returns dict with "results" key
|
||||
...
|
||||
```
|
||||
|
||||
#### Changes Summary
|
||||
|
||||
| Parameter | v0.x | v1.0.0 | Change |
|
||||
|-----------|------|-----------|---------|
|
||||
| `messages` | ✅ | ✅ | Unchanged |
|
||||
| `user_id` | ✅ | ✅ | Unchanged |
|
||||
| `agent_id` | ✅ | ✅ | Unchanged |
|
||||
| `run_id` | ✅ | ✅ | Unchanged |
|
||||
| `metadata` | ✅ | ✅ | Unchanged |
|
||||
| `filters` | ✅ | ✅ | Unchanged |
|
||||
| `output_format` | ✅ | ❌ | **REMOVED** |
|
||||
| `version` | ✅ | ❌ | **REMOVED** |
|
||||
| `infer` | ❌ | ✅ | **NEW** |
|
||||
|
||||
#### Response Format Changes
|
||||
|
||||
**v0.x Response (variable format):**
|
||||
```python
|
||||
# With output_format="v1.0"
|
||||
[
|
||||
{
|
||||
"id": "mem_123",
|
||||
"memory": "User loves pizza",
|
||||
"event": "ADD"
|
||||
}
|
||||
]
|
||||
|
||||
# With output_format="v1.1"
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "mem_123",
|
||||
"memory": "User loves pizza",
|
||||
"event": "ADD"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**v1.0.0 Response (standardized):**
|
||||
```python
|
||||
# Always returns this format
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": "mem_123",
|
||||
"memory": "User loves pizza",
|
||||
"metadata": {...},
|
||||
"event": "ADD"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### search() Method
|
||||
|
||||
#### v0.x Signature
|
||||
```python
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
user_id: str = None,
|
||||
agent_id: str = None,
|
||||
run_id: str = None,
|
||||
limit: int = 100,
|
||||
filters: dict = None, # Basic key-value only
|
||||
output_format: str = None, # ❌ REMOVED
|
||||
version: str = None # ❌ REMOVED
|
||||
) -> Union[List[dict], dict]:
|
||||
...
|
||||
```
|
||||
|
||||
#### v1.0.0 Signature
|
||||
```python
|
||||
def search(
|
||||
self,
|
||||
query: str,
|
||||
user_id: str = None,
|
||||
agent_id: str = None,
|
||||
run_id: str = None,
|
||||
limit: int = 100,
|
||||
filters: dict = None, # ✅ ENHANCED: Advanced operators
|
||||
rerank: bool = True # ✅ NEW: Reranking support
|
||||
) -> dict: # Always returns dict with "results" key
|
||||
...
|
||||
```
|
||||
|
||||
#### Enhanced Filtering
|
||||
|
||||
**v0.x Filters (basic):**
|
||||
```python
|
||||
# Simple key-value filtering only
|
||||
filters = {
|
||||
"category": "food",
|
||||
"user_id": "alice"
|
||||
}
|
||||
```
|
||||
|
||||
**v1.0.0 Filters (enhanced):**
|
||||
```python
|
||||
# Advanced filtering with operators
|
||||
filters = {
|
||||
"AND": [
|
||||
{"category": "food"},
|
||||
{"score": {"gte": 0.8}},
|
||||
{
|
||||
"OR": [
|
||||
{"priority": "high"},
|
||||
{"urgent": True}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Comparison operators
|
||||
filters = {
|
||||
"score": {"gt": 0.5}, # Greater than
|
||||
"priority": {"gte": 5}, # Greater than or equal
|
||||
"rating": {"lt": 3}, # Less than
|
||||
"confidence": {"lte": 0.9}, # Less than or equal
|
||||
"status": {"eq": "active"}, # Equal
|
||||
"archived": {"ne": True}, # Not equal
|
||||
"tags": {"in": ["work", "personal"]}, # In list
|
||||
"category": {"nin": ["spam", "deleted"]} # Not in list
|
||||
}
|
||||
```
|
||||
|
||||
### get_all() Method
|
||||
|
||||
#### v0.x Signature
|
||||
```python
|
||||
def get_all(
|
||||
self,
|
||||
user_id: str = None,
|
||||
agent_id: str = None,
|
||||
run_id: str = None,
|
||||
filters: dict = None,
|
||||
output_format: str = None, # ❌ REMOVED
|
||||
version: str = None # ❌ REMOVED
|
||||
) -> Union[List[dict], dict]:
|
||||
...
|
||||
```
|
||||
|
||||
#### v1.0.0 Signature
|
||||
```python
|
||||
def get_all(
|
||||
self,
|
||||
user_id: str = None,
|
||||
agent_id: str = None,
|
||||
run_id: str = None,
|
||||
filters: dict = None # ✅ ENHANCED: Advanced operators
|
||||
) -> dict: # Always returns dict with "results" key
|
||||
...
|
||||
```
|
||||
|
||||
### update() Method
|
||||
|
||||
#### No Breaking Changes
|
||||
```python
|
||||
# Same signature in both versions
|
||||
def update(
|
||||
self,
|
||||
memory_id: str,
|
||||
data: str
|
||||
) -> dict:
|
||||
...
|
||||
```
|
||||
|
||||
### delete() Method
|
||||
|
||||
#### No Breaking Changes
|
||||
```python
|
||||
# Same signature in both versions
|
||||
def delete(
|
||||
self,
|
||||
memory_id: str
|
||||
) -> dict:
|
||||
...
|
||||
```
|
||||
|
||||
### delete_all() Method
|
||||
|
||||
#### Breaking Change — Empty filter no longer silently deletes everything
|
||||
|
||||
**Before:** calling `delete_all()` with no filters silently deleted **all memories in the project**.
|
||||
|
||||
**After:**
|
||||
- No filters → raises a validation error (prevents accidental full-project wipe).
|
||||
- Concrete ID (e.g. `user_id="alice"`) → deletes memories for that entity (unchanged).
|
||||
- `"*"` for a filter → deletes all memories for that entity type across the project (new).
|
||||
- All four filters set to `"*"` → explicit full project wipe (new, requires opt-in on every parameter).
|
||||
|
||||
This change replaces the silent full-project delete (triggered by an empty or missing filter) with a validation error, and introduces `"*"` wildcards as the intentional path for bulk deletion.
|
||||
|
||||
```python
|
||||
# v0.x — no filter silently wiped all project memories
|
||||
m.delete_all() # DANGER: deleted everything
|
||||
m.delete_all(user_id="alice") # deleted alice's memories
|
||||
|
||||
# v1.x — no filter now raises an error; use "*" for intentional bulk deletes
|
||||
m.delete_all() # ERROR: at least one filter required
|
||||
m.delete_all(user_id="alice") # unchanged
|
||||
m.delete_all(user_id="*") # NEW — delete all users' memories
|
||||
m.delete_all(user_id="*", agent_id="*", app_id="*", run_id="*") # NEW — full project wipe
|
||||
```
|
||||
|
||||
## Platform Client (MemoryClient) Changes
|
||||
|
||||
### async_mode Default Changed
|
||||
|
||||
#### v0.x
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key="your-key")
|
||||
|
||||
# async_mode had to be explicitly set or had different default
|
||||
result = client.add("content", user_id="alice", async_mode=True)
|
||||
```
|
||||
|
||||
#### v1.0.0
|
||||
```python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key="your-key")
|
||||
|
||||
# async_mode defaults to True now (better performance)
|
||||
result = client.add("content", user_id="alice") # Uses async_mode=True by default
|
||||
|
||||
# Can still override if needed
|
||||
result = client.add("content", user_id="alice", async_mode=False)
|
||||
```
|
||||
|
||||
## Configuration Changes
|
||||
|
||||
### Memory Configuration
|
||||
|
||||
#### v0.x Config Options
|
||||
```python
|
||||
config = {
|
||||
"vector_store": {...},
|
||||
"llm": {...},
|
||||
"embedder": {...},
|
||||
"graph_store": {...},
|
||||
"version": "v1.0", # ❌ v1.0 no longer supported
|
||||
"history_db_path": "...",
|
||||
"custom_instructions": "..."
|
||||
}
|
||||
```
|
||||
|
||||
#### v1.0.0 Config Options
|
||||
```python
|
||||
config = {
|
||||
"vector_store": {...},
|
||||
"llm": {...},
|
||||
"embedder": {...},
|
||||
"graph_store": {...},
|
||||
"reranker": { # ✅ NEW: Reranker support
|
||||
"provider": "cohere",
|
||||
"config": {...}
|
||||
},
|
||||
"version": "v1.1", # ✅ v1.1+ only
|
||||
"history_db_path": "...",
|
||||
"custom_instructions": "...",
|
||||
"custom_update_memory_prompt": "..." # ✅ NEW: Custom update prompt
|
||||
}
|
||||
```
|
||||
|
||||
### New Configuration Options
|
||||
|
||||
#### Reranker Configuration
|
||||
```text
|
||||
# Cohere reranker
|
||||
"reranker": {
|
||||
"provider": "cohere",
|
||||
"config": {
|
||||
"model": "rerank-english-v3.0",
|
||||
"api_key": "your-api-key",
|
||||
"top_k": 10
|
||||
}
|
||||
}
|
||||
|
||||
# Sentence Transformer reranker
|
||||
"reranker": {
|
||||
"provider": "sentence_transformer",
|
||||
"config": {
|
||||
"model": "cross-encoder/ms-marco-MiniLM-L-6-v2",
|
||||
"device": "cuda"
|
||||
}
|
||||
}
|
||||
|
||||
# Hugging Face reranker
|
||||
"reranker": {
|
||||
"provider": "huggingface",
|
||||
"config": {
|
||||
"model": "BAAI/bge-reranker-base",
|
||||
"device": "cuda"
|
||||
}
|
||||
}
|
||||
|
||||
# LLM-based reranker
|
||||
"reranker": {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"api_key": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Changes
|
||||
|
||||
### New Error Types
|
||||
|
||||
#### v0.x Errors
|
||||
```python
|
||||
# Generic exceptions
|
||||
try:
|
||||
result = m.add("content", user_id="alice", version="v1.0")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
```
|
||||
|
||||
#### v1.0.0 Errors
|
||||
```python
|
||||
# More specific error handling
|
||||
try:
|
||||
result = m.add("content", user_id="alice")
|
||||
except ValueError as e:
|
||||
if "v1.0 API format is no longer supported" in str(e):
|
||||
# Handle version compatibility error
|
||||
pass
|
||||
elif "Invalid filter operator" in str(e):
|
||||
# Handle filter syntax error
|
||||
pass
|
||||
except TypeError as e:
|
||||
# Handle parameter errors
|
||||
pass
|
||||
except Exception as e:
|
||||
# Handle unexpected errors
|
||||
pass
|
||||
```
|
||||
|
||||
### Validation Changes
|
||||
|
||||
#### Stricter Parameter Validation
|
||||
|
||||
**v0.x (Lenient):**
|
||||
```python
|
||||
# Unknown parameters might be ignored
|
||||
result = m.add("content", user_id="alice", unknown_param="value")
|
||||
```
|
||||
|
||||
**v1.0.0 (Strict):**
|
||||
```python
|
||||
# Unknown parameters raise TypeError
|
||||
try:
|
||||
result = m.add("content", user_id="alice", unknown_param="value")
|
||||
except TypeError as e:
|
||||
print(f"Invalid parameter: {e}")
|
||||
```
|
||||
|
||||
## Response Schema Changes
|
||||
|
||||
### Memory Object Schema
|
||||
|
||||
#### v0.x Schema
|
||||
```python
|
||||
{
|
||||
"id": "mem_123",
|
||||
"memory": "User loves pizza",
|
||||
"user_id": "alice",
|
||||
"metadata": {...},
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
"score": 0.95 # In search results
|
||||
}
|
||||
```
|
||||
|
||||
#### v1.0.0 Schema (Enhanced)
|
||||
```python
|
||||
{
|
||||
"id": "mem_123",
|
||||
"memory": "User loves pizza",
|
||||
"user_id": "alice",
|
||||
"agent_id": "assistant", # ✅ More context
|
||||
"run_id": "session_001", # ✅ More context
|
||||
"metadata": {...},
|
||||
"categories": ["food"], # ✅ NEW: Auto-categorization
|
||||
"immutable": false, # ✅ NEW: Immutability flag
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-01T00:00:00Z",
|
||||
"score": 0.95, # In search results
|
||||
"rerank_score": 0.98 # ✅ NEW: If reranking used
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Code Examples
|
||||
|
||||
### Simple Migration
|
||||
|
||||
#### Before (v0.x)
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
m = Memory()
|
||||
|
||||
# Add with deprecated parameters
|
||||
result = m.add(
|
||||
"I love pizza",
|
||||
user_id="alice",
|
||||
output_format="v1.1",
|
||||
version="v1.0"
|
||||
)
|
||||
|
||||
# Handle variable response format
|
||||
if isinstance(result, list):
|
||||
memories = result
|
||||
else:
|
||||
memories = result.get("results", [])
|
||||
|
||||
for memory in memories:
|
||||
print(memory["memory"])
|
||||
```
|
||||
|
||||
#### After (v1.0.0 )
|
||||
```python
|
||||
from mem0 import Memory
|
||||
|
||||
m = Memory()
|
||||
|
||||
# Add without deprecated parameters
|
||||
result = m.add(
|
||||
"I love pizza",
|
||||
user_id="alice"
|
||||
)
|
||||
|
||||
# Always dict format with "results" key
|
||||
for memory in result["results"]:
|
||||
print(memory["memory"])
|
||||
```
|
||||
|
||||
### Advanced Migration
|
||||
|
||||
#### Before (v0.x)
|
||||
```python
|
||||
# Basic filtering
|
||||
results = m.search(
|
||||
"food preferences",
|
||||
user_id="alice",
|
||||
filters={"category": "food"},
|
||||
output_format="v1.1"
|
||||
)
|
||||
```
|
||||
|
||||
#### After (v1.0.0 )
|
||||
```python
|
||||
# Enhanced filtering with reranking
|
||||
results = m.search(
|
||||
"food preferences",
|
||||
user_id="alice",
|
||||
filters={
|
||||
"AND": [
|
||||
{"category": "food"},
|
||||
{"score": {"gte": 0.8}}
|
||||
]
|
||||
},
|
||||
rerank=True
|
||||
)
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
| Component | v0.x | v1.0.0 | Status |
|
||||
|-----------|------|-----------|---------|
|
||||
| `add()` method | Variable response | Standardized response | ⚠️ Breaking |
|
||||
| `search()` method | Basic filtering | Enhanced filtering + reranking | ⚠️ Breaking |
|
||||
| `get_all()` method | Variable response | Standardized response | ⚠️ Breaking |
|
||||
| Response format | Variable | Always `{"results": [...]}` | ⚠️ Breaking |
|
||||
| Reranking | ❌ Not available | ✅ Full support | ✅ New feature |
|
||||
| Advanced filtering | ❌ Basic only | ✅ Full operators | ✅ Enhancement |
|
||||
| Error handling | Generic | Specific error types | ✅ Improvement |
|
||||
|
||||
<Info>
|
||||
Use this reference to systematically update your codebase. Test each change thoroughly before deploying to production.
|
||||
</Info>
|
||||
@@ -120,13 +120,14 @@ client = MemoryClient(api_key="m0-...")
|
||||
|
||||
| Method | Open Source | Platform |
|
||||
| ------ | ----------- | -------- |
|
||||
| `search()` | `m.search(query, user_id="alex")` | `client.search(query, filters={"user_id": "alex"})` |
|
||||
| `get_all()` | `m.get_all(user_id="alex")` | `client.get_all(filters={"user_id": "alex"})` |
|
||||
| `search()` | `m.search(query, filters={"user_id": "alex"})` | `client.search(query, filters={"user_id": "alex"})` |
|
||||
| `get_all()` | `m.get_all(filters={"user_id": "alex"})` | `client.get_all(filters={"user_id": "alex"})` |
|
||||
| `add()` | `m.add(memory, user_id="alex")` | `client.add(memory, user_id="alex")` |
|
||||
| `update()` | `m.update(memory_id, data="Updated content")` | `client.update(memory_id, text="Updated content")` |
|
||||
| `delete()` | `m.delete(memory_id)` | `client.delete(memory_id)` |
|
||||
| `delete_all()` | `m.delete_all(user_id="alex")` | `client.delete_all(user_id="alex")` |
|
||||
|
||||
Note: `add()` and `delete()` methods remain unchanged. The `update()` method is not available in Platform - use delete + add pattern instead.
|
||||
Note: `add()` and `delete()` methods remain unchanged. The `update()` method is available in Platform via `client.update(memory_id, text="Updated content")`.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Search Memories">
|
||||
@@ -158,18 +159,15 @@ Note: `add()` and `delete()` methods remain unchanged. The `update()` method is
|
||||
<CodeGroup>
|
||||
```python Open Source (Old)
|
||||
# Get all memories for a user
|
||||
memories = m.get_all(user_id="alex", top_k=10)
|
||||
|
||||
# Get memories with pagination
|
||||
memories = m.get_all(user_id="alex", top_k=5, offset=10)
|
||||
memories = m.get_all(filters={"user_id": "alex"}, top_k=10)
|
||||
```
|
||||
|
||||
```python Platform (New)
|
||||
# Get all memories for a user
|
||||
memories = client.get_all(filters={"user_id": "alex"}, top_k=10)
|
||||
|
||||
# Get memories with pagination
|
||||
memories = client.get_all(filters={"user_id": "alex"}, top_k=5, offset=10)
|
||||
# Get memories with pagination (Platform supports page/page_size)
|
||||
memories = client.get_all(filters={"user_id": "alex"}, page=2, page_size=10)
|
||||
```
|
||||
</CodeGroup>
|
||||
</Accordion>
|
||||
@@ -218,16 +216,18 @@ Note: `add()` and `delete()` methods remain unchanged. The `update()` method is
|
||||
<CodeGroup>
|
||||
```python Open Source (Old)
|
||||
# Update memory content
|
||||
m.update(memory_id="mem_123", new_memory="Updated content")
|
||||
m.update(memory_id="mem_123", data="Updated content")
|
||||
```
|
||||
|
||||
```python Platform (New)
|
||||
# Update memory (not available in Platform)
|
||||
# Use delete + add pattern instead
|
||||
client.delete(memory_id="mem_123")
|
||||
client.add("Updated content", user_id="alex")
|
||||
# Update memory content
|
||||
client.update(memory_id="mem_123", text="Updated content")
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
The parameter name differs between SDKs: OSS `Memory.update()` takes `data=`, while the Platform `MemoryClient.update()` (Python and JS/TS) takes `text=`. When migrating, rename this keyword argument.
|
||||
</Note>
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
@@ -392,7 +392,7 @@ The Platform introduces powerful capabilities not available in OSS:
|
||||
| **Add Method** | `m.add(memory, user_id="x")` | `client.add(memory, user_id="x")` | No change |
|
||||
| **Delete Method** | `m.delete(memory_id)` | `client.delete(memory_id)` | No change |
|
||||
| **Delete All** | `m.delete_all(user_id="x")` | `client.delete_all(user_id="x")` | No change |
|
||||
| **Update Method** | `m.update(memory_id, new_memory)` | Use delete + add pattern | Replace with delete then add |
|
||||
| **Update Method** | `m.update(memory_id, data="Updated content")` | `client.update(memory_id, text="Updated content")` | Rename `data=` kwarg to `text=` |
|
||||
| **Config** | Local vector store + LLM config | Managed cloud infrastructure | Remove local config setup |
|
||||
|
||||
## Rollback plan
|
||||
|
||||
@@ -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 |
|
||||
| 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
|
||||
|
||||
@@ -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` |
|
||||
| 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 |
|
||||
| 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`
|
||||
|
||||
**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`
|
||||
|
||||
@@ -437,7 +437,7 @@ These parameters have been removed across all SDKs. Remove them from your code:
|
||||
|
||||
**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`
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ client.add(messages, user_id="alice")
|
||||
# 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
|
||||
|
||||
@@ -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>
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
## Tune component settings
|
||||
|
||||
@@ -18,7 +18,7 @@ icon: "bolt"
|
||||
</Warning>
|
||||
|
||||
<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>
|
||||
|
||||
## 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": "Order ID: 12345", "event": "ADD"},
|
||||
{"memory": "Order placed yesterday", "event": "ADD"}
|
||||
],
|
||||
"relations": []
|
||||
]
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
@@ -188,8 +187,7 @@ await memory.add("I like going to hikes", { userId: "user123" });
|
||||
|
||||
```json Output
|
||||
{
|
||||
"results": [],
|
||||
"relations": []
|
||||
"results": []
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
@@ -41,6 +41,14 @@ Multimodal support lets Mem0 extract facts from images alongside regular text. A
|
||||
|
||||
## 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
|
||||
|
||||
<CodeGroup>
|
||||
@@ -66,7 +74,7 @@ client.add(messages, user_id="alice")
|
||||
```
|
||||
|
||||
```ts TypeScript
|
||||
import { Memory } from "mem0ai";
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const client = new Memory();
|
||||
|
||||
@@ -123,7 +131,7 @@ client.add(messages, user_id="alice")
|
||||
|
||||
```ts TypeScript
|
||||
import fs from "fs";
|
||||
import { Memory } from "mem0ai";
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
function encodeImage(imagePath: string) {
|
||||
const buffer = fs.readFileSync(imagePath);
|
||||
@@ -226,7 +234,7 @@ client.add(messages, user_id="user123")
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from mem0 import Memory
|
||||
from mem0.exceptions import InvalidImageError, FileSizeError
|
||||
from mem0.exceptions import ValidationError
|
||||
|
||||
client = Memory()
|
||||
|
||||
@@ -242,16 +250,14 @@ try:
|
||||
client.add(messages, user_id="user123")
|
||||
print("Image processed successfully")
|
||||
|
||||
except InvalidImageError:
|
||||
print("Invalid image format or corrupted file")
|
||||
except FileSizeError:
|
||||
print("Image file too large")
|
||||
except ValidationError as exc:
|
||||
print(f"Image validation error: {exc}")
|
||||
except Exception as exc:
|
||||
print(f"Unexpected error: {exc}")
|
||||
```
|
||||
|
||||
```ts TypeScript
|
||||
import { Memory } from "mem0ai";
|
||||
import { Memory } from "mem0ai/oss";
|
||||
|
||||
const client = new Memory();
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@ config = {
|
||||
"provider": "llm_reranker",
|
||||
"config": {
|
||||
"provider": "openai",
|
||||
"model": "gpt-4o-mini",
|
||||
"model": "gpt-5-mini",
|
||||
"api_key": "your-openai-api-key",
|
||||
"top_k": 5
|
||||
}
|
||||
@@ -150,7 +150,7 @@ config = {
|
||||
"llm": {
|
||||
"provider": "openai",
|
||||
"config": {
|
||||
"model": "gpt-4",
|
||||
"model": "gpt-5-mini",
|
||||
"api_key": "your-openai-api-key"
|
||||
}
|
||||
},
|
||||
|
||||
+43
-8
@@ -1779,6 +1779,11 @@
|
||||
"type": "object",
|
||||
"description": "Entity and metadata filters. Must include at least one entity ID (`user_id`, `agent_id`, `app_id`, or `run_id`).",
|
||||
"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,
|
||||
"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": {
|
||||
"type": "string",
|
||||
"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`).",
|
||||
"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": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
@@ -2432,6 +2448,12 @@
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4861,8 +4883,7 @@
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"memory_id",
|
||||
"text"
|
||||
"memory_id"
|
||||
],
|
||||
"properties": {
|
||||
"memory_id": {
|
||||
@@ -4873,6 +4894,11 @@
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "The new text content for the memory"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "Updated metadata to associate with the memory."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4948,18 +4974,27 @@
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_ids": {
|
||||
"memories": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"memory_id": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "The unique identifier of the memory to delete."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"memory_id"
|
||||
]
|
||||
},
|
||||
"maxItems": 1000,
|
||||
"description": "Array of memory IDs to delete."
|
||||
"description": "Array of memory objects to delete."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"memory_ids"
|
||||
"memories"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6256,4 +6291,4 @@
|
||||
}
|
||||
},
|
||||
"x-original-swagger-version": "2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@
|
||||
"picomatch@<2.3.2": "^2.3.2",
|
||||
"@qdrant/js-client-rest": "^1.18.0",
|
||||
"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
|
||||
uuid@<11.1.1: '>=11.1.1'
|
||||
esbuild: '>=0.28.1'
|
||||
undici@<6.27.0: '>=6.27.0 <8.0.0'
|
||||
|
||||
importers:
|
||||
|
||||
@@ -1996,9 +1997,9 @@ packages:
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
undici@6.26.0:
|
||||
resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==}
|
||||
engines: {node: '>=18.17'}
|
||||
undici@7.28.0:
|
||||
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
@@ -2509,7 +2510,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@qdrant/openapi-typescript-fetch': 1.2.6
|
||||
typescript: 5.9.3
|
||||
undici: 6.26.0
|
||||
undici: 7.28.0
|
||||
|
||||
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
||||
|
||||
@@ -4063,7 +4064,7 @@ snapshots:
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
undici@6.26.0: {}
|
||||
undici@7.28.0: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
|
||||
@@ -21,3 +21,4 @@ overrides:
|
||||
"@qdrant/js-client-rest": "^1.18.0"
|
||||
"uuid@<11.1.1": ">=11.1.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 {
|
||||
safePath,
|
||||
normalizeModuleUrlToPath,
|
||||
loadSkill,
|
||||
loadTriagePrompt,
|
||||
loadCompactTriagePrompt,
|
||||
} from "./skill-loader.ts";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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", () => {
|
||||
it("keeps the core triage instructions without inlining the full skill body", () => {
|
||||
const prompt = loadCompactTriagePrompt();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
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 { 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
|
||||
// ============================================================================
|
||||
@@ -96,7 +104,7 @@ function resolveSkillsDir(): string {
|
||||
|
||||
// Strategy 1: import.meta.url (works in native ESM)
|
||||
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"));
|
||||
} catch {
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
"form-data@<4.0.6": ">=4.0.6",
|
||||
"uuid@<11.1.1": ">=11.1.1",
|
||||
"esbuild": ">=0.28.1",
|
||||
"undici@<6.27.0": ">=6.27.0 <8.0.0",
|
||||
"undici@>=8.0.0 <8.5.0": ">=8.5.0"
|
||||
}
|
||||
}
|
||||
|
||||
+6
-5
@@ -8,6 +8,7 @@ overrides:
|
||||
form-data@<4.0.6: '>=4.0.6'
|
||||
uuid@<11.1.1: '>=11.1.1'
|
||||
esbuild: '>=0.28.1'
|
||||
undici@<6.27.0: '>=6.27.0 <8.0.0'
|
||||
undici@>=8.0.0 <8.5.0: '>=8.5.0'
|
||||
|
||||
importers:
|
||||
@@ -2351,9 +2352,9 @@ packages:
|
||||
undici-types@7.24.6:
|
||||
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
|
||||
|
||||
undici@6.26.0:
|
||||
resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==}
|
||||
engines: {node: '>=18.17'}
|
||||
undici@7.28.0:
|
||||
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
undici@8.5.0:
|
||||
resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==}
|
||||
@@ -3202,7 +3203,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@qdrant/openapi-typescript-fetch': 1.2.6
|
||||
typescript: 6.0.3
|
||||
undici: 6.26.0
|
||||
undici: 7.28.0
|
||||
|
||||
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
||||
|
||||
@@ -4894,7 +4895,7 @@ snapshots:
|
||||
|
||||
undici-types@7.24.6: {}
|
||||
|
||||
undici@6.26.0: {}
|
||||
undici@7.28.0: {}
|
||||
|
||||
undici@8.5.0: {}
|
||||
|
||||
|
||||
@@ -5,4 +5,5 @@ overrides:
|
||||
"form-data@<4.0.6": ">=4.0.6"
|
||||
"uuid@<11.1.1": ">=11.1.1"
|
||||
"esbuild": ">=0.28.1"
|
||||
"undici@<6.27.0": ">=6.27.0 <8.0.0"
|
||||
"undici@>=8.0.0 <8.5.0": ">=8.5.0"
|
||||
|
||||
@@ -156,7 +156,8 @@
|
||||
"tar-fs@>=2.0.0 <2.1.4": "^2.1.4",
|
||||
"glob@>=10.2.0 <10.5.0": "^10.5.0",
|
||||
"@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
|
||||
'@modelcontextprotocol/sdk': ^1.25.4
|
||||
esbuild: '>=0.28.1'
|
||||
undici@<6.27.0: '>=6.27.0 <8.0.0'
|
||||
|
||||
importers:
|
||||
|
||||
@@ -2925,9 +2926,9 @@ packages:
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
undici@6.26.0:
|
||||
resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==}
|
||||
engines: {node: '>=18.17'}
|
||||
undici@7.28.0:
|
||||
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
update-browserslist-db@1.2.3:
|
||||
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
|
||||
@@ -3747,7 +3748,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@qdrant/openapi-typescript-fetch': 1.2.6
|
||||
typescript: 5.5.4
|
||||
undici: 6.26.0
|
||||
undici: 7.28.0
|
||||
|
||||
'@qdrant/openapi-typescript-fetch@1.2.6': {}
|
||||
|
||||
@@ -6113,7 +6114,7 @@ snapshots:
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
undici@6.26.0: {}
|
||||
undici@7.28.0: {}
|
||||
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.2):
|
||||
dependencies:
|
||||
|
||||
@@ -24,3 +24,4 @@ overrides:
|
||||
"glob@>=10.2.0 <10.5.0": "^10.5.0"
|
||||
"@modelcontextprotocol/sdk": "^1.25.4"
|
||||
"esbuild": ">=0.28.1"
|
||||
"undici@<6.27.0": ">=6.27.0 <8.0.0"
|
||||
|
||||
@@ -281,19 +281,22 @@ export default class MemoryClient {
|
||||
text,
|
||||
metadata,
|
||||
timestamp,
|
||||
expirationDate,
|
||||
}: {
|
||||
text?: string;
|
||||
metadata?: Record<string, any>;
|
||||
timestamp?: number | string;
|
||||
expirationDate?: string | null;
|
||||
},
|
||||
): Promise<Array<Memory>> {
|
||||
if (
|
||||
text === undefined &&
|
||||
metadata === undefined &&
|
||||
timestamp === undefined
|
||||
timestamp === undefined &&
|
||||
expirationDate === undefined
|
||||
) {
|
||||
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 (metadata !== undefined) payload.metadata = metadata;
|
||||
if (timestamp !== undefined) payload.timestamp = timestamp;
|
||||
if (expirationDate !== undefined) payload.expiration_date = expirationDate;
|
||||
|
||||
const payloadKeys = Object.keys(payload);
|
||||
this._captureEvent("update", [payloadKeys]);
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface AddMemoryOptions extends EntityOptions {
|
||||
customCategories?: custom_categories[];
|
||||
customInstructions?: string;
|
||||
timestamp?: number;
|
||||
expirationDate?: string;
|
||||
structuredDataSchema?: Record<string, any>;
|
||||
}
|
||||
|
||||
@@ -25,6 +26,7 @@ export interface SearchMemoryOptions {
|
||||
latestOnly?: boolean;
|
||||
fields?: string[];
|
||||
categories?: string[];
|
||||
showExpired?: boolean;
|
||||
}
|
||||
|
||||
export interface GetAllMemoryOptions {
|
||||
@@ -35,6 +37,7 @@ export interface GetAllMemoryOptions {
|
||||
endDate?: string;
|
||||
latestOnly?: boolean;
|
||||
categories?: string[];
|
||||
showExpired?: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteAllMemoryOptions extends EntityOptions {}
|
||||
@@ -119,6 +122,7 @@ export interface Memory {
|
||||
memoryType?: string;
|
||||
score?: number;
|
||||
metadata?: any | null;
|
||||
expirationDate?: string | null;
|
||||
owner?: string | null;
|
||||
agentId?: string | null;
|
||||
appId?: string | null;
|
||||
|
||||
@@ -59,6 +59,21 @@ describe("MemoryClient - add()", () => {
|
||||
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 () => {
|
||||
setupMockFetch();
|
||||
|
||||
@@ -176,11 +191,26 @@ describe("MemoryClient - update()", () => {
|
||||
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 () => {
|
||||
setupMockFetch();
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
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);
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v3/memories/search/", {
|
||||
@@ -300,4 +318,22 @@ describe("MemoryClient - getAll() entity param rejection", () => {
|
||||
const call = findFetchCall(mock, "/v3/memories/", "POST");
|
||||
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/mistral";
|
||||
export * from "./llms/langchain";
|
||||
export * from "./llms/litellm";
|
||||
export * from "./vector_stores/base";
|
||||
export * from "./vector_stores/memory";
|
||||
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_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)
|
||||
const filters: SearchFilters = {};
|
||||
|
||||
@@ -22,6 +22,8 @@ import { RedisDB } from "../vector_stores/redis";
|
||||
import { OllamaLLM } from "../llms/ollama";
|
||||
import { LMStudioLLM } from "../llms/lmstudio";
|
||||
import { DeepSeekLLM } from "../llms/deepseek";
|
||||
import { LiteLLM } from "../llms/litellm";
|
||||
import { MiniMaxLLM } from "../llms/minimax";
|
||||
import { SupabaseDB } from "../vector_stores/supabase";
|
||||
import { SQLiteManager } from "../storage/SQLiteManager";
|
||||
import { MemoryHistoryManager } from "../storage/MemoryHistoryManager";
|
||||
@@ -85,6 +87,10 @@ export class LLMFactory {
|
||||
return new LangchainLLM(config);
|
||||
case "deepseek":
|
||||
return new DeepSeekLLM(config);
|
||||
case "litellm":
|
||||
return new LiteLLM(config);
|
||||
case "minimax":
|
||||
return new MiniMaxLLM(config);
|
||||
default:
|
||||
throw new Error(`Unsupported LLM provider: ${provider}`);
|
||||
}
|
||||
|
||||
@@ -337,11 +337,14 @@ export class RedisDB implements VectorStore {
|
||||
const id = ids[idx];
|
||||
|
||||
// Create entry with required fields
|
||||
const createdAt = payload.created_at
|
||||
? new Date(payload.created_at).getTime()
|
||||
: 0;
|
||||
const entry: Record<string, any> = {
|
||||
memory_id: id,
|
||||
hash: payload.hash,
|
||||
memory: payload.data,
|
||||
created_at: new Date(payload.created_at).getTime(),
|
||||
hash: payload.hash ?? "",
|
||||
memory: payload.data ?? "",
|
||||
created_at: createdAt,
|
||||
embedding: new Float32Array(vector).buffer,
|
||||
};
|
||||
|
||||
@@ -561,12 +564,18 @@ export class RedisDB implements VectorStore {
|
||||
payload: Record<string, any>,
|
||||
): Promise<void> {
|
||||
const snakePayload = toSnakeCase(payload);
|
||||
const createdAt = snakePayload.created_at
|
||||
? new Date(snakePayload.created_at).getTime()
|
||||
: 0;
|
||||
const updatedAt = snakePayload.updated_at
|
||||
? new Date(snakePayload.updated_at).getTime()
|
||||
: 0;
|
||||
const entry: Record<string, any> = {
|
||||
memory_id: vectorId,
|
||||
hash: snakePayload.hash,
|
||||
memory: snakePayload.data,
|
||||
created_at: new Date(snakePayload.created_at).getTime(),
|
||||
updated_at: new Date(snakePayload.updated_at).getTime(),
|
||||
hash: snakePayload.hash ?? "",
|
||||
memory: snakePayload.data ?? "",
|
||||
created_at: createdAt,
|
||||
updated_at: updatedAt,
|
||||
embedding: Buffer.from(new Float32Array(vector).buffer),
|
||||
};
|
||||
|
||||
|
||||
@@ -92,6 +92,16 @@ jest.mock("../src/llms/deepseek", () => ({
|
||||
.fn()
|
||||
.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", () => ({
|
||||
Qdrant: jest
|
||||
@@ -206,6 +216,8 @@ describe("LLMFactory", () => {
|
||||
["langchain"],
|
||||
["lmstudio"],
|
||||
["deepseek"],
|
||||
["litellm"],
|
||||
["minimax"],
|
||||
])("creates LLM for provider '%s'", (provider) => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,128 @@
|
||||
import { RedisDB } from "../src/vector_stores/redis";
|
||||
|
||||
jest.mock("redis", () => ({
|
||||
createClient: jest.fn(() => ({
|
||||
connect: jest.fn(),
|
||||
on: jest.fn(),
|
||||
moduleList: jest.fn().mockResolvedValue([{ name: "search", ver: 20800 }]),
|
||||
ft: {
|
||||
create: jest.fn(),
|
||||
search: jest.fn(),
|
||||
info: jest.fn().mockRejectedValue(new Error("Unknown index")),
|
||||
_list: jest.fn().mockResolvedValue([]),
|
||||
},
|
||||
hSet: jest.fn(),
|
||||
hGetAll: jest.fn(),
|
||||
del: jest.fn(),
|
||||
exists: jest.fn(),
|
||||
quit: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
function createStore(): RedisDB {
|
||||
return new RedisDB({
|
||||
redisUrl: "redis://localhost:6379",
|
||||
collectionName: "test",
|
||||
embeddingModelDims: 4,
|
||||
});
|
||||
}
|
||||
|
||||
describe("RedisDB – entity payload handling", () => {
|
||||
let store: RedisDB;
|
||||
let mockClient: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
store = createStore();
|
||||
await store.initialize();
|
||||
mockClient = (store as any).client;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("insert with entity payload (no hash/created_at) does not produce NaN", async () => {
|
||||
const entityPayload = {
|
||||
data: "OpenAI",
|
||||
entityType: "organization",
|
||||
linkedMemoryIds: ["mem-1"],
|
||||
userId: "test_user",
|
||||
};
|
||||
|
||||
await store.insert([[0.1, 0.2, 0.3, 0.4]], ["entity-1"], [entityPayload]);
|
||||
|
||||
expect(mockClient.hSet).toHaveBeenCalledTimes(1);
|
||||
const call = mockClient.hSet.mock.calls[0];
|
||||
const entry = call[1];
|
||||
|
||||
expect(entry.memory_id).toBe("entity-1");
|
||||
expect(entry.memory).toBe("OpenAI");
|
||||
expect(entry.hash).toBe("");
|
||||
expect(entry.created_at).toBe(0);
|
||||
expect(Number.isNaN(entry.created_at)).toBe(false);
|
||||
});
|
||||
|
||||
test("update with entity payload (no hash/created_at/updated_at) does not produce NaN", async () => {
|
||||
const entityPayload = {
|
||||
data: "OpenAI",
|
||||
entityType: "organization",
|
||||
linkedMemoryIds: ["mem-1"],
|
||||
userId: "test_user",
|
||||
};
|
||||
|
||||
await store.update("entity-1", [0.1, 0.2, 0.3, 0.4], entityPayload);
|
||||
|
||||
expect(mockClient.hSet).toHaveBeenCalledTimes(1);
|
||||
const call = mockClient.hSet.mock.calls[0];
|
||||
const entry = call[1];
|
||||
|
||||
expect(entry.memory_id).toBe("entity-1");
|
||||
expect(entry.memory).toBe("OpenAI");
|
||||
expect(entry.hash).toBe("");
|
||||
expect(entry.created_at).toBe(0);
|
||||
expect(entry.updated_at).toBe(0);
|
||||
expect(Number.isNaN(entry.created_at)).toBe(false);
|
||||
expect(Number.isNaN(entry.updated_at)).toBe(false);
|
||||
});
|
||||
|
||||
test("update with normal payload preserves timestamps", async () => {
|
||||
const normalPayload = {
|
||||
data: "likes coffee",
|
||||
hash: "abc123",
|
||||
createdAt: "2026-06-25T10:00:00.000Z",
|
||||
updatedAt: "2026-06-25T12:00:00.000Z",
|
||||
userId: "test_user",
|
||||
};
|
||||
|
||||
await store.update("mem-1", [0.1, 0.2, 0.3, 0.4], normalPayload);
|
||||
|
||||
const call = mockClient.hSet.mock.calls[0];
|
||||
const entry = call[1];
|
||||
|
||||
expect(entry.hash).toBe("abc123");
|
||||
expect(entry.memory).toBe("likes coffee");
|
||||
expect(entry.created_at).toBeGreaterThan(0);
|
||||
expect(entry.updated_at).toBeGreaterThan(0);
|
||||
expect(Number.isNaN(entry.created_at)).toBe(false);
|
||||
expect(Number.isNaN(entry.updated_at)).toBe(false);
|
||||
});
|
||||
|
||||
test("insert with normal payload preserves timestamp", async () => {
|
||||
const normalPayload = {
|
||||
data: "likes coffee",
|
||||
hash: "abc123",
|
||||
createdAt: "2026-06-25T10:00:00.000Z",
|
||||
userId: "test_user",
|
||||
};
|
||||
|
||||
await store.insert([[0.1, 0.2, 0.3, 0.4]], ["mem-1"], [normalPayload]);
|
||||
|
||||
const call = mockClient.hSet.mock.calls[0];
|
||||
const entry = call[1];
|
||||
|
||||
expect(entry.hash).toBe("abc123");
|
||||
expect(entry.memory).toBe("likes coffee");
|
||||
expect(entry.created_at).toBeGreaterThan(0);
|
||||
expect(Number.isNaN(entry.created_at)).toBe(false);
|
||||
});
|
||||
});
|
||||
+14
-10
@@ -344,24 +344,26 @@ class MemoryClient:
|
||||
Args:
|
||||
memory_id: The ID of the memory to update.
|
||||
options: Typed options (UpdateMemoryOptions) with text, metadata,
|
||||
and/or timestamp fields.
|
||||
**kwargs: Alternatively pass text, metadata, timestamp as keyword args.
|
||||
timestamp, and/or expiration_date fields.
|
||||
**kwargs: Alternatively pass text, metadata, timestamp, or
|
||||
expiration_date as keyword args.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The response from the server.
|
||||
|
||||
Raises:
|
||||
ValueError: If none of text, metadata, or timestamp are provided.
|
||||
ValueError: If none of text, metadata, timestamp, or expiration_date are provided.
|
||||
|
||||
Example:
|
||||
>>> client.update("mem_123", UpdateMemoryOptions(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 = {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:
|
||||
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"})
|
||||
params = self._prepare_params()
|
||||
@@ -1260,24 +1262,26 @@ class AsyncMemoryClient:
|
||||
Args:
|
||||
memory_id: The ID of the memory to update.
|
||||
options: Typed options (UpdateMemoryOptions) with text, metadata,
|
||||
and/or timestamp fields.
|
||||
**kwargs: Alternatively pass text, metadata, timestamp as keyword args.
|
||||
timestamp, and/or expiration_date fields.
|
||||
**kwargs: Alternatively pass text, metadata, timestamp, or
|
||||
expiration_date as keyword args.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: The response from the server.
|
||||
|
||||
Raises:
|
||||
ValueError: If none of text, metadata, or timestamp are provided.
|
||||
ValueError: If none of text, metadata, timestamp, or expiration_date are provided.
|
||||
|
||||
Example:
|
||||
>>> await client.update("mem_123", UpdateMemoryOptions(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 = {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:
|
||||
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"})
|
||||
params = self._prepare_params()
|
||||
|
||||
@@ -29,6 +29,7 @@ class AddMemoryOptions(BaseModel):
|
||||
)
|
||||
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")
|
||||
expiration_date: Optional[str] = Field(default=None, description="Expiration date in YYYY-MM-DD format")
|
||||
structured_data_schema: Optional[Dict[str, Any]] = Field(
|
||||
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")
|
||||
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")
|
||||
show_expired: Optional[bool] = Field(default=None, description="Whether to include expired memories")
|
||||
|
||||
|
||||
class GetAllMemoryOptions(BaseModel):
|
||||
@@ -71,6 +73,7 @@ class GetAllMemoryOptions(BaseModel):
|
||||
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")
|
||||
show_expired: Optional[bool] = Field(default=None, description="Whether to include expired memories")
|
||||
|
||||
|
||||
class DeleteAllMemoryOptions(BaseModel):
|
||||
@@ -91,6 +94,7 @@ class UpdateMemoryOptions(BaseModel):
|
||||
text: Optional[str] = Field(default=None, description="New text content for the memory")
|
||||
metadata: Optional[Dict[str, Any]] = Field(default=None, description="Updated metadata")
|
||||
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):
|
||||
|
||||
+153
-25
@@ -9,7 +9,7 @@ import time
|
||||
import uuid
|
||||
import warnings
|
||||
from copy import deepcopy
|
||||
from datetime import datetime, timezone
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from pydantic import ValidationError
|
||||
@@ -378,9 +378,37 @@ def _entity_collection_name(provider: str, collection_name: str) -> str:
|
||||
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()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_UNSET = object()
|
||||
_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,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
timestamp: Optional[Any] = None,
|
||||
expiration_date: Optional[Any] = None,
|
||||
infer: bool = True,
|
||||
memory_type: 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.
|
||||
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
|
||||
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
|
||||
'messages' and decide whether to add, update, or delete related memories.
|
||||
If False, 'messages' are added as raw memories directly.
|
||||
@@ -737,6 +768,7 @@ class Memory(MemoryBase):
|
||||
if timestamp is not None:
|
||||
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)
|
||||
processed_metadata, effective_filters = _build_filters_and_metadata(
|
||||
user_id=user_id,
|
||||
@@ -744,6 +776,8 @@ class Memory(MemoryBase):
|
||||
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:
|
||||
raise Mem0ValidationError(
|
||||
@@ -1141,6 +1175,7 @@ class Memory(MemoryBase):
|
||||
"actor_id",
|
||||
"role",
|
||||
"attributed_to",
|
||||
"expiration_date",
|
||||
]
|
||||
|
||||
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,
|
||||
top_k: int = 20,
|
||||
show_expired: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -1179,6 +1215,7 @@ class Memory(MemoryBase):
|
||||
Must contain at least one of: user_id, agent_id, run_id.
|
||||
Example: filters={"user_id": "u1", "agent_id": "a1"}
|
||||
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:
|
||||
dict: A dictionary containing a list of memories under the "results" key.
|
||||
@@ -1217,6 +1254,7 @@ class Memory(MemoryBase):
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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"}
|
||||
)
|
||||
|
||||
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:
|
||||
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")
|
||||
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)
|
||||
|
||||
# Handle different vector store return formats by inspecting first element
|
||||
@@ -1255,11 +1293,14 @@ class Memory(MemoryBase):
|
||||
"actor_id",
|
||||
"role",
|
||||
"attributed_to",
|
||||
"expiration_date",
|
||||
]
|
||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
||||
|
||||
formatted_memories = []
|
||||
for mem in actual_memories:
|
||||
if not show_expired and _payload_is_expired(mem.payload):
|
||||
continue
|
||||
memory_item_dict = MemoryItem(
|
||||
id=mem.id,
|
||||
memory=mem.payload.get("data", ""),
|
||||
@@ -1277,6 +1318,8 @@ class Memory(MemoryBase):
|
||||
memory_item_dict["metadata"] = additional_metadata
|
||||
|
||||
formatted_memories.append(memory_item_dict)
|
||||
if output_limit is not None and len(formatted_memories) >= output_limit:
|
||||
break
|
||||
|
||||
return formatted_memories
|
||||
|
||||
@@ -1290,6 +1333,7 @@ class Memory(MemoryBase):
|
||||
rerank: bool = False,
|
||||
explain: bool = False,
|
||||
reference_date: Optional[Any] = None,
|
||||
show_expired: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -1322,6 +1366,7 @@ class Memory(MemoryBase):
|
||||
rerank (bool, optional): Whether to rerank results. 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.
|
||||
show_expired (bool, optional): Include expired memories. Defaults to False.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the search results under a "results" key.
|
||||
@@ -1393,7 +1438,9 @@ class Memory(MemoryBase):
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
# Apply reranking if enabled and reranker is available
|
||||
@@ -1525,7 +1572,7 @@ class Memory(MemoryBase):
|
||||
return True
|
||||
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)
|
||||
if threshold is None:
|
||||
threshold = 0.1
|
||||
@@ -1566,11 +1613,14 @@ class Memory(MemoryBase):
|
||||
# Step 7: Build candidate set from semantic results
|
||||
candidates = []
|
||||
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)
|
||||
candidates.append({
|
||||
"id": mem_id,
|
||||
"score": mem.score,
|
||||
"payload": mem.payload if hasattr(mem, 'payload') else {},
|
||||
"payload": payload,
|
||||
})
|
||||
|
||||
# Step 8: Score and rank
|
||||
@@ -1591,6 +1641,7 @@ class Memory(MemoryBase):
|
||||
"actor_id",
|
||||
"role",
|
||||
"attributed_to",
|
||||
"expiration_date",
|
||||
]
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
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.
|
||||
expiration_date (Any, optional): Date in YYYY-MM-DD format, or None to clear it.
|
||||
|
||||
Returns:
|
||||
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"})
|
||||
|
||||
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")
|
||||
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.
|
||||
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] = {}
|
||||
if 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'")
|
||||
|
||||
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)
|
||||
if metadata is not None:
|
||||
@@ -1932,8 +2009,9 @@ class Memory(MemoryBase):
|
||||
# Entity-store cleanup: strip this memory's id from old-text entities,
|
||||
# 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)}
|
||||
self._remove_memory_from_entity_store(memory_id, session_filters)
|
||||
self._link_entities_for_memory(memory_id, data, session_filters)
|
||||
if text_changed:
|
||||
self._remove_memory_from_entity_store(memory_id, session_filters)
|
||||
self._link_entities_for_memory(memory_id, data, session_filters)
|
||||
|
||||
return memory_id
|
||||
|
||||
@@ -2280,6 +2358,7 @@ class AsyncMemory(MemoryBase):
|
||||
run_id: Optional[str] = None,
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
timestamp: Optional[Any] = None,
|
||||
expiration_date: Optional[Any] = None,
|
||||
infer: bool = True,
|
||||
memory_type: 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.
|
||||
metadata (dict, optional): Metadata to store with the memory. Defaults to None.
|
||||
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.
|
||||
memory_type (str, optional): Type of memory to create. Defaults to None.
|
||||
Pass "procedural_memory" to create procedural memories.
|
||||
@@ -2306,10 +2387,13 @@ class AsyncMemory(MemoryBase):
|
||||
if timestamp is not None:
|
||||
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)
|
||||
processed_metadata, effective_filters = _build_filters_and_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:
|
||||
raise ValueError(
|
||||
@@ -2712,6 +2796,7 @@ class AsyncMemory(MemoryBase):
|
||||
"actor_id",
|
||||
"role",
|
||||
"attributed_to",
|
||||
"expiration_date",
|
||||
]
|
||||
|
||||
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,
|
||||
top_k: int = 20,
|
||||
show_expired: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -2750,6 +2836,7 @@ class AsyncMemory(MemoryBase):
|
||||
Must contain at least one of: user_id, agent_id, run_id.
|
||||
Example: filters={"user_id": "u1", "agent_id": "a1"}
|
||||
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:
|
||||
dict: A dictionary containing a list of memories under the "results" key.
|
||||
@@ -2788,6 +2875,7 @@ class AsyncMemory(MemoryBase):
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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"}
|
||||
)
|
||||
|
||||
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:
|
||||
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")
|
||||
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)
|
||||
|
||||
# Handle different vector store return formats by inspecting first element
|
||||
@@ -2826,11 +2914,14 @@ class AsyncMemory(MemoryBase):
|
||||
"actor_id",
|
||||
"role",
|
||||
"attributed_to",
|
||||
"expiration_date",
|
||||
]
|
||||
core_and_promoted_keys = {"data", "hash", "created_at", "updated_at", "id", "text_lemmatized", "attributed_to", *promoted_payload_keys}
|
||||
|
||||
formatted_memories = []
|
||||
for mem in actual_memories:
|
||||
if not show_expired and _payload_is_expired(mem.payload):
|
||||
continue
|
||||
memory_item_dict = MemoryItem(
|
||||
id=mem.id,
|
||||
memory=mem.payload.get("data", ""),
|
||||
@@ -2848,6 +2939,8 @@ class AsyncMemory(MemoryBase):
|
||||
memory_item_dict["metadata"] = additional_metadata
|
||||
|
||||
formatted_memories.append(memory_item_dict)
|
||||
if output_limit is not None and len(formatted_memories) >= output_limit:
|
||||
break
|
||||
|
||||
return formatted_memories
|
||||
|
||||
@@ -2861,6 +2954,7 @@ class AsyncMemory(MemoryBase):
|
||||
rerank: bool = False,
|
||||
explain: bool = False,
|
||||
reference_date: Optional[Any] = None,
|
||||
show_expired: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
@@ -2893,6 +2987,7 @@ class AsyncMemory(MemoryBase):
|
||||
rerank (bool, optional): Whether to rerank results. 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.
|
||||
show_expired (bool, optional): Include expired memories. Defaults to False.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the search results under a "results" key.
|
||||
@@ -2968,7 +3063,9 @@ class AsyncMemory(MemoryBase):
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
# Apply reranking if enabled and reranker is available
|
||||
@@ -3103,7 +3200,7 @@ class AsyncMemory(MemoryBase):
|
||||
return True
|
||||
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:
|
||||
threshold = 0.1
|
||||
|
||||
@@ -3143,11 +3240,14 @@ class AsyncMemory(MemoryBase):
|
||||
# Step 7: Build candidate set from semantic results
|
||||
candidates = []
|
||||
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)
|
||||
candidates.append({
|
||||
"id": mem_id,
|
||||
"score": mem.score,
|
||||
"payload": mem.payload if hasattr(mem, 'payload') else {},
|
||||
"payload": payload,
|
||||
})
|
||||
|
||||
# Step 8: Score and rank
|
||||
@@ -3168,6 +3268,7 @@ class AsyncMemory(MemoryBase):
|
||||
"actor_id",
|
||||
"role",
|
||||
"attributed_to",
|
||||
"expiration_date",
|
||||
]
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
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.
|
||||
expiration_date (Any, optional): Date in YYYY-MM-DD format, or None to clear it.
|
||||
|
||||
Returns:
|
||||
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"})
|
||||
|
||||
embeddings = await asyncio.to_thread(self.embedding_model.embed, data, "update")
|
||||
existing_embeddings = {data: embeddings}
|
||||
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.")
|
||||
|
||||
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")
|
||||
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.
|
||||
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 = {}
|
||||
if user_id:
|
||||
filters["user_id"] = user_id
|
||||
@@ -3456,7 +3578,7 @@ class AsyncMemory(MemoryBase):
|
||||
if llm is not None:
|
||||
parsed_messages = convert_to_messages(parsed_messages)
|
||||
response = await asyncio.to_thread(llm.invoke, input=parsed_messages)
|
||||
procedural_memory = response.content
|
||||
procedural_memory = remove_code_blocks(response.content)
|
||||
else:
|
||||
procedural_memory = await asyncio.to_thread(self.llm.generate_response, messages=parsed_messages)
|
||||
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'")
|
||||
|
||||
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)
|
||||
if metadata is not None:
|
||||
@@ -3534,8 +3661,9 @@ class AsyncMemory(MemoryBase):
|
||||
# Entity-store cleanup: strip this memory's id from old-text entities,
|
||||
# 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)}
|
||||
await self._remove_memory_from_entity_store(memory_id, session_filters)
|
||||
await self._link_entities_for_memory(memory_id, data, session_filters)
|
||||
if text_changed:
|
||||
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
|
||||
|
||||
|
||||
+26
-3
@@ -1418,7 +1418,30 @@ def _get_provider_memory_count(memory_instance) -> Optional[int]:
|
||||
try:
|
||||
col_info = getattr(vector_store, "col_info", None)
|
||||
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:
|
||||
return None
|
||||
|
||||
@@ -1430,7 +1453,7 @@ def _extract_count(info: Any) -> Optional[int]:
|
||||
return None
|
||||
|
||||
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)
|
||||
if value is not None:
|
||||
return value
|
||||
@@ -1445,7 +1468,7 @@ def _extract_count(info: Any) -> Optional[int]:
|
||||
except Exception:
|
||||
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)
|
||||
if value is not None:
|
||||
return value
|
||||
|
||||
@@ -59,6 +59,26 @@ class HuggingFaceReranker(BaseReranker):
|
||||
self.model.to(self.device)
|
||||
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]]:
|
||||
"""
|
||||
Rerank documents using HuggingFace cross-encoder model.
|
||||
@@ -118,9 +138,7 @@ class HuggingFaceReranker(BaseReranker):
|
||||
|
||||
# Normalize scores if requested
|
||||
if self.config.normalize:
|
||||
scores = np.array(scores)
|
||||
scores = (scores - scores.min()) / (scores.max() - scores.min() + 1e-8)
|
||||
scores = scores.tolist()
|
||||
scores = self._normalize_scores(scores)
|
||||
|
||||
# Combine documents with scores
|
||||
doc_score_pairs = list(zip(documents, scores))
|
||||
|
||||
@@ -124,11 +124,13 @@ class RedisDB(VectorStoreBase):
|
||||
data = []
|
||||
for vector, payload, id in zip(vectors, payloads, ids):
|
||||
# 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 = {
|
||||
"memory_id": id,
|
||||
"hash": payload["hash"],
|
||||
"memory": payload["data"],
|
||||
"created_at": int(datetime.fromisoformat(payload["created_at"]).timestamp()),
|
||||
"hash": payload.get("hash", ""),
|
||||
"memory": payload.get("data", ""),
|
||||
"created_at": created_at_ts,
|
||||
"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}")
|
||||
|
||||
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 = {
|
||||
"memory_id": vector_id,
|
||||
"hash": payload["hash"],
|
||||
"memory": payload["data"],
|
||||
"created_at": int(datetime.fromisoformat(payload["created_at"]).timestamp()),
|
||||
"updated_at": int(datetime.fromisoformat(payload["updated_at"]).timestamp()),
|
||||
"hash": payload.get("hash", ""),
|
||||
"memory": payload.get("data", ""),
|
||||
"created_at": created_at_ts,
|
||||
"updated_at": updated_at_ts,
|
||||
}
|
||||
|
||||
# Only update embedding if vector is provided
|
||||
|
||||
@@ -41,7 +41,20 @@ class OutputData(BaseModel):
|
||||
payload: Dict
|
||||
|
||||
|
||||
_VALKEY_TAG_SPECIAL = set(r',.<>{}[]"\':;!@#$%^&*()-+=~| ')
|
||||
|
||||
|
||||
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__(
|
||||
self,
|
||||
valkey_url: str,
|
||||
@@ -329,8 +342,8 @@ class ValkeyDB(VectorStoreBase):
|
||||
knn_part (str): The KNN part of the query.
|
||||
filters (dict, optional): Filters to apply to the search. 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. Multiple filters are
|
||||
Values are escaped via _escape_tag_value() before interpolation
|
||||
to prevent wildcard/operator injection. Multiple filters are
|
||||
combined with AND logic (space-separated).
|
||||
|
||||
Returns:
|
||||
@@ -345,8 +358,8 @@ class ValkeyDB(VectorStoreBase):
|
||||
filter_parts = []
|
||||
for key, value in filters.items():
|
||||
if value is not None:
|
||||
# Use the correct filter syntax for Valkey
|
||||
filter_parts.append(f"@{key}:{{{value}}}")
|
||||
escaped = self._escape_tag_value(value)
|
||||
filter_parts.append(f"@{key}:{{{escaped}}}")
|
||||
|
||||
# No valid filter parts
|
||||
if not filter_parts:
|
||||
@@ -750,34 +763,6 @@ class ValkeyDB(VectorStoreBase):
|
||||
logger.exception(f"Error resetting index {self.collection_name}: {e}")
|
||||
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:
|
||||
"""
|
||||
List all recent created memories from the vector store.
|
||||
@@ -785,8 +770,8 @@ class ValkeyDB(VectorStoreBase):
|
||||
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 without validation - wildcards, special characters,
|
||||
lists, etc. are passed through literally to Valkey search.
|
||||
Values are escaped via _escape_tag_value() before interpolation
|
||||
to prevent wildcard/operator injection.
|
||||
Multiple filters are combined with AND logic.
|
||||
top_k (int, optional): Maximum number of results to return. Defaults to 1000
|
||||
if not specified.
|
||||
|
||||
+20
-5
@@ -182,14 +182,16 @@ class MemoryCreate(BaseModel):
|
||||
agent_id: Optional[str] = None
|
||||
run_id: Optional[str] = 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.")
|
||||
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.")
|
||||
|
||||
|
||||
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.")
|
||||
expiration_date: Optional[str] = Field(None, description="Expiration date in YYYY-MM-DD format, or null to clear.")
|
||||
|
||||
|
||||
class SearchRequest(BaseModel):
|
||||
@@ -201,6 +203,7 @@ class SearchRequest(BaseModel):
|
||||
top_k: Optional[int] = Field(None, description="Maximum number of results to return.")
|
||||
threshold: Optional[float] = Field(None, description="Minimum similarity score for results.")
|
||||
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):
|
||||
@@ -379,7 +382,7 @@ def add_memory(memory_create: MemoryCreate, _auth=Depends(verify_auth)):
|
||||
|
||||
|
||||
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]:
|
||||
@@ -391,6 +394,7 @@ def _serialize_memory(row: Any) -> Dict[str, Any]:
|
||||
"agent_id": payload.get("agent_id"),
|
||||
"run_id": payload.get("run_id"),
|
||||
"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},
|
||||
"created_at": payload.get("created_at"),
|
||||
"updated_at": payload.get("updated_at"),
|
||||
@@ -410,6 +414,7 @@ def get_all_memories(
|
||||
run_id: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
top_k: Optional[int] = Query(None, ge=0, le=ALL_MEMORIES_LIMIT),
|
||||
show_expired: bool = Query(False),
|
||||
_auth=Depends(verify_auth),
|
||||
):
|
||||
"""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")
|
||||
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.")
|
||||
# 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)
|
||||
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
|
||||
@@ -425,6 +431,7 @@ def get_all_memories(
|
||||
params = {"filters": filters}
|
||||
if top_k is not None:
|
||||
params["top_k"] = top_k
|
||||
params["show_expired"] = show_expired
|
||||
return get_memory_instance().get_all(**params)
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -465,6 +472,8 @@ def search_memories(search_req: SearchRequest, _auth=Depends(verify_auth)):
|
||||
params["threshold"] = search_req.threshold
|
||||
if search_req.explain is not None:
|
||||
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)
|
||||
except ValueError as 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)):
|
||||
"""Update an existing memory."""
|
||||
try:
|
||||
return get_memory_instance().update(
|
||||
memory_id=memory_id, data=updated_memory.text, metadata=updated_memory.metadata
|
||||
)
|
||||
fields_set = getattr(updated_memory, "model_fields_set", getattr(updated_memory, "__fields_set__", set()))
|
||||
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:
|
||||
raise _client_error(e)
|
||||
except Exception:
|
||||
|
||||
@@ -167,6 +167,33 @@ class TestAsyncUpdate:
|
||||
"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
|
||||
class TestAsyncAddToVectorStoreErrors:
|
||||
|
||||
@@ -1403,6 +1403,30 @@ def test_scale_threshold_provider_count_helpers_are_safe():
|
||||
def col_info(self):
|
||||
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.vector_store = CountStore()
|
||||
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()
|
||||
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({"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"}},
|
||||
)
|
||||
|
||||
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):
|
||||
"""search() should reject user_id as top-level kwarg."""
|
||||
with pytest.raises(ValueError, match=r"user_id"):
|
||||
@@ -100,6 +114,39 @@ class TestGetAllEntityParamRejection:
|
||||
with pytest.raises(ValueError, match=r"run_id"):
|
||||
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:
|
||||
"""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):
|
||||
mock_memory = Mock(
|
||||
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):
|
||||
memory_instance.embedding_model = Mock()
|
||||
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):
|
||||
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]["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():
|
||||
@@ -286,6 +396,24 @@ class TestEntityIdValidation:
|
||||
with pytest.raises(ValueError, match="Invalid user_id.*cannot contain whitespace"):
|
||||
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:
|
||||
"""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")
|
||||
|
||||
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"})
|
||||
assert resp.status_code == 200
|
||||
_, kwargs = mock_memory.update.call_args
|
||||
assert kwargs["metadata"] is None
|
||||
assert "metadata" not in kwargs
|
||||
|
||||
def test_missing_text_returns_422(self, client):
|
||||
"""text is required — omitting it should fail validation."""
|
||||
resp = client.put("/memories/mem-1", json={"metadata": {"k": "v"}})
|
||||
assert resp.status_code == 422
|
||||
def test_expiration_date_forwarded_without_text(self, client, mock_memory):
|
||||
resp = client.put("/memories/mem-1", json={"expiration_date": "2999-01-01"})
|
||||
assert resp.status_code == 200
|
||||
_, 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):
|
||||
"""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
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
"""Test processing document fields from hash results."""
|
||||
# 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.
|
||||
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