@peopl-health/nexus 2.5.1 → 2.5.2
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.
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { airtable, getBase } = require('../config/airtableConfig');
|
|
2
|
+
const { Message } = require('../models/messageModel');
|
|
2
3
|
const { addMsgAssistant, replyAssistant } = require('../services/assistantService');
|
|
3
4
|
const { createProvider } = require('../adapters/registry');
|
|
4
5
|
const runtimeConfig = require('../config/runtimeConfig');
|
|
@@ -42,7 +43,7 @@ class NexusMessaging {
|
|
|
42
43
|
this.pendingResponses = new Map();
|
|
43
44
|
this.batchingConfig = {
|
|
44
45
|
enabled: config.messageBatching?.enabled ?? false,
|
|
45
|
-
baseWaitTime: config.messageBatching?.baseWaitTime ??
|
|
46
|
+
baseWaitTime: config.messageBatching?.baseWaitTime ?? 10000
|
|
46
47
|
};
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -353,6 +354,18 @@ class NexusMessaging {
|
|
|
353
354
|
|
|
354
355
|
const chatId = messageData.from || messageData.From;
|
|
355
356
|
|
|
357
|
+
// Send initial typing indicator with delay
|
|
358
|
+
if (chatId && this.provider && typeof this.provider.sendTypingIndicator === 'function') {
|
|
359
|
+
const messageId = messageData.id || messageData.MessageSid || messageData.message_id;
|
|
360
|
+
if (messageId) {
|
|
361
|
+
setTimeout(() => {
|
|
362
|
+
this.provider.sendTypingIndicator(messageId).catch(err =>
|
|
363
|
+
logger.debug('[processIncomingMessage] Typing indicator failed', { error: err.message })
|
|
364
|
+
);
|
|
365
|
+
}, 3000);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
356
369
|
if (chatId && hasPreprocessingHandler()) {
|
|
357
370
|
const stop = await invokePreprocessingHandler({
|
|
358
371
|
code: chatId,
|
|
@@ -372,28 +385,11 @@ class NexusMessaging {
|
|
|
372
385
|
} else if (messageData.flow) {
|
|
373
386
|
return await this.handleFlow(messageData);
|
|
374
387
|
} else {
|
|
375
|
-
if (chatId && this.provider && typeof this.provider.sendTypingIndicator === 'function') {
|
|
376
|
-
const messageId = messageData.id || messageData.MessageSid || messageData.message_id;
|
|
377
|
-
if (messageId) {
|
|
378
|
-
this.provider.sendTypingIndicator(messageId)
|
|
379
|
-
.then(() => logger.debug('[processIncomingMessage] Typing indicator sent successfully', { messageId }))
|
|
380
|
-
.catch(err => logger.warn('[processIncomingMessage] Typing indicator failed', {
|
|
381
|
-
error: err.message,
|
|
382
|
-
messageId
|
|
383
|
-
}));
|
|
384
|
-
} else {
|
|
385
|
-
logger.warn('[processIncomingMessage] No message ID found for typing indicator', {
|
|
386
|
-
messageData: JSON.stringify(messageData).substring(0, 200)
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
388
|
// For regular messages and media, use batching if enabled
|
|
392
389
|
logger.info('Batching config:', this.batchingConfig);
|
|
393
390
|
if (this.batchingConfig.enabled && chatId) {
|
|
394
391
|
return await this._handleWithBatching(messageData, chatId);
|
|
395
392
|
} else {
|
|
396
|
-
// Handle media and regular messages without batching
|
|
397
393
|
if (messageData.media) {
|
|
398
394
|
return await this.handleMedia(messageData);
|
|
399
395
|
} else {
|
|
@@ -650,14 +646,24 @@ class NexusMessaging {
|
|
|
650
646
|
* Handle message with batching - waits for additional messages before processing
|
|
651
647
|
*/
|
|
652
648
|
async _handleWithBatching(messageData, chatId) {
|
|
653
|
-
|
|
654
|
-
|
|
649
|
+
const existing = this.pendingResponses.get(chatId);
|
|
650
|
+
if (existing) {
|
|
651
|
+
clearTimeout(existing.timeoutId);
|
|
652
|
+
if (existing.typingInterval) {
|
|
653
|
+
clearInterval(existing.typingInterval);
|
|
654
|
+
}
|
|
655
655
|
logger.info(`Received additional message from ${chatId}, resetting wait timer`);
|
|
656
656
|
}
|
|
657
657
|
|
|
658
|
+
// Start typing indicator refresh for batching period
|
|
659
|
+
const typingInterval = await this._startTypingRefresh(chatId);
|
|
660
|
+
|
|
658
661
|
const waitTime = this.batchingConfig.baseWaitTime;
|
|
659
662
|
const timeoutId = setTimeout(async () => {
|
|
660
663
|
try {
|
|
664
|
+
if (typingInterval) {
|
|
665
|
+
clearInterval(typingInterval);
|
|
666
|
+
}
|
|
661
667
|
this.pendingResponses.delete(chatId);
|
|
662
668
|
await this._handleBatchedMessages(chatId);
|
|
663
669
|
} catch (error) {
|
|
@@ -665,17 +671,44 @@ class NexusMessaging {
|
|
|
665
671
|
}
|
|
666
672
|
}, waitTime);
|
|
667
673
|
|
|
668
|
-
this.pendingResponses.set(chatId, timeoutId);
|
|
674
|
+
this.pendingResponses.set(chatId, { timeoutId, typingInterval });
|
|
669
675
|
logger.info(`Waiting ${Math.round(waitTime/1000)} seconds for more messages from ${chatId}`);
|
|
670
676
|
}
|
|
671
677
|
|
|
678
|
+
/**
|
|
679
|
+
* Start typing indicator refresh interval
|
|
680
|
+
*/
|
|
681
|
+
async _startTypingRefresh(chatId) {
|
|
682
|
+
if (!this.provider || typeof this.provider.sendTypingIndicator !== 'function') {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const lastMessage = await Message.findOne({
|
|
687
|
+
numero: chatId,
|
|
688
|
+
from_me: false,
|
|
689
|
+
message_id: { $exists: true, $ne: null }
|
|
690
|
+
}).sort({ createdAt: -1 });
|
|
691
|
+
|
|
692
|
+
if (!lastMessage?.message_id) return null;
|
|
693
|
+
|
|
694
|
+
return setInterval(() =>
|
|
695
|
+
this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
|
|
696
|
+
logger.debug('[_startTypingRefresh] Failed', { error: err.message })
|
|
697
|
+
), 5000
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
|
|
672
701
|
/**
|
|
673
702
|
* Process all batched messages for a chat
|
|
674
703
|
*/
|
|
675
704
|
async _handleBatchedMessages(chatId) {
|
|
705
|
+
let typingInterval = null;
|
|
706
|
+
|
|
676
707
|
try {
|
|
677
708
|
logger.info(`Processing batched messages from ${chatId} (including media if any)`);
|
|
678
709
|
|
|
710
|
+
typingInterval = await this._startTypingRefresh(chatId);
|
|
711
|
+
|
|
679
712
|
// Get assistant response
|
|
680
713
|
const result = await replyAssistant(chatId);
|
|
681
714
|
const botResponse = typeof result === 'string' ? result : result?.output;
|
|
@@ -696,6 +729,10 @@ class NexusMessaging {
|
|
|
696
729
|
|
|
697
730
|
} catch (error) {
|
|
698
731
|
logger.error('Error in batched message handling:', { error: error.message });
|
|
732
|
+
} finally {
|
|
733
|
+
if (typingInterval) {
|
|
734
|
+
clearInterval(typingInterval);
|
|
735
|
+
}
|
|
699
736
|
}
|
|
700
737
|
}
|
|
701
738
|
|
|
@@ -703,8 +740,14 @@ class NexusMessaging {
|
|
|
703
740
|
* Clear pending response for a chat (useful for cleanup)
|
|
704
741
|
*/
|
|
705
742
|
clearPendingResponse(chatId) {
|
|
706
|
-
|
|
707
|
-
|
|
743
|
+
const pending = this.pendingResponses.get(chatId);
|
|
744
|
+
if (pending) {
|
|
745
|
+
if (pending.timeoutId) {
|
|
746
|
+
clearTimeout(pending.timeoutId);
|
|
747
|
+
}
|
|
748
|
+
if (pending.typingInterval) {
|
|
749
|
+
clearInterval(pending.typingInterval);
|
|
750
|
+
}
|
|
708
751
|
this.pendingResponses.delete(chatId);
|
|
709
752
|
}
|
|
710
753
|
}
|
|
@@ -1,32 +1,18 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const AWS = require('../config/awsConfig.js');
|
|
1
|
+
const { withTracing } = require('../utils/tracingDecorator.js');
|
|
3
2
|
const llmConfig = require('../config/llmConfig');
|
|
4
|
-
const runtimeConfig = require('../config/runtimeConfig');
|
|
5
3
|
const { BaseAssistant } = require('../assistants/BaseAssistant');
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const { getCurRow } = require('../helpers/assistantHelper.js');
|
|
14
|
-
const { runAssistantAndWait, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
|
|
15
|
-
const { getThread, getThreadInfo } = require('../helpers/threadHelper.js');
|
|
16
|
-
const { withTracing } = require('../utils/tracingDecorator.js');
|
|
17
|
-
const { processThreadMessage } = require('../helpers/processHelper.js');
|
|
18
|
-
const { getLastMessages, updateMessageRecord } = require('../helpers/messageHelper.js');
|
|
19
|
-
const { withThreadRecovery } = require('../helpers/threadRecoveryHelper.js');
|
|
20
|
-
const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
|
|
21
|
-
const { logger } = require('../utils/logger');
|
|
22
|
-
|
|
23
|
-
const DEFAULT_MAX_RETRIES = parseInt(process.env.MAX_RETRIES || '30', 10);
|
|
4
|
+
const {
|
|
5
|
+
createAssistantCore,
|
|
6
|
+
addMsgAssistantCore,
|
|
7
|
+
addInstructionCore,
|
|
8
|
+
replyAssistantCore,
|
|
9
|
+
switchAssistantCore
|
|
10
|
+
} = require('./assistantServiceCore');
|
|
24
11
|
|
|
25
12
|
let assistantConfig = null;
|
|
26
13
|
let assistantRegistry = {};
|
|
27
14
|
let customGetAssistantById = null;
|
|
28
15
|
|
|
29
|
-
|
|
30
16
|
const configureAssistants = (config) => {
|
|
31
17
|
if (!config) {
|
|
32
18
|
throw new Error('Assistant configuration is required');
|
|
@@ -99,224 +85,46 @@ const overrideGetAssistantById = (resolverFn) => {
|
|
|
99
85
|
|
|
100
86
|
const getAssistantById = (assistant_id, thread) => {
|
|
101
87
|
if (customGetAssistantById) {
|
|
102
|
-
|
|
103
|
-
if (inst) return inst;
|
|
88
|
+
return customGetAssistantById(assistant_id, thread);
|
|
104
89
|
}
|
|
105
90
|
|
|
106
|
-
if (
|
|
107
|
-
|
|
91
|
+
if (assistantRegistry[assistant_id]) {
|
|
92
|
+
const AssistantClass = assistantRegistry[assistant_id];
|
|
93
|
+
return new AssistantClass({ thread });
|
|
108
94
|
}
|
|
109
|
-
|
|
110
|
-
const AssistantClass = assistantRegistry[assistant_id];
|
|
111
|
-
if (!AssistantClass) {
|
|
112
|
-
throw new Error(`Assistant '${assistant_id}' not found. Available assistants: ${Object.keys(assistantRegistry).join(', ')}`);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
116
|
-
const sharedClient = provider?.getClient?.() || llmConfig.openaiClient || null;
|
|
117
95
|
|
|
118
|
-
if (
|
|
119
|
-
|
|
96
|
+
if (assistantConfig && assistantConfig[assistant_id]) {
|
|
97
|
+
const config = assistantConfig[assistant_id];
|
|
98
|
+
return new BaseAssistant({
|
|
99
|
+
...config,
|
|
120
100
|
assistantId: assistant_id,
|
|
121
|
-
thread
|
|
122
|
-
client: sharedClient,
|
|
123
|
-
provider
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
return new AssistantClass(thread);
|
|
129
|
-
} catch (error) {
|
|
130
|
-
return new AssistantClass({
|
|
131
|
-
thread,
|
|
132
|
-
assistantId: assistant_id,
|
|
133
|
-
client: sharedClient,
|
|
134
|
-
provider
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const createAssistant = async (code, assistant_id, messages=[], force=false) => {
|
|
141
|
-
const findThread = await Thread.findOne({ code: code });
|
|
142
|
-
logger.info('[createAssistant] findThread', findThread);
|
|
143
|
-
if (findThread && findThread.getConversationId() && !force) {
|
|
144
|
-
logger.info('[createAssistant] Thread already exists');
|
|
145
|
-
const updateFields = { active: true, stopped: false };
|
|
146
|
-
Thread.setAssistantId(updateFields, assistant_id);
|
|
147
|
-
await Thread.updateOne({ code: code }, { $set: updateFields });
|
|
148
|
-
return findThread;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (force && findThread?.getConversationId()) {
|
|
152
|
-
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
153
|
-
await provider.deleteConversation(findThread.getConversationId());
|
|
154
|
-
logger.info('[createAssistant] Deleted old conversation, will create new one');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const curRow = await getCurRow(Historial_Clinico_ID, code);
|
|
158
|
-
logger.info('[createAssistant] curRow', curRow[0]);
|
|
159
|
-
const nombre = curRow?.[0]?.['name'] || null;
|
|
160
|
-
const patientId = curRow?.[0]?.['record_id'] || null;
|
|
161
|
-
|
|
162
|
-
const assistant = getAssistantById(assistant_id, null);
|
|
163
|
-
const initialThread = await assistant.create(code, curRow[0]);
|
|
164
|
-
|
|
165
|
-
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
166
|
-
for (const message of messages) {
|
|
167
|
-
await provider.addMessage({
|
|
168
|
-
threadId: initialThread.id,
|
|
169
|
-
role: 'assistant',
|
|
170
|
-
content: message
|
|
101
|
+
thread
|
|
171
102
|
});
|
|
172
103
|
}
|
|
173
|
-
|
|
174
|
-
const thread = {
|
|
175
|
-
code: code,
|
|
176
|
-
patient_id: patientId,
|
|
177
|
-
nombre: nombre,
|
|
178
|
-
active: true
|
|
179
|
-
};
|
|
180
|
-
Thread.setAssistantId(thread, assistant_id);
|
|
181
|
-
Thread.setConversationId(thread, initialThread.id);
|
|
182
104
|
|
|
183
|
-
|
|
184
|
-
const options = { new: true, upsert: true };
|
|
185
|
-
const updatedThread = await Thread.findOneAndUpdate(condition, {run_id: null, ...thread}, options);
|
|
186
|
-
logger.info('[createAssistant] Updated thread:', updatedThread);
|
|
187
|
-
|
|
188
|
-
return thread;
|
|
105
|
+
throw new Error(`Assistant with ID "${assistant_id}" not found`);
|
|
189
106
|
};
|
|
190
107
|
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
async (recoveredThread = thread) => {
|
|
201
|
-
thread = recoveredThread;
|
|
202
|
-
for (const message of inMessages) {
|
|
203
|
-
logger.info(message);
|
|
204
|
-
await provider.addMessage({
|
|
205
|
-
threadId: thread.getConversationId(),
|
|
206
|
-
role: role,
|
|
207
|
-
content: message
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// Save system message to database for frontend visibility
|
|
211
|
-
// Skip if message is already saved (e.g., from getConversationReplyController)
|
|
212
|
-
if (!skipSystemMessage) {
|
|
213
|
-
try {
|
|
214
|
-
const message_id = `system_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
215
|
-
await insertMessage({
|
|
216
|
-
nombre_whatsapp: 'System',
|
|
217
|
-
numero: code,
|
|
218
|
-
body: message,
|
|
219
|
-
timestamp: new Date(),
|
|
220
|
-
message_id: message_id,
|
|
221
|
-
is_group: false,
|
|
222
|
-
is_media: false,
|
|
223
|
-
from_me: true,
|
|
224
|
-
processed: true,
|
|
225
|
-
origin: 'system',
|
|
226
|
-
thread_id: thread.getConversationId(),
|
|
227
|
-
assistant_id: thread.getAssistantId(),
|
|
228
|
-
raw: { role: role }
|
|
229
|
-
});
|
|
230
|
-
} catch (err) {
|
|
231
|
-
// Don't throw - we don't want to break the flow if logging fails
|
|
232
|
-
logger.error('[addMsgAssistant] Error saving system message:', err);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
thread,
|
|
238
|
-
process.env.VARIANT || 'assistants'
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
if (!reply) return null;
|
|
242
|
-
|
|
243
|
-
let output, completed;
|
|
244
|
-
let retries = 0;
|
|
245
|
-
const maxRetries = DEFAULT_MAX_RETRIES;
|
|
246
|
-
const assistant = getAssistantById(thread.getAssistantId(), thread);
|
|
247
|
-
do {
|
|
248
|
-
({ output, completed } = await runAssistantAndWait({ thread, assistant }));
|
|
249
|
-
logger.info(`Attempt ${retries + 1}: completed=${completed}, output=${output || '(empty)'}`);
|
|
250
|
-
|
|
251
|
-
if (completed && output) break;
|
|
252
|
-
if (retries < maxRetries) await new Promise(resolve => setTimeout(resolve, 2000));
|
|
253
|
-
retries++;
|
|
254
|
-
} while (retries <= maxRetries && (!completed || !output));
|
|
255
|
-
|
|
256
|
-
logger.info('THE ANS IS', output);
|
|
257
|
-
return output;
|
|
258
|
-
} catch (error) {
|
|
259
|
-
logger.info(error);
|
|
260
|
-
return null;
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const addInstructionCore = async (code, instruction, role = 'user') => {
|
|
265
|
-
const thread = await withTracing(getThread, 'get_thread_operation',
|
|
266
|
-
(threadCode) => ({
|
|
267
|
-
'thread.code': threadCode,
|
|
268
|
-
'operation.type': 'thread_retrieval'
|
|
269
|
-
})
|
|
270
|
-
)(code);
|
|
271
|
-
if (thread === null) return null;
|
|
272
|
-
|
|
273
|
-
const assistant = getAssistantById(thread.getAssistantId(), thread);
|
|
274
|
-
const { output, completed, retries } = await withTracing(
|
|
275
|
-
runAssistantWithRetries,
|
|
276
|
-
'run_assistant_with_retries',
|
|
277
|
-
(thread, assistant, runConfig, patientReply) => ({
|
|
278
|
-
'assistant.id': thread.getAssistantId(),
|
|
279
|
-
'assistant.max_retries': DEFAULT_MAX_RETRIES,
|
|
280
|
-
'assistant.has_patient_reply': !!patientReply
|
|
281
|
-
})
|
|
282
|
-
)(
|
|
283
|
-
thread,
|
|
284
|
-
assistant,
|
|
285
|
-
{
|
|
286
|
-
additionalInstructions: instruction,
|
|
287
|
-
additionalMessages: [
|
|
288
|
-
{ role: role, content: instruction }
|
|
289
|
-
]
|
|
290
|
-
},
|
|
291
|
-
null // no patientReply for instructions
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
// Save instruction to database for frontend visibility
|
|
295
|
-
try {
|
|
296
|
-
const message_id = `instruction_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
297
|
-
await insertMessage({
|
|
298
|
-
nombre_whatsapp: 'Instruction',
|
|
299
|
-
numero: code,
|
|
300
|
-
body: instruction,
|
|
301
|
-
timestamp: new Date(),
|
|
302
|
-
message_id: message_id,
|
|
303
|
-
is_group: false,
|
|
304
|
-
is_media: false,
|
|
305
|
-
from_me: true,
|
|
306
|
-
processed: true,
|
|
307
|
-
origin: 'instruction',
|
|
308
|
-
thread_id: thread.getConversationId(),
|
|
309
|
-
assistant_id: thread.getAssistantId(),
|
|
310
|
-
raw: { role: role }
|
|
311
|
-
});
|
|
312
|
-
} catch (err) {
|
|
313
|
-
// Don't throw - we don't want to break the flow if logging fails
|
|
314
|
-
logger.error('[addInstructionCore] Error saving instruction message:', err);
|
|
315
|
-
}
|
|
108
|
+
const createAssistant = withTracing(
|
|
109
|
+
(code, assistant_id) => createAssistantCore(code, assistant_id, getAssistantById),
|
|
110
|
+
'create_assistant',
|
|
111
|
+
(code, assistant_id) => ({
|
|
112
|
+
'assistant.thread_code': code,
|
|
113
|
+
'assistant.id': assistant_id,
|
|
114
|
+
'operation.type': 'create_assistant'
|
|
115
|
+
})
|
|
116
|
+
);
|
|
316
117
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
118
|
+
const addMsgAssistant = withTracing(
|
|
119
|
+
addMsgAssistantCore,
|
|
120
|
+
'add_message_assistant',
|
|
121
|
+
(code, message, role) => ({
|
|
122
|
+
'message.thread_code': code,
|
|
123
|
+
'message.content_length': message?.length || 0,
|
|
124
|
+
'message.role': role,
|
|
125
|
+
'operation.type': 'add_message'
|
|
126
|
+
})
|
|
127
|
+
);
|
|
320
128
|
|
|
321
129
|
const addInsAssistant = withTracing(
|
|
322
130
|
addInstructionCore,
|
|
@@ -329,226 +137,28 @@ const addInsAssistant = withTracing(
|
|
|
329
137
|
})
|
|
330
138
|
);
|
|
331
139
|
|
|
332
|
-
const startTypingIndicator = async (provider, code) => {
|
|
333
|
-
try {
|
|
334
|
-
const lastMessage = await Message.findOne({
|
|
335
|
-
numero: code,
|
|
336
|
-
from_me: false,
|
|
337
|
-
message_id: { $exists: true, $ne: null }
|
|
338
|
-
}).sort({ createdAt: -1 });
|
|
339
|
-
|
|
340
|
-
if (!lastMessage?.message_id) return null;
|
|
341
|
-
|
|
342
|
-
return setInterval(() =>
|
|
343
|
-
provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
|
|
344
|
-
logger.debug('[startTypingIndicator] Interval failed', { error: err.message })
|
|
345
|
-
), 25000
|
|
346
|
-
);
|
|
347
|
-
} catch (err) {
|
|
348
|
-
logger.debug('[startTypingIndicator] Failed to start', { error: err.message });
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
};
|
|
352
|
-
|
|
353
|
-
const replyAssistantCore = async (code, message_ = null, thread_ = null, runOptions = {}) => {
|
|
354
|
-
const timings = {};
|
|
355
|
-
const startTotal = Date.now();
|
|
356
|
-
|
|
357
|
-
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
358
|
-
const typingInterval = await startTypingIndicator(provider, code);
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
const { result: thread, duration: getThreadMs } = await withTracing(
|
|
362
|
-
getThread,
|
|
363
|
-
'get_thread_operation',
|
|
364
|
-
(threadCode) => ({
|
|
365
|
-
'thread.code': threadCode,
|
|
366
|
-
'operation.type': 'thread_retrieval',
|
|
367
|
-
'thread.provided': !!thread_
|
|
368
|
-
}),
|
|
369
|
-
{ returnTiming: true }
|
|
370
|
-
)(code);
|
|
371
|
-
timings.get_thread_ms = getThreadMs;
|
|
372
|
-
|
|
373
|
-
if (!thread_ && !thread) return null;
|
|
374
|
-
const finalThread = thread_ || thread;
|
|
375
|
-
|
|
376
|
-
const { result: patientReply, duration: getMessagesMs } = await withTracing(
|
|
377
|
-
getLastMessages,
|
|
378
|
-
'get_last_messages',
|
|
379
|
-
(code) => ({ 'thread.code': code }),
|
|
380
|
-
{ returnTiming: true }
|
|
381
|
-
)(code);
|
|
382
|
-
timings.get_messages_ms = getMessagesMs;
|
|
383
|
-
|
|
384
|
-
if (!patientReply) {
|
|
385
|
-
logger.info('[replyAssistantCore] No relevant data found for this assistant.');
|
|
386
|
-
return null;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
logger.info(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
|
|
390
|
-
const { result: processResult, duration: processMessagesMs } = await withTracing(
|
|
391
|
-
processThreadMessage,
|
|
392
|
-
'process_thread_messages',
|
|
393
|
-
(code, patientReply, provider) => ({
|
|
394
|
-
'messages.count': patientReply.length,
|
|
395
|
-
'thread.code': code
|
|
396
|
-
}),
|
|
397
|
-
{ returnTiming: true }
|
|
398
|
-
)(code, patientReply, provider);
|
|
399
|
-
|
|
400
|
-
const { results: processResults, timings: processTimings } = processResult;
|
|
401
|
-
timings.process_messages_ms = processMessagesMs;
|
|
402
|
-
|
|
403
|
-
logger.debug('[replyAssistantCore] Process timings breakdown', { processTimings });
|
|
404
|
-
|
|
405
|
-
if (processTimings) {
|
|
406
|
-
timings.process_messages_breakdown = {
|
|
407
|
-
download_ms: processTimings.download_ms || 0,
|
|
408
|
-
image_analysis_ms: processTimings.image_analysis_ms || 0,
|
|
409
|
-
audio_transcription_ms: processTimings.audio_transcription_ms || 0,
|
|
410
|
-
url_generation_ms: processTimings.url_generation_ms || 0,
|
|
411
|
-
total_media_ms: processTimings.total_media_ms || 0
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const patientMsg = processResults.some(r => r.isPatient);
|
|
416
|
-
const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
|
|
417
|
-
const allMessagesToAdd = processResults.flatMap(r => r.messages || []);
|
|
418
|
-
const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
|
|
419
|
-
|
|
420
|
-
if (allMessagesToAdd.length > 0) {
|
|
421
|
-
logger.info(`[replyAssistantCore] Adding ${allMessagesToAdd.length} messages to thread in batch`);
|
|
422
|
-
await withThreadRecovery(
|
|
423
|
-
async (thread = finalThread) => {
|
|
424
|
-
const threadId = thread.getConversationId();
|
|
425
|
-
await provider.addMessage({ threadId, messages: allMessagesToAdd });
|
|
426
|
-
},
|
|
427
|
-
finalThread,
|
|
428
|
-
process.env.VARIANT || 'assistants'
|
|
429
|
-
);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
await Promise.all(processResults.map(r => updateMessageRecord(r.reply, finalThread)));
|
|
433
|
-
await cleanupFiles(allTempFiles);
|
|
434
|
-
|
|
435
|
-
if (urls.length > 0) {
|
|
436
|
-
logger.info(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
|
|
437
|
-
const { result: pdfResult, duration: pdfCombinationMs } = await withTracing(
|
|
438
|
-
combineImagesToPDF,
|
|
439
|
-
'combine_images_to_pdf',
|
|
440
|
-
({ code }) => ({
|
|
441
|
-
'pdf.thread_code': code,
|
|
442
|
-
'pdf.url_count': urls.length
|
|
443
|
-
}),
|
|
444
|
-
{ returnTiming: true }
|
|
445
|
-
)({ code });
|
|
446
|
-
timings.pdf_combination_ms = pdfCombinationMs;
|
|
447
|
-
const { pdfBuffer, processedFiles } = pdfResult;
|
|
448
|
-
logger.info(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
|
|
449
|
-
|
|
450
|
-
if (pdfBuffer) {
|
|
451
|
-
const key = `${code}-${Date.now()}-combined.pdf`;
|
|
452
|
-
const bucket = runtimeConfig.get('AWS_S3_BUCKET_NAME');
|
|
453
|
-
if (bucket) {
|
|
454
|
-
await AWS.uploadBufferToS3(pdfBuffer, bucket, key, 'application/pdf');
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (processedFiles && processedFiles.length) {
|
|
459
|
-
cleanupFiles(processedFiles);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
if (!patientMsg || finalThread.stopped) return null;
|
|
464
|
-
|
|
465
|
-
const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
|
|
466
|
-
const { result: runResult, duration: runAssistantMs } = await withTracing(
|
|
467
|
-
runAssistantWithRetries,
|
|
468
|
-
'run_assistant_with_retries',
|
|
469
|
-
(thread, assistant, runConfig, patientReply) => ({
|
|
470
|
-
'assistant.id': thread.getAssistantId(),
|
|
471
|
-
'assistant.max_retries': DEFAULT_MAX_RETRIES,
|
|
472
|
-
'assistant.has_patient_reply': !!patientReply
|
|
473
|
-
}),
|
|
474
|
-
{ returnTiming: true }
|
|
475
|
-
)(finalThread, assistant, runOptions, patientReply);
|
|
476
|
-
timings.run_assistant_ms = runAssistantMs;
|
|
477
|
-
timings.total_ms = Date.now() - startTotal;
|
|
478
|
-
|
|
479
|
-
const { run, output, completed, retries, predictionTimeMs, tools_executed } = runResult;
|
|
480
|
-
|
|
481
|
-
logger.info('[Assistant Reply Complete]', {
|
|
482
|
-
code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
|
|
483
|
-
messageCount: patientReply.length,
|
|
484
|
-
hasMedia: urls.length > 0,
|
|
485
|
-
retries,
|
|
486
|
-
totalMs: timings.total_ms,
|
|
487
|
-
toolsExecuted: tools_executed?.length || 0
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
if (output && predictionTimeMs) {
|
|
491
|
-
logger.debug('[replyAssistantCore] Storing metrics with timing_breakdown', {
|
|
492
|
-
timing_breakdown: timings,
|
|
493
|
-
has_breakdown: !!timings.process_messages_breakdown
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
await PredictionMetrics.create({
|
|
497
|
-
message_id: `${code}-${Date.now()}`,
|
|
498
|
-
numero: code,
|
|
499
|
-
assistant_id: finalThread.getAssistantId(),
|
|
500
|
-
thread_id: finalThread.getConversationId(),
|
|
501
|
-
prediction_time_ms: predictionTimeMs,
|
|
502
|
-
retry_count: retries,
|
|
503
|
-
completed: completed,
|
|
504
|
-
timing_breakdown: timings
|
|
505
|
-
}).catch(err => logger.error('[replyAssistantCore] Failed to store metrics:', err));
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return { output, tools_executed };
|
|
509
|
-
} finally {
|
|
510
|
-
if (typingInterval) {
|
|
511
|
-
clearInterval(typingInterval);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
};
|
|
515
|
-
|
|
516
140
|
const replyAssistant = withTracing(
|
|
517
|
-
replyAssistantCore,
|
|
141
|
+
(code, message_, thread_, runOptions) => replyAssistantCore(code, message_, thread_, runOptions, getAssistantById),
|
|
518
142
|
'assistant_reply',
|
|
519
143
|
(code, message_, thread_, runOptions) => ({
|
|
520
144
|
'assistant.thread_code': code,
|
|
521
145
|
'assistant.has_message': !!message_,
|
|
522
146
|
'assistant.has_custom_thread': !!thread_,
|
|
523
|
-
'assistant.
|
|
147
|
+
'assistant.has_run_options': !!runOptions && Object.keys(runOptions).length > 0
|
|
524
148
|
})
|
|
525
149
|
);
|
|
526
150
|
|
|
527
|
-
const switchAssistant =
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (variant === 'responses') {
|
|
537
|
-
updateFields.prompt_id = assistant_id;
|
|
538
|
-
} else {
|
|
539
|
-
updateFields.assistant_id = assistant_id;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
await Thread.updateOne({ code }, { $set: updateFields });
|
|
543
|
-
} catch (error) {
|
|
544
|
-
logger.info(error);
|
|
545
|
-
return null;
|
|
546
|
-
}
|
|
547
|
-
};
|
|
151
|
+
const switchAssistant = withTracing(
|
|
152
|
+
switchAssistantCore,
|
|
153
|
+
'switch_assistant',
|
|
154
|
+
(code, assistant_id) => ({
|
|
155
|
+
'assistant.thread_code': code,
|
|
156
|
+
'assistant.new_id': assistant_id,
|
|
157
|
+
'operation.type': 'switch_assistant'
|
|
158
|
+
})
|
|
159
|
+
);
|
|
548
160
|
|
|
549
161
|
module.exports = {
|
|
550
|
-
getThread,
|
|
551
|
-
getThreadInfo,
|
|
552
162
|
getAssistantById,
|
|
553
163
|
createAssistant,
|
|
554
164
|
replyAssistant,
|
|
@@ -557,6 +167,5 @@ module.exports = {
|
|
|
557
167
|
switchAssistant,
|
|
558
168
|
configureAssistants,
|
|
559
169
|
registerAssistant,
|
|
560
|
-
overrideGetAssistantById
|
|
561
|
-
runAssistantAndWait
|
|
170
|
+
overrideGetAssistantById
|
|
562
171
|
};
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
const AWS = require('../config/awsConfig.js');
|
|
2
|
+
const runtimeConfig = require('../config/runtimeConfig');
|
|
3
|
+
const { createProvider } = require('../providers/createProvider');
|
|
4
|
+
|
|
5
|
+
const { Thread } = require('../models/threadModel.js');
|
|
6
|
+
const { PredictionMetrics } = require('../models/predictionMetricsModel');
|
|
7
|
+
const { insertMessage } = require('../models/messageModel');
|
|
8
|
+
|
|
9
|
+
const { getCurRow, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
|
|
10
|
+
const { getThread } = require('../helpers/threadHelper.js');
|
|
11
|
+
const { processThreadMessage } = require('../helpers/processHelper.js');
|
|
12
|
+
const { getLastMessages, updateMessageRecord } = require('../helpers/messageHelper.js');
|
|
13
|
+
const { withThreadRecovery } = require('../helpers/threadRecoveryHelper.js');
|
|
14
|
+
const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
|
|
15
|
+
const { logger } = require('../utils/logger');
|
|
16
|
+
|
|
17
|
+
const createAssistantCore = async (code, assistant_id, getAssistantById) => {
|
|
18
|
+
const thread = await getThread(code);
|
|
19
|
+
if (!thread) return null;
|
|
20
|
+
|
|
21
|
+
const assistant = getAssistantById(assistant_id, thread);
|
|
22
|
+
const curRow = await getCurRow(code);
|
|
23
|
+
const context = { curRow };
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await assistant.create(code, context);
|
|
27
|
+
return { success: true, assistant_id };
|
|
28
|
+
} catch (error) {
|
|
29
|
+
logger.error('[createAssistantCore] Error:', error);
|
|
30
|
+
return { success: false, error: error.message };
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const addMsgAssistantCore = async (code, message, role = 'user') => {
|
|
35
|
+
const thread = await getThread(code);
|
|
36
|
+
if (!thread) return null;
|
|
37
|
+
|
|
38
|
+
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
39
|
+
const threadId = thread.getConversationId();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await provider.addMessage({ threadId, messages: [{ role, content: message }] });
|
|
43
|
+
await insertMessage({ code, message, role });
|
|
44
|
+
return { success: true };
|
|
45
|
+
} catch (error) {
|
|
46
|
+
logger.error('[addMsgAssistantCore] Error:', error);
|
|
47
|
+
return { success: false, error: error.message };
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const addInstructionCore = async (code, instruction, role = 'user') => {
|
|
52
|
+
const thread = await getThread(code);
|
|
53
|
+
if (!thread) return null;
|
|
54
|
+
|
|
55
|
+
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
56
|
+
const threadId = thread.getConversationId();
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await provider.addMessage({ threadId, messages: [{ role, content: instruction }] });
|
|
60
|
+
return { success: true };
|
|
61
|
+
} catch (error) {
|
|
62
|
+
logger.error('[addInstructionCore] Error:', error);
|
|
63
|
+
return { success: false, error: error.message };
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const replyAssistantCore = async (code, message_ = null, thread_ = null, runOptions = {}, getAssistantById) => {
|
|
68
|
+
const timings = {};
|
|
69
|
+
const startTotal = Date.now();
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const thread = thread_ || await getThread(code);
|
|
73
|
+
timings.get_thread_ms = 0;
|
|
74
|
+
|
|
75
|
+
if (!thread) return null;
|
|
76
|
+
const finalThread = thread;
|
|
77
|
+
|
|
78
|
+
const patientReply = await getLastMessages(code);
|
|
79
|
+
timings.get_messages_ms = 0;
|
|
80
|
+
|
|
81
|
+
if (!patientReply) {
|
|
82
|
+
logger.info('[replyAssistantCore] No relevant data found for this assistant.');
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
|
|
87
|
+
logger.info(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
|
|
88
|
+
const processResult = await processThreadMessage(code, patientReply, provider);
|
|
89
|
+
|
|
90
|
+
const { results: processResults, timings: processTimings } = processResult;
|
|
91
|
+
timings.process_messages_ms = 0;
|
|
92
|
+
|
|
93
|
+
logger.debug('[replyAssistantCore] Process timings breakdown', { processTimings });
|
|
94
|
+
|
|
95
|
+
if (processTimings) {
|
|
96
|
+
timings.process_messages_breakdown = {
|
|
97
|
+
download_ms: processTimings.download_ms || 0,
|
|
98
|
+
image_analysis_ms: processTimings.image_analysis_ms || 0,
|
|
99
|
+
audio_transcription_ms: processTimings.audio_transcription_ms || 0,
|
|
100
|
+
url_generation_ms: processTimings.url_generation_ms || 0,
|
|
101
|
+
total_media_ms: processTimings.total_media_ms || 0
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const patientMsg = processResults.some(r => r.isPatient);
|
|
106
|
+
const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
|
|
107
|
+
const allMessagesToAdd = processResults.flatMap(r => r.messages || []);
|
|
108
|
+
const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
|
|
109
|
+
|
|
110
|
+
if (allMessagesToAdd.length > 0) {
|
|
111
|
+
logger.info(`[replyAssistantCore] Adding ${allMessagesToAdd.length} messages to thread in batch`);
|
|
112
|
+
await withThreadRecovery(
|
|
113
|
+
async (thread = finalThread) => {
|
|
114
|
+
const threadId = thread.getConversationId();
|
|
115
|
+
await provider.addMessage({ threadId, messages: allMessagesToAdd });
|
|
116
|
+
},
|
|
117
|
+
finalThread,
|
|
118
|
+
process.env.VARIANT || 'assistants'
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await Promise.all(processResults.map(r => updateMessageRecord(r.reply, finalThread)));
|
|
123
|
+
await cleanupFiles(allTempFiles);
|
|
124
|
+
|
|
125
|
+
if (urls.length > 0) {
|
|
126
|
+
logger.info(`[replyAssistantCore] Processing ${urls.length} URLs for PDF combination`);
|
|
127
|
+
const pdfResult = await combineImagesToPDF({ code });
|
|
128
|
+
timings.pdf_combination_ms = 0;
|
|
129
|
+
const { pdfBuffer, processedFiles } = pdfResult;
|
|
130
|
+
logger.info(`[replyAssistantCore] PDF combination complete: ${processedFiles?.length || 0} files processed`);
|
|
131
|
+
|
|
132
|
+
if (pdfBuffer) {
|
|
133
|
+
const key = `${code}-${Date.now()}-combined.pdf`;
|
|
134
|
+
const bucket = runtimeConfig.get('AWS_S3_BUCKET_NAME');
|
|
135
|
+
if (bucket) {
|
|
136
|
+
await AWS.uploadBufferToS3(pdfBuffer, bucket, key, 'application/pdf');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (processedFiles && processedFiles.length) {
|
|
141
|
+
cleanupFiles(processedFiles);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!patientMsg || finalThread.stopped) return null;
|
|
146
|
+
|
|
147
|
+
const assistant = getAssistantById(finalThread.getAssistantId(), finalThread);
|
|
148
|
+
const runResult = await runAssistantWithRetries(finalThread, assistant, runOptions, patientReply);
|
|
149
|
+
timings.run_assistant_ms = 0;
|
|
150
|
+
timings.total_ms = Date.now() - startTotal;
|
|
151
|
+
|
|
152
|
+
const { run, output, completed, retries, predictionTimeMs, tools_executed } = runResult;
|
|
153
|
+
|
|
154
|
+
logger.info('[Assistant Reply Complete]', {
|
|
155
|
+
code: code ? `${code.substring(0, 3)}***${code.slice(-4)}` : 'unknown',
|
|
156
|
+
messageCount: patientReply.length,
|
|
157
|
+
hasMedia: urls.length > 0,
|
|
158
|
+
retries,
|
|
159
|
+
totalMs: timings.total_ms,
|
|
160
|
+
toolsExecuted: tools_executed?.length || 0
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (output && predictionTimeMs) {
|
|
164
|
+
logger.debug('[replyAssistantCore] Storing metrics with timing_breakdown', {
|
|
165
|
+
timing_breakdown: timings,
|
|
166
|
+
has_breakdown: !!timings.process_messages_breakdown
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await PredictionMetrics.create({
|
|
170
|
+
message_id: `${code}-${Date.now()}`,
|
|
171
|
+
numero: code,
|
|
172
|
+
assistant_id: finalThread.getAssistantId(),
|
|
173
|
+
thread_id: finalThread.getConversationId(),
|
|
174
|
+
prediction_time_ms: predictionTimeMs,
|
|
175
|
+
retry_count: retries,
|
|
176
|
+
completed: completed,
|
|
177
|
+
timing_breakdown: timings
|
|
178
|
+
}).catch(err => logger.error('[replyAssistantCore] Failed to store metrics:', err));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { output, tools_executed };
|
|
182
|
+
} catch (error) {
|
|
183
|
+
logger.error('[replyAssistantCore] Error:', { error: error.message });
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const switchAssistantCore = async (code, assistant_id) => {
|
|
189
|
+
try {
|
|
190
|
+
const thread = await Thread.findOne({ code });
|
|
191
|
+
if (!thread) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const updateFields = {
|
|
196
|
+
assistant_id,
|
|
197
|
+
stopped: false,
|
|
198
|
+
updatedAt: new Date()
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
await Thread.updateOne({ code }, { $set: updateFields });
|
|
202
|
+
return { success: true, assistant_id };
|
|
203
|
+
} catch (error) {
|
|
204
|
+
logger.info(error);
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
createAssistantCore,
|
|
211
|
+
addMsgAssistantCore,
|
|
212
|
+
addInstructionCore,
|
|
213
|
+
replyAssistantCore,
|
|
214
|
+
switchAssistantCore
|
|
215
|
+
};
|