import os import json import uuid import time import re import requests from bs4 import BeautifulSoup from dotenv import load_dotenv from fasthtml.common import * from starlette.responses import FileResponse from openai import OpenAI from groq import Groq from google import genai # ========================================== # SECTION 1. INITIALISATION, SÉCURITÉ & HISTORIQUE # ========================================== load_dotenv() HISTORY_FILE = "aethas_history.json" def load_history(): if not os.path.exists(HISTORY_FILE): with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump({}, f) with open(HISTORY_FILE, 'r', encoding='utf-8') as f: return json.load(f) def save_history(history): with open(HISTORY_FILE, 'w', encoding='utf-8') as f: json.dump(history, f, indent=4) # --- SCRIPT DE NETTOYAGE AUTO --- def nettoyer_vieilles_discussions(): history = load_history() now = time.time() un_an_en_secondes = 365 * 24 * 60 * 60 a_supprimer = [] for chat_id, data in history.items(): if not data.get('pinned', False): if now - data.get('timestamp', now) > un_an_en_secondes: a_supprimer.append(chat_id) if a_supprimer: for chat_id in a_supprimer: del history[chat_id] save_history(history) print(f"🧹 AETHAS CORE : Nettoyage auto terminé. {len(a_supprimer)} discussion(s) de plus de 365 jours supprimée(s).") nettoyer_vieilles_discussions() svg_favicon = "%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cstyle%3E@keyframes pulse { 0% { opacity: 1; stroke-width: 5; } 50% { opacity: 0.5; stroke-width: 7; } 100% { opacity: 1; stroke-width: 5; } } .neon { animation: pulse 2s infinite; transform-origin: 50px 50px; transform: rotate(45deg); }%3C/style%3E%3Crect width='100' height='100' fill='%23121212'/%3E%3Cg class='neon'%3E%3Crect x='25' y='25' width='50' height='50' fill='none' stroke='%2300e5ff'/%3E%3Cpath d='M25 50 L75 50 M50 25 L50 75' stroke='%23b300ff'/%3E%3Ccircle cx='50' cy='50' r='8' fill='%23e0e0e0'/%3E%3C/g%3E%3C/svg%3E" theme_hdrs = ( Script(src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"), Script(src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"), Script(src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"), Link(rel="stylesheet", href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css"), Link(rel="icon", type="image/svg+xml", href=f"data:image/svg+xml;utf8,{svg_favicon}"), Style(""" :root { --bg-dark: #121212; --bg-panel: #1e1e24; --accent-blue: #00e5ff; --accent-purple: #b300ff; --text-main: #e0e0e0; } body { background-color: var(--bg-dark); color: var(--text-main); font-family: 'Segoe UI', Tahoma, sans-serif; margin: 0; display: flex; height: 100vh; overflow: hidden; box-sizing: border-box; } .sidebar { width: 280px; background-color: #0a0a0c; border-right: 1px solid #333; padding: 20px; display: flex; flex-direction: column; height: 100vh; box-sizing: border-box; } .sidebar-top { flex-shrink: 0; } .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; } .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: 1em; text-align:center; text-decoration:none; display:block;} .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:hover { background: #1a1a20; } .history-link { flex: 1; text-decoration: none; color: inherit; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display: block; padding-right: 10px; } .history-btn { background: none; border: none; color: #888; cursor: pointer; padding: 2px 5px; font-size: 1.1em; } .history-btn:hover { color: var(--accent-blue); } .main-content { flex: 1; display: flex; flex-direction: column; height: 100vh; position: relative; box-sizing: border-box; } .chat-container { flex: 1; padding: 20px; max-width: 1000px; margin: 0 auto; width: 100%; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; padding-bottom: 140px; box-sizing: border-box; } .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; 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); } .code-header { background: #111; padding: 8px 15px; display: flex; justify-content: space-between; align-items: center; border-radius: 8px 8px 0 0; font-family: monospace; font-size: 0.85em; border: 1px solid #333; border-bottom: none; } .code-header button { background: #333; border: 1px solid #444; color: white; cursor: pointer; padding: 4px 10px; border-radius: 5px; margin-left: 8px; transition: 0.2s; } .code-header button:hover { background: var(--accent-blue); color: black; border-color: var(--accent-blue); } .markdown-rendered pre { margin-top: 0; border-radius: 0 0 8px 8px; border: 1px solid #333; padding: 15px; overflow-x: auto; } .input-area { position: absolute; bottom: 0; width: 100%; padding: 20px; background: linear-gradient(0deg, #121212 85%, transparent); display: flex; justify-content: center; box-sizing: border-box; } .input-container-box { width: 100%; max-width: 900px; background: #1e1e24; border-radius: 24px; border: 1px solid #444; padding: 10px 15px; box-sizing: border-box; display: flex; flex-direction: column; gap: 8px; } #file-preview-list { display: flex; flex-wrap: wrap; gap: 6px; max-height: 60px; overflow-y: auto; } .file-pill { background: #2a2a35; border: 1px solid #555; color: var(--accent-blue); padding: 4px 10px; border-radius: 12px; font-size: 0.8em; display: flex; align-items: center; gap: 6px; } .input-row { display: flex; align-items: flex-end; gap: 10px; width: 100%; } .input-field { background: transparent; border: none; color: white; resize: none; flex: 1; font-size: 1em; max-height: 120px; overflow-y: auto; padding: 8px; font-family: inherit; box-sizing: border-box; } .input-field:focus { outline: none; } .file-btn { background: #2a2a35; border: 1px solid #444; color: var(--accent-blue); width: 36px !important; height: 36px !important; min-width: 36px !important; max-width: 36px !important; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.3em; flex-shrink: 0 !important; box-sizing: border-box; text-decoration: none; } .file-btn:hover { background: rgba(0, 229, 255, 0.2); border-color: var(--accent-blue); } .btn-submit { background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); color: white; border: none; width: 36px !important; height: 36px !important; min-width: 36px !important; max-width: 36px !important; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.1em; flex-shrink: 0 !important; box-sizing: border-box; } .btn-submit:active { transform: scale(0.95); } .htmx-indicator { display: none; } .htmx-request .htmx-indicator { display: flex; } .htmx-request.htmx-indicator { display: flex; } .tech-loader-container { display: flex; align-items: center; gap: 15px; color: var(--accent-blue); font-style: italic; background: var(--bg-panel); padding: 15px 20px; border-radius: 15px 15px 15px 0; border: 1px dashed var(--accent-blue); width: fit-content; margin-bottom: 20px; font-size: 0.9em; box-shadow: 0 0 10px rgba(0, 229, 255, 0.1); } .tech-spinner { width: 20px; height: 20px; border: 3px solid rgba(0, 229, 255, 0.2); border-top-color: var(--accent-blue); border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } """), Script(""" function renderMarkdown() { try { document.querySelectorAll('.markdown-raw').forEach(el => { if (el.dataset.rendered) return; let container = el.nextElementSibling; if (typeof marked !== 'undefined') { container.innerHTML = marked.parse(el.textContent); } else { container.innerText = el.textContent; } el.dataset.rendered = "true"; el.classList.remove('markdown-raw'); container.querySelectorAll('pre').forEach(pre => { try { let code = pre.querySelector('code'); if(code) { let rawLang = code.className.replace('language-', '') || 'texte'; let lang = rawLang.toUpperCase(); let header = document.createElement('div'); header.className = 'code-header'; header.innerHTML = '' + lang + '
'; pre.parentNode.insertBefore(header, pre); if (typeof hljs !== 'undefined') hljs.highlightElement(code); } } catch(err) { console.warn("Aethas - Erreur formatage code", err); } }); }); let chat = document.getElementById('chat-history'); if (chat) chat.scrollTop = chat.scrollHeight; } catch(globalErr) { console.error("Aethas - Erreur fatale rendu Markdown", globalErr); } } function downloadResponsePDF(btn) { try { let renderedNode = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); let rawNode = btn.closest('.ai-bubble').querySelector('.markdown-raw'); let originalContent = (renderedNode && renderedNode.innerHTML.trim() !== "") ? renderedNode : rawNode; let content = originalContent.cloneNode(true); let wrapper = document.createElement('div'); wrapper.style.backgroundColor = '#ffffff'; wrapper.style.padding = '20px'; wrapper.style.fontFamily = 'Arial, sans-serif'; wrapper.style.color = '#000000'; content.querySelectorAll('*').forEach(el => { el.style.color = '#000000'; if (el.tagName === 'PRE' || el.tagName === 'CODE' || el.tagName === 'TABLE' || el.tagName === 'TH' || el.tagName === 'TD') { el.style.backgroundColor = '#f4f4f4'; el.style.borderColor = '#aaaaaa'; el.style.borderWidth = '1px'; el.style.borderStyle = 'solid'; el.style.borderCollapse = 'collapse'; } else { el.style.backgroundColor = 'transparent'; } }); wrapper.appendChild(content); let opt = { margin: 10, filename: 'Aethas38_Reponse.pdf', image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, backgroundColor: '#ffffff', useCORS: true }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } }; html2pdf().set(opt).from(wrapper).save(); } catch (err) { alert("Erreur lors de la génération du PDF."); } } function downloadResponseTXT(btn) { let rendered = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; let raw = btn.closest('.ai-bubble').querySelector('.markdown-raw').innerText; let finalContent = rendered ? rendered : raw; let blob = new Blob([finalContent], {type: 'text/plain;charset=utf-8'}); let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.txt'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function downloadResponseDOC(btn) { let renderedNode = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); let content = (renderedNode && renderedNode.innerHTML.trim() !== "") ? renderedNode.innerHTML : btn.closest('.ai-bubble').querySelector('.markdown-raw').innerText; let preHtml = "Aethas Export"; let postHtml = ""; let blob = new Blob(['\\ufeff', preHtml + content + postHtml], { type: 'application/msword' }); let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.doc'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } function downloadResponseJSON(btn) { let rendered = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; let raw = btn.closest('.ai-bubble').querySelector('.markdown-raw').innerText; let finalContent = rendered ? rendered : raw; let data = { "timestamp": new Date().toISOString(), "content": finalContent }; 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); } function copyCode(btn) { let pre = btn.closest('.code-header').nextElementSibling; let code = pre.querySelector('code').innerText; navigator.clipboard.writeText(code); btn.innerText = "✅ Copié !"; setTimeout(() => btn.innerText = "📋 Copier", 2000); } function handleFileSelection(input) { let previewList = document.getElementById('file-preview-list'); previewList.innerHTML = ''; if (input.files) { for (let i = 0; i < input.files.length; i++) { let pill = document.createElement('div'); pill.className = 'file-pill'; pill.innerHTML = '📎 ' + input.files[i].name; previewList.appendChild(pill); } } } function autoResize(textarea) { if(!textarea) return; textarea.style.height = 'auto'; textarea.style.height = textarea.scrollHeight + 'px'; } function shareWhatsApp(btn) { let rendered = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; let raw = btn.closest('.ai-bubble').querySelector('.markdown-raw').innerText; let content = rendered ? rendered : raw; window.open('https://api.whatsapp.com/send?text=' + encodeURIComponent("🤖 *Aethas38 (IA) :*\n\n" + content), '_blank'); } function shareEmail(btn) { let rendered = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; let raw = btn.closest('.ai-bubble').querySelector('.markdown-raw').innerText; let content = rendered ? rendered : raw; window.open('mailto:?subject=Réponse Aethas38 IA&body=' + encodeURIComponent(content), '_self'); } """) ) app, rt = fast_app(hdrs=theme_hdrs, secret_key="super_secret_aethas") # ========================================== # SECTION 2. CONNEXION AUX IA & RECHERCHE WEB # ========================================== local_client = OpenAI(base_url="http://192.168.1.20:11434/v1", api_key="ollama") groq_client = Groq(api_key=os.getenv("GROQ_API_KEY")) gemini_client = genai.Client(api_key=os.getenv("GEMINI_API_KEY")) def interroger_gemini_cascade(prompt): modeles_a_tester = ["gemini-3.1-pro-preview", "gemini-3.1-pro", "gemini-2.5-pro", "gemini-3.7-flash", "gemini-3.6-flash"] for nom_modele in modeles_a_tester: try: config = {"system_instruction": "Réponds toujours dans la langue de l'utilisateur. Utilise exclusivement le format Markdown pour les tableaux et la mise en forme. N'utilise JAMAIS de code LaTeX."} chat = gemini_client.chats.create(model=nom_modele, config=config) ia_response = chat.send_message(prompt) if not ia_response.text: raise Exception("Réponse vide de Google.") return ia_response.text, nom_modele except Exception: continue raise Exception("Tous les modèles Gemini sont indisponibles actuellement.") def rechercher_sur_google(requete): try: from googlesearch import search urls = [] for url in search(requete, num_results=5, lang="fr"): urls.append(url) return urls except ImportError: return [] def extraire_contenu_urls(urls_list): scraped_text = "" for url in urls_list: try: resp = requests.get(url, timeout=5, headers={"User-Agent": "Mozilla/5.0"}) soup = BeautifulSoup(resp.text, 'html.parser') for script in soup(["script", "style", "nav", "footer"]): script.extract() scraped_text += f"\n\n--- Source web extraite ({url}) ---\n{soup.get_text(separator=' ', strip=True)[:3000]}" except Exception as e: scraped_text += f"\n\n[Impossible de récupérer le contenu de {url}: {e}]" return scraped_text 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"} def calculer_cout_ia(fournisseur, prompt_tokens, completion_tokens): if fournisseur == "GROQ": input_cost_per_m, output_cost_per_m = 0.59, 0.79 elif fournisseur == "GOOGLE GEMINI": input_cost_per_m, output_cost_per_m = 0.075, 0.30 else: return 0.00 return round(((prompt_tokens * input_cost_per_m) + (completion_tokens * output_cost_per_m)) / 1_000_000, 6) # ========================================== # SECTION 3. INTERFACE UTILISATEUR & HISTORIQUE # ========================================== def render_user_msg(text): return Div(P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"), cls="user-bubble") def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur): encart_info = Div( Span(f"🎯 {intention}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"), Span(f"🏢 {nom_fournisseur}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"), Span(f"🧠 {nom_modele}", style=f"margin-right: 10px; color:{couleur}; font-weight:bold; font-size:0.8em;"), style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;" ) actions = Div( Button("📄 PDF", onclick="downloadResponsePDF(this)", cls="msg-action-btn", title="Télécharger en PDF"), Button("📘 DOC", onclick="downloadResponseDOC(this)", cls="msg-action-btn", title="Télécharger en Word"), Button("📝 TXT", onclick="downloadResponseTXT(this)", cls="msg-action-btn", title="Télécharger en Texte"), Button("🗄️ JSON", onclick="downloadResponseJSON(this)", cls="msg-action-btn", title="Télécharger en JSON"), Button("💬 WhatsApp", onclick="shareWhatsApp(this)", cls="msg-action-btn", title="Partager sur WhatsApp"), Button("📧 Email", onclick="shareEmail(this)", cls="msg-action-btn", title="Envoyer par Email"), cls="msg-actions" ) return Div( encart_info, Div(text, cls="markdown-raw", style="display:none;"), Div(cls="markdown-rendered", style="line-height: 1.6;"), actions, cls="ai-bubble", style=f"border-left: 4px solid {couleur};" ) def render_sidebar(): history = load_history() sorted_chats = sorted(history.items(), key=lambda x: (not x[1].get('pinned', False), -x[1].get('timestamp', 0))) items = [] for chat_id, data in sorted_chats: pin_icon = "📌" if data.get('pinned', False) else "📍" items.append( Div( A(Span(data['title'], style="color:inherit;"), href=f"/load_chat/{chat_id}", cls="history-link"), Button(pin_icon, cls="history-btn", hx_post=f"/pin/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"), Button("✏️", cls="history-btn", hx_post=f"/rename/{chat_id}", hx_prompt="Nouveau nom :", hx_target="#sidebar-content", hx_swap="outerHTML"), Button("🗑️", cls="history-btn", hx_post=f"/delete/{chat_id}", hx_target="#sidebar-content", hx_swap="outerHTML"), cls="history-item", id=f"chat-item-{chat_id}" ) ) return Div(*items, id="sidebar-content", cls="sidebar-history", hx_swap_oob="true") @rt('/') def get(session): if 'current_chat_id' not in session or not session['current_chat_id']: session['current_chat_id'] = str(uuid.uuid4()) chat_id = session['current_chat_id'] history = load_history() messages_ui = [] if chat_id in history: for msg in history[chat_id].get('messages', []): if msg['role'] == 'user': messages_ui.append(render_user_msg(msg['content'])) else: messages_ui.append(render_ai_msg(msg['content'], msg.get('intention', ''), msg.get('fournisseur', ''), msg.get('modele', ''), msg.get('couleur', '#333'))) if messages_ui: messages_ui.append(Script("setTimeout(renderMarkdown, 150);")) return Title("Aethas38 - Mine_AI"), Body( Div( Div( 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"), A("💾 Exporter JSON", href="/export_json", target="_blank", cls="export-btn", title="Télécharger la conversation Globale"), cls="nav-buttons" ), cls="sidebar-top" ), render_sidebar(), Div( P("👤 Admin (Xavier)", style="color: #aaa; margin: 0; font-size: 0.9em;"), cls="sidebar-footer" ), cls="sidebar" ), Div( Div(*messages_ui, id="chat-history", cls="chat-container", style="padding-bottom: 20px;"), Div( Div(Div(cls="tech-spinner"), Span("Aethas Core analyse la requête..."), cls="tech-loader-container"), id="global-loader", cls="htmx-indicator", style="max-width: 1000px; margin: 0 auto; width: 100%; padding: 0 20px; box-sizing: border-box; padding-bottom: 120px;" ), Div( Form( Input(type="hidden", name="chat_id", value=chat_id), Div( Div(id="file-preview-list"), Div( Label("+", _for="file-upload", cls="file-btn", title="Importer des fichiers (max 10)"), Input(type="file", name="files", id="file-upload", multiple=True, style="display:none;", onchange="handleFileSelection(this)"), Textarea(name="prompt", id="prompt-input", placeholder="Entrez votre requête ou déposez vos fichiers / liens ici...", cls="input-field", rows="1", oninput="autoResize(this)"), Button("↑", type="submit", cls="btn-submit", title="Exécuter"), cls="input-row" ), cls="input-container-box" ), hx_post="/chat", hx_target="#chat-history", hx_swap="beforeend", hx_indicator="#global-loader", hx_on__after_request="this.reset(); document.getElementById('file-preview-list').innerHTML=''; document.getElementById('prompt-input').style.height='auto';", enctype="multipart/form-data", style="width: 100%; display: flex; justify-content: center;" ), cls="input-area" ), cls="main-content" ) ) # ========================================== # SECTION 4. LE MOTEUR DE ROUTAGE (IA + WEB + FICHIERS) # ========================================== @rt('/chat') async def post(request, session: dict): try: form = await request.form() prompt = form.get("prompt", "") chat_id = form.get("chat_id") files = form.getlist("files") if not chat_id: chat_id = str(uuid.uuid4()) session['current_chat_id'] = chat_id history = load_history() 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) decision = nettoyer_decision_router(router_response.choices[0].message.content) intention = decision.get('intention', '') fournisseur = decision.get("fournisseur", "local").lower() 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"}) save_history(history) return render_user_msg(prompt), render_ai_msg(reponse_securite, "Alerte Sécurité", "AETHAS CORE", "Cyber-Sec", "#ff0044"), Script("renderMarkdown();"), render_sidebar() 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): urls_to_scrape.extend(rechercher_sur_google(prompt)) web_content = "" if urls_to_scrape: web_content = extraire_contenu_urls(list(set(urls_to_scrape))) file_contents_summary = [] file_names = [] if files: for file in files[:10]: if file and file.filename: file_names.append(file.filename) content_bytes = await file.read() try: text_content = content_bytes.decode('utf-8', errors='ignore') except Exception: text_content = "[Fichier binaire]" file_contents_summary.append(f"\n--- Fichier joint: {file.filename} ---\n{text_content}") 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 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}" 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. Utilise uniquement Markdown. Pas de code LaTeX."}, {"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: reponse_finale, nom_modele = interroger_gemini_cascade(full_prompt) couleur, nom_fournisseur = "#3b82f6", "GOOGLE GEMINI" except Exception as e_gemini: 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. Utilise le Markdown."}, {"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": [] } 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}) save_history(history) user_bubble = render_user_msg(display_msg) ai_bubble = render_ai_msg(reponse_finale, intention, nom_fournisseur, nom_modele, couleur) return user_bubble, ai_bubble, Script("renderMarkdown();"), render_sidebar() except Exception as e: return Div(f"Erreur : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;") # ========================================== # SECTION 5. ROUTES DE L'HISTORIQUE # ========================================== @rt('/favicon.ico') def get(): return HTTPResponse(status_code=204) @rt('/new_chat') def get(session): session['current_chat_id'] = str(uuid.uuid4()) return RedirectResponse('/', status_code=303) @rt('/load_chat/{chat_id}') def get(chat_id: str, session): if chat_id and chat_id != "null": session['current_chat_id'] = chat_id return RedirectResponse('/', status_code=303) @rt('/rename/{chat_id}') def post(chat_id: str, req): if not chat_id or chat_id == "null": return render_sidebar() nouveau_nom = req.headers.get("HX-Prompt") if nouveau_nom: history = load_history() if chat_id in history: history[chat_id]['title'] = nouveau_nom save_history(history) return render_sidebar() @rt('/pin/{chat_id}') def post(chat_id: str): if not chat_id or chat_id == "null": return render_sidebar() history = load_history() if chat_id in history: history[chat_id]['pinned'] = not history[chat_id].get('pinned', False) save_history(history) return render_sidebar() @rt('/delete/{chat_id}') def post(chat_id: str): if not chat_id or chat_id == "null": return render_sidebar() history = load_history() if chat_id in history: del history[chat_id] save_history(history) return render_sidebar() # --- ROUTE : EXPORT JSON COMPLET --- @rt('/export_json') def get(): return FileResponse(HISTORY_FILE, media_type='application/json', filename='Aethas38_Discussion_Globale.json') if __name__ == '__main__': serve(port=5001)