getpatter 0.4.1 → 0.4.3

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.
Files changed (35) hide show
  1. package/README.md +185 -587
  2. package/dist/chunk-35EVXMGB.mjs +4472 -0
  3. package/dist/chunk-AFUYSNDH.mjs +31 -0
  4. package/dist/chunk-JO5C35FM.mjs +65 -0
  5. package/dist/chunk-OOIUSZB4.mjs +37 -0
  6. package/dist/cli.js +1139 -0
  7. package/dist/index.d.mts +1063 -85
  8. package/dist/index.d.ts +1063 -85
  9. package/dist/index.js +8969 -3904
  10. package/dist/index.mjs +2382 -3354
  11. package/dist/lib-4WCAS54J.mjs +830 -0
  12. package/dist/node-cron-373UVDIO.mjs +935 -0
  13. package/dist/persistence-CYIGNHSU.mjs +7 -0
  14. package/dist/resources/audio/NOTICE +2 -0
  15. package/dist/resources/audio/city-ambience.ogg +0 -0
  16. package/dist/resources/audio/crowded-room.ogg +0 -0
  17. package/dist/resources/audio/forest-ambience.ogg +0 -0
  18. package/dist/resources/audio/hold_music.ogg +0 -0
  19. package/dist/resources/audio/keyboard-typing.ogg +0 -0
  20. package/dist/resources/audio/keyboard-typing2.ogg +0 -0
  21. package/dist/resources/audio/office-ambience.ogg +0 -0
  22. package/dist/resources/silero_vad.onnx +0 -0
  23. package/dist/{test-mode-JMXZSAJS.mjs → test-mode-RH65MMSP.mjs} +2 -1
  24. package/dist/{tunnel-HYSU7EF2.mjs → tunnel-BL7A7GXW.mjs} +2 -1
  25. package/package.json +25 -8
  26. package/src/resources/audio/NOTICE +2 -0
  27. package/src/resources/audio/city-ambience.ogg +0 -0
  28. package/src/resources/audio/crowded-room.ogg +0 -0
  29. package/src/resources/audio/forest-ambience.ogg +0 -0
  30. package/src/resources/audio/hold_music.ogg +0 -0
  31. package/src/resources/audio/keyboard-typing.ogg +0 -0
  32. package/src/resources/audio/keyboard-typing2.ogg +0 -0
  33. package/src/resources/audio/office-ambience.ogg +0 -0
  34. package/dist/chunk-TAATEHKF.mjs +0 -396
  35. package/dist/chunk-VNU4GNW3.mjs +0 -45
@@ -0,0 +1,7 @@
1
+ import {
2
+ notifyDashboard
3
+ } from "./chunk-AFUYSNDH.mjs";
4
+ import "./chunk-OOIUSZB4.mjs";
5
+ export {
6
+ notifyDashboard
7
+ };
@@ -0,0 +1,2 @@
1
+ keyboard-typing.ogg by Anton -- https://freesound.org/s/137/ -- License: Attribution 4.0
2
+ keyboard-typing2.opg by Anton -- https://freesound.org/s/137/ -- License: Attribution 4.0
Binary file
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  TestSession
3
- } from "./chunk-TAATEHKF.mjs";
3
+ } from "./chunk-35EVXMGB.mjs";
4
4
  import "./chunk-FMNRCP5X.mjs";
5
+ import "./chunk-OOIUSZB4.mjs";
5
6
  export {
6
7
  TestSession
7
8
  };
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  startTunnel
3
- } from "./chunk-VNU4GNW3.mjs";
3
+ } from "./chunk-JO5C35FM.mjs";
4
4
  import "./chunk-FMNRCP5X.mjs";
5
+ import "./chunk-OOIUSZB4.mjs";
5
6
  export {
6
7
  startTunnel
7
8
  };
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "getpatter",
3
- "version": "0.4.1",
4
- "description": "Connect AI agents to phone numbers with 10 lines of code",
3
+ "version": "0.4.3",
4
+ "description": "Open-source voice AI SDK — connect any AI agent to real phone calls in 4 lines of code",
5
5
  "license": "MIT",
