# primitif

Python SDK for [Primitif](https://primitif.ai) — give your AI agents email and human approval.

```
pip install primitif
```

## Setup

Set your API key as an env var and you're good to go:

```bash
export PRIMITIF_API_KEY="pk_live_..."
```

## Mail

Create a temporary mailbox, send and receive emails, then throw it away.

```python
from primitif import Primitif

with Primitif() as p:
    mb = p.mail.create_mailbox(name="support-agent", ttl=3600)
    print(mb.address)  # support-agent-x7k2@mail.primitif.ai

    # send an email
    mb.send(to="user@example.com", subject="Hey", body_text="Following up on your ticket.")

    # check inbox
    for msg in mb.inbox():
        email = mb.read(msg.id)
        print(email.from_address, email.subject, email.body_text)
        # reply to it
        mb.reply(msg.id, body_text="Thanks, we're on it.")

    # done — delete the mailbox
    mb.delete()
```

If you already have a mailbox token, use it directly:

```python
from primitif import Mailbox

with Mailbox(token="mb_tok_...") as mb:
    mb.send(to="someone@example.com", subject="Hi", body_text="Hello")
```

Tokens can be persisted for crash recovery:

```python
mb = p.mail.create_mailbox(name="agent")
save_to_db(mb.token)  # persist the token

# later, reconnect:
mb = Mailbox(token=load_from_db())
```

### Send with human approval

Emails can be held for human approval before sending:

```python
result = mb.send(
    to="ceo@bigclient.com",
    subject="Contract proposal",
    body_text="Please find attached...",
    require_approval=True,
)
print(result.status)        # "held"
print(result.approval_url)  # link for a human to approve/reject
```

### Pagination

All list methods return a `PaginatedList` that you can iterate directly,
or use `auto_paging_iter()` to fetch all pages automatically:

```python
for msg in mb.inbox().auto_paging_iter():
    print(msg.subject)
```

## Approval

Gate your agent's tools behind human approval.

```python
from primitif import Primitif

p = Primitif()

# Protect sensitive tools with a decorator
@p.approval.require_approval()
def send_invoice(client, amount):
    billing.send(client, amount)

@p.approval.require_approval()
def delete_account(user_id):
    accounts.delete(user_id)

# Agent calls the tool — gets a pending approval, carries on
result = send_invoice("Acme", 45000)
print(result.status)        # "pending"
print(result.approval_url)  # human reviews here
```

Without the decorator:

```python
request = p.approval.create_request(
    action="Delete user account #1234",
    context={"user_id": 1234, "reason": "requested by user"},
    callback_url="https://myapp.com/webhooks/approval",
)
```

## Webhooks

When the human approves, `dispatch()` automatically executes the right tool
with the original arguments:

```python
from flask import Flask, request
from primitif import Primitif, verify_webhook, InvalidSignature

app = Flask(__name__)
p = Primitif()

WEBHOOK_SECRET = "whsec_..."

@p.approval.require_approval()
def send_invoice(client, amount):
    billing.send(client, amount)

@app.post("/webhooks/approval")
def handle_approval():
    try:
        event = verify_webhook(
            secret=WEBHOOK_SECRET,
            signature=request.headers["X-Webhook-Signature"],
            timestamp=request.headers["X-Webhook-Timestamp"],
            body=request.data,
        )
    except InvalidSignature:
        return "", 401

    p.approval.dispatch(event)
    return "", 200
```

> Set your webhook URL in the [Console](https://dash.primitif.ai) under Settings → Webhooks.

## Error handling

All errors inherit from `PrimitifError`, so you can catch broadly or narrowly:

```python
from primitif import PrimitifError, AuthError, NetworkError
from primitif.mail import MailRateLimitError

try:
    mb.send(to="user@test.com", subject="Hi", body_text="Hello")
except MailRateLimitError as e:
    print(f"Slow down — retry after {e.retry_after}s")
except NetworkError:
    print("Network issue — timeout or DNS failure")
except AuthError:
    print("Bad credentials")
except PrimitifError:
    print("Something went wrong")
```

## License

MIT
