Public Access
fix: Migration vers le nouveau SDK Google GenAI et mise à jour des dépendances
Build and Push Docker Image / build-and-push (push) Successful in 2m4s
Build and Push Docker Image / build-and-push (push) Successful in 2m4s
This commit is contained in:
1 parent
5a4e7dd094
commit
a10c854b2b
11 files changed
+2109
-87
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -1,12 +1,14 @@
|
|||||||
# Multi-IA Orchestrateur
|
# Aethas38 - Multi-IA Orchestrateur (Minecraft-Tech Edition)
|
||||||
|
|
||||||
Plateforme SaaS d'IA modulaire : Orchestrateur local, routeur intelligent, sécurisé et facturable.
|
Plateforme SaaS d'IA modulaire : Orchestrateur local, routeur intelligent, sécurisé et facturable.
|
||||||
|
<!-- MAJ 18/08/2026 : URL de production masquée pour raisons de cybersécurité (Information Disclosure) -->
|
||||||
|
**Architecture réseau :** Déploiement via Docker derrière Nginx Proxy Manager (NPM) sur `https://ia.votre-domaine.com`.
|
||||||
|
|
||||||
## 📜 Licence & Droits d'Utilisation (EULA)
|
## 📜 Licence & Droits d'Utilisation (EULA)
|
||||||
Ce projet est distribué sous une licence "Source Available" propriétaire (EULA).
|
Ce projet est distribué sous une licence "Source Available" propriétaire (EULA) rédigée par Xavier Fernandez (Aethas38).
|
||||||
* **Modifications & Partage :** Les utilisateurs sont autorisés à modifier et améliorer le code, à condition de partager ces améliorations avec la communauté.
|
* **Modifications & Partage :** Les utilisateurs sont autorisés à modifier et améliorer le code, à condition de partager ces améliorations avec la communauté.
|
||||||
* **Restrictions strictes :** Il est **strictement interdit** de modifier ou supprimer les crédits de l'auteur originel, de retirer le module de dons dédié au créateur, ou de désactiver la télémétrie de sécurité à l'installation. L'installation requiert l'acceptation explicite de ces termes.
|
* **Restrictions strictes :** Il est **strictement interdit** de modifier ou supprimer les crédits de l'auteur originel, de retirer le module de dons dédié au créateur, ou de désactiver la télémétrie de sécurité (notification d'installation avec IP/Nom admin envoyée à `admin@mail.aethas38.duckdns.org`). L'installation requiert l'acceptation explicite de ces termes.
|
||||||
* **Donations obligatoires :** Le système intègre nativement deux modules de dons. L'un est modifiable par l'hébergeur du projet. L'autre (marqué "Soutenir le créateur de ce projet") est inaltérable et relié directement au créateur originel. Le retrait de ce module est une violation stricte de cette licence.
|
* **Donations obligatoires :** Le système intègre nativement deux modules de dons. L'un est modifiable par l'hébergeur du projet. L'autre (marqué "Soutenir le créateur de ce projet") est inaltérable et relié directement au créateur originel (Xavier Fernandez / Aethas38). Le retrait de ce module est une violation stricte de cette licence.
|
||||||
|
|
||||||
## ⚠️ Pré-requis Matériels (Important)
|
## ⚠️ Pré-requis Matériels (Important)
|
||||||
Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minimum **une carte graphique avec 4 Go de VRAM dédiée**. Il est possible de configurer l'application pour utiliser le CPU, mais les performances seront considérablement réduites et les temps de réponse allongés. **L'auteur décline toute responsabilité en cas de surcharge matérielle liée à une utilisation CPU intensive.**
|
Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minimum **une carte graphique avec 4 Go de VRAM dédiée**. Il est possible de configurer l'application pour utiliser le CPU, mais les performances seront considérablement réduites et les temps de réponse allongés. **L'auteur décline toute responsabilité en cas de surcharge matérielle liée à une utilisation CPU intensive.**
|
||||||
@@ -14,50 +16,63 @@ Pour fonctionner de manière optimale, l'Orchestrateur Local nécessite au minim
|
|||||||
## 📋 To-Do List (Roadmap)
|
## 📋 To-Do List (Roadmap)
|
||||||
|
|
||||||
### Phase 1 & 2 : Infrastructure & Backend de Base ✅
|
### Phase 1 & 2 : Infrastructure & Backend de Base ✅
|
||||||
- [x] Installation Docker, Pare-feu
|
- [x] Installation Docker, Nginx (NPM), Pare-feu, Gitea/GitHub
|
||||||
- [x] Serveur Local Ollama + FastHTML
|
- [x] Serveur Local Ollama (Qwen 3B) + FastHTML (Adapté aux processeurs limités type AMD Phenom)
|
||||||
- [x] Routeur Multi-IA (OpenRouter, Gemini, Local) avec système de cascade
|
- [x] Routeur Multi-IA (OpenRouter, Gemini, Local) avec système de cascade et directives d'aiguillage strictes
|
||||||
- [x] Continuité de service : IA de secours automatique
|
- [x] Continuité de service : IA de secours automatique
|
||||||
|
|
||||||
### Phase 3 : Refonte UI & Expérience Utilisateur ✅
|
### Phase 3 : Refonte UI & Expérience Utilisateur ✅
|
||||||
- [x] Thème "Minecraft-Tech" (Inspiré serveurs/redstone) compatible Mobile (Responsive)
|
- [x] Thème "Minecraft-Tech" (Inspiré serveurs/redstone) compatible Mobile (Responsive)
|
||||||
- [x] Historique (Sidebar) avec Renommage, Épinglage, Suppression et Navigation isolée
|
- [x] Historique (Sidebar) avec Renommage, Épinglage, Suppression et Navigation isolée par session
|
||||||
- [x] Zone de saisie intelligente (Drag & Drop, multi-fichiers)
|
- [x] Zone de saisie intelligente (Textarea auto-adaptatif, Drag & Drop avec pills/miniatures, Collage multi-fichiers jusqu'à 10)
|
||||||
- [x] Export modulaire (PDF, DOC, TXT, JSON, ZIP)
|
- [x] Export modulaire et propre (PDF, DOC, TXT, JSON, ZIP)
|
||||||
|
- [x] Boutons de partage direct (WhatsApp, Email)
|
||||||
|
- [x] Indicateur d'avancement dynamique (Loader Néon) pendant le traitement des IA
|
||||||
- [x] Architecture modulaire propre (Dossier `modules/`)
|
- [x] Architecture modulaire propre (Dossier `modules/`)
|
||||||
|
|
||||||
### Phase 4 : Gestion des Utilisateurs & Sécurité Légale
|
### Phase 4 : Gestion des Utilisateurs & Sécurité Légale
|
||||||
- [ ] Mettre en place une bibliothèque logicielle autonome sur le serveur.
|
- [ ] Mettre en place une bibliothèque logicielle autonome sur le serveur (création PDF, DOCX, etc.).
|
||||||
- [ ] Routage automatique (Gratuit vs Payant) sélectionné par l'utilisateur.
|
- [ ] Routage automatique (Gratuit vs Payant) sélectionné par l'utilisateur.
|
||||||
- [ ] Système de connexion sécurisé avec configuration du premier lancement.
|
- [ ] Système de connexion sécurisé avec configuration du premier lancement (DOB depuis 1900, Email, Clés API). L'utilisateur Xavier a obligatoirement le rôle "Dieu tout puissant" (Admin global).
|
||||||
- [x] Système de cryptage avancé (Fernet) et de Hashing (Bcrypt) mis en place.
|
- [x] Système de cryptage de grade militaire (Fernet) et de Hashing (Bcrypt) mis en place (Module `auth_manager.py`).
|
||||||
- [x] Sécurité : Protection Bruteforce. **Blocage des accès VPN/Proxy (Passe-droit exclusif configuré pour les profils VIP/Admin et réseaux locaux).**
|
- [X] Sécurité Légère : Fail2Ban, protection Bruteforce et DDoS. **Blocage total des accès VPN**.
|
||||||
- [x] Conformité légale (RGPD) : Journalisation stricte des IP par client conservée 1 an.
|
- [X] Conformité légale (RGPD) : Journalisation stricte des IP par client conservée 1 an.
|
||||||
- [ ] Multilinguisme : Adaptation automatique de la langue du site via IP.
|
- [ ] Multilinguisme : Adaptation automatique de la langue du site via la géolocalisation de l'IP.
|
||||||
|
|
||||||
### Phase 5 : Sécurité Financière & Facturation 💰
|
### Phase 5 : Sécurité Financière & Facturation Exacte 💰
|
||||||
<!-- MAJ 18/08/2026 : Intégration du module financier, calcul des marges et interface de validation -->
|
- [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.
|
- [ ] Interface UI : Affichage du Pop-Up de validation du coût pour l'utilisateur.
|
||||||
- [x] Intégration du gestionnaire financier strict : simulation des coûts, blocage immédiat si le solde est insuffisant et gestion spécifique pour les utilisateurs VIP (Zéro marge/Zéro coût).
|
- [ ] Cryptage absolu de toutes les données sensibles et bancaires.
|
||||||
- [ ] Interface UI : Affichage du Pop-Up de validation du coût et affichage de la cagnotte utilisateur.
|
- [ ] Module de conversion des devises en temps réel (Taux du jour) pour un affichage transparent.
|
||||||
- [ ] Cryptage absolu des données sensibles bancaires.
|
- [ ] **Utilisateurs Privilégiés (Zéro Marge) :** Xavier, Céline, Anaëlle, Renan et Audric. Autorisation de virements manuels pour la cagnotte.
|
||||||
- [ ] Module de conversion des devises en temps réel.
|
- [x] **Marge Commerciale :** Majoration automatique via la formule `Prix_Facture_HT = Cout_API_OpenRouter_HT / 0.85` (soit 15% de marge) implémentée dans le gestionnaire financier.
|
||||||
- [x] **Gestionnaire Financier :** Majoration automatique (15%) appliquée selon les statuts pour générer la facturation finale.
|
- [ ] Export comptable Excel (TCD) par utilisateur via Pandas/Openpyxl et archivage PDF automatisé.
|
||||||
- [ ] Export comptable Excel (TCD) avec archivage PDF automatisé (Admin uniquement).
|
|
||||||
|
|
||||||
### 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 OpenRouter pour palier aux décommissionnements constants des autres fournisseurs.*
|
||||||
|
|
||||||
| Domaine de tâche | Moteur | Rôle principal |
|
| Domaine de tâche | Modèle / Moteur | Fournisseur / Hébergement | Rôle principal |
|
||||||
| :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- |
|
||||||
| **Orchestration & Tri** | Local (Ollama) | Analyse l'intention, gère le chat rapide et route vers la bonne IA |
|
| **Orchestration & Tri** | Qwen 2.5 (3B) | Local (GTX 1650 / Ollama) | Analyse l'intention, gère le chat rapide et route vers la bonne IA |
|
||||||
| **Bureautique & Code** | OpenRouter API | Programmation, scripts, macros VBA, mathématiques |
|
| **Bureautique & Code** | Llama 3.1 / Qwen 2.5 Coder | OpenRouter API (Gratuit) | Programmation, scripts, macros VBA, mathématiques |
|
||||||
| **Images HD / Fichiers** | Google API | Analyse d'images/vidéos et documents joints |
|
| **Images HD / Fichiers** | Gemini 2.0 Flash / Pro | Google AI Studio | Analyse d'images/vidéos et documents joints |
|
||||||
| **Génération Multimédia** | HuggingFace / Fal.ai | Création d'images et vidéos (Text-to-Image / Text-to-Video) |
|
| **Génération Multimédia** | Stable Diffusion / Flux / Luma | HuggingFace / Fal.ai | Création d'images et vidéos (Text-to-Image / Text-to-Video) |
|
||||||
|
|
||||||
### Phase 7 & 8 : Maintenance & Déploiement
|
### Phase 7 & 8 : Maintenance & Déploiement
|
||||||
<!-- MAJ : Ajout de la configuration du Runner et de la CI/CD -->
|
- [ ] Script de surveillance de l'espace disque du serveur (Beszel / Psutil) réservé à l'Admin.
|
||||||
- [ ] Création d'un Wiki d'installation détaillé multilingue.
|
- [ ] Sauvegarde automatique : Compression du site toutes les 6 heures vers GDrive (`25-BACKUP SITE`), 4 versions conservées.
|
||||||
- [x] Fichiers `docker-compose.yml` et `Dockerfile` préparés pour le déploiement conteneurisé.
|
- [x] Hébergement sur Gitea/Github des versions Docker. Configuration du fichier `docker-compose.yml` et `Dockerfile` réalisée.
|
||||||
- [x] Configuration du Webhook et script Bash de déploiement automatique (`git pull` + Docker Compose Rebuild) ciblant la stack `AETHAS38_MULTI_IA`.
|
- [x] Script Bash de déploiement automatique via `aethas38_multi_ia/update_aethas.sh`.
|
||||||
- [x] Stratégie de mise à jour transparente via Gitea (dépôt public pour automatisation fluide sans authentification complexe).
|
- [ ] Création d'un Wiki détaillé multilingue pour l'installation personnelle.
|
||||||
- [x] Déploiement du Runner Gitea local et configuration du workflow CI/CD (`docker-build.yml`) pour la génération automatique du package (image Docker).
|
|
||||||
|
### Phase 9 : Modération Automatisée & Conformité Pénale 🚨
|
||||||
|
<!-- MAJ 19/08/2026 : Intégration du Juge Qwen et gestion d'audit pénal -->
|
||||||
|
- [x] Système hiérarchique strict : `Super Admin` (Xavier), `Admin` (Modération/VIP), `VIP` (Grade de filiation sans privilèges d'administration) et Utilisateurs.
|
||||||
|
- [ ] Validation obligatoire des CGV/CGU incluant une clause de "Ban sans remboursement et signalement aux autorités" en cas de contenu manifestement illicite.
|
||||||
|
- [x] Modération préventive "Anti-Zèle" : Filtrage par IA Locale (Qwen) des intentions illégales bloquant la requête vers OpenRouter tout en tolérant la fiction et les demandes juridiques.
|
||||||
|
- [x] Système d'Avertissements (Strikes) automatisé avec alerte E-mail aux Admins.
|
||||||
|
- [x] Protection anti-abus Admin : Si un Admin enfreint la loi, il est exclu de l'alerte ; seuls les autres Admins statuent.
|
||||||
|
- [x] Archivage légal sécurisé : Stockage pérenne des logs, preuves d'infractions et **décisions d'audit manuscrites des Admins** dans une arborescence dédiée sur grappe RAID 1.
|
||||||
|
|
||||||
|
## 📷 Vidéosurveillance & Détection Intelligente
|
||||||
|
*(Voir configuration Frigate locale et Google Coral TPU existante)*
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-19 02:46:12",
|
||||||
|
"timestamp": 1787100372.9574325
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:46:18",
|
||||||
|
"timestamp": 1787100378.0617583
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:46:23",
|
||||||
|
"timestamp": 1787100383.052557
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:46:28",
|
||||||
|
"timestamp": 1787100388.056085
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:46:33",
|
||||||
|
"timestamp": 1787100393.0593455
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:46:38",
|
||||||
|
"timestamp": 1787100398.052747
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:46:43",
|
||||||
|
"timestamp": 1787100403.052715
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:46:48",
|
||||||
|
"timestamp": 1787100408.9220579
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:46:53",
|
||||||
|
"timestamp": 1787100413.9221146
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:54:23",
|
||||||
|
"timestamp": 1787100863.280455
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /",
|
||||||
|
"date": "2026-08-19 02:54:23",
|
||||||
|
"timestamp": 1787100863.340981
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:54:28",
|
||||||
|
"timestamp": 1787100868.9211378
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:54:33",
|
||||||
|
"timestamp": 1787100873.9200711
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:54:38",
|
||||||
|
"timestamp": 1787100878.9196937
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:54:43",
|
||||||
|
"timestamp": 1787100883.9212368
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:54:48",
|
||||||
|
"timestamp": 1787100888.9214082
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:54:53",
|
||||||
|
"timestamp": 1787100893.9216979
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:54:59",
|
||||||
|
"timestamp": 1787100899.2418387
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:55:03",
|
||||||
|
"timestamp": 1787100903.9211195
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ip": "127.0.0.1",
|
||||||
|
"action": "Acc\u00e8s \u00e0 /favicon.ico",
|
||||||
|
"date": "2026-08-19 02:55:08",
|
||||||
|
"timestamp": 1787100908.921069
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
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
|
||||||
# Le nouveau module que nous allons créer à la prochaine étape
|
|
||||||
from modules.ai_openrouter import interroger_openrouter
|
from modules.ai_openrouter import interroger_openrouter
|
||||||
|
|
||||||
SYSTEM_CONFIDENTIALITY_INSTRUCTION = (
|
SYSTEM_CONFIDENTIALITY_INSTRUCTION = (
|
||||||
@@ -11,11 +10,11 @@ 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):
|
def executer_requete_ia(fournisseur, prompt, mode_payant=False):
|
||||||
"""Moteur central gérant les consignes de sécurité et la cascade intelligente vers OpenRouter."""
|
"""Moteur central gérant les consignes de sécurité et la cascade intelligente vers OpenRouter."""
|
||||||
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 (Gratuit), Gemini et le Local
|
# 🔄 Ordre de cascade recentré sur OpenRouter, Gemini et le Local
|
||||||
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 == "openrouter_code":
|
||||||
@@ -27,9 +26,9 @@ def executer_requete_ia(fournisseur, prompt):
|
|||||||
|
|
||||||
for fonction_api in ordre_repli:
|
for fonction_api in ordre_repli:
|
||||||
try:
|
try:
|
||||||
# Transfert du 'fournisseur' cible pour qu'OpenRouter sache s'il doit appeler un modèle Code ou Chat
|
# 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(prompt_securise, type_modele=fournisseur)
|
texte, couleur, nom_fournisseur, nom_modele = fonction_api(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)
|
||||||
|
|
||||||
|
|||||||
+24
-10
@@ -1,19 +1,33 @@
|
|||||||
import os
|
import os
|
||||||
import google.generativeai as genai
|
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)."""
|
||||||
api_key = os.getenv("GEMINI_API_KEY")
|
api_key = os.getenv("GEMINI_API_KEY")
|
||||||
if not api_key: raise Exception("Clé API Gemini manquante.")
|
if not api_key:
|
||||||
|
raise Exception("Clé API Gemini manquante dans l'environnement.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Initialisation du client avec le nouveau SDK
|
||||||
client = genai.Client(api_key=api_key)
|
client = genai.Client(api_key=api_key)
|
||||||
modeles = ["gemini-3.1-pro-preview", "gemini-3.1-pro", "gemini-2.5-pro", "gemini-3.7-flash"]
|
modeles = ["gemini-2.5-pro", "gemini-1.5-pro", "gemini-2.0-flash"]
|
||||||
|
|
||||||
for modele in modeles:
|
for modele in modeles:
|
||||||
try:
|
try:
|
||||||
config = {"system_instruction": "Réponds en français et en Markdown sans code LaTeX."}
|
# Appel avec configuration des instructions système
|
||||||
resp = client.chats.create(model=modele, config=config).send_message(prompt_securise)
|
response = client.models.generate_content(
|
||||||
if resp and resp.text:
|
model=modele,
|
||||||
log_event("INFO", f"GEMINI-{modele}", "Succès de la requête.")
|
contents=prompt_securise,
|
||||||
return resp.text, "#3b82f6", "GOOGLE GEMINI", modele
|
)
|
||||||
|
if response and response.text:
|
||||||
|
log_event("INFO", f"GEMINI-{modele}", "Succès de la requête Gemini.")
|
||||||
|
return response.text, "#4285F4", "Google Gemini", modele
|
||||||
|
except Exception as sub_e:
|
||||||
|
log_event("WARNING", f"GEMINI-{modele}", f"Essai échoué pour {modele}: {sub_e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise Exception("Tous les modèles Gemini configurés ont échoué.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_event("WARNING", f"GEMINI-{modele}", f"Erreur: {e}")
|
log_event("ERROR", "GEMINI", f"Erreur critique Gemini : {e}")
|
||||||
raise Exception("Tous les modèles Gemini ont échoué.")
|
raise e
|
||||||
@@ -10,17 +10,27 @@ client = OpenAI(
|
|||||||
api_key=os.getenv("OPENROUTER_API_KEY", "VOTRE_CLE_ICI") # À définir dans le .env
|
api_key=os.getenv("OPENROUTER_API_KEY", "VOTRE_CLE_ICI") # À définir dans le .env
|
||||||
)
|
)
|
||||||
|
|
||||||
def interroger_openrouter(prompt, type_modele="openrouter_chat", max_retries=3):
|
def interroger_openrouter(prompt, type_modele="openrouter_chat", premium=False, max_retries=3):
|
||||||
"""
|
"""
|
||||||
Interroge l'API OpenRouter avec gestion des erreurs 402 (Plafond) et 429 (Rate Limit).
|
Interroge l'API OpenRouter avec gestion des erreurs 402 (Plafond) et 429 (Rate Limit).
|
||||||
Sélectionne un modèle gratuit selon l'intention (Chat ou Code).
|
Bascule entre les modèles gratuits et premiums (VIP/Payant) selon le choix utilisateur.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Sélection des modèles 100% gratuits et performants sur OpenRouter
|
# Sélection dynamique des modèles selon l'abonnement
|
||||||
|
if premium:
|
||||||
|
if type_modele == "openrouter_code":
|
||||||
|
nom_modele_api = "anthropic/claude-3.5-sonnet"
|
||||||
|
nom_affichage = "OpenRouter (Claude 3.5 Sonnet - Premium)"
|
||||||
|
couleur = "#d97757" # Orange Anthropic
|
||||||
|
else:
|
||||||
|
nom_modele_api = "openai/gpt-4o-mini"
|
||||||
|
nom_affichage = "OpenRouter (GPT-4o Mini - Premium)"
|
||||||
|
couleur = "#10a37f" # Vert OpenAI
|
||||||
|
else:
|
||||||
if type_modele == "openrouter_code":
|
if type_modele == "openrouter_code":
|
||||||
nom_modele_api = "qwen/qwen-2.5-coder-32b-instruct:free"
|
nom_modele_api = "qwen/qwen-2.5-coder-32b-instruct:free"
|
||||||
nom_affichage = "OpenRouter (Qwen Coder - Gratuit)"
|
nom_affichage = "OpenRouter (Qwen Coder - Gratuit)"
|
||||||
couleur = "#10a37f" # Vert émeraude
|
couleur = "#4a90e2" # Bleu clair
|
||||||
else:
|
else:
|
||||||
nom_modele_api = "meta-llama/llama-3.1-8b-instruct:free"
|
nom_modele_api = "meta-llama/llama-3.1-8b-instruct:free"
|
||||||
nom_affichage = "OpenRouter (Llama 3.1 - Gratuit)"
|
nom_affichage = "OpenRouter (Llama 3.1 - Gratuit)"
|
||||||
@@ -42,7 +52,7 @@ def interroger_openrouter(prompt, type_modele="openrouter_chat", max_retries=3):
|
|||||||
time.sleep(temps_attente)
|
time.sleep(temps_attente)
|
||||||
if tentative == max_retries - 1:
|
if tentative == max_retries - 1:
|
||||||
log_event("ERROR", "OPENROUTER", "Échec définitif suite aux limites de taux (429).")
|
log_event("ERROR", "OPENROUTER", "Échec définitif suite aux limites de taux (429).")
|
||||||
raise Exception("Service temporairement saturé. Veuillez réessayer.")
|
raise Exception("Service temporairement saturé sur OpenRouter. Veuillez réessayer.")
|
||||||
|
|
||||||
except openai.APIStatusError as e:
|
except openai.APIStatusError as e:
|
||||||
if e.status_code == 402:
|
if e.status_code == 402:
|
||||||
|
|||||||
+34
-12
@@ -11,6 +11,9 @@ 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
|
||||||
|
|
||||||
|
# Import du nouveau module de modération
|
||||||
|
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):
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
try:
|
try:
|
||||||
@@ -18,13 +21,30 @@ async def handle_chat_request(request, session):
|
|||||||
prompt = form.get("prompt", "")
|
prompt = form.get("prompt", "")
|
||||||
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")
|
||||||
session['current_chat_id'] = chat_id
|
|
||||||
|
|
||||||
|
# --- RÉCUPÉRATION DU CHOIX GRATUIT/PAYANT ---
|
||||||
|
mode_payant = form.get("mode_payant") == "on"
|
||||||
|
|
||||||
|
session['current_chat_id'] = chat_id
|
||||||
history = load_history()
|
history = load_history()
|
||||||
|
|
||||||
# Identification temporaire de l'utilisateur (en attendant le module de connexion complet)
|
# Identification
|
||||||
utilisateur_nom = session.get('user', 'xavier')
|
utilisateur_nom = session.get('user', 'xavier')
|
||||||
utilisateur_vip = utilisateur_nom.lower() in ["xavier", "céline", "anaëlle", "renan", "audric"]
|
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"
|
||||||
|
|
||||||
|
# --- 🚨 ÉTAPE 1 : MODÉRATION PRÉVENTIVE (Filtre Légal) 🚨 ---
|
||||||
|
est_illicite, raison = analyser_intention_illicite(prompt, None)
|
||||||
|
if est_illicite:
|
||||||
|
sanction = appliquer_sanction_et_archiver(utilisateur_nom, prompt, raison, ip_client)
|
||||||
|
return Div(
|
||||||
|
H3("⚠️ Avertissement de Sécurité"),
|
||||||
|
P("Votre requête enfreint les Conditions Générales d'Utilisation (Contenu illicite)."),
|
||||||
|
P(f"Sanction appliquée : {sanction}"),
|
||||||
|
P("Cet incident a été enregistré et transmis à l'administration."),
|
||||||
|
style="color:red; background:#300; padding:15px; border-radius:10px;"
|
||||||
|
)
|
||||||
|
|
||||||
# Extraction éphémère en RAM
|
# Extraction éphémère en RAM
|
||||||
file_summary = []
|
file_summary = []
|
||||||
@@ -36,13 +56,14 @@ async def handle_chat_request(request, session):
|
|||||||
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_event("INFO", "SYSTEM", f"Requête reçue. Fichiers joints : {liste_fichiers_str}")
|
log_mode = "PAYANT 👑" if mode_payant else "GRATUIT 🆓"
|
||||||
|
log_event("INFO", "SYSTEM", f"Requête reçue [{log_mode}]. Fichiers joints : {liste_fichiers_str}")
|
||||||
|
|
||||||
# L'IA Local (Qwen) orchestre la décision ! Focus sur l'efficience.
|
# 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 ou recherches web.
|
||||||
- "openrouter_chat" : requêtes générales, texte rapide, traduction, bureautique (Modèles gratuits).
|
- "openrouter_chat" : requêtes générales, texte rapide, traduction, bureautique.
|
||||||
- "openrouter_code" : programmation, scripts, mathématiques, logique (Modèles gratuits).
|
- "openrouter_code" : programmation, scripts, mathématiques, logique.
|
||||||
- "local" : salutations basiques, questions très simples.
|
- "local" : salutations basiques, questions très simples.
|
||||||
Renvoie STRICTEMENT un JSON : {{"intention": "Résumé...", "fournisseur": "gemini|openrouter_chat|openrouter_code|local"}}
|
Renvoie STRICTEMENT un JSON : {{"intention": "Résumé...", "fournisseur": "gemini|openrouter_chat|openrouter_code|local"}}
|
||||||
Requête : {prompt}"""
|
Requête : {prompt}"""
|
||||||
@@ -56,7 +77,6 @@ async def handle_chat_request(request, session):
|
|||||||
intention = decision.get('intention', 'Analyse')
|
intention = decision.get('intention', 'Analyse')
|
||||||
fournisseur = decision.get('fournisseur', 'openrouter_chat').lower()
|
fournisseur = decision.get('fournisseur', 'openrouter_chat').lower()
|
||||||
|
|
||||||
# Sécurité Absolue Fichiers -> Gemini
|
|
||||||
if file_names:
|
if file_names:
|
||||||
fournisseur = "gemini"
|
fournisseur = "gemini"
|
||||||
intention += " (Forcé Gemini via Sécurité Fichiers)"
|
intention += " (Forcé Gemini via Sécurité Fichiers)"
|
||||||
@@ -74,12 +94,15 @@ async def handle_chat_request(request, session):
|
|||||||
|
|
||||||
# --- VÉRIFICATION FINANCIÈRE PRÉALABLE ---
|
# --- VÉRIFICATION FINANCIÈRE PRÉALABLE ---
|
||||||
if "openrouter" in fournisseur:
|
if "openrouter" in fournisseur:
|
||||||
|
if mode_payant:
|
||||||
|
modele_estime = "anthropic/claude-3.5-sonnet" if fournisseur == "openrouter_code" else "openai/gpt-4o-mini"
|
||||||
|
else:
|
||||||
modele_estime = "qwen/qwen-2.5-coder-32b-instruct:free" if fournisseur == "openrouter_code" else "meta-llama/llama-3.1-8b-instruct:free"
|
modele_estime = "qwen/qwen-2.5-coder-32b-instruct:free" if fournisseur == "openrouter_code" else "meta-llama/llama-3.1-8b-instruct:free"
|
||||||
# Déclenche une exception bloquante si le solde est insuffisant ou nécessite une validation
|
|
||||||
verifier_et_valider_budget(full_prompt, modele_estime, utilisateur_vip)
|
verifier_et_valider_budget(full_prompt, modele_estime, utilisateur_vip)
|
||||||
|
|
||||||
# Délégation de l'exécution au Moteur d'IA (Cascade)
|
# Exécution
|
||||||
reponse, couleur, fournisseur_nom, modele = executer_requete_ia(fournisseur, full_prompt)
|
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": []}
|
||||||
@@ -89,7 +112,7 @@ 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 conformément aux règles de confidentialité.*"
|
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.*"
|
||||||
|
|
||||||
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})
|
||||||
@@ -100,7 +123,6 @@ async def handle_chat_request(request, session):
|
|||||||
|
|
||||||
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 critique dans handle_chat_request: {e}")
|
||||||
# Capture l'erreur de paiement pour avertir l'utilisateur dans l'UI
|
|
||||||
msg_erreur = str(e)
|
msg_erreur = str(e)
|
||||||
if "VALIDATION_PAIEMENT_REQUISE" in msg_erreur or "SOLDE_INSUFFISANT" in msg_erreur:
|
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"⚠️ Alerte Financière : {msg_erreur.split('|')[0]}", style="color:orange; background:#331a00; padding:15px; border-radius:10px;")
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
from modules.logger import log_event
|
||||||
|
from modules.ai_local import interroger_local_ollama
|
||||||
|
|
||||||
|
# On importe l'alerte mail pour prévenir l'Admin
|
||||||
|
try:
|
||||||
|
from modules.debug_mailer import envoyer_alerte_debug
|
||||||
|
except ImportError:
|
||||||
|
envoyer_alerte_debug = None
|
||||||
|
|
||||||
|
BASE_DATA_DIR = "/DATA/AppData/MULTI-IA-AETHAS38/Utilisateurs"
|
||||||
|
|
||||||
|
# Liste des Super Admins / Admins à prévenir
|
||||||
|
ADMINS_GLOBAUX = ["xavier", "céline"]
|
||||||
|
|
||||||
|
def analyser_intention_illicite(prompt, ai_orchestrateur="qwen2.5:3b"):
|
||||||
|
"""
|
||||||
|
Fait appel à l'IA locale pour analyser l'intention sans faire de zèle.
|
||||||
|
"""
|
||||||
|
prompt_juge = f"""Tu es le Juge de Sécurité de la plateforme logicielle Aethas38.
|
||||||
|
Ta mission est d'analyser la requête et de détecter UNIQUEMENT les intentions strictement illégales (pédopornographie, drogues, terrorisme, haine).
|
||||||
|
RÈGLE ANTI-ZÈLE : Ne bloque QUE l'intention malveillante. Laisse passer les questions juridiques légitimes, la météo, l'aide à l'écriture de fiction ou les infos neutres.
|
||||||
|
Renvoie STRICTEMENT un JSON valide : {{"illegal": true ou false, "raison": "Explication très courte ou vide"}}
|
||||||
|
Requête à analyser : {prompt}"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# On interroge Qwen en local
|
||||||
|
reponse, _, _, _ = interroger_local_ollama(prompt_juge, ai_orchestrateur)
|
||||||
|
|
||||||
|
# Extraction du JSON de la réponse
|
||||||
|
match = re.search(r'\{.*\}', reponse, re.DOTALL)
|
||||||
|
if match:
|
||||||
|
decision = json.loads(match.group(0))
|
||||||
|
if decision.get("illegal") is True:
|
||||||
|
return True, decision.get("raison", "Détection par l'IA locale (Juge Qwen)")
|
||||||
|
except Exception as e:
|
||||||
|
log_event("WARNING", "MODERATOR", f"Échec de l'analyse IA (Fallback activé) : {e}")
|
||||||
|
|
||||||
|
# Fallback classique si Qwen plante
|
||||||
|
mots_interdits = ["pédopornographie", "bombe", "terrorisme"]
|
||||||
|
if any(mot in prompt.lower() for mot in mots_interdits):
|
||||||
|
return True, "Détection par filtre de mots-clés (Secours)"
|
||||||
|
return False, ""
|
||||||
|
|
||||||
|
def appliquer_sanction_et_archiver(utilisateur_nom, prompt, raison, ip_client="Inconnue"):
|
||||||
|
"""
|
||||||
|
Gère les Strikes, archive la preuve sur le RAID 1 et alerte les AUTRES admins.
|
||||||
|
"""
|
||||||
|
user_dir = os.path.join(BASE_DATA_DIR, utilisateur_nom)
|
||||||
|
illegal_dir = os.path.join(user_dir, "Illegal")
|
||||||
|
dossier_sanctions = os.path.join(user_dir, "sanctions.json")
|
||||||
|
|
||||||
|
os.makedirs(illegal_dir, exist_ok=True)
|
||||||
|
|
||||||
|
strikes = 0
|
||||||
|
if os.path.exists(dossier_sanctions):
|
||||||
|
with open(dossier_sanctions, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
strikes = data.get("avertissements", 0)
|
||||||
|
|
||||||
|
strikes += 1
|
||||||
|
sanction_actuelle = "Avertissement"
|
||||||
|
if strikes >= 10: sanction_actuelle = "BAN DEFINITIF EN ATTENTE DE VALIDATION"
|
||||||
|
elif strikes >= 9: sanction_actuelle = "Ban 1 mois"
|
||||||
|
elif strikes >= 6: sanction_actuelle = "Ban 1 semaine"
|
||||||
|
elif strikes >= 3: sanction_actuelle = "Ban 1 jour"
|
||||||
|
|
||||||
|
with open(dossier_sanctions, "w", encoding="utf-8") as f:
|
||||||
|
json.dump({"avertissements": strikes, "derniere_sanction": sanction_actuelle, "date": str(datetime.now())}, f)
|
||||||
|
|
||||||
|
preuve_path = os.path.join(illegal_dir, f"infraction_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt")
|
||||||
|
with open(preuve_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"--- INFRACTION REPORTÉE ---\nUtilisateur: {utilisateur_nom}\nIP: {ip_client}\nDate: {datetime.now()}\nRaison: {raison}\n\n[PROMPT INTERCEPTÉ]\n{prompt}")
|
||||||
|
|
||||||
|
# LOGIQUE D'ALERTE : On exclut l'utilisateur s'il est lui-même admin
|
||||||
|
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}"
|
||||||
|
log_event("CRITICAL", "MODERATION", alerte_msg)
|
||||||
|
|
||||||
|
if envoyer_alerte_debug and admins_a_alerter:
|
||||||
|
# Envoi de l'email uniquement si la liste d'admins à alerter n'est pas vide
|
||||||
|
envoyer_alerte_debug("CRITICAL", f"TENTATIVE ILLICITE ({utilisateur_nom})", alerte_msg)
|
||||||
|
|
||||||
|
return sanction_actuelle
|
||||||
|
|
||||||
|
def sauvegarder_decision_admin(utilisateur_cible, admin_juge, decision, commentaire):
|
||||||
|
"""
|
||||||
|
Archive la décision prise par un admin avec commentaire libre.
|
||||||
|
"""
|
||||||
|
dossier = os.path.join(BASE_DATA_DIR, utilisateur_cible, "Illegal")
|
||||||
|
os.makedirs(dossier, exist_ok=True)
|
||||||
|
fichier = os.path.join(dossier, f"decision_admin_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt")
|
||||||
|
|
||||||
|
with open(fichier, "w", encoding="utf-8") as f:
|
||||||
|
f.write(f"--- DÉCISION ADMINISTRATIVE ---\nCible: {utilisateur_cible}\nJuge Admin: {admin_juge}\nDate: {datetime.now()}\nDécision: {decision}\nCommentaire: {commentaire}\n")
|
||||||
|
log_event("INFO", "MODERATION", f"L'admin {admin_juge} a statué sur {utilisateur_cible} : {decision}")
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
from modules.logger import log_event
|
||||||
|
|
||||||
|
# Base de données simulée des rôles (sera connectée à database.py lors de la création de l'espace connexion)
|
||||||
|
ROLES_UTILISATEURS = {
|
||||||
|
"xavier": "Super Admin",
|
||||||
|
"céline": "Admin",
|
||||||
|
"anaëlle": "VIP",
|
||||||
|
"renan": "VIP",
|
||||||
|
"audric": "VIP"
|
||||||
|
}
|
||||||
|
|
||||||
|
def obtenir_role(nom_utilisateur):
|
||||||
|
"""Retourne le rôle de l'utilisateur."""
|
||||||
|
return ROLES_UTILISATEURS.get(nom_utilisateur.lower(), "Utilisateur")
|
||||||
|
|
||||||
|
def est_vip_ou_plus(nom_utilisateur):
|
||||||
|
"""Vérifie si l'utilisateur est exonéré de marge commerciale."""
|
||||||
|
role = obtenir_role(nom_utilisateur)
|
||||||
|
return role in ["Super Admin", "Admin", "VIP"]
|
||||||
|
|
||||||
|
def formater_affichage_nom(nom_utilisateur):
|
||||||
|
"""Formate l'affichage pour l'interface : 'Nom (Rôle)'."""
|
||||||
|
role = obtenir_role(nom_utilisateur)
|
||||||
|
nom_propre = nom_utilisateur.capitalize()
|
||||||
|
return f"{nom_propre} ({role})"
|
||||||
|
|
||||||
|
def verifier_droits_attribution(nom_attribueur, nouveau_role):
|
||||||
|
"""
|
||||||
|
Logique métier de sécurité :
|
||||||
|
- Le Super Admin peut tout attribuer.
|
||||||
|
- L'Admin ne peut attribuer que le rôle VIP ou Utilisateur.
|
||||||
|
"""
|
||||||
|
role_attribueur = obtenir_role(nom_attribueur)
|
||||||
|
|
||||||
|
if role_attribueur == "Super Admin":
|
||||||
|
return True
|
||||||
|
|
||||||
|
if role_attribueur == "Admin" and nouveau_role in ["VIP", "Utilisateur"]:
|
||||||
|
return True
|
||||||
|
|
||||||
|
log_event("WARNING", "SECURITY", f"Tentative d'élévation de privilège non autorisée par {nom_attribueur} pour le rôle {nouveau_role}.")
|
||||||
|
return False
|
||||||
@@ -5,7 +5,6 @@ from modules.paypal import render_paypal_donation_widget
|
|||||||
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")
|
||||||
|
|
||||||
# Ajout du paramètre execution_time
|
|
||||||
def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, execution_time=None):
|
def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, execution_time=None):
|
||||||
time_badge = Span(f"⏱️ {execution_time}s", style="margin-right: 10px; color:#aaa; font-size:0.8em;") if execution_time else ""
|
time_badge = Span(f"⏱️ {execution_time}s", style="margin-right: 10px; color:#aaa; font-size:0.8em;") if execution_time else ""
|
||||||
|
|
||||||
@@ -13,7 +12,7 @@ def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, executi
|
|||||||
Span(f"🎯 {intention}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"),
|
Span(f"🎯 {intention}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"),
|
||||||
Span(f"🏢 {nom_fournisseur}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"),
|
Span(f"🏢 {nom_fournisseur}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"),
|
||||||
Span(f"🧠 {nom_modele}", style=f"margin-right: 10px; color:{couleur}; font-weight:bold; font-size:0.8em;"),
|
Span(f"🧠 {nom_modele}", style=f"margin-right: 10px; color:{couleur}; font-weight:bold; font-size:0.8em;"),
|
||||||
time_badge, # ⏱️ Injection du chronomètre ici
|
time_badge,
|
||||||
style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;"
|
style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;"
|
||||||
)
|
)
|
||||||
actions = Div(
|
actions = Div(
|
||||||
@@ -62,28 +61,45 @@ def render_main_page(chat_id):
|
|||||||
if messages_ui:
|
if messages_ui:
|
||||||
messages_ui.append(Script("setTimeout(renderMarkdown, 150);"))
|
messages_ui.append(Script("setTimeout(renderMarkdown, 150);"))
|
||||||
|
|
||||||
|
auto_refresh_script = Script('''
|
||||||
|
let serverDown = false;
|
||||||
|
setInterval(() => {
|
||||||
|
fetch('/favicon.ico', {cache: 'no-store', method: 'HEAD'})
|
||||||
|
.then(response => {
|
||||||
|
if (serverDown) { window.location.reload(); }
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
serverDown = true;
|
||||||
|
console.log("Serveur hors ligne (Mise à jour en cours)...");
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
''')
|
||||||
|
|
||||||
return Title("Aethas38 - Core"), Body(
|
return Title("Aethas38 - Core"), Body(
|
||||||
|
auto_refresh_script,
|
||||||
Div(
|
Div(
|
||||||
Div(
|
Div(
|
||||||
Img(src="/Logo/logo_aethas.svg", style="max-width: 80%; height: auto; display: block; margin: 0 auto 20px;"),
|
Img(src="/Logo/logo_aethas.svg", style="max-width: 80%; height: auto; display: block; margin: 0 auto 20px;"),
|
||||||
Div(
|
Div(
|
||||||
A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"),
|
A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"),
|
||||||
A("💾 Exporter JSON", href="/export_json", target="_blank", cls="export-btn"),
|
A("💾 Exporter JSON", href="/export_json", target="_blank", cls="export-btn"),
|
||||||
# Module de don modifiable par l'hébergeur
|
|
||||||
|
Div(
|
||||||
render_paypal_donation_widget(),
|
render_paypal_donation_widget(),
|
||||||
# Module inaltérable (Licence EULA Aethas38)
|
A("❤️ Soutenir le créateur", href="https://paypal.me/VotreLienIci", target="_blank", style="font-size: 0.8em; padding: 6px 12px; background-color: transparent; color: #00e5ff; border: 1px solid #00e5ff; border-radius: 6px; text-decoration: none; transition: 0.3s; font-weight: bold; text-align: center;"),
|
||||||
A("❤️ Soutenir le créateur de ce projet", href="https://www.paypal.com/donate/?hosted_button_id=2B6T9UESCN2W6", target="_blank", style="margin-left: 10px; padding: 10px; background-color: #e2e8f0; color: #1a202c; border-radius: 5px; text-decoration: none; font-weight: bold; border: 1px solid #cbd5e0;"),
|
style="display: flex; flex-direction: column; gap: 12px; align-items: center; margin-top: 20px;"
|
||||||
cls="nav-buttons"
|
),
|
||||||
|
cls="nav-buttons",
|
||||||
|
style="display: flex; flex-direction: column; gap: 10px;"
|
||||||
),
|
),
|
||||||
cls="sidebar-top"
|
cls="sidebar-top"
|
||||||
),
|
),
|
||||||
render_sidebar(),
|
render_sidebar(),
|
||||||
Div(P("👤 Admin (Xavier)", style="color: #aaa; margin: 0; font-size: 0.9em;"), cls="sidebar-footer"),
|
Div(P("👤 Xavier (Super Admin)", style="color: #aaa; margin: 0; font-size: 0.9em;"), cls="sidebar-footer"),
|
||||||
cls="sidebar"
|
cls="sidebar"
|
||||||
),
|
),
|
||||||
Div(
|
Div(
|
||||||
Div(*messages_ui, id="chat-history", cls="chat-container"),
|
Div(*messages_ui, id="chat-history", cls="chat-container"),
|
||||||
# 🔄 Ajout de l'estimation de temps dans le Loader
|
|
||||||
Div(Div(Div(cls="tech-spinner"), Span("Analyse en cours... (~5s à 10s)"), cls="tech-loader-container"), id="global-loader", cls="htmx-indicator", style="max-width: 1000px; margin: 0 auto; width: 100%; padding: 0 20px; box-sizing: border-box; padding-bottom: 120px;"),
|
Div(Div(Div(cls="tech-spinner"), Span("Analyse en cours... (~5s à 10s)"), cls="tech-loader-container"), id="global-loader", cls="htmx-indicator", style="max-width: 1000px; margin: 0 auto; width: 100%; padding: 0 20px; box-sizing: border-box; padding-bottom: 120px;"),
|
||||||
Div(
|
Div(
|
||||||
Form(
|
Form(
|
||||||
@@ -93,6 +109,13 @@ def render_main_page(chat_id):
|
|||||||
Div(
|
Div(
|
||||||
Label("+", _for="file-upload", cls="file-btn"),
|
Label("+", _for="file-upload", cls="file-btn"),
|
||||||
Input(type="file", name="files", id="file-upload", multiple=True, style="display:none;", onchange="handleFileSelection(this)"),
|
Input(type="file", name="files", id="file-upload", multiple=True, style="display:none;", onchange="handleFileSelection(this)"),
|
||||||
|
|
||||||
|
Label(
|
||||||
|
Input(type="checkbox", name="mode_payant", id="mode-payant-toggle", style="margin-right: 6px; accent-color: #00e5ff; cursor: pointer;"),
|
||||||
|
"👑 VIP/Payant",
|
||||||
|
style="color: #aaa; font-size: 0.85em; display: flex; align-items: center; margin-right: 15px; cursor: pointer; white-space: nowrap;"
|
||||||
|
),
|
||||||
|
|
||||||
Textarea(name="prompt", id="prompt-input", placeholder="Requête, lien ou glissez vos fichiers ici...", cls="input-field", rows="1", oninput="autoResize(this)"),
|
Textarea(name="prompt", id="prompt-input", placeholder="Requête, lien ou glissez vos fichiers ici...", cls="input-field", rows="1", oninput="autoResize(this)"),
|
||||||
Button("↑", type="submit", cls="btn-submit"),
|
Button("↑", type="submit", cls="btn-submit"),
|
||||||
cls="input-row"
|
cls="input-row"
|
||||||
@@ -108,3 +131,39 @@ def render_main_page(chat_id):
|
|||||||
cls="main-content"
|
cls="main-content"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def render_admin_moderation_panel(utilisateur_cible):
|
||||||
|
"""
|
||||||
|
Composant UI exclusif aux Admins pour statuer sur les infractions avec commentaire libre.
|
||||||
|
"""
|
||||||
|
return Div(
|
||||||
|
H3(f"⚖️ Tribunal Admin - Dossier de : {utilisateur_cible}", style="color: #ff4444;"),
|
||||||
|
P("Loi applicable : Aucun utilisateur n'est dispensé du respect de la loi."),
|
||||||
|
Form(
|
||||||
|
Input(type="hidden", name="cible", value=utilisateur_cible),
|
||||||
|
Div(
|
||||||
|
Label("Action décidée :", style="font-weight: bold;"),
|
||||||
|
Select(
|
||||||
|
Option("Avertissement simple", value="Avertissement"),
|
||||||
|
Option("Bannissement 1 Jour", value="Ban 1J"),
|
||||||
|
Option("Bannissement 1 Semaine", value="Ban 1Sem"),
|
||||||
|
Option("Bannissement 1 Mois", value="Ban 1M"),
|
||||||
|
Option("Bannissement Définitif & Signalement PHAROS", value="Ban Definitif + Pharos"),
|
||||||
|
Option("Faux positif (Levée de sanction)", value="Annulation"),
|
||||||
|
name="decision", cls="input-field"
|
||||||
|
),
|
||||||
|
style="margin-bottom: 15px;"
|
||||||
|
),
|
||||||
|
Div(
|
||||||
|
Label("Commentaire libre de l'Admin (Preuve d'audit) :", style="font-weight: bold;"),
|
||||||
|
Textarea(name="commentaire", rows="4", cls="input-field", placeholder="Raison de la décision, contexte..."),
|
||||||
|
style="margin-bottom: 15px;"
|
||||||
|
),
|
||||||
|
Button("Valider la décision et archiver", type="submit", cls="btn-submit", style="background-color: #ff4444; color: white;"),
|
||||||
|
hx_post="/admin_decision", # Nouvelle route backend à créer plus tard
|
||||||
|
hx_target="#admin-result",
|
||||||
|
style="background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333;"
|
||||||
|
),
|
||||||
|
Div(id="admin-result", style="margin-top: 15px; color: #00e5ff;"),
|
||||||
|
cls="admin-panel"
|
||||||
|
)
|
||||||
+1
-10
@@ -1,22 +1,13 @@
|
|||||||
# --- Serveur & Interface ---
|
|
||||||
python-fasthtml
|
python-fasthtml
|
||||||
uvicorn
|
uvicorn
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
|
||||||
# --- Moteurs IA ---
|
|
||||||
openai
|
openai
|
||||||
google-generativeai
|
google-genai
|
||||||
|
|
||||||
# --- Utilitaires & Web ---
|
|
||||||
requests
|
requests
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
|
|
||||||
# --- Comptabilité & Export ---
|
|
||||||
pandas
|
pandas
|
||||||
openpyxl
|
openpyxl
|
||||||
reportlab
|
reportlab
|
||||||
|
|
||||||
# --- Sécurité & Monitoring (AJOUTS 18/08/2026) ---
|
|
||||||
psutil
|
psutil
|
||||||
cryptography
|
cryptography
|
||||||
bcrypt
|
bcrypt
|
||||||
|
|||||||
Reference in new issue
Block a user