@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.cjs
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var chunk6IQN5RQD_cjs = require('../chunk-6IQN5RQD.cjs');
|
|
4
|
+
var pino = require('pino');
|
|
4
5
|
var fs = require('fs');
|
|
5
6
|
var path = require('path');
|
|
6
7
|
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
7
10
|
function _interopNamespace(e) {
|
|
8
11
|
if (e && e.__esModule) return e;
|
|
9
12
|
var n = Object.create(null);
|
|
@@ -22,6 +25,7 @@ function _interopNamespace(e) {
|
|
|
22
25
|
return Object.freeze(n);
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
var pino__default = /*#__PURE__*/_interopDefault(pino);
|
|
25
29
|
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
26
30
|
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
27
31
|
|
|
@@ -345,6 +349,14 @@ function resamplePcm16(pcm, fromRate, toRate) {
|
|
|
345
349
|
}
|
|
346
350
|
return out;
|
|
347
351
|
}
|
|
352
|
+
var DEFAULT_LOGGER = pino__default.default({ name: "clawops.agent" });
|
|
353
|
+
var NOOP_LOGGER = pino__default.default({ level: "silent" });
|
|
354
|
+
function createAgentLogger(userLogger) {
|
|
355
|
+
return userLogger ?? DEFAULT_LOGGER;
|
|
356
|
+
}
|
|
357
|
+
function createPipelineLogger(parent) {
|
|
358
|
+
return parent.child({ module: "pipeline" });
|
|
359
|
+
}
|
|
348
360
|
|
|
349
361
|
// src/agent/control-ws.ts
|
|
350
362
|
var INITIAL_RECONNECT_DELAY = 1e3;
|
|
@@ -373,6 +385,10 @@ var ControlWebSocket = class {
|
|
|
373
385
|
_closed = false;
|
|
374
386
|
_connectedResolve = null;
|
|
375
387
|
_connectedPromise;
|
|
388
|
+
_log = NOOP_LOGGER;
|
|
389
|
+
setLogger(logger) {
|
|
390
|
+
this._log = logger;
|
|
391
|
+
}
|
|
376
392
|
/** Register an event handler for a specific event type. */
|
|
377
393
|
on(event, handler) {
|
|
378
394
|
let list = this._handlers.get(event);
|
|
@@ -420,13 +436,14 @@ var ControlWebSocket = class {
|
|
|
420
436
|
this._connectedResolve();
|
|
421
437
|
this._connectedResolve = null;
|
|
422
438
|
}
|
|
439
|
+
this._log.info("Control WS connected: %s", this._url);
|
|
423
440
|
});
|
|
424
441
|
ws.on("message", (data) => {
|
|
425
442
|
try {
|
|
426
443
|
const msg = JSON.parse(data.toString());
|
|
427
444
|
this._dispatchEvent(msg);
|
|
428
445
|
} catch {
|
|
429
|
-
|
|
446
|
+
this._log.warn("Control WS parse error");
|
|
430
447
|
}
|
|
431
448
|
});
|
|
432
449
|
ws.on("close", () => {
|
|
@@ -435,7 +452,7 @@ var ControlWebSocket = class {
|
|
|
435
452
|
}
|
|
436
453
|
});
|
|
437
454
|
ws.on("error", (err) => {
|
|
438
|
-
|
|
455
|
+
this._log.warn("Control WS error: %s", err.message);
|
|
439
456
|
});
|
|
440
457
|
}
|
|
441
458
|
_dispatchEvent(event) {
|
|
@@ -446,11 +463,11 @@ var ControlWebSocket = class {
|
|
|
446
463
|
const result = handler(event);
|
|
447
464
|
if (result && typeof result.catch === "function") {
|
|
448
465
|
result.catch((err) => {
|
|
449
|
-
|
|
466
|
+
this._log.error({ err }, "Control WS handler error: %s", event.event);
|
|
450
467
|
});
|
|
451
468
|
}
|
|
452
469
|
} catch (err) {
|
|
453
|
-
|
|
470
|
+
this._log.error({ err }, "Control WS handler error: %s", event.event);
|
|
454
471
|
}
|
|
455
472
|
}
|
|
456
473
|
}
|
|
@@ -458,10 +475,11 @@ var ControlWebSocket = class {
|
|
|
458
475
|
_scheduleReconnect() {
|
|
459
476
|
const delay = this._reconnectDelay;
|
|
460
477
|
this._reconnectDelay = Math.min(this._reconnectDelay * 2, MAX_RECONNECT_DELAY);
|
|
478
|
+
this._log.info("Control WS reconnecting in %ds...", delay / 1e3);
|
|
461
479
|
setTimeout(() => {
|
|
462
480
|
if (!this._closed) {
|
|
463
481
|
this._doConnect().catch((err) => {
|
|
464
|
-
|
|
482
|
+
this._log.warn({ err }, "Control WS reconnect failed");
|
|
465
483
|
this._scheduleReconnect();
|
|
466
484
|
});
|
|
467
485
|
}
|
|
@@ -473,6 +491,10 @@ var ControlWebSocket = class {
|
|
|
473
491
|
var MCPClient = class {
|
|
474
492
|
_servers = /* @__PURE__ */ new Map();
|
|
475
493
|
_clients = /* @__PURE__ */ new Map();
|
|
494
|
+
_log = NOOP_LOGGER;
|
|
495
|
+
setLogger(logger) {
|
|
496
|
+
this._log = logger;
|
|
497
|
+
}
|
|
476
498
|
/** Add an MCP server configuration. */
|
|
477
499
|
addServer(name, config) {
|
|
478
500
|
this._servers.set(name, config);
|
|
@@ -485,7 +507,7 @@ var MCPClient = class {
|
|
|
485
507
|
const tools = await this._connectServer(name, config);
|
|
486
508
|
allTools.push(...tools);
|
|
487
509
|
} catch (err) {
|
|
488
|
-
|
|
510
|
+
this._log.error({ err }, "MCP connection failed: %s", name);
|
|
489
511
|
}
|
|
490
512
|
}
|
|
491
513
|
return allTools;
|
|
@@ -494,12 +516,13 @@ var MCPClient = class {
|
|
|
494
516
|
async disconnect() {
|
|
495
517
|
for (const [name, client] of this._clients) {
|
|
496
518
|
try {
|
|
519
|
+
this._log.debug("MCP closing: %s", name);
|
|
497
520
|
const c = client;
|
|
498
521
|
if (c.close) {
|
|
499
522
|
await c.close();
|
|
500
523
|
}
|
|
501
524
|
} catch (err) {
|
|
502
|
-
|
|
525
|
+
this._log.error({ err }, "MCP disconnect error: %s", name);
|
|
503
526
|
}
|
|
504
527
|
}
|
|
505
528
|
this._clients.clear();
|
|
@@ -508,6 +531,11 @@ var MCPClient = class {
|
|
|
508
531
|
const sdk = await import('@modelcontextprotocol/sdk/client/index.js');
|
|
509
532
|
const { Client } = sdk;
|
|
510
533
|
const client = new Client({ name: `clawops-${name}`, version: "1.0.0" });
|
|
534
|
+
if (config.type === "stdio") {
|
|
535
|
+
this._log.debug("MCP connecting (stdio): %s", config["command"]);
|
|
536
|
+
} else if (config.type === "http") {
|
|
537
|
+
this._log.debug("MCP connecting (http): %s", config["url"]);
|
|
538
|
+
}
|
|
511
539
|
let transport;
|
|
512
540
|
if (config.type === "stdio") {
|
|
513
541
|
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
|
|
@@ -536,6 +564,7 @@ var MCPClient = class {
|
|
|
536
564
|
parameters: inputSchema.properties ?? {},
|
|
537
565
|
required: inputSchema.required ?? [],
|
|
538
566
|
handler: async (args) => {
|
|
567
|
+
this._log.debug("MCP call_tool: %s", toolDef.name);
|
|
539
568
|
const result = await client.callTool({
|
|
540
569
|
name: toolDef.name,
|
|
541
570
|
arguments: args
|
|
@@ -548,6 +577,8 @@ var MCPClient = class {
|
|
|
548
577
|
}
|
|
549
578
|
});
|
|
550
579
|
}
|
|
580
|
+
this._log.info("MCP server connected: %d tools found", tools.length);
|
|
581
|
+
this._log.debug("MCP tools: %s", tools.map((t) => t.name));
|
|
551
582
|
return tools;
|
|
552
583
|
}
|
|
553
584
|
};
|
|
@@ -578,6 +609,20 @@ function buildMediaResponse(audioBase64) {
|
|
|
578
609
|
}
|
|
579
610
|
});
|
|
580
611
|
}
|
|
612
|
+
var VALID_DTMF_DIGITS = new Set("0123456789*#");
|
|
613
|
+
function parseDtmfEvent(data) {
|
|
614
|
+
const dtmf = data["dtmf"];
|
|
615
|
+
return {
|
|
616
|
+
digit: dtmf["digit"] ?? "",
|
|
617
|
+
track: dtmf["track"] ?? ""
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function buildDtmfMessage(digit) {
|
|
621
|
+
if (!VALID_DTMF_DIGITS.has(digit)) {
|
|
622
|
+
throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 DTMF digit: ${digit}`);
|
|
623
|
+
}
|
|
624
|
+
return JSON.stringify({ event: "dtmf", dtmf: { digit } });
|
|
625
|
+
}
|
|
581
626
|
var MediaWebSocket = class {
|
|
582
627
|
_ws = null;
|
|
583
628
|
_audioQueue = [];
|
|
@@ -586,6 +631,12 @@ var MediaWebSocket = class {
|
|
|
586
631
|
_onAudio = null;
|
|
587
632
|
_onStart = null;
|
|
588
633
|
_onClose = null;
|
|
634
|
+
_onDtmf = null;
|
|
635
|
+
_markWaiters = /* @__PURE__ */ new Map();
|
|
636
|
+
_log = NOOP_LOGGER;
|
|
637
|
+
setLogger(logger) {
|
|
638
|
+
this._log = logger;
|
|
639
|
+
}
|
|
589
640
|
/** Set the handler for inbound audio data. */
|
|
590
641
|
onAudio(handler) {
|
|
591
642
|
this._onAudio = handler;
|
|
@@ -598,6 +649,20 @@ var MediaWebSocket = class {
|
|
|
598
649
|
onClose(handler) {
|
|
599
650
|
this._onClose = handler;
|
|
600
651
|
}
|
|
652
|
+
/** Set the handler for inbound DTMF events. */
|
|
653
|
+
onDtmf(handler) {
|
|
654
|
+
this._onDtmf = handler;
|
|
655
|
+
}
|
|
656
|
+
/** Send a single DTMF digit to the platform. */
|
|
657
|
+
sendDtmf(digit) {
|
|
658
|
+
if (this._ws && this._ws.readyState === 1) {
|
|
659
|
+
this._ws.send(buildDtmfMessage(digit));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/** Whether the WebSocket is connected. */
|
|
663
|
+
get isConnected() {
|
|
664
|
+
return this._ws !== null && this._ws.readyState === 1 && !this._closed;
|
|
665
|
+
}
|
|
601
666
|
/** Connect to a media WebSocket URL with Bearer authentication. */
|
|
602
667
|
async connect(url, apiKey) {
|
|
603
668
|
const { WebSocket } = await import('ws');
|
|
@@ -613,6 +678,7 @@ var MediaWebSocket = class {
|
|
|
613
678
|
ws.on("open", () => {
|
|
614
679
|
this._startSendLoop();
|
|
615
680
|
resolve();
|
|
681
|
+
this._log.info("Media WS connected: %s", url);
|
|
616
682
|
});
|
|
617
683
|
ws.on("message", (data) => {
|
|
618
684
|
try {
|
|
@@ -631,7 +697,7 @@ var MediaWebSocket = class {
|
|
|
631
697
|
if (!this._ws) {
|
|
632
698
|
reject(err);
|
|
633
699
|
}
|
|
634
|
-
|
|
700
|
+
this._log.error({ err }, "Media WS error");
|
|
635
701
|
});
|
|
636
702
|
});
|
|
637
703
|
}
|
|
@@ -657,6 +723,34 @@ var MediaWebSocket = class {
|
|
|
657
723
|
);
|
|
658
724
|
}
|
|
659
725
|
}
|
|
726
|
+
/** Wait for all queued audio to be sent. */
|
|
727
|
+
flush() {
|
|
728
|
+
if (this._audioQueue.length === 0 || this._closed) return Promise.resolve();
|
|
729
|
+
return new Promise((resolve) => {
|
|
730
|
+
const check = () => {
|
|
731
|
+
if (this._audioQueue.length === 0 || this._closed) {
|
|
732
|
+
resolve();
|
|
733
|
+
} else {
|
|
734
|
+
setTimeout(check, 5);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
setTimeout(check, 5);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
/** Wait for a named mark to be echoed back by the server. */
|
|
741
|
+
waitForMark(name, timeoutMs = 5e3) {
|
|
742
|
+
if (this._closed) return Promise.resolve();
|
|
743
|
+
return new Promise((resolve) => {
|
|
744
|
+
const timer = setTimeout(() => {
|
|
745
|
+
this._markWaiters.delete(name);
|
|
746
|
+
resolve();
|
|
747
|
+
}, timeoutMs);
|
|
748
|
+
this._markWaiters.set(name, () => {
|
|
749
|
+
clearTimeout(timer);
|
|
750
|
+
resolve();
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
}
|
|
660
754
|
/** Close the media WebSocket. */
|
|
661
755
|
close() {
|
|
662
756
|
this._closed = true;
|
|
@@ -682,6 +776,24 @@ var MediaWebSocket = class {
|
|
|
682
776
|
}
|
|
683
777
|
break;
|
|
684
778
|
}
|
|
779
|
+
case "dtmf": {
|
|
780
|
+
const dtmfEvt = parseDtmfEvent(msg);
|
|
781
|
+
if (this._onDtmf) {
|
|
782
|
+
this._onDtmf(dtmfEvt.digit);
|
|
783
|
+
}
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
case "mark": {
|
|
787
|
+
const markName = msg["mark"]?.["name"];
|
|
788
|
+
if (markName) {
|
|
789
|
+
const resolve = this._markWaiters.get(markName);
|
|
790
|
+
if (resolve) {
|
|
791
|
+
this._markWaiters.delete(markName);
|
|
792
|
+
resolve();
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
break;
|
|
796
|
+
}
|
|
685
797
|
case "stop": {
|
|
686
798
|
this.close();
|
|
687
799
|
break;
|
|
@@ -746,6 +858,10 @@ var AudioRecorder = class {
|
|
|
746
858
|
_mixWritten = 0;
|
|
747
859
|
_startTime = 0;
|
|
748
860
|
_started = false;
|
|
861
|
+
_log = NOOP_LOGGER;
|
|
862
|
+
setLogger(logger) {
|
|
863
|
+
this._log = logger;
|
|
864
|
+
}
|
|
749
865
|
constructor(recordingPath, callId) {
|
|
750
866
|
this._dir = path__namespace.join(recordingPath, callId);
|
|
751
867
|
}
|
|
@@ -760,6 +876,7 @@ var AudioRecorder = class {
|
|
|
760
876
|
fs__namespace.writeSync(this._fdMix, header);
|
|
761
877
|
this._startTime = performance.now();
|
|
762
878
|
this._started = true;
|
|
879
|
+
this._log.info("Recording started: %s", this._dir);
|
|
763
880
|
}
|
|
764
881
|
_expectedBytes() {
|
|
765
882
|
const elapsed = (performance.now() - this._startTime) / 1e3;
|
|
@@ -812,7 +929,7 @@ var AudioRecorder = class {
|
|
|
812
929
|
this._inWritten += pcm16_8k.length;
|
|
813
930
|
this._writeToMix(pcm16_8k, posBefore);
|
|
814
931
|
} catch (err) {
|
|
815
|
-
|
|
932
|
+
this._log.error({ err }, "Recording write error (inbound)");
|
|
816
933
|
}
|
|
817
934
|
}
|
|
818
935
|
writeOutbound(pcm16_8k) {
|
|
@@ -825,7 +942,7 @@ var AudioRecorder = class {
|
|
|
825
942
|
this._outWritten += pcm16_8k.length;
|
|
826
943
|
this._writeToMix(pcm16_8k, posBefore);
|
|
827
944
|
} catch (err) {
|
|
828
|
-
|
|
945
|
+
this._log.error({ err }, "Recording write error (outbound)");
|
|
829
946
|
}
|
|
830
947
|
}
|
|
831
948
|
stop() {
|
|
@@ -846,8 +963,10 @@ var AudioRecorder = class {
|
|
|
846
963
|
fs__namespace.writeSync(fd, makeWavHeader(maxWritten), 0, 44, 0);
|
|
847
964
|
fs__namespace.closeSync(fd);
|
|
848
965
|
}
|
|
966
|
+
const maxSec = maxWritten / 16e3;
|
|
967
|
+
this._log.info("Recording stopped: %s (%ds)", this._dir, maxSec);
|
|
849
968
|
} catch (err) {
|
|
850
|
-
|
|
969
|
+
this._log.error({ err }, "Recording stop error");
|
|
851
970
|
} finally {
|
|
852
971
|
this._fdIn = null;
|
|
853
972
|
this._fdOut = null;
|
|
@@ -870,6 +989,14 @@ var CallSession = class {
|
|
|
870
989
|
_sendAudioFn = null;
|
|
871
990
|
_clearAudioFn = null;
|
|
872
991
|
_hangupFn = null;
|
|
992
|
+
/** @internal */
|
|
993
|
+
_sendDtmfFn = null;
|
|
994
|
+
/** @internal */
|
|
995
|
+
_isTransportConnected = null;
|
|
996
|
+
_dtmfCollectorActive = false;
|
|
997
|
+
_dtmfResolvers = [];
|
|
998
|
+
_dtmfBuffer = [];
|
|
999
|
+
_log = NOOP_LOGGER;
|
|
873
1000
|
_handlers = /* @__PURE__ */ new Map();
|
|
874
1001
|
_endedPromise;
|
|
875
1002
|
_resolveEnded;
|
|
@@ -886,6 +1013,9 @@ var CallSession = class {
|
|
|
886
1013
|
this._resolveEnded = resolve;
|
|
887
1014
|
});
|
|
888
1015
|
}
|
|
1016
|
+
setLogger(logger) {
|
|
1017
|
+
this._log = logger;
|
|
1018
|
+
}
|
|
889
1019
|
get status() {
|
|
890
1020
|
return this._status;
|
|
891
1021
|
}
|
|
@@ -893,10 +1023,12 @@ var CallSession = class {
|
|
|
893
1023
|
return (Date.now() - this.startTime.getTime()) / 1e3;
|
|
894
1024
|
}
|
|
895
1025
|
/** Bind transport functions (called internally by the agent). */
|
|
896
|
-
_bindTransport(send, clear, hangup) {
|
|
1026
|
+
_bindTransport(send, clear, hangup, sendDtmf, isConnected) {
|
|
897
1027
|
this._sendAudioFn = send;
|
|
898
1028
|
this._clearAudioFn = clear;
|
|
899
1029
|
this._hangupFn = hangup;
|
|
1030
|
+
if (sendDtmf) this._sendDtmfFn = sendDtmf;
|
|
1031
|
+
if (isConnected) this._isTransportConnected = isConnected;
|
|
900
1032
|
this._status = "active";
|
|
901
1033
|
}
|
|
902
1034
|
/** Send PCM16 or ulaw audio to the caller. */
|
|
@@ -911,10 +1043,78 @@ var CallSession = class {
|
|
|
911
1043
|
this._clearAudioFn();
|
|
912
1044
|
}
|
|
913
1045
|
}
|
|
914
|
-
/** Hang up the call. */
|
|
915
|
-
hangup() {
|
|
1046
|
+
/** Hang up the call, waiting for pending audio to finish. */
|
|
1047
|
+
async hangup() {
|
|
916
1048
|
if (this._hangupFn) {
|
|
917
|
-
this._hangupFn();
|
|
1049
|
+
await this._hangupFn();
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
/** @internal Route a received DTMF digit to an active collector or buffer. */
|
|
1053
|
+
_routeDtmf(digit) {
|
|
1054
|
+
if (this._dtmfCollectorActive && this._dtmfResolvers.length > 0) {
|
|
1055
|
+
const resolve = this._dtmfResolvers.shift();
|
|
1056
|
+
resolve(digit);
|
|
1057
|
+
} else {
|
|
1058
|
+
this._dtmfBuffer.push(digit);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
/** Collect DTMF digits from the caller. */
|
|
1062
|
+
async collectDtmf(options) {
|
|
1063
|
+
if (this._dtmfCollectorActive) {
|
|
1064
|
+
throw new Error("\uC774\uBBF8 DTMF \uC218\uC9D1 \uC911\uC785\uB2C8\uB2E4");
|
|
1065
|
+
}
|
|
1066
|
+
const { maxDigits, finishOnKey = "#", timeout = 5 } = options;
|
|
1067
|
+
this._dtmfCollectorActive = true;
|
|
1068
|
+
const collected = [];
|
|
1069
|
+
try {
|
|
1070
|
+
while (collected.length < maxDigits) {
|
|
1071
|
+
if (this._dtmfBuffer.length > 0) {
|
|
1072
|
+
const digit2 = this._dtmfBuffer.shift();
|
|
1073
|
+
if (digit2 === finishOnKey) break;
|
|
1074
|
+
collected.push(digit2);
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
const digit = await Promise.race([
|
|
1078
|
+
new Promise((resolve) => {
|
|
1079
|
+
this._dtmfResolvers.push(resolve);
|
|
1080
|
+
}),
|
|
1081
|
+
new Promise((resolve) => {
|
|
1082
|
+
setTimeout(() => resolve(null), timeout * 1e3);
|
|
1083
|
+
})
|
|
1084
|
+
]);
|
|
1085
|
+
if (digit === null) break;
|
|
1086
|
+
if (digit === finishOnKey) break;
|
|
1087
|
+
collected.push(digit);
|
|
1088
|
+
}
|
|
1089
|
+
} finally {
|
|
1090
|
+
this._dtmfCollectorActive = false;
|
|
1091
|
+
this._dtmfResolvers = [];
|
|
1092
|
+
this._dtmfBuffer = [];
|
|
1093
|
+
}
|
|
1094
|
+
const result = collected.join("");
|
|
1095
|
+
this._log.info("DTMF collected: %s", result);
|
|
1096
|
+
return result;
|
|
1097
|
+
}
|
|
1098
|
+
/** Send a sequence of DTMF digits. */
|
|
1099
|
+
async sendDtmfSequence(digits) {
|
|
1100
|
+
if (!this._sendDtmfFn) {
|
|
1101
|
+
throw new Error("DTMF \uC804\uC1A1 \uD568\uC218\uAC00 \uBC14\uC778\uB529\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4");
|
|
1102
|
+
}
|
|
1103
|
+
for (const ch of digits) {
|
|
1104
|
+
if (this._isTransportConnected && !this._isTransportConnected()) {
|
|
1105
|
+
throw new Error("DTMF \uC804\uC1A1 \uC911 \uC5F0\uACB0\uC774 \uB04A\uC5B4\uC84C\uC2B5\uB2C8\uB2E4");
|
|
1106
|
+
}
|
|
1107
|
+
if (ch === "w") {
|
|
1108
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1109
|
+
} else if (ch === "W") {
|
|
1110
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
1111
|
+
} else if ("0123456789*#".includes(ch)) {
|
|
1112
|
+
if (this._sendDtmfFn) {
|
|
1113
|
+
await this._sendDtmfFn(ch);
|
|
1114
|
+
}
|
|
1115
|
+
} else {
|
|
1116
|
+
throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 DTMF \uBB38\uC790: ${ch}`);
|
|
1117
|
+
}
|
|
918
1118
|
}
|
|
919
1119
|
}
|
|
920
1120
|
/** Register an event handler. */
|
|
@@ -945,17 +1145,44 @@ var CallSession = class {
|
|
|
945
1145
|
const result = handler(this, ...args);
|
|
946
1146
|
if (result && typeof result.catch === "function") {
|
|
947
1147
|
result.catch((err) => {
|
|
948
|
-
|
|
1148
|
+
this._log.error({ err }, "CallSession handler error: %s", event);
|
|
949
1149
|
});
|
|
950
1150
|
}
|
|
951
1151
|
} catch (err) {
|
|
952
|
-
|
|
1152
|
+
this._log.error({ err }, "CallSession handler error: %s", event);
|
|
953
1153
|
}
|
|
954
1154
|
}
|
|
955
1155
|
}
|
|
956
1156
|
}
|
|
957
1157
|
};
|
|
958
1158
|
|
|
1159
|
+
// src/agent/builtin-tool.ts
|
|
1160
|
+
var BuiltinTool = /* @__PURE__ */ ((BuiltinTool2) => {
|
|
1161
|
+
BuiltinTool2["HANG_UP"] = "hang_up";
|
|
1162
|
+
BuiltinTool2["COLLECT_DTMF"] = "collect_dtmf";
|
|
1163
|
+
BuiltinTool2["SEND_DTMF"] = "send_dtmf";
|
|
1164
|
+
BuiltinTool2["ALL"] = "all";
|
|
1165
|
+
BuiltinTool2["NONE"] = "none";
|
|
1166
|
+
return BuiltinTool2;
|
|
1167
|
+
})(BuiltinTool || {});
|
|
1168
|
+
var INDIVIDUAL_TOOLS = /* @__PURE__ */ new Set([
|
|
1169
|
+
"hang_up" /* HANG_UP */,
|
|
1170
|
+
"collect_dtmf" /* COLLECT_DTMF */,
|
|
1171
|
+
"send_dtmf" /* SEND_DTMF */
|
|
1172
|
+
]);
|
|
1173
|
+
function resolveBuiltinTools(value) {
|
|
1174
|
+
if (typeof value === "string") {
|
|
1175
|
+
if (value === "all" /* ALL */) {
|
|
1176
|
+
return new Set(INDIVIDUAL_TOOLS);
|
|
1177
|
+
}
|
|
1178
|
+
if (value === "none" /* NONE */) {
|
|
1179
|
+
return /* @__PURE__ */ new Set();
|
|
1180
|
+
}
|
|
1181
|
+
return /* @__PURE__ */ new Set([value]);
|
|
1182
|
+
}
|
|
1183
|
+
return new Set(value.filter((t) => INDIVIDUAL_TOOLS.has(t)));
|
|
1184
|
+
}
|
|
1185
|
+
|
|
959
1186
|
// src/agent/tool.ts
|
|
960
1187
|
function functionTool(fn) {
|
|
961
1188
|
return fn;
|
|
@@ -1164,6 +1391,15 @@ var ClawOpsAgent = class {
|
|
|
1164
1391
|
_recording;
|
|
1165
1392
|
_recordingPath;
|
|
1166
1393
|
_activeSessions = /* @__PURE__ */ new Map();
|
|
1394
|
+
_builtinTools;
|
|
1395
|
+
_passiveDtmfDebounceMs;
|
|
1396
|
+
_passiveDtmfBuffer = [];
|
|
1397
|
+
_passiveDtmfTimer = null;
|
|
1398
|
+
_passiveDtmfCallId = null;
|
|
1399
|
+
_callSessions = /* @__PURE__ */ new Map();
|
|
1400
|
+
_log;
|
|
1401
|
+
_pipelineLog;
|
|
1402
|
+
_isPipelineSession = false;
|
|
1167
1403
|
constructor(options) {
|
|
1168
1404
|
this._apiKey = options.apiKey ?? process.env["CLAWOPS_API_KEY"] ?? "";
|
|
1169
1405
|
this._accountId = options.accountId ?? process.env["CLAWOPS_ACCOUNT_ID"] ?? "";
|
|
@@ -1173,9 +1409,14 @@ var ClawOpsAgent = class {
|
|
|
1173
1409
|
this._recording = options.recording ?? false;
|
|
1174
1410
|
this._recordingPath = options.recordingPath ?? "./recordings";
|
|
1175
1411
|
this._mcpServers = options.mcpServers ?? [];
|
|
1412
|
+
this._builtinTools = resolveBuiltinTools(options.builtinTools ?? "all" /* ALL */);
|
|
1413
|
+
this._passiveDtmfDebounceMs = options.passiveDtmfDebounceMs ?? 500;
|
|
1176
1414
|
if (options.tracing) {
|
|
1177
1415
|
setTracingConfig(options.tracing);
|
|
1178
1416
|
}
|
|
1417
|
+
this._log = createAgentLogger(options.logger);
|
|
1418
|
+
this._pipelineLog = createPipelineLogger(this._log);
|
|
1419
|
+
this._isPipelineSession = "_stt" in this._session && "_llm" in this._session;
|
|
1179
1420
|
}
|
|
1180
1421
|
/**
|
|
1181
1422
|
* Register a function tool.
|
|
@@ -1187,7 +1428,9 @@ var ClawOpsAgent = class {
|
|
|
1187
1428
|
tool(nameOrTool, description, parameters, handler) {
|
|
1188
1429
|
if (typeof nameOrTool === "string") {
|
|
1189
1430
|
if (!description || !parameters || !handler) {
|
|
1190
|
-
throw new chunk6IQN5RQD_cjs.AgentError(
|
|
1431
|
+
throw new chunk6IQN5RQD_cjs.AgentError(
|
|
1432
|
+
"tool(name, description, parameters, handler) requires all arguments."
|
|
1433
|
+
);
|
|
1191
1434
|
}
|
|
1192
1435
|
this._tools.register({
|
|
1193
1436
|
name: nameOrTool,
|
|
@@ -1223,7 +1466,9 @@ var ClawOpsAgent = class {
|
|
|
1223
1466
|
throw new chunk6IQN5RQD_cjs.AgentError("API key is required. Set CLAWOPS_API_KEY or pass apiKey option.");
|
|
1224
1467
|
}
|
|
1225
1468
|
if (!this._accountId) {
|
|
1226
|
-
throw new chunk6IQN5RQD_cjs.AgentError(
|
|
1469
|
+
throw new chunk6IQN5RQD_cjs.AgentError(
|
|
1470
|
+
"Account ID is required. Set CLAWOPS_ACCOUNT_ID or pass accountId option."
|
|
1471
|
+
);
|
|
1227
1472
|
}
|
|
1228
1473
|
this._controlWs = new ControlWebSocket({
|
|
1229
1474
|
baseUrl: this._baseUrl,
|
|
@@ -1231,6 +1476,7 @@ var ClawOpsAgent = class {
|
|
|
1231
1476
|
accountId: this._accountId,
|
|
1232
1477
|
number: this._fromNumber
|
|
1233
1478
|
});
|
|
1479
|
+
this._controlWs.setLogger(this._log);
|
|
1234
1480
|
this._controlWs.on("call.incoming", (event) => this._handleIncoming(event));
|
|
1235
1481
|
this._controlWs.on("call.ended", (event) => this._handleEnded(event));
|
|
1236
1482
|
this._controlWs.on("call.outbound_ready", (event) => this._handleOutboundReady(event));
|
|
@@ -1244,7 +1490,7 @@ var ClawOpsAgent = class {
|
|
|
1244
1490
|
`Failed to connect to ClawOps: ${err instanceof Error ? err.message : String(err)}`
|
|
1245
1491
|
);
|
|
1246
1492
|
}
|
|
1247
|
-
|
|
1493
|
+
this._log.info("ClawOpsAgent connected on %s", this._fromNumber);
|
|
1248
1494
|
}
|
|
1249
1495
|
/**
|
|
1250
1496
|
* Connect and block until disconnected.
|
|
@@ -1270,7 +1516,8 @@ var ClawOpsAgent = class {
|
|
|
1270
1516
|
session._markEnded();
|
|
1271
1517
|
}
|
|
1272
1518
|
this._activeSessions.clear();
|
|
1273
|
-
|
|
1519
|
+
this._callSessions.clear();
|
|
1520
|
+
this._log.info("ClawOpsAgent disconnected");
|
|
1274
1521
|
}
|
|
1275
1522
|
/**
|
|
1276
1523
|
* Initiate an outbound call.
|
|
@@ -1305,8 +1552,9 @@ var ClawOpsAgent = class {
|
|
|
1305
1552
|
callSession.on(evt, handler);
|
|
1306
1553
|
}
|
|
1307
1554
|
}
|
|
1555
|
+
callSession.setLogger(this._log);
|
|
1308
1556
|
this._activeSessions.set(callSession.callId, callSession);
|
|
1309
|
-
|
|
1557
|
+
this._log.info("Outbound call initiated: %s -> %s (%s)", this._fromNumber, to, callSession.callId);
|
|
1310
1558
|
return callSession;
|
|
1311
1559
|
}
|
|
1312
1560
|
_handleIncoming(event) {
|
|
@@ -1325,13 +1573,15 @@ var ClawOpsAgent = class {
|
|
|
1325
1573
|
session.on(evt, handler);
|
|
1326
1574
|
}
|
|
1327
1575
|
}
|
|
1576
|
+
session.setLogger(this._log);
|
|
1328
1577
|
this._activeSessions.set(callId, session);
|
|
1578
|
+
this._log.info("Incoming call: %s -> %s (%s)", fromNumber, this._fromNumber, callId);
|
|
1329
1579
|
if (this._controlWs) {
|
|
1330
1580
|
this._controlWs.send({ event: "call.accept", callId });
|
|
1331
1581
|
}
|
|
1332
1582
|
if (mediaUrl) {
|
|
1333
1583
|
this._startCallSession(session, mediaUrl).catch((err) => {
|
|
1334
|
-
|
|
1584
|
+
this._log.error({ err }, "Call session error: %s", callId);
|
|
1335
1585
|
});
|
|
1336
1586
|
}
|
|
1337
1587
|
}
|
|
@@ -1339,6 +1589,7 @@ var ClawOpsAgent = class {
|
|
|
1339
1589
|
const callId = event["callId"];
|
|
1340
1590
|
const session = this._activeSessions.get(callId);
|
|
1341
1591
|
if (session) {
|
|
1592
|
+
this._log.info("Call ended (server): %s", callId);
|
|
1342
1593
|
session._markEnded();
|
|
1343
1594
|
this._activeSessions.delete(callId);
|
|
1344
1595
|
}
|
|
@@ -1355,6 +1606,7 @@ var ClawOpsAgent = class {
|
|
|
1355
1606
|
accountId: this._accountId,
|
|
1356
1607
|
direction: "outbound"
|
|
1357
1608
|
});
|
|
1609
|
+
session.setLogger(this._log);
|
|
1358
1610
|
for (const [evt, handlers] of this._handlers) {
|
|
1359
1611
|
for (const handler of handlers) {
|
|
1360
1612
|
session.on(evt, handler);
|
|
@@ -1363,8 +1615,9 @@ var ClawOpsAgent = class {
|
|
|
1363
1615
|
this._activeSessions.set(callId, session);
|
|
1364
1616
|
}
|
|
1365
1617
|
if (mediaUrl) {
|
|
1618
|
+
this._log.info("Outbound call answered: %s -> %s (%s)", this._fromNumber, session.toNumber, callId);
|
|
1366
1619
|
this._startCallSession(session, mediaUrl).catch((err) => {
|
|
1367
|
-
|
|
1620
|
+
this._log.error({ err }, "Call session error: %s", callId);
|
|
1368
1621
|
});
|
|
1369
1622
|
}
|
|
1370
1623
|
}
|
|
@@ -1372,18 +1625,43 @@ var ClawOpsAgent = class {
|
|
|
1372
1625
|
const callId = event["callId"];
|
|
1373
1626
|
const session = this._activeSessions.get(callId);
|
|
1374
1627
|
if (session) {
|
|
1375
|
-
|
|
1628
|
+
this._log.info("Outbound call ringing: %s", callId);
|
|
1376
1629
|
}
|
|
1377
1630
|
}
|
|
1378
1631
|
_handleFailed(event) {
|
|
1379
1632
|
const callId = event["callId"];
|
|
1380
1633
|
const session = this._activeSessions.get(callId);
|
|
1381
1634
|
if (session) {
|
|
1635
|
+
this._log.info("Outbound call failed: %s (%s)", callId, event["reason"] ?? "failed");
|
|
1382
1636
|
session._emit("call_failed", event["reason"] ?? "failed");
|
|
1383
1637
|
session._markEnded();
|
|
1384
1638
|
this._activeSessions.delete(callId);
|
|
1385
1639
|
}
|
|
1386
1640
|
}
|
|
1641
|
+
_onDtmfEvent(callSession, digit) {
|
|
1642
|
+
callSession._emit("dtmf", digit);
|
|
1643
|
+
callSession._routeDtmf(digit);
|
|
1644
|
+
if (callSession._dtmfCollectorActive) {
|
|
1645
|
+
callSession.clearAudio();
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
this._passiveDtmfBuffer.push(digit);
|
|
1649
|
+
this._passiveDtmfCallId = callSession.callId;
|
|
1650
|
+
if (this._passiveDtmfTimer) {
|
|
1651
|
+
clearTimeout(this._passiveDtmfTimer);
|
|
1652
|
+
}
|
|
1653
|
+
this._passiveDtmfTimer = setTimeout(() => {
|
|
1654
|
+
const digits = this._passiveDtmfBuffer.join("");
|
|
1655
|
+
this._passiveDtmfBuffer = [];
|
|
1656
|
+
const sessionHandler = this._passiveDtmfCallId ? this._callSessions.get(this._passiveDtmfCallId) : null;
|
|
1657
|
+
this._passiveDtmfCallId = null;
|
|
1658
|
+
if (digits && sessionHandler && sessionHandler.feedDtmf) {
|
|
1659
|
+
sessionHandler.feedDtmf(digits).catch((err) => {
|
|
1660
|
+
this._log.error({ err }, "DTMF feed error");
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
}, this._passiveDtmfDebounceMs);
|
|
1664
|
+
}
|
|
1387
1665
|
async _startCallSession(session, mediaWsUrl) {
|
|
1388
1666
|
await withSpan(
|
|
1389
1667
|
"clawops.call_session",
|
|
@@ -1398,22 +1676,25 @@ var ClawOpsAgent = class {
|
|
|
1398
1676
|
if (this._mcpServers.length > 0) {
|
|
1399
1677
|
for (const serverConfig of this._mcpServers) {
|
|
1400
1678
|
const client = new MCPClient();
|
|
1679
|
+
client.setLogger(this._log);
|
|
1401
1680
|
client.addServer("mcp", serverConfig);
|
|
1402
1681
|
try {
|
|
1403
1682
|
const tools = await client.connect();
|
|
1404
1683
|
sessionTools.registerMcpTools(tools);
|
|
1405
1684
|
mcpClients.push(client);
|
|
1406
1685
|
} catch (err) {
|
|
1407
|
-
|
|
1686
|
+
this._log.error({ err }, "MCP connection error");
|
|
1408
1687
|
}
|
|
1409
1688
|
}
|
|
1410
1689
|
}
|
|
1411
1690
|
let recorder = null;
|
|
1412
1691
|
if (this._recording) {
|
|
1413
1692
|
recorder = new AudioRecorder(this._recordingPath, session.callId);
|
|
1693
|
+
recorder.setLogger(this._log);
|
|
1414
1694
|
recorder.start();
|
|
1415
1695
|
}
|
|
1416
1696
|
const mediaWs = new MediaWebSocket();
|
|
1697
|
+
mediaWs.setLogger(this._log);
|
|
1417
1698
|
session._bindTransport(
|
|
1418
1699
|
(audio) => {
|
|
1419
1700
|
mediaWs.sendAudio(audio.toString("base64"));
|
|
@@ -1421,9 +1702,17 @@ var ClawOpsAgent = class {
|
|
|
1421
1702
|
() => {
|
|
1422
1703
|
mediaWs.sendClear();
|
|
1423
1704
|
},
|
|
1424
|
-
() => {
|
|
1705
|
+
async () => {
|
|
1706
|
+
await mediaWs.flush();
|
|
1707
|
+
const markName = `hangup-${Date.now()}`;
|
|
1708
|
+
mediaWs.sendMark(markName);
|
|
1709
|
+
await mediaWs.waitForMark(markName, 5e3);
|
|
1425
1710
|
mediaWs.close();
|
|
1426
|
-
}
|
|
1711
|
+
},
|
|
1712
|
+
async (digit) => {
|
|
1713
|
+
mediaWs.sendDtmf(digit);
|
|
1714
|
+
},
|
|
1715
|
+
() => mediaWs.isConnected
|
|
1427
1716
|
);
|
|
1428
1717
|
const sessionHandler = this._session;
|
|
1429
1718
|
if ("setToolRegistry" in sessionHandler && typeof sessionHandler.setToolRegistry === "function") {
|
|
@@ -1432,6 +1721,13 @@ var ClawOpsAgent = class {
|
|
|
1432
1721
|
if (recorder && "setRecorder" in sessionHandler && typeof sessionHandler.setRecorder === "function") {
|
|
1433
1722
|
sessionHandler.setRecorder(recorder);
|
|
1434
1723
|
}
|
|
1724
|
+
if ("setBuiltinTools" in sessionHandler && typeof sessionHandler.setBuiltinTools === "function") {
|
|
1725
|
+
sessionHandler.setBuiltinTools(this._builtinTools);
|
|
1726
|
+
}
|
|
1727
|
+
if ("setLogger" in sessionHandler && typeof sessionHandler.setLogger === "function") {
|
|
1728
|
+
sessionHandler.setLogger(this._isPipelineSession ? this._pipelineLog : this._log);
|
|
1729
|
+
}
|
|
1730
|
+
this._callSessions.set(session.callId, sessionHandler);
|
|
1435
1731
|
mediaWs.onAudio((ulawAudio, _timestamp) => {
|
|
1436
1732
|
if (sessionHandler) {
|
|
1437
1733
|
sessionHandler.feedAudio(ulawAudio);
|
|
@@ -1440,7 +1736,11 @@ var ClawOpsAgent = class {
|
|
|
1440
1736
|
recorder.writeInbound(ulawToPcm16(ulawAudio));
|
|
1441
1737
|
}
|
|
1442
1738
|
});
|
|
1739
|
+
mediaWs.onDtmf((digit) => {
|
|
1740
|
+
this._onDtmfEvent(session, digit);
|
|
1741
|
+
});
|
|
1443
1742
|
mediaWs.onClose(() => {
|
|
1743
|
+
this._log.info("Media stream stopped: %s", session.callId);
|
|
1444
1744
|
if (recorder) {
|
|
1445
1745
|
recorder.stop();
|
|
1446
1746
|
}
|
|
@@ -1449,11 +1749,12 @@ var ClawOpsAgent = class {
|
|
|
1449
1749
|
session._emit("call_start");
|
|
1450
1750
|
try {
|
|
1451
1751
|
await mediaWs.connect(mediaWsUrl, this._apiKey);
|
|
1752
|
+
this._log.info("Media stream started: %s", session.callId);
|
|
1452
1753
|
await sessionHandler.start(session, sessionTools);
|
|
1453
1754
|
await session.wait();
|
|
1454
1755
|
await sessionHandler.stop();
|
|
1455
1756
|
} catch (err) {
|
|
1456
|
-
|
|
1757
|
+
this._log.error({ err }, "Call session error: %s", session.callId);
|
|
1457
1758
|
} finally {
|
|
1458
1759
|
if (mcpClients.length > 0) {
|
|
1459
1760
|
sessionTools.clearMcpTools();
|
|
@@ -1468,6 +1769,7 @@ var ClawOpsAgent = class {
|
|
|
1468
1769
|
session._emit("call_end");
|
|
1469
1770
|
session._markEnded();
|
|
1470
1771
|
this._activeSessions.delete(session.callId);
|
|
1772
|
+
this._callSessions.delete(session.callId);
|
|
1471
1773
|
}
|
|
1472
1774
|
}
|
|
1473
1775
|
);
|
|
@@ -1480,7 +1782,40 @@ var HANG_UP_TOOL = {
|
|
|
1480
1782
|
type: "function",
|
|
1481
1783
|
name: "hang_up",
|
|
1482
1784
|
description: "End the phone call. Use when the conversation is finished or the caller says goodbye.",
|
|
1483
|
-
parameters: {
|
|
1785
|
+
parameters: {
|
|
1786
|
+
type: "object",
|
|
1787
|
+
properties: {},
|
|
1788
|
+
required: []
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
var COLLECT_DTMF_TOOL = {
|
|
1792
|
+
type: "function",
|
|
1793
|
+
name: "collect_dtmf",
|
|
1794
|
+
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.",
|
|
1795
|
+
parameters: {
|
|
1796
|
+
type: "object",
|
|
1797
|
+
properties: {
|
|
1798
|
+
max_digits: { type: "integer", description: "\uC218\uC9D1\uD560 \uCD5C\uB300 \uC790\uB9BF\uC218" },
|
|
1799
|
+
finish_on_key: { type: "string", description: "\uC785\uB825 \uC885\uB8CC \uD0A4 (\uAE30\uBCF8: #)" },
|
|
1800
|
+
timeout: { type: "integer", description: "\uC785\uB825 \uB300\uAE30 \uC2DC\uAC04(\uCD08, \uAE30\uBCF8: 5)" }
|
|
1801
|
+
},
|
|
1802
|
+
required: ["max_digits"]
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
var SEND_DTMF_TOOL = {
|
|
1806
|
+
type: "function",
|
|
1807
|
+
name: "send_dtmf",
|
|
1808
|
+
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.",
|
|
1809
|
+
parameters: {
|
|
1810
|
+
type: "object",
|
|
1811
|
+
properties: {
|
|
1812
|
+
digits: {
|
|
1813
|
+
type: "string",
|
|
1814
|
+
description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30."
|
|
1815
|
+
}
|
|
1816
|
+
},
|
|
1817
|
+
required: ["digits"]
|
|
1818
|
+
}
|
|
1484
1819
|
};
|
|
1485
1820
|
var OpenAIRealtime = class {
|
|
1486
1821
|
_apiKey;
|
|
@@ -1490,6 +1825,14 @@ var OpenAIRealtime = class {
|
|
|
1490
1825
|
_language;
|
|
1491
1826
|
_eagerness;
|
|
1492
1827
|
_greeting;
|
|
1828
|
+
_log = NOOP_LOGGER;
|
|
1829
|
+
_builtinTools = null;
|
|
1830
|
+
setLogger(logger) {
|
|
1831
|
+
this._log = logger;
|
|
1832
|
+
}
|
|
1833
|
+
setBuiltinTools(tools) {
|
|
1834
|
+
this._builtinTools = tools;
|
|
1835
|
+
}
|
|
1493
1836
|
_ws = null;
|
|
1494
1837
|
_call = null;
|
|
1495
1838
|
_tools = null;
|
|
@@ -1500,6 +1843,9 @@ var OpenAIRealtime = class {
|
|
|
1500
1843
|
_responseStartTs = null;
|
|
1501
1844
|
_sentAudioChunks = 0;
|
|
1502
1845
|
_audioRemainder = Buffer.alloc(0);
|
|
1846
|
+
// Response state tracking — prevent sending response.create while one is active
|
|
1847
|
+
_responseInProgress = false;
|
|
1848
|
+
_onResponseDone = null;
|
|
1503
1849
|
constructor(options = {}) {
|
|
1504
1850
|
this._apiKey = options.apiKey ?? process.env["OPENAI_API_KEY"] ?? "";
|
|
1505
1851
|
this._systemPrompt = options.systemPrompt ?? "";
|
|
@@ -1540,6 +1886,7 @@ var OpenAIRealtime = class {
|
|
|
1540
1886
|
const ws = this._ws;
|
|
1541
1887
|
ws.on("open", () => {
|
|
1542
1888
|
this._sendSessionUpdate();
|
|
1889
|
+
this._log.info("OpenAI Realtime connected");
|
|
1543
1890
|
if (this._greeting) {
|
|
1544
1891
|
this._send({ type: "response.create" });
|
|
1545
1892
|
}
|
|
@@ -1559,10 +1906,22 @@ var OpenAIRealtime = class {
|
|
|
1559
1906
|
if (!this._ws) {
|
|
1560
1907
|
reject(err);
|
|
1561
1908
|
}
|
|
1562
|
-
|
|
1909
|
+
this._log.error({ err }, "OpenAI Realtime WS error");
|
|
1563
1910
|
});
|
|
1564
1911
|
});
|
|
1565
1912
|
}
|
|
1913
|
+
async feedDtmf(digits) {
|
|
1914
|
+
await this._waitForResponseDone();
|
|
1915
|
+
this._send({
|
|
1916
|
+
type: "conversation.item.create",
|
|
1917
|
+
item: {
|
|
1918
|
+
type: "message",
|
|
1919
|
+
role: "user",
|
|
1920
|
+
content: [{ type: "input_text", text: `[DTMF \uC785\uB825: ${digits}]` }]
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
this._send({ type: "response.create" });
|
|
1924
|
+
}
|
|
1566
1925
|
feedAudio(audio) {
|
|
1567
1926
|
if (this._ws && this._ws.readyState === 1 && !this._closed) {
|
|
1568
1927
|
this._send({
|
|
@@ -1581,7 +1940,12 @@ var OpenAIRealtime = class {
|
|
|
1581
1940
|
_sendSessionUpdate() {
|
|
1582
1941
|
if (!this._ws || this._ws.readyState !== 1) return;
|
|
1583
1942
|
const toolSchemas = this._tools ? this._tools.toOpenAITools().map((t) => ({ type: "function", ...t.function })) : [];
|
|
1584
|
-
|
|
1943
|
+
if (!this._builtinTools || this._builtinTools.has("hang_up" /* HANG_UP */))
|
|
1944
|
+
toolSchemas.push(HANG_UP_TOOL);
|
|
1945
|
+
if (!this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */))
|
|
1946
|
+
toolSchemas.push(COLLECT_DTMF_TOOL);
|
|
1947
|
+
if (!this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */))
|
|
1948
|
+
toolSchemas.push(SEND_DTMF_TOOL);
|
|
1585
1949
|
this._send({
|
|
1586
1950
|
type: "session.update",
|
|
1587
1951
|
session: {
|
|
@@ -1591,7 +1955,7 @@ var OpenAIRealtime = class {
|
|
|
1591
1955
|
input_audio_format: "g711_ulaw",
|
|
1592
1956
|
output_audio_format: "g711_ulaw",
|
|
1593
1957
|
input_audio_transcription: {
|
|
1594
|
-
model: "
|
|
1958
|
+
model: "whisper-1",
|
|
1595
1959
|
language: this._language
|
|
1596
1960
|
},
|
|
1597
1961
|
input_audio_noise_reduction: { type: "far_field" },
|
|
@@ -1648,8 +2012,21 @@ var OpenAIRealtime = class {
|
|
|
1648
2012
|
}
|
|
1649
2013
|
break;
|
|
1650
2014
|
}
|
|
2015
|
+
case "response.created": {
|
|
2016
|
+
this._responseInProgress = true;
|
|
2017
|
+
break;
|
|
2018
|
+
}
|
|
2019
|
+
case "response.done": {
|
|
2020
|
+
this._responseInProgress = false;
|
|
2021
|
+
if (this._onResponseDone) {
|
|
2022
|
+
const cb = this._onResponseDone;
|
|
2023
|
+
this._onResponseDone = null;
|
|
2024
|
+
cb();
|
|
2025
|
+
}
|
|
2026
|
+
break;
|
|
2027
|
+
}
|
|
1651
2028
|
case "error": {
|
|
1652
|
-
|
|
2029
|
+
this._log.error({ apiError: msg["error"] }, "OpenAI error");
|
|
1653
2030
|
break;
|
|
1654
2031
|
}
|
|
1655
2032
|
}
|
|
@@ -1699,14 +2076,64 @@ var OpenAIRealtime = class {
|
|
|
1699
2076
|
async _handleToolCall(item) {
|
|
1700
2077
|
const funcName = item["name"];
|
|
1701
2078
|
const callId = item["call_id"];
|
|
2079
|
+
this._log.info("Tool call: %s", funcName);
|
|
1702
2080
|
if (funcName === "hang_up") {
|
|
1703
2081
|
if (this._call) {
|
|
1704
|
-
this._call.hangup();
|
|
2082
|
+
await this._call.hangup();
|
|
2083
|
+
}
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
if (funcName === "collect_dtmf") {
|
|
2087
|
+
if (this._call) {
|
|
2088
|
+
let result2;
|
|
2089
|
+
try {
|
|
2090
|
+
const args = JSON.parse(item["arguments"] ?? "{}");
|
|
2091
|
+
result2 = await this._call.collectDtmf({
|
|
2092
|
+
maxDigits: args["max_digits"] ?? 4,
|
|
2093
|
+
finishOnKey: args["finish_on_key"] ?? "#",
|
|
2094
|
+
timeout: args["timeout"] ?? 5
|
|
2095
|
+
});
|
|
2096
|
+
} catch (err) {
|
|
2097
|
+
result2 = `Error: ${err}`;
|
|
2098
|
+
}
|
|
2099
|
+
await this._waitForResponseDone();
|
|
2100
|
+
this._send({
|
|
2101
|
+
type: "conversation.item.create",
|
|
2102
|
+
item: {
|
|
2103
|
+
type: "function_call_output",
|
|
2104
|
+
call_id: callId,
|
|
2105
|
+
output: result2 || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)"
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
this._send({ type: "response.create" });
|
|
2109
|
+
}
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
if (funcName === "send_dtmf") {
|
|
2113
|
+
if (this._call) {
|
|
2114
|
+
let result2;
|
|
2115
|
+
try {
|
|
2116
|
+
const args = JSON.parse(item["arguments"] ?? "{}");
|
|
2117
|
+
await this._call.sendDtmfSequence(args["digits"] ?? "");
|
|
2118
|
+
result2 = "sent";
|
|
2119
|
+
} catch (err) {
|
|
2120
|
+
result2 = `Error: ${err}`;
|
|
2121
|
+
}
|
|
2122
|
+
await this._waitForResponseDone();
|
|
2123
|
+
this._send({
|
|
2124
|
+
type: "conversation.item.create",
|
|
2125
|
+
item: {
|
|
2126
|
+
type: "function_call_output",
|
|
2127
|
+
call_id: callId,
|
|
2128
|
+
output: result2
|
|
2129
|
+
}
|
|
2130
|
+
});
|
|
2131
|
+
this._send({ type: "response.create" });
|
|
1705
2132
|
}
|
|
1706
2133
|
return;
|
|
1707
2134
|
}
|
|
1708
2135
|
if (!this._tools || !this._tools.has(funcName)) {
|
|
1709
|
-
|
|
2136
|
+
this._log.error("Unknown tool: %s", funcName);
|
|
1710
2137
|
return;
|
|
1711
2138
|
}
|
|
1712
2139
|
let result;
|
|
@@ -1714,9 +2141,10 @@ var OpenAIRealtime = class {
|
|
|
1714
2141
|
const args = JSON.parse(item["arguments"] ?? "{}");
|
|
1715
2142
|
result = await this._tools.call(funcName, args);
|
|
1716
2143
|
} catch (err) {
|
|
1717
|
-
|
|
2144
|
+
this._log.error({ err }, "Tool call failed: %s", funcName);
|
|
1718
2145
|
result = `Error: ${err}`;
|
|
1719
2146
|
}
|
|
2147
|
+
await this._waitForResponseDone();
|
|
1720
2148
|
this._send({
|
|
1721
2149
|
type: "conversation.item.create",
|
|
1722
2150
|
item: {
|
|
@@ -1727,6 +2155,12 @@ var OpenAIRealtime = class {
|
|
|
1727
2155
|
});
|
|
1728
2156
|
this._send({ type: "response.create" });
|
|
1729
2157
|
}
|
|
2158
|
+
_waitForResponseDone() {
|
|
2159
|
+
if (!this._responseInProgress) return Promise.resolve();
|
|
2160
|
+
return new Promise((resolve) => {
|
|
2161
|
+
this._onResponseDone = resolve;
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
1730
2164
|
_send(data) {
|
|
1731
2165
|
if (this._ws && this._ws.readyState === 1 && !this._closed) {
|
|
1732
2166
|
this._ws.send(JSON.stringify(data));
|
|
@@ -1740,6 +2174,33 @@ var HANG_UP_TOOL2 = {
|
|
|
1740
2174
|
description: "End the phone call. Use when the conversation is finished or the caller says goodbye.",
|
|
1741
2175
|
parameters: { type: "object", properties: {} }
|
|
1742
2176
|
};
|
|
2177
|
+
var COLLECT_DTMF_TOOL2 = {
|
|
2178
|
+
name: "collect_dtmf",
|
|
2179
|
+
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.",
|
|
2180
|
+
parameters: {
|
|
2181
|
+
type: "object",
|
|
2182
|
+
properties: {
|
|
2183
|
+
max_digits: { type: "integer", description: "\uC218\uC9D1\uD560 \uCD5C\uB300 \uC790\uB9BF\uC218" },
|
|
2184
|
+
finish_on_key: { type: "string", description: "\uC785\uB825 \uC885\uB8CC \uD0A4 (\uAE30\uBCF8: #)" },
|
|
2185
|
+
timeout: { type: "integer", description: "\uC785\uB825 \uB300\uAE30 \uC2DC\uAC04(\uCD08, \uAE30\uBCF8: 5)" }
|
|
2186
|
+
},
|
|
2187
|
+
required: ["max_digits"]
|
|
2188
|
+
}
|
|
2189
|
+
};
|
|
2190
|
+
var SEND_DTMF_TOOL2 = {
|
|
2191
|
+
name: "send_dtmf",
|
|
2192
|
+
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.",
|
|
2193
|
+
parameters: {
|
|
2194
|
+
type: "object",
|
|
2195
|
+
properties: {
|
|
2196
|
+
digits: {
|
|
2197
|
+
type: "string",
|
|
2198
|
+
description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30."
|
|
2199
|
+
}
|
|
2200
|
+
},
|
|
2201
|
+
required: ["digits"]
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
1743
2204
|
function resolveRef(ref, defs) {
|
|
1744
2205
|
const parts = ref.replace(/^#\//, "").split("/");
|
|
1745
2206
|
let result = defs;
|
|
@@ -1802,7 +2263,11 @@ function sanitizeSchemaForGemini(schema, defs, depth = 0) {
|
|
|
1802
2263
|
result["properties"] = props;
|
|
1803
2264
|
}
|
|
1804
2265
|
if (schema["items"] && typeof schema["items"] === "object" && !Array.isArray(schema["items"])) {
|
|
1805
|
-
result["items"] = sanitizeSchemaForGemini(
|
|
2266
|
+
result["items"] = sanitizeSchemaForGemini(
|
|
2267
|
+
schema["items"],
|
|
2268
|
+
defs,
|
|
2269
|
+
depth + 1
|
|
2270
|
+
);
|
|
1806
2271
|
}
|
|
1807
2272
|
if (!result["type"] && result["properties"]) result["type"] = "object";
|
|
1808
2273
|
if (result["type"] === "object" && !result["properties"]) result["properties"] = {};
|
|
@@ -1823,6 +2288,9 @@ var GeminiRealtime = class {
|
|
|
1823
2288
|
_closed = false;
|
|
1824
2289
|
_sentAudioChunks = 0;
|
|
1825
2290
|
_audioRemainder = Buffer.alloc(0);
|
|
2291
|
+
_builtinTools = null;
|
|
2292
|
+
_toolCallInProgress = false;
|
|
2293
|
+
_log = NOOP_LOGGER;
|
|
1826
2294
|
constructor(options = {}) {
|
|
1827
2295
|
this._apiKey = options.apiKey ?? process.env["GOOGLE_API_KEY"] ?? "";
|
|
1828
2296
|
this._systemPrompt = options.systemPrompt ?? "";
|
|
@@ -1839,6 +2307,12 @@ var GeminiRealtime = class {
|
|
|
1839
2307
|
setRecorder(recorder) {
|
|
1840
2308
|
this._recorder = recorder;
|
|
1841
2309
|
}
|
|
2310
|
+
setBuiltinTools(tools) {
|
|
2311
|
+
this._builtinTools = tools;
|
|
2312
|
+
}
|
|
2313
|
+
setLogger(logger) {
|
|
2314
|
+
this._log = logger;
|
|
2315
|
+
}
|
|
1842
2316
|
async start(callSession, tools) {
|
|
1843
2317
|
this._call = callSession;
|
|
1844
2318
|
if (tools) this._tools = tools;
|
|
@@ -1875,9 +2349,10 @@ var GeminiRealtime = class {
|
|
|
1875
2349
|
callbacks: {
|
|
1876
2350
|
onmessage: (msg) => this._handleMessage(msg),
|
|
1877
2351
|
onerror: (err) => {
|
|
1878
|
-
|
|
2352
|
+
this._log.error({ err }, "Gemini SDK error");
|
|
1879
2353
|
},
|
|
1880
|
-
onclose: () => {
|
|
2354
|
+
onclose: (ev) => {
|
|
2355
|
+
this._log.info({ code: ev?.code ?? "unknown" }, "Gemini connection closed");
|
|
1881
2356
|
this._closed = true;
|
|
1882
2357
|
}
|
|
1883
2358
|
}
|
|
@@ -1895,7 +2370,7 @@ var GeminiRealtime = class {
|
|
|
1895
2370
|
}
|
|
1896
2371
|
}
|
|
1897
2372
|
feedAudio(audio) {
|
|
1898
|
-
if (this._session && !this._closed) {
|
|
2373
|
+
if (this._session && !this._closed && !this._toolCallInProgress) {
|
|
1899
2374
|
const pcm8k = ulawToPcm16(audio);
|
|
1900
2375
|
if (this._recorder) {
|
|
1901
2376
|
this._recorder.writeInbound(pcm8k);
|
|
@@ -1909,6 +2384,14 @@ var GeminiRealtime = class {
|
|
|
1909
2384
|
});
|
|
1910
2385
|
}
|
|
1911
2386
|
}
|
|
2387
|
+
async feedDtmf(digits) {
|
|
2388
|
+
if (this._session) {
|
|
2389
|
+
this._session.sendClientContent({
|
|
2390
|
+
turns: [{ role: "user", parts: [{ text: `[DTMF \uC785\uB825: ${digits}]` }] }],
|
|
2391
|
+
turnComplete: true
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
1912
2395
|
async stop() {
|
|
1913
2396
|
this._closed = true;
|
|
1914
2397
|
if (this._session) {
|
|
@@ -1927,7 +2410,9 @@ var GeminiRealtime = class {
|
|
|
1927
2410
|
t.function.parameters ?? { type: "object", properties: {} }
|
|
1928
2411
|
)
|
|
1929
2412
|
})) : [];
|
|
1930
|
-
toolDefs.push(HANG_UP_TOOL2);
|
|
2413
|
+
if (!this._builtinTools || this._builtinTools.has("hang_up" /* HANG_UP */)) toolDefs.push(HANG_UP_TOOL2);
|
|
2414
|
+
if (!this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */)) toolDefs.push(COLLECT_DTMF_TOOL2);
|
|
2415
|
+
if (!this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */)) toolDefs.push(SEND_DTMF_TOOL2);
|
|
1931
2416
|
return toolDefs;
|
|
1932
2417
|
}
|
|
1933
2418
|
_handleMessage(msg) {
|
|
@@ -1947,9 +2432,11 @@ var GeminiRealtime = class {
|
|
|
1947
2432
|
}
|
|
1948
2433
|
}
|
|
1949
2434
|
if (serverContent.turnComplete) {
|
|
2435
|
+
this._log.debug("Turn complete");
|
|
1950
2436
|
this._flushAudioRemainder();
|
|
1951
2437
|
}
|
|
1952
2438
|
if (serverContent.interrupted) {
|
|
2439
|
+
this._log.info("Barge-in detected");
|
|
1953
2440
|
if (this._call) {
|
|
1954
2441
|
this._call.clearAudio();
|
|
1955
2442
|
}
|
|
@@ -1958,16 +2445,22 @@ var GeminiRealtime = class {
|
|
|
1958
2445
|
}
|
|
1959
2446
|
const inputText = serverContent.inputTranscription?.text;
|
|
1960
2447
|
if (inputText && this._call) {
|
|
2448
|
+
this._log.info("User: %s", inputText);
|
|
1961
2449
|
this._call._emit("transcript", "user", inputText);
|
|
1962
2450
|
}
|
|
1963
2451
|
const outputText = serverContent.outputTranscription?.text;
|
|
1964
2452
|
if (outputText && this._call) {
|
|
2453
|
+
this._log.info("Assistant: %s", outputText);
|
|
1965
2454
|
this._call._emit("transcript", "assistant", outputText);
|
|
1966
2455
|
}
|
|
1967
2456
|
}
|
|
1968
2457
|
if (msg.toolCall) {
|
|
1969
2458
|
this._handleToolCall(msg.toolCall);
|
|
1970
2459
|
}
|
|
2460
|
+
const toolCancellation = msg["toolCallCancellation"];
|
|
2461
|
+
if (toolCancellation) {
|
|
2462
|
+
this._log.info({ ids: toolCancellation.ids }, "Tool call cancelled");
|
|
2463
|
+
}
|
|
1971
2464
|
}
|
|
1972
2465
|
_handleAudioData(b64Data) {
|
|
1973
2466
|
if (!this._call) return;
|
|
@@ -1980,9 +2473,9 @@ var GeminiRealtime = class {
|
|
|
1980
2473
|
const combined = Buffer.concat([this._audioRemainder, ulaw]);
|
|
1981
2474
|
const chunkSize = 160;
|
|
1982
2475
|
const fullEnd = Math.floor(combined.length / chunkSize) * chunkSize;
|
|
1983
|
-
|
|
1984
|
-
this._call.sendAudio(combined.subarray(
|
|
1985
|
-
this._sentAudioChunks
|
|
2476
|
+
if (fullEnd > 0) {
|
|
2477
|
+
this._call.sendAudio(combined.subarray(0, fullEnd));
|
|
2478
|
+
this._sentAudioChunks += fullEnd / chunkSize;
|
|
1986
2479
|
}
|
|
1987
2480
|
this._audioRemainder = combined.subarray(fullEnd);
|
|
1988
2481
|
}
|
|
@@ -2000,31 +2493,75 @@ var GeminiRealtime = class {
|
|
|
2000
2493
|
async _handleToolCall(toolCall) {
|
|
2001
2494
|
const functionCalls = toolCall.functionCalls;
|
|
2002
2495
|
if (!functionCalls) return;
|
|
2496
|
+
this._toolCallInProgress = true;
|
|
2003
2497
|
const responses = [];
|
|
2004
2498
|
for (const fc of functionCalls) {
|
|
2005
2499
|
const name = fc.name ?? "";
|
|
2006
2500
|
const fcId = fc.id ?? "";
|
|
2007
2501
|
const args = fc.args ?? {};
|
|
2502
|
+
this._log.info({ tool: name, args }, "Tool call: %s", name);
|
|
2008
2503
|
if (name === "hang_up") {
|
|
2504
|
+
this._log.info("hang_up: ending call");
|
|
2009
2505
|
if (this._call) {
|
|
2010
|
-
this._call.hangup();
|
|
2506
|
+
await this._call.hangup();
|
|
2011
2507
|
}
|
|
2012
2508
|
return;
|
|
2013
2509
|
}
|
|
2510
|
+
if (name === "collect_dtmf") {
|
|
2511
|
+
if (this._call) {
|
|
2512
|
+
let result;
|
|
2513
|
+
try {
|
|
2514
|
+
this._log.info({ maxDigits: args["max_digits"] ?? 4, timeout: args["timeout"] ?? 5 }, "collect_dtmf: waiting for digits");
|
|
2515
|
+
result = await this._call.collectDtmf({
|
|
2516
|
+
maxDigits: args["max_digits"] ?? 4,
|
|
2517
|
+
finishOnKey: args["finish_on_key"] ?? "#",
|
|
2518
|
+
timeout: args["timeout"] ?? 5
|
|
2519
|
+
});
|
|
2520
|
+
this._log.info("DTMF collected: %s", result || "(empty)");
|
|
2521
|
+
} catch (err) {
|
|
2522
|
+
this._log.error({ err }, "collect_dtmf error");
|
|
2523
|
+
result = `Error: ${err}`;
|
|
2524
|
+
}
|
|
2525
|
+
responses.push({
|
|
2526
|
+
id: fcId,
|
|
2527
|
+
name,
|
|
2528
|
+
response: { result: result || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)" }
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
continue;
|
|
2532
|
+
}
|
|
2533
|
+
if (name === "send_dtmf") {
|
|
2534
|
+
if (this._call) {
|
|
2535
|
+
let result;
|
|
2536
|
+
try {
|
|
2537
|
+
this._log.info('send_dtmf: digits="%s"', args["digits"] ?? "");
|
|
2538
|
+
await this._call.sendDtmfSequence(args["digits"] ?? "");
|
|
2539
|
+
result = "sent";
|
|
2540
|
+
this._log.info("send_dtmf: sent");
|
|
2541
|
+
} catch (err) {
|
|
2542
|
+
this._log.error({ err }, "send_dtmf error");
|
|
2543
|
+
result = `Error: ${err}`;
|
|
2544
|
+
}
|
|
2545
|
+
responses.push({ id: fcId, name, response: { result } });
|
|
2546
|
+
}
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2014
2549
|
if (!this._tools || !this._tools.has(name)) {
|
|
2015
|
-
|
|
2550
|
+
this._log.error("Unknown tool: %s", name);
|
|
2016
2551
|
responses.push({ id: fcId, name, response: { error: `Unknown tool: ${name}` } });
|
|
2017
2552
|
continue;
|
|
2018
2553
|
}
|
|
2019
2554
|
try {
|
|
2020
2555
|
const result = await this._tools.call(name, args);
|
|
2556
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
2557
|
+
this._log.info("Tool result: %s -> %s", name, resultStr.substring(0, 200));
|
|
2021
2558
|
responses.push({
|
|
2022
2559
|
id: fcId,
|
|
2023
2560
|
name,
|
|
2024
|
-
response: { result:
|
|
2561
|
+
response: { result: resultStr }
|
|
2025
2562
|
});
|
|
2026
2563
|
} catch (err) {
|
|
2027
|
-
|
|
2564
|
+
this._log.error({ err }, "Tool call failed: %s", name);
|
|
2028
2565
|
responses.push({
|
|
2029
2566
|
id: fcId,
|
|
2030
2567
|
name,
|
|
@@ -2032,15 +2569,41 @@ var GeminiRealtime = class {
|
|
|
2032
2569
|
});
|
|
2033
2570
|
}
|
|
2034
2571
|
}
|
|
2035
|
-
if (this._session) {
|
|
2572
|
+
if (responses.length > 0 && this._session) {
|
|
2573
|
+
this._log.debug("Sending %d tool response(s)", responses.length);
|
|
2036
2574
|
this._session.sendToolResponse({
|
|
2037
2575
|
functionResponses: responses
|
|
2038
2576
|
});
|
|
2039
2577
|
}
|
|
2578
|
+
this._toolCallInProgress = false;
|
|
2040
2579
|
}
|
|
2041
2580
|
};
|
|
2042
2581
|
|
|
2043
2582
|
// src/agent/pipeline/pipeline-session.ts
|
|
2583
|
+
var COLLECT_DTMF_TOOL3 = {
|
|
2584
|
+
function: {
|
|
2585
|
+
description: "\uC0AC\uC6A9\uC790\uB85C\uBD80\uD130 DTMF(\uC804\uD654 \uD0A4\uD328\uB4DC) \uC785\uB825\uC744 \uC218\uC9D1\uD569\uB2C8\uB2E4.",
|
|
2586
|
+
parameters: {
|
|
2587
|
+
properties: {
|
|
2588
|
+
max_digits: { type: "integer", description: "\uC218\uC9D1\uD560 \uCD5C\uB300 \uC790\uB9BF\uC218" },
|
|
2589
|
+
finish_on_key: { type: "string", description: "\uC785\uB825 \uC885\uB8CC \uD0A4 (\uAE30\uBCF8: #)" },
|
|
2590
|
+
timeout: { type: "integer", description: "\uC785\uB825 \uB300\uAE30 \uC2DC\uAC04(\uCD08, \uAE30\uBCF8: 5)" }
|
|
2591
|
+
},
|
|
2592
|
+
required: ["max_digits"]
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
};
|
|
2596
|
+
var SEND_DTMF_TOOL3 = {
|
|
2597
|
+
function: {
|
|
2598
|
+
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.",
|
|
2599
|
+
parameters: {
|
|
2600
|
+
properties: {
|
|
2601
|
+
digits: { type: "string", description: "\uC804\uC1A1\uD560 \uBC88\uD638 (0-9, *, #). 'w'\uB294 500ms \uB300\uAE30, 'W'\uB294 1000ms \uB300\uAE30." }
|
|
2602
|
+
},
|
|
2603
|
+
required: ["digits"]
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
};
|
|
2044
2607
|
var PipelineSession = class {
|
|
2045
2608
|
_stt;
|
|
2046
2609
|
_llm;
|
|
@@ -2059,6 +2622,8 @@ var PipelineSession = class {
|
|
|
2059
2622
|
_audioBuffer = [];
|
|
2060
2623
|
_running = false;
|
|
2061
2624
|
_speaking = false;
|
|
2625
|
+
_builtinTools = null;
|
|
2626
|
+
_log = NOOP_LOGGER;
|
|
2062
2627
|
constructor(options) {
|
|
2063
2628
|
this._stt = options.stt;
|
|
2064
2629
|
this._llm = options.llm;
|
|
@@ -2079,10 +2644,23 @@ var PipelineSession = class {
|
|
|
2079
2644
|
setRecorder(recorder) {
|
|
2080
2645
|
this._recorder = recorder;
|
|
2081
2646
|
}
|
|
2647
|
+
setBuiltinTools(tools) {
|
|
2648
|
+
this._builtinTools = tools;
|
|
2649
|
+
}
|
|
2650
|
+
setLogger(logger) {
|
|
2651
|
+
this._log = logger;
|
|
2652
|
+
if ("setLogger" in this._stt && typeof this._stt.setLogger === "function") {
|
|
2653
|
+
this._stt.setLogger(logger);
|
|
2654
|
+
}
|
|
2655
|
+
if ("setLogger" in this._tts && typeof this._tts.setLogger === "function") {
|
|
2656
|
+
this._tts.setLogger(logger);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2082
2659
|
async start(callSession, tools) {
|
|
2083
2660
|
this._callSession = callSession;
|
|
2084
2661
|
this._tools = tools ?? null;
|
|
2085
2662
|
this._running = true;
|
|
2663
|
+
this._log.info("PipelineSession started");
|
|
2086
2664
|
this._conversation = [];
|
|
2087
2665
|
if (this._systemPrompt) {
|
|
2088
2666
|
this._conversation.push({
|
|
@@ -2092,11 +2670,11 @@ var PipelineSession = class {
|
|
|
2092
2670
|
}
|
|
2093
2671
|
if (this._greeting) {
|
|
2094
2672
|
this._generateGreeting().catch((err) => {
|
|
2095
|
-
|
|
2673
|
+
this._log.error({ err }, "Greeting error");
|
|
2096
2674
|
});
|
|
2097
2675
|
}
|
|
2098
2676
|
this._runSttLoop().catch((err) => {
|
|
2099
|
-
|
|
2677
|
+
this._log.error({ err }, "STT loop error");
|
|
2100
2678
|
});
|
|
2101
2679
|
}
|
|
2102
2680
|
feedAudio(audio) {
|
|
@@ -2104,8 +2682,16 @@ var PipelineSession = class {
|
|
|
2104
2682
|
this._audioBuffer.push(audio);
|
|
2105
2683
|
}
|
|
2106
2684
|
}
|
|
2685
|
+
async feedDtmf(digits) {
|
|
2686
|
+
this._conversation.push({
|
|
2687
|
+
role: "user",
|
|
2688
|
+
content: `[DTMF \uC785\uB825: ${digits}]`
|
|
2689
|
+
});
|
|
2690
|
+
await this._respond();
|
|
2691
|
+
}
|
|
2107
2692
|
async stop() {
|
|
2108
2693
|
this._running = false;
|
|
2694
|
+
this._log.info("PipelineSession stopped");
|
|
2109
2695
|
this._audioBuffer = [];
|
|
2110
2696
|
}
|
|
2111
2697
|
async _runSttLoop() {
|
|
@@ -2119,8 +2705,10 @@ var PipelineSession = class {
|
|
|
2119
2705
|
if (this._callSession) {
|
|
2120
2706
|
this._callSession.clearAudio();
|
|
2121
2707
|
}
|
|
2708
|
+
this._log.info('Barge-in: "%s"', event.transcript.substring(0, 30));
|
|
2122
2709
|
}
|
|
2123
2710
|
if (event.type === "final" && event.transcript.trim()) {
|
|
2711
|
+
this._log.info("STT: %s", event.transcript);
|
|
2124
2712
|
await this._handleUserSpeech(event.transcript);
|
|
2125
2713
|
}
|
|
2126
2714
|
}
|
|
@@ -2145,11 +2733,37 @@ var PipelineSession = class {
|
|
|
2145
2733
|
this._conversation.push({ role: "user", content: transcript });
|
|
2146
2734
|
await this._respond();
|
|
2147
2735
|
}
|
|
2736
|
+
_buildEffectiveTools() {
|
|
2737
|
+
const includeCollectDtmf = !this._builtinTools || this._builtinTools.has("collect_dtmf" /* COLLECT_DTMF */);
|
|
2738
|
+
const includeSendDtmf = !this._builtinTools || this._builtinTools.has("send_dtmf" /* SEND_DTMF */);
|
|
2739
|
+
if (!includeCollectDtmf && !includeSendDtmf) return this._tools ?? void 0;
|
|
2740
|
+
const base = this._tools ? this._tools.fork() : new ToolRegistry();
|
|
2741
|
+
if (includeCollectDtmf) {
|
|
2742
|
+
base.register({
|
|
2743
|
+
name: "collect_dtmf",
|
|
2744
|
+
description: COLLECT_DTMF_TOOL3.function.description,
|
|
2745
|
+
parameters: COLLECT_DTMF_TOOL3.function.parameters.properties,
|
|
2746
|
+
required: COLLECT_DTMF_TOOL3.function.parameters.required,
|
|
2747
|
+
handler: async () => ""
|
|
2748
|
+
});
|
|
2749
|
+
}
|
|
2750
|
+
if (includeSendDtmf) {
|
|
2751
|
+
base.register({
|
|
2752
|
+
name: "send_dtmf",
|
|
2753
|
+
description: SEND_DTMF_TOOL3.function.description,
|
|
2754
|
+
parameters: SEND_DTMF_TOOL3.function.parameters.properties,
|
|
2755
|
+
required: SEND_DTMF_TOOL3.function.parameters.required,
|
|
2756
|
+
handler: async () => ""
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
return base;
|
|
2760
|
+
}
|
|
2148
2761
|
async _respond() {
|
|
2149
2762
|
let fullResponse = "";
|
|
2150
2763
|
const textChunks = [];
|
|
2764
|
+
const effectiveTools = this._buildEffectiveTools();
|
|
2151
2765
|
const llmStream = this._llm.generate(this._conversation, {
|
|
2152
|
-
tools:
|
|
2766
|
+
tools: effectiveTools,
|
|
2153
2767
|
temperature: this._temperature,
|
|
2154
2768
|
maxTokens: this._maxTokens
|
|
2155
2769
|
});
|
|
@@ -2163,15 +2777,44 @@ var PipelineSession = class {
|
|
|
2163
2777
|
}
|
|
2164
2778
|
}
|
|
2165
2779
|
if (fullResponse.trim()) {
|
|
2780
|
+
this._log.info("Assistant: %s", fullResponse.substring(0, 100));
|
|
2166
2781
|
this._conversation.push({ role: "assistant", content: fullResponse });
|
|
2167
2782
|
await this._synthesizeAndSend(fullResponse);
|
|
2168
2783
|
}
|
|
2169
2784
|
}
|
|
2170
2785
|
async _handleToolCall(chunk) {
|
|
2171
|
-
if (!chunk.toolCall
|
|
2786
|
+
if (!chunk.toolCall) return;
|
|
2172
2787
|
const { id, name, arguments: argsStr } = chunk.toolCall;
|
|
2173
2788
|
try {
|
|
2174
2789
|
const args = JSON.parse(argsStr);
|
|
2790
|
+
if (name === "collect_dtmf" && this._callSession) {
|
|
2791
|
+
let result2;
|
|
2792
|
+
try {
|
|
2793
|
+
result2 = await this._callSession.collectDtmf({
|
|
2794
|
+
maxDigits: args["max_digits"] ?? 4,
|
|
2795
|
+
finishOnKey: args["finish_on_key"] ?? "#",
|
|
2796
|
+
timeout: args["timeout"] ?? 5
|
|
2797
|
+
});
|
|
2798
|
+
} catch (err) {
|
|
2799
|
+
result2 = `Error: ${err}`;
|
|
2800
|
+
}
|
|
2801
|
+
this._conversation.push({ role: "tool", content: result2 || "(\uD0C0\uC784\uC544\uC6C3 - \uC785\uB825 \uC5C6\uC74C)", tool_call_id: id, name });
|
|
2802
|
+
await this._respond();
|
|
2803
|
+
return;
|
|
2804
|
+
}
|
|
2805
|
+
if (name === "send_dtmf" && this._callSession) {
|
|
2806
|
+
let result2;
|
|
2807
|
+
try {
|
|
2808
|
+
await this._callSession.sendDtmfSequence(args["digits"] ?? "");
|
|
2809
|
+
result2 = "sent";
|
|
2810
|
+
} catch (err) {
|
|
2811
|
+
result2 = `Error: ${err}`;
|
|
2812
|
+
}
|
|
2813
|
+
this._conversation.push({ role: "tool", content: result2, tool_call_id: id, name });
|
|
2814
|
+
await this._respond();
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
if (!this._tools) return;
|
|
2175
2818
|
const result = await this._tools.call(name, args);
|
|
2176
2819
|
this._conversation.push({
|
|
2177
2820
|
role: "assistant",
|
|
@@ -2184,9 +2827,10 @@ var PipelineSession = class {
|
|
|
2184
2827
|
tool_call_id: id,
|
|
2185
2828
|
name
|
|
2186
2829
|
});
|
|
2830
|
+
const effectiveTools = this._buildEffectiveTools();
|
|
2187
2831
|
let followUpText = "";
|
|
2188
2832
|
const followUpStream = this._llm.generate(this._conversation, {
|
|
2189
|
-
tools:
|
|
2833
|
+
tools: effectiveTools,
|
|
2190
2834
|
temperature: this._temperature,
|
|
2191
2835
|
maxTokens: this._maxTokens
|
|
2192
2836
|
});
|
|
@@ -2201,7 +2845,7 @@ var PipelineSession = class {
|
|
|
2201
2845
|
await this._synthesizeAndSend(followUpText);
|
|
2202
2846
|
}
|
|
2203
2847
|
} catch (err) {
|
|
2204
|
-
|
|
2848
|
+
this._log.error({ err }, "Tool call failed: %s", name);
|
|
2205
2849
|
}
|
|
2206
2850
|
}
|
|
2207
2851
|
async _synthesizeAndSend(text) {
|
|
@@ -2227,7 +2871,7 @@ var PipelineSession = class {
|
|
|
2227
2871
|
}
|
|
2228
2872
|
}
|
|
2229
2873
|
} catch (err) {
|
|
2230
|
-
|
|
2874
|
+
this._log.error({ err }, "TTS error");
|
|
2231
2875
|
} finally {
|
|
2232
2876
|
this._speaking = false;
|
|
2233
2877
|
}
|
|
@@ -2237,6 +2881,10 @@ var PipelineSession = class {
|
|
|
2237
2881
|
// src/agent/pipeline/deepgram-stt.ts
|
|
2238
2882
|
var DeepgramSTT = class {
|
|
2239
2883
|
_options;
|
|
2884
|
+
_log = NOOP_LOGGER;
|
|
2885
|
+
setLogger(logger) {
|
|
2886
|
+
this._log = logger;
|
|
2887
|
+
}
|
|
2240
2888
|
constructor(options = {}) {
|
|
2241
2889
|
this._options = {
|
|
2242
2890
|
model: "nova-3",
|
|
@@ -2306,7 +2954,7 @@ var DeepgramSTT = class {
|
|
|
2306
2954
|
}
|
|
2307
2955
|
});
|
|
2308
2956
|
ws.on("error", (err) => {
|
|
2309
|
-
|
|
2957
|
+
this._log.error({ err }, "Deepgram STT error");
|
|
2310
2958
|
done = true;
|
|
2311
2959
|
if (resolveWait) {
|
|
2312
2960
|
resolveWait();
|
|
@@ -2317,6 +2965,7 @@ var DeepgramSTT = class {
|
|
|
2317
2965
|
ws.on("open", resolve);
|
|
2318
2966
|
ws.on("error", reject);
|
|
2319
2967
|
});
|
|
2968
|
+
this._log.info("Deepgram STT connected");
|
|
2320
2969
|
const feedPromise = (async () => {
|
|
2321
2970
|
try {
|
|
2322
2971
|
for await (const chunk of audioStream) {
|
|
@@ -2353,6 +3002,10 @@ var DeepgramSTT = class {
|
|
|
2353
3002
|
// src/agent/pipeline/elevenlabs-tts.ts
|
|
2354
3003
|
var ElevenLabsTTS = class {
|
|
2355
3004
|
_options;
|
|
3005
|
+
_log = NOOP_LOGGER;
|
|
3006
|
+
setLogger(logger) {
|
|
3007
|
+
this._log = logger;
|
|
3008
|
+
}
|
|
2356
3009
|
constructor(options = {}) {
|
|
2357
3010
|
this._options = {
|
|
2358
3011
|
voiceId: "EXAVITQu4vr4xnSDxMaL",
|
|
@@ -2452,7 +3105,7 @@ var ElevenLabsTTS = class {
|
|
|
2452
3105
|
}
|
|
2453
3106
|
});
|
|
2454
3107
|
ws.on("error", (err) => {
|
|
2455
|
-
|
|
3108
|
+
this._log.error({ err }, "ElevenLabs TTS error");
|
|
2456
3109
|
done = true;
|
|
2457
3110
|
if (resolveWait) {
|
|
2458
3111
|
resolveWait();
|
|
@@ -2463,16 +3116,19 @@ var ElevenLabsTTS = class {
|
|
|
2463
3116
|
ws.on("open", resolve);
|
|
2464
3117
|
ws.on("error", reject);
|
|
2465
3118
|
});
|
|
3119
|
+
this._log.info("ElevenLabs TTS connected");
|
|
2466
3120
|
const feedPromise = (async () => {
|
|
2467
3121
|
try {
|
|
2468
3122
|
for await (const chunk of textStream) {
|
|
2469
3123
|
if (done) break;
|
|
2470
3124
|
if (ws.readyState === 1) {
|
|
3125
|
+
this._log.debug("ElevenLabs sending text: %s", chunk.substring(0, 60));
|
|
2471
3126
|
ws.send(JSON.stringify({ text: chunk }));
|
|
2472
3127
|
}
|
|
2473
3128
|
}
|
|
2474
3129
|
} finally {
|
|
2475
3130
|
if (ws.readyState === 1) {
|
|
3131
|
+
this._log.debug("ElevenLabs sending EOS");
|
|
2476
3132
|
ws.send(JSON.stringify({ text: "" }));
|
|
2477
3133
|
}
|
|
2478
3134
|
}
|
|
@@ -3012,6 +3668,7 @@ function mcpServerHTTP(options) {
|
|
|
3012
3668
|
|
|
3013
3669
|
exports.AnthropicLLM = AnthropicLLM;
|
|
3014
3670
|
exports.AudioRecorder = AudioRecorder;
|
|
3671
|
+
exports.BuiltinTool = BuiltinTool;
|
|
3015
3672
|
exports.CallSession = CallSession;
|
|
3016
3673
|
exports.ClawOpsAgent = ClawOpsAgent;
|
|
3017
3674
|
exports.DECODE_TABLE = DECODE_TABLE;
|
|
@@ -3033,6 +3690,8 @@ exports.PipelineSession = PipelineSession;
|
|
|
3033
3690
|
exports.TogetherLLM = TogetherLLM;
|
|
3034
3691
|
exports.ToolRegistry = ToolRegistry;
|
|
3035
3692
|
exports.XaiLLM = XaiLLM;
|
|
3693
|
+
exports.createAgentLogger = createAgentLogger;
|
|
3694
|
+
exports.createPipelineLogger = createPipelineLogger;
|
|
3036
3695
|
exports.functionTool = functionTool;
|
|
3037
3696
|
exports.getTracingConfig = getTracingConfig;
|
|
3038
3697
|
exports.mcpServerHTTP = mcpServerHTTP;
|