6
6
  "author": {
7
- "name": "Nicolò Tognoni",
8
- "email": "nicolo@getpatter.com"
7
+ "name": "PatterAI",
8
+ "email": "hello@getpatter.com"
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
@@ -19,6 +19,7 @@
19
19
  "keywords": [
20
20
  "voice",
21
21
  "ai",
22
+ "voice-ai",
22
23
  "phone",
23
24
  "telephony",
24
25
  "speech",
@@ -27,7 +28,15 @@
27
28
  "twilio",
28
29
  "telnyx",
29
30
  "openai",
30
- "elevenlabs"
31
+ "elevenlabs",
32
+ "llm",
33
+ "agent",
34
+ "ai-agent",
35
+ "conversational-ai",
36
+ "realtime",
37
+ "call",
38
+ "ivr",
39
+ "chatbot"
31
40
  ],
32
41
  "main": "dist/index.js",
33
42
  "module": "dist/index.mjs",
@@ -39,12 +48,17 @@
39
48
  "require": "./dist/index.js"
40
49
  }
41
50
  },
51
+ "bin": {
52
+ "getpatter": "dist/cli.js"
53
+ },
42
54
  "sideEffects": false,
43
55
  "files": [
44
- "dist"
56
+ "dist",
57
+ "src/resources/audio/NOTICE",
58
+ "src/resources/audio/*.ogg"
45
59
  ],
46
60
  "scripts": {
47
- "build": "tsup src/index.ts --format cjs,esm --dts",
61
+ "build": "tsup src/index.ts --format cjs,esm --dts && tsup src/cli.ts --format cjs --no-dts && node -e \"const fs=require('fs');fs.cpSync('src/resources','dist/resources',{recursive:true,filter:p=>!p.endsWith('.ts')})\"",
48
62
  "prepublishOnly": "npm run build",
49
63
  "test": "vitest run",
50
64
  "test:watch": "vitest",
@@ -55,7 +69,10 @@
55
69
  "ws": "^8.18.0"
56
70
  },
57
71
  "optionalDependencies": {
58
- "cloudflared": "^1.0.0"
72
+ "cloudflared": "^0.7.1",
73
+ "onnxruntime-node": "^1.18.0",
74
+ "node-cron": "^3.0.3",
75
+ "@google/genai": "^0.3.0"
59
76
  },
