mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-11-14 09:01:45 +00:00
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:
parent
cbf55e59a8
commit
c0320ca833
7 changed files with 102 additions and 43 deletions
|
|
@ -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__":
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue