@peopl-health/nexus 2.4.5 → 2.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/helpers/llmsHelper.js +20 -6
- package/lib/helpers/processHelper.js +61 -40
- package/lib/helpers/twilioMediaProcessor.js +31 -11
- package/lib/providers/OpenAIAssistantsProvider.js +21 -15
- package/lib/providers/OpenAIResponsesProvider.js +17 -14
- package/lib/services/airtableService.js +33 -1
- package/lib/services/assistantService.js +19 -14
- package/package.json +1 -1
|
@@ -3,12 +3,13 @@ const fs = require('fs');
|
|
|
3
3
|
const mime = require('mime-types');
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
async function analyzeImage(imagePath) {
|
|
6
|
+
async function analyzeImage(imagePath, isSticker = false) {
|
|
7
7
|
try {
|
|
8
8
|
const anthropicClient = llmConfig.anthropicClient;
|
|
9
9
|
if (!anthropicClient || !anthropicClient.messages) {
|
|
10
10
|
console.warn('[llmsHelper] Anthropics client not configured; skipping image analysis');
|
|
11
11
|
return {
|
|
12
|
+
description: 'Image could not be analyzed - Anthropic client not configured',
|
|
12
13
|
medical_analysis: 'QUALITY_INSUFFICIENT',
|
|
13
14
|
medical_relevance: false,
|
|
14
15
|
has_table: false,
|
|
@@ -16,10 +17,11 @@ async function analyzeImage(imagePath) {
|
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
// Skip WBMP images
|
|
20
|
-
if (imagePath.toLowerCase().includes('.wbmp')
|
|
21
|
-
console.log('Skipping WBMP image
|
|
20
|
+
// Skip only WBMP images (unsupported format)
|
|
21
|
+
if (imagePath.toLowerCase().includes('.wbmp')) {
|
|
22
|
+
console.log('Skipping WBMP image analysis:', imagePath);
|
|
22
23
|
return {
|
|
24
|
+
description: 'Unsupported image format',
|
|
23
25
|
medical_analysis: 'NOT_MEDICAL',
|
|
24
26
|
medical_relevance: false,
|
|
25
27
|
has_table: false,
|
|
@@ -29,9 +31,10 @@ async function analyzeImage(imagePath) {
|
|
|
29
31
|
|
|
30
32
|
// Check MIME type
|
|
31
33
|
const mimeType = mime.lookup(imagePath) || 'image/jpeg';
|
|
32
|
-
if (mimeType === 'image/vnd.wap.wbmp'
|
|
34
|
+
if (mimeType === 'image/vnd.wap.wbmp') {
|
|
33
35
|
console.log('Skipping image with MIME type:', mimeType);
|
|
34
36
|
return {
|
|
37
|
+
description: 'Unsupported image format',
|
|
35
38
|
medical_analysis: 'NOT_MEDICAL',
|
|
36
39
|
medical_relevance: false,
|
|
37
40
|
has_table: false,
|
|
@@ -42,7 +45,7 @@ async function analyzeImage(imagePath) {
|
|
|
42
45
|
const imageBuffer = await fs.promises.readFile(imagePath);
|
|
43
46
|
const base64Image = imageBuffer.toString('base64');
|
|
44
47
|
|
|
45
|
-
//
|
|
48
|
+
// Description of the image (for both stickers and regular images)
|
|
46
49
|
const imageDescription = 'Describe the image in detail.';
|
|
47
50
|
const messageDescription = await anthropicClient.messages.create({
|
|
48
51
|
model: 'claude-sonnet-4-5',
|
|
@@ -69,6 +72,17 @@ async function analyzeImage(imagePath) {
|
|
|
69
72
|
});
|
|
70
73
|
const description = messageDescription.content[0].text;
|
|
71
74
|
|
|
75
|
+
// For stickers, skip medical analysis and table extraction
|
|
76
|
+
if (isSticker) {
|
|
77
|
+
return {
|
|
78
|
+
description: description,
|
|
79
|
+
medical_analysis: 'NOT_MEDICAL',
|
|
80
|
+
medical_relevance: false,
|
|
81
|
+
has_table: false,
|
|
82
|
+
table_data: null
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
72
86
|
// Create a more specific prompt for table detection and extraction
|
|
73
87
|
const tablePrompt = `Please analyze this image and respond in the following format:
|
|
74
88
|
1. First, determine if there is a table in the image.
|
|
@@ -61,13 +61,18 @@ const processImageFile = async (fileName, reply) => {
|
|
|
61
61
|
let url = null;
|
|
62
62
|
const messagesChat = [];
|
|
63
63
|
|
|
64
|
+
const isSticker = reply.media?.mediaType === 'sticker' ||
|
|
65
|
+
fileName.toLowerCase().includes('sticker/') ||
|
|
66
|
+
fileName.toLowerCase().includes('/sticker/');
|
|
67
|
+
|
|
64
68
|
try {
|
|
65
|
-
imageAnalysis = await analyzeImage(fileName);
|
|
69
|
+
imageAnalysis = await analyzeImage(fileName, isSticker);
|
|
66
70
|
|
|
67
71
|
logger.info('processImageFile', {
|
|
68
72
|
message_id: reply.message_id,
|
|
69
73
|
bucketName: reply.media?.bucketName,
|
|
70
74
|
key: reply.media?.key,
|
|
75
|
+
is_sticker: isSticker,
|
|
71
76
|
medical_relevance: imageAnalysis?.medical_relevance,
|
|
72
77
|
has_table: imageAnalysis?.has_table,
|
|
73
78
|
analysis_type: imageAnalysis?.medical_analysis ? 'medical' : 'general'
|
|
@@ -77,18 +82,18 @@ const processImageFile = async (fileName, reply) => {
|
|
|
77
82
|
|
|
78
83
|
const invalidAnalysis = ['NOT_MEDICAL', 'QUALITY_INSUFFICIENT'];
|
|
79
84
|
|
|
80
|
-
// Generate presigned URL if medically relevant
|
|
81
|
-
if (imageAnalysis?.medical_relevance) {
|
|
85
|
+
// Generate presigned URL only if medically relevant AND not a sticker
|
|
86
|
+
if (imageAnalysis?.medical_relevance && !isSticker) {
|
|
82
87
|
url = await generatePresignedUrl(reply.media.bucketName, reply.media.key);
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
// Add appropriate text based on analysis
|
|
86
|
-
if (imageAnalysis?.has_table && imageAnalysis.table_data) {
|
|
91
|
+
if (!isSticker && imageAnalysis?.has_table && imageAnalysis.table_data) {
|
|
87
92
|
messagesChat.push({
|
|
88
93
|
type: 'text',
|
|
89
94
|
text: imageAnalysis.table_data,
|
|
90
95
|
});
|
|
91
|
-
} else if (imageAnalysis?.medical_analysis && !invalidAnalysis.some(tag => imageAnalysis.medical_analysis.includes(tag))) {
|
|
96
|
+
} else if (!isSticker && imageAnalysis?.medical_analysis && !invalidAnalysis.some(tag => imageAnalysis.medical_analysis.includes(tag))) {
|
|
92
97
|
messagesChat.push({
|
|
93
98
|
type: 'text',
|
|
94
99
|
text: imageAnalysis.medical_analysis,
|
|
@@ -102,7 +107,8 @@ const processImageFile = async (fileName, reply) => {
|
|
|
102
107
|
} catch (error) {
|
|
103
108
|
logger.error('processImageFile', error, {
|
|
104
109
|
message_id: reply.message_id,
|
|
105
|
-
fileName: fileName ? fileName.split('/').pop() : 'unknown'
|
|
110
|
+
fileName: fileName ? fileName.split('/').pop() : 'unknown',
|
|
111
|
+
is_sticker: isSticker
|
|
106
112
|
});
|
|
107
113
|
messagesChat.push({
|
|
108
114
|
type: 'text',
|
|
@@ -175,8 +181,8 @@ const processMediaFiles = async (code, reply, provider) => {
|
|
|
175
181
|
fileName: safeFileName
|
|
176
182
|
});
|
|
177
183
|
|
|
178
|
-
// Skip unsupported
|
|
179
|
-
if (fileName.toLowerCase().includes('.wbmp')
|
|
184
|
+
// Skip only WBMP files (unsupported format)
|
|
185
|
+
if (fileName.toLowerCase().includes('.wbmp')) {
|
|
180
186
|
logger.info('processMediaFiles_skip', {
|
|
181
187
|
message_id: reply.message_id,
|
|
182
188
|
fileName: safeFileName,
|
|
@@ -185,7 +191,15 @@ const processMediaFiles = async (code, reply, provider) => {
|
|
|
185
191
|
continue;
|
|
186
192
|
}
|
|
187
193
|
|
|
188
|
-
|
|
194
|
+
// Process images, documents, applications, and stickers as images
|
|
195
|
+
const isImageLike = fileName.includes('image') ||
|
|
196
|
+
fileName.includes('document') ||
|
|
197
|
+
fileName.includes('application') ||
|
|
198
|
+
reply.media?.mediaType === 'sticker' ||
|
|
199
|
+
fileName.toLowerCase().includes('sticker/') ||
|
|
200
|
+
fileName.toLowerCase().includes('/sticker/');
|
|
201
|
+
|
|
202
|
+
if (isImageLike) {
|
|
189
203
|
const { messagesChat: imageMessages, url: imageUrl } = await processImageFile(fileName, reply);
|
|
190
204
|
messagesChat.push(...imageMessages);
|
|
191
205
|
if (imageUrl) url = imageUrl;
|
|
@@ -198,36 +212,43 @@ const processMediaFiles = async (code, reply, provider) => {
|
|
|
198
212
|
return { messagesChat, url, tempFiles };
|
|
199
213
|
};
|
|
200
214
|
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
215
|
+
const processThreadMessage = async (code, replies, provider) => {
|
|
216
|
+
const replyArray = Array.isArray(replies) ? replies : [replies];
|
|
217
|
+
|
|
218
|
+
const results = await Promise.all(
|
|
219
|
+
replyArray.map(async (reply, i) => {
|
|
220
|
+
let tempFiles = [];
|
|
221
|
+
try {
|
|
222
|
+
const isPatient = reply.origin === 'patient';
|
|
223
|
+
const [textMessages, mediaResult] = await Promise.all([
|
|
224
|
+
Promise.resolve(processTextMessage(reply)),
|
|
225
|
+
processMediaFiles(code, reply, provider)
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
const { messagesChat: mediaMessages, url, tempFiles: mediaFiles } = mediaResult;
|
|
229
|
+
tempFiles = mediaFiles;
|
|
230
|
+
|
|
231
|
+
const allMessages = [...textMessages, ...mediaMessages];
|
|
232
|
+
const role = reply.origin === 'patient' ? 'user' : 'assistant';
|
|
233
|
+
const messages = allMessages.map(content => ({ role, content }));
|
|
234
|
+
|
|
235
|
+
logger.info('processThreadMessage', {
|
|
236
|
+
index: i + 1,
|
|
237
|
+
total: replyArray.length,
|
|
238
|
+
isPatient,
|
|
239
|
+
hasUrl: !!url
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return { isPatient, url, messages, reply, tempFiles };
|
|
243
|
+
} catch (error) {
|
|
244
|
+
logger.error('processThreadMessage', error, { message_id: reply.message_id, origin: reply.origin });
|
|
245
|
+
await cleanupFiles(tempFiles);
|
|
246
|
+
return { isPatient: false, url: null, messages: [], reply, tempFiles: [] };
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
return results;
|
|
231
252
|
};
|
|
232
253
|
|
|
233
254
|
module.exports = {
|
|
@@ -235,5 +256,5 @@ module.exports = {
|
|
|
235
256
|
processImageFile,
|
|
236
257
|
processAudioFile,
|
|
237
258
|
processMediaFiles,
|
|
238
|
-
|
|
259
|
+
processThreadMessage
|
|
239
260
|
};
|
|
@@ -7,7 +7,7 @@ const {
|
|
|
7
7
|
} = require('./twilioHelper');
|
|
8
8
|
const { validateMedia } = require('../utils/mediaValidator');
|
|
9
9
|
const { generatePresignedUrl } = require('../config/awsConfig');
|
|
10
|
-
const {
|
|
10
|
+
const { addLinkedRecord } = require('../services/airtableService');
|
|
11
11
|
const { Monitoreo_ID } = require('../config/airtableConfig');
|
|
12
12
|
|
|
13
13
|
const ensureLogger = (logger) => {
|
|
@@ -24,6 +24,34 @@ const normalizeMediaType = (type) => {
|
|
|
24
24
|
return type.replace(/Message$/i, '').replace(/WithCaption$/i, '').toLowerCase();
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
async function uploadMediaToAirtable(whatsappId, mediaUrl, log, baseID, tableName) {
|
|
28
|
+
if (!baseID) {
|
|
29
|
+
log.debug && log.debug('[uploadMediaToAirtable] Base ID not configured; skipping Airtable upload');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await addLinkedRecord(
|
|
35
|
+
baseID,
|
|
36
|
+
tableName,
|
|
37
|
+
{
|
|
38
|
+
estudios: [{ url: mediaUrl }],
|
|
39
|
+
combined_estudios: [{ url: mediaUrl }]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
referenceTable: 'estado_general',
|
|
43
|
+
referenceFilter: `whatsapp_id = "${whatsappId}"`,
|
|
44
|
+
linkFieldName: 'patient_id'
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
log.debug && log.debug('[uploadMediaToAirtable] Successfully uploaded media to estudios table', { whatsappId });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
log.warn && log.warn('[uploadMediaToAirtable] Failed to upload media to estudios table', { whatsappId, error: error?.message || error });
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
27
55
|
async function processTwilioMediaMessage(twilioMessage, logger, bucketName) {
|
|
28
56
|
if (!twilioMessage) return [];
|
|
29
57
|
|
|
@@ -117,16 +145,8 @@ async function processTwilioMediaMessage(twilioMessage, logger, bucketName) {
|
|
|
117
145
|
const url = await generatePresignedUrl(bucketName, key);
|
|
118
146
|
item.metadata.presignedUrl = url;
|
|
119
147
|
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
const patientId = Array.isArray(patient) && patient.length > 0 ? [patient[0].recordID || patient[0].recordId || patient[0].record_id] : [];
|
|
123
|
-
await addRecord(Monitoreo_ID, 'estudios', [{
|
|
124
|
-
fields: {
|
|
125
|
-
estudios: [{ url }],
|
|
126
|
-
combined_estudios: [{ url }],
|
|
127
|
-
patient_id: patientId
|
|
128
|
-
}
|
|
129
|
-
}]);
|
|
148
|
+
if (derivedMediaType !== 'sticker') {
|
|
149
|
+
await uploadMediaToAirtable(code, url, log, Monitoreo_ID, 'estudios');
|
|
130
150
|
}
|
|
131
151
|
} catch (error) {
|
|
132
152
|
log.warn && log.warn('[TwilioMedia] Failed to update Airtable with media reference', { index: i, error: error?.message || error });
|
|
@@ -82,22 +82,28 @@ class OpenAIAssistantsProvider {
|
|
|
82
82
|
await this.client.beta.threads.del(this._ensureId(threadId));
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
async addMessage({ threadId, role = 'user', content, metadata }) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
85
|
+
async addMessage({ threadId, messages, role = 'user', content, metadata }) {
|
|
86
|
+
const id = this._ensureId(threadId);
|
|
87
|
+
const messagesToAdd = messages || [{ role, content, metadata }];
|
|
88
|
+
|
|
89
|
+
const results = [];
|
|
90
|
+
for (const msg of messagesToAdd) {
|
|
91
|
+
const formattedContent = this._normalizeContent(msg.content);
|
|
92
|
+
const payload = {
|
|
93
|
+
role: msg.role || 'user',
|
|
94
|
+
content: formattedContent,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (msg.metadata && Object.keys(msg.metadata).length > 0) {
|
|
98
|
+
payload.metadata = msg.metadata;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = await this._retryWithRateLimit(() =>
|
|
102
|
+
this.client.beta.threads.messages.create(id, payload)
|
|
103
|
+
);
|
|
104
|
+
results.push(result);
|
|
96
105
|
}
|
|
97
|
-
|
|
98
|
-
return this._retryWithRateLimit(() =>
|
|
99
|
-
this.client.beta.threads.messages.create(this._ensureId(threadId), payload)
|
|
100
|
-
);
|
|
106
|
+
return messages ? results : results[0];
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
async listMessages({ threadId, runId, order = 'desc', limit } = {}) {
|
|
@@ -197,23 +197,26 @@ class OpenAIResponsesProvider {
|
|
|
197
197
|
return await this._delete(`/conversations/${id}`);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
async addMessage({ threadId, role = 'user', content, metadata }) {
|
|
200
|
+
async addMessage({ threadId, messages, role = 'user', content, metadata }) {
|
|
201
201
|
const id = this._ensurethreadId(threadId);
|
|
202
|
+
const messagesToAdd = messages || [{ role, content, metadata }];
|
|
202
203
|
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
const payloads = messagesToAdd.map(msg =>
|
|
205
|
+
this._cleanObject({
|
|
206
|
+
role: msg.role || 'user',
|
|
207
|
+
content: this._normalizeContent(msg.role || 'user', msg.content),
|
|
208
|
+
type: 'message',
|
|
209
|
+
})
|
|
210
|
+
).filter(p => p.content);
|
|
208
211
|
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
});
|
|
216
|
-
}
|
|
212
|
+
if (payloads.length === 0) return null;
|
|
213
|
+
|
|
214
|
+
return this._retryWithRateLimit(async () => {
|
|
215
|
+
if (this.conversations?.items?.create) {
|
|
216
|
+
return await this.conversations.items.create(id, { items: payloads });
|
|
217
|
+
}
|
|
218
|
+
return await this._post(`/conversations/${id}/items`, { items: payloads });
|
|
219
|
+
});
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
async listMessages({ threadId, order = 'desc', limit } = {}) {
|
|
@@ -78,9 +78,41 @@ async function updateRecordByFilter(baseID, tableName, filter, updateFields) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
|
|
82
|
+
async function addLinkedRecord(baseID, targetTable, fields, linkConfig) {
|
|
83
|
+
if (!baseID) {
|
|
84
|
+
throw new Error('[addLinkedRecord] Base ID is required');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
if (linkConfig) {
|
|
89
|
+
const { referenceTable, referenceFilter, linkFieldName } = linkConfig;
|
|
90
|
+
|
|
91
|
+
const base = airtable.base(baseID);
|
|
92
|
+
const referenceRecords = await base(referenceTable).select({
|
|
93
|
+
filterByFormula: referenceFilter
|
|
94
|
+
}).firstPage();
|
|
95
|
+
|
|
96
|
+
if (linkFieldName && referenceRecords && referenceRecords.length > 0) {
|
|
97
|
+
const recordId = referenceRecords[0].id;
|
|
98
|
+
if (recordId) {
|
|
99
|
+
fields[linkFieldName] = [recordId];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const record = await addRecord(baseID, targetTable, [{ fields }]);
|
|
105
|
+
return record;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('[addLinkedRecord] Error adding linked record:', error);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
81
112
|
module.exports = {
|
|
82
113
|
addRecord,
|
|
83
114
|
getRecords,
|
|
84
115
|
getRecordByFilter,
|
|
85
|
-
updateRecordByFilter
|
|
116
|
+
updateRecordByFilter,
|
|
117
|
+
addLinkedRecord
|
|
86
118
|
};
|
|
@@ -12,8 +12,8 @@ const { getCurRow } = require('../helpers/assistantHelper.js');
|
|
|
12
12
|
const { runAssistantAndWait, runAssistantWithRetries } = require('../helpers/assistantHelper.js');
|
|
13
13
|
const { getThread, getThreadInfo } = require('../helpers/threadHelper.js');
|
|
14
14
|
const { withTracing } = require('../utils/tracingDecorator.js');
|
|
15
|
-
const {
|
|
16
|
-
const { getLastMessages } = require('../helpers/messageHelper.js');
|
|
15
|
+
const { processThreadMessage } = require('../helpers/processHelper.js');
|
|
16
|
+
const { getLastMessages, updateMessageRecord } = require('../helpers/messageHelper.js');
|
|
17
17
|
const { combineImagesToPDF, cleanupFiles } = require('../helpers/filesHelper.js');
|
|
18
18
|
const { logger } = require('../middleware/requestId');
|
|
19
19
|
|
|
@@ -298,19 +298,24 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
|
|
|
298
298
|
|
|
299
299
|
timings.processMessages = Date.now();
|
|
300
300
|
logger.log(`[replyAssistantCore] Processing ${patientReply.length} messages in parallel`);
|
|
301
|
-
const results = await Promise.all(
|
|
302
|
-
patientReply.map((reply, i) =>
|
|
303
|
-
processIndividualMessage(code, reply, provider, thread)
|
|
304
|
-
.then(result => {
|
|
305
|
-
logger.log(`[replyAssistantCore] Message ${i + 1}/${patientReply.length}: isPatient=${result.isPatient}, hasUrl=${!!result.url}`);
|
|
306
|
-
return result;
|
|
307
|
-
})
|
|
308
|
-
)
|
|
309
|
-
);
|
|
310
|
-
timings.processMessages = Date.now() - timings.processMessages;
|
|
311
301
|
|
|
312
|
-
const
|
|
313
|
-
|
|
302
|
+
const processResults = await processThreadMessage(code, patientReply, provider);
|
|
303
|
+
|
|
304
|
+
const patientMsg = processResults.some(r => r.isPatient);
|
|
305
|
+
const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
|
|
306
|
+
const allMessagesToAdd = processResults.flatMap(r => r.messages || []);
|
|
307
|
+
const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
|
|
308
|
+
|
|
309
|
+
if (allMessagesToAdd.length > 0) {
|
|
310
|
+
const threadId = thread.getConversationId();
|
|
311
|
+
logger.log(`[replyAssistantCore] Adding ${allMessagesToAdd.length} messages to thread in batch`);
|
|
312
|
+
await provider.addMessage({ threadId, messages: allMessagesToAdd });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await Promise.all(processResults.map(r => updateMessageRecord(r.reply, thread)));
|
|
316
|
+
await cleanupFiles(allTempFiles);
|
|
317
|
+
|
|
318
|
+
timings.processMessages = Date.now() - timings.processMessages;
|
|
314
319
|
|
|
315
320
|
if (urls.length > 0) {
|
|
316
321
|
timings.pdfCombination = Date.now();
|