@peopl-health/nexus 4.4.4 → 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.
Files changed (45) hide show
  1. package/README.md +4 -9
  2. package/lib/adapters/BaileysProvider.js +4 -2
  3. package/lib/adapters/MessageProvider.js +2 -2
  4. package/lib/adapters/TwilioProvider.js +7 -3
  5. package/lib/controllers/assistantController.js +2 -6
  6. package/lib/controllers/bugReportController.js +2 -2
  7. package/lib/controllers/conversationController.js +13 -13
  8. package/lib/controllers/interactionController.js +2 -2
  9. package/lib/controllers/messageController.js +6 -5
  10. package/lib/controllers/qualityMessageController.js +3 -2
  11. package/lib/core/AssistantProcessor.js +3 -3
  12. package/lib/core/BatchingManager.js +6 -5
  13. package/lib/core/NexusMessaging.js +230 -155
  14. package/lib/core/PhiProcessor.js +113 -0
  15. package/lib/eval/EvalProvider.js +6 -1
  16. package/lib/helpers/baileysHelper.js +3 -1
  17. package/lib/helpers/conversationWindowHelper.js +4 -4
  18. package/lib/helpers/deliveryAttemptHelper.js +3 -1
  19. package/lib/helpers/filesHelper.js +2 -5
  20. package/lib/helpers/messageHelper.js +10 -71
  21. package/lib/helpers/messageStatusHelper.js +3 -3
  22. package/lib/helpers/nerHelper.js +64 -0
  23. package/lib/helpers/templateRecoveryHelper.js +2 -0
  24. package/lib/index.d.ts +16 -1
  25. package/lib/jobs/ScheduledMessageJob.js +15 -23
  26. package/lib/jobs/TemplateApprovalJob.js +4 -1
  27. package/lib/memory/DefaultMemoryManager.js +5 -5
  28. package/lib/memory/SessionManager.js +3 -6
  29. package/lib/models/deliveryAttemptModel.js +1 -1
  30. package/lib/models/globalEntityMapModel.js +27 -0
  31. package/lib/models/messageModel.js +0 -94
  32. package/lib/models/tokenMapModel.js +28 -0
  33. package/lib/providers/NerClient.js +43 -0
  34. package/lib/providers/OpenAIResponsesProvider.js +9 -7
  35. package/lib/providers/OpenAIResponsesProviderTools.js +26 -9
  36. package/lib/services/assistantService.js +20 -11
  37. package/lib/services/dashboardService.js +4 -4
  38. package/lib/services/globalEntityService.js +59 -0
  39. package/lib/services/messageService.js +107 -0
  40. package/lib/services/metaService.js +5 -13
  41. package/lib/services/patientService.js +3 -2
  42. package/lib/services/tokenMapService.js +100 -0
  43. package/lib/storage/MongoStorage.js +9 -14
  44. package/lib/utils/tokenMapUtils.js +12 -0
  45. package/package.json +1 -1
@@ -0,0 +1,43 @@
1
+ const axios = require('axios');
2
+
3
+ const { logger } = require('../utils/logger');
4
+
5
+ const { detectStructured, personaFallback, mergeSpans } = require('../helpers/nerHelper');
6
+
7
+ class NerClient {
8
+ constructor({ nerUrl, nerApiKey, nerTimeoutMs } = {}) {
9
+ this.url = nerUrl || null;
10
+ this.apiKey = nerApiKey || null;
11
+ this.timeout = nerTimeoutMs || 3000;
12
+
13
+ if (this.url && !this.apiKey) {
14
+ logger.warn('[NerClient] NER URL configured without an API key');
15
+ }
16
+ }
17
+
18
+ async detectEntities(text) {
19
+ if (typeof text !== 'string' || !text.trim()) return [];
20
+
21
+ const structured = detectStructured(text);
22
+
23
+ if (!this.url) {
24
+ logger.debug('[NerClient] No NER endpoint configured, using regex fallback');
25
+ return mergeSpans(personaFallback(text), structured);
26
+ }
27
+
28
+ try {
29
+ const headers = { 'Content-Type': 'application/json' };
30
+ if (this.apiKey) headers['x-api-key'] = this.apiKey;
31
+
32
+ // same-length replacement keeps span offsets valid against original text
33
+ const cleaned = text.replace(/[?!]/g, ' ');
34
+ const { data } = await axios.post(this.url, { text: cleaned }, { headers, timeout: this.timeout });
35
+ return mergeSpans(data.entities || [], structured);
36
+ } catch (err) {
37
+ logger.warn('[NerClient] Lambda call failed, falling back to regex', { error: err.message });
38
+ return mergeSpans(personaFallback(text), structured);
39
+ }
40
+ }
41
+ }
42
+
43
+ module.exports = { NerClient };
@@ -7,10 +7,10 @@ const { getCurrentMexicoDateTime } = require('../utils/dateUtils');
7
7
 
