@unboundcx/sdk 4.13.97 → 4.13.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboundcx/sdk",
3
- "version": "4.13.97",
3
+ "version": "4.13.99",
4
4
  "description": "Official JavaScript SDK for the Unbound API - A comprehensive toolkit for integrating with Unbound's communication, AI, and data management services",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,155 @@
1
+ import { internalRequest } from '../../base.js';
2
+
3
+ /**
4
+ * Text-to-speech. `sdk.ai.tts.*`.
5
+ *
6
+ * @see app1-api src/services/ai/routes/tts.js
7
+ */
8
+ export class TextToSpeechService {
9
+ constructor(sdk) {
10
+ this.sdk = sdk;
11
+ }
12
+
13
+ /**
14
+ * Render text to speech and get back a storage id/url (buffered — waits
15
+ * for the whole file). For low-latency playback (e.g. a voice bot) use
16
+ * `stream()` instead.
17
+ * @param {Object} params
18
+ * @param {string} params.text
19
+ * @param {string} [params.voice]
20
+ * @param {string} [params.languageCode]
21
+ * @param {string} [params.ssmlGender]
22
+ * @param {string} [params.audioEncoding]
23
+ * @param {number} [params.speakingRate]
24
+ * @param {number} [params.pitch]
25
+ * @param {number} [params.volumeGainDb]
26
+ * @param {string[]} [params.effectsProfileIds]
27
+ * @param {boolean} [params.createAccessKey]
28
+ * @returns {Promise<Object>} `{ id, storageId, url? }`
29
+ */
30
+ async create({
31
+ text,
32
+ voice,
33
+ languageCode,
34
+ ssmlGender,
35
+ audioEncoding,
36
+ speakingRate,
37
+ pitch,
38
+ volumeGainDb,
39
+ effectsProfileIds,
40
+ createAccessKey,
41
+ }) {
42
+ this.sdk.validateParams(
43
+ {
44
+ text,
45
+ voice,
46
+ languageCode,
47
+ ssmlGender,
48
+ audioEncoding,
49
+ speakingRate,
50
+ pitch,
51
+ volumeGainDb,
52
+ effectsProfileIds,
53
+ createAccessKey,
54
+ },
55
+ {
56
+ text: { type: 'string', required: true },
57
+ voice: { type: 'string', required: false },
58
+ languageCode: { type: 'string', required: false },
59
+ ssmlGender: { type: 'string', required: false },
60
+ audioEncoding: { type: 'string', required: false },
61
+ speakingRate: { type: 'number', required: false },
62
+ pitch: { type: 'number', required: false },
63
+ volumeGainDb: { type: 'number', required: false },
64
+ effectsProfileIds: { type: 'array', required: false },
65
+ createAccessKey: { type: 'boolean', required: false },
66
+ },
67
+ );
68
+
69
+ const ttsData = { text };
70
+ if (voice) ttsData.voice = voice;
71
+ if (languageCode) ttsData.languageCode = languageCode;
72
+ if (ssmlGender) ttsData.ssmlGender = ssmlGender;
73
+ if (audioEncoding) ttsData.audioEncoding = audioEncoding;
74
+ if (speakingRate) ttsData.speakingRate = speakingRate;
75
+ if (pitch) ttsData.pitch = pitch;
76
+ if (volumeGainDb) ttsData.volumeGainDb = volumeGainDb;
77
+ if (effectsProfileIds) ttsData.effectsProfileIds = effectsProfileIds;
78
+ if (createAccessKey) ttsData.createAccessKey = createAccessKey;
79
+
80
+ const params = {
81
+ body: ttsData,
82
+ };
83
+
84
+ const result = await internalRequest(this.sdk, '/ai/tts', 'POST', params);
85
+ return result;
86
+ }
87
+
88
+ /**
89
+ * List available TTS voices
90
+ * @returns {Promise<Object>} { voices: Array, count: number, supportedEncodings: Array, supportedLanguages: Array }
91
+ */
92
+ async list() {
93
+ const result = await internalRequest(this.sdk, '/ai/tts', 'GET');
94
+ return result;
95
+ }
96
+
97
+ /**
98
+ * Stream TTS audio as it's generated — first bytes can arrive well before
99
+ * the whole utterance finishes rendering (Groq voices: TTFB ~0.2s).
100
+ * Always renders wav; shares its cache with `create()` when
101
+ * audioEncoding:'wav' is used there.
102
+ *
103
+ * Node: wrap the result in `Readable.fromWeb(result.body)` to get a
104
+ * normal Node stream (e.g. to pipe into ffmpeg). Browser: `result.body`
105
+ * is already a web ReadableStream you can read directly or hand to
106
+ * `new Response(result.body)` / a `<audio>` element via a Blob.
107
+ *
108
+ * Response headers of note: `x-tts-cache` (`hit`|`miss`) and `x-tts-id`
109
+ * (set on a cache hit, when the id is already known).
110
+ *
111
+ * @param {Object} params
112
+ * @param {string} params.text
113
+ * @param {string} [params.voice]
114
+ * @param {string} [params.languageCode]
115
+ * @returns {Promise<{body: ReadableStream, headers: Headers, status: number}>}
116
+ * @example
117
+ * const result = await sdk.ai.tts.stream({ text: 'Hi there', voice: 'hannah' });
118
+ * const nodeStream = Readable.fromWeb(result.body);
119
+ * nodeStream.pipe(ffmpegProcess.stdin);
120
+ */
121
+ async stream({ text, voice, languageCode }) {
122
+ this.sdk.validateParams(
123
+ { text, voice, languageCode },
124
+ {
125
+ text: { type: 'string', required: true },
126
+ voice: { type: 'string', required: false },
127
+ languageCode: { type: 'string', required: false },
128
+ },
129
+ );
130
+
131
+ const ttsData = { text };
132
+ if (voice) ttsData.voice = voice;
133
+ if (languageCode) ttsData.languageCode = languageCode;
134
+
135
+ const params = {
136
+ body: ttsData,
137
+ returnRawResponse: true,
138
+ };
139
+
140
+ // forceFetch: true — NATS transport can't carry a streamed body.
141
+ const response = await internalRequest(
142
+ this.sdk,
143
+ '/ai/tts/stream',
144
+ 'POST',
145
+ params,
146
+ true,
147
+ );
148
+
149
+ return {
150
+ body: response.body,
151
+ headers: response.headers,
152
+ status: response.status,
153
+ };
154
+ }
155
+ }
package/services/ai.js CHANGED
@@ -5,6 +5,7 @@ import { AssistService } from './ai/assist.js';
5
5
  import { VocabularyService } from './ai/vocabulary.js';
