@peopl-health/nexus 4.4.5 → 4.5.21
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/README.md +4 -9
- package/lib/adapters/BaileysProvider.js +4 -2
- package/lib/adapters/MessageProvider.js +2 -2
- package/lib/adapters/TwilioProvider.js +7 -3
- package/lib/controllers/assistantController.js +2 -6
- package/lib/controllers/bugReportController.js +2 -2
- package/lib/controllers/conversationController.js +13 -13
- package/lib/controllers/interactionController.js +2 -2
- package/lib/controllers/messageController.js +6 -5
- package/lib/controllers/qualityMessageController.js +3 -2
- package/lib/core/AssistantProcessor.js +3 -3
- package/lib/core/BatchingManager.js +6 -5
- package/lib/core/NexusMessaging.js +225 -154
- package/lib/core/PhiProcessor.js +113 -0
- package/lib/eval/EvalProvider.js +6 -1
- package/lib/helpers/baileysHelper.js +3 -1
- package/lib/helpers/conversationWindowHelper.js +4 -4
- package/lib/helpers/deliveryAttemptHelper.js +3 -1
- package/lib/helpers/filesHelper.js +2 -5
- package/lib/helpers/messageHelper.js +10 -71
- package/lib/helpers/messageStatusHelper.js +3 -3
- package/lib/helpers/nerHelper.js +64 -0
- package/lib/helpers/templateRecoveryHelper.js +2 -0
- package/lib/index.d.ts +16 -1
- package/lib/jobs/ScheduledMessageJob.js +15 -23
- package/lib/jobs/TemplateApprovalJob.js +4 -1
- package/lib/memory/DefaultMemoryManager.js +5 -5
- package/lib/memory/SessionManager.js +3 -6
- package/lib/models/deliveryAttemptModel.js +1 -1
- package/lib/models/globalEntityMapModel.js +27 -0
- package/lib/models/messageModel.js +0 -94
- package/lib/models/tokenMapModel.js +28 -0
- package/lib/providers/NerClient.js +43 -0
- package/lib/providers/OpenAIResponsesProvider.js +9 -7
- package/lib/providers/OpenAIResponsesProviderTools.js +26 -9
- package/lib/services/assistantService.js +20 -11
- package/lib/services/dashboardService.js +4 -4
- package/lib/services/globalEntityService.js +59 -0
- package/lib/services/messageService.js +107 -0
- package/lib/services/patientService.js +3 -2
- package/lib/services/tokenMapService.js +100 -0
- package/lib/storage/MongoStorage.js +9 -14
- package/lib/utils/tokenMapUtils.js +12 -0
- package/package.json +1 -1
package/lib/eval/EvalProvider.js
CHANGED
|
@@ -11,6 +11,7 @@ const { handleFunctionCalls } = require('../providers/OpenAIResponsesProviderToo
|
|
|
11
11
|
const { composePrompt, resolveTools } = require('../services/promptComposerService');
|
|
12
12
|
const { getToolSchemas: getRegistrySchemas } = require('../services/toolRegistryService');
|
|
13
13
|
const { getAssistantById } = require('../services/assistantResolver');
|
|
14
|
+
const { PhiProcessor } = require('../core/PhiProcessor');
|
|
14
15
|
|
|
15
16
|
const MAX_FUNCTION_ROUNDS = parseInt(process.env.MAX_FUNCTION_ROUNDS || '5', 10);
|
|
16
17
|
|
|
@@ -47,6 +48,10 @@ class EvalProvider {
|
|
|
47
48
|
client: this.client,
|
|
48
49
|
defaultModels: { responseModel: this.model },
|
|
49
50
|
});
|
|
51
|
+
this.phiProcessor = new PhiProcessor({
|
|
52
|
+
encode: config.phi?.encode || false,
|
|
53
|
+
ner: config.phi?.ner || null,
|
|
54
|
+
});
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
id() {
|
|
@@ -254,7 +259,7 @@ class EvalProvider {
|
|
|
254
259
|
const calls = finalResponse.output.filter(item => item.type === 'function_call');
|
|
255
260
|
if (!calls.length) break;
|
|
256
261
|
|
|
257
|
-
const { outputs, toolsExecuted } = await handleFunctionCalls(calls, assistant);
|
|
262
|
+
const { outputs, toolsExecuted } = await handleFunctionCalls(calls, assistant, this.phiProcessor);
|
|
258
263
|
currentInput.push(...finalResponse.output, ...outputs);
|
|
259
264
|
allToolsExecuted.push(...toolsExecuted);
|
|
260
265
|
|
|
@@ -2,10 +2,12 @@ const { downloadMediaMessage } = require('baileys');
|
|
|
2
2
|
|
|
3
3
|
const { logger } = require('../utils/logger');
|
|
4
4
|
|
|
5
|
-
const {
|
|
5
|
+
const { getMessageValues } = require('../models/messageModel.js');
|
|
6
6
|
|
|
7
7
|
const { uploadMediaToS3 } = require('../helpers/mediaHelper.js');
|
|
8
8
|
|
|
9
|
+
const { insertMessage } = require('../services/messageService');
|
|
10
|
+
|
|
9
11
|
async function processMessage(message, messageType) {
|
|
10
12
|
try {
|
|
11
13
|
const { content } = extractMessageContent(message, messageType);
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const { getMessages } = require('../services/messageService');
|
|
2
2
|
|
|
3
3
|
const WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
4
4
|
|
|
5
5
|
async function isWithin24HourWindow(code) {
|
|
6
6
|
if (!code) return false;
|
|
7
7
|
const cutoff = new Date(Date.now() - WINDOW_MS);
|
|
8
|
-
const recent = await
|
|
8
|
+
const [recent] = await getMessages(
|
|
9
9
|
{ numero: code, from_me: false, createdAt: { $gte: cutoff } },
|
|
10
|
-
'_id'
|
|
11
|
-
)
|
|
10
|
+
{ select: '_id', limit: 1 }
|
|
11
|
+
);
|
|
12
12
|
return !!recent;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -3,6 +3,8 @@ const { logger } = require('../utils/logger');
|
|
|
3
3
|
const { Message } = require('../models/messageModel');
|
|
4
4
|
const { DeliveryAttempt, DELIVERY_ATTEMPT_TERMINAL_STATUSES } = require('../models/deliveryAttemptModel');
|
|
5
5
|
|
|
6
|
+
const { getMessages } = require('../services/messageService');
|
|
7
|
+
|
|
6
8
|
async function recordDeliveryAttempt({
|
|
7
9
|
messageData = null, messageId = null, twilioResult = null, kind, body = null,
|
|
8
10
|
errorSource = null, errorCode = null, errorMessage = null
|
|
@@ -13,7 +15,7 @@ async function recordDeliveryAttempt({
|
|
|
13
15
|
let targetId = messageId;
|
|
14
16
|
if (!targetId) {
|
|
15
17
|
if (!sid) return null;
|
|
16
|
-
const msgDoc = await
|
|
18
|
+
const [msgDoc] = await getMessages({ message_id: sid }, { select: '_id', limit: 1 });
|
|
17
19
|
if (!msgDoc) {
|
|
18
20
|
logger.warn('[deliveryAttemptHelper] Message not found for SID; skipping attempt record', { twilioSid: sid });
|
|
19
21
|
return null;
|
|
@@ -8,7 +8,7 @@ const { downloadFileFromS3 } = require('../config/awsConfig.js');
|
|
|
8
8
|
const { sanitizeFilename } = require('../utils/sanitizerUtils.js');
|
|
9
9
|
const { logger } = require('../utils/logger');
|
|
10
10
|
|
|
11
|
-
const {
|
|
11
|
+
const { getMessages } = require('../services/messageService');
|
|
12
12
|
|
|
13
13
|
async function convertPdfToImages(pdfName, existingPdfPath = null) {
|
|
14
14
|
const outputDir = path.join(__dirname, 'assets', 'tmp');
|
|
@@ -89,10 +89,7 @@ const cleanupFiles = async (files) => {
|
|
|
89
89
|
|
|
90
90
|
async function downloadMediaAndCreateFile(code, reply) {
|
|
91
91
|
try {
|
|
92
|
-
const resultMedia = await
|
|
93
|
-
message_id: reply.message_id,
|
|
94
|
-
media: { $ne: null }
|
|
95
|
-
});
|
|
92
|
+
const [resultMedia] = await getMessages({ message_id: reply.message_id, media: { $ne: null } }, { limit: 1 });
|
|
96
93
|
|
|
97
94
|
const { bucketName, key } = resultMedia?.media || {};
|
|
98
95
|
if (!bucketName || !key) return [];
|
|
@@ -1,67 +1,8 @@
|
|
|
1
1
|
const moment = require('moment-timezone');
|
|
2
2
|
|
|
3
3
|
const { logger } = require('../utils/logger');
|
|
4
|
-
const { maskSensitiveValue } = require('../utils/sanitizerUtils');
|
|
5
4
|
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
const storeProcessedContent = async (reply, thread, processedContent) => {
|
|
9
|
-
if (!processedContent || !reply.media) return;
|
|
10
|
-
|
|
11
|
-
await Message.updateOne(
|
|
12
|
-
{ message_id: reply.message_id },
|
|
13
|
-
{ $set: {
|
|
14
|
-
assistant_id: thread.getAssistantId(),
|
|
15
|
-
thread_id: thread.getConversationId(),
|
|
16
|
-
media: {
|
|
17
|
-
...reply.media,
|
|
18
|
-
metadata: {
|
|
19
|
-
...(reply.media.metadata || {}),
|
|
20
|
-
processedContent
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}}
|
|
24
|
-
);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
async function getLastMessages(code) {
|
|
28
|
-
try {
|
|
29
|
-
const messages = await Message.find({
|
|
30
|
-
processed: false, numero: code,
|
|
31
|
-
$or: [{ origin: 'patient' }, { origin: 'whatsapp_platform' }]
|
|
32
|
-
}).sort({ createdAt: 1 });
|
|
33
|
-
|
|
34
|
-
if (!messages?.length) {
|
|
35
|
-
logger.info('[getLastMessages] No unprocessed messages', { code: maskSensitiveValue(code) });
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
logger.info('[getLastMessages] Found messages', { count: messages.length, code: maskSensitiveValue(code) });
|
|
40
|
-
return messages;
|
|
41
|
-
} catch (error) {
|
|
42
|
-
logger.error('[getLastMessages] Error', { code: maskSensitiveValue(code), error: error.message });
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function getLastNMessages(code, n, before = null, options = {}) {
|
|
48
|
-
try {
|
|
49
|
-
const query = { numero: code, ...options.query };
|
|
50
|
-
if (before) query.createdAt = { [options.beforeOperator || '$lte']: before };
|
|
51
|
-
|
|
52
|
-
const messages = await Message.find(query)
|
|
53
|
-
.select(options.select || null)
|
|
54
|
-
.sort(options.sort || { createdAt: -1 })
|
|
55
|
-
.limit(n)
|
|
56
|
-
.lean();
|
|
57
|
-
|
|
58
|
-
logger.debug('[getLastNMessages] Retrieved', { count: messages?.length || 0, code: maskSensitiveValue(code), n });
|
|
59
|
-
return messages || [];
|
|
60
|
-
} catch (error) {
|
|
61
|
-
logger.error('[getLastNMessages] Error', { code: maskSensitiveValue(code), n, error: error.message });
|
|
62
|
-
return [];
|
|
63
|
-
}
|
|
64
|
-
}
|
|
5
|
+
const { getMessages } = require('../services/messageService');
|
|
65
6
|
|
|
66
7
|
function formatMessage(reply) {
|
|
67
8
|
try {
|
|
@@ -142,20 +83,18 @@ function getMessageTools(reply) {
|
|
|
142
83
|
}
|
|
143
84
|
}
|
|
144
85
|
|
|
145
|
-
async function
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
86
|
+
async function getLastNMessages(code, n, anchor = null, opts = {}) {
|
|
87
|
+
const { beforeOperator = '$lte', query: extraQuery = {} } = opts;
|
|
88
|
+
const query = {
|
|
89
|
+
...extraQuery,
|
|
90
|
+
numero: code,
|
|
91
|
+
...(anchor ? { createdAt: { [beforeOperator]: anchor } } : {}),
|
|
92
|
+
};
|
|
93
|
+
return getMessages(query, { sort: { createdAt: -1 }, limit: n });
|
|
152
94
|
}
|
|
153
95
|
|
|
154
96
|
module.exports = {
|
|
155
|
-
storeProcessedContent,
|
|
156
|
-
getLastMessages,
|
|
157
|
-
getLastNMessages,
|
|
158
97
|
formatMessage,
|
|
159
98
|
getMessageTools,
|
|
160
|
-
|
|
99
|
+
getLastNMessages,
|
|
161
100
|
};
|
|
@@ -10,6 +10,7 @@ const { handle24HourWindowError } = require('../helpers/templateRecoveryHelper')
|
|
|
10
10
|
const { updateDeliveryAttemptByTwilioSid } = require('../helpers/deliveryAttemptHelper');
|
|
11
11
|
|
|
12
12
|
const { addLinkedRecord, updateRecordByFilter } = require('../services/airtableService');
|
|
13
|
+
const { getMessages } = require('../services/messageService');
|
|
13
14
|
|
|
14
15
|
async function updateMessageStatus(messageSid, status, errorCode = null, errorMessage = null) {
|
|
15
16
|
try {
|
|
@@ -114,9 +115,8 @@ async function handleStatusCallback(twilioStatusData, { eventBus } = {}) {
|
|
|
114
115
|
|
|
115
116
|
async function getMessageStatus(messageSid) {
|
|
116
117
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
.lean();
|
|
118
|
+
const [msg] = await getMessages({ message_id: messageSid }, { select: 'statusInfo message_id numero body', limit: 1 });
|
|
119
|
+
return msg ?? null;
|
|
120
120
|
} catch (error) {
|
|
121
121
|
logger.error('[MessageStatus] Error fetching status', { messageSid, error: error.message });
|
|
122
122
|
return null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const STRUCTURED_PATTERNS = [
|
|
2
|
+
{
|
|
3
|
+
label: 'TELEFONO',
|
|
4
|
+
regex: /(?<!\d)(?:(?:\+?52[\s-]?)?(?:\d{2,3}[\s-]\d{3,4}[\s-]\d{4}|\d{10})|(?:\+?51[\s-]?)?9\d{2}[\s-]?\d{3}[\s-]?\d{3})(?!\d)/g,
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
label: 'EMAIL',
|
|
8
|
+
regex: /\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/g,
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
label: 'ID',
|
|
12
|
+
regex: /(?<!\w)(?:[A-Z]{4}\d{6}[HM][A-Z]{5}[A-Z\d]\d|[A-ZÑ&]{3,4}\d{6}[A-Z\d]{3}|\d{8})\b/g,
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const WORD_CHARS = '0-9A-Za-z_ÁÉÍÓÚÑáéíóúñ';
|
|
17
|
+
const NAME_REGEX = new RegExp(`(?<![${WORD_CHARS}])([A-ZÁÉÍÓÚÑ][a-záéíóúñ]{1,}(?:\\s+[A-ZÁÉÍÓÚÑ][a-záéíóúñ]{1,}){1,3})(?![${WORD_CHARS}])`, 'g');
|
|
18
|
+
|
|
19
|
+
const detectStructured = (text) => {
|
|
20
|
+
const entities = [];
|
|
21
|
+
for (const { label, regex } of STRUCTURED_PATTERNS) {
|
|
22
|
+
regex.lastIndex = 0;
|
|
23
|
+
let match;
|
|
24
|
+
while ((match = regex.exec(text)) !== null) {
|
|
25
|
+
entities.push({
|
|
26
|
+
start: match.index,
|
|
27
|
+
end: match.index + match[0].length,
|
|
28
|
+
plaintext: match[0],
|
|
29
|
+
label,
|
|
30
|
+
score: null,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return entities;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const personaFallback = (text) => {
|
|
38
|
+
const entities = [];
|
|
39
|
+
NAME_REGEX.lastIndex = 0;
|
|
40
|
+
let match;
|
|
41
|
+
while ((match = NAME_REGEX.exec(text)) !== null) {
|
|
42
|
+
entities.push({
|
|
43
|
+
start: match.index,
|
|
44
|
+
end: match.index + match[0].length,
|
|
45
|
+
plaintext: match[0],
|
|
46
|
+
label: 'PERSONA',
|
|
47
|
+
score: null,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return entities;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const mergeSpans = (bert, structured) => {
|
|
54
|
+
const filtered = bert.filter((b) =>
|
|
55
|
+
!structured.some((s) => b.start < s.end && b.end > s.start)
|
|
56
|
+
);
|
|
57
|
+
return [...filtered, ...structured].sort((a, b) => a.start - b.start);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
detectStructured,
|
|
62
|
+
personaFallback,
|
|
63
|
+
mergeSpans,
|
|
64
|
+
};
|
|
@@ -103,6 +103,8 @@ async function triggerTemplateRecovery(messageDoc, { source = 'reactive', messag
|
|
|
103
103
|
messageId: messageDocId,
|
|
104
104
|
originalMessageSid: messageSid
|
|
105
105
|
});
|
|
106
|
+
|
|
107
|
+
return { delivered: false, deferred: true };
|
|
106
108
|
} catch (error) {
|
|
107
109
|
logger.error('[TemplateRecovery] Error', { source, messageSid, error: error.message });
|
|
108
110
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -246,7 +246,7 @@ declare module '@peopl-health/nexus' {
|
|
|
246
246
|
getPipeline(): ProcessingPipeline;
|
|
247
247
|
getAssistantProcessor(): AssistantProcessor;
|
|
248
248
|
processInstruction(code: string, instruction: string, role?: string): Promise<string | null>;
|
|
249
|
-
processSystemMessage(code: string, messages: string | string[], role?: string): Promise<string | null>;
|
|
249
|
+
processSystemMessage(code: string, messages: string | string[], role?: string, options?: { triggeredBy?: string; reply?: boolean }): Promise<string | null>;
|
|
250
250
|
isConnected(): boolean;
|
|
251
251
|
disconnect(): Promise<void>;
|
|
252
252
|
}
|
|
@@ -289,6 +289,12 @@ declare module '@peopl-health/nexus' {
|
|
|
289
289
|
messaging?: any;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
export interface NerOptions {
|
|
293
|
+
nerUrl?: string;
|
|
294
|
+
nerApiKey?: string;
|
|
295
|
+
nerTimeoutMs?: number;
|
|
296
|
+
}
|
|
297
|
+
|
|
292
298
|
export interface InitializeOptions {
|
|
293
299
|
provider?: 'twilio' | 'baileys';
|
|
294
300
|
providerConfig?: TwilioConfig | BaileysConfig;
|
|
@@ -297,6 +303,15 @@ declare module '@peopl-health/nexus' {
|
|
|
297
303
|
assistant?: AssistantConfig;
|
|
298
304
|
parser?: boolean;
|
|
299
305
|
parserConfig?: ParserConfig;
|
|
306
|
+
ner?: NerOptions;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export interface NerEntity {
|
|
310
|
+
start: number;
|
|
311
|
+
end: number;
|
|
312
|
+
plaintext: string;
|
|
313
|
+
label: string;
|
|
314
|
+
score: number | null;
|
|
300
315
|
}
|
|
301
316
|
|
|
302
317
|
export class Nexus {
|
|
@@ -16,13 +16,12 @@ const TERMINAL_STATUSES = ['sent', 'cancelled'];
|
|
|
16
16
|
const STALE_SENDING_MS = 5 * 60 * 1000;
|
|
17
17
|
|
|
18
18
|
class ScheduledMessageJob extends BaseJob {
|
|
19
|
-
constructor({ queueAdapter, sendMessage,
|
|
19
|
+
constructor({ queueAdapter, sendMessage, requireProvider = null } = {}) {
|
|
20
20
|
super({ queueAdapter, queueName: QUEUE_NAME });
|
|
21
21
|
if (typeof sendMessage !== 'function') {
|
|
22
22
|
throw new Error('ScheduledMessageJob requires a sendMessage function');
|
|
23
23
|
}
|
|
24
24
|
this.sendMessage = sendMessage;
|
|
25
|
-
this.requireMessageStorage = requireMessageStorage;
|
|
26
25
|
this.requireProvider = requireProvider;
|
|
27
26
|
}
|
|
28
27
|
|
|
@@ -94,26 +93,11 @@ class ScheduledMessageJob extends BaseJob {
|
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
try {
|
|
97
|
-
|
|
98
|
-
if (!parentMessageId) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (rendered) msg.message = rendered;
|
|
103
|
-
}
|
|
104
|
-
const storage = this.requireMessageStorage?.();
|
|
105
|
-
if (storage?.savePendingMessage) {
|
|
106
|
-
const parent = await storage.savePendingMessage({
|
|
107
|
-
code: msg.code, body: msg.message, fileUrl: msg.fileUrl, fileType: msg.fileType,
|
|
108
|
-
contentSid: msg.contentSid, variables: msg.variables,
|
|
109
|
-
fromMe: true, processed: true,
|
|
110
|
-
frontendId: msg.frontendId, triggeredBy: msg.triggeredBy
|
|
111
|
-
});
|
|
112
|
-
parentMessageId = parent?._id || null;
|
|
113
|
-
if (parentMessageId) {
|
|
114
|
-
await ScheduledMessage.updateOne({ _id: scheduledMessageId }, { $set: { parentMessageId } });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
96
|
+
const parentMessageId = msg.parentMessageId || null;
|
|
97
|
+
if (!parentMessageId && msg.contentSid && !msg.message) {
|
|
98
|
+
const provider = this.requireProvider?.();
|
|
99
|
+
const rendered = await provider?.renderTemplate?.(msg.contentSid, msg.variables);
|
|
100
|
+
if (rendered) msg.message = rendered;
|
|
117
101
|
}
|
|
118
102
|
|
|
119
103
|
const result = await this.sendMessage({
|
|
@@ -124,7 +108,15 @@ class ScheduledMessageJob extends BaseJob {
|
|
|
124
108
|
contentSid: msg.contentSid,
|
|
125
109
|
variables: msg.variables,
|
|
126
110
|
hidePreview: msg.hidePreview,
|
|
127
|
-
|
|
111
|
+
frontendId: msg.frontendId,
|
|
112
|
+
triggeredBy: msg.triggeredBy,
|
|
113
|
+
processed: true,
|
|
114
|
+
...(parentMessageId ? { parentMessageId } : {
|
|
115
|
+
onParentMessageId: async (id) => {
|
|
116
|
+
if (!id) return;
|
|
117
|
+
await ScheduledMessage.updateOne({ _id: scheduledMessageId }, { $set: { parentMessageId: String(id) } });
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
128
120
|
});
|
|
129
121
|
|
|
130
122
|
const messageId = result?.messageId || result?.sid || null;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
const { logger } = require('../utils/logger');
|
|
2
2
|
const { Message } = require('../models/messageModel');
|
|
3
3
|
const { DeliveryAttempt } = require('../models/deliveryAttemptModel');
|
|
4
|
+
|
|
5
|
+
const { getMessages } = require('../services/messageService');
|
|
6
|
+
|
|
4
7
|
const { BaseJob } = require('./BaseJob');
|
|
5
8
|
|
|
6
9
|
const QUEUE_NAME = 'template-approval';
|
|
@@ -48,7 +51,7 @@ class TemplateApprovalJob extends BaseJob {
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
async _process({ templateSid, messageId, originalMessageSid, attempt = 0 }) {
|
|
51
|
-
const message = await
|
|
54
|
+
const [message] = await getMessages({ _id: messageId }, { limit: 1 });
|
|
52
55
|
|
|
53
56
|
if (!message) {
|
|
54
57
|
logger.warn('[TemplateApprovalJob] Message not found', { messageId, templateSid });
|
|
@@ -4,11 +4,12 @@ const { logger } = require('../utils/logger');
|
|
|
4
4
|
|
|
5
5
|
const { MemoryManager } = require('../memory/MemoryManager');
|
|
6
6
|
|
|
7
|
-
const {
|
|
7
|
+
const { getMessageTools, formatMessage } = require('../helpers/messageHelper');
|
|
8
8
|
|
|
9
9
|
const { handlePendingFunctionCalls: handlePendingFunctionCallsUtil } = require('../providers/OpenAIResponsesProviderTools');
|
|
10
10
|
|
|
11
11
|
const { getRecordByFilter } = require('../services/airtableService');
|
|
12
|
+
const { getMessages } = require('../services/messageService');
|
|
12
13
|
|
|
13
14
|
class DefaultMemoryManager extends MemoryManager {
|
|
14
15
|
constructor(options = {}) {
|
|
@@ -21,10 +22,9 @@ class DefaultMemoryManager extends MemoryManager {
|
|
|
21
22
|
|
|
22
23
|
try {
|
|
23
24
|
const beforeCheckpoint = config.beforeCheckpoint ?? message?.createdAt ?? null;
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
});
|
|
25
|
+
const query = { numero: thread.code, origin: { $ne: 'instruction' } };
|
|
26
|
+
if (beforeCheckpoint) query.createdAt = { [config.beforeOperator || '$lte']: beforeCheckpoint };
|
|
27
|
+
const allMessages = await getMessages(query, { sort: { createdAt: -1 }, limit: this.maxHistoricalMessages });
|
|
28
28
|
const additional = config.additionalMessages || [];
|
|
29
29
|
|
|
30
30
|
if (!allMessages?.length) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { logger } = require('../utils/logger');
|
|
2
2
|
const { isBenchMode } = require('../utils/benchModeHelper');
|
|
3
3
|
|
|
4
|
-
const {
|
|
4
|
+
const { getMessages } = require('../services/messageService');
|
|
5
5
|
|
|
6
6
|
const { MemoryExtractor } = require('./MemoryExtractor');
|
|
7
7
|
|
|
@@ -77,11 +77,8 @@ class SessionManager {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
_fetchSessionMessages(numero, sessionStart) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
origin: { $ne: 'instruction' },
|
|
83
|
-
createdAt: { $gte: new Date(sessionStart) },
|
|
84
|
-
}).sort({ createdAt: 1 }).lean();
|
|
80
|
+
const query = { numero, origin: { $ne: 'instruction' }, createdAt: { $gte: new Date(sessionStart) } };
|
|
81
|
+
return getMessages(query, { sort: { createdAt: 1 } });
|
|
85
82
|
}
|
|
86
83
|
|
|
87
84
|
_generateSessionId(numero) {
|
|
@@ -2,7 +2,7 @@ const mongoose = require('mongoose');
|
|
|
2
2
|
|
|
3
3
|
const KIND_VALUES = ['freeform', 'template', 'recovery_template', 'recovery_template_setup'];
|
|
4
4
|
const STATUS_VALUES = [null, 'queued', 'sending', 'sent', 'delivered', 'undelivered', 'failed', 'read'];
|
|
5
|
-
const TERMINAL_STATUS_VALUES = ['
|
|
5
|
+
const TERMINAL_STATUS_VALUES = ['read', 'failed', 'undelivered'];
|
|
6
6
|
const ERROR_SOURCE_VALUES = [null, 'twilio_sync', 'twilio_async', 'server'];
|
|
7
7
|
|
|
8
8
|
const deliveryAttemptSchema = new mongoose.Schema({
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const { getDb, getModelDatabase } = require('../config/mongoConfig');
|
|
4
|
+
|
|
5
|
+
const { logger } = require('../utils/logger');
|
|
6
|
+
|
|
7
|
+
const GLOBAL_ENTITY_CATEGORIES = Object.freeze(['STAFF', 'DOCTOR', 'INTERNAL_EMAIL', 'LOCATION', 'BANK_ACCOUNT']);
|
|
8
|
+
|
|
9
|
+
const globalEntityMapSchema = new mongoose.Schema({
|
|
10
|
+
category: { type: String, enum: GLOBAL_ENTITY_CATEGORIES, required: true },
|
|
11
|
+
encode: { type: Map, of: String, default: () => new Map() },
|
|
12
|
+
decode: { type: Map, of: String, default: () => new Map() },
|
|
13
|
+
}, { timestamps: true });
|
|
14
|
+
|
|
15
|
+
globalEntityMapSchema.index({ category: 1 }, { unique: true });
|
|
16
|
+
|
|
17
|
+
const getGlobalEntityMap = () => {
|
|
18
|
+
const dbName = getModelDatabase('GlobalEntityMap');
|
|
19
|
+
const db = dbName ? getDb(dbName) : mongoose;
|
|
20
|
+
logger.debug('[GlobalEntityMap] Using database', { dbName: dbName || 'default' });
|
|
21
|
+
return db.models.GlobalEntityMap || db.model('GlobalEntityMap', globalEntityMapSchema);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
getGlobalEntityMap,
|
|
26
|
+
GLOBAL_ENTITY_CATEGORIES,
|
|
27
|
+
};
|
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
const mongoose = require('mongoose');
|
|
2
2
|
|
|
3
|
-
const { Monitoreo_ID } = require('../config/airtableConfig');
|
|
4
|
-
|
|
5
|
-
const { logger } = require('../utils/logger');
|
|
6
|
-
|
|
7
|
-
const { getClinicalContext } = require('../helpers/patientInformationHelper');
|
|
8
|
-
|
|
9
|
-
const { updateRecordByFilter } = require('../services/airtableService');
|
|
10
3
|
const { DELIVERY_ATTEMPT_STATUSES } = require('./deliveryAttemptModel');
|
|
11
4
|
|
|
12
|
-
const { Thread } = require('./threadModel');
|
|
13
|
-
|
|
14
|
-
const INTERNAL_ORIGINS = new Set(['system', 'instruction']);
|
|
15
|
-
|
|
16
5
|
const messageSchema = new mongoose.Schema({
|
|
17
6
|
raw: { type: Object, default: null },
|
|
18
7
|
body: { type: String, default: '' },
|
|
@@ -109,67 +98,6 @@ messageSchema.index({ triggeredBy: 1, createdAt: -1 }, { name: 'triggered_by_idx
|
|
|
109
98
|
|
|
110
99
|
const Message = mongoose.model('Message', messageSchema);
|
|
111
100
|
|
|
112
|
-
async function insertMessage(values) {
|
|
113
|
-
try {
|
|
114
|
-
if (!values.clinical_context) {
|
|
115
|
-
const { clinicalContext} = await getClinicalContext(values.numero);
|
|
116
|
-
values.clinical_context = clinicalContext;
|
|
117
|
-
}
|
|
118
|
-
const messageData = Object.fromEntries(
|
|
119
|
-
Object.entries(values).filter(([, v]) => v !== undefined)
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
if (!Array.isArray(messageData.tools_executed)) {
|
|
123
|
-
messageData.tools_executed = [];
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
let doc;
|
|
127
|
-
let isNew;
|
|
128
|
-
if (values.message_id == null) {
|
|
129
|
-
doc = await Message.create(messageData);
|
|
130
|
-
isNew = true;
|
|
131
|
-
} else {
|
|
132
|
-
const result = await Message.findOneAndUpdate(
|
|
133
|
-
{ message_id: values.message_id },
|
|
134
|
-
{ $setOnInsert: messageData },
|
|
135
|
-
{ upsert: true, new: true, setDefaultsOnInsert: true, includeResultMetadata: true }
|
|
136
|
-
);
|
|
137
|
-
doc = result.value;
|
|
138
|
-
isNew = !result.lastErrorObject?.updatedExisting;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (isNew && values.numero && !INTERNAL_ORIGINS.has(values.origin)) {
|
|
142
|
-
Thread.findOneAndUpdate(
|
|
143
|
-
{ code: values.numero },
|
|
144
|
-
{
|
|
145
|
-
$set: {
|
|
146
|
-
lastMessageAt: new Date(),
|
|
147
|
-
lastMessageBody: values.body || '',
|
|
148
|
-
lastMessageFromMe: !!values.from_me,
|
|
149
|
-
lastMessageMedia: values.media || null
|
|
150
|
-
},
|
|
151
|
-
$inc: {
|
|
152
|
-
messageCount: 1,
|
|
153
|
-
...(!values.from_me ? { unreadCount: 1 } : {})
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
{ upsert: true, setDefaultsOnInsert: true }
|
|
157
|
-
).catch(err => logger.error('[MongoStorage] Failed to denormalize thread', { numero: values.numero, error: err.message }));
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
updateRecordByFilter(Monitoreo_ID, 'message_monitor', `{whatsapp_id} = "${values.numero}"`, {
|
|
161
|
-
...(values.from_me ? { last_message_bot: values.body } : { last_message_patient: values.body, read: false }),
|
|
162
|
-
...(values.from_me ? { last_message_bot_time: doc.createdAt.toISOString() } : { last_message_patient_time: doc.createdAt.toISOString() })
|
|
163
|
-
}, values.numero).catch(err => logger.error('[MongoStorage] Failed to update message_monitor table', { numero: values.numero, error: err.message }));
|
|
164
|
-
|
|
165
|
-
logger.info('[MongoStorage] Message inserted or updated successfully');
|
|
166
|
-
return { isNew, doc };
|
|
167
|
-
} catch (err) {
|
|
168
|
-
logger.error('[MongoStorage] Error inserting message', { error: err.message, stack: err.stack });
|
|
169
|
-
throw err;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
101
|
function getMessageValues(message, content) {
|
|
174
102
|
return {
|
|
175
103
|
nombre_whatsapp: message.pushName,
|
|
@@ -181,29 +109,7 @@ function getMessageValues(message, content) {
|
|
|
181
109
|
};
|
|
182
110
|
}
|
|
183
111
|
|
|
184
|
-
async function getContactDisplayName(contactNumber) {
|
|
185
|
-
try {
|
|
186
|
-
const latestMessage = await Message.findOne({
|
|
187
|
-
numero: contactNumber,
|
|
188
|
-
from_me: false
|
|
189
|
-
})
|
|
190
|
-
.sort({ createdAt: -1 })
|
|
191
|
-
.select('nombre_whatsapp');
|
|
192
|
-
|
|
193
|
-
if (latestMessage && latestMessage.nombre_whatsapp && latestMessage.nombre_whatsapp.trim() !== '') {
|
|
194
|
-
return latestMessage.nombre_whatsapp;
|
|
195
|
-
} else {
|
|
196
|
-
return contactNumber;
|
|
197
|
-
}
|
|
198
|
-
} catch (error) {
|
|
199
|
-
logger.error('[MongoStorage] Error fetching display name for ${contactNumber}:', error);
|
|
200
|
-
return contactNumber;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
112
|
module.exports = {
|
|
205
113
|
Message,
|
|
206
|
-
insertMessage,
|
|
207
114
|
getMessageValues,
|
|
208
|
-
getContactDisplayName
|
|
209
115
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const mongoose = require('mongoose');
|
|
2
|
+
|
|
3
|
+
const { getDb, getModelDatabase } = require('../config/mongoConfig');
|
|
4
|
+
|
|
5
|
+
const { logger } = require('../utils/logger');
|
|
6
|
+
|
|
7
|
+
const PHI_CATEGORIES = Object.freeze(['PERSONA', 'TELEFONO', 'EMAIL', 'ID']);
|
|
8
|
+
|
|
9
|
+
const tokenMapSchema = new mongoose.Schema({
|
|
10
|
+
code: { type: String, required: true },
|
|
11
|
+
encode: { type: Map, of: String, default: () => new Map() },
|
|
12
|
+
decode: { type: Map, of: String, default: () => new Map() }
|
|
13
|
+
}, { timestamps: true });
|
|
14
|
+
|
|
15
|
+
tokenMapSchema.index({ code: 1 }, { unique: true });
|
|
16
|
+
|
|
17
|
+
const getTokenMap = () => {
|
|
18
|
+
const dbName = getModelDatabase('TokenMap');
|
|
19
|
+
const db = dbName ? getDb(dbName) : mongoose;
|
|
20
|
+
logger.debug('[TokenMap] Using database', { dbName: dbName || 'default' });
|
|
21
|
+
return db.models.TokenMap || db.model('TokenMap', tokenMapSchema);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
getTokenMap,
|
|
26
|
+
tokenMapSchema,
|
|
27
|
+
PHI_CATEGORIES,
|
|
28
|
+
};
|