getpatter 0.4.1__py3-none-any.whl
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.
- getpatter-0.4.1.dist-info/METADATA +759 -0
- getpatter-0.4.1.dist-info/RECORD +47 -0
- getpatter-0.4.1.dist-info/WHEEL +5 -0
- getpatter-0.4.1.dist-info/licenses/LICENSE +21 -0
- getpatter-0.4.1.dist-info/top_level.txt +1 -0
- patter/__init__.py +36 -0
- patter/api_routes.py +148 -0
- patter/banner.py +17 -0
- patter/client.py +810 -0
- patter/connection.py +123 -0
- patter/dashboard/__init__.py +5 -0
- patter/dashboard/auth.py +27 -0
- patter/dashboard/export.py +55 -0
- patter/dashboard/routes.py +142 -0
- patter/dashboard/store.py +254 -0
- patter/dashboard/ui.py +547 -0
- patter/exceptions.py +14 -0
- patter/handlers/__init__.py +0 -0
- patter/handlers/common.py +74 -0
- patter/handlers/stream_handler.py +1054 -0
- patter/handlers/telnyx_handler.py +347 -0
- patter/handlers/twilio_handler.py +458 -0
- patter/local_config.py +15 -0
- patter/models.py +199 -0
- patter/pricing.py +107 -0
- patter/providers/__init__.py +26 -0
- patter/providers/base.py +55 -0
- patter/providers/deepgram_stt.py +110 -0
- patter/providers/elevenlabs_convai.py +127 -0
- patter/providers/elevenlabs_tts.py +19 -0
- patter/providers/openai_realtime.py +196 -0
- patter/providers/openai_tts.py +64 -0
- patter/providers/telnyx_adapter.py +52 -0
- patter/providers/twilio_adapter.py +58 -0
- patter/providers/whisper_stt.py +111 -0
- patter/py.typed +0 -0
- patter/server.py +379 -0
- patter/services/__init__.py +0 -0
- patter/services/call_orchestrator.py +83 -0
- patter/services/llm_loop.py +293 -0
- patter/services/metrics.py +329 -0
- patter/services/remote_message.py +163 -0
- patter/services/session_manager.py +40 -0
- patter/services/tool_executor.py +154 -0
- patter/services/transcoding.py +39 -0
- patter/test_mode.py +242 -0
- patter/tunnel.py +133 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: getpatter
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Connect AI agents to phone numbers with 10 lines of code
|
|
5
|
+
Author-email: Nicolò Tognoni <nicolo@getpatter.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://www.getpatter.com
|
|
8
|
+
Project-URL: Documentation, https://docs.getpatter.com
|
|
9
|
+
Project-URL: Repository, https://github.com/PatterAI/Patter
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/PatterAI/Patter/issues
|
|
11
|
+
Keywords: voice,ai,phone,telephony,speech,tts,stt,twilio,telnyx,openai,elevenlabs
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Communications :: Telephony
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: websockets>=13.0
|
|
26
|
+
Requires-Dist: httpx>=0.27.0
|
|
27
|
+
Provides-Extra: local
|
|
28
|
+
Requires-Dist: fastapi>=0.115.0; extra == "local"
|
|
29
|
+
Requires-Dist: uvicorn[standard]>=0.30.0; extra == "local"
|
|
30
|
+
Requires-Dist: twilio>=9.0.0; extra == "local"
|
|
31
|
+
Requires-Dist: audioop-lts>=0.2.1; python_version >= "3.13" and extra == "local"
|
|
32
|
+
Requires-Dist: openai>=1.0.0; extra == "local"
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-asyncio>=0.24.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: psutil>=5.9.0; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<h1 align="center">Patter</h1>
|
|
42
|
+
<p align="center">Connect AI agents to phone numbers with 10 lines of code</p>
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<p align="center">
|
|
46
|
+
<a href="#quickstart">Quickstart</a> •
|
|
47
|
+
<a href="#features">Features</a> •
|
|
48
|
+
<a href="#installation">Installation</a> •
|
|
49
|
+
<a href="#documentation">Documentation</a> •
|
|
50
|
+
<a href="#self-hosting">Self-Hosting</a>
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
<img src="https://img.shields.io/badge/python-3.11%2B-blue?logo=python&logoColor=white" alt="Python 3.11+" />
|
|
55
|
+
<img src="https://img.shields.io/badge/typescript-5.0%2B-3178c6?logo=typescript&logoColor=white" alt="TypeScript 5+" />
|
|
56
|
+
<img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License" />
|
|
57
|
+
</p>
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
Patter is an open-source platform that gives your AI agent a voice and a phone number. Point it at any function that returns a string, and Patter handles the rest: telephony, speech-to-text, text-to-speech, and real-time audio streaming.
|
|
62
|
+
|
|
63
|
+
## Quickstart
|
|
64
|
+
|
|
65
|
+
<details open>
|
|
66
|
+
<summary><strong>Python</strong></summary>
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
from patter import Patter, IncomingMessage
|
|
71
|
+
|
|
72
|
+
async def on_message(msg: IncomingMessage) -> str:
|
|
73
|
+
# Your agent logic here — return what the AI should say
|
|
74
|
+
return f"You said: {msg.text}"
|
|
75
|
+
|
|
76
|
+
async def main():
|
|
77
|
+
phone = Patter(api_key="pt_xxx")
|
|
78
|
+
await phone.connect(on_message=on_message) # starts listening for inbound calls
|
|
79
|
+
|
|
80
|
+
asyncio.run(main())
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
</details>
|
|
84
|
+
|
|
85
|
+
<details>
|
|
86
|
+
<summary><strong>TypeScript</strong></summary>
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { Patter } from "getpatter";
|
|
90
|
+
|
|
91
|
+
const phone = new Patter({ apiKey: "pt_xxx" });
|
|
92
|
+
|
|
93
|
+
await phone.connect({
|
|
94
|
+
onMessage: async (msg) => {
|
|
95
|
+
// Your agent logic here — return what the AI should say
|
|
96
|
+
return `You said: ${msg.text}`;
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
</details>
|
|
102
|
+
|
|
103
|
+
## Local Mode (No Cloud Required)
|
|
104
|
+
|
|
105
|
+
Run Patter entirely in your process — no Patter account, no cloud backend.
|
|
106
|
+
|
|
107
|
+
<details open>
|
|
108
|
+
<summary><strong>Python</strong></summary>
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
import asyncio
|
|
112
|
+
from patter import Patter
|
|
113
|
+
|
|
114
|
+
async def main():
|
|
115
|
+
phone = Patter(
|
|
116
|
+
mode="local",
|
|
117
|
+
twilio_sid="AC...", twilio_token="...",
|
|
118
|
+
openai_key="sk-...",
|
|
119
|
+
phone_number="+1...",
|
|
120
|
+
webhook_url="xxx.ngrok-free.dev",
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
agent = phone.agent(
|
|
124
|
+
system_prompt="You are a friendly customer service agent for Acme Corp.",
|
|
125
|
+
voice="alloy",
|
|
126
|
+
first_message="Hello! Thanks for calling. How can I help?",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
print("Listening for calls...")
|
|
130
|
+
await phone.serve(agent=agent, port=8000)
|
|
131
|
+
|
|
132
|
+
asyncio.run(main())
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
</details>
|
|
136
|
+
|
|
137
|
+
<details>
|
|
138
|
+
<summary><strong>TypeScript</strong></summary>
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { Patter } from "getpatter";
|
|
142
|
+
|
|
143
|
+
const phone = new Patter({
|
|
144
|
+
mode: "local",
|
|
145
|
+
twilioSid: "AC...", twilioToken: "...",
|
|
146
|
+
openaiKey: "sk-...",
|
|
147
|
+
phoneNumber: "+1...",
|
|
148
|
+
webhookUrl: "xxx.ngrok-free.dev",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const agent = phone.agent({
|
|
152
|
+
systemPrompt: "You are a friendly customer service agent for Acme Corp.",
|
|
153
|
+
voice: "alloy",
|
|
154
|
+
firstMessage: "Hello! Thanks for calling. How can I help?",
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
console.log("Listening for calls...");
|
|
158
|
+
await phone.serve({ agent, port: 8000 });
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
</details>
|
|
162
|
+
|
|
163
|
+
## Local vs Cloud
|
|
164
|
+
|
|
165
|
+
| | Cloud Mode | Local Mode |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| **Setup** | Patter API key only | Twilio/Telnyx + OpenAI keys |
|
|
168
|
+
| **Infrastructure** | Managed by Patter | Runs in your process |
|
|
169
|
+
| **Backend** | `wss://api.getpatter.com` | Built-in (FastAPI / Express) |
|
|
170
|
+
| **Webhook** | Configured automatically | Requires public URL (e.g. ngrok) |
|
|
171
|
+
| **Voice modes** | All three | All three |
|
|
172
|
+
| **Best for** | Production, multi-tenant | Development, on-prem, full control |
|
|
173
|
+
|
|
174
|
+
## Features
|
|
175
|
+
|
|
176
|
+
### Voice
|
|
177
|
+
- Three voice modes: OpenAI Realtime, ElevenLabs ConvAI, Pipeline (any STT + TTS)
|
|
178
|
+
- Any STT: Deepgram, OpenAI Whisper
|
|
179
|
+
- Any TTS: ElevenLabs, OpenAI TTS
|
|
180
|
+
- Natural barge-in with mark-based audio tracking
|
|
181
|
+
- DTMF keypad input forwarded to agent as `[DTMF: 1]`
|
|
182
|
+
|
|
183
|
+
### Agent
|
|
184
|
+
- Bring your own agent (any LLM in pipeline mode)
|
|
185
|
+
- System prompt with dynamic `{variable}` substitution
|
|
186
|
+
- Conversation history tracked per call (`data.history` in all callbacks)
|
|
187
|
+
- Tool calling via webhooks with automatic 3x retry
|
|
188
|
+
- Built-in tools: `transfer_call`, `end_call` (auto-injected)
|
|
189
|
+
|
|
190
|
+
### Telephony
|
|
191
|
+
- Twilio and Telnyx carriers
|
|
192
|
+
- Inbound and outbound calls
|
|
193
|
+
- Call transfer to humans (`transfer_call` system tool)
|
|
194
|
+
- Call recording (`recording: true` in `serve()`)
|
|
195
|
+
- Answering machine detection (`machineDetection: true` for outbound)
|
|
196
|
+
- Voicemail drop (`voicemailMessage: "..."` plays on voicemail detection)
|
|
197
|
+
- Custom parameters passthrough via TwiML
|
|
198
|
+
|
|
199
|
+
### Developer Experience
|
|
200
|
+
- `pip install patter` / `npm install getpatter`
|
|
201
|
+
- 10 lines of code to connect an agent to a phone
|
|
202
|
+
- Local mode (embedded, no backend) + Cloud mode
|
|
203
|
+
- Python + TypeScript SDKs with full parity
|
|
204
|
+
- MCP server for Claude Desktop
|
|
205
|
+
- Open-source (MIT)
|
|
206
|
+
|
|
207
|
+
## Complete Example
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
const phone = new Patter({
|
|
211
|
+
mode: 'local',
|
|
212
|
+
twilioSid: process.env.TWILIO_SID,
|
|
213
|
+
twilioToken: process.env.TWILIO_TOKEN,
|
|
214
|
+
openaiKey: process.env.OPENAI_KEY,
|
|
215
|
+
phoneNumber: '+16592214527',
|
|
216
|
+
webhookUrl: 'your-domain.ngrok-free.dev',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const agent = phone.agent({
|
|
220
|
+
systemPrompt: `You are a customer service agent for Acme Corp.
|
|
221
|
+
The customer is {customer_name} with order #{order_id}.
|
|
222
|
+
Check inventory before answering stock questions.
|
|
223
|
+
Transfer to a human if the customer is upset.`,
|
|
224
|
+
voice: 'alloy',
|
|
225
|
+
language: 'en',
|
|
226
|
+
firstMessage: 'Hi {customer_name}! How can I help with order #{order_id}?',
|
|
227
|
+
variables: {
|
|
228
|
+
customer_name: 'John',
|
|
229
|
+
order_id: '12345',
|
|
230
|
+
},
|
|
231
|
+
tools: [{
|
|
232
|
+
name: 'check_inventory',
|
|
233
|
+
description: 'Check product stock',
|
|
234
|
+
parameters: { type: 'object', properties: { product: { type: 'string' } } },
|
|
235
|
+
webhookUrl: 'https://api.acme.com/inventory',
|
|
236
|
+
}],
|
|
237
|
+
// Built-in: transfer_call, end_call (auto-injected)
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await phone.serve({
|
|
241
|
+
agent,
|
|
242
|
+
port: 8000,
|
|
243
|
+
recording: true,
|
|
244
|
+
onCallStart: async (data) => console.log(`Call from ${data.caller}`),
|
|
245
|
+
onCallEnd: async (data) => console.log(`Transcript: ${data.transcript?.length} turns`),
|
|
246
|
+
onTranscript: async (data) => console.log(`${data.role}: ${data.text}`),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// Outbound with machine detection
|
|
250
|
+
await phone.call({
|
|
251
|
+
to: '+1234567890',
|
|
252
|
+
machineDetection: true,
|
|
253
|
+
voicemailMessage: 'Hi, please call us back at 555-0123.',
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## How It Works
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
Your Code (on_message handler)
|
|
261
|
+
│
|
|
262
|
+
▼
|
|
263
|
+
Patter SDK ──WebSocket──► Patter Backend ──────────────────────────────┐
|
|
264
|
+
│ │
|
|
265
|
+
┌───────┴────────┐ │
|
|
266
|
+
▼ ▼ ▼
|
|
267
|
+
STT Engine TTS Engine Telephony Provider
|
|
268
|
+
(Deepgram / (ElevenLabs / (Twilio / Telnyx)
|
|
269
|
+
Whisper / OpenAI TTS) │
|
|
270
|
+
OpenAI RT) │ │
|
|
271
|
+
│ └───────────────────────►│
|
|
272
|
+
└────────────────────────────────────────►│
|
|
273
|
+
▼
|
|
274
|
+
Phone Call
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The audio path: **Phone → Telephony → WebSocket → Backend → STT → your handler → TTS → Backend → WebSocket → Phone**
|
|
278
|
+
|
|
279
|
+
## Installation
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
# Python
|
|
283
|
+
pip install patter
|
|
284
|
+
|
|
285
|
+
# TypeScript / Node.js
|
|
286
|
+
npm install getpatter
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Documentation
|
|
290
|
+
|
|
291
|
+
### Inbound Calls (AI answers the phone)
|
|
292
|
+
|
|
293
|
+
<details open>
|
|
294
|
+
<summary><strong>Python</strong></summary>
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
import asyncio
|
|
298
|
+
from patter import Patter, IncomingMessage
|
|
299
|
+
|
|
300
|
+
async def agent(msg: IncomingMessage) -> str:
|
|
301
|
+
if "hours" in msg.text.lower():
|
|
302
|
+
return "We're open Monday through Friday, 9 to 5."
|
|
303
|
+
return "How can I help you today?"
|
|
304
|
+
|
|
305
|
+
async def main():
|
|
306
|
+
phone = Patter(api_key="pt_xxx")
|
|
307
|
+
await phone.connect(
|
|
308
|
+
on_message=agent,
|
|
309
|
+
on_call_start=lambda data: print(f"Call from {data['caller']}"),
|
|
310
|
+
on_call_end=lambda data: print("Call ended"),
|
|
311
|
+
)
|
|
312
|
+
await asyncio.Event().wait() # keep the process alive
|
|
313
|
+
|
|
314
|
+
asyncio.run(main())
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
</details>
|
|
318
|
+
|
|
319
|
+
<details>
|
|
320
|
+
<summary><strong>TypeScript</strong></summary>
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
import { Patter } from "getpatter";
|
|
324
|
+
|
|
325
|
+
const phone = new Patter({ apiKey: "pt_xxx" });
|
|
326
|
+
|
|
327
|
+
await phone.connect({
|
|
328
|
+
onMessage: async (msg) => {
|
|
329
|
+
if (msg.text.toLowerCase().includes("hours")) {
|
|
330
|
+
return "We're open Monday through Friday, 9 to 5.";
|
|
331
|
+
}
|
|
332
|
+
return "How can I help you today?";
|
|
333
|
+
},
|
|
334
|
+
onCallStart: (data) => console.log(`Call from ${data.caller}`),
|
|
335
|
+
onCallEnd: () => console.log("Call ended"),
|
|
336
|
+
});
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
</details>
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
### Outbound Calls (AI calls someone)
|
|
344
|
+
|
|
345
|
+
<details open>
|
|
346
|
+
<summary><strong>Python</strong></summary>
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
import asyncio
|
|
350
|
+
from patter import Patter, IncomingMessage
|
|
351
|
+
|
|
352
|
+
async def agent(msg: IncomingMessage) -> str:
|
|
353
|
+
return "Thanks for picking up. This is a reminder about your appointment tomorrow."
|
|
354
|
+
|
|
355
|
+
async def main():
|
|
356
|
+
phone = Patter(api_key="pt_xxx")
|
|
357
|
+
await phone.connect(on_message=agent)
|
|
358
|
+
await phone.call(
|
|
359
|
+
to="+14155551234",
|
|
360
|
+
first_message="Hi, this is an automated reminder from Acme Corp.",
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
asyncio.run(main())
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
</details>
|
|
367
|
+
|
|
368
|
+
<details>
|
|
369
|
+
<summary><strong>TypeScript</strong></summary>
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
import { Patter } from "getpatter";
|
|
373
|
+
|
|
374
|
+
const phone = new Patter({ apiKey: "pt_xxx" });
|
|
375
|
+
|
|
376
|
+
await phone.connect({
|
|
377
|
+
onMessage: async () =>
|
|
378
|
+
"Thanks for picking up. This is a reminder about your appointment tomorrow.",
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
await phone.call({
|
|
382
|
+
to: "+14155551234",
|
|
383
|
+
firstMessage: "Hi, this is an automated reminder from Acme Corp.",
|
|
384
|
+
});
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
</details>
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
### Outbound with Machine Detection + Voicemail Drop
|
|
392
|
+
|
|
393
|
+
<details open>
|
|
394
|
+
<summary><strong>Python</strong></summary>
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
await phone.call(
|
|
398
|
+
to="+14155551234",
|
|
399
|
+
first_message="Hi, this is an automated reminder from Acme Corp.",
|
|
400
|
+
machine_detection=True,
|
|
401
|
+
voicemail_message="Hi, we tried to reach you. Please call us back at 555-0123.",
|
|
402
|
+
)
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
</details>
|
|
406
|
+
|
|
407
|
+
<details>
|
|
408
|
+
<summary><strong>TypeScript</strong></summary>
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
await phone.call({
|
|
412
|
+
to: "+14155551234",
|
|
413
|
+
firstMessage: "Hi, this is an automated reminder from Acme Corp.",
|
|
414
|
+
machineDetection: true,
|
|
415
|
+
voicemailMessage: "Hi, we tried to reach you. Please call us back at 555-0123.",
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
</details>
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
### Dynamic Variables in Prompts
|
|
424
|
+
|
|
425
|
+
Inject call-specific data into system prompts and first messages using `{variable}` placeholders.
|
|
426
|
+
|
|
427
|
+
<details open>
|
|
428
|
+
<summary><strong>Python</strong></summary>
|
|
429
|
+
|
|
430
|
+
```python
|
|
431
|
+
agent = phone.agent(
|
|
432
|
+
system_prompt="You are helping {customer_name}, account #{account_id}.",
|
|
433
|
+
first_message="Hi {customer_name}! How can I help you today?",
|
|
434
|
+
variables={
|
|
435
|
+
"customer_name": "Jane",
|
|
436
|
+
"account_id": "A-789",
|
|
437
|
+
},
|
|
438
|
+
)
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
</details>
|
|
442
|
+
|
|
443
|
+
<details>
|
|
444
|
+
<summary><strong>TypeScript</strong></summary>
|
|
445
|
+
|
|
446
|
+
```typescript
|
|
447
|
+
const agent = phone.agent({
|
|
448
|
+
systemPrompt: "You are helping {customer_name}, account #{account_id}.",
|
|
449
|
+
firstMessage: "Hi {customer_name}! How can I help you today?",
|
|
450
|
+
variables: {
|
|
451
|
+
customer_name: "Jane",
|
|
452
|
+
account_id: "A-789",
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
</details>
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
### Tool Calling via Webhooks
|
|
462
|
+
|
|
463
|
+
Agents can call external APIs mid-conversation. Patter POSTs to your webhook URL and retries up to 3 times on failure.
|
|
464
|
+
|
|
465
|
+
<details open>
|
|
466
|
+
<summary><strong>Python</strong></summary>
|
|
467
|
+
|
|
468
|
+
```python
|
|
469
|
+
agent = phone.agent(
|
|
470
|
+
system_prompt="You are a booking assistant. Check availability before confirming.",
|
|
471
|
+
tools=[{
|
|
472
|
+
"name": "check_availability",
|
|
473
|
+
"description": "Check appointment availability for a given date",
|
|
474
|
+
"parameters": {
|
|
475
|
+
"type": "object",
|
|
476
|
+
"properties": {
|
|
477
|
+
"date": {"type": "string", "description": "ISO date, e.g. 2025-06-15"},
|
|
478
|
+
},
|
|
479
|
+
"required": ["date"],
|
|
480
|
+
},
|
|
481
|
+
"webhook_url": "https://api.example.com/availability",
|
|
482
|
+
}],
|
|
483
|
+
)
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
</details>
|
|
487
|
+
|
|
488
|
+
<details>
|
|
489
|
+
<summary><strong>TypeScript</strong></summary>
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
const agent = phone.agent({
|
|
493
|
+
systemPrompt: "You are a booking assistant. Check availability before confirming.",
|
|
494
|
+
tools: [{
|
|
495
|
+
name: "check_availability",
|
|
496
|
+
description: "Check appointment availability for a given date",
|
|
497
|
+
parameters: {
|
|
498
|
+
type: "object",
|
|
499
|
+
properties: {
|
|
500
|
+
date: { type: "string", description: "ISO date, e.g. 2025-06-15" },
|
|
501
|
+
},
|
|
502
|
+
required: ["date"],
|
|
503
|
+
},
|
|
504
|
+
webhookUrl: "https://api.example.com/availability",
|
|
505
|
+
}],
|
|
506
|
+
});
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
</details>
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
### Built-in Tools: Transfer & End Call
|
|
514
|
+
|
|
515
|
+
`transfer_call` and `end_call` are automatically injected into every agent — no configuration needed.
|
|
516
|
+
|
|
517
|
+
- The agent calls `transfer_call` when it decides to route to a human (e.g. "Let me transfer you now.")
|
|
518
|
+
- The agent calls `end_call` when the conversation is complete (e.g. after a confirmed booking.)
|
|
519
|
+
|
|
520
|
+
---
|
|
521
|
+
|
|
522
|
+
### Call Recording
|
|
523
|
+
|
|
524
|
+
<details open>
|
|
525
|
+
<summary><strong>Python</strong></summary>
|
|
526
|
+
|
|
527
|
+
```python
|
|
528
|
+
await phone.serve(agent=agent, port=8000, recording=True)
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
</details>
|
|
532
|
+
|
|
533
|
+
<details>
|
|
534
|
+
<summary><strong>TypeScript</strong></summary>
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
await phone.serve({ agent, port: 8000, recording: true });
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
</details>
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
### Conversation History
|
|
545
|
+
|
|
546
|
+
Every callback receives `data.history` — the full conversation so far as a list of `{role, text}` turns.
|
|
547
|
+
|
|
548
|
+
<details open>
|
|
549
|
+
<summary><strong>Python</strong></summary>
|
|
550
|
+
|
|
551
|
+
```python
|
|
552
|
+
await phone.serve(
|
|
553
|
+
agent=agent,
|
|
554
|
+
port=8000,
|
|
555
|
+
on_transcript=lambda data: print(f"[{data['role']}] {data['text']}"),
|
|
556
|
+
on_call_end=lambda data: print(f"Full history: {data['history']}"),
|
|
557
|
+
)
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
</details>
|
|
561
|
+
|
|
562
|
+
<details>
|
|
563
|
+
<summary><strong>TypeScript</strong></summary>
|
|
564
|
+
|
|
565
|
+
```typescript
|
|
566
|
+
await phone.serve({
|
|
567
|
+
agent,
|
|
568
|
+
port: 8000,
|
|
569
|
+
onTranscript: (data) => console.log(`[${data.role}] ${data.text}`),
|
|
570
|
+
onCallEnd: (data) => console.log(`Full history:`, data.history),
|
|
571
|
+
});
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
</details>
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
### Custom Voice (choose your providers)
|
|
579
|
+
|
|
580
|
+
<details open>
|
|
581
|
+
<summary><strong>Python</strong></summary>
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
phone = Patter(api_key="pt_xxx", backend_url="ws://localhost:8000")
|
|
585
|
+
|
|
586
|
+
await phone.connect(
|
|
587
|
+
on_message=agent,
|
|
588
|
+
provider="twilio",
|
|
589
|
+
provider_key="ACxxxxxxxx",
|
|
590
|
+
provider_secret="your_auth_token",
|
|
591
|
+
number="+14155550000",
|
|
592
|
+
stt=Patter.deepgram(api_key="dg_xxx", language="en"),
|
|
593
|
+
tts=Patter.elevenlabs(api_key="el_xxx", voice="rachel"),
|
|
594
|
+
)
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
</details>
|
|
598
|
+
|
|
599
|
+
<details>
|
|
600
|
+
<summary><strong>TypeScript</strong></summary>
|
|
601
|
+
|
|
602
|
+
```typescript
|
|
603
|
+
const phone = new Patter({ apiKey: "pt_xxx", backendUrl: "ws://localhost:8000" });
|
|
604
|
+
|
|
605
|
+
await phone.connect({
|
|
606
|
+
onMessage: agent,
|
|
607
|
+
provider: "twilio",
|
|
608
|
+
providerKey: "ACxxxxxxxx",
|
|
609
|
+
providerSecret: "your_auth_token",
|
|
610
|
+
number: "+14155550000",
|
|
611
|
+
stt: Patter.deepgram({ apiKey: "dg_xxx", language: "en" }),
|
|
612
|
+
tts: Patter.elevenlabs({ apiKey: "el_xxx", voice: "rachel" }),
|
|
613
|
+
});
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
</details>
|
|
617
|
+
|
|
618
|
+
## Voice Modes
|
|
619
|
+
|
|
620
|
+
| Mode | Latency | Quality | Best For |
|
|
621
|
+
|---|---|---|---|
|
|
622
|
+
| **OpenAI Realtime** | Lowest | High | Fluid, low-latency conversations |
|
|
623
|
+
| **Deepgram + ElevenLabs** | Low | High | Independent control over STT and TTS |
|
|
624
|
+
| **ElevenLabs ConvAI** | Low | High | ElevenLabs-managed conversation flow |
|
|
625
|
+
|
|
626
|
+
The voice mode is configured on the backend. Your `on_message` handler works identically regardless of mode.
|
|
627
|
+
|
|
628
|
+
## MCP Server (Claude Desktop)
|
|
629
|
+
|
|
630
|
+
Patter ships an MCP server so you can control calls directly from Claude Desktop.
|
|
631
|
+
|
|
632
|
+
```json
|
|
633
|
+
{
|
|
634
|
+
"mcpServers": {
|
|
635
|
+
"patter": {
|
|
636
|
+
"command": "patter-mcp",
|
|
637
|
+
"env": { "PATTER_API_KEY": "pt_xxx" }
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
## Self-Hosting
|
|
644
|
+
|
|
645
|
+
Run the full stack yourself — no Patter Cloud account needed.
|
|
646
|
+
|
|
647
|
+
```bash
|
|
648
|
+
# 1. Clone the repo
|
|
649
|
+
git clone https://github.com/your-org/patter
|
|
650
|
+
cd patter
|
|
651
|
+
|
|
652
|
+
# 2. Copy env and fill in your keys
|
|
653
|
+
cp .env.example .env
|
|
654
|
+
|
|
655
|
+
# 3. Start the backend
|
|
656
|
+
cd backend
|
|
657
|
+
pip install -e ".[dev]"
|
|
658
|
+
alembic upgrade head
|
|
659
|
+
uvicorn app.main:app --reload
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
Then point your SDK at your local backend:
|
|
663
|
+
|
|
664
|
+
```python
|
|
665
|
+
phone = Patter(
|
|
666
|
+
api_key="pt_xxx",
|
|
667
|
+
backend_url="ws://localhost:8000",
|
|
668
|
+
rest_url="http://localhost:8000",
|
|
669
|
+
)
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**Required environment variables:**
|
|
673
|
+
|
|
674
|
+
| Variable | Description |
|
|
675
|
+
|---|---|
|
|
676
|
+
| `PATTER_DATABASE_URL` | PostgreSQL connection string |
|
|
677
|
+
| `PATTER_ENCRYPTION_KEY` | Key for encrypting stored provider credentials |
|
|
678
|
+
| `PATTER_SECRET_KEY` | JWT / HMAC signing secret |
|
|
679
|
+
|
|
680
|
+
See `backend/.env.example` for the full list.
|
|
681
|
+
|
|
682
|
+
## API Reference
|
|
683
|
+
|
|
684
|
+
### `Patter` (Python & TypeScript)
|
|
685
|
+
|
|
686
|
+
| Method | Description |
|
|
687
|
+
|---|---|
|
|
688
|
+
| `Patter(api_key, backend_url?, rest_url?)` | Create client. `backend_url` defaults to `wss://api.getpatter.com`. |
|
|
689
|
+
| `connect(on_message, ...)` | Connect and start receiving calls. Blocks until disconnected. |
|
|
690
|
+
| `call(to, first_message?, machine_detection?, voicemail_message?, ...)` | Place an outbound call. |
|
|
691
|
+
| `disconnect()` | Gracefully close the connection. |
|
|
692
|
+
|
|
693
|
+
**`serve()` options:**
|
|
694
|
+
|
|
695
|
+
| Option | Type | Description |
|
|
696
|
+
|---|---|---|
|
|
697
|
+
| `agent` | `Agent` | Agent configuration to use for calls |
|
|
698
|
+
| `port` | `int` | Port to listen on |
|
|
699
|
+
| `recording` | `bool` | Enable call recording via the telephony provider |
|
|
700
|
+
| `onCallStart` | `callable` | Called when a call connects; receives `data.caller`, `data.call_id` |
|
|
701
|
+
| `onCallEnd` | `callable` | Called when a call ends; receives `data.history`, `data.transcript`, `data.duration` |
|
|
702
|
+
| `onTranscript` | `callable` | Called on each transcript turn; receives `data.role`, `data.text`, `data.history` |
|
|
703
|
+
|
|
704
|
+
**`agent()` options:**
|
|
705
|
+
|
|
706
|
+
| Option | Type | Description |
|
|
707
|
+
|---|---|---|
|
|
708
|
+
| `system_prompt` | `str` | Prompt with optional `{variable}` placeholders |
|
|
709
|
+
| `variables` | `dict` | Values substituted into `system_prompt` and `first_message` |
|
|
710
|
+
| `voice` | `str` | TTS voice name |
|
|
711
|
+
| `language` | `str` | BCP-47 language code |
|
|
712
|
+
| `first_message` | `str` | Opening message (supports `{variable}` placeholders) |
|
|
713
|
+
| `tools` | `list` | Tool definitions with `name`, `description`, `parameters`, `webhook_url` |
|
|
714
|
+
|
|
715
|
+
**`call()` options:**
|
|
716
|
+
|
|
717
|
+
| Option | Type | Description |
|
|
718
|
+
|---|---|---|
|
|
719
|
+
| `to` | `str` | Destination phone number |
|
|
720
|
+
| `first_message` | `str` | Opening message for the outbound call |
|
|
721
|
+
| `machine_detection` | `bool` | Enable answering machine detection |
|
|
722
|
+
| `voicemail_message` | `str` | Message to play when voicemail is detected |
|
|
723
|
+
|
|
724
|
+
**Static provider helpers** (for self-hosted mode):
|
|
725
|
+
|
|
726
|
+
| Helper | Type | Description |
|
|
727
|
+
|---|---|---|
|
|
728
|
+
| `Patter.deepgram(api_key, language?)` | STT | Deepgram Nova |
|
|
729
|
+
| `Patter.whisper(api_key, language?)` | STT | OpenAI Whisper |
|
|
730
|
+
| `Patter.elevenlabs(api_key, voice?)` | TTS | ElevenLabs |
|
|
731
|
+
| `Patter.openai_tts(api_key, voice?)` | TTS | OpenAI TTS |
|
|
732
|
+
|
|
733
|
+
### `IncomingMessage`
|
|
734
|
+
|
|
735
|
+
| Field | Type | Description |
|
|
736
|
+
|---|---|---|
|
|
737
|
+
| `text` | `str` | Transcribed speech from the caller (includes `[DTMF: N]` for keypad presses) |
|
|
738
|
+
| `call_id` | `str` | Unique identifier for the current call |
|
|
739
|
+
| `history` | `list` | Conversation turns so far: `[{role, text}, ...]` |
|
|
740
|
+
|
|
741
|
+
## Contributing
|
|
742
|
+
|
|
743
|
+
Pull requests are welcome.
|
|
744
|
+
|
|
745
|
+
```bash
|
|
746
|
+
# Run tests
|
|
747
|
+
cd backend && pytest tests/ -v
|
|
748
|
+
cd sdk && pytest tests/ -v
|
|
749
|
+
|
|
750
|
+
# Install dev dependencies
|
|
751
|
+
cd backend && pip install -e ".[dev]"
|
|
752
|
+
cd sdk && pip install -e ".[dev]"
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
Please open an issue before submitting large changes so we can discuss the approach first.
|
|
756
|
+
|
|
757
|
+
## License
|
|
758
|
+
|
|
759
|
+
MIT — see [LICENSE](./LICENSE).
|