@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/dist/src/agent.js CHANGED
@@ -66,6 +66,58 @@ var __importStar = (this && this.__importStar) || (function () {
66
66
  return result;
67
67
  };
68
68
  })();
69
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
70
+ if (value !== null && value !== void 0) {
71
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
72
+ var dispose, inner;
73
+ if (async) {
74
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
75
+ dispose = value[Symbol.asyncDispose];
76
+ }
77
+ if (dispose === void 0) {
78
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
79
+ dispose = value[Symbol.dispose];
80
+ if (async) inner = dispose;
81
+ }
82
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
83
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
84
+ env.stack.push({ value: value, dispose: dispose, async: async });
85
+ }
86
+ else if (async) {
87
+ env.stack.push({ async: true });
88
+ }
89
+ return value;
90
+ };
91
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
92
+ return function (env) {
93
+ function fail(e) {
94
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
95
+ env.hasError = true;
96
+ }
97
+ var r, s = 0;
98
+ function next() {
99
+ while (r = env.stack.pop()) {
100
+ try {
101
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
102
+ if (r.dispose) {
103
+ var result = r.dispose.call(r.value);
104
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
105
+ }
106
+ else s |= 1;
107
+ }
108
+ catch (e) {
109
+ fail(e);
110
+ }
111
+ }
112
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
113
+ if (env.hasError) throw env.error;
114
+ }
115
+ return next();
116
+ };
117
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
118
+ var e = new Error(message);
119
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
120
+ });
69
121
  var __importDefault = (this && this.__importDefault) || function (mod) {
70
122
  return (mod && mod.__esModule) ? mod : { "default": mod };
71
123
  };
@@ -76,16 +128,15 @@ const index_ts_1 = require("./index.js");
76
128
  const logging_ts_1 = require("./logging.js");
77
129
  const utils_ts_1 = require("./utils.js");
78
130
  const webrtc_helper_ts_1 = require("./webrtc-helper.js");
79
- const z = __importStar(require("zod"));
80
131
  const commands_ts_1 = require("./commands.js");
81
132
  const events_ts_1 = require("./events.js");
82
133
  const telemetry_ts_1 = require("./telemetry.js");
