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:
@@ -39,30 +39,7 @@
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import json\n",
|
||||
"import os\n",
|
||||
"import time\n",
|
||||
"import uuid\n",
|
||||
"\n",
|
||||
"import mem0\n",
|
||||
"from mem0 import MemoryClient\n",
|
||||
"\n",
|
||||
"API_KEY = os.environ.get(\"MEM0_API_KEY\")\n",
|
||||
"if not API_KEY:\n",
|
||||
" import getpass\n",
|
||||
"\n",
|
||||
" API_KEY = getpass.getpass(\"API key: \")\n",
|
||||
"\n",
|
||||
"client = MemoryClient(api_key=API_KEY, host=os.environ.get(\"MEM0_API_HOST\") or None)\n",
|
||||
"\n",
|
||||
"# Fresh id each run, so nothing below is stale from a previous pass.\n",
|
||||
"USER_ID = f\"demo_{uuid.uuid4().hex[:8]}\"\n",
|
||||
"\n",
|
||||
"print(\"sdk :\", mem0.__file__) # must be this worktree\n",
|
||||
"print(\"host :\", client.host)\n",
|
||||
"print(\"demo user:\", USER_ID)\n"
|
||||
]
|
||||
"source": "import json\nimport os\nimport time\nimport uuid\n\nimport mem0\nfrom mem0 import MemoryClient\n\nAPI_KEY = os.environ.get(\"MEM0_API_KEY\")\nif not API_KEY:\n import getpass\n\n API_KEY = getpass.getpass(\"API key: \")\n\nclient = MemoryClient(api_key=API_KEY, host=os.environ.get(\"MEM0_API_HOST\") or None)\n\n# Fresh id each run, so nothing below is stale from a previous pass.\nUSER_ID = f\"demo_{uuid.uuid4().hex[:8]}\"\n\n# Snapshot the project's profile settings up front. This notebook overwrites the\n# shared project schema/instructions/enabled below; the cleanup cell restores this.\nORIGINAL_SETTINGS = client.get_profile_settings()\n\nprint(\"sdk :\", mem0.__file__) # must be this worktree\nprint(\"host :\", client.host)\nprint(\"demo user:\", USER_ID)"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -527,43 +504,7 @@
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"JOB_TERMINAL = {\"SUCCEEDED\", \"FAILED\", \"COMPLETED\", \"CANCELLED\"}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def wait_for_job(job_response, timeout=300, interval=5):\n",
|
||||
" \"\"\"Poll a generation job. Prefer status_url over a bare job id, so a route\n",
|
||||
" change needs no client update.\"\"\"\n",
|
||||
" handle = job_response.get(\"status_url\") or job_response[\"job_id\"]\n",
|
||||
" deadline = time.time() + timeout\n",
|
||||
" status = None\n",
|
||||
" while time.time() < deadline:\n",
|
||||
" status = client.get_profile_job(handle)[\"job\"]\n",
|
||||
" total = status.get(\"total\")\n",
|
||||
" print(\n",
|
||||
" f\" {status['status']} \"\n",
|
||||
" f\"completed={status.get('completed', 0)}/{total if total is not None else '?'} \"\n",
|
||||
" f\"succeeded={status.get('succeeded', 0)} \"\n",
|
||||
" f\"failed={status.get('failed', 0)} \"\n",
|
||||
" f\"skipped={status.get('skipped', 0)}\"\n",
|
||||
" )\n",
|
||||
" if str(status.get(\"status\", \"\")).upper() in JOB_TERMINAL:\n",
|
||||
" return status\n",
|
||||
" time.sleep(interval)\n",
|
||||
" return status\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"if job is None:\n",
|
||||
" print(\"no sample job to poll\")\n",
|
||||
"else:\n",
|
||||
" final = wait_for_job(job)\n",
|
||||
"\n",
|
||||
" print(\"\\n--- what the sample produced ---\")\n",
|
||||
" for entity_id in job.get(\"entity_ids\", []):\n",
|
||||
" got = client.get_profile(entity_id)\n",
|
||||
" print(f\"\\n{entity_id} [{got['status']}]\")\n",
|
||||
" print(\" \", json.dumps(got[\"profile\"])[:220])\n"
|
||||
]
|
||||
"source": "JOB_TERMINAL = {\"SUCCEEDED\", \"PARTIALLY_SUCCEEDED\", \"FAILED\", \"CANCELLED\"}\n\n\ndef wait_for_job(job_response, timeout=300, interval=5):\n \"\"\"Poll a generation job. Prefer status_url over a bare job id, so a route\n change needs no client update. Raise on timeout so an unfinished job is never\n mistaken for a finished one.\"\"\"\n handle = job_response.get(\"status_url\") or job_response[\"job_id\"]\n deadline = time.time() + timeout\n status = None\n while time.time() < deadline:\n status = client.get_profile_job(handle)[\"job\"]\n total = status.get(\"total\")\n print(\n f\" {status['status']} \"\n f\"completed={status.get('completed', 0)}/{total if total is not None else '?'} \"\n f\"succeeded={status.get('succeeded', 0)} \"\n f\"failed={status.get('failed', 0)} \"\n f\"skipped={status.get('skipped', 0)}\"\n )\n if str(status.get(\"status\", \"\")).upper() in JOB_TERMINAL:\n return status\n time.sleep(interval)\n raise TimeoutError(\n f\"job not terminal in {timeout}s (last status: {status.get('status') if status else 'none'})\"\n )\n\n\nif job is None:\n print(\"no sample job to poll\")\nelse:\n final = wait_for_job(job)\n\n print(\"\\n--- what the sample produced ---\")\n for entity_id in job.get(\"entity_ids\", []):\n got = client.get_profile(entity_id)\n print(f\"\\n{entity_id} [{got['status']}]\")\n print(\" \", json.dumps(got[\"profile\"])[:220])"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -735,13 +676,7 @@
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"for entity_id in (USER_ID, fresh):\n",
|
||||
" r = client.client.delete(f\"/v2/entities/user/{entity_id}/\")\n",
|
||||
" print(entity_id, \"->\", r.status_code)\n",
|
||||
"\n",
|
||||
"# Leave the project's own profile settings alone — other people may share this env.\n"
|
||||
]
|
||||
"source": "for entity_id in (USER_ID, fresh):\n r = client.client.delete(f\"/v2/entities/user/{entity_id}/\")\n print(entity_id, \"->\", r.status_code)\n\n# Restore the project's profile settings to the start-of-run snapshot, so a shared\n# env is left exactly as we found it. Passing the original values (including None)\n# clears anything this notebook set — the SDK distinguishes an explicit None from an\n# omitted argument.\n_user = ORIGINAL_SETTINGS.get(\"entities\", {}).get(\"user\", {})\nclient.update_profile_settings(\n enabled=ORIGINAL_SETTINGS.get(\"enabled\", False),\n schema=_user.get(\"schema\"),\n custom_instructions=_user.get(\"custom_instructions\"),\n)\nprint(\"profile settings restored to the pre-notebook snapshot\")"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@@ -805,4 +740,4 @@
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user