fix(profiles): address SDK/docs review for user profiles v1

Addresses @kartik-mem0's review on mem0#7340, verified against the live
staging profiles API on a neuron:

- generate_profile / sample_profiles accept a caller-supplied idempotency_key,
  so retrying a lost request reuses the job instead of creating a second
  billable one (Python sync+async and TS)
- TS uses the uuid dependency instead of the global crypto.randomUUID(), which
  throws on the supported Node 18 target
- update_profile_settings distinguishes an omitted argument from an explicit
  None, so schema / custom_instructions can be cleared (Python sentinel)
- export ProfileJobResponse and ProfileJobStatus; drop the deprecated
  ProfileTriggerResponse / ProfileSamplesResponse aliases and the unpopulated
  results field; correct usageUnits -> entityCountReserved; add error to
  ProfileResponse
- openapi: nest schema / custom_instructions under entities in the settings
  request and response, add capabilities, and mark entity_type required on the
  job body
- docs sample example polls to a terminal job status with a timeout, then reads
  the create response's entity_ids (status.results raised KeyError)
- notebook: include PARTIALLY_SUCCEEDED in terminal states, raise on timeout,
  and snapshot/restore project settings so a shared env is left as found
- tests: real job_id create shape, idempotency-key reuse, and clear-with-None

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
karthik
2026-09-23 22:56:48 +05:30
parent f29820886a
commit be1ded5793
9 changed files with 322 additions and 163 deletions
+133 -31
View File
@@ -8175,18 +8175,63 @@
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether profile generation runs for this project."
"description": "Whether profile generation runs for this project. Project-wide."
},
"schema": {
"entities": {
"type": "object",
"additionalProperties": true,
"nullable": true,
"description": "JSON Schema describing the profile. Every property needs a description."
"description": "Per-entity-type settings. Only `user` is available in this release.",
"properties": {
"user": {
"type": "object",
"properties": {
"schema": {
"type": "object",
"additionalProperties": true,
"nullable": true,
"description": "JSON Schema describing the profile. Every property needs a description."
},
"custom_instructions": {
"type": "string",
"nullable": true,
"description": "Extra guidance for the extraction step."
}
}
},
"agent": {
"type": "object",
"properties": {
"schema": {
"type": "object",
"additionalProperties": true,
"nullable": true,
"description": "JSON Schema describing the profile. Every property needs a description."
},
"custom_instructions": {
"type": "string",
"nullable": true,
"description": "Extra guidance for the extraction step."
}
}
}
}
},
"custom_instructions": {
"type": "string",
"nullable": true,
"description": "Extra guidance for the extraction step."
"capabilities": {
"type": "object",
"properties": {
"jobs": {
"type": "boolean"
},
"estimates": {
"type": "boolean"
},
"samples": {
"type": "boolean"
},
"full_rebuild": {
"type": "boolean",
"description": "Whether a project-wide rebuild (regenerate/backfill) is available. Currently false."
}
}
}
}
}
@@ -8208,21 +8253,33 @@
"application/json": {
"schema": {
"type": "object",
"description": "Only the fields present are written. `schema` and `custom_instructions` nest under `entities.user`; a flat body is rejected.",
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether profile generation runs for this project."
"description": "Whether profile generation runs for this project. Project-wide."
},
"schema": {
"entities": {
"type": "object",
"additionalProperties": true,
"nullable": true,
"description": "JSON Schema describing the profile. Every property needs a description."
},
"custom_instructions": {
"type": "string",
"nullable": true,
"description": "Extra guidance for the extraction step."
"description": "Per-entity-type settings. Only `user` is available in this release.",
"properties": {
"user": {
"type": "object",
"properties": {
"schema": {
"type": "object",
"additionalProperties": true,
"nullable": true,
"description": "JSON Schema describing the profile. Every property needs a description. Send null to clear it."
},
"custom_instructions": {
"type": "string",
"nullable": true,
"description": "Extra guidance for the extraction step. Send null to clear it."
}
}
}
}
}
}
}
@@ -8239,18 +8296,63 @@
"properties": {
"enabled": {
"type": "boolean",
"description": "Whether profile generation runs for this project."
"description": "Whether profile generation runs for this project. Project-wide."
},
"schema": {
"entities": {
"type": "object",
"additionalProperties": true,
"nullable": true,
"description": "JSON Schema describing the profile. Every property needs a description."
"description": "Per-entity-type settings. Only `user` is available in this release.",
"properties": {
"user": {
"type": "object",
"properties": {
"schema": {
"type": "object",
"additionalProperties": true,
"nullable": true,
"description": "JSON Schema describing the profile. Every property needs a description."
},
"custom_instructions": {
"type": "string",
"nullable": true,
"description": "Extra guidance for the extraction step."
}
}
},
"agent": {
"type": "object",
"properties": {
"schema": {
"type": "object",
"additionalProperties": true,
"nullable": true,
"description": "JSON Schema describing the profile. Every property needs a description."
},
"custom_instructions": {
"type": "string",
"nullable": true,
"description": "Extra guidance for the extraction step."
}
}
}
}
},
"custom_instructions": {
"type": "string",
"nullable": true,
"description": "Extra guidance for the extraction step."
"capabilities": {
"type": "object",
"properties": {
"jobs": {
"type": "boolean"
},
"estimates": {
"type": "boolean"
},
"samples": {
"type": "boolean"
},
"full_rebuild": {
"type": "boolean",
"description": "Whether a project-wide rebuild (regenerate/backfill) is available. Currently false."
}
}
}
}
}
@@ -8291,7 +8393,8 @@
"schema": {
"type": "object",
"required": [
"operation"
"operation",
"entity_type"
],
"properties": {
"operation": {
@@ -8306,8 +8409,7 @@
"type": "string",
"enum": [
"user"
],
"default": "user"
]
},
"entity_id": {
"type": "string",
+31 -10
View File
@@ -182,27 +182,48 @@ Sampling is asynchronous: the call returns a job as soon as it is queued. Poll `
<CodeGroup>
```python Python
import time
job = client.sample_profiles(limit=5)
# Poll until the sample job finishes.
status = client.get_profile_job(job["status_url"])["job"]
# Poll until the sample job reaches a terminal state (job status is UPPERCASE).
TERMINAL = {"SUCCEEDED", "PARTIALLY_SUCCEEDED", "FAILED", "CANCELLED"}
deadline = time.time() + 120
while True:
status = client.get_profile_job(job["status_url"])["job"]
if status["status"] in TERMINAL:
break
if time.time() > deadline:
raise TimeoutError("Sample job did not finish in time")
time.sleep(3)
print(status["status"], status["succeeded"], "of", status["total"])
# Each result names one sampled entity; read its saved profile.
for row in status["results"]:
print(client.get_profile(row["entity_id"]))
# The create response lists the sampled entities; read each one's saved profile.
for entity_id in job["entity_ids"]:
print(client.get_profile(entity_id))
```
```typescript TypeScript
const job = await client.sampleProfiles({ limit: 5 });
// Poll until the sample job finishes.
const { job: status } = await client.getProfileJob(job.statusUrl);
// Poll until the sample job reaches a terminal state (job status is UPPERCASE).
const TERMINAL = ["SUCCEEDED", "PARTIALLY_SUCCEEDED", "FAILED", "CANCELLED"];
const deadline = Date.now() + 120_000;
let status;
while (true) {
status = (await client.getProfileJob(job.statusUrl)).job;
if (TERMINAL.includes(status.status)) break;
if (Date.now() > deadline)
throw new Error("Sample job did not finish in time");
await new Promise((resolve) => setTimeout(resolve, 3000));
}
console.log(status.status, status.succeeded, "of", status.total);
// Each result names one sampled entity; read its saved profile.
for (const row of status.results ?? []) {
console.log(await client.getProfile({ entityId: row.entityId }));
// The create response lists the sampled entities; read each one's saved profile.
for (const entityId of job.entityIds ?? []) {
console.log(await client.getProfile({ entityId }));
}
```
</CodeGroup>