83
- /**
84
- * @description convenience function for stringifying data according to a schema
85
- */
86
- function stringifyZod(schema, data) {
87
- return JSON.stringify(schema.parse(data));
88
- }
134
+ const client_ts_1 = require("./socket/client.js");
135
+ const ListenInbound = __importStar(require("./socket/listen-inbound.js"));
136
+ const session_ts_1 = require("./testing/session.js");
137
+ const protocol_ts_1 = require("./testing/protocol.js");
138
+ const chat_ts_1 = require("./testing/chat.js");
139
+ const llm_ts_1 = require("./helpers/llm.js");
89
140
  let Agent = (() => {
90
141
  let _classDecorators = [telemetry_ts_1.telemetryClient.trackClass()];
91
142
  let _classDescriptor;
@@ -119,6 +170,7 @@ let Agent = (() => {
119
170
  _onActionGeneric;
120
171
  _onActionHandlers = {};
121
172
  _onSessionEnd;
173
+ _onDtmf;
122
174
  constructor(args) {
123
175
  this._name = args?.name;
124
176
  this._organization = args?.organization;
@@ -175,16 +227,71 @@ let Agent = (() => {
175
227
  onSessionEnd(callback) {
176
228
  this._onSessionEnd = callback;
177
229
  }
230
+ onDtmf(callback) {
231
+ this._onDtmf = callback;
232
+ }
233
+ get handlers() {
234
+ return {
235
+ onCallReceived: (callInfo) => this._onCallReceived(callInfo),
236
+ onCallStart: (call) => {
237
+ if (!this._onCallStart)
238
+ throw new Error("No onCallStart handler registered.");
239
+ return this._onCallStart(call);
240
+ },
241
+ onCallerSpeech: (call, event) => {
242
+ if (!this._onCallerSpeech)
243
+ throw new Error("No onCallerSpeech handler registered.");
244
+ return this._onCallerSpeech(call, event);
245
+ },
246
+ onAgentSpeech: (call, event) => {
247
+ if (!this._onAgentSpeech)
248
+ throw new Error("No onAgentSpeech handler registered.");
249
+ return this._onAgentSpeech(call, event);
250
+ },
251
+ onQuestion: (call, question) => {
252
+ if (!this._onQuestion)
253
+ throw new Error("No onQuestion handler registered.");
254
+ return this._onQuestion(call, question);
255
+ },
256
+ onTaskComplete: (taskId, call) => {
257
+ if (this._onTaskCompleteGeneric)
258
+ return this._onTaskCompleteGeneric(call, taskId);
259
+ if (taskId in this._onTaskCompleteHandlers)
260
+ return this._onTaskCompleteHandlers[taskId](call);
261
+ throw new Error(`No onTaskComplete handler registered for task '${taskId}'.`);
262
+ },
263
+ onSearchQuery: (fieldKey, call, query) => {
264
+ if (!(fieldKey in this._searchQueryHandlers))
265
+ throw new Error(`No onSearchQuery handler registered for field '${fieldKey}'.`);
266
+ return this._searchQueryHandlers[fieldKey](call, query);
267
+ },
268
+ onActionRequest: (call, intentSummary) => {
269
+ if (!this._onActionRequested)
270
+ throw new Error("No onActionRequest handler registered.");
271
+ return this._onActionRequested(call, intentSummary);
272
+ },
273
+ onAction: (actionKey, call) => {
274
+ if (this._onActionGeneric)
275
+ return this._onActionGeneric(call, actionKey);
276
+ if (actionKey in this._onActionHandlers)
277
+ return this._onActionHandlers[actionKey](call);
278
+ throw new Error(`No onAction handler registered for action '${actionKey}'.`);
279
+ },
280
+ onSessionEnd: (call, event) => {
281
+ if (!this._onSessionEnd)
282
+ throw new Error("No onSessionEnd handler registered.");
283
+ return this._onSessionEnd(call, event);
284
+ },
285
+ };
286
+ }
178
287
  onReachPerson(callback) {
179
288
  this.onTaskComplete("reach_person", async (call) => {
180
289
  const availability = (await call.getField("contact_availability"));
181
290
  await callback(call, availability);
182
291
  });
183
292
  }
184
- listenPhone(phoneNumber) {
185
- return this._listenInbound({
186
- agent_number: phoneNumber,
187
- });
293
+ async listenPhone(phoneNumber) {
294
+ return this._listenInbound({ agent_number: phoneNumber });
188
295
  }
189
296
  async listenWebrtc(webrtcCode) {
190
297
  if (!webrtcCode) {
@@ -195,10 +302,12 @@ let Agent = (() => {
195
302
  }
196
303
  async callLocal() {
197
304
  const webrtcCode = await this._client.createWebrtcAgent(300);
198
- this._listenInbound({ webrtc_code: webrtcCode });
305
+ this._listenInbound({ webrtc_code: webrtcCode }).catch((err) => {
306
+ this._logger.error("Listen loop error: %s", err);
307
+ });
199
308
  await (0, webrtc_helper_ts_1.runWebrtcHelper)(webrtcCode, (0, utils_ts_1.getBaseUrl)());
200
309
  }
201
- async _dispatchEvent(call, event) {
310
+ async _dispatchEvent(call, event, testSession) {
202
311
  if (event.event_type === "caller-speech") {
203
312
  if (this._onCallerSpeech !== undefined) {
204
313
  await this._onCallerSpeech(call, event);
@@ -209,19 +318,6 @@ let Agent = (() => {
209
318
  await this._onAgentSpeech(call, event);
210
319
  }
211
320
  }
212
- else if (event.event_type === "inbound-call") {
213
- this._logger.info(`Received inbound call from ${event.caller_number ?? "unknown"}`);
214
- const action = await this._onCallReceived({
215
- caller_number: event.caller_number,
216
- agent_number: event.agent_number,
217
- });
218
- if (action.action === "accept") {
219
- call.sendCommand(commands_ts_1.AcceptInboundCallCommand, { command_type: "accept-inbound" });
220
- }
221
- else {
222
- call.sendCommand(commands_ts_1.RejectInboundCallCommand, { command_type: "reject-inbound" });
223
- }
224
- }
225
321
  else if (event.event_type === "task-done") {
226
322
  this._logger.info(`Task ${event.task_id} completed.`);
227
323
  if (this._onTaskCompleteGeneric !== undefined) {
@@ -245,7 +341,7 @@ let Agent = (() => {
245
341
  this._logger.error(`Error occurred while answering question: ${err}`);
246
342
  answer = "An error occurred and the question could not be answered.";
247
343
  }
248
- call.sendCommand(commands_ts_1.AnswerQuestionCommand, {
344
+ await call.sendCommand(commands_ts_1.AnswerQuestionCommand, {
249
345
  command_type: "answer-question",
250
346
  question_id: event.question_id,
251
347
  answer,
@@ -253,7 +349,7 @@ let Agent = (() => {
253
349
  }
254
350
  else {
255
351
  this._logger.warn(`Received question but no onQuestion handler is registered: ${event.question}`);
256
- call.sendCommand(commands_ts_1.AnswerQuestionCommand, {
352
+ await call.sendCommand(commands_ts_1.AnswerQuestionCommand, {
257
353
  command_type: "answer-question",
258
354
  question_id: event.question_id,
259
355
  answer: "I don't have an answer to that question.",
@@ -261,7 +357,6 @@ let Agent = (() => {
261
357
  }
262
358
  }
263
359
  else if (event.event_type === "action-item-done") {
264
- this._logger.info(`Action item '${event.key}' completed.`);
265
360
  call._fieldValues[event.key] = event.payload;
266
361
  }
267
362
  else if (event.event_type === "choice-query") {
@@ -272,7 +367,7 @@ let Agent = (() => {
272
367
  }
273
368
  else {
274
369
  const [matchedChoices, otherChoices] = await handler(call, event.query);
275
- call.sendCommand(commands_ts_1.ChoiceResultCommand, {
370
+ await call.sendCommand(commands_ts_1.ChoiceResultCommand, {
276
371
  command_type: "choice-query-result",
277
372
  field_key: event.field_key,
278
373
  query_id: event.query_id,
@@ -287,7 +382,7 @@ let Agent = (() => {
287
382
  if (this._onActionRequested !== undefined) {
288
383
  suggestion = await this._onActionRequested(call, event.intent_summary);
289
384
  }
290
- call.sendCommand(commands_ts_1.ActionSuggestionCommand, {
385
+ await call.sendCommand(commands_ts_1.ActionSuggestionCommand, {
291
386
  command_type: "action-suggestion",
292
387
  intent_id: event.intent_id,
293
388
  action_key: suggestion?.key ?? null,
@@ -296,6 +391,9 @@ let Agent = (() => {
296
391
  }
297
392
  else if (event.event_type === "execute-action") {
298
393
  this._logger.info(`Executing action '${event.action_key}'`);
394
+ if (testSession) {
395
+ testSession.executedActions.push(event.action_key);
396
+ }
299
397
  let onActionFunc;
300
398
  if (this._onActionGeneric !== undefined) {
301
399
  onActionFunc = () => this._onActionGeneric(call, event.action_key);
@@ -311,9 +409,15 @@ let Agent = (() => {
311
409
  }
312
410
  }
313
411
  else if (event.event_type === "bot-session-ended") {
314
- this._logger.info("Session ended.");
315
- if (this._onSessionEnd !== undefined) {
316
- await this._onSessionEnd(call);
412
+ this._logger.info(`Session ended: ${event.termination_reason}`);
413
+ if (testSession) {
414
+ testSession.terminationReason = event.termination_reason;
415
+ }
416
+ await this._onSessionEnd?.(call, event);
417
+ }
418
+ else if (event.event_type === "dtmf") {
419
+ if (this._onDtmf !== undefined) {
420
+ await this._onDtmf(call, event);
317
421
  }
318
422
  }
319
423
  else if (event.event_type === "error") {
@@ -323,14 +427,14 @@ let Agent = (() => {
323
427
  this._logger.warn(`The Guava agent reported a warning: ${event.content}`);
324
428
  }
325
429
  }
326
- async _startCall(variables = {}) {
430
+ async _initCall(variables = {}) {
327
431
  const call = new index_ts_1.Call(variables);
328
- call.setPersona({
432
+ await call.setPersona({
329
433
  agentName: this._name,
330
434
  agentPurpose: this._purpose,
331
435
  organizationName: this._organization,
332
436
  });
333
- call.sendCommand(commands_ts_1.RegisteredHooksCommand, {
437
+ await call.sendCommand(commands_ts_1.RegisteredHooksCommand, {
334
438
  command_type: "registered-hooks",
335
439
  has_on_question: this._onQuestion !== undefined,
336
440
  has_on_intent: false,
@@ -341,111 +445,291 @@ let Agent = (() => {
341
445
  }
342
446
  return call;
343
447
  }
344
- _listenInbound(conn) {
345
- const calls = {};
346
- // return a way to *stop* listening
347
- const url = new URL("v1/listen-inbound", this._client.getWebsocketBase());
348
- const ws = new ws_1.default(url, {
349
- headers: this._client.headers(),
350
- });
351
- let agent_number;
352
- let webrtc_code;
353
- if ("agent_number" in conn) {
354
- agent_number = conn.agent_number;
448
+ async _attachToCall(callId, initialVariables = {}, testSession) {
449
+ const env_1 = { stack: [], error: void 0, hasError: false };
450
+ try {
451
+ const call = await this._initCall(initialVariables);
452
+ const url = new URL(`v2/connect-call/${callId}`, this._client.getWebsocketBase());
453
+ const socket = __addDisposableResource(env_1, await new client_ts_1.GuavaSocket(`call-connection-${callId}`, url.toString(), this._client, (cmd) => cmd, (payload) => (0, events_ts_1.decodeEventDict)(payload), 18000).connect(), true);
454
+ await call.setDrain(async (commands) => {
455
+ for (const command of commands.splice(0)) {
456
+ socket.send(command);
457
+ }
458
+ });
459
+ try {
460
+ for await (const event of socket) {
461
+ if (event === null)
462
+ continue;
463
+ await this._dispatchEvent(call, event, testSession);
464
+ if (event.event_type === "bot-session-ended" ||
465
+ event.event_type === "outbound-call-failed") {
466
+ break;
467
+ }
468
+ }
469
+ }
470
+ catch (e) {
471
+ if (!(e instanceof client_ts_1.GuavaSocketClosedError))
472
+ throw e;
473
+ }
355
474
  }
356
- else {
357
- webrtc_code = conn.webrtc_code;
475
+ catch (e_1) {
476
+ env_1.error = e_1;
477
+ env_1.hasError = true;
358
478
  }
359
- this._logger.info(`Listening for calls to ${agent_number ?? webrtc_code}`);
360
- if (webrtc_code) {
361
- const debugurl = new URL(`debug-webrtc?webrtc_code=${webrtc_code}`, this._client.getHttpBase());
362
- this._logger.info(`Call your agent at: ${debugurl}`);
479
+ finally {
480
+ const result_1 = __disposeResources(env_1);
481
+ if (result_1)
482
+ await result_1;
363
483
  }
364
- ws.addEventListener("open", (_ev) => {
365
- ws.send(stringifyZod(commands_ts_1.ListenInboundCommand, {
366
- command_type: "listen-inbound",
367
- agent_number: agent_number,
368
- webrtc_code: webrtc_code,
369
- }));
370
- });
371
- ws.addEventListener("close", (_ev) => {
372
- ws.removeAllListeners();
373
- });
374
- ws.addEventListener("message", async (ev) => {
375
- this._logger.debug("Received message: %s", ev.data.toString("utf8"));
376
- const tunnel_event = events_ts_1.InboundTunnelEvent.parse(JSON.parse(ev.data.toString("utf8")));
377
- if (!(tunnel_event.call_id in calls)) {
378
- this._logger.info(`Received tunnel event for new call ID: ${tunnel_event.call_id}. Creating call object.`);
379
- const call = await this._startCall();
380
- await call.setDrain(async (commands) => {
381
- for (const command of commands.splice(0)) {
382
- this._logger.debug(`Sending command: ${JSON.stringify(command)} for call ID: ${tunnel_event.call_id}`);
383
- ws.send(stringifyZod(commands_ts_1.InboundTunnelCommand, {
384
- call_id: tunnel_event.call_id,
385
- command,
386
- }));
484
+ }
485
+ async _listenInbound(conn) {
486
+ const env_2 = { stack: [], error: void 0, hasError: false };
487
+ try {
488
+ const url = new URL("v2/listen-inbound", this._client.getWebsocketBase());
489
+ if ("agent_number" in conn) {
490
+ url.searchParams.set("phone_number", conn.agent_number);
491
+ }
492
+ else {
493
+ url.searchParams.set("webrtc_code", conn.webrtc_code);
494
+ }
495
+ const socket = __addDisposableResource(env_2, await new client_ts_1.GuavaSocket("listen-inbound", url.toString(), this._client, (msg) => msg, ListenInbound.decodeServerMessage).connect(), true);
496
+ try {
497
+ for await (const msg of socket) {
498
+ switch (msg.message_type) {
499
+ case "listen-started":
500
+ if ("agent_number" in conn) {
501
+ this._logger.info("Listening on %s (%d other listeners).", conn.agent_number, msg.other_listeners);
502
+ }
503
+ else {
504
+ this._logger.info("Listening on WebRTC code %s (%d other listeners).", conn.webrtc_code, msg.other_listeners);
505
+ const debugUrl = new URL(`debug-webrtc?webrtc_code=${conn.webrtc_code}`, this._client.getHttpBase());
506
+ this._logger.info("Call your agent at: %s", debugUrl);
507
+ }
508
+ break;
509
+ case "incoming-call":
510
+ socket.send({ message_type: "claim-call", call_id: msg.call_id });
511
+ break;
512
+ case "assign-call": {
513
+ const { call_id, call_info } = msg;
514
+ this._logger.info("Received call (session ID: %s)", call_id);
515
+ this._handleAssignedCall(call_id, call_info, socket).catch((err) => {
516
+ this._logger.error("Error handling assigned call %s: %s", call_id, err);
517
+ });
518
+ break;
519
+ }
387
520
  }
388
- });
389
- calls[tunnel_event.call_id] = call;
521
+ }
390
522
  }
391
- this._dispatchEvent(calls[tunnel_event.call_id], tunnel_event.event);
392
- });
393
- return new InboundListener(ws);
523
+ catch (e) {
524
+ if (!(e instanceof client_ts_1.GuavaSocketClosedError))
525
+ throw e;
526
+ }
527
+ }
528
+ catch (e_2) {
529
+ env_2.error = e_2;
530
+ env_2.hasError = true;
531
+ }
532
+ finally {
533
+ const result_2 = __disposeResources(env_2);
534
+ if (result_2)
535
+ await result_2;
536
+ }
537
+ }
538
+ async _handleAssignedCall(callId, callInfo, socket, initialVariables = {}) {
539
+ const action = await this._onCallReceived(callInfo);
540
+ if (action.action === "decline") {
541
+ this._logger.info("Declining call %s.", callId);
542
+ socket.send({ message_type: "decline-call", call_id: callId });
543
+ }
544
+ else {
545
+ this._logger.info("Accepting call %s.", callId);
546
+ socket.send({ message_type: "answer-call", call_id: callId });
547
+ await this._attachToCall(callId, initialVariables);
548
+ }
394
549
  }
395
550
  /**
396
551
  * @description use the Guava API to call out to a number
397
552
  */
398
553
  async callPhone(fromNumber, toNumber, variables = {}) {
399
- const url = new URL("v1/create-outbound", this._client.getWebsocketBase());
400
- const ws = new ws_1.default(url, {
401
- headers: this._client.headers(),
554
+ const url = new URL("v2/create-outbound", this._client.getHttpBase());
555
+ if (fromNumber)
556
+ url.searchParams.set("from_number", fromNumber);
557
+ url.searchParams.set("to_number", toNumber);
558
+ const response = await (0, utils_ts_1.fetchOrThrow)(url, {
559
+ method: "POST",
560
+ headers: await this._client.headers(),
402
561
  });
403
- const call = await this._startCall(variables);
404
- let socketInitialized = false;
405
- ws.addEventListener("open", async (_ev) => {
406
- ws.send(stringifyZod(commands_ts_1.StartOutboundCallCommand, {
407
- command_type: "start-outbound",
408
- to_number: toNumber,
409
- from_number: fromNumber,
410
- }));
411
- // set the callController drain function to send all commands
412
- // through the now open websocket
413
- call.setDrain(async (commands) => {
414
- for (const command of commands.splice(0)) {
415
- this._logger.debug(`Sending command ${JSON.stringify(command)}`);
416
- ws.send(JSON.stringify(command));
417
- }
418
- });
562
+ const { call_id } = (await response.json());
563
+ this._logger.info("Outbound call created with session ID: %s", call_id);
564
+ await this._attachToCall(call_id, variables);
565
+ }
566
+ /**
567
+ * Run the agent against a live test session.
568
+ *
569
+ * Connects to the Guava test endpoint, starts the agent's call handling, and
570
+ * calls `callback` with a TestSession for driving the conversation
571
+ * programmatically. Returns the completed TestSession after the callback and
572
+ * call handler both finish.
573
+ *
574
+ * @example
575
+ * const session = await agent.test(async (session) => {
576
+ * await session.waitForTurn();
577
+ * session.say("Hi, I'd like to make a purchase.");
578
+ * await session.waitForEnd();
579
+ * });
580
+ * assert(session.executedActions.includes("sales"));
581
+ */
582
+ async test(callback, variables = {}) {
583
+ const url = new URL("v1/test-agent", this._client.getWebsocketBase());
584
+ const headers = await this._client.headers();
585
+ const ws = new ws_1.default(url.toString(), { headers });
586
+ await new Promise((resolve, reject) => {
587
+ ws.once("open", resolve);
588
+ ws.once("error", reject);
589
+ });
590
+ const rawFirst = await new Promise((resolve, reject) => {
591
+ ws.once("message", (data) => resolve(data.toString()));
592
+ ws.once("error", reject);
419
593
  });
420
- ws.addEventListener("message", (ev) => {
421
- if (socketInitialized) {
422
- const session_started = z
423
- .union([events_ts_1.SessionStartedEvent, events_ts_1.ErrorEvent])
424
- .parse(JSON.parse(ev.data.toString("utf8")));
425
- if (session_started.event_type === "error") {
426
- throw new Error(`Outbound call failed: ${session_started.content}`);
594
+ const sessionStarted = protocol_ts_1.SessionStarted.parse(JSON.parse(rawFirst));
595
+ const testSession = new session_ts_1.TestSession(ws, this._client);
596
+ const attachPromise = this._attachToCall(sessionStarted.session_id, variables, testSession).catch((err) => {
597
+ this._logger.error("Error in _attachToCall during test: %s", err);
598
+ });
599
+ try {
600
+ await callback(testSession);
601
+ }
602
+ finally {
603
+ ws.close();
604
+ await attachPromise;
605
+ }
606
+ return testSession;
607
+ }
608
+ /**
609
+ * Run an automated test conversation where an LLM plays the caller.
610
+ *
611
+ * Connects to the Guava test endpoint, starts the agent, then drives a
612
+ * back-and-forth conversation by repeatedly asking the Guava LLM to decide
613
+ * whether to speak or hang up based on the transcript so far.
614
+ *
615
+ * @param roleplayPrompt - Instructions for the simulated caller, e.g.
616
+ * `"You are a frustrated customer trying to cancel your subscription."`
617
+ * @param variables - Optional initial call variables.
618
+ * @returns The completed TestSession. Call `session.evaluate()` to assert
619
+ * pass/fail criteria, or `session.getTranscript()` to inspect the conversation.
620
+ *
621
+ * @example
622
+ * const session = await agent.testRoleplay(
623
+ * "You are a caller trying to buy a new table.",
624
+ * );
625
+ * assert(session.executedActions.includes("sales"));
626
+ */
627
+ async testRoleplay(roleplayPrompt, variables = {}) {
628
+ const roleplaySchema = {
629
+ type: "object",
630
+ properties: {
631
+ action: { type: "string", enum: ["speak", "hangup"] },
632
+ utterance: { type: "string" },
633
+ },
634
+ required: ["action"],
635
+ additionalProperties: false,
636
+ };
637
+ return this.test(async (session) => {
638
+ let snapshotLen = 0;
639
+ try {
640
+ while (true) {
641
+ await session.waitForTurn();
642
+ for (const event of session._events.slice(snapshotLen)) {
643
+ if (event.message_type === "bot-tts") {
644
+ this._logger.info("(Roleplay Session) [agent]: %s", event.transcript);
645
+ }
646
+ }
647
+ snapshotLen = session._events.length;
648
+ const transcript = session.getTranscript();
649
+ const prompt = `${roleplayPrompt}
650
+
651
+ You are roleplaying as a caller on a phone call. Decide what to do next based on the conversation so far.
652
+
653
+ Conversation:
654
+ ${transcript || "(The agent has not spoken yet)"}
655
+
656
+ Choose "speak" and provide your next utterance, or choose "hangup" if the conversation has naturally concluded.`;
657
+ const raw = await (0, llm_ts_1._generate)(this._client, prompt, roleplaySchema);
658
+ const action = JSON.parse(raw);
659
+ if (action.action === "hangup") {
660
+ this._logger.info("(Roleplay Session) [caller hangs up]");
661
+ break;
662
+ }
663
+ if (action.action === "speak" && action.utterance) {
664
+ this._logger.info("(Roleplay Session) [caller]: %s", action.utterance);
665
+ session.say(action.utterance);
666
+ }
667
+ }
668
+ }
669
+ catch (err) {
670
+ if (err.message === "Test session WebSocket closed") {
671
+ this._logger.info("Roleplay session ended by server.");
427
672
  }
428
673
  else {
429
- this._logger.info(`Started session with ID: ${session_started.session_id}`);
430
- socketInitialized = true;
674
+ throw err;
431
675
  }
432
676
  }
433
- else {
434
- // handle the received event
435
- const event = (0, events_ts_1.decodeEvent)(ev.data);
436
- if (event) {
437
- this._dispatchEvent(call, event);
677
+ for (const event of session._events.slice(snapshotLen)) {
678
+ if (event.message_type === "bot-tts") {
679
+ this._logger.info("(Roleplay Session) [agent]: %s", event.transcript);
438
680
  }
439
681
  }
682
+ }, variables);
683
+ }
684
+ /**
685
+ * Start an interactive terminal chat session with the agent.
686
+ *
687
+ * Opens a TUI with a scrolling conversation panel and an input line.
688
+ * Agent responses appear in real time. Press Ctrl+C or let the agent
689
+ * end the session to exit.
690
+ *
691
+ * @param variables - Optional initial call variables.
692
+ *
693
+ * @example
694
+ * await agent.chat();
695
+ * // or: await agent.chat({ patient_name: "Benjamin Buttons" });
696
+ */
697
+ async chat(variables = {}) {
698
+ await this.test(async (session) => {
699
+ await (0, chat_ts_1.runChat)(session);
700
+ }, variables);
701
+ }
702
+ /**
703
+ * Return a shallow copy of this agent with independently overridable
704
+ * callbacks.
705
+ *
706
+ * Use in tests to register alternative handlers on the clone without
707
+ * affecting the original agent.
708
+ */
709
+ patch() {
710
+ const cloned = new Agent({
711
+ name: this._name,
712
+ organization: this._organization,
713
+ purpose: this._purpose,
440
714
  });
441
- ws.addEventListener("close", (_ev) => {
442
- // we are closing the socket, so don't trigger any other listeners
443
- ws.removeAllListeners();
444
- });
715
+ cloned._onCallReceived = this._onCallReceived;
716
+ cloned._onCallStart = this._onCallStart;
717
+ cloned._onCallerSpeech = this._onCallerSpeech;
718
+ cloned._onAgentSpeech = this._onAgentSpeech;
719
+ cloned._onQuestion = this._onQuestion;
720
+ cloned._onTaskCompleteGeneric = this._onTaskCompleteGeneric;
721
+ cloned._onTaskCompleteHandlers = { ...this._onTaskCompleteHandlers };
722
+ cloned._searchQueryHandlers = { ...this._searchQueryHandlers };
723
+ cloned._onActionRequested = this._onActionRequested;
724
+ cloned._onActionGeneric = this._onActionGeneric;
725
+ cloned._onActionHandlers = { ...this._onActionHandlers };
726
+ cloned._onSessionEnd = this._onSessionEnd;
727
+ cloned._onDtmf = this._onDtmf;
728
+ return cloned;
445
729
  }
446
730
  /* ===== Aliases to be removed at some point. ===== */
447
731
  /** @deprecated Use {@link listenPhone} instead. */
448
- inboundPhone(phoneNumber) {
732
+ async inboundPhone(phoneNumber) {
449
733
  return this.listenPhone(phoneNumber);
450
734
  }
451
735
  /** @deprecated Use {@link callPhone} instead. */
@@ -456,13 +740,4 @@ let Agent = (() => {
456
740
  return Agent = _classThis;
457
741
  })();
458
742
  exports.Agent = Agent;
459
- class InboundListener {
460
- ws;
461
- constructor(ws) {
462
- this.ws = ws;
463
- }
464
- close() {
465
- this.ws.close();
466
- }
467
- }
468
743
  //# sourceMappingURL=agent.js.map