Public Access
feat(core): optimisation du routeur Qwen, augmentation des sources web et ajout de la cascade de secours Groq
This commit is contained in:
1 parent
e033876858
commit
a8f9ab28ed
2 files changed
+68
-48
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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,7 +411,7 @@ 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": []}
|
||||
@@ -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":
|
||||
# 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:
|
||||
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"
|
||||
if hasattr(ia_response, 'usage') and ia_response.usage:
|
||||
prompt_tokens = ia_response.usage.prompt_tokens
|
||||
completion_tokens = ia_response.usage.completion_tokens
|
||||
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():
|
||||
|
||||
Reference in new issue
Block a user