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:
Pratik
2026-09-10 16:04:09 -07:00
parent d873892dad
commit 623e33f2db
17 changed files with 1668 additions and 4 deletions
@@ -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/
---
+13
View File
@@ -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",
+7
View File
@@ -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
View File
@@ -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"
}
}
+272
View File
@@ -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>
+8
View File
@@ -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
View File
@@ -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 }> {
+54
View File
@@ -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");
});
});
+3
View File
@@ -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",
]);
/**
+298
View File
@@ -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.
+224
View File
@@ -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={})