getpatter 0.4.3 → 0.5.0

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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <p align="center">
2
2
  <h1 align="center">Patter TypeScript SDK</h1>
3
- <p align="center">Connect AI agents to phone numbers with 4 lines of code</p>
3
+ <p align="center">Connect AI agents to phone numbers in four lines of code</p>
4
4
  </p>
5
5
 
6
6
  <p align="center">
@@ -28,52 +28,58 @@ Patter is the open-source SDK that gives your AI agent a phone number. Point it
28
28
  npm install getpatter
29
29
  ```
30
30
 
31
- ```typescript
32
- import { Patter } from "getpatter";
31
+ Set the env vars your carrier and engine need:
33
32
 
34
- const phone = new Patter({
35
- twilioSid: "AC...", twilioToken: "...",
36
- openaiKey: "sk-...",
37
- phoneNumber: "+1...",
38
- });
33
+ ```bash
34
+ export TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
35
+ export TWILIO_AUTH_TOKEN=your_auth_token
36
+ export OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxx
37
+ ```
39
38
 
40
- const agent = phone.agent({
41
- systemPrompt: "You are a friendly customer service agent for Acme Corp.",
42
- voice: "alloy",
43
- firstMessage: "Hello! Thanks for calling. How can I help?",
44
- });
39
+ Four lines of TypeScript:
40
+
41
+ ```typescript
42
+ import { Patter, Twilio, OpenAIRealtime } from "getpatter";
45
43
 
46
- await phone.serve({ agent, port: 8000 });
44
+ const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
45
+ const agent = phone.agent({ engine: new OpenAIRealtime(), systemPrompt: "You are a friendly receptionist for Acme Corp.", firstMessage: "Hello! How can I help?" });
46
+ await phone.serve({ agent, tunnel: true });
47
47
  ```
48
48
 
49
+ `tunnel: true` spawns a Cloudflare tunnel and points your Twilio number at it. In production, pass `webhookUrl: "api.prod.example.com"` to the constructor instead.
50
+
49
51
  ## Features
50
52
 
51
53
  | Feature | Method | Example |
52
54
  |---|---|---|
53
- | Inbound calls | `phone.serve(agent)` | Answer calls as an AI |
54
- | Outbound calls + AMD | `phone.call(to, machineDetection)` | Place calls with voicemail detection |
55
- | Tool calling (webhooks) | `agent(tools=[...])` | Agent calls external APIs mid-conversation |
56
- | Custom STT + TTS | `agent(provider="pipeline")` | Bring your own voice providers |
57
- | Dynamic variables | `agent(variables={...})` | Personalize prompts per caller |
58
- | Custom LLM (any model) | `serve(onMessage=handler)` | Claude, Mistral, LLaMA, etc. |
59
- | Call recording | `serve(recording=true)` | Record all calls |
55
+ | Inbound calls | `phone.serve({ agent })` | Answer calls as an AI |
56
+ | Outbound calls + AMD | `phone.call({ to, machineDetection: true })` | Place calls with voicemail detection |
57
+ | Tool calling | `agent({ tools: [tool(...)] })` | Agent calls external APIs mid-conversation |
58
+ | Custom STT + TTS | `agent({ stt: new DeepgramSTT(), tts: new ElevenLabsTTS() })` | Bring your own voice providers |
59
+ | Dynamic variables | `agent({ variables: {...} })` | Personalize prompts per caller |
60
+ | Custom LLM (any model) | `serve({ onMessage })` | Claude, Mistral, LLaMA, etc. |
61
+ | Call recording | `serve({ recording: true })` | Record all calls |
60
62
  | Call transfer | `transfer_call` (auto-injected) | Transfer to a human |
61
- | Voicemail drop | `call(voicemailMessage="...")` | Play message on voicemail |
63
+ | Voicemail drop | `call({ voicemailMessage: "..." })` | Play message on voicemail |
62
64
 
63
65
  ## Configuration
64
66
 
65
- ### Environment Variables
67
+ ### Environment variables
66
68
 
