@peopl-health/nexus 5.44.0-dev.6241 → 5.44.0-dev.6258

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.
@@ -13,7 +13,8 @@ const UNSUPPORTED_RESULT = {
13
13
  medical_analysis: 'NOT_MEDICAL',
14
14
  medical_relevance: false,
15
15
  has_table: false,
16
- table_data: null
16
+ table_data: null,
17
+ measurements: null
17
18
  };
18
19
 
19
20
  const MIME_MAP = {
@@ -23,22 +24,35 @@ const MIME_MAP = {
23
24
 
24
25
  const SUPPORTED_MIMES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
25
26
 
26
- const ANALYSIS_MODEL = 'claude-sonnet-4-5-20250929';
27
+ const CONFIDENCE_LEVELS = ['high', 'medium', 'low'];
28
+
29
+ const DISPLAYED_NUMBER = /^-?\d+(?:[.,]\d+)?$/;
30
+
31
+ const ANALYSIS_MODEL = 'claude-sonnet-5';
27
32
  const MAX_ANALYSIS_TOKENS = 4096;
28
33
 
29
- const ANALYSIS_PROMPT = `Eres un oncólogo clínico con experiencia. Recibirás una imagen que un paciente envió por WhatsApp. Puede ser una receta, un resultado de laboratorio, un estudio de imagen, una nota clínica, la caja de un medicamento, o algo sin relación médica.
34
+ const ANALYSIS_PROMPT = `Eres un oncólogo clínico con experiencia. Recibirás una imagen que un paciente envió por WhatsApp. Puede ser una receta, un resultado de laboratorio, un estudio de imagen, una nota clínica, la pantalla de un aparato de medición casero, la caja de un medicamento, o algo sin relación médica.
30
35
 
31
36
  Responde ÚNICAMENTE con un objeto JSON, sin texto antes ni después y sin bloque de código, con esta forma exacta:
32
37
 
33
38
  {
34
- "doc_type": "prescription" | "lab_result" | "imaging" | "clinical_note" | "other" | "not_medical",
39
+ "doc_type": "prescription" | "lab_result" | "imaging" | "clinical_note" | "device_reading" | "other" | "not_medical",
35
40
  "quality": "clear" | "unclear" | "incomplete",
36
41
  "medical_relevance": true | false,
37
42
  "transcription": "<todo el texto legible de la imagen, transcrito tal cual, incluida la letra manuscrita; si la imagen no tiene texto legible, describe en su lugar lo que se ve>",
43
+ "measurements": [{ "label": "<la etiqueta impresa junto al número: SYS, DIA, PUL, mg/dL, SpO2, °C...>", "value": "<el número tal cual>", "unit": "<la unidad impresa, o null>", "confidence": "high" | "medium" | "low" }] | null,
38
44
  "table_md": "<la tabla en markdown>" | null,
39
45
  "reading": "<tu lectura clínica, en el formato que corresponde al doc_type>"
40
46
  }
41
47
 
48
+ Usa "doc_type": "device_reading" cuando la imagen sea la pantalla de un aparato de medición casero (tensiómetro, glucómetro, oxímetro, termómetro, báscula).
49
+
50
+ Reglas para "measurements": inclúyelo solo cuando doc_type sea "device_reading"; en cualquier otro caso usa null.
51
+ - Es una pantalla LCD de segmentos: los segmentos apagados se ven como sombras grises tenues. Cuenta únicamente los segmentos oscuros y bien contrastados.
52
+ - Incluye una entrada por cada número que tenga una etiqueta impresa al lado. Copia la etiqueta tal como aparece en el aparato.
53
+ - NUNCA conviertas en medición la hora, la fecha, el número de memoria ni el índice de usuario. Esos datos van en "transcription", no aquí.
54
+ - Si no estás seguro de algún dígito, marca esa entrada con "confidence": "low". No adivines.
55
+
42
56
  Reglas para "table_md": inclúyela solo si la imagen contiene una verdadera tabla de filas y columnas. Transcríbela completa, conservando el orden de las columnas, todas las unidades y todos los rangos de referencia tal como aparecen. Verifica la alineación vertical de cada valor con el nombre de su parámetro antes de escribirlo. Si no hay tabla, usa null; nunca expliques por qué no la hay.
43
57
 
44
58
  Reglas para "reading" según "doc_type":
@@ -52,6 +66,7 @@ Reglas para "reading" según "doc_type":
52
66
  - hallazgos clave y anomalías; estructuras normales relevantes; medidas críticas si aplican
53
67
  3. Action Items:
54
68
  - estudios o proyecciones adicionales recomendadas; limitaciones de la evaluación
69
+ - device_reading: escribe UNA sola línea que nombre el aparato y, si son legibles, la fecha y la hora que muestra la pantalla. NO repitas los valores medidos: esos ya van en "measurements". No los interpretes ni des recomendaciones; la lectura clínica la hace el agente. Ejemplo: "Tensiómetro digital de muñeca Neutek; la pantalla marca el 24/08 a las 5:54 PM."
55
70
  - clinical_note / other: resume la información clínica que contiene el documento.
56
71
  - not_medical: escribe exactamente NOT_MEDICAL.
57
72
 
@@ -109,6 +124,31 @@ const parseAnalysis = (text) => {
109
124
  }
110
125
  };
111
126
 
127
+ const isText = (value) => typeof value === 'string' && value.trim() !== '';
128
+
129
+ const toMeasurement = (entry) => {
130
+ if (!entry || typeof entry !== 'object') return null;
131
+ const { label, value, unit, confidence } = entry;
132
+ if (!isText(label)) return null;
133
+ if (unit !== null && unit !== undefined && typeof unit !== 'string') return null;
134
+
135
+ const displayed = String(value ?? '').trim();
136
+ if (!DISPLAYED_NUMBER.test(displayed)) return null;
137
+
138
+ return {
139
+ label: label.trim(),
140
+ value: displayed,
141
+ unit: isText(unit) ? unit.trim() : null,
142
+ confidence: CONFIDENCE_LEVELS.includes(confidence) ? confidence : 'low'
143
+ };
144
+ };
145
+
146
+ const toMeasurements = (raw) => {
147
+ if (!Array.isArray(raw) || !raw.length) return null;
148
+ const measurements = raw.map(toMeasurement);
149
+ return measurements.every(Boolean) ? measurements : null;
150
+ };
151
+
112
152
  const toResult = (analysis, rawText) => {
113
153
  if (!analysis) return { ...UNSUPPORTED_RESULT, description: rawText, medical_analysis: '' };
114
154
 
@@ -116,12 +156,20 @@ const toResult = (analysis, rawText) => {
116
156
  const reading = typeof analysis.reading === 'string' ? analysis.reading.trim() : '';
117
157
  const transcription = typeof analysis.transcription === 'string' ? analysis.transcription.trim() : '';
118
158
 
159
+ const isDevice = analysis.doc_type === 'device_reading';
160
+ const measurements = isDevice ? toMeasurements(analysis.measurements) : null;
161
+
162
+ let medicalAnalysis = reading;
163
+ if (analysis.doc_type === 'not_medical') medicalAnalysis = 'NOT_MEDICAL';
164
+ else if (isDevice && !measurements) medicalAnalysis = 'QUALITY_INSUFFICIENT';
165
+
119
166
  return {
120
167
  description: transcription,
121
- medical_analysis: analysis.doc_type === 'not_medical' ? 'NOT_MEDICAL' : reading,
168
+ medical_analysis: medicalAnalysis,
122
169
  medical_relevance: analysis.medical_relevance === true,
123
170
  has_table: table !== null,
124
- table_data: table
171
+ table_data: table,
172
+ measurements
125
173
  };
126
174
  };
127
175
 
@@ -40,6 +40,16 @@ const processTextMessage = (reply) => {
40
40
  return [{ type: 'text', text: reply.body }];
41
41
  };
42
42
 
43
+ const renderMeasurements = (measurements) => {
44
+ const values = measurements
45
+ .map(({ label, value, unit }) => [label, value, unit].filter(Boolean).join(' '))
46
+ .join(' - ');
47
+ const unsure = measurements.filter(({ confidence }) => confidence !== 'high').map(({ label }) => label);
48
+ const line = `Valores leídos en pantalla: ${values}.`;
49
+ if (!unsure.length) return line;
50
+ return `${line}\nLectura dudosa en ${unsure.join(', ')}: pídele al paciente que confirme ese número antes de darlo por bueno.`;
51
+ };
52
+
43
53
  const processImageFileCore = async (fileName, reply) => {
44
54
  const messagesChat = [];
45
55
  const timings = { analysis_ms: 0, url_generation_ms: 0 };
@@ -62,6 +72,7 @@ const processImageFileCore = async (fileName, reply) => {
62
72
  timings.analysis_ms = analysisDuration;
63
73
 
64
74
  const invalidAnalysis = ['NOT_MEDICAL', 'QUALITY_INSUFFICIENT'];
75
+ const unusableAnalysis = invalidAnalysis.some(tag => imageAnalysis?.medical_analysis?.includes(tag));
65
76
 
66
77
  if (imageAnalysis?.medical_relevance && !isSticker) {
67
78
  const { result: presignedUrl, duration: urlDuration } = await withTracing(
@@ -74,11 +85,13 @@ const processImageFileCore = async (fileName, reply) => {
74
85
  }
75
86
 
76
87
  const parts = [];
88
+ if (!isSticker && !unusableAnalysis && imageAnalysis?.measurements?.length) {
89
+ parts.push(renderMeasurements(imageAnalysis.measurements));
90
+ }
77
91
  if (!isSticker && imageAnalysis?.has_table && imageAnalysis.table_data) {
78
92
  parts.push(imageAnalysis.table_data);
79
93
  }
80
- if (!isSticker && imageAnalysis?.medical_analysis &&
81
- !invalidAnalysis.some(tag => imageAnalysis.medical_analysis.includes(tag))) {
94
+ if (!isSticker && imageAnalysis?.medical_analysis && !unusableAnalysis) {
82
95
  parts.push(imageAnalysis.medical_analysis);
83
96
  }
84
97
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.44.0-dev.6241",
3
+ "version": "5.44.0-dev.6258",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",