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:
co-authored by
Claude Opus 4.8
parent
f29820886a
commit
be1ded5793
@@ -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}}},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user