67
- | Variable | Required | Description |
68
- |---|---|---|
69
- | `OPENAI_API_KEY` | Yes (Realtime mode) | OpenAI API key with Realtime access |
70
- | `TWILIO_ACCOUNT_SID` | Yes | Twilio account SID |
71
- | `TWILIO_AUTH_TOKEN` | Yes | Twilio auth token |
72
- | `TWILIO_PHONE_NUMBER` | Yes | Your Twilio phone number (E.164) |
73
- | `DEEPGRAM_API_KEY` | Pipeline mode | Deepgram STT key |
74
- | `ELEVENLABS_API_KEY` | Pipeline mode | ElevenLabs TTS key |
75
- | `ANTHROPIC_API_KEY` | Custom LLM | For bringing your own model |
76
- | `WEBHOOK_URL` | No | Public URL (auto-tunneled via Cloudflare if omitted) |
69
+ Every provider reads its credentials from the environment by default. Pass `apiKey: "..."` to any constructor to override.
70
+
71
+ | Variable | Used by |
72
+ |---|---|
73
+ | `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN` | `new Twilio()` carrier |
74
+ | `TELNYX_API_KEY`, `TELNYX_CONNECTION_ID`, `TELNYX_PUBLIC_KEY` (optional) | `new Telnyx()` carrier |
75
+ | `OPENAI_API_KEY` | `OpenAIRealtime`, `WhisperSTT`, `OpenAITTS` |
76
+ | `ELEVENLABS_API_KEY`, `ELEVENLABS_AGENT_ID` | `ElevenLabsConvAI`, `ElevenLabsTTS` |
77
+ | `DEEPGRAM_API_KEY` | `DeepgramSTT` |
78
+ | `CARTESIA_API_KEY` | `CartesiaSTT`, `CartesiaTTS` |
79
+ | `RIME_API_KEY` | `RimeTTS` |
80
+ | `LMNT_API_KEY` | `LMNTTTS` |
81
+ | `SONIOX_API_KEY` | `SonioxSTT` |
82
+ | `ASSEMBLYAI_API_KEY` | `AssemblyAISTT` |
77
83
 
78
84
  ```bash
79
85
  cp .env.example .env
@@ -87,219 +93,187 @@ cp .env.example .env
87
93
  | Mode | Latency | Quality | Best For |
88
94
  |---|---|---|---|
89
95
  | **OpenAI Realtime** | Lowest | High | Fluid, low-latency conversations |
90
- | **Deepgram + ElevenLabs** | Low | High | Independent control over STT and TTS |
96
+ | **Pipeline** (STT + LLM + TTS) | Low | High | Independent control over STT and TTS |
91
97
  | **ElevenLabs ConvAI** | Low | High | ElevenLabs-managed conversation flow |
92
98
 
93
99
  ## API Reference
94
100
 
95
- ### `Patter` Constructor
101
+ ### `Patter` constructor
96
102
 
97
103
  ```typescript
98
104
  new Patter({
99
- twilioSid: string;
100
- twilioToken: string;
101
- openaiKey: string;
105
+ carrier: Twilio | Telnyx;
102
106
  phoneNumber: string;
103
- webhookUrl?: string; // Optional; auto-tunneled via Cloudflare if omitted
107
+ webhookUrl?: string; // Public hostname. Mutually exclusive with tunnel.
108
+ tunnel?: CloudflareTunnel | StaticTunnel; // Or pass tunnel: true on serve() for dev.
104
109
  })
105
110
  ```
106
111
 
107
112
  | Parameter | Type | Description |
108
113
  |---|---|---|
109
- | `twilioSid` | `string` | Twilio account SID |
110
- | `twilioToken` | `string` | Twilio auth token |
111
- | `openaiKey` | `string` | OpenAI API key |
112
- | `phoneNumber` | `string` | Your Twilio phone number (E.164 format) |
113
- | `webhookUrl` | `string` | Public URL for Twilio webhooks (optional) |
114
+ | `carrier` | `Twilio` / `Telnyx` | Carrier instance. Reads env vars by default. |
115
+ | `phoneNumber` | `string` | Your phone number in E.164 format. |
116
+ | `webhookUrl` | `string` | Public hostname your local server is reachable on. |
117
+ | `tunnel` | instance | `new CloudflareTunnel()` or `new StaticTunnel({ hostname: ... })`. |
114
118
 
115
- ### `phone.agent()` Method
119
+ ### `phone.agent()`
116
120
 
117
121
  ```typescript
