diff --git a/docs/api-reference/organizations-projects.mdx b/docs/api-reference/organizations-projects.mdx index e67d4d20c..c1b111fb9 100644 --- a/docs/api-reference/organizations-projects.mdx +++ b/docs/api-reference/organizations-projects.mdx @@ -95,6 +95,12 @@ client.project.update( custom_instructions="..." ) +# Separate extraction instructions for agent-scoped memories +# (see /platform/features/custom-instructions) +client.project.update( + agent_custom_instructions="..." +) + # Use the input language for memory storage and retrieval client.project.update(multilingual=True) diff --git a/docs/changelog/sdk.mdx b/docs/changelog/sdk.mdx index 3eea4b1a8..9eade0355 100644 --- a/docs/changelog/sdk.mdx +++ b/docs/changelog/sdk.mdx @@ -7,6 +7,13 @@ mode: "wide" + + +**New Features:** +- **Client:** Add `agent_custom_instructions` to `project.update()`/`update_project()` (sync and async) and to the `ProjectUpdateOptions` and `AddMemoryOptions` typed models. It sets a second extraction instruction set that applies only to agent-scoped memories: an add passing `agent_id` without `user_id` uses it, one passing both splits by attribution, and while it is unset `custom_instructions` continues to apply to every memory ([#6809](https://github.com/mem0ai/mem0/pull/6809)) + + + **New Features:** @@ -1189,6 +1196,13 @@ See the [OSS v2 to v3 migration guide](https://docs.mem0.ai/migration/oss-v2-to- + + +**New Features:** +- **Client:** Add `agentCustomInstructions` to `PromptUpdatePayload`, `AddMemoryOptions`, and `ProjectResponse`. It sets a second extraction instruction set that applies only to agent-scoped memories: an add passing `agentId` without `userId` uses it, one passing both splits by attribution, and while it is unset `customInstructions` continues to apply to every memory ([#6809](https://github.com/mem0ai/mem0/pull/6809)) + + + **New Features:** diff --git a/docs/openapi.json b/docs/openapi.json index d9130ce54..4b8cf0378 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2784,6 +2784,10 @@ "type": "string", "description": "Project-level instructions that guide extraction for this call." }, + "agent_custom_instructions": { + "type": "string", + "description": "Extraction instructions for agent-scoped memories, overriding the project-level setting for this call. Applied when `agent_id` is sent without `user_id`; when both are sent it governs the assistant-attributed memories while `custom_instructions` governs the rest." + }, "custom_categories": { "type": "array", "description": "Category catalog for this call. Replaces the project-level list rather than merging with it. Omit to fall back to the project list, then the default catalog.", @@ -5805,6 +5809,11 @@ }, "description": "Custom instructions for memory processing in this project" }, + "agent_custom_instructions": { + "type": "string", + "nullable": true, + "description": "Extraction instructions for agent-scoped memories. Falls back to `custom_instructions` when unset. Send an empty string to clear it." + }, "custom_categories": { "type": "array", "items": { @@ -7431,6 +7440,12 @@ "type": "string", "nullable": true }, + "agent_custom_instructions": { + "description": "Extraction instructions that apply only to agent-scoped memories. Used when `agent_id` is sent without `user_id`; when both are sent it governs the assistant-attributed memories while `custom_instructions` governs the rest. Falls back to `custom_instructions` when unset.", + "title": "Agent custom instructions", + "type": "string", + "nullable": true + }, "immutable": { "description": "Whether the memory is immutable.", "title": "Immutable", diff --git a/docs/platform/features/custom-instructions.mdx b/docs/platform/features/custom-instructions.mdx index a864fe8fa..d44014508 100644 --- a/docs/platform/features/custom-instructions.mdx +++ b/docs/platform/features/custom-instructions.mdx @@ -95,6 +95,100 @@ Exclude: - [Irrelevant information] ``` +## Agent Custom Instructions + +`custom_instructions` applies to every memory your project extracts, no matter whose it is. But what is worth remembering about an agent is rarely what is worth remembering about a user: an agent's useful memories are things like which tools fail, which retry strategies work, and how a given environment behaves, not personal preferences. + +`agent_custom_instructions` is an optional second set of extraction rules that applies only to agent-scoped memories. + +Available from Python SDK `v2.0.17` and TypeScript SDK `v3.1.5`. Upgrade first if you are on an earlier release. + +Set it on the project alongside `custom_instructions`: + + +```python Python +client.project.update( + custom_instructions="Extract the user's preferences, goals, and constraints.", + agent_custom_instructions=( + "Extract operational lessons for the agent:\n" + "- Tools that failed and the error returned\n" + "- Retry or fallback strategies that worked\n" + "- Environment quirks worth recalling on the next run\n\n" + "Exclude: user preferences, personal details." + ), +) +``` + +```javascript JavaScript +await client.updateProject({ + customInstructions: "Extract the user's preferences, goals, and constraints.", + agentCustomInstructions: `Extract operational lessons for the agent: +- Tools that failed and the error returned +- Retry or fallback strategies that worked +- Environment quirks worth recalling on the next run + +Exclude: user preferences, personal details.`, +}); +``` + + +### Which instructions apply + +Which set governs an `add()` call depends on the entity IDs you pass with it: + +| The add call passes | Instructions applied | +|---------------------|----------------------| +| `user_id` only | `custom_instructions` | +| `agent_id` only | `agent_custom_instructions` | +| `user_id` **and** `agent_id` | `agent_custom_instructions` govern the memories attributed to the assistant; `custom_instructions` govern the rest | + +`agent_custom_instructions` is unset by default. While it is unset, `custom_instructions` applies to every memory, so projects that don't set it behave exactly as they did before. + +### Overriding for a single call + +Both fields are also accepted per request, overriding the project setting for that `add()` only: + + +```python Python +client.add( + messages, + filters={"agent_id": "support-agent"}, + agent_custom_instructions="Only remember which tools errored and why.", +) +``` + +```javascript JavaScript +await client.add(messages, { + agentId: "support-agent", + agentCustomInstructions: "Only remember which tools errored and why.", +}); +``` + + +### Reading and clearing + + +```python Python +# Read the current value +response = client.project.get(fields=["agent_custom_instructions"]) +print(response["agent_custom_instructions"]) + +# Clear it; agent memories fall back to custom_instructions +client.project.update(agent_custom_instructions="") +``` + +```javascript JavaScript +// Read the current value +const response = await client.getProject({ + fields: ["agentCustomInstructions"], +}); +console.log(response.agentCustomInstructions); + +// Clear it; agent memories fall back to customInstructions +await client.updateProject({ agentCustomInstructions: "" }); +``` + + ## Real-World Examples diff --git a/mem0-ts/package.json b/mem0-ts/package.json index 75bdb3a93..20cba8de7 100644 --- a/mem0-ts/package.json +++ b/mem0-ts/package.json @@ -1,6 +1,6 @@ { "name": "mem0ai", - "version": "3.1.4", + "version": "3.1.5", "description": "The Memory Layer For Your AI Apps", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/mem0-ts/src/client/mem0.types.ts b/mem0-ts/src/client/mem0.types.ts index b66d4a444..c230441e0 100644 --- a/mem0-ts/src/client/mem0.types.ts +++ b/mem0-ts/src/client/mem0.types.ts @@ -12,6 +12,7 @@ export interface AddMemoryOptions extends EntityOptions { infer?: boolean; customCategories?: custom_categories[]; customInstructions?: string; + agentCustomInstructions?: string; timestamp?: number; expirationDate?: string; structuredDataSchema?: Record; @@ -60,6 +61,7 @@ export interface ProjectOptions { export interface PromptUpdatePayload { customInstructions?: string; + agentCustomInstructions?: string; customCategories?: custom_categories[]; version?: string; memoryDepth?: string | null; @@ -174,6 +176,7 @@ export interface PaginatedMemories { export interface ProjectResponse { customInstructions?: string; + agentCustomInstructions?: string; // The API returns category objects (`[{ "": "" }]`), // not bare strings (see issue #5738). customCategories?: custom_categories[]; diff --git a/mem0-ts/src/client/tests/memoryClient.crud.test.ts b/mem0-ts/src/client/tests/memoryClient.crud.test.ts index f116955f1..d1645ab40 100644 --- a/mem0-ts/src/client/tests/memoryClient.crud.test.ts +++ b/mem0-ts/src/client/tests/memoryClient.crud.test.ts @@ -74,6 +74,35 @@ describe("MemoryClient - add()", () => { expect(getFetchBody(call!).expiration_date).toBe("2030-01-31"); }); + test("serializes agentCustomInstructions as agent_custom_instructions", async () => { + const extra = new Map(); + extra.set("/v3/memories/add/", { status: 200, body: [createMockMemory()] }); + const mock = setupMockFetch(extra); + + const client = new MemoryClient({ apiKey: TEST_API_KEY }); + await client.add([{ role: "user", content: "test" }], { + agentId: "a1", + agentCustomInstructions: "Remember tool failures", + }); + + const call = findFetchCall(mock, "/v3/memories/add/", "POST"); + expect(getFetchBody(call!).agent_custom_instructions).toBe( + "Remember tool failures", + ); + }); + + test("omits agent_custom_instructions when it is not passed", async () => { + const extra = new Map(); + extra.set("/v3/memories/add/", { status: 200, body: [createMockMemory()] }); + const mock = setupMockFetch(extra); + + const client = new MemoryClient({ apiKey: TEST_API_KEY }); + await client.add([{ role: "user", content: "test" }], { userId: "u1" }); + + const call = findFetchCall(mock, "/v3/memories/add/", "POST"); + expect(getFetchBody(call!)).not.toHaveProperty("agent_custom_instructions"); + }); + test("throws an error when given an empty messages array", async () => { setupMockFetch(); diff --git a/mem0-ts/src/client/tests/memoryClient.project.test.ts b/mem0-ts/src/client/tests/memoryClient.project.test.ts index ed5dd6696..2fcfb1f0c 100644 --- a/mem0-ts/src/client/tests/memoryClient.project.test.ts +++ b/mem0-ts/src/client/tests/memoryClient.project.test.ts @@ -96,6 +96,84 @@ describe("MemoryClient - updateProject()", () => { "Updated instructions", ); }); + + test("camelCases agentCustomInstructions into agent_custom_instructions", async () => { + const extra = new Map(); + extra.set("/api/v1/orgs/organizations/", { + status: 200, + body: { message: "Updated" }, + }); + const mock = setupMockFetch(extra); + + const client = new MemoryClient({ apiKey: TEST_API_KEY }); + await client.ping(); + await client.updateProject({ + agentCustomInstructions: "Remember tool failures", + }); + + const call = findFetchCall(mock, "/api/v1/orgs/organizations/", "PATCH"); + expect(getFetchBody(call!).agent_custom_instructions).toBe( + "Remember tool failures", + ); + }); + + test("sends both instruction sets in one PATCH", async () => { + const extra = new Map(); + extra.set("/api/v1/orgs/organizations/", { + status: 200, + body: { message: "Updated" }, + }); + const mock = setupMockFetch(extra); + + const client = new MemoryClient({ apiKey: TEST_API_KEY }); + await client.ping(); + await client.updateProject({ + customInstructions: "Remember user preferences", + agentCustomInstructions: "Remember tool failures", + }); + + const body = getFetchBody( + findFetchCall(mock, "/api/v1/orgs/organizations/", "PATCH")!, + ); + expect(body.custom_instructions).toBe("Remember user preferences"); + expect(body.agent_custom_instructions).toBe("Remember tool failures"); + }); + + test("an empty string round-trips, so the field can be cleared", async () => { + const extra = new Map(); + extra.set("/api/v1/orgs/organizations/", { + status: 200, + body: { message: "Updated" }, + }); + const mock = setupMockFetch(extra); + + const client = new MemoryClient({ apiKey: TEST_API_KEY }); + await client.ping(); + await client.updateProject({ agentCustomInstructions: "" }); + + const body = getFetchBody( + findFetchCall(mock, "/api/v1/orgs/organizations/", "PATCH")!, + ); + expect(body).toHaveProperty("agent_custom_instructions", ""); + }); + + test("omits agent_custom_instructions when it is not passed", async () => { + const extra = new Map(); + extra.set("/api/v1/orgs/organizations/", { + status: 200, + body: { message: "Updated" }, + }); + const mock = setupMockFetch(extra); + + const client = new MemoryClient({ apiKey: TEST_API_KEY }); + await client.ping(); + await client.updateProject({ customInstructions: "Be helpful" }); + + const body = getFetchBody( + findFetchCall(mock, "/api/v1/orgs/organizations/", "PATCH")!, + ); + expect(body).not.toHaveProperty("agent_custom_instructions"); + }); }); // ─── feedback() ───────────────────────────────────────── diff --git a/mem0/client/main.py b/mem0/client/main.py index 4556d6a3f..a1b68d73b 100644 --- a/mem0/client/main.py +++ b/mem0/client/main.py @@ -734,6 +734,7 @@ class MemoryClient: memory_depth: Optional[str] = None, usecase_setting: Optional[str] = None, multilingual: Optional[bool] = None, + agent_custom_instructions: Optional[str] = None, ) -> Dict[str, Any]: """Update the project settings. @@ -744,6 +745,7 @@ class MemoryClient: memory_depth: Memory depth for the project. usecase_setting: Usecase setting for the project. multilingual: Whether to use the input language for memory storage and retrieval. + agent_custom_instructions: New extraction instructions for agent-scoped memories. Returns: Dictionary containing the API response. @@ -768,6 +770,7 @@ class MemoryClient: "memory_depth": memory_depth, "usecase_setting": usecase_setting, "multilingual": multilingual, + "agent_custom_instructions": agent_custom_instructions, }.items() if v is not None }, @@ -1636,6 +1639,7 @@ class AsyncMemoryClient: memory_depth: Optional[str] = None, usecase_setting: Optional[str] = None, multilingual: Optional[bool] = None, + agent_custom_instructions: Optional[str] = None, ) -> Dict[str, Any]: """Update the project settings. @@ -1646,6 +1650,7 @@ class AsyncMemoryClient: memory_depth: Memory depth for the project. usecase_setting: Usecase setting for the project. multilingual: Whether to use the input language for memory storage and retrieval. + agent_custom_instructions: New extraction instructions for agent-scoped memories. Returns: Dictionary containing the API response. @@ -1670,6 +1675,7 @@ class AsyncMemoryClient: "memory_depth": memory_depth, "usecase_setting": usecase_setting, "multilingual": multilingual, + "agent_custom_instructions": agent_custom_instructions, }.items() if v is not None }, diff --git a/mem0/client/project.py b/mem0/client/project.py index 854c3a6cb..a7280c00d 100644 --- a/mem0/client/project.py +++ b/mem0/client/project.py @@ -177,6 +177,7 @@ class BaseProject(ABC): self, custom_instructions: Optional[str] = None, custom_categories: Optional[List[str]] = None, + agent_custom_instructions: Optional[str] = None, ) -> Dict[str, Any]: """ Update project settings. @@ -184,6 +185,7 @@ class BaseProject(ABC): Args: custom_instructions: New instructions for the project custom_categories: New categories for the project + agent_custom_instructions: New extraction instructions for agent-scoped memories Returns: Dictionary containing the API response. @@ -396,6 +398,7 @@ class Project(BaseProject): custom_categories: Optional[List[str]] = None, multilingual: Optional[bool] = None, decay: Optional[bool] = None, + agent_custom_instructions: Optional[str] = None, ) -> Dict[str, Any]: """ Update project settings. @@ -407,6 +410,7 @@ class Project(BaseProject): decay: Toggle Memory Decay for this project. When True, search-time ranking boosts recently-used memories and gently dampens stale ones; when False, ranking is restored to the pre-decay behaviour. Off by default. + agent_custom_instructions: New extraction instructions for agent-scoped memories Returns: Dictionary containing the API response. @@ -418,10 +422,17 @@ class Project(BaseProject): NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ - if custom_instructions is None and custom_categories is None and multilingual is None and decay is None: + if ( + custom_instructions is None + and custom_categories is None + and multilingual is None + and decay is None + and agent_custom_instructions is None + ): raise ValueError( "At least one parameter must be provided for update: " - "custom_instructions, custom_categories, multilingual, decay" + "custom_instructions, custom_categories, multilingual, decay, " + "agent_custom_instructions" ) payload = self._prepare_params( @@ -430,6 +441,7 @@ class Project(BaseProject): "custom_categories": custom_categories, "multilingual": multilingual, "decay": decay, + "agent_custom_instructions": agent_custom_instructions, } ) response = self._client.patch( @@ -445,6 +457,7 @@ class Project(BaseProject): "custom_categories": custom_categories, "multilingual": multilingual, "decay": decay, + "agent_custom_instructions": agent_custom_instructions, "sync_type": "sync", }, ) @@ -709,6 +722,7 @@ class AsyncProject(BaseProject): custom_categories: Optional[List[str]] = None, multilingual: Optional[bool] = None, decay: Optional[bool] = None, + agent_custom_instructions: Optional[str] = None, ) -> Dict[str, Any]: """ Update project settings. @@ -720,6 +734,7 @@ class AsyncProject(BaseProject): decay: Toggle Memory Decay for this project. When True, search-time ranking boosts recently-used memories and gently dampens stale ones; when False, ranking is restored to the pre-decay behaviour. Off by default. + agent_custom_instructions: New extraction instructions for agent-scoped memories Returns: Dictionary containing the API response. @@ -731,10 +746,17 @@ class AsyncProject(BaseProject): NetworkError: If network connectivity issues occur. ValueError: If org_id or project_id are not set. """ - if custom_instructions is None and custom_categories is None and multilingual is None and decay is None: + if ( + custom_instructions is None + and custom_categories is None + and multilingual is None + and decay is None + and agent_custom_instructions is None + ): raise ValueError( "At least one parameter must be provided for update: " - "custom_instructions, custom_categories, multilingual, decay" + "custom_instructions, custom_categories, multilingual, decay, " + "agent_custom_instructions" ) payload = self._prepare_params( @@ -743,6 +765,7 @@ class AsyncProject(BaseProject): "custom_categories": custom_categories, "multilingual": multilingual, "decay": decay, + "agent_custom_instructions": agent_custom_instructions, } ) response = await self._client.patch( @@ -758,6 +781,7 @@ class AsyncProject(BaseProject): "custom_categories": custom_categories, "multilingual": multilingual, "decay": decay, + "agent_custom_instructions": agent_custom_instructions, "sync_type": "async", }, ) diff --git a/mem0/client/types.py b/mem0/client/types.py index 527bc2fc6..5663dcf5d 100644 --- a/mem0/client/types.py +++ b/mem0/client/types.py @@ -28,6 +28,9 @@ class AddMemoryOptions(BaseModel): default=None, description="Custom categories for memory classification" ) custom_instructions: Optional[str] = Field(default=None, description="Custom instructions for fact extraction") + agent_custom_instructions: Optional[str] = Field( + default=None, description="Custom instructions for fact extraction from agent-scoped memories" + ) timestamp: Optional[int] = Field(default=None, description="Unix timestamp for the memory") expiration_date: Optional[str] = Field(default=None, description="Expiration date in YYYY-MM-DD format") structured_data_schema: Optional[Dict[str, Any]] = Field( @@ -107,6 +110,9 @@ class ProjectUpdateOptions(BaseModel): """Options for project update operations.""" custom_instructions: Optional[str] = Field(default=None, description="Custom instructions for fact extraction") + agent_custom_instructions: Optional[str] = Field( + default=None, description="Custom instructions for fact extraction from agent-scoped memories" + ) custom_categories: Optional[List[Dict[str, Any]]] = Field( default=None, description="Custom categories for classification" ) diff --git a/pyproject.toml b/pyproject.toml index 0bc386d1b..650fe0df3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mem0ai" -version = "2.0.16" +version = "2.0.17" description = "Long-term memory for AI Agents" authors = [ { name = "Mem0", email = "support@mem0.ai" } diff --git a/tests/test_client.py b/tests/test_client.py index aa8c16ce5..d7d11ca86 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -440,3 +440,51 @@ class TestValidateApiKeyHttpError: assert not isinstance(exc_info.value, requests.exceptions.JSONDecodeError) assert "Error:" in str(exc_info.value) + + +class TestAddAgentCustomInstructions: + """Per-request override of the project-level setting, forwarded to the add payload.""" + + def _mock_add(self, client): + response = MagicMock() + response.json.return_value = {"results": []} + response.raise_for_status.return_value = None + client.client.post.return_value = response + return response + + def test_kwarg_reaches_the_add_payload(self, mock_memory_client): + self._mock_add(mock_memory_client) + + mock_memory_client.add( + "hello", + filters={"agent_id": "a1"}, + agent_custom_instructions="remember tool failures", + ) + + _, kwargs = mock_memory_client.client.post.call_args + assert kwargs["json"]["agent_custom_instructions"] == "remember tool failures" + + def test_typed_option_reaches_the_add_payload(self, mock_memory_client): + from mem0.client.types import AddMemoryOptions + + self._mock_add(mock_memory_client) + + mock_memory_client.add( + "hello", + AddMemoryOptions( + filters={"agent_id": "a1"}, + agent_custom_instructions="remember tool failures", + ), + ) + + _, kwargs = mock_memory_client.client.post.call_args + assert kwargs["json"]["agent_custom_instructions"] == "remember tool failures" + + def test_absent_when_not_passed(self, mock_memory_client): + """Callers that don't use the feature send an unchanged payload.""" + self._mock_add(mock_memory_client) + + mock_memory_client.add("hello", filters={"user_id": "u1"}) + + _, kwargs = mock_memory_client.client.post.call_args + assert "agent_custom_instructions" not in kwargs["json"] diff --git a/tests/test_project.py b/tests/test_project.py index b3f48f051..8cea43b2b 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -3,8 +3,8 @@ parameter-passthrough surface. Verifies the kwarg → JSON payload mapping for every supported field (``custom_instructions``, ``custom_categories``, ``multilingual``, -``decay``), the ValueError when no field is provided, and the -URL/method shape. The HTTP layer is mocked. +``decay``, ``agent_custom_instructions``), the ValueError when no field +is provided, and the URL/method shape. The HTTP layer is mocked. """ from unittest.mock import MagicMock, patch @@ -84,6 +84,50 @@ class TestProjectUpdateDecay: assert args[0] == "/api/v1/orgs/organizations/org1/projects/proj1/" +class TestProjectUpdateAgentCustomInstructions: + def test_agent_custom_instructions_sent_in_payload(self, project): + proj, http = project + proj.update(agent_custom_instructions="remember tool failures") + assert _patch_payload(http) == {"agent_custom_instructions": "remember tool failures"} + + def test_agent_custom_instructions_alone_satisfies_the_guard(self, project): + """It is a standalone field, so setting only it must not raise.""" + proj, http = project + proj.update(agent_custom_instructions="remember tool failures") + assert http.patch.called + + def test_empty_string_round_trips_to_clear_the_field(self, project): + """An empty string must survive the ``is not None`` filter, not be dropped as falsy.""" + proj, http = project + proj.update(agent_custom_instructions="") + assert _patch_payload(http) == {"agent_custom_instructions": ""} + + def test_combined_with_custom_instructions(self, project): + """Both sets in one PATCH: the split-instruction configuration.""" + proj, http = project + proj.update( + custom_instructions="remember user preferences", + agent_custom_instructions="remember tool failures", + ) + assert _patch_payload(http) == { + "custom_instructions": "remember user preferences", + "agent_custom_instructions": "remember tool failures", + } + + def test_omitted_when_none(self, project): + """Callers that don't pass it must send an unchanged payload.""" + proj, http = project + proj.update(custom_instructions="be concise") + payload = _patch_payload(http) + assert payload == {"custom_instructions": "be concise"} + assert "agent_custom_instructions" not in payload + + def test_no_args_raises_with_agent_instructions_in_message(self, project): + proj, _ = project + with pytest.raises(ValueError, match=r"agent_custom_instructions"): + proj.update() + + class TestProjectUpdateBackwardsCompat: def test_multilingual_only_still_works(self, project): """Pre-decay callers (multilingual only) keep working unchanged."""