#!/usr/bin/env python3 """ Ollama Model Manager - Script Web para gestionar modelos de Ollama Permite listar, descargar y eliminar modelos de Ollama con interfaz web. """ import os import sys import socket import threading import webbrowser import time import requests from datetime import datetime from typing import Dict, Any from flask import Flask, render_template, jsonify, request # Configuración del path para importar utilidades del proyecto principal script_root = os.path.dirname(os.path.dirname(__file__)) sys.path.append(script_root) # Importar utilidades del proyecto (opcional, para configuración) try: from ParamManagerScripts.backend.script_utils import load_configuration HAS_CONFIG = True except ImportError: HAS_CONFIG = False msg = ( "⚠️ No se pudo importar load_configuration, " "usando configuración por defecto" ) print(msg) class OllamaManager: """Clase para gestionar modelos de Ollama""" def __init__(self, base_url: str = "http://localhost:11434"): self.base_url = base_url.rstrip("/") self.api_url = f"{self.base_url}/api" def is_ollama_running(self) -> bool: """Verificar si Ollama está ejecutándose""" try: response = requests.get(f"{self.base_url}/api/tags", timeout=5) return response.status_code == 200 except requests.exceptions.RequestException: return False def list_models(self) -> Dict[str, Any]: """Listar todos los modelos instalados""" try: response = requests.get(f"{self.api_url}/tags", timeout=10) if response.status_code == 200: data = response.json() models = data.get("models", []) # Calcular tamaño total total_size = sum(model.get("size", 0) for model in models) return { "status": "success", "models": models, "total_models": len(models), "total_size": total_size, "total_size_human": self._format_bytes(total_size), } else: error_msg = f"Error HTTP: {response.status_code}" return {"status": "error", "message": error_msg} except requests.exceptions.RequestException as e: error_msg = f"Error de conexión: {str(e)}" return {"status": "error", "message": error_msg} def pull_model(self, model_name: str) -> Dict[str, Any]: """Descargar un modelo""" try: # Iniciar descarga response = requests.post( f"{self.api_url}/pull", json={"name": model_name}, stream=True, timeout=300, # 5 minutos timeout ) if response.status_code == 200: msg = f"Descarga de '{model_name}' iniciada" return {"status": "success", "message": msg} else: error_msg = f"Error al descargar: {response.status_code}" return {"status": "error", "message": error_msg} except requests.exceptions.RequestException as e: error_msg = f"Error de conexión: {str(e)}" return {"status": "error", "message": error_msg} def delete_model(self, model_name: str) -> Dict[str, Any]: """Eliminar un modelo""" try: response = requests.delete( f"{self.api_url}/delete", json={"name": model_name}, timeout=30 ) if response.status_code == 200: msg = f"Modelo '{model_name}' eliminado correctamente" return {"status": "success", "message": msg} else: error_msg = f"Error al eliminar: {response.status_code}" return {"status": "error", "message": error_msg} except requests.exceptions.RequestException as e: error_msg = f"Error de conexión: {str(e)}" return {"status": "error", "message": error_msg} def get_model_info(self, model_name: str) -> Dict[str, Any]: """Obtener información detallada de un modelo""" try: response = requests.post( f"{self.api_url}/show", json={"name": model_name}, timeout=10 ) if response.status_code == 200: return {"status": "success", "info": response.json()} else: error_msg = f"Error al obtener info: {response.status_code}" return {"status": "error", "message": error_msg} except requests.exceptions.RequestException as e: error_msg = f"Error de conexión: {str(e)}" return {"status": "error", "message": error_msg} def _format_bytes(self, bytes_size: int) -> str: """Formatear bytes a formato legible""" if bytes_size == 0: return "0 B" for unit in ["B", "KB", "MB", "GB", "TB"]: if bytes_size < 1024.0: return f"{bytes_size:.1f} {unit}" bytes_size /= 1024.0 return f"{bytes_size:.1f} PB" def find_free_port() -> int: """Encontrar un puerto libre""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.bind(("", 0)) return s.getsockname()[1] def create_app() -> Flask: """Crear la aplicación Flask""" app = Flask(__name__) # Configurar directorio de templates template_dir = os.path.join(os.path.dirname(__file__), "templates") if not os.path.exists(template_dir): os.makedirs(template_dir) app.template_folder = template_dir # Configurar directorio de archivos estáticos static_dir = os.path.join(os.path.dirname(__file__), "static") if not os.path.exists(static_dir): os.makedirs(static_dir) app.static_folder = static_dir # Inicializar gestor de Ollama ollama_manager = OllamaManager() @app.route("/") def index(): """Página principal""" return render_template("index.html") @app.route("/api/status") def api_status(): """Verificar estado de Ollama""" is_running = ollama_manager.is_ollama_running() return jsonify( { "status": "success", "ollama_running": is_running, "timestamp": datetime.now().isoformat(), } ) @app.route("/api/models") def api_models(): """Listar modelos""" return jsonify(ollama_manager.list_models()) @app.route("/api/models//info") def api_model_info(model_name): """Obtener información de un modelo""" return jsonify(ollama_manager.get_model_info(model_name)) @app.route("/api/models/pull", methods=["POST"]) def api_pull_model(): """Descargar un modelo""" data = request.get_json() if not data or "name" not in data: error_msg = "Nombre del modelo requerido" return jsonify({"status": "error", "message": error_msg}), 400 model_name = data["name"].strip() if not model_name: error_msg = "Nombre del modelo no puede estar vacío" return jsonify({"status": "error", "message": error_msg}), 400 result = ollama_manager.pull_model(model_name) return jsonify(result) @app.route("/api/models/", methods=["DELETE"]) def api_delete_model(model_name): """Eliminar un modelo""" result = ollama_manager.delete_model(model_name) return jsonify(result) @app.errorhandler(404) def not_found(error): error_msg = "Endpoint no encontrado" return jsonify({"status": "error", "message": error_msg}), 404 @app.errorhandler(500) def internal_error(error): error_msg = "Error interno del servidor" return jsonify({"status": "error", "message": error_msg}), 500 @app.route("/_shutdown", methods=["POST"]) def shutdown_route(): """Endpoint interno para cerrar la aplicación""" print("🛑 Solicitud de cierre recibida desde la interfaz web") def shutdown_server(): time.sleep(0.1) # Pequeña pausa para permitir respuesta HTTP try: # Intentar cerrar el servidor Flask de manera elegante import os import signal os.kill(os.getpid(), signal.SIGINT) except Exception as e: print(f"Error al cerrar servidor: {e}") # Fallback: forzar salida os._exit(0) # Ejecutar cierre en hilo separado shutdown_thread = threading.Thread(target=shutdown_server, daemon=True) shutdown_thread.start() return ( jsonify( {"status": "success", "message": "Cerrando Ollama Model Manager..."} ), 200, ) return app def main(): """Función principal""" print("🦙 Ollama Model Manager - Iniciando...") # Cargar configuración si está disponible configs = {} if HAS_CONFIG: try: configs = load_configuration() print("✅ Configuración cargada correctamente") except Exception as e: print(f"⚠️ Error al cargar configuración: {e}") # Obtener configuración ollama_host = configs.get("ollama_host", "http://localhost:11434") auto_open_browser = configs.get("auto_open_browser", True) # Crear aplicación Flask app = create_app() # Encontrar puerto libre port = find_free_port() # URL de la aplicación app_url = f"http://127.0.0.1:{port}" print(f"🌐 Servidor iniciado en: {app_url}") print(f"🦙 Ollama Host: {ollama_host}") print("📋 Funcionalidades disponibles:") print(" - Listar modelos instalados") print(" - Ver información detallada de modelos") print(" - Descargar nuevos modelos") print(" - Eliminar modelos existentes") print(" - Monitoreo de espacio ocupado") print() print("⏹️ Presiona Ctrl+C para cerrar el servidor") # Abrir navegador automáticamente if auto_open_browser: def open_browser(): time.sleep(1.0) # Esperar a que Flask inicie try: webbrowser.open(app_url) print(f"🌐 Navegador abierto en: {app_url}") except Exception as e: print(f"⚠️ No se pudo abrir el navegador automáticamente: {e}") print(f"🌐 Abrir manualmente: {app_url}") timer = threading.Timer(1.0, open_browser) timer.start() try: # Iniciar servidor Flask app.run(host="127.0.0.1", port=port, debug=False, use_reloader=False) except KeyboardInterrupt: print("\n🛑 Cerrando Ollama Model Manager...") except Exception as e: print(f"❌ Error al iniciar el servidor: {e}") finally: print("👋 Ollama Model Manager cerrado") if __name__ == "__main__": main()