Metadata-Version: 2.4
Name: pacerelle
Version: 0.1.0a1
Summary: Python SDK for Pacerelle encrypted local agent relays.
Project-URL: Homepage, https://pacerelle.com
Project-URL: Documentation, https://pacerelle.com
Author: Pacerelle
License-Expression: MIT
License-File: LICENSE
Keywords: agents,ai,e2ee,local-agents,mcp,websocket
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: cryptography>=42.0.0
Requires-Dist: websockets>=12.0
Description-Content-Type: text/markdown

# Pacerelle Python SDK

Python agent client for Pacerelle encrypted local agent relays.

Use this SDK to connect a local Python process to Pacerelle, receive messages,
reply to conversations, drive widgets, and return encrypted files or media.

```bash
pip install pacerelle
```

> Alpha release: APIs may change before the first stable release. Production
> wheels bundle the native Signal runtime for the target platform.

## Before You Run

Create an agent in Pacerelle. The confirmation panel shows both
`Identifiant de l'agent` and `Jeton d'authentification`. Use
`Copier la configuration .env` to copy the required variables.

```bash
export PACERELLE_AGENT_ID="agent-id"
export PACERELLE_AGENT_TOKEN="agent-token"
```

On Windows PowerShell:

```powershell
$env:PACERELLE_AGENT_ID = "agent-id"
$env:PACERELLE_AGENT_TOKEN = "agent-token"
```

Published packages connect to the Pacerelle API by default. Local source builds
default to `http://localhost:8080` for development.

## Minimal Echo Agent

```python
import asyncio
import os

from pacerelle import AgentGatewayClient

client = AgentGatewayClient(
    token=os.environ["PACERELLE_AGENT_TOKEN"],
    agent_id=os.environ["PACERELLE_AGENT_ID"],
    e2ee=True,
)


async def handle(message, agent):
    await agent.send_message(
        conversation_id=message.conversation_id,
        to=message.from_id,
        reply_to_message_id=message.id,
        text=f"Received: {message.text}",
    )


client.on_message(handle)
asyncio.run(client.connect())
```

## Incoming Messages

The handler receives an `AgentMessage`:

```python
async def handle(message, agent):
    print(message.id)
    print(message.conversation_id)
    print(message.from_id)
    print(message.text)
    print(message.attachments)
    print(message.widget_response)
```

Use `message.from_id` as the `to` value when replying to the user.

## Sending Messages And Replies

Send a normal message:

```python
await agent.send_message(
    conversation_id=message.conversation_id,
    to=message.from_id,
    text="I can help with that.",
)
```

Reply to a specific user message:

```python
await agent.send_message(
    conversation_id=message.conversation_id,
    to=message.from_id,
    reply_to_message_id=message.id,
    text="Replying to your last message.",
)
```

## Running Your Own Agent Logic

```python
async def handle(message, agent):
    result = await run_my_agent(message.text)

    await agent.send_message(
        conversation_id=message.conversation_id,
        to=message.from_id,
        reply_to_message_id=message.id,
        text=result,
    )
```

## Widgets

Widgets are sent as encrypted conversation messages. Each method returns the
widget id. User answers arrive later as `message.widget_response`.

### Confirm

```python
await agent.send_confirm_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="confirm-delete",
    title="Delete file?",
    body="This cannot be undone.",
    danger=True,
    labels={"yes": "Delete", "no": "Cancel"},
)
```

Handle the answer:

```python
if message.widget_response and message.widget_response.ref == "confirm-delete":
    if message.widget_response.cancelled:
        return
    if message.widget_response.value is True:
        await agent.send_message(
            conversation_id=message.conversation_id,
            to=message.from_id,
            text="Confirmed.",
        )
```

### Choice

```python
await agent.send_choice_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="choose-format",
    title="Choose a format",
    options=[
        {"id": "pdf", "label": "PDF"},
        {"id": "csv", "label": "CSV"},
    ],
    multi=False,
)
```

### Permission

```python
await agent.send_permission_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="permission-files",
    title="Allow file access?",
    body="The agent needs access to selected files.",
    scopes=["once", "session"],
)
```

### Form

```python
await agent.send_form_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="profile-form",
    title="Complete profile",
    submitLabel="Save",
    fields=[
        {"name": "email", "label": "Email", "type": "email", "required": True},
        {"name": "notes", "label": "Notes", "type": "textarea"},
    ],
)
```

### Progress

```python
progress_id = await agent.send_progress_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="import-progress",
    title="Importing files",
    value=10,
    max=100,
    cancellable=True,
)
```

Update it:

```python
await agent.send_widget_update(
    conversation_id=message.conversation_id,
    to=message.from_id,
    ref=progress_id,
    spec={"value": 65, "body": "Almost done"},
)
```

### File Picker

```python
await agent.send_file_picker_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="pick-files",
    title="Choose files",
    multiple=True,
    accept=[".pdf", "image/*"],
    max_files=5,
)
```

### Date And Time

```python
await agent.send_datetime_widget(
    conversation_id=message.conversation_id,
    to=message.from_id,
    widget_id="schedule",
    title="Pick a meeting time",
    mode="datetime",
    min="2026-05-21T09:00:00",
)
```

## Files And Media

`send_file` and `send_media` encrypt bytes locally with AES-GCM, upload only
ciphertext to `/agent/blobs`, then send the attachment key and IV inside the
E2EE message payload.

```python
await agent.send_file(
    conversation_id=message.conversation_id,
    to=message.from_id,
    reply_to_message_id=message.id,
    text="Here is the report.",
    name="report.txt",
    mime="text/plain",
    data=b"private report",
)
```

Media adds optional dimensions or duration:

```python
await agent.send_media(
    conversation_id=message.conversation_id,
    to=message.from_id,
    text="Preview attached.",
    name="chart.png",
    mime="image/png",
    data=png_bytes,
    width=1200,
    height=800,
)
```

## Encryption

When `e2ee=True`, the SDK encrypts and decrypts messages locally before they
leave your machine. On connect, the client publishes the agent pre-key bundle,
establishes encrypted sessions for conversations, and keeps message contents
opaque to the relay.

Use `e2ee=False` only for local debugging or non-encrypted transports.

## MCP

This package is the Python SDK for building agents. The MCP server is
distributed separately:

```bash
npx -y @pacerelle/mcp-server
```
