Compare commits

..
5 Commits
Author SHA1 Message Date
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
xavier b3d92558fc fix: correction de l'indentation Python dans models.py provoquant le crash du backend
Build and Push Docker Image / build-and-push (push) Successful in 41s
2026-10-07 14:55:42 +02:00
6 changed files with 281 additions and 84 deletions

No files matched your search

+108 -40
View File
@@ -5,56 +5,124 @@ 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. 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 ## 🏗️ Stack Technique & Architecture
- **Backend :** FastAPI (Python) modulaire (une fonction = un module).
- **Frontend :** Vue.js + Tailwind CSS (Responsive / Mobile-first). * **Backend :** FastAPI (Python) modulaire (une fonction = un module) asynchrone (httpx, asyncio).
- **Base de Données :** PostgreSQL (gestion des utilisateurs, historiques, suivi financier). * **Frontend :** Vue.js + Tailwind CSS (Responsive / Mobile-first).
- **Traitement Documentaire :** LibreOffice Headless (génération/conversion Excel, Word, PDF). * **Base de Données :** PostgreSQL (gestion des utilisateurs, historiques, suivi financier).
- **Déploiement :** Docker, géré via Dockge et Gitea, sécurisé sur grappe RAID. * **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.
## 📌 Règles de Versioning (SemVer) ## 📌 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 : 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. * **Majeur (X) :** Changement profond d'architecture ou refonte globale.
- **Patch (Z) :** Corrections de bugs, correctifs de sécurité ou ajustements visuels mineurs. * **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.
## 📦 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.
* **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.10.1`, `latest`, etc.).
* **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.
## 🚀 ROADMAP & CAHIER DES CHARGES ## 🚀 ROADMAP & CAHIER DES CHARGES
### Sécurité & Infrastructure ### 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] Installation stricte via Dockge et Gitea (création automatique des dossiers inclus).
- [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] Sécurisation de la totalité du projet (fichiers, BDD PostgreSQL) sur le stockage RAID.
- [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] 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).
* [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.
* [ ] Procédure de réinitialisation/modification du mot de passe (Admin et Utilisateurs).
### Administration & Gestion des Utilisateurs ### 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. * [x] Assistant de première installation (création du compte admin complet).
- [ ] 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. * [x] Refonte du setup initial (Super Admin) : Obligation de configurer le serveur mail (aide opérateurs FR/Gmail) et paramétrage complet des clés fournisseurs IA (OpenRouter, Groq, Gemini, DeepSeek, Mistral, Cloudflare, HF).
- [ ] 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). * [x] Intégration de la roue crantée (Paramètres) en bas de la barre latérale.
- [ ] Export de l'activité Admin : Fichier Excel avec KPI, graphiques "camembert", et suivi financier par utilisateur. * [ ] Page de configuration Utilisateur : changement d'email (avec validation via lien envoyé) et de mot de passe.
- [ ] Suivi financier en temps réel et vérification du budget API.
- [ ] Envoi de rapport d'activité automatisé chaque jeudi à 04h00.
* [ ] 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.
### Expérience Utilisateur (UI/UX) ### 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] Thème moderne (Gemini-like) et 100% compatible smartphones.
- [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. * [x] Refonte de la gestion des projets : séparation Épinglés (en haut) / Non Épinglés (Récents).
- [ ] Fonctionnalité de renommage manuel des discussions/projets.
* [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).
* [x] Indicateur visuel (Loader/Animation) signalant que l'IA AETHAS38 "réfléchit" ou travaille.
* [x] Fonctionnalité de renommage manuel des discussions/projets.
* [ ] Affichage natif et fluide des images directement dans le flux de la discussion.
* [ ] Système de partage des réponses sur les réseaux et messageries externes (Facebook, WhatsApp, etc.).
### Moteur IA, Modèles & Fonctionnalités ### 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"). * [x] Cœur asynchrone pour la parallélisation des appels API (module `orchestrator.py`).
- [ ] Tâche planifiée (CRON) pour la mise à jour des modèles toutes les 12h (le timer se reset si mise à jour manuelle). * [ ] Pipeline multi-agents complet avec sélection dynamique :
- [ ] Classement, filtrage (Gratuit/Payant) et catégorisation des modèles (Texte, Code, Image, Audio, Vidéo). * Possibilité de sélectionner de 1 à 5 travailleurs IA en parallèle.
- [ ] Upload manuel du fichier d'extraction des modèles avec intégration dans l'interface filtrable. * Si un seul travailleur est sélectionné, bypass automatique du prompteur et du concaténeur.
- [ ] Traduction automatisée des descriptions des modèles via Ollama (local) ou Gemini. * Si > 1 travailleur, activation de la chaîne complète (1 Prompteur ➔ N Travailleurs ➔ 1 Concaténeur).
- [ ] 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). * [ ] Extraction, mise à jour (via tâche planifiée CRON toutes les 12h ou manuelle) et gestion des modèles depuis les API fournisseurs vers la BDD.
* [ ] Page spécifique d'interface pour les modèles permettant l'export, l'import manuel d'un fichier d'extraction, et la sélection.
* [ ] Extraction unitaire des retours IA.
* [ ] Extraction/Téléchargement de la totalité d'une conversation dans des formats utiles aux différents fournisseurs (JSONL, TXT, etc.).
* [ ] Détection et téléchargement ciblé de pièces jointes ou blocs spécifiques (Code brut, fichiers Excel `.xlsx`) générés par l'IA en respectant le nommage exact demandé.
* [ ] Classement, filtrage (Gratuit/Payant) et catégorisation des modèles (Texte, Code, Image, Audio, Vidéo).
* [ ] 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).
+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] formatted_messages = [{"role": msg.role, "content": msg.content} for msg in messages]
try: try:
# 1. Priorité absolue : Test Gemini direct (Rapide, sans intermédiaire) # 1. Priorité absolue : Test Gemini direct
if settings.gemini_api_key: if settings.gemini_api_key:
client = OpenAI( client = OpenAI(
base_url="https://generativelanguage.googleapis.com/v1beta/openai/", base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
api_key=settings.gemini_api_key api_key=settings.gemini_api_key
) )
# Utilisation de l'identifiant standard API pour la version Flash # Correction : Utilisation exacte du modèle 3.5 Flash-Lite
model = "gemini-1.5-flash" model = "gemini-3.5-flash-lite"
response = client.chat.completions.create(model=model, messages=formatted_messages) response = client.chat.completions.create(model=model, messages=formatted_messages)
# 2. Test OpenRouter (Fallback) # 2. Test OpenRouter (Fallback)
elif settings.openrouter_api_key: elif settings.openrouter_api_key:
client = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=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( response = client.chat.completions.create(
model=model, model=model,
messages=formatted_messages, messages=formatted_messages,
+23 -4
View File
@@ -10,7 +10,8 @@ from .database import engine, Base, get_db
from .auth import get_password_hash, generate_totp_secret, get_totp_uri, verify_password, verify_totp, create_access_token, verify_token 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 .schemas import AdminCreate, LoginRequest, ProjectCreate, ProjectResponse, ProjectRename, MessageCreate, MessageResponse
from .models import User, Project, Message, SystemSettings from .models import User, Project, Message, SystemSettings
from .ai import get_ai_response from .orchestrator import run_orchestrator, sync_providers_models
from .models import User, Project, Message, SystemSettings, AIModel
# Création des tables dans la base de données # Création des tables dans la base de données
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
@@ -151,7 +152,8 @@ def get_messages(project_id: int, db: Session = Depends(get_db), current_user: U
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()
@app.post("/api/projects/{project_id}/messages", response_model=List[MessageResponse]) @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)): async def create_message(project_id: int, message: MessageCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Ajoute un message, lance l'Orchestrateur asynchrone et retourne l'historique."""
project = db.query(Project).filter(Project.id == project_id, Project.user_id == current_user.id).first() 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") if not project: raise HTTPException(status_code=404, detail="Projet introuvable")
@@ -162,10 +164,27 @@ def create_message(project_id: int, message: MessageCreate, db: Session = Depend
history = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all() history = db.query(Message).filter(Message.project_id == project_id).order_by(Message.created_at.asc()).all()
settings = db.query(SystemSettings).first() settings = db.query(SystemSettings).first()
ai_response_text = get_ai_response(history, settings) # Configuration temporaire (sera remplacée par les choix du Frontend à la Phase 2)
# Ex pour tester le pipeline complet : {"workers": ["gemini-3.5-flash-lite", "google/gemini-1.5-pro"]}
orchestrator_config = {"workers": ["gemini-3.5-flash-lite"]}
ai_response_text = await run_orchestrator(history, settings, orchestrator_config)
ai_message = Message(role="assistant", content=ai_response_text, project_id=project_id) ai_message = Message(role="assistant", content=ai_response_text, project_id=project_id)
db.add(ai_message) db.add(ai_message)
db.commit() 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()
@app.post("/api/models/sync")
async def trigger_model_sync(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Déclenche manuellement l'extraction et la mise à jour des modèles depuis les API."""
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Accès réservé aux administrateurs.")
settings = db.query(SystemSettings).first()
return await sync_providers_models(db, settings)
@app.get("/api/models")
def get_models(db: Session = Depends(get_db), current_user: User = Depends(get_current_user)):
"""Renvoie la liste complète des modèles stockés en base de données."""
return db.query(AIModel).order_by(AIModel.name.asc()).all()
+17 -35
View File
@@ -1,60 +1,44 @@
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Text from sqlalchemy import Column, Integer, String, Boolean, ForeignKey, DateTime, Float
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from datetime import datetime, timezone from datetime import datetime, timezone
from .database import Base from .database import Base
from sqlalchemy import Float, DateTime
from datetime import datetime, timezone
class User(Base): class User(Base):
__tablename__ = "users" __tablename__ = "users"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True, nullable=False) email = Column(String, unique=True, index=True)
username = Column(String, unique=True, index=True, nullable=False) username = Column(String, unique=True, index=True)
hashed_password = Column(String, nullable=False) hashed_password = Column(String)
totp_secret = Column(String, nullable=False) totp_secret = Column(String)
is_admin = Column(Boolean, default=False) is_admin = Column(Boolean, default=False)
projects = relationship("Project", back_populates="owner")
# Relation : Un utilisateur peut avoir plusieurs projets
projects = relationship("Project", back_populates="owner", cascade="all, delete-orphan")
class Project(Base): class Project(Base):
__tablename__ = "projects" __tablename__ = "projects"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
title = Column(String, nullable=False) title = Column(String, index=True)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
is_pinned = Column(Boolean, default=False) is_pinned = Column(Boolean, default=False)
user_id = Column(Integer, ForeignKey("users.id"), nullable=False) user_id = Column(Integer, ForeignKey("users.id"))
# Relations
owner = relationship("User", back_populates="projects") owner = relationship("User", back_populates="projects")
messages = relationship("Message", back_populates="project", cascade="all, delete-orphan") messages = relationship("Message", back_populates="project", cascade="all, delete-orphan")
class Message(Base): class Message(Base):
__tablename__ = "messages" __tablename__ = "messages"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
role = Column(String, nullable=False) # Ex: 'user', 'assistant', 'worker_1', 'system' role = Column(String)
content = Column(Text, nullable=False) content = Column(String)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
project_id = Column(Integer, ForeignKey("projects.id"), nullable=False) project_id = Column(Integer, ForeignKey("projects.id"))
# Relation
project = relationship("Project", back_populates="messages") project = relationship("Project", back_populates="messages")
class SystemSettings(Base): class SystemSettings(Base):
__tablename__ = "system_settings" __tablename__ = "system_settings"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
# Serveur Mail (Obligatoire)
smtp_host = Column(String, nullable=False) smtp_host = Column(String, nullable=False)
smtp_port = Column(Integer, nullable=False) smtp_port = Column(Integer, nullable=False)
smtp_user = Column(String, nullable=False) smtp_user = Column(String, nullable=False)
smtp_password = Column(String, nullable=False) smtp_password = Column(String, nullable=False)
# Clés API (Optionnelles)
openrouter_api_key = Column(String, nullable=True) openrouter_api_key = Column(String, nullable=True)
openrouter_management_key = Column(String, nullable=True) openrouter_management_key = Column(String, nullable=True)
groq_api_key = Column(String, nullable=True) groq_api_key = Column(String, nullable=True)
@@ -65,25 +49,23 @@ class SystemSettings(Base):
cloudflare_api_token = Column(String, nullable=True) cloudflare_api_token = Column(String, nullable=True)
huggingface_api_key = Column(String, nullable=True) huggingface_api_key = Column(String, nullable=True)
class AIModel(Base): class AIModel(Base):
"""Stocke la liste des modèles extraits depuis les fournisseurs.""" """Stocke la liste des modèles extraits depuis les fournisseurs."""
__tablename__ = "ai_models" __tablename__ = "ai_models"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
provider = Column(String, index=True) # ex: openrouter, groq provider = Column(String, index=True)
model_id = Column(String, unique=True, index=True) # ex: google/gemini-1.5-pro model_id = Column(String, unique=True, index=True)
name = Column(String) name = Column(String)
context_length = Column(Integer) context_length = Column(Integer)
pricing_prompt = Column(Float) # Coût pour 1M tokens pricing_prompt = Column(Float)
pricing_completion = Column(Float) pricing_completion = Column(Float)
last_updated = Column(DateTime, default=lambda: datetime.now(timezone.utc)) last_updated = Column(DateTime, default=lambda: datetime.now(timezone.utc))
class FinancialLog(Base): class FinancialLog(Base):
"""Suivi financier par fournisseur.""" """Suivi financier par fournisseur."""
__tablename__ = "financial_logs" __tablename__ = "financial_logs"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
provider = Column(String, index=True) provider = Column(String, index=True)
balance = Column(Float) # Crédit restant balance = Column(Float)
total_usage = Column(Float) # Consommation totale total_usage = Column(Float)
checked_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) checked_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
+127
View File
@@ -0,0 +1,127 @@
import asyncio
import httpx
from openai import AsyncOpenAI
from fastapi import HTTPException
from sqlalchemy.orm import Session
from .models import SystemSettings, AIModel
from .logger import system_logger
from datetime import datetime, timezone
# --- PARTIE 1 : EXTRACTION DES MODÈLES ---
async def sync_providers_models(db: Session, settings: SystemSettings):
"""Extrait et met à jour les modèles depuis les fournisseurs configurés."""
added_or_updated = 0
# httpx.AsyncClient permet des requêtes non-bloquantes (ultra rapide)
async with httpx.AsyncClient() as client:
# 1. OpenRouter (Exemple principal pour l'extraction massive)
if settings.openrouter_api_key:
try:
response = await client.get("https://openrouter.ai/api/v1/models")
if response.status_code == 200:
for item in response.json().get("data", []):
model_id = item["id"]
existing = db.query(AIModel).filter(AIModel.model_id == model_id).first()
pricing = item.get("pricing", {})
# Conversion en coût pour 1 Million de tokens
p_prompt = float(pricing.get("prompt", 0)) * 1000000 if pricing.get("prompt") else 0.0
p_comp = float(pricing.get("completion", 0)) * 1000000 if pricing.get("completion") else 0.0
if existing:
existing.pricing_prompt = p_prompt
existing.pricing_completion = p_comp
existing.last_updated = datetime.now(timezone.utc)
else:
new_model = AIModel(
provider="openrouter",
model_id=model_id,
name=item["name"],
context_length=item.get("context_length", 0),
pricing_prompt=p_prompt,
pricing_completion=p_comp
)
db.add(new_model)
added_or_updated += 1
except Exception as e:
system_logger.error(f"Erreur Sync OpenRouter: {e}")
db.commit()
return {"status": "success", "models_processed": added_or_updated}
# --- PARTIE 2 : MOTEUR MULTI-AGENTS ---
def get_client_for_model(model_id: str, settings: SystemSettings):
"""Retourne le client AsyncOpenAI approprié selon le modèle sélectionné."""
if "gemini" in model_id.lower() and settings.gemini_api_key:
return AsyncOpenAI(base_url="https://generativelanguage.googleapis.com/v1beta/openai/", api_key=settings.gemini_api_key)
elif settings.openrouter_api_key:
return AsyncOpenAI(base_url="https://openrouter.ai/api/v1", api_key=settings.openrouter_api_key)
raise ValueError(f"Aucun fournisseur configuré pour {model_id}")
async def ask_agent(client, model_id, messages, is_openrouter=False):
"""Appel asynchrone à un modèle IA."""
kwargs = {"model": model_id, "messages": messages}
if is_openrouter:
kwargs["extra_headers"] = {"HTTP-Referer": "https://aethas38.duckdns.org", "X-Title": "AETHAS38 Orchestrator"}
response = await client.chat.completions.create(**kwargs)
return response.choices[0].message.content
async def run_orchestrator(history: list, settings: SystemSettings, config: dict) -> str:
"""
Gère la logique : 1 Prompteur -> N Travailleurs -> 1 Concaténeur.
Le dictionnaire 'config' proviendra de l'interface graphique.
"""
workers = config.get("workers", [])
if not workers:
workers = ["gemini-3.5-flash-lite"] # Fallback de sécurité
user_prompt = history[-1].content
formatted_history = [{"role": msg.role, "content": msg.content} for msg in history[:-1]]
# SCÉNARIO 1 : Un seul travailleur (Pas besoin de prompteur/concaténeur)
if len(workers) == 1:
worker_model = workers[0]
client = get_client_for_model(worker_model, settings)
messages = formatted_history + [{"role": "user", "content": user_prompt}]
return await ask_agent(client, worker_model, messages, "openrouter" in worker_model.lower())
# SCÉNARIO 2 : Multi-Travailleurs (Le pipeline complet)
try:
# Étape 1 : Le Prompteur améliore la requête
prompter_model = config.get("prompter", "gemini-3.5-flash-lite")
p_client = get_client_for_model(prompter_model, settings)
p_messages = [{"role": "system", "content": "Tu es un expert en Prompt Engineering. Optimise la requête de l'utilisateur pour qu'elle soit claire, directive et parfaite pour des IAs de génération. Retourne UNIQUEMENT le prompt optimisé."}]
p_messages.append({"role": "user", "content": user_prompt})
system_logger.info("Démarrage du Prompteur...")
optimized_prompt = await ask_agent(p_client, prompter_model, p_messages)
# Étape 2 : Les Travailleurs en parallèle (Magie de l'Asynchrone)
system_logger.info(f"Lancement de {len(workers)} travailleurs en parallèle...")
w_tasks = []
for w_model in workers:
w_client = get_client_for_model(w_model, settings)
w_messages = formatted_history + [{"role": "user", "content": optimized_prompt}]
# On stocke les tâches sans les attendre immédiatement
w_tasks.append(ask_agent(w_client, w_model, w_messages, "openrouter" in w_model.lower()))
# 'gather' exécute toutes les requêtes en même temps !
workers_responses = await asyncio.gather(*w_tasks, return_exceptions=True)
# Étape 3 : Le Concaténeur synthétise
concat_model = config.get("concatenator", "gemini-3.5-flash-lite")
c_client = get_client_for_model(concat_model, settings)
synthesis_prompt = f"Voici la requête initiale : {user_prompt}\n\nVoici les réponses de {len(workers)} experts IA différents :\n"
for i, resp in enumerate(workers_responses):
synthesis_prompt += f"--- EXPERT {i+1} ---\n{resp if not isinstance(resp, Exception) else 'Erreur de génération'}\n\n"
synthesis_prompt += "Fais une synthèse finale parfaite, complète et structurée de ces réponses, en gardant le meilleur de chacune."
system_logger.info("Démarrage du Concaténeur...")
c_messages = [{"role": "user", "content": synthesis_prompt}]
return await ask_agent(c_client, concat_model, c_messages)
except Exception as e:
system_logger.error(f"Erreur Pipeline Multi-Agents: {e}")
raise HTTPException(status_code=502, detail=f"Échec de l'orchestration : {str(e)}")
+2 -1
View File
@@ -11,4 +11,5 @@ python-multipart
bcrypt bcrypt
email-validator email-validator
PyJWT PyJWT
openai openai
httpx