getpatter 0.5.0 → 0.5.1
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 +25 -1
- package/dist/{chunk-JO5C35FM.mjs → chunk-AKQFOFLG.mjs} +1 -1
- package/dist/{chunk-757NVN4L.mjs → chunk-B6C3KIBG.mjs} +48 -23
- package/dist/index.d.mts +363 -56
- package/dist/index.d.ts +363 -56
- package/dist/index.js +690 -850
- package/dist/index.mjs +634 -4
- package/dist/{test-mode-YFOL2HYH.mjs → test-mode-JZMYE5HY.mjs} +1 -1
- package/dist/{tunnel-BL7A7GXW.mjs → tunnel-O7ICMSTP.mjs} +1 -1
- package/package.json +1 -1
- package/dist/lib-4WCAS54J.mjs +0 -830
package/README.md
CHANGED
|
@@ -57,7 +57,8 @@ await phone.serve({ agent, tunnel: true });
|
|
|
57
57
|
| Tool calling | `agent({ tools: [tool(...)] })` | Agent calls external APIs mid-conversation |
|
|
58
58
|
| Custom STT + TTS | `agent({ stt: new DeepgramSTT(), tts: new ElevenLabsTTS() })` | Bring your own voice providers |
|
|
59
59
|
| Dynamic variables | `agent({ variables: {...} })` | Personalize prompts per caller |
|
|
60
|
-
|
|
|
60
|
+
| Pluggable LLM | `agent({ llm: new AnthropicLLM() })` | 5 built-in providers: OpenAI, Anthropic, Groq, Cerebras, Google |
|
|
61
|
+
| Custom LLM (any model) | `serve({ onMessage })` | Route to anything — local inference, internal gateways, etc. |
|
|
61
62
|
| Call recording | `serve({ recording: true })` | Record all calls |
|
|
62
63
|
| Call transfer | `transfer_call` (auto-injected) | Transfer to a human |
|
|
63
64
|
| Voicemail drop | `call({ voicemailMessage: "..." })` | Play message on voicemail |
|
|
@@ -80,6 +81,10 @@ Every provider reads its credentials from the environment by default. Pass `apiK
|
|
|
80
81
|
| `LMNT_API_KEY` | `LMNTTTS` |
|
|
81
82
|
| `SONIOX_API_KEY` | `SonioxSTT` |
|
|
82
83
|
| `ASSEMBLYAI_API_KEY` | `AssemblyAISTT` |
|
|
84
|
+
| `ANTHROPIC_API_KEY` | `AnthropicLLM` |
|
|
85
|
+
| `GROQ_API_KEY` | `GroqLLM` |
|
|
86
|
+
| `CEREBRAS_API_KEY` | `CerebrasLLM` |
|
|
87
|
+
| `GEMINI_API_KEY` (or `GOOGLE_API_KEY`) | `GoogleLLM` |
|
|
83
88
|
|
|
84
89
|
```bash
|
|
85
90
|
cp .env.example .env
|
|
@@ -180,6 +185,8 @@ import {
|
|
|
180
185
|
DeepgramSTT, WhisperSTT, CartesiaSTT, SonioxSTT, AssemblyAISTT,
|
|
181
186
|
// TTS
|
|
182
187
|
ElevenLabsTTS, OpenAITTS, CartesiaTTS, RimeTTS, LMNTTTS,
|
|
188
|
+
// LLM
|
|
189
|
+
OpenAILLM, AnthropicLLM, GroqLLM, CerebrasLLM, GoogleLLM,
|
|
183
190
|
// Tunnels
|
|
184
191
|
CloudflareTunnel, StaticTunnel,
|
|
185
192
|
// Primitives
|
|
@@ -225,6 +232,23 @@ const agent = phone.agent({
|
|
|
225
232
|
await phone.serve({ agent, tunnel: true });
|
|
226
233
|
```
|
|
227
234
|
|
|
235
|
+
### Pipeline mode — pick STT, LLM, TTS independently
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { Patter, Twilio, DeepgramSTT, AnthropicLLM, ElevenLabsTTS } from "getpatter";
|
|
239
|
+
|
|
240
|
+
const phone = new Patter({ carrier: new Twilio(), phoneNumber: "+15550001234" });
|
|
241
|
+
const agent = phone.agent({
|
|
242
|
+
stt: new DeepgramSTT(), // reads DEEPGRAM_API_KEY
|
|
243
|
+
llm: new AnthropicLLM(), // reads ANTHROPIC_API_KEY
|
|
244
|
+
tts: new ElevenLabsTTS({ voiceId: "rachel" }), // reads ELEVENLABS_API_KEY
|
|
245
|
+
systemPrompt: "You are a helpful voice assistant.",
|
|
246
|
+
});
|
|
247
|
+
await phone.serve({ agent, tunnel: true });
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Available LLM providers: `OpenAILLM`, `AnthropicLLM`, `GroqLLM`, `CerebrasLLM`, `GoogleLLM`. Tool calling works across all five. For fully custom logic, drop `llm` and pass an `onMessage` callback to `serve()` instead.
|
|
251
|
+
|
|
228
252
|
### Tool calling
|
|
229
253
|
|
|
230
254
|
```typescript
|
|
@@ -7,7 +7,7 @@ var log = getLogger();
|
|
|
7
7
|
async function startTunnel(port, timeoutMs = 3e4) {
|
|
8
8
|
let tunnelMod;
|
|
9
9
|
try {
|
|
10
|
-
tunnelMod = await import("
|
|
10
|
+
tunnelMod = await import("cloudflared");
|
|
11
11
|
} catch {
|
|
12
12
|
throw new Error(
|
|
13
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.'
|
|
@@ -2351,7 +2351,7 @@ var StreamHandler = class {
|
|
|
2351
2351
|
ttsProvider: ttsProviderName,
|
|
2352
2352
|
pricing: deps.pricing
|
|
2353
2353
|
});
|
|
2354
|
-
getLogger().
|
|
2354
|
+
getLogger().debug(`WebSocket connection opened (${deps.bridge.label})`);
|
|
2355
2355
|
}
|
|
2356
2356
|
// ---------------------------------------------------------------------------
|
|
2357
2357
|
// Public: called by the provider-specific parsers in server.ts
|
|
@@ -2367,9 +2367,12 @@ var StreamHandler = class {
|
|
|
2367
2367
|
this.metricsAcc.callId = callId;
|
|
2368
2368
|
if (customParams.caller && !this.caller) this.caller = customParams.caller;
|
|
2369
2369
|
if (customParams.callee && !this.callee) this.callee = customParams.callee;
|
|
2370
|
-
|
|
2370
|
+
const mode = this.deps.agent.engine ? `engine=${this.deps.agent.engine.kind ?? "unknown"}` : "pipeline";
|
|
2371
|
+
getLogger().info(
|
|
2372
|
+
`Call started: ${callId} (${this.deps.bridge.label}, ${mode}, ${sanitizeLogValue(this.caller || "?")} \u2192 ${sanitizeLogValue(this.callee || "?")})`
|
|
2373
|
+
);
|
|
2371
2374
|
if (Object.keys(customParams).length > 0) {
|
|
2372
|
-
getLogger().
|
|
2375
|
+
getLogger().debug(`Custom params: ${sanitizeLogValue(JSON.stringify(customParams))}`);
|
|
2373
2376
|
}
|
|
2374
2377
|
this.deps.metricsStore.recordCallStart({
|
|
2375
2378
|
call_id: callId,
|
|
@@ -2417,7 +2420,7 @@ var StreamHandler = class {
|
|
|
2417
2420
|
}
|
|
2418
2421
|
});
|
|
2419
2422
|
if (recResp.ok) {
|
|
2420
|
-
getLogger().
|
|
2423
|
+
getLogger().debug(`Recording started for ${callId}`);
|
|
2421
2424
|
} else {
|
|
2422
2425
|
getLogger().warn(`could not start recording: ${await recResp.text()}`);
|
|
2423
2426
|
}
|
|
@@ -2472,7 +2475,7 @@ var StreamHandler = class {
|
|
|
2472
2475
|
}
|
|
2473
2476
|
/** Handle a DTMF keypress event (Twilio only). */
|
|
2474
2477
|
async handleDtmf(digit) {
|
|
2475
|
-
getLogger().
|
|
2478
|
+
getLogger().debug(`DTMF: ${digit}`);
|
|
2476
2479
|
if (this.adapter instanceof OpenAIRealtimeAdapter) {
|
|
2477
2480
|
await this.adapter.sendText(`The user pressed key ${digit} on their phone keypad.`);
|
|
2478
2481
|
}
|
|
@@ -2534,14 +2537,14 @@ var StreamHandler = class {
|
|
|
2534
2537
|
this.stt = await this.deps.bridge.createStt(this.deps.agent);
|
|
2535
2538
|
this.tts = await createTTS(this.deps.agent);
|
|
2536
2539
|
if (!this.stt) {
|
|
2537
|
-
getLogger().
|
|
2540
|
+
getLogger().debug(`Pipeline mode (${label}): no STT configured`);
|
|
2538
2541
|
}
|
|
2539
2542
|
if (!this.tts) {
|
|
2540
|
-
getLogger().
|
|
2543
|
+
getLogger().debug(`Pipeline mode (${label}): no TTS configured`);
|
|
2541
2544
|
}
|
|
2542
2545
|
try {
|
|
2543
2546
|
if (this.stt) await this.stt.connect();
|
|
2544
|
-
getLogger().
|
|
2547
|
+
getLogger().debug(`Pipeline mode (${label}): STT + TTS connected`);
|
|
2545
2548
|
} catch (e) {
|
|
2546
2549
|
getLogger().error(`Pipeline connect FAILED (${label}):`, e);
|
|
2547
2550
|
try {
|
|
@@ -2574,7 +2577,24 @@ var StreamHandler = class {
|
|
|
2574
2577
|
this.history.push({ role: "assistant", text: this.deps.agent.firstMessage, timestamp: Date.now() });
|
|
2575
2578
|
}
|
|
2576
2579
|
}
|
|
2577
|
-
if (
|
|
2580
|
+
if (this.deps.agent.llm) {
|
|
2581
|
+
if (this.deps.onMessage) {
|
|
2582
|
+
throw new Error(
|
|
2583
|
+
"Cannot pass both agent({ llm }) and serve({ onMessage }). Pick one \u2014 `llm` for built-in LLMs, `onMessage` for custom logic."
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2586
|
+
this.llmLoop = new LLMLoop(
|
|
2587
|
+
"",
|
|
2588
|
+
// apiKey unused when llmProvider is supplied
|
|
2589
|
+
"",
|
|
2590
|
+
// model unused when llmProvider is supplied
|
|
2591
|
+
resolvedPrompt,
|
|
2592
|
+
this.deps.agent.tools,
|
|
2593
|
+
this.deps.agent.llm
|
|
2594
|
+
);
|
|
2595
|
+
const llmLabel = this.deps.agent.llm.constructor?.name ?? "custom";
|
|
2596
|
+
getLogger().debug(`Built-in LLM loop active (pipeline, ${label}, llm=${llmLabel})`);
|
|
2597
|
+
} else if (!this.deps.onMessage && this.deps.config.openaiKey) {
|
|
2578
2598
|
let llmModel = this.deps.agent.model || "gpt-4o-mini";
|
|
2579
2599
|
if (llmModel.includes("realtime")) llmModel = "gpt-4o-mini";
|
|
2580
2600
|
this.llmLoop = new LLMLoop(
|
|
@@ -2583,7 +2603,7 @@ var StreamHandler = class {
|
|
|
2583
2603
|
resolvedPrompt,
|
|
2584
2604
|
this.deps.agent.tools
|
|
2585
2605
|
);
|
|
2586
|
-
getLogger().
|
|
2606
|
+
getLogger().debug(`Built-in LLM loop active (pipeline, ${label})`);
|
|
2587
2607
|
}
|
|
2588
2608
|
if (this.stt) {
|
|
2589
2609
|
this.stt.onTranscript(async (transcript) => {
|
|
@@ -2644,7 +2664,7 @@ var StreamHandler = class {
|
|
|
2644
2664
|
}
|
|
2645
2665
|
async processTranscript(transcript) {
|
|
2646
2666
|
if (transcript.text && this.isSpeaking) {
|
|
2647
|
-
getLogger().
|
|
2667
|
+
getLogger().debug(
|
|
2648
2668
|
`Barge-in: caller spoke over agent (${sanitizeLogValue(transcript.text.slice(0, 40))})`
|
|
2649
2669
|
);
|
|
2650
2670
|
this.isSpeaking = false;
|
|
@@ -2679,17 +2699,17 @@ var StreamHandler = class {
|
|
|
2679
2699
|
"cool"
|
|
2680
2700
|
]);
|
|
2681
2701
|
if (HALLUCINATIONS.has(stripped) || stripped === "") {
|
|
2682
|
-
getLogger().
|
|
2702
|
+
getLogger().debug(`Dropped likely STT hallucination: ${sanitizeLogValue(normalised.slice(0, 40))}`);
|
|
2683
2703
|
return;
|
|
2684
2704
|
}
|
|
2685
2705
|
if (sinceLastMs < 2e3 && normalised === this.lastCommitText) {
|
|
2686
|
-
getLogger().
|
|
2706
|
+
getLogger().debug(
|
|
2687
2707
|
`Dropped duplicate final transcript (${(sinceLastMs / 1e3).toFixed(1)}s since last): ${sanitizeLogValue(normalised.slice(0, 40))}`
|
|
2688
2708
|
);
|
|
2689
2709
|
return;
|
|
2690
2710
|
}
|
|
2691
2711
|
if (sinceLastMs < 500) {
|
|
2692
|
-
getLogger().
|
|
2712
|
+
getLogger().debug(
|
|
2693
2713
|
`Dropped back-to-back final transcript (${(sinceLastMs / 1e3).toFixed(2)}s since last): ${sanitizeLogValue(normalised.slice(0, 40))}`
|
|
2694
2714
|
);
|
|
2695
2715
|
return;
|
|
@@ -2697,7 +2717,7 @@ var StreamHandler = class {
|
|
|
2697
2717
|
this.lastCommitText = normalised;
|
|
2698
2718
|
this.lastCommitAt = now;
|
|
2699
2719
|
const label = this.deps.bridge.label;
|
|
2700
|
-
getLogger().
|
|
2720
|
+
getLogger().debug(`User (${label} pipeline): ${sanitizeLogValue(transcript.text)}`);
|
|
2701
2721
|
this.metricsAcc.startTurn();
|
|
2702
2722
|
this.metricsAcc.recordSttComplete(transcript.text);
|
|
2703
2723
|
if (this.deps.onTranscript) {
|
|
@@ -2712,7 +2732,7 @@ var StreamHandler = class {
|
|
|
2712
2732
|
const hookCtx = this.buildHookContext();
|
|
2713
2733
|
const filteredTranscript = await hookExecutor.runAfterTranscribe(transcript.text, hookCtx);
|
|
2714
2734
|
if (filteredTranscript === null) {
|
|
2715
|
-
getLogger().
|
|
2735
|
+
getLogger().debug(`afterTranscribe hook vetoed turn (${label})`);
|
|
2716
2736
|
this.metricsAcc.recordTurnInterrupted();
|
|
2717
2737
|
return;
|
|
2718
2738
|
}
|
|
@@ -2800,7 +2820,7 @@ var StreamHandler = class {
|
|
|
2800
2820
|
if (!this.llmLoop) {
|
|
2801
2821
|
const guard = checkGuardrails(responseText, this.deps.agent.guardrails);
|
|
2802
2822
|
if (guard) {
|
|
2803
|
-
getLogger().
|
|
2823
|
+
getLogger().debug(`Guardrail '${guard.name}' triggered (pipeline)`);
|
|
2804
2824
|
responseText = guard.replacement ?? "I'm sorry, I can't respond to that.";
|
|
2805
2825
|
}
|
|
2806
2826
|
this.metricsAcc.recordLlmComplete();
|
|
@@ -2878,7 +2898,7 @@ var StreamHandler = class {
|
|
|
2878
2898
|
this.adapter = this.deps.buildAIAdapter(resolvedPrompt);
|
|
2879
2899
|
try {
|
|
2880
2900
|
await this.adapter.connect();
|
|
2881
|
-
getLogger().
|
|
2901
|
+
getLogger().debug(`AI adapter connected (${label})`);
|
|
2882
2902
|
} catch (e) {
|
|
2883
2903
|
getLogger().error(`AI adapter connect FAILED (${label}):`, e);
|
|
2884
2904
|
try {
|
|
@@ -2920,7 +2940,7 @@ var StreamHandler = class {
|
|
|
2920
2940
|
this.deps.bridge.sendMark(this.ws, `audio_${this.chunkCount}`, this.streamSid);
|
|
2921
2941
|
} else if (type === "transcript_input") {
|
|
2922
2942
|
const inputText = eventData;
|
|
2923
|
-
getLogger().
|
|
2943
|
+
getLogger().debug(`User (${this.deps.bridge.label}): ${sanitizeLogValue(inputText)}`);
|
|
2924
2944
|
this.history.push({ role: "user", text: inputText, timestamp: Date.now() });
|
|
2925
2945
|
this.metricsAcc.startTurn();
|
|
2926
2946
|
this.currentAgentText = "";
|
|
@@ -2938,7 +2958,7 @@ var StreamHandler = class {
|
|
|
2938
2958
|
if (outputText) {
|
|
2939
2959
|
const triggered = checkGuardrails(outputText, this.deps.agent.guardrails);
|
|
2940
2960
|
if (triggered) {
|
|
2941
|
-
getLogger().
|
|
2961
|
+
getLogger().debug(`Guardrail '${triggered.name}' triggered`);
|
|
2942
2962
|
if (this.adapter instanceof OpenAIRealtimeAdapter) {
|
|
2943
2963
|
this.adapter.cancelResponse();
|
|
2944
2964
|
await this.adapter.sendText(triggered.replacement ?? "I'm sorry, I can't respond to that.");
|
|
@@ -2997,7 +3017,7 @@ var StreamHandler = class {
|
|
|
2997
3017
|
await adapter.sendFunctionResult(fc.call_id, JSON.stringify({ error: "Invalid phone number format", status: "rejected" }));
|
|
2998
3018
|
return;
|
|
2999
3019
|
}
|
|
3000
|
-
getLogger().
|
|
3020
|
+
getLogger().debug(`Transferring call to ${transferTo}`);
|
|
3001
3021
|
await adapter.sendFunctionResult(fc.call_id, JSON.stringify({ status: "transferring", to: transferTo }));
|
|
3002
3022
|
await this.deps.bridge.transferCall(this.callId, transferTo);
|
|
3003
3023
|
if (this.deps.onTranscript) {
|
|
@@ -3013,7 +3033,7 @@ var StreamHandler = class {
|
|
|
3013
3033
|
endArgs = {};
|
|
3014
3034
|
}
|
|
3015
3035
|
const reason = endArgs.reason ?? "conversation_complete";
|
|
3016
|
-
getLogger().
|
|
3036
|
+
getLogger().debug(`Ending call (${this.deps.bridge.label}): ${reason}`);
|
|
3017
3037
|
await adapter.sendFunctionResult(fc.call_id, JSON.stringify({ status: "ending", reason }));
|
|
3018
3038
|
await this.deps.bridge.endCall(this.callId, this.ws);
|
|
3019
3039
|
if (this.deps.onTranscript) {
|
|
@@ -3065,6 +3085,11 @@ var StreamHandler = class {
|
|
|
3065
3085
|
transcript: [...this.history.entries],
|
|
3066
3086
|
metrics: finalMetrics
|
|
3067
3087
|
};
|
|
3088
|
+
const cost = finalMetrics.cost?.total ?? 0;
|
|
3089
|
+
const latencyP95 = finalMetrics.latency_p95?.total_ms ?? 0;
|
|
3090
|
+
getLogger().info(
|
|
3091
|
+
`Call ended: ${this.callId} (${finalMetrics.duration_seconds.toFixed(1)}s, ${finalMetrics.turns.length} turns, cost=$${cost.toFixed(4)}, p95=${Math.round(latencyP95)}ms)`
|
|
3092
|
+
);
|
|
3068
3093
|
this.deps.metricsStore.recordCallEnd(
|
|
3069
3094
|
callEndData,
|
|
3070
3095
|
finalMetrics
|
|
@@ -3101,7 +3126,7 @@ async function queryDeepgramCost(metricsAcc, deepgramKey, deepgramRequestId) {
|
|
|
3101
3126
|
const usd = reqData.response?.details?.usd;
|
|
3102
3127
|
if (usd != null) {
|
|
3103
3128
|
metricsAcc.setActualSttCost(usd);
|
|
3104
|
-
getLogger().
|
|
3129
|
+
getLogger().debug(`Deepgram actual cost: $${usd}`);
|
|
3105
3130
|
}
|
|
3106
3131
|
}
|
|
3107
3132
|
}
|