@peopl-health/nexus 2.6.0 → 3.0.0

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.
@@ -170,22 +170,7 @@ class BaseAssistant {
170
170
  async create(code, context = {}) {
171
171
  this._ensureClient();
172
172
  this.status = 'active';
173
-
174
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
175
- if (!provider || typeof provider.createConversation !== 'function') {
176
- throw new Error('Provider not configured. Cannot create conversation.');
177
- }
178
-
179
- const initialMessages = await this.buildInitialMessages({ code, context });
180
- const conversation = await provider.createConversation({
181
- messages: initialMessages,
182
- });
183
-
184
- this.thread = {
185
- ...conversation,
186
- thread_id: conversation?.id,
187
- code,
188
- };
173
+ this.thread = { code };
189
174
 
190
175
  return this.thread;
191
176
  }
@@ -13,6 +13,7 @@ const Logging_ID = require('./runtimeConfig').get('AIRTABLE_LOGGING_ID') || 'app
13
13
  const Monitoreo_ID = require('./runtimeConfig').get('AIRTABLE_MONITOREO_ID') || 'appdvraKSdp0XVn5n';
14
14
  const Programa_Juntas_ID = require('./runtimeConfig').get('AIRTABLE_PROGRAMA_JUNTAS_ID') || 'appKFWzkcDEWlrXBE';
15
15
  const Symptoms_ID = require('./runtimeConfig').get('AIRTABLE_SYMPTOMS_ID') || 'appQRhZlQ9tMfYZWJ';
16
+ const Follow_Up_ID = require('./runtimeConfig').get('AIRTABLE_FOLLOW_UP_ID') || 'appBjKw1Ub0KkbZf0';
16
17
  const Webinars_Leads_ID = require('./runtimeConfig').get('AIRTABLE_WEBINARS_LEADS_ID') || 'appzjpVXTI0TgqGPq';
17
18
  const Product_ID = require('./runtimeConfig').get('AIRTABLE_PRODUCT_ID') || 'appu2YDW2pKDYLL5H';
18
19
 
@@ -30,6 +31,7 @@ const BASE_MAP = {
30
31
  monitoreo: Monitoreo_ID,
31
32
  programa: Programa_Juntas_ID,
32
33
  symptoms: Symptoms_ID,
34
+ followup: Follow_Up_ID,
33
35
  webinars: Webinars_Leads_ID,
34
36
  product: Product_ID
35
37
  };
@@ -44,6 +46,7 @@ module.exports = {
44
46
  Monitoreo_ID,
45
47
  Programa_Juntas_ID,
46
48
  Symptoms_ID,
49
+ Follow_Up_ID,
47
50
  Webinars_Leads_ID,
48
51
  Product_ID,
49
52
  // Helper function to get base by ID
@@ -13,7 +13,7 @@ const Message = mongoose.models.Message;
13
13
 
14
14
  const getConversationController = async (req, res) => {
15
15
  const startTime = Date.now();
16
- logger.info('Starting getConversationController at', { time: new Date().toISOString() });
16
+ logger.info(`Starting getConversationController at ${new Date().toISOString()}`);
17
17
 
18
18
  try {
19
19
  // Parse pagination parameters
@@ -34,7 +34,7 @@ const getConversationController = async (req, res) => {
34
34
  }
35
35
 
36
36
  const messageCount = await Message.countDocuments({});
37
- logger.info('Total message count:', { messageCount });
37
+ logger.info(`Total message count: ${messageCount}`);
38
38
 
39
39
  if (messageCount === 0) {
40
40
  logger.info('No messages found in database, returning empty conversations list');
@@ -63,7 +63,7 @@ const getConversationController = async (req, res) => {
63
63
  const totalPages = Math.ceil(total / limit);
64
64
  const totalTime = Date.now() - startTime;
65
65
 
66
- logger.info('Number of conversations found:', { conversationsCount: conversations?.length || 0 });
66
+ logger.info(`Number of conversations found: ${conversations?.length || 0}`);
67
67
  logger.info(`Total controller execution time: ${totalTime}ms`);
68
68
  logger.info(`Filter: ${filter}, Pagination: ${conversations.length} of ${total} conversations (page ${page}/${totalPages})`);
69
69
 
@@ -84,7 +84,7 @@ const getConversationController = async (req, res) => {
84
84
 
85
85
  } catch (error) {
86
86
  logger.error('Error fetching conversations:', { error });
87
- logger.error('Error stack:', { error: error.stack });
87
+ logger.error('Error stack:', { stack: error.stack });
88
88
  res.status(500).json({
89
89
  success: false,
90
90
  error: error.message || 'Failed to fetch conversations'
@@ -93,10 +93,10 @@ const getConversationController = async (req, res) => {
93
93
  };
94
94
 
95
95
  const getConversationMessagesController = async (req, res) => {
96
- logger.info('Starting getConversationMessagesController at', { time: new Date().toISOString() });
96
+ logger.info(`Starting getConversationMessagesController at ${new Date().toISOString()}`);
97
97
  try {
98
98
  const { phoneNumber } = req.params;
99
- logger.info('Requested conversation for phone number:', { phoneNumber });
99
+ logger.info(`Requested conversation for phone number: ${phoneNumber}`);
100
100
 
101
101
  if (!phoneNumber) {
102
102
  return res.status(400).json({
@@ -113,13 +113,12 @@ const getConversationMessagesController = async (req, res) => {
113
113
  try {
114
114
  query.createdAt = { $lt: new Date(before) };
115
115
  } catch (parseError) {
116
- logger.warn('Invalid date format for before parameter:', { before, parseError });
116
+ logger.warn(`Invalid date format for before parameter: ${before}`, { parseError });
117
117
  query.createdAt = { $lt: before };
118
118
  }
119
119
  }
120
120
 
121
- logger.info('Fetching conversation messages', { query, limit });
122
- logger.info('Executing Message.find', { query });
121
+ logger.info(`Fetching conversation messages ${query}, limit ${limit}`);
123
122
  let messages = [];
124
123
 
125
124
  try {
@@ -136,18 +135,18 @@ const getConversationMessagesController = async (req, res) => {
136
135
 
137
136
  const problematicMessages = messages.filter(msg => {
138
137
  if (!msg || !msg.numero || !msg.createdAt) {
139
- logger.warn('Found message missing required fields:', msg?._id || 'unknown');
138
+ logger.warn(`Found message missing required fields: ${msg?._id || 'unknown'}`);
140
139
  return true;
141
140
  }
142
141
 
143
142
  if (msg.media) {
144
143
  if (!msg.media || typeof msg.media !== 'object') {
145
- logger.warn('Found media message with invalid media data:', { msgId: msg._id });
144
+ logger.warn(`Found media message with invalid media data: ${msg._id}`);
146
145
  return true;
147
146
  }
148
147
 
149
148
  if (msg.media && (typeof msg.media.data === 'function' || msg.media.data instanceof Buffer)) {
150
- logger.warn('Found media message with Buffer data that might cause serialization issues:', { msgId: msg._id });
149
+ logger.warn(`Found media message with Buffer data that might cause serialization issues: ${msg._id}`);
151
150
  return true;
152
151
  }
153
152
  }
@@ -157,12 +156,13 @@ const getConversationMessagesController = async (req, res) => {
157
156
 
158
157
  if (problematicMessages.length > 0) {
159
158
  logger.warn(`Found ${problematicMessages.length} potentially problematic messages`);
160
- logger.info('First problematic message IDs:', problematicMessages.slice(0, 3).map(m => m?._id || 'unknown'));
159
+ logger.info(`First problematic message IDs: ${problematicMessages.slice(0, 3).map(m => m?._id || 'unknown')}`);
161
160
  }
162
161
  } else {
163
162
  logger.info('No messages found for this query');
164
163
  }
165
164
  } catch (err) {
165
+ logger.error('Database query error in message retrieval:', { err });
166
166
  logger.error('Database query error in message retrieval:', { err });
167
167
  messages = [];
168
168
  }
@@ -174,7 +174,7 @@ const getConversationMessagesController = async (req, res) => {
174
174
  JSON.stringify(msg);
175
175
  return msg;
176
176
  } catch (serializationError) {
177
- logger.error(`Found non-serializable message with ID ${msg._id}:`, { serializationError });
177
+ logger.error(`Found non-serializable message with ID ${msg._id}:`, { serializationError}) ;
178
178
  return {
179
179
  _id: msg._id?.toString() || 'unknown',
180
180
  numero: msg.numero || phoneNumber,
@@ -197,6 +197,7 @@ const getConversationMessagesController = async (req, res) => {
197
197
  });
198
198
  logger.info('Successfully sent conversation messages response');
199
199
  } catch (error) {
200
+ logger.error(`Error fetching conversation for ${req.params?.phoneNumber || 'unknown'}:`, { error });
200
201
  logger.error(`Error fetching conversation for ${req.params?.phoneNumber || 'unknown'}:`, { error });
201
202
  res.status(500).json({
202
203
  success: false,
@@ -206,7 +207,7 @@ const getConversationMessagesController = async (req, res) => {
206
207
  };
207
208
 
208
209
  const getConversationReplyController = async (req, res) => {
209
- logger.info('Starting getConversationReplyController at', { time: new Date().toISOString() });
210
+ logger.info(`Starting getConversationReplyController at ${new Date().toISOString()}`);
210
211
  try {
211
212
  const { phoneNumber, message, mediaData, contentSid, variables } = req.body;
212
213
  logger.info('Reply request params:', {
@@ -225,7 +226,7 @@ const getConversationReplyController = async (req, res) => {
225
226
  }
226
227
 
227
228
  const formattedPhoneNumber = ensureWhatsAppFormat(phoneNumber);
228
- logger.info('Formatted phone number:', { formattedPhoneNumber });
229
+ logger.info(`Formatted phone number: ${formattedPhoneNumber}`);
229
230
 
230
231
  const messageData = {
231
232
  code: formattedPhoneNumber,
@@ -235,7 +236,7 @@ const getConversationReplyController = async (req, res) => {
235
236
 
236
237
  // Handle template message (contentSid provided)
237
238
  if (contentSid) {
238
- logger.info('Processing template message with contentSid:', { contentSid });
239
+ logger.info(`Processing template message with contentSid: ${contentSid}`);
239
240
  messageData.contentSid = contentSid;
240
241
 
241
242
  if (variables && Object.keys(variables).length > 0) {
@@ -281,10 +282,12 @@ const getConversationReplyController = async (req, res) => {
281
282
  message: 'Reply sent successfully'
282
283
  });
283
284
  } catch (error) {
285
+ logger.error('Error sending reply:', { error });
284
286
  logger.error('Error sending reply:', { error });
285
287
  logger.info('Request body', { body: req.body || {} });
286
288
  const errorMsg = error.message || 'Failed to send reply';
287
289
  logger.error('Responding with error:', { errorMsg });
290
+ logger.error('Responding with error:', { errorMsg });
288
291
  res.status(500).json({
289
292
  success: false,
290
293
  error: errorMsg
@@ -361,6 +364,7 @@ const searchConversationsController = async (req, res) => {
361
364
  batch.map(p => `{whatsapp_id} = "${p}"`).join(', ') +
362
365
  ')';
363
366
  return getRecordByFilter(Historial_Clinico_ID, 'estado_general', formula).catch(error => {
367
+ logger.error('Error fetching Airtable batch for search:', { error });
364
368
  logger.error('Error fetching Airtable batch for search:', { error });
365
369
  return [];
366
370
  });
@@ -381,6 +385,7 @@ const searchConversationsController = async (req, res) => {
381
385
  logger.info(`Found ${Object.keys(airtableNameMap).length} names in Airtable for search results (${batches.length} batches)`);
382
386
  } catch (error) {
383
387
  logger.error('Error fetching names from Airtable for search, falling back to nombre_whatsapp:', { error });
388
+ logger.error('Error fetching names from Airtable for search, falling back to nombre_whatsapp:', { error });
384
389
  }
385
390
  }
386
391
 
@@ -445,6 +450,7 @@ const searchConversationsController = async (req, res) => {
445
450
  });
446
451
 
447
452
  } catch (error) {
453
+ logger.error('Error searching conversations:', { error });
448
454
  logger.error('Error searching conversations:', { error });
449
455
  res.status(500).json({
450
456
  success: false,
@@ -478,6 +484,7 @@ const getConversationsByNameController = async (req, res) => {
478
484
  }))
479
485
  });
480
486
  } catch (error) {
487
+ logger.error('Error fetching conversations by name:', { error });
481
488
  logger.error('Error fetching conversations by name:', { error });
482
489
  res.status(500).json({
483
490
  success: false,
@@ -524,6 +531,7 @@ const getNewMessagesController = async (req, res) => {
524
531
  messages: messages
525
532
  });
526
533
  } catch (error) {
534
+ logger.error(`Error fetching new messages for ${req.params.phoneNumber}:`, { error });
527
535
  logger.error(`Error fetching new messages for ${req.params.phoneNumber}:`, { error });
528
536
  res.status(500).json({
529
537
  success: false,
@@ -533,7 +541,7 @@ const getNewMessagesController = async (req, res) => {
533
541
  };
534
542
 
535
543
  const markMessagesAsReadController = async (req, res) => {
536
- logger.info('Starting markMessagesAsReadController at', { time: new Date().toISOString() });
544
+ logger.info(`Starting markMessagesAsReadController at ${new Date().toISOString()}`);
537
545
  try {
538
546
  const { phoneNumber } = req.params;
539
547
 
@@ -544,7 +552,7 @@ const markMessagesAsReadController = async (req, res) => {
544
552
  });
545
553
  }
546
554
 
547
- logger.info('Marking messages as read for phone number:', { phoneNumber });
555
+ logger.info(`Marking messages as read for phone number: ${phoneNumber}`);
548
556
  const result = await Message.updateMany(
549
557
  {
550
558
  numero: phoneNumber,
@@ -564,7 +572,7 @@ const markMessagesAsReadController = async (req, res) => {
564
572
  });
565
573
  } catch (error) {
566
574
  logger.error(`Error marking messages as read for ${req.params?.phoneNumber || 'unknown'}:`, { error });
567
- logger.error('Error stack:', { error: error.stack });
575
+ logger.error('Error stack:', { errorStack: error.stack });
568
576
  res.status(500).json({
569
577
  success: false,
570
578
  error: error.message || 'Failed to mark messages as read'
@@ -573,7 +581,7 @@ const markMessagesAsReadController = async (req, res) => {
573
581
  };
574
582
 
575
583
  const sendTemplateToNewNumberController = async (req, res) => {
576
- logger.info('Starting sendTemplateToNewNumberController at', { time: new Date().toISOString() });
584
+ logger.info(`Starting sendTemplateToNewNumberController at ${new Date().toISOString()}`);
577
585
  try {
578
586
  const { phoneNumber, templateId, variables } = req.body;
579
587
 
@@ -618,6 +626,7 @@ const sendTemplateToNewNumberController = async (req, res) => {
618
626
  messageId: message.sid
619
627
  });
620
628
  } catch (error) {
629
+ logger.error('Error sending template to new number:', { error });
621
630
  logger.error('Error sending template to new number:', { error });
622
631
  res.status(500).json({
623
632
  success: false,
@@ -627,14 +636,14 @@ const sendTemplateToNewNumberController = async (req, res) => {
627
636
  };
628
637
 
629
638
  const getOpenAIThreadMessagesController = async (req, res) => {
630
- logger.info('Starting getOpenAIThreadMessagesController at', { time: new Date().toISOString() });
639
+ logger.info(`Starting getOpenAIThreadMessagesController at ${new Date().toISOString()}`);
631
640
  try {
632
641
  const { phoneNumber } = req.params;
633
642
  const { limit = 50, order = 'desc', runId } = req.query;
634
643
  const variant = process.env.VARIANT || 'assistants';
635
644
 
636
- logger.info('Fetching OpenAI thread messages for:', { phoneNumber });
637
- logger.info('Variant:', { variant }, 'Limit:', { limit }, 'Order:', { order });
645
+ logger.info(`Fetching OpenAI thread messages for: ${phoneNumber}`);
646
+ logger.info(`Variant: ${variant}, Limit: ${limit}, Order: ${order}`);
638
647
 
639
648
  if (!phoneNumber) {
640
649
  return res.status(400).json({
@@ -649,7 +658,7 @@ const getOpenAIThreadMessagesController = async (req, res) => {
649
658
  }).sort({ createdAt: -1 });
650
659
 
651
660
  if (!thread) {
652
- logger.info('No active OpenAI thread found for:', { phoneNumber });
661
+ logger.info(`No active OpenAI thread found for: ${phoneNumber}`);
653
662
  return res.status(404).json({
654
663
  success: false,
655
664
  error: 'No active OpenAI thread found for this phone number'
@@ -657,14 +666,14 @@ const getOpenAIThreadMessagesController = async (req, res) => {
657
666
  }
658
667
 
659
668
  let conversationId = thread.conversation_id;
660
- logger.info('Thread found - Conversation ID:', { conversationId });
669
+ logger.info(`Thread found - Conversation ID: ${conversationId}`);
661
670
 
662
671
  const provider = llmConfig.getOpenAIProvider({ instantiate: true, variant });
663
672
  if (!provider) {
664
673
  throw new Error('OpenAI provider not initialized');
665
674
  }
666
675
 
667
- logger.info('Using provider variant:', { variant: provider.getVariant() });
676
+ logger.info(`Using provider variant: ${provider.getVariant()}`);
668
677
 
669
678
  const queryParams = {
670
679
  threadId: conversationId,
@@ -679,6 +688,7 @@ const getOpenAIThreadMessagesController = async (req, res) => {
679
688
  let messages;
680
689
  let threadRecreated = false;
681
690
 
691
+ logger.info('Calling listMessages with params:', { queryParams });
682
692
  logger.info('Calling listMessages with params:', { queryParams });
683
693
  messages = await withThreadRecovery(
684
694
  async (currentThread = thread) => {
@@ -713,7 +723,7 @@ const getOpenAIThreadMessagesController = async (req, res) => {
713
723
 
714
724
  } catch (error) {
715
725
  logger.error('Error fetching OpenAI thread messages:', { error });
716
- logger.error('Error stack:', { error: error.stack });
726
+ logger.error('Error stack:', { errorStack: error.stack });
717
727
  res.status(500).json({
718
728
  success: false,
719
729
  error: error.message || 'Failed to fetch OpenAI thread messages'
@@ -2,31 +2,6 @@ const moment = require('moment-timezone');
2
2
  const { Message } = require('../models/messageModel.js');
3
3
  const { logger } = require('../utils/logger');
4
4
 
5
- const addMessageToThread = async (reply, messagesChat, provider, thread) => {
6
- const threadId = thread.getConversationId();
7
-
8
- if (reply.interactive_type === 'flow') {
9
- logger.info(`[addMessageToThread] Skipping flow message (UI only) - ID: ${reply.message_id}`);
10
- return;
11
- }
12
-
13
- if (reply.origin === 'whatsapp_platform') {
14
- await provider.addMessage({
15
- threadId,
16
- role: 'assistant',
17
- content: messagesChat
18
- });
19
- } else if (reply.origin === 'patient') {
20
- await provider.addMessage({
21
- threadId,
22
- role: 'user',
23
- content: messagesChat
24
- });
25
- }
26
-
27
- logger.info(`[addMessageToThread] Message added - ID: ${reply.message_id}, Thread: ${threadId}, Origin: ${reply.origin}`);
28
- };
29
-
30
5
  const updateMessageRecord = async (reply, thread) => {
31
6
  const threadId = thread.getConversationId();
32
7
 
@@ -125,7 +100,6 @@ async function isRecentMessage(chatId) {
125
100
  }
126
101
 
127
102
  module.exports = {
128
- addMessageToThread,
129
103
  updateMessageRecord,
130
104
  getLastMessages,
131
105
  getLastNMessages,
@@ -3,9 +3,8 @@ const { Thread } = require('../models/threadModel');
3
3
  const { retryWithBackoff } = require('../utils/retryHelper');
4
4
  const {
5
5
  handleFunctionCalls: handleFunctionCallsUtil,
6
- handlePendingFunctionCalls: handlePendingFunctionCallsUtil,
7
- transformToolsForResponsesAPI: transformToolsForResponsesAPIUtil
8
6
  } = require('./OpenAIResponsesProviderTools');
7
+ const { DefaultConversationManager } = require('../services/DefaultConversationManager');
9
8
  const { logger } = require('../utils/logger');
10
9
 
11
10
  const CONVERSATION_PREFIX = 'conv_';
@@ -25,6 +24,7 @@ class OpenAIResponsesProvider {
25
24
  organization,
26
25
  client,
27
26
  defaultModels = {},
27
+ conversationManager,
28
28
  } = options;
29
29
 
30
30
  if (!client && !apiKey) {
@@ -41,6 +41,7 @@ class OpenAIResponsesProvider {
41
41
  };
42
42
 
43
43
  this.variant = 'responses';
44
+ this.conversationManager = conversationManager || new DefaultConversationManager();
44
45
 
45
46
  this.responses = this.client.responses;
46
47
  this.conversations = this.client.conversations;
@@ -161,33 +162,6 @@ class OpenAIResponsesProvider {
161
162
  return await this.client.conversations.items.list(id, { order, limit });
162
163
  }
163
164
 
164
- async cleanupOrphanedFunctionCalls(threadId, deleteAll = false) {
165
- try {
166
- const id = this._ensurethreadId(threadId);
167
- const messages = await this.listMessages({ threadId: id, order: 'desc' });
168
- const items = messages?.data || [];
169
-
170
- if (items.length === 0) return;
171
-
172
- if (deleteAll) {
173
- logger.info(`[OpenAIResponsesProvider] Deleting all ${items.length} items from conversation`);
174
- await Promise.all(items.map(item =>
175
- this.conversations.items.delete(item.id, {conversation_id: id})
176
- ));
177
- return;
178
- }
179
-
180
- const outputCallIds = new Set(items.filter(i => i.type === 'function_call_output').map(o => o.call_id));
181
- const orphanedCalls = items.filter(i => i.type === 'function_call' && !outputCallIds.has(i.call_id));
182
-
183
- if (orphanedCalls.length > 0) {
184
- await Promise.all(orphanedCalls.map(call => this.conversations.items.delete(call.id, {conversation_id: id})));
185
- }
186
- } catch (error) {
187
- logger.warn('[OpenAIResponsesProvider] Failed to cleanup conversation:', error?.message);
188
- }
189
- }
190
-
191
165
  _normalizeThread(thread) {
192
166
  return {
193
167
  conversationId: thread.conversation_id || thread.getConversationId?.(),
@@ -200,107 +174,138 @@ class OpenAIResponsesProvider {
200
174
  */
201
175
  async executeRun({ thread, assistant, tools = [], config = {} }) {
202
176
  const { conversationId, assistantId } = this._normalizeThread(thread);
203
- const preparedConfig = {
204
- ...config,
205
- assistant,
206
- toolMetadata: {
207
- numero: thread.code,
208
- assistant_id: assistantId
209
- }
210
- };
211
-
177
+
212
178
  logger.info('[OpenAIResponsesProvider] Starting run', {
213
179
  conversationId,
214
180
  assistantId
215
181
  });
216
182
 
217
- const filter = thread.code ? { code: thread.code, active: true } : null;
218
- const result = await this.runConversation({
219
- threadId: conversationId,
220
- assistantId,
221
- tools,
222
- ...preparedConfig
223
- });
224
-
225
- const completed = result.status === 'completed';
226
- const output = await this.getRunText({
227
- runId: result.id,
228
- fallback: ''
229
- });
183
+ try {
184
+ // Delegate context building to conversation manager
185
+ const context = await this.conversationManager.buildContext({
186
+ thread,
187
+ assistant,
188
+ config: {
189
+ ...config,
190
+ threadId: conversationId,
191
+ assistantId,
192
+ toolMetadata: {
193
+ numero: thread.code,
194
+ assistant_id: assistantId
195
+ }
196
+ }
197
+ });
230
198
 
231
- if (filter) {
232
- await Thread.updateOne(filter, { $set: { run_id: null } });
233
- }
199
+ const filter = thread.code ? { code: thread.code, active: true } : null;
200
+
201
+ // Get clinical context for prompt variables
202
+ const clinicalData = await this.conversationManager.getClinicalData(thread.code);
203
+ const promptVariables = clinicalData ? {
204
+ clinical_context: clinicalData.clinicalContext || '',
205
+ last_symptoms: clinicalData.lastSymptoms || ''
206
+ } : null;
207
+
208
+ // Execute with built context
209
+ const result = await this.runConversation({
210
+ threadId: conversationId,
211
+ assistantId,
212
+ tools,
213
+ context,
214
+ promptVariables,
215
+ assistant,
216
+ ...config
217
+ });
218
+
219
+ // Delegate response processing to conversation manager
220
+ await this.conversationManager.processResponse(result, thread, config);
221
+
222
+ const completed = result.status === 'completed';
223
+ const output = await this.getRunText({
224
+ runId: result.id,
225
+ fallback: ''
226
+ });
227
+
228
+ if (filter) {
229
+ await Thread.updateOne(filter, { $set: { run_id: null } });
230
+ }
234
231
 
235
- logger.info('[OpenAIResponsesProvider] Run complete', {
236
- runId: result.id,
237
- completed,
238
- toolsExecuted: result.tools_executed?.length || 0
239
- });
232
+ logger.info('[OpenAIResponsesProvider] Run complete', {
233
+ runId: result.id,
234
+ completed,
235
+ toolsExecuted: result.tools_executed?.length || 0
236
+ });
240
237
 
241
- return {
242
- run: result,
243
- completed,
244
- output,
245
- tools_executed: result.tools_executed || [],
246
- retries: result.retries || 0
247
- };
238
+ return {
239
+ run: result,
240
+ completed,
241
+ output,
242
+ tools_executed: result.tools_executed || [],
243
+ retries: result.retries || 0
244
+ };
245
+
246
+ } catch (error) {
247
+ logger.error('[OpenAIResponsesProvider] Execute run failed', {
248
+ conversationId,
249
+ assistantId,
250
+ error: error.message
251
+ });
252
+ throw error;
253
+ }
248
254
  }
249
255
 
250
256
  async runConversation({
251
257
  threadId,
252
258
  assistantId,
253
259
  additionalMessages = [],
260
+ context = null,
254
261
  instructions = null,
255
262
  additionalInstructions = null,
256
- toolOutputs = [],
257
263
  metadata = {},
258
264
  topP,
259
265
  temperature,
260
266
  maxOutputTokens,
261
267
  truncationStrategy = 'auto',
262
- tools = [],
263
268
  model,
264
269
  assistant,
265
270
  toolMetadata,
271
+ promptVariables = null,
266
272
  } = {}) {
267
273
  try {
268
274
  const id = this._ensurethreadId(threadId);
269
- let input = this._convertItemsToApiFormat(additionalMessages);
275
+
276
+ let input = context || this._convertItemsToApiFormat(additionalMessages);
270
277
  let allToolsExecuted = [];
271
278
  let totalRetries = 0;
272
279
 
273
- // Handle pending function calls
274
- if (assistant && toolOutputs.length === 0) {
275
- const conversationMessages = await this.listMessages({ threadId: id, order: 'desc', limit: 50 });
276
- const result = await handlePendingFunctionCallsUtil(assistant, conversationMessages?.data || [], toolMetadata || { thread_id: id, assistant_id: assistantId });
277
- toolOutputs = result.outputs || [];
278
- allToolsExecuted = result.toolsExecuted || [];
279
- }
280
+ input = input.filter(item => item.type !== 'function_call' && item.type !== 'function_call_output');
281
+
282
+ const promptConfig = promptVariables
283
+ ? { id: assistantId, variables: promptVariables }
284
+ : { id: assistantId };
285
+ logger.info('[OpenAIResponsesProvider] Prompt config', { promptConfig });
280
286
 
281
- input = [...input, ...toolOutputs];
282
287
  const makeAPICall = (inputData) => retryWithBackoff(() =>
283
288
  this.client.responses.create({
284
- conversation: id,
285
- prompt: { id: assistantId },
289
+ prompt: promptConfig,
286
290
  model: model || this.defaults.responseModel,
287
291
  instructions: additionalInstructions || instructions,
288
292
  input: inputData,
289
293
  metadata, top_p: topP, temperature, max_output_tokens: maxOutputTokens,
290
294
  truncation: truncationStrategy,
291
- tools: transformToolsForResponsesAPIUtil(this.variant, tools),
292
295
  }), { providerName: PROVIDER_NAME });
293
296
 
294
297
  const { result: response, retries } = await makeAPICall(input);
295
298
  totalRetries += retries;
296
299
  let finalResponse = response;
297
300
 
301
+ // Handle function calls following OpenAI pattern
298
302
  if (assistant && response.output) {
299
303
  const functionCalls = response.output.filter(item => item.type === 'function_call');
300
304
 
301
305
  if (functionCalls.length > 0) {
302
306
  const { outputs, toolsExecuted } = await handleFunctionCallsUtil(functionCalls, assistant, toolMetadata || { thread_id: id, assistant_id: assistantId });
303
307
 
308
+ input.push(...response.output);
304
309
  input.push(...outputs);
305
310
  allToolsExecuted.push(...toolsExecuted);
306
311
 
@@ -5,13 +5,16 @@ async function executeFunctionCall(assistant, call, metadata = {}) {
5
5
  const name = call.name;
6
6
  const args = call.arguments ? JSON.parse(call.arguments) : {};
7
7
 
8
+ logger.info('[executeFunctionCall] Calling tool', { name, args, call_id: call.call_id });
9
+
8
10
  let result, success = true;
9
11
  try {
10
12
  result = await assistant.executeTool(name, args);
13
+ logger.info('[executeFunctionCall] Tool completed', { name, call_id: call.call_id, duration_ms: Date.now() - startTime });
11
14
  } catch (error) {
12
15
  result = { error: error?.message || 'Tool execution failed' };
13
16
  success = false;
14
- logger.error('[OpenAIResponsesProvider] Tool execution failed', { error });
17
+ logger.error('[executeFunctionCall] Tool execution failed', { name, call_id: call.call_id, error: error?.message });
15
18
  }
16
19
 
17
20
  const toolData = {
@@ -1,5 +1,6 @@
1
1
  const { OpenAIAssistantsProvider } = require('./OpenAIAssistantsProvider');
2
2
  const { OpenAIResponsesProvider } = require('./OpenAIResponsesProvider');
3
+ const { DefaultConversationManager } = require('../services/DefaultConversationManager');
3
4
  const { logger } = require('../utils/logger');
4
5
 
5
6
  const PROVIDER_VARIANTS = {
@@ -16,8 +17,17 @@ function createProvider(config = {}) {
16
17
  .toString()
17
18
  .toLowerCase();
18
19
 
20
+ // Create conversation manager if not provided
21
+ const conversationManager = config.conversationManager || new DefaultConversationManager({
22
+ memorySystem: config.memorySystem
23
+ });
24
+
19
25
  const ProviderClass = PROVIDER_VARIANTS[variant] || OpenAIAssistantsProvider;
20
- return new ProviderClass({ ...config, variant });
26
+ return new ProviderClass({
27
+ ...config,
28
+ variant,
29
+ conversationManager
30
+ });
21
31
  }
22
32
 
23
33
  module.exports = {
@@ -0,0 +1,43 @@
1
+ const { logger } = require('../utils/logger');
2
+
3
+ class ConversationManager {
4
+ constructor(options = {}) {
5
+ this.memorySystem = options.memorySystem || null;
6
+ }
7
+
8
+ async buildContext({ thread, assistant, config = {} }) {
9
+ throw new Error('buildContext must be implemented by subclass');
10
+ }
11
+
12
+ async processResponse(response, thread, config = {}) {
13
+ throw new Error('processResponse must be implemented by subclass');
14
+ }
15
+
16
+ setMemorySystem(memorySystem) {
17
+ this.memorySystem = memorySystem;
18
+ }
19
+
20
+ getMemorySystem() {
21
+ return this.memorySystem;
22
+ }
23
+
24
+ optimizeContextWindow(messages, maxTokens = 8000) {
25
+ if (!Array.isArray(messages) || messages.length === 0) return [];
26
+ const systemMessages = messages.filter(m => m.role === 'system');
27
+ const recentMessages = messages.filter(m => m.role !== 'system').slice(-20);
28
+ return [...systemMessages, ...recentMessages];
29
+ }
30
+
31
+ _convertToApiFormat(messages) {
32
+ return messages.map(msg => ({
33
+ role: msg.role || 'user',
34
+ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
35
+ }));
36
+ }
37
+
38
+ _logActivity(action, metadata = {}) {
39
+ logger.info(`[ConversationManager] ${action}`, metadata);
40
+ }
41
+ }
42
+
43
+ module.exports = { ConversationManager };
@@ -0,0 +1,207 @@
1
+ const { ConversationManager } = require('./ConversationManager');
2
+ const { getLastNMessages } = require('../helpers/messageHelper');
3
+ const { handlePendingFunctionCalls: handlePendingFunctionCallsUtil } = require('../providers/OpenAIResponsesProviderTools');
4
+ const { getRecordByFilter } = require('./airtableService');
5
+ const { Follow_Up_ID } = require('../config/airtableConfig');
6
+ const { logger } = require('../utils/logger');
7
+
8
+ class DefaultConversationManager extends ConversationManager {
9
+ constructor(options = {}) {
10
+ super(options);
11
+ this.maxHistoricalMessages = parseInt(process.env.MAX_HISTORICAL_MESSAGES || '50', 10);
12
+ }
13
+
14
+ async buildContext({ thread, assistant, config = {} }) {
15
+ this._logActivity('Building context', { threadCode: thread.code });
16
+
17
+ try {
18
+ const allMessages = await getLastNMessages(thread.code, this.maxHistoricalMessages);
19
+ const additionalMessages = config.additionalMessages || [];
20
+
21
+ // New conversation - no history yet
22
+ if (!allMessages?.length) {
23
+ return additionalMessages;
24
+ }
25
+
26
+ const messageContext = allMessages.reverse().map(msg => ({
27
+ role: msg.origin === 'patient' ? 'user' : 'assistant',
28
+ content: msg.body || msg.content || ''
29
+ }));
30
+
31
+ return [...additionalMessages, ...messageContext];
32
+ } catch (error) {
33
+ logger.error('[DefaultConversationManager] Context building failed', {
34
+ threadCode: thread.code,
35
+ error: error.message
36
+ });
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ async processResponse(response, thread, config = {}) {
42
+ this._logActivity('Processing response', { threadCode: thread.code, responseId: response.id });
43
+
44
+ try {
45
+ if (this.memorySystem && response.output) {
46
+ await this.memorySystem.processResponse(response, thread);
47
+ }
48
+ this._logActivity('Response processed successfully', {
49
+ threadCode: thread.code,
50
+ responseId: response.id
51
+ });
52
+ } catch (error) {
53
+ logger.error('[DefaultConversationManager] Response processing failed', {
54
+ threadCode: thread.code,
55
+ error: error.message
56
+ });
57
+ }
58
+ }
59
+
60
+ async handlePendingFunctionCalls(assistant, conversationMessages, toolMetadata) {
61
+ if (!assistant || !conversationMessages?.length) return { outputs: [], toolsExecuted: [] };
62
+
63
+ try {
64
+ return await handlePendingFunctionCallsUtil(assistant, conversationMessages, toolMetadata);
65
+ } catch (error) {
66
+ logger.error('[DefaultConversationManager] Function call handling failed', { error: error.message });
67
+ return { outputs: [], toolsExecuted: [] };
68
+ }
69
+ }
70
+
71
+ optimizeContextWindow(messages) {
72
+ if (!Array.isArray(messages) || !messages.length) return [];
73
+
74
+ const cappedMessages = messages.length > this.maxHistoricalMessages
75
+ ? messages.slice(-this.maxHistoricalMessages)
76
+ : messages;
77
+
78
+ if (messages.length > this.maxHistoricalMessages) {
79
+ this._logActivity('Context capped', { originalCount: messages.length, cappedCount: this.maxHistoricalMessages });
80
+ }
81
+
82
+ return this._convertToApiFormat(cappedMessages);
83
+ }
84
+
85
+ _convertToApiFormat(items) {
86
+ return items.map(item => ({
87
+ role: item.role || 'user',
88
+ content: this._normalizeContent(item.content),
89
+ type: item.type || 'message',
90
+ }));
91
+ }
92
+
93
+ _normalizeContent(content) {
94
+ if (typeof content === 'string') return content;
95
+ if (Array.isArray(content)) return content;
96
+ if (content?.text) return content.text;
97
+ if (content?.type === 'text' && content.text) return content.text;
98
+ return content && typeof content === 'object' ? JSON.stringify(content) : content || '';
99
+ }
100
+
101
+ async _getClinicalContext(whatsappId) {
102
+ try {
103
+ const [clinicalRecords, symptomsRecords] = await Promise.all([
104
+ getRecordByFilter(Follow_Up_ID, 'estado_general', `{whatsapp_id}='${whatsappId}'`),
105
+ getRecordByFilter(Follow_Up_ID, 'all_triages_last_month', `{whatsapp_id}='${whatsappId}'`)
106
+ ]);
107
+
108
+ let clinicalContext = null;
109
+ if (clinicalRecords && clinicalRecords.length > 0 && clinicalRecords[0]['clinical-context-json']) {
110
+ clinicalContext = JSON.parse(clinicalRecords[0]['clinical-context-json']);
111
+ }
112
+
113
+ const sortedSymptoms = symptomsRecords
114
+ ?.filter(r => r.Date)
115
+ .sort((a, b) => new Date(b.Date) - new Date(a.Date))
116
+ .slice(0, 3) || [];
117
+
118
+ const symptoms = this._parseSymptoms(sortedSymptoms);
119
+
120
+ return { ...clinicalContext, symptoms };
121
+ } catch (error) {
122
+ logger.error('[DefaultConversationManager] Error fetching clinical context', { error: error.message });
123
+ return null;
124
+ }
125
+ }
126
+
127
+ _parseSymptoms(records) {
128
+ if (!records || records.length === 0) return [];
129
+
130
+ return records.map(record => {
131
+ const symptomNames = record['symptom_name (from symptoms)'];
132
+ const grades = record['grade_num (from symptoms)'];
133
+ const recommendations = record['short_recommendations'];
134
+ const date = record['Date'];
135
+
136
+ if (!symptomNames || !grades) return null;
137
+
138
+ const nameArray = typeof symptomNames === 'string'
139
+ ? symptomNames.split(',').map(s => s.trim())
140
+ : Array.isArray(symptomNames) ? symptomNames : [symptomNames];
141
+
142
+ const gradeArray = typeof grades === 'string'
143
+ ? grades.split(',').map(g => parseInt(g.trim()))
144
+ : Array.isArray(grades) ? grades.map(g => parseInt(g)) : [parseInt(grades)];
145
+
146
+ const recArray = recommendations && typeof recommendations === 'string'
147
+ ? recommendations.split('.,').map(r => r.trim())
148
+ : [];
149
+
150
+ const symptoms = nameArray
151
+ .map((name, idx) => ({
152
+ name,
153
+ grade: gradeArray[idx] || 0,
154
+ recommendation: recArray[idx] || ''
155
+ }))
156
+ .filter(s => s.grade > 0);
157
+
158
+ return {
159
+ date,
160
+ symptoms
161
+ };
162
+ }).filter(Boolean);
163
+ }
164
+
165
+ async getClinicalData(whatsappId) {
166
+ try {
167
+ const data = await this._getClinicalContext(whatsappId);
168
+ if (!data) return null;
169
+
170
+ // Format clinical context (demographics + clinical data)
171
+ const contextParts = [];
172
+ if (data.demografia) {
173
+ const d = data.demografia;
174
+ if (d.edad) contextParts.push(`Edad: ${d.edad}`);
175
+ if (d.sexo) contextParts.push(`Sexo: ${d.sexo}`);
176
+ }
177
+ if (data.datos_clinicos) {
178
+ const c = data.datos_clinicos;
179
+ if (c.diagnostico) contextParts.push(`Diagnóstico: ${c.diagnostico}`);
180
+ if (c.tratamiento_actual) contextParts.push(`Tratamiento actual: ${c.tratamiento_actual}`);
181
+ if (c.medicacion_actual) contextParts.push(`Medicación: ${c.medicacion_actual}`);
182
+ if (c.alergias_medicamentos) contextParts.push(`Alergias: ${c.alergias_medicamentos}`);
183
+ if (c.comorbilidades) contextParts.push(`Comorbilidades: ${c.comorbilidades}`);
184
+ }
185
+
186
+ // Format last symptoms
187
+ const symptomParts = [];
188
+ if (data.symptoms?.length > 0) {
189
+ data.symptoms.forEach(triage => {
190
+ const dateStr = new Date(triage.date).toLocaleDateString('es-PE');
191
+ const symptomsStr = triage.symptoms.map(s => `${s.name} (grado ${s.grade})`).join(', ');
192
+ if (symptomsStr) symptomParts.push(`${dateStr}: ${symptomsStr}`);
193
+ });
194
+ }
195
+
196
+ return {
197
+ clinicalContext: contextParts.join('. ') || '',
198
+ lastSymptoms: symptomParts.join('; ') || 'Sin síntomas reportados recientemente'
199
+ };
200
+ } catch (error) {
201
+ logger.error('[DefaultConversationManager] Error getting clinical data', { error: error.message });
202
+ return null;
203
+ }
204
+ }
205
+ }
206
+
207
+ module.exports = { DefaultConversationManager };
@@ -52,7 +52,7 @@ async function getRecordByFilter(baseID, tableName, filter, view = 'Grid view')
52
52
  });
53
53
  return records;
54
54
  } catch (error) {
55
- logger.error(`Error fetching records by ${filter}:`, error);
55
+ logger.error(`Error fetching records from ${tableName} by ${filter}:`, error);
56
56
  }
57
57
  }
58
58
 
@@ -41,15 +41,6 @@ const createAssistantCore = async (code, assistant_id, messages = [], force = fa
41
41
  try {
42
42
  const assistant = getAssistantById(assistant_id, null);
43
43
  const initialThread = await assistant.create(code, curRow[0]);
44
-
45
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
46
- for (const message of messages) {
47
- await provider.addMessage({
48
- threadId: initialThread.id,
49
- role: 'assistant',
50
- content: message
51
- });
52
- }
53
44
 
54
45
  const thread = {
55
46
  code: code,
@@ -76,17 +67,9 @@ const addMsgAssistantCore = async (code, inMessages, role = 'system', reply = fa
76
67
  const thread = await getThread(code);
77
68
  if (!thread) return null;
78
69
 
79
- const provider = createProvider({ variant: process.env.VARIANT || 'assistants' });
80
- const threadId = thread.getConversationId();
81
-
82
70
  try {
83
71
  const messages = Array.isArray(inMessages) ? inMessages : [inMessages];
84
72
 
85
- await provider.addMessage({
86
- threadId,
87
- messages: messages.map(message => ({ role, content: message }))
88
- });
89
-
90
73
  // Save system messages to database for frontend visibility
91
74
  if (!skipSystemMessage) {
92
75
  for (let i = 0; i < messages.length; i++) {
@@ -205,12 +188,6 @@ const replyAssistantCore = async (code, message_ = null, thread_ = null, runOpti
205
188
  const urls = processResults.filter(r => r.url).map(r => ({ url: r.url }));
206
189
  const allMessagesToAdd = processResults.flatMap(r => r.messages || []);
207
190
  const allTempFiles = processResults.flatMap(r => r.tempFiles || []);
208
-
209
- if (allMessagesToAdd.length > 0) {
210
- logger.info(`[replyAssistantCore] Adding ${allMessagesToAdd.length} messages to thread in batch`);
211
- const threadId = finalThread.getConversationId();
212
- await provider.addMessage({ threadId, messages: allMessagesToAdd });
213
- }
214
191
 
215
192
  await Promise.all(processResults.map(r => updateMessageRecord(r.reply, finalThread)));
216
193
  await cleanupFiles(allTempFiles);
@@ -24,6 +24,7 @@ const fetchConversationData = async (filter, skip, limit) => {
24
24
  { $project: { numero: 1, body: 1, createdAt: 1, timestamp: 1, media: 1, nombre_whatsapp: 1, from_me: 1 } },
25
25
  { $sort: { createdAt: 1, timestamp: 1 } },
26
26
  { $group: { _id: '$numero', latestMessage: { $last: '$$ROOT' }, messageCount: { $sum: 1 } } },
27
+ { $match: { _id: { $nin: [null, ''] } } },
27
28
  ...(filter === 'no-response' ? [{ $match: { 'latestMessage.from_me': false } }] : []),
28
29
  {
29
30
  $lookup: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "2.6.0",
3
+ "version": "3.0.0",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",