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