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:
karthik
2026-09-23 19:50:48 +05:30
parent 949b026991
commit 48ae36ad94
6 changed files with 93 additions and 297 deletions
-1
View File
@@ -32,7 +32,6 @@ export type {
EntityProfileSettings,
ProfileSampleResult,
ProfileSamplesResponse,
ProfileRegenerateResponse,
} from "./mem0.types";
// Re-export enums as values (not type-only)
+15 -56
View File
@@ -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.
+8 -9
View File
@@ -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/");
});
});
+56 -133
View File
@@ -49,11 +49,10 @@ def _profile_settings_payload(
enabled: Optional[bool],
schema: Optional[Dict[str, Any]],
custom_instructions: Optional[str],
entity_type: str,
) -> Dict[str, Any]:
"""Build the settings body the API accepts.
``schema`` and ``custom_instructions`` are per entity type and nest under
``schema`` and ``custom_instructions`` are per user and nest under
``entities``; only ``enabled`` is project-wide. This mirrors what
``get_profile_settings`` returns, so the two round-trip.
@@ -72,7 +71,7 @@ def _profile_settings_payload(
entity_settings["custom_instructions"] = custom_instructions
if entity_settings:
payload["entities"] = {entity_type: entity_settings}
payload["entities"] = {"user": entity_settings}
return payload
@@ -719,15 +718,14 @@ class MemoryClient:
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.
def get_profile(self, entity_id: str) -> Dict[str, Any]:
"""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.
asynchronous, so a known user 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".
entity_id: The user's id, as you supplied it on ``add`` (e.g. "alice").
Returns:
Dict with ``profile``, ``status``, ``entity_type``, ``entity_id``,
@@ -735,47 +733,43 @@ class MemoryClient:
"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.
NotFoundError: If no such user exists in the project.
"""
response = self.client.get(
f"/v2/entities/{_encode_path_segment(entity_type)}/{_encode_path_segment(entity_id)}/profile/"
)
response = self.client.get(f"/v2/entities/user/{_encode_path_segment(entity_id)}/profile/")
response.raise_for_status()
capture_client_event("client.get_profile", self, {"entity_type": entity_type, "sync_type": "sync"})
capture_client_event("client.get_profile", self, {"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.
def generate_profile(self, entity_id: str) -> Dict[str, Any]:
"""Generate or refresh the profile for a single 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 :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".
entity_id: The user's id, as you supplied it on ``add``.
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.
ValidationError: If profiles are not enabled and configured for the
project.
NotFoundError: If no such user exists in the project.
"""
response = self.client.post(
PROFILE_JOBS_PATH,
json={"operation": "trigger", "entity_type": entity_type, "entity_id": entity_id},
json={"operation": "trigger", "entity_type": "user", "entity_id": entity_id},
headers={"Idempotency-Key": uuid.uuid4().hex},
)
response.raise_for_status()
capture_client_event("client.generate_profile", self, {"entity_type": entity_type, "sync_type": "sync"})
capture_client_event("client.generate_profile", self, {"sync_type": "sync"})
return response.json()
@api_error_handler
@@ -799,7 +793,6 @@ class MemoryClient:
enabled: Optional[bool] = None,
schema: Optional[Dict[str, Any]] = None,
custom_instructions: Optional[str] = None,
entity_type: str = "user",
) -> Dict[str, Any]:
"""Update the profile settings for the current project.
@@ -808,11 +801,9 @@ class MemoryClient:
Args:
enabled: Turn profile generation on or off. Project-wide.
schema: JSON Schema for the profile. Every property needs a
``description``. Applies to ``entity_type``.
``description``. Applies to user profiles.
custom_instructions: Extra guidance for the extraction step.
Applies to ``entity_type``.
entity_type: Which entity kind ``schema`` and
``custom_instructions`` belong to. Defaults to "user".
Applies to user profiles.
Returns:
Dict with the settings as stored after the update, in the same
@@ -822,26 +813,25 @@ class MemoryClient:
ValidationError: If the schema is not a valid profile schema.
"""
payload = _profile_settings_payload(enabled, schema, custom_instructions, entity_type)
payload = _profile_settings_payload(enabled, schema, custom_instructions)
response = self.client.post(PROFILE_SETTINGS_PATH, json=payload)
response.raise_for_status()
capture_client_event(
"client.update_profile_settings",
self,
{"keys": list(payload.keys()), "entity_type": entity_type, "sync_type": "sync"},
{"keys": list(payload.keys()), "sync_type": "sync"},
)
return response.json()
@api_error_handler
def sample_profiles(self, limit: Optional[int] = None, entity_type: str = "user") -> Dict[str, Any]:
"""Generate profiles for a few real entities, to check a schema.
def sample_profiles(self, limit: Optional[int] = None) -> Dict[str, Any]:
"""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.
Args:
limit: How many entities to sample, 1-10. Defaults to the server value.
entity_type: Which entity kind to sample. Defaults to "user".
limit: How many users to sample, 1-10. Defaults to the server value.
Returns:
Dict containing ``job_id``, ``status``, ``status_url``, ``sampled``
@@ -855,42 +845,14 @@ class MemoryClient:
payload = self._prepare_params({"limit": limit})
payload["operation"] = "sample"
payload["entity_type"] = entity_type
payload["entity_type"] = "user"
response = self.client.post(
PROFILE_JOBS_PATH,
json=payload,
headers={"Idempotency-Key": uuid.uuid4().hex},
)
response.raise_for_status()
capture_client_event("client.sample_profiles", self, {"entity_type": entity_type, "sync_type": "sync"})
return response.json()
@api_error_handler
def regenerate_profiles(self, entity_type: str = "user") -> Dict[str, Any]:
"""Rebuild the profile of every entity of one kind in the project.
Not available yet: the server answers 409 with ``not_yet_available`` and
creates nothing. Use :meth:`sample_profiles` or :meth:`generate_profile`
until ``capabilities.full_rebuild`` in :meth:`get_profile_settings` is true.
Args:
entity_type: Which entity kind to rebuild. Defaults to "user".
Returns:
Dict containing ``job_id``, ``status`` and ``status_url``.
Raises:
ValidationError: If profiles are not enabled and configured.
RateLimitError: If a regenerate already ran for this project recently.
"""
response = self.client.post(
PROFILE_JOBS_PATH,
json={"operation": "regenerate", "entity_type": entity_type},
headers={"Idempotency-Key": uuid.uuid4().hex},
)
response.raise_for_status()
capture_client_event("client.regenerate_profiles", self, {"entity_type": entity_type, "sync_type": "sync"})
capture_client_event("client.sample_profiles", self, {"sync_type": "sync"})
return response.json()
@api_error_handler
@@ -1820,15 +1782,14 @@ class AsyncMemoryClient:
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.
async def get_profile(self, entity_id: str) -> Dict[str, Any]:
"""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.
asynchronous, so a known user 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".
entity_id: The user's id, as you supplied it on ``add`` (e.g. "alice").
Returns:
Dict with ``profile``, ``status``, ``entity_type``, ``entity_id``,
@@ -1836,47 +1797,43 @@ class AsyncMemoryClient:
"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.
NotFoundError: If no such user 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 = await self.async_client.get(f"/v2/entities/user/{_encode_path_segment(entity_id)}/profile/")
response.raise_for_status()
capture_client_event("client.get_profile", self, {"entity_type": entity_type, "sync_type": "async"})
capture_client_event("client.get_profile", self, {"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.
async def generate_profile(self, entity_id: str) -> Dict[str, Any]:
"""Generate or refresh the profile for a single 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 :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".
entity_id: The user's id, as you supplied it on ``add``.
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.
ValidationError: If profiles are not enabled and configured for the
project.
NotFoundError: If no such user exists in the project.
"""
response = await self.async_client.post(
PROFILE_JOBS_PATH,
json={"operation": "trigger", "entity_type": entity_type, "entity_id": entity_id},
json={"operation": "trigger", "entity_type": "user", "entity_id": entity_id},
headers={"Idempotency-Key": uuid.uuid4().hex},
)
response.raise_for_status()
capture_client_event("client.generate_profile", self, {"entity_type": entity_type, "sync_type": "async"})
capture_client_event("client.generate_profile", self, {"sync_type": "async"})
return response.json()
@api_error_handler
@@ -1900,7 +1857,6 @@ class AsyncMemoryClient:
enabled: Optional[bool] = None,
schema: Optional[Dict[str, Any]] = None,
custom_instructions: Optional[str] = None,
entity_type: str = "user",
) -> Dict[str, Any]:
"""Update the profile settings for the current project.
@@ -1909,11 +1865,9 @@ class AsyncMemoryClient:
Args:
enabled: Turn profile generation on or off. Project-wide.
schema: JSON Schema for the profile. Every property needs a
``description``. Applies to ``entity_type``.
``description``. Applies to user profiles.
custom_instructions: Extra guidance for the extraction step.
Applies to ``entity_type``.
entity_type: Which entity kind ``schema`` and
``custom_instructions`` belong to. Defaults to "user".
Applies to user profiles.
Returns:
Dict with the settings as stored after the update, in the same
@@ -1923,26 +1877,25 @@ class AsyncMemoryClient:
ValidationError: If the schema is not a valid profile schema.
"""
payload = _profile_settings_payload(enabled, schema, custom_instructions, entity_type)
payload = _profile_settings_payload(enabled, schema, custom_instructions)
response = await self.async_client.post(PROFILE_SETTINGS_PATH, json=payload)
response.raise_for_status()
capture_client_event(
"client.update_profile_settings",
self,
{"keys": list(payload.keys()), "entity_type": entity_type, "sync_type": "async"},
{"keys": list(payload.keys()), "sync_type": "async"},
)
return response.json()
@api_error_handler
async def sample_profiles(self, limit: Optional[int] = None, entity_type: str = "user") -> Dict[str, Any]:
"""Generate profiles for a few real entities, to check a schema.
async def sample_profiles(self, limit: Optional[int] = None) -> Dict[str, Any]:
"""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.
Args:
limit: How many entities to sample, 1-10. Defaults to the server value.
entity_type: Which entity kind to sample. Defaults to "user".
limit: How many users to sample, 1-10. Defaults to the server value.
Returns:
Dict containing ``job_id``, ``status``, ``status_url``, ``sampled``
@@ -1956,44 +1909,14 @@ class AsyncMemoryClient:
payload = self._prepare_params({"limit": limit})
payload["operation"] = "sample"
payload["entity_type"] = entity_type
payload["entity_type"] = "user"
response = await self.async_client.post(
PROFILE_JOBS_PATH,
json=payload,
headers={"Idempotency-Key": uuid.uuid4().hex},
)
response.raise_for_status()
capture_client_event("client.sample_profiles", self, {"entity_type": entity_type, "sync_type": "async"})
return response.json()
@api_error_handler
async def regenerate_profiles(self, entity_type: str = "user") -> Dict[str, Any]:
"""Rebuild the profile of every entity of one kind in the project.
Not available yet: the server answers 409 with ``not_yet_available`` and
creates nothing. Use :meth:`sample_profiles` or :meth:`generate_profile`
until ``capabilities.full_rebuild`` in :meth:`get_profile_settings` is true.
Args:
entity_type: Which entity kind to rebuild. Defaults to "user".
Returns:
Dict containing ``job_id``, ``status`` and ``status_url``.
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(
PROFILE_JOBS_PATH,
json={"operation": "regenerate", "entity_type": entity_type},
headers={"Idempotency-Key": uuid.uuid4().hex},
)
response.raise_for_status()
capture_client_event(
"client.regenerate_profiles", self, {"entity_type": entity_type, "sync_type": "async"}
)
capture_client_event("client.sample_profiles", self, {"sync_type": "async"})
return response.json()
@api_error_handler
+6 -47
View File
@@ -57,13 +57,6 @@ class TestGetProfile:
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"})
@@ -98,16 +91,6 @@ class TestGenerateProfile:
{"operation": "trigger", "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")
_assert_job_call(
mock_memory_client.client.post,
{"operation": "trigger", "entity_type": "agent", "entity_id": "support-bot"},
)
class TestProfileSettings:
def test_get_reads_v2(self, mock_memory_client):
@@ -145,14 +128,14 @@ class TestProfileSettings:
assert set(kwargs["json"]) == {"enabled", "entities"}
assert "schema" not in kwargs["json"]
def test_update_targets_the_named_entity_type(self, mock_memory_client):
def test_update_targets_the_user_entity_type(self, mock_memory_client):
schema = {"type": "object", "properties": {"x": {"type": "string", "description": "d"}}}
mock_memory_client.client.post.return_value = _mock_response({"enabled": True})
mock_memory_client.update_profile_settings(schema=schema, entity_type="agent")
mock_memory_client.update_profile_settings(schema=schema)
_, kwargs = mock_memory_client.client.post.call_args
assert kwargs["json"] == {"entities": {"agent": {"schema": schema}}}
assert kwargs["json"] == {"entities": {"user": {"schema": schema}}}
def test_update_passes_schema_verbatim(self, mock_memory_client):
schema = {
@@ -178,7 +161,7 @@ class TestProfileSettings:
)
class TestSampleAndRegenerate:
class TestSampleProfiles:
def test_sample_without_limit(self, mock_memory_client):
mock_memory_client.client.post.return_value = _mock_response({"sampled": 5, "entity_ids": []})
@@ -196,23 +179,6 @@ class TestSampleAndRegenerate:
{"operation": "sample", "limit": 3, "entity_type": "user"},
)
def test_sample_names_the_entity_type(self, mock_memory_client):
"""Every job names an entity kind: the API refuses one that does not."""
mock_memory_client.client.post.return_value = _mock_response({"sampled": 1, "entity_ids": []})
mock_memory_client.sample_profiles(entity_type="agent")
_assert_job_call(mock_memory_client.client.post, {"operation": "sample", "entity_type": "agent"})
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()
_assert_job_call(mock_memory_client.client.post, {"operation": "regenerate", "entity_type": "user"})
class TestAsyncClientParity:
"""The async client must speak the same wire protocol as the sync one."""
@@ -246,11 +212,11 @@ class TestAsyncClientParity:
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"))
asyncio.run(async_client.generate_profile("alice"))
_assert_job_call(
async_client.async_client.post,
{"operation": "trigger", "entity_type": "agent", "entity_id": "alice"},
{"operation": "trigger", "entity_type": "user", "entity_id": "alice"},
)
def test_update_settings_partial(self, async_client):
@@ -274,10 +240,3 @@ class TestAsyncClientParity:
"/v2/profiles/settings/",
json={"enabled": True, "entities": {"user": {"schema": schema}}},
)
def test_regenerate(self, async_client):
async_client.async_client.post = AsyncMock(return_value=_mock_response({"status": "accepted"}))
asyncio.run(async_client.regenerate_profiles())
_assert_job_call(async_client.async_client.post, {"operation": "regenerate", "entity_type": "user"})