Aplicación con código fuente en Python que usa la API de la IA de Google Gemini para establecer conversaciones. Permite guardar prompts por defecto y establecer configuración de visualización y API.
- Requisitos para desarrollar una app que permita establecer una conversación con la IA de Google Gemini en Python.
- Código fuente en Python de la aplicación ProyectoA Cliente IA Google Gemini.
- Aplicación ProyectoA Cliente IA Google Gemini en funcionamiento.
Requisitos para desarrollar una app que permita establecer una conversación con la IA de Google Gemini en Python
Únicamente necesitaremos usar un IDE de desarrollo, como puede ser Visual Studio Code y disponer de una API Key de Google Gemini, que podemos obtener como indicamos en este tutorial:
La aplicación requerirá de los siguientes paquetes Python: google-generativeai y customtkinter, que podemos instalar con el comando:
|
1 |
pip install google-generativeai customtkinter |
Código fuente en Python de la aplicación ProyectoA Cliente IA Google Gemini
A continuación, mostramos el enlace a la descarga del código fuente completo en Python de la aplicación ProyectoA Cliente IA Google Gemini:
Mostramos también el código de cada fichero.
./config.py
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
import json import os # Constantes de ficheros y Logs FICHERO_PROMPTS = 'prompts.json' FICHERO_LOG = 'chat_log.txt' FICHERO_CONFIG = 'config.json' FICHERO_CLAVE_SECRETA = 'secret.key' # Constantes del modelo de IA para Gemini (modelo y valores de configuración) MODELOS_COMUNES = ["gemini-1.5-pro-latest", "gemini-1.5-flash-latest", "gemini-1.0-pro", "gemini-pro-vision"] GENERATION_CONFIG_DEFECTO = { "temperature": 0.2, "top_p": 0.95, "top_k": 40, "max_output_tokens": 8192 } # Constantes de la interfaz (colores) COLORES_POR_DEFECTO = { "fondo_memo": "#2B2B2B", "texto_usuario": "#33AFFF", "texto_ia": "#40E0D0", "fondo_seleccion": "#005A9E" } # Gestiona la carga y guardado de la configuración de la aplicación class ConfigManager: def __init__(self): self.config = {} self.cargar_configuracion() def get(self, key, default=None): return self.config.get(key, default) def set(self, key, value): self.config[key] = value # Carga el fichero de configuración o crea uno nuevo con valores por defecto def cargar_configuracion(self): config_defecto = { "clave_api_cifrada": "", "modelo_seleccionado": MODELOS_COMUNES[0], "generation_config": GENERATION_CONFIG_DEFECTO.copy(), "colores": COLORES_POR_DEFECTO.copy() } if os.path.exists(FICHERO_CONFIG): try: with open(FICHERO_CONFIG, 'r', encoding='utf-8') as f: self.config = json.load(f) # Asegura que todas las claves existan for clave, valor_defecto in config_defecto.items(): if clave not in self.config: self.config[clave] = valor_defecto elif isinstance(valor_defecto, dict): for sub_clave, sub_valor_defecto in valor_defecto.items(): if sub_clave not in self.config.get(clave, {}): self.config[clave][sub_clave] = sub_valor_defecto except json.JSONDecodeError: self.config = config_defecto else: self.config = config_defecto self.guardar_configuracion() # Guarda la configuración actual en el fichero JSON def guardar_configuracion(self): with open(FICHERO_CONFIG, 'w', encoding='utf-8') as f: json.dump(self.config, f, indent=4, ensure_ascii=False) |
./gemini_client.py
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
import google.generativeai as genai # Cliente para interactuar con la API de Google Gemini class GeminiClient: def __init__(self, config_manager, security_manager): self.config = config_manager self.security = security_manager self.modelo_ia = None self.chat = None self.modelo_actual_nombre = "" self.is_initialized = False # Inicializa la conexión con la API usando la configuración del JSON def initialize(self, modelo_override=None): clave_api_cifrada = self.config.get("clave_api_cifrada", "") clave_api = self.security.descifrar_datos(clave_api_cifrada) self.modelo_actual_nombre = modelo_override if modelo_override else self.config.get("modelo_seleccionado") if clave_api: try: genai.configure(api_key=clave_api) self.modelo_ia = genai.GenerativeModel(self.modelo_actual_nombre) self.chat = self.modelo_ia.start_chat(history=[]) self.is_initialized = True print(f"IA de Google Gemini inicializada con el modelo '{self.modelo_actual_nombre}'.") return True except Exception as e: print(f"Error al inicializar IA de Google Gemini con el modelo '{self.modelo_actual_nombre}': {e}") self.is_initialized = False return False else: print("API Key no encontrada o no válida.") self.is_initialized = False return False # Envía un mensaje al chat actual y devuelve la respuesta def send_message(self, pregunta: str): if not self.is_initialized or not self.chat: raise ConnectionError("El cliente de IA de Gemini no está inicializado.") gen_config = self.config.get("generation_config") return self.chat.send_message(pregunta, generation_config=gen_config) # Inicia una nueva sesión de chat, inicia una nueva conversación def start_new_chat(self): if self.modelo_ia: self.chat = self.modelo_ia.start_chat(history=[]) return True return False |
./main.py
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
import customtkinter as ctk import argparse import sys from pathlib import Path # Añadir el directorio raíz al path para permitir importaciones absolutas # Esto hace que el script se pueda ejecutar desde cualquier lugar file = Path(__file__).resolve() parent, root = file.parent, file.parents[1] sys.path.append(str(root)) from config import ConfigManager from security import SecurityManager from gemini_client import GeminiClient from ui.main_window import AppGemini """ Punto de entrada principal de la aplicación. 1. Parsea los argumentos de la línea de comandos. 2. Configura la apariencia visual de la aplicación. 3. Inicializa los componentes de la lógica de negocio (configuración, seguridad, cliente API). 4. Crea la ventana principal de la interfaz de usuario. 5. Inyecta los componentes de lógica en la UI. 6. Inicia el bucle de eventos de la aplicación. """ def main(): # Parsear argumentos de la línea de comandos parser = argparse.ArgumentParser( description="ProyectoA - App para interacturar con API de IA de Google Gemini.", formatter_class=argparse.RawTextHelpFormatter ) parser.add_argument( "--prompt", type=str, help="Enviar el prompt a mostrar por argumento. Admite cargar el contenido de un fichero.n" "Puede ser una cadena de texto o una ruta a un fichero usando el prefijo [FICHERO].n" "Ejemplo 1: --prompt "Resume este texto: esto es una prueba de prompt de IA" n" "Ejemplo 2: --prompt "[FICHERO]C:IApromptsprompt1_desarrollo.txt"" ) parser.add_argument( "--modelo", type=str, help="Establece este modelo para uso de la conversación actual." ) args = parser.parse_args() # Procesar el argumento --pregunta si se ha proporcionado pregunta_cargada = None if args.prompt: prompt_arg = args.prompt if prompt_arg.startswith("[FICHERO]"): # Se ha proporcionado una ruta de fichero ruta_fichero = prompt_arg.replace("[FICHERO]", "").strip() try: with open(ruta_fichero, 'r', encoding='utf-8') as f: pregunta_cargada = f.read() print(f"Prompt cargado correctamente desde '{ruta_fichero}'.") except FileNotFoundError: print(f"Error: no se pudo encontrar el fichero de prompt en '{ruta_fichero}'. La aplicación continuará sin el prompt inicial.") except Exception as e: print(f"Error al leer el fichero de promptpregunta: {e}. La aplicación continuará sin el prompt inicial.") else: # Es una cadena de texto directa pregunta_cargada = prompt_arg # 2. Configuración de la apariencia global de la aplicación ctk.set_appearance_mode("Dark") ctk.set_default_color_theme("blue") # Inicialización de los componentes de lógica de negocio #print("Inicializando gestor de configuración y seguridad...") config_manager = ConfigManager() security_manager = SecurityManager() #print("Inicializando cliente de la API de Gemini...") gemini_client = GeminiClient(config_manager, security_manager) gemini_client.initialize(modelo_override=args.modelo) # Creación y ejecución de la aplicación de la interfaz gráfica #print("Lanzando la interfaz de usuario...") app = AppGemini( config_manager=config_manager, security_manager=security_manager, gemini_client=gemini_client, pregunta_inicial=pregunta_cargada ) # Iniciar el bucle principal de la aplicación app.mainloop() if __name__ == "__main__": main() |
./ui/__init__.py
|
1 2 |
# ui/__init__.py # Fichero vacío para que Python reconozca el directorio 'ui' como un paquete. |
./ui/chat_tab.py
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 |
import customtkinter as ctk import tkinter as tk from tkinter import messagebox """ Frame que encapsula todos los widgets y la lógica de negocio para la pestaña de Chat Este componente: - Muestra la conversación entre el usuario y la IA. - Provee un campo de entrada para las preguntas del usuario. - Envía las preguntas al GeminiClient y muestra las respuestas. - Gestiona el inicio de nuevas conversaciones. - Ofrece un menú contextual para copiar, seleccionar, etc. - Aplica los temas de color definidos en la configuración. """ class ChatTab(ctk.CTkFrame): """ Inicializa la pestaña de Chat. Argumentos: master: el widget padre (la pestaña creada por CTkTabview). gemini_client: una instancia del cliente de Gemini para la comunicación con la API. config_manager: una instancia del gestor de configuración para acceder a los ajustes. status_update_callback: una función para llamar cuando se necesita actualizar la barra de estado. pregunta_inicial (str, optional): texto para precargar en la caja de preguntas. """ def __init__(self, master, gemini_client, config_manager, status_update_callback, pregunta_inicial=None): super().__init__(master, fg_color="transparent") self.gemini_client = gemini_client self.config_manager = config_manager self.status_update_callback = status_update_callback # Se accede a la instancia principal de AppGemini para usar su método de logging self.parent_app = master.master.master self.prompts = {} # Se llenará con el callback desde la pestaña de prompts self._build_ui() if pregunta_inicial: self.memo_pregunta.insert("1.0", pregunta_inicial) # Construye la interfaz de usuario de la pestaña, creando y posicionando los widgets def _build_ui(self): self.pack(expand=True, fill="both") # El frame ocupa todo el espacio de la pestaña self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) # Área de conversación self.memo_conversacion = ctk.CTkTextbox(self, state='disabled', wrap=tk.WORD) self.memo_conversacion.grid(row=0, column=0, padx=5, pady=5, sticky="nsew") self.memo_conversacion.bind("<Button-3>", self._mostrar_menu_contextual) # Marco inferior (agrupa prompts y entrada de texto) marco_inferior = ctk.CTkFrame(self, fg_color="transparent") marco_inferior.grid(row=1, column=0, sticky="ew", padx=5, pady=(0, 5)) marco_inferior.grid_columnconfigure(0, weight=1) ctk.CTkLabel(marco_inferior, text="Prompts rápidos", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, sticky="w", padx=10, pady=(5,0)) self.desplegable_prompts_rapidos = ctk.CTkComboBox(marco_inferior, values=[], command=self._al_seleccionar_prompt, state="readonly") self.desplegable_prompts_rapidos.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 10)) self.desplegable_prompts_rapidos.set("Gestiona los prompts en su pestaña") # Marco de entrada de texto y botones marco_entrada = ctk.CTkFrame(marco_inferior) marco_entrada.grid(row=2, column=0, sticky="ew") marco_entrada.grid_columnconfigure(0, weight=1) self.memo_pregunta = ctk.CTkTextbox(marco_entrada, height=120, wrap=tk.WORD) self.memo_pregunta.grid(row=0, column=0, rowspan=2, padx=10, pady=10, sticky="nsew") self.memo_pregunta.bind("<Control-Return>", self.enviar_pregunta) self.boton_nuevo_chat = ctk.CTkButton(marco_entrada, text="Nuevo Chat", command=self.iniciar_nuevo_chat, width=100, fg_color="gray40", hover_color="gray25") self.boton_nuevo_chat.grid(row=0, column=1, padx=(0,10), pady=(10,5), sticky="n") self.boton_enviar = ctk.CTkButton(marco_entrada, text="Enviarn(Ctrl+Intro)", command=self.enviar_pregunta, width=100, height=50) self.boton_enviar.grid(row=1, column=1, padx=(0,10), pady=(5,10), sticky="s") # Recoge el texto del usuario, lo envía a la API y muestra la respuesta def enviar_pregunta(self, evento=None): if not self.gemini_client.is_initialized: self.mostrar_error("La API de Gemini no está configurada. Acceda a la pestaña 'Configuración'.") return pregunta = self.memo_pregunta.get("1.0", tk.END).strip() if not pregunta: return self.memo_pregunta.delete("1.0", tk.END) texto_a_mostrar = f"Tú: {pregunta}" self.mostrar_mensaje(f"{texto_a_mostrar}nn", "pregunta") self.parent_app.registrar_log(texto_a_mostrar) try: self.boton_enviar.configure(state="disabled") self.boton_nuevo_chat.configure(state="disabled") self.status_update_callback("Esperando respuesta de la IA...") respuesta = self.gemini_client.send_message(pregunta) texto_respuesta = f"IA: {respuesta.text}" self.mostrar_mensaje(f"{texto_respuesta}nn", "respuesta") self.parent_app.registrar_log(texto_respuesta) except Exception as e: mensaje_error = f"Error al contactar con la API de Gemini: {e}" self.mostrar_error(mensaje_error) self.parent_app.registrar_log(mensaje_error) self.status_update_callback("Error") finally: # Re-habilita los botones y actualiza el estado si no hubo un error persistente self.boton_enviar.configure(state="normal") self.boton_nuevo_chat.configure(state="normal") if "Error" not in self.parent_app.barra_estado.cget("text"): self.status_update_callback("Listo") # Limpia la conversación y reinicia el historial en el cliente de Gemini def iniciar_nuevo_chat(self): if not self.gemini_client.is_initialized: self.mostrar_error("La API de Gemini no está configurada.") return if not messagebox.askyesno("Iniciar un nuevo chat...", "¿Desea iniciar un nuevo chat? La conversación actual se borrará de la pantalla."): return self._limpiar_memo_conversacion() self.gemini_client.start_new_chat() mensaje = "Nuevo chat iniciado." self.status_update_callback(mensaje) self.parent_app.registrar_log(f"--- {mensaje.upper()} ---") # Aplica los colores de la configuración a los widgets del chat def aplicar_colores_tema(self): colores = self.config_manager.get('colores', {}) self.memo_conversacion.configure(fg_color=colores.get('fondo_memo', '#2B2B2B')) self.memo_conversacion.tag_config("pregunta", foreground=colores.get('texto_usuario', '#33AFFF')) self.memo_conversacion.tag_config("respuesta", foreground=colores.get('texto_ia', '#40E0D0')) # Nos aseguramos de que el color de selección también se aplique try: self.memo_conversacion._textbox.tag_config("sel", background=colores.get('fondo_seleccion', '#005A9E')) except Exception as e: print(f"No se pudo aplicar el color de selección: {e}") """ Callback para actualizar la lista de prompts rápidos en el ComboBox Es llamado desde la ventana principal cuando los prompts cambian """ def refrescar_prompts(self, prompts_dict): self.prompts = prompts_dict titulos_prompts = sorted(list(self.prompts.keys())) self.desplegable_prompts_rapidos.configure(values=titulos_prompts) if titulos_prompts: self.desplegable_prompts_rapidos.set("Elija un prompt...") else: self.desplegable_prompts_rapidos.set("No hay prompts disponibles") # Helper para añadir texto a la caja de conversación de forma segura def mostrar_mensaje(self, mensaje, etiqueta): self.memo_conversacion.configure(state='normal') self.memo_conversacion.insert(tk.END, mensaje, etiqueta) self.memo_conversacion.configure(state='disabled') self.memo_conversacion.see(tk.END) # Auto-scroll al final # Helper para mostrar un mensaje de error con formato especial def mostrar_error(self, mensaje): self.memo_conversacion.configure(state='normal') self.memo_conversacion.insert(tk.END, f"Error: {mensaje}nn", ("error",)) self.memo_conversacion.tag_config("error", foreground="red") self.memo_conversacion.configure(state='disabled') self.memo_conversacion.see(tk.END) # Maneja la selección de un prompt del ComboBox def _al_seleccionar_prompt(self, opcion_elegida): cuerpo_prompt = self.prompts.get(opcion_elegida, "") self.memo_pregunta.delete("1.0", tk.END) self.memo_pregunta.insert("1.0", cuerpo_prompt) # Crea y muestra el menú contextual en la posición del cursor def _mostrar_menu_contextual(self, evento): menu = tk.Menu(self.memo_conversacion, tearoff=0, bg="#2B2B2B", fg="white", activebackground="#005A9E") menu.add_command(label="Copiar", command=self._copiar_texto) menu.add_command(label="Seleccionar todo", command=self._seleccionar_todo) menu.add_separator() menu.add_command(label="Limpiar pantalla", command=self._limpiar_memo_conversacion) try: menu.tk_popup(evento.x_root, evento.y_root) finally: menu.grab_release() # Copia el texto seleccionado al portapapeles def _copiar_texto(self): try: texto_seleccionado = self.memo_conversacion.get(tk.SEL_FIRST, tk.SEL_LAST) self.clipboard_clear() self.clipboard_append(texto_seleccionado) except tk.TclError: pass # Si no hay nada seleccionado, se ignora # Selecciona todo el texto en la caja de conversación def _seleccionar_todo(self): self.memo_conversacion.tag_add(tk.SEL, "1.0", tk.END) return "break" # Evita que otros bindings se activen # Borra todo el contenido de la caja de conversación def _limpiar_memo_conversacion(self): self.memo_conversacion.configure(state='normal') self.memo_conversacion.delete("1.0", tk.END) self.memo_conversacion.configure(state='disabled') |
./ui/config_tab.py
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
import customtkinter as ctk import tkinter as tk from tkinter import messagebox, colorchooser from config import MODELOS_COMUNES, GENERATION_CONFIG_DEFECTO, COLORES_POR_DEFECTO """ Frame que gestiona la pestaña de configuración, conteniendo un área de scroll para las opciones y un área fija para el botón de aplicar """ class ConfigTab(ctk.CTkFrame): def __init__(self, master, config_manager, security_manager, config_applied_callback): super().__init__(master, fg_color="transparent") # El frame principal se empaqueta para llenar la pestaña self.pack(expand=True, fill="both") # Se define un layout de grid para posicionar el área de scroll y el botón self.grid_rowconfigure(0, weight=1) # El área de scroll se expandirá verticalmente self.grid_columnconfigure(0, weight=1) # El área de scroll se expandirá horizontalmente self.config_manager = config_manager self.security_manager = security_manager self.config_applied_callback = config_applied_callback self.widgets_color = {} # Se crea un CTkScrollableFrame dentro de la clase self.scrollable_frame = ctk.CTkScrollableFrame(self, fg_color="transparent") self.scrollable_frame.grid(row=0, column=0, sticky="nsew") self.scrollable_frame.grid_columnconfigure(0, weight=1) self._build_ui() self._cargar_valores_a_ui() # Todos los widgets de opciones se añaden al 'self.scrollable_frame' def _build_ui(self): # Frame para API Key api_frame = ctk.CTkFrame(self.scrollable_frame) api_frame.pack(fill="x", padx=10, pady=10) api_frame.grid_columnconfigure(1, weight=1) ctk.CTkLabel(api_frame, text="API Key de Gemini:", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, padx=10, pady=10) self.entrada_clave_api = ctk.CTkEntry(api_frame, placeholder_text="Introduce la clave (API Key) aquí", show="*") self.entrada_clave_api.grid(row=0, column=1, padx=10, pady=10, sticky="ew") # Frame para parámetros del modelo model_params_frame = ctk.CTkFrame(self.scrollable_frame) model_params_frame.pack(fill="x", padx=10, pady=10) model_params_frame.grid_columnconfigure(1, weight=1) ctk.CTkLabel(model_params_frame, text="Configuración del modelo", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky="w") self._crear_entrada_param(model_params_frame, 1, "Modelo a usar:", "entrada_modelo") self._crear_entrada_param(model_params_frame, 2, "Temperatura:", "entrada_temperatura") self._crear_entrada_param(model_params_frame, 3, "Top-P:", "entrada_topp") self._crear_entrada_param(model_params_frame, 4, "Top-K:", "entrada_topk") self._crear_entrada_param(model_params_frame, 5, "Max. Tokens:", "entrada_maxtokens") self.entrada_modelo.configure(values=MODELOS_COMUNES) ctk.CTkButton(model_params_frame, text="Restaurar parámetros de IA por defecto", command=self._restaurar_parametros_ia).grid(row=6, column=0, columnspan=2, pady=10) # Frame para personalización de colores colors_frame = ctk.CTkFrame(self.scrollable_frame) colors_frame.pack(fill="x", padx=10, pady=10, expand=True) colors_frame.grid_columnconfigure(1, weight=1) ctk.CTkLabel(colors_frame, text="Personalización de colores", font=ctk.CTkFont(weight="bold")).grid(row=0, column=0, columnspan=3, padx=10, pady=10, sticky="w") self._crear_fila_selector_color(colors_frame, 1, "Fondo del chat:", "fondo_memo") self._crear_fila_selector_color(colors_frame, 2, "Texto de usuario (Tú):", "texto_usuario") self._crear_fila_selector_color(colors_frame, 3, "Texto de la IA:", "texto_ia") self._crear_fila_selector_color(colors_frame, 4, "Fondo de selección:", "fondo_seleccion") ctk.CTkButton(colors_frame, text="Restaurar colores por defecto", command=self._restaurar_colores_defecto).grid(row=5, column=0, columnspan=2, pady=10) # Frame de acciones action_frame = ctk.CTkFrame(self, fg_color="transparent") # El botón se añade a 'self' (el frame principal), en la fila 1 de la grid action_frame.grid(row=1, column=0, pady=(5,10), sticky="s") apply_button = ctk.CTkButton(action_frame, text="Guardar y aplicar configuración", command=self.aplicar_configuracion, height=40) apply_button.pack() def _crear_entrada_param(self, parent, row, label_text, widget_name): ctk.CTkLabel(parent, text=label_text).grid(row=row, column=0, padx=10, pady=5, sticky="w") if "modelo" in widget_name: widget = ctk.CTkComboBox(parent, values=MODELOS_COMUNES) else: widget = ctk.CTkEntry(parent) widget.grid(row=row, column=1, padx=10, pady=5, sticky="ew") setattr(self, widget_name, widget) def _cargar_valores_a_ui(self): clave_cifrada = self.config_manager.get("clave_api_cifrada", "") clave_descifrada = self.security_manager.descifrar_datos(clave_cifrada) if clave_descifrada: self.entrada_clave_api.insert(0, clave_descifrada) self.entrada_modelo.set(self.config_manager.get("modelo_seleccionado")) gen_cfg = self.config_manager.get("generation_config") self.entrada_temperatura.insert(0, str(gen_cfg.get("temperature"))) self.entrada_topp.insert(0, str(gen_cfg.get("top_p"))) self.entrada_topk.insert(0, str(gen_cfg.get("top_k"))) self.entrada_maxtokens.insert(0, str(gen_cfg.get("max_output_tokens"))) def aplicar_configuracion(self): try: clave_api = self.entrada_clave_api.get() self.config_manager.set("clave_api_cifrada", self.security_manager.cifrar_datos(clave_api)) self.config_manager.set("modelo_seleccionado", self.entrada_modelo.get()) gen_config = self.config_manager.get("generation_config") gen_config["temperature"] = float(self.entrada_temperatura.get()) gen_config["top_p"] = float(self.entrada_topp.get()) gen_config["top_k"] = int(self.entrada_topk.get()) gen_config["max_output_tokens"] = int(self.entrada_maxtokens.get()) for clave_cfg, widget in self.widgets_color.items(): self.config_manager.get("colores")[clave_cfg] = widget.cget("fg_color") except ValueError as e: messagebox.showerror("Error de dormato...", f"Valor no válido en los parámetros de IA.nnDetalle: {e}") return self.config_manager.guardar_configuracion() messagebox.showinfo("Configuración guardada...", "La configuración se ha guardado correctamente. Aplicados los cambios.") self.config_applied_callback() def _restaurar_parametros_ia(self): self.entrada_modelo.set(MODELOS_COMUNES[0]) def_cfg = GENERATION_CONFIG_DEFECTO self.entrada_temperatura.delete(0, tk.END); self.entrada_temperatura.insert(0, str(def_cfg["temperature"])) self.entrada_topp.delete(0, tk.END); self.entrada_topp.insert(0, str(def_cfg["top_p"])) self.entrada_topk.delete(0, tk.END); self.entrada_topk.insert(0, str(def_cfg["top_k"])) self.entrada_maxtokens.delete(0, tk.END); self.entrada_maxtokens.insert(0, str(def_cfg["max_output_tokens"])) def _restaurar_colores_defecto(self): for clave, valor in COLORES_POR_DEFECTO.items(): self.widgets_color[clave].configure(fg_color=valor) messagebox.showinfo("Colores restaurados...", "Pulse en 'Guardar y aplicar' para hacer los cambios permanentes.") def _crear_fila_selector_color(self, padre, fila, texto_etiqueta, clave_config): ctk.CTkLabel(padre, text=texto_etiqueta).grid(row=fila, column=0, padx=10, pady=5, sticky="w") color_actual = self.config_manager.get("colores")[clave_config] vista_previa = ctk.CTkFrame(padre, height=20, width=50, fg_color=color_actual, border_width=1, border_color="gray50") vista_previa.grid(row=fila, column=1, padx=10, pady=5, sticky="w") ctk.CTkButton(padre, text="Elegir...", width=80, command=lambda: self._elegir_color(clave_config)).grid(row=fila, column=2, padx=10, pady=5) self.widgets_color[clave_config] = vista_previa def _elegir_color(self, clave_config): color_inicial = self.widgets_color[clave_config].cget("fg_color") color_elegido = colorchooser.askcolor(title=f"Elija un color para {clave_config}", initialcolor=color_inicial) if color_elegido and color_elegido[1]: self.widgets_color[clave_config].configure(fg_color=color_elegido[1]) |
./ui/main_window.py
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
import customtkinter as ctk from datetime import datetime from config import FICHERO_LOG from .chat_tab import ChatTab from .prompts_tab import PromptsTab from .config_tab import ConfigTab """ Clase principal de la aplicación. Gestiona la ventana, las pestañas y la comunicación entre los componentes de la UI y la lógica de negocio """ class AppGemini(ctk.CTk): def __init__(self, config_manager, security_manager, gemini_client, pregunta_inicial=None): super().__init__() self.config_manager = config_manager self.security_manager = security_manager self.gemini_client = gemini_client self.pregunta_inicial = pregunta_inicial # Configuración de la ventana principal self.title("ProyectoA - Cliente IA Google Gemini") self.ancho_inicial, self.alto_inicial = 950, 700 self.geometry(f"{self.ancho_inicial}x{self.alto_inicial}") self.centrar_ventana() # Creación de las pestañas self.vista_pestanas = ctk.CTkTabview(self, anchor="w") self.vista_pestanas.pack(expand=True, fill="both", padx=10, pady=5) # Instanciar cada pestaña, pasándole los gestores y callbacks necesarios self.chat_tab = ChatTab( master=self.vista_pestanas.add("Chat"), gemini_client=self.gemini_client, config_manager=self.config_manager, status_update_callback=self.actualizar_barra_estado, pregunta_inicial=self.pregunta_inicial ) self.prompts_tab = PromptsTab( master=self.vista_pestanas.add("Gestionar prompts"), prompts_updated_callback=self._on_prompts_updated ) self.config_tab = ConfigTab( master=self.vista_pestanas.add("Configuración"), config_manager=self.config_manager, security_manager=self.security_manager, config_applied_callback=self._on_config_applied ) # Barra de estado self.barra_estado = ctk.CTkLabel(self, text="", anchor="w") self.barra_estado.pack(side="bottom", fill="x", padx=10, pady=(0, 5)) # Inicialización Final # CORRECCIÓN: Se añade esta línea. # Indicamos a la pestaña de Chat que cargue la lista de prompst del fichero JSON self.chat_tab.refrescar_prompts(self.prompts_tab.prompts_predefinidos) self.chat_tab.aplicar_colores_tema() self.actualizar_barra_estado() self.registrar_log("Aplicación iniciada") # Centra la ventana principal en la pantalla def centrar_ventana(self): self.update_idletasks() ancho_pantalla, alto_pantalla = self.winfo_screenwidth(), self.winfo_screenheight() x = (ancho_pantalla / 2) - (self.ancho_inicial / 2) y = (alto_pantalla / 2) - (self.alto_inicial / 2) self.geometry(f'{self.ancho_inicial}x{self.alto_inicial}+{int(x)}+{int(y)}') # Actualiza el texto de la barra de estado inferior def actualizar_barra_estado(self, texto_estado=None): if texto_estado is None: texto_estado = "Iniciada OK" if self.gemini_client.is_initialized else "API Key no configurada" modelo_nombre = self.gemini_client.modelo_actual_nombre or "N/A" texto_completo = f"Modelo: {modelo_nombre} | IA: Gemini | Estado: {texto_estado}" self.barra_estado.configure(text=texto_completo) self.update_idletasks() # Registra un mensaje en el fichero de log con marca de tiempo def registrar_log(self, texto): with open(FICHERO_LOG, 'a', encoding='utf-8') as archivo: marca_tiempo = datetime.now().strftime("%Y-%m-%d %H:%M:%S") archivo.write(f"[{marca_tiempo}] {texto}n") # --- Callbacks para la comunicación entre componentes --- """ Callback que se ejecuta cuando los prompts son modificados en la pestaña PromptsTab Refresca la lista de prompts en la petaña ChatTab """ def _on_prompts_updated(self): self.chat_tab.refrescar_prompts(self.prompts_tab.prompts_predefinidos) """ Callback que se ejecuta cuando se guarda una nueva configuración en ConfigTab Re-inicializa el cliente de Gemini y aplica los cambios visuales """ def _on_config_applied(self): self.gemini_client.initialize() self.chat_tab.aplicar_colores_tema() self.actualizar_barra_estado() |
./ui/prompts_tab.py
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 |
import customtkinter as ctk import tkinter as tk from tkinter import messagebox import json import os from config import FICHERO_PROMPTS # Frame que contiene todos los widgets y la lógica para gestionar los prompts class PromptsTab(ctk.CTkFrame): def __init__(self, master, prompts_updated_callback): super().__init__(master, fg_color="transparent") self.prompts_updated_callback = prompts_updated_callback # Indicamos al frame que se dibuje y ocupe todo el espacio disponible dentro de la pestaña que lo contiene self.pack(expand=True, fill="both") self.prompts_predefinidos = self._cargar_prompts() self._build_ui() self._actualizar_estado_widgets('inicio') self.refrescar_desplegables() def _build_ui(self): self.grid_columnconfigure(1, weight=1) self.grid_rowconfigure(5, weight=1) ctk.CTkLabel(self, text="Selecciona un prompt para editarlo o eliminarlo:").grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky="w") self.desplegable_gestion = ctk.CTkComboBox(self, values=[], command=self._on_prompt_select, state="readonly") self.desplegable_gestion.grid(row=1, column=0, columnspan=2, padx=10, pady=5, sticky="ew") ctk.CTkLabel(self, text="Título del prompt:").grid(row=2, column=0, padx=10, pady=(10,0), sticky="w") self.entrada_titulo_prompt = ctk.CTkEntry(self, state="disabled") self.entrada_titulo_prompt.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky="ew") ctk.CTkLabel(self, text="Contenido del prompt:").grid(row=4, column=0, padx=10, pady=(10,0), sticky="w") self.texto_cuerpo_prompt = ctk.CTkTextbox(self, wrap=tk.WORD, height=200, state="disabled") self.texto_cuerpo_prompt.grid(row=5, column=0, columnspan=2, padx=10, pady=5, sticky="nsew") # --- Botones de Acción --- marco_botones = ctk.CTkFrame(self) marco_botones.grid(row=6, column=0, columnspan=2, pady=10) self.boton_guardar = ctk.CTkButton(marco_botones, text="Guardar cambios", command=self.guardar_cambios_prompt) self.boton_guardar.pack(side="left", padx=5) self.boton_nuevo = ctk.CTkButton(marco_botones, text="Añadir nuevo", command=self._preparar_nuevo_prompt) self.boton_nuevo.pack(side="left", padx=5) self.boton_eliminar = ctk.CTkButton(marco_botones, text="Eliminar", command=self.eliminar_prompt, fg_color="#D32F2F", hover_color="#B71C1C") self.boton_eliminar.pack(side="left", padx=5) # El botón cancelar se mostrará/ocultará dinámicamente self.boton_cancelar = ctk.CTkButton(marco_botones, text="Cancelar", command=self._cancelar_edicion, fg_color="gray40", hover_color="gray25") def _cargar_prompts(self): if not os.path.exists(FICHERO_PROMPTS): return {"Ejemplo": "Este es un prompt de ejemplo que puedes editar o eliminar."} try: with open(FICHERO_PROMPTS, 'r', encoding='utf-8') as archivo: return json.load(archivo) except (json.JSONDecodeError, FileNotFoundError): return {} def _guardar_prompts(self): with open(FICHERO_PROMPTS, 'w', encoding='utf-8') as archivo: json.dump(self.prompts_predefinidos, archivo, indent=2, ensure_ascii=False) self.refrescar_desplegables() self.prompts_updated_callback() def _on_prompt_select(self, opcion_elegida): self._actualizar_estado_widgets('seleccionado') titulo = opcion_elegida cuerpo = self.prompts_predefinidos.get(titulo, "") self.entrada_titulo_prompt.delete(0, tk.END) self.entrada_titulo_prompt.insert(0, titulo) self.texto_cuerpo_prompt.delete("1.0", tk.END) self.texto_cuerpo_prompt.insert("1.0", cuerpo) def guardar_cambios_prompt(self): nuevo_titulo = self.entrada_titulo_prompt.get().strip() cuerpo = self.texto_cuerpo_prompt.get("1.0", tk.END).strip() titulo_original = self.desplegable_gestion.get() if not nuevo_titulo or not cuerpo: messagebox.showwarning("Campos vacíos", "El título y el contenido del prompt no pueden estar vacíos.") return if titulo_original and titulo_original != nuevo_titulo and titulo_original in self.prompts_predefinidos: del self.prompts_predefinidos[titulo_original] self.prompts_predefinidos[nuevo_titulo] = cuerpo self._guardar_prompts() messagebox.showinfo("Prompt guardado...", f"Prompt '{nuevo_titulo}' guardado correctamente.") def eliminar_prompt(self): titulo_a_eliminar = self.desplegable_gestion.get() if not titulo_a_eliminar or titulo_a_eliminar not in self.prompts_predefinidos: messagebox.showwarning("Sin selección...", "Seleccione un prompt para eliminar.") return if messagebox.askyesno("Confirmar...", f"¿Desea eliminar el prompt '{titulo_a_eliminar}'?"): del self.prompts_predefinidos[titulo_a_eliminar] self._guardar_prompts() messagebox.showinfo("Prompt eliminado...", "Prompt eliminado correctamente.") def refrescar_desplegables(self): titulos = sorted(list(self.prompts_predefinidos.keys())) self.desplegable_gestion.configure(values=titulos) self._limpiar_campos_edicion() self._actualizar_estado_widgets('inicio') def _limpiar_campos_edicion(self): self.desplegable_gestion.set("") self.entrada_titulo_prompt.delete(0, tk.END) self.texto_cuerpo_prompt.delete("1.0", tk.END) def _preparar_nuevo_prompt(self): self._limpiar_campos_edicion() self.entrada_titulo_prompt.focus() self._actualizar_estado_widgets('nuevo') def _cancelar_edicion(self): self._limpiar_campos_edicion() self._actualizar_estado_widgets('inicio') def _actualizar_estado_widgets(self, estado): if estado == 'inicio': self.desplegable_gestion.configure(state="readonly") self.entrada_titulo_prompt.configure(state="disabled") self.texto_cuerpo_prompt.configure(state="disabled") self.boton_guardar.configure(state="disabled") self.boton_nuevo.configure(state="normal") self.boton_eliminar.configure(state="disabled") self.boton_cancelar.pack_forget() elif estado == 'seleccionado': self.entrada_titulo_prompt.configure(state="normal") self.texto_cuerpo_prompt.configure(state="normal") self.boton_guardar.configure(state="normal") self.boton_nuevo.configure(state="normal") self.boton_eliminar.configure(state="normal") self.boton_cancelar.pack_forget() elif estado == 'nuevo': self.desplegable_gestion.configure(state="disabled") self.entrada_titulo_prompt.configure(state="normal") self.texto_cuerpo_prompt.configure(state="normal") self.boton_guardar.configure(state="normal") self.boton_nuevo.configure(state="disabled") self.boton_eliminar.configure(state="disabled") self.boton_cancelar.pack(side="left", padx=5) |
Aplicación ProyectoA Cliente IA Google Gemini en funcionamiento
Una vez obtenida la API Key de Google Gemini:

