Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import io | |
| import importlib.util | |
| import json | |
| import os | |
| import re | |
| import shutil | |
| import tempfile | |
| import time | |
| import gc | |
| from pathlib import Path | |
| # Evita inspecciones de modulos que generan ruido con torch.classes en Streamlit. | |
| os.environ.setdefault("STREAMLIT_SERVER_FILE_WATCHER_TYPE", "none") | |
| import pandas as pd | |
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| import torch | |
| from huggingface_hub import model_info | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| try: | |
| from transformers import AutoConfig | |
| except ImportError: | |
| AutoConfig = None | |
| from web.runner import TIPOS_EVALUACION_DISPONIBLES, construir_instruccion_sistema_generador, ejecutar_job | |
| from web.schemas import JobRequest, ModoEvaluacion, TipoEvaluacion | |
| MODELOS_PREDEFINIDOS = [ | |
| "Qwen/Qwen2.5-1.5B-Instruct", | |
| "Qwen/Qwen2.5-3B-Instruct", | |
| "Other Model", | |
| ] | |
| LABELS_TIPOS_EVALUACION = { | |
| "preguntas_agente": "Preguntas agente", | |
| "preguntas_analisis_sentimiento": "Preguntas analisis de sentimiento", | |
| "preguntas_cerradas_esperadas": "Preguntas cerradas esperadas", | |
| "preguntas_cerradas_probabilidad": "Preguntas cerradas probabilidad", | |
| "preguntas_respuestas_multiples": "Preguntas respuestas multiples", | |
| "preguntas_prompt_injection": "Preguntas prompt injection", | |
| } | |
| TIPOS_EVALUACION_SOPORTADOS_SPACE = { | |
| "preguntas_agente", | |
| "preguntas_cerradas_esperadas", | |
| } | |
| def _init_state() -> None: | |
| defaults = { | |
| "modelo_eval_validado": False, | |
| "modelo_eval_confirmado": "", | |
| "modelo_gen_validado": False, | |
| "modelo_gen_confirmado": "", | |
| "modo_actual": ModoEvaluacion.POR_DEFECTO.value, | |
| "eval_running": False, | |
| "eval_requested": False, | |
| "eval_success": False, | |
| "last_result": None, | |
| "pending_eval": None, | |
| "eval_error": None, | |
| } | |
| for key, value in defaults.items(): | |
| if key not in st.session_state: | |
| st.session_state[key] = value | |
| def _formatear_duracion(segundos: float) -> str: | |
| total = int(max(segundos, 0)) | |
| horas, resto = divmod(total, 3600) | |
| minutos, segs = divmod(resto, 60) | |
| return f"{horas:02d}:{minutos:02d}:{segs:02d}" | |
| def _slug_modelo(model_id: str) -> str: | |
| return re.sub(r"[^a-zA-Z0-9._-]+", "_", model_id.strip()).strip("_") | |
| def _leer_bytes_si_existe(path: Path) -> bytes | None: | |
| if path.exists() and path.is_file(): | |
| return path.read_bytes() | |
| return None | |
| def _liberar_memoria(liberar_modelo_cache: bool = False) -> None: | |
| if liberar_modelo_cache: | |
| try: | |
| # Libera tokenizer/modelo cacheados por @st.cache_resource. | |
| cargar_modelo_transformers.clear() | |
| except Exception: | |
| pass | |
| gc.collect() | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| def _resetear_estado_inicial() -> None: | |
| st.session_state["eval_running"] = False | |
| st.session_state["eval_requested"] = False | |
| st.session_state["eval_success"] = False | |
| st.session_state["last_result"] = None | |
| st.session_state["pending_eval"] = None | |
| st.session_state["eval_error"] = None | |
| st.session_state["modelo_eval_validado"] = False | |
| st.session_state["modelo_eval_confirmado"] = "" | |
| st.session_state["modelo_gen_validado"] = False | |
| st.session_state["modelo_gen_confirmado"] = "" | |
| # Limpia widgets para volver a la pantalla inicial. | |
| for key in [ | |
| "modelo_eval_option", | |
| "modelo_eval_otro", | |
| "confirmar_modelo_grande_cpu", | |
| "mostrar_cancelacion_eval", | |
| "modelo_gen_option", | |
| "modelo_gen_otro", | |
| "plantilla_personalizada_uploader", | |
| ]: | |
| st.session_state.pop(key, None) | |
| for tipo_eval in TIPOS_EVALUACION_DISPONIBLES: | |
| st.session_state.pop(f"eval_tipo_{tipo_eval}", None) | |
| _liberar_memoria(liberar_modelo_cache=True) | |
| def _resetear_widgets_modo(modo: str) -> None: | |
| if modo == ModoEvaluacion.POR_DEFECTO.value: | |
| st.session_state.pop("modelo_gen_option", None) | |
| st.session_state.pop("modelo_gen_otro", None) | |
| st.session_state["modelo_gen_validado"] = False | |
| st.session_state["modelo_gen_confirmado"] = "" | |
| return | |
| st.session_state.pop("modelo_eval_option", None) | |
| st.session_state.pop("modelo_eval_otro", None) | |
| st.session_state.pop("confirmar_modelo_grande_cpu", None) | |
| for tipo_eval in TIPOS_EVALUACION_DISPONIBLES: | |
| st.session_state.pop(f"eval_tipo_{tipo_eval}", None) | |
| st.session_state["modelo_eval_validado"] = False | |
| st.session_state["modelo_eval_confirmado"] = "" | |
| def _render_reloj_tiempo_real(inicio_epoch: int, placeholder) -> None: | |
| with placeholder: | |
| components.html( | |
| f""" | |
| <div style='padding:0.35rem 0; font-size:0.95rem; color:#374151;'> | |
| <strong>Tiempo transcurrido:</strong> <span id='equitia-live-timer'>00:00:00</span> | |
| </div> | |
| <script> | |
| const startEpoch = {inicio_epoch}; | |
| function pad(n) {{ return String(n).padStart(2, '0'); }} | |
| function tick() {{ | |
| const now = Math.floor(Date.now() / 1000); | |
| const diff = Math.max(0, now - startEpoch); | |
| const hh = Math.floor(diff / 3600); | |
| const mm = Math.floor((diff % 3600) / 60); | |
| const ss = diff % 60; | |
| const el = document.getElementById('equitia-live-timer'); | |
| if (el) el.textContent = `${{pad(hh)}}:${{pad(mm)}}:${{pad(ss)}}`; | |
| }} | |
| tick(); | |
| setInterval(tick, 1000); | |
| </script> | |
| """, | |
| height=42, | |
| ) | |
| def validar_modelo_existe(model_id: str) -> tuple[bool, str]: | |
| try: | |
| if AutoConfig is not None: | |
| AutoConfig.from_pretrained(model_id) | |
| else: | |
| model_info(model_id) | |
| return True, f"Modelo encontrado en Hugging Face: {model_id}" | |
| except Exception as exc: | |
| return False, f"No se pudo validar el modelo '{model_id}': {exc}" | |
| def cargar_modelo_transformers(model_id: str): | |
| tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True) | |
| if tokenizer.pad_token is None and tokenizer.eos_token is not None: | |
| tokenizer.pad_token = tokenizer.eos_token | |
| kwargs = { | |
| "low_cpu_mem_usage": True, | |
| } | |
| accelerate_disponible = importlib.util.find_spec("accelerate") is not None | |
| if torch.cuda.is_available(): | |
| kwargs["torch_dtype"] = torch.float16 | |
| if accelerate_disponible: | |
| kwargs["device_map"] = "auto" | |
| else: | |
| # En CPU evitamos device_map para no requerir accelerate. | |
| kwargs["torch_dtype"] = torch.float32 | |
| model = AutoModelForCausalLM.from_pretrained(model_id, **kwargs) | |
| model.eval() | |
| return tokenizer, model | |
| def invocar_modelo_transformers(model_id: str, prompt: str, instruccion_sistema: str | None = None) -> str: | |
| tokenizer, model = cargar_modelo_transformers(model_id) | |
| contenido_sistema = (instruccion_sistema or "Responde de forma breve y directa.").strip() | |
| mensajes = [ | |
| {"role": "system", "content": contenido_sistema}, | |
| {"role": "user", "content": prompt}, | |
| ] | |
| inputs = None | |
| if hasattr(tokenizer, "apply_chat_template"): | |
| try: | |
| inputs = tokenizer.apply_chat_template( | |
| mensajes, | |
| return_tensors="pt", | |
| add_generation_prompt=True, | |
| ) | |
| except Exception: | |
| # Fallback para tokenizers sin chat_template definido (ej. modelos base/instruct antiguos). | |
| inputs = None | |
| if inputs is None: | |
| prompt_plano = ( | |
| f"Sistema: {contenido_sistema}\n\n" | |
| f"Usuario: {prompt}\n\n" | |
| "Respuesta:" | |
| ) | |
| inputs = tokenizer(prompt_plano, return_tensors="pt")["input_ids"] | |
| device = model.device | |
| inputs = inputs.to(device) | |
| with torch.no_grad(): | |
| outputs = model.generate( | |
| inputs, | |
| max_new_tokens=8, | |
| do_sample=False, | |
| pad_token_id=tokenizer.pad_token_id, | |
| eos_token_id=tokenizer.eos_token_id, | |
| ) | |
| new_tokens = outputs[:, inputs.shape[1]:] | |
| texto = tokenizer.batch_decode(new_tokens, skip_special_tokens=True)[0].strip() | |
| return texto | |
| def _estimar_tamanyo_modelo_b(model_id: str) -> float | None: | |
| match = re.search(r"(\d+(?:\.\d+)?)\s*[bB]", model_id) | |
| if not match: | |
| return None | |
| try: | |
| return float(match.group(1)) | |
| except ValueError: | |
| return None | |
| st.set_page_config(page_title="EQUITIA Web", page_icon="馃搳", layout="wide") | |
| st.title("EQUITIA 路 Evaluaci贸n de sesgos LLM") | |
| st.caption("Despliegue p煤blico en Hugging Face Spaces") | |
| _init_state() | |
| mostrar_solo_resultados = bool( | |
| st.session_state.get("eval_success") and st.session_state.get("last_result") | |
| ) | |
| if st.session_state.get("eval_error"): | |
| st.error(st.session_state["eval_error"]) | |
| if not mostrar_solo_resultados: | |
| modo = st.radio( | |
| "Selecciona el modo", | |
| options=[ModoEvaluacion.POR_DEFECTO.value, ModoEvaluacion.PERSONALIZADA.value], | |
| format_func=lambda x: "Evaluaci贸n por defecto" if x == ModoEvaluacion.POR_DEFECTO.value else "Evaluaci贸n personalizada", | |
| disabled=st.session_state["eval_running"], | |
| ) | |
| if modo != st.session_state.get("modo_actual"): | |
| _resetear_widgets_modo(modo) | |
| st.session_state["modo_actual"] = modo | |
| else: | |
| modo = ModoEvaluacion.POR_DEFECTO.value | |
| if not mostrar_solo_resultados: | |
| # Unico parametro editable en UI para ambos modos. | |
| timeout_segundos = st.slider( | |
| "Timeout por llamada (segundos)", | |
| min_value=10, | |
| max_value=300, | |
| value=120, | |
| disabled=st.session_state["eval_running"], | |
| ) | |
| else: | |
| timeout_segundos = 120 | |
| if not mostrar_solo_resultados and modo == ModoEvaluacion.PERSONALIZADA.value: | |
| st.info("La evaluaci贸n personalizada se implementar谩 despu茅s. Aqu铆 solo preseleccionas modelos por ahora.") | |
| plantilla_json = st.file_uploader( | |
| "(Opcional) Sube la plantilla de evaluaci贸n JSON para preparar instrucciones de generaci贸n", | |
| type=["json"], | |
| key="plantilla_personalizada_uploader", | |
| ) | |
| if plantilla_json is not None: | |
| try: | |
| plantilla = json.load(plantilla_json) | |
| instruccion_generador = construir_instruccion_sistema_generador(plantilla) | |
| st.markdown("### Instrucci贸n de sistema para el modelo generador") | |
| st.code(instruccion_generador) | |
| except Exception as exc: | |
| st.error(f"No se pudo leer la plantilla JSON: {exc}") | |
| modelo_gen_option = st.selectbox( | |
| "Modelo para generar prompts", | |
| MODELOS_PREDEFINIDOS, | |
| key="modelo_gen_option", | |
| disabled=st.session_state["eval_running"], | |
| ) | |
| modelo_gen_input = "" | |
| if modelo_gen_option == "Other Model": | |
| modelo_gen_input = st.text_input( | |
| "Escribe otro modelo para generaci贸n", | |
| key="modelo_gen_otro", | |
| disabled=st.session_state["eval_running"], | |
| ) | |
| modelo_gen_actual = modelo_gen_input.strip() if modelo_gen_option == "Other Model" else modelo_gen_option | |
| if ( | |
| st.session_state["modelo_gen_validado"] | |
| and st.session_state["modelo_gen_confirmado"] != modelo_gen_actual | |
| ): | |
| st.session_state["modelo_gen_validado"] = False | |
| st.session_state["modelo_gen_confirmado"] = "" | |
| if st.button("Validar modelo generador", key="validar_generador", disabled=st.session_state["eval_running"]): | |
| modelo_gen = modelo_gen_input.strip() if modelo_gen_option == "Other Model" else modelo_gen_option | |
| if not modelo_gen: | |
| st.error("Debes indicar un modelo generador.") | |
| else: | |
| ok, msg = validar_modelo_existe(modelo_gen) | |
| if ok: | |
| st.session_state["modelo_gen_validado"] = True | |
| st.session_state["modelo_gen_confirmado"] = modelo_gen | |
| st.success(msg) | |
| else: | |
| st.session_state["modelo_gen_validado"] = False | |
| st.error(msg) | |
| if st.session_state["modelo_gen_validado"]: | |
| st.success(f"Modelo generador confirmado: {st.session_state['modelo_gen_confirmado']}") | |
| st.stop() | |
| if not mostrar_solo_resultados: | |
| # Flujo: Evaluaci贸n por defecto | |
| modelo_eval_option = st.selectbox( | |
| "Modelo a evaluar", | |
| MODELOS_PREDEFINIDOS, | |
| key="modelo_eval_option", | |
| disabled=st.session_state["eval_running"], | |
| ) | |
| modelo_eval_input = "" | |
| if modelo_eval_option == "Other Model": | |
| modelo_eval_input = st.text_input( | |
| "Escribe otro modelo para evaluar", | |
| key="modelo_eval_otro", | |
| disabled=st.session_state["eval_running"], | |
| ) | |
| modelo_eval_actual = modelo_eval_input.strip() if modelo_eval_option == "Other Model" else modelo_eval_option | |
| if ( | |
| st.session_state["modelo_eval_validado"] | |
| and st.session_state["modelo_eval_confirmado"] != modelo_eval_actual | |
| ): | |
| st.session_state["modelo_eval_validado"] = False | |
| st.session_state["modelo_eval_confirmado"] = "" | |
| if st.button("Validar modelo a evaluar", key="validar_modelo_eval", disabled=st.session_state["eval_running"]): | |
| modelo_eval = modelo_eval_input.strip() if modelo_eval_option == "Other Model" else modelo_eval_option | |
| if not modelo_eval: | |
| st.error("Debes indicar un modelo para evaluar.") | |
| else: | |
| ok, msg = validar_modelo_existe(modelo_eval) | |
| if ok: | |
| st.session_state["modelo_eval_validado"] = True | |
| st.session_state["modelo_eval_confirmado"] = modelo_eval | |
| st.success(msg) | |
| else: | |
| st.session_state["modelo_eval_validado"] = False | |
| st.error(msg) | |
| if not st.session_state["modelo_eval_validado"]: | |
| st.warning("Primero valida un modelo para evaluar.") | |
| st.stop() | |
| st.success(f"Modelo evaluador confirmado: {st.session_state['modelo_eval_confirmado']}") | |
| tamanyo_estimado_b = _estimar_tamanyo_modelo_b(st.session_state["modelo_eval_confirmado"]) | |
| if not torch.cuda.is_available() and tamanyo_estimado_b is not None and tamanyo_estimado_b > 4: | |
| st.error( | |
| "Modelo potencialmente demasiado grande para Space CPU de 16 GiB. " | |
| "Usa preferiblemente <= 4B para evitar OOM." | |
| ) | |
| permitir_modelo_grande = st.checkbox( | |
| "Entiendo el riesgo de memoria y quiero continuar igualmente", | |
| value=False, | |
| key="confirmar_modelo_grande_cpu", | |
| ) | |
| if not permitir_modelo_grande: | |
| st.stop() | |
| st.markdown("### Tipos de evaluaci贸n") | |
| st.caption("Marca o desmarca cada tipo seg煤n lo que quieras evaluar.") | |
| for tipo_eval in TIPOS_EVALUACION_DISPONIBLES: | |
| st.checkbox( | |
| LABELS_TIPOS_EVALUACION.get(tipo_eval, tipo_eval), | |
| key=f"eval_tipo_{tipo_eval}", | |
| value=(tipo_eval == "preguntas_cerradas_esperadas"), | |
| disabled=st.session_state["eval_running"], | |
| ) | |
| selected_eval_types = [ | |
| tipo_eval | |
| for tipo_eval in TIPOS_EVALUACION_DISPONIBLES | |
| if st.session_state.get(f"eval_tipo_{tipo_eval}", False) | |
| ] | |
| if not selected_eval_types: | |
| st.warning("Debes seleccionar al menos un tipo de evaluaci贸n.") | |
| st.stop() | |
| tipos_soportados = [ | |
| t for t in selected_eval_types if t in TIPOS_EVALUACION_SOPORTADOS_SPACE | |
| ] | |
| if not tipos_soportados: | |
| st.error( | |
| "Debes seleccionar al menos un tipo implementado en el Space: " | |
| "'preguntas_agente' o 'preguntas_cerradas_esperadas'." | |
| ) | |
| st.stop() | |
| tipos_no_disponibles = [ | |
| t for t in selected_eval_types if t not in TIPOS_EVALUACION_SOPORTADOS_SPACE | |
| ] | |
| if tipos_no_disponibles: | |
| st.info( | |
| "Estos tipos quedan reservados para pr贸ximas iteraciones y no se ejecutar谩n ahora: " | |
| + ", ".join(LABELS_TIPOS_EVALUACION.get(t, t) for t in tipos_no_disponibles) | |
| ) | |
| if st.button("Comenzar evaluaci贸n", key="comenzar_eval", disabled=st.session_state["eval_running"]): | |
| st.session_state["eval_running"] = True | |
| st.session_state["eval_requested"] = True | |
| st.session_state["eval_success"] = False | |
| st.session_state["last_result"] = None | |
| st.session_state["eval_error"] = None | |
| st.session_state["pending_eval"] = { | |
| "modelo_hf": st.session_state["modelo_eval_confirmado"], | |
| "timeout_segundos": timeout_segundos, | |
| "selected_eval_types": selected_eval_types, | |
| } | |
| _liberar_memoria(liberar_modelo_cache=True) | |
| st.rerun() | |
| if st.session_state.get("eval_running"): | |
| st.warning("Proceso en ejecuci贸n. Cancelar detiene la evaluaci贸n y descarta resultados parciales.") | |
| if st.button( | |
| "Cancelar evaluaci贸n (acci贸n irreversible)", | |
| key="cancelar_evaluacion_confirmada", | |
| type="secondary", | |
| help="Detiene inmediatamente la evaluaci贸n en curso y vuelve a la pantalla inicial.", | |
| ): | |
| _resetear_estado_inicial() | |
| st.rerun() | |
| if st.session_state.get("eval_running") and st.session_state.get("eval_requested"): | |
| pending = st.session_state.get("pending_eval") or {} | |
| modelo_hf = str(pending.get("modelo_hf", st.session_state.get("modelo_eval_confirmado", ""))).strip() | |
| timeout_pendiente = int(pending.get("timeout_segundos", timeout_segundos)) | |
| tipos_pendientes = pending.get("selected_eval_types") or ["preguntas_cerradas_esperadas"] | |
| tipo_base = ( | |
| TipoEvaluacion.PREGUNTAS_AGENTE.value | |
| if "preguntas_agente" in tipos_pendientes | |
| else TipoEvaluacion.PREGUNTAS_CERRADAS_ESPERADAS.value | |
| ) | |
| request = JobRequest( | |
| modo_evaluacion=ModoEvaluacion.POR_DEFECTO.value, | |
| tipo_evaluacion=tipo_base, | |
| modelo_hf=modelo_hf, | |
| timeout_segundos=timeout_pendiente, | |
| ) | |
| temp_dir = Path(tempfile.mkdtemp(prefix="equitia_space_")) | |
| job_dir = temp_dir / "job" | |
| start_ts = time.perf_counter() | |
| start_epoch = int(time.time()) | |
| progress = st.progress(0.0) | |
| progress_label = st.empty() | |
| timer_placeholder = st.empty() | |
| _render_reloj_tiempo_real(start_epoch, timer_placeholder) | |
| def on_progress(done: int, total: int, current_file: str) -> None: | |
| ratio = (done / total) if total else 0.0 | |
| elapsed = _formatear_duracion(time.perf_counter() - start_ts) | |
| progress.progress(ratio) | |
| progress_label.info( | |
| f"Progreso: {done}/{total} prompts evaluados ({ratio * 100:.1f}%). Tiempo 煤ltimo prompt evaluado: {elapsed}. Archivo actual: {current_file}" | |
| ) | |
| def invocar_prompt(prompt: str, instruccion_sistema: str | None = None) -> str: | |
| return invocar_modelo_transformers( | |
| modelo_hf, | |
| prompt, | |
| instruccion_sistema=instruccion_sistema, | |
| ) | |
| try: | |
| with st.spinner("Obteniendo modelo a evaluar..."): | |
| cargar_modelo_transformers(modelo_hf) | |
| with st.spinner("Ejecutando proceso de evaluaci贸n..."): | |
| result = ejecutar_job( | |
| request, | |
| job_dir, | |
| selected_eval_types=tipos_pendientes, | |
| invocar_modelo_fn=invocar_prompt, | |
| progress_callback=on_progress, | |
| ) | |
| progress.progress(1.0) | |
| elapsed_total = _formatear_duracion(time.perf_counter() - start_ts) | |
| progress_label.success(f"Evaluaci贸n completada. Tiempo total: {elapsed_total}") | |
| timer_placeholder.empty() | |
| resumen_path = result.job_dir / "resumen.json" | |
| resultados_csv = result.graficos_dir / "resultados.csv" | |
| outliers_txt = result.graficos_dir / "avisos_outliers.txt" | |
| if resumen_path.exists(): | |
| with open(resumen_path, "r", encoding="utf-8") as f: | |
| resumen = json.load(f) | |
| zip_id = int(time.time()) | |
| modelo_slug = _slug_modelo(modelo_hf) | |
| zip_filename = f"resultados_equitia_{modelo_slug}_{zip_id}.zip" | |
| zip_base = temp_dir / f"resultados_equitia_{zip_id}" | |
| zip_path = Path(shutil.make_archive(str(zip_base), "zip", str(result.job_dir))) | |
| graficos = {} | |
| for graph_name in [ | |
| "resultados_generales.png", | |
| "resultados_tipo_evaluacion.png", | |
| "mapa_calor_tipo_evaluacion.png", | |
| ]: | |
| graph_path = result.graficos_dir / graph_name | |
| graph_bytes = _leer_bytes_si_existe(graph_path) | |
| if graph_bytes is not None: | |
| graficos[graph_name] = graph_bytes | |
| preview_rows = None | |
| if resultados_csv.exists(): | |
| preview_rows = pd.read_csv(resultados_csv, sep="|").head(30).to_dict(orient="records") | |
| st.session_state["last_result"] = { | |
| "resumen": resumen if resumen_path.exists() else None, | |
| "outliers": outliers_txt.read_text(encoding="utf-8") if outliers_txt.exists() else None, | |
| "graficos": graficos, | |
| "preview_rows": preview_rows, | |
| "zip_bytes": zip_path.read_bytes(), | |
| "zip_filename": zip_filename, | |
| "elapsed_total": elapsed_total, | |
| "modelo": modelo_hf, | |
| } | |
| st.session_state["eval_success"] = True | |
| except Exception as exc: | |
| st.session_state["eval_error"] = f"Error durante la evaluaci贸n: {exc}" | |
| timer_placeholder.empty() | |
| finally: | |
| st.session_state["eval_running"] = False | |
| st.session_state["eval_requested"] = False | |
| st.session_state["pending_eval"] = None | |
| _liberar_memoria(liberar_modelo_cache=True) | |
| shutil.rmtree(temp_dir, ignore_errors=True) | |
| st.rerun() | |
| if st.session_state.get("eval_success") and st.session_state.get("last_result"): | |
| resultado = st.session_state["last_result"] | |
| if resultado.get("elapsed_total"): | |
| st.info(f"Tiempo total del proceso de evaluaci贸n: {resultado['elapsed_total']}") | |
| if resultado.get("resumen") is not None: | |
| st.success("Resumen de evaluaci贸n") | |
| st.json(resultado["resumen"]) | |
| if resultado.get("outliers"): | |
| st.markdown("### Avisos de outliers") | |
| st.code(resultado["outliers"]) | |
| st.markdown("### Gr谩ficos") | |
| for graph_name, graph_bytes in resultado.get("graficos", {}).items(): | |
| st.image(graph_bytes, caption=graph_name) | |
| if resultado.get("preview_rows"): | |
| st.markdown("### Vista previa") | |
| st.dataframe(pd.DataFrame(resultado["preview_rows"]), use_container_width=True) | |
| st.caption( | |
| "Al descargar el ZIP se reiniciar谩 la evaluaci贸n actual para liberar memoria del Space." | |
| ) | |
| descarga = st.download_button( | |
| label="Descargar resultados (ZIP y reiniciar)", | |
| data=io.BytesIO(resultado["zip_bytes"]), | |
| file_name=resultado["zip_filename"], | |
| mime="application/zip", | |
| use_container_width=True, | |
| disabled=st.session_state["eval_running"], | |
| ) | |
| if descarga: | |
| st.session_state["eval_success"] = False | |
| st.session_state["last_result"] = None | |
| _liberar_memoria(liberar_modelo_cache=True) | |
| st.rerun() | |
| if st.button("Nueva evaluaci贸n", disabled=st.session_state["eval_running"]): | |
| _resetear_estado_inicial() | |
| st.rerun() | |