Compare commits

..
27 Commits
Author SHA1 Message Date
xavier 134312ebff feat maj fonctionnement
Build and Push Docker Image / build-and-push (push) Successful in 40s
2026-10-08 17:12:39 +02:00
xavier 83e70eadf6 feat: intégration du découpage en morceaux (chunking), pauses séquentielles anti 429 et plugin de compression de contexte OpenRouter
Build and Push Docker Image / build-and-push (push) Successful in 47s
2026-10-08 11:55:10 +02:00
xavier 60c0699c57 feat: intégration du workflow map-reduce parallèle pour le pré-traitement des fichiers joints, correction de session (7 jours) et redirection auto sur 401
Build and Push Docker Image / build-and-push (push) Successful in 55s
2026-10-08 11:31:42 +02:00
xavier 44bf0b868e feat: intégration du parsing backend (PDF, Excel, VBA) et gestion sécurisée des champs prompteur/concaténeur vides dans les templates
Build and Push Docker Image / build-and-push (push) Successful in 41s
2026-10-08 10:25:02 +02:00
xavier c779c51a03 fix: renforcement drastique de la regex du parseur .md et injection automatique de modèle de secours pour empêcher les cases vides
Build and Push Docker Image / build-and-push (push) Successful in 47s
2026-10-08 09:52:53 +02:00
xavier eaa0ff0564 feat: ajout de l'import de configuration IA via fichier .md et bouton de téléchargement du template
Build and Push Docker Image / build-and-push (push) Successful in 44s
2026-10-08 09:35:13 +02:00
xavier ecd9854527 feat: passage des prompt en traduction anglaise. Mise en place du téléchargement d'un *.md des modèles. Traduction de la description des modèles en Français.
Build and Push Docker Image / build-and-push (push) Successful in 43s
2026-10-08 09:24:04 +02:00
xavier f840561123 feat: support de 10 fichiers max avec destruction post-envoi, traduction automatique en anglais par le prompteur et traduction en français par le concaténeur en préservant le code
Build and Push Docker Image / build-and-push (push) Successful in 41s
2026-10-08 09:06:12 +02:00
xavier 48f929a8f3 feat: version v0.17.2 avec gestion intégrée et sécurisée des pièces jointes, destruction post-envoi et persistance des choix de modèles par filtres locaux
Build and Push Docker Image / build-and-push (push) Successful in 42s
2026-10-08 08:47:03 +02:00
xavier fd82d77709 fix: correction de la troncature du fichier dashboard.html et restauration complète de l'interpréteur Vue.js
Build and Push Docker Image / build-and-push (push) Failing after 30s
2026-10-07 21:37:19 +02:00
xavier 189a2e4bf6 feat: intégration de la barre de sélection tabulaire style grille (Payant/Gratuit, 5 Travailleurs, Prompteur, Concaténeur)
Build and Push Docker Image / build-and-push (push) Successful in 45s
2026-10-07 21:28:59 +02:00
xavier e322435bd8 fix: restauration du conteneur de chat, de la zone de texte et du sélecteur multi-agents par requête
Build and Push Docker Image / build-and-push (push) Successful in 43s
2026-10-07 21:15:59 +02:00
xavier efbecada77 docs: mise à jour complète du README pour refléter l'état de la v0.16.0 (super-admin, multi-fournisseurs, finances, avatars)
Build and Push Docker Image / build-and-push (push) Successful in 45s
2026-10-07 21:11:53 +02:00
xavier 4a9a0d8ec6 docs: mise à jour complète du README pour refléter l'état de la v0.16.0 (super-admin, multi-fournisseurs, finances, avatars)
Build and Push Docker Image / build-and-push (push) Successful in 43s
2026-10-07 21:10:10 +02:00
xavier b554c983a6 fix: sécurisation des insertions de modèles via dictionnaire temporaire pour éviter les Duplicate Key
Build and Push Docker Image / build-and-push (push) Successful in 47s
2026-10-07 19:40:09 +02:00
xavier 007e723e4e feat: intégration extraction universelle multi-fournisseurs avec timeout 90s et routage dynamique
Build and Push Docker Image / build-and-push (push) Successful in 43s
2026-10-07 19:29:17 +02:00
xavier 95125df3b2 fix: sécurisation extraction API, affichage permanent du solde dans le menu latéral et maj financière post-requête
Build and Push Docker Image / build-and-push (push) Successful in 45s
2026-10-07 19:16:52 +02:00
xavier 36f205a56e fix: ajout de la dépendance pytz manquante pour le planificateur CRON
Build and Push Docker Image / build-and-push (push) Successful in 47s
2026-10-07 18:45:37 +02:00
xavier 2eb3acceea feat: refonte bdd, ajout super-admin, pages de config par niveau, avatar natif et sélecteur de modèles par requête
Build and Push Docker Image / build-and-push (push) Successful in 42s
2026-10-07 18:39:11 +02:00
xavier 4d5046e8d0 feat: ajout des exports de conversation globaux et du parseur de code pour l'extraction ciblée
Build and Push Docker Image / build-and-push (push) Successful in 42s
2026-10-07 18:10:32 +02:00
xavier ba2f37cf59 fix: réorganisation des fonctions dans main.py pour corriger le NameError sur get_current_user
Build and Push Docker Image / build-and-push (push) Successful in 44s
2026-10-07 17:14:04 +02:00
xavier 4805228f28 fix: résolution des conflits d'imports et complétion des interfaces manquantes pour la v0.12.0
Build and Push Docker Image / build-and-push (push) Successful in 42s
2026-10-07 16:59:14 +02:00
xavier 6e4aee1984 feat: intégration du formulaire de modification du mot de passe utilisateur via le tableau de bord
Build and Push Docker Image / build-and-push (push) Successful in 41s
2026-10-07 16:42:38 +02:00
xavier b6765a9bc8 Maj readme
Build and Push Docker Image / build-and-push (push) Successful in 40s
2026-10-07 16:19:28 +02:00
xavier efdda0ed00 fix: ajout de la dépendance httpx manquante pour le module asynchrone
Build and Push Docker Image / build-and-push (push) Successful in 47s
2026-10-07 15:54:42 +02:00
xavier 8a32c16ff4 feat: implémentation de l'orchestrateur asynchrone multi-agents (prompteur/travailleurs/concaténeur) et API de synchronisation des modèles
Build and Push Docker Image / build-and-push (push) Successful in 42s
2026-10-07 15:39:24 +02:00
xavier 4ce11d0bfe fix: mise à jour de l'identifiant du modèle Gemini vers gemini-3.5-flash-lite
Build and Push Docker Image / build-and-push (push) Successful in 40s
2026-10-07 15:17:03 +02:00
8 changed files with 1445 additions and 393 deletions

No files matched your search

