Public Access
chore: Serveur 100% stable, ports corrigés, prêt pour le RAG
Build and Push Docker Image / build-and-push (push) Successful in 1m50s
Build and Push Docker Image / build-and-push (push) Successful in 1m50s
This commit is contained in:
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
@@ -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) |
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"cagnotte_commune_vip": 35.0
|
||||
}
|
||||
+3468
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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.")
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}")
|
||||
@@ -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)]
|
||||
@@ -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
@@ -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.
Reference in new issue
Block a user