getpatter 0.6.6 → 0.6.8
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/{aec-PJJMUM5E.mjs → aec-ZZ5HGKS3.mjs} +10 -2
- package/dist/{carrier-config-7YGNRBPO.mjs → carrier-config-6L5NND7B.mjs} +19 -5
- package/dist/chunk-3JNVSNLV.mjs +428 -0
- package/dist/{chunk-3VVATR6A.mjs → chunk-C2LWB42T.mjs} +9 -6
- package/dist/{chunk-BO227NTF.mjs → chunk-I56S5MDJ.mjs} +121 -43
- package/dist/chunk-OV252D2V.mjs +198 -0
- package/dist/{chunk-YJX2EKON.mjs → chunk-YJ4HKJL6.mjs} +7404 -4593
- package/dist/cli.js +32813 -710
- package/dist/dashboard/ui.html +9 -9
- package/dist/index.d.mts +2829 -47
- package/dist/index.d.ts +2829 -47
- package/dist/index.js +5972 -948
- package/dist/index.mjs +2299 -845
- package/dist/{openai-realtime-2-L5EKAAUH.mjs → openai-realtime-2-O4DP3LXN.mjs} +1 -1
- package/dist/session-N3CBCYYN.mjs +12 -0
- package/dist/silero-vad-SSGHVHLA.mjs +9 -0
- package/dist/{test-mode-XFOADUNE.mjs → test-mode-5CNXC447.mjs} +3 -2
- package/package.json +1 -1
- package/src/dashboard/ui.html +9 -9
- package/dist/silero-vad-RGF5HCIR.mjs +0 -7
|
@@ -11,7 +11,7 @@ var DEFAULT_WARMUP_SECONDS = 0.5;
|
|
|
11
11
|
var DEFAULT_LEAKAGE = 0.9999;
|
|
12
12
|
var DEFAULT_DOUBLE_TALK_RHO = 0.6;
|
|
13
13
|
var FAR_END_BUFFER_SECONDS = 0.5;
|
|
14
|
-
var NlmsEchoCanceller = class {
|
|
14
|
+
var NlmsEchoCanceller = class _NlmsEchoCanceller {
|
|
15
15
|
taps;
|
|
16
16
|
step;
|
|
17
17
|
warmupStep;
|
|
@@ -88,8 +88,12 @@ var NlmsEchoCanceller = class {
|
|
|
88
88
|
* rate — same shape as what we hand off to the audio sender before the
|
|
89
89
|
* carrier-specific transcode.
|
|
90
90
|
*/
|
|
91
|
+
/** Far-end staleness window (ms): beyond this, processNearEnd passes through. */
|
|
92
|
+
static FAR_STALE_MS = 250;
|
|
93
|
+
lastFarPushAt = null;
|
|
91
94
|
pushFarEnd(pcm) {
|
|
92
95
|
if (pcm.length === 0) return;
|
|
96
|
+
this.lastFarPushAt = Date.now();
|
|
93
97
|
const samples = int16BufferToFloat32(pcm);
|
|
94
98
|
const n = samples.length;
|
|
95
99
|
const bufLen = this.farBuf.length;
|
|
@@ -120,6 +124,9 @@ var NlmsEchoCanceller = class {
|
|
|
120
124
|
processNearEnd(pcm) {
|
|
121
125
|
if (pcm.length === 0) return pcm;
|
|
122
126
|
if (this.farFilled < this.taps) return pcm;
|
|
127
|
+
if (this.lastFarPushAt === null || Date.now() - this.lastFarPushAt > _NlmsEchoCanceller.FAR_STALE_MS) {
|
|
128
|
+
return pcm;
|
|
129
|
+
}
|
|
123
130
|
const near = int16BufferToFloat32(pcm);
|
|
124
131
|
const cleaned = this.blockNlms(near);
|
|
125
132
|
this.framesProcessed += 1;
|
|
@@ -177,7 +184,8 @@ var NlmsEchoCanceller = class {
|
|
|
177
184
|
const a = Math.abs(near[i]);
|
|
178
185
|
if (a > nearMax) nearMax = a;
|
|
179
186
|
}
|
|
180
|
-
|
|
187
|
+
if (farMax <= 1e-3) return near;
|
|
188
|
+
const doubleTalk = nearMax > this.rho * farMax;
|
|
181
189
|
if (doubleTalk) this.doubleTalkFrames += 1;
|
|
182
190
|
const out = new Float32Array(near.length);
|
|
183
191
|
const w = this.w;
|
|
@@ -47,15 +47,29 @@ async function configureTwilioNumber(accountSid, authToken, phoneNumber, voiceUr
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
async function configureTelnyxNumber(apiKey, connectionId, phoneNumber) {
|
|
50
|
+
const headers = {
|
|
51
|
+
Authorization: `Bearer ${apiKey}`,
|
|
52
|
+
"Content-Type": "application/json"
|
|
53
|
+
};
|
|
54
|
+
const assoc = await fetch(
|
|
55
|
+
`${TELNYX_API_BASE}/phone_numbers/${encodeURIComponent(phoneNumber)}`,
|
|
56
|
+
{
|
|
57
|
+
method: "PATCH",
|
|
58
|
+
headers,
|
|
59
|
+
body: JSON.stringify({ connection_id: connectionId })
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
if (!assoc.ok) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Telnyx PATCH /phone_numbers/${redactPhone(phoneNumber)} failed: ${assoc.status} ${await assoc.text()}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
50
67
|
const resp = await fetch(
|
|
51
68
|
`${TELNYX_API_BASE}/phone_numbers/${encodeURIComponent(phoneNumber)}/voice`,
|
|
52
69
|
{
|
|
53
70
|
method: "PATCH",
|
|
54
|
-
headers
|
|
55
|
-
|
|
56
|
-
"Content-Type": "application/json"
|
|
57
|
-
},
|
|
58
|
-
body: JSON.stringify({ connection_id: connectionId, tech_prefix_enabled: false })
|
|
71
|
+
headers,
|
|
72
|
+
body: JSON.stringify({ tech_prefix_enabled: false })
|
|
59
73
|
}
|
|
60
74
|
);
|
|
61
75
|
if (!resp.ok) {
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ErrorCode,
|
|
3
|
+
MetricsStore,
|
|
4
|
+
PatterConfigError,
|
|
5
|
+
PatterError,
|
|
6
|
+
RemoteMessageHandler,
|
|
7
|
+
StreamHandler,
|
|
8
|
+
resolveVariables,
|
|
9
|
+
sanitizeVariables
|
|
10
|
+
} from "./chunk-YJ4HKJL6.mjs";
|
|
11
|
+
import {
|
|
12
|
+
getLogger
|
|
13
|
+
} from "./chunk-MVOQFAEO.mjs";
|
|
14
|
+
import {
|
|
15
|
+
init_esm_shims
|
|
16
|
+
} from "./chunk-N565J3CF.mjs";
|
|
17
|
+
|
|
18
|
+
// src/evals/session.ts
|
|
19
|
+
init_esm_shims();
|
|
20
|
+
import { randomUUID } from "crypto";
|
|
21
|
+
|
|
22
|
+
// src/evals/fakes.ts
|
|
23
|
+
init_esm_shims();
|
|
24
|
+
function makeFakeCarrierWs() {
|
|
25
|
+
const noop = () => void 0;
|
|
26
|
+
return {
|
|
27
|
+
send: noop,
|
|
28
|
+
close: noop,
|
|
29
|
+
on: noop,
|
|
30
|
+
once: noop,
|
|
31
|
+
removeListener: noop,
|
|
32
|
+
addEventListener: noop,
|
|
33
|
+
removeEventListener: noop,
|
|
34
|
+
readyState: 1,
|
|
35
|
+
OPEN: 1
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
var FakeAudioSender = class {
|
|
39
|
+
label = "Eval";
|
|
40
|
+
// The metrics/dashboard tag for the faked carrier — mirrors Python's
|
|
41
|
+
// ``telephony_provider="eval"``. No pricing row exists for 'eval', so
|
|
42
|
+
// telephony cost is always 0. The cast is deliberate: CarrierKind
|
|
43
|
+
// enumerates real carriers and the harness is not one.
|
|
44
|
+
telephonyProvider = "eval";
|
|
45
|
+
inputWireFormat = "pcm_16000";
|
|
46
|
+
sentAudio = [];
|
|
47
|
+
marks = [];
|
|
48
|
+
endedCalls = [];
|
|
49
|
+
transfers = [];
|
|
50
|
+
clears = 0;
|
|
51
|
+
handler = null;
|
|
52
|
+
sttFactory = null;
|
|
53
|
+
/** Wire the auto-ack loopback for ``sendMark`` → ``onMark``. */
|
|
54
|
+
attachHandler(handler) {
|
|
55
|
+
this.handler = handler;
|
|
56
|
+
}
|
|
57
|
+
/** Wire the STT instance ``createStt`` should hand to the handler. */
|
|
58
|
+
attachSttFactory(factory) {
|
|
59
|
+
this.sttFactory = factory;
|
|
60
|
+
}
|
|
61
|
+
sendAudio(_ws, audioBase64, _streamSid) {
|
|
62
|
+
this.sentAudio.push(Buffer.from(audioBase64, "base64"));
|
|
63
|
+
}
|
|
64
|
+
sendMark(_ws, markName, _streamSid) {
|
|
65
|
+
this.marks.push(markName);
|
|
66
|
+
if (this.handler !== null) {
|
|
67
|
+
void this.handler.onMark(markName).catch((err) => {
|
|
68
|
+
getLogger().debug(`FakeAudioSender mark ack failed: ${String(err)}`);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
sendClear(_ws, _streamSid) {
|
|
73
|
+
this.clears += 1;
|
|
74
|
+
}
|
|
75
|
+
async transferCall(callId, toNumber, _options) {
|
|
76
|
+
this.transfers.push({ callId, toNumber });
|
|
77
|
+
}
|
|
78
|
+
async endCall(callId, _ws) {
|
|
79
|
+
this.endedCalls.push(callId);
|
|
80
|
+
}
|
|
81
|
+
async createStt(_agent) {
|
|
82
|
+
return this.sttFactory ? this.sttFactory() : null;
|
|
83
|
+
}
|
|
84
|
+
async queryTelephonyCost() {
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var FakeSTT = class {
|
|
88
|
+
sentAudio = [];
|
|
89
|
+
connected = false;
|
|
90
|
+
closed = false;
|
|
91
|
+
callback = null;
|
|
92
|
+
async connect() {
|
|
93
|
+
this.connected = true;
|
|
94
|
+
}
|
|
95
|
+
sendAudio(pcm) {
|
|
96
|
+
this.sentAudio.push(pcm);
|
|
97
|
+
}
|
|
98
|
+
onTranscript(cb) {
|
|
99
|
+
this.callback = cb;
|
|
100
|
+
}
|
|
101
|
+
/** Inject one final transcript through the handler's real receive path. */
|
|
102
|
+
async pushFinal(text) {
|
|
103
|
+
if (this.closed) {
|
|
104
|
+
throw new PatterError(
|
|
105
|
+
"FakeSTT is closed \u2014 the EvalSession was already cleaned up",
|
|
106
|
+
{ code: ErrorCode.CONFIG }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (this.callback === null) {
|
|
110
|
+
throw new PatterError(
|
|
111
|
+
"FakeSTT has no transcript consumer \u2014 the handler is not started",
|
|
112
|
+
{ code: ErrorCode.CONFIG }
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
await this.callback({ text, isFinal: true, confidence: 1 });
|
|
116
|
+
}
|
|
117
|
+
async close() {
|
|
118
|
+
this.closed = true;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var FakeTTS = class {
|
|
122
|
+
spoken = [];
|
|
123
|
+
closed = false;
|
|
124
|
+
chunk;
|
|
125
|
+
constructor(chunk = Buffer.alloc(320)) {
|
|
126
|
+
this.chunk = chunk;
|
|
127
|
+
}
|
|
128
|
+
async *synthesizeStream(text) {
|
|
129
|
+
this.spoken.push(text);
|
|
130
|
+
yield this.chunk;
|
|
131
|
+
}
|
|
132
|
+
/** Invoked by the handler's real teardown (``handleStop``). */
|
|
133
|
+
cancelActiveStream() {
|
|
134
|
+
this.closed = true;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
var NoopVad = class {
|
|
138
|
+
async processFrame(_pcmChunk, _sampleRate) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
reset() {
|
|
142
|
+
}
|
|
143
|
+
async close() {
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// src/evals/session.ts
|
|
148
|
+
function historyTranscript(history) {
|
|
149
|
+
return history.map((entry) => ({
|
|
150
|
+
role: entry.role === "assistant" ? "agent" : String(entry.role ?? "user"),
|
|
151
|
+
text: String(entry.text ?? "")
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
154
|
+
async function awaitWithTimeout(promise, timeoutMs) {
|
|
155
|
+
let timer;
|
|
156
|
+
try {
|
|
157
|
+
return await Promise.race([
|
|
158
|
+
promise.then(() => "ok"),
|
|
159
|
+
new Promise((resolve) => {
|
|
160
|
+
timer = setTimeout(() => resolve("timeout"), timeoutMs);
|
|
161
|
+
})
|
|
162
|
+
]);
|
|
163
|
+
} finally {
|
|
164
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
var EvalSession = class _EvalSession {
|
|
168
|
+
callId;
|
|
169
|
+
caller;
|
|
170
|
+
callee;
|
|
171
|
+
/** The faked carrier boundary — records audio / clears / marks. */
|
|
172
|
+
audioSender = new FakeAudioSender();
|
|
173
|
+
/** The faked STT boundary — inject transcripts via the session, not directly. */
|
|
174
|
+
stt = new FakeSTT();
|
|
175
|
+
/** The faked TTS boundary — ``spoken`` records every synthesised sentence. */
|
|
176
|
+
tts = new FakeTTS();
|
|
177
|
+
/**
|
|
178
|
+
* Every payload the handler fired through ``onTranscript`` — user /
|
|
179
|
+
* assistant / tool events, in emission order.
|
|
180
|
+
*/
|
|
181
|
+
transcriptEvents = [];
|
|
182
|
+
sourceAgent;
|
|
183
|
+
llmProvider;
|
|
184
|
+
openaiKey;
|
|
185
|
+
customParams;
|
|
186
|
+
turnTimeoutS;
|
|
187
|
+
streamHandler = null;
|
|
188
|
+
toolCalls = [];
|
|
189
|
+
metricsTurns = [];
|
|
190
|
+
interruptedTurns = 0;
|
|
191
|
+
offTurnEnded = null;
|
|
192
|
+
started = false;
|
|
193
|
+
closed = false;
|
|
194
|
+
constructor(options) {
|
|
195
|
+
if (!options || !options.agent) {
|
|
196
|
+
throw new PatterConfigError("EvalSession requires an agent");
|
|
197
|
+
}
|
|
198
|
+
this.sourceAgent = options.agent;
|
|
199
|
+
this.llmProvider = options.llmProvider;
|
|
200
|
+
this.openaiKey = options.openaiKey ?? "";
|
|
201
|
+
this.callId = options.callId ?? `eval_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
|
|
202
|
+
this.caller = options.caller ?? "+15550000001";
|
|
203
|
+
this.callee = options.callee ?? "+15550000002";
|
|
204
|
+
this.customParams = options.customParams;
|
|
205
|
+
this.turnTimeoutS = options.turnTimeoutS ?? 60;
|
|
206
|
+
}
|
|
207
|
+
/** Build AND start a session — the canonical entry point. */
|
|
208
|
+
static async create(options) {
|
|
209
|
+
const session = new _EvalSession(options);
|
|
210
|
+
await session.start();
|
|
211
|
+
return session;
|
|
212
|
+
}
|
|
213
|
+
// -- lifecycle ------------------------------------------------------------
|
|
214
|
+
/** Build and start the real handler (idempotent). */
|
|
215
|
+
async start() {
|
|
216
|
+
if (this.started) return;
|
|
217
|
+
const evalAgent = this.buildEvalAgent();
|
|
218
|
+
if (evalAgent.llm == null && !this.openaiKey) {
|
|
219
|
+
throw new PatterConfigError(
|
|
220
|
+
"EvalSession needs an LLM: pass llmProvider (e.g. a ScriptedLLMProvider for CI), set agent.llm, or supply openaiKey for the built-in OpenAI provider."
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
this.audioSender.attachSttFactory(() => this.stt);
|
|
224
|
+
const deps = {
|
|
225
|
+
config: this.openaiKey ? { openaiKey: this.openaiKey } : {},
|
|
226
|
+
agent: evalAgent,
|
|
227
|
+
bridge: this.audioSender,
|
|
228
|
+
metricsStore: new MetricsStore(),
|
|
229
|
+
pricing: null,
|
|
230
|
+
remoteHandler: new RemoteMessageHandler(),
|
|
231
|
+
recording: false,
|
|
232
|
+
onTranscript: async (data) => {
|
|
233
|
+
this.transcriptEvents.push(data);
|
|
234
|
+
},
|
|
235
|
+
onMetrics: async (data) => {
|
|
236
|
+
const turn = data.turn;
|
|
237
|
+
if (turn != null) this.metricsTurns.push(turn);
|
|
238
|
+
},
|
|
239
|
+
// Pipeline mode never builds a Realtime/ConvAI adapter; loud guard in
|
|
240
|
+
// case a future refactor routes the eval agent elsewhere.
|
|
241
|
+
buildAIAdapter: () => {
|
|
242
|
+
throw new PatterConfigError("EvalSession supports pipeline mode only");
|
|
243
|
+
},
|
|
244
|
+
sanitizeVariables,
|
|
245
|
+
resolveVariables
|
|
246
|
+
};
|
|
247
|
+
const handler = new StreamHandler(deps, makeFakeCarrierWs(), this.caller, this.callee);
|
|
248
|
+
const internals = handler;
|
|
249
|
+
this.audioSender.attachHandler(handler);
|
|
250
|
+
handler.setStreamSid("eval");
|
|
251
|
+
this.offTurnEnded = handler.addObserver((payload) => {
|
|
252
|
+
if (payload?.turn?.agent_text === "[interrupted]") this.interruptedTurns += 1;
|
|
253
|
+
}, "turn_ended");
|
|
254
|
+
this.streamHandler = handler;
|
|
255
|
+
try {
|
|
256
|
+
await handler.handleCallStart(this.callId, { ...this.customParams ?? {} });
|
|
257
|
+
if (internals.firstMessageTask) await internals.firstMessageTask;
|
|
258
|
+
if (internals.llmLoop == null) {
|
|
259
|
+
throw new PatterConfigError(
|
|
260
|
+
"EvalSession could not build the LLM loop \u2014 the agent has onMessage-style wiring or no usable LLM provider."
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
internals.llmLoop.setOnToolCall(async (name, args, result) => {
|
|
264
|
+
await internals.recordToolCall(name, args, result);
|
|
265
|
+
this.toolCalls.push({ name, arguments: { ...args ?? {} }, result });
|
|
266
|
+
});
|
|
267
|
+
} catch (err) {
|
|
268
|
+
try {
|
|
269
|
+
await handler.handleStop();
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
this.offTurnEnded?.();
|
|
273
|
+
this.offTurnEnded = null;
|
|
274
|
+
this.streamHandler = null;
|
|
275
|
+
throw err;
|
|
276
|
+
}
|
|
277
|
+
this.started = true;
|
|
278
|
+
}
|
|
279
|
+
/** Run the handler's real teardown (``handleStop``). Idempotent. */
|
|
280
|
+
async close() {
|
|
281
|
+
if (this.closed) return;
|
|
282
|
+
this.closed = true;
|
|
283
|
+
this.offTurnEnded?.();
|
|
284
|
+
this.offTurnEnded = null;
|
|
285
|
+
const handler = this.streamHandler;
|
|
286
|
+
if (handler === null) return;
|
|
287
|
+
await handler.handleStop();
|
|
288
|
+
}
|
|
289
|
+
// -- turn driving -----------------------------------------------------------
|
|
290
|
+
/**
|
|
291
|
+
* Inject one final user transcript and await the full agent turn.
|
|
292
|
+
*
|
|
293
|
+
* The transcript flows through the handler's real receive path — barge-in
|
|
294
|
+
* handling, dedup/hallucination filtering, hooks, the LLM loop with real
|
|
295
|
+
* tool execution, guardrails, sentence chunking, and TTS — exactly as on
|
|
296
|
+
* a live call. Returns once the turn's dispatch task settles.
|
|
297
|
+
*
|
|
298
|
+
* Throws:
|
|
299
|
+
* - ``PatterError`` (``INPUT_VALIDATION``) — the REAL pipeline dropped
|
|
300
|
+
* the transcript (duplicate within the 2 s throttle window, known STT
|
|
301
|
+
* hallucination, or empty text) — same behaviour as a live call.
|
|
302
|
+
* - ``PatterError`` (``TIMEOUT``) — the turn did not settle within
|
|
303
|
+
* ``timeoutS`` (default: the session's ``turnTimeoutS``).
|
|
304
|
+
*/
|
|
305
|
+
async userSays(text, options = {}) {
|
|
306
|
+
if (!this.started || this.streamHandler === null) {
|
|
307
|
+
throw new PatterError(
|
|
308
|
+
"EvalSession is not started \u2014 use `await EvalSession.create(...)`",
|
|
309
|
+
{ code: ErrorCode.CONFIG }
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (this.closed) {
|
|
313
|
+
throw new PatterError("EvalSession is closed", { code: ErrorCode.CONFIG });
|
|
314
|
+
}
|
|
315
|
+
const timeoutS = options.timeoutS ?? this.turnTimeoutS;
|
|
316
|
+
const timeoutMs = timeoutS * 1e3;
|
|
317
|
+
const internals = this.streamHandler;
|
|
318
|
+
const historyLen = internals.history.entries.length;
|
|
319
|
+
const toolsLen = this.toolCalls.length;
|
|
320
|
+
const spokenLen = this.tts.spoken.length;
|
|
321
|
+
const turnsLen = this.metricsTurns.length;
|
|
322
|
+
const interruptedLen = this.interruptedTurns;
|
|
323
|
+
const commitTextBefore = internals.lastCommitText;
|
|
324
|
+
const commitAtBefore = internals.lastCommitAt;
|
|
325
|
+
const consumed = await awaitWithTimeout(this.stt.pushFinal(text), timeoutMs);
|
|
326
|
+
if (consumed === "timeout") {
|
|
327
|
+
throw new PatterError(
|
|
328
|
+
`user turn ${JSON.stringify(text)} was not consumed by the STT transcript loop within ${timeoutS}s \u2014 a hook or the previous turn may be hung (check logs)`,
|
|
329
|
+
{ code: ErrorCode.TIMEOUT }
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const committed = internals.lastCommitText !== commitTextBefore || internals.lastCommitAt !== commitAtBefore;
|
|
333
|
+
if (!committed) {
|
|
334
|
+
throw new PatterError(
|
|
335
|
+
`user turn ${JSON.stringify(text)} was dropped by the pipeline's commit filter (duplicate within 2s, known STT hallucination, or empty) \u2014 exactly as it would be on a live call. Vary the text between turns or split the case.`,
|
|
336
|
+
{ code: ErrorCode.INPUT_VALIDATION }
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const dispatch = internals.dispatchTask;
|
|
340
|
+
if (dispatch !== null) {
|
|
341
|
+
const settled = await awaitWithTimeout(dispatch, timeoutMs);
|
|
342
|
+
if (settled === "timeout") {
|
|
343
|
+
try {
|
|
344
|
+
internals.llmAbort?.abort();
|
|
345
|
+
} catch {
|
|
346
|
+
}
|
|
347
|
+
await Promise.race([
|
|
348
|
+
dispatch.catch(() => void 0),
|
|
349
|
+
new Promise((resolve) => setTimeout(resolve, 1e3))
|
|
350
|
+
]);
|
|
351
|
+
throw new PatterError(
|
|
352
|
+
`agent turn for ${JSON.stringify(text)} did not settle within ${timeoutS}s`,
|
|
353
|
+
{ code: ErrorCode.TIMEOUT }
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return this.buildTurnResult(text, { historyLen, toolsLen, spokenLen, turnsLen, interruptedLen });
|
|
358
|
+
}
|
|
359
|
+
// -- convenience accessors ----------------------------------------------------
|
|
360
|
+
/** The live {@link StreamHandler} (``null`` before ``start``). */
|
|
361
|
+
get handler() {
|
|
362
|
+
return this.streamHandler;
|
|
363
|
+
}
|
|
364
|
+
/** Current conversation history (shallow copies of the entries). */
|
|
365
|
+
get history() {
|
|
366
|
+
if (this.streamHandler === null) return [];
|
|
367
|
+
const internals = this.streamHandler;
|
|
368
|
+
return internals.history.entries.map((e) => ({ ...e }));
|
|
369
|
+
}
|
|
370
|
+
/** Full-session transcript in the judge shape (assistant → agent). */
|
|
371
|
+
transcript() {
|
|
372
|
+
return historyTranscript(this.history);
|
|
373
|
+
}
|
|
374
|
+
// -- internals --------------------------------------------------------------
|
|
375
|
+
/** Return the agent reshaped for harness execution. */
|
|
376
|
+
buildEvalAgent() {
|
|
377
|
+
const agent = this.sourceAgent;
|
|
378
|
+
const overrides = {};
|
|
379
|
+
if (agent.provider !== "pipeline") {
|
|
380
|
+
overrides.provider = "pipeline";
|
|
381
|
+
}
|
|
382
|
+
if (agent.engine !== void 0) {
|
|
383
|
+
getLogger().info("EvalSession: agent.engine ignored \u2014 the harness runs pipeline mode");
|
|
384
|
+
overrides.engine = void 0;
|
|
385
|
+
}
|
|
386
|
+
if (agent.stt !== void 0) {
|
|
387
|
+
getLogger().info(
|
|
388
|
+
"EvalSession: agent.stt config ignored \u2014 transcripts are injected directly"
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
overrides.stt = void 0;
|
|
392
|
+
if (agent.tts !== void 0) {
|
|
393
|
+
getLogger().info("EvalSession: agent.tts config replaced by FakeTTS");
|
|
394
|
+
}
|
|
395
|
+
overrides.tts = this.tts;
|
|
396
|
+
if (this.llmProvider !== void 0) {
|
|
397
|
+
overrides.llm = this.llmProvider;
|
|
398
|
+
}
|
|
399
|
+
if (agent.vad === void 0) {
|
|
400
|
+
overrides.vad = new NoopVad();
|
|
401
|
+
}
|
|
402
|
+
return { ...agent, ...overrides };
|
|
403
|
+
}
|
|
404
|
+
buildTurnResult(userText, snapshot) {
|
|
405
|
+
const internals = this.streamHandler;
|
|
406
|
+
const spoken = this.tts.spoken.slice(snapshot.spokenLen);
|
|
407
|
+
const fullHistory = internals.history.entries.map((e) => ({ ...e }));
|
|
408
|
+
const newHistory = fullHistory.slice(snapshot.historyLen);
|
|
409
|
+
const agentText = spoken.length > 0 ? spoken.join(" ") : newHistory.filter((e) => e.role === "assistant").map((e) => e.text).join(" ");
|
|
410
|
+
const newTurns = this.metricsTurns.slice(snapshot.turnsLen);
|
|
411
|
+
return {
|
|
412
|
+
userText,
|
|
413
|
+
agentText,
|
|
414
|
+
toolCalls: this.toolCalls.slice(snapshot.toolsLen),
|
|
415
|
+
historySnapshot: fullHistory,
|
|
416
|
+
interrupted: this.interruptedTurns > snapshot.interruptedLen,
|
|
417
|
+
metricsTurn: newTurns.length > 0 ? newTurns[newTurns.length - 1] : null
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
export {
|
|
423
|
+
FakeAudioSender,
|
|
424
|
+
FakeSTT,
|
|
425
|
+
FakeTTS,
|
|
426
|
+
historyTranscript,
|
|
427
|
+
EvalSession
|
|
428
|
+
};
|
|
@@ -57,7 +57,7 @@ function classifyOnnxError(err) {
|
|
|
57
57
|
if (/listSupportedBackends|backend_\d/.test(msg)) return "api-drift";
|
|
58
58
|
return "unknown";
|
|
59
59
|
}
|
|
60
|
-
async function loadOnnxRuntime() {
|
|
60
|
+
async function loadOnnxRuntime(feature = "SileroVAD") {
|
|
61
61
|
let firstErr;
|
|
62
62
|
try {
|
|
63
63
|
const mod = await import("./dist-RYMPCILF.mjs");
|
|
@@ -76,16 +76,18 @@ async function loadOnnxRuntime() {
|
|
|
76
76
|
let header;
|
|
77
77
|
let remedy;
|
|
78
78
|
if (importClass === "missing" && requireClass === "missing") {
|
|
79
|
-
header =
|
|
80
|
-
remedy =
|
|
79
|
+
header = `${feature} requires the "onnxruntime-node" package \u2014 it is not installed.`;
|
|
80
|
+
remedy = ` Install: npm install onnxruntime-node@~1.18.0
|
|
81
|
+
|
|
82
|
+
(~210 MB. Only needed when you actually use ${feature} in pipeline mode.)`;
|
|
81
83
|
} else if (importClass === "api-drift" || requireClass === "api-drift") {
|
|
82
|
-
header =
|
|
84
|
+
header = `${feature} found onnxruntime-node but the installed version uses an API the SDK does not support.`;
|
|
83
85
|
remedy = " Patter is currently tested against onnxruntime-node 1.18.x.\n\n Fix: npm install onnxruntime-node@~1.18.0\n\n Versions 1.24+ removed `listSupportedBackends` from the public surface \u2014 track\n https://github.com/PatterAI/Patter/issues for the SDK update that targets 1.24.";
|
|
84
86
|
} else if (importClass === "binding" || requireClass === "binding") {
|
|
85
|
-
header =
|
|
87
|
+
header = `${feature} found onnxruntime-node but the native binding for this platform is missing.`;
|
|
86
88
|
remedy = " Common cause on macOS x86_64: the prebuilt bin/ layout drifted between releases.\n\n Fix: npm install onnxruntime-node@~1.18.0\n\n Or rebuild from source: npm rebuild onnxruntime-node";
|
|
87
89
|
} else {
|
|
88
|
-
header =
|
|
90
|
+
header = `${feature} requires the "onnxruntime-node" package, which could not be resolved.`;
|
|
89
91
|
remedy = " Install: npm install onnxruntime-node@~1.18.0\n\n This is an optional peer dependency of getpatter (~210 MB).";
|
|
90
92
|
}
|
|
91
93
|
const err = new Error(
|
|
@@ -408,5 +410,6 @@ var SileroVAD = class _SileroVAD {
|
|
|
408
410
|
};
|
|
409
411
|
|
|
410
412
|
export {
|
|
413
|
+
loadOnnxRuntime,
|
|
411
414
|
SileroVAD
|
|
412
415
|
};
|