@guava-ai/guava-sdk 0.43.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/example-runner.ts +4 -0
- package/dist/bin/example-runner.js +4 -0
- package/dist/bin/example-runner.js.map +1 -1
- package/dist/examples/warm-transfer.d.ts +1 -0
- package/dist/examples/warm-transfer.js +111 -0
- package/dist/examples/warm-transfer.js.map +1 -0
- package/dist/src/agent.d.ts +5 -2
- package/dist/src/agent.js +114 -108
- package/dist/src/agent.js.map +1 -1
- package/dist/src/call.d.ts +82 -37
- package/dist/src/call.js +227 -91
- package/dist/src/call.js.map +1 -1
- package/dist/src/commands.d.ts +36 -0
- package/dist/src/commands.js +17 -1
- package/dist/src/commands.js.map +1 -1
- package/dist/src/events.d.ts +12 -0
- package/dist/src/events.js +7 -1
- package/dist/src/events.js.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/runner.js +12 -2
- package/dist/src/runner.js.map +1 -1
- package/dist/src/utils.d.ts +12 -0
- package/dist/src/utils.js +43 -1
- package/dist/src/utils.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/examples/README.md +1 -0
- package/examples/warm-transfer.ts +90 -0
- package/package.json +1 -1
- package/src/agent.ts +105 -45
- package/src/call.ts +207 -111
- package/src/commands.ts +22 -0
- package/src/events.ts +8 -0
- package/src/index.ts +2 -1
- package/src/runner.ts +11 -2
- package/src/utils.ts +48 -1
- package/src/version.ts +1 -1
package/src/agent.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type EscalateEvent,
|
|
19
19
|
type GuavaEvent,
|
|
20
20
|
type OutboundCallFailed,
|
|
21
|
+
type WebRTCMessageEvent,
|
|
21
22
|
decodeEventDict,
|
|
22
23
|
} from "./events.ts";
|
|
23
24
|
import {
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
type ServerMessage as DialerServerMessage,
|
|
26
27
|
decodeServerMessage as decodeDialerServerMessage,
|
|
27
28
|
} from "./guavadialer-events.ts";
|
|
28
|
-
import { HealthContext
|
|
29
|
+
import { HealthContext } from "./health.ts";
|
|
29
30
|
import { _generate } from "./helpers/llm.ts";
|
|
30
31
|
import { Call, Client } from "./index.ts";
|
|
31
32
|
import { type Logger, getDefaultLogger } from "./logging.ts";
|
|
@@ -33,6 +34,7 @@ import type { CallInfo } from "./socket/call-info.ts";
|
|
|
33
34
|
import { GuavaSocket, GuavaSocketClosedError } from "./socket/client.ts";
|
|
34
35
|
import * as ListenInbound from "./socket/listen-inbound.ts";
|
|
35
36
|
import { telemetryClient } from "./telemetry.ts";
|
|
37
|
+
import { Runner } from "./runner.ts";
|
|
36
38
|
import { runChat } from "./testing/chat.ts";
|
|
37
39
|
import { SessionStarted } from "./testing/protocol.ts";
|
|
38
40
|
import { TestSession } from "./testing/session.ts";
|
|
@@ -99,6 +101,7 @@ export class Agent {
|
|
|
99
101
|
private _onEscalate?: (call: Call, event: EscalateEvent) => Promise<void>;
|
|
100
102
|
private _onCallerDtmf?: (call: Call, event: DTMFPressedEvent) => Promise<void>;
|
|
101
103
|
private _onAgentDtmf?: (call: Call, event: AgentDTMFSentEvent) => Promise<void>;
|
|
104
|
+
private _onWebrtcMessage?: (call: Call, event: WebRTCMessageEvent) => Promise<void>;
|
|
102
105
|
private _onOutboundFailed?: (call: Call, event: OutboundCallFailed) => Promise<void>;
|
|
103
106
|
|
|
104
107
|
constructor(args?: {
|
|
@@ -217,6 +220,11 @@ export class Agent {
|
|
|
217
220
|
this._onAgentDtmf = callback;
|
|
218
221
|
}
|
|
219
222
|
|
|
223
|
+
/** Register a handler for messages sent by a connected WebRTC client. */
|
|
224
|
+
onWebrtcMessage(callback: (call: Call, event: WebRTCMessageEvent) => Promise<void>): void {
|
|
225
|
+
this._onWebrtcMessage = callback;
|
|
226
|
+
}
|
|
227
|
+
|
|
220
228
|
onOutboundFailed(callback: (call: Call, event: OutboundCallFailed) => Promise<void>): void {
|
|
221
229
|
this._onOutboundFailed = callback;
|
|
222
230
|
}
|
|
@@ -275,25 +283,15 @@ export class Agent {
|
|
|
275
283
|
}
|
|
276
284
|
|
|
277
285
|
async listenPhone(phoneNumber: string): Promise<void> {
|
|
278
|
-
|
|
279
|
-
await using _server = await getHealthServer(healthCtx);
|
|
280
|
-
return await this._listenInbound(healthCtx, { agent_number: phoneNumber });
|
|
286
|
+
await new Runner().listenPhone(this, phoneNumber).run();
|
|
281
287
|
}
|
|
282
288
|
|
|
283
289
|
async listenWebrtc(webrtcCode?: string): Promise<void> {
|
|
284
|
-
|
|
285
|
-
this._logger.info("No WebRTC code provided. Creating a temporary one.");
|
|
286
|
-
webrtcCode = await this._client.createWebrtcAgent(3600);
|
|
287
|
-
}
|
|
288
|
-
const healthCtx = new HealthContext();
|
|
289
|
-
await using _server = await getHealthServer(healthCtx);
|
|
290
|
-
return await this._listenInbound(healthCtx, { webrtc_code: webrtcCode });
|
|
290
|
+
await new Runner().listenWebrtc(this, webrtcCode).run();
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
async listenSip(sipCode: string): Promise<void> {
|
|
294
|
-
|
|
295
|
-
await using _server = await getHealthServer(healthCtx);
|
|
296
|
-
return await this._listenInbound(healthCtx, { sip_code: sipCode });
|
|
294
|
+
await new Runner().listenSip(this, sipCode).run();
|
|
297
295
|
}
|
|
298
296
|
|
|
299
297
|
async callLocal(variables: Record<string, any> = {}): Promise<void> {
|
|
@@ -511,6 +509,12 @@ export class Agent {
|
|
|
511
509
|
this._onAgentDtmf!(call, event),
|
|
512
510
|
);
|
|
513
511
|
}
|
|
512
|
+
} else if (event.event_type === "webrtc-message") {
|
|
513
|
+
if (this._onWebrtcMessage !== undefined) {
|
|
514
|
+
await this._invokeHandler(call, "onWebrtcMessage", false, () =>
|
|
515
|
+
this._onWebrtcMessage!(call, event),
|
|
516
|
+
);
|
|
517
|
+
}
|
|
514
518
|
} else if (event.event_type === "escalate") {
|
|
515
519
|
if (this._onEscalate !== undefined) {
|
|
516
520
|
await this._invokeHandler(call, "onEscalate", true, () => this._onEscalate!(call, event));
|
|
@@ -542,7 +546,7 @@ export class Agent {
|
|
|
542
546
|
callInfo: CallInfo,
|
|
543
547
|
variables: Record<string, any> = {},
|
|
544
548
|
): Promise<Call> {
|
|
545
|
-
const call = new Call(callId, callInfo
|
|
549
|
+
const call = new Call(callId, callInfo);
|
|
546
550
|
await call.setPersona({
|
|
547
551
|
agentName: this._name,
|
|
548
552
|
agentPurpose: this._purpose,
|
|
@@ -560,6 +564,9 @@ export class Agent {
|
|
|
560
564
|
accept_dtmf_for_numbers: this._acceptDtmfForNumbers,
|
|
561
565
|
has_on_agent_dtmf: this._onAgentDtmf !== undefined,
|
|
562
566
|
});
|
|
567
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
568
|
+
await call.setVariable(key, value);
|
|
569
|
+
}
|
|
563
570
|
if (this._onCallStart !== undefined) {
|
|
564
571
|
await this._onCallStart(call);
|
|
565
572
|
}
|
|
@@ -568,13 +575,10 @@ export class Agent {
|
|
|
568
575
|
|
|
569
576
|
async _attachToCall(
|
|
570
577
|
callId: string,
|
|
571
|
-
|
|
572
|
-
initialVariables: Record<string, any> = {},
|
|
578
|
+
call: Call,
|
|
573
579
|
testSession?: TestSession,
|
|
574
580
|
route: string = "v2/connect-call",
|
|
575
581
|
): Promise<void> {
|
|
576
|
-
const call = await this._initCall(callId, callInfo, initialVariables);
|
|
577
|
-
|
|
578
582
|
const url = new URL(`${route}/${callId}`, this._client.getWebsocketBase());
|
|
579
583
|
await using socket = await new GuavaSocket<Command, GuavaEvent | null>(
|
|
580
584
|
`call-connection-${callId}`,
|
|
@@ -712,8 +716,19 @@ export class Agent {
|
|
|
712
716
|
socket.send({ message_type: "decline-call", call_id: callId });
|
|
713
717
|
} else {
|
|
714
718
|
this._logger.info("Accepting call %s.", callId);
|
|
719
|
+
|
|
720
|
+
// Initialize the call, invoking onCallStart.
|
|
721
|
+
let call: Call;
|
|
722
|
+
try {
|
|
723
|
+
call = await this._initCall(callId, callInfo, initialVariables);
|
|
724
|
+
} catch (err) {
|
|
725
|
+
this._logger.error("Failed to initialize inbound call %s. Declining: %s", callId, err);
|
|
726
|
+
socket.send({ message_type: "decline-call", call_id: callId });
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
715
730
|
socket.send({ message_type: "answer-call", call_id: callId });
|
|
716
|
-
await this._attachToCall(callId,
|
|
731
|
+
await this._attachToCall(callId, call);
|
|
717
732
|
}
|
|
718
733
|
}
|
|
719
734
|
|
|
@@ -725,24 +740,51 @@ export class Agent {
|
|
|
725
740
|
toNumber: string,
|
|
726
741
|
variables: Record<string, any> = {},
|
|
727
742
|
): Promise<void> {
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
743
|
+
for (const [key, value] of Object.entries(variables)) {
|
|
744
|
+
if (!z.json().safeParse(value).success) {
|
|
745
|
+
throw new Error(`Variable '${key}' value is not JSON serializable: ${String(value)}`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const reserveUrl = new URL("v3/reserve-outbound", this._client.getHttpBase());
|
|
750
|
+
reserveUrl.searchParams.set("from_number", fromNumber);
|
|
751
|
+
reserveUrl.searchParams.set("to_number", toNumber);
|
|
731
752
|
|
|
732
|
-
const response = await fetchOrThrow(
|
|
753
|
+
const response = await fetchOrThrow(reserveUrl, {
|
|
733
754
|
method: "POST",
|
|
734
755
|
headers: await this._client.headers(),
|
|
735
756
|
});
|
|
736
757
|
const { call_id } = (await response.json()) as { call_id: string };
|
|
737
758
|
|
|
738
|
-
this._logger.info("Outbound call created with session ID: %s", call_id);
|
|
739
759
|
const callInfo: CallInfo = {
|
|
740
760
|
call_type: "pstn",
|
|
741
761
|
from_number: fromNumber ?? null,
|
|
742
762
|
to_number: toNumber,
|
|
743
763
|
caller_id: null,
|
|
744
764
|
};
|
|
745
|
-
|
|
765
|
+
|
|
766
|
+
// Initialize the call before dialing, invoking onCallStart.
|
|
767
|
+
let call: Call;
|
|
768
|
+
try {
|
|
769
|
+
call = await this._initCall(call_id, callInfo, variables);
|
|
770
|
+
} catch (err) {
|
|
771
|
+
this._logger.error(
|
|
772
|
+
"Failed to initialize outbound call %s. The call will not be dialed: %s",
|
|
773
|
+
call_id,
|
|
774
|
+
err,
|
|
775
|
+
);
|
|
776
|
+
throw err;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const createUrl = new URL("v3/create-outbound", this._client.getHttpBase());
|
|
780
|
+
createUrl.searchParams.set("call_id", call_id);
|
|
781
|
+
await fetchOrThrow(createUrl, {
|
|
782
|
+
method: "POST",
|
|
783
|
+
headers: await this._client.headers(),
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
this._logger.info("Outbound call created with session ID: %s", call_id);
|
|
787
|
+
await this._attachToCall(call_id, call);
|
|
746
788
|
}
|
|
747
789
|
|
|
748
790
|
/**
|
|
@@ -788,14 +830,20 @@ export class Agent {
|
|
|
788
830
|
to_number: "+15555555555",
|
|
789
831
|
caller_id: null,
|
|
790
832
|
};
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
variables
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
}
|
|
833
|
+
// Initialize the call, invoking onCallStart.
|
|
834
|
+
let call: Call;
|
|
835
|
+
try {
|
|
836
|
+
call = await this._initCall(sessionStarted.session_id, testCallInfo, variables);
|
|
837
|
+
} catch (err) {
|
|
838
|
+
ws.close();
|
|
839
|
+
throw err;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const attachPromise = this._attachToCall(sessionStarted.session_id, call, testSession).catch(
|
|
843
|
+
(err: unknown) => {
|
|
844
|
+
this._logger.error("Error in _attachToCall during test: %s", err);
|
|
845
|
+
},
|
|
846
|
+
);
|
|
799
847
|
|
|
800
848
|
try {
|
|
801
849
|
await callback(testSession);
|
|
@@ -917,6 +965,7 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
917
965
|
* affecting the original agent.
|
|
918
966
|
*/
|
|
919
967
|
patch(): Agent {
|
|
968
|
+
// Keep this up to date with the Agent constructor args.
|
|
920
969
|
const cloned = new Agent({
|
|
921
970
|
name: this._name,
|
|
922
971
|
organization: this._organization,
|
|
@@ -926,6 +975,8 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
926
975
|
acceptDtmf: this._acceptDtmfForNumbers,
|
|
927
976
|
speechSpeed: this._speechSpeed,
|
|
928
977
|
});
|
|
978
|
+
|
|
979
|
+
// Keep this up to date with the handlers on Agent.
|
|
929
980
|
cloned._onCallReceived = this._onCallReceived;
|
|
930
981
|
cloned._onCallStart = this._onCallStart;
|
|
931
982
|
cloned._onCallerSpeech = this._onCallerSpeech;
|
|
@@ -942,6 +993,7 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
942
993
|
cloned._onEscalate = this._onEscalate;
|
|
943
994
|
cloned._onCallerDtmf = this._onCallerDtmf;
|
|
944
995
|
cloned._onAgentDtmf = this._onAgentDtmf;
|
|
996
|
+
cloned._onWebrtcMessage = this._onWebrtcMessage;
|
|
945
997
|
cloned._onOutboundFailed = this._onOutboundFailed;
|
|
946
998
|
return cloned;
|
|
947
999
|
}
|
|
@@ -990,7 +1042,6 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
990
1042
|
// TODO: Possible memory leak.
|
|
991
1043
|
activeCalls.push(
|
|
992
1044
|
(async () => {
|
|
993
|
-
socket.send({ message_type: "controller-ready", call_id });
|
|
994
1045
|
const variables = (data?.data as Record<string, unknown>) ?? {};
|
|
995
1046
|
const campaignCallInfo: CallInfo = {
|
|
996
1047
|
call_type: "pstn",
|
|
@@ -998,13 +1049,24 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
998
1049
|
to_number: (data?.phone_number as string) ?? "",
|
|
999
1050
|
caller_id: null,
|
|
1000
1051
|
};
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1052
|
+
|
|
1053
|
+
// Initialize the call, invoking onCallStart.
|
|
1054
|
+
let call: Call;
|
|
1055
|
+
try {
|
|
1056
|
+
call = await this._initCall(call_id, campaignCallInfo, variables);
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
this._logger.error(
|
|
1059
|
+
"Failed to initialize campaign call %s. The call will not be dialed: %s",
|
|
1060
|
+
call_id,
|
|
1061
|
+
err,
|
|
1062
|
+
);
|
|
1063
|
+
socket.send({ message_type: "init-controller-failed", call_id });
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// Signal that we're ready for this call to go out, and attach to the endpoint.
|
|
1068
|
+
socket.send({ message_type: "controller-ready", call_id });
|
|
1069
|
+
await this._attachToCall(call_id, call, undefined, "v2/connect-campaign-call");
|
|
1008
1070
|
})().catch((err) => {
|
|
1009
1071
|
this._logger.error("Error handling campaign call %s: %s", call_id, err);
|
|
1010
1072
|
}),
|
|
@@ -1036,9 +1098,7 @@ Choose "speak" and provide your next utterance, or choose "hangup" if the conver
|
|
|
1036
1098
|
* await agent.attachCampaign("gcmp-abc123");
|
|
1037
1099
|
*/
|
|
1038
1100
|
async attachCampaign(campaignCode: string): Promise<void> {
|
|
1039
|
-
|
|
1040
|
-
await using _server = await getHealthServer(healthCtx);
|
|
1041
|
-
return await this._serveCampaign(healthCtx, campaignCode);
|
|
1101
|
+
await new Runner().attachCampaign(this, campaignCode).run();
|
|
1042
1102
|
}
|
|
1043
1103
|
|
|
1044
1104
|
/* ===== Aliases to be removed at some point. ===== */
|
package/src/call.ts
CHANGED
|
@@ -8,13 +8,16 @@ import {
|
|
|
8
8
|
SetTaskCommand,
|
|
9
9
|
SendInstructionCommand,
|
|
10
10
|
TransferCommand,
|
|
11
|
+
HangupCommand,
|
|
11
12
|
ReadScriptCommand,
|
|
12
13
|
RetryTaskCommand,
|
|
13
14
|
SetAgentDTMFCommand,
|
|
14
15
|
SendAgentDTMFCommand,
|
|
16
|
+
SetVariableCommand,
|
|
17
|
+
SendWebRTCMessageCommand,
|
|
15
18
|
} from "./commands.ts";
|
|
16
19
|
import { DTMF_DIGITS, type DTMFDigit } from "./events.ts";
|
|
17
|
-
import
|
|
20
|
+
import * as z from "zod";
|
|
18
21
|
import type {
|
|
19
22
|
ActionItem,
|
|
20
23
|
FieldItem,
|
|
@@ -26,6 +29,8 @@ import { Say } from "./action-item.ts";
|
|
|
26
29
|
import { telemetryClient } from "./telemetry.ts";
|
|
27
30
|
import type { CallInfo } from "./socket/call-info.ts";
|
|
28
31
|
import type { SipUri } from "./sip.ts";
|
|
32
|
+
import { Client } from "./client.ts";
|
|
33
|
+
import { idempotentPost } from "./utils.ts";
|
|
29
34
|
|
|
30
35
|
export type TaskObjective =
|
|
31
36
|
| { objective: string }
|
|
@@ -60,8 +65,179 @@ function voicemailMessageInstruction(message: string): string {
|
|
|
60
65
|
return `Say this message VERBATIM: "${message}" Then hang up.`;
|
|
61
66
|
}
|
|
62
67
|
|
|
68
|
+
/** Call-control methods shared by {@link Call} and {@link RemoteCall}. */
|
|
69
|
+
export abstract class BaseCall {
|
|
70
|
+
/** Validate a command against its schema and send it to the call. */
|
|
71
|
+
async sendCommand<C extends Command, Schema extends z.ZodType<C>>(
|
|
72
|
+
schema: Schema,
|
|
73
|
+
data: z.input<Schema>,
|
|
74
|
+
) {
|
|
75
|
+
await this._sendCommand(schema.parse(data));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected abstract _sendCommand(command: Command): Promise<void>;
|
|
79
|
+
|
|
80
|
+
async setLanguageMode(args: { primary?: Language; secondary?: Language[] }) {
|
|
81
|
+
await this.sendCommand(SetLanguageModeCommand, {
|
|
82
|
+
command_type: "set-language-mode",
|
|
83
|
+
primary: args.primary ?? "english",
|
|
84
|
+
secondary: args.secondary ?? [],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @description provide identifiers the agent will use to identify the virtual agent
|
|
90
|
+
*/
|
|
91
|
+
async setPersona(args: {
|
|
92
|
+
organizationName?: string;
|
|
93
|
+
agentName?: string;
|
|
94
|
+
agentPurpose?: string;
|
|
95
|
+
voice?: string;
|
|
96
|
+
speechSpeed?: SpeedPreset;
|
|
97
|
+
pronunciations?: Record<string, string>;
|
|
98
|
+
}) {
|
|
99
|
+
await this.sendCommand(SetPersona, {
|
|
100
|
+
command_type: "set-persona",
|
|
101
|
+
organization_name: args.organizationName,
|
|
102
|
+
agent_name: args.agentName,
|
|
103
|
+
agent_purpose: args.agentPurpose,
|
|
104
|
+
voice: args.voice,
|
|
105
|
+
tts_speed: args.speechSpeed,
|
|
106
|
+
tts_replacements: args.pronunciations,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Transfer the call to another destination.
|
|
112
|
+
*
|
|
113
|
+
* This method does not interfere with turn-taking — use this when
|
|
114
|
+
* you want the agent to naturally transfer the call.
|
|
115
|
+
*
|
|
116
|
+
* @param destination - The phone number, SIP address, or SipUri to transfer the call to.
|
|
117
|
+
* @param instructions - Optional guidance for how the agent should handle the transfer.
|
|
118
|
+
*/
|
|
119
|
+
async transfer(destination: string | SipUri, instructions?: string) {
|
|
120
|
+
await this.sendCommand(TransferCommand, {
|
|
121
|
+
command_type: "transfer-call",
|
|
122
|
+
to_number: destination.toString(),
|
|
123
|
+
transfer_message:
|
|
124
|
+
instructions ?? "Notify the caller that you will be transferring them, and then transfer.",
|
|
125
|
+
soft_transfer: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Transfer the call immediately.
|
|
131
|
+
*
|
|
132
|
+
* Unlike transfer(), this does not respect turn-taking. The agent immediately
|
|
133
|
+
* initiates the transfer, saying the script (if provided).
|
|
134
|
+
*
|
|
135
|
+
* @param destination - The phone number, SIP address, or SipUri to transfer the call to.
|
|
136
|
+
* @param finalScript - Optional message for the agent to deliver immediately
|
|
137
|
+
* before transferring. Defaults to no message.
|
|
138
|
+
*/
|
|
139
|
+
async immediateTransfer(destination: string | SipUri, finalScript?: string) {
|
|
140
|
+
await this.sendCommand(TransferCommand, {
|
|
141
|
+
command_type: "transfer-call",
|
|
142
|
+
to_number: destination.toString(),
|
|
143
|
+
transfer_message: finalScript ?? "",
|
|
144
|
+
soft_transfer: false,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Hang up the call immediately.
|
|
150
|
+
*
|
|
151
|
+
* Unlike hangup(), this does not respect turn-taking. Any speech the agent
|
|
152
|
+
* has already queued finishes, then the agent says the script (if provided)
|
|
153
|
+
* and the call ends. The caller cannot interrupt once this has been sent.
|
|
154
|
+
*
|
|
155
|
+
* @param finalScript - Optional message for the agent to deliver word-for-word
|
|
156
|
+
* immediately before hanging up. Defaults to no message.
|
|
157
|
+
*/
|
|
158
|
+
async immediateHangup(finalScript?: string) {
|
|
159
|
+
await this.sendCommand(HangupCommand, {
|
|
160
|
+
command_type: "hangup-call",
|
|
161
|
+
final_script: finalScript ?? null,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async addInfo(label: string, info: unknown) {
|
|
166
|
+
await this.sendInstruction(
|
|
167
|
+
`Here is some information about the following topic ${label}:\n${JSON.stringify(info, null, 2)}`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async retryTask(reason: string) {
|
|
172
|
+
await this.sendCommand(RetryTaskCommand, {
|
|
173
|
+
command_type: "retry-task",
|
|
174
|
+
reason,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async readScript(script: string | Partial<Record<Language, string>>) {
|
|
179
|
+
await this.sendCommand(ReadScriptCommand, {
|
|
180
|
+
command_type: "read-script",
|
|
181
|
+
script,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async sendInstruction(instruction: string) {
|
|
186
|
+
await this.sendCommand(SendInstructionCommand, {
|
|
187
|
+
command_type: "send-instruction",
|
|
188
|
+
instruction: instruction,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @description hang up an accepted call
|
|
194
|
+
*/
|
|
195
|
+
async hangup(final_instructions: string = "") {
|
|
196
|
+
let instructions: string;
|
|
197
|
+
if (final_instructions) {
|
|
198
|
+
instructions = `Start ending the conversation. Here are your final instructions: ${final_instructions} Once you've completed the final instructions, naturally end the conversation and hang up the call.`;
|
|
199
|
+
} else {
|
|
200
|
+
instructions = "Naturally end the conversation and hang up the call.";
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await this.sendInstruction(instructions);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Controls a call from outside the process handling it, e.g. to transfer a
|
|
209
|
+
* call that another agent is handling.
|
|
210
|
+
*/
|
|
211
|
+
@telemetryClient.trackClass()
|
|
212
|
+
export class RemoteCall extends BaseCall {
|
|
213
|
+
private _callId: string;
|
|
214
|
+
private _client: Client;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* @param callId - The ID of the call to control.
|
|
218
|
+
* @param client - Client used to send commands. Defaults to a new {@link Client}.
|
|
219
|
+
*/
|
|
220
|
+
constructor(callId: string, client: Client = new Client()) {
|
|
221
|
+
super();
|
|
222
|
+
this._callId = callId;
|
|
223
|
+
this._client = client;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
get id(): string {
|
|
227
|
+
return this._callId;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
protected async _sendCommand(command: Command): Promise<void> {
|
|
231
|
+
const url = new URL(`v1/call/${this._callId}/send-commands`, this._client.getHttpBase());
|
|
232
|
+
await idempotentPost(url, {
|
|
233
|
+
headers: { ...(await this._client.headers()), "Content-Type": "application/json" },
|
|
234
|
+
body: JSON.stringify([command]),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
63
239
|
@telemetryClient.trackClass()
|
|
64
|
-
export class Call {
|
|
240
|
+
export class Call extends BaseCall {
|
|
65
241
|
private _callId: string;
|
|
66
242
|
private _callInfo: CallInfo;
|
|
67
243
|
protected _commandQueue: Command[] = [];
|
|
@@ -81,6 +257,7 @@ export class Call {
|
|
|
81
257
|
variables: Record<string, any> = {},
|
|
82
258
|
logger: Logger = getDefaultLogger(),
|
|
83
259
|
) {
|
|
260
|
+
super();
|
|
84
261
|
this._callId = callId;
|
|
85
262
|
this._callInfo = callInfo;
|
|
86
263
|
|
|
@@ -127,11 +304,7 @@ export class Call {
|
|
|
127
304
|
return key in this._fieldValues;
|
|
128
305
|
}
|
|
129
306
|
|
|
130
|
-
async
|
|
131
|
-
schema: Schema,
|
|
132
|
-
data: z.input<Schema>,
|
|
133
|
-
) {
|
|
134
|
-
const command = schema.parse(data);
|
|
307
|
+
protected async _sendCommand(command: Command): Promise<void> {
|
|
135
308
|
this._commandQueue.push(command);
|
|
136
309
|
await this.flush();
|
|
137
310
|
}
|
|
@@ -166,33 +339,22 @@ export class Call {
|
|
|
166
339
|
});
|
|
167
340
|
}
|
|
168
341
|
|
|
169
|
-
async setLanguageMode(args: { primary?: Language; secondary?: Language[] }) {
|
|
170
|
-
await this.sendCommand(SetLanguageModeCommand, {
|
|
171
|
-
command_type: "set-language-mode",
|
|
172
|
-
primary: args.primary ?? "english",
|
|
173
|
-
secondary: args.secondary ?? [],
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
342
|
/**
|
|
178
|
-
*
|
|
343
|
+
* Send a message to a connected WebRTC client.
|
|
344
|
+
*
|
|
345
|
+
* @param payload - A JSON-serializable object delivered to the client.
|
|
346
|
+
* @throws If this is not a WebRTC call, or `payload` is not JSON-serializable.
|
|
179
347
|
*/
|
|
180
|
-
async
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
organization_name: args.organizationName,
|
|
191
|
-
agent_name: args.agentName,
|
|
192
|
-
agent_purpose: args.agentPurpose,
|
|
193
|
-
voice: args.voice,
|
|
194
|
-
tts_speed: args.speechSpeed,
|
|
195
|
-
tts_replacements: args.pronunciations,
|
|
348
|
+
async sendWebrtcMessage(payload: Record<string, unknown>) {
|
|
349
|
+
if (this._callInfo.call_type !== "webrtc") {
|
|
350
|
+
throw new Error("Only WebRTC calls support sending WebRTC messages.");
|
|
351
|
+
}
|
|
352
|
+
if (!z.record(z.string(), z.json()).safeParse(payload).success) {
|
|
353
|
+
throw new Error("WebRTC message payload must be JSON-serializable.");
|
|
354
|
+
}
|
|
355
|
+
await this.sendCommand(SendWebRTCMessageCommand, {
|
|
356
|
+
command_type: "send-webrtc-message",
|
|
357
|
+
payload: payload as Record<string, z.core.util.JSONType>,
|
|
196
358
|
});
|
|
197
359
|
}
|
|
198
360
|
|
|
@@ -243,85 +405,6 @@ export class Call {
|
|
|
243
405
|
});
|
|
244
406
|
}
|
|
245
407
|
|
|
246
|
-
/**
|
|
247
|
-
* Transfer the call to another destination.
|
|
248
|
-
*
|
|
249
|
-
* This method does not interfere with turn-taking — use this when
|
|
250
|
-
* you want the agent to naturally transfer the call.
|
|
251
|
-
*
|
|
252
|
-
* @param destination - The phone number, SIP address, or SipUri to transfer the call to.
|
|
253
|
-
* @param instructions - Optional guidance for how the agent should handle the transfer.
|
|
254
|
-
*/
|
|
255
|
-
async transfer(destination: string | SipUri, instructions?: string) {
|
|
256
|
-
await this.sendCommand(TransferCommand, {
|
|
257
|
-
command_type: "transfer-call",
|
|
258
|
-
to_number: destination.toString(),
|
|
259
|
-
transfer_message:
|
|
260
|
-
instructions ?? "Notify the caller that you will be transferring them, and then transfer.",
|
|
261
|
-
soft_transfer: true,
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Transfer the call immediately.
|
|
267
|
-
*
|
|
268
|
-
* Unlike transfer(), this does not respect turn-taking. The agent immediately
|
|
269
|
-
* initiates the transfer, saying the script (if provided).
|
|
270
|
-
*
|
|
271
|
-
* @param destination - The phone number, SIP address, or SipUri to transfer the call to.
|
|
272
|
-
* @param finalScript - Optional message for the agent to deliver immediately
|
|
273
|
-
* before transferring. Defaults to no message.
|
|
274
|
-
*/
|
|
275
|
-
async immediateTransfer(destination: string | SipUri, finalScript?: string) {
|
|
276
|
-
await this.sendCommand(TransferCommand, {
|
|
277
|
-
command_type: "transfer-call",
|
|
278
|
-
to_number: destination.toString(),
|
|
279
|
-
transfer_message: finalScript ?? "",
|
|
280
|
-
soft_transfer: false,
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async addInfo(label: string, info: unknown) {
|
|
285
|
-
await this.sendInstruction(
|
|
286
|
-
`Here is some information about the following topic ${label}:\n${JSON.stringify(info, null, 2)}`,
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async retryTask(reason: string) {
|
|
291
|
-
await this.sendCommand(RetryTaskCommand, {
|
|
292
|
-
command_type: "retry-task",
|
|
293
|
-
reason,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
async readScript(script: string | Partial<Record<Language, string>>) {
|
|
298
|
-
await this.sendCommand(ReadScriptCommand, {
|
|
299
|
-
command_type: "read-script",
|
|
300
|
-
script,
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
async sendInstruction(instruction: string) {
|
|
305
|
-
await this.sendCommand(SendInstructionCommand, {
|
|
306
|
-
command_type: "send-instruction",
|
|
307
|
-
instruction: instruction,
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* @description hang up an accepted call
|
|
313
|
-
*/
|
|
314
|
-
async hangup(final_instructions: string = "") {
|
|
315
|
-
let instructions: string;
|
|
316
|
-
if (final_instructions) {
|
|
317
|
-
instructions = `Start ending the conversation. Here are your final instructions: ${final_instructions} Once you've completed the final instructions, naturally end the conversation and hang up the call.`;
|
|
318
|
-
} else {
|
|
319
|
-
instructions = "Naturally end the conversation and hang up the call.";
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
await this.sendInstruction(instructions);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
408
|
async reachPerson(
|
|
326
409
|
contactFullName: string,
|
|
327
410
|
options: {
|
|
@@ -431,8 +514,21 @@ TASK COMPLETION REQUIREMENTS:
|
|
|
431
514
|
}
|
|
432
515
|
}
|
|
433
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Set a variable on the call, both locally and on the server.
|
|
519
|
+
*
|
|
520
|
+
* @throws If `variableValue` is not JSON-serializable.
|
|
521
|
+
*/
|
|
434
522
|
async setVariable(variableName: string, variableValue: any) {
|
|
523
|
+
if (!z.json().safeParse(variableValue).success) {
|
|
524
|
+
throw new Error(`Variable value for key '${variableName}' must be JSON-serializable.`);
|
|
525
|
+
}
|
|
435
526
|
this._variables[variableName] = variableValue;
|
|
527
|
+
await this.sendCommand(SetVariableCommand, {
|
|
528
|
+
command_type: "set-variable",
|
|
529
|
+
key: variableName,
|
|
530
|
+
value: variableValue,
|
|
531
|
+
});
|
|
436
532
|
}
|
|
437
533
|
|
|
438
534
|
async getVariable(variableName: string) {
|