118
122
  phone.agent({
119
123
  systemPrompt: string;
124
+ engine?: OpenAIRealtime | ElevenLabsConvAI; // default: new OpenAIRealtime()
125
+ stt?: STTProvider; // e.g. new DeepgramSTT()
126
+ tts?: TTSProvider; // e.g. new ElevenLabsTTS()
120
127
  voice?: string;
128
+ model?: string;
129
+ language?: string;
121
130
  firstMessage?: string;
131
+ tools?: Tool[];
132
+ guardrails?: Guardrail[];
122
133
  variables?: Record<string, string>;
123
- tools?: Array<{name, description, parameters, webhookUrl}>;
124
134
  })
125
135
  ```
126
136
 
127
- | Parameter | Type | Description |
128
- |---|---|---|
129
- | `systemPrompt` | `string` | Prompt with optional `{variable}` placeholders |
130
- | `voice` | `string` | TTS voice name (e.g., "alloy", "echo", "fable") |
131
- | `firstMessage` | `string` | Opening message (supports `{variable}` placeholders) |
132
- | `variables` | `Record<string, string>` | Values substituted into prompts |
133
- | `tools` | `Array` | Tool definitions: `{name, description, parameters, webhookUrl}` |
137
+ Pass `engine` for end-to-end mode, `stt` + `tts` for pipeline mode. Both arguments may take plain adapter instances (e.g. `new DeepgramSTT()`) that read their API key from the environment.
134
138
 
135
- ### `phone.serve()` Method
139
+ ### `phone.serve()`
136
140
 
137
141
  ```typescript
138
142
  await phone.serve({
139
143
  agent: Agent;
140
144
  port?: number;
145
+ tunnel?: boolean; // shortcut for Patter({ tunnel: new CloudflareTunnel() })
141
146
  dashboard?: boolean;
142
147
  recording?: boolean;
143
- onCallStart?: (data: CallData) => Promise<void>;
144
- onCallEnd?: (data: CallData) => Promise<void>;
145
- onTranscript?: (data: TranscriptData) => Promise<void>;
146
- })
148
+ onCallStart?: (data) => Promise<void>;
149
+ onCallEnd?: (data) => Promise<void>;
150
+ onTranscript?: (data) => Promise<void>;
151
+ onMessage?: (data) => Promise<string> | string;
152
+ voicemailMessage?: string;
153
+ dashboardToken?: string;
154
+ });
147
155
  ```
148
156
 
149
- | Parameter | Type | Description |
150
- |---|---|---|
151
- | `agent` | `Agent` | Agent configuration to use for calls |
152
- | `port` | `number` | Port to listen on (default: 8000) |
153
- | `dashboard` | `boolean` | Enable the built-in monitoring dashboard |
154
- | `recording` | `boolean` | Enable call recording via the telephony provider |
155
- | `onCallStart` | `(data) => Promise<void>` | Called when a call connects; receives `data.caller`, `data.callId` |
156
- | `onCallEnd` | `(data) => Promise<void>` | Called when a call ends; receives `data.history`, `data.transcript`, `data.duration` |
157
- | `onTranscript` | `(data) => Promise<void>` | Called on each transcript turn; receives `data.role`, `data.text`, `data.history` |
158
-
159
- ### `phone.call()` Method
157
+ ### `phone.call()`
160
158
 
161
159
  ```typescript
162
160
  await phone.call({
163
161
  to: string;
162
+ agent?: Agent;
163
+ from?: string;
164
164
  firstMessage?: string;
165
165
  machineDetection?: boolean;
166
166
  voicemailMessage?: string;
167
- })
167
+ ringTimeout?: number;
168
+ });
168
169
  ```
169
170
 
170
- | Parameter | Type | Description |
171
- |---|---|---|
172
- | `to` | `string` | Destination phone number (E.164 format) |
173
- | `firstMessage` | `string` | Opening message for the outbound call |
174
- | `machineDetection` | `boolean` | Enable answering machine detection |
175
- | `voicemailMessage` | `string` | Message to play when voicemail is detected |
176
-
177
- ### Static Provider Helpers
171
+ ### STT / TTS catalog
178
172
 
179
173
  ```typescript
