diff --git a/main.py b/main.py index a956f46..351f400 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,7 @@ from dotenv import load_dotenv load_dotenv() # Force la lecture du fichier .env en local import uuid import json +import threading from fasthtml.common import * from starlette.staticfiles import StaticFiles from starlette.responses import FileResponse, Response, HTMLResponse, RedirectResponse @@ -10,13 +11,14 @@ from starlette.middleware.base import BaseHTTPMiddleware from modules.theme import get_theme_hdrs from modules.database import nettoyer_vieilles_discussions, HISTORY_FILE, load_history, save_history -from modules.ui_components import render_main_page, render_sidebar, render_login_page +from modules.ui_components import render_main_page, render_sidebar, render_login_page, render_rag_upload_modal from modules.chat_handler import handle_chat_request from modules.security import verifier_et_bloquer_vpn, journaliser_ip from modules.auth_manager import IDENTIFIANTS_ADMIN, verifier_mot_de_passe from modules.logger import log_event from modules.billing_manager import charger_soldes, sauvegarder_soldes from modules.rag_manager import auto_apprendre_rag +from modules.vector_rag import indexer_dossier_thematique # Lancement du module d'auto-mise à jour en arrière-plan import modules.model_updater @@ -152,5 +154,42 @@ def post(prompt: str, reponse: str, theme: str): auto_apprendre_rag(prompt, reponse, theme) return Span("✅ Indexé", cls="msg-action-btn", style="background-color: #4caf50; color: white; border: none; cursor: default;") +# --- NOUVELLES ROUTES POUR L'UPLOAD MODAL DU RAG --- +@rt('/admin/rag_modal') +def get(session): + if session.get('user', '') != 'xavier': + return HTMLResponse("Accès refusé.") + return render_rag_upload_modal() + +@rt('/api/upload_rag') +async def post(request, session): + if session.get('user', '') != 'xavier': + return Span("❌ Accès refusé", style="color: red;") + + form = await request.form() + theme = form.get("theme") + fichiers = form.getlist("fichiers_rag") + + if not theme or not fichiers: + return Span("❌ Thème ou fichiers manquants", style="color: red;") + + dossier_cible = f"/DATA/AppData/MULTI-IA-AETHAS38/RAG/{theme}" + os.makedirs(dossier_cible, exist_ok=True) + + fichiers_sauves = 0 + for f in fichiers: + if f.filename: + chemin_fichier = os.path.join(dossier_cible, f.filename) + contenu = await f.read() + with open(chemin_fichier, "wb") as out: + out.write(contenu) + fichiers_sauves += 1 + + if fichiers_sauves > 0: + threading.Thread(target=indexer_dossier_thematique, args=(theme,)).start() + return Span(f"✅ {fichiers_sauves} fichier(s) sauvé(s). Indexation FAISS démarrée en arrière-plan !", style="color: #4caf50;") + else: + return Span("❌ Aucun fichier valide détecté.", style="color: red;") + if __name__ == '__main__': serve(port=5001) \ No newline at end of file diff --git a/modules/ui_components.py b/modules/ui_components.py index aa9776a..b9c95d2 100644 --- a/modules/ui_components.py +++ b/modules/ui_components.py @@ -17,7 +17,6 @@ def get_openrouter_stats(): def render_user_msg(text): return Div(P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"), cls="user-bubble") -# --- LA FONCTION CORRIGÉE AVEC SES 10 ARGUMENTS --- def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, execution_time=None, trace=None, prompt_associe="", thematique="general", msg_id=""): time_badge = Span(f"⏱️ {execution_time}s", style="margin-right: 10px; color:#aaa; font-size:0.8em;") if execution_time else "" @@ -33,7 +32,6 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, executi time_badge, style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 10px; display: flex; flex-wrap: wrap; gap: 5px;" ) - # --- BOUTON FORCER RAG (HTMX) --- bouton_forcer = "" if msg_id and prompt_associe and thematique != "aucun": safe_prompt = prompt_associe.replace('"', '\\"').replace('\n', ' ') @@ -110,7 +108,6 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"): .catch(() => { serverDown = true; console.log("Serveur hors ligne..."); }); }, 5000); - // Script pour gérer l'avertissement de gratuité sur les médias function checkMediaPaymentWarning() { const mediaMode = document.querySelector('input[name="media_mode"]:checked').value; const isPayant = document.getElementById('mode-payant-toggle').checked; @@ -155,6 +152,7 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"): Div( A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"), A("💾 Exporter JSON", href="/export_json", target="_blank", cls="export-btn"), + Button("📚 Gestion RAG", hx_get="/admin/rag_modal", hx_target="body", hx_swap="beforeend", cls="export-btn", style="background-color: #2196f3; color: white; border-color: #2196f3; margin-top: 5px; cursor: pointer;") if role.lower() in ["super admin", "admin"] else "", A("🚪 Déconnexion", href="/logout", cls="export-btn", style="background-color: #8b0000; color: white; border-color: #8b0000; margin-top: 5px;"), cls="nav-buttons", style="display: flex; flex-direction: column; gap: 10px;" ), cls="sidebar-top" @@ -252,4 +250,41 @@ def render_popup_paiement(cout_estime, prompt_sauvegarde, chat_id, multi_ia_stat ), id="popup-paiement", style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 9999;" + ) + +def render_rag_upload_modal(): + """Génère la fenêtre pop-up pour téléverser des documents dans le RAG.""" + themes = [ + "Minecraft_KubeJS", "Juridique_Loi_Francaise", "Serveur_Debian", + "Bureautique_Excel", "Regles_Aethas38", "Education_Nationale", "Connaissances_Generales" + ] + options = [Option(t.replace("_", " "), value=t) for t in themes] + + return Div( + Div( + H3("📚 Administration RAG (Base Vectorielle)", style="color: #2196f3; margin-top: 0;"), + P("Formats supportés : PDF, MD, TXT, JS, JSON, YML, CSV", style="color: #ccc; font-size: 0.9em;"), + Form( + Div( + Label("1. Choisir la thématique :", style="font-weight: bold; color: white;"), + Select(*options, name="theme", cls="input-field", style="margin-bottom: 15px;") + ), + Div( + Label("2. Sélectionner les fichiers :", style="font-weight: bold; color: white;"), + Input(type="file", name="fichiers_rag", multiple=True, required=True, cls="input-field", style="margin-bottom: 15px; background: #222; padding: 10px; border: 1px dashed #2196f3; cursor: pointer;") + ), + Div( + Button("Annuler", type="button", onclick="document.getElementById('rag-modal').remove();", cls="btn-submit", style="background-color: #555; width: 48%;"), + Button("Téléverser & Indexer", type="submit", cls="btn-submit", style="background-color: #2196f3; color: white; width: 48%;"), + style="display: flex; justify-content: space-between;" + ), + hx_post="/api/upload_rag", + hx_target="#rag-upload-result", + enctype="multipart/form-data" + ), + Div(id="rag-upload-result", style="margin-top: 15px; text-align: center; font-weight: bold;"), + style="background: #1a1a1a; padding: 25px; border-radius: 12px; border: 2px solid #2196f3; width: 500px; max-width: 90vw; box-shadow: 0 10px 30px rgba(0,0,0,0.8);" + ), + id="rag-modal", + style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 9999;" ) \ No newline at end of file diff --git a/modules/vector_rag.py b/modules/vector_rag.py index 9d0b2aa..a5814c2 100644 --- a/modules/vector_rag.py +++ b/modules/vector_rag.py @@ -46,7 +46,7 @@ def indexer_dossier_thematique(thematique: str): reader = pypdf.PdfReader(chemin_complet) for page in reader.pages: texte += page.extract_text() or "" - elif fichier.lower().endswith(('.md', '.txt')): + elif fichier.lower().endswith(('.md', '.txt', '.yml', '.yaml', '.js', '.json', '.csv', '.toml', '.properties')): with open(chemin_complet, 'r', encoding='utf-8') as f: texte = f.read()