From 44bf0b868e4826c31e9717d0a6c2cf374ffd9941 Mon Sep 17 00:00:00 2001 From: Xavier Date: Thu, 8 Oct 2026 10:25:02 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20int=C3=A9gration=20du=20parsing=20backe?= =?UTF-8?q?nd=20(PDF,=20Excel,=20VBA)=20et=20gestion=20s=C3=A9curis=C3=A9e?= =?UTF-8?q?=20des=20champs=20prompteur/concat=C3=A9neur=20vides=20dans=20l?= =?UTF-8?q?es=20templates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/main.py | 56 +++++++++++++++++++++++++++++++++++++++-- backend/orchestrator.py | 14 +++++++---- frontend/dashboard.html | 44 +++++++++++++++++++------------- requirements.txt | 5 +++- 4 files changed, 93 insertions(+), 26 deletions(-) diff --git a/backend/main.py b/backend/main.py index e68557e..25c4a2c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,7 +3,7 @@ from fastapi.responses import RedirectResponse, FileResponse from fastapi.staticfiles import StaticFiles from sqlalchemy.orm import Session from typing import List -import os, json, asyncio, shutil +import os, json, asyncio, shutil, base64, io from datetime import datetime import pytz @@ -147,7 +147,59 @@ async def create_message(project_id: int, message: MessageCreate, db: Session = if message.files: files_text = "" for f in message.files: - files_text += f"\n\n[Fichier attaché : {f.name}]\n```\n{f.content}\n```" + content = f.content + if content.startswith("data:"): + try: + header, b64data = content.split(",", 1) + file_bytes = base64.b64decode(b64data) + ext = f.name.split('.')[-1].lower() + extracted_text = "" + + if ext == 'pdf': + try: + import PyPDF2 + reader = PyPDF2.PdfReader(io.BytesIO(file_bytes)) + extracted_text = "\n".join([page.extract_text() for page in reader.pages if page.extract_text()]) + except ImportError: + extracted_text = "[Erreur: L'administrateur doit exécuter 'pip install PyPDF2' sur le serveur pour lire les PDF.]" + + elif ext in ['xls', 'xlsx', 'xlsm', 'xlsb']: + try: + import openpyxl + wb = openpyxl.load_workbook(io.BytesIO(file_bytes), data_only=False) + for sheet_name in wb.sheetnames: + sheet = wb[sheet_name] + extracted_text += f"\n--- Feuille : {sheet_name} ---\n" + for row in sheet.iter_rows(values_only=True): + row_vals = [str(cell) if cell is not None else "" for cell in row] + if any(row_vals): + extracted_text += "\t".join(row_vals) + "\n" + + # Extraction des Macros VBA + if ext in ['xlsm', 'xlsb', 'xls']: + try: + from oletools.olevba import VBA_Parser + vbaparser = VBA_Parser("filename", data=file_bytes) + if vbaparser.detect_vba_macros(): + extracted_text += "\n\n--- MACROS VBA DETECTEES ---\n" + for (filename, stream_path, vba_filename, vba_code) in vbaparser.extract_macros(): + extracted_text += f"\n// Module: {vba_filename}\n{vba_code}\n" + except ImportError: + extracted_text += "\n[Extraction VBA impossible: L'administrateur doit exécuter 'pip install oletools' sur le serveur.]\n" + except Exception as e: + extracted_text += f"\n[Erreur de lecture VBA interne: {str(e)}]\n" + + except ImportError: + extracted_text = "[Erreur: L'administrateur doit exécuter 'pip install openpyxl' sur le serveur pour lire Excel.]" + else: + extracted_text = f"[Fichier binaire non supporté textuellement : {f.name}]" + + files_text += f"\n\n[Fichier attaché : {f.name}]\n```text\n{extracted_text}\n```" + except Exception as e: + files_text += f"\n\n[Fichier attaché : {f.name} - ERREUR DE DECODAGE: {str(e)}]" + else: + files_text += f"\n\n[Fichier attaché : {f.name}]\n```\n{content}\n```" + final_content = message.content + files_text db.add(Message(role=message.role, content=final_content, project_id=project_id)) diff --git a/backend/orchestrator.py b/backend/orchestrator.py index 85cd573..14bb251 100644 --- a/backend/orchestrator.py +++ b/backend/orchestrator.py @@ -16,10 +16,9 @@ def determine_domain(model_id: str) -> str: return "Texte Polyvalent" async def translate_en_to_fr(client: httpx.AsyncClient, text: str) -> str: - """Traduit automatiquement l'anglais vers le français via l'API publique Google Translate.""" if not text: return "Aucune description fournie." try: - short_text = text[:300].strip() # Limite stricte pour l'API gratuite + short_text = text[:300].strip() url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=fr&dt=t&q={urllib.parse.quote(short_text)}" resp = await client.get(url, timeout=4.0) if resp.status_code == 200: @@ -205,7 +204,10 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings, final_response = "" try: - p_mod = config.get("prompter", "gemini-3.5-flash-lite") + # Fallback sécurisé en cas de champ vide + p_mod = config.get("prompter") + if not p_mod: p_mod = "gemini-3.5-flash-lite" + p_client, p_prov = get_client_for_model(db, p_mod, settings) prompt_system = "You are an expert prompt engineer. Translate and optimize the user request into clear, precise English tailored for AI execution." optimized = await ask_agent(p_client, p_mod, [{"role": "system", "content": prompt_system}, {"role": "user", "content": user_prompt}], p_prov) @@ -222,7 +224,9 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings, w_tasks.append(ask_agent(w_client, w, formatted_history + [{"role": "user", "content": optimized}], w_prov)) responses = await asyncio.gather(*w_tasks, return_exceptions=True) - c_mod = config.get("concatenator", "gemini-3.5-flash-lite") + c_mod = config.get("concatenator") + if not c_mod: c_mod = "gemini-3.5-flash-lite" + c_client, c_prov = get_client_for_model(db, c_mod, settings) concat_system = ( @@ -233,7 +237,7 @@ async def run_orchestrator(db: Session, history: list, settings: SystemSettings, "You may translate code comments into French if appropriate, but leave code syntax strictly intact." ) - synth = f"User Request: {user_prompt}\n\n" + "\n".join([f"--- EXPERT {i+1} ---\n{r}" for i, r in enumerate(responses)]) + synth = f"User Request: {user_prompt}\n\n" + "\n".join([f"--- EXPERT {i+1} ---\n{str(r)}" for i, r in enumerate(responses)]) final_response = await ask_agent(c_client, c_mod, [{"role": "system", "content": concat_system}, {"role": "user", "content": synth}], c_prov) except Exception as e: diff --git a/frontend/dashboard.html b/frontend/dashboard.html index 65c25b8..7d7394c 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -369,7 +369,7 @@
@@ -377,7 +377,7 @@
@@ -444,9 +444,9 @@ const colPricing = ref(""); const colDomain = ref(""); - const workers = ref(["gemini-3.5-flash-lite", "", "", "", ""]); - const selectedPrompter = ref("gemini-3.5-flash-lite"); - const selectedConcatenator = ref("gemini-3.5-flash-lite"); + const workers = ref(["", "", "", "", ""]); + const selectedPrompter = ref(""); + const selectedConcatenator = ref(""); const attachedFiles = ref([]); @@ -473,8 +473,7 @@ return matchDomain && matchPrice; }); - // Injection dynamique pour empêcher les cases vides si le modèle n'est pas dans les filtres - if (currentSelectedId && currentSelectedId.trim() !== "" && currentSelectedId !== "gemini-3.5-flash-lite") { + if (currentSelectedId && currentSelectedId.trim() !== "") { const exists = filtered.find(m => m.model_id === currentSelectedId); if (!exists) { const original = modelInfo.models.find(m => m.model_id === currentSelectedId); @@ -546,7 +545,7 @@ }; const downloadTemplate = () => { - const templateContent = `# AETHAS38 - Configuration Multi-Agents\n\n**Rappel important :**\n- **Prompteur :** Le système de l'orchestrateur utilisera ce modèle pour traduire et optimiser votre requête initiale en ANGLAIS (langue native des IA).\n- **Concaténeur :** Le système utilisera ce modèle pour regrouper les réponses des travailleurs et traduire la synthèse finale en FRANÇAIS (en préservant le code brut).\n\n## Sélection des Modèles\nRenseignez l'ID exact du modèle (ex: gemini-3.5-flash-lite, mistralai/mistral-large-4-0). Laissez vide si inutilisé.\n\n- Prompteur : gemini-3.5-flash-lite\n- Travailleur 1 : \n- Travailleur 2 : \n- Travailleur 3 : \n- Travailleur 4 : \n- Travailleur 5 : \n- Concaténeur : gemini-3.5-flash-lite\n`; + const templateContent = `# AETHAS38 - Configuration Multi-Agents\n\n**Rappel important :**\n- **Prompteur :** Le système de l'orchestrateur utilisera ce modèle pour traduire et optimiser votre requête initiale en ANGLAIS (langue native des IA).\n- **Concaténeur :** Le système utilisera ce modèle pour regrouper les réponses des travailleurs et traduire la synthèse finale en FRANÇAIS (en préservant le code brut).\n\n## Sélection des Modèles\nRenseignez l'ID exact du modèle (ex: gemini-3.5-flash-lite, mistralai/mistral-large-4-0). Laissez vide si inutilisé.\n\n- Prompteur : \n- Travailleur 1 : \n- Travailleur 2 : \n- Travailleur 3 : \n- Travailleur 4 : \n- Travailleur 5 : \n- Concaténeur : \n`; downloadFile(templateContent, 'aethas38-config-agents.md'); }; @@ -557,7 +556,6 @@ reader.onload = (e) => { const content = e.target.result; - // Regex hyper souple pour ignorer la casse, les espaces bizarres et les accents const extractModel = (key) => { const safeKey = key.replace(/[éèe]/gi, '[éèe]'); const regex = new RegExp(`-\\s*${safeKey}\\s*:\\s*([^\\r\\n]*)`, 'i'); @@ -571,14 +569,9 @@ const w4 = extractModel('Travailleur 4'); const w5 = extractModel('Travailleur 5'); - // Réassignation complète du tableau pour garantir la réactivité Vue.js workers.value = [w1, w2, w3, w4, w5]; - - const p = extractModel('Prompteur'); - selectedPrompter.value = p !== "" ? p : 'gemini-3.5-flash-lite'; - - const c = extractModel('Concateneur'); - selectedConcatenator.value = c !== "" ? c : 'gemini-3.5-flash-lite'; + selectedPrompter.value = extractModel('Prompteur'); + selectedConcatenator.value = extractModel('Concateneur'); alert("Configuration IA importée et appliquée avec succès !"); }; @@ -631,10 +624,19 @@ const processFile = (file) => { const reader = new FileReader(); + const textExts = ['.txt', '.md', '.csv', '.json', '.py', '.js', '.html', '.css', '.xml', '.yml', '.yaml', '.sh', '.bat', '.ps1', '.ini', '.cfg', '.conf', '.log']; + const isText = textExts.some(ext => file.name.toLowerCase().endsWith(ext)) || file.type.startsWith('text/'); + reader.onload = (e) => { attachedFiles.value.push({ name: file.name, content: e.target.result }); }; - reader.readAsText(file); + + // Les fichiers binaires (PDF, Excel, etc.) sont lus en Base64 pour que Python s'en charge. + if (isText) { + reader.readAsText(file); + } else { + reader.readAsDataURL(file); + } }; const removeFile = (index) => { attachedFiles.value.splice(index, 1); }; @@ -688,7 +690,13 @@ } else { const err = await response.json(); alert("Erreur API IA : " + err.detail); } - } catch (error) {} finally { isAiThinking.value = false; scrollToBottom(); nextTick(()=>lucide.createIcons()); } + } catch (error) { + alert("Erreur locale lors de l'envoi : " + error.message); + } finally { + isAiThinking.value = false; + scrollToBottom(); + nextTick(()=>lucide.createIcons()); + } }; const changePassword = async () => { diff --git a/requirements.txt b/requirements.txt index c436941..47d138f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,7 @@ PyJWT openai httpx python-multipart -pytz \ No newline at end of file +pytz +PyPDF2 +openpyxl +oletools \ No newline at end of file