aarnal80 commited on
Commit
12a7975
·
verified ·
1 Parent(s): f33e4aa

Upload analysisModule.js

Browse files
Files changed (1) hide show
  1. js/analysisModule.js +50 -235
js/analysisModule.js CHANGED
@@ -1,241 +1,56 @@
1
- // js/labAnalysisModule.js
2
-
3
- // Importar funciones/datos necesarios de otros módulos
4
- import { getIaConfig, llmProviders } from './iaConfigModule.js';
5
-
6
- /**
7
- * Construye el prompt para que la IA analice los resultados de laboratorio.
8
- * (Sin cambios en esta función)
9
- * @param {string} rawLabText - El texto crudo pegado por el usuario.
10
- * @returns {string} El prompt formateado como string.
11
- */
12
- function buildLabAnalysisPrompt(rawLabText) {
13
- // --- INICIO PROMPT LABORATORIO ---
14
- const prompt = `Quiero que me ayudes a ordenar resultados de análisis médicos. Te proporcionaré datos de entrada desordenados y debes extraer los datos relevantes siguiendo el formato XLabs. El formato XLabs se define como seis categorías en este orden exacto, cada una en una línea continua:
15
-
16
- Hematología
17
- Coagulación
18
- Bioquímica
19
- Gasometría
20
- Orina
21
- Otras pruebas
22
-
23
- La salida debe estar en español, formateada así:
24
- Categoría: Parametro Resultado unidades | Parametro Resultado unidades | ...
25
-
26
- Ejemplo de Entrada:
27
- Glucosa * 171 mg/dL 74 - 109
28
- Urea * 56 mg/dL 17 - 48
29
- Creatinina 0.82 mg/dL 0.7
30
-
31
- Ejemplo de Salida Esperada:
32
- Bioquímica: Glucosa **171** mg/dL | Urea **56** mg/dL | Creatinina 0.82 mg/dL
33
-
34
- Ajusta el formato con estas modificaciones específicas:
35
- Hematología:
36
- Incluye únicamente: Hematíes, Hemoglobina, Hematocrito, VCM, HCM.
37
- Para elementos celulares (Leucocitos, Neutrófilos, Linfocitos, Monocitos, etc.): agrupa mostrando primero el porcentaje y, entre paréntesis, el valor absoluto. Ejemplo: Neutrofilos 49.1 % (4.03 10^9/L) | Linfocitos 39.8 % (3.27 10^9/L).
38
- No incluyas Eosinófilos ni Basófilos si están dentro de los rangos normales.
39
- Incluye Plaquetas, pero NO incluyas VPM.
40
-
41
- Coagulación:
42
- Resume los datos usando abreviaturas: "INR" para Ratio normalizado internacional, "PT" para Tiempo de protrombina (Quick), "PTT" para T. tromboplastina parcial activada.
43
-
44
- Otras pruebas:
45
- Si no hay elementos en una categoría (ej., "Otras pruebas: Ninguna"), OMITE completamente esa categoría en la salida. No escribas "Ninguna".
46
-
47
- **MUY IMPORTANTE: Alerta de valores alterados:**
48
- Si CUALQUIER parámetro está fuera del rango normal (alterado), **DEBES envolver el valor numérico resultado entre DOBLES ASTERISCOS**. Ejemplo: Glucosa **171** mg/dL. Asegúrate de aplicar esto consistentemente a TODOS y CADA UNO de los valores que identifiques como alterados según los rangos de referencia si están disponibles en la entrada, o si están marcados explícitamente como alterados (ej. con '*'). Si no puedes determinar si está alterado, no lo marques.
49
-
50
- Reglas generales:
51
- 1. Usa normas gramaticales y mayúsculas correctas (ej., "Hematíes" no "HEMATIES").
52
- 2. Si un apartado (Categoría) no tiene ninguna prueba que mostrar después de aplicar las reglas, omite el apartado completo.
53
- 3. NO uses formato bold (negrita) ni cambios de tamaño de texto en tu respuesta, EXCEPTO los dobles asteriscos (**valor**) para marcar los valores alterados.
54
- 4. NO incluyas introducciones, conclusiones, ni frases extra (ej., evita decir "Aquí está el formato XLabs...", "Resultados formateados:", etc.).
55
- 5. Proporciona ÚNICAMENTE la respuesta directa con los resultados formateados como se especifica.
56
- 6. Sigue estrictamente TODAS estas instrucciones. No inventes ningún resultado.
57
-
58
- Datos de laboratorio para analizar:
59
- --- INICIO DATOS ---
60
- ${rawLabText}
61
- --- FIN DATOS ---`;
62
- // --- FIN PROMPT LABORATORIO ---
63
- return prompt;
64
- }
65
-
66
- /**
67
- * Llama a la API del LLM configurado para analizar el texto de resultados de laboratorio.
68
- * (Sin cambios en esta función)
69
- * @param {string} text - El texto crudo de los resultados de laboratorio.
70
- * @returns {Promise<string>} Una promesa que resuelve con la respuesta formateada de la IA como string.
71
- */
72
- export async function analyzeLabResults(text) {
73
- // --- INICIO VALIDACIÓN ENTRADA Y CONFIGURACIÓN ---
74
- if (!text || !text.trim()) {
75
- console.warn("[labAnalysisModule] analyzeLabResults llamada con texto vacío.");
76
- return Promise.resolve("");
77
- }
78
- console.log("[labAnalysisModule] Iniciando análisis de exámenes...");
79
- const config = getIaConfig();
80
- if (!config || !config.llm || !config.llm.provider || !config.llm.model) {
81
- throw new Error("Configuración del LLM incompleta. Por favor, revisa la Configuración IA.");
82
- }
83
- const provider = config.llm.provider;
84
- const model = config.llm.model;
85
- const apiKey = config.llm.apiKeys?.[provider];
86
- if (!apiKey) {
87
- throw new Error(`No se encontró API Key para el proveedor LLM '${provider}'. Revisa la Configuración IA.`);
88
- }
89
- const providerDetails = llmProviders.find(p => p.value === provider);
90
- if (!providerDetails || !providerDetails.url) {
91
- throw new Error(`Detalles (URL) no encontrados para el proveedor LLM '${provider}'. Revisa la lista de proveedores en iaConfigModule.js.`);
92
- }
93
- // --- FIN VALIDACIÓN ENTRADA Y CONFIGURACIÓN ---
94
-
95
- // --- INICIO PREPARACIÓN LLAMADA API ---
96
- let apiUrl;
97
- if (provider === 'openai' || provider === 'deepseek') {
98
- apiUrl = `${providerDetails.url}/v1/chat/completions`;
99
  } else {
100
- console.error(`[labAnalysisModule] Proveedor LLM '${provider}' no tiene un endpoint definido.`);
101
- throw new Error(`Proveedor LLM '${provider}' no soportado actualmente por labAnalysisModule.`);
102
- }
103
- const systemMessage = "Eres un asistente experto en formatear resultados de análisis de laboratorio médicos siguiendo el formato XLabs y destacando valores alterados.";
104
- const userPrompt = buildLabAnalysisPrompt(text);
 
 
 
105
  let messages;
106
  if (provider === 'openai') {
107
- messages = [{ role: 'user', content: `${systemMessage}\n\n${userPrompt}` }];
 
 
 
108
  } else {
109
- messages = [{ role: 'system', content: systemMessage }, { role: 'user', content: userPrompt }];
110
- }
111
- const payload = { model: model, messages: messages, temperature: 0.2 };
112
- console.log(`[labAnalysisModule] Enviando petición a ${apiUrl} con modelo ${model}`);
113
- // --- FIN PREPARACIÓN LLAMADA API ---
114
-
115
- // --- INICIO EJECUCIÓN LLAMADA API Y MANEJO RESPUESTA ---
116
- try {
117
- const response = await fetch(apiUrl, {
118
- method: 'POST',
119
- headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
120
- body: JSON.stringify(payload)
121
- });
122
- if (!response.ok) {
123
- const errorBody = await response.text();
124
- console.error(`[labAnalysisModule] Error API ${response.status}: ${errorBody}`);
125
- throw new Error(`Error ${response.status} de la API LLM: ${errorBody || response.statusText}`);
126
- }
127
- const data = await response.json();
128
- const content = data.choices?.[0]?.message?.content;
129
- if (!content) {
130
- console.error("[labAnalysisModule] Respuesta inesperada de la API (sin contenido):", data);
131
- throw new Error("La respuesta de la API no contiene el contenido esperado.");
132
- }
133
- console.log("[labAnalysisModule] Análisis de exámenes completado exitosamente.");
134
- return content.trim();
135
- } catch (error) {
136
- console.error("[labAnalysisModule] Fallo en la llamada API o procesamiento:", error);
137
- throw error; // Relanzar para manejo en main.js
138
- }
139
- // --- FIN EJECUCIÓN LLAMADA API Y MANEJO RESPUESTA ---
140
- }
141
-
142
-
143
- // --- INICIO: Función displayLabResults (MODIFICADA con filtro mejorado) ---
144
- /**
145
- * Muestra los resultados de laboratorio formateados en la UI, aplicando el filtro mejorado si se indica.
146
- * @param {string} resultsText - El texto formateado recibido de la IA.
147
- * @param {HTMLElement} containerElement - El elemento del DOM donde mostrar los resultados.
148
- * @param {boolean} filterAltered - `true` si se debe mostrar solo resultados alterados y sus cabeceras.
149
- */
150
- export function displayLabResults(resultsText, containerElement, filterAltered) {
151
- // --- INICIO VALIDACIÓN CONTENEDOR ---
152
- if (!containerElement) {
153
- console.error("[labAnalysisModule] El contenedor para mostrar resultados no fue encontrado en el DOM.");
154
- return;
155
- }
156
- containerElement.innerHTML = ''; // Limpiar resultados anteriores
157
- // --- FIN VALIDACIÓN CONTENEDOR ---
158
-
159
- // --- INICIO MANEJO RESULTADOS VACÍOS ---
160
- if (!resultsText || !resultsText.trim()) {
161
- containerElement.textContent = 'No se generaron resultados o la respuesta estaba vacía.';
162
- return;
163
- }
164
- // --- FIN MANEJO RESULTADOS VACÍOS ---
165
-
166
- // --- INICIO LÓGICA DE FILTRADO Y VISUALIZACIÓN ---
167
- const lines = resultsText.split('\n').filter(line => line.trim() !== ''); // Dividir y quitar líneas vacías
168
- let linesToDisplay = lines; // Por defecto, mostrar todas las líneas
169
-
170
- // Aplicar filtro MEJORADO si está activo
171
- if (filterAltered) {
172
- console.log("[labAnalysisModule] Aplicando filtro de alterados (mejorado).");
173
- const filteredLines = [];
174
- let currentHeader = null; // Guarda la última cabecera encontrada
175
- let currentAlteredResults = []; // Guarda resultados alterados bajo la cabecera actual
176
- let headerAdded = false; // Flag para saber si ya añadimos la cabecera actual
177
-
178
- for (const line of lines) {
179
- const trimmedLine = line.trim();
180
- const isHeader = trimmedLine.endsWith(':'); // Identifica cabeceras (ej: "Hematología:")
181
- const hasAlteration = line.includes('**'); // Identifica líneas con alteración
182
-
183
- if (isHeader) {
184
- // Al encontrar una NUEVA cabecera, procesamos la anterior si tuvo resultados alterados
185
- if (currentHeader && currentAlteredResults.length > 0) {
186
- // La sección anterior sí tenía alteraciones, la añadimos completa
187
- // (la cabecera ya se añadió cuando se encontró la primera alteración)
188
- filteredLines.push(...currentAlteredResults);
189
- }
190
- // Reiniciamos para la nueva categoría
191
- currentHeader = line; // Guardamos la nueva cabecera
192
- currentAlteredResults = []; // Limpiamos la lista de resultados alterados
193
- headerAdded = false; // Reseteamos el flag para la nueva cabecera
194
- } else if (hasAlteration) {
195
- // Es una línea de resultado Y está alterada
196
- if (currentHeader && !headerAdded) {
197
- // Si es la PRIMERA alteración que encontramos para esta cabecera, añadimos la cabecera PRIMERO
198
- filteredLines.push(currentHeader);
199
- headerAdded = true; // Marcamos que la cabecera ya fue añadida
200
- }
201
- // Añadimos la línea alterada a la lista temporal (si no hay cabecera, se añade igual por si acaso)
202
- currentAlteredResults.push(line);
203
- }
204
- // Las líneas de resultado NO alteradas simplemente se ignoran cuando el filtro está activo
205
- }
206
-
207
- // Procesar la última categoría después de salir del bucle
208
- if (currentHeader && currentAlteredResults.length > 0) {
209
- // Si no se añadió la última cabecera (porque la única alteración fue la última línea)
210
- if (!headerAdded) {
211
- filteredLines.push(currentHeader);
212
- }
213
- filteredLines.push(...currentAlteredResults);
214
- }
215
-
216
- // Si después de filtrar no queda NADA, mostrar un mensaje
217
- if (filteredLines.length === 0) {
218
- linesToDisplay = ["No se encontraron resultados marcados como alterados."];
219
- } else {
220
- linesToDisplay = filteredLines; // Usar las líneas filtradas
221
- }
222
- }
223
-
224
- // --- INICIO FORMATEO Y RENDERIZADO ---
225
- // (Esta parte no cambia, solo opera sobre `linesToDisplay`)
226
- const fragment = document.createDocumentFragment();
227
- linesToDisplay.forEach(line => {
228
- const p = document.createElement('p');
229
- p.style.margin = '0 0 0.3em 0'; // Pequeño margen inferior
230
- // Reemplazar **valor** por <strong> con estilo para resaltado rojo/negrita
231
- p.innerHTML = line.replace(
232
- /\*\*(.*?)\*\*/g, // Regex: captura texto entre ** (no greedy)
233
- '<strong class="text-red-600 font-bold">$1</strong>' // $1 es el texto capturado
234
- );
235
- fragment.appendChild(p);
236
  });
237
- containerElement.appendChild(fragment); // Añadir al DOM
238
- // --- FIN FORMATEO Y RENDERIZADO ---
239
- // --- FIN LÓGICA DE FILTRADO Y VISUALIZACIÓN ---
 
 
 
240
  }
