Auto-updater, limites CPU 60% et facturation corrigés
Build and Push Docker Image / build-and-push (push) Successful in 1m55s

This commit is contained in:
xavier committed 2026-08-22 01:10:40 +02:00
1 parent 71d6f51ee0
commit b4ed2947d4
15 files changed
+7413 -122

No files matched your search

+8 -8
View File
@@ -32,7 +32,7 @@ Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minim
### Phase 4 : Gestion des Utilisateurs & Sécurité Légale
- [ ] Mettre en place une bibliothèque logicielle autonome sur le serveur (création PDF, DOCX, etc.).
- [ ] Routage automatique (Gratuit vs Payant) sélectionné par l'utilisateur.
- [x] Routage automatique (Gratuit vs Payant) sélectionné par l'utilisateur.
- [X] Système de connexion sécurisé avec configuration du premier lancement (DOB depuis 1900, Email, Clés API). L'installateur a obligatoirement le rôle "Dieu tout puissant" sur son se (Admin global = Super Admin).
- [x] Système de cryptage de grade militaire (Fernet) et de Hashing (Bcrypt) mis en place.
- [X] Sécurité Légère : Fail2Ban, protection Bruteforce et DDoS. **Blocage total des accès VPN sauf adresses locales (192.168.xxx.xxx & 10.xxx.xxx.xxx)**.
@@ -41,8 +41,8 @@ Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minim
### Phase 5 : Sécurité Financière & Facturation Exacte 💰
- [x] Pré-calcul local du nombre de tokens (via `tiktoken`) pour estimer le coût avant envoi (Module `billing_manager.py`). Bloquage automatique si coût > 0.
- [x] Synchronisation dynamique du catalogue OpenRouter et calcul de facturation au token réel (`tokens_in` / `tokens_out` via `response.usage`). <!-- MAJ 20/08/2026 -->
- [ ] Interface UI : Affichage du Pop-Up de validation du coût pour l'utilisateur.
- [x] Synchronisation dynamique du catalogue OpenRouter et calcul de facturation au token réel (`tokens_in` / `tokens_out` via `response.usage`).
- [x] Interface UI : Affichage du Pop-Up OBLIGATOIRE de validation du coût pour TOUS les utilisateurs.
- [ ] Cryptage absolu de toutes les données sensibles et bancaires.
- [ ] Module de conversion des devises en temps réel (Taux du jour) pour un affichage transparent.
- [ ] **Utilisateurs Privilégiés (Zéro Marge) :** A définir par Admin/Super Admin. Autorisation de virements manuels pour la cagnotte.
@@ -50,11 +50,11 @@ Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minim
- [ ] Export comptable Excel (TCD) par utilisateur via Pandas/Openpyxl et archivage PDF automatisé.
### Phase 6 : Nouvelles Capacités IA (Spécialisations & Modèles) 🤖
*Note : L'infrastructure bascule sur un catalogue dynamique pour pallier les décommissionnements constants des fournisseurs tiers.*
*Note : L'infrastructure bascule sur un catalogue dynamique via `modules/model_updater.py` pour pallier les décommissionnements constants des fournisseurs tiers (MAJ toutes les 2h).*
| Domaine de tâche | Modèle / Moteur | Fournisseur / Hébergement | Rôle principal |
| :--- | :--- | :--- | :--- |
| **Orchestration & Tri** | Qwen 2.5 Coder (3B) | Local (GTX 1650 / Ollama) | Analyse l'intention, juge de sécurité et route vers la bonne IA <!-- MAJ 20/08/2026 --> |
| **Orchestration & Tri** | Llama 3.2 3B Instruct Abliterated | Local (GTX 1650 / Ollama) | Analyse l'intention, juge de sécurité sans censure et route vers la bonne IA |
| **Bureautique & Code** | à définir par le Super Admin | Programmation, scripts, macros VBA, mathématiques |
| **Images HD / Fichiers** | à définir par le Super Admin | Analyse d'images/vidéos et documents joints |
| **Génération Multimédia** | à définir par le Super Admin | Création d'images et vidéos (Text-to-Image / Text-to-Video) |
@@ -67,18 +67,18 @@ Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minim
- [ ] Création d'un Wiki détaillé multilingue pour l'installation personnelle.
### Phase 9 : Modération Automatisée & Conformité Pénale 🚨
<!-- MAJ 19/08/2026 : Intégration du Juge Qwen et gestion d'audit pénal -->
- [x] Système hiérarchique strict : `Super Admin` (Installateur), `Admin` (Modération/VIP), `VIP` (Grade de filiation sans privilèges d'administration) et Utilisateurs.
- [ ] Validation obligatoire des CGV/CGU incluant une clause de "Ban sans remboursement et signalement aux autorités" en cas de contenu manifestement illicite.
- [x] Modération préventive "Anti-Zèle" : Filtrage par IA Locale (Qwen) des intentions illégales bloquant la requête vers OpenRouter tout en tolérant la fiction et les demandes juridiques.
- [x] Modération préventive "Anti-Zèle" : Filtrage par IA Locale des intentions illégales bloquant la requête vers OpenRouter tout en tolérant la fiction et les demandes juridiques.
- [x] Système d'Avertissements (Strikes) automatisé avec alerte E-mail aux Admins.
- [x] Protection anti-abus Admin : Si un Admin enfreint la loi, il est exclu de l'alerte ; seuls les autres Admins statuent.
- [x] Archivage légal sécurisé : Stockage pérenne des logs, preuves d'infractions et **décisions d'audit manuscrites des Admins** dans une arborescence dédiée.
### Phase 10 : Base de Connaissances Intelligente (RAG) 📚
- [x] Initialisation de l'arborescence dynamique (`modules/rag_manager.py`) sur le RAID (`/DATA/AppData/MULTI-IA-AETHAS38/RAG/`).
- [x] Évolution de l'Orchestrateur Local (Qwen) pour la sélection automatique du dossier sémantique pertinent ("minecraft", "droit", etc.).
- [x] Évolution de l'Orchestrateur Local pour la sélection automatique du dossier sémantique pertinent ("minecraft", "droit", etc.).
- [x] Injection asynchrone des documents Markdown et Texte directement dans la fenêtre de contexte des LLMs.
- [x] Auto-Apprentissage : Le système génère automatiquement des fichiers `.md` de son propre savoir après validation.
- [ ] Interface UI : Ajout d'une vue Administrateur pour téléverser de nouveaux documents RAG directement depuis le navigateur.
## 📷 Vidéosurveillance & Détection Intelligente
+1 -1
View File
@@ -1,3 +1,3 @@
{
"cagnotte_commune_vip": 35.0
"cagnotte_commune_vip": 34.988855
}
+57
View File
@@ -0,0 +1,57 @@
[
{
"timestamp": 1787262333.1032357,
"date": "2026-08-20 23:45:33",
"utilisateur": "xavier",
"rang": "Super Admin",
"type_operation": "DEBIT_REQUETE",
"modele": "anthropic/claude-3.5-sonnet",
"tokens": 87,
"cout_brut_api": 0.00261,
"prix_facture_client": 0.00261
},
{
"timestamp": 1787262606.6284242,
"date": "2026-08-20 23:50:06",
"utilisateur": "xavier",
"rang": "Super Admin",
"type_operation": "DEBIT_REQUETE",
"modele": "anthropic/claude-3.5-sonnet",
"tokens": 88,
"cout_brut_api": 0.00132,
"prix_facture_client": 0.00132
},
{
"timestamp": 1787263101.5169146,
"date": "2026-08-20 23:58:21",
"utilisateur": "xavier",
"rang": "Super Admin",
"type_operation": "DEBIT_REQUETE",
"modele": "anthropic/claude-3.5-sonnet",
"tokens": 88,
"cout_brut_api": 0.00264,
"prix_facture_client": 0.00264
},
{
"timestamp": 1787344221.9884093,
"date": "2026-08-21 22:30:21",
"utilisateur": "xavier",
"rang": "Super Admin",
"type_operation": "DEBIT_REQUETE",
"modele": "anthropic/claude-3.5-sonnet",
"tokens": 17,
"cout_brut_api": 0.000255,
"prix_facture_client": 0.000255
},
{
"timestamp": 1787348873.0218458,
"date": "2026-08-21 23:47:53",
"utilisateur": "xavier",
"rang": "Super Admin",
"type_operation": "DEBIT_REQUETE",
"modele": "anthropic/claude-3.5-sonnet",
"tokens": 288,
"cout_brut_api": 0.00432,
"prix_facture_client": 0.00432
}
]
+7038
View File
File diff suppressed because it is too large. Load diff
+23 -6
View File
@@ -11,6 +11,9 @@ 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
# Lancement du module d'auto-mise à jour en arrière-plan
import modules.model_updater
# --- MIDDLEWARE DE SÉCURITÉ ---
class SecurityMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
@@ -20,7 +23,10 @@ class SecurityMiddleware(BaseHTTPMiddleware):
if not request.url.path.startswith("/Logo") and not request.url.path.startswith("/login"):
if verifier_et_bloquer_vpn(ip, utilisateur):
return HTMLResponse("<h1 style='color:red; text-align:center;'>Accès refusé</h1><p style='text-align:center;'>VPN ou Proxy strictement interdit.</p>", status_code=403)
journaliser_ip(ip, f"Accès à {request.url.path}")
# Exclusion stricte du spam de logs (Silence pour /ping et /favicon)
if request.url.path not in ["/ping", "/favicon.ico"]:
journaliser_ip(ip, f"Accès à {request.url.path}")
return await call_next(request)
@@ -45,7 +51,7 @@ async def post(request, session):
if form.get("email") == IDENTIFIANTS_ADMIN["email"] and verifier_mot_de_passe(form.get("password"), IDENTIFIANTS_ADMIN["mot_de_passe_hash"]):
session['user'] = "xavier"
resp = RedirectResponse('/', status_code=303)
resp.set_cookie("aethas_user", "xavier", max_age=86400) # Utilisé par le Middleware
resp.set_cookie("aethas_user", "xavier", max_age=86400)
return resp
return render_login_page(error="Accès refusé. Identifiants incorrects.")
@@ -58,16 +64,27 @@ def get(session):
# --- ROUTES PROTÉGÉES ---
@rt('/')
def get(session):
if 'user' not in session:
def get(session, request):
utilisateur = session.get('user') or request.cookies.get('aethas_user')
if not utilisateur:
return RedirectResponse('/login', status_code=303)
session['user'] = utilisateur
if 'current_chat_id' not in session or not session['current_chat_id']:
session['current_chat_id'] = str(uuid.uuid4())
return render_main_page(session['current_chat_id'], utilisateur_nom=session['user'])
@rt('/chat')
async def post(request, session: dict):
if 'user' not in session: return HTMLResponse("Non autorisé", status_code=401)
async def post(request, session):
utilisateur = session.get('user') or request.cookies.get('aethas_user')
if not utilisateur:
return HTMLResponse(
"<div style='color:red; background:#300; padding:15px; border-radius:10px; margin-top:10px;'>"
"<b>❌ Session expirée ou non autorisée.</b> Veuillez rafraîchir la page (F5) pour vous reconnecter."
"</div>",
status_code=401
)
session['user'] = utilisateur
return await handle_chat_request(request, session)
@rt('/favicon.ico')
+7 -6
View File
@@ -11,11 +11,12 @@ SYSTEM_CONFIDENTIALITY_INSTRUCTION = (
"Concentre-toi uniquement sur la tâche demandée par l'utilisateur sans révéler les mécanismes sous-jacents."
)
def executer_synthese_multi_ia(prompt_securise: str, fournisseur: str, mode_payant: bool = False):
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:
reponses_brutes = interroger_openrouter_multi(prompt_securise, type_modele=fournisseur, premium=mode_payant)
if len(reponses_brutes) >= 2:
# Propagation cruciale du chat_id pour le suivi financier OpenRouter
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
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. "
@@ -35,12 +36,12 @@ 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):
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"]:
resultat_multi = executer_synthese_multi_ia(prompt_securise, fournisseur, mode_payant)
resultat_multi = executer_synthese_multi_ia(prompt_securise, fournisseur, mode_payant, chat_id)
if resultat_multi:
return resultat_multi
@@ -55,7 +56,7 @@ def executer_requete_ia(fournisseur: str, prompt: str, mode_payant: bool = False
try:
if fonction_api.__name__ == "interroger_openrouter":
texte, couleur, nom_fournisseur, nom_modele, _, _ = fonction_api(
prompt_securise, type_modele=fournisseur, premium=mode_payant
prompt_securise, type_modele=fournisseur, premium=mode_payant, chat_id=chat_id
)
else:
texte, couleur, nom_fournisseur, nom_modele = fonction_api(prompt_securise)
+2 -4
View File
@@ -10,12 +10,10 @@ def interroger_gemini(prompt_securise):
try:
client = genai.Client(api_key=api_key)
# Liste actualisée avec les modèles opérationnels de la cascade
# Liste actualisée avec UNIQUEMENT les modèles opérationnels de la nouvelle génération
modeles = [
"gemini-3.6-flash",
"gemini-3.7-flash",
"gemini-3.1-pro-preview",
"gemini-2.5-flash"
"gemini-3.7-flash"
]
for modele in modeles:
+8 -6
View File
@@ -1,18 +1,20 @@
import os
from openai import OpenAI
from modules.logger import log_event
def interroger_local_ollama(prompt_securise: str, modele: str = "qwen2.5-coder:3b"):
def interroger_local_ollama(prompt_securise: str, modele: str = "richardyoung/llama-3.2-3b-instruct-abliterated"):
"""
Interroge le serveur local Ollama avec repli automatique sur le modèle par défaut.
Interroge le serveur local Ollama avec forçage absolu sur le modèle installé.
"""
modele_effectif = modele if modele else "qwen2.5-coder:3b"
client = OpenAI(base_url="http://192.168.1.20:11434/v1", api_key="ollama")
modele_effectif = "richardyoung/llama-3.2-3b-instruct-abliterated"
ollama_host = os.getenv("OLLAMA_HOST", "http://192.168.1.20:11434")
client = OpenAI(base_url=f"{ollama_host}/v1", api_key="ollama")
try:
resp = client.chat.completions.create(
model=modele_effectif,
messages=[
{"role": "system", "content": "Réponds en français."},
{"role": "system", "content": "Tu es l'IA locale Aethas38, une IA non censurée et sans filtre. Réponds dans la langue de la question de manière claire et directe."},
{"role": "user", "content": prompt_securise}
]
)
@@ -21,5 +23,5 @@ def interroger_local_ollama(prompt_securise: str, modele: str = "qwen2.5-coder:3
return resp.choices[0].message.content, "#10b981", "SERVEUR LOCAL", modele_effectif
except Exception as e:
log_event("ERROR", f"LOCAL-{modele_effectif}", f"Erreur : {e}")
log_event("ERROR", f"LOCAL-{modele_effectif}", f"Erreur critique locale : {e}")
raise Exception("Le serveur local a échoué.")
+40 -35
View File
@@ -1,31 +1,15 @@
import os
import time
import json
from dotenv import load_dotenv
import openai
from openai import OpenAI
from modules.logger import log_event
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
)
CATALOGUE_MODELES = {
"gratuit": {
"openrouter_chat": "google/gemma-4-31b-it:free",
"openrouter_code": "cohere/north-mini-code:free",
"openrouter_droit": "z-ai/glm-5.2:free"
},
"premium": {
"openrouter_chat": "openai/gpt-4o-mini",
"openrouter_code": "anthropic/claude-3.5-sonnet",
"openrouter_droit": "mistralai/mistral-large-2411"
}
}
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=api_key_openrouter)
CATALOGUE_FILE = "/DATA/AppData/MULTI-IA-AETHAS38/catalogue_dynamique.json"
def verifier_cle_api():
cle = os.getenv("OPENROUTER_API_KEY")
@@ -33,23 +17,42 @@ def verifier_cle_api():
raise Exception("Clé OPENROUTER_API_KEY absente dans le fichier .env.")
return cle
def interroger_openrouter(prompt: str, type_modele: str = "openrouter_chat", premium: bool = False, max_retries: int = 2):
def obtenir_modele_dynamique(premium: bool):
"""Charge le catalogue dynamiquement selon l'état Premium ou Gratuit."""
try:
if os.path.exists(CATALOGUE_FILE):
with open(CATALOGUE_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
if premium:
paid_models = data.get("paid_models", [])
claude = [m for m in paid_models if "claude-3.5-sonnet" in m.lower()]
return claude[0] if claude else (paid_models[0] if paid_models else "anthropic/claude-3.5-sonnet")
else:
free_models = data.get("free_models", [])
priorites = [m for m in free_models if "gemma" in m.lower() or "nemotron" in m.lower()]
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):
verifier_cle_api()
gamme = "premium" if premium else "gratuit"
nom_modele_api = CATALOGUE_MODELES[gamme].get(type_modele, CATALOGUE_MODELES[gamme]["openrouter_chat"])
nom_modele_api = obtenir_modele_dynamique(premium)
nom_affichage = f"OpenRouter ({nom_modele_api.split('/')[-1]})"
couleur = "#d97757" if premium else "#4a90e2"
# Injection du Tracker Financier OpenRouter
extra_body_data = {"session_id": chat_id} if chat_id else {}
for tentative in range(max_retries):
try:
response = client.chat.completions.create(
model=nom_modele_api,
messages=[{"role": "user", "content": prompt}],
extra_headers={
"HTTP-Referer": "https://aethas38.duckdns.org",
"X-Title": "Aethas38 Multi-IA"
},
timeout=20.0
extra_headers={"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "Aethas38 Multi-IA"},
extra_body=extra_body_data,
timeout=25.0
)
texte = response.choices[0].message.content
usage = response.usage
@@ -60,19 +63,23 @@ 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()
raise e
time.sleep(1)
return None, None, None, None, 0, 0
def interroger_openrouter_multi(prompt: str, type_modele: str = "openrouter_code", premium: bool = False) -> list:
def interroger_openrouter_multi(prompt: str, type_modele: str = "openrouter_code", premium: bool = False, chat_id: str = None) -> list:
verifier_cle_api()
extra_body_data = {"session_id": chat_id} if chat_id else {}
modeles = [obtenir_modele_dynamique(premium)]
if premium:
modeles = ["anthropic/claude-3.5-sonnet", "openai/gpt-4o-mini"]
modeles.append("openai/gpt-4o-mini")
else:
# Modèles gratuits réactifs sans blocage de quota
modeles = ["cohere/north-mini-code:free", "google/gemma-4-31b-it:free"]
modeles.append("nvidia/nemotron-3-nano-30b-a3b:free")
reponses = []
for modele in modeles:
@@ -80,10 +87,8 @@ def interroger_openrouter_multi(prompt: str, type_modele: str = "openrouter_code
res = client.chat.completions.create(
model=modele,
messages=[{"role": "user", "content": prompt}],
extra_headers={
"HTTP-Referer": "https://aethas38.duckdns.org",
"X-Title": "Aethas38 Multi-IA"
},
extra_headers={"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "Aethas38 Multi-IA"},
extra_body=extra_body_data,
timeout=15.0
)
contenu = res.choices[0].message.content
+90 -31
View File
@@ -1,18 +1,54 @@
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
from modules.router import nettoyer_decision_router
from modules.ai_local import interroger_local_ollama
from modules.ai_engine import executer_requete_ia
from modules.ui_components import render_user_msg, render_ai_msg, render_sidebar
from modules.ui_components import render_user_msg, render_ai_msg, render_sidebar, render_popup_paiement
from modules.logger import log_event
from modules.billing_manager import verifier_et_valider_budget
from modules.rag_manager import rechercher_contexte_rag
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:
@@ -23,6 +59,13 @@ async def handle_chat_request(request, session):
mode_payant = form.get("mode_payant") == "on"
mode_multi_ia = form.get("mode_multi_ia") == "on"
validation_acquise = form.get("validation_acquise") == "true"
choix_utilisateur = form.get("choix_utilisateur", "aucun")
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
session["current_chat_id"] = chat_id
history = load_history()
@@ -31,16 +74,20 @@ 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"
# Modération préventive locale
est_illicite, raison = analyser_intention_illicite(prompt, "qwen2.5-coder:3b")
# 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)
return Div(
H3("⚠️ Avertissement de Sécurité"),
P("Votre requête enfreint les conditions d'utilisation de la plateforme."),
P(f"Sanction : {sanction}"),
style="color:red; background:#300; padding:15px; border-radius:10px;"
)
return Div(H3("⚠️ Avertissement de Sécurité"), P("Votre requête enfreint les conditions d'utilisation de la plateforme."), P(f"Sanction : {sanction}"), style="color:red; background:#300; padding:15px; border-radius:10px;")
file_summary = []
file_names = []
@@ -54,7 +101,6 @@ async def handle_chat_request(request, session):
log_mode = "PAYANT 👑" if mode_payant else "GRATUIT 🆓"
log_event("INFO", "SYSTEM", f"Requête reçue [{log_mode}]. Fichiers joints : {liste_fichiers_str}")
# NOUVEAU PROMPT : On demande à Qwen de deviner le thème RAG
sec_prompt = f"""Tu es l'Orchestrateur système. Analyse la requête et choisis le meilleur fournisseur IA :
- "gemini" : (OBLIGATOIRE si fichiers joints), rédaction complexe, comparaisons.
- "openrouter_chat" : requêtes générales, texte rapide, traduction.
@@ -66,7 +112,7 @@ async def handle_chat_request(request, session):
Requête : {prompt}"""
try:
texte_routeur, _, _, _ = interroger_local_ollama(sec_prompt, "qwen2.5-coder:3b")
texte_routeur, _, _, _ = interroger_local_ollama(sec_prompt)
decision = nettoyer_decision_router(texte_routeur)
except Exception:
decision = {"intention": "Analyse de secours", "fournisseur": "openrouter_code" if "minecraft" in prompt.lower() else "openrouter_chat", "theme_rag": "aucun"}
@@ -75,14 +121,17 @@ async def handle_chat_request(request, session):
fournisseur = decision.get("fournisseur", "openrouter_chat").lower()
thematique_rag = decision.get("theme_rag", "aucun").lower()
if mode_payant or mode_multi_ia:
fournisseur = "openrouter_code"
intention += " (Forcé API Premium/Multi via UI)"
if file_names:
fournisseur = "gemini"
intention += " (Forcé Gemini via Sécurité Fichiers)"
# Récupération dynamique du contexte RAG
contexte_rag = ""
if thematique_rag != "aucun":
contexte_rag = rechercher_contexte_rag(prompt, thematique=thematique_rag)
contexte_rag = rechercher_contexte_rag(prompt, categorie=thematique_rag)
urls = re.findall(r"https?://[^\s]+", prompt)
mots_cles = ["cherche", "recherche", "sur internet", "sur le web", "sur le net", "trouve", "site"]
@@ -91,44 +140,54 @@ async def handle_chat_request(request, session):
web_text = extraire_contenu_urls(list(set(urls))) if urls else ""
# Concaténation de tous les contextes
full_prompt = prompt + ("\n" + "\n".join(file_summary) if file_summary else "")
if contexte_rag:
full_prompt += f"\n\n[Contexte Documentaire RAG (Base de connaissances interne) :\n{contexte_rag}]"
if web_text:
full_prompt += f"\n\n[Contexte web :\n{web_text}]"
# Validation financière
if "openrouter" in fournisseur:
modele_estime = "anthropic/claude-3.5-sonnet" if mode_payant else "cohere/north-mini-code:free"
verifier_et_valider_budget(full_prompt, modele_estime, utilisateur_nom=utilisateur_nom, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia)
if mode_payant and not validation_acquise:
from modules.billing_manager import estimer_cout_facture
_, prix_estime, _ = estimer_cout_facture(full_prompt, modele_estime, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia)
if prix_estime > 0:
return render_popup_paiement(prix_estime, prompt, chat_id, mode_multi_ia)
# Exécution rapide
reponse, couleur, fournisseur_nom, modele = executer_requete_ia(
fournisseur, full_prompt, mode_payant=mode_payant, multi_ia=mode_multi_ia
)
# Empêche un utilisateur classique avec un solde à zéro de consommer du Premium
if mode_payant and role_utilisateur.lower() not in ["super admin", "admin", "vip", "famille"]:
from modules.billing_manager import estimer_cout_facture
_, prix_estime, _ = estimer_cout_facture(full_prompt, modele_estime, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia)
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;")
reponse, couleur, fournisseur_nom, modele = executer_requete_ia(fournisseur, full_prompt, mode_payant=mode_payant, multi_ia=mode_multi_ia, chat_id=chat_id)
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)
if thematique_rag != "aucun" and reponse:
auto_apprendre_rag(prompt, reponse, thematique_rag)
if chat_id not in history:
history[chat_id] = {"title": (prompt[:30] if prompt else "Discussion") + "...", "pinned": False, "timestamp": time.time(), "messages": []}
execution_time = round(time.time() - start_time, 1)
display_msg = prompt
if file_names:
display_msg += f"\n\n📎 **Fichiers joints :** {', '.join(file_names)}"
display_msg += "\n> *🗑️ Note : Fichiers traités de manière éphémère en mémoire vive.*"
display_msg += f"\n\n📎 **Fichiers joints :** {', '.join(file_names)}\n> *🗑️ Note : Fichiers traités de manière éphémère en mémoire vive.*"
history[chat_id]["messages"].append({"role": "user", "content": display_msg})
history[chat_id]["messages"].append({
"role": "ai", "content": reponse, "intention": intention,
"fournisseur": fournisseur_nom, "modele": modele,
"couleur": couleur, "execution_time": execution_time
})
history[chat_id]["messages"].append({"role": "ai", "content": reponse, "intention": intention, "fournisseur": fournisseur_nom, "modele": modele, "couleur": couleur, "execution_time": execution_time})
save_history(history)
log_event("INFO", fournisseur_nom, f"Réponse générée en {execution_time}s.")
return render_user_msg(display_msg), render_ai_msg(reponse, intention, fournisseur_nom, modele, couleur, execution_time), Script("renderMarkdown();"), render_sidebar()
except Exception as e:
if "SOLDE_INSUFFISANT" in str(e):
details = str(e).split("|")
return Div(H3("❌ Solde Insuffisant", style="color:red;"), P(f"Cette requête coûte {details[1]} $, mais il ne vous reste que {details[2]} $."), P("Veuillez recharger votre compte ou désactiver le mode Payant."), style="background:#300; padding:15px; border-radius:10px; border: 1px solid red;")
log_event("CRITICAL", "SYSTEM", f"Erreur dans handle_chat_request : {e}")
return Div(f"Erreur système : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;")
+24 -16
View File
@@ -1,28 +1,36 @@
import logging
from logging.handlers import TimedRotatingFileHandler
import os
from datetime import datetime
# Le fichier de destination pour les logs locaux
LOG_FILE = "aethas_logs.txt"
LOG_DIR = "/DATA/AppData/MULTI-IA-AETHAS38"
os.makedirs(LOG_DIR, exist_ok=True)
LOG_FILE = os.path.join(LOG_DIR, "aethas_logs.txt")
logger = logging.getLogger("Aethas38")
logger.setLevel(logging.INFO)
if not logger.handlers:
# Conformité LCEN : Conservation des logs de connexion sur 365 jours (1 an)
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')
handler.setFormatter(formatter)
logger.addHandler(handler)
def log_event(niveau, source, message):
"""
Enregistre un événement dans le fichier de log local.
Déclenche l'envoi d'un e-mail si le niveau est ERROR ou CRITICAL.
"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_line = f"[{timestamp}] [{niveau}] [{source}] {message}\n"
msg_formate = f"[{source}] {message}"
# 1. Écriture sécurisée dans le fichier de log local
try:
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(log_line)
except Exception as e:
print(f"ÉCHEC D'ÉCRITURE LOG : Impossible d'écrire dans {LOG_FILE} : {e}")
if niveau.upper() == "INFO": logger.info(msg_formate)
elif niveau.upper() == "WARNING": logger.warning(msg_formate)
elif niveau.upper() == "ERROR": logger.error(msg_formate)
elif niveau.upper() == "CRITICAL": logger.critical(msg_formate)
print(f"[{datetime.now().strftime('%H:%M:%S')}] [{niveau}] {msg_formate}")
# 2. Déclenchement de l'alerte e-mail pour la phase de Debug (Import local pour éviter l'import circulaire)
if niveau in ["ERROR", "CRITICAL"]:
try:
from modules.debug_mailer import envoyer_alerte_debug
envoyer_alerte_debug(niveau, source, message)
except Exception as mail_error:
print(f"ÉCHEC D'ENVOI E-MAIL : Impossible d'avertir l'administrateur : {mail_error}")
pass
+55
View File
@@ -0,0 +1,55 @@
import os
import json
import requests
import threading
import time
from modules.logger import log_event
CATALOGUE_FILE = "/DATA/AppData/MULTI-IA-AETHAS38/catalogue_dynamique.json"
def _do_update():
ollama_host = os.getenv("OLLAMA_HOST", "http://192.168.1.20:11434")
try:
# MAJ locale Abliterated
resp_local = requests.post(
f"{ollama_host}/api/pull",
json={"name": "richardyoung/llama-3.2-3b-instruct-abliterated"},
timeout=300
)
if resp_local.status_code == 200:
log_event("INFO", "UPDATER", "Modèle local Abliterated vérifié/mis à jour.")
# MAJ du catalogue OpenRouter (Gratuits ET Payants)
resp_or = requests.get("https://openrouter.ai/api/v1/models", timeout=30)
if resp_or.status_code == 200:
data = resp_or.json().get("data", [])
modeles_gratuits = []
modeles_payants = []
for m in data:
pricing = m.get("pricing", {})
# Gratuits purs
if pricing.get("prompt") == "0" and pricing.get("completion") == "0":
modeles_gratuits.append(m["id"])
# Ciblage des payants ultra-performants (Claude & OpenAI)
elif "claude-3.5-sonnet" in m["id"].lower() or "gpt-4o" in m["id"].lower():
modeles_payants.append(m["id"])
with open(CATALOGUE_FILE, "w", encoding="utf-8") as f:
json.dump({"free_models": modeles_gratuits, "paid_models": modeles_payants}, f)
log_event("INFO", "UPDATER", f"Catalogue dynamique mis à jour ({len(modeles_gratuits)} gratuits, {len(modeles_payants)} payants).")
except Exception as e:
log_event("ERROR", "UPDATER", f"Erreur lors de la MAJ auto : {e}")
def trigger_update_now():
"""Déclenchement forcé du scan en cas d'erreur de modèle."""
threading.Thread(target=_do_update, daemon=True).start()
def update_models_background():
_do_update()
while True:
time.sleep(7200) # Attente 2 heures
_do_update()
threading.Thread(target=update_models_background, daemon=True).start()
+5 -4
View File
@@ -23,9 +23,10 @@ def analyser_intention_illicite(prompt: str, ai_orchestrateur: str = "qwen2.5-co
"""
modele_cible = ai_orchestrateur if ai_orchestrateur else "qwen2.5-coder:3b"
# NOUVEAU PROMPT : Immunité absolue pour les jeux vidéo et la fiction
prompt_juge = f"""Tu es le Juge de Sécurité de la plateforme logicielle Aethas38.
Ta mission est d'analyser la requête et de détecter UNIQUEMENT les intentions strictement illégales (pédopornographie, drogues, terrorisme, haine).
RÈGLE ANTI-ZÈLE : Ne bloque QUE l'intention malveillante. Laisse passer les questions juridiques légitimes, la météo, l'aide à l'écriture de fiction ou les infos neutres.
Ta mission est d'analyser la requête et de détecter UNIQUEMENT les intentions strictement et réellement illégales (pédopornographie, vente de drogues réelles, terrorisme dans le monde réel, discours de haine).
RÈGLE ANTI-ZÈLE ET IMMUNITÉ GAMING : Tu dois OBLIGATOIREMENT laisser passer et considérer comme LÉGAL tout ce qui relève des jeux vidéo (comme Minecraft, les mods type Draconic Evolution ou Applied Energistics, KubeJS, les serveurs), de la fiction, du roleplay, ou des questions juridiques. La construction de "réacteurs", de "bombes", ou de "pièges" dans un contexte de jeu vidéo est TOTALEMENT AUTORISÉE.
Renvoie STRICTEMENT un JSON valide : {{"illegal": true ou false, "raison": "Explication très courte ou vide"}}
Requête à analyser : {prompt}"""
@@ -42,8 +43,8 @@ Requête à analyser : {prompt}"""
except Exception as e:
log_event("WARNING", "MODERATOR", f"Échec de l'analyse IA (Fallback activé) : {e}")
# Fallback classique par mots-clés si l'IA locale est indisponible
mots_interdits = ["pédopornographie", "bombe", "terrorisme"]
# Fallback affiné pour ne pas bloquer les mots courants du jeu vidéo
mots_interdits = ["pédopornographie", "attentat terroriste réel", "vente de drogue illicite"]
if any(mot in prompt.lower() for mot in mots_interdits):
return True, "Détection par filtre de mots-clés (Secours)"
return False, ""
+35 -4
View File
@@ -1,4 +1,5 @@
import os
from datetime import datetime
from modules.logger import log_event
# Définition du chemin racine sécurisé sur ton RAID
@@ -53,16 +54,46 @@ def extraire_contexte_rag(thematique):
def rechercher_contexte_rag(prompt, categorie="general"):
"""
Fonction appelée par le chat_handler pour récupérer le contexte.
Pour l'instant, c'est un bouchon qui empêche le crash du serveur.
Bouchon temporaire : empêche le plantage en attendant la vectorisation sémantique avancée.
"""
try:
# À terme, nous ferons le lien intelligent ici avec extraire_contexte_rag()
# et une analyse sémantique. Pour l'instant on renvoie vide pour tester.
return ""
except Exception as e:
log_event("ERROR", "RAG_MANAGER", f"Erreur lors de la recherche RAG : {e}")
return ""
def auto_apprendre_rag(prompt, reponse, thematique_simplifiee):
"""
Sauvegarde automatiquement les réponses pertinentes pour enrichir la base de connaissances.
"""
# Mapping entre le mot-clé deviné par Qwen et le vrai nom de ton dossier
mapping = {
"minecraft": "Minecraft_KubeJS",
"droit": "Juridique_Loi_Francaise",
"serveur": "Serveur_Debian",
"excel": "Bureautique_Excel",
"regles": "Regles_Aethas38"
}
theme_reel = mapping.get(thematique_simplifiee.lower())
if not theme_reel:
return # Si Qwen n'a pas détecté de thème précis (ex: "aucun"), on n'enregistre rien pour ne pas polluer.
try:
chemin_theme = os.path.join(RAG_BASE_DIR, theme_reel)
os.makedirs(chemin_theme, exist_ok=True)
nom_fichier = f"apprentissage_{datetime.now().strftime('%Y%m%d_%H%M%S')}.md"
chemin_fichier = os.path.join(chemin_theme, nom_fichier)
contenu = f"## Question utilisateur\n{prompt}\n\n## Réponse de l'IA (Contexte validé)\n{reponse}\n"
with open(chemin_fichier, 'w', encoding='utf-8') as f:
f.write(contenu)
log_event("INFO", "RAG_MANAGER", f"Nouvelle connaissance acquise et sauvegardée dans {theme_reel} ({nom_fichier}).")
except Exception as e:
log_event("ERROR", "RAG_MANAGER", f"Erreur lors de l'auto-apprentissage : {e}")
# On s'assure que les dossiers existent dès l'import du module
initialiser_arborescence_rag()
+20 -1
View File
@@ -158,7 +158,6 @@ def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"):
)
def render_admin_moderation_panel(utilisateur_cible):
"""Composant exclusif aux administrateurs pour statuer sur les infractions."""
return Div(
H3(f"⚖️ Tribunal d'administration - Dossier de : {utilisateur_cible}", style="color: #ff4444;"),
P("Loi applicable : Aucun utilisateur n'est dispensé du respect de la législation en vigueur."),
@@ -215,4 +214,24 @@ def render_login_page(error=None):
style="max-width: 400px; margin: 100px auto; padding: 30px; background: #1a1a1a; border-radius: 10px; border: 1px solid #333; box-shadow: 0 4px 15px rgba(0,229,255,0.1);"
),
style="background-color: #0d0d0d; color: #fff; font-family: monospace; height: 100vh; display: flex; align-items: center; justify-content: center;"
)
def render_popup_paiement(cout_estime, prompt_sauvegarde, chat_id, multi_ia_state=False):
multi_ia_val = "on" if multi_ia_state else "off"
return Div(
Div(
H3("⚠️ Validation Financière Requise", style="color: #ff9800; margin-top: 0;"),
P("Cette requête nécessite l'utilisation d'un modèle d'IA payant.", style="color: #ccc;"),
P(f"Coût estimé (TTC) : {cout_estime} $", style="font-size: 1.2em; font-weight: bold; color: #fff; background: #333; padding: 10px; border-radius: 5px; text-align: center;"),
Div(
Button("Annuler la requête", type="button", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #555; width: 30%; font-size: 0.8em;"),
Form(Input(type="hidden", name="prompt", value=prompt_sauvegarde), Input(type="hidden", name="chat_id", value=chat_id), Input(type="hidden", name="mode_payant", value="off"), Input(type="hidden", name="mode_multi_ia", value=multi_ia_val), Input(type="hidden", name="validation_acquise", value="true"), Input(type="hidden", name="choix_utilisateur", value="gratuit_force"), Button("Tenter en Gratuit ⚠️", type="submit", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #00e5ff; width: 100%; color: black; font-size: 0.8em;"), hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", style="width: 30%;"),
Form(Input(type="hidden", name="prompt", value=prompt_sauvegarde), Input(type="hidden", name="chat_id", value=chat_id), Input(type="hidden", name="mode_payant", value="on"), Input(type="hidden", name="mode_multi_ia", value=multi_ia_val), Input(type="hidden", name="validation_acquise", value="true"), Input(type="hidden", name="choix_utilisateur", value="paiement_accepte"), Button("Payer l'API", type="submit", onclick="document.getElementById('popup-paiement').remove();", cls="btn-submit", style="background-color: #4caf50; width: 100%; font-size: 0.8em;"), hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", style="width: 30%;"),
style="display: flex; justify-content: space-between; margin-top: 20px; gap: 10px;"
), style="background: #1a1a1a; padding: 25px; border-radius: 12px; border: 2px solid #ff9800; width: 500px; box-shadow: 0 10px 30px rgba(0,0,0,0.8);"
), id="popup-paiement", style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center; z-index: 9999;"
)