From 632c23069ab908ace9fbeca35724fc0365e40730 Mon Sep 17 00:00:00 2001 From: Xavier Date: Sat, 22 Aug 2026 03:08:57 +0200 Subject: [PATCH] =?UTF-8?q?MAJ=20Globale=20:=20Chemins=20absolus=20(NAS),?= =?UTF-8?q?=20Model=20Updater=20auto,=20Timeout=2090s=20OpenRouter,=20Pop-?= =?UTF-8?q?up=20et=20tra=C3=A7abilit=C3=A9=20financi=C3=A8re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aethas_ip_logs.json | 6 +++++ modules/ai_engine.py | 6 ++--- modules/ai_openrouter.py | 15 ++++++----- modules/billing_manager.py | 32 ++++-------------------- modules/chat_handler.py | 51 +++----------------------------------- modules/database.py | 7 ++++-- modules/logger.py | 7 +++--- modules/ui_components.py | 1 + test_openrouter.py | 30 ++++++++++++++++++++++ 9 files changed, 66 insertions(+), 89 deletions(-) create mode 100644 test_openrouter.py diff --git a/aethas_ip_logs.json b/aethas_ip_logs.json index 8e07766..f0dc6f9 100644 --- a/aethas_ip_logs.json +++ b/aethas_ip_logs.json @@ -11596,5 +11596,11 @@ "action": "Acc\u00e8s \u00e0 /", "date": "2026-08-22 01:10:01", "timestamp": 1787353801.8667424 + }, + { + "ip": "127.0.0.1", + "action": "Acc\u00e8s \u00e0 /chat", + "date": "2026-08-22 01:59:10", + "timestamp": 1787356750.8306415 } ] \ No newline at end of file diff --git a/modules/ai_engine.py b/modules/ai_engine.py index d9ca621..1b9203a 100644 --- a/modules/ai_engine.py +++ b/modules/ai_engine.py @@ -12,11 +12,10 @@ SYSTEM_CONFIDENTIALITY_INSTRUCTION = ( ) def executer_synthese_multi_ia(prompt_securise: str, fournisseur: str, mode_payant: bool = False, chat_id: str = None): - """Interroge plusieurs modèles en parallèle rapide et confie la synthèse à Gemini.""" try: - # Propagation cruciale du chat_id pour le suivi financier OpenRouter + # Passage du chat_id (code hexadécimal) à l'API Multi reponses_brutes = interroger_openrouter_multi(prompt_securise, type_modele=fournisseur, premium=mode_payant, chat_id=chat_id) - if len(reponses_brutes) >= 1: # Modifié à 1 pour garantir un fallback même partiel + if len(reponses_brutes) >= 1: instruction_synthese = ( "RÈGLE DE CONFIDENTIALITÉ : Tu es un assistant IA. " "Tu ne dois JAMAIS divulguer l'architecture logicielle interne de la plateforme hôte Aethas38. " @@ -37,7 +36,6 @@ def executer_synthese_multi_ia(prompt_securise: str, fournisseur: str, mode_paya return None def executer_requete_ia(fournisseur: str, prompt: str, mode_payant: bool = False, multi_ia: bool = False, chat_id: str = None): - """Moteur central gérant la synthèse multi-modèles et la cascade intelligente.""" prompt_securise = f"{SYSTEM_CONFIDENTIALITY_INSTRUCTION}\n\nRequête utilisateur :\n{prompt}" if multi_ia or fournisseur in ["openrouter_code", "openrouter_droit"]: diff --git a/modules/ai_openrouter.py b/modules/ai_openrouter.py index 6760c65..9850431 100644 --- a/modules/ai_openrouter.py +++ b/modules/ai_openrouter.py @@ -9,6 +9,8 @@ load_dotenv() api_key_openrouter = os.getenv("OPENROUTER_API_KEY") or "sk-or-v1-cle-non-definie" client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=api_key_openrouter) + +# Chemin ABSOLU strict CATALOGUE_FILE = "/DATA/AppData/MULTI-IA-AETHAS38/catalogue_dynamique.json" def verifier_cle_api(): @@ -33,7 +35,6 @@ def obtenir_modele_dynamique(premium: bool): return priorites[0] if priorites else (free_models[0] if free_models else "google/gemma-2-9b-it:free") except Exception: pass - # Fallbacks absolus si fichier introuvable return "anthropic/claude-3.5-sonnet" if premium else "google/gemma-2-9b-it:free" def interroger_openrouter(prompt: str, type_modele: str = "openrouter_chat", premium: bool = False, max_retries: int = 2, chat_id: str = None): @@ -52,7 +53,7 @@ def interroger_openrouter(prompt: str, type_modele: str = "openrouter_chat", pre messages=[{"role": "user", "content": prompt}], extra_headers={"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "Aethas38 Multi-IA"}, extra_body=extra_body_data, - timeout=25.0 + timeout=90.0 # TIMEOUT MONTE A 90s ) texte = response.choices[0].message.content usage = response.usage @@ -63,9 +64,11 @@ def interroger_openrouter(prompt: str, type_modele: str = "openrouter_chat", pre except Exception as e: log_event("WARNING", "OPENROUTER", f"Tentative {tentative + 1} échouée pour {nom_modele_api} : {e}") if tentative == max_retries - 1: - # Force la MAJ du catalogue si le modèle crashe - from modules.model_updater import trigger_update_now - trigger_update_now() + try: + from modules.model_updater import trigger_update_now + trigger_update_now() + except Exception: + pass raise e time.sleep(1) @@ -89,7 +92,7 @@ def interroger_openrouter_multi(prompt: str, type_modele: str = "openrouter_code messages=[{"role": "user", "content": prompt}], extra_headers={"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "Aethas38 Multi-IA"}, extra_body=extra_body_data, - timeout=15.0 + timeout=90.0 # TIMEOUT MONTE A 90s ) contenu = res.choices[0].message.content if contenu: diff --git a/modules/billing_manager.py b/modules/billing_manager.py index 1e7c06c..44bf2a1 100644 --- a/modules/billing_manager.py +++ b/modules/billing_manager.py @@ -5,8 +5,11 @@ from datetime import datetime import tiktoken from modules.logger import log_event -BALANCES_FILE = "aethas_balances.json" -AUDIT_FILE = "aethas_financial_audit.json" +# FORCAGE DES CHEMINS ABSOLUS +BASE_DIR = "/DATA/AppData/MULTI-IA-AETHAS38" +os.makedirs(BASE_DIR, exist_ok=True) +BALANCES_FILE = os.path.join(BASE_DIR, "aethas_balances.json") +AUDIT_FILE = os.path.join(BASE_DIR, "aethas_financial_audit.json") TARIFS_OPENROUTER = { "meta-llama/llama-3.1-8b-instruct:free": 0.0, @@ -21,25 +24,21 @@ TARIFS_OPENROUTER = { VIP_ADMIN_USERS = ["xavier", "céline", "celine", "anaëlle", "anaelle", "renan", "audric"] def charger_soldes() -> dict: - """Charge les soldes réels depuis le stockage persistant.""" if os.path.exists(BALANCES_FILE): try: with open(BALANCES_FILE, "r", encoding="utf-8") as f: return json.load(f) except Exception: pass - # Initialisation de base : cagnotte commune de 35.00 $ soldes = {"cagnotte_commune_vip": 35.00} sauvegarder_soldes(soldes) return soldes def sauvegarder_soldes(soldes: dict): - """Sauvegarde les soldes sur disque.""" with open(BALANCES_FILE, "w", encoding="utf-8") as f: json.dump(soldes, f, indent=4) def enregistrer_transaction_audit(utilisateur: str, rang: str, modele: str, tokens: int, cout_brut: float, prix_facture: float, type_op: str = "DEBIT_REQUETE"): - """Journalise chaque opération financière de manière inaltérable pour les litiges.""" entree = { "timestamp": time.time(), "date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), @@ -51,7 +50,6 @@ def enregistrer_transaction_audit(utilisateur: str, rang: str, modele: str, toke "cout_brut_api": cout_brut, "prix_facture_client": prix_facture } - audits = [] if os.path.exists(AUDIT_FILE): try: @@ -59,13 +57,11 @@ def enregistrer_transaction_audit(utilisateur: str, rang: str, modele: str, toke audits = json.load(f) except Exception: audits = [] - audits.append(entree) with open(AUDIT_FILE, "w", encoding="utf-8") as f: json.dump(audits, f, indent=4) def compter_tokens(texte: str, modele: str = "cl100k_base") -> int: - """Compte le nombre de tokens d'un texte.""" try: enc = tiktoken.get_encoding(modele) return len(enc.encode(texte)) @@ -74,12 +70,6 @@ def compter_tokens(texte: str, modele: str = "cl100k_base") -> int: return 0 def estimer_cout_facture(prompt: str, modele_id: str, rang_utilisateur: str = "standard", multi_ia: bool = False) -> tuple[float, float, int]: - """ - Calcule le coût brut API et le prix facturé selon le rang : - - VIP / Admin : marge 0 % (coût réel déduit de la cagnotte commune) - - Famille : marge 7,5 % (coût / 0.925) - - Standard : marge 15 % (coût / 0.85) - """ tokens = compter_tokens(prompt) cout_unitaire = TARIFS_OPENROUTER.get(modele_id, 0.0) cout_brut = tokens * cout_unitaire @@ -101,16 +91,13 @@ def estimer_cout_facture(prompt: str, modele_id: str, rang_utilisateur: str = "s return round(cout_brut, 6), round(prix_facture, 6), tokens def verifier_et_valider_budget(prompt: str, modele_id: str, utilisateur_nom: str = "xavier", rang_utilisateur: str = "super admin", multi_ia: bool = False): - """Vérifie le solde avant exécution et applique le débit approprié.""" cout_brut, prix_facture, tokens = estimer_cout_facture(prompt, modele_id, rang_utilisateur, multi_ia) - if prix_facture == 0.0: return True, 0.0 soldes = charger_soldes() utilisateur_cle = utilisateur_nom.lower() - # Distinction cagnotte commune VIP/Admin vs compte individuel if utilisateur_cle in VIP_ADMIN_USERS or rang_utilisateur.lower() in ["super admin", "admin", "vip"]: solde_disponible = soldes.get("cagnotte_commune_vip", 0.0) cle_compte = "cagnotte_commune_vip" @@ -122,7 +109,6 @@ def verifier_et_valider_budget(prompt: str, modele_id: str, utilisateur_nom: str log_event("WARNING", "BILLING", f"Rejet : solde insuffisant pour {utilisateur_nom} ({solde_disponible} $ < {prix_facture} $)") raise Exception(f"SOLDE_INSUFFISANT|{prix_facture}|{solde_disponible}") - # Débit strict soldes[cle_compte] = round(solde_disponible - prix_facture, 6) sauvegarder_soldes(soldes) @@ -131,21 +117,14 @@ def verifier_et_valider_budget(prompt: str, modele_id: str, utilisateur_nom: str return True, prix_facture def obtenir_solde(utilisateur_nom: str) -> float: - """Retourne le solde actuel de l'utilisateur ou de la cagnotte commune.""" soldes = charger_soldes() if utilisateur_nom.lower() in VIP_ADMIN_USERS: return soldes.get("cagnotte_commune_vip", 0.0) return soldes.get(utilisateur_nom.lower(), 0.0) def generer_dossier_litige(utilisateur_nom: str, est_admin: bool = False) -> list: - """ - Génère les éléments d'audit pour un litige : - - Pour l'utilisateur : date, modèle, tokens, prix facturé (marge masquée). - - Pour l'Admin : audit complet avec coût brut API. - """ if not os.path.exists(AUDIT_FILE): return [] - try: with open(AUDIT_FILE, "r", encoding="utf-8") as f: tous_les_audits = json.load(f) @@ -158,7 +137,6 @@ def generer_dossier_litige(utilisateur_nom: str, est_admin: bool = False) -> lis if est_admin: dossier.append(item) else: - # Filtrage des marges pour le client dossier.append({ "date": item.get("date"), "type_operation": item.get("type_operation"), diff --git a/modules/chat_handler.py b/modules/chat_handler.py index dcbbc3e..b0c1824 100644 --- a/modules/chat_handler.py +++ b/modules/chat_handler.py @@ -1,10 +1,6 @@ -import os -import json -from datetime import date import uuid import time import re -import psutil from fasthtml.common import * from modules.database import load_history, save_history from modules.scraper import rechercher_sur_google, extraire_contenu_urls @@ -17,41 +13,10 @@ from modules.billing_manager import verifier_et_valider_budget, obtenir_solde from modules.rag_manager import rechercher_contexte_rag, auto_apprendre_rag from modules.moderator import analyser_intention_illicite, appliquer_sanction_et_archiver -def check_and_update_resource_quota(utilisateur_nom, role_utilisateur): - """Vérifie si l'utilisateur a dépassé son quota quotidien d'utilisation intensive du serveur (>60%).""" - quota_file = "/DATA/AppData/MULTI-IA-AETHAS38/Utilisateurs/quotas_ressources.json" - today = str(date.today()) - - limites = {"super admin": 10, "admin": 10, "vip": 5, "famille": 5, "standard": 3} - limite_max = limites.get(role_utilisateur.lower(), 3) - - try: - if os.path.exists(quota_file): - with open(quota_file, "r") as f: - data = json.load(f) - else: - data = {} - except Exception: - data = {} - - if data.get("date") != today: - data = {"date": today, "users": {}} - - user_uses = data["users"].get(utilisateur_nom, 0) - - if user_uses >= limite_max: - return False # Quota dépassé - - data["users"][utilisateur_nom] = user_uses + 1 - os.makedirs(os.path.dirname(quota_file), exist_ok=True) - with open(quota_file, "w") as f: - json.dump(data, f) - - return True - async def handle_chat_request(request, session): start_time = time.time() try: + # Psutil et la gestion de surcharge retirés comme demandé form = await request.form() prompt = form.get("prompt", "") chat_id = form.get("chat_id", str(uuid.uuid4())) @@ -65,7 +30,7 @@ async def handle_chat_request(request, session): if choix_utilisateur != "aucun": log_event("INFO", "BILLING", f"Décision utilisateur face au pop-up : {choix_utilisateur}") if choix_utilisateur == "gratuit_force": - mode_payant = False # Le client a reculé devant le prix + mode_payant = False session["current_chat_id"] = chat_id history = load_history() @@ -74,16 +39,6 @@ async def handle_chat_request(request, session): role_utilisateur = "Super Admin" if utilisateur_nom.lower() == "xavier" else ("VIP" if utilisateur_nom.lower() in ["céline", "anaëlle", "renan", "audric"] else "standard") ip_client = request.client.host if request.client else "Inconnue" - # Protection CPU/RAM (60%) combinée au Quota Utilisateur - cpu_usage = psutil.cpu_percent(interval=0.1) - ram_usage = psutil.virtual_memory().percent - if cpu_usage > 60.0 or ram_usage > 60.0: - if not check_and_update_resource_quota(utilisateur_nom, role_utilisateur): - log_event("WARNING", "SYSTEM", f"Surcharge (CPU: {cpu_usage}%, RAM: {ram_usage}%) + Quota intensif dépassé pour {utilisateur_nom}.") - return Div(H3("⚠️ Limite de requêtes intensives atteinte", style="color:#ff9800;"), P(f"Le serveur est actuellement très sollicité (>60%). En tant que '{role_utilisateur}', vous avez épuisé votre quota de passages en force pour aujourd'hui."), style="background:#333; padding:15px; border-radius:10px; border: 1px solid #ff9800;") - else: - log_event("INFO", "SYSTEM", f"Serveur en charge (>60%), mais quota intensif autorisé utilisé par {utilisateur_nom}.") - est_illicite, raison = analyser_intention_illicite(prompt) if est_illicite: sanction = appliquer_sanction_et_archiver(utilisateur_nom, prompt, raison, ip_client) @@ -162,8 +117,10 @@ async def handle_chat_request(request, session): if prix_estime > obtenir_solde(utilisateur_nom): return Div(H3("❌ Solde Insuffisant", style="color:red;"), P("Veuillez recharger votre compte."), style="background:#300; padding:15px; border-radius:10px;") + # CRUCIAL : Transmission du chat_id à l'Engine reponse, couleur, fournisseur_nom, modele = executer_requete_ia(fournisseur, full_prompt, mode_payant=mode_payant, multi_ia=mode_multi_ia, chat_id=chat_id) + # Le paiement véritable s'exécute APRÈS la réponse de l'IA OpenRouter if "openrouter" in fournisseur_nom.lower() and mode_payant: verifier_et_valider_budget(full_prompt, modele, utilisateur_nom=utilisateur_nom, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia) diff --git a/modules/database.py b/modules/database.py index af98d4a..098d3e9 100644 --- a/modules/database.py +++ b/modules/database.py @@ -2,7 +2,10 @@ import os import json import time -HISTORY_FILE = "aethas_history.json" +# FORCAGE DU CHEMIN ABSOLU +BASE_DIR = "/DATA/AppData/MULTI-IA-AETHAS38" +os.makedirs(BASE_DIR, exist_ok=True) +HISTORY_FILE = os.path.join(BASE_DIR, "aethas_history.json") def load_history(): if not os.path.exists(HISTORY_FILE): @@ -24,4 +27,4 @@ def nettoyer_vieilles_discussions(): for cid in a_supprimer: del history[cid] save_history(history) - print(f"🧹 AETHAS CORE : Nettoyage auto terminé. {len(a_supprimer)} discussion(s) supprimée(s).") + print(f"🧹 AETHAS CORE : Nettoyage auto terminé. {len(a_supprimer)} discussion(s) supprimée(s).") \ No newline at end of file diff --git a/modules/logger.py b/modules/logger.py index 8de8da2..071ac77 100644 --- a/modules/logger.py +++ b/modules/logger.py @@ -1,8 +1,9 @@ +import os import logging from logging.handlers import TimedRotatingFileHandler -import os from datetime import datetime +# Chemin absolu obligatoire pour correspondre au volume Docker LOG_DIR = "/DATA/AppData/MULTI-IA-AETHAS38" os.makedirs(LOG_DIR, exist_ok=True) LOG_FILE = os.path.join(LOG_DIR, "aethas_logs.txt") @@ -11,7 +12,7 @@ logger = logging.getLogger("Aethas38") logger.setLevel(logging.INFO) if not logger.handlers: - # Conformité LCEN : Conservation des logs de connexion sur 365 jours (1 an) + # Conservation 365 jours pour conformité légale handler = TimedRotatingFileHandler(LOG_FILE, when="midnight", interval=1, backupCount=365, encoding="utf-8") handler.suffix = "%Y-%m-%d" formatter = logging.Formatter('[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S') @@ -32,5 +33,5 @@ def log_event(niveau, source, message): try: from modules.debug_mailer import envoyer_alerte_debug envoyer_alerte_debug(niveau, source, message) - except Exception as mail_error: + except Exception: pass \ No newline at end of file diff --git a/modules/ui_components.py b/modules/ui_components.py index d600c7a..404cf72 100644 --- a/modules/ui_components.py +++ b/modules/ui_components.py @@ -103,6 +103,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"), + A("🚪 Déconnexion", href="/logout", cls="export-btn", style="background-color: #8b0000; color: white; border-color: #8b0000; margin-top: 5px;"), # NOUVEAU BOUTON ICI Div( render_paypal_donation_widget(), diff --git a/test_openrouter.py b/test_openrouter.py new file mode 100644 index 0000000..542f5d7 --- /dev/null +++ b/test_openrouter.py @@ -0,0 +1,30 @@ +import os +import requests +from dotenv import load_dotenv +import json + +# Charge ta clé API depuis le fichier .env +load_dotenv() +api_key = os.getenv("OPENROUTER_API_KEY") + +if not api_key: + print("Erreur : Clé OPENROUTER_API_KEY introuvable dans le .env") + exit() + +print("🔍 Interrogation des serveurs OpenRouter...") + +try: + response = requests.get( + "https://openrouter.ai/api/v1/auth/key", + headers={"Authorization": f"Bearer {api_key}"} + ) + + if response.status_code == 200: + print("\n✅ CONNEXION RÉUSSIE ! Voici le JSON de ton compte :") + # Affiche le JSON formaté de manière lisible + print(json.dumps(response.json(), indent=4)) + else: + print(f"\n❌ ERREUR {response.status_code}: {response.text}") + +except Exception as e: + print(f"Erreur de connexion : {e}") \ No newline at end of file