import os import json from dotenv import load_dotenv from fasthtml.common import * from openai import OpenAI from groq import Groq from google import genai # ========================================== # SECTION 1. INITIALISATION & SÉCURITÉ # ========================================== load_dotenv() app, rt = fast_app() # ========================================== # 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")) # --- 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("Multi-IA : Demandez, l'Orchestrateur choisit !"), Form( 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", hx_target="#result", hx_indicator="#loading" ), 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" ) ) # ========================================== # SECTION 4. LE MOTEUR DE ROUTAGE (L'Aiguilleur) # ========================================== @rt('/chat') def post(prompt: str): try: # --- SOUS-SECTION 4A : Analyse par Qwen (Le Cerveau) --- router_prompt = """Tu es l'Aiguilleur (Routeur) du projet Aethas38. 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. """ router_response = local_client.chat.completions.create( model="qwen2.5:3b", messages=[{"role": "system", "content": router_prompt}, {"role": "user", "content": prompt}], temperature=0.1 ) raw_decision = router_response.choices[0].message.content # --- 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 du système : {e}", style="color: red; font-weight: bold; padding: 15px; background: #330000; border: 1px solid red;") # ========================================== # SECTION 5. DÉMARRAGE DU SERVEUR # ========================================== if __name__ == '__main__': serve(port=5001)