#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Panneau de configuration des agents LLM """ import os import json from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QComboBox, QTextEdit, QGroupBox, QListWidget, QSplitter, QTabWidget, QSpinBox, QDoubleSpinBox, QFormLayout, QMessageBox, QCheckBox) from PyQt6.QtCore import Qt, QSize class LLMConfigPanel(QWidget): """Panneau de configuration des agents LLM et de gestion des sélections""" def __init__(self, parent): super().__init__(parent) self.parent = parent self.current_selection = None self.analysis_results = {} self.init_ui() self.load_llm_profiles() def init_ui(self): """Initialise l'interface utilisateur du panneau""" main_layout = QVBoxLayout(self) # Tabs pour organiser les fonctionnalités tabs = QTabWidget() main_layout.addWidget(tabs) # 1. Onglet Sélections selections_tab = QWidget() tab_layout = QVBoxLayout(selections_tab) # Liste des sélections selection_group = QGroupBox("Régions sélectionnées") selection_layout = QVBoxLayout(selection_group) self.selection_list = QListWidget() self.selection_list.setMinimumHeight(100) self.selection_list.currentRowChanged.connect(self.selection_changed) selection_layout.addWidget(self.selection_list) # Boutons pour la gestion des sélections selection_btns = QHBoxLayout() self.remove_btn = QPushButton("Supprimer") self.remove_btn.clicked.connect(self.remove_selection) selection_btns.addWidget(self.remove_btn) selection_layout.addLayout(selection_btns) tab_layout.addWidget(selection_group) # Type de la sélection type_group = QGroupBox("Type de contenu") type_layout = QVBoxLayout(type_group) self.type_combo = QComboBox() self.type_combo.addItems(["schéma", "tableau", "formule", "texte", "autre"]) self.type_combo.currentTextChanged.connect(self.update_selection_type) type_layout.addWidget(self.type_combo) tab_layout.addWidget(type_group) # Contexte textuel context_group = QGroupBox("Contexte textuel") context_layout = QVBoxLayout(context_group) self.context_edit = QTextEdit() self.context_edit.setPlaceholderText("Ajoutez ici le contexte pour cette sélection...") self.context_edit.textChanged.connect(self.update_selection_context) context_layout.addWidget(self.context_edit) tab_layout.addWidget(context_group) # 2. Onglet Agents LLM agent_tab = QWidget() agent_layout = QVBoxLayout(agent_tab) # Mode d'analyse mode_group = QGroupBox("Mode d'analyse") mode_layout = QHBoxLayout(mode_group) mode_label = QLabel("Niveau :") mode_layout.addWidget(mode_label) self.mode_combo = QComboBox() self.mode_combo.addItems(["🔹 Léger", "⚪ Moyen", "🔸 Avancé"]) self.mode_combo.currentTextChanged.connect(self.update_mode) mode_layout.addWidget(self.mode_combo) agent_layout.addWidget(mode_group) # Sélection des agents agent_config_group = QGroupBox("Configuration des agents") agent_config_layout = QVBoxLayout(agent_config_group) # Sélection du type d'agent agent_type_layout = QFormLayout() # Agent Vision agent_type_layout.addRow(QLabel("Agent Vision")) self.vision_model_combo = QComboBox() self.vision_model_combo.addItems(["llava:34b", "llava", "llama3.2-vision"]) agent_type_layout.addRow("Modèle:", self.vision_model_combo) self.vision_lang_combo = QComboBox() self.vision_lang_combo.addItems(["fr", "en"]) agent_type_layout.addRow("Langue:", self.vision_lang_combo) # Agent de résumé/reformulation agent_type_layout.addRow(QLabel("Agent Résumé")) self.summary_model_combo = QComboBox() self.summary_model_combo.addItems(["mistral", "deepseek-r1"]) agent_type_layout.addRow("Modèle:", self.summary_model_combo) # Agent de traduction agent_type_layout.addRow(QLabel("Agent Traduction")) self.translation_model_combo = QComboBox() self.translation_model_combo.addItems(["mistral", "qwen2.5", "deepseek"]) agent_type_layout.addRow("Modèle:", self.translation_model_combo) agent_config_layout.addLayout(agent_type_layout) # Paramètres communs params_layout = QFormLayout() params_layout.addRow(QLabel("Paramètres de génération")) self.temp_spin = QDoubleSpinBox() self.temp_spin.setRange(0.1, 1.0) self.temp_spin.setSingleStep(0.1) self.temp_spin.setValue(0.2) params_layout.addRow("Température:", self.temp_spin) self.top_p_spin = QDoubleSpinBox() self.top_p_spin.setRange(0.1, 1.0) self.top_p_spin.setSingleStep(0.05) self.top_p_spin.setValue(0.95) params_layout.addRow("Top-p:", self.top_p_spin) self.token_spin = QSpinBox() self.token_spin.setRange(100, 4000) self.token_spin.setSingleStep(100) self.token_spin.setValue(1024) params_layout.addRow("Max tokens:", self.token_spin) agent_config_layout.addLayout(params_layout) agent_layout.addWidget(agent_config_group) # Boutons d'action action_layout = QHBoxLayout() self.run_btn = QPushButton("▶ Appliquer l'agent") self.run_btn.setMinimumHeight(40) self.run_btn.clicked.connect(self.run_agent) action_layout.addWidget(self.run_btn) agent_layout.addLayout(action_layout) # Aperçu des résultats result_group = QGroupBox("Résultat") result_layout = QVBoxLayout(result_group) self.result_text = QTextEdit() self.result_text.setReadOnly(True) self.result_text.setMinimumHeight(100) result_layout.addWidget(self.result_text) agent_layout.addWidget(result_group) # 3. Onglet Export export_tab = QWidget() export_layout = QVBoxLayout(export_tab) export_group = QGroupBox("Options d'export") export_group_layout = QVBoxLayout(export_group) export_format_layout = QHBoxLayout() export_format_layout.addWidget(QLabel("Format:")) self.export_format_combo = QComboBox() self.export_format_combo.addItems(["Markdown (.md)"]) export_format_layout.addWidget(self.export_format_combo) export_group_layout.addLayout(export_format_layout) self.include_images_check = QCheckBox("Inclure les images") self.include_images_check.setChecked(True) export_group_layout.addWidget(self.include_images_check) export_layout.addWidget(export_group) export_btn = QPushButton("Exporter") export_btn.setMinimumHeight(40) export_btn.clicked.connect(self.export_results) export_layout.addWidget(export_btn) export_layout.addStretch() # Ajout des onglets tabs.addTab(selections_tab, "Sélections") tabs.addTab(agent_tab, "Agents LLM") tabs.addTab(export_tab, "Export") def load_llm_profiles(self): """Charge les profils LLM depuis le fichier de configuration""" try: config_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config") profile_path = os.path.join(config_dir, "llm_profiles.json") if os.path.exists(profile_path): with open(profile_path, "r", encoding="utf-8") as f: self.profiles = json.load(f) else: # Profil par défaut si le fichier n'existe pas self.profiles = { "léger": { "vision": {"model": "llava:34b", "language": "en", "temperature": 0.2}, "translation": {"model": "mistral", "language": "fr", "temperature": 0.1}, "summary": {"model": "mistral", "language": "fr", "temperature": 0.2} }, "moyen": { "vision": {"model": "llava", "language": "en", "temperature": 0.2}, "translation": {"model": "qwen2.5", "language": "fr", "temperature": 0.1}, "summary": {"model": "deepseek-r1", "language": "fr", "temperature": 0.2} }, "avancé": { "vision": {"model": "llama3.2-vision", "language": "en", "temperature": 0.2}, "translation": {"model": "deepseek", "language": "fr", "temperature": 0.1}, "summary": {"model": "deepseek-r1", "language": "fr", "temperature": 0.2} } } # Créer le répertoire de configuration s'il n'existe pas os.makedirs(config_dir, exist_ok=True) # Sauvegarder le profil par défaut with open(profile_path, "w", encoding="utf-8") as f: json.dump(self.profiles, f, indent=2, ensure_ascii=False) except Exception as e: print(f"Erreur lors du chargement des profils: {str(e)}") def update_selection_list(self): """Met à jour la liste des sélections""" self.selection_list.clear() for i, selection in enumerate(self.parent.selected_regions): page = selection["page"] + 1 typ = selection["type"] self.selection_list.addItem(f"Page {page} - {typ}") def selection_changed(self, index): """Appelé lorsque la sélection change dans la liste""" if index >= 0 and index < len(self.parent.selected_regions): selection = self.parent.selected_regions[index] self.current_selection = selection # Mettre à jour l'interface self.type_combo.setCurrentText(selection["type"]) self.context_edit.setText(selection["context"]) # Afficher les résultats si disponibles if id(selection) in self.analysis_results: self.result_text.setText(self.analysis_results[id(selection)]) else: self.result_text.clear() def update_selection_type(self, new_type): """Met à jour le type de la sélection actuelle""" if self.current_selection: self.current_selection["type"] = new_type self.update_selection_list() def update_selection_context(self): """Met à jour le contexte de la sélection actuelle""" if self.current_selection: self.current_selection["context"] = self.context_edit.toPlainText() def remove_selection(self): """Supprime la sélection actuelle""" if self.current_selection: idx = self.parent.selected_regions.index(self.current_selection) self.parent.selected_regions.remove(self.current_selection) # Supprimer les résultats associés if id(self.current_selection) in self.analysis_results: del self.analysis_results[id(self.current_selection)] self.current_selection = None self.update_selection_list() # Sélectionner le prochain élément si disponible if self.selection_list.count() > 0: new_idx = min(idx, self.selection_list.count() - 1) self.selection_list.setCurrentRow(new_idx) else: self.context_edit.clear() self.result_text.clear() def update_mode(self, mode_text): """Met à jour le mode d'analyse en fonction du niveau sélectionné""" if "léger" in mode_text.lower(): profile = self.profiles.get("léger", {}) elif "moyen" in mode_text.lower(): profile = self.profiles.get("moyen", {}) elif "avancé" in mode_text.lower(): profile = self.profiles.get("avancé", {}) else: return # Mise à jour des combobox avec les paramètres du profil if "vision" in profile: vision = profile["vision"] if "model" in vision and vision["model"] in [self.vision_model_combo.itemText(i) for i in range(self.vision_model_combo.count())]: self.vision_model_combo.setCurrentText(vision["model"]) if "language" in vision: self.vision_lang_combo.setCurrentText(vision["language"]) if "temperature" in vision: self.temp_spin.setValue(vision["temperature"]) if "summary" in profile: summary = profile["summary"] if "model" in summary and summary["model"] in [self.summary_model_combo.itemText(i) for i in range(self.summary_model_combo.count())]: self.summary_model_combo.setCurrentText(summary["model"]) if "translation" in profile: translation = profile["translation"] if "model" in translation and translation["model"] in [self.translation_model_combo.itemText(i) for i in range(self.translation_model_combo.count())]: self.translation_model_combo.setCurrentText(translation["model"]) def run_agent(self): """Execute l'agent LLM sur la sélection actuelle""" if not self.current_selection: QMessageBox.warning(self, "Aucune sélection", "Veuillez sélectionner une région à analyser.") return # Pour cette démonstration, nous simulons le résultat # Dans une implémentation réelle, nous utiliserions les agents LLM # Récupérer les paramètres vision_model = self.vision_model_combo.currentText() summary_model = self.summary_model_combo.currentText() translation_model = self.translation_model_combo.currentText() lang = self.vision_lang_combo.currentText() temp = self.temp_spin.value() # Exemple de résultat simulé result = f"**Analyse de l'agent Vision ({vision_model})**\n\n" if self.current_selection["type"] == "schéma": result += "Le schéma illustre un processus en plusieurs étapes avec des connections entre les différents éléments.\n\n" elif self.current_selection["type"] == "tableau": result += "Le tableau contient des données structurées avec plusieurs colonnes et rangées.\n\n" elif self.current_selection["type"] == "formule": result += "La formule mathématique représente une équation complexe.\n\n" else: result += "Le contenu sélectionné a été analysé.\n\n" result += f"**Résumé ({summary_model})**\n\n" result += "Ce contenu montre l'importance des éléments sélectionnés dans le contexte du document.\n\n" if lang == "en": result += f"**Traduction ({translation_model})**\n\n" result += "The selected content has been analyzed and shows the importance of the selected elements in the context of the document.\n\n" result += "**Paramètres utilisés**\n" result += f"modèle vision={vision_model}, modèle résumé={summary_model}, " result += f"temperature={temp}, langue={lang}" # Stocker et afficher le résultat self.analysis_results[id(self.current_selection)] = result self.result_text.setText(result) # Mise à jour du statut self.parent.status_bar.showMessage("Analyse terminée.") def export_results(self): """Exporte les résultats au format Markdown""" if not self.parent.selected_regions: QMessageBox.warning(self, "Aucune sélection", "Il n'y a aucune sélection à exporter.") return try: # Créer le répertoire de sortie s'il n'existe pas output_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "outputs") os.makedirs(output_dir, exist_ok=True) # Générer le contenu Markdown md_content = "# Analyse de document\n\n" # Trier les sélections par numéro de page sorted_selections = sorted(self.parent.selected_regions, key=lambda x: x["page"]) for selection in sorted_selections: page = selection["page"] + 1 typ = selection["type"] context = selection["context"] md_content += f"## {typ.capitalize()} - Page {page}\n\n" if context: md_content += f"**Contexte** :\n{context}\n\n" # Ajouter les résultats d'analyse si disponibles if id(selection) in self.analysis_results: md_content += f"**Analyse IA** :\n{self.analysis_results[id(selection)]}\n\n" md_content += "---\n\n" # Écrire le fichier Markdown file_path = os.path.join(output_dir, "analyse_document.md") with open(file_path, "w", encoding="utf-8") as f: f.write(md_content) QMessageBox.information(self, "Export réussi", f"Le fichier a été exporté avec succès :\n{file_path}") except Exception as e: QMessageBox.critical(self, "Erreur d'export", f"Une erreur est survenue lors de l'export :\n{str(e)}")