# Zymemory Python SDK

**Official Python client for the Zymemory API** - Long-term memory for AI applications.

Zymemory provides a powerful memory system for AI assistants, chatbots, and agents, enabling them to remember and recall information across conversations.

[![PyPI version](https://badge.fury.io/py/zymemory.svg)](https://pypi.org/project/zymemory/)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Features

- **Semantic Memory Search** - Find relevant memories using natural language
- **Memory Linking** - Connect related memories for graph-based recall
- **Conversation Storage** - Automatically cluster conversations into structured memories
- **Smart Clustering** - AI-powered organization of information
- **Multi-tenant** - Secure isolation for different users and organizations
- **Fast & Scalable** - Built on high-performance infrastructure

## Installation

```bash
pip install zymemory
```

## Quick Start

```python
from zymemory import ZymemoryClient

# Initialize client
client = ZymemoryClient(
    api_key="your-org-api-key",
    org_email="your-org@example.com",
    user_token="user-specific-token"
)

# Search for memories
results = client.search("What are my coffee preferences?")
for memory in results.memories:
    print(f"{memory.content}")
    print(f"   Keywords: {', '.join(memory.keywords)}")

# Create a new memory
memory = client.create_memory("I prefer oat milk lattes in the morning")
print(f"Created memory #{memory.id}")

# Store a conversation
client.store_conversation(
    user_input="What's the weather today?",
    assistant_output="It's sunny and 72°F!"
)
```

## Authentication

Zymemory uses a two-tier authentication system:

1. **Organization API Key** - Identifies your organization
2. **User Token** - Identifies the end user

### Getting Started

1. **Get your organization API key** from the Zymemory dashboard
2. **Register your end users:**

```python
# First, initialize with just org credentials
client = ZymemoryClient(
    api_key="your-org-key",
    org_email="your-org@example.com"
)

# Register a new end user
user = client.register_user(
    external_id="user_123",  # Your system's user ID
    name="John Doe",
    email="john@example.com"
)

# Save the user token for future requests
print(f"User token: {user.token}")

# Now use it for all memory operations
client.user_token = user.token
```

## Usage Examples

### Memory Management

```python
from zymemory import ZymemoryClient

client = ZymemoryClient(
    api_key="your-key",
    org_email="org@example.com",
    user_token="user-token"
)

# Create memories
memory1 = client.create_memory("I love dark roast coffee")
memory2 = client.create_memory("My favorite cafe is Blue Bottle", auto_merge=True)

# List all memories with pagination
result = client.list_memories(page=1, page_size=20)
print(f"Total memories: {result.total}")
for memory in result.memories:
    print(f"  {memory.id}: {memory.content}")

# Delete a memory
client.delete_memory(memory1.id)
```

### Searching Memories

```python
# Simple search
results = client.search("coffee preferences", top_k=5)

# Process results
print(f"Found {len(results.memories)} memories:")
for memory in results.memories:
    print(f"  {memory.content}")
    print(f"     Keywords: {memory.keywords}")
    print(f"     Connections: {memory.link_count}")

# Get just the memories (exclude unassigned conversations)
memories = client.search_memories_only("favorite foods", top_k=10)
```

### Memory Linking

```python
# Create links between related memories
client.create_link(source_id=1, destination_id=2)

# Get connected memories
links = client.get_memory_radius(memory_id=1, depth=2)
print(f"Outgoing links: {len(links['outgoing'])}")
print(f"Incoming links: {len(links['incoming'])}")

# Explore the memory graph
for link in links['outgoing']:
    dest = link['destination']
    print(f"  → {dest['content']}")
```

### Conversation Storage

```python
# Store conversation turns
# The backend automatically processes these into structured memories
client.store_conversation(
    user_input="I want to learn Python",
    assistant_output="Great! Python is perfect for beginners..."
)

client.store_conversation(
    user_input="What's a good first project?",
    assistant_output="Try building a to-do list app..."
)

# Later, search across conversations
results = client.search("learning programming")
# The system will have clustered related conversations into memories
```

### Building a Chatbot with Memory

```python
from zymemory import ZymemoryClient
from anthropic import Anthropic  # or openai, etc.

class MemoryChatbot:
    def __init__(self, zymemory_client, llm_client):
        self.memory = zymemory_client
        self.llm = llm_client

    def chat(self, user_message):
        # 1. Search for relevant memories
        context = self.memory.search(user_message, top_k=5)

        # 2. Build prompt with memory context
        memory_context = "\\n".join([
            f"- {m.content}" for m in context.memories[:3]
        ])

        system_prompt = f"""You are a helpful assistant with long-term memory.

Here's what you remember:
{memory_context}

Use this information to provide personalized responses."""

        # 3. Get LLM response
        response = self.llm.messages.create(
            model="claude-3-haiku-20240307",
            system=system_prompt,
            messages=[{"role": "user", "content": user_message}]
        )

        assistant_message = response.content[0].text

        # 4. Store conversation for future recall
        self.memory.store_conversation(user_message, assistant_message)

        return assistant_message

# Use it
memory_client = ZymemoryClient(api_key="...", org_email="...", user_token="...")
llm = Anthropic(api_key="...")
bot = MemoryChatbot(memory_client, llm)

print(bot.chat("What did I say about coffee?"))
```

### Bulk Operations

```python
# Get all memories (useful for export)
all_memories = client.get_all_memories(max_pages=10)
print(f"Total: {len(all_memories)} memories")

# Export to JSON
import json
with open("memories_backup.json", "w") as f:
    json.dump([{
        "id": m.id,
        "content": m.content,
        "keywords": m.keywords
    } for m in all_memories], f, indent=2)
```

## API Reference

### `ZymemoryClient`

#### Constructor

```python
ZymemoryClient(
    api_key: str,
    org_email: str,
    user_token: Optional[str] = None,
    api_url: str = "https://conv-service-273184263530.europe-west2.run.app",
    timeout: int = 30
)
```

#### Methods

##### Memory Management
- `create_memory(content: str, auto_merge: bool = False) -> Memory`
- `delete_memory(memory_id: int) -> dict`
- `list_memories(page: int = 1, page_size: int = 20) -> MemoryList`

##### Search & Retrieval
- `search(query: str, top_k: int = 5) -> SearchResult`
- `search_memories_only(query: str, top_k: int = 5) -> List[Memory]`

##### Memory Links
- `create_link(source_id: int, destination_id: int) -> dict`
- `get_memory_radius(memory_id: int, depth: int = 1) -> dict`

##### Conversations
- `store_conversation(user_input: str, assistant_output: str) -> dict`

##### User Management
- `register_user(external_id: str, name: str, email: Optional[str] = None) -> User`

##### Utilities
- `get_all_memories(max_pages: int = 10, page_size: int = 100) -> List[Memory]`

### Models

#### `Memory`
```python
@dataclass
class Memory:
    id: int
    content: str
    keywords: List[str]
    conversations: List[tuple[str, str]]
    created_at: Optional[str]
    link_count: Optional[int]
    metadata: Optional[Dict[str, Any]]
```

#### `SearchResult`
```python
@dataclass
class SearchResult:
    memories: List[Memory]
    unassigned: List[Dict[str, Any]]
```

#### `MemoryList`
```python
@dataclass
class MemoryList:
    memories: List[Memory]
    total: int
    page: int
    page_size: int
```

## Error Handling

```python
from zymemory import ZymemoryClient, ZymemoryError, AuthenticationError, APIError

client = ZymemoryClient(api_key="...", org_email="...", user_token="...")

try:
    memory = client.create_memory("Important information")
except AuthenticationError as e:
    print(f"Authentication failed: {e}")
except APIError as e:
    print(f"API error {e.status_code}: {e}")
except ZymemoryError as e:
    print(f"Zymemory error: {e}")
```

### Exception Hierarchy

- `ZymemoryError` - Base exception
  - `AuthenticationError` - Invalid credentials
  - `APIError` - API returned error response
  - `ValidationError` - Invalid request data
  - `NetworkError` - Connection/timeout issues
  - `RateLimitError` - Rate limit exceeded

## Advanced Features

### Custom API URL (Self-hosted)

```python
client = ZymemoryClient(
    api_key="your-key",
    org_email="org@example.com",
    user_token="user-token",
    api_url="https://your-self-hosted-instance.com"
)
```

### Per-Request User Override

```python
# Use different user token for specific requests
memory = client.create_memory("content", user_token="different-user-token")
```

### Request Timeout

```python
client = ZymemoryClient(
    api_key="your-key",
    org_email="org@example.com",
    timeout=60  # 60 seconds
)
```

## Development

### Running Tests

```bash
cd zymemory
pip install -e ".[dev]"
pytest tests/
```

### Type Checking

```bash
mypy zymemory/
```

### Code Formatting

```bash
black zymemory/
```

## Support

- **Documentation**: https://heymaple.app/docs
- **Issues**: https://github.com/ranjalii/zymemory/issues
- **Email**: contact@heymaple.app

## License

MIT License - see LICENSE file for details

## Changelog

### v0.1.0 (2024-03-18)
- Initial release
- Core memory management
- Search and retrieval
- Memory linking
- Conversation storage
- User registration

---

Built with by Hey Maple
