# SDK - Official MagickMind Python Client

> Python SDK for the Bifrost API gateway. For **service users** (backends/bots) to authenticate and interact with MagickMind on behalf of their end users.

**Python runtime required** - Uses `httpx` and `centrifuge-python`. Runs in Python backends, CLI, scripts, desktop apps.

## Architecture: Service Users vs End Users

```
┌─────────────────────────────────────────────────────────────────┐
│  YOUR SYSTEM                                                     │
│  ┌──────────────┐     ┌──────────────────────────────────────┐  │
│  │  Your End    │────▶│  Your Backend (Service User)         │  │
│  │  Users       │     │  - Authenticates with Bifrost (JWT)  │  │
│  │  (own auth)  │◀────│  - Manages end users via end-user svc│  │
│  └──────────────┘     │  - Calls Bifrost with sender_id param│  │
│                       └──────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼ SDK / REST
┌─────────────────────────────────────────────────────────────────┐
│  MAGICKMIND                                                      │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────────────┐ │
│  │ Bifrost  │──│ Xavier   │──│ Cortex   │──│ Centrifugo       │ │
│  │ (gateway)│  │ (chat)   │  │ (AI)     │  │ (streaming)      │ │
│  └──────────┘  └──────────┘  └──────────┘  └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```

**Key concepts:**
- **Service User**: Your backend/bot that authenticates with Bifrost via JWT (email/password login)
- **End User**: YOUR users, managed by YOU. Created via `end-user` gRPC service. No direct Bifrost auth.
- **sender_id/user_id**: End user identifier passed in API calls (e.g., chat requests)

**Centrifugo channel format:** `personal:{end_user_id}#{service_account_id}`
- Service subscribes to channels for its end users
- Streams responses back to end users via YOUR transport (WebSocket, Telegram, etc.)

## Purpose & Responsibilities

- **Authentication**: Service user email/password → JWT with auto-refresh
- **HTTP Client**: Authenticated requests to Bifrost REST API
- **Typed Resources**: v1/chat, mindspace, artifact, end-user, etc. with Pydantic models
- **Real-time**: Centrifugo pub/sub for streaming responses to relay to your end users
- **End User Management**: Create/query/update end users scoped to your service account

## Key Files & Entry Points

| Need | Look Here |
|------|-----------|
| Main client | `magick_mind/client.py` - `MagickMind` class |
| HTTP client | `magick_mind/http/client.py` - authenticated requests |
| Real-time | `magick_mind/realtime/client.py` - Centrifugo subscriptions |
| Auth provider | `magick_mind/auth/email_password.py` - JWT login/refresh |
| Chat resource | `magick_mind/resources/v1/chat.py` |
| Mindspace resource | `magick_mind/resources/v1/mindspace.py` |
| Artifact resource | `magick_mind/resources/v1/artifact.py` |
| Request/Response models | `magick_mind/models/v1/` |
| Exceptions | `magick_mind/exceptions.py` |
| Examples | `examples/` - usage patterns |

## Quick Start

```python
from magick_mind import MagickMind

# Initialize client as SERVICE USER (your backend)
client = MagickMind(
    email="myservice@example.com",  # Your service account
    password="secret",
    base_url="https://bifrost.magickmind.io"
)

# Create/get an end user (YOUR user, e.g., from your auth system)
end_user = await client.v1.end_user.create_or_get(
    external_id="your-user-123",  # Your user ID
    metadata={"name": "Alice"}
)

# Send chat message ON BEHALF OF your end user
response = await client.v1.chat.send(
    mindspace_id="ms-123",
    sender_id=end_user.id,  # End user ID
    message="Hello, world!"
)

# Subscribe to Centrifugo for this end user's responses
class MyHandler:
    async def on_message(self, user_id: str, payload: dict):
        # Relay this to YOUR end user via your transport
        print(f"Response for {user_id}: {payload}")

await client.realtime.connect(events=MyHandler())
await client.realtime.subscribe(end_user.id)  # Subscribe for this end user
```

## Service-Specific Patterns

### Authentication (Auto-Refresh)
```python
# magick_mind/auth/email_password.py
# JWT refreshed automatically (expires_in - 10s buffer)
client = MagickMind(email="...", password="...")
# Token injected into all requests automatically

# Check auth status
if client.is_authenticated():
    ...
```