180
- Patter.deepgram(options: { apiKey: string; language?: string }) -> STT
181
- Patter.elevenlabs(options: { apiKey: string; voice?: string }) -> TTS
182
- Patter.openaiTts(options: { apiKey: string; voice?: string }) -> TTS
183
- Patter.whisper(options: { apiKey: string; language?: string }) -> STT
174
+ import {
175
+ // Carriers
176
+ Twilio, Telnyx,
177
+ // Engines
178
+ OpenAIRealtime, ElevenLabsConvAI,
179
+ // STT
180
+ DeepgramSTT, WhisperSTT, CartesiaSTT, SonioxSTT, AssemblyAISTT,
181
+ // TTS
182
+ ElevenLabsTTS, OpenAITTS, CartesiaTTS, RimeTTS, LMNTTTS,
183
+ // Tunnels
184
+ CloudflareTunnel, StaticTunnel,
185
+ // Primitives
186
+ Tool, Guardrail, tool, guardrail,
187
+ } from "getpatter";
184
188
  ```
185
189
 
190
+ Every class reads its API key from the environment by default, so `new DeepgramSTT()` / `new ElevenLabsTTS()` work out of the box when the corresponding env var is set.
191
+
186
192
  ## Examples
187
193
 
188
- ### Inbound Calls (AI answers the phone)
194
+ ### Inbound calls default engine
189
195
 
190
196
  ```typescript
191
- import { Patter, IncomingMessage } from "getpatter";
197
+ import { Patter, Twilio, OpenAIRealtime } from "getpatter";
192
198
 
