@teamlearners/clawops 0.5.1 → 0.5.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/README.md +9 -0
- package/dist/agent/index.cjs +718 -59
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +113 -6
- package/dist/agent/index.d.ts +113 -6
- package/dist/agent/index.js +713 -60
- package/dist/agent/index.js.map +1 -1
- package/dist/index.cjs +2 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -5
- package/dist/index.d.ts +8 -5
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +47 -21
package/dist/agent/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_BASE_URL, AgentError, AgentConnectionError } from '../chunk-7S2OEBS6.js';
|
|
2
|
+
import pino from 'pino';
|
|
2
3
|
import * as fs from 'fs';
|
|
3
4
|
import * as path from 'path';
|
|
4
5
|
|
|
@@ -322,6 +323,14 @@ function resamplePcm16(pcm, fromRate, toRate) {
|
|
|
322
323
|
}
|
|
323
324
|
return out;
|
|
324
325
|
}
|
|
326
|
+
var DEFAULT_LOGGER = pino({ name: "clawops.agent" });
|
|
327
|
+
var NOOP_LOGGER = pino({ level: "silent" });
|
|
328
|
+
function createAgentLogger(userLogger) {
|
|
329
|
+
return userLogger ?? DEFAULT_LOGGER;
|
|
330
|
+
}
|
|
331
|
+
function createPipelineLogger(parent) {
|
|
332
|
+
return parent.child({ module: "pipeline" });
|
|
333
|
+
}
|
|
325
334
|
|
|
326
335
|
// src/agent/control-ws.ts
|
|
327
336
|
var INITIAL_RECONNECT_DELAY = 1e3;
|
|
@@ -350,6 +359,10 @@ var ControlWebSocket = class {
|
|
|
350
359
|
_closed = false;
|
|
351
360
|
_connectedResolve = null;
|
|
352
361
|
_connectedPromise;
|
|
362
|
+
_log = NOOP_LOGGER;
|
|
363
|
+
setLogger(logger) {
|
|
364
|
+
this._log = logger;
|
|
365
|
+
}
|
|
353
366
|
/** Register an event handler for a specific event type. */
|
|
354
367
|
on(event, handler) {
|
|
355
368
|
let list = this._handlers.get(event);
|
|
@@ -397,13 +410,14 @@ var ControlWebSocket = class {
|
|
|
397
410
|
this._connectedResolve();
|
|
398
411
|
this._connectedResolve = null;
|
|
399
412
|
}
|
|
413
|
+
this._log.info("Control WS connected: %s", this._url);
|
|
400
414
|
});
|
|
401
415
|
ws.on("message", (data) => {
|
|
402
416
|
try {
|
|
403
417
|
const msg = JSON.parse(data.toString());
|
|
404
418
|
this._dispatchEvent(msg);
|
|
405
419
|
} catch {
|
|
406
|
-
|
|
420
|
+
this._log.warn("Control WS parse error");
|
|
407
421
|
}
|
|
408
422
|
});
|
|
409
423
|
ws.on("close", () => {
|
|
@@ -412,7 +426,7 @@ var ControlWebSocket = class {
|
|
|
412
426
|
}
|
|
413
427
|
});
|
|
414
428
|
ws.on("error", (err) => {
|
|
415
|
-
|
|
429
|
+
this._log.warn("Control WS error: %s", err.message);
|
|
416
430
|
});
|
|
417
431
|
}
|
|
418
432
|
_dispatchEvent(event) {
|
|
@@ -423,11 +437,11 @@ var ControlWebSocket = class {
|
|
|
423
437
|
const result = handler(event);
|
|
424
438
|
if (result && typeof result.catch === "function") {
|
|
425
439
|
result.catch((err) => {
|
|
426
|
-
|
|
440
|
+
this._log.error({ err }, "Control WS handler error: %s", event.event);
|
|
427
441
|
});
|
|
428
442
|
}
|
|
429
443
|
} catch (err) {
|
|
430
|
-
|
|
444
|
+
this._log.error({ err }, "Control WS handler error: %s", event.event);
|
|
431
445
|
}
|
|
432
446
|
}
|
|
433
447
|
}
|
|
@@ -435,10 +449,11 @@ var ControlWebSocket = class {
|
|
|
435
449
|
_scheduleReconnect() {
|
|
436
450
|
const delay = this._reconnectDelay;
|
|
437
451
|
this._reconnectDelay = Math.min(this._reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
|
452
|
+
this._log.info("Control WS reconnecting in %ds...", delay / 1e3);
|
|
438
453
|
setTimeout(() => {
|
|
439
454
|
if (!this._closed) {
|
|
440
455
|
this._doConnect().catch((err) => {
|
|
441
|
-
|
|
456
|
+
this._log.warn({ err }, "Control WS reconnect failed");
|
|
442
457
|
this._scheduleReconnect();
|
|
443
458
|
});
|
|
444
459
|
}
|
|
@@ -450,6 +465,10 @@ var ControlWebSocket = class {
|
|
|
450
465
|
var MCPClient = class {
|
|
451
466
|
_servers = /* @__PURE__ */ new Map();
|
|
452
467
|
_clients = /* @__PURE__ */ new Map();
|
|
468
|
+
_log = NOOP_LOGGER;
|
|
469
|
+
setLogger(logger) {
|
|
470
|
+
this._log = logger;
|
|
471
|
+
}
|
|
453
472
|
/** Add an MCP server configuration. */
|
|
454
473
|
addServer(name, config) {
|
|
455
474
|
this._servers.set(name, config);
|
|
@@ -462,7 +481,7 @@ var MCPClient = class {
|
|
|
462
481
|
const tools = await this._connectServer(name, config);
|
|
463
482
|
allTools.push(...tools);
|
|
464
483
|
} catch (err) {
|
|
465
|
-
|
|
484
|
+
this._log.error({ err }, "MCP connection failed: %s", name);
|
|
466
485
|
}
|
|
467
486
|
}
|
|
468
487
|
return allTools;
|
|
@@ -471,12 +490,13 @@ var MCPClient = class {
|
|
|
471
490
|
async disconnect() {
|
|
472
491
|
for (const [name, client] of this._clients) {
|
|
473
492
|
try {
|
|
493
|
+
this._log.debug("MCP closing: %s", name);
|
|
474
494
|
const c = client;
|
|
475
495
|
if (c.close) {
|
|
476
496
|
await c.close();
|
|
477
497
|
}
|
|
478
498
|
} catch (err) {
|
|
479
|
-
|
|
499
|
+
this._log.error({ err }, "MCP disconnect error: %s", name);
|
|
480
500
|
}
|
|
481
501
|
}
|
|
482
502
|
this._clients.clear();
|
|
@@ -485,6 +505,11 @@ var MCPClient = class {
|
|
|
485
505
|
const sdk = await import('@modelcontextprotocol/sdk/client/index.js');
|
|
486
506
|
const { Client } = sdk;
|
|
487
507
|
const client = new Client({ name: `clawops-${name}`, version: "1.0.0" });
|
|
508
|
+
if (config.type === "stdio") {
|
|
509
|
+
this._log.debug("MCP connecting (stdio): %s", config["command"]);
|
|
510
|
+
} else if (config.type === "http") {
|
|
511
|
+
this._log.debug("MCP connecting (http): %s", config["url"]);
|
|
512
|
+
}
|
|
488
513
|
let transport;
|
|
489
514
|
if (config.type === "stdio") {
|
|
490
515
|
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
|
|
@@ -513,6 +538,7 @@ var MCPClient = class {
|
|
|
513
538
|
parameters: inputSchema.properties ?? {},
|
|
514
539
|
required: inputSchema.required ?? [],
|
|
515
540
|
handler: async (args) => {
|
|
541
|
+
this._log.debug("MCP call_tool: %s", toolDef.name);
|
|
516
542
|
const result = await client.callTool({
|
|
517
543
|
name: toolDef.name,
|
|
518
544
|
arguments: args
|
|
@@ -525,6 +551,8 @@ var MCPClient = class {
|
|
|
525
551
|
}
|
|
526
552
|
});
|
|
527
553
|
}
|
|
554
|
+
this._log.info("MCP server connected: %d tools found", tools.length);
|
|
555
|
+
this._log.debug("MCP tools: %s", tools.map((t) => t.name));
|
|
528
556
|
return tools;
|
|
529
557
|
}
|
|
530
558
|
};
|
|
@@ -555,6 +583,20 @@ function buildMediaResponse(audioBase64) {
|
|
|
555
583
|
}
|
|
556
584
|
});
|
|
557
585
|
}
|
|
586
|
+
var VALID_DTMF_DIGITS = new Set("0123456789*#");
|
|
587
|
+
function parseDtmfEvent(data) {
|
|
588
|
+
const dtmf = data["dtmf"];
|
|
589
|
+
return {
|
|
590
|
+
digit: dtmf["digit"] ?? "",
|
|
591
|
+
track: dtmf["track"] ?? ""
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function buildDtmfMessage(digit) {
|
|
595
|
+
if (!VALID_DTMF_DIGITS.has(digit)) {
|
|
596
|
+
throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 DTMF digit: ${digit}`);
|
|
597
|
+
}
|
|
598
|
+
return JSON.stringify({ event: "dtmf", dtmf: { digit } });
|
|
599
|
+
}
|
|
558
600
|
var MediaWebSocket = class {
|
|
559
601
|
_ws = null;
|
|
560
602
|
_audioQueue = [];
|
|
@@ -563,6 +605,12 @@ var MediaWebSocket = class {
|
|
|
563
605
|
_onAudio = null;
|
|
564
606
|
_onStart = null;
|
|
565
607
|
_onClose = null;
|
|
608
|
+
_onDtmf = null;
|
|
609
|
+
_markWaiters = /* @__PURE__ */ new Map();
|
|
610
|
+
_log = NOOP_LOGGER;
|
|
611
|
+
setLogger(logger) {
|
|
612
|
+
this._log = logger;
|
|
613
|
+
}
|
|
566
614
|
/** Set the handler for inbound audio data. */
|
|
567
615
|
onAudio(handler) {
|
|
568
616
|
this._onAudio = handler;
|
|
@@ -575,6 +623,20 @@ var MediaWebSocket = class {
|
|
|
575
623
|
onClose(handler) {
|
|
576
624
|
this._onClose = handler;
|
|
577
625
|
}
|
|
626
|
+
/** Set the handler for inbound DTMF events. */
|
|
627
|
+
onDtmf(handler) {
|
|
628
|
+
this._onDtmf = handler;
|
|
629
|
+
}
|
|
630
|
+
/** Send a single DTMF digit to the platform. */
|
|
631
|
+
sendDtmf(digit) {
|
|
632
|
+
if (this._ws && this._ws.readyState === 1) {
|
|
633
|
+
this._ws.send(buildDtmfMessage(digit));
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
/** Whether the WebSocket is connected. */
|
|
637
|
+
get isConnected() {
|
|
638
|
+
return this._ws !== null && this._ws.readyState === 1 && !this._closed;
|
|
639
|
+
}
|
|
578
640
|
/** Connect to a media WebSocket URL with Bearer authentication. */
|
|
579
641
|
async connect(url, apiKey) {
|
|
580
642
|
const { WebSocket } = await import('ws');
|
|
@@ -590,6 +652,7 @@ var MediaWebSocket = class {
|
|
|
590
652
|
ws.on("open", () => {
|
|
591
653
|
this._startSendLoop();
|
|
592
654
|
resolve();
|
|
655
|
+
this._log.info("Media WS connected: %s", url);
|
|
593
656
|
});
|
|
594
657
|
ws.on("message", (data) => {
|
|
595
658
|
try {
|
|
@@ -608,7 +671,7 @@ var MediaWebSocket = class {
|
|
|
608
671
|
if (!this._ws) {
|
|
609
672
|
reject(err);
|
|
610
673
|
}
|
|
611
|
-
|
|
674
|
+
this._log.error({ err }, "Media WS error");
|
|
612
675
|
});
|
|
613
676
|
});
|
|
614
677
|
}
|
|
@@ -634,6 +697,34 @@ var MediaWebSocket = class {
|
|
|
634
697
|
);
|
|
635
698
|
}
|
|
636
699
|
}
|
|
700
|
+
/** Wait for all queued audio to be sent. */
|
|
701
|
+
flush() {
|
|
702
|
+
if (this._audioQueue.length === 0 || this._closed) return Promise.resolve();
|
|
703
|
+
return new Promise((resolve) => {
|
|
704
|
+
const check = () => {
|
|
705
|
+
if (this._audioQueue.length === 0 || this._closed) {
|
|
706
|
+
resolve();
|
|
707
|
+
} else {
|
|
708
|
+
setTimeout(check, 5);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
setTimeout(check, 5);
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
/** Wait for a named mark to be echoed back by the server. */
|
|
715
|
+
waitForMark(name, timeoutMs = 5e3) {
|
|
716
|
+
if (this._closed) return Promise.resolve();
|
|
717
|
+
return new Promise((resolve) => {
|
|
718
|
+
const timer = setTimeout(() => {
|
|
719
|
+
this._markWaiters.delete(name);
|
|
720
|
+
resolve();
|
|
721
|
+
}, timeoutMs);
|
|
722
|
+
this._markWaiters.set(name, () => {
|
|
723
|
+
clearTimeout(timer);
|
|
724
|
+
resolve();
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
}
|
|
637
728
|
/** Close the media WebSocket. */
|
|
638
729
|
close() {
|
|
639
730
|
this._closed = true;
|
|
@@ -659,6 +750,24 @@ var MediaWebSocket = class {
|
|
|
659
750
|
}
|
|
660
751
|
break;
|
|
661
752
|
}
|
|
753
|
+
case "dtmf": {
|
|
754
|
+
const dtmfEvt = parseDtmfEvent(msg);
|
|
755
|
+
if (this._onDtmf) {
|
|
756
|
+
this._onDtmf(dtmfEvt.digit);
|
|
757
|
+
}
|
|
758
|
+
break;
|
|
759
|
+
}
|
|
760
|
+
case "mark": {
|
|
761
|
+
const markName = msg["mark"]?.["name"];
|
|
762
|
+
if (markName) {
|
|
763
|
+
const resolve = this._markWaiters.get(markName);
|
|
764
|
+
if (resolve) {
|
|
765
|
+
this._markWaiters.delete(markName);
|
|
766
|
+
resolve();
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
662
771
|
case "stop": {
|
|
663
772
|
this.close();
|
|
664
773
|
break;
|
|
@@ -723,6 +832,10 @@ var AudioRecorder = class {
|
|
|
723
832
|
_mixWritten = 0;
|
|
724
833
|
_startTime = 0;
|
|
725
834
|
_started = false;
|
|
835
|
+
_log = NOOP_LOGGER;
|
|
836
|
+
setLogger(logger) {
|
|
837
|
+
this._log = logger;
|
|
838
|
+
}
|
|
726
839
|
constructor(recordingPath, callId) {
|
|
727
840
|
this._dir = path.join(recordingPath, callId);
|
|
728
841
|
}
|
|
@@ -737,6 +850,7 @@ var AudioRecorder = class {
|
|
|
737
850
|
fs.writeSync(this._fdMix, header);
|
|
738
851
|
this._startTime = performance.now();
|
|
739
852
|
this._started = true;
|
|
853
|
+
this._log.info("Recording started: %s", this._dir);
|
|
740
854
|
}
|
|
741
855
|
_expectedBytes() {
|
|
742
856
|
const elapsed = (performance.now() - this._startTime) / 1e3;
|
|
@@ -789,7 +903,7 @@ var AudioRecorder = class {
|
|
|
789
903
|
this._inWritten += pcm16_8k.length;
|
|
790
904
|
this._writeToMix(pcm16_8k, posBefore);
|
|
791
905
|
} catch (err) {
|
|
792
|
-
|
|
906
|
+
this._log.error({ err }, "Recording write error (inbound)");
|
|
793
907
|
}
|
|
794
908
|
}
|
|
795
909
|
writeOutbound(pcm16_8k) {
|
|
@@ -802,7 +916,7 @@ var AudioRecorder = class {
|
|
|
802
916
|
this._outWritten += pcm16_8k.length;
|
|
803
917
|
this._writeToMix(pcm16_8k, posBefore);
|
|
804
918
|
} catch (err) {
|
|
805
|
-
|
|
919
|
+
this._log.error({ err }, "Recording write error (outbound)");
|
|
806
920
|
}
|
|
807
921
|
}
|
|
808
922
|
stop() {
|
|
@@ -823,8 +937,10 @@ var AudioRecorder = class {
|
|
|
823
937
|
fs.writeSync(fd, makeWavHeader(maxWritten), 0, 44, 0);
|
|
824
938
|
fs.closeSync(fd);
|
|
825
939
|
}
|
|
940
|
+
const maxSec = maxWritten / 16e3;
|
|
941
|
+
this._log.info("Recording stopped: %s (%ds)", this._dir, maxSec);
|
|
826
942
|
} catch (err) {
|
|
827
|
-
|
|
943
|
+
this._log.error({ err }, "Recording stop error");
|
|
828
944
|
} finally {
|
|
829
945
|
this._fdIn = null;
|
|
830
946
|
this._fdOut = null;
|
|
@@ -847,6 +963,14 @@ var CallSession = class {
|
|
|
847
963
|
_sendAudioFn = null;
|
|
848
964
|
_clearAudioFn = null;
|
|
849
965
|
_hangupFn = null;
|
|
966
|
+
/** @internal */
|
|
967
|
+
_sendDtmfFn = null;
|
|
968
|
+
/** @internal */
|
|
969
|
+
_isTransportConnected = null;
|
|
970
|
+
_dtmfCollectorActive = false;
|
|
971
|
+
_dtmfResolvers = [];
|
|
972
|
+
_dtmfBuffer = [];
|
|
973
|
+
_log = NOOP_LOGGER;
|
|
850
974
|
_handlers = /* @__PURE__ */ new Map();
|
|
851
975
|
_endedPromise;
|
|
852
976
|
_resolveEnded;
|
|
@@ -863,6 +987,9 @@ var CallSession = class {
|
|
|
863
987
|
this._resolveEnded = resolve;
|
|
864
988
|
});
|
|
865
989
|
}
|
|
990
|
+
setLogger(logger) {
|
|
991
|
+
this._log = logger;
|
|
992
|
+
}
|
|
866
993
|
get status() {
|
|
867
994
|
return this._status;
|
|
868
995
|
}
|
|
@@ -870,10 +997,12 @@ var CallSession = class {
|
|
|
870
997
|
return (Date.now() - this.startTime.getTime()) / 1e3;
|
|
871
998
|
}
|
|
872
999
|
/** Bind transport functions (called internally by the agent). */
|
|
873
|
-
_bindTransport(send, clear, hangup) {
|
|
1000
|
+
_bindTransport(send, clear, hangup, sendDtmf, isConnected) {
|
|
874
1001
|
this._sendAudioFn = send;
|
|
875
1002
|
this._clearAudioFn = clear;
|
|
876
1003
|
this._hangupFn = hangup;
|
|
1004
|
+
if (sendDtmf) this._sendDtmfFn = sendDtmf;
|
|
1005
|
+
if (isConnected) this._isTransportConnected = isConnected;
|
|
877
1006
|
this._status = "active";
|
|
878
1007
|
}
|
|
879
1008
|
/** Send PCM16 or ulaw audio to the caller. */
|
|
@@ -888,10 +1017,78 @@ var CallSession = class {
|
|
|
888
1017
|
this._clearAudioFn();
|
|
889
1018
|
}
|
|
890
1019
|
}
|
|
891
|
-
/** Hang up the call. */
|
|
892
|
-
hangup() {
|
|
1020
|
+
/** Hang up the call, waiting for pending audio to finish. */
|
|
1021
|
+
async hangup() {
|
|
893
1022
|
if (this._hangupFn) {
|
|
894
|
-
this._hangupFn();
|
|
1023
|
+
await this._hangupFn();
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
/** @internal Route a received DTMF digit to an active collector or buffer. */
|
|
1027
|
+
_routeDtmf(digit) {
|
|
1028
|
+
if (this._dtmfCollectorActive && this._dtmfResolvers.length > 0) {
|
|
1029
|
+
const resolve = this._dtmfResolvers.shift();
|
|
1030
|
+
resolve(digit);
|
|
1031
|
+
} else {
|
|
1032
|
+
this._dtmfBuffer.push(digit);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
/** Collect DTMF digits from the caller. */
|
|
1036
|
+
async collectDtmf(options) {
|
|
1037
|
+
if (this._dtmfCollectorActive) {
|
|
1038
|
+
throw new Error("\uC774\uBBF8 DTMF \uC218\uC9D1 \uC911\uC785\uB2C8\uB2E4");
|
|
1039
|
+
}
|
|
1040
|
+
const { maxDigits, finishOnKey = "#", timeout = 5 } = options;
|
|
1041
|
+
this._dtmfCollectorActive = true;
|
|
1042
|
+
const collected = [];
|
|
1043
|
+
try {
|
|
1044
|
+
while (collected.length < maxDigits) {
|
|
1045
|
+
if (this._dtmfBuffer.length > 0) {
|
|
1046
|
+
const digit2 = this._dtmfBuffer.shift();
|
|
1047
|
+
if (digit2 === finishOnKey) break;
|
|
1048
|
+
collected.push(digit2);
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
const digit = await Promise.race([
|
|
1052
|
+
new Promise((resolve) => {
|
|
1053
|
+
this._dtmfResolvers.push(resolve);
|
|
1054
|
+
}),
|
|
1055
|
+
new Promise((resolve) => {
|
|
1056
|
+
setTimeout(() => resolve(null), timeout * 1e3);
|
|
1057
|
+
})
|
|
1058
|
+
]);
|
|
1059
|
+
if (digit === null) break;
|
|
1060
|
+
if (digit === finishOnKey) break;
|
|
1061
|
+
collected.push(digit);
|
|
1062
|
+
}
|
|
1063
|
+
} finally {
|
|
1064
|
+
this._dtmfCollectorActive = false;
|
|
1065
|
+
this._dtmfResolvers = [];
|
|
1066
|
+
this._dtmfBuffer = [];
|
|
1067
|
+
}
|
|
1068
|
+
const result = collected.join("");
|
|
1069
|
+
this._log.info("DTMF collected: %s", result);
|
|
1070
|
+
return result;
|
|
1071
|
+
}
|
|
1072
|
+
/** Send a sequence of DTMF digits. */
|
|
1073
|
+
async sendDtmfSequence(digits) {
|
|
1074
|
+
if (!this._sendDtmfFn) {
|
|
1075
|
+
throw new Error("DTMF \uC804\uC1A1 \uD568\uC218\uAC00 \uBC14\uC778\uB529\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4");
|
|
1076
|
+
}
|
|
1077
|
+
for (const ch of digits) {
|
|
1078
|
+
if (this._isTransportConnected && !this._isTransportConnected()) {
|
|
1079
|
+
throw new Error("DTMF \uC804\uC1A1 \uC911 \uC5F0\uACB0\uC774 \uB04A\uC5B4\uC84C\uC2B5\uB2C8\uB2E4");
|
|
1080
|
+
}
|
|
1081
|
+
if (ch === "w") {
|
|
1082
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1083
|
+
} else if (ch === "W") {
|
|
1084
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1085
|
+
} else if ("0123456789*#".includes(ch)) {
|
|
1086
|
+
if (this._sendDtmfFn) {
|
|
1087
|
+
await this._sendDtmfFn(ch);
|
|
1088
|
+
}
|
|
1089
|
+
} else {
|
|
1090
|
+
throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 DTMF \uBB38\uC790: ${ch}`);
|
|
1091
|
+
}
|
|
895
1092
|
}
|
|
896
1093
|
}
|
|
897
1094
|
/** Register an event handler. */
|
|
@@ -922,17 +1119,44 @@ var CallSession = class {
|
|
|
922
1119
|
const result = handler(this, ...args);
|
|
923
1120
|
if (result && typeof result.catch === "function") {
|
|
924
1121
|
result.catch((err) => {
|
|
925
|
-
|
|
1122
|
+
this._log.error({ err }, "CallSession handler error: %s", event);
|
|
926
1123
|
});
|
|
927
1124
|
}
|
|
928
1125
|
} catch (err) {
|
|
929
|
-
|
|
1126
|
+
this._log.error({ err }, "CallSession handler error: %s", event);
|
|
930
1127
|
}
|
|
931
1128
|
}
|
|
932
1129
|
}
|
|
933
1130
|
}
|
|
934
1131
|
};
|
|
935
1132
|
|
|
1133
|
+
// src/agent/builtin-tool.ts
|
|
1134
|
+
var BuiltinTool = /* @__PURE__ */ ((BuiltinTool2) => {
|
|
1135
|
+
BuiltinTool2["HANG_UP"] = "hang_up";
|
|
1136
|
+
BuiltinTool2["COLLECT_DTMF"] = "collect_dtmf";
|
|
1137
|
+
BuiltinTool2["SEND_DTMF"] = "send_dtmf";
|
|
1138
|
+
BuiltinTool2["ALL"] = "all";
|
|
1139
|
+
BuiltinTool2["NONE"] = "none";
|
|
1140
|
+
return BuiltinTool2;
|
|
1141
|
+
})(BuiltinTool || {});
|
|
1142
|
+
var INDIVIDUAL_TOOLS = /* @__PURE__ */ new Set([
|
|
1143
|
+
"hang_up" /* HANG_UP */,
|
|
1144
|
+
"collect_dtmf" /* COLLECT_DTMF */,
|
|
1145
|
+
"send_dtmf" /* SEND_DTMF */
|
|
1146
|
+
]);
|
|
1147
|
+
function resolveBuiltinTools(value) {
|
|
1148
|
+
if (typeof value === "string") {
|
|
1149
|
+
if (value === "all" /* ALL */) {
|
|
1150
|
+
return new Set(INDIVIDUAL_TOOLS);
|
|
1151
|
+
}
|
|
1152
|
+
if (value === "none" /* NONE */) {
|
|
1153
|
+
return /* @__PURE__ */ new Set();
|
|
1154
|
+
}
|
|
1155
|
+
return /* @__PURE__ */ new Set([value]);
|
|
1156
|
+
}
|
|
1157
|
+
return new Set(value.filter((t) => INDIVIDUAL_TOOLS.has(t)));
|
|
1158
|
+
}
|
|
1159
|
+
|
|
936
1160
|
// src/agent/tool.ts
|
|
937
1161
|
function functionTool(fn) {
|
|
938
1162
|
return fn;
|
|
@@ -1141,6 +1365,15 @@ var ClawOpsAgent = class {
|
|
|
1141
1365
|
_recording;
|
|
1142
1366
|
_recordingPath;
|
|
1143
1367
|
_activeSessions = /* @__PURE__ */ new Map();
|
|
1368
|
+
_builtinTools;
|
|
1369
|
+
_passiveDtmfDebounceMs;
|
|
1370
|
+
_passiveDtmfBuffer = [];
|
|
1371
|
+
_passiveDtmfTimer = null;
|
|
1372
|
+
_passiveDtmfCallId = null;
|
|
1373
|
+
_callSessions = /* @__PURE__ */ new Map();
|
|
1374
|
+
_log;
|
|
1375
|
+
_pipelineLog;
|
|
1376
|
+
_isPipelineSession = false;
|
|
1144
1377
|
constructor(options) {
|
|
1145
1378
|
this._apiKey = options.apiKey ?? process.env["CLAWOPS_API_KEY"] ?? "";
|
|
1146
1379
|
this._accountId = options.accountId ?? process.env["CLAWOPS_ACCOUNT_ID"] ?? "";
|
|
@@ -1150,9 +1383,14 @@ var ClawOpsAgent = class {
|
|
|
1150
1383
|
this._recording = options.recording ?? false;
|
|
1151
1384
|
this._recordingPath = options.recordingPath ?? "./recordings";
|
|
1152
1385
|
this._mcpServers = options.mcpServers ?? [];
|
|
1386
|
+
this._builtinTools = resolveBuiltinTools(options.builtinTools ?? "all" /* ALL */);
|
|
1387
|
+
this._passiveDtmfDebounceMs = options.passiveDtmfDebounceMs ?? 500;
|
|
1153
1388
|
if (options.tracing) {
|
|
1154
1389
|
setTracingConfig(options.tracing);
|
|
1155
1390
|
}
|
|
1391
|
+
this._log = createAgentLogger(options.logger);
|
|
1392
|
+
this._pipelineLog = createPipelineLogger(this._log);
|
|
1393
|
+
this._isPipelineSession = "_stt" in this._session && "_llm" in this._session;
|
|
1156
1394
|
}
|
|
1157
1395
|
/**
|
|
1158
1396
|
* Register a function tool.
|
|
@@ -1164,7 +1402,9 @@ var ClawOpsAgent = class {
|
|
|
1164
1402
|
tool(nameOrTool, description, parameters, handler) {
|
|
1165
1403
|
if (typeof nameOrTool === "string") {
|
|
1166
1404
|
if (!description || !parameters || !handler) {
|
|
1167
|
-
throw new AgentError(
|
|
1405
|
+
throw new AgentError(
|
|
1406
|
+
"tool(name, description, parameters, handler) requires all arguments."
|
|
1407
|
+
);
|
|
1168
1408
|
}
|
|
1169
1409
|
this._tools.register({
|
|
1170
1410
|
name: nameOrTool,
|
|
@@ -1200,7 +1440,9 @@ var ClawOpsAgent = class {
|
|
|
1200
1440
|
throw new AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
|
|
1201
1441
|
}
|
|
1202
1442
|
if (!this._accountId) {
|
|
1203
|
-
throw new AgentError(
|
|
1443
|
+
throw new AgentError(
|
|
1444
|
+
"Account ID is required. Set CLAWOPS_ACCOUNT_ID or pass accountId option."
|
|
1445
|
+
);
|
|
1204
1446
|
}
|
|
1205
1447
|
this._controlWs = new ControlWebSocket({
|
|
1206
1448
|
baseUrl: this._baseUrl,
|
|
@@ -1208,6 +1450,7 @@ var ClawOpsAgent = class {
|
|
|
1208
1450
|
accountId: this._accountId,
|
|
1209
1451
|
number: this._fromNumber
|
|
1210
1452
|
});
|
|
1453
|
+
this._controlWs.setLogger(this._log);
|
|
1211
1454
|
this._controlWs.on("call.incoming", (event) => this._handleIncoming(event));
|
|
1212
1455
|
this._controlWs.on("call.ended", (event) => this._handleEnded(event));
|
|
1213
1456
|
this._controlWs.on("call.outbound_ready", (event) => this._handleOutboundReady(event));
|
|
@@ -1221,7 +1464,7 @@ var ClawOpsAgent = class {
|
|
|
1221
1464
|
`Failed to connect to ClawOps: ${err instanceof Error ? err.message : String(err)}`
|
|
1222
1465
|
);
|
|
1223
1466
|
}
|
|
1224
|
-
|
|
1467
|
+
this._log.info("ClawOpsAgent connected on %s", this._fromNumber);
|
|
1225
1468
|
}
|
|
1226
1469
|
/**
|
|
1227
1470
|
* Connect and block until disconnected.
|
|
@@ -1247,7 +1490,8 @@ var ClawOpsAgent = class {
|
|
|
1247
1490
|
session._markEnded();
|
|
1248
1491
|
}
|
|
1249
1492
|
this._activeSessions.clear();
|
|
1250
|
-
|
|
1493
|
+
this._callSessions.clear();
|
|
1494
|
+
this._log.info("ClawOpsAgent disconnected");
|
|
1251
1495
|
}
|
|
1252
1496
|
/**
|
|
1253
1497
|
* Initiate an outbound call.
|
|
@@ -1282,8 +1526,9 @@ var ClawOpsAgent = class {
|
|
|
1282
1526
|
callSession.on(evt, handler);
|
|
1283
1527
|
}
|
|
1284
1528
|
}
|
|
1529
|
+
callSession.setLogger(this._log);
|
|
1285
1530
|
this._activeSessions.set(callSession.callId, callSession);
|
|
1286
|
-
|
|
1531
|
+
this._log.info("Outbound call initiated: %s -> %s (%s)", this._fromNumber, to, callSession.callId);
|
|
1287
1532
|
return callSession;
|
|
1288
1533
|
}
|
|
1289
1534
|
_handleIncoming(event) {
|
|
@@ -1302,13 +1547,15 @@ var ClawOpsAgent = class {
|
|
|
1302
1547
|
session.on(evt, handler);
|
|
1303
1548
|
}
|
|
1304
1549
|
}
|
|
1550
|
+
session.setLogger(this._log);
|
|
1305
1551
|
this._activeSessions.set(callId, session);
|
|
1552
|
+
this._log.info("Incoming call: %s -> %s (%s)", fromNumber, this._fromNumber, callId);
|
|
1306
1553
|
if (this._controlWs) {
|
|
1307
1554
|
this._controlWs.send({ event: "call.accept", callId });
|
|
1308
1555
|
}
|
|
1309
1556
|
if (mediaUrl) {
|
|
1310
1557
|
this._startCallSession(session, mediaUrl).catch((err) => {
|
|
1311
|
-
|
|
1558
|
+
this._log.error({ err }, "Call session error: %s", callId);
|
|
1312
1559
|
});
|
|
1313
1560
|
}
|
|
1314
1561
|
}
|
|
@@ -1316,6 +1563,7 @@ var ClawOpsAgent = class {
|
|
|
1316
1563
|
const callId = event["callId"];
|
|
1317
1564
|
const session = this._activeSessions.get(callId);
|
|
1318
1565
|
if (session) {
|
|
1566
|
+
this._log.info("Call ended (server): %s", callId);
|
|
1319
1567
|
session._markEnded();
|
|
1320
1568
|
this._activeSessions.delete(callId);
|
|
1321
1569
|
}
|
|
@@ -1332,6 +1580,7 @@ var ClawOpsAgent = class {
|
|
|
1332
1580
|
accountId: this._accountId,
|
|
1333
1581
|
direction: "outbound"
|
|
1334
1582
|
});
|
|
1583
|
+
session.setLogger(this._log);
|
|
1335
1584
|
for (const [evt, handlers] of this._handlers) {
|
|
1336
1585
|
for (const handler of handlers) {
|
|
1337
1586
|
session.on(evt, handler);
|
|
@@ -1340,8 +1589,9 @@ var ClawOpsAgent = class {
|
|
|
1340
1589
|
this._activeSessions.set(callId, session);
|
|
1341
1590
|
}
|
|
1342
1591
|
if (mediaUrl) {
|
|
1592
|
+
this._log.info("Outbound call answered: %s -> %s (%s)", this._fromNumber, session.toNumber, callId);
|
|
1343
1593
|
this._startCallSession(session, mediaUrl).catch((err) => {
|
|
1344
|
-
|
|
1594
|
+
this._log.error({ err }, "Call session error: %s", callId);
|
|
1345
1595
|
});
|
|
1346
1596
|
}
|
|
1347
1597
|
}
|
|
@@ -1349,18 +1599,43 @@ var ClawOpsAgent = class {
|
|
|
1349
1599
|
const callId = event["callId"];
|
|
1350
1600
|
const session = this._activeSessions.get(callId);
|
|
1351
1601
|
if (session) {
|
|
1352
|
-
|
|
1602
|
+
this._log.info("Outbound call ringing: %s", callId);
|
|
1353
1603
|
}
|
|
1354
1604
|
}
|
|
1355
1605
|
_handleFailed(event) {
|
|
1356
1606
|
const callId = event["callId"];
|
|
1357
1607
|
const session = this._activeSessions.get(callId);
|
|
1358
1608
|
if (session) {
|
|
1609
|
+
this._log.info("Outbound call failed: %s (%s)", callId, event["reason"] ?? "failed");
|
|
1359
1610
|
session._emit("call_failed", event["reason"] ?? "failed");
|
|
1360
1611
|
session._markEnded();
|
|
1361
1612
|
this._activeSessions.delete(callId);
|
|
1362
1613
|
}
|
|
1363
1614
|
}
|
|
1615
|
+
_onDtmfEvent(callSession, digit) {
|
|
1616
|
+
callSession._emit("dtmf", digit);
|
|
1617
|
+
callSession._routeDtmf(digit);
|
|
1618
|
+
if (callSession._dtmfCollectorActive) {
|
|
1619
|
+
callSession.clearAudio();
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
this._passiveDtmfBuffer.push(digit);
|
|
1623
|
+
this._passiveDtmfCallId = callSession.callId;
|
|
1624
|
+
if (this._passiveDtmfTimer) {
|
|
1625
|
+
clearTimeout(this._passiveDtmfTimer);
|
|
1626
|
+
}
|
|
1627
|
+
this._passiveDtmfTimer = setTimeout(() => {
|
|
1628
|
+
const digits = this._passiveDtmfBuffer.join("");
|
|
1629
|
+
this._passiveDtmfBuffer = [];
|
|
1630
|
+
const sessionHandler = this._passiveDtmfCallId ? this._callSessions.get(this._passiveDtmfCallId) : null;
|
|
1631
|
+
this._passiveDtmfCallId = null;
|
|
1632
|
+
if (digits && sessionHandler && sessionHandler.feedDtmf) {
|
|
1633
|
+
sessionHandler.feedDtmf(digits).catch((err) => {
|
|
1634
|
+
this._log.error({ err }, "DTMF feed error");
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
}, this._passiveDtmfDebounceMs);
|
|
1638
|
+
}
|
|
1364
1639
|
async _startCallSession(session, mediaWsUrl) {
|
|
1365
1640
|
await withSpan(
|
|
1366
1641
|
"clawops.call_session",
|
|
@@ -1375,22 +1650,25 @@ var ClawOpsAgent = class {
|
|
|
1375
1650
|
if (this._mcpServers.length > 0) {
|
|
1376
1651
|
for (const serverConfig of this._mcpServers) {
|
|
1377
1652
|
const client = new MCPClient();
|
|
1653
|
+
client.setLogger(this._log);
|
|
1378
1654
|
client.addServer("mcp", serverConfig);
|
|
1379
1655
|
try {
|
|
1380
1656
|
const tools = await client.connect();
|
|
1381
1657
|
sessionTools.registerMcpTools(tools);
|
|
1382
1658
|
mcpClients.push(client);
|
|
1383
1659
|
} catch (err) {
|
|
1384
|
-
|
|
1660
|
+
this._log.error({ err }, "MCP connection error");
|
|
1385
1661
|
}
|
|
1386
1662
|
}
|
|
1387
1663
|
}
|
|
1388
1664
|
let recorder = null;
|
|
1389
1665
|
if (this._recording) {
|
|
1390
1666
|
recorder = new AudioRecorder(this._recordingPath, session.callId);
|
|
1667
|
+
recorder.setLogger(this._log);
|
|
1391
1668
|
recorder.start();
|
|
1392
1669
|
}
|
|
1393
1670
|
const mediaWs = new MediaWebSocket();
|
|
1671
|
+
mediaWs.setLogger(this._log);
|
|
1394
1672
|
session._bindTransport(
|
|
1395
1673
|
(audio) => {
|
|
1396
1674
|
mediaWs.sendAudio(audio.toString("base64"));
|
|
@@ -1398,9 +1676,17 @@ var ClawOpsAgent = class {
|
|
|
1398
1676
|
() => {
|
|
1399
1677
|
mediaWs.sendClear();
|
|
1400
1678
|
},
|
|
1401
|
-
() => {
|
|
1679
|
+
async () => {
|
|
1680
|
+
await mediaWs.flush();
|
|
1681
|
+
const markName = `hangup-${Date.now()}`;
|
|
1682
|
+
mediaWs.sendMark(markName);
|
|
1683
|
+
await mediaWs.waitForMark(markName, 5e3);
|
|
1402
1684
|
mediaWs.close();
|
|
1403
|
-
}
|
|
1685
|
+
},
|
|
1686
|
+
async (digit) => {
|
|
1687
|
+
mediaWs.sendDtmf(digit);
|
|
1688
|
+
},
|
|
1689
|
+
() => mediaWs.isConnected
|
|
1404
1690
|
);
|
|
1405
1691
|
const sessionHandler = this._session;
|
|
1406
1692
|
if ("setToolRegistry" in sessionHandler && typeof sessionHandler.setToolRegistry === "function") {
|
|
@@ -1409,6 +1695,13 @@ var ClawOpsAgent = class {
|
|
|
1409
1695
|
if (recorder && "setRecorder" in sessionHandler && typeof sessionHandler.setRecorder === "function") {
|
|
1410
1696
|
sessionHandler.setRecorder(recorder);
|
|
1411
1697
|
}
|
|
1698
|
+
if ("setBuiltinTools" in sessionHandler && typeof sessionHandler.setBuiltinTools === "function") {
|
|
1699
|
+
sessionHandler.setBuiltinTools(this._builtinTools);
|
|
1700
|
+
}
|
|
1701
|
+
if ("setLogger" in sessionHandler && typeof sessionHandler.setLogger === "function") {
|
|
1702
|
+
sessionHandler.setLogger(this._isPipelineSession ? this._pipelineLog : this._log);
|
|
1703
|
+
}
|
|
1704
|
+
this._callSessions.set(session.callId, sessionHandler);
|
|
1412
1705
|
mediaWs.onAudio((ulawAudio, _timestamp) => {
|
|
1413
1706
|
if (sessionHandler) {
|
|
1414
1707
|
sessionHandler.feedAudio(ulawAudio);
|
|
@@ -1417,7 +1710,11 @@ var ClawOpsAgent = class {
|
|
|
1417
1710
|
recorder.writeInbound(ulawToPcm16(ulawAudio));
|
|
1418
1711
|
}
|
|
1419
1712
|
});
|
|
1713
|
+
mediaWs.onDtmf((digit) => {
|
|
1714
|
+
this._onDtmfEvent(session, digit);
|
|
1715
|
+
});
|
|
1420
1716
|
mediaWs.onClose(() => {
|
|
1717
|
+
this._log.info("Media stream stopped: %s", session.callId);
|
|
1421
1718
|
if (recorder) {
|
|
1422
1719
|
recorder.stop();
|
|
1423
1720
|
}
|
|
@@ -1426,11 +1723,12 @@ var ClawOpsAgent = class {
|
|
|
1426
1723
|
session._emit("call_start");
|
|
1427
1724
|
try {
|
|
1428
1725
|
await mediaWs.connect(mediaWsUrl, this._apiKey);
|
|
1726
|
+
this._log.info("Media stream started: %s", session.callId);
|
|
1429
1727
|
await sessionHandler.start(session, sessionTools);
|
|
1430
1728
|
await session.wait();
|
|
1431
1729
|
await sessionHandler.stop();
|
|
1432
1730
|
} catch (err) {
|
|
1433
|
-
|
|
1731
|
+
this._log.error({ err }, "Call session error: %s", session.callId);
|
|
1434
1732
|
} finally {
|
|
1435
1733
|
if (mcpClients.length > 0) {
|
|
1436
1734
|
sessionTools.clearMcpTools();
|
|
@@ -1445,6 +1743,7 @@ var ClawOpsAgent = class {
|
|
|
1445
1743
|
session._emit("call_end");
|
|
1446
1744
|
session._markEnded();
|
|
1447
1745
|
this._activeSessions.delete(session.callId);
|
|
1746
|
+
this._callSessions.delete(session.callId);
|
|
1448
1747
|
}
|
|
1449
1748
|
}
|
|
1450
1749
|
);
|
|
@@ -1457,7 +1756,40 @@ var HANG_UP_TOOL = {
|
|
|
1457
1756
|
type: "function",
|
|
1458
1757
|
name: "hang_up",
|
|
1459
1758
|
description: "End the phone call. Use when the conversation is finished or the caller says goodbye.",
|
|
1460
|
-
parameters: {
|
|
1759
|
+
parameters: {
|
|
1760
|
+
type: "object",
|
|
1761
|
+
properties: {},
|
|
1762
|
+
required: []
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
var COLLECT_DTMF_TOOL = {
|
|
1766
|
+
type: "function",
|
|
1767
|
+
name: "collect_dtmf",
|
|
1768
|
+
description: "\uC0AC\uC6A9\uC790\uB85C\uBD80\uD130 DTMF(\uC804\uD654 \uD0A4\uD328\uB4DC) \uC785\uB825\uC744 \uC218\uC9D1\uD569\uB2C8\uB2E4. \uBC18\uB4DC\uC2DC \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB34\uC5C7\uC744 \uC785\uB825\uD574\uC57C \uD558\uB294\uC9C0 \uC548\uB0B4\uD55C \uD6C4 \uD638\uCD9C\uD558\uC138\uC694.",
|
|
1769
|
+
parameters: {
|
|
1770
|
+
type: "object",
|
|
1771
|
+
properties: {
|
|
1772
|
+
max_digits: { type: "integer", description: "\uC218\uC9D1\uD560 \uCD5C\uB300 \uC790\uB9BF\uC218" },
|
|
1773
|
+
finish_on_key: { type: "string", description: "\uC785\uB825 \uC885\uB8CC \uD0A4 (\uAE30\uBCF8: #)" },
|
|
1774
|
+
timeout: { type: "integer", description: "\uC785\uB825 \uB300\uAE30 \uC2DC\uAC04(\uCD08, \uAE30\uBCF8: 5)" }
|
|
1775
|
+
},
|
|
1776
|
+
required: ["max_digits"]
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1779
|
+
var SEND_DTMF_TOOL = {
|
|
1780
|
+
type: "function",
|
|
1781
|
+
name: "send_dtmf",
|
|
1782
|
+
description: "DTMF \uC2E0\uD638\uB97C \uC804\uC1A1\uD569\uB2C8\uB2E4. ARS \uBA54\uB274 \uD0D0\uC0C9\uC774\uB098 \uB0B4\uC120\uBC88\uD638 \uC785\uB825 \uC2DC \uC0AC\uC6A9\uD569\uB2C8\uB2E4.",
|
|
1783
|
+
parameters: {
|
|
1784
|
+
type: "object",
|
|
1785
|
+
properties: {
|
|
1786
|
+
digits: {
|
|
1787
|
+
type: "string",
|
|
1788
|
+
description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30."
|
|
1789
|
+
}
|
|
1790
|
+
},
|
|
1791
|
+
required: ["digits"]
|
|
1792
|
+
}
|
|
1461
1793
|
};
|
|
1462
1794
|
var OpenAIRealtime = class {
|
|
1463
1795
|
_apiKey;
|
|
@@ -1467,6 +1799,14 @@ var OpenAIRealtime = class {
|
|
|
1467
1799
|
_language;
|
|
1468
1800
|
_eagerness;
|
|
1469
1801
|
_greeting;
|
|
1802
|
+
_log = NOOP_LOGGER;
|
|
1803
|
+
_builtinTools = null;
|
|
1804
|
+
setLogger(logger) {
|
|
1805
|
+
this._log = logger;
|
|
1806
|
+
}
|
|
1807
|
+
setBuiltinTools(tools) {
|
|
1808
|
+
this._builtinTools = tools;
|
|
1809
|
+
}
|
|
1470
1810
|
_ws = null;
|
|
1471
1811
|
_call = null;
|
|
1472
1812
|
_tools = null;
|
|
@@ -1477,6 +1817,9 @@ var OpenAIRealtime = class {
|
|
|
1477
1817
|
_responseStartTs = null;
|
|
1478
1818
|
_sentAudioChunks = 0;
|
|
1479
1819
|
_audioRemainder = Buffer.alloc(0);
|
|
1820
|
+
// Response state tracking — prevent sending response.create while one is active
|
|
1821
|
+
_responseInProgress = false;
|
|
1822
|
+
_onResponseDone = null;
|
|
1480
1823
|
constructor(options = {}) {
|
|
1481
1824
|
this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
|
|
1482
1825
|
this._systemPrompt = options.systemPrompt ?? "";
|
|
@@ -1517,6 +1860,7 @@ var OpenAIRealtime = class {
|
|
|
1517
1860
|
const ws = this._ws;
|
|
1518
1861
|
ws.on("open", () => {
|
|
1519
1862
|
this._sendSessionUpdate();
|
|
1863
|
+
this._log.info("OpenAI Realtime connected");
|
|
1520
1864
|
if (this._greeting) {
|
|
1521
1865
|
this._send({ type: "response.create" });
|
|
1522
1866
|
}
|
|
@@ -1536,10 +1880,22 @@ var OpenAIRealtime = class {
|
|
|
1536
1880
|
if (!this._ws) {
|
|
1537
1881
|
reject(err);
|
|
1538
1882
|
}
|
|
1539
|
-
|
|
1883
|
+
this._log.error({ err }, "OpenAI Realtime WS error");
|
|
1540
1884
|
});
|
|
1541
1885
|
});
|
|
1542
1886
|
}
|
|
1887
|
+
async feedDtmf(digits) {
|
|
1888
|
+
await this._waitForResponseDone();
|
|
1889
|
+
this._send({
|
|
1890
|
+
type: "conversation.item.create",
|
|
1891
|
+
item: {
|
|
1892
|
+
type: "message",
|
|
1893
|
+
role: "user",
|
|
1894
|
+
content: [{ type: "input_text", text: `[DTMF \uC785\uB825: ${digits}]` }]
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
this._send({ type: "response.create" });
|
|
1898
|
+
}
|
|
1543
1899
|
feedAudio(audio) {
|
|
1544
1900
|
if (this._ws && this._ws.readyState === 1 && !this._closed) {
|
|
1545
1901
|
this._send({
|
|
@@ -1558,7 +1914,12 @@ var OpenAIRealtime = class {
|
|
|
1558
1914
|
_sendSessionUpdate() {
|
|
1559
1915
|
if (!this._ws || this._ws.readyState !== 1) return;
|
|
1560
1916
|
const toolSchemas = this._tools ? this._tools.toOpenAITools().map((t) => ({ type: "function", ...t.function })) : [];
|
|
1561
|
-
|
|
1917
|
+
if (!this._builtinTools || this._builtinTools.has("hang_up" /* HANG_UP */))
|
|
1918
|
+
toolSchemas.push(HANG_UP_TOOL);
|
|
1919
|
+
if (!this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */))
|
|
1920
|
+
toolSchemas.push(COLLECT_DTMF_TOOL);
|
|
1921
|
+
if (!this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */))
|
|
1922
|
+
toolSchemas.push(SEND_DTMF_TOOL);
|
|
1562
1923
|
this._send({
|
|
1563
1924
|
type: "session.update",
|
|
1564
1925
|
session: {
|
|
@@ -1568,7 +1929,7 @@ var OpenAIRealtime = class {
|
|
|
1568
1929
|
input_audio_format: "g711_ulaw",
|
|
1569
1930
|
output_audio_format: "g711_ulaw",
|
|
1570
1931
|
input_audio_transcription: {
|
|
1571
|
-
model: "
|
|
1932
|
+
model: "whisper-1",
|
|
1572
1933
|
language: this._language
|
|
1573
1934
|
},
|
|
1574
1935
|
input_audio_noise_reduction: { type: "far_field" },
|
|
@@ -1625,8 +1986,21 @@ var OpenAIRealtime = class {
|
|
|
1625
1986
|
}
|
|
1626
1987
|
break;
|
|
1627
1988
|
}
|
|
1989
|
+
case "response.created": {
|
|
1990
|
+
this._responseInProgress = true;
|
|
1991
|
+
break;
|
|
1992
|
+
}
|
|
1993
|
+
case "response.done": {
|
|
1994
|
+
this._responseInProgress = false;
|
|
1995
|
+
if (this._onResponseDone) {
|
|
1996
|
+
const cb = this._onResponseDone;
|
|
1997
|
+
this._onResponseDone = null;
|
|
1998
|
+
cb();
|
|
1999
|
+
}
|
|
2000
|
+
break;
|
|
2001
|
+
}
|
|
1628
2002
|
case "error": {
|
|
1629
|
-
|
|
2003
|
+
this._log.error({ apiError: msg["error"] }, "OpenAI error");
|
|
1630
2004
|
break;
|
|
1631
2005
|
}
|
|
1632
2006
|
}
|
|
@@ -1676,14 +2050,64 @@ var OpenAIRealtime = class {
|
|
|
1676
2050
|
async _handleToolCall(item) {
|
|
1677
2051
|
const funcName = item["name"];
|
|
1678
2052
|
const callId = item["call_id"];
|
|
2053
|
+
this._log.info("Tool call: %s", funcName);
|
|
1679
2054
|
if (funcName === "hang_up") {
|
|
1680
2055
|
if (this._call) {
|
|
1681
|
-
this._call.hangup();
|
|
2056
|
+
await this._call.hangup();
|
|
2057
|
+
}
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
if (funcName === "collect_dtmf") {
|
|
2061
|
+
if (this._call) {
|
|
2062
|
+
let result2;
|
|
2063
|
+
try {
|
|
2064
|
+
const args = JSON.parse(item["arguments"] ?? "{}");
|
|
2065
|
+
result2 = await this._call.collectDtmf({
|
|
2066
|
+
maxDigits: args["max_digits"] ?? 4,
|
|
2067
|
+
finishOnKey: args["finish_on_key"] ?? "#",
|
|
2068
|
+
timeout: args["timeout"] ?? 5
|
|
2069
|
+
});
|
|
2070
|
+
} catch (err) {
|
|
2071
|
+
result2 = `Error: ${err}`;
|
|
2072
|
+
}
|
|
2073
|
+
await this._waitForResponseDone();
|
|
2074
|
+
this._send({
|
|
2075
|
+
type: "conversation.item.create",
|
|
2076
|
+
item: {
|
|
2077
|
+
type: "function_call_output",
|
|
2078
|
+
call_id: callId,
|
|
2079
|
+
output: result2 || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)"
|
|
2080
|
+
}
|
|
2081
|
+
});
|
|
2082
|
+
this._send({ type: "response.create" });
|
|
2083
|
+
}
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
if (funcName === "send_dtmf") {
|
|
2087
|
+
if (this._call) {
|
|
2088
|
+
let result2;
|
|
2089
|
+
try {
|
|
2090
|
+
const args = JSON.parse(item["arguments"] ?? "{}");
|
|
2091
|
+
await this._call.sendDtmfSequence(args["digits"] ?? "");
|
|
2092
|
+
result2 = "sent";
|
|
2093
|
+
} catch (err) {
|
|
2094
|
+
result2 = `Error: ${err}`;
|
|
2095
|
+
}
|
|
2096
|
+
await this._waitForResponseDone();
|
|
2097
|
+
this._send({
|
|
2098
|
+
type: "conversation.item.create",
|
|
2099
|
+
item: {
|
|
2100
|
+
type: "function_call_output",
|
|
2101
|
+
call_id: callId,
|
|
2102
|
+
output: result2
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
this._send({ type: "response.create" });
|
|
1682
2106
|
}
|
|
1683
2107
|
return;
|
|
1684
2108
|
}
|
|
1685
2109
|
if (!this._tools || !this._tools.has(funcName)) {
|
|
1686
|
-
|
|
2110
|
+
this._log.error("Unknown tool: %s", funcName);
|
|
1687
2111
|
return;
|
|
1688
2112
|
}
|
|
1689
2113
|
let result;
|
|
@@ -1691,9 +2115,10 @@ var OpenAIRealtime = class {
|
|
|
1691
2115
|
const args = JSON.parse(item["arguments"] ?? "{}");
|
|
1692
2116
|
result = await this._tools.call(funcName, args);
|
|
1693
2117
|
} catch (err) {
|
|
1694
|
-
|
|
2118
|
+
this._log.error({ err }, "Tool call failed: %s", funcName);
|
|
1695
2119
|
result = `Error: ${err}`;
|
|
1696
2120
|
}
|
|
2121
|
+
await this._waitForResponseDone();
|
|
1697
2122
|
this._send({
|
|
1698
2123
|
type: "conversation.item.create",
|
|
1699
2124
|
item: {
|
|
@@ -1704,6 +2129,12 @@ var OpenAIRealtime = class {
|
|
|
1704
2129
|
});
|
|
1705
2130
|
this._send({ type: "response.create" });
|
|
1706
2131
|
}
|
|
2132
|
+
_waitForResponseDone() {
|
|
2133
|
+
if (!this._responseInProgress) return Promise.resolve();
|
|
2134
|
+
return new Promise((resolve) => {
|
|
2135
|
+
this._onResponseDone = resolve;
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
1707
2138
|
_send(data) {
|
|
1708
2139
|
if (this._ws && this._ws.readyState === 1 && !this._closed) {
|
|
1709
2140
|
this._ws.send(JSON.stringify(data));
|
|
@@ -1717,6 +2148,33 @@ var HANG_UP_TOOL2 = {
|
|
|
1717
2148
|
description: "End the phone call. Use when the conversation is finished or the caller says goodbye.",
|
|
1718
2149
|
parameters: { type: "object", properties: {} }
|
|
1719
2150
|
};
|
|
2151
|
+
var COLLECT_DTMF_TOOL2 = {
|
|
2152
|
+
name: "collect_dtmf",
|
|
2153
|
+
description: "\uC0AC\uC6A9\uC790\uB85C\uBD80\uD130 DTMF(\uC804\uD654 \uD0A4\uD328\uB4DC) \uC785\uB825\uC744 \uC218\uC9D1\uD569\uB2C8\uB2E4. \uBC18\uB4DC\uC2DC \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB34\uC5C7\uC744 \uC785\uB825\uD574\uC57C \uD558\uB294\uC9C0 \uC548\uB0B4\uD55C \uD6C4 \uD638\uCD9C\uD558\uC138\uC694.",
|
|
2154
|
+
parameters: {
|
|
2155
|
+
type: "object",
|
|
2156
|
+
properties: {
|
|
2157
|
+
max_digits: { type: "integer", description: "\uC218\uC9D1\uD560 \uCD5C\uB300 \uC790\uB9BF\uC218" },
|
|
2158
|
+
finish_on_key: { type: "string", description: "\uC785\uB825 \uC885\uB8CC \uD0A4 (\uAE30\uBCF8: #)" },
|
|
2159
|
+
timeout: { type: "integer", description: "\uC785\uB825 \uB300\uAE30 \uC2DC\uAC04(\uCD08, \uAE30\uBCF8: 5)" }
|
|
2160
|
+
},
|
|
2161
|
+
required: ["max_digits"]
|
|
2162
|
+
}
|
|
2163
|
+
};
|
|
2164
|
+
var SEND_DTMF_TOOL2 = {
|
|
2165
|
+
name: "send_dtmf",
|
|
2166
|
+
description: "DTMF \uC2E0\uD638\uB97C \uC804\uC1A1\uD569\uB2C8\uB2E4. ARS \uBA54\uB274 \uD0D0\uC0C9\uC774\uB098 \uB0B4\uC120\uBC88\uD638 \uC785\uB825 \uC2DC \uC0AC\uC6A9\uD569\uB2C8\uB2E4.",
|
|
2167
|
+
parameters: {
|
|
2168
|
+
type: "object",
|
|
2169
|
+
properties: {
|
|
2170
|
+
digits: {
|
|
2171
|
+
type: "string",
|
|
2172
|
+
description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30."
|
|
2173
|
+
}
|
|
2174
|
+
},
|
|
2175
|
+
required: ["digits"]
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
1720
2178
|
function resolveRef(ref, defs) {
|
|
1721
2179
|
const parts = ref.replace(/^#\//, "").split("/");
|
|
1722
2180
|
let result = defs;
|
|
@@ -1779,7 +2237,11 @@ function sanitizeSchemaForGemini(schema, defs, depth = 0) {
|
|
|
1779
2237
|
result["properties"] = props;
|
|
1780
2238
|
}
|
|
1781
2239
|
if (schema["items"] && typeof schema["items"] === "object" && !Array.isArray(schema["items"])) {
|
|
1782
|
-
result["items"] = sanitizeSchemaForGemini(
|
|
2240
|
+
result["items"] = sanitizeSchemaForGemini(
|
|
2241
|
+
schema["items"],
|
|
2242
|
+
defs,
|
|
2243
|
+
depth + 1
|
|
2244
|
+
);
|
|
1783
2245
|
}
|
|
1784
2246
|
if (!result["type"] && result["properties"]) result["type"] = "object";
|
|
1785
2247
|
if (result["type"] === "object" && !result["properties"]) result["properties"] = {};
|
|
@@ -1800,6 +2262,9 @@ var GeminiRealtime = class {
|
|
|
1800
2262
|
_closed = false;
|
|
1801
2263
|
_sentAudioChunks = 0;
|
|
1802
2264
|
_audioRemainder = Buffer.alloc(0);
|
|
2265
|
+
_builtinTools = null;
|
|
2266
|
+
_toolCallInProgress = false;
|
|
2267
|
+
_log = NOOP_LOGGER;
|
|
1803
2268
|
constructor(options = {}) {
|
|
1804
2269
|
this._apiKey = options.apiKey ?? process.env["GOOGLE_API_KEY"] ?? "";
|
|
1805
2270
|
this._systemPrompt = options.systemPrompt ?? "";
|
|
@@ -1816,6 +2281,12 @@ var GeminiRealtime = class {
|
|
|
1816
2281
|
setRecorder(recorder) {
|
|
1817
2282
|
this._recorder = recorder;
|
|
1818
2283
|
}
|
|
2284
|
+
setBuiltinTools(tools) {
|
|
2285
|
+
this._builtinTools = tools;
|
|
2286
|
+
}
|
|
2287
|
+
setLogger(logger) {
|
|
2288
|
+
this._log = logger;
|
|
2289
|
+
}
|
|
1819
2290
|
async start(callSession, tools) {
|
|
1820
2291
|
this._call = callSession;
|
|
1821
2292
|
if (tools) this._tools = tools;
|
|
@@ -1852,9 +2323,10 @@ var GeminiRealtime = class {
|
|
|
1852
2323
|
callbacks: {
|
|
1853
2324
|
onmessage: (msg) => this._handleMessage(msg),
|
|
1854
2325
|
onerror: (err) => {
|
|
1855
|
-
|
|
2326
|
+
this._log.error({ err }, "Gemini SDK error");
|
|
1856
2327
|
},
|
|
1857
|
-
onclose: () => {
|
|
2328
|
+
onclose: (ev) => {
|
|
2329
|
+
this._log.info({ code: ev?.code ?? "unknown" }, "Gemini connection closed");
|
|
1858
2330
|
this._closed = true;
|
|
1859
2331
|
}
|
|
1860
2332
|
}
|
|
@@ -1872,7 +2344,7 @@ var GeminiRealtime = class {
|
|
|
1872
2344
|
}
|
|
1873
2345
|
}
|
|
1874
2346
|
feedAudio(audio) {
|
|
1875
|
-
if (this._session && !this._closed) {
|
|
2347
|
+
if (this._session && !this._closed && !this._toolCallInProgress) {
|
|
1876
2348
|
const pcm8k = ulawToPcm16(audio);
|
|
1877
2349
|
if (this._recorder) {
|
|
1878
2350
|
this._recorder.writeInbound(pcm8k);
|
|
@@ -1886,6 +2358,14 @@ var GeminiRealtime = class {
|
|
|
1886
2358
|
});
|
|
1887
2359
|
}
|
|
1888
2360
|
}
|
|
2361
|
+
async feedDtmf(digits) {
|
|
2362
|
+
if (this._session) {
|
|
2363
|
+
this._session.sendClientContent({
|
|
2364
|
+
turns: [{ role: "user", parts: [{ text: `[DTMF \uC785\uB825: ${digits}]` }] }],
|
|
2365
|
+
turnComplete: true
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
1889
2369
|
async stop() {
|
|
1890
2370
|
this._closed = true;
|
|
1891
2371
|
if (this._session) {
|
|
@@ -1904,7 +2384,9 @@ var GeminiRealtime = class {
|
|
|
1904
2384
|
t.function.parameters ?? { type: "object", properties: {} }
|
|
1905
2385
|
)
|
|
1906
2386
|
})) : [];
|
|
1907
|
-
toolDefs.push(HANG_UP_TOOL2);
|
|
2387
|
+
if (!this._builtinTools || this._builtinTools.has("hang_up" /* HANG_UP */)) toolDefs.push(HANG_UP_TOOL2);
|
|
2388
|
+
if (!this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */)) toolDefs.push(COLLECT_DTMF_TOOL2);
|
|
2389
|
+
if (!this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */)) toolDefs.push(SEND_DTMF_TOOL2);
|
|
1908
2390
|
return toolDefs;
|
|
1909
2391
|
}
|
|
1910
2392
|
_handleMessage(msg) {
|
|
@@ -1924,9 +2406,11 @@ var GeminiRealtime = class {
|
|
|
1924
2406
|
}
|
|
1925
2407
|
}
|
|
1926
2408
|
if (serverContent.turnComplete) {
|
|
2409
|
+
this._log.debug("Turn complete");
|
|
1927
2410
|
this._flushAudioRemainder();
|
|
1928
2411
|
}
|
|
1929
2412
|
if (serverContent.interrupted) {
|
|
2413
|
+
this._log.info("Barge-in detected");
|
|
1930
2414
|
if (this._call) {
|
|
1931
2415
|
this._call.clearAudio();
|
|
1932
2416
|
}
|
|
@@ -1935,16 +2419,22 @@ var GeminiRealtime = class {
|
|
|
1935
2419
|
}
|
|
1936
2420
|
const inputText = serverContent.inputTranscription?.text;
|
|
1937
2421
|
if (inputText && this._call) {
|
|
2422
|
+
this._log.info("User: %s", inputText);
|
|
1938
2423
|
this._call._emit("transcript", "user", inputText);
|
|
1939
2424
|
}
|
|
1940
2425
|
const outputText = serverContent.outputTranscription?.text;
|
|
1941
2426
|
if (outputText && this._call) {
|
|
2427
|
+
this._log.info("Assistant: %s", outputText);
|
|
1942
2428
|
this._call._emit("transcript", "assistant", outputText);
|
|
1943
2429
|
}
|
|
1944
2430
|
}
|
|
1945
2431
|
if (msg.toolCall) {
|
|
1946
2432
|
this._handleToolCall(msg.toolCall);
|
|
1947
2433
|
}
|
|
2434
|
+
const toolCancellation = msg["toolCallCancellation"];
|
|
2435
|
+
if (toolCancellation) {
|
|
2436
|
+
this._log.info({ ids: toolCancellation.ids }, "Tool call cancelled");
|
|
2437
|
+
}
|
|
1948
2438
|
}
|
|
1949
2439
|
_handleAudioData(b64Data) {
|
|
1950
2440
|
if (!this._call) return;
|
|
@@ -1957,9 +2447,9 @@ var GeminiRealtime = class {
|
|
|
1957
2447
|
const combined = Buffer.concat([this._audioRemainder, ulaw]);
|
|
1958
2448
|
const chunkSize = 160;
|
|
1959
2449
|
const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
|
|
1960
|
-
|
|
1961
|
-
this._call.sendAudio(combined.subarray(
|
|
1962
|
-
this._sentAudioChunks
|
|
2450
|
+
if (fullEnd > 0) {
|
|
2451
|
+
this._call.sendAudio(combined.subarray(0, fullEnd));
|
|
2452
|
+
this._sentAudioChunks += fullEnd / chunkSize;
|
|
1963
2453
|
}
|
|
1964
2454
|
this._audioRemainder = combined.subarray(fullEnd);
|
|
1965
2455
|
}
|
|
@@ -1977,31 +2467,75 @@ var GeminiRealtime = class {
|
|
|
1977
2467
|
async _handleToolCall(toolCall) {
|
|
1978
2468
|
const functionCalls = toolCall.functionCalls;
|
|
1979
2469
|
if (!functionCalls) return;
|
|
2470
|
+
this._toolCallInProgress = true;
|
|
1980
2471
|
const responses = [];
|
|
1981
2472
|
for (const fc of functionCalls) {
|
|
1982
2473
|
const name = fc.name ?? "";
|
|
1983
2474
|
const fcId = fc.id ?? "";
|
|
1984
2475
|
const args = fc.args ?? {};
|
|
2476
|
+
this._log.info({ tool: name, args }, "Tool call: %s", name);
|
|
1985
2477
|
if (name === "hang_up") {
|
|
2478
|
+
this._log.info("hang_up: ending call");
|
|
1986
2479
|
if (this._call) {
|
|
1987
|
-
this._call.hangup();
|
|
2480
|
+
await this._call.hangup();
|
|
1988
2481
|
}
|
|
1989
2482
|
return;
|
|
1990
2483
|
}
|
|
2484
|
+
if (name === "collect_dtmf") {
|
|
2485
|
+
if (this._call) {
|
|
2486
|
+
let result;
|
|
2487
|
+
try {
|
|
2488
|
+
this._log.info({ maxDigits: args["max_digits"] ?? 4, timeout: args["timeout"] ?? 5 }, "collect_dtmf: waiting for digits");
|
|
2489
|
+
result = await this._call.collectDtmf({
|
|
2490
|
+
maxDigits: args["max_digits"] ?? 4,
|
|
2491
|
+
finishOnKey: args["finish_on_key"] ?? "#",
|
|
2492
|
+
timeout: args["timeout"] ?? 5
|
|
2493
|
+
});
|
|
2494
|
+
this._log.info("DTMF collected: %s", result || "(empty)");
|
|
2495
|
+
} catch (err) {
|
|
2496
|
+
this._log.error({ err }, "collect_dtmf error");
|
|
2497
|
+
result = `Error: ${err}`;
|
|
2498
|
+
}
|
|
2499
|
+
responses.push({
|
|
2500
|
+
id: fcId,
|
|
2501
|
+
name,
|
|
2502
|
+
response: { result: result || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)" }
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
if (name === "send_dtmf") {
|
|
2508
|
+
if (this._call) {
|
|
2509
|
+
let result;
|
|
2510
|
+
try {
|
|
2511
|
+
this._log.info('send_dtmf: digits="%s"', args["digits"] ?? "");
|
|
2512
|
+
await this._call.sendDtmfSequence(args["digits"] ?? "");
|
|
2513
|
+
result = "sent";
|
|
2514
|
+
this._log.info("send_dtmf: sent");
|
|
2515
|
+
} catch (err) {
|
|
2516
|
+
this._log.error({ err }, "send_dtmf error");
|
|
2517
|
+
result = `Error: ${err}`;
|
|
2518
|
+
}
|
|
2519
|
+
responses.push({ id: fcId, name, response: { result } });
|
|
2520
|
+
}
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
1991
2523
|
if (!this._tools || !this._tools.has(name)) {
|
|
1992
|
-
|
|
2524
|
+
this._log.error("Unknown tool: %s", name);
|
|
1993
2525
|
responses.push({ id: fcId, name, response: { error: `Unknown tool: ${name}` } });
|
|
1994
2526
|
continue;
|
|
1995
2527
|
}
|
|
1996
2528
|
try {
|
|
1997
2529
|
const result = await this._tools.call(name, args);
|
|
2530
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
2531
|
+
this._log.info("Tool result: %s -> %s", name, resultStr.substring(0, 200));
|
|
1998
2532
|
responses.push({
|
|
1999
2533
|
id: fcId,
|
|
2000
2534
|
name,
|
|
2001
|
-
response: { result:
|
|
2535
|
+
response: { result: resultStr }
|
|
2002
2536
|
});
|
|
2003
2537
|
} catch (err) {
|
|
2004
|
-
|
|
2538
|
+
this._log.error({ err }, "Tool call failed: %s", name);
|
|
2005
2539
|
responses.push({
|
|
2006
2540
|
id: fcId,
|
|
2007
2541
|
name,
|
|
@@ -2009,15 +2543,41 @@ var GeminiRealtime = class {
|
|
|
2009
2543
|
});
|
|
2010
2544
|
}
|
|
2011
2545
|
}
|
|
2012
|
-
if (this._session) {
|
|
2546
|
+
if (responses.length > 0 && this._session) {
|
|
2547
|
+
this._log.debug("Sending %d tool response(s)", responses.length);
|
|
2013
2548
|
this._session.sendToolResponse({
|
|
2014
2549
|
functionResponses: responses
|
|
2015
2550
|
});
|
|
2016
2551
|
}
|
|
2552
|
+
this._toolCallInProgress = false;
|
|
2017
2553
|
}
|
|
2018
2554
|
};
|
|
2019
2555
|
|
|
2020
2556
|
// src/agent/pipeline/pipeline-session.ts
|
|
2557
|
+
var COLLECT_DTMF_TOOL3 = {
|
|
2558
|
+
function: {
|
|
2559
|
+
description: "\uC0AC\uC6A9\uC790\uB85C\uBD80\uD130 DTMF(\uC804\uD654 \uD0A4\uD328\uB4DC) \uC785\uB825\uC744 \uC218\uC9D1\uD569\uB2C8\uB2E4.",
|
|
2560
|
+
parameters: {
|
|
2561
|
+
properties: {
|
|
2562
|
+
max_digits: { type: "integer", description: "\uC218\uC9D1\uD560 \uCD5C\uB300 \uC790\uB9BF\uC218" },
|
|
2563
|
+
finish_on_key: { type: "string", description: "\uC785\uB825 \uC885\uB8CC \uD0A4 (\uAE30\uBCF8: #)" },
|
|
2564
|
+
timeout: { type: "integer", description: "\uC785\uB825 \uB300\uAE30 \uC2DC\uAC04(\uCD08, \uAE30\uBCF8: 5)" }
|
|
2565
|
+
},
|
|
2566
|
+
required: ["max_digits"]
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
};
|
|
2570
|
+
var SEND_DTMF_TOOL3 = {
|
|
2571
|
+
function: {
|
|
2572
|
+
description: "DTMF \uC2E0\uD638\uB97C \uC804\uC1A1\uD569\uB2C8\uB2E4. ARS \uBA54\uB274 \uD0D0\uC0C9\uC774\uB098 \uB0B4\uC120\uBC88\uD638 \uC785\uB825 \uC2DC \uC0AC\uC6A9\uD569\uB2C8\uB2E4.",
|
|
2573
|
+
parameters: {
|
|
2574
|
+
properties: {
|
|
2575
|
+
digits: { type: "string", description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30." }
|
|
2576
|
+
},
|
|
2577
|
+
required: ["digits"]
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
};
|
|
2021
2581
|
var PipelineSession = class {
|
|
2022
2582
|
_stt;
|
|
2023
2583
|
_llm;
|
|
@@ -2036,6 +2596,8 @@ var PipelineSession = class {
|
|
|
2036
2596
|
_audioBuffer = [];
|
|
2037
2597
|
_running = false;
|
|
2038
2598
|
_speaking = false;
|
|
2599
|
+
_builtinTools = null;
|
|
2600
|
+
_log = NOOP_LOGGER;
|
|
2039
2601
|
constructor(options) {
|
|
2040
2602
|
this._stt = options.stt;
|
|
2041
2603
|
this._llm = options.llm;
|
|
@@ -2056,10 +2618,23 @@ var PipelineSession = class {
|
|
|
2056
2618
|
setRecorder(recorder) {
|
|
2057
2619
|
this._recorder = recorder;
|
|
2058
2620
|
}
|
|
2621
|
+
setBuiltinTools(tools) {
|
|
2622
|
+
this._builtinTools = tools;
|
|
2623
|
+
}
|
|
2624
|
+
setLogger(logger) {
|
|
2625
|
+
this._log = logger;
|
|
2626
|
+
if ("setLogger" in this._stt && typeof this._stt.setLogger === "function") {
|
|
2627
|
+
this._stt.setLogger(logger);
|
|
2628
|
+
}
|
|
2629
|
+
if ("setLogger" in this._tts && typeof this._tts.setLogger === "function") {
|
|
2630
|
+
this._tts.setLogger(logger);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2059
2633
|
async start(callSession, tools) {
|
|
2060
2634
|
this._callSession = callSession;
|
|
2061
2635
|
this._tools = tools ?? null;
|
|
2062
2636
|
this._running = true;
|
|
2637
|
+
this._log.info("PipelineSession started");
|
|
2063
2638
|
this._conversation = [];
|
|
2064
2639
|
if (this._systemPrompt) {
|
|
2065
2640
|
this._conversation.push({
|
|
@@ -2069,11 +2644,11 @@ var PipelineSession = class {
|
|
|
2069
2644
|
}
|
|
2070
2645
|
if (this._greeting) {
|
|
2071
2646
|
this._generateGreeting().catch((err) => {
|
|
2072
|
-
|
|
2647
|
+
this._log.error({ err }, "Greeting error");
|
|
2073
2648
|
});
|
|
2074
2649
|
}
|
|
2075
2650
|
this._runSttLoop().catch((err) => {
|
|
2076
|
-
|
|
2651
|
+
this._log.error({ err }, "STT loop error");
|
|
2077
2652
|
});
|
|
2078
2653
|
}
|
|
2079
2654
|
feedAudio(audio) {
|
|
@@ -2081,8 +2656,16 @@ var PipelineSession = class {
|
|
|
2081
2656
|
this._audioBuffer.push(audio);
|
|
2082
2657
|
}
|
|
2083
2658
|
}
|
|
2659
|
+
async feedDtmf(digits) {
|
|
2660
|
+
this._conversation.push({
|
|
2661
|
+
role: "user",
|
|
2662
|
+
content: `[DTMF \uC785\uB825: ${digits}]`
|
|
2663
|
+
});
|
|
2664
|
+
await this._respond();
|
|
2665
|
+
}
|
|
2084
2666
|
async stop() {
|
|
2085
2667
|
this._running = false;
|
|
2668
|
+
this._log.info("PipelineSession stopped");
|
|
2086
2669
|
this._audioBuffer = [];
|
|
2087
2670
|
}
|
|
2088
2671
|
async _runSttLoop() {
|
|
@@ -2096,8 +2679,10 @@ var PipelineSession = class {
|
|
|
2096
2679
|
if (this._callSession) {
|
|
2097
2680
|
this._callSession.clearAudio();
|
|
2098
2681
|
}
|
|
2682
|
+
this._log.info('Barge-in: "%s"', event.transcript.substring(0, 30));
|
|
2099
2683
|
}
|
|
2100
2684
|
if (event.type === "final" && event.transcript.trim()) {
|
|
2685
|
+
this._log.info("STT: %s", event.transcript);
|
|
2101
2686
|
await this._handleUserSpeech(event.transcript);
|
|
2102
2687
|
}
|
|
2103
2688
|
}
|
|
@@ -2122,11 +2707,37 @@ var PipelineSession = class {
|
|
|
2122
2707
|
this._conversation.push({ role: "user", content: transcript });
|
|
2123
2708
|
await this._respond();
|
|
2124
2709
|
}
|
|
2710
|
+
_buildEffectiveTools() {
|
|
2711
|
+
const includeCollectDtmf = !this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */);
|
|
2712
|
+
const includeSendDtmf = !this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */);
|
|
2713
|
+
if (!includeCollectDtmf && !includeSendDtmf) return this._tools ?? void 0;
|
|
2714
|
+
const base = this._tools ? this._tools.fork() : new ToolRegistry();
|
|
2715
|
+
if (includeCollectDtmf) {
|
|
2716
|
+
base.register({
|
|
2717
|
+
name: "collect_dtmf",
|
|
2718
|
+
description: COLLECT_DTMF_TOOL3.function.description,
|
|
2719
|
+
parameters: COLLECT_DTMF_TOOL3.function.parameters.properties,
|
|
2720
|
+
required: COLLECT_DTMF_TOOL3.function.parameters.required,
|
|
2721
|
+
handler: async () => ""
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
if (includeSendDtmf) {
|
|
2725
|
+
base.register({
|
|
2726
|
+
name: "send_dtmf",
|
|
2727
|
+
description: SEND_DTMF_TOOL3.function.description,
|
|
2728
|
+
parameters: SEND_DTMF_TOOL3.function.parameters.properties,
|
|
2729
|
+
required: SEND_DTMF_TOOL3.function.parameters.required,
|
|
2730
|
+
handler: async () => ""
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2733
|
+
return base;
|
|
2734
|
+
}
|
|
2125
2735
|
async _respond() {
|
|
2126
2736
|
let fullResponse = "";
|
|
2127
2737
|
const textChunks = [];
|
|
2738
|
+
const effectiveTools = this._buildEffectiveTools();
|
|
2128
2739
|
const llmStream = this._llm.generate(this._conversation, {
|
|
2129
|
-
tools:
|
|
2740
|
+
tools: effectiveTools,
|
|
2130
2741
|
temperature: this._temperature,
|
|
2131
2742
|
maxTokens: this._maxTokens
|
|
2132
2743
|
});
|
|
@@ -2140,15 +2751,44 @@ var PipelineSession = class {
|
|
|
2140
2751
|
}
|
|
2141
2752
|
}
|
|
2142
2753
|
if (fullResponse.trim()) {
|
|
2754
|
+
this._log.info("Assistant: %s", fullResponse.substring(0, 100));
|
|
2143
2755
|
this._conversation.push({ role: "assistant", content: fullResponse });
|
|
2144
2756
|
await this._synthesizeAndSend(fullResponse);
|
|
2145
2757
|
}
|
|
2146
2758
|
}
|
|
2147
2759
|
async _handleToolCall(chunk) {
|
|
2148
|
-
if (!chunk.toolCall
|
|
2760
|
+
if (!chunk.toolCall) return;
|
|
2149
2761
|
const { id, name, arguments: argsStr } = chunk.toolCall;
|
|
2150
2762
|
try {
|
|
2151
2763
|
const args = JSON.parse(argsStr);
|
|
2764
|
+
if (name === "collect_dtmf" && this._callSession) {
|
|
2765
|
+
let result2;
|
|
2766
|
+
try {
|
|
2767
|
+
result2 = await this._callSession.collectDtmf({
|
|
2768
|
+
maxDigits: args["max_digits"] ?? 4,
|
|
2769
|
+
finishOnKey: args["finish_on_key"] ?? "#",
|
|
2770
|
+
timeout: args["timeout"] ?? 5
|
|
2771
|
+
});
|
|
2772
|
+
} catch (err) {
|
|
2773
|
+
result2 = `Error: ${err}`;
|
|
2774
|
+
}
|
|
2775
|
+
this._conversation.push({ role: "tool", content: result2 || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)", tool_call_id: id, name });
|
|
2776
|
+
await this._respond();
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
if (name === "send_dtmf" && this._callSession) {
|
|
2780
|
+
let result2;
|
|
2781
|
+
try {
|
|
2782
|
+
await this._callSession.sendDtmfSequence(args["digits"] ?? "");
|
|
2783
|
+
result2 = "sent";
|
|
2784
|
+
} catch (err) {
|
|
2785
|
+
result2 = `Error: ${err}`;
|
|
2786
|
+
}
|
|
2787
|
+
this._conversation.push({ role: "tool", content: result2, tool_call_id: id, name });
|
|
2788
|
+
await this._respond();
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
if (!this._tools) return;
|
|
2152
2792
|
const result = await this._tools.call(name, args);
|
|
2153
2793
|
this._conversation.push({
|
|
2154
2794
|
role: "assistant",
|
|
@@ -2161,9 +2801,10 @@ var PipelineSession = class {
|
|
|
2161
2801
|
tool_call_id: id,
|
|
2162
2802
|
name
|
|
2163
2803
|
});
|
|
2804
|
+
const effectiveTools = this._buildEffectiveTools();
|
|
2164
2805
|
let followUpText = "";
|
|
2165
2806
|
const followUpStream = this._llm.generate(this._conversation, {
|
|
2166
|
-
tools:
|
|
2807
|
+
tools: effectiveTools,
|
|
2167
2808
|
temperature: this._temperature,
|
|
2168
2809
|
maxTokens: this._maxTokens
|
|
2169
2810
|
});
|
|
@@ -2178,7 +2819,7 @@ var PipelineSession = class {
|
|
|
2178
2819
|
await this._synthesizeAndSend(followUpText);
|
|
2179
2820
|
}
|
|
2180
2821
|
} catch (err) {
|
|
2181
|
-
|
|
2822
|
+
this._log.error({ err }, "Tool call failed: %s", name);
|
|
2182
2823
|
}
|
|
2183
2824
|
}
|
|
2184
2825
|
async _synthesizeAndSend(text) {
|
|
@@ -2204,7 +2845,7 @@ var PipelineSession = class {
|
|
|
2204
2845
|
}
|
|
2205
2846
|
}
|
|
2206
2847
|
} catch (err) {
|
|
2207
|
-
|
|
2848
|
+
this._log.error({ err }, "TTS error");
|
|
2208
2849
|
} finally {
|
|
2209
2850
|
this._speaking = false;
|
|
2210
2851
|
}
|
|
@@ -2214,6 +2855,10 @@ var PipelineSession = class {
|
|
|
2214
2855
|
// src/agent/pipeline/deepgram-stt.ts
|
|
2215
2856
|
var DeepgramSTT = class {
|
|
2216
2857
|
_options;
|
|
2858
|
+
_log = NOOP_LOGGER;
|
|
2859
|
+
setLogger(logger) {
|
|
2860
|
+
this._log = logger;
|
|
2861
|
+
}
|
|
2217
2862
|
constructor(options = {}) {
|
|
2218
2863
|
this._options = {
|
|
2219
2864
|
model: "nova-3",
|
|
@@ -2283,7 +2928,7 @@ var DeepgramSTT = class {
|
|
|
2283
2928
|
}
|
|
2284
2929
|
});
|
|
2285
2930
|
ws.on("error", (err) => {
|
|
2286
|
-
|
|
2931
|
+
this._log.error({ err }, "Deepgram STT error");
|
|
2287
2932
|
done = true;
|
|
2288
2933
|
if (resolveWait) {
|
|
2289
2934
|
resolveWait();
|
|
@@ -2294,6 +2939,7 @@ var DeepgramSTT = class {
|
|
|
2294
2939
|
ws.on("open", resolve);
|
|
2295
2940
|
ws.on("error", reject);
|
|
2296
2941
|
});
|
|
2942
|
+
this._log.info("Deepgram STT connected");
|
|
2297
2943
|
const feedPromise = (async () => {
|
|
2298
2944
|
try {
|
|
2299
2945
|
for await (const chunk of audioStream) {
|
|
@@ -2330,6 +2976,10 @@ var DeepgramSTT = class {
|
|
|
2330
2976
|
// src/agent/pipeline/elevenlabs-tts.ts
|
|
2331
2977
|
var ElevenLabsTTS = class {
|
|
2332
2978
|
_options;
|
|
2979
|
+
_log = NOOP_LOGGER;
|
|
2980
|
+
setLogger(logger) {
|
|
2981
|
+
this._log = logger;
|
|
2982
|
+
}
|
|
2333
2983
|
constructor(options = {}) {
|
|
2334
2984
|
this._options = {
|
|
2335
2985
|
voiceId: "EXAVITQu4vr4xnSDxMaL",
|
|
@@ -2429,7 +3079,7 @@ var ElevenLabsTTS = class {
|
|
|
2429
3079
|
}
|
|
2430
3080
|
});
|
|
2431
3081
|
ws.on("error", (err) => {
|
|
2432
|
-
|
|
3082
|
+
this._log.error({ err }, "ElevenLabs TTS error");
|
|
2433
3083
|
done = true;
|
|
2434
3084
|
if (resolveWait) {
|
|
2435
3085
|
resolveWait();
|
|
@@ -2440,16 +3090,19 @@ var ElevenLabsTTS = class {
|
|
|
2440
3090
|
ws.on("open", resolve);
|
|
2441
3091
|
ws.on("error", reject);
|
|
2442
3092
|
});
|
|
3093
|
+
this._log.info("ElevenLabs TTS connected");
|
|
2443
3094
|
const feedPromise = (async () => {
|
|
2444
3095
|
try {
|
|
2445
3096
|
for await (const chunk of textStream) {
|
|
2446
3097
|
if (done) break;
|
|
2447
3098
|
if (ws.readyState === 1) {
|
|
3099
|
+
this._log.debug("ElevenLabs sending text: %s", chunk.substring(0, 60));
|
|
2448
3100
|
ws.send(JSON.stringify({ text: chunk }));
|
|
2449
3101
|
}
|
|
2450
3102
|
}
|
|
2451
3103
|
} finally {
|
|
2452
3104
|
if (ws.readyState === 1) {
|
|
3105
|
+
this._log.debug("ElevenLabs sending EOS");
|
|
2453
3106
|
ws.send(JSON.stringify({ text: "" }));
|
|
2454
3107
|
}
|
|
2455
3108
|
}
|
|
@@ -2987,6 +3640,6 @@ function mcpServerHTTP(options) {
|
|
|
2987
3640
|
};
|
|
2988
3641
|
}
|
|
2989
3642
|
|
|
2990
|
-
export { AnthropicLLM, AudioRecorder, CallSession, ClawOpsAgent, DECODE_TABLE, DeepSeekLLM, DeepgramSTT, ElevenLabsTTS, FireworksLLM, GeminiLLM, GeminiRealtime, GroqLLM, MCPClient, MistralLLM, OllamaLLM, OpenAICompatLLM, OpenAILLM, OpenAIRealtime, PerplexityLLM, PipelineSession, TogetherLLM, ToolRegistry, XaiLLM, functionTool, getTracingConfig, mcpServerHTTP, mcpServerStdio, pcm16ToUlaw, resamplePcm16, resetTracingConfig, setTracingConfig, ulawToPcm16, zodToToolParams };
|
|
3643
|
+
export { AnthropicLLM, AudioRecorder, BuiltinTool, CallSession, ClawOpsAgent, DECODE_TABLE, DeepSeekLLM, DeepgramSTT, ElevenLabsTTS, FireworksLLM, GeminiLLM, GeminiRealtime, GroqLLM, MCPClient, MistralLLM, OllamaLLM, OpenAICompatLLM, OpenAILLM, OpenAIRealtime, PerplexityLLM, PipelineSession, TogetherLLM, ToolRegistry, XaiLLM, createAgentLogger, createPipelineLogger, functionTool, getTracingConfig, mcpServerHTTP, mcpServerStdio, pcm16ToUlaw, resamplePcm16, resetTracingConfig, setTracingConfig, ulawToPcm16, zodToToolParams };
|
|
2991
3644
|
//# sourceMappingURL=index.js.map
|
|
2992
3645
|
//# sourceMappingURL=index.js.map
|