Public Access
feat: Moteur de routage Qwen terminé, intégration Groq/Gemini, système de cascade et UI
This commit is contained in:
1 parent
bc700dfb44
commit
4ae3c49595
2 files changed
+146
-57
No files matched your search
@@ -10,29 +10,31 @@ Système d'IA modulaire avec orchestrateur local et routeur intelligent.
|
|||||||
- [x] Configuration réseau (Pare-feu / Ports)
|
- [x] Configuration réseau (Pare-feu / Ports)
|
||||||
- [x] Initialisation dépôts Gitea/GitHub
|
- [x] Initialisation dépôts Gitea/GitHub
|
||||||
|
|
||||||
### Phase 2 : Orchestrateur Local (Backend) ⏳
|
### Phase 2 : Orchestrateur Local (Backend) ✅
|
||||||
- [x] Installation Ollama + Qwen 2.5 3B (Serveur)
|
- [x] Installation Ollama + Qwen 2.5 3B (Serveur)
|
||||||
- [x] Développement de l'interface de base FastHTML
|
- [x] Développement de l'interface de base FastHTML
|
||||||
- [x] Connexion réussie entre PC de dev et Serveur IA
|
- [x] Connexion réussie entre PC de dev et Serveur IA
|
||||||
- [X] Développement du script de Routage (Gateway)
|
- [x] Développement du script de Routage (Gateway avec Qwen)
|
||||||
- [ ] Intégration des APIs tierces (Groq, Gemini, etc.)
|
- [x] Intégration des APIs tierces (Groq, Gemini avec système de cascade)
|
||||||
- [ ] Système de gestion de file d'attente (Queue List)
|
- [ ] Système de gestion de file d'attente (Queue List)
|
||||||
- [ ] Création du système d'interdiction automatique (Fichiers refusés)
|
- [ ] Création du système d'interdiction automatique (Fichiers refusés)
|
||||||
|
|
||||||
### Phase 3 : Front-Office (FastHTML)
|
### Phase 3 : Front-Office & Sécurité Financière ⏳
|
||||||
- [ ] Création UI légère (Thème Aethas)
|
- [ ] Création UI légère (Thème Aethas avec encarts dynamiques)
|
||||||
- [ ] Implémentation du switch Gratuit/Payant
|
- [ ] Dashboard Finance (Compteur de frais et analyse consommation tokens)
|
||||||
- [ ] Dashboard Finance (Analyse consommation tokens)
|
- [ ] **Simulateur de coût avant requête** (pour éviter les défauts de paiement)
|
||||||
|
- [ ] Passage des clés API en mode "Pay-as-you-go" (Une fois le simulateur testé et validé)
|
||||||
|
- [ ] Implémentation du switch Gratuit/Payant pour les utilisateurs
|
||||||
- [ ] Système d'inscription et validation Admin
|
- [ ] Système d'inscription et validation Admin
|
||||||
|
|
||||||
### Phase 4 : Déploiement & Sécurité
|
### Phase 4 : Déploiement & Sécurité
|
||||||
- [ ] Mise en conteneur Docker
|
- [ ] Mise en conteneur Docker
|
||||||
- [ ] Mise en production sur aethas38.duckdns.org
|
- [ ] Mise en production sur aethas38.duckdns.org
|
||||||
- [ ] Tests de charge et vérification coûts
|
- [ ] Tests de charge et vérification des sécurités de facturation
|
||||||
|
|
||||||
---
|
---
|
||||||
## 💡 Informations Techniques
|
## 💡 Informations Techniques
|
||||||
* **Orchestrateur :** Local (Ollama / Qwen 2.5 3B)
|
* **Orchestrateur :** Local (Ollama / Qwen 2.5 3B)
|
||||||
* **Framework Interface :** FastHTML
|
* **Framework Interface :** FastHTML
|
||||||
* **Langage :** Python 3.12 (Environnement Virtuel)
|
* **Langage :** Python 3.13 (Environnement Virtuel strict)
|
||||||
* **Serveur :** Debian (Headless, GTX 1650 4Go VRAM)
|
* **Serveur :** Debian (Headless, GTX 1650 4Go VRAM)
|
||||||
@@ -3,83 +3,170 @@ import json
|
|||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from fasthtml.common import *
|
from fasthtml.common import *
|
||||||
from openai import OpenAI
|
from openai import OpenAI
|
||||||
|
from groq import Groq
|
||||||
|
from google import genai
|
||||||
|
|
||||||
# Chargement du coffre-fort des clés API
|
# ==========================================
|
||||||
|
# SECTION 1. INITIALISATION & SÉCURITÉ
|
||||||
|
# ==========================================
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
# Petit test pour vérifier que Python trouve bien les clés au démarrage
|
|
||||||
print("Vérification des API :")
|
|
||||||
print("- Groq chargée :", "✅" if os.getenv("GROQ_API_KEY") else "❌")
|
|
||||||
print("- Gemini chargée :", "✅" if os.getenv("GEMINI_API_KEY") else "❌")
|
|
||||||
|
|
||||||
# Initialisation de l'application FastHTML
|
|
||||||
app, rt = fast_app()
|
app, rt = fast_app()
|
||||||
|
|
||||||
# Configuration du client local (Qwen)
|
# ==========================================
|
||||||
client = OpenAI(
|
# SECTION 2. CONNEXION AUX IA
|
||||||
base_url="http://192.168.1.20:11434/v1",
|
# ==========================================
|
||||||
api_key="ollama"
|
# --- SOUS-SECTION 2A : Initialisation des clients ---
|
||||||
)
|
local_client = OpenAI(base_url="http://192.168.1.20:11434/v1", api_key="ollama")
|
||||||
|
groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))
|
||||||
|
gemini_client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
|
||||||
|
|
||||||
# Route principale : l'interface utilisateur
|
# --- SOUS-SECTION 2B : Cascade Google Gemini ---
|
||||||
|
def interroger_gemini_cascade(prompt):
|
||||||
|
"""
|
||||||
|
Tente systématiquement la version MAXIMUM (Pro).
|
||||||
|
Si le compte n'a pas les droits ou si le serveur est plein, descend en gamme.
|
||||||
|
"""
|
||||||
|
modeles_a_tester = [
|
||||||
|
"gemini-3.1-pro-preview", # Max absolu
|
||||||
|
"gemini-3.1-pro", # Max stable
|
||||||
|
"gemini-2.5-pro", # Max ancienne génération
|
||||||
|
"gemini-3.7-flash", # Meilleur modèle rapide et gratuit
|
||||||
|
"gemini-3.6-flash" # Secours
|
||||||
|
]
|
||||||
|
|
||||||
|
for nom_modele in modeles_a_tester:
|
||||||
|
try:
|
||||||
|
ia_response = gemini_client.models.generate_content(
|
||||||
|
model=nom_modele,
|
||||||
|
contents=prompt,
|
||||||
|
config={"system_instruction": "Réponds toujours dans la même langue que celle utilisée par l'utilisateur."}
|
||||||
|
)
|
||||||
|
return ia_response.text, nom_modele
|
||||||
|
except Exception:
|
||||||
|
# On ignore l'erreur silencieusement et on passe au modèle suivant
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise Exception("Tous les modèles Gemini sont indisponibles actuellement.")
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# SECTION 3. INTERFACE UTILISATEUR (UI FastHTML)
|
||||||
|
# ==========================================
|
||||||
@rt('/')
|
@rt('/')
|
||||||
def get():
|
def get():
|
||||||
return Titled("Aethas38 - Orchestrateur",
|
return Titled("Aethas38 - Orchestrateur",
|
||||||
Main(
|
Main(
|
||||||
H1("Interface de Test - Qwen 2.5 (3B)"),
|
H1("Multi-IA : Demandez, l'Orchestrateur choisit !"),
|
||||||
Form(
|
Form(
|
||||||
Input(type="text", name="prompt", placeholder="Posez votre question à Qwen...", style="width: 100%; padding: 10px; margin-bottom: 10px;"),
|
Input(type="text", name="prompt", placeholder="Posez votre question...", style="width: 100%; padding: 10px; margin-bottom: 10px;"),
|
||||||
Button("Envoyer la requête", type="submit", style="padding: 10px 20px; cursor: pointer;"),
|
Button("Envoyer la requête", type="submit", style="padding: 10px 20px; cursor: pointer;"),
|
||||||
hx_post="/chat", # Envoie la requête à la route /chat
|
hx_post="/chat",
|
||||||
hx_target="#result", # Injecte la réponse dans la div "result"
|
hx_target="#result",
|
||||||
hx_indicator="#loading" # Affiche un indicateur de chargement
|
hx_indicator="#loading"
|
||||||
),
|
),
|
||||||
Div("Traitement en cours par le serveur Aethas38...", id="loading", cls="htmx-indicator", style="color: gray; font-style: italic; margin-top: 10px;"),
|
Div("L'Orchestrateur analyse et route la requête...", id="loading", cls="htmx-indicator", style="color: gray; font-style: italic; margin-top: 10px;"),
|
||||||
Div(id="result", style="margin-top: 20px; padding: 15px; border: 1px solid #444; border-radius: 5px; background-color: #222; color: white;"),
|
Div(id="result", style="margin-top: 20px;"), # Zone vide où le résultat s'injectera
|
||||||
cls="container"
|
cls="container"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Route API : Traitement de la requête par l'IA
|
# ==========================================
|
||||||
|
# SECTION 4. LE MOTEUR DE ROUTAGE (L'Aiguilleur)
|
||||||
|
# ==========================================
|
||||||
@rt('/chat')
|
@rt('/chat')
|
||||||
def post(prompt: str):
|
def post(prompt: str):
|
||||||
try:
|
try:
|
||||||
# 1. Le Prompt Système amélioré pour éviter les pièges
|
# --- SOUS-SECTION 4A : Analyse par Qwen (Le Cerveau) ---
|
||||||
router_prompt = """Tu es l'Aiguilleur (Routeur) du projet Aethas38.
|
router_prompt = """Tu es l'Aiguilleur (Routeur) du projet Aethas38.
|
||||||
Analyse la demande de l'utilisateur et choisis le fournisseur d'IA le plus adapté selon ces règles strictes :
|
Choisis le fournisseur d'IA le plus adapté :
|
||||||
- "groq" : programmation, code, scripts, mathématiques, logique, informatique.
|
- "groq" : programmation, code, scripts, mathématiques, logique.
|
||||||
- "gemini" : rédaction, recettes de cuisine, culture générale, histoire, résumés, art.
|
- "gemini" : rédaction, littérature, recettes, culture générale, résumés, art.
|
||||||
- "local" : UNIQUEMENT pour les salutations (bonjour, test, coucou) ou les banalités très courtes.
|
- "local" : UNIQUEMENT pour les salutations (bonjour, test, coucou).
|
||||||
|
Format strict : {"intention": "description courte", "fournisseur": "nom"}
|
||||||
Format de réponse obligatoire : un objet JSON strict. AUCUN texte d'accompagnement.
|
Ne dis absolument RIEN d'autre.
|
||||||
Exemple 1 : {"intention": "créer un script", "fournisseur": "groq"}
|
|
||||||
Exemple 2 : {"intention": "recette de gâteau", "fournisseur": "gemini"}
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 2. On interroge notre Qwen local (la GTX 1650 bosse !)
|
router_response = local_client.chat.completions.create(
|
||||||
response = client.chat.completions.create(
|
|
||||||
model="qwen2.5:3b",
|
model="qwen2.5:3b",
|
||||||
messages=[
|
messages=[{"role": "system", "content": router_prompt}, {"role": "user", "content": prompt}],
|
||||||
{"role": "system", "content": router_prompt},
|
temperature=0.1
|
||||||
{"role": "user", "content": prompt}
|
|
||||||
],
|
|
||||||
temperature=0.1 # Température basse pour qu'il soit très précis et formel
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 3. On récupère et on lit sa décision brute
|
raw_decision = router_response.choices[0].message.content
|
||||||
raw_decision = response.choices[0].message.content
|
|
||||||
|
|
||||||
# 4. On l'affiche sur l'interface de test (temporaire, pour voir la magie opérer)
|
# --- NOUVEAU : Nettoyage blindé (Anticrash) ---
|
||||||
return Div(
|
# On cherche manuellement les accolades pour extraire le JSON pur
|
||||||
P(f"🗣️ Votre question : {prompt}", style="color: #888;"),
|
start_idx = raw_decision.find('{')
|
||||||
P("🧠 Décision de l'Orchestrateur (Qwen) :", style="color: #00d2ff; font-weight: bold; margin-top: 10px;"),
|
end_idx = raw_decision.rfind('}') + 1
|
||||||
Pre(raw_decision, style="background: #111; padding: 10px; border-radius: 5px; color: #0f0;"),
|
|
||||||
style="border-left: 4px solid #00d2ff; padding-left: 15px; margin-top: 15px;"
|
if start_idx != -1 and end_idx != 0:
|
||||||
|
clean_json = raw_decision[start_idx:end_idx]
|
||||||
|
decision = json.loads(clean_json)
|
||||||
|
else:
|
||||||
|
# Filet de sécurité si Qwen ne renvoie pas du tout de JSON
|
||||||
|
decision = {"intention": "Analyse échouée (Secours)", "fournisseur": "gemini"}
|
||||||
|
|
||||||
|
fournisseur = decision.get("fournisseur", "local").lower()
|
||||||
|
intention = decision.get("intention", "Non définie")
|
||||||
|
|
||||||
|
# --- SOUS-SECTION 4B : Exécution de la Requête (avec respect de la langue) ---
|
||||||
|
nom_modele_utilise = ""
|
||||||
|
|
||||||
|
if fournisseur == "groq":
|
||||||
|
nom_modele_utilise = "llama3-70b-8192"
|
||||||
|
ia_response = groq_client.chat.completions.create(
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "Réponds systématiquement dans la même langue que celle utilisée par l'utilisateur."},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
],
|
||||||
|
model=nom_modele_utilise,
|
||||||
|
)
|
||||||
|
reponse_finale = ia_response.choices[0].message.content
|
||||||
|
couleur = "#f97316" # Orange Groq
|
||||||
|
nom_fournisseur = "GROQ (Llama 3)"
|
||||||
|
|
||||||
|
elif fournisseur == "gemini":
|
||||||
|
reponse_finale, nom_modele_utilise = interroger_gemini_cascade(prompt)
|
||||||
|
couleur = "#3b82f6" # Bleu Gemini
|
||||||
|
nom_fournisseur = "GOOGLE GEMINI"
|
||||||
|
|
||||||
|
else:
|
||||||
|
nom_modele_utilise = "qwen2.5:3b"
|
||||||
|
ia_response = local_client.chat.completions.create(
|
||||||
|
model=nom_modele_utilise,
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": "Réponds systématiquement dans la langue utilisée par l'utilisateur."},
|
||||||
|
{"role": "user", "content": prompt}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
reponse_finale = ia_response.choices[0].message.content
|
||||||
|
couleur = "#10b981" # Vert Local
|
||||||
|
nom_fournisseur = "SERVEUR LOCAL"
|
||||||
|
|
||||||
|
# --- SOUS-SECTION 4C : Formatage Visuel (L'encart spécifique) ---
|
||||||
|
|
||||||
|
# 1. Le bloc d'informations (Encart visuel distinct)
|
||||||
|
encart_info = Div(
|
||||||
|
H4("Aperçu IA", style="margin: 0 0 10px 0; color: #fff;"),
|
||||||
|
P(f"🎯 Intention : {intention}", style="margin: 0 0 5px 0; font-size: 0.9em;"),
|
||||||
|
P(f"🏢 Fournisseur : {nom_fournisseur}", style="margin: 0 0 5px 0; font-size: 0.9em;"),
|
||||||
|
P(f"🧠 Modèle utilisé : ", Span(nom_modele_utilise, style="font-weight: bold; color: #fff;"), style="margin: 0; font-size: 0.9em;"),
|
||||||
|
style=f"background-color: {couleur}22; border: 1px solid {couleur}; border-left: 5px solid {couleur}; padding: 15px; border-radius: 5px; margin-bottom: 20px;"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 2. Le bloc de réponse de l'IA
|
||||||
|
bloc_reponse = Div(
|
||||||
|
P(reponse_finale, style="white-space: pre-wrap; margin: 0; line-height: 1.6;"),
|
||||||
|
style="background-color: #222; border: 1px solid #444; padding: 20px; border-radius: 5px; color: #eee;"
|
||||||
|
)
|
||||||
|
|
||||||
|
# On retourne les deux blocs empilés
|
||||||
|
return Div(encart_info, bloc_reponse)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return P(f"Erreur de l'orchestrateur : {e}", style="color: red; font-weight: bold;")
|
return P(f"Erreur du système : {e}", style="color: red; font-weight: bold; padding: 15px; background: #330000; border: 1px solid red;")
|
||||||
|
|
||||||
# Lancement du serveur local
|
# ==========================================
|
||||||
|
# SECTION 5. DÉMARRAGE DU SERVEUR
|
||||||
|
# ==========================================
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
serve(port=5001)
|
serve(port=5001)
|
||||||
Reference in new issue
Block a user