60
77
  "devDependencies": {
61
78
  "@playwright/test": "^1.59.1",
@@ -0,0 +1,2 @@
1
+ keyboard-typing.ogg by Anton -- https://freesound.org/s/137/ -- License: Attribution 4.0
2
+ keyboard-typing2.opg by Anton -- https://freesound.org/s/137/ -- License: Attribution 4.0
@@ -1,396 +0,0 @@
1
- import {
2
- getLogger
3
- } from "./chunk-FMNRCP5X.mjs";
4
-
5
- // src/test-mode.ts
6
- import { createInterface } from "readline";
7
-
8
- // src/llm-loop.ts
9
- var OpenAILLMProvider = class {
10
- apiKey;
11
- model;
12
- constructor(apiKey, model) {
13
- this.apiKey = apiKey;
14
- this.model = model;
15
- }
16
- async *stream(messages, tools) {
17
- const body = {
18
- model: this.model,
19
- messages,
20
- stream: true
21
- };
22
- if (tools) {
23
- body.tools = tools;
24
- }
25
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
26
- method: "POST",
27
- headers: {
28
- "Content-Type": "application/json",
29
- "Authorization": `Bearer ${this.apiKey}`
30
- },
31
- body: JSON.stringify(body)
32
- });
33
- if (!response.ok) {
34
- const errText = await response.text();
35
- getLogger().error(`LLM API error: ${response.status} ${errText}`);
36
- return;
37
- }
38
- const reader = response.body?.getReader();
39
- if (!reader) return;
40
- const decoder = new TextDecoder();
41
- let buffer = "";
42
- while (true) {
43
- const { done, value } = await reader.read();
44
- if (done) break;
45
- buffer += decoder.decode(value, { stream: true });
46
- const lines = buffer.split("\n");
47
- buffer = lines.pop() || "";
48
- for (const line of lines) {
49
- const trimmed = line.trim();
50
- if (!trimmed || !trimmed.startsWith("data: ")) continue;
51
- const data = trimmed.slice(6);
52
- if (data === "[DONE]") continue;
53
- let chunk;
54
- try {
55
- chunk = JSON.parse(data);
56
- } catch {
57
- continue;
58
- }
59
- const delta = chunk.choices?.[0]?.delta;
60
- if (!delta) continue;
61
- if (delta.content) {
62
- yield { type: "text", content: delta.content };
63
- }
64
- if (delta.tool_calls) {
65
- for (const tc of delta.tool_calls) {
66
- yield {
67
- type: "tool_call",
68
- index: tc.index,
69
- id: tc.id,
70
- name: tc.function?.name,
71
- arguments: tc.function?.arguments
72
- };
73
- }
74
- }
75
- }
76
- }
77
- }
78
- };
79
- var LLMLoop = class {
80
- provider;
81
- systemPrompt;
82
- tools;
83
- openaiTools;
84
- toolMap;
85
- constructor(apiKey, model, systemPrompt, tools, llmProvider) {
86
- this.provider = llmProvider ?? new OpenAILLMProvider(apiKey, model);
87
- this.systemPrompt = systemPrompt;
88
- this.tools = tools ?? null;
89
- this.toolMap = /* @__PURE__ */ new Map();
90
- this.openaiTools = null;
91
- if (this.tools && this.tools.length > 0) {
92
- this.openaiTools = [];
93
- for (const t of this.tools) {
94
- this.openaiTools.push({
95
- type: "function",
96
- function: {
97
- name: t.name,
98
- description: t.description || "",
99
- parameters: t.parameters || { type: "object", properties: {} }
100
- }
101
- });
102
- this.toolMap.set(t.name, t);
103
- }
104
- }
105
- }
106
- /**
107
- * Stream LLM response tokens, handling tool calls automatically.
108
- * Yields text tokens as they arrive from the LLM.
109
- */
110
- async *run(userText, history, callContext) {
111
- const messages = this.buildMessages(history, userText);
112
- const maxIterations = 10;
113
- for (let iter = 0; iter < maxIterations; iter++) {
114
- const toolCallsAccumulated = /* @__PURE__ */ new Map();
115
- const textParts = [];
116
- let hasToolCalls = false;
117
- for await (const chunk of this.provider.stream(messages, this.openaiTools)) {
118
- if (chunk.type === "text" && chunk.content) {
119
- textParts.push(chunk.content);
120
- yield chunk.content;
121
- } else if (chunk.type === "tool_call") {
122
- hasToolCalls = true;
123
- const idx = chunk.index ?? 0;
124
- if (!toolCallsAccumulated.has(idx)) {
125
- toolCallsAccumulated.set(idx, { id: "", name: "", arguments: "" });
126
- }
127
- const acc = toolCallsAccumulated.get(idx);
128
- if (chunk.id) acc.id = chunk.id;
129
- if (chunk.name) acc.name = chunk.name;
130
- if (chunk.arguments) acc.arguments += chunk.arguments;
131
- }
132
- }
133
- if (!hasToolCalls) return;
134
- const assistantMsg = {
135
- role: "assistant",
136
- content: textParts.join("") || null,
137
- tool_calls: []
138
- };
139
- const sortedIndices = [...toolCallsAccumulated.keys()].sort((a, b) => a - b);
140
- for (const idx of sortedIndices) {
141
- const tc = toolCallsAccumulated.get(idx);
142
- assistantMsg.tool_calls.push({
143
- id: tc.id,
144
- type: "function",
145
- function: { name: tc.name, arguments: tc.arguments }
146
- });
147
- }
148
- messages.push(assistantMsg);
149
- for (const tcData of assistantMsg.tool_calls) {
150
- const toolName = tcData.function.name;
151
- let args;
152
- try {
153
- args = JSON.parse(tcData.function.arguments);
154
- } catch {
155
- args = {};
156
- }
157
- const result = await this.executeTool(toolName, args, callContext);
158
- messages.push({
159
- role: "tool",
160
- tool_call_id: tcData.id,
161
- content: result
162
- });
163
- }
164
- }
165
- getLogger().warn(`LLM loop hit max iterations (${maxIterations})`);
166
- }
167
- async executeTool(toolName, args, callContext) {
168
- const toolDef = this.toolMap.get(toolName);
169
- if (!toolDef) {
170
- return JSON.stringify({ error: `Unknown tool: ${toolName}` });
171
- }
172
- if (toolDef.handler) {
173
- try {
174
- return await toolDef.handler(args, callContext);
175
- } catch (e) {
176
- return JSON.stringify({ error: `Tool handler error: ${String(e)}` });
177
- }
178
- }
179
- if (toolDef.webhookUrl) {
180
- for (let attempt = 0; attempt < 3; attempt++) {
181
- try {
182
- const resp = await fetch(toolDef.webhookUrl, {
183
- method: "POST",
184
- headers: { "Content-Type": "application/json" },
185
- body: JSON.stringify({
186
- tool: toolName,
187
- arguments: args,
188
- ...callContext,
189
- attempt: attempt + 1
190
- }),
191
- signal: AbortSignal.timeout(1e4)
192
- });
193
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
194
- const result = JSON.stringify(await resp.json());
195
- const MAX_RESPONSE_BYTES = 1 * 1024 * 1024;
196
- if (result.length > MAX_RESPONSE_BYTES) {
197
- return JSON.stringify({ error: `Webhook response too large: ${result.length} bytes (max ${MAX_RESPONSE_BYTES})`, fallback: true });
198
- }
199
- return result;
200
- } catch (e) {
201
- if (attempt < 2) {
202
- await new Promise((r) => setTimeout(r, 500));
203
- } else {
204
- return JSON.stringify({ error: `Tool failed after 3 attempts: ${String(e)}` });
205
- }
206
- }
207
- }
208
- }
209
- return JSON.stringify({ error: `No handler or webhookUrl for tool '${toolName}'` });
210
- }
211
- buildMessages(history, userText) {
212
- const messages = [
213
- { role: "system", content: this.systemPrompt }
214
- ];
215
- for (const entry of history) {
216
- messages.push({
217
- role: entry.role === "assistant" ? "assistant" : "user",
218
- content: entry.text
219
- });
220
- }
221
- messages.push({ role: "user", content: userText });
222
- return messages;
223
- }
224
- };
225
-
226
- // src/test-mode.ts
227
- var TestSession = class {
228
- async run(opts) {
229
- const { agent, openaiKey, onMessage, onCallStart, onCallEnd } = opts;
230
- const callId = `test_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
231
- const caller = "+15550000001";
232
- const callee = "+15550000002";
233
- const conversationHistory = [];
234
- const log = getLogger();
235
- log.info("");
236
- log.info("=".repeat(60));
237
- log.info(" PATTER TEST MODE");
238
- log.info("=".repeat(60));
239
- log.info(` Agent: ${agent.model || "default"} / ${agent.voice || "default"}`);
240
- log.info(` Provider: ${agent.provider || "openai_realtime"}`);
241
- log.info(` Call ID: ${callId}`);
242
- log.info(` Caller: ${caller} -> Callee: ${callee}`);
243
- log.info("-".repeat(60));
244
- log.info(" Commands: /quit /transfer <number> /hangup /history");
245
- log.info("=".repeat(60));
246
- log.info("");
247
- if (onCallStart) {
248
- await onCallStart({
249
- call_id: callId,
250
- caller,
251
- callee,
252
- direction: "test"
253
- });
254
- }
255
- if (agent.firstMessage) {
256
- log.info(` Agent: ${agent.firstMessage}`);
257
- log.info("");
258
- conversationHistory.push({
259
- role: "assistant",
260
- text: agent.firstMessage,
261
- timestamp: Date.now()
262
- });
263
- }
264
- let llmLoop = null;
265
- if (!onMessage && openaiKey) {
266
- let llmModel = agent.model || "gpt-4o-mini";
267
- if (llmModel.includes("realtime")) llmModel = "gpt-4o-mini";
268
- let resolvedPrompt = agent.systemPrompt;
269
- if (agent.variables) {
270
- for (const [k, v] of Object.entries(agent.variables)) {
271
- resolvedPrompt = resolvedPrompt.replaceAll(`{${k}}`, v);
272
- }
273
- }
274
- llmLoop = new LLMLoop(
275
- openaiKey,
276
- llmModel,
277
- resolvedPrompt,
278
- agent.tools
279
- );
280
- }
281
- let ended = false;
282
- const _callControl = {
283
- callId,
284
- caller,
285
- callee,
286
- transfer: async (number) => {
287
- ended = true;
288
- log.info(` [Transfer -> ${number}]`);
289
- },
290
- hangup: async () => {
291
- ended = true;
292
- log.info(" [Call ended by agent]");
293
- }
294
- };
295
- void _callControl;
296
- const rl = createInterface({
297
- input: process.stdin,
298
- output: process.stdout
299
- });
300
- const askQuestion = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
301
- try {
302
- while (!ended) {
303
- let userInput;
304
- try {
305
- userInput = await askQuestion(" You: ");
306
- } catch {
307
- log.info("\n [Session ended]");
308
- break;
309
- }
310
- userInput = userInput.trim();
311
- if (!userInput) continue;
312
- if (userInput === "/quit") {
313
- log.info(" [Session ended]");
314
- break;
315
- } else if (userInput === "/hangup") {
316
- log.info(" [You hung up]");
317
- break;
318
- } else if (userInput.startsWith("/transfer ")) {
319
- const number = userInput.slice(10).trim();
320
- log.info(` [Transfer -> ${number}]`);
321
- break;
322
- } else if (userInput === "/history") {
323
- for (const entry of conversationHistory) {
324
- const role = entry.role.charAt(0).toUpperCase() + entry.role.slice(1);
325
- log.info(` ${role}: ${entry.text}`);
326
- }
327
- continue;
328
- }
329
- conversationHistory.push({
330
- role: "user",
331
- text: userInput,
332
- timestamp: Date.now()
333
- });
334
- if (onMessage) {
335
- try {
336
- const responseText = await onMessage({
337
- text: userInput,
338
- call_id: callId,
339
- caller,
340
- history: [...conversationHistory]
341
- });
342
- if (responseText) {
343
- log.info(` Agent: ${responseText}`);
344
- conversationHistory.push({
345
- role: "assistant",
346
- text: responseText,
347
- timestamp: Date.now()
348
- });
349
- log.info("");
350
- }
351
- } catch (e) {
352
- log.error(` [Error: ${String(e)}]`);
353
- }
354
- } else if (llmLoop) {
355
- const callCtx = { call_id: callId, caller, callee };
356
- const parts = [];
357
- process.stdout.write(" Agent: ");
358
- for await (const token of llmLoop.run(userInput, conversationHistory, callCtx)) {
359
- parts.push(token);
360
- process.stdout.write(token);
361
- }
362
- log.info("");
363
- const responseText = parts.join("");
364
- if (responseText) {
365
- conversationHistory.push({
366
- role: "assistant",
367
- text: responseText,
368
- timestamp: Date.now()
369
- });
370
- }
371
- log.info("");
372
- } else {
373
- log.info(" [No onMessage handler or LLM loop configured]");
374
- }
375
- if (ended) break;
376
- }
377
- } finally {
378
- rl.close();
379
- }
380
- if (onCallEnd) {
381
- await onCallEnd({
382
- call_id: callId,
383
- caller,
384
- callee,
385
- direction: "test",
386
- transcript: conversationHistory
387
- });
388
- }
389
- }
390
- };
391
-
392
- export {
393
- OpenAILLMProvider,
394
- LLMLoop,
395
- TestSession
396
- };
@@ -1,45 +0,0 @@
1
- import {
2
- getLogger
3
- } from "./chunk-FMNRCP5X.mjs";
4
-
5
- // src/tunnel.ts
6
- var log = getLogger();
7
- async function startTunnel(port, timeoutMs = 3e4) {
8
- let tunnelMod;
9
- try {
10
- tunnelMod = await import("cloudflared");
11
- } catch {
12
- throw new Error(
13
- 'Built-in tunnel requires the "cloudflared" package. Install it with:\n\n npm install cloudflared\n\nOr provide your own webhookUrl instead of using tunnel: true.'
14
- );
15
- }
16
- log.info("Starting tunnel to localhost:%d ...", port);
17
- const result = tunnelMod.tunnel({
18
- "--url": `http://localhost:${port}`
19
- });
20
- const tunnelUrl = await Promise.race([
21
- result.url,
22
- new Promise(
23
- (_, reject) => setTimeout(() => reject(new Error(
24
- `Tunnel failed to start within ${timeoutMs / 1e3}s. Check your internet connection or provide webhookUrl manually.`
25
- )), timeoutMs)
26
- )
27
- ]);
28
- const hostname = tunnelUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
29
- log.info("Tunnel ready: https://%s", hostname);
30
- result.connections.then(() => {
31
- log.info("Tunnel connections established");
32
- }).catch(() => {
33
- });
34
- return {
35
- hostname,
36
- stop: () => {
37
- log.info("Stopping tunnel...");
38
- result.stop();
39
- }
40
- };
41
- }
42
-
43
- export {
44
- startTunnel
45
- };