maj auto 2

This commit is contained in:
xavier committed 2026-08-16 23:26:07 +02:00
1 parent d47bd67391
commit f91b5ae215
11 files changed
+647 -477

No files matched your search

+177 -477
View File
@@ -1,49 +1,19 @@
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
# ==========================================
# Import propre depuis le dossier modules
from modules.database import load_history, save_history, nettoyer_vieilles_discussions, HISTORY_FILE
from modules.scraper import rechercher_sur_google, extraire_contenu_urls
from modules.ai_engine import local_client, interroger_gemini_cascade, appeler_groq_api
from modules.router import nettoyer_decision_router
from modules.ui_components import render_user_msg, render_ai_msg, render_sidebar
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"
@@ -52,6 +22,7 @@ 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"),
Script(src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.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}"),
@@ -79,39 +50,35 @@ theme_hdrs = (
.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; }
.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; word-break: break-word; overflow-wrap: break-word; }
.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; word-break: break-word; overflow-wrap: break-word; }
.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; }
.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 { background: #16161a; 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-actions-group { display: flex; gap: 8px; }
.code-header button { background: #2a2a35; border: 1px solid #444; color: #e0e0e0; cursor: pointer; padding: 4px 10px; border-radius: 5px; font-size: 0.85em; 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; }
.markdown-rendered pre { margin-top: 0; border-radius: 0 0 8px 8px; border: 1px solid #333; padding: 15px; overflow-x: auto; background-color: #0d0d0f !important; }
.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; }
#file-preview-list { display: flex; flex-wrap: wrap; gap: 6px; max-height: 80px; overflow-y: auto; padding-bottom: 5px; }
.file-pill { background: #2a2a35; border: 1px solid #555; color: var(--accent-blue); padding: 5px 12px; border-radius: 12px; font-size: 0.8em; display: flex; align-items: center; gap: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.2); }
.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 { background: transparent; border: none; color: white; resize: none; flex: 1; font-size: 1em; height: 36px; max-height: 140px; overflow-y: hidden; padding: 8px; font-family: inherit; box-sizing: border-box; line-height: 20px; }
.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); }
.file-btn { background: #2a2a35; border: 1px solid #444; color: var(--accent-blue); width: 36px !important; height: 36px !important; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.3em; text-decoration: none; flex-shrink: 0; }
.btn-submit { background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); color: white; border: none; width: 36px !important; height: 36px !important; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.1em; flex-shrink: 0; }
.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-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; }
.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); } }
"""),
@@ -122,332 +89,220 @@ theme_hdrs = (
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;
}
container.innerHTML = (typeof marked !== 'undefined') ? marked.parse(el.textContent) : 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();
if (code && !pre.previousElementSibling?.classList.contains('code-header')) {
let rawLang = code.className.replace('language-', '') || 'snippet';
let header = document.createElement('div');
header.className = 'code-header';
header.innerHTML = '<span style="color:#00e5ff; font-weight:bold;">' + lang + '</span><div><button onclick="copyCode(this)">📋 Copier</button></div>';
header.innerHTML = `
<span style="color:#00e5ff; font-weight:bold;">${rawLang.toUpperCase()}</span>
<div class="code-actions-group">
<button onclick="copyCodeBlock(this)">📋 Copier</button>
<button onclick="downloadCodeZip(this, '${rawLang}')">📦 ZIP</button>
</div>`;
pre.parentNode.insertBefore(header, pre);
if (typeof hljs !== 'undefined') hljs.highlightElement(code);
}
} catch(err) { console.warn("Aethas - Erreur formatage code", err); }
} catch(err) { console.warn("Erreur entête code", err); }
});
});
let chat = document.getElementById('chat-history');
if (chat) chat.scrollTop = chat.scrollHeight;
} catch(globalErr) {
console.error("Aethas - Erreur fatale rendu Markdown", globalErr);
} catch(e) { console.error("Erreur markdown", e); }
}
function autoResize(textarea) {
if (!textarea) return;
textarea.style.height = '36px';
let scrollH = textarea.scrollHeight;
if (scrollH > 100) {
textarea.style.height = '100px';
textarea.style.overflowY = 'auto';
} else {
textarea.style.height = scrollH + 'px';
textarea.style.overflowY = 'hidden';
}
}
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';
document.addEventListener('DOMContentLoaded', () => {
const ta = document.getElementById('prompt-input');
const dropZone = document.querySelector('.input-container-box');
const fileInput = document.getElementById('file-upload');
if (ta) {
ta.addEventListener('input', () => autoResize(ta));
ta.addEventListener('paste', (e) => {
let items = (e.clipboardData || e.originalEvent.clipboardData).items;
let dt = new DataTransfer();
if (fileInput && fileInput.files) {
for (let f of fileInput.files) dt.items.add(f);
}
let hasFiles = false;
for (let i = 0; i < items.length; i++) {
if (items[i].kind === 'file') {
let blob = items[i].getAsFile();
if (blob) { dt.items.add(blob); hasFiles = true; }
}
}
if (hasFiles) {
e.preventDefault();
fileInput.files = dt.files;
handleFileSelection(fileInput);
} else {
el.style.backgroundColor = 'transparent';
setTimeout(() => autoResize(ta), 20);
}
});
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);
}
if (dropZone && fileInput) {
['dragenter', 'dragover'].forEach(eName => dropZone.addEventListener(eName, e => { e.preventDefault(); dropZone.style.borderColor = 'var(--accent-blue)'; }));
['dragleave', 'drop'].forEach(eName => dropZone.addEventListener(eName, e => { e.preventDefault(); dropZone.style.borderColor = '#444'; }));
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
let dt = new DataTransfer();
if (fileInput.files) { for (let f of fileInput.files) dt.items.add(f); }
for (let f of e.dataTransfer.files) { dt.items.add(f); }
fileInput.files = dt.files;
handleFileSelection(fileInput);
});
}
});
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 = "<html xmlns:o='urn:schemas-microsoft-com:office:office' xmlns:w='urn:schemas-microsoft-com:office:word' xmlns='http://www.w3.org/TR/REC-html40'><head><meta charset='utf-8'><title>Aethas Export</title></head><body style='font-family: Arial, sans-serif; color: #000000; background-color: #ffffff;'>";
let postHtml = "</body></html>";
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) {
function copyCodeBlock(btn) {
let pre = btn.closest('.code-header').nextElementSibling;
let code = pre.querySelector('code').innerText;
navigator.clipboard.writeText(code);
navigator.clipboard.writeText(pre.querySelector('code').innerText);
btn.innerText = "✅ Copié !";
setTimeout(() => btn.innerText = "📋 Copier", 2000);
}
function downloadCodeZip(btn, lang) {
let pre = btn.closest('.code-header').nextElementSibling;
let code = pre.querySelector('code').innerText;
let ext = lang === 'python' ? 'py' : (lang === 'javascript' ? 'js' : (lang === 'html' ? 'html' : 'txt'));
let fileName = 'aethas_snippet.' + ext;
if (typeof JSZip !== 'undefined') {
let zip = new JSZip();
zip.file(fileName, code);
zip.generateAsync({type: "blob"}).then(function(content) {
let link = document.createElement('a');
link.href = URL.createObjectURL(content);
link.download = "aethas_code_snippet.zip";
link.click();
});
} else {
let blob = new Blob([code], {type: 'text/plain;charset=utf-8'});
let link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
link.click();
}
}
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;
let file = input.files[i];
let pill = document.createElement('div');
pill.className = 'file-pill';
pill.innerHTML = `📎 ${file.name} <span style="cursor:pointer; color:#ff5555; margin-left:4px;" onclick="removeFile(${i})">✕</span>`;
previewList.appendChild(pill);
}
}
}
function autoResize(textarea) {
if(!textarea) return;
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
function removeFile(index) {
let fileInput = document.getElementById('file-upload');
let dt = new DataTransfer();
let { files } = fileInput;
for (let i = 0; i < files.length; i++) {
if (i !== index) dt.items.add(files[i]);
}
fileInput.files = dt.files;
handleFileSelection(fileInput);
}
function downloadResponsePDF(btn) {
let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered');
html2pdf().set({ margin: 10, filename: 'Aethas38_Reponse.pdf', html2canvas: { scale: 2 }, jsPDF: { unit: 'mm', format: 'a4' } }).from(content).save();
}
function downloadResponseTXT(btn) {
let text = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText;
let blob = new Blob([text], {type: 'text/plain;charset=utf-8'});
let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.txt'; link.click();
}
function downloadResponseDOC(btn) {
let html = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerHTML;
let blob = new Blob(['\\ufeff' + html], { type: 'application/msword' });
let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.doc'; link.click();
}
function downloadResponseJSON(btn) {
let text = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText;
let blob = new Blob([JSON.stringify({content: text}, null, 2)], {type: 'application/json;charset=utf-8'});
let link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = 'Aethas38_Reponse.json'; link.click();
}
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');
let text = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText;
window.open('https://api.whatsapp.com/send?text=' + encodeURIComponent(text), '_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');
let text = btn.closest('.ai-bubble').querySelector('.markdown-rendered').innerText;
window.open('mailto:?subject=Aethas38&body=' + encodeURIComponent(text), '_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);"))
msgs = [render_user_msg(m['content']) if m['role']=='user' else render_ai_msg(m['content'], m.get('intention',''), m.get('fournisseur',''), m.get('modele',''), m.get('couleur','#333')) for m in history.get(chat_id, {}).get('messages', [])]
if msgs: msgs.append(Script("setTimeout(renderMarkdown, 150);"))
return Title("Aethas38 - Mine_AI"), Body(
return Title("Aethas38 - Core"), 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"
),
Div(A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"), A("💾 Exporter JSON", href="/export_json", target="_blank", cls="export-btn"), cls="nav-buttons"),
cls="sidebar-top"
),
render_sidebar(),
Div(
P("👤 Admin (Xavier)", style="color: #aaa; margin: 0; font-size: 0.9em;"),
cls="sidebar-footer"
),
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(*msgs, id="chat-history", cls="chat-container"),
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)"),
Label("+", _for="file-upload", cls="file-btn"),
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"),
Textarea(name="prompt", id="prompt-input", placeholder="Requête, lien ou glissez vos fichiers ici...", cls="input-field", rows="1", oninput="autoResize(this)"),
Button("↑", type="submit", cls="btn-submit"),
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;"
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='36px';",
enctype="multipart/form-data", style="width: 100%; display: flex; justify-content: center;"
),
cls="input-area"
),
@@ -455,188 +310,33 @@ def get(session):
)
)
# ==========================================
# 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")
chat_id = form.get("chat_id", str(uuid.uuid4()))
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()
sec_prompt = f"""Analyse et route cette demande. Si recherche web nécessaire ("va sur", "site", "historique", "version", "cherche", etc.) -> choisis "gemini". Si code/technique -> "groq". Sinon -> "local".
Renvoie STRICTEMENT un JSON : {{"intention": "...", "fournisseur": "gemini|groq|local"}}
Requête : {prompt}"""
router_res = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "JSON uniquement."}, {"role": "user", "content": sec_prompt}], temperature=0.1)
decision = nettoyer_decision_router(router_res.choices[0].message.content)
intention, fournisseur = decision.get('intention', 'Analyse'), decision.get('fournisseur', 'gemini').lower()
@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()
urls = re.findall(r'https?://[^\s]+', prompt)
mots_cles = ["cherche", "recherche", "sur internet", "sur le web", "sur le net", "va sur", "trouve", "site", "donne", "historique", "version"]
if any(m in prompt.lower() for m in mots_cles):
urls.extend(rechercher_sur_google(prompt))
@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)
web_text = extraire_contenu_urls(list(set(urls))) if urls else ""
file_summary = []
file_names = []
for f in files[:10]:
if f && f.filename: # Wait, Python uses `and` instead of `&&`