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 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) 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; } /* 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: 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; } .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); } """), Script(""" function renderMarkdown() { document.querySelectorAll('.markdown-raw').forEach(el => { let container = el.nextElementSibling; container.innerHTML = marked.parse(el.textContent); el.classList.remove('markdown-raw'); container.querySelectorAll('pre').forEach(pre => { let code = pre.querySelector('code'); let lang = code.className.replace('language-', '') || 'texte'; let header = document.createElement('div'); header.className = 'code-header'; header.innerHTML = `${lang.toUpperCase()}
`; pre.parentNode.insertBefore(header, pre); hljs.highlightElement(code); }); }); let chat = document.getElementById('chat-history'); chat.scrollTop = chat.scrollHeight; } // --- SCRIPTS D'EXPORTATION --- function downloadResponsePDF(btn) { let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); let originalColor = content.style.color; let originalBg = content.style.backgroundColor; // Forçage du texte en noir sur blanc pour le PDF content.style.color = '#000000'; content.style.backgroundColor = '#ffffff'; content.style.padding = '20px'; let opt = { margin: 10, filename: 'Aethas38_Reponse.pdf', image: { type: 'jpeg', quality: 0.98 }, html2canvas: { scale: 2, backgroundColor: '#ffffff' }, jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } }; html2pdf().set(opt).from(content).save().then(() => { content.style.color = originalColor; content.style.backgroundColor = originalBg; content.style.padding = ''; }); } function downloadResponseTXT(btn) { let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); let blob = new Blob([content.innerText], {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 content = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerHTML; 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 content = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText; 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); } 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 exportChat() { let chat = document.getElementById('chat-history'); if(!chat) return; let blob = new Blob([chat.innerText], {type: 'text/plain;charset=utf-8'}); let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Discussion_Globale.txt'; document.body.appendChild(link); link.click(); document.body.removeChild(link); } 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) { textarea.style.height = 'auto'; textarea.style.height = textarea.scrollHeight + 'px'; } """) ) 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: chat = gemini_client.chats.create(model=nom_modele, config={"system_instruction": "Réponds toujours dans la langue de l'utilisateur."}) ia_response = chat.send_message(prompt) 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 = [] # 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: 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"), 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, 100);")) 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"), Button("💾 Exporter JSON", cls="export-btn", onclick="exportChat()", 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"), 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_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() # --- 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) decision = nettoyer_decision_router(router_response.choices[0].message.content) intention = decision.get('intention', '') fournisseur = decision.get("fournisseur", "local").lower() # 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"}) 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() # --- 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): 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}" prompt_tokens = len(full_prompt.split()) * 2 completion_tokens = 0 # 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" 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() if __name__ == '__main__': serve(port=5001)