@scout9/app 1.0.0-alpha.0.1.9 → 1.0.0-alpha.0.1.90

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 (70) hide show
  1. package/README.md +32 -0
  2. package/dist/{index-92deaa5f.cjs → exports-e7d51b70.cjs} +46618 -4591
  3. package/dist/index.cjs +58 -15
  4. package/dist/{multipart-parser-090f08a9.cjs → multipart-parser-e09a67c9.cjs} +13 -7
  5. package/dist/spirits-3b603262.cjs +1218 -0
  6. package/dist/spirits.cjs +9 -0
  7. package/dist/testing-tools.cjs +48 -0
  8. package/package.json +30 -8
  9. package/src/cli.js +162 -69
  10. package/src/core/config/agents.js +300 -7
  11. package/src/core/config/entities.js +58 -28
  12. package/src/core/config/index.js +37 -15
  13. package/src/core/config/project.js +160 -6
  14. package/src/core/config/workflow.js +13 -12
  15. package/src/core/data.js +27 -0
  16. package/src/core/index.js +386 -137
  17. package/src/core/sync.js +71 -0
  18. package/src/core/templates/Dockerfile +22 -0
  19. package/src/core/templates/app.js +453 -0
  20. package/src/core/templates/project-files.js +36 -0
  21. package/src/core/templates/template-package.json +13 -0
  22. package/src/exports.js +21 -17
  23. package/src/platform.js +189 -33
  24. package/src/public.d.ts.text +330 -0
  25. package/src/report.js +117 -0
  26. package/src/runtime/client/api.js +56 -159
  27. package/src/runtime/client/config.js +60 -11
  28. package/src/runtime/client/entity.js +19 -6
  29. package/src/runtime/client/index.js +5 -3
  30. package/src/runtime/client/message.js +13 -3
  31. package/src/runtime/client/platform.js +86 -0
  32. package/src/runtime/client/{agent.js → users.js} +35 -3
  33. package/src/runtime/client/utils.js +10 -9
  34. package/src/runtime/client/workflow.js +131 -9
  35. package/src/runtime/entry.js +2 -2
  36. package/src/testing-tools/dev.js +373 -0
  37. package/src/testing-tools/index.js +1 -0
  38. package/src/testing-tools/mocks.js +37 -5
  39. package/src/testing-tools/spirits.js +530 -0
  40. package/src/utils/audio-buffer.js +16 -0
  41. package/src/utils/audio-type.js +27 -0
  42. package/src/utils/configs/agents.js +68 -0
  43. package/src/utils/configs/entities.js +145 -0
  44. package/src/utils/configs/project.js +23 -0
  45. package/src/utils/configs/workflow.js +47 -0
  46. package/src/utils/file-type.js +569 -0
  47. package/src/utils/file.js +158 -0
  48. package/src/utils/glob.js +30 -0
  49. package/src/utils/image-buffer.js +23 -0
  50. package/src/utils/image-type.js +39 -0
  51. package/src/utils/index.js +1 -0
  52. package/src/utils/is-svg.js +37 -0
  53. package/src/utils/logger.js +111 -0
  54. package/src/utils/module.js +14 -25
  55. package/src/utils/project-templates.js +191 -0
  56. package/src/utils/project.js +387 -0
  57. package/src/utils/video-type.js +29 -0
  58. package/types/index.d.ts +7588 -206
  59. package/types/index.d.ts.map +97 -22
  60. package/dist/index-1b8d7dd2.cjs +0 -49555
  61. package/dist/index-2ccb115e.cjs +0 -49514
  62. package/dist/index-66b06a30.cjs +0 -49549
  63. package/dist/index-bc029a1d.cjs +0 -49528
  64. package/dist/index-d9a93523.cjs +0 -49527
  65. package/dist/multipart-parser-1508046a.cjs +0 -413
  66. package/dist/multipart-parser-7007403a.cjs +0 -413
  67. package/dist/multipart-parser-70c32c1d.cjs +0 -413
  68. package/dist/multipart-parser-71dec101.cjs +0 -413
  69. package/dist/multipart-parser-f15bf2e0.cjs +0 -414
  70. package/src/public.d.ts +0 -209
