supermemory/packages/cartesia-sdk-python/src/supermemory_cartesia/utils.py
sreedharsreeram e672af6b3d Supermemory-Cartesia SDK (#744)
### TL;DR

Added Python SDK for integrating Supermemory with Cartesia Line voice agents, enabling persistent memory capabilities.

### What changed?

Created a new Python SDK package (`supermemory_cartesia`) that provides:

- `SupermemoryCartesiaAgent` wrapper class that enhances Cartesia Line agents with memory capabilities
- Memory retrieval and storage functionality that integrates with the Supermemory API
- Utility functions for memory formatting, deduplication, and time formatting
- Custom exception classes for error handling
- Comprehensive documentation and type hints

The implementation includes:
- Memory enrichment for user queries
- Automatic storage of conversation history
- Configurable memory retrieval modes (profile, query, full)
- Background processing to avoid blocking the main conversation flow

### How to test?

```python
from supermemory_cartesia import SupermemoryCartesiaAgent
from line.llm_agent import LlmAgent, LlmConfig
import os

# Create base LLM agent
base_agent = LlmAgent(
    model="gemini/gemini-2.5-flash-preview-09-2025",
    config=LlmConfig(
        system_prompt="You are a helpful assistant.",
        introduction="Hello!"
    )
)

# Wrap with Supermemory
memory_agent = SupermemoryCartesiaAgent(
    agent=base_agent,
    api_key=os.getenv("SUPERMEMORY_API_KEY"),
    user_id="user-123",
)

# Use memory_agent in your Cartesia Line application
```

### Why make this change?

This SDK enables Cartesia Line voice agents to maintain persistent memory across conversations, enhancing user experience by:

1. Providing contextual awareness of past interactions
2. Remembering user preferences and important information
3. Reducing repetition in conversations
4. Creating more personalized and natural voice interactions

The integration is designed to be lightweight and non-blocking, ensuring that memory operations don't impact the responsiveness of voice interactions.
2026-04-15 16:27:23 +00:00

134 lines
4.1 KiB
Python

"""Utility functions for Supermemory Cartesia integration."""
from datetime import datetime, timezone
from typing import Any, Dict, List, Union
def get_last_user_message(messages: List[Dict[str, str]]) -> str | None:
"""Extract the last user message content from a list of messages."""
for msg in reversed(messages):
if msg["role"] == "user":
return msg["content"]
return None
def format_relative_time(iso_timestamp: str) -> str:
"""Convert ISO timestamp to relative time string.
Format rules:
- [just now] - within 30 minutes
- [Xmins ago] - 30-60 minutes
- [X hrs ago] - less than 1 day
- [Xd ago] - less than 1 week
- [X Jul] - more than 1 week, same year
- [X Jul, 2023] - different year
"""
try:
dt = datetime.fromisoformat(iso_timestamp.replace("Z", "+00:00"))
now = datetime.now(timezone.utc)
diff = now - dt
seconds = diff.total_seconds()
minutes = seconds / 60
hours = seconds / 3600
days = seconds / 86400
if minutes < 30:
return "just now"
elif minutes < 60:
return f"{int(minutes)}mins ago"
elif hours < 24:
return f"{int(hours)} hrs ago"
elif days < 7:
return f"{int(days)}d ago"
elif dt.year == now.year:
return f"{dt.day} {dt.strftime('%b')}"
else:
return f"{dt.day} {dt.strftime('%b')}, {dt.year}"
except Exception:
return ""
def deduplicate_memories(
static: List[str],
dynamic: List[str],
search_results: List[Dict[str, Any]],
) -> Dict[str, Union[List[str], List[Dict[str, Any]]]]:
"""Deduplicate memories. Priority: static > dynamic > search.
Args:
static: List of static memory strings.
dynamic: List of dynamic memory strings.
search_results: List of search result dicts with 'memory' and 'updatedAt'.
"""
seen = set()
def unique_strings(memories: List[str]) -> List[str]:
out = []
for m in memories:
if m not in seen:
seen.add(m)
out.append(m)
return out
def unique_search(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
out = []
for r in results:
memory = r.get("memory", "")
if memory and memory not in seen:
seen.add(memory)
out.append(r)
return out
return {
"static": unique_strings(static),
"dynamic": unique_strings(dynamic),
"search_results": unique_search(search_results),
}
def format_memories_to_text(
memories: Dict[str, Union[List[str], List[Dict[str, Any]]]],
system_prompt: str = "Based on previous conversations, I recall:\n\n",
include_static: bool = True,
include_dynamic: bool = True,
include_search: bool = True,
) -> str:
"""Format deduplicated memories into a text string for injection.
Search results include temporal context (e.g., '3d ago') from updatedAt.
"""
sections = []
static = memories["static"]
dynamic = memories["dynamic"]
search_results = memories["search_results"]
if include_static and static:
sections.append("## User Profile (Persistent)")
sections.append("\n".join(f"- {item}" for item in static))
if include_dynamic and dynamic:
sections.append("## Recent Context")
sections.append("\n".join(f"- {item}" for item in dynamic))
if include_search and search_results:
sections.append("## Relevant Memories")
lines = []
for item in search_results:
if isinstance(item, dict):
memory = item.get("memory", "")
updated_at = item.get("updatedAt", "")
time_str = format_relative_time(updated_at) if updated_at else ""
if time_str:
lines.append(f"- [{time_str}] {memory}")
else:
lines.append(f"- {memory}")
else:
lines.append(f"- {item}")
sections.append("\n".join(lines))
if not sections:
return ""
return f"{system_prompt}\n" + "\n\n".join(sections)