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,
|
EntityProfileSettings,
|
||||||
ProfileSampleResult,
|
ProfileSampleResult,
|
||||||
ProfileSamplesResponse,
|
ProfileSamplesResponse,
|
||||||
ProfileRegenerateResponse,
|
|
||||||
} from "./mem0.types";
|
} from "./mem0.types";
|
||||||
|
|
||||||
// Re-export enums as values (not type-only)
|
// Re-export enums as values (not type-only)
|
||||||
|
|||||||
+15
-56
@@ -28,7 +28,6 @@ import {
|
|||||||
ProfileSettings,
|
ProfileSettings,
|
||||||
ProfileSettingsResponse,
|
ProfileSettingsResponse,
|
||||||
ProfileSamplesResponse,
|
ProfileSamplesResponse,
|
||||||
ProfileRegenerateResponse,
|
|
||||||
} from "./mem0.types";
|
} from "./mem0.types";
|
||||||
import {
|
import {
|
||||||
captureClientEvent,
|
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,
|
* 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: {
|
async getProfile(data: { entityId: string }): Promise<ProfileResponse> {
|
||||||
entityId: string;
|
|
||||||
entityType?: ProfileEntityType;
|
|
||||||
}): Promise<ProfileResponse> {
|
|
||||||
this._captureEvent("get_profile", []);
|
this._captureEvent("get_profile", []);
|
||||||
await this._awaitIdentity();
|
await this._awaitIdentity();
|
||||||
|
|
||||||
const entityType = data.entityType ?? "user";
|
|
||||||
const response = await this._fetchWithErrorHandling(
|
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,
|
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
|
* Profiles are otherwise built once a user crosses an internal message
|
||||||
* threshold, so a new entity has none for its first few memories. Returns as
|
* 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`.
|
* soon as the work is queued: poll {@link getProfile} and branch on `status`.
|
||||||
*/
|
*/
|
||||||
async generateProfile(data: {
|
async generateProfile(data: {
|
||||||
entityId: string;
|
entityId: string;
|
||||||
entityType?: ProfileEntityType;
|
|
||||||
}): Promise<ProfileTriggerResponse> {
|
}): Promise<ProfileTriggerResponse> {
|
||||||
this._captureEvent("generate_profile", []);
|
this._captureEvent("generate_profile", []);
|
||||||
await this._awaitIdentity();
|
await this._awaitIdentity();
|
||||||
@@ -815,7 +809,7 @@ export default class MemoryClient {
|
|||||||
headers: { ...this.headers, "Idempotency-Key": crypto.randomUUID() },
|
headers: { ...this.headers, "Idempotency-Key": crypto.randomUUID() },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
operation: "trigger",
|
operation: "trigger",
|
||||||
entity_type: data.entityType ?? "user",
|
entity_type: "user",
|
||||||
entity_id: data.entityId,
|
entity_id: data.entityId,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -873,7 +867,7 @@ export default class MemoryClient {
|
|||||||
/**
|
/**
|
||||||
* Update profile settings. Only the fields you pass are written.
|
* 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
|
* `entities` for the API; only `enabled` is project-wide. Sending them flat
|
||||||
* is rejected with `Unsupported settings`.
|
* is rejected with `Unsupported settings`.
|
||||||
*/
|
*/
|
||||||
@@ -884,12 +878,7 @@ export default class MemoryClient {
|
|||||||
this._captureEvent("update_profile_settings", [payloadKeys]);
|
this._captureEvent("update_profile_settings", [payloadKeys]);
|
||||||
await this._awaitIdentity();
|
await this._awaitIdentity();
|
||||||
|
|
||||||
const {
|
const { schema, customInstructions, enabled } = settings || {};
|
||||||
schema,
|
|
||||||
customInstructions,
|
|
||||||
enabled,
|
|
||||||
entityType = "user",
|
|
||||||
} = settings || {};
|
|
||||||
|
|
||||||
const body: Record<string, any> = {};
|
const body: Record<string, any> = {};
|
||||||
if (enabled !== undefined) {
|
if (enabled !== undefined) {
|
||||||
@@ -905,7 +894,7 @@ export default class MemoryClient {
|
|||||||
entitySettings.custom_instructions = customInstructions;
|
entitySettings.custom_instructions = customInstructions;
|
||||||
}
|
}
|
||||||
if (Object.keys(entitySettings).length > 0) {
|
if (Object.keys(entitySettings).length > 0) {
|
||||||
body.entities = { [entityType]: entitySettings };
|
body.entities = { user: entitySettings };
|
||||||
}
|
}
|
||||||
|
|
||||||
const raw = await this._fetchRawJson(`${this.host}/v2/profiles/settings/`, {
|
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
|
* 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?: {
|
async sampleProfiles(data?: { limit?: number }): Promise<ProfileJobResponse> {
|
||||||
limit?: number;
|
|
||||||
entityType?: ProfileEntityType;
|
|
||||||
}): Promise<ProfileJobResponse> {
|
|
||||||
this._captureEvent("sample_profiles", []);
|
this._captureEvent("sample_profiles", []);
|
||||||
await this._awaitIdentity();
|
await this._awaitIdentity();
|
||||||
|
|
||||||
@@ -937,7 +923,7 @@ export default class MemoryClient {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
operation: "sample",
|
operation: "sample",
|
||||||
// Required: the API refuses a job that does not name an entity kind.
|
// 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 }),
|
...this._prepareParams({ limit: data?.limit }),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -945,33 +931,6 @@ export default class MemoryClient {
|
|||||||
return response;
|
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
|
* 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.
|
* bare job id. Prefer `statusUrl` so a route change needs no client update.
|
||||||
|
|||||||
@@ -239,8 +239,8 @@ export interface GetMemoryExportPayload {
|
|||||||
|
|
||||||
// ─── Profile Types ──────────────────────────────────────────
|
// ─── Profile Types ──────────────────────────────────────────
|
||||||
|
|
||||||
/** Entity kinds that can carry a profile. */
|
/** The entity kind that carries a profile. */
|
||||||
export type ProfileEntityType = "user" | "agent";
|
export type ProfileEntityType = "user";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `succeeded` is the only state in which `profile` is guaranteed to hold content.
|
* `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.
|
* spelling is what a comparison has to match.
|
||||||
*/
|
*/
|
||||||
export type ProfileStatus =
|
export type ProfileStatus =
|
||||||
"succeeded" | "pending" | "failed" | "not_enabled" | "insufficient_data";
|
| "succeeded"
|
||||||
|
| "pending"
|
||||||
|
| "failed"
|
||||||
|
| "not_enabled"
|
||||||
|
| "insufficient_data";
|
||||||
|
|
||||||
export interface ProfileResponse {
|
export interface ProfileResponse {
|
||||||
/** Shaped by the project's schema; keys are not camel-cased. */
|
/** Shaped by the project's schema; keys are not camel-cased. */
|
||||||
@@ -282,15 +286,13 @@ export interface ProfileJobResponse {
|
|||||||
/** @deprecated Use {@link ProfileJobResponse}. */
|
/** @deprecated Use {@link ProfileJobResponse}. */
|
||||||
export type ProfileTriggerResponse = 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 {
|
export interface ProfileSettings {
|
||||||
/** Turn profile generation on or off. Project-wide. */
|
/** Turn profile generation on or off. Project-wide. */
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
/** JSON Schema for the profile. Every property needs a `description`. */
|
/** JSON Schema for the profile. Every property needs a `description`. */
|
||||||
schema?: Record<string, any> | null;
|
schema?: Record<string, any> | null;
|
||||||
customInstructions?: string | null;
|
customInstructions?: string | null;
|
||||||
/** Which entity kind `schema` and `customInstructions` belong to. Defaults to "user". */
|
|
||||||
entityType?: ProfileEntityType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One entity type's stored configuration. */
|
/** One entity type's stored configuration. */
|
||||||
@@ -322,9 +324,6 @@ export interface ProfileSampleResult {
|
|||||||
/** @deprecated Use {@link ProfileJobResponse}. */
|
/** @deprecated Use {@link ProfileJobResponse}. */
|
||||||
export type ProfileSamplesResponse = ProfileJobResponse;
|
export type ProfileSamplesResponse = ProfileJobResponse;
|
||||||
|
|
||||||
/** @deprecated Use {@link ProfileJobResponse}. */
|
|
||||||
export type ProfileRegenerateResponse = ProfileJobResponse;
|
|
||||||
|
|
||||||
/** `GET /v2/profiles/jobs/{id}/`. The job nests under `job`. */
|
/** `GET /v2/profiles/jobs/{id}/`. The job nests under `job`. */
|
||||||
export interface ProfileJobStatus {
|
export interface ProfileJobStatus {
|
||||||
job: {
|
job: {
|
||||||
|
|||||||
@@ -76,18 +76,18 @@ describe("MemoryClient - getProfile()", () => {
|
|||||||
expect(status).not.toBe("insufficientData");
|
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 }>();
|
const extra = new Map<string, { status: number; body: unknown }>();
|
||||||
extra.set("/v2/entities/agent/", {
|
extra.set("/v2/entities/user/", {
|
||||||
status: 200,
|
status: 200,
|
||||||
body: { profile: {}, status: "pending", entity_type: "agent" },
|
body: { profile: {}, status: "pending", entity_type: "user" },
|
||||||
});
|
});
|
||||||
const mock = setupMockFetch(extra);
|
const mock = setupMockFetch(extra);
|
||||||
|
|
||||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
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();
|
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 }>();
|
const extra = new Map<string, { status: number; body: unknown }>();
|
||||||
extra.set("/v2/profiles/settings/", {
|
extra.set("/v2/profiles/settings/", {
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -190,13 +190,12 @@ describe("MemoryClient - profile settings", () => {
|
|||||||
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
const client = new MemoryClient({ apiKey: TEST_API_KEY });
|
||||||
await client.updateProfileSettings({
|
await client.updateProfileSettings({
|
||||||
schema: { type: "object", properties: {} },
|
schema: { type: "object", properties: {} },
|
||||||
entityType: "agent",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const body = getFetchBody(
|
const body = getFetchBody(
|
||||||
findFetchCall(mock, "/v2/profiles/settings/", "POST")!,
|
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 () => {
|
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 () => {
|
test("sampleProfiles omits limit when unset", async () => {
|
||||||
const extra = new Map<string, { status: number; body: unknown }>();
|
const extra = new Map<string, { status: number; body: unknown }>();
|
||||||
extra.set("/v2/profiles/jobs/", {
|
extra.set("/v2/profiles/jobs/", {
|
||||||
@@ -304,46 +303,4 @@ describe("MemoryClient - sampleProfiles() / regenerateProfiles()", () => {
|
|||||||
expect(getFetchBody(call!).entity_type).toBe("user");
|
expect(getFetchBody(call!).entity_type).toBe("user");
|
||||||
expect(result.sampled).toBe(3);
|
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
@@ -49,11 +49,10 @@ def _profile_settings_payload(
|
|||||||
enabled: Optional[bool],
|
enabled: Optional[bool],
|
||||||
schema: Optional[Dict[str, Any]],
|
schema: Optional[Dict[str, Any]],
|
||||||
custom_instructions: Optional[str],
|
custom_instructions: Optional[str],
|
||||||
entity_type: str,
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Build the settings body the API accepts.
|
"""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
|
``entities``; only ``enabled`` is project-wide. This mirrors what
|
||||||
``get_profile_settings`` returns, so the two round-trip.
|
``get_profile_settings`` returns, so the two round-trip.
|
||||||
|
|
||||||
@@ -72,7 +71,7 @@ def _profile_settings_payload(
|
|||||||
entity_settings["custom_instructions"] = custom_instructions
|
entity_settings["custom_instructions"] = custom_instructions
|
||||||
|
|
||||||
if entity_settings:
|
if entity_settings:
|
||||||
payload["entities"] = {entity_type: entity_settings}
|
payload["entities"] = {"user": entity_settings}
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
@@ -719,15 +718,14 @@ class MemoryClient:
|
|||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@api_error_handler
|
||||||
def get_profile(self, entity_id: str, entity_type: str = "user") -> Dict[str, Any]:
|
def get_profile(self, entity_id: str) -> Dict[str, Any]:
|
||||||
"""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
|
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:
|
Args:
|
||||||
entity_id: The entity's id, as you supplied it on ``add`` (e.g. "alice").
|
entity_id: The user's id, as you supplied it on ``add`` (e.g. "alice").
|
||||||
entity_type: Either "user" or "agent". Defaults to "user".
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with ``profile``, ``status``, ``entity_type``, ``entity_id``,
|
Dict with ``profile``, ``status``, ``entity_type``, ``entity_id``,
|
||||||
@@ -735,47 +733,43 @@ class MemoryClient:
|
|||||||
"succeeded", "pending", "failed", "not_enabled" or "insufficient_data".
|
"succeeded", "pending", "failed", "not_enabled" or "insufficient_data".
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValidationError: If entity_type is not a supported entity kind.
|
|
||||||
AuthenticationError: If authentication fails.
|
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(
|
response = self.client.get(f"/v2/entities/user/{_encode_path_segment(entity_id)}/profile/")
|
||||||
f"/v2/entities/{_encode_path_segment(entity_type)}/{_encode_path_segment(entity_id)}/profile/"
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
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()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@api_error_handler
|
||||||
def generate_profile(self, entity_id: str, entity_type: str = "user") -> Dict[str, Any]:
|
def generate_profile(self, entity_id: str) -> Dict[str, Any]:
|
||||||
"""Generate or refresh the profile for a single entity, now.
|
"""Generate or refresh the profile for a single user, now.
|
||||||
|
|
||||||
Profiles are otherwise built once an entity crosses an internal message
|
Profiles are otherwise built once a user crosses an internal message
|
||||||
threshold, so a new entity has none for its first few memories. Returns as
|
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``.
|
soon as the work is queued: poll :meth:`get_profile` and branch on ``status``.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
entity_id: The entity's id, as you supplied it on ``add``.
|
entity_id: The user's id, as you supplied it on ``add``.
|
||||||
entity_type: Either "user" or "agent". Defaults to "user".
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict containing ``profile_id``, ``entity_type``, ``entity_id`` and
|
Dict containing ``profile_id``, ``entity_type``, ``entity_id`` and
|
||||||
``status``.
|
``status``.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValidationError: If entity_type is unsupported or profiles are not
|
ValidationError: If profiles are not enabled and configured for the
|
||||||
enabled and configured for the project.
|
project.
|
||||||
NotFoundError: If no such entity exists in the project.
|
NotFoundError: If no such user exists in the project.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
PROFILE_JOBS_PATH,
|
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},
|
headers={"Idempotency-Key": uuid.uuid4().hex},
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
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()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@api_error_handler
|
||||||
@@ -799,7 +793,6 @@ class MemoryClient:
|
|||||||
enabled: Optional[bool] = None,
|
enabled: Optional[bool] = None,
|
||||||
schema: Optional[Dict[str, Any]] = None,
|
schema: Optional[Dict[str, Any]] = None,
|
||||||
custom_instructions: Optional[str] = None,
|
custom_instructions: Optional[str] = None,
|
||||||
entity_type: str = "user",
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Update the profile settings for the current project.
|
"""Update the profile settings for the current project.
|
||||||
|
|
||||||
@@ -808,11 +801,9 @@ class MemoryClient:
|
|||||||
Args:
|
Args:
|
||||||
enabled: Turn profile generation on or off. Project-wide.
|
enabled: Turn profile generation on or off. Project-wide.
|
||||||
schema: JSON Schema for the profile. Every property needs a
|
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.
|
custom_instructions: Extra guidance for the extraction step.
|
||||||
Applies to ``entity_type``.
|
Applies to user profiles.
|
||||||
entity_type: Which entity kind ``schema`` and
|
|
||||||
``custom_instructions`` belong to. Defaults to "user".
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with the settings as stored after the update, in the same
|
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.
|
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 = self.client.post(PROFILE_SETTINGS_PATH, json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
capture_client_event(
|
capture_client_event(
|
||||||
"client.update_profile_settings",
|
"client.update_profile_settings",
|
||||||
self,
|
self,
|
||||||
{"keys": list(payload.keys()), "entity_type": entity_type, "sync_type": "sync"},
|
{"keys": list(payload.keys()), "sync_type": "sync"},
|
||||||
)
|
)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@api_error_handler
|
||||||
def sample_profiles(self, limit: Optional[int] = None, entity_type: str = "user") -> Dict[str, Any]:
|
def sample_profiles(self, limit: Optional[int] = None) -> Dict[str, Any]:
|
||||||
"""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
|
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:
|
Args:
|
||||||
limit: How many entities to sample, 1-10. Defaults to the server value.
|
limit: How many users to sample, 1-10. Defaults to the server value.
|
||||||
entity_type: Which entity kind to sample. Defaults to "user".
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict containing ``job_id``, ``status``, ``status_url``, ``sampled``
|
Dict containing ``job_id``, ``status``, ``status_url``, ``sampled``
|
||||||
@@ -855,42 +845,14 @@ class MemoryClient:
|
|||||||
|
|
||||||
payload = self._prepare_params({"limit": limit})
|
payload = self._prepare_params({"limit": limit})
|
||||||
payload["operation"] = "sample"
|
payload["operation"] = "sample"
|
||||||
payload["entity_type"] = entity_type
|
payload["entity_type"] = "user"
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
PROFILE_JOBS_PATH,
|
PROFILE_JOBS_PATH,
|
||||||
json=payload,
|
json=payload,
|
||||||
headers={"Idempotency-Key": uuid.uuid4().hex},
|
headers={"Idempotency-Key": uuid.uuid4().hex},
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
capture_client_event("client.sample_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
|
|
||||||
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"})
|
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@api_error_handler
|
||||||
@@ -1820,15 +1782,14 @@ class AsyncMemoryClient:
|
|||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@api_error_handler
|
||||||
async def get_profile(self, entity_id: str, entity_type: str = "user") -> Dict[str, Any]:
|
async def get_profile(self, entity_id: str) -> Dict[str, Any]:
|
||||||
"""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
|
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:
|
Args:
|
||||||
entity_id: The entity's id, as you supplied it on ``add`` (e.g. "alice").
|
entity_id: The user's id, as you supplied it on ``add`` (e.g. "alice").
|
||||||
entity_type: Either "user" or "agent". Defaults to "user".
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with ``profile``, ``status``, ``entity_type``, ``entity_id``,
|
Dict with ``profile``, ``status``, ``entity_type``, ``entity_id``,
|
||||||
@@ -1836,47 +1797,43 @@ class AsyncMemoryClient:
|
|||||||
"succeeded", "pending", "failed", "not_enabled" or "insufficient_data".
|
"succeeded", "pending", "failed", "not_enabled" or "insufficient_data".
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValidationError: If entity_type is not a supported entity kind.
|
|
||||||
AuthenticationError: If authentication fails.
|
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(
|
response = await self.async_client.get(f"/v2/entities/user/{_encode_path_segment(entity_id)}/profile/")
|
||||||
f"/v2/entities/{_encode_path_segment(entity_type)}/{_encode_path_segment(entity_id)}/profile/"
|
|
||||||
)
|
|
||||||
response.raise_for_status()
|
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()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@api_error_handler
|
||||||
async def generate_profile(self, entity_id: str, entity_type: str = "user") -> Dict[str, Any]:
|
async def generate_profile(self, entity_id: str) -> Dict[str, Any]:
|
||||||
"""Generate or refresh the profile for a single entity, now.
|
"""Generate or refresh the profile for a single user, now.
|
||||||
|
|
||||||
Profiles are otherwise built once an entity crosses an internal message
|
Profiles are otherwise built once a user crosses an internal message
|
||||||
threshold, so a new entity has none for its first few memories. Returns as
|
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``.
|
soon as the work is queued: poll :meth:`get_profile` and branch on ``status``.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
entity_id: The entity's id, as you supplied it on ``add``.
|
entity_id: The user's id, as you supplied it on ``add``.
|
||||||
entity_type: Either "user" or "agent". Defaults to "user".
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict containing ``profile_id``, ``entity_type``, ``entity_id`` and
|
Dict containing ``profile_id``, ``entity_type``, ``entity_id`` and
|
||||||
``status``.
|
``status``.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValidationError: If entity_type is unsupported or profiles are not
|
ValidationError: If profiles are not enabled and configured for the
|
||||||
enabled and configured for the project.
|
project.
|
||||||
NotFoundError: If no such entity exists in the project.
|
NotFoundError: If no such user exists in the project.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = await self.async_client.post(
|
response = await self.async_client.post(
|
||||||
PROFILE_JOBS_PATH,
|
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},
|
headers={"Idempotency-Key": uuid.uuid4().hex},
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
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()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@api_error_handler
|
||||||
@@ -1900,7 +1857,6 @@ class AsyncMemoryClient:
|
|||||||
enabled: Optional[bool] = None,
|
enabled: Optional[bool] = None,
|
||||||
schema: Optional[Dict[str, Any]] = None,
|
schema: Optional[Dict[str, Any]] = None,
|
||||||
custom_instructions: Optional[str] = None,
|
custom_instructions: Optional[str] = None,
|
||||||
entity_type: str = "user",
|
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Update the profile settings for the current project.
|
"""Update the profile settings for the current project.
|
||||||
|
|
||||||
@@ -1909,11 +1865,9 @@ class AsyncMemoryClient:
|
|||||||
Args:
|
Args:
|
||||||
enabled: Turn profile generation on or off. Project-wide.
|
enabled: Turn profile generation on or off. Project-wide.
|
||||||
schema: JSON Schema for the profile. Every property needs a
|
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.
|
custom_instructions: Extra guidance for the extraction step.
|
||||||
Applies to ``entity_type``.
|
Applies to user profiles.
|
||||||
entity_type: Which entity kind ``schema`` and
|
|
||||||
``custom_instructions`` belong to. Defaults to "user".
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with the settings as stored after the update, in the same
|
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.
|
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 = await self.async_client.post(PROFILE_SETTINGS_PATH, json=payload)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
capture_client_event(
|
capture_client_event(
|
||||||
"client.update_profile_settings",
|
"client.update_profile_settings",
|
||||||
self,
|
self,
|
||||||
{"keys": list(payload.keys()), "entity_type": entity_type, "sync_type": "async"},
|
{"keys": list(payload.keys()), "sync_type": "async"},
|
||||||
)
|
)
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@api_error_handler
|
||||||
async def sample_profiles(self, limit: Optional[int] = None, entity_type: str = "user") -> Dict[str, Any]:
|
async def sample_profiles(self, limit: Optional[int] = None) -> Dict[str, Any]:
|
||||||
"""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
|
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:
|
Args:
|
||||||
limit: How many entities to sample, 1-10. Defaults to the server value.
|
limit: How many users to sample, 1-10. Defaults to the server value.
|
||||||
entity_type: Which entity kind to sample. Defaults to "user".
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict containing ``job_id``, ``status``, ``status_url``, ``sampled``
|
Dict containing ``job_id``, ``status``, ``status_url``, ``sampled``
|
||||||
@@ -1956,44 +1909,14 @@ class AsyncMemoryClient:
|
|||||||
|
|
||||||
payload = self._prepare_params({"limit": limit})
|
payload = self._prepare_params({"limit": limit})
|
||||||
payload["operation"] = "sample"
|
payload["operation"] = "sample"
|
||||||
payload["entity_type"] = entity_type
|
payload["entity_type"] = "user"
|
||||||
response = await self.async_client.post(
|
response = await self.async_client.post(
|
||||||
PROFILE_JOBS_PATH,
|
PROFILE_JOBS_PATH,
|
||||||
json=payload,
|
json=payload,
|
||||||
headers={"Idempotency-Key": uuid.uuid4().hex},
|
headers={"Idempotency-Key": uuid.uuid4().hex},
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
capture_client_event("client.sample_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
|
|
||||||
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"}
|
|
||||||
)
|
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
@api_error_handler
|
@api_error_handler
|
||||||
|
|||||||
@@ -57,13 +57,6 @@ class TestGetProfile:
|
|||||||
|
|
||||||
mock_memory_client.client.get.assert_called_once_with("/v2/entities/user/alice/profile/")
|
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):
|
def test_encodes_path_segments(self, mock_memory_client):
|
||||||
"""An id with a slash must not open a new path segment."""
|
"""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.client.get.return_value = _mock_response({"profile": {}, "status": "pending"})
|
||||||
@@ -98,16 +91,6 @@ class TestGenerateProfile:
|
|||||||
{"operation": "trigger", "entity_type": "user", "entity_id": "alice"},
|
{"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:
|
class TestProfileSettings:
|
||||||
def test_get_reads_v2(self, mock_memory_client):
|
def test_get_reads_v2(self, mock_memory_client):
|
||||||
@@ -145,14 +128,14 @@ class TestProfileSettings:
|
|||||||
assert set(kwargs["json"]) == {"enabled", "entities"}
|
assert set(kwargs["json"]) == {"enabled", "entities"}
|
||||||
assert "schema" not in kwargs["json"]
|
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"}}}
|
schema = {"type": "object", "properties": {"x": {"type": "string", "description": "d"}}}
|
||||||
mock_memory_client.client.post.return_value = _mock_response({"enabled": True})
|
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
|
_, 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):
|
def test_update_passes_schema_verbatim(self, mock_memory_client):
|
||||||
schema = {
|
schema = {
|
||||||
@@ -178,7 +161,7 @@ class TestProfileSettings:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestSampleAndRegenerate:
|
class TestSampleProfiles:
|
||||||
def test_sample_without_limit(self, mock_memory_client):
|
def test_sample_without_limit(self, mock_memory_client):
|
||||||
mock_memory_client.client.post.return_value = _mock_response({"sampled": 5, "entity_ids": []})
|
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"},
|
{"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:
|
class TestAsyncClientParity:
|
||||||
"""The async client must speak the same wire protocol as the sync one."""
|
"""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):
|
def test_generate_profile(self, async_client):
|
||||||
async_client.async_client.post = AsyncMock(return_value=_mock_response({"profile_id": "p_1"}))
|
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(
|
_assert_job_call(
|
||||||
async_client.async_client.post,
|
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):
|
def test_update_settings_partial(self, async_client):
|
||||||
@@ -274,10 +240,3 @@ class TestAsyncClientParity:
|
|||||||
"/v2/profiles/settings/",
|
"/v2/profiles/settings/",
|
||||||
json={"enabled": True, "entities": {"user": {"schema": schema}}},
|
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"})
|
|
||||||
|
|||||||
Reference in New Issue
Block a user