@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.
- package/README.md +32 -0
- package/dist/{index-92deaa5f.cjs → exports-e7d51b70.cjs} +46618 -4591
- package/dist/index.cjs +58 -15
- package/dist/{multipart-parser-090f08a9.cjs → multipart-parser-e09a67c9.cjs} +13 -7
- package/dist/spirits-3b603262.cjs +1218 -0
- package/dist/spirits.cjs +9 -0
- package/dist/testing-tools.cjs +48 -0
- package/package.json +30 -8
- package/src/cli.js +162 -69
- package/src/core/config/agents.js +300 -7
- package/src/core/config/entities.js +58 -28
- package/src/core/config/index.js +37 -15
- package/src/core/config/project.js +160 -6
- package/src/core/config/workflow.js +13 -12
- package/src/core/data.js +27 -0
- package/src/core/index.js +386 -137
- package/src/core/sync.js +71 -0
- package/src/core/templates/Dockerfile +22 -0
- package/src/core/templates/app.js +453 -0
- package/src/core/templates/project-files.js +36 -0
- package/src/core/templates/template-package.json +13 -0
- package/src/exports.js +21 -17
- package/src/platform.js +189 -33
- package/src/public.d.ts.text +330 -0
- package/src/report.js +117 -0
- package/src/runtime/client/api.js +56 -159
- package/src/runtime/client/config.js +60 -11
- package/src/runtime/client/entity.js +19 -6
- package/src/runtime/client/index.js +5 -3
- package/src/runtime/client/message.js +13 -3
- package/src/runtime/client/platform.js +86 -0
- package/src/runtime/client/{agent.js → users.js} +35 -3
- package/src/runtime/client/utils.js +10 -9
- package/src/runtime/client/workflow.js +131 -9
- package/src/runtime/entry.js +2 -2
- package/src/testing-tools/dev.js +373 -0
- package/src/testing-tools/index.js +1 -0
- package/src/testing-tools/mocks.js +37 -5
- package/src/testing-tools/spirits.js +530 -0
- package/src/utils/audio-buffer.js +16 -0
- package/src/utils/audio-type.js +27 -0
- package/src/utils/configs/agents.js +68 -0
- package/src/utils/configs/entities.js +145 -0
- package/src/utils/configs/project.js +23 -0
- package/src/utils/configs/workflow.js +47 -0
- package/src/utils/file-type.js +569 -0
- package/src/utils/file.js +158 -0
- package/src/utils/glob.js +30 -0
- package/src/utils/image-buffer.js +23 -0
- package/src/utils/image-type.js +39 -0
- package/src/utils/index.js +1 -0
- package/src/utils/is-svg.js +37 -0
- package/src/utils/logger.js +111 -0
- package/src/utils/module.js +14 -25
- package/src/utils/project-templates.js +191 -0
- package/src/utils/project.js +387 -0
- package/src/utils/video-type.js +29 -0
- package/types/index.d.ts +7588 -206
- package/types/index.d.ts.map +97 -22
- package/dist/index-1b8d7dd2.cjs +0 -49555
- package/dist/index-2ccb115e.cjs +0 -49514
- package/dist/index-66b06a30.cjs +0 -49549
- package/dist/index-bc029a1d.cjs +0 -49528
- package/dist/index-d9a93523.cjs +0 -49527
- package/dist/multipart-parser-1508046a.cjs +0 -413
- package/dist/multipart-parser-7007403a.cjs +0 -413
- package/dist/multipart-parser-70c32c1d.cjs +0 -413
- package/dist/multipart-parser-71dec101.cjs +0 -413
- package/dist/multipart-parser-f15bf2e0.cjs +0 -414
- 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
|
-
|
|
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
|
+
}
|