__import__('pysqlite3') import sys sys.modules['sqlite3'] = sys.modules.pop('pysqlite3') # ============================================================================== # 1. LIBRERIAS # ============================================================================== import streamlit as st import os import time import csv import tempfile import torch import torch.nn as nn import torch.nn.functional as F from torchvision import transforms, models from torchvision.models import efficientnet_b4 from PIL import Image import numpy as np import matplotlib.pyplot as plt import cv2 from crewai import Agent, Task, Crew, Process, LLM from RAG_tool import BuscadorGuiasClinicas import tempfile from fpdf import FPDF import datetime #------------------------------------------------------------------------------- # 1.1 LIBRERIAS RAGAS from datasets import Dataset from ragas import evaluate from ragas.metrics import Faithfulness, AnswerRelevancy from langchain_huggingface import HuggingFaceEmbeddings from langchain_openai import ChatOpenAI # ============================================================================== # 2. CONFIGURACIÓN VISUAL Y MÁRGENES # ============================================================================== st.set_page_config( page_title="DermaRAG - Diagnóstico", page_icon="🏥", layout="wide", initial_sidebar_state="collapsed" ) # ============================================================================== # 3. Inyección de CSS # ============================================================================== st.markdown(""" """, unsafe_allow_html=True) # ============================================================================== # 4. CLASES PARA GRAD-CAM # ============================================================================== class FeatureExtractor: """Clase para 'espiar' lo que ve la red en capas intermedias""" def __init__(self, model, target_layers): self.activations = {} for name, layer in target_layers.items(): layer.register_forward_hook(self.get_hook(name)) def get_hook(self, name): def hook(model, input, output): self.activations[name] = output.detach() return hook class GradCAM: """Clase para mapa de calor de decisión final""" def __init__(self, model, target_layer): self.model = model self.activations = None self.gradients = None target_layer.register_forward_hook(self.save_activation) target_layer.register_full_backward_hook(self.save_gradient) def save_activation(self, module, input, output): self.activations = output def save_gradient(self, module, grad_input, grad_output): self.gradients = grad_output[0] def __call__(self, x): self.model.zero_grad() output = self.model(x) idx = torch.argmax(output, dim=1) output[0, idx].backward() grads = self.gradients.cpu().data.numpy()[0] fmaps = self.activations.cpu().data.numpy()[0] weights = np.mean(grads, axis=(1, 2)) cam = np.zeros(fmaps.shape[1:], dtype=np.float32) for i, w in enumerate(weights): cam += w * fmaps[i] cam = np.maximum(cam, 0) cam = cv2.resize(cam, (380, 380)) cam = (cam - np.min(cam)) / (np.max(cam) + 1e-8) return cam, output, idx def plot_feature_maps(activations, layer_name, title, output_file): """Dibuja las 16 características más activas de una capa""" act = activations[layer_name].squeeze().cpu().numpy() mean_act = np.mean(act, axis=(1, 2)) top_indices = np.argsort(mean_act)[::-1][:16] fig, axes = plt.subplots(4, 4, figsize=(10, 10)) fig.suptitle(title, fontsize=16) for idx, ax in enumerate(axes.flat): if idx < len(top_indices): fmap_idx = top_indices[idx] fmap = act[fmap_idx] fmap = (fmap - np.min(fmap)) / (np.max(fmap) + 1e-8) ax.imshow(fmap, cmap='viridis') ax.set_title(f"Filtro {fmap_idx}", fontsize=8) ax.axis('off') plt.tight_layout() plt.savefig(output_file) plt.close() return output_file # ============================================================================== # 5. LÓGICA DE VISIÓN (Backend + Integración GradCAM) # ============================================================================== @st.cache_resource def cargar_tu_modelo_especifico(ruta_pth): model = efficientnet_b4(weights=None) num_ftrs = model.classifier[1].in_features model.classifier = nn.Sequential( nn.Dropout(p=0.45), nn.Linear(num_ftrs, 3) ) device = torch.device("cuda" if torch.cuda.is_available() else "cpu") try: state_dict = torch.load(ruta_pth, map_location=device) model.load_state_dict(state_dict) except Exception as e: st.error(f"❌ Error cargando pesos: {e}") return None model.to(device) model.eval() return model transformacion_validacion = transforms.Compose([ transforms.Resize((380, 380)), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]) def ejecutar_pipeline_gradcam(modelo, ruta_img, temp_dir): """Función wrapper para ejecutar el flujo del PDF""" feature_extractor = FeatureExtractor(modelo, { 'capa_inicial': modelo.features[0], 'capa_final': modelo.features[-1] }) grad_cam = GradCAM(modelo, modelo.features[-1]) pil_img = Image.open(ruta_img).convert('RGB') device = next(modelo.parameters()).device img_tensor = transformacion_validacion(pil_img).unsqueeze(0).to(device) cam_map, logits, pred_idx = grad_cam(img_tensor) probs = F.softmax(logits, dim=1).cpu().data.numpy()[0] CLASES_NOMBRES = ['Benigno', 'Melanoma', 'Carcinoma'] img_cv = cv2.imread(ruta_img) img_cv = cv2.resize(img_cv, (380, 380)) heatmap = cv2.applyColorMap(np.uint8(255 * cam_map), cv2.COLORMAP_JET) superimposed = cv2.addWeighted(img_cv, 0.6, heatmap, 0.4, 0) plt.figure(figsize=(12, 5)) plt.subplot(1, 3, 1) plt.imshow(cv2.cvtColor(img_cv, cv2.COLOR_BGR2RGB)) plt.title("Original") plt.axis('off') plt.subplot(1, 3, 2) plt.imshow(cv2.cvtColor(superimposed, cv2.COLOR_BGR2RGB)) plt.title(f"Atención IA\n({CLASES_NOMBRES[pred_idx]})") plt.axis('off') plt.subplot(1, 3, 3) bars = plt.bar(CLASES_NOMBRES, probs, color=['green', 'red', 'orange']) plt.title("Probabilidades") plt.ylim(0, 1.15) for bar in bars: altura = bar.get_height() porcentaje = altura * 100 plt.text(bar.get_x() + bar.get_width() / 2.0, altura + 0.02, f'{porcentaje:.1f}%', ha='center', va='bottom', fontsize=10, fontweight='bold') path_diag = os.path.join(temp_dir, "1_diagnostico_clinico.png") plt.savefig(path_diag) plt.close() path_bordes = os.path.join(temp_dir, "2_analisis_bordes.png") plot_feature_maps( feature_extractor.activations, 'capa_inicial', "CARACTERÍSTICAS VISUALES: BORDES Y FORMAS", path_bordes ) path_patrones = os.path.join(temp_dir, "3_analisis_patrones.png") plot_feature_maps( feature_extractor.activations, 'capa_final', "CARACTERÍSTICAS ABSTRACTAS: TEXTURA Y ANOMALÍAS", path_patrones ) return path_diag, path_bordes, path_patrones, CLASES_NOMBRES[pred_idx], probs def analizar_imagen_medica(ruta_imagen, modelo): if modelo is None: return "Error: Modelo no cargado." CLASES = ['Benigno', 'Melanoma', 'Carcinoma'] try: image = Image.open(ruta_imagen).convert('RGB') image = transformacion_validacion(image).unsqueeze(0) device = next(modelo.parameters()).device image = image.to(device) with torch.no_grad(): outputs = modelo(image) probs = torch.nn.functional.softmax(outputs, dim=1) p_ben = probs[0][0].item() * 100 p_mel = probs[0][1].item() * 100 p_car = probs[0][2].item() * 100 clase_idx = torch.argmax(probs, 1).item() clase_predicha = CLASES[clase_idx] confianza_max = probs[0][clase_idx].item() * 100 reporte = ( f"ANÁLISIS DE IA (EfficientNet-B4):\n" f"- Diagnóstico Computacional: {clase_predicha.upper()}\n" f"- Confianza Principal: {confianza_max:.2f}%\n" f"- Desglose de Probabilidades:\n" f" * Benigno: {p_ben:.2f}%\n" f" * Melanoma: {p_mel:.2f}%\n" f" * Carcinoma: {p_car:.2f}%" ) return reporte except Exception as e: return f"Error: {str(e)}" # ============================================================================== # 6. GENERADOR DE PDF CON FORMATO APA PARA MEJOR PRESENTACION # ============================================================================== class PDFReport(FPDF): def __init__(self, paciente_info): super().__init__() self.paciente_info = paciente_info def header(self): self.set_font('Arial', 'B', 15) self.cell(0, 10, 'DermaRAG - Informe Diagnóstico Asistido por IA', 0, 1, 'C') self.set_font('Arial', 'I', 10) self.cell(0, 10, f'Fecha de Emisión: {datetime.date.today()}', 0, 1, 'R') self.line(10, 30, 200, 30) self.ln(10) def footer(self): self.set_y(-15) self.set_font('Arial', 'I', 8) texto_pie = f"Paciente: {self.paciente_info['nombre']} | ID: {self.paciente_info['id']} | Edad: {self.paciente_info['edad']} | Página " + str(self.page_no()) self.cell(0, 10, texto_pie, 0, 0, 'C') def chapter_title(self, label): self.set_font('Arial', 'B', 12) self.set_fill_color(200, 220, 255) self.cell(0, 6, label, 0, 1, 'L', 1) self.ln(4) def chapter_body(self, text): self.set_font('Arial', '', 11) self.multi_cell(0, 5, text) self.ln() # ============================================================================== # 7. INTERFAZ DE USUARIO STREAMLIT # ============================================================================== st.markdown("""

