@guava-ai/guava-sdk 0.18.0 → 0.19.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 (98) 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 +81 -16
  15. package/dist/src/agent.js +394 -127
  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 +4 -11
  24. package/dist/src/client.js +22 -14
  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 +22 -0
  28. package/dist/src/events.js +19 -5
  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 +2 -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/socket/call-info.d.ts +35 -0
  39. package/dist/src/socket/call-info.js +59 -0
  40. package/dist/src/socket/call-info.js.map +1 -0
  41. package/dist/src/socket/client.d.ts +51 -0
  42. package/dist/src/socket/client.js +455 -0
  43. package/dist/src/socket/client.js.map +1 -0
  44. package/dist/src/socket/listen-inbound.d.ts +83 -0
  45. package/dist/src/socket/listen-inbound.js +82 -0
  46. package/dist/src/socket/listen-inbound.js.map +1 -0
  47. package/dist/src/socket/protocol.d.ts +127 -0
  48. package/dist/src/socket/protocol.js +69 -0
  49. package/dist/src/socket/protocol.js.map +1 -0
  50. package/dist/src/socket/utils.d.ts +8 -0
  51. package/dist/src/socket/utils.js +26 -0
  52. package/dist/src/socket/utils.js.map +1 -0
  53. package/dist/src/telemetry.d.ts +3 -3
  54. package/dist/src/telemetry.js +9 -7
  55. package/dist/src/telemetry.js.map +1 -1
  56. package/dist/src/testing/chat.d.ts +2 -0
  57. package/dist/src/testing/chat.js +181 -0
  58. package/dist/src/testing/chat.js.map +1 -0
  59. package/dist/src/testing/mocks.d.ts +6 -0
  60. package/dist/src/testing/mocks.js +14 -0
  61. package/dist/src/testing/mocks.js.map +1 -0
  62. package/dist/src/testing/protocol.d.ts +46 -0
  63. package/dist/src/testing/protocol.js +61 -0
  64. package/dist/src/testing/protocol.js.map +1 -0
  65. package/dist/src/testing/session.d.ts +26 -0
  66. package/dist/src/testing/session.js +219 -0
  67. package/dist/src/testing/session.js.map +1 -0
  68. package/dist/src/utils.d.ts +1 -0
  69. package/dist/src/utils.js +15 -1
  70. package/dist/src/utils.js.map +1 -1
  71. package/dist/src/version.d.ts +1 -1
  72. package/dist/src/version.js +1 -1
  73. package/examples/example.test.ts +58 -0
  74. package/examples/help-desk.ts +14 -3
  75. package/examples/property-insurance.ts +3 -1
  76. package/examples/restaurant-waitlist.ts +3 -1
  77. package/examples/scheduling-outbound.ts +7 -0
  78. package/package.json +9 -1
  79. package/src/agent.ts +372 -162
  80. package/src/auth.ts +109 -0
  81. package/src/call.ts +3 -3
  82. package/src/client.ts +32 -15
  83. package/src/events.ts +24 -10
  84. package/src/helpers/llm.ts +20 -0
  85. package/src/index.ts +2 -0
  86. package/src/logging.ts +21 -13
  87. package/src/socket/call-info.ts +30 -0
  88. package/src/socket/client.ts +433 -0
  89. package/src/socket/listen-inbound.ts +62 -0
  90. package/src/socket/protocol.ts +89 -0
  91. package/src/socket/utils.ts +25 -0
  92. package/src/telemetry.ts +11 -8
  93. package/src/testing/chat.ts +196 -0
  94. package/src/testing/mocks.ts +12 -0
  95. package/src/testing/protocol.ts +40 -0
  96. package/src/testing/session.ts +218 -0
  97. package/src/utils.ts +15 -1
  98. 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,9 +9,18 @@ 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 * as fs from "node:fs";
12
13
  import { getBaseUrl, fetchOrThrow } from "./utils.ts";
13
14
  import { telemetryClient } from "./telemetry.ts";
14
15
  import type { CallController } from "./call-controller.ts";
16
+ import {
17
+ type AuthStrategy,
18
+ APIKeyAuth,
19
+ GuavaDeploy,
20
+ CLIAuth,
21
+ getCLIAuth,
22
+ GUAVA_DEPLOY_TOKEN_PATH,
23
+ } from "./auth.ts";
15
24
 
16
25
  const SDK_NAME = "typescript-sdk";
17
26
 
@@ -28,7 +37,7 @@ const https_start = /^https:\/\//;
28
37
 
29
38
  @telemetryClient.trackClass()
30
39
  export class Client {
31
- private _apiKey: string;
40
+ private _auth: AuthStrategy;
32
41
  private _baseUrl: string;
33
42
  private _logger: Logger;
34
43
  private _ws?: WebSocket;
@@ -50,14 +59,18 @@ export class Client {
50
59
  this._baseUrl = getBaseUrl();
51
60
  }
52
61
 
53
- // Resolve the API key.
62
+ // Resolve auth strategy.
54
63
  if (apiKey) {
55
- this._apiKey = apiKey;
64
+ this._auth = new APIKeyAuth(apiKey);
65
+ } else if (fs.existsSync(GUAVA_DEPLOY_TOKEN_PATH)) {
66
+ this._auth = new GuavaDeploy();
56
67
  } else if (process.env.GUAVA_API_KEY) {
57
- this._apiKey = process.env.GUAVA_API_KEY;
68
+ this._auth = new APIKeyAuth(process.env.GUAVA_API_KEY);
69
+ } else if (CLIAuth.exists()) {
70
+ this._auth = getCLIAuth();
58
71
  } else {
59
72
  throw new Error(
60
- "Guava API key must be provided either as argument to client constructor, or in environment variable GUAVA_API_KEY.",
73
+ "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
74
  );
62
75
  }
63
76
 
@@ -70,7 +83,7 @@ export class Client {
70
83
  });
