fix(pipecat): preserve user_id interface with v5 namespace calls

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
sreedharsreeram 2026-11-03 03:09:26 +00:00
parent cbf55e59a8
commit c0320ca833
7 changed files with 102 additions and 43 deletions

View file

@ -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__":

View file

@ -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

View file

@ -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",
)

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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"),
)