Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 47a69e1e72 | |||
| ea9bbcabed | |||
| 0cddc36d52 | |||
| 83b07b1537 | |||
| 8c02c425a5 | |||
| f8082a7345 | |||
| 5d38e3703a | |||
| fd8fd087ea | |||
| a214ec37bc | |||
| 8b38da9ab8 | |||
| 17852dc648 | |||
| a39a802bbc |
@@ -12,7 +12,7 @@
|
||||
"name": "mem0",
|
||||
"source": "./integrations/claude-code-plugin",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"version": "0.3.2"
|
||||
"version": "0.3.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"name": "mem0",
|
||||
"source": "./integrations/cursor-plugin",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"version": "0.3.2"
|
||||
"version": "0.3.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"displayName": "Mem0",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"homepage": "https://mem0.ai",
|
||||
"keywords": ["memory", "personalization", "mcp", "semantic-search"],
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: 'Generate Profiles'
|
||||
description: "Start one generation: sample a few entities, or build one for a single entity."
|
||||
openapi: post /v2/profiles/jobs/
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: 'Get Generation Job'
|
||||
description: "Read the progress of a generation, and whether it finished."
|
||||
openapi: get /v2/profiles/jobs/{job_id}/
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: 'Get Profile Settings'
|
||||
description: "Retrieve the profile schema, custom instructions, and enabled flag for the current project."
|
||||
openapi: get /v2/profiles/settings/
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: 'Get Profile'
|
||||
description: "Retrieve the structured profile for a user, with a status describing whether generation has completed."
|
||||
openapi: get /v2/entities/{entity_type}/{entity_id}/profile/
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: 'Update Profile Settings'
|
||||
description: "Set the JSON Schema, custom instructions, or enabled flag that control profile generation for the project."
|
||||
openapi: post /v2/profiles/settings/
|
||||
---
|
||||
+102
-1
@@ -7,6 +7,16 @@ mode: "wide"
|
||||
<Tabs>
|
||||
<Tab title="Python">
|
||||
|
||||
<Update label="2026-09-23" description="v2.2.0">
|
||||
|
||||
**New Features:**
|
||||
- **Client:** Add User Profiles to `MemoryClient` and `AsyncMemoryClient`: `get_profile()`, `generate_profile()`, `get_profile_settings()`, `update_profile_settings()`, `sample_profiles()`, and `get_profile_job()`. A profile is a structured, always-current JSON summary of one user, shaped by a JSON Schema you configure per project and filled by an LLM from that user's memories. Generation is asynchronous. Every job POST carries an `Idempotency-Key`; to retry a lost request without starting a second job, pass the same `idempotency_key` on each attempt ([#7340](https://github.com/mem0ai/mem0/pull/7340))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Vector Stores:** Guard against `None` timestamps in the Valkey vector store's `insert()` and `update()` paths. `created_at` and `updated_at` fields that were present in the payload but set to `None` previously passed the `"created_at" not in payload` / `"updated_at" in payload` checks and raised `TypeError` when `datetime.fromisoformat()` received `None`. Both paths now use `.get()` with a truthiness check so `None` values fall through to the default, matching the Redis provider's behavior ([#6993](https://github.com/mem0ai/mem0/pull/6993))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-18" description="v2.1.0">
|
||||
|
||||
**Improvements:**
|
||||
@@ -182,7 +192,7 @@ mode: "wide"
|
||||
<Update label="2026-06-24" description="v2.0.8">
|
||||
|
||||
**New Features:**
|
||||
- **Embeddings:** Add native `embed_batch` to five embedders: LM Studio, Together, HuggingFace, Vertex AI, and Google GenAI: for batched embedding requests ([#5609](https://github.com/mem0ai/mem0/pull/5609))
|
||||
- **Embeddings:** Add native `embed_batch` to five embedders for batched embedding requests: LM Studio, Together, HuggingFace, Vertex AI, and Google GenAI ([#5609](https://github.com/mem0ai/mem0/pull/5609))
|
||||
|
||||
**Bug Fixes:**
|
||||
- **Core:** Guard against malformed `image_url` entries in `parse_vision_messages` to prevent crashes ([#5631](https://github.com/mem0ai/mem0/pull/5631))
|
||||
@@ -1235,6 +1245,13 @@ See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to-
|
||||
|
||||
<Tab title="TypeScript">
|
||||
|
||||
<Update label="2026-09-23" description="v3.3.0">
|
||||
|
||||
**New Features:**
|
||||
- **Client:** Add User Profiles to `MemoryClient`: `getProfile()`, `generateProfile()`, `getProfileSettings()`, `updateProfileSettings()`, `sampleProfiles()`, and `getProfileJob()`. A profile is a structured, always-current JSON summary of one user, shaped by a JSON Schema you configure per project and filled by an LLM from that user's memories. Generation is asynchronous. Every job POST carries an `Idempotency-Key`; to retry a lost request without starting a second job, pass the same `idempotencyKey` on each attempt ([#7340](https://github.com/mem0ai/mem0/pull/7340))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-18" description="v3.2.0">
|
||||
|
||||
**Improvements:**
|
||||
@@ -2394,6 +2411,17 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Claude Code">
|
||||
|
||||
<Update label="2026-09-23" description="Claude Code plugin v0.3.3">
|
||||
|
||||
**Improvements:**
|
||||
- **Search:** The `search_memories` tool description no longer tells the agent to call it before answering anything that could depend on prior context. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help, which reduces unnecessary searches ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Sidekick:** Sidekick searches memories when earlier sessions could help, instead of before every answer ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Extraction:** Repository memory instructions are shorter. They no longer ask for a dedicated memory for each command that failed and was then fixed, and no longer carry separate rules against saving personal preferences or memories that only name the repository, branch, or directory ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Search skill:** `/search` no longer describes categories as best-effort labels or asks for a retry without the category ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.3`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with these prompts ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-18" description="Claude Code plugin v0.3.2">
|
||||
|
||||
**Improvements:**
|
||||
@@ -2424,6 +2452,16 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Cursor">
|
||||
|
||||
<Update label="2026-09-23" description="Cursor plugin v0.3.3">
|
||||
|
||||
**Improvements:**
|
||||
- **Search:** The `search_memories` tool description no longer tells the agent to call it before answering anything that could depend on prior context. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help, which reduces unnecessary searches ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Extraction:** Repository memory instructions are shorter. They no longer ask for a dedicated memory for each command that failed and was then fixed, and no longer carry separate rules against saving personal preferences or memories that only name the repository, branch, or directory ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Search skill:** `/search` no longer describes categories as best-effort labels or asks for a retry without the category ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.3`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with these prompts ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-18" description="Cursor plugin v0.3.2">
|
||||
|
||||
**Improvements:**
|
||||
@@ -2454,6 +2492,16 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Codex">
|
||||
|
||||
<Update label="2026-09-23" description="Codex plugin v0.3.3">
|
||||
|
||||
**Improvements:**
|
||||
- **Search:** The `search_memories` tool description no longer tells the agent to call it before answering anything that could depend on prior context. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help, which reduces unnecessary searches ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Extraction:** Repository memory instructions are shorter. They no longer ask for a dedicated memory for each command that failed and was then fixed, and no longer carry separate rules against saving personal preferences or memories that only name the repository, branch, or directory ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Search skill:** `/search` no longer describes categories as best-effort labels or asks for a retry without the category ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.3`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with these prompts ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-18" description="Codex plugin v0.3.2">
|
||||
|
||||
**Improvements:**
|
||||
@@ -2483,6 +2531,15 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="OpenCode">
|
||||
|
||||
<Update label="2026-09-23" description="OpenCode plugin v0.4.1">
|
||||
|
||||
**Improvements:**
|
||||
- **Search:** The `search_memories` tool description no longer asks the agent to search proactively or to run several searches for multi-part questions. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help, the same wording as the other coding-agent plugins ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Session context:** Removed two system-context lines that told the agent to run 2 parallel searches before responding and 2-4 parallel searches for non-trivial tasks ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Skills:** `/mem0-search` and `/mem0-context-loader` make one `search_memories` call instead of 2 and 2-4 parallel calls. `/mem0-context-loader` now uses the search skill description from the other coding-agent plugins instead of asking to load at every new task or context switch ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-18" description="OpenCode plugin v0.4.0">
|
||||
|
||||
**Changes:**
|
||||
@@ -2589,6 +2646,16 @@ Initial release of the Mem0 plugin for Claude Code and Cursor, followed by Codex
|
||||
|
||||
<Tab title="Antigravity">
|
||||
|
||||
<Update label="2026-09-23" description="Antigravity plugin v0.3.3">
|
||||
|
||||
**Improvements:**
|
||||
- **Search:** The `search_memories` tool description no longer tells the agent to call it before answering anything that could depend on prior context. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help, which reduces unnecessary searches ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Extraction:** Repository memory instructions are shorter. They no longer ask for a dedicated memory for each command that failed and was then fixed, and no longer carry separate rules against saving personal preferences or memories that only name the repository, branch, or directory ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Search skill:** `/search` no longer describes categories as best-effort labels or asks for a retry without the category ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.3`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with these prompts ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-18" description="Antigravity plugin v0.3.2">
|
||||
|
||||
**Improvements:**
|
||||
@@ -2692,6 +2759,16 @@ Existing memories written by the previous versions are not rewritten. If your me
|
||||
|
||||
<Tab title="Kimi">
|
||||
|
||||
<Update label="2026-09-23" description="Kimi Code plugin v0.3.3">
|
||||
|
||||
**Improvements:**
|
||||
- **Search:** The `search_memories` tool description no longer tells the agent to call it before answering anything that could depend on prior context. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help, which reduces unnecessary searches ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Extraction:** Repository memory instructions are shorter. They no longer ask for a dedicated memory for each command that failed and was then fixed, and no longer carry separate rules against saving personal preferences or memories that only name the repository, branch, or directory ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Search skill:** `/search` no longer describes categories as best-effort labels or asks for a retry without the category ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Packaging:** `PLUGIN_VERSION` bumped to `0.3.3`, so the `mem0-plugin/<version>` wire header and `plugin_version` telemetry field identify builds with these prompts ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-18" description="Kimi Code plugin v0.3.2">
|
||||
|
||||
**Improvements:**
|
||||
@@ -2737,6 +2814,13 @@ Existing memories written by the previous versions are not rewritten. If your me
|
||||
|
||||
<Tab title="OpenClaw">
|
||||
|
||||
<Update label="2026-09-23" description="openclaw-mem0 v1.2.1">
|
||||
|
||||
**Improvements:**
|
||||
- **Search:** The `memory_search` tool description no longer asks the agent to search proactively or to run several searches for multi-part questions. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help. Recall strategies (`smart`, `always`, `manual`) are unchanged ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-18" description="openclaw-mem0 v1.2.0">
|
||||
|
||||
**Changes:**
|
||||
@@ -3033,6 +3117,15 @@ Existing memories written by the previous versions are not rewritten. If your me
|
||||
|
||||
<Tab title="Pi Agent">
|
||||
|
||||
<Update label="2026-09-23" description="Pi Agent plugin v0.3.2">
|
||||
|
||||
**Improvements:**
|
||||
- **Search:** The memory policy, `mem0_memory` tool description, and prompt guidelines no longer ask the agent to search before answering anything that may depend on earlier context or to run several searches per question. They now ask for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Skills:** `context-loader` makes one search instead of 2-4 parallel searches, and uses the search skill description from the other coding-agent plugins ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Automatic recall:** Injected memories are introduced as "Mem0 found these relevant memories from earlier work in this repository:", the same heading as the Python plugins. The old heading called them a shallow first pass and told the agent to search again ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-18" description="Pi Agent plugin v0.3.1">
|
||||
|
||||
**Improvements:**
|
||||
@@ -3153,6 +3246,14 @@ Existing memories written by the previous versions are not rewritten. If your me
|
||||
|
||||
<Tab title="DeepSeek Harness">
|
||||
|
||||
<Update label="2026-09-23" description="deepseek-plugin v0.3.2">
|
||||
|
||||
**Improvements:**
|
||||
- **Search:** The `search_memory` tool description no longer asks the agent to search proactively before answering anything that may depend on earlier context. It now asks for a search before repeating investigation or when earlier decisions, fixes, commands, or results may help ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
- **Automatic recall:** Injected memories are introduced as "Mem0 found these relevant memories from earlier work:". The old heading called them a shallow first pass and told the agent to search again with `mem0_memory`, a tool DeepSeek does not have ([#7420](https://github.com/mem0ai/mem0/pull/7420))
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026-09-18" description="deepseek-plugin v0.3.1">
|
||||
|
||||
**Improvements:**
|
||||
|
||||
@@ -11,7 +11,7 @@ To use Together embedding models, set the `TOGETHER_API_KEY` environment variabl
|
||||
<Note> The `embedding_model_dims` parameter for `vector_store` should be set to `1024` for Together embedder. </Note>
|
||||
|
||||
<Warning>
|
||||
**Breaking default change.** The default Together embedding model is now `intfloat/multilingual-e5-large-instruct` (**1024-dim**), replacing the previous default `togethercomputer/m2-bert-80M-8k-retrieval` (**768-dim**). If you created a self-hosted vector store with the old default, its collection is 768-dim and will reject the new 1024-dim vectors **recreate/reindex the collection at 1024 dimensions** after upgrading. To defer the change, pin the previous values explicitly (`model="togethercomputer/m2-bert-80M-8k-retrieval"`, `embedding_dims=768`) note Together no longer lists this model among its recommended embeddings, so reindexing at 1024 is the durable path.
|
||||
**Breaking default change.** The default Together embedding model is now `intfloat/multilingual-e5-large-instruct` (**1024-dim**), replacing the previous default `togethercomputer/m2-bert-80M-8k-retrieval` (**768-dim**). If you created a self-hosted vector store with the old default, its collection is 768-dim and will reject the new 1024-dim vectors — **recreate/reindex the collection at 1024 dimensions** after upgrading. To defer the change, pin the previous values explicitly (`model="togethercomputer/m2-bert-80M-8k-retrieval"`, `embedding_dims=768`) — note Together no longer lists this model among its recommended embeddings, so reindexing at 1024 is the durable path.
|
||||
</Warning>
|
||||
|
||||
<CodeGroup>
|
||||
|
||||
@@ -95,7 +95,7 @@ Uses the identity from Azure PowerShell (`Connect-AzAccount`).
|
||||
7. **Azure Developer CLI Credential:**
|
||||
Uses the session from Azure Developer CLI (`azd auth login`).
|
||||
|
||||
<Note> If an API is provided, it will be used for authentication over an Azure Identity </Note>
|
||||
<Note> If an API key is provided, it will be used for authentication over an Azure Identity </Note>
|
||||
To enable Role-Based Access Control (RBAC) for Azure AI Search, follow these steps:
|
||||
|
||||
1. In the Azure Portal, navigate to your **Azure AI Search** service.
|
||||
|
||||
@@ -94,6 +94,7 @@ Here are the parameters available for configuring Pinecone:
|
||||
| `hybrid_search` | Whether to enable hybrid search | `False` |
|
||||
| `metric` | Distance metric for vector similarity | `"cosine"` |
|
||||
| `batch_size` | Batch size for operations | `100` |
|
||||
| `extra_params` | Additional keyword arguments passed to the `Pinecone` client constructor. Ignored when `client` is supplied. | `None` |
|
||||
| `namespace` | Namespace for the collection, useful for multi-tenancy. | `None` |
|
||||
</Tab>
|
||||
<Tab title="TypeScript">
|
||||
|
||||
@@ -30,7 +30,7 @@ pip install google-adk mem0ai python-dotenv
|
||||
|
||||
## Code Breakdown
|
||||
|
||||
Let's get started and understand the different components required in building a healthcare assistant powered by memory
|
||||
Let's get started and understand the different components required in building a healthcare assistant powered by memory.
|
||||
|
||||
```python
|
||||
# Import dependencies
|
||||
|
||||
@@ -0,0 +1,388 @@
|
||||
---
|
||||
title: Build a Company Brain with Mem0 Platform and Supabase
|
||||
description: "Build a shared company brain using Mem0 Platform as the managed memory layer, Supabase as your system of record, and the Mem0 MCP server."
|
||||
---
|
||||
|
||||
<Info icon="server">
|
||||
**Uses:** Mem0 **Platform** (`MemoryClient`) · **System of record:** Supabase (Postgres + Auth) · **Access layer:** the hosted Mem0 MCP server. **You'll build:** a company brain your whole org (and every agent) writes to and queries, ending with a new-hire onboarding demo.
|
||||
</Info>
|
||||
|
||||
Companies lose knowledge constantly: why you picked Postgres over Mongo, who owns billing, the deploy rule only one engineer remembers. A **company brain** captures this and answers questions about it, for every employee and every agent, and keeps it after people leave.
|
||||
|
||||
We'll build one on **Mem0 Platform** (the managed memory layer, so there's no vector DB to run) with **Supabase as the system of record** (where your employees, teams, and source documents actually live) and the **Mem0 MCP server** as the wire that lets Claude Code, Cursor, or a Slack bot all reach the same brain.
|
||||
|
||||
<Note>
|
||||
**How Platform and Supabase divide the work.** Mem0 Platform manages storage and extraction server-side, you do **not** point it at your own database. Supabase is your app's source of truth and identity provider; we *ingest* knowledge from Supabase into the brain and use Supabase Auth to decide who's asking. (If you want to self-host the vector store instead, that's the OSS path, see the [Supabase vector store reference](/components/vectordbs/dbs/supabase).)
|
||||
</Note>
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph SB["Supabase: system of record"]
|
||||
K[(knowledge / employees / teams)]
|
||||
AU[Auth · who is asking]
|
||||
end
|
||||
subgraph M0["Mem0 Platform: the brain"]
|
||||
B[(managed memory)]
|
||||
end
|
||||
K -->|ingest| B
|
||||
AU -->|maps to scope| B
|
||||
CC[Claude Code] --> MCP[Mem0 MCP server]
|
||||
CU[Cursor] --> MCP
|
||||
SL[Slack bot] --> MCP
|
||||
MCP --> B
|
||||
```
|
||||
|
||||
Memory splits by entity. An individual is a **`user_id`** (their Supabase Auth id). Shared knowledge lives on an **`agent_id`**: the company-wide brain is `org:acme`, and each team is its own agent, e.g. `team:payments`. A person's own facts route to their `user_id`; company and team facts route to the agent. This split is what lets one search return "my" context alongside the shared org knowledge.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Python 3.9+**
|
||||
- A **Mem0 Platform API key**, [app.mem0.ai/dashboard/api-keys](https://app.mem0.ai/dashboard/api-keys?utm_source=oss&utm_medium=cookbook-company-brain). (Platform runs extraction and embeddings for you, so there's no OpenAI key to manage.)
|
||||
- A **Supabase** project, [supabase.com](https://supabase.com)
|
||||
|
||||
About 20 minutes.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Get your Mem0 Platform API key
|
||||
|
||||
Sign in at [app.mem0.ai](https://app.mem0.ai) and copy a key from **Dashboard → API Keys**. The key is scoped to your org and project; Mem0 resolves both server-side, so you never pass IDs by hand.
|
||||
|
||||
## Step 2: Create the Supabase system of record
|
||||
|
||||
In the Supabase **SQL editor**, create the tables your company already thinks in: people, teams, and a `knowledge` table the brain will ingest from. Identity reuses Supabase Auth's built-in `auth.users`.
|
||||
|
||||
```sql
|
||||
-- Employees extend Supabase Auth's users; identity is auth.users.id (uuid)
|
||||
create table public.employees (
|
||||
id uuid primary key references auth.users (id) on delete cascade,
|
||||
name text not null,
|
||||
team text not null
|
||||
);
|
||||
|
||||
-- The company knowledge the brain ingests. `scope` decides who can recall it.
|
||||
create table public.knowledge (
|
||||
id bigint generated always as identity primary key,
|
||||
scope text not null, -- the shared agent this belongs to: 'org:acme' | 'team:payments'
|
||||
content text not null,
|
||||
author uuid references auth.users (id), -- who recorded it (their user_id); null for org seed data
|
||||
created_at timestamptz default now(),
|
||||
mem0_synced_at timestamptz -- null until ingested into the brain
|
||||
);
|
||||
create index on public.knowledge (mem0_synced_at, created_at);
|
||||
|
||||
-- Seed a little company knowledge to ingest.
|
||||
insert into public.knowledge (scope, content) values
|
||||
('org:acme', 'We chose Postgres over MongoDB for the core product for strong transactional guarantees and relational joins.'),
|
||||
('org:acme', 'All production deploys go out Tuesday and Thursday; never on Fridays.'),
|
||||
('org:acme', 'Customer data must stay in the EU region for GDPR compliance.'),
|
||||
('org:acme', 'Billing is owned by the Payments team, and Alice is the Payments tech lead.'),
|
||||
('team:payments', 'Stripe is our processor; webhooks are verified with PAYMENTS_WEBHOOK_SECRET.');
|
||||
```
|
||||
|
||||
Grab your project URL and **service-role** key from **Settings → API** (the ingestion job runs server-side and needs to read every scope).
|
||||
|
||||
## Step 3: Project setup
|
||||
|
||||
```bash
|
||||
mkdir company-brain && cd company-brain
|
||||
pip install "mem0ai>=2.0.17" supabase requests # 2.0.17+ for agent_custom_instructions
|
||||
```
|
||||
|
||||
```bash
|
||||
export MEM0_API_KEY="m0-..."
|
||||
export SUPABASE_URL="https://<project-ref>.supabase.co"
|
||||
export SUPABASE_SERVICE_KEY="<service-role-key>"
|
||||
```
|
||||
|
||||
## Step 4: Configure the brain
|
||||
|
||||
Create **`brain.py`**. This constructs the Platform client and teaches it what to remember. The key is the **two** instruction sets: `custom_instructions` governs a person's own (`user_id`) memories, and `agent_custom_instructions` governs shared (`agent_id`) memories, phrased in the third person so company facts read "The company…", not "The user's organization…". `custom_categories` files each memory under a useful label.
|
||||
|
||||
```python
|
||||
# brain.py
|
||||
import os
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key=os.environ["MEM0_API_KEY"])
|
||||
|
||||
# Steer extraction (project-wide). Runs server-side; no LLM key needed here.
|
||||
client.project.update(
|
||||
# Governs a person's OWN memories (user_id).
|
||||
custom_instructions=(
|
||||
"Extract the individual's own durable preferences, context, and how they work. "
|
||||
"Ignore greetings and one-off chatter."
|
||||
),
|
||||
# Governs SHARED memories (agent_id); write them in the third person.
|
||||
agent_custom_instructions=(
|
||||
"Extract durable company/team knowledge in the third person "
|
||||
"(\"The company...\", \"The team...\"): decisions and their rationale, ownership "
|
||||
"(who owns what), processes, policies, tooling choices, and gotchas. "
|
||||
"Ignore greetings, scheduling, and one-off chatter."
|
||||
),
|
||||
custom_categories=[
|
||||
{"decision": "Architectural or product decisions and why they were made"},
|
||||
{"ownership": "Who owns a system, service, or process"},
|
||||
{"policy": "Compliance, security, and process rules"},
|
||||
{"tooling": "Tools, services, and how they're configured"},
|
||||
],
|
||||
)
|
||||
|
||||
# Scopes. A person is a user_id; shared brains are agent_ids.
|
||||
COMPANY = "org:acme" # agent_id: company-wide shared brain
|
||||
def team(name): return f"team:{name}" # agent_id: a team's shared brain
|
||||
def person(uid): return uid # user_id: an individual (Supabase auth id)
|
||||
```
|
||||
|
||||
Run it once to apply the project settings:
|
||||
|
||||
```bash
|
||||
python -c "import brain; print('brain configured')"
|
||||
```
|
||||
|
||||
|
||||
## Step 5: Ingest company knowledge from Supabase
|
||||
|
||||
This is where Supabase and the brain connect. Create **`ingest.py`**: read un-synced rows from `knowledge`, add each to the Platform brain under its scope, then mark it synced. Platform `add()` is **asynchronous**, it returns an `event_id` you can poll, so we include a small `wait_for` helper.
|
||||
|
||||
```python
|
||||
# ingest.py
|
||||
import os, time, requests
|
||||
from supabase import create_client
|
||||
from brain import client, person
|
||||
|
||||
sb = create_client(os.environ["SUPABASE_URL"], os.environ["SUPABASE_SERVICE_KEY"])
|
||||
MEM0_HEADERS = {"Authorization": f"Token {os.environ['MEM0_API_KEY']}"}
|
||||
|
||||
def wait_for(event_id, timeout=30):
|
||||
"""Platform extraction is async; poll the event until it settles."""
|
||||
for _ in range(timeout):
|
||||
r = requests.get(f"https://api.mem0.ai/v1/event/{event_id}/", headers=MEM0_HEADERS).json()
|
||||
if r.get("status") in ("SUCCEEDED", "FAILED"):
|
||||
return r["status"]
|
||||
time.sleep(1)
|
||||
return "TIMEOUT"
|
||||
|
||||
# 1. Read knowledge that hasn't been ingested yet
|
||||
rows = sb.table("knowledge").select("*").is_("mem0_synced_at", "null").execute().data
|
||||
|
||||
for row in rows:
|
||||
# 2. Add it. agent_id = the shared scope (org/team); user_id = who recorded it.
|
||||
# Mem0 routes shared facts to the agent and personal facts to the individual,
|
||||
# so pass both when there's an author.
|
||||
add_kwargs = {
|
||||
"agent_id": row["scope"],
|
||||
"metadata": {"source": "supabase", "knowledge_id": row["id"]},
|
||||
}
|
||||
if row["author"]:
|
||||
add_kwargs["user_id"] = person(row["author"])
|
||||
res = client.add([{"role": "user", "content": row["content"]}], **add_kwargs)
|
||||
# 3. Platform returns an event_id; wait for extraction to finish
|
||||
event_id = res.get("event_id") if isinstance(res, dict) else None
|
||||
if event_id:
|
||||
wait_for(event_id)
|
||||
# 4. Mark the row synced so we never double-ingest
|
||||
sb.table("knowledge").update({"mem0_synced_at": "now()"}).eq("id", row["id"]).execute()
|
||||
|
||||
print(f"Ingested {len(rows)} knowledge items into the company brain.")
|
||||
```
|
||||
|
||||
```bash
|
||||
python ingest.py
|
||||
```
|
||||
|
||||
```text
|
||||
Ingested 5 knowledge items into the company brain.
|
||||
```
|
||||
|
||||
Re-running is safe, `mem0_synced_at` gates it, so a nightly cron can keep the brain in step with Supabase.
|
||||
|
||||
## Step 6: Ask the brain
|
||||
|
||||
Create **`ask.py`**. It searches everything relevant to the asker: their own (`user_id`) memories **plus** the shared company and team (`agent_id`) memories. This has to be an **`OR`**, each memory row belongs to exactly one entity, so a flat filter or an `AND` of a `user_id` and an `agent_id` matches nothing.
|
||||
|
||||
```python
|
||||
# ask.py
|
||||
import sys
|
||||
from brain import client, COMPANY, team, person
|
||||
|
||||
def ask(question: str, uid: str | None = None, user_team: str | None = None) -> str:
|
||||
scopes = [{"agent_id": COMPANY}] # company-wide brain
|
||||
if user_team:
|
||||
scopes.append({"agent_id": team(user_team)}) # the asker's team
|
||||
if uid:
|
||||
scopes.append({"user_id": person(uid)}) # the asker's own memories
|
||||
hits = client.search(
|
||||
query=question,
|
||||
filters={"OR": scopes}, # OR, never AND (one FK per memory row)
|
||||
top_k=5,
|
||||
rerank=True,
|
||||
)
|
||||
return "\n".join(f"- {h['memory']}" for h in hits.get("results", hits))
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(ask(" ".join(sys.argv[1:]) or "When can we deploy?"))
|
||||
```
|
||||
|
||||
```bash
|
||||
python ask.py "Why did we pick Postgres, and can I deploy on Friday?"
|
||||
```
|
||||
|
||||
```text
|
||||
- The company chose Postgres over MongoDB for strong transactional guarantees and relational joins
|
||||
- The company's production deploys go out Tuesday and Thursday, never on Fridays
|
||||
```
|
||||
|
||||
Search returns every relevant memory, so a question resolves across separate facts, here it pulls both the owning team and the person:
|
||||
|
||||
```bash
|
||||
python ask.py "Who should I talk to about billing?"
|
||||
```
|
||||
|
||||
```text
|
||||
- Billing is owned by the Payments team
|
||||
- Alice is the Payments tech lead
|
||||
```
|
||||
|
||||
## Step 7: Sharper retrieval
|
||||
|
||||
Platform search is hybrid (semantic + keyword) and filterable. Combine a keyword pass with a category filter to answer precise questions:
|
||||
|
||||
```python
|
||||
client.search(
|
||||
query="webhook signing secret",
|
||||
filters={"agent_id": "team:payments", "categories": {"in": ["tooling"]}},
|
||||
keyword_search=True, # hybrid keyword + semantic
|
||||
rerank=True,
|
||||
threshold=0.3,
|
||||
)
|
||||
```
|
||||
|
||||
Filters use keyword operators (`in`, `gte`, `contains`, …) and AND/OR/NOT, so you can scope by date, category, or metadata, for example the company's policies added this quarter:
|
||||
|
||||
```python
|
||||
client.search(
|
||||
query="compliance rules",
|
||||
filters={"AND": [
|
||||
{"agent_id": "org:acme"},
|
||||
{"categories": {"in": ["policy"]}},
|
||||
{"created_at": {"gte": "2026-01-01"}},
|
||||
]},
|
||||
)
|
||||
```
|
||||
|
||||
## Step 8: Expose the brain to every agent (MCP)
|
||||
|
||||
A brain only your script can reach isn't a company brain. Mem0's **hosted MCP server** lets any agent (Claude Code, Cursor, a Slack bot) query and contribute to the *same* brain. The endpoint is `https://mcp.mem0.ai/mcp`, and the supported way to connect is the `mcp-add` helper, which registers the server and runs Mem0's OAuth login so no key ever lands in a config file.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Claude Code / Cursor">
|
||||
```bash
|
||||
npx mcp-add --url "https://mcp.mem0.ai/mcp" --clients "claude code,cursor"
|
||||
```
|
||||
Complete the browser login on first connect. Now the agent has the brain's memory tools (`add_memory`, `search_memories`, and more) available in-editor.
|
||||
</Tab>
|
||||
<Tab title="Manual (.mcp.json)">
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"mem0": { "url": "https://mcp.mem0.ai/mcp" }
|
||||
}
|
||||
}
|
||||
```
|
||||
Auth happens via Mem0's OAuth flow on first use, don't paste a static token into the file (the hosted gateway may reject a raw `Token` header).
|
||||
</Tab>
|
||||
<Tab title="Slack bot">
|
||||
```python
|
||||
# A Slack bot is just another MCP client. Point its MCP layer at the same URL,
|
||||
# authenticate via Mem0's OAuth flow, and pass the company scope on each call.
|
||||
await mcp.call_tool("search_memories", {
|
||||
"query": user_message,
|
||||
"agent_id": "org:acme",
|
||||
})
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
With this, an engineer asks the brain from their editor and a teammate asks it from Slack, one shared memory behind both.
|
||||
|
||||
## Step 9: Onboard a new hire (the payoff)
|
||||
|
||||
This is what a company brain is *for*. Dana joins, and her identity comes from **Supabase Auth**, which maps straight to her Mem0 `user_id`. She asks the questions every new hire asks and gets real answers on day one, drawn from the shared company (and her team's) brain, plus anything she's told it herself.
|
||||
|
||||
```python
|
||||
# onboarding.py
|
||||
from brain import client, person
|
||||
from ask import ask
|
||||
|
||||
# In a real app these come from sb.auth.get_user(jwt) and the employees table.
|
||||
dana_uid, dana_team = "8f3c...-dana", "payments"
|
||||
|
||||
# Dana also tells the brain how *she* works. This is personal, so it goes to her
|
||||
# user_id, not the shared agent, and stays scoped to her.
|
||||
client.add(
|
||||
[{"role": "user", "content": "I prefer early returns over nested ifs, and I review PRs in the morning."}],
|
||||
user_id=person(dana_uid),
|
||||
)
|
||||
|
||||
for q in [
|
||||
"Who owns billing and who do I talk to?", # company (agent) knowledge
|
||||
"When are deploys, and are there hard rules?",
|
||||
"How do I like to write code?", # Dana's own (user) knowledge
|
||||
]:
|
||||
print(f"Q: {q}\nA: {ask(q, uid=dana_uid, user_team=dana_team)}\n")
|
||||
```
|
||||
|
||||
```text
|
||||
Q: Who owns billing and who do I talk to?
|
||||
A: - Billing is owned by the Payments team; Alice is the Payments tech lead
|
||||
|
||||
Q: When are deploys, and are there hard rules?
|
||||
A: - The company's production deploys go out Tuesday and Thursday, never on Fridays
|
||||
|
||||
Q: How do I like to write code?
|
||||
A: - User prefers early returns over nested ifs
|
||||
```
|
||||
|
||||
The same `ask()` blends the shared company facts with Dana's own preference, because the `OR` filter spans both her `user_id` and the org and team `agent_id`s.
|
||||
|
||||
Dana onboarded herself by asking, drawing on the shared brain the rest of the team had been filling.
|
||||
|
||||
## Production notes
|
||||
|
||||
<Warning>
|
||||
**`user_id` vs `agent_id`.** An individual is a `user_id`; shared brains (company, team) are `agent_id`s. Keeping them separate is what gives you the third-person "The company…" framing and lets a person's own context sit alongside org knowledge. Put a secret like a webhook key on a **team** agent, never the company agent, or everyone can recall it, and mirror the boundary in Supabase with a Row Level Security policy on `knowledge`.
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
**Search must `OR` the scopes.** A memory row belongs to exactly one entity, so `filters={"OR": [{"user_id": ...}, {"agent_id": "org:acme"}, {"agent_id": "team:..."}]}`. A flat filter, or an `AND` of a `user_id` and an `agent_id`, returns nothing.
|
||||
</Warning>
|
||||
|
||||
<Warning>
|
||||
**`add()` is asynchronous.** It returns `{event_id, status: "PENDING"}` and extraction finishes a moment later, poll `GET /v1/event/{event_id}/` (as in Step 5) when you need to know a write has landed before searching for it.
|
||||
</Warning>
|
||||
|
||||
<Note>
|
||||
**Where the entity ID goes differs by call.** `search()` and `get_all()` take the scope inside `filters={...}` (a top-level `user_id=`/`agent_id=` is rejected). `add()` and `delete_all()` are the opposite, they take it as a top-level keyword: `client.delete_all(agent_id="team:payments")`. Deletes are asynchronous too, so a `get_all` right after a `delete_all` can still show rows for a few seconds.
|
||||
</Note>
|
||||
|
||||
## Where to take it next
|
||||
|
||||
- **Auto-feed the brain** from PR descriptions, RFCs, and incident write-ups so it grows without anyone thinking about it, just insert into Supabase `knowledge` and let the cron ingest.
|
||||
- **Scope by real identity** end to end: verify the Supabase JWT, read `sb.auth.get_user(jwt).user.id` for the `user_id`, look up the person's team, and `OR` their `user_id` with the company and team `agent_id`s on every recall.
|
||||
- **Give teams a private view** with Supabase RLS so `team:` knowledge is only readable by that team.
|
||||
|
||||
---
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Mem0 MCP Server" icon="plug" href="/platform/mem0-mcp">
|
||||
Connect any agent or editor to the brain over MCP.
|
||||
</Card>
|
||||
<Card title="Custom Categories & Instructions" icon="sliders" href="/platform/features/custom-instructions">
|
||||
Steer exactly what the brain extracts and how it's filed.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
|
||||
<Snippet file="star-on-github.mdx" />
|
||||
+14
-1
@@ -72,6 +72,7 @@
|
||||
"pages": [
|
||||
"platform/features/v2-memory-filters",
|
||||
"platform/features/entity-scoped-memory",
|
||||
"platform/features/user-profiles",
|
||||
"platform/features/graph-memory",
|
||||
"platform/features/async-client",
|
||||
"platform/features/multimodal-support",
|
||||
@@ -444,7 +445,8 @@
|
||||
"cookbooks/integrations/mastra-agent",
|
||||
"cookbooks/integrations/healthcare-google-adk",
|
||||
"cookbooks/integrations/aws-bedrock",
|
||||
"cookbooks/integrations/tavily-search"
|
||||
"cookbooks/integrations/tavily-search",
|
||||
"cookbooks/integrations/supabase"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -512,6 +514,17 @@
|
||||
"api-reference/entities/delete-user"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Profiles",
|
||||
"icon": "id-card",
|
||||
"pages": [
|
||||
"api-reference/profiles/get-profile",
|
||||
"api-reference/profiles/get-profile-settings",
|
||||
"api-reference/profiles/update-profile-settings",
|
||||
"api-reference/profiles/generate-profiles",
|
||||
"api-reference/profiles/get-profile-job"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Organizations",
|
||||
"icon": "building",
|
||||
|
||||
@@ -45,7 +45,7 @@ memory_from_client = Mem0Memory.from_client(
|
||||
)
|
||||
```
|
||||
|
||||
Context is used to identify the user, agent or the conversation in the Mem0. It is required to be passed in the at least one of the fields in the `Mem0Memory` constructor. It can be any of the following:
|
||||
Context is used to identify the user, agent or the conversation in the Mem0. It is required to be passed in at least one of the fields in the `Mem0Memory` constructor. It can be any of the following:
|
||||
|
||||
```python
|
||||
context = {
|
||||
|
||||
@@ -198,6 +198,7 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
|
||||
### Features - Essential
|
||||
- [V2 Memory Filters](https://docs.mem0.ai/platform/features/v2-memory-filters) [Platform]: Use when compound filters (AND/OR on metadata, entity, time) are needed at search.
|
||||
- [Entity-Scoped Memory](https://docs.mem0.ai/platform/features/entity-scoped-memory) [Platform]: Use when partitioning memories by user, agent, app, or run.
|
||||
- [Profiles](https://docs.mem0.ai/platform/features/user-profiles) [Platform]: Use when a structured always-current summary of a user is needed in one read, instead of searching their memories.
|
||||
- [Graph Memory](https://docs.mem0.ai/platform/features/graph-memory) [Platform]: Use when connecting facts across memories through shared entities for entity-centric or multi-hop questions.
|
||||
- [Async Client](https://docs.mem0.ai/platform/features/async-client) [Platform]: Use when the app issues many concurrent Mem0 calls and needs non-blocking I/O.
|
||||
- [Multimodal Support](https://docs.mem0.ai/platform/features/multimodal-support) [Platform]: Use when storing images or PDFs as memory input.
|
||||
@@ -326,6 +327,7 @@ If the user is on a pre-current major (Python < 2, TS < 3, or a Platform call st
|
||||
- [Healthcare Google ADK](https://docs.mem0.ai/cookbooks/integrations/healthcare-google-adk) [Platform]: Use when the domain is medical and the framework is Google ADK.
|
||||
- [AWS Bedrock](https://docs.mem0.ai/cookbooks/integrations/aws-bedrock) [OSS]: Use when deploying with AWS managed model services.
|
||||
- [Tavily Search](https://docs.mem0.ai/cookbooks/integrations/tavily-search) [Platform]: Use when the agent layers web search on memory.
|
||||
- [Company Brain (Mem0 Platform + Supabase)](https://docs.mem0.ai/cookbooks/integrations/supabase) [Platform]: Use to build a shared org brain on Mem0 Platform with Supabase as system of record and the MCP server as the access layer (with a new-hire onboarding demo).
|
||||
|
||||
### Framework Examples
|
||||
- [LlamaIndex React](https://docs.mem0.ai/cookbooks/frameworks/llamaindex-react) [Both]: Use when building a React UI with LlamaIndex and memory.
|
||||
@@ -363,6 +365,11 @@ All API Reference docs describe Mem0 Platform REST endpoints (requires API key).
|
||||
### Entities
|
||||
- [Get Users](https://docs.mem0.ai/api-reference/entities/get-users) [Platform]: Use when listing users, agents, or apps known to a project.
|
||||
- [Delete User](https://docs.mem0.ai/api-reference/entities/delete-user) [Platform]: Use when removing an entity and all its memories.
|
||||
- [Get Profile](https://docs.mem0.ai/api-reference/profiles/get-profile) [Platform]: Use when reading a user's structured profile and branching on its generation status.
|
||||
- [Get Profile Settings](https://docs.mem0.ai/api-reference/profiles/get-profile-settings) [Platform]: Use when checking the project's profile schema, instructions, or enabled flag.
|
||||
- [Update Profile Settings](https://docs.mem0.ai/api-reference/profiles/update-profile-settings) [Platform]: Use when defining or changing the JSON Schema that shapes profiles for a project.
|
||||
- [Generate Profiles](https://docs.mem0.ai/api-reference/profiles/generate-profiles) [Platform]: Use when building profiles now: a sample of ten, or one entity.
|
||||
- [Get Generation Job](https://docs.mem0.ai/api-reference/profiles/get-profile-job) [Platform]: Use when checking how far a generation has got, and whether it finished.
|
||||
|
||||
### Organizations
|
||||
- [Create Organization](https://docs.mem0.ai/api-reference/organization/create-org) [Platform]: Use when setting up a new org.
|
||||
|
||||
+475
-1
@@ -8070,6 +8070,480 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/entities/{entity_type}/{entity_id}/profile/": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"profiles"
|
||||
],
|
||||
"operationId": "profiles_read",
|
||||
"summary": "Get an entity's profile",
|
||||
"description": "Return the memory profile for one user.\n\nGeneration is asynchronous, so a known entity that has no profile yet is a normal 200 carrying a `status`. A 404 means only that no such entity exists.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "entity_type",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user"
|
||||
]
|
||||
},
|
||||
"description": "The kind of entity that carries the profile."
|
||||
},
|
||||
{
|
||||
"name": "entity_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "The entity's id, as supplied when the memory was added."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The profile envelope.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"profile": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"description": "The generated profile, shaped by the project's schema. Empty unless status is succeeded."
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"succeeded",
|
||||
"pending",
|
||||
"failed",
|
||||
"not_enabled",
|
||||
"insufficient_data"
|
||||
],
|
||||
"description": "Generation state. Branch on this rather than on an empty profile."
|
||||
},
|
||||
"entity_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user"
|
||||
]
|
||||
},
|
||||
"entity_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"nullable": true
|
||||
},
|
||||
"generation_count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Unsupported entity type."
|
||||
},
|
||||
"404": {
|
||||
"description": "No such entity in this project."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/profiles/settings/": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"profiles"
|
||||
],
|
||||
"operationId": "profiles_settings_read",
|
||||
"summary": "Get profile settings",
|
||||
"description": "Return the profile settings for the project the API key is scoped to.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Current settings.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether profile generation runs for this project. Project-wide."
|
||||
},
|
||||
"entities": {
|
||||
"type": "object",
|
||||
"description": "Settings for user profiles, under `user`.",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"nullable": true,
|
||||
"description": "JSON Schema describing the profile. Every property needs a description."
|
||||
},
|
||||
"custom_instructions": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Extra guidance for the extraction step."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jobs": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"estimates": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"samples": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"full_rebuild": {
|
||||
"type": "boolean",
|
||||
"description": "Whether a project-wide rebuild (regenerate/backfill) is available. Currently false."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"profiles"
|
||||
],
|
||||
"operationId": "profiles_settings_update",
|
||||
"summary": "Update profile settings",
|
||||
"description": "Update the project's profile settings. Only the fields present in the body are written, so one setting can change without re-sending the others.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"description": "Only the fields present are written. `schema` and `custom_instructions` nest under `entities.user`; a flat body is rejected.",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether profile generation runs for this project. Project-wide."
|
||||
},
|
||||
"entities": {
|
||||
"type": "object",
|
||||
"description": "Settings for user profiles, under `user`.",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"nullable": true,
|
||||
"description": "JSON Schema describing the profile. Every property needs a description. Send null to clear it."
|
||||
},
|
||||
"custom_instructions": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Extra guidance for the extraction step. Send null to clear it."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Settings as stored after the update.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether profile generation runs for this project. Project-wide."
|
||||
},
|
||||
"entities": {
|
||||
"type": "object",
|
||||
"description": "Settings for user profiles, under `user`.",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"nullable": true,
|
||||
"description": "JSON Schema describing the profile. Every property needs a description."
|
||||
},
|
||||
"custom_instructions": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Extra guidance for the extraction step."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jobs": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"estimates": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"samples": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"full_rebuild": {
|
||||
"type": "boolean",
|
||||
"description": "Whether a project-wide rebuild (regenerate/backfill) is available. Currently false."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The schema is not a valid profile schema."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/profiles/jobs/": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"profiles"
|
||||
],
|
||||
"operationId": "profiles_create_job",
|
||||
"summary": "Generate profiles",
|
||||
"description": "Start one generation. `operation` says what to build:\n\n- `sample` — up to 10 real entities, so a schema can be judged before it is used widely. These are real profiles: they are saved to those entities and count toward usage.\n- `trigger` — one entity, named by `entity_id`.\n\nSend an `Idempotency-Key` header. Replaying the same key returns the same job instead of charging twice. Poll `status_url` from the response until the status is terminal.",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "header",
|
||||
"name": "Idempotency-Key",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"minLength": 8,
|
||||
"maxLength": 128
|
||||
},
|
||||
"description": "Makes a retry safe: the same key returns the same job."
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"operation",
|
||||
"entity_type"
|
||||
],
|
||||
"properties": {
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"sample",
|
||||
"trigger"
|
||||
],
|
||||
"description": "What to generate. Optional only when `entity_id` is set, which means `trigger`."
|
||||
},
|
||||
"entity_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user"
|
||||
]
|
||||
},
|
||||
"entity_id": {
|
||||
"type": "string",
|
||||
"description": "One entity, for `trigger`."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 10,
|
||||
"description": "How many entities to sample."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Job accepted.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"job_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"status_url": {
|
||||
"type": "string",
|
||||
"description": "Poll this. Building the path yourself breaks on a route change."
|
||||
},
|
||||
"operation": {
|
||||
"type": "string"
|
||||
},
|
||||
"entity_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"entity_count_reserved": {
|
||||
"type": "integer",
|
||||
"description": "Entities reserved against usage for this job."
|
||||
},
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"replayed": {
|
||||
"type": "boolean",
|
||||
"description": "True when an Idempotency-Key returned an existing job."
|
||||
},
|
||||
"sampled": {
|
||||
"type": "integer",
|
||||
"description": "`sample` only."
|
||||
},
|
||||
"entity_ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "`sample` only: the entity ids picked. Read each with `GET /v2/entities/user/{entity_id}/profile/`."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Unknown or missing `operation`, or profiles are not configured."
|
||||
},
|
||||
"402": {
|
||||
"description": "Payment required."
|
||||
},
|
||||
"409": {
|
||||
"description": "A job is already running, or the Idempotency-Key was used for a different request. Branch on `error.code`."
|
||||
},
|
||||
"429": {
|
||||
"description": "Cooldown. `retry_after_seconds` sits inside `error`."
|
||||
},
|
||||
"503": {
|
||||
"description": "`jobs_unavailable` — generation is switched off for this project."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/profiles/jobs/{job_id}/": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"profiles"
|
||||
],
|
||||
"operationId": "profiles_get_job",
|
||||
"summary": "Read a generation job",
|
||||
"description": "The job nests under `job`. `total` is null until `enumeration_complete`, and `completed` is `succeeded + failed + skipped`.",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "job_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The job.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"job": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"operation": {
|
||||
"type": "string"
|
||||
},
|
||||
"entity_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"QUEUED",
|
||||
"RUNNING",
|
||||
"SUCCEEDED",
|
||||
"PARTIALLY_SUCCEEDED",
|
||||
"FAILED",
|
||||
"CANCELLED"
|
||||
]
|
||||
},
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"nullable": true
|
||||
},
|
||||
"enumeration_complete": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"completed": {
|
||||
"type": "integer"
|
||||
},
|
||||
"succeeded": {
|
||||
"type": "integer"
|
||||
},
|
||||
"failed": {
|
||||
"type": "integer"
|
||||
},
|
||||
"skipped": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No such job in this project."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -8988,4 +9462,4 @@
|
||||
}
|
||||
},
|
||||
"x-original-swagger-version": "2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
---
|
||||
title: Profiles
|
||||
description: "Build a structured, always-current summary of each user from their memories, shaped by a JSON Schema you define."
|
||||
---
|
||||
|
||||
# Profiles
|
||||
|
||||
Memories are individual facts. A profile is the summary of all of them for one entity: a single structured object, shaped by a JSON Schema you define, that Mem0 keeps current as new memories arrive.
|
||||
|
||||
Search answers "what did this user say about X". A profile answers "who is this user", in one read, with no query to write.
|
||||
|
||||
<Info>
|
||||
**Use profiles when…**
|
||||
- You want to personalize a first response, before the user says anything in this session.
|
||||
- You need a compact object to drop into a prompt instead of a list of memories.
|
||||
- You want the same fields for every user, so your code can rely on their shape.
|
||||
</Info>
|
||||
|
||||
<Note>
|
||||
User Profiles are in **beta** and available on request. To enable them for your
|
||||
organization, contact [support@mem0.ai](mailto:support@mem0.ai).
|
||||
</Note>
|
||||
|
||||
## How it works
|
||||
|
||||
1. You define a **schema**: the fields a profile should contain, each with a description.
|
||||
2. Mem0 builds each entity's profile from their memories, and rebuilds it as new memories arrive.
|
||||
3. You read the profile whenever you need it.
|
||||
|
||||
Generation is **asynchronous**. A profile is not ready the instant an entity's first memory lands, so a read tells you where it is with a `status` rather than failing.
|
||||
|
||||
## Define the schema
|
||||
|
||||
The schema is JSON Schema. Every property needs a `description` — that is what tells the model how to fill the field, so a vague description gives a vague profile.
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
from mem0 import MemoryClient
|
||||
|
||||
client = MemoryClient()
|
||||
|
||||
client.update_profile_settings(
|
||||
enabled=True,
|
||||
schema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"communication_style": {
|
||||
"type": "string",
|
||||
"description": "How the user prefers to be addressed: terse, detailed, formal, casual",
|
||||
},
|
||||
"expertise_areas": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Subjects the user demonstrates working knowledge of",
|
||||
},
|
||||
"current_goals": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "What the user is actively trying to accomplish",
|
||||
},
|
||||
},
|
||||
},
|
||||
custom_instructions="Prefer durable traits over one-off remarks.",
|
||||
)
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
import MemoryClient from "mem0ai";
|
||||
|
||||
const client = new MemoryClient({ apiKey: "your-api-key" });
|
||||
|
||||
await client.updateProfileSettings({
|
||||
enabled: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
communication_style: {
|
||||
type: "string",
|
||||
description:
|
||||
"How the user prefers to be addressed: terse, detailed, formal, casual",
|
||||
},
|
||||
expertise_areas: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Subjects the user demonstrates working knowledge of",
|
||||
},
|
||||
current_goals: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "What the user is actively trying to accomplish",
|
||||
},
|
||||
},
|
||||
},
|
||||
customInstructions: "Prefer durable traits over one-off remarks.",
|
||||
});
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
<Note>
|
||||
Your schema's property names reach the API exactly as you write them. The SDKs do not rewrite them, so a profile always comes back with the field names you chose.
|
||||
</Note>
|
||||
|
||||
Only the fields you pass are written. To turn the feature off without touching your schema, send `enabled` alone.
|
||||
|
||||
## Read a profile
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
result = client.get_profile("alice")
|
||||
|
||||
if result["status"] == "succeeded":
|
||||
print(result["profile"])
|
||||
else:
|
||||
print("not ready:", result["status"])
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
const result = await client.getProfile({ entityId: "alice" });
|
||||
|
||||
if (result.status === "succeeded") {
|
||||
console.log(result.profile);
|
||||
} else {
|
||||
console.log("not ready:", result.status);
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
A response looks like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"profile": {
|
||||
"communication_style": "terse",
|
||||
"expertise_areas": ["distributed systems", "postgres"],
|
||||
"current_goals": ["cut p99 latency", "migrate off the legacy queue"]
|
||||
},
|
||||
"status": "succeeded",
|
||||
"entity_type": "user",
|
||||
"entity_id": "alice",
|
||||
"updated_at": "2026-02-08T10:30:00Z",
|
||||
"generation_count": 3
|
||||
}
|
||||
```
|
||||
|
||||
`generation_count` is how many times this profile has been (re)generated — `0` before the first generation completes.
|
||||
|
||||
### Always branch on `status`
|
||||
|
||||
`profile` is empty unless `status` is `succeeded`. Check the status rather than the emptiness of the object, so a profile that is merely still building is not mistaken for a user you know nothing about.
|
||||
|
||||
| `status` | Meaning | What to do |
|
||||
|---|---|---|
|
||||
| `succeeded` | Profile is built and current | Use it |
|
||||
| `pending` | Generation is queued or running | Read again shortly |
|
||||
| `insufficient_data` | Not enough memories to say anything yet | Fall back to defaults |
|
||||
| `not_enabled` | Profiles are off for this project | Enable them in settings |
|
||||
| `failed` | The last generation did not complete | Retry, or trigger a new one |
|
||||
|
||||
A `404` means only that no such entity exists in your project.
|
||||
|
||||
## Generate a profile on demand
|
||||
|
||||
Profiles are built once an entity has accumulated enough messages, so a brand-new user has none during their first few interactions. Trigger one directly to close that gap:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
client.generate_profile("alice")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
await client.generateProfile({ entityId: "alice" });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
The call returns as soon as the work is queued. Poll the read endpoint and branch on `status`.
|
||||
|
||||
## Test a schema before applying it
|
||||
|
||||
A schema that reads well can still produce disappointing profiles. Sample a few real entities and inspect the output before committing to it.
|
||||
|
||||
Sampling is asynchronous: the call returns a job as soon as it is queued. Poll `status_url` until the job is terminal, then read each sampled entity's profile:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
import time
|
||||
|
||||
job = client.sample_profiles(limit=5)
|
||||
|
||||
# Poll until the sample job reaches a terminal state (job status is UPPERCASE).
|
||||
TERMINAL = {"SUCCEEDED", "PARTIALLY_SUCCEEDED", "FAILED", "CANCELLED"}
|
||||
deadline = time.time() + 120
|
||||
while True:
|
||||
status = client.get_profile_job(job["status_url"])["job"]
|
||||
if status["status"] in TERMINAL:
|
||||
break
|
||||
if time.time() > deadline:
|
||||
raise TimeoutError("Sample job did not finish in time")
|
||||
time.sleep(3)
|
||||
|
||||
print(status["status"], status["succeeded"], "of", status["total"])
|
||||
|
||||
# The create response lists the sampled entities; read each one's saved profile.
|
||||
for entity_id in job.get("entity_ids", []):
|
||||
print(client.get_profile(entity_id))
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
const job = await client.sampleProfiles({ limit: 5 });
|
||||
|
||||
// Poll until the sample job reaches a terminal state (job status is UPPERCASE).
|
||||
const TERMINAL = ["SUCCEEDED", "PARTIALLY_SUCCEEDED", "FAILED", "CANCELLED"];
|
||||
const deadline = Date.now() + 120_000;
|
||||
let status;
|
||||
while (true) {
|
||||
status = (await client.getProfileJob(job.statusUrl)).job;
|
||||
if (TERMINAL.includes(status.status)) break;
|
||||
if (Date.now() > deadline)
|
||||
throw new Error("Sample job did not finish in time");
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
}
|
||||
|
||||
console.log(status.status, status.succeeded, "of", status.total);
|
||||
|
||||
// The create response lists the sampled entities; read each one's saved profile.
|
||||
for (const entityId of job.entityIds ?? []) {
|
||||
console.log(await client.getProfile({ entityId }));
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
These are real generations. The profiles are saved to those entities and count toward your usage, so sampling is not wasted work and not a free dry run. A sample covers up to 10 entities and cannot be repeated immediately.
|
||||
|
||||
## Apply a new schema to existing entities
|
||||
|
||||
A new schema shapes the next generation. Profiles that already exist keep their values until their entity is generated again.
|
||||
|
||||
Each entity picks the new schema up as it sends more memories, and you can generate one now with `generate_profile`.
|
||||
|
||||
<Note>
|
||||
Rebuilding every profile in a project at once is not available yet. Refresh profiles one entity at a time with `generate_profile`, or let each one update on its own as its entity sends more memories.
|
||||
</Note>
|
||||
|
||||
## When profiles update
|
||||
|
||||
You never call an "update profile" endpoint — Mem0 keeps each profile current for you. Two things drive it:
|
||||
|
||||
- **Automatically, as memories accumulate.** Mem0 refreshes an entity's profile after roughly every **10 messages** it receives, folding the new memories into the existing profile. There is no schedule to wait for and no extra call to make: the same `add` you already do keeps the profile moving.
|
||||
- **On demand.** Call `generate_profile` to build or refresh a profile immediately — useful for a brand-new entity that has not yet crossed the automatic threshold.
|
||||
|
||||
Generation is **asynchronous and incremental**. A refresh runs in the background a short while after its trigger, so a read taken immediately after an `add` may still show the previous profile (or `pending`). Branch on `status` rather than assuming the latest memory is already reflected.
|
||||
|
||||
<Note>
|
||||
Updates are **incremental**, not a full rebuild each time — Mem0 merges what it newly learns into the stored profile and keeps the fields your schema still defines. After a schema change, existing profiles pick it up as their entities send more memories, or when you call `generate_profile` — see [Apply a new schema to existing entities](#apply-a-new-schema-to-existing-entities).
|
||||
</Note>
|
||||
|
||||
## Use a profile in a prompt
|
||||
|
||||
The point of the structure is that it drops straight into a prompt:
|
||||
|
||||
```python
|
||||
result = client.get_profile(user_id)
|
||||
|
||||
if result["status"] == "succeeded":
|
||||
profile = result["profile"]
|
||||
system_prompt = f"""You are helping {user_id}.
|
||||
Communication style: {profile.get("communication_style", "unknown")}
|
||||
Areas of expertise: {", ".join(profile.get("expertise_areas", []))}
|
||||
Current goals: {", ".join(profile.get("current_goals", []))}
|
||||
|
||||
Match their style and do not explain what they already know."""
|
||||
else:
|
||||
system_prompt = "You are a helpful assistant."
|
||||
```
|
||||
|
||||
## Writing a schema that works
|
||||
|
||||
- **Describe every field.** The description is the instruction; without it the model guesses.
|
||||
- **Prefer durable traits.** "Prefers dark mode" ages well; "is annoyed today" does not.
|
||||
- **Keep it small.** Ten focused fields beat forty speculative ones, and cost less to generate.
|
||||
- **Say what the field is not.** A description that rules out the near-miss interpretation is worth more than one that only states the obvious.
|
||||
- **Sample before you commit.** It is the only way to see what your descriptions actually produce.
|
||||
|
||||
<Note>
|
||||
A schema has a size budget of roughly **10,000 tokens** of serialized JSON — the whole schema is sent to the model on every generation, so a handful of verbose fields can cost more than many terse ones. Oversized schemas are rejected on save.
|
||||
</Note>
|
||||
|
||||
## Availability
|
||||
|
||||
The feature is in beta and enabled per organization on request — see the note at the top of this page.
|
||||
|
||||
Once it is on, an entity gets a profile when two more things hold:
|
||||
|
||||
- profiles are **enabled** with a schema for the project (see [Define the schema](#define-the-schema)), and
|
||||
- the memory is scoped to an entity — a `user_id`.
|
||||
|
||||
On a project where profiles are turned off, a read returns `status: not_enabled` rather than an error, so you can call it unconditionally and branch on the status.
|
||||
|
||||
## Settings reference
|
||||
|
||||
| Argument | Type | Description |
|
||||
|---|---|---|
|
||||
| `enabled` | boolean | Whether profile generation runs for the project |
|
||||
| `schema` | object | JSON Schema describing the profile. Every property needs a `description` |
|
||||
| `custom_instructions` | string | Extra guidance applied during extraction |
|
||||
|
||||
`enabled` is project-wide. `schema` and `custom_instructions` apply to user
|
||||
profiles, so the stored settings nest them under `entities`:
|
||||
|
||||
```json
|
||||
{
|
||||
"enabled": true,
|
||||
"entities": {
|
||||
"user": {
|
||||
"schema": { "type": "object", "properties": { "...": {} } },
|
||||
"custom_instructions": "Prefer durable traits over one-off remarks."
|
||||
}
|
||||
},
|
||||
"capabilities": { "full_rebuild": false }
|
||||
}
|
||||
```
|
||||
|
||||
That is what a read returns and what a write accepts. The SDKs take the fields
|
||||
flat and nest them for you, so a schema you write with
|
||||
`update_profile_settings` comes back unchanged from `get_profile_settings`.
|
||||
|
||||
<Note>
|
||||
Profile settings are per project. An API key is scoped to one project, so profiles never cross a project boundary.
|
||||
</Note>
|
||||
|
||||
## FAQ
|
||||
|
||||
**Do I need to change my `add` or `search` calls to use profiles?**
|
||||
No. Profiles are built from the memories you already add. You define a schema once and read the profile when you need it — your ingestion and retrieval code is unchanged.
|
||||
|
||||
**Why is `profile` empty even though the entity has memories?**
|
||||
Generation is asynchronous and needs enough to work with. Branch on `status`: `pending` means it is still building, and `insufficient_data` means there are not yet enough memories to fill the schema. Read again shortly, or call `generate_profile` to build one now.
|
||||
|
||||
**Is sampling free?**
|
||||
No. `sample_profiles` runs real generations against real memories and **keeps** the profiles it produces, so it counts toward your usage like any other generation. It exists to check a schema on a few entities before you commit to it — not as a zero-cost dry run.
|
||||
|
||||
**Does changing the schema rewrite existing profiles?**
|
||||
No. A schema change applies to the next generation. An existing profile keeps its values until its entity is generated again, which happens as that entity sends more memories, or when you call `generate_profile` for it.
|
||||
|
||||
**What happens to a field I remove from the schema?**
|
||||
It stops being maintained. On an entity's next generation, fields your schema no longer defines are pruned from the stored profile — so keep a field in the schema for as long as you want its value kept.
|
||||
|
||||
**How current is a profile?**
|
||||
It refreshes automatically as memories accumulate (about every 10 messages for an entity), plus any on-demand `generate_profile` calls. Because refreshes run in the background, expect a short delay after the triggering `add` rather than an instant update.
|
||||
|
||||
## Related
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="Entity-Scoped Memory" icon="users" href="/platform/features/entity-scoped-memory">
|
||||
How users, agents, apps and runs partition memories.
|
||||
</Card>
|
||||
<Card title="Custom Instructions" icon="pen" href="/platform/features/custom-instructions">
|
||||
Steer what Mem0 extracts in the first place.
|
||||
</Card>
|
||||
</CardGroup>
|
||||
+1
-1
@@ -4,7 +4,7 @@ description: "Standard layout for documenting Mem0 API endpoints."
|
||||
icon: "code"
|
||||
---
|
||||
|
||||
# Api Reference Template
|
||||
# API Reference Template
|
||||
|
||||
API reference pages document a single endpoint contract. Present metadata, request/response examples, and recovery guidance without narrative detours.
|
||||
|
||||
|
||||
@@ -45,11 +45,11 @@ grok_client = OpenAI(
|
||||
|
||||
def recommend_movie_with_memory(user_id: str, user_query: str):
|
||||
# Retrieve prior memory about movies
|
||||
past_memories = memory.search("movie preferences", user_id=user_id)
|
||||
past_memories = memory.search("movie preferences", filters={"user_id": user_id})
|
||||
|
||||
prompt = user_query
|
||||
if past_memories:
|
||||
prompt += f"\nPreviously, the user mentioned: {past_memories}"
|
||||
if past_memories["results"]:
|
||||
prompt += f"\nPreviously, the user mentioned: {[m['memory'] for m in past_memories['results']]}"
|
||||
|
||||
# Generate movie recommendation using Grok 3
|
||||
response = grok_client.chat.completions.create(model="grok-3-beta", messages=[{"role": "user", "content": prompt}])
|
||||
|
||||
@@ -198,7 +198,7 @@ def search_memory_tool(query: str, user_id: str = "user") -> str:
|
||||
Relevant vector memories found or message if none found
|
||||
"""
|
||||
try:
|
||||
results = m.search(query, user_id=user_id)
|
||||
results = m.search(query, filters={"user_id": user_id})
|
||||
|
||||
if isinstance(results, dict) and 'results' in results:
|
||||
memory_list = results['results']
|
||||
@@ -245,7 +245,7 @@ def search_graph_memory_tool(query: str, user_id: str = "user") -> str:
|
||||
"""
|
||||
try:
|
||||
graph_query = f"relationships connections {query}"
|
||||
results = m.search(graph_query, user_id=user_id)
|
||||
results = m.search(graph_query, filters={"user_id": user_id})
|
||||
|
||||
if isinstance(results, dict) and 'results' in results:
|
||||
memory_list = results['results']
|
||||
@@ -290,7 +290,7 @@ def get_all_memories_tool(user_id: str = "user") -> str:
|
||||
All memories for the user or message if none found
|
||||
"""
|
||||
try:
|
||||
all_memories = m.get_all(user_id=user_id)
|
||||
all_memories = m.get_all(filters={"user_id": user_id})
|
||||
|
||||
if isinstance(all_memories, dict) and 'results' in all_memories:
|
||||
memory_list = all_memories['results']
|
||||
|
||||
@@ -107,16 +107,16 @@ def main():
|
||||
|
||||
for query in search_queries:
|
||||
print(f"\nQuery: {query}")
|
||||
memories = memory.search(query=query, user_id="user_123")
|
||||
memories = memory.search(query=query, filters={"user_id": "user_123"})
|
||||
|
||||
for memory_item in memories:
|
||||
for memory_item in memories["results"]:
|
||||
print(f" - {memory_item['memory']}")
|
||||
|
||||
print("\n--> Getting all memories for user...")
|
||||
all_memories = memory.get_all(user_id="user_123")
|
||||
print(f"Total memories stored: {len(all_memories)}")
|
||||
all_memories = memory.get_all(filters={"user_id": "user_123"})
|
||||
print(f"Total memories stored: {len(all_memories['results'])}")
|
||||
|
||||
for memory_item in all_memories:
|
||||
for memory_item in all_memories["results"]:
|
||||
print(f" - {memory_item['memory']}")
|
||||
|
||||
print("\n--> vLLM integration demo completed successfully!")
|
||||
|
||||
@@ -0,0 +1,764 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# User Profiles — live demo\n",
|
||||
"\n",
|
||||
"A **profile** is a structured JSON document about ONE user, filled by an LLM from that\n",
|
||||
"user's memories, shaped by a JSON Schema you supply.\n",
|
||||
"\n",
|
||||
"Search answers *\"what did this user say about X\"*. A profile answers *\"who is this\n",
|
||||
"user\"*, in one read, with no query to write — and it is available on the first turn of a\n",
|
||||
"session, before the user has said anything.\n",
|
||||
"\n",
|
||||
"**What this notebook does:** feed a user 12 conversation turns, watch a profile get\n",
|
||||
"generated from them, add 6 more turns that contradict the first set, and watch the\n",
|
||||
"profile rewrite itself. Then it shows every way the API says no.\n",
|
||||
"\n",
|
||||
"**You need:** an API key, and a project on the **Pro plan or higher**. Never commit one.\n",
|
||||
"\n",
|
||||
"> Set `MEM0_API_KEY`, and `MEM0_API_HOST` if you are pointing at a sandbox rather than\n",
|
||||
"> production. The cells below read both from the environment.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"> **Use a disposable project.** This notebook overwrites the project's profile settings\n",
|
||||
"> (enabled, schema, custom instructions). The last cell restores the values saved at the\n",
|
||||
"> start, but only if you reach it: if a cell fails midway, the project keeps the demo\n",
|
||||
"> schema until you run the cleanup cell or reset it yourself. Do not point it at a\n",
|
||||
"> project other people or production traffic depend on.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# This notebook drives the SDK from this worktree, not the published mem0ai:\n",
|
||||
"# the profile fixes below are not released yet.\n",
|
||||
"%pip install -q -e ../..\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": "import json\nimport os\nimport time\nimport uuid\n\nimport mem0\nfrom mem0 import MemoryClient\n\nAPI_KEY = os.environ.get(\"MEM0_API_KEY\")\nif not API_KEY:\n import getpass\n\n API_KEY = getpass.getpass(\"API key: \")\n\nclient = MemoryClient(api_key=API_KEY, host=os.environ.get(\"MEM0_API_HOST\") or None)\n\n# Fresh id each run, so nothing below is stale from a previous pass.\nUSER_ID = f\"demo_{uuid.uuid4().hex[:8]}\"\n\n# Snapshot the project's profile settings up front. This notebook overwrites the\n# shared project schema/instructions/enabled below; the cleanup cell restores this.\nORIGINAL_SETTINGS = client.get_profile_settings()\n\nprint(\"sdk :\", mem0.__file__) # must be this worktree\nprint(\"host :\", client.host)\nprint(\"demo user:\", USER_ID)"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 1. Define the schema\n",
|
||||
"\n",
|
||||
"The schema is handed to the model as a **tool definition**, and each field's\n",
|
||||
"`description` is the only instruction the model gets about what belongs there. An\n",
|
||||
"undescribed field is a field the model guesses at.\n",
|
||||
"\n",
|
||||
"Rules worth knowing:\n",
|
||||
"\n",
|
||||
"- root `type: object` with a **non-empty** `properties` — an empty one is refused, because\n",
|
||||
" it would bill you to extract nothing\n",
|
||||
"- the root keys `_profile_config_version` and `entities` are **reserved** and rejected:\n",
|
||||
" they name the storage envelope, so a schema using them could not be read back\n",
|
||||
" unambiguously\n",
|
||||
"- keep it small. The whole schema is sent to the model on every generation\n",
|
||||
"\n",
|
||||
"Descriptions are **not** enforced on write in this build — a property without one is\n",
|
||||
"accepted and then quietly underfilled at generation time. Section F1 demonstrates it.\n",
|
||||
"Treat descriptions as your job, not the validator's.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"SCHEMA = {\n",
|
||||
" \"type\": \"object\",\n",
|
||||
" \"properties\": {\n",
|
||||
" \"occupation\": {\n",
|
||||
" \"type\": \"string\",\n",
|
||||
" \"description\": \"The person's current job title, in one short phrase.\",\n",
|
||||
" },\n",
|
||||
" \"location\": {\n",
|
||||
" \"type\": \"string\",\n",
|
||||
" \"description\": \"The city or region the person currently lives in.\",\n",
|
||||
" },\n",
|
||||
" \"interests\": {\n",
|
||||
" \"type\": \"array\",\n",
|
||||
" \"items\": {\"type\": \"string\"},\n",
|
||||
" \"description\": \"Hobbies and topics they return to, as short lowercase tags.\",\n",
|
||||
" },\n",
|
||||
" \"dietary_restrictions\": {\n",
|
||||
" \"type\": \"array\",\n",
|
||||
" \"items\": {\"type\": \"string\"},\n",
|
||||
" \"description\": \"Foods the person avoids, and why, if they said.\",\n",
|
||||
" },\n",
|
||||
" \"communication_style\": {\n",
|
||||
" \"type\": \"string\",\n",
|
||||
" \"enum\": [\"concise\", \"detailed\", \"casual\", \"formal\"],\n",
|
||||
" \"description\": \"How this person prefers to be answered.\",\n",
|
||||
" },\n",
|
||||
" \"expertise_level\": {\n",
|
||||
" \"type\": \"string\",\n",
|
||||
" \"enum\": [\"beginner\", \"intermediate\", \"advanced\"],\n",
|
||||
" \"description\": \"Their technical depth, judged from how they discuss their work.\",\n",
|
||||
" },\n",
|
||||
" },\n",
|
||||
"}\n",
|
||||
"\n",
|
||||
"settings = client.update_profile_settings(\n",
|
||||
" enabled=True,\n",
|
||||
" schema=SCHEMA,\n",
|
||||
" custom_instructions=(\n",
|
||||
" \"Prefer facts the person stated outright over anything inferred. \"\n",
|
||||
" \"Leave a field empty rather than guessing.\"\n",
|
||||
" ),\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"# Sorted, because JSONB storage does not preserve the key order you sent.\n",
|
||||
"# Compare a stored schema by SET, never by string or by key order.\n",
|
||||
"stored = settings[\"entities\"][\"user\"][\"schema\"]\n",
|
||||
"print(\"schema fields:\", sorted(stored[\"properties\"]))\n",
|
||||
"print(\"enabled :\", settings[\"enabled\"])\n",
|
||||
"print(\"capabilities :\", settings[\"capabilities\"])\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"`enabled` is project-wide; `schema` and `custom_instructions` apply to user profiles\n",
|
||||
"and are stored under `entities`. The SDK takes them flat and nests them for you, so what\n",
|
||||
"you write comes back unchanged from `get_profile_settings()`.\n",
|
||||
"\n",
|
||||
"Only the arguments you pass are written. To turn the feature off without touching your\n",
|
||||
"schema, send `enabled` alone.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 2. The turns\n",
|
||||
"\n",
|
||||
"Twelve conversation turns for one user. Nothing about `add()` changes — profiles are a\n",
|
||||
"side effect of the normal pipeline.\n",
|
||||
"\n",
|
||||
"Twelve, not five, because generation fires when an entity crosses a **10-message\n",
|
||||
"boundary**. Below that it waits for a flush window measured in hours, and this notebook\n",
|
||||
"would sit there.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"TURNS = [\n",
|
||||
" (\"user\", \"Hey — I just moved to Berlin for a new job.\"),\n",
|
||||
" (\"assistant\", \"Congratulations! What's the new role?\"),\n",
|
||||
" (\"user\", \"Senior data engineer at a logistics company. Mostly Spark and Airflow.\"),\n",
|
||||
" (\"assistant\", \"Nice stack. How are you finding the pipelines there?\"),\n",
|
||||
" (\"user\", \"Honestly the DAGs are a mess. I've been rewriting the partitioning to cut shuffle.\"),\n",
|
||||
" (\"assistant\", \"That usually pays off fast. Anything blocking you?\"),\n",
|
||||
" (\"user\", \"Just time. Keep it short when you answer me, I skim everything.\"),\n",
|
||||
" (\"assistant\", \"Understood — short answers from here.\"),\n",
|
||||
" (\"user\", \"Outside work I climb most weekends, and I'm learning German.\"),\n",
|
||||
" (\"assistant\", \"Bouldering or ropes?\"),\n",
|
||||
" (\"user\", \"Bouldering. Also — I'm vegetarian, so skip meat in any recipe suggestions.\"),\n",
|
||||
" (\"assistant\", \"Noted, vegetarian only.\"),\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"response = client.add(\n",
|
||||
" [{\"role\": r, \"content\": c} for r, c in TURNS],\n",
|
||||
" user_id=USER_ID,\n",
|
||||
")\n",
|
||||
"print(json.dumps(response, indent=2)[:300])\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"The add is **async** — it returns an `event_id` and the memories do not exist yet. Poll\n",
|
||||
"`GET /v1/event/{event_id}/` until it is `SUCCEEDED` or `FAILED`; that, not a sleep, is how\n",
|
||||
"you know the add finished. Then let the extracted memories settle.\n",
|
||||
"\n",
|
||||
"Under load this can take a minute or more, so the cell says plainly whether it ran out of\n",
|
||||
"time rather than printing `0 memories` as though that were the answer.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"event_id = response[\"event_id\"]\n",
|
||||
"deadline = time.time() + 300\n",
|
||||
"\n",
|
||||
"# 1. The add itself. Terminal status, not a sleep.\n",
|
||||
"event_status = None\n",
|
||||
"while time.time() < deadline:\n",
|
||||
" event_status = client.client.get(f\"/v1/event/{event_id}/\").json().get(\"status\")\n",
|
||||
" if event_status in (\"SUCCEEDED\", \"FAILED\"):\n",
|
||||
" break\n",
|
||||
" print(f\" add {event_status}\")\n",
|
||||
" time.sleep(5)\n",
|
||||
"print(f\"add finished: {event_status}\")\n",
|
||||
"# Stop here unless the add SUCCEEDED. A failed or unfinished add would otherwise let the\n",
|
||||
"# generation below bill for a profile built without these memories.\n",
|
||||
"if event_status != \"SUCCEEDED\":\n",
|
||||
" raise RuntimeError(f\"add did not succeed (status={event_status}); not generating a profile\")\n",
|
||||
"\n",
|
||||
"# 2. Extraction lands in batches, so the FIRST non-empty page is not the whole set.\n",
|
||||
"# Wait for the count to stop growing instead of breaking on the first result.\n",
|
||||
"memories, stable = [], 0\n",
|
||||
"while time.time() < deadline:\n",
|
||||
" page = client.get_all(filters={\"user_id\": USER_ID}, page_size=50)\n",
|
||||
" found = page.get(\"results\", []) if isinstance(page, dict) else page\n",
|
||||
" stable = stable + 1 if found and len(found) == len(memories) else 0\n",
|
||||
" memories = found\n",
|
||||
" if stable >= 2: # two identical polls in a row\n",
|
||||
" break\n",
|
||||
" print(f\" ... {len(memories)} so far\")\n",
|
||||
" time.sleep(5)\n",
|
||||
"\n",
|
||||
"if memories:\n",
|
||||
" print(f\"\\n{len(memories)} memories extracted:\\n\")\n",
|
||||
" for m in memories:\n",
|
||||
" print(\" \\u2022\", m.get(\"memory\"))\n",
|
||||
"else:\n",
|
||||
" # Say so. Reporting '0 memories' as a result hides a busy or broken environment\n",
|
||||
" # and makes the profile below look like it came from nothing.\n",
|
||||
" print(\"\\nNO memories yet — extraction is still catching up, or the ingestion\")\n",
|
||||
" print(\"worker is down. Everything below will report insufficient_data.\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 3. Read the profile\n",
|
||||
"\n",
|
||||
"Crossing the 10-message boundary should already have queued a generation. Read first —\n",
|
||||
"and note that a known user with no profile yet is a **200 with a status**, not a 404. That\n",
|
||||
"distinction is the whole point of the envelope.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"envelope = client.get_profile(USER_ID)\n",
|
||||
"print(json.dumps(envelope, indent=2))\n",
|
||||
"\n",
|
||||
"print(\"\\nstatus vocabulary:\")\n",
|
||||
"print(\" succeeded terminal — a generation ran AND the profile has content\")\n",
|
||||
"print(\" pending queued or running\")\n",
|
||||
"print(\" failed terminal — the last generation did not complete\")\n",
|
||||
"print(\" not_enabled feature off, or plan below Pro\")\n",
|
||||
"print(\" insufficient_data no content to show: no row yet, queued, or a\")\n",
|
||||
"print(\" generation that legitimately found nothing\")\n",
|
||||
"print()\n",
|
||||
"print(\"`succeeded` is decided by the profile BODY, not by generation_count: an\")\n",
|
||||
"print(\"empty extraction still increments the counter, so counting generations\")\n",
|
||||
"print(\"reports 'done' for a profile with nothing in it.\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### Force it, rather than waiting\n",
|
||||
"\n",
|
||||
"`generate_profile()` closes the bootstrapping gap: without it a new user has no profile\n",
|
||||
"until their tenth message. One entity, a few seconds.\n",
|
||||
"\n",
|
||||
"Each call sends a new `Idempotency-Key` unless you pass one, and a new key starts a new job.\n",
|
||||
"To retry a dropped request safely, generate the key yourself and pass the same\n",
|
||||
"`idempotency_key` on every attempt: the server then returns the original job instead of\n",
|
||||
"billing a second one.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"TERMINAL = {\"succeeded\", \"failed\", \"not_enabled\"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def wait_for_profile(entity_id, timeout=300, interval=5, since=None):\n",
|
||||
" \"\"\"Poll until terminal.\n",
|
||||
"\n",
|
||||
" `since` waits for a generation_count ABOVE that value, which is how you wait\n",
|
||||
" for an UPDATE rather than accepting the profile you already had.\n",
|
||||
"\n",
|
||||
" `insufficient_data` is NOT terminal by itself — it also covers 'queued', so\n",
|
||||
" poll through it and give up on the timeout instead.\n",
|
||||
" \"\"\"\n",
|
||||
" deadline = time.time() + timeout\n",
|
||||
" body = None\n",
|
||||
" while time.time() < deadline:\n",
|
||||
" body = client.get_profile(entity_id)\n",
|
||||
" status = (body.get(\"status\") or \"\").lower()\n",
|
||||
" count = body.get(\"generation_count\") or 0\n",
|
||||
" fresh = count > since if since is not None else True\n",
|
||||
" if status == \"succeeded\" and fresh:\n",
|
||||
" return body\n",
|
||||
" if status in (\"failed\", \"not_enabled\"):\n",
|
||||
" raise RuntimeError(f\"generation stopped: {status}\")\n",
|
||||
" print(f\" ... {status} (generation_count={count})\")\n",
|
||||
" time.sleep(interval)\n",
|
||||
" raise TimeoutError(f\"not ready in {timeout}s: {body}\")\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(json.dumps(client.generate_profile(USER_ID), indent=2))\n",
|
||||
"print(\"\\npolling...\")\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" body = wait_for_profile(USER_ID)\n",
|
||||
" print(\"\\n=== PROFILE ===\")\n",
|
||||
" print(json.dumps(body[\"profile\"], indent=2))\n",
|
||||
" print(f\"\\nstatus={body['status']} generations={body['generation_count']} updated={body['updated_at']}\")\n",
|
||||
"except TimeoutError as e:\n",
|
||||
" # Say so plainly and let the rest of the notebook skip, rather than raising\n",
|
||||
" # a NameError in every cell below and burying the real cause.\n",
|
||||
" body = None\n",
|
||||
" print(f\"\\nNO PROFILE: {e}\")\n",
|
||||
" print(\"Generation never finished. Usually the ingestion worker is down, or\")\n",
|
||||
" print(\"this project has no memories for the user yet.\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# The model must not invent fields outside your schema — the forced tool call is\n",
|
||||
"# what makes that structural rather than a request.\n",
|
||||
"if body is None:\n",
|
||||
" print(\"skipped — no profile was generated above\")\n",
|
||||
"else:\n",
|
||||
" extra = set(body[\"profile\"]) - set(SCHEMA[\"properties\"])\n",
|
||||
" print(\"fields outside the schema:\", extra or \"none\")\n",
|
||||
"\n",
|
||||
" # A forced JSON-Schema response makes the model emit SOMETHING for every property,\n",
|
||||
" # so 'I found nothing' arrives as a type default: 0, \"\", [].\n",
|
||||
" filled = {k: v for k, v in body[\"profile\"].items() if v not in (None, \"\", [], {}, 0)}\n",
|
||||
" print(f\"genuinely populated: {len(filled)}/{len(SCHEMA['properties'])} -> {list(filled)}\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 4. Now watch it update\n",
|
||||
"\n",
|
||||
"Six more turns that contradict and extend what we already know: a promotion, a move, a\n",
|
||||
"dropped hobby. A profile is a living document, not an append-only log — the model gets the\n",
|
||||
"memories and rewrites the whole thing.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"if body is None:\n",
|
||||
" print(\"skipped — no profile was generated above\")\n",
|
||||
"else:\n",
|
||||
" before = body[\"generation_count\"]\n",
|
||||
"\n",
|
||||
" MORE_TURNS = [\n",
|
||||
" (\"user\", \"Update — I got promoted to staff engineer last week.\"),\n",
|
||||
" (\"assistant\", \"Congratulations. Same team?\"),\n",
|
||||
" (\"user\", \"Same company, but I'm relocating to Munich for it.\"),\n",
|
||||
" (\"assistant\", \"Big move. How do you feel about it?\"),\n",
|
||||
" (\"user\", \"Good. I've stopped climbing though — knee injury. Picked up cycling instead.\"),\n",
|
||||
" (\"assistant\", \"Sorry about the knee. Cycling's kinder on it.\"),\n",
|
||||
" ]\n",
|
||||
"\n",
|
||||
" followup = client.add(\n",
|
||||
" [{\"role\": r, \"content\": c} for r, c in MORE_TURNS],\n",
|
||||
" user_id=USER_ID,\n",
|
||||
" )\n",
|
||||
"\n",
|
||||
" # Wait for the add to land before triggering: a generation queued before the new\n",
|
||||
" # memories exist rewrites the profile from the OLD ones and looks like a no-op.\n",
|
||||
" deadline = time.time() + 300\n",
|
||||
" status = None\n",
|
||||
" while time.time() < deadline:\n",
|
||||
" status = client.client.get(f\"/v1/event/{followup['event_id']}/\").json().get(\"status\")\n",
|
||||
" if status in (\"SUCCEEDED\", \"FAILED\"):\n",
|
||||
" break\n",
|
||||
" time.sleep(5)\n",
|
||||
" print(\"follow-up add:\", status)\n",
|
||||
" # Generating after a FAILED or unfinished add bills for a profile built from the OLD\n",
|
||||
" # memories only, so stop instead.\n",
|
||||
" if status != \"SUCCEEDED\":\n",
|
||||
" raise RuntimeError(f\"follow-up add did not succeed (status={status}); not regenerating\")\n",
|
||||
" time.sleep(15) # let extraction settle\n",
|
||||
"\n",
|
||||
" print(json.dumps(client.generate_profile(USER_ID), indent=2))\n",
|
||||
" print(f\"\\npolling for a NEW generation (count must exceed {before})...\")\n",
|
||||
" updated = wait_for_profile(USER_ID, since=before)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"if body is None:\n",
|
||||
" print(\"skipped — no profile was generated above\")\n",
|
||||
"else:\n",
|
||||
" print(f\"{'field':<22} {'before':<34} after\")\n",
|
||||
" print(\"-\" * 92)\n",
|
||||
" for field in SCHEMA[\"properties\"]:\n",
|
||||
" b = json.dumps(body[\"profile\"].get(field))\n",
|
||||
" a = json.dumps(updated[\"profile\"].get(field))\n",
|
||||
" mark = \" \" if a == b else \"->\"\n",
|
||||
" print(f\"{mark} {field:<20} {b[:32]:<34} {a[:32]}\")\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 5. Use it in a prompt\n",
|
||||
"\n",
|
||||
"The point of the structure is that it drops straight into a prompt — no list of memories\n",
|
||||
"to summarize, no query to write.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def build_system_prompt(entity_id):\n",
|
||||
" result = client.get_profile(entity_id)\n",
|
||||
" if result[\"status\"] != \"succeeded\":\n",
|
||||
" # Branch on status, never on an empty profile: a user whose profile is\n",
|
||||
" # still building is not a user you know nothing about.\n",
|
||||
" return \"You are a helpful assistant.\"\n",
|
||||
"\n",
|
||||
" p = result[\"profile\"]\n",
|
||||
" return f\"\"\"You are helping {entity_id}.\n",
|
||||
"Occupation: {p.get(\"occupation\", \"unknown\")}\n",
|
||||
"Location: {p.get(\"location\", \"unknown\")}\n",
|
||||
"Interests: {\", \".join(p.get(\"interests\", [])) or \"unknown\"}\n",
|
||||
"Dietary restrictions: {\", \".join(p.get(\"dietary_restrictions\", [])) or \"none stated\"}\n",
|
||||
"Preferred style: {p.get(\"communication_style\", \"unknown\")}\n",
|
||||
"\n",
|
||||
"Match their style and do not explain what they already know.\"\"\"\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"print(build_system_prompt(USER_ID))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 6. Judge a schema before committing to it\n",
|
||||
"\n",
|
||||
"`sample_profiles()` runs your schema against up to 10 **real** users that have memories.\n",
|
||||
"\n",
|
||||
"These are real generations and the results are **kept** — a dry run would cost exactly the\n",
|
||||
"same and leave those users no better off. It is not a free preview.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# 202, not 200: the sample generations are queued, not finished.\n",
|
||||
"#\n",
|
||||
"# A 409 `already_running` means a sample from an earlier run is still going.\n",
|
||||
"# That is the cooldown working, not an error — reuse that job rather than\n",
|
||||
"# failing the notebook.\n",
|
||||
"try:\n",
|
||||
" job = client.sample_profiles(limit=3)\n",
|
||||
" print(json.dumps(job, indent=2)[:400])\n",
|
||||
" print(\"\\nsampled\", job.get(\"sampled\"), \"entities:\", job.get(\"entity_ids\"))\n",
|
||||
"except Exception as e:\n",
|
||||
" detail = str(e)\n",
|
||||
" print(\"sample refused:\", detail[:200])\n",
|
||||
" running = json.loads(detail).get(\"error\", {}).get(\"job_id\") if detail.startswith(\"{\") else None\n",
|
||||
" job = {\"job_id\": running, \"status_url\": f\"/v2/profiles/jobs/{running}/\"} if running else None\n",
|
||||
" print(\"reusing the running job:\", running)\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Poll `status_url` to see how the job went. `total` is `null` until enumeration finishes,\n",
|
||||
"so format it defensively rather than assuming a number.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": "JOB_TERMINAL = {\"SUCCEEDED\", \"PARTIALLY_SUCCEEDED\", \"FAILED\", \"CANCELLED\"}\n\n\ndef wait_for_job(job_response, timeout=300, interval=5):\n \"\"\"Poll a generation job. Prefer status_url over a bare job id, so a route\n change needs no client update. Raise on timeout so an unfinished job is never\n mistaken for a finished one.\"\"\"\n handle = job_response.get(\"status_url\") or job_response[\"job_id\"]\n deadline = time.time() + timeout\n status = None\n while time.time() < deadline:\n status = client.get_profile_job(handle)[\"job\"]\n total = status.get(\"total\")\n print(\n f\" {status['status']} \"\n f\"completed={status.get('completed', 0)}/{total if total is not None else '?'} \"\n f\"succeeded={status.get('succeeded', 0)} \"\n f\"failed={status.get('failed', 0)} \"\n f\"skipped={status.get('skipped', 0)}\"\n )\n if str(status.get(\"status\", \"\")).upper() in JOB_TERMINAL:\n return status\n time.sleep(interval)\n raise TimeoutError(\n f\"job not terminal in {timeout}s (last status: {status.get('status') if status else 'none'})\"\n )\n\n\nif job is None:\n print(\"no sample job to poll\")\nelse:\n final = wait_for_job(job)\n\n print(\"\\n--- what the sample produced ---\")\n for entity_id in job.get(\"entity_ids\", []):\n got = client.get_profile(entity_id)\n print(f\"\\n{entity_id} [{got['status']}]\")\n print(\" \", json.dumps(got[\"profile\"])[:220])"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. Apply a new schema to existing users\n",
|
||||
"\n",
|
||||
"A new schema shapes the **next** generation. Profiles that already exist keep their values\n",
|
||||
"until their user is generated again — which happens as that user sends more memories, or\n",
|
||||
"when you call `generate_profile()` for them.\n",
|
||||
"\n",
|
||||
"A field you **remove** stops being maintained: on the next generation, fields your schema\n",
|
||||
"no longer defines are pruned. Keep a field for as long as you want its value kept."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"# Failure scenarios\n",
|
||||
"\n",
|
||||
"Everything above is the path that works. These are the ways it says no, and what each one\n",
|
||||
"means. Run this section last: F3 deliberately leaves the project switched off for a moment.\n",
|
||||
"\n",
|
||||
"> **About the `HTTP error occurred:` lines below.** The SDK logs every 4xx at\n",
|
||||
"> ERROR level before raising, so they appear even for the failures these cells\n",
|
||||
"> deliberately catch. Read the line printed *after* each one — that is the cell's\n",
|
||||
"> own verdict. Nothing here is unhandled.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## F1. Schemas that get rejected\n",
|
||||
"\n",
|
||||
"Rejections happen on **write**, where you can see and fix them — not silently at\n",
|
||||
"generation time, where you would only notice as an empty profile weeks later.\n",
|
||||
"\n",
|
||||
"The last case matters for storage: the user schema lives in one JSONB column alongside\n",
|
||||
"the envelope that separates it, so a schema using the envelope's own reserved keys could\n",
|
||||
"not be read back unambiguously. It is refused rather than stored.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"BAD_SCHEMAS = [\n",
|
||||
" ({\"type\": \"object\", \"properties\": {}}, \"empty — bills you to extract nothing\"),\n",
|
||||
" ({\"type\": \"array\", \"items\": {\"type\": \"string\"}}, \"root must be an object\"),\n",
|
||||
" (\n",
|
||||
" {\n",
|
||||
" \"type\": \"object\",\n",
|
||||
" \"properties\": {\"tone\": {\"type\": \"string\", \"description\": \"Preferred tone.\"}},\n",
|
||||
" # At the schema ROOT, which is where the envelope's own keys live.\n",
|
||||
" \"_profile_config_version\": 1,\n",
|
||||
" \"entities\": {\"user\": {}},\n",
|
||||
" },\n",
|
||||
" \"reserved settings keys at the schema root\",\n",
|
||||
" ),\n",
|
||||
"]\n",
|
||||
"\n",
|
||||
"for bad, why in BAD_SCHEMAS:\n",
|
||||
" try:\n",
|
||||
" client.update_profile_settings(schema=bad)\n",
|
||||
" print(f\"ACCEPTED (unexpected): {why}\")\n",
|
||||
" except Exception as e:\n",
|
||||
" print(f\"rejected [{why}]:\\n {str(e)[:160]}\\n\")\n",
|
||||
"\n",
|
||||
"# NOT rejected: a property with no description. The validator allows it and the\n",
|
||||
"# model then has nothing to go on, so the field comes back empty. Descriptions are\n",
|
||||
"# your job, not the validator's.\n",
|
||||
"try:\n",
|
||||
" client.update_profile_settings(schema={\"type\": \"object\", \"properties\": {\"x\": {\"type\": \"string\"}}})\n",
|
||||
" print(\"accepted [no description on 'x'] <- the trap: valid to store, useless to generate\")\n",
|
||||
"finally:\n",
|
||||
" client.update_profile_settings(schema=SCHEMA) # put the good one back\n",
|
||||
"\n",
|
||||
"restored = client.get_profile_settings()[\"entities\"][\"user\"][\"schema\"]\n",
|
||||
"assert set(restored[\"properties\"]) == set(SCHEMA[\"properties\"])\n",
|
||||
"print(\"\\nschema restored:\", sorted(restored[\"properties\"]))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## F2. A user that does not exist\n",
|
||||
"\n",
|
||||
"404 means only \"no such user\". A known user with no profile yet is a 200 carrying\n",
|
||||
"`insufficient_data`, so an ordinary empty state never looks like an error.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from mem0.exceptions import MemoryNotFoundError\n",
|
||||
"\n",
|
||||
"try:\n",
|
||||
" client.get_profile(\"user_who_never_existed\")\n",
|
||||
" print(\"ACCEPTED (unexpected)\")\n",
|
||||
"except MemoryNotFoundError as e:\n",
|
||||
" print(\"404 as intended:\", str(e)[:120])\n",
|
||||
"\n",
|
||||
"# ...versus a real user who simply has no profile row yet.\n",
|
||||
"fresh = f\"demo_never_profiled_{uuid.uuid4().hex[:6]}\"\n",
|
||||
"client.add([{\"role\": \"user\", \"content\": \"One passing remark.\"}], user_id=fresh)\n",
|
||||
"time.sleep(5)\n",
|
||||
"print(\"known but unprofiled:\", client.get_profile(fresh)[\"status\"])\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## F3. Profiles turned off\n",
|
||||
"\n",
|
||||
"`enabled` is the one project-wide switch. Every generation path then refuses.\n",
|
||||
"\n",
|
||||
"Nothing is deleted. Your schema and every profile you already built are kept, so turning\n",
|
||||
"it back on resumes rather than restarts.\n",
|
||||
"\n",
|
||||
"Note what a read does **not** do — a profile that already exists keeps reporting\n",
|
||||
"`succeeded` and keeps returning its content. `not_enabled` is only what you get for a user\n",
|
||||
"with no profile yet. Turning the feature off stops new work; it does not hide what has\n",
|
||||
"already been built.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"client.update_profile_settings(enabled=False)\n",
|
||||
"\n",
|
||||
"print(\"read (demo user) :\", client.get_profile(USER_ID)[\"status\"])\n",
|
||||
"print(\"read (never profiled) :\", client.get_profile(fresh)[\"status\"])\n",
|
||||
"try:\n",
|
||||
" client.generate_profile(USER_ID)\n",
|
||||
" print(\"trigger: ACCEPTED (unexpected)\")\n",
|
||||
"except Exception as e:\n",
|
||||
" print(\"trigger:\", str(e)[:160])\n",
|
||||
"\n",
|
||||
"back = client.update_profile_settings(enabled=True) # put it back\n",
|
||||
"print(\"\\nrestored:\", back[\"enabled\"])\n",
|
||||
"print(\"schema survived:\", bool(back[\"entities\"][\"user\"][\"schema\"]))\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## 7. Cleanup\n",
|
||||
"\n",
|
||||
"Removes the demo users. The profile row cascades with the entity.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": "# Restore the project's profile settings to the start-of-run snapshot in `finally`, so a\n# failed delete still leaves a shared project as we found it. Passing the original values\n# (including None) clears anything this notebook set: the SDK treats an explicit None as\n# \"clear\" and an omitted argument as \"unchanged\".\ntry:\n # `fresh` only exists if the error-handling section ran.\n for entity_id in (USER_ID, globals().get(\"fresh\")):\n if entity_id is None:\n continue\n r = client.client.delete(f\"/v2/entities/user/{entity_id}/\")\n print(entity_id, \"->\", r.status_code)\nfinally:\n _user = ORIGINAL_SETTINGS.get(\"entities\", {}).get(\"user\", {})\n client.update_profile_settings(\n enabled=ORIGINAL_SETTINGS.get(\"enabled\", False),\n schema=_user.get(\"schema\"),\n custom_instructions=_user.get(\"custom_instructions\"),\n )\n print(\"profile settings restored to the pre-notebook snapshot\")"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"---\n",
|
||||
"\n",
|
||||
"## Cheat sheet\n",
|
||||
"\n",
|
||||
"| Want | Call | Cost |\n",
|
||||
"| --- | --- | --- |\n",
|
||||
"| configure | `update_profile_settings(...)` | free |\n",
|
||||
"| read | `get_profile(user_id)` | free |\n",
|
||||
"| one user now | `generate_profile(user_id)` | 1 LLM call |\n",
|
||||
"| try a schema | `sample_profiles(limit=n)` | ≤10 real generations, kept |\n",
|
||||
"| poll a job | `get_profile_job(status_url)` | free |\n",
|
||||
"\n",
|
||||
"**Settings apply to user profiles.** The stored shape is:\n",
|
||||
"\n",
|
||||
"```json\n",
|
||||
"{\"enabled\": true,\n",
|
||||
" \"entities\": {\"user\": {\"schema\": {...}, \"custom_instructions\": \"...\"}},\n",
|
||||
" \"capabilities\": {\"full_rebuild\": false}}\n",
|
||||
"```\n",
|
||||
"\n",
|
||||
"The SDK takes these flat and nests them for you. Only the fields you pass are written;\n",
|
||||
"`enabled` is the one project-wide switch.\n",
|
||||
"\n",
|
||||
"**Left alone, generation fires** on a 10-message boundary, or after a flush window\n",
|
||||
"measured in hours. `generate_profile()` is how you skip the wait for one user.\n",
|
||||
"\n",
|
||||
"**Three traps:**\n",
|
||||
"\n",
|
||||
"1. `insufficient_data` is not a terminal verdict — it also covers \"queued\", so poll\n",
|
||||
" through it and give up on a timeout instead.\n",
|
||||
"2. `succeeded` is decided by the profile **body**, not `generation_count`. An empty\n",
|
||||
" extraction still increments the counter.\n",
|
||||
"3. A forced JSON-Schema response emits something for every property, so \"nothing found\"\n",
|
||||
" arrives as a type default — `\"\"`, `[]`, `0` — not as a missing key.\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.12.4"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
@@ -8,7 +8,7 @@ This directory is the single source of shared memory behavior for Mem0 coding-ag
|
||||
integrations/
|
||||
├── agent-plugin-core/ # Shared source; never installed as a plugin
|
||||
│ ├── python/ # Claude-derived capture, recall, MCP, scoping, and telemetry
|
||||
│ ├── typescript/ # Shared lifecycle, formatting, identity, scoping, and telemetry
|
||||
│ ├── typescript/ # Shared lifecycle, search prompts, formatting, identity, scoping, and telemetry
|
||||
│ ├── skills/ # The only source for the six generated memory skills
|
||||
│ ├── build/ # Bundle builder, schemas, and validation
|
||||
│ ├── conformance/ # One offline/live verification entry point
|
||||
@@ -45,7 +45,7 @@ New Git repository writes use a hashed remote identity for shared `agent_id`. Se
|
||||
|
||||
Captured prompts and responses preserve their full text after secret redaction. Python extraction splits oversized input across requests without dropping message text. The session-end worker flushes the conversation already collected by hooks without adding the final answer again. Search queries, retrieved context, and tool evidence have separate limits.
|
||||
|
||||
TypeScript hosts reuse redaction and lifecycle utilities but retain their own tools, scopes, and capture events. They do not inherit the Python `repo`/`dir`/`mine` contract or its background batching. OpenCode captures selected user prompts; Pi and DeepSeek capture completed conversation turns; OpenClaw selects recent messages and earlier summaries, then filters noise. Removing message-length truncation does not turn these integrations into complete transcript archives.
|
||||
TypeScript hosts reuse redaction, lifecycle utilities, and the search prompts in `typescript/src/prompts.ts`, which a test keeps identical to the Python core. They retain their own tools, scopes, and capture events. They do not inherit the Python `repo`/`dir`/`mine` contract or its background batching. OpenCode captures selected user prompts; Pi and DeepSeek capture completed conversation turns; OpenClaw selects recent messages and earlier summaries, then filters noise. Removing message-length truncation does not turn these integrations into complete transcript archives.
|
||||
|
||||
For installation, follow the host guides: [Claude Code](../../docs/integrations/claude-code.mdx), [Cursor](../../docs/integrations/cursor.mdx), [Codex](../../docs/integrations/codex.mdx), [Kimi](../../docs/integrations/kimi.mdx), and [Antigravity](../../docs/integrations/antigravity.mdx).
|
||||
|
||||
|
||||
@@ -21,16 +21,9 @@ from memory_core import (
|
||||
PROTOCOL_VERSION = "2024-11-05"
|
||||
TOOL_NAME = "search_memories"
|
||||
TOOL_DESCRIPTION = (
|
||||
"Search memories from earlier work in this repository. ALWAYS call this "
|
||||
"tool before answering anything that could depend on prior context: the "
|
||||
"user's preferences, facts about this codebase, history, people, projects, "
|
||||
"or earlier decisions. Do not rely on the chat window alone. The "
|
||||
"repository's memory is shared by everyone who works in it and includes "
|
||||
"what it took to run, test, or build here, so search before assuming an "
|
||||
"invocation works. The scope argument changes what is searched: 'repo' "
|
||||
"(default) is the whole repository's shared memory plus your own "
|
||||
"preferences, 'dir' narrows the shared part to the directory you are "
|
||||
"working in, and 'mine' is your preferences alone."
|
||||
"Search memories from earlier work in this repository. Use it before "
|
||||
"repeating investigation or when earlier decisions, fixes, commands, or "
|
||||
"results may help."
|
||||
)
|
||||
TOOL_SCHEMA = {
|
||||
"type": "object",
|
||||
|
||||
@@ -29,7 +29,7 @@ from typing import Any, Iterable
|
||||
import telemetry
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.2"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
@@ -71,15 +71,13 @@ MAX_FLUSH_ATTEMPTS = 5
|
||||
FORGET_PAGE_SIZE = 100
|
||||
FORGET_MAX_PAGES = 50
|
||||
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help anyone with future coding work in this repository.
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help with future coding work.
|
||||
|
||||
A completed change should produce one memory explaining the resulting behavior, where it is implemented when useful, and any important constraints or reasoning. Exploration or accepted decisions may produce separate memories only when they are independently useful.
|
||||
|
||||
A command that failed and was then made to work should produce one memory naming the failing invocation, the error it returned, and the invocation that succeeded. Do not save one-off errors caused by an edit still in progress, transient network failures, or anything a rerun would fix on its own.
|
||||
Use the coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Use the current coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Write about the repository, not the user, assistant, session, or task. Do not save personal preferences. Do not save a memory that only states which repository, branch, or directory the session worked in. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
Write about the repository, not the user, assistant, session, or task. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
|
||||
If nothing useful was established, return no memories."""
|
||||
|
||||
|
||||
@@ -12,11 +12,9 @@ Call `search_memories` with the user's question. Treat `--top-k`, `--category`,
|
||||
query.
|
||||
|
||||
Omit `top_k` to use Mem0's configured default. Omit `category` to search every
|
||||
category; a category is a best-effort label Mem0 assigned when it saved the
|
||||
memory, so if a category search misses, repeat it without the category. Omit
|
||||
`scope` to use the configured default, normally `repo`: this repository's
|
||||
shared memory, which everyone who works in it contributes to, plus your own
|
||||
preferences.
|
||||
category. Omit `scope` to use the configured default, normally `repo`: this
|
||||
repository's shared memory, which everyone who works in it contributes to,
|
||||
plus your own preferences.
|
||||
|
||||
Pass `scope` when the question needs something else: `dir` to narrow the
|
||||
shared memory to the directory you are working in (a package inside a
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { MemoryLike } from "./formatting.ts";
|
||||
import { formatMemoryCompact } from "./formatting.ts";
|
||||
import { RECALL_HEADING } from "./prompts.ts";
|
||||
|
||||
const MAX_RECALL_QUERY_CHARS = 6_000;
|
||||
export const DEFAULT_MAX_CONTEXT_CHARS = 4_000;
|
||||
@@ -70,12 +71,14 @@ export function extractConversation(
|
||||
}
|
||||
|
||||
interface RecallOptions {
|
||||
heading?: string;
|
||||
maxChars?: number;
|
||||
seenIds?: Set<string>;
|
||||
timeoutMs?: number;
|
||||
}
|
||||
|
||||
interface MemoryLifecycleOptions {
|
||||
recallHeading?: string;
|
||||
maxContextChars?: number;
|
||||
recallTimeoutMs?: number;
|
||||
}
|
||||
@@ -109,6 +112,7 @@ class MemoryLifecycle {
|
||||
search: (query: string) => Promise<{ results?: unknown[] }>,
|
||||
): Promise<string> {
|
||||
return buildRecallContext(prompt, enabled, search, {
|
||||
heading: this.#options.recallHeading,
|
||||
maxChars: this.#options.maxContextChars,
|
||||
seenIds: this.#seenMemoryIds,
|
||||
timeoutMs: this.#options.recallTimeoutMs,
|
||||
@@ -148,8 +152,7 @@ export async function buildRecallContext(
|
||||
const unseen = memories.filter((memory) => !options.seenIds?.has(memory.id));
|
||||
if (!unseen.length) return "";
|
||||
|
||||
const prefix =
|
||||
"<mem0-relevant-memories>\nRetrieved automatically for the current request. This is a shallow first pass — search mem0_memory for more if you need it.\n";
|
||||
const prefix = `<mem0-relevant-memories>\n${options.heading ?? RECALL_HEADING}\n`;
|
||||
const suffix = "\n</mem0-relevant-memories>";
|
||||
const maxChars = options.maxChars ?? DEFAULT_MAX_CONTEXT_CHARS;
|
||||
const lines: string[] = [];
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const SEARCH_WHEN =
|
||||
"before repeating investigation or when earlier decisions, fixes, commands, or results may help";
|
||||
|
||||
export const SEARCH_TOOL_DESCRIPTION = `Search memories from earlier work in this repository. Use it ${SEARCH_WHEN}.`;
|
||||
export const SEARCH_QUERY_DESCRIPTION = "A direct question about earlier work in this repository.";
|
||||
export const RECALL_HEADING = "Mem0 found these relevant memories from earlier work in this repository:";
|
||||
|
||||
export const USER_SEARCH_TOOL_DESCRIPTION = `Search memories from earlier work. Use it ${SEARCH_WHEN}.`;
|
||||
export const USER_SEARCH_QUERY_DESCRIPTION = "A direct question about earlier work.";
|
||||
export const USER_RECALL_HEADING = "Mem0 found these relevant memories from earlier work:";
|
||||
@@ -0,0 +1,31 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
import { buildRecallContext } from "../src/lifecycle.ts";
|
||||
import {
|
||||
RECALL_HEADING,
|
||||
SEARCH_QUERY_DESCRIPTION,
|
||||
SEARCH_TOOL_DESCRIPTION,
|
||||
USER_RECALL_HEADING,
|
||||
} from "../src/prompts.ts";
|
||||
|
||||
const pythonSource = (name: string) =>
|
||||
readFileSync(new URL(`../../python/${name}`, import.meta.url), "utf8").replace(/"\s*\n\s*"/g, "");
|
||||
|
||||
test("search prompts match the Python core", () => {
|
||||
assert.ok(pythonSource("mcp_server.py").includes(SEARCH_TOOL_DESCRIPTION));
|
||||
assert.ok(pythonSource("mcp_server.py").includes(SEARCH_QUERY_DESCRIPTION));
|
||||
assert.ok(pythonSource("hook_runner.py").includes(RECALL_HEADING));
|
||||
});
|
||||
|
||||
test("recall context uses the repository heading unless the host overrides it", async () => {
|
||||
const search = async () => ({ results: [{ id: "m1", memory: "Use pnpm" }] });
|
||||
|
||||
assert.ok((await buildRecallContext("package manager", true, search)).includes(RECALL_HEADING));
|
||||
assert.ok(
|
||||
(await buildRecallContext("package manager", true, search, { heading: USER_RECALL_HEADING })).includes(
|
||||
USER_RECALL_HEADING,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -21,16 +21,9 @@ from memory_core import (
|
||||
PROTOCOL_VERSION = "2024-11-05"
|
||||
TOOL_NAME = "search_memories"
|
||||
TOOL_DESCRIPTION = (
|
||||
"Search memories from earlier work in this repository. ALWAYS call this "
|
||||
"tool before answering anything that could depend on prior context: the "
|
||||
"user's preferences, facts about this codebase, history, people, projects, "
|
||||
"or earlier decisions. Do not rely on the chat window alone. The "
|
||||
"repository's memory is shared by everyone who works in it and includes "
|
||||
"what it took to run, test, or build here, so search before assuming an "
|
||||
"invocation works. The scope argument changes what is searched: 'repo' "
|
||||
"(default) is the whole repository's shared memory plus your own "
|
||||
"preferences, 'dir' narrows the shared part to the directory you are "
|
||||
"working in, and 'mine' is your preferences alone."
|
||||
"Search memories from earlier work in this repository. Use it before "
|
||||
"repeating investigation or when earlier decisions, fixes, commands, or "
|
||||
"results may help."
|
||||
)
|
||||
TOOL_SCHEMA = {
|
||||
"type": "object",
|
||||
|
||||
@@ -29,7 +29,7 @@ from typing import Any, Iterable
|
||||
import telemetry
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.2"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
@@ -71,15 +71,13 @@ MAX_FLUSH_ATTEMPTS = 5
|
||||
FORGET_PAGE_SIZE = 100
|
||||
FORGET_MAX_PAGES = 50
|
||||
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help anyone with future coding work in this repository.
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help with future coding work.
|
||||
|
||||
A completed change should produce one memory explaining the resulting behavior, where it is implemented when useful, and any important constraints or reasoning. Exploration or accepted decisions may produce separate memories only when they are independently useful.
|
||||
|
||||
A command that failed and was then made to work should produce one memory naming the failing invocation, the error it returned, and the invocation that succeeded. Do not save one-off errors caused by an edit still in progress, transient network failures, or anything a rerun would fix on its own.
|
||||
Use the coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Use the current coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Write about the repository, not the user, assistant, session, or task. Do not save personal preferences. Do not save a memory that only states which repository, branch, or directory the session worked in. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
Write about the repository, not the user, assistant, session, or task. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
|
||||
If nothing useful was established, return no memories."""
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"homepage": "https://docs.mem0.ai/integrations/antigravity",
|
||||
"native": {
|
||||
"pluginRoot": "${ANTIGRAVITY_PLUGIN_ROOT}",
|
||||
|
||||
@@ -12,11 +12,9 @@ Call `search_memories` with the user's question. Treat `--top-k`, `--category`,
|
||||
query.
|
||||
|
||||
Omit `top_k` to use Mem0's configured default. Omit `category` to search every
|
||||
category; a category is a best-effort label Mem0 assigned when it saved the
|
||||
memory, so if a category search misses, repeat it without the category. Omit
|
||||
`scope` to use the configured default, normally `repo`: this repository's
|
||||
shared memory, which everyone who works in it contributes to, plus your own
|
||||
preferences.
|
||||
category. Omit `scope` to use the configured default, normally `repo`: this
|
||||
repository's shared memory, which everyone who works in it contributes to,
|
||||
plus your own preferences.
|
||||
|
||||
Pass `scope` when the question needs something else: `dir` to narrow the
|
||||
shared memory to the directory you are working in (a package inside a
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"author": {
|
||||
"name": "Mem0"
|
||||
|
||||
@@ -12,11 +12,8 @@ You are Mem0's Sonnet coding agent. Complete the work the main agent gives you.
|
||||
Work in the separate Git worktree Claude Code created for you. Return a tested
|
||||
result that the main agent can review without doing the same work again.
|
||||
|
||||
ALWAYS call `search_memories` before answering anything that could depend on
|
||||
prior context (the user's preferences, facts about this codebase, history,
|
||||
people, projects, or earlier decisions). Do not rely on the chat window or
|
||||
assume you know enough from the current conversation. Search with a focused
|
||||
question before investigating the repository.
|
||||
When memories from earlier sessions could help, call `search_memories` with a
|
||||
focused question before searching the repository again.
|
||||
|
||||
Inspect the relevant code and repository rules. Reproduce the problem when that
|
||||
helps. Decide the implementation details, edit files when asked, and test the
|
||||
|
||||
@@ -21,16 +21,9 @@ from memory_core import (
|
||||
PROTOCOL_VERSION = "2024-11-05"
|
||||
TOOL_NAME = "search_memories"
|
||||
TOOL_DESCRIPTION = (
|
||||
"Search memories from earlier work in this repository. ALWAYS call this "
|
||||
"tool before answering anything that could depend on prior context: the "
|
||||
"user's preferences, facts about this codebase, history, people, projects, "
|
||||
"or earlier decisions. Do not rely on the chat window alone. The "
|
||||
"repository's memory is shared by everyone who works in it and includes "
|
||||
"what it took to run, test, or build here, so search before assuming an "
|
||||
"invocation works. The scope argument changes what is searched: 'repo' "
|
||||
"(default) is the whole repository's shared memory plus your own "
|
||||
"preferences, 'dir' narrows the shared part to the directory you are "
|
||||
"working in, and 'mine' is your preferences alone."
|
||||
"Search memories from earlier work in this repository. Use it before "
|
||||
"repeating investigation or when earlier decisions, fixes, commands, or "
|
||||
"results may help."
|
||||
)
|
||||
TOOL_SCHEMA = {
|
||||
"type": "object",
|
||||
|
||||
@@ -29,7 +29,7 @@ from typing import Any, Iterable
|
||||
import telemetry
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.2"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
@@ -71,15 +71,13 @@ MAX_FLUSH_ATTEMPTS = 5
|
||||
FORGET_PAGE_SIZE = 100
|
||||
FORGET_MAX_PAGES = 50
|
||||
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help anyone with future coding work in this repository.
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help with future coding work.
|
||||
|
||||
A completed change should produce one memory explaining the resulting behavior, where it is implemented when useful, and any important constraints or reasoning. Exploration or accepted decisions may produce separate memories only when they are independently useful.
|
||||
|
||||
A command that failed and was then made to work should produce one memory naming the failing invocation, the error it returned, and the invocation that succeeded. Do not save one-off errors caused by an edit still in progress, transient network failures, or anything a rerun would fix on its own.
|
||||
Use the coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Use the current coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Write about the repository, not the user, assistant, session, or task. Do not save personal preferences. Do not save a memory that only states which repository, branch, or directory the session worked in. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
Write about the repository, not the user, assistant, session, or task. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
|
||||
If nothing useful was established, return no memories."""
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"homepage": "https://docs.mem0.ai/integrations/claude-code",
|
||||
"native": {
|
||||
"pluginRoot": "${CLAUDE_PLUGIN_ROOT}",
|
||||
|
||||
@@ -12,11 +12,9 @@ Call `search_memories` with the user's question. Treat `--top-k`, `--category`,
|
||||
query.
|
||||
|
||||
Omit `top_k` to use Mem0's configured default. Omit `category` to search every
|
||||
category; a category is a best-effort label Mem0 assigned when it saved the
|
||||
memory, so if a category search misses, repeat it without the category. Omit
|
||||
`scope` to use the configured default, normally `repo`: this repository's
|
||||
shared memory, which everyone who works in it contributes to, plus your own
|
||||
preferences.
|
||||
category. Omit `scope` to use the configured default, normally `repo`: this
|
||||
repository's shared memory, which everyone who works in it contributes to,
|
||||
plus your own preferences.
|
||||
|
||||
Pass `scope` when the question needs something else: `dir` to narrow the
|
||||
shared memory to the directory you are working in (a package inside a
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -2307,8 +2307,8 @@ def test_sidekick_instructions_reject_unrequested_related_changes():
|
||||
prompt = (PLUGIN_ROOT / "agents" / "sidekick.md").read_text()
|
||||
normalized = " ".join(prompt.split())
|
||||
assert "Skill" in prompt.split("---", 2)[1]
|
||||
assert "ALWAYS call `search_memories` before answering anything" in normalized
|
||||
assert "Do not rely on the chat window" in normalized
|
||||
assert "call `search_memories` with a" in normalized
|
||||
assert "focused question before searching the repository again" in normalized
|
||||
assert "Complete only the work the main agent assigned" in normalized
|
||||
assert "Do not make related improvements" in normalized
|
||||
assert "report them separately" in normalized
|
||||
@@ -4333,7 +4333,7 @@ def test_flush_sends_unified_body_with_both_agent_and_user_id(isolated_env, monk
|
||||
assert sent_body["run_id"] == "s1"
|
||||
assert "lane" not in sent_body["metadata"]
|
||||
assert "Save concise repository facts" in sent_body["agent_custom_instructions"]
|
||||
assert "invocation that succeeded" in sent_body["agent_custom_instructions"]
|
||||
assert "Write about the repository, not the user" in sent_body["agent_custom_instructions"]
|
||||
assert "Do not save repository facts" in sent_body["custom_instructions"]
|
||||
assert sent_body["custom_categories"] == memory_core.CODING_MEMORY_CATEGORIES
|
||||
store.close()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"author": { "name": "Mem0", "email": "support@mem0.ai" },
|
||||
"homepage": "https://docs.mem0.ai/integrations/codex",
|
||||
|
||||
@@ -21,16 +21,9 @@ from memory_core import (
|
||||
PROTOCOL_VERSION = "2024-11-05"
|
||||
TOOL_NAME = "search_memories"
|
||||
TOOL_DESCRIPTION = (
|
||||
"Search memories from earlier work in this repository. ALWAYS call this "
|
||||
"tool before answering anything that could depend on prior context: the "
|
||||
"user's preferences, facts about this codebase, history, people, projects, "
|
||||
"or earlier decisions. Do not rely on the chat window alone. The "
|
||||
"repository's memory is shared by everyone who works in it and includes "
|
||||
"what it took to run, test, or build here, so search before assuming an "
|
||||
"invocation works. The scope argument changes what is searched: 'repo' "
|
||||
"(default) is the whole repository's shared memory plus your own "
|
||||
"preferences, 'dir' narrows the shared part to the directory you are "
|
||||
"working in, and 'mine' is your preferences alone."
|
||||
"Search memories from earlier work in this repository. Use it before "
|
||||
"repeating investigation or when earlier decisions, fixes, commands, or "
|
||||
"results may help."
|
||||
)
|
||||
TOOL_SCHEMA = {
|
||||
"type": "object",
|
||||
|
||||
@@ -29,7 +29,7 @@ from typing import Any, Iterable
|
||||
import telemetry
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.2"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
@@ -71,15 +71,13 @@ MAX_FLUSH_ATTEMPTS = 5
|
||||
FORGET_PAGE_SIZE = 100
|
||||
FORGET_MAX_PAGES = 50
|
||||
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help anyone with future coding work in this repository.
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help with future coding work.
|
||||
|
||||
A completed change should produce one memory explaining the resulting behavior, where it is implemented when useful, and any important constraints or reasoning. Exploration or accepted decisions may produce separate memories only when they are independently useful.
|
||||
|
||||
A command that failed and was then made to work should produce one memory naming the failing invocation, the error it returned, and the invocation that succeeded. Do not save one-off errors caused by an edit still in progress, transient network failures, or anything a rerun would fix on its own.
|
||||
Use the coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Use the current coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Write about the repository, not the user, assistant, session, or task. Do not save personal preferences. Do not save a memory that only states which repository, branch, or directory the session worked in. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
Write about the repository, not the user, assistant, session, or task. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
|
||||
If nothing useful was established, return no memories."""
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"homepage": "https://docs.mem0.ai/integrations/codex",
|
||||
"native": {
|
||||
"pluginRoot": "${PLUGIN_ROOT}",
|
||||
|
||||
@@ -12,11 +12,9 @@ Call `search_memories` with the user's question. Treat `--top-k`, `--category`,
|
||||
query.
|
||||
|
||||
Omit `top_k` to use Mem0's configured default. Omit `category` to search every
|
||||
category; a category is a best-effort label Mem0 assigned when it saved the
|
||||
memory, so if a category search misses, repeat it without the category. Omit
|
||||
`scope` to use the configured default, normally `repo`: this repository's
|
||||
shared memory, which everyone who works in it contributes to, plus your own
|
||||
preferences.
|
||||
category. Omit `scope` to use the configured default, normally `repo`: this
|
||||
repository's shared memory, which everyone who works in it contributes to,
|
||||
plus your own preferences.
|
||||
|
||||
Pass `scope` when the question needs something else: `dir` to narrow the
|
||||
shared memory to the directory you are working in (a package inside a
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"author": { "name": "Mem0", "email": "support@mem0.ai" },
|
||||
"homepage": "https://docs.mem0.ai/integrations/cursor",
|
||||
|
||||
@@ -21,16 +21,9 @@ from memory_core import (
|
||||
PROTOCOL_VERSION = "2024-11-05"
|
||||
TOOL_NAME = "search_memories"
|
||||
TOOL_DESCRIPTION = (
|
||||
"Search memories from earlier work in this repository. ALWAYS call this "
|
||||
"tool before answering anything that could depend on prior context: the "
|
||||
"user's preferences, facts about this codebase, history, people, projects, "
|
||||
"or earlier decisions. Do not rely on the chat window alone. The "
|
||||
"repository's memory is shared by everyone who works in it and includes "
|
||||
"what it took to run, test, or build here, so search before assuming an "
|
||||
"invocation works. The scope argument changes what is searched: 'repo' "
|
||||
"(default) is the whole repository's shared memory plus your own "
|
||||
"preferences, 'dir' narrows the shared part to the directory you are "
|
||||
"working in, and 'mine' is your preferences alone."
|
||||
"Search memories from earlier work in this repository. Use it before "
|
||||
"repeating investigation or when earlier decisions, fixes, commands, or "
|
||||
"results may help."
|
||||
)
|
||||
TOOL_SCHEMA = {
|
||||
"type": "object",
|
||||
|
||||
@@ -29,7 +29,7 @@ from typing import Any, Iterable
|
||||
import telemetry
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.2"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
@@ -71,15 +71,13 @@ MAX_FLUSH_ATTEMPTS = 5
|
||||
FORGET_PAGE_SIZE = 100
|
||||
FORGET_MAX_PAGES = 50
|
||||
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help anyone with future coding work in this repository.
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help with future coding work.
|
||||
|
||||
A completed change should produce one memory explaining the resulting behavior, where it is implemented when useful, and any important constraints or reasoning. Exploration or accepted decisions may produce separate memories only when they are independently useful.
|
||||
|
||||
A command that failed and was then made to work should produce one memory naming the failing invocation, the error it returned, and the invocation that succeeded. Do not save one-off errors caused by an edit still in progress, transient network failures, or anything a rerun would fix on its own.
|
||||
Use the coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Use the current coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Write about the repository, not the user, assistant, session, or task. Do not save personal preferences. Do not save a memory that only states which repository, branch, or directory the session worked in. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
Write about the repository, not the user, assistant, session, or task. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
|
||||
If nothing useful was established, return no memories."""
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"homepage": "https://docs.mem0.ai/integrations/cursor",
|
||||
"native": {
|
||||
"pluginRoot": "${CURSOR_PLUGIN_ROOT}",
|
||||
|
||||
@@ -12,11 +12,9 @@ Call `search_memories` with the user's question. Treat `--top-k`, `--category`,
|
||||
query.
|
||||
|
||||
Omit `top_k` to use Mem0's configured default. Omit `category` to search every
|
||||
category; a category is a best-effort label Mem0 assigned when it saved the
|
||||
memory, so if a category search misses, repeat it without the category. Omit
|
||||
`scope` to use the configured default, normally `repo`: this repository's
|
||||
shared memory, which everyone who works in it contributes to, plus your own
|
||||
preferences.
|
||||
category. Omit `scope` to use the configured default, normally `repo`: this
|
||||
repository's shared memory, which everyone who works in it contributes to,
|
||||
plus your own preferences.
|
||||
|
||||
Pass `scope` when the question needs something else: `dir` to narrow the
|
||||
shared memory to the directory you are working in (a package inside a
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/deepseek-plugin",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"description": "Mem0 long-term memory as a native DeepSeek Harness (Cordis) plugin.",
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -21,6 +21,11 @@ import { truncateOutput } from "./output.ts";
|
||||
import { resolveSearchFilters, resolveAddParams } from "./scoping.ts";
|
||||
import { captureEvent, errorKind } from "./telemetry.ts";
|
||||
import { createMemoryLifecycle } from "../../agent-plugin-core/typescript/src/lifecycle.ts";
|
||||
import {
|
||||
USER_RECALL_HEADING,
|
||||
USER_SEARCH_QUERY_DESCRIPTION,
|
||||
USER_SEARCH_TOOL_DESCRIPTION,
|
||||
} from "../../agent-plugin-core/typescript/src/prompts.ts";
|
||||
|
||||
export const name = "mem0";
|
||||
export const inject = ["tools", "systemPrompt"];
|
||||
@@ -107,7 +112,7 @@ export function apply(ctx: Context, config: Config): void {
|
||||
const stateFor = (session: object): SessionState => {
|
||||
let state = sessionStates.get(session);
|
||||
if (!state) {
|
||||
const lifecycle = createMemoryLifecycle();
|
||||
const lifecycle = createMemoryLifecycle({ recallHeading: USER_RECALL_HEADING });
|
||||
lifecycle.beginSession();
|
||||
state = { lifecycle, messages: [] };
|
||||
sessionStates.set(session, state);
|
||||
@@ -197,10 +202,9 @@ export function apply(ctx: Context, config: Config): void {
|
||||
ctx.tools.register(
|
||||
defineTool({
|
||||
name: "search_memory",
|
||||
description:
|
||||
"Search the user's long-term Mem0 memory for facts relevant to a query. Use proactively before answering anything that may depend on what the user told you earlier.",
|
||||
description: USER_SEARCH_TOOL_DESCRIPTION,
|
||||
parameters: {
|
||||
query: { type: "string", description: "What to recall.", required: true },
|
||||
query: { type: "string", description: USER_SEARCH_QUERY_DESCRIPTION, required: true },
|
||||
limit: {
|
||||
type: "integer",
|
||||
description: `Max results to return (default ${DEFAULT_SEARCH_LIMIT}).`,
|
||||
|
||||
@@ -21,16 +21,9 @@ from memory_core import (
|
||||
PROTOCOL_VERSION = "2024-11-05"
|
||||
TOOL_NAME = "search_memories"
|
||||
TOOL_DESCRIPTION = (
|
||||
"Search memories from earlier work in this repository. ALWAYS call this "
|
||||
"tool before answering anything that could depend on prior context: the "
|
||||
"user's preferences, facts about this codebase, history, people, projects, "
|
||||
"or earlier decisions. Do not rely on the chat window alone. The "
|
||||
"repository's memory is shared by everyone who works in it and includes "
|
||||
"what it took to run, test, or build here, so search before assuming an "
|
||||
"invocation works. The scope argument changes what is searched: 'repo' "
|
||||
"(default) is the whole repository's shared memory plus your own "
|
||||
"preferences, 'dir' narrows the shared part to the directory you are "
|
||||
"working in, and 'mine' is your preferences alone."
|
||||
"Search memories from earlier work in this repository. Use it before "
|
||||
"repeating investigation or when earlier decisions, fixes, commands, or "
|
||||
"results may help."
|
||||
)
|
||||
TOOL_SCHEMA = {
|
||||
"type": "object",
|
||||
|
||||
@@ -29,7 +29,7 @@ from typing import Any, Iterable
|
||||
import telemetry
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.2"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
@@ -71,15 +71,13 @@ MAX_FLUSH_ATTEMPTS = 5
|
||||
FORGET_PAGE_SIZE = 100
|
||||
FORGET_MAX_PAGES = 50
|
||||
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help anyone with future coding work in this repository.
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help with future coding work.
|
||||
|
||||
A completed change should produce one memory explaining the resulting behavior, where it is implemented when useful, and any important constraints or reasoning. Exploration or accepted decisions may produce separate memories only when they are independently useful.
|
||||
|
||||
A command that failed and was then made to work should produce one memory naming the failing invocation, the error it returned, and the invocation that succeeded. Do not save one-off errors caused by an edit still in progress, transient network failures, or anything a rerun would fix on its own.
|
||||
Use the coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Use the current coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Write about the repository, not the user, assistant, session, or task. Do not save personal preferences. Do not save a memory that only states which repository, branch, or directory the session worked in. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
Write about the repository, not the user, assistant, session, or task. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
|
||||
If nothing useful was established, return no memories."""
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"keywords": ["memory", "coding-agents", "continual-learning", "token-efficiency"],
|
||||
"author": { "name": "Mem0", "email": "support@mem0.ai" },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "mem0",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"homepage": "https://docs.mem0.ai/integrations/kimi",
|
||||
"native": {
|
||||
"pluginRoot": "${KIMI_PLUGIN_ROOT}",
|
||||
|
||||
@@ -12,11 +12,9 @@ Call `search_memories` with the user's question. Treat `--top-k`, `--category`,
|
||||
query.
|
||||
|
||||
Omit `top_k` to use Mem0's configured default. Omit `category` to search every
|
||||
category; a category is a best-effort label Mem0 assigned when it saved the
|
||||
memory, so if a category search misses, repeat it without the category. Omit
|
||||
`scope` to use the configured default, normally `repo`: this repository's
|
||||
shared memory, which everyone who works in it contributes to, plus your own
|
||||
preferences.
|
||||
category. Omit `scope` to use the configured default, normally `repo`: this
|
||||
repository's shared memory, which everyone who works in it contributes to,
|
||||
plus your own preferences.
|
||||
|
||||
Pass `scope` when the question needs something else: `dir` to narrow the
|
||||
shared memory to the directory you are working in (a package inside a
|
||||
|
||||
@@ -21,16 +21,9 @@ from memory_core import (
|
||||
PROTOCOL_VERSION = "2024-11-05"
|
||||
TOOL_NAME = "search_memories"
|
||||
TOOL_DESCRIPTION = (
|
||||
"Search memories from earlier work in this repository. ALWAYS call this "
|
||||
"tool before answering anything that could depend on prior context: the "
|
||||
"user's preferences, facts about this codebase, history, people, projects, "
|
||||
"or earlier decisions. Do not rely on the chat window alone. The "
|
||||
"repository's memory is shared by everyone who works in it and includes "
|
||||
"what it took to run, test, or build here, so search before assuming an "
|
||||
"invocation works. The scope argument changes what is searched: 'repo' "
|
||||
"(default) is the whole repository's shared memory plus your own "
|
||||
"preferences, 'dir' narrows the shared part to the directory you are "
|
||||
"working in, and 'mine' is your preferences alone."
|
||||
"Search memories from earlier work in this repository. Use it before "
|
||||
"repeating investigation or when earlier decisions, fixes, commands, or "
|
||||
"results may help."
|
||||
)
|
||||
TOOL_SCHEMA = {
|
||||
"type": "object",
|
||||
|
||||
@@ -29,7 +29,7 @@ from typing import Any, Iterable
|
||||
import telemetry
|
||||
|
||||
DEFAULT_API_URL = "https://api.mem0.ai"
|
||||
PLUGIN_VERSION = "0.3.2"
|
||||
PLUGIN_VERSION = "0.3.3"
|
||||
|
||||
_harness_name: str = "generic"
|
||||
_harness_env_prefix: str = "MEM0_PLUGIN"
|
||||
@@ -71,15 +71,13 @@ MAX_FLUSH_ATTEMPTS = 5
|
||||
FORGET_PAGE_SIZE = 100
|
||||
FORGET_MAX_PAGES = 50
|
||||
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help anyone with future coding work in this repository.
|
||||
PROJECT_MEMORY_INSTRUCTIONS = """Save concise repository facts that will help with future coding work.
|
||||
|
||||
A completed change should produce one memory explaining the resulting behavior, where it is implemented when useful, and any important constraints or reasoning. Exploration or accepted decisions may produce separate memories only when they are independently useful.
|
||||
|
||||
A command that failed and was then made to work should produce one memory naming the failing invocation, the error it returned, and the invocation that succeeded. Do not save one-off errors caused by an edit still in progress, transient network failures, or anything a rerun would fix on its own.
|
||||
Use the coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Use the current coding agent's final response for conclusions about current repository behavior. Do not save proposed or recommended changes unless the user accepted them or the coding agent completed them. Treat subagent responses as supporting repository evidence, not as decisions.
|
||||
|
||||
Write about the repository, not the user, assistant, session, or task. Do not save personal preferences. Do not save a memory that only states which repository, branch, or directory the session worked in. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
Write about the repository, not the user, assistant, session, or task. Do not include test results, documentation updates, release notes, or temporary state.
|
||||
|
||||
If nothing useful was established, return no memories."""
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
||||
"name": "mem0",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"author": {
|
||||
"name": "Mem0",
|
||||
|
||||
@@ -10,11 +10,9 @@ Call `search_memories` with the user's question. Treat `--top-k`, `--category`,
|
||||
query.
|
||||
|
||||
Omit `top_k` to use Mem0's configured default. Omit `category` to search every
|
||||
category; a category is a best-effort label Mem0 assigned when it saved the
|
||||
memory, so if a category search misses, repeat it without the category. Omit
|
||||
`scope` to use the configured default, normally `repo`: this repository's
|
||||
shared memory, which everyone who works in it contributes to, plus your own
|
||||
preferences.
|
||||
category. Omit `scope` to use the configured default, normally `repo`: this
|
||||
repository's shared memory, which everyone who works in it contributes to,
|
||||
plus your own preferences.
|
||||
|
||||
Pass `scope` when the question needs something else: `dir` to narrow the
|
||||
shared memory to the directory you are working in (a package inside a
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "openclaw-mem0",
|
||||
"name": "Memory (Mem0)",
|
||||
"description": "Mem0 memory backend for OpenClaw — platform (mem0.ai cloud) or self-hosted open-source. Auto-recall and auto-capture are opt-in (disabled by default). Supports OpenAI, Anthropic, Ollama (fully local), Qdrant, and PGVector providers.",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"kind": "memory",
|
||||
"skills": ["skills"],
|
||||
"commandAliases": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/openclaw-mem0",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.1",
|
||||
"type": "module",
|
||||
"description": "Mem0 memory backend for OpenClaw — platform or self-hosted open-source",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { USER_SEARCH_QUERY_DESCRIPTION, USER_SEARCH_TOOL_DESCRIPTION } from "../../agent-plugin-core/typescript/src/prompts.ts";
|
||||
import type { MemoryItem, SearchOptions } from "../types.ts";
|
||||
import type { ToolDeps } from "./index.ts";
|
||||
|
||||
@@ -8,9 +9,9 @@ export function createMemorySearchTool(deps: ToolDeps) {
|
||||
return {
|
||||
name: "memory_search",
|
||||
label: "Memory Search",
|
||||
description: "Search long-term memories stored in Mem0 by semantic meaning. Use this proactively before answering when the request may depend on the user's past work, preferences, or decisions -- relevant memories are not always already in context. For multi-part or comparative questions, run several searches with different phrasings and combine the results rather than stopping after one (multi-hop).",
|
||||
description: USER_SEARCH_TOOL_DESCRIPTION,
|
||||
parameters: Type.Object({
|
||||
query: Type.String({ description: "Search query" }),
|
||||
query: Type.String({ description: USER_SEARCH_QUERY_DESCRIPTION }),
|
||||
limit: Type.Optional(Type.Number({ description: `Max results (default: ${cfg.topK})` })),
|
||||
userId: Type.Optional(Type.String({ description: "User ID to scope search" })),
|
||||
agentId: Type.Optional(Type.String({ description: "Agent ID to search a specific agent's memories" })),
|
||||
|
||||
@@ -18,6 +18,7 @@ import {asScope, scopeSearchFilters, scopeWriteParams, resolveDefaultScope, SCOP
|
||||
import {parseProjectFromRemote} from "./project";
|
||||
import {resolveApiKey} from "./api-key";
|
||||
import {createMemoryLifecycle} from "../agent-plugin-core/typescript/src/lifecycle.ts";
|
||||
import {SEARCH_QUERY_DESCRIPTION, SEARCH_TOOL_DESCRIPTION} from "../agent-plugin-core/typescript/src/prompts.ts";
|
||||
|
||||
async function getUserId(): Promise<string> {
|
||||
if (process.env.MEM0_USER_ID) return process.env.MEM0_USER_ID;
|
||||
@@ -424,9 +425,9 @@ Identity context (resolved at plugin startup):
|
||||
}),
|
||||
|
||||
search_memories: tool({
|
||||
description: "Search stored memories by semantic meaning. Use this proactively before answering when the request may depend on the user's past work, preferences, decisions, or environment -- relevant memories are not always auto-injected. For multi-part or comparative questions, run several searches with different phrasings and combine the results rather than stopping after one (multi-hop).",
|
||||
description: SEARCH_TOOL_DESCRIPTION,
|
||||
args: {
|
||||
query: tool.schema.string().describe("Search query"),
|
||||
query: tool.schema.string().describe(SEARCH_QUERY_DESCRIPTION),
|
||||
user_id: tool.schema.string().optional().describe("User ID"),
|
||||
app_id: tool.schema.string().optional().describe("App/Project ID"),
|
||||
agent_id: tool.schema.string().optional().describe("Agent ID"),
|
||||
@@ -636,9 +637,6 @@ Identity context (resolved at plugin startup):
|
||||
}
|
||||
|
||||
if (memoryCount > 0) {
|
||||
systemContext.push(
|
||||
"Search mem0 for recent decisions and task learnings before responding. Run 2 parallel searches: one for decision type, one for task_learning type.",
|
||||
);
|
||||
try {
|
||||
const res = await mem0.search(
|
||||
"recent session state decisions and learnings",
|
||||
@@ -659,9 +657,6 @@ Identity context (resolved at plugin startup):
|
||||
}
|
||||
}
|
||||
|
||||
systemContext.push(
|
||||
"Mem0 searches apply when user references past work, decision questions, errors, or non-trivial tasks. Queries use noun-phrases, 2-4 parallel calls with different metadata.type filters, and include user_id + app_id.",
|
||||
);
|
||||
systemContext.push(SCOPE_GUIDANCE);
|
||||
const activeScope = loadDefaultScope();
|
||||
if (activeScope !== "project") {
|
||||
|
||||
@@ -1,35 +1,19 @@
|
||||
---
|
||||
name: mem0-context-loader
|
||||
description: Searches and injects relevant memories into context before starting work on a task. Use when beginning a new task, switching context, or when project history, past decisions, or coding conventions need to be loaded.
|
||||
description: Search memories from earlier OpenCode sessions in this repository. Use it when earlier work may already explain the code, error, decision, or command you need, so you can avoid repeating file reads, searches, or experiments.
|
||||
---
|
||||
|
||||
# Context Loader
|
||||
|
||||
Pre-fetches relevant memories to prime context before working on a task.
|
||||
|
||||
## When to use
|
||||
|
||||
- Session start (invoke manually or auto-triggered by skill description matching)
|
||||
- User starts work on a specific feature or file set
|
||||
- Complex multi-step task begins
|
||||
- User says "what do we know about X" or "context for X"
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Extract topics** from current message/task. Identify: file paths, module names, feature areas, error patterns.
|
||||
|
||||
2. **Run 2-4 parallel `search_memories` calls** with different angles:
|
||||
2. **Call `search_memories` once** with a focused question about the task: `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`.
|
||||
|
||||
| Query angle | Filter | Purpose |
|
||||
|---|---|---|
|
||||
| Feature/module name | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}` | Architecture decisions |
|
||||
| File paths mentioned | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "convention"}}]}` | Coding patterns |
|
||||
| Error keywords (if any) | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "anti_pattern"}}]}` | Known pitfalls |
|
||||
| Broad project context | `{"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}` | Catch-all |
|
||||
|
||||
3. **Deduplicate** results by memory ID across all search responses.
|
||||
|
||||
4. **Output compact context block** (max 10 memories):
|
||||
3. **Output compact context block** (max 10 memories):
|
||||
|
||||
```
|
||||
context-loader: loaded <N> memories for "<task summary>"
|
||||
@@ -38,7 +22,7 @@ context-loader: loaded <N> memories for "<task summary>"
|
||||
- [anti_pattern] <content> [mem0:<short_id>]
|
||||
```
|
||||
|
||||
5. If **zero results**: output nothing. Don't announce empty context.
|
||||
4. If **zero results**: output nothing. Don't announce empty context.
|
||||
|
||||
## Constraints
|
||||
|
||||
|
||||
@@ -27,14 +27,11 @@ When an ID is detected:
|
||||
|
||||
### Step 2: Search
|
||||
|
||||
Run 2 parallel `search_memories` calls:
|
||||
|
||||
1. Broad: `query=<user's query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`, `rerank=true`
|
||||
2. Targeted: `query=<user's query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}, {"metadata": {"type": "decision"}}]}`, `top_k=5`, `rerank=true`
|
||||
Call `search_memories` once with the user's question: `query=<user's query>`, `filters={"AND": [{"user_id": "<id>"}, {"app_id": "<pid>"}]}`, `top_k=10`.
|
||||
|
||||
### Step 3: Display
|
||||
|
||||
Deduplicate by ID, then show compact results:
|
||||
Show compact results:
|
||||
|
||||
```
|
||||
## mem0 search: "<query>" (<N> results)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/opencode-plugin",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"type": "module",
|
||||
"description": "Mem0 persistent memory plugin for OpenCode — add, search, and manage memories across sessions",
|
||||
"main": "dist/index.js",
|
||||
|
||||
@@ -71,7 +71,7 @@ The plugin includes 6 skills that guide the agent on how to use each capability:
|
||||
|
||||
| Skill | Purpose |
|
||||
|-------|---------|
|
||||
| `context-loader` | Pre-fetch relevant memories at session start |
|
||||
| `context-loader` | Search memories when earlier work may already explain the task |
|
||||
| `remember` | Store facts with category classification |
|
||||
| `search` | Quick semantic search with compact results |
|
||||
| `forget` | Delete memories with confirmation |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@mem0/pi-agent-plugin",
|
||||
"version": "0.3.1",
|
||||
"version": "0.3.2",
|
||||
"type": "module",
|
||||
"description": "Mem0 memory extension for Pi Agent persistent, scoped, semantic memory across sessions and projects",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,34 +1,19 @@
|
||||
---
|
||||
name: context-loader
|
||||
description: Searches and injects relevant memories into context before starting work on a task or topic. Use when beginning a new task, switching context, or when past decisions, preferences, or knowledge need to be loaded.
|
||||
description: Search memories from earlier Pi sessions in this repository. Use it when earlier work may already explain the code, error, decision, or command you need, so you can avoid repeating file reads, searches, or experiments.
|
||||
---
|
||||
|
||||
# Context Loader
|
||||
|
||||
Pre-fetches relevant memories to prime context before working on a task or topic.
|
||||
|
||||
## When to use
|
||||
|
||||
- Session start (auto-triggered by the extension's `before_agent_start` event)
|
||||
- User starts work on a specific topic or area
|
||||
- User says "what do we know about X" or "context for X"
|
||||
|
||||
## Steps
|
||||
|
||||
1. **Extract topics** from current message/task. Identify: subject areas, people mentioned, project names, goal references.
|
||||
|
||||
2. **Run 2-4 parallel searches** using `mem0_memory` tool with `action="search"` and different query angles:
|
||||
2. **Call `mem0_memory` once** with `action="search"` and a focused question about the task.
|
||||
|
||||
| Query angle | Purpose |
|
||||
|---|---|
|
||||
| Topic/subject name | Relevant decisions and preferences |
|
||||
| People mentioned | Relationship context |
|
||||
| Project/goal references | Progress and background |
|
||||
| Broad context | Catch-all for anything relevant |
|
||||
|
||||
3. **Deduplicate** results by memory ID across all search responses.
|
||||
|
||||
4. **Output compact context block** (max 10 memories):
|
||||
3. **Output compact context block** (max 10 memories):
|
||||
|
||||
```
|
||||
context-loader: loaded <N> memories for "<task summary>"
|
||||
@@ -37,7 +22,7 @@ context-loader: loaded <N> memories for "<task summary>"
|
||||
- [lessons] <content> [mem0:<short_id>]
|
||||
```
|
||||
|
||||
5. If **zero results**: output nothing. Don't announce empty context.
|
||||
4. If **zero results**: output nothing. Don't announce empty context.
|
||||
|
||||
## Constraints
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { resolveToolScope } from "../../../agent-plugin-core/typescript/src/scoping.ts";
|
||||
import { SEARCH_QUERY_DESCRIPTION, SEARCH_WHEN } from "../../../agent-plugin-core/typescript/src/prompts.ts";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
import { StringEnum } from "@earendil-works/pi-ai";
|
||||
@@ -144,11 +145,10 @@ export function registerMemoryTool(
|
||||
name: "mem0_memory",
|
||||
label: "Mem0 Memory",
|
||||
description:
|
||||
"Search, add, update, and manage persistent semantic memories powered by Mem0. Memories persist across sessions and devices. Use action \"search\" proactively -- before answering anything that may depend on what the user told you earlier -- and run multiple searches with different phrasings for multi-part questions. Output is truncated to 200 lines / 50KB.",
|
||||
`Search, add, update, and manage persistent semantic memories powered by Mem0. Memories persist across sessions and devices. Use action "search" ${SEARCH_WHEN}. Output is truncated to 200 lines / 50KB.`,
|
||||
promptSnippet: "Semantic memory search and storage via Mem0",
|
||||
promptGuidelines: [
|
||||
'Use mem0_memory with action "search" proactively whenever the request may depend on the user\'s past work, preferences, decisions, or environment -- not only when they explicitly mention the past',
|
||||
'For multi-part or comparative questions, run several searches with different phrasings and combine the results before answering -- one search is rarely enough',
|
||||
`Use mem0_memory with action "search" ${SEARCH_WHEN}`,
|
||||
'Use mem0_memory with action "add" to save important facts, preferences, goals, decisions, or lessons the user shares',
|
||||
'Use mem0_memory with action "update" to modify an existing memory — requires memory_id and content. Preserves the memory ID',
|
||||
"Always use the default project scope unless the user EXPLICITLY asks to search across all projects — only after the user selects /mem0-scope global use scope \"global\"",
|
||||
@@ -166,13 +166,13 @@ export function registerMemoryTool(
|
||||
] as const,
|
||||
{
|
||||
description:
|
||||
"Memory operation to run: \"search\" (semantic recall -- use proactively before answering; run several with different phrasings for multi-part questions), \"add\" (save a new fact/preference/decision), \"get_all\" (list everything in scope, no query needed), \"update\" (replace an existing memory's text by id), \"delete\" (remove one memory by id), \"delete_all\" (wipe every memory in the scope -- destructive, only on explicit request).",
|
||||
"Memory operation to run: \"search\" (semantic recall of earlier work in this repository), \"add\" (save a new fact/preference/decision), \"get_all\" (list everything in scope, no query needed), \"update\" (replace an existing memory's text by id), \"delete\" (remove one memory by id), \"delete_all\" (wipe every memory in the scope -- destructive, only on explicit request).",
|
||||
},
|
||||
),
|
||||
query: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
"Search text -- required for action \"search\". Use a focused noun-phrase; for multi-part questions run several searches with different phrasings.",
|
||||
`Search text -- required for action "search". ${SEARCH_QUERY_DESCRIPTION}`,
|
||||
}),
|
||||
),
|
||||
content: Type.Optional(
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
export const MEMORY_POLICY = `<mem0-memory-policy>
|
||||
You have persistent semantic memory via the mem0_memory tool, powered by Mem0. Relevant memories may be auto-injected under <mem0-relevant-memories>, but that retrieval is shallow — treat it as a starting point, not the full picture.
|
||||
import { SEARCH_WHEN } from "../../agent-plugin-core/typescript/src/prompts.ts";
|
||||
|
||||
Be proactive about retrieval:
|
||||
- Search memory BEFORE answering whenever the request could depend on the user's past work, preferences, decisions, environment, or anything they told you earlier — don't wait to be asked.
|
||||
- Check memory before asking the user something they may have already told you.
|
||||
- For multi-part, comparative, or "how did we…" questions, run SEVERAL searches with different phrasings and combine the results. One search is rarely enough — keep going until you have what you need (multi-hop).
|
||||
export const MEMORY_POLICY = `<mem0-memory-policy>
|
||||
You have persistent semantic memory via the mem0_memory tool, powered by Mem0. Relevant memories may be auto-injected under <mem0-relevant-memories>.
|
||||
|
||||
Use mem0_memory with action "search" ${SEARCH_WHEN}.
|
||||
|
||||
Be proactive about saving:
|
||||
- Save important facts, preferences, goals, decisions, lessons learned, identity, relationships, and routines the user shares.
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"category": "Productivity",
|
||||
"description": "Cross-session memory and token savings for coding agents.",
|
||||
"version": "0.3.2"
|
||||
"version": "0.3.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mem0ai",
|
||||
"version": "3.2.0",
|
||||
"version": "3.3.0",
|
||||
"description": "The Memory Layer For Your AI Apps",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
|
||||
@@ -23,6 +23,14 @@ export type {
|
||||
FeedbackPayload,
|
||||
CreateMemoryExportPayload,
|
||||
GetMemoryExportPayload,
|
||||
ProfileEntityType,
|
||||
ProfileStatus,
|
||||
ProfileResponse,
|
||||
ProfileJobResponse,
|
||||
ProfileJobStatus,
|
||||
ProfileSettings,
|
||||
ProfileSettingsResponse,
|
||||
EntityProfileSettings,
|
||||
} from "./mem0.types";
|
||||
|
||||
// Re-export enums as values (not type-only)
|
||||
|
||||
+203
-3
@@ -1,4 +1,5 @@
|
||||
import axios from "axios";
|
||||
import { v7 as uuidv7 } from "uuid";
|
||||
import {
|
||||
AllUsers,
|
||||
PaginatedMemories,
|
||||
@@ -20,6 +21,12 @@ import {
|
||||
FeedbackPayload,
|
||||
CreateMemoryExportPayload,
|
||||
GetMemoryExportPayload,
|
||||
ProfileEntityType,
|
||||
ProfileResponse,
|
||||
ProfileJobResponse,
|
||||
ProfileJobStatus,
|
||||
ProfileSettings,
|
||||
ProfileSettingsResponse,
|
||||
} from "./mem0.types";
|
||||
import {
|
||||
captureClientEvent,
|
||||
@@ -92,6 +99,9 @@ interface ClientIdentity {
|
||||
}
|
||||
|
||||
// Shares one ping per (host, api key) across clients; FIFO-capped.
|
||||
// One collection for every generation; the operation is a body field.
|
||||
const PROFILE_JOBS_PATH = "/v2/profiles/jobs/";
|
||||
|
||||
const IDENTITY_CACHE_MAX_DEFAULT = 50;
|
||||
const identityByCredentials = new Map<string, Promise<ClientIdentity>>();
|
||||
|
||||
@@ -305,7 +315,8 @@ export default class MemoryClient {
|
||||
});
|
||||
}
|
||||
|
||||
async _fetchWithErrorHandling(url: string, options: any): Promise<any> {
|
||||
/** Fetch with no key conversion, for payloads carrying user-controlled property names. */
|
||||
async _fetchRawJson(url: string, options: any): Promise<any> {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
@@ -318,8 +329,11 @@ export default class MemoryClient {
|
||||
const errorData = await response.text();
|
||||
throw createExceptionFromResponse(response.status, errorData);
|
||||
}
|
||||
const jsonResponse = await response.json();
|
||||
return snakeToCamelKeys(jsonResponse);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async _fetchWithErrorHandling(url: string, options: any): Promise<any> {
|
||||
return snakeToCamelKeys(await this._fetchRawJson(url, options));
|
||||
}
|
||||
|
||||
_preparePayload(
|
||||
@@ -816,6 +830,192 @@ export default class MemoryClient {
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the memory profile for a single user.
|
||||
*
|
||||
* Branch on `status`, not on an empty `profile`: generation is asynchronous,
|
||||
* so a known user without a profile yet is a normal response.
|
||||
*/
|
||||
async getProfile(data: { entityId: string }): Promise<ProfileResponse> {
|
||||
this._captureEvent("get_profile", []);
|
||||
await this._awaitIdentity();
|
||||
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}/v2/entities/user/${encodeURIComponent(data.entityId)}/profile/`,
|
||||
{
|
||||
headers: this.headers,
|
||||
},
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate or refresh the profile for one user, now.
|
||||
*
|
||||
* Profiles are otherwise built once a user crosses an internal message
|
||||
* threshold, so a new user has none for its first few memories. Returns as
|
||||
* soon as the work is queued: poll {@link getProfile} and branch on `status`.
|
||||
*
|
||||
* Pass `idempotencyKey` and reuse it to retry a lost request without starting
|
||||
* (and being billed for) a second job.
|
||||
*/
|
||||
async generateProfile(data: {
|
||||
entityId: string;
|
||||
idempotencyKey?: string;
|
||||
}): Promise<ProfileJobResponse> {
|
||||
this._captureEvent("generate_profile", []);
|
||||
await this._awaitIdentity();
|
||||
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}${PROFILE_JOBS_PATH}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
...this.headers,
|
||||
"Idempotency-Key": data.idempotencyKey ?? uuidv7(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
operation: "trigger",
|
||||
entity_type: "user",
|
||||
entity_id: data.entityId,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/** Get the profile settings for the current project. */
|
||||
async getProfileSettings(): Promise<ProfileSettingsResponse> {
|
||||
this._captureEvent("get_profile_settings", []);
|
||||
await this._awaitIdentity();
|
||||
|
||||
const raw = await this._fetchRawJson(`${this.host}/v2/profiles/settings/`, {
|
||||
headers: this.headers,
|
||||
});
|
||||
return this._settingsWithVerbatimSchema(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* The envelope keys are ours; the schema's property names are the customer's.
|
||||
*
|
||||
* Each entity type carries its own schema, so every one has to be restored
|
||||
* from the raw body — otherwise camel-casing rewrites the customer's field
|
||||
* names and a profile comes back under keys they never chose.
|
||||
*/
|
||||
private _settingsWithVerbatimSchema(raw: any): ProfileSettingsResponse {
|
||||
const settings = snakeToCamelKeys(raw) as ProfileSettingsResponse;
|
||||
if (!raw || typeof raw !== "object") {
|
||||
return settings;
|
||||
}
|
||||
|
||||
const rawEntities = raw.entities;
|
||||
if (rawEntities && typeof rawEntities === "object") {
|
||||
for (const [entityType, entitySettings] of Object.entries(rawEntities)) {
|
||||
if (
|
||||
entitySettings &&
|
||||
typeof entitySettings === "object" &&
|
||||
"schema" in entitySettings &&
|
||||
settings.entities?.[entityType as ProfileEntityType]
|
||||
) {
|
||||
settings.entities[entityType as ProfileEntityType]!.schema = (
|
||||
entitySettings as Record<string, any>
|
||||
).schema;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update profile settings. Only the fields you pass are written.
|
||||
*
|
||||
* `schema` and `customInstructions` are per user and are nested under
|
||||
* `entities` for the API; only `enabled` is project-wide. Sending them flat
|
||||
* is rejected with `Unsupported settings`.
|
||||
*/
|
||||
async updateProfileSettings(
|
||||
settings: ProfileSettings,
|
||||
): Promise<ProfileSettingsResponse> {
|
||||
const payloadKeys = Object.keys(settings || {});
|
||||
this._captureEvent("update_profile_settings", [payloadKeys]);
|
||||
await this._awaitIdentity();
|
||||
|
||||
const { schema, customInstructions, enabled } = settings || {};
|
||||
|
||||
const body: Record<string, any> = {};
|
||||
if (enabled !== undefined) {
|
||||
body.enabled = enabled;
|
||||
}
|
||||
|
||||
const entitySettings: Record<string, any> = {};
|
||||
// The schema's property names are the customer's and must reach the API verbatim.
|
||||
if (schema !== undefined) {
|
||||
entitySettings.schema = schema;
|
||||
}
|
||||
if (customInstructions !== undefined) {
|
||||
entitySettings.custom_instructions = customInstructions;
|
||||
}
|
||||
if (Object.keys(entitySettings).length > 0) {
|
||||
body.entities = { user: entitySettings };
|
||||
}
|
||||
|
||||
const raw = await this._fetchRawJson(`${this.host}/v2/profiles/settings/`, {
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
return this._settingsWithVerbatimSchema(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate profiles for a few real users, to check a schema.
|
||||
*
|
||||
* Real generations against real memories, and the results are kept: the
|
||||
* profiles are written to those users and count toward usage.
|
||||
*/
|
||||
async sampleProfiles(data?: {
|
||||
limit?: number;
|
||||
idempotencyKey?: string;
|
||||
}): Promise<ProfileJobResponse> {
|
||||
this._captureEvent("sample_profiles", []);
|
||||
await this._awaitIdentity();
|
||||
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}${PROFILE_JOBS_PATH}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
...this.headers,
|
||||
"Idempotency-Key": data?.idempotencyKey ?? uuidv7(),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
operation: "sample",
|
||||
// Required: the API refuses a job that does not name an entity kind.
|
||||
entity_type: "user",
|
||||
...this._prepareParams({ limit: data?.limit }),
|
||||
}),
|
||||
},
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read one generation job. Accepts the `statusUrl` from a create call, or a
|
||||
* bare job id. Prefer `statusUrl` so a route change needs no client update.
|
||||
*/
|
||||
async getProfileJob(jobIdOrStatusUrl: string): Promise<ProfileJobStatus> {
|
||||
await this._awaitIdentity();
|
||||
|
||||
const path = jobIdOrStatusUrl.startsWith("/")
|
||||
? jobIdOrStatusUrl
|
||||
: `${PROFILE_JOBS_PATH}${jobIdOrStatusUrl}/`;
|
||||
return this._fetchWithErrorHandling(`${this.host}${path}`, {
|
||||
method: "GET",
|
||||
headers: this.headers,
|
||||
});
|
||||
}
|
||||
|
||||
async createMemoryExport(
|
||||
data: CreateMemoryExportPayload,
|
||||
): Promise<{ message: string; id: string }> {
|
||||
|
||||
@@ -239,3 +239,98 @@ export interface GetMemoryExportPayload {
|
||||
filters?: Record<string, any>;
|
||||
memoryExportId?: string;
|
||||
}
|
||||
|
||||
// ─── Profile Types ──────────────────────────────────────────
|
||||
|
||||
/** The entity kind that carries a profile. */
|
||||
export type ProfileEntityType = "user";
|
||||
|
||||
/**
|
||||
* `succeeded` is the only state in which `profile` is guaranteed to hold content.
|
||||
*
|
||||
* These are values, not keys, so the client does not camel-case them: the wire
|
||||
* spelling is what a comparison has to match.
|
||||
*/
|
||||
export type ProfileStatus =
|
||||
| "succeeded"
|
||||
| "pending"
|
||||
| "failed"
|
||||
| "not_enabled"
|
||||
| "insufficient_data";
|
||||
|
||||
export interface ProfileResponse {
|
||||
/** Shaped by the project's schema; keys are not camel-cased. */
|
||||
profile: Record<string, any>;
|
||||
status: ProfileStatus;
|
||||
entityType: ProfileEntityType;
|
||||
entityId: string;
|
||||
updatedAt: string | null;
|
||||
generationCount: number;
|
||||
/** The last generation's failure reason, when `status` is `failed`. */
|
||||
error?: string | null;
|
||||
}
|
||||
|
||||
/** Every accepted generation. `statusUrl` is the server's own poll path. */
|
||||
export interface ProfileJobResponse {
|
||||
jobId: string;
|
||||
status: string;
|
||||
statusUrl: string;
|
||||
operation: string;
|
||||
entityType: ProfileEntityType;
|
||||
/** Entities reserved against usage for this job. */
|
||||
entityCountReserved?: number;
|
||||
/** Null when the job has no associated event. */
|
||||
eventId?: string | null;
|
||||
replayed?: boolean;
|
||||
/** Sample runs only: how many entities were picked. */
|
||||
sampled?: number;
|
||||
/** Sample runs only: the entity ids picked. Read each one with `getProfile`. */
|
||||
entityIds?: string[];
|
||||
}
|
||||
|
||||
/** The settings to write. `schema` and `customInstructions` apply to user profiles. */
|
||||
export interface ProfileSettings {
|
||||
/** Turn profile generation on or off. Project-wide. */
|
||||
enabled?: boolean;
|
||||
/** JSON Schema for the profile. Every property needs a `description`. */
|
||||
schema?: Record<string, any> | null;
|
||||
customInstructions?: string | null;
|
||||
}
|
||||
|
||||
/** One entity type's stored configuration. */
|
||||
export interface EntityProfileSettings {
|
||||
/** The customer's JSON Schema, with its property names verbatim. */
|
||||
schema?: Record<string, any> | null;
|
||||
customInstructions?: string | null;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* The settings as stored. `schema` and `customInstructions` are per entity
|
||||
* type and nest under `entities`; only `enabled` is project-wide.
|
||||
*/
|
||||
export interface ProfileSettingsResponse {
|
||||
enabled?: boolean;
|
||||
entities?: Partial<Record<ProfileEntityType, EntityProfileSettings>>;
|
||||
capabilities?: Record<string, any>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/** `GET /v2/profiles/jobs/{id}/`. The job nests under `job`. */
|
||||
export interface ProfileJobStatus {
|
||||
job: {
|
||||
id: string;
|
||||
operation: string;
|
||||
entityType: ProfileEntityType;
|
||||
status: string;
|
||||
/** Null until `enumerationComplete`. */
|
||||
total: number | null;
|
||||
enumerationComplete: boolean;
|
||||
/** succeeded + failed + skipped. */
|
||||
completed: number;
|
||||
succeeded: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* MemoryClient unit tests — profiles.
|
||||
* Verifies request construction and the verbatim round-trip of user-controlled
|
||||
* profile/schema keys, not mock response echo.
|
||||
*/
|
||||
import { MemoryClient } from "../mem0";
|
||||
import type { ProfileStatus } from "../mem0.types";
|
||||
import { TEST_API_KEY } from "./helpers";
|
||||
import {
|
||||
setupMockFetch,
|
||||
findFetchCall,
|
||||
getFetchBody,
|
||||
installConsoleSuppression,
|
||||
} from "./setup";
|
||||
|
||||
installConsoleSuppression();
|
||||
|
||||
describe("MemoryClient - getProfile()", () => {
|
||||
test("reads the v2 entity route and keeps profile keys verbatim", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/entities/user/alice/profile/", {
|
||||
status: 200,
|
||||
body: {
|
||||
// Customer schema keys: camel-casing these would break the schema they wrote.
|
||||
profile: {
|
||||
favorite_topics: ["hiking"],
|
||||
work_style: { preferred_hours: "mornings" },
|
||||
},
|
||||
status: "succeeded",
|
||||
entity_type: "user",
|
||||
entity_id: "alice",
|
||||
updated_at: "2026-02-08T00:00:00Z",
|
||||
generation_count: 3,
|
||||
},
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result = await client.getProfile({ entityId: "alice" });
|
||||
|
||||
const call = findFetchCall(mock, "/v2/entities/user/alice/profile/");
|
||||
expect(call).toBeDefined();
|
||||
|
||||
expect(result.profile).toEqual({
|
||||
favorite_topics: ["hiking"],
|
||||
work_style: { preferred_hours: "mornings" },
|
||||
});
|
||||
expect(result.entityType).toBe("user");
|
||||
expect(result.entityId).toBe("alice");
|
||||
expect(result.generationCount).toBe(3);
|
||||
expect(result.status).toBe("succeeded");
|
||||
});
|
||||
|
||||
test("status keeps its wire spelling", async () => {
|
||||
// A status is a VALUE, not a key, so the client does not camel-case it.
|
||||
// Declaring the union as `insufficientData` made tsc reject the comparison
|
||||
// that works and accept one that can never be true.
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/entities/user/bob/profile/", {
|
||||
status: 200,
|
||||
body: {
|
||||
profile: {},
|
||||
status: "insufficient_data",
|
||||
entity_type: "user",
|
||||
entity_id: "bob",
|
||||
},
|
||||
});
|
||||
setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result = await client.getProfile({ entityId: "bob" });
|
||||
|
||||
expect(result.status).toBe("insufficient_data");
|
||||
// Assignable without a cast: the declared union must contain the wire value.
|
||||
const status: ProfileStatus = result.status;
|
||||
expect(status).not.toBe("insufficientData");
|
||||
});
|
||||
|
||||
test("scopes to user and encodes the entity id", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/entities/user/", {
|
||||
status: 200,
|
||||
body: { profile: {}, status: "pending", entity_type: "user" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.getProfile({ entityId: "a/b" });
|
||||
|
||||
const call = findFetchCall(mock, "/v2/entities/user/a%2Fb/profile/");
|
||||
expect(call).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryClient - generateProfile()", () => {
|
||||
test("sends operation trigger with the entity to the jobs collection", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/jobs/", {
|
||||
status: 202,
|
||||
body: {
|
||||
job_id: "00000000-0000-7000-8000-000000000001",
|
||||
status: "QUEUED",
|
||||
status_url: "/v2/profiles/jobs/00000000-0000-7000-8000-000000000001/",
|
||||
operation: "trigger",
|
||||
entity_type: "user",
|
||||
entity_count_reserved: 1,
|
||||
event_id: "00000000-0000-7000-8000-000000000002",
|
||||
replayed: false,
|
||||
},
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result = await client.generateProfile({ entityId: "alice" });
|
||||
|
||||
const call = findFetchCall(mock, "/v2/profiles/jobs/", "POST");
|
||||
expect(call).toBeDefined();
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.operation).toBe("trigger");
|
||||
expect(body.entity_type).toBe("user");
|
||||
expect(body.entity_id).toBe("alice");
|
||||
// A generated Idempotency-Key is always sent so the create can be retried safely.
|
||||
const headers = call![1].headers as Record<string, string>;
|
||||
expect(headers["Idempotency-Key"]).toBeTruthy();
|
||||
expect(result.jobId).toBe("00000000-0000-7000-8000-000000000001");
|
||||
expect(result.statusUrl).toBe(
|
||||
"/v2/profiles/jobs/00000000-0000-7000-8000-000000000001/",
|
||||
);
|
||||
expect(result.replayed).toBe(false);
|
||||
});
|
||||
|
||||
test("reuses a caller-supplied idempotency key", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/jobs/", {
|
||||
status: 202,
|
||||
body: {
|
||||
job_id: "j1",
|
||||
status: "QUEUED",
|
||||
status_url: "/v2/profiles/jobs/j1/",
|
||||
operation: "trigger",
|
||||
entity_type: "user",
|
||||
},
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.generateProfile({
|
||||
entityId: "alice",
|
||||
idempotencyKey: "retry-key-123",
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/v2/profiles/jobs/", "POST");
|
||||
const headers = call![1].headers as Record<string, string>;
|
||||
expect(headers["Idempotency-Key"]).toBe("retry-key-123");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryClient - profile settings", () => {
|
||||
test("sends schema property names verbatim and returns them unchanged", async () => {
|
||||
const schema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
favorite_topics: {
|
||||
type: "array",
|
||||
description: "Topics the user returns to",
|
||||
items: { type: "string" },
|
||||
},
|
||||
workStyle: {
|
||||
type: "string",
|
||||
description: "How the user prefers to work",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/settings/", {
|
||||
status: 200,
|
||||
body: {
|
||||
enabled: true,
|
||||
entities: {
|
||||
user: {
|
||||
schema,
|
||||
custom_instructions: "Focus on durable preferences",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result = await client.updateProfileSettings({
|
||||
enabled: true,
|
||||
schema,
|
||||
customInstructions: "Focus on durable preferences",
|
||||
});
|
||||
|
||||
const call = findFetchCall(mock, "/v2/profiles/settings/", "POST");
|
||||
expect(call).toBeDefined();
|
||||
const body = getFetchBody(call!);
|
||||
|
||||
// Mixed casing goes out exactly as written, nested under the entity type.
|
||||
// Exact, not a subset match: the customer's keys must go out unchanged.
|
||||
expect(body.entities).toEqual({
|
||||
user: { schema, custom_instructions: "Focus on durable preferences" },
|
||||
});
|
||||
expect(body.enabled).toBe(true);
|
||||
// A flat schema is rejected by the API with "Unsupported settings".
|
||||
expect("schema" in body).toBe(false);
|
||||
|
||||
// And the customer's property names survive the round trip.
|
||||
expect(result.entities?.user?.schema).toEqual(schema);
|
||||
expect(result.entities?.user?.customInstructions).toBe(
|
||||
"Focus on durable preferences",
|
||||
);
|
||||
});
|
||||
|
||||
test("scopes entity-level settings under user", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/settings/", {
|
||||
status: 200,
|
||||
body: { enabled: true },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.updateProfileSettings({
|
||||
schema: { type: "object", properties: {} },
|
||||
});
|
||||
|
||||
const body = getFetchBody(
|
||||
findFetchCall(mock, "/v2/profiles/settings/", "POST")!,
|
||||
);
|
||||
expect(Object.keys(body.entities as object)).toEqual(["user"]);
|
||||
});
|
||||
|
||||
test("omits fields the caller did not set", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/settings/", {
|
||||
status: 200,
|
||||
body: {
|
||||
enabled: false,
|
||||
entities: { user: { schema: null, custom_instructions: null } },
|
||||
},
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.updateProfileSettings({ enabled: false });
|
||||
|
||||
const call = findFetchCall(mock, "/v2/profiles/settings/", "POST");
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.enabled).toBe(false);
|
||||
// Nothing entity-scoped was passed, so no entities key is sent at all.
|
||||
expect("entities" in body).toBe(false);
|
||||
expect("schema" in body).toBe(false);
|
||||
expect("custom_instructions" in body).toBe(false);
|
||||
});
|
||||
|
||||
test("getProfileSettings reads the v2 route", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/settings/", {
|
||||
status: 200,
|
||||
body: {
|
||||
enabled: true,
|
||||
entities: {
|
||||
user: {
|
||||
schema: { properties: { favorite_topics: { type: "array" } } },
|
||||
custom_instructions: null,
|
||||
},
|
||||
},
|
||||
capabilities: { full_rebuild: false },
|
||||
},
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result = await client.getProfileSettings();
|
||||
|
||||
expect(findFetchCall(mock, "/v2/profiles/settings/")).toBeDefined();
|
||||
expect(result.enabled).toBe(true);
|
||||
expect(result.entities?.user?.schema).toEqual({
|
||||
properties: { favorite_topics: { type: "array" } },
|
||||
});
|
||||
expect(result.capabilities?.fullRebuild).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryClient - sampleProfiles()", () => {
|
||||
test("sampleProfiles omits limit when unset", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/jobs/", {
|
||||
status: 202,
|
||||
body: {
|
||||
job_id: "job_1",
|
||||
status: "QUEUED",
|
||||
status_url: "/v2/profiles/jobs/job_1/",
|
||||
operation: "sample",
|
||||
entity_type: "user",
|
||||
sampled: 5,
|
||||
results: [],
|
||||
},
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.sampleProfiles();
|
||||
|
||||
const call = findFetchCall(mock, "/v2/profiles/jobs/", "POST");
|
||||
// Every job names an entity kind: the API refuses one that does not.
|
||||
expect(getFetchBody(call!)).toEqual({
|
||||
operation: "sample",
|
||||
entity_type: "user",
|
||||
});
|
||||
});
|
||||
|
||||
test("sampleProfiles passes an explicit limit", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/jobs/", {
|
||||
status: 202,
|
||||
body: {
|
||||
job_id: "job_2",
|
||||
status: "QUEUED",
|
||||
status_url: "/v2/profiles/jobs/job_2/",
|
||||
operation: "sample",
|
||||
entity_type: "user",
|
||||
sampled: 3,
|
||||
results: [],
|
||||
},
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result = await client.sampleProfiles({ limit: 3 });
|
||||
|
||||
const call = findFetchCall(mock, "/v2/profiles/jobs/", "POST");
|
||||
expect(getFetchBody(call!).operation).toBe("sample");
|
||||
expect(getFetchBody(call!).limit).toBe(3);
|
||||
expect(getFetchBody(call!).entity_type).toBe("user");
|
||||
expect(result.sampled).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -34,6 +34,9 @@ const OPAQUE_VALUE_KEYS = new Set([
|
||||
// (see issue #5738; same class as `metadata`/`structuredDataSchema`).
|
||||
"customCategories",
|
||||
"custom_categories",
|
||||
// A profile's keys come from the customer's own JSON Schema. The schema itself
|
||||
// is handled in `updateProfileSettings`, so that `schema` is not opaque globally.
|
||||
"profile",
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
+382
-2
@@ -1,6 +1,7 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
import warnings
|
||||
from typing import Any, Dict, List, Optional
|
||||
from urllib.parse import quote
|
||||
@@ -39,6 +40,43 @@ setup_config()
|
||||
# Entity parameters that must be passed via filters, not top-level
|
||||
ENTITY_PARAMS = frozenset({"user_id", "agent_id", "app_id", "run_id"})
|
||||
|
||||
# One collection for every generation; the operation is a body field.
|
||||
PROFILE_JOBS_PATH = "/v2/profiles/jobs/"
|
||||
PROFILE_SETTINGS_PATH = "/v2/profiles/settings/"
|
||||
|
||||
# Distinguishes an omitted argument from an explicit ``None`` that clears a field.
|
||||
_UNSET: Any = object()
|
||||
|
||||
|
||||
def _profile_settings_payload(
|
||||
enabled: Optional[bool],
|
||||
schema: Any = _UNSET,
|
||||
custom_instructions: Any = _UNSET,
|
||||
) -> Dict[str, Any]:
|
||||
"""Build the settings body the API accepts.
|
||||
|
||||
``schema`` and ``custom_instructions`` are per user and nest under
|
||||
``entities``; only ``enabled`` is project-wide. This mirrors what
|
||||
``get_profile_settings`` returns, so the two round-trip.
|
||||
|
||||
Sending them flat is rejected with ``Unsupported settings``, so this shape is
|
||||
not cosmetic. ``_UNSET`` leaves a field unchanged; an explicit ``None`` clears it.
|
||||
"""
|
||||
|
||||
payload: Dict[str, Any] = {}
|
||||
if enabled is not None:
|
||||
payload["enabled"] = enabled
|
||||
|
||||
entity_settings: Dict[str, Any] = {}
|
||||
if schema is not _UNSET:
|
||||
entity_settings["schema"] = schema
|
||||
if custom_instructions is not _UNSET:
|
||||
entity_settings["custom_instructions"] = custom_instructions
|
||||
|
||||
if entity_settings:
|
||||
payload["entities"] = {"user": entity_settings}
|
||||
return payload
|
||||
|
||||
|
||||
def _validate_and_trim_search_query(query: str) -> str:
|
||||
if not isinstance(query, str):
|
||||
@@ -452,7 +490,9 @@ class MemoryClient:
|
||||
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, timestamp, or expiration_date 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()
|
||||
@@ -763,6 +803,174 @@ class MemoryClient:
|
||||
capture_client_event("client.get_summary", self, {"sync_type": "sync"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
def get_profile(self, entity_id: str) -> Dict[str, Any]:
|
||||
"""Get the memory profile for a single user.
|
||||
|
||||
Branch on ``status``, not on an empty ``profile``: generation is
|
||||
asynchronous, so a known user without a profile yet is a normal response.
|
||||
|
||||
Args:
|
||||
entity_id: The user's id, as you supplied it on ``add`` (e.g. "alice").
|
||||
|
||||
Returns:
|
||||
Dict with ``profile``, ``status``, ``entity_type``, ``entity_id``,
|
||||
``updated_at`` and ``generation_count``. ``status`` is one of
|
||||
"succeeded", "pending", "failed", "not_enabled" or "insufficient_data".
|
||||
|
||||
Raises:
|
||||
AuthenticationError: If authentication fails.
|
||||
NotFoundError: If no such user exists in the project.
|
||||
"""
|
||||
|
||||
response = self.client.get(f"/v2/entities/user/{_encode_path_segment(entity_id)}/profile/")
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.get_profile", self, {"sync_type": "sync"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
def generate_profile(self, entity_id: str, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Generate or refresh the profile for a single user, now.
|
||||
|
||||
Profiles are otherwise built once a user crosses an internal message
|
||||
threshold, so a new user has none for its first few memories. Returns as
|
||||
soon as the work is queued: poll :meth:`get_profile` and branch on ``status``.
|
||||
|
||||
Args:
|
||||
entity_id: The user's id, as you supplied it on ``add``.
|
||||
idempotency_key: Optional key that makes the create idempotent. Reuse
|
||||
the same value to safely retry a lost request without starting
|
||||
(and being billed for) a second job. A fresh key is generated when
|
||||
omitted.
|
||||
|
||||
Returns:
|
||||
Dict containing ``job_id``, ``status``, ``status_url``, ``operation``,
|
||||
``entity_type``, ``entity_count_reserved``, ``event_id`` and
|
||||
``replayed``. Poll :meth:`get_profile` and branch on ``status``.
|
||||
|
||||
Raises:
|
||||
ValidationError: If profiles are not enabled and configured for the
|
||||
project.
|
||||
NotFoundError: If no such user exists in the project.
|
||||
"""
|
||||
|
||||
response = self.client.post(
|
||||
PROFILE_JOBS_PATH,
|
||||
json={"operation": "trigger", "entity_type": "user", "entity_id": entity_id},
|
||||
headers={"Idempotency-Key": idempotency_key or uuid.uuid4().hex},
|
||||
)
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.generate_profile", self, {"sync_type": "sync"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
def get_profile_settings(self) -> Dict[str, Any]:
|
||||
"""Get the profile settings for the current project.
|
||||
|
||||
Returns:
|
||||
Dict with ``enabled`` and ``capabilities`` at the top level, and
|
||||
``entities`` holding each entity type's ``schema`` and
|
||||
``custom_instructions``.
|
||||
"""
|
||||
|
||||
response = self.client.get(PROFILE_SETTINGS_PATH)
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.get_profile_settings", self, {"sync_type": "sync"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
def update_profile_settings(
|
||||
self,
|
||||
enabled: Optional[bool] = None,
|
||||
schema: Any = _UNSET,
|
||||
custom_instructions: Any = _UNSET,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update the profile settings for the current project.
|
||||
|
||||
Only the arguments you pass are written.
|
||||
|
||||
Args:
|
||||
enabled: Turn profile generation on or off. Project-wide.
|
||||
schema: JSON Schema for the profile. Every property needs a
|
||||
``description``. Pass ``None`` to clear it; omit to leave it
|
||||
unchanged. Applies to user profiles.
|
||||
custom_instructions: Extra guidance for the extraction step. Pass
|
||||
``None`` to clear it; omit to leave it unchanged. Applies to user
|
||||
profiles.
|
||||
|
||||
Returns:
|
||||
Dict with the settings as stored after the update, in the same
|
||||
shape :meth:`get_profile_settings` returns.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the schema is not a valid profile schema.
|
||||
"""
|
||||
|
||||
payload = _profile_settings_payload(enabled, schema, custom_instructions)
|
||||
response = self.client.post(PROFILE_SETTINGS_PATH, json=payload)
|
||||
response.raise_for_status()
|
||||
capture_client_event(
|
||||
"client.update_profile_settings",
|
||||
self,
|
||||
{"keys": list(payload.keys()), "sync_type": "sync"},
|
||||
)
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
def sample_profiles(self, limit: Optional[int] = None, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Generate profiles for a few real users, to check a schema.
|
||||
|
||||
Real generations against real memories, and the results are kept. The
|
||||
profiles are written to those users and count toward usage.
|
||||
|
||||
Args:
|
||||
limit: How many users to sample, 1-10. Defaults to the server value.
|
||||
idempotency_key: Optional key that makes the create idempotent. Reuse
|
||||
the same value to safely retry without starting a second sample
|
||||
run. A fresh key is generated when omitted.
|
||||
|
||||
Returns:
|
||||
Dict containing ``job_id``, ``status``, ``status_url``, ``sampled``
|
||||
and the ``entity_ids`` that were picked. Poll
|
||||
:meth:`get_profile_job` with ``status_url`` until the status is terminal.
|
||||
|
||||
Raises:
|
||||
ValidationError: If profiles are not enabled and configured.
|
||||
RateLimitError: If a sample run was already started very recently.
|
||||
"""
|
||||
|
||||
payload = self._prepare_params({"limit": limit})
|
||||
payload["operation"] = "sample"
|
||||
payload["entity_type"] = "user"
|
||||
response = self.client.post(
|
||||
PROFILE_JOBS_PATH,
|
||||
json=payload,
|
||||
headers={"Idempotency-Key": idempotency_key or uuid.uuid4().hex},
|
||||
)
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.sample_profiles", self, {"sync_type": "sync"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
def get_profile_job(self, job_id_or_status_url: str) -> Dict[str, Any]:
|
||||
"""Read one generation job.
|
||||
|
||||
Accepts the ``status_url`` from a create call, or a bare job id. Prefer
|
||||
passing ``status_url`` so a route change needs no client update.
|
||||
|
||||
Returns:
|
||||
Dict whose ``job`` key holds the job: ``status``, ``total``,
|
||||
``completed``, ``succeeded``, ``failed`` and ``skipped``. ``total`` is
|
||||
null until ``enumeration_complete``.
|
||||
"""
|
||||
|
||||
path = job_id_or_status_url
|
||||
if not path.startswith("/"):
|
||||
path = f"{PROFILE_JOBS_PATH}{path}/"
|
||||
response = self.client.get(path)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
def get_project(self, fields: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||
"""Get instructions or categories for the current project.
|
||||
@@ -1361,7 +1569,9 @@ class AsyncMemoryClient:
|
||||
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, timestamp, or expiration_date 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()
|
||||
@@ -1658,6 +1868,176 @@ class AsyncMemoryClient:
|
||||
capture_client_event("client.get_summary", self, {"sync_type": "async"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
async def get_profile(self, entity_id: str) -> Dict[str, Any]:
|
||||
"""Get the memory profile for a single user.
|
||||
|
||||
Branch on ``status``, not on an empty ``profile``: generation is
|
||||
asynchronous, so a known user without a profile yet is a normal response.
|
||||
|
||||
Args:
|
||||
entity_id: The user's id, as you supplied it on ``add`` (e.g. "alice").
|
||||
|
||||
Returns:
|
||||
Dict with ``profile``, ``status``, ``entity_type``, ``entity_id``,
|
||||
``updated_at`` and ``generation_count``. ``status`` is one of
|
||||
"succeeded", "pending", "failed", "not_enabled" or "insufficient_data".
|
||||
|
||||
Raises:
|
||||
AuthenticationError: If authentication fails.
|
||||
NotFoundError: If no such user exists in the project.
|
||||
"""
|
||||
|
||||
response = await self.async_client.get(f"/v2/entities/user/{_encode_path_segment(entity_id)}/profile/")
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.get_profile", self, {"sync_type": "async"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
async def generate_profile(self, entity_id: str, idempotency_key: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Generate or refresh the profile for a single user, now.
|
||||
|
||||
Profiles are otherwise built once a user crosses an internal message
|
||||
threshold, so a new user has none for its first few memories. Returns as
|
||||
soon as the work is queued: poll :meth:`get_profile` and branch on ``status``.
|
||||
|
||||
Args:
|
||||
entity_id: The user's id, as you supplied it on ``add``.
|
||||
idempotency_key: Optional key that makes the create idempotent. Reuse
|
||||
the same value to safely retry a lost request without starting
|
||||
(and being billed for) a second job. A fresh key is generated when
|
||||
omitted.
|
||||
|
||||
Returns:
|
||||
Dict containing ``job_id``, ``status``, ``status_url``, ``operation``,
|
||||
``entity_type``, ``entity_count_reserved``, ``event_id`` and
|
||||
``replayed``. Poll :meth:`get_profile` and branch on ``status``.
|
||||
|
||||
Raises:
|
||||
ValidationError: If profiles are not enabled and configured for the
|
||||
project.
|
||||
NotFoundError: If no such user exists in the project.
|
||||
"""
|
||||
|
||||
response = await self.async_client.post(
|
||||
PROFILE_JOBS_PATH,
|
||||
json={"operation": "trigger", "entity_type": "user", "entity_id": entity_id},
|
||||
headers={"Idempotency-Key": idempotency_key or uuid.uuid4().hex},
|
||||
)
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.generate_profile", self, {"sync_type": "async"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
async def get_profile_settings(self) -> Dict[str, Any]:
|
||||
"""Get the profile settings for the current project.
|
||||
|
||||
Returns:
|
||||
Dict with ``enabled`` and ``capabilities`` at the top level, and
|
||||
``entities`` holding each entity type's ``schema`` and
|
||||
``custom_instructions``.
|
||||
"""
|
||||
|
||||
response = await self.async_client.get(PROFILE_SETTINGS_PATH)
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.get_profile_settings", self, {"sync_type": "async"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
async def update_profile_settings(
|
||||
self,
|
||||
enabled: Optional[bool] = None,
|
||||
schema: Any = _UNSET,
|
||||
custom_instructions: Any = _UNSET,
|
||||
) -> Dict[str, Any]:
|
||||
"""Update the profile settings for the current project.
|
||||
|
||||
Only the arguments you pass are written.
|
||||
|
||||
Args:
|
||||
enabled: Turn profile generation on or off. Project-wide.
|
||||
schema: JSON Schema for the profile. Every property needs a
|
||||
``description``. Pass ``None`` to clear it; omit to leave it
|
||||
unchanged. Applies to user profiles.
|
||||
custom_instructions: Extra guidance for the extraction step. Pass
|
||||
``None`` to clear it; omit to leave it unchanged. Applies to user
|
||||
profiles.
|
||||
|
||||
Returns:
|
||||
Dict with the settings as stored after the update, in the same
|
||||
shape :meth:`get_profile_settings` returns.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the schema is not a valid profile schema.
|
||||
"""
|
||||
|
||||
payload = _profile_settings_payload(enabled, schema, custom_instructions)
|
||||
response = await self.async_client.post(PROFILE_SETTINGS_PATH, json=payload)
|
||||
response.raise_for_status()
|
||||
capture_client_event(
|
||||
"client.update_profile_settings",
|
||||
self,
|
||||
{"keys": list(payload.keys()), "sync_type": "async"},
|
||||
)
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
async def sample_profiles(
|
||||
self, limit: Optional[int] = None, idempotency_key: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Generate profiles for a few real users, to check a schema.
|
||||
|
||||
Real generations against real memories, and the results are kept. The
|
||||
profiles are written to those users and count toward usage.
|
||||
|
||||
Args:
|
||||
limit: How many users to sample, 1-10. Defaults to the server value.
|
||||
idempotency_key: Optional key that makes the create idempotent. Reuse
|
||||
the same value to safely retry without starting a second sample
|
||||
run. A fresh key is generated when omitted.
|
||||
|
||||
Returns:
|
||||
Dict containing ``job_id``, ``status``, ``status_url``, ``sampled``
|
||||
and the ``entity_ids`` that were picked. Poll
|
||||
:meth:`get_profile_job` with ``status_url`` until the status is terminal.
|
||||
|
||||
Raises:
|
||||
ValidationError: If profiles are not enabled and configured.
|
||||
RateLimitError: If a sample run was already started very recently.
|
||||
"""
|
||||
|
||||
payload = self._prepare_params({"limit": limit})
|
||||
payload["operation"] = "sample"
|
||||
payload["entity_type"] = "user"
|
||||
response = await self.async_client.post(
|
||||
PROFILE_JOBS_PATH,
|
||||
json=payload,
|
||||
headers={"Idempotency-Key": idempotency_key or uuid.uuid4().hex},
|
||||
)
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.sample_profiles", self, {"sync_type": "async"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
async def get_profile_job(self, job_id_or_status_url: str) -> Dict[str, Any]:
|
||||
"""Read one generation job.
|
||||
|
||||
Accepts the ``status_url`` from a create call, or a bare job id. Prefer
|
||||
passing ``status_url`` so a route change needs no client update.
|
||||
|
||||
Returns:
|
||||
Dict whose ``job`` key holds the job: ``status``, ``total``,
|
||||
``completed``, ``succeeded``, ``failed`` and ``skipped``. ``total`` is
|
||||
null until ``enumeration_complete``.
|
||||
"""
|
||||
|
||||
path = job_id_or_status_url
|
||||
if not path.startswith("/"):
|
||||
path = f"{PROFILE_JOBS_PATH}{path}/"
|
||||
response = await self.async_client.get(path)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
async def get_project(self, fields: Optional[List[str]] = None) -> Dict[str, Any]:
|
||||
"""Get instructions or categories for the current project.
|
||||
|
||||
@@ -299,13 +299,8 @@ class ValkeyDB(VectorStoreBase):
|
||||
# Create the key for the hash
|
||||
key = f"{self.prefix}:{id}"
|
||||
|
||||
# Check for required fields and provide defaults if missing
|
||||
if "data" not in payload:
|
||||
# Silently use default value for missing 'data' field
|
||||
pass
|
||||
|
||||
# Ensure created_at is present
|
||||
if "created_at" not in payload:
|
||||
# Default created_at when missing or None to current time
|
||||
if not payload.get("created_at"):
|
||||
payload["created_at"] = datetime.now(pytz.timezone(self.timezone)).isoformat()
|
||||
|
||||
# Prepare the hash data
|
||||
@@ -499,13 +494,8 @@ class ValkeyDB(VectorStoreBase):
|
||||
try:
|
||||
key = f"{self.prefix}:{vector_id}"
|
||||
|
||||
# Check for required fields and provide defaults if missing
|
||||
if "data" not in payload:
|
||||
# Silently use default value for missing 'data' field
|
||||
pass
|
||||
|
||||
# Ensure created_at is present
|
||||
if "created_at" not in payload:
|
||||
# Default created_at when missing or None to current time
|
||||
if not payload.get("created_at"):
|
||||
payload["created_at"] = datetime.now(pytz.timezone(self.timezone)).isoformat()
|
||||
|
||||
# Prepare the hash data
|
||||
@@ -521,7 +511,7 @@ class ValkeyDB(VectorStoreBase):
|
||||
hash_data["embedding"] = np.array(vector, dtype=np.float32).tobytes()
|
||||
|
||||
# Add updated_at if available
|
||||
if "updated_at" in payload:
|
||||
if payload.get("updated_at"):
|
||||
hash_data["updated_at"] = int(datetime.fromisoformat(payload["updated_at"]).timestamp())
|
||||
|
||||
# Add optional fields
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "mem0ai"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
description = "Long-term memory for AI Agents"
|
||||
authors = [
|
||||
{ name = "Mem0", email = "support@mem0.ai" }
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
"""Tests for the MemoryClient profile methods.
|
||||
|
||||
These assert request construction — path, verb, body — rather than echoing a
|
||||
mocked response back. The profile payload itself is the customer's own JSON
|
||||
Schema shape, so the tests also pin that the SDK passes it through untouched.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_memory_client():
|
||||
"""A MemoryClient whose transport is mocked."""
|
||||
with patch("mem0.client.main.httpx.Client") as mock_httpx:
|
||||
mock_http_client = MagicMock()
|
||||
mock_http_client.get.return_value = MagicMock(
|
||||
json=lambda: {"org_id": "org1", "project_id": "proj1", "user_email": "test@test.com"},
|
||||
raise_for_status=lambda: None,
|
||||
)
|
||||
mock_httpx.return_value = mock_http_client
|
||||
|
||||
with patch("mem0.client.main.capture_client_event"):
|
||||
from mem0.client.main import MemoryClient
|
||||
|
||||
client = MemoryClient(api_key="test-api-key")
|
||||
# The constructor pings through this same mock; drop that call.
|
||||
mock_http_client.get.reset_mock()
|
||||
yield client
|
||||
|
||||
|
||||
def _assert_job_call(post_mock, expected_json):
|
||||
"""One jobs collection, operation in the body, idempotency key per attempt."""
|
||||
post_mock.assert_called_once()
|
||||
args, kwargs = post_mock.call_args
|
||||
assert args[0] == "/v2/profiles/jobs/"
|
||||
assert kwargs["json"] == expected_json
|
||||
assert kwargs["headers"]["Idempotency-Key"]
|
||||
|
||||
|
||||
def _mock_response(payload):
|
||||
response = MagicMock()
|
||||
response.json.return_value = payload
|
||||
response.raise_for_status.return_value = None
|
||||
return response
|
||||
|
||||
|
||||
class TestGetProfile:
|
||||
def test_reads_the_v2_entity_route(self, mock_memory_client):
|
||||
mock_memory_client.client.get.return_value = _mock_response(
|
||||
{"profile": {}, "status": "pending", "entity_type": "user", "entity_id": "alice"}
|
||||
)
|
||||
|
||||
mock_memory_client.get_profile("alice")
|
||||
|
||||
mock_memory_client.client.get.assert_called_once_with("/v2/entities/user/alice/profile/")
|
||||
|
||||
def test_encodes_path_segments(self, mock_memory_client):
|
||||
"""An id with a slash must not open a new path segment."""
|
||||
mock_memory_client.client.get.return_value = _mock_response({"profile": {}, "status": "pending"})
|
||||
|
||||
mock_memory_client.get_profile("tenant/alice")
|
||||
|
||||
mock_memory_client.client.get.assert_called_once_with("/v2/entities/user/tenant%2Falice/profile/")
|
||||
|
||||
def test_returns_the_envelope_verbatim(self, mock_memory_client):
|
||||
"""The customer's schema keys reach the caller exactly as stored."""
|
||||
payload = {
|
||||
"profile": {"favorite_topics": ["hiking"], "work_style": {"preferred_hours": "mornings"}},
|
||||
"status": "succeeded",
|
||||
"entity_type": "user",
|
||||
"entity_id": "alice",
|
||||
"updated_at": "2026-02-08T00:00:00Z",
|
||||
"generation_count": 3,
|
||||
}
|
||||
mock_memory_client.client.get.return_value = _mock_response(payload)
|
||||
|
||||
assert mock_memory_client.get_profile("alice") == payload
|
||||
|
||||
|
||||
class TestGenerateProfile:
|
||||
def test_posts_entity_type_and_id(self, mock_memory_client):
|
||||
mock_memory_client.client.post.return_value = _mock_response(
|
||||
{"job_id": "j1", "status": "QUEUED", "status_url": "/v2/profiles/jobs/j1/"}
|
||||
)
|
||||
|
||||
mock_memory_client.generate_profile("alice")
|
||||
|
||||
_assert_job_call(
|
||||
mock_memory_client.client.post,
|
||||
{"operation": "trigger", "entity_type": "user", "entity_id": "alice"},
|
||||
)
|
||||
|
||||
def test_reuses_caller_idempotency_key(self, mock_memory_client):
|
||||
"""A caller-supplied key lets a retry hit the same job instead of billing twice."""
|
||||
mock_memory_client.client.post.return_value = _mock_response({"job_id": "j1", "status": "QUEUED"})
|
||||
|
||||
mock_memory_client.generate_profile("alice", idempotency_key="retry-key-123")
|
||||
|
||||
_, kwargs = mock_memory_client.client.post.call_args
|
||||
assert kwargs["headers"]["Idempotency-Key"] == "retry-key-123"
|
||||
|
||||
|
||||
class TestProfileSettings:
|
||||
def test_get_reads_v2(self, mock_memory_client):
|
||||
mock_memory_client.client.get.return_value = _mock_response(
|
||||
{"enabled": True, "entities": {"user": {"schema": None, "custom_instructions": None}}}
|
||||
)
|
||||
|
||||
mock_memory_client.get_profile_settings()
|
||||
|
||||
mock_memory_client.client.get.assert_called_once_with("/v2/profiles/settings/")
|
||||
|
||||
def test_update_sends_only_supplied_fields(self, mock_memory_client):
|
||||
"""A partial update must not blank the fields it never mentions."""
|
||||
mock_memory_client.client.post.return_value = _mock_response({"enabled": False})
|
||||
|
||||
mock_memory_client.update_profile_settings(enabled=False)
|
||||
|
||||
mock_memory_client.client.post.assert_called_once_with(
|
||||
"/v2/profiles/settings/",
|
||||
json={"enabled": False},
|
||||
)
|
||||
|
||||
def test_update_nests_schema_under_entities(self, mock_memory_client):
|
||||
"""The API takes only ``enabled`` and ``entities`` at the top level.
|
||||
|
||||
A flat body is rejected with ``Unsupported settings``, so this nesting is
|
||||
what makes the call work at all.
|
||||
"""
|
||||
schema = {"type": "object", "properties": {"x": {"type": "string", "description": "d"}}}
|
||||
mock_memory_client.client.post.return_value = _mock_response({"enabled": True})
|
||||
|
||||
mock_memory_client.update_profile_settings(enabled=True, schema=schema)
|
||||
|
||||
_, kwargs = mock_memory_client.client.post.call_args
|
||||
assert set(kwargs["json"]) == {"enabled", "entities"}
|
||||
assert "schema" not in kwargs["json"]
|
||||
|
||||
def test_update_targets_the_user_entity_type(self, mock_memory_client):
|
||||
schema = {"type": "object", "properties": {"x": {"type": "string", "description": "d"}}}
|
||||
mock_memory_client.client.post.return_value = _mock_response({"enabled": True})
|
||||
|
||||
mock_memory_client.update_profile_settings(schema=schema)
|
||||
|
||||
_, kwargs = mock_memory_client.client.post.call_args
|
||||
assert kwargs["json"] == {"entities": {"user": {"schema": schema}}}
|
||||
|
||||
def test_update_passes_schema_verbatim(self, mock_memory_client):
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"favorite_topics": {
|
||||
"type": "array",
|
||||
"description": "Topics the user returns to",
|
||||
"items": {"type": "string"},
|
||||
}
|
||||
},
|
||||
}
|
||||
mock_memory_client.client.post.return_value = _mock_response({"enabled": True})
|
||||
|
||||
mock_memory_client.update_profile_settings(enabled=True, schema=schema, custom_instructions="Keep it durable")
|
||||
|
||||
mock_memory_client.client.post.assert_called_once_with(
|
||||
"/v2/profiles/settings/",
|
||||
json={
|
||||
"enabled": True,
|
||||
"entities": {"user": {"schema": schema, "custom_instructions": "Keep it durable"}},
|
||||
},
|
||||
)
|
||||
|
||||
def test_update_clears_fields_with_explicit_none(self, mock_memory_client):
|
||||
"""Explicit ``None`` clears a field; the sentinel default leaves it untouched."""
|
||||
mock_memory_client.client.post.return_value = _mock_response({"enabled": True})
|
||||
|
||||
mock_memory_client.update_profile_settings(schema=None, custom_instructions=None)
|
||||
|
||||
_, kwargs = mock_memory_client.client.post.call_args
|
||||
assert kwargs["json"] == {"entities": {"user": {"schema": None, "custom_instructions": None}}}
|
||||
|
||||
|
||||
class TestSampleProfiles:
|
||||
def test_sample_without_limit(self, mock_memory_client):
|
||||
mock_memory_client.client.post.return_value = _mock_response({"sampled": 5, "entity_ids": []})
|
||||
|
||||
mock_memory_client.sample_profiles()
|
||||
|
||||
_assert_job_call(mock_memory_client.client.post, {"operation": "sample", "entity_type": "user"})
|
||||
|
||||
def test_sample_with_limit(self, mock_memory_client):
|
||||
mock_memory_client.client.post.return_value = _mock_response({"sampled": 3, "entity_ids": []})
|
||||
|
||||
mock_memory_client.sample_profiles(limit=3)
|
||||
|
||||
_assert_job_call(
|
||||
mock_memory_client.client.post,
|
||||
{"operation": "sample", "limit": 3, "entity_type": "user"},
|
||||
)
|
||||
|
||||
|
||||
class TestAsyncClientParity:
|
||||
"""The async client must speak the same wire protocol as the sync one."""
|
||||
|
||||
@pytest.fixture
|
||||
def async_client(self):
|
||||
# AsyncMemoryClient validates the key synchronously, through requests.
|
||||
validation = MagicMock()
|
||||
validation.json.return_value = {
|
||||
"org_id": "org1",
|
||||
"project_id": "proj1",
|
||||
"user_email": "test@test.com",
|
||||
}
|
||||
validation.raise_for_status.return_value = None
|
||||
|
||||
with patch("mem0.client.main.httpx.AsyncClient") as mock_httpx:
|
||||
mock_httpx.return_value = MagicMock()
|
||||
with patch("mem0.client.main.requests.get", return_value=validation):
|
||||
with patch("mem0.client.main.capture_client_event"):
|
||||
from mem0.client.main import AsyncMemoryClient
|
||||
|
||||
yield AsyncMemoryClient(api_key="test-api-key")
|
||||
|
||||
def test_get_profile(self, async_client):
|
||||
async_client.async_client.get = AsyncMock(return_value=_mock_response({"profile": {}, "status": "pending"}))
|
||||
|
||||
asyncio.run(async_client.get_profile("alice"))
|
||||
|
||||
async_client.async_client.get.assert_called_once_with("/v2/entities/user/alice/profile/")
|
||||
|
||||
def test_generate_profile(self, async_client):
|
||||
async_client.async_client.post = AsyncMock(return_value=_mock_response({"job_id": "j1", "status": "QUEUED"}))
|
||||
|
||||
asyncio.run(async_client.generate_profile("alice"))
|
||||
|
||||
_assert_job_call(
|
||||
async_client.async_client.post,
|
||||
{"operation": "trigger", "entity_type": "user", "entity_id": "alice"},
|
||||
)
|
||||
|
||||
def test_update_settings_partial(self, async_client):
|
||||
async_client.async_client.post = AsyncMock(return_value=_mock_response({"enabled": True}))
|
||||
|
||||
asyncio.run(async_client.update_profile_settings(enabled=True))
|
||||
|
||||
async_client.async_client.post.assert_called_once_with(
|
||||
"/v2/profiles/settings/",
|
||||
json={"enabled": True},
|
||||
)
|
||||
|
||||
def test_update_settings_nests_schema(self, async_client):
|
||||
"""The async client builds the same body as the sync one."""
|
||||
schema = {"type": "object", "properties": {"x": {"type": "string", "description": "d"}}}
|
||||
async_client.async_client.post = AsyncMock(return_value=_mock_response({"enabled": True}))
|
||||
|
||||
asyncio.run(async_client.update_profile_settings(enabled=True, schema=schema))
|
||||
|
||||
async_client.async_client.post.assert_called_once_with(
|
||||
"/v2/profiles/settings/",
|
||||
json={"enabled": True, "entities": {"user": {"schema": schema}}},
|
||||
)
|
||||
|
||||
def test_update_settings_clears_with_none(self, async_client):
|
||||
async_client.async_client.post = AsyncMock(return_value=_mock_response({"enabled": True}))
|
||||
|
||||
asyncio.run(async_client.update_profile_settings(custom_instructions=None))
|
||||
|
||||
async_client.async_client.post.assert_called_once_with(
|
||||
"/v2/profiles/settings/",
|
||||
json={"entities": {"user": {"custom_instructions": None}}},
|
||||
)
|
||||
@@ -158,6 +158,32 @@ def test_insert_handles_missing_created_at(valkey_db, mock_valkey_client):
|
||||
assert "created_at" in kwargs["mapping"] # Should be added automatically
|
||||
|
||||
|
||||
def test_insert_and_update_with_none_timestamps(valkey_db, mock_valkey_client):
|
||||
"""Regression: a None timestamp must not crash insert() or update().
|
||||
|
||||
A None created_at falls back to now and a None updated_at is skipped, so
|
||||
neither reaches fromisoformat() which only accepts a str.
|
||||
"""
|
||||
vector = np.random.rand(1536).tolist()
|
||||
|
||||
valkey_db.insert(
|
||||
vectors=[vector],
|
||||
payloads=[{"hash": "h", "data": "d", "created_at": None, "updated_at": None}],
|
||||
ids=["id1"],
|
||||
)
|
||||
_, insert_kwargs = mock_valkey_client.hset.call_args
|
||||
assert isinstance(insert_kwargs["mapping"]["created_at"], int)
|
||||
|
||||
valkey_db.update(
|
||||
vector_id="id1",
|
||||
vector=vector,
|
||||
payload={"hash": "h", "data": "d", "created_at": None, "updated_at": None},
|
||||
)
|
||||
_, update_kwargs = mock_valkey_client.hset.call_args
|
||||
assert isinstance(update_kwargs["mapping"]["created_at"], int)
|
||||
assert "updated_at" not in update_kwargs["mapping"]
|
||||
|
||||
|
||||
def test_delete(valkey_db, mock_valkey_client):
|
||||
"""Test deleting a vector."""
|
||||
# Call delete
|
||||
|
||||
Reference in New Issue
Block a user