Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import google.generativeai as genai | |
| import os | |
| import time | |
| import json | |
| from datetime import datetime | |
| from typing import List, Tuple, Optional, Dict, Any | |
| # --- Configuración de la API --- | |
| GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY') | |
| if not GOOGLE_API_KEY: | |
| raise gr.Error("La variable de entorno GOOGLE_API_KEY no está configurada. Por favor, añádela como un secreto en Hugging Face Spaces.") | |
| try: | |
| genai.configure(api_key=GOOGLE_API_KEY) | |
| _ = genai.list_models() # Prueba básica de configuración | |
| except Exception as e: | |
| raise gr.Error(f"Error al configurar la API de Google Gemini o listar modelos: {e}. Verifica tu GOOGLE_API_KEY.") | |
| # --- Gestión de Modelos Disponibles --- | |
| def get_available_models() -> List[str]: | |
| """ | |
| Obtiene y filtra los modelos de Google Gemini disponibles que soportan 'generateContent' | |
| y son adecuados para chat basados en texto. | |
| """ | |
| available_models = [] | |
| try: | |
| for m in genai.list_models(): | |
| if 'generateContent' in m.supported_generation_methods: | |
| if 'vision' not in m.name.lower() and 'tts' not in m.name.lower() and 'audio' not in m.name.lower(): | |
| available_models.append(m.name) | |
| except Exception as e: | |
| print(f"Advertencia: No se pudieron listar los modelos: {e}. Se intentará continuar sin una lista completa.") | |
| return sorted(list(set(available_models))) | |
| AVAILABLE_MODELS = get_available_models() | |
| if not AVAILABLE_MODELS: | |
| raise gr.Error("No se encontraron modelos compatibles con 'generateContent'. Verifica tu clave API y la disponibilidad de modelos.") | |
| # --- Configuración de Almacenamiento de Historial --- | |
| CHATS_DIR = "saved_chats" | |
| CHATS_FILE_PATH = os.path.join(CHATS_DIR, "chats.jsonl") | |
| # Asegurarse de que el directorio exista | |
| os.makedirs(CHATS_DIR, exist_ok=True) | |
| # --- Funciones de Gestión de Historial de Chats --- | |
| def save_chat_history(current_chat_history: List[List[Optional[str]]]) -> str: | |
| """ | |
| Guarda el historial de chat actual en un archivo JSON Lines. | |
| Genera un ID único basado en la marca de tiempo. | |
| """ | |
| if not current_chat_history: | |
| return "No hay chat para guardar." | |
| chat_id = datetime.now().strftime("%Y%m%d_%H%M%S") # ID basado en fecha y hora | |
| timestamp = datetime.now().isoformat() | |
| # Filtra el historial para guardar solo mensajes completos (no los [user, None] pendientes) | |
| # Aunque la función de Gradio debería pasar el historial ya renderizado, | |
| # esta es una precaución. | |
| filtered_history = [[u, b] for u, b in current_chat_history if u is not None and b is not None] | |
| if not filtered_history: # Si el chat está vacío después de filtrar, no lo guardamos | |
| return "Chat vacío. No se guardó." | |
| chat_data = { | |
| "id": chat_id, | |
| "timestamp": timestamp, | |
| "history": filtered_history | |
| } | |
| try: | |
| with open(CHATS_FILE_PATH, "a", encoding="utf-8") as f: | |
| f.write(json.dumps(chat_data, ensure_ascii=False) + "\n") | |
| return f"Chat '{chat_id}' guardado exitosamente." | |
| except Exception as e: | |
| return f"Error al guardar el chat: {e}" | |
| def load_all_chat_summaries() -> List[Tuple[str, str]]: | |
| """ | |
| Carga los IDs y resúmenes de todos los chats guardados para mostrarlos en un Dropdown. | |
| El formato de salida es [(valor, etiqueta), ...]. | |
| """ | |
| summaries = [] | |
| try: | |
| with open(CHATS_FILE_PATH, "r", encoding="utf-8") as f: | |
| for line in f: | |
| try: | |
| chat_data = json.loads(line) | |
| chat_id = chat_data.get("id", "ID_Desconocido") | |
| timestamp = chat_data.get("timestamp", "Fecha_Desconocida") | |
| # Puedes crear un resumen más descriptivo si lo deseas | |
| first_message = chat_data.get("history", [])[0][0] if chat_data.get("history") else "Sin mensaje inicial" | |
| summary = f"{timestamp} - {first_message[:50]}..." # Resumen del primer mensaje | |
| summaries.append((chat_id, summary)) | |
| except json.JSONDecodeError: | |
| print(f"Advertencia: Línea corrupta en el archivo de chats: {line.strip()}") | |
| except FileNotFoundError: | |
| pass # No hay chats guardados aún, no es un error | |
| except Exception as e: | |
| print(f"Error al cargar resúmenes de chat: {e}") | |
| return summaries | |
| def load_specific_chat(chat_id: str) -> List[List[Optional[str]]]: | |
| """ | |
| Carga un historial de chat específico dado su ID. | |
| """ | |
| try: | |
| with open(CHATS_FILE_PATH, "r", encoding="utf-8") as f: | |
| for line in f: | |
| try: | |
| chat_data = json.loads(line) | |
| if chat_data.get("id") == chat_id: | |
| return chat_data.get("history", []) | |
| except json.JSONDecodeError: | |
| continue # Saltar líneas corruptas | |
| except FileNotFoundError: | |
| pass | |
| return [] # Devolver historial vacío si no se encuentra o hay error | |
| # --- Función para convertir el historial de Gradio a formato de Gemini API --- | |
| def format_history_for_gemini( | |
| history: List[List[Optional[str]]] | |
| ) -> List[Dict[str, Any]]: | |
| """ | |
| Convierte el historial de chat de Gradio (lista de listas [mensaje_usuario, mensaje_bot]) | |
| al formato requerido por el historial de start_chat de la API de Gemini. | |
| """ | |
| gemini_history = [] | |
| for turn in history: | |
| if isinstance(turn, (list, tuple)) and len(turn) >= 2: | |
| user_message = turn[0] | |
| bot_message = turn[1] | |
| if user_message is not None: | |
| gemini_history.append({ | |
| 'role': 'user', | |
| 'parts': [{'text': str(user_message)}] | |
| }) | |
| if bot_message is not None: | |
| gemini_history.append({ | |
| 'role': 'model', | |
| 'parts': [{'text': str(bot_message)}] | |
| }) | |
| return gemini_history | |
| # --- Función principal de respuesta del chatbot --- | |
| def respond( | |
| history: List[List[Optional[str]]], | |
| model_name: str | |
| ) -> List[List[Optional[str]]]: | |
| """ | |
| Genera una respuesta del modelo Gemini seleccionado basada en el historial de chat. | |
| """ | |
| if not history or not isinstance(history[-1], (list, tuple)) or len(history[-1]) < 1 or history[-1][0] is None: | |
| print("Advertencia: 'respond' llamada con historial vacío o mal formado. Abortando.") | |
| yield history | |
| return | |
| current_user_message = history[-1][0] | |
| if model_name not in AVAILABLE_MODELS: | |
| error_message = f"Error: Modelo no válido seleccionado: {model_name}. Por favor, elige un modelo de la lista." | |
| print(error_message) | |
| history[-1][1] = error_message | |
| yield history | |
| return | |
| # Formatear el historial previo (excluyendo el mensaje actual del usuario) | |
| api_history_for_start = format_history_for_gemini(history[:-1]) | |
| full_response_text = "" | |
| try: | |
| model = genai.GenerativeModel(model_name=model_name) | |
| chat = model.start_chat(history=api_history_for_start) | |
| response_stream = chat.send_message(str(current_user_message), stream=True) | |
| for chunk in response_stream: | |
| if chunk.text: | |
| full_response_text += chunk.text | |
| history[-1][1] = full_response_text | |
| yield history | |
| time.sleep(0.02) | |
| except Exception as e: | |
| error_message = f"Ocurrió un error con el modelo '{model_name}': {e}" | |
| print(f"Error de API: {e}") | |
| if full_response_text: | |
| history[-1][1] = full_response_text + "\n\n" + error_message | |
| else: | |
| history[-1][1] = error_message | |
| yield history | |
| # --- Función de ayuda para eventos de Gradio --- | |
| def add_user_message_to_history( | |
| message: str, | |
| history: List[List[Optional[str]]] | |
| ) -> Tuple[str, List[List[Optional[str]]]]: | |
| """ | |
| Añade el mensaje del usuario al historial del chat y limpia el cuadro de entrada. | |
| """ | |
| if not message: | |
| return "", history | |
| return "", history + [[message, None]] | |
| # --- Interfaz de Gradio --- | |
| with gr.Blocks(theme=gr.themes.Soft()) as demo: | |
| gr.Markdown("# Chatbot con Modelos Gemini/Gemma (Solo Texto)") | |
| gr.Markdown("Selecciona un modelo y comienza a chatear. La API Key debe estar configurada como un secreto en Hugging Face (variable de entorno `GOOGLE_API_KEY`).") | |
| with gr.Row(): | |
| model_selector = gr.Dropdown( | |
| choices=AVAILABLE_MODELS, | |
| value=AVAILABLE_MODELS[0] if AVAILABLE_MODELS else None, | |
| label="Selecciona un modelo", | |
| interactive=True | |
| ) | |
| # Nota: El UserWarning sobre 'type' en Chatbot es una advertencia de Gradio | |
| # para futuras versiones. Por ahora, el formato de tuplas sigue siendo compatible. | |
| # Si quieres eliminar la advertencia y usar el nuevo formato, tendrías que | |
| # adaptar 'format_history_for_gemini' y la forma en que 'respond' maneja el historial. | |
| chatbot = gr.Chatbot(height=500, label="Chat") | |
| msg = gr.Textbox(label="Tu mensaje", placeholder="Escribe tu mensaje aquí...") | |
| with gr.Row(): | |
| # Componentes para guardar y cargar | |
| with gr.Column(scale=1): | |
| save_chat_btn = gr.Button("Guardar Chat Actual", variant="secondary") | |
| new_chat_btn = gr.Button("Nuevo Chat (guarda y limpia)", variant="secondary") # Guarda el actual y empieza uno nuevo | |
| with gr.Column(scale=2): | |
| saved_chat_selector = gr.Dropdown( | |
| choices=load_all_chat_summaries(), # Carga inicial de resúmenes | |
| label="Cargar Chat Guardado", | |
| interactive=True, | |
| allow_custom_value=False # No permitir valores personalizados | |
| ) | |
| load_chat_btn = gr.Button("Cargar Seleccionado", variant="secondary") | |
| refresh_chats_btn = gr.Button("Actualizar Chats Guardados") | |
| with gr.Row(): | |
| clear_current_chat_btn = gr.Button("Limpiar Chat Actual") # Renombrado para mayor claridad | |
| send_btn = gr.Button("Enviar", variant="primary") | |
| # --- Lógica de Eventos --- | |
| # Eventos de envío de mensaje | |
| submit_event = msg.submit( | |
| add_user_message_to_history, | |
| [msg, chatbot], | |
| [msg, chatbot], | |
| queue=False | |
| ).then( | |
| respond, | |
| inputs=[chatbot, model_selector], | |
| outputs=[chatbot] | |
| ) | |
| send_btn.click( | |
| add_user_message_to_history, | |
| [msg, chatbot], | |
| [msg, chatbot], | |
| queue=False | |
| ).then( | |
| respond, | |
| inputs=[chatbot, model_selector], | |
| outputs=[chatbot] | |
| ) | |
| # Evento para guardar el chat actual | |
| save_chat_btn.click( | |
| save_chat_history, | |
| inputs=[chatbot], | |
| outputs=[gr.Textbox(value="", interactive=False, visible=True, label="Mensaje del sistema")] # Mostrar mensaje de estado | |
| ).then( | |
| lambda: gr.update(choices=load_all_chat_summaries()), # <--- CORREGIDO AQUÍ | |
| outputs=[saved_chat_selector] | |
| ) | |
| # Evento para iniciar un nuevo chat (guarda el actual y limpia) | |
| new_chat_btn.click( | |
| save_chat_history, # Primero guarda el chat actual | |
| inputs=[chatbot], | |
| outputs=[gr.Textbox(value="", interactive=False, visible=True, label="Mensaje del sistema")] | |
| ).then( | |
| lambda: ["", [], gr.update(choices=load_all_chat_summaries())], # <--- CORREGIDO AQUÍ | |
| outputs=[msg, chatbot, saved_chat_selector] | |
| ) | |
| # Evento para cargar un chat seleccionado | |
| load_chat_btn.click( | |
| load_specific_chat, | |
| inputs=[saved_chat_selector], # El valor del dropdown es el ID del chat | |
| outputs=[chatbot] | |
| ).then( | |
| lambda: "", # Limpiar el cuadro de mensaje después de cargar | |
| outputs=[msg] | |
| ) | |
| # Evento para actualizar la lista de chats guardados | |
| refresh_chats_btn.click( | |
| lambda: gr.update(choices=load_all_chat_summaries()), # <--- CORREGIDO AQUÍ | |
| outputs=[saved_chat_selector] | |
| ) | |
| # Evento: Limpiar solo el chat actual sin guardar | |
| clear_current_chat_btn.click( | |
| lambda: ["", []], | |
| outputs=[msg, chatbot], | |
| queue=False | |
| ) | |
| # Inicializar el Dropdown de chats guardados al cargar la interfaz | |
| demo.load( | |
| lambda: gr.update(choices=load_all_chat_summaries()), # <--- CORREGIDO AQUÍ | |
| outputs=[saved_chat_selector], | |
| queue=False | |
| ) | |
| # --- Lanzar la aplicación --- | |
| demo.launch() |