@guava-ai/guava-sdk 0.18.0 → 0.20.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/example.test.d.ts +5 -0
- package/dist/examples/example.test.js +46 -0
- package/dist/examples/example.test.js.map +1 -0
- package/dist/examples/help-desk.d.ts +3 -1
- package/dist/examples/help-desk.js +25 -9
- package/dist/examples/help-desk.js.map +1 -1
- package/dist/examples/property-insurance.js +4 -1
- package/dist/examples/property-insurance.js.map +1 -1
- package/dist/examples/restaurant-waitlist.js +4 -1
- package/dist/examples/restaurant-waitlist.js.map +1 -1
- package/dist/examples/scheduling-outbound.js +6 -0
- package/dist/examples/scheduling-outbound.js.map +1 -1
- package/dist/src/action-item.d.ts +4 -4
- package/dist/src/agent.d.ts +85 -18
- package/dist/src/agent.js +404 -129
- package/dist/src/agent.js.map +1 -1
- package/dist/src/auth.d.ts +27 -0
- package/dist/src/auth.js +127 -0
- package/dist/src/auth.js.map +1 -0
- package/dist/src/call.d.ts +1 -1
- package/dist/src/call.js +2 -2
- package/dist/src/call.js.map +1 -1
- package/dist/src/client.d.ts +38 -12
- package/dist/src/client.js +88 -16
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands.d.ts +3 -3
- package/dist/src/events.d.ts +87 -0
- package/dist/src/events.js +25 -6
- package/dist/src/events.js.map +1 -1
- package/dist/src/helpers/llm.d.ts +2 -0
- package/dist/src/helpers/llm.js +17 -0
- package/dist/src/helpers/llm.js.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +5 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/logging.js +16 -11
- package/dist/src/logging.js.map +1 -1
- package/dist/src/sms.d.ts +19 -0
- package/dist/src/sms.js +52 -0
- package/dist/src/sms.js.map +1 -0
- package/dist/src/socket/call-info.d.ts +35 -0
- package/dist/src/socket/call-info.js +59 -0
- package/dist/src/socket/call-info.js.map +1 -0
- package/dist/src/socket/client.d.ts +51 -0
- package/dist/src/socket/client.js +455 -0
- package/dist/src/socket/client.js.map +1 -0
- package/dist/src/socket/listen-inbound.d.ts +83 -0
- package/dist/src/socket/listen-inbound.js +82 -0
- package/dist/src/socket/listen-inbound.js.map +1 -0
- package/dist/src/socket/protocol.d.ts +127 -0
- package/dist/src/socket/protocol.js +69 -0
- package/dist/src/socket/protocol.js.map +1 -0
- package/dist/src/socket/utils.d.ts +8 -0
- package/dist/src/socket/utils.js +26 -0
- package/dist/src/socket/utils.js.map +1 -0
- package/dist/src/telemetry.d.ts +3 -3
- package/dist/src/telemetry.js +9 -7
- package/dist/src/telemetry.js.map +1 -1
- package/dist/src/testing/chat.d.ts +2 -0
- package/dist/src/testing/chat.js +181 -0
- package/dist/src/testing/chat.js.map +1 -0
- package/dist/src/testing/mocks.d.ts +6 -0
- package/dist/src/testing/mocks.js +14 -0
- package/dist/src/testing/mocks.js.map +1 -0
- package/dist/src/testing/protocol.d.ts +46 -0
- package/dist/src/testing/protocol.js +61 -0
- package/dist/src/testing/protocol.js.map +1 -0
- package/dist/src/testing/session.d.ts +26 -0
- package/dist/src/testing/session.js +219 -0
- package/dist/src/testing/session.js.map +1 -0
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.js +19 -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/example.test.ts +58 -0
- package/examples/help-desk.ts +14 -3
- package/examples/property-insurance.ts +3 -1
- package/examples/restaurant-waitlist.ts +3 -1
- package/examples/scheduling-outbound.ts +7 -0
- package/package.json +10 -2
- package/src/agent.ts +386 -166
- package/src/auth.ts +109 -0
- package/src/call.ts +3 -3
- package/src/client.ts +119 -18
- package/src/events.ts +52 -10
- package/src/helpers/llm.ts +20 -0
- package/src/index.ts +4 -0
- package/src/logging.ts +21 -13
- package/src/sms.ts +17 -0
- package/src/socket/call-info.ts +30 -0
- package/src/socket/client.ts +433 -0
- package/src/socket/listen-inbound.ts +62 -0
- package/src/socket/protocol.ts +89 -0
- package/src/socket/utils.ts +25 -0
- package/src/telemetry.ts +11 -8
- package/src/testing/chat.ts +196 -0
- package/src/testing/mocks.ts +12 -0
- package/src/testing/protocol.ts +40 -0
- package/src/testing/session.ts +218 -0
- package/src/utils.ts +19 -1
- package/src/version.ts +1 -1
package/src/auth.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { getCliConfigPath, getBaseUrl, fetchOrThrow } from "./utils.ts";
|
|
3
|
+
import { getDefaultLogger } from "./logging.ts";
|
|
4
|
+
|
|
5
|
+
const logger = getDefaultLogger();
|
|
6
|
+
|
|
7
|
+
export interface AuthStrategy {
|
|
8
|
+
getHeaders(): Promise<Record<string, string>>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class APIKeyAuth implements AuthStrategy {
|
|
12
|
+
constructor(private readonly _apiKey: string) {}
|
|
13
|
+
|
|
14
|
+
async getHeaders(): Promise<Record<string, string>> {
|
|
15
|
+
return { Authorization: `Bearer ${this._apiKey}` };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const GUAVA_DEPLOY_TOKEN_PATH = "/var/run/secrets/guava/token";
|
|
20
|
+
const _GUAVA_DEPLOY_TOKEN_PREFIX = "gva-deploy2-";
|
|
21
|
+
|
|
22
|
+
export class GuavaDeploy implements AuthStrategy {
|
|
23
|
+
constructor(private readonly _tokenPath: string = GUAVA_DEPLOY_TOKEN_PATH) {}
|
|
24
|
+
|
|
25
|
+
async getHeaders(): Promise<Record<string, string>> {
|
|
26
|
+
const token = fs.readFileSync(this._tokenPath, "utf8").trim();
|
|
27
|
+
return { Authorization: `Bearer ${_GUAVA_DEPLOY_TOKEN_PREFIX}${token}` };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const TOKEN_REFRESH_BUFFER_MS = 60_000;
|
|
32
|
+
|
|
33
|
+
interface CliConfig {
|
|
34
|
+
access_token: string;
|
|
35
|
+
expires_at: number;
|
|
36
|
+
refresh_token: string;
|
|
37
|
+
org_id: string;
|
|
38
|
+
base_url?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class CLIAuth implements AuthStrategy {
|
|
42
|
+
static exists(): boolean {
|
|
43
|
+
const configPath = getCliConfigPath();
|
|
44
|
+
if (!fs.existsSync(configPath)) return false;
|
|
45
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8")) as Record<string, unknown>;
|
|
46
|
+
return "refresh_token" in config;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private _accessToken: string;
|
|
50
|
+
private _expiresAt: number; // ms since epoch
|
|
51
|
+
private _refreshToken: string;
|
|
52
|
+
private _orgId: string;
|
|
53
|
+
private _baseUrl: string;
|
|
54
|
+
private _pendingRefresh: Promise<void> | null = null;
|
|
55
|
+
|
|
56
|
+
constructor() {
|
|
57
|
+
const config = JSON.parse(fs.readFileSync(getCliConfigPath(), "utf8")) as CliConfig;
|
|
58
|
+
this._accessToken = config.access_token;
|
|
59
|
+
this._expiresAt = config.expires_at * 1000;
|
|
60
|
+
this._refreshToken = config.refresh_token;
|
|
61
|
+
this._orgId = config.org_id;
|
|
62
|
+
this._baseUrl = config.base_url ?? getBaseUrl();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async _doRefresh(): Promise<void> {
|
|
66
|
+
logger.debug("Refreshing access token...");
|
|
67
|
+
const response = await fetchOrThrow(new URL("/oauth/token", this._baseUrl), {
|
|
68
|
+
method: "POST",
|
|
69
|
+
body: new URLSearchParams({
|
|
70
|
+
grant_type: "refresh_token",
|
|
71
|
+
refresh_token: this._refreshToken,
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
const token = (await response.json()) as {
|
|
75
|
+
access_token: string;
|
|
76
|
+
expires_in: number;
|
|
77
|
+
refresh_token?: string;
|
|
78
|
+
};
|
|
79
|
+
this._accessToken = token.access_token;
|
|
80
|
+
this._expiresAt = Date.now() + token.expires_in * 1000;
|
|
81
|
+
if (token.refresh_token) {
|
|
82
|
+
logger.warn("Unexpected refresh token in response.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async getHeaders(): Promise<Record<string, string>> {
|
|
87
|
+
if (Date.now() >= this._expiresAt - TOKEN_REFRESH_BUFFER_MS) {
|
|
88
|
+
if (!this._pendingRefresh) {
|
|
89
|
+
this._pendingRefresh = this._doRefresh().finally(() => {
|
|
90
|
+
this._pendingRefresh = null;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
await this._pendingRefresh;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
Authorization: `Bearer ${this._accessToken}`,
|
|
97
|
+
"x-guava-org-id": this._orgId,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let _cliAuthInstance: CLIAuth | null = null;
|
|
103
|
+
|
|
104
|
+
export function getCLIAuth(): CLIAuth {
|
|
105
|
+
if (!_cliAuthInstance) {
|
|
106
|
+
_cliAuthInstance = new CLIAuth();
|
|
107
|
+
}
|
|
108
|
+
return _cliAuthInstance;
|
|
109
|
+
}
|
package/src/call.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type ReachPersonOutcome = {
|
|
|
33
33
|
|
|
34
34
|
@telemetryClient.trackClass()
|
|
35
35
|
export class Call {
|
|
36
|
-
|
|
36
|
+
protected _commandQueue: Command[] = [];
|
|
37
37
|
private _variables: Record<string, any> = {};
|
|
38
38
|
protected logger: Logger;
|
|
39
39
|
|
|
@@ -272,11 +272,11 @@ TASK COMPLETION REQUIREMENTS:
|
|
|
272
272
|
async setVoicemailAction(action: { hangup: true } | { message: string }) {
|
|
273
273
|
if ("hangup" in action) {
|
|
274
274
|
await this.sendInstruction(
|
|
275
|
-
"If you encounter an answering machine, DO NOT leave a message. REMAIN SILENT AND HANG UP WITHOUT RESPONDING.",
|
|
275
|
+
"If you encounter an answering machine, DO NOT leave a message. REMAIN SILENT AND HANG UP WITHOUT RESPONDING. You should only do this when it's clear you are unable to reach the person.",
|
|
276
276
|
);
|
|
277
277
|
} else {
|
|
278
278
|
await this.sendInstruction(
|
|
279
|
-
`If you encounter an answering machine, say this message VERBATIM: ${action.message}
|
|
279
|
+
`If you encounter an answering machine, say this message VERBATIM: ${action.message}. You should only leave this message if it's clear you are unable to reach the person.`,
|
|
280
280
|
);
|
|
281
281
|
}
|
|
282
282
|
}
|
package/src/client.ts
CHANGED
|
@@ -9,12 +9,30 @@ import * as z from "zod";
|
|
|
9
9
|
import { ErrorEvent, SessionStartedEvent, decodeEvent, InboundTunnelEvent } from "./events.ts";
|
|
10
10
|
import { SDK_VERSION } from "./version.ts";
|
|
11
11
|
import os from "node:os";
|
|
12
|
-
import
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import { getBaseUrl, fetchOrThrow, sleep } from "./utils.ts";
|
|
14
|
+
import { SmsMessage } from "./sms.ts";
|
|
13
15
|
import { telemetryClient } from "./telemetry.ts";
|
|
14
16
|
import type { CallController } from "./call-controller.ts";
|
|
17
|
+
import {
|
|
18
|
+
type AuthStrategy,
|
|
19
|
+
APIKeyAuth,
|
|
20
|
+
GuavaDeploy,
|
|
21
|
+
CLIAuth,
|
|
22
|
+
getCLIAuth,
|
|
23
|
+
GUAVA_DEPLOY_TOKEN_PATH,
|
|
24
|
+
} from "./auth.ts";
|
|
15
25
|
|
|
16
26
|
const SDK_NAME = "typescript-sdk";
|
|
17
27
|
|
|
28
|
+
export interface ClientOptions {
|
|
29
|
+
apiKey?: string;
|
|
30
|
+
baseUrl?: string;
|
|
31
|
+
logger?: Logger;
|
|
32
|
+
captureWarnings?: boolean;
|
|
33
|
+
checkDeprecation?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
18
36
|
let firstClient = false;
|
|
19
37
|
|
|
20
38
|
function stringifyZod<Schema extends z.ZodType>(schema: Schema, data: z.input<Schema>): string {
|
|
@@ -28,14 +46,20 @@ const https_start = /^https:\/\//;
|
|
|
28
46
|
|
|
29
47
|
@telemetryClient.trackClass()
|
|
30
48
|
export class Client {
|
|
31
|
-
private
|
|
49
|
+
private _auth: AuthStrategy;
|
|
32
50
|
private _baseUrl: string;
|
|
33
51
|
private _logger: Logger;
|
|
34
52
|
private _ws?: WebSocket;
|
|
35
53
|
private _controller?: CallController;
|
|
36
54
|
private messageHandler?: (_: WebSocket.MessageEvent) => void;
|
|
37
55
|
|
|
38
|
-
constructor(
|
|
56
|
+
constructor({
|
|
57
|
+
apiKey,
|
|
58
|
+
baseUrl,
|
|
59
|
+
logger,
|
|
60
|
+
captureWarnings = true,
|
|
61
|
+
checkDeprecation = true,
|
|
62
|
+
}: ClientOptions = {}) {
|
|
39
63
|
// Set up the default logger.
|
|
40
64
|
if (logger) {
|
|
41
65
|
this._logger = logger;
|
|
@@ -50,14 +74,18 @@ export class Client {
|
|
|
50
74
|
this._baseUrl = getBaseUrl();
|
|
51
75
|
}
|
|
52
76
|
|
|
53
|
-
// Resolve
|
|
77
|
+
// Resolve auth strategy.
|
|
54
78
|
if (apiKey) {
|
|
55
|
-
this.
|
|
79
|
+
this._auth = new APIKeyAuth(apiKey);
|
|
80
|
+
} else if (fs.existsSync(GUAVA_DEPLOY_TOKEN_PATH)) {
|
|
81
|
+
this._auth = new GuavaDeploy();
|
|
56
82
|
} else if (process.env.GUAVA_API_KEY) {
|
|
57
|
-
this.
|
|
83
|
+
this._auth = new APIKeyAuth(process.env.GUAVA_API_KEY);
|
|
84
|
+
} else if (CLIAuth.exists()) {
|
|
85
|
+
this._auth = getCLIAuth();
|
|
58
86
|
} else {
|
|
59
87
|
throw new Error(
|
|
60
|
-
"Guava
|
|
88
|
+
"Unable to authenticate to Guava. You must do one of the following:\n- Sign in using the Guava CLI.\n- Or, provide an API key using the GUAVA_API_KEY environment variable.\n- Or, provide the API key as an argument to the constructor.",
|
|
61
89
|
);
|
|
62
90
|
}
|
|
63
91
|
|
|
@@ -70,8 +98,10 @@ export class Client {
|
|
|
70
98
|
});
|
|
71
99
|
}
|
|
72
100
|
|
|
73
|
-
telemetryClient.
|
|
74
|
-
|
|
101
|
+
telemetryClient.setSdkClient(this);
|
|
102
|
+
if (checkDeprecation) {
|
|
103
|
+
this._checkSdkDeprecation();
|
|
104
|
+
}
|
|
75
105
|
}
|
|
76
106
|
}
|
|
77
107
|
|
|
@@ -89,9 +119,9 @@ export class Client {
|
|
|
89
119
|
return this._baseUrl;
|
|
90
120
|
}
|
|
91
121
|
|
|
92
|
-
headers() {
|
|
122
|
+
async headers(): Promise<Record<string, string>> {
|
|
93
123
|
return {
|
|
94
|
-
|
|
124
|
+
...(await this._auth.getHeaders()),
|
|
95
125
|
"x-guava-platform": os.platform(),
|
|
96
126
|
"x-guava-runtime": process.release.name,
|
|
97
127
|
"x-guava-runtime-version": process.version,
|
|
@@ -108,7 +138,7 @@ export class Client {
|
|
|
108
138
|
url.searchParams.set("sdk_version", SDK_VERSION);
|
|
109
139
|
const response = await fetchOrThrow(url, {
|
|
110
140
|
method: "POST",
|
|
111
|
-
headers: this.headers(),
|
|
141
|
+
headers: await this.headers(),
|
|
112
142
|
});
|
|
113
143
|
const body = (await response.json()) as { deprecation_status: string };
|
|
114
144
|
if (body.deprecation_status === "supported") {
|
|
@@ -136,19 +166,90 @@ export class Client {
|
|
|
136
166
|
}
|
|
137
167
|
const response = await fetchOrThrow(url, {
|
|
138
168
|
method: "POST",
|
|
139
|
-
headers: this.headers(),
|
|
169
|
+
headers: await this.headers(),
|
|
140
170
|
});
|
|
141
171
|
const body = (await response.json()) as { webrtc_code: string };
|
|
142
172
|
return body.webrtc_code;
|
|
143
173
|
}
|
|
144
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Sends an SMS message from one of your Guava numbers.
|
|
177
|
+
* @param fromNumber - One of your Guava numbers (E.164). Must have SMS configured.
|
|
178
|
+
* @param toNumber - The recipient's number (E.164).
|
|
179
|
+
* @param message - The message body to send.
|
|
180
|
+
*/
|
|
181
|
+
async sendSms(fromNumber: string, toNumber: string, message: string): Promise<void> {
|
|
182
|
+
const url = new URL("v1/send-sms", this.getHttpBase());
|
|
183
|
+
await fetchOrThrow(url, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: { ...(await this.headers()), "Content-Type": "application/json" },
|
|
186
|
+
body: JSON.stringify({
|
|
187
|
+
from_number: fromNumber,
|
|
188
|
+
to_number: toNumber,
|
|
189
|
+
message,
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Waits for and returns the next inbound SMS sent from `fromNumber` to `toNumber`.
|
|
196
|
+
*
|
|
197
|
+
* Polls the inbox for messages received after this call begins, resolving once one
|
|
198
|
+
* arrives or `timeoutMs` elapses. Note the direction: `fromNumber` is the external
|
|
199
|
+
* number you're waiting to hear from, and `toNumber` is your Guava number — the
|
|
200
|
+
* opposite of {@link sendSms}.
|
|
201
|
+
*
|
|
202
|
+
* @param fromNumber - The external number to wait for a message from (E.164).
|
|
203
|
+
* @param toNumber - Your Guava number that will receive the message (E.164).
|
|
204
|
+
* @param options.timeoutMs - Max time to wait before giving up. Defaults to 60000.
|
|
205
|
+
* @param options.pollIntervalMs - Time between inbox checks. Defaults to 2000.
|
|
206
|
+
* @returns The message, or `null` if `timeoutMs` elapses with no new message.
|
|
207
|
+
*/
|
|
208
|
+
async nextSms(
|
|
209
|
+
fromNumber: string,
|
|
210
|
+
toNumber: string,
|
|
211
|
+
options?: { timeoutMs?: number; pollIntervalMs?: number },
|
|
212
|
+
): Promise<SmsMessage | null> {
|
|
213
|
+
const timeoutMs = options?.timeoutMs ?? 60_000;
|
|
214
|
+
const pollIntervalMs = options?.pollIntervalMs ?? 2_000;
|
|
215
|
+
const start = new Date().toISOString();
|
|
216
|
+
const deadline = Date.now() + timeoutMs;
|
|
217
|
+
while (true) {
|
|
218
|
+
const url = new URL("v1/messages", this.getHttpBase());
|
|
219
|
+
url.searchParams.set("to_number", toNumber);
|
|
220
|
+
url.searchParams.set("from_number", fromNumber);
|
|
221
|
+
url.searchParams.set("modality", "sms");
|
|
222
|
+
url.searchParams.set("start", start);
|
|
223
|
+
const response = await fetchOrThrow(url, {
|
|
224
|
+
method: "GET",
|
|
225
|
+
headers: await this.headers(),
|
|
226
|
+
});
|
|
227
|
+
// The endpoint returns matches oldest-first, so the earliest message after
|
|
228
|
+
// `start` is always the first element — we only need one, so `has_more`
|
|
229
|
+
// (which signals additional *later* messages) is irrelevant here.
|
|
230
|
+
const body = (await response.json()) as { messages: unknown[] };
|
|
231
|
+
if (body.messages?.length) {
|
|
232
|
+
return SmsMessage.parse(body.messages[0]);
|
|
233
|
+
}
|
|
234
|
+
const remaining = deadline - Date.now();
|
|
235
|
+
if (remaining <= 0) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
await sleep(Math.min(pollIntervalMs, remaining));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
145
242
|
/**
|
|
146
243
|
* @description use the Guava API to call out to a number
|
|
147
244
|
*/
|
|
148
|
-
createOutbound(
|
|
245
|
+
async createOutbound(
|
|
246
|
+
fromNumber: string | undefined,
|
|
247
|
+
toNumber: string,
|
|
248
|
+
callController: CallController,
|
|
249
|
+
) {
|
|
149
250
|
const url = new URL("v1/create-outbound", this.getWebsocketBase());
|
|
150
251
|
const ws = new WebSocket(url, {
|
|
151
|
-
headers: this.headers(),
|
|
252
|
+
headers: await this.headers(),
|
|
152
253
|
});
|
|
153
254
|
|
|
154
255
|
ws.addEventListener("open", async (_ev) => {
|
|
@@ -236,16 +337,16 @@ export class Client {
|
|
|
236
337
|
/**
|
|
237
338
|
* @description use the Guava API to receive calls at a given number
|
|
238
339
|
*/
|
|
239
|
-
listenInbound<U extends CallController>(
|
|
340
|
+
async listenInbound<U extends CallController>(
|
|
240
341
|
conn: InboundConnection,
|
|
241
342
|
controllerClassFactory: (logger: Logger) => U,
|
|
242
|
-
) {
|
|
343
|
+
): Promise<InboundListener> {
|
|
243
344
|
const callControllers: Record<string, U> = {};
|
|
244
345
|
|
|
245
346
|
// return a way to *stop* listening
|
|
246
347
|
const url = new URL("v1/listen-inbound", this.getWebsocketBase());
|
|
247
348
|
const ws = new WebSocket(url, {
|
|
248
|
-
headers: this.headers(),
|
|
349
|
+
headers: await this.headers(),
|
|
249
350
|
});
|
|
250
351
|
let agent_number: string | undefined;
|
|
251
352
|
let webrtc_code: string | undefined;
|
package/src/events.ts
CHANGED
|
@@ -89,9 +89,19 @@ export type OutboundCallFailed = z.infer<typeof OutboundCallFailed>;
|
|
|
89
89
|
|
|
90
90
|
export const BotSessionEnded = z.object({
|
|
91
91
|
event_type: z.literal("bot-session-ended"),
|
|
92
|
+
termination_reason: z.enum([
|
|
93
|
+
"user-hangup",
|
|
94
|
+
"bot-hangup",
|
|
95
|
+
"bot-failure",
|
|
96
|
+
"bot-transfer",
|
|
97
|
+
"voicemail",
|
|
98
|
+
]),
|
|
92
99
|
});
|
|
93
100
|
export type BotSessionEnded = z.infer<typeof BotSessionEnded>;
|
|
94
101
|
|
|
102
|
+
/** Why a bot session ended. */
|
|
103
|
+
export type TerminationReason = BotSessionEnded["termination_reason"];
|
|
104
|
+
|
|
95
105
|
export const ChoiceQueryEvent = z.object({
|
|
96
106
|
event_type: z.literal("choice-query"),
|
|
97
107
|
field_key: z.string(),
|
|
@@ -113,6 +123,30 @@ export const ExecuteActionEvent = z.object({
|
|
|
113
123
|
});
|
|
114
124
|
export type ExecuteActionEvent = z.infer<typeof ExecuteActionEvent>;
|
|
115
125
|
|
|
126
|
+
export type DTMFDigit =
|
|
127
|
+
| "0"
|
|
128
|
+
| "1"
|
|
129
|
+
| "2"
|
|
130
|
+
| "3"
|
|
131
|
+
| "4"
|
|
132
|
+
| "5"
|
|
133
|
+
| "6"
|
|
134
|
+
| "7"
|
|
135
|
+
| "8"
|
|
136
|
+
| "9"
|
|
137
|
+
| "*"
|
|
138
|
+
| "#"
|
|
139
|
+
| "A"
|
|
140
|
+
| "B"
|
|
141
|
+
| "C"
|
|
142
|
+
| "D";
|
|
143
|
+
|
|
144
|
+
export const DTMFPressedEvent = z.object({
|
|
145
|
+
event_type: z.literal("dtmf"),
|
|
146
|
+
digit: z.enum(["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "*", "#", "A", "B", "C", "D"]),
|
|
147
|
+
});
|
|
148
|
+
export type DTMFPressedEvent = z.infer<typeof DTMFPressedEvent>;
|
|
149
|
+
|
|
116
150
|
export const GuavaEvent = z.discriminatedUnion("event_type", [
|
|
117
151
|
SessionStartedEvent,
|
|
118
152
|
InboundCallEvent,
|
|
@@ -130,13 +164,29 @@ export const GuavaEvent = z.discriminatedUnion("event_type", [
|
|
|
130
164
|
ChoiceQueryEvent,
|
|
131
165
|
ActionRequestEvent,
|
|
132
166
|
ExecuteActionEvent,
|
|
167
|
+
DTMFPressedEvent,
|
|
133
168
|
]);
|
|
134
169
|
export type GuavaEvent = z.infer<typeof GuavaEvent>;
|
|
135
170
|
|
|
136
|
-
const _KNOWN_EVENT_TYPES = new Set(
|
|
171
|
+
const _KNOWN_EVENT_TYPES: Set<string> = new Set(
|
|
137
172
|
GuavaEvent.options.map((schema) => schema.shape.event_type.value),
|
|
138
173
|
);
|
|
139
174
|
|
|
175
|
+
export function decodeEventDict(data: Record<string, unknown>): GuavaEvent | null {
|
|
176
|
+
if (typeof data.event_type !== "string") {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Received event with non-string event_type: ${JSON.stringify(data.event_type)}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (!_KNOWN_EVENT_TYPES.has(data.event_type)) {
|
|
182
|
+
process.emitWarning(
|
|
183
|
+
`Received an unknown event type ${data.event_type}. Update to a newer version of this SDK.`,
|
|
184
|
+
);
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
return GuavaEvent.parse(data);
|
|
188
|
+
}
|
|
189
|
+
|
|
140
190
|
export function decodeEvent(
|
|
141
191
|
serialized_event: string | ArrayBuffer | Buffer | Buffer[],
|
|
142
192
|
): GuavaEvent | null {
|
|
@@ -154,15 +204,7 @@ export function decodeEvent(
|
|
|
154
204
|
} else {
|
|
155
205
|
data = JSON.parse(serialized_event.toString("utf8"));
|
|
156
206
|
}
|
|
157
|
-
|
|
158
|
-
if (!_KNOWN_EVENT_TYPES.has(data.event_type)) {
|
|
159
|
-
process.emitWarning(
|
|
160
|
-
`Received an unknown event type ${data.event_type}. Update to a newer version of this SDK.`,
|
|
161
|
-
);
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return GuavaEvent.parse(data);
|
|
207
|
+
return decodeEventDict(data);
|
|
166
208
|
}
|
|
167
209
|
|
|
168
210
|
export const InboundTunnelEvent = z.object({
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Client } from "../client.ts";
|
|
2
|
+
import { fetchOrThrow } from "../utils.ts";
|
|
3
|
+
|
|
4
|
+
export async function _generate(
|
|
5
|
+
client: Client,
|
|
6
|
+
prompt: string,
|
|
7
|
+
jsonSchema?: object,
|
|
8
|
+
): Promise<string> {
|
|
9
|
+
const url = new URL("v1/llm/generate", client.getHttpBase());
|
|
10
|
+
const body: Record<string, unknown> = { prompt };
|
|
11
|
+
if (jsonSchema !== undefined) body.json_schema = jsonSchema;
|
|
12
|
+
|
|
13
|
+
const response = await fetchOrThrow(url, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: { ...(await client.headers()), "Content-Type": "application/json" },
|
|
16
|
+
body: JSON.stringify(body),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return ((await response.json()) as { text: string }).text;
|
|
20
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export { Client, type InboundConnection } from "./client.ts";
|
|
2
|
+
export type { SmsMessage } from "./sms.ts";
|
|
2
3
|
export { CallController, type TaskObjective } from "./call-controller.ts";
|
|
3
4
|
export { Say, Field } from "./action-item.ts";
|
|
4
5
|
export { Logger, getConsoleLogger, getDefaultLogger } from "./logging.ts";
|
|
5
6
|
export { Agent, CallInfo } from "./agent.ts";
|
|
6
7
|
export { Call } from "./call.ts";
|
|
8
|
+
export type { BotSessionEnded, TerminationReason, DTMFPressedEvent, DTMFDigit } from "./events.ts";
|
|
9
|
+
export { TestSession } from "./testing/session.ts";
|
|
10
|
+
export { MockCall } from "./testing/mocks.ts";
|
package/src/logging.ts
CHANGED
|
@@ -37,26 +37,34 @@ function shouldLog(messageLevel: LogLevel, loggerLevel: LogLevel) {
|
|
|
37
37
|
|
|
38
38
|
function noop(format: string, ...args: unknown[]) {}
|
|
39
39
|
|
|
40
|
+
type ConsoleLevel = "debug" | "info" | "warn" | "error";
|
|
41
|
+
|
|
40
42
|
function makeColoredMethod(
|
|
41
|
-
|
|
42
|
-
level: LogLevel,
|
|
43
|
+
level: ConsoleLevel,
|
|
43
44
|
useColor: boolean,
|
|
44
45
|
): (format: string, ...args: unknown[]) => void {
|
|
45
|
-
if (!useColor) return
|
|
46
|
-
return (format: string, ...args: unknown[]) =>
|
|
47
|
-
|
|
46
|
+
if (!useColor) return (format: string, ...args: unknown[]) => console[level](format, ...args);
|
|
47
|
+
return (format: string, ...args: unknown[]) => {
|
|
48
|
+
const now = new Date();
|
|
49
|
+
const time = now.toLocaleTimeString("en-US", {
|
|
50
|
+
hour12: false,
|
|
51
|
+
hour: "2-digit",
|
|
52
|
+
minute: "2-digit",
|
|
53
|
+
second: "2-digit",
|
|
54
|
+
});
|
|
55
|
+
console[level](
|
|
56
|
+
`${LEVEL_COLORS[level]}[${level.toLocaleUpperCase().padEnd(5)} ${time}] ${format}${ANSI_RESET}`,
|
|
57
|
+
...args,
|
|
58
|
+
);
|
|
59
|
+
};
|
|
48
60
|
}
|
|
49
61
|
|
|
50
62
|
export function getConsoleLogger(loggerLevel: LogLevel, useColor = false): Logger {
|
|
51
63
|
return {
|
|
52
|
-
debug: shouldLog("debug", loggerLevel)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
warn: shouldLog("warn", loggerLevel) ? makeColoredMethod(console.warn, "warn", useColor) : noop,
|
|
57
|
-
error: shouldLog("error", loggerLevel)
|
|
58
|
-
? makeColoredMethod(console.error, "error", useColor)
|
|
59
|
-
: noop,
|
|
64
|
+
debug: shouldLog("debug", loggerLevel) ? makeColoredMethod("debug", useColor) : noop,
|
|
65
|
+
info: shouldLog("info", loggerLevel) ? makeColoredMethod("info", useColor) : noop,
|
|
66
|
+
warn: shouldLog("warn", loggerLevel) ? makeColoredMethod("warn", useColor) : noop,
|
|
67
|
+
error: shouldLog("error", loggerLevel) ? makeColoredMethod("error", useColor) : noop,
|
|
60
68
|
};
|
|
61
69
|
}
|
|
62
70
|
|
package/src/sms.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An inbound SMS message received on one of your Guava numbers.
|
|
5
|
+
*
|
|
6
|
+
* Field names mirror the wire format returned by `GET /v1/messages`.
|
|
7
|
+
*/
|
|
8
|
+
export const SmsMessage = z.object({
|
|
9
|
+
id: z.string(),
|
|
10
|
+
from_number: z.string(),
|
|
11
|
+
to_number: z.string(),
|
|
12
|
+
content: z.string(),
|
|
13
|
+
received_at: z.string(),
|
|
14
|
+
modality: z.literal("sms"),
|
|
15
|
+
direction: z.enum(["inbound", "outbound"]),
|
|
16
|
+
});
|
|
17
|
+
export type SmsMessage = z.infer<typeof SmsMessage>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import * as z from "zod";
|
|
2
|
+
|
|
3
|
+
export const PSTNCallInfo = z.object({
|
|
4
|
+
call_type: z.literal("pstn"),
|
|
5
|
+
from_number: z.string().nullable(),
|
|
6
|
+
to_number: z.string(),
|
|
7
|
+
caller_id: z.string().nullable(),
|
|
8
|
+
});
|
|
9
|
+
export type PSTNCallInfo = z.infer<typeof PSTNCallInfo>;
|
|
10
|
+
|
|
11
|
+
export const WebRTCCallInfo = z.object({
|
|
12
|
+
call_type: z.literal("webrtc"),
|
|
13
|
+
webrtc_code: z.string(),
|
|
14
|
+
});
|
|
15
|
+
export type WebRTCCallInfo = z.infer<typeof WebRTCCallInfo>;
|
|
16
|
+
|
|
17
|
+
export const SIPCallInfo = z.object({
|
|
18
|
+
call_type: z.literal("sip"),
|
|
19
|
+
from_aor: z.string(),
|
|
20
|
+
sip_code: z.string().optional(),
|
|
21
|
+
sip_headers: z.record(z.string(), z.string()).default({}),
|
|
22
|
+
});
|
|
23
|
+
export type SIPCallInfo = z.infer<typeof SIPCallInfo>;
|
|
24
|
+
|
|
25
|
+
export const CallInfo = z.discriminatedUnion("call_type", [
|
|
26
|
+
PSTNCallInfo,
|
|
27
|
+
WebRTCCallInfo,
|
|
28
|
+
SIPCallInfo,
|
|
29
|
+
]);
|
|
30
|
+
export type CallInfo = z.infer<typeof CallInfo>;
|