71
84
  }
72
85
 
73
- telemetryClient.setSdkHeaders(this.headers());
86
+ telemetryClient.setSdkClient(this);
74
87
  this._checkSdkDeprecation();
75
88
  }
76
89
  }
@@ -89,9 +102,9 @@ export class Client {
89
102
  return this._baseUrl;
90
103
  }
91
104
 
92
- headers() {
105
+ async headers(): Promise<Record<string, string>> {
93
106
  return {
94
- Authorization: `Bearer ${this._apiKey}`,
107
+ ...(await this._auth.getHeaders()),
95
108
  "x-guava-platform": os.platform(),
96
109
  "x-guava-runtime": process.release.name,
97
110
  "x-guava-runtime-version": process.version,
@@ -108,7 +121,7 @@ export class Client {
108
121
  url.searchParams.set("sdk_version", SDK_VERSION);
109
122
  const response = await fetchOrThrow(url, {
110
123
  method: "POST",
111
- headers: this.headers(),
124
+ headers: await this.headers(),
112
125
  });
113
126
  const body = (await response.json()) as { deprecation_status: string };
114
127
  if (body.deprecation_status === "supported") {
@@ -136,7 +149,7 @@ export class Client {
136
149
  }
137
150
  const response = await fetchOrThrow(url, {
138
151
  method: "POST",
139
- headers: this.headers(),
152
+ headers: await this.headers(),
140
153
  });
141
154
  const body = (await response.json()) as { webrtc_code: string };
142
155
  return body.webrtc_code;
@@ -145,10 +158,14 @@ export class Client {
145
158
  /**
146
159
  * @description use the Guava API to call out to a number
147
160
  */
148
- createOutbound(fromNumber: string | undefined, toNumber: string, callController: CallController) {
161
+ async createOutbound(
162
+ fromNumber: string | undefined,
163
+ toNumber: string,
164
+ callController: CallController,
165
+ ) {
149
166
  const url = new URL("v1/create-outbound", this.getWebsocketBase());
150
167
  const ws = new WebSocket(url, {
151
- headers: this.headers(),
168
+ headers: await this.headers(),
152
169
  });
153
170
 
154
171
  ws.addEventListener("open", async (_ev) => {
@@ -236,16 +253,16 @@ export class Client {
236
253
  /**
237
254
  * @description use the Guava API to receive calls at a given number
238
255
  */
239
- listenInbound<U extends CallController>(
256
+ async listenInbound<U extends CallController>(
240
257
  conn: InboundConnection,
241
258
  controllerClassFactory: (logger: Logger) => U,
242
- ) {
259
+ ): Promise<InboundListener> {
243
260
  const callControllers: Record<string, U> = {};
244
261
 
245
262
  // return a way to *stop* listening
246
263
  const url = new URL("v1/listen-inbound", this.getWebsocketBase());
247
264
  const ws = new WebSocket(url, {
248
- headers: this.headers(),
265
+ headers: await this.headers(),
249
266
  });
250
267
  let agent_number: string | undefined;
251
268
  let webrtc_code: string | undefined;
package/src/events.ts CHANGED
@@ -89,6 +89,13 @@ 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
 
@@ -133,10 +140,25 @@ export const GuavaEvent = z.discriminatedUnion("event_type", [
133
140
  ]);
134
141
  export type GuavaEvent = z.infer<typeof GuavaEvent>;
135
142
 
136
- const _KNOWN_EVENT_TYPES = new Set(
143
+ const _KNOWN_EVENT_TYPES: Set<string> = new Set(
137
144
  GuavaEvent.options.map((schema) => schema.shape.event_type.value),
138
145
  );
139
146
 
147
+ export function decodeEventDict(data: Record<string, unknown>): GuavaEvent | null {
148
+ if (typeof data.event_type !== "string") {
149
+ throw new Error(
150
+ `Received event with non-string event_type: ${JSON.stringify(data.event_type)}`,
151
+ );
152
+ }
153
+ if (!_KNOWN_EVENT_TYPES.has(data.event_type)) {
154
+ process.emitWarning(
155
+ `Received an unknown event type ${data.event_type}. Update to a newer version of this SDK.`,
156
+ );
157
+ return null;
158
+ }
159
+ return GuavaEvent.parse(data);
160
+ }
161
+
140
162
  export function decodeEvent(
141
163
  serialized_event: string | ArrayBuffer | Buffer | Buffer[],
142
164
  ): GuavaEvent | null {
@@ -154,15 +176,7 @@ export function decodeEvent(
154
176
  } else {
155
177
  data = JSON.parse(serialized_event.toString("utf8"));
156
178
  }
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);
179
+ return decodeEventDict(data);
166
180
  }
167
181
 
168
182
  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
@@ -4,3 +4,5 @@ export { Say, Field } from "./action-item.ts";
4
4
  export { Logger, getConsoleLogger, getDefaultLogger } from "./logging.ts";
5
5
  export { Agent, CallInfo } from "./agent.ts";
6
6
  export { Call } from "./call.ts";
7
+ export { TestSession } from "./testing/session.ts";
8
+ 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
 
@@ -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>;