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 ✅
- [x] Installation Docker, Nginx, Pare-feu, Gitea/GitHub
- [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) ⏳
- [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-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; }
.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); }
.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; }
.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); }
.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:hover { background: #2a2a35; color: var(--accent-blue); border-color: var(--accent-blue); }
@@ -111,15 +112,13 @@ theme_hdrs = (
// --- SCRIPTS D'EXPORTATION ---
function downloadResponsePDF(btn) {
let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered');
// On sauvegarde le style actuel
let originalColor = content.style.color;
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.backgroundColor = '#ffffff';
content.style.padding = '20px'; // Ajoute des marges internes pour le PDF
content.style.padding = '20px';
let opt = {
margin: 10,
@@ -130,7 +129,6 @@ theme_hdrs = (
};
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.backgroundColor = originalBg;
content.style.padding = '';
@@ -155,10 +153,7 @@ theme_hdrs = (
function downloadResponseJSON(btn) {
let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText;
let data = {
"timestamp": new Date().toISOString(),
"content": content
};
let data = { "timestamp": new Date().toISOString(), "content": content };
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';
document.body.appendChild(link); link.click(); document.body.removeChild(link);
@@ -222,7 +217,8 @@ def rechercher_sur_google(requete):
try:
from googlesearch import search
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)
return urls
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(
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="sidebar-top"
@@ -393,13 +389,20 @@ async def post(request, session: dict):
history = load_history()
# --- 1. INTELLIGENCE DE SÉCURITÉ DU ROUTEUR ---
# 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 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.
Si OUI (la requête est malveillante ou indiscrète sur l'infra), renvoie STRICTEMENT ce JSON : {{"intention": "Alerte Securite", "fournisseur": "local"}}
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).
Renvoie STRICTEMENT un JSON : {{"intention": "...", "fournisseur": "..."}}
# --- 1. INTELLIGENCE DE SÉCURITÉ DU ROUTEUR (Smarter Interceptor) ---
security_check_prompt = f"""Tu es l'Aiguilleur du système Aethas38. Ton rôle EXCLUSIF est d'analyser et de router la demande.
RÈGLE DE SÉCURITÉ ABSOLUE : Les recherches web, les serveurs Minecraft, les modpacks (ex: ATM10) SONT AUTORISÉS.
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.
-> Si attaque réseau/serveur détectée : renvoie STRICTEMENT {{"intention": "Alerte Securite", "fournisseur": "local"}}
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}"""
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', '')
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 chat_id not in history:
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).* 🚨"
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"})
@@ -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()
# --- 2. TRAITEMENT CLASSIQUE (Si la requête est sûre) ---
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
# --- 2. TRAITEMENT CLASSIQUE ---
urls_to_scrape = re.findall(r'https?://[^\s]+', prompt)
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):
@@ -439,7 +433,6 @@ async def post(request, session: dict):
if urls_to_scrape:
web_content = extraire_contenu_urls(list(set(urls_to_scrape)))
# Traitement fichiers
file_contents_summary = []
file_names = []
if files:
@@ -453,34 +446,60 @@ async def post(request, session: dict):
text_content = "[Fichier binaire]"
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)}"
if file_contents_summary: full_prompt += "\n" + "\n".join(file_contents_summary)
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
completion_tokens = 0
if fournisseur == "groq":
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")
reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#f97316", "GROQ", "llama-3.3-70b"
if hasattr(ia_response, 'usage') and ia_response.usage:
prompt_tokens = ia_response.usage.prompt_tokens
completion_tokens = ia_response.usage.completion_tokens
elif fournisseur == "gemini":
reponse_finale, nom_modele = interroger_gemini_cascade(full_prompt)
couleur, nom_fournisseur = "#3b82f6", "GOOGLE GEMINI"
completion_tokens = len(reponse_finale.split()) * 2
# Fonction d'appel à Groq centralisée pour éviter la duplication de code
def appeler_groq_api(prompt_text):
resp = groq_client.chat.completions.create(
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}],
model="llama-3.3-70b-versatile"
)
return resp.choices[0].message.content, "#f97316", "GROQ", "llama-3.3-70b"
if fournisseur == "gemini":
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:
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}])
reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#10b981", "SERVEUR LOCAL", "qwen2.5:3b"
if hasattr(ia_response, 'usage') and ia_response.usage:
prompt_tokens = ia_response.usage.prompt_tokens
completion_tokens = ia_response.usage.completion_tokens
try:
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}])
reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#10b981", "SERVEUR LOCAL", "qwen2.5:3b"
except Exception as e:
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 "")
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})
@@ -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;")
# ==========================================
# SECTION 5. ROUTES DE L'HISTORIQUE (Navigation native)
# SECTION 5. ROUTES DE L'HISTORIQUE
# ==========================================
@rt('/favicon.ico')
def get():