6
6
  import { EmailService } from './ai/email.js';
7
7
  import { ModelsService } from './ai/models.js';
8
+ import { TextToSpeechService } from './ai/tts.js';
8
9
  import { translate as translateItems } from './ai/translate.js';
9
10
  import {
10
11
  getSettings as getAiSettings,
@@ -487,79 +488,6 @@ export class GenerativeService {
487
488
  // }
488
489
  }
489
490
 
490
- export class TextToSpeechService {
491
- constructor(sdk) {
492
- this.sdk = sdk;
493
- }
494
-
495
- async create({
496
- text,
497
- voice,
498
- languageCode,
499
- ssmlGender,
500
- audioEncoding,
501
- speakingRate,
502
- pitch,
503
- volumeGainDb,
504
- effectsProfileIds,
505
- createAccessKey,
506
- }) {
507
- this.sdk.validateParams(
508
- {
509
- text,
510
- voice,
511
- languageCode,
512
- ssmlGender,
513
- audioEncoding,
514
- speakingRate,
515
- pitch,
516
- volumeGainDb,
517
- effectsProfileIds,
518
- createAccessKey,
519
- },
520
- {
521
- text: { type: 'string', required: true },
522
- voice: { type: 'string', required: false },
523
- languageCode: { type: 'string', required: false },
524
- ssmlGender: { type: 'string', required: false },
525
- audioEncoding: { type: 'string', required: false },
526
- speakingRate: { type: 'number', required: false },
527
- pitch: { type: 'number', required: false },
528
- volumeGainDb: { type: 'number', required: false },
529
- effectsProfileIds: { type: 'array', required: false },
530
- createAccessKey: { type: 'boolean', required: false },
531
- },
532
- );
533
-
534
- const ttsData = { text };
535
- if (voice) ttsData.voice = voice;
536
- if (languageCode) ttsData.languageCode = languageCode;
537
- if (ssmlGender) ttsData.ssmlGender = ssmlGender;
538
- if (audioEncoding) ttsData.audioEncoding = audioEncoding;
539
- if (speakingRate) ttsData.speakingRate = speakingRate;
540
- if (pitch) ttsData.pitch = pitch;
541
- if (volumeGainDb) ttsData.volumeGainDb = volumeGainDb;
542
- if (effectsProfileIds) ttsData.effectsProfileIds = effectsProfileIds;
543
- if (createAccessKey) ttsData.createAccessKey = createAccessKey;
544
-
545
- const params = {
546
- body: ttsData,
547
- };
548
-
549
- const result = await internalRequest(this.sdk, '/ai/tts', 'POST', params);
550
- return result;
551
- }
552
-
553
- /**
554
- * List available TTS voices
555
- * @returns {Promise<Object>} { voices: Array, count: number, supportedEncodings: Array, supportedLanguages: Array }
556
- */
557
- async list() {
558
- const result = await internalRequest(this.sdk, '/ai/tts', 'GET');
559
- return result;
560
- }
561
- }
562
-
563
491
  export class SpeechToTextService {
564
492
  constructor(sdk) {
565
493
  this.sdk = sdk;
@@ -439,6 +439,80 @@ export class TaskService {
439
439
  );
440
440
  }
441
441
 
442
+ /**
443
+ * Release a task back to the queue for a human (same queue, same task
444
+ * id) — the bot-task-lifecycle release contract. Stamps
445
+ * botEligible:false (when humanOnly) plus named release-reason
446
+ * metadata, and for a voice task in 'callback' mode hands the customer
447
+ * off to the existing queue-wait callback contract (hangs up, task goes
448
+ * pending, human accept later auto-dials the customer back).
449
+ *
450
+ * @param {Object} options - Parameters
451
+ * @param {string} options.taskId - The task ID to release (required)
452
+ * @param {'live'|'callback'} [options.mode] - Required for a voice task (has a live call); ignored for a digital task
453
+ * @param {string} options.reasonCode - Release reason code (required) — e.g. 'callback_promised', 'human_requested', 'no_human_available', 'bot_cannot_resolve', 'customer_frustrated', 'review_failed', 'policy_human_only', 'other'
454
+ * @param {string} options.reason - One-sentence reason shown to the next agent (required)
455
+ * @param {string} [options.callbackNumber] - E.164 callback number ('callback' mode only; defaults to the task's `from`)
456
+ * @param {boolean} [options.humanOnly=true] - Stamp botEligible:false so only a human is offered this task
457
+ * @param {boolean} [options.hangup=true] - Hang up the customer leg ('callback' mode only)
458
+ * @returns {Promise<Object>} { taskId, status: 'pending', mode, humanOnly, reasonCode }
459
+ *
460
+ * @example
461
+ * // Caller confirmed a callback -- release and hang up
462
+ * await sdk.taskRouter.task.release({
463
+ * taskId: 'task123',
464
+ * mode: 'callback',
465
+ * reasonCode: 'callback_promised',
466
+ * reason: 'Caller asked for a callback once an agent frees up',
467
+ * });
468
+ *
469
+ * @example
470
+ * // Caller wants to hold for a human -- release, keep the call live
471
+ * await sdk.taskRouter.task.release({
472
+ * taskId: 'task123',
473
+ * mode: 'live',
474
+ * reasonCode: 'no_human_available',
475
+ * reason: 'No agents available, caller chose to hold',
476
+ * });
477
+ */
478
+ async release(options = {}) {
479
+ const {
480
+ taskId,
481
+ mode,
482
+ reasonCode,
483
+ reason,
484
+ callbackNumber,
485
+ humanOnly,
486
+ hangup,
487
+ } = options;
488
+
489
+ this.sdk.validateParams(
490
+ { taskId, mode, reasonCode, reason, callbackNumber, humanOnly, hangup },
491
+ {
492
+ taskId: { type: 'string', required: true },
493
+ mode: { type: 'string', required: false },
494
+ reasonCode: { type: 'string', required: true },
495
+ reason: { type: 'string', required: true },
496
+ callbackNumber: { type: 'string', required: false },
497
+ humanOnly: { type: 'boolean', required: false },
498
+ hangup: { type: 'boolean', required: false },
499
+ },
500
+ );
501
+
502
+ const params = { body: { taskId, reasonCode, reason } };
503
+ if (mode !== undefined) params.body.mode = mode;
504
+ if (callbackNumber !== undefined) params.body.callbackNumber = callbackNumber;
505
+ if (humanOnly !== undefined) params.body.humanOnly = humanOnly;
506
+ if (hangup !== undefined) params.body.hangup = hangup;
507
+
508
+ return await internalRequest(
509
+ this.sdk,
510
+ '/taskRouter/tasks/release',
511
+ 'PUT',
512
+ params,
513
+ );
514
+ }
515
+
442
516
  /**
443
517
  * Staff-only internal note on a task (webchat/SMS/voice feed, or
444
518
  * timeline). Never sent to the customer.
@@ -871,6 +945,8 @@ export class TaskService {
871
945
  * @param {string} [options.subject] - The new subject/title for the task
872
946
  * @param {string} [options.summary] - The overall summary for the task
873
947
  * @param {string} [options.disposition] - The disposition code or outcome for the task (e.g., 'resolved', 'escalated', 'callback-scheduled')
948
+ * @param {boolean} [options.botEligible] - Routing flag. `false` = never offer this task to bot workers (human only); `true` re-allows bots
949
+ * @param {?string} [options.humanFollowUp] - What a human still owes this customer: 'callback' | 'message' | 'dispatch' | 'quote' | 'other', or null to clear. Read by the caller-hangup safety net so an abandoned call with this set releases to the queue instead of completing.
874
950
  * @returns {Promise<Object>} Object containing the task ID
875
951
  * @returns {string} result.taskId - The task ID that was updated
876
952
  *
@@ -898,6 +974,10 @@ export class TaskService {
898
974
  * disposition: 'escalated'
899
975
  * });
900
976
  * console.log(result.taskId); // "task789"
977
+ *
978
+ * @example
979
+ * // Human-only routing: bot workers are no longer offered this task
980
+ * await sdk.taskRouter.task.update({ taskId: 'task789', botEligible: false });
901
981
  */
902
982
  async update(options = {}) {
903
983
  const {
@@ -908,10 +988,22 @@ export class TaskService {
908
988
  cdrId,
909
989
  summary,
910
990
  sentiment,
991
+ botEligible,
992
+ humanFollowUp,
911
993
  } = options;
912
994
 
913
995
  this.sdk.validateParams(
914
- { taskId, subject, disposition, sipCallId, cdrId, summary, sentiment },
996
+ {
997
+ taskId,
998
+ subject,
999
+ disposition,
1000
+ sipCallId,
1001
+ cdrId,
1002
+ summary,
1003
+ sentiment,
1004
+ botEligible,
1005
+ humanFollowUp,
1006
+ },
915
1007
  {
916
1008
  taskId: { type: 'string', required: true },
917
1009
  subject: { type: 'string', required: false },
@@ -920,6 +1012,10 @@ export class TaskService {
920
1012
  sipCallId: { type: 'string', required: false },
921
1013
  summary: { type: 'string', required: false },
922
1014
  sentiment: { type: 'object', required: false },
1015
+ botEligible: { type: 'boolean', required: false },
1016
+ // validateParams already skips type-checking a null value (see
1017
+ // base.js) -- 'string' here only constrains the non-null case.
1018
+ humanFollowUp: { type: 'string', required: false },
923
1019
  },
924
1020
  );
925
1021
 
@@ -929,6 +1025,14 @@ export class TaskService {
929
1025
  },
930
1026
  };
931
1027
 
1028
+ if (botEligible !== undefined) {
1029
+ params.body.botEligible = botEligible;
1030
+ }
1031
+
1032
+ if (humanFollowUp !== undefined) {
1033
+ params.body.humanFollowUp = humanFollowUp;
1034
+ }
1035
+
932
1036
  if (subject !== undefined) {
933
1037
  params.body.subject = subject;
934
1038
  }
@@ -1141,22 +1245,37 @@ export class TaskService {
1141
1245
  * @param {string} [options.target.queueId] - Destination queue ID
1142
1246
  * @param {string} [options.target.workerId] - Destination worker ID
1143
1247
  * @param {string} [options.note] - Optional note for the receiving agent
1248
+ * @param {string} [options.reasonCode] - Transfer reason code — e.g. 'wrong_department', 'customer_requested', 'out_of_scope', 'policy_never_bot', 'language', 'other'. Required when the caller's worker is a bot.
1249
+ * @param {string} [options.reason] - One-sentence transfer reason. Required when the caller's worker is a bot.
1144
1250
  * @returns {Promise<Object>} { taskId, newTaskId }
1251
+ *
1252
+ * @example
1253
+ * // Bot transferring to a configured queue target
1254
+ * await sdk.taskRouter.task.transfer({
1255
+ * taskId: 'task123',
1256
+ * target: { queueId: 'billingQueue1' },
1257
+ * reasonCode: 'wrong_department',
1258
+ * reason: 'Caller has a billing question',
1259
+ * });
1145
1260
  */
1146
1261
  async transfer(options = {}) {
1147
- const { taskId, target, note } = options;
1262
+ const { taskId, target, note, reasonCode, reason } = options;
1148
1263
 
1149
1264
  this.sdk.validateParams(
1150
- { taskId, target, note },
1265
+ { taskId, target, note, reasonCode, reason },
1151
1266
  {
1152
1267
  taskId: { type: 'string', required: true },
1153
1268
  target: { type: 'object', required: true },
1154
1269
  note: { type: 'string', required: false },
1270
+ reasonCode: { type: 'string', required: false },
1271
+ reason: { type: 'string', required: false },
1155
1272
  },
1156
1273
  );
1157
1274
 
1158
1275
  const params = { body: { taskId, target } };
1159
1276
  if (note !== undefined) params.body.note = note;
1277
+ if (reasonCode !== undefined) params.body.reasonCode = reasonCode;
1278
+ if (reason !== undefined) params.body.reason = reason;
1160
1279
 
1161
1280
  return await internalRequest(
1162
1281
  this.sdk,