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:
+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 }> {
|
||||
|
||||
Reference in New Issue
Block a user