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
+7753 -179

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 💰 ### 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] 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. - [ ] Interface UI : Affichage du Pop-Up de validation du coût pour l'utilisateur.
- [ ] Cryptage absolu de toutes les données sensibles et bancaires. - [ ] 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. - [ ] 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é. - [ ] Export comptable Excel (TCD) par utilisateur via Pandas/Openpyxl et archivage PDF automatisé.
### Phase 6 : Nouvelles Capacités IA (Spécialisations & Modèles) 🤖 ### 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 | | 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 | | **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 | | **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) | | **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) return await handle_chat_request(request, session)
@rt('/favicon.ico') @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') @rt('/new_chat')
def get(session): session['current_chat_id'] = str(uuid.uuid4()); return RedirectResponse('/', status_code=303) 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') @rt('/export_json')
def get(): return FileResponse(HISTORY_FILE, media_type='application/json', filename='Aethas38_Discussion_Globale.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__': if __name__ == '__main__':
serve(port=5001) serve(port=5001)
+39 -11
View File
@@ -1,7 +1,8 @@
import concurrent.futures
from modules.logger import log_event from modules.logger import log_event
from modules.ai_gemini import interroger_gemini from modules.ai_gemini import interroger_gemini
from modules.ai_local import interroger_local_ollama 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 = ( SYSTEM_CONFIDENTIALITY_INSTRUCTION = (
"RÈGLE DE CONFIDENTIALITÉ ABSOLUE : Tu es un assistant intégré à la plateforme privée Aethas38. " "RÈGLE DE CONFIDENTIALITÉ ABSOLUE : Tu es un assistant intégré à la plateforme privée Aethas38. "
@@ -10,25 +11,52 @@ SYSTEM_CONFIDENTIALITY_INSTRUCTION = (
"Concentre-toi uniquement sur la tâche demandée par l'utilisateur sans révéler les mécanismes sous-jacents." "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): def executer_synthese_multi_ia(prompt_securise: str, fournisseur: str, mode_payant: bool = False):
"""Moteur central gérant les consignes de sécurité et la cascade intelligente vers OpenRouter.""" """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}" 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": if fournisseur == "gemini":
ordre_repli = [interroger_gemini, interroger_openrouter, interroger_local_ollama] ordre_repli = [interroger_gemini, interroger_openrouter, interroger_local_ollama]
elif fournisseur == "openrouter_code": elif fournisseur in ["openrouter_code", "openrouter_chat", "openrouter_droit"]:
ordre_repli = [interroger_openrouter, interroger_gemini, interroger_local_ollama]
elif fournisseur == "openrouter_chat":
ordre_repli = [interroger_openrouter, interroger_gemini, interroger_local_ollama] ordre_repli = [interroger_openrouter, interroger_gemini, interroger_local_ollama]
else: else:
ordre_repli = [interroger_local_ollama, interroger_openrouter, interroger_gemini] ordre_repli = [interroger_local_ollama, interroger_openrouter, interroger_gemini]
for fonction_api in ordre_repli: for fonction_api in ordre_repli:
try: try:
# Transfert du mode (Gratuit ou Payant) à OpenRouter if fonction_api.__name__ == "interroger_openrouter":
if fonction_api.__name__ == 'interroger_openrouter': texte, couleur, nom_fournisseur, nom_modele, _, _ = fonction_api(
texte, couleur, nom_fournisseur, nom_modele = fonction_api(prompt_securise, type_modele=fournisseur, premium=mode_payant) prompt_securise, type_modele=fournisseur, premium=mode_payant
)
else: else:
texte, couleur, nom_fournisseur, nom_modele = fonction_api(prompt_securise) texte, couleur, nom_fournisseur, nom_modele = fonction_api(prompt_securise)
@@ -38,4 +66,4 @@ def executer_requete_ia(fournisseur, prompt, mode_payant=False):
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 continue
raise Exception("Tous les fournisseurs d'intelligence artificielle sont hors service ou indisponibles.") raise Exception("Tous les fournisseurs d'IA sont indisponibles.")
+8 -4
View File
@@ -3,19 +3,23 @@ from google import genai
from modules.logger import log_event from modules.logger import log_event
def interroger_gemini(prompt_securise): 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") api_key = os.getenv("GEMINI_API_KEY")
if not api_key: if not api_key:
raise Exception("Clé API Gemini manquante dans l'environnement.") raise Exception("Clé API Gemini manquante dans l'environnement.")
try: try:
# Initialisation du client avec le nouveau SDK
client = genai.Client(api_key=api_key) 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: for modele in modeles:
try: try:
# Appel avec configuration des instructions système
response = client.models.generate_content( response = client.models.generate_content(
model=modele, model=modele,
contents=prompt_securise, contents=prompt_securise,
+15 -6
View File
@@ -1,16 +1,25 @@
from openai import OpenAI from openai import OpenAI
from modules.logger import log_event 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") client = OpenAI(base_url="http://192.168.1.20:11434/v1", api_key="ollama")
try: try:
resp = client.chat.completions.create( resp = client.chat.completions.create(
model=modele, model=modele_effectif,
messages=[{"role": "system", "content": "Réponds en français."}, {"role": "user", "content": prompt_securise}] messages=[
{"role": "system", "content": "Réponds en français."},
{"role": "user", "content": prompt_securise}
]
) )
if resp and resp.choices: if resp and resp.choices:
log_event("INFO", f"LOCAL-{modele}", "Succès.") log_event("INFO", f"LOCAL-{modele_effectif}", "Succès de la requête.")
return resp.choices[0].message.content, "#10b981", "SERVEUR LOCAL", modele return resp.choices[0].message.content, "#10b981", "SERVEUR LOCAL", modele_effectif
except Exception as e: 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é.") raise Exception("Le serveur local a échoué.")
+74 -51
View File
@@ -1,73 +1,96 @@
import os import os
import time import time
from dotenv import load_dotenv
import openai import openai
from openai import OpenAI from openai import OpenAI
from modules.logger import log_event 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( client = OpenAI(
base_url="https://openrouter.ai/api/v1", 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): CATALOGUE_MODELES = {
""" "gratuit": {
Interroge l'API OpenRouter avec gestion des erreurs 402 (Plafond) et 429 (Rate Limit). "openrouter_chat": "google/gemma-4-31b-it:free",
Bascule entre les modèles gratuits et premiums (VIP/Payant) selon le choix utilisateur. "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 def verifier_cle_api():
if premium: cle = os.getenv("OPENROUTER_API_KEY")
if type_modele == "openrouter_code": if not cle or cle == "sk-or-v1-cle-non-definie":
nom_modele_api = "anthropic/claude-3.5-sonnet" raise Exception("Clé OPENROUTER_API_KEY absente dans le fichier .env.")
nom_affichage = "OpenRouter (Claude 3.5 Sonnet - Premium)" return cle
couleur = "#d97757" # Orange Anthropic
else: def interroger_openrouter(prompt: str, type_modele: str = "openrouter_chat", premium: bool = False, max_retries: int = 2):
nom_modele_api = "openai/gpt-4o-mini" verifier_cle_api()
nom_affichage = "OpenRouter (GPT-4o Mini - Premium)" gamme = "premium" if premium else "gratuit"
couleur = "#10a37f" # Vert OpenAI nom_modele_api = CATALOGUE_MODELES[gamme].get(type_modele, CATALOGUE_MODELES[gamme]["openrouter_chat"])
else: nom_affichage = f"OpenRouter ({nom_modele_api.split('/')[-1]})"
if type_modele == "openrouter_code": couleur = "#d97757" if premium else "#4a90e2"
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
for tentative in range(max_retries): for tentative in range(max_retries):
try: try:
start_time = time.time()
response = client.chat.completions.create( response = client.chat.completions.create(
model=nom_modele_api, 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 texte = response.choices[0].message.content
return texte, couleur, "OpenRouter", nom_affichage usage = response.usage
tokens_in = usage.prompt_tokens if usage else 0
except openai.RateLimitError as e: tokens_out = usage.completion_tokens if usage else 0
temps_attente = 2 ** tentative return texte, couleur, "OpenRouter", nom_affichage, tokens_in, tokens_out
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.")
except Exception as e: 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 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 import tiktoken
from modules.logger import log_event 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 = { TARIFS_OPENROUTER = {
"meta-llama/llama-3.1-8b-instruct:free": 0.0, "meta-llama/llama-3.1-8b-instruct:free": 0.0,
"qwen/qwen-2.5-coder-32b-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"): VIP_ADMIN_USERS = ["xavier", "céline", "celine", "anaëlle", "anaelle", "renan", "audric"]
"""Compte le nombre de tokens en local pour éviter les requêtes API inutiles."""
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: try:
enc = tiktoken.get_encoding(modele) enc = tiktoken.get_encoding(modele)
return len(enc.encode(texte)) return len(enc.encode(texte))
except Exception as e: 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 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. Calcule le coût brut API et le prix facturé selon le rang :
Applique la marge stricte de 15% pour générer le Prix Facture HT. - 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) 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: if multi_ia and cout_brut > 0:
return 0.0 cout_brut *= 2
# Application stricte de votre formule comptable if cout_brut == 0.0:
prix_facture_ht = cout_brut_api / 0.85 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): return round(cout_brut, 6), round(prix_facture, 6), tokens
"""
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)
# Les VIP ne paient pas de marge, on affiche juste le coût brut pour information def verifier_et_valider_budget(prompt: str, modele_id: str, utilisateur_nom: str = "xavier", rang_utilisateur: str = "super admin", multi_ia: bool = False):
if utilisateur_vip: """Vérifie le solde avant exécution et applique le débit approprié."""
log_event("INFO", "BILLING", f"Utilisateur VIP. Coût brut estimé de la requête : {cout_estime * 0.85} $") cout_brut, prix_facture, tokens = estimer_cout_facture(prompt, modele_id, rang_utilisateur, multi_ia)
if prix_facture == 0.0:
return True, 0.0 return True, 0.0
if cout_estime > 0.0: soldes = charger_soldes()
# Lève une exception spécifique que l'interface FastHTML devra attraper pour afficher le pop-up utilisateur_cle = utilisateur_nom.lower()
raise Exception(f"VALIDATION_PAIEMENT_REQUISE|{cout_estime}")
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
+43 -44
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.ui_components import render_user_msg, render_ai_msg, render_sidebar
from modules.logger import log_event from modules.logger import log_event
from modules.billing_manager import verifier_et_valider_budget from modules.billing_manager import verifier_et_valider_budget
from modules.rag_manager import rechercher_contexte_rag
# Import du nouveau module de modération
from modules.moderator import analyser_intention_illicite, appliquer_sanction_et_archiver from modules.moderator import analyser_intention_illicite, appliquer_sanction_et_archiver
async def handle_chat_request(request, session): async def handle_chat_request(request, session):
@@ -22,31 +21,27 @@ async def handle_chat_request(request, session):
chat_id = form.get("chat_id", str(uuid.uuid4())) chat_id = form.get("chat_id", str(uuid.uuid4()))
files = form.getlist("files") files = form.getlist("files")
# --- RÉCUPÉRATION DU CHOIX GRATUIT/PAYANT ---
mode_payant = form.get("mode_payant") == "on" 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() history = load_history()
# Identification utilisateur_nom = session.get("user", "xavier")
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")
utilisateur_vip = utilisateur_nom.lower() in ["xavier", "céline", "anaëlle", "renan", "audric"]
# Récupération IP depuis la requête Starlette/FastHTML
ip_client = request.client.host if request.client else "Inconnue" ip_client = request.client.host if request.client else "Inconnue"
# --- 🚨 ÉTAPE 1 : MODÉRATION PRÉVENTIVE (Filtre Légal) 🚨 --- # Modération préventive locale
est_illicite, raison = analyser_intention_illicite(prompt, None) est_illicite, raison = analyser_intention_illicite(prompt, "qwen2.5-coder:3b")
if est_illicite: if est_illicite:
sanction = appliquer_sanction_et_archiver(utilisateur_nom, prompt, raison, ip_client) sanction = appliquer_sanction_et_archiver(utilisateur_nom, prompt, raison, ip_client)
return Div( return Div(
H3("⚠️ Avertissement de Sécurité"), H3("⚠️ Avertissement de Sécurité"),
P("Votre requête enfreint les Conditions Générales d'Utilisation (Contenu illicite)."), P("Votre requête enfreint les conditions d'utilisation de la plateforme."),
P(f"Sanction appliquée : {sanction}"), P(f"Sanction : {sanction}"),
P("Cet incident a été enregistré et transmis à l'administration."),
style="color:red; background:#300; padding:15px; border-radius:10px;" style="color:red; background:#300; padding:15px; border-radius:10px;"
) )
# Extraction éphémère en RAM
file_summary = [] file_summary = []
file_names = [] file_names = []
for f in files[:10]: for f in files[:10]:
@@ -55,54 +50,57 @@ async def handle_chat_request(request, session):
content = await f.read() 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_mode = "PAYANT 👑" if mode_payant else "GRATUIT 🆓"
log_event("INFO", "SYSTEM", f"Requête reçue [{log_mode}]. Fichiers joints : {liste_fichiers_str}") 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 : 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. - "gemini" : (OBLIGATOIRE si fichiers joints), rédaction complexe, comparaisons.
- "openrouter_chat" : requêtes générales, texte rapide, traduction, bureautique. - "openrouter_chat" : requêtes générales, texte rapide, traduction.
- "openrouter_code" : programmation, scripts, mathématiques, logique. - "openrouter_code" : programmation, Minecraft/KubeJS, scripts, mathématiques.
- "local" : salutations basiques, questions très simples. - "openrouter_droit" : droit français, CGV, CGU, RGPD.
Renvoie STRICTEMENT un JSON : {{"intention": "Résumé...", "fournisseur": "gemini|openrouter_chat|openrouter_code|local"}} - "local" : salutations basiques, questions simples.
Renvoie STRICTEMENT un JSON : {{"intention": "Résumé...", "fournisseur": "gemini|openrouter_chat|openrouter_code|openrouter_droit|local"}}
Requête : {prompt}""" Requête : {prompt}"""
try: 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) decision = nettoyer_decision_router(texte_routeur)
except Exception: 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') intention = decision.get("intention", "Analyse")
fournisseur = decision.get('fournisseur', 'openrouter_chat').lower() fournisseur = decision.get("fournisseur", "openrouter_chat").lower()
if file_names: if file_names:
fournisseur = "gemini" fournisseur = "gemini"
intention += " (Forcé Gemini via Sécurité Fichiers)" intention += " (Forcé Gemini via Sécurité Fichiers)"
urls = re.findall(r'https?://[^\s]+', prompt) thematique_rag = "minecraft" if "mine" in prompt.lower() or "kubejs" in prompt.lower() else ("droit" if "droit" in fournisseur else None)
mots_cles = ["cherche", "recherche", "sur internet", "sur le web", "sur le net", "va sur", "trouve", "site", "donne", "historique", "version"] 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): if any(m in prompt.lower() for m in mots_cles):
urls.extend(rechercher_sur_google(prompt)) urls.extend(rechercher_sur_google(prompt))
web_text = extraire_contenu_urls(list(set(urls))) if urls else "" web_text = extraire_contenu_urls(list(set(urls))) if urls else ""
full_prompt = prompt + ("\n" + "\n".join(file_summary) if file_summary 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: 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 "openrouter" in fournisseur:
if mode_payant: modele_estime = "anthropic/claude-3.5-sonnet" if mode_payant else "cohere/north-mini-code:free"
modele_estime = "anthropic/claude-3.5-sonnet" if fournisseur == "openrouter_code" else "openai/gpt-4o-mini" verifier_et_valider_budget(full_prompt, modele_estime, utilisateur_nom=utilisateur_nom, rang_utilisateur=role_utilisateur, multi_ia=mode_multi_ia)
else:
modele_estime = "qwen/qwen-2.5-coder-32b-instruct:free" if fournisseur == "openrouter_code" else "meta-llama/llama-3.1-8b-instruct:free"
verifier_et_valider_budget(full_prompt, modele_estime, utilisateur_vip) # Exécution rapide
reponse, couleur, fournisseur_nom, modele = executer_requete_ia(
# Exécution fournisseur, full_prompt, mode_payant=mode_payant, multi_ia=mode_multi_ia
reponse, couleur, fournisseur_nom, modele = executer_requete_ia(fournisseur, full_prompt, mode_payant) )
if chat_id not in history: if chat_id not in history:
history[chat_id] = {"title": (prompt[:30] if prompt else "Discussion") + "...", "pinned": False, "timestamp": time.time(), "messages": []} 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 display_msg = prompt
if file_names: if file_names:
display_msg += f"\n\n📎 **Fichiers joints :** {', '.join(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": "user", "content": display_msg})
history[chat_id]['messages'].append({"role": "ai", "content": reponse, "intention": intention, "fournisseur": fournisseur_nom, "modele": modele, "couleur": couleur, "execution_time": execution_time}) history[chat_id]["messages"].append({
"role": "ai", "content": reponse, "intention": intention,
"fournisseur": fournisseur_nom, "modele": modele,
"couleur": couleur, "execution_time": execution_time
})
save_history(history) save_history(history)
log_event("INFO", fournisseur_nom, f"Réponse générée en {execution_time}s.") 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() 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: except Exception as e:
log_event("CRITICAL", "SYSTEM", f"Erreur critique dans handle_chat_request: {e}") log_event("CRITICAL", "SYSTEM", f"Erreur 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;")
return Div(f"Erreur système : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;") return Div(f"Erreur système : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;")
+23 -14
View File
@@ -16,10 +16,13 @@ BASE_DATA_DIR = "/DATA/AppData/MULTI-IA-AETHAS38/Utilisateurs"
# Liste des Super Admins / Admins à prévenir # Liste des Super Admins / Admins à prévenir
ADMINS_GLOBAUX = ["xavier", "céline"] 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. 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. 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). 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. 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}""" Requête à analyser : {prompt}"""
try: try:
# On interroge Qwen en local # On interroge le modèle local sécurisé
reponse, _, _, _ = interroger_local_ollama(prompt_juge, ai_orchestrateur) reponse, _, _, _ = interroger_local_ollama(prompt_juge, modele_cible)
# Extraction du JSON de la réponse # Extraction du JSON de la réponse
match = re.search(r'\{.*\}', reponse, re.DOTALL) match = re.search(r'\{.*\}', reponse, re.DOTALL)
@@ -39,15 +42,15 @@ Requête à analyser : {prompt}"""
except Exception as e: except Exception as e:
log_event("WARNING", "MODERATOR", f"Échec de l'analyse IA (Fallback activé) : {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"] mots_interdits = ["pédopornographie", "bombe", "terrorisme"]
if any(mot in prompt.lower() for mot in mots_interdits): if any(mot in prompt.lower() for mot in mots_interdits):
return True, "Détection par filtre de mots-clés (Secours)" return True, "Détection par filtre de mots-clés (Secours)"
return False, "" 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) user_dir = os.path.join(BASE_DATA_DIR, utilisateur_nom)
illegal_dir = os.path.join(user_dir, "Illegal") 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 strikes = 0
if os.path.exists(dossier_sanctions): if os.path.exists(dossier_sanctions):
try:
with open(dossier_sanctions, "r", encoding="utf-8") as f: with open(dossier_sanctions, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
strikes = data.get("avertissements", 0) strikes = data.get("avertissements", 0)
except Exception:
strikes = 0
strikes += 1 strikes += 1
sanction_actuelle = "Avertissement" sanction_actuelle = "Avertissement"
if strikes >= 10: sanction_actuelle = "BAN DEFINITIF EN ATTENTE DE VALIDATION" if strikes >= 10:
elif strikes >= 9: sanction_actuelle = "Ban 1 mois" sanction_actuelle = "BAN DEFINITIF EN ATTENTE DE VALIDATION"
elif strikes >= 6: sanction_actuelle = "Ban 1 semaine" elif strikes >= 9:
elif strikes >= 3: sanction_actuelle = "Ban 1 jour" 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: 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") 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: 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()] 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) log_event("CRITICAL", "MODERATION", alerte_msg)
if envoyer_alerte_debug and admins_a_alerter: 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) envoyer_alerte_debug("CRITICAL", f"TENTATIVE ILLICITE ({utilisateur_nom})", alerte_msg)
return sanction_actuelle 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. Archive la décision prise par un admin avec commentaire libre.
""" """
+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 fasthtml.common import *
from modules.database import load_history from modules.database import load_history
from modules.paypal import render_paypal_donation_widget from modules.paypal import render_paypal_donation_widget
from modules.billing_manager import obtenir_solde
def render_user_msg(text): def render_user_msg(text):
return Div(P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"), cls="user-bubble") 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("📝 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("🗄️ 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("💬 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" cls="msg-actions"
) )
return Div( return Div(
@@ -49,7 +50,7 @@ def render_sidebar():
) )
return Div(*items, id="sidebar-content", cls="sidebar-history", hx_swap_oob="true") 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() history = load_history()
messages_ui = [] messages_ui = []
if chat_id in history: if chat_id in history:
@@ -57,16 +58,19 @@ def render_main_page(chat_id):
if msg['role'] == 'user': if msg['role'] == 'user':
messages_ui.append(render_user_msg(msg['content'])) messages_ui.append(render_user_msg(msg['content']))
else: 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: if messages_ui:
messages_ui.append(Script("setTimeout(renderMarkdown, 150);")) messages_ui.append(Script("setTimeout(renderMarkdown, 150);"))
auto_refresh_script = Script(''' auto_refresh_script = Script('''
let serverDown = false; let serverDown = false;
setInterval(() => { setInterval(() => {
fetch('/favicon.ico', {cache: 'no-store', method: 'HEAD'}) fetch('/ping', {cache: 'no-store'})
.then(response => { .then(response => {
if (serverDown) { window.location.reload(); } if (serverDown && response.ok) { window.location.reload(); }
}) })
.catch(() => { .catch(() => {
serverDown = true; serverDown = true;
@@ -75,6 +79,22 @@ def render_main_page(chat_id):
}, 5000); }, 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( return Title("Aethas38 - Core"), Body(
auto_refresh_script, auto_refresh_script,
Div( Div(
@@ -95,7 +115,7 @@ def render_main_page(chat_id):
cls="sidebar-top" cls="sidebar-top"
), ),
render_sidebar(), 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" cls="sidebar"
), ),
Div( Div(
@@ -112,7 +132,12 @@ def render_main_page(chat_id):
Label( Label(
Input(type="checkbox", name="mode_payant", id="mode-payant-toggle", style="margin-right: 6px; accent-color: #00e5ff; cursor: pointer;"), 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;" 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): def render_admin_moderation_panel(utilisateur_cible):
""" """Composant exclusif aux administrateurs pour statuer sur les infractions."""
Composant UI exclusif aux Admins pour statuer sur les infractions avec commentaire libre.
"""
return Div( return Div(
H3(f"⚖️ Tribunal Admin - Dossier de : {utilisateur_cible}", style="color: #ff4444;"), H3(f"⚖️ Tribunal d'administration - Dossier de : {utilisateur_cible}", style="color: #ff4444;"),
P("Loi applicable : Aucun utilisateur n'est dispensé du respect de la loi."), P("Loi applicable : Aucun utilisateur n'est dispensé du respect de la législation en vigueur."),
Form( Form(
Input(type="hidden", name="cible", value=utilisateur_cible), Input(type="hidden", name="cible", value=utilisateur_cible),
Div( Div(
Label("Action décidée :", style="font-weight: bold;"), Label("Action décidée :", style="font-weight: bold;"),
Select( Select(
Option("Avertissement simple", value="Avertissement"), Option("Avertissement simple", value="Avertissement"),
Option("Bannissement 1 Jour", value="Ban 1J"), Option("Bannissement 1 jour", value="Ban 1J"),
Option("Bannissement 1 Semaine", value="Ban 1Sem"), Option("Bannissement 1 semaine", value="Ban 1Sem"),
Option("Bannissement 1 Mois", value="Ban 1M"), Option("Bannissement 1 mois", value="Ban 1M"),
Option("Bannissement Définitif & Signalement PHAROS", value="Ban Definitif + Pharos"), Option("Bannissement définitif et signalement PHAROS", value="Ban Definitif + Pharos"),
Option("Faux positif (Levée de sanction)", value="Annulation"), Option("Faux positif (Levée de sanction)", value="Annulation"),
name="decision", cls="input-field" name="decision", cls="input-field"
), ),
style="margin-bottom: 15px;" style="margin-bottom: 15px;"
), ),
Div( Div(
Label("Commentaire libre de l'Admin (Preuve d'audit) :", style="font-weight: bold;"), 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..."), Textarea(name="commentaire", rows="4", cls="input-field", placeholder="Raison de la décision, contexte juridique..."),
style="margin-bottom: 15px;" style="margin-bottom: 15px;"
), ),
Button("Valider la décision et archiver", type="submit", cls="btn-submit", style="background-color: #ff4444; color: white;"), 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", hx_target="#admin-result",
style="background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333;" 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.