@peopl-health/nexus 2.4.6 → 2.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,12 +3,13 @@ const fs = require('fs');
3
3
  const mime = require('mime-types');
4
4
 
5
5
 
6
- async function analyzeImage(imagePath) {
6
+ async function analyzeImage(imagePath, isSticker = false) {
7
7
  try {
8
8
  const anthropicClient = llmConfig.anthropicClient;
9
9
  if (!anthropicClient || !anthropicClient.messages) {
10
10
  console.warn('[llmsHelper] Anthropics client not configured; skipping image analysis');
11
11
  return {
12
+ description: 'Image could not be analyzed - Anthropic client not configured',
12
13
  medical_analysis: 'QUALITY_INSUFFICIENT',
13
14
  medical_relevance: false,
14
15
  has_table: false,
@@ -16,10 +17,11 @@ async function analyzeImage(imagePath) {
16
17
  };
17
18
  }
18
19
 
19
- // Skip WBMP images and stickers
20
- if (imagePath.toLowerCase().includes('.wbmp') || imagePath.toLowerCase().includes('sticker')) {
21
- console.log('Skipping WBMP image or sticker analysis:', imagePath);
20
+ // Skip only WBMP images (unsupported format)
21
+ if (imagePath.toLowerCase().includes('.wbmp')) {
22
+ console.log('Skipping WBMP image analysis:', imagePath);
22
23
  return {
24
+ description: 'Unsupported image format',
23
25
  medical_analysis: 'NOT_MEDICAL',
24
26
  medical_relevance: false,
25
27
  has_table: false,
@@ -29,9 +31,10 @@ async function analyzeImage(imagePath) {
29
31
 
30
32
  // Check MIME type
31
33
  const mimeType = mime.lookup(imagePath) || 'image/jpeg';
32
- if (mimeType === 'image/vnd.wap.wbmp' || mimeType.includes('sticker')) {
34
+ if (mimeType === 'image/vnd.wap.wbmp') {
33
35
  console.log('Skipping image with MIME type:', mimeType);
34
36
  return {
37
+ description: 'Unsupported image format',
35
38
  medical_analysis: 'NOT_MEDICAL',
36
39
  medical_relevance: false,
37
40
  has_table: false,
@@ -42,7 +45,7 @@ async function analyzeImage(imagePath) {
42
45
  const imageBuffer = await fs.promises.readFile(imagePath);
43
46
  const base64Image = imageBuffer.toString('base64');
44
47
 
45
- // Descrciption of the image
48
+ // Description of the image (for both stickers and regular images)
46
49
  const imageDescription = 'Describe the image in detail.';
47
50
  const messageDescription = await anthropicClient.messages.create({
48
51
  model: 'claude-sonnet-4-5',
@@ -69,6 +72,17 @@ async function analyzeImage(imagePath) {
69
72
  });
70
73
  const description = messageDescription.content[0].text;
71
74
 
75
+ // For stickers, skip medical analysis and table extraction
76
+ if (isSticker) {
77
+ return {
78
+ description: description,
79
+ medical_analysis: 'NOT_MEDICAL',
80
+ medical_relevance: false,
81
+ has_table: false,
82
+ table_data: null
83
+ };
84
+ }
85
+
72
86
  // Create a more specific prompt for table detection and extraction
73
87
  const tablePrompt = `Please analyze this image and respond in the following format:
74
88
  1. First, determine if there is a table in the image.
@@ -61,13 +61,18 @@ const processImageFile = async (fileName, reply) => {
61
61
  let url = null;
62
62
  const messagesChat = [];
63
63
 
64
+ const isSticker = reply.media?.mediaType === 'sticker' ||
65
+ fileName.toLowerCase().includes('sticker/') ||
66
+ fileName.toLowerCase().includes('/sticker/');
67
+
64
68
  try {
65
- imageAnalysis = await analyzeImage(fileName);
69
+ imageAnalysis = await analyzeImage(fileName, isSticker);
66
70
 
67
71
  logger.info('processImageFile', {
68
72
  message_id: reply.message_id,
69
73
  bucketName: reply.media?.bucketName,
70
74
  key: reply.media?.key,
75
+ is_sticker: isSticker,
71
76
  medical_relevance: imageAnalysis?.medical_relevance,
72
77
  has_table: imageAnalysis?.has_table,
73
78
  analysis_type: imageAnalysis?.medical_analysis ? 'medical' : 'general'
@@ -77,18 +82,18 @@ const processImageFile = async (fileName, reply) => {
77
82
 
78
83
  const invalidAnalysis = ['NOT_MEDICAL', 'QUALITY_INSUFFICIENT'];
79
84
 
80
- // Generate presigned URL if medically relevant
81
- if (imageAnalysis?.medical_relevance) {
85
+ // Generate presigned URL only if medically relevant AND not a sticker
86
+ if (imageAnalysis?.medical_relevance && !isSticker) {
82
87
  url = await generatePresignedUrl(reply.media.bucketName, reply.media.key);
83
88
  }
84
89
 
85
90
  // Add appropriate text based on analysis
86
- if (imageAnalysis?.has_table && imageAnalysis.table_data) {
91
+ if (!isSticker && imageAnalysis?.has_table && imageAnalysis.table_data) {
87
92
  messagesChat.push({
88
93
  type: 'text',
89
94
  text: imageAnalysis.table_data,
90
95
  });
91
- } else if (imageAnalysis?.medical_analysis && !invalidAnalysis.some(tag => imageAnalysis.medical_analysis.includes(tag))) {
96
+ } else if (!isSticker && imageAnalysis?.medical_analysis && !invalidAnalysis.some(tag => imageAnalysis.medical_analysis.includes(tag))) {
92
97
  messagesChat.push({
93
98
  type: 'text',
94
99
  text: imageAnalysis.medical_analysis,
@@ -102,7 +107,8 @@ const processImageFile = async (fileName, reply) => {
102
107
  } catch (error) {
103
108
  logger.error('processImageFile', error, {
104
109
  message_id: reply.message_id,
105
- fileName: fileName ? fileName.split('/').pop() : 'unknown'
110
+ fileName: fileName ? fileName.split('/').pop() : 'unknown',
111
+ is_sticker: isSticker
106
112
  });
107
113
  messagesChat.push({
108
114
  type: 'text',
@@ -175,8 +181,8 @@ const processMediaFiles = async (code, reply, provider) => {
175
181
  fileName: safeFileName
176
182
  });
177
183
 
178
- // Skip unsupported file types
179
- if (fileName.toLowerCase().includes('.wbmp') || fileName.toLowerCase().includes('sticker')) {
184
+ // Skip only WBMP files (unsupported format)
185
+ if (fileName.toLowerCase().includes('.wbmp')) {
180
186
  logger.info('processMediaFiles_skip', {
181
187
  message_id: reply.message_id,
182
188
  fileName: safeFileName,
@@ -185,7 +191,15 @@ const processMediaFiles = async (code, reply, provider) => {
185
191
  continue;
186
192
  }
187
193
 
188
- if (fileName.includes('image') || fileName.includes('document') || fileName.includes('application')) {
194
+ // Process images, documents, applications, and stickers as images
195
+ const isImageLike = fileName.includes('image') ||
196
+ fileName.includes('document') ||
197
+ fileName.includes('application') ||
198
+ reply.media?.mediaType === 'sticker' ||
199
+ fileName.toLowerCase().includes('sticker/') ||
200
+ fileName.toLowerCase().includes('/sticker/');
201
+
202
+ if (isImageLike) {
189
203
  const { messagesChat: imageMessages, url: imageUrl } = await processImageFile(fileName, reply);
190
204
  messagesChat.push(...imageMessages);
191
205
  if (imageUrl) url = imageUrl;
@@ -7,7 +7,7 @@ const {
7
7
  } = require('./twilioHelper');
8
8
  const { validateMedia } = require('../utils/mediaValidator');
9
9
  const { generatePresignedUrl } = require('../config/awsConfig');
10
- const { addRecord, getRecordByFilter } = require('../services/airtableService');
10
+ const { addLinkedRecord } = require('../services/airtableService');
11
11
  const { Monitoreo_ID } = require('../config/airtableConfig');
12
12
 
13
13
  const ensureLogger = (logger) => {
@@ -24,6 +24,34 @@ const normalizeMediaType = (type) => {
24
24
  return type.replace(/Message$/i, '').replace(/WithCaption$/i, '').toLowerCase();
25
25
  };
26
26
 
27
+ async function uploadMediaToAirtable(whatsappId, mediaUrl, log, baseID, tableName) {
28
+ if (!baseID) {
29
+ log.debug && log.debug('[uploadMediaToAirtable] Base ID not configured; skipping Airtable upload');
30
+ return;
31
+ }
32
+
33
+ try {
34
+ await addLinkedRecord(
35
+ baseID,
36
+ tableName,
37
+ {
38
+ estudios: [{ url: mediaUrl }],
39
+ combined_estudios: [{ url: mediaUrl }]
40
+ },
41
+ {
42
+ referenceTable: 'estado_general',
43
+ referenceFilter: `whatsapp_id = "${whatsappId}"`,
44
+ linkFieldName: 'patient_id'
45
+ }
46
+ );
47
+
48
+ log.debug && log.debug('[uploadMediaToAirtable] Successfully uploaded media to estudios table', { whatsappId });
49
+ } catch (error) {
50
+ log.warn && log.warn('[uploadMediaToAirtable] Failed to upload media to estudios table', { whatsappId, error: error?.message || error });
51
+ throw error;
52
+ }
53
+ }
54
+
27
55
  async function processTwilioMediaMessage(twilioMessage, logger, bucketName) {
28
56
  if (!twilioMessage) return [];
29
57
 
@@ -117,16 +145,8 @@ async function processTwilioMediaMessage(twilioMessage, logger, bucketName) {
117
145
  const url = await generatePresignedUrl(bucketName, key);
118
146
  item.metadata.presignedUrl = url;
119
147
 
120
- if (Monitoreo_ID) {
121
- const patient = await getRecordByFilter(Monitoreo_ID, 'estado_general', `whatsapp_id = "${code}"`);
122
- const patientId = Array.isArray(patient) && patient.length > 0 ? [patient[0].recordID || patient[0].recordId || patient[0].record_id] : [];
123
- await addRecord(Monitoreo_ID, 'estudios', [{
124
- fields: {
125
- estudios: [{ url }],
126
- combined_estudios: [{ url }],
127
- patient_id: patientId
128
- }
129
- }]);
148
+ if (derivedMediaType !== 'sticker') {
149
+ await uploadMediaToAirtable(code, url, log, Monitoreo_ID, 'estudios');
130
150
  }
131
151
  } catch (error) {
132
152
  log.warn && log.warn('[TwilioMedia] Failed to update Airtable with media reference', { index: i, error: error?.message || error });
@@ -78,9 +78,41 @@ async function updateRecordByFilter(baseID, tableName, filter, updateFields) {
78
78
  }
79
79
  }
80
80
 
81
+
82
+ async function addLinkedRecord(baseID, targetTable, fields, linkConfig) {
83
+ if (!baseID) {
84
+ throw new Error('[addLinkedRecord] Base ID is required');
85
+ }
86
+
87
+ try {
88
+ if (linkConfig) {
89
+ const { referenceTable, referenceFilter, linkFieldName } = linkConfig;
90
+
91
+ const base = airtable.base(baseID);
92
+ const referenceRecords = await base(referenceTable).select({
93
+ filterByFormula: referenceFilter
94
+ }).firstPage();
95
+
96
+ if (linkFieldName && referenceRecords && referenceRecords.length > 0) {
97
+ const recordId = referenceRecords[0].id;
98
+ if (recordId) {
99
+ fields[linkFieldName] = [recordId];
100
+ }
101
+ }
102
+ }
103
+
104
+ const record = await addRecord(baseID, targetTable, [{ fields }]);
105
+ return record;
106
+ } catch (error) {
107
+ console.error('[addLinkedRecord] Error adding linked record:', error);
108
+ throw error;
109
+ }
110
+ }
111
+
81
112
  module.exports = {
82
113
  addRecord,
83
114
  getRecords,
84
115
  getRecordByFilter,
85
- updateRecordByFilter
116
+ updateRecordByFilter,
117
+ addLinkedRecord
86
118
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "2.4.6",
3
+ "version": "2.4.7",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",