@@ -1,27 +1,49 @@
1
1
  import moment from 'moment';
2
2
 
3
+ /**
4
+ * @returns {import('../runtime/client/users.js').IAgent}
5
+ */
3
6
  export const createMockAgent = (firstName = 'Carmela', lastName = 'Soprano') => {
4
7
  return {
8
+ id: Math.random().toString(36).substring(7),
5
9
  firstName,
6
10
  lastName
7
11
  }
8
12
  }
9
13
 
14
+ /**
15
+ * @returns {import('../runtime/client/users.js').ICustomer}
16
+ */
10
17
  export const createMockCustomer = (firstName = 'Tony', lastName = 'Soprano') => {
11
18
  return {
19
+ id: Math.random().toString(36).substring(7),
12
20
  name: `${firstName} ${lastName}`,
13
21
  firstName,
14
22
  lastName
15
23
  }
16
24
  }
17
25
 
26
+ /**
27
+ *
28
+ * @param content
29
+ * @param role
30
+ * @param time
31
+ * @returns {import('../runtime/client/message.js').IMessage}
32
+ */
18
33
  export const createMockMessage = (content, role = 'customer', time = moment().toISOString()) => {
19
34
  return {
35
+ id: Math.random().toString(36).substring(7),
20
36
  role,
21
37
  content,
22
- time
38
+ time,
39
+ intent: null,
40
+ intentScore: null
23
41
  }
24
42
  }
