From eab18d832466d953c30df22216ce61e717535bb5 Mon Sep 17 00:00:00 2001 From: Xavier Date: Thu, 27 Aug 2026 19:23:04 +0200 Subject: [PATCH] =?UTF-8?q?Ajout=20du=20connecteur=20Hugging=20Face=20et?= =?UTF-8?q?=20mise=20=C3=A0=20jour=20du=20Multi-IA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/ai_engine.py | 47 ++++++++++++++++++++++++------ modules/ai_huggingface.py | 42 +++++++++++++++++++++++++++ modules/model_updater.py | 60 +++++++++++++++++++++++++++++++++------ modules/router - Copie.py | 14 --------- 4 files changed, 131 insertions(+), 32 deletions(-) create mode 100644 modules/ai_huggingface.py delete mode 100644 modules/router - Copie.py diff --git a/modules/ai_engine.py b/modules/ai_engine.py index 7cb6265..fcd38a0 100644 --- a/modules/ai_engine.py +++ b/modules/ai_engine.py @@ -3,6 +3,7 @@ from modules.logger import log_event from modules.ai_gemini import interroger_gemini from modules.ai_local import interroger_local_ollama from modules.ai_openrouter import interroger_openrouter, interroger_openrouter_multi +from modules.ai_huggingface import interroger_huggingface SYSTEM_CONFIDENTIALITY_INSTRUCTION = ( "RÈGLE DE CONFIDENTIALITÉ ABSOLUE : Tu es un assistant intégré à la plateforme privée Aethas38. " @@ -12,9 +13,33 @@ SYSTEM_CONFIDENTIALITY_INSTRUCTION = ( ) def executer_synthese_multi_ia(prompt_securise: str, fournisseur: str, mode_payant: bool = False, chat_id: str = None): + reponses_brutes = [] + try: - # Passage du chat_id (code hexadécimal) à l'API Multi - reponses_brutes = interroger_openrouter_multi(prompt_securise, type_modele=fournisseur, premium=mode_payant, chat_id=chat_id) + # 1. Lancement PARALLÈLE vers OpenRouter et Hugging Face + with concurrent.futures.ThreadPoolExecutor() as executor: + future_or = executor.submit(interroger_openrouter_multi, prompt_securise, type_modele=fournisseur, premium=mode_payant, chat_id=chat_id) + future_hf = executor.submit(interroger_huggingface, prompt_securise) + + # 2. Récupération des résultats OpenRouter + try: + resultat_or = future_or.result() + if resultat_or: + reponses_brutes.extend(resultat_or) + except Exception as e: + log_event("WARNING", "MULTI_IA", f"Échec OpenRouter : {e}") + + # 3. Récupération des résultats Hugging Face + try: + resultat_hf = future_hf.result() + if resultat_hf and resultat_hf[0]: + texte_hf, _, _, modele_hf = resultat_hf + # Formatage identique à OpenRouter pour la synthèse + reponses_brutes.append({"modele": f"HuggingFace ({modele_hf})", "texte": texte_hf}) + except Exception as e: + log_event("WARNING", "MULTI_IA", f"Échec HuggingFace : {e}") + + # 4. Synthèse par Gemini si au moins un modèle a répondu if len(reponses_brutes) >= 1: instruction_synthese = ( "RÈGLE DE CONFIDENTIALITÉ : Tu es un assistant IA. " @@ -29,9 +54,13 @@ def executer_synthese_multi_ia(prompt_securise: str, fournisseur: str, mode_paya prompt_synthese += f"Demande initiale :\n{prompt_securise}" texte, couleur, _, _ = interroger_gemini(prompt_synthese) - return texte, couleur, "Multi-IA (Synthèse Gemini)", "Pipeline Multi-Modèles" + + # Mise à jour du nom de fournisseur pour refléter le travail de l'orchestrateur + vrai_fournisseur = f"Multi-IA (OR + HF -> Gemini)" if len(reponses_brutes) > 1 else "Multi-IA (Partiel -> Gemini)" + return texte, couleur, vrai_fournisseur, f"Synthèse de {len(reponses_brutes)} modèles" + except Exception as e: - log_event("WARNING", "MULTI_IA", f"Échec synthèse, repli : {e}") + log_event("WARNING", "MULTI_IA", f"Échec total synthèse : {e}") return None @@ -42,14 +71,14 @@ def executer_requete_ia(fournisseur: str, prompt: str, mode_payant: bool = False if multi_ia or fournisseur in ["openrouter_code", "openrouter_droit"]: resultat_multi = executer_synthese_multi_ia(prompt_securise, fournisseur, mode_payant, chat_id) if resultat_multi: - return resultat_multi[0], resultat_multi[1], resultat_multi[2], resultat_multi[3], ["🟢 Multi-IA / Synthèse Gemini réussie"] + return resultat_multi[0], resultat_multi[1], resultat_multi[2], resultat_multi[3], [f"🟢 {resultat_multi[2]} réussie"] if fournisseur == "gemini": - ordre_repli = [interroger_gemini, interroger_openrouter, interroger_local_ollama] + ordre_repli = [interroger_gemini, interroger_openrouter, interroger_huggingface, interroger_local_ollama] elif fournisseur in ["openrouter_code", "openrouter_chat", "openrouter_droit"]: - ordre_repli = [interroger_openrouter, interroger_gemini, interroger_local_ollama] + ordre_repli = [interroger_openrouter, interroger_gemini, interroger_huggingface, interroger_local_ollama] else: - ordre_repli = [interroger_local_ollama, interroger_openrouter, interroger_gemini] + ordre_repli = [interroger_local_ollama, interroger_openrouter, interroger_huggingface, interroger_gemini] for fonction_api in ordre_repli: nom_tente = fonction_api.__name__.replace("interroger_", "") @@ -65,7 +94,7 @@ def executer_requete_ia(fournisseur: str, prompt: str, mode_payant: bool = False trace_tentatives.append(f"🟢 {nom_tente.upper()} ({nom_modele}) : Succès") return texte, couleur, nom_fournisseur, nom_modele, trace_tentatives except Exception as e: - msg_err = str(e).split("-")[0].strip()[:50] # On raccourcit l'erreur pour l'affichage + msg_err = str(e).split("-")[0].strip()[:50] trace_tentatives.append(f"🔴 {nom_tente.upper()} : Échec ({msg_err})") log_event("WARNING", "ORCHESTRATEUR", f"Basculement suite à l'échec de {fonction_api.__name__} : {e}") continue diff --git a/modules/ai_huggingface.py b/modules/ai_huggingface.py new file mode 100644 index 0000000..e4c14f4 --- /dev/null +++ b/modules/ai_huggingface.py @@ -0,0 +1,42 @@ +import os +import requests +from modules.logger import log_event + +def interroger_huggingface(prompt: str, modele_cible="mistralai/Mistral-7B-Instruct-v0.3"): + """ + Connecteur pour l'API Serverless gratuite de Hugging Face. + S'intègre nativement dans la boucle de repli de ai_engine.py. + """ + api_key = os.getenv("HUGGINGFACE_API_KEY") + if not api_key: + raise Exception("Clé API HuggingFace manquante dans le .env") + + url = f"https://api-inference.huggingface.co/models/{modele_cible}" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + # Paramètres optimisés pour les LLMs d'instruction + payload = { + "inputs": prompt, + "parameters": { + "max_new_tokens": 2048, + "return_full_text": False, + "temperature": 0.7 + } + } + + try: + resp = requests.post(url, headers=headers, json=payload, timeout=60) + if resp.status_code == 200: + data = resp.json() + if isinstance(data, list) and len(data) > 0 and "generated_text" in data[0]: + texte = data[0]["generated_text"].strip() + # Renvoie la signature exacte attendue par ai_engine (texte, couleur, fournisseur, modele) + return texte, "#ffb000", "HuggingFace", modele_cible + + raise Exception(f"Erreur {resp.status_code} : {resp.text}") + except Exception as e: + log_event("WARNING", "HUGGINGFACE", f"Échec de la requête : {e}") + raise e \ No newline at end of file diff --git a/modules/model_updater.py b/modules/model_updater.py index 54175a3..041b472 100644 --- a/modules/model_updater.py +++ b/modules/model_updater.py @@ -7,6 +7,41 @@ from modules.logger import log_event CATALOGUE_FILE = "/DATA/AppData/MULTI-IA-AETHAS38/catalogue_dynamique.json" +def recuperer_modeles_hf(): + """ + Récupère les modèles populaires de Hugging Face (Texte, Image, Vidéo). + Les identifiants sont préfixés par 'huggingface/' pour le routage Multi-IA. + """ + api_key = os.getenv("HUGGINGFACE_API_KEY") + headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} + + # Catégories et nombre limite de modèles à récupérer pour garantir les plus fiables + categories = { + "text-generation": 20, # 20 LLMs texte + "text-to-image": 5, # 5 Générateurs d'images + "text-to-video": 2 # 2 Générateurs vidéo + } + + hf_models = [] + for tag, limit in categories.items(): + url = "https://huggingface.co/api/models" + params = { + "pipeline_tag": tag, + "sort": "downloads", + "direction": -1, + "limit": limit + } + try: + resp = requests.get(url, headers=headers, params=params, timeout=15) + if resp.status_code == 200: + for m in resp.json(): + # Formatage identifiable pour l'orchestrateur + hf_models.append(f"huggingface/{m['id']}") + except Exception as e: + log_event("WARNING", "UPDATER", f"Erreur de récupération HF pour {tag}: {e}") + + return hf_models + def _do_update(): ollama_host = os.getenv("OLLAMA_HOST", "http://192.168.1.20:11434") try: @@ -21,25 +56,32 @@ def _do_update(): # MAJ DU CATALOGUE COMPLET OPENROUTER resp_or = requests.get("https://openrouter.ai/api/v1/models", timeout=30) + modeles_gratuits = [] + modeles_payants = [] + if resp_or.status_code == 200: data = resp_or.json().get("data", []) - modeles_gratuits = [] - modeles_payants = [] - for m in data: pricing = m.get("pricing", {}) # S'il est 100% gratuit if pricing.get("prompt") == "0" and pricing.get("completion") == "0": - if "nvidia/nemotron" not in m["id"].lower(): # On garde juste cette exclusion sécurité + if "nvidia/nemotron" not in m["id"].lower(): # On garde l'exclusion sécurité modeles_gratuits.append(m["id"]) else: - # On intègre TOUS les modèles payants (Texte, Vision, 3D, etc.) + # Intégration de TOUS les modèles payants modeles_payants.append(m["id"]) - - with open(CATALOGUE_FILE, "w", encoding="utf-8") as f: - json.dump({"free_models": modeles_gratuits, "paid_models": modeles_payants}, f) - log_event("INFO", "UPDATER", f"Catalogue Global mis à jour : {len(modeles_gratuits)} gratuits, {len(modeles_payants)} payants détectés.") + else: + log_event("WARNING", "UPDATER", "Échec de récupération du catalogue OpenRouter.") + + # MAJ DU CATALOGUE HUGGING FACE + hf_gratuits = recuperer_modeles_hf() + modeles_gratuits.extend(hf_gratuits) + + with open(CATALOGUE_FILE, "w", encoding="utf-8") as f: + json.dump({"free_models": modeles_gratuits, "paid_models": modeles_payants}, f) + + log_event("INFO", "UPDATER", f"Catalogue Global mis à jour : {len(modeles_gratuits)} gratuits (dont {len(hf_gratuits)} HF), {len(modeles_payants)} payants détectés.") except Exception as e: log_event("ERROR", "UPDATER", f"Erreur lors de la MAJ auto : {e}") diff --git a/modules/router - Copie.py b/modules/router - Copie.py deleted file mode 100644 index d11b0c5..0000000 --- a/modules/router - Copie.py +++ /dev/null @@ -1,14 +0,0 @@ -import json -import re - -def nettoyer_decision_router(raw_text): - try: - clean = re.sub(r'```(?:json)?\s*', '', raw_text) - clean = clean.replace('```', '').strip() - start = clean.find('{') - end = clean.rfind('}') + 1 - if start != -1 and end != 0: - return json.loads(clean[start:end]) - except Exception: - pass - return {"intention": "Analyse", "fournisseur": "gemini"}