8
8
  const { DefaultMemoryManager } = require('../memory/DefaultMemoryManager');
9
9
 
10
- const { getLastNMessages } = require('../helpers/messageHelper');
11
-
12
10
  const { composePrompt, resolveTools } = require('../services/promptComposerService');
13
11
  const { getToolSchemas: getRegistrySchemas } = require('../services/toolRegistryService');
12
+ const { getMessages } = require('../services/messageService');
13
+
14
14
  const { logBugReportToAirtable } = require('../controllers/bugReportController');
15
15
  const { handleFunctionCalls } = require('./OpenAIResponsesProviderTools');
16
16
 
@@ -248,10 +248,12 @@ class OpenAIResponsesProvider {
248
248
  }
249
249
  });
250
250
 
251
- const lastMessage = await getLastNMessages(thread.code, 1);
251
+ const [lastMessage] = await getMessages({ numero: thread.code }, { sort: { createdAt: -1 }, limit: 1 });
252
+ const phiProcessor = config.phiProcessor;
253
+ const maskedCode = phiProcessor ? phiProcessor.hashCode(thread.code) : thread.code;
252
254
  const metadata = {
253
- numero: thread.code,
254
- message_id: message?.message_id || lastMessage[0]?.message_id || null
255
+ numero: maskedCode,
256
+ message_id: message?.message_id || lastMessage?.message_id || null
255
257
  };
256
258
 
257
259
  logger.info('[executeRun] Context built', { conversationId, messageCount: context?.length || 0 });
@@ -342,7 +344,7 @@ class OpenAIResponsesProvider {
342
344
  threadId, assistantId, presetId = null, presetVersion = null, additionalMessages = [], context = null,
343
345
  instructions = null, additionalInstructions = null, metadata = {},
344
346
  assistant, promptVersion = null, promptVariables = null,
345
- toolChoice = 'auto', prePromptResult = null
347
+ toolChoice = 'auto', prePromptResult = null, phiProcessor = null
346
348
  } = config;
347
349
 
348
350
  let totalRetries = 0;
