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