maj auto 2

This commit is contained in:
xavier committed 2026-08-17 12:45:08 +02:00
1 parent db2b137d6b
commit 98bd4d7fbf
5 files changed
+142 -73

No files matched your search

+2 -2
View File
@@ -27,6 +27,6 @@ def interroger_gemini_cascade(prompt):
def appeler_groq_api(prompt_text): def appeler_groq_api(prompt_text):
resp = groq_client.chat.completions.create( 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}], 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" model="mixtral-8x7b-32768" # 🛠️ CORRECTION : Modèle Groq stable avec 32 768 tokens de contexte
) )
return resp.choices[0].message.content, "#f97316", "GROQ", "llama-3.3-70b" return resp.choices[0].message.content, "#f97316", "GROQ", "mixtral-8x7b-32768"
+45 -19
View File
@@ -9,6 +9,7 @@ from modules.ai_engine import local_client, interroger_gemini_cascade, appeler_g
from modules.ui_components import render_user_msg, render_ai_msg, render_sidebar from modules.ui_components import render_user_msg, render_ai_msg, render_sidebar
async def handle_chat_request(request, session): async def handle_chat_request(request, session):
start_time = time.time()
try: try:
form = await request.form() form = await request.form()
prompt = form.get("prompt", "") prompt = form.get("prompt", "")
@@ -17,22 +18,8 @@ async def handle_chat_request(request, session):
session['current_chat_id'] = chat_id session['current_chat_id'] = chat_id
history = load_history() history = load_history()
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()
urls = re.findall(r'https?://[^\s]+', prompt) # Extraction des fichiers
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))
web_text = extraire_contenu_urls(list(set(urls))) if urls else ""
file_summary = [] file_summary = []
file_names = [] file_names = []
for f in files[:10]: for f in files[:10]:
@@ -41,10 +28,37 @@ async def handle_chat_request(request, session):
content = await f.read() content = await f.read()
file_summary.append(f"\n--- Fichier: {f.filename} ---\n{content.decode('utf-8', errors='ignore')}") file_summary.append(f"\n--- Fichier: {f.filename} ---\n{content.decode('utf-8', errors='ignore')}")
liste_fichiers_str = ', '.join(file_names) if file_names else 'Aucun fichier'
sec_prompt = f"""Tu es l'Aiguilleur système.
RÈGLES STRICTES DE ROUTAGE :
1. "gemini" : OBLIGATOIRE si des fichiers sont joints à analyser (ex: {liste_fichiers_str}), ou pour des requêtes de rédaction complexe, comparaisons, ou recherche web.
2. "groq" : Pour le code, la programmation, l'architecture système ou les mathématiques.
3. "local" : UNIQUEMENT pour les salutations, blagues ou questions très simples sans fichiers. NE JAMAIS utiliser pour analyser des documents.
Renvoie STRICTEMENT un JSON : {{"intention": "Résumé de l'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()
# 🛡️ PROTECTION HARDCODÉE : Si des fichiers sont présents, forcer Gemini
if file_names:
fournisseur = "gemini"
intention += " (Forcé Gemini via Sécurité)"
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))
web_text = extraire_contenu_urls(list(set(urls))) if urls else ""
full_prompt = prompt + ("\n" + "\n".join(file_summary) if file_summary else "") full_prompt = prompt + ("\n" + "\n".join(file_summary) if file_summary else "")
if web_text: if web_text:
full_prompt += f"\n\n[Contexte web extrait :\n{web_text}\nUtilise ces informations pour répondre précisément.]" full_prompt += f"\n\n[Contexte web extrait :\n{web_text}\nUtilise ces informations pour répondre précisément.]"
# Routage avec Fallback robuste
if fournisseur == "gemini": if fournisseur == "gemini":
try: try:
reponse, modele = interroger_gemini_cascade(full_prompt) reponse, modele = interroger_gemini_cascade(full_prompt)
@@ -53,7 +67,13 @@ async def handle_chat_request(request, session):
reponse, couleur, fournisseur_nom, modele = appeler_groq_api(full_prompt) reponse, couleur, fournisseur_nom, modele = appeler_groq_api(full_prompt)
fournisseur_nom, intention = "GROQ (Secours)", intention + " (Secours)" fournisseur_nom, intention = "GROQ (Secours)", intention + " (Secours)"
elif fournisseur == "groq": elif fournisseur == "groq":
reponse, couleur, fournisseur_nom, modele = appeler_groq_api(full_prompt) try:
reponse, couleur, fournisseur_nom, modele = appeler_groq_api(full_prompt)
except Exception as e:
# 🛡️ FALLBACK GROQ -> GEMINI EN CAS DE DÉPASSEMENT DE CONTEXTE OU ERREUR
reponse, modele = interroger_gemini_cascade(full_prompt)
couleur, fournisseur_nom = "#3b82f6", "GOOGLE GEMINI"
intention += " (Secours suite Erreur Groq)"
else: else:
res_local = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "Réponds en français."}, {"role": "user", "content": full_prompt}]) res_local = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "Réponds en français."}, {"role": "user", "content": full_prompt}])
reponse, couleur, fournisseur_nom, modele = res_local.choices[0].message.content, "#10b981", "SERVEUR LOCAL", "qwen2.5:3b" reponse, couleur, fournisseur_nom, modele = res_local.choices[0].message.content, "#10b981", "SERVEUR LOCAL", "qwen2.5:3b"
@@ -61,12 +81,18 @@ async def handle_chat_request(request, session):
if chat_id not in history: if chat_id not in history:
history[chat_id] = {"title": (prompt[:30] if prompt else "Discussion") + "...", "pinned": False, "timestamp": time.time(), "messages": []} history[chat_id] = {"title": (prompt[:30] if prompt else "Discussion") + "...", "pinned": False, "timestamp": time.time(), "messages": []}
display_msg = prompt + (f"\n📎 [Fichiers: {', '.join(file_names)}]" if file_names else "") execution_time = round(time.time() - start_time, 1)
display_msg = prompt
if file_names:
display_msg += f"\n\n📎 **Fichiers joints :** {', '.join(file_names)}"
display_msg += "\n> *🗑️ Note : Fichiers traités de manière éphémère en mémoire vive et détruits instantanément après analyse conformément aux règles de confidentialité.*"
history[chat_id]['messages'].append({"role": "user", "content": display_msg}) history[chat_id]['messages'].append({"role": "user", "content": display_msg})
history[chat_id]['messages'].append({"role": "ai", "content": reponse, "intention": intention, "fournisseur": fournisseur_nom, "modele": modele, "couleur": couleur}) history[chat_id]['messages'].append({"role": "ai", "content": reponse, "intention": intention, "fournisseur": fournisseur_nom, "modele": modele, "couleur": couleur, "execution_time": execution_time})
save_history(history) save_history(history)
return render_user_msg(display_msg), render_ai_msg(reponse, intention, fournisseur_nom, modele, couleur), Script("renderMarkdown();"), render_sidebar() return render_user_msg(display_msg), render_ai_msg(reponse, intention, fournisseur_nom, modele, couleur, execution_time), Script("renderMarkdown();"), render_sidebar()
except Exception as e: except Exception as e:
return Div(f"Erreur : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;") return Div(f"Erreur : {e}", style="color:red; background:#300; padding:15px; border-radius:10px;")
+8 -4
View File
@@ -1,14 +1,18 @@
# modules/paypal.py import os
from fasthtml.common import * from fasthtml.common import *
def render_paypal_donation_widget(): def render_paypal_donation_widget():
"""Génère le bouton de don PayPal pour l'interface sidebar.""" """Génère le bouton de don PayPal pour l'interface sidebar."""
# Remplacez l'URL ci-dessous par votre véritable lien ou bouton de don PayPal
paypal_url = "https://www.paypal.com/donate?hosted_button_id=VOTRE_ID_PAYPAL" # Lecture forcée de la variable d'environnement
paypal_url = os.environ.get("PAYPAL_DONATION_LINK", "")
if not paypal_url:
return Div(id="paypal-widget-placeholder")
return Div( return Div(
A( A(
"☕ Faire un don PayPal", "☕ Faire un don",
href=paypal_url, href=paypal_url,
target="_blank", target="_blank",
cls="export-btn", cls="export-btn",
+79 -44
View File
@@ -48,13 +48,72 @@ def get_theme_hdrs():
.input-field:focus { outline: none; } .input-field:focus { outline: none; }
.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; } .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; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; font-size: 1.1em; flex-shrink: 0; }
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator { display: flex; } /* LOADER HTMX SURÉLEVÉ ET CENTRÉ (Flotte au-dessus du texte) */
.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; } .htmx-indicator { display: none; opacity: 0; transition: opacity 0.3s ease-in; position: absolute; bottom: 140px; left: 50%; transform: translateX(-50%); z-index: 9999; }
.htmx-request.htmx-indicator { display: flex !important; opacity: 1; }
.htmx-request .htmx-indicator { display: flex !important; opacity: 1; }
.tech-loader-container { display: flex; align-items: center; gap: 15px; color: var(--accent-blue); font-style: italic; background: #1e1e24; padding: 15px 25px; border-radius: 30px; border: 1px dashed var(--accent-blue); box-shadow: 0 5px 20px rgba(0,229,255,0.2); font-size: 0.9em; white-space: nowrap; }
.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; } .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); } } @keyframes spin { to { transform: rotate(360deg); } }
@media (max-width: 768px) {
body { flex-direction: column; }
.sidebar { width: 100%; height: auto; max-height: 35vh; border-right: none; border-bottom: 1px solid #333; padding: 10px; }
.sidebar-history { max-height: 120px; }
.main-content { flex: 1; height: 65vh; }
.chat-container { padding-bottom: 180px; padding-left: 10px; padding-right: 10px; }
.user-bubble, .ai-bubble { max-width: 95%; }
.input-area { padding: 10px; }
}
"""), """),
Script(r""" Script(r"""
// Sac à dos virtuel global pour additionner les fichiers sans les écraser
let aethasFiles = new DataTransfer();
function syncFiles() {
const fileInput = document.getElementById('file-upload');
if(fileInput) fileInput.files = aethasFiles.files;
renderFilePills();
}
function renderFilePills() {
let previewList = document.getElementById('file-preview-list');
if(!previewList) return;
previewList.innerHTML = '';
for (let i = 0; i < aethasFiles.files.length; i++) {
let file = aethasFiles.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 removeFile(index) {
let dt = new DataTransfer();
for (let i = 0; i < aethasFiles.files.length; i++) {
if (i !== index) dt.items.add(aethasFiles.files[i]);
}
aethasFiles = dt;
syncFiles();
}
function handleFileSelection(input) {
if (input.files) {
for (let f of input.files) {
let exists = false;
for (let existing of aethasFiles.files) {
if (existing.name === f.name) exists = true;
}
if (!exists && aethasFiles.files.length < 10) {
aethasFiles.items.add(f);
}
}
}
syncFiles();
}
function renderMarkdown() { function renderMarkdown() {
try { try {
document.querySelectorAll('.markdown-raw').forEach(el => { document.querySelectorAll('.markdown-raw').forEach(el => {
@@ -124,47 +183,50 @@ def get_theme_hdrs():
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const ta = document.getElementById('prompt-input'); const ta = document.getElementById('prompt-input');
const dropZone = document.querySelector('.input-container-box'); const dropZone = document.querySelector('.input-container-box');
const fileInput = document.getElementById('file-upload');
if (ta) { if (ta) {
ta.addEventListener('input', () => autoResize(ta)); ta.addEventListener('input', () => autoResize(ta));
ta.addEventListener('paste', (e) => { ta.addEventListener('paste', (e) => {
let items = (e.clipboardData || e.originalEvent.clipboardData).items; 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; let hasFiles = false;
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
if (items[i].kind === 'file') { if (items[i].kind === 'file') {
let blob = items[i].getAsFile(); let blob = items[i].getAsFile();
if (blob) { dt.items.add(blob); hasFiles = true; } if (blob && aethasFiles.files.length < 10) {
aethasFiles.items.add(blob);
hasFiles = true;
}
} }
} }
if (hasFiles) { if (hasFiles) {
e.preventDefault(); e.preventDefault();
fileInput.files = dt.files; syncFiles();
handleFileSelection(fileInput);
} else { } else {
setTimeout(() => autoResize(ta), 20); setTimeout(() => autoResize(ta), 20);
} }
}); });
} }
if (dropZone && fileInput) { if (dropZone) {
['dragenter', 'dragover'].forEach(eName => dropZone.addEventListener(eName, e => { e.preventDefault(); dropZone.style.borderColor = 'var(--accent-blue)'; })); ['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'; })); ['dragleave', 'drop'].forEach(eName => dropZone.addEventListener(eName, e => { e.preventDefault(); dropZone.style.borderColor = '#444'; }));
dropZone.addEventListener('drop', (e) => { dropZone.addEventListener('drop', (e) => {
e.preventDefault(); 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) { for (let f of e.dataTransfer.files) {
if (dt.files.length < 10) dt.items.add(f); if (aethasFiles.files.length < 10) aethasFiles.items.add(f);
} }
fileInput.files = dt.files; syncFiles();
handleFileSelection(fileInput);
}); });
} }
// Vider le sac à dos virtel une fois la requête HTMX envoyée !
document.body.addEventListener('htmx:afterRequest', function(evt) {
if(evt.detail.target.id === 'chat-history') {
aethasFiles = new DataTransfer();
syncFiles();
if(ta) ta.style.height = '36px';
}
});
}); });
function copyCodeBlock(btn) { function copyCodeBlock(btn) {
@@ -198,33 +260,6 @@ def get_theme_hdrs():
} }
} }
function handleFileSelection(input) {
let previewList = document.getElementById('file-preview-list');
previewList.innerHTML = '';
if (input.files) {
if (input.files.length > 10) alert("Maximum 10 fichiers autorisés.");
let count = Math.min(input.files.length, 10);
for (let i = 0; i < count; i++) {
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 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) { function downloadResponsePDF(btn) {
let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered'); let content = btn.closest('.ai-bubble').querySelector('.markdown-rendered');
let filename = getSmartFilename(btn, 'Aethas38_Reponse', 'pdf'); let filename = getSmartFilename(btn, 'Aethas38_Reponse', 'pdf');
+8 -4
View File
@@ -5,11 +5,15 @@ from modules.paypal import render_paypal_donation_widget
def render_user_msg(text): def render_user_msg(text):
return Div(P(text, style="margin:0; font-weight:bold; white-space: pre-wrap;"), cls="user-bubble") 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): # Ajout du paramètre execution_time
def render_ai_msg(text, intention, nom_fournisseur, nom_modele, couleur, execution_time=None):
time_badge = Span(f"⏱️ {execution_time}s", style="margin-right: 10px; color:#aaa; font-size:0.8em;") if execution_time else ""
encart_info = Div( encart_info = Div(
Span(f"🎯 {intention}", style="margin-right: 10px; color:#aaa; font-size:0.8em;"), 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_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;"), Span(f"🧠 {nom_modele}", style=f"margin-right: 10px; color:{couleur}; font-weight:bold; font-size:0.8em;"),
time_badge, # ⏱️ Injection du chronomètre ici
style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;" style="border-bottom: 1px solid #333; padding-bottom: 10px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 5px;"
) )
actions = Div( actions = Div(
@@ -54,14 +58,13 @@ def render_main_page(chat_id):
if msg['role'] == 'user': if msg['role'] == 'user':
messages_ui.append(render_user_msg(msg['content'])) messages_ui.append(render_user_msg(msg['content']))
else: else:
messages_ui.append(render_ai_msg(msg['content'], msg.get('intention', ''), msg.get('fournisseur', ''), msg.get('modele', ''), msg.get('couleur', '#333'))) messages_ui.append(render_ai_msg(msg['content'], msg.get('intention', ''), msg.get('fournisseur', ''), msg.get('modele', ''), msg.get('couleur', '#333'), msg.get('execution_time', '')))
if messages_ui: if messages_ui:
messages_ui.append(Script("setTimeout(renderMarkdown, 150);")) messages_ui.append(Script("setTimeout(renderMarkdown, 150);"))
return Title("Aethas38 - Core"), Body( return Title("Aethas38 - Core"), Body(
Div( Div(
Div( Div(
# L'image est appelée depuis le dossier local "Logo" monté dans FastHTML
Img(src="/Logo/logo_aethas.svg", style="max-width: 80%; height: auto; display: block; margin: 0 auto 20px;"), Img(src="/Logo/logo_aethas.svg", style="max-width: 80%; height: auto; display: block; margin: 0 auto 20px;"),
Div( Div(
A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"), A("+ Nouvelle discussion", href="/new_chat", cls="new-chat-btn"),
@@ -77,7 +80,8 @@ def render_main_page(chat_id):
), ),
Div( Div(
Div(*messages_ui, id="chat-history", cls="chat-container"), Div(*messages_ui, 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;"), # 🔄 Ajout de l'estimation de temps dans le Loader
Div(Div(Div(cls="tech-spinner"), Span("Analyse en cours... (~5s à 10s)"), 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( Div(
Form( Form(
Input(type="hidden", name="chat_id", value=chat_id), Input(type="hidden", name="chat_id", value=chat_id),