DermaRAG-DEMO / app.py
MGC1991MF's picture
Update app.py
86c6e71 verified
__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("""
<style>
/* AJUSTE DE MÁRGENES */
.block-container {
padding-top: 3rem;
padding-bottom: 5rem;
padding-left: 5rem;
padding-right: 5rem;
max-width: 80% !important;
}
/* Fondo General Claro */
.stApp {
background-color: #f4f6f9;
color: #333333;
}
/* Header Principal */
.header-container {
background: linear-gradient(135deg, #003366 0%, #004080 100%);
padding: 30px;
border-radius: 12px;
color: white;
text-align: center;
margin-bottom: 30px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.header-container h1, .header-container p {
color: white !important;
border-bottom: none !important;
}
/* Paneles / Recuadros (Containers de Streamlit) */
div[data-testid="stVerticalBlockBorderWrapper"] {
background-color: #ffffff !important;
border-radius: 12px !important;
padding: 20px !important;
border: 1px solid #e0e0e0 !important;
box-shadow: 0 4px 10px rgba(0,0,0,0.05) !important;
}
/* Títulos */
h1, h2, h3, h4, h5 {
color: #003366 !important;
}
h2 {
border-bottom: 2px solid #667eea;
padding-bottom: 8px;
margin-bottom: 20px !important;
}
/* ---------------------------------------------------
REPARACIÓN DE COMPONENTES REBELDES DE STREAMLIT
--------------------------------------------------- */
/* 1. Todos los Text Inputs, Text Areas y Select Boxes */
.stTextInput input, .stTextArea textarea, .stSelectbox div[data-baseweb="select"] {
background-color: #ffffff !important;
color: #333333 !important;
border: 1px solid #cccccc !important;
}
/* 2. REPARACIÓN: Inputs Numéricos (Edad, Tamaño, Evolución) */
.stNumberInput div[data-baseweb="input"] {
background-color: #ffffff !important;
border: 1px solid #cccccc !important;
}
.stNumberInput input {
background-color: #ffffff !important;
color: #333333 !important;
}
/* Botones de + y - en los inputs numéricos */
.stNumberInput button {
background-color: #f0f2f6 !important;
color: #333333 !important;
}
/* 3. REPARACIÓN: File Uploader (Área de Drag & Drop) */
[data-testid="stFileUploaderDropzone"] {
background-color: #f8f9fa !important;
border: 2px dashed #667eea !important;
}
[data-testid="stFileUploaderDropzone"] section,
[data-testid="stFileUploaderDropzone"] div,
[data-testid="stFileUploaderDropzone"] span {
color: #333333 !important;
}
/* Botón "Browse Files" del Uploader */
[data-testid="stFileUploaderDropzone"] button {
background-color: #ffffff !important;
color: #003366 !important;
border: 1px solid #003366 !important;
}
/* 4. REPARACIÓN: Textos de Checkboxes (Criterios ABCDE) y Etiquetas */
.stCheckbox label p, .stCheckbox label span,
label p, label span, .stMarkdown p {
color: #333333 !important;
font-weight: 500 !important;
}
/* Botón CTA Verde (Analizar) */
div.stButton > button {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important;
color: white !important;
border: none !important;
padding: 15px 30px !important;
font-size: 18px !important;
font-weight: bold !important;
border-radius: 8px !important;
width: 100% !important;
box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3) !important;
}
div.stButton > button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(40, 167, 69, 0.4) !important;
}
div.stButton > button p {
color: white !important;
}
</style>
""", 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("""
<div class="header-container">
<h1 style="color: #2c3e50 !important;">🏥 DermaRAG - Sistema Multiagente de Diagnóstico Dermatológico</h1>
<p style="color: #e74c3c !important;">IA Explicable con Retrieval-Augmented Generation | Guías AAD/BAD/NCCN</p>
<div style="background: #28a745; color: white; padding: 5px 15px; border-radius: 20px; display: inline-block; font-size: 12px; margin-top: 10px; font-weight: bold;">Potenciado por Groq LPU</div>
</div>
""", 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("<br>", unsafe_allow_html=True)
analyze_btn = st.button("🔍 Analizar con IA Multiagente + GradCAM", use_container_width=True)
st.markdown("""
<div style="margin-top: 20px; padding: 15px; background: #e3f2fd; border-radius: 8px; font-size: 13px; color: #0d47a1; border: 1px solid #90caf9;">
<div style="font-weight: bold; margin-bottom: 8px; font-size: 14px; display: flex; align-items: center; color: #003366;">
<span style="margin-right: 5px; font-size: 16px;">ℹ️</span> Flujo del Sistema:
</div>
<div style="margin-bottom: 4px; color: #0d47a1;">
<span style="background-color: #1d4ed8; color: white; padding: 2px 7px; border-radius: 4px; font-weight: bold; font-size: 11px; margin-right: 5px;">1</span>
<strong>Agente Percepción:</strong> CNN (EfficientNet-B4) + GradCAM
</div>
<div style="margin-bottom: 4px; color: #0d47a1;">
<span style="background-color: #1d4ed8; color: white; padding: 2px 7px; border-radius: 4px; font-weight: bold; font-size: 11px; margin-right: 5px;">2</span>
<strong>Agente Investigación:</strong> RAG busca en guías AAD/BAD/NCCN
</div>
<div style="color: #0d47a1;">
<span style="background-color: #1d4ed8; color: white; padding: 2px 7px; border-radius: 4px; font-weight: bold; font-size: 11px; margin-right: 5px;">3</span>
<strong>Agente Síntesis:</strong> LLM por Groq genera explicación pedagógica
</div>
</div>
""", 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"<span style='color: {color}; font-size: 24px; font-weight: bold;'>{score:.2f}</span> / 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("<div class='disclaimer'><strong>⚠️ AVISO MÉDICO-LEGAL:</strong> Este sistema es una herramienta de apoyo y <strong>NO reemplaza el juicio clínico</strong>.</div>", 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("<div style='text-align: center; color: #666666; padding: 20px;'>DermaRAG MVP v1.5 | Desarrollado con Mixtral 8x7B + EfficientNet-B4</div>", unsafe_allow_html=True)