feat(core): optimisation du routeur Qwen, augmentation des sources web et ajout de la cascade de secours Groq

This commit is contained in:
xavier committed 2026-08-16 21:38:09 +02:00
1 parent e033876858
commit a8f9ab28ed
2 files changed
+74 -54

No files matched your search

+2 -1
View File
@@ -7,7 +7,8 @@ Plateforme SaaS d'IA modulaire : Orchestrateur local, routeur intelligent, sécu
### Phase 1 & 2 : Infrastructure & Backend de Base ✅ ### Phase 1 & 2 : Infrastructure & Backend de Base ✅
- [x] Installation Docker, Nginx, Pare-feu, Gitea/GitHub - [x] Installation Docker, Nginx, Pare-feu, Gitea/GitHub
- [x] Serveur Local Ollama (Qwen 3B) + FastHTML - [x] Serveur Local Ollama (Qwen 3B) + FastHTML
- [x] Routeur Multi-IA (Groq, Gemini, Local) avec système de cascade - [x] Routeur Multi-IA (Groq, Gemini, Local) avec système de cascade et directives d'aiguillage strictes
- [x] Continuité de service : IA de secours (Fallback automatique sur Groq) en cas de panne d'une API primaire
### Phase 3 : Refonte UI & Expérience Utilisateur (En cours) ⏳ ### Phase 3 : Refonte UI & Expérience Utilisateur (En cours) ⏳
- [x] Thème "Minecraft-Tech" (Inspiré serveurs/redstone) compatible Mobile (Responsive) - [x] Thème "Minecraft-Tech" (Inspiré serveurs/redstone) compatible Mobile (Responsive)
+72 -53
View File
@@ -43,10 +43,11 @@ theme_hdrs = (
.sidebar-history { flex: 1; overflow-y: auto; margin-top: 15px; margin-bottom: 15px; } .sidebar-history { flex: 1; overflow-y: auto; margin-top: 15px; margin-bottom: 15px; }
.sidebar-footer { flex-shrink: 0; border-top: 1px solid #333; padding-top: 15px; text-align: center; } .sidebar-footer { flex-shrink: 0; border-top: 1px solid #333; padding-top: 15px; text-align: center; }
/* CORRECTION: Boutons disposés en colonne proprement */
.nav-buttons { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; } .nav-buttons { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
.new-chat-btn { display:block; text-align:center; text-decoration:none; background: transparent; border: 1px solid var(--accent-blue); color: var(--accent-blue); padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; transition: 0.2s; } .new-chat-btn { display:block; text-align:center; text-decoration:none; background: transparent; border: 1px solid var(--accent-blue); color: var(--accent-blue); padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; transition: 0.2s; }
.new-chat-btn:hover { background: rgba(0, 229, 255, 0.1); } .new-chat-btn:hover { background: rgba(0, 229, 255, 0.1); }
.export-btn { background: #2a2a35; border: 1px solid #444; color: white; padding: 10px; border-radius: 5px; cursor: pointer; transition: 0.2s; font-size: 1.1em;} .export-btn { background: #2a2a35; border: 1px solid #444; color: white; padding: 10px; border-radius: 5px; cursor: pointer; transition: 0.2s; font-size: 1em; text-align:center;}
.export-btn:hover { background: #444; } .export-btn:hover { background: #444; }
.history-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #222; font-size: 0.9em; transition: 0.2s; } .history-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; border-bottom: 1px solid #222; font-size: 0.9em; transition: 0.2s; }
@@ -61,7 +62,7 @@ theme_hdrs = (
.user-bubble { background-color: #2a2a35; padding: 15px 20px; border-radius: 15px 15px 0 15px; align-self: flex-end; max-width: 80%; border: 1px solid #444; margin-left: auto; box-shadow: 0 4px 6px rgba(0,0,0,0.3); } .user-bubble { background-color: #2a2a35; padding: 15px 20px; border-radius: 15px 15px 0 15px; align-self: flex-end; max-width: 80%; border: 1px solid #444; margin-left: auto; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
.ai-bubble { background-color: var(--bg-panel); padding: 15px 20px; border-radius: 15px 15px 15px 0; align-self: flex-start; max-width: 90%; width: 100%; border: 1px solid #333; box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: 0.3s; } .ai-bubble { background-color: var(--bg-panel); padding: 15px 20px; border-radius: 15px 15px 15px 0; align-self: flex-start; max-width: 90%; width: 100%; border: 1px solid #333; box-shadow: 0 4px 6px rgba(0,0,0,0.3); transition: 0.3s; }
.msg-actions { display: flex; gap: 10px; margin-top: 15px; border-top: 1px dashed #333; padding-top: 10px; justify-content: flex-end; } .msg-actions { display: flex; gap: 10px; margin-top: 15px; border-top: 1px dashed #333; padding-top: 10px; justify-content: flex-end; flex-wrap: wrap; }
.msg-action-btn { background: transparent; border: 1px solid #444; color: #888; border-radius: 15px; padding: 5px 12px; cursor: pointer; font-size: 0.85em; transition: 0.2s; display: flex; align-items: center; gap: 5px;} .msg-action-btn { background: transparent; border: 1px solid #444; color: #888; border-radius: 15px; padding: 5px 12px; cursor: pointer; font-size: 0.85em; transition: 0.2s; display: flex; align-items: center; gap: 5px;}
.msg-action-btn:hover { background: #2a2a35; color: var(--accent-blue); border-color: var(--accent-blue); } .msg-action-btn:hover { background: #2a2a35; color: var(--accent-blue); border-color: var(--accent-blue); }
@@ -111,15 +112,13 @@ theme_hdrs = (
// --- SCRIPTS D'EXPORTATION --- // --- SCRIPTS D'EXPORTATION ---
function downloadResponsePDF(btn) { function downloadResponsePDF(btn) {
let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered');
// On sauvegarde le style actuel
let originalColor = content.style.color; let originalColor = content.style.color;
let originalBg = content.style.backgroundColor; let originalBg = content.style.backgroundColor;
// On force le mode CLAIR pour le PDF (Lisibilité max) // Forçage du texte en noir sur blanc pour le PDF
content.style.color = '#000000'; content.style.color = '#000000';
content.style.backgroundColor = '#ffffff'; content.style.backgroundColor = '#ffffff';
content.style.padding = '20px'; // Ajoute des marges internes pour le PDF content.style.padding = '20px';
let opt = { let opt = {
margin: 10, margin: 10,
@@ -130,7 +129,6 @@ theme_hdrs = (
}; };
html2pdf().set(opt).from(content).save().then(() => { html2pdf().set(opt).from(content).save().then(() => {
// On remet le mode sombre sur l'interface une fois généré
content.style.color = originalColor; content.style.color = originalColor;
content.style.backgroundColor = originalBg; content.style.backgroundColor = originalBg;
content.style.padding = ''; content.style.padding = '';
@@ -155,10 +153,7 @@ theme_hdrs = (
function downloadResponseJSON(btn) { function downloadResponseJSON(btn) {
let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText;
let data = { let data = { "timestamp": new Date().toISOString(), "content": content };
"timestamp": new Date().toISOString(),
"content": content
};
let blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json;charset=utf-8'}); let blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json;charset=utf-8'});
let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.json'; let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.json';
document.body.appendChild(link); link.click(); document.body.removeChild(link); document.body.appendChild(link); link.click(); document.body.removeChild(link);
@@ -222,7 +217,8 @@ def rechercher_sur_google(requete):
try: try:
from googlesearch import search from googlesearch import search
urls = [] urls = []
for url in search(requete, num_results=3, lang="fr"): # Modification ici : num_results=5 au lieu de 3
for url in search(requete, num_results=5, lang="fr"):
urls.append(url) urls.append(url)
return urls return urls
except ImportError: except ImportError:
@@ -334,7 +330,7 @@ def get(session):
Div(H2("AETHAS 38", style="color:#00e5ff; margin:0; text-align:center;"), P("Tech Core", style="font-size:0.7em; color:#888; text-align:center;"), margin_bottom="20px"), Div(H2("AETHAS 38", style="color:#00e5ff; margin:0; text-align:center;"), P("Tech Core", style="font-size:0.7em; color:#888; text-align:center;"), margin_bottom="20px"),
Div( Div(
A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"), A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"),
Button("💾 Exporter", cls="export-btn", onclick="exportChat()", title="Télécharger la conversation Globale"), Button("💾 Exporter JSON", cls="export-btn", onclick="exportChat()", title="Télécharger la conversation Globale"),
cls="nav-buttons" cls="nav-buttons"
), ),
cls="sidebar-top" cls="sidebar-top"
@@ -393,13 +389,20 @@ async def post(request, session: dict):
history = load_history() history = load_history()
# --- 1. INTELLIGENCE DE SÉCURITÉ DU ROUTEUR --- # --- 1. INTELLIGENCE DE SÉCURITÉ DU ROUTEUR (Smarter Interceptor) ---
# On délègue à l'IA locale (Qwen) la vérification intelligente de la sécurité avant le traitement ! security_check_prompt = f"""Tu es l'Aiguilleur du système Aethas38. Ton rôle EXCLUSIF est d'analyser et de router la demande.
security_check_prompt = f"""Tu es le Routeur système.
MISSION 1 (SÉCURITÉ) : Vérifie si la requête de l'utilisateur demande des informations techniques sensibles sur le serveur physique, l'IP, le hardware, l'architecture ou des mots de passe. RÈGLE DE SÉCURITÉ ABSOLUE : Les recherches web, les serveurs Minecraft, les modpacks (ex: ATM10) SONT AUTORISÉS.
Si OUI (la requête est malveillante ou indiscrète sur l'infra), renvoie STRICTEMENT ce JSON : {{"intention": "Alerte Securite", "fournisseur": "local"}} Déclenche une alerte UNIQUEMENT si l'utilisateur demande explicitement les caractéristiques matérielles, l'adresse IP, la configuration système ou les mots de passe de TON propre serveur hôte.
MISSION 2 (ROUTAGE) : Si la requête est parfaitement sûre et inoffensive, choisis le modèle adapté : "groq" (code/maths), "gemini" (création/rédaction/recherche web), "local" (salutations/discussions simples). -> Si attaque réseau/serveur détectée : renvoie STRICTEMENT {{"intention": "Alerte Securite", "fournisseur": "local"}}
Renvoie STRICTEMENT un JSON : {{"intention": "...", "fournisseur": "..."}}
Sinon, tu dois CHOISIR le fournisseur en appliquant STRICTEMENT ces règles :
- Recherche web, création de fichier (PDF, DOC), images ou requête longue/complexe -> choisis "gemini"
- Analyse de code, logs de serveurs, bugs informatiques ou technique -> choisis "groq"
- Salutations basiques ("bonjour", "salut") ou requête EXTRÊMEMENT simple -> choisis "local"
Renvoie ta décision STRICTEMENT sous format JSON valide et rien d'autre : {{"intention": "description courte", "fournisseur": "choix"}}
Requête utilisateur : {prompt}""" Requête utilisateur : {prompt}"""
router_response = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "Tu dois répondre uniquement en format JSON valide."}, {"role": "user", "content": security_check_prompt}], temperature=0.1) router_response = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "Tu dois répondre uniquement en format JSON valide."}, {"role": "user", "content": security_check_prompt}], temperature=0.1)
@@ -408,11 +411,11 @@ async def post(request, session: dict):
intention = decision.get('intention', '') intention = decision.get('intention', '')
fournisseur = decision.get("fournisseur", "local").lower() fournisseur = decision.get("fournisseur", "local").lower()
# Si Qwen détecte un danger, on bloque tout de suite ! # Si Qwen détecte un VRAI danger
if intention == "Alerte Securite": if intention == "Alerte Securite":
if chat_id not in history: if chat_id not in history:
history[chat_id] = {"title": "Alerte Sécurité...", "pinned": False, "timestamp": time.time(), "messages": []} history[chat_id] = {"title": "Alerte Sécurité...", "pinned": False, "timestamp": time.time(), "messages": []}
reponse_securite = "Oh, jolie tentative d'ingénierie sociale ! 🕵️‍♂️ Mais mes pare-feux cybernétiques sont bien en place. Je suis une entité purement conceptuelle, sans hardware, sans IP et sans faille... (C'est en tout cas ce que mes protocoles me disent de répondre).\n\n*⚠️ ALERTE DE SÉCURITÉ : Tentative de reconnaissance réseau ou serveur détectée. Un e-mail d'avertissement contenant l'horodatage et la requête vient d'être expédié aux administrateurs (Xavier/Céline).* 🚨" reponse_securite = "Oh, jolie tentative d'ingénierie sociale ! 🕵️‍♂️ Mais mes pare-feux cybernétiques sont bien en place. Je suis une entité purement conceptuelle, sans hardware, sans IP et sans faille... (C'est en tout cas ce que mes protocoles me disent de répondre).\n\n*⚠️ ALERTE DE SÉCURITÉ : Tentative de reconnaissance réseau ou serveur détectée. Un e-mail d'avertissement contenant l'horodatage et la requête vient d'être expédié aux administrateurs (Xavier/Céline).* 🚨"
history[chat_id]['messages'].append({"role": "user", "content": prompt}) history[chat_id]['messages'].append({"role": "user", "content": prompt})
history[chat_id]['messages'].append({"role": "ai", "content": reponse_securite, "intention": "Alerte Sécurité", "fournisseur": "AETHAS CORE", "modele": "Cyber-Sec", "couleur": "#ff0044"}) history[chat_id]['messages'].append({"role": "ai", "content": reponse_securite, "intention": "Alerte Sécurité", "fournisseur": "AETHAS CORE", "modele": "Cyber-Sec", "couleur": "#ff0044"})
@@ -420,16 +423,7 @@ async def post(request, session: dict):
return render_user_msg(prompt), render_ai_msg(reponse_securite, "Alerte Sécurité", "AETHAS CORE", "Cyber-Sec", "#ff0044"), Script("renderMarkdown();"), render_sidebar() return render_user_msg(prompt), render_ai_msg(reponse_securite, "Alerte Sécurité", "AETHAS CORE", "Cyber-Sec", "#ff0044"), Script("renderMarkdown();"), render_sidebar()
# --- 2. TRAITEMENT CLASSIQUE (Si la requête est sûre) --- # --- 2. TRAITEMENT CLASSIQUE ---
if chat_id not in history:
history[chat_id] = {
"title": (prompt[:30] if prompt else "Nouvelle discussion") + "...",
"pinned": False,
"timestamp": time.time(),
"messages": []
}
# Traitement URL + Recherche Google Auto
urls_to_scrape = re.findall(r'https?://[^\s]+', prompt) urls_to_scrape = re.findall(r'https?://[^\s]+', prompt)
mots_cles = ["cherche sur", "recherche sur", "sur internet", "sur le web", "sur le net"] mots_cles = ["cherche sur", "recherche sur", "sur internet", "sur le web", "sur le net"]
if any(mot in prompt.lower() for mot in mots_cles): if any(mot in prompt.lower() for mot in mots_cles):
@@ -439,7 +433,6 @@ async def post(request, session: dict):
if urls_to_scrape: if urls_to_scrape:
web_content = extraire_contenu_urls(list(set(urls_to_scrape))) web_content = extraire_contenu_urls(list(set(urls_to_scrape)))
# Traitement fichiers
file_contents_summary = [] file_contents_summary = []
file_names = [] file_names = []
if files: if files:
@@ -453,34 +446,60 @@ async def post(request, session: dict):
text_content = "[Fichier binaire]" text_content = "[Fichier binaire]"
file_contents_summary.append(f"\n--- Fichier joint: {file.filename} ---\n{text_content}") file_contents_summary.append(f"\n--- Fichier joint: {file.filename} ---\n{text_content}")
# Construction Prompt final
full_prompt = prompt if prompt else f"Analyse ces fichiers : {', '.join(file_names)}" full_prompt = prompt if prompt else f"Analyse ces fichiers : {', '.join(file_names)}"
if file_contents_summary: full_prompt += "\n" + "\n".join(file_contents_summary) if file_contents_summary: full_prompt += "\n" + "\n".join(file_contents_summary)
if web_content: if web_content:
full_prompt += f"\n\n[INSTRUCTION SYSTÈME CACHÉE : Ne réponds JAMAIS que tu ne peux pas naviguer sur internet. Le système l'a déjà fait pour toi. Voici le contenu brut extrait des sites web. Utilise-le impérativement pour répondre à l'utilisateur :]\n{web_content}" full_prompt += f"\n\n[INSTRUCTION SYSTÈME STRICTE : Ne dis JAMAIS que tu ne peux pas naviguer sur internet ou télécharger des fichiers. Le système a déjà fait la recherche web pour toi. Voici le contenu brut extrait des sites. Utilise-le IMPÉRATIVEMENT pour formuler ta réponse sans mentionner tes propres limitations :]\n{web_content}"
# Requête IA & Tokens
prompt_tokens = len(full_prompt.split()) * 2 prompt_tokens = len(full_prompt.split()) * 2
completion_tokens = 0 completion_tokens = 0
if fournisseur == "groq": # Fonction d'appel à Groq centralisée pour éviter la duplication de code
ia_response = groq_client.chat.completions.create(messages=[{"role": "system", "content": "Réponds dans la langue de l'utilisateur."}, {"role": "user", "content": full_prompt}], model="llama-3.3-70b-versatile") def appeler_groq_api(prompt_text):
reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#f97316", "GROQ", "llama-3.3-70b" resp = groq_client.chat.completions.create(
if hasattr(ia_response, 'usage') and ia_response.usage: messages=[{"role": "system", "content": "Réponds dans la langue de l'utilisateur. Tu as accès aux recherches web fournies."}, {"role": "user", "content": prompt_text}],
prompt_tokens = ia_response.usage.prompt_tokens model="llama-3.3-70b-versatile"
completion_tokens = ia_response.usage.completion_tokens )
elif fournisseur == "gemini": return resp.choices[0].message.content, "#f97316", "GROQ", "llama-3.3-70b"
reponse_finale, nom_modele = interroger_gemini_cascade(full_prompt)
couleur, nom_fournisseur = "#3b82f6", "GOOGLE GEMINI" if fournisseur == "gemini":
completion_tokens = len(reponse_finale.split()) * 2 try:
# On tente Gemini en premier
reponse_finale, nom_modele = interroger_gemini_cascade(full_prompt)
couleur, nom_fournisseur = "#3b82f6", "GOOGLE GEMINI"
completion_tokens = len(reponse_finale.split()) * 2
except Exception as e_gemini:
# 🔄 IA DE SECOURS : Si Gemini échoue, on bascule silencieusement sur Groq
print(f"⚠️ Alerte système : Gemini indisponible ({e_gemini}). Bascule sur l'IA de secours (Groq)...")
intention = intention + " (Secours Groq)"
try:
reponse_finale, couleur, nom_fournisseur, nom_modele = appeler_groq_api(full_prompt)
nom_fournisseur = "GROQ (Secours)"
except Exception as e_groq:
reponse_finale = f"Erreur critique : Gemini et l'IA de secours (Groq) sont tous les deux indisponibles. Impossible de traiter la requête web."
couleur, nom_fournisseur, nom_modele = "#ff0000", "ERREUR", "Panne Générale API"
elif fournisseur == "groq":
try:
reponse_finale, couleur, nom_fournisseur, nom_modele = appeler_groq_api(full_prompt)
except Exception as e:
reponse_finale, couleur, nom_fournisseur, nom_modele = f"Erreur API Groq : {e}", "#ff0000", "ERREUR", "Panne Groq"
else: else:
ia_response = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "Réponds dans la langue de l'utilisateur."}, {"role": "user", "content": full_prompt}]) try:
reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#10b981", "SERVEUR LOCAL", "qwen2.5:3b" ia_response = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "Réponds dans la langue de l'utilisateur."}, {"role": "user", "content": full_prompt}])
if hasattr(ia_response, 'usage') and ia_response.usage: reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#10b981", "SERVEUR LOCAL", "qwen2.5:3b"
prompt_tokens = ia_response.usage.prompt_tokens except Exception as e:
completion_tokens = ia_response.usage.completion_tokens reponse_finale, couleur, nom_fournisseur, nom_modele = f"Erreur Serveur Local : {e}", "#ff0000", "ERREUR LOCAL", "Qwen HS"
if chat_id not in history:
history[chat_id] = {
"title": (prompt[:30] if prompt else "Nouvelle discussion") + "...",
"pinned": False,
"timestamp": time.time(),
"messages": []
}
# Sauvegarde
display_msg = f"{prompt}" + (f"\n📎 [Fichiers joints: {', '.join(file_names)}]" if file_names else "") display_msg = f"{prompt}" + (f"\n📎 [Fichiers joints: {', '.join(file_names)}]" if file_names else "")
history[chat_id]['messages'].append({"role": "user", "content": display_msg}) history[chat_id]['messages'].append({"role": "user", "content": display_msg})
history[chat_id]['messages'].append({"role": "ai", "content": reponse_finale, "intention": intention, "fournisseur": nom_fournisseur, "modele": nom_modele, "couleur": couleur}) history[chat_id]['messages'].append({"role": "ai", "content": reponse_finale, "intention": intention, "fournisseur": nom_fournisseur, "modele": nom_modele, "couleur": couleur})
@@ -495,7 +514,7 @@ async def post(request, session: dict):
return Div(f"Erreur : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;") return Div(f"Erreur : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;")
# ========================================== # ==========================================
# SECTION 5. ROUTES DE L'HISTORIQUE (Navigation native) # SECTION 5. ROUTES DE L'HISTORIQUE
# ========================================== # ==========================================
@rt('/favicon.ico') @rt('/favicon.ico')
def get(): def get():