@guava-ai/guava-sdk 0.29.0 → 0.31.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/dist/examples/multiple-agents.js +2 -1
- package/dist/examples/multiple-agents.js.map +1 -1
- package/dist/examples/property-insurance.d.ts +2 -0
- package/dist/examples/property-insurance.js +8 -7
- package/dist/examples/property-insurance.js.map +1 -1
- package/dist/examples/restaurant-waitlist.d.ts +2 -0
- package/dist/examples/restaurant-waitlist.js +10 -9
- package/dist/examples/restaurant-waitlist.js.map +1 -1
- package/dist/src/agent.d.ts +2 -2
- package/dist/src/agent.js +68 -43
- package/dist/src/agent.js.map +1 -1
- package/dist/src/call.d.ts +20 -0
- package/dist/src/call.js +27 -0
- package/dist/src/call.js.map +1 -1
- package/dist/src/runner.d.ts +10 -1
- package/dist/src/runner.js +16 -5
- package/dist/src/runner.js.map +1 -1
- package/dist/src/socket/client.js +19 -2
- package/dist/src/socket/client.js.map +1 -1
- package/dist/src/testing/session.d.ts +2 -1
- package/dist/src/testing/session.js +3 -1
- package/dist/src/testing/session.js.map +1 -1
- package/dist/src/utils.d.ts +4 -0
- package/dist/src/utils.js +35 -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/multiple-agents.ts +2 -1
- package/examples/property-insurance.ts +1 -1
- package/examples/restaurant-waitlist.ts +1 -1
- package/package.json +11 -2
- package/src/agent.ts +43 -18
- package/src/call.ts +28 -0
- package/src/runner.ts +28 -5
- package/src/socket/client.ts +31 -13
- package/src/testing/session.ts +3 -1
- package/src/utils.ts +38 -0
- package/src/version.ts +1 -1
package/src/call.ts
CHANGED
|
@@ -237,6 +237,15 @@ export class Call {
|
|
|
237
237
|
});
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Transfer the call to another destination.
|
|
242
|
+
*
|
|
243
|
+
* This method does not interfere with turn-taking — use this when
|
|
244
|
+
* you want the agent to naturally transfer the call.
|
|
245
|
+
*
|
|
246
|
+
* @param destination - The phone number or SIP address to transfer the call to.
|
|
247
|
+
* @param instructions - Optional guidance for how the agent should handle the transfer.
|
|
248
|
+
*/
|
|
240
249
|
async transfer(destination: string, instructions?: string) {
|
|
241
250
|
await this.sendCommand(TransferCommand, {
|
|
242
251
|
command_type: "transfer-call",
|
|
@@ -247,6 +256,25 @@ export class Call {
|
|
|
247
256
|
});
|
|
248
257
|
}
|
|
249
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Transfer the call immediately.
|
|
261
|
+
*
|
|
262
|
+
* Unlike transfer(), this does not respect turn-taking. The agent immediately
|
|
263
|
+
* initiates the transfer, saying the script (if provided).
|
|
264
|
+
*
|
|
265
|
+
* @param destination - The phone number or SIP address to transfer the call to.
|
|
266
|
+
* @param finalScript - Optional message for the agent to deliver immediately
|
|
267
|
+
* before transferring. Defaults to no message.
|
|
268
|
+
*/
|
|
269
|
+
async immediateTransfer(destination: string, finalScript?: string) {
|
|
270
|
+
await this.sendCommand(TransferCommand, {
|
|
271
|
+
command_type: "transfer-call",
|
|
272
|
+
to_number: destination,
|
|
273
|
+
transfer_message: finalScript ?? "",
|
|
274
|
+
soft_transfer: false,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
250
278
|
async addInfo(label: string, info: unknown) {
|
|
251
279
|
await this.sendInstruction(
|
|
252
280
|
`Here is some information about the following topic ${label}:\n${JSON.stringify(info, null, 2)}`,
|
package/src/runner.ts
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import type { Agent } from "./agent.ts";
|
|
2
2
|
import { MultiHealthContext, getHealthServer } from "./health.ts";
|
|
3
|
+
import { nullDisposable, onSignal } from "./utils.ts";
|
|
4
|
+
import { getDefaultLogger, type Logger } from "./logging.ts";
|
|
5
|
+
|
|
6
|
+
export interface RunOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Install SIGINT/SIGTERM handlers that drain in-flight calls on the first
|
|
9
|
+
* signal; a second signal terminates the process. Defaults to `true`.
|
|
10
|
+
*/
|
|
11
|
+
handleSignals?: boolean;
|
|
12
|
+
}
|
|
3
13
|
|
|
4
14
|
export class Runner {
|
|
5
15
|
private _healthCtx = new MultiHealthContext();
|
|
6
16
|
private _tasks: Array<() => Promise<void>> = [];
|
|
17
|
+
private _drainController: AbortController = new AbortController();
|
|
18
|
+
private _logger: Logger = getDefaultLogger();
|
|
7
19
|
|
|
8
20
|
listenPhone(agent: Agent, agentNumber: string): this {
|
|
9
21
|
const ctx = this._healthCtx.createCtx();
|
|
10
|
-
this._tasks.push(() =>
|
|
22
|
+
this._tasks.push(() =>
|
|
23
|
+
agent._listenInbound(ctx, { agent_number: agentNumber }, {}, this._drainController.signal),
|
|
24
|
+
);
|
|
11
25
|
return this;
|
|
12
26
|
}
|
|
13
27
|
|
|
@@ -15,25 +29,34 @@ export class Runner {
|
|
|
15
29
|
const ctx = this._healthCtx.createCtx();
|
|
16
30
|
this._tasks.push(async () => {
|
|
17
31
|
const code = webrtcCode ?? (await agent._client.createWebrtcAgent(3600));
|
|
18
|
-
return agent._listenInbound(ctx, { webrtc_code: code });
|
|
32
|
+
return agent._listenInbound(ctx, { webrtc_code: code }, {}, this._drainController.signal);
|
|
19
33
|
});
|
|
20
34
|
return this;
|
|
21
35
|
}
|
|
22
36
|
|
|
23
37
|
listenSip(agent: Agent, sipCode: string): this {
|
|
24
38
|
const ctx = this._healthCtx.createCtx();
|
|
25
|
-
this._tasks.push(() =>
|
|
39
|
+
this._tasks.push(() =>
|
|
40
|
+
agent._listenInbound(ctx, { sip_code: sipCode }, {}, this._drainController.signal),
|
|
41
|
+
);
|
|
26
42
|
return this;
|
|
27
43
|
}
|
|
28
44
|
|
|
29
45
|
attachCampaign(agent: Agent, campaignCode: string): this {
|
|
30
46
|
const ctx = this._healthCtx.createCtx();
|
|
31
|
-
this._tasks.push(() => agent._serveCampaign(ctx, campaignCode));
|
|
47
|
+
this._tasks.push(() => agent._serveCampaign(ctx, campaignCode, this._drainController.signal));
|
|
32
48
|
return this;
|
|
33
49
|
}
|
|
34
50
|
|
|
35
|
-
async run(): Promise<void> {
|
|
51
|
+
async run({ handleSignals = true }: RunOptions = {}): Promise<void> {
|
|
36
52
|
await using _server = await getHealthServer(this._healthCtx);
|
|
53
|
+
using _ = handleSignals
|
|
54
|
+
? onSignal((signal) => {
|
|
55
|
+
this._logger.info(`Received ${signal} - draining calls. Send signal again to terminate.`);
|
|
56
|
+
this._drainController.abort();
|
|
57
|
+
})
|
|
58
|
+
: nullDisposable;
|
|
37
59
|
await Promise.all(this._tasks.map((task) => task()));
|
|
60
|
+
this._logger.info("All tasks done.");
|
|
38
61
|
}
|
|
39
62
|
}
|
package/src/socket/client.ts
CHANGED
|
@@ -49,6 +49,16 @@ export class GuavaSocketConnectionFailed extends Error {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/** Raised when the server rejects the websocket handshake with an HTTP response. */
|
|
53
|
+
class UnexpectedResponseError extends Error {
|
|
54
|
+
constructor(
|
|
55
|
+
readonly statusCode: number | undefined,
|
|
56
|
+
readonly body: string,
|
|
57
|
+
) {
|
|
58
|
+
super(`Unexpected server response: ${statusCode ?? "unknown"}. Body: ${body || "<empty>"}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
const HandshakeResponse = z.union([GuavaOpenAck, GuavaClose]);
|
|
53
63
|
|
|
54
64
|
function _sendMessage(ws: WebSocket, message: GuavaClientMessage): void {
|
|
@@ -160,13 +170,7 @@ export class GuavaSocket<SendT, RecvT> {
|
|
|
160
170
|
body += chunk.toString();
|
|
161
171
|
});
|
|
162
172
|
res.on("end", () => {
|
|
163
|
-
settle(() =>
|
|
164
|
-
reject(
|
|
165
|
-
new Error(
|
|
166
|
-
`Unexpected server response: ${res.statusCode}. Body: ${body || "<empty>"}`,
|
|
167
|
-
),
|
|
168
|
-
),
|
|
169
|
-
);
|
|
173
|
+
settle(() => reject(new UnexpectedResponseError(res.statusCode, body)));
|
|
170
174
|
});
|
|
171
175
|
});
|
|
172
176
|
socket.once("close", () =>
|
|
@@ -225,6 +229,7 @@ export class GuavaSocket<SendT, RecvT> {
|
|
|
225
229
|
if (e instanceof GuavaSocketClosedError) throw e;
|
|
226
230
|
|
|
227
231
|
if (attempt >= MAX_RECONNECT_ATTEMPTS) {
|
|
232
|
+
this._logger.error("Couldn't connect to server after %d attempts.", attempt);
|
|
228
233
|
this._setCloseReason(
|
|
229
234
|
"reconnection-failed",
|
|
230
235
|
`Couldn't connect after ${attempt} attempts.`,
|
|
@@ -232,12 +237,25 @@ export class GuavaSocket<SendT, RecvT> {
|
|
|
232
237
|
throw new GuavaSocketConnectionFailed();
|
|
233
238
|
}
|
|
234
239
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
if (e instanceof UnexpectedResponseError) {
|
|
241
|
+
const interpretation =
|
|
242
|
+
e.statusCode === 401 || e.statusCode === 403 ? "Authentication Failure" : "HTTP error";
|
|
243
|
+
this._logger.error(
|
|
244
|
+
"Failed to connect to websocket endpoint %s due to %s. HTTP %s, Body: %s. Retrying in a few seconds...",
|
|
245
|
+
this._connectionUrl,
|
|
246
|
+
interpretation,
|
|
247
|
+
e.statusCode ?? "unknown",
|
|
248
|
+
e.body || "No response body.",
|
|
249
|
+
);
|
|
250
|
+
} else {
|
|
251
|
+
this._logger.error(
|
|
252
|
+
"Failed to connect to %s (attempt %d/%d). Retrying in a few seconds...\n",
|
|
253
|
+
this._connectionUrl,
|
|
254
|
+
attempt,
|
|
255
|
+
MAX_RECONNECT_ATTEMPTS,
|
|
256
|
+
e,
|
|
257
|
+
);
|
|
258
|
+
}
|
|
241
259
|
await sleep(reconnectDelay(attempt));
|
|
242
260
|
}
|
|
243
261
|
}
|
package/src/testing/session.ts
CHANGED
|
@@ -29,15 +29,17 @@ export class TestSession {
|
|
|
29
29
|
|
|
30
30
|
executedActions: string[] = [];
|
|
31
31
|
terminationReason: string | null = null;
|
|
32
|
+
readonly id: string;
|
|
32
33
|
|
|
33
34
|
/** @internal */
|
|
34
35
|
get _events(): ReadonlyArray<InjectASR | BotTTS> {
|
|
35
36
|
return this._sessionEvents;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
constructor(ws: WebSocket, client: Client) {
|
|
39
|
+
constructor(ws: WebSocket, client: Client, id: string) {
|
|
39
40
|
this._ws = ws;
|
|
40
41
|
this._client = client;
|
|
42
|
+
this.id = id;
|
|
41
43
|
|
|
42
44
|
ws.on("message", (data) => {
|
|
43
45
|
let event: TestingEvent;
|
package/src/utils.ts
CHANGED
|
@@ -63,3 +63,41 @@ export async function fetchOrThrow(
|
|
|
63
63
|
|
|
64
64
|
return res;
|
|
65
65
|
}
|
|
66
|
+
|
|
67
|
+
/** A Disposable whose disposal does nothing. Useful as a conditional no-op. */
|
|
68
|
+
export const nullDisposable: Disposable = { [Symbol.dispose]() {} };
|
|
69
|
+
|
|
70
|
+
export function onAbort(signal: AbortSignal, listener: () => void): Disposable {
|
|
71
|
+
signal.throwIfAborted();
|
|
72
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
[Symbol.dispose]() {
|
|
76
|
+
signal.removeEventListener("abort", listener);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function onSignal(handler: (signal: "SIGINT" | "SIGTERM") => void): Disposable {
|
|
82
|
+
// Remove both handlers on the first signal so any second signal (of either
|
|
83
|
+
// type) falls through to Node's default behavior and terminates the process.
|
|
84
|
+
const remove = () => {
|
|
85
|
+
process.off("SIGINT", sigint);
|
|
86
|
+
process.off("SIGTERM", sigterm);
|
|
87
|
+
};
|
|
88
|
+
const sigint = () => {
|
|
89
|
+
remove();
|
|
90
|
+
handler("SIGINT");
|
|
91
|
+
};
|
|
92
|
+
const sigterm = () => {
|
|
93
|
+
remove();
|
|
94
|
+
handler("SIGTERM");
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
process.on("SIGINT", sigint);
|
|
98
|
+
process.on("SIGTERM", sigterm);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
[Symbol.dispose]: remove,
|
|
102
|
+
};
|
|
103
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SDK_VERSION = "0.
|
|
1
|
+
export const SDK_VERSION = "0.31.0";
|