241
- // --- FIN: Función displayLabResults ---
 
1
+ import { llmProviders } from './iaConfigModule.js';
2
+
3
+ // Módulo de análisis médico con LLM configurado (llamadas front-end)
4
+ export async function analyzeMedical(text) {
5
+ if (!text) return '';
6
+ const cfg = JSON.parse(localStorage.getItem('iaConfig')) || {};
7
+ const provider = cfg.llm.provider;
8
+ const provObj = llmProviders.find(p => p.value === provider) || {};
9
+ // Obtener API key adecuada del proveedor
10
+ const apiKey = cfg.llm.apiKeys?.[provider] ?? cfg.llm.apiKey;
11
+ const model = cfg.llm.model;
12
+ // Determinar endpoint según proveedor
13
+ let url;
14
+ if (provider === 'openai') {
15
+ url = 'https://api.openai.com/v1/chat/completions';
16
+ } else if (provider === 'deepseek') {
17
+ url = 'https://api.deepseek.com/v1/chat/completions';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  } else {
19
+ throw new Error('Proveedor no soportado');
20
+ }
21
+ const systemMessage = 'Eres un médico experto especializado en generar informes clínicos concisos y estructurados.';
22
+ const userPrompt = `Te daré la transcripción detallada de mi conversación con la paciente y escribe una descripción detallada de la enfermedad actual y la exploración física de un paciente en contexto clínico, siguiendo estas características:\n
23
+ Enfermedad actual:\n- Incluye la edad, el género y el motivo de consulta del paciente. (si no te doy el dato, omite).\n- Detalla evolución de síntomas y su progresión.\n- Describe signos y antecedentes relevantes con lenguaje técnico comprensible.\n
24
+ Exploración física:\n- Describe hallazgos objetivos observados en la exploración.\n- Usa términos médicos precisos, sin juicios diagnósticos.\n
25
+ Tareas del modelo:\n- Responde en dos párrafos, sin títulos 'Enfermedad actual:' ni 'Exploración física:'.\n- El primero para la enfermedad actual.\n- El segundo para la exploración.\n
26
+ Transcripción: ${text}`;
27
  let messages;
28
  if (provider === 'openai') {
29
+ // Some OpenAI models don't support 'system' role
30
+ messages = [
31
+ { role: 'user', content: `${systemMessage}\n\n${userPrompt}` }
32
+ ];
33
  } else {
34
+ messages = [
35
+ { role: 'system', content: systemMessage },
36
+ { role: 'user', content: userPrompt }
37
+ ];
38
+ }
39
+ // Enviar solicitud directa al proveedor
40
+ const headers = {
41
+ 'Content-Type': 'application/json',
42
+ 'Authorization': `Bearer ${apiKey}`
43
+ };
44
+ const payload = { model, messages, temperature: 0.5 };
45
+ const res = await fetch(url, {
46
+ method: 'POST',
47
+ headers,
48
+ body: JSON.stringify(payload)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  });
50
+ if (!res.ok) {
51
+ const err = await res.text();
52
+ throw new Error(`Error en análisis médico: ${res.status} ${err}`);
53
+ }
54
+ const data = await res.json();
55
+ return data.choices?.[0]?.message?.content || '';
56
  }