llm_lab_perso/utils/chat_ui.py
2025-03-27 18:40:52 +01:00

980 lines
40 KiB
Python

"""
Interface graphique pour le chat avec les modèles LLM
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox, filedialog
import json
import os
from datetime import datetime
from utils.agent_manager import AgentManager
from core.factory import LLMFactory
class ChatUI:
"""Interface graphique pour interagir avec les agents LLM"""
def __init__(self, root):
"""Initialise l'interface graphique"""
self.root = root
self.root.title("LLM Lab - Chat")
self.root.geometry("1000x700")
self.root.minsize(800, 600)
# Variables pour le chat
self.current_agent = None
self.conversation_history = []
self.custom_params = {}
self.history_files = []
# Style
self.style = ttk.Style()
self.style.theme_use('alt') # 'clam', 'alt', 'default', 'classic'
# Création de l'interface
self._create_ui()
# Chargement de la liste des agents
self._load_agents()
# Chargement de l'historique des conversations
self._load_conversation_history()
def _create_ui(self):
"""Crée l'interface utilisateur"""
# Interface principale en deux parties
self.main_paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
self.main_paned.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Panneau de gauche : configuration et historique
self.left_frame = ttk.Frame(self.main_paned, width=300)
self.main_paned.add(self.left_frame, weight=1)
# Panneau de droite : chat
self.right_frame = ttk.Frame(self.main_paned)
self.main_paned.add(self.right_frame, weight=3)
# Configuration du panneau de gauche
self._create_config_panel()
# Configuration du panneau de droite
self._create_chat_panel()
def _create_config_panel(self):
"""Crée le panneau de configuration (gauche)"""
# Utilisation d'un notebook pour organiser les paramètres
self.config_notebook = ttk.Notebook(self.left_frame)
self.config_notebook.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Onglet 1: Sélection d'agent
self.agent_frame = ttk.Frame(self.config_notebook)
self.config_notebook.add(self.agent_frame, text="Agent")
# Onglet 2: Paramètres
self.params_frame = ttk.Frame(self.config_notebook)
self.config_notebook.add(self.params_frame, text="Paramètres")
# Onglet 3: Historique
self.history_frame = ttk.Frame(self.config_notebook)
self.config_notebook.add(self.history_frame, text="Historique")
# Remplissage de l'onglet Agent
self._create_agent_tab()
# Remplissage de l'onglet Paramètres
self._create_params_tab()
# Remplissage de l'onglet Historique
self._create_history_tab()
def _create_agent_tab(self):
"""Crée l'onglet de sélection d'agent"""
# Label et liste des agents
ttk.Label(self.agent_frame, text="Sélectionnez un agent:",
font=("Arial", 11, "bold")).pack(pady=(10, 5), padx=5, anchor=tk.W)
# Frame pour la liste des agents
agent_list_frame = ttk.Frame(self.agent_frame)
agent_list_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Scrollbar
scrollbar = ttk.Scrollbar(agent_list_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# Liste des agents
self.agent_listbox = tk.Listbox(agent_list_frame, yscrollcommand=scrollbar.set,
selectmode=tk.SINGLE, activestyle='dotbox',
font=("Arial", 10))
self.agent_listbox.pack(fill=tk.BOTH, expand=True)
scrollbar.config(command=self.agent_listbox.yview)
# Gestionnaire d'événements pour la sélection d'agent
self.agent_listbox.bind('<<ListboxSelect>>', self._on_agent_select)
# Informations sur l'agent
agent_info_frame = ttk.LabelFrame(self.agent_frame, text="Informations sur l'agent")
agent_info_frame.pack(fill=tk.X, padx=5, pady=5)
# Description de l'agent
ttk.Label(agent_info_frame, text="Description:").pack(anchor=tk.W, padx=5, pady=(5, 0))
self.agent_description = tk.Text(agent_info_frame, wrap=tk.WORD, height=5,
width=30, font=("Arial", 9))
self.agent_description.pack(fill=tk.X, padx=5, pady=5)
self.agent_description.config(state=tk.DISABLED)
# Modèle utilisé
model_frame = ttk.Frame(agent_info_frame)
model_frame.pack(fill=tk.X, padx=5, pady=(0, 5))
ttk.Label(model_frame, text="Modèle:").pack(side=tk.LEFT, padx=(0, 5))
self.model_label = ttk.Label(model_frame, text="-")
self.model_label.pack(side=tk.LEFT)
# Bouton pour changer de modèle
self.model_combo = ttk.Combobox(self.agent_frame, state="readonly")
self.model_combo.pack(fill=tk.X, padx=5, pady=5)
# Événement de changement
self.model_combo.bind("<<ComboboxSelected>>", self._on_model_change)
# Bouton de test
test_btn = ttk.Button(self.agent_frame, text="Tester l'agent",
command=self._test_agent)
test_btn.pack(fill=tk.X, padx=5, pady=(0, 10))
def _create_params_tab(self):
"""Crée l'onglet de configuration des paramètres"""
# Canvas et scrollbar pour permettre le défilement
canvas = tk.Canvas(self.params_frame)
scrollbar = ttk.Scrollbar(self.params_frame, orient="vertical", command=canvas.yview)
scroll_frame = ttk.Frame(canvas)
scroll_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scroll_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# Titre
ttk.Label(scroll_frame, text="Paramètres du modèle:",
font=("Arial", 11, "bold")).pack(pady=(10, 5), padx=5, anchor=tk.W)
# Température
temp_frame = ttk.Frame(scroll_frame)
temp_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(temp_frame, text="Température:").pack(side=tk.LEFT, padx=(0, 5))
self.temp_var = tk.DoubleVar(value=0.7)
temp_scale = ttk.Scale(temp_frame, from_=0.0, to=2.0, orient=tk.HORIZONTAL,
variable=self.temp_var, length=150)
temp_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
temp_label = ttk.Label(temp_frame, textvariable=self.temp_var, width=5)
temp_label.pack(side=tk.LEFT)
self.temp_var.trace_add("write", self._on_param_change)
# Top-p
top_p_frame = ttk.Frame(scroll_frame)
top_p_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(top_p_frame, text="Top-p:").pack(side=tk.LEFT, padx=(0, 5))
self.top_p_var = tk.DoubleVar(value=0.9)
top_p_scale = ttk.Scale(top_p_frame, from_=0.0, to=1.0, orient=tk.HORIZONTAL,
variable=self.top_p_var, length=150)
top_p_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
top_p_label = ttk.Label(top_p_frame, textvariable=self.top_p_var, width=5)
top_p_label.pack(side=tk.LEFT)
self.top_p_var.trace_add("write", self._on_param_change)
# Top-k
top_k_frame = ttk.Frame(scroll_frame)
top_k_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(top_k_frame, text="Top-k:").pack(side=tk.LEFT, padx=(0, 5))
self.top_k_var = tk.IntVar(value=40)
top_k_scale = ttk.Scale(top_k_frame, from_=0, to=100, orient=tk.HORIZONTAL,
variable=self.top_k_var, length=150)
top_k_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
top_k_label = ttk.Label(top_k_frame, textvariable=self.top_k_var, width=5)
top_k_label.pack(side=tk.LEFT)
self.top_k_var.trace_add("write", self._on_param_change)
# Repeat penalty
repeat_frame = ttk.Frame(scroll_frame)
repeat_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(repeat_frame, text="Repeat Penalty:").pack(side=tk.LEFT, padx=(0, 5))
self.repeat_var = tk.DoubleVar(value=1.1)
repeat_scale = ttk.Scale(repeat_frame, from_=1.0, to=2.0, orient=tk.HORIZONTAL,
variable=self.repeat_var, length=150)
repeat_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
repeat_label = ttk.Label(repeat_frame, textvariable=self.repeat_var, width=5)
repeat_label.pack(side=tk.LEFT)
self.repeat_var.trace_add("write", self._on_param_change)
# Tokens max
tokens_frame = ttk.Frame(scroll_frame)
tokens_frame.pack(fill=tk.X, padx=5, pady=5)
ttk.Label(tokens_frame, text="Tokens max:").pack(side=tk.LEFT, padx=(0, 5))
self.tokens_var = tk.IntVar(value=512)
tokens_scale = ttk.Scale(tokens_frame, from_=32, to=4096, orient=tk.HORIZONTAL,
variable=self.tokens_var, length=150)
tokens_scale.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
tokens_label = ttk.Label(tokens_frame, textvariable=self.tokens_var, width=5)
tokens_label.pack(side=tk.LEFT)
self.tokens_var.trace_add("write", self._on_param_change)
# Boutons de gestion
buttons_frame = ttk.Frame(scroll_frame)
buttons_frame.pack(fill=tk.X, padx=5, pady=10)
self.reset_btn = ttk.Button(buttons_frame, text="Réinitialiser",
command=self._reset_params)
self.reset_btn.pack(side=tk.LEFT, padx=(0, 5))
self.save_btn = ttk.Button(buttons_frame, text="Enregistrer",
command=self._save_params)
self.save_btn.pack(side=tk.LEFT)
# Séparateur
ttk.Separator(scroll_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, padx=5, pady=10)
# Système de presets
ttk.Label(scroll_frame, text="Presets:",
font=("Arial", 11, "bold")).pack(pady=(5, 5), padx=5, anchor=tk.W)
presets_frame = ttk.Frame(scroll_frame)
presets_frame.pack(fill=tk.X, padx=5, pady=5)
self.preset_combo = ttk.Combobox(presets_frame, values=[
"Créatif", "Précis", "Équilibré", "Code", "Conversation"
])
self.preset_combo.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5))
self.preset_combo.current(2) # Par défaut: Équilibré
self.apply_preset_btn = ttk.Button(presets_frame, text="Appliquer",
command=self._apply_preset)
self.apply_preset_btn.pack(side=tk.LEFT)
def _create_history_tab(self):
"""Crée l'onglet d'historique des conversations"""
# Label
ttk.Label(self.history_frame, text="Historique des conversations:",
font=("Arial", 11, "bold")).pack(pady=(10, 5), padx=5, anchor=tk.W)
# Liste des conversations
history_list_frame = ttk.Frame(self.history_frame)
history_list_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
scrollbar = ttk.Scrollbar(history_list_frame)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.history_listbox = tk.Listbox(history_list_frame, yscrollcommand=scrollbar.set,
font=("Arial", 10))
self.history_listbox.pack(fill=tk.BOTH, expand=True)
scrollbar.config(command=self.history_listbox.yview)
# Événement de sélection
self.history_listbox.bind('<<ListboxSelect>>', self._on_history_select)
# Boutons
button_frame = ttk.Frame(self.history_frame)
button_frame.pack(fill=tk.X, padx=5, pady=(0, 10))
self.load_history_btn = ttk.Button(button_frame, text="Charger",
command=self._load_selected_history)
self.load_history_btn.pack(side=tk.LEFT, padx=(0, 5))
self.delete_history_btn = ttk.Button(button_frame, text="Supprimer",
command=self._delete_history)
self.delete_history_btn.pack(side=tk.LEFT, padx=(0, 5))
self.export_history_btn = ttk.Button(button_frame, text="Exporter",
command=self._export_history)
self.export_history_btn.pack(side=tk.LEFT)
def _create_chat_panel(self):
"""Crée le panneau de chat (droite)"""
# Titre
chat_title_frame = ttk.Frame(self.right_frame)
chat_title_frame.pack(fill=tk.X, padx=5, pady=5)
self.chat_title = ttk.Label(chat_title_frame, text="Chat - Aucun agent sélectionné",
font=("Arial", 12, "bold"))
self.chat_title.pack(side=tk.LEFT)
# Zone de chat
chat_frame = ttk.Frame(self.right_frame)
chat_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Zone d'affichage des messages
self.chat_display = scrolledtext.ScrolledText(chat_frame, wrap=tk.WORD,
font=("Arial", 10))
self.chat_display.pack(fill=tk.BOTH, expand=True)
self.chat_display.config(state=tk.DISABLED)
# Zone de saisie
input_frame = ttk.Frame(self.right_frame)
input_frame.pack(fill=tk.X, padx=5, pady=(0, 5))
self.user_input = scrolledtext.ScrolledText(input_frame, wrap=tk.WORD,
height=3, font=("Arial", 10))
self.user_input.pack(fill=tk.X, pady=(0, 5))
self.user_input.bind("<Shift-Return>", self._on_shift_enter)
# Boutons d'action
button_frame = ttk.Frame(self.right_frame)
button_frame.pack(fill=tk.X, padx=5, pady=(0, 5))
self.send_btn = ttk.Button(button_frame, text="Envoyer", width=10,
command=self._send_message)
self.send_btn.pack(side=tk.RIGHT, padx=(5, 0))
self.clear_btn = ttk.Button(button_frame, text="Effacer", width=10,
command=self._clear_chat)
self.clear_btn.pack(side=tk.RIGHT, padx=(5, 0))
self.new_chat_btn = ttk.Button(button_frame, text="Nouvelle conversation", width=20,
command=self._new_chat)
self.new_chat_btn.pack(side=tk.RIGHT)
# Barre d'état
self.status_bar = ttk.Label(self.right_frame, text="Prêt", relief=tk.SUNKEN, anchor=tk.W)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
def _load_agents(self):
"""Charge la liste des agents disponibles"""
# Effacer la liste actuelle
self.agent_listbox.delete(0, tk.END)
# Récupérer les agents
agents_info = AgentManager.list_agents()
# Ajouter les agents à la liste
for agent_name in agents_info:
self.agent_listbox.insert(tk.END, agent_name)
# Charger la liste des modèles disponibles
self._load_models()
def _load_models(self):
"""Charge la liste des modèles disponibles"""
try:
# Récupérer les modèles disponibles
models = LLMFactory.get_available_models()
self.model_combo['values'] = models
except Exception as e:
print(f"Erreur lors du chargement des modèles: {str(e)}")
self.model_combo['values'] = ["mistral:latest", "codellama:13b-python", "llama2:13b"]
def _on_agent_select(self, event):
"""Gestionnaire d'événement pour la sélection d'un agent"""
# Récupérer l'index sélectionné
selection = self.agent_listbox.curselection()
if not selection:
return
# Récupérer le nom de l'agent
agent_name = self.agent_listbox.get(selection[0])
# Récupérer les informations de l'agent
agents_info = AgentManager.list_agents()
if agent_name not in agents_info:
return
agent_info = agents_info[agent_name]
# Mettre à jour les informations affichées
self.agent_description.config(state=tk.NORMAL)
self.agent_description.delete(1.0, tk.END)
self.agent_description.insert(tk.END, agent_info['description'])
self.agent_description.config(state=tk.DISABLED)
# Mettre à jour le modèle
model_name = agent_info['model']
self.model_label.config(text=model_name)
# Sélectionner le modèle dans la combobox
if model_name in self.model_combo['values']:
self.model_combo.set(model_name)
# Mettre à jour le titre du chat
self.chat_title.config(text=f"Chat avec {agent_name} ({model_name})")
# Mettre à jour l'agent courant
self.current_agent = agent_name
# Charger les paramètres de l'agent
self._load_agent_params(agent_name)
# Mettre à jour le statut
self.status_bar.config(text=f"Agent {agent_name} sélectionné avec le modèle {model_name}")
def _on_model_change(self, event):
"""Gestionnaire d'événement pour le changement de modèle"""
if not self.current_agent:
messagebox.showwarning("Aucun agent sélectionné",
"Veuillez d'abord sélectionner un agent.")
return
# Récupérer le modèle sélectionné
model_name = self.model_combo.get()
# Mettre à jour l'affichage
self.model_label.config(text=model_name)
# Mettre à jour le titre du chat
self.chat_title.config(text=f"Chat avec {self.current_agent} ({model_name})")
# Mettre à jour les paramètres du modèle
self._load_model_params(model_name)
# Mettre à jour le statut
self.status_bar.config(text=f"Modèle changé pour {model_name}")
def _test_agent(self):
"""Fonction de test rapide de l'agent sélectionné"""
if not self.current_agent:
messagebox.showwarning("Aucun agent sélectionné",
"Veuillez d'abord sélectionner un agent.")
return
# Message de test
test_message = "Présente-toi brièvement et explique tes capacités."
# Ajouter le message au chat
self._add_message_to_chat("Utilisateur", test_message)
# Générer une réponse
try:
# Mettre à jour le statut
self.status_bar.config(text=f"Génération en cours...")
self.root.update()
# Créer l'agent avec les paramètres personnalisés
agent = self._create_agent_with_custom_params()
# Générer la réponse
response = agent.generate(test_message)
# Ajouter la réponse au chat
self._add_message_to_chat(self.current_agent, response)
# Mettre à jour le statut
self.status_bar.config(text=f"Test terminé")
except Exception as e:
messagebox.showerror("Erreur", f"Erreur lors du test de l'agent: {str(e)}")
self.status_bar.config(text=f"Erreur lors du test")
def _on_param_change(self, *args):
"""Gestionnaire d'événement pour le changement des paramètres"""
if not self.current_agent:
return
# Mettre à jour les paramètres personnalisés
self.custom_params = {
"temperature": self.temp_var.get(),
"top_p": self.top_p_var.get(),
"top_k": self.top_k_var.get(),
"repeat_penalty": self.repeat_var.get(),
"num_predict": self.tokens_var.get()
}
def _reset_params(self):
"""Réinitialise les paramètres aux valeurs par défaut de l'agent"""
if not self.current_agent:
messagebox.showwarning("Aucun agent sélectionné",
"Veuillez d'abord sélectionner un agent.")
return
# Charger les paramètres de l'agent
self._load_agent_params(self.current_agent)
# Mettre à jour le statut
self.status_bar.config(text=f"Paramètres réinitialisés")
def _save_params(self):
"""Enregistre les paramètres personnalisés"""
if not self.current_agent:
messagebox.showwarning("Aucun agent sélectionné",
"Veuillez d'abord sélectionner un agent.")
return
# Créer le dossier de sauvegarde si nécessaire
save_dir = "saved_params"
os.makedirs(save_dir, exist_ok=True)
# Nom du fichier de sauvegarde
file_name = f"{save_dir}/{self.current_agent}_{self.model_combo.get()}.json"
# Enregistrer les paramètres
with open(file_name, "w") as f:
json.dump(self.custom_params, f, indent=2)
# Mettre à jour le statut
self.status_bar.config(text=f"Paramètres enregistrés dans {file_name}")
def _apply_preset(self):
"""Applique un preset de paramètres"""
preset = self.preset_combo.get()
# Définir les presets
presets = {
"Créatif": {
"temperature": 1.2,
"top_p": 0.95,
"top_k": 60,
"repeat_penalty": 1.0,
"num_predict": 1024
},
"Précis": {
"temperature": 0.3,
"top_p": 0.85,
"top_k": 30,
"repeat_penalty": 1.2,
"num_predict": 512
},
"Équilibré": {
"temperature": 0.7,
"top_p": 0.9,
"top_k": 40,
"repeat_penalty": 1.1,
"num_predict": 768
},
"Code": {
"temperature": 0.2,
"top_p": 0.95,
"top_k": 30,
"repeat_penalty": 1.2,
"num_predict": 2048
},
"Conversation": {
"temperature": 0.8,
"top_p": 0.92,
"top_k": 50,
"repeat_penalty": 1.05,
"num_predict": 512
}
}
# Appliquer le preset
if preset in presets:
params = presets[preset]
# Mettre à jour les variables
self.temp_var.set(params["temperature"])
self.top_p_var.set(params["top_p"])
self.top_k_var.set(params["top_k"])
self.repeat_var.set(params["repeat_penalty"])
self.tokens_var.set(params["num_predict"])
# Mettre à jour les paramètres personnalisés
self.custom_params = params.copy()
# Mettre à jour le statut
self.status_bar.config(text=f"Preset '{preset}' appliqué")
def _load_agent_params(self, agent_name):
"""Charge les paramètres par défaut de l'agent"""
try:
# Récupérer la configuration de l'agent
from agents.roles import AGENTS
if agent_name in AGENTS:
agent_config = AGENTS[agent_name]
params = agent_config.get("params", {})
# Mettre à jour les variables avec les valeurs par défaut
self.temp_var.set(params.get("temperature", 0.7))
self.top_p_var.set(params.get("top_p", 0.9))
self.top_k_var.set(params.get("top_k", 40))
self.repeat_var.set(params.get("repeat_penalty", 1.1))
self.tokens_var.set(params.get("num_predict", 512))
# Mettre à jour les paramètres personnalisés
self.custom_params = params.copy()
except Exception as e:
print(f"Erreur lors du chargement des paramètres de l'agent: {str(e)}")
def _load_model_params(self, model_name):
"""Charge les paramètres par défaut du modèle"""
# Vérifier si un fichier de paramètres personnalisés existe
save_dir = "saved_params"
file_name = f"{save_dir}/{self.current_agent}_{model_name}.json"
if os.path.exists(file_name):
try:
# Charger les paramètres personnalisés
with open(file_name, "r") as f:
params = json.load(f)
# Mettre à jour les variables
self.temp_var.set(params.get("temperature", 0.7))
self.top_p_var.set(params.get("top_p", 0.9))
self.top_k_var.set(params.get("top_k", 40))
self.repeat_var.set(params.get("repeat_penalty", 1.1))
self.tokens_var.set(params.get("num_predict", 512))
# Mettre à jour les paramètres personnalisés
self.custom_params = params.copy()
# Mettre à jour le statut
self.status_bar.config(text=f"Paramètres personnalisés chargés depuis {file_name}")
except Exception as e:
print(f"Erreur lors du chargement des paramètres personnalisés: {str(e)}")
else:
# Si aucun fichier personnalisé, charger les paramètres par défaut
self._load_agent_params(self.current_agent)
def _create_agent_with_custom_params(self):
"""Crée un agent avec les paramètres personnalisés"""
if not self.current_agent:
raise ValueError("Aucun agent sélectionné")
# Créer l'agent
agent = AgentManager.create(self.current_agent)
# Appliquer les paramètres personnalisés
for key, value in self.custom_params.items():
agent.params[key] = value
# Appliquer le modèle si différent
model_name = self.model_combo.get()
if model_name != agent.model:
# Créer un nouveau modèle
from core.factory import LLMFactory
new_model = LLMFactory.create(model_name)
# Copier les paramètres
new_model.params.update(agent.params)
new_model.system_prompt = agent.system_prompt
new_model.agent = agent.agent
agent = new_model
return agent
def _load_conversation_history(self):
"""Charge l'historique des conversations"""
# Créer le dossier d'historique si nécessaire
history_dir = "chat_history"
os.makedirs(history_dir, exist_ok=True)
# Lister les fichiers d'historique
self.history_files = []
for file in os.listdir(history_dir):
if file.endswith(".json"):
self.history_files.append(os.path.join(history_dir, file))
# Trier par date (plus récent en premier)
self.history_files.sort(key=os.path.getmtime, reverse=True)
# Mettre à jour la liste d'historique
self.history_listbox.delete(0, tk.END)
for file in self.history_files:
try:
with open(file, "r", encoding="utf-8") as f:
history = json.load(f)
# Extraire les informations
agent = history.get("agent", "Inconnu")
date = history.get("date", "Inconnue")
messages = history.get("messages", [])
# Ajouter à la liste
if messages:
first_message = messages[0].get("content", "")
preview = first_message[:30] + "..." if len(first_message) > 30 else first_message
self.history_listbox.insert(tk.END, f"{agent} - {date} - {preview}")
except Exception as e:
print(f"Erreur lors du chargement de l'historique {file}: {str(e)}")
def _on_history_select(self, event):
"""Gestionnaire d'événement pour la sélection d'un historique"""
# Récupérer l'index sélectionné
selection = self.history_listbox.curselection()
if not selection:
return
# Activer le bouton de chargement
self.load_history_btn.config(state=tk.NORMAL)
def _load_selected_history(self):
"""Charge l'historique sélectionné"""
# Récupérer l'index sélectionné
selection = self.history_listbox.curselection()
if not selection or selection[0] >= len(self.history_files):
return
# Récupérer le fichier d'historique
file = self.history_files[selection[0]]
try:
# Charger l'historique
with open(file, "r", encoding="utf-8") as f:
history = json.load(f)
# Extraire les informations
agent = history.get("agent", None)
messages = history.get("messages", [])
# Sélectionner l'agent si disponible
if agent:
# Trouver l'index de l'agent dans la listbox
for i in range(self.agent_listbox.size()):
if self.agent_listbox.get(i) == agent:
self.agent_listbox.selection_clear(0, tk.END)
self.agent_listbox.selection_set(i)
self.agent_listbox.see(i)
self._on_agent_select(None)
break
# Effacer le chat actuel
self._clear_chat()
# Ajouter les messages au chat
for message in messages:
sender = message.get("sender", "Inconnu")
content = message.get("content", "")
self._add_message_to_chat(sender, content, add_to_history=False)
# Mettre à jour l'historique
self.conversation_history = messages
# Mettre à jour le statut
self.status_bar.config(text=f"Historique chargé depuis {file}")
except Exception as e:
messagebox.showerror("Erreur", f"Erreur lors du chargement de l'historique: {str(e)}")
def _delete_history(self):
"""Supprime l'historique sélectionné"""
# Récupérer l'index sélectionné
selection = self.history_listbox.curselection()
if not selection or selection[0] >= len(self.history_files):
return
# Récupérer le fichier d'historique
file = self.history_files[selection[0]]
# Demander confirmation
if messagebox.askyesno("Confirmation", f"Voulez-vous vraiment supprimer cet historique ?"):
try:
# Supprimer le fichier
os.remove(file)
# Mettre à jour la liste
self.history_files.pop(selection[0])
self.history_listbox.delete(selection[0])
# Mettre à jour le statut
self.status_bar.config(text=f"Historique supprimé")
except Exception as e:
messagebox.showerror("Erreur", f"Erreur lors de la suppression: {str(e)}")
def _export_history(self):
"""Exporte l'historique sélectionné"""
# Récupérer l'index sélectionné
selection = self.history_listbox.curselection()
if not selection or selection[0] >= len(self.history_files):
return
# Récupérer le fichier d'historique
file = self.history_files[selection[0]]
try:
# Charger l'historique
with open(file, "r", encoding="utf-8") as f:
history = json.load(f)
# Ouvrir une boîte de dialogue pour choisir l'emplacement d'exportation
export_file = filedialog.asksaveasfilename(
defaultextension=".md",
filetypes=[("Markdown", "*.md"), ("Texte", "*.txt"), ("JSON", "*.json")],
title="Exporter l'historique"
)
if not export_file:
return
# Format d'exportation selon l'extension
ext = os.path.splitext(export_file)[1].lower()
if ext == ".json":
# Exporter au format JSON
with open(export_file, "w", encoding="utf-8") as f:
json.dump(history, f, indent=2, ensure_ascii=False)
elif ext == ".md" or ext == ".txt":
# Exporter au format Markdown/Texte
with open(export_file, "w", encoding="utf-8") as f:
# En-tête
agent = history.get("agent", "Inconnu")
date = history.get("date", "Inconnue")
f.write(f"# Conversation avec {agent} - {date}\n\n")
# Messages
for message in history.get("messages", []):
sender = message.get("sender", "Inconnu")
content = message.get("content", "")
if ext == ".md":
f.write(f"## {sender}\n\n{content}\n\n")
else:
f.write(f"{sender}:\n{content}\n\n")
# Mettre à jour le statut
self.status_bar.config(text=f"Historique exporté vers {export_file}")
except Exception as e:
messagebox.showerror("Erreur", f"Erreur lors de l'exportation: {str(e)}")
def _on_shift_enter(self, event):
"""Gestionnaire d'événement pour Shift+Enter"""
# Ajouter un saut de ligne
return
def _send_message(self):
"""Envoie le message saisi par l'utilisateur"""
if not self.current_agent:
messagebox.showwarning("Aucun agent sélectionné",
"Veuillez d'abord sélectionner un agent.")
return
# Récupérer le message
message = self.user_input.get(1.0, tk.END).strip()
if not message:
return
# Effacer le champ de saisie
self.user_input.delete(1.0, tk.END)
# Ajouter le message au chat
self._add_message_to_chat("Utilisateur", message)
# Générer une réponse
try:
# Mettre à jour le statut
self.status_bar.config(text=f"Génération en cours...")
self.root.update()
# Créer l'agent avec les paramètres personnalisés
agent = self._create_agent_with_custom_params()
# Générer la réponse
response = agent.generate(message)
# Ajouter la réponse au chat
self._add_message_to_chat(self.current_agent, response)
# Mettre à jour le statut
self.status_bar.config(text=f"Réponse générée")
except Exception as e:
messagebox.showerror("Erreur", f"Erreur lors de la génération: {str(e)}")
self.status_bar.config(text=f"Erreur lors de la génération")
def _add_message_to_chat(self, sender, content, add_to_history=True):
"""Ajoute un message au chat"""
# Activer la zone de chat pour modification
self.chat_display.config(state=tk.NORMAL)
# Ajouter le message
self.chat_display.insert(tk.END, f"{sender}:\n", "sender")
self.chat_display.insert(tk.END, f"{content}\n\n", "message")
# Configurer les tags
self.chat_display.tag_configure("sender", font=("Arial", 10, "bold"))
self.chat_display.tag_configure("message", font=("Arial", 10))
# Défiler vers le bas
self.chat_display.see(tk.END)
# Désactiver la zone de chat
self.chat_display.config(state=tk.DISABLED)
# Ajouter à l'historique
if add_to_history:
self.conversation_history.append({
"sender": sender,
"content": content,
"timestamp": datetime.now().isoformat()
})
# Sauvegarder l'historique
self._save_conversation_history()
def _clear_chat(self):
"""Efface le contenu du chat"""
# Effacer la zone de chat
self.chat_display.config(state=tk.NORMAL)
self.chat_display.delete(1.0, tk.END)
self.chat_display.config(state=tk.DISABLED)
# Ne pas effacer l'historique
def _new_chat(self):
"""Démarre une nouvelle conversation"""
# Demander confirmation si la conversation actuelle n'est pas vide
if self.conversation_history and messagebox.askyesno(
"Nouvelle conversation",
"Voulez-vous démarrer une nouvelle conversation ?"
):
# Effacer la zone de chat
self._clear_chat()
# Réinitialiser l'historique
self.conversation_history = []
# Mettre à jour le statut
self.status_bar.config(text=f"Nouvelle conversation démarrée")
def _save_conversation_history(self):
"""Sauvegarde l'historique de la conversation actuelle"""
if not self.conversation_history or not self.current_agent:
return
# Créer le dossier d'historique si nécessaire
history_dir = "chat_history"
os.makedirs(history_dir, exist_ok=True)
# Créer un ID unique pour la conversation
now = datetime.now()
date_str = now.strftime("%Y-%m-%d_%H-%M-%S")
file_name = f"{history_dir}/{self.current_agent}_{date_str}.json"
# Préparer les données
history_data = {
"agent": self.current_agent,
"model": self.model_combo.get(),
"date": now.strftime("%Y-%m-%d %H:%M:%S"),
"params": self.custom_params,
"messages": self.conversation_history
}
# Sauvegarder l'historique
try:
with open(file_name, "w", encoding="utf-8") as f:
json.dump(history_data, f, indent=2, ensure_ascii=False)
# Mettre à jour la liste des fichiers d'historique
if file_name not in self.history_files:
self.history_files.insert(0, file_name)
self.history_listbox.insert(0, f"{self.current_agent} - {now.strftime('%Y-%m-%d %H:%M:%S')}")
except Exception as e:
print(f"Erreur lors de la sauvegarde de l'historique: {str(e)}")
def main():
"""Point d'entrée principal"""
root = tk.Tk()
app = ChatUI(root)
root.mainloop()
if __name__ == "__main__":
main()