getpatter 0.4.0 → 0.4.2

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 (37) 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-KB57IV4K.mjs +0 -410
  35. package/dist/chunk-TAATEHKF.mjs +0 -396
  36. package/dist/chunk-VNU4GNW3.mjs +0 -45
  37. package/dist/test-mode-RTQAK5CP.mjs +0 -6
@@ -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.0",
4
- "description": "Connect AI agents to phone numbers with 10 lines of code",
3
+ "version": "0.4.2",
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,410 +0,0 @@
1
- // src/test-mode.ts
2
- import { createInterface } from "readline";
3
-
4
- // src/logger.ts
5
- var defaultLogger = {
6
- info: (msg, ...args) => console.log(`[PATTER] ${msg}`, ...args),
7
- warn: (msg, ...args) => console.warn(`[PATTER] WARNING: ${msg}`, ...args),
8
- error: (msg, ...args) => console.error(`[PATTER] ERROR: ${msg}`, ...args),
9
- debug: () => {
10
- }
11
- };
12
- var currentLogger = defaultLogger;
13
- function getLogger() {
14
- return currentLogger;
15
- }
16
- function setLogger(logger) {
17
- currentLogger = logger;
18
- }
19
-
20
- // src/llm-loop.ts
21
- var OpenAILLMProvider = class {
22
- apiKey;
23
- model;
24
- constructor(apiKey, model) {
25
- this.apiKey = apiKey;
26
- this.model = model;
27
- }
28
- async *stream(messages, tools) {
29
- const body = {
30
- model: this.model,
31
- messages,
32
- stream: true
33
- };
34
- if (tools) {
35
- body.tools = tools;
36
- }
37
- const response = await fetch("https://api.openai.com/v1/chat/completions", {
38
- method: "POST",
39
- headers: {
40
- "Content-Type": "application/json",
41
- "Authorization": `Bearer ${this.apiKey}`
42
- },
43
- body: JSON.stringify(body)
44
- });
45
- if (!response.ok) {
46
- const errText = await response.text();
47
- getLogger().error(`LLM API error: ${response.status} ${errText}`);
48
- return;
49
- }
50
- const reader = response.body?.getReader();
51
- if (!reader) return;
52
- const decoder = new TextDecoder();
53
- let buffer = "";
54
- while (true) {
55
- const { done, value } = await reader.read();
56
- if (done) break;
57
- buffer += decoder.decode(value, { stream: true });
58
- const lines = buffer.split("\n");
59
- buffer = lines.pop() || "";
60
- for (const line of lines) {
61
- const trimmed = line.trim();
62
- if (!trimmed || !trimmed.startsWith("data: ")) continue;
63
- const data = trimmed.slice(6);
64
- if (data === "[DONE]") continue;
65
- let chunk;
66
- try {
67
- chunk = JSON.parse(data);
68
- } catch {
69
- continue;
70
- }
71
- const delta = chunk.choices?.[0]?.delta;
72
- if (!delta) continue;
73
- if (delta.content) {
74
- yield { type: "text", content: delta.content };
75
- }
76
- if (delta.tool_calls) {
77
- for (const tc of delta.tool_calls) {
78
- yield {
79
- type: "tool_call",
80
- index: tc.index,
81
- id: tc.id,
82
- name: tc.function?.name,
83
- arguments: tc.function?.arguments
84
- };
85
- }
86
- }
87
- }
88
- }
89
- }
90
- };
91
- var LLMLoop = class {
92
- provider;
93
- systemPrompt;
94
- tools;
95
- openaiTools;
96
- toolMap;
97
- constructor(apiKey, model, systemPrompt, tools, llmProvider) {
98
- this.provider = llmProvider ?? new OpenAILLMProvider(apiKey, model);
99
- this.systemPrompt = systemPrompt;
100
- this.tools = tools ?? null;
101
- this.toolMap = /* @__PURE__ */ new Map();
102
- this.openaiTools = null;
103
- if (this.tools && this.tools.length > 0) {
104
- this.openaiTools = [];
105
- for (const t of this.tools) {
106
- this.openaiTools.push({
107
- type: "function",
108
- function: {
109
- name: t.name,
110
- description: t.description || "",
111
- parameters: t.parameters || { type: "object", properties: {} }
112
- }
113
- });
114
- this.toolMap.set(t.name, t);
115
- }
116
- }
117
- }
118
- /**
119
- * Stream LLM response tokens, handling tool calls automatically.
120
- * Yields text tokens as they arrive from the LLM.
121
- */
122
- async *run(userText, history, callContext) {
123
- const messages = this.buildMessages(history, userText);
124
- const maxIterations = 10;
125
- for (let iter = 0; iter < maxIterations; iter++) {
126
- const toolCallsAccumulated = /* @__PURE__ */ new Map();
127
- const textParts = [];
128
- let hasToolCalls = false;
129
- for await (const chunk of this.provider.stream(messages, this.openaiTools)) {
130
- if (chunk.type === "text" && chunk.content) {
131
- textParts.push(chunk.content);
132
- yield chunk.content;
133
- } else if (chunk.type === "tool_call") {
134
- hasToolCalls = true;
135
- const idx = chunk.index ?? 0;
136
- if (!toolCallsAccumulated.has(idx)) {
137
- toolCallsAccumulated.set(idx, { id: "", name: "", arguments: "" });
138
- }
139
- const acc = toolCallsAccumulated.get(idx);
140
- if (chunk.id) acc.id = chunk.id;
141
- if (chunk.name) acc.name = chunk.name;
142
- if (chunk.arguments) acc.arguments += chunk.arguments;
143
- }
144
- }
145
- if (!hasToolCalls) return;
146
- const assistantMsg = {
147
- role: "assistant",
148
- content: textParts.join("") || null,
149
- tool_calls: []
150
- };
151
- const sortedIndices = [...toolCallsAccumulated.keys()].sort((a, b) => a - b);
152
- for (const idx of sortedIndices) {
153
- const tc = toolCallsAccumulated.get(idx);
154
- assistantMsg.tool_calls.push({
155
- id: tc.id,
156
- type: "function",
157
- function: { name: tc.name, arguments: tc.arguments }
158
- });
159
- }
160
- messages.push(assistantMsg);
161
- for (const tcData of assistantMsg.tool_calls) {
162
- const toolName = tcData.function.name;
163
- let args;
164
- try {
165
- args = JSON.parse(tcData.function.arguments);
166
- } catch {
167
- args = {};
168
- }
169
- const result = await this.executeTool(toolName, args, callContext);
170
- messages.push({
171
- role: "tool",
172
- tool_call_id: tcData.id,
173
- content: result
174
- });
175
- }
176
- }
177
- getLogger().warn(`LLM loop hit max iterations (${maxIterations})`);
178
- }
179
- async executeTool(toolName, args, callContext) {
180
- const toolDef = this.toolMap.get(toolName);
181
- if (!toolDef) {
182
- return JSON.stringify({ error: `Unknown tool: ${toolName}` });
183
- }
184
- if (toolDef.handler) {
185
- try {
186
- return await toolDef.handler(args, callContext);
187
- } catch (e) {
188
- return JSON.stringify({ error: `Tool handler error: ${String(e)}` });
189
- }
190
- }
191
- if (toolDef.webhookUrl) {
192
- for (let attempt = 0; attempt < 3; attempt++) {
193
- try {
194
- const resp = await fetch(toolDef.webhookUrl, {
195
- method: "POST",
196
- headers: { "Content-Type": "application/json" },
197
- body: JSON.stringify({
198
- tool: toolName,
199
- arguments: args,
200
- ...callContext,
201
- attempt: attempt + 1
202
- }),
203
- signal: AbortSignal.timeout(1e4)
204
- });
205
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
206
- const result = JSON.stringify(await resp.json());
207
- const MAX_RESPONSE_BYTES = 1 * 1024 * 1024;
208
- if (result.length > MAX_RESPONSE_BYTES) {
209
- return JSON.stringify({ error: `Webhook response too large: ${result.length} bytes (max ${MAX_RESPONSE_BYTES})`, fallback: true });
210
- }
211
- return result;
212
- } catch (e) {
213
- if (attempt < 2) {
214
- await new Promise((r) => setTimeout(r, 500));
215
- } else {
216
- return JSON.stringify({ error: `Tool failed after 3 attempts: ${String(e)}` });
217
- }
218
- }
219
- }
220
- }
221
- return JSON.stringify({ error: `No handler or webhookUrl for tool '${toolName}'` });
222
- }
223
- buildMessages(history, userText) {
224
- const messages = [
225
- { role: "system", content: this.systemPrompt }
226
- ];
227
- for (const entry of history) {
228
- messages.push({
229
- role: entry.role === "assistant" ? "assistant" : "user",
230
- content: entry.text
231
- });
232
- }
233
- messages.push({ role: "user", content: userText });
234
- return messages;
235
- }
236
- };
237
-
238
- // src/test-mode.ts
239
- var TestSession = class {
240
- async run(opts) {
241
- const { agent, openaiKey, onMessage, onCallStart, onCallEnd } = opts;
242
- const callId = `test_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
243
- const caller = "+15550000001";
244
- const callee = "+15550000002";
245
- const conversationHistory = [];
246
- const log = getLogger();
247
- log.info("");
248
- log.info("=".repeat(60));
249
- log.info(" PATTER TEST MODE");
250
- log.info("=".repeat(60));
251
- log.info(` Agent: ${agent.model || "default"} / ${agent.voice || "default"}`);
252
- log.info(` Provider: ${agent.provider || "openai_realtime"}`);
253
- log.info(` Call ID: ${callId}`);
254
- log.info(` Caller: ${caller} -> Callee: ${callee}`);
255
- log.info("-".repeat(60));
256
- log.info(" Commands: /quit /transfer <number> /hangup /history");
257
- log.info("=".repeat(60));
258
- log.info("");
259
- if (onCallStart) {
260
- await onCallStart({
261
- call_id: callId,
262
- caller,
263
- callee,
264
- direction: "test"
265
- });
266
- }
267
- if (agent.firstMessage) {
268
- log.info(` Agent: ${agent.firstMessage}`);
269
- log.info("");
270
- conversationHistory.push({
271
- role: "assistant",
272
- text: agent.firstMessage,
273
- timestamp: Date.now()
274
- });
275
- }
276
- let llmLoop = null;
277
- if (!onMessage && openaiKey) {
278
- let llmModel = agent.model || "gpt-4o-mini";
279
- if (llmModel.includes("realtime")) llmModel = "gpt-4o-mini";
280
- let resolvedPrompt = agent.systemPrompt;
281
- if (agent.variables) {
282
- for (const [k, v] of Object.entries(agent.variables)) {
283
- resolvedPrompt = resolvedPrompt.replaceAll(`{${k}}`, v);
284
- }
285
- }
286
- llmLoop = new LLMLoop(
287
- openaiKey,
288
- llmModel,
289
- resolvedPrompt,
290
- agent.tools
291
- );
292
- }
293
- let ended = false;
294
- const _callControl = {
295
- callId,
296
- caller,
297
- callee,
298
- transfer: async (number) => {
299
- ended = true;
300
- log.info(` [Transfer -> ${number}]`);
301
- },
302
- hangup: async () => {
303
- ended = true;
304
- log.info(" [Call ended by agent]");
305
- }
306
- };
307
- void _callControl;
308
- const rl = createInterface({
309
- input: process.stdin,
310
- output: process.stdout
311
- });
312
- const askQuestion = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
313
- try {
314
- while (!ended) {
315
- let userInput;
316
- try {
317
- userInput = await askQuestion(" You: ");
318
- } catch {
319
- log.info("\n [Session ended]");
320
- break;
321
- }
322
- userInput = userInput.trim();
323
- if (!userInput) continue;
324
- if (userInput === "/quit") {
325
- log.info(" [Session ended]");
326
- break;
327
- } else if (userInput === "/hangup") {
328
- log.info(" [You hung up]");
329
- break;
330
- } else if (userInput.startsWith("/transfer ")) {
331
- const number = userInput.slice(10).trim();
332
- log.info(` [Transfer -> ${number}]`);
333
- break;
334
- } else if (userInput === "/history") {
335
- for (const entry of conversationHistory) {
336
- const role = entry.role.charAt(0).toUpperCase() + entry.role.slice(1);
337
- log.info(` ${role}: ${entry.text}`);
338
- }
339
- continue;
340
- }
341
- conversationHistory.push({
342
- role: "user",
343
- text: userInput,
344
- timestamp: Date.now()
345
- });
346
- if (onMessage) {
347
- try {
348
- const responseText = await onMessage({
349
- text: userInput,
350
- call_id: callId,
351
- caller,
352
- history: [...conversationHistory]
353
- });
354
- if (responseText) {
355
- log.info(` Agent: ${responseText}`);
356
- conversationHistory.push({
357
- role: "assistant",
358
- text: responseText,
359
- timestamp: Date.now()
360
- });
361
- log.info("");
362
- }
363
- } catch (e) {
364
- log.error(` [Error: ${String(e)}]`);
365
- }
366
- } else if (llmLoop) {
367
- const callCtx = { call_id: callId, caller, callee };
368
- const parts = [];
369
- process.stdout.write(" Agent: ");
370
- for await (const token of llmLoop.run(userInput, conversationHistory, callCtx)) {
371
- parts.push(token);
372
- process.stdout.write(token);
373
- }
374
- log.info("");
375
- const responseText = parts.join("");
376
- if (responseText) {
377
- conversationHistory.push({
378
- role: "assistant",
379
- text: responseText,
380
- timestamp: Date.now()
381
- });
382
- }
383
- log.info("");
384
- } else {
385
- log.info(" [No onMessage handler or LLM loop configured]");
386
- }
387
- if (ended) break;
388
- }
389
- } finally {
390
- rl.close();
391
- }
392
- if (onCallEnd) {
393
- await onCallEnd({
394
- call_id: callId,
395
- caller,
396
- callee,
397
- direction: "test",
398
- transcript: conversationHistory
399
- });
400
- }
401
- }
402
- };
403
-
404
- export {
405
- getLogger,
406
- setLogger,
407
- OpenAILLMProvider,
408
- LLMLoop,
409
- TestSession
410
- };