getpatter 0.4.2 → 0.4.4

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/dist/index.mjs CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  resample16kTo8k,
40
40
  resample24kTo16k,
41
41
  resample8kTo16k
42
- } from "./chunk-35EVXMGB.mjs";
42
+ } from "./chunk-O3RQG3NL.mjs";
43
43
  import {
44
44
  getLogger,
45
45
  setLogger
@@ -191,13 +191,21 @@ var STTConfigImpl = class {
191
191
  provider;
192
192
  apiKey;
193
193
  language;
194
- constructor(provider, apiKey, language = "en") {
194
+ options;
195
+ constructor(provider, apiKey, language = "en", options) {
195
196
  this.provider = provider;
196
197
  this.apiKey = apiKey;
197
198
  this.language = language;
199
+ if (options) this.options = options;
198
200
  }
199
201
  toDict() {
200
- return { provider: this.provider, api_key: this.apiKey, language: this.language };
202
+ const out = {
203
+ provider: this.provider,
204
+ api_key: this.apiKey,
205
+ language: this.language
206
+ };
207
+ if (this.options) out.options = { ...this.options };
208
+ return out;
201
209
  }
202
210
  };
203
211
  var TTSConfigImpl = class {
@@ -214,7 +222,15 @@ var TTSConfigImpl = class {
214
222
  }
215
223
  };
216
224
  function deepgram(opts) {
217
- return new STTConfigImpl("deepgram", opts.apiKey, opts.language ?? "en");
225
+ const options = {
226
+ model: opts.model ?? "nova-3",
227
+ endpointing_ms: opts.endpointingMs ?? 150,
228
+ utterance_end_ms: opts.utteranceEndMs === null ? null : opts.utteranceEndMs ?? 1e3,
229
+ smart_format: opts.smartFormat ?? true,
230
+ interim_results: opts.interimResults ?? true
231
+ };
232
+ if (opts.vadEvents !== void 0) options.vad_events = opts.vadEvents;
233
+ return new STTConfigImpl("deepgram", opts.apiKey, opts.language ?? "en", options);
218
234
  }
219
235
  function whisper(opts) {
220
236
  return new STTConfigImpl("whisper", opts.apiKey, opts.language ?? "en");
@@ -225,10 +241,41 @@ function elevenlabs(opts) {
225
241
  function openaiTts(opts) {
226
242
  return new TTSConfigImpl("openai", opts.apiKey, opts.voice ?? "alloy");
227
243
  }
244
+ function cartesia(opts) {
245
+ return new TTSConfigImpl(
246
+ "cartesia",
247
+ opts.apiKey,
248
+ opts.voice ?? "f786b574-daa5-4673-aa0c-cbe3e8534c02"
249
+ );
250
+ }
251
+ function rime(opts) {
252
+ return new TTSConfigImpl("rime", opts.apiKey, opts.voice ?? "astra");
253
+ }
254
+ function lmnt(opts) {
255
+ return new TTSConfigImpl("lmnt", opts.apiKey, opts.voice ?? "leah");
256
+ }
228
257
 
229
258
  // src/client.ts
230
259
  var DEFAULT_BACKEND_URL2 = "wss://api.getpatter.com";
231
260
  var DEFAULT_REST_URL = "https://api.getpatter.com";
261
+ function sttConfigToDict(cfg) {
262
+ const out = {
263
+ provider: cfg.provider,
264
+ api_key: cfg.apiKey,
265
+ language: cfg.language
266
+ };
267
+ if (cfg.options) out.options = { ...cfg.options };
268
+ return out;
269
+ }
270
+ function ttsConfigToDict(cfg) {
271
+ const out = {
272
+ provider: cfg.provider,
273
+ api_key: cfg.apiKey,
274
+ voice: cfg.voice
275
+ };
276
+ if (cfg.options) out.options = { ...cfg.options };
277
+ return out;
278
+ }
232
279
  var Patter = class {
233
280
  apiKey;
234
281
  backendUrl;
@@ -239,7 +286,8 @@ var Patter = class {
239
286
  embeddedServer = null;
240
287
  tunnelHandle = null;
241
288
  constructor(options) {
242
- if ("mode" in options && options.mode === "local") {
289
+ const isLocal = "mode" in options && options.mode === "local" || !("apiKey" in options) && ("twilioSid" in options && options.twilioSid || "telnyxKey" in options && options.telnyxKey);
290
+ if (isLocal) {
243
291
  const local = options;
244
292
  if (!local.phoneNumber) {
245
293
  throw new Error("Local mode requires phoneNumber");
@@ -353,7 +401,7 @@ var Patter = class {
353
401
  if (this.mode !== "local") {
354
402
  throw new Error("test() is only available in local mode");
355
403
  }
356
- const { TestSession: TestSession2 } = await import("./test-mode-RH65MMSP.mjs");
404
+ const { TestSession: TestSession2 } = await import("./test-mode-ASSLSQU2.mjs");
357
405
  const session = new TestSession2();
358
406
  await session.run({
359
407
  agent: opts.agent,
@@ -399,23 +447,42 @@ var Patter = class {
399
447
  const telnyxKey = this.localConfig.telnyxKey ?? "";
400
448
  const connectionId = this.localConfig.telnyxConnectionId ?? "";
401
449
  const streamUrl = `wss://${webhookUrl}/ws/stream/${encodeURIComponent(localOpts.to)}?caller=${encodeURIComponent(phoneNumber)}&callee=${encodeURIComponent(localOpts.to)}`;
450
+ const telnyxPayload = {
451
+ connection_id: connectionId,
452
+ from: phoneNumber,
453
+ to: localOpts.to,
454
+ stream_url: streamUrl,
455
+ stream_track: "both_tracks"
456
+ };
457
+ if (localOpts.ringTimeout !== void 0) {
458
+ telnyxPayload.timeout_secs = Math.max(1, Math.floor(localOpts.ringTimeout));
459
+ }
402
460
  const response2 = await fetch("https://api.telnyx.com/v2/calls", {
403
461
  method: "POST",
404
462
  headers: {
405
463
  "Content-Type": "application/json",
406
464
  Authorization: `Bearer ${telnyxKey}`
407
465
  },
408
- body: JSON.stringify({
409
- connection_id: connectionId,
410
- from: phoneNumber,
411
- to: localOpts.to,
412
- stream_url: streamUrl,
413
- stream_track: "both_tracks"
414
- })
466
+ body: JSON.stringify(telnyxPayload)
415
467
  });
416
468
  if (!response2.ok) {
417
469
  throw new ProvisionError(`Failed to initiate Telnyx call: ${await response2.text()}`);
418
470
  }
471
+ if (this.embeddedServer) {
472
+ try {
473
+ const body = await response2.clone().json();
474
+ const callId = body.data?.call_control_id;
475
+ if (callId) {
476
+ this.embeddedServer.metricsStore.recordCallInitiated({
477
+ call_id: callId,
478
+ caller: phoneNumber,
479
+ callee: localOpts.to,
480
+ direction: "outbound"
481
+ });
482
+ }
483
+ } catch {
484
+ }
485
+ }
419
486
  return;
420
487
  }
421
488
  const twilioSid = this.localConfig.twilioSid ?? "";
@@ -427,13 +494,19 @@ var Patter = class {
427
494
  From: phoneNumber,
428
495
  Url: `https://${webhookUrl}/webhooks/twilio/voice`,
429
496
  StatusCallback: statusCallbackUrl,
430
- StatusCallbackMethod: "POST"
497
+ StatusCallbackMethod: "POST",
498
+ // Full lifecycle so the dashboard sees ringing/no-answer/busy/failed
499
+ // transitions even when media never arrives.
500
+ StatusCallbackEvent: "initiated ringing answered completed"
431
501
  });
432
502
  if (localOpts.machineDetection) {
433
503
  params.append("MachineDetection", "DetectMessageEnd");
434
504
  params.append("AsyncAmd", "true");
435
505
  params.append("AsyncAmdStatusCallback", `https://${webhookUrl}/webhooks/twilio/amd`);
436
506
  }
507
+ if (localOpts.ringTimeout !== void 0) {
508
+ params.append("Timeout", String(Math.max(1, Math.floor(localOpts.ringTimeout))));
509
+ }
437
510
  if (localOpts.voicemailMessage && this.embeddedServer) {
438
511
  this.embeddedServer.voicemailMessage = localOpts.voicemailMessage;
439
512
  }
@@ -448,6 +521,21 @@ var Patter = class {
448
521
  if (!response.ok) {
449
522
  throw new ProvisionError(`Failed to initiate call: ${await response.text()}`);
450
523
  }
524
+ if (this.embeddedServer) {
525
+ try {
526
+ const body = await response.clone().json();
527
+ const callSid = body.sid;
528
+ if (callSid) {
529
+ this.embeddedServer.metricsStore.recordCallInitiated({
530
+ call_id: callSid,
531
+ caller: phoneNumber,
532
+ callee: localOpts.to,
533
+ direction: "outbound"
534
+ });
535
+ }
536
+ } catch {
537
+ }
538
+ }
451
539
  return;
452
540
  }
453
541
  const cloudOpts = options;
@@ -530,11 +618,15 @@ var Patter = class {
530
618
  const data = await response.json();
531
619
  return data.map((c) => ({ id: c.id, direction: c.direction, caller: c.caller, callee: c.callee, startedAt: c.started_at, endedAt: c.ended_at, durationSeconds: c.duration_seconds, status: c.status, transcript: c.transcript }));
532
620
  }
533
- // Provider helpers
621
+ // Provider helpers — mirror the Python classmethod factories so callers can
622
+ // write ``Patter.deepgram({ apiKey })`` without importing the top-level.
534
623
  static deepgram = deepgram;
535
624
  static whisper = whisper;
536
625
  static elevenlabs = elevenlabs;
537
626
  static openaiTts = openaiTts;
627
+ static cartesia = cartesia;
628
+ static rime = rime;
629
+ static lmnt = lmnt;
538
630
  static guardrail(opts) {
539
631
  return {
540
632
  name: opts.name,
@@ -600,8 +692,8 @@ var Patter = class {
600
692
  provider,
601
693
  provider_credentials: credentials,
602
694
  country,
603
- stt_config: stt?.toDict() ?? null,
604
- tts_config: tts?.toDict() ?? null
695
+ stt_config: stt ? stt.toDict?.() ?? sttConfigToDict(stt) : null,
696
+ tts_config: tts ? tts.toDict?.() ?? ttsConfigToDict(tts) : null
605
697
  })
606
698
  });
607
699
  if (response.status === 409) return;
@@ -719,6 +811,37 @@ var FallbackLLMProvider = class {
719
811
  }
720
812
  }
721
813
  }
814
+ /**
815
+ * Async-friendly disposer. Parity with Python's ``FallbackLLMProvider.aclose()``
816
+ * — safe to call multiple times, returns a resolved Promise once all probe
817
+ * timers are cleared. Prefer this in async contexts so awaiting the
818
+ * shutdown integrates naturally with the owning lifecycle.
819
+ */
820
+ async aclose() {
821
+ this.destroy();
822
+ }
823
+ /**
824
+ * Explicit-resource-management hook so callers can write
825
+ * ``await using fallback = new FallbackLLMProvider([...])`` and have
826
+ * background probe timers cleared automatically when the block exits.
827
+ * Mirrors Python's ``async with FallbackLLMProvider(...)``.
828
+ */
829
+ async [Symbol.asyncDispose]() {
830
+ await this.aclose();
831
+ }
832
+ /**
833
+ * Stream only the text deltas, flattening the chunk envelope. Parity with
834
+ * Python's ``FallbackLLMProvider.complete_stream``. Tool-call and done
835
+ * markers are filtered out so callers can concatenate the yielded strings
836
+ * directly.
837
+ */
838
+ async *completeStream(messages, tools) {
839
+ for await (const chunk of this.stream(messages, tools)) {
840
+ if (chunk.type === "text") {
841
+ yield chunk.content ?? "";
842
+ }
843
+ }
844
+ }
722
845
  // -----------------------------------------------------------------------
723
846
  // LLMProvider implementation
724
847
  // -----------------------------------------------------------------------
@@ -1235,13 +1358,37 @@ function wrapCallback(cb) {
1235
1358
  }
1236
1359
  };
1237
1360
  }
1238
- async function scheduleCron(cron, callback) {
1239
- const cm = await loadCron();
1240
- if (!cm.validate(cron)) {
1241
- throw new Error(`Invalid cron expression: ${cron}`);
1242
- }
1243
- const task = cm.schedule(cron, wrapCallback(callback));
1244
- return makeHandle(`cron-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, task);
1361
+ function scheduleCron(cron, callback) {
1362
+ let cancelled = false;
1363
+ let task = null;
1364
+ const jobId = `cron-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1365
+ loadCron().then((cm) => {
1366
+ if (cancelled) return;
1367
+ if (!cm.validate(cron)) {
1368
+ throw new Error(`Invalid cron expression: ${cron}`);
1369
+ }
1370
+ task = cm.schedule(cron, wrapCallback(callback));
1371
+ }).catch((err) => getLogger().error(`scheduleCron failed: ${String(err)}`));
1372
+ return {
1373
+ jobId,
1374
+ cancel() {
1375
+ if (cancelled) return;
1376
+ cancelled = true;
1377
+ if (task) {
1378
+ try {
1379
+ task.stop();
1380
+ } catch {
1381
+ }
1382
+ try {
1383
+ task.destroy?.();
1384
+ } catch {
1385
+ }
1386
+ }
1387
+ },
1388
+ get pending() {
1389
+ return !cancelled;
1390
+ }
1391
+ };
1245
1392
  }
1246
1393
  function scheduleOnce(at, callback) {
1247
1394
  const delayMs = at.getTime() - Date.now();
@@ -1263,8 +1410,18 @@ function scheduleOnce(at, callback) {
1263
1410
  }
1264
1411
  };
1265
1412
  }
1266
- function scheduleInterval(intervalMs, callback) {
1267
- if (intervalMs <= 0) throw new Error("intervalMs must be positive");
1413
+ function scheduleInterval(intervalOrOpts, callback) {
1414
+ let intervalMs;
1415
+ if (typeof intervalOrOpts === "number") {
1416
+ intervalMs = intervalOrOpts;
1417
+ } else if (intervalOrOpts.intervalMs !== void 0) {
1418
+ intervalMs = intervalOrOpts.intervalMs;
1419
+ } else if (intervalOrOpts.seconds !== void 0) {
1420
+ intervalMs = intervalOrOpts.seconds * 1e3;
1421
+ } else {
1422
+ throw new Error("scheduleInterval requires seconds or intervalMs");
1423
+ }
1424
+ if (intervalMs <= 0) throw new Error("interval must be positive");
1268
1425
  let cancelled = false;
1269
1426
  const wrapped = wrapCallback(callback);
1270
1427
  const timer = setInterval(() => {
@@ -1281,27 +1438,6 @@ function scheduleInterval(intervalMs, callback) {
1281
1438
  }
1282
1439
  };
1283
1440
  }
1284
- function makeHandle(jobId, task) {
1285
- let cancelled = false;
1286
- return {
1287
- jobId,
1288
- cancel() {
1289
- if (cancelled) return;
1290
- cancelled = true;
1291
- try {
1292
- task.stop();
1293
- } catch {
1294
- }
1295
- try {
1296
- task.destroy?.();
1297
- } catch {
1298
- }
1299
- },
1300
- get pending() {
1301
- return !cancelled;
1302
- }
1303
- };
1304
- }
1305
1441
 
1306
1442
  // src/providers/soniox-stt.ts
1307
1443
  import WebSocket3 from "ws";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  TestSession
3
- } from "./chunk-35EVXMGB.mjs";
3
+ } from "./chunk-O3RQG3NL.mjs";
4
4
  import "./chunk-FMNRCP5X.mjs";
5
5
  import "./chunk-OOIUSZB4.mjs";
6
6
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "getpatter",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
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": {