# modal_app.py import modal import sys from pathlib import Path import os import traceback # --- Configuración --- PYTHON_VERSION = "3.10" APP_NAME = "bioprocess-custom-eq-agent-modal" LOCAL_APP_DIR = Path(__file__).parent REMOTE_APP_DIR = "/app" # Directorio donde se copiarán los archivos en el contenedor Modal stub = modal.Stub(APP_NAME) app_image = ( modal.Image.debian_slim(python_version=PYTHON_VERSION) .pip_install_from_requirements(LOCAL_APP_DIR / "requirements.txt") # Lee desde tu requirements.txt .copy_mount( # Copia todos los archivos de la app modal.Mount.from_local_dir(LOCAL_APP_DIR, remote_path=REMOTE_APP_DIR) ) .env({ # Configura variables de entorno dentro del contenedor "PYTHONPATH": REMOTE_APP_DIR, # Permite importar módulos desde /app "HF_HOME": "/cache/huggingface", "HF_HUB_CACHE": "/cache/huggingface/hub", "TRANSFORMERS_CACHE": "/cache/huggingface/hub", "MPLCONFIGDIR": "/tmp/matplotlib_cache" # Evita warnings de matplotlib }) .run_commands( # Comandos a ejecutar durante la construcción de la imagen "apt-get update && apt-get install -y git git-lfs && rm -rf /var/lib/apt/lists/*", "mkdir -p /cache/huggingface/hub /tmp/matplotlib_cache" # Crea directorios de caché ) ) # --- Función Modal para LLM (sin cambios significativos respecto a la respuesta completa anterior) --- @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 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, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( model_path_config, torch_dtype="auto", device_map="auto", cache_dir="/cache/huggingface/hub", token=hf_token, trust_remote_code=True ) model_context_window = getattr(model.config, 'max_position_embeddings', getattr(model.config, 'sliding_window', 4096)) if model_context_window is None : model_context_window = 4096 max_prompt_len = model_context_window - max_new_tokens_config - 50 if max_prompt_len <=0 : max_prompt_len = model_context_window // 2 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(image=app_image) def serve_gradio_app_asgi(): import gradio as gr # El PYTHONPATH ya está configurado en la imagen, pero una verificación no daña. if REMOTE_APP_DIR not in sys.path: sys.path.insert(0, REMOTE_APP_DIR) print(f"INFO (modal_app.py @asgi): Añadido {REMOTE_APP_DIR} a sys.path") # --- Neutralización del decorador GPU de Spaces, DENTRO del contexto Modal --- # Esto asegura que cualquier importación de `gradio` o `decorators` # no cause problemas con el chequeo de `@spaces.GPU`. try: import decorators # Importa TU decorators.py class _GPUNeutralizerInModal: def __init__(self, *args, **kwargs): pass def __call__(self, func): return func # Sobrescribir la clase GPU y el decorador en el módulo 'decorators' # para esta instancia del servidor ASGI. decorators.ActualSpacesGPU = _GPUNeutralizerInModal # Si decorators.py usa este nombre decorators._GPU_decorator_target = _GPUNeutralizerInModal # Si decorators.py usa este decorators.gpu_decorator = lambda duration=0: lambda func: func # Neutralizar el decorador print("INFO (modal_app.py @asgi): Decoradores GPU de 'spaces' neutralizados para el entorno Modal.") except ImportError: print("ADVERTENCIA (modal_app.py @asgi): Módulo 'decorators' no encontrado durante la neutralización. Puede ser OK.") except Exception as e_neut_modal: print(f"ADVERTENCIA (modal_app.py @asgi): Error durante la neutralización de decoradores en Modal: {e_neut_modal}") # --- Fin de la neutralización --- # Importar los módulos de la aplicación DESPUÉS de la neutralización y config de path from UI import create_interface import interface as app_interface_module 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_modal(prompt: str) -> str: print("Gradio Backend (Modal): Llamando a generate_analysis_llm_modal_remote.remote...") return generate_analysis_llm_modal_remote.remote(prompt, cfg_MODEL_PATH, cfg_MAX_LENGTH) app_interface_module.generate_analysis_from_modal = analysis_func_wrapper_for_interface_modal app_interface_module.USE_MODAL_FOR_LLM_ANALYSIS = True print("INFO (modal_app.py @asgi): Runner de LLM Modal inyectado en el módulo 'interface'.") gradio_ui_instance = create_interface(process_function_for_button=app_interface_module.process_and_plot) print("INFO (modal_app.py @asgi): Interfaz Gradio creada y lista para ser servida por Modal.") return gr.routes.App.create_app(gradio_ui_instance) @stub.local_entrypoint() def test_llm_local_entry(): print("Probando la generación de LLM con Modal (local_entrypoint)...") if str(LOCAL_APP_DIR) not in sys.path: 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." 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_local_entry: {e}") traceback.print_exc()