feat(profiles): user-only SDK for v1 (drop regenerate + agent)
Reduce the User Profiles SDK to the shipped v1 scope: user profiles only,
no full rebuild.
- Remove regenerate_profiles / regenerateProfiles (sync + async, Python + TS)
and the ProfileRegenerateResponse type. Full rebuild is gated off server-side
(409 not_yet_available).
- Drop the entity_type / entityType parameter from get_profile,
generate_profile, sample_profiles, and update_profile_settings; every path
and payload is scoped to "user". Narrow ProfileEntityType to "user".
- Update tests to the user-only surface.
Endpoint paths (/v2/profiles/jobs/, /v2/profiles/settings/,
/v2/entities/user/{id}/profile/) and the per-job Idempotency-Key are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
949b026991
commit
48ae36ad94
@@ -32,7 +32,6 @@ export type {
|
||||
EntityProfileSettings,
|
||||
ProfileSampleResult,
|
||||
ProfileSamplesResponse,
|
||||
ProfileRegenerateResponse,
|
||||
} from "./mem0.types";
|
||||
|
||||
// Re-export enums as values (not type-only)
|
||||
|
||||
+15
-56
@@ -28,7 +28,6 @@ import {
|
||||
ProfileSettings,
|
||||
ProfileSettingsResponse,
|
||||
ProfileSamplesResponse,
|
||||
ProfileRegenerateResponse,
|
||||
} from "./mem0.types";
|
||||
import {
|
||||
captureClientEvent,
|
||||
@@ -772,21 +771,17 @@ export default class MemoryClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the memory profile for a single entity.
|
||||
* Get the memory profile for a single user.
|
||||
*
|
||||
* Branch on `status`, not on an empty `profile`: generation is asynchronous,
|
||||
* so a known entity without a profile yet is a normal response.
|
||||
* so a known user without a profile yet is a normal response.
|
||||
*/
|
||||
async getProfile(data: {
|
||||
entityId: string;
|
||||
entityType?: ProfileEntityType;
|
||||
}): Promise<ProfileResponse> {
|
||||
async getProfile(data: { entityId: string }): 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/`,
|
||||
`${this.host}/v2/entities/user/${encodeURIComponent(data.entityId)}/profile/`,
|
||||
{
|
||||
headers: this.headers,
|
||||
},
|
||||
@@ -795,15 +790,14 @@ export default class MemoryClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate or refresh the profile for one entity, now.
|
||||
* Generate or refresh the profile for one user, 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
|
||||
* Profiles are otherwise built once a user crosses an internal message
|
||||
* threshold, so a new user has none for its first few memories. Returns as
|
||||
* soon as the work is queued: poll {@link getProfile} and branch on `status`.
|
||||
*/
|
||||
async generateProfile(data: {
|
||||
entityId: string;
|
||||
entityType?: ProfileEntityType;
|
||||
}): Promise<ProfileTriggerResponse> {
|
||||
this._captureEvent("generate_profile", []);
|
||||
await this._awaitIdentity();
|
||||
@@ -815,7 +809,7 @@ export default class MemoryClient {
|
||||
headers: { ...this.headers, "Idempotency-Key": crypto.randomUUID() },
|
||||
body: JSON.stringify({
|
||||
operation: "trigger",
|
||||
entity_type: data.entityType ?? "user",
|
||||
entity_type: "user",
|
||||
entity_id: data.entityId,
|
||||
}),
|
||||
},
|
||||
@@ -873,7 +867,7 @@ export default class MemoryClient {
|
||||
/**
|
||||
* Update profile settings. Only the fields you pass are written.
|
||||
*
|
||||
* `schema` and `customInstructions` are per entity type and are nested under
|
||||
* `schema` and `customInstructions` are per user and are nested under
|
||||
* `entities` for the API; only `enabled` is project-wide. Sending them flat
|
||||
* is rejected with `Unsupported settings`.
|
||||
*/
|
||||
@@ -884,12 +878,7 @@ export default class MemoryClient {
|
||||
this._captureEvent("update_profile_settings", [payloadKeys]);
|
||||
await this._awaitIdentity();
|
||||
|
||||
const {
|
||||
schema,
|
||||
customInstructions,
|
||||
enabled,
|
||||
entityType = "user",
|
||||
} = settings || {};
|
||||
const { schema, customInstructions, enabled } = settings || {};
|
||||
|
||||
const body: Record<string, any> = {};
|
||||
if (enabled !== undefined) {
|
||||
@@ -905,7 +894,7 @@ export default class MemoryClient {
|
||||
entitySettings.custom_instructions = customInstructions;
|
||||
}
|
||||
if (Object.keys(entitySettings).length > 0) {
|
||||
body.entities = { [entityType]: entitySettings };
|
||||
body.entities = { user: entitySettings };
|
||||
}
|
||||
|
||||
const raw = await this._fetchRawJson(`${this.host}/v2/profiles/settings/`, {
|
||||
@@ -917,15 +906,12 @@ export default class MemoryClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate profiles for a few real entities, to check a schema.
|
||||
* Generate profiles for a few real users, to check a schema.
|
||||
*
|
||||
* Real generations against real memories, and the results are kept: the
|
||||
* profiles are written to those entities and count toward usage.
|
||||
* profiles are written to those users and count toward usage.
|
||||
*/
|
||||
async sampleProfiles(data?: {
|
||||
limit?: number;
|
||||
entityType?: ProfileEntityType;
|
||||
}): Promise<ProfileJobResponse> {
|
||||
async sampleProfiles(data?: { limit?: number }): Promise<ProfileJobResponse> {
|
||||
this._captureEvent("sample_profiles", []);
|
||||
await this._awaitIdentity();
|
||||
|
||||
@@ -937,7 +923,7 @@ export default class MemoryClient {
|
||||
body: JSON.stringify({
|
||||
operation: "sample",
|
||||
// Required: the API refuses a job that does not name an entity kind.
|
||||
entity_type: data?.entityType ?? "user",
|
||||
entity_type: "user",
|
||||
...this._prepareParams({ limit: data?.limit }),
|
||||
}),
|
||||
},
|
||||
@@ -945,33 +931,6 @@ export default class MemoryClient {
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the profile of every entity of one kind in the project.
|
||||
*
|
||||
* Not available yet: the server answers 409 `not_yet_available` and creates
|
||||
* nothing. Use {@link sampleProfiles} or {@link generateProfile} until
|
||||
* `capabilities.full_rebuild` from {@link getProfileSettings} is true.
|
||||
*/
|
||||
async regenerateProfiles(data?: {
|
||||
entityType?: ProfileEntityType;
|
||||
}): Promise<ProfileJobResponse> {
|
||||
this._captureEvent("regenerate_profiles", []);
|
||||
await this._awaitIdentity();
|
||||
|
||||
const response = await this._fetchWithErrorHandling(
|
||||
`${this.host}${PROFILE_JOBS_PATH}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { ...this.headers, "Idempotency-Key": crypto.randomUUID() },
|
||||
body: JSON.stringify({
|
||||
operation: "regenerate",
|
||||
entity_type: data?.entityType ?? "user",
|
||||
}),
|
||||
},
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read one generation job. Accepts the `statusUrl` from a create call, or a
|
||||
* bare job id. Prefer `statusUrl` so a route change needs no client update.
|
||||
|
||||
@@ -239,8 +239,8 @@ export interface GetMemoryExportPayload {
|
||||
|
||||
// ─── Profile Types ──────────────────────────────────────────
|
||||
|
||||
/** Entity kinds that can carry a profile. */
|
||||
export type ProfileEntityType = "user" | "agent";
|
||||
/** The entity kind that carries a profile. */
|
||||
export type ProfileEntityType = "user";
|
||||
|
||||
/**
|
||||
* `succeeded` is the only state in which `profile` is guaranteed to hold content.
|
||||
@@ -249,7 +249,11 @@ export type ProfileEntityType = "user" | "agent";
|
||||
* spelling is what a comparison has to match.
|
||||
*/
|
||||
export type ProfileStatus =
|
||||
"succeeded" | "pending" | "failed" | "not_enabled" | "insufficient_data";
|
||||
| "succeeded"
|
||||
| "pending"
|
||||
| "failed"
|
||||
| "not_enabled"
|
||||
| "insufficient_data";
|
||||
|
||||
export interface ProfileResponse {
|
||||
/** Shaped by the project's schema; keys are not camel-cased. */
|
||||
@@ -282,15 +286,13 @@ export interface ProfileJobResponse {
|
||||
/** @deprecated Use {@link ProfileJobResponse}. */
|
||||
export type ProfileTriggerResponse = ProfileJobResponse;
|
||||
|
||||
/** The settings to write. `schema` and `customInstructions` apply to one entity type. */
|
||||
/** The settings to write. `schema` and `customInstructions` apply to user profiles. */
|
||||
export interface ProfileSettings {
|
||||
/** Turn profile generation on or off. Project-wide. */
|
||||
enabled?: boolean;
|
||||
/** JSON Schema for the profile. Every property needs a `description`. */
|
||||
schema?: Record<string, any> | null;
|
||||
customInstructions?: string | null;
|
||||
/** Which entity kind `schema` and `customInstructions` belong to. Defaults to "user". */
|
||||
entityType?: ProfileEntityType;
|
||||
}
|
||||
|
||||
/** One entity type's stored configuration. */
|
||||
@@ -322,9 +324,6 @@ export interface ProfileSampleResult {
|
||||
/** @deprecated Use {@link ProfileJobResponse}. */
|
||||
export type ProfileSamplesResponse = ProfileJobResponse;
|
||||
|
||||
/** @deprecated Use {@link ProfileJobResponse}. */
|
||||
export type ProfileRegenerateResponse = ProfileJobResponse;
|
||||
|
||||
/** `GET /v2/profiles/jobs/{id}/`. The job nests under `job`. */
|
||||
export interface ProfileJobStatus {
|
||||
job: {
|
||||
|
||||
@@ -76,18 +76,18 @@ describe("MemoryClient - getProfile()", () => {
|
||||
expect(status).not.toBe("insufficientData");
|
||||
});
|
||||
|
||||
test("defaults to user and encodes the entity id", async () => {
|
||||
test("scopes to user and encodes the entity id", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/entities/agent/", {
|
||||
extra.set("/v2/entities/user/", {
|
||||
status: 200,
|
||||
body: { profile: {}, status: "pending", entity_type: "agent" },
|
||||
body: { profile: {}, status: "pending", entity_type: "user" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.getProfile({ entityId: "a/b", entityType: "agent" });
|
||||
await client.getProfile({ entityId: "a/b" });
|
||||
|
||||
const call = findFetchCall(mock, "/v2/entities/agent/a%2Fb/profile/");
|
||||
const call = findFetchCall(mock, "/v2/entities/user/a%2Fb/profile/");
|
||||
expect(call).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -179,7 +179,7 @@ describe("MemoryClient - profile settings", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("targets the entity type the caller named", async () => {
|
||||
test("scopes entity-level settings under user", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/settings/", {
|
||||
status: 200,
|
||||
@@ -190,13 +190,12 @@ describe("MemoryClient - profile settings", () => {
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.updateProfileSettings({
|
||||
schema: { type: "object", properties: {} },
|
||||
entityType: "agent",
|
||||
});
|
||||
|
||||
const body = getFetchBody(
|
||||
findFetchCall(mock, "/v2/profiles/settings/", "POST")!,
|
||||
);
|
||||
expect(Object.keys(body.entities)).toEqual(["agent"]);
|
||||
expect(Object.keys(body.entities)).toEqual(["user"]);
|
||||
});
|
||||
|
||||
test("omits fields the caller did not set", async () => {
|
||||
@@ -251,7 +250,7 @@ describe("MemoryClient - profile settings", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryClient - sampleProfiles() / regenerateProfiles()", () => {
|
||||
describe("MemoryClient - sampleProfiles()", () => {
|
||||
test("sampleProfiles omits limit when unset", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/jobs/", {
|
||||
@@ -304,46 +303,4 @@ describe("MemoryClient - sampleProfiles() / regenerateProfiles()", () => {
|
||||
expect(getFetchBody(call!).entity_type).toBe("user");
|
||||
expect(result.sampled).toBe(3);
|
||||
});
|
||||
|
||||
test("sampleProfiles targets the entity type the caller named", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/jobs/", {
|
||||
status: 202,
|
||||
body: { job_id: "job_4", status: "QUEUED", entity_type: "agent" },
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
await client.sampleProfiles({ entityType: "agent" });
|
||||
|
||||
const call = findFetchCall(mock, "/v2/profiles/jobs/", "POST");
|
||||
expect(getFetchBody(call!).entity_type).toBe("agent");
|
||||
});
|
||||
|
||||
test("sends operation regenerate to the jobs collection", async () => {
|
||||
const extra = new Map<string, { status: number; body: unknown }>();
|
||||
extra.set("/v2/profiles/jobs/", {
|
||||
status: 202,
|
||||
body: {
|
||||
job_id: "job_3",
|
||||
status: "QUEUED",
|
||||
status_url: "/v2/profiles/jobs/job_3/",
|
||||
operation: "regenerate",
|
||||
entity_type: "user",
|
||||
},
|
||||
});
|
||||
const mock = setupMockFetch(extra);
|
||||
|
||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||
const result = await client.regenerateProfiles();
|
||||
|
||||
const call = findFetchCall(mock, "/v2/profiles/jobs/", "POST");
|
||||
expect(call).toBeDefined();
|
||||
expect(getFetchBody(call!)).toEqual({
|
||||
operation: "regenerate",
|
||||
entity_type: "user",
|
||||
});
|
||||
expect(result.jobId).toBe("job_3");
|
||||
expect(result.statusUrl).toBe("/v2/profiles/jobs/job_3/");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user