ragflow_preprocess/ui/llm_config_panel.py
2025-03-27 17:59:10 +01:00

513 lines
22 KiB
Python

#!/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("<b>Agent Vision</b>"))
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("<b>Agent Résumé</b>"))
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("<b>Agent Traduction</b>"))
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("<b>Paramètres de génération</b>"))
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)}")