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:
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
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
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
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:\\IA\prompts\prompt1_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
# ui/__init__.py
# Fichero vacío para que Python reconozca el directorio 'ui' como un paquete.
./ui/chat_tab.py
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="Enviar\n(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}\n\n", "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}\n\n", "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}\n\n", ("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
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.\n\nDetalle: {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
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
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:
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:
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):
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:
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:
