@peopl-health/nexus 3.0.7 → 3.1.1

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.
@@ -46,8 +46,8 @@ const reportBugController = async (req, res) => {
46
46
 
47
47
  if (!reporter) return res.status(400).json({ success: false, error: 'Reporter username is required' });
48
48
  if (!whatsapp_id) return res.status(400).json({ success: false, error: 'WhatsApp ID is required' });
49
- if (!severity || !['low', 'medium', 'high'].includes(severity)) {
50
- return res.status(400).json({ success: false, error: 'Severity must be low, medium, or high' });
49
+ if (!severity || !['low', 'medium', 'high', 'critical'].includes(severity)) {
50
+ return res.status(400).json({ success: false, error: 'Severity must be low, medium, high, or critical' });
51
51
  }
52
52
 
53
53
  logBugReportToAirtable(reporter, whatsapp_id, description, severity, messages).catch(err =>
@@ -1,27 +1,28 @@
1
1
  const { Message } = require('../models/messageModel');
2
2
  const { logger } = require('../utils/logger');
3
+ const { handle24HourWindowError } = require('./templateRecoveryHelper');
3
4
 
4
5
  /**
5
6
  * Update message delivery status in the database based on Twilio status callback data
6
7
  */
7
8
  async function updateMessageStatus(messageSid, status, errorCode = null, errorMessage = null) {
8
9
  try {
9
- const statusInfo = {
10
- status,
11
- updatedAt: new Date()
10
+ const updateData = {
11
+ 'statusInfo.status': status,
12
+ 'statusInfo.updatedAt': new Date()
12
13
  };
13
14
 
14
15
  if (errorCode) {
15
- statusInfo.errorCode = errorCode;
16
+ updateData['statusInfo.errorCode'] = errorCode;
16
17
  }
17
18
 
18
19
  if (errorMessage) {
19
- statusInfo.errorMessage = errorMessage;
20
+ updateData['statusInfo.errorMessage'] = errorMessage;
20
21
  }
21
22
 
22
23
  const updated = await Message.findOneAndUpdate(
23
24
  { message_id: messageSid },
24
- { $set: { statusInfo } },
25
+ { $set: updateData },
25
26
  { new: true }
26
27
  );
27
28
 
@@ -62,12 +63,20 @@ async function handleStatusCallback(twilioStatusData) {
62
63
  return null;
63
64
  }
64
65
 
65
- return await updateMessageStatus(
66
+ const updated = await updateMessageStatus(
66
67
  MessageSid,
67
68
  MessageStatus.toLowerCase(),
68
69
  ErrorCode || null,
69
70
  ErrorMessage || null
70
71
  );
72
+
73
+ if ((ErrorCode === 63016 || ErrorCode === '63016') && updated) {
74
+ handle24HourWindowError(updated, MessageSid).catch(err =>
75
+ logger.error('[MessageStatus] Recovery error', { messageSid: MessageSid, error: err.message })
76
+ );
77
+ }
78
+
79
+ return updated;
71
80
  }
72
81
 
73
82
  /**
@@ -147,9 +147,41 @@ const processAudioFileCore = async (fileName, provider) => {
147
147
  };
148
148
 
149
149
  try {
150
+ const fileExtension = fileName.split('.').pop().toLowerCase();
151
+ const needsConversion = fileExtension === 'ogg'; // Convert OGG for OpenAI compatibility
152
+ let transcriptionFile = fileName;
153
+ let convertedFile = null;
154
+
155
+ if (needsConversion) {
156
+ logger.info('[processAudioFile] Converting OGG to MP3 for transcription', {
157
+ originalFile: fileName ? fileName.split('/').pop().replace(/^[^-]+-[^-]+-/, 'xxx-xxx-') : 'unknown',
158
+ format: fileExtension
159
+ });
160
+
161
+ convertedFile = fileName.replace('.ogg', '_converted.mp3');
162
+
163
+ try {
164
+ const { execSync } = require('child_process');
165
+ execSync(`ffmpeg -i "${fileName}" -acodec mp3 -ab 128k "${convertedFile}" -y`, {
166
+ stdio: 'pipe'
167
+ });
168
+ transcriptionFile = convertedFile;
169
+
170
+ logger.info('[processAudioFile] Conversion successful', {
171
+ convertedFile: convertedFile.split('/').pop().replace(/^[^-]+-[^-]+-/, 'xxx-xxx-')
172
+ });
173
+ } catch (conversionError) {
174
+ logger.error('[processAudioFile] Conversion failed, attempting with original file', {
175
+ error: conversionError.message,
176
+ originalFile: fileName ? fileName.split('/').pop().replace(/^[^-]+-[^-]+-/, 'xxx-xxx-') : 'unknown'
177
+ });
178
+ transcriptionFile = fileName;
179
+ }
180
+ }
181
+
150
182
  const { result: audioTranscript, duration: transcribeDuration } = await withTracing(
151
183
  async () => provider.transcribeAudio({
152
- file: fs.createReadStream(fileName),
184
+ file: fs.createReadStream(transcriptionFile),
153
185
  responseFormat: 'text',
154
186
  language: 'es'
155
187
  }),
@@ -157,6 +189,17 @@ const processAudioFileCore = async (fileName, provider) => {
157
189
  () => ({ 'audio.file_name': fileName ? fileName.split('/').pop().replace(/^[^-]+-[^-]+-/, 'xxx-xxx-') : 'unknown' }),
158
190
  { returnTiming: true }
159
191
  )();
192
+
193
+ if (convertedFile && fs.existsSync(convertedFile)) {
194
+ try {
195
+ fs.unlinkSync(convertedFile);
196
+ logger.debug('[processAudioFile] Cleaned up converted file');
197
+ } catch (cleanupError) {
198
+ logger.warn('[processAudioFile] Failed to cleanup converted file', {
199
+ error: cleanupError.message
200
+ });
201
+ }
202
+ }
160
203
  timings.transcribe_ms = transcribeDuration;
161
204
 
162
205
  const transcriptText = audioTranscript?.text || audioTranscript;
@@ -0,0 +1,72 @@
1
+ const { logger } = require('../utils/logger');
2
+ const { getDefaultInstance } = require('../core/NexusMessaging');
3
+ const { Template, configureNexusProvider: configureTemplateProvider } = require('../templates/templateStructure');
4
+ const { sendMessage } = require('../core/NexusMessaging');
5
+ const { Message } = require('../models/messageModel');
6
+
7
+ /**
8
+ * Handle 24-hour window error by creating template and sending when approved
9
+ */
10
+ async function handle24HourWindowError(message, messageSid) {
11
+ try {
12
+ if (!message?.body || !message?.numero) return;
13
+
14
+ const messaging = getDefaultInstance();
15
+ const provider = messaging?.getProvider();
16
+ if (!provider?.createTemplate) return;
17
+
18
+ configureTemplateProvider(provider);
19
+
20
+ const templateName = `auto_${Date.now()}_${Math.floor(Math.random() * 1000)}`;
21
+ const template = new Template(templateName, 'UTILITY', 'es');
22
+ template.setBody(message.body, []);
23
+ const twilioContent = await template.save();
24
+
25
+ // Submit for approval
26
+ const approvalName = `${templateName}_${Date.now()}`.toLowerCase().replace(/[^a-z0-9_]/g, '_');
27
+ await provider.submitForApproval(twilioContent.sid, approvalName, 'UTILITY');
28
+
29
+ await Message.updateOne(
30
+ { message_id: messageSid },
31
+ { $set: { 'statusInfo.recoveryTemplateSid': twilioContent.sid } }
32
+ );
33
+
34
+ logger.info('[TemplateRecovery] Template created', { messageSid, templateSid: twilioContent.sid });
35
+
36
+ const checkApproval = async (attempt = 0, maxAttempts = 40) => {
37
+ if (attempt >= maxAttempts) {
38
+ logger.warn('[TemplateRecovery] Max attempts reached, template not approved yet', { messageSid, templateSid: twilioContent.sid });
39
+ return;
40
+ }
41
+
42
+ setTimeout(async () => {
43
+ try {
44
+ const status = await provider.checkApprovalStatus(twilioContent.sid);
45
+ const approvalStatus = status?.approvalRequest?.status?.toUpperCase();
46
+
47
+ if (approvalStatus === 'APPROVED') {
48
+ await sendMessage({ code: message.numero, contentSid: twilioContent.sid, variables: {} });
49
+ logger.info('[TemplateRecovery] Template sent', { messageSid, templateSid: twilioContent.sid });
50
+ } else if (approvalStatus === 'REJECTED') {
51
+ logger.warn('[TemplateRecovery] Template rejected', { messageSid, templateSid: twilioContent.sid });
52
+ } else {
53
+ checkApproval(attempt + 1, maxAttempts);
54
+ }
55
+ } catch (err) {
56
+ logger.error('[TemplateRecovery] Error checking approval', { error: err.message, attempt });
57
+ // Retry on error (but count as attempt)
58
+ if (attempt + 1 < maxAttempts) {
59
+ checkApproval(attempt + 1, maxAttempts);
60
+ }
61
+ }
62
+ }, 15 * 60 * 1000);
63
+ };
64
+
65
+ checkApproval(0);
66
+
67
+ } catch (error) {
68
+ logger.error('[TemplateRecovery] Error', { messageSid, error: error.message });
69
+ }
70
+ }
71
+
72
+ module.exports = { handle24HourWindowError };
@@ -124,6 +124,9 @@ async function insertMessage(values) {
124
124
  updatedAt: values.delivery_status_updated_at || null
125
125
  };
126
126
  }
127
+ if (messageData.tools_executed === undefined) {
128
+ messageData.tools_executed = [];
129
+ }
127
130
 
128
131
  await Message.findOneAndUpdate(
129
132
  { message_id: values.message_id, body: values.body },
@@ -200,7 +200,7 @@ class OpenAIResponsesProvider {
200
200
  logger.info('[OpenAIResponsesProvider] Context built', {
201
201
  conversationId,
202
202
  assistantId,
203
- context
203
+ lastContext: context[-1] || null
204
204
  });
205
205
 
206
206
  const filter = thread.code ? { code: thread.code, active: true } : null;
@@ -39,6 +39,8 @@ const messageRouteDefinitions = {
39
39
  'GET /last': 'getLastInteractionController',
40
40
  'GET /scheduled-status': 'checkScheduledMessageStatusController',
41
41
  'GET /status': 'checkMessageStatusController',
42
+ 'GET /status/:messageSid': 'getMessageStatusController',
43
+ 'POST /status-callback': 'messageStatusCallbackController',
42
44
  'POST /quality': 'addQualityVoteController',
43
45
  'GET /quality/:message_id': 'getQualityVotesByMessageController',
44
46
  'GET /quality/:message_id/voter/:voter_username': 'getQualityVoteByMessageAndVoterController',
@@ -97,6 +99,7 @@ const conversationController = require('../controllers/conversationController');
97
99
  const interactionController = require('../controllers/interactionController');
98
100
  const mediaController = require('../controllers/mediaController');
99
101
  const messageController = require('../controllers/messageController');
102
+ const messageStatusController = require('../controllers/messageStatusController');
100
103
  const patientController = require('../controllers/patientController');
101
104
  const qualityMessageController = require('../controllers/qualityMessageController');
102
105
  const templateController = require('../controllers/templateController');
@@ -144,6 +147,8 @@ const builtInControllers = {
144
147
  getLastInteractionController: messageController.getLastInteractionController,
145
148
  checkScheduledMessageStatusController: messageController.checkScheduledMessageStatusController,
146
149
  checkMessageStatusController: messageController.checkMessageStatusController,
150
+ getMessageStatusController: messageStatusController.getMessageStatusController,
151
+ messageStatusCallbackController: messageStatusController.messageStatusCallbackController,
147
152
  addQualityVoteController: qualityMessageController.addQualityVoteController,
148
153
  getQualityVotesByMessageController: qualityMessageController.getQualityVotesByMessageController,
149
154
  getQualityVoteByMessageAndVoterController: qualityMessageController.getQualityVoteByMessageAndVoterController,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "3.0.7",
3
+ "version": "3.1.1",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",