@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.
Files changed (102) hide show
  1. package/dist/examples/example.test.d.ts +5 -0
  2. package/dist/examples/example.test.js +46 -0
  3. package/dist/examples/example.test.js.map +1 -0
  4. package/dist/examples/help-desk.d.ts +3 -1
  5. package/dist/examples/help-desk.js +25 -9
  6. package/dist/examples/help-desk.js.map +1 -1
  7. package/dist/examples/property-insurance.js +4 -1
  8. package/dist/examples/property-insurance.js.map +1 -1
  9. package/dist/examples/restaurant-waitlist.js +4 -1
  10. package/dist/examples/restaurant-waitlist.js.map +1 -1
  11. package/dist/examples/scheduling-outbound.js +6 -0
  12. package/dist/examples/scheduling-outbound.js.map +1 -1
  13. package/dist/src/action-item.d.ts +4 -4
  14. package/dist/src/agent.d.ts +85 -18
  15. package/dist/src/agent.js +404 -129
  16. package/dist/src/agent.js.map +1 -1
  17. package/dist/src/auth.d.ts +27 -0
  18. package/dist/src/auth.js +127 -0
  19. package/dist/src/auth.js.map +1 -0
  20. package/dist/src/call.d.ts +1 -1
  21. package/dist/src/call.js +2 -2
  22. package/dist/src/call.js.map +1 -1
  23. package/dist/src/client.d.ts +38 -12
  24. package/dist/src/client.js +88 -16
  25. package/dist/src/client.js.map +1 -1
  26. package/dist/src/commands.d.ts +3 -3
  27. package/dist/src/events.d.ts +87 -0
  28. package/dist/src/events.js +25 -6
  29. package/dist/src/events.js.map +1 -1
  30. package/dist/src/helpers/llm.d.ts +2 -0
  31. package/dist/src/helpers/llm.js +17 -0
  32. package/dist/src/helpers/llm.js.map +1 -0
  33. package/dist/src/index.d.ts +4 -0
  34. package/dist/src/index.js +5 -1
  35. package/dist/src/index.js.map +1 -1
  36. package/dist/src/logging.js +16 -11
  37. package/dist/src/logging.js.map +1 -1
  38. package/dist/src/sms.d.ts +19 -0
  39. package/dist/src/sms.js +52 -0
  40. package/dist/src/sms.js.map +1 -0
  41. package/dist/src/socket/call-info.d.ts +35 -0
  42. package/dist/src/socket/call-info.js +59 -0
  43. package/dist/src/socket/call-info.js.map +1 -0
  44. package/dist/src/socket/client.d.ts +51 -0
  45. package/dist/src/socket/client.js +455 -0
  46. package/dist/src/socket/client.js.map +1 -0
  47. package/dist/src/socket/listen-inbound.d.ts +83 -0
  48. package/dist/src/socket/listen-inbound.js +82 -0
  49. package/dist/src/socket/listen-inbound.js.map +1 -0
  50. package/dist/src/socket/protocol.d.ts +127 -0
  51. package/dist/src/socket/protocol.js +69 -0
  52. package/dist/src/socket/protocol.js.map +1 -0
  53. package/dist/src/socket/utils.d.ts +8 -0
  54. package/dist/src/socket/utils.js +26 -0
  55. package/dist/src/socket/utils.js.map +1 -0
  56. package/dist/src/telemetry.d.ts +3 -3
  57. package/dist/src/telemetry.js +9 -7
  58. package/dist/src/telemetry.js.map +1 -1
  59. package/dist/src/testing/chat.d.ts +2 -0
  60. package/dist/src/testing/chat.js +181 -0
  61. package/dist/src/testing/chat.js.map +1 -0
  62. package/dist/src/testing/mocks.d.ts +6 -0
  63. package/dist/src/testing/mocks.js +14 -0
  64. package/dist/src/testing/mocks.js.map +1 -0
  65. package/dist/src/testing/protocol.d.ts +46 -0
  66. package/dist/src/testing/protocol.js +61 -0
  67. package/dist/src/testing/protocol.js.map +1 -0
  68. package/dist/src/testing/session.d.ts +26 -0
  69. package/dist/src/testing/session.js +219 -0
  70. package/dist/src/testing/session.js.map +1 -0
  71. package/dist/src/utils.d.ts +2 -0
  72. package/dist/src/utils.js +19 -1
  73. package/dist/src/utils.js.map +1 -1
  74. package/dist/src/version.d.ts +1 -1
  75. package/dist/src/version.js +1 -1
  76. package/examples/example.test.ts +58 -0
  77. package/examples/help-desk.ts +14 -3
  78. package/examples/property-insurance.ts +3 -1
  79. package/examples/restaurant-waitlist.ts +3 -1
  80. package/examples/scheduling-outbound.ts +7 -0
  81. package/package.json +10 -2
  82. package/src/agent.ts +386 -166
  83. package/src/auth.ts +109 -0
  84. package/src/call.ts +3 -3
  85. package/src/client.ts +119 -18
  86. package/src/events.ts +52 -10
  87. package/src/helpers/llm.ts +20 -0
  88. package/src/index.ts +4 -0
  89. package/src/logging.ts +21 -13
  90. package/src/sms.ts +17 -0
  91. package/src/socket/call-info.ts +30 -0
  92. package/src/socket/client.ts +433 -0
  93. package/src/socket/listen-inbound.ts +62 -0
  94. package/src/socket/protocol.ts +89 -0
  95. package/src/socket/utils.ts +25 -0
  96. package/src/telemetry.ts +11 -8
  97. package/src/testing/chat.ts +196 -0
  98. package/src/testing/mocks.ts +12 -0
  99. package/src/testing/protocol.ts +40 -0
  100. package/src/testing/session.ts +218 -0
  101. package/src/utils.ts +19 -1
  102. 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
- private _commandQueue: Command[] = [];
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 { getBaseUrl, fetchOrThrow } from "./utils.ts";
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 _apiKey: string;
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(apiKey?: string, baseUrl?: string, logger?: Logger, captureWarnings: boolean = true) {
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 the API key.
77
+ // Resolve auth strategy.
54
78
  if (apiKey) {
55
- this._apiKey = apiKey;
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._apiKey = process.env.GUAVA_API_KEY;
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 API key must be provided either as argument to client constructor, or in environment variable GUAVA_API_KEY.",
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.setSdkHeaders(this.headers());
74
- this._checkSdkDeprecation();
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
- Authorization: `Bearer ${this._apiKey}`,
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(fromNumber: string | undefined, toNumber: string, callController: CallController) {
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
- fn: (...args: unknown[]) => void,
42
- level: LogLevel,
43
+ level: ConsoleLevel,
43
44
  useColor: boolean,
44
45
  ): (format: string, ...args: unknown[]) => void {
45
- if (!useColor) return fn.bind(console);
46
- return (format: string, ...args: unknown[]) =>
47
- fn(`${LEVEL_COLORS[level]}[${level.toLocaleUpperCase()}] ${format}${ANSI_RESET}`, ...args);
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
- ? makeColoredMethod(console.debug, "debug", useColor)
54
- : noop,
55
- info: shouldLog("info", loggerLevel) ? makeColoredMethod(console.info, "info", useColor) : noop,
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>;