@@ -466,7 +468,7 @@ class OpenAIResponsesProvider {
466
468
  const functionCalls = finalResponse.output.filter(item => item.type === 'function_call');
467
469
  if (!functionCalls.length) break;
468
470
 
469
- const { outputs, toolsExecuted } = await handleFunctionCalls(functionCalls, assistant);
471
+ const { outputs, toolsExecuted } = await handleFunctionCalls(functionCalls, assistant, phiProcessor);
470
472
 
471
473
  currentInput.push(...finalResponse.output, ...outputs);
472
474
  allToolsExecuted.push(...toolsExecuted);
@@ -1,32 +1,49 @@
1
1
  const { logger } = require('../utils/logger');
2
+ const { safeParse } = require('../utils/jsonUtils');
3
+
2
4
  const { hasTool, executeTool: registryExecuteTool } = require('../services/toolRegistryService');
3
5
 
4
- async function executeFunctionCall(assistant, call) {
6
+ async function executeFunctionCall(assistant, call, phiProcessor) {
5
7
  const startTime = Date.now();
6
8
  const name = call.name;
7
- const args = call.arguments ? JSON.parse(call.arguments) : {};
9
+ const code = assistant?.thread?.code;
10
+ const argsStr = call.arguments || '{}';
11
+ const args = safeParse(argsStr);
12
+ let decodedArgs = args;
13
+ try {
14
+ decodedArgs = JSON.parse(await phiProcessor?.decodeBody(argsStr, code) ?? argsStr);
15
+ } catch (e) {
16
+ logger.warn('[executeFunctionCall] arg decode produced invalid JSON, falling back to original args', { name, error: e.message });
17
+ }
8
18
 
9
19
  logger.info('[executeFunctionCall] Calling', { name, call_id: call.call_id });
10
20
 
11
21
  let result, success = true;
12
22
  try {
13
23
  if (hasTool(name)) {
14
- result = await registryExecuteTool(name, args, assistant?.thread);
24
+ result = await registryExecuteTool(name, decodedArgs, assistant?.thread);
15
25
  } else {
16
- result = await assistant.executeTool(name, args);
26
+ result = await assistant.executeTool(name, decodedArgs);
17
27
  }
18
28
  logger.info('[executeFunctionCall] Completed', { name, call_id: call.call_id, source: hasTool(name) ? 'registry' : 'assistant', duration_ms: Date.now() - startTime });
19
29
  } catch (error) {
20
- result = { error: error?.message || 'Tool execution failed' };
30
+ result = JSON.stringify({ success: false, error: error?.message || 'Tool execution failed' });
21
31
  success = false;
22
32
  logger.error('[executeFunctionCall] Failed', { name, call_id: call.call_id, error: error?.message });
23
33
  }
24
-
34
+
35
+ try {
36
+ result = await phiProcessor?.encodeBody(result, code) ?? result;
37
+ } catch (e) {
38
+ logger.error('[executeFunctionCall] PHI encoding failed, returning safe error', { name, call_id: call.call_id, error: e.message });
39
+ result = JSON.stringify({ success: false, error: 'PHI encoding failed' });
40
+ }
41
+
25
42
  return {
26
43
  functionOutput: {
27
44
  type: 'function_call_output',
28
45
  call_id: call.call_id,
29
- output: success && typeof result === 'string' ? result : JSON.stringify(success ? result : { success: false, error: result.error })
46
+ output: result,
30
47
  },
31
48
  toolData: {
32
49
  tool_name: name,
@@ -40,14 +57,14 @@ async function executeFunctionCall(assistant, call) {
40
57
  };
41
58
  }
42
59
 
43
- async function handleFunctionCalls(functionCalls, assistant) {
60
+ async function handleFunctionCalls(functionCalls, assistant, phiProcessor) {
44
61
  if (!functionCalls?.length) return { outputs: [], toolsExecuted: [] };
45
62
 
46
63
  const outputs = [];
47
64
  const toolsExecuted = [];
48
65
 
49
66
  for (const call of functionCalls) {
50
- const { functionOutput, toolData } = await executeFunctionCall(assistant, call);
67
+ const { functionOutput, toolData } = await executeFunctionCall(assistant, call, phiProcessor);
51
68
  outputs.push(functionOutput);
52
69
  toolsExecuted.push(toolData);
53
70
  }
@@ -5,15 +5,16 @@ const { logger } = require('../utils/logger');
5
5
  const { withTracing } = require('../utils/tracingDecorator.js');
6
6
 
7
7
  const { Thread } = require('../models/threadModel.js');
8
- const { insertMessage } = require('../models/messageModel');
9
8
 
10
9
  const { getCurRow } = require('../helpers/assistantHelper.js');
11
10
  const { getThread, switchThreadStoppedStatus, setThreadPromptId } = require('../helpers/threadHelper.js');
12
11
  const { processThreadMessage } = require('../helpers/processHelper.js');
13
- const { getLastNMessages, storeProcessedContent } = require('../helpers/messageHelper.js');
14
12
  const { cleanupFiles } = require('../helpers/filesHelper.js');
15
13
 
16
14
  const { createLLMProvider } = require('../providers/createLLMProvider');
15
+
16
+ const { getMessages, insertMessage } = require('../services/messageService');
17
+
17
18
  const { getAssistantById } = require('./assistantResolver');
18
19
 
19
20
  const createAssistantCore = async (code, assistant_id, _messages = [], force = false) => {
@@ -127,14 +128,14 @@ const preProcessMessagesCore = async (code, message_ = null, thread) => {
127
128
  try {
128
129
  const messagesStart = Date.now();
129
130
  const beforeCheckpoint = message_?.createdAt ?? null;
130
- const lastMessage = await getLastNMessages(code, 1, beforeCheckpoint, {
131
- query: { from_me: false }
132
- });
131
+ const query = { numero: code, from_me: false };
132
+ if (beforeCheckpoint) query.createdAt = { $lte: beforeCheckpoint };
133
+ const lastMessage = await getMessages(query, { sort: { createdAt: -1 }, limit: 1 });
133
134
  timings.get_messages_ms = Date.now() - messagesStart;
134
135
 
135
136
  if (!lastMessage || lastMessage.length === 0) {
136
137
  logger.info('[preProcessMessages] No relevant data found for this assistant.');
137
- return { shouldProcess: false, messages: null, timings };
138
+ return { shouldProcess: false, messages: null, timings, pendingMediaUpdates: [] };
138
139
  }
139
140
 
140
141
  const provider = createLLMProvider({ variant: getProviderVariant() });
@@ -160,24 +161,32 @@ const preProcessMessagesCore = async (code, message_ = null, thread) => {
160
161
  const patientMsg = processResults.some(r => r.isPatient);
161
162
  const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
162
163
 
163
- await Promise.all(processResults.map(r => {
164
+ const pendingMediaUpdates = processResults.reduce((acc, r) => {
164
165
  const processedContent = r.messages && r.messages.length > 0
165
166
  ? r.messages
166
167
  .filter(msg => msg.content.text !== r.reply?.body)
167
168
  .map(msg => msg.content.text)
168
169
  .join(' ')
169
170
  : null;
170
- return r.reply ? storeProcessedContent(r.reply, thread, processedContent) : null;
171
- }).filter(Boolean));
171
+ if (!r.reply || !processedContent || !r.reply.media) return acc;
172
+ acc.push({
173
+ message_id: r.reply.message_id,
174
+ media: r.reply.media,
175
+ processedContent,
176
+ assistantId: thread.getAssistantId(),
177
+ threadId: thread.getConversationId(),
178
+ });
179
+ return acc;
180
+ }, []);
172
181
 
173
182
  await cleanupFiles(allTempFiles);
174
183
 
175
184
  if (!patientMsg || thread.stopped) {
176
185
  logger.info('[preProcessMessages] Skipping AI processing', { patientMsg, stopped: thread.stopped, code });
177
- return { shouldProcess: false, messages: null, timings };
186
+ return { shouldProcess: false, messages: null, timings, pendingMediaUpdates };
178
187
  }
179
188
 
180
- return { shouldProcess: true, messages: lastMessage, timings };
189
+ return { shouldProcess: true, messages: lastMessage, timings, pendingMediaUpdates };
181
190
  } catch (error) {
182
191
  logger.error('[preProcessMessages] Error', { error: error.message, code });
183
192
  throw error;
@@ -1,11 +1,11 @@
1
1
  const { logger } = require('../utils/logger');
2
2
  const MapCache = require('../utils/MapCache');
3
3
 
4
- const { Message } = require('../models/messageModel');
5
-
6
4
  const { fetchBoxesFromAirtable, fetchDetailsFromAirtable, attachPreview } = require('../helpers/dashboardHelper');
7
5
  const { getMexicoDateRange, buildPatientMessagesPipeline, buildToolDistributionPipeline, mergeTrendData } = require('../helpers/trendHelper');
8
6
 
7
+ const { aggregateMessages } = require('./messageService');
8
+
9
9
  const boxCache = new MapCache({ maxSize: 100 });
10
10
  const detailCache = new MapCache({ maxSize: 100 });
11
11
 
@@ -72,10 +72,10 @@ async function getDailyTrend(days) {
72
72
  const { startDate, today } = getMexicoDateRange(days);
73
73
 
74
74
  const patientPipeline = buildPatientMessagesPipeline(startDate);
75
- const patientData = await Message.aggregate(patientPipeline, { allowDiskUse: true });
75
+ const patientData = await aggregateMessages(patientPipeline, { allowDiskUse: true });
76
76
 
77
77
  const toolPipeline = buildToolDistributionPipeline(startDate);
78
- const toolData = await Message.aggregate(toolPipeline, { allowDiskUse: true });
78
+ const toolData = await aggregateMessages(toolPipeline, { allowDiskUse: true });
79
79
 
80
80
  const dailyData = mergeTrendData(patientData, toolData, today, days);
81
81
 
@@ -0,0 +1,59 @@
1
+ const { logger } = require('../utils/logger');
2
+ const MapCache = require('../utils/MapCache');
3
+ const { normalize } = require('../utils/tokenMapUtils');
4
+
5
+ const { getGlobalEntityMap } = require('../models/globalEntityMapModel');
6
+
7
+ const RELOAD_TTL_MS = 5 * 60 * 1000;
8
+ const CACHE_KEY = 'all';
9
+
10
+ const cache = new MapCache({ maxSize: 1, ttl: RELOAD_TTL_MS });
11
+
12
+ const loadAll = async () => {
13
+ const byNormalized = new Map();
14
+ const byToken = new Map();
15
+ const docs = await getGlobalEntityMap().find({}).lean();
16
+ for (const doc of docs) {
17
+ for (const [k, token] of Object.entries(doc.encode || {})) {
18
+ if (byNormalized.has(k)) {
19
+ logger.error('[loadAll] Normalized key collision', { key: k, existing: byNormalized.get(k).token, incoming: token, category: doc.category });
20
+ continue;
21
+ }
22
+ byNormalized.set(k, { token, category: doc.category });
23
+ }
24
+ for (const [token, plaintext] of Object.entries(doc.decode || {})) {
25
+ byToken.set(token, plaintext);
26
+ }
27
+ }
28
+ cache.set(CACHE_KEY, { byNormalized, byToken });
29
+ logger.debug('[loadAll] Loaded global entities', { count: byNormalized.size });
30
+ };
31
+
32
+ const getIndexes = async () => {
33
+ const hit = cache.get(CACHE_KEY);
34
+ if (hit) return hit;
35
+ await loadAll();
36
+ return cache.get(CACHE_KEY);
37
+ };
38
+
39
+ const lookupGlobal = async (plaintext) => {
40
+ const key = normalize(plaintext);
41
+ if (!key) return null;
42
+ const { byNormalized } = await getIndexes();
43
+ const entry = byNormalized.get(key);
44
+ if (!entry) return null;
45
+ return { token: entry.token, category: entry.category };
46
+ };
47
+
48
+ const lookupGlobalToken = async (token) => {
49
+ if (!token) return null;
50
+ const { byToken } = await getIndexes();
51
+ const plaintext = byToken.get(token);
52
+ return plaintext !== undefined ? plaintext : null;
53
+ };
54
+
55
+ module.exports = {
56
+ lookupGlobal,
57
+ lookupGlobalToken,
58
+ loadAll,
59
+ };
@@ -0,0 +1,107 @@
1
+ const { Monitoreo_ID } = require('../config/airtableConfig');
2
+
3
+ const { logger } = require('../utils/logger');
4
+
5
+ const { Message } = require('../models/messageModel');
6
+ const { Thread } = require('../models/threadModel');
7
+
8
+ const { getClinicalContext } = require('../helpers/patientInformationHelper');
9
+
10
+ const { updateRecordByFilter } = require('../services/airtableService');
11
+
12
+ const INTERNAL_ORIGINS = new Set(['system', 'instruction']);
13
+
14
+ async function getMessages(query, options = {}) {
15
+ let cursor = Message.find(query);
16
+ if (options.select) cursor = cursor.select(options.select);
17
+ if (options.sort) cursor = cursor.sort(options.sort);
18
+ if (options.skip != null) cursor = cursor.skip(options.skip);
19
+ if (options.limit != null) cursor = cursor.limit(options.limit);
20
+ return cursor.lean();
21
+ }
22
+
23
+ async function countMessages(query) {
24
+ return Message.countDocuments(query);
25
+ }
26
+
27
+ async function aggregateMessages(pipeline, options = {}) {
28
+ return Message.aggregate(pipeline, options);
29
+ }
30
+
31
+ async function insertMessage(values) {
32
+ try {
33
+ if (!values.clinical_context) {
34
+ const { clinicalContext } = await getClinicalContext(values.numero);
35
+ values.clinical_context = clinicalContext;
36
+ }
37
+ const messageData = Object.fromEntries(
38
+ Object.entries(values).filter(([, v]) => v !== undefined)
39
+ );
40
+
41
+ if (!Array.isArray(messageData.tools_executed)) {
42
+ messageData.tools_executed = [];
43
+ }
44
+
45
+ let doc;
46
+ let isNew;
47
+ if (values.message_id == null) {
48
+ doc = await Message.create(messageData);
49
+ isNew = true;
50
+ } else {
51
+ const result = await Message.findOneAndUpdate(
52
+ { message_id: values.message_id },
53
+ { $setOnInsert: messageData },
54
+ { upsert: true, new: true, setDefaultsOnInsert: true, includeResultMetadata: true }
55
+ );
56
+ doc = result.value;
57
+ isNew = !result.lastErrorObject?.updatedExisting;
58
+ }
59
+
60
+ if (isNew && values.numero && !INTERNAL_ORIGINS.has(values.origin)) {
61
+ Thread.findOneAndUpdate(
62
+ { code: values.numero },
63
+ {
64
+ $set: {
65
+ lastMessageAt: new Date(),
66
+ lastMessageBody: values.body || '',
67
+ lastMessageFromMe: !!values.from_me,
68
+ lastMessageMedia: values.media || null
69
+ },
70
+ $inc: {
71
+ messageCount: 1,
72
+ ...(!values.from_me ? { unreadCount: 1 } : {})
73
+ }
74
+ },
75
+ { upsert: true, setDefaultsOnInsert: true }
76
+ ).catch(err => logger.error('[MongoStorage] Failed to denormalize thread', { numero: values.numero, error: err.message }));
77
+ }
78
+
79
+ updateRecordByFilter(Monitoreo_ID, 'message_monitor', `{whatsapp_id} = "${values.numero}"`, {
80
+ ...(values.from_me ? { last_message_bot: values.body } : { last_message_patient: values.body, read: false }),
81
+ ...(values.from_me ? { last_message_bot_time: doc.createdAt.toISOString() } : { last_message_patient_time: doc.createdAt.toISOString() })
82
+ }, values.numero).catch(err => logger.error('[MongoStorage] Failed to update message_monitor table', { numero: values.numero, error: err.message }));
83
+
84
+ logger.info('[MongoStorage] Message inserted or updated successfully');
85
+ return { isNew, doc };
86
+ } catch (err) {
87
+ logger.error('[MongoStorage] Error inserting message', { error: err.message, stack: err.stack });
88
+ throw err;
89
+ }
90
+ }
91
+
92
+ async function updateMessage(filter, update) {
93
+ try {
94
+ await Message.updateMany(filter, update);
95
+ } catch (err) {
96
+ logger.error('[messageService] updateMessage failed', { error: err.message });
97
+ throw err;
98
+ }
99
+ }
100
+
101
+ module.exports = {
102
+ getMessages,
103
+ countMessages,
104
+ aggregateMessages,
105
+ insertMessage,
106
+ updateMessage,
107
+ };
@@ -5,16 +5,8 @@ const { getMetaConfig } = require('../config/metaConfig');
5
5
  const { logger } = require('../utils/logger');
6
6
  const { jsonStringifyWithUnicodeEscapes } = require('../utils/jsonUtils');
7
7
 
8
- const requireConfig = () => {
9
- const config = getMetaConfig();
10
- if (!config.accessToken || !config.wabaId) {
11
- throw new Error('Meta API not configured. Call setMetaConfig() first with accessToken and wabaId.');
12
- }
13
- return config;
14
- };
15
-
16
8
  const getApiUrl = (path) => {
17
- const config = requireConfig();
9
+ const config = getMetaConfig();
18
10
  return `https://graph.facebook.com/${config.apiVersion}/${path}`;
19
11
  };
20
12
 
@@ -27,7 +19,7 @@ const errorResult = (error) => ({
27
19
  });
28
20
 
29
21
  async function createFlowInMeta(flowJSON, name, category = 'OTHER') {
30
- const config = requireConfig();
22
+ const config = getMetaConfig();
31
23
  const categories = Array.isArray(category) ? category : [category];
32
24
 
33
25
  logger.info('[createFlowInMeta] Creating', { name, categories });
@@ -58,7 +50,7 @@ async function createFlowInMeta(flowJSON, name, category = 'OTHER') {
58
50
  }
59
51
 
60
52
  async function setFlowEndpoint(flowId, endpointUri, applicationId) {
61
- const config = requireConfig();
53
+ const config = getMetaConfig();
62
54
 
63
55
  try {
64
56
  const response = await axios.post(getApiUrl(flowId), {
@@ -78,7 +70,7 @@ async function setFlowEndpoint(flowId, endpointUri, applicationId) {
78
70
  }
79
71
 
80
72
  async function getFlowFromMeta(flowId) {
81
- const config = requireConfig();
73
+ const config = getMetaConfig();
82
74
 
83
75
  try {
84
76
  const response = await axios.get(getApiUrl(flowId), {
@@ -101,7 +93,7 @@ async function getFlowFromMeta(flowId) {
101
93
  }
102
94
 
103
95
  async function publishFlowInMeta(flowId) {
104
- const config = requireConfig();
96
+ const config = getMetaConfig();
105
97
 
106
98
  try {
107
99
  const current = await getFlowFromMeta(flowId);
@@ -3,11 +3,12 @@ const { Estado_General_ID } = require('../config/airtableConfig');
3
3
  const { logger } = require('../utils/logger');
4
4
 
5
5
  const { Thread } = require('../models/threadModel');
6
- const { Message } = require('../models/messageModel');
7
6
  const { getPatientAuditLog } = require('../models/patientAuditLogModel');
8
7
 
9
8
  const { getRecordByFilter, updateRecordByFilter } = require('../services/airtableService');
10
9
 
10
+ const { updateMessage } = require('../services/messageService');
11
+
11
12
  const getPatientInformation = async (id) => {
12
13
  const records = await getRecordByFilter(Estado_General_ID, 'estado_general', `{whatsapp_id}='${id}'`);
13
14
  if (!records || records.length === 0) return null;
@@ -83,7 +84,7 @@ const updatePatientInformation = async (code, fields, auditContext = {}) => {
83
84
  const nameUpdated = fields.updt_name && !failedFields.some(f => f.key === 'updt_name');
84
85
  if (nameUpdated) {
85
86
  await Thread.updateOne({ code }, { nombre: fields.updt_name });
86
- await Message.updateMany({ numero: code, from_me: false }, { nombre_whatsapp: fields.updt_name });
87
+ await updateMessage({ numero: code, from_me: false }, { $set: { nombre_whatsapp: fields.updt_name } });
87
88
  }
88
89
 
89
90
  const status = failedFields.length > 0 ? 'partial' : 'success';
@@ -0,0 +1,100 @@
1
+ const { logger } = require('../utils/logger');
2
+ const MapCache = require('../utils/MapCache');
3
+ const { normalize } = require('../utils/tokenMapUtils');
4
+
5
+ const { getTokenMap, PHI_CATEGORIES } = require('../models/tokenMapModel');
6
+
7
+ const tokenCache = new MapCache({ maxSize: 500 });
8
+
9
+ const getSubmap = async (code) => {
10
+ const cached = tokenCache.get(code);
11
+ if (cached) return cached;
12
+
13
+ const doc = await getTokenMap().findOne({ code }).lean();
14
+ const submap = {
15
+ encode: new Map(Object.entries(doc?.encode || {})),
16
+ decode: new Map(Object.entries(doc?.decode || {})),
17
+ };
18
+ tokenCache.set(code, submap);
19
+ return submap;
20
+ };
21
+
22
+ const allocateToken = async (code, plaintext, category, normalized, submap) => {
23
+ const counter = submap.encode.size + 1;
24
+ const token = `<<${category}_${counter}>>`;
25
+
26
+ submap.encode.set(normalized, token);
27
+ submap.decode.set(token, plaintext);
28
+
29
+ const res = await getTokenMap().updateOne(
30
+ { code },
31
+ {
32
+ $set: {
33
+ encode: Object.fromEntries(submap.encode),
34
+ decode: Object.fromEntries(submap.decode),
35
+ }
36
+ },
37
+ { upsert: true }
38
+ );
39
+ if (!res || (res.modifiedCount === 0 && !res.upsertedCount)) {
40
+ logger.error('[allocateToken] Mapping write modified 0 docs', { code, category, token });
41
+ throw new Error(`[allocateToken] Mapping write modified 0 docs for token ${token}`);
42
+ }
43
+ return token;
44
+ };
45
+
46
+ const encode = async (code, plaintext, category) => {
47
+ if (!PHI_CATEGORIES.includes(category)) {
48
+ throw new Error(`[encode] Unknown category: ${category}`);
49
+ }
50
+ if (plaintext === null || plaintext === undefined || String(plaintext).length === 0) {
51
+ return '';
52
+ }
53
+
54
+ const normalized = normalize(plaintext);
55
+ if (normalized === '') return '';
56
+
57
+ const submap = await getSubmap(code);
58
+ const existing = submap.encode.get(normalized);
59
+ if (existing) return existing;
60
+
61
+ const token = await allocateToken(code, String(plaintext), category, normalized, submap);
62
+ logger.debug('[encode] Allocated', { code, category, token });
63
+ return token;
64
+ };
65
+
66
+ const decode = async (code, token) => {
67
+ if (!token) return token;
68
+ const submap = await getSubmap(code);
69
+ const plaintext = submap.decode.get(token);
70
+ return plaintext !== undefined ? plaintext : token;
71
+ };
72
+
73
+ const encodeMany = async (code, items) => {
74
+ const tokens = [];
75
+ for (const { plaintext, category } of items || []) {
76
+ tokens.push(await encode(code, plaintext, category));
77
+ }
78
+ return tokens;
79
+ };
80
+
81
+ const decodeMany = async (code, tokens) => {
82
+ const out = [];
83
+ for (const token of tokens || []) {
84
+ out.push(await decode(code, token));
85
+ }
86
+ return out;
87
+ };
88
+
89
+ const reset = async (code) => {
90
+ await getTokenMap().deleteOne({ code });
91
+ tokenCache.delete(code);
92
+ };
93
+
94
+ module.exports = {
95
+ encode,
96
+ decode,
97
+ encodeMany,
98
+ decodeMany,
99
+ reset,
100
+ };
@@ -4,11 +4,13 @@ const runtimeConfig = require('../config/runtimeConfig');
4
4
  const { logger } = require('../utils/logger');
5
5
  const { createEvent, safeEmit } = require('../utils/eventUtils');
6
6
 
7
- const { Message, insertMessage } = require('../models/messageModel');
7
+ const { Message } = require('../models/messageModel');
8
8
  const { Thread } = require('../models/threadModel');
9
9
 
10
10
  const { ensureWhatsAppFormat } = require('../helpers/twilioHelper');
11
11
 
12
+ const { getMessages, insertMessage } = require('../services/messageService');
13
+
12
14
  const { getEventBus: getStatusEventBus } = require('../core/NexusMessaging');
13
15
 
14
16
  class MongoStorage {
@@ -39,8 +41,8 @@ class MongoStorage {
39
41
  const values = this.buildMessageValues(messageData);
40
42
  const { doc } = await insertMessage(values);
41
43
  logger.info('[MongoStorage] Message stored');
42
- this._emitMessageNew(doc, messageData.from, messageData.frontendId);
43
- return values;
44
+ if (!messageData.silent) this._emitMessageNew(doc, messageData.from, messageData.frontendId);
45
+ return doc;
44
46
  } catch (error) {
45
47
  logger.error('Error saving message', { error });
46
48
  throw error;
@@ -53,7 +55,7 @@ class MongoStorage {
53
55
  statusInfo: { status: 'queued', updatedAt: new Date() }
54
56
  });
55
57
  const { doc } = await insertMessage(values);
56
- this._emitMessageNew(doc, messageData.from, messageData.frontendId, 'queued');
58
+ if (!messageData.silent) this._emitMessageNew(doc, messageData.from, messageData.frontendId, 'queued');
57
59
  return doc;
58
60
  }
59
61
 
@@ -101,7 +103,7 @@ class MongoStorage {
101
103
  const fromMe = messageData.fromMe !== undefined ? messageData.fromMe : true;
102
104
 
103
105
  const values = {
104
- nombre_whatsapp: fromMe ? runtimeConfig.get('USER_DB_MONGO') : messageData.pushName,
106
+ nombre_whatsapp: messageData.pushName ?? (fromMe ? runtimeConfig.get('USER_DB_MONGO') : null),
105
107
  numero: ensureWhatsAppFormat(messageData.code),
106
108
  body: messageData.body || '',
107
109
  processed: messageData.processed || false,
@@ -114,6 +116,7 @@ class MongoStorage {
114
116
  template_variables: messageData.variables ? JSON.stringify(messageData.variables) : null,
115
117
  raw: messageData.raw || null,
116
118
  origin: messageData.origin || 'whatsapp_platform',
119
+ assistant_id: messageData.assistantId || null,
117
120
  tools_executed: messageData.tools_executed,
118
121
  prompt: messageData.prompt || null,
119
122
  preset: messageData.preset || null,
@@ -126,15 +129,7 @@ class MongoStorage {
126
129
  }
127
130
 
128
131
  async getMessages(numero, limit = 50) {
129
- try {
130
- return await this.schemas.Message
131
- .find({ numero })
132
- .sort({ createdAt: -1 })
133
- .limit(limit);
134
- } catch (error) {
135
- logger.error('Error getting messages', { error });
136
- throw error;
137
- }
132
+ return getMessages({ numero }, { sort: { createdAt: -1 }, limit });
138
133
  }
139
134
 
140
135
  async getThread(code) {