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
co-authored by Claude Opus 4.8
parent f29820886a
commit be1ded5793
9 changed files with 322 additions and 163 deletions
+32 -2
View File
@@ -82,7 +82,9 @@ class TestGetProfile:
class TestGenerateProfile:
def test_posts_entity_type_and_id(self, mock_memory_client):
mock_memory_client.client.post.return_value = _mock_response({"profile_id": "p_1", "status": "PENDING"})
mock_memory_client.client.post.return_value = _mock_response(
{"job_id": "j1", "status": "QUEUED", "status_url": "/v2/profiles/jobs/j1/"}
)
mock_memory_client.generate_profile("alice")
@@ -91,6 +93,15 @@ class TestGenerateProfile:
{"operation": "trigger", "entity_type": "user", "entity_id": "alice"},
)
def test_reuses_caller_idempotency_key(self, mock_memory_client):
"""A caller-supplied key lets a retry hit the same job instead of billing twice."""
mock_memory_client.client.post.return_value = _mock_response({"job_id": "j1", "status": "QUEUED"})
mock_memory_client.generate_profile("alice", idempotency_key="retry-key-123")
_, kwargs = mock_memory_client.client.post.call_args
assert kwargs["headers"]["Idempotency-Key"] == "retry-key-123"
class TestProfileSettings:
def test_get_reads_v2(self, mock_memory_client):
@@ -160,6 +171,15 @@ class TestProfileSettings:
},
)
def test_update_clears_fields_with_explicit_none(self, mock_memory_client):
"""Explicit ``None`` clears a field; the sentinel default leaves it untouched."""
mock_memory_client.client.post.return_value = _mock_response({"enabled": True})
mock_memory_client.update_profile_settings(schema=None, custom_instructions=None)
_, kwargs = mock_memory_client.client.post.call_args
assert kwargs["json"] == {"entities": {"user": {"schema": None, "custom_instructions": None}}}
class TestSampleProfiles:
def test_sample_without_limit(self, mock_memory_client):
@@ -210,7 +230,7 @@ class TestAsyncClientParity:
async_client.async_client.get.assert_called_once_with("/v2/entities/user/alice/profile/")
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({"job_id": "j1", "status": "QUEUED"}))
asyncio.run(async_client.generate_profile("alice"))
@@ -240,3 +260,13 @@ class TestAsyncClientParity:
"/v2/profiles/settings/",
json={"enabled": True, "entities": {"user": {"schema": schema}}},
)
def test_update_settings_clears_with_none(self, async_client):
async_client.async_client.post = AsyncMock(return_value=_mock_response({"enabled": True}))
asyncio.run(async_client.update_profile_settings(custom_instructions=None))
async_client.async_client.post.assert_called_once_with(
"/v2/profiles/settings/",
json={"entities": {"user": {"custom_instructions": None}}},
)