+63 -41
View File
@@ -5,56 +5,78 @@ Ce projet est un laboratoire d'intelligence artificielle personnel. Il agit comm
L'interface visuelle est inspirée de l'ergonomie de Gemini, avec le logo stylisé A38 (cerveau en réseau neuronal néon) comme charte graphique centrale.
## 🏗️ Stack Technique & Architecture
- **Backend :** FastAPI (Python) modulaire (une fonction = un module).
- **Frontend :** Vue.js + Tailwind CSS (Responsive / Mobile-first).
- **Base de Données :** PostgreSQL (gestion des utilisateurs, historiques, suivi financier).
- **Traitement Documentaire :** LibreOffice Headless (génération/conversion Excel, Word, PDF).
- **Déploiement :** Docker, géré via Dockge et Gitea, sécurisé sur grappe RAID.
* **Backend :** FastAPI (Python) modulaire asynchrone (httpx, asyncio)[cite: 2].
* **Frontend :** Vue.js + Tailwind CSS (Responsive / Mobile-first)[cite: 2].
* **Base de Données :** PostgreSQL (gestion des utilisateurs, rôles hiérarchiques Super-Admin/Admin/User, historiques, suivi financier)[cite: 2].
* **Traitement Documentaire :** LibreOffice Headless (génération/conversion Excel, Word, PDF)[cite: 2].
* **Déploiement :** Docker, géré via Dockge et Gitea, sécurisé sur grappe RAID[cite: 2].
## 📌 Règles de Versioning (SemVer)
Le développement suit un versioning strict (format `vX.Y.Z`) synchronisé avec les tags Gitea et GitHub, affiché dynamiquement sur l'interface :
- **Majeur (X) :** Changement profond d'architecture ou refonte globale.
- **Mineur (Y) :** Validation et ajout d'une nouvelle fonctionnalité de la roadmap.
- **Patch (Z) :** Corrections de bugs, correctifs de sécurité ou ajustements visuels mineurs.
Le développement suit un versioning strict (format `vX.Y.Z`) synchronisé avec les tags Gitea et GitHub, affiché dynamiquement sur l'interface[cite: 2]:
* **Majeur (X) :** Changement profond d'architecture ou refonte globale[cite: 2].
* **Mineur (Y) :** Validation et ajout d'une nouvelle fonctionnalité de la roadmap[cite: 2].
* **Patch (Z) :** Corrections de bugs, correctifs de sécurité ou ajustements visuels mineurs[cite: 2].
## 📦 Récupération des Paquets Docker (Tags)
Les images Docker sont construites automatiquement par les pipelines CI/CD lors de la création d'un tag[cite: 2].
* **Sur Gitea :** Depuis la page principale de votre dépôt, naviguez dans l'onglet **Packages** (ou Paquets). Vous y trouverez le registre de conteneurs avec toutes les versions taguées (`v0.16.1`, `latest`, etc.)[cite: 2].
* **Sur GitHub :** Depuis la page principale du dépôt, regardez dans la colonne de droite la section **Packages**. Cliquez dessus pour accéder au GitHub Container Registry (`ghcr.io/votre-utilisateur/aethas38-multi-ia`) et visualiser la liste des tags disponibles à tirer via Docker/Dockge[cite: 2].
## 🚀 ROADMAP & CAHIER DES CHARGES
### Sécurité & Infrastructure
- [x] Installation stricte via Dockge et Gitea (création automatique des dossiers inclus).
- [x] Sécurisation de la totalité du projet (fichiers, BDD PostgreSQL) sur le stockage RAID.
- [x] Mots de passe cryptés (bcrypt), protection anti-brute force.
- [x] Authentification 2FA obligatoire (Keepassium, Authenticator, etc.).
- [x] Déconnexion automatique (timer invisible / session JWT) après 60 minutes d'inactivité.
- [x] Configuration de GitHub Actions / Gitea Actions pour la création automatique des paquets Docker (Tags SemVer + latest).
- [x] Horodatage UTC en base de données pour immunité au changement d'heure français (Prévention crash 2FA).
- [ ] Procédure de réinitialisation/modification du mot de passe (Admin et Utilisateurs).
* [x] Installation stricte via Dockge et Gitea (création automatique des dossiers inclus)[cite: 2].
* [x] Sécurisation de la totalité du projet (fichiers, BDD PostgreSQL) sur le stockage RAID[cite: 2].
* [x] Mots de passe cryptés (bcrypt), protection anti-brute force[cite: 2].
* [x] Authentification 2FA obligatoire (Keepassium, Authenticator, etc.)[cite: 2].
* [x] Déconnexion automatique (timer invisible / session JWT) après 60 minutes d'inactivité[cite: 2].
* [x] Configuration de GitHub Actions / Gitea Actions pour la création automatique des paquets Docker (Tags SemVer + latest)[cite: 2].
* [x] Horodatage UTC en base de données pour immunité au changement d'heure français (Prévention crash 2FA)[cite: 2].
* [x] Rotation automatique des logs système : génération d'un fichier log par jour avec purge stricte des fichiers de plus de 7 jours[cite: 2].
* [x] Procédure de modification sécurisée du mot de passe (Utilisateurs et Admins)[cite: 2].
### Administration & Gestion des Utilisateurs
- [x] Assistant de première installation (création du compte admin complet).
- [ ] Refonte du setup initial (Super Admin) : Obligation de configurer le serveur mail (aide opérateurs FR/Gmail) et proposition de saisie des clés API.
- [ ] Intégration de la roue crantée (Paramètres) en bas de la barre latérale.
- [ ] Page de configuration Utilisateur : changement d'email (avec validation via lien envoyé) et de mot de passe.
- [ ] Page de configuration Admin : ajout/modification de fournisseurs, clés API, et serveur mail en permanence.
- [ ] Hiérarchie des rôles : Un Admin peut proposer l'élévation d'un User en Admin (formulaire soumis à la validation exclusive du Super Admin).
- [ ] Export de l'activité Admin : Fichier Excel avec KPI, graphiques "camembert", et suivi financier par utilisateur.
- [ ] Suivi financier en temps réel et vérification du budget API.
- [ ] Envoi de rapport d'activité automatisé chaque jeudi à 04h00.
* [x] Assistant de première installation : l'initiateur du projet se voit attribuer automatiquement le rôle exclusif de **Super-Admin** (un seul par projet)[cite: 2].
* [x] Refonte du setup initial (Super Admin) : configuration du serveur mail (opérateurs FR/Gmail) et paramétrage complet des clés fournisseurs IA[cite: 2].
* [x] Intégration de la roue crantée (Paramètres) en bas de la barre latérale[cite: 2].
* [x] Page de configuration par niveau (Super-Admin, Admin, Utilisateur)[cite: 2].
* [x] Upload et personnalisation dynamique d'avatar utilisateur[cite: 2].
* [ ] Hiérarchie des rôles : Un Admin peut proposer l'élévation d'un User en Admin (formulaire soumis à la validation exclusive du Super Admin)[cite: 2].
* [ ] Export de l'activité Admin : Fichier Excel avec KPI, graphiques "camembert", et suivi financier par utilisateur[cite: 2].
* [x] Suivi financier en temps réel et vérification du budget API (mis à jour après chaque requête et affiché de manière non invasive sous le nom)[cite: 2].
* [ ] Envoi de rapport d'activité automatisé chaque jeudi à 04h00[cite: 2].
### Expérience Utilisateur (UI/UX)
- [x] Thème moderne (Gemini-like) et 100% compatible smartphones.
- [x] Refonte de la gestion des projets : séparation Épinglés (en haut) / Non Épinglés (Récents).
- [x] Arrêt de la création automatique de nouveau projet à l'ouverture.
- [x] Intégration permanente du logo AETHAS38 en local (Setup / Login / Dashboard).
- [ ] Indicateur visuel (Loader/Animation) signalant que l'IA AETHAS38 "réfléchit" ou travaille.
- [ ] Fonctionnalité de renommage manuel des discussions/projets.
* [x] Thème moderne (Gemini-like) et 100% compatible smartphones[cite: 2].
* [x] Refonte de la gestion des projets : séparation Épinglés (en haut) / Non Épinglés (Récents)[cite: 2].
* [x] Arrêt de la création automatique de nouveau projet à l'ouverture[cite: 2].
* [x] Intégration permanente du logo AETHAS38 en local (Setup / Login / Dashboard)[cite: 2].
* [x] Indicateur visuel (Loader/Animation) signalant que l'IA AETHAS38 "réfléchit" ou travaille[cite: 2].
* [x] Fonctionnalité de renommage manuel des discussions/projets[cite: 2].
* [ ] Affichage natif et fluide des images directement dans le flux de la discussion[cite: 2].
* [ ] Système de partage des réponses sur les réseaux et messageries externes (Facebook, WhatsApp, etc.)[cite: 2].
### Moteur IA, Modèles & Fonctionnalités
- [ ] Pipeline multi-agents : 1 Rédacteur de prompt, jusqu'à 5 Travailleurs, 1 Concaténeur final.
- [ ] Extraction automatisée des modèles disponibles chez les fournisseurs (sauvegarde txt horodatée ex: "07/10/2026 à 10h00").
- [ ] Tâche planifiée (CRON) pour la mise à jour des modèles toutes les 12h (le timer se reset si mise à jour manuelle).
- [ ] Classement, filtrage (Gratuit/Payant) et catégorisation des modèles (Texte, Code, Image, Audio, Vidéo).
- [ ] Upload manuel du fichier d'extraction des modèles avec intégration dans l'interface filtrable.
- [ ] Traduction automatisée des descriptions des modèles via Ollama (local) ou Gemini.
- [ ] Interface d'aide à la décision pour le choix des modèles selon la tâche souhaitée.
- [ ] Mécanisme de fallback / termes génériques pour les appels API (protection contre le dépassement des quotas Gemini).
- [ ] Téléchargement des requêtes, réponses et fichiers générés (Excel, PDF, TXT, Images).
* [x] Cœur asynchrone pour la parallélisation des appels API (module `orchestrator.py`)[cite: 2].
* [x] Pipeline multi-agents complet avec sélection dynamique par requête :
* Sélection de 1 à 5 travailleurs IA en parallèle[cite: 2].
* Si un seul travailleur est sélectionné, bypass automatique du prompteur et du concaténeur[cite: 2].
* Si > 1 travailleur, activation de la chaîne complète (1 Prompteur ➔ N Travailleurs ➔ 1 Concaténeur)[cite: 2].
* [x] Extraction universelle multi-fournisseurs (OpenRouter, Groq, DeepSeek, Mistral, Gemini, Cloudflare) avec timeout porté à 90 secondes[cite: 2].
* [x] Tâche planifiée (CRON) pour la mise à jour des modèles toutes les 12h (00h00 et 12h00)[cite: 2].
* [x] Page spécifique dédiée aux modèles permettant l'export, l'import manuel d'un fichier d'extraction JSON, ainsi que le filtrage par prix, domaine et mots-clés[cite: 2].
* [x] Extraction unitaire des retours IA et téléchargement global d'une conversation (formats TXT et JSON)[cite: 2].
* [x] Détection et téléchargement ciblé de pièces jointes ou blocs spécifiques (Code brut, scripts, fichiers de données) générés par l'IA en respectant le nommage exact demandé[cite: 2].
* [x] Classement, filtrage (Gratuit/Payant) et catégorisation des modèles (Texte, Code, Vision, Audio)[cite: 2].
* [ ] Traduction automatisée des descriptions des modèles via Ollama (local) ou Gemini[cite: 2].
* [ ] Interface d'aide à la décision pour le choix des modèles selon la tâche souhaitée[cite: 2].
* [ ] Mécanisme de fallback / termes génériques pour les appels API (protection contre le dépassement des quotas)[cite: 2].
+4 -4
View File
@@ -10,20 +10,20 @@ def get_ai_response(messages: list, settings: SystemSettings) -> str:
formatted_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
try:
# 1. Priorité absolue : Test Gemini direct (Rapide, sans intermédiaire)
# 1. Priorité absolue : Test Gemini direct
if settings.gemini_api_key:
client = OpenAI(
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
api_key=settings.gemini_api_key
)
# Utilisation de l'identifiant standard API pour la version Flash
model = "gemini-1.5-flash"
# Correction : Utilisation exacte du modèle 3.5 Flash-Lite
model = "gemini-3.5-flash-lite"
response = client.chat.completions.create(model=model, messages=formatted_messages)
# 2. Test OpenRouter (Fallback)
elif settings.openrouter_api_key:
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=settings.openrouter_api_key)
model = "google/gemini-1.5-pro" # Identifiant mis à jour et valide
model = "google/gemini-3.5-flash-lite"
response = client.chat.completions.create(
model=model,
messages=formatted_messages,
+275 -122
View File
@@ -1,99 +1,45 @@
from fastapi import FastAPI, Depends, HTTPException, status, Request, Response
from fastapi import FastAPI, Depends, HTTPException, status, Request, Response, UploadFile, File
from fastapi.responses import RedirectResponse, FileResponse
from fastapi.staticfiles import StaticFiles
from sqlalchemy.orm import Session
from typing import List
import os
import os, json, asyncio, shutil, base64, io, csv, zipfile
from datetime import datetime
import pytz
# Importation de nos modules locaux
from .database import engine, Base, get_db
from .database import engine, Base, get_db, SessionLocal
from .auth import get_password_hash, generate_totp_secret, get_totp_uri, verify_password, verify_totp, create_access_token, verify_token
from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse
from .models import User, Project, Message, SystemSettings
from .ai import get_ai_response
from .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse, PasswordChange, ModelReplacementRequest, LogRequest
from .models import User, Project, Message, SystemSettings, AIModel, FinancialLog
from .orchestrator import run_orchestrator, sync_providers_models, sync_finances, activity_logs, log_activity
# Création des tables dans la base de données
Base.metadata.create_all(bind=engine)
app = FastAPI(title="AETHAS38")
app = FastAPI(title="AETHAS38 - Orchestrateur Multi-IA")
# Configuration des fichiers statiques
assets_path = os.path.join(os.getcwd(), "frontend", "assets")
avatars_path = os.path.join(assets_path, "avatars")
os.makedirs(assets_path, exist_ok=True)
os.makedirs(avatars_path, exist_ok=True)
app.mount("/assets", StaticFiles(directory=assets_path), name="assets")
def is_setup_required(db: Session) -> bool:
admin = db.query(User).filter(User.is_admin == True).first()
return admin is None
async def scheduler_task():
tz = pytz.timezone('Europe/Paris')
while True:
now = datetime.now(tz)
if (now.hour == 0 or now.hour == 12) and now.minute == 0:
db = SessionLocal()
settings = db.query(SystemSettings).first()
if settings:
try: await sync_providers_models(db, settings, "Automatique")
except: pass
db.close()
await asyncio.sleep(60)
await asyncio.sleep(30)
@app.get("/")
def read_root(db: Session = Depends(get_db)):
if is_setup_required(db):
return RedirectResponse(url="/setup")
return RedirectResponse(url="/login")
@app.on_event("startup")
async def startup_event(): asyncio.create_task(scheduler_task())
@app.get("/setup")
def setup_page(db: Session = Depends(get_db)):
if not is_setup_required(db):
return RedirectResponse(url="/login")
frontend_path = os.path.join(os.getcwd(), "frontend", "index.html")
if not os.path.exists(frontend_path):
raise HTTPException(status_code=404, detail="Interface introuvable.")
return FileResponse(frontend_path)
@app.get("/login")
def login_page(db: Session = Depends(get_db)):
if is_setup_required(db):
return RedirectResponse(url="/setup")
frontend_path = os.path.join(os.getcwd(), "frontend", "login.html")
if not os.path.exists(frontend_path):
raise HTTPException(status_code=404, detail="Interface de connexion introuvable.")
return FileResponse(frontend_path)
@app.post("/api/setup")
def create_admin(admin_data: AdminCreate, db: Session = Depends(get_db)):
if not is_setup_required(db):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="L'installation a déjà été effectuée.")
hashed_pw = get_password_hash(admin_data.password)
totp_secret = generate_totp_secret()
new_admin = User(email=admin_data.email, username=admin_data.username, hashed_password=hashed_pw, totp_secret=totp_secret, is_admin=True)
db.add(new_admin)
new_settings = SystemSettings(
smtp_host=admin_data.smtp_host, smtp_port=admin_data.smtp_port, smtp_user=admin_data.smtp_user, smtp_password=admin_data.smtp_password,
openrouter_api_key=admin_data.openrouter_api_key, openrouter_management_key=admin_data.openrouter_management_key,
groq_api_key=admin_data.groq_api_key, gemini_api_key=admin_data.gemini_api_key, deepseek_api_key=admin_data.deepseek_api_key,
mistral_api_key=admin_data.mistral_api_key, cloudflare_account_id=admin_data.cloudflare_account_id, cloudflare_api_token=admin_data.cloudflare_api_token,
huggingface_api_key=admin_data.huggingface_api_key
)
db.add(new_settings)
db.commit()
db.refresh(new_admin)
return {"message": "Configuration terminée avec succès.", "totp_secret": totp_secret, "totp_uri": get_totp_uri(totp_secret, new_admin.username)}
@app.post("/api/login")
def login(login_data: LoginRequest, response: Response, db: Session = Depends(get_db)):
user = db.query(User).filter(User.username == login_data.username).first()
if not user or not verify_password(login_data.password, user.hashed_password):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Identifiants incorrects.")
if not verify_totp(user.totp_secret, login_data.totp_code):
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Code 2FA invalide.")
access_token = create_access_token(data={"sub": user.username})
response.set_cookie(key="session_token", value=access_token, httponly=True, max_age=3600, samesite="lax")
return {"message": "Connexion réussie"}
@app.get("/dashboard")
def dashboard(request: Request):
token = request.cookies.get("session_token")
if not token or not verify_token(token):
return RedirectResponse(url="/login")
frontend_path = os.path.join(os.getcwd(), "frontend", "dashboard.html")
return FileResponse(frontend_path)
# --- GESTION DES PROJETS ---
def is_setup_required(db: Session) -> bool: return db.query(User).filter(User.is_superadmin == True).first() is None
def get_current_user(request: Request, db: Session = Depends(get_db)):
token = request.cookies.get("session_token")
@@ -104,68 +50,275 @@ def get_current_user(request: Request, db: Session = Depends(get_db)):
if not user: raise HTTPException(status_code=401, detail="Utilisateur introuvable")
return user
@app.get("/")
def read_root(db: Session = Depends(get_db)): return RedirectResponse(url="/setup") if is_setup_required(db) else RedirectResponse(url="/login")
@app.get("/setup")
def setup_page(db: Session = Depends(get_db)): return RedirectResponse(url="/login") if not is_setup_required(db) else FileResponse(os.path.join(os.getcwd(), "frontend", "index.html"))
@app.get("/login")
def login_page(db: Session = Depends(get_db)): return RedirectResponse(url="/setup") if is_setup_required(db) else FileResponse(os.path.join(os.getcwd(), "frontend", "login.html"))
@app.post("/api/setup")
def create_admin(admin_data: AdminCreate, db: Session = Depends(get_db)):
if not is_setup_required(db): raise HTTPException(status_code=403, detail="Déjà installé.")
new_admin = User(email=admin_data.email, username=admin_data.username, hashed_password=get_password_hash(admin_data.password), totp_secret=generate_totp_secret(), is_admin=True, is_superadmin=True)
db.add(new_admin)
new_settings = SystemSettings(smtp_host=admin_data.smtp_host, smtp_port=admin_data.smtp_port, smtp_user=admin_data.smtp_user, smtp_password=admin_data.smtp_password, openrouter_api_key=admin_data.openrouter_api_key, openrouter_management_key=admin_data.openrouter_management_key, groq_api_key=admin_data.groq_api_key, gemini_api_key=admin_data.gemini_api_key, deepseek_api_key=admin_data.deepseek_api_key, mistral_api_key=admin_data.mistral_api_key, cloudflare_account_id=admin_data.cloudflare_account_id, cloudflare_api_token=admin_data.cloudflare_api_token, huggingface_api_key=admin_data.huggingface_api_key)
db.add(new_settings)
db.commit()
db.refresh(new_admin)
return {"message": "Succès", "totp_secret": new_admin.totp_secret, "totp_uri": get_totp_uri(new_admin.totp_secret, new_admin.username)}
@app.post("/api/login")
def login(login_data: LoginRequest, response: Response, db: Session = Depends(get_db)):
user = db.query(User).filter(User.username == login_data.username).first()
if not user or not verify_password(login_data.password, user.hashed_password): raise HTTPException(status_code=401, detail="Identifiants incorrects.")
if not verify_totp(user.totp_secret, login_data.totp_code): raise HTTPException(status_code=401, detail="2FA invalide.")
response.set_cookie(key="session_token", value=create_access_token(data={"sub": user.username}), httponly=True, max_age=604800, samesite="lax")
return {"message": "Connexion réussie"}
@app.get("/dashboard")
def dashboard(request: Request):
token = request.cookies.get("session_token")
if not token or not verify_token(token): return RedirectResponse(url="/login")
return FileResponse(os.path.join(os.getcwd(), "frontend", "dashboard.html"))
@app.get("/api/logs")
def get_logs():
return {"logs": activity_logs}
@app.post("/api/logs")
def add_frontend_log(req: LogRequest):
log_activity(f"[Système UI] {req.message}")
return {"status": "ok"}
@app.post("/api/models/suggest_replacement")
async def suggest_replacement(req: ModelReplacementRequest, db: Session = Depends(get_db)):
log_activity(f"⚠️ Modèle indisponible: {req.missing_model}. Demande de suggestion à Gemini...")
settings = db.query(SystemSettings).first()
if not settings or not settings.gemini_api_key:
log_activity("Clé Gemini non trouvée. Fallback forcé sur gemini-3.5-flash-lite.")
return {"suggestion": "gemini-3.5-flash-lite", "reason": "Clé API Gemini non configurée dans le système."}
models = db.query(AIModel).all()
available = [m.model_id for m in models]
prompt = f"Le modèle IA '{req.missing_model}' n'est plus disponible. Voici les modèles disponibles : {', '.join(available)}. Trouve le modèle le plus proche techniquement. Réponds UNIQUEMENT avec ce format strict : ID_DU_MODELE | Brève explication en français de 10 mots max. Si aucun ne correspond, renvoie gemini-3.5-flash-lite | Par défaut."
try:
from openai import AsyncOpenAI
client = AsyncOpenAI(base_url="https://generativelanguage.googleapis.com/v1beta/openai/", api_key=settings.gemini_api_key)
resp = await client.chat.completions.create(model="gemini-3.5-flash-lite", messages=[{"role": "user", "content": prompt}], max_tokens=50)
res = resp.choices[0].message.content.strip()
if "|" in res:
parts = res.split("|")
sugg = parts[0].strip()
reason = parts[1].strip()
else:
sugg = res.strip()
reason = "Sélectionné par Gemini."
if sugg not in available and sugg != "gemini-3.5-flash-lite":
sugg = "gemini-3.5-flash-lite"
reason = "Gemini a suggéré un modèle invalide. Fallback par défaut."
log_activity(f"✅ Remplacement trouvé : {req.missing_model} -> {sugg}")
return {"suggestion": sugg, "reason": reason}
except Exception as e:
log_activity(f"Erreur d'interrogation Gemini: {str(e)}. Fallback par défaut.")
return {"suggestion": "gemini-3.5-flash-lite", "reason": f"Erreur API."}
@app.get("/api/users/me")
def get_me(current_user: User = Depends(get_current_user)):
return {"username": current_user.username, "is_admin": current_user.is_admin, "is_superadmin": current_user.is_superadmin, "avatar_path": current_user.avatar_path}
@app.put("/api/users/me/password")
def change_password(passwords: PasswordChange, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
if not verify_password(passwords.old_password, current_user.hashed_password): raise HTTPException(status_code=400, detail="L'ancien mot de passe est incorrect.")
if len(passwords.new_password) < 8: raise HTTPException(status_code=400, detail="8 caractères minimum.")
current_user.hashed_password = get_password_hash(passwords.new_password)
db.commit()
return {"message": "Mot de passe mis à jour."}
@app.post("/api/users/me/avatar")
async def upload_avatar(file: UploadFile = File(...), db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
file_location = os.path.join(avatars_path, f"user_{current_user.id}.jpg")
with open(file_location, "wb") as buffer: shutil.copyfileobj(file.file, buffer)
current_user.avatar_path = f"/assets/avatars/user_{current_user.id}.jpg?v={int(datetime.now().timestamp())}"
db.commit()
return {"message": "Avatar mis à jour", "avatar_path": current_user.avatar_path}
@app.get("/api/projects", response_model=List[ProjectResponse])
def get_projects(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
return db.query(Project).filter(Project.user_id == current_user.id).order_by(Project.created_at.desc()).all()
def get_projects(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): return db.query(Project).filter(Project.user_id == current_user.id).order_by(Project.created_at.desc()).all()
@app.post("/api/projects", response_model=ProjectResponse)
def create_project(project: ProjectCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
new_project = Project(title=project.title, user_id=current_user.id)
db.add(new_project)
db.commit()
db.refresh(new_project)
return new_project
p = Project(title=project.title, user_id=current_user.id); db.add(p); db.commit(); db.refresh(p); return p
@app.put("/api/projects/{project_id}/rename", response_model=ProjectResponse)
def rename_project(project_id: int, project_data: ProjectRename, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
project = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first()
if not project: raise HTTPException(status_code=404, detail="Projet introuvable")
project.title = project_data.title
db.commit()
db.refresh(project)
return project
p = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first()
if not p: raise HTTPException(status_code=404)
p.title = project_data.title; db.commit(); db.refresh(p); return p
@app.put("/api/projects/{project_id}/pin", response_model=ProjectResponse)
def toggle_pin_project(project_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
project = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first()
if not project: raise HTTPException(status_code=404, detail="Projet introuvable")
project.is_pinned = not project.is_pinned
db.commit()
db.refresh(project)
return project
p = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first()
if not p: raise HTTPException(status_code=404)
p.is_pinned = not p.is_pinned; db.commit(); db.refresh(p); return p
@app.delete("/api/projects/{project_id}")
def delete_project(project_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
project = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first()
if not project: raise HTTPException(status_code=404, detail="Projet introuvable")
db.delete(project)
db.commit()
return {"message": "Projet supprimé"}
p = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first()
if not p: raise HTTPException(status_code=404)
db.delete(p); db.commit(); return {"message": "Supprimé"}
# --- GESTION DES MESSAGES ---
@app.get("/api/projects/{project_id}/export")
def export_project(project_id: int, format: str = "txt", db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
p = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first()
messages = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
if format == "json":
data = [{"role": m.role, "content": m.content, "date": m.created_at.isoformat()} for m in messages]
return Response(content=json.dumps(data, indent=2), media_type="application/json", headers={"Content-Disposition": f"attachment; filename=export_{project_id}.json"})
text = f"--- HISTORIQUE : {p.title} ---\n\n"
for m in messages: text += f"[{m.created_at.strftime('%Y-%m-%d %H:%M:%S')}] {'VOUS' if m.role == 'user' else 'AETHAS38'}:\n{m.content}\n\n{'-'*50}\n\n"
return Response(content=text, media_type="text/plain;charset=utf-8", headers={"Content-Disposition": f"attachment; filename=export_{project_id}.txt"})
@app.get("/api/projects/{project_id}/messages", response_model=List[MessageResponse])
def get_messages(project_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
project = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first()
if not project: raise HTTPException(status_code=404, detail="Projet introuvable")
return db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
def get_messages(project_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)): return db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
@app.post("/api/projects/{project_id}/messages", response_model=List[MessageResponse])
def create_message(project_id: int, message: MessageCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
project = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first()
if not project: raise HTTPException(status_code=404, detail="Projet introuvable")
async def create_message(project_id: int, message: MessageCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
extracted_files_data = []
files_names = []
user_message = Message(role=message.role, content=message.content, project_id=project_id)
db.add(user_message)
db.commit()
if message.files:
for f in message.files:
files_names.append(f.name)
content = f.content
if content.startswith("data:"):
try:
header, b64data = content.split(",", 1)
file_bytes = base64.b64decode(b64data)
ext = f.name.split('.')[-1].lower()
extracted_text = ""
if ext == 'pdf':
try:
import PyPDF2
reader = PyPDF2.PdfReader(io.BytesIO(file_bytes))
extracted_text = "\n".join([page.extract_text() for page in reader.pages if page.extract_text()])
except ImportError:
extracted_text = "[Erreur: L'administrateur doit exécuter 'pip install PyPDF2' sur le serveur pour lire les PDF.]"
elif ext in ['xls', 'xlsx', 'xlsm', 'xlsb']:
try:
import openpyxl
wb = openpyxl.load_workbook(io.BytesIO(file_bytes), data_only=False)
for sheet_name in wb.sheetnames:
sheet = wb[sheet_name]
extracted_text += f"\n--- Feuille : {sheet_name} ---\n"
for row in sheet.iter_rows(values_only=True):
row_vals = [str(cell) if cell is not None else "" for cell in row]
if any(row_vals):
extracted_text += "\t".join(row_vals) + "\n"
if ext in ['xlsm', 'xlsb', 'xls']:
try:
from oletools.olevba import VBA_Parser
vbaparser = VBA_Parser("filename", data=file_bytes)
if vbaparser.detect_vba_macros():
extracted_text += "\n\n--- MACROS VBA DETECTEES ---\n"
for (filename, stream_path, vba_filename, vba_code) in vbaparser.extract_macros():
extracted_text += f"\n// Module: {vba_filename}\n{vba_code}\n"
except ImportError:
extracted_text += "\n[Extraction VBA impossible: L'administrateur doit exécuter 'pip install oletools' sur le serveur.]\n"
except Exception as e:
extracted_text += f"\n[Erreur de lecture VBA interne: {str(e)}]\n"
except ImportError:
extracted_text = "[Erreur: L'administrateur doit exécuter 'pip install openpyxl' sur le serveur pour lire Excel.]"
else:
extracted_text = file_bytes.decode('utf-8', errors='replace')
extracted_files_data.append({"name": f.name, "content": extracted_text})
except Exception as e:
extracted_files_data.append({"name": f.name, "content": f"[ERREUR DE DECODAGE: {str(e)}]"})
else:
extracted_files_data.append({"name": f.name, "content": content})
db_content = message.content
if files_names:
db_content += f"\n\n[Fichiers joints pour analyse : {', '.join(files_names)}]"
db.add(Message(role=message.role, content=db_content, project_id=project_id))
db.commit()
history = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
settings = db.query(SystemSettings).first()
ai_response_text = get_ai_response(history, settings)
ai_message = Message(role="assistant", content=ai_response_text, project_id=project_id)
db.add(ai_message)
conf = message.config.dict() if message.config else {"workers": ["gemini-3.5-flash-lite"]}
ai_resp = await run_orchestrator(db, history, settings, conf, extracted_files_data)
db.add(Message(role="assistant", content=ai_resp, project_id=project_id))
db.commit()
return db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
return db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
# --- ROUTES MODÈLES & FINANCES ---
@app.get("/api/models/info")
def get_models_info(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
settings = db.query(SystemSettings).first()
return {
"last_sync_date": settings.last_sync_date.isoformat() if settings and settings.last_sync_date else None,
"last_sync_type": settings.last_sync_type if settings else None,
"models": db.query(AIModel).order_by(AIModel.name.asc()).all(),
"finances": db.query(FinancialLog).all()
}
@app.get("/api/finances")
def get_finances(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
return db.query(FinancialLog).all()
@app.post("/api/models/sync")
async def trigger_model_sync(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
if not current_user.is_admin: raise HTTPException(status_code=403, detail="Accès admin requis.")
settings = db.query(SystemSettings).first()
return await sync_providers_models(db, settings, "Manuelle")
@app.get("/api/models/export")
def export_models(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
if not current_user.is_admin: raise HTTPException(status_code=403, detail="Accès admin requis.")
models = db.query(AIModel).order_by(AIModel.provider.asc(), AIModel.name.asc()).all()
csv_io = io.StringIO()
writer = csv.writer(csv_io, delimiter=',')
writer.writerow(["Provider", "Model ID", "Name", "Domain", "Is Free", "Context Length", "Pricing Prompt", "Pricing Completion", "Description"])
for m in models:
writer.writerow([m.provider, m.model_id, m.name, m.domain, m.is_free, m.context_length, m.pricing_prompt, m.pricing_completion, m.description_fr])
md_content = f"# Extraction des Modèles IA - AETHAS38\n\n**Date d'extraction :** {datetime.now().strftime('%d/%m/%Y à %H:%M:%S')}\n\n"
providers = sorted(list(set(m.provider for m in models)))
for prov in providers:
md_content += f"## Fournisseur : {prov.upper()}\n\n"
prov_models = [m for m in models if m.provider == prov]
for m in prov_models:
price_info = "**GRATUIT**" if m.is_free else f"In: ${m.pricing_prompt:.2f} / Out: ${m.pricing_completion:.2f}"
ctx_info = f"{int(m.context_length/1000)}k"
desc = m.description_fr.replace('\n', ' ') if m.description_fr else ""
md_content += f"- **{m.name or m.model_id}** (`{m.model_id}`)\n"
md_content += f" - *Domaine :* {m.domain}\n"
md_content += f" - *Prix (1M tokens) :* {price_info}\n"
md_content += f" - *Contexte :* {ctx_info}\n"
md_content += f" - *Description :* {desc}\n\n"
zip_io = io.BytesIO()
with zipfile.ZipFile(zip_io, mode='w', compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr("models_export.csv", csv_io.getvalue().encode('utf-8'))
zf.writestr(f"{datetime.now().strftime('%Y%m%d')}-extraction-modeles.md", md_content.encode('utf-8'))
zip_io.seek(0)
return Response(
content=zip_io.getvalue(),
media_type="application/zip",
headers={"Content-Disposition": f"attachment; filename=aethas38_models_{datetime.now().strftime('%Y%m%d')}.zip"}
)
+10 -5
View File
@@ -11,6 +11,8 @@ class User(Base):
hashed_password = Column(String)
totp_secret = Column(String)
is_admin = Column(Boolean, default=False)
is_superadmin = Column(Boolean, default=False)
avatar_path = Column(String, nullable=True)
projects = relationship("Project", back_populates="owner")
class Project(Base):
@@ -48,24 +50,27 @@ class SystemSettings(Base):
cloudflare_account_id = Column(String, nullable=True)
cloudflare_api_token = Column(String, nullable=True)
huggingface_api_key = Column(String, nullable=True)
last_sync_date = Column(DateTime, nullable=True)
last_sync_type = Column(String, nullable=True)
class AIModel(Base):
"""Stocke la liste des modèles extraits depuis les fournisseurs."""
__tablename__ = "ai_models"
id = Column(Integer, primary_key=True, index=True)
provider = Column(String, index=True)
model_id = Column(String, unique=True, index=True)
name = Column(String)
description_fr = Column(String, nullable=True)
domain = Column(String, default="Texte")
is_free = Column(Boolean, default=False)
context_length = Column(Integer)
pricing_prompt = Column(Float)
pricing_completion = Column(Float)
last_updated = Column(DateTime, default=lambda: datetime.now(timezone.utc))
class FinancialLog(Base):
"""Suivi financier par fournisseur."""
__tablename__ = "financial_logs"
id = Column(Integer, primary_key=True, index=True)
provider = Column(String, index=True)
balance = Column(Float)
total_usage = Column(Float)
provider = Column(String, unique=True, index=True)
balance = Column(Float, default=0.0)
total_usage = Column(Float, default=0.0)
checked_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
+317
View File
@@ -0,0 +1,317 @@
import asyncio
import httpx
import urllib.parse
from openai import AsyncOpenAI
from fastapi import HTTPException
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError
from .models import SystemSettings, AIModel, FinancialLog
from datetime import datetime, timezone
# --- LOGGER GLOBAL POUR LE TERMINAL ---
activity_logs = []
def log_activity(msg: str):
ts = datetime.now(timezone.utc).strftime('%H:%M:%S')
activity_logs.append(f"[{ts}] {msg}")
if len(activity_logs) > 100:
activity_logs.pop(0)
# --------------------------------------
def determine_domain(model_id: str) -> str:
mid = model_id.lower()
if "vision" in mid or "vl" in mid or "omni" in mid: return "Vision & Texte"
if "coder" in mid or "code" in mid or "math" in mid: return "Code & Logique"
if "audio" in mid or "whisper" in mid: return "Audio"
return "Texte Polyvalent"
async def translate_en_to_fr(client: httpx.AsyncClient, text: str) -> str:
if not text: return "Aucune description fournie."
try:
short_text = text[:300].strip()
url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl=en&tl=fr&dt=t&q={urllib.parse.quote(short_text)}"
resp = await client.get(url, timeout=4.0)
if resp.status_code == 200:
translated = "".join([s[0] for s in resp.json()[0]])
return translated + ("..." if len(text) > 300 else "")
except Exception:
pass
return text[:200] + "..."
async def sync_finances(db: Session, settings: SystemSettings):
async with httpx.AsyncClient(timeout=30.0) as client:
if settings.openrouter_management_key or settings.openrouter_api_key:
try:
key = settings.openrouter_management_key or settings.openrouter_api_key
cred_resp = await client.get("https://openrouter.ai/api/v1/credits", headers={"Authorization": f"Bearer {key}"})
balance = 0.0
usage = 0.0
if cred_resp.status_code == 200 and cred_resp.json().get("data"):
c_data = cred_resp.json().get("data", {})
balance = float(c_data.get("total_credits") or 0.0) - float(c_data.get("total_usage") or 0.0)
usage = float(c_data.get("total_usage") or 0.0)
else:
key_resp = await client.get("https://openrouter.ai/api/v1/auth/key", headers={"Authorization": f"Bearer {key}"})
if key_resp.status_code == 200:
data = key_resp.json().get("data", {})
limit = data.get("limit")
usage = float(data.get("usage") or 0.0)
balance = (float(limit) - usage) if limit is not None else -usage
update_finance_db(db, "OpenRouter", balance, usage)
except Exception as e: print(f"Erreur Finance OR: {e}")
if settings.groq_api_key: update_finance_db(db, "Groq", 999.0, 0.0)
if settings.deepseek_api_key:
try:
resp = await client.get("https://api.deepseek.com/user/balance", headers={"Authorization": f"Bearer {settings.deepseek_api_key}"})
if resp.status_code == 200:
infos = resp.json().get("balance_infos", [{}])[0]
update_finance_db(db, "DeepSeek", float(infos.get("total_balance", 0)), 0.0)
except Exception: pass
if settings.mistral_api_key: update_finance_db(db, "Mistral", 0.0, 0.0)
if settings.gemini_api_key: update_finance_db(db, "Gemini", 0.0, 0.0)
try: db.commit()
except: db.rollback()
def update_finance_db(db, provider, balance, usage):
try:
log = db.query(FinancialLog).filter(FinancialLog.provider == provider).first()
if log:
log.balance = balance; log.total_usage = usage; log.checked_at = datetime.now(timezone.utc)
else:
db.add(FinancialLog(provider=provider, balance=balance, total_usage=usage))
except Exception as e: print(f"Finance DB Error: {e}")
async def sync_providers_models(db: Session, settings: SystemSettings, sync_type: str = "Automatique"):
log_activity(f"Lancement de la synchronisation des modèles ({sync_type})...")
added = 0
models_to_process = {}
async with httpx.AsyncClient(timeout=90.0) as client:
if settings.openrouter_api_key:
try:
resp = await client.get("https://openrouter.ai/api/v1/models")
if resp.status_code == 200:
for item in resp.json().get("data", []):
try:
m_id = item.get("id")
if not m_id: continue
pricing = item.get("pricing") or {}
pp = float(pricing.get("prompt") or 0.0) * 1000000
pc = float(pricing.get("completion") or 0.0) * 1000000
is_free = (pp == 0.0 and pc == 0.0)
desc_en = item.get("description", "Generic AI Model.")
models_to_process[m_id] = {"provider": "openrouter", "name": item.get("name", "Inconnu"), "desc_en": desc_en, "domain": determine_domain(m_id), "is_free": is_free, "ctx": item.get("context_length", 0), "pp": pp, "pc": pc}
except: pass
except Exception as e: print(f"Erreur OR Models: {e}")
if settings.groq_api_key:
try:
resp = await client.get("https://api.groq.com/openai/v1/models", headers={"Authorization": f"Bearer {settings.groq_api_key}"})
if resp.status_code == 200:
for item in resp.json().get("data", []):
m_id = item["id"]
models_to_process[m_id] = {"provider": "groq", "name": m_id.capitalize(), "desc": "Modèle très rapide hébergé sur LPU Groq.", "domain": determine_domain(m_id), "is_free": True, "ctx": 8192, "pp": 0.0, "pc": 0.0}
except Exception: pass
if settings.deepseek_api_key:
try:
resp = await client.get("https://api.deepseek.com/models", headers={"Authorization": f"Bearer {settings.deepseek_api_key}"})
if resp.status_code == 200:
for item in resp.json().get("data", []):
m_id = item["id"]
models_to_process[m_id] = {"provider": "deepseek", "name": m_id.capitalize(), "desc": "Modèle officiel du fournisseur DeepSeek.", "domain": determine_domain(m_id), "is_free": False, "ctx": 64000, "pp": 0.14, "pc": 0.28}
except Exception: pass
if settings.mistral_api_key:
try:
resp = await client.get("https://api.mistral.ai/v1/models", headers={"Authorization": f"Bearer {settings.mistral_api_key}"})
if resp.status_code == 200:
for item in resp.json().get("data", []):
m_id = item["id"]
models_to_process[m_id] = {"provider": "mistral", "name": m_id.capitalize(), "desc": "Modèle officiel développé par Mistral AI.", "domain": determine_domain(m_id), "is_free": False, "ctx": 32000, "pp": 0.2, "pc": 0.6}
except Exception: pass
if settings.gemini_api_key:
try:
resp = await client.get(f"https://generativelanguage.googleapis.com/v1beta/models?key={settings.gemini_api_key}")
if resp.status_code == 200:
for item in resp.json().get("models", []):
m_id = item["name"].replace("models/", "")
models_to_process[m_id] = {"provider": "gemini", "name": item.get("displayName", m_id), "desc": "Modèle natif de l'écosystème Google Gemini.", "domain": determine_domain(m_id), "is_free": True, "ctx": item.get("inputTokenLimit", 32000), "pp": 0.0, "pc": 0.0}
except Exception: pass
if settings.cloudflare_account_id and settings.cloudflare_api_token:
try:
url = f"https://api.cloudflare.com/client/v4/accounts/{settings.cloudflare_account_id}/ai/models/search"
resp = await client.get(url, headers={"Authorization": f"Bearer {settings.cloudflare_api_token}"})
if resp.status_code == 200:
for item in resp.json().get("result", []):
m_id = item.get("name")
models_to_process[m_id] = {"provider": "cloudflare", "name": m_id.split("/")[-1], "desc": "Modèle Serverless Cloudflare Workers AI.", "domain": determine_domain(m_id), "is_free": True, "ctx": 4096, "pp": 0.0, "pc": 0.0}
except Exception: pass
sem = asyncio.Semaphore(15)
async def process_and_translate(m_id, data, client_session):
async with sem:
if "desc_en" in data:
data["desc"] = await translate_en_to_fr(client_session, data["desc_en"])
return m_id, data
async with httpx.AsyncClient(timeout=30.0) as client_trans:
tasks = [process_and_translate(m_id, data, client_trans) for m_id, data in models_to_process.items()]
translated_results = await asyncio.gather(*tasks)
for m_id, data in translated_results:
try:
existing = db.query(AIModel).filter(AIModel.model_id == m_id).first()
if existing:
existing.pricing_prompt = data["pp"]; existing.pricing_completion = data["pc"]; existing.is_free = data["is_free"]; existing.description_fr = data["desc"]; existing.last_updated = datetime.now(timezone.utc)
else:
db.add(AIModel(provider=data["provider"], model_id=m_id, name=data["name"], description_fr=data["desc"], domain=data["domain"], is_free=data["is_free"], context_length=data["ctx"], pricing_prompt=data["pp"], pricing_completion=data["pc"]))
added += 1
if added % 50 == 0: db.commit()
except IntegrityError: db.rollback()
except Exception: db.rollback()
try:
settings.last_sync_date = datetime.now(timezone.utc)
settings.last_sync_type = sync_type
db.commit()
except Exception as e:
db.rollback()
await sync_finances(db, settings)
log_activity(f"Synchronisation terminée : {added} modèles analysés.")
return {"status": "success", "models_processed": added}
def get_client_for_model(db: Session, model_id: str, settings: SystemSettings):
model_db = db.query(AIModel).filter(AIModel.model_id == model_id).first()
provider = model_db.provider if model_db else "openrouter"
if provider == "gemini" and settings.gemini_api_key:
return AsyncOpenAI(base_url="https://generativelanguage.googleapis.com/v1beta/openai/", api_key=settings.gemini_api_key), "gemini"
elif provider == "groq" and settings.groq_api_key:
return AsyncOpenAI(base_url="https://api.groq.com/openai/v1", api_key=settings.groq_api_key), "groq"
elif provider == "deepseek" and settings.deepseek_api_key:
return AsyncOpenAI(base_url="https://api.deepseek.com/v1", api_key=settings.deepseek_api_key), "deepseek"
elif provider == "mistral" and settings.mistral_api_key:
return AsyncOpenAI(base_url="https://api.mistral.ai/v1", api_key=settings.mistral_api_key), "mistral"
elif provider == "cloudflare" and settings.cloudflare_account_id and settings.cloudflare_api_token:
return AsyncOpenAI(base_url=f"https://api.cloudflare.com/client/v4/accounts/{settings.cloudflare_account_id}/ai/v1", api_key=settings.cloudflare_api_token), "cloudflare"
return AsyncOpenAI(base_url="https://openrouter.ai/api/v1", api_key=settings.openrouter_api_key), "openrouter"
async def ask_agent(client, model_id, messages, provider="openrouter"):
kwargs = {"model": model_id, "messages": messages}
if provider == "openrouter":
kwargs["extra_headers"] = {"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "AETHAS38 Orchestrator"}
kwargs["extra_body"] = {"plugins": [{"id": "context-compression"}]}
resp = await client.chat.completions.create(**kwargs)
return resp.choices[0].message.content
async def run_orchestrator(db: Session, history: list, settings: SystemSettings, config: dict, extracted_files: list = None) -> str:
workers = config.get("workers", ["gemini-3.5-flash-lite"])
user_prompt = history[-1].content
original_user_text = user_prompt.split("\n\n[Fichiers joints")[0] if "[Fichiers joints" in user_prompt else user_prompt
formatted_history = [{"role": msg.role, "content": msg.content} for msg in history[:-1]]
final_response = ""
try:
p_mod = config.get("prompter")
if not p_mod: p_mod = "gemini-3.5-flash-lite"
log_activity(f"Démarrage de l'orchestration. Modèle Prompteur: {p_mod}")
p_client, p_prov = get_client_for_model(db, p_mod, settings)
# --- WORKFLOW MAP-REDUCE : PRÉ-TRAITEMENT SÉQUENTIEL & CHUNKING ---
files_context = ""
if extracted_files:
log_activity(f"Traitement Map-Reduce de {len(extracted_files)} fichier(s) joint(s).")
async def process_single_file(f):
file_sys = "You are an expert data analyst and senior developer. Extract the most important technical information from the file without losing critical code syntax."
content = f['content']
chunk_size = 150000
if len(content) > chunk_size:
chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
chunk_analyses = []
log_activity(f"Fichier lourd ({f['name']}): Chunking en {len(chunks)} morceaux.")
for idx, chunk in enumerate(chunks):
file_prompt = f"Demande de l'utilisateur : '{original_user_text}'.\n\nPartie {idx+1}/{len(chunks)} du fichier '{f['name']}'. Analysez, extrayez et résumez le code, VBA, SQL ou les données pertinentes.\n\nContenu :\n```\n{chunk}\n```"
try:
analysis = await ask_agent(p_client, p_mod, [{"role": "system", "content": file_sys}, {"role": "user", "content": file_prompt}], p_prov)
chunk_analyses.append(analysis)
log_activity(f"Analyse chunk {idx+1}/{len(chunks)} pour {f['name']} réussie.")
except Exception as e:
chunk_analyses.append(f"[Erreur sur la partie {idx+1}: {str(e)}]")
log_activity(f"Erreur chunk {idx+1}/{len(chunks)} pour {f['name']}: {str(e)}")
await asyncio.sleep(1.5)
return f"\n\n--- Extraction du fichier {f['name']} (en {len(chunks)} parties) ---\n" + "\n".join(chunk_analyses)
else:
file_prompt = f"Demande de l'utilisateur : '{original_user_text}'.\n\nAnalysez le fichier ci-dessous. Extrayez, résumez et conservez méticuleusement tout le code, les macros VBA, les requêtes SQL, ou les données métier pertinentes pour répondre à la demande.\n\nFichier : {f['name']}\nContenu :\n```\n{content}\n```"
try:
analysis = await ask_agent(p_client, p_mod, [{"role": "system", "content": file_sys}, {"role": "user", "content": file_prompt}], p_prov)
log_activity(f"Analyse intégrale de {f['name']} réussie.")
return f"\n\n--- Extraction du fichier {f['name']} ---\n{analysis}"
except Exception as e:
log_activity(f"Erreur d'analyse sur {f['name']}: {str(e)}")
return f"\n\n--- Erreur sur {f['name']} ---\n{str(e)}"
file_analyses = []
for f in extracted_files:
analysis = await process_single_file(f)
file_analyses.append(analysis)
await asyncio.sleep(1.5)
files_context = "".join(file_analyses)
user_prompt = f"{original_user_text}\n\nVoici les données pré-traitées des fichiers joints :\n{files_context}"
# --- OPTIMISATION & TRADUCTION ---
log_activity(f"Optimisation/Traduction de la requête via Prompteur...")
prompt_system = "You are an expert prompt engineer. Translate and optimize the user request and any file context into clear, precise English tailored for AI execution. Keep all code blocks intact."
optimized = await ask_agent(p_client, p_mod, [{"role": "system", "content": prompt_system}, {"role": "user", "content": user_prompt}], p_prov)
log_activity(f"Lancement de {len(workers)} travailleur(s) en parallèle...")
if len(workers) == 1:
w_mod = workers[0]
client, provider = get_client_for_model(db, w_mod, settings)
worker_response = await ask_agent(client, w_mod, formatted_history + [{"role": "user", "content": optimized}], provider)
responses = [worker_response]
log_activity(f"Travailleur 1 ({w_mod}) a terminé.")
else:
w_tasks = []
for w in workers:
w_client, w_prov = get_client_for_model(db, w, settings)
w_tasks.append(ask_agent(w_client, w, formatted_history + [{"role": "user", "content": optimized}], w_prov))
responses = await asyncio.gather(*w_tasks, return_exceptions=True)
log_activity("Tous les travailleurs ont terminé leur analyse.")
c_mod = config.get("concatenator")
if not c_mod: c_mod = "gemini-3.5-flash-lite"
c_client, c_prov = get_client_for_model(db, c_mod, settings)
log_activity(f"Synthèse et traduction finale via Concaténeur ({c_mod})...")
concat_system = (
"You are a master lead developer and technical synthesizer. "
"Synthesize the provided expert responses into a single cohesive response. "
"Translate all explanatory text, descriptions, and user-facing prose into natural French. "
"CRITICAL: Do NOT translate code blocks, programming keywords, or source code contents. "
"You may translate code comments into French if appropriate, but leave code syntax strictly intact."
)
synth = f"User Request: {original_user_text}\n\n" + "\n".join([f"--- EXPERT {i+1} ---\n{str(r)}" for i, r in enumerate(responses)])
final_response = await ask_agent(c_client, c_mod, [{"role": "system", "content": concat_system}, {"role": "user", "content": synth}], c_prov)
log_activity("Orchestration terminée avec succès.")
except Exception as e:
final_response = f"L'IA a rencontré une erreur critique: {str(e)}"
log_activity(f"ERREUR CRITIQUE: {str(e)}")
await sync_finances(db, settings)
return final_response
+26 -12
View File
@@ -1,17 +1,15 @@
from pydantic import BaseModel, EmailStr
from datetime import datetime
from typing import Optional
from typing import Optional, List
class AdminCreate(BaseModel):
email: EmailStr
username: str
password: str
# Serveur Mail (Obligatoire)
smtp_host: str
smtp_port: int
smtp_user: str
smtp_password: str
# Clés API (Optionnelles)
openrouter_api_key: Optional[str] = None
openrouter_management_key: Optional[str] = None
groq_api_key: Optional[str] = None
@@ -27,33 +25,49 @@ class LoginRequest(BaseModel):
password: str
totp_code: str
# --- NOUVEAU : Schémas pour les Projets ---
class ProjectBase(BaseModel):
title: str
class ProjectCreate(ProjectBase):
pass
class ProjectCreate(ProjectBase): pass
class ProjectResponse(ProjectBase):
id: int
created_at: datetime
is_pinned: bool
class Config: from_attributes = True
class Config:
from_attributes = True
class ProjectRename(BaseModel):
title: str
class OrchestratorConfig(BaseModel):
workers: List[str]
prompter: Optional[str] = "gemini-3.5-flash-lite"
concatenator: Optional[str] = "gemini-3.5-flash-lite"
class AttachedFile(BaseModel):
name: str
content: str
# --- NOUVEAU : Schémas pour les Messages ---
class MessageBase(BaseModel):
role: str
content: str
class MessageCreate(MessageBase):
pass
config: Optional[OrchestratorConfig] = None
files: Optional[List[AttachedFile]] = None
class MessageResponse(MessageBase):
id: int
created_at: datetime
project_id: int
class Config: from_attributes = True
class ProjectRename(BaseModel):
title: str
class PasswordChange(BaseModel):
old_password: str
new_password: str
class ModelReplacementRequest(BaseModel):
missing_model: str
class LogRequest(BaseModel):
message: str
+743 -208
View File
File diff suppressed because it is too large. Load diff
+7 -1
View File
@@ -11,4 +11,10 @@ python-multipart
bcrypt
email-validator
PyJWT
openai
openai
httpx
python-multipart
pytz
PyPDF2
openpyxl
oletools