@guava-ai/guava-sdk 0.28.0 → 0.30.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/health.d.ts +4 -1
- package/dist/src/health.js +14 -2
- package/dist/src/health.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/health.ts +17 -3
- 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/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.30.0";
|