Public Access
MAJ Globale : Chemins absolus (NAS), Model Updater auto, Timeout 90s OpenRouter, Pop-up et traçabilité financière
Build and Push Docker Image / build-and-push (push) Successful in 1m48s
Build and Push Docker Image / build-and-push (push) Successful in 1m48s
This commit is contained in:
1 parent
b4ed2947d4
commit
632c23069a
9 files changed
+63
-86
No files matched your search
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
|
||||
@@ -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"),
|
||||
|
||||
+4
-47
@@ -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)
|
||||
|
||||
|
||||
+4
-1
@@ -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):
|
||||
|
||||
+4
-3
@@ -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
|
||||
@@ -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(),
|
||||
|
||||
@@ -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}")
|
||||
Reference in new issue
Block a user