chore: Serveur 100% stable, ports corrigés, prêt pour le RAG
Build and Push Docker Image / build-and-push (push) Successful in 1m50s

This commit is contained in:
xavier committed 2026-08-20 15:24:28 +02:00
1 parent 7125842853
commit ffe820b00f
20 files changed
+7759 -185

No files matched your search

File diff suppressed because it is too large. Load diff
+3 -2
View File
@@ -41,6 +41,7 @@ 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.
- [ ] 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.
@@ -49,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 fournisseur alternatif pour palier aux décommissionnements constants des autres fournisseurs.*
*Note : L'infrastructure bascule sur un catalogue dynamique pour pallier les décommissionnements constants des fournisseurs tiers.*
| Domaine de tâche | Modèle / Moteur | Fournisseur / Hébergement | Rôle principal |
| :--- | :--- | :--- | :--- |
| **Orchestration & Tri** | Qwen 2.5 (3B) | Local (GTX 1650 / Ollama) | Analyse l'intention, gère le chat rapide et route vers la bonne IA |
| **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 --> |
| **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) |
+3
View File
@@ -0,0 +1,3 @@
{
"cagnotte_commune_vip": 35.0
}
+3468
View File
File diff suppressed because it is too large. Load diff
+8 -1
View File
@@ -54,7 +54,10 @@ async def post(request, session: dict):
return await handle_chat_request(request, session)
@rt('/favicon.ico')
def get(): return HTTPResponse(status_code=204)
def favicon_get(): return HTTPResponse(status_code=204)
@rt('/favicon.ico')
def favicon_head(): return HTTPResponse(status_code=204)
@rt('/new_chat')
def get(session): session['current_chat_id'] = str(uuid.uuid4()); return RedirectResponse('/', status_code=303)
@@ -85,5 +88,9 @@ def post(cid: str):
@rt('/export_json')
def get(): return FileResponse(HISTORY_FILE, media_type='application/json', filename='Aethas38_Discussion_Globale.json')
@rt('/ping')
def get():
return HTTPResponse(status_code=204)
if __name__ == '__main__':
serve(port=5001)
+40 -12
View File
@@ -1,7 +1,8 @@
import concurrent.futures
from modules.logger import log_event
from modules.ai_gemini import interroger_gemini
from modules.ai_local import interroger_local_ollama
from modules.ai_openrouter import interroger_openrouter
from modules.ai_openrouter import interroger_openrouter, interroger_openrouter_multi
SYSTEM_CONFIDENTIALITY_INSTRUCTION = (
"RÈGLE DE CONFIDENTIALITÉ ABSOLUE : Tu es un assistant intégré à la plateforme privée Aethas38. "
@@ -10,32 +11,59 @@ 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_requete_ia(fournisseur, prompt, mode_payant=False):
"""Moteur central gérant les consignes de sécurité et la cascade intelligente vers OpenRouter."""
def executer_synthese_multi_ia(prompt_securise: str, fournisseur: str, mode_payant: bool = False):
"""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:
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. "
"Tu es ENTIÈREMENT AUTORISÉ à générer du code Minecraft (KubeJS, Java, Datapacks), du code applicatif, ou des scripts."
)
prompt_synthese = f"{instruction_synthese}\n\nTu es un moteur de synthèse expert. Fusionne les réponses suivantes de manière claire, concise et sans répétitions :\n\n"
for item in reponses_brutes:
prompt_synthese += f"--- Modèle {item['modele']} ---\n{item['texte']}\n\n"
prompt_synthese += f"Demande initiale :\n{prompt_securise}"
texte, couleur, _, _ = interroger_gemini(prompt_synthese)
return texte, couleur, "Multi-IA (Synthèse Gemini)", "Pipeline Multi-Modèles"
except Exception as e:
log_event("WARNING", "MULTI_IA", f"Échec synthèse, repli : {e}")
return None
def executer_requete_ia(fournisseur: str, prompt: str, mode_payant: bool = False, multi_ia: bool = False):
"""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}"
# 🔄 Ordre de cascade recentré sur OpenRouter, Gemini et le Local
if multi_ia or fournisseur in ["openrouter_code", "openrouter_droit"]:
resultat_multi = executer_synthese_multi_ia(prompt_securise, fournisseur, mode_payant)
if resultat_multi:
return resultat_multi
if fournisseur == "gemini":
ordre_repli = [interroger_gemini, interroger_openrouter, interroger_local_ollama]
elif fournisseur == "openrouter_code":
ordre_repli = [interroger_openrouter, interroger_gemini, interroger_local_ollama]
elif fournisseur == "openrouter_chat":
elif fournisseur in ["openrouter_code", "openrouter_chat", "openrouter_droit"]:
ordre_repli = [interroger_openrouter, interroger_gemini, interroger_local_ollama]
else:
ordre_repli = [interroger_local_ollama, interroger_openrouter, interroger_gemini]
for fonction_api in ordre_repli:
try:
# Transfert du mode (Gratuit ou Payant) à OpenRouter
if fonction_api.__name__ == 'interroger_openrouter':
texte, couleur, nom_fournisseur, nom_modele = fonction_api(prompt_securise, type_modele=fournisseur, premium=mode_payant)
if fonction_api.__name__ == "interroger_openrouter":
texte, couleur, nom_fournisseur, nom_modele, _, _ = fonction_api(
prompt_securise, type_modele=fournisseur, premium=mode_payant
)
else:
texte, couleur, nom_fournisseur, nom_modele = fonction_api(prompt_securise)
if texte:
return texte, couleur, nom_fournisseur, nom_modele
except Exception as e:
log_event("WARNING", "ORCHESTRATEUR", f"Basculement suite à l'échec de {fonction_api.__name__}: {e}")
log_event("WARNING", "ORCHESTRATEUR", f"Basculement suite à l'échec de {fonction_api.__name__} : {e}")
continue
raise Exception("Tous les fournisseurs d'intelligence artificielle sont hors service ou indisponibles.")
raise Exception("Tous les fournisseurs d'IA sont indisponibles.")
+9 -5
View File
@@ -3,19 +3,23 @@ from google import genai
from modules.logger import log_event
def interroger_gemini(prompt_securise):
"""Interroge l'API Google Gemini via le nouveau SDK officiel (google-genai)."""
"""Interroge l'API Google Gemini via le SDK officiel (google-genai) sur les modèles actifs."""
api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
raise Exception("Clé API Gemini manquante dans l'environnement.")
try:
# Initialisation du client avec le nouveau SDK
client = genai.Client(api_key=api_key)
modeles = ["gemini-2.5-pro", "gemini-1.5-pro", "gemini-2.0-flash"]
# Liste actualisée avec les modèles opérationnels de la cascade
modeles = [
"gemini-3.6-flash",
"gemini-3.7-flash",
"gemini-3.1-pro-preview",
"gemini-2.5-flash"
]
for modele in modeles:
try:
# Appel avec configuration des instructions système
response = client.models.generate_content(
model=modele,
contents=prompt_securise,
@@ -24,7 +28,7 @@ def interroger_gemini(prompt_securise):
log_event("INFO", f"GEMINI-{modele}", "Succès de la requête Gemini.")
return response.text, "#4285F4", "Google Gemini", modele
except Exception as sub_e:
log_event("WARNING", f"GEMINI-{modele}", f"Essai échoué pour {modele}: {sub_e}")
log_event("WARNING", f"GEMINI-{modele}", f"Essai échoué pour {modele} : {sub_e}")
continue
raise Exception("Tous les modèles Gemini configurés ont échoué.")
+15 -6
View File
@@ -1,16 +1,25 @@
from openai import OpenAI
from modules.logger import log_event
def interroger_local_ollama(prompt_securise, modele="qwen2.5:3b"):
def interroger_local_ollama(prompt_securise: str, modele: str = "qwen2.5-coder:3b"):
"""
Interroge le serveur local Ollama avec repli automatique sur le modèle par défaut.
"""
modele_effectif = modele if modele else "qwen2.5-coder:3b"
client = OpenAI(base_url="http://192.168.1.20:11434/v1", api_key="ollama")
try:
resp = client.chat.completions.create(
model=modele,
messages=[{"role": "system", "content": "Réponds en français."}, {"role": "user", "content": prompt_securise}]
model=modele_effectif,
messages=[
{"role": "system", "content": "Réponds en français."},
{"role": "user", "content": prompt_securise}
]
)
if resp and resp.choices:
log_event("INFO", f"LOCAL-{modele}", "Succès.")
return resp.choices[0].message.content, "#10b981", "SERVEUR LOCAL", modele
log_event("INFO", f"LOCAL-{modele_effectif}", "Succès de la requête.")
return resp.choices[0].message.content, "#10b981", "SERVEUR LOCAL", modele_effectif
except Exception as e:
log_event("ERROR", f"LOCAL-{modele}", f"Erreur: {e}")
log_event("ERROR", f"LOCAL-{modele_effectif}", f"Erreur : {e}")
raise Exception("Le serveur local a échoué.")
+74 -51
View File
@@ -1,73 +1,96 @@
import os
import time
from dotenv import load_dotenv
import openai
from openai import OpenAI
from modules.logger import log_event
# Initialisation du client OpenRouter avec l'API compatible OpenAI
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=os.getenv("OPENROUTER_API_KEY", "VOTRE_CLE_ICI") # À définir dans le .env
api_key=api_key_openrouter
)
def interroger_openrouter(prompt, type_modele="openrouter_chat", premium=False, max_retries=3):
"""
Interroge l'API OpenRouter avec gestion des erreurs 402 (Plafond) et 429 (Rate Limit).
Bascule entre les modèles gratuits et premiums (VIP/Payant) selon le choix utilisateur.
"""
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"
}
}
# Sélection dynamique des modèles selon l'abonnement
if premium:
if type_modele == "openrouter_code":
nom_modele_api = "anthropic/claude-3.5-sonnet"
nom_affichage = "OpenRouter (Claude 3.5 Sonnet - Premium)"
couleur = "#d97757" # Orange Anthropic
else:
nom_modele_api = "openai/gpt-4o-mini"
nom_affichage = "OpenRouter (GPT-4o Mini - Premium)"
couleur = "#10a37f" # Vert OpenAI
else:
if type_modele == "openrouter_code":
nom_modele_api = "qwen/qwen-2.5-coder-32b-instruct:free"
nom_affichage = "OpenRouter (Qwen Coder - Gratuit)"
couleur = "#4a90e2" # Bleu clair
else:
nom_modele_api = "meta-llama/llama-3.1-8b-instruct:free"
nom_affichage = "OpenRouter (Llama 3.1 - Gratuit)"
couleur = "#4a90e2" # Bleu clair
def verifier_cle_api():
cle = os.getenv("OPENROUTER_API_KEY")
if not cle or cle == "sk-or-v1-cle-non-definie":
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):
verifier_cle_api()
gamme = "premium" if premium else "gratuit"
nom_modele_api = CATALOGUE_MODELES[gamme].get(type_modele, CATALOGUE_MODELES[gamme]["openrouter_chat"])
nom_affichage = f"OpenRouter ({nom_modele_api.split('/')[-1]})"
couleur = "#d97757" if premium else "#4a90e2"
for tentative in range(max_retries):
try:
start_time = time.time()
response = client.chat.completions.create(
model=nom_modele_api,
messages=[{"role": "user", "content": prompt}]
messages=[{"role": "user", "content": prompt}],
extra_headers={
"HTTP-Referer": "https://aethas38.duckdns.org",
"X-Title": "Aethas38 Multi-IA"
},
timeout=20.0
)
texte = response.choices[0].message.content
return texte, couleur, "OpenRouter", nom_affichage
except openai.RateLimitError as e:
temps_attente = 2 ** tentative
log_event("WARNING", "OPENROUTER", f"Limite de taux atteinte (429). Essai {tentative + 1}/{max_retries}. Attente de {temps_attente}s...")
time.sleep(temps_attente)
if tentative == max_retries - 1:
log_event("ERROR", "OPENROUTER", "Échec définitif suite aux limites de taux (429).")
raise Exception("Service temporairement saturé sur OpenRouter. Veuillez réessayer.")
except openai.APIStatusError as e:
if e.status_code == 402:
log_event("CRITICAL", "OPENROUTER", "Erreur 402 : Fonds insuffisants ou limite de clé atteinte.")
raise Exception("Action requise : Limite de facturation atteinte sur OpenRouter.")
else:
log_event("ERROR", "OPENROUTER", f"Code {e.status_code} : {e.message}")
raise Exception(f"Erreur API OpenRouter : {e.status_code}")
except openai.APIConnectionError as e:
log_event("ERROR", "OPENROUTER", f"Erreur réseau : {e}")
raise Exception("Impossible de joindre les serveurs d'OpenRouter.")
usage = response.usage
tokens_in = usage.prompt_tokens if usage else 0
tokens_out = usage.completion_tokens if usage else 0
return texte, couleur, "OpenRouter", nom_affichage, tokens_in, tokens_out
except Exception as e:
log_event("ERROR", "OPENROUTER", f"Erreur inattendue : {e}")
log_event("WARNING", "OPENROUTER", f"Tentative {tentative + 1} échouée pour {nom_modele_api} : {e}")
if tentative == max_retries - 1:
raise e
time.sleep(1)
return None, None, None, None
return None, None, None, None, 0, 0
def interroger_openrouter_multi(prompt: str, type_modele: str = "openrouter_code", premium: bool = False) -> list:
verifier_cle_api()
if premium:
modeles = ["anthropic/claude-3.5-sonnet", "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"]
reponses = []
for modele in modeles:
try:
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"
},
timeout=15.0
)
contenu = res.choices[0].message.content
if contenu:
reponses.append({"modele": modele, "texte": contenu})
except Exception as e:
log_event("WARNING", "OPENROUTER_MULTI", f"Échec rapide pour {modele} : {e}")
continue
return reponses
+144 -27
View File
@@ -1,52 +1,169 @@
import os
import json
import time
from datetime import datetime
import tiktoken
from modules.logger import log_event
# Tarification indicative par token (À synchroniser avec OpenRouter)
BALANCES_FILE = "aethas_balances.json"
AUDIT_FILE = "aethas_financial_audit.json"
TARIFS_OPENROUTER = {
"meta-llama/llama-3.1-8b-instruct:free": 0.0,
"qwen/qwen-2.5-coder-32b-instruct:free": 0.0,
"google/gemini-2.5-pro": 0.000005 # Exemple de modèle payant
"mistralai/mistral-small-24b-instruct-2501:free": 0.0,
"openai/gpt-4o-mini": 0.0000015,
"anthropic/claude-3.5-sonnet": 0.000015,
"mistralai/mistral-large-2411": 0.000006,
"google/gemini-2.5-pro": 0.000005
}
def compter_tokens(texte, modele="cl100k_base"):
"""Compte le nombre de tokens en local pour éviter les requêtes API inutiles."""
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"),
"utilisateur": utilisateur,
"rang": rang,
"type_operation": type_op,
"modele": modele,
"tokens": tokens,
"cout_brut_api": cout_brut,
"prix_facture_client": prix_facture
}
audits = []
if os.path.exists(AUDIT_FILE):
try:
with open(AUDIT_FILE, "r", encoding="utf-8") as f:
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))
except Exception as e:
log_event("ERROR", "BILLING", f"Erreur lors du comptage des tokens : {e}")
log_event("ERROR", "BILLING", f"Erreur comptage tokens : {e}")
return 0
def estimer_cout_facture(prompt, modele_id):
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 de la requête.
Applique la marge stricte de 15% pour générer le Prix Facture HT.
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_brut_api = tokens * TARIFS_OPENROUTER.get(modele_id, 0.0)
cout_unitaire = TARIFS_OPENROUTER.get(modele_id, 0.0)
cout_brut = tokens * cout_unitaire
if cout_brut_api == 0.0:
return 0.0
if multi_ia and cout_brut > 0:
cout_brut *= 2
# Application stricte de votre formule comptable
prix_facture_ht = cout_brut_api / 0.85
if cout_brut == 0.0:
return 0.0, 0.0, tokens
return round(prix_facture_ht, 6)
rang = rang_utilisateur.lower()
if rang in ["super admin", "admin", "vip"] or rang in VIP_ADMIN_USERS:
prix_facture = cout_brut
elif rang == "famille":
prix_facture = cout_brut / 0.925
else:
prix_facture = cout_brut / 0.85
def verifier_et_valider_budget(prompt, modele_id, utilisateur_vip=False):
"""
Fonction appelée avant chaque requête par le Chat Handler.
Si le coût > 0 et que l'utilisateur n'a pas explicitement validé, on stoppe tout.
"""
cout_estime = estimer_cout_facture(prompt, modele_id)
return round(cout_brut, 6), round(prix_facture, 6), tokens
# Les VIP ne paient pas de marge, on affiche juste le coût brut pour information
if utilisateur_vip:
log_event("INFO", "BILLING", f"Utilisateur VIP. Coût brut estimé de la requête : {cout_estime * 0.85} $")
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
if cout_estime > 0.0:
# Lève une exception spécifique que l'interface FastHTML devra attraper pour afficher le pop-up
raise Exception(f"VALIDATION_PAIEMENT_REQUISE|{cout_estime}")
soldes = charger_soldes()
utilisateur_cle = utilisateur_nom.lower()
return True, cout_estime
# 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"
else:
solde_disponible = soldes.get(utilisateur_cle, 0.0)
cle_compte = utilisateur_cle
if solde_disponible < prix_facture:
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)
enregistrer_transaction_audit(utilisateur_nom, rang_utilisateur, modele_id, tokens, cout_brut, prix_facture)
log_event("INFO", "BILLING", f"Requête validée [{rang_utilisateur}] - Débit : {prix_facture} $ - Solde restant : {soldes[cle_compte]} $")
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)
except Exception:
return []
dossier = []
for item in tous_les_audits:
if item.get("utilisateur", "").lower() == utilisateur_nom.lower():
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"),
"modele": item.get("modele"),
"tokens": item.get("tokens"),
"montant_facture_ttc": item.get("prix_facture_client")
})
return dossier
+44 -45
View File
@@ -10,8 +10,7 @@ from modules.ai_engine import executer_requete_ia
from modules.ui_components import render_user_msg, render_ai_msg, render_sidebar
from modules.logger import log_event
from modules.billing_manager import verifier_et_valider_budget
# Import du nouveau module de modération
from modules.rag_manager import rechercher_contexte_rag
from modules.moderator import analyser_intention_illicite, appliquer_sanction_et_archiver
async def handle_chat_request(request, session):
@@ -22,87 +21,86 @@ async def handle_chat_request(request, session):
chat_id = form.get("chat_id", str(uuid.uuid4()))
files = form.getlist("files")
# --- RÉCUPÉRATION DU CHOIX GRATUIT/PAYANT ---
mode_payant = form.get("mode_payant") == "on"
mode_multi_ia = form.get("mode_multi_ia") == "on"
session['current_chat_id'] = chat_id
session["current_chat_id"] = chat_id
history = load_history()
# Identification
utilisateur_nom = session.get('user', 'xavier')
utilisateur_vip = utilisateur_nom.lower() in ["xavier", "céline", "anaëlle", "renan", "audric"]
# Récupération IP depuis la requête Starlette/FastHTML
utilisateur_nom = session.get("user", "xavier")
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"
# --- 🚨 ÉTAPE 1 : MODÉRATION PRÉVENTIVE (Filtre Légal) 🚨 ---
est_illicite, raison = analyser_intention_illicite(prompt, None)
# Modération préventive locale
est_illicite, raison = analyser_intention_illicite(prompt, "qwen2.5-coder:3b")
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 Générales d'Utilisation (Contenu illicite)."),
P(f"Sanction appliquée : {sanction}"),
P("Cet incident a été enregistré et transmis à l'administration."),
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;"
)
# Extraction éphémère en RAM
file_summary = []
file_names = []
for f in files[:10]:
if f and f.filename:
file_names.append(f.filename)
content = await f.read()
file_summary.append(f"\n--- Fichier: {f.filename} ---\n{content.decode('utf-8', errors='ignore')}")
file_summary.append(f"\n--- Fichier : {f.filename} ---\n{content.decode('utf-8', errors='ignore')}")
liste_fichiers_str = ', '.join(file_names) if file_names else 'Aucun fichier'
liste_fichiers_str = ", ".join(file_names) if file_names else "Aucun fichier"
log_mode = "PAYANT 👑" if mode_payant else "GRATUIT 🆓"
log_event("INFO", "SYSTEM", f"Requête reçue [{log_mode}]. Fichiers joints : {liste_fichiers_str}")
# L'IA Local (Qwen) orchestre la décision
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 ou recherches web.
- "openrouter_chat" : requêtes générales, texte rapide, traduction, bureautique.
- "openrouter_code" : programmation, scripts, mathématiques, logique.
- "local" : salutations basiques, questions très simples.
Renvoie STRICTEMENT un JSON : {{"intention": "Résumé...", "fournisseur": "gemini|openrouter_chat|openrouter_code|local"}}
- "gemini" : (OBLIGATOIRE si fichiers joints), rédaction complexe, comparaisons.
- "openrouter_chat" : requêtes générales, texte rapide, traduction.
- "openrouter_code" : programmation, Minecraft/KubeJS, scripts, mathématiques.
- "openrouter_droit" : droit français, CGV, CGU, RGPD.
- "local" : salutations basiques, questions simples.
Renvoie STRICTEMENT un JSON : {{"intention": "Résumé...", "fournisseur": "gemini|openrouter_chat|openrouter_code|openrouter_droit|local"}}
Requête : {prompt}"""
try:
texte_routeur, _, _, _ = interroger_local_ollama(sec_prompt, "qwen2.5:3b")
texte_routeur, _, _, _ = interroger_local_ollama(sec_prompt, "qwen2.5-coder:3b")
decision = nettoyer_decision_router(texte_routeur)
except Exception:
decision = {"intention": "Analyse de secours", "fournisseur": "gemini"}
decision = {"intention": "Analyse de secours", "fournisseur": "openrouter_code" if "minecraft" in prompt.lower() else "openrouter_chat"}
intention = decision.get('intention', 'Analyse')
fournisseur = decision.get('fournisseur', 'openrouter_chat').lower()
intention = decision.get("intention", "Analyse")
fournisseur = decision.get("fournisseur", "openrouter_chat").lower()
if file_names:
fournisseur = "gemini"
intention += " (Forcé Gemini via Sécurité Fichiers)"
urls = re.findall(r'https?://[^\s]+', prompt)
mots_cles = ["cherche", "recherche", "sur internet", "sur le web", "sur le net", "va sur", "trouve", "site", "donne", "historique", "version"]
thematique_rag = "minecraft" if "mine" in prompt.lower() or "kubejs" in prompt.lower() else ("droit" if "droit" in fournisseur else None)
contexte_rag = rechercher_contexte_rag(prompt, thematique=thematique_rag)
urls = re.findall(r"https?://[^\s]+", prompt)
mots_cles = ["cherche", "recherche", "sur internet", "sur le web", "sur le net", "trouve", "site"]
if any(m in prompt.lower() for m in mots_cles):
urls.extend(rechercher_sur_google(prompt))
web_text = extraire_contenu_urls(list(set(urls))) if urls else ""
full_prompt = prompt + ("\n" + "\n".join(file_summary) if file_summary else "")
if contexte_rag:
full_prompt += f"\n\n[Contexte Documentaire RAG :\n{contexte_rag}]"
if web_text:
full_prompt += f"\n\n[Contexte web extrait :\n{web_text}\nUtilise ces informations pour répondre.]"
full_prompt += f"\n\n[Contexte web :\n{web_text}]"
# --- VÉRIFICATION FINANCIÈRE PRÉALABLE ---
# Validation financière
if "openrouter" in fournisseur:
if mode_payant:
modele_estime = "anthropic/claude-3.5-sonnet" if fournisseur == "openrouter_code" else "openai/gpt-4o-mini"
else:
modele_estime = "qwen/qwen-2.5-coder-32b-instruct:free" if fournisseur == "openrouter_code" else "meta-llama/llama-3.1-8b-instruct:free"
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)
verifier_et_valider_budget(full_prompt, modele_estime, utilisateur_vip)
# Exécution
reponse, couleur, fournisseur_nom, modele = executer_requete_ia(fournisseur, full_prompt, mode_payant)
# Exécution rapide
reponse, couleur, fournisseur_nom, modele = executer_requete_ia(
fournisseur, full_prompt, mode_payant=mode_payant, multi_ia=mode_multi_ia
)
if chat_id not in history:
history[chat_id] = {"title": (prompt[:30] if prompt else "Discussion") + "...", "pinned": False, "timestamp": time.time(), "messages": []}
@@ -112,18 +110,19 @@ async def handle_chat_request(request, session):
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 et détruits instantanément après analyse.*"
display_msg += "\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": "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
})
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:
log_event("CRITICAL", "SYSTEM", f"Erreur critique dans handle_chat_request: {e}")
msg_erreur = str(e)
if "VALIDATION_PAIEMENT_REQUISE" in msg_erreur or "SOLDE_INSUFFISANT" in msg_erreur:
return Div(f"⚠️ Alerte Financière : {msg_erreur.split('|')[0]}", style="color:orange; background:#331a00; padding:15px; border-radius:10px;")
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;")
+26 -17
View File
@@ -16,10 +16,13 @@ BASE_DATA_DIR = "/DATA/AppData/MULTI-IA-AETHAS38/Utilisateurs"
# Liste des Super Admins / Admins à prévenir
ADMINS_GLOBAUX = ["xavier", "céline"]
def analyser_intention_illicite(prompt, ai_orchestrateur="qwen2.5:3b"):
def analyser_intention_illicite(prompt: str, ai_orchestrateur: str = "qwen2.5-coder:3b") -> tuple[bool, str]:
"""
Fait appel à l'IA locale pour analyser l'intention sans faire de zèle.
Sécurisé contre les valeurs None passées en paramètre.
"""
modele_cible = ai_orchestrateur if ai_orchestrateur else "qwen2.5-coder:3b"
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.
@@ -27,8 +30,8 @@ Renvoie STRICTEMENT un JSON valide : {{"illegal": true ou false, "raison": "Expl
Requête à analyser : {prompt}"""
try:
# On interroge Qwen en local
reponse, _, _, _ = interroger_local_ollama(prompt_juge, ai_orchestrateur)
# On interroge le modèle local sécurisé
reponse, _, _, _ = interroger_local_ollama(prompt_juge, modele_cible)
# Extraction du JSON de la réponse
match = re.search(r'\{.*\}', reponse, re.DOTALL)
@@ -39,15 +42,15 @@ Requête à analyser : {prompt}"""
except Exception as e:
log_event("WARNING", "MODERATOR", f"Échec de l'analyse IA (Fallback activé) : {e}")
# Fallback classique si Qwen plante
# Fallback classique par mots-clés si l'IA locale est indisponible
mots_interdits = ["pédopornographie", "bombe", "terrorisme"]
if any(mot in prompt.lower() for mot in mots_interdits):
return True, "Détection par filtre de mots-clés (Secours)"
return False, ""
def appliquer_sanction_et_archiver(utilisateur_nom, prompt, raison, ip_client="Inconnue"):
def appliquer_sanction_et_archiver(utilisateur_nom: str, prompt: str, raison: str, ip_client: str = "Inconnue") -> str:
"""
Gère les Strikes, archive la preuve sur le RAID 1 et alerte les AUTRES admins.
Gère les avertissements, archive la preuve sur le RAID 1 et alerte les administrateurs.
"""
user_dir = os.path.join(BASE_DATA_DIR, utilisateur_nom)
illegal_dir = os.path.join(user_dir, "Illegal")
@@ -57,36 +60,42 @@ def appliquer_sanction_et_archiver(utilisateur_nom, prompt, raison, ip_client="I
strikes = 0
if os.path.exists(dossier_sanctions):
try:
with open(dossier_sanctions, "r", encoding="utf-8") as f:
data = json.load(f)
strikes = data.get("avertissements", 0)
except Exception:
strikes = 0
strikes += 1
sanction_actuelle = "Avertissement"
if strikes >= 10: sanction_actuelle = "BAN DEFINITIF EN ATTENTE DE VALIDATION"
elif strikes >= 9: sanction_actuelle = "Ban 1 mois"
elif strikes >= 6: sanction_actuelle = "Ban 1 semaine"
elif strikes >= 3: sanction_actuelle = "Ban 1 jour"
if strikes >= 10:
sanction_actuelle = "BAN DEFINITIF EN ATTENTE DE VALIDATION"
elif strikes >= 9:
sanction_actuelle = "Ban 1 mois"
elif strikes >= 6:
sanction_actuelle = "Ban 1 semaine"
elif strikes >= 3:
sanction_actuelle = "Ban 1 jour"
with open(dossier_sanctions, "w", encoding="utf-8") as f:
json.dump({"avertissements": strikes, "derniere_sanction": sanction_actuelle, "date": str(datetime.now())}, f)
json.dump({"avertissements": strikes, "derniere_sanction": sanction_actuelle, "date": str(datetime.now())}, f, indent=4)
preuve_path = os.path.join(illegal_dir, f"infraction_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt")
with open(preuve_path, "w", encoding="utf-8") as f:
f.write(f"--- INFRACTION REPORTÉE ---\nUtilisateur: {utilisateur_nom}\nIP: {ip_client}\nDate: {datetime.now()}\nRaison: {raison}\n\n[PROMPT INTERCEPTÉ]\n{prompt}")
f.write(f"--- INFRACTION REPORTÉE ---\nUtilisateur : {utilisateur_nom}\nIP : {ip_client}\nDate : {datetime.now()}\nRaison : {raison}\n\n[PROMPT INTERCEPTÉ]\n{prompt}")
# LOGIQUE D'ALERTE : On exclut l'utilisateur s'il est lui-même admin
# Exclusion de l'admin s'il est lui-même l'auteur de l'infraction
admins_a_alerter = [admin for admin in ADMINS_GLOBAUX if admin.lower() != utilisateur_nom.lower()]
alerte_msg = f"Violation détectée pour {utilisateur_nom} (Avertissement n°{strikes}).\nSanction: {sanction_actuelle}\nPreuve stockée: {preuve_path}"
alerte_msg = f"Violation détectée pour {utilisateur_nom} (Avertissement n°{strikes}).\nSanction : {sanction_actuelle}\nPreuve stockée : {preuve_path}"
log_event("CRITICAL", "MODERATION", alerte_msg)
if envoyer_alerte_debug and admins_a_alerter:
# Envoi de l'email uniquement si la liste d'admins à alerter n'est pas vide
envoyer_alerte_debug("CRITICAL", f"TENTATIVE ILLICITE ({utilisateur_nom})", alerte_msg)
return sanction_actuelle
def sauvegarder_decision_admin(utilisateur_cible, admin_juge, decision, commentaire):
def sauvegarder_decision_admin(utilisateur_cible: str, admin_juge: str, decision: str, commentaire: str):
"""
Archive la décision prise par un admin avec commentaire libre.
"""
@@ -95,5 +104,5 @@ def sauvegarder_decision_admin(utilisateur_cible, admin_juge, decision, commenta
fichier = os.path.join(dossier, f"decision_admin_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt")
with open(fichier, "w", encoding="utf-8") as f:
f.write(f"--- DÉCISION ADMINISTRATIVE ---\nCible: {utilisateur_cible}\nJuge Admin: {admin_juge}\nDate: {datetime.now()}\nDécision: {decision}\nCommentaire: {commentaire}\n")
f.write(f"--- DÉCISION ADMINISTRATIVE ---\nCible : {utilisateur_cible}\nJuge Admin : {admin_juge}\nDate : {datetime.now()}\nDécision : {decision}\nCommentaire : {commentaire}\n")
log_event("INFO", "MODERATION", f"L'admin {admin_juge} a statué sur {utilisateur_cible} : {decision}")
+77
View File
@@ -0,0 +1,77 @@
import os
import json
import time
import requests
from modules.logger import log_event
CACHE_MODELS_FILE = "openrouter_models_cache.json"
CACHE_TTL_SECONDES = 86400 # Rafraîchissement automatique toutes les 24 heures
def synchroniser_catalogue_openrouter(forcer: bool = False) -> dict:
"""
Interroge l'API OpenRouter pour récupérer en direct tous les modèles,
leurs spécialités et leurs tarifs exacts (Prompt & Completion).
Met en cache le résultat localement.
"""
# Vérification du cache local
if not forcer and os.path.exists(CACHE_MODELS_FILE):
try:
with open(CACHE_MODELS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
if time.time() - data.get("derniere_synchro", 0) < CACHE_TTL_SECONDES:
return data.get("modeles", {})
except Exception as e:
log_event("WARNING", "OPENROUTER_SYNC", f"Erreur lecture cache modèles : {e}")
url = "https://openrouter.ai/api/v1/models"
headers = {
"Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY', '')}",
"HTTP-Referer": "https://aethas38.duckdns.org",
"X-Title": "Aethas38 Multi-IA"
}
try:
reponse = requests.get(url, headers=headers, timeout=10)
reponse.raise_for_status()
catalogue_brut = reponse.json().get("data", [])
catalogue_traite = {}
for item in catalogue_brut:
modele_id = item.get("id")
pricing = item.get("pricing", {})
# Conversion des prix en float (prix par token unitaire)
prix_prompt = float(pricing.get("prompt", 0.0) or 0.0)
prix_completion = float(pricing.get("completion", 0.0) or 0.0)
catalogue_traite[modele_id] = {
"nom": item.get("name", modele_id),
"contexte_max": item.get("context_length", 4096),
"est_gratuit": prix_prompt == 0.0 and prix_completion == 0.0,
"prix_token_prompt": prix_prompt,
"prix_token_completion": prix_completion
}
# Sauvegarde en cache
with open(CACHE_MODELS_FILE, "w", encoding="utf-8") as f:
json.dump({
"derniere_synchro": time.time(),
"total_modeles": len(catalogue_traite),
"modeles": catalogue_traite
}, f, indent=4)
log_event("INFO", "OPENROUTER_SYNC", f"Catalogue OpenRouter synchronisé : {len(catalogue_traite)} modèles chargés.")
return catalogue_traite
except Exception as e:
log_event("ERROR", "OPENROUTER_SYNC", f"Échec de synchronisation OpenRouter : {e}")
# En cas d'échec réseau, on tente de renvoyer le vieux cache existant
if os.path.exists(CACHE_MODELS_FILE):
with open(CACHE_MODELS_FILE, "r", encoding="utf-8") as f:
return json.load(f).get("modeles", {})
return {}
def obtenir_modeles_gratuits() -> list:
"""Renvoie uniquement la liste des slugs des modèles 100 % gratuits."""
catalogue = synchroniser_catalogue_openrouter()
return [slug for slug, info in catalogue.items() if info.get("est_gratuit", False)]
+63
View File
@@ -0,0 +1,63 @@
import os
import re
from modules.logger import log_event
RAG_BASE_DIR = "rag_data"
THEMATIQUES = ["minecraft", "droit", "serveur", "general"]
def initialiser_structure_rag():
"""Crée l'arborescence des dossiers RAG s'ils n'existent pas."""
os.makedirs(RAG_BASE_DIR, exist_ok=True)
for theme in THEMATIQUES:
os.makedirs(os.path.join(RAG_BASE_DIR, theme), exist_ok=True)
def indexer_document(thematique: str, nom_fichier: str, contenu: str):
"""Enregistre un document dans la thématique correspondante."""
initialiser_structure_rag()
if thematique not in THEMATIQUES:
thematique = "general"
chemin = os.path.join(RAG_BASE_DIR, thematique, nom_fichier)
try:
with open(chemin, "w", encoding="utf-8") as f:
f.write(contenu)
log_event("INFO", "RAG", f"Document {nom_fichier} indexé dans {thematique}.")
return True
except Exception as e:
log_event("ERROR", "RAG", f"Échec d'indexation pour {nom_fichier} : {e}")
return False
def rechercher_contexte_rag(requete: str, thematique: str = None, top_k: int = 3) -> str:
"""
Recherche les passages les plus pertinents dans la base documentaire.
Filtre par thématique si spécifiée, sinon cherche globalement.
"""
initialiser_structure_rag()
dossiers_cibles = [os.path.join(RAG_BASE_DIR, thematique)] if thematique in THEMATIQUES else [os.path.join(RAG_BASE_DIR, t) for t in THEMATIQUES]
mots_cles = set(re.findall(r'\w+', requete.lower()))
resultats = []
for dossier in dossiers_cibles:
if not os.path.exists(dossier):
continue
for fichier in os.listdir(dossier):
chemin = os.path.join(dossier, fichier)
if os.path.isfile(chemin):
try:
with open(chemin, "r", encoding="utf-8", errors="ignore") as f:
contenu = f.read()
paragraphes = contenu.split("\n\n")
for p in paragraphes:
texte_p = p.strip()
if not texte_p:
continue
mots_p = set(re.findall(r'\w+', texte_p.lower()))
score = len(mots_cles.intersection(mots_p))
if score > 0:
resultats.append((score, fichier, texte_p))
except Exception as e:
log_event("WARNING", "RAG", f"Erreur lecture {chemin} : {e}")
resultats.sort(key=lambda x: x[0], reverse=True)
extraits = [f"[{res[1]}] {res[2]}" for res in resultats[:top_k]]
return "\n---\n".join(extraits)
+42 -19
View File
@@ -1,6 +1,7 @@
from fasthtml.common import *
from modules.database import load_history
from modules.paypal import render_paypal_donation_widget
from modules.billing_manager import obtenir_solde
def render_user_msg(text):
return Div(P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"), cls="user-bubble")
@@ -21,7 +22,7 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, executi
Button("📝 TXT", onclick="downloadResponseTXT(this)", cls="msg-action-btn", title="Télécharger en Texte"),
Button("🗄️ JSON", onclick="downloadResponseJSON(this)", cls="msg-action-btn", title="Télécharger en JSON"),
Button("💬 WhatsApp", onclick="shareWhatsApp(this)", cls="msg-action-btn", title="Partager sur WhatsApp"),
Button("📧 Email", onclick="shareEmail(this)", cls="msg-action-btn", title="Envoyer par Email"),
Button("📧 Email", onclick="shareEmail(this)", cls="msg-action-btn", title="Envoyer par courriel"),
cls="msg-actions"
)
return Div(
@@ -49,7 +50,7 @@ def render_sidebar():
)
return Div(*items, id="sidebar-content", cls="sidebar-history", hx_swap_oob="true")
def render_main_page(chat_id):
def render_main_page(chat_id, utilisateur_nom="xavier", role="Super Admin"):
history = load_history()
messages_ui = []
if chat_id in history:
@@ -57,16 +58,19 @@ def render_main_page(chat_id):
if msg['role'] == 'user':
messages_ui.append(render_user_msg(msg['content']))
else:
messages_ui.append(render_ai_msg(msg['content'], msg.get('intention', ''), msg.get('fournisseur', ''), msg.get('modele', ''), msg.get('couleur', '#333'), msg.get('execution_time', '')))
messages_ui.append(render_ai_msg(
msg['content'], msg.get('intention', ''), msg.get('fournisseur', ''),
msg.get('modele', ''), msg.get('couleur', '#333'), msg.get('execution_time', '')
))
if messages_ui:
messages_ui.append(Script("setTimeout(renderMarkdown, 150);"))
auto_refresh_script = Script('''
let serverDown = false;
setInterval(() => {
fetch('/favicon.ico', {cache: 'no-store', method: 'HEAD'})
fetch('/ping', {cache: 'no-store'})
.then(response => {
if (serverDown) { window.location.reload(); }
if (serverDown && response.ok) { window.location.reload(); }
})
.catch(() => {
serverDown = true;
@@ -75,6 +79,22 @@ def render_main_page(chat_id):
}, 5000);
''')
solde_perso = obtenir_solde(utilisateur_nom)
if role.lower() in ["super admin", "admin"]:
badge_finance = Div(
P(f"👤 {utilisateur_nom.capitalize()} ({role})", style="color: #00e5ff; margin: 0; font-weight: bold; font-size: 0.9em;"),
P(f"💳 Solde personnel : {solde_perso:.2f} $", style="color: #aaa; margin: 2px 0; font-size: 0.8em;"),
P(f"🏦 Cagnotte globale : {obtenir_solde('xavier'):.2f} $", style="color: #4caf50; margin: 0; font-size: 0.8em; font-weight: bold;"),
style="padding: 10px; background: #181818; border-radius: 6px; border: 1px solid #333;"
)
else:
badge_finance = Div(
P(f"👤 {utilisateur_nom.capitalize()} ({role})", style="color: #fff; margin: 0; font-weight: bold; font-size: 0.9em;"),
P(f"💳 Solde disponible : {solde_perso:.2f} $", style="color: #00e5ff; margin: 2px 0 0; font-size: 0.85em;"),
style="padding: 10px; background: #181818; border-radius: 6px; border: 1px solid #333;"
)
return Title("Aethas38 - Core"), Body(
auto_refresh_script,
Div(
@@ -95,7 +115,7 @@ def render_main_page(chat_id):
cls="sidebar-top"
),
render_sidebar(),
Div(P("👤 Xavier (Super Admin)", style="color: #aaa; margin: 0; font-size: 0.9em;"), cls="sidebar-footer"),
Div(badge_finance, cls="sidebar-footer"),
cls="sidebar"
),
Div(
@@ -112,7 +132,12 @@ def render_main_page(chat_id):
Label(
Input(type="checkbox", name="mode_payant", id="mode-payant-toggle", style="margin-right: 6px; accent-color: #00e5ff; cursor: pointer;"),
"👑 VIP/Payant",
"👑 Payant",
style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; margin-right: 10px; cursor: pointer; white-space: nowrap;"
),
Label(
Input(type="checkbox", name="mode_multi_ia", id="mode-multi-toggle", style="margin-right: 6px; accent-color: #d97757; cursor: pointer;"),
"⚡ Multi-IA",
style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; margin-right: 15px; cursor: pointer; white-space: nowrap;"
),
@@ -133,34 +158,32 @@ def render_main_page(chat_id):
)
def render_admin_moderation_panel(utilisateur_cible):
"""
Composant UI exclusif aux Admins pour statuer sur les infractions avec commentaire libre.
"""
"""Composant exclusif aux administrateurs pour statuer sur les infractions."""
return Div(
H3(f"⚖️ Tribunal Admin - Dossier de : {utilisateur_cible}", style="color: #ff4444;"),
P("Loi applicable : Aucun utilisateur n'est dispensé du respect de la loi."),
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."),
Form(
Input(type="hidden", name="cible", value=utilisateur_cible),
Div(
Label("Action décidée :", style="font-weight: bold;"),
Select(
Option("Avertissement simple", value="Avertissement"),
Option("Bannissement 1 Jour", value="Ban 1J"),
Option("Bannissement 1 Semaine", value="Ban 1Sem"),
Option("Bannissement 1 Mois", value="Ban 1M"),
Option("Bannissement Définitif & Signalement PHAROS", value="Ban Definitif + Pharos"),
Option("Bannissement 1 jour", value="Ban 1J"),
Option("Bannissement 1 semaine", value="Ban 1Sem"),
Option("Bannissement 1 mois", value="Ban 1M"),
Option("Bannissement définitif et signalement PHAROS", value="Ban Definitif + Pharos"),
Option("Faux positif (Levée de sanction)", value="Annulation"),
name="decision", cls="input-field"
),
style="margin-bottom: 15px;"
),
Div(
Label("Commentaire libre de l'Admin (Preuve d'audit) :", style="font-weight: bold;"),
Textarea(name="commentaire", rows="4", cls="input-field", placeholder="Raison de la décision, contexte..."),
Label("Commentaire libre de l'administrateur (Preuve d'audit) :", style="font-weight: bold;"),
Textarea(name="commentaire", rows="4", cls="input-field", placeholder="Raison de la décision, contexte juridique..."),
style="margin-bottom: 15px;"
),
Button("Valider la décision et archiver", type="submit", cls="btn-submit", style="background-color: #ff4444; color: white;"),
hx_post="/admin_decision", # Nouvelle route backend à créer plus tard
hx_post="/admin_decision",
hx_target="#admin-result",
style="background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333;"
),
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.