diff --git a/backend/main.py b/backend/main.py index 25c4a2c..4e2b8d2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -73,7 +73,8 @@ def login(login_data: LoginRequest, response: Response, db: Session = Depends(ge user = db.query(User).filter(User.username == login_data.username).first() if not user or not verify_password(login_data.password, user.hashed_password): raise HTTPException(status_code=401, detail="Identifiants incorrects.") if not verify_totp(user.totp_secret, login_data.totp_code): raise HTTPException(status_code=401, detail="2FA invalide.") - response.set_cookie(key="session_token", value=create_access_token(data={"sub": user.username}), httponly=True, max_age=3600, samesite="lax") + # Correction : Extension de la durée de session à 7 jours (604800 secondes) pour éviter les erreurs 401 intempestives + response.set_cookie(key="session_token", value=create_access_token(data={"sub": user.username}), httponly=True, max_age=604800, samesite="lax") return {"message": "Connexion réussie"} @app.get("/dashboard") @@ -143,10 +144,12 @@ def get_messages(project_id: int, db: Session = Depends(get_db), current_user: U @app.post("/api/projects/{project_id}/messages", response_model=List[MessageResponse]) async def create_message(project_id: int, message: MessageCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): - final_content = message.content + extracted_files_data = [] + files_names = [] + if message.files: - files_text = "" for f in message.files: + files_names.append(f.name) content = f.content if content.startswith("data:"): try: @@ -175,7 +178,6 @@ async def create_message(project_id: int, message: MessageCreate, db: Session = 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 @@ -192,24 +194,28 @@ async def create_message(project_id: int, message: MessageCreate, db: Session = 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}]" + extracted_text = file_bytes.decode('utf-8', errors='replace') - files_text += f"\n\n[Fichier attaché : {f.name}]\n```text\n{extracted_text}\n```" + extracted_files_data.append({"name": f.name, "content": extracted_text}) except Exception as e: - files_text += f"\n\n[Fichier attaché : {f.name} - ERREUR DE DECODAGE: {str(e)}]" + extracted_files_data.append({"name": f.name, "content": f"[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 + extracted_files_data.append({"name": f.name, "content": content}) - db.add(Message(role=message.role, content=final_content, project_id=project_id)) + # On ne stocke plus le contenu brut des fichiers en DB pour éviter d'exploser le contexte des requêtes suivantes + db_content = message.content + if files_names: + db_content += f"\n\n[Fichiers joints pour analyse : {', '.join(files_names)}]" + + db.add(Message(role=message.role, content=db_content, project_id=project_id)) db.commit() history = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all() settings = db.query(SystemSettings).first() conf = message.config.dict() if message.config else {"workers": ["gemini-3.5-flash-lite"]} - ai_resp = await run_orchestrator(db, history, settings, conf) + # Transmission des données de fichiers en mémoire vive à l'orchestrateur (Map-Reduce) + ai_resp = await run_orchestrator(db, history, settings, conf, extracted_files_data) db.add(Message(role="assistant", content=ai_resp, project_id=project_id)) db.commit() diff --git a/backend/orchestrator.py b/backend/orchestrator.py index 14bb251..f66ada3 100644 --- a/backend/orchestrator.py +++ b/backend/orchestrator.py @@ -197,19 +197,39 @@ async def ask_agent(client, model_id, messages, provider="openrouter"): resp = await client.chat.completions.create(**kwargs) return resp.choices[0].message.content -async def run_orchestrator(db: Session, history: list, settings: SystemSettings, config: dict) -> str: +async def run_orchestrator(db: Session, history: list, settings: SystemSettings, config: dict, extracted_files: list = None) -> str: workers = config.get("workers", ["gemini-3.5-flash-lite"]) user_prompt = history[-1].content + original_user_text = user_prompt.split("\n\n[Fichiers joints")[0] if "[Fichiers joints" in user_prompt else user_prompt formatted_history = [{"role": msg.role, "content": msg.content} for msg in history[:-1]] final_response = "" try: - # 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." + + # --- WORKFLOW MAP-REDUCE : PRÉ-TRAITEMENT PARALLÈLE DES FICHIERS --- + files_context = "" + if extracted_files: + async def process_single_file(f): + file_prompt = f"Demande de l'utilisateur : '{original_user_text}'.\n\nAnalysez le fichier ci-dessous. Extrayez, résumez et conservez méticuleusement tout le code, les macros VBA, les requêtes SQL, ou les données métier pertinentes pour répondre à la demande.\n\nFichier : {f['name']}\nContenu :\n```\n{f['content']}\n```" + file_sys = "You are an expert data analyst and senior developer. Extract the most important technical information from the file without losing critical code syntax." + try: + analysis = await ask_agent(p_client, p_mod, [{"role": "system", "content": file_sys}, {"role": "user", "content": file_prompt}], p_prov) + return f"\n\n--- Extraction du fichier {f['name']} ---\n{analysis}" + except Exception as e: + return f"\n\n--- Erreur sur {f['name']} ---\n{str(e)}" + + file_tasks = [process_single_file(f) for f in extracted_files] + file_analyses = await asyncio.gather(*file_tasks) + files_context = "".join(file_analyses) + + user_prompt = f"{original_user_text}\n\nVoici les données pré-traitées des fichiers joints :\n{files_context}" + + # --- OPTIMISATION & TRADUCTION --- + prompt_system = "You are an expert prompt engineer. Translate and optimize the user request and any file context into clear, precise English tailored for AI execution. Keep all code blocks intact." optimized = await ask_agent(p_client, p_mod, [{"role": "system", "content": prompt_system}, {"role": "user", "content": user_prompt}], p_prov) if len(workers) == 1: @@ -237,7 +257,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{str(r)}" for i, r in enumerate(responses)]) + synth = f"User Request: {original_user_text}\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 7d7394c..4446af1 100644 --- a/frontend/dashboard.html +++ b/frontend/dashboard.html @@ -631,7 +631,7 @@ attachedFiles.value.push({ name: file.name, content: e.target.result }); }; - // Les fichiers binaires (PDF, Excel, etc.) sont lus en Base64 pour que Python s'en charge. + // Les fichiers binaires sont lus en Base64. if (isText) { reader.readAsText(file); } else { @@ -687,6 +687,10 @@ const data = await response.json(); if (Array.isArray(data)) messages.value = data; await fetchFinances(); + } else if (response.status === 401) { + alert("Votre session a expiré. Veuillez vous reconnecter."); + window.location.href = '/login'; + return; } else { const err = await response.json(); alert("Erreur API IA : " + err.detail); }