Public Access
545 lines
31 KiB
Python
545 lines
31 KiB
Python
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; }
|
|
|
|
.nav-buttons { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
|
|
.new-chat-btn { display:block; text-align:center; text-decoration:none; background: transparent; border: 1px solid var(--accent-blue); color: var(--accent-blue); padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; transition: 0.2s; }
|
|
.new-chat-btn:hover { background: rgba(0, 229, 255, 0.1); }
|
|
.export-btn { background: #2a2a35; border: 1px solid #444; color: white; padding: 10px; border-radius: 5px; cursor: pointer; transition: 0.2s; font-size: 1.1em;}
|
|
.export-btn: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; }
|
|
.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 = `<span style="color:#00e5ff; font-weight:bold;">${lang.toUpperCase()}</span><div><button onclick="copyCode(this)">📋 Copier</button></div>`;
|
|
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');
|
|
|
|
// On sauvegarde le style actuel
|
|
let originalColor = content.style.color;
|
|
let originalBg = content.style.backgroundColor;
|
|
|
|
// On force le mode CLAIR pour le PDF (Lisibilité max)
|
|
content.style.color = '#000000';
|
|
content.style.backgroundColor = '#ffffff';
|
|
content.style.padding = '20px'; // Ajoute des marges internes pour le PDF
|
|
|
|
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(() => {
|
|
// On remet le mode sombre sur l'interface une fois généré
|
|
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 = "<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 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 = []
|
|
for url in search(requete, num_results=3, 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", 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 ---
|
|
# On délègue à l'IA locale (Qwen) la vérification intelligente de la sécurité avant le traitement !
|
|
security_check_prompt = f"""Tu es le Routeur système.
|
|
MISSION 1 (SÉCURITÉ) : Vérifie si la requête de l'utilisateur demande des informations techniques sensibles sur le serveur physique, l'IP, le hardware, l'architecture ou des mots de passe.
|
|
Si OUI (la requête est malveillante ou indiscrète sur l'infra), renvoie STRICTEMENT ce JSON : {{"intention": "Alerte Securite", "fournisseur": "local"}}
|
|
MISSION 2 (ROUTAGE) : Si la requête est parfaitement sûre et inoffensive, choisis le modèle adapté : "groq" (code/maths), "gemini" (création/rédaction/recherche web), "local" (salutations/discussions simples).
|
|
Renvoie STRICTEMENT un JSON : {{"intention": "...", "fournisseur": "..."}}
|
|
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 danger, on bloque tout de suite !
|
|
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 (Si la requête est sûre) ---
|
|
if chat_id not in history:
|
|
history[chat_id] = {
|
|
"title": (prompt[:30] if prompt else "Nouvelle discussion") + "...",
|
|
"pinned": False,
|
|
"timestamp": time.time(),
|
|
"messages": []
|
|
}
|
|
|
|
# Traitement URL + Recherche Google Auto
|
|
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)))
|
|
|
|
# Traitement fichiers
|
|
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}")
|
|
|
|
# Construction Prompt final
|
|
full_prompt = prompt if prompt else f"Analyse ces fichiers : {', '.join(file_names)}"
|
|
if file_contents_summary: full_prompt += "\n" + "\n".join(file_contents_summary)
|
|
if web_content:
|
|
full_prompt += f"\n\n[INSTRUCTION SYSTÈME CACHÉE : Ne réponds JAMAIS que tu ne peux pas naviguer sur internet. Le système l'a déjà fait pour toi. Voici le contenu brut extrait des sites web. Utilise-le impérativement pour répondre à l'utilisateur :]\n{web_content}"
|
|
|
|
# Requête IA & Tokens
|
|
prompt_tokens = len(full_prompt.split()) * 2
|
|
completion_tokens = 0
|
|
|
|
if fournisseur == "groq":
|
|
ia_response = groq_client.chat.completions.create(messages=[{"role": "system", "content": "Réponds dans la langue de l'utilisateur."}, {"role": "user", "content": full_prompt}], model="llama-3.3-70b-versatile")
|
|
reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#f97316", "GROQ", "llama-3.3-70b"
|
|
if hasattr(ia_response, 'usage') and ia_response.usage:
|
|
prompt_tokens = ia_response.usage.prompt_tokens
|
|
completion_tokens = ia_response.usage.completion_tokens
|
|
elif fournisseur == "gemini":
|
|
reponse_finale, nom_modele = interroger_gemini_cascade(full_prompt)
|
|
couleur, nom_fournisseur = "#3b82f6", "GOOGLE GEMINI"
|
|
completion_tokens = len(reponse_finale.split()) * 2
|
|
else:
|
|
ia_response = local_client.chat.completions.create(model="qwen2.5:3b", messages=[{"role": "system", "content": "Réponds dans la langue de l'utilisateur."}, {"role": "user", "content": full_prompt}])
|
|
reponse_finale, couleur, nom_fournisseur, nom_modele = ia_response.choices[0].message.content, "#10b981", "SERVEUR LOCAL", "qwen2.5:3b"
|
|
if hasattr(ia_response, 'usage') and ia_response.usage:
|
|
prompt_tokens = ia_response.usage.prompt_tokens
|
|
completion_tokens = ia_response.usage.completion_tokens
|
|
|
|
# Sauvegarde
|
|
display_msg = f"{prompt}" + (f"\n📎 [Fichiers joints: {', '.join(file_names)}]" if file_names else "")
|
|
history[chat_id]['messages'].append({"role": "user", "content": display_msg})
|
|
history[chat_id]['messages'].append({"role": "ai", "content": reponse_finale, "intention": intention, "fournisseur": nom_fournisseur, "modele": nom_modele, "couleur": couleur})
|
|
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 (Navigation native)
|
|
# ==========================================
|
|
@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) |