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 💰
|
### 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) |
|
||||||
|
|||||||
@@ -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)
|
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)
|
||||||
+40
-12
@@ -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,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."
|
"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)
|
||||||
|
|
||||||
if texte:
|
if texte:
|
||||||
return texte, couleur, nom_fournisseur, nom_modele
|
return texte, couleur, nom_fournisseur, nom_modele
|
||||||
except Exception as e:
|
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
|
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
|
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,
|
||||||
@@ -24,7 +28,7 @@ def interroger_gemini(prompt_securise):
|
|||||||
log_event("INFO", f"GEMINI-{modele}", "Succès de la requête Gemini.")
|
log_event("INFO", f"GEMINI-{modele}", "Succès de la requête Gemini.")
|
||||||
return response.text, "#4285F4", "Google Gemini", modele
|
return response.text, "#4285F4", "Google Gemini", modele
|
||||||
except Exception as sub_e:
|
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
|
continue
|
||||||
|
|
||||||
raise Exception("Tous les modèles Gemini configurés ont échoué.")
|
raise Exception("Tous les modèles Gemini configurés ont échoué.")
|
||||||
|
|||||||
+15
-6
@@ -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
@@ -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
@@ -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
|
||||||
+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.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,87 +21,86 @@ 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]:
|
||||||
if f and f.filename:
|
if f and f.filename:
|
||||||
file_names.append(f.filename)
|
file_names.append(f.filename)
|
||||||
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;")
|
||||||
+26
-17
@@ -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.
|
||||||
"""
|
"""
|
||||||
@@ -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")
|
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:
|
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}")
|
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 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.
Reference in new issue
Block a user