@teamlearners/clawops 0.8.1 → 0.12.0
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 -0
- package/dist/agent/index.cjs +416 -80
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +40 -3
- package/dist/agent/index.d.ts +40 -3
- package/dist/agent/index.js +409 -73
- package/dist/agent/index.js.map +1 -1
- package/dist/{chunk-RUVY7MYW.js → chunk-KK3OXQT6.js} +3 -3
- package/dist/{chunk-RUVY7MYW.js.map → chunk-KK3OXQT6.js.map} +1 -1
- package/dist/{chunk-MOH4FRJZ.cjs → chunk-X6VG5NNG.cjs} +3 -3
- package/dist/{chunk-MOH4FRJZ.cjs.map → chunk-X6VG5NNG.cjs.map} +1 -1
- package/dist/index.cjs +38 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/agent/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-
|
|
1
|
+
import { DEFAULT_BASE_URL, AgentError, AgentConnectionError, VERSION } from '../chunk-KK3OXQT6.js';
|
|
2
2
|
import pino from 'pino';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
@@ -357,6 +357,7 @@ var ControlWebSocket = class {
|
|
|
357
357
|
_url;
|
|
358
358
|
_ws = null;
|
|
359
359
|
_handlers = /* @__PURE__ */ new Map();
|
|
360
|
+
_transferResolvers = /* @__PURE__ */ new Map();
|
|
360
361
|
_reconnectDelay = INITIAL_RECONNECT_DELAY;
|
|
361
362
|
_closed = false;
|
|
362
363
|
_connectedResolve = null;
|
|
@@ -384,6 +385,31 @@ var ControlWebSocket = class {
|
|
|
384
385
|
async waitConnected() {
|
|
385
386
|
return this._connectedPromise;
|
|
386
387
|
}
|
|
388
|
+
/** Request a call transfer and wait for the result. */
|
|
389
|
+
async requestTransfer(callId, params) {
|
|
390
|
+
return new Promise((resolve, reject) => {
|
|
391
|
+
const timeout = (params.timeout || 30) + 10;
|
|
392
|
+
const timer = setTimeout(() => {
|
|
393
|
+
this._transferResolvers.delete(callId);
|
|
394
|
+
reject(new Error("transfer timeout"));
|
|
395
|
+
}, timeout * 1e3);
|
|
396
|
+
this._transferResolvers.set(callId, {
|
|
397
|
+
resolve: (value) => {
|
|
398
|
+
clearTimeout(timer);
|
|
399
|
+
resolve(value);
|
|
400
|
+
},
|
|
401
|
+
reject: (reason) => {
|
|
402
|
+
clearTimeout(timer);
|
|
403
|
+
reject(reason);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
this.send({
|
|
407
|
+
event: "call.transfer",
|
|
408
|
+
callId,
|
|
409
|
+
transfer: params
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
}
|
|
387
413
|
/** Send a JSON message over the control WebSocket. */
|
|
388
414
|
send(message) {
|
|
389
415
|
if (this._ws && this._ws.readyState === 1) {
|
|
@@ -394,6 +420,10 @@ var ControlWebSocket = class {
|
|
|
394
420
|
close() {
|
|
395
421
|
this._closed = true;
|
|
396
422
|
this._clearPingTimer();
|
|
423
|
+
for (const [, resolver] of this._transferResolvers) {
|
|
424
|
+
resolver.reject(new Error("connection closed"));
|
|
425
|
+
}
|
|
426
|
+
this._transferResolvers.clear();
|
|
397
427
|
if (this._ws) {
|
|
398
428
|
this._ws.close();
|
|
399
429
|
this._ws = null;
|
|
@@ -439,6 +469,14 @@ var ControlWebSocket = class {
|
|
|
439
469
|
});
|
|
440
470
|
}
|
|
441
471
|
_dispatchEvent(event) {
|
|
472
|
+
if (["call.transfer.completed", "call.transfer.failed"].includes(event.event)) {
|
|
473
|
+
const callId = event.callId;
|
|
474
|
+
const resolver = this._transferResolvers.get(callId);
|
|
475
|
+
if (resolver) {
|
|
476
|
+
this._transferResolvers.delete(callId);
|
|
477
|
+
resolver.resolve(event.transfer || {});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
442
480
|
const handlers = this._handlers.get(event.event);
|
|
443
481
|
if (handlers) {
|
|
444
482
|
for (const handler of handlers) {
|
|
@@ -908,8 +946,8 @@ var AudioRecorder = class {
|
|
|
908
946
|
let gap = trackPos - this._mixWritten;
|
|
909
947
|
gap = gap - gap % 2;
|
|
910
948
|
if (gap > 0) {
|
|
911
|
-
const
|
|
912
|
-
fs.writeSync(this._fdMix,
|
|
949
|
+
const silence2 = Buffer.alloc(gap);
|
|
950
|
+
fs.writeSync(this._fdMix, silence2, 0, silence2.length, 44 + this._mixWritten);
|
|
913
951
|
this._mixWritten += gap;
|
|
914
952
|
}
|
|
915
953
|
}
|
|
@@ -1020,6 +1058,8 @@ var CallSession = class {
|
|
|
1020
1058
|
/** @internal */
|
|
1021
1059
|
_sendDtmfFn = null;
|
|
1022
1060
|
/** @internal */
|
|
1061
|
+
_transferFn = null;
|
|
1062
|
+
/** @internal */
|
|
1023
1063
|
_isTransportConnected = null;
|
|
1024
1064
|
_dtmfCollectorActive = false;
|
|
1025
1065
|
_dtmfResolvers = [];
|
|
@@ -1171,6 +1211,22 @@ var CallSession = class {
|
|
|
1171
1211
|
}
|
|
1172
1212
|
}
|
|
1173
1213
|
}
|
|
1214
|
+
/** Transfer the call to another destination. */
|
|
1215
|
+
async transfer(to, options) {
|
|
1216
|
+
if (!this._transferFn) {
|
|
1217
|
+
throw new Error("transfer not available");
|
|
1218
|
+
}
|
|
1219
|
+
return this._transferFn({
|
|
1220
|
+
to,
|
|
1221
|
+
mode: options?.mode ?? "blind",
|
|
1222
|
+
afterTransfer: options?.afterTransfer ?? "terminate",
|
|
1223
|
+
holdMedia: options?.holdMedia ?? "ringback",
|
|
1224
|
+
whisper: options?.whisper ?? null,
|
|
1225
|
+
context: options?.context ?? null,
|
|
1226
|
+
callerId: options?.callerId ?? null,
|
|
1227
|
+
timeout: options?.timeout ?? 30
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1174
1230
|
/** Register an event handler. */
|
|
1175
1231
|
on(event, handler) {
|
|
1176
1232
|
let list = this._handlers.get(event);
|
|
@@ -1215,6 +1271,7 @@ var BuiltinTool = /* @__PURE__ */ ((BuiltinTool2) => {
|
|
|
1215
1271
|
BuiltinTool2["HANG_UP"] = "hang_up";
|
|
1216
1272
|
BuiltinTool2["COLLECT_DTMF"] = "collect_dtmf";
|
|
1217
1273
|
BuiltinTool2["SEND_DTMF"] = "send_dtmf";
|
|
1274
|
+
BuiltinTool2["TRANSFER_CALL"] = "transfer_call";
|
|
1218
1275
|
BuiltinTool2["ALL"] = "all";
|
|
1219
1276
|
BuiltinTool2["NONE"] = "none";
|
|
1220
1277
|
return BuiltinTool2;
|
|
@@ -1222,7 +1279,8 @@ var BuiltinTool = /* @__PURE__ */ ((BuiltinTool2) => {
|
|
|
1222
1279
|
var INDIVIDUAL_TOOLS = /* @__PURE__ */ new Set([
|
|
1223
1280
|
"hang_up" /* HANG_UP */,
|
|
1224
1281
|
"collect_dtmf" /* COLLECT_DTMF */,
|
|
1225
|
-
"send_dtmf" /* SEND_DTMF
|
|
1282
|
+
"send_dtmf" /* SEND_DTMF */,
|
|
1283
|
+
"transfer_call" /* TRANSFER_CALL */
|
|
1226
1284
|
]);
|
|
1227
1285
|
function resolveBuiltinTools(value) {
|
|
1228
1286
|
if (typeof value === "string") {
|
|
@@ -1236,6 +1294,187 @@ function resolveBuiltinTools(value) {
|
|
|
1236
1294
|
}
|
|
1237
1295
|
return new Set(value.filter((t) => INDIVIDUAL_TOOLS.has(t)));
|
|
1238
1296
|
}
|
|
1297
|
+
var CHUNK_SIZE = 160;
|
|
1298
|
+
var SAMPLE_RATE2 = 8e3;
|
|
1299
|
+
var BELL_PARTIALS = [
|
|
1300
|
+
{ freqRatio: 1, amplitude: 1, decayRate: 1.2 },
|
|
1301
|
+
{ freqRatio: 2.76, amplitude: 0.5, decayRate: 2.5 },
|
|
1302
|
+
{ freqRatio: 5.4, amplitude: 0.25, decayRate: 4 }
|
|
1303
|
+
];
|
|
1304
|
+
function bellNote(freq, durationMs, volume) {
|
|
1305
|
+
const n = SAMPLE_RATE2 * durationMs / 1e3 | 0;
|
|
1306
|
+
const attackSamples = 3e-3 * SAMPLE_RATE2 | 0;
|
|
1307
|
+
const samples = new Int16Array(n);
|
|
1308
|
+
for (let i = 0; i < n; i++) {
|
|
1309
|
+
const t = i / SAMPLE_RATE2;
|
|
1310
|
+
let val = 0;
|
|
1311
|
+
for (const p of BELL_PARTIALS) {
|
|
1312
|
+
const f = freq * p.freqRatio;
|
|
1313
|
+
if (f >= SAMPLE_RATE2 / 2) continue;
|
|
1314
|
+
const env = p.amplitude * Math.exp(-p.decayRate * t * (1e3 / durationMs));
|
|
1315
|
+
val += env * Math.sin(2 * Math.PI * f * t);
|
|
1316
|
+
}
|
|
1317
|
+
if (i < attackSamples) {
|
|
1318
|
+
val *= i / attackSamples;
|
|
1319
|
+
}
|
|
1320
|
+
samples[i] = Math.max(-32768, Math.min(32767, volume * 32767 * val | 0));
|
|
1321
|
+
}
|
|
1322
|
+
return samples;
|
|
1323
|
+
}
|
|
1324
|
+
function silence(durationMs) {
|
|
1325
|
+
return new Int16Array(SAMPLE_RATE2 * durationMs / 1e3 | 0);
|
|
1326
|
+
}
|
|
1327
|
+
function int16ArrayToBuffer(samples) {
|
|
1328
|
+
const buf = Buffer.alloc(samples.length * 2);
|
|
1329
|
+
for (let i = 0; i < samples.length; i++) {
|
|
1330
|
+
buf.writeInt16LE(samples[i], i * 2);
|
|
1331
|
+
}
|
|
1332
|
+
return buf;
|
|
1333
|
+
}
|
|
1334
|
+
function concatInt16Arrays(...arrays) {
|
|
1335
|
+
let total = 0;
|
|
1336
|
+
for (const a of arrays) total += a.length;
|
|
1337
|
+
const result = new Int16Array(total);
|
|
1338
|
+
let offset = 0;
|
|
1339
|
+
for (const a of arrays) {
|
|
1340
|
+
result.set(a, offset);
|
|
1341
|
+
offset += a.length;
|
|
1342
|
+
}
|
|
1343
|
+
return result;
|
|
1344
|
+
}
|
|
1345
|
+
var PENTATONIC_C5 = {
|
|
1346
|
+
C5: 523.25,
|
|
1347
|
+
D5: 587.33,
|
|
1348
|
+
E5: 659.25,
|
|
1349
|
+
G5: 783.99,
|
|
1350
|
+
A5: 880,
|
|
1351
|
+
C6: 1046.5
|
|
1352
|
+
};
|
|
1353
|
+
function generateComfortTone(volume = 0.12) {
|
|
1354
|
+
const p = PENTATONIC_C5;
|
|
1355
|
+
const melody = [
|
|
1356
|
+
[p.E5, 450],
|
|
1357
|
+
[p.G5, 450],
|
|
1358
|
+
[p.A5, 450],
|
|
1359
|
+
[p.C6, 600],
|
|
1360
|
+
[0, 800],
|
|
1361
|
+
[p.A5, 400],
|
|
1362
|
+
[p.G5, 400],
|
|
1363
|
+
[p.E5, 400],
|
|
1364
|
+
[p.D5, 600],
|
|
1365
|
+
[0, 2500],
|
|
1366
|
+
[p.C5, 500],
|
|
1367
|
+
[p.E5, 500],
|
|
1368
|
+
[p.C6, 700],
|
|
1369
|
+
[0, 2500]
|
|
1370
|
+
];
|
|
1371
|
+
const parts = [];
|
|
1372
|
+
for (const [freq, durMs] of melody) {
|
|
1373
|
+
if (freq === 0) {
|
|
1374
|
+
parts.push(silence(durMs));
|
|
1375
|
+
} else {
|
|
1376
|
+
parts.push(bellNote(freq, durMs, volume));
|
|
1377
|
+
parts.push(silence(150));
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
const pcm = int16ArrayToBuffer(concatInt16Arrays(...parts));
|
|
1381
|
+
const ulaw = pcm16ToUlaw(pcm);
|
|
1382
|
+
const chunks = [];
|
|
1383
|
+
for (let i = 0; i < ulaw.length; i += CHUNK_SIZE) {
|
|
1384
|
+
chunks.push(ulaw.subarray(i, i + CHUNK_SIZE));
|
|
1385
|
+
}
|
|
1386
|
+
return chunks;
|
|
1387
|
+
}
|
|
1388
|
+
function loadHoldAudio(source) {
|
|
1389
|
+
if (source === true) {
|
|
1390
|
+
return generateComfortTone();
|
|
1391
|
+
}
|
|
1392
|
+
if (Buffer.isBuffer(source)) {
|
|
1393
|
+
const chunks = [];
|
|
1394
|
+
for (let i = 0; i < source.length; i += CHUNK_SIZE) {
|
|
1395
|
+
chunks.push(source.subarray(i, i + CHUNK_SIZE));
|
|
1396
|
+
}
|
|
1397
|
+
return chunks;
|
|
1398
|
+
}
|
|
1399
|
+
if (typeof source === "string") {
|
|
1400
|
+
const data = fs.readFileSync(source);
|
|
1401
|
+
const riff = data.toString("ascii", 0, 4);
|
|
1402
|
+
if (riff !== "RIFF") {
|
|
1403
|
+
throw new Error(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${source}`);
|
|
1404
|
+
}
|
|
1405
|
+
let offset = 12;
|
|
1406
|
+
let channels = 1;
|
|
1407
|
+
let sampleRate = 8e3;
|
|
1408
|
+
let bitsPerSample = 16;
|
|
1409
|
+
let pcmData = null;
|
|
1410
|
+
while (offset < data.length - 8) {
|
|
1411
|
+
const chunkId = data.toString("ascii", offset, offset + 4);
|
|
1412
|
+
const chunkSize = data.readUInt32LE(offset + 4);
|
|
1413
|
+
if (chunkId === "fmt ") {
|
|
1414
|
+
channels = data.readUInt16LE(offset + 10);
|
|
1415
|
+
sampleRate = data.readUInt32LE(offset + 12);
|
|
1416
|
+
bitsPerSample = data.readUInt16LE(offset + 22);
|
|
1417
|
+
} else if (chunkId === "data") {
|
|
1418
|
+
pcmData = data.subarray(offset + 8, offset + 8 + chunkSize);
|
|
1419
|
+
}
|
|
1420
|
+
offset += 8 + chunkSize;
|
|
1421
|
+
if (chunkSize % 2 !== 0) offset++;
|
|
1422
|
+
}
|
|
1423
|
+
if (!pcmData) {
|
|
1424
|
+
throw new Error(`WAV data chunk\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${source}`);
|
|
1425
|
+
}
|
|
1426
|
+
if (bitsPerSample !== 16) {
|
|
1427
|
+
throw new Error(`16-bit PCM wav\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4 (\uD604\uC7AC: ${bitsPerSample}-bit)`);
|
|
1428
|
+
}
|
|
1429
|
+
if (channels === 2) {
|
|
1430
|
+
const monoLen = pcmData.length / 2;
|
|
1431
|
+
const mono = Buffer.alloc(monoLen);
|
|
1432
|
+
for (let i = 0; i < monoLen / 2; i++) {
|
|
1433
|
+
mono.writeInt16LE(pcmData.readInt16LE(i * 4), i * 2);
|
|
1434
|
+
}
|
|
1435
|
+
pcmData = mono;
|
|
1436
|
+
}
|
|
1437
|
+
if (sampleRate !== SAMPLE_RATE2) {
|
|
1438
|
+
pcmData = resamplePcm16(pcmData, sampleRate, SAMPLE_RATE2);
|
|
1439
|
+
}
|
|
1440
|
+
const ulaw = pcm16ToUlaw(pcmData);
|
|
1441
|
+
const chunks = [];
|
|
1442
|
+
for (let i = 0; i < ulaw.length; i += CHUNK_SIZE) {
|
|
1443
|
+
chunks.push(ulaw.subarray(i, i + CHUNK_SIZE));
|
|
1444
|
+
}
|
|
1445
|
+
return chunks;
|
|
1446
|
+
}
|
|
1447
|
+
throw new TypeError(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 holdAudio \uD0C0\uC785: ${typeof source}`);
|
|
1448
|
+
}
|
|
1449
|
+
var HoldAudioPlayer = class {
|
|
1450
|
+
_call;
|
|
1451
|
+
_chunks;
|
|
1452
|
+
_timer = null;
|
|
1453
|
+
_index = 0;
|
|
1454
|
+
constructor(call, chunks) {
|
|
1455
|
+
this._call = call;
|
|
1456
|
+
this._chunks = chunks;
|
|
1457
|
+
}
|
|
1458
|
+
start() {
|
|
1459
|
+
if (this._timer !== null) return;
|
|
1460
|
+
this._index = 0;
|
|
1461
|
+
this._timer = setInterval(() => {
|
|
1462
|
+
if (this._index >= this._chunks.length) {
|
|
1463
|
+
this._index = 0;
|
|
1464
|
+
}
|
|
1465
|
+
const chunk = this._chunks[this._index++];
|
|
1466
|
+
if (chunk) {
|
|
1467
|
+
this._call.sendAudio(chunk);
|
|
1468
|
+
}
|
|
1469
|
+
}, 20);
|
|
1470
|
+
}
|
|
1471
|
+
stop() {
|
|
1472
|
+
if (this._timer === null) return;
|
|
1473
|
+
clearInterval(this._timer);
|
|
1474
|
+
this._timer = null;
|
|
1475
|
+
this._call.clearAudio();
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1239
1478
|
|
|
1240
1479
|
// src/agent/tool.ts
|
|
1241
1480
|
function functionTool(fn) {
|
|
@@ -1454,6 +1693,7 @@ var ClawOpsAgent = class {
|
|
|
1454
1693
|
_log;
|
|
1455
1694
|
_pipelineLog;
|
|
1456
1695
|
_isPipelineSession = false;
|
|
1696
|
+
_holdAudioChunks = null;
|
|
1457
1697
|
constructor(options) {
|
|
1458
1698
|
this._apiKey = options.apiKey ?? process.env["CLAWOPS_API_KEY"] ?? "";
|
|
1459
1699
|
this._accountId = options.accountId ?? process.env["CLAWOPS_ACCOUNT_ID"] ?? "";
|
|
@@ -1471,6 +1711,9 @@ var ClawOpsAgent = class {
|
|
|
1471
1711
|
this._log = createAgentLogger(options.logger);
|
|
1472
1712
|
this._pipelineLog = createPipelineLogger(this._log);
|
|
1473
1713
|
this._isPipelineSession = "_stt" in this._session && "_llm" in this._session;
|
|
1714
|
+
if (options.toolConfig?.holdAudio) {
|
|
1715
|
+
this._holdAudioChunks = loadHoldAudio(options.toolConfig.holdAudio);
|
|
1716
|
+
}
|
|
1474
1717
|
}
|
|
1475
1718
|
/**
|
|
1476
1719
|
* Register a function tool.
|
|
@@ -1775,6 +2018,7 @@ var ClawOpsAgent = class {
|
|
|
1775
2018
|
},
|
|
1776
2019
|
() => mediaWs.isConnected
|
|
1777
2020
|
);
|
|
2021
|
+
session._transferFn = (params) => this._controlWs.requestTransfer(session.callId, params);
|
|
1778
2022
|
const sessionHandler = this._session;
|
|
1779
2023
|
if ("setToolRegistry" in sessionHandler && typeof sessionHandler.setToolRegistry === "function") {
|
|
1780
2024
|
sessionHandler.setToolRegistry(sessionTools);
|
|
@@ -1788,6 +2032,9 @@ var ClawOpsAgent = class {
|
|
|
1788
2032
|
if ("setLogger" in sessionHandler && typeof sessionHandler.setLogger === "function") {
|
|
1789
2033
|
sessionHandler.setLogger(this._isPipelineSession ? this._pipelineLog : this._log);
|
|
1790
2034
|
}
|
|
2035
|
+
if (this._holdAudioChunks && "setHoldAudio" in sessionHandler && typeof sessionHandler.setHoldAudio === "function") {
|
|
2036
|
+
sessionHandler.setHoldAudio(this._holdAudioChunks);
|
|
2037
|
+
}
|
|
1791
2038
|
this._callSessions.set(session.callId, sessionHandler);
|
|
1792
2039
|
mediaWs.onAudio((ulawAudio, _timestamp) => {
|
|
1793
2040
|
if (sessionHandler) {
|
|
@@ -1889,10 +2136,27 @@ var SEND_DTMF = {
|
|
|
1889
2136
|
required: ["digits"]
|
|
1890
2137
|
}
|
|
1891
2138
|
};
|
|
2139
|
+
var TRANSFER_CALL = {
|
|
2140
|
+
name: "transfer_call",
|
|
2141
|
+
description: "Transfer the current call to another phone number. Use for blind transfer (direct handoff) or warm transfer (with whisper message to the target).",
|
|
2142
|
+
parameters: {
|
|
2143
|
+
type: "object",
|
|
2144
|
+
properties: {
|
|
2145
|
+
to: { type: "string", description: "Phone number to transfer to" },
|
|
2146
|
+
mode: { type: "string", enum: ["blind", "warm"], description: "blind: direct transfer (default), warm: play whisper to target first" },
|
|
2147
|
+
after_transfer: { type: "string", enum: ["terminate", "return"], description: "terminate: end AI session (default), return: AI resumes after transfer ends" },
|
|
2148
|
+
whisper: { type: "string", description: "Message to speak to transfer target before connecting customer (warm mode only)" },
|
|
2149
|
+
caller_id: { type: "string", description: "Override caller ID for the transfer leg" },
|
|
2150
|
+
timeout: { type: "integer", description: "Seconds to wait for transfer target to answer (default 30)" }
|
|
2151
|
+
},
|
|
2152
|
+
required: ["to"]
|
|
2153
|
+
}
|
|
2154
|
+
};
|
|
1892
2155
|
var TOOL_MAP = /* @__PURE__ */ new Map([
|
|
1893
2156
|
["hang_up" /* HANG_UP */, HANG_UP],
|
|
1894
2157
|
["collect_dtmf" /* COLLECT_DTMF */, COLLECT_DTMF],
|
|
1895
|
-
["send_dtmf" /* SEND_DTMF */, SEND_DTMF]
|
|
2158
|
+
["send_dtmf" /* SEND_DTMF */, SEND_DTMF],
|
|
2159
|
+
["transfer_call" /* TRANSFER_CALL */, TRANSFER_CALL]
|
|
1896
2160
|
]);
|
|
1897
2161
|
var BUILTIN_TOOL_NAMES = new Set(
|
|
1898
2162
|
Array.from(TOOL_MAP.values()).map((s) => s.name)
|
|
@@ -1965,6 +2229,21 @@ async function executeBuiltinTool(funcName, args, call) {
|
|
|
1965
2229
|
return `Error: ${e}`;
|
|
1966
2230
|
}
|
|
1967
2231
|
}
|
|
2232
|
+
if (funcName === "transfer_call") {
|
|
2233
|
+
try {
|
|
2234
|
+
call.transfer(args["to"], {
|
|
2235
|
+
mode: args["mode"] ?? void 0,
|
|
2236
|
+
afterTransfer: args["after_transfer"] ?? void 0,
|
|
2237
|
+
whisper: args["whisper"] ?? void 0,
|
|
2238
|
+
callerId: args["caller_id"] ?? void 0,
|
|
2239
|
+
timeout: args["timeout"] ?? void 0
|
|
2240
|
+
}).catch(() => {
|
|
2241
|
+
});
|
|
2242
|
+
return JSON.stringify({ status: "transfer_initiated" });
|
|
2243
|
+
} catch (e) {
|
|
2244
|
+
return `Error: ${e}`;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
1968
2247
|
return null;
|
|
1969
2248
|
}
|
|
1970
2249
|
|
|
@@ -1988,6 +2267,7 @@ var PipelineSession = class {
|
|
|
1988
2267
|
_running = false;
|
|
1989
2268
|
_speaking = false;
|
|
1990
2269
|
_builtinTools = null;
|
|
2270
|
+
_holdAudioChunks = null;
|
|
1991
2271
|
_log = NOOP_LOGGER;
|
|
1992
2272
|
constructor(options) {
|
|
1993
2273
|
this._stt = options.stt;
|
|
@@ -2012,6 +2292,10 @@ var PipelineSession = class {
|
|
|
2012
2292
|
setBuiltinTools(tools) {
|
|
2013
2293
|
this._builtinTools = tools;
|
|
2014
2294
|
}
|
|
2295
|
+
/** Tool 실행 중 재생할 hold audio 청크를 설정한다. */
|
|
2296
|
+
setHoldAudio(chunks) {
|
|
2297
|
+
this._holdAudioChunks = chunks;
|
|
2298
|
+
}
|
|
2015
2299
|
getTelemetry() {
|
|
2016
2300
|
const llm = this._llm;
|
|
2017
2301
|
const stt = this._stt;
|
|
@@ -2177,7 +2461,14 @@ var PipelineSession = class {
|
|
|
2177
2461
|
}
|
|
2178
2462
|
if (!this._tools) return;
|
|
2179
2463
|
this._callSession?.recordToolCall();
|
|
2180
|
-
const
|
|
2464
|
+
const player = this._holdAudioChunks && this._callSession ? new HoldAudioPlayer(this._callSession, this._holdAudioChunks) : null;
|
|
2465
|
+
player?.start();
|
|
2466
|
+
let result;
|
|
2467
|
+
try {
|
|
2468
|
+
result = await this._tools.call(name, args);
|
|
2469
|
+
} finally {
|
|
2470
|
+
player?.stop();
|
|
2471
|
+
}
|
|
2181
2472
|
this._conversation.push({
|
|
2182
2473
|
role: "assistant",
|
|
2183
2474
|
content: ""
|
|
@@ -2289,6 +2580,8 @@ var OpenAIRealtime = class {
|
|
|
2289
2580
|
// Response state tracking — prevent sending response.create while one is active
|
|
2290
2581
|
_responseInProgress = false;
|
|
2291
2582
|
_onResponseDone = null;
|
|
2583
|
+
// Hold audio — tool 실행 중 대기음
|
|
2584
|
+
_holdAudioChunks = null;
|
|
2292
2585
|
constructor(options = {}) {
|
|
2293
2586
|
this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
|
|
2294
2587
|
this._systemPrompt = options.systemPrompt ?? "";
|
|
@@ -2306,6 +2599,10 @@ var OpenAIRealtime = class {
|
|
|
2306
2599
|
setRecorder(recorder) {
|
|
2307
2600
|
this._recorder = recorder;
|
|
2308
2601
|
}
|
|
2602
|
+
/** Tool 실행 중 재생할 hold audio 청크를 설정한다. */
|
|
2603
|
+
setHoldAudio(chunks) {
|
|
2604
|
+
this._holdAudioChunks = chunks;
|
|
2605
|
+
}
|
|
2309
2606
|
async start(callSession, tools) {
|
|
2310
2607
|
this._call = callSession;
|
|
2311
2608
|
if (tools) this._tools = tools;
|
|
@@ -2561,6 +2858,8 @@ var OpenAIRealtime = class {
|
|
|
2561
2858
|
return;
|
|
2562
2859
|
}
|
|
2563
2860
|
let result;
|
|
2861
|
+
const player = this._holdAudioChunks && this._call ? new HoldAudioPlayer(this._call, this._holdAudioChunks) : null;
|
|
2862
|
+
player?.start();
|
|
2564
2863
|
try {
|
|
2565
2864
|
const args = JSON.parse(item["arguments"] ?? "{}");
|
|
2566
2865
|
this._call?.recordToolCall();
|
|
@@ -2571,6 +2870,8 @@ var OpenAIRealtime = class {
|
|
|
2571
2870
|
this._call?.recordToolError(err);
|
|
2572
2871
|
}
|
|
2573
2872
|
result = `Error: ${err}`;
|
|
2873
|
+
} finally {
|
|
2874
|
+
player?.stop();
|
|
2574
2875
|
}
|
|
2575
2876
|
if (controller.signal.aborted) {
|
|
2576
2877
|
this._log.info("Tool call cancelled (user interrupted): %s", funcName);
|
|
@@ -2678,7 +2979,7 @@ function sanitizeSchemaForGemini(schema, defs, depth = 0) {
|
|
|
2678
2979
|
if (result["type"] === "object" && !result["properties"]) result["properties"] = {};
|
|
2679
2980
|
return result;
|
|
2680
2981
|
}
|
|
2681
|
-
var GeminiRealtime = class {
|
|
2982
|
+
var GeminiRealtime = class _GeminiRealtime {
|
|
2682
2983
|
_apiKey;
|
|
2683
2984
|
_systemPrompt;
|
|
2684
2985
|
_model;
|
|
@@ -2694,12 +2995,15 @@ var GeminiRealtime = class {
|
|
|
2694
2995
|
_sentAudioChunks = 0;
|
|
2695
2996
|
_audioRemainder = Buffer.alloc(0);
|
|
2696
2997
|
_builtinTools = null;
|
|
2697
|
-
|
|
2998
|
+
_pendingToolCall = null;
|
|
2999
|
+
_toolDrainTimer = null;
|
|
3000
|
+
_lastAudioTime = 0;
|
|
3001
|
+
_holdAudioChunks = null;
|
|
2698
3002
|
_log = NOOP_LOGGER;
|
|
2699
3003
|
constructor(options = {}) {
|
|
2700
3004
|
this._apiKey = options.apiKey ?? process.env["GOOGLE_API_KEY"] ?? "";
|
|
2701
3005
|
this._systemPrompt = options.systemPrompt ?? "";
|
|
2702
|
-
this._model = options.model ?? "gemini-
|
|
3006
|
+
this._model = options.model ?? "gemini-3.1-flash-live-preview";
|
|
2703
3007
|
this._voice = options.voice ?? "Kore";
|
|
2704
3008
|
this._language = options.language ?? "ko";
|
|
2705
3009
|
this._greeting = options.greeting ?? true;
|
|
@@ -2715,6 +3019,10 @@ var GeminiRealtime = class {
|
|
|
2715
3019
|
setBuiltinTools(tools) {
|
|
2716
3020
|
this._builtinTools = tools;
|
|
2717
3021
|
}
|
|
3022
|
+
/** Tool 실행 중 재생할 hold audio 청크를 설정한다. */
|
|
3023
|
+
setHoldAudio(chunks) {
|
|
3024
|
+
this._holdAudioChunks = chunks;
|
|
3025
|
+
}
|
|
2718
3026
|
setLogger(logger) {
|
|
2719
3027
|
this._log = logger;
|
|
2720
3028
|
}
|
|
@@ -2739,11 +3047,8 @@ var GeminiRealtime = class {
|
|
|
2739
3047
|
this._closed = false;
|
|
2740
3048
|
this._sentAudioChunks = 0;
|
|
2741
3049
|
this._audioRemainder = Buffer.alloc(0);
|
|
2742
|
-
if (!this._apiKey) {
|
|
2743
|
-
throw new Error("Google API key is required. Set GOOGLE_API_KEY or pass apiKey option.");
|
|
2744
|
-
}
|
|
2745
3050
|
const { GoogleGenAI } = await import('@google/genai/node');
|
|
2746
|
-
const client = new GoogleGenAI({ apiKey: this._apiKey });
|
|
3051
|
+
const client = this._apiKey ? new GoogleGenAI({ apiKey: this._apiKey }) : new GoogleGenAI({});
|
|
2747
3052
|
const config = {
|
|
2748
3053
|
responseModalities: ["AUDIO"],
|
|
2749
3054
|
speechConfig: {
|
|
@@ -2757,7 +3062,9 @@ var GeminiRealtime = class {
|
|
|
2757
3062
|
outputAudioTranscription: {}
|
|
2758
3063
|
};
|
|
2759
3064
|
if (this._systemPrompt) {
|
|
2760
|
-
config["systemInstruction"] =
|
|
3065
|
+
config["systemInstruction"] = {
|
|
3066
|
+
parts: [{ text: this._systemPrompt }]
|
|
3067
|
+
};
|
|
2761
3068
|
}
|
|
2762
3069
|
const toolSchemas = this._buildToolSchemas();
|
|
2763
3070
|
if (toolSchemas.length > 0) {
|
|
@@ -2772,25 +3079,20 @@ var GeminiRealtime = class {
|
|
|
2772
3079
|
this._log.error({ err }, "Gemini SDK error");
|
|
2773
3080
|
},
|
|
2774
3081
|
onclose: (ev) => {
|
|
2775
|
-
this._log.info(
|
|
3082
|
+
this._log.info(
|
|
3083
|
+
{ code: ev?.code ?? "unknown" },
|
|
3084
|
+
"Gemini connection closed"
|
|
3085
|
+
);
|
|
2776
3086
|
this._closed = true;
|
|
2777
3087
|
}
|
|
2778
3088
|
}
|
|
2779
3089
|
});
|
|
2780
3090
|
if (this._greeting) {
|
|
2781
|
-
this._session.
|
|
2782
|
-
turns: [
|
|
2783
|
-
{
|
|
2784
|
-
role: "user",
|
|
2785
|
-
parts: [{ text: "\uC778\uC0AC\uD574 \uC8FC\uC138\uC694." }]
|
|
2786
|
-
}
|
|
2787
|
-
],
|
|
2788
|
-
turnComplete: true
|
|
2789
|
-
});
|
|
3091
|
+
this._session.sendRealtimeInput({ text: "\uC778\uC0AC\uD574 \uC8FC\uC138\uC694." });
|
|
2790
3092
|
}
|
|
2791
3093
|
}
|
|
2792
3094
|
feedAudio(audio) {
|
|
2793
|
-
if (this._session && !this._closed
|
|
3095
|
+
if (this._session && !this._closed) {
|
|
2794
3096
|
const pcm8k = ulawToPcm16(audio);
|
|
2795
3097
|
if (this._recorder) {
|
|
2796
3098
|
this._recorder.writeInbound(pcm8k);
|
|
@@ -2806,14 +3108,16 @@ var GeminiRealtime = class {
|
|
|
2806
3108
|
}
|
|
2807
3109
|
async feedDtmf(digits) {
|
|
2808
3110
|
if (this._session) {
|
|
2809
|
-
this._session.
|
|
2810
|
-
turns: [{ role: "user", parts: [{ text: `[DTMF \uC785\uB825: ${digits}]` }] }],
|
|
2811
|
-
turnComplete: true
|
|
2812
|
-
});
|
|
3111
|
+
this._session.sendRealtimeInput({ text: `[DTMF \uC785\uB825: ${digits}]` });
|
|
2813
3112
|
}
|
|
2814
3113
|
}
|
|
2815
3114
|
async stop() {
|
|
2816
3115
|
this._closed = true;
|
|
3116
|
+
if (this._toolDrainTimer) {
|
|
3117
|
+
clearTimeout(this._toolDrainTimer);
|
|
3118
|
+
this._toolDrainTimer = null;
|
|
3119
|
+
}
|
|
3120
|
+
this._pendingToolCall = null;
|
|
2817
3121
|
if (this._session) {
|
|
2818
3122
|
try {
|
|
2819
3123
|
this._session.close();
|
|
@@ -2845,6 +3149,7 @@ var GeminiRealtime = class {
|
|
|
2845
3149
|
const mimeType = inlineData.mimeType ?? "";
|
|
2846
3150
|
if (mimeType.includes("audio")) {
|
|
2847
3151
|
this._handleAudioData(inlineData.data);
|
|
3152
|
+
if (this._pendingToolCall) this._lastAudioTime = Date.now();
|
|
2848
3153
|
}
|
|
2849
3154
|
}
|
|
2850
3155
|
}
|
|
@@ -2873,11 +3178,17 @@ var GeminiRealtime = class {
|
|
|
2873
3178
|
}
|
|
2874
3179
|
}
|
|
2875
3180
|
if (msg.toolCall) {
|
|
2876
|
-
this.
|
|
3181
|
+
this._pendingToolCall = msg.toolCall;
|
|
3182
|
+
this._scheduleToolExecution();
|
|
2877
3183
|
}
|
|
2878
3184
|
const toolCancellation = msg["toolCallCancellation"];
|
|
2879
3185
|
if (toolCancellation) {
|
|
2880
3186
|
this._log.info({ ids: toolCancellation.ids }, "Tool call cancelled");
|
|
3187
|
+
this._pendingToolCall = null;
|
|
3188
|
+
if (this._toolDrainTimer) {
|
|
3189
|
+
clearTimeout(this._toolDrainTimer);
|
|
3190
|
+
this._toolDrainTimer = null;
|
|
3191
|
+
}
|
|
2881
3192
|
}
|
|
2882
3193
|
}
|
|
2883
3194
|
_handleAudioData(b64Data) {
|
|
@@ -2908,54 +3219,81 @@ var GeminiRealtime = class {
|
|
|
2908
3219
|
this._audioRemainder = Buffer.alloc(0);
|
|
2909
3220
|
}
|
|
2910
3221
|
}
|
|
3222
|
+
// Gemini는 tool_call 후에도 오디오를 계속 보내므로, 이 시간 내 응답이 없으면 drain 완료로 간주
|
|
3223
|
+
static TOOL_DRAIN_TIMEOUT = 300;
|
|
3224
|
+
_scheduleToolExecution() {
|
|
3225
|
+
if (this._toolDrainTimer) return;
|
|
3226
|
+
this._lastAudioTime = Date.now();
|
|
3227
|
+
this._toolDrainTimer = setTimeout(() => {
|
|
3228
|
+
this._toolDrainTimer = null;
|
|
3229
|
+
if (Date.now() - this._lastAudioTime < _GeminiRealtime.TOOL_DRAIN_TIMEOUT) {
|
|
3230
|
+
this._scheduleToolExecution();
|
|
3231
|
+
return;
|
|
3232
|
+
}
|
|
3233
|
+
if (this._pendingToolCall) {
|
|
3234
|
+
const tc = this._pendingToolCall;
|
|
3235
|
+
this._pendingToolCall = null;
|
|
3236
|
+
this._handleToolCall(tc);
|
|
3237
|
+
}
|
|
3238
|
+
}, _GeminiRealtime.TOOL_DRAIN_TIMEOUT);
|
|
3239
|
+
}
|
|
2911
3240
|
async _handleToolCall(toolCall) {
|
|
2912
3241
|
const functionCalls = toolCall.functionCalls;
|
|
2913
3242
|
if (!functionCalls) return;
|
|
2914
|
-
this._toolCallInProgress = true;
|
|
2915
3243
|
const responses = [];
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
const
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
const
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3244
|
+
const player = this._holdAudioChunks && this._call ? new HoldAudioPlayer(this._call, this._holdAudioChunks) : null;
|
|
3245
|
+
player?.start();
|
|
3246
|
+
try {
|
|
3247
|
+
for (const fc of functionCalls) {
|
|
3248
|
+
const name = fc.name ?? "";
|
|
3249
|
+
const fcId = fc.id ?? "";
|
|
3250
|
+
const args = fc.args ?? {};
|
|
3251
|
+
this._log.info({ tool: name, args }, "Tool call: %s", name);
|
|
3252
|
+
if (BUILTIN_TOOL_NAMES.has(name) && this._call) {
|
|
3253
|
+
const result = await executeBuiltinTool(
|
|
3254
|
+
name,
|
|
3255
|
+
args,
|
|
3256
|
+
this._call
|
|
3257
|
+
);
|
|
3258
|
+
if (result !== null) {
|
|
3259
|
+
if (name === "hang_up") {
|
|
3260
|
+
this._log.info("hang_up: ending call");
|
|
3261
|
+
return;
|
|
3262
|
+
}
|
|
3263
|
+
this._log.info("Builtin tool result: %s -> %s", name, result);
|
|
3264
|
+
responses.push({ id: fcId, name, response: { result } });
|
|
3265
|
+
continue;
|
|
2927
3266
|
}
|
|
2928
|
-
|
|
2929
|
-
|
|
3267
|
+
}
|
|
3268
|
+
if (!this._tools || !this._tools.has(name)) {
|
|
3269
|
+
this._log.error("Unknown tool: %s", name);
|
|
3270
|
+
responses.push({ id: fcId, name, response: { error: `Unknown tool: ${name}` } });
|
|
2930
3271
|
continue;
|
|
2931
3272
|
}
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
3273
|
+
try {
|
|
3274
|
+
this._call?.recordToolCall();
|
|
3275
|
+
const result = await this._tools.call(name, args);
|
|
3276
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
3277
|
+
this._log.info("Tool result: %s -> %s", name, resultStr.substring(0, 200));
|
|
3278
|
+
responses.push({
|
|
3279
|
+
id: fcId,
|
|
3280
|
+
name,
|
|
3281
|
+
response: { result: resultStr }
|
|
3282
|
+
});
|
|
3283
|
+
} catch (err) {
|
|
3284
|
+
this._log.error({ err }, "Tool call failed: %s", name);
|
|
3285
|
+
if (err instanceof Error) {
|
|
3286
|
+
this._call?.recordToolError(err);
|
|
3287
|
+
}
|
|
3288
|
+
responses.push({
|
|
3289
|
+
id: fcId,
|
|
3290
|
+
name,
|
|
3291
|
+
response: { error: String(err) }
|
|
3292
|
+
});
|
|
2952
3293
|
}
|
|
2953
|
-
responses.push({
|
|
2954
|
-
id: fcId,
|
|
2955
|
-
name,
|
|
2956
|
-
response: { error: String(err) }
|
|
2957
|
-
});
|
|
2958
3294
|
}
|
|
3295
|
+
} finally {
|
|
3296
|
+
player?.stop();
|
|
2959
3297
|
}
|
|
2960
3298
|
if (responses.length > 0 && this._session) {
|
|
2961
3299
|
this._log.debug("Sending %d tool response(s)", responses.length);
|
|
@@ -2963,7 +3301,6 @@ var GeminiRealtime = class {
|
|
|
2963
3301
|
functionResponses: responses
|
|
2964
3302
|
});
|
|
2965
3303
|
}
|
|
2966
|
-
this._toolCallInProgress = false;
|
|
2967
3304
|
}
|
|
2968
3305
|
};
|
|
2969
3306
|
|
|
@@ -3478,9 +3815,8 @@ var GeminiLLM = class {
|
|
|
3478
3815
|
}
|
|
3479
3816
|
async *generate(messages, options) {
|
|
3480
3817
|
const { GoogleGenAI } = await import('@google/genai');
|
|
3481
|
-
const
|
|
3482
|
-
|
|
3483
|
-
});
|
|
3818
|
+
const apiKey = this._options.apiKey ?? process.env["GOOGLE_API_KEY"];
|
|
3819
|
+
const client = apiKey ? new GoogleGenAI({ apiKey }) : new GoogleGenAI();
|
|
3484
3820
|
let systemInstruction;
|
|
3485
3821
|
const contents = [];
|
|
3486
3822
|
for (const msg of messages) {
|