pacerelle 0.1.0a0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pacerelle-0.1.0a0/.gitignore +56 -0
- pacerelle-0.1.0a0/LICENSE +21 -0
- pacerelle-0.1.0a0/PKG-INFO +318 -0
- pacerelle-0.1.0a0/README.md +296 -0
- pacerelle-0.1.0a0/RELEASING.md +26 -0
- pacerelle-0.1.0a0/hatch_build.py +54 -0
- pacerelle-0.1.0a0/pyproject.toml +49 -0
- pacerelle-0.1.0a0/scripts/build_release.py +20 -0
- pacerelle-0.1.0a0/scripts/readme_echo_agent.py +29 -0
- pacerelle-0.1.0a0/scripts/upload_testpypi.py +51 -0
- pacerelle-0.1.0a0/src/agent_gateway_sdk/__init__.py +1200 -0
- pacerelle-0.1.0a0/src/agent_gateway_sdk/native/libsignalwrap.so +0 -0
- pacerelle-0.1.0a0/src/agent_gateway_sdk/py.typed +1 -0
- pacerelle-0.1.0a0/src/pacerelle/__init__.py +3 -0
- pacerelle-0.1.0a0/src/pacerelle/py.typed +1 -0
- pacerelle-0.1.0a0/tests/test_e2ee.py +21 -0
- pacerelle-0.1.0a0/tests/test_sqlite_store.py +76 -0
- pacerelle-0.1.0a0/tests/test_version.py +178 -0
- pacerelle-0.1.0a0/uv.lock +508 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Secrets and local env
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
!.env.example
|
|
5
|
+
!.env.dev.example
|
|
6
|
+
|
|
7
|
+
# Node / TypeScript
|
|
8
|
+
node_modules/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
.turbo/
|
|
12
|
+
.vite/
|
|
13
|
+
coverage/
|
|
14
|
+
playwright-report/
|
|
15
|
+
test-results/
|
|
16
|
+
*.tsbuildinfo
|
|
17
|
+
|
|
18
|
+
# Go
|
|
19
|
+
bin/
|
|
20
|
+
*.test
|
|
21
|
+
*.out
|
|
22
|
+
vendor/
|
|
23
|
+
gateway/gateway
|
|
24
|
+
mcp-bridge/mcp-bridge
|
|
25
|
+
|
|
26
|
+
# Python
|
|
27
|
+
.venv/
|
|
28
|
+
__pycache__/
|
|
29
|
+
*.py[cod]
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
.ruff_cache/
|
|
32
|
+
.mypy_cache/
|
|
33
|
+
*.egg-info/
|
|
34
|
+
|
|
35
|
+
# Databases and local state
|
|
36
|
+
*.sqlite
|
|
37
|
+
*.sqlite3
|
|
38
|
+
tmp/
|
|
39
|
+
temp/
|
|
40
|
+
|
|
41
|
+
# Local proof-of-concept workspace
|
|
42
|
+
poc-workspace/
|
|
43
|
+
target/
|
|
44
|
+
|
|
45
|
+
# IDE / OS
|
|
46
|
+
.idea/
|
|
47
|
+
.vscode/
|
|
48
|
+
.DS_Store
|
|
49
|
+
Thumbs.db
|
|
50
|
+
|
|
51
|
+
# Logs
|
|
52
|
+
*.log
|
|
53
|
+
npm-debug.log*
|
|
54
|
+
yarn-debug.log*
|
|
55
|
+
yarn-error.log*
|
|
56
|
+
pnpm-debug.log*
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pacerelle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pacerelle
|
|
3
|
+
Version: 0.1.0a0
|
|
4
|
+
Summary: Python SDK for Pacerelle encrypted local agent relays.
|
|
5
|
+
Project-URL: Homepage, https://pacerelle.com
|
|
6
|
+
Project-URL: Documentation, https://pacerelle.com
|
|
7
|
+
Author: Pacerelle
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agents,ai,e2ee,local-agents,mcp,websocket
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: cryptography>=42.0.0
|
|
20
|
+
Requires-Dist: websockets>=12.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Pacerelle Python SDK
|
|
24
|
+
|
|
25
|
+
Python agent client for Pacerelle encrypted local agent relays.
|
|
26
|
+
|
|
27
|
+
Use this SDK to connect a local Python process to Pacerelle, receive messages,
|
|
28
|
+
reply to conversations, drive widgets, and return encrypted files or media.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install pacerelle
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
> Alpha release: APIs and native E2EE packaging may change before the first
|
|
35
|
+
> stable release.
|
|
36
|
+
|
|
37
|
+
## Before You Run
|
|
38
|
+
|
|
39
|
+
Create an agent in Pacerelle. The confirmation panel shows both
|
|
40
|
+
`Identifiant de l'agent` and `Jeton d'authentification`. Use
|
|
41
|
+
`Copier la configuration .env` to copy the required variables.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
export PACERELLE_AGENT_ID="agent-id"
|
|
45
|
+
export PACERELLE_AGENT_TOKEN="agent-token"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
On Windows PowerShell:
|
|
49
|
+
|
|
50
|
+
```powershell
|
|
51
|
+
$env:PACERELLE_AGENT_ID = "agent-id"
|
|
52
|
+
$env:PACERELLE_AGENT_TOKEN = "agent-token"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Published packages connect to the Pacerelle API by default. Local source builds
|
|
56
|
+
default to `http://localhost:8080` for development.
|
|
57
|
+
|
|
58
|
+
## Minimal Echo Agent
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
import os
|
|
63
|
+
|
|
64
|
+
from pacerelle import AgentGatewayClient
|
|
65
|
+
|
|
66
|
+
client = AgentGatewayClient(
|
|
67
|
+
token=os.environ["PACERELLE_AGENT_TOKEN"],
|
|
68
|
+
agent_id=os.environ["PACERELLE_AGENT_ID"],
|
|
69
|
+
e2ee=True,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def handle(message, agent):
|
|
74
|
+
await agent.send_message(
|
|
75
|
+
conversation_id=message.conversation_id,
|
|
76
|
+
to=message.from_id,
|
|
77
|
+
reply_to_message_id=message.id,
|
|
78
|
+
text=f"Received: {message.text}",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
client.on_message(handle)
|
|
83
|
+
asyncio.run(client.connect())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Incoming Messages
|
|
87
|
+
|
|
88
|
+
The handler receives an `AgentMessage`:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
async def handle(message, agent):
|
|
92
|
+
print(message.id)
|
|
93
|
+
print(message.conversation_id)
|
|
94
|
+
print(message.from_id)
|
|
95
|
+
print(message.text)
|
|
96
|
+
print(message.attachments)
|
|
97
|
+
print(message.widget_response)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Use `message.from_id` as the `to` value when replying to the user.
|
|
101
|
+
|
|
102
|
+
## Sending Messages And Replies
|
|
103
|
+
|
|
104
|
+
Send a normal message:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
await agent.send_message(
|
|
108
|
+
conversation_id=message.conversation_id,
|
|
109
|
+
to=message.from_id,
|
|
110
|
+
text="I can help with that.",
|
|
111
|
+
)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Reply to a specific user message:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
await agent.send_message(
|
|
118
|
+
conversation_id=message.conversation_id,
|
|
119
|
+
to=message.from_id,
|
|
120
|
+
reply_to_message_id=message.id,
|
|
121
|
+
text="Replying to your last message.",
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Running Your Own Agent Logic
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
async def handle(message, agent):
|
|
129
|
+
result = await run_my_agent(message.text)
|
|
130
|
+
|
|
131
|
+
await agent.send_message(
|
|
132
|
+
conversation_id=message.conversation_id,
|
|
133
|
+
to=message.from_id,
|
|
134
|
+
reply_to_message_id=message.id,
|
|
135
|
+
text=result,
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Widgets
|
|
140
|
+
|
|
141
|
+
Widgets are sent as encrypted conversation messages. Each method returns the
|
|
142
|
+
widget id. User answers arrive later as `message.widget_response`.
|
|
143
|
+
|
|
144
|
+
### Confirm
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
await agent.send_confirm_widget(
|
|
148
|
+
conversation_id=message.conversation_id,
|
|
149
|
+
to=message.from_id,
|
|
150
|
+
widget_id="confirm-delete",
|
|
151
|
+
title="Delete file?",
|
|
152
|
+
body="This cannot be undone.",
|
|
153
|
+
danger=True,
|
|
154
|
+
labels={"yes": "Delete", "no": "Cancel"},
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Handle the answer:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
if message.widget_response and message.widget_response.ref == "confirm-delete":
|
|
162
|
+
if message.widget_response.cancelled:
|
|
163
|
+
return
|
|
164
|
+
if message.widget_response.value is True:
|
|
165
|
+
await agent.send_message(
|
|
166
|
+
conversation_id=message.conversation_id,
|
|
167
|
+
to=message.from_id,
|
|
168
|
+
text="Confirmed.",
|
|
169
|
+
)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Choice
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
await agent.send_choice_widget(
|
|
176
|
+
conversation_id=message.conversation_id,
|
|
177
|
+
to=message.from_id,
|
|
178
|
+
widget_id="choose-format",
|
|
179
|
+
title="Choose a format",
|
|
180
|
+
options=[
|
|
181
|
+
{"id": "pdf", "label": "PDF"},
|
|
182
|
+
{"id": "csv", "label": "CSV"},
|
|
183
|
+
],
|
|
184
|
+
multi=False,
|
|
185
|
+
)
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Permission
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
await agent.send_permission_widget(
|
|
192
|
+
conversation_id=message.conversation_id,
|
|
193
|
+
to=message.from_id,
|
|
194
|
+
widget_id="permission-files",
|
|
195
|
+
title="Allow file access?",
|
|
196
|
+
body="The agent needs access to selected files.",
|
|
197
|
+
scopes=["once", "session"],
|
|
198
|
+
)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Form
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
await agent.send_form_widget(
|
|
205
|
+
conversation_id=message.conversation_id,
|
|
206
|
+
to=message.from_id,
|
|
207
|
+
widget_id="profile-form",
|
|
208
|
+
title="Complete profile",
|
|
209
|
+
submitLabel="Save",
|
|
210
|
+
fields=[
|
|
211
|
+
{"name": "email", "label": "Email", "type": "email", "required": True},
|
|
212
|
+
{"name": "notes", "label": "Notes", "type": "textarea"},
|
|
213
|
+
],
|
|
214
|
+
)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Progress
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
progress_id = await agent.send_progress_widget(
|
|
221
|
+
conversation_id=message.conversation_id,
|
|
222
|
+
to=message.from_id,
|
|
223
|
+
widget_id="import-progress",
|
|
224
|
+
title="Importing files",
|
|
225
|
+
value=10,
|
|
226
|
+
max=100,
|
|
227
|
+
cancellable=True,
|
|
228
|
+
)
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Update it:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
await agent.send_widget_update(
|
|
235
|
+
conversation_id=message.conversation_id,
|
|
236
|
+
to=message.from_id,
|
|
237
|
+
ref=progress_id,
|
|
238
|
+
spec={"value": 65, "body": "Almost done"},
|
|
239
|
+
)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### File Picker
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
await agent.send_file_picker_widget(
|
|
246
|
+
conversation_id=message.conversation_id,
|
|
247
|
+
to=message.from_id,
|
|
248
|
+
widget_id="pick-files",
|
|
249
|
+
title="Choose files",
|
|
250
|
+
multiple=True,
|
|
251
|
+
accept=[".pdf", "image/*"],
|
|
252
|
+
max_files=5,
|
|
253
|
+
)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Date And Time
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
await agent.send_datetime_widget(
|
|
260
|
+
conversation_id=message.conversation_id,
|
|
261
|
+
to=message.from_id,
|
|
262
|
+
widget_id="schedule",
|
|
263
|
+
title="Pick a meeting time",
|
|
264
|
+
mode="datetime",
|
|
265
|
+
min="2026-05-21T09:00:00",
|
|
266
|
+
)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
## Files And Media
|
|
270
|
+
|
|
271
|
+
`send_file` and `send_media` encrypt bytes locally with AES-GCM, upload only
|
|
272
|
+
ciphertext to `/agent/blobs`, then send the attachment key and IV inside the
|
|
273
|
+
E2EE message payload.
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
await agent.send_file(
|
|
277
|
+
conversation_id=message.conversation_id,
|
|
278
|
+
to=message.from_id,
|
|
279
|
+
reply_to_message_id=message.id,
|
|
280
|
+
text="Here is the report.",
|
|
281
|
+
name="report.txt",
|
|
282
|
+
mime="text/plain",
|
|
283
|
+
data=b"private report",
|
|
284
|
+
)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Media adds optional dimensions or duration:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
await agent.send_media(
|
|
291
|
+
conversation_id=message.conversation_id,
|
|
292
|
+
to=message.from_id,
|
|
293
|
+
text="Preview attached.",
|
|
294
|
+
name="chart.png",
|
|
295
|
+
mime="image/png",
|
|
296
|
+
data=png_bytes,
|
|
297
|
+
width=1200,
|
|
298
|
+
height=800,
|
|
299
|
+
)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Encryption
|
|
303
|
+
|
|
304
|
+
When `e2ee=True`, the SDK encrypts and decrypts messages locally before they
|
|
305
|
+
leave your machine. On connect, the client publishes the agent pre-key bundle,
|
|
306
|
+
establishes encrypted sessions for conversations, and keeps message contents
|
|
307
|
+
opaque to the relay.
|
|
308
|
+
|
|
309
|
+
Use `e2ee=False` only for local debugging or non-encrypted transports.
|
|
310
|
+
|
|
311
|
+
## MCP
|
|
312
|
+
|
|
313
|
+
This package is the Python SDK for building agents. The MCP server is
|
|
314
|
+
distributed separately:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
npx -y @pacerelle/mcp-server
|
|
318
|
+
```
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# Pacerelle Python SDK
|
|
2
|
+
|
|
3
|
+
Python agent client for Pacerelle encrypted local agent relays.
|
|
4
|
+
|
|
5
|
+
Use this SDK to connect a local Python process to Pacerelle, receive messages,
|
|
6
|
+
reply to conversations, drive widgets, and return encrypted files or media.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install pacerelle
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
> Alpha release: APIs and native E2EE packaging may change before the first
|
|
13
|
+
> stable release.
|
|
14
|
+
|
|
15
|
+
## Before You Run
|
|
16
|
+
|
|
17
|
+
Create an agent in Pacerelle. The confirmation panel shows both
|
|
18
|
+
`Identifiant de l'agent` and `Jeton d'authentification`. Use
|
|
19
|
+
`Copier la configuration .env` to copy the required variables.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
export PACERELLE_AGENT_ID="agent-id"
|
|
23
|
+
export PACERELLE_AGENT_TOKEN="agent-token"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
On Windows PowerShell:
|
|
27
|
+
|
|
28
|
+
```powershell
|
|
29
|
+
$env:PACERELLE_AGENT_ID = "agent-id"
|
|
30
|
+
$env:PACERELLE_AGENT_TOKEN = "agent-token"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Published packages connect to the Pacerelle API by default. Local source builds
|
|
34
|
+
default to `http://localhost:8080` for development.
|
|
35
|
+
|
|
36
|
+
## Minimal Echo Agent
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import asyncio
|
|
40
|
+
import os
|
|
41
|
+
|
|
42
|
+
from pacerelle import AgentGatewayClient
|
|
43
|
+
|
|
44
|
+
client = AgentGatewayClient(
|
|
45
|
+
token=os.environ["PACERELLE_AGENT_TOKEN"],
|
|
46
|
+
agent_id=os.environ["PACERELLE_AGENT_ID"],
|
|
47
|
+
e2ee=True,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def handle(message, agent):
|
|
52
|
+
await agent.send_message(
|
|
53
|
+
conversation_id=message.conversation_id,
|
|
54
|
+
to=message.from_id,
|
|
55
|
+
reply_to_message_id=message.id,
|
|
56
|
+
text=f"Received: {message.text}",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
client.on_message(handle)
|
|
61
|
+
asyncio.run(client.connect())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Incoming Messages
|
|
65
|
+
|
|
66
|
+
The handler receives an `AgentMessage`:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
async def handle(message, agent):
|
|
70
|
+
print(message.id)
|
|
71
|
+
print(message.conversation_id)
|
|
72
|
+
print(message.from_id)
|
|
73
|
+
print(message.text)
|
|
74
|
+
print(message.attachments)
|
|
75
|
+
print(message.widget_response)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Use `message.from_id` as the `to` value when replying to the user.
|
|
79
|
+
|
|
80
|
+
## Sending Messages And Replies
|
|
81
|
+
|
|
82
|
+
Send a normal message:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
await agent.send_message(
|
|
86
|
+
conversation_id=message.conversation_id,
|
|
87
|
+
to=message.from_id,
|
|
88
|
+
text="I can help with that.",
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Reply to a specific user message:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
await agent.send_message(
|
|
96
|
+
conversation_id=message.conversation_id,
|
|
97
|
+
to=message.from_id,
|
|
98
|
+
reply_to_message_id=message.id,
|
|
99
|
+
text="Replying to your last message.",
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Running Your Own Agent Logic
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
async def handle(message, agent):
|
|
107
|
+
result = await run_my_agent(message.text)
|
|
108
|
+
|
|
109
|
+
await agent.send_message(
|
|
110
|
+
conversation_id=message.conversation_id,
|
|
111
|
+
to=message.from_id,
|
|
112
|
+
reply_to_message_id=message.id,
|
|
113
|
+
text=result,
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Widgets
|
|
118
|
+
|
|
119
|
+
Widgets are sent as encrypted conversation messages. Each method returns the
|
|
120
|
+
widget id. User answers arrive later as `message.widget_response`.
|
|
121
|
+
|
|
122
|
+
### Confirm
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
await agent.send_confirm_widget(
|
|
126
|
+
conversation_id=message.conversation_id,
|
|
127
|
+
to=message.from_id,
|
|
128
|
+
widget_id="confirm-delete",
|
|
129
|
+
title="Delete file?",
|
|
130
|
+
body="This cannot be undone.",
|
|
131
|
+
danger=True,
|
|
132
|
+
labels={"yes": "Delete", "no": "Cancel"},
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Handle the answer:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
if message.widget_response and message.widget_response.ref == "confirm-delete":
|
|
140
|
+
if message.widget_response.cancelled:
|
|
141
|
+
return
|
|
142
|
+
if message.widget_response.value is True:
|
|
143
|
+
await agent.send_message(
|
|
144
|
+
conversation_id=message.conversation_id,
|
|
145
|
+
to=message.from_id,
|
|
146
|
+
text="Confirmed.",
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Choice
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
await agent.send_choice_widget(
|
|
154
|
+
conversation_id=message.conversation_id,
|
|
155
|
+
to=message.from_id,
|
|
156
|
+
widget_id="choose-format",
|
|
157
|
+
title="Choose a format",
|
|
158
|
+
options=[
|
|
159
|
+
{"id": "pdf", "label": "PDF"},
|
|
160
|
+
{"id": "csv", "label": "CSV"},
|
|
161
|
+
],
|
|
162
|
+
multi=False,
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Permission
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
await agent.send_permission_widget(
|
|
170
|
+
conversation_id=message.conversation_id,
|
|
171
|
+
to=message.from_id,
|
|
172
|
+
widget_id="permission-files",
|
|
173
|
+
title="Allow file access?",
|
|
174
|
+
body="The agent needs access to selected files.",
|
|
175
|
+
scopes=["once", "session"],
|
|
176
|
+
)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Form
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
await agent.send_form_widget(
|
|
183
|
+
conversation_id=message.conversation_id,
|
|
184
|
+
to=message.from_id,
|
|
185
|
+
widget_id="profile-form",
|
|
186
|
+
title="Complete profile",
|
|
187
|
+
submitLabel="Save",
|
|
188
|
+
fields=[
|
|
189
|
+
{"name": "email", "label": "Email", "type": "email", "required": True},
|
|
190
|
+
{"name": "notes", "label": "Notes", "type": "textarea"},
|
|
191
|
+
],
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Progress
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
progress_id = await agent.send_progress_widget(
|
|
199
|
+
conversation_id=message.conversation_id,
|
|
200
|
+
to=message.from_id,
|
|
201
|
+
widget_id="import-progress",
|
|
202
|
+
title="Importing files",
|
|
203
|
+
value=10,
|
|
204
|
+
max=100,
|
|
205
|
+
cancellable=True,
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Update it:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
await agent.send_widget_update(
|
|
213
|
+
conversation_id=message.conversation_id,
|
|
214
|
+
to=message.from_id,
|
|
215
|
+
ref=progress_id,
|
|
216
|
+
spec={"value": 65, "body": "Almost done"},
|
|
217
|
+
)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### File Picker
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
await agent.send_file_picker_widget(
|
|
224
|
+
conversation_id=message.conversation_id,
|
|
225
|
+
to=message.from_id,
|
|
226
|
+
widget_id="pick-files",
|
|
227
|
+
title="Choose files",
|
|
228
|
+
multiple=True,
|
|
229
|
+
accept=[".pdf", "image/*"],
|
|
230
|
+
max_files=5,
|
|
231
|
+
)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Date And Time
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
await agent.send_datetime_widget(
|
|
238
|
+
conversation_id=message.conversation_id,
|
|
239
|
+
to=message.from_id,
|
|
240
|
+
widget_id="schedule",
|
|
241
|
+
title="Pick a meeting time",
|
|
242
|
+
mode="datetime",
|
|
243
|
+
min="2026-05-21T09:00:00",
|
|
244
|
+
)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Files And Media
|
|
248
|
+
|
|
249
|
+
`send_file` and `send_media` encrypt bytes locally with AES-GCM, upload only
|
|
250
|
+
ciphertext to `/agent/blobs`, then send the attachment key and IV inside the
|
|
251
|
+
E2EE message payload.
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
await agent.send_file(
|
|
255
|
+
conversation_id=message.conversation_id,
|
|
256
|
+
to=message.from_id,
|
|
257
|
+
reply_to_message_id=message.id,
|
|
258
|
+
text="Here is the report.",
|
|
259
|
+
name="report.txt",
|
|
260
|
+
mime="text/plain",
|
|
261
|
+
data=b"private report",
|
|
262
|
+
)
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Media adds optional dimensions or duration:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
await agent.send_media(
|
|
269
|
+
conversation_id=message.conversation_id,
|
|
270
|
+
to=message.from_id,
|
|
271
|
+
text="Preview attached.",
|
|
272
|
+
name="chart.png",
|
|
273
|
+
mime="image/png",
|
|
274
|
+
data=png_bytes,
|
|
275
|
+
width=1200,
|
|
276
|
+
height=800,
|
|
277
|
+
)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Encryption
|
|
281
|
+
|
|
282
|
+
When `e2ee=True`, the SDK encrypts and decrypts messages locally before they
|
|
283
|
+
leave your machine. On connect, the client publishes the agent pre-key bundle,
|
|
284
|
+
establishes encrypted sessions for conversations, and keeps message contents
|
|
285
|
+
opaque to the relay.
|
|
286
|
+
|
|
287
|
+
Use `e2ee=False` only for local debugging or non-encrypted transports.
|
|
288
|
+
|
|
289
|
+
## MCP
|
|
290
|
+
|
|
291
|
+
This package is the Python SDK for building agents. The MCP server is
|
|
292
|
+
distributed separately:
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
npx -y @pacerelle/mcp-server
|
|
296
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
The source tree keeps `AgentGatewayClient` pointed at `http://localhost:8080`
|
|
4
|
+
so local development works without extra configuration.
|
|
5
|
+
|
|
6
|
+
Release artifacts must be built with the public API URL injected:
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
python scripts/build_release.py
|
|
10
|
+
uvx twine check dist/pacerelle-*
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The release script sets `PACERELLE_SDK_DEFAULT_BASE_URL=https://api.pacerelle.com`
|
|
14
|
+
for the Hatch build. The build hook restores the source file after the artifact
|
|
15
|
+
is created, so the working tree keeps the local default.
|
|
16
|
+
|
|
17
|
+
## TestPyPI
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python scripts/upload_testpypi.py
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The current helper uploads the source distribution only. Wheels built directly
|
|
24
|
+
from the local WSL environment can use a raw `linux_aarch64` platform tag, which
|
|
25
|
+
TestPyPI rejects. Build publishable binary wheels with a manylinux-compatible
|
|
26
|
+
builder before uploading wheels.
|