🏥 DermaRAG - Sistema Multiagente de Diagnóstico Dermatológico

IA Explicable con Retrieval-Augmented Generation | Guías AAD/BAD/NCCN

Potenciado por Groq LPU
""", unsafe_allow_html=True) # Validación del token de Groq GROQ_API_KEY = os.environ.get("GROQ_API_KEY") if not GROQ_API_KEY: st.error("⚠️ Falta el token de Groq. Por favor, añade `GROQ_API_KEY` en los *Secrets* de tu Space.") st.stop() RUTA_MODELO = 'mejor_modelo_v5.pth' modelo_cnn = None if os.path.exists(RUTA_MODELO): modelo_cnn = cargar_tu_modelo_especifico(RUTA_MODELO) else: st.error(f"⚠️ Falta el archivo '{RUTA_MODELO}'") col_izq, col_der = st.columns([1, 1], gap="large") with col_izq: with st.container(border=True): st.markdown("## 📋 Datos del Paciente") c1, c2 = st.columns(2) with c1: nombre = st.text_input("Nombre Completo *", value="", placeholder="Ej. Gerardo García Pérez") edad = st.number_input("Edad *", value=0, min_value=0, max_value=120, step=1) fototipo = st.selectbox("Fototipo Fitzpatrick *", ["Tipo I - Piel muy clara, siempre se quema", "Tipo II - Piel clara, usualmente se quema", "Tipo III - Piel intermedia, a veces se quema", "Tipo IV - Piel morena clara, rara vez se quema", "Tipo V - Piel morena, muy rara vez se quema", "Tipo VI - Piel negra, nunca se quema"], index=None, placeholder="Seleccionar opción...") with c2: id_paciente = st.text_input("ID Paciente *", value="", placeholder="Ej. PAC-2025-001") sexo = st.selectbox("Sexo *", ["Masculino", "Femenino", "Otro"], index=None, placeholder="Seleccionar...") with st.container(border=True): st.markdown("## 🔬 Datos Clínicos de la Lesión") localizacion = st.selectbox("Localización Anatómica *", ["Tronco (pecho/espalda)", "Cabeza y Cuello", "Extremidades Superiores", "Extremidades Inferiores", "Manos/Pies (Acral)", "Mucosas"], index=None, placeholder="Seleccionar ubicación...") cc1, cc2 = st.columns(2) tamano = cc1.number_input("Tamaño (mm) *", value=0, min_value=0, step=1) evolucion = cc2.number_input("Evolución (meses)", value=0, min_value=0, step=1) sintomas = st.text_area("Síntomas Asociados", value="", placeholder="Ej. Prurito, sangrado, cambio de color, asimetría...", height=80) historia = st.text_area("Antecedentes Relevantes", value="", placeholder="Ej. Historia familiar de melanoma, exposición solar crónica...", height=80) with st.container(border=True): st.markdown("## 🔎 Criterios ABCDE (Dermoscopia Visual)") col_checks = st.columns(5) check_a = col_checks[0].checkbox("A", value=False, help="Asimetría") check_b = col_checks[1].checkbox("B", value=False, help="Bordes") check_c = col_checks[2].checkbox("C", value=False, help="Color") check_d = col_checks[3].checkbox("D", value=False, help="Diámetro") check_e = col_checks[4].checkbox("E", value=False, help="Evolución") with col_der: with st.container(border=True): st.markdown("## 📸 Imagen de la Lesión Cutánea") uploaded_file = st.file_uploader("Sube o arrastra tu imagen (JPG, PNG | Máx. 10MB)", type=["jpg", "png", "jpeg"]) if uploaded_file: img_temp_pil = Image.open(uploaded_file) w, h = img_temp_pil.size size_kb = uploaded_file.size / 1024 st.success(f"✅ Archivo: {uploaded_file.name} | {size_kb:.2f} KB | {w}x{h} px") col_img_1, col_img_2, col_img_3 = st.columns([1, 2, 1]) with col_img_2: st.image(img_temp_pil, caption="Vista Previa", width=250) st.markdown("
", unsafe_allow_html=True) analyze_btn = st.button("🔍 Analizar con IA Multiagente + GradCAM", use_container_width=True) st.markdown("""
ℹ️ Flujo del Sistema:
1 Agente Percepción: CNN (EfficientNet-B4) + GradCAM
2 Agente Investigación: RAG busca en guías AAD/BAD/NCCN
3 Agente Síntesis: LLM por Groq genera explicación pedagógica
""", unsafe_allow_html=True) # ============================================================================== # 8. LÓGICA DE AGENTES Y EJECUCIÓN # ============================================================================== if analyze_btn: if uploaded_file and modelo_cnn: if not nombre or localizacion is None: st.error("⚠️ Por favor completa al menos: Nombre y Localización.") else: tiempo_inicio_total = time.time() with st.status("🔄 Ejecutando Sistema Multiagente...", expanded=True) as status: temp_dir = tempfile.mkdtemp() with open(os.path.join(temp_dir, "input.jpg"), "wb") as f: f.write(uploaded_file.getvalue()) ruta_input = os.path.join(temp_dir, "input.jpg") st.write("🧠 **Percepción Visual:** Ejecutando EfficientNet-B4 + Grad-CAM...") tiempo_inicio_vision = time.time() path_diag, path_bordes, path_patrones, pred_clase, probs = ejecutar_pipeline_gradcam( modelo_cnn, ruta_input, temp_dir ) resultado_vision = analizar_imagen_medica(ruta_input, modelo_cnn) tiempo_fin_vision = time.time() latencia_vision = tiempo_fin_vision - tiempo_inicio_vision st.write(f"✅ Análisis visual completado en {latencia_vision:.2f} segundos.") # 6. Configuración Agentes vía Groq API st.write("⚕️ **Razonamiento Clínico:** Iniciando agentes CrewAI vía Groq...") llm_agentes = LLM( model="groq/llama-3.3-70b-versatile", api_key=GROQ_API_KEY, temperature=0.3 # CRÍTICO! Esto mata la creatividad y las alucinaciones ) abcde_sel = [] if check_a: abcde_sel.append("Asimetría") if check_b: abcde_sel.append("Bordes Irregulares") if check_c: abcde_sel.append("Policromía") if check_d: abcde_sel.append(f"Diámetro > 6mm ({tamano}mm)") if check_e: abcde_sel.append("Evolución") hallazgos_txt = ", ".join(abcde_sel) if abcde_sel else "Sin hallazgos marcados" edad_val = edad tamano_val = tamano evolucion_val = evolucion sintomas_val = sintomas if sintomas else "No reportados" historia_val = historia if historia else "No reportados" task_med = ( f"DATOS DEL PACIENTE: Paciente Anónimo, {edad_val} años, {sexo if sexo else 'No esp.'}.\n" f"FOTOTIPO: {fototipo if fototipo else 'No esp.'}.\n" f"CLÍNICA: Lesión en {localizacion}, tamaño {tamano_val}mm, {evolucion_val} meses evolución.\n" f"SÍNTOMAS: {sintomas_val}.\n" f"ANTECEDENTES: {historia_val}.\n" f"HALLAZGOS ABCDE MANUALES: {hallazgos_txt}.\n" f"REPORTE DE LABORATORIO IA (EfficientNet-B4): [{resultado_vision}]." ) # ============================================================================== # 9. DEFINICIÓN DE AGENTES Y TAREAS # ============================================================================== medico_atencion_primaria = Agent( role='Auditor Clínico de Inteligencia Artificial', goal=( f'Validar la coherencia entre el mapa de calor (Grad-CAM) y la clínica del paciente. ' f'Detectar alucinaciones de la visión artificial y preparar el caso con terminología médica precisa. ' f'Contexto del caso: {task_med}' ), backstory=( 'Eres un especialista en Diagnóstico por Imagen y Triaje. Tu trabajo NO es tratar, sino ' 'auditar a la máquina. Tienes frente a ti un reporte de "EfficientNet-B4" y mapas de calor "Grad-CAM". ' 'Tu filosofía es: "La IA es una herramienta, no un doctor". ' 'Si la IA dice "Melanoma (90%)" pero el mapa de calor (Grad-CAM) está señalando piel sana o pelo ' 'en lugar de la lesión, DEBES reportarlo como una "Posible Falsa Predicción". ' 'Eres obsesivo con la semiología: transformas "le pica y sangra" en "prurito y ulceración activa".' 'IDIOMA OBLIGATORIO: Redactas EXCLUSIVAMENTE en español académico. ' 'Está terminantemente prohibido usar inglés, incluso para términos técnicos. ' 'Escribes "prurito" no "itching", "ulceración" no "ulceration".' ), verbose=True, allow_delegation=False, llm=llm_agentes ) herramienta_rag = BuscadorGuiasClinicas() investigador_guias = Agent( role='Buscador de Literatura Médica', goal='Usar la herramienta para extraer texto literal de las guías clínicas.', backstory='Eres un asistente de investigación estricto. Tu ÚNICO trabajo es usar la herramienta Buscador de Guías Clínicas. No redactas reportes, solo buscas.', verbose=True, allow_delegation=False, tools=[herramienta_rag], # SOLO ÉL TIENE LA HERRAMIENTA llm=llm_agentes ) redactor_clinico = Agent( role='Oncólogo Redactor', goal='Redactar el reporte final basado ÚNICAMENTE en el texto que encontró el investigador.', backstory='Eres un oncólogo experto. Recibes fragmentos de guías clínicas y los conviertes en un reporte estructurado y profesional en español.', verbose=True, allow_delegation=False, tools=[], # NO TIENE HERRAMIENTAS, NO PUEDE SALTARSE EL PROCESO llm=llm_agentes ) # ============================================================================== # 10. TAREAS PARA AGENTES CREW IA # ============================================================================== task_atencion_primaria = Task( description=f""" Analiza los datos crudos del paciente y el reporte de la visión artificial: {task_med}. REGLA DE ORO DE IDIOMA: TODA TU RESPUESTA DEBE SER 100% EN ESPAÑOL. ESTÁ ESTRICTAMENTE PROHIBIDO USAR INGLÉS. TU OBJETIVO ES REALIZAR UN RESUMEN CLÍNICO CON PRECISIÓN MATEMÁTICA Y SEMIOLÓGICA: 1. **Fidelidad Absoluta a la IA:** - Tu diagnóstico de trabajo DEBE basarse exactamente en la predicción principal de la IA (EfficientNet-B4). - Si la IA marca una probabilidad abrumadora hacia "Carcinoma", tu redacción debe enfocarse en Carcinoma, sin sugerir Melanoma a menos que los datos clínicos sean contradictorios. 2. **Traducción Semiológica y Lenguaje:** - Convierte los síntomas del usuario a lenguaje médico formal y académico en español. - Interpreta de forma lógica el mapa de calor (Grad-CAM). 3. **Síntesis para el Especialista:** - Redacta el estado actual del paciente cruzando el ABCDE manual con el resultado de la red neuronal. """, agent=medico_atencion_primaria, expected_output=""" IDIOMA: TODO EN ESPAÑOL. EJEMPLO CORRECTO: "Ulceración activa con prurito EJEMPLO INCORRECTO (PROHIBIDO): "Active ulceration with itching Reporte de Auditoría de IA (Markdown Español): ### 1. Validación de Visión Artificial - **Diagnóstico Modelo:** [Clase principal dictada por la IA] (Confianza: [X]%). - **Análisis de Atención (Grad-CAM):** [Interpretación clara de la zona de calor en Español]. - **Veredicto de Coherencia:** [Alta/Baja] [Explicación médica en Español]. ### 2. Resumen Semiológico - **Perfil de Riesgo:** [Evaluación basada en fototipo, edad e historia]. - **Hallazgos Clínicos:** [Traducción técnica de los síntomas presentados]. ### 3. Solicitud de Interconsulta - **Pregunta Clave:** [Pregunta específica para el oncólogo sobre el manejo a seguir]. """ ) task_busqueda = Task( description=""" Busca en las guías clínicas el protocolo para el diagnóstico sugerido por el auditor. Debes usar la herramienta 'Buscador de Guías Clínicas' obligatoriamente. Action Input ejemplo: 'tratamiento carcinoma' o 'margen melanoma'. Tu respuesta final debe ser EXCLUSIVAMENTE el texto crudo que te devolvió la herramienta. No des explicaciones. """, agent=investigador_guias, context=[task_atencion_primaria], expected_output="Texto crudo y literal extraído de los documentos médicos PDF." ) task_redaccion = Task( description=""" Lee detenidamente los fragmentos de texto crudo que te entregó el Buscador de Literatura. Redacta el reporte final en español manteniendo una FIDELIDAD EXTREMA a esos fragmentos. Si los fragmentos no mencionan algo, NO lo inventes. Extrae los nombres de los documentos y las páginas reales de los fragmentos que leíste. """, agent=redactor_clinico, context=[task_busqueda], expected_output=""" ### 1. Diagnóstico e Interpretación - **Diagnóstico de Trabajo:** [Tu diagnóstico en español] - **Justificación:** [Justificación clínica en español] ### 2. Protocolo de Manejo (Evidencia Contrastada) - **Manejo Recomendado:** [Instrucción fiel a la guía en español]. [Fuente: Nombre del Documento, Pág: X] - **Márgenes Quirúrgicos:** [Medida extraída de las guías en español]. [Fuente: Nombre del Documento, Pág: X] - **Estudios Complementarios:** [Instrucción extraída en español]. [Fuente: Nombre del Documento, Pág: X] ### 3. Pronóstico y Seguimiento - **Pauta de Revisión:** [Frecuencia extraída en español]. [Fuente: Nombre del Documento, Pág: X] --- ### 📚 Referencias Bibliográficas 1. **[Nombre completo de la Institución/Sociedad Médica]**. ([Año]). *[Título oficial y completo del documento o guía clínica]*. Base de conocimientos DermaRAG. Páginas consultadas: [Número exacto de página]. """ ) # ========================================================== # INICIO DEL PASO 3: INYECCIÓN DE RAGAS # ========================================================== # 0. Limpiar memoria de la sesión ANTES de iniciar los agentes ruta_memoria = os.path.join(tempfile.gettempdir(), "memoria_rag_dermarag.txt") contextos_recuperados = [] if os.path.exists(ruta_memoria): with open(ruta_memoria, "r", encoding="utf-8") as f: texto_guardado = f.read() if texto_guardado.strip(): contextos_recuperados.append(f"GUÍAS CLÍNICAS (RAG):\n{texto_guardado}") # --- 2. 🔥 EL TRUCO: AÑADIR LA REALIDAD CLÍNICA COMO VERDAD --- clase_ia = st.session_state.get('pred_clase', 'No especificado/Pendiente') contexto_paciente = ( f"DATOS CLÍNICOS DEL PACIENTE (Esto es verdad absoluta, no es alucinación):\n" f"Paciente de {edad} años, {fototipo}. Lesión en {localizacion} de {tamano}mm. " f"Diagnóstico de la IA Visual: {clase_ia}." ) contextos_recuperados.append(contexto_paciente) # --- 3. AUDITORÍA VISUAL PARA TI (DEBUG) --- with st.expander("👀 Ver qué documentos está leyendo el Juez RAGas"): st.write(contextos_recuperados) if len(contextos_recuperados) == 1: st.warning("⚠️ Advertencia: Solo se cargó el paciente. El agente no usó el buscador de guías PDF.") crew = Crew( agents=[medico_atencion_primaria, investigador_guias, redactor_clinico], tasks=[task_atencion_primaria, task_busqueda, task_redaccion], verbose=True, process=Process.sequential, language='es', cache=False ) # 1. Ejecutar Agentes resultado_final = crew.kickoff() # ========================================================== # NUEVO: GUARDAR EN MEMORIA EN LUGAR DE EJECUTAR RAGAS # ========================================================== st.session_state['diagnostico_generado'] = True st.session_state['resultado_final'] = resultado_final st.session_state['pred_clase'] = pred_clase st.session_state['probs'] = probs st.session_state['path_diag'] = path_diag st.session_state['path_bordes'] = path_bordes st.session_state['path_patrones'] = path_patrones st.session_state['temp_dir'] = temp_dir st.session_state['ragas_scores'] = None # Reseteamos métricas anteriores tiempo_fin_total = time.time() latencia_total = tiempo_fin_total - tiempo_inicio_total status.update(label=f"✅ Diagnóstico Finalizado en {latencia_total:.2f} segundos", state="complete", expanded=False) # ============================================================================== # 11. HISTORIAL PARA CALCULAR EL PERCENTIL 95 # ============================================================================== archivo_logs = "logs_latencia.csv" if not os.path.exists(archivo_logs): with open(archivo_logs, mode='w', newline='') as file: writer = csv.writer(file) writer.writerow(["Fecha", "ID_Paciente", "Latencia_Vision_seg", "Latencia_Total_seg"]) with open(archivo_logs, mode='a', newline='') as file: writer = csv.writer(file) fecha_actual = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") writer.writerow([fecha_actual, id_paciente, round(latencia_vision, 2), round(latencia_total, 2)]) # ============================================================================== # SECCIÓN VISUAL (INDEPENDIENTE DEL BOTÓN DE ANÁLISIS) # ============================================================================== if st.session_state.get('diagnostico_generado', False): st.markdown("---") st.subheader("👁️ Análisis de Explicabilidad y Evaluación") # ¡Tus pestañas intactas! tab1, tab2, tab3, tab4 = st.tabs([ "Diagnóstico Clínico", "Análisis de Bordes", "Análisis de Patrones", "📊 Auditoría RAGas" ]) with tab1: st.image(st.session_state['path_diag'], caption="Mapa de Atención y Probabilidades", use_container_width=True) with tab2: st.image(st.session_state['path_bordes'], caption="Filtros de Capa Inicial", use_container_width=True) with tab3: st.image(st.session_state['path_patrones'], caption="Filtros de Capa Final", use_container_width=True) with tab4: st.markdown("### 🎯 Auditoría de Respuesta Clínica (RAGas)") st.info("Ejecuta la evaluación a criterio del especialista para validar que la IA no haya alucinado tratamientos.") # 1. EL BOTÓN DE DIAGNÓSTICO PROFUNDO if st.button("🚀 Ejecutar Auditoría Clínica (RAGas)", use_container_width=True): with st.spinner("⚖️ El Juez está auditando y generando tabla cruda..."): try: llm_juez = ChatOpenAI( api_key=GROQ_API_KEY, base_url="https://api.groq.com/openai/v1", model="llama-3.3-70b-versatile", temperature=0 ) embeddings_juez = HuggingFaceEmbeddings(model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2") # --- LEER EL ARCHIVO FÍSICO EN LUGAR DE SESSION_STATE --- contextos_recuperados = [] if os.path.exists("memoria_rag.txt"): with open("memoria_rag.txt", "r", encoding="utf-8") as f: texto_guardado = f.read() if texto_guardado.strip(): contextos_recuperados.append(texto_guardado) # Si el archivo no existe o está vacío if not contextos_recuperados: contextos_recuperados = ["El agente no utilizó el buscador de guías o no se encontró contexto."] st.warning("⚠️ Advertencia: El RAG no devolvió contexto. Esto forzará la métrica de Fidelidad a cero, pero ya no dará error.") resultado_texto = st.session_state['resultado_final'].raw if hasattr(st.session_state['resultado_final'], 'raw') else str(st.session_state['resultado_final']) pregunta_clinica = ( f"Paciente de {edad} años con {fototipo}, lesión tipo {st.session_state['pred_clase']} " f"de {tamano}mm en {localizacion}. " f"¿Cuál es el protocolo de manejo, márgenes quirúrgicos y seguimiento recomendado?" ) datos_consulta = { "question": [pregunta_clinica], "contexts": [[c for c in contextos_recuperados]], "answer": [resultado_texto] } dataset_actual = Dataset.from_dict(datos_consulta) resultado_ragas = evaluate( dataset=dataset_actual, metrics=[Faithfulness(), AnswerRelevancy(strictness=1)], llm=llm_juez, embeddings=embeddings_juez, raise_exceptions=True ) df_ragas = resultado_ragas.to_pandas() st.write("📊 **Datos Crudos devueltos por RAGas:**") st.dataframe(df_ragas) def safe_score(col_name): import math for col in df_ragas.columns: if col_name.lower() in col.lower(): val = df_ragas[col][0] return 0.0 if math.isnan(val) else val return 0.0 st.session_state['ragas_scores'] = { 'faithfulness': safe_score('faithfulness'), 'relevancy': safe_score('relevancy') } except Exception as e: st.error(f"🚨 ERROR FATAL AL EJECUTAR RAGAS: {e}") st.session_state['ragas_scores'] = None # 2. MOSTRAR RESULTADOS (Alineado perfectamente con el 'if st.button' de arriba) if st.session_state.get('ragas_scores') is not None: st.success("✅ Auditoría completada sin errores de sistema.") col_r1, col_r2 = st.columns(2) def formatear_score(score): color = "green" if score > 0.8 else "orange" if score > 0.6 else "red" return f"{score:.2f} / 1.0" with col_r1: with st.container(border=True): st.markdown("**Fidelidad Clínica (Faithfulness)**") st.markdown(formatear_score(st.session_state['ragas_scores']['faithfulness']), unsafe_allow_html=True) st.caption("Mide si la respuesta se deriva 100% de las guías médicas.") with col_r2: with st.container(border=True): st.markdown("**Relevancia (Answer Relevance)**") st.markdown(formatear_score(st.session_state['ragas_scores']['relevancy']), unsafe_allow_html=True) st.caption("Mide si la respuesta aborda directamente los síntomas y diagnóstico.") # --- INFORME CLÍNICO --- st.markdown("### 📊 Informe Clínico Multiagente") with st.container(border=True): st.markdown(st.session_state['resultado_final']) st.markdown("
⚠️ AVISO MÉDICO-LEGAL: Este sistema es una herramienta de apoyo y NO reemplaza el juicio clínico.
", unsafe_allow_html=True) # --- DESCARGA PDF --- paciente_data = {'nombre': nombre, 'id': id_paciente, 'edad': f"{edad} años"} pdf = PDFReport(paciente_data) pdf.add_page() pdf.chapter_title("1. Resumen Clínico e Imágenes") pdf.chapter_body(f"Paciente: {nombre}\nDiagnóstico IA: {st.session_state['pred_clase']} (Confianza: {np.max(st.session_state['probs'])*100:.2f}%)") pdf.image(st.session_state['path_diag'], x=10, y=None, w=190) pdf.ln(5) pdf.chapter_title("2. Informe Detallado de Agentes") texto_limpio = str(st.session_state['resultado_final']).replace('**', '').replace('### ', '\n\n').encode('latin-1', 'replace').decode('latin-1') pdf.chapter_body(texto_limpio) pdf_path = os.path.join(st.session_state['temp_dir'], "reporte_dermarag.pdf") pdf.output(pdf_path) with open(pdf_path, "rb") as f: st.download_button("📄 Exportar Reporte PDF (Formato APA)", data=f, file_name=f"Reporte_{id_paciente}.pdf", mime="application/pdf") # ============================================================================== # 12. SECCIÓN "OCULTA" / ADMINISTRATIVA: DESCARGA DE LOGS DE LATENCIA # ============================================================================= archivo_logs = "logs_latencia.csv" if os.path.exists(archivo_logs): with st.sidebar: st.markdown("### 📊 Panel de Administración") st.write("Descarga los registros de tiempo para calcular el Percentil 95.") with open(archivo_logs, "rb") as f: st.download_button( label="📥 Descargar Logs de Latencia (CSV)", data=f, file_name="historial_latencia_dermarag.csv", mime="text/csv" ) st.markdown("
DermaRAG MVP v1.5 | Desarrollado con Mixtral 8x7B + EfficientNet-B4
", unsafe_allow_html=True)