Iniciaremos la aplicación, ejecutando el siguiente comando en la carpeta donde hayamos descomprimido los ficheros de código fuente en Python, donde se encuentre el fichero main.py:
|
1 |
python main.py |
Pulsaremos en la pestaña «Configuración» y pegaremos la API Key de Gemini. Aprovecharemos para establecer las personalizaciones que consideremos (modelo, temperatura, top-p, top-k, tokens máximos, colores, etc. Pulsaremos en «Guardar y aplicar configuración».

Tras pulsar en «Guardar y aplicar configuración» nos mostrará este mensaje:

A partir de ahora podremos usar la aplicación para entablar conversaciones con la IA de Gemini. Pulsaremos en la pestaña «Chat» para realizar una prueba, escribiremos el siguiente prompt para revisar que el API Key funciona correctamente:
Solo dime si el API funciona correctamente, si tengo conexión con la IA de Gémini
Si el API Key es correcta, la IA devolverá un mensaje como este:
Si estás recibiendo respuestas de mí, entonces sí, el API funciona correctamente y tienes conexión con la IA de Gemini.
La aplicación permite añadir prompts por defecto, para ello, pulsaremos en la pestaña «Gestionar prompts». En esta pestaña, pulsaremos en «Añadir nuevo»:

Introduciremos el título del prompt (para identificarlo en el desplegable) e introduciremos el prompt que se enviará a la IA de Gemini en «Contenido del prompt». Una vez introducidos estos datos pulsaremos en «Guardar cambios»:

La app nos indicará que el prompt ha sido guardado correctamente:

Ahora lo tendremos disponible para usarlo desde la pestaña de «Chat», eligiendo en el desplegable el prompt añadido:

El prompt se introducirá automáticamente, si no queremos hacer modificaciones, pulsaremos Control + INTRO o el botón «Enviar» para enviar la petición a la IA de Gemini:

La IA de Gemini nos contestará y nos mostrará, en este caso, la función en Delphi que obtiene el HASH MD5 de un texto pasado por argumento:

Desde la pestaña «Gestionar prompts» añadiremos, modificaremos o eliminaremos los prompts que queramos tener disponibles para uso rápido:

La aplicación también admite pasarle como argumento por la línea de comandos un prompt que cargará por defecto, por ejemplo:
|
1 |
python main.py --prompt "Muéstrame una contraseña segura de, al menos, 12 caracteres con letras mayúsculas y minúsculas, números y caracteres especiales" |

Iniciará la aplicación con el prompt pasado por argumento:

También podremos pasar el prompt desde un fichero de texto. Por ejemplo, si tenemos el fichero de texto prompt_json.txt con el siguiente contenido (comentemos dos errores de sintaxis para verificar que la IA nos muestra que la sintaxis del JSON no es correcta y las líneas donde hay errores):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Dime si el siguiente JSON tiene una sintaxis correcta, solo dime si es correcta o no, no me des explicaciones. En caso de que haya alguna línea incorrecta, muéstrame las líneas con error: { "clave_api_cifrada": "", "modelo_seleccionado": "gemini-1.5-pro-latest", "generation_config": { "temperature": 0.2, "top_p": 0.95, "top_k": 40, "max_output_tokens" 8192 }, "colores": { "fondo_memo": "#2B2B2B", "texto_usuario": "#33AFFF", "texto_ia": "#40E0D0", "fondo_seleccion": "#005A9E } } |
Para cargar el prompt del fichero anterior, ejecutaremos el comando:
|
1 |
python main.py --prompt "[FICHERO]prompt_json.txt" |

La aplicación se iniciará con el prompt por defecto del contenido del fichero introducido por argumento. Si es correcto, pulsaremos en «Enviar» para enviarlo a la IA de Gemini:
