From 4ae3c495952e32434ec6d37db800795c7d8cf9b9 Mon Sep 17 00:00:00 2001 From: Xavier Date: Sun, 16 Aug 2026 09:05:04 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Moteur=20de=20routage=20Qwen=20termin?= =?UTF-8?q?=C3=A9,=20int=C3=A9gration=20Groq/Gemini,=20syst=C3=A8me=20de?= =?UTF-8?q?=20cascade=20et=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 20 +++--- main.py | 183 ++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 146 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index df8739f..e8e8724 100644 --- a/README.md +++ b/README.md @@ -10,29 +10,31 @@ Système d'IA modulaire avec orchestrateur local et routeur intelligent. - [x] Configuration réseau (Pare-feu / Ports) - [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] Développement de l'interface de base FastHTML - [x] Connexion réussie entre PC de dev et Serveur IA -- [X] Développement du script de Routage (Gateway) -- [ ] Intégration des APIs tierces (Groq, Gemini, etc.) +- [x] Développement du script de Routage (Gateway avec Qwen) +- [x] Intégration des APIs tierces (Groq, Gemini avec système de cascade) - [ ] Système de gestion de file d'attente (Queue List) - [ ] Création du système d'interdiction automatique (Fichiers refusés) -### Phase 3 : Front-Office (FastHTML) -- [ ] Création UI légère (Thème Aethas) -- [ ] Implémentation du switch Gratuit/Payant -- [ ] Dashboard Finance (Analyse consommation tokens) +### Phase 3 : Front-Office & Sécurité Financière ⏳ +- [ ] Création UI légère (Thème Aethas avec encarts dynamiques) +- [ ] Dashboard Finance (Compteur de frais et 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 ### Phase 4 : Déploiement & Sécurité - [ ] Mise en conteneur Docker - [ ] 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 * **Orchestrateur :** Local (Ollama / Qwen 2.5 3B) * **Framework Interface :** FastHTML -* **Langage :** Python 3.12 (Environnement Virtuel) +* **Langage :** Python 3.13 (Environnement Virtuel strict) * **Serveur :** Debian (Headless, GTX 1650 4Go VRAM) \ No newline at end of file diff --git a/main.py b/main.py index 1c42fc2..cc5ed8a 100644 --- a/main.py +++ b/main.py @@ -3,83 +3,170 @@ import json from dotenv import load_dotenv from fasthtml.common import * 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() - -# 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() -# Configuration du client local (Qwen) -client = OpenAI( - base_url="http://192.168.1.20:11434/v1", - api_key="ollama" -) +# ========================================== +# SECTION 2. CONNEXION AUX IA +# ========================================== +# --- 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('/') def get(): return Titled("Aethas38 - Orchestrateur", Main( - H1("Interface de Test - Qwen 2.5 (3B)"), + H1("Multi-IA : Demandez, l'Orchestrateur choisit !"), 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;"), - hx_post="/chat", # Envoie la requête à la route /chat - hx_target="#result", # Injecte la réponse dans la div "result" - hx_indicator="#loading" # Affiche un indicateur de chargement + hx_post="/chat", + hx_target="#result", + 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(id="result", style="margin-top: 20px; padding: 15px; border: 1px solid #444; border-radius: 5px; background-color: #222; color: white;"), + 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;"), # Zone vide où le résultat s'injectera cls="container" ) ) -# Route API : Traitement de la requête par l'IA +# ========================================== +# SECTION 4. LE MOTEUR DE ROUTAGE (L'Aiguilleur) +# ========================================== @rt('/chat') def post(prompt: str): 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. - Analyse la demande de l'utilisateur et choisis le fournisseur d'IA le plus adapté selon ces règles strictes : - - "groq" : programmation, code, scripts, mathématiques, logique, informatique. - - "gemini" : rédaction, recettes de cuisine, culture générale, histoire, résumés, art. - - "local" : UNIQUEMENT pour les salutations (bonjour, test, coucou) ou les banalités très courtes. - - Format de réponse obligatoire : un objet JSON strict. AUCUN texte d'accompagnement. - Exemple 1 : {"intention": "créer un script", "fournisseur": "groq"} - Exemple 2 : {"intention": "recette de gâteau", "fournisseur": "gemini"} + Choisis le fournisseur d'IA le plus adapté : + - "groq" : programmation, code, scripts, mathématiques, logique. + - "gemini" : rédaction, littérature, recettes, culture générale, résumés, art. + - "local" : UNIQUEMENT pour les salutations (bonjour, test, coucou). + Format strict : {"intention": "description courte", "fournisseur": "nom"} + Ne dis absolument RIEN d'autre. """ - # 2. On interroge notre Qwen local (la GTX 1650 bosse !) - response = client.chat.completions.create( + router_response = local_client.chat.completions.create( model="qwen2.5:3b", - messages=[ - {"role": "system", "content": router_prompt}, - {"role": "user", "content": prompt} - ], - temperature=0.1 # Température basse pour qu'il soit très précis et formel + messages=[{"role": "system", "content": router_prompt}, {"role": "user", "content": prompt}], + temperature=0.1 ) - # 3. On récupère et on lit sa décision brute - raw_decision = response.choices[0].message.content + raw_decision = router_response.choices[0].message.content - # 4. On l'affiche sur l'interface de test (temporaire, pour voir la magie opérer) - return Div( - P(f"🗣️ Votre question : {prompt}", style="color: #888;"), - P("🧠 Décision de l'Orchestrateur (Qwen) :", style="color: #00d2ff; font-weight: bold; margin-top: 10px;"), - 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;" + # --- NOUVEAU : Nettoyage blindé (Anticrash) --- + # On cherche manuellement les accolades pour extraire le JSON pur + start_idx = raw_decision.find('{') + end_idx = raw_decision.rfind('}') + 1 + + 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: - 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__': serve(port=5001) \ No newline at end of file