@peopl-health/nexus 2.5.1 → 2.5.2-fix
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
|
|
|
@@ -372,28 +373,11 @@ class NexusMessaging {
|
|
|
372
373
|
} else if (messageData.flow) {
|
|
373
374
|
return await this.handleFlow(messageData);
|
|
374
375
|
} 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
376
|
// For regular messages and media, use batching if enabled
|
|
392
377
|
logger.info('Batching config:', this.batchingConfig);
|
|
393
378
|
if (this.batchingConfig.enabled && chatId) {
|
|
394
379
|
return await this._handleWithBatching(messageData, chatId);
|
|
395
380
|
} else {
|
|
396
|
-
// Handle media and regular messages without batching
|
|
397
381
|
if (messageData.media) {
|
|
398
382
|
return await this.handleMedia(messageData);
|
|
399
383
|
} else {
|
|
@@ -650,14 +634,24 @@ class NexusMessaging {
|
|
|
650
634
|
* Handle message with batching - waits for additional messages before processing
|
|
651
635
|
*/
|
|
652
636
|
async _handleWithBatching(messageData, chatId) {
|
|
653
|
-
|
|
654
|
-
|
|
637
|
+
const existing = this.pendingResponses.get(chatId);
|
|
638
|
+
if (existing) {
|
|
639
|
+
clearTimeout(existing.timeoutId);
|
|
640
|
+
if (existing.typingInterval) {
|
|
641
|
+
clearInterval(existing.typingInterval);
|
|
642
|
+
}
|
|
655
643
|
logger.info(`Received additional message from ${chatId}, resetting wait timer`);
|
|
656
644
|
}
|
|
657
645
|
|
|
646
|
+
// Start typing indicator refresh for batching period
|
|
647
|
+
const typingInterval = await this._startTypingRefresh(chatId);
|
|
648
|
+
|
|
658
649
|
const waitTime = this.batchingConfig.baseWaitTime;
|
|
659
650
|
const timeoutId = setTimeout(async () => {
|
|
660
651
|
try {
|
|
652
|
+
if (typingInterval) {
|
|
653
|
+
clearInterval(typingInterval);
|
|
654
|
+
}
|
|
661
655
|
this.pendingResponses.delete(chatId);
|
|
662
656
|
await this._handleBatchedMessages(chatId);
|
|
663
657
|
} catch (error) {
|
|
@@ -665,17 +659,44 @@ class NexusMessaging {
|
|
|
665
659
|
}
|
|
666
660
|
}, waitTime);
|
|
667
661
|
|
|
668
|
-
this.pendingResponses.set(chatId, timeoutId);
|
|
662
|
+
this.pendingResponses.set(chatId, { timeoutId, typingInterval });
|
|
669
663
|
logger.info(`Waiting ${Math.round(waitTime/1000)} seconds for more messages from ${chatId}`);
|
|
670
664
|
}
|
|
671
665
|
|
|
666
|
+
/**
|
|
667
|
+
* Start typing indicator refresh interval
|
|
668
|
+
*/
|
|
669
|
+
async _startTypingRefresh(chatId) {
|
|
670
|
+
if (!this.provider || typeof this.provider.sendTypingIndicator !== 'function') {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const lastMessage = await Message.findOne({
|
|
675
|
+
numero: chatId,
|
|
676
|
+
from_me: false,
|
|
677
|
+
message_id: { $exists: true, $ne: null }
|
|
678
|
+
}).sort({ createdAt: -1 });
|
|
679
|
+
|
|
680
|
+
if (!lastMessage?.message_id) return null;
|
|
681
|
+
|
|
682
|
+
return setInterval(() =>
|
|
683
|
+
this.provider.sendTypingIndicator(lastMessage.message_id).catch(err =>
|
|
684
|
+
logger.debug('[_startTypingRefresh] Failed', { error: err.message })
|
|
685
|
+
), 5000
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
|
|
672
689
|
/**
|
|
673
690
|
* Process all batched messages for a chat
|
|
674
691
|
*/
|
|
675
692
|
async _handleBatchedMessages(chatId) {
|
|
693
|
+
let typingInterval = null;
|
|
694
|
+
|
|
676
695
|
try {
|
|
677
696
|
logger.info(`Processing batched messages from ${chatId} (including media if any)`);
|
|
678
697
|
|
|
698
|
+
typingInterval = await this._startTypingRefresh(chatId);
|
|
699
|
+
|
|
679
700
|
// Get assistant response
|
|
680
701
|
const result = await replyAssistant(chatId);
|
|
681
702
|
const botResponse = typeof result === 'string' ? result : result?.output;
|
|
@@ -696,6 +717,10 @@ class NexusMessaging {
|
|
|
696
717
|
|
|
697
718
|
} catch (error) {
|
|
698
719
|
logger.error('Error in batched message handling:', { error: error.message });
|
|
720
|
+
} finally {
|
|
721
|
+
if (typingInterval) {
|
|
722
|
+
clearInterval(typingInterval);
|
|
723
|
+
}
|
|
699
724
|
}
|
|
700
725
|
}
|
|
701
726
|
|
|
@@ -703,8 +728,14 @@ class NexusMessaging {
|
|
|
703
728
|
* Clear pending response for a chat (useful for cleanup)
|
|
704
729
|
*/
|
|
705
730
|
clearPendingResponse(chatId) {
|
|
706
|
-
|
|
707
|
-
|
|
731
|
+
const pending = this.pendingResponses.get(chatId);
|
|
732
|
+
if (pending) {
|
|
733
|
+
if (pending.timeoutId) {
|
|
734
|
+
clearTimeout(pending.timeoutId);
|
|
735
|
+
}
|
|
736
|
+
if (pending.typingInterval) {
|
|
737
|
+
clearInterval(pending.typingInterval);
|
|
738
|
+
}
|
|
708
739
|
this.pendingResponses.delete(chatId);
|
|
709
740
|
}
|
|
710
741
|
}
|
|
@@ -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
|
+
};
|