@scout9/app 1.0.0-alpha.0.1.91 → 1.0.0-alpha.0.1.92
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/dist/{exports-212ef6be.cjs → exports-dfabefaf.cjs} +6 -6
- package/dist/index.cjs +2 -2
- package/dist/{multipart-parser-54a3ab5f.cjs → multipart-parser-52e1536f.cjs} +2 -2
- package/dist/{spirits-3b603262.cjs → spirits-6a709255.cjs} +102 -85
- package/dist/spirits.cjs +1 -1
- package/dist/testing-tools.cjs +2 -2
- package/package.json +2 -2
- package/src/testing-tools/spirits.js +446 -406
|
@@ -97,434 +97,474 @@
|
|
|
97
97
|
*/
|
|
98
98
|
export const Spirits = {
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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(
|
|
150
|
+
_conversation,
|
|
151
|
+
{locked: true, lockedReason: conversation.lockedReason || reason || 'Unknown'}
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const incrementLockAttempt = (_conversation, _config) => {
|
|
156
|
+
if (typeof _conversation.lockAttempts !== 'number') {
|
|
157
|
+
_conversation.lockAttempts = 0;
|
|
158
|
+
}
|
|
159
|
+
_conversation.lockAttempts++;
|
|
160
|
+
if (_conversation.lockAttempts > (_config?.maxLockAttempts || 3)) {
|
|
161
|
+
_conversation.locked = true;
|
|
162
|
+
_conversation.lockedReason = `Max lock attempts exceeded (${_conversation.lockAttempts} > ${(_config?.maxLockAttempts || 3)})`;
|
|
163
|
+
}
|
|
164
|
+
progress(
|
|
165
|
+
'Incremented lock attempt',
|
|
166
|
+
'info',
|
|
167
|
+
'UPDATE_CONVERSATION',
|
|
168
|
+
{
|
|
169
|
+
lockAttempts: _conversation.lockAttempts,
|
|
170
|
+
locked: _conversation.locked,
|
|
171
|
+
lockedReason: _conversation.lockedReason || ''
|
|
129
172
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
173
|
+
);
|
|
174
|
+
return _conversation;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const _addInstruction = (
|
|
178
|
+
instruction,
|
|
179
|
+
_messages,
|
|
180
|
+
_conversation,
|
|
181
|
+
_config,
|
|
182
|
+
previousLockAttempt,
|
|
183
|
+
id
|
|
184
|
+
) => {
|
|
185
|
+
const systemMessages = _messages.filter(m => m.role === 'system');
|
|
186
|
+
const lastSystemMessage = systemMessages[systemMessages.length - 1];
|
|
187
|
+
let addedMessage = false;
|
|
188
|
+
let changedConversation = false;
|
|
189
|
+
|
|
190
|
+
// If instruction does not equal previous system message, add it, otherwise lock attempt
|
|
191
|
+
if (!lastSystemMessage || instruction !== lastSystemMessage.content) {
|
|
192
|
+
_messages.push({
|
|
193
|
+
id,
|
|
194
|
+
role: 'system',
|
|
195
|
+
content: instruction,
|
|
196
|
+
time: new Date().toISOString()
|
|
197
|
+
});
|
|
198
|
+
addedMessage = true;
|
|
199
|
+
} else {
|
|
200
|
+
// Handle repeated instruction
|
|
201
|
+
// Increment lock attempt if instructions are repeated and we haven't already incremented lock attempt (for example if a forward is provided)
|
|
202
|
+
if (previousLockAttempt === (conversation.lockAttempts || 0)) {
|
|
203
|
+
_conversation = incrementLockAttempt(_conversation, _config);
|
|
204
|
+
changedConversation = true;
|
|
137
205
|
}
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
conversation: _conversation,
|
|
209
|
+
messages: _messages,
|
|
210
|
+
addedMessage,
|
|
211
|
+
changedConversation
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const addInstruction = (instruction, previousLockAttempt, id = idGenerator('sys')) => {
|
|
216
|
+
const {
|
|
217
|
+
conversation: newConversation,
|
|
218
|
+
messages: newMessages,
|
|
219
|
+
addedMessage,
|
|
220
|
+
changedConversation
|
|
221
|
+
} = _addInstruction(instruction, messages, conversation, config, previousLockAttempt, id);
|
|
222
|
+
conversation = newConversation;
|
|
223
|
+
messages = newMessages;
|
|
224
|
+
if (addedMessage) {
|
|
225
|
+
progress('Added instruction', 'info', 'ADD_MESSAGE', newMessages[newMessages.length - 1]);
|
|
226
|
+
}
|
|
227
|
+
if (changedConversation) {
|
|
228
|
+
progress('Updated conversation', 'info', 'UPDATE_CONVERSATION', newConversation);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// 1. Check inputs
|
|
233
|
+
if (!conversation.$agent) {
|
|
234
|
+
throw new Error(`SpiritsError: No agent found in conversation, must define ".$agent" in the conversation`);
|
|
235
|
+
}
|
|
236
|
+
const persona = (config.persona || config.agents).find(p => p.id === conversation.$agent);
|
|
237
|
+
if (!persona) {
|
|
238
|
+
if ((config.persona || config.agents).some(a => !a.id)) {
|
|
239
|
+
throw new Error(`SpiritsError: No persona found ("${conversation.$agent}") in provided config, some persona's did not contain an "id" (Internal Mapping Error)`);
|
|
240
|
+
}
|
|
241
|
+
throw new Error(`SpiritsError: No persona found ("${conversation.$agent}") in provided config`);
|
|
242
|
+
}
|
|
243
|
+
if (!messages.every(m => !!m.id)) {
|
|
244
|
+
throw new Error(`SpiritsError: Every message must have an ".id", ensure all messages have an id assigned before running`);
|
|
245
|
+
}
|
|
246
|
+
if (!messages.every(m => m.role === 'customer' || m.role === 'agent' || m.role === 'system')) {
|
|
247
|
+
const invalidRoles = messages.filter(m => m.role !== 'customer' && m.role !== 'agent' && m.role !== 'system');
|
|
248
|
+
throw new Error(`SpiritsError: Every message must have a role of "customer", "agent", or "system". Got invalid roles: ${invalidRoles.map(
|
|
249
|
+
m => m.role).join(', ')}`);
|
|
250
|
+
}
|
|
251
|
+
// if message is not in messages, then add it
|
|
252
|
+
if (!messages.find(m => m.id === input.message.id)) {
|
|
253
|
+
messages.push(input.message);
|
|
254
|
+
}
|
|
138
255
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
256
|
+
// 2. Parse the message
|
|
257
|
+
progress('Parsing message', 'info', 'SET_PROCESSING', 'user');
|
|
258
|
+
const parsePayload = await parser(message.content, 'en');
|
|
259
|
+
if (parsePayload.intent) {
|
|
260
|
+
message.intent = parsePayload.intent;
|
|
261
|
+
}
|
|
262
|
+
if (typeof parsePayload.intentScore === 'number') {
|
|
263
|
+
message.intentScore = parsePayload.intentScore;
|
|
264
|
+
}
|
|
265
|
+
message.context = parsePayload.context;
|
|
266
|
+
const index = messages.findIndex(m => m.content === message.content || m.id === message.id);
|
|
267
|
+
if (index === -1) {
|
|
268
|
+
const _message = {
|
|
269
|
+
id: idGenerator('customer'),
|
|
270
|
+
role: 'customer',
|
|
271
|
+
content: message,
|
|
272
|
+
context: parsePayload.context,
|
|
273
|
+
time: new Date().toISOString()
|
|
274
|
+
};
|
|
275
|
+
if (parsePayload.intent) {
|
|
276
|
+
_message.intent = parsePayload.intent;
|
|
277
|
+
}
|
|
278
|
+
if (typeof parsePayload.intentScore === 'number') {
|
|
279
|
+
_message.intentScore = parsePayload.intentScore;
|
|
280
|
+
}
|
|
281
|
+
message = _message;
|
|
282
|
+
messages.push(_message);
|
|
283
|
+
progress('Added message', 'info', 'ADD_MESSAGE', _message);
|
|
284
|
+
} else {
|
|
285
|
+
messages[index].context = parsePayload.context;
|
|
286
|
+
if (parsePayload.intent) {
|
|
287
|
+
messages[index].intent = parsePayload.intent;
|
|
288
|
+
}
|
|
289
|
+
if (typeof parsePayload.intentScore === 'number') {
|
|
290
|
+
messages[index].intentScore = parsePayload.intentScore;
|
|
291
|
+
}
|
|
292
|
+
message = messages[index];
|
|
293
|
+
progress('Parsed message', 'success', 'UPDATE_MESSAGE', message);
|
|
294
|
+
}
|
|
295
|
+
// If this is the first user message, then update conversations intent
|
|
296
|
+
const previousUserMessages = messages.filter(m => m.role === 'customer' && m.content !== message.content);
|
|
297
|
+
if (!conversation.intent || previousUserMessages.length === 0 && parsePayload.intent) {
|
|
298
|
+
conversation.intent = parsePayload.intent;
|
|
299
|
+
conversation.intentScore = parsePayload?.intentScore || 0;
|
|
300
|
+
progress(
|
|
301
|
+
'Updated conversation intent',
|
|
302
|
+
'info',
|
|
303
|
+
'UPDATE_CONVERSATION',
|
|
304
|
+
{intent: parsePayload.intent, intentScore: parsePayload?.intentScore || 0}
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
const oldKeyCount = Object.keys(context).length;
|
|
308
|
+
context = updateContext(context, parsePayload.context);
|
|
309
|
+
const newKeyCount = Object.keys(context).length;
|
|
310
|
+
|
|
311
|
+
if (!conversation.locked && (newKeyCount > oldKeyCount)) {
|
|
312
|
+
// Reset lock attempts
|
|
313
|
+
conversation.locked = false;
|
|
314
|
+
conversation.lockAttempts = 0;
|
|
315
|
+
conversation.lockedReason = '';
|
|
316
|
+
progress('Reset lock', 'info', 'UPDATE_CONVERSATION', {locked: false, lockAttempts: 0, lockedReason: ''});
|
|
317
|
+
}
|
|
142
318
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
319
|
+
const noNewContext = Object.keys(parsePayload.context).length === 0;
|
|
320
|
+
|
|
321
|
+
// 3. Run the workflow
|
|
322
|
+
progress('Running workflow', 'info', 'SET_PROCESSING', 'system');
|
|
323
|
+
const slots = await workflow({
|
|
324
|
+
messages,
|
|
325
|
+
conversation,
|
|
326
|
+
context,
|
|
327
|
+
message,
|
|
328
|
+
agent: persona,
|
|
329
|
+
customer,
|
|
330
|
+
intent: {
|
|
331
|
+
current: recentUserMessage(messages)?.intent || null,
|
|
332
|
+
flow: messages.map(m => m.intent).filter(Boolean),
|
|
333
|
+
initial: conversation.intent || null
|
|
334
|
+
},
|
|
335
|
+
stagnationCount: conversation.lockAttempts || 0
|
|
336
|
+
}).then((res) => Array.isArray(res) ? res : [res]);
|
|
337
|
+
const hasNoInstructions = slots.every(s => !s.instructions || (Array.isArray(s) && s.instructions.length === 0));
|
|
338
|
+
const hasNoCustomMessage = slots.every(s => !s.message);
|
|
339
|
+
const previousLockAttempt = conversation.lockAttempts || 0; // Used to track
|
|
340
|
+
|
|
341
|
+
if (hasNoInstructions && noNewContext) {
|
|
342
|
+
conversation = incrementLockAttempt(conversation, config);
|
|
343
|
+
} else {
|
|
344
|
+
conversation.lockAttempts = 0;
|
|
345
|
+
conversation.locked = false;
|
|
346
|
+
conversation.lockedReason = '';
|
|
347
|
+
progress('Reset lock', 'info', 'UPDATE_CONVERSATION', {lockAttempts: 0, locked: false, lockedReason: ''});
|
|
348
|
+
}
|
|
147
349
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
350
|
+
let resettedIntent = false;
|
|
351
|
+
let _forward;
|
|
352
|
+
let _forwardNote;
|
|
353
|
+
|
|
354
|
+
for (const {
|
|
355
|
+
forward,
|
|
356
|
+
forwardNote,
|
|
357
|
+
instructions,
|
|
358
|
+
removeInstructions,
|
|
359
|
+
message: manualMessage,
|
|
360
|
+
scheduled,
|
|
361
|
+
resetIntent,
|
|
362
|
+
secondsDelay,
|
|
363
|
+
contextUpsert
|
|
364
|
+
} of slots) {
|
|
365
|
+
|
|
366
|
+
// Forward to agent or other agent
|
|
367
|
+
if (forward) {
|
|
368
|
+
conversation = lockConversation(conversation, 'App instructed forward');
|
|
369
|
+
_forward = forward;
|
|
370
|
+
_forwardNote = forwardNote;
|
|
371
|
+
if (typeof forward === 'string') {
|
|
372
|
+
updateConversation(conversation, {forwarded: forward});
|
|
373
|
+
messages.push({
|
|
374
|
+
id: idGenerator('sys'),
|
|
375
|
+
role: 'system',
|
|
376
|
+
content: `forwarded to "${forward}"`,
|
|
377
|
+
time: new Date().toISOString()
|
|
378
|
+
});
|
|
379
|
+
progress(`Forwarded to "${forward}"`, 'info', 'ADD_MESSAGE', messages[messages.length - 1]);
|
|
380
|
+
} else if (typeof forward === 'boolean') {
|
|
381
|
+
updateConversation(conversation, {forwarded: conversation.$agent});
|
|
382
|
+
messages.push({
|
|
383
|
+
id: idGenerator('sys'),
|
|
384
|
+
role: 'system',
|
|
385
|
+
content: `forwarded to "${forward}"`,
|
|
386
|
+
time: new Date().toISOString()
|
|
387
|
+
});
|
|
388
|
+
progress(`Forwarded to agent`, 'info', 'ADD_MESSAGE', messages[messages.length - 1]);
|
|
151
389
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
390
|
+
} else {
|
|
391
|
+
messages.push({
|
|
392
|
+
id: idGenerator('sys'),
|
|
393
|
+
role: 'system',
|
|
394
|
+
content: `forwarded to "${forward.to}" ${forward.mode ? ' (' + forward.mode + ')' : ''}`,
|
|
395
|
+
time: new Date().toISOString()
|
|
396
|
+
});
|
|
397
|
+
progress(
|
|
398
|
+
`Forwarded to "${forward.to}" ${forward.mode ? ' (' + forward.mode + ')' : ''}`,
|
|
399
|
+
'info',
|
|
400
|
+
'ADD_MESSAGE',
|
|
401
|
+
messages[messages.length - 1]
|
|
402
|
+
);
|
|
403
|
+
updateConversation(conversation, {forwarded: forward.to});
|
|
163
404
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Insert instructions context
|
|
408
|
+
if (instructions) {
|
|
409
|
+
if (typeof instructions === 'string') {
|
|
410
|
+
addInstruction(instructions, previousLockAttempt);
|
|
411
|
+
} else if (Array.isArray(instructions)) {
|
|
412
|
+
for (const instruction of instructions) {
|
|
413
|
+
if (typeof instruction === 'string') {
|
|
414
|
+
addInstruction(instruction, previousLockAttempt);
|
|
187
415
|
} else {
|
|
188
|
-
|
|
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;
|
|
416
|
+
addInstruction(instruction.content, previousLockAttempt, instruction.id);
|
|
261
417
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
message = _message;
|
|
266
|
-
messages.push(_message);
|
|
267
|
-
progress('Added message', 'info', 'ADD_MESSAGE', _message);
|
|
418
|
+
}
|
|
419
|
+
} else if (typeof instructions === 'object' && 'content' in instructions && 'id' in instructions) {
|
|
420
|
+
addInstruction(instructions.content, previousLockAttempt, instructions.id);
|
|
268
421
|
} else {
|
|
269
|
-
|
|
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);
|
|
422
|
+
throw new Error('SpiritsError: instructions must be a string or array or {content: "", id: ""}');
|
|
278
423
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
// Remove messages that have the given ids
|
|
428
|
+
if (removeInstructions) {
|
|
429
|
+
for (const instructionId of removeInstructions) {
|
|
430
|
+
const index = messages.findIndex(m => m.id === instructionId);
|
|
431
|
+
if (index > -1) {
|
|
432
|
+
messages.splice(index, 1);
|
|
433
|
+
progress('Remove instruction', 'info', 'REMOVE_MESSAGE', instructionId);
|
|
434
|
+
} else {
|
|
435
|
+
console.log('instruction not found', instructionId);
|
|
436
|
+
}
|
|
285
437
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (manualMessage) {
|
|
441
|
+
let manualMessageObj = {
|
|
442
|
+
id: idGenerator('agent'),
|
|
443
|
+
role: 'agent',
|
|
444
|
+
content: manualMessage,
|
|
445
|
+
time: new Date().toISOString()
|
|
446
|
+
};
|
|
447
|
+
if (typeof manualMessage !== 'string') {
|
|
448
|
+
throw new Error('Manual message must be of type "string"');
|
|
296
449
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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: ''});
|
|
450
|
+
if (scheduled) {
|
|
451
|
+
manualMessageObj.time = new Date(scheduled * 1000).toISOString();
|
|
452
|
+
manualMessageObj.scheduled = manualMessageObj.time;
|
|
453
|
+
} else if (secondsDelay) {
|
|
454
|
+
const now = new Date();
|
|
455
|
+
now.setSeconds(now.getSeconds() + secondsDelay);
|
|
456
|
+
manualMessageObj.time = now.toISOString();
|
|
457
|
+
manualMessageObj.delayInSeconds = secondsDelay;
|
|
327
458
|
}
|
|
459
|
+
messages.push(manualMessageObj);
|
|
460
|
+
progress('Added manual message', 'info', 'ADD_MESSAGE', messages[messages.length - 1]);
|
|
461
|
+
}
|
|
328
462
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
}
|
|
463
|
+
if (contextUpsert) {
|
|
464
|
+
context = updateContext(context, contextUpsert);
|
|
465
|
+
progress('Upserted context', 'info', 'UPDATE_CONTEXT', contextUpsert);
|
|
466
|
+
}
|
|
399
467
|
|
|
468
|
+
if (resetIntent) {
|
|
469
|
+
resettedIntent = true;
|
|
470
|
+
}
|
|
400
471
|
|
|
401
|
-
|
|
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
|
-
}
|
|
472
|
+
}
|
|
413
473
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
}
|
|
474
|
+
if (resettedIntent && !_forward) {
|
|
475
|
+
conversation.intent = null;
|
|
476
|
+
conversation.intentScore = null;
|
|
477
|
+
conversation.locked = false;
|
|
478
|
+
conversation.lockedReason = '';
|
|
479
|
+
conversation.lockAttempts = 0;
|
|
480
|
+
progress(
|
|
481
|
+
'Reset conversation intent',
|
|
482
|
+
'info',
|
|
483
|
+
'UPDATE_CONVERSATION',
|
|
484
|
+
{intent: null, intentScore: null, locked: false, lockAttempts: 0, lockedReason: ''}
|
|
485
|
+
);
|
|
486
|
+
}
|
|
433
487
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
488
|
+
// 4. Generate response
|
|
489
|
+
// If conversation previously locked, don't generate
|
|
490
|
+
if (!input.conversation.locked) {
|
|
491
|
+
// If conversation is newly locked, don't generate, unless instructions were provided and no custom messages were provided
|
|
492
|
+
if ((!conversation.locked || !hasNoInstructions) && !!hasNoCustomMessage) {
|
|
493
|
+
try {
|
|
494
|
+
progress('Parsing message', 'info', 'SET_PROCESSING', 'system');
|
|
495
|
+
const generatorPayload = await generator({
|
|
496
|
+
messages,
|
|
497
|
+
persona,
|
|
498
|
+
context,
|
|
499
|
+
llm: config.llm,
|
|
500
|
+
pmt: config.pmt
|
|
501
|
+
});
|
|
502
|
+
if (!generatorPayload.send) {
|
|
503
|
+
progress('Generated response', 'failed', undefined, {error: generatorPayload.error || 'Unknown Reason'});
|
|
504
|
+
console.error(
|
|
505
|
+
`Locking conversation, api returned send false: ${generatorPayload.message}`,
|
|
506
|
+
generatorPayload.error || 'Unknown Reason'
|
|
507
|
+
);
|
|
508
|
+
conversation = lockConversation(conversation, 'API: ' + generatorPayload.error || 'Unknown Reason');
|
|
509
|
+
} else {
|
|
510
|
+
progress('Generated response', 'success', undefined, undefined);
|
|
511
|
+
// Check if already had message
|
|
512
|
+
const agentMessages = messages.filter(m => m.role === 'agent');
|
|
513
|
+
const lastAgentMessage = agentMessages[agentMessages.length - 1];
|
|
514
|
+
if (lastAgentMessage && lastAgentMessage.content === generatorPayload.message) {
|
|
515
|
+
// Error should not have happened
|
|
516
|
+
conversation = lockConversation(conversation, 'Duplicate message');
|
|
517
|
+
} else {
|
|
518
|
+
messages.push({
|
|
519
|
+
id: idGenerator('agent'),
|
|
520
|
+
role: 'agent',
|
|
521
|
+
content: generatorPayload.message,
|
|
522
|
+
time: new Date().toISOString()
|
|
523
|
+
});
|
|
524
|
+
progress('Added agent message', 'info', 'ADD_MESSAGE', messages[messages.length - 1]);
|
|
437
525
|
}
|
|
438
526
|
|
|
439
|
-
if (
|
|
440
|
-
|
|
527
|
+
// Check if conversation was marked for forward (generator message still allowed to be sent)
|
|
528
|
+
if (generatorPayload.forward) {
|
|
529
|
+
conversation = lockConversation(
|
|
530
|
+
conversation,
|
|
531
|
+
'API: ' + generatorPayload.forwardNote || 'Forwarded by API'
|
|
532
|
+
);
|
|
533
|
+
if (!_forward) {
|
|
534
|
+
_forward = generatorPayload.forward;
|
|
535
|
+
_forwardNote = generatorPayload.forwardNote;
|
|
536
|
+
}
|
|
441
537
|
}
|
|
538
|
+
}
|
|
442
539
|
|
|
540
|
+
} catch (e) {
|
|
541
|
+
console.error(`Locking conversation, error generating response: ${e.message}`);
|
|
542
|
+
conversation = lockConversation(conversation, 'API: ' + e.message);
|
|
443
543
|
}
|
|
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
|
-
}
|
|
544
|
+
}
|
|
529
545
|
}
|
|
530
|
-
|
|
546
|
+
|
|
547
|
+
progress('Parsing message', 'info', 'SET_PROCESSING', null);
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
conversation: {
|
|
551
|
+
before: conversationBefore,
|
|
552
|
+
after: conversation,
|
|
553
|
+
forward: _forward || null,
|
|
554
|
+
forwardNote: _forwardNote || ''
|
|
555
|
+
},
|
|
556
|
+
messages: {
|
|
557
|
+
before: messagesBefore,
|
|
558
|
+
after: messages
|
|
559
|
+
},
|
|
560
|
+
message: {
|
|
561
|
+
before: messageBefore,
|
|
562
|
+
after: message
|
|
563
|
+
},
|
|
564
|
+
context: {
|
|
565
|
+
before: contextBefore,
|
|
566
|
+
after: context
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
};
|