#!/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, QApplication) 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) # Case à cocher pour activer/désactiver l'agent de résumé self.summary_enabled_checkbox = QCheckBox("Activer l'agent de résumé") self.summary_enabled_checkbox.setChecked(False) # Désactivé par défaut agent_type_layout.addRow("", self.summary_enabled_checkbox) # 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) # Option pour enregistrer les images capturées self.save_captured_images_check = QCheckBox("Enregistrer les images capturées") self.save_captured_images_check.setChecked(True) export_group_layout.addWidget(self.save_captured_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): """Exécute l'agent LLM sur la sélection actuelle""" if not self.current_selection: self.result_text.setText("Veuillez sélectionner une région avant d'exécuter l'agent.") return try: # Récupération des paramètres selection_type = self.current_selection.get("type", "autre") context = self.current_selection.get("context", "") # Paramètres de génération gen_params = { "temperature": self.temp_spin.value(), "top_p": self.top_p_spin.value(), "max_tokens": self.token_spin.value(), "save_images": self.save_captured_images_check.isChecked() # Ajout du paramètre d'enregistrement } self.result_text.setText("Analyse en cours...\nCette opération peut prendre plusieurs minutes.") self.parent.status_bar.showMessage("Traitement de l'image en cours...") # Forcer la mise à jour de l'interface QApplication.processEvents() # Afficher les informations sur la sélection actuelle (débogue) rect = self.current_selection.get("rect") if rect: rect_info = f"Sélection: x={rect.x()}, y={rect.y()}, w={rect.width()}, h={rect.height()}, page={self.current_selection.get('page', 0)+1}" print(f"INFO SÉLECTION: {rect_info}") # Récupérer l'image de la sélection selection_image = self.parent.get_selection_image(self.current_selection) if selection_image: # Vérifier la taille des données d'image pour le débogue print(f"Taille des données image: {len(selection_image)} octets") # Récupération des paramètres depuis l'interface vision_model = self.vision_model_combo.currentText() vision_lang = self.vision_lang_combo.currentText() summary_model = self.summary_model_combo.currentText() translation_model = self.translation_model_combo.currentText() # Configuration du pipeline de traitement from utils.workflow_manager import WorkflowManager from config.agent_config import ACTIVE_AGENTS # Mise à jour dynamique de l'activation des agents active_agents = ACTIVE_AGENTS.copy() active_agents["summary"] = self.summary_enabled_checkbox.isChecked() # Afficher les agents actifs pour le débogage print(f"Agents actifs: {active_agents}") print(f"Modèles utilisés: vision={vision_model}, translation={translation_model}, summary={summary_model}") # Créer le gestionnaire de workflow workflow = WorkflowManager( vision_model=vision_model, summary_model=summary_model, translation_model=translation_model, active_agents=active_agents, config=gen_params ) # Exécution du workflow results = workflow.process_image( image_data=selection_image, content_type=selection_type, context=context, target_lang=vision_lang ) # Affichage des résultats if results: result_text = "" # Vision (original) if "vision" in results and results["vision"]: result_text += "🔍 ANALYSE VISUELLE (en):\n" result_text += results["vision"] result_text += "\n\n" # Traduction if "translation" in results and results["translation"]: result_text += "🌐 TRADUCTION (fr):\n" result_text += results["translation"] result_text += "\n\n" # Résumé (si activé) if "summary" in results and results["summary"]: result_text += "📝 RÉSUMÉ (fr):\n" result_text += results["summary"] # Message d'erreur if "error" in results: result_text += "\n\n❌ ERREUR:\n" result_text += results["error"] # Enregistrer l'analyse dans l'historique self.analysis_results[self.selection_list.currentRow()] = results self.result_text.setText(result_text) self.parent.status_bar.showMessage("Analyse terminée.") else: self.result_text.setText("Erreur: Impossible d'obtenir une analyse de l'image.") self.parent.status_bar.showMessage("Analyse échouée.") else: self.result_text.setText("Erreur: Impossible d'extraire l'image de la sélection.") self.parent.status_bar.showMessage("Erreur: Extraction d'image impossible.") except Exception as e: import traceback error_details = traceback.format_exc() print(f"Erreur lors de l'analyse: {error_details}") self.result_text.setText(f"Erreur lors de l'analyse: {str(e)}\n\nDétails de l'erreur:\n{error_details}") self.parent.status_bar.showMessage("Erreur: Analyse échoué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)}")