# modal_app.py import modal import sys from pathlib import Path import os import traceback # Para imprimir traceback detallado # --- Configuración --- PYTHON_VERSION = "3.10" APP_NAME = "bioprocess-custom-eq-agent-modal" LOCAL_APP_DIR = Path(__file__).parent REMOTE_APP_DIR = "/app" stub = modal.Stub(APP_NAME) app_image = ( modal.Image.debian_slim(python_version=PYTHON_VERSION) .pip_install_from_requirements(LOCAL_APP_DIR / "requirements.txt") .copy_mount( modal.Mount.from_local_dir(LOCAL_APP_DIR, remote_path=REMOTE_APP_DIR) ) .env({ "PYTHONPATH": REMOTE_APP_DIR, "HF_HOME": "/cache/huggingface", "HF_HUB_CACHE": "/cache/huggingface/hub", "TRANSFORMERS_CACHE": "/cache/huggingface/hub", "MPLCONFIGDIR": "/tmp/matplotlib_cache" }) .run_commands( "apt-get update && apt-get install -y git git-lfs && rm -rf /var/lib/apt/lists/*", "mkdir -p /cache/huggingface/hub /tmp/matplotlib_cache" ) ) # --- Función Modal para LLM (sin cambios respecto a la anterior respuesta) --- @stub.function( image=app_image, gpu="any", secrets=[modal.Secret.from_name("huggingface-read-token", optional=True)], timeout=600, volumes={"/cache/huggingface": modal.Volume.persisted(f"{APP_NAME}-hf-cache-vol")} ) def generate_analysis_llm_modal_remote(prompt: str, model_path_config: str, max_new_tokens_config: int) -> str: import torch # Mover importaciones pesadas dentro de la función Modal from transformers import AutoTokenizer, AutoModelForCausalLM hf_token = os.environ.get("HUGGING_FACE_TOKEN") device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"LLM Modal Func: Usando dispositivo: {device}") print(f"LLM Modal Func: Cargando modelo: {model_path_config} con token: {'Sí' if hf_token else 'No'}") try: tokenizer = AutoTokenizer.from_pretrained(model_path_config, cache_dir="/cache/huggingface/hub", token=hf_token) model = AutoModelForCausalLM.from_pretrained( model_path_config, torch_dtype="auto", device_map="auto", cache_dir="/cache/huggingface/hub", token=hf_token, ) # Ajustar longitud de truncamiento para el prompt para dejar espacio a max_new_tokens # La longitud total (prompt + generado) no debe exceder el context window del modelo. # Asumamos un context window conservador de 4096 si no se conoce. model_context_window = getattr(model.config, 'max_position_embeddings', 4096) max_prompt_len = model_context_window - max_new_tokens_config - 50 # 50 tokens de buffer inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=max_prompt_len).to(model.device) with torch.no_grad(): outputs = model.generate( **inputs, max_new_tokens=max_new_tokens_config, eos_token_id=tokenizer.eos_token_id, pad_token_id=tokenizer.pad_token_id if tokenizer.pad_token_id is not None else tokenizer.eos_token_id, do_sample=True, temperature=0.6, top_p=0.9, ) input_length = inputs.input_ids.shape[1] generated_ids = outputs[0][input_length:] analysis = tokenizer.decode(generated_ids, skip_special_tokens=True) print(f"LLM Modal Func: Longitud del análisis generado: {len(analysis)} caracteres.") return analysis.strip() except Exception as e: error_traceback = traceback.format_exc() print(f"Error en generate_analysis_llm_modal_remote: {e}\n{error_traceback}") return f"Error al generar análisis con el modelo LLM: {str(e)}" # --- Servidor Gradio --- @stub.asgi_app() def serve_gradio_app_asgi(): # Estas importaciones ocurren DENTRO del contenedor Modal import gradio as gr # sys.path ya está configurado por la imagen, pero por si acaso: if REMOTE_APP_DIR not in sys.path: sys.path.insert(0, REMOTE_APP_DIR) from UI import create_interface # De tu UI.py import interface as app_interface_module # El módulo interface.py from config import MODEL_PATH as cfg_MODEL_PATH, MAX_LENGTH as cfg_MAX_LENGTH # Wrapper para llamar a la función Modal remota def analysis_func_wrapper_for_interface(prompt: str) -> str: print("Gradio Backend: Llamando a generate_analysis_llm_modal_remote.remote...") return generate_analysis_llm_modal_remote.remote(prompt, cfg_MODEL_PATH, cfg_MAX_LENGTH) # Inyectar esta función wrapper en el módulo `interface` app_interface_module.generate_analysis_from_modal = analysis_func_wrapper_for_interface app_interface_module.USE_MODAL_FOR_LLM_ANALYSIS = True # Crear la app Gradio, pasándole la función de procesamiento que ahora está en app_interface_module # create_interface ahora toma la función de callback como argumento. gradio_ui = create_interface(process_function_for_button=app_interface_module.process_and_plot) return gr.routes.App.create_app(gradio_ui) @stub.local_entrypoint() def test_llm(): print("Probando la generación de LLM con Modal (localmente)...") # Necesitas importar config para que MODEL_PATH y MAX_LENGTH estén definidos # Esto debe hacerse dentro de un contexto donde los módulos de la app sean accesibles. # Es mejor llamar a la función stub desde aquí. if REMOTE_APP_DIR not in sys.path: # Asegurar path para pruebas locales también sys.path.insert(0, str(LOCAL_APP_DIR)) from config import MODEL_PATH, MAX_LENGTH sample_prompt = "Explica brevemente el concepto de R cuadrado (R²) en el ajuste de modelos." # Ejecuta la función Modal directamente (no .remote() para prueba local de lógica) # Para probar la ejecución remota real, necesitarías `modal run modal_app.py test_llm` # o que test_llm llame a .remote(). # Aquí vamos a probar la llamada remota. try: analysis = generate_analysis_llm_modal_remote.remote(sample_prompt, MODEL_PATH, MAX_LENGTH) print("\nRespuesta del LLM:") print(analysis) except Exception as e: print(f"Error durante test_llm: {e}") traceback.print_exc()