43
+
44
+ /**
45
+ * @returns {import('../runtime/client/workflow.js').IConversation}
46
+ */
25
47
  export const createMockConversation = (environment = 'phone', $agent = 'default', $customer = 'default') => {
26
48
  return {
27
49
  $agent,
@@ -29,19 +51,29 @@ export const createMockConversation = (environment = 'phone', $agent = 'default'
29
51
  environment
30
52
  }
31
53
  }
54
+
55
+ /**
56
+ * @param {string} message
57
+ * @param {string | import('../runtime/client/workflow.js').IWorkflowEvent['intent'] | null} intent
58
+ * @returns {import('../runtime/client/workflow.js').IWorkflowEvent}
59
+ */
32
60
  export const createMockWorkflowEvent = (
33
61
  message,
34
- intent,
62
+ intent = null,
35
63
  ) => {
36
64
  return {
37
65
  messages: [],
38
66
  conversation: createMockConversation(),
39
67
  context: {},
40
68
  message: createMockMessage(message),
41
- stagnationCount: 0,
42
- customer: createMockCustomer(),
43
69
  agent: createMockAgent(),
44
- intent,
70
+ customer: createMockCustomer(),
71
+ intent: typeof intent === 'string' ? {
72
+ current: intent,
73
+ flow: [],
74
+ initial: intent
75
+ } : typeof intent === 'object' ? intent : null,
76
+ stagnationCount: 0,
45
77
  }
46
78
  }
47
79
 
@@ -0,0 +1,530 @@
1
+ /**
2
+ * @typedef {Object} Document
3
+ * @property {string} id
4
+ */
5
+
6
+
7
+ /**
8
+ * Represents a change with before and after states of a given type.
9
+ * @template Type The type of the before and after properties.
10
+ * @typedef {Object} Change
11
+ * @property {Type} before - The state before the change.
12
+ * @property {Type} after - The state after the change.
13
+ */
14
+
15
+ /**
16
+ * @typedef {Object} ConversationData
17
+ * @property {import('../runtime/client/config.js').IScout9ProjectBuildConfig} config - used to define generation and extract persona metadata
18
+ * @property {import('../runtime/client/workflow.js').IConversation} conversation
19
+ * @property {Array<import('../runtime/client/message.js').IMessage>} messages
20
+ * @property {import('../runtime/client/message.js').IMessage} message - the message sent by the customer (should exist in messages)
21
+ * @property {import('../runtime/client/users.js').ICustomer} customer
22
+ * @property {any} context
23
+ */
24
+
25
+ /**
26
+ * @typedef {Object} ParseOutput
27
+ * @property {Array<import('../runtime/client/message.js').IMessage>} messages
28
+ * @property {import('../runtime/client/workflow.js').IConversation} conversation
29
+ * @property {import('../runtime/client/message.js').IMessage} message
30
+ * @property {any} context
31
+ */
32
+
33
+ /**
34
+ * @typedef {Object} WorkflowOutput
35
+ * @property {Array<import('../runtime/client/workflow.js').IWorkflowResponseSlot>} slots
36
+ * @property {Array<import('../runtime/client/message.js').IMessage>} messages
37
+ * @property {import('../runtime/client/workflow.js').IConversation} conversation
38
+ * @property {any} context
39
+ */
40
+
41
+ /**
42
+ * @typedef {Object} GenerateOutput
43
+ * @property {import('@scout9/admin').GenerateResponse | undefined} generate
44
+ * @property {Array<import('../runtime/client/message.js').IMessage>} messages
45
+ * @property {import('../runtime/client/workflow.js').IConversation} conversation
46
+ * @property {any} context
47
+ */
48
+
49
+ /**
50
+ * @callback ParseFun
51
+ * @param {string} message - message to send
52
+ * @param {string | undefined} language - language to parse in, defaults to "en" for english
53
+ * @returns {Promise<import('@scout9/admin').ParseResponse>}
54
+ */
55
+
56
+ /**
57
+ * @callback WorkflowFun
58
+ * @param {import('../runtime/client/workflow.js').IWorkflowEvent} event - conversation data
59
+ * @returns {Promise<import('../runtime/client/workflow.js').IWorkflowResponse>}
60
+ */
61
+
62
+ /**
63
+ * @callback GenerateFun
64
+ * @param {import('@scout9/admin').GenerateRequestOneOf} data - data to generate from
65
+ * @returns {Promise<import('@scout9/admin').GenerateResponse>}
66
+ */
67
+
68
+ /**
69
+ * @callback IdGeneratorFun
70
+ * @param {import('../runtime/client/message.js').IMessage.role} prefix
71
+ * @returns {string}
72
+ */
73
+ /**
74
+ * @callback StatusCallback
75
+ * @param {string} message
76
+ * @param {'info' | 'warn' | 'error' | 'success' | undefined} [level]
77
+ * @param {string | undefined} [type]
78
+ * @param {any | undefined} [payload]
79
+ * @returns {void}
80
+ */
81
+
82
+ /**
83
+ * @typedef {Object} CustomerSpiritCallbacks
84
+ * @property {ParseFun} parser
85
+ * @property {WorkflowFun} workflow
86
+ * @property {GenerateFun} generator
87
+ * @property {IdGeneratorFun} idGenerator
88
+ * @property {StatusCallback | undefined} [progress]
89
+ */
90
+
91
+ /**
92
+ * @typedef {Object} ConversationEvent
93
+ * @property {Change<import('../runtime/client/workflow.js').IConversation> & {forwardNote?: string; forward?: import('../runtime/client/message.js').IWorkflowResponseSlot['forward']}} conversation
94
+ * @property {Change<Array<import('../runtime/client/message.js').IMessage>>} messages
95
+ * @property {Change<Object>} context
96
+ * @property {Change<import('../runtime/client/message.js').IMessage>} message
97
+ */
98
+ export const Spirits = {
99
+
100
+ /**
101
+ * Customer message
102
+ * @param {ConversationData & CustomerSpiritCallbacks} input
103
+ * @returns {Promise<ConversationEvent>}
104
+ */
105
+ customer: async function (input) {
106
+ const {
107
+ customer,
108
+ config,
109
+ parser,
110
+ workflow,
111
+ generator,
112
+ idGenerator,
113
+ progress = (message, level, type, payload) => {
114
+ },
115
+ message: messageBefore,
116
+ context: contextBefore,
117
+ messages: messagesBefore,
118
+ conversation: conversationBefore
119
+ } = input;
120
+ let {conversation, messages, context, message} = input;
121
+
122
+ // 0. Setup Helpers
123
+ const updateConversation = (previousConversation, conversationUpdates) => {
124
+ progress('Update conversation', 'info', 'UPDATE_CONVERSATION', conversationUpdates);
125
+ return {
126
+ ...previousConversation,
127
+ ...conversationUpdates
128
+ };
129
+ }
130
+
131
+ const updateContext = (previousContext, newContext) => {
132
+ progress('Update context', 'info', 'UPDATE_CONTEXT', newContext);
133
+ return {
134
+ ...previousContext,
135
+ ...newContext
136
+ };
137
+ }
138
+
139
+ const userMessages = (_messages) => {
140
+ return _messages.filter(m => m.role === 'customer' || m.role === 'user')
141
+ }
142
+
143
+ const recentUserMessage = (_messages) => {
144
+ const _userMessages = userMessages(_messages);
145
+ return _userMessages[_userMessages.length - 1];
146
+ }
147
+
148
+ const lockConversation = (_conversation, reason) => {
149
+ return updateConversation(_conversation, {locked: true, lockedReason: conversation.lockedReason || reason || 'Unknown'});
150
+ }
151
+
152
+ const incrementLockAttempt = (_conversation, _config) => {
153
+ if (typeof _conversation.lockAttempts !== 'number') {
154
+ _conversation.lockAttempts = 0;
155
+ }
156
+ _conversation.lockAttempts++;
157
+ if (_conversation.lockAttempts > (_config?.maxLockAttempts || 3)) {
158
+ _conversation.locked = true;
159
+ _conversation.lockedReason = `Max lock attempts exceeded (${_conversation.lockAttempts} > ${(_config?.maxLockAttempts || 3)})`;
160
+ }
161
+ progress('Incremented lock attempt', 'info', 'UPDATE_CONVERSATION', {lockAttempts: _conversation.lockAttempts, locked: _conversation.locked, lockedReason: _conversation.lockedReason || ''});
162
+ return _conversation;
163
+ }
164
+
165
+ const _addInstruction = (
166
+ instruction,
167
+ _messages,
168
+ _conversation,
169
+ _config,
170
+ previousLockAttempt,
171
+ id
172
+ ) => {
173
+ const systemMessages = _messages.filter(m => m.role === 'system');
174
+ const lastSystemMessage = systemMessages[systemMessages.length - 1];
175
+ let addedMessage = false;
176
+ let changedConversation = false;
177
+
178
+ // If instruction does not equal previous system message, add it, otherwise lock attempt
179
+ if (!lastSystemMessage || instruction !== lastSystemMessage.content) {
180
+ _messages.push({
181
+ id,
182
+ role: 'system',
183
+ content: instruction,
184
+ time: new Date().toISOString()
185
+ });
186
+ addedMessage = true;
187
+ } else {
188
+ // Handle repeated instruction
189
+ // Increment lock attempt if instructions are repeated and we haven't already incremented lock attempt (for example if a forward is provided)
190
+ if (previousLockAttempt === (conversation.lockAttempts || 0)) {
191
+ _conversation = incrementLockAttempt(_conversation, _config);
192
+ changedConversation = true;
193
+ }
194
+ }
195
+ return {
196
+ conversation: _conversation,
197
+ messages: _messages,
198
+ addedMessage,
199
+ changedConversation
200
+ }
201
+ }
202
+
203
+ const addInstruction = (instruction, previousLockAttempt, id = idGenerator('sys')) => {
204
+ const {
205
+ conversation: newConversation,
206
+ messages: newMessages,
207
+ addedMessage,
208
+ changedConversation
209
+ } = _addInstruction(instruction, messages, conversation, config, previousLockAttempt, id);
210
+ conversation = newConversation;
211
+ messages = newMessages;
212
+ if (addedMessage) {
213
+ progress('Added instruction', 'info', 'ADD_MESSAGE', newMessages[newMessages.length - 1]);
214
+ }
215
+ if (changedConversation) {
216
+ progress('Updated conversation', 'info', 'UPDATE_CONVERSATION', newConversation);
217
+ }
218
+ }
219
+
220
+ // 1. Check inputs
221
+ if (!conversation.$agent) {
222
+ throw new Error(`No agent found in conversation, must define ".$agent" in the conversation`);
223
+ }
224
+ const persona = (config.persona || config.agents).find(p => p.id === conversation.$agent);
225
+ if (!persona) {
226
+ throw new Error(`No persona found ("${conversation.$agent}") in provided config`);
227
+ }
228
+ if (!messages.every(m => !!m.id)) {
229
+ throw new Error(`Every message must have an ".id", ensure all messages have an id assigned before running`);
230
+ }
231
+ if (!messages.every(m => m.role === 'customer' || m.role === 'agent' || m.role === 'system')) {
232
+ const invalidRoles = messages.filter(m => m.role !== 'customer' && m.role !== 'agent' && m.role !== 'system');
233
+ throw new Error(`Every message must have a role of "customer", "agent", or "system". Got invalid roles: ${invalidRoles.map(m => m.role).join(', ')}`);
234
+ }
235
+ // if message is not in messages, then add it
236
+ if (!messages.find(m => m.id === input.message.id)) {
237
+ messages.push(input.message);
238
+ }
239
+
240
+ // 2. Parse the message
241
+ progress('Parsing message', 'info', 'SET_PROCESSING', 'user');
242
+ const parsePayload = await parser(message.content, 'en');
243
+ if (parsePayload.intent) {
244
+ message.intent = parsePayload.intent;
245
+ }
246
+ if (typeof parsePayload.intentScore === 'number') {
247
+ message.intentScore = parsePayload.intentScore;
248
+ }
249
+ message.context = parsePayload.context;
250
+ const index = messages.findIndex(m => m.content === message.content || m.id === message.id);
251
+ if (index === -1) {
252
+ const _message = {
253
+ id: idGenerator('customer'),
254
+ role: 'customer',
255
+ content: message,
256
+ context: parsePayload.context,
257
+ time: new Date().toISOString(),
258
+ };
259
+ if (parsePayload.intent) {
260
+ _message.intent = parsePayload.intent;
261
+ }
262
+ if (typeof parsePayload.intentScore === 'number') {
263
+ _message.intentScore = parsePayload.intentScore;
264
+ }
265
+ message = _message;
266
+ messages.push(_message);
267
+ progress('Added message', 'info', 'ADD_MESSAGE', _message);
268
+ } else {
269
+ messages[index].context = parsePayload.context;
270
+ if (parsePayload.intent) {
271
+ messages[index].intent = parsePayload.intent;
272
+ }
273
+ if (typeof parsePayload.intentScore === 'number') {
274
+ messages[index].intentScore = parsePayload.intentScore;
275
+ }
276
+ message = messages[index];
277
+ progress('Parsed message', 'success', 'UPDATE_MESSAGE', message);
278
+ }
279
+ // If this is the first user message, then update conversations intent
280
+ const previousUserMessages = messages.filter(m => m.role === 'customer' && m.content !== message.content);
281
+ if (!conversation.intent || previousUserMessages.length === 0 && parsePayload.intent) {
282
+ conversation.intent = parsePayload.intent;
283
+ conversation.intentScore = parsePayload?.intentScore || 0;
284
+ progress('Updated conversation intent', 'info', 'UPDATE_CONVERSATION', {intent: parsePayload.intent, intentScore: parsePayload?.intentScore || 0});
285
+ }
286
+ const oldKeyCount = Object.keys(context).length;
287
+ context = updateContext(context, parsePayload.context);
288
+ const newKeyCount = Object.keys(context).length;
289
+
290
+ if (!conversation.locked && (newKeyCount > oldKeyCount)) {
291
+ // Reset lock attempts
292
+ conversation.locked = false;
293
+ conversation.lockAttempts = 0;
294
+ conversation.lockedReason = '';
295
+ progress('Reset lock', 'info', 'UPDATE_CONVERSATION', {locked: false, lockAttempts: 0, lockedReason: ''});
296
+ }
297
+
298
+ const noNewContext = Object.keys(parsePayload.context).length === 0;
299
+
300
+ // 3. Run the workflow
301
+ progress('Running workflow', 'info', 'SET_PROCESSING', 'system');
302
+ const slots = await workflow({
303
+ messages,
304
+ conversation,
305
+ context,
306
+ message,
307
+ agent: persona,
308
+ customer,
309
+ intent: {
310
+ current: recentUserMessage(messages)?.intent || null,
311
+ flow: messages.map(m => m.intent).filter(Boolean),
312
+ initial: conversation.intent || null
313
+ },
314
+ stagnationCount: conversation.lockAttempts || 0
315
+ }).then((res) => Array.isArray(res) ? res : [res]);
316
+ const hasNoInstructions = slots.every(s => !s.instructions || (Array.isArray(s) && s.instructions.length === 0));
317
+ const hasNoCustomMessage = slots.every(s => !s.message);
318
+ const previousLockAttempt = conversation.lockAttempts || 0; // Used to track
319
+
320
+ if (hasNoInstructions && noNewContext) {
321
+ conversation = incrementLockAttempt(conversation, config);
322
+ } else {
323
+ conversation.lockAttempts = 0;
324
+ conversation.locked = false;
325
+ conversation.lockedReason = '';
326
+ progress('Reset lock', 'info', 'UPDATE_CONVERSATION', {lockAttempts: 0, locked: false, lockedReason: ''});
327
+ }
328
+
329
+ let resettedIntent = false;
330
+ let _forward;
331
+ let _forwardNote;
332
+
333
+ for (const {
334
+ forward,
335
+ forwardNote,
336
+ instructions,
337
+ removeInstructions,
338
+ message: manualMessage,
339
+ scheduled,
340
+ resetIntent,
341
+ secondsDelay,
342
+ contextUpsert
343
+ } of slots) {
344
+
345
+ // Forward to agent or other agent
346
+ if (forward) {
347
+ conversation = lockConversation(conversation, 'App instructed forward');
348
+ _forward = forward;
349
+ _forwardNote = forwardNote;
350
+ if (typeof forward === 'string') {
351
+ updateConversation(conversation, {forwarded: forward});
352
+ messages.push({
353
+ id: idGenerator('sys'),
354
+ role: 'system',
355
+ content: `forwarded to "${forward}"`,
356
+ time: new Date().toISOString()
357
+ });
358
+ progress(`Forwarded to "${forward}"`, 'info', 'ADD_MESSAGE', messages[messages.length - 1]);
359
+ } else if (typeof forward === 'boolean') {
360
+ updateConversation(conversation, {forwarded: conversation.$agent});
361
+ messages.push({
362
+ id: idGenerator('sys'),
363
+ role: 'system',
364
+ content: `forwarded to "${forward}"`,
365
+ time: new Date().toISOString()
366
+ });
367
+ progress(`Forwarded to agent`, 'info', 'ADD_MESSAGE', messages[messages.length - 1]);
368
+
369
+ } else {
370
+ messages.push({
371
+ id: idGenerator('sys'),
372
+ role: 'system',
373
+ content: `forwarded to "${forward.to}" ${forward.mode ? ' (' + forward.mode + ')' : ''}`,
374
+ time: new Date().toISOString()
375
+ });
376
+ progress(`Forwarded to "${forward.to}" ${forward.mode ? ' (' + forward.mode + ')' : ''}`, 'info', 'ADD_MESSAGE', messages[messages.length - 1]);
377
+ updateConversation(conversation, {forwarded: forward.to});
378
+ }
379
+ }
380
+
381
+ // Insert instructions context
382
+ if (instructions) {
383
+ if (typeof instructions === 'string') {
384
+ addInstruction(instructions, previousLockAttempt)
385
+ } else if (Array.isArray(instructions)) {
386
+ for (const instruction of instructions) {
387
+ if (typeof instruction === 'string') {
388
+ addInstruction(instruction, previousLockAttempt)
389
+ } else {
390
+ addInstruction(instruction.content, previousLockAttempt, instruction.id)
391
+ }
392
+ }
393
+ } else if (typeof instructions === 'object' && 'content' in instructions && 'id' in instructions) {
394
+ addInstruction(instructions.content, previousLockAttempt, instructions.id)
395
+ } else {
396
+ throw new Error('instructions must be a string or array or {content: "", id: ""}');
397
+ }
398
+ }
399
+
400
+
401
+ // Remove messages that have the given ids
402
+ if (removeInstructions) {
403
+ for (const instructionId of removeInstructions) {
404
+ const index = messages.findIndex(m => m.id === instructionId);
405
+ if (index > -1) {
406
+ messages.splice(index, 1);
407
+ progress('Remove instruction', 'info', 'REMOVE_MESSAGE', instructionId);
408
+ } else {
409
+ console.log('instruction not found', instructionId);
410
+ }
411
+ }
412
+ }
413
+
414
+ if (manualMessage) {
415
+ let manualMessageObj = {
416
+ id: idGenerator('agent'),
417
+ role: 'agent',
418
+ content: message,
419
+ time: new Date().toISOString()
420
+ };
421
+ if (scheduled) {
422
+ manualMessageObj.time = new Date(scheduled * 1000).toISOString();
423
+ manualMessageObj.scheduled = manualMessageObj.time;
424
+ } else if (secondsDelay) {
425
+ const now = new Date();
426
+ now.setSeconds(now.getSeconds() + secondsDelay);
427
+ manualMessageObj.time = now.toISOString();
428
+ manualMessageObj.delayInSeconds = secondsDelay;
429
+ }
430
+ messages.push(manualMessageObj);
431
+ progress('Added manual message', 'info', 'ADD_MESSAGE', messages[messages.length - 1]);
432
+ }
433
+
434
+ if (contextUpsert) {
435
+ context = updateContext(context, contextUpsert);
436
+ progress('Upserted context', 'info', 'UPDATE_CONTEXT', contextUpsert);
437
+ }
438
+
439
+ if (resetIntent) {
440
+ resettedIntent = true;
441
+ }
442
+
443
+ }
444
+
445
+ if (resettedIntent && !_forward) {
446
+ conversation.intent = null;
447
+ conversation.intentScore = null;
448
+ conversation.locked = false;
449
+ conversation.lockedReason = '';
450
+ conversation.lockAttempts = 0;
451
+ progress('Reset conversation intent', 'info', 'UPDATE_CONVERSATION', {intent: null, intentScore: null, locked: false, lockAttempts: 0, lockedReason: ''});
452
+ }
453
+
454
+ // 4. Generate response
455
+ // If conversation previously locked, don't generate
456
+ if (!input.conversation.locked) {
457
+ // If conversation is newly locked, don't generate, unless instructions were provided and no custom messages were provided
458
+ if ((!conversation.locked || !hasNoInstructions) && !!hasNoCustomMessage) {
459
+ try {
460
+ progress('Parsing message', 'info', 'SET_PROCESSING', 'system');
461
+ const generatorPayload = await generator({
462
+ messages,
463
+ persona,
464
+ context,
465
+ llm: config.llm,
466
+ pmt: config.pmt,
467
+ });
468
+ if (!generatorPayload.send) {
469
+ progress('Generated response', 'failed', undefined, {error: generatorPayload.error || 'Unknown Reason'});
470
+ console.error(`Locking conversation, api returned send false: ${generatorPayload.message}`, generatorPayload.error || 'Unknown Reason');
471
+ conversation = lockConversation(conversation, 'API: ' + generatorPayload.error || 'Unknown Reason');
472
+ } else {
473
+ progress('Generated response', 'success', undefined, undefined);
474
+ // Check if already had message
475
+ const agentMessages = messages.filter(m => m.role === 'agent');
476
+ const lastAgentMessage = agentMessages[agentMessages.length - 1];
477
+ if (lastAgentMessage && lastAgentMessage.content === generatorPayload.message) {
478
+ // Error should not have happened
479
+ conversation = lockConversation(conversation, 'Duplicate message');
480
+ } else {
481
+ messages.push({
482
+ id: idGenerator('agent'),
483
+ role: 'agent',
484
+ content: generatorPayload.message,
485
+ time: new Date().toISOString()
486
+ });
487
+ progress('Added agent message', 'info', 'ADD_MESSAGE', messages[messages.length - 1]);
488
+ }
489
+
490
+ // Check if conversation was marked for forward (generator message still allowed to be sent)
491
+ if (generatorPayload.forward) {
492
+ conversation = lockConversation(conversation, 'API: ' + generatorPayload.forwardNote || 'Forwarded by API');
493
+ if (!_forward) {
494
+ _forward = generatorPayload.forward;
495
+ _forwardNote = generatorPayload.forwardNote;
496
+ }
497
+ }
498
+ }
499
+
500
+ } catch (e) {
501
+ console.error(`Locking conversation, error generating response: ${e.message}`);
502
+ conversation = lockConversation(conversation, 'API: ' + e.message);
503
+ }
504
+ }
505
+ }
506
+
507
+ progress('Parsing message', 'info', 'SET_PROCESSING', null);
508
+
509
+ return {
510
+ conversation: {
511
+ before: conversationBefore,
512
+ after: conversation,
513
+ forward: _forward || null,
514
+ forwardNote: _forwardNote || '',
515
+ },
516
+ messages: {
517
+ before: messagesBefore,
518
+ after: messages
519
+ },
520
+ message: {
521
+ before: messageBefore,
522
+ after: message
523
+ },
524
+ context: {
525
+ before: contextBefore,
526
+ after: context
527
+ }
528
+ }
529
+ }
530
+ }
@@ -0,0 +1,16 @@
1
+ import { toBuffer } from './file.js';
2
+ import { audioExtensions } from './audio-type.js';
3
+ import { videoExtensions } from './video-type.js';
4
+
5
+ export default async function audioBuffer(audio, allowVideo = false, source = '') {
6
+ const result = await toBuffer(audio, source);
7
+ if (!result) {
8
+ throw new Error(`Invalid audio type: ${typeof audio}`);
9
+ }
10
+ if (!audioExtensions.has(result.ext)) {
11
+ if (!(allowVideo && videoExtensions.has(result.ext))) {
12
+ throw new Error(`Invalid audio type: ${result.ext}`);
13
+ }
14
+ }
15
+ return result;
16
+ }
@@ -0,0 +1,27 @@
1
+ import { fileTypeFromBuffer } from './file-type.js';
2
+
3
+ export const audioExtensions = new Set([
4
+ 'mp3',
5
+ 'flac',
6
+ 'm4a',
7
+ 'opus',
8
+ 'ogg',
9
+ 'wav',
10
+ 'amr',
11
+ ]);
12
+
13
+ /**
14
+ * @typedef {Object} AudioTypeResult
15
+ * @property {string} ext - One of the supported [file types](https://github.com/sindresorhus/image-type#supported-file-types).
16
+ * @property {string} mime - The detected [MIME type](https://en.wikipedia.org/wiki/Internet_media_type).
17
+ */
18
+
19
+ /**
20
+ *
21
+ * @param {Buffer | Uint8Array} input
22
+ * @returns {Promise<AudioTypeResult | undefined>}
23
+ */
24
+ export default async function audioType(input) {
25
+ const result = await fileTypeFromBuffer(input);
26
+ return audioExtensions.has(result?.ext) && result;
27
+ }