### HTTP Requests
```python
# Low-level (magick_mind/http/client.py)
response = await client.http.request("GET", "/v1/chat/messages")

# High-level via resources
messages = await client.v1.mindspace.get_messages(
    mindspace_id="ms-123",
    after_id="msg-456"  # For pagination/sync
)
```

### Real-time Subscriptions
```python
# magick_mind/realtime/client.py
# Channel format: personal:{end_user_id}#{service_account_id}
# Service subscribes to channels for its end users

class EventHandler:
    async def on_message(self, end_user_id: str, payload: dict):
        # Relay to YOUR end user via your transport (WebSocket, Telegram, etc.)
        your_websocket.send(end_user_id, payload)
    
    async def on_error(self, error: Exception):
        # Handle connection error
        pass

await client.realtime.connect(events=EventHandler())
await client.realtime.subscribe(end_user_id="your-end-user-123")
```

### History Sync Pattern
```python
# Get messages since last known, then subscribe for real-time
last_known_id = get_last_message_id_from_db()
messages = await client.v1.mindspace.get_messages(
    mindspace_id="ms-123",
    after_id=last_known_id
)
save_to_db(messages)

# Now subscribe for new messages for this end user
await client.realtime.subscribe(end_user_id="your-end-user-123")
```

### Error Handling
```python
from magick_mind.exceptions import (
    MagickMindError,      # Base exception
    AuthenticationError,   # Login failed
    TokenExpiredError,     # Token refresh failed
    APIError,              # API returned error
    RateLimitError,        # Rate limited
)

try:
    await client.v1.chat.send(...)
except AuthenticationError:
    # Re-authenticate
except APIError as e:
    print(f"API error: {e.message}, status: {e.status_code}")
```

## Testing

```bash
# Run all tests
uv run pytest

# With coverage
uv run pytest --cov=magick_mind
```

**Test structure**:
```
tests/
├── conftest.py              # Fixtures: mock_auth, test_client, schema_registry
├── test_auth.py             # Auth provider tests
├── test_resources/          # Resource client tests
├── test_realtime/           # Real-time subscription tests
└── contract/                # OpenAPI contract validation
    ├── openapi.json         # API schema
    └── test_contracts.py    # Payload validation
```

**Fixtures** (`conftest.py`):
- `mock_client` - httpx.MockTransport for HTTP mocking
- `authenticated_client` - Pre-authenticated MagickMind instance
- `mock_auth` - Mocked auth provider
- `schema_registry` - OpenAPI schema validation

**Mocking patterns**:
```python
# Mock auth
@patch("magick_mind.auth.email_password._login")
def test_auth(mock_login):
    mock_login.return_value = {"access_token": "...", "expires_in": 3600}
    ...

# Mock HTTP with pytest-httpx
def test_chat(httpx_mock):
    httpx_mock.add_response(json={"message_id": "123"})
    ...
```

## Configuration

```python
from magick_mind import MagickMind

client = MagickMind(
    email="service@example.com",
    password="secret",
    base_url="https://bifrost.magickmind.io",  # Or from BIFROST_BASE_URL env
)
```

**Environment variables**:
```bash
BIFROST_BASE_URL=https://bifrost.magickmind.io
# Credentials typically passed directly, not via env
```

## Dependencies

Key packages from `pyproject.toml`:
- `httpx>=0.27` - HTTP client
- `pydantic>=2.0` - Models and validation
- `centrifuge>=0.9` - Real-time WebSocket
- `pydantic-settings` - Configuration

**Dev dependencies**:
- `pytest`, `pytest-httpx`, `pytest-asyncio` - Testing

## Common Commands

```bash
# Install dependencies
uv sync

# Run tests
uv run pytest -v

# Type check
uv run pyright

# Format
uv run ruff format .
```

## Gotchas

1. **Service users only** - This SDK authenticates service accounts (backends/bots), not end users. End users are managed via `end-user` service and passed as `sender_id`/`user_id` params.
2. **Python only** - Uses httpx/centrifuge-python; for other languages, use Bifrost REST directly
3. **Async preferred** - Most methods are async; use `asyncio.to_thread` for sync contexts
4. **Auto-refresh timing** - Token refreshes 10s before expiry; long-running ops may still fail
5. **Real-time before REST** - For chat, subscribe to channel BEFORE sending message to catch response
6. **Contract tests** - Tests validate against `tests/contract/openapi.json`; update if API changes
7. **Channel format** - `personal:{end_user_id}#{service_account_id}` - service subscribes for its end users
8. **No direct frontend access** - End users don't call Bifrost directly; your backend relays via this SDK
