feat(profiles): SDK methods and docs for entity profiles
Adds the profile surface to both SDKs and documents it. Python (MemoryClient + AsyncMemoryClient) and TypeScript gain: get/generate profile, get/update settings, sample, regenerate. A profile's keys come from the customer's own JSON Schema, so the TS client keeps `profile` opaque and passes `schema` through untouched in both directions. Camel-casing them would return field names that do not match the schema the customer wrote. Reads use the v2 envelope, where a known entity with no profile yet is a 200 carrying a status rather than a 404, so an empty state is distinguishable from an error. Verified against a live environment running this API: settings round-trip, a partial update leaves the schema intact, generation produces a profile under the configured schema, and sample, regenerate, cooldown and the 400/404 paths all answer as documented.
This commit is contained in:
@@ -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 or agent, with a status describing whether generation has completed."
|
||||
openapi: get /v2/entities/{entity_type}/{entity_id}/profile/
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: 'Regenerate Profiles'
|
||||
description: "Rebuild the profile of every entity in the project, applying the current schema to entities that already have one."
|
||||
openapi: post /v2/profiles/regenerate/
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: 'Sample Profiles'
|
||||
description: "Generate profiles for a small set of real entities to evaluate a schema before applying it across the project."
|
||||
openapi: post /v2/profiles/samples/
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: 'Generate Profile'
|
||||
description: "Generate or refresh the profile for a single user or agent immediately, instead of waiting for the message threshold."
|
||||
openapi: post /v2/profiles/trigger/
|
||||
---
|
||||
@@ -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/
|
||||
---
|
||||
@@ -71,6 +71,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",
|
||||
@@ -511,6 +512,18 @@
|
||||
"api-reference/entities/delete-user"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Profiles",
|
||||
"icon": "id-card",
|
||||
"pages": [
|
||||
"api-reference/profiles/get-profile",
|
||||
"api-reference/profiles/trigger-profile",
|
||||
"api-reference/profiles/get-profile-settings",
|
||||
"api-reference/profiles/update-profile-settings",
|
||||
"api-reference/profiles/sample-profiles",
|
||||
"api-reference/profiles/regenerate-profiles"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Organizations",
|
||||
"icon": "building",
|
||||
|
||||
@@ -197,6 +197,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 or agent 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.
|
||||
@@ -362,6 +363,12 @@ 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 or agent's structured profile and branching on its generation status.
|
||||
- [Generate Profile](https://docs.mem0.ai/api-reference/profiles/trigger-profile) [Platform]: Use when a profile is needed before the entity reaches the automatic message threshold.
|
||||
- [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.
|
||||
- [Sample Profiles](https://docs.mem0.ai/api-reference/profiles/sample-profiles) [Platform]: Use when validating a profile schema against a few real entities before applying it project-wide.
|
||||
- [Regenerate Profiles](https://docs.mem0.ai/api-reference/profiles/regenerate-profiles) [Platform]: Use when a new schema must be applied to entities that already have a profile.
|
||||
|
||||
### Organizations
|
||||
- [Create Organization](https://docs.mem0.ai/api-reference/organization/create-org) [Platform]: Use when setting up a new org.
|
||||
|
||||
+372
-1
@@ -8070,6 +8070,377 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/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 or agent.\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",
|
||||
"agent"
|
||||
]
|
||||
},
|
||||
"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",
|
||||
"agent"
|
||||
]
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether profile generation runs for this project."
|
||||
},
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "The schema is not a valid profile schema."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/profiles/trigger/": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"profiles"
|
||||
],
|
||||
"operationId": "profiles_trigger",
|
||||
"summary": "Generate one entity's profile now",
|
||||
"description": "Generate or refresh the profile for a single entity immediately.\n\nProfiles are otherwise built once an entity crosses an internal message threshold, so a new entity has none for its first few memories. Returns 202: poll the read endpoint and branch on `status`.",
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"entity_id"
|
||||
],
|
||||
"properties": {
|
||||
"entity_type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user",
|
||||
"agent"
|
||||
],
|
||||
"default": "user"
|
||||
},
|
||||
"entity_id": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Generation queued.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"entity_type": {
|
||||
"type": "string"
|
||||
},
|
||||
"entity_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"profile_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Unsupported entity type, or profiles are not enabled and configured."
|
||||
},
|
||||
"404": {
|
||||
"description": "No such entity in this project."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/profiles/samples/": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"profiles"
|
||||
],
|
||||
"operationId": "profiles_samples",
|
||||
"summary": "Sample profiles to check a schema",
|
||||
"description": "Generate profiles for a handful of real entities so a schema can be judged before it is applied project-wide.\n\nThese are real generations and the results are kept, so the work counts toward a later regenerate.",
|
||||
"requestBody": {
|
||||
"required": false,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 10,
|
||||
"description": "How many entities to sample."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Sampling queued.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"sampled": {
|
||||
"type": "integer"
|
||||
},
|
||||
"results": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Profiles are not enabled and configured."
|
||||
},
|
||||
"429": {
|
||||
"description": "A sample run was already started for this project very recently."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/v2/profiles/regenerate/": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"profiles"
|
||||
],
|
||||
"operationId": "profiles_regenerate",
|
||||
"summary": "Rebuild every profile in the project",
|
||||
"description": "Rebuild the profile of every entity in the project. This is how a new schema reaches entities that already have a profile.\n\nReturns 202 immediately; the rebuild runs in the background.",
|
||||
"responses": {
|
||||
"202": {
|
||||
"description": "Regenerate queued.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"project_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"existing_profile_count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Profiles are not enabled and configured."
|
||||
},
|
||||
"409": {
|
||||
"description": "A regenerate is already running for this project."
|
||||
},
|
||||
"429": {
|
||||
"description": "A regenerate already ran for this project within the last hour."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@@ -8988,4 +9359,4 @@
|
||||
}
|
||||
},
|
||||
"x-original-swagger-version": "2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
---
|
||||
title: Profiles
|
||||
description: "Build a structured, always-current summary of each user or agent from their memories, shaped by a JSON Schema you define."
|
||||
icon: "id-card"
|
||||
---
|
||||
|
||||
# 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>
|
||||
|
||||
Profiles cover **users and agents**. An agent profile summarizes how an agent behaves, in the same way a user profile summarizes a person.
|
||||
|
||||
## 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": 4
|
||||
}
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
For an agent, pass the entity type:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
client.get_profile("support-bot", entity_type="agent")
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
await client.getProfile({ entityId: "support-bot", entityType: "agent" });
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
## 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:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
result = client.sample_profiles(limit=5)
|
||||
|
||||
for row in result["results"]:
|
||||
print(client.get_profile(row["entity_id"]))
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
const result = await client.sampleProfiles({ limit: 5 });
|
||||
|
||||
for (const row of result.results) {
|
||||
console.log(await client.getProfile({ entityId: row.entityId }));
|
||||
}
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
These are real generations against real memories, and the results are kept — sampling is not wasted work. Sampling is limited to 10 entities per call and cannot be repeated immediately.
|
||||
|
||||
## Apply a new schema to existing entities
|
||||
|
||||
Changing the schema does not retroactively rewrite profiles that already exist. To rebuild every profile in the project:
|
||||
|
||||
<CodeGroup>
|
||||
```python Python
|
||||
client.regenerate_profiles()
|
||||
```
|
||||
|
||||
```typescript TypeScript
|
||||
await client.regenerateProfiles();
|
||||
```
|
||||
</CodeGroup>
|
||||
|
||||
This returns immediately and runs in the background; a large project takes a while. It is limited to one run per project per hour, so sample first and regenerate once you are satisfied.
|
||||
|
||||
## 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.
|
||||
|
||||
## Settings reference
|
||||
|
||||
| Field | 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 |
|
||||
|
||||
<Note>
|
||||
Profile settings are per project. An API key is scoped to one project, so profiles never cross a project boundary.
|
||||
</Note>
|
||||
|
||||
## 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>
|
||||
@@ -23,6 +23,14 @@ export type {
|
||||
FeedbackPayload,
|
||||
CreateMemoryExportPayload,
|
||||
GetMemoryExportPayload,
|
||||
ProfileEntityType,
|
||||
ProfileStatus,
|
||||
ProfileResponse,
|
||||
ProfileTriggerResponse,
|
||||
ProfileSettings,
|
||||
ProfileSampleResult,
|
||||
ProfileSamplesResponse,
|
||||
ProfileRegenerateResponse,
|
||||
} from "./mem0.types";
|
||||
|
||||
// Re-export enums as values (not type-only)
|
||||
|
||||
+149
-3
@@ -20,6 +20,12 @@ import {
|
||||
FeedbackPayload,
|
||||
CreateMemoryExportPayload,
|
||||
GetMemoryExportPayload,
|
||||
ProfileEntityType,
|
||||
ProfileResponse,
|
||||
ProfileTriggerResponse,
|
||||
ProfileSettings,
|
||||
ProfileSamplesResponse,
|
||||
ProfileRegenerateResponse,
|
||||
} from "./mem0.types";
|
||||
import {
|
||||
captureClientEvent,
|
||||
@@ -244,7 +250,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: {
|
||||
@@ -257,8 +264,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(
|
||||
@@ -755,6 +765,142 @@ export default class MemoryClient {
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the memory profile for a single entity.
|
||||
*
|
||||
* Branch on `status`, not on an empty `profile`: generation is asynchronous,
|
||||
* so a known entity without a profile yet is a normal response.
|
||||
*/
|
||||
async getProfile(data: {
|
||||
entityId: string;
|
||||
entityType?: ProfileEntityType;
|
||||
}): Promise<ProfileResponse> {
|
||||
this._captureEvent("get_profile", []);
|
||||
await this._awaitIdentity();
|
||||
|
||||
const entityType = data.entityType ?? "user";
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}/v2/entities/${encodeURIComponent(entityType)}/${encodeURIComponent(data.entityId)}/profile/`,
|
||||
{
|
||||
headers: this.headers,
|
||||
},
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate or refresh the profile for one entity, now.
|
||||
*
|
||||
* Profiles are otherwise built once an entity crosses an internal message
|
||||
* threshold, so a new entity has none for its first few memories. Returns as
|
||||
* soon as the work is queued: poll {@link getProfile} and branch on `status`.
|
||||
*/
|
||||
async generateProfile(data: {
|
||||
entityId: string;
|
||||
entityType?: ProfileEntityType;
|
||||
}): Promise<ProfileTriggerResponse> {
|
||||
this._captureEvent("generate_profile", []);
|
||||
await this._awaitIdentity();
|
||||
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}/v2/profiles/trigger/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify({
|
||||
entity_type: data.entityType ?? "user",
|
||||
entity_id: data.entityId,
|
||||
}),
|
||||
},
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/** Get the profile settings for the current project. */
|
||||
async getProfileSettings(): Promise<ProfileSettings> {
|
||||
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. */
|
||||
private _settingsWithVerbatimSchema(raw: any): ProfileSettings {
|
||||
const settings = snakeToCamelKeys(raw) as ProfileSettings;
|
||||
if (raw && typeof raw === "object" && "schema" in raw) {
|
||||
settings.schema = raw.schema;
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
/** Update profile settings. Only the fields you pass are written. */
|
||||
async updateProfileSettings(
|
||||
settings: ProfileSettings,
|
||||
): Promise<ProfileSettings> {
|
||||
const payloadKeys = Object.keys(settings || {});
|
||||
this._captureEvent("update_profile_settings", [payloadKeys]);
|
||||
await this._awaitIdentity();
|
||||
|
||||
// The schema's property names are the customer's and must reach the API verbatim.
|
||||
const { schema, ...rest } = settings;
|
||||
const body: Record<string, any> = camelToSnakeKeys(rest);
|
||||
if (schema !== undefined) {
|
||||
body.schema = schema;
|
||||
}
|
||||
|
||||
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 entities, to check a schema.
|
||||
*
|
||||
* Real generations against real memories, and the results are kept.
|
||||
*/
|
||||
async sampleProfiles(data?: {
|
||||
limit?: number;
|
||||
}): Promise<ProfileSamplesResponse> {
|
||||
this._captureEvent("sample_profiles", []);
|
||||
await this._awaitIdentity();
|
||||
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}/v2/profiles/samples/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify(this._prepareParams({ limit: data?.limit })),
|
||||
},
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the profile of every entity in the project.
|
||||
*
|
||||
* This is how a new schema reaches entities that already have a profile.
|
||||
*/
|
||||
async regenerateProfiles(): Promise<ProfileRegenerateResponse> {
|
||||
this._captureEvent("regenerate_profiles", []);
|
||||
await this._awaitIdentity();
|
||||
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}/v2/profiles/regenerate/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: this.headers,
|
||||
body: JSON.stringify({}),
|
||||
},
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
async createMemoryExport(
|
||||
data: CreateMemoryExportPayload,
|
||||
): Promise<{ message: string; id: string }> {
|
||||
|
||||
@@ -236,3 +236,57 @@ export interface GetMemoryExportPayload {
|
||||
filters?: Record<string, any>;
|
||||
memoryExportId?: string;
|
||||
}
|
||||
|
||||
// ─── Profile Types ──────────────────────────────────────────
|
||||
|
||||
/** Entity kinds that can carry a profile. */
|
||||
export type ProfileEntityType = "user" | "agent";
|
||||
|
||||
/** `succeeded` is the only state in which `profile` is guaranteed to hold content. */
|
||||
export type ProfileStatus =
|
||||
"succeeded" | "pending" | "failed" | "notEnabled" | "insufficientData";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export interface ProfileTriggerResponse {
|
||||
message: string;
|
||||
entityType: ProfileEntityType;
|
||||
entityId: string;
|
||||
profileId: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface ProfileSettings {
|
||||
enabled?: boolean;
|
||||
/** JSON Schema for the profile. Every property needs a `description`. */
|
||||
schema?: Record<string, any> | null;
|
||||
customInstructions?: string | null;
|
||||
}
|
||||
|
||||
export interface ProfileSampleResult {
|
||||
entityType: ProfileEntityType;
|
||||
entityId: string;
|
||||
profileId?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ProfileSamplesResponse {
|
||||
message: string;
|
||||
sampled: number;
|
||||
results: Array<ProfileSampleResult>;
|
||||
}
|
||||
|
||||
export interface ProfileRegenerateResponse {
|
||||
status: string;
|
||||
message: string;
|
||||
projectId: string;
|
||||
existingProfileCount: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* 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 { 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("defaults to user and encodes the entity id", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/entities/agent/", {
|
||||
status: 200,
|
||||
body: { profile: {}, status: "pending", entity_type: "agent" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.getProfile({ entityId: "a/b", entityType: "agent" });
|
||||
|
||||
const call = findFetchCall(mock, "/v2/entities/agent/a%2Fb/profile/");
|
||||
expect(call).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryClient - generateProfile()", () => {
|
||||
test("posts entity_type and entity_id to the trigger route", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/trigger/", {
|
||||
status: 202,
|
||||
body: {
|
||||
message: "Profile generation started.",
|
||||
entity_type: "user",
|
||||
entity_id: "alice",
|
||||
profile_id: "p_1",
|
||||
status: "PENDING",
|
||||
},
|
||||
});
|
||||
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/trigger/", "POST");
|
||||
expect(call).toBeDefined();
|
||||
const body = getFetchBody(call!);
|
||||
expect(body.entity_type).toBe("user");
|
||||
expect(body.entity_id).toBe("alice");
|
||||
expect(result.profileId).toBe("p_1");
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
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.
|
||||
expect(body.schema).toEqual(schema);
|
||||
expect(body.custom_instructions).toBe("Focus on durable preferences");
|
||||
expect(body.enabled).toBe(true);
|
||||
expect(result.schema).toEqual(schema);
|
||||
expect(result.customInstructions).toBe("Focus on durable preferences");
|
||||
});
|
||||
|
||||
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, 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);
|
||||
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,
|
||||
schema: { properties: { favorite_topics: { type: "array" } } },
|
||||
custom_instructions: null,
|
||||
},
|
||||
});
|
||||
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.schema).toEqual({
|
||||
properties: { favorite_topics: { type: "array" } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryClient - sampleProfiles() / regenerateProfiles()", () => {
|
||||
test("sampleProfiles omits limit when unset", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/samples/", {
|
||||
status: 202,
|
||||
body: { message: "Sampling 5 users.", sampled: 5, results: [] },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.sampleProfiles();
|
||||
|
||||
const call = findFetchCall(mock, "/v2/profiles/samples/", "POST");
|
||||
expect(getFetchBody(call!)).toEqual({});
|
||||
});
|
||||
|
||||
test("sampleProfiles passes an explicit limit", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/samples/", {
|
||||
status: 202,
|
||||
body: { message: "Sampling 3 users.", 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/samples/", "POST");
|
||||
expect(getFetchBody(call!).limit).toBe(3);
|
||||
expect(result.sampled).toBe(3);
|
||||
});
|
||||
|
||||
test("regenerateProfiles posts to the regenerate route", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/regenerate/", {
|
||||
status: 202,
|
||||
body: {
|
||||
status: "accepted",
|
||||
message: "Regenerating profiles.",
|
||||
project_id: "proj_abc",
|
||||
existing_profile_count: 12,
|
||||
},
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result = await client.regenerateProfiles();
|
||||
|
||||
expect(
|
||||
findFetchCall(mock, "/v2/profiles/regenerate/", "POST"),
|
||||
).toBeDefined();
|
||||
expect(result.existingProfileCount).toBe(12);
|
||||
expect(result.projectId).toBe("proj_abc");
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -682,6 +682,155 @@ 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, entity_type: str = "user") -> Dict[str, Any]:
|
||||
"""Get the memory profile for a single entity.
|
||||
|
||||
Branch on ``status``, not on an empty ``profile``: generation is
|
||||
asynchronous, so a known entity without a profile yet is a normal response.
|
||||
|
||||
Args:
|
||||
entity_id: The entity's id, as you supplied it on ``add`` (e.g. "alice").
|
||||
entity_type: Either "user" or "agent". Defaults to "user".
|
||||
|
||||
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:
|
||||
ValidationError: If entity_type is not a supported entity kind.
|
||||
AuthenticationError: If authentication fails.
|
||||
NotFoundError: If no such entity exists in the project.
|
||||
"""
|
||||
|
||||
response = self.client.get(
|
||||
f"/v2/entities/{_encode_path_segment(entity_type)}/{_encode_path_segment(entity_id)}/profile/"
|
||||
)
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.get_profile", self, {"entity_type": entity_type, "sync_type": "sync"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
def generate_profile(self, entity_id: str, entity_type: str = "user") -> Dict[str, Any]:
|
||||
"""Generate or refresh the profile for a single entity, now.
|
||||
|
||||
Profiles are otherwise built once an entity crosses an internal message
|
||||
threshold, so a new entity 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 entity's id, as you supplied it on ``add``.
|
||||
entity_type: Either "user" or "agent". Defaults to "user".
|
||||
|
||||
Returns:
|
||||
Dict containing ``profile_id``, ``entity_type``, ``entity_id`` and
|
||||
``status``.
|
||||
|
||||
Raises:
|
||||
ValidationError: If entity_type is unsupported or profiles are not
|
||||
enabled and configured for the project.
|
||||
NotFoundError: If no such entity exists in the project.
|
||||
"""
|
||||
|
||||
response = self.client.post(
|
||||
"/v2/profiles/trigger/",
|
||||
json={"entity_type": entity_type, "entity_id": entity_id},
|
||||
)
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.generate_profile", self, {"entity_type": entity_type, "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``, ``schema`` and ``custom_instructions``.
|
||||
"""
|
||||
|
||||
response = self.client.get("/v2/profiles/settings/")
|
||||
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: Optional[Dict[str, Any]] = None,
|
||||
custom_instructions: Optional[str] = None,
|
||||
) -> 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.
|
||||
schema: JSON Schema for the profile. Every property needs a
|
||||
``description``.
|
||||
custom_instructions: Extra guidance for the extraction step.
|
||||
|
||||
Returns:
|
||||
Dict with the settings as stored after the update.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the schema is not a valid profile schema.
|
||||
"""
|
||||
|
||||
payload = self._prepare_params(
|
||||
{"enabled": enabled, "schema": schema, "custom_instructions": custom_instructions}
|
||||
)
|
||||
response = self.client.post("/v2/profiles/settings/", 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) -> Dict[str, Any]:
|
||||
"""Generate profiles for a few real entities, to check a schema.
|
||||
|
||||
Real generations against real memories, and the results are kept.
|
||||
|
||||
Args:
|
||||
limit: How many entities to sample, 1-10. Defaults to the server value.
|
||||
|
||||
Returns:
|
||||
Dict containing ``sampled`` and one ``results`` row per entity.
|
||||
|
||||
Raises:
|
||||
ValidationError: If profiles are not enabled and configured.
|
||||
RateLimitError: If a sample run was already started very recently.
|
||||
"""
|
||||
|
||||
response = self.client.post("/v2/profiles/samples/", json=self._prepare_params({"limit": limit}))
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.sample_profiles", self, {"sync_type": "sync"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
def regenerate_profiles(self) -> Dict[str, Any]:
|
||||
"""Rebuild the profile of every entity in the current project.
|
||||
|
||||
This is how a new schema reaches entities that already have a profile.
|
||||
Returns as soon as the work is queued.
|
||||
|
||||
Returns:
|
||||
Dict containing ``status``, ``message``, ``project_id`` and
|
||||
``existing_profile_count``.
|
||||
|
||||
Raises:
|
||||
ValidationError: If profiles are not enabled and configured.
|
||||
RateLimitError: If a regenerate already ran for this project recently.
|
||||
"""
|
||||
|
||||
response = self.client.post("/v2/profiles/regenerate/", json={})
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.regenerate_profiles", self, {"sync_type": "sync"})
|
||||
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.
|
||||
@@ -1588,6 +1737,155 @@ 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, entity_type: str = "user") -> Dict[str, Any]:
|
||||
"""Get the memory profile for a single entity.
|
||||
|
||||
Branch on ``status``, not on an empty ``profile``: generation is
|
||||
asynchronous, so a known entity without a profile yet is a normal response.
|
||||
|
||||
Args:
|
||||
entity_id: The entity's id, as you supplied it on ``add`` (e.g. "alice").
|
||||
entity_type: Either "user" or "agent". Defaults to "user".
|
||||
|
||||
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:
|
||||
ValidationError: If entity_type is not a supported entity kind.
|
||||
AuthenticationError: If authentication fails.
|
||||
NotFoundError: If no such entity exists in the project.
|
||||
"""
|
||||
|
||||
response = await self.async_client.get(
|
||||
f"/v2/entities/{_encode_path_segment(entity_type)}/{_encode_path_segment(entity_id)}/profile/"
|
||||
)
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.get_profile", self, {"entity_type": entity_type, "sync_type": "async"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
async def generate_profile(self, entity_id: str, entity_type: str = "user") -> Dict[str, Any]:
|
||||
"""Generate or refresh the profile for a single entity, now.
|
||||
|
||||
Profiles are otherwise built once an entity crosses an internal message
|
||||
threshold, so a new entity 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 entity's id, as you supplied it on ``add``.
|
||||
entity_type: Either "user" or "agent". Defaults to "user".
|
||||
|
||||
Returns:
|
||||
Dict containing ``profile_id``, ``entity_type``, ``entity_id`` and
|
||||
``status``.
|
||||
|
||||
Raises:
|
||||
ValidationError: If entity_type is unsupported or profiles are not
|
||||
enabled and configured for the project.
|
||||
NotFoundError: If no such entity exists in the project.
|
||||
"""
|
||||
|
||||
response = await self.async_client.post(
|
||||
"/v2/profiles/trigger/",
|
||||
json={"entity_type": entity_type, "entity_id": entity_id},
|
||||
)
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.generate_profile", self, {"entity_type": entity_type, "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``, ``schema`` and ``custom_instructions``.
|
||||
"""
|
||||
|
||||
response = await self.async_client.get("/v2/profiles/settings/")
|
||||
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: Optional[Dict[str, Any]] = None,
|
||||
custom_instructions: Optional[str] = None,
|
||||
) -> 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.
|
||||
schema: JSON Schema for the profile. Every property needs a
|
||||
``description``.
|
||||
custom_instructions: Extra guidance for the extraction step.
|
||||
|
||||
Returns:
|
||||
Dict with the settings as stored after the update.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the schema is not a valid profile schema.
|
||||
"""
|
||||
|
||||
payload = self._prepare_params(
|
||||
{"enabled": enabled, "schema": schema, "custom_instructions": custom_instructions}
|
||||
)
|
||||
response = await self.async_client.post("/v2/profiles/settings/", 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) -> Dict[str, Any]:
|
||||
"""Generate profiles for a few real entities, to check a schema.
|
||||
|
||||
Real generations against real memories, and the results are kept.
|
||||
|
||||
Args:
|
||||
limit: How many entities to sample, 1-10. Defaults to the server value.
|
||||
|
||||
Returns:
|
||||
Dict containing ``sampled`` and one ``results`` row per entity.
|
||||
|
||||
Raises:
|
||||
ValidationError: If profiles are not enabled and configured.
|
||||
RateLimitError: If a sample run was already started very recently.
|
||||
"""
|
||||
|
||||
response = await self.async_client.post("/v2/profiles/samples/", json=self._prepare_params({"limit": limit}))
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.sample_profiles", self, {"sync_type": "async"})
|
||||
return response.json()
|
||||
|
||||
@api_error_handler
|
||||
async def regenerate_profiles(self) -> Dict[str, Any]:
|
||||
"""Rebuild the profile of every entity in the current project.
|
||||
|
||||
This is how a new schema reaches entities that already have a profile.
|
||||
Returns as soon as the work is queued.
|
||||
|
||||
Returns:
|
||||
Dict containing ``status``, ``message``, ``project_id`` and
|
||||
``existing_profile_count``.
|
||||
|
||||
Raises:
|
||||
ValidationError: If profiles are not enabled and configured.
|
||||
RateLimitError: If a regenerate already ran for this project recently.
|
||||
"""
|
||||
|
||||
response = await self.async_client.post("/v2/profiles/regenerate/", json={})
|
||||
response.raise_for_status()
|
||||
capture_client_event("client.regenerate_profiles", self, {"sync_type": "async"})
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
"""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 _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_supports_agents(self, mock_memory_client):
|
||||
mock_memory_client.client.get.return_value = _mock_response({"profile": {}, "status": "pending"})
|
||||
|
||||
mock_memory_client.get_profile("support-bot", entity_type="agent")
|
||||
|
||||
mock_memory_client.client.get.assert_called_once_with("/v2/entities/agent/support-bot/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({"profile_id": "p_1", "status": "PENDING"})
|
||||
|
||||
mock_memory_client.generate_profile("alice")
|
||||
|
||||
mock_memory_client.client.post.assert_called_once_with(
|
||||
"/v2/profiles/trigger/",
|
||||
json={"entity_type": "user", "entity_id": "alice"},
|
||||
)
|
||||
|
||||
def test_agent_entity_type(self, mock_memory_client):
|
||||
mock_memory_client.client.post.return_value = _mock_response({"profile_id": "p_2", "status": "PENDING"})
|
||||
|
||||
mock_memory_client.generate_profile("support-bot", entity_type="agent")
|
||||
|
||||
mock_memory_client.client.post.assert_called_once_with(
|
||||
"/v2/profiles/trigger/",
|
||||
json={"entity_type": "agent", "entity_id": "support-bot"},
|
||||
)
|
||||
|
||||
|
||||
class TestProfileSettings:
|
||||
def test_get_reads_v2(self, mock_memory_client):
|
||||
mock_memory_client.client.get.return_value = _mock_response(
|
||||
{"enabled": True, "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_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, "schema": schema})
|
||||
|
||||
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, "schema": schema, "custom_instructions": "Keep it durable"},
|
||||
)
|
||||
|
||||
|
||||
class TestSampleAndRegenerate:
|
||||
def test_sample_without_limit(self, mock_memory_client):
|
||||
mock_memory_client.client.post.return_value = _mock_response({"sampled": 5, "results": []})
|
||||
|
||||
mock_memory_client.sample_profiles()
|
||||
|
||||
mock_memory_client.client.post.assert_called_once_with("/v2/profiles/samples/", json={})
|
||||
|
||||
def test_sample_with_limit(self, mock_memory_client):
|
||||
mock_memory_client.client.post.return_value = _mock_response({"sampled": 3, "results": []})
|
||||
|
||||
mock_memory_client.sample_profiles(limit=3)
|
||||
|
||||
mock_memory_client.client.post.assert_called_once_with("/v2/profiles/samples/", json={"limit": 3})
|
||||
|
||||
def test_regenerate(self, mock_memory_client):
|
||||
mock_memory_client.client.post.return_value = _mock_response(
|
||||
{"status": "accepted", "project_id": "proj_abc", "existing_profile_count": 12}
|
||||
)
|
||||
|
||||
mock_memory_client.regenerate_profiles()
|
||||
|
||||
mock_memory_client.client.post.assert_called_once_with("/v2/profiles/regenerate/", json={})
|
||||
|
||||
|
||||
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({"profile_id": "p_1"}))
|
||||
|
||||
asyncio.run(async_client.generate_profile("alice", entity_type="agent"))
|
||||
|
||||
async_client.async_client.post.assert_called_once_with(
|
||||
"/v2/profiles/trigger/",
|
||||
json={"entity_type": "agent", "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_regenerate(self, async_client):
|
||||
async_client.async_client.post = AsyncMock(return_value=_mock_response({"status": "accepted"}))
|
||||
|
||||
asyncio.run(async_client.regenerate_profiles())
|
||||
|
||||
async_client.async_client.post.assert_called_once_with("/v2/profiles/regenerate/", json={})
|
||||
Reference in New Issue
Block a user