diff --git a/apps/docs/integrations/pipecat.mdx b/apps/docs/integrations/pipecat.mdx index 7c904b69..ae189343 100644 --- a/apps/docs/integrations/pipecat.mdx +++ b/apps/docs/integrations/pipecat.mdx @@ -15,8 +15,9 @@ To use Supermemory with Pipecat, install the required dependencies: pip install supermemory-pipecat ``` -Version 0.2.0 uses the Supermemory Python SDK v5. If upgrading from 0.1.x, -replace `user_id=` with `namespace=` when constructing the service. +Version 0.2.0 uses the Supermemory Python SDK v5. Keep using `user_id=`; +the service passes that value as the namespace for profile, search, and writes. +`session_id` is passed as the v5 document `id` (previously `custom_id`). Set up your API key as an environment variable: @@ -35,7 +36,7 @@ from supermemory_pipecat import InputParams, SupermemoryPipecatService memory = SupermemoryPipecatService( api_key=os.getenv("SUPERMEMORY_API_KEY"), - namespace="unique_user_id", + user_id="unique_user_id", session_id="session_123", params=InputParams( mode="full", # "profile" | "query" | "full" @@ -132,7 +133,7 @@ from supermemory_pipecat import SupermemoryPipecatService # Auto-detection works out of the box memory = SupermemoryPipecatService( api_key=os.getenv("SUPERMEMORY_API_KEY"), - namespace="unique_user_id", + user_id="unique_user_id", ) ``` @@ -169,7 +170,7 @@ You remember information from past conversations and use it to provide personali Keep responses brief and conversational.""" -async def run_bot(websocket_client, namespace: str, session_id: str): +async def run_bot(websocket_client, user_id: str, session_id: str): transport = FastAPIWebsocketTransport( websocket=websocket_client, params=FastAPIWebsocketParams( @@ -188,7 +189,7 @@ async def run_bot(websocket_client, namespace: str, session_id: str): # Supermemory memory service memory = SupermemoryPipecatService( - namespace=namespace, + user_id=user_id, session_id=session_id, params=InputParams( mode="full", @@ -224,7 +225,7 @@ async def run_bot(websocket_client, namespace: str, session_id: str): @app.websocket("/ws") async def websocket_endpoint(websocket: WebSocket): await websocket.accept() - await run_bot(websocket, namespace="alice", session_id="session-123") + await run_bot(websocket, user_id="alice", session_id="session-123") if __name__ == "__main__": diff --git a/packages/pipecat-sdk-python/Agents.md b/packages/pipecat-sdk-python/Agents.md index ca01bd22..f218be88 100644 --- a/packages/pipecat-sdk-python/Agents.md +++ b/packages/pipecat-sdk-python/Agents.md @@ -20,7 +20,7 @@ Place `SupermemoryPipecatService` between context aggregator and LLM in the pipe from supermemory_pipecat import SupermemoryPipecatService memory = SupermemoryPipecatService( - namespace="user-123", # Required: scopes all memory calls + user_id="user-123", # Required: scopes all memory calls session_id="session-456", # Optional: groups conversations ) @@ -41,7 +41,7 @@ pipeline = Pipeline([ ```python memory = SupermemoryPipecatService( api_key="...", # Or use SUPERMEMORY_API_KEY env var - namespace="user-123", + user_id="user-123", session_id="session-456", params=SupermemoryPipecatService.InputParams( search_limit=10, # Max memories to retrieve @@ -68,5 +68,5 @@ memory = SupermemoryPipecatService( ## Boundaries - Always place memory service after `context_aggregator.user()` and before `llm` -- Always provide `namespace` - it's required +- Always provide `user_id` - it's required - Never hardcode API keys in code - use environment variables diff --git a/packages/pipecat-sdk-python/README.md b/packages/pipecat-sdk-python/README.md index e3b43215..1d692d7d 100644 --- a/packages/pipecat-sdk-python/README.md +++ b/packages/pipecat-sdk-python/README.md @@ -8,9 +8,9 @@ Memory-enhanced conversational AI pipelines with [Supermemory](https://supermemo pip install supermemory-pipecat ``` -Version 0.2.0 requires the Supermemory Python SDK v5. When upgrading from -0.1.x, pass `namespace=` instead of `user_id=`. Use the same namespace for -all memory operations belonging to one user, project, or tenant. +Version 0.2.0 requires the Supermemory Python SDK v5. Keep using `user_id=`; +the service passes that value as the namespace for profile, search, and writes. +`session_id` is passed as the v5 document `id` (previously `custom_id`). ## Quick Start @@ -23,7 +23,7 @@ from supermemory_pipecat import SupermemoryPipecatService # Create memory service memory = SupermemoryPipecatService( api_key=os.getenv("SUPERMEMORY_API_KEY"), - namespace="user-123", # Required: scopes all memory calls + user_id="user-123", # Required: scopes all memory calls session_id="conversation-456", # Optional: groups memories by session ) @@ -49,7 +49,7 @@ pipeline = Pipeline([ | Parameter | Type | Required | Description | | ------------ | ----------- | -------- | ---------------------------------------------------------- | -| `namespace` | str | **Yes** | Namespace for memory scoping (for example, a user ID) | +| `user_id` | str | **Yes** | User identifier passed as the v5 namespace for memory scoping | | `session_id` | str | No | Session/conversation ID for grouping memories | | `api_key` | str | No | Supermemory API key (or set `SUPERMEMORY_API_KEY` env var) | | `params` | InputParams | No | Advanced configuration | @@ -61,7 +61,7 @@ pipeline = Pipeline([ from supermemory_pipecat import SupermemoryPipecatService memory = SupermemoryPipecatService( - namespace="user-123", + user_id="user-123", session_id="conv-456", params=SupermemoryPipecatService.InputParams( search_limit=10, # Max memories to retrieve @@ -151,7 +151,7 @@ async def websocket_endpoint(websocket: WebSocket): # Supermemory memory service memory = SupermemoryPipecatService( - namespace="alice", + user_id="alice", session_id="session-123", ) diff --git a/packages/pipecat-sdk-python/src/supermemory_pipecat/__init__.py b/packages/pipecat-sdk-python/src/supermemory_pipecat/__init__.py index 0838fae8..7377900e 100644 --- a/packages/pipecat-sdk-python/src/supermemory_pipecat/__init__.py +++ b/packages/pipecat-sdk-python/src/supermemory_pipecat/__init__.py @@ -10,7 +10,7 @@ Example: # Create memory service memory = SupermemoryPipecatService( api_key=os.getenv("SUPERMEMORY_API_KEY"), - namespace="user-123", + user_id="user-123", ) # Add to Pipecat pipeline diff --git a/packages/pipecat-sdk-python/src/supermemory_pipecat/service.py b/packages/pipecat-sdk-python/src/supermemory_pipecat/service.py index 262fcec3..3b71243f 100644 --- a/packages/pipecat-sdk-python/src/supermemory_pipecat/service.py +++ b/packages/pipecat-sdk-python/src/supermemory_pipecat/service.py @@ -133,7 +133,7 @@ class SupermemoryPipecatService(FrameProcessor): memory = SupermemoryPipecatService( api_key=os.getenv("SUPERMEMORY_API_KEY"), - namespace="user-123", + user_id="user-123", ) ``` """ @@ -159,7 +159,7 @@ class SupermemoryPipecatService(FrameProcessor): self, *, api_key: Optional[str] = None, - namespace: str, + user_id: str, session_id: Optional[str] = None, params: Optional[InputParams] = None, base_url: Optional[str] = None, @@ -168,13 +168,13 @@ class SupermemoryPipecatService(FrameProcessor): Args: api_key: Supermemory API key. Falls back to SUPERMEMORY_API_KEY env var. - namespace: The user, project, or tenant namespace for memory scoping. + user_id: User identifier passed as the v5 namespace for memory scoping. session_id: Session/conversation ID for grouping memories. params: Configuration parameters for memory retrieval. base_url: Optional custom base URL for Supermemory API. Raises: - ConfigurationError: If API key or namespace is missing. + ConfigurationError: If API key or user_id is missing. """ super().__init__() @@ -184,10 +184,11 @@ class SupermemoryPipecatService(FrameProcessor): "API key is required. Provide api_key parameter or set SUPERMEMORY_API_KEY environment variable." ) - if not namespace: - raise ConfigurationError("namespace is required") + if not user_id: + raise ConfigurationError("user_id is required") - self.namespace = namespace + self.user_id = user_id + self.container_tag = user_id self.session_id = session_id self.params = params or SupermemoryPipecatService.InputParams() @@ -233,9 +234,9 @@ class SupermemoryPipecatService(FrameProcessor): if include_profile and include_search: profile_response, search_response = await asyncio.gather( - self._supermemory_client.profile(self.namespace), + self._supermemory_client.profile(self.container_tag), self._supermemory_client.search( - self.namespace, + self.container_tag, query=query, search_mode="memories", limit=self.params.search_limit, @@ -243,10 +244,10 @@ class SupermemoryPipecatService(FrameProcessor): ), ) elif include_profile: - profile_response = await self._supermemory_client.profile(self.namespace) + profile_response = await self._supermemory_client.profile(self.container_tag) elif include_search: search_response = await self._supermemory_client.search( - self.namespace, + self.container_tag, query=query, search_mode="memories", limit=self.params.search_limit, @@ -292,7 +293,7 @@ class SupermemoryPipecatService(FrameProcessor): if self.session_id: add_params["id"] = self.session_id - await self._supermemory_client.add(self.namespace, **add_params) + await self._supermemory_client.add(self.container_tag, **add_params) except Exception as e: raise MemoryStorageError("Failed to store messages", e) from e diff --git a/packages/pipecat-sdk-python/src/supermemory_pipecat/utils.py b/packages/pipecat-sdk-python/src/supermemory_pipecat/utils.py index 2b09f058..b8bfd697 100644 --- a/packages/pipecat-sdk-python/src/supermemory_pipecat/utils.py +++ b/packages/pipecat-sdk-python/src/supermemory_pipecat/utils.py @@ -70,11 +70,7 @@ def format_relative_time(iso_timestamp: str) -> str: def _field(item: Any, *names: str, default: Any = None) -> Any: - """Read a field from a dict or pydantic/SDK model. - - Accepts camelCase and snake_case names so helpers work with both raw JSON - dicts and generated SDK response models. - """ + """Accept alternative field names across raw JSON and SDK response models.""" if item is None: return default if isinstance(item, dict): @@ -134,7 +130,6 @@ def deduplicate_memories( def unique_search(results: List[Any]) -> List[Any]: out: List[Any] = [] for r in results: - # v5 search results can contain a memory or a source chunk. memory = _memory_text(r) key = comparison_key(memory) if key and key not in seen: diff --git a/packages/pipecat-sdk-python/tests/test_empty_profile.py b/packages/pipecat-sdk-python/tests/test_empty_profile.py index 5215baa2..d7e21c24 100644 --- a/packages/pipecat-sdk-python/tests/test_empty_profile.py +++ b/packages/pipecat-sdk-python/tests/test_empty_profile.py @@ -97,6 +97,7 @@ def _install_test_stubs() -> None: _install_test_stubs() from supermemory_pipecat.service import SupermemoryPipecatService +from supermemory_pipecat.exceptions import ConfigurationError class _MockSupermemoryClient: @@ -107,8 +108,69 @@ class _MockSupermemoryClient: class TestSupermemoryPipecatNullProfile(unittest.IsolatedAsyncioTestCase): + def test_user_id_and_legacy_attribute_are_preserved(self) -> None: + service = SupermemoryPipecatService(api_key="mock_key", user_id="user-123") + + self.assertEqual(service.user_id, "user-123") + self.assertEqual(service.container_tag, "user-123") + + def test_empty_user_id_is_rejected(self) -> None: + with self.assertRaisesRegex(ConfigurationError, "^user_id is required$"): + SupermemoryPipecatService(api_key="mock_key", user_id="") + + async def test_real_v5_client_serializes_user_id_as_namespace(self) -> None: + import json + + import httpx + from supermemory import AsyncSupermemory + + requests = {} + + def respond(request): + requests[request.url.path] = json.loads(request.content) + if request.url.path.endswith("/profile"): + payload = {"profile": {"static": [{"memory": "Likes tea"}], "dynamic": []}} + elif request.url.path.endswith("/search"): + payload = {"results": [{"memory": "Plans a trip"}]} + else: + payload = {"id": "conversation-456"} + return httpx.Response(200, json=payload) + + async with AsyncSupermemory( + api_key="mock_key", + base_url="https://example.test", + http_client=httpx.AsyncClient(transport=httpx.MockTransport(respond)), + ) as client: + service = SupermemoryPipecatService( + api_key="mock_key", user_id="user-123", session_id="conversation-456" + ) + service._supermemory_client = client + + result = await service._retrieve_memories("Where am I going?") + await service._store_messages([{"role": "user", "content": "Hello"}]) + + self.assertEqual(result["profile"], {"static": ["Likes tea"], "dynamic": []}) + self.assertEqual(result["search_results"][0].memory, "Plans a trip") + self.assertEqual( + requests, + { + "/ns/user-123/profile": {}, + "/ns/user-123/search": { + "query": "Where am I going?", + "searchMode": "memories", + "limit": 10, + "threshold": 0.1, + }, + "/ns/user-123/document": { + "content": '[{"role": "user", "content": "Hello"}]', + "metadata": {"platform": "pipecat"}, + "id": "conversation-456", + }, + }, + ) + async def test_retrieve_memories_handles_null_profile(self) -> None: - service = SupermemoryPipecatService(api_key="mock_key", namespace="new_user_123") + service = SupermemoryPipecatService(api_key="mock_key", user_id="new_user_123") client = _MockSupermemoryClient( profile_response=SimpleNamespace(profile=None), @@ -135,7 +197,7 @@ class TestSupermemoryPipecatNullProfile(unittest.IsolatedAsyncioTestCase): ) async def test_full_mode_reads_v5_profile_objects_and_search_results(self) -> None: - service = SupermemoryPipecatService(api_key="mock_key", namespace="user-123") + service = SupermemoryPipecatService(api_key="mock_key", user_id="user-123") client = _MockSupermemoryClient( profile_response=SimpleNamespace( profile=SimpleNamespace( @@ -171,7 +233,7 @@ class TestSupermemoryPipecatNullProfile(unittest.IsolatedAsyncioTestCase): async def test_query_mode_only_searches(self) -> None: service = SupermemoryPipecatService( api_key="mock_key", - namespace="user-123", + user_id="user-123", params=SupermemoryPipecatService.InputParams( mode="query", search_limit=3, search_threshold=0.5 ), @@ -194,7 +256,7 @@ class TestSupermemoryPipecatNullProfile(unittest.IsolatedAsyncioTestCase): async def test_profile_mode_only_reads_profile(self) -> None: service = SupermemoryPipecatService( api_key="mock_key", - namespace="user-123", + user_id="user-123", params=SupermemoryPipecatService.InputParams(mode="profile"), ) client = _MockSupermemoryClient( @@ -213,7 +275,7 @@ class TestSupermemoryPipecatNullProfile(unittest.IsolatedAsyncioTestCase): async def test_store_uses_namespace_and_document_id(self) -> None: service = SupermemoryPipecatService( - api_key="mock_key", namespace="user-123", session_id="conversation-456" + api_key="mock_key", user_id="user-123", session_id="conversation-456" ) client = _MockSupermemoryClient() service._supermemory_client = client @@ -231,7 +293,7 @@ class TestSupermemoryPipecatNullProfile(unittest.IsolatedAsyncioTestCase): fact = "User likes machine learning projects" service = SupermemoryPipecatService( api_key="mock_key", - namespace="user-123", + user_id="user-123", session_id="conversation-456", params=SupermemoryPipecatService.InputParams(mode="query"), ) @@ -264,7 +326,7 @@ class TestSupermemoryPipecatNullProfile(unittest.IsolatedAsyncioTestCase): fact = "User's repo is at C:" + backslash + "Users" + backslash + "alice" service = SupermemoryPipecatService( api_key="mock_key", - namespace="user-123", + user_id="user-123", session_id="conversation-456", params=SupermemoryPipecatService.InputParams(inject_mode="system"), )