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
+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 }> {