193
- const phone = new Patter({
194
- twilioSid: "AC...", twilioToken: "...",
195
- openaiKey: "sk-...",
196
- phoneNumber: "+1...",
199
+ const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
200
+ const agent = phone.agent({
201
+ engine: new OpenAIRealtime(),
202
+ systemPrompt: "You are a helpful customer service agent.",
203
+ firstMessage: "Hello! How can I help?",
197
204
  });
198
205
 
199
- async function agent(msg: IncomingMessage): Promise<string> {
200
- if (msg.text.toLowerCase().includes("hours")) {
201
- return "We're open Monday through Friday, 9 to 5.";
202
- }
203
- return "How can I help you today?";
204
- }
205
-
206
206
  await phone.serve({
207
- agent: phone.agent({
208
- systemPrompt: "You are a helpful customer service agent.",
209
- firstMessage: "Hello! How can I help?",
210
- }),
211
- port: 8000,
207
+ agent,
208
+ tunnel: true,
212
209
  onCallStart: (data) => console.log(`Call from ${data.caller}`),
213
- onCallEnd: (data) => console.log("Call ended"),
210
+ onCallEnd: () => console.log("Call ended"),
214
211
  });
215
212
  ```
216
213
 
217
- ### Outbound Calls (AI calls someone)
214
+ ### Custom voice Deepgram STT + ElevenLabs TTS
218
215
 
219
216
  ```typescript
220
- import { Patter } from "getpatter";
221
-
222
- const phone = new Patter({
223
- twilioSid: "AC...", twilioToken: "...",
224
- openaiKey: "sk-...",
225
- phoneNumber: "+1...",
226
- });
217
+ import { Patter, Twilio, DeepgramSTT, ElevenLabsTTS } from "getpatter";
227
218
 
228
- const agentConfig = phone.agent({
229
- systemPrompt: "You are making reminder calls.",
230
- firstMessage: "Hi, this is an automated reminder from Acme Corp.",
231
- });
232
-
233
- await phone.serve({ agent: agentConfig, port: 8000 });
234
- await phone.call({
235
- to: "+14155551234",
236
- firstMessage: "Hi, just checking in.",
237
- });
238
- ```
239
-
240
- ### Tool Calling (Agent calls external APIs)
241
-
242
- ```typescript
219
+ const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
243
220
  const agent = phone.agent({
244
- systemPrompt: "You are a booking assistant. Check availability before confirming.",
245
- tools: [{
246
- name: "check_availability",
247
- description: "Check appointment availability for a given date",
248
- parameters: {
249
- type: "object",
250
- properties: {
251
- date: { type: "string", description: "ISO date, e.g. 2025-06-15" },
252
- },
253
- required: ["date"],
254
- },
255
- webhookUrl: "https://api.example.com/availability",
256
- }],
221
+ stt: new DeepgramSTT(), // reads DEEPGRAM_API_KEY
222
+ tts: new ElevenLabsTTS({ voice: "rachel" }), // reads ELEVENLABS_API_KEY
223
+ systemPrompt: "You are a helpful voice assistant.",
257
224
  });
225
+ await phone.serve({ agent, tunnel: true });
258
226
  ```
259
227
 
260
- ### Custom Voice (Deepgram STT + ElevenLabs TTS)
228
+ ### Tool calling
261
229
 
262
230
  ```typescript
263
- const phone = new Patter({
264
- twilioSid: "AC...", twilioToken: "...",
265
- openaiKey: "sk-...",
266
- phoneNumber: "+1...",
231
+ import { Patter, Twilio, OpenAIRealtime, tool } from "getpatter";
232
+
233
+ const checkAvailability = tool({
234
+ name: "check_availability",
235
+ description: "Check appointment availability for a given ISO date.",
236
+ parameters: {
237
+ type: "object",
238
+ properties: { date: { type: "string" } },
239
+ required: ["date"],
240
+ },
241
+ handler: async ({ date }) => ({ available: true }),
267
242
  });
268
243
 
244
+ const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
269
245
  const agent = phone.agent({
270
- systemPrompt: "You are a helpful voice assistant.",
271
- voice: "aria",
272
- });
273
-
274
- // Use custom STT and TTS in pipeline mode
275
- await phone.serve({
276
- agent,
277
- port: 8000,
278
- stt: Patter.deepgram({ apiKey: "dg_...", language: "en" }),
279
- tts: Patter.elevenlabs({ apiKey: "el_...", voice: "aria" }),
246
+ engine: new OpenAIRealtime(),
247
+ systemPrompt: "You are a booking assistant.",
248
+ tools: [checkAvailability],
280
249
  });
250
+ await phone.serve({ agent, tunnel: true });
281
251
  ```
282
252
 
283
- ### Call Recording
253
+ ### Outbound calls
284
254
 
285
255
  ```typescript
286
- await phone.serve({
287
- agent,
288
- port: 8000,
289
- recording: true, // Records all inbound and outbound calls
256
+ import { Patter, Twilio, OpenAIRealtime } from "getpatter";
257
+
258
+ const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
259
+ const agent = phone.agent({
260
+ engine: new OpenAIRealtime(),
261
+ systemPrompt: "You are making reminder calls.",
262
+ firstMessage: "Hi, this is a reminder from Acme Corp.",
290
263
  });
264
+
265
+ await phone.serve({ agent, tunnel: true });
266
+ await phone.call({ to: "+14155551234", agent });
291
267
  ```
292
268
 
293
- ### Dynamic Variables in Prompts
269
+ ### Dynamic variables
294
270
 
295
271
  ```typescript
296
272
  const agent = phone.agent({
273
+ engine: new OpenAIRealtime(),
297
274
  systemPrompt: "You are helping {customer_name}, account #{account_id}.",
298
275
  firstMessage: "Hi {customer_name}! How can I help you today?",
299
- variables: {
300
- customer_name: "Jane",
301
- account_id: "A-789",
302
- },
276
+ variables: { customer_name: "Jane", account_id: "A-789" },
303
277
  });
304
278
  ```
305
279
 
@@ -0,0 +1,84 @@
1
+ import {
2
+ getLogger
3
+ } from "./chunk-FMNRCP5X.mjs";
4
+ import "./chunk-OOIUSZB4.mjs";
5
+
6
+ // src/carrier-config.ts
7
+ var TWILIO_API_BASE = "https://api.twilio.com/2010-04-01";
8
+ var TELNYX_API_BASE = "https://api.telnyx.com/v2";
9
+ async function configureTwilioNumber(accountSid, authToken, phoneNumber, voiceUrl) {
10
+ const auth = `Basic ${Buffer.from(`${accountSid}:${authToken}`).toString("base64")}`;
11
+ const listUrl = `${TWILIO_API_BASE}/Accounts/${accountSid}/IncomingPhoneNumbers.json?PhoneNumber=${encodeURIComponent(phoneNumber)}`;
12
+ const listResp = await fetch(listUrl, {
13
+ method: "GET",
14
+ headers: { Authorization: auth }
15
+ });
16
+ if (!listResp.ok) {
17
+ throw new Error(
18
+ `Twilio IncomingPhoneNumbers.list failed: ${listResp.status} ${await listResp.text()}`
19
+ );
20
+ }
21
+ const body = await listResp.json();
22
+ const match = body.incoming_phone_numbers?.[0];
23
+ if (!match) {
24
+ throw new Error(`Twilio number ${phoneNumber} not found on account ${accountSid}`);
25
+ }
26
+ const updateUrl = `${TWILIO_API_BASE}/Accounts/${accountSid}/IncomingPhoneNumbers/${match.sid}.json`;
27
+ const form = new URLSearchParams({ VoiceUrl: voiceUrl, VoiceMethod: "POST" });
28
+ const updateResp = await fetch(updateUrl, {
29
+ method: "POST",
30
+ headers: {
31
+ Authorization: auth,
32
+ "Content-Type": "application/x-www-form-urlencoded"
33
+ },
34
+ body: form.toString()
35
+ });
36
+ if (!updateResp.ok) {
37
+ throw new Error(
38
+ `Twilio IncomingPhoneNumbers.update failed: ${updateResp.status} ${await updateResp.text()}`
39
+ );
40
+ }
41
+ }
42
+ async function configureTelnyxNumber(apiKey, connectionId, phoneNumber) {
43
+ const resp = await fetch(`${TELNYX_API_BASE}/phone_numbers/${encodeURIComponent(phoneNumber)}`, {
44
+ method: "PATCH",
45
+ headers: {
46
+ Authorization: `Bearer ${apiKey}`,
47
+ "Content-Type": "application/json"
48
+ },
49
+ body: JSON.stringify({ connection_id: connectionId })
50
+ });
51
+ if (!resp.ok) {
52
+ throw new Error(
53
+ `Telnyx PATCH /phone_numbers/${phoneNumber} failed: ${resp.status} ${await resp.text()}`
54
+ );
55
+ }
56
+ }
57
+ async function autoConfigureCarrier(params) {
58
+ const log = getLogger();
59
+ const provider = params.telephonyProvider ?? (params.twilioSid ? "twilio" : "telnyx");
60
+ if (provider === "twilio" && params.twilioSid && params.twilioToken) {
61
+ const voiceUrl = `https://${params.webhookHost}/webhooks/twilio/voice`;
62
+ try {
63
+ await configureTwilioNumber(params.twilioSid, params.twilioToken, params.phoneNumber, voiceUrl);
64
+ log.info("Twilio webhook set to %s", voiceUrl);
65
+ } catch (err) {
66
+ log.warn("Could not auto-configure Twilio webhook: %s", err instanceof Error ? err.message : String(err));
67
+ log.info("Set webhook manually to: %s", voiceUrl);
68
+ }
69
+ return;
70
+ }
71
+ if (provider === "telnyx" && params.telnyxKey && params.telnyxConnectionId) {
72
+ try {
73
+ await configureTelnyxNumber(params.telnyxKey, params.telnyxConnectionId, params.phoneNumber);
74
+ log.info("Telnyx number %s associated with connection %s", params.phoneNumber, params.telnyxConnectionId);
75
+ } catch (err) {
76
+ log.warn("Could not auto-configure Telnyx number: %s", err instanceof Error ? err.message : String(err));
77
+ }
78
+ }
79
+ }
80
+ export {
81
+ autoConfigureCarrier,
82
+ configureTelnyxNumber,
83
+ configureTwilioNumber
84
+ };