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
+73 -53

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)
+71 -52
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,7 +411,